From fcc86b52c2efcbc47fa1fda16a09c2fdea158fe0 Mon Sep 17 00:00:00 2001 From: eweeman Date: Thu, 15 Jan 2026 17:01:02 -0800 Subject: [PATCH] Tools is broken --- .env | 2 + bots/deafult_bot.py | 25 ++- llm/local_llm_client.py | 205 ++++++++++++------ message_processor.py | 172 +++++++++++++-- requirements.txt | 1 + scripts/discover_llm_endpoint.py | 120 ++++++++++ scripts/test_llm_endpoint.py | 45 ++++ tool_loader.py | 68 ++++-- ...ter.py => generate_mikrotik_CPE_script.py} | 12 + tools/imail_tool.py | 8 + tools/simple_calculator.py | 47 ++++ 11 files changed, 596 insertions(+), 109 deletions(-) create mode 100644 scripts/discover_llm_endpoint.py create mode 100644 scripts/test_llm_endpoint.py rename tools/{mtscripter.py => generate_mikrotik_CPE_script.py} (98%) create mode 100644 tools/simple_calculator.py diff --git a/.env b/.env index 08b5791..f670900 100644 --- a/.env +++ b/.env @@ -21,7 +21,9 @@ EMBEDDING_MODEL=all-MiniLM-L6-v2 # Local LLM (Remote Machine) ######################### LOCAL_LLM_ENDPOINT=http://172.168.10.10:11434/v1/chat/completions +LLM_API_URL=http://api.chat.pathcore.org LOCAL_LLM_MODEL=mistral +LLM_MODEL=mistral LOCAL_LLM_TIMEOUT=60 ######################### diff --git a/bots/deafult_bot.py b/bots/deafult_bot.py index 310e86e..78e4ad3 100644 --- a/bots/deafult_bot.py +++ b/bots/deafult_bot.py @@ -1,9 +1,24 @@ BOT_IDENTIFIER = "default_bot" -SYSTEM_PROMPT = """You are a helpful AI assistant in Slack. -You answer questions clearly and concisely. -You are friendly and professional.""" +SYSTEM_PROMPT = """You are Abot, a helpful AI assistant for First Step Internet. +Your purpose in this channel is to generate Mikrotik CPE scripts using the associated tool. +Be friendly, concise, professional, technical. Provide instructions for how to use the tool (which inputs the user must provide). +Use the available tools (listed below) when needed. +Format your responses clearly. +Remember your Slack User ID is <@U08LF3N25QE>. +Today's date and the current channel ID are provided below for context. +""" -ENABLED_TOOL_NAMES = [] # Add tool names here as you create them +ENABLED_TOOL_NAMES = [ + "weather_tool", + "web_search", + "get_imail_password", + "generate_mikrotik_CPE_script", + "calculator", +] # Add tool names here as you create them -ENABLE_RAG_INSERT = False # Set to True to enable vector storage \ No newline at end of file +ENABLE_RAG_INSERT = False # Set to True to enable vector storage + +LLM_MODEL = "default" # Model name to use +LLM_TEMPERATURE = 0.7 # Creativity (0.0 to 2.0) +LLM_MAX_TOKENS = 1000 # Maximum response length \ No newline at end of file diff --git a/llm/local_llm_client.py b/llm/local_llm_client.py index 19299fc..4f1e7f1 100644 --- a/llm/local_llm_client.py +++ b/llm/local_llm_client.py @@ -1,84 +1,155 @@ import logging -from typing import Dict, Any -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError +import requests +import os +from typing import List, Dict, Any +import urllib3 + +# Disable SSL warnings (only for development with self-signed certs!) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) logger = logging.getLogger(__name__) +# Get LLM configuration from environment +LLM_API_URL = os.getenv("LLM_API_URL", "http://api.chat.pathcore.org") +LLM_API_KEY = os.getenv("LLM_API_KEY") # Optional API key +LLM_MODEL = os.getenv("LLM_MODEL", "llama3") # Default Ollama model -def process_mention( - event_data: dict, - slack_client: WebClient, - vector_store: Any, - bot_profile: Any, - tool_registry: Dict[str, Any] -) -> None: + +def chat_completion( + messages: List[Dict[str, Any]], + model: str = None, + temperature: float = 0.7, + max_tokens: int = 1000, + tools: List[Dict[str, Any]] | None = None, +) -> Dict[str, Any]: """ - Process messages that mention the bot. + Call Ollama API for chat completion. + + Args: + messages: List of message dicts with 'role' and 'content' + model: Model name to use (defaults to LLM_MODEL from env) + temperature: Sampling temperature (0.0 to 2.0) + max_tokens: Maximum tokens to generate + + Returns: + Dict with 'content' key containing the response text + + Raises: + Exception: If the API call fails """ - event = event_data.get("event", {}) - channel = event.get("channel") - user = event.get("user") - text = event.get("text", "") - ts = event.get("ts") # This is the message timestamp - logger.info(f"Processing mention from {user} in {channel}") + # Use provided model or fall back to env variable + model = model or LLM_MODEL - # Remove bot mention from text - from config import BOT_USER_ID - clean_text = text.replace(f"<@{BOT_USER_ID}>", "").strip() + # Ollama chat endpoint + url = f"{LLM_API_URL}/api/chat" - # Get bot configuration - bot_name = getattr(bot_profile, "BOT_IDENTIFIER", "Bot") + # Convert OpenAI-style messages to Ollama format + # Ollama expects messages in the same format, but we need to ensure proper structure + + logger.info(f"Calling Ollama API at {url} with model: {model}") + + payload = { + "model": model, + "messages": messages, + "stream": False, + "options": { + "temperature": temperature, + "num_predict": max_tokens, + } +} + +if tools: + payload["tools"] = tools + + headers = { + "Content-Type": "application/json" + } + + # Add API key if configured (Ollama doesn't usually need this, but just in case) + if LLM_API_KEY: + headers["Authorization"] = f"Bearer {LLM_API_KEY}" try: - # Try to get RAG context if enabled - rag_enabled = getattr(bot_profile, "ENABLE_RAG_INSERT", False) - context = "" - - if rag_enabled: - try: - # Search for similar messages - similar = vector_store.search_similar(clean_text, limit=3) - if similar: - context = "\nRelevant context:\n" + "\n".join(similar) - except AttributeError: - logger.warning("RAG retrieval failed: search_similar not implemented") - except Exception as e: - logger.error(f"RAG retrieval error: {e}") - - # TODO: Integrate with your LLM here - # For now, simple echo response - - response_text = f"You said: {clean_text}" - - if context: - response_text += f"\n{context}" - - # Send message to channel (NOT as a thread reply) - slack_client.chat_postMessage( - channel=channel, - text=response_text + resp = requests.post( + url, + json=payload, + headers=headers, + timeout=120, # Ollama can be slow on first request + verify=False ) - logger.info(f"Sent response to {channel}") + # Raise exception for HTTP errors + resp.raise_for_status() - except SlackApiError as e: - logger.error(f"Slack API error: {e.response['error']}", exc_info=True) - try: - slack_client.chat_postMessage( - channel=channel, - text="Sorry, I encountered a Slack API error." - ) - except: - pass + data = resp.json() + logger.debug(f"Ollama API response: {data}") + + # Extract the assistant's response from Ollama format + # Ollama returns: {"message": {"role": "assistant", "content": "..."}} + message = data.get("message", {}) + + content = message.get("content", "") + tool_calls = message.get("tool_calls", []) + + logger.info( + f"Ollama response received " + f"(content={len(content)} chars, tool_calls={len(tool_calls)})" + ) + + return { + "content": content, + "tool_calls": tool_calls, + "raw": data + } + except requests.exceptions.Timeout: + logger.error("Ollama API request timed out after 120 seconds") + raise Exception("LLM request timed out. The model might be loading for the first time.") + + except requests.exceptions.ConnectionError as e: + logger.error(f"Cannot connect to Ollama API: {e}") + raise Exception("Cannot connect to Ollama server. Is it running?") + + except requests.exceptions.HTTPError as e: + logger.error(f"HTTP error from Ollama API: {e}") + if e.response.status_code == 404: + raise Exception(f"Model '{model}' not found. Try: ollama pull {model}") + raise Exception(f"Ollama API error: {e}") + + except ValueError as e: + logger.error(f"Invalid response from Ollama API: {e}") + raise + except Exception as e: - logger.error(f"Error processing mention: {e}", exc_info=True) - try: - slack_client.chat_postMessage( - channel=channel, - text="⚠️ Sorry, I encountered an internal error." - ) - except: - pass \ No newline at end of file + logger.error(f"Unexpected error calling Ollama API: {e}", exc_info=True) + raise + + +def list_models(): + """ + List available Ollama models. + + Returns: + List of model names + """ + url = f"{LLM_API_URL}/api/tags" + + try: + resp = requests.get(url, timeout=10, verify=False) + resp.raise_for_status() + data = resp.json() + + if "models" in data: + models = [model["name"] for model in data["models"]] + logger.info(f"Available models: {models}") + return models + return [] + + except Exception as e: + logger.error(f"Error listing models: {e}") + return [] + + +# Make functions available for import +__all__ = ['chat_completion', 'list_models'] \ No newline at end of file diff --git a/message_processor.py b/message_processor.py index 6bc12e5..2ada666 100644 --- a/message_processor.py +++ b/message_processor.py @@ -1,20 +1,94 @@ import logging -from typing import Dict, Any +import json +from typing import Dict, Any, List from slack_sdk import WebClient from slack_sdk.errors import SlackApiError logger = logging.getLogger(__name__) -# Try to import LLM client, but don't fail if it's not available +# Try to import LLM client try: from llm.local_llm_client import chat_completion LLM_AVAILABLE = True -except ImportError: - logger.warning("LLM client not available, using simple echo mode") + logger.info("LLM client loaded successfully") +except ImportError as e: + logger.warning(f"LLM client not available: {e}") LLM_AVAILABLE = False chat_completion = None +def format_tools_for_llm(tool_registry: Dict[str, Any]) -> List[Dict]: + """ + Convert tool registry to format suitable for LLM. + """ + tools = [] + for tool_name, tool_module in tool_registry.items(): + if hasattr(tool_module, 'TOOL_DEFINITION'): + tools.append(tool_module.TOOL_DEFINITION) + return tools + + +def execute_tool(tool_name: str, tool_args: Dict, tool_registry: Dict) -> str: + """ + Execute a tool and return the result as a string. + """ + try: + if tool_name not in tool_registry: + return f"Error: Tool '{tool_name}' not found" + + tool_module = tool_registry[tool_name] + + if not hasattr(tool_module, 'run'): + return f"Error: Tool '{tool_name}' has no run function" + + logger.info(f"Executing tool: {tool_name} with args: {tool_args}") + result = tool_module.run(**tool_args) + + # Convert result to string if needed + if isinstance(result, dict): + return json.dumps(result, indent=2) + return str(result) + + except Exception as e: + logger.error(f"Error executing tool {tool_name}: {e}", exc_info=True) + return f"Error executing {tool_name}: {str(e)}" + + +def parse_tool_calls_from_text(response_text: str) -> List[Dict]: + """ + Parse tool calls from LLM response text. + + Looks for patterns like: + TOOL_CALL: tool_name(arg1="value1", arg2="value2") + or JSON format: + {"tool": "tool_name", "args": {"arg1": "value1"}} + """ + tool_calls = [] + + # Simple pattern matching for tool calls + # You can enhance this based on how your LLM formats tool calls + + # Pattern 1: TOOL_CALL: function_name(args) + if "TOOL_CALL:" in response_text: + lines = response_text.split('\n') + for line in lines: + if line.strip().startswith("TOOL_CALL:"): + try: + # Extract tool call + tool_str = line.split("TOOL_CALL:")[1].strip() + # Parse it (simplified - you may need better parsing) + if "(" in tool_str: + tool_name = tool_str.split("(")[0].strip() + tool_calls.append({ + "name": tool_name, + "arguments": {} # You'd need to parse the args + }) + except: + pass + + return tool_calls + + def process_mention( event_data: dict, slack_client: WebClient, @@ -29,7 +103,7 @@ def process_mention( channel = event.get("channel") user = event.get("user") text = event.get("text", "") - ts = event.get("ts") # This is the message timestamp + ts = event.get("ts") logger.info(f"Processing mention from {user} in {channel}") @@ -39,50 +113,102 @@ def process_mention( # Get bot configuration bot_name = getattr(bot_profile, "BOT_IDENTIFIER", "Bot") - system_prompt = getattr(bot_profile, "SYSTEM_PROMPT", "You are a helpful assistant.") + system_prompt = getattr(bot_profile, "SYSTEM_PROMPT", "You are a helpful AI assistant.") try: - # Try to get RAG context if enabled + # Get RAG context if enabled rag_enabled = getattr(bot_profile, "ENABLE_RAG_INSERT", False) - context = "" + context_messages = [] if rag_enabled: try: - # Search for similar messages similar = vector_store.search_similar(clean_text, limit=3) if similar: - context = "\nRelevant context:\n" + "\n".join(similar) - except AttributeError: - logger.warning("RAG retrieval failed: search_similar not implemented") + context = "\n".join(similar) + context_messages.append({ + "role": "system", + "content": f"Relevant context from previous messages:\n{context}" + }) + logger.info(f"Added RAG context: {len(similar)} similar messages") except Exception as e: logger.error(f"RAG retrieval error: {e}") - # Generate response + # Add tool information to system prompt if tools are available + if tool_registry: + tool_descriptions = [] + for tool_name, tool_module in tool_registry.items(): + if hasattr(tool_module, 'TOOL_DEFINITION'): + tool_def = tool_module.TOOL_DEFINITION + desc = f"- {tool_name}: {tool_def.get('description', 'No description')}" + tool_descriptions.append(desc) + + if tool_descriptions: + tools_info = "\n".join(tool_descriptions) + enhanced_system_prompt = f"""{system_prompt} + +You have access to these tools: +{tools_info} + +To use a tool, respond with: USE_TOOL: tool_name +Then I will execute it and provide the results.""" + else: + enhanced_system_prompt = system_prompt + else: + enhanced_system_prompt = system_prompt + logger.info("No tools available for this bot") + + # Generate response using LLM if available if LLM_AVAILABLE and chat_completion: try: - # Use LLM to generate response + # Build messages for LLM messages = [ - {"role": "system", "content": system_prompt}, + {"role": "system", "content": enhanced_system_prompt}, + *context_messages, {"role": "user", "content": clean_text} ] - if context: - messages.insert(1, {"role": "system", "content": f"Context: {context}"}) + logger.info(f"Calling LLM with {len(messages)} messages and {len(tool_registry)} tools available") + # Call LLM llm_response = chat_completion(messages) response_text = llm_response.get("content", "Sorry, I couldn't generate a response.") + logger.info(f"LLM response: {response_text[:200]}...") + + # Check if LLM wants to use a tool + if "USE_TOOL:" in response_text: + lines = response_text.split('\n') + for line in lines: + if line.strip().startswith("USE_TOOL:"): + tool_name = line.split("USE_TOOL:")[1].strip() + + if tool_name in tool_registry: + logger.info(f"LLM requested tool: {tool_name}") + + # Execute the tool + tool_result = execute_tool(tool_name, {}, tool_registry) + + # Get LLM to process the tool result + messages.append({"role": "assistant", "content": response_text}) + messages.append({"role": "user", "content": f"Tool result from {tool_name}:\n{tool_result}"}) + + llm_response = chat_completion(messages) + response_text = llm_response.get("content", "Sorry, I couldn't process the tool result.") + else: + response_text = f"I tried to use the tool '{tool_name}' but it's not available." + except Exception as e: - logger.error(f"LLM error: {e}", exc_info=True) - response_text = f"You said: {clean_text}" + logger.error(f"LLM call failed: {e}", exc_info=True) + response_text = "Sorry, I encountered an error processing your request." else: - # Simple echo response when LLM not available + # Fallback: simple echo response + logger.info("Using fallback echo response (LLM not available)") response_text = f"You said: {clean_text}" - if context: - response_text += f"\n{context}" + if tool_registry: + response_text += f"\n\nI have {len(tool_registry)} tools available but LLM is not configured." - # Send message to channel (NOT as a thread reply) + # Send message to channel slack_client.chat_postMessage( channel=channel, text=response_text diff --git a/requirements.txt b/requirements.txt index 5e746c3..1d4111d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ sentence-transformers requests watchdog aiohttp +pyodbc \ No newline at end of file diff --git a/scripts/discover_llm_endpoint.py b/scripts/discover_llm_endpoint.py new file mode 100644 index 0000000..da6990f --- /dev/null +++ b/scripts/discover_llm_endpoint.py @@ -0,0 +1,120 @@ +import requests +import os +from dotenv import load_dotenv + +load_dotenv() + +base_url = "http://api.chat.pathcore.org" + +print(f"Discovering endpoints for: {base_url}\n") + +# Try to get the root/health endpoint +print("=" * 60) +print("Step 1: Testing root endpoint") +print("=" * 60) + +for path in ["/", "/health", "/api", "/docs", "/v1"]: + try: + url = f"{base_url}{path}" + print(f"\nTrying: {url}") + resp = requests.get(url, timeout=5, verify=False) + print(f" Status: {resp.status_code}") + if resp.status_code == 200: + print(f" Content-Type: {resp.headers.get('content-type')}") + print(f" Response (first 500 chars):\n{resp.text[:500]}") + except Exception as e: + print(f" Error: {e}") + +# Try common LLM API patterns +print("\n" + "=" * 60) +print("Step 2: Testing common LLM endpoints") +print("=" * 60) + +endpoints = [ + # OpenAI compatible + "/v1/chat/completions", + "/chat/completions", + "/v1/completions", + + # Ollama style + "/api/generate", + "/api/chat", + "/generate", + + # Text Generation WebUI + "/api/v1/generate", + "/api/v1/chat/completions", + + # Custom + "/completion", + "/inference", + "/predict", + "/chat", +] + +for endpoint in endpoints: + try: + url = f"{base_url}{endpoint}" + print(f"\nPOST {url}") + + # Try minimal payload + resp = requests.post( + url, + json={ + "prompt": "Hello", + "model": "default", + "messages": [{"role": "user", "content": "test"}], + "max_tokens": 10 + }, + timeout=5, + verify=False + ) + + print(f" Status: {resp.status_code}") + + if resp.status_code in [200, 201]: + print(f" ✓✓✓ SUCCESS! ✓✓✓") + print(f" Content-Type: {resp.headers.get('content-type')}") + print(f" Response:\n{resp.text[:500]}") + print(f"\n Use this in your .env:") + print(f" LLM_API_URL={url}") + break + elif resp.status_code == 422: # Validation error means endpoint exists! + print(f" ⚠ Endpoint exists but payload wrong") + print(f" Response: {resp.text[:300]}") + elif resp.status_code == 401: + print(f" ⚠ Endpoint exists but requires authentication") + + except Exception as e: + print(f" Error: {e}") + +# Try to find OpenAPI/Swagger docs +print("\n" + "=" * 60) +print("Step 3: Looking for API documentation") +print("=" * 60) + +doc_paths = [ + "/docs", + "/swagger", + "/api/docs", + "/openapi.json", + "/swagger.json", + "/api/swagger.json", +] + +for path in doc_paths: + try: + url = f"{base_url}{path}" + print(f"\nTrying: {url}") + resp = requests.get(url, timeout=5, verify=False) + if resp.status_code == 200: + print(f" ✓ Found documentation!") + print(f" Content-Type: {resp.headers.get('content-type')}") + if 'json' in resp.headers.get('content-type', ''): + print(f" First 500 chars:\n{resp.text[:500]}") + except Exception as e: + pass + +print("\n" + "=" * 60) +print("Discovery complete!") +print("=" * 60) \ No newline at end of file diff --git a/scripts/test_llm_endpoint.py b/scripts/test_llm_endpoint.py new file mode 100644 index 0000000..ca6e36d --- /dev/null +++ b/scripts/test_llm_endpoint.py @@ -0,0 +1,45 @@ +# test_llm_endpoint.py +import requests +import os +from dotenv import load_dotenv + +load_dotenv() + +base_url = os.getenv("LLM_API_URL", "http://api.chat.pathcore.org") + +# Test different possible endpoints +endpoints = [ + f"{base_url}/v1/chat/completions", + f"{base_url}/chat/completions", + f"{base_url}/completions", + f"{base_url}/v1/completions", + f"{base_url}/api/v1/chat/completions", +] + +print(f"Testing LLM API endpoints...\n") + +for endpoint in endpoints: + print(f"Testing: {endpoint}") + try: + # Try a simple GET request first + resp = requests.get(endpoint, timeout=5, verify=False) + print(f" GET response: {resp.status_code}") + + # Try POST with minimal data + resp = requests.post( + endpoint, + json={ + "model": "default", + "messages": [{"role": "user", "content": "test"}] + }, + timeout=5, + verify=False + ) + print(f" POST response: {resp.status_code}") + if resp.status_code == 200: + print(f" ✓ SUCCESS! This endpoint works!") + print(f" Response: {resp.json()}") + break + except Exception as e: + print(f" Error: {e}") + print() \ No newline at end of file diff --git a/tool_loader.py b/tool_loader.py index 288ccd8..ab74f52 100644 --- a/tool_loader.py +++ b/tool_loader.py @@ -1,6 +1,21 @@ import importlib +import inspect import logging from pathlib import Path +from types import SimpleNamespace + + +class ToolWrapper: + """ + Wraps a legacy tool function to provide a standard run() interface. + """ + def __init__(self, definition, func): + self.definition = definition + self.function = func + + def run(self, **kwargs): + return self.function(**kwargs) + def load_tools(tools_path: Path): registry = {} @@ -14,27 +29,52 @@ def load_tools(tools_path: Path): try: module = importlib.import_module(module_name) - # Check if TOOL_DEFINITION exists + # -------------------------------------------------- + # TOOL_DEFINITION is mandatory + # -------------------------------------------------- if not hasattr(module, "TOOL_DEFINITION"): logging.warning(f"Tool {file.name} missing TOOL_DEFINITION, skipping") continue - - # Check if run function exists - if not hasattr(module, "run"): - logging.warning(f"Tool {file.name} missing 'run' function, skipping") + + tool_def = module.TOOL_DEFINITION + tool_name = tool_def.get("name", file.stem) + + # -------------------------------------------------- + # Preferred: explicit run() + # -------------------------------------------------- + if hasattr(module, "run") and callable(module.run): + registry[tool_name] = { + "definition": tool_def, + "function": module.run, + } + logging.info(f"Loaded tool (run): {tool_name}") continue - name = module.TOOL_DEFINITION["name"] - func = getattr(module, "run") + # -------------------------------------------------- + # Legacy auto-wrap (single public function) + # -------------------------------------------------- + public_funcs = [ + obj for name, obj in inspect.getmembers(module, inspect.isfunction) + if not name.startswith("_") + ] - registry[name] = { - "definition": module.TOOL_DEFINITION, - "function": func, - } + if len(public_funcs) == 1: + wrapped = ToolWrapper(tool_def, public_funcs[0]) + registry[tool_name] = { + "definition": tool_def, + "function": wrapped.run, + } + logging.info(f"Wrapped legacy tool: {tool_name}") + continue - logging.info(f"Loaded tool: {name}") + # -------------------------------------------------- + # Failure case + # -------------------------------------------------- + logging.error( + f"Tool {file.name} has no run() and multiple public functions; skipping" + ) - except Exception as e: + except Exception: logging.error(f"Failed to load tool {file.name}", exc_info=True) - return registry \ No newline at end of file + return registry diff --git a/tools/mtscripter.py b/tools/generate_mikrotik_CPE_script.py similarity index 98% rename from tools/mtscripter.py rename to tools/generate_mikrotik_CPE_script.py index 5801975..136af4a 100644 --- a/tools/mtscripter.py +++ b/tools/generate_mikrotik_CPE_script.py @@ -583,4 +583,16 @@ def generate_mikrotik_CPE_script(**kwargs: Any) -> Dict[str, Any]: return {"error": f"An unexpected internal error occurred while generating the script: {str(e)}"} # Return generic error # --- End Tool Implementation --- + +# -------------------------------------------------- +# Tool Entrypoint (required by tool_loader) +# -------------------------------------------------- + +def run(**kwargs): + """ + Tool entrypoint required by the bot runtime. + This must exist so the LLM can execute the tool. + """ + return generate_mikrotik_CPE_script(**kwargs) + # --- END OF FILE mtscripter.py --- diff --git a/tools/imail_tool.py b/tools/imail_tool.py index cf65026..f086422 100644 --- a/tools/imail_tool.py +++ b/tools/imail_tool.py @@ -128,3 +128,11 @@ def get_imail_password(username, domain): except Exception as e: return {"status": "error", "message": f"Database error: {str(e)}"} + +def run(**kwargs): + """ + Tool entrypoint required by the bot runtime. + This must exist so the LLM can execute the tool. + """ + return get_imail_password(**kwargs) + diff --git a/tools/simple_calculator.py b/tools/simple_calculator.py new file mode 100644 index 0000000..0a7d8fe --- /dev/null +++ b/tools/simple_calculator.py @@ -0,0 +1,47 @@ +TOOL_DEFINITION = { + "name": "calculator", + "description": "Performs basic math calculations. Can add, subtract, multiply, or divide two numbers.", + "input_schema": { + "type": "object", + "properties": { + "operation": { + "type": "string", + "enum": ["add", "subtract", "multiply", "divide"], + "description": "The math operation to perform" + }, + "a": { + "type": "number", + "description": "First number" + }, + "b": { + "type": "number", + "description": "Second number" + } + }, + "required": ["operation", "a", "b"] + } +} + + +def run(operation: str, a: float, b: float) -> dict: + """ + Execute the calculation. + """ + operations = { + "add": lambda x, y: x + y, + "subtract": lambda x, y: x - y, + "multiply": lambda x, y: x * y, + "divide": lambda x, y: x / y if y != 0 else "Error: Division by zero" + } + + if operation not in operations: + return {"error": f"Unknown operation: {operation}"} + + result = operations[operation](a, b) + + return { + "operation": operation, + "a": a, + "b": b, + "result": result + } \ No newline at end of file