Complete API documentation for the structured questions framework.
Represents a single option in a structured question.
Location: claude_mpm.utils.structured_questions.QuestionOption
QuestionOption(label: str, description: str)Parameters:
label(str): Display text shown to user (1-5 words recommended, max 50 chars)description(str): Explanation of what this option means or implies
Raises:
QuestionValidationError: If label or description is empty or label exceeds 50 characters
Example:
from claude_mpm.utils.structured_questions import QuestionOption
option = QuestionOption(
label="PostgreSQL",
description="Robust, feature-rich relational database"
)Convert option to AskUserQuestion tool format.
Returns:
- Dictionary with keys
labelanddescription
Example:
option = QuestionOption("FastAPI", "Modern async web framework")
print(option.to_dict())
# {'label': 'FastAPI', 'description': 'Modern async web framework'}Represents a single structured question with validation.
Location: claude_mpm.utils.structured_questions.StructuredQuestion
StructuredQuestion(
question: str,
header: str,
options: list[QuestionOption],
multi_select: bool = False
)Parameters:
question(str): The complete question text (must end with '?')header(str): Short label displayed as chip/tag (max 12 chars)options(list[QuestionOption]): List of 2-4 QuestionOption objectsmulti_select(bool): Whether user can select multiple options (default: False)
Raises:
QuestionValidationError: If validation fails:- Question text is empty or doesn't end with '?'
- Header is empty or exceeds 12 characters
- Options list has fewer than 2 or more than 4 options
- Options are not QuestionOption instances
Example:
from claude_mpm.utils.structured_questions import (
StructuredQuestion,
QuestionOption
)
question = StructuredQuestion(
question="Which database should we use?",
header="Database",
options=[
QuestionOption("PostgreSQL", "Relational database"),
QuestionOption("MongoDB", "NoSQL document database")
],
multi_select=False
)Convert question to AskUserQuestion tool format.
Returns:
- Dictionary with keys:
question,header,options,multiSelect
Example:
question_dict = question.to_dict()
# {
# 'question': 'Which database should we use?',
# 'header': 'Database',
# 'options': [
# {'label': 'PostgreSQL', 'description': 'Relational database'},
# {'label': 'MongoDB', 'description': 'NoSQL document database'}
# ],
# 'multiSelect': False
# }Collection of structured questions for a single AskUserQuestion call.
Location: claude_mpm.utils.structured_questions.QuestionSet
QuestionSet(questions: list[StructuredQuestion] = [])Parameters:
questions(list[StructuredQuestion]): List of 1-4 StructuredQuestion objects (default: empty list)
Raises:
QuestionValidationError: If validation fails:- Questions list is empty
- Questions list has more than 4 questions
- Items are not StructuredQuestion instances
Example:
from claude_mpm.utils.structured_questions import QuestionSet
question_set = QuestionSet([question1, question2])Add a question to the set.
Parameters:
question(StructuredQuestion): Question to add
Returns:
- Self for method chaining
Raises:
QuestionValidationError: If adding would exceed 4 questions
Example:
question_set = QuestionSet()
question_set.add(question1).add(question2)Convert question set to AskUserQuestion tool parameters.
Returns:
- Dictionary suitable for AskUserQuestion tool parameters
Example:
params = question_set.to_ask_user_question_params()
# Use with AskUserQuestion tool
# tool.invoke(params)execute(response: dict[str, Any] | None = None, use_fallback_if_needed: bool = True) -> ParsedResponse
Execute questions with automatic fallback on AskUserQuestion failure.
This is the recommended method for executing questions. It provides graceful degradation when the AskUserQuestion tool fails or returns empty/invalid responses, automatically falling back to text-based questions.
Parameters:
response(dict[str, Any] | None): Response from AskUserQuestion tool (optional)use_fallback_if_needed(bool): Auto-fallback if AskUserQuestion fails (default: True)
Returns:
ParsedResponseobject with user answers
Raises:
QuestionValidationError: If response is None and fallback is disabled
Fallback Trigger Conditions:
The method automatically detects AskUserQuestion failures by checking for:
- Empty or missing response
- Missing "answers" key in response
- Empty answers dictionary
- Fake/placeholder responses (e.g., all answers are "." or "")
Example:
# Basic usage (recommended)
question_set = QuestionSet([question])
# Option 1: With AskUserQuestion response
response = {"answers": {"Database": "PostgreSQL"}}
parsed = question_set.execute(response)
db = parsed.get("Database") # "PostgreSQL"
# Option 2: With failed/empty response (triggers fallback)
response = {"answers": {}}
parsed = question_set.execute(response) # Falls back to text input
db = parsed.get("Database")
# Option 3: Disable fallback (raises error on failure)
try:
parsed = question_set.execute(response, use_fallback_if_needed=False)
except QuestionValidationError:
# Handle error
passText Fallback Format:
When fallback is triggered, users see:
============================================================
📋 USER INPUT REQUIRED
(AskUserQuestion tool unavailable - using text fallback)
============================================================
=== Question 1 of 2 ===
[Database] Which database should we use?
Options:
1. PostgreSQL - Robust relational database
2. MongoDB - Flexible NoSQL database
Your answer:
Supported Input Formats (Fallback Mode):
Single-select:
- Numeric:
1,2,3,4 - Exact label:
PostgreSQL,MongoDB - Partial match:
postgres,mongo(case-insensitive) - Custom answer: Any text not matching options
Multi-select:
- Comma-separated numbers:
1,2,3 - Comma-separated labels:
PostgreSQL, MongoDB - Mixed:
1, MongoDB, Custom DB
Fluent API for building StructuredQuestion objects.
Location: claude_mpm.utils.structured_questions.QuestionBuilder
QuestionBuilder()Initializes builder with empty state.
Example:
from claude_mpm.utils.structured_questions import QuestionBuilder
builder = QuestionBuilder()Set the question text.
Parameters:
question(str): The question text (should end with '?')
Returns:
- Self for method chaining
Example:
builder.ask("Which testing framework?")Set the header label.
Parameters:
header(str): Short label (max 12 chars)
Returns:
- Self for method chaining
Example:
builder.header("Testing")Add an option to the question.
Parameters:
label(str): Display text for the optiondescription(str): Explanation of the option
Returns:
- Self for method chaining
Example:
builder.add_option("pytest", "Python's most popular testing framework")Set all options at once.
Parameters:
options(list[QuestionOption]): List of QuestionOption objects
Returns:
- Self for method chaining
Example:
options = [
QuestionOption("Option 1", "Description 1"),
QuestionOption("Option 2", "Description 2")
]
builder.with_options(options)Enable or disable multi-select mode.
Parameters:
enabled(bool): Whether to allow multiple selections (default: True)
Returns:
- Self for method chaining
Example:
builder.multi_select(enabled=True)Build and validate the StructuredQuestion.
Returns:
- Validated StructuredQuestion instance
Raises:
QuestionValidationError: If validation fails or required fields missing
Example:
question = (
QuestionBuilder()
.ask("Which framework?")
.header("Framework")
.add_option("FastAPI", "Modern async framework")
.add_option("Flask", "Lightweight WSGI framework")
.build()
)Wrapper for parsed question responses with convenient accessor methods.
Location: claude_mpm.utils.structured_questions.ParsedResponse
This is the recommended interface for accessing user answers. It provides a clean, consistent API for both AskUserQuestion and text fallback responses.
ParsedResponse(question_set: QuestionSet, answers: dict[str, str | list[str]])Parameters:
question_set(QuestionSet): The QuestionSet that was executedanswers(dict[str, str | list[str]]): Parsed answers dictionary
Note: Typically you won't construct this directly - use QuestionSet.execute() instead.
Example:
# Recommended: Use execute() which returns ParsedResponse
parsed = question_set.execute(response)
# Direct construction (not recommended)
from claude_mpm.utils.structured_questions import ParsedResponse
parsed = ParsedResponse(question_set, {"Database": "PostgreSQL"})Get answer for a specific question by header.
Parameters:
header(str): Question header to look updefault(Any): Default value if not answered (default: None)
Returns:
- Selected option label(s), custom answer, or default value
- For single-select: Returns
str - For multi-select: Returns
list[str]
Example:
parsed = question_set.execute(response)
# Single-select question
database = parsed.get("Database") # "PostgreSQL"
database = parsed.get("Database", "SQLite") # "SQLite" if not answered
# Multi-select question
features = parsed.get("Features") # ["Auth", "Search", "Analytics"]
features = parsed.get("Features", []) # [] if not answeredCheck if a question was answered.
Parameters:
header(str): Question header to check
Returns:
- True if question was answered, False otherwise
Example:
if parsed.was_answered("Database"):
database = parsed.get("Database")
print(f"User selected: {database}")
else:
print("Database question was not answered")Get all answers as a dictionary.
Returns:
- Dictionary mapping question headers to selected option labels
Example:
parsed = question_set.execute(response)
all_answers = parsed.get_all()
# {
# 'Database': 'PostgreSQL',
# 'Features': ['Auth', 'Search'],
# 'Testing': 'pytest'
# }
for header, answer in all_answers.items():
print(f"{header}: {answer}")Parses and validates responses from AskUserQuestion tool.
Location: claude_mpm.utils.structured_questions.ResponseParser
ResponseParser(question_set: QuestionSet)Parameters:
question_set(QuestionSet): The QuestionSet that was sent to AskUserQuestion
Example:
from claude_mpm.utils.structured_questions import ResponseParser
parser = ResponseParser(question_set)Parse AskUserQuestion response into header → answer mapping.
Parameters:
response(dict[str, Any]): Raw response from AskUserQuestion tool- Expected format:
{"answers": {"header": "label", ...}}
- Expected format:
Returns:
- Dictionary mapping question headers to selected option labels
- For multi-select questions, values are lists of labels
- For single-select questions, values are strings
Raises:
QuestionValidationError: If response format is invalid:- Response is not a dictionary
- Response doesn't contain 'answers' key
- Answer format doesn't match question type
Example:
response = {
"answers": {
"Database": "PostgreSQL",
"Features": ["Auth", "Search", "Analytics"]
}
}
parsed = parser.parse(response)
# {
# 'Database': 'PostgreSQL',
# 'Features': ['Auth', 'Search', 'Analytics']
# }Get answer for a specific question by header.
Parameters:
parsed_answers(dict): Result fromparse()header(str): Question header to look up
Returns:
- Selected option label(s) or None if not answered
Example:
database = parser.get_answer(parsed_answers, "Database")
# 'PostgreSQL'
features = parser.get_answer(parsed_answers, "Features")
# ['Auth', 'Search', 'Analytics']
missing = parser.get_answer(parsed_answers, "NonExistent")
# NoneCheck if a question was answered.
Parameters:
parsed_answers(dict): Result fromparse()header(str): Question header to check
Returns:
- True if question was answered, False otherwise
Example:
if parser.was_answered(parsed_answers, "Database"):
database = parser.get_answer(parsed_answers, "Database")
else:
database = "PostgreSQL" # DefaultAbstract base class for question templates.
Location: claude_mpm.templates.questions.base.QuestionTemplate
Build and return a QuestionSet.
Returns:
- QuestionSet ready for use with AskUserQuestion tool
Raises:
QuestionValidationError: If question construction fails
Example:
from claude_mpm.templates.questions.base import QuestionTemplate
from claude_mpm.utils.structured_questions import QuestionSet, QuestionBuilder
class MyTemplate(QuestionTemplate):
def build(self) -> QuestionSet:
question = (
QuestionBuilder()
.ask("Which option?")
.header("Option")
.add_option("A", "Option A")
.add_option("B", "Option B")
.build()
)
return QuestionSet([question])Build question set and convert to AskUserQuestion parameters.
Returns:
- Dictionary suitable for AskUserQuestion tool
Example:
template = MyTemplate()
params = template.to_params()
# Use with AskUserQuestion toolTemplate that adjusts questions based on context.
Location: claude_mpm.templates.questions.base.ConditionalTemplate
Extends: QuestionTemplate
ConditionalTemplate(**context: Any)Parameters:
**context(Any): Arbitrary context values used to determine questions
Example:
template = ConditionalTemplate(
num_tickets=3,
has_ci=True,
project_type="web"
)context(dict[str, Any]): Dictionary of context values
Get a context value.
Parameters:
key(str): Context key to retrievedefault(Any): Default value if key not found (default: None)
Returns:
- Context value or default
Example:
num_tickets = template.get_context("num_tickets", 1)Check if context key exists.
Parameters:
key(str): Context key to check
Returns:
- True if key exists in context, False otherwise
Example:
if template.has_context("has_ci"):
# CI context provided
passDetermine if a question should be included based on context.
Parameters:
question_id(str): Identifier for the question being considered
Returns:
- True if question should be included, False otherwise
Example:
class PRWorkflowTemplate(ConditionalTemplate):
def should_include_question(self, question_id: str) -> bool:
if question_id == "auto_merge":
return self.get_context("has_ci", False)
return TrueBuild QuestionSet based on context.
Returns:
- QuestionSet with questions appropriate for the context
Template for multi-step question workflows.
Location: claude_mpm.templates.questions.base.MultiStepTemplate
Extends: QuestionTemplate
MultiStepTemplate()_current_step(int): Current step number (0-indexed)_answers(dict[str, Any]): Stored answers from previous steps
Record answers from a previous step.
Parameters:
step(int): Step number (0-indexed)answers(dict): Parsed answers from ResponseParser
Example:
template.set_answers(0, parsed_answers)Get answers from a previous step.
Parameters:
step(int): Step number (0-indexed)
Returns:
- Answers dictionary or None if step not completed
Example:
step0_answers = template.get_answers(0)Build questions for a specific step.
Parameters:
step(int): Step number (0-indexed)
Returns:
- QuestionSet for the specified step
Build questions for the current step.
Returns:
- QuestionSet for current step
Move to the next step.
Example:
template.advance_step() # Move from step 0 to step 1Get current step number.
Returns:
- Current step number (int)
Example:
step = template.current_step # 0, 1, 2, etc.Check if all steps are complete.
Returns:
- True if workflow is complete, False otherwise
Exception raised when question validation fails.
Location: claude_mpm.utils.structured_questions.QuestionValidationError
Extends: Exception
Common Causes:
-
Question text validation:
- Empty question text
- Question doesn't end with '?'
-
Header validation:
- Empty header
- Header exceeds 12 characters
-
Options validation:
- Fewer than 2 options
- More than 4 options
- Empty option label or description
- Option label exceeds 50 characters
-
Question set validation:
- Empty question set
- More than 4 questions in set
-
Response parsing:
- Invalid response format
- Answer type doesn't match question type
Example:
from claude_mpm.utils.structured_questions import (
QuestionBuilder,
QuestionValidationError
)
try:
question = (
QuestionBuilder()
.ask("Invalid question without question mark")
.header("Test")
.add_option("A", "Option A")
.build()
)
except QuestionValidationError as e:
print(f"Validation failed: {e}")
# "Question should end with '?': Invalid question without question mark"from typing import Any
# Core classes
class QuestionOption:
label: str
description: str
def __init__(self, label: str, description: str) -> None: ...
def to_dict(self) -> dict[str, str]: ...
class StructuredQuestion:
question: str
header: str
options: list[QuestionOption]
multi_select: bool
def __init__(
self,
question: str,
header: str,
options: list[QuestionOption],
multi_select: bool = False
) -> None: ...
def to_dict(self) -> dict[str, Any]: ...
class QuestionSet:
questions: list[StructuredQuestion]
def __init__(self, questions: list[StructuredQuestion] = []) -> None: ...
def add(self, question: StructuredQuestion) -> QuestionSet: ...
def to_ask_user_question_params(self) -> dict[str, Any]: ...
def execute(
self,
response: dict[str, Any] | None = None,
use_fallback_if_needed: bool = True,
) -> ParsedResponse: ...
# Builder
class QuestionBuilder:
def __init__(self) -> None: ...
def ask(self, question: str) -> QuestionBuilder: ...
def header(self, header: str) -> QuestionBuilder: ...
def add_option(self, label: str, description: str) -> QuestionBuilder: ...
def with_options(self, options: list[QuestionOption]) -> QuestionBuilder: ...
def multi_select(self, enabled: bool = True) -> QuestionBuilder: ...
def build(self) -> StructuredQuestion: ...
# Response objects
class ParsedResponse:
def __init__(self, question_set: QuestionSet, answers: dict[str, str | list[str]]) -> None: ...
def get(self, header: str, default: Any = None) -> str | list[str] | Any: ...
def was_answered(self, header: str) -> bool: ...
def get_all(self) -> dict[str, str | list[str]]: ...
# Legacy parser (prefer ParsedResponse)
class ResponseParser:
def __init__(self, question_set: QuestionSet) -> None: ...
def parse(self, response: dict[str, Any]) -> dict[str, str | list[str]]: ...
def get_answer(
self,
parsed_answers: dict[str, str | list[str]],
header: str
) -> str | list[str] | None: ...
def was_answered(
self,
parsed_answers: dict[str, str | list[str]],
header: str
) -> bool: ...
# Templates
class QuestionTemplate(ABC):
@abstractmethod
def build(self) -> QuestionSet: ...
def to_params(self) -> dict[str, Any]: ...
class ConditionalTemplate(QuestionTemplate):
context: dict[str, Any]
def __init__(self, **context: Any) -> None: ...
def get_context(self, key: str, default: Any = None) -> Any: ...
def has_context(self, key: str) -> bool: ...
def should_include_question(self, question_id: str) -> bool: ...
class MultiStepTemplate(QuestionTemplate):
_current_step: int
_answers: dict[str, Any]
def __init__(self) -> None: ...
def set_answers(self, step: int, answers: dict[str, Any]) -> None: ...
def get_answers(self, step: int) -> dict[str, Any] | None: ...
@abstractmethod
def build_step(self, step: int) -> QuestionSet: ...
def advance_step(self) -> None: ...
@property
def current_step(self) -> int: ...
@abstractmethod
def is_complete(self) -> bool: ...
# Exception
class QuestionValidationError(Exception):
pass- User Guide - User-friendly overview and common use cases
- Template Catalog - Complete list of available templates
- Integration Guide - How to use in custom agents
- Design Document - Architecture and design decisions
For More Examples: See EXAMPLES.md for integration patterns and complete workflows.