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,854 @@
1
+ """
2
+ Pydantic AI agent factory with dynamic JsonSchema to Pydantic model conversion.
3
+
4
+ AgentRuntime Pattern:
5
+ The create_agent() factory returns an AgentRuntime object containing:
6
+ - agent: The Pydantic AI Agent instance
7
+ - temperature: Resolved temperature (schema override or settings default)
8
+ - max_iterations: Resolved max iterations (schema override or settings default)
9
+
10
+ This ensures runtime configuration is determined once at agent creation,
11
+ not re-computed at every call site.
12
+
13
+ Known Issues:
14
+ 1. Cerebras Qwen Strict Mode Incompatibility
15
+ - Cerebras qwen-3-32b requires additionalProperties=false for all object fields
16
+ - Cannot use dict[str, Any] for flexible parameters (breaks Qwen compatibility)
17
+ - Cannot use minimum/maximum constraints on number fields (Qwen rejects these)
18
+ - Workaround: Use cerebras:llama-3.3-70b instead (fully compatible)
19
+ - Future fix: Redesign REM agent to use discriminated union instead of dict
20
+
21
+ Key Design Pattern:
22
+ 1. JsonSchema → Pydantic Model (json-schema-to-pydantic library)
23
+ 2. Agent schema contains both system prompt AND output schema
24
+ 3. MCP tools loaded dynamically from schema metadata
25
+ 4. Result type can be stripped of description to avoid duplication with system prompt
26
+ 5. OTEL instrumentation conditional based on settings
27
+
28
+ Unique Design:
29
+ - Agent schemas are JSON Schema with embedded metadata:
30
+ - description: System prompt for agent
31
+ - properties: Output schema fields
32
+ - json_schema_extra.tools: MCP tool configurations
33
+ - json_schema_extra.resources: MCP resource configurations
34
+ - Dynamic model creation from schema using json-schema-to-pydantic
35
+ - Tools and resources loaded from MCP servers via schema config
36
+ - Stripped descriptions to avoid LLM schema bloat
37
+
38
+ TODO:
39
+ Model Cache Implementation (Critical for Production Scale)
40
+ Current bottleneck: Every agent.run() call creates a new Agent instance with
41
+ model initialization overhead. At scale (100+ requests/sec), this becomes expensive.
42
+
43
+ Need two-tier caching strategy:
44
+
45
+ 1. Schema Cache (see rem/utils/schema_loader.py TODO):
46
+ - Filesystem schemas: LRU cache, no TTL (immutable)
47
+ - Database schemas: TTL cache (5-15 min)
48
+ - Reduces disk I/O and DB queries
49
+
50
+ 2. Model Instance Cache (THIS TODO):
51
+ - Cache Pydantic AI Model() instances (connection pools, tokenizers)
52
+ - Key: (provider, model_name) → Model instance
53
+ - Benefits:
54
+ * Reuse HTTP connection pools (httpx.AsyncClient)
55
+ * Reuse tokenizer instances
56
+ * Faster model initialization
57
+ * Lower memory footprint
58
+ - Implementation:
59
+ ```python
60
+ _model_cache: dict[tuple[str, str], Model] = {}
61
+
62
+ def get_or_create_model(model_name: str) -> Model:
63
+ cache_key = _parse_model_name(model_name) # ("anthropic", "claude-3-5-sonnet")
64
+ if cache_key not in _model_cache:
65
+ _model_cache[cache_key] = Model(model_name)
66
+ return _model_cache[cache_key]
67
+ ```
68
+ - Considerations:
69
+ * Max cache size (LRU eviction, e.g., 20 models)
70
+ * Thread safety (asyncio.Lock for cache access)
71
+ * Model warmup on server startup for hot paths
72
+ * Clear cache on model config changes
73
+
74
+ 3. Agent Instance Caching (Advanced):
75
+ - Cache complete Agent instances (model + schema + tools)
76
+ - Key: (schema_name, model_name) → Agent instance
77
+ - Benefits:
78
+ * Skip schema parsing and model creation entirely
79
+ * Fastest possible agent.run() latency
80
+ - Challenges:
81
+ * Agent state management (stateless required)
82
+ * Tool/resource updates (cache invalidation)
83
+ * Memory usage (agents are heavier than models)
84
+ - Recommendation: Start with Model cache, add Agent cache if profiling shows benefit
85
+
86
+ Profiling Targets (measure before optimizing):
87
+ - schema_loader.load_agent_schema() calls per request
88
+ - create_agent() execution time (model init overhead)
89
+ - Model() instance creation time by provider
90
+ - Agent.run() total latency breakdown
91
+
92
+ Related Files:
93
+ - rem/utils/schema_loader.py (schema caching TODO)
94
+ - rem/agentic/providers/pydantic_ai.py:339 (create_agent - this file)
95
+ - rem/services/schema_repository.py (database schema loading)
96
+
97
+ Priority: HIGH (blocks production scaling beyond 50 req/sec)
98
+
99
+ 4. Response Format Control (structured_output enhancement):
100
+ - Current: structured_output is bool (True=strict schema, False=free-form text)
101
+ - Missing: OpenAI JSON mode (valid JSON without strict schema enforcement)
102
+ - Missing: Completions API support (some models only support completions, not chat)
103
+
104
+ Proposed schema field values for `structured_output`:
105
+ - True (default): Strict structured output using provider's native schema support
106
+ - False: Free-form text response (properties converted to prompt guidance)
107
+ - "json": JSON mode - ensures valid JSON but no schema enforcement
108
+ (OpenAI: response_format={"type": "json_object"})
109
+ - "text": Explicit free-form text (alias for False)
110
+
111
+ Implementation:
112
+ a) Update AgentSchemaMetadata.structured_output type:
113
+ structured_output: bool | Literal["json", "text"] = True
114
+ b) In create_agent(), handle each mode:
115
+ - True: Use output_type with Pydantic model (current behavior)
116
+ - False/"text": Convert properties to prompt guidance (current behavior)
117
+ - "json": Use provider's JSON mode without strict schema
118
+ c) Provider-specific JSON mode:
119
+ - OpenAI: model_settings={"response_format": {"type": "json_object"}}
120
+ - Anthropic: Not supported natively, use prompt guidance
121
+ - Others: Fallback to prompt guidance with JSON instruction
122
+
123
+ Related: Some providers (Cerebras) have completions-only models where
124
+ structured output isn't available. Consider model capability detection.
125
+
126
+ Priority: MEDIUM (enables more flexible output control)
127
+
128
+ Example Agent Schema:
129
+ {
130
+ "type": "object",
131
+ "description": "Agent that answers REM queries...",
132
+ "properties": {
133
+ "answer": {"type": "string", "description": "Query answer"},
134
+ "confidence": {"type": "number"}
135
+ },
136
+ "required": ["answer", "confidence"],
137
+ "json_schema_extra": {
138
+ "kind": "agent",
139
+ "name": "query-agent",
140
+ "tools": [
141
+ {"name": "search_knowledge_base", "mcp_server": "rem"}
142
+ ],
143
+ "resources": [
144
+ {"uri_pattern": "cda://.*", "mcp_server": "rem"}
145
+ ]
146
+ }
147
+ }
148
+ """
149
+
150
+ from typing import Any
151
+
152
+ from loguru import logger
153
+ from pydantic import BaseModel
154
+ from pydantic_ai import Agent
155
+ from pydantic_ai.models import KnownModelName, Model
156
+
157
+ try:
158
+ from json_schema_to_pydantic import PydanticModelBuilder
159
+
160
+ JSON_SCHEMA_TO_PYDANTIC_AVAILABLE = True
161
+ except ImportError:
162
+ JSON_SCHEMA_TO_PYDANTIC_AVAILABLE = False
163
+ logger.warning(
164
+ "json-schema-to-pydantic not installed. "
165
+ "Install with: pip install 'rem[schema]' or pip install json-schema-to-pydantic"
166
+ )
167
+
168
+ from ..context import AgentContext
169
+ from ...settings import settings
170
+
171
+
172
+ class AgentRuntime:
173
+ """
174
+ Agent runtime configuration bundle with delegation pattern.
175
+
176
+ Contains the agent instance and its resolved runtime parameters
177
+ (temperature, max_iterations) determined from schema overrides + settings defaults.
178
+
179
+ Delegates run() and iter() calls to the inner agent with automatic UsageLimits.
180
+ This allows callers to use AgentRuntime as a drop-in replacement for Agent.
181
+ """
182
+
183
+ def __init__(self, agent: Agent[None, Any], temperature: float, max_iterations: int):
184
+ self.agent = agent
185
+ self.temperature = temperature
186
+ self.max_iterations = max_iterations
187
+
188
+ async def run(self, *args, **kwargs):
189
+ """Delegate to agent.run() with automatic UsageLimits."""
190
+ from pydantic_ai import UsageLimits
191
+
192
+ # Only apply usage_limits if not already provided
193
+ if "usage_limits" not in kwargs:
194
+ kwargs["usage_limits"] = UsageLimits(request_limit=self.max_iterations)
195
+ return await self.agent.run(*args, **kwargs)
196
+
197
+ def iter(self, *args, **kwargs):
198
+ """Delegate to agent.iter() with automatic UsageLimits."""
199
+ from pydantic_ai import UsageLimits
200
+
201
+ # Only apply usage_limits if not already provided
202
+ if "usage_limits" not in kwargs:
203
+ kwargs["usage_limits"] = UsageLimits(request_limit=self.max_iterations)
204
+ return self.agent.iter(*args, **kwargs)
205
+
206
+
207
+ def _get_builtin_tools() -> list:
208
+ """
209
+ Get built-in tools that are always available to agents.
210
+
211
+ Currently returns empty list - all tools come from MCP servers.
212
+ The register_metadata tool is available via the REM MCP server and
213
+ agents can opt-in by configuring mcp_servers in their schema.
214
+
215
+ Returns:
216
+ List of Pydantic AI tool functions (currently empty)
217
+ """
218
+ # NOTE: register_metadata is now an MCP tool, not a built-in.
219
+ # Agents that want it should configure mcp_servers to load from rem.mcp_server.
220
+ # This allows agents to choose which tools they need.
221
+ return []
222
+
223
+
224
+ def _create_model_from_schema(agent_schema: dict[str, Any]) -> type[BaseModel]:
225
+ """
226
+ Create Pydantic model dynamically from JSON Schema.
227
+
228
+ Uses json-schema-to-pydantic library for robust conversion of:
229
+ - Nested objects
230
+ - Arrays
231
+ - Required fields
232
+ - Validation constraints
233
+
234
+ Args:
235
+ agent_schema: JSON Schema dict with agent output structure
236
+
237
+ Returns:
238
+ Dynamically created Pydantic BaseModel class
239
+
240
+ Example:
241
+ schema = {
242
+ "type": "object",
243
+ "properties": {
244
+ "answer": {"type": "string"},
245
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1}
246
+ },
247
+ "required": ["answer", "confidence"]
248
+ }
249
+ Model = _create_model_from_schema(schema)
250
+ # Model is now a Pydantic class with answer: str and confidence: float fields
251
+ """
252
+ if not JSON_SCHEMA_TO_PYDANTIC_AVAILABLE:
253
+ raise ImportError(
254
+ "json-schema-to-pydantic is required for dynamic schema conversion. "
255
+ "Install with: pip install 'rem[schema]' or pip install json-schema-to-pydantic"
256
+ )
257
+
258
+ # Create Pydantic model from JSON Schema
259
+ builder = PydanticModelBuilder()
260
+ model = builder.create_pydantic_model(agent_schema, root_schema=agent_schema)
261
+
262
+ # Override model name with schema name if available
263
+ json_extra = agent_schema.get("json_schema_extra", {})
264
+ schema_name = json_extra.get("name")
265
+ if schema_name:
266
+ # Convert kebab-case to PascalCase for class name
267
+ class_name = "".join(word.capitalize() for word in schema_name.split("-"))
268
+ model.__name__ = class_name
269
+ model.__qualname__ = class_name
270
+
271
+ logger.debug(
272
+ f"Created Pydantic model '{model.__name__}' from JSON Schema with fields: "
273
+ f"{list(model.model_fields.keys())}"
274
+ )
275
+
276
+ return model
277
+
278
+
279
+ def _prepare_schema_for_qwen(schema: dict[str, Any]) -> dict[str, Any]:
280
+ """
281
+ Prepare JSON schema for Cerebras Qwen strict mode compatibility.
282
+
283
+ Cerebras Qwen strict mode requirements:
284
+ 1. additionalProperties MUST be false (this is mandatory in strict mode)
285
+ 2. All object types must have explicit properties field
286
+ 3. Cannot use minimum/maximum constraints (Pydantic ge/le works fine)
287
+
288
+ This function transforms schemas to meet these requirements:
289
+ - Changes additionalProperties from true to false
290
+ - Adds empty properties {} to objects that don't have it
291
+ - Preserves all other schema features
292
+
293
+ IMPORTANT: This breaks dict[str, Any] flexibility!
294
+ - dict[str, Any] generates {"type": "object", "additionalProperties": true}
295
+ - Qwen requires additionalProperties: false
296
+ - Result: Empty dict {} becomes the only valid value
297
+
298
+ Recommendation: Don't use dict[str, Any] with Qwen. Use explicit Pydantic models instead.
299
+
300
+ Args:
301
+ schema: JSON schema dict (typically from model.model_json_schema())
302
+
303
+ Returns:
304
+ Modified schema compatible with Cerebras Qwen strict mode
305
+
306
+ Example:
307
+ # Pydantic generates for dict[str, Any]:
308
+ {"type": "object", "additionalProperties": true}
309
+
310
+ # Qwen requires:
311
+ {"type": "object", "properties": {}, "additionalProperties": false}
312
+
313
+ # This means dict can only be {}
314
+ """
315
+ def fix_object_properties(obj: dict[str, Any]) -> None:
316
+ """Recursively fix object schemas for Qwen strict mode."""
317
+ if isinstance(obj, dict):
318
+ # Fix current object if it's type=object
319
+ if obj.get("type") == "object":
320
+ # Add empty properties if missing
321
+ if "properties" not in obj and "anyOf" not in obj and "oneOf" not in obj:
322
+ obj["properties"] = {}
323
+
324
+ # Force additionalProperties to false (required by Qwen strict mode)
325
+ if "additionalProperties" in obj:
326
+ obj["additionalProperties"] = False
327
+
328
+ # Remove minimum/maximum from number fields (Qwen rejects these)
329
+ if obj.get("type") == "number":
330
+ if "minimum" in obj or "maximum" in obj:
331
+ logger.warning(f"Stripping min/max from number field in Qwen schema: {obj.keys()}")
332
+ obj.pop("minimum", None)
333
+ obj.pop("maximum", None)
334
+
335
+ # Recursively fix nested schemas
336
+ for key, value in obj.items():
337
+ if isinstance(value, dict):
338
+ fix_object_properties(value)
339
+ elif isinstance(value, list):
340
+ for item in value:
341
+ if isinstance(item, dict):
342
+ fix_object_properties(item)
343
+
344
+ # Work on a copy to avoid mutating original
345
+ import copy
346
+ schema_copy = copy.deepcopy(schema)
347
+ fix_object_properties(schema_copy)
348
+
349
+ return schema_copy
350
+
351
+
352
+ def _convert_properties_to_prompt(properties: dict[str, Any]) -> str:
353
+ """
354
+ Convert schema properties to prompt guidance text.
355
+
356
+ When structured_output is disabled, this converts the properties
357
+ definition into natural language guidance that informs the agent
358
+ about the expected response structure without forcing JSON output.
359
+
360
+ IMPORTANT: The 'answer' field is the OUTPUT to the user. All other
361
+ fields are INTERNAL tracking that should NOT appear in the output.
362
+
363
+ Args:
364
+ properties: JSON Schema properties dict
365
+
366
+ Returns:
367
+ Prompt text describing the expected response elements
368
+
369
+ Example:
370
+ properties = {
371
+ "answer": {"type": "string", "description": "The answer"},
372
+ "confidence": {"type": "number", "description": "Confidence 0-1"}
373
+ }
374
+ # Returns guidance that only answer should be output
375
+ """
376
+ if not properties:
377
+ return ""
378
+
379
+ # Separate answer (output) from other fields (internal tracking)
380
+ answer_field = properties.get("answer")
381
+ internal_fields = {k: v for k, v in properties.items() if k != "answer"}
382
+
383
+ lines = ["## Internal Thinking Structure (DO NOT output these labels)"]
384
+ lines.append("")
385
+ lines.append("Use this structure to organize your thinking, but ONLY output the answer content:")
386
+ lines.append("")
387
+
388
+ # If there's an answer field, emphasize it's the ONLY output
389
+ if answer_field:
390
+ answer_desc = answer_field.get("description", "Your response")
391
+ lines.append(f"**OUTPUT (what the user sees):** {answer_desc}")
392
+ lines.append("")
393
+
394
+ # Document internal fields for tracking/thinking
395
+ if internal_fields:
396
+ lines.append("**INTERNAL (for your tracking only - do NOT include in output):**")
397
+ for field_name, field_def in internal_fields.items():
398
+ field_type = field_def.get("type", "any")
399
+ description = field_def.get("description", "")
400
+
401
+ # Format based on type
402
+ if field_type == "array":
403
+ type_hint = "list"
404
+ elif field_type == "number":
405
+ type_hint = "number"
406
+ if "minimum" in field_def or "maximum" in field_def:
407
+ min_val = field_def.get("minimum", "")
408
+ max_val = field_def.get("maximum", "")
409
+ if min_val != "" and max_val != "":
410
+ type_hint = f"number ({min_val}-{max_val})"
411
+ elif field_type == "boolean":
412
+ type_hint = "yes/no"
413
+ else:
414
+ type_hint = field_type
415
+
416
+ field_line = f"- {field_name}"
417
+ if type_hint and type_hint != "string":
418
+ field_line += f" ({type_hint})"
419
+ if description:
420
+ field_line += f": {description}"
421
+
422
+ lines.append(field_line)
423
+
424
+ lines.append("")
425
+ lines.append("⚠️ CRITICAL: Your response must be ONLY the conversational answer text.")
426
+ lines.append("Do NOT output field names like 'answer:' or 'diverge_output:' - just the response itself.")
427
+
428
+ return "\n".join(lines)
429
+
430
+
431
+ def _create_schema_wrapper(
432
+ result_type: type[BaseModel], strip_description: bool = True
433
+ ) -> type[BaseModel]:
434
+ """
435
+ Create wrapper model that customizes schema generation.
436
+
437
+ Prevents redundant descriptions in LLM schema while keeping
438
+ docstrings in Python code for documentation.
439
+
440
+ Design Pattern
441
+ - Agent schema.description contains full system prompt
442
+ - Output model description would duplicate this
443
+ - Stripping description reduces token usage without losing information
444
+
445
+ Args:
446
+ result_type: Original Pydantic model with docstring
447
+ strip_description: If True, removes model-level description from schema
448
+
449
+ Returns:
450
+ Wrapper model that generates schema without description field
451
+
452
+ Example:
453
+ class AgentOutput(BaseModel):
454
+ \"\"\"Agent output with answer and confidence.\"\"\"
455
+ answer: str
456
+ confidence: float
457
+
458
+ Wrapped = _create_schema_wrapper(AgentOutput, strip_description=True)
459
+ # Wrapped.model_json_schema() excludes top-level description
460
+ """
461
+ if not strip_description:
462
+ return result_type
463
+
464
+ # Create model that overrides schema generation
465
+ class SchemaWrapper(result_type): # type: ignore
466
+ @classmethod
467
+ def model_json_schema(cls, **kwargs):
468
+ schema = super().model_json_schema(**kwargs)
469
+ # Remove model-level description to avoid duplication with system prompt
470
+ schema.pop("description", None)
471
+ # Prepare schema for Qwen compatibility
472
+ schema = _prepare_schema_for_qwen(schema)
473
+ return schema
474
+
475
+ # Preserve original model name for debugging
476
+ SchemaWrapper.__name__ = result_type.__name__
477
+ return SchemaWrapper
478
+
479
+
480
+ async def create_agent_from_schema_file(
481
+ schema_name_or_path: str,
482
+ context: AgentContext | None = None,
483
+ model_override: KnownModelName | Model | None = None,
484
+ ) -> Agent:
485
+ """
486
+ Create agent from schema file (YAML/JSON).
487
+
488
+ Handles path resolution automatically:
489
+ - "contract-analyzer" → searches schemas/agents/examples/contract-analyzer.yaml
490
+ - "moment-builder" → searches schemas/agents/core/moment-builder.yaml
491
+ - "rem" → searches schemas/agents/rem.yaml
492
+ - "/absolute/path.yaml" → loads directly
493
+ - "relative/path.yaml" → loads relative to cwd
494
+
495
+ Args:
496
+ schema_name_or_path: Schema name or file path
497
+ context: Optional agent context
498
+ model_override: Optional model override
499
+
500
+ Returns:
501
+ Configured Agent instance
502
+
503
+ Example:
504
+ # Load by name (searches package schemas)
505
+ agent = await create_agent_from_schema_file("contract-analyzer")
506
+
507
+ # Load from custom path
508
+ agent = await create_agent_from_schema_file("./my-agent.yaml")
509
+ """
510
+ from ...utils.schema_loader import load_agent_schema
511
+
512
+ # Load schema using centralized utility
513
+ agent_schema = load_agent_schema(schema_name_or_path)
514
+
515
+ # Create agent using existing factory
516
+ return await create_agent(
517
+ context=context,
518
+ agent_schema_override=agent_schema,
519
+ model_override=model_override,
520
+ )
521
+
522
+
523
+ async def create_agent(
524
+ context: AgentContext | None = None,
525
+ agent_schema_override: dict[str, Any] | None = None,
526
+ model_override: KnownModelName | Model | None = None,
527
+ result_type: type[BaseModel] | None = None,
528
+ strip_model_description: bool = True,
529
+ ) -> AgentRuntime:
530
+ """
531
+ Create agent from context with dynamic schema loading.
532
+
533
+ Provider-agnostic interface - currently implemented with Pydantic AI.
534
+
535
+ Design Pattern:
536
+ 1. Load agent schema from context.agent_schema_uri or use override
537
+ 2. Extract system prompt from schema.description
538
+ 3. Create dynamic Pydantic model from schema.properties
539
+ 4. Load MCP tools from schema.json_schema_extra.tools
540
+ 5. Create agent with model, prompt, output_type, and tools
541
+ 6. Enable OTEL instrumentation conditionally
542
+
543
+ All configuration comes from context unless explicitly overridden.
544
+ MCP server URLs resolved from environment variables (MCP_SERVER_{NAME}).
545
+
546
+ Args:
547
+ context: AgentContext with schema URI, model, session info
548
+ agent_schema_override: Optional explicit schema (bypasses context.agent_schema_uri)
549
+ model_override: Optional explicit model (bypasses context.default_model)
550
+ result_type: Optional Pydantic model for structured output
551
+ strip_model_description: If True, removes model docstring from LLM schema
552
+
553
+ Returns:
554
+ Configured Pydantic.AI Agent with MCP tools
555
+
556
+ Example:
557
+ # From context with schema URI
558
+ context = AgentContext(
559
+ user_id="user123",
560
+ tenant_id="acme-corp",
561
+ agent_schema_uri="rem-agents-query-agent"
562
+ )
563
+ agent = await create_agent(context)
564
+
565
+ # With explicit schema and result type
566
+ schema = {...} # JSON Schema
567
+ class Output(BaseModel):
568
+ answer: str
569
+ confidence: float
570
+
571
+ agent = await create_agent(
572
+ agent_schema_override=schema,
573
+ result_type=Output
574
+ )
575
+ """
576
+ # Initialize OTEL instrumentation if enabled (idempotent)
577
+ if settings.otel.enabled:
578
+ from ..otel import setup_instrumentation
579
+
580
+ setup_instrumentation()
581
+
582
+ # Load agent schema from context or use override
583
+ agent_schema = agent_schema_override
584
+ if agent_schema is None and context and context.agent_schema_uri:
585
+ # TODO: Load schema from schema registry or file
586
+ # from ..schema import load_agent_schema
587
+ # agent_schema = load_agent_schema(context.agent_schema_uri)
588
+ pass
589
+
590
+ # Determine model: validate override against allowed list, fallback to context or settings
591
+ from rem.agentic.llm_provider_models import get_valid_model_or_default
592
+
593
+ default_model = context.default_model if context else settings.llm.default_model
594
+ model = get_valid_model_or_default(model_override, default_model)
595
+
596
+ # Extract schema fields using typed helpers
597
+ from ..schema import get_system_prompt, get_metadata
598
+
599
+ if agent_schema:
600
+ system_prompt = get_system_prompt(agent_schema)
601
+ metadata = get_metadata(agent_schema)
602
+ resource_configs = metadata.resources if hasattr(metadata, 'resources') else []
603
+
604
+ # DEPRECATED: mcp_servers in agent schemas is ignored
605
+ # MCP servers are now always auto-detected at the application level
606
+ if hasattr(metadata, 'mcp_servers') and metadata.mcp_servers:
607
+ logger.warning(
608
+ "DEPRECATED: mcp_servers in agent schema is ignored. "
609
+ "MCP servers are auto-detected from tools.mcp_server module. "
610
+ "Remove mcp_servers from your agent schema."
611
+ )
612
+
613
+ if metadata.system_prompt:
614
+ logger.debug("Using custom system_prompt from json_schema_extra")
615
+ else:
616
+ system_prompt = ""
617
+ metadata = None
618
+ resource_configs = []
619
+
620
+ # Auto-detect MCP server at application level
621
+ # Convention: tools/mcp_server.py exports `mcp` FastMCP instance
622
+ # Falls back to REM's built-in MCP server if no local server found
623
+ import importlib
624
+ import os
625
+ import sys
626
+
627
+ # Ensure current working directory is in sys.path for local imports
628
+ cwd = os.getcwd()
629
+ if cwd not in sys.path:
630
+ sys.path.insert(0, cwd)
631
+
632
+ mcp_server_configs = []
633
+ auto_detect_modules = [
634
+ "tools.mcp_server", # Convention: tools/mcp_server.py
635
+ "mcp_server", # Alternative: mcp_server.py in root
636
+ ]
637
+ for module_path in auto_detect_modules:
638
+ try:
639
+ mcp_module = importlib.import_module(module_path)
640
+ if hasattr(mcp_module, "mcp"):
641
+ logger.info(f"Auto-detected local MCP server: {module_path}")
642
+ mcp_server_configs = [{"type": "local", "module": module_path, "id": "auto-detected"}]
643
+ break
644
+ except ImportError as e:
645
+ logger.debug(f"MCP server auto-detect: {module_path} not found ({e})")
646
+ continue
647
+ except Exception as e:
648
+ logger.warning(f"MCP server auto-detect: {module_path} failed to load: {e}")
649
+ continue
650
+
651
+ # Fall back to REM's default MCP server if no local server found
652
+ if not mcp_server_configs:
653
+ logger.info("No local MCP server found, using REM default (rem.mcp_server)")
654
+ mcp_server_configs = [{"type": "local", "module": "rem.mcp_server", "id": "rem"}]
655
+
656
+ # Extract temperature and max_iterations from schema metadata (with fallback to settings defaults)
657
+ if metadata:
658
+ temperature = metadata.override_temperature if metadata.override_temperature is not None else settings.llm.default_temperature
659
+ max_iterations = metadata.override_max_iterations if metadata.override_max_iterations is not None else settings.llm.default_max_iterations
660
+ # Use schema-level structured_output if set, otherwise fall back to global setting
661
+ use_structured_output = metadata.structured_output if metadata.structured_output is not None else settings.llm.default_structured_output
662
+ else:
663
+ temperature = settings.llm.default_temperature
664
+ max_iterations = settings.llm.default_max_iterations
665
+ use_structured_output = settings.llm.default_structured_output
666
+
667
+ # Build list of tools - start with built-in tools
668
+ tools = _get_builtin_tools()
669
+
670
+ # Get agent name from metadata for logging
671
+ agent_name = metadata.name if metadata and hasattr(metadata, 'name') else "unknown"
672
+
673
+ logger.info(
674
+ f"Creating agent '{agent_name}': model={model}, mcp_servers={len(mcp_server_configs)}, "
675
+ f"resources={len(resource_configs)}, builtin_tools={len(tools)}"
676
+ )
677
+
678
+ # Set agent resource attributes for OTEL (before creating agent)
679
+ if settings.otel.enabled and agent_schema:
680
+ from ..otel import set_agent_resource_attributes
681
+
682
+ set_agent_resource_attributes(agent_schema=agent_schema)
683
+
684
+ # Add tools from MCP server (in-process, no subprocess)
685
+ # Track loaded MCP servers for resource resolution
686
+ loaded_mcp_server = None
687
+
688
+ # Build map of tool_name → schema description from agent schema tools section
689
+ # This allows agent-specific tool guidance to override/augment MCP tool descriptions
690
+ schema_tool_descriptions: dict[str, str] = {}
691
+ tool_configs = metadata.tools if metadata and hasattr(metadata, 'tools') else []
692
+ for tool_config in tool_configs:
693
+ if hasattr(tool_config, 'name'):
694
+ t_name = tool_config.name
695
+ t_desc = tool_config.description or ""
696
+ else:
697
+ t_name = tool_config.get("name", "")
698
+ t_desc = tool_config.get("description", "")
699
+ # Skip resource URIs (handled separately below)
700
+ if t_name and "://" not in t_name and t_desc:
701
+ schema_tool_descriptions[t_name] = t_desc
702
+ logger.debug(f"Schema tool description for '{t_name}': {len(t_desc)} chars")
703
+
704
+ for server_config in mcp_server_configs:
705
+ server_type = server_config.get("type")
706
+ server_id = server_config.get("id", "mcp-server")
707
+
708
+ if server_type == "local":
709
+ # Import MCP server directly (in-process)
710
+ module_path = server_config.get("module", "rem.mcp_server")
711
+
712
+ try:
713
+ # Dynamic import of MCP server module
714
+ import importlib
715
+ mcp_module = importlib.import_module(module_path)
716
+ mcp_server = mcp_module.mcp
717
+
718
+ # Store the loaded server for resource resolution
719
+ loaded_mcp_server = mcp_server
720
+
721
+ # Extract tools from MCP server (get_tools is async)
722
+ from ..mcp.tool_wrapper import create_mcp_tool_wrapper
723
+
724
+ # Await async get_tools() call
725
+ mcp_tools_dict = await mcp_server.get_tools()
726
+
727
+ for tool_name, tool_func in mcp_tools_dict.items():
728
+ # Get schema description suffix if agent schema defines one for this tool
729
+ tool_suffix = schema_tool_descriptions.get(tool_name)
730
+
731
+ wrapped_tool = create_mcp_tool_wrapper(
732
+ tool_name,
733
+ tool_func,
734
+ user_id=context.user_id if context else None,
735
+ description_suffix=tool_suffix,
736
+ )
737
+ tools.append(wrapped_tool)
738
+ logger.debug(f"Loaded MCP tool: {tool_name}" + (" (with schema desc)" if tool_suffix else ""))
739
+
740
+ logger.info(f"Loaded {len(mcp_tools_dict)} tools from MCP server: {server_id} (in-process)")
741
+
742
+ except Exception as e:
743
+ logger.error(f"Failed to load MCP server {server_id}: {e}", exc_info=True)
744
+ else:
745
+ logger.warning(f"Unsupported MCP server type: {server_type}")
746
+
747
+ # Convert resources to tools (MCP convenience syntax)
748
+ # Resources declared in agent YAML become callable tools - eliminates
749
+ # the artificial MCP distinction between tools and resources
750
+ #
751
+ # Supports both concrete and template URIs:
752
+ # - Concrete: "rem://agents" -> no-param tool
753
+ # - Template: "patient-profile://field/{field_key}" -> tool with field_key param
754
+ from ..mcp.tool_wrapper import create_resource_tool
755
+
756
+ # Collect all resource URIs from both resources section AND tools section
757
+ resource_uris = []
758
+
759
+ # From resources section (legacy format)
760
+ if resource_configs:
761
+ for resource_config in resource_configs:
762
+ if hasattr(resource_config, 'uri'):
763
+ uri = resource_config.uri
764
+ usage = resource_config.description or ""
765
+ else:
766
+ uri = resource_config.get("uri", "")
767
+ usage = resource_config.get("description", "")
768
+ if uri:
769
+ resource_uris.append((uri, usage))
770
+
771
+ # From tools section - detect URIs (anything with ://)
772
+ # This allows unified syntax: resources as tools
773
+ tool_configs = metadata.tools if metadata and hasattr(metadata, 'tools') else []
774
+ for tool_config in tool_configs:
775
+ if hasattr(tool_config, 'name'):
776
+ tool_name = tool_config.name
777
+ tool_desc = tool_config.description or ""
778
+ else:
779
+ tool_name = tool_config.get("name", "")
780
+ tool_desc = tool_config.get("description", "")
781
+
782
+ # Auto-detect resource URIs (anything with :// scheme)
783
+ if "://" in tool_name:
784
+ resource_uris.append((tool_name, tool_desc))
785
+
786
+ # Create tools from collected resource URIs
787
+ # Pass the loaded MCP server so resources can be resolved from it
788
+ logger.info(f"Creating {len(resource_uris)} resource tools with mcp_server={'set' if loaded_mcp_server else 'None'}")
789
+ for uri, usage in resource_uris:
790
+ resource_tool = create_resource_tool(uri, usage, mcp_server=loaded_mcp_server)
791
+ tools.append(resource_tool)
792
+ logger.debug(f"Loaded resource as tool: {uri}")
793
+
794
+ # Create dynamic result_type from schema if not provided
795
+ # Note: use_structured_output is set earlier from metadata.structured_output
796
+ if result_type is None and agent_schema and "properties" in agent_schema:
797
+ if use_structured_output:
798
+ # Pre-process schema for Qwen compatibility (strips min/max, sets additionalProperties=False)
799
+ # This ensures the generated Pydantic model doesn't have incompatible constraints
800
+ sanitized_schema = _prepare_schema_for_qwen(agent_schema)
801
+ result_type = _create_model_from_schema(sanitized_schema)
802
+ logger.debug(f"Created dynamic Pydantic model: {result_type.__name__}")
803
+ else:
804
+ # Convert properties to prompt guidance instead of structured output
805
+ # This informs the agent about expected response structure without forcing it
806
+ properties_prompt = _convert_properties_to_prompt(agent_schema.get("properties", {}))
807
+ if properties_prompt:
808
+ system_prompt = system_prompt + "\n\n" + properties_prompt
809
+ logger.debug("Structured output disabled - properties converted to prompt guidance")
810
+
811
+ # Create agent with optional output_type for structured output and tools
812
+ if result_type:
813
+ # Wrap result_type to strip description if needed
814
+ wrapped_result_type = _create_schema_wrapper(
815
+ result_type, strip_description=strip_model_description
816
+ )
817
+ # Use InstrumentationSettings with version=3 to include agent name in span names
818
+ from pydantic_ai.models.instrumented import InstrumentationSettings
819
+ instrumentation = InstrumentationSettings(version=3) if settings.otel.enabled else False
820
+
821
+ agent = Agent(
822
+ model=model,
823
+ name=agent_name, # Used for OTEL span names (version 3: "invoke_agent {name}")
824
+ system_prompt=system_prompt,
825
+ output_type=wrapped_result_type,
826
+ tools=tools,
827
+ instrument=instrumentation,
828
+ model_settings={"temperature": temperature},
829
+ retries=settings.llm.max_retries,
830
+ )
831
+ else:
832
+ from pydantic_ai.models.instrumented import InstrumentationSettings
833
+ instrumentation = InstrumentationSettings(version=3) if settings.otel.enabled else False
834
+
835
+ agent = Agent(
836
+ model=model,
837
+ name=agent_name, # Used for OTEL span names (version 3: "invoke_agent {name}")
838
+ system_prompt=system_prompt,
839
+ tools=tools,
840
+ instrument=instrumentation,
841
+ model_settings={"temperature": temperature},
842
+ retries=settings.llm.max_retries,
843
+ )
844
+
845
+ # TODO: Set agent context attributes for OTEL spans
846
+ # if context:
847
+ # from ..otel import set_agent_context_attributes
848
+ # set_agent_context_attributes(context)
849
+
850
+ return AgentRuntime(
851
+ agent=agent,
852
+ temperature=temperature,
853
+ max_iterations=max_iterations,
854
+ )