remdb 0.3.14__py3-none-any.whl → 0.3.157__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 (112) hide show
  1. rem/agentic/README.md +76 -0
  2. rem/agentic/__init__.py +15 -0
  3. rem/agentic/agents/__init__.py +32 -2
  4. rem/agentic/agents/agent_manager.py +310 -0
  5. rem/agentic/agents/sse_simulator.py +502 -0
  6. rem/agentic/context.py +51 -27
  7. rem/agentic/context_builder.py +5 -3
  8. rem/agentic/llm_provider_models.py +301 -0
  9. rem/agentic/mcp/tool_wrapper.py +155 -18
  10. rem/agentic/otel/setup.py +93 -4
  11. rem/agentic/providers/phoenix.py +371 -108
  12. rem/agentic/providers/pydantic_ai.py +280 -57
  13. rem/agentic/schema.py +361 -21
  14. rem/agentic/tools/rem_tools.py +3 -3
  15. rem/api/README.md +215 -1
  16. rem/api/deps.py +255 -0
  17. rem/api/main.py +132 -40
  18. rem/api/mcp_router/resources.py +1 -1
  19. rem/api/mcp_router/server.py +28 -5
  20. rem/api/mcp_router/tools.py +555 -7
  21. rem/api/routers/admin.py +494 -0
  22. rem/api/routers/auth.py +278 -4
  23. rem/api/routers/chat/completions.py +402 -20
  24. rem/api/routers/chat/models.py +88 -10
  25. rem/api/routers/chat/otel_utils.py +33 -0
  26. rem/api/routers/chat/sse_events.py +542 -0
  27. rem/api/routers/chat/streaming.py +697 -45
  28. rem/api/routers/dev.py +81 -0
  29. rem/api/routers/feedback.py +268 -0
  30. rem/api/routers/messages.py +473 -0
  31. rem/api/routers/models.py +78 -0
  32. rem/api/routers/query.py +360 -0
  33. rem/api/routers/shared_sessions.py +406 -0
  34. rem/auth/__init__.py +13 -3
  35. rem/auth/middleware.py +186 -22
  36. rem/auth/providers/__init__.py +4 -1
  37. rem/auth/providers/email.py +215 -0
  38. rem/cli/commands/README.md +237 -64
  39. rem/cli/commands/cluster.py +1808 -0
  40. rem/cli/commands/configure.py +4 -7
  41. rem/cli/commands/db.py +386 -143
  42. rem/cli/commands/experiments.py +468 -76
  43. rem/cli/commands/process.py +14 -8
  44. rem/cli/commands/schema.py +97 -50
  45. rem/cli/commands/session.py +336 -0
  46. rem/cli/dreaming.py +2 -2
  47. rem/cli/main.py +29 -6
  48. rem/config.py +10 -3
  49. rem/models/core/core_model.py +7 -1
  50. rem/models/core/experiment.py +58 -14
  51. rem/models/core/rem_query.py +5 -2
  52. rem/models/entities/__init__.py +25 -0
  53. rem/models/entities/domain_resource.py +38 -0
  54. rem/models/entities/feedback.py +123 -0
  55. rem/models/entities/message.py +30 -1
  56. rem/models/entities/ontology.py +1 -1
  57. rem/models/entities/ontology_config.py +1 -1
  58. rem/models/entities/session.py +83 -0
  59. rem/models/entities/shared_session.py +180 -0
  60. rem/models/entities/subscriber.py +175 -0
  61. rem/models/entities/user.py +1 -0
  62. rem/registry.py +10 -4
  63. rem/schemas/agents/core/agent-builder.yaml +134 -0
  64. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  65. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  66. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  67. rem/schemas/agents/rem.yaml +7 -3
  68. rem/services/__init__.py +3 -1
  69. rem/services/content/service.py +92 -19
  70. rem/services/email/__init__.py +10 -0
  71. rem/services/email/service.py +459 -0
  72. rem/services/email/templates.py +360 -0
  73. rem/services/embeddings/api.py +4 -4
  74. rem/services/embeddings/worker.py +16 -16
  75. rem/services/phoenix/client.py +154 -14
  76. rem/services/postgres/README.md +197 -15
  77. rem/services/postgres/__init__.py +2 -1
  78. rem/services/postgres/diff_service.py +547 -0
  79. rem/services/postgres/pydantic_to_sqlalchemy.py +470 -140
  80. rem/services/postgres/repository.py +132 -0
  81. rem/services/postgres/schema_generator.py +205 -4
  82. rem/services/postgres/service.py +6 -6
  83. rem/services/rem/parser.py +44 -9
  84. rem/services/rem/service.py +36 -2
  85. rem/services/session/compression.py +137 -51
  86. rem/services/session/reload.py +15 -8
  87. rem/settings.py +515 -27
  88. rem/sql/background_indexes.sql +21 -16
  89. rem/sql/migrations/001_install.sql +387 -54
  90. rem/sql/migrations/002_install_models.sql +2304 -377
  91. rem/sql/migrations/003_optional_extensions.sql +326 -0
  92. rem/sql/migrations/004_cache_system.sql +548 -0
  93. rem/sql/migrations/005_schema_update.sql +145 -0
  94. rem/utils/README.md +45 -0
  95. rem/utils/__init__.py +18 -0
  96. rem/utils/date_utils.py +2 -2
  97. rem/utils/files.py +157 -1
  98. rem/utils/model_helpers.py +156 -1
  99. rem/utils/schema_loader.py +220 -22
  100. rem/utils/sql_paths.py +146 -0
  101. rem/utils/sql_types.py +3 -1
  102. rem/utils/vision.py +1 -1
  103. rem/workers/__init__.py +3 -1
  104. rem/workers/db_listener.py +579 -0
  105. rem/workers/unlogged_maintainer.py +463 -0
  106. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/METADATA +340 -229
  107. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/RECORD +109 -80
  108. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/WHEEL +1 -1
  109. rem/sql/002_install_models.sql +0 -1068
  110. rem/sql/install_models.sql +0 -1051
  111. rem/sql/migrations/003_seed_default_user.sql +0 -48
  112. {remdb-0.3.14.dist-info → remdb-0.3.157.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,80 @@ 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
469
546
 
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", [])
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
552
+
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') and 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 = []
566
+
567
+ # Auto-detect local MCP server if not explicitly configured
568
+ # This makes mcp_servers config optional - agents get tools automatically
569
+ if not mcp_server_configs:
570
+ import importlib
571
+ import os
572
+ import sys
573
+
574
+ # Ensure current working directory is in sys.path for local imports
575
+ cwd = os.getcwd()
576
+ if cwd not in sys.path:
577
+ sys.path.insert(0, cwd)
578
+
579
+ # Try common local MCP server module paths first
580
+ auto_detect_modules = [
581
+ "tools.mcp_server", # Convention: tools/mcp_server.py
582
+ "mcp_server", # Alternative: mcp_server.py in root
583
+ ]
584
+ for module_path in auto_detect_modules:
585
+ try:
586
+ mcp_module = importlib.import_module(module_path)
587
+ if hasattr(mcp_module, "mcp"):
588
+ logger.info(f"Auto-detected local MCP server: {module_path}")
589
+ mcp_server_configs = [{"type": "local", "module": module_path, "id": "auto-detected"}]
590
+ break
591
+ except ImportError:
592
+ continue
593
+
594
+ # Fall back to REM's default MCP server if no local server found
595
+ if not mcp_server_configs:
596
+ logger.debug("No local MCP server found, using REM default")
597
+ mcp_server_configs = [{"type": "local", "module": "rem.mcp_server", "id": "rem"}]
475
598
 
476
599
  # 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)
600
+ if metadata:
601
+ temperature = metadata.override_temperature if metadata.override_temperature is not None else settings.llm.default_temperature
602
+ max_iterations = metadata.override_max_iterations if metadata.override_max_iterations is not None else settings.llm.default_max_iterations
603
+ use_structured_output = metadata.structured_output
604
+ else:
605
+ temperature = settings.llm.default_temperature
606
+ max_iterations = settings.llm.default_max_iterations
607
+ use_structured_output = True
608
+
609
+ # Build list of tools - start with built-in tools
610
+ tools = _get_builtin_tools()
611
+
612
+ # Get agent name from metadata for logging
613
+ agent_name = metadata.name if metadata and hasattr(metadata, 'name') else "unknown"
479
614
 
480
615
  logger.info(
481
- f"Creating agent: model={model}, mcp_servers={len(mcp_server_configs)}, resources={len(resource_configs)}"
616
+ f"Creating agent '{agent_name}': model={model}, mcp_servers={len(mcp_server_configs)}, "
617
+ f"resources={len(resource_configs)}, builtin_tools={len(tools)}"
482
618
  )
483
619
 
484
620
  # Set agent resource attributes for OTEL (before creating agent)
@@ -487,53 +623,131 @@ async def create_agent(
487
623
 
488
624
  set_agent_resource_attributes(agent_schema=agent_schema)
489
625
 
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
-
626
+ # Extract schema metadata for search_rem tool description suffix
627
+ # This allows entity schemas to add context-specific notes to the search_rem tool
628
+ search_rem_suffix = None
629
+ if metadata:
630
+ # Check for default_search_table in metadata (set by entity schemas)
631
+ extra = agent_schema.get("json_schema_extra", {}) if agent_schema else {}
632
+ default_table = extra.get("default_search_table")
633
+ has_embeddings = extra.get("has_embeddings", False)
634
+
635
+ if default_table:
636
+ # Build description suffix for search_rem
637
+ search_rem_suffix = f"\n\nFor this schema, use `search_rem` to query `{default_table}`. "
638
+ if has_embeddings:
639
+ search_rem_suffix += f"SEARCH works well on {default_table} (has embeddings). "
640
+ search_rem_suffix += f"Example: `SEARCH \"your query\" FROM {default_table} LIMIT 10`"
641
+
642
+ # Add tools from MCP server (in-process, no subprocess)
643
+ # Track loaded MCP servers for resource resolution
644
+ loaded_mcp_server = None
645
+
646
+ for server_config in mcp_server_configs:
647
+ server_type = server_config.get("type")
648
+ server_id = server_config.get("id", "mcp-server")
649
+
650
+ if server_type == "local":
651
+ # Import MCP server directly (in-process)
652
+ module_path = server_config.get("module", "rem.mcp_server")
653
+
654
+ try:
655
+ # Dynamic import of MCP server module
656
+ import importlib
657
+ mcp_module = importlib.import_module(module_path)
658
+ mcp_server = mcp_module.mcp
659
+
660
+ # Store the loaded server for resource resolution
661
+ loaded_mcp_server = mcp_server
662
+
663
+ # Extract tools from MCP server (get_tools is async)
664
+ from ..mcp.tool_wrapper import create_mcp_tool_wrapper
665
+
666
+ # Await async get_tools() call
667
+ mcp_tools_dict = await mcp_server.get_tools()
668
+
669
+ for tool_name, tool_func in mcp_tools_dict.items():
670
+ # Add description suffix to search_rem tool if schema specifies a default table
671
+ tool_suffix = search_rem_suffix if tool_name == "search_rem" else None
672
+
673
+ wrapped_tool = create_mcp_tool_wrapper(
674
+ tool_name,
675
+ tool_func,
676
+ user_id=context.user_id if context else None,
677
+ description_suffix=tool_suffix,
678
+ )
679
+ tools.append(wrapped_tool)
680
+ logger.debug(f"Loaded MCP tool: {tool_name}" + (" (with schema suffix)" if tool_suffix else ""))
681
+
682
+ logger.info(f"Loaded {len(mcp_tools_dict)} tools from MCP server: {server_id} (in-process)")
683
+
684
+ except Exception as e:
685
+ logger.error(f"Failed to load MCP server {server_id}: {e}", exc_info=True)
686
+ else:
687
+ logger.warning(f"Unsupported MCP server type: {server_type}")
688
+
689
+ # Convert resources to tools (MCP convenience syntax)
690
+ # Resources declared in agent YAML become callable tools - eliminates
691
+ # the artificial MCP distinction between tools and resources
692
+ #
693
+ # Supports both concrete and template URIs:
694
+ # - Concrete: "rem://schemas" -> no-param tool
695
+ # - Template: "patient-profile://field/{field_key}" -> tool with field_key param
696
+ from ..mcp.tool_wrapper import create_resource_tool
697
+
698
+ # Collect all resource URIs from both resources section AND tools section
699
+ resource_uris = []
700
+
701
+ # From resources section (legacy format)
525
702
  if resource_configs:
526
- # TODO: Convert resources to tools (MCP convenience syntax)
527
- pass
703
+ for resource_config in resource_configs:
704
+ if hasattr(resource_config, 'uri'):
705
+ uri = resource_config.uri
706
+ usage = resource_config.description or ""
707
+ else:
708
+ uri = resource_config.get("uri", "")
709
+ usage = resource_config.get("description", "")
710
+ if uri:
711
+ resource_uris.append((uri, usage))
712
+
713
+ # From tools section - detect URIs (anything with ://)
714
+ # This allows unified syntax: resources as tools
715
+ tool_configs = metadata.tools if metadata and hasattr(metadata, 'tools') else []
716
+ for tool_config in tool_configs:
717
+ if hasattr(tool_config, 'name'):
718
+ tool_name = tool_config.name
719
+ tool_desc = tool_config.description or ""
720
+ else:
721
+ tool_name = tool_config.get("name", "")
722
+ tool_desc = tool_config.get("description", "")
723
+
724
+ # Auto-detect resource URIs (anything with :// scheme)
725
+ if "://" in tool_name:
726
+ resource_uris.append((tool_name, tool_desc))
727
+
728
+ # Create tools from collected resource URIs
729
+ # Pass the loaded MCP server so resources can be resolved from it
730
+ for uri, usage in resource_uris:
731
+ resource_tool = create_resource_tool(uri, usage, mcp_server=loaded_mcp_server)
732
+ tools.append(resource_tool)
733
+ logger.debug(f"Loaded resource as tool: {uri}")
528
734
 
529
735
  # Create dynamic result_type from schema if not provided
736
+ # Note: use_structured_output is set earlier from metadata.structured_output
530
737
  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__}")
738
+ if use_structured_output:
739
+ # Pre-process schema for Qwen compatibility (strips min/max, sets additionalProperties=False)
740
+ # This ensures the generated Pydantic model doesn't have incompatible constraints
741
+ sanitized_schema = _prepare_schema_for_qwen(agent_schema)
742
+ result_type = _create_model_from_schema(sanitized_schema)
743
+ logger.debug(f"Created dynamic Pydantic model: {result_type.__name__}")
744
+ else:
745
+ # Convert properties to prompt guidance instead of structured output
746
+ # This informs the agent about expected response structure without forcing it
747
+ properties_prompt = _convert_properties_to_prompt(agent_schema.get("properties", {}))
748
+ if properties_prompt:
749
+ system_prompt = system_prompt + "\n\n" + properties_prompt
750
+ logger.debug("Structured output disabled - properties converted to prompt guidance")
537
751
 
538
752
  # Create agent with optional output_type for structured output and tools
539
753
  if result_type:
@@ -541,21 +755,30 @@ async def create_agent(
541
755
  wrapped_result_type = _create_schema_wrapper(
542
756
  result_type, strip_description=strip_model_description
543
757
  )
758
+ # Use InstrumentationSettings with version=3 to include agent name in span names
759
+ from pydantic_ai.models.instrumented import InstrumentationSettings
760
+ instrumentation = InstrumentationSettings(version=3) if settings.otel.enabled else False
761
+
544
762
  agent = Agent(
545
763
  model=model,
764
+ name=agent_name, # Used for OTEL span names (version 3: "invoke_agent {name}")
546
765
  system_prompt=system_prompt,
547
766
  output_type=wrapped_result_type,
548
767
  tools=tools,
549
- instrument=settings.otel.enabled, # Conditional OTEL instrumentation
768
+ instrument=instrumentation,
550
769
  model_settings={"temperature": temperature},
551
770
  retries=settings.llm.max_retries,
552
771
  )
553
772
  else:
773
+ from pydantic_ai.models.instrumented import InstrumentationSettings
774
+ instrumentation = InstrumentationSettings(version=3) if settings.otel.enabled else False
775
+
554
776
  agent = Agent(
555
777
  model=model,
778
+ name=agent_name, # Used for OTEL span names (version 3: "invoke_agent {name}")
556
779
  system_prompt=system_prompt,
557
780
  tools=tools,
558
- instrument=settings.otel.enabled,
781
+ instrument=instrumentation,
559
782
  model_settings={"temperature": temperature},
560
783
  retries=settings.llm.max_retries,
561
784
  )