mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-09 10:18:57 +00:00
feat: initialize tfcode project structure
- Add README.md as living documentation - Add tfcode.json schema and config template - Add FORK_MANAGEMENT.md with mirror-based fork strategy - Add scripts/rebrand.sh for reapplying branding after upstream merges - Add packages/tf-sync Python module using official ToothFairyAI SDK - Add packages/tf-mcp-bridge TypeScript module (stub) - Multi-region support (AU, EU, US) - Tool sync: MCP servers, Agent Skills, Database Scripts, API Functions
This commit is contained in:
21
packages/tf-sync/src/tf_sync/__init__.py
Normal file
21
packages/tf-sync/src/tf_sync/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
tf-sync: ToothFairyAI workspace sync layer for tfcode
|
||||
"""
|
||||
|
||||
from tf_sync.agents import sync_agents
|
||||
from tf_sync.mcp import sync_mcp_servers
|
||||
from tf_sync.tools import sync_tools, ToolType
|
||||
from tf_sync.config import TFConfig, load_config, validate_credentials, get_region_urls
|
||||
|
||||
__all__ = [
|
||||
"sync_agents",
|
||||
"sync_mcp_servers",
|
||||
"sync_tools",
|
||||
"ToolType",
|
||||
"TFConfig",
|
||||
"load_config",
|
||||
"validate_credentials",
|
||||
"get_region_urls",
|
||||
]
|
||||
|
||||
__version__ = "0.1.0"
|
||||
39
packages/tf-sync/src/tf_sync/agents.py
Normal file
39
packages/tf-sync/src/tf_sync/agents.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Agent sync module for tfcode.
|
||||
|
||||
NOTE: This module is reserved for future implementation.
|
||||
Currently, tfcode only syncs tools (MCP, Skills, Database, Functions).
|
||||
Agent sync will be added in a later phase.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from tf_sync.config import TFConfig
|
||||
|
||||
|
||||
class AgentSyncResult(BaseModel):
|
||||
"""Result of agent sync operation."""
|
||||
|
||||
success: bool
|
||||
agents: list[dict[str, Any]] = []
|
||||
error: str | None = None
|
||||
|
||||
|
||||
def sync_agents(config: TFConfig) -> AgentSyncResult:
|
||||
"""
|
||||
Sync agents from ToothFairyAI workspace.
|
||||
|
||||
NOTE: Currently not implemented. Reserved for future use.
|
||||
|
||||
Args:
|
||||
config: TFConfig instance
|
||||
|
||||
Returns:
|
||||
AgentSyncResult (currently always returns not implemented)
|
||||
"""
|
||||
return AgentSyncResult(
|
||||
success=False,
|
||||
error="Agent sync not yet implemented. Use tools sync for now.",
|
||||
)
|
||||
211
packages/tf-sync/src/tf_sync/config.py
Normal file
211
packages/tf-sync/src/tf_sync/config.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Configuration management for tfcode ToothFairyAI integration.
|
||||
Uses the official ToothFairyAI Python SDK for multi-region support.
|
||||
"""
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, SecretStr
|
||||
from toothfairyai import ToothFairyClient
|
||||
from toothfairyai.errors import ToothFairyError
|
||||
|
||||
|
||||
class Region(str, Enum):
|
||||
AU = "au"
|
||||
EU = "eu"
|
||||
US = "us"
|
||||
|
||||
|
||||
class ToolType(str, Enum):
|
||||
MCP_SERVER = "mcp_server"
|
||||
AGENT_SKILL = "agent_skill"
|
||||
DATABASE_SCRIPT = "database_script"
|
||||
API_FUNCTION = "api_function"
|
||||
|
||||
|
||||
class FunctionRequestType(str, Enum):
|
||||
GET = "get"
|
||||
POST = "post"
|
||||
PUT = "put"
|
||||
DELETE = "delete"
|
||||
PATCH = "patch"
|
||||
CUSTOM = "custom"
|
||||
GRAPHQL_QUERY = "graphql_query"
|
||||
GRAPHQL_MUTATION = "graphql_mutation"
|
||||
|
||||
|
||||
# Region-specific URL configurations
|
||||
REGION_URLS = {
|
||||
Region.AU: {
|
||||
"base_url": "https://api.au.toothfairyai.com",
|
||||
"ai_url": "https://ai.au.toothfairyai.com",
|
||||
"ai_stream_url": "https://ais.au.toothfairyai.com",
|
||||
"mcp_url": "https://mcp.au.toothfairyai.com/sse",
|
||||
"mcp_proxy_url": "https://mcp-proxy.au.toothfairyai.com",
|
||||
},
|
||||
Region.EU: {
|
||||
"base_url": "https://api.eu.toothfairyai.com",
|
||||
"ai_url": "https://ai.eu.toothfairyai.com",
|
||||
"ai_stream_url": "https://ais.eu.toothfairyai.com",
|
||||
"mcp_url": "https://mcp.eu.toothfairyai.com/sse",
|
||||
"mcp_proxy_url": "https://mcp-proxy.eu.toothfairyai.com",
|
||||
},
|
||||
Region.US: {
|
||||
"base_url": "https://api.us.toothfairyai.com",
|
||||
"ai_url": "https://ai.us.toothfairyai.com",
|
||||
"ai_stream_url": "https://ais.us.toothfairyai.com",
|
||||
"mcp_url": "https://mcp.us.toothfairyai.com/sse",
|
||||
"mcp_proxy_url": "https://mcp-proxy.us.toothfairyai.com",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_region_urls(region: Region) -> dict[str, str]:
|
||||
"""Get URLs for a specific region."""
|
||||
return REGION_URLS.get(region, REGION_URLS[Region.AU])
|
||||
|
||||
|
||||
class TFConfig(BaseModel):
|
||||
"""ToothFairyAI workspace configuration."""
|
||||
|
||||
workspace_id: str
|
||||
api_key: SecretStr
|
||||
region: Region = Region.AU
|
||||
enabled: bool = True
|
||||
|
||||
sync_interval: int = Field(default=3600, ge=60)
|
||||
mcp_proxy_timeout: int = Field(default=30000, ge=1000)
|
||||
|
||||
_client: Optional[ToothFairyClient] = None
|
||||
|
||||
def get_client(self) -> ToothFairyClient:
|
||||
"""
|
||||
Get or create a ToothFairyClient instance configured for this region.
|
||||
|
||||
Returns:
|
||||
ToothFairyClient configured with region-specific URLs
|
||||
"""
|
||||
if self._client is None:
|
||||
urls = get_region_urls(self.region)
|
||||
self._client = ToothFairyClient(
|
||||
api_key=self.api_key.get_secret_value(),
|
||||
workspace_id=self.workspace_id,
|
||||
base_url=urls["base_url"],
|
||||
ai_url=urls["ai_url"],
|
||||
ai_stream_url=urls["ai_stream_url"],
|
||||
)
|
||||
return self._client
|
||||
|
||||
@property
|
||||
def mcp_sse_url(self) -> str:
|
||||
"""Get the MCP SSE endpoint URL for this region."""
|
||||
return get_region_urls(self.region)["mcp_url"]
|
||||
|
||||
@property
|
||||
def mcp_proxy_url(self) -> str:
|
||||
"""Get the MCP proxy URL for this region."""
|
||||
return get_region_urls(self.region)["mcp_proxy_url"]
|
||||
|
||||
|
||||
class CredentialValidationResult(BaseModel):
|
||||
"""Result of credential validation."""
|
||||
success: bool
|
||||
workspace_id: Optional[str] = None
|
||||
workspace_name: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
def load_config(
|
||||
workspace_id: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
region: Optional[Region] = None,
|
||||
) -> TFConfig:
|
||||
"""
|
||||
Load ToothFairyAI configuration from environment or parameters.
|
||||
|
||||
Args:
|
||||
workspace_id: Workspace UUID (defaults to TF_WORKSPACE_ID env var)
|
||||
api_key: API key (defaults to TF_API_KEY env var)
|
||||
region: Region (defaults to TF_REGION env var or 'au')
|
||||
|
||||
Returns:
|
||||
TFConfig instance
|
||||
|
||||
Raises:
|
||||
ValueError: If required configuration is missing
|
||||
"""
|
||||
ws_id = workspace_id or os.environ.get("TF_WORKSPACE_ID")
|
||||
key = api_key or os.environ.get("TF_API_KEY")
|
||||
|
||||
# Parse region from env or use provided/default
|
||||
region_str = os.environ.get("TF_REGION", "au")
|
||||
reg = region or Region(region_str)
|
||||
|
||||
if not ws_id:
|
||||
raise ValueError("TF_WORKSPACE_ID not set. Set environment variable or pass workspace_id.")
|
||||
if not key:
|
||||
raise ValueError("TF_API_KEY not set. Set environment variable or pass api_key.")
|
||||
|
||||
return TFConfig(
|
||||
workspace_id=ws_id,
|
||||
api_key=SecretStr(key),
|
||||
region=reg,
|
||||
)
|
||||
|
||||
|
||||
def validate_credentials(config: TFConfig) -> CredentialValidationResult:
|
||||
"""
|
||||
Validate ToothFairyAI credentials using the SDK.
|
||||
|
||||
Args:
|
||||
config: TFConfig instance
|
||||
|
||||
Returns:
|
||||
CredentialValidationResult indicating success or failure
|
||||
"""
|
||||
try:
|
||||
client = config.get_client()
|
||||
|
||||
# Test connection by listing chats (lightweight operation)
|
||||
if client.test_connection():
|
||||
return CredentialValidationResult(
|
||||
success=True,
|
||||
workspace_id=config.workspace_id,
|
||||
workspace_name="Connected",
|
||||
)
|
||||
else:
|
||||
return CredentialValidationResult(
|
||||
success=False,
|
||||
error="Connection test failed. Check credentials and region.",
|
||||
)
|
||||
|
||||
except ToothFairyError as e:
|
||||
error_msg = str(e)
|
||||
|
||||
if "401" in error_msg or "Unauthorized" in error_msg:
|
||||
return CredentialValidationResult(
|
||||
success=False,
|
||||
error="Invalid API key. Check TF_API_KEY environment variable.",
|
||||
)
|
||||
elif "403" in error_msg or "Forbidden" in error_msg:
|
||||
return CredentialValidationResult(
|
||||
success=False,
|
||||
error="API access not allowed. Business or Enterprise subscription required.",
|
||||
)
|
||||
elif "404" in error_msg or "Not Found" in error_msg:
|
||||
return CredentialValidationResult(
|
||||
success=False,
|
||||
error="Workspace not found. Check TF_WORKSPACE_ID environment variable.",
|
||||
)
|
||||
else:
|
||||
return CredentialValidationResult(
|
||||
success=False,
|
||||
error=f"API error: {error_msg}",
|
||||
)
|
||||
except Exception as e:
|
||||
return CredentialValidationResult(
|
||||
success=False,
|
||||
error=f"Unexpected error: {str(e)}",
|
||||
)
|
||||
54
packages/tf-sync/src/tf_sync/mcp.py
Normal file
54
packages/tf-sync/src/tf_sync/mcp.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
MCP server sync module for tfcode.
|
||||
Uses the official ToothFairyAI Python SDK.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from tf_sync.config import TFConfig
|
||||
from tf_sync.tools import SyncedTool, sync_tools, ToolType
|
||||
|
||||
|
||||
class MCPServerSyncResult(BaseModel):
|
||||
"""Result of MCP server sync operation."""
|
||||
|
||||
success: bool
|
||||
servers: list[SyncedTool] = []
|
||||
error: str | None = None
|
||||
|
||||
|
||||
def sync_mcp_servers(config: TFConfig) -> MCPServerSyncResult:
|
||||
"""
|
||||
Sync MCP servers from ToothFairyAI workspace.
|
||||
|
||||
MCP servers are tools with isMCPServer=true.
|
||||
Credentials stay in TF and are accessed via tf_proxy.
|
||||
|
||||
Args:
|
||||
config: TFConfig instance
|
||||
|
||||
Returns:
|
||||
MCPServerSyncResult with synced MCP servers
|
||||
"""
|
||||
result = sync_tools_by_type(config, [ToolType.MCP_SERVER])
|
||||
|
||||
if not result.success:
|
||||
return MCPServerSyncResult(
|
||||
success=False,
|
||||
error=result.error,
|
||||
)
|
||||
|
||||
# Get MCP servers from tools with isMCPServer
|
||||
mcp_servers = [
|
||||
t for t in result.tools
|
||||
if t.is_mcp_server
|
||||
]
|
||||
|
||||
return MCPServerSyncResult(
|
||||
success=True,
|
||||
servers=mcp_servers,
|
||||
)
|
||||
|
||||
|
||||
# Import from tools module
|
||||
from tf_sync.tools import sync_tools_by_type
|
||||
185
packages/tf-sync/src/tf_sync/tools.py
Normal file
185
packages/tf-sync/src/tf_sync/tools.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Tool sync module for tfcode.
|
||||
Syncs MCP servers, Agent Skills, Database Scripts, and API Functions from ToothFairyAI workspace.
|
||||
Uses the official ToothFairyAI Python SDK.
|
||||
"""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from toothfairyai.types import AgentFunction
|
||||
|
||||
from tf_sync.config import TFConfig, ToolType, FunctionRequestType
|
||||
|
||||
|
||||
class SyncedTool(BaseModel):
|
||||
"""A tool synced from ToothFairyAI workspace."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
tool_type: ToolType
|
||||
|
||||
is_mcp_server: bool = False
|
||||
is_agent_skill: bool = False
|
||||
is_database_script: bool = False
|
||||
|
||||
request_type: Optional[FunctionRequestType] = None
|
||||
url: Optional[str] = None
|
||||
tools: list[str] = []
|
||||
|
||||
auth_via: str = "tf_proxy"
|
||||
|
||||
|
||||
class ToolSyncResult(BaseModel):
|
||||
"""Result of tool sync operation."""
|
||||
|
||||
success: bool
|
||||
tools: list[SyncedTool] = []
|
||||
by_type: dict[str, int] = {}
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
def classify_tool(tool: AgentFunction) -> ToolType:
|
||||
"""
|
||||
Classify a tool based on its flags and fields.
|
||||
|
||||
Args:
|
||||
tool: AgentFunction from TF SDK
|
||||
|
||||
Returns:
|
||||
ToolType enum value
|
||||
"""
|
||||
if tool.is_mcp_server:
|
||||
return ToolType.MCP_SERVER
|
||||
if tool.is_agent_skill:
|
||||
return ToolType.AGENT_SKILL
|
||||
if tool.is_database_script:
|
||||
return ToolType.DATABASE_SCRIPT
|
||||
if tool.request_type:
|
||||
return ToolType.API_FUNCTION
|
||||
|
||||
return ToolType.API_FUNCTION
|
||||
|
||||
|
||||
def parse_tool(tool: AgentFunction) -> SyncedTool:
|
||||
"""
|
||||
Parse AgentFunction from SDK into SyncedTool.
|
||||
|
||||
Args:
|
||||
tool: AgentFunction from TF SDK
|
||||
|
||||
Returns:
|
||||
SyncedTool instance
|
||||
"""
|
||||
tool_type = classify_tool(tool)
|
||||
|
||||
request_type_enum = None
|
||||
if tool.request_type:
|
||||
try:
|
||||
request_type_enum = FunctionRequestType(tool.request_type)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
auth_via = "user_provided" if tool_type == ToolType.API_FUNCTION else "tf_proxy"
|
||||
|
||||
return SyncedTool(
|
||||
id=tool.id,
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
tool_type=tool_type,
|
||||
is_mcp_server=tool.is_mcp_server or False,
|
||||
is_agent_skill=tool.is_agent_skill or False,
|
||||
is_database_script=tool.is_database_script or False,
|
||||
request_type=request_type_enum,
|
||||
url=tool.url,
|
||||
tools=[],
|
||||
auth_via=auth_via,
|
||||
)
|
||||
|
||||
|
||||
def sync_tools(config: TFConfig) -> ToolSyncResult:
|
||||
"""
|
||||
Sync all tools from ToothFairyAI workspace using SDK.
|
||||
|
||||
Args:
|
||||
config: TFConfig instance
|
||||
|
||||
Returns:
|
||||
ToolSyncResult with synced tools
|
||||
"""
|
||||
try:
|
||||
client = config.get_client()
|
||||
result = client.agent_functions.list()
|
||||
|
||||
tools = [parse_tool(f) for f in result.items]
|
||||
|
||||
by_type = {}
|
||||
for tool in tools:
|
||||
type_name = tool.tool_type.value
|
||||
by_type[type_name] = by_type.get(type_name, 0) + 1
|
||||
|
||||
return ToolSyncResult(
|
||||
success=True,
|
||||
tools=tools,
|
||||
by_type=by_type,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ToolSyncResult(
|
||||
success=False,
|
||||
error=f"Sync failed: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
def sync_tools_by_type(
|
||||
config: TFConfig,
|
||||
tool_types: Optional[list[ToolType]] = None,
|
||||
) -> ToolSyncResult:
|
||||
"""
|
||||
Sync tools of specific types from ToothFairyAI workspace.
|
||||
|
||||
Args:
|
||||
config: TFConfig instance
|
||||
tool_types: List of ToolType to sync (None = all)
|
||||
|
||||
Returns:
|
||||
ToolSyncResult with filtered tools
|
||||
"""
|
||||
result = sync_tools(config)
|
||||
|
||||
if not result.success or not tool_types:
|
||||
return result
|
||||
|
||||
filtered = [t for t in result.tools if t.tool_type in tool_types]
|
||||
|
||||
by_type = {}
|
||||
for tool in filtered:
|
||||
type_name = tool.tool_type.value
|
||||
by_type[type_name] = by_type.get(type_name, 0) + 1
|
||||
|
||||
return ToolSyncResult(
|
||||
success=True,
|
||||
tools=filtered,
|
||||
by_type=by_type,
|
||||
)
|
||||
|
||||
|
||||
def sync_mcp_servers_only(config: TFConfig) -> ToolSyncResult:
|
||||
"""Sync only MCP servers (isMCPServer=true)."""
|
||||
return sync_tools_by_type(config, [ToolType.MCP_SERVER])
|
||||
|
||||
|
||||
def sync_agent_skills_only(config: TFConfig) -> ToolSyncResult:
|
||||
"""Sync only Agent Skills (isAgentSkill=true)."""
|
||||
return sync_tools_by_type(config, [ToolType.AGENT_SKILL])
|
||||
|
||||
|
||||
def sync_database_scripts_only(config: TFConfig) -> ToolSyncResult:
|
||||
"""Sync only Database Scripts (isDatabaseScript=true)."""
|
||||
return sync_tools_by_type(config, [ToolType.DATABASE_SCRIPT])
|
||||
|
||||
|
||||
def sync_api_functions_only(config: TFConfig) -> ToolSyncResult:
|
||||
"""Sync only API Functions (has requestType)."""
|
||||
return sync_tools_by_type(config, [ToolType.API_FUNCTION])
|
||||
Reference in New Issue
Block a user