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,5 @@
1
+ """Logging module for MCP Agent."""
2
+
3
+ from fast_agent.core.logging.logger import Logger, LoggingConfig, get_logger
4
+
5
+ __all__ = ["Logger", "LoggingConfig", "get_logger"]
@@ -0,0 +1,138 @@
1
+ """
2
+ Events and event filters for the logger module for the MCP Agent
3
+ """
4
+
5
+ import logging
6
+ import random
7
+ from datetime import datetime
8
+ from typing import Any, Literal
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+
12
+ EventType = Literal["debug", "info", "warning", "error", "progress"]
13
+ """Broad categories for events (severity or role)."""
14
+
15
+
16
+ class EventContext(BaseModel):
17
+ """
18
+ Stores correlation or cross-cutting data (workflow IDs, user IDs, etc.).
19
+ Also used for distributed environments or advanced logging.
20
+ """
21
+
22
+ session_id: str | None = None
23
+ workflow_id: str | None = None
24
+ # request_id: str | None = None
25
+ # parent_event_id: str | None = None
26
+ # correlation_id: str | None = None
27
+ # user_id: str | None = None
28
+
29
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
30
+
31
+
32
+ class Event(BaseModel):
33
+ """
34
+ Core event structure. Allows both a broad 'type' (EventType)
35
+ and a more specific 'name' string for domain-specific labeling (e.g. "ORDER_PLACED").
36
+ """
37
+
38
+ type: EventType
39
+ name: str | None = None
40
+ namespace: str
41
+ message: str
42
+ timestamp: datetime = Field(default_factory=datetime.now)
43
+ data: dict[str, Any] = Field(default_factory=dict)
44
+ context: EventContext | None = None
45
+
46
+ # For distributed tracing
47
+ span_id: str | None = None
48
+ trace_id: str | None = None
49
+
50
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
51
+
52
+
53
+ class EventFilter(BaseModel):
54
+ """
55
+ Filter events by:
56
+ - allowed EventTypes (types)
57
+ - allowed event 'names'
58
+ - allowed namespace prefixes
59
+ - a minimum severity level (DEBUG < INFO < WARNING < ERROR)
60
+ """
61
+
62
+ types: set[EventType] = Field(default_factory=set)
63
+ names: set[str] = Field(default_factory=set)
64
+ namespaces: set[str] = Field(default_factory=set)
65
+ min_level: EventType | None = "debug"
66
+
67
+ def matches(self, event: Event) -> bool:
68
+ """
69
+ Check if an event matches this EventFilter criteria.
70
+ """
71
+ # 1) Filter by broad event type
72
+ if self.types:
73
+ if event.type not in self.types:
74
+ return False
75
+
76
+ # 2) Filter by custom event name
77
+ if self.names:
78
+ if not event.name or event.name not in self.names:
79
+ return False
80
+
81
+ # 3) Filter by namespace prefix
82
+ if self.namespaces and not any(event.namespace.startswith(ns) for ns in self.namespaces):
83
+ return False
84
+
85
+ # 4) Minimum severity
86
+ if self.min_level:
87
+ level_map: dict[EventType, int] = {
88
+ "debug": logging.DEBUG,
89
+ "info": logging.INFO,
90
+ "warning": logging.WARNING,
91
+ "error": logging.ERROR,
92
+ }
93
+
94
+ min_val = level_map.get(self.min_level, logging.DEBUG)
95
+ event_val = level_map.get(event.type, logging.DEBUG)
96
+ if event_val < min_val:
97
+ return False
98
+
99
+ return True
100
+
101
+
102
+ class SamplingFilter(EventFilter):
103
+ """
104
+ Random sampling on top of base filter.
105
+ Only pass an event if it meets the base filter AND random() < sample_rate.
106
+ """
107
+
108
+ sample_rate: float = 0.1
109
+ """Fraction of events to pass through"""
110
+
111
+ def matches(self, event: Event) -> bool:
112
+ if not super().matches(event):
113
+ return False
114
+ return random.random() < self.sample_rate
115
+
116
+
117
+ class StreamingExclusionFilter(EventFilter):
118
+ """
119
+ Event filter that excludes streaming progress events from logs.
120
+ This prevents token count updates from flooding the logs when info level is enabled.
121
+ """
122
+
123
+ def matches(self, event: Event) -> bool:
124
+ # First check if it passes the base filter
125
+ if not super().matches(event):
126
+ return False
127
+
128
+ # Exclude events with "Streaming progress" message
129
+ if event.message == "Streaming progress":
130
+ return False
131
+
132
+ # Also check for events with progress_action = STREAMING in data
133
+ if event.data and isinstance(event.data.get("data"), dict):
134
+ event_data = event.data["data"]
135
+ if event_data.get("progress_action") == "Streaming":
136
+ return False
137
+
138
+ return True
@@ -0,0 +1,164 @@
1
+ import dataclasses
2
+ import inspect
3
+ import os
4
+ import warnings
5
+ from datetime import date, datetime
6
+ from decimal import Decimal
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Any, Iterable
10
+ from uuid import UUID
11
+
12
+ import httpx
13
+
14
+ from fast_agent.core.logging import logger
15
+
16
+
17
+ class JSONSerializer:
18
+ """
19
+ A robust JSON serializer that handles various Python objects by attempting
20
+ different serialization strategies recursively.
21
+ """
22
+
23
+ MAX_DEPTH = 99 # Maximum recursion depth
24
+
25
+ # Fields that are likely to contain sensitive information
26
+ SENSITIVE_FIELDS = {
27
+ "api_key",
28
+ "secret",
29
+ "password",
30
+ "token",
31
+ "auth",
32
+ "private_key",
33
+ "client_secret",
34
+ "access_token",
35
+ "refresh_token",
36
+ }
37
+
38
+ def __init__(self) -> None:
39
+ # Set of already processed objects to prevent infinite recursion
40
+ self._processed_objects: set[int] = set()
41
+ # Check if secrets should be logged in full
42
+ self._log_secrets = os.getenv("LOG_SECRETS", "").upper() == "TRUE"
43
+
44
+ def _redact_sensitive_value(self, value: str) -> str:
45
+ """Redact sensitive values to show only first 10 chars."""
46
+ if not value or not isinstance(value, str):
47
+ return value
48
+ if self._log_secrets:
49
+ return value
50
+ if len(value) <= 10:
51
+ return value + "....."
52
+ return value[:10] + "....."
53
+
54
+ def serialize(self, obj: Any) -> Any:
55
+ """Main entry point for serialization."""
56
+ # Reset processed objects for new serialization
57
+ self._processed_objects.clear()
58
+ return self._serialize_object(obj, depth=0)
59
+
60
+ def _is_sensitive_key(self, key: str) -> bool:
61
+ """Check if a key likely contains sensitive information."""
62
+ key = str(key).lower()
63
+ return any(sensitive in key for sensitive in self.SENSITIVE_FIELDS)
64
+
65
+ def _serialize_object(self, obj: Any, depth: int = 0) -> Any:
66
+ """Recursively serialize an object using various strategies."""
67
+ # Handle None
68
+ if obj is None:
69
+ return None
70
+
71
+ if depth == 0:
72
+ self._parent_obj = obj
73
+ # Check depth
74
+ if depth > self.MAX_DEPTH:
75
+ warnings.warn(
76
+ f"Maximum recursion depth ({self.MAX_DEPTH}) exceeded while serializing object of type {type(obj).__name__} parent: {type(self._parent_obj).__name__}"
77
+ )
78
+ return str(obj)
79
+
80
+ # Prevent infinite recursion
81
+ obj_id = id(obj)
82
+ if obj_id in self._processed_objects:
83
+ return str(obj)
84
+ self._processed_objects.add(obj_id)
85
+
86
+ # Try different serialization strategies in order
87
+ try:
88
+ if isinstance(obj, httpx.Response):
89
+ return f"<httpx.Response [{obj.status_code}] {obj.url}>"
90
+
91
+ if isinstance(obj, logger.Logger):
92
+ return "<logging: logger>"
93
+
94
+ # Basic JSON-serializable types
95
+ if isinstance(obj, (str, int, float, bool)):
96
+ return obj
97
+
98
+ # Handle common built-in types
99
+ if isinstance(obj, (datetime, date)):
100
+ return obj.isoformat()
101
+ if isinstance(obj, (Decimal, UUID)):
102
+ return str(obj)
103
+ if isinstance(obj, Path):
104
+ return str(obj)
105
+ if isinstance(obj, Enum):
106
+ return obj.value
107
+
108
+ # Handle callables
109
+ if callable(obj):
110
+ return f"<callable: {obj.__name__}>"
111
+
112
+ # Handle Pydantic models
113
+ if hasattr(obj, "model_dump"): # Pydantic v2
114
+ return self._serialize_object(obj.model_dump())
115
+ if hasattr(obj, "dict"): # Pydantic v1
116
+ return self._serialize_object(obj.dict())
117
+
118
+ # Handle dataclasses
119
+ if dataclasses.is_dataclass(obj):
120
+ return self._serialize_object(dataclasses.asdict(obj))
121
+
122
+ # Handle objects with custom serialization method
123
+ if hasattr(obj, "to_json"):
124
+ return self._serialize_object(obj.to_json())
125
+ if hasattr(obj, "to_dict"):
126
+ return self._serialize_object(obj.to_dict())
127
+
128
+ # Handle dictionaries with sensitive data redaction
129
+ if isinstance(obj, dict):
130
+ return {
131
+ str(key): self._redact_sensitive_value(value)
132
+ if self._is_sensitive_key(key)
133
+ else self._serialize_object(value, depth + 1)
134
+ for key, value in obj.items()
135
+ }
136
+
137
+ # Handle iterables (lists, tuples, sets)
138
+ if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)):
139
+ return [self._serialize_object(item, depth + 1) for item in obj]
140
+
141
+ # Handle objects with __dict__
142
+ if hasattr(obj, "__dict__"):
143
+ return self._serialize_object(obj.__dict__, depth + 1)
144
+
145
+ # Handle objects with attributes
146
+ if inspect.getmembers(obj):
147
+ return {
148
+ name: self._redact_sensitive_value(value)
149
+ if self._is_sensitive_key(name)
150
+ else self._serialize_object(value, depth + 1)
151
+ for name, value in inspect.getmembers(obj)
152
+ if not name.startswith("_") and not inspect.ismethod(value)
153
+ }
154
+
155
+ # Fallback: convert to string
156
+ return str(obj)
157
+
158
+ except Exception as e:
159
+ # If all serialization attempts fail, return string representation
160
+ return f"<unserializable: {type(obj).__name__}, error: {str(e)}>"
161
+
162
+ def __call__(self, obj: Any) -> Any:
163
+ """Make the serializer callable."""
164
+ return self.serialize(obj)
@@ -0,0 +1,309 @@
1
+ """
2
+ Listeners for the logger module of MCP Agent.
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from abc import ABC, abstractmethod
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from fast_agent.event_progress import ProgressEvent
13
+
14
+ from fast_agent.core.logging.events import Event, EventFilter, EventType
15
+
16
+
17
+ def convert_log_event(event: Event) -> "ProgressEvent | None":
18
+ """Convert a log event to a progress event if applicable."""
19
+
20
+ # Import at runtime to avoid circular imports
21
+ from fast_agent.event_progress import ProgressAction, ProgressEvent
22
+
23
+ # Check to see if there is any additional data
24
+ if not event.data:
25
+ return None
26
+
27
+ event_data = event.data.get("data")
28
+ if not isinstance(event_data, dict):
29
+ return None
30
+
31
+ raw_action = event_data.get("progress_action")
32
+ if not raw_action:
33
+ return None
34
+
35
+ # Coerce raw_action (enum or string) into a ProgressAction instance
36
+ try:
37
+ action = (
38
+ raw_action
39
+ if isinstance(raw_action, ProgressAction)
40
+ else ProgressAction(str(raw_action))
41
+ )
42
+ except Exception:
43
+ # If we cannot coerce, drop this event from progress handling
44
+ return None
45
+
46
+ # Build target string based on the event type.
47
+ # Progress display is currently [time] [event] --- [target] [details]
48
+ namespace = event.namespace
49
+ agent_name = event_data.get("agent_name")
50
+
51
+ target = agent_name
52
+ details = ""
53
+ if action == ProgressAction.FATAL_ERROR:
54
+ details = event_data.get("error_message", "An error occurred")
55
+ elif "mcp_aggregator" in namespace:
56
+ server_name = event_data.get("server_name", "")
57
+ tool_name = event_data.get("tool_name")
58
+ if tool_name:
59
+ # fetch(fetch)
60
+ details = f"{server_name} ({tool_name})"
61
+ else:
62
+ details = f"{server_name}"
63
+
64
+ # For TOOL_PROGRESS, use progress message if available, otherwise keep default
65
+ if action == ProgressAction.TOOL_PROGRESS:
66
+ progress_message = event_data.get("details", "")
67
+ if progress_message: # Only override if message is non-empty
68
+ details = progress_message
69
+
70
+ # TODO: there must be a better way :D?!
71
+ elif "llm" in namespace:
72
+ model = event_data.get("model", "")
73
+
74
+ # For all augmented_llm events, put model info in details column
75
+ details = f"{model}"
76
+ chat_turn = event_data.get("chat_turn")
77
+ if chat_turn is not None:
78
+ details = f"{model} turn {chat_turn}"
79
+
80
+ tool_name = event_data.get("tool_name")
81
+ tool_event = event_data.get("tool_event")
82
+ if tool_name:
83
+ tool_suffix = tool_name
84
+ if tool_event:
85
+ tool_suffix = f"{tool_suffix} ({tool_event})"
86
+ details = f"{details} • {tool_suffix}".strip()
87
+ else:
88
+ if not target:
89
+ target = event_data.get("target", "unknown")
90
+
91
+ # Extract streaming token count for STREAMING/THINKING actions
92
+ streaming_tokens = None
93
+ if action == ProgressAction.STREAMING or action == ProgressAction.THINKING:
94
+ streaming_tokens = event_data.get("details", "")
95
+
96
+ # Extract progress data for TOOL_PROGRESS actions
97
+ progress = None
98
+ total = None
99
+ if action == ProgressAction.TOOL_PROGRESS:
100
+ progress = event_data.get("progress")
101
+ total = event_data.get("total")
102
+
103
+ return ProgressEvent(
104
+ action=action,
105
+ target=target or "unknown",
106
+ details=details,
107
+ agent_name=event_data.get("agent_name"),
108
+ streaming_tokens=streaming_tokens,
109
+ progress=progress,
110
+ total=total,
111
+ )
112
+
113
+
114
+ class EventListener(ABC):
115
+ """Base async listener that processes events."""
116
+
117
+ @abstractmethod
118
+ async def handle_event(self, event: Event):
119
+ """Process an incoming event."""
120
+
121
+
122
+ class LifecycleAwareListener(EventListener):
123
+ """
124
+ Optionally override start()/stop() for setup/teardown.
125
+ The event bus calls these at bus start/stop time.
126
+ """
127
+
128
+ async def start(self) -> None:
129
+ """Start an event listener, usually when the event bus is set up."""
130
+ pass
131
+
132
+ async def stop(self) -> None:
133
+ """Stop an event listener, usually when the event bus is shutting down."""
134
+ pass
135
+
136
+
137
+ class FilteredListener(LifecycleAwareListener):
138
+ """
139
+ Only processes events that pass the given filter.
140
+ Subclasses override _handle_matched_event().
141
+ """
142
+
143
+ def __init__(self, event_filter: EventFilter | None = None) -> None:
144
+ """
145
+ Initialize the listener.
146
+ Args:
147
+ filter: Event filter to apply to incoming events.
148
+ """
149
+ self.filter = event_filter
150
+
151
+ async def handle_event(self, event) -> None:
152
+ if not self.filter or self.filter.matches(event):
153
+ await self.handle_matched_event(event)
154
+
155
+ async def handle_matched_event(self, event: Event) -> None:
156
+ """Process an event that matches the filter."""
157
+ pass
158
+
159
+
160
+ class LoggingListener(FilteredListener):
161
+ """
162
+ Routes events to Python's logging facility with appropriate severity level.
163
+ """
164
+
165
+ def __init__(
166
+ self,
167
+ event_filter: EventFilter | None = None,
168
+ logger: logging.Logger | None = None,
169
+ ) -> None:
170
+ """
171
+ Initialize the listener.
172
+ Args:
173
+ logger: Logger to use for event processing. Defaults to 'fast_agent'.
174
+ """
175
+ super().__init__(event_filter=event_filter)
176
+ self.logger = logger or logging.getLogger("fast_agent")
177
+
178
+ async def handle_matched_event(self, event) -> None:
179
+ level_map: dict[EventType, int] = {
180
+ "debug": logging.DEBUG,
181
+ "info": logging.INFO,
182
+ "warning": logging.WARNING,
183
+ "error": logging.ERROR,
184
+ }
185
+ level = level_map.get(event.type, logging.INFO)
186
+
187
+ # Check if this is a server stderr message and format accordingly
188
+ if event.name == "mcpserver.stderr":
189
+ message = f"MCP Server: {event.message}"
190
+ else:
191
+ message = event.message
192
+
193
+ self.logger.log(
194
+ level,
195
+ "[%s] %s",
196
+ event.namespace,
197
+ message,
198
+ extra={
199
+ "event_data": event.data,
200
+ "span_id": event.span_id,
201
+ "trace_id": event.trace_id,
202
+ "event_name": event.name,
203
+ },
204
+ )
205
+
206
+
207
+ class ProgressListener(LifecycleAwareListener):
208
+ """
209
+ Listens for all events pre-filtering and converts them to progress events
210
+ for display. By inheriting directly from LifecycleAwareListener instead of
211
+ FilteredListener, we get events before any filtering occurs.
212
+ """
213
+
214
+ def __init__(self, display=None) -> None:
215
+ """Initialize the progress listener.
216
+ Args:
217
+ display: Optional display handler. If None, the shared progress_display will be used.
218
+ """
219
+ from fast_agent.ui.progress_display import progress_display
220
+
221
+ self.display = display or progress_display
222
+
223
+ async def start(self) -> None:
224
+ """Start the progress display."""
225
+ self.display.start()
226
+
227
+ async def stop(self) -> None:
228
+ """Stop the progress display."""
229
+ self.display.stop()
230
+
231
+ async def handle_event(self, event: Event) -> None:
232
+ """Process an incoming event and display progress if relevant."""
233
+
234
+ if event.data:
235
+ progress_event = convert_log_event(event)
236
+ if progress_event:
237
+ self.display.update(progress_event)
238
+
239
+
240
+ class BatchingListener(FilteredListener):
241
+ """
242
+ Accumulates events in memory, flushes them in batches.
243
+ Here we just print the batch size, but you might store or forward them.
244
+ """
245
+
246
+ def __init__(
247
+ self,
248
+ event_filter: EventFilter | None = None,
249
+ batch_size: int = 5,
250
+ flush_interval: float = 2.0,
251
+ ) -> None:
252
+ """
253
+ Initialize the listener.
254
+ Args:
255
+ batch_size: Number of events to accumulate before flushing.
256
+ flush_interval: Time in seconds to wait before flushing events.
257
+ """
258
+ super().__init__(event_filter=event_filter)
259
+ self.batch_size = batch_size
260
+ self.flush_interval = flush_interval
261
+ self.batch: list[Event] = []
262
+ self.last_flush: float = time.time() # Time of last flush
263
+ self._flush_task: asyncio.Task | None = None # Task for periodic flush loop
264
+ self._stop_event = None # Event to signal flush task to stop
265
+
266
+ async def start(self, loop=None) -> None:
267
+ """Spawn a periodic flush loop."""
268
+ self._stop_event = asyncio.Event()
269
+ self._flush_task = asyncio.create_task(self._periodic_flush())
270
+
271
+ async def stop(self) -> None:
272
+ """Stop flush loop and flush any remaining events."""
273
+ if self._stop_event:
274
+ self._stop_event.set()
275
+
276
+ if self._flush_task and not self._flush_task.done():
277
+ self._flush_task.cancel()
278
+ await self._flush_task
279
+ self._flush_task = None
280
+ await self.flush()
281
+
282
+ async def _periodic_flush(self) -> None:
283
+ try:
284
+ while not self._stop_event.is_set():
285
+ try:
286
+ await asyncio.wait_for(self._stop_event.wait(), timeout=self.flush_interval)
287
+ except asyncio.TimeoutError:
288
+ await self.flush()
289
+ # except asyncio.CancelledError:
290
+ # break
291
+ finally:
292
+ await self.flush() # Final flush
293
+
294
+ async def handle_matched_event(self, event) -> None:
295
+ self.batch.append(event)
296
+ if len(self.batch) >= self.batch_size:
297
+ await self.flush()
298
+
299
+ async def flush(self) -> None:
300
+ """Flush the current batch of events."""
301
+ if not self.batch:
302
+ return
303
+ to_process = self.batch[:]
304
+ self.batch.clear()
305
+ self.last_flush = time.time()
306
+ await self._process_batch(to_process)
307
+
308
+ async def _process_batch(self, events: list[Event]) -> None:
309
+ pass