remdb 0.3.7__py3-none-any.whl → 0.3.133__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.
- rem/__init__.py +129 -2
- rem/agentic/README.md +76 -0
- rem/agentic/__init__.py +15 -0
- rem/agentic/agents/__init__.py +16 -2
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +51 -25
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/tool_wrapper.py +112 -17
- rem/agentic/otel/setup.py +93 -4
- rem/agentic/providers/phoenix.py +314 -132
- rem/agentic/providers/pydantic_ai.py +215 -26
- rem/agentic/schema.py +361 -21
- rem/agentic/tools/rem_tools.py +3 -3
- rem/api/README.md +238 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +154 -37
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +26 -5
- rem/api/mcp_router/tools.py +465 -7
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +494 -0
- rem/api/routers/auth.py +124 -0
- rem/api/routers/chat/completions.py +402 -20
- rem/api/routers/chat/models.py +88 -10
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +542 -0
- rem/api/routers/chat/streaming.py +642 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +268 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/query.py +360 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/middleware.py +126 -27
- rem/cli/commands/README.md +237 -64
- rem/cli/commands/ask.py +13 -10
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +5 -6
- rem/cli/commands/db.py +396 -139
- rem/cli/commands/experiments.py +469 -74
- rem/cli/commands/process.py +22 -15
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +97 -50
- rem/cli/main.py +29 -6
- rem/config.py +10 -3
- rem/models/core/core_model.py +7 -1
- rem/models/core/experiment.py +54 -0
- rem/models/core/rem_query.py +5 -2
- rem/models/entities/__init__.py +21 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/message.py +30 -1
- rem/models/entities/session.py +83 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/user.py +10 -3
- rem/registry.py +373 -0
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/content/providers.py +92 -133
- rem/services/content/service.py +92 -20
- rem/services/dreaming/affinity_service.py +2 -16
- rem/services/dreaming/moment_service.py +2 -15
- rem/services/embeddings/api.py +24 -17
- rem/services/embeddings/worker.py +16 -16
- rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
- rem/services/phoenix/client.py +302 -28
- rem/services/postgres/README.md +159 -15
- rem/services/postgres/__init__.py +2 -1
- rem/services/postgres/diff_service.py +531 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +291 -9
- rem/services/postgres/service.py +6 -6
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +14 -0
- rem/services/rem/parser.py +44 -9
- rem/services/rem/service.py +36 -2
- rem/services/session/compression.py +24 -1
- rem/services/session/reload.py +1 -1
- rem/services/user_service.py +98 -0
- rem/settings.py +399 -29
- rem/sql/background_indexes.sql +21 -16
- rem/sql/migrations/001_install.sql +387 -54
- rem/sql/migrations/002_install_models.sql +2320 -393
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/utils/__init__.py +18 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/embeddings.py +17 -4
- rem/utils/files.py +167 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +156 -1
- rem/utils/schema_loader.py +282 -35
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +3 -1
- rem/utils/vision.py +9 -14
- rem/workers/README.md +14 -14
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/METADATA +460 -303
- {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/RECORD +105 -74
- {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1038
- {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/entry_points.txt +0 -0
rem/api/mcp_router/tools.py
CHANGED
|
@@ -15,6 +15,9 @@ Available Tools:
|
|
|
15
15
|
- ask_rem_agent: Natural language to REM query conversion via agent
|
|
16
16
|
- ingest_into_rem: Full file ingestion pipeline (read + store + parse + chunk)
|
|
17
17
|
- read_resource: Access MCP resources (for Claude Desktop compatibility)
|
|
18
|
+
- register_metadata: Register response metadata for SSE MetadataEvent
|
|
19
|
+
- list_schema: List all schemas (tables, agents) in the database with row counts
|
|
20
|
+
- get_schema: Get detailed schema for a table (columns, types, indexes)
|
|
18
21
|
"""
|
|
19
22
|
|
|
20
23
|
from functools import wraps
|
|
@@ -53,7 +56,7 @@ def init_services(postgres_service: PostgresService, rem_service: RemService):
|
|
|
53
56
|
"""
|
|
54
57
|
_service_cache["postgres"] = postgres_service
|
|
55
58
|
_service_cache["rem"] = rem_service
|
|
56
|
-
logger.
|
|
59
|
+
logger.debug("MCP tools initialized with service instances")
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
async def get_rem_service() -> RemService:
|
|
@@ -79,7 +82,7 @@ async def get_rem_service() -> RemService:
|
|
|
79
82
|
_service_cache["postgres"] = postgres_service
|
|
80
83
|
_service_cache["rem"] = rem_service
|
|
81
84
|
|
|
82
|
-
logger.
|
|
85
|
+
logger.debug("MCP tools: lazy initialized services")
|
|
83
86
|
return rem_service
|
|
84
87
|
|
|
85
88
|
|
|
@@ -399,14 +402,14 @@ async def ask_rem_agent(
|
|
|
399
402
|
)
|
|
400
403
|
|
|
401
404
|
# Run agent (errors handled by decorator)
|
|
402
|
-
logger.
|
|
405
|
+
logger.debug(f"Running ask_rem agent for query: {query[:100]}...")
|
|
403
406
|
result = await agent_runtime.run(query)
|
|
404
407
|
|
|
405
408
|
# Extract output
|
|
406
409
|
from rem.agentic.serialization import serialize_agent_result
|
|
407
410
|
query_output = serialize_agent_result(result.output)
|
|
408
411
|
|
|
409
|
-
logger.
|
|
412
|
+
logger.debug("Agent execution completed successfully")
|
|
410
413
|
|
|
411
414
|
return {
|
|
412
415
|
"response": str(result.output),
|
|
@@ -422,6 +425,7 @@ async def ingest_into_rem(
|
|
|
422
425
|
tags: list[str] | None = None,
|
|
423
426
|
is_local_server: bool = False,
|
|
424
427
|
user_id: str | None = None,
|
|
428
|
+
resource_type: str | None = None,
|
|
425
429
|
) -> dict[str, Any]:
|
|
426
430
|
"""
|
|
427
431
|
Ingest file into REM, creating searchable resources and embeddings.
|
|
@@ -448,6 +452,11 @@ async def ingest_into_rem(
|
|
|
448
452
|
tags: Optional tags for file
|
|
449
453
|
is_local_server: True if running as local/stdio MCP server
|
|
450
454
|
user_id: Optional user identifier (defaults to authenticated user or "default")
|
|
455
|
+
resource_type: Optional resource type for storing chunks (case-insensitive).
|
|
456
|
+
Supports flexible naming:
|
|
457
|
+
- "resource", "resources", "Resource" → Resource (default)
|
|
458
|
+
- "domain-resource", "domain_resource", "DomainResource",
|
|
459
|
+
"domain-resources" → DomainResource (curated internal knowledge)
|
|
451
460
|
|
|
452
461
|
Returns:
|
|
453
462
|
Dict with:
|
|
@@ -478,6 +487,13 @@ async def ingest_into_rem(
|
|
|
478
487
|
file_uri="https://example.com/whitepaper.pdf",
|
|
479
488
|
tags=["research", "whitepaper"]
|
|
480
489
|
)
|
|
490
|
+
|
|
491
|
+
# Ingest as curated domain knowledge
|
|
492
|
+
ingest_into_rem(
|
|
493
|
+
file_uri="s3://bucket/internal/procedures.pdf",
|
|
494
|
+
resource_type="domain-resource",
|
|
495
|
+
category="procedures"
|
|
496
|
+
)
|
|
481
497
|
"""
|
|
482
498
|
from ...services.content import ContentService
|
|
483
499
|
|
|
@@ -493,9 +509,10 @@ async def ingest_into_rem(
|
|
|
493
509
|
category=category,
|
|
494
510
|
tags=tags,
|
|
495
511
|
is_local_server=is_local_server,
|
|
512
|
+
resource_type=resource_type,
|
|
496
513
|
)
|
|
497
514
|
|
|
498
|
-
logger.
|
|
515
|
+
logger.debug(
|
|
499
516
|
f"MCP ingestion complete: {result['file_name']} "
|
|
500
517
|
f"(status: {result['processing_status']}, "
|
|
501
518
|
f"resources: {result['resources_created']})"
|
|
@@ -550,7 +567,7 @@ async def read_resource(uri: str) -> dict[str, Any]:
|
|
|
550
567
|
# Check system status
|
|
551
568
|
read_resource(uri="rem://status")
|
|
552
569
|
"""
|
|
553
|
-
logger.
|
|
570
|
+
logger.debug(f"Reading resource: {uri}")
|
|
554
571
|
|
|
555
572
|
# Import here to avoid circular dependency
|
|
556
573
|
from .resources import load_resource
|
|
@@ -558,7 +575,7 @@ async def read_resource(uri: str) -> dict[str, Any]:
|
|
|
558
575
|
# Load resource using the existing resource handler (errors handled by decorator)
|
|
559
576
|
result = await load_resource(uri)
|
|
560
577
|
|
|
561
|
-
logger.
|
|
578
|
+
logger.debug(f"Resource loaded successfully: {uri}")
|
|
562
579
|
|
|
563
580
|
# If result is already a dict, return it
|
|
564
581
|
if isinstance(result, dict):
|
|
@@ -582,3 +599,444 @@ async def read_resource(uri: str) -> dict[str, Any]:
|
|
|
582
599
|
"uri": uri,
|
|
583
600
|
"data": {"content": result},
|
|
584
601
|
}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
async def register_metadata(
|
|
605
|
+
confidence: float | None = None,
|
|
606
|
+
references: list[str] | None = None,
|
|
607
|
+
sources: list[str] | None = None,
|
|
608
|
+
flags: list[str] | None = None,
|
|
609
|
+
# Session naming
|
|
610
|
+
session_name: str | None = None,
|
|
611
|
+
# Risk assessment fields (used by specialized agents)
|
|
612
|
+
risk_level: str | None = None,
|
|
613
|
+
risk_score: int | None = None,
|
|
614
|
+
risk_reasoning: str | None = None,
|
|
615
|
+
recommended_action: str | None = None,
|
|
616
|
+
# Generic extension - any additional key-value pairs
|
|
617
|
+
extra: dict[str, Any] | None = None,
|
|
618
|
+
) -> dict[str, Any]:
|
|
619
|
+
"""
|
|
620
|
+
Register response metadata to be emitted as an SSE MetadataEvent.
|
|
621
|
+
|
|
622
|
+
Call this tool BEFORE generating your final response to provide structured
|
|
623
|
+
metadata that will be sent to the client alongside your natural language output.
|
|
624
|
+
This allows you to stream conversational responses while still providing
|
|
625
|
+
machine-readable confidence scores, references, and other metadata.
|
|
626
|
+
|
|
627
|
+
**Design Pattern**: Agents can call this once before their final response to
|
|
628
|
+
register metadata that the streaming layer will emit as a MetadataEvent.
|
|
629
|
+
This decouples structured metadata from the response format.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
confidence: Confidence score (0.0-1.0) for the response quality.
|
|
633
|
+
- 0.9-1.0: High confidence, answer is well-supported
|
|
634
|
+
- 0.7-0.9: Medium confidence, some uncertainty
|
|
635
|
+
- 0.5-0.7: Low confidence, significant gaps
|
|
636
|
+
- <0.5: Very uncertain, may need clarification
|
|
637
|
+
references: List of reference identifiers (file paths, document IDs,
|
|
638
|
+
entity labels) that support the response.
|
|
639
|
+
sources: List of source descriptions (e.g., "REM database",
|
|
640
|
+
"search results", "user context").
|
|
641
|
+
flags: Optional flags for the response (e.g., "needs_review",
|
|
642
|
+
"uncertain", "incomplete", "crisis_alert").
|
|
643
|
+
|
|
644
|
+
session_name: Short 1-3 phrase name describing the session topic.
|
|
645
|
+
Used by the UI to label conversations in the sidebar.
|
|
646
|
+
Examples: "Prescription Drug Questions", "AWS Setup Help",
|
|
647
|
+
"Python Code Review", "Travel Planning".
|
|
648
|
+
|
|
649
|
+
risk_level: Risk level indicator (e.g., "green", "orange", "red").
|
|
650
|
+
Used by mental health agents for C-SSRS style assessment.
|
|
651
|
+
risk_score: Numeric risk score (e.g., 0-6 for C-SSRS).
|
|
652
|
+
risk_reasoning: Brief explanation of risk assessment.
|
|
653
|
+
recommended_action: Suggested next steps based on assessment.
|
|
654
|
+
|
|
655
|
+
extra: Dict of arbitrary additional metadata. Use this for any
|
|
656
|
+
domain-specific fields not covered by the standard parameters.
|
|
657
|
+
Example: {"topics_detected": ["anxiety", "sleep"], "session_count": 5}
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
Dict with:
|
|
661
|
+
- status: "success"
|
|
662
|
+
- _metadata_event: True (marker for streaming layer)
|
|
663
|
+
- All provided fields merged into response
|
|
664
|
+
|
|
665
|
+
Examples:
|
|
666
|
+
# High confidence answer with references
|
|
667
|
+
register_metadata(
|
|
668
|
+
confidence=0.95,
|
|
669
|
+
references=["sarah-chen", "q3-report-2024"],
|
|
670
|
+
sources=["REM database lookup"]
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
# Risk assessment example
|
|
674
|
+
register_metadata(
|
|
675
|
+
confidence=0.9,
|
|
676
|
+
risk_level="green",
|
|
677
|
+
risk_score=0,
|
|
678
|
+
risk_reasoning="No risk indicators detected in message",
|
|
679
|
+
sources=["mental_health_resources"]
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Orange risk with recommended action
|
|
683
|
+
register_metadata(
|
|
684
|
+
risk_level="orange",
|
|
685
|
+
risk_score=2,
|
|
686
|
+
risk_reasoning="Passive ideation detected - 'feeling hopeless'",
|
|
687
|
+
recommended_action="Schedule care team check-in within 24-48 hours",
|
|
688
|
+
flags=["care_team_alert"]
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
# Custom domain-specific metadata
|
|
692
|
+
register_metadata(
|
|
693
|
+
confidence=0.8,
|
|
694
|
+
extra={
|
|
695
|
+
"topics_detected": ["medication", "side_effects"],
|
|
696
|
+
"drug_mentioned": "sertraline",
|
|
697
|
+
"sentiment": "concerned"
|
|
698
|
+
}
|
|
699
|
+
)
|
|
700
|
+
"""
|
|
701
|
+
logger.debug(
|
|
702
|
+
f"Registering metadata: confidence={confidence}, "
|
|
703
|
+
f"risk_level={risk_level}, refs={len(references or [])}, "
|
|
704
|
+
f"sources={len(sources or [])}"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
result = {
|
|
708
|
+
"status": "success",
|
|
709
|
+
"_metadata_event": True, # Marker for streaming layer
|
|
710
|
+
"confidence": confidence,
|
|
711
|
+
"references": references,
|
|
712
|
+
"sources": sources,
|
|
713
|
+
"flags": flags,
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
# Add session name if provided
|
|
717
|
+
if session_name is not None:
|
|
718
|
+
result["session_name"] = session_name
|
|
719
|
+
|
|
720
|
+
# Add risk assessment fields if provided
|
|
721
|
+
if risk_level is not None:
|
|
722
|
+
result["risk_level"] = risk_level
|
|
723
|
+
if risk_score is not None:
|
|
724
|
+
result["risk_score"] = risk_score
|
|
725
|
+
if risk_reasoning is not None:
|
|
726
|
+
result["risk_reasoning"] = risk_reasoning
|
|
727
|
+
if recommended_action is not None:
|
|
728
|
+
result["recommended_action"] = recommended_action
|
|
729
|
+
|
|
730
|
+
# Merge any extra fields
|
|
731
|
+
if extra:
|
|
732
|
+
result["extra"] = extra
|
|
733
|
+
|
|
734
|
+
return result
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
@mcp_tool_error_handler
|
|
738
|
+
async def list_schema(
|
|
739
|
+
include_system: bool = False,
|
|
740
|
+
user_id: str | None = None,
|
|
741
|
+
) -> dict[str, Any]:
|
|
742
|
+
"""
|
|
743
|
+
List all schemas (tables) in the REM database.
|
|
744
|
+
|
|
745
|
+
Returns metadata about all available tables including their names,
|
|
746
|
+
row counts, and descriptions. Use this to discover what data is
|
|
747
|
+
available before constructing queries.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
include_system: If True, include PostgreSQL system tables (pg_*, information_schema).
|
|
751
|
+
Default False shows only REM application tables.
|
|
752
|
+
user_id: Optional user identifier (defaults to authenticated user or "default")
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Dict with:
|
|
756
|
+
- status: "success" or "error"
|
|
757
|
+
- tables: List of table metadata dicts with:
|
|
758
|
+
- name: Table name
|
|
759
|
+
- schema: Schema name (usually "public")
|
|
760
|
+
- estimated_rows: Approximate row count
|
|
761
|
+
- description: Table comment if available
|
|
762
|
+
|
|
763
|
+
Examples:
|
|
764
|
+
# List all REM schemas
|
|
765
|
+
list_schema()
|
|
766
|
+
|
|
767
|
+
# Include system tables
|
|
768
|
+
list_schema(include_system=True)
|
|
769
|
+
"""
|
|
770
|
+
rem_service = await get_rem_service()
|
|
771
|
+
user_id = AgentContext.get_user_id_or_default(user_id, source="list_schema")
|
|
772
|
+
|
|
773
|
+
# Query information_schema for tables
|
|
774
|
+
schema_filter = ""
|
|
775
|
+
if not include_system:
|
|
776
|
+
schema_filter = """
|
|
777
|
+
AND table_schema = 'public'
|
|
778
|
+
AND table_name NOT LIKE 'pg_%'
|
|
779
|
+
AND table_name NOT LIKE '_pg_%'
|
|
780
|
+
"""
|
|
781
|
+
|
|
782
|
+
query = f"""
|
|
783
|
+
SELECT
|
|
784
|
+
t.table_schema,
|
|
785
|
+
t.table_name,
|
|
786
|
+
pg_catalog.obj_description(
|
|
787
|
+
(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name))::regclass,
|
|
788
|
+
'pg_class'
|
|
789
|
+
) as description,
|
|
790
|
+
(
|
|
791
|
+
SELECT reltuples::bigint
|
|
792
|
+
FROM pg_class c
|
|
793
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
794
|
+
WHERE c.relname = t.table_name
|
|
795
|
+
AND n.nspname = t.table_schema
|
|
796
|
+
) as estimated_rows
|
|
797
|
+
FROM information_schema.tables t
|
|
798
|
+
WHERE t.table_type = 'BASE TABLE'
|
|
799
|
+
{schema_filter}
|
|
800
|
+
ORDER BY t.table_schema, t.table_name
|
|
801
|
+
"""
|
|
802
|
+
|
|
803
|
+
# Access postgres service directly from cache
|
|
804
|
+
postgres_service = _service_cache.get("postgres")
|
|
805
|
+
if not postgres_service:
|
|
806
|
+
postgres_service = rem_service._postgres
|
|
807
|
+
|
|
808
|
+
rows = await postgres_service.fetch(query)
|
|
809
|
+
|
|
810
|
+
tables = []
|
|
811
|
+
for row in rows:
|
|
812
|
+
tables.append({
|
|
813
|
+
"name": row["table_name"],
|
|
814
|
+
"schema": row["table_schema"],
|
|
815
|
+
"estimated_rows": int(row["estimated_rows"]) if row["estimated_rows"] else 0,
|
|
816
|
+
"description": row["description"],
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
logger.info(f"Listed {len(tables)} schemas for user {user_id}")
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
"tables": tables,
|
|
823
|
+
"count": len(tables),
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
@mcp_tool_error_handler
|
|
828
|
+
async def get_schema(
|
|
829
|
+
table_name: str,
|
|
830
|
+
include_indexes: bool = True,
|
|
831
|
+
include_constraints: bool = True,
|
|
832
|
+
columns: list[str] | None = None,
|
|
833
|
+
user_id: str | None = None,
|
|
834
|
+
) -> dict[str, Any]:
|
|
835
|
+
"""
|
|
836
|
+
Get detailed schema information for a specific table.
|
|
837
|
+
|
|
838
|
+
Returns column definitions, data types, constraints, and indexes.
|
|
839
|
+
Use this to understand table structure before writing SQL queries.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
table_name: Name of the table to inspect (e.g., "resources", "moments")
|
|
843
|
+
include_indexes: Include index information (default True)
|
|
844
|
+
include_constraints: Include constraint information (default True)
|
|
845
|
+
columns: Optional list of specific columns to return. If None, returns all columns.
|
|
846
|
+
user_id: Optional user identifier (defaults to authenticated user or "default")
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
Dict with:
|
|
850
|
+
- status: "success" or "error"
|
|
851
|
+
- table_name: Name of the table
|
|
852
|
+
- columns: List of column definitions with:
|
|
853
|
+
- name: Column name
|
|
854
|
+
- type: PostgreSQL data type
|
|
855
|
+
- nullable: Whether NULL is allowed
|
|
856
|
+
- default: Default value if any
|
|
857
|
+
- description: Column comment if available
|
|
858
|
+
- indexes: List of indexes (if include_indexes=True)
|
|
859
|
+
- constraints: List of constraints (if include_constraints=True)
|
|
860
|
+
- primary_key: Primary key column(s)
|
|
861
|
+
|
|
862
|
+
Examples:
|
|
863
|
+
# Get full schema for resources table
|
|
864
|
+
get_schema(table_name="resources")
|
|
865
|
+
|
|
866
|
+
# Get only specific columns
|
|
867
|
+
get_schema(
|
|
868
|
+
table_name="resources",
|
|
869
|
+
columns=["id", "name", "created_at"]
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
# Get schema without indexes
|
|
873
|
+
get_schema(
|
|
874
|
+
table_name="moments",
|
|
875
|
+
include_indexes=False
|
|
876
|
+
)
|
|
877
|
+
"""
|
|
878
|
+
rem_service = await get_rem_service()
|
|
879
|
+
user_id = AgentContext.get_user_id_or_default(user_id, source="get_schema")
|
|
880
|
+
|
|
881
|
+
# Access postgres service
|
|
882
|
+
postgres_service = _service_cache.get("postgres")
|
|
883
|
+
if not postgres_service:
|
|
884
|
+
postgres_service = rem_service._postgres
|
|
885
|
+
|
|
886
|
+
# Verify table exists
|
|
887
|
+
exists_query = """
|
|
888
|
+
SELECT EXISTS (
|
|
889
|
+
SELECT 1 FROM information_schema.tables
|
|
890
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
891
|
+
)
|
|
892
|
+
"""
|
|
893
|
+
exists = await postgres_service.fetchval(exists_query, table_name)
|
|
894
|
+
if not exists:
|
|
895
|
+
return {
|
|
896
|
+
"status": "error",
|
|
897
|
+
"error": f"Table '{table_name}' not found in public schema",
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
# Get columns
|
|
901
|
+
columns_filter = ""
|
|
902
|
+
if columns:
|
|
903
|
+
placeholders = ", ".join(f"${i+2}" for i in range(len(columns)))
|
|
904
|
+
columns_filter = f"AND column_name IN ({placeholders})"
|
|
905
|
+
|
|
906
|
+
columns_query = f"""
|
|
907
|
+
SELECT
|
|
908
|
+
c.column_name,
|
|
909
|
+
c.data_type,
|
|
910
|
+
c.udt_name,
|
|
911
|
+
c.is_nullable,
|
|
912
|
+
c.column_default,
|
|
913
|
+
c.character_maximum_length,
|
|
914
|
+
c.numeric_precision,
|
|
915
|
+
pg_catalog.col_description(
|
|
916
|
+
(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass,
|
|
917
|
+
c.ordinal_position
|
|
918
|
+
) as description
|
|
919
|
+
FROM information_schema.columns c
|
|
920
|
+
WHERE c.table_schema = 'public'
|
|
921
|
+
AND c.table_name = $1
|
|
922
|
+
{columns_filter}
|
|
923
|
+
ORDER BY c.ordinal_position
|
|
924
|
+
"""
|
|
925
|
+
|
|
926
|
+
params = [table_name]
|
|
927
|
+
if columns:
|
|
928
|
+
params.extend(columns)
|
|
929
|
+
|
|
930
|
+
column_rows = await postgres_service.fetch(columns_query, *params)
|
|
931
|
+
|
|
932
|
+
column_defs = []
|
|
933
|
+
for row in column_rows:
|
|
934
|
+
# Build a more readable type string
|
|
935
|
+
data_type = row["data_type"]
|
|
936
|
+
if row["character_maximum_length"]:
|
|
937
|
+
data_type = f"{data_type}({row['character_maximum_length']})"
|
|
938
|
+
elif row["udt_name"] in ("int4", "int8", "float4", "float8"):
|
|
939
|
+
# Use common type names
|
|
940
|
+
type_map = {"int4": "integer", "int8": "bigint", "float4": "real", "float8": "double precision"}
|
|
941
|
+
data_type = type_map.get(row["udt_name"], data_type)
|
|
942
|
+
elif row["udt_name"] == "vector":
|
|
943
|
+
data_type = "vector"
|
|
944
|
+
|
|
945
|
+
column_defs.append({
|
|
946
|
+
"name": row["column_name"],
|
|
947
|
+
"type": data_type,
|
|
948
|
+
"nullable": row["is_nullable"] == "YES",
|
|
949
|
+
"default": row["column_default"],
|
|
950
|
+
"description": row["description"],
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
result = {
|
|
954
|
+
"table_name": table_name,
|
|
955
|
+
"columns": column_defs,
|
|
956
|
+
"column_count": len(column_defs),
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
# Get primary key
|
|
960
|
+
pk_query = """
|
|
961
|
+
SELECT a.attname as column_name
|
|
962
|
+
FROM pg_index i
|
|
963
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
964
|
+
WHERE i.indrelid = $1::regclass
|
|
965
|
+
AND i.indisprimary
|
|
966
|
+
ORDER BY array_position(i.indkey, a.attnum)
|
|
967
|
+
"""
|
|
968
|
+
pk_rows = await postgres_service.fetch(pk_query, table_name)
|
|
969
|
+
result["primary_key"] = [row["column_name"] for row in pk_rows]
|
|
970
|
+
|
|
971
|
+
# Get indexes
|
|
972
|
+
if include_indexes:
|
|
973
|
+
indexes_query = """
|
|
974
|
+
SELECT
|
|
975
|
+
i.relname as index_name,
|
|
976
|
+
am.amname as index_type,
|
|
977
|
+
ix.indisunique as is_unique,
|
|
978
|
+
ix.indisprimary as is_primary,
|
|
979
|
+
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns
|
|
980
|
+
FROM pg_index ix
|
|
981
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
982
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
983
|
+
JOIN pg_am am ON am.oid = i.relam
|
|
984
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
985
|
+
WHERE t.relname = $1
|
|
986
|
+
GROUP BY i.relname, am.amname, ix.indisunique, ix.indisprimary
|
|
987
|
+
ORDER BY i.relname
|
|
988
|
+
"""
|
|
989
|
+
index_rows = await postgres_service.fetch(indexes_query, table_name)
|
|
990
|
+
result["indexes"] = [
|
|
991
|
+
{
|
|
992
|
+
"name": row["index_name"],
|
|
993
|
+
"type": row["index_type"],
|
|
994
|
+
"unique": row["is_unique"],
|
|
995
|
+
"primary": row["is_primary"],
|
|
996
|
+
"columns": row["columns"],
|
|
997
|
+
}
|
|
998
|
+
for row in index_rows
|
|
999
|
+
]
|
|
1000
|
+
|
|
1001
|
+
# Get constraints
|
|
1002
|
+
if include_constraints:
|
|
1003
|
+
constraints_query = """
|
|
1004
|
+
SELECT
|
|
1005
|
+
con.conname as constraint_name,
|
|
1006
|
+
con.contype as constraint_type,
|
|
1007
|
+
array_agg(a.attname ORDER BY array_position(con.conkey, a.attnum)) as columns,
|
|
1008
|
+
pg_get_constraintdef(con.oid) as definition
|
|
1009
|
+
FROM pg_constraint con
|
|
1010
|
+
JOIN pg_class t ON t.oid = con.conrelid
|
|
1011
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(con.conkey)
|
|
1012
|
+
WHERE t.relname = $1
|
|
1013
|
+
GROUP BY con.conname, con.contype, con.oid
|
|
1014
|
+
ORDER BY con.contype, con.conname
|
|
1015
|
+
"""
|
|
1016
|
+
constraint_rows = await postgres_service.fetch(constraints_query, table_name)
|
|
1017
|
+
|
|
1018
|
+
# Map constraint types to readable names
|
|
1019
|
+
type_map = {
|
|
1020
|
+
"p": "PRIMARY KEY",
|
|
1021
|
+
"u": "UNIQUE",
|
|
1022
|
+
"f": "FOREIGN KEY",
|
|
1023
|
+
"c": "CHECK",
|
|
1024
|
+
"x": "EXCLUSION",
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
result["constraints"] = []
|
|
1028
|
+
for row in constraint_rows:
|
|
1029
|
+
# contype is returned as bytes (char type), decode it
|
|
1030
|
+
con_type = row["constraint_type"]
|
|
1031
|
+
if isinstance(con_type, bytes):
|
|
1032
|
+
con_type = con_type.decode("utf-8")
|
|
1033
|
+
result["constraints"].append({
|
|
1034
|
+
"name": row["constraint_name"],
|
|
1035
|
+
"type": type_map.get(con_type, con_type),
|
|
1036
|
+
"columns": row["columns"],
|
|
1037
|
+
"definition": row["definition"],
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
logger.info(f"Retrieved schema for table '{table_name}' with {len(column_defs)} columns")
|
|
1041
|
+
|
|
1042
|
+
return result
|