Spaces:
Running
Running
File size: 7,758 Bytes
85a0eea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | # =============================================================================
# app/tools.py
# Tool Registry — Modular Wrapper
# Universal MCP Hub (Sandboxed) - based on PyFundaments Architecture
# Copyright 2026 - Volkan Kücükbudak
# Apache License V. 2 + ESOL 1.1
# Repo: https://github.com/VolkanSah/Universal-MCP-Hub-sandboxed
# =============================================================================
# ARCHITECTURE NOTE:
# This file lives exclusively in app/ and is ONLY started by app/app.py.
# NO direct access to fundaments/*, .env, or Guardian (main.py).
# All config comes from app/.pyfun via app/config.py.
#
# TOOL REGISTRY PRINCIPLE:
# Tools are defined in .pyfun [TOOLS] — never hardcoded here.
# Adding a new tool = update .pyfun only. Never touch this file.
# config.py parses [TOOLS] and delivers ready-to-use dicts.
#
# DEPENDENCY CHAIN:
# .pyfun → config.py → tools.py → mcp.py
# tools.py delegates execution to providers.py — never calls APIs directly.
# =============================================================================
import logging
import os
from typing import Any, Dict, Optional
from . import config # reads app/.pyfun — single source of truth
from . import providers # LLM + Search execution + fallback chain
logger = logging.getLogger("tools")
# =============================================================================
# Internal Registry — built from .pyfun [TOOLS] at initialize()
# =============================================================================
_registry: Dict[str, Dict] = {}
# =============================================================================
# Initialization — called by app/app.py (parameterless, sandboxed)
# =============================================================================
def initialize() -> None:
"""
Builds the tool registry from .pyfun [TOOLS].
Called once by app/app.py during startup sequence.
No fundaments passed in — fully sandboxed.
Loads all active tools and their config (description, provider_type,
default_provider, timeout_sec, system_prompt, etc.) into _registry.
Inactive tools (active = "false") are skipped silently.
"""
global _registry
_registry = config.get_active_tools()
logger.info(f"Tools loaded: {list(_registry.keys())}")
# =============================================================================
# Public API — used by mcp.py tool handlers
# =============================================================================
async def run(
tool_name: str,
prompt: str,
provider_name: Optional[str] = None,
model: Optional[str] = None,
max_tokens: int = 1024,
) -> str:
"""
Execute a tool by name.
Reads tool config from registry, delegates to providers.py.
Applies system_prompt from .pyfun if defined.
Args:
tool_name: Tool name as defined in .pyfun [TOOLS] (e.g. 'llm_complete').
prompt: User input / query string.
provider_name: Override provider. Defaults to tool's default_provider in .pyfun.
model: Override model. Defaults to provider's default_model in .pyfun.
max_tokens: Max tokens for LLM response. Default: 1024.
Returns:
Tool response as plain text string.
Raises:
ValueError: If tool_name is not found in registry.
RuntimeError: If all providers fail (propagated from providers.py).
"""
tool_cfg = _registry.get(tool_name)
if not tool_cfg:
raise ValueError(f"Tool '{tool_name}' not found in registry or not active.")
provider_type = tool_cfg.get("provider_type", "llm")
default_provider = provider_name or tool_cfg.get("default_provider", "")
system_prompt = tool_cfg.get("system_prompt", "")
# Build full prompt — prepend system_prompt if defined in .pyfun
full_prompt = f"{system_prompt}\n\n{prompt}".strip() if system_prompt else prompt
# --- LLM tools ---
if provider_type == "llm":
return await providers.llm_complete(
prompt=full_prompt,
provider_name=default_provider,
model=model,
max_tokens=max_tokens,
)
# --- Search tools ---
if provider_type == "search":
return await providers.search(
query=prompt,
provider_name=default_provider,
max_results=int(tool_cfg.get("default_results", "5")),
)
# --- DB tools (read-only, delegated to db_sync when ready) ---
if provider_type == "db":
# db_sync not yet implemented — return informative message
logger.info("db_query tool called — db_sync.py not yet active.")
return "Database query tool is not yet active. Configure db_sync.py first."
# --- Unknown provider type ---
logger.warning(f"Tool '{tool_name}' has unknown provider_type '{provider_type}' — skipped.")
return f"Tool '{tool_name}' provider type '{provider_type}' is not yet implemented."
# =============================================================================
# Registry helpers — used by mcp.py and system tools
# =============================================================================
def get(tool_name: str) -> Dict[str, Any]:
"""
Get full config dict for a tool.
Args:
tool_name: Tool name as defined in .pyfun [TOOLS].
Returns:
Tool config dict, or empty dict if not found.
"""
return _registry.get(tool_name, {})
def get_description(tool_name: str) -> str:
"""
Get the description of a tool (from .pyfun).
Args:
tool_name: Tool name as defined in .pyfun [TOOLS].
Returns:
Description string, or empty string if not found.
"""
return _registry.get(tool_name, {}).get("description", "")
def get_system_prompt(tool_name: str) -> str:
"""
Get the system_prompt of a tool (from .pyfun).
Returns empty string if no system_prompt is defined.
Args:
tool_name: Tool name as defined in .pyfun [TOOLS].
Returns:
System prompt string, or empty string if not configured.
"""
return _registry.get(tool_name, {}).get("system_prompt", "")
def get_timeout(tool_name: str) -> int:
"""
Get the timeout in seconds for a tool (from .pyfun).
Args:
tool_name: Tool name as defined in .pyfun [TOOLS].
Returns:
Timeout in seconds (int). Defaults to 60 if not configured.
"""
return int(_registry.get(tool_name, {}).get("timeout_sec", "60"))
def get_provider_type(tool_name: str) -> str:
"""
Get the provider_type of a tool (llm | search | db | image | sandbox).
Args:
tool_name: Tool name as defined in .pyfun [TOOLS].
Returns:
Provider type string, or empty string if not found.
"""
return _registry.get(tool_name, {}).get("provider_type", "")
def list_all() -> list:
"""
List all active tool names from registry.
Returns:
List of active tool name strings.
"""
return list(_registry.keys())
def list_by_type(provider_type: str) -> list:
"""
List all active tools of a specific provider_type.
Args:
provider_type: e.g. 'llm', 'search', 'db', 'image', 'sandbox'.
Returns:
List of tool name strings matching the provider_type.
"""
return [
name for name, cfg in _registry.items()
if cfg.get("provider_type", "") == provider_type
]
# =============================================================================
# Direct execution guard
# =============================================================================
if __name__ == "__main__":
print("WARNING: Run via main.py → app.py, not directly.")
|