Skip to content

LLM-Human-LLM Interrogation

For complex tasks where the initial instructions or data might be ambiguous, you can implement a multi-turn "interrogation" loop. In this pattern, the agent can choose to either provide a final result or request clarification from the human.

Pattern: The Interrogation Loop

This pattern feeds user responses back into the agent's message history until a final verdict is reached.

1. Define the Unified Output Model

Create a Pydantic model that allows the agent to return either the final result or a request for more information.

from pydantic import BaseModel, Field
from typing import Literal

class ClarificationRequest(BaseModel):
    """The agent needs more info before it can proceed."""
    type: Literal["clarification"] = "clarification"
    question: str = Field(..., description="The question to ask the user.")
    options: list[str] | None = Field(
        None, 
        description="Suggested labels for the user to choose from."
    )
    context: str | None = Field(
        None, 
        description="Extra context to show the user above the question."
    )

class FinalAnalysis(BaseModel):
    """The agent has enough info to provide the final result."""
    type: Literal["final"] = "final"
    verdict: str
    confidence: float

# The agent will return one of these two types
AgentOutput = ClarificationRequest | FinalAnalysis

2. Configure the Agent

Use build_agent_from_settings with your union type as the output_type. This returns an AgentRunner, which supports fallback chains automatically.

from aifred_tk.core.llm import build_agent_from_settings, AgentRunner

self._agent: AgentRunner[AgentOutput] = build_agent_from_settings(
    self.tool_id,
    self._settings,
    output_type=AgentOutput,
    system_prompt="""
    You are a requirements analyst. 
    Analyze the user's request. 
    If anything is ambiguous, return a ClarificationRequest.
    If you have everything you need, return a FinalAnalysis.
    """
)

3. Implement the Loop

The execute method runs a loop that feeds user responses back into the agent's message history.

from aifred_tk.elicitation import Choice, elicit_choice
from aifred_tk.core.models import ElicitationCancelledError
from aifred_tk.core.interfaces import ToolResult
from pydantic_ai.messages import ModelMessage

def execute(self, args: ToolArgs) -> ToolResult:
    agent = self._get_agent()
    user_prompt = f"Analyze this: {args.input_text}"
    message_history: list[ModelMessage] = []

    while True:
        # 1. Run the agent
        result = agent.run_sync(user_prompt, message_history=message_history)
        output = result.output

        # Update history for the next turn
        message_history = list(result.all_messages())

        # 2. Check if it's a final result
        if isinstance(output, FinalAnalysis):
            return ToolResult(
                output={"verdict": output.verdict, "confidence": output.confidence},
                llm_name=agent.model_name
            )

        # 3. Otherwise, it's a clarification request. Elicit info from the human.
        choices = [Choice(opt) for opt in (output.options or [])]

        try:
            elicit_result = elicit_choice(
                question=output.question,
                choices=choices,
                context=output.context,
                allow_free_input=True
            )
        except ElicitationCancelledError:
            return ToolResult(output={"status": "error", "message": "User cancelled the interrogation."})

        # 4. Feed the human's answer back as the next user prompt
        if elicit_result.is_free_input:
            user_prompt = elicit_result.free_text
        else:
            user_prompt = f"User selected: {elicit_result.selected_label}"

Handling Cancellations

In MCP environments, a user might close the dialog or explicitly hit "Cancel". This raises an ElicitationCancelledError. Your tool should handle this gracefully (as shown in the loop example above).