Tools is broken

This commit is contained in:
eweeman
2026-01-15 17:01:02 -08:00
parent 4a6e2b898f
commit fcc86b52c2
11 changed files with 596 additions and 109 deletions

2
.env
View File

@@ -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
#########################

View File

@@ -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
LLM_MODEL = "default" # Model name to use
LLM_TEMPERATURE = 0.7 # Creativity (0.0 to 2.0)
LLM_MAX_TOKENS = 1000 # Maximum response length

View File

@@ -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
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']

View File

@@ -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

View File

@@ -8,3 +8,4 @@ sentence-transformers
requests
watchdog
aiohttp
pyodbc

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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 ---

View File

@@ -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)

View File

@@ -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
}