/ tools / clarify_tool.py
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  )