remdb 0.3.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. rem/__init__.py +2 -0
  2. rem/agentic/README.md +650 -0
  3. rem/agentic/__init__.py +39 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +8 -0
  6. rem/agentic/context.py +148 -0
  7. rem/agentic/context_builder.py +329 -0
  8. rem/agentic/mcp/__init__.py +0 -0
  9. rem/agentic/mcp/tool_wrapper.py +107 -0
  10. rem/agentic/otel/__init__.py +5 -0
  11. rem/agentic/otel/setup.py +151 -0
  12. rem/agentic/providers/phoenix.py +674 -0
  13. rem/agentic/providers/pydantic_ai.py +572 -0
  14. rem/agentic/query.py +117 -0
  15. rem/agentic/query_helper.py +89 -0
  16. rem/agentic/schema.py +396 -0
  17. rem/agentic/serialization.py +245 -0
  18. rem/agentic/tools/__init__.py +5 -0
  19. rem/agentic/tools/rem_tools.py +231 -0
  20. rem/api/README.md +420 -0
  21. rem/api/main.py +324 -0
  22. rem/api/mcp_router/prompts.py +182 -0
  23. rem/api/mcp_router/resources.py +536 -0
  24. rem/api/mcp_router/server.py +213 -0
  25. rem/api/mcp_router/tools.py +584 -0
  26. rem/api/routers/auth.py +229 -0
  27. rem/api/routers/chat/__init__.py +5 -0
  28. rem/api/routers/chat/completions.py +281 -0
  29. rem/api/routers/chat/json_utils.py +76 -0
  30. rem/api/routers/chat/models.py +124 -0
  31. rem/api/routers/chat/streaming.py +185 -0
  32. rem/auth/README.md +258 -0
  33. rem/auth/__init__.py +26 -0
  34. rem/auth/middleware.py +100 -0
  35. rem/auth/providers/__init__.py +13 -0
  36. rem/auth/providers/base.py +376 -0
  37. rem/auth/providers/google.py +163 -0
  38. rem/auth/providers/microsoft.py +237 -0
  39. rem/cli/README.md +455 -0
  40. rem/cli/__init__.py +8 -0
  41. rem/cli/commands/README.md +126 -0
  42. rem/cli/commands/__init__.py +3 -0
  43. rem/cli/commands/ask.py +566 -0
  44. rem/cli/commands/configure.py +497 -0
  45. rem/cli/commands/db.py +493 -0
  46. rem/cli/commands/dreaming.py +324 -0
  47. rem/cli/commands/experiments.py +1302 -0
  48. rem/cli/commands/mcp.py +66 -0
  49. rem/cli/commands/process.py +245 -0
  50. rem/cli/commands/schema.py +183 -0
  51. rem/cli/commands/serve.py +106 -0
  52. rem/cli/dreaming.py +363 -0
  53. rem/cli/main.py +96 -0
  54. rem/config.py +237 -0
  55. rem/mcp_server.py +41 -0
  56. rem/models/core/__init__.py +49 -0
  57. rem/models/core/core_model.py +64 -0
  58. rem/models/core/engram.py +333 -0
  59. rem/models/core/experiment.py +628 -0
  60. rem/models/core/inline_edge.py +132 -0
  61. rem/models/core/rem_query.py +243 -0
  62. rem/models/entities/__init__.py +43 -0
  63. rem/models/entities/file.py +57 -0
  64. rem/models/entities/image_resource.py +88 -0
  65. rem/models/entities/message.py +35 -0
  66. rem/models/entities/moment.py +123 -0
  67. rem/models/entities/ontology.py +191 -0
  68. rem/models/entities/ontology_config.py +131 -0
  69. rem/models/entities/resource.py +95 -0
  70. rem/models/entities/schema.py +87 -0
  71. rem/models/entities/user.py +85 -0
  72. rem/py.typed +0 -0
  73. rem/schemas/README.md +507 -0
  74. rem/schemas/__init__.py +6 -0
  75. rem/schemas/agents/README.md +92 -0
  76. rem/schemas/agents/core/moment-builder.yaml +178 -0
  77. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  78. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  79. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  80. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  81. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  82. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  83. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  84. rem/schemas/agents/examples/hello-world.yaml +37 -0
  85. rem/schemas/agents/examples/query.yaml +54 -0
  86. rem/schemas/agents/examples/simple.yaml +21 -0
  87. rem/schemas/agents/examples/test.yaml +29 -0
  88. rem/schemas/agents/rem.yaml +128 -0
  89. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  90. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  91. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  92. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  93. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  94. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  95. rem/services/__init__.py +16 -0
  96. rem/services/audio/INTEGRATION.md +308 -0
  97. rem/services/audio/README.md +376 -0
  98. rem/services/audio/__init__.py +15 -0
  99. rem/services/audio/chunker.py +354 -0
  100. rem/services/audio/transcriber.py +259 -0
  101. rem/services/content/README.md +1269 -0
  102. rem/services/content/__init__.py +5 -0
  103. rem/services/content/providers.py +801 -0
  104. rem/services/content/service.py +676 -0
  105. rem/services/dreaming/README.md +230 -0
  106. rem/services/dreaming/__init__.py +53 -0
  107. rem/services/dreaming/affinity_service.py +336 -0
  108. rem/services/dreaming/moment_service.py +264 -0
  109. rem/services/dreaming/ontology_service.py +54 -0
  110. rem/services/dreaming/user_model_service.py +297 -0
  111. rem/services/dreaming/utils.py +39 -0
  112. rem/services/embeddings/__init__.py +11 -0
  113. rem/services/embeddings/api.py +120 -0
  114. rem/services/embeddings/worker.py +421 -0
  115. rem/services/fs/README.md +662 -0
  116. rem/services/fs/__init__.py +62 -0
  117. rem/services/fs/examples.py +206 -0
  118. rem/services/fs/examples_paths.py +204 -0
  119. rem/services/fs/git_provider.py +935 -0
  120. rem/services/fs/local_provider.py +760 -0
  121. rem/services/fs/parsing-hooks-examples.md +172 -0
  122. rem/services/fs/paths.py +276 -0
  123. rem/services/fs/provider.py +460 -0
  124. rem/services/fs/s3_provider.py +1042 -0
  125. rem/services/fs/service.py +186 -0
  126. rem/services/git/README.md +1075 -0
  127. rem/services/git/__init__.py +17 -0
  128. rem/services/git/service.py +469 -0
  129. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  130. rem/services/phoenix/README.md +453 -0
  131. rem/services/phoenix/__init__.py +46 -0
  132. rem/services/phoenix/client.py +686 -0
  133. rem/services/phoenix/config.py +88 -0
  134. rem/services/phoenix/prompt_labels.py +477 -0
  135. rem/services/postgres/README.md +575 -0
  136. rem/services/postgres/__init__.py +23 -0
  137. rem/services/postgres/migration_service.py +427 -0
  138. rem/services/postgres/pydantic_to_sqlalchemy.py +232 -0
  139. rem/services/postgres/register_type.py +352 -0
  140. rem/services/postgres/repository.py +337 -0
  141. rem/services/postgres/schema_generator.py +379 -0
  142. rem/services/postgres/service.py +802 -0
  143. rem/services/postgres/sql_builder.py +354 -0
  144. rem/services/rem/README.md +304 -0
  145. rem/services/rem/__init__.py +23 -0
  146. rem/services/rem/exceptions.py +71 -0
  147. rem/services/rem/executor.py +293 -0
  148. rem/services/rem/parser.py +145 -0
  149. rem/services/rem/queries.py +196 -0
  150. rem/services/rem/query.py +371 -0
  151. rem/services/rem/service.py +527 -0
  152. rem/services/session/README.md +374 -0
  153. rem/services/session/__init__.py +6 -0
  154. rem/services/session/compression.py +360 -0
  155. rem/services/session/reload.py +77 -0
  156. rem/settings.py +1235 -0
  157. rem/sql/002_install_models.sql +1068 -0
  158. rem/sql/background_indexes.sql +42 -0
  159. rem/sql/install_models.sql +1038 -0
  160. rem/sql/migrations/001_install.sql +503 -0
  161. rem/sql/migrations/002_install_models.sql +1202 -0
  162. rem/utils/AGENTIC_CHUNKING.md +597 -0
  163. rem/utils/README.md +583 -0
  164. rem/utils/__init__.py +43 -0
  165. rem/utils/agentic_chunking.py +622 -0
  166. rem/utils/batch_ops.py +343 -0
  167. rem/utils/chunking.py +108 -0
  168. rem/utils/clip_embeddings.py +276 -0
  169. rem/utils/dict_utils.py +98 -0
  170. rem/utils/embeddings.py +423 -0
  171. rem/utils/examples/embeddings_example.py +305 -0
  172. rem/utils/examples/sql_types_example.py +202 -0
  173. rem/utils/markdown.py +16 -0
  174. rem/utils/model_helpers.py +236 -0
  175. rem/utils/schema_loader.py +336 -0
  176. rem/utils/sql_types.py +348 -0
  177. rem/utils/user_id.py +81 -0
  178. rem/utils/vision.py +330 -0
  179. rem/workers/README.md +506 -0
  180. rem/workers/__init__.py +5 -0
  181. rem/workers/dreaming.py +502 -0
  182. rem/workers/engram_processor.py +312 -0
  183. rem/workers/sqs_file_processor.py +193 -0
  184. remdb-0.3.7.dist-info/METADATA +1473 -0
  185. remdb-0.3.7.dist-info/RECORD +187 -0
  186. remdb-0.3.7.dist-info/WHEEL +4 -0
  187. remdb-0.3.7.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,572 @@
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
+ Example Agent Schema:
100
+ {
101
+ "type": "object",
102
+ "description": "Agent that answers REM queries...",
103
+ "properties": {
104
+ "answer": {"type": "string", "description": "Query answer"},
105
+ "confidence": {"type": "number"}
106
+ },
107
+ "required": ["answer", "confidence"],
108
+ "json_schema_extra": {
109
+ "kind": "agent",
110
+ "name": "query-agent",
111
+ "tools": [
112
+ {"name": "search_knowledge_base", "mcp_server": "rem"}
113
+ ],
114
+ "resources": [
115
+ {"uri_pattern": "cda://.*", "mcp_server": "rem"}
116
+ ]
117
+ }
118
+ }
119
+ """
120
+
121
+ from typing import Any
122
+
123
+ from loguru import logger
124
+ from pydantic import BaseModel
125
+ from pydantic_ai import Agent
126
+ from pydantic_ai.models import KnownModelName, Model
127
+
128
+ try:
129
+ from json_schema_to_pydantic import PydanticModelBuilder
130
+
131
+ JSON_SCHEMA_TO_PYDANTIC_AVAILABLE = True
132
+ except ImportError:
133
+ JSON_SCHEMA_TO_PYDANTIC_AVAILABLE = False
134
+ logger.warning(
135
+ "json-schema-to-pydantic not installed. "
136
+ "Install with: pip install 'rem[schema]' or pip install json-schema-to-pydantic"
137
+ )
138
+
139
+ from ..context import AgentContext
140
+ from ...settings import settings
141
+
142
+
143
+ class AgentRuntime:
144
+ """
145
+ Agent runtime configuration bundle with delegation pattern.
146
+
147
+ Contains the agent instance and its resolved runtime parameters
148
+ (temperature, max_iterations) determined from schema overrides + settings defaults.
149
+
150
+ Delegates run() and iter() calls to the inner agent with automatic UsageLimits.
151
+ This allows callers to use AgentRuntime as a drop-in replacement for Agent.
152
+ """
153
+
154
+ def __init__(self, agent: Agent[None, Any], temperature: float, max_iterations: int):
155
+ self.agent = agent
156
+ self.temperature = temperature
157
+ self.max_iterations = max_iterations
158
+
159
+ async def run(self, *args, **kwargs):
160
+ """Delegate to agent.run() with automatic UsageLimits."""
161
+ from pydantic_ai import UsageLimits
162
+
163
+ # Only apply usage_limits if not already provided
164
+ if "usage_limits" not in kwargs:
165
+ kwargs["usage_limits"] = UsageLimits(request_limit=self.max_iterations)
166
+ return await self.agent.run(*args, **kwargs)
167
+
168
+ def iter(self, *args, **kwargs):
169
+ """Delegate to agent.iter() with automatic UsageLimits."""
170
+ from pydantic_ai import UsageLimits
171
+
172
+ # Only apply usage_limits if not already provided
173
+ if "usage_limits" not in kwargs:
174
+ kwargs["usage_limits"] = UsageLimits(request_limit=self.max_iterations)
175
+ return self.agent.iter(*args, **kwargs)
176
+
177
+
178
+ def _create_model_from_schema(agent_schema: dict[str, Any]) -> type[BaseModel]:
179
+ """
180
+ Create Pydantic model dynamically from JSON Schema.
181
+
182
+ Uses json-schema-to-pydantic library for robust conversion of:
183
+ - Nested objects
184
+ - Arrays
185
+ - Required fields
186
+ - Validation constraints
187
+
188
+ Args:
189
+ agent_schema: JSON Schema dict with agent output structure
190
+
191
+ Returns:
192
+ Dynamically created Pydantic BaseModel class
193
+
194
+ Example:
195
+ schema = {
196
+ "type": "object",
197
+ "properties": {
198
+ "answer": {"type": "string"},
199
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1}
200
+ },
201
+ "required": ["answer", "confidence"]
202
+ }
203
+ Model = _create_model_from_schema(schema)
204
+ # Model is now a Pydantic class with answer: str and confidence: float fields
205
+ """
206
+ if not JSON_SCHEMA_TO_PYDANTIC_AVAILABLE:
207
+ raise ImportError(
208
+ "json-schema-to-pydantic is required for dynamic schema conversion. "
209
+ "Install with: pip install 'rem[schema]' or pip install json-schema-to-pydantic"
210
+ )
211
+
212
+ # Create Pydantic model from JSON Schema
213
+ builder = PydanticModelBuilder()
214
+ model = builder.create_pydantic_model(agent_schema, root_schema=agent_schema)
215
+
216
+ # Override model name with schema name if available
217
+ json_extra = agent_schema.get("json_schema_extra", {})
218
+ schema_name = json_extra.get("name")
219
+ if schema_name:
220
+ # Convert kebab-case to PascalCase for class name
221
+ class_name = "".join(word.capitalize() for word in schema_name.split("-"))
222
+ model.__name__ = class_name
223
+ model.__qualname__ = class_name
224
+
225
+ logger.debug(
226
+ f"Created Pydantic model '{model.__name__}' from JSON Schema with fields: "
227
+ f"{list(model.model_fields.keys())}"
228
+ )
229
+
230
+ return model
231
+
232
+
233
+ def _prepare_schema_for_qwen(schema: dict[str, Any]) -> dict[str, Any]:
234
+ """
235
+ Prepare JSON schema for Cerebras Qwen strict mode compatibility.
236
+
237
+ Cerebras Qwen strict mode requirements:
238
+ 1. additionalProperties MUST be false (this is mandatory in strict mode)
239
+ 2. All object types must have explicit properties field
240
+ 3. Cannot use minimum/maximum constraints (Pydantic ge/le works fine)
241
+
242
+ This function transforms schemas to meet these requirements:
243
+ - Changes additionalProperties from true to false
244
+ - Adds empty properties {} to objects that don't have it
245
+ - Preserves all other schema features
246
+
247
+ IMPORTANT: This breaks dict[str, Any] flexibility!
248
+ - dict[str, Any] generates {"type": "object", "additionalProperties": true}
249
+ - Qwen requires additionalProperties: false
250
+ - Result: Empty dict {} becomes the only valid value
251
+
252
+ Recommendation: Don't use dict[str, Any] with Qwen. Use explicit Pydantic models instead.
253
+
254
+ Args:
255
+ schema: JSON schema dict (typically from model.model_json_schema())
256
+
257
+ Returns:
258
+ Modified schema compatible with Cerebras Qwen strict mode
259
+
260
+ Example:
261
+ # Pydantic generates for dict[str, Any]:
262
+ {"type": "object", "additionalProperties": true}
263
+
264
+ # Qwen requires:
265
+ {"type": "object", "properties": {}, "additionalProperties": false}
266
+
267
+ # This means dict can only be {}
268
+ """
269
+ def fix_object_properties(obj: dict[str, Any]) -> None:
270
+ """Recursively fix object schemas for Qwen strict mode."""
271
+ if isinstance(obj, dict):
272
+ # Fix current object if it's type=object
273
+ if obj.get("type") == "object":
274
+ # Add empty properties if missing
275
+ if "properties" not in obj and "anyOf" not in obj and "oneOf" not in obj:
276
+ obj["properties"] = {}
277
+
278
+ # Force additionalProperties to false (required by Qwen strict mode)
279
+ if "additionalProperties" in obj:
280
+ obj["additionalProperties"] = False
281
+
282
+ # Remove minimum/maximum from number fields (Qwen rejects these)
283
+ if obj.get("type") == "number":
284
+ if "minimum" in obj or "maximum" in obj:
285
+ logger.warning(f"Stripping min/max from number field in Qwen schema: {obj.keys()}")
286
+ obj.pop("minimum", None)
287
+ obj.pop("maximum", None)
288
+
289
+ # Recursively fix nested schemas
290
+ for key, value in obj.items():
291
+ if isinstance(value, dict):
292
+ fix_object_properties(value)
293
+ elif isinstance(value, list):
294
+ for item in value:
295
+ if isinstance(item, dict):
296
+ fix_object_properties(item)
297
+
298
+ # Work on a copy to avoid mutating original
299
+ import copy
300
+ schema_copy = copy.deepcopy(schema)
301
+ fix_object_properties(schema_copy)
302
+
303
+ return schema_copy
304
+
305
+
306
+ def _create_schema_wrapper(
307
+ result_type: type[BaseModel], strip_description: bool = True
308
+ ) -> type[BaseModel]:
309
+ """
310
+ Create wrapper model that customizes schema generation.
311
+
312
+ Prevents redundant descriptions in LLM schema while keeping
313
+ docstrings in Python code for documentation.
314
+
315
+ Design Pattern
316
+ - Agent schema.description contains full system prompt
317
+ - Output model description would duplicate this
318
+ - Stripping description reduces token usage without losing information
319
+
320
+ Args:
321
+ result_type: Original Pydantic model with docstring
322
+ strip_description: If True, removes model-level description from schema
323
+
324
+ Returns:
325
+ Wrapper model that generates schema without description field
326
+
327
+ Example:
328
+ class AgentOutput(BaseModel):
329
+ \"\"\"Agent output with answer and confidence.\"\"\"
330
+ answer: str
331
+ confidence: float
332
+
333
+ Wrapped = _create_schema_wrapper(AgentOutput, strip_description=True)
334
+ # Wrapped.model_json_schema() excludes top-level description
335
+ """
336
+ if not strip_description:
337
+ return result_type
338
+
339
+ # Create model that overrides schema generation
340
+ class SchemaWrapper(result_type): # type: ignore
341
+ @classmethod
342
+ def model_json_schema(cls, **kwargs):
343
+ schema = super().model_json_schema(**kwargs)
344
+ # Remove model-level description to avoid duplication with system prompt
345
+ schema.pop("description", None)
346
+ # Prepare schema for Qwen compatibility
347
+ schema = _prepare_schema_for_qwen(schema)
348
+ return schema
349
+
350
+ # Preserve original model name for debugging
351
+ SchemaWrapper.__name__ = result_type.__name__
352
+ return SchemaWrapper
353
+
354
+
355
+ async def create_agent_from_schema_file(
356
+ schema_name_or_path: str,
357
+ context: AgentContext | None = None,
358
+ model_override: KnownModelName | Model | None = None,
359
+ ) -> Agent:
360
+ """
361
+ Create agent from schema file (YAML/JSON).
362
+
363
+ Handles path resolution automatically:
364
+ - "contract-analyzer" → searches schemas/agents/examples/contract-analyzer.yaml
365
+ - "moment-builder" → searches schemas/agents/core/moment-builder.yaml
366
+ - "rem" → searches schemas/agents/rem.yaml
367
+ - "/absolute/path.yaml" → loads directly
368
+ - "relative/path.yaml" → loads relative to cwd
369
+
370
+ Args:
371
+ schema_name_or_path: Schema name or file path
372
+ context: Optional agent context
373
+ model_override: Optional model override
374
+
375
+ Returns:
376
+ Configured Agent instance
377
+
378
+ Example:
379
+ # Load by name (searches package schemas)
380
+ agent = await create_agent_from_schema_file("contract-analyzer")
381
+
382
+ # Load from custom path
383
+ agent = await create_agent_from_schema_file("./my-agent.yaml")
384
+ """
385
+ from ...utils.schema_loader import load_agent_schema
386
+
387
+ # Load schema using centralized utility
388
+ agent_schema = load_agent_schema(schema_name_or_path)
389
+
390
+ # Create agent using existing factory
391
+ return await create_agent(
392
+ context=context,
393
+ agent_schema_override=agent_schema,
394
+ model_override=model_override,
395
+ )
396
+
397
+
398
+ async def create_agent(
399
+ context: AgentContext | None = None,
400
+ agent_schema_override: dict[str, Any] | None = None,
401
+ model_override: KnownModelName | Model | None = None,
402
+ result_type: type[BaseModel] | None = None,
403
+ strip_model_description: bool = True,
404
+ ) -> AgentRuntime:
405
+ """
406
+ Create agent from context with dynamic schema loading.
407
+
408
+ Provider-agnostic interface - currently implemented with Pydantic AI.
409
+
410
+ Design Pattern:
411
+ 1. Load agent schema from context.agent_schema_uri or use override
412
+ 2. Extract system prompt from schema.description
413
+ 3. Create dynamic Pydantic model from schema.properties
414
+ 4. Load MCP tools from schema.json_schema_extra.tools
415
+ 5. Create agent with model, prompt, output_type, and tools
416
+ 6. Enable OTEL instrumentation conditionally
417
+
418
+ All configuration comes from context unless explicitly overridden.
419
+ MCP server URLs resolved from environment variables (MCP_SERVER_{NAME}).
420
+
421
+ Args:
422
+ context: AgentContext with schema URI, model, session info
423
+ agent_schema_override: Optional explicit schema (bypasses context.agent_schema_uri)
424
+ model_override: Optional explicit model (bypasses context.default_model)
425
+ result_type: Optional Pydantic model for structured output
426
+ strip_model_description: If True, removes model docstring from LLM schema
427
+
428
+ Returns:
429
+ Configured Pydantic.AI Agent with MCP tools
430
+
431
+ Example:
432
+ # From context with schema URI
433
+ context = AgentContext(
434
+ user_id="user123",
435
+ tenant_id="acme-corp",
436
+ agent_schema_uri="rem-agents-query-agent"
437
+ )
438
+ agent = await create_agent(context)
439
+
440
+ # With explicit schema and result type
441
+ schema = {...} # JSON Schema
442
+ class Output(BaseModel):
443
+ answer: str
444
+ confidence: float
445
+
446
+ agent = await create_agent(
447
+ agent_schema_override=schema,
448
+ result_type=Output
449
+ )
450
+ """
451
+ # Initialize OTEL instrumentation if enabled (idempotent)
452
+ if settings.otel.enabled:
453
+ from ..otel import setup_instrumentation
454
+
455
+ setup_instrumentation()
456
+
457
+ # Load agent schema from context or use override
458
+ agent_schema = agent_schema_override
459
+ if agent_schema is None and context and context.agent_schema_uri:
460
+ # TODO: Load schema from schema registry or file
461
+ # from ..schema import load_agent_schema
462
+ # agent_schema = load_agent_schema(context.agent_schema_uri)
463
+ pass
464
+
465
+ # Determine model: override > context.default_model > settings
466
+ model = (
467
+ model_override or (context.default_model if context else settings.llm.default_model)
468
+ )
469
+
470
+ # Extract schema fields
471
+ system_prompt = agent_schema.get("description", "") if agent_schema else ""
472
+ metadata = agent_schema.get("json_schema_extra", {}) if agent_schema else {}
473
+ mcp_server_configs = metadata.get("mcp_servers", [])
474
+ resource_configs = metadata.get("resources", [])
475
+
476
+ # Extract temperature and max_iterations from schema metadata (with fallback to settings defaults)
477
+ temperature = metadata.get("override_temperature", settings.llm.default_temperature)
478
+ max_iterations = metadata.get("override_max_iterations", settings.llm.default_max_iterations)
479
+
480
+ logger.info(
481
+ f"Creating agent: model={model}, mcp_servers={len(mcp_server_configs)}, resources={len(resource_configs)}"
482
+ )
483
+
484
+ # Set agent resource attributes for OTEL (before creating agent)
485
+ if settings.otel.enabled and agent_schema:
486
+ from ..otel import set_agent_resource_attributes
487
+
488
+ set_agent_resource_attributes(agent_schema=agent_schema)
489
+
490
+ # Build list of tools from MCP server (in-process, no subprocess)
491
+ tools = []
492
+ if mcp_server_configs:
493
+ for server_config in mcp_server_configs:
494
+ server_type = server_config.get("type")
495
+ server_id = server_config.get("id", "mcp-server")
496
+
497
+ if server_type == "local":
498
+ # Import MCP server directly (in-process)
499
+ module_path = server_config.get("module", "rem.mcp_server")
500
+
501
+ try:
502
+ # Dynamic import of MCP server module
503
+ import importlib
504
+ mcp_module = importlib.import_module(module_path)
505
+ mcp_server = mcp_module.mcp
506
+
507
+ # Extract tools from MCP server (get_tools is async)
508
+ from ..mcp.tool_wrapper import create_mcp_tool_wrapper
509
+
510
+ # Await async get_tools() call
511
+ mcp_tools_dict = await mcp_server.get_tools()
512
+
513
+ for tool_name, tool_func in mcp_tools_dict.items():
514
+ wrapped_tool = create_mcp_tool_wrapper(tool_name, tool_func, user_id=context.user_id if context else None)
515
+ tools.append(wrapped_tool)
516
+ logger.debug(f"Loaded MCP tool: {tool_name}")
517
+
518
+ logger.info(f"Loaded {len(mcp_tools_dict)} tools from MCP server: {server_id} (in-process)")
519
+
520
+ except Exception as e:
521
+ logger.error(f"Failed to load MCP server {server_id}: {e}", exc_info=True)
522
+ else:
523
+ logger.warning(f"Unsupported MCP server type: {server_type}")
524
+
525
+ if resource_configs:
526
+ # TODO: Convert resources to tools (MCP convenience syntax)
527
+ pass
528
+
529
+ # Create dynamic result_type from schema if not provided
530
+ if result_type is None and agent_schema and "properties" in agent_schema:
531
+ # Pre-process schema for Qwen compatibility (strips min/max, sets additionalProperties=False)
532
+ # This ensures the generated Pydantic model doesn't have incompatible constraints
533
+ sanitized_schema = _prepare_schema_for_qwen(agent_schema)
534
+ result_type = _create_model_from_schema(sanitized_schema)
535
+ logger.debug(f"Created dynamic Pydantic model: {result_type.__name__}")
536
+ logger.debug(f"Created dynamic Pydantic model: {result_type.__name__}")
537
+
538
+ # Create agent with optional output_type for structured output and tools
539
+ if result_type:
540
+ # Wrap result_type to strip description if needed
541
+ wrapped_result_type = _create_schema_wrapper(
542
+ result_type, strip_description=strip_model_description
543
+ )
544
+ agent = Agent(
545
+ model=model,
546
+ system_prompt=system_prompt,
547
+ output_type=wrapped_result_type,
548
+ tools=tools,
549
+ instrument=settings.otel.enabled, # Conditional OTEL instrumentation
550
+ model_settings={"temperature": temperature},
551
+ retries=settings.llm.max_retries,
552
+ )
553
+ else:
554
+ agent = Agent(
555
+ model=model,
556
+ system_prompt=system_prompt,
557
+ tools=tools,
558
+ instrument=settings.otel.enabled,
559
+ model_settings={"temperature": temperature},
560
+ retries=settings.llm.max_retries,
561
+ )
562
+
563
+ # TODO: Set agent context attributes for OTEL spans
564
+ # if context:
565
+ # from ..otel import set_agent_context_attributes
566
+ # set_agent_context_attributes(context)
567
+
568
+ return AgentRuntime(
569
+ agent=agent,
570
+ temperature=temperature,
571
+ max_iterations=max_iterations,
572
+ )
rem/agentic/query.py ADDED
@@ -0,0 +1,117 @@
1
+ """
2
+ Agent query model for structured agent input.
3
+
4
+ Design pattern for standardized agent query structure with:
5
+ - Primary query (user question/task)
6
+ - Knowledge context (retrieved context, documentation)
7
+ - Scratchpad (working memory, session state)
8
+
9
+ Key Design Pattern
10
+ - Separates query from retrieval (query is what user asks, knowledge is what we retrieve)
11
+ - Scratchpad enables multi-turn reasoning and state tracking
12
+ - Supports markdown + fenced JSON for structured data
13
+ - Converts to single prompt string for agent consumption
14
+ """
15
+
16
+ from typing import Any
17
+
18
+ from pydantic import BaseModel, Field
19
+
20
+
21
+ class AgentQuery(BaseModel):
22
+ """
23
+ Standard query structure for agent execution.
24
+
25
+ Provides consistent structure for queries, knowledge context, and
26
+ working memory across all agent types.
27
+
28
+ Design Pattern
29
+ - query: User's question/task (markdown + fenced JSON)
30
+ - knowledge: Retrieved context from REM queries (markdown + fenced JSON)
31
+ - scratchpad: Working memory for multi-turn reasoning (JSON or markdown)
32
+
33
+ Example:
34
+ query = AgentQuery(
35
+ query="Find all documents Sarah authored",
36
+ knowledge=\"\"\"
37
+ # Entity Information
38
+ Sarah Chen (person/employee)
39
+ - Role: Senior Engineer
40
+ - Projects: [Project Alpha, TiDB Migration]
41
+ \"\"\",
42
+ scratchpad={"current_case": "TAP-1234", "stage": "entity_lookup"}
43
+ )
44
+
45
+ prompt = query.to_prompt()
46
+ result = await agent.run(prompt)
47
+ """
48
+
49
+ query: str = Field(
50
+ ...,
51
+ description="Primary user query or task (markdown format, may include fenced JSON)",
52
+ examples=[
53
+ "Find all documents Sarah authored",
54
+ "What happened in Q4 retrospective?",
55
+ "TRAVERSE manages WITH LOOKUP sarah-chen DEPTH 2",
56
+ ],
57
+ )
58
+
59
+ knowledge: str = Field(
60
+ default="",
61
+ description="Background knowledge and context (markdown, may include fenced JSON/code)",
62
+ examples=[
63
+ "# Entity: sarah-chen\nType: person/employee\nRole: Senior Engineer",
64
+ "Retrieved resources:\n```json\n[{...}]\n```",
65
+ ],
66
+ )
67
+
68
+ scratchpad: str | dict[str, Any] = Field(
69
+ default="",
70
+ description="Working memory for session state (JSON object or markdown with fenced JSON)",
71
+ examples=[
72
+ {"stage": "lookup", "visited_entities": ["sarah-chen"]},
73
+ "# Session State\n\nStage: TRAVERSE depth 1\n\n```json\n{\"nodes\": [...]}\n```",
74
+ ],
75
+ )
76
+
77
+ def to_prompt(self) -> str:
78
+ """
79
+ Convert query components to single prompt string.
80
+
81
+ Combines query, knowledge, and scratchpad into formatted prompt
82
+ for agent consumption.
83
+
84
+ Returns:
85
+ Formatted prompt string with sections
86
+
87
+ Example:
88
+ # Query
89
+
90
+ Find all documents Sarah authored
91
+
92
+ # Knowledge
93
+
94
+ Entity: sarah-chen
95
+ Type: person/employee
96
+
97
+ # Scratchpad
98
+
99
+ ```json
100
+ {"stage": "lookup"}
101
+ ```
102
+ """
103
+ parts = [f"# Query\n\n{self.query}"]
104
+
105
+ if self.knowledge:
106
+ parts.append(f"\n# Knowledge\n\n{self.knowledge}")
107
+
108
+ if self.scratchpad:
109
+ if isinstance(self.scratchpad, dict):
110
+ import json
111
+
112
+ scratchpad_str = json.dumps(self.scratchpad, indent=2)
113
+ parts.append(f"\n# Scratchpad\n\n```json\n{scratchpad_str}\n```")
114
+ else:
115
+ parts.append(f"\n# Scratchpad\n\n{self.scratchpad}")
116
+
117
+ return "\n".join(parts)