Alibrown's picture
Upload 15 files
85a0eea verified
# =============================================================================
# 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.")