Tools is broken
This commit is contained in:
2
.env
2
.env
@@ -21,7 +21,9 @@ EMBEDDING_MODEL=all-MiniLM-L6-v2
|
|||||||
# Local LLM (Remote Machine)
|
# Local LLM (Remote Machine)
|
||||||
#########################
|
#########################
|
||||||
LOCAL_LLM_ENDPOINT=http://172.168.10.10:11434/v1/chat/completions
|
LOCAL_LLM_ENDPOINT=http://172.168.10.10:11434/v1/chat/completions
|
||||||
|
LLM_API_URL=http://api.chat.pathcore.org
|
||||||
LOCAL_LLM_MODEL=mistral
|
LOCAL_LLM_MODEL=mistral
|
||||||
|
LLM_MODEL=mistral
|
||||||
LOCAL_LLM_TIMEOUT=60
|
LOCAL_LLM_TIMEOUT=60
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
BOT_IDENTIFIER = "default_bot"
|
BOT_IDENTIFIER = "default_bot"
|
||||||
|
|
||||||
SYSTEM_PROMPT = """You are a helpful AI assistant in Slack.
|
SYSTEM_PROMPT = """You are Abot, a helpful AI assistant for First Step Internet.
|
||||||
You answer questions clearly and concisely.
|
Your purpose in this channel is to generate Mikrotik CPE scripts using the associated tool.
|
||||||
You are friendly and professional."""
|
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
|
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
|
||||||
@@ -1,84 +1,155 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any
|
import requests
|
||||||
from slack_sdk import WebClient
|
import os
|
||||||
from slack_sdk.errors import SlackApiError
|
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__)
|
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,
|
def chat_completion(
|
||||||
slack_client: WebClient,
|
messages: List[Dict[str, Any]],
|
||||||
vector_store: Any,
|
model: str = None,
|
||||||
bot_profile: Any,
|
temperature: float = 0.7,
|
||||||
tool_registry: Dict[str, Any]
|
max_tokens: int = 1000,
|
||||||
) -> None:
|
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
|
# Ollama chat endpoint
|
||||||
from config import BOT_USER_ID
|
url = f"{LLM_API_URL}/api/chat"
|
||||||
clean_text = text.replace(f"<@{BOT_USER_ID}>", "").strip()
|
|
||||||
|
|
||||||
# Get bot configuration
|
# Convert OpenAI-style messages to Ollama format
|
||||||
bot_name = getattr(bot_profile, "BOT_IDENTIFIER", "Bot")
|
# 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:
|
||||||
# Try to get RAG context if enabled
|
resp = requests.post(
|
||||||
rag_enabled = getattr(bot_profile, "ENABLE_RAG_INSERT", False)
|
url,
|
||||||
context = ""
|
json=payload,
|
||||||
|
headers=headers,
|
||||||
if rag_enabled:
|
timeout=120, # Ollama can be slow on first request
|
||||||
try:
|
verify=False
|
||||||
# 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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Sent response to {channel}")
|
# Raise exception for HTTP errors
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
except SlackApiError as e:
|
data = resp.json()
|
||||||
logger.error(f"Slack API error: {e.response['error']}", exc_info=True)
|
logger.debug(f"Ollama API response: {data}")
|
||||||
try:
|
|
||||||
slack_client.chat_postMessage(
|
# Extract the assistant's response from Ollama format
|
||||||
channel=channel,
|
# Ollama returns: {"message": {"role": "assistant", "content": "..."}}
|
||||||
text="Sorry, I encountered a Slack API error."
|
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)})"
|
||||||
)
|
)
|
||||||
except:
|
|
||||||
pass
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error processing mention: {e}", exc_info=True)
|
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:
|
try:
|
||||||
slack_client.chat_postMessage(
|
resp = requests.get(url, timeout=10, verify=False)
|
||||||
channel=channel,
|
resp.raise_for_status()
|
||||||
text="⚠️ Sorry, I encountered an internal error."
|
data = resp.json()
|
||||||
)
|
|
||||||
except:
|
if "models" in data:
|
||||||
pass
|
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']
|
||||||
@@ -1,20 +1,94 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any
|
import json
|
||||||
|
from typing import Dict, Any, List
|
||||||
from slack_sdk import WebClient
|
from slack_sdk import WebClient
|
||||||
from slack_sdk.errors import SlackApiError
|
from slack_sdk.errors import SlackApiError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Try to import LLM client, but don't fail if it's not available
|
# Try to import LLM client
|
||||||
try:
|
try:
|
||||||
from llm.local_llm_client import chat_completion
|
from llm.local_llm_client import chat_completion
|
||||||
LLM_AVAILABLE = True
|
LLM_AVAILABLE = True
|
||||||
except ImportError:
|
logger.info("LLM client loaded successfully")
|
||||||
logger.warning("LLM client not available, using simple echo mode")
|
except ImportError as e:
|
||||||
|
logger.warning(f"LLM client not available: {e}")
|
||||||
LLM_AVAILABLE = False
|
LLM_AVAILABLE = False
|
||||||
chat_completion = None
|
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(
|
def process_mention(
|
||||||
event_data: dict,
|
event_data: dict,
|
||||||
slack_client: WebClient,
|
slack_client: WebClient,
|
||||||
@@ -29,7 +103,7 @@ def process_mention(
|
|||||||
channel = event.get("channel")
|
channel = event.get("channel")
|
||||||
user = event.get("user")
|
user = event.get("user")
|
||||||
text = event.get("text", "")
|
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}")
|
logger.info(f"Processing mention from {user} in {channel}")
|
||||||
|
|
||||||
@@ -39,50 +113,102 @@ def process_mention(
|
|||||||
|
|
||||||
# Get bot configuration
|
# Get bot configuration
|
||||||
bot_name = getattr(bot_profile, "BOT_IDENTIFIER", "Bot")
|
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:
|
||||||
# Try to get RAG context if enabled
|
# Get RAG context if enabled
|
||||||
rag_enabled = getattr(bot_profile, "ENABLE_RAG_INSERT", False)
|
rag_enabled = getattr(bot_profile, "ENABLE_RAG_INSERT", False)
|
||||||
context = ""
|
context_messages = []
|
||||||
|
|
||||||
if rag_enabled:
|
if rag_enabled:
|
||||||
try:
|
try:
|
||||||
# Search for similar messages
|
|
||||||
similar = vector_store.search_similar(clean_text, limit=3)
|
similar = vector_store.search_similar(clean_text, limit=3)
|
||||||
if similar:
|
if similar:
|
||||||
context = "\nRelevant context:\n" + "\n".join(similar)
|
context = "\n".join(similar)
|
||||||
except AttributeError:
|
context_messages.append({
|
||||||
logger.warning("RAG retrieval failed: search_similar not implemented")
|
"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:
|
except Exception as e:
|
||||||
logger.error(f"RAG retrieval error: {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:
|
if LLM_AVAILABLE and chat_completion:
|
||||||
try:
|
try:
|
||||||
# Use LLM to generate response
|
# Build messages for LLM
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": system_prompt},
|
{"role": "system", "content": enhanced_system_prompt},
|
||||||
|
*context_messages,
|
||||||
{"role": "user", "content": clean_text}
|
{"role": "user", "content": clean_text}
|
||||||
]
|
]
|
||||||
|
|
||||||
if context:
|
logger.info(f"Calling LLM with {len(messages)} messages and {len(tool_registry)} tools available")
|
||||||
messages.insert(1, {"role": "system", "content": f"Context: {context}"})
|
|
||||||
|
|
||||||
|
# Call LLM
|
||||||
llm_response = chat_completion(messages)
|
llm_response = chat_completion(messages)
|
||||||
response_text = llm_response.get("content", "Sorry, I couldn't generate a response.")
|
response_text = llm_response.get("content", "Sorry, I couldn't generate a response.")
|
||||||
|
|
||||||
except Exception as e:
|
logger.info(f"LLM response: {response_text[:200]}...")
|
||||||
logger.error(f"LLM error: {e}", exc_info=True)
|
|
||||||
response_text = f"You said: {clean_text}"
|
# 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:
|
else:
|
||||||
# Simple echo response when LLM not available
|
response_text = f"I tried to use the tool '{tool_name}' but it's not available."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"LLM call failed: {e}", exc_info=True)
|
||||||
|
response_text = "Sorry, I encountered an error processing your request."
|
||||||
|
else:
|
||||||
|
# Fallback: simple echo response
|
||||||
|
logger.info("Using fallback echo response (LLM not available)")
|
||||||
response_text = f"You said: {clean_text}"
|
response_text = f"You said: {clean_text}"
|
||||||
|
|
||||||
if context:
|
if tool_registry:
|
||||||
response_text += f"\n{context}"
|
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(
|
slack_client.chat_postMessage(
|
||||||
channel=channel,
|
channel=channel,
|
||||||
text=response_text
|
text=response_text
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ sentence-transformers
|
|||||||
requests
|
requests
|
||||||
watchdog
|
watchdog
|
||||||
aiohttp
|
aiohttp
|
||||||
|
pyodbc
|
||||||
120
scripts/discover_llm_endpoint.py
Normal file
120
scripts/discover_llm_endpoint.py
Normal 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)
|
||||||
45
scripts/test_llm_endpoint.py
Normal file
45
scripts/test_llm_endpoint.py
Normal 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()
|
||||||
@@ -1,6 +1,21 @@
|
|||||||
import importlib
|
import importlib
|
||||||
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
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):
|
def load_tools(tools_path: Path):
|
||||||
registry = {}
|
registry = {}
|
||||||
@@ -14,27 +29,52 @@ def load_tools(tools_path: Path):
|
|||||||
try:
|
try:
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
# Check if TOOL_DEFINITION exists
|
# --------------------------------------------------
|
||||||
|
# TOOL_DEFINITION is mandatory
|
||||||
|
# --------------------------------------------------
|
||||||
if not hasattr(module, "TOOL_DEFINITION"):
|
if not hasattr(module, "TOOL_DEFINITION"):
|
||||||
logging.warning(f"Tool {file.name} missing TOOL_DEFINITION, skipping")
|
logging.warning(f"Tool {file.name} missing TOOL_DEFINITION, skipping")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if run function exists
|
tool_def = module.TOOL_DEFINITION
|
||||||
if not hasattr(module, "run"):
|
tool_name = tool_def.get("name", file.stem)
|
||||||
logging.warning(f"Tool {file.name} missing 'run' function, skipping")
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# 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
|
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] = {
|
if len(public_funcs) == 1:
|
||||||
"definition": module.TOOL_DEFINITION,
|
wrapped = ToolWrapper(tool_def, public_funcs[0])
|
||||||
"function": func,
|
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)
|
logging.error(f"Failed to load tool {file.name}", exc_info=True)
|
||||||
|
|
||||||
return registry
|
return registry
|
||||||
@@ -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
|
return {"error": f"An unexpected internal error occurred while generating the script: {str(e)}"} # Return generic error
|
||||||
|
|
||||||
# --- End Tool Implementation ---
|
# --- 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 ---
|
# --- END OF FILE mtscripter.py ---
|
||||||
@@ -128,3 +128,11 @@ def get_imail_password(username, domain):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"status": "error", "message": f"Database error: {str(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)
|
||||||
|
|
||||||
|
|||||||
47
tools/simple_calculator.py
Normal file
47
tools/simple_calculator.py
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user