mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-29 21:33:54 +00:00
feat: agents and skills
This commit is contained in:
parent
485cc7649e
commit
ff77a81141
Binary file not shown.
@ -50,8 +50,9 @@ def classify_tool(func: AgentFunction) -> ToolType:
|
||||
"""
|
||||
Classify a tool based on its properties.
|
||||
|
||||
Currently the SDK exposes:
|
||||
- agent_functions: API functions with request_type
|
||||
Types:
|
||||
- AGENT_SKILL: is_agent_skill=True
|
||||
- API_FUNCTION: has request_type
|
||||
|
||||
Args:
|
||||
func: AgentFunction from TF SDK
|
||||
@ -59,6 +60,10 @@ def classify_tool(func: AgentFunction) -> ToolType:
|
||||
Returns:
|
||||
ToolType enum value
|
||||
"""
|
||||
# Agent skills have is_agent_skill=True
|
||||
if getattr(func, 'is_agent_skill', None) is True:
|
||||
return ToolType.AGENT_SKILL
|
||||
|
||||
# All agent_functions with request_type are API Functions
|
||||
if func.request_type:
|
||||
return ToolType.API_FUNCTION
|
||||
@ -89,6 +94,10 @@ def parse_function(func: AgentFunction) -> SyncedTool:
|
||||
# or may use TF proxy
|
||||
auth_via = "user_provided" if func.authorisation_type == "api_key" else "tf_proxy"
|
||||
|
||||
# Agent skills use skill script
|
||||
if tool_type == ToolType.AGENT_SKILL:
|
||||
auth_via = "tf_skill"
|
||||
|
||||
return SyncedTool(
|
||||
id=func.id,
|
||||
name=func.name,
|
||||
@ -98,6 +107,7 @@ def parse_function(func: AgentFunction) -> SyncedTool:
|
||||
url=func.url,
|
||||
authorisation_type=func.authorisation_type,
|
||||
auth_via=auth_via,
|
||||
is_agent_skill=tool_type == ToolType.AGENT_SKILL,
|
||||
)
|
||||
|
||||
|
||||
@ -126,7 +136,8 @@ def sync_tools(config: TFConfig) -> ToolSyncResult:
|
||||
Sync all tools from ToothFairyAI workspace using SDK.
|
||||
|
||||
Includes:
|
||||
- Agent Functions (API Functions)
|
||||
- Agent Functions (API Functions with request_type)
|
||||
- Agent Skills (functions with is_agent_skill=True)
|
||||
- Coder Agents (agents with mode='coder')
|
||||
|
||||
Args:
|
||||
@ -138,7 +149,7 @@ def sync_tools(config: TFConfig) -> ToolSyncResult:
|
||||
try:
|
||||
client = config.get_client()
|
||||
|
||||
# Sync agent functions
|
||||
# Sync agent functions (includes API Functions and Agent Skills)
|
||||
func_result = client.agent_functions.list()
|
||||
tools = [parse_function(f) for f in func_result.items]
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ import { DialogMcp } from "@tui/component/dialog-mcp"
|
||||
import { DialogStatus } from "@tui/component/dialog-status"
|
||||
import { DialogThemeList } from "@tui/component/dialog-theme-list"
|
||||
import { DialogHelp } from "./ui/dialog-help"
|
||||
import { DialogChangelog } from "./ui/dialog-changelog"
|
||||
import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command"
|
||||
import { DialogAgent } from "@tui/component/dialog-agent"
|
||||
import { DialogSessionList } from "@tui/component/dialog-session-list"
|
||||
@ -576,11 +577,22 @@ function App() {
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Changelog",
|
||||
value: "changelog.show",
|
||||
slash: {
|
||||
name: "changelog",
|
||||
},
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogChangelog />)
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Open docs",
|
||||
value: "docs.open",
|
||||
onSelect: () => {
|
||||
open("https://opencode.ai/docs").catch(() => {})
|
||||
open("https://toothfairyai.com/developers/tfcode").catch(() => {})
|
||||
dialog.clear()
|
||||
},
|
||||
category: "System",
|
||||
|
||||
@ -15,12 +15,13 @@ import { Clipboard } from "@tui/util/clipboard"
|
||||
import { useToast } from "../ui/toast"
|
||||
|
||||
const PROVIDER_PRIORITY: Record<string, number> = {
|
||||
opencode: 0,
|
||||
"opencode-go": 1,
|
||||
openai: 2,
|
||||
"github-copilot": 3,
|
||||
anthropic: 4,
|
||||
google: 5,
|
||||
toothfairyai: 0,
|
||||
opencode: 1,
|
||||
"opencode-go": 2,
|
||||
openai: 3,
|
||||
"github-copilot": 4,
|
||||
anthropic: 5,
|
||||
google: 6,
|
||||
}
|
||||
|
||||
export function createDialogProviderOptions() {
|
||||
@ -36,6 +37,7 @@ export function createDialogProviderOptions() {
|
||||
title: provider.name,
|
||||
value: provider.id,
|
||||
description: {
|
||||
toothfairyai: "(Recommended - ToothFairyAI)",
|
||||
opencode: "(Recommended)",
|
||||
anthropic: "(API key)",
|
||||
openai: "(ChatGPT Plus/Pro or API key)",
|
||||
@ -237,6 +239,19 @@ function ApiMethod(props: ApiMethodProps) {
|
||||
placeholder="API key"
|
||||
description={
|
||||
{
|
||||
toothfairyai: (
|
||||
<box gap={1}>
|
||||
<text fg={theme.textMuted}>
|
||||
ToothFairyAI gives you access to AI coding models through your workspace credentials.
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
Set credentials via <span style={{ fg: theme.primary }}>tfcode setup</span> or environment variables
|
||||
</text>
|
||||
<text fg={theme.textMuted}>
|
||||
TF_WORKSPACE_ID, TF_API_KEY, TF_REGION (dev/au/eu/us)
|
||||
</text>
|
||||
</box>
|
||||
),
|
||||
opencode: (
|
||||
<box gap={1}>
|
||||
<text fg={theme.textMuted}>
|
||||
|
||||
@ -67,7 +67,7 @@ const TIPS = [
|
||||
"Press {highlight}Ctrl+X X{/highlight} or {highlight}/export{/highlight} to save the conversation as Markdown",
|
||||
"Press {highlight}Ctrl+X Y{/highlight} to copy the assistant's last message to clipboard",
|
||||
"Press {highlight}Ctrl+P{/highlight} to see all available actions and commands",
|
||||
"Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers",
|
||||
"Run {highlight}/connect{/highlight} to configure your ToothFairyAI provider",
|
||||
"The leader key is {highlight}Ctrl+X{/highlight}; combine with other keys for quick actions",
|
||||
"Press {highlight}F2{/highlight} to quickly switch between recently used models",
|
||||
"Press {highlight}Ctrl+X B{/highlight} to show/hide the sidebar panel",
|
||||
|
||||
74
packages/tfcode/src/cli/cmd/tui/ui/dialog-changelog.tsx
Normal file
74
packages/tfcode/src/cli/cmd/tui/ui/dialog-changelog.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { useDialog } from "./dialog"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
import { For } from "solid-js"
|
||||
|
||||
interface ChangelogEntry {
|
||||
version: string
|
||||
date: string
|
||||
changes: string[]
|
||||
}
|
||||
|
||||
const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
version: "1.0.0-beta",
|
||||
date: "2026-03-24",
|
||||
changes: [
|
||||
"Initial tfcode release based on opencode",
|
||||
"Custom TF CODE logo with teal branding",
|
||||
"ToothFairyAI workspace integration",
|
||||
"Sync tools from ToothFairyAI workspace",
|
||||
"TF coder agents appear in /agents",
|
||||
"Commands: tfcode sync, tfcode validate, tfcode tools list",
|
||||
"Support for dev, au, eu, us regions",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function DialogChangelog() {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.name === "return" || evt.name === "escape") {
|
||||
dialog.clear()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
tfcode Changelog
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc/enter
|
||||
</text>
|
||||
</box>
|
||||
<box flexDirection="column" gap={1}>
|
||||
<For each={CHANGELOG}>
|
||||
{(release) => (
|
||||
<box flexDirection="column" gap={0}>
|
||||
<text fg={theme.primary} attributes={TextAttributes.BOLD}>
|
||||
v{release.version} ({release.date})
|
||||
</text>
|
||||
<For each={release.changes}>
|
||||
{(change) => (
|
||||
<text fg={theme.textMuted}>
|
||||
{" "}• {change}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
|
||||
<box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={() => dialog.clear()}>
|
||||
<text fg={theme.selectedListItemText}>ok</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@ -15,6 +15,13 @@ export namespace ModelsDev {
|
||||
const log = Log.create({ service: "models.dev" })
|
||||
const filepath = path.join(Global.Path.cache, "models.json")
|
||||
|
||||
const REGION_URLS: Record<string, string> = {
|
||||
dev: "https://ai.toothfairylab.link",
|
||||
au: "https://ai.toothfairyai.com",
|
||||
eu: "https://ai.eu.toothfairyai.com",
|
||||
us: "https://ai.us.toothfairyai.com",
|
||||
}
|
||||
|
||||
export const Model = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
@ -81,6 +88,21 @@ export namespace ModelsDev {
|
||||
|
||||
export type Provider = z.infer<typeof Provider>
|
||||
|
||||
interface TFModel {
|
||||
name: string
|
||||
provider: string
|
||||
modelType: string
|
||||
reasoner: boolean
|
||||
supportsVision: boolean
|
||||
toolCalling: boolean
|
||||
maxTokens: number
|
||||
deprecated: boolean
|
||||
pricing?: {
|
||||
inputPer1mTokens: number
|
||||
outputPer1mTokens: number
|
||||
}
|
||||
}
|
||||
|
||||
function url() {
|
||||
return Flag.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
}
|
||||
@ -100,7 +122,122 @@ export namespace ModelsDev {
|
||||
|
||||
export async function get() {
|
||||
const result = await Data()
|
||||
return result as Record<string, Provider>
|
||||
const providers = result as Record<string, Provider>
|
||||
|
||||
// Try to fetch ToothFairyAI models dynamically
|
||||
// First check env vars, then stored credentials
|
||||
let tfApiKey = process.env.TF_API_KEY
|
||||
let tfRegion = process.env.TF_REGION || "au"
|
||||
|
||||
// Try to load from stored credentials
|
||||
if (!tfApiKey) {
|
||||
try {
|
||||
const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json")
|
||||
const credData = await Bun.file(credPath).json() as { api_key?: string; region?: string }
|
||||
if (credData.api_key) {
|
||||
tfApiKey = credData.api_key
|
||||
tfRegion = credData.region || "au"
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const tfBaseUrl = REGION_URLS[tfRegion] || REGION_URLS.au
|
||||
|
||||
if (tfApiKey) {
|
||||
try {
|
||||
const tfResponse = await fetch(`${tfBaseUrl}/models_list`, {
|
||||
headers: {
|
||||
"x-api-key": tfApiKey,
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
|
||||
if (tfResponse.ok) {
|
||||
const tfData = await tfResponse.json() as { templates: Record<string, TFModel> }
|
||||
const tfModels: Record<string, Model> = {}
|
||||
|
||||
for (const [key, model] of Object.entries(tfData.templates || {})) {
|
||||
if (model.deprecated) continue
|
||||
|
||||
const modelId = key.startsWith("z/") ? key.slice(2) : key
|
||||
|
||||
tfModels[modelId] = {
|
||||
id: modelId,
|
||||
name: model.name,
|
||||
family: model.provider,
|
||||
release_date: new Date().toISOString().split("T")[0],
|
||||
attachment: model.supportsVision || false,
|
||||
reasoning: model.reasoner || false,
|
||||
temperature: true,
|
||||
tool_call: model.toolCalling || false,
|
||||
options: {},
|
||||
cost: {
|
||||
input: model.pricing?.inputPer1mTokens || 0,
|
||||
output: model.pricing?.outputPer1mTokens || 0,
|
||||
},
|
||||
limit: {
|
||||
context: model.maxTokens ? model.maxTokens * 4 : 128000,
|
||||
output: model.maxTokens || 16000,
|
||||
},
|
||||
modalities: {
|
||||
input: model.supportsVision ? ["text", "image"] : ["text"],
|
||||
output: ["text"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
providers["toothfairyai"] = {
|
||||
id: "toothfairyai",
|
||||
name: "ToothFairyAI",
|
||||
env: ["TF_API_KEY", "TF_WORKSPACE_ID"],
|
||||
models: tfModels,
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("Failed to fetch ToothFairyAI models", { error: e })
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to static models if dynamic fetch failed
|
||||
if (!providers["toothfairyai"] || Object.keys(providers["toothfairyai"].models).length === 0) {
|
||||
providers["toothfairyai"] = {
|
||||
id: "toothfairyai",
|
||||
name: "ToothFairyAI",
|
||||
env: ["TF_API_KEY", "TF_WORKSPACE_ID"],
|
||||
models: {
|
||||
"sorcerer": {
|
||||
id: "sorcerer",
|
||||
name: "TF Sorcerer",
|
||||
family: "groq",
|
||||
release_date: "2025-01-01",
|
||||
attachment: true,
|
||||
reasoning: false,
|
||||
temperature: true,
|
||||
tool_call: true,
|
||||
options: {},
|
||||
cost: { input: 0.62, output: 1.75 },
|
||||
limit: { context: 128000, output: 16000 },
|
||||
modalities: { input: ["text", "image"], output: ["text"] },
|
||||
},
|
||||
"mystica": {
|
||||
id: "mystica",
|
||||
name: "TF Mystica",
|
||||
family: "fireworks",
|
||||
release_date: "2025-01-01",
|
||||
attachment: true,
|
||||
reasoning: false,
|
||||
temperature: true,
|
||||
tool_call: true,
|
||||
options: {},
|
||||
cost: { input: 2.5, output: 7.5 },
|
||||
limit: { context: 128000, output: 16000 },
|
||||
modalities: { input: ["text", "image"], output: ["text"] },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
export async function refresh() {
|
||||
|
||||
@ -182,6 +182,29 @@ export namespace Provider {
|
||||
options: hasKey ? {} : { apiKey: "public" },
|
||||
}
|
||||
},
|
||||
async toothfairyai(input) {
|
||||
const hasCredentials = await (async () => {
|
||||
const env = Env.all()
|
||||
if (env.TF_API_KEY) return true
|
||||
if (await Auth.get(input.id)) return true
|
||||
const config = await Config.get()
|
||||
if (config.provider?.["toothfairyai"]?.options?.apiKey) return true
|
||||
|
||||
// Check stored credentials file
|
||||
try {
|
||||
const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json")
|
||||
const credData = await Bun.file(credPath).json() as { api_key?: string }
|
||||
if (credData.api_key) return true
|
||||
} catch {}
|
||||
|
||||
return false
|
||||
})()
|
||||
|
||||
return {
|
||||
autoload: hasCredentials,
|
||||
options: hasCredentials ? {} : { apiKey: "setup-required" },
|
||||
}
|
||||
},
|
||||
openai: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
|
||||
@ -12,6 +12,7 @@ export const ProviderID = providerIdSchema.pipe(
|
||||
make: (id: string) => schema.makeUnsafe(id),
|
||||
zod: z.string().pipe(z.custom<ProviderID>()),
|
||||
// Well-known providers
|
||||
toothfairyai: schema.makeUnsafe("toothfairyai"),
|
||||
opencode: schema.makeUnsafe("opencode"),
|
||||
anthropic: schema.makeUnsafe("anthropic"),
|
||||
openai: schema.makeUnsafe("openai"),
|
||||
|
||||
19
packages/tfcode/test-creds.ts
Normal file
19
packages/tfcode/test-creds.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Global } from "./src/global"
|
||||
import path from "path"
|
||||
import { ModelsDev } from "./src/provider/models"
|
||||
|
||||
console.log("Global.Path.data:", Global.Path.data)
|
||||
const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json")
|
||||
console.log("Credentials path:", credPath)
|
||||
|
||||
try {
|
||||
const credData = await Bun.file(credPath).json()
|
||||
console.log("Credentials loaded:", { api_key: (credData as any).api_key?.slice(0,10)+"...", region: (credData as any).region })
|
||||
} catch (e) {
|
||||
console.log("Error loading credentials:", e)
|
||||
}
|
||||
|
||||
const providers = await ModelsDev.get()
|
||||
console.log("ToothFairyAI provider found:", !!providers["toothfairyai"])
|
||||
console.log("Models count:", Object.keys(providers["toothfairyai"]?.models || {}).length)
|
||||
console.log("Sample models:", Object.keys(providers["toothfairyai"]?.models || {}).slice(0, 5))
|
||||
Loading…
x
Reference in New Issue
Block a user