clarify_tool.py
1 #!/usr/bin/env python3 2 """ 3 Clarify Tool Module - Interactive Clarifying Questions 4 5 Allows the agent to present structured multiple-choice questions or open-ended 6 prompts to the user. In CLI mode, choices are navigable with arrow keys. On 7 messaging platforms, choices are rendered as a numbered list. 8 9 The actual user-interaction logic lives in the platform layer (cli.py for CLI, 10 gateway/run.py for messaging). This module defines the schema, validation, and 11 a thin dispatcher that delegates to a platform-provided callback. 12 """ 13 14 import json 15 from typing import List, Optional, Callable 16 17 18 # Maximum number of predefined choices the agent can offer. 19 # A 5th "Other (type your answer)" option is always appended by the UI. 20 MAX_CHOICES = 4 21 22 23 def clarify_tool( 24 question: str, 25 choices: Optional[List[str]] = None, 26 callback: Optional[Callable] = None, 27 ) -> str: 28 """ 29 Ask the user a question, optionally with multiple-choice options. 30 31 Args: 32 question: The question text to present. 33 choices: Up to 4 predefined answer choices. When omitted the 34 question is purely open-ended. 35 callback: Platform-provided function that handles the actual UI 36 interaction. Signature: callback(question, choices) -> str. 37 Injected by the agent runner (cli.py / gateway). 38 39 Returns: 40 JSON string with the user's response. 41 """ 42 if not question or not question.strip(): 43 return tool_error("Question text is required.") 44 45 question = question.strip() 46 47 # Validate and trim choices 48 if choices is not None: 49 if not isinstance(choices, list): 50 return tool_error("choices must be a list of strings.") 51 choices = [str(c).strip() for c in choices if str(c).strip()] 52 if len(choices) > MAX_CHOICES: 53 choices = choices[:MAX_CHOICES] 54 if not choices: 55 choices = None # empty list → open-ended 56 57 if callback is None: 58 return json.dumps( 59 {"error": "Clarify tool is not available in this execution context."}, 60 ensure_ascii=False, 61 ) 62 63 try: 64 user_response = callback(question, choices) 65 except Exception as exc: 66 return json.dumps( 67 {"error": f"Failed to get user input: {exc}"}, 68 ensure_ascii=False, 69 ) 70 71 return json.dumps({ 72 "question": question, 73 "choices_offered": choices, 74 "user_response": str(user_response).strip(), 75 }, ensure_ascii=False) 76 77 78 def check_clarify_requirements() -> bool: 79 """Clarify tool has no external requirements -- always available.""" 80 return True 81 82 83 # ============================================================================= 84 # OpenAI Function-Calling Schema 85 # ============================================================================= 86 87 CLARIFY_SCHEMA = { 88 "name": "clarify", 89 "description": ( 90 "Ask the user a question when you need clarification, feedback, or a " 91 "decision before proceeding. Supports two modes:\n\n" 92 "1. **Multiple choice** — provide up to 4 choices. The user picks one " 93 "or types their own answer via a 5th 'Other' option.\n" 94 "2. **Open-ended** — omit choices entirely. The user types a free-form " 95 "response.\n\n" 96 "Use this tool when:\n" 97 "- The task is ambiguous and you need the user to choose an approach\n" 98 "- You want post-task feedback ('How did that work out?')\n" 99 "- You want to offer to save a skill or update memory\n" 100 "- A decision has meaningful trade-offs the user should weigh in on\n\n" 101 "Do NOT use this tool for simple yes/no confirmation of dangerous " 102 "commands (the terminal tool handles that). Prefer making a reasonable " 103 "default choice yourself when the decision is low-stakes." 104 ), 105 "parameters": { 106 "type": "object", 107 "properties": { 108 "question": { 109 "type": "string", 110 "description": "The question to present to the user.", 111 }, 112 "choices": { 113 "type": "array", 114 "items": {"type": "string"}, 115 "maxItems": MAX_CHOICES, 116 "description": ( 117 "Up to 4 answer choices. Omit this parameter entirely to " 118 "ask an open-ended question. When provided, the UI " 119 "automatically appends an 'Other (type your answer)' option." 120 ), 121 }, 122 }, 123 "required": ["question"], 124 }, 125 } 126 127 128 # --- Registry --- 129 from tools.registry import registry, tool_error 130 131 registry.register( 132 name="clarify", 133 toolset="clarify", 134 schema=CLARIFY_SCHEMA, 135 handler=lambda args, **kw: clarify_tool( 136 question=args.get("question", ""), 137 choices=args.get("choices"), 138 callback=kw.get("callback")), 139 check_fn=check_clarify_requirements, 140 emoji="❓", 141 )