mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-29 21:33:54 +00:00
feat: integration with agents
This commit is contained in:
parent
cf023340a6
commit
b9ced47bf8
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -35,6 +35,14 @@ class SyncedTool(BaseModel):
|
||||
authorisation_type: Optional[str] = None
|
||||
|
||||
auth_via: str = "tf_proxy"
|
||||
|
||||
# Coder agent specific fields for prompting/model configuration
|
||||
interpolation_string: Optional[str] = None
|
||||
goals: Optional[str] = None
|
||||
temperature: Optional[float] = None
|
||||
max_tokens: Optional[int] = None
|
||||
llm_base_model: Optional[str] = None
|
||||
llm_provider: Optional[str] = None
|
||||
|
||||
|
||||
class ToolSyncResult(BaseModel):
|
||||
@ -121,7 +129,7 @@ def parse_agent(agent) -> SyncedTool:
|
||||
agent: Agent from TF SDK
|
||||
|
||||
Returns:
|
||||
SyncedTool instance
|
||||
SyncedTool instance with full agent configuration
|
||||
"""
|
||||
return SyncedTool(
|
||||
id=agent.id,
|
||||
@ -130,6 +138,14 @@ def parse_agent(agent) -> SyncedTool:
|
||||
tool_type=ToolType.CODER_AGENT,
|
||||
is_agent_skill=False,
|
||||
auth_via="tf_agent",
|
||||
# Agent prompting configuration
|
||||
interpolation_string=getattr(agent, 'interpolation_string', None),
|
||||
goals=getattr(agent, 'goals', None),
|
||||
# Agent model configuration
|
||||
temperature=getattr(agent, 'temperature', None),
|
||||
max_tokens=getattr(agent, 'max_tokens', None),
|
||||
llm_base_model=getattr(agent, 'llm_base_model', None),
|
||||
llm_provider=getattr(agent, 'llm_provider', None),
|
||||
)
|
||||
|
||||
|
||||
|
||||
310
packages/tf-sync/test_tools.py
Normal file
310
packages/tf-sync/test_tools.py
Normal file
@ -0,0 +1,310 @@
|
||||
"""
|
||||
End-to-end tests for ToothFairyAI agent sync and prompt injection.
|
||||
Tests the entire flow from Python sync to TypeScript agent loading to prompt building.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# Test 1: Verify parse_agent extracts all required fields
|
||||
def test_parse_agent_extracts_all_fields():
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 1: Verify parse_agent extracts all required fields")
|
||||
print("=" * 80)
|
||||
|
||||
from tf_sync.tools import parse_agent, SyncedTool, ToolType
|
||||
|
||||
# Create a mock agent with all the fields we need
|
||||
class MockAgent:
|
||||
id = "test-agent-123"
|
||||
label = "Code Reviewer"
|
||||
description = "Reviews code for quality and best practices"
|
||||
interpolation_string = "You are a code reviewer. Always check for bugs and suggest improvements."
|
||||
goals = "Review code thoroughly. Provide actionable feedback. Ensure code quality."
|
||||
temperature = 0.3
|
||||
max_tokens = 4096
|
||||
llm_base_model = "claude-3-5-sonnet"
|
||||
llm_provider = "toothfairyai"
|
||||
mode = "coder"
|
||||
|
||||
agent = MockAgent()
|
||||
result = parse_agent(agent)
|
||||
|
||||
print(f"\nParsed tool:")
|
||||
print(f" id: {result.id}")
|
||||
print(f" name: {result.name}")
|
||||
print(f" description: {result.description}")
|
||||
print(f" tool_type: {result.tool_type}")
|
||||
print(f" interpolation_string: {result.interpolation_string}")
|
||||
print(f" goals: {result.goals}")
|
||||
print(f" temperature: {result.temperature}")
|
||||
print(f" max_tokens: {result.max_tokens}")
|
||||
print(f" llm_base_model: {result.llm_base_model}")
|
||||
print(f" llm_provider: {result.llm_provider}")
|
||||
|
||||
# Verify all fields are populated
|
||||
assert result.id == "test-agent-123", f"Expected id='test-agent-123', got '{result.id}'"
|
||||
assert result.name == "Code Reviewer", f"Expected name='Code Reviewer', got '{result.name}'"
|
||||
assert result.tool_type == ToolType.CODER_AGENT, f"Expected CODER_AGENT, got {result.tool_type}"
|
||||
assert result.interpolation_string == "You are a code reviewer. Always check for bugs and suggest improvements.", \
|
||||
f"interpolation_string not set correctly"
|
||||
assert result.goals == "Review code thoroughly. Provide actionable feedback. Ensure code quality.", \
|
||||
f"goals not set correctly"
|
||||
assert result.temperature == 0.3, f"Expected temperature=0.3, got {result.temperature}"
|
||||
assert result.max_tokens == 4096, f"Expected max_tokens=4096, got {result.max_tokens}"
|
||||
assert result.llm_base_model == "claude-3-5-sonnet", f"llm_base_model not set correctly"
|
||||
assert result.llm_provider == "toothfairyai", f"llm_provider not set correctly"
|
||||
|
||||
print("\n✅ TEST 1 PASSED: parse_agent extracts all fields correctly")
|
||||
return True
|
||||
|
||||
|
||||
# Test 2: Verify model mapping for different llm_provider values
|
||||
def test_model_mapping_for_tf_providers():
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 2: Verify model mapping for different llm_provider values")
|
||||
print("=" * 80)
|
||||
|
||||
from tf_sync.tools import parse_agent, ToolType
|
||||
|
||||
# Test with None provider (should map to toothfairyai in TypeScript)
|
||||
class MockAgentNone:
|
||||
id = "test-agent-none"
|
||||
label = "Agent None"
|
||||
description = "Test"
|
||||
interpolation_string = "Test prompt"
|
||||
goals = "Test goals"
|
||||
temperature = 0.7
|
||||
max_tokens = 2048
|
||||
llm_base_model = "gpt-4"
|
||||
llm_provider = None
|
||||
mode = "coder"
|
||||
|
||||
# Test with "toothfairyai" provider
|
||||
class MockAgentTF:
|
||||
id = "test-agent-tf"
|
||||
label = "Agent TF"
|
||||
description = "Test"
|
||||
interpolation_string = "Test prompt"
|
||||
goals = "Test goals"
|
||||
temperature = 0.7
|
||||
max_tokens = 2048
|
||||
llm_base_model = "gpt-4"
|
||||
llm_provider = "toothfairyai"
|
||||
mode = "coder"
|
||||
|
||||
# Test with "tf" provider
|
||||
class MockAgentTFShort:
|
||||
id = "test-agent-tf-short"
|
||||
label = "Agent TF Short"
|
||||
description = "Test"
|
||||
interpolation_string = "Test prompt"
|
||||
goals = "Test goals"
|
||||
temperature = 0.7
|
||||
max_tokens = 2048
|
||||
llm_base_model = "gpt-4"
|
||||
llm_provider = "tf"
|
||||
mode = "coder"
|
||||
|
||||
# Test with external provider (should NOT map to toothfairyai)
|
||||
class MockAgentExternal:
|
||||
id = "test-agent-external"
|
||||
label = "Agent External"
|
||||
description = "Test"
|
||||
interpolation_string = "Test prompt"
|
||||
goals = "Test goals"
|
||||
temperature = 0.7
|
||||
max_tokens = 2048
|
||||
llm_base_model = "claude-3-5-sonnet"
|
||||
llm_provider = "anthropic"
|
||||
mode = "coder"
|
||||
|
||||
results = {
|
||||
"None provider": parse_agent(MockAgentNone()),
|
||||
"toothfairyai provider": parse_agent(MockAgentTF()),
|
||||
"tf provider": parse_agent(MockAgentTFShort()),
|
||||
"anthropic provider": parse_agent(MockAgentExternal()),
|
||||
}
|
||||
|
||||
for name, result in results.items():
|
||||
print(f"\n{name}:")
|
||||
print(f" llm_provider: {result.llm_provider}")
|
||||
print(f" llm_base_model: {result.llm_base_model}")
|
||||
|
||||
# The TypeScript code will check if llm_provider is None, "toothfairyai", or "tf"
|
||||
# and map to toothfairyai provider. Here we just verify the values are preserved.
|
||||
assert results["None provider"].llm_provider is None
|
||||
assert results["toothfairyai provider"].llm_provider == "toothfairyai"
|
||||
assert results["tf provider"].llm_provider == "tf"
|
||||
assert results["anthropic provider"].llm_provider == "anthropic"
|
||||
|
||||
print("\n✅ TEST 2 PASSED: Provider values are preserved correctly for TypeScript mapping")
|
||||
return True
|
||||
|
||||
|
||||
# Test 3: Verify SyncedTool serializes correctly to JSON
|
||||
def test_synced_tool_json_serialization():
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 3: Verify SyncedTool serializes correctly to JSON")
|
||||
print("=" * 80)
|
||||
|
||||
from tf_sync.tools import parse_agent
|
||||
|
||||
class MockAgent:
|
||||
id = "test-agent-json"
|
||||
label = "JSON Test Agent"
|
||||
description = "Test JSON serialization"
|
||||
interpolation_string = "You are a JSON test agent."
|
||||
goals = "Test JSON output."
|
||||
temperature = 0.5
|
||||
max_tokens = 8192
|
||||
llm_base_model = "gpt-4-turbo"
|
||||
llm_provider = "toothfairyai"
|
||||
mode = "coder"
|
||||
|
||||
result = parse_agent(MockAgent())
|
||||
|
||||
# Simulate what tfcode.js does
|
||||
tool_data = {
|
||||
"id": result.id,
|
||||
"name": result.name,
|
||||
"description": result.description,
|
||||
"tool_type": result.tool_type.value,
|
||||
"request_type": result.request_type.value if result.request_type else None,
|
||||
"url": result.url,
|
||||
"auth_via": result.auth_via,
|
||||
"interpolation_string": result.interpolation_string,
|
||||
"goals": result.goals,
|
||||
"temperature": result.temperature,
|
||||
"max_tokens": result.max_tokens,
|
||||
"llm_base_model": result.llm_base_model,
|
||||
"llm_provider": result.llm_provider,
|
||||
}
|
||||
|
||||
print(f"\nSerialized JSON:")
|
||||
print(json.dumps(tool_data, indent=2))
|
||||
|
||||
# Verify all fields are present in JSON
|
||||
assert tool_data["id"] == "test-agent-json"
|
||||
assert tool_data["name"] == "JSON Test Agent"
|
||||
assert tool_data["tool_type"] == "coder_agent"
|
||||
assert tool_data["interpolation_string"] == "You are a JSON test agent."
|
||||
assert tool_data["goals"] == "Test JSON output."
|
||||
assert tool_data["temperature"] == 0.5
|
||||
assert tool_data["max_tokens"] == 8192
|
||||
assert tool_data["llm_base_model"] == "gpt-4-turbo"
|
||||
assert tool_data["llm_provider"] == "toothfairyai"
|
||||
|
||||
print("\n✅ TEST 3 PASSED: SyncedTool serializes correctly to JSON")
|
||||
return True
|
||||
|
||||
|
||||
# Test 4: Create a mock tools.json and verify TypeScript can parse it
|
||||
def test_tools_json_format():
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST 4: Verify tools.json format matches TypeScript expectations")
|
||||
print("=" * 80)
|
||||
|
||||
# Create a mock tools.json content
|
||||
mock_tools = {
|
||||
"success": True,
|
||||
"tools": [
|
||||
{
|
||||
"id": "coder-agent-1",
|
||||
"name": "Code Reviewer",
|
||||
"description": "Reviews code for quality and best practices",
|
||||
"tool_type": "coder_agent",
|
||||
"request_type": None,
|
||||
"url": None,
|
||||
"auth_via": "tf_agent",
|
||||
"interpolation_string": "You are a code reviewer. Your job is to review code thoroughly and provide actionable feedback.",
|
||||
"goals": "Review all code changes. Identify bugs. Suggest improvements. Ensure best practices.",
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 4096,
|
||||
"llm_base_model": "claude-3-5-sonnet",
|
||||
"llm_provider": "toothfairyai",
|
||||
},
|
||||
{
|
||||
"id": "coder-agent-2",
|
||||
"name": "Test Writer",
|
||||
"description": "Writes comprehensive tests",
|
||||
"tool_type": "coder_agent",
|
||||
"request_type": None,
|
||||
"url": None,
|
||||
"auth_via": "tf_agent",
|
||||
"interpolation_string": "You are a test writer. Write comprehensive tests for all code.",
|
||||
"goals": "Write unit tests. Write integration tests. Ensure code coverage.",
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 8192,
|
||||
"llm_base_model": None,
|
||||
"llm_provider": None, # Should map to toothfairyai in TypeScript
|
||||
},
|
||||
],
|
||||
"by_type": {
|
||||
"coder_agent": 2,
|
||||
},
|
||||
}
|
||||
|
||||
print(f"\nMock tools.json content:")
|
||||
print(json.dumps(mock_tools, indent=2))
|
||||
|
||||
# Verify the structure matches what TypeScript expects
|
||||
assert mock_tools["success"] == True
|
||||
assert len(mock_tools["tools"]) == 2
|
||||
|
||||
for tool in mock_tools["tools"]:
|
||||
assert "id" in tool
|
||||
assert "name" in tool
|
||||
assert "tool_type" in tool
|
||||
assert "interpolation_string" in tool
|
||||
assert "goals" in tool
|
||||
assert "temperature" in tool
|
||||
assert "max_tokens" in tool
|
||||
assert "llm_base_model" in tool
|
||||
assert "llm_provider" in tool
|
||||
assert tool["tool_type"] == "coder_agent"
|
||||
|
||||
print("\n✅ TEST 4 PASSED: tools.json format matches TypeScript expectations")
|
||||
return True
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all tests in sequence."""
|
||||
print("\n" + "=" * 80)
|
||||
print("RUNNING ALL PYTHON TESTS")
|
||||
print("=" * 80)
|
||||
|
||||
tests = [
|
||||
test_parse_agent_extracts_all_fields,
|
||||
test_model_mapping_for_tf_providers,
|
||||
test_synced_tool_json_serialization,
|
||||
test_tools_json_format,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
if test():
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"\n❌ TEST FAILED: {test.__name__}")
|
||||
print(f" Error: {e}")
|
||||
failed += 1
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print(f"PYTHON TEST RESULTS: {passed} passed, {failed} failed")
|
||||
print("=" * 80)
|
||||
|
||||
return failed == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
@ -1,63 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import * as readline from 'readline';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { spawn } from "child_process"
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs"
|
||||
import { join, dirname } from "path"
|
||||
import { homedir } from "os"
|
||||
import * as readline from "readline"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const TFCODE_DIR = join(homedir(), '.tfcode');
|
||||
const TOOLS_FILE = join(TFCODE_DIR, 'tools.json');
|
||||
const CREDENTIALS_FILE = join(TFCODE_DIR, 'credentials.json');
|
||||
const CONFIG_FILE = join(TFCODE_DIR, 'config.json');
|
||||
const TFCODE_DIR = join(homedir(), ".tfcode")
|
||||
const TOOLS_FILE = join(TFCODE_DIR, "tools.json")
|
||||
const CREDENTIALS_FILE = join(TFCODE_DIR, "credentials.json")
|
||||
const CONFIG_FILE = join(TFCODE_DIR, "config.json")
|
||||
|
||||
const COLORS = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
green: '\x1b[32m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m',
|
||||
dim: '\x1b[90m',
|
||||
yellow: '\x1b[33m',
|
||||
magenta: '\x1b[35m'
|
||||
};
|
||||
reset: "\x1b[0m",
|
||||
bold: "\x1b[1m",
|
||||
green: "\x1b[32m",
|
||||
red: "\x1b[31m",
|
||||
cyan: "\x1b[36m",
|
||||
dim: "\x1b[90m",
|
||||
yellow: "\x1b[33m",
|
||||
magenta: "\x1b[35m",
|
||||
}
|
||||
|
||||
function log(msg) { console.log(msg); }
|
||||
function success(msg) { console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`); }
|
||||
function error(msg) { console.error(`${COLORS.red}✗${COLORS.reset} ${msg}`); }
|
||||
function info(msg) { console.log(`${COLORS.cyan}ℹ${COLORS.reset} ${msg}`); }
|
||||
function log(msg) {
|
||||
console.log(msg)
|
||||
}
|
||||
function success(msg) {
|
||||
console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`)
|
||||
}
|
||||
function error(msg) {
|
||||
console.error(`${COLORS.red}✗${COLORS.reset} ${msg}`)
|
||||
}
|
||||
function info(msg) {
|
||||
console.log(`${COLORS.cyan}ℹ${COLORS.reset} ${msg}`)
|
||||
}
|
||||
|
||||
function ensureConfigDir() {
|
||||
if (!existsSync(TFCODE_DIR)) mkdirSync(TFCODE_DIR, { recursive: true });
|
||||
if (!existsSync(TFCODE_DIR)) mkdirSync(TFCODE_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
const envConfig = {
|
||||
workspace_id: process.env.TF_WORKSPACE_ID,
|
||||
api_key: process.env.TF_API_KEY,
|
||||
region: process.env.TF_REGION
|
||||
};
|
||||
if (envConfig.workspace_id && envConfig.api_key) return envConfig;
|
||||
if (existsSync(CONFIG_FILE)) {
|
||||
try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
|
||||
region: process.env.TF_REGION,
|
||||
}
|
||||
return null;
|
||||
if (envConfig.workspace_id && envConfig.api_key) return envConfig
|
||||
if (existsSync(CONFIG_FILE)) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
|
||||
} catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function saveConfig(config) {
|
||||
ensureConfigDir();
|
||||
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||
ensureConfigDir()
|
||||
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
||||
}
|
||||
|
||||
function runPythonSync(method, config = null) {
|
||||
const wsId = config?.workspace_id || process.env.TF_WORKSPACE_ID || '';
|
||||
const apiKey = config?.api_key || process.env.TF_API_KEY || '';
|
||||
const region = config?.region || process.env.TF_REGION || 'au';
|
||||
|
||||
const wsId = config?.workspace_id || process.env.TF_WORKSPACE_ID || ""
|
||||
const apiKey = config?.api_key || process.env.TF_API_KEY || ""
|
||||
const region = config?.region || process.env.TF_REGION || "au"
|
||||
|
||||
const pythonCode = `
|
||||
import json, sys, os
|
||||
try:
|
||||
@ -77,222 +87,430 @@ try:
|
||||
elif method == "sync":
|
||||
config = load_config()
|
||||
result = sync_tools(config)
|
||||
tools_data = [{"id": t.id, "name": t.name, "description": t.description, "tool_type": t.tool_type.value, "request_type": t.request_type.value if t.request_type else None, "url": t.url, "auth_via": t.auth_via} for t in result.tools]
|
||||
tools_data = [{"id": t.id, "name": t.name, "description": t.description, "tool_type": t.tool_type.value, "request_type": t.request_type.value if t.request_type else None, "url": t.url, "auth_via": t.auth_via, "interpolation_string": t.interpolation_string, "goals": t.goals, "temperature": t.temperature, "max_tokens": t.max_tokens, "llm_base_model": t.llm_base_model, "llm_provider": t.llm_provider} for t in result.tools]
|
||||
print(json.dumps({"success": result.success, "tools": tools_data, "by_type": result.by_type, "error": result.error}))
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": str(e)}))
|
||||
`;
|
||||
`
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(process.env.TFCODE_PYTHON_PATH || 'python3', ['-c', pythonCode], { env: { ...process.env } });
|
||||
let stdout = '', stderr = '';
|
||||
proc.stdout.on('data', (d) => stdout += d);
|
||||
proc.stderr.on('data', (d) => stderr += d);
|
||||
proc.on('close', (code) => {
|
||||
if (code !== 0 && !stdout) reject(new Error(`Python failed: ${stderr}`));
|
||||
else try { resolve(JSON.parse(stdout.trim())); } catch (e) { reject(new Error(`Parse error: ${stdout}`)); }
|
||||
});
|
||||
proc.on('error', reject);
|
||||
});
|
||||
const proc = spawn(process.env.TFCODE_PYTHON_PATH || "python3", ["-c", pythonCode], { env: { ...process.env } })
|
||||
let stdout = "",
|
||||
stderr = ""
|
||||
proc.stdout.on("data", (d) => (stdout += d))
|
||||
proc.stderr.on("data", (d) => (stderr += d))
|
||||
proc.on("close", (code) => {
|
||||
if (code !== 0 && !stdout) reject(new Error(`Python failed: ${stderr}`))
|
||||
else
|
||||
try {
|
||||
resolve(JSON.parse(stdout.trim()))
|
||||
} catch (e) {
|
||||
reject(new Error(`Parse error: ${stdout}`))
|
||||
}
|
||||
})
|
||||
proc.on("error", reject)
|
||||
})
|
||||
}
|
||||
|
||||
function loadCachedTools() {
|
||||
if (!existsSync(TOOLS_FILE)) return null;
|
||||
try { return JSON.parse(readFileSync(TOOLS_FILE, 'utf-8')); } catch { return null; }
|
||||
if (!existsSync(TOOLS_FILE)) return null
|
||||
try {
|
||||
return JSON.parse(readFileSync(TOOLS_FILE, "utf-8"))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function saveToolsCache(tools) {
|
||||
ensureConfigDir();
|
||||
writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2));
|
||||
ensureConfigDir()
|
||||
writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2))
|
||||
}
|
||||
|
||||
async function question(prompt) {
|
||||
return new Promise((resolve) => {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim()); });
|
||||
});
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||
rl.question(prompt, (answer) => {
|
||||
rl.close()
|
||||
resolve(answer.trim())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function select(prompt, options) {
|
||||
log('');
|
||||
log(prompt);
|
||||
log('');
|
||||
options.forEach((opt, i) => log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`));
|
||||
log('');
|
||||
const answer = await question('Select (1-' + options.length + '): ');
|
||||
const idx = parseInt(answer) - 1;
|
||||
return idx >= 0 && idx < options.length ? idx : 0;
|
||||
log("")
|
||||
log(prompt)
|
||||
log("")
|
||||
options.forEach((opt, i) => log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`))
|
||||
log("")
|
||||
const answer = await question("Select (1-" + options.length + "): ")
|
||||
const idx = parseInt(answer) - 1
|
||||
return idx >= 0 && idx < options.length ? idx : 0
|
||||
}
|
||||
|
||||
async function interactiveSetup() {
|
||||
log('');
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
||||
log(`${COLORS.bold}${COLORS.magenta} tfcode Setup${COLORS.reset}`);
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
||||
log('');
|
||||
log('This will guide you through setting up your ToothFairyAI credentials.');
|
||||
log('');
|
||||
log(`${COLORS.dim}You can find your credentials at:${COLORS.reset}`);
|
||||
log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`);
|
||||
log('');
|
||||
|
||||
log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`);
|
||||
log(`${COLORS.dim}This is your workspace UUID${COLORS.reset}`);
|
||||
log('');
|
||||
const workspaceId = await question('Enter your Workspace ID: ');
|
||||
if (!workspaceId) { error('Workspace ID is required'); process.exit(1); }
|
||||
log('');
|
||||
|
||||
log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`);
|
||||
log(`${COLORS.dim}Paste or type your API key${COLORS.reset}`);
|
||||
log('');
|
||||
const apiKey = await question('Enter your API Key: ');
|
||||
if (!apiKey) { error('API Key is required'); process.exit(1); }
|
||||
log('');
|
||||
|
||||
log(`${COLORS.bold}Step 3: Region${COLORS.reset}`);
|
||||
const regions = ['dev (Development)', 'au (Australia)', 'eu (Europe)', 'us (United States)'];
|
||||
const regionIdx = await select('Select your region:', regions);
|
||||
const region = ['dev', 'au', 'eu', 'us'][regionIdx];
|
||||
|
||||
log('');
|
||||
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
||||
log('');
|
||||
log(`${COLORS.bold}Summary:${COLORS.reset}`);
|
||||
log(` Workspace ID: ${workspaceId}`);
|
||||
log(` API Key: ***${apiKey.slice(-4)}`);
|
||||
log(` Region: ${region}`);
|
||||
log('');
|
||||
|
||||
const confirm = await question('Save these credentials? (Y/n): ');
|
||||
if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') { log('Setup cancelled.'); return; }
|
||||
|
||||
const config = { workspace_id: workspaceId, api_key: apiKey, region };
|
||||
saveConfig(config);
|
||||
success('Credentials saved to ~/.tfcode/config.json');
|
||||
log('');
|
||||
|
||||
const testNow = await question('Validate credentials now? (Y/n): ');
|
||||
if (testNow.toLowerCase() === 'n' || testNow.toLowerCase() === 'no') return;
|
||||
|
||||
log('');
|
||||
info('Validating credentials...');
|
||||
log('');
|
||||
|
||||
log("")
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.magenta} tfcode Setup${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log("")
|
||||
log("This will guide you through setting up your ToothFairyAI credentials.")
|
||||
log("")
|
||||
log(`${COLORS.dim}You can find your credentials at:${COLORS.reset}`)
|
||||
log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`)
|
||||
log("")
|
||||
|
||||
log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`)
|
||||
log(`${COLORS.dim}This is your workspace UUID${COLORS.reset}`)
|
||||
log("")
|
||||
const workspaceId = await question("Enter your Workspace ID: ")
|
||||
if (!workspaceId) {
|
||||
error("Workspace ID is required")
|
||||
process.exit(1)
|
||||
}
|
||||
log("")
|
||||
|
||||
log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`)
|
||||
log(`${COLORS.dim}Paste or type your API key${COLORS.reset}`)
|
||||
log("")
|
||||
const apiKey = await question("Enter your API Key: ")
|
||||
if (!apiKey) {
|
||||
error("API Key is required")
|
||||
process.exit(1)
|
||||
}
|
||||
log("")
|
||||
|
||||
log(`${COLORS.bold}Step 3: Region${COLORS.reset}`)
|
||||
const regions = ["dev (Development)", "au (Australia)", "eu (Europe)", "us (United States)"]
|
||||
const regionIdx = await select("Select your region:", regions)
|
||||
const region = ["dev", "au", "eu", "us"][regionIdx]
|
||||
|
||||
log("")
|
||||
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log("")
|
||||
log(`${COLORS.bold}Summary:${COLORS.reset}`)
|
||||
log(` Workspace ID: ${workspaceId}`)
|
||||
log(` API Key: ***${apiKey.slice(-4)}`)
|
||||
log(` Region: ${region}`)
|
||||
log("")
|
||||
|
||||
const confirm = await question("Save these credentials? (Y/n): ")
|
||||
if (confirm.toLowerCase() === "n" || confirm.toLowerCase() === "no") {
|
||||
log("Setup cancelled.")
|
||||
return
|
||||
}
|
||||
|
||||
const config = { workspace_id: workspaceId, api_key: apiKey, region }
|
||||
saveConfig(config)
|
||||
success("Credentials saved to ~/.tfcode/config.json")
|
||||
log("")
|
||||
|
||||
const testNow = await question("Validate credentials now? (Y/n): ")
|
||||
if (testNow.toLowerCase() === "n" || testNow.toLowerCase() === "no") return
|
||||
|
||||
log("")
|
||||
info("Validating credentials...")
|
||||
log("")
|
||||
|
||||
try {
|
||||
const result = await runPythonSync('validate', config);
|
||||
const result = await runPythonSync("validate", config)
|
||||
if (result.success) {
|
||||
success('Credentials valid!');
|
||||
log(` API URL: ${result.base_url}`);
|
||||
log(` Workspace ID: ${result.workspace_id}`);
|
||||
log('');
|
||||
|
||||
const syncNow = await question('Sync tools now? (Y/n): ');
|
||||
if (syncNow.toLowerCase() === 'n' || syncNow.toLowerCase() === 'no') return;
|
||||
|
||||
log('');
|
||||
info('Syncing tools...');
|
||||
log('');
|
||||
|
||||
const syncResult = await runPythonSync('sync', config);
|
||||
success("Credentials valid!")
|
||||
log(` API URL: ${result.base_url}`)
|
||||
log(` Workspace ID: ${result.workspace_id}`)
|
||||
log("")
|
||||
|
||||
const syncNow = await question("Sync tools now? (Y/n): ")
|
||||
if (syncNow.toLowerCase() === "n" || syncNow.toLowerCase() === "no") return
|
||||
|
||||
log("")
|
||||
info("Syncing tools...")
|
||||
log("")
|
||||
|
||||
const syncResult = await runPythonSync("sync", config)
|
||||
if (syncResult.success) {
|
||||
saveToolsCache(syncResult);
|
||||
success(`Synced ${syncResult.tools.length} tools`);
|
||||
saveToolsCache(syncResult)
|
||||
success(`Synced ${syncResult.tools.length} tools`)
|
||||
if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) {
|
||||
log('');
|
||||
log('By type:');
|
||||
for (const [type, count] of Object.entries(syncResult.by_type)) log(` ${type}: ${count}`);
|
||||
log("")
|
||||
log("By type:")
|
||||
for (const [type, count] of Object.entries(syncResult.by_type)) log(` ${type}: ${count}`)
|
||||
}
|
||||
} else {
|
||||
error(`Sync failed: ${syncResult.error}`);
|
||||
error(`Sync failed: ${syncResult.error}`)
|
||||
}
|
||||
} else {
|
||||
error(`Validation failed: ${result.error}`);
|
||||
error(`Validation failed: ${result.error}`)
|
||||
}
|
||||
} catch (e) {
|
||||
error(`Failed: ${e.message}`);
|
||||
error(`Failed: ${e.message}`)
|
||||
}
|
||||
|
||||
log('');
|
||||
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
||||
log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`);
|
||||
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
||||
log('');
|
||||
log('Commands:');
|
||||
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Check credentials`);
|
||||
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools`);
|
||||
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List tools`);
|
||||
log('');
|
||||
|
||||
log("")
|
||||
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log("")
|
||||
log("Commands:")
|
||||
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Check credentials`)
|
||||
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools`)
|
||||
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List tools`)
|
||||
log("")
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
log('');
|
||||
log(`${COLORS.bold}tfcode${COLORS.reset} - ToothFairyAI's AI coding agent`);
|
||||
log('');
|
||||
log('Commands:');
|
||||
log(` ${COLORS.cyan}tfcode setup${COLORS.reset} Interactive credential setup`);
|
||||
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Test credentials`);
|
||||
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools from workspace`);
|
||||
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List synced tools`);
|
||||
log(` ${COLORS.cyan}tfcode --help${COLORS.reset} Show this help`);
|
||||
log(` ${COLORS.cyan}tfcode --version${COLORS.reset} Show version`);
|
||||
log('');
|
||||
log(`${COLORS.dim}For full TUI, run from source:${COLORS.reset}`);
|
||||
log(`${COLORS.dim} bun run packages/tfcode/src/index.ts${COLORS.reset}`);
|
||||
log('');
|
||||
log("")
|
||||
log(`${COLORS.bold}tfcode${COLORS.reset} - ToothFairyAI's AI coding agent`)
|
||||
log("")
|
||||
log("Commands:")
|
||||
log(` ${COLORS.cyan}tfcode setup${COLORS.reset} Interactive credential setup`)
|
||||
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Test credentials`)
|
||||
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools from workspace`)
|
||||
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List synced tools`)
|
||||
log(` ${COLORS.cyan}tfcode test-agent <id>${COLORS.reset} Test agent prompt injection`)
|
||||
log(` ${COLORS.cyan}tfcode debug${COLORS.reset} Show debug info`)
|
||||
log(` ${COLORS.cyan}tfcode --help${COLORS.reset} Show this help`)
|
||||
log(` ${COLORS.cyan}tfcode --version${COLORS.reset} Show version`)
|
||||
log("")
|
||||
log(`${COLORS.dim}For full TUI, run from source:${COLORS.reset}`)
|
||||
log(`${COLORS.dim} bun run packages/tfcode/src/index.ts${COLORS.reset}`)
|
||||
log("")
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
showHelp();
|
||||
} else if (args.includes('--version') || args.includes('-v')) {
|
||||
log('tfcode v1.0.0-beta.9');
|
||||
} else if (command === 'setup') {
|
||||
interactiveSetup();
|
||||
} else if (command === 'validate') {
|
||||
(async () => {
|
||||
const config = loadConfig();
|
||||
if (!config) { error('No credentials. Run: tfcode setup'); process.exit(1); }
|
||||
info('Validating...');
|
||||
try {
|
||||
const result = await runPythonSync('validate', config);
|
||||
if (result.success) { success('Credentials valid'); log(` API URL: ${result.base_url}`); }
|
||||
else { error(`Failed: ${result.error}`); process.exit(1); }
|
||||
} catch (e) { error(`Failed: ${e.message}`); process.exit(1); }
|
||||
})();
|
||||
} else if (command === 'sync') {
|
||||
(async () => {
|
||||
const config = loadConfig();
|
||||
if (!config) { error('No credentials. Run: tfcode setup'); process.exit(1); }
|
||||
info('Syncing tools...');
|
||||
try {
|
||||
const result = await runPythonSync('sync', config);
|
||||
if (result.success) {
|
||||
saveToolsCache(result);
|
||||
success(`Synced ${result.tools.length} tools`);
|
||||
if (result.by_type) { log(''); log('By type:'); for (const [t, c] of Object.entries(result.by_type)) log(` ${t}: ${c}`); }
|
||||
} else { error(`Failed: ${result.error}`); process.exit(1); }
|
||||
} catch (e) { error(`Failed: ${e.message}`); process.exit(1); }
|
||||
})();
|
||||
} else if (command === 'tools' && args[1] === 'list') {
|
||||
const cached = loadCachedTools();
|
||||
if (!cached?.success) { error('No tools. Run: tfcode sync'); process.exit(1); }
|
||||
let tools = cached.tools;
|
||||
if (args[3] === '--type' && args[4]) tools = tools.filter(t => t.tool_type === args[4]);
|
||||
log(`\n${tools.length} tool(s):\n`);
|
||||
for (const t of tools) {
|
||||
log(` ${COLORS.cyan}${t.name}${COLORS.reset}`);
|
||||
log(` Type: ${t.tool_type}`);
|
||||
if (t.description) log(` ${COLORS.dim}${t.description.slice(0, 60)}${COLORS.reset}`);
|
||||
log(` Auth: ${t.auth_via}\n`);
|
||||
function showDebugInfo() {
|
||||
log("")
|
||||
log(`${COLORS.bold}Debug Information${COLORS.reset}`)
|
||||
log("")
|
||||
log(` ${COLORS.bold}TFCODE_DIR:${COLORS.reset} ${TFCODE_DIR}`)
|
||||
log(` ${COLORS.bold}TOOLS_FILE:${COLORS.reset} ${TOOLS_FILE}`)
|
||||
log(` ${COLORS.bold}CONFIG_FILE:${COLORS.reset} ${CONFIG_FILE}`)
|
||||
log(` ${COLORS.bold}CREDENTIALS_FILE:${COLORS.reset} ${CREDENTIALS_FILE}`)
|
||||
log("")
|
||||
log(` ${COLORS.bold}tools.json exists:${COLORS.reset} ${existsSync(TOOLS_FILE)}`)
|
||||
if (existsSync(TOOLS_FILE)) {
|
||||
const tools = loadCachedTools()
|
||||
log(` ${COLORS.bold}tools.json valid:${COLORS.reset} ${tools?.success ?? false}`)
|
||||
log(` ${COLORS.bold}tools count:${COLORS.reset} ${tools?.tools?.length ?? 0}`)
|
||||
const coderAgents = tools?.tools?.filter((t) => t.tool_type === "coder_agent") ?? []
|
||||
log(` ${COLORS.bold}coder_agent count:${COLORS.reset} ${coderAgents.length}`)
|
||||
if (coderAgents.length > 0) {
|
||||
log("")
|
||||
log(` ${COLORS.bold}Coder Agents:${COLORS.reset}`)
|
||||
coderAgents.forEach((a) => {
|
||||
log(` - ${a.name} (id: ${a.id})`)
|
||||
log(` interpolation_string: ${a.interpolation_string ? "YES" : "NO"}`)
|
||||
log(` goals: ${a.goals ? "YES" : "NO"}`)
|
||||
log(` llm_provider: ${a.llm_provider ?? "(null)"}`)
|
||||
log(` llm_base_model: ${a.llm_base_model ?? "(null)"}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
log("")
|
||||
}
|
||||
|
||||
function testAgentPrompt(agentId) {
|
||||
const tools = loadCachedTools()
|
||||
if (!tools?.success) {
|
||||
error("No tools. Run: tfcode sync")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const agent = tools.tools.find((t) => t.id === agentId || t.name === agentId)
|
||||
if (!agent) {
|
||||
error(`Agent not found: ${agentId}`)
|
||||
log("")
|
||||
log("Available coder agents:")
|
||||
tools.tools
|
||||
.filter((t) => t.tool_type === "coder_agent")
|
||||
.forEach((t) => {
|
||||
log(` ${COLORS.cyan}${t.id}${COLORS.reset} - ${t.name}`)
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
log("")
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.magenta} Agent Data from tools.json${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log("")
|
||||
log(` ${COLORS.bold}id:${COLORS.reset} ${agent.id}`)
|
||||
log(` ${COLORS.bold}name:${COLORS.reset} ${agent.name}`)
|
||||
log(` ${COLORS.bold}description:${COLORS.reset} ${agent.description || "(none)"}`)
|
||||
log(` ${COLORS.bold}tool_type:${COLORS.reset} ${agent.tool_type}`)
|
||||
log(` ${COLORS.bold}auth_via:${COLORS.reset} ${agent.auth_via}`)
|
||||
log("")
|
||||
log(` ${COLORS.bold}interpolation_string:${COLORS.reset}`)
|
||||
if (agent.interpolation_string) {
|
||||
log(` ${agent.interpolation_string.substring(0, 200)}${agent.interpolation_string.length > 200 ? "..." : ""}`)
|
||||
} else {
|
||||
log(` ${COLORS.dim}(none)${COLORS.reset}`)
|
||||
}
|
||||
log("")
|
||||
log(` ${COLORS.bold}goals:${COLORS.reset}`)
|
||||
if (agent.goals) {
|
||||
log(` ${agent.goals.substring(0, 200)}${agent.goals.length > 200 ? "..." : ""}`)
|
||||
} else {
|
||||
log(` ${COLORS.dim}(none)${COLORS.reset}`)
|
||||
}
|
||||
log("")
|
||||
log(` ${COLORS.bold}temperature:${COLORS.reset} ${agent.temperature ?? "(none)"}`)
|
||||
log(` ${COLORS.bold}max_tokens:${COLORS.reset} ${agent.max_tokens ?? "(none)"}`)
|
||||
log(` ${COLORS.bold}llm_base_model:${COLORS.reset} ${agent.llm_base_model ?? "(none)"}`)
|
||||
log(` ${COLORS.bold}llm_provider:${COLORS.reset} ${agent.llm_provider ?? "(none)"}`)
|
||||
log("")
|
||||
|
||||
// Build highlighted instructions
|
||||
const isTFProvider = !agent.llm_provider || agent.llm_provider === "toothfairyai" || agent.llm_provider === "tf"
|
||||
|
||||
const hasPrompt = agent.interpolation_string && agent.interpolation_string.trim().length > 0
|
||||
const hasGoals = agent.goals && agent.goals.trim().length > 0
|
||||
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.magenta} Model Mapping${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log("")
|
||||
log(` ${COLORS.bold}isTFProvider:${COLORS.reset} ${isTFProvider}`)
|
||||
log(
|
||||
` ${COLORS.bold}mapped model:${COLORS.reset} ${isTFProvider && agent.llm_base_model ? `toothfairyai/${agent.llm_base_model}` : "(no mapping)"}`,
|
||||
)
|
||||
log("")
|
||||
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.magenta} Highlighted Instructions Preview${COLORS.reset}`)
|
||||
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
||||
log("")
|
||||
|
||||
if (!hasPrompt && !hasGoals) {
|
||||
log(
|
||||
` ${COLORS.dim}(No interpolation_string or goals - no highlighted instructions will be generated)${COLORS.reset}`,
|
||||
)
|
||||
} else {
|
||||
log("")
|
||||
log("═══════════════════════════════════════════════════════════════════════════════")
|
||||
log("⚠️ ULTRA IMPORTANT - AGENT CONFIGURATION ⚠️")
|
||||
log("═══════════════════════════════════════════════════════════════════════════════")
|
||||
log("")
|
||||
log(`You are acting as the agent: "${agent.name}"`)
|
||||
if (agent.description) {
|
||||
log(`Description: ${agent.description}`)
|
||||
}
|
||||
log("")
|
||||
log("The following instructions and goals are MANDATORY and MUST be followed")
|
||||
log("with the HIGHEST PRIORITY. These override any conflicting default behaviors.")
|
||||
log("═══════════════════════════════════════════════════════════════════════════════")
|
||||
|
||||
if (hasPrompt) {
|
||||
log("")
|
||||
log("┌─────────────────────────────────────────────────────────────────────────────┐")
|
||||
log(`│ 🎯 AGENT "${agent.name}" INSTRUCTIONS (CRITICAL - MUST FOLLOW) │`)
|
||||
log("└─────────────────────────────────────────────────────────────────────────────┘")
|
||||
log("")
|
||||
log(agent.interpolation_string)
|
||||
}
|
||||
|
||||
if (hasGoals) {
|
||||
log("")
|
||||
log("┌─────────────────────────────────────────────────────────────────────────────┐")
|
||||
log(`│ 🎯 AGENT "${agent.name}" GOALS (CRITICAL - MUST ACHIEVE) │`)
|
||||
log("└─────────────────────────────────────────────────────────────────────────────┘")
|
||||
log("")
|
||||
log(agent.goals)
|
||||
}
|
||||
|
||||
log("")
|
||||
log("═══════════════════════════════════════════════════════════════════════════════")
|
||||
log(`⚠️ END OF ULTRA IMPORTANT AGENT "${agent.name}" CONFIGURATION ⚠️`)
|
||||
log("═══════════════════════════════════════════════════════════════════════════════")
|
||||
}
|
||||
log("")
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const command = args[0]
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
showHelp()
|
||||
} else if (args.includes("--version") || args.includes("-v")) {
|
||||
log("tfcode v1.0.0-beta.9")
|
||||
} else if (command === "setup") {
|
||||
interactiveSetup()
|
||||
} else if (command === "validate") {
|
||||
;(async () => {
|
||||
const config = loadConfig()
|
||||
if (!config) {
|
||||
error("No credentials. Run: tfcode setup")
|
||||
process.exit(1)
|
||||
}
|
||||
info("Validating...")
|
||||
try {
|
||||
const result = await runPythonSync("validate", config)
|
||||
if (result.success) {
|
||||
success("Credentials valid")
|
||||
log(` API URL: ${result.base_url}`)
|
||||
} else {
|
||||
error(`Failed: ${result.error}`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (e) {
|
||||
error(`Failed: ${e.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
} else if (command === "sync") {
|
||||
;(async () => {
|
||||
const config = loadConfig()
|
||||
if (!config) {
|
||||
error("No credentials. Run: tfcode setup")
|
||||
process.exit(1)
|
||||
}
|
||||
info("Syncing tools...")
|
||||
try {
|
||||
const result = await runPythonSync("sync", config)
|
||||
if (result.success) {
|
||||
saveToolsCache(result)
|
||||
success(`Synced ${result.tools.length} tools`)
|
||||
if (result.by_type) {
|
||||
log("")
|
||||
log("By type:")
|
||||
for (const [t, c] of Object.entries(result.by_type)) log(` ${t}: ${c}`)
|
||||
}
|
||||
} else {
|
||||
error(`Failed: ${result.error}`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (e) {
|
||||
error(`Failed: ${e.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
} else if (command === "tools" && args[1] === "list") {
|
||||
const cached = loadCachedTools()
|
||||
if (!cached?.success) {
|
||||
error("No tools. Run: tfcode sync")
|
||||
process.exit(1)
|
||||
}
|
||||
let tools = cached.tools
|
||||
if (args[2] === "--type" && args[3]) tools = tools.filter((t) => t.tool_type === args[3])
|
||||
log(`\n${tools.length} tool(s):\n`)
|
||||
for (const t of tools) {
|
||||
log(` ${COLORS.cyan}${t.name}${COLORS.reset}`)
|
||||
log(` Type: ${t.tool_type}`)
|
||||
if (t.description) log(` ${COLORS.dim}${t.description.slice(0, 60)}${COLORS.reset}`)
|
||||
log(` Auth: ${t.auth_via}\n`)
|
||||
}
|
||||
} else if (command === "test-agent") {
|
||||
const agentId = args[1]
|
||||
if (!agentId) {
|
||||
error("Usage: tfcode test-agent <agent-id-or-name>")
|
||||
process.exit(1)
|
||||
}
|
||||
testAgentPrompt(agentId)
|
||||
} else if (command === "debug") {
|
||||
showDebugInfo()
|
||||
} else if (!command) {
|
||||
// Show help instead of trying TUI (TUI requires full build)
|
||||
showHelp();
|
||||
showHelp()
|
||||
} else {
|
||||
error(`Unknown command: ${command}`);
|
||||
showHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
error(`Unknown command: ${command}`)
|
||||
showHelp()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { Instance } from "../project/instance"
|
||||
import { Truncate } from "../tool/truncate"
|
||||
import { Auth } from "../auth"
|
||||
import { ProviderTransform } from "../provider/transform"
|
||||
import os from "os"
|
||||
|
||||
import PROMPT_GENERATE from "./generate.txt"
|
||||
import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||||
@ -41,6 +42,7 @@ export namespace Agent {
|
||||
.optional(),
|
||||
variant: z.string().optional(),
|
||||
prompt: z.string().optional(),
|
||||
goals: z.string().optional(),
|
||||
options: z.record(z.string(), z.any()),
|
||||
steps: z.number().int().positive().optional(),
|
||||
})
|
||||
@ -263,7 +265,8 @@ export namespace Agent {
|
||||
}
|
||||
|
||||
async function loadTFCoderAgents(): Promise<Info[]> {
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
// tools.json is synced to ~/.tfcode/tools.json by the CLI
|
||||
const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json")
|
||||
try {
|
||||
const content = await Bun.file(toolsPath).text()
|
||||
const data = JSON.parse(content)
|
||||
@ -271,19 +274,33 @@ export namespace Agent {
|
||||
|
||||
return data.tools
|
||||
.filter((t: any) => t.tool_type === "coder_agent")
|
||||
.map(
|
||||
(t: any): Info => ({
|
||||
.map((t: any): Info => {
|
||||
// Map model for ToothFairyAI providers only
|
||||
// Only map when llm_provider is None, "toothfairyai", or "tf"
|
||||
const isTFProvider = !t.llm_provider || t.llm_provider === "toothfairyai" || t.llm_provider === "tf"
|
||||
|
||||
const model =
|
||||
isTFProvider && t.llm_base_model
|
||||
? { modelID: t.llm_base_model as ModelID, providerID: "toothfairyai" as ProviderID }
|
||||
: undefined
|
||||
|
||||
return {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
mode: "primary" as const,
|
||||
permission: Permission.fromConfig({ "*": "allow" }),
|
||||
native: false,
|
||||
prompt: t.interpolation_string,
|
||||
goals: t.goals,
|
||||
temperature: t.temperature,
|
||||
model,
|
||||
options: {
|
||||
tf_agent_id: t.id,
|
||||
tf_auth_via: t.auth_via,
|
||||
tf_max_tokens: t.max_tokens,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
@ -68,10 +68,16 @@ export namespace LLM {
|
||||
const isOpenaiOauth = provider.id === "openai" && auth?.type === "oauth"
|
||||
|
||||
const system: string[] = []
|
||||
|
||||
// Build highlighted agent instructions for ToothFairyAI agents
|
||||
const tfHighlightedInstructions = buildTFAgentInstructions(input.agent)
|
||||
|
||||
system.push(
|
||||
[
|
||||
// use agent prompt otherwise provider prompt
|
||||
...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
|
||||
// highlighted TF agent instructions (if any)
|
||||
...(tfHighlightedInstructions ? [tfHighlightedInstructions] : []),
|
||||
// any custom prompt passed into this call
|
||||
...input.system,
|
||||
// any custom prompt from last user message
|
||||
@ -168,7 +174,7 @@ export namespace LLM {
|
||||
const maxOutputTokens =
|
||||
isOpenaiOauth || provider.id.includes("github-copilot")
|
||||
? undefined
|
||||
: ProviderTransform.maxOutputTokens(input.model)
|
||||
: (input.agent.options?.tf_max_tokens ?? ProviderTransform.maxOutputTokens(input.model))
|
||||
|
||||
const tools = await resolveTools(input)
|
||||
|
||||
@ -320,4 +326,63 @@ export namespace LLM {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Build highlighted instructions for ToothFairyAI agents.
|
||||
* When a TF agent is selected, its instructions and goals are highlighted
|
||||
* as ULTRA IMPORTANT to ensure they are followed with highest priority.
|
||||
*/
|
||||
function buildTFAgentInstructions(agent: Agent.Info): string | null {
|
||||
// Only highlight for ToothFairyAI agents (non-native with tf_agent_id)
|
||||
const isTFAgent = !agent.native && agent.options?.tf_agent_id
|
||||
if (!isTFAgent) return null
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
// Check if agent has custom prompt (interpolation_string) or goals
|
||||
const hasPrompt = agent.prompt && agent.prompt.trim().length > 0
|
||||
const hasGoals = agent.goals && agent.goals.trim().length > 0
|
||||
|
||||
if (!hasPrompt && !hasGoals) return null
|
||||
|
||||
parts.push("")
|
||||
parts.push("═══════════════════════════════════════════════════════════════════════════════")
|
||||
parts.push("⚠️ ULTRA IMPORTANT - AGENT CONFIGURATION ⚠️")
|
||||
parts.push("═══════════════════════════════════════════════════════════════════════════════")
|
||||
parts.push("")
|
||||
parts.push(`You are acting as the agent: "${agent.name}"`)
|
||||
if (agent.description) {
|
||||
parts.push(`Description: ${agent.description}`)
|
||||
}
|
||||
parts.push("")
|
||||
parts.push("The following instructions and goals are MANDATORY and MUST be followed")
|
||||
parts.push("with the HIGHEST PRIORITY. These override any conflicting default behaviors.")
|
||||
parts.push("═══════════════════════════════════════════════════════════════════════════════")
|
||||
|
||||
if (hasPrompt) {
|
||||
parts.push("")
|
||||
parts.push("┌─────────────────────────────────────────────────────────────────────────────┐")
|
||||
parts.push(`│ 🎯 AGENT "${agent.name}" INSTRUCTIONS (CRITICAL - MUST FOLLOW) │`)
|
||||
parts.push("└─────────────────────────────────────────────────────────────────────────────┘")
|
||||
parts.push("")
|
||||
parts.push(agent.prompt!)
|
||||
}
|
||||
|
||||
if (hasGoals) {
|
||||
parts.push("")
|
||||
parts.push("┌─────────────────────────────────────────────────────────────────────────────┐")
|
||||
parts.push(`│ 🎯 AGENT "${agent.name}" GOALS (CRITICAL - MUST ACHIEVE) │`)
|
||||
parts.push("└─────────────────────────────────────────────────────────────────────────────┘")
|
||||
parts.push("")
|
||||
parts.push(agent.goals!)
|
||||
}
|
||||
|
||||
parts.push("")
|
||||
parts.push("═══════════════════════════════════════════════════════════════════════════════")
|
||||
parts.push(`⚠️ END OF ULTRA IMPORTANT AGENT "${agent.name}" CONFIGURATION ⚠️`)
|
||||
parts.push("═══════════════════════════════════════════════════════════════════════════════")
|
||||
parts.push("")
|
||||
|
||||
return parts.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
492
packages/tfcode/test/agent/tf-agent.test.ts
Normal file
492
packages/tfcode/test/agent/tf-agent.test.ts
Normal file
@ -0,0 +1,492 @@
|
||||
import { afterAll, beforeAll, beforeEach, afterEach, test, expect, describe } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { Global } from "../../src/global"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
import { ModelsDev } from "../../src/provider/models"
|
||||
import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
import type { MessageV2 } from "../../src/session/message-v2"
|
||||
|
||||
// Server for capturing LLM requests
|
||||
const state = {
|
||||
server: null as ReturnType<typeof Bun.serve> | null,
|
||||
queue: [] as Array<{ path: string; response: Response; resolve: (value: any) => void }>,
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
const result = {} as { promise: Promise<T>; resolve: (value: T) => void }
|
||||
result.promise = new Promise((resolve) => {
|
||||
result.resolve = resolve
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function waitRequest(pathname: string, response: Response) {
|
||||
const pending = deferred<{ url: URL; headers: Headers; body: Record<string, unknown> }>()
|
||||
state.queue.push({ path: pathname, response, resolve: pending.resolve })
|
||||
return pending.promise
|
||||
}
|
||||
|
||||
function createChatStream(text: string) {
|
||||
const payload =
|
||||
[
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl-1",
|
||||
object: "chat.completion.chunk",
|
||||
choices: [{ delta: { role: "assistant" } }],
|
||||
})}`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl-1",
|
||||
object: "chat.completion.chunk",
|
||||
choices: [{ delta: { content: text } }],
|
||||
})}`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl-1",
|
||||
object: "chat.completion.chunk",
|
||||
choices: [{ delta: {}, finish_reason: "stop" }],
|
||||
})}`,
|
||||
"data: [DONE]",
|
||||
].join("\n\n") + "\n\n"
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(payload))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function loadFixture(providerID: string, modelID: string) {
|
||||
const fixturePath = path.join(import.meta.dir, "../tool/fixtures/models-api.json")
|
||||
const data = await Filesystem.readJson<Record<string, ModelsDev.Provider>>(fixturePath)
|
||||
const provider = data[providerID]
|
||||
if (!provider) throw new Error(`Missing provider in fixture: ${providerID}`)
|
||||
const model = provider.models[modelID]
|
||||
if (!model) throw new Error(`Missing model in fixture: ${modelID}`)
|
||||
return { provider, model }
|
||||
}
|
||||
|
||||
// Test the flow from tools.json -> Agent.Info -> highlighted instructions
|
||||
|
||||
describe("ToothFairyAI Agent Loading", () => {
|
||||
let originalDataPath: string
|
||||
|
||||
beforeEach(async () => {
|
||||
originalDataPath = Global.Path.data
|
||||
const testDataDir = path.join(path.dirname(originalDataPath), "tf-agent-test-data")
|
||||
;(Global.Path as { data: string }).data = testDataDir
|
||||
await fs.promises.mkdir(path.join(testDataDir, ".tfcode"), { recursive: true })
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
;(Global.Path as { data: string }).data = originalDataPath
|
||||
})
|
||||
|
||||
describe("loadTFCoderAgents", () => {
|
||||
test("parses tools.json with full agent data", async () => {
|
||||
const toolsData = {
|
||||
success: true,
|
||||
tools: [
|
||||
{
|
||||
id: "coder-agent-1",
|
||||
name: "Code Reviewer",
|
||||
description: "Reviews code for quality",
|
||||
tool_type: "coder_agent",
|
||||
request_type: null,
|
||||
url: null,
|
||||
auth_via: "tf_agent",
|
||||
interpolation_string: "You are a code reviewer. Review code thoroughly.",
|
||||
goals: "Identify bugs. Suggest improvements.",
|
||||
temperature: 0.3,
|
||||
max_tokens: 4096,
|
||||
llm_base_model: "claude-3-5-sonnet",
|
||||
llm_provider: "toothfairyai",
|
||||
},
|
||||
{
|
||||
id: "coder-agent-2",
|
||||
name: "Test Writer",
|
||||
description: "Writes tests",
|
||||
tool_type: "coder_agent",
|
||||
request_type: null,
|
||||
url: null,
|
||||
auth_via: "tf_agent",
|
||||
interpolation_string: "You are a test writer.",
|
||||
goals: "Write comprehensive tests.",
|
||||
temperature: 0.5,
|
||||
max_tokens: null,
|
||||
llm_base_model: "gpt-4",
|
||||
llm_provider: null,
|
||||
},
|
||||
],
|
||||
by_type: { coder_agent: 2 },
|
||||
}
|
||||
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData, null, 2))
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agents = await Agent.list()
|
||||
const codeReviewer = agents.find((a) => a.name === "Code Reviewer")
|
||||
const testWriter = agents.find((a) => a.name === "Test Writer")
|
||||
|
||||
expect(codeReviewer).toBeDefined()
|
||||
expect(codeReviewer?.description).toBe("Reviews code for quality")
|
||||
expect(codeReviewer?.prompt).toBe("You are a code reviewer. Review code thoroughly.")
|
||||
expect(codeReviewer?.goals).toBe("Identify bugs. Suggest improvements.")
|
||||
expect(codeReviewer?.temperature).toBe(0.3)
|
||||
expect(codeReviewer?.native).toBe(false)
|
||||
expect(codeReviewer?.options?.tf_agent_id).toBe("coder-agent-1")
|
||||
expect(codeReviewer?.options?.tf_auth_via).toBe("tf_agent")
|
||||
expect(codeReviewer?.options?.tf_max_tokens).toBe(4096)
|
||||
expect(String(codeReviewer?.model?.providerID)).toBe("toothfairyai")
|
||||
expect(String(codeReviewer?.model?.modelID)).toBe("claude-3-5-sonnet")
|
||||
|
||||
expect(testWriter).toBeDefined()
|
||||
expect(String(testWriter?.model?.providerID)).toBe("toothfairyai")
|
||||
expect(String(testWriter?.model?.modelID)).toBe("gpt-4")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("maps tf provider to toothfairyai", async () => {
|
||||
const toolsData = {
|
||||
success: true,
|
||||
tools: [
|
||||
{
|
||||
id: "tf-provider-agent",
|
||||
name: "TF Provider Agent",
|
||||
description: "Test",
|
||||
tool_type: "coder_agent",
|
||||
interpolation_string: "Test",
|
||||
goals: "Test",
|
||||
temperature: 0.7,
|
||||
max_tokens: 2048,
|
||||
llm_base_model: "test-model",
|
||||
llm_provider: "tf",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData))
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agent = await Agent.get("TF Provider Agent")
|
||||
expect(String(agent?.model?.providerID)).toBe("toothfairyai")
|
||||
expect(String(agent?.model?.modelID)).toBe("test-model")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("does not map external providers", async () => {
|
||||
const toolsData = {
|
||||
success: true,
|
||||
tools: [
|
||||
{
|
||||
id: "external-agent",
|
||||
name: "External Agent",
|
||||
description: "Test",
|
||||
tool_type: "coder_agent",
|
||||
interpolation_string: "Test",
|
||||
goals: "Test",
|
||||
temperature: 0.7,
|
||||
max_tokens: 2048,
|
||||
llm_base_model: "claude-3-5-sonnet",
|
||||
llm_provider: "anthropic",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData))
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agent = await Agent.get("External Agent")
|
||||
expect(agent?.model).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("handles agent without interpolation_string or goals", async () => {
|
||||
const toolsData = {
|
||||
success: true,
|
||||
tools: [
|
||||
{
|
||||
id: "minimal-agent",
|
||||
name: "Minimal Agent",
|
||||
description: "Test",
|
||||
tool_type: "coder_agent",
|
||||
interpolation_string: null,
|
||||
goals: null,
|
||||
temperature: null,
|
||||
max_tokens: null,
|
||||
llm_base_model: null,
|
||||
llm_provider: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData))
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agent = await Agent.get("Minimal Agent")
|
||||
expect(agent).toBeDefined()
|
||||
expect(agent?.prompt).toBeNull()
|
||||
expect(agent?.goals).toBeNull()
|
||||
expect(agent?.model).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Separate describe block for LLM stream tests
|
||||
describe("ToothFairyAI Agent Instructions in LLM Stream", () => {
|
||||
let originalDataPath: string
|
||||
|
||||
beforeAll(() => {
|
||||
state.server = Bun.serve({
|
||||
port: 0,
|
||||
async fetch(req) {
|
||||
const next = state.queue.shift()
|
||||
if (!next) return new Response("unexpected request", { status: 500 })
|
||||
const url = new URL(req.url)
|
||||
const body = (await req.json()) as Record<string, unknown>
|
||||
next.resolve({ url, headers: req.headers, body })
|
||||
if (!url.pathname.endsWith(next.path)) return new Response("not found", { status: 404 })
|
||||
return next.response
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
state.server?.stop()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
state.queue.length = 0
|
||||
originalDataPath = Global.Path.data
|
||||
const testDataDir = path.join(path.dirname(originalDataPath), "tf-agent-test-data")
|
||||
;(Global.Path as { data: string }).data = testDataDir
|
||||
await fs.promises.mkdir(path.join(testDataDir, ".tfcode"), { recursive: true })
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
;(Global.Path as { data: string }).data = originalDataPath
|
||||
})
|
||||
|
||||
test("includes highlighted TF agent instructions in system prompt", async () => {
|
||||
const server = state.server
|
||||
if (!server) throw new Error("Server not initialized")
|
||||
|
||||
const providerID = "alibaba"
|
||||
const modelID = "qwen-plus"
|
||||
const fixture = await loadFixture(providerID, modelID)
|
||||
|
||||
// Setup TF agent with this model
|
||||
const toolsData = {
|
||||
success: true,
|
||||
tools: [
|
||||
{
|
||||
id: "code-reviewer-123",
|
||||
name: "Code Reviewer",
|
||||
description: "Reviews code for quality and best practices",
|
||||
tool_type: "coder_agent",
|
||||
auth_via: "tf_agent",
|
||||
interpolation_string: "You are a code reviewer. Always check for bugs, security issues, and suggest improvements.",
|
||||
goals: "Review all code thoroughly. Provide actionable feedback. Ensure code quality standards.",
|
||||
temperature: 0.3,
|
||||
max_tokens: 4096,
|
||||
llm_base_model: modelID,
|
||||
llm_provider: providerID,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData, null, 2))
|
||||
|
||||
const request = waitRequest(
|
||||
"/chat/completions",
|
||||
new Response(createChatStream("I'll review your code."), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
}),
|
||||
)
|
||||
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: [providerID],
|
||||
provider: {
|
||||
[providerID]: {
|
||||
options: {
|
||||
apiKey: "test-key",
|
||||
baseURL: `${server.url.origin}/v1`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agent = await Agent.get("Code Reviewer")
|
||||
expect(agent).toBeDefined()
|
||||
|
||||
const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(modelID))
|
||||
const sessionID = SessionID.make("test-session")
|
||||
const user: MessageV2.User = {
|
||||
id: MessageID.make("user-1"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: "Code Reviewer",
|
||||
model: { providerID: ProviderID.make(providerID), modelID: ModelID.make(modelID) },
|
||||
}
|
||||
|
||||
const stream = await LLM.stream({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent: agent!,
|
||||
system: [],
|
||||
abort: new AbortController().signal,
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
})
|
||||
|
||||
for await (const _ of stream.fullStream) {}
|
||||
|
||||
const capture = await request
|
||||
const body = capture.body
|
||||
const messages = body.messages as Array<{ role: string; content: string }>
|
||||
|
||||
const systemMessage = messages.find((m) => m.role === "system")
|
||||
expect(systemMessage).toBeDefined()
|
||||
|
||||
const systemContent = systemMessage!.content
|
||||
|
||||
expect(systemContent).toContain("ULTRA IMPORTANT - AGENT CONFIGURATION")
|
||||
expect(systemContent).toContain('You are acting as the agent: "Code Reviewer"')
|
||||
expect(systemContent).toContain("Reviews code for quality and best practices")
|
||||
expect(systemContent).toContain("AGENT \"Code Reviewer\" INSTRUCTIONS")
|
||||
expect(systemContent).toContain("You are a code reviewer. Always check for bugs, security issues, and suggest improvements.")
|
||||
expect(systemContent).toContain("AGENT \"Code Reviewer\" GOALS")
|
||||
expect(systemContent).toContain("Review all code thoroughly. Provide actionable feedback. Ensure code quality standards.")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("does NOT include highlighted instructions for native agents", async () => {
|
||||
const server = state.server
|
||||
if (!server) throw new Error("Server not initialized")
|
||||
|
||||
const providerID = "alibaba"
|
||||
const modelID = "qwen-plus"
|
||||
const fixture = await loadFixture(providerID, modelID)
|
||||
|
||||
const request = waitRequest(
|
||||
"/chat/completions",
|
||||
new Response(createChatStream("Hello"), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
}),
|
||||
)
|
||||
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: [providerID],
|
||||
provider: {
|
||||
[providerID]: {
|
||||
options: {
|
||||
apiKey: "test-key",
|
||||
baseURL: `${server.url.origin}/v1`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agent = await Agent.get("build")
|
||||
expect(agent).toBeDefined()
|
||||
expect(agent?.native).toBe(true)
|
||||
|
||||
const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(modelID))
|
||||
const sessionID = SessionID.make("test-session")
|
||||
const user: MessageV2.User = {
|
||||
id: MessageID.make("user-1"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: "build",
|
||||
model: { providerID: ProviderID.make(providerID), modelID: ModelID.make(modelID) },
|
||||
}
|
||||
|
||||
const stream = await LLM.stream({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent: agent!,
|
||||
system: [],
|
||||
abort: new AbortController().signal,
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
})
|
||||
|
||||
for await (const _ of stream.fullStream) {}
|
||||
|
||||
const capture = await request
|
||||
const body = capture.body
|
||||
const messages = body.messages as Array<{ role: string; content: string }>
|
||||
|
||||
const systemMessage = messages.find((m) => m.role === "system")
|
||||
expect(systemMessage).toBeDefined()
|
||||
|
||||
const systemContent = systemMessage!.content
|
||||
|
||||
expect(systemContent).not.toContain("ULTRA IMPORTANT - AGENT CONFIGURATION")
|
||||
expect(systemContent).not.toContain("You are acting as the agent:")
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user