fast-agent-mcp 0.3.7__py3-none-any.whl → 0.3.9__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.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- fast_agent/agents/llm_agent.py +30 -8
- fast_agent/agents/llm_decorator.py +2 -7
- fast_agent/agents/mcp_agent.py +9 -4
- fast_agent/cli/commands/auth.py +14 -1
- fast_agent/core/direct_factory.py +20 -8
- fast_agent/core/logging/listeners.py +2 -1
- fast_agent/interfaces.py +2 -2
- fast_agent/llm/model_database.py +7 -1
- fast_agent/llm/model_factory.py +2 -3
- fast_agent/llm/provider/anthropic/llm_anthropic.py +107 -62
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +4 -3
- fast_agent/llm/provider/bedrock/llm_bedrock.py +1 -1
- fast_agent/llm/provider/google/google_converter.py +8 -41
- fast_agent/llm/provider/google/llm_google_native.py +1 -3
- fast_agent/llm/provider/openai/llm_azure.py +1 -1
- fast_agent/llm/provider/openai/llm_openai.py +3 -3
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +1 -1
- fast_agent/llm/request_params.py +1 -1
- fast_agent/mcp/mcp_agent_client_session.py +45 -2
- fast_agent/mcp/mcp_aggregator.py +282 -5
- fast_agent/mcp/mcp_connection_manager.py +86 -10
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/transport_tracking.py +598 -0
- fast_agent/resources/examples/data-analysis/analysis.py +7 -3
- fast_agent/ui/console_display.py +22 -1
- fast_agent/ui/enhanced_prompt.py +21 -1
- fast_agent/ui/interactive_prompt.py +5 -0
- fast_agent/ui/mcp_display.py +636 -0
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/METADATA +6 -6
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/RECORD +34 -30
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from mcp.types import (
|
|
10
|
+
JSONRPCError,
|
|
11
|
+
JSONRPCMessage,
|
|
12
|
+
JSONRPCNotification,
|
|
13
|
+
JSONRPCRequest,
|
|
14
|
+
JSONRPCResponse,
|
|
15
|
+
RequestId,
|
|
16
|
+
)
|
|
17
|
+
from pydantic import BaseModel, ConfigDict
|
|
18
|
+
|
|
19
|
+
ChannelName = Literal["post-json", "post-sse", "get", "resumption", "stdio"]
|
|
20
|
+
EventType = Literal["message", "connect", "disconnect", "keepalive", "error"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class ChannelEvent:
|
|
25
|
+
"""Event emitted by the tracking transport indicating channel activity."""
|
|
26
|
+
|
|
27
|
+
channel: ChannelName
|
|
28
|
+
event_type: EventType
|
|
29
|
+
message: JSONRPCMessage | None = None
|
|
30
|
+
raw_event: str | None = None
|
|
31
|
+
detail: str | None = None
|
|
32
|
+
status_code: int | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ModeStats:
|
|
37
|
+
messages: int = 0
|
|
38
|
+
request: int = 0
|
|
39
|
+
notification: int = 0
|
|
40
|
+
response: int = 0
|
|
41
|
+
last_summary: str | None = None
|
|
42
|
+
last_at: datetime | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _summarise_message(message: JSONRPCMessage) -> str:
|
|
46
|
+
root = message.root
|
|
47
|
+
if isinstance(root, JSONRPCRequest):
|
|
48
|
+
method = root.method or ""
|
|
49
|
+
return f"request {method}"
|
|
50
|
+
if isinstance(root, JSONRPCNotification):
|
|
51
|
+
method = root.method or ""
|
|
52
|
+
return f"notify {method}"
|
|
53
|
+
if isinstance(root, JSONRPCResponse):
|
|
54
|
+
return "response"
|
|
55
|
+
if isinstance(root, JSONRPCError):
|
|
56
|
+
code = getattr(root.error, "code", None)
|
|
57
|
+
return f"error {code}" if code is not None else "error"
|
|
58
|
+
return "message"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ChannelSnapshot(BaseModel):
|
|
62
|
+
"""Snapshot of aggregated activity for a single transport channel."""
|
|
63
|
+
|
|
64
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
65
|
+
|
|
66
|
+
message_count: int = 0
|
|
67
|
+
mode: str | None = None
|
|
68
|
+
mode_counts: dict[str, int] | None = None
|
|
69
|
+
last_message_summary: str | None = None
|
|
70
|
+
last_message_at: datetime | None = None
|
|
71
|
+
connected: bool | None = None
|
|
72
|
+
state: str | None = None
|
|
73
|
+
last_event: str | None = None
|
|
74
|
+
last_event_at: datetime | None = None
|
|
75
|
+
ping_count: int | None = None
|
|
76
|
+
ping_last_at: datetime | None = None
|
|
77
|
+
last_error: str | None = None
|
|
78
|
+
connect_at: datetime | None = None
|
|
79
|
+
disconnect_at: datetime | None = None
|
|
80
|
+
last_status_code: int | None = None
|
|
81
|
+
request_count: int = 0
|
|
82
|
+
response_count: int = 0
|
|
83
|
+
notification_count: int = 0
|
|
84
|
+
activity_buckets: list[str] | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TransportSnapshot(BaseModel):
|
|
88
|
+
"""Collection of channel snapshots for a transport."""
|
|
89
|
+
|
|
90
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
91
|
+
|
|
92
|
+
post: ChannelSnapshot | None = None
|
|
93
|
+
post_json: ChannelSnapshot | None = None
|
|
94
|
+
post_sse: ChannelSnapshot | None = None
|
|
95
|
+
get: ChannelSnapshot | None = None
|
|
96
|
+
resumption: ChannelSnapshot | None = None
|
|
97
|
+
stdio: ChannelSnapshot | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TransportChannelMetrics:
|
|
101
|
+
"""Aggregates low-level channel events into user-visible metrics."""
|
|
102
|
+
|
|
103
|
+
def __init__(self) -> None:
|
|
104
|
+
self._lock = Lock()
|
|
105
|
+
|
|
106
|
+
self._post_modes: set[str] = set()
|
|
107
|
+
self._post_count = 0
|
|
108
|
+
self._post_request_count = 0
|
|
109
|
+
self._post_response_count = 0
|
|
110
|
+
self._post_notification_count = 0
|
|
111
|
+
self._post_last_summary: str | None = None
|
|
112
|
+
self._post_last_at: datetime | None = None
|
|
113
|
+
self._post_mode_stats: dict[str, ModeStats] = {
|
|
114
|
+
"json": ModeStats(),
|
|
115
|
+
"sse": ModeStats(),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
self._get_connected = False
|
|
119
|
+
self._get_had_connection = False
|
|
120
|
+
self._get_connect_at: datetime | None = None
|
|
121
|
+
self._get_disconnect_at: datetime | None = None
|
|
122
|
+
self._get_last_summary: str | None = None
|
|
123
|
+
self._get_last_at: datetime | None = None
|
|
124
|
+
self._get_last_event: str | None = None
|
|
125
|
+
self._get_last_event_at: datetime | None = None
|
|
126
|
+
self._get_last_error: str | None = None
|
|
127
|
+
self._get_last_status_code: int | None = None
|
|
128
|
+
self._get_message_count = 0
|
|
129
|
+
self._get_request_count = 0
|
|
130
|
+
self._get_response_count = 0
|
|
131
|
+
self._get_notification_count = 0
|
|
132
|
+
self._get_ping_count = 0
|
|
133
|
+
self._get_last_ping_at: datetime | None = None
|
|
134
|
+
|
|
135
|
+
self._resumption_count = 0
|
|
136
|
+
self._resumption_last_summary: str | None = None
|
|
137
|
+
self._resumption_last_at: datetime | None = None
|
|
138
|
+
self._resumption_request_count = 0
|
|
139
|
+
self._resumption_response_count = 0
|
|
140
|
+
self._resumption_notification_count = 0
|
|
141
|
+
|
|
142
|
+
self._stdio_connected = False
|
|
143
|
+
self._stdio_had_connection = False
|
|
144
|
+
self._stdio_connect_at: datetime | None = None
|
|
145
|
+
self._stdio_disconnect_at: datetime | None = None
|
|
146
|
+
self._stdio_count = 0
|
|
147
|
+
self._stdio_last_summary: str | None = None
|
|
148
|
+
self._stdio_last_at: datetime | None = None
|
|
149
|
+
self._stdio_last_event: str | None = None
|
|
150
|
+
self._stdio_last_event_at: datetime | None = None
|
|
151
|
+
self._stdio_last_error: str | None = None
|
|
152
|
+
self._stdio_request_count = 0
|
|
153
|
+
self._stdio_response_count = 0
|
|
154
|
+
self._stdio_notification_count = 0
|
|
155
|
+
|
|
156
|
+
self._response_channel_by_id: dict[RequestId, ChannelName] = {}
|
|
157
|
+
|
|
158
|
+
self._history_bucket_seconds = 30
|
|
159
|
+
self._history_bucket_count = 20
|
|
160
|
+
self._history_priority = {
|
|
161
|
+
"error": 5,
|
|
162
|
+
"disabled": 4,
|
|
163
|
+
"request": 4,
|
|
164
|
+
"response": 3,
|
|
165
|
+
"notification": 2,
|
|
166
|
+
"ping": 2,
|
|
167
|
+
"none": 1,
|
|
168
|
+
}
|
|
169
|
+
self._history: dict[str, deque[tuple[int, str]]] = {
|
|
170
|
+
"post-json": deque(maxlen=self._history_bucket_count),
|
|
171
|
+
"post-sse": deque(maxlen=self._history_bucket_count),
|
|
172
|
+
"get": deque(maxlen=self._history_bucket_count),
|
|
173
|
+
"resumption": deque(maxlen=self._history_bucket_count),
|
|
174
|
+
"stdio": deque(maxlen=self._history_bucket_count),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def record_event(self, event: ChannelEvent) -> None:
|
|
178
|
+
now = datetime.now(timezone.utc)
|
|
179
|
+
with self._lock:
|
|
180
|
+
if event.channel in ("post-json", "post-sse"):
|
|
181
|
+
self._handle_post_event(event, now)
|
|
182
|
+
elif event.channel == "get":
|
|
183
|
+
self._handle_get_event(event, now)
|
|
184
|
+
elif event.channel == "resumption":
|
|
185
|
+
self._handle_resumption_event(event, now)
|
|
186
|
+
elif event.channel == "stdio":
|
|
187
|
+
self._handle_stdio_event(event, now)
|
|
188
|
+
|
|
189
|
+
def _handle_post_event(self, event: ChannelEvent, now: datetime) -> None:
|
|
190
|
+
mode = "json" if event.channel == "post-json" else "sse"
|
|
191
|
+
if event.event_type == "message" and event.message is not None:
|
|
192
|
+
self._post_modes.add(mode)
|
|
193
|
+
self._post_count += 1
|
|
194
|
+
|
|
195
|
+
mode_stats = self._post_mode_stats[mode]
|
|
196
|
+
mode_stats.messages += 1
|
|
197
|
+
|
|
198
|
+
classification = self._tally_message_counts("post", event.message, now, sub_mode=mode)
|
|
199
|
+
|
|
200
|
+
summary = "ping" if classification == "ping" else _summarise_message(event.message)
|
|
201
|
+
mode_stats.last_summary = summary
|
|
202
|
+
mode_stats.last_at = now
|
|
203
|
+
self._post_last_summary = summary
|
|
204
|
+
self._post_last_at = now
|
|
205
|
+
|
|
206
|
+
self._record_response_channel(event)
|
|
207
|
+
self._record_history(event.channel, classification, now)
|
|
208
|
+
elif event.event_type == "error":
|
|
209
|
+
self._record_history(event.channel, "error", now)
|
|
210
|
+
|
|
211
|
+
def _handle_get_event(self, event: ChannelEvent, now: datetime) -> None:
|
|
212
|
+
if event.event_type == "connect":
|
|
213
|
+
self._get_connected = True
|
|
214
|
+
self._get_had_connection = True
|
|
215
|
+
self._get_connect_at = now
|
|
216
|
+
self._get_last_event = "connect"
|
|
217
|
+
self._get_last_event_at = now
|
|
218
|
+
self._get_last_error = None
|
|
219
|
+
self._get_last_status_code = None
|
|
220
|
+
elif event.event_type == "disconnect":
|
|
221
|
+
self._get_connected = False
|
|
222
|
+
self._get_disconnect_at = now
|
|
223
|
+
self._get_last_event = "disconnect"
|
|
224
|
+
self._get_last_event_at = now
|
|
225
|
+
elif event.event_type == "keepalive":
|
|
226
|
+
self._register_ping(now)
|
|
227
|
+
self._get_last_event = event.raw_event or "keepalive"
|
|
228
|
+
self._get_last_event_at = now
|
|
229
|
+
self._record_history("get", "ping", now)
|
|
230
|
+
elif event.event_type == "message" and event.message is not None:
|
|
231
|
+
self._get_message_count += 1
|
|
232
|
+
classification = self._tally_message_counts("get", event.message, now)
|
|
233
|
+
summary = "ping" if classification == "ping" else _summarise_message(event.message)
|
|
234
|
+
self._get_last_summary = summary
|
|
235
|
+
self._get_last_at = now
|
|
236
|
+
self._get_last_event = "ping" if classification == "ping" else "message"
|
|
237
|
+
self._get_last_event_at = now
|
|
238
|
+
|
|
239
|
+
self._record_response_channel(event)
|
|
240
|
+
self._record_history("get", classification, now)
|
|
241
|
+
elif event.event_type == "error":
|
|
242
|
+
self._get_last_status_code = event.status_code
|
|
243
|
+
self._get_last_error = event.detail
|
|
244
|
+
self._get_last_event = "error"
|
|
245
|
+
self._get_last_event_at = now
|
|
246
|
+
self._record_history("get", "error", now)
|
|
247
|
+
|
|
248
|
+
def _handle_resumption_event(self, event: ChannelEvent, now: datetime) -> None:
|
|
249
|
+
if event.event_type == "message" and event.message is not None:
|
|
250
|
+
self._resumption_count += 1
|
|
251
|
+
classification = self._tally_message_counts("resumption", event.message, now)
|
|
252
|
+
summary = "ping" if classification == "ping" else _summarise_message(event.message)
|
|
253
|
+
self._resumption_last_summary = summary
|
|
254
|
+
self._resumption_last_at = now
|
|
255
|
+
|
|
256
|
+
self._record_response_channel(event)
|
|
257
|
+
self._record_history("resumption", classification, now)
|
|
258
|
+
elif event.event_type == "error":
|
|
259
|
+
self._record_history("resumption", "error", now)
|
|
260
|
+
|
|
261
|
+
def _handle_stdio_event(self, event: ChannelEvent, now: datetime) -> None:
|
|
262
|
+
if event.event_type == "connect":
|
|
263
|
+
self._stdio_connected = True
|
|
264
|
+
self._stdio_had_connection = True
|
|
265
|
+
self._stdio_connect_at = now
|
|
266
|
+
self._stdio_last_event = "connect"
|
|
267
|
+
self._stdio_last_event_at = now
|
|
268
|
+
self._stdio_last_error = None
|
|
269
|
+
elif event.event_type == "disconnect":
|
|
270
|
+
self._stdio_connected = False
|
|
271
|
+
self._stdio_disconnect_at = now
|
|
272
|
+
self._stdio_last_event = "disconnect"
|
|
273
|
+
self._stdio_last_event_at = now
|
|
274
|
+
elif event.event_type == "message":
|
|
275
|
+
self._stdio_count += 1
|
|
276
|
+
|
|
277
|
+
# Handle synthetic events (from ServerStats) vs real message events
|
|
278
|
+
if event.message is not None:
|
|
279
|
+
# Real message event with JSON-RPC content
|
|
280
|
+
classification = self._tally_message_counts("stdio", event.message, now)
|
|
281
|
+
summary = "ping" if classification == "ping" else _summarise_message(event.message)
|
|
282
|
+
self._record_response_channel(event)
|
|
283
|
+
else:
|
|
284
|
+
# Synthetic event from MCP operation activity
|
|
285
|
+
classification = "request" # MCP operations are always requests from client perspective
|
|
286
|
+
self._stdio_request_count += 1
|
|
287
|
+
summary = event.detail or "request"
|
|
288
|
+
|
|
289
|
+
self._stdio_last_summary = summary
|
|
290
|
+
self._stdio_last_at = now
|
|
291
|
+
self._stdio_last_event = "message"
|
|
292
|
+
self._stdio_last_event_at = now
|
|
293
|
+
self._record_history("stdio", classification, now)
|
|
294
|
+
elif event.event_type == "error":
|
|
295
|
+
self._stdio_last_error = event.detail
|
|
296
|
+
self._stdio_last_event = "error"
|
|
297
|
+
self._stdio_last_event_at = now
|
|
298
|
+
self._record_history("stdio", "error", now)
|
|
299
|
+
|
|
300
|
+
def _record_response_channel(self, event: ChannelEvent) -> None:
|
|
301
|
+
if event.message is None:
|
|
302
|
+
return
|
|
303
|
+
root = event.message.root
|
|
304
|
+
request_id: RequestId | None = None
|
|
305
|
+
if isinstance(root, (JSONRPCResponse, JSONRPCError, JSONRPCRequest)):
|
|
306
|
+
request_id = getattr(root, "id", None)
|
|
307
|
+
if request_id is None:
|
|
308
|
+
return
|
|
309
|
+
self._response_channel_by_id[request_id] = event.channel
|
|
310
|
+
|
|
311
|
+
def consume_response_channel(self, request_id: RequestId | None) -> ChannelName | None:
|
|
312
|
+
if request_id is None:
|
|
313
|
+
return None
|
|
314
|
+
with self._lock:
|
|
315
|
+
return self._response_channel_by_id.pop(request_id, None)
|
|
316
|
+
|
|
317
|
+
def _tally_message_counts(
|
|
318
|
+
self,
|
|
319
|
+
channel_key: str,
|
|
320
|
+
message: JSONRPCMessage,
|
|
321
|
+
timestamp: datetime,
|
|
322
|
+
*,
|
|
323
|
+
sub_mode: str | None = None,
|
|
324
|
+
) -> str:
|
|
325
|
+
classification = self._classify_message(message)
|
|
326
|
+
|
|
327
|
+
if channel_key == "post":
|
|
328
|
+
if classification == "request":
|
|
329
|
+
self._post_request_count += 1
|
|
330
|
+
elif classification == "notification":
|
|
331
|
+
self._post_notification_count += 1
|
|
332
|
+
elif classification == "response":
|
|
333
|
+
self._post_response_count += 1
|
|
334
|
+
|
|
335
|
+
if sub_mode:
|
|
336
|
+
stats = self._post_mode_stats[sub_mode]
|
|
337
|
+
if classification in {"request", "notification", "response"}:
|
|
338
|
+
setattr(stats, classification, getattr(stats, classification) + 1)
|
|
339
|
+
elif channel_key == "get":
|
|
340
|
+
if classification == "ping":
|
|
341
|
+
self._register_ping(timestamp)
|
|
342
|
+
elif classification == "request":
|
|
343
|
+
self._get_request_count += 1
|
|
344
|
+
elif classification == "notification":
|
|
345
|
+
self._get_notification_count += 1
|
|
346
|
+
elif classification == "response":
|
|
347
|
+
self._get_response_count += 1
|
|
348
|
+
elif channel_key == "resumption":
|
|
349
|
+
if classification == "request":
|
|
350
|
+
self._resumption_request_count += 1
|
|
351
|
+
elif classification == "notification":
|
|
352
|
+
self._resumption_notification_count += 1
|
|
353
|
+
elif classification == "response":
|
|
354
|
+
self._resumption_response_count += 1
|
|
355
|
+
elif channel_key == "stdio":
|
|
356
|
+
if classification == "request":
|
|
357
|
+
self._stdio_request_count += 1
|
|
358
|
+
elif classification == "notification":
|
|
359
|
+
self._stdio_notification_count += 1
|
|
360
|
+
elif classification == "response":
|
|
361
|
+
self._stdio_response_count += 1
|
|
362
|
+
|
|
363
|
+
return classification
|
|
364
|
+
|
|
365
|
+
def _register_ping(self, timestamp: datetime) -> None:
|
|
366
|
+
self._get_ping_count += 1
|
|
367
|
+
self._get_last_ping_at = timestamp
|
|
368
|
+
|
|
369
|
+
def _classify_message(self, message: JSONRPCMessage | None) -> str:
|
|
370
|
+
if message is None:
|
|
371
|
+
return "none"
|
|
372
|
+
root = message.root
|
|
373
|
+
method = getattr(root, "method", "")
|
|
374
|
+
method_lower = method.lower() if isinstance(method, str) else ""
|
|
375
|
+
|
|
376
|
+
if isinstance(root, JSONRPCRequest):
|
|
377
|
+
if self._is_ping_method(method_lower):
|
|
378
|
+
return "ping"
|
|
379
|
+
return "request"
|
|
380
|
+
if isinstance(root, JSONRPCNotification):
|
|
381
|
+
if self._is_ping_method(method_lower):
|
|
382
|
+
return "ping"
|
|
383
|
+
return "notification"
|
|
384
|
+
if isinstance(root, (JSONRPCResponse, JSONRPCError)):
|
|
385
|
+
return "response"
|
|
386
|
+
return "none"
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def _is_ping_method(method: str) -> bool:
|
|
390
|
+
if not method:
|
|
391
|
+
return False
|
|
392
|
+
return (
|
|
393
|
+
method == "ping"
|
|
394
|
+
or method.endswith("/ping")
|
|
395
|
+
or method.endswith(".ping")
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def _record_history(self, channel: str, state: str, timestamp: datetime) -> None:
|
|
399
|
+
if state in {"none", ""}:
|
|
400
|
+
return
|
|
401
|
+
history = self._history.get(channel)
|
|
402
|
+
if history is None:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
bucket = int(timestamp.timestamp() // self._history_bucket_seconds)
|
|
406
|
+
if history and history[-1][0] == bucket:
|
|
407
|
+
existing = history[-1][1]
|
|
408
|
+
if self._history_priority.get(state, 0) >= self._history_priority.get(existing, 0):
|
|
409
|
+
history[-1] = (bucket, state)
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
while history and bucket - history[0][0] >= self._history_bucket_count:
|
|
413
|
+
history.popleft()
|
|
414
|
+
|
|
415
|
+
history.append((bucket, state))
|
|
416
|
+
|
|
417
|
+
def _build_activity_buckets(self, key: str, now: datetime) -> list[str]:
|
|
418
|
+
history = self._history.get(key)
|
|
419
|
+
if not history:
|
|
420
|
+
return ["none"] * self._history_bucket_count
|
|
421
|
+
|
|
422
|
+
history_map = {bucket: state for bucket, state in history}
|
|
423
|
+
current_bucket = int(now.timestamp() // self._history_bucket_seconds)
|
|
424
|
+
buckets: list[str] = []
|
|
425
|
+
for offset in range(self._history_bucket_count - 1, -1, -1):
|
|
426
|
+
bucket_index = current_bucket - offset
|
|
427
|
+
buckets.append(history_map.get(bucket_index, "none"))
|
|
428
|
+
return buckets
|
|
429
|
+
|
|
430
|
+
def _merge_activity_buckets(self, keys: list[str], now: datetime) -> list[str] | None:
|
|
431
|
+
sequences = [self._build_activity_buckets(key, now) for key in keys if key in self._history]
|
|
432
|
+
if not sequences:
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
merged: list[str] = []
|
|
436
|
+
for idx in range(self._history_bucket_count):
|
|
437
|
+
best_state = "none"
|
|
438
|
+
best_priority = 0
|
|
439
|
+
for seq in sequences:
|
|
440
|
+
state = seq[idx]
|
|
441
|
+
priority = self._history_priority.get(state, 0)
|
|
442
|
+
if priority > best_priority:
|
|
443
|
+
best_state = state
|
|
444
|
+
best_priority = priority
|
|
445
|
+
merged.append(best_state)
|
|
446
|
+
|
|
447
|
+
if all(state == "none" for state in merged):
|
|
448
|
+
return None
|
|
449
|
+
return merged
|
|
450
|
+
|
|
451
|
+
def _build_post_mode_snapshot(self, mode: str, now: datetime) -> ChannelSnapshot | None:
|
|
452
|
+
stats = self._post_mode_stats[mode]
|
|
453
|
+
if stats.messages == 0:
|
|
454
|
+
return None
|
|
455
|
+
return ChannelSnapshot(
|
|
456
|
+
message_count=stats.messages,
|
|
457
|
+
mode=mode,
|
|
458
|
+
request_count=stats.request,
|
|
459
|
+
response_count=stats.response,
|
|
460
|
+
notification_count=stats.notification,
|
|
461
|
+
last_message_summary=stats.last_summary,
|
|
462
|
+
last_message_at=stats.last_at,
|
|
463
|
+
activity_buckets=self._build_activity_buckets(f"post-{mode}", now),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
def snapshot(self) -> TransportSnapshot:
|
|
467
|
+
with self._lock:
|
|
468
|
+
if (
|
|
469
|
+
not self._post_count
|
|
470
|
+
and not self._get_message_count
|
|
471
|
+
and not self._get_ping_count
|
|
472
|
+
and not self._resumption_count
|
|
473
|
+
and not self._stdio_count
|
|
474
|
+
and not self._get_connected
|
|
475
|
+
and not self._stdio_connected
|
|
476
|
+
):
|
|
477
|
+
return TransportSnapshot()
|
|
478
|
+
|
|
479
|
+
now = datetime.now(timezone.utc)
|
|
480
|
+
|
|
481
|
+
post_mode_counts = {
|
|
482
|
+
mode: stats.messages
|
|
483
|
+
for mode, stats in self._post_mode_stats.items()
|
|
484
|
+
if stats.messages
|
|
485
|
+
}
|
|
486
|
+
post_snapshot = None
|
|
487
|
+
if self._post_count:
|
|
488
|
+
if len(self._post_modes) == 0:
|
|
489
|
+
mode = None
|
|
490
|
+
elif len(self._post_modes) == 1:
|
|
491
|
+
mode = next(iter(self._post_modes))
|
|
492
|
+
else:
|
|
493
|
+
mode = "mixed"
|
|
494
|
+
post_snapshot = ChannelSnapshot(
|
|
495
|
+
message_count=self._post_count,
|
|
496
|
+
mode=mode,
|
|
497
|
+
mode_counts=post_mode_counts or None,
|
|
498
|
+
last_message_summary=self._post_last_summary,
|
|
499
|
+
last_message_at=self._post_last_at,
|
|
500
|
+
request_count=self._post_request_count,
|
|
501
|
+
response_count=self._post_response_count,
|
|
502
|
+
notification_count=self._post_notification_count,
|
|
503
|
+
activity_buckets=self._merge_activity_buckets(["post-json", "post-sse"], now),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
post_json_snapshot = self._build_post_mode_snapshot("json", now)
|
|
507
|
+
post_sse_snapshot = self._build_post_mode_snapshot("sse", now)
|
|
508
|
+
|
|
509
|
+
get_snapshot = None
|
|
510
|
+
if (
|
|
511
|
+
self._get_message_count
|
|
512
|
+
or self._get_ping_count
|
|
513
|
+
or self._get_connected
|
|
514
|
+
or self._get_disconnect_at
|
|
515
|
+
or self._get_last_error
|
|
516
|
+
):
|
|
517
|
+
if self._get_connected:
|
|
518
|
+
state = "open"
|
|
519
|
+
elif self._get_last_error is not None:
|
|
520
|
+
state = "disabled" if self._get_last_status_code == 405 else "error"
|
|
521
|
+
elif self._get_had_connection:
|
|
522
|
+
state = "off"
|
|
523
|
+
else:
|
|
524
|
+
state = "idle"
|
|
525
|
+
|
|
526
|
+
get_snapshot = ChannelSnapshot(
|
|
527
|
+
connected=self._get_connected,
|
|
528
|
+
state=state,
|
|
529
|
+
connect_at=self._get_connect_at,
|
|
530
|
+
disconnect_at=self._get_disconnect_at,
|
|
531
|
+
message_count=self._get_message_count,
|
|
532
|
+
last_message_summary=self._get_last_summary,
|
|
533
|
+
last_message_at=self._get_last_at,
|
|
534
|
+
ping_count=self._get_ping_count,
|
|
535
|
+
ping_last_at=self._get_last_ping_at,
|
|
536
|
+
last_error=self._get_last_error,
|
|
537
|
+
last_event=self._get_last_event,
|
|
538
|
+
last_event_at=self._get_last_event_at,
|
|
539
|
+
last_status_code=self._get_last_status_code,
|
|
540
|
+
request_count=self._get_request_count,
|
|
541
|
+
response_count=self._get_response_count,
|
|
542
|
+
notification_count=self._get_notification_count,
|
|
543
|
+
activity_buckets=self._build_activity_buckets("get", now),
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
resumption_snapshot = None
|
|
547
|
+
if self._resumption_count:
|
|
548
|
+
resumption_snapshot = ChannelSnapshot(
|
|
549
|
+
message_count=self._resumption_count,
|
|
550
|
+
last_message_summary=self._resumption_last_summary,
|
|
551
|
+
last_message_at=self._resumption_last_at,
|
|
552
|
+
request_count=self._resumption_request_count,
|
|
553
|
+
response_count=self._resumption_response_count,
|
|
554
|
+
notification_count=self._resumption_notification_count,
|
|
555
|
+
activity_buckets=self._build_activity_buckets("resumption", now),
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
stdio_snapshot = None
|
|
559
|
+
if (
|
|
560
|
+
self._stdio_count
|
|
561
|
+
or self._stdio_connected
|
|
562
|
+
or self._stdio_disconnect_at
|
|
563
|
+
or self._stdio_last_error
|
|
564
|
+
):
|
|
565
|
+
if self._stdio_connected:
|
|
566
|
+
state = "open"
|
|
567
|
+
elif self._stdio_last_error is not None:
|
|
568
|
+
state = "error"
|
|
569
|
+
elif self._stdio_had_connection:
|
|
570
|
+
state = "off"
|
|
571
|
+
else:
|
|
572
|
+
state = "idle"
|
|
573
|
+
|
|
574
|
+
stdio_snapshot = ChannelSnapshot(
|
|
575
|
+
connected=self._stdio_connected,
|
|
576
|
+
state=state,
|
|
577
|
+
connect_at=self._stdio_connect_at,
|
|
578
|
+
disconnect_at=self._stdio_disconnect_at,
|
|
579
|
+
message_count=self._stdio_count,
|
|
580
|
+
last_message_summary=self._stdio_last_summary,
|
|
581
|
+
last_message_at=self._stdio_last_at,
|
|
582
|
+
last_error=self._stdio_last_error,
|
|
583
|
+
last_event=self._stdio_last_event,
|
|
584
|
+
last_event_at=self._stdio_last_event_at,
|
|
585
|
+
request_count=self._stdio_request_count,
|
|
586
|
+
response_count=self._stdio_response_count,
|
|
587
|
+
notification_count=self._stdio_notification_count,
|
|
588
|
+
activity_buckets=self._build_activity_buckets("stdio", now),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
return TransportSnapshot(
|
|
592
|
+
post=post_snapshot,
|
|
593
|
+
post_json=post_json_snapshot,
|
|
594
|
+
post_sse=post_sse_snapshot,
|
|
595
|
+
get=get_snapshot,
|
|
596
|
+
resumption=resumption_snapshot,
|
|
597
|
+
stdio=stdio_snapshot,
|
|
598
|
+
)
|
|
@@ -18,18 +18,22 @@ Common analysis packages such as Pandas, Seaborn and Matplotlib are already inst
|
|
|
18
18
|
You can add further packages if needed.
|
|
19
19
|
Data files are accessible from the /mnt/data/ directory (this is the current working directory).
|
|
20
20
|
Visualisations should be saved as .png files in the current working directory.
|
|
21
|
+
|
|
22
|
+
{{serverInstructions}}
|
|
23
|
+
|
|
21
24
|
""",
|
|
22
25
|
servers=["interpreter"],
|
|
23
26
|
)
|
|
24
|
-
@fast.agent(name="another_test", instruction="", servers=["filesystem"])
|
|
27
|
+
# @fast.agent(name="another_test", instruction="", servers=["filesystem"])
|
|
25
28
|
async def main() -> None:
|
|
26
29
|
# Use the app's context manager
|
|
27
30
|
async with fast.run() as agent:
|
|
28
|
-
await agent(
|
|
31
|
+
await agent.interactive()
|
|
32
|
+
await agent.data_analysis(
|
|
29
33
|
"There is a csv file in the current directory. "
|
|
30
34
|
"Analyse the file, produce a detailed description of the data, and any patterns it contains.",
|
|
31
35
|
)
|
|
32
|
-
await agent(
|
|
36
|
+
await agent.data_analysis(
|
|
33
37
|
"Consider the data, and how to usefully group it for presentation to a Human. Find insights, using the Python Interpreter as needed.\n"
|
|
34
38
|
"Use MatPlotLib to produce insightful visualisations. Save them as '.png' files in the current directory. Be sure to run the code and save the files.\n"
|
|
35
39
|
"Produce a summary with major insights to the data",
|
fast_agent/ui/console_display.py
CHANGED
|
@@ -542,7 +542,27 @@ class ConsoleDisplay:
|
|
|
542
542
|
f"{len(content)} Content Blocks" if len(content) > 1 else "1 Content Block"
|
|
543
543
|
)
|
|
544
544
|
|
|
545
|
-
# Build
|
|
545
|
+
# Build transport channel info for bottom bar
|
|
546
|
+
channel = getattr(result, "transport_channel", None)
|
|
547
|
+
bottom_metadata = None
|
|
548
|
+
if channel:
|
|
549
|
+
# Format channel info for bottom bar
|
|
550
|
+
if channel == "post-json":
|
|
551
|
+
transport_info = "HTTP (JSON-RPC)"
|
|
552
|
+
elif channel == "post-sse":
|
|
553
|
+
transport_info = "HTTP (SSE)"
|
|
554
|
+
elif channel == "get":
|
|
555
|
+
transport_info = "HTTP (SSE)"
|
|
556
|
+
elif channel == "resumption":
|
|
557
|
+
transport_info = "Resumption"
|
|
558
|
+
elif channel == "stdio":
|
|
559
|
+
transport_info = "STDIO"
|
|
560
|
+
else:
|
|
561
|
+
transport_info = channel.upper()
|
|
562
|
+
|
|
563
|
+
bottom_metadata = [transport_info]
|
|
564
|
+
|
|
565
|
+
# Build right info (without channel info)
|
|
546
566
|
right_info = f"[dim]tool result - {status}[/dim]"
|
|
547
567
|
|
|
548
568
|
# Display using unified method
|
|
@@ -551,6 +571,7 @@ class ConsoleDisplay:
|
|
|
551
571
|
message_type=MessageType.TOOL_RESULT,
|
|
552
572
|
name=name,
|
|
553
573
|
right_info=right_info,
|
|
574
|
+
bottom_metadata=bottom_metadata,
|
|
554
575
|
is_error=result.isError,
|
|
555
576
|
truncate_content=True,
|
|
556
577
|
)
|