diff --git a/discord_glhf/handlers/tool_handler.py b/discord_glhf/handlers/tool_handler.py index 5eb4501..43f5518 100644 --- a/discord_glhf/handlers/tool_handler.py +++ b/discord_glhf/handlers/tool_handler.py @@ -223,6 +223,45 @@ class ToolHandler: except Exception as e: logger.error(f"Tool Use - Create Thread: Failed to create thread: {e}") + def validate_tool_response(self, response: str) -> bool: + """Validate proper tool marker closure. + + Args: + response: The response string to validate + + Returns: + bool: True if all tool markers are properly closed, False otherwise + """ + from .tools import TOOL_MARKERS + try: + stack = [] + + # Find all tool markers + pattern = f"{re.escape(TOOL_MARKERS['start'])}\\w+\\]|{re.escape(TOOL_MARKERS['end'])}" + markers = re.finditer(pattern, response) + + for match in markers: + marker = match.group(0) + if TOOL_MARKERS['end'] in marker: + if not stack: + logger.warning(f"Tool Use - Validate: Found end marker without matching start: {marker}") + return False + start_marker = stack.pop() + logger.debug(f"Tool Use - Validate: Matched {start_marker} with {marker}") + else: + stack.append(marker) + logger.debug(f"Tool Use - Validate: Found start marker: {marker}") + + if stack: + logger.warning(f"Tool Use - Validate: Unclosed tool markers found: {stack}") + return False + + return True + + except Exception as e: + logger.error(f"Tool Use - Validate: Error validating tool markers: {e}") + return False + def parse_tool_calls( self, response: str, @@ -236,101 +275,87 @@ class ToolHandler: message_id: Optional ID of the message being responded to channel_id: Optional ID of the channel the message is in """ + from .tools import TOOL_MARKERS + try: + # Validate tool markers first + if not self.validate_tool_response(response): + logger.warning("Tool Use - Parse: Invalid tool marker structure, skipping tool parsing") + return [], response, {} + tool_calls = [] - lines = response.split("\n") - response_lines = [] - i = 0 + final_response = response - while i < len(lines): - line = lines[i].strip() - if not line: - i += 1 - continue - - # Convert line to lowercase for command checking - line_lower = line.lower() - command_found = False - - # Process tools based on natural language patterns + # Process tool markers in order of appearance + for tool_type, markers in TOOL_MARKERS["patterns"].items(): + pattern = f"{re.escape(markers['start'])}(.*?){re.escape(markers['end'])}" + matches = list(re.finditer(pattern, response, re.DOTALL)) - # Simple pattern matching for basic tool usage - # Let the LLM decide most tool usage through natural language - if "@" in line: # Basic mention support - logger.info(f"Tool Use - Parse: Detected mention pattern in line: '{line}'") - for match in re.finditer(r"@(\w+(?:\s+\w+)*)", line): - name = match.group(1).strip() - logger.info(f"Tool Use - Parse: Adding find_user tool call for '{name}'") - tool_calls.append(("find_user", {"name": name})) - command_found = True + for match in matches: + content = match.group(1).strip() + if not content: + continue - # Thread creation patterns - thread_patterns = [ - (r'(?i)(.*?\bvs\.?\b.*?(?:debate|discussion)?)', r'\1 debate'), # X vs Y - (r'(?i)(.*?\b(?:over|under)rated\b.*?(?:discussion)?)', r'\1 discussion'), # overrated/underrated - (r'(?i)(.*?\b(?:safety|maintenance|review)\b.*?(?:discussion|thread)?)', r'\1 discussion') # Specific topics - ] - - for pattern, name_format in thread_patterns: - if re.search(pattern, line_lower): - # Extract the topic from the line - match = re.search(pattern, line, re.IGNORECASE) - if match: - thread_name = re.sub(pattern, name_format, match.group(1)) - logger.info(f"Tool Use - Parse: Adding create_thread tool call for '{thread_name}'") + logger.info(f"Tool Use - Parse: Found {tool_type} tool usage: {content[:50]}...") + + # Process based on tool type + if tool_type == "mention": + # Extract username from @mention + if "@" in content: + name = re.search(r"@(\w+(?:\s+\w+)*)", content) + if name: + name = name.group(1).strip() + tool_calls.append(("find_user", {"name": name})) + logger.info(f"Tool Use - Parse: Adding find_user tool call for '{name}'") + + elif tool_type == "reaction": + # Process emoji reactions + emoji_pattern = r'([😀-🙏🌀-🗿]||:[a-zA-Z0-9_]+:)' + emoji_matches = re.finditer(emoji_pattern, content) + for emoji_match in emoji_matches: + emoji = emoji_match.group(1) + if emoji.strip(): + tool_calls.append(("add_reaction", { + "emoji": emoji, + "message_id": message_id, + "channel_id": channel_id + })) + logger.info(f"Tool Use - Parse: Adding add_reaction tool call for emoji '{emoji}'") + + elif tool_type == "embed": + # Process embed content + lines = content.strip().split("\n") + if lines: + title = lines[0] + description = "\n".join(lines[1:]) if len(lines) > 1 else "" + tool_calls.append(("create_embed", { + "title": title, + "description": description, + "color": 0xFF0000 # Default red color + })) + logger.info(f"Tool Use - Parse: Adding create_embed tool call with title '{title}'") + + elif tool_type == "thread": + # Process thread creation + thread_name = content.strip() + if thread_name: tool_calls.append(("create_thread", { "channel_id": channel_id, "name": thread_name, "message_id": message_id })) - command_found = True - break + logger.info(f"Tool Use - Parse: Adding create_thread tool call for '{thread_name}'") - # Support emoji reactions (both explicit and from text) - # 1. Match Unicode emojis, custom emojis, and standard Discord emojis - emoji_pattern = r'([😀-🙏🌀-🗿]||:[a-zA-Z0-9_]+:)' - emoji_matches = re.finditer(emoji_pattern, line) - for match in emoji_matches: - emoji = match.group(1) - if emoji.strip(): - logger.info(f"Tool Use - Parse: Adding add_reaction tool call for emoji '{emoji}'") - tool_calls.append(("add_reaction", { - "emoji": emoji, - "message_id": message_id, - "channel_id": channel_id - })) - command_found = True + # Remove the tool marker and its content from the final response + final_response = final_response.replace(match.group(0), "") - # 2. Also detect emoticons and convert them to emojis - emoticon_map = { - r'(?:^|\s)[:;]-?[)D](?:\s|$)': '😊', # :) ;) :-) :D - r'(?:^|\s)[:;]-?[(\[](?:\s|$)': '😢', # :( ;( :-( :[ - r'(?:^|\s)[:;]-?[pP](?:\s|$)': '😛', # :p ;p :-p - r'(?:^|\s)[:;]-?[oO](?:\s|$)': '😮', # :o ;o :-o - r'(?:^|\s)[xX][dD](?:\s|$)': '😂', # xD XD - } - for pattern, emoji in emoticon_map.items(): - if re.search(pattern, line): - logger.info(f"Tool Use - Parse: Converting emoticon to emoji reaction '{emoji}'") - tool_calls.append(("add_reaction", { - "emoji": emoji, - "message_id": message_id, - "channel_id": channel_id - })) - command_found = True + # Clean up the final response + final_response = "\n".join(line for line in final_response.split("\n") if line.strip()) - # Always include the line in the response, regardless of commands - response_lines.append(line) - - i += 1 - - # Join response lines, removing empty lines at start/end - final_response = "\n".join(response_lines).strip() - # Log summary of detected tools if tool_calls: logger.info(f"Tool Use - Parse: Detected {len(tool_calls)} tool calls: {[call[0] for call in tool_calls]}") - + return tool_calls, final_response, self.mentioned_users except Exception as e: diff --git a/discord_glhf/handlers/tools.py b/discord_glhf/handlers/tools.py index 135cc7e..af62732 100644 --- a/discord_glhf/handlers/tools.py +++ b/discord_glhf/handlers/tools.py @@ -1,5 +1,29 @@ """Tools available for LLM use and their documentation.""" +# Tool Markers Configuration +TOOL_MARKERS = { + "start": "[TOOL:", + "end": "[/TOOL]", + "patterns": { + "mention": { + "start": "[TOOL:mention]", + "end": "[/TOOL]" + }, + "reaction": { + "start": "[TOOL:reaction]", + "end": "[/TOOL]" + }, + "embed": { + "start": "[TOOL:embed]", + "end": "[/TOOL]" + }, + "thread": { + "start": "[TOOL:thread]", + "end": "[/TOOL]" + } + } +} + # Tool Definitions TOOLS = { "mention_user": { @@ -7,8 +31,8 @@ TOOLS = { "parameters": { "name": "The username or nickname to mention", }, - "example": "@username", - "usage": "Simply include @ before the username in your response: @john" + "example": "[TOOL:mention]@username[/TOOL]", + "usage": "Wrap mentions with [TOOL:mention] markers: [TOOL:mention]@john[/TOOL]" }, "add_reaction": { @@ -16,8 +40,8 @@ TOOLS = { "parameters": { "emoji": "The emoji to add as a reaction. Can be Unicode emoji, custom emoji (<:name:id>), or standard emoji (:name:)" }, - "example": "😊 or :smile: or <:custom:123456789>", - "usage": "Include the emoji naturally in your response text" + "example": "[TOOL:reaction]😊[/TOOL] or [TOOL:reaction]:smile:[/TOOL]", + "usage": "Wrap emojis with [TOOL:reaction] markers" }, "create_embed": { @@ -28,13 +52,13 @@ TOOLS = { "color": "Optional hex color code (defaults to red)" }, "example": """ - [Embed] + [TOOL:embed] Title Goes Here This is the description content It can span multiple lines - [/Embed] + [/TOOL] """, - "usage": "Wrap embed content in [Embed] tags" + "usage": "Wrap embed content with [TOOL:embed] markers" }, "create_thread": { @@ -43,34 +67,36 @@ TOOLS = { "name": "The name/topic for the thread", "message_id": "Optional ID of message to create thread from" }, - "example": "Let's discuss X vs Y or This coaster is overrated", - "usage": "Use natural language patterns like 'X vs Y' or include keywords like 'overrated', 'safety', 'maintenance', 'review'" + "example": "[TOOL:thread]Let's discuss X vs Y[/TOOL] or [TOOL:thread]This coaster is overrated[/TOOL]", + "usage": "Wrap thread topics with [TOOL:thread] markers" } } # Tool Usage Guidelines USAGE_GUIDELINES = """ -Tools can be used naturally in conversation: +Tools must be used with explicit markers in conversation: -1. To mention a user: Just include @ before their name - Example: "Hey @john, what do you think?" +1. To mention a user: + Example: "Hey [TOOL:mention]@john[/TOOL], what do you think?" -2. To add reactions: Simply include emojis in your response - Example: "That's awesome! 👍" +2. To add reactions: + Example: "That's a great point! [TOOL:reaction]👍[/TOOL]" -3. To create embeds: Use [Embed] tags - Example: - [Embed] - Poll Results - First place: X - Second place: Y - [/Embed] +3. To create embeds: + Example: + [TOOL:embed] + Poll Results + First place: X + Second place: Y + [/TOOL] -4. To create threads: Use natural discussion patterns - Examples: - - "Let's compare X vs Y" - - "Is this coaster overrated?" - - "Time for a safety discussion" +4. To create threads: + Examples: + - [TOOL:thread]Let's compare X vs Y[/TOOL] + - [TOOL:thread]Is this coaster overrated?[/TOOL] + - [TOOL:thread]Time for a safety discussion[/TOOL] + +Note: Always ensure tool markers are properly closed with their corresponding end tags. """ # Error Messages