fast-agent-mcp 0.4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,438 @@
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 Any, Literal
11
+
12
+ from mcp.types import (
13
+ EmbeddedResource,
14
+ TextContent,
15
+ TextResourceContents,
16
+ )
17
+ from pydantic import BaseModel, field_validator
18
+
19
+ from fast_agent.mcp.prompt_serialization import (
20
+ multipart_messages_to_delimited_format,
21
+ )
22
+ from fast_agent.mcp.prompts.prompt_constants import (
23
+ ASSISTANT_DELIMITER,
24
+ DEFAULT_DELIMITER_MAP,
25
+ RESOURCE_DELIMITER,
26
+ USER_DELIMITER,
27
+ )
28
+ from fast_agent.types import PromptMessageExtended
29
+
30
+
31
+ class PromptMetadata(BaseModel):
32
+ """Metadata about a prompt file"""
33
+
34
+ name: str
35
+ description: str
36
+ template_variables: set[str] = set()
37
+ resource_paths: list[str] = []
38
+ file_path: Path
39
+
40
+
41
+ # Define valid message roles for better type safety
42
+ MessageRole = Literal["user", "assistant"]
43
+
44
+
45
+ class PromptContent(BaseModel):
46
+ """Content of a prompt, which may include template variables"""
47
+
48
+ text: str
49
+ role: str = "user"
50
+ resources: list[str] = []
51
+
52
+ @field_validator("role")
53
+ @classmethod
54
+ def validate_role(cls, role: str) -> str:
55
+ """Validate that the role is a known value"""
56
+ if role not in ("user", "assistant"):
57
+ raise ValueError(f"Invalid role: {role}. Must be one of: user, assistant")
58
+ return role
59
+
60
+ def apply_substitutions(self, context: dict[str, Any]) -> "PromptContent":
61
+ """Apply variable substitutions to the text and resources"""
62
+
63
+ # Define placeholder pattern once to avoid repetition
64
+ def make_placeholder(key: str) -> str:
65
+ return f"{{{{{key}}}}}"
66
+
67
+ # Apply substitutions to text
68
+ result = self.text
69
+ for key, value in context.items():
70
+ result = result.replace(make_placeholder(key), str(value))
71
+
72
+ # Apply substitutions to resource paths
73
+ substituted_resources = []
74
+ for resource in self.resources:
75
+ substituted = resource
76
+ for key, value in context.items():
77
+ substituted = substituted.replace(make_placeholder(key), str(value))
78
+ substituted_resources.append(substituted)
79
+
80
+ return PromptContent(text=result, role=self.role, resources=substituted_resources)
81
+
82
+
83
+ class PromptTemplate:
84
+ """
85
+ A template for a prompt that can have variables substituted.
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ template_text: str,
91
+ delimiter_map: dict[str, str] | None = None,
92
+ template_file_path: Path | None = None,
93
+ ) -> None:
94
+ """
95
+ Initialize a prompt template.
96
+
97
+ Args:
98
+ template_text: The text of the template
99
+ delimiter_map: Optional map of delimiters to roles (e.g. {"---USER": "user"})
100
+ template_file_path: Optional path to the template file (for resource resolution)
101
+ """
102
+ self.template_text = template_text
103
+ self.template_file_path = template_file_path
104
+ self.delimiter_map = delimiter_map or DEFAULT_DELIMITER_MAP
105
+ self._template_variables = self._extract_template_variables(template_text)
106
+ self._parsed_content = self._parse_template()
107
+
108
+ @classmethod
109
+ def from_multipart_messages(
110
+ cls,
111
+ messages: list[PromptMessageExtended],
112
+ delimiter_map: dict[str, str] | None = None,
113
+ ) -> "PromptTemplate":
114
+ """
115
+ Create a PromptTemplate from a list of PromptMessageExtended objects.
116
+
117
+ Args:
118
+ messages: List of PromptMessageExtended objects
119
+ delimiter_map: Optional map of delimiters to roles
120
+
121
+ Returns:
122
+ A new PromptTemplate object
123
+ """
124
+ # Use default delimiter map if none provided
125
+ delimiter_map = delimiter_map or DEFAULT_DELIMITER_MAP
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_DELIMITER
132
+ ),
133
+ assistant_delimiter=next(
134
+ (k for k, v in delimiter_map.items() if v == "assistant"),
135
+ ASSISTANT_DELIMITER,
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
+ # Create a new list with substitutions applied to each section
166
+ return [section.apply_substitutions(context) for section in self._parsed_content]
167
+
168
+ def apply_substitutions_to_extended(
169
+ self, context: dict[str, Any]
170
+ ) -> list[PromptMessageExtended]:
171
+ """
172
+ Apply variable substitutions to the template and return PromptMessageExtended objects.
173
+
174
+ Args:
175
+ context: Dictionary of variable names to values
176
+
177
+ Returns:
178
+ List of PromptMessageExtended objects with substitutions applied
179
+ """
180
+ # First create a substituted template
181
+ content_sections = self.apply_substitutions(context)
182
+
183
+ # Convert content sections to multipart messages
184
+ multiparts = []
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(PromptMessageExtended(role=section.role, content=content_items))
205
+
206
+ return multiparts
207
+
208
+ def _extract_template_variables(self, text: str) -> set[str]:
209
+ """Extract template variables from text using regex"""
210
+ variable_pattern = r"{{([^}]+)}}"
211
+ matches = re.findall(variable_pattern, text)
212
+ return set(matches)
213
+
214
+ def to_extended_messages(self) -> list[PromptMessageExtended]:
215
+ """
216
+ Convert this template to a list of PromptMessageExtended objects.
217
+
218
+ Returns:
219
+ List of PromptMessageExtended objects
220
+ """
221
+ multiparts = []
222
+
223
+ for section in self._parsed_content:
224
+ # Convert each section to a multipart message
225
+ content_items = [TextContent(type="text", text=section.text)]
226
+
227
+ # Add any resources as embedded resources
228
+ for resource_path in section.resources:
229
+ # In a real implementation, you would determine the MIME type
230
+ # and load the resource appropriately. Here we'll just use a placeholder.
231
+ content_items.append(
232
+ EmbeddedResource(
233
+ type="resource",
234
+ resource=TextResourceContents(
235
+ uri=f"resource://{resource_path}",
236
+ mimeType="text/plain",
237
+ text=f"Content of {resource_path}",
238
+ ),
239
+ )
240
+ )
241
+
242
+ multiparts.append(PromptMessageExtended(role=section.role, content=content_items))
243
+
244
+ return multiparts
245
+
246
+ def _parse_template(self) -> list[PromptContent]:
247
+ """
248
+ Parse the template into sections based on delimiters.
249
+ If no delimiters are found, treat the entire template as a single user message.
250
+
251
+ Resources are now collected within their parent sections, keeping the same role.
252
+ """
253
+ lines = self.template_text.split("\n")
254
+
255
+ # Check if we're in simple mode (no delimiters)
256
+ first_non_empty_line = next((line for line in lines if line.strip()), "")
257
+ delimiter_values = set(self.delimiter_map.keys())
258
+
259
+ is_simple_mode = first_non_empty_line and first_non_empty_line not in delimiter_values
260
+
261
+ if is_simple_mode:
262
+ # Simple mode: treat the entire content as a single user message
263
+ return [PromptContent(text=self.template_text, role="user", resources=[])]
264
+
265
+ # Standard mode with delimiters
266
+ sections = []
267
+ current_role = None
268
+ current_content = ""
269
+ current_resources = []
270
+
271
+ i = 0
272
+ while i < len(lines):
273
+ line = lines[i]
274
+
275
+ # Check if we hit a delimiter
276
+ if line.strip() in self.delimiter_map:
277
+ role_type = self.delimiter_map[line.strip()]
278
+
279
+ # If we're moving to a new user/assistant section (not resource)
280
+ if role_type != "resource":
281
+ # Save the previous section if it exists
282
+ if current_role is not None and current_content:
283
+ sections.append(
284
+ PromptContent(
285
+ text=current_content.strip(),
286
+ role=current_role,
287
+ resources=current_resources,
288
+ )
289
+ )
290
+
291
+ # Start a new section
292
+ current_role = role_type
293
+ current_content = ""
294
+ current_resources = []
295
+
296
+ # Handle resource delimiters within sections
297
+ elif role_type == "resource" and i + 1 < len(lines):
298
+ resource_path = lines[i + 1].strip()
299
+ current_resources.append(resource_path)
300
+ # Skip the resource path line
301
+ i += 1
302
+
303
+ # If we're in a section, add to the current content
304
+ elif current_role is not None:
305
+ current_content += line + "\n"
306
+
307
+ i += 1
308
+
309
+ # Add the last section if there is one
310
+ if current_role is not None and current_content:
311
+ sections.append(
312
+ PromptContent(
313
+ text=current_content.strip(),
314
+ role=current_role,
315
+ resources=current_resources,
316
+ )
317
+ )
318
+
319
+ return sections
320
+
321
+
322
+ class PromptTemplateLoader:
323
+ """
324
+ Loads and processes prompt templates from files.
325
+ """
326
+
327
+ def __init__(self, delimiter_map: dict[str, str] | None = None) -> None:
328
+ """
329
+ Initialize the loader with optional custom delimiters.
330
+
331
+ Args:
332
+ delimiter_map: Optional map of delimiters to roles
333
+ """
334
+ self.delimiter_map = delimiter_map or DEFAULT_DELIMITER_MAP
335
+
336
+ def load_from_file(self, file_path: Path) -> PromptTemplate:
337
+ """
338
+ Load a prompt template from a file.
339
+
340
+ Args:
341
+ file_path: Path to the template file
342
+
343
+ Returns:
344
+ A PromptTemplate object
345
+ """
346
+ with open(file_path, "r", encoding="utf-8") as f:
347
+ content = f.read()
348
+
349
+ return PromptTemplate(content, self.delimiter_map, template_file_path=file_path)
350
+
351
+ def load_from_multipart(self, messages: list[PromptMessageExtended]) -> PromptTemplate:
352
+ """
353
+ Create a PromptTemplate from a list of PromptMessageExtended objects.
354
+
355
+ Args:
356
+ messages: List of PromptMessageExtended objects
357
+
358
+ Returns:
359
+ A PromptTemplate object
360
+ """
361
+ # Use the class method directly to avoid code duplication
362
+ return PromptTemplate.from_multipart_messages(messages, self.delimiter_map)
363
+
364
+ def get_metadata(self, file_path: Path) -> PromptMetadata:
365
+ """
366
+ Analyze a prompt file to extract metadata and template variables.
367
+
368
+ Args:
369
+ file_path: Path to the template file
370
+
371
+ Returns:
372
+ PromptMetadata with information about the template
373
+ """
374
+ template = self.load_from_file(file_path)
375
+
376
+ # Generate a description based on content
377
+ lines = template.template_text.split("\n")
378
+ first_non_empty_line = next((line for line in lines if line.strip()), "")
379
+
380
+ # Check if we're in simple mode
381
+ is_simple_mode = first_non_empty_line and first_non_empty_line not in self.delimiter_map
382
+
383
+ if is_simple_mode:
384
+ # In simple mode, use first line as description if it seems like one
385
+ first_line = lines[0].strip() if lines else ""
386
+ if len(first_line) < 60 and "{{" not in first_line and "}}" not in first_line:
387
+ description = first_line
388
+ else:
389
+ description = f"Simple prompt: {file_path.stem}"
390
+ else:
391
+ # Regular mode - find text after first delimiter for the description
392
+ description = file_path.stem
393
+
394
+ # Look for first delimiter and role
395
+ first_role = None
396
+ first_content_index = None
397
+
398
+ for i, line in enumerate(lines):
399
+ stripped = line.strip()
400
+ if stripped in self.delimiter_map:
401
+ first_role = self.delimiter_map[stripped]
402
+ first_content_index = i + 1
403
+ break
404
+
405
+ if first_role and first_content_index and first_content_index < len(lines):
406
+ # Get up to 3 non-empty lines after the delimiter for a preview
407
+ preview_lines = []
408
+ for j in range(first_content_index, min(first_content_index + 10, len(lines))):
409
+ stripped = lines[j].strip()
410
+ if stripped and stripped not in self.delimiter_map:
411
+ preview_lines.append(stripped)
412
+ if len(preview_lines) >= 3:
413
+ break
414
+
415
+ if preview_lines:
416
+ preview = " ".join(preview_lines)
417
+ if len(preview) > 50:
418
+ preview = preview[:47] + "..."
419
+ # Include role in the description but not the filename
420
+ description = f"[{first_role.upper()}] {preview}"
421
+
422
+ # Extract resource paths from all sections that come after RESOURCE delimiters
423
+ resource_paths = []
424
+ resource_delimiter = next(
425
+ (k for k, v in self.delimiter_map.items() if v == "resource"), RESOURCE_DELIMITER
426
+ )
427
+ for i, line in enumerate(lines):
428
+ if line.strip() == resource_delimiter:
429
+ if i + 1 < len(lines) and lines[i + 1].strip():
430
+ resource_paths.append(lines[i + 1].strip())
431
+
432
+ return PromptMetadata(
433
+ name=file_path.stem,
434
+ description=description,
435
+ template_variables=template.template_variables,
436
+ resource_paths=resource_paths,
437
+ file_path=file_path,
438
+ )
@@ -0,0 +1,215 @@
1
+ import base64
2
+ from pathlib import Path
3
+
4
+ from mcp.types import (
5
+ BlobResourceContents,
6
+ EmbeddedResource,
7
+ ImageContent,
8
+ TextResourceContents,
9
+ )
10
+ from pydantic import AnyUrl
11
+
12
+ import fast_agent.mcp.mime_utils as mime_utils
13
+
14
+ HTTP_TIMEOUT = 10 # Default timeout for HTTP requests
15
+
16
+ # Define a type alias for resource content results
17
+ ResourceContent = tuple[str, str, bool]
18
+
19
+
20
+ def find_resource_file(resource_path: str, prompt_files: list[Path]) -> Path | None:
21
+ """Find a resource file relative to one of the prompt files"""
22
+ for prompt_file in prompt_files:
23
+ potential_path = prompt_file.parent / resource_path
24
+ if potential_path.exists():
25
+ return potential_path
26
+ return None
27
+
28
+
29
+ def load_resource_content(resource_path: str, prompt_files: list[Path]) -> ResourceContent:
30
+ """
31
+ Load a resource's content and determine its mime type
32
+
33
+ Args:
34
+ resource_path: Path to the resource file
35
+ prompt_files: List of prompt files (to find relative paths)
36
+
37
+ Returns:
38
+ Tuple of (content, mime_type, is_binary)
39
+ - content: String content for text files, base64-encoded string for binary files
40
+ - mime_type: The MIME type of the resource
41
+ - is_binary: Whether the content is binary (and base64-encoded)
42
+
43
+ Raises:
44
+ FileNotFoundError: If the resource cannot be found
45
+ """
46
+ # Try to locate the resource file
47
+ resource_file = find_resource_file(resource_path, prompt_files)
48
+ if resource_file is None:
49
+ raise FileNotFoundError(f"Resource not found: {resource_path}")
50
+
51
+ # Determine mime type
52
+ mime_type = mime_utils.guess_mime_type(str(resource_file))
53
+ is_binary = mime_utils.is_binary_content(mime_type)
54
+
55
+ if is_binary:
56
+ # For binary files, read as binary and base64 encode
57
+ with open(resource_file, "rb") as f:
58
+ content = base64.b64encode(f.read()).decode("utf-8")
59
+ else:
60
+ # For text files, read as text
61
+ with open(resource_file, "r", encoding="utf-8") as f:
62
+ content = f.read()
63
+
64
+ return content, mime_type, is_binary
65
+
66
+
67
+ # Create a safe way to generate resource URIs that Pydantic accepts
68
+ def create_resource_uri(path: str) -> str:
69
+ """Create a resource URI from a path"""
70
+ return f"resource://fast-agent/{Path(path).name}"
71
+
72
+
73
+ def create_resource_reference(uri: str, mime_type: str) -> "EmbeddedResource":
74
+ """
75
+ Create a reference to a resource without embedding its content directly.
76
+
77
+ This creates an EmbeddedResource that references another resource URI.
78
+ When the client receives this, it will make a separate request to fetch
79
+ the resource content using the provided URI.
80
+
81
+ Args:
82
+ uri: URI for the resource
83
+ mime_type: MIME type of the resource
84
+
85
+ Returns:
86
+ An EmbeddedResource object
87
+ """
88
+ from mcp.types import EmbeddedResource, TextResourceContents
89
+
90
+ # Create a resource reference
91
+ resource_contents = TextResourceContents(
92
+ uri=uri,
93
+ mimeType=mime_type,
94
+ text="", # Empty text as we're just referencing
95
+ )
96
+
97
+ return EmbeddedResource(type="resource", resource=resource_contents)
98
+
99
+
100
+ def create_embedded_resource(
101
+ resource_path: str, content: str, mime_type: str, is_binary: bool = False
102
+ ) -> EmbeddedResource:
103
+ """Create an embedded resource content object"""
104
+ # Format a valid resource URI string
105
+ resource_uri_str = create_resource_uri(resource_path)
106
+
107
+ # Create common resource args dict to reduce duplication
108
+ resource_args = {
109
+ "uri": resource_uri_str, # type: ignore
110
+ "mimeType": mime_type,
111
+ }
112
+
113
+ if is_binary:
114
+ return EmbeddedResource(
115
+ type="resource",
116
+ resource=BlobResourceContents(
117
+ **resource_args,
118
+ blob=content,
119
+ ),
120
+ )
121
+ else:
122
+ return EmbeddedResource(
123
+ type="resource",
124
+ resource=TextResourceContents(
125
+ **resource_args,
126
+ text=content,
127
+ ),
128
+ )
129
+
130
+
131
+ def create_image_content(data: str, mime_type: str) -> ImageContent:
132
+ """Create an image content object from base64-encoded data"""
133
+ return ImageContent(
134
+ type="image",
135
+ data=data,
136
+ mimeType=mime_type,
137
+ )
138
+
139
+
140
+ def create_blob_resource(resource_path: str, content: str, mime_type: str) -> EmbeddedResource:
141
+ """Create an embedded resource for binary data"""
142
+ return EmbeddedResource(
143
+ type="resource",
144
+ resource=BlobResourceContents(
145
+ uri=resource_path,
146
+ mimeType=mime_type,
147
+ blob=content, # Content should already be base64 encoded
148
+ ),
149
+ )
150
+
151
+
152
+ def create_text_resource(resource_path: str, content: str, mime_type: str) -> EmbeddedResource:
153
+ """Create an embedded resource for text data"""
154
+ return EmbeddedResource(
155
+ type="resource",
156
+ resource=TextResourceContents(
157
+ uri=resource_path,
158
+ mimeType=mime_type,
159
+ text=content,
160
+ ),
161
+ )
162
+
163
+
164
+ def normalize_uri(uri_or_filename: str) -> str:
165
+ """
166
+ Normalize a URI or filename to ensure it's a valid URI.
167
+ Converts simple filenames to file:// URIs if needed.
168
+
169
+ Args:
170
+ uri_or_filename: A URI string or simple filename
171
+
172
+ Returns:
173
+ A properly formatted URI string
174
+ """
175
+ if not uri_or_filename:
176
+ return ""
177
+
178
+ # Check if it's already a valid URI with a scheme
179
+ if "://" in uri_or_filename:
180
+ return uri_or_filename
181
+
182
+ # Handle Windows-style paths with backslashes
183
+ normalized_path = uri_or_filename.replace("\\", "/")
184
+
185
+ # If it's a simple filename or relative path, convert to file:// URI
186
+ # Make sure it has three slashes for an absolute path
187
+ if normalized_path.startswith("/"):
188
+ return f"file://{normalized_path}"
189
+ else:
190
+ return f"file:///{normalized_path}"
191
+
192
+
193
+ def extract_title_from_uri(uri: AnyUrl) -> str:
194
+ """Extract a readable title from a URI."""
195
+ # Simple attempt to get filename from path
196
+ uri_str = uri._url
197
+ try:
198
+ # For HTTP(S) URLs
199
+ if uri.scheme in ("http", "https"):
200
+ # Get the last part of the path
201
+ path_parts = uri.path.split("/")
202
+ filename = next((p for p in reversed(path_parts) if p), "")
203
+ return filename if filename else uri_str
204
+
205
+ # For file URLs or other schemes
206
+ elif uri.path:
207
+ import os.path
208
+
209
+ return os.path.basename(uri.path)
210
+
211
+ except Exception:
212
+ pass
213
+
214
+ # Fallback to the full URI if parsing fails
215
+ return uri_str