remdb 0.3.242__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 remdb might be problematic. Click here for more details.

Files changed (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. remdb-0.3.242.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,273 @@
1
+ """
2
+ MCP Tool Wrappers for Pydantic AI.
3
+
4
+ This module provides functions to convert MCP tool functions and resources
5
+ into a format compatible with the Pydantic AI library.
6
+ """
7
+
8
+ from typing import Any, Callable
9
+
10
+ from loguru import logger
11
+ from pydantic_ai.tools import Tool
12
+
13
+
14
+ def create_pydantic_tool(func: Callable[..., Any]) -> Tool:
15
+ """
16
+ Create a Pydantic AI Tool from a given function.
17
+
18
+ This uses the Tool constructor, which inspects the
19
+ function's signature and docstring to create the tool schema.
20
+
21
+ Args:
22
+ func: The function to wrap as a tool.
23
+
24
+ Returns:
25
+ A Pydantic AI Tool instance.
26
+ """
27
+ logger.debug(f"Creating Pydantic tool from function: {func.__name__}")
28
+ return Tool(func)
29
+
30
+
31
+ def create_mcp_tool_wrapper(
32
+ tool_name: str,
33
+ mcp_tool: Any,
34
+ user_id: str | None = None,
35
+ description_suffix: str | None = None,
36
+ ) -> Tool:
37
+ """
38
+ Create a Pydantic AI Tool from a FastMCP FunctionTool.
39
+
40
+ FastMCP tools are FunctionTool objects that wrap the actual async function.
41
+ We pass the function directly to Pydantic AI's Tool class, which will
42
+ inspect its signature properly. User ID injection happens in the wrapper.
43
+
44
+ Args:
45
+ tool_name: Name of the MCP tool
46
+ mcp_tool: The FastMCP FunctionTool object
47
+ user_id: Optional user_id to inject into tool calls
48
+ description_suffix: Optional text to append to the tool's docstring.
49
+ Used to add schema-specific context (e.g., default table for search_rem).
50
+
51
+ Returns:
52
+ A Pydantic AI Tool instance
53
+ """
54
+ # Extract the actual function from FastMCP FunctionTool
55
+ tool_func = mcp_tool.fn
56
+
57
+ # Check if function accepts user_id parameter
58
+ import inspect
59
+ sig = inspect.signature(tool_func)
60
+ has_user_id = "user_id" in sig.parameters
61
+
62
+ # Build the docstring with optional suffix
63
+ base_doc = tool_func.__doc__ or ""
64
+ final_doc = base_doc + description_suffix if description_suffix else base_doc
65
+
66
+ # If we need to inject user_id or modify docstring, create a wrapper
67
+ # Otherwise, use the function directly for better signature preservation
68
+ if user_id and has_user_id:
69
+ async def wrapped_tool(**kwargs) -> Any:
70
+ """Wrapper that injects user_id."""
71
+ if "user_id" not in kwargs:
72
+ kwargs["user_id"] = user_id
73
+ logger.debug(f"Injecting user_id={user_id} into tool {tool_name}")
74
+
75
+ # Filter kwargs to only include parameters that the function accepts
76
+ valid_params = set(sig.parameters.keys())
77
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_params}
78
+
79
+ return await tool_func(**filtered_kwargs)
80
+
81
+ # Copy signature from original function for Pydantic AI inspection
82
+ wrapped_tool.__name__ = tool_name
83
+ wrapped_tool.__doc__ = final_doc
84
+ wrapped_tool.__annotations__ = tool_func.__annotations__
85
+ wrapped_tool.__signature__ = sig # Important: preserve full signature
86
+
87
+ logger.debug(f"Creating MCP tool wrapper with user_id injection: {tool_name}")
88
+ return Tool(wrapped_tool)
89
+ elif description_suffix:
90
+ # Need to wrap just for docstring modification
91
+ async def wrapped_tool(**kwargs) -> Any:
92
+ """Wrapper for docstring modification."""
93
+ valid_params = set(sig.parameters.keys())
94
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_params}
95
+ return await tool_func(**filtered_kwargs)
96
+
97
+ wrapped_tool.__name__ = tool_name
98
+ wrapped_tool.__doc__ = final_doc
99
+ wrapped_tool.__annotations__ = tool_func.__annotations__
100
+ wrapped_tool.__signature__ = sig
101
+
102
+ logger.debug(f"Creating MCP tool wrapper with description suffix: {tool_name}")
103
+ return Tool(wrapped_tool)
104
+ else:
105
+ # No injection needed - use original function directly
106
+ logger.debug(f"Creating MCP tool wrapper (no injection): {tool_name}")
107
+ return Tool(tool_func)
108
+
109
+
110
+ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> Tool:
111
+ """
112
+ Build a Tool instance from an MCP resource URI.
113
+
114
+ Creates a tool that fetches the resource content when called.
115
+ Resources declared in agent YAML become callable tools - this eliminates
116
+ the artificial MCP distinction between tools and resources.
117
+
118
+ Supports both:
119
+ - Concrete URIs: "rem://agents" -> tool with no parameters
120
+ - Template URIs: "patient-profile://field/{field_key}" -> tool with field_key parameter
121
+
122
+ Args:
123
+ uri: The resource URI (concrete or template with {variable} placeholders).
124
+ usage: The description of what this resource provides.
125
+ mcp_server: Optional FastMCP server instance to resolve resources from.
126
+ If provided, resources are resolved from this server's registry.
127
+ If not provided, falls back to REM's built-in load_resource().
128
+
129
+ Returns:
130
+ A Pydantic AI Tool instance that fetches the resource.
131
+
132
+ Example:
133
+ # Concrete URI -> no-param tool
134
+ tool = create_resource_tool("rem://agents", "List all agent schemas")
135
+
136
+ # Template URI -> parameterized tool
137
+ tool = create_resource_tool("patient-profile://field/{field_key}", "Get field definition", mcp_server=mcp)
138
+ # Agent calls: get_patient_profile_field(field_key="safety.suicidality")
139
+ """
140
+ import json
141
+ import re
142
+
143
+ # Extract template variables from URI (e.g., {field_key}, {domain_name})
144
+ template_vars = re.findall(r'\{([^}]+)\}', uri)
145
+
146
+ # Parse URI to create function name (strip template vars for cleaner name)
147
+ clean_uri = re.sub(r'\{[^}]+\}', '', uri)
148
+ parts = clean_uri.replace("://", "_").replace("-", "_").replace("/", "_").replace(".", "_")
149
+ parts = re.sub(r'_+', '_', parts).strip('_') # Clean up multiple underscores
150
+ func_name = f"get_{parts}"
151
+
152
+ # For parameterized URIs, append _by_{params} to avoid naming conflicts
153
+ # e.g., rem://agents/{name} -> get_rem_agents_by_name (distinct from get_rem_agents)
154
+ if template_vars:
155
+ param_suffix = "_by_" + "_".join(template_vars)
156
+ func_name = f"{func_name}{param_suffix}"
157
+
158
+ # Build description including parameter info
159
+ description = usage or f"Fetch {uri} resource"
160
+ if template_vars:
161
+ param_desc = ", ".join(template_vars)
162
+ description = f"{description}\n\nParameters: {param_desc}"
163
+
164
+ # Capture mcp_server reference at tool creation time (for closure)
165
+ # This ensures the correct server is used even if called later
166
+ _captured_mcp_server = mcp_server
167
+ _captured_uri = uri # Also capture URI for consistent logging
168
+
169
+ if template_vars:
170
+ # Template URI -> create parameterized tool
171
+ async def wrapper(**kwargs: Any) -> str:
172
+ """Fetch MCP resource with substituted parameters."""
173
+ import asyncio
174
+ import inspect
175
+
176
+ logger.debug(f"Resource tool invoked: uri={_captured_uri}, kwargs={kwargs}, mcp_server={'set' if _captured_mcp_server else 'None'}")
177
+
178
+ # Try to resolve from MCP server's resource templates first
179
+ if _captured_mcp_server is not None:
180
+ try:
181
+ # Get resource templates from MCP server
182
+ templates = await _captured_mcp_server.get_resource_templates()
183
+ logger.debug(f"MCP server templates: {list(templates.keys())}")
184
+ if _captured_uri in templates:
185
+ template = templates[_captured_uri]
186
+ logger.debug(f"Found template for {_captured_uri}, calling fn with kwargs={kwargs}")
187
+ # Call the template's underlying function directly
188
+ # The fn expects the template variables as kwargs
189
+ fn_result = template.fn(**kwargs)
190
+ # Handle both sync and async functions
191
+ if inspect.iscoroutine(fn_result):
192
+ fn_result = await fn_result
193
+ if isinstance(fn_result, str):
194
+ return fn_result
195
+ return json.dumps(fn_result, indent=2)
196
+ else:
197
+ logger.warning(f"Template {_captured_uri} not found in MCP server templates: {list(templates.keys())}")
198
+ except Exception as e:
199
+ logger.warning(f"Failed to resolve resource {_captured_uri} from MCP server: {e}", exc_info=True)
200
+ else:
201
+ logger.warning(f"No MCP server provided for resource tool {_captured_uri}, using fallback")
202
+
203
+ # Fallback: substitute template variables and use load_resource
204
+ resolved_uri = _captured_uri
205
+ for var in template_vars:
206
+ if var in kwargs:
207
+ resolved_uri = resolved_uri.replace(f"{{{var}}}", str(kwargs[var]))
208
+ else:
209
+ return json.dumps({"error": f"Missing required parameter: {var}"})
210
+
211
+ logger.debug(f"Using fallback load_resource for resolved URI: {resolved_uri}")
212
+ from rem.api.mcp_router.resources import load_resource
213
+ result = await load_resource(resolved_uri)
214
+ if isinstance(result, str):
215
+ return result
216
+ return json.dumps(result, indent=2)
217
+
218
+ # Build parameter annotations for Pydantic AI
219
+ wrapper.__name__ = func_name
220
+ wrapper.__doc__ = description
221
+ # Add type hints for parameters
222
+ wrapper.__annotations__ = {var: str for var in template_vars}
223
+ wrapper.__annotations__['return'] = str
224
+
225
+ logger.info(f"Built parameterized resource tool: {func_name} (uri: {uri}, params: {template_vars}, mcp_server={'provided' if mcp_server else 'None'})")
226
+ else:
227
+ # Concrete URI -> no-param tool
228
+ async def wrapper(**kwargs: Any) -> str:
229
+ """Fetch MCP resource and return contents."""
230
+ import asyncio
231
+ import inspect
232
+
233
+ if kwargs:
234
+ logger.warning(f"Resource tool {func_name} called with unexpected kwargs: {list(kwargs.keys())}")
235
+
236
+ logger.debug(f"Concrete resource tool invoked: uri={_captured_uri}, mcp_server={'set' if _captured_mcp_server else 'None'}")
237
+
238
+ # Try to resolve from MCP server's resources first
239
+ if _captured_mcp_server is not None:
240
+ try:
241
+ resources = await _captured_mcp_server.get_resources()
242
+ logger.debug(f"MCP server resources: {list(resources.keys())}")
243
+ if _captured_uri in resources:
244
+ resource = resources[_captured_uri]
245
+ logger.debug(f"Found resource for {_captured_uri}")
246
+ # Call the resource's underlying function
247
+ fn_result = resource.fn()
248
+ if inspect.iscoroutine(fn_result):
249
+ fn_result = await fn_result
250
+ if isinstance(fn_result, str):
251
+ return fn_result
252
+ return json.dumps(fn_result, indent=2)
253
+ else:
254
+ logger.warning(f"Resource {_captured_uri} not found in MCP server resources: {list(resources.keys())}")
255
+ except Exception as e:
256
+ logger.warning(f"Failed to resolve resource {_captured_uri} from MCP server: {e}", exc_info=True)
257
+ else:
258
+ logger.warning(f"No MCP server provided for resource tool {_captured_uri}, using fallback")
259
+
260
+ # Fallback to load_resource
261
+ logger.debug(f"Using fallback load_resource for URI: {_captured_uri}")
262
+ from rem.api.mcp_router.resources import load_resource
263
+ result = await load_resource(_captured_uri)
264
+ if isinstance(result, str):
265
+ return result
266
+ return json.dumps(result, indent=2)
267
+
268
+ wrapper.__name__ = func_name
269
+ wrapper.__doc__ = description
270
+
271
+ logger.info(f"Built resource tool: {func_name} (uri: {uri}, mcp_server={'provided' if mcp_server else 'None'})")
272
+
273
+ return Tool(wrapper)
@@ -0,0 +1,5 @@
1
+ """OpenTelemetry instrumentation for REM agents."""
2
+
3
+ from .setup import setup_instrumentation, set_agent_resource_attributes
4
+
5
+ __all__ = ["setup_instrumentation", "set_agent_resource_attributes"]
@@ -0,0 +1,240 @@
1
+ """
2
+ OpenTelemetry instrumentation setup for REM agents.
3
+
4
+ Provides:
5
+ - OTLP exporter configuration
6
+ - Phoenix integration (OpenInference conventions)
7
+ - Resource attributes for agent metadata
8
+ - Idempotent setup (safe to call multiple times)
9
+ """
10
+
11
+ from typing import Any
12
+
13
+ from loguru import logger
14
+
15
+ from ...settings import settings
16
+
17
+
18
+ # Global flag to track if instrumentation is initialized
19
+ _instrumentation_initialized = False
20
+
21
+
22
+ def setup_instrumentation() -> None:
23
+ """
24
+ Initialize OpenTelemetry instrumentation for REM agents.
25
+
26
+ Idempotent - safe to call multiple times, only initializes once.
27
+
28
+ Configures:
29
+ - OTLP exporter (HTTP or gRPC)
30
+ - Phoenix integration if enabled
31
+ - Pydantic AI instrumentation (automatic via agent.instrument=True)
32
+ - Resource attributes (service name, environment, etc.)
33
+
34
+ Environment variables:
35
+ OTEL__ENABLED - Enable instrumentation (default: false)
36
+ OTEL__SERVICE_NAME - Service name (default: rem-api)
37
+ OTEL__COLLECTOR_ENDPOINT - OTLP endpoint (default: http://localhost:4318)
38
+ OTEL__PROTOCOL - Protocol (http or grpc, default: http)
39
+ PHOENIX__ENABLED - Enable Phoenix (default: false)
40
+ PHOENIX__COLLECTOR_ENDPOINT - Phoenix endpoint (default: http://localhost:6006/v1/traces)
41
+ """
42
+ global _instrumentation_initialized
43
+
44
+ if _instrumentation_initialized:
45
+ logger.debug("OTEL instrumentation already initialized, skipping")
46
+ return
47
+
48
+ if not settings.otel.enabled:
49
+ logger.debug("OTEL instrumentation disabled (OTEL__ENABLED=false)")
50
+ return
51
+
52
+ logger.info("Initializing OpenTelemetry instrumentation...")
53
+
54
+ try:
55
+ from opentelemetry import trace
56
+ from opentelemetry.sdk.trace import TracerProvider, ReadableSpan
57
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter, SpanExportResult
58
+ from opentelemetry.sdk.resources import Resource, SERVICE_NAME, DEPLOYMENT_ENVIRONMENT
59
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPExporter
60
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GRPCExporter
61
+
62
+ class SanitizingSpanExporter(SpanExporter):
63
+ """
64
+ Wrapper exporter that sanitizes span attributes before export.
65
+
66
+ Removes None values that cause OTLP encoding failures like:
67
+ - llm.input_messages.3.message.content: None
68
+ """
69
+
70
+ def __init__(self, wrapped_exporter: SpanExporter):
71
+ self._wrapped = wrapped_exporter
72
+
73
+ def _sanitize_value(self, value):
74
+ """Recursively sanitize a value, replacing None with empty string."""
75
+ if value is None:
76
+ return "" # Replace None with empty string
77
+ if isinstance(value, dict):
78
+ return {k: self._sanitize_value(v) for k, v in value.items()}
79
+ if isinstance(value, (list, tuple)):
80
+ return [self._sanitize_value(v) for v in value]
81
+ return value
82
+
83
+ def export(self, spans: tuple[ReadableSpan, ...]) -> SpanExportResult:
84
+ # Create sanitized copies of spans
85
+ sanitized_spans = []
86
+ for span in spans:
87
+ if span.attributes:
88
+ # Sanitize all attribute values - replace None with empty string
89
+ sanitized_attrs = {}
90
+ for k, v in span.attributes.items():
91
+ sanitized_attrs[k] = self._sanitize_value(v)
92
+ sanitized_spans.append(_SanitizedSpan(span, sanitized_attrs))
93
+ else:
94
+ sanitized_spans.append(span)
95
+
96
+ return self._wrapped.export(tuple(sanitized_spans))
97
+
98
+ def shutdown(self) -> None:
99
+ self._wrapped.shutdown()
100
+
101
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
102
+ return self._wrapped.force_flush(timeout_millis)
103
+
104
+ class _SanitizedSpan(ReadableSpan):
105
+ """ReadableSpan wrapper with sanitized attributes."""
106
+
107
+ def __init__(self, original: ReadableSpan, sanitized_attributes: dict):
108
+ self._original = original
109
+ self._sanitized_attributes = sanitized_attributes
110
+
111
+ @property
112
+ def name(self): return self._original.name
113
+ @property
114
+ def context(self): return self._original.context
115
+ @property
116
+ def parent(self): return self._original.parent
117
+ @property
118
+ def resource(self): return self._original.resource
119
+ @property
120
+ def instrumentation_scope(self): return self._original.instrumentation_scope
121
+ @property
122
+ def status(self): return self._original.status
123
+ @property
124
+ def start_time(self): return self._original.start_time
125
+ @property
126
+ def end_time(self): return self._original.end_time
127
+ @property
128
+ def links(self): return self._original.links
129
+ @property
130
+ def events(self): return self._original.events
131
+ @property
132
+ def kind(self): return self._original.kind
133
+ @property
134
+ def attributes(self): return self._sanitized_attributes
135
+ @property
136
+ def dropped_attributes(self): return self._original.dropped_attributes
137
+ @property
138
+ def dropped_events(self): return self._original.dropped_events
139
+ @property
140
+ def dropped_links(self): return self._original.dropped_links
141
+
142
+ def get_span_context(self): return self._original.get_span_context()
143
+
144
+ # Create resource with service metadata
145
+ resource = Resource(
146
+ attributes={
147
+ SERVICE_NAME: settings.otel.service_name,
148
+ DEPLOYMENT_ENVIRONMENT: settings.environment,
149
+ "service.team": settings.team,
150
+ }
151
+ )
152
+
153
+ # Create tracer provider
154
+ tracer_provider = TracerProvider(resource=resource)
155
+
156
+ # Configure OTLP exporter based on protocol
157
+ if settings.otel.protocol == "grpc":
158
+ base_exporter = GRPCExporter(
159
+ endpoint=settings.otel.collector_endpoint,
160
+ timeout=settings.otel.export_timeout,
161
+ insecure=settings.otel.insecure,
162
+ )
163
+ else: # http
164
+ base_exporter = HTTPExporter(
165
+ endpoint=f"{settings.otel.collector_endpoint}/v1/traces",
166
+ timeout=settings.otel.export_timeout,
167
+ )
168
+
169
+ # Wrap with sanitizing exporter to handle None values
170
+ exporter = SanitizingSpanExporter(base_exporter)
171
+
172
+ # Add span processor
173
+ tracer_provider.add_span_processor(BatchSpanProcessor(exporter))
174
+
175
+ # Set as global tracer provider
176
+ trace.set_tracer_provider(tracer_provider)
177
+
178
+ logger.info(
179
+ f"OTLP exporter configured: {settings.otel.collector_endpoint} ({settings.otel.protocol})"
180
+ )
181
+
182
+ # Add OpenInference span processor for Pydantic AI
183
+ # This adds rich attributes (openinference.span.kind, input/output, etc.) to ALL traces
184
+ # Phoenix receives these traces via the OTLP collector - no separate "Phoenix integration" needed
185
+ # Note: The OTEL exporter may log warnings about None values in tool call messages,
186
+ # but this is a known limitation in openinference-instrumentation-pydantic-ai
187
+ try:
188
+ from openinference.instrumentation.pydantic_ai import OpenInferenceSpanProcessor as PydanticAISpanProcessor
189
+
190
+ tracer_provider.add_span_processor(PydanticAISpanProcessor())
191
+ logger.info("Added OpenInference span processor for Pydantic AI")
192
+
193
+ except ImportError:
194
+ logger.warning(
195
+ "openinference-instrumentation-pydantic-ai not installed - traces will lack OpenInference attributes. "
196
+ "Install with: pip install openinference-instrumentation-pydantic-ai"
197
+ )
198
+
199
+ _instrumentation_initialized = True
200
+ logger.info("OpenTelemetry instrumentation initialized successfully")
201
+
202
+ except Exception as e:
203
+ logger.error(f"Failed to initialize OTEL instrumentation: {e}")
204
+ # Don't raise - allow application to continue without tracing
205
+
206
+
207
+ def set_agent_resource_attributes(agent_schema: dict[str, Any] | None = None) -> None:
208
+ """
209
+ Set resource attributes for agent execution.
210
+
211
+ Called before creating agent to set span attributes with agent metadata.
212
+
213
+ Args:
214
+ agent_schema: Agent schema with metadata (kind, name, version, etc.)
215
+ """
216
+ if not settings.otel.enabled or not agent_schema:
217
+ return
218
+
219
+ try:
220
+ from opentelemetry import trace
221
+
222
+ # Get current span and set attributes
223
+ span = trace.get_current_span()
224
+ if span.is_recording():
225
+ json_extra = agent_schema.get("json_schema_extra", {})
226
+ kind = json_extra.get("kind")
227
+ name = json_extra.get("name")
228
+ version = json_extra.get("version", "unknown")
229
+
230
+ if kind:
231
+ span.set_attribute("agent.kind", kind)
232
+ if name:
233
+ span.set_attribute("agent.name", name)
234
+ if version:
235
+ span.set_attribute("agent.version", version)
236
+
237
+ logger.debug(f"Set agent resource attributes: kind={kind}, name={name}, version={version}")
238
+
239
+ except Exception as e:
240
+ logger.warning(f"Failed to set agent resource attributes: {e}")