fast-agent-mcp 0.1.8__py3-none-any.whl → 0.1.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.
- {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/METADATA +26 -4
- {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/RECORD +43 -22
- {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/entry_points.txt +1 -0
- mcp_agent/agents/agent.py +5 -11
- mcp_agent/core/agent_app.py +89 -13
- mcp_agent/core/fastagent.py +13 -3
- mcp_agent/core/mcp_content.py +222 -0
- mcp_agent/core/prompt.py +132 -0
- mcp_agent/core/proxies.py +41 -36
- mcp_agent/logging/transport.py +30 -3
- mcp_agent/mcp/mcp_aggregator.py +11 -10
- mcp_agent/mcp/mime_utils.py +69 -0
- mcp_agent/mcp/prompt_message_multipart.py +64 -0
- mcp_agent/mcp/prompt_serialization.py +447 -0
- mcp_agent/mcp/prompts/__init__.py +0 -0
- mcp_agent/mcp/prompts/__main__.py +10 -0
- mcp_agent/mcp/prompts/prompt_server.py +508 -0
- mcp_agent/mcp/prompts/prompt_template.py +469 -0
- mcp_agent/mcp/resource_utils.py +203 -0
- mcp_agent/resources/examples/internal/agent.py +1 -1
- mcp_agent/resources/examples/internal/fastagent.config.yaml +2 -2
- mcp_agent/resources/examples/internal/sizer.py +0 -5
- mcp_agent/resources/examples/prompting/__init__.py +3 -0
- mcp_agent/resources/examples/prompting/agent.py +23 -0
- mcp_agent/resources/examples/prompting/fastagent.config.yaml +44 -0
- mcp_agent/resources/examples/prompting/image_server.py +56 -0
- mcp_agent/workflows/llm/anthropic_utils.py +101 -0
- mcp_agent/workflows/llm/augmented_llm.py +139 -66
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +127 -251
- mcp_agent/workflows/llm/augmented_llm_openai.py +149 -305
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +43 -0
- mcp_agent/workflows/llm/augmented_llm_playback.py +109 -0
- mcp_agent/workflows/llm/model_factory.py +20 -3
- mcp_agent/workflows/llm/openai_utils.py +65 -0
- mcp_agent/workflows/llm/providers/__init__.py +8 -0
- mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +348 -0
- mcp_agent/workflows/llm/providers/multipart_converter_openai.py +426 -0
- mcp_agent/workflows/llm/providers/openai_multipart.py +197 -0
- mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +258 -0
- mcp_agent/workflows/llm/providers/sampling_converter_openai.py +229 -0
- mcp_agent/workflows/llm/sampling_format_converter.py +39 -0
- mcp_agent/core/server_validation.py +0 -44
- mcp_agent/core/simulator_registry.py +0 -22
- mcp_agent/workflows/llm/enhanced_passthrough.py +0 -70
- {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,447 @@
|
|
1
|
+
"""
|
2
|
+
Utilities for converting between different prompt message formats.
|
3
|
+
|
4
|
+
This module provides utilities for converting between different serialization formats
|
5
|
+
and PromptMessageMultipart objects. It includes functionality for:
|
6
|
+
|
7
|
+
1. JSON Serialization:
|
8
|
+
- Converting PromptMessageMultipart objects to pure JSON format
|
9
|
+
- Parsing JSON into PromptMessageMultipart objects
|
10
|
+
- This is ideal for programmatic use and ensures all data fidelity
|
11
|
+
|
12
|
+
2. Delimited Text Format:
|
13
|
+
- Converting PromptMessageMultipart objects to delimited text (---USER, ---ASSISTANT)
|
14
|
+
- Converting resources to JSON after resource delimiter (---RESOURCE)
|
15
|
+
- Parsing delimited text back into PromptMessageMultipart objects
|
16
|
+
- This maintains human readability for text content while preserving structure for resources
|
17
|
+
"""
|
18
|
+
|
19
|
+
import json
|
20
|
+
from typing import List
|
21
|
+
|
22
|
+
from mcp.types import TextContent, EmbeddedResource, ImageContent, TextResourceContents
|
23
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
24
|
+
|
25
|
+
|
26
|
+
# -------------------------------------------------------------------------
|
27
|
+
# JSON Serialization Functions
|
28
|
+
# -------------------------------------------------------------------------
|
29
|
+
|
30
|
+
|
31
|
+
def multipart_messages_to_json(messages: List[PromptMessageMultipart]) -> str:
|
32
|
+
"""
|
33
|
+
Convert PromptMessageMultipart objects to a pure JSON string.
|
34
|
+
|
35
|
+
This approach preserves all data and structure exactly as is, but is less
|
36
|
+
human-readable for text content.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
messages: List of PromptMessageMultipart objects
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
JSON string representation of the messages
|
43
|
+
"""
|
44
|
+
# Convert to dictionaries using model_dump with proper JSON mode
|
45
|
+
# The mode="json" parameter ensures proper handling of AnyUrl objects
|
46
|
+
message_dicts = [
|
47
|
+
message.model_dump(by_alias=True, mode="json", exclude_none=True)
|
48
|
+
for message in messages
|
49
|
+
]
|
50
|
+
|
51
|
+
# Convert to JSON string
|
52
|
+
return json.dumps(message_dicts, indent=2)
|
53
|
+
|
54
|
+
|
55
|
+
def json_to_multipart_messages(json_str: str) -> List[PromptMessageMultipart]:
|
56
|
+
"""
|
57
|
+
Parse a JSON string into PromptMessageMultipart objects.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
json_str: JSON string representation of messages
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
List of PromptMessageMultipart objects
|
64
|
+
"""
|
65
|
+
# Parse JSON to list of dictionaries
|
66
|
+
message_dicts = json.loads(json_str)
|
67
|
+
|
68
|
+
# Convert dictionaries to PromptMessageMultipart objects
|
69
|
+
messages = []
|
70
|
+
for message_dict in message_dicts:
|
71
|
+
# Parse message using Pydantic's model_validate method (Pydantic v2)
|
72
|
+
# For Pydantic v1, this would use parse_obj instead
|
73
|
+
message = PromptMessageMultipart.model_validate(message_dict)
|
74
|
+
messages.append(message)
|
75
|
+
|
76
|
+
return messages
|
77
|
+
|
78
|
+
|
79
|
+
def save_messages_to_json_file(
|
80
|
+
messages: List[PromptMessageMultipart], file_path: str
|
81
|
+
) -> None:
|
82
|
+
"""
|
83
|
+
Save PromptMessageMultipart objects to a JSON file.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
messages: List of PromptMessageMultipart objects
|
87
|
+
file_path: Path to save the JSON file
|
88
|
+
"""
|
89
|
+
json_str = multipart_messages_to_json(messages)
|
90
|
+
|
91
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
92
|
+
f.write(json_str)
|
93
|
+
|
94
|
+
|
95
|
+
def load_messages_from_json_file(file_path: str) -> List[PromptMessageMultipart]:
|
96
|
+
"""
|
97
|
+
Load PromptMessageMultipart objects from a JSON file.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
file_path: Path to the JSON file
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
List of PromptMessageMultipart objects
|
104
|
+
"""
|
105
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
106
|
+
json_str = f.read()
|
107
|
+
|
108
|
+
return json_to_multipart_messages(json_str)
|
109
|
+
|
110
|
+
|
111
|
+
# -------------------------------------------------------------------------
|
112
|
+
# Delimited Text Format Functions
|
113
|
+
# -------------------------------------------------------------------------
|
114
|
+
|
115
|
+
|
116
|
+
def multipart_messages_to_delimited_format(
|
117
|
+
messages: List[PromptMessageMultipart],
|
118
|
+
user_delimiter: str = "---USER",
|
119
|
+
assistant_delimiter: str = "---ASSISTANT",
|
120
|
+
resource_delimiter: str = "---RESOURCE",
|
121
|
+
combine_text: bool = True, # Set to False to maintain backward compatibility
|
122
|
+
) -> List[str]:
|
123
|
+
"""
|
124
|
+
Convert PromptMessageMultipart objects to a hybrid delimited format:
|
125
|
+
- Plain text for user/assistant text content with delimiters
|
126
|
+
- JSON for resources after resource delimiter
|
127
|
+
|
128
|
+
This approach maintains human readability for text content while
|
129
|
+
preserving structure for resources.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
messages: List of PromptMessageMultipart objects
|
133
|
+
user_delimiter: Delimiter for user messages
|
134
|
+
assistant_delimiter: Delimiter for assistant messages
|
135
|
+
resource_delimiter: Delimiter for resources
|
136
|
+
combine_text: Whether to combine multiple text contents into one (default: True)
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
List of strings representing the delimited content
|
140
|
+
"""
|
141
|
+
delimited_content = []
|
142
|
+
|
143
|
+
for message in messages:
|
144
|
+
# Add role delimiter
|
145
|
+
if message.role == "user":
|
146
|
+
delimited_content.append(user_delimiter)
|
147
|
+
else:
|
148
|
+
delimited_content.append(assistant_delimiter)
|
149
|
+
|
150
|
+
# Process content parts based on combine_text preference
|
151
|
+
if combine_text:
|
152
|
+
# Collect text content parts
|
153
|
+
text_contents = []
|
154
|
+
|
155
|
+
# First, add all text content
|
156
|
+
for content in message.content:
|
157
|
+
if content.type == "text":
|
158
|
+
# Collect text content to combine
|
159
|
+
text_contents.append(content.text)
|
160
|
+
|
161
|
+
# Add combined text content if any exists
|
162
|
+
if text_contents:
|
163
|
+
delimited_content.append("\n\n".join(text_contents))
|
164
|
+
|
165
|
+
# Then add resources and images
|
166
|
+
for content in message.content:
|
167
|
+
if content.type != "text":
|
168
|
+
# Resource or image - add delimiter and JSON
|
169
|
+
delimited_content.append(resource_delimiter)
|
170
|
+
|
171
|
+
# Convert to dictionary using proper JSON mode
|
172
|
+
content_dict = content.model_dump(
|
173
|
+
by_alias=True, mode="json", exclude_none=True
|
174
|
+
)
|
175
|
+
|
176
|
+
# Add to delimited content as JSON
|
177
|
+
delimited_content.append(json.dumps(content_dict, indent=2))
|
178
|
+
else:
|
179
|
+
# Don't combine text contents - preserve each content part in sequence
|
180
|
+
for content in message.content:
|
181
|
+
if content.type == "text":
|
182
|
+
# Add each text content separately
|
183
|
+
delimited_content.append(content.text)
|
184
|
+
else:
|
185
|
+
# Resource or image - add delimiter and JSON
|
186
|
+
delimited_content.append(resource_delimiter)
|
187
|
+
|
188
|
+
# Convert to dictionary using proper JSON mode
|
189
|
+
content_dict = content.model_dump(
|
190
|
+
by_alias=True, mode="json", exclude_none=True
|
191
|
+
)
|
192
|
+
|
193
|
+
# Add to delimited content as JSON
|
194
|
+
delimited_content.append(json.dumps(content_dict, indent=2))
|
195
|
+
|
196
|
+
return delimited_content
|
197
|
+
|
198
|
+
|
199
|
+
def delimited_format_to_multipart_messages(
|
200
|
+
content: str,
|
201
|
+
user_delimiter: str = "---USER",
|
202
|
+
assistant_delimiter: str = "---ASSISTANT",
|
203
|
+
resource_delimiter: str = "---RESOURCE",
|
204
|
+
) -> List[PromptMessageMultipart]:
|
205
|
+
"""
|
206
|
+
Parse hybrid delimited format into PromptMessageMultipart objects:
|
207
|
+
- Plain text for user/assistant text content with delimiters
|
208
|
+
- JSON for resources after resource delimiter
|
209
|
+
|
210
|
+
Args:
|
211
|
+
content: String containing the delimited content
|
212
|
+
user_delimiter: Delimiter for user messages
|
213
|
+
assistant_delimiter: Delimiter for assistant messages
|
214
|
+
resource_delimiter: Delimiter for resources
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
List of PromptMessageMultipart objects
|
218
|
+
"""
|
219
|
+
lines = content.split("\n")
|
220
|
+
messages = []
|
221
|
+
|
222
|
+
current_role = None
|
223
|
+
text_contents = [] # List of TextContent
|
224
|
+
resource_contents = [] # List of EmbeddedResource or ImageContent
|
225
|
+
collecting_json = False
|
226
|
+
json_lines = []
|
227
|
+
collecting_text = False
|
228
|
+
text_lines = []
|
229
|
+
|
230
|
+
# Check if this is a legacy format (pre-JSON serialization)
|
231
|
+
legacy_format = resource_delimiter in content and '"type":' not in content
|
232
|
+
|
233
|
+
# Add a condition to ensure we process the first user message properly
|
234
|
+
# This is the key fix: We need to process the first line correctly
|
235
|
+
if lines and lines[0].strip() == user_delimiter:
|
236
|
+
current_role = "user"
|
237
|
+
collecting_text = True
|
238
|
+
|
239
|
+
# Process each line
|
240
|
+
for line in (
|
241
|
+
lines[1:] if lines else []
|
242
|
+
): # Skip the first line if already processed above
|
243
|
+
line_stripped = line.strip()
|
244
|
+
|
245
|
+
# Handle role delimiters
|
246
|
+
if line_stripped == user_delimiter or line_stripped == assistant_delimiter:
|
247
|
+
# Save previous message if it exists
|
248
|
+
if current_role is not None and (
|
249
|
+
text_contents or resource_contents or text_lines
|
250
|
+
):
|
251
|
+
# If we were collecting text, add it to the text contents
|
252
|
+
if collecting_text and text_lines:
|
253
|
+
text_contents.append(
|
254
|
+
TextContent(type="text", text="\n".join(text_lines))
|
255
|
+
)
|
256
|
+
text_lines = []
|
257
|
+
|
258
|
+
# Create content list with text parts first, then resource parts
|
259
|
+
combined_content = []
|
260
|
+
|
261
|
+
# Filter out any empty text content items
|
262
|
+
filtered_text_contents = [
|
263
|
+
tc for tc in text_contents if tc.text.strip() != ""
|
264
|
+
]
|
265
|
+
|
266
|
+
combined_content.extend(filtered_text_contents)
|
267
|
+
combined_content.extend(resource_contents)
|
268
|
+
|
269
|
+
messages.append(
|
270
|
+
PromptMessageMultipart(
|
271
|
+
role=current_role,
|
272
|
+
content=combined_content,
|
273
|
+
)
|
274
|
+
)
|
275
|
+
|
276
|
+
# Start a new message
|
277
|
+
current_role = "user" if line_stripped == user_delimiter else "assistant"
|
278
|
+
text_contents = []
|
279
|
+
resource_contents = []
|
280
|
+
collecting_json = False
|
281
|
+
json_lines = []
|
282
|
+
collecting_text = False
|
283
|
+
text_lines = []
|
284
|
+
|
285
|
+
# Handle resource delimiter
|
286
|
+
elif line_stripped == resource_delimiter:
|
287
|
+
# If we were collecting text, add it to text contents
|
288
|
+
if collecting_text and text_lines:
|
289
|
+
text_contents.append(
|
290
|
+
TextContent(type="text", text="\n".join(text_lines))
|
291
|
+
)
|
292
|
+
text_lines = []
|
293
|
+
|
294
|
+
# Switch to collecting JSON or legacy format
|
295
|
+
collecting_text = False
|
296
|
+
collecting_json = True
|
297
|
+
json_lines = []
|
298
|
+
|
299
|
+
# Process content based on context
|
300
|
+
elif current_role is not None:
|
301
|
+
if collecting_json:
|
302
|
+
# Collect JSON data
|
303
|
+
json_lines.append(line)
|
304
|
+
|
305
|
+
# For legacy format or files where resources are just plain text
|
306
|
+
if (
|
307
|
+
legacy_format
|
308
|
+
and line_stripped
|
309
|
+
and not line_stripped.startswith("{")
|
310
|
+
):
|
311
|
+
# This is probably a legacy resource reference like a filename
|
312
|
+
resource_uri = line_stripped
|
313
|
+
if not resource_uri.startswith("resource://"):
|
314
|
+
resource_uri = f"resource://fast-agent/{resource_uri}"
|
315
|
+
|
316
|
+
# Create a simple resource with just the URI
|
317
|
+
resource = EmbeddedResource(
|
318
|
+
type="resource",
|
319
|
+
resource=TextResourceContents(
|
320
|
+
uri=resource_uri,
|
321
|
+
mimeType="text/plain",
|
322
|
+
),
|
323
|
+
)
|
324
|
+
resource_contents.append(resource)
|
325
|
+
collecting_json = False
|
326
|
+
json_lines = []
|
327
|
+
continue
|
328
|
+
|
329
|
+
# Try to parse the JSON to see if we have a complete object
|
330
|
+
try:
|
331
|
+
json_text = "\n".join(json_lines)
|
332
|
+
json_data = json.loads(json_text)
|
333
|
+
|
334
|
+
# Successfully parsed JSON
|
335
|
+
content_type = json_data.get("type")
|
336
|
+
|
337
|
+
if content_type == "resource":
|
338
|
+
# Create resource object using model_validate
|
339
|
+
resource = EmbeddedResource.model_validate(json_data)
|
340
|
+
resource_contents.append(resource) # Add to resource contents
|
341
|
+
elif content_type == "image":
|
342
|
+
# Create image object using model_validate
|
343
|
+
image = ImageContent.model_validate(json_data)
|
344
|
+
resource_contents.append(image) # Add to resource contents
|
345
|
+
|
346
|
+
# Reset JSON collection
|
347
|
+
collecting_json = False
|
348
|
+
json_lines = []
|
349
|
+
|
350
|
+
except json.JSONDecodeError:
|
351
|
+
# Not a complete JSON object yet, keep collecting
|
352
|
+
pass
|
353
|
+
else:
|
354
|
+
# Regular text content
|
355
|
+
if not collecting_text:
|
356
|
+
collecting_text = True
|
357
|
+
text_lines = []
|
358
|
+
|
359
|
+
text_lines.append(line)
|
360
|
+
|
361
|
+
# Handle any remaining content
|
362
|
+
if current_role is not None:
|
363
|
+
# Add any remaining text
|
364
|
+
if collecting_text and text_lines:
|
365
|
+
text_contents.append(TextContent(type="text", text="\n".join(text_lines)))
|
366
|
+
|
367
|
+
# Add the final message if it has content
|
368
|
+
if text_contents or resource_contents:
|
369
|
+
# Create content list with text parts first, then resource parts
|
370
|
+
combined_content = []
|
371
|
+
|
372
|
+
# Filter out any empty text content items
|
373
|
+
filtered_text_contents = [
|
374
|
+
tc for tc in text_contents if tc.text.strip() != ""
|
375
|
+
]
|
376
|
+
|
377
|
+
combined_content.extend(filtered_text_contents)
|
378
|
+
combined_content.extend(resource_contents)
|
379
|
+
|
380
|
+
messages.append(
|
381
|
+
PromptMessageMultipart(
|
382
|
+
role=current_role,
|
383
|
+
content=combined_content,
|
384
|
+
)
|
385
|
+
)
|
386
|
+
|
387
|
+
return messages
|
388
|
+
|
389
|
+
|
390
|
+
def save_messages_to_delimited_file(
|
391
|
+
messages: List[PromptMessageMultipart],
|
392
|
+
file_path: str,
|
393
|
+
user_delimiter: str = "---USER",
|
394
|
+
assistant_delimiter: str = "---ASSISTANT",
|
395
|
+
resource_delimiter: str = "---RESOURCE",
|
396
|
+
combine_text: bool = True,
|
397
|
+
) -> None:
|
398
|
+
"""
|
399
|
+
Save PromptMessageMultipart objects to a file in hybrid delimited format.
|
400
|
+
|
401
|
+
Args:
|
402
|
+
messages: List of PromptMessageMultipart objects
|
403
|
+
file_path: Path to save the file
|
404
|
+
user_delimiter: Delimiter for user messages
|
405
|
+
assistant_delimiter: Delimiter for assistant messages
|
406
|
+
resource_delimiter: Delimiter for resources
|
407
|
+
combine_text: Whether to combine multiple text contents into one (default: True)
|
408
|
+
"""
|
409
|
+
delimited_content = multipart_messages_to_delimited_format(
|
410
|
+
messages,
|
411
|
+
user_delimiter,
|
412
|
+
assistant_delimiter,
|
413
|
+
resource_delimiter,
|
414
|
+
combine_text=combine_text,
|
415
|
+
)
|
416
|
+
|
417
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
418
|
+
f.write("\n".join(delimited_content))
|
419
|
+
|
420
|
+
|
421
|
+
def load_messages_from_delimited_file(
|
422
|
+
file_path: str,
|
423
|
+
user_delimiter: str = "---USER",
|
424
|
+
assistant_delimiter: str = "---ASSISTANT",
|
425
|
+
resource_delimiter: str = "---RESOURCE",
|
426
|
+
) -> List[PromptMessageMultipart]:
|
427
|
+
"""
|
428
|
+
Load PromptMessageMultipart objects from a file in hybrid delimited format.
|
429
|
+
|
430
|
+
Args:
|
431
|
+
file_path: Path to the file
|
432
|
+
user_delimiter: Delimiter for user messages
|
433
|
+
assistant_delimiter: Delimiter for assistant messages
|
434
|
+
resource_delimiter: Delimiter for resources
|
435
|
+
|
436
|
+
Returns:
|
437
|
+
List of PromptMessageMultipart objects
|
438
|
+
"""
|
439
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
440
|
+
content = f.read()
|
441
|
+
|
442
|
+
return delimited_format_to_multipart_messages(
|
443
|
+
content,
|
444
|
+
user_delimiter,
|
445
|
+
assistant_delimiter,
|
446
|
+
resource_delimiter,
|
447
|
+
)
|
File without changes
|