remdb 0.3.114__py3-none-any.whl → 0.3.172__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.
- rem/agentic/agents/__init__.py +16 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +2 -0
- rem/agentic/context.py +103 -5
- rem/agentic/context_builder.py +36 -9
- rem/agentic/mcp/tool_wrapper.py +161 -18
- rem/agentic/otel/setup.py +1 -0
- rem/agentic/providers/phoenix.py +371 -108
- rem/agentic/providers/pydantic_ai.py +172 -30
- rem/agentic/schema.py +8 -4
- rem/api/deps.py +3 -5
- rem/api/main.py +26 -4
- rem/api/mcp_router/resources.py +15 -10
- rem/api/mcp_router/server.py +11 -3
- rem/api/mcp_router/tools.py +418 -4
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/admin.py +218 -1
- rem/api/routers/auth.py +349 -6
- rem/api/routers/chat/completions.py +255 -7
- rem/api/routers/chat/models.py +81 -7
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +17 -1
- rem/api/routers/chat/streaming.py +126 -19
- rem/api/routers/feedback.py +134 -14
- rem/api/routers/messages.py +24 -15
- rem/api/routers/query.py +6 -3
- rem/auth/__init__.py +13 -3
- rem/auth/jwt.py +352 -0
- rem/auth/middleware.py +115 -10
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/README.md +42 -0
- rem/cli/commands/cluster.py +617 -168
- rem/cli/commands/configure.py +4 -7
- rem/cli/commands/db.py +66 -22
- rem/cli/commands/experiments.py +468 -76
- rem/cli/commands/schema.py +6 -5
- rem/cli/commands/session.py +336 -0
- rem/cli/dreaming.py +2 -2
- rem/cli/main.py +2 -0
- rem/config.py +8 -1
- rem/models/core/experiment.py +58 -14
- rem/models/entities/__init__.py +4 -0
- rem/models/entities/ontology.py +1 -1
- rem/models/entities/ontology_config.py +1 -1
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
- rem/schemas/agents/examples/contract-extractor.yaml +1 -1
- rem/schemas/agents/examples/cv-parser.yaml +1 -1
- rem/services/__init__.py +3 -1
- rem/services/content/service.py +4 -3
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +513 -0
- rem/services/email/templates.py +360 -0
- rem/services/phoenix/client.py +59 -18
- rem/services/postgres/README.md +38 -0
- rem/services/postgres/diff_service.py +127 -6
- rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
- rem/services/postgres/repository.py +5 -4
- rem/services/postgres/schema_generator.py +205 -4
- rem/services/session/compression.py +120 -50
- rem/services/session/reload.py +14 -7
- rem/services/user_service.py +41 -9
- rem/settings.py +442 -23
- rem/sql/migrations/001_install.sql +156 -0
- rem/sql/migrations/002_install_models.sql +1951 -88
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/README.md +45 -0
- rem/utils/__init__.py +18 -0
- rem/utils/files.py +157 -1
- rem/utils/schema_loader.py +139 -10
- rem/utils/sql_paths.py +146 -0
- rem/utils/vision.py +1 -1
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/METADATA +218 -180
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/RECORD +83 -68
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/WHEEL +0 -0
- {remdb-0.3.114.dist-info → remdb-0.3.172.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
|
|
@@ -113,7 +116,8 @@ def mcp_tool_error_handler(func: Callable) -> Callable:
|
|
|
113
116
|
# Otherwise wrap in success response
|
|
114
117
|
return {"status": "success", **result}
|
|
115
118
|
except Exception as e:
|
|
116
|
-
|
|
119
|
+
# Use %s format to avoid issues with curly braces in error messages
|
|
120
|
+
logger.opt(exception=True).error("{} failed: {}", func.__name__, str(e))
|
|
117
121
|
return {
|
|
118
122
|
"status": "error",
|
|
119
123
|
"error": str(e),
|
|
@@ -377,9 +381,10 @@ async def ask_rem_agent(
|
|
|
377
381
|
from ...utils.schema_loader import load_agent_schema
|
|
378
382
|
|
|
379
383
|
# Create agent context
|
|
384
|
+
# Note: tenant_id defaults to "default" if user_id is None
|
|
380
385
|
context = AgentContext(
|
|
381
386
|
user_id=user_id,
|
|
382
|
-
tenant_id=user_id, #
|
|
387
|
+
tenant_id=user_id or "default", # Use default tenant for anonymous users
|
|
383
388
|
default_model=settings.llm.default_model,
|
|
384
389
|
)
|
|
385
390
|
|
|
@@ -603,7 +608,9 @@ async def register_metadata(
|
|
|
603
608
|
references: list[str] | None = None,
|
|
604
609
|
sources: list[str] | None = None,
|
|
605
610
|
flags: list[str] | None = None,
|
|
606
|
-
#
|
|
611
|
+
# Session naming
|
|
612
|
+
session_name: str | None = None,
|
|
613
|
+
# Risk assessment fields (used by specialized agents)
|
|
607
614
|
risk_level: str | None = None,
|
|
608
615
|
risk_score: int | None = None,
|
|
609
616
|
risk_reasoning: str | None = None,
|
|
@@ -636,6 +643,11 @@ async def register_metadata(
|
|
|
636
643
|
flags: Optional flags for the response (e.g., "needs_review",
|
|
637
644
|
"uncertain", "incomplete", "crisis_alert").
|
|
638
645
|
|
|
646
|
+
session_name: Short 1-3 phrase name describing the session topic.
|
|
647
|
+
Used by the UI to label conversations in the sidebar.
|
|
648
|
+
Examples: "Prescription Drug Questions", "AWS Setup Help",
|
|
649
|
+
"Python Code Review", "Travel Planning".
|
|
650
|
+
|
|
639
651
|
risk_level: Risk level indicator (e.g., "green", "orange", "red").
|
|
640
652
|
Used by mental health agents for C-SSRS style assessment.
|
|
641
653
|
risk_score: Numeric risk score (e.g., 0-6 for C-SSRS).
|
|
@@ -660,7 +672,7 @@ async def register_metadata(
|
|
|
660
672
|
sources=["REM database lookup"]
|
|
661
673
|
)
|
|
662
674
|
|
|
663
|
-
#
|
|
675
|
+
# Risk assessment example
|
|
664
676
|
register_metadata(
|
|
665
677
|
confidence=0.9,
|
|
666
678
|
risk_level="green",
|
|
@@ -703,6 +715,10 @@ async def register_metadata(
|
|
|
703
715
|
"flags": flags,
|
|
704
716
|
}
|
|
705
717
|
|
|
718
|
+
# Add session name if provided
|
|
719
|
+
if session_name is not None:
|
|
720
|
+
result["session_name"] = session_name
|
|
721
|
+
|
|
706
722
|
# Add risk assessment fields if provided
|
|
707
723
|
if risk_level is not None:
|
|
708
724
|
result["risk_level"] = risk_level
|
|
@@ -718,3 +734,401 @@ async def register_metadata(
|
|
|
718
734
|
result["extra"] = extra
|
|
719
735
|
|
|
720
736
|
return result
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
@mcp_tool_error_handler
|
|
740
|
+
async def list_schema(
|
|
741
|
+
include_system: bool = False,
|
|
742
|
+
user_id: str | None = None,
|
|
743
|
+
) -> dict[str, Any]:
|
|
744
|
+
"""
|
|
745
|
+
List all schemas (tables) in the REM database.
|
|
746
|
+
|
|
747
|
+
Returns metadata about all available tables including their names,
|
|
748
|
+
row counts, and descriptions. Use this to discover what data is
|
|
749
|
+
available before constructing queries.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
include_system: If True, include PostgreSQL system tables (pg_*, information_schema).
|
|
753
|
+
Default False shows only REM application tables.
|
|
754
|
+
user_id: Optional user identifier (defaults to authenticated user or "default")
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
Dict with:
|
|
758
|
+
- status: "success" or "error"
|
|
759
|
+
- tables: List of table metadata dicts with:
|
|
760
|
+
- name: Table name
|
|
761
|
+
- schema: Schema name (usually "public")
|
|
762
|
+
- estimated_rows: Approximate row count
|
|
763
|
+
- description: Table comment if available
|
|
764
|
+
|
|
765
|
+
Examples:
|
|
766
|
+
# List all REM schemas
|
|
767
|
+
list_schema()
|
|
768
|
+
|
|
769
|
+
# Include system tables
|
|
770
|
+
list_schema(include_system=True)
|
|
771
|
+
"""
|
|
772
|
+
rem_service = await get_rem_service()
|
|
773
|
+
user_id = AgentContext.get_user_id_or_default(user_id, source="list_schema")
|
|
774
|
+
|
|
775
|
+
# Query information_schema for tables
|
|
776
|
+
schema_filter = ""
|
|
777
|
+
if not include_system:
|
|
778
|
+
schema_filter = """
|
|
779
|
+
AND table_schema = 'public'
|
|
780
|
+
AND table_name NOT LIKE 'pg_%'
|
|
781
|
+
AND table_name NOT LIKE '_pg_%'
|
|
782
|
+
"""
|
|
783
|
+
|
|
784
|
+
query = f"""
|
|
785
|
+
SELECT
|
|
786
|
+
t.table_schema,
|
|
787
|
+
t.table_name,
|
|
788
|
+
pg_catalog.obj_description(
|
|
789
|
+
(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name))::regclass,
|
|
790
|
+
'pg_class'
|
|
791
|
+
) as description,
|
|
792
|
+
(
|
|
793
|
+
SELECT reltuples::bigint
|
|
794
|
+
FROM pg_class c
|
|
795
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
796
|
+
WHERE c.relname = t.table_name
|
|
797
|
+
AND n.nspname = t.table_schema
|
|
798
|
+
) as estimated_rows
|
|
799
|
+
FROM information_schema.tables t
|
|
800
|
+
WHERE t.table_type = 'BASE TABLE'
|
|
801
|
+
{schema_filter}
|
|
802
|
+
ORDER BY t.table_schema, t.table_name
|
|
803
|
+
"""
|
|
804
|
+
|
|
805
|
+
# Access postgres service directly from cache
|
|
806
|
+
postgres_service = _service_cache.get("postgres")
|
|
807
|
+
if not postgres_service:
|
|
808
|
+
postgres_service = rem_service._postgres
|
|
809
|
+
|
|
810
|
+
rows = await postgres_service.fetch(query)
|
|
811
|
+
|
|
812
|
+
tables = []
|
|
813
|
+
for row in rows:
|
|
814
|
+
tables.append({
|
|
815
|
+
"name": row["table_name"],
|
|
816
|
+
"schema": row["table_schema"],
|
|
817
|
+
"estimated_rows": int(row["estimated_rows"]) if row["estimated_rows"] else 0,
|
|
818
|
+
"description": row["description"],
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
logger.info(f"Listed {len(tables)} schemas for user {user_id}")
|
|
822
|
+
|
|
823
|
+
return {
|
|
824
|
+
"tables": tables,
|
|
825
|
+
"count": len(tables),
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
@mcp_tool_error_handler
|
|
830
|
+
async def get_schema(
|
|
831
|
+
table_name: str,
|
|
832
|
+
include_indexes: bool = True,
|
|
833
|
+
include_constraints: bool = True,
|
|
834
|
+
columns: list[str] | None = None,
|
|
835
|
+
user_id: str | None = None,
|
|
836
|
+
) -> dict[str, Any]:
|
|
837
|
+
"""
|
|
838
|
+
Get detailed schema information for a specific table.
|
|
839
|
+
|
|
840
|
+
Returns column definitions, data types, constraints, and indexes.
|
|
841
|
+
Use this to understand table structure before writing SQL queries.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
table_name: Name of the table to inspect (e.g., "resources", "moments")
|
|
845
|
+
include_indexes: Include index information (default True)
|
|
846
|
+
include_constraints: Include constraint information (default True)
|
|
847
|
+
columns: Optional list of specific columns to return. If None, returns all columns.
|
|
848
|
+
user_id: Optional user identifier (defaults to authenticated user or "default")
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
Dict with:
|
|
852
|
+
- status: "success" or "error"
|
|
853
|
+
- table_name: Name of the table
|
|
854
|
+
- columns: List of column definitions with:
|
|
855
|
+
- name: Column name
|
|
856
|
+
- type: PostgreSQL data type
|
|
857
|
+
- nullable: Whether NULL is allowed
|
|
858
|
+
- default: Default value if any
|
|
859
|
+
- description: Column comment if available
|
|
860
|
+
- indexes: List of indexes (if include_indexes=True)
|
|
861
|
+
- constraints: List of constraints (if include_constraints=True)
|
|
862
|
+
- primary_key: Primary key column(s)
|
|
863
|
+
|
|
864
|
+
Examples:
|
|
865
|
+
# Get full schema for resources table
|
|
866
|
+
get_schema(table_name="resources")
|
|
867
|
+
|
|
868
|
+
# Get only specific columns
|
|
869
|
+
get_schema(
|
|
870
|
+
table_name="resources",
|
|
871
|
+
columns=["id", "name", "created_at"]
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
# Get schema without indexes
|
|
875
|
+
get_schema(
|
|
876
|
+
table_name="moments",
|
|
877
|
+
include_indexes=False
|
|
878
|
+
)
|
|
879
|
+
"""
|
|
880
|
+
rem_service = await get_rem_service()
|
|
881
|
+
user_id = AgentContext.get_user_id_or_default(user_id, source="get_schema")
|
|
882
|
+
|
|
883
|
+
# Access postgres service
|
|
884
|
+
postgres_service = _service_cache.get("postgres")
|
|
885
|
+
if not postgres_service:
|
|
886
|
+
postgres_service = rem_service._postgres
|
|
887
|
+
|
|
888
|
+
# Verify table exists
|
|
889
|
+
exists_query = """
|
|
890
|
+
SELECT EXISTS (
|
|
891
|
+
SELECT 1 FROM information_schema.tables
|
|
892
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
893
|
+
)
|
|
894
|
+
"""
|
|
895
|
+
exists = await postgres_service.fetchval(exists_query, table_name)
|
|
896
|
+
if not exists:
|
|
897
|
+
return {
|
|
898
|
+
"status": "error",
|
|
899
|
+
"error": f"Table '{table_name}' not found in public schema",
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
# Get columns
|
|
903
|
+
columns_filter = ""
|
|
904
|
+
if columns:
|
|
905
|
+
placeholders = ", ".join(f"${i+2}" for i in range(len(columns)))
|
|
906
|
+
columns_filter = f"AND column_name IN ({placeholders})"
|
|
907
|
+
|
|
908
|
+
columns_query = f"""
|
|
909
|
+
SELECT
|
|
910
|
+
c.column_name,
|
|
911
|
+
c.data_type,
|
|
912
|
+
c.udt_name,
|
|
913
|
+
c.is_nullable,
|
|
914
|
+
c.column_default,
|
|
915
|
+
c.character_maximum_length,
|
|
916
|
+
c.numeric_precision,
|
|
917
|
+
pg_catalog.col_description(
|
|
918
|
+
(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass,
|
|
919
|
+
c.ordinal_position
|
|
920
|
+
) as description
|
|
921
|
+
FROM information_schema.columns c
|
|
922
|
+
WHERE c.table_schema = 'public'
|
|
923
|
+
AND c.table_name = $1
|
|
924
|
+
{columns_filter}
|
|
925
|
+
ORDER BY c.ordinal_position
|
|
926
|
+
"""
|
|
927
|
+
|
|
928
|
+
params = [table_name]
|
|
929
|
+
if columns:
|
|
930
|
+
params.extend(columns)
|
|
931
|
+
|
|
932
|
+
column_rows = await postgres_service.fetch(columns_query, *params)
|
|
933
|
+
|
|
934
|
+
column_defs = []
|
|
935
|
+
for row in column_rows:
|
|
936
|
+
# Build a more readable type string
|
|
937
|
+
data_type = row["data_type"]
|
|
938
|
+
if row["character_maximum_length"]:
|
|
939
|
+
data_type = f"{data_type}({row['character_maximum_length']})"
|
|
940
|
+
elif row["udt_name"] in ("int4", "int8", "float4", "float8"):
|
|
941
|
+
# Use common type names
|
|
942
|
+
type_map = {"int4": "integer", "int8": "bigint", "float4": "real", "float8": "double precision"}
|
|
943
|
+
data_type = type_map.get(row["udt_name"], data_type)
|
|
944
|
+
elif row["udt_name"] == "vector":
|
|
945
|
+
data_type = "vector"
|
|
946
|
+
|
|
947
|
+
column_defs.append({
|
|
948
|
+
"name": row["column_name"],
|
|
949
|
+
"type": data_type,
|
|
950
|
+
"nullable": row["is_nullable"] == "YES",
|
|
951
|
+
"default": row["column_default"],
|
|
952
|
+
"description": row["description"],
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
result = {
|
|
956
|
+
"table_name": table_name,
|
|
957
|
+
"columns": column_defs,
|
|
958
|
+
"column_count": len(column_defs),
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
# Get primary key
|
|
962
|
+
pk_query = """
|
|
963
|
+
SELECT a.attname as column_name
|
|
964
|
+
FROM pg_index i
|
|
965
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
966
|
+
WHERE i.indrelid = $1::regclass
|
|
967
|
+
AND i.indisprimary
|
|
968
|
+
ORDER BY array_position(i.indkey, a.attnum)
|
|
969
|
+
"""
|
|
970
|
+
pk_rows = await postgres_service.fetch(pk_query, table_name)
|
|
971
|
+
result["primary_key"] = [row["column_name"] for row in pk_rows]
|
|
972
|
+
|
|
973
|
+
# Get indexes
|
|
974
|
+
if include_indexes:
|
|
975
|
+
indexes_query = """
|
|
976
|
+
SELECT
|
|
977
|
+
i.relname as index_name,
|
|
978
|
+
am.amname as index_type,
|
|
979
|
+
ix.indisunique as is_unique,
|
|
980
|
+
ix.indisprimary as is_primary,
|
|
981
|
+
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns
|
|
982
|
+
FROM pg_index ix
|
|
983
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
984
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
985
|
+
JOIN pg_am am ON am.oid = i.relam
|
|
986
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
987
|
+
WHERE t.relname = $1
|
|
988
|
+
GROUP BY i.relname, am.amname, ix.indisunique, ix.indisprimary
|
|
989
|
+
ORDER BY i.relname
|
|
990
|
+
"""
|
|
991
|
+
index_rows = await postgres_service.fetch(indexes_query, table_name)
|
|
992
|
+
result["indexes"] = [
|
|
993
|
+
{
|
|
994
|
+
"name": row["index_name"],
|
|
995
|
+
"type": row["index_type"],
|
|
996
|
+
"unique": row["is_unique"],
|
|
997
|
+
"primary": row["is_primary"],
|
|
998
|
+
"columns": row["columns"],
|
|
999
|
+
}
|
|
1000
|
+
for row in index_rows
|
|
1001
|
+
]
|
|
1002
|
+
|
|
1003
|
+
# Get constraints
|
|
1004
|
+
if include_constraints:
|
|
1005
|
+
constraints_query = """
|
|
1006
|
+
SELECT
|
|
1007
|
+
con.conname as constraint_name,
|
|
1008
|
+
con.contype as constraint_type,
|
|
1009
|
+
array_agg(a.attname ORDER BY array_position(con.conkey, a.attnum)) as columns,
|
|
1010
|
+
pg_get_constraintdef(con.oid) as definition
|
|
1011
|
+
FROM pg_constraint con
|
|
1012
|
+
JOIN pg_class t ON t.oid = con.conrelid
|
|
1013
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(con.conkey)
|
|
1014
|
+
WHERE t.relname = $1
|
|
1015
|
+
GROUP BY con.conname, con.contype, con.oid
|
|
1016
|
+
ORDER BY con.contype, con.conname
|
|
1017
|
+
"""
|
|
1018
|
+
constraint_rows = await postgres_service.fetch(constraints_query, table_name)
|
|
1019
|
+
|
|
1020
|
+
# Map constraint types to readable names
|
|
1021
|
+
type_map = {
|
|
1022
|
+
"p": "PRIMARY KEY",
|
|
1023
|
+
"u": "UNIQUE",
|
|
1024
|
+
"f": "FOREIGN KEY",
|
|
1025
|
+
"c": "CHECK",
|
|
1026
|
+
"x": "EXCLUSION",
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
result["constraints"] = []
|
|
1030
|
+
for row in constraint_rows:
|
|
1031
|
+
# contype is returned as bytes (char type), decode it
|
|
1032
|
+
con_type = row["constraint_type"]
|
|
1033
|
+
if isinstance(con_type, bytes):
|
|
1034
|
+
con_type = con_type.decode("utf-8")
|
|
1035
|
+
result["constraints"].append({
|
|
1036
|
+
"name": row["constraint_name"],
|
|
1037
|
+
"type": type_map.get(con_type, con_type),
|
|
1038
|
+
"columns": row["columns"],
|
|
1039
|
+
"definition": row["definition"],
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
logger.info(f"Retrieved schema for table '{table_name}' with {len(column_defs)} columns")
|
|
1043
|
+
|
|
1044
|
+
return result
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
@mcp_tool_error_handler
|
|
1048
|
+
async def save_agent(
|
|
1049
|
+
name: str,
|
|
1050
|
+
description: str,
|
|
1051
|
+
properties: dict[str, Any] | None = None,
|
|
1052
|
+
required: list[str] | None = None,
|
|
1053
|
+
tools: list[str] | None = None,
|
|
1054
|
+
tags: list[str] | None = None,
|
|
1055
|
+
version: str = "1.0.0",
|
|
1056
|
+
user_id: str | None = None,
|
|
1057
|
+
) -> dict[str, Any]:
|
|
1058
|
+
"""
|
|
1059
|
+
Save an agent schema to REM, making it available for use.
|
|
1060
|
+
|
|
1061
|
+
This tool creates or updates an agent definition in the user's schema space.
|
|
1062
|
+
The agent becomes immediately available for conversations.
|
|
1063
|
+
|
|
1064
|
+
**Default Tools**: All agents automatically get `search_rem` and `register_metadata`
|
|
1065
|
+
tools unless explicitly overridden.
|
|
1066
|
+
|
|
1067
|
+
Args:
|
|
1068
|
+
name: Agent name in kebab-case (e.g., "code-reviewer", "sales-assistant").
|
|
1069
|
+
Must be unique within the user's schema space.
|
|
1070
|
+
description: The agent's system prompt. This is the full instruction set
|
|
1071
|
+
that defines the agent's behavior, personality, and capabilities.
|
|
1072
|
+
Use markdown formatting for structure.
|
|
1073
|
+
properties: Output schema properties as a dict. Each property should have:
|
|
1074
|
+
- type: "string", "number", "boolean", "array", "object"
|
|
1075
|
+
- description: What this field captures
|
|
1076
|
+
Example: {"answer": {"type": "string", "description": "Response to user"}}
|
|
1077
|
+
If not provided, defaults to a simple {"answer": {"type": "string"}} schema.
|
|
1078
|
+
required: List of required property names. Defaults to ["answer"] if not provided.
|
|
1079
|
+
tools: List of tool names the agent can use. Defaults to ["search_rem", "register_metadata"].
|
|
1080
|
+
tags: Optional tags for categorizing the agent.
|
|
1081
|
+
version: Semantic version string (default: "1.0.0").
|
|
1082
|
+
user_id: User identifier for scoping. Uses authenticated user if not provided.
|
|
1083
|
+
|
|
1084
|
+
Returns:
|
|
1085
|
+
Dict with:
|
|
1086
|
+
- status: "success" or "error"
|
|
1087
|
+
- agent_name: Name of the saved agent
|
|
1088
|
+
- version: Version saved
|
|
1089
|
+
- message: Human-readable status
|
|
1090
|
+
|
|
1091
|
+
Examples:
|
|
1092
|
+
# Create a simple agent
|
|
1093
|
+
save_agent(
|
|
1094
|
+
name="greeting-bot",
|
|
1095
|
+
description="You are a friendly greeter. Say hello warmly.",
|
|
1096
|
+
properties={"answer": {"type": "string", "description": "Greeting message"}},
|
|
1097
|
+
required=["answer"]
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# Create agent with structured output
|
|
1101
|
+
save_agent(
|
|
1102
|
+
name="sentiment-analyzer",
|
|
1103
|
+
description="Analyze sentiment of text provided by the user.",
|
|
1104
|
+
properties={
|
|
1105
|
+
"answer": {"type": "string", "description": "Analysis explanation"},
|
|
1106
|
+
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
|
|
1107
|
+
"confidence": {"type": "number", "minimum": 0, "maximum": 1}
|
|
1108
|
+
},
|
|
1109
|
+
required=["answer", "sentiment"],
|
|
1110
|
+
tags=["analysis", "nlp"]
|
|
1111
|
+
)
|
|
1112
|
+
"""
|
|
1113
|
+
from ...agentic.agents.agent_manager import save_agent as _save_agent
|
|
1114
|
+
|
|
1115
|
+
# Get user_id from context if not provided
|
|
1116
|
+
user_id = AgentContext.get_user_id_or_default(user_id, source="save_agent")
|
|
1117
|
+
|
|
1118
|
+
# Delegate to agent_manager
|
|
1119
|
+
result = await _save_agent(
|
|
1120
|
+
name=name,
|
|
1121
|
+
description=description,
|
|
1122
|
+
user_id=user_id,
|
|
1123
|
+
properties=properties,
|
|
1124
|
+
required=required,
|
|
1125
|
+
tools=tools,
|
|
1126
|
+
tags=tags,
|
|
1127
|
+
version=version,
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
# Add helpful message for Slack users
|
|
1131
|
+
if result.get("status") == "success":
|
|
1132
|
+
result["message"] = f"Agent '{name}' saved. Use `/custom-agent {name}` to chat with it."
|
|
1133
|
+
|
|
1134
|
+
return result
|
rem/api/middleware/tracking.py
CHANGED
|
@@ -102,14 +102,14 @@ class AnonymousTrackingMiddleware(BaseHTTPMiddleware):
|
|
|
102
102
|
# Tenant ID from header or default
|
|
103
103
|
tenant_id = request.headers.get("X-Tenant-Id", "default")
|
|
104
104
|
|
|
105
|
-
# 4. Rate Limiting
|
|
106
|
-
if settings.postgres.enabled:
|
|
105
|
+
# 4. Rate Limiting (skip if disabled via settings)
|
|
106
|
+
if settings.postgres.enabled and settings.api.rate_limit_enabled:
|
|
107
107
|
is_allowed, current, limit = await self.rate_limiter.check_rate_limit(
|
|
108
108
|
tenant_id=tenant_id,
|
|
109
109
|
identifier=identifier,
|
|
110
110
|
tier=tier
|
|
111
111
|
)
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
if not is_allowed:
|
|
114
114
|
return JSONResponse(
|
|
115
115
|
status_code=429,
|
|
@@ -141,8 +141,8 @@ class AnonymousTrackingMiddleware(BaseHTTPMiddleware):
|
|
|
141
141
|
secure=settings.environment == "production"
|
|
142
142
|
)
|
|
143
143
|
|
|
144
|
-
# Add Rate Limit headers
|
|
145
|
-
if settings.postgres.enabled and 'limit' in locals():
|
|
144
|
+
# Add Rate Limit headers (only if rate limiting is enabled)
|
|
145
|
+
if settings.postgres.enabled and settings.api.rate_limit_enabled and 'limit' in locals():
|
|
146
146
|
response.headers["X-RateLimit-Limit"] = str(limit)
|
|
147
147
|
response.headers["X-RateLimit-Remaining"] = str(max(0, limit - current))
|
|
148
148
|
|