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.
- rem/__init__.py +129 -0
- rem/agentic/README.md +760 -0
- rem/agentic/__init__.py +54 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +38 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +425 -0
- rem/agentic/context_builder.py +360 -0
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +273 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +240 -0
- rem/agentic/providers/phoenix.py +926 -0
- rem/agentic/providers/pydantic_ai.py +854 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +737 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +242 -0
- rem/api/README.md +657 -0
- rem/api/deps.py +253 -0
- rem/api/main.py +460 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +820 -0
- rem/api/mcp_router/server.py +243 -0
- rem/api/mcp_router/tools.py +1605 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +520 -0
- rem/api/routers/auth.py +898 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +702 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +202 -0
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +546 -0
- rem/api/routers/chat/streaming.py +950 -0
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +87 -0
- rem/api/routers/feedback.py +276 -0
- rem/api/routers/messages.py +620 -0
- rem/api/routers/models.py +86 -0
- rem/api/routers/query.py +362 -0
- rem/api/routers/shared_sessions.py +422 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +36 -0
- rem/auth/jwt.py +367 -0
- rem/auth/middleware.py +318 -0
- rem/auth/providers/__init__.py +16 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/email.py +215 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +517 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +299 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +549 -0
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +495 -0
- rem/cli/commands/db.py +828 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1698 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +388 -0
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +230 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/commands/session.py +453 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +123 -0
- rem/config.py +244 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +70 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +672 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +246 -0
- rem/models/entities/__init__.py +68 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +64 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +181 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/session.py +84 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +93 -0
- rem/py.typed +0 -0
- rem/registry.py +373 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +132 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +18 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +760 -0
- rem/services/content/service.py +762 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +322 -0
- rem/services/dreaming/moment_service.py +251 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +127 -0
- rem/services/embeddings/worker.py +435 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +960 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +757 -0
- rem/services/postgres/__init__.py +49 -0
- rem/services/postgres/diff_service.py +599 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
- rem/services/postgres/register_type.py +353 -0
- rem/services/postgres/repository.py +481 -0
- rem/services/postgres/schema_generator.py +661 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +355 -0
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +318 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +180 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +608 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +13 -0
- rem/services/session/compression.py +488 -0
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +85 -0
- rem/services/user_service.py +130 -0
- rem/settings.py +1877 -0
- rem/sql/background_indexes.sql +52 -0
- rem/sql/migrations/001_install.sql +983 -0
- rem/sql/migrations/002_install_models.sql +3157 -0
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +282 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +628 -0
- rem/utils/__init__.py +61 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +436 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/files.py +323 -0
- rem/utils/markdown.py +16 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +492 -0
- rem/utils/schema_loader.py +649 -0
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +350 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +325 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +7 -0
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- rem/workers/unlogged_maintainer.py +463 -0
- remdb-0.3.242.dist-info/METADATA +1632 -0
- remdb-0.3.242.dist-info/RECORD +235 -0
- remdb-0.3.242.dist-info/WHEEL +4 -0
- remdb-0.3.242.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Resources for REM system information.
|
|
3
|
+
|
|
4
|
+
Resources are read-only data sources that LLMs can access for context.
|
|
5
|
+
They provide schema information, documentation, and system status.
|
|
6
|
+
|
|
7
|
+
Design Pattern:
|
|
8
|
+
- Resources are registered with the FastMCP server
|
|
9
|
+
- Resources return structured data (typically as strings or JSON)
|
|
10
|
+
- Resources don't modify system state (read-only)
|
|
11
|
+
- Resources help LLMs understand available operations
|
|
12
|
+
|
|
13
|
+
Available Resources:
|
|
14
|
+
- rem://schema/entities - Entity schemas documentation
|
|
15
|
+
- rem://schema/query-types - REM query types documentation
|
|
16
|
+
- rem://status - System health and statistics
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from fastmcp import FastMCP
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register_schema_resources(mcp: FastMCP):
|
|
23
|
+
"""
|
|
24
|
+
Register schema documentation resources.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
mcp: FastMCP server instance
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@mcp.resource("rem://schema/entities")
|
|
31
|
+
def get_entity_schemas() -> str:
|
|
32
|
+
"""
|
|
33
|
+
Get REM entity schemas documentation.
|
|
34
|
+
|
|
35
|
+
Returns complete schema information for all entity types:
|
|
36
|
+
- Resource: Chunked, embedded content
|
|
37
|
+
- Entity: Domain knowledge nodes
|
|
38
|
+
- Moment: Temporal narratives
|
|
39
|
+
- Message: Conversation messages
|
|
40
|
+
- User: System users
|
|
41
|
+
- File: File uploads
|
|
42
|
+
"""
|
|
43
|
+
return """
|
|
44
|
+
# REM Entity Schemas
|
|
45
|
+
|
|
46
|
+
## Resource
|
|
47
|
+
Chunked, embedded content from documents, files, conversations.
|
|
48
|
+
|
|
49
|
+
Fields:
|
|
50
|
+
- id: UUID (auto-generated)
|
|
51
|
+
- user_id: User identifier (primary data scoping field)
|
|
52
|
+
- tenant_id: Tenant identifier (legacy, set to user_id)
|
|
53
|
+
- name: Resource name/label (used for LOOKUP)
|
|
54
|
+
- content: Main text content
|
|
55
|
+
- category: Optional category (document, conversation, etc.)
|
|
56
|
+
- related_entities: JSONB array of extracted entity references
|
|
57
|
+
- graph_paths: JSONB array of InlineEdge objects
|
|
58
|
+
- resource_timestamp: Timestamp of resource creation
|
|
59
|
+
- metadata: JSONB flexible metadata dict
|
|
60
|
+
- created_at, updated_at, deleted_at: Temporal tracking
|
|
61
|
+
|
|
62
|
+
## Entity
|
|
63
|
+
Domain knowledge nodes with properties and relationships.
|
|
64
|
+
|
|
65
|
+
NOTE: Entities are stored within resources/moments, not in a separate table.
|
|
66
|
+
Entity IDs are human-readable labels (e.g., "sarah-chen", "api-design-v2").
|
|
67
|
+
|
|
68
|
+
## Moment
|
|
69
|
+
Temporal narratives and time-bound events.
|
|
70
|
+
|
|
71
|
+
Fields:
|
|
72
|
+
- id: UUID (auto-generated)
|
|
73
|
+
- user_id: User identifier (primary data scoping field)
|
|
74
|
+
- tenant_id: Tenant identifier (legacy, set to user_id)
|
|
75
|
+
- name: Moment name/label (used for LOOKUP)
|
|
76
|
+
- moment_type: Type (meeting, coding_session, conversation, etc.)
|
|
77
|
+
- resource_timestamp: Start time
|
|
78
|
+
- resource_ends_timestamp: End time
|
|
79
|
+
- present_persons: JSONB array of Person objects
|
|
80
|
+
- speakers: JSONB array of Speaker objects
|
|
81
|
+
- emotion_tags: Array of emotion tags
|
|
82
|
+
- topic_tags: Array of topic tags
|
|
83
|
+
- summary: Natural language summary
|
|
84
|
+
- source_resource_ids: Array of referenced resource UUIDs
|
|
85
|
+
- created_at, updated_at, deleted_at: Temporal tracking
|
|
86
|
+
|
|
87
|
+
## Message
|
|
88
|
+
Conversation messages with agents.
|
|
89
|
+
|
|
90
|
+
Fields:
|
|
91
|
+
- id: UUID (auto-generated)
|
|
92
|
+
- user_id: User identifier (primary data scoping field)
|
|
93
|
+
- tenant_id: Tenant identifier (legacy, set to user_id)
|
|
94
|
+
- role: Message role (user, assistant, system)
|
|
95
|
+
- content: Message text
|
|
96
|
+
- session_id: Conversation session identifier
|
|
97
|
+
- metadata: JSONB flexible metadata dict
|
|
98
|
+
- created_at, updated_at, deleted_at: Temporal tracking
|
|
99
|
+
|
|
100
|
+
## User
|
|
101
|
+
System users with authentication.
|
|
102
|
+
|
|
103
|
+
Fields:
|
|
104
|
+
- id: UUID (auto-generated)
|
|
105
|
+
- user_id: User identifier (primary data scoping field)
|
|
106
|
+
- tenant_id: Tenant identifier (legacy, set to user_id)
|
|
107
|
+
- name: User name
|
|
108
|
+
- email: User email
|
|
109
|
+
- metadata: JSONB flexible metadata dict
|
|
110
|
+
- created_at, updated_at, deleted_at: Temporal tracking
|
|
111
|
+
|
|
112
|
+
## File
|
|
113
|
+
File uploads with S3 storage.
|
|
114
|
+
|
|
115
|
+
Fields:
|
|
116
|
+
- id: UUID (auto-generated)
|
|
117
|
+
- user_id: User identifier (primary data scoping field)
|
|
118
|
+
- tenant_id: Tenant identifier (legacy, set to user_id)
|
|
119
|
+
- name: File name
|
|
120
|
+
- s3_key: S3 object key
|
|
121
|
+
- s3_bucket: S3 bucket name
|
|
122
|
+
- content_type: MIME type
|
|
123
|
+
- size_bytes: File size
|
|
124
|
+
- metadata: JSONB flexible metadata dict
|
|
125
|
+
- created_at, updated_at, deleted_at: Temporal tracking
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
@mcp.resource("rem://schema/query-types")
|
|
129
|
+
def get_query_types() -> str:
|
|
130
|
+
"""
|
|
131
|
+
Get REM query types documentation.
|
|
132
|
+
|
|
133
|
+
Returns comprehensive documentation for all REM query types
|
|
134
|
+
with examples and parameter specifications.
|
|
135
|
+
"""
|
|
136
|
+
return """
|
|
137
|
+
# REM Query Types
|
|
138
|
+
|
|
139
|
+
## LOOKUP
|
|
140
|
+
O(1) entity resolution across ALL tables using KV_STORE.
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
- entity_key (required): Entity label/name (e.g., "sarah-chen", "api-design-v2")
|
|
144
|
+
- user_id (optional): User scoping for private entities
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
```
|
|
148
|
+
rem_query(query_type="lookup", entity_key="Sarah Chen", user_id="user-123")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
- entity_key: The looked-up key
|
|
153
|
+
- entity_type: Entity type (person, document, etc.)
|
|
154
|
+
- entity_id: UUID of the entity
|
|
155
|
+
- content_summary: Summary of entity content
|
|
156
|
+
- metadata: Additional metadata
|
|
157
|
+
|
|
158
|
+
## FUZZY
|
|
159
|
+
Fuzzy text matching using pg_trgm similarity.
|
|
160
|
+
|
|
161
|
+
Parameters:
|
|
162
|
+
- query_text (required): Query string
|
|
163
|
+
- threshold (optional): Similarity threshold 0.0-1.0 (default: 0.7)
|
|
164
|
+
- limit (optional): Max results (default: 10)
|
|
165
|
+
- user_id (optional): User scoping
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
```
|
|
169
|
+
rem_query(query_type="fuzzy", query_text="sara", threshold=0.7, user_id="user-123")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
- Entities matching query with similarity scores
|
|
174
|
+
- Ordered by similarity (highest first)
|
|
175
|
+
|
|
176
|
+
## SEARCH
|
|
177
|
+
Semantic vector search using embeddings (table-specific).
|
|
178
|
+
|
|
179
|
+
Parameters:
|
|
180
|
+
- query_text (required): Natural language query
|
|
181
|
+
- table_name (required): Table to search (resources, moments, etc.)
|
|
182
|
+
- field_name (optional): Field to search (defaults to "content")
|
|
183
|
+
- provider (optional): Embedding provider (default: from LLM__EMBEDDING_PROVIDER setting)
|
|
184
|
+
- min_similarity (optional): Minimum similarity 0.0-1.0 (default: 0.3)
|
|
185
|
+
- limit (optional): Max results (default: 10)
|
|
186
|
+
- user_id (optional): User scoping
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
```
|
|
190
|
+
rem_query(
|
|
191
|
+
query_type="search",
|
|
192
|
+
query_text="database migration",
|
|
193
|
+
table_name="resources",
|
|
194
|
+
user_id="user-123"
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
- Semantically similar entities
|
|
200
|
+
- Ordered by similarity score
|
|
201
|
+
|
|
202
|
+
## SQL
|
|
203
|
+
Direct SQL queries with WHERE clauses (tenant-scoped).
|
|
204
|
+
|
|
205
|
+
Parameters:
|
|
206
|
+
- table_name (required): Table to query
|
|
207
|
+
- where_clause (optional): SQL WHERE condition
|
|
208
|
+
- limit (optional): Max results
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
```
|
|
212
|
+
rem_query(
|
|
213
|
+
query_type="sql",
|
|
214
|
+
table_name="moments",
|
|
215
|
+
where_clause="moment_type='meeting' AND resource_timestamp > '2025-01-01'",
|
|
216
|
+
user_id="user-123"
|
|
217
|
+
)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
- Matching rows from table
|
|
222
|
+
- Automatically scoped to user_id
|
|
223
|
+
|
|
224
|
+
## TRAVERSE
|
|
225
|
+
Multi-hop graph traversal with depth control.
|
|
226
|
+
|
|
227
|
+
Parameters:
|
|
228
|
+
- start_key (required): Starting entity key
|
|
229
|
+
- max_depth (optional): Maximum traversal depth (default: 1)
|
|
230
|
+
- depth=0: PLAN mode (analyze edges without traversal)
|
|
231
|
+
- depth=1+: Full traversal with cycle detection
|
|
232
|
+
- rel_type (optional): Filter by relationship type (e.g., "manages", "authored_by")
|
|
233
|
+
- user_id (optional): User scoping
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
```
|
|
237
|
+
rem_query(
|
|
238
|
+
query_type="traverse",
|
|
239
|
+
start_key="Sarah Chen",
|
|
240
|
+
max_depth=2,
|
|
241
|
+
rel_type="manages",
|
|
242
|
+
user_id="user-123"
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
- Traversed entities with depth info
|
|
248
|
+
- Relationship types and weights
|
|
249
|
+
- Path information for each node
|
|
250
|
+
|
|
251
|
+
## Multi-Turn Exploration
|
|
252
|
+
|
|
253
|
+
REM supports iterated retrieval where LLMs conduct multi-turn conversations
|
|
254
|
+
with the database:
|
|
255
|
+
|
|
256
|
+
Turn 1: Find entry point
|
|
257
|
+
```
|
|
258
|
+
LOOKUP "Sarah Chen"
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Turn 2: Analyze neighborhood (PLAN mode)
|
|
262
|
+
```
|
|
263
|
+
TRAVERSE start_key="Sarah Chen" max_depth=0
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Turn 3: Selective traversal
|
|
267
|
+
```
|
|
268
|
+
TRAVERSE start_key="Sarah Chen" rel_type="manages" max_depth=2
|
|
269
|
+
```
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def register_agent_resources(mcp: FastMCP):
|
|
274
|
+
"""
|
|
275
|
+
Register agent schema resources.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
mcp: FastMCP server instance
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
@mcp.resource("rem://agents")
|
|
282
|
+
def list_available_agents() -> str:
|
|
283
|
+
"""
|
|
284
|
+
List all available agent schemas.
|
|
285
|
+
|
|
286
|
+
Returns a list of agent schemas packaged with REM, including:
|
|
287
|
+
- Agent name
|
|
288
|
+
- Description
|
|
289
|
+
- Available tools
|
|
290
|
+
- Version information
|
|
291
|
+
|
|
292
|
+
TODO: Add pagination support if agent count grows large (not needed for now)
|
|
293
|
+
"""
|
|
294
|
+
import importlib.resources
|
|
295
|
+
import yaml
|
|
296
|
+
from pathlib import Path
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
# Find packaged agent schemas
|
|
300
|
+
agents_ref = importlib.resources.files("rem") / "schemas" / "agents"
|
|
301
|
+
agents_dir = Path(str(agents_ref))
|
|
302
|
+
|
|
303
|
+
if not agents_dir.exists():
|
|
304
|
+
return "# Available Agents\n\nNo agent schemas found in package."
|
|
305
|
+
|
|
306
|
+
# Discover all agent schemas recursively
|
|
307
|
+
agent_files = sorted(agents_dir.rglob("*.yaml")) + sorted(agents_dir.rglob("*.yml")) + sorted(agents_dir.rglob("*.json"))
|
|
308
|
+
|
|
309
|
+
if not agent_files:
|
|
310
|
+
return "# Available Agents\n\nNo agent schemas found."
|
|
311
|
+
|
|
312
|
+
output = ["# Available Agent Schemas\n"]
|
|
313
|
+
output.append("Packaged agent schemas available for use:\n")
|
|
314
|
+
|
|
315
|
+
for agent_file in agent_files:
|
|
316
|
+
try:
|
|
317
|
+
with open(agent_file, "r") as f:
|
|
318
|
+
schema = yaml.safe_load(f)
|
|
319
|
+
|
|
320
|
+
agent_name = agent_file.stem
|
|
321
|
+
description = schema.get("description", "No description")
|
|
322
|
+
# Get first 200 characters of description
|
|
323
|
+
desc_snippet = description[:200] + "..." if len(description) > 200 else description
|
|
324
|
+
|
|
325
|
+
# Get additional metadata
|
|
326
|
+
extra = schema.get("json_schema_extra", {})
|
|
327
|
+
version = extra.get("version", "unknown")
|
|
328
|
+
tools = extra.get("tools", [])
|
|
329
|
+
|
|
330
|
+
output.append(f"\n## {agent_name}")
|
|
331
|
+
output.append(f"**Path:** `agents/{agent_file.name}`")
|
|
332
|
+
output.append(f"**Version:** {version}")
|
|
333
|
+
output.append(f"**Description:** {desc_snippet}")
|
|
334
|
+
if tools:
|
|
335
|
+
output.append(f"**Tools:** {', '.join(tools[:5])}" + (" ..." if len(tools) > 5 else ""))
|
|
336
|
+
|
|
337
|
+
# Usage example
|
|
338
|
+
output.append(f"\n**Usage:**")
|
|
339
|
+
output.append(f"```python")
|
|
340
|
+
output.append(f'rem ask agents/{agent_file.name} "Your query here"')
|
|
341
|
+
output.append(f"```")
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
output.append(f"\n## {agent_file.stem}")
|
|
345
|
+
output.append(f"⚠️ Error loading schema: {e}")
|
|
346
|
+
|
|
347
|
+
return "\n".join(output)
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
return f"# Available Agents\n\nError listing agents: {e}"
|
|
351
|
+
|
|
352
|
+
@mcp.resource("rem://agents/{agent_name}")
|
|
353
|
+
def get_agent_schema(agent_name: str) -> str:
|
|
354
|
+
"""
|
|
355
|
+
Get a specific agent schema by name.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
agent_name: Name of the agent (e.g., "ask_rem", "agent-builder")
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Full agent schema as YAML string, or error message if not found.
|
|
362
|
+
"""
|
|
363
|
+
import importlib.resources
|
|
364
|
+
import yaml
|
|
365
|
+
from pathlib import Path
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
# Find packaged agent schemas
|
|
369
|
+
agents_ref = importlib.resources.files("rem") / "schemas" / "agents"
|
|
370
|
+
agents_dir = Path(str(agents_ref))
|
|
371
|
+
|
|
372
|
+
if not agents_dir.exists():
|
|
373
|
+
return f"# Agent Not Found\n\nNo agent schemas directory found."
|
|
374
|
+
|
|
375
|
+
# Search for agent file (try multiple extensions)
|
|
376
|
+
for ext in [".yaml", ".yml", ".json"]:
|
|
377
|
+
# Try exact match first
|
|
378
|
+
agent_file = agents_dir / f"{agent_name}{ext}"
|
|
379
|
+
if agent_file.exists():
|
|
380
|
+
with open(agent_file, "r") as f:
|
|
381
|
+
content = f.read()
|
|
382
|
+
return f"# Agent Schema: {agent_name}\n\n```yaml\n{content}\n```"
|
|
383
|
+
|
|
384
|
+
# Try recursive search
|
|
385
|
+
matches = list(agents_dir.rglob(f"{agent_name}{ext}"))
|
|
386
|
+
if matches:
|
|
387
|
+
with open(matches[0], "r") as f:
|
|
388
|
+
content = f.read()
|
|
389
|
+
return f"# Agent Schema: {agent_name}\n\n```yaml\n{content}\n```"
|
|
390
|
+
|
|
391
|
+
# Not found - list available agents
|
|
392
|
+
available = [f.stem for f in agents_dir.rglob("*.yaml")] + \
|
|
393
|
+
[f.stem for f in agents_dir.rglob("*.yml")]
|
|
394
|
+
return f"# Agent Not Found\n\nAgent '{agent_name}' not found.\n\nAvailable agents: {', '.join(sorted(set(available)))}"
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
return f"# Error\n\nError loading agent '{agent_name}': {e}"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def register_file_resources(mcp: FastMCP):
|
|
401
|
+
"""
|
|
402
|
+
Register file operation resources.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
mcp: FastMCP server instance
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
@mcp.resource("rem://files/presigned-url/{s3_key}")
|
|
409
|
+
def get_presigned_url(s3_key: str, expiration: int = 3600) -> str:
|
|
410
|
+
"""
|
|
411
|
+
Generate presigned URL for S3 object download.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
s3_key: S3 object key (e.g., "tenant/files/uuid/file.pdf")
|
|
415
|
+
expiration: URL expiration time in seconds (default: 3600 = 1 hour)
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Presigned URL for downloading the file
|
|
419
|
+
|
|
420
|
+
Raises:
|
|
421
|
+
RuntimeError: If S3 is not configured
|
|
422
|
+
|
|
423
|
+
Example:
|
|
424
|
+
>>> url = get_presigned_url("acme/files/123/document.pdf")
|
|
425
|
+
>>> # Returns: https://s3.amazonaws.com/bucket/acme/files/123/document.pdf?signature=...
|
|
426
|
+
"""
|
|
427
|
+
from ...settings import settings
|
|
428
|
+
|
|
429
|
+
# Check if S3 is configured
|
|
430
|
+
if not settings.s3.bucket_name:
|
|
431
|
+
raise RuntimeError(
|
|
432
|
+
"S3 is not configured. Cannot generate presigned URLs.\n"
|
|
433
|
+
"Configure S3 settings in ~/.rem/config.yaml or environment variables."
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
import aioboto3
|
|
437
|
+
import asyncio
|
|
438
|
+
from botocore.exceptions import ClientError
|
|
439
|
+
|
|
440
|
+
async def _generate_url():
|
|
441
|
+
session = aioboto3.Session()
|
|
442
|
+
async with session.client(
|
|
443
|
+
"s3",
|
|
444
|
+
endpoint_url=settings.s3.endpoint_url,
|
|
445
|
+
aws_access_key_id=settings.s3.access_key_id,
|
|
446
|
+
aws_secret_access_key=settings.s3.secret_access_key,
|
|
447
|
+
region_name=settings.s3.region,
|
|
448
|
+
) as s3_client:
|
|
449
|
+
try:
|
|
450
|
+
url = await s3_client.generate_presigned_url(
|
|
451
|
+
"get_object",
|
|
452
|
+
Params={
|
|
453
|
+
"Bucket": settings.s3.bucket_name,
|
|
454
|
+
"Key": s3_key,
|
|
455
|
+
},
|
|
456
|
+
ExpiresIn=expiration,
|
|
457
|
+
)
|
|
458
|
+
return url
|
|
459
|
+
except ClientError as e:
|
|
460
|
+
raise RuntimeError(f"Failed to generate presigned URL: {e}")
|
|
461
|
+
|
|
462
|
+
# Run async function
|
|
463
|
+
loop = asyncio.get_event_loop()
|
|
464
|
+
if loop.is_running():
|
|
465
|
+
# If already in async context, create new loop
|
|
466
|
+
import nest_asyncio
|
|
467
|
+
nest_asyncio.apply()
|
|
468
|
+
url = loop.run_until_complete(_generate_url())
|
|
469
|
+
else:
|
|
470
|
+
url = asyncio.run(_generate_url())
|
|
471
|
+
|
|
472
|
+
return url
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def register_status_resources(mcp: FastMCP):
|
|
476
|
+
"""
|
|
477
|
+
Register system status resources.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
mcp: FastMCP server instance
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
@mcp.resource("rem://status")
|
|
484
|
+
def get_system_status() -> str:
|
|
485
|
+
"""
|
|
486
|
+
Get REM system health and statistics.
|
|
487
|
+
|
|
488
|
+
Returns system information including:
|
|
489
|
+
- Service health
|
|
490
|
+
- Database connection status
|
|
491
|
+
- Environment configuration
|
|
492
|
+
- Available query types
|
|
493
|
+
"""
|
|
494
|
+
from ...settings import settings
|
|
495
|
+
|
|
496
|
+
return f"""
|
|
497
|
+
# REM System Status
|
|
498
|
+
|
|
499
|
+
## Environment
|
|
500
|
+
- Environment: {settings.environment}
|
|
501
|
+
- Team: {settings.team}
|
|
502
|
+
- Root Path: {settings.root_path or '/'}
|
|
503
|
+
|
|
504
|
+
## LLM Configuration
|
|
505
|
+
- Default Model: {settings.llm.default_model}
|
|
506
|
+
- Default Temperature: {settings.llm.default_temperature}
|
|
507
|
+
- Embedding Provider: {settings.llm.embedding_provider}
|
|
508
|
+
- Embedding Model: {settings.llm.embedding_model}
|
|
509
|
+
- OpenAI API Key: {"✓ Configured" if settings.llm.openai_api_key else "✗ Not configured"}
|
|
510
|
+
- Anthropic API Key: {"✓ Configured" if settings.llm.anthropic_api_key else "✗ Not configured"}
|
|
511
|
+
|
|
512
|
+
## Database
|
|
513
|
+
- PostgreSQL: {settings.postgres.connection_string}
|
|
514
|
+
|
|
515
|
+
## S3 Storage
|
|
516
|
+
- Bucket: {settings.s3.bucket_name}
|
|
517
|
+
- Region: {settings.s3.region}
|
|
518
|
+
|
|
519
|
+
## Observability
|
|
520
|
+
- OTEL Enabled: {settings.otel.enabled}
|
|
521
|
+
- Phoenix Enabled: {settings.phoenix.enabled}
|
|
522
|
+
|
|
523
|
+
## Authentication
|
|
524
|
+
- Auth Enabled: {settings.auth.enabled}
|
|
525
|
+
|
|
526
|
+
## Available Query Types
|
|
527
|
+
- LOOKUP: O(1) entity resolution
|
|
528
|
+
- FUZZY: Fuzzy text matching
|
|
529
|
+
- SEARCH: Semantic vector search
|
|
530
|
+
- SQL: Direct SQL queries
|
|
531
|
+
- TRAVERSE: Multi-hop graph traversal
|
|
532
|
+
|
|
533
|
+
## MCP Tools
|
|
534
|
+
- search_rem: Execute REM queries (LOOKUP, FUZZY, SEARCH, SQL, TRAVERSE)
|
|
535
|
+
- ask_rem_agent: Natural language to REM query conversion
|
|
536
|
+
- ingest_into_rem: File ingestion pipeline
|
|
537
|
+
- read_resource: Access MCP resources
|
|
538
|
+
|
|
539
|
+
## Status
|
|
540
|
+
✓ System operational
|
|
541
|
+
✓ Ready to process queries
|
|
542
|
+
"""
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def register_session_resources(mcp: FastMCP):
|
|
546
|
+
"""
|
|
547
|
+
Register session resources for loading conversation history.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
mcp: FastMCP server instance
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
@mcp.resource("rem://sessions/{session_id}")
|
|
554
|
+
async def get_session_messages(session_id: str) -> str:
|
|
555
|
+
"""
|
|
556
|
+
Load a conversation session by ID.
|
|
557
|
+
|
|
558
|
+
Returns the full message history including user messages, assistant responses,
|
|
559
|
+
and tool calls. Useful for evaluators and analysis agents.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
session_id: Session UUID or identifier
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Formatted conversation history as markdown string with:
|
|
566
|
+
- Message type (user/assistant/tool)
|
|
567
|
+
- Content
|
|
568
|
+
- Timestamps
|
|
569
|
+
- Tool call details (if any)
|
|
570
|
+
"""
|
|
571
|
+
from ...services.postgres import get_postgres_service
|
|
572
|
+
|
|
573
|
+
pg = get_postgres_service()
|
|
574
|
+
await pg.connect()
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
# Query messages for session
|
|
578
|
+
query = """
|
|
579
|
+
SELECT id, message_type, content, metadata, created_at
|
|
580
|
+
FROM messages
|
|
581
|
+
WHERE session_id = $1
|
|
582
|
+
ORDER BY created_at ASC
|
|
583
|
+
"""
|
|
584
|
+
messages = await pg.fetch(query, session_id)
|
|
585
|
+
|
|
586
|
+
if not messages:
|
|
587
|
+
return f"# Session Not Found\n\nNo messages found for session_id: {session_id}"
|
|
588
|
+
|
|
589
|
+
# Format output
|
|
590
|
+
output = [f"# Session: {session_id}\n"]
|
|
591
|
+
output.append(f"**Total messages:** {len(messages)}\n")
|
|
592
|
+
|
|
593
|
+
for i, msg in enumerate(messages, 1):
|
|
594
|
+
msg_type = msg['message_type']
|
|
595
|
+
content = msg['content'] or "(empty)"
|
|
596
|
+
created = msg['created_at']
|
|
597
|
+
metadata = msg.get('metadata') or {}
|
|
598
|
+
|
|
599
|
+
# Format based on message type
|
|
600
|
+
if msg_type == 'user':
|
|
601
|
+
output.append(f"\n## [{i}] USER ({created})")
|
|
602
|
+
output.append(f"```\n{content[:1000]}{'...' if len(content) > 1000 else ''}\n```")
|
|
603
|
+
elif msg_type == 'assistant':
|
|
604
|
+
output.append(f"\n## [{i}] ASSISTANT ({created})")
|
|
605
|
+
output.append(f"```\n{content[:1000]}{'...' if len(content) > 1000 else ''}\n```")
|
|
606
|
+
elif msg_type == 'tool':
|
|
607
|
+
tool_name = metadata.get('tool_name', 'unknown')
|
|
608
|
+
output.append(f"\n## [{i}] TOOL: {tool_name} ({created})")
|
|
609
|
+
# Truncate tool results more aggressively
|
|
610
|
+
output.append(f"```json\n{content[:500]}{'...' if len(content) > 500 else ''}\n```")
|
|
611
|
+
else:
|
|
612
|
+
output.append(f"\n## [{i}] {msg_type.upper()} ({created})")
|
|
613
|
+
output.append(f"```\n{content[:500]}{'...' if len(content) > 500 else ''}\n```")
|
|
614
|
+
|
|
615
|
+
return "\n".join(output)
|
|
616
|
+
|
|
617
|
+
finally:
|
|
618
|
+
await pg.disconnect()
|
|
619
|
+
|
|
620
|
+
@mcp.resource("rem://sessions")
|
|
621
|
+
async def list_recent_sessions() -> str:
|
|
622
|
+
"""
|
|
623
|
+
List recent sessions with basic info.
|
|
624
|
+
|
|
625
|
+
Returns the most recent 20 sessions with:
|
|
626
|
+
- Session ID
|
|
627
|
+
- First user message (preview)
|
|
628
|
+
- Message count
|
|
629
|
+
- Timestamp
|
|
630
|
+
"""
|
|
631
|
+
from ...services.postgres import get_postgres_service
|
|
632
|
+
|
|
633
|
+
pg = get_postgres_service()
|
|
634
|
+
await pg.connect()
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
# Query recent sessions
|
|
638
|
+
query = """
|
|
639
|
+
SELECT
|
|
640
|
+
session_id,
|
|
641
|
+
MIN(created_at) as started_at,
|
|
642
|
+
COUNT(*) as message_count,
|
|
643
|
+
MIN(CASE WHEN message_type = 'user' THEN content END) as first_message
|
|
644
|
+
FROM messages
|
|
645
|
+
WHERE session_id IS NOT NULL
|
|
646
|
+
GROUP BY session_id
|
|
647
|
+
ORDER BY MIN(created_at) DESC
|
|
648
|
+
LIMIT 20
|
|
649
|
+
"""
|
|
650
|
+
sessions = await pg.fetch(query)
|
|
651
|
+
|
|
652
|
+
if not sessions:
|
|
653
|
+
return "# Recent Sessions\n\nNo sessions found."
|
|
654
|
+
|
|
655
|
+
output = ["# Recent Sessions\n"]
|
|
656
|
+
output.append(f"Showing {len(sessions)} most recent sessions:\n")
|
|
657
|
+
|
|
658
|
+
for session in sessions:
|
|
659
|
+
session_id = session['session_id']
|
|
660
|
+
started = session['started_at']
|
|
661
|
+
count = session['message_count']
|
|
662
|
+
first_msg = session['first_message'] or "(no user message)"
|
|
663
|
+
preview = first_msg[:80] + "..." if len(first_msg) > 80 else first_msg
|
|
664
|
+
|
|
665
|
+
output.append(f"\n## {session_id}")
|
|
666
|
+
output.append(f"- **Started:** {started}")
|
|
667
|
+
output.append(f"- **Messages:** {count}")
|
|
668
|
+
output.append(f"- **First message:** {preview}")
|
|
669
|
+
output.append(f"- **Load:** `rem://sessions/{session_id}`")
|
|
670
|
+
|
|
671
|
+
return "\n".join(output)
|
|
672
|
+
|
|
673
|
+
finally:
|
|
674
|
+
await pg.disconnect()
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def register_user_resources(mcp: FastMCP):
|
|
678
|
+
"""
|
|
679
|
+
Register user profile resources for on-demand profile loading.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
mcp: FastMCP server instance
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
@mcp.resource("user://profile/{user_id}")
|
|
686
|
+
async def get_user_profile(user_id: str) -> str:
|
|
687
|
+
"""
|
|
688
|
+
Load a user's profile by ID.
|
|
689
|
+
|
|
690
|
+
Returns the user's profile information including:
|
|
691
|
+
- Email and name
|
|
692
|
+
- Summary (AI-generated profile summary)
|
|
693
|
+
- Interests and preferred topics
|
|
694
|
+
- Activity level
|
|
695
|
+
|
|
696
|
+
This resource is protected - each user can only access their own profile.
|
|
697
|
+
The user_id should match the authenticated user's ID from the JWT token.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
user_id: User UUID from authentication
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
Formatted user profile as markdown string, or error if not found
|
|
704
|
+
"""
|
|
705
|
+
from ...services.postgres import get_postgres_service
|
|
706
|
+
from ...services.postgres.repository import Repository
|
|
707
|
+
from ...models.entities.user import User
|
|
708
|
+
|
|
709
|
+
pg = get_postgres_service()
|
|
710
|
+
await pg.connect()
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
user_repo = Repository(User, "users", db=pg)
|
|
714
|
+
# Look up user by ID (user_id from JWT is the primary key)
|
|
715
|
+
user = await user_repo.get_by_id(user_id, tenant_id=None)
|
|
716
|
+
|
|
717
|
+
if not user:
|
|
718
|
+
return f"# User Profile Not Found\n\nNo user found with ID: {user_id}"
|
|
719
|
+
|
|
720
|
+
# Build profile output
|
|
721
|
+
output = [f"# User Profile: {user.name or user.email or 'Unknown'}"]
|
|
722
|
+
output.append("")
|
|
723
|
+
|
|
724
|
+
if user.email:
|
|
725
|
+
output.append(f"**Email:** {user.email}")
|
|
726
|
+
|
|
727
|
+
if user.role:
|
|
728
|
+
output.append(f"**Role:** {user.role}")
|
|
729
|
+
|
|
730
|
+
if user.tier:
|
|
731
|
+
output.append(f"**Tier:** {user.tier.value if hasattr(user.tier, 'value') else user.tier}")
|
|
732
|
+
|
|
733
|
+
if user.summary:
|
|
734
|
+
output.append(f"\n## Summary\n{user.summary}")
|
|
735
|
+
|
|
736
|
+
if user.interests:
|
|
737
|
+
output.append(f"\n## Interests\n- " + "\n- ".join(user.interests[:10]))
|
|
738
|
+
|
|
739
|
+
if user.preferred_topics:
|
|
740
|
+
output.append(f"\n## Preferred Topics\n- " + "\n- ".join(user.preferred_topics[:10]))
|
|
741
|
+
|
|
742
|
+
if user.activity_level:
|
|
743
|
+
output.append(f"\n**Activity Level:** {user.activity_level}")
|
|
744
|
+
|
|
745
|
+
if user.last_active_at:
|
|
746
|
+
output.append(f"**Last Active:** {user.last_active_at}")
|
|
747
|
+
|
|
748
|
+
# Add metadata if present (but redact sensitive fields)
|
|
749
|
+
if user.metadata:
|
|
750
|
+
safe_metadata = {k: v for k, v in user.metadata.items()
|
|
751
|
+
if k not in ('login_code', 'password', 'token', 'secret')}
|
|
752
|
+
if safe_metadata:
|
|
753
|
+
output.append(f"\n## Additional Info")
|
|
754
|
+
for key, value in list(safe_metadata.items())[:5]:
|
|
755
|
+
output.append(f"- **{key}:** {value}")
|
|
756
|
+
|
|
757
|
+
return "\n".join(output)
|
|
758
|
+
|
|
759
|
+
except Exception as e:
|
|
760
|
+
return f"# Error Loading Profile\n\nFailed to load user profile: {e}"
|
|
761
|
+
|
|
762
|
+
finally:
|
|
763
|
+
await pg.disconnect()
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
# Resource dispatcher for read_resource tool
|
|
767
|
+
async def load_resource(uri: str) -> dict | str:
|
|
768
|
+
"""
|
|
769
|
+
Load an MCP resource by URI.
|
|
770
|
+
|
|
771
|
+
This function is called by the read_resource tool to dispatch to
|
|
772
|
+
registered resource handlers. Supports both regular resources and
|
|
773
|
+
parameterized resource templates (e.g., rem://agents/{agent_name}).
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
uri: Resource URI (e.g., "rem://agents", "rem://agents/ask_rem", "rem://status")
|
|
777
|
+
|
|
778
|
+
Returns:
|
|
779
|
+
Resource data (dict or string)
|
|
780
|
+
|
|
781
|
+
Raises:
|
|
782
|
+
ValueError: If URI is invalid or resource not found
|
|
783
|
+
"""
|
|
784
|
+
import inspect
|
|
785
|
+
from fastmcp import FastMCP
|
|
786
|
+
|
|
787
|
+
# Create temporary MCP instance with resources
|
|
788
|
+
mcp = FastMCP(name="temp")
|
|
789
|
+
|
|
790
|
+
# Register all resources
|
|
791
|
+
register_schema_resources(mcp)
|
|
792
|
+
register_agent_resources(mcp)
|
|
793
|
+
register_file_resources(mcp)
|
|
794
|
+
register_status_resources(mcp)
|
|
795
|
+
register_session_resources(mcp)
|
|
796
|
+
register_user_resources(mcp)
|
|
797
|
+
|
|
798
|
+
# 1. Try exact match in regular resources
|
|
799
|
+
resources = await mcp.get_resources()
|
|
800
|
+
if uri in resources:
|
|
801
|
+
resource = resources[uri]
|
|
802
|
+
result = resource.fn()
|
|
803
|
+
if inspect.iscoroutine(result):
|
|
804
|
+
result = await result
|
|
805
|
+
return result if result else {"error": "Resource returned None"}
|
|
806
|
+
|
|
807
|
+
# 2. Try matching against parameterized resource templates
|
|
808
|
+
templates = await mcp.get_resource_templates()
|
|
809
|
+
for template_uri, template in templates.items():
|
|
810
|
+
params = template.matches(uri)
|
|
811
|
+
if params is not None:
|
|
812
|
+
# Template matched - call function with extracted parameters
|
|
813
|
+
result = template.fn(**params)
|
|
814
|
+
if inspect.iscoroutine(result):
|
|
815
|
+
result = await result
|
|
816
|
+
return result if result else {"error": "Resource returned None"}
|
|
817
|
+
|
|
818
|
+
# 3. Not found - include both resources and templates in error
|
|
819
|
+
available = list(resources.keys()) + list(templates.keys())
|
|
820
|
+
raise ValueError(f"Resource not found: {uri}. Available resources: {available}")
|