feat: new integration

This commit is contained in:
Gab 2026-03-27 09:11:17 +11:00
parent 0f62ba8dd5
commit 8e05565e84
7 changed files with 192 additions and 7 deletions

View File

@ -25,6 +25,7 @@ class ToolType(str, Enum):
CODER_AGENT = "coder_agent" CODER_AGENT = "coder_agent"
DATABASE_SCRIPT = "database_script" DATABASE_SCRIPT = "database_script"
API_FUNCTION = "api_function" API_FUNCTION = "api_function"
PROMPT = "prompt"
class FunctionRequestType(str, Enum): class FunctionRequestType(str, Enum):

View File

@ -6,9 +6,10 @@ SDK Structure:
- agent_functions: API Functions (with request_type) - agent_functions: API Functions (with request_type)
- connections: Provider connections (openai, anthropic, etc.) - connections: Provider connections (openai, anthropic, etc.)
- agents: TF workspace agents - agents: TF workspace agents
- prompts: Prompt templates (with available_to_agents mapping)
""" """
from typing import Any, Optional from typing import Any, Optional, List
from pydantic import BaseModel from pydantic import BaseModel
from toothfairyai.types import AgentFunction from toothfairyai.types import AgentFunction
@ -45,11 +46,23 @@ class SyncedTool(BaseModel):
llm_provider: Optional[str] = None llm_provider: Optional[str] = None
class SyncedPrompt(BaseModel):
"""A prompt template synced from ToothFairyAI workspace."""
id: str
label: str
interpolation_string: str
prompt_type: Optional[str] = None
available_to_agents: Optional[List[str]] = None
description: Optional[str] = None
class ToolSyncResult(BaseModel): class ToolSyncResult(BaseModel):
"""Result of tool sync operation.""" """Result of tool sync operation."""
success: bool success: bool
tools: list[SyncedTool] = [] tools: list[SyncedTool] = []
prompts: list[SyncedPrompt] = []
by_type: dict[str, int] = {} by_type: dict[str, int] = {}
error: Optional[str] = None error: Optional[str] = None
@ -149,6 +162,26 @@ def parse_agent(agent) -> SyncedTool:
) )
def parse_prompt(prompt) -> SyncedPrompt:
"""
Parse Prompt from SDK into SyncedPrompt.
Args:
prompt: Prompt from TF SDK
Returns:
SyncedPrompt instance
"""
return SyncedPrompt(
id=prompt.id,
label=prompt.label,
interpolation_string=prompt.interpolation_string,
prompt_type=getattr(prompt, 'prompt_type', None),
available_to_agents=getattr(prompt, 'available_to_agents', None),
description=getattr(prompt, 'description', None),
)
def sync_tools(config: TFConfig) -> ToolSyncResult: def sync_tools(config: TFConfig) -> ToolSyncResult:
""" """
Sync all tools from ToothFairyAI workspace using SDK. Sync all tools from ToothFairyAI workspace using SDK.
@ -157,12 +190,13 @@ def sync_tools(config: TFConfig) -> ToolSyncResult:
- Agent Functions (API Functions with request_type) - Agent Functions (API Functions with request_type)
- Agent Skills (functions with is_agent_skill=True) - Agent Skills (functions with is_agent_skill=True)
- Coder Agents (agents with mode='coder') - Coder Agents (agents with mode='coder')
- Prompts (prompt templates with available_to_agents mapping)
Args: Args:
config: TFConfig instance config: TFConfig instance
Returns: Returns:
ToolSyncResult with synced tools ToolSyncResult with synced tools and prompts
""" """
try: try:
client = config.get_client() client = config.get_client()
@ -180,14 +214,26 @@ def sync_tools(config: TFConfig) -> ToolSyncResult:
except Exception as e: except Exception as e:
pass pass
# Sync prompts
prompts = []
try:
prompts_result = client.prompts.list()
prompts = [parse_prompt(p) for p in prompts_result.items]
except Exception as e:
pass
by_type = {} by_type = {}
for tool in tools: for tool in tools:
type_name = tool.tool_type.value type_name = tool.tool_type.value
by_type[type_name] = by_type.get(type_name, 0) + 1 by_type[type_name] = by_type.get(type_name, 0) + 1
if prompts:
by_type['prompt'] = len(prompts)
return ToolSyncResult( return ToolSyncResult(
success=True, success=True,
tools=tools, tools=tools,
prompts=prompts,
by_type=by_type, by_type=by_type,
) )

View File

@ -306,6 +306,37 @@ export namespace Agent {
} }
} }
export interface TFPrompt {
id: string
label: string
interpolation_string: string
available_to_agents?: string[]
}
async function loadTFPrompts(): Promise<TFPrompt[]> {
const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json")
try {
const content = await Bun.file(toolsPath).text()
const data = JSON.parse(content)
if (!data.success || !data.prompts) return []
return data.prompts as TFPrompt[]
} catch {
return []
}
}
export async function getPromptForAgent(agentId: string): Promise<TFPrompt | null> {
const prompts = await loadTFPrompts()
return prompts.find((p) => p.available_to_agents?.includes(agentId)) ?? null
}
export async function getPromptForAgentName(agentName: string): Promise<TFPrompt | null> {
const agents = await loadTFCoderAgents()
const agent = agents.find((a) => a.name === agentName)
if (!agent?.options?.tf_agent_id) return null
return getPromptForAgent(agent.options.tf_agent_id)
}
export async function list() { export async function list() {
const cfg = await Config.get() const cfg = await Config.get()
const localAgents = await state() const localAgents = await state()

View File

@ -14,7 +14,7 @@ const printError = (msg: string) => UI.error(msg)
const success = (msg: string) => UI.println(UI.Style.TEXT_SUCCESS_BOLD + msg + UI.Style.TEXT_NORMAL) const success = (msg: string) => UI.println(UI.Style.TEXT_SUCCESS_BOLD + msg + UI.Style.TEXT_NORMAL)
const info = (msg: string) => UI.println(UI.Style.TEXT_NORMAL + msg) const info = (msg: string) => UI.println(UI.Style.TEXT_NORMAL + msg)
type ToolType = "mcp_server" | "agent_skill" | "database_script" | "api_function" type ToolType = "mcp_server" | "agent_skill" | "database_script" | "api_function" | "coder_agent" | "prompt"
interface SyncedTool { interface SyncedTool {
id: string id: string
@ -28,11 +28,27 @@ interface SyncedTool {
url?: string url?: string
tools: string[] tools: string[]
auth_via: string auth_via: string
interpolation_string?: string
goals?: string
temperature?: number
max_tokens?: number
llm_base_model?: string
llm_provider?: string
}
interface SyncedPrompt {
id: string
label: string
interpolation_string: string
prompt_type?: string
available_to_agents?: string[]
description?: string
} }
interface ToolSyncResult { interface ToolSyncResult {
success: boolean success: boolean
tools: SyncedTool[] tools: SyncedTool[]
prompts: SyncedPrompt[]
by_type: Record<string, number> by_type: Record<string, number>
error?: string error?: string
} }
@ -101,12 +117,30 @@ try:
"request_type": tool.request_type.value if tool.request_type else None, "request_type": tool.request_type.value if tool.request_type else None,
"url": tool.url, "url": tool.url,
"tools": tool.tools, "tools": tool.tools,
"auth_via": tool.auth_via "auth_via": tool.auth_via,
"interpolation_string": tool.interpolation_string,
"goals": tool.goals,
"temperature": tool.temperature,
"max_tokens": tool.max_tokens,
"llm_base_model": tool.llm_base_model,
"llm_provider": tool.llm_provider
})
prompts_data = []
for prompt in result.prompts:
prompts_data.append({
"id": prompt.id,
"label": prompt.label,
"interpolation_string": prompt.interpolation_string,
"prompt_type": prompt.prompt_type,
"available_to_agents": prompt.available_to_agents,
"description": prompt.description
}) })
print(json.dumps({ print(json.dumps({
"success": result.success, "success": result.success,
"tools": tools_data, "tools": tools_data,
"prompts": prompts_data,
"by_type": result.by_type, "by_type": result.by_type,
"error": result.error "error": result.error
})) }))
@ -138,7 +172,13 @@ try:
"request_type": tool.request_type.value if tool.request_type else None, "request_type": tool.request_type.value if tool.request_type else None,
"url": tool.url, "url": tool.url,
"tools": tool.tools, "tools": tool.tools,
"auth_via": tool.auth_via "auth_via": tool.auth_via,
"interpolation_string": tool.interpolation_string,
"goals": tool.goals,
"temperature": tool.temperature,
"max_tokens": tool.max_tokens,
"llm_base_model": tool.llm_base_model,
"llm_provider": tool.llm_provider
}) })
print(json.dumps({ print(json.dumps({
@ -353,6 +393,8 @@ const ToolsListCommand = cmd({
agent_skill: "Skill", agent_skill: "Skill",
database_script: "DB", database_script: "DB",
api_function: "API", api_function: "API",
coder_agent: "Coder Agent",
prompt: "Prompt",
}[tool.tool_type] }[tool.tool_type]
info(` ${tool.name}`) info(` ${tool.name}`)

View File

@ -13,6 +13,8 @@ import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util/locale" import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history" import type { PromptInfo } from "./history"
import { useFrecency } from "./frecency" import { useFrecency } from "./frecency"
import { Agent } from "@/agent/agent"
import { useLocal } from "@tui/context/local"
function removeLineRange(input: string) { function removeLineRange(input: string) {
const hashIndex = input.lastIndexOf("#") const hashIndex = input.lastIndexOf("#")
@ -77,6 +79,7 @@ export function Autocomplete(props: {
}) { }) {
const sdk = useSDK() const sdk = useSDK()
const sync = useSync() const sync = useSync()
const local = useLocal()
const command = useCommandDialog() const command = useCommandDialog()
const { theme } = useTheme() const { theme } = useTheme()
const dimensions = useTerminalDimensions() const dimensions = useTerminalDimensions()
@ -353,6 +356,42 @@ export function Autocomplete(props: {
) )
}) })
const tfPrompts = createMemo(() => {
if (!store.visible || store.visible === "/") return []
const currentAgent = local.agent.current()
const agentId = currentAgent.options?.tf_agent_id as string | undefined
if (!agentId) return []
const options: AutocompleteOption[] = []
const width = props.anchor().width - 4
const prompts = sync.data.prompts || []
for (const prompt of prompts) {
const isAvailable =
!prompt.available_to_agents ||
prompt.available_to_agents.length === 0 ||
prompt.available_to_agents.includes(agentId)
if (isAvailable) {
options.push({
display: Locale.truncateMiddle("@" + prompt.label, width),
value: prompt.label,
description: "Prompt template",
onSelect: () => {
const cursor = props.input().logicalCursor
props.input().deleteRange(0, 0, cursor.row, cursor.col)
props.input().insertText(prompt.interpolation_string)
props.input().cursorOffset = Bun.stringWidth(prompt.interpolation_string)
},
})
}
}
return options
})
const commands = createMemo((): AutocompleteOption[] => { const commands = createMemo((): AutocompleteOption[] => {
const results: AutocompleteOption[] = [...command.slashes()] const results: AutocompleteOption[] = [...command.slashes()]
@ -386,9 +425,12 @@ export function Autocomplete(props: {
const filesValue = files() const filesValue = files()
const agentsValue = agents() const agentsValue = agents()
const commandsValue = commands() const commandsValue = commands()
const promptsValue = tfPrompts()
const mixed: AutocompleteOption[] = const mixed: AutocompleteOption[] =
store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue] store.visible === "@"
? [...agentsValue, ...promptsValue, ...(filesValue || []), ...mcpResources()]
: [...commandsValue]
const searchValue = search() const searchValue = search()

View File

@ -29,6 +29,8 @@ import { batch, onMount } from "solid-js"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import type { Path } from "@opencode-ai/sdk" import type { Path } from "@opencode-ai/sdk"
import type { Workspace } from "@opencode-ai/sdk/v2" import type { Workspace } from "@opencode-ai/sdk/v2"
import path from "path"
import os from "os"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync", name: "Sync",
@ -75,6 +77,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
vcs: VcsInfo | undefined vcs: VcsInfo | undefined
path: Path path: Path
workspaceList: Workspace[] workspaceList: Workspace[]
prompts: Array<{
id: string
label: string
interpolation_string: string
available_to_agents?: string[]
}>
}>({ }>({
provider_next: { provider_next: {
all: [], all: [],
@ -103,6 +111,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
vcs: undefined, vcs: undefined,
path: { state: "", config: "", worktree: "", directory: "" }, path: { state: "", config: "", worktree: "", directory: "" },
workspaceList: [], workspaceList: [],
prompts: [],
}) })
const sdk = useSDK() const sdk = useSDK()
@ -113,6 +122,19 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
setStore("workspaceList", reconcile(result.data)) setStore("workspaceList", reconcile(result.data))
} }
async function loadTFPrompts() {
const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json")
try {
const content = await Bun.file(toolsPath).text()
const data = JSON.parse(content)
if (data.success && data.prompts) {
setStore("prompts", reconcile(data.prompts))
}
} catch {
// File doesn't exist or is invalid, that's OK
}
}
sdk.event.listen((e) => { sdk.event.listen((e) => {
const event = e.details const event = e.details
switch (event.type) { switch (event.type) {
@ -423,6 +445,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))), sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))),
sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))), sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))),
syncWorkspaces(), syncWorkspaces(),
loadTFPrompts(),
]).then(() => { ]).then(() => {
setStore("status", "complete") setStore("status", "complete")
}) })

View File

@ -70,7 +70,7 @@ export namespace LLM {
const system: string[] = [] const system: string[] = []
// Build highlighted agent instructions for ToothFairyAI agents // Build highlighted agent instructions for ToothFairyAI agents
const tfHighlightedInstructions = buildTFAgentInstructions(input.agent) const tfHighlightedInstructions = await buildTFAgentInstructions(input.agent)
system.push( system.push(
[ [