remdb 0.3.0__py3-none-any.whl → 0.3.114__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 (98) hide show
  1. rem/__init__.py +129 -2
  2. rem/agentic/README.md +76 -0
  3. rem/agentic/__init__.py +15 -0
  4. rem/agentic/agents/__init__.py +16 -2
  5. rem/agentic/agents/sse_simulator.py +500 -0
  6. rem/agentic/context.py +28 -22
  7. rem/agentic/llm_provider_models.py +301 -0
  8. rem/agentic/otel/setup.py +92 -4
  9. rem/agentic/providers/phoenix.py +32 -43
  10. rem/agentic/providers/pydantic_ai.py +142 -22
  11. rem/agentic/schema.py +358 -21
  12. rem/agentic/tools/rem_tools.py +3 -3
  13. rem/api/README.md +238 -1
  14. rem/api/deps.py +255 -0
  15. rem/api/main.py +151 -37
  16. rem/api/mcp_router/resources.py +1 -1
  17. rem/api/mcp_router/server.py +17 -2
  18. rem/api/mcp_router/tools.py +143 -7
  19. rem/api/middleware/tracking.py +172 -0
  20. rem/api/routers/admin.py +277 -0
  21. rem/api/routers/auth.py +124 -0
  22. rem/api/routers/chat/completions.py +152 -16
  23. rem/api/routers/chat/models.py +7 -3
  24. rem/api/routers/chat/sse_events.py +526 -0
  25. rem/api/routers/chat/streaming.py +608 -45
  26. rem/api/routers/dev.py +81 -0
  27. rem/api/routers/feedback.py +148 -0
  28. rem/api/routers/messages.py +473 -0
  29. rem/api/routers/models.py +78 -0
  30. rem/api/routers/query.py +357 -0
  31. rem/api/routers/shared_sessions.py +406 -0
  32. rem/auth/middleware.py +126 -27
  33. rem/cli/commands/README.md +201 -70
  34. rem/cli/commands/ask.py +13 -10
  35. rem/cli/commands/cluster.py +1359 -0
  36. rem/cli/commands/configure.py +4 -3
  37. rem/cli/commands/db.py +350 -137
  38. rem/cli/commands/experiments.py +76 -72
  39. rem/cli/commands/process.py +22 -15
  40. rem/cli/commands/scaffold.py +47 -0
  41. rem/cli/commands/schema.py +95 -49
  42. rem/cli/main.py +29 -6
  43. rem/config.py +2 -2
  44. rem/models/core/core_model.py +7 -1
  45. rem/models/core/rem_query.py +5 -2
  46. rem/models/entities/__init__.py +21 -0
  47. rem/models/entities/domain_resource.py +38 -0
  48. rem/models/entities/feedback.py +123 -0
  49. rem/models/entities/message.py +30 -1
  50. rem/models/entities/session.py +83 -0
  51. rem/models/entities/shared_session.py +180 -0
  52. rem/models/entities/user.py +10 -3
  53. rem/registry.py +373 -0
  54. rem/schemas/agents/rem.yaml +7 -3
  55. rem/services/content/providers.py +94 -140
  56. rem/services/content/service.py +92 -20
  57. rem/services/dreaming/affinity_service.py +2 -16
  58. rem/services/dreaming/moment_service.py +2 -15
  59. rem/services/embeddings/api.py +24 -17
  60. rem/services/embeddings/worker.py +16 -16
  61. rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
  62. rem/services/phoenix/client.py +252 -19
  63. rem/services/postgres/README.md +159 -15
  64. rem/services/postgres/__init__.py +2 -1
  65. rem/services/postgres/diff_service.py +426 -0
  66. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  67. rem/services/postgres/repository.py +132 -0
  68. rem/services/postgres/schema_generator.py +86 -5
  69. rem/services/postgres/service.py +6 -6
  70. rem/services/rate_limit.py +113 -0
  71. rem/services/rem/README.md +14 -0
  72. rem/services/rem/parser.py +44 -9
  73. rem/services/rem/service.py +36 -2
  74. rem/services/session/compression.py +17 -1
  75. rem/services/session/reload.py +1 -1
  76. rem/services/user_service.py +98 -0
  77. rem/settings.py +169 -17
  78. rem/sql/background_indexes.sql +21 -16
  79. rem/sql/migrations/001_install.sql +231 -54
  80. rem/sql/migrations/002_install_models.sql +457 -393
  81. rem/sql/migrations/003_optional_extensions.sql +326 -0
  82. rem/utils/constants.py +97 -0
  83. rem/utils/date_utils.py +228 -0
  84. rem/utils/embeddings.py +17 -4
  85. rem/utils/files.py +167 -0
  86. rem/utils/mime_types.py +158 -0
  87. rem/utils/model_helpers.py +156 -1
  88. rem/utils/schema_loader.py +191 -35
  89. rem/utils/sql_types.py +3 -1
  90. rem/utils/vision.py +9 -14
  91. rem/workers/README.md +14 -14
  92. rem/workers/db_maintainer.py +74 -0
  93. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/METADATA +303 -164
  94. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/RECORD +96 -70
  95. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/WHEEL +1 -1
  96. rem/sql/002_install_models.sql +0 -1068
  97. rem/sql/install_models.sql +0 -1038
  98. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/entry_points.txt +0 -0
@@ -175,6 +175,23 @@ class AgentRuntime:
175
175
  return self.agent.iter(*args, **kwargs)
176
176
 
177
177
 
178
+ def _get_builtin_tools() -> list:
179
+ """
180
+ Get built-in tools that are always available to agents.
181
+
182
+ Currently returns empty list - all tools come from MCP servers.
183
+ The register_metadata tool is available via the REM MCP server and
184
+ agents can opt-in by configuring mcp_servers in their schema.
185
+
186
+ Returns:
187
+ List of Pydantic AI tool functions (currently empty)
188
+ """
189
+ # NOTE: register_metadata is now an MCP tool, not a built-in.
190
+ # Agents that want it should configure mcp_servers to load from rem.mcp_server.
191
+ # This allows agents to choose which tools they need.
192
+ return []
193
+
194
+
178
195
  def _create_model_from_schema(agent_schema: dict[str, Any]) -> type[BaseModel]:
179
196
  """
180
197
  Create Pydantic model dynamically from JSON Schema.
@@ -303,6 +320,68 @@ def _prepare_schema_for_qwen(schema: dict[str, Any]) -> dict[str, Any]:
303
320
  return schema_copy
304
321
 
305
322
 
323
+ def _convert_properties_to_prompt(properties: dict[str, Any]) -> str:
324
+ """
325
+ Convert schema properties to prompt guidance text.
326
+
327
+ When structured_output is disabled, this converts the properties
328
+ definition into natural language guidance that informs the agent
329
+ about the expected response structure without forcing JSON output.
330
+
331
+ Args:
332
+ properties: JSON Schema properties dict
333
+
334
+ Returns:
335
+ Prompt text describing the expected response elements
336
+
337
+ Example:
338
+ properties = {
339
+ "answer": {"type": "string", "description": "The answer"},
340
+ "confidence": {"type": "number", "description": "Confidence 0-1"}
341
+ }
342
+ # Returns:
343
+ # "## Response Structure\n\nYour response should include:\n- **answer**: The answer\n..."
344
+ """
345
+ if not properties:
346
+ return ""
347
+
348
+ lines = ["## Response Guidelines", "", "Your response should address the following elements:"]
349
+
350
+ for field_name, field_def in properties.items():
351
+ field_type = field_def.get("type", "any")
352
+ description = field_def.get("description", "")
353
+
354
+ # Format based on type
355
+ if field_type == "array":
356
+ type_hint = "list"
357
+ elif field_type == "number":
358
+ type_hint = "number"
359
+ # Include min/max if specified
360
+ if "minimum" in field_def or "maximum" in field_def:
361
+ min_val = field_def.get("minimum", "")
362
+ max_val = field_def.get("maximum", "")
363
+ if min_val != "" and max_val != "":
364
+ type_hint = f"number ({min_val}-{max_val})"
365
+ elif field_type == "boolean":
366
+ type_hint = "yes/no"
367
+ else:
368
+ type_hint = field_type
369
+
370
+ # Build field description
371
+ field_line = f"- **{field_name}**"
372
+ if type_hint and type_hint != "string":
373
+ field_line += f" ({type_hint})"
374
+ if description:
375
+ field_line += f": {description}"
376
+
377
+ lines.append(field_line)
378
+
379
+ lines.append("")
380
+ lines.append("Respond naturally in prose, addressing these elements where relevant.")
381
+
382
+ return "\n".join(lines)
383
+
384
+
306
385
  def _create_schema_wrapper(
307
386
  result_type: type[BaseModel], strip_description: bool = True
308
387
  ) -> type[BaseModel]:
@@ -462,23 +541,48 @@ async def create_agent(
462
541
  # agent_schema = load_agent_schema(context.agent_schema_uri)
463
542
  pass
464
543
 
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
- )
544
+ # Determine model: validate override against allowed list, fallback to context or settings
545
+ from rem.agentic.llm_provider_models import get_valid_model_or_default
546
+
547
+ default_model = context.default_model if context else settings.llm.default_model
548
+ model = get_valid_model_or_default(model_override, default_model)
549
+
550
+ # Extract schema fields using typed helpers
551
+ from ..schema import get_system_prompt, get_metadata
469
552
 
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", [])
553
+ if agent_schema:
554
+ system_prompt = get_system_prompt(agent_schema)
555
+ metadata = get_metadata(agent_schema)
556
+ mcp_server_configs = [s.model_dump() for s in metadata.mcp_servers] if hasattr(metadata, 'mcp_servers') else []
557
+ resource_configs = metadata.resources if hasattr(metadata, 'resources') else []
558
+
559
+ if metadata.system_prompt:
560
+ logger.debug("Using custom system_prompt from json_schema_extra")
561
+ else:
562
+ system_prompt = ""
563
+ metadata = None
564
+ mcp_server_configs = []
565
+ resource_configs = []
475
566
 
476
567
  # 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)
568
+ if metadata:
569
+ temperature = metadata.override_temperature if metadata.override_temperature is not None else settings.llm.default_temperature
570
+ max_iterations = metadata.override_max_iterations if metadata.override_max_iterations is not None else settings.llm.default_max_iterations
571
+ use_structured_output = metadata.structured_output
572
+ else:
573
+ temperature = settings.llm.default_temperature
574
+ max_iterations = settings.llm.default_max_iterations
575
+ use_structured_output = True
576
+
577
+ # Build list of tools - start with built-in tools
578
+ tools = _get_builtin_tools()
579
+
580
+ # Get agent name from metadata for logging
581
+ agent_name = metadata.name if metadata and hasattr(metadata, 'name') else "unknown"
479
582
 
480
583
  logger.info(
481
- f"Creating agent: model={model}, mcp_servers={len(mcp_server_configs)}, resources={len(resource_configs)}"
584
+ f"Creating agent '{agent_name}': model={model}, mcp_servers={len(mcp_server_configs)}, "
585
+ f"resources={len(resource_configs)}, builtin_tools={len(tools)}"
482
586
  )
483
587
 
484
588
  # Set agent resource attributes for OTEL (before creating agent)
@@ -487,8 +591,7 @@ async def create_agent(
487
591
 
488
592
  set_agent_resource_attributes(agent_schema=agent_schema)
489
593
 
490
- # Build list of tools from MCP server (in-process, no subprocess)
491
- tools = []
594
+ # Add tools from MCP server (in-process, no subprocess)
492
595
  if mcp_server_configs:
493
596
  for server_config in mcp_server_configs:
494
597
  server_type = server_config.get("type")
@@ -527,13 +630,21 @@ async def create_agent(
527
630
  pass
528
631
 
529
632
  # Create dynamic result_type from schema if not provided
633
+ # Note: use_structured_output is set earlier from metadata.structured_output
530
634
  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__}")
635
+ if use_structured_output:
636
+ # Pre-process schema for Qwen compatibility (strips min/max, sets additionalProperties=False)
637
+ # This ensures the generated Pydantic model doesn't have incompatible constraints
638
+ sanitized_schema = _prepare_schema_for_qwen(agent_schema)
639
+ result_type = _create_model_from_schema(sanitized_schema)
640
+ logger.debug(f"Created dynamic Pydantic model: {result_type.__name__}")
641
+ else:
642
+ # Convert properties to prompt guidance instead of structured output
643
+ # This informs the agent about expected response structure without forcing it
644
+ properties_prompt = _convert_properties_to_prompt(agent_schema.get("properties", {}))
645
+ if properties_prompt:
646
+ system_prompt = system_prompt + "\n\n" + properties_prompt
647
+ logger.debug("Structured output disabled - properties converted to prompt guidance")
537
648
 
538
649
  # Create agent with optional output_type for structured output and tools
539
650
  if result_type:
@@ -541,21 +652,30 @@ async def create_agent(
541
652
  wrapped_result_type = _create_schema_wrapper(
542
653
  result_type, strip_description=strip_model_description
543
654
  )
655
+ # Use InstrumentationSettings with version=3 to include agent name in span names
656
+ from pydantic_ai.models.instrumented import InstrumentationSettings
657
+ instrumentation = InstrumentationSettings(version=3) if settings.otel.enabled else False
658
+
544
659
  agent = Agent(
545
660
  model=model,
661
+ name=agent_name, # Used for OTEL span names (version 3: "invoke_agent {name}")
546
662
  system_prompt=system_prompt,
547
663
  output_type=wrapped_result_type,
548
664
  tools=tools,
549
- instrument=settings.otel.enabled, # Conditional OTEL instrumentation
665
+ instrument=instrumentation,
550
666
  model_settings={"temperature": temperature},
551
667
  retries=settings.llm.max_retries,
552
668
  )
553
669
  else:
670
+ from pydantic_ai.models.instrumented import InstrumentationSettings
671
+ instrumentation = InstrumentationSettings(version=3) if settings.otel.enabled else False
672
+
554
673
  agent = Agent(
555
674
  model=model,
675
+ name=agent_name, # Used for OTEL span names (version 3: "invoke_agent {name}")
556
676
  system_prompt=system_prompt,
557
677
  tools=tools,
558
- instrument=settings.otel.enabled,
678
+ instrument=instrumentation,
559
679
  model_settings={"temperature": temperature},
560
680
  retries=settings.llm.max_retries,
561
681
  )
rem/agentic/schema.py CHANGED
@@ -13,7 +13,7 @@ The schema protocol serves as:
13
13
  """
14
14
 
15
15
  from typing import Any, Literal
16
- from pydantic import BaseModel, Field
16
+ from pydantic import BaseModel, Field, field_validator
17
17
 
18
18
 
19
19
  class MCPToolReference(BaseModel):
@@ -23,11 +23,21 @@ class MCPToolReference(BaseModel):
23
23
  Tools are functions that agents can call during execution to
24
24
  interact with external systems, retrieve data, or perform actions.
25
25
 
26
- Example:
26
+ Two usage patterns:
27
+ 1. With mcp_servers config: Just declare name + description, tools loaded from MCP servers
28
+ 2. Explicit MCP server: Specify mcp_server to load tool from specific server
29
+
30
+ Example (declarative with mcp_servers):
31
+ {
32
+ "name": "search_rem",
33
+ "description": "Execute REM queries for entity lookup and search"
34
+ }
35
+
36
+ Example (explicit server):
27
37
  {
28
38
  "name": "lookup_entity",
29
39
  "mcp_server": "rem",
30
- "description": "Lookup entities by exact key with O(1) performance"
40
+ "description": "Lookup entities by exact key"
31
41
  }
32
42
  """
33
43
 
@@ -38,20 +48,20 @@ class MCPToolReference(BaseModel):
38
48
  )
39
49
  )
40
50
 
41
- mcp_server: str = Field(
51
+ mcp_server: str | None = Field(
52
+ default=None,
42
53
  description=(
43
- "MCP server identifier. Resolved via environment variable: "
44
- "MCP_SERVER_{NAME} or MCP__{NAME}__URL. "
45
- "Common values: 'rem' (REM knowledge graph), 'filesystem', 'web'."
54
+ "MCP server identifier (optional when using mcp_servers config). "
55
+ "If not specified, tool is expected from configured mcp_servers. "
56
+ "Resolved via environment variable: MCP_SERVER_{NAME} or MCP__{NAME}__URL."
46
57
  )
47
58
  )
48
59
 
49
60
  description: str | None = Field(
50
61
  default=None,
51
62
  description=(
52
- "Optional description override. If provided, replaces the tool's "
53
- "description from the MCP server in the agent's context. "
54
- "Use this to provide agent-specific guidance on tool usage."
63
+ "Tool description for the agent. Explains what the tool does "
64
+ "and when to use it. This is visible to the LLM."
55
65
  ),
56
66
  )
57
67
 
@@ -63,29 +73,90 @@ class MCPResourceReference(BaseModel):
63
73
  Resources are data sources that can be read by agents, such as
64
74
  knowledge graph entities, files, or API endpoints.
65
75
 
66
- Example:
76
+ Two formats supported:
77
+ 1. uri: Exact URI or URI with query params
78
+ 2. uri_pattern: Regex pattern for flexible matching
79
+
80
+ Example (exact URI):
81
+ {
82
+ "uri": "rem://schemas",
83
+ "name": "Agent Schemas",
84
+ "description": "List all available agent schemas"
85
+ }
86
+
87
+ Example (pattern):
67
88
  {
68
89
  "uri_pattern": "rem://resources/.*",
69
90
  "mcp_server": "rem"
70
91
  }
71
92
  """
72
93
 
73
- uri_pattern: str = Field(
94
+ # Support both exact URI and pattern
95
+ uri: str | None = Field(
96
+ default=None,
97
+ description=(
98
+ "Exact resource URI or URI with query parameters. "
99
+ "Examples: 'rem://schemas', 'rem://resources?category=drug.*'"
100
+ )
101
+ )
102
+
103
+ uri_pattern: str | None = Field(
104
+ default=None,
74
105
  description=(
75
106
  "Regex pattern matching resource URIs. "
76
- "Examples: "
77
- "'rem://resources/.*' (all resources), "
78
- "'rem://moments/.*' (all moments), "
79
- "'file:///data/.*' (local files). "
80
- "Supports full regex syntax for flexible matching."
107
+ "Examples: 'rem://resources/.*' (all resources). "
108
+ "Use uri for exact URIs, uri_pattern for regex matching."
109
+ )
110
+ )
111
+
112
+ name: str | None = Field(
113
+ default=None,
114
+ description="Human-readable name for the resource."
115
+ )
116
+
117
+ description: str | None = Field(
118
+ default=None,
119
+ description="Description of what the resource provides."
120
+ )
121
+
122
+ mcp_server: str | None = Field(
123
+ default=None,
124
+ description=(
125
+ "MCP server identifier (optional when using mcp_servers config). "
126
+ "Resolved via environment variable MCP_SERVER_{NAME}."
127
+ )
128
+ )
129
+
130
+
131
+ class MCPServerConfig(BaseModel):
132
+ """
133
+ MCP server configuration for in-process tool loading.
134
+
135
+ Example:
136
+ {
137
+ "type": "local",
138
+ "module": "rem.mcp_server",
139
+ "id": "rem-local"
140
+ }
141
+ """
142
+
143
+ type: Literal["local"] = Field(
144
+ default="local",
145
+ description="Server type. Currently only 'local' (in-process) is supported.",
146
+ )
147
+
148
+ module: str = Field(
149
+ description=(
150
+ "Python module path containing the MCP server. "
151
+ "The module must export an 'mcp' object that supports get_tools(). "
152
+ "Example: 'rem.mcp_server'"
81
153
  )
82
154
  )
83
155
 
84
- mcp_server: str = Field(
156
+ id: str = Field(
85
157
  description=(
86
- "MCP server identifier that provides these resources. "
87
- "Resolved via environment variable MCP_SERVER_{NAME}. "
88
- "The server must expose resources matching the uri_pattern."
158
+ "Server identifier for logging and debugging. "
159
+ "Example: 'rem-local'"
89
160
  )
90
161
  )
91
162
 
@@ -130,6 +201,37 @@ class AgentSchemaMetadata(BaseModel):
130
201
  ),
131
202
  )
132
203
 
204
+ # System prompt override (takes precedence over description when present)
205
+ system_prompt: str | None = Field(
206
+ default=None,
207
+ description=(
208
+ "Custom system prompt that overrides or extends the schema description. "
209
+ "When present, this is combined with the main schema.description field "
210
+ "to form the complete system prompt. Use this for detailed instructions "
211
+ "that you don't want in the public schema description."
212
+ ),
213
+ )
214
+
215
+ # Structured output toggle
216
+ structured_output: bool = Field(
217
+ default=True,
218
+ description=(
219
+ "Whether to enforce structured JSON output. "
220
+ "When False, the agent produces free-form text and schema properties "
221
+ "are converted to prompt guidance instead. Default: True (JSON output)."
222
+ ),
223
+ )
224
+
225
+ # MCP server configurations (for dynamic tool loading)
226
+ mcp_servers: list[MCPServerConfig] = Field(
227
+ default_factory=list,
228
+ description=(
229
+ "MCP server configurations for dynamic tool loading. "
230
+ "Servers are loaded in-process at agent creation time. "
231
+ "All tools from configured servers become available to the agent."
232
+ ),
233
+ )
234
+
133
235
  tools: list[MCPToolReference] = Field(
134
236
  default_factory=list,
135
237
  description=(
@@ -394,3 +496,238 @@ def create_agent_schema(
394
496
  json_schema_extra=metadata.model_dump(),
395
497
  **kwargs,
396
498
  )
499
+
500
+
501
+ # =============================================================================
502
+ # YAML and Database Serialization
503
+ # =============================================================================
504
+
505
+
506
+ def schema_to_dict(schema: AgentSchema, exclude_none: bool = True) -> dict[str, Any]:
507
+ """
508
+ Serialize AgentSchema to a dictionary suitable for YAML or database storage.
509
+
510
+ This produces the canonical format used in:
511
+ - YAML files (schemas/agents/*.yaml)
512
+ - Database spec column (schemas table)
513
+ - API responses
514
+
515
+ Args:
516
+ schema: AgentSchema instance to serialize
517
+ exclude_none: If True, omit None values from output
518
+
519
+ Returns:
520
+ Dictionary representation of the schema
521
+
522
+ Example:
523
+ >>> schema = AgentSchema(
524
+ ... description="System prompt...",
525
+ ... properties={"answer": {"type": "string"}},
526
+ ... json_schema_extra={"name": "my-agent", "structured_output": False}
527
+ ... )
528
+ >>> d = schema_to_dict(schema)
529
+ >>> d["json_schema_extra"]["name"]
530
+ "my-agent"
531
+ """
532
+ return schema.model_dump(exclude_none=exclude_none)
533
+
534
+
535
+ def schema_from_dict(data: dict[str, Any]) -> AgentSchema:
536
+ """
537
+ Deserialize a dictionary to AgentSchema.
538
+
539
+ This handles:
540
+ - YAML files loaded with yaml.safe_load()
541
+ - Database spec column (JSON)
542
+ - API request bodies
543
+
544
+ Args:
545
+ data: Dictionary containing schema data
546
+
547
+ Returns:
548
+ Validated AgentSchema instance
549
+
550
+ Raises:
551
+ ValidationError: If data doesn't match schema structure
552
+
553
+ Example:
554
+ >>> data = {"type": "object", "description": "...", "properties": {}, "json_schema_extra": {"name": "test"}}
555
+ >>> schema = schema_from_dict(data)
556
+ >>> schema.json_schema_extra["name"]
557
+ "test"
558
+ """
559
+ return AgentSchema.model_validate(data)
560
+
561
+
562
+ def schema_to_yaml(schema: AgentSchema) -> str:
563
+ """
564
+ Serialize AgentSchema to YAML string.
565
+
566
+ The output format matches the canonical schema file format:
567
+ ```yaml
568
+ type: object
569
+ description: |
570
+ System prompt here...
571
+ properties:
572
+ answer:
573
+ type: string
574
+ json_schema_extra:
575
+ name: my-agent
576
+ system_prompt: |
577
+ Extended prompt here...
578
+ ```
579
+
580
+ Args:
581
+ schema: AgentSchema instance to serialize
582
+
583
+ Returns:
584
+ YAML string representation
585
+
586
+ Example:
587
+ >>> schema = create_agent_schema(
588
+ ... description="You are a test agent",
589
+ ... properties={"answer": {"type": "string"}},
590
+ ... required=["answer"],
591
+ ... name="test-agent"
592
+ ... )
593
+ >>> yaml_str = schema_to_yaml(schema)
594
+ >>> "test-agent" in yaml_str
595
+ True
596
+ """
597
+ import yaml
598
+
599
+ return yaml.dump(
600
+ schema_to_dict(schema),
601
+ default_flow_style=False,
602
+ allow_unicode=True,
603
+ sort_keys=False,
604
+ )
605
+
606
+
607
+ def schema_from_yaml(yaml_content: str) -> AgentSchema:
608
+ """
609
+ Deserialize YAML string to AgentSchema.
610
+
611
+ Args:
612
+ yaml_content: YAML string containing schema definition
613
+
614
+ Returns:
615
+ Validated AgentSchema instance
616
+
617
+ Raises:
618
+ yaml.YAMLError: If YAML parsing fails
619
+ ValidationError: If schema structure is invalid
620
+
621
+ Example:
622
+ >>> yaml_str = '''
623
+ ... type: object
624
+ ... description: Test agent
625
+ ... properties:
626
+ ... answer:
627
+ ... type: string
628
+ ... json_schema_extra:
629
+ ... name: test
630
+ ... '''
631
+ >>> schema = schema_from_yaml(yaml_str)
632
+ >>> schema.json_schema_extra["name"]
633
+ "test"
634
+ """
635
+ import yaml
636
+
637
+ data = yaml.safe_load(yaml_content)
638
+ return schema_from_dict(data)
639
+
640
+
641
+ def schema_from_yaml_file(file_path: str) -> AgentSchema:
642
+ """
643
+ Load AgentSchema from a YAML file.
644
+
645
+ Args:
646
+ file_path: Path to YAML file
647
+
648
+ Returns:
649
+ Validated AgentSchema instance
650
+
651
+ Raises:
652
+ FileNotFoundError: If file doesn't exist
653
+ yaml.YAMLError: If YAML parsing fails
654
+ ValidationError: If schema structure is invalid
655
+
656
+ Example:
657
+ >>> schema = schema_from_yaml_file("schemas/agents/rem.yaml")
658
+ >>> schema.json_schema_extra["name"]
659
+ "rem"
660
+ """
661
+ with open(file_path, "r") as f:
662
+ return schema_from_yaml(f.read())
663
+
664
+
665
+ def get_system_prompt(schema: AgentSchema | dict[str, Any]) -> str:
666
+ """
667
+ Extract the complete system prompt from a schema.
668
+
669
+ Combines:
670
+ 1. schema.description (base system prompt / public description)
671
+ 2. json_schema_extra.system_prompt (extended instructions if present)
672
+
673
+ Args:
674
+ schema: AgentSchema instance or raw dict
675
+
676
+ Returns:
677
+ Complete system prompt string
678
+
679
+ Example:
680
+ >>> schema = AgentSchema(
681
+ ... description="Base description",
682
+ ... properties={},
683
+ ... json_schema_extra={"name": "test", "system_prompt": "Extended instructions"}
684
+ ... )
685
+ >>> prompt = get_system_prompt(schema)
686
+ >>> "Base description" in prompt and "Extended instructions" in prompt
687
+ True
688
+ """
689
+ if isinstance(schema, dict):
690
+ base = schema.get("description", "")
691
+ extra = schema.get("json_schema_extra", {})
692
+ custom = extra.get("system_prompt") if isinstance(extra, dict) else None
693
+ else:
694
+ base = schema.description
695
+ extra = schema.json_schema_extra
696
+ if isinstance(extra, dict):
697
+ custom = extra.get("system_prompt")
698
+ elif isinstance(extra, AgentSchemaMetadata):
699
+ custom = extra.system_prompt
700
+ else:
701
+ custom = None
702
+
703
+ if custom:
704
+ return f"{base}\n\n{custom}" if base else custom
705
+ return base
706
+
707
+
708
+ def get_metadata(schema: AgentSchema | dict[str, Any]) -> AgentSchemaMetadata:
709
+ """
710
+ Extract and validate metadata from a schema.
711
+
712
+ Args:
713
+ schema: AgentSchema instance or raw dict
714
+
715
+ Returns:
716
+ Validated AgentSchemaMetadata instance
717
+
718
+ Example:
719
+ >>> schema = {"json_schema_extra": {"name": "test", "system_prompt": "hello"}}
720
+ >>> meta = get_metadata(schema)
721
+ >>> meta.name
722
+ "test"
723
+ >>> meta.system_prompt
724
+ "hello"
725
+ """
726
+ if isinstance(schema, dict):
727
+ extra = schema.get("json_schema_extra", {})
728
+ else:
729
+ extra = schema.json_schema_extra
730
+
731
+ if isinstance(extra, AgentSchemaMetadata):
732
+ return extra
733
+ return AgentSchemaMetadata.model_validate(extra)
@@ -162,10 +162,10 @@ async def search_rem_tool(
162
162
  return {"status": "error", "error": f"Unknown query_type: {query_type}"}
163
163
 
164
164
  # Execute query
165
- logger.info(f"Executing REM query: {query_type} for user {user_id}")
165
+ logger.debug(f"Executing REM query: {query_type} for user {user_id}")
166
166
  result = await rem_service.execute_query(query)
167
167
 
168
- logger.info(f"Query completed: {query_type}")
168
+ logger.debug(f"Query completed: {query_type}")
169
169
  return {
170
170
  "status": "success",
171
171
  "query_type": query_type,
@@ -212,7 +212,7 @@ async def ingest_file_tool(
212
212
  is_local_server=is_local_server,
213
213
  )
214
214
 
215
- logger.info(
215
+ logger.debug(
216
216
  f"File ingestion complete: {result['file_name']} "
217
217
  f"(status: {result['processing_status']}, "
218
218
  f"resources: {result['resources_created']})"