Elicitation¶
The aifred_tk.elicitation module lets plugin tools (and LLM agents) ask the
human user to select from a list of labelled choices at runtime.
Quick start¶
from aifred_tk.elicitation import Choice, elicit_choice
result = elicit_choice(
"Which deployment target?",
[
Choice("staging", description="Pre-production environment"),
Choice("production", description="Live environment", example="us-east-1"),
],
allow_free_input=True,
)
if result.is_free_input:
print(f"Custom answer: {result.free_text}")
else:
print(f"Selected: {result.selected_label} (index {result.selected_index})")
Data model¶
Choice¶
| Field | Type | Description |
|---|---|---|
label |
str |
Short display text shown to the user. |
description |
str \| None |
Optional sentence explaining the option. |
example |
str \| None |
Optional concrete example (shown as "e.g. …"). |
ElicitationResult¶
| Field | Type | Description |
|---|---|---|
selected_index |
int |
0-based index of the chosen option; -1 for free text. |
selected_label |
str \| None |
Label of the chosen option; None for free text. |
free_text |
str \| None |
User-supplied text when allow_free_input=True was picked. |
is_free_input |
bool |
True iff the user typed a custom answer. |
elicit_choice() API¶
def elicit_choice(
question: str,
choices: list[Choice],
*,
context: str | None = None,
allow_free_input: bool = False,
settings: Dynaconf | None = None,
) -> ElicitationResult: ...
elicit_choice automatically selects the right interaction mode:
| Execution context | Behaviour |
|---|---|
| Inside an MCP tool call | Uses FastMCP ctx.elicit() — the MCP client (e.g. Claude Code) shows a native dialog. |
| CLI / plain Python, TTY stdout | interactive mode — cursor-key selector via questionary. |
| CLI / plain Python, non-TTY stdout | numbered mode — prints a numbered list, reads a number from input(). |
The mode can be overridden via configuration (see below).
Parameters¶
question— The question string displayed to the user.choices— Ordered list ofChoiceobjects; must not be empty.context— Optional paragraph of context shown above the choices.allow_free_input— WhenTrue, an "Other (specify…)" option is appended; if the user selects it, a follow-up prompt collects free text.settings— OptionalDynaconfinstance for reading the elicitation mode setting. Defaults to the global settings when omitted.
Errors¶
ElicitationCancelledError is raised when the MCP client explicitly declines
or cancels the elicitation dialog. Terminal elicitors always return a result
and never raise this error.
Configuration¶
Add to your settings file or environment:
| Value | Description |
|---|---|
auto |
Detects TTY: interactive if stdout is a terminal, numbered otherwise. |
interactive |
Always use the cursor-key questionary selector. |
numbered |
Always use the plain numbered-list input() selector. |
Environment variable override: AIFRED_TK__ELICITATION__MODE=numbered.
MCP tool for LLM agents¶
When running as an MCP server, aifred_elicit_choice is also exposed as an MCP tool
so that LLM agents (PydanticAI, LangChain, Claude, etc.) can ask the user a
question on their own initiative — without needing a dedicated plugin tool as
an intermediary.
Tool name: aifred_elicit_choice
Input schema:
| Parameter | Type | Required | Description |
|---|---|---|---|
question |
string | yes | The question to display. |
choices |
string[] | yes | Labels for each option. |
descriptions |
string[] | no | Optional explanation per label (same order). |
examples |
string[] | no | Optional example per label (same order). |
context |
string | no | Optional context paragraph shown above the choices. |
allow_free_input |
boolean | no | Append an "Other (specify…)" option. |
Output schema:
For free-text answers, selected_index is -1, selected_label is null,
and free_text contains the user's input.
Using elicitation inside a plugin tool¶
Plugin tools run synchronously inside an anyio worker thread. elicit_choice
bridges back to the async event loop automatically via
anyio.from_thread.run(), so no special setup is required:
from aifred_tk.core.interfaces import Plugin, Tool, ToolArgs
from aifred_tk.elicitation import Choice, elicit_choice
from pydantic import BaseModel
class DeployArgs(ToolArgs):
service: str
class DeployTool(Tool):
tool_id = "deploy"
description = "Deploy a service to a chosen environment."
args_schema = DeployArgs
def execute(self, args: DeployArgs) -> ToolResult:
result = elicit_choice(
f"Deploy {args.service} to which environment?",
[Choice("staging"), Choice("production")],
)
return ToolResult(output={"environment": result.selected_label, "service": args.service})