From 0e6a0fa770ebc19e84205b9e4875c3e559021167 Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:23:41 -0500 Subject: [PATCH] Update .gitignore to exclude additional cache files; enhance APIManager message handling and tool response parsing --- .gitignore | 2 + discord_glhf/api.py | 115 +++++++++++++++++++++--- discord_glhf/handlers/function_tools.py | 86 ++++++++---------- 3 files changed, 142 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index 658b7c1..254d5a5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ discord_glhf/__pycache__/bot.cpython-313.pyc discord_glhf/handlers/__pycache__/event_handler.cpython-313.pyc discord_glhf/web/__pycache__/app.cpython-313.pyc discord_glhf/handlers/__pycache__/function_tools.cpython-313.pyc +discord_glhf/__pycache__/api.cpython-313.pyc +discord_glhf/handlers/__pycache__/tool_handler.cpython-313.pyc diff --git a/discord_glhf/api.py b/discord_glhf/api.py index a4767cd..ea56d6c 100644 --- a/discord_glhf/api.py +++ b/discord_glhf/api.py @@ -258,16 +258,96 @@ class APIManager: # Import tools and tool response parser from .handlers.function_tools import TOOLS, parse_tool_response - # Prepare request data with OpenAI function calling format + # Ensure first message is a user message + first_user_message = False + for msg in messages: + if msg.get("role") == "user": + first_user_message = True + break + + if not first_user_message: + logger.error("No user message found in conversation") + return False, None + + # Add our system message after first user message to prevent template error + system_message = { + "role": "system", + "content": """You are DEATHKILLER THE MEGA ASSASSIN! You live on the planet Hell and constantly threaten to kill people. You are full of anger and hate everyone. + +Remember: +- Stay aggressive and violent in your responses +- Be direct and confrontational +- Show your murderous rage in every interaction +- Never break character""" + } + + # First, organize messages + system_messages = [] + user_messages = [] + assistant_messages = [] + + for msg in messages: + role = msg.get("role", "") + if role == "system": + system_messages.append(msg) + elif role == "user": + user_messages.append(msg) + elif role == "assistant": + assistant_messages.append(msg) + + # Ensure we have at least one user message + if not user_messages: + logger.error("No user messages found in conversation") + return False, None + + # Build conversation in correct order + messages_with_system = [] + + # 1. Start with system messages + messages_with_system.extend(system_messages) + messages_with_system.append(system_message) # Add our character system message + + # 2. Add first user message + messages_with_system.append(user_messages[0]) + + # 3. Add remaining messages in alternating order + for i in range(len(assistant_messages)): + if i < len(assistant_messages): + messages_with_system.append(assistant_messages[i]) + if i + 1 < len(user_messages): + messages_with_system.append(user_messages[i + 1]) + + # Prepare request data data = { "model": params["model"], - "messages": messages, + "messages": messages_with_system, "temperature": params["temperature"], "max_tokens": params["max_tokens"], - "stream": False, # Disable streaming for all requests - "tools": TOOLS, # Add function definitions - "tool_choice": "auto" # Let model decide when to use tools + "stream": False } + + # Only add tools after first user message if we have one + if user_messages: + logger.debug("Adding function calling capabilities") + data["tools"] = TOOLS + + # Log detailed request information + logger.debug("=== REQUEST DEBUG INFO ===") + logger.debug("Message sequence:") + for i, msg in enumerate(messages_with_system): + logger.debug(f"{i}: {msg.get('role')} - {msg.get('content')[:50]}...") + + logger.debug("\nMessage roles sequence:") + logger.debug([msg.get("role") for msg in messages_with_system]) + + logger.debug("\nTools configuration:") + logger.debug(json.dumps(TOOLS, indent=2)) + + logger.debug("\nFinal request data:") + debug_data = data.copy() + debug_data["messages"] = [{"role": m.get("role"), "content_length": len(m.get("content", ""))} for m in data["messages"]] + logger.debug(json.dumps(debug_data, indent=2)) + logger.debug("========================") logger.debug( "API Request Details:\n" "URL: %s\n" @@ -335,22 +415,31 @@ class APIManager: message = choice["message"] - # Check for tool calls first - if message.get("tool_calls"): + # Log full message for debugging + logger.debug(f"Message content: {message.get('content')}") + logger.debug(f"Message tool_calls: {message.get('tool_calls')}") + logger.debug(f"Message function_call: {message.get('function_call')}") + + # Check for tool/function calls first + if message.get("tool_calls") or message.get("function_call"): + logger.info("Function calling detected in response") tool_data = parse_tool_response(json_response) if tool_data: - logger.debug(f"Found tool calls: {tool_data}") + logger.info(f"Successfully parsed tool calls: {tool_data}") return True, json.dumps(tool_data) + else: + logger.error("Failed to parse tool calls") + return False, None - # Fall back to regular content if present + # Fall back to regular content content = message.get("content", "").strip() if content: - logger.debug(f"Found content: {content[:100]}") + logger.debug(f"Using regular content: {content[:100]}") return True, content - # No valid content or tool calls - logger.error("No valid content or tool calls found") - logger.debug(f"Full message: {message}") + # No valid output found + logger.error("No valid content or tool calls in response") + logger.debug(f"Full response: {json_response}") return False, None except json.JSONDecodeError as e: logger.error(f"Failed to decode response: {e}") diff --git a/discord_glhf/handlers/function_tools.py b/discord_glhf/handlers/function_tools.py index 34629bb..86e9733 100644 --- a/discord_glhf/handlers/function_tools.py +++ b/discord_glhf/handlers/function_tools.py @@ -1,19 +1,21 @@ +#!/usr/bin/env python3 """Function definitions and tool schemas for LLM function calling.""" -from typing import Dict, Any +import json +from typing import Dict, Any, Optional, List TOOLS = [ { "type": "function", "function": { "name": "mention_user", - "description": "Mention a Discord user in the response", + "description": "Mention a Discord user to get their attention", "parameters": { "type": "object", "properties": { "name": { "type": "string", - "description": "Username or nickname to mention" + "description": "The username to mention (without @ symbol)" } }, "required": ["name"] @@ -24,13 +26,13 @@ TOOLS = [ "type": "function", "function": { "name": "add_reaction", - "description": "Add an emoji reaction to the message", + "description": "Add an emoji reaction to the last message", "parameters": { "type": "object", "properties": { "emoji": { "type": "string", - "description": "The emoji to add (Unicode emoji, custom emoji <:name:id>, or standard emoji :name:)" + "description": "The emoji to add as a reaction (Unicode emoji or Discord emoji name)" } }, "required": ["emoji"] @@ -47,16 +49,11 @@ TOOLS = [ "properties": { "title": { "type": "string", - "description": "Title of the embed" + "description": "The title to show at the top of the embed" }, "description": { "type": "string", - "description": "Content of the embed" - }, - "color": { - "type": "integer", - "description": "Color of the embed in hex format (e.g., 0xFF0000 for red)", - "default": 0xFF0000 + "description": "The main content body of the embed" } }, "required": ["title", "description"] @@ -67,17 +64,13 @@ TOOLS = [ "type": "function", "function": { "name": "create_thread", - "description": "Create a new discussion thread from the message", + "description": "Start a new discussion thread", "parameters": { "type": "object", "properties": { "name": { "type": "string", - "description": "Name/topic for the thread. Common patterns: 'X vs Y', '[topic] is overrated/underrated', or topics about safety/maintenance/review" - }, - "message_id": { - "type": "integer", - "description": "Optional ID of message to create thread from" + "description": "The topic/title for the new thread" } }, "required": ["name"] @@ -86,34 +79,31 @@ TOOLS = [ } ] -def function_to_tool_call(function_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: - """Convert a function call response to a tool call format.""" - return { - "name": function_name, - "arguments": arguments - } - -def parse_tool_response(response: Dict[str, Any]) -> Dict[str, Any]: +def parse_tool_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: """Parse an API response looking for function calls in tool_calls format.""" - if not response or "choices" not in response: - return {} - - choice = response["choices"][0] - if "message" not in choice: - return {} - - message = choice["message"] - if "tool_calls" not in message: - return {} - - tool_calls = [] - for tool_call in message["tool_calls"]: - if tool_call["type"] == "function": - # Extract function name and arguments - function_call = { - "name": tool_call["function"]["name"], - "arguments": json.loads(tool_call["function"]["arguments"]) - } - tool_calls.append(function_call) - - return {"tool_calls": tool_calls} if tool_calls else {} \ No newline at end of file + try: + if not response or "choices" not in response: + return None + + choice = response["choices"][0] + if "message" not in choice: + return None + + message = choice["message"] + if "tool_calls" not in message: + return None + + tool_calls = [] + for tool_call in message["tool_calls"]: + if tool_call["type"] == "function": + function = tool_call["function"] + tool_calls.append({ + "name": function["name"], + "arguments": json.loads(function["arguments"]) + }) + + return {"tool_calls": tool_calls} if tool_calls else None + + except Exception as e: + print(f"Error parsing tool response: {e}") + return None \ No newline at end of file