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.
Files changed (46) hide show
  1. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/METADATA +26 -4
  2. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/RECORD +43 -22
  3. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/entry_points.txt +1 -0
  4. mcp_agent/agents/agent.py +5 -11
  5. mcp_agent/core/agent_app.py +89 -13
  6. mcp_agent/core/fastagent.py +13 -3
  7. mcp_agent/core/mcp_content.py +222 -0
  8. mcp_agent/core/prompt.py +132 -0
  9. mcp_agent/core/proxies.py +41 -36
  10. mcp_agent/logging/transport.py +30 -3
  11. mcp_agent/mcp/mcp_aggregator.py +11 -10
  12. mcp_agent/mcp/mime_utils.py +69 -0
  13. mcp_agent/mcp/prompt_message_multipart.py +64 -0
  14. mcp_agent/mcp/prompt_serialization.py +447 -0
  15. mcp_agent/mcp/prompts/__init__.py +0 -0
  16. mcp_agent/mcp/prompts/__main__.py +10 -0
  17. mcp_agent/mcp/prompts/prompt_server.py +508 -0
  18. mcp_agent/mcp/prompts/prompt_template.py +469 -0
  19. mcp_agent/mcp/resource_utils.py +203 -0
  20. mcp_agent/resources/examples/internal/agent.py +1 -1
  21. mcp_agent/resources/examples/internal/fastagent.config.yaml +2 -2
  22. mcp_agent/resources/examples/internal/sizer.py +0 -5
  23. mcp_agent/resources/examples/prompting/__init__.py +3 -0
  24. mcp_agent/resources/examples/prompting/agent.py +23 -0
  25. mcp_agent/resources/examples/prompting/fastagent.config.yaml +44 -0
  26. mcp_agent/resources/examples/prompting/image_server.py +56 -0
  27. mcp_agent/workflows/llm/anthropic_utils.py +101 -0
  28. mcp_agent/workflows/llm/augmented_llm.py +139 -66
  29. mcp_agent/workflows/llm/augmented_llm_anthropic.py +127 -251
  30. mcp_agent/workflows/llm/augmented_llm_openai.py +149 -305
  31. mcp_agent/workflows/llm/augmented_llm_passthrough.py +43 -0
  32. mcp_agent/workflows/llm/augmented_llm_playback.py +109 -0
  33. mcp_agent/workflows/llm/model_factory.py +20 -3
  34. mcp_agent/workflows/llm/openai_utils.py +65 -0
  35. mcp_agent/workflows/llm/providers/__init__.py +8 -0
  36. mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +348 -0
  37. mcp_agent/workflows/llm/providers/multipart_converter_openai.py +426 -0
  38. mcp_agent/workflows/llm/providers/openai_multipart.py +197 -0
  39. mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +258 -0
  40. mcp_agent/workflows/llm/providers/sampling_converter_openai.py +229 -0
  41. mcp_agent/workflows/llm/sampling_format_converter.py +39 -0
  42. mcp_agent/core/server_validation.py +0 -44
  43. mcp_agent/core/simulator_registry.py +0 -22
  44. mcp_agent/workflows/llm/enhanced_passthrough.py +0 -70
  45. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/WHEEL +0 -0
  46. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,469 @@
1
+ """
2
+ Prompt Template Module
3
+
4
+ Handles prompt templating, variable extraction, and substitution for the prompt server.
5
+ Provides clean, testable classes for managing template substitution.
6
+ """
7
+
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Dict, List, Set, Any, Optional, Literal
11
+
12
+ from pydantic import BaseModel, field_validator
13
+
14
+ from mcp.types import TextContent, EmbeddedResource, TextResourceContents
15
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
16
+ from mcp_agent.mcp.prompt_serialization import (
17
+ multipart_messages_to_delimited_format,
18
+ )
19
+
20
+
21
+ class PromptMetadata(BaseModel):
22
+ """Metadata about a prompt file"""
23
+
24
+ name: str
25
+ description: str
26
+ template_variables: Set[str] = set()
27
+ resource_paths: List[str] = []
28
+ file_path: Path
29
+
30
+
31
+ # Define valid message roles for better type safety
32
+ MessageRole = Literal["user", "assistant"]
33
+
34
+
35
+ class PromptContent(BaseModel):
36
+ """Content of a prompt, which may include template variables"""
37
+
38
+ text: str
39
+ role: str = "user"
40
+ resources: List[str] = []
41
+
42
+ @field_validator("role")
43
+ @classmethod
44
+ def validate_role(cls, role: str) -> str:
45
+ """Validate that the role is a known value"""
46
+ if role not in ("user", "assistant"):
47
+ raise ValueError(f"Invalid role: {role}. Must be one of: user, assistant")
48
+ return role
49
+
50
+ def apply_substitutions(self, context: Dict[str, Any]) -> "PromptContent":
51
+ """Apply variable substitutions to the text and resources"""
52
+
53
+ # Define placeholder pattern once to avoid repetition
54
+ def make_placeholder(key: str) -> str:
55
+ return f"{{{{{key}}}}}"
56
+
57
+ # Apply substitutions to text
58
+ result = self.text
59
+ for key, value in context.items():
60
+ result = result.replace(make_placeholder(key), str(value))
61
+
62
+ # Apply substitutions to resource paths
63
+ substituted_resources = []
64
+ for resource in self.resources:
65
+ substituted = resource
66
+ for key, value in context.items():
67
+ substituted = substituted.replace(make_placeholder(key), str(value))
68
+ substituted_resources.append(substituted)
69
+
70
+ return PromptContent(
71
+ text=result, role=self.role, resources=substituted_resources
72
+ )
73
+
74
+
75
+ class PromptTemplate:
76
+ """
77
+ A template for a prompt that can have variables substituted.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ template_text: str,
83
+ delimiter_map: Optional[Dict[str, str]] = None,
84
+ template_file_path: Optional[Path] = None,
85
+ ):
86
+ """
87
+ Initialize a prompt template.
88
+
89
+ Args:
90
+ template_text: The text of the template
91
+ delimiter_map: Optional map of delimiters to roles (e.g. {"---USER": "user"})
92
+ template_file_path: Optional path to the template file (for resource resolution)
93
+ """
94
+ self.template_text = template_text
95
+ self.template_file_path = template_file_path
96
+ self.delimiter_map = delimiter_map or {
97
+ "---USER": "user",
98
+ "---ASSISTANT": "assistant",
99
+ "---RESOURCE": "resource",
100
+ }
101
+ self._template_variables = self._extract_template_variables(template_text)
102
+ self._parsed_content = self._parse_template()
103
+
104
+ @classmethod
105
+ def from_multipart_messages(
106
+ cls,
107
+ messages: List[PromptMessageMultipart],
108
+ delimiter_map: Optional[Dict[str, str]] = None,
109
+ ) -> "PromptTemplate":
110
+ """
111
+ Create a PromptTemplate from a list of PromptMessageMultipart objects.
112
+
113
+ Args:
114
+ messages: List of PromptMessageMultipart objects
115
+ delimiter_map: Optional map of delimiters to roles
116
+
117
+ Returns:
118
+ A new PromptTemplate object
119
+ """
120
+ # Use default delimiter map if none provided
121
+ delimiter_map = delimiter_map or {
122
+ "---USER": "user",
123
+ "---ASSISTANT": "assistant",
124
+ "---RESOURCE": "resource",
125
+ }
126
+
127
+ # Convert to delimited format
128
+ delimited_content = multipart_messages_to_delimited_format(
129
+ messages,
130
+ user_delimiter=next(
131
+ (k for k, v in delimiter_map.items() if v == "user"), "---USER"
132
+ ),
133
+ assistant_delimiter=next(
134
+ (k for k, v in delimiter_map.items() if v == "assistant"),
135
+ "---ASSISTANT",
136
+ ),
137
+ )
138
+
139
+ # Join into a single string
140
+ content = "\n".join(delimited_content)
141
+
142
+ # Create and return the template
143
+ return cls(content, delimiter_map)
144
+
145
+ @property
146
+ def template_variables(self) -> Set[str]:
147
+ """Get the template variables in this template"""
148
+ return self._template_variables
149
+
150
+ @property
151
+ def content_sections(self) -> List[PromptContent]:
152
+ """Get the parsed content sections"""
153
+ return self._parsed_content
154
+
155
+ def apply_substitutions(self, context: Dict[str, Any]) -> List[PromptContent]:
156
+ """
157
+ Apply variable substitutions to the template.
158
+
159
+ Args:
160
+ context: Dictionary of variable names to values
161
+
162
+ Returns:
163
+ List of PromptContent with substitutions applied
164
+ """
165
+ result = []
166
+ for section in self._parsed_content:
167
+ result.append(section.apply_substitutions(context))
168
+ return result
169
+
170
+ def apply_substitutions_to_multipart(
171
+ self, context: Dict[str, Any]
172
+ ) -> List[PromptMessageMultipart]:
173
+ """
174
+ Apply variable substitutions to the template and return PromptMessageMultipart objects.
175
+
176
+ Args:
177
+ context: Dictionary of variable names to values
178
+
179
+ Returns:
180
+ List of PromptMessageMultipart objects with substitutions applied
181
+ """
182
+ content_sections = self.apply_substitutions(context)
183
+ multiparts = []
184
+
185
+ for section in content_sections:
186
+ # Handle text content
187
+ content_items = [TextContent(type="text", text=section.text)]
188
+
189
+ # Handle resources (if any)
190
+ for resource_path in section.resources:
191
+ # In a real implementation, you would load the resource here
192
+ # For now, we'll just create a placeholder
193
+ content_items.append(
194
+ EmbeddedResource(
195
+ type="resource",
196
+ resource=TextResourceContents(
197
+ uri=f"resource://fast-agent/{resource_path}",
198
+ mimeType="text/plain",
199
+ text=f"Content of {resource_path}",
200
+ ),
201
+ )
202
+ )
203
+
204
+ multiparts.append(
205
+ PromptMessageMultipart(role=section.role, content=content_items)
206
+ )
207
+
208
+ return multiparts
209
+
210
+ def _extract_template_variables(self, text: str) -> Set[str]:
211
+ """Extract template variables from text using regex"""
212
+ variable_pattern = r"{{([^}]+)}}"
213
+ matches = re.findall(variable_pattern, text)
214
+ return set(matches)
215
+
216
+ def to_multipart_messages(self) -> List[PromptMessageMultipart]:
217
+ """
218
+ Convert this template to a list of PromptMessageMultipart objects.
219
+
220
+ Returns:
221
+ List of PromptMessageMultipart objects
222
+ """
223
+ multiparts = []
224
+
225
+ for section in self._parsed_content:
226
+ # Convert each section to a multipart message
227
+ content_items = [TextContent(type="text", text=section.text)]
228
+
229
+ # Add any resources as embedded resources
230
+ for resource_path in section.resources:
231
+ # In a real implementation, you would determine the MIME type
232
+ # and load the resource appropriately. Here we'll just use a placeholder.
233
+ content_items.append(
234
+ EmbeddedResource(
235
+ type="resource",
236
+ resource=TextResourceContents(
237
+ uri=f"resource://{resource_path}",
238
+ mimeType="text/plain",
239
+ text=f"Content of {resource_path}",
240
+ ),
241
+ )
242
+ )
243
+
244
+ multiparts.append(
245
+ PromptMessageMultipart(role=section.role, content=content_items)
246
+ )
247
+
248
+ return multiparts
249
+
250
+ def _parse_template(self) -> List[PromptContent]:
251
+ """
252
+ Parse the template into sections based on delimiters.
253
+ If no delimiters are found, treat the entire template as a single user message.
254
+
255
+ Resources are now collected within their parent sections, keeping the same role.
256
+ """
257
+ lines = self.template_text.split("\n")
258
+
259
+ # Check if we're in simple mode (no delimiters)
260
+ first_non_empty_line = next((line for line in lines if line.strip()), "")
261
+ delimiter_values = set(self.delimiter_map.keys())
262
+
263
+ is_simple_mode = (
264
+ first_non_empty_line and first_non_empty_line not in delimiter_values
265
+ )
266
+
267
+ if is_simple_mode:
268
+ # Simple mode: treat the entire content as a single user message
269
+ return [PromptContent(text=self.template_text, role="user", resources=[])]
270
+
271
+ # Standard mode with delimiters
272
+ sections = []
273
+ current_role = None
274
+ current_content = ""
275
+ current_resources = []
276
+
277
+ i = 0
278
+ while i < len(lines):
279
+ line = lines[i]
280
+
281
+ # Check if we hit a delimiter
282
+ if line.strip() in self.delimiter_map:
283
+ role_type = self.delimiter_map[line.strip()]
284
+
285
+ # If we're moving to a new user/assistant section (not resource)
286
+ if role_type != "resource":
287
+ # Save the previous section if it exists
288
+ if current_role is not None and current_content:
289
+ sections.append(
290
+ PromptContent(
291
+ text=current_content.strip(),
292
+ role=current_role,
293
+ resources=current_resources,
294
+ )
295
+ )
296
+
297
+ # Start a new section
298
+ current_role = role_type
299
+ current_content = ""
300
+ current_resources = []
301
+
302
+ # Handle resource delimiters within sections
303
+ elif role_type == "resource" and i + 1 < len(lines):
304
+ resource_path = lines[i + 1].strip()
305
+ current_resources.append(resource_path)
306
+ # Skip the resource path line
307
+ i += 1
308
+
309
+ # If we're in a section, add to the current content
310
+ elif current_role is not None:
311
+ current_content += line + "\n"
312
+
313
+ i += 1
314
+
315
+ # Add the last section if there is one
316
+ if current_role is not None and current_content:
317
+ sections.append(
318
+ PromptContent(
319
+ text=current_content.strip(),
320
+ role=current_role,
321
+ resources=current_resources,
322
+ )
323
+ )
324
+
325
+ return sections
326
+
327
+
328
+ class PromptTemplateLoader:
329
+ """
330
+ Loads and processes prompt templates from files.
331
+ """
332
+
333
+ def __init__(self, delimiter_map: Optional[Dict[str, str]] = None):
334
+ """
335
+ Initialize the loader with optional custom delimiters.
336
+
337
+ Args:
338
+ delimiter_map: Optional map of delimiters to roles
339
+ """
340
+ self.delimiter_map = delimiter_map or {
341
+ "---USER": "user",
342
+ "---ASSISTANT": "assistant",
343
+ "---RESOURCE": "resource",
344
+ }
345
+
346
+ def load_from_file(self, file_path: Path) -> PromptTemplate:
347
+ """
348
+ Load a prompt template from a file.
349
+
350
+ Args:
351
+ file_path: Path to the template file
352
+
353
+ Returns:
354
+ A PromptTemplate object
355
+ """
356
+ with open(file_path, "r", encoding="utf-8") as f:
357
+ content = f.read()
358
+
359
+ return PromptTemplate(content, self.delimiter_map, template_file_path=file_path)
360
+
361
+ def load_from_multipart(
362
+ self, messages: List[PromptMessageMultipart]
363
+ ) -> PromptTemplate:
364
+ """
365
+ Create a PromptTemplate from a list of PromptMessageMultipart objects.
366
+
367
+ Args:
368
+ messages: List of PromptMessageMultipart objects
369
+
370
+ Returns:
371
+ A PromptTemplate object
372
+ """
373
+ delimited_content = multipart_messages_to_delimited_format(
374
+ messages,
375
+ user_delimiter=next(
376
+ (k for k, v in self.delimiter_map.items() if v == "user"), "---USER"
377
+ ),
378
+ assistant_delimiter=next(
379
+ (k for k, v in self.delimiter_map.items() if v == "assistant"),
380
+ "---ASSISTANT",
381
+ ),
382
+ )
383
+
384
+ content = "\n".join(delimited_content)
385
+ return PromptTemplate(content, self.delimiter_map)
386
+
387
+ def get_metadata(self, file_path: Path) -> PromptMetadata:
388
+ """
389
+ Analyze a prompt file to extract metadata and template variables.
390
+
391
+ Args:
392
+ file_path: Path to the template file
393
+
394
+ Returns:
395
+ PromptMetadata with information about the template
396
+ """
397
+ template = self.load_from_file(file_path)
398
+
399
+ # Generate a description based on content
400
+ lines = template.template_text.split("\n")
401
+ first_non_empty_line = next((line for line in lines if line.strip()), "")
402
+
403
+ # Check if we're in simple mode
404
+ is_simple_mode = (
405
+ first_non_empty_line and first_non_empty_line not in self.delimiter_map
406
+ )
407
+
408
+ if is_simple_mode:
409
+ # In simple mode, use first line as description if it seems like one
410
+ first_line = lines[0].strip() if lines else ""
411
+ if (
412
+ len(first_line) < 60
413
+ and "{{" not in first_line
414
+ and "}}" not in first_line
415
+ ):
416
+ description = first_line
417
+ else:
418
+ description = f"Simple prompt: {file_path.stem}"
419
+ else:
420
+ # Regular mode - find text after first delimiter for the description
421
+ description = file_path.stem
422
+
423
+ # Look for first delimiter and role
424
+ first_role = None
425
+ first_content_index = None
426
+
427
+ for i, line in enumerate(lines):
428
+ stripped = line.strip()
429
+ if stripped in self.delimiter_map:
430
+ first_role = self.delimiter_map[stripped]
431
+ first_content_index = i + 1
432
+ break
433
+
434
+ if first_role and first_content_index and first_content_index < len(lines):
435
+ # Get up to 3 non-empty lines after the delimiter for a preview
436
+ preview_lines = []
437
+ for j in range(
438
+ first_content_index, min(first_content_index + 10, len(lines))
439
+ ):
440
+ stripped = lines[j].strip()
441
+ if stripped and stripped not in self.delimiter_map:
442
+ preview_lines.append(stripped)
443
+ if len(preview_lines) >= 3:
444
+ break
445
+
446
+ if preview_lines:
447
+ preview = " ".join(preview_lines)
448
+ if len(preview) > 50:
449
+ preview = preview[:47] + "..."
450
+ # Include role in the description but not the filename
451
+ description = f"[{first_role.upper()}] {preview}"
452
+
453
+ # Extract resource paths from all sections that come after RESOURCE delimiters
454
+ resource_paths = []
455
+ resource_delimiter = next(
456
+ (k for k, v in self.delimiter_map.items() if v == "resource"), "---RESOURCE"
457
+ )
458
+ for i, line in enumerate(lines):
459
+ if line.strip() == resource_delimiter:
460
+ if i + 1 < len(lines) and lines[i + 1].strip():
461
+ resource_paths.append(lines[i + 1].strip())
462
+
463
+ return PromptMetadata(
464
+ name=file_path.stem,
465
+ description=description,
466
+ template_variables=template.template_variables,
467
+ resource_paths=resource_paths,
468
+ file_path=file_path,
469
+ )
@@ -0,0 +1,203 @@
1
+ import base64
2
+ from pathlib import Path
3
+ from typing import List, Optional, Tuple
4
+ from mcp.types import (
5
+ EmbeddedResource,
6
+ TextResourceContents,
7
+ BlobResourceContents,
8
+ ImageContent,
9
+ )
10
+ from pydantic import AnyUrl
11
+ import mcp_agent.mcp.mime_utils as mime_utils
12
+
13
+ HTTP_TIMEOUT = 10 # Default timeout for HTTP requests
14
+
15
+ # Define a type alias for resource content results
16
+ ResourceContent = Tuple[str, str, bool]
17
+
18
+
19
+ def find_resource_file(resource_path: str, prompt_files: List[Path]) -> Optional[Path]:
20
+ """Find a resource file relative to one of the prompt files"""
21
+ for prompt_file in prompt_files:
22
+ potential_path = prompt_file.parent / resource_path
23
+ if potential_path.exists():
24
+ return potential_path
25
+ return None
26
+
27
+
28
+ # TODO -- decide how to deal with this. Both Anthropic and OpenAI allow sending URLs in
29
+ # input message
30
+ # TODO -- used?
31
+ # async def fetch_remote_resource(
32
+ # url: str, timeout: int = HTTP_TIMEOUT
33
+ # ) -> ResourceContent:
34
+ # """
35
+ # Fetch a remote resource from a URL
36
+
37
+ # Returns:
38
+ # Tuple of (content, mime_type, is_binary)
39
+ # - content: Text content or base64-encoded binary content
40
+ # - mime_type: The MIME type of the resource
41
+ # - is_binary: Whether the content is binary (and base64-encoded)
42
+ # """
43
+
44
+ # async with httpx.AsyncClient(timeout=timeout) as client:
45
+ # response = await client.get(url)
46
+ # response.raise_for_status()
47
+
48
+ # # Get the content type or guess from URL
49
+ # mime_type = response.headers.get("content-type", "").split(";")[0]
50
+ # if not mime_type:
51
+ # mime_type = mime_utils.guess_mime_type(url)
52
+
53
+ # # Check if this is binary content
54
+ # is_binary = mime_utils.is_binary_content(mime_type)
55
+
56
+ # if is_binary:
57
+ # # For binary responses, get the binary content and base64 encode it
58
+ # content = base64.b64encode(response.content).decode("utf-8")
59
+ # else:
60
+ # # For text responses, just get the text
61
+ # content = response.text
62
+
63
+ # return content, mime_type, is_binary
64
+
65
+
66
+ def load_resource_content(
67
+ resource_path: str, prompt_files: List[Path]
68
+ ) -> ResourceContent:
69
+ """
70
+ Load a resource's content and determine its mime type
71
+
72
+ Args:
73
+ resource_path: Path to the resource file
74
+ prompt_files: List of prompt files (to find relative paths)
75
+
76
+ Returns:
77
+ Tuple of (content, mime_type, is_binary)
78
+ - content: String content for text files, base64-encoded string for binary files
79
+ - mime_type: The MIME type of the resource
80
+ - is_binary: Whether the content is binary (and base64-encoded)
81
+
82
+ Raises:
83
+ FileNotFoundError: If the resource cannot be found
84
+ """
85
+ # Try to locate the resource file
86
+ resource_file = find_resource_file(resource_path, prompt_files)
87
+ if resource_file is None:
88
+ raise FileNotFoundError(f"Resource not found: {resource_path}")
89
+
90
+ # Determine mime type
91
+ mime_type = mime_utils.guess_mime_type(str(resource_file))
92
+ is_binary = mime_utils.is_binary_content(mime_type)
93
+
94
+ if is_binary:
95
+ # For binary files, read as binary and base64 encode
96
+ with open(resource_file, "rb") as f:
97
+ content = base64.b64encode(f.read()).decode("utf-8")
98
+ else:
99
+ # For text files, read as text
100
+ with open(resource_file, "r", encoding="utf-8") as f:
101
+ content = f.read()
102
+
103
+ return content, mime_type, is_binary
104
+
105
+
106
+ # Create a safe way to generate resource URIs that Pydantic accepts
107
+ def create_resource_uri(path: str) -> str:
108
+ """Create a resource URI from a path"""
109
+ return f"resource://fast-agent/{Path(path).name}"
110
+
111
+
112
+ def create_embedded_resource(
113
+ resource_path: str, content: str, mime_type: str, is_binary: bool = False
114
+ ) -> EmbeddedResource:
115
+ """Create an embedded resource content object"""
116
+ # Format a valid resource URI string
117
+ resource_uri_str = create_resource_uri(resource_path)
118
+
119
+ # Create common resource args dict to reduce duplication
120
+ resource_args = {
121
+ "uri": resource_uri_str, # type: ignore
122
+ "mimeType": mime_type,
123
+ }
124
+
125
+ if is_binary:
126
+ return EmbeddedResource(
127
+ type="resource",
128
+ resource=BlobResourceContents(
129
+ **resource_args,
130
+ blob=content,
131
+ ),
132
+ )
133
+ else:
134
+ return EmbeddedResource(
135
+ type="resource",
136
+ resource=TextResourceContents(
137
+ **resource_args,
138
+ text=content,
139
+ ),
140
+ )
141
+
142
+
143
+ def create_image_content(data: str, mime_type: str) -> ImageContent:
144
+ """Create an image content object from base64-encoded data"""
145
+ return ImageContent(
146
+ type="image",
147
+ data=data,
148
+ mimeType=mime_type,
149
+ )
150
+
151
+
152
+ def normalize_uri(uri_or_filename: str) -> str:
153
+ """
154
+ Normalize a URI or filename to ensure it's a valid URI.
155
+ Converts simple filenames to file:// URIs if needed.
156
+
157
+ Args:
158
+ uri_or_filename: A URI string or simple filename
159
+
160
+ Returns:
161
+ A properly formatted URI string
162
+ """
163
+ if not uri_or_filename:
164
+ return ""
165
+
166
+ # Check if it's already a valid URI with a scheme
167
+ if "://" in uri_or_filename:
168
+ return uri_or_filename
169
+
170
+ # Handle Windows-style paths with backslashes
171
+ normalized_path = uri_or_filename.replace("\\", "/")
172
+
173
+ # If it's a simple filename or relative path, convert to file:// URI
174
+ # Make sure it has three slashes for an absolute path
175
+ if normalized_path.startswith("/"):
176
+ return f"file://{normalized_path}"
177
+ else:
178
+ return f"file:///{normalized_path}"
179
+
180
+
181
+ def extract_title_from_uri(uri: AnyUrl) -> str:
182
+ """Extract a readable title from a URI."""
183
+ # Simple attempt to get filename from path
184
+ uri_str = uri._url
185
+ try:
186
+ # For HTTP(S) URLs
187
+ if uri.scheme in ("http", "https"):
188
+ # Get the last part of the path
189
+ path_parts = uri.path.split("/")
190
+ filename = next((p for p in reversed(path_parts) if p), "")
191
+ return filename if filename else uri_str
192
+
193
+ # For file URLs or other schemes
194
+ elif uri.path:
195
+ import os.path
196
+
197
+ return os.path.basename(uri.path)
198
+
199
+ except Exception:
200
+ pass
201
+
202
+ # Fallback to the full URI if parsing fails
203
+ return uri_str
@@ -10,7 +10,7 @@ fast = FastAgent("FastAgent Example")
10
10
  async def main():
11
11
  # use the --model command line switch or agent arguments to change model
12
12
  async with fast.run() as agent:
13
- await agent()
13
+ await agent.prompt()
14
14
 
15
15
 
16
16
  if __name__ == "__main__":
@@ -44,9 +44,9 @@ mcp:
44
44
  # args: ["c:/Program Files/nodejs/node_modules/@modelcontextprotocol/server-brave-search/dist/index.js"]
45
45
  command: "npx"
46
46
  args: ["-y", "@modelcontextprotocol/server-brave-search"]
47
- sizer:
47
+ sizing_setup:
48
48
  command: "uv"
49
- args: ["run", "prompt_sizing.py"]
49
+ args: ["run", "prompt_sizing1.py"]
50
50
  category:
51
51
  command: "uv"
52
52
  args: ["run", "prompt_category.py"]