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.
- rem/__init__.py +129 -0
- rem/agentic/README.md +760 -0
- rem/agentic/__init__.py +54 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +38 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +425 -0
- rem/agentic/context_builder.py +360 -0
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +273 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +240 -0
- rem/agentic/providers/phoenix.py +926 -0
- rem/agentic/providers/pydantic_ai.py +854 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +737 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +242 -0
- rem/api/README.md +657 -0
- rem/api/deps.py +253 -0
- rem/api/main.py +460 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +820 -0
- rem/api/mcp_router/server.py +243 -0
- rem/api/mcp_router/tools.py +1605 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +520 -0
- rem/api/routers/auth.py +898 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +702 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +202 -0
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +546 -0
- rem/api/routers/chat/streaming.py +950 -0
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +87 -0
- rem/api/routers/feedback.py +276 -0
- rem/api/routers/messages.py +620 -0
- rem/api/routers/models.py +86 -0
- rem/api/routers/query.py +362 -0
- rem/api/routers/shared_sessions.py +422 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +36 -0
- rem/auth/jwt.py +367 -0
- rem/auth/middleware.py +318 -0
- rem/auth/providers/__init__.py +16 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/email.py +215 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +517 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +299 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +549 -0
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +495 -0
- rem/cli/commands/db.py +828 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1698 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +388 -0
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +230 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/commands/session.py +453 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +123 -0
- rem/config.py +244 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +70 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +672 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +246 -0
- rem/models/entities/__init__.py +68 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +64 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +181 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/session.py +84 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +93 -0
- rem/py.typed +0 -0
- rem/registry.py +373 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +132 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +18 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +760 -0
- rem/services/content/service.py +762 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +322 -0
- rem/services/dreaming/moment_service.py +251 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +127 -0
- rem/services/embeddings/worker.py +435 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +960 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +757 -0
- rem/services/postgres/__init__.py +49 -0
- rem/services/postgres/diff_service.py +599 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
- rem/services/postgres/register_type.py +353 -0
- rem/services/postgres/repository.py +481 -0
- rem/services/postgres/schema_generator.py +661 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +355 -0
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +318 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +180 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +608 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +13 -0
- rem/services/session/compression.py +488 -0
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +85 -0
- rem/services/user_service.py +130 -0
- rem/settings.py +1877 -0
- rem/sql/background_indexes.sql +52 -0
- rem/sql/migrations/001_install.sql +983 -0
- rem/sql/migrations/002_install_models.sql +3157 -0
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +282 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +628 -0
- rem/utils/__init__.py +61 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +436 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/files.py +323 -0
- rem/utils/markdown.py +16 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +492 -0
- rem/utils/schema_loader.py +649 -0
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +350 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +325 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +7 -0
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- rem/workers/unlogged_maintainer.py +463 -0
- remdb-0.3.242.dist-info/METADATA +1632 -0
- remdb-0.3.242.dist-info/RECORD +235 -0
- remdb-0.3.242.dist-info/WHEEL +4 -0
- 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,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}")
|