fast-agent-mcp 0.4.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conversation statistics and analysis utilities.
|
|
3
|
+
|
|
4
|
+
This module provides ConversationSummary for analyzing message history
|
|
5
|
+
and extracting useful statistics like tool call counts, error rates, etc.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from collections import Counter
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, computed_field
|
|
12
|
+
|
|
13
|
+
from fast_agent.constants import FAST_AGENT_TIMING
|
|
14
|
+
from fast_agent.mcp.helpers.content_helpers import get_text
|
|
15
|
+
from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConversationSummary(BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
Analyzes a conversation's message history and provides computed statistics.
|
|
21
|
+
|
|
22
|
+
This class takes a list of PromptMessageExtended messages and provides
|
|
23
|
+
convenient computed properties for common statistics like tool call counts,
|
|
24
|
+
error rates, per-tool breakdowns, and timing information.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
```python
|
|
28
|
+
from fast_agent import ConversationSummary
|
|
29
|
+
|
|
30
|
+
# After running an agent
|
|
31
|
+
summary = ConversationSummary(agent.message_history)
|
|
32
|
+
|
|
33
|
+
# Access computed statistics
|
|
34
|
+
print(f"Tool calls: {summary.tool_calls}")
|
|
35
|
+
print(f"Tool errors: {summary.tool_errors}")
|
|
36
|
+
print(f"Error rate: {summary.tool_error_rate:.1%}")
|
|
37
|
+
print(f"Tool breakdown: {summary.tool_call_map}")
|
|
38
|
+
|
|
39
|
+
# Timing statistics
|
|
40
|
+
print(f"Total time: {summary.total_elapsed_time_ms}ms")
|
|
41
|
+
print(f"Avg response time: {summary.average_assistant_response_time_ms}ms")
|
|
42
|
+
|
|
43
|
+
# Export to dict for CSV/JSON
|
|
44
|
+
data = summary.model_dump()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
All computed properties are included in .model_dump() for easy serialization.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
messages: list[PromptMessageExtended]
|
|
51
|
+
|
|
52
|
+
@computed_field # type: ignore[prop-decorator]
|
|
53
|
+
@property
|
|
54
|
+
def message_count(self) -> int:
|
|
55
|
+
"""Total number of messages in the conversation."""
|
|
56
|
+
return len(self.messages)
|
|
57
|
+
|
|
58
|
+
@computed_field # type: ignore[prop-decorator]
|
|
59
|
+
@property
|
|
60
|
+
def user_message_count(self) -> int:
|
|
61
|
+
"""Number of messages from the user."""
|
|
62
|
+
return sum(1 for msg in self.messages if msg.role == "user")
|
|
63
|
+
|
|
64
|
+
@computed_field # type: ignore[prop-decorator]
|
|
65
|
+
@property
|
|
66
|
+
def assistant_message_count(self) -> int:
|
|
67
|
+
"""Number of messages from the assistant."""
|
|
68
|
+
return sum(1 for msg in self.messages if msg.role == "assistant")
|
|
69
|
+
|
|
70
|
+
@computed_field # type: ignore[prop-decorator]
|
|
71
|
+
@property
|
|
72
|
+
def tool_calls(self) -> int:
|
|
73
|
+
"""Total number of tool calls made across all messages."""
|
|
74
|
+
return sum(
|
|
75
|
+
len(msg.tool_calls) for msg in self.messages if msg.tool_calls
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@computed_field # type: ignore[prop-decorator]
|
|
79
|
+
@property
|
|
80
|
+
def tool_errors(self) -> int:
|
|
81
|
+
"""Total number of tool calls that resulted in errors."""
|
|
82
|
+
return sum(
|
|
83
|
+
sum(1 for result in msg.tool_results.values() if result.isError)
|
|
84
|
+
for msg in self.messages if msg.tool_results
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@computed_field # type: ignore[prop-decorator]
|
|
88
|
+
@property
|
|
89
|
+
def tool_successes(self) -> int:
|
|
90
|
+
"""Total number of tool calls that completed successfully."""
|
|
91
|
+
return sum(
|
|
92
|
+
sum(1 for result in msg.tool_results.values() if not result.isError)
|
|
93
|
+
for msg in self.messages if msg.tool_results
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@computed_field # type: ignore[prop-decorator]
|
|
97
|
+
@property
|
|
98
|
+
def tool_error_rate(self) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Proportion of tool calls that resulted in errors (0.0 to 1.0).
|
|
101
|
+
Returns 0.0 if there were no tool calls.
|
|
102
|
+
"""
|
|
103
|
+
total_results = self.tool_errors + self.tool_successes
|
|
104
|
+
if total_results == 0:
|
|
105
|
+
return 0.0
|
|
106
|
+
return self.tool_errors / total_results
|
|
107
|
+
|
|
108
|
+
@computed_field # type: ignore[prop-decorator]
|
|
109
|
+
@property
|
|
110
|
+
def tool_call_map(self) -> dict[str, int]:
|
|
111
|
+
"""
|
|
112
|
+
Mapping of tool names to the number of times they were called.
|
|
113
|
+
|
|
114
|
+
Example: {"fetch_weather": 3, "calculate": 1}
|
|
115
|
+
"""
|
|
116
|
+
tool_names: list[str] = []
|
|
117
|
+
for msg in self.messages:
|
|
118
|
+
if msg.tool_calls:
|
|
119
|
+
tool_names.extend(
|
|
120
|
+
call.params.name for call in msg.tool_calls.values()
|
|
121
|
+
)
|
|
122
|
+
return dict(Counter(tool_names))
|
|
123
|
+
|
|
124
|
+
@computed_field # type: ignore[prop-decorator]
|
|
125
|
+
@property
|
|
126
|
+
def tool_error_map(self) -> dict[str, int]:
|
|
127
|
+
"""
|
|
128
|
+
Mapping of tool names to the number of errors they produced.
|
|
129
|
+
|
|
130
|
+
Example: {"fetch_weather": 1, "invalid_tool": 2}
|
|
131
|
+
|
|
132
|
+
Note: This maps tool call IDs back to their original tool names by
|
|
133
|
+
finding corresponding CallToolRequest entries in assistant messages.
|
|
134
|
+
"""
|
|
135
|
+
# First, build a map from tool_id -> tool_name by scanning tool_calls
|
|
136
|
+
tool_id_to_name: dict[str, str] = {}
|
|
137
|
+
for msg in self.messages:
|
|
138
|
+
if msg.tool_calls:
|
|
139
|
+
for tool_id, call in msg.tool_calls.items():
|
|
140
|
+
tool_id_to_name[tool_id] = call.params.name
|
|
141
|
+
|
|
142
|
+
# Then, count errors by tool name
|
|
143
|
+
error_names: list[str] = []
|
|
144
|
+
for msg in self.messages:
|
|
145
|
+
if msg.tool_results:
|
|
146
|
+
for tool_id, result in msg.tool_results.items():
|
|
147
|
+
if result.isError:
|
|
148
|
+
# Look up the tool name from the tool_id
|
|
149
|
+
tool_name = tool_id_to_name.get(tool_id, "unknown")
|
|
150
|
+
error_names.append(tool_name)
|
|
151
|
+
|
|
152
|
+
return dict(Counter(error_names))
|
|
153
|
+
|
|
154
|
+
@computed_field # type: ignore[prop-decorator]
|
|
155
|
+
@property
|
|
156
|
+
def has_tool_calls(self) -> bool:
|
|
157
|
+
"""Whether any tool calls were made in this conversation."""
|
|
158
|
+
return self.tool_calls > 0
|
|
159
|
+
|
|
160
|
+
@computed_field # type: ignore[prop-decorator]
|
|
161
|
+
@property
|
|
162
|
+
def has_tool_errors(self) -> bool:
|
|
163
|
+
"""Whether any tool errors occurred in this conversation."""
|
|
164
|
+
return self.tool_errors > 0
|
|
165
|
+
|
|
166
|
+
@computed_field # type: ignore[prop-decorator]
|
|
167
|
+
@property
|
|
168
|
+
def total_elapsed_time_ms(self) -> float:
|
|
169
|
+
"""
|
|
170
|
+
Total elapsed time in milliseconds across all assistant message generations.
|
|
171
|
+
|
|
172
|
+
This sums the duration_ms from timing data stored in message channels.
|
|
173
|
+
Only messages with FAST_AGENT_TIMING channel data are included.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Total time in milliseconds, or 0.0 if no timing data is available.
|
|
177
|
+
"""
|
|
178
|
+
total = 0.0
|
|
179
|
+
for msg in self.messages:
|
|
180
|
+
if msg.role == "assistant" and msg.channels:
|
|
181
|
+
timing_blocks = msg.channels.get(FAST_AGENT_TIMING, [])
|
|
182
|
+
if timing_blocks:
|
|
183
|
+
try:
|
|
184
|
+
# Parse timing data from first block
|
|
185
|
+
timing_text = get_text(timing_blocks[0])
|
|
186
|
+
if timing_text:
|
|
187
|
+
timing_data = json.loads(timing_text)
|
|
188
|
+
total += timing_data.get("duration_ms", 0)
|
|
189
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
190
|
+
# Skip messages with invalid timing data
|
|
191
|
+
continue
|
|
192
|
+
return total
|
|
193
|
+
|
|
194
|
+
@computed_field # type: ignore[prop-decorator]
|
|
195
|
+
@property
|
|
196
|
+
def assistant_message_timings(self) -> list[dict[str, float]]:
|
|
197
|
+
"""
|
|
198
|
+
List of timing data for each assistant message.
|
|
199
|
+
|
|
200
|
+
Returns a list of dicts containing start_time, end_time, and duration_ms
|
|
201
|
+
for each assistant message that has timing data.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
[
|
|
205
|
+
{"start_time": 1234567890.123, "end_time": 1234567892.456, "duration_ms": 2333.0},
|
|
206
|
+
{"start_time": 1234567893.789, "end_time": 1234567895.012, "duration_ms": 1223.0},
|
|
207
|
+
]
|
|
208
|
+
"""
|
|
209
|
+
timings = []
|
|
210
|
+
for msg in self.messages:
|
|
211
|
+
if msg.role == "assistant" and msg.channels:
|
|
212
|
+
timing_blocks = msg.channels.get(FAST_AGENT_TIMING, [])
|
|
213
|
+
if timing_blocks:
|
|
214
|
+
try:
|
|
215
|
+
timing_text = get_text(timing_blocks[0])
|
|
216
|
+
if timing_text:
|
|
217
|
+
timing_data = json.loads(timing_text)
|
|
218
|
+
timings.append(timing_data)
|
|
219
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
220
|
+
# Skip messages with invalid timing data
|
|
221
|
+
continue
|
|
222
|
+
return timings
|
|
223
|
+
|
|
224
|
+
@computed_field # type: ignore[prop-decorator]
|
|
225
|
+
@property
|
|
226
|
+
def average_assistant_response_time_ms(self) -> float:
|
|
227
|
+
"""
|
|
228
|
+
Average response time in milliseconds for assistant messages.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Average time in milliseconds, or 0.0 if no timing data is available.
|
|
232
|
+
"""
|
|
233
|
+
timings = self.assistant_message_timings
|
|
234
|
+
if not timings:
|
|
235
|
+
return 0.0
|
|
236
|
+
total = sum(t.get("duration_ms", 0) for t in timings)
|
|
237
|
+
return total / len(timings)
|
|
238
|
+
|
|
239
|
+
@computed_field # type: ignore[prop-decorator]
|
|
240
|
+
@property
|
|
241
|
+
def first_llm_start_time(self) -> float | None:
|
|
242
|
+
"""
|
|
243
|
+
Timestamp when the first LLM call started.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Unix timestamp (from perf_counter) or None if no timing data.
|
|
247
|
+
"""
|
|
248
|
+
timings = self.assistant_message_timings
|
|
249
|
+
if not timings:
|
|
250
|
+
return None
|
|
251
|
+
return timings[0].get("start_time")
|
|
252
|
+
|
|
253
|
+
@computed_field # type: ignore[prop-decorator]
|
|
254
|
+
@property
|
|
255
|
+
def last_llm_end_time(self) -> float | None:
|
|
256
|
+
"""
|
|
257
|
+
Timestamp when the last LLM call ended.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Unix timestamp (from perf_counter) or None if no timing data.
|
|
261
|
+
"""
|
|
262
|
+
timings = self.assistant_message_timings
|
|
263
|
+
if not timings:
|
|
264
|
+
return None
|
|
265
|
+
return timings[-1].get("end_time")
|
|
266
|
+
|
|
267
|
+
@computed_field # type: ignore[prop-decorator]
|
|
268
|
+
@property
|
|
269
|
+
def conversation_span_ms(self) -> float:
|
|
270
|
+
"""
|
|
271
|
+
Wall-clock time from first LLM call start to last LLM call end.
|
|
272
|
+
|
|
273
|
+
This represents the active conversation time, including:
|
|
274
|
+
- All LLM inference time
|
|
275
|
+
- All tool execution time between LLM calls
|
|
276
|
+
- Agent orchestration overhead between turns
|
|
277
|
+
|
|
278
|
+
This is different from total_elapsed_time_ms which only sums LLM call durations.
|
|
279
|
+
|
|
280
|
+
Example:
|
|
281
|
+
If you have 3 LLM calls (2s, 1.5s, 1s) with tool execution in between:
|
|
282
|
+
- total_elapsed_time_ms = 4500ms (sum of LLM times only)
|
|
283
|
+
- conversation_span_ms = 9000ms (first start to last end, includes everything)
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Time in milliseconds, or 0.0 if no timing data is available.
|
|
287
|
+
"""
|
|
288
|
+
first_start = self.first_llm_start_time
|
|
289
|
+
last_end = self.last_llm_end_time
|
|
290
|
+
|
|
291
|
+
if first_start is None or last_end is None:
|
|
292
|
+
return 0.0
|
|
293
|
+
|
|
294
|
+
return round((last_end - first_start) * 1000, 2)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""LLM-related type definitions for fast-agent."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LlmStopReason(str, Enum):
|
|
8
|
+
"""
|
|
9
|
+
Enumeration of stop reasons for LLM message generation.
|
|
10
|
+
|
|
11
|
+
Extends the MCP SDK's standard stop reasons with additional custom values.
|
|
12
|
+
Inherits from str to ensure compatibility with string-based APIs.
|
|
13
|
+
Used primarily in PromptMessageExtended and LLM response handling.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# MCP SDK standard values (from mcp.types.StopReason)
|
|
17
|
+
END_TURN = "endTurn"
|
|
18
|
+
STOP_SEQUENCE = "stopSequence"
|
|
19
|
+
MAX_TOKENS = "maxTokens"
|
|
20
|
+
TOOL_USE = "toolUse" # Used when LLM stops to call tools
|
|
21
|
+
PAUSE = "pause"
|
|
22
|
+
|
|
23
|
+
# Custom extensions for fast-agent
|
|
24
|
+
ERROR = "error" # Used when there's an error in generation
|
|
25
|
+
CANCELLED = "cancelled" # Used when generation is cancelled by user
|
|
26
|
+
|
|
27
|
+
TIMEOUT = "timeout" # Used when generation times out
|
|
28
|
+
SAFETY = "safety" # a safety or content warning was triggered
|
|
29
|
+
|
|
30
|
+
def __eq__(self, other: object) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Allow comparison with both enum members and raw strings.
|
|
33
|
+
|
|
34
|
+
This enables code like:
|
|
35
|
+
- result.stopReason == LlmStopReason.END_TURN
|
|
36
|
+
- result.stopReason == "endTurn"
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(other, str):
|
|
39
|
+
return self.value == other
|
|
40
|
+
return super().__eq__(other)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_string(cls, value: Union[str, "LlmStopReason"]) -> "LlmStopReason":
|
|
44
|
+
"""
|
|
45
|
+
Convert a string to a LlmStopReason enum member.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
value: A string or LlmStopReason enum member
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The corresponding LlmStopReason enum member
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If the string doesn't match any enum value
|
|
55
|
+
"""
|
|
56
|
+
if isinstance(value, cls):
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
for member in cls:
|
|
60
|
+
if member.value == value:
|
|
61
|
+
return member
|
|
62
|
+
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Invalid stop reason: {value}. Valid values are: {[m.value for m in cls]}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def is_valid(cls, value: str) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check if a string is a valid stop reason.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
value: A string to check
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if the string matches a valid stop reason, False otherwise
|
|
77
|
+
"""
|
|
78
|
+
return value in [member.value for member in cls]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for searching and extracting content from message histories.
|
|
3
|
+
|
|
4
|
+
This module provides functions to search through PromptMessageExtended lists
|
|
5
|
+
for content matching patterns, with filtering by message role and content type.
|
|
6
|
+
|
|
7
|
+
Search Scopes:
|
|
8
|
+
--------------
|
|
9
|
+
- "user": Searches in user message content blocks (text content only)
|
|
10
|
+
- "assistant": Searches in assistant message content blocks (text content only)
|
|
11
|
+
- "tool_calls": Searches in tool call names AND stringified arguments
|
|
12
|
+
- "tool_results": Searches in tool result content blocks (text content)
|
|
13
|
+
- "all": Searches all of the above (default)
|
|
14
|
+
|
|
15
|
+
Note: The search looks at text content extracted with get_text(), not raw ContentBlock objects.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from typing import Literal
|
|
20
|
+
|
|
21
|
+
from fast_agent.mcp.helpers.content_helpers import get_text
|
|
22
|
+
from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
|
|
23
|
+
|
|
24
|
+
SearchScope = Literal["user", "assistant", "tool_calls", "tool_results", "all"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def search_messages(
|
|
28
|
+
messages: list[PromptMessageExtended],
|
|
29
|
+
pattern: str | re.Pattern,
|
|
30
|
+
scope: SearchScope = "all",
|
|
31
|
+
) -> list[PromptMessageExtended]:
|
|
32
|
+
"""
|
|
33
|
+
Find messages containing content that matches a pattern.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
messages: List of messages to search
|
|
37
|
+
pattern: String or compiled regex pattern to search for
|
|
38
|
+
scope: Where to search - "user", "assistant", "tool_calls", "tool_results", or "all"
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of messages that contain at least one match
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```python
|
|
45
|
+
# Find messages with error content
|
|
46
|
+
error_messages = search_messages(
|
|
47
|
+
agent.message_history,
|
|
48
|
+
r"error|failed",
|
|
49
|
+
scope="tool_results"
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
"""
|
|
53
|
+
compiled_pattern = re.compile(pattern) if isinstance(pattern, str) else pattern
|
|
54
|
+
matching_messages = []
|
|
55
|
+
|
|
56
|
+
for msg in messages:
|
|
57
|
+
if _message_contains_pattern(msg, compiled_pattern, scope):
|
|
58
|
+
matching_messages.append(msg)
|
|
59
|
+
|
|
60
|
+
return matching_messages
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find_matches(
|
|
64
|
+
messages: list[PromptMessageExtended],
|
|
65
|
+
pattern: str | re.Pattern,
|
|
66
|
+
scope: SearchScope = "all",
|
|
67
|
+
) -> list[tuple[PromptMessageExtended, re.Match]]:
|
|
68
|
+
"""
|
|
69
|
+
Find all pattern matches in messages, returning match objects.
|
|
70
|
+
|
|
71
|
+
This is useful when you need access to match groups or match positions.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
messages: List of messages to search
|
|
75
|
+
pattern: String or compiled regex pattern to search for
|
|
76
|
+
scope: Where to search - "user", "assistant", "tool_calls", "tool_results", or "all"
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
List of (message, match) tuples for each match found
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
```python
|
|
83
|
+
# Extract job IDs with capture groups
|
|
84
|
+
matches = find_matches(
|
|
85
|
+
agent.message_history,
|
|
86
|
+
r"Job started: ([a-f0-9]+)",
|
|
87
|
+
scope="tool_results"
|
|
88
|
+
)
|
|
89
|
+
for msg, match in matches:
|
|
90
|
+
job_id = match.group(1)
|
|
91
|
+
print(f"Found job: {job_id}")
|
|
92
|
+
```
|
|
93
|
+
"""
|
|
94
|
+
compiled_pattern = re.compile(pattern) if isinstance(pattern, str) else pattern
|
|
95
|
+
results = []
|
|
96
|
+
|
|
97
|
+
for msg in messages:
|
|
98
|
+
matches = _find_in_message(msg, compiled_pattern, scope)
|
|
99
|
+
for match in matches:
|
|
100
|
+
results.append((msg, match))
|
|
101
|
+
|
|
102
|
+
return results
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def extract_first(
|
|
106
|
+
messages: list[PromptMessageExtended],
|
|
107
|
+
pattern: str | re.Pattern,
|
|
108
|
+
scope: SearchScope = "all",
|
|
109
|
+
group: int = 0,
|
|
110
|
+
) -> str | None:
|
|
111
|
+
"""
|
|
112
|
+
Extract the first match from messages.
|
|
113
|
+
|
|
114
|
+
This is a convenience function for the common case of extracting a single value.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
messages: List of messages to search
|
|
118
|
+
pattern: String or compiled regex pattern to search for
|
|
119
|
+
scope: Where to search - "user", "assistant", "tool_calls", "tool_results", or "all"
|
|
120
|
+
group: Regex group to extract (0 = whole match, 1+ = capture groups)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Extracted string or None if no match found
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
```python
|
|
127
|
+
# Extract job ID in one line
|
|
128
|
+
job_id = extract_first(
|
|
129
|
+
agent.message_history,
|
|
130
|
+
r"Job started: ([a-f0-9]+)",
|
|
131
|
+
scope="tool_results",
|
|
132
|
+
group=1
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
"""
|
|
136
|
+
matches = find_matches(messages, pattern, scope)
|
|
137
|
+
if not matches:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
_, match = matches[0]
|
|
141
|
+
return match.group(group)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def extract_last(
|
|
145
|
+
messages: list[PromptMessageExtended],
|
|
146
|
+
pattern: str | re.Pattern,
|
|
147
|
+
scope: SearchScope = "all",
|
|
148
|
+
group: int = 0,
|
|
149
|
+
) -> str | None:
|
|
150
|
+
"""
|
|
151
|
+
Extract the last match from messages.
|
|
152
|
+
|
|
153
|
+
This is useful when you want the most recent occurrence of a pattern,
|
|
154
|
+
such as the final status update or most recent job ID.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
messages: List of messages to search
|
|
158
|
+
pattern: String or compiled regex pattern to search for
|
|
159
|
+
scope: Where to search - "user", "assistant", "tool_calls", "tool_results", or "all"
|
|
160
|
+
group: Regex group to extract (0 = whole match, 1+ = capture groups)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Extracted string or None if no match found
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
```python
|
|
167
|
+
# Extract the most recent status update
|
|
168
|
+
final_status = extract_last(
|
|
169
|
+
agent.message_history,
|
|
170
|
+
r"Status: (\\w+)",
|
|
171
|
+
scope="tool_results",
|
|
172
|
+
group=1
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
"""
|
|
176
|
+
matches = find_matches(messages, pattern, scope)
|
|
177
|
+
if not matches:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
_, match = matches[-1]
|
|
181
|
+
return match.group(group)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _message_contains_pattern(
|
|
185
|
+
msg: PromptMessageExtended,
|
|
186
|
+
pattern: re.Pattern,
|
|
187
|
+
scope: SearchScope,
|
|
188
|
+
) -> bool:
|
|
189
|
+
"""Check if a message contains the pattern in the specified scope."""
|
|
190
|
+
texts = _extract_searchable_text(msg, scope)
|
|
191
|
+
for text in texts:
|
|
192
|
+
if pattern.search(text):
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _find_in_message(
|
|
198
|
+
msg: PromptMessageExtended,
|
|
199
|
+
pattern: re.Pattern,
|
|
200
|
+
scope: SearchScope,
|
|
201
|
+
) -> list[re.Match]:
|
|
202
|
+
"""Find all matches of pattern in a message."""
|
|
203
|
+
texts = _extract_searchable_text(msg, scope)
|
|
204
|
+
matches = []
|
|
205
|
+
for text in texts:
|
|
206
|
+
for match in pattern.finditer(text):
|
|
207
|
+
matches.append(match)
|
|
208
|
+
return matches
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _extract_searchable_text(
|
|
212
|
+
msg: PromptMessageExtended,
|
|
213
|
+
scope: SearchScope,
|
|
214
|
+
) -> list[str]:
|
|
215
|
+
"""Extract text from message based on scope."""
|
|
216
|
+
texts = []
|
|
217
|
+
|
|
218
|
+
# User content
|
|
219
|
+
if scope in ("user", "all") and msg.role == "user":
|
|
220
|
+
for content in msg.content:
|
|
221
|
+
text = get_text(content)
|
|
222
|
+
if text:
|
|
223
|
+
texts.append(text)
|
|
224
|
+
|
|
225
|
+
# Assistant content
|
|
226
|
+
if scope in ("assistant", "all") and msg.role == "assistant":
|
|
227
|
+
for content in msg.content:
|
|
228
|
+
text = get_text(content)
|
|
229
|
+
if text:
|
|
230
|
+
texts.append(text)
|
|
231
|
+
|
|
232
|
+
# Tool calls (search in tool names and serialized arguments)
|
|
233
|
+
if scope in ("tool_calls", "all") and msg.tool_calls:
|
|
234
|
+
for tool_call in msg.tool_calls.values():
|
|
235
|
+
# Add tool name
|
|
236
|
+
texts.append(tool_call.params.name)
|
|
237
|
+
# Add stringified arguments
|
|
238
|
+
if tool_call.params.arguments:
|
|
239
|
+
texts.append(str(tool_call.params.arguments))
|
|
240
|
+
|
|
241
|
+
# Tool results
|
|
242
|
+
if scope in ("tool_results", "all") and msg.tool_results:
|
|
243
|
+
for tool_result in msg.tool_results.values():
|
|
244
|
+
for content in tool_result.content:
|
|
245
|
+
text = get_text(content)
|
|
246
|
+
if text:
|
|
247
|
+
texts.append(text)
|
|
248
|
+
|
|
249
|
+
return texts
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""UI utilities and primitives for interactive console features.
|
|
2
|
+
|
|
3
|
+
Design goals:
|
|
4
|
+
- Keep import side-effects minimal to avoid circular imports.
|
|
5
|
+
- Make primitives easy to access with lazy attribute loading.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ElicitationForm",
|
|
12
|
+
"show_simple_elicitation_form",
|
|
13
|
+
"form_dialog",
|
|
14
|
+
"ELICITATION_STYLE",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def __getattr__(name: str) -> Any:
|
|
19
|
+
"""Lazy attribute loader to avoid importing heavy modules at package import time."""
|
|
20
|
+
if name == "ELICITATION_STYLE":
|
|
21
|
+
from .elicitation_style import ELICITATION_STYLE as _STYLE
|
|
22
|
+
|
|
23
|
+
return _STYLE
|
|
24
|
+
if name in ("ElicitationForm", "show_simple_elicitation_form", "form_dialog"):
|
|
25
|
+
from .elicitation_form import (
|
|
26
|
+
ElicitationForm as _Form,
|
|
27
|
+
)
|
|
28
|
+
from .elicitation_form import (
|
|
29
|
+
show_simple_elicitation_form as _show,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if name == "ElicitationForm":
|
|
33
|
+
return _Form
|
|
34
|
+
if name == "show_simple_elicitation_form":
|
|
35
|
+
return _show
|
|
36
|
+
if name == "form_dialog":
|
|
37
|
+
return _show
|
|
38
|
+
raise AttributeError(name)
|