remdb 0.3.242__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 (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. remdb-0.3.242.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,54 @@
1
+ """
2
+ REM Agentic Framework.
3
+
4
+ Provider-agnostic agent orchestration with JSON Schema agents,
5
+ MCP tool integration, and structured output.
6
+ """
7
+
8
+ from .context import AgentContext
9
+ from .query import AgentQuery
10
+ from .schema import (
11
+ AgentSchema,
12
+ AgentSchemaMetadata,
13
+ MCPToolReference,
14
+ MCPResourceReference,
15
+ validate_agent_schema,
16
+ create_agent_schema,
17
+ )
18
+ from .providers.pydantic_ai import create_agent_from_schema_file, create_agent, AgentRuntime
19
+ from .query_helper import ask_rem, REMQueryOutput
20
+ from .llm_provider_models import (
21
+ ModelInfo,
22
+ AVAILABLE_MODELS,
23
+ ALLOWED_MODEL_IDS,
24
+ is_valid_model,
25
+ get_valid_model_or_default,
26
+ get_model_by_id,
27
+ )
28
+
29
+ __all__ = [
30
+ # Context and Query
31
+ "AgentContext",
32
+ "AgentQuery",
33
+ # Schema Protocol
34
+ "AgentSchema",
35
+ "AgentSchemaMetadata",
36
+ "MCPToolReference",
37
+ "MCPResourceReference",
38
+ "validate_agent_schema",
39
+ "create_agent_schema",
40
+ # Agent Factories
41
+ "create_agent_from_schema_file",
42
+ "create_agent",
43
+ "AgentRuntime",
44
+ # REM Query Helpers
45
+ "ask_rem",
46
+ "REMQueryOutput",
47
+ # LLM Provider Models
48
+ "ModelInfo",
49
+ "AVAILABLE_MODELS",
50
+ "ALLOWED_MODEL_IDS",
51
+ "is_valid_model",
52
+ "get_valid_model_or_default",
53
+ "get_model_by_id",
54
+ ]
@@ -0,0 +1,155 @@
1
+ # REM Agents
2
+
3
+ Built-in agents for REM system operations.
4
+
5
+ ## Overview
6
+
7
+ This folder contains specialized agents that provide high-level interfaces for REM operations. These agents use LLMs to interpret natural language and convert it to structured REM queries.
8
+
9
+ ## REM Query Agent
10
+
11
+ **File**: `rem_query_agent.py`
12
+
13
+ Converts natural language questions into structured REM queries with PostgreSQL dialect awareness.
14
+
15
+ ### Features
16
+
17
+ - **Query Type Selection**: Automatically chooses optimal query type (LOOKUP, FUZZY, SEARCH, SQL, TRAVERSE)
18
+ - **PostgreSQL Dialect Aware**: Knows when to use KV_STORE vs primary tables
19
+ - **Token Optimized**: Minimal output fields for fast generation and low cost
20
+ - **Confidence Scoring**: Returns confidence (0-1) with reasoning for low scores
21
+ - **Multi-Step Planning**: Can break complex queries into multiple REM calls
22
+
23
+ ### Usage
24
+
25
+ #### Simple Query
26
+
27
+ ```python
28
+ from rem.agentic.agents import ask_rem
29
+
30
+ # Convert natural language to REM query
31
+ result = await ask_rem("Show me Sarah Chen")
32
+
33
+ print(result.query_type) # QueryType.LOOKUP
34
+ print(result.parameters) # {"entity_key": "sarah-chen"}
35
+ print(result.confidence) # 1.0
36
+ ```
37
+
38
+ #### With Custom Model
39
+
40
+ ```python
41
+ # Use fast, cheap model for query generation
42
+ result = await ask_rem(
43
+ "Find documents about databases",
44
+ llm_model="gpt-4o-mini"
45
+ )
46
+
47
+ print(result.query_type) # QueryType.SEARCH
48
+ print(result.parameters)
49
+ # {
50
+ # "query_text": "database",
51
+ # "table_name": "resources",
52
+ # "field_name": "content",
53
+ # "limit": 10
54
+ # }
55
+ ```
56
+
57
+ #### Integration with RemService
58
+
59
+ ```python
60
+ from rem.services.rem import RemService
61
+
62
+ # RemService automatically uses REM Query Agent
63
+ result = await rem_service.ask_rem(
64
+ natural_query="What does Sarah manage?",
65
+ tenant_id="acme-corp"
66
+ )
67
+
68
+ # Returns:
69
+ # {
70
+ # "query_output": {
71
+ # "query_type": "TRAVERSE",
72
+ # "parameters": {"start_key": "sarah-chen", "max_depth": 1, "rel_type": "manages"},
73
+ # "confidence": 0.85,
74
+ # "reasoning": "TRAVERSE query to find entities Sarah manages via graph edges"
75
+ # },
76
+ # "results": [...], # Executed query results (if confidence >= 0.7)
77
+ # "natural_query": "What does Sarah manage?"
78
+ # }
79
+ ```
80
+
81
+ ### Query Types
82
+
83
+ | Type | Description | When to Use | Example |
84
+ |------|-------------|-------------|---------|
85
+ | `LOOKUP` | O(1) entity lookup by natural key | User references specific entity by name | "Show me Sarah Chen" |
86
+ | `FUZZY` | Trigram text similarity (pg_trgm) | Partial/misspelled names, approximate matches | "Find people named Sara" |
87
+ | `SEARCH` | Semantic vector similarity | Conceptual questions, semantic similarity | "Documents about databases" |
88
+ | `SQL` | Direct table queries with WHERE | Temporal, filtered, or aggregate queries | "Meetings in Q4 2024" |
89
+ | `TRAVERSE` | Recursive graph traversal | Relationships, connections, "what's related" | "What does Sarah manage?" |
90
+
91
+ ### Configuration
92
+
93
+ Set the model for REM Query Agent in your environment:
94
+
95
+ ```bash
96
+ # .env
97
+ LLM__QUERY_AGENT_MODEL=gpt-4o-mini # Fast, cheap model recommended
98
+ ```
99
+
100
+ If not set, uses `settings.llm.default_model`.
101
+
102
+ ### Output Schema
103
+
104
+ ```python
105
+ class REMQueryOutput(BaseModel):
106
+ query_type: QueryType # Selected query type
107
+ parameters: dict # Query parameters
108
+ confidence: float # 0.0-1.0 confidence score
109
+ reasoning: str | None # Only if confidence < 0.7 or multi-step
110
+ multi_step: list[dict] | None # For complex queries
111
+ ```
112
+
113
+ ### Design Philosophy
114
+
115
+ 1. **Token Efficiency**: Output is concise by design
116
+ - Reasoning only included when needed (low confidence or multi-step)
117
+ - Minimal fields to reduce generation time and cost
118
+
119
+ 2. **PostgreSQL Awareness**: Agent knows the database schema
120
+ - LOOKUP/FUZZY use UNLOGGED KV_STORE (fast cache)
121
+ - SEARCH joins KV_STORE + embeddings_<table>
122
+ - SQL queries primary tables directly
123
+ - TRAVERSE follows graph_edges JSONB field
124
+
125
+ 3. **Progressive Complexity**: Prefer simple queries over complex
126
+ - LOOKUP is fastest (O(1))
127
+ - FUZZY uses indexed trigrams
128
+ - SEARCH requires embedding generation
129
+ - SQL scans tables (filtered)
130
+ - TRAVERSE is recursive (most complex)
131
+
132
+ 4. **Confidence-Based Execution**: RemService auto-executes if confidence >= 0.7
133
+ - High confidence: Execute immediately
134
+ - Low confidence: Return query + reasoning for review
135
+
136
+ ### Testing
137
+
138
+ See `tests/unit/agentic/agents/test_rem_query_agent.py` for unit tests.
139
+
140
+ Tests cover:
141
+ - Schema structure validation
142
+ - Output model creation
143
+ - Confidence validation
144
+ - Multi-step query support
145
+
146
+ Integration tests with actual LLM execution require API keys and are in `tests/integration/`.
147
+
148
+ ## Future Agents
149
+
150
+ Additional agents can be added following the same pattern:
151
+
152
+ - **Entity Summarization Agent**: Summarize entity relationships
153
+ - **Query Explanation Agent**: Explain REM query results in natural language
154
+ - **Schema Discovery Agent**: Discover available tables and fields
155
+ - **Data Quality Agent**: Identify data quality issues in entities
@@ -0,0 +1,38 @@
1
+ """
2
+ REM Agents - Specialized agents for REM operations.
3
+
4
+ Most agents are defined as YAML schemas in src/rem/schemas/agents/.
5
+ Use create_agent_from_schema_file() to instantiate agents.
6
+
7
+ The SSE Simulator is a special programmatic "agent" that generates
8
+ scripted SSE events for testing and demonstration - it doesn't use an LLM.
9
+
10
+ Agent Manager provides functions for saving/loading user-created agents.
11
+ """
12
+
13
+ from .sse_simulator import (
14
+ stream_simulator_events,
15
+ stream_minimal_demo,
16
+ stream_error_demo,
17
+ )
18
+
19
+ from .agent_manager import (
20
+ save_agent,
21
+ get_agent,
22
+ list_agents,
23
+ delete_agent,
24
+ build_agent_spec,
25
+ )
26
+
27
+ __all__ = [
28
+ # SSE Simulator (programmatic, no LLM)
29
+ "stream_simulator_events",
30
+ "stream_minimal_demo",
31
+ "stream_error_demo",
32
+ # Agent Manager
33
+ "save_agent",
34
+ "get_agent",
35
+ "list_agents",
36
+ "delete_agent",
37
+ "build_agent_spec",
38
+ ]
@@ -0,0 +1,311 @@
1
+ """
2
+ Agent Manager - Save, load, and manage user-created agents.
3
+
4
+ This module provides the core functionality for persisting agent schemas
5
+ to the database with user scoping.
6
+
7
+ Usage:
8
+ from rem.agentic.agents.agent_manager import save_agent, get_agent, list_agents
9
+
10
+ # Save an agent
11
+ result = await save_agent(
12
+ name="my-assistant",
13
+ description="You are a helpful assistant.",
14
+ user_id="user-123"
15
+ )
16
+
17
+ # Get an agent
18
+ agent = await get_agent("my-assistant", user_id="user-123")
19
+
20
+ # List user's agents
21
+ agents = await list_agents(user_id="user-123")
22
+ """
23
+
24
+ from typing import Any
25
+ from loguru import logger
26
+
27
+
28
+ DEFAULT_TOOLS = ["search_rem", "register_metadata"]
29
+
30
+
31
+ def build_agent_spec(
32
+ name: str,
33
+ description: str,
34
+ properties: dict[str, Any] | None = None,
35
+ required: list[str] | None = None,
36
+ tools: list[str] | None = None,
37
+ tags: list[str] | None = None,
38
+ version: str = "1.0.0",
39
+ ) -> dict[str, Any]:
40
+ """
41
+ Build a valid agent schema spec.
42
+
43
+ Args:
44
+ name: Agent name in kebab-case
45
+ description: System prompt for the agent
46
+ properties: Output schema properties
47
+ required: Required property names
48
+ tools: Tool names (defaults to search_rem, register_metadata)
49
+ tags: Categorization tags
50
+ version: Semantic version
51
+
52
+ Returns:
53
+ Valid agent schema spec dict
54
+ """
55
+ # Default properties
56
+ if properties is None:
57
+ properties = {
58
+ "answer": {
59
+ "type": "string",
60
+ "description": "Natural language response to the user"
61
+ }
62
+ }
63
+
64
+ # Default required
65
+ if required is None:
66
+ required = ["answer"]
67
+
68
+ # Default tools
69
+ if tools is None:
70
+ tools = DEFAULT_TOOLS.copy()
71
+
72
+ return {
73
+ "type": "object",
74
+ "description": description,
75
+ "properties": properties,
76
+ "required": required,
77
+ "json_schema_extra": {
78
+ "kind": "agent",
79
+ "name": name,
80
+ "version": version,
81
+ "tags": tags or [],
82
+ "tools": [{"name": t, "description": f"Tool: {t}"} for t in tools],
83
+ }
84
+ }
85
+
86
+
87
+ async def save_agent(
88
+ name: str,
89
+ description: str,
90
+ user_id: str,
91
+ properties: dict[str, Any] | None = None,
92
+ required: list[str] | None = None,
93
+ tools: list[str] | None = None,
94
+ tags: list[str] | None = None,
95
+ version: str = "1.0.0",
96
+ ) -> dict[str, Any]:
97
+ """
98
+ Save an agent schema to the database.
99
+
100
+ Args:
101
+ name: Agent name in kebab-case (e.g., "code-reviewer")
102
+ description: The agent's system prompt
103
+ user_id: User identifier for scoping
104
+ properties: Output schema properties
105
+ required: Required property names
106
+ tools: Tool names
107
+ tags: Categorization tags
108
+ version: Semantic version
109
+
110
+ Returns:
111
+ Dict with status, agent_name, version, message
112
+
113
+ Raises:
114
+ RuntimeError: If database is not available
115
+ """
116
+ from rem.models.entities import Schema
117
+ from rem.services.postgres import get_postgres_service
118
+
119
+ # Build the spec
120
+ spec = build_agent_spec(
121
+ name=name,
122
+ description=description,
123
+ properties=properties,
124
+ required=required,
125
+ tools=tools,
126
+ tags=tags,
127
+ version=version,
128
+ )
129
+
130
+ # Create Schema entity (user-scoped)
131
+ # Note: tenant_id defaults to "default" for anonymous users
132
+ schema_entity = Schema(
133
+ tenant_id=user_id or "default",
134
+ user_id=user_id,
135
+ name=name,
136
+ spec=spec,
137
+ category="agent",
138
+ metadata={
139
+ "version": version,
140
+ "tags": tags or [],
141
+ "created_via": "agent_manager",
142
+ },
143
+ )
144
+
145
+ # Save to database
146
+ postgres = get_postgres_service()
147
+ if not postgres:
148
+ raise RuntimeError("Database not available")
149
+
150
+ await postgres.connect()
151
+ try:
152
+ await postgres.batch_upsert(
153
+ records=[schema_entity],
154
+ model=Schema,
155
+ table_name="schemas",
156
+ entity_key_field="name",
157
+ generate_embeddings=False,
158
+ )
159
+ logger.info(f"✅ Agent saved: {name} (user={user_id}, version={version})")
160
+ finally:
161
+ await postgres.disconnect()
162
+
163
+ return {
164
+ "status": "success",
165
+ "agent_name": name,
166
+ "version": version,
167
+ "message": f"Agent '{name}' saved successfully.",
168
+ }
169
+
170
+
171
+ async def get_agent(
172
+ name: str,
173
+ user_id: str,
174
+ ) -> dict[str, Any] | None:
175
+ """
176
+ Get an agent schema by name.
177
+
178
+ Checks user's schemas first, then falls back to system schemas.
179
+
180
+ Args:
181
+ name: Agent name
182
+ user_id: User identifier
183
+
184
+ Returns:
185
+ Agent spec dict if found, None otherwise
186
+ """
187
+ from rem.services.postgres import get_postgres_service
188
+
189
+ postgres = get_postgres_service()
190
+ if not postgres:
191
+ return None
192
+
193
+ await postgres.connect()
194
+ try:
195
+ query = """
196
+ SELECT spec FROM schemas
197
+ WHERE LOWER(name) = LOWER($1)
198
+ AND category = 'agent'
199
+ AND (user_id = $2 OR user_id IS NULL OR tenant_id = 'system')
200
+ ORDER BY CASE WHEN user_id = $2 THEN 0 ELSE 1 END
201
+ LIMIT 1
202
+ """
203
+ row = await postgres.fetchrow(query, name, user_id)
204
+ if row:
205
+ return row["spec"]
206
+ return None
207
+ finally:
208
+ await postgres.disconnect()
209
+
210
+
211
+ async def list_agents(
212
+ user_id: str,
213
+ include_system: bool = True,
214
+ ) -> list[dict[str, Any]]:
215
+ """
216
+ List available agents for a user.
217
+
218
+ Args:
219
+ user_id: User identifier
220
+ include_system: Include system agents
221
+
222
+ Returns:
223
+ List of agent metadata dicts
224
+ """
225
+ from rem.services.postgres import get_postgres_service
226
+
227
+ postgres = get_postgres_service()
228
+ if not postgres:
229
+ return []
230
+
231
+ await postgres.connect()
232
+ try:
233
+ if include_system:
234
+ query = """
235
+ SELECT name, metadata, user_id, tenant_id
236
+ FROM schemas
237
+ WHERE category = 'agent'
238
+ AND (user_id = $1 OR user_id IS NULL OR tenant_id = 'system')
239
+ ORDER BY name
240
+ """
241
+ rows = await postgres.fetch(query, user_id)
242
+ else:
243
+ query = """
244
+ SELECT name, metadata, user_id, tenant_id
245
+ FROM schemas
246
+ WHERE category = 'agent'
247
+ AND user_id = $1
248
+ ORDER BY name
249
+ """
250
+ rows = await postgres.fetch(query, user_id)
251
+
252
+ return [
253
+ {
254
+ "name": row["name"],
255
+ "version": row["metadata"].get("version", "1.0.0") if row["metadata"] else "1.0.0",
256
+ "tags": row["metadata"].get("tags", []) if row["metadata"] else [],
257
+ "is_system": row["tenant_id"] == "system" or row["user_id"] is None,
258
+ }
259
+ for row in rows
260
+ ]
261
+ finally:
262
+ await postgres.disconnect()
263
+
264
+
265
+ async def delete_agent(
266
+ name: str,
267
+ user_id: str,
268
+ ) -> dict[str, Any]:
269
+ """
270
+ Delete a user's agent.
271
+
272
+ Only allows deleting user-owned agents, not system agents.
273
+
274
+ Args:
275
+ name: Agent name
276
+ user_id: User identifier
277
+
278
+ Returns:
279
+ Dict with status and message
280
+ """
281
+ from rem.services.postgres import get_postgres_service
282
+
283
+ postgres = get_postgres_service()
284
+ if not postgres:
285
+ raise RuntimeError("Database not available")
286
+
287
+ await postgres.connect()
288
+ try:
289
+ # Only delete user's own agents
290
+ query = """
291
+ DELETE FROM schemas
292
+ WHERE LOWER(name) = LOWER($1)
293
+ AND category = 'agent'
294
+ AND user_id = $2
295
+ RETURNING name
296
+ """
297
+ row = await postgres.fetchrow(query, name, user_id)
298
+
299
+ if row:
300
+ logger.info(f"🗑️ Agent deleted: {name} (user={user_id})")
301
+ return {
302
+ "status": "success",
303
+ "message": f"Agent '{name}' deleted.",
304
+ }
305
+ else:
306
+ return {
307
+ "status": "error",
308
+ "message": f"Agent '{name}' not found or not owned by you.",
309
+ }
310
+ finally:
311
+ await postgres.disconnect()