Wizard Observability¶
This guide covers the observability features for wizard flows, including task tracking, state snapshots, and transition auditing.
Overview¶
Wizard observability provides:
- Task Tracking - Granular progress tracking within and across wizard stages
- Transition Audit Trail - Complete history of state transitions with timing and context
- State Snapshots - Read-only snapshots for UI display and debugging
- Statistics - Aggregated metrics for monitoring and analysis
All observability types are in dataknobs_bots.reasoning.observability.
Task Tracking¶
Tasks provide granular progress tracking beyond stage-level progress. A stage may have multiple tasks (e.g., collect bot name, collect description), and global tasks can span stages (e.g., validate config, save config).
WizardTask¶
A WizardTask represents a single trackable action within the wizard:
from dataknobs_bots.reasoning.observability import WizardTask
task = WizardTask(
id="collect_bot_name",
description="Collect bot name",
status="pending", # pending, in_progress, completed, skipped
stage="configure_identity", # None for global tasks
required=True,
depends_on=[], # Task IDs that must complete first
completed_by="field_extraction", # What triggers completion
field_name="bot_name", # For field_extraction trigger
)
# Check task state
if task.is_pending:
print(f"Task {task.id} is waiting")
if task.is_complete:
print(f"Completed at {task.completed_at}")
if task.is_global:
print("This is a global task")
Task Completion Triggers¶
Tasks can be completed by different triggers:
| Trigger | Description | Required Field |
|---|---|---|
field_extraction |
Completed when a field is extracted | field_name |
tool_result |
Completed when a tool succeeds | tool_name |
stage_exit |
Completed when leaving a stage | - |
manual |
Completed programmatically | - |
WizardTaskList¶
WizardTaskList manages a collection of tasks with dependency tracking:
from dataknobs_bots.reasoning.observability import WizardTask, WizardTaskList
# Create task list with dependencies
task_list = WizardTaskList(tasks=[
WizardTask(id="validate", description="Validate config", depends_on=[]),
WizardTask(id="save", description="Save config", depends_on=["validate"]),
])
# Query tasks
pending = task_list.get_pending_tasks()
completed = task_list.get_completed_tasks()
available = task_list.get_available_tasks() # Pending with deps met
# Get tasks by scope
stage_tasks = task_list.get_tasks_for_stage("configure_identity")
global_tasks = task_list.get_global_tasks()
# Complete a task (checks dependencies)
if task_list.complete_task("validate"):
print("Validate completed!")
# Now "save" is available
print(task_list.get_available_tasks()) # [save]
# Skip a task
task_list.skip_task("validate")
# Calculate progress (based on required tasks)
progress = task_list.calculate_progress() # 0.0 to 100.0
print(f"Progress: {progress}%")
Defining Tasks in Configuration¶
Tasks can be defined in wizard YAML configuration:
stages:
configure_identity:
prompt: "Let's set up your bot's identity..."
schema:
type: object
properties:
bot_name: { type: string }
description: { type: string }
# Task definitions for this stage
tasks:
- id: collect_bot_name
description: "Collect bot name"
completed_by: field_extraction
field_name: bot_name
required: true
- id: collect_description
description: "Collect bot description"
completed_by: field_extraction
field_name: description
required: false
# Global tasks (not tied to a specific stage)
global_tasks:
- id: preview_config
description: "Preview the configuration"
completed_by: tool_result
tool_name: preview_config
required: false
- id: validate_config
description: "Validate the configuration"
completed_by: tool_result
tool_name: validate_config
required: true
- id: save_config
description: "Save the configuration"
completed_by: tool_result
tool_name: save_config
required: true
depends_on: [validate_config] # Must validate first
Transition Tracking¶
TransitionRecord¶
A TransitionRecord captures a single state transition with full context:
from dataknobs_bots.reasoning.observability import TransitionRecord, create_transition_record
# Create using factory function (auto-sets timestamp)
record = create_transition_record(
from_stage="welcome",
to_stage="configure",
trigger="user_input",
duration_in_stage_ms=5000.0,
data_snapshot={"intent": "create"},
user_input="I want to create a math tutor bot",
condition_evaluated="data.get('intent')",
condition_result=True,
)
# Access fields
print(f"Transition: {record.from_stage} -> {record.to_stage}")
print(f"Trigger: {record.trigger}")
print(f"Duration: {record.duration_in_stage_ms}ms")
Trigger Types¶
| Trigger | Description |
|---|---|
user_input |
User message triggered the transition |
navigation_back |
User navigated backward |
navigation_skip |
User skipped the stage |
restart |
Wizard was restarted |
auto |
Automatic transition (e.g., condition-based) |
TransitionTracker¶
TransitionTracker maintains a bounded history of transitions with query and statistics capabilities:
from dataknobs_bots.reasoning.observability import (
TransitionTracker,
TransitionHistoryQuery,
create_transition_record,
)
# Create tracker with max history
tracker = TransitionTracker(max_history=100)
# Record transitions
tracker.record(create_transition_record(
from_stage="welcome",
to_stage="configure",
trigger="user_input",
duration_in_stage_ms=3000.0,
))
tracker.record(create_transition_record(
from_stage="configure",
to_stage="review",
trigger="user_input",
duration_in_stage_ms=15000.0,
))
# Query history
all_transitions = tracker.query() # All records
# Query with filters
query = TransitionHistoryQuery(
trigger="user_input",
since=time.time() - 3600, # Last hour
limit=10,
)
recent_user_transitions = tracker.query(query)
# Get statistics
stats = tracker.get_stats()
print(f"Total transitions: {stats.total_transitions}")
print(f"Unique paths: {stats.unique_paths}")
print(f"Avg duration: {stats.avg_duration_per_stage_ms}ms")
print(f"Backtracks: {stats.backtrack_count}")
print(f"Restarts: {stats.restart_count}")
print(f"Most common trigger: {stats.most_common_trigger}")
TransitionHistoryQuery¶
Filter transitions with flexible query parameters:
from dataknobs_bots.reasoning.observability import TransitionHistoryQuery
query = TransitionHistoryQuery(
from_stage="welcome", # Filter by source stage
to_stage="configure", # Filter by target stage
trigger="user_input", # Filter by trigger type
since=1700000000.0, # After this timestamp
until=1700100000.0, # Before this timestamp
limit=50, # Max records to return
)
State Snapshots¶
WizardStateSnapshot¶
WizardStateSnapshot provides a complete read-only view of wizard state, useful for UI rendering and debugging:
from dataknobs_bots.reasoning.observability import WizardStateSnapshot
# Snapshots are typically created by WizardReasoning.get_state_snapshot()
# but can be created directly or deserialized:
snapshot = WizardStateSnapshot(
current_stage="configure_identity",
data={"bot_name": "MathHelper"},
history=["welcome", "configure_identity"],
transitions=[transition_record],
completed=False,
# Task tracking
tasks=[{"id": "collect_name", "status": "completed"}, ...],
pending_tasks=3,
completed_tasks=2,
total_tasks=5,
available_task_ids=["collect_description"],
task_progress_percent=40.0,
# Stage context
stage_index=1,
total_stages=4,
can_skip=True,
can_go_back=True,
suggestions=["Create a math tutor", "Build a quiz bot"],
)
# Query tasks from snapshot
task = snapshot.get_task("collect_name")
stage_tasks = snapshot.get_tasks_for_stage("configure_identity")
global_tasks = snapshot.get_global_tasks()
if snapshot.is_task_available("save_config"):
print("Save is available!")
# Get latest transition
latest = snapshot.get_latest_transition()
if latest:
print(f"Last: {latest['from_stage']} -> {latest['to_stage']}")
# Serialize for storage or API response
data = snapshot.to_dict()
# Deserialize
restored = WizardStateSnapshot.from_dict(data)
Using Snapshots for UI¶
Snapshots are designed for driving UI components:
# In your web application
snapshot = reasoning.get_state_snapshot(manager)
# Display stage progress
progress_bar.set_value(snapshot.stage_index / snapshot.total_stages)
stage_label.set_text(f"Stage {snapshot.stage_index + 1} of {snapshot.total_stages}")
# Display task checklist
for task in snapshot.tasks:
status_icon = "✅" if task["status"] == "completed" else "⬜"
if task["status"] == "skipped":
status_icon = "⏭️"
task_list.add_item(f"{status_icon} {task['description']}")
# Show available actions
if snapshot.can_go_back:
show_back_button()
if snapshot.can_skip:
show_skip_button()
# Enable/disable action buttons based on task availability
save_button.enabled = snapshot.is_task_available("save_config")
# Display suggestions
for suggestion in snapshot.suggestions:
quick_reply_buttons.add(suggestion)
Integration with WizardReasoning¶
The observability features integrate with WizardReasoning:
from dataknobs_bots.reasoning import WizardReasoning
# Get current state snapshot
snapshot = reasoning.get_state_snapshot(manager)
# Access from conversation metadata (static method)
snapshot = WizardReasoning.snapshot_from_metadata(
manager.metadata,
stage_definitions=wizard_config.get("stages"),
)
# Tasks are automatically completed when:
# - Fields are extracted (completed_by: field_extraction)
# - Tools succeed (completed_by: tool_result)
# - Stages are exited (completed_by: stage_exit)
Conversion Utilities¶
Convert between wizard and FSM observability types:
from dataknobs_bots.reasoning.observability import (
TransitionRecord,
transition_record_to_execution_record,
execution_record_to_transition_record,
transition_stats_to_execution_stats,
)
from dataknobs_fsm.observability import ExecutionRecord
# Convert wizard record to FSM record
wizard_record = TransitionRecord(...)
fsm_record = transition_record_to_execution_record(wizard_record)
# Convert FSM record to wizard record
wizard_record = execution_record_to_transition_record(
fsm_record,
user_input="optional user message",
)
# Convert stats
fsm_stats = transition_stats_to_execution_stats(wizard_stats)
Best Practices¶
1. Use Tasks for Granular Progress¶
Define tasks for each meaningful action in your wizard:
# Instead of relying only on stage progress:
stages:
configure:
tasks:
- id: collect_name
completed_by: field_extraction
field_name: name
- id: collect_email
completed_by: field_extraction
field_name: email
- id: verify_email
completed_by: tool_result
tool_name: send_verification
2. Use Dependencies for Workflow Control¶
Express task ordering through dependencies:
global_tasks:
- id: validate
required: true
- id: preview
depends_on: [validate]
required: false
- id: save
depends_on: [validate] # Not preview - it's optional
required: true
3. Query Statistics for Monitoring¶
Use TransitionStats for monitoring wizard performance:
stats = tracker.get_stats()
# Monitor backtracking (may indicate confusing UX)
if stats.backtrack_count > stats.total_transitions * 0.3:
log.warning("High backtrack rate - review wizard flow")
# Monitor average time per stage
if stats.avg_duration_per_stage_ms > 60000: # 1 minute
log.info("Users spending significant time on stages")
4. Serialize Snapshots for APIs¶
Expose snapshots through your API for frontend state:
@app.get("/wizard/state")
async def get_wizard_state(conversation_id: str):
manager = await get_manager(conversation_id)
snapshot = reasoning.get_state_snapshot(manager)
return snapshot.to_dict()
API Reference¶
Type Aliases¶
TaskStatus = Literal["pending", "in_progress", "completed", "skipped"]
TaskCompletionTrigger = Literal["field_extraction", "tool_result", "stage_exit", "manual"]
Classes¶
| Class | Description |
|---|---|
WizardTask |
Single trackable task within wizard flow |
WizardTaskList |
Collection of tasks with dependency tracking |
TransitionRecord |
Record of a single state transition |
TransitionHistoryQuery |
Query parameters for filtering transitions |
TransitionStats |
Aggregated transition statistics |
TransitionTracker |
Manages transition history with queries |
WizardStateSnapshot |
Complete read-only wizard state snapshot |
Factory Functions¶
| Function | Description |
|---|---|
create_transition_record() |
Create TransitionRecord with auto-timestamp |
Conversion Utilities¶
| Function | Description |
|---|---|
transition_record_to_execution_record() |
Convert wizard to FSM record |
execution_record_to_transition_record() |
Convert FSM to wizard record |
transition_stats_to_execution_stats() |
Convert wizard to FSM stats |
Subflow Tracking¶
When a wizard pushes or pops a subflow, the transition record captures the subflow context so you can audit nested flows.
TransitionRecord Subflow Fields¶
| Field | Type | Description |
|---|---|---|
subflow_push |
str \| None |
Network name if this transition pushes a subflow |
subflow_pop |
str \| None |
Network name if this transition pops a subflow |
subflow_depth |
int |
Nesting depth after this transition (0 = main flow) |
from dataknobs_bots.reasoning.observability import create_transition_record
# Record a subflow push
record = create_transition_record(
from_stage="configure",
to_stage="sub_start",
trigger="user_input",
subflow_push="kb_setup",
subflow_depth=1,
)
# Record a subflow pop (return to parent)
record = create_transition_record(
from_stage="sub_end",
to_stage="review",
trigger="auto",
subflow_pop="kb_setup",
subflow_depth=0,
)
WizardStateSnapshot Stages¶
The stages field on WizardStateSnapshot provides an ordered list of stage
entries for UI rendering (breadcrumbs, progress bars, checklists):
snapshot.stages
# [
# {"name": "welcome", "label": "Welcome", "status": "completed"},
# {"name": "configure", "label": "Configuration", "status": "current"},
# {"name": "review", "label": "Review", "status": "pending"},
# {"name": "complete", "label": "Done", "status": "pending"},
# ]
Each entry has:
name-- stage identifierlabel-- human-readable label (falls back tonameif not configured)status-- one of"completed","current", or"pending"
During a subflow, the parent stage that triggered it is marked "current" and
all previously visited stages are "completed". The subflow's own stages do
not appear in the parent roadmap.
The stages field round-trips through to_dict() / from_dict().
create_transition_record Factory¶
The create_transition_record factory function accepts the subflow kwargs:
create_transition_record(
from_stage="configure",
to_stage="sub_start",
trigger="user_input",
subflow_push="child_flow", # optional
subflow_pop=None, # optional
subflow_depth=1, # optional, default 0
)
The timestamp is set automatically to time.time().
See Also¶
- Configuration Reference - Task definition in wizard config
- User Guide - Wizard reasoning tutorial
- Wizard Subflows Guide - Subflow configuration and data mapping
- Architecture - System design overview