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,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
|