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.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. 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)