remdb 0.3.202__py3-none-any.whl → 0.3.245__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/README.md +36 -2
- rem/agentic/context.py +86 -3
- rem/agentic/context_builder.py +39 -33
- rem/agentic/mcp/tool_wrapper.py +2 -2
- rem/agentic/providers/pydantic_ai.py +68 -51
- rem/agentic/schema.py +2 -2
- rem/api/mcp_router/resources.py +223 -0
- rem/api/mcp_router/tools.py +170 -18
- rem/api/routers/admin.py +30 -4
- rem/api/routers/auth.py +175 -18
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +24 -29
- rem/api/routers/chat/sse_events.py +5 -1
- rem/api/routers/chat/streaming.py +242 -272
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +7 -1
- rem/api/routers/feedback.py +9 -1
- rem/api/routers/messages.py +80 -15
- rem/api/routers/models.py +9 -1
- rem/api/routers/query.py +17 -15
- rem/api/routers/shared_sessions.py +16 -0
- rem/cli/commands/ask.py +205 -114
- rem/cli/commands/process.py +12 -4
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/session.py +117 -0
- rem/cli/main.py +2 -0
- rem/models/entities/session.py +1 -0
- rem/schemas/agents/rem.yaml +1 -1
- rem/services/postgres/repository.py +7 -7
- rem/services/rem/service.py +47 -0
- rem/services/session/__init__.py +2 -1
- rem/services/session/compression.py +14 -12
- rem/services/session/pydantic_messages.py +111 -11
- rem/services/session/reload.py +2 -1
- rem/settings.py +71 -0
- rem/sql/migrations/001_install.sql +4 -4
- rem/sql/migrations/004_cache_system.sql +3 -1
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/schema_loader.py +139 -111
- {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/METADATA +2 -2
- {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/RECORD +44 -39
- {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/WHEEL +0 -0
- {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/entry_points.txt +0 -0
rem/agentic/README.md
CHANGED
|
@@ -716,11 +716,45 @@ curl -X POST http://localhost:8000/api/v1/chat/completions \
|
|
|
716
716
|
|
|
717
717
|
See `rem/api/README.md` for full SSE event protocol documentation.
|
|
718
718
|
|
|
719
|
+
## Multi-Agent Orchestration
|
|
720
|
+
|
|
721
|
+
Agents can delegate work to other agents via the `ask_agent` tool. This enables orchestrator patterns where a parent agent routes to specialists.
|
|
722
|
+
|
|
723
|
+
### How It Works
|
|
724
|
+
|
|
725
|
+
1. **Parent agent** calls `ask_agent(agent_name, input_text)`
|
|
726
|
+
2. **Child agent** executes and streams its response
|
|
727
|
+
3. **Child events** bubble up to parent via an event sink (asyncio.Queue in ContextVar)
|
|
728
|
+
4. **All tool calls** are saved to the database for the session
|
|
729
|
+
|
|
730
|
+
### Key Components
|
|
731
|
+
|
|
732
|
+
| Component | Location | Purpose |
|
|
733
|
+
|-----------|----------|---------|
|
|
734
|
+
| `ask_agent` tool | `mcp_router/tools.py` | Loads child agent, runs with streaming, pushes events to sink |
|
|
735
|
+
| Event sink | `context.py` | ContextVar holding asyncio.Queue for child→parent event flow |
|
|
736
|
+
| Streaming controller | `streaming.py` | Drains event sink, emits SSE events, saves to DB |
|
|
737
|
+
|
|
738
|
+
### Event Types
|
|
739
|
+
|
|
740
|
+
Child agents emit events that the parent streams to the client:
|
|
741
|
+
|
|
742
|
+
- **`child_tool_start`**: Child is calling a tool (logged, streamed, saved to DB)
|
|
743
|
+
- **`child_content`**: Child's text response (streamed as SSE content delta)
|
|
744
|
+
- **`child_tool_result`**: Tool completed with result (metadata extraction)
|
|
745
|
+
|
|
746
|
+
### Testing
|
|
747
|
+
|
|
748
|
+
Integration tests in `tests/integration/test_ask_agent_streaming.py` verify:
|
|
749
|
+
- Child content streams correctly to client
|
|
750
|
+
- Tool calls are persisted to database
|
|
751
|
+
- Multi-turn conversations save all messages
|
|
752
|
+
|
|
719
753
|
## Future Work
|
|
720
754
|
|
|
721
755
|
- [ ] Phoenix evaluator integration
|
|
722
756
|
- [ ] Agent schema registry (load schemas by URI)
|
|
723
757
|
- [ ] Schema validation and versioning
|
|
724
|
-
- [
|
|
725
|
-
- [
|
|
758
|
+
- [x] Multi-turn conversation management
|
|
759
|
+
- [x] Agent composition (agents calling agents)
|
|
726
760
|
- [ ] Alternative provider implementations (if needed)
|
rem/agentic/context.py
CHANGED
|
@@ -16,6 +16,7 @@ Headers Mapping:
|
|
|
16
16
|
X-Agent-Schema → context.agent_schema_uri (default: "rem")
|
|
17
17
|
X-Model-Name → context.default_model
|
|
18
18
|
X-Is-Eval → context.is_eval (marks session as evaluation)
|
|
19
|
+
X-Client-Id → context.client_id (e.g., "web", "mobile", "cli")
|
|
19
20
|
|
|
20
21
|
Key Design Pattern:
|
|
21
22
|
- AgentContext is passed to agent factory, not stored in agents
|
|
@@ -30,9 +31,10 @@ Multi-Agent Context Propagation:
|
|
|
30
31
|
- Child agents inherit user_id, tenant_id, session_id, is_eval from parent
|
|
31
32
|
"""
|
|
32
33
|
|
|
34
|
+
import asyncio
|
|
33
35
|
from contextlib import contextmanager
|
|
34
36
|
from contextvars import ContextVar
|
|
35
|
-
from typing import Generator
|
|
37
|
+
from typing import Any, Generator
|
|
36
38
|
|
|
37
39
|
from loguru import logger
|
|
38
40
|
from pydantic import BaseModel, Field
|
|
@@ -46,6 +48,13 @@ _current_agent_context: ContextVar["AgentContext | None"] = ContextVar(
|
|
|
46
48
|
"current_agent_context", default=None
|
|
47
49
|
)
|
|
48
50
|
|
|
51
|
+
# Event sink for streaming child agent events to parent
|
|
52
|
+
# When set, child agents (via ask_agent) should push their events here
|
|
53
|
+
# for the parent's streaming loop to proxy to the client
|
|
54
|
+
_parent_event_sink: ContextVar["asyncio.Queue | None"] = ContextVar(
|
|
55
|
+
"parent_event_sink", default=None
|
|
56
|
+
)
|
|
57
|
+
|
|
49
58
|
|
|
50
59
|
def get_current_context() -> "AgentContext | None":
|
|
51
60
|
"""
|
|
@@ -97,6 +106,70 @@ def agent_context_scope(ctx: "AgentContext") -> Generator["AgentContext", None,
|
|
|
97
106
|
_current_agent_context.set(previous)
|
|
98
107
|
|
|
99
108
|
|
|
109
|
+
# =============================================================================
|
|
110
|
+
# Event Sink for Streaming Multi-Agent Delegation
|
|
111
|
+
# =============================================================================
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_event_sink() -> "asyncio.Queue | None":
|
|
115
|
+
"""
|
|
116
|
+
Get the parent's event sink for streaming child events.
|
|
117
|
+
|
|
118
|
+
Used by ask_agent to push child agent events to the parent's stream.
|
|
119
|
+
Returns None if not in a streaming context.
|
|
120
|
+
"""
|
|
121
|
+
return _parent_event_sink.get()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def set_event_sink(sink: "asyncio.Queue | None") -> None:
|
|
125
|
+
"""Set the event sink for child agents to push events to."""
|
|
126
|
+
_parent_event_sink.set(sink)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@contextmanager
|
|
130
|
+
def event_sink_scope(sink: "asyncio.Queue") -> Generator["asyncio.Queue", None, None]:
|
|
131
|
+
"""
|
|
132
|
+
Context manager for scoped event sink setting.
|
|
133
|
+
|
|
134
|
+
Used by streaming layer to set up event proxying before tool execution.
|
|
135
|
+
Child agents (via ask_agent) will push their events to this sink.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
event_queue = asyncio.Queue()
|
|
139
|
+
with event_sink_scope(event_queue):
|
|
140
|
+
# ask_agent will push child events to event_queue
|
|
141
|
+
async for event in tools_stream:
|
|
142
|
+
...
|
|
143
|
+
# Also consume from event_queue
|
|
144
|
+
"""
|
|
145
|
+
previous = _parent_event_sink.get()
|
|
146
|
+
_parent_event_sink.set(sink)
|
|
147
|
+
try:
|
|
148
|
+
yield sink
|
|
149
|
+
finally:
|
|
150
|
+
_parent_event_sink.set(previous)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def push_event(event: Any) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
Push an event to the parent's event sink (if available).
|
|
156
|
+
|
|
157
|
+
Used by ask_agent to proxy child agent events to the parent's stream.
|
|
158
|
+
Returns True if event was pushed, False if no sink available.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
event: Any streaming event (ToolCallEvent, content chunk, etc.)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if event was pushed to sink, False otherwise
|
|
165
|
+
"""
|
|
166
|
+
sink = _parent_event_sink.get()
|
|
167
|
+
if sink is not None:
|
|
168
|
+
await sink.put(event)
|
|
169
|
+
return True
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
100
173
|
class AgentContext(BaseModel):
|
|
101
174
|
"""
|
|
102
175
|
Session and configuration context for agent execution.
|
|
@@ -150,6 +223,11 @@ class AgentContext(BaseModel):
|
|
|
150
223
|
description="Whether this is an evaluation session (set via X-Is-Eval header)",
|
|
151
224
|
)
|
|
152
225
|
|
|
226
|
+
client_id: str | None = Field(
|
|
227
|
+
default=None,
|
|
228
|
+
description="Client identifier (e.g., 'web', 'mobile', 'cli') set via X-Client-Id header",
|
|
229
|
+
)
|
|
230
|
+
|
|
153
231
|
model_config = {"populate_by_name": True}
|
|
154
232
|
|
|
155
233
|
def child_context(
|
|
@@ -160,7 +238,7 @@ class AgentContext(BaseModel):
|
|
|
160
238
|
"""
|
|
161
239
|
Create a child context for nested agent calls.
|
|
162
240
|
|
|
163
|
-
Inherits user_id, tenant_id, session_id, is_eval from parent.
|
|
241
|
+
Inherits user_id, tenant_id, session_id, is_eval, client_id from parent.
|
|
164
242
|
Allows overriding agent_schema_uri and default_model for the child.
|
|
165
243
|
|
|
166
244
|
Args:
|
|
@@ -184,6 +262,7 @@ class AgentContext(BaseModel):
|
|
|
184
262
|
default_model=model_override or self.default_model,
|
|
185
263
|
agent_schema_uri=agent_schema_uri or self.agent_schema_uri,
|
|
186
264
|
is_eval=self.is_eval,
|
|
265
|
+
client_id=self.client_id,
|
|
187
266
|
)
|
|
188
267
|
|
|
189
268
|
@staticmethod
|
|
@@ -302,6 +381,7 @@ class AgentContext(BaseModel):
|
|
|
302
381
|
default_model=normalized.get("x-model-name") or settings.llm.default_model,
|
|
303
382
|
agent_schema_uri=normalized.get("x-agent-schema"),
|
|
304
383
|
is_eval=is_eval,
|
|
384
|
+
client_id=normalized.get("x-client-id"),
|
|
305
385
|
)
|
|
306
386
|
|
|
307
387
|
@classmethod
|
|
@@ -319,6 +399,7 @@ class AgentContext(BaseModel):
|
|
|
319
399
|
- X-Model-Name: Model override
|
|
320
400
|
- X-Agent-Schema: Agent schema URI
|
|
321
401
|
- X-Is-Eval: Whether this is an evaluation session (true/false)
|
|
402
|
+
- X-Client-Id: Client identifier (e.g., "web", "mobile", "cli")
|
|
322
403
|
|
|
323
404
|
Args:
|
|
324
405
|
headers: Dictionary of HTTP headers (case-insensitive)
|
|
@@ -332,7 +413,8 @@ class AgentContext(BaseModel):
|
|
|
332
413
|
"X-Tenant-Id": "acme-corp",
|
|
333
414
|
"X-Session-Id": "sess-456",
|
|
334
415
|
"X-Model-Name": "anthropic:claude-opus-4-20250514",
|
|
335
|
-
"X-Is-Eval": "true"
|
|
416
|
+
"X-Is-Eval": "true",
|
|
417
|
+
"X-Client-Id": "web"
|
|
336
418
|
}
|
|
337
419
|
context = AgentContext.from_headers(headers)
|
|
338
420
|
"""
|
|
@@ -350,4 +432,5 @@ class AgentContext(BaseModel):
|
|
|
350
432
|
default_model=normalized.get("x-model-name") or settings.llm.default_model,
|
|
351
433
|
agent_schema_uri=normalized.get("x-agent-schema"),
|
|
352
434
|
is_eval=is_eval,
|
|
435
|
+
client_id=normalized.get("x-client-id"),
|
|
353
436
|
)
|
rem/agentic/context_builder.py
CHANGED
|
@@ -4,15 +4,12 @@ Centralized context builder for agent execution.
|
|
|
4
4
|
Session History (ALWAYS loaded with compression):
|
|
5
5
|
- Each chat request is a single message, so session history MUST be recovered
|
|
6
6
|
- Uses SessionMessageStore with compression to keep context efficient
|
|
7
|
-
- Long assistant responses include REM LOOKUP hints: "... [REM LOOKUP session-{id}-msg-{index}] ..."
|
|
8
|
-
- Agent can retrieve full content on-demand using REM LOOKUP
|
|
9
7
|
- Prevents context window bloat while maintaining conversation continuity
|
|
10
8
|
|
|
11
9
|
User Context (on-demand by default):
|
|
12
|
-
- System message includes
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- Example: "User: sarah@example.com. To load user profile: Use REM LOOKUP \"sarah@example.com\""
|
|
10
|
+
- System message includes user email for context awareness
|
|
11
|
+
- Fails silently if user not found - agent proceeds without user context
|
|
12
|
+
- Example: "User: sarah@example.com"
|
|
16
13
|
|
|
17
14
|
User Context (auto-inject when enabled):
|
|
18
15
|
- Set CHAT__AUTO_INJECT_USER_CONTEXT=true
|
|
@@ -22,8 +19,8 @@ User Context (auto-inject when enabled):
|
|
|
22
19
|
Design Pattern:
|
|
23
20
|
1. Extract AgentContext from headers (user_id, tenant_id, session_id)
|
|
24
21
|
2. If auto-inject enabled: Load User/Session from database
|
|
25
|
-
3. If auto-inject disabled:
|
|
26
|
-
4. Construct system message with date + context
|
|
22
|
+
3. If auto-inject disabled: Show user email for context (fail silently if not found)
|
|
23
|
+
4. Construct system message with date + context
|
|
27
24
|
5. Return complete context ready for agent execution
|
|
28
25
|
|
|
29
26
|
Integration Points:
|
|
@@ -40,11 +37,10 @@ Usage (on-demand, default):
|
|
|
40
37
|
|
|
41
38
|
# Messages list structure (on-demand):
|
|
42
39
|
# [
|
|
43
|
-
# {"role": "system", "content": "Today's date: 2025-11-22\nUser: sarah@example.com
|
|
40
|
+
# {"role": "system", "content": "Today's date: 2025-11-22\n\nUser: sarah@example.com"},
|
|
44
41
|
# {"role": "user", "content": "What's next for the API migration?"}
|
|
45
42
|
# ]
|
|
46
43
|
|
|
47
|
-
# Agent receives hints and can decide to load context if needed
|
|
48
44
|
agent = await create_agent(context=context, ...)
|
|
49
45
|
prompt = "\n".join(msg.content for msg in messages)
|
|
50
46
|
result = await agent.run(prompt)
|
|
@@ -52,7 +48,7 @@ Usage (on-demand, default):
|
|
|
52
48
|
Usage (auto-inject, CHAT__AUTO_INJECT_USER_CONTEXT=true):
|
|
53
49
|
# Messages list structure (auto-inject):
|
|
54
50
|
# [
|
|
55
|
-
# {"role": "system", "content": "Today's date: 2025-11-22\n\nUser Context (auto-injected):\nSummary: ...\nInterests:
|
|
51
|
+
# {"role": "system", "content": "Today's date: 2025-11-22\n\nUser Context (auto-injected):\nSummary: ...\nInterests: ..."},
|
|
56
52
|
# {"role": "user", "content": "Previous message"},
|
|
57
53
|
# {"role": "assistant", "content": "Previous response"},
|
|
58
54
|
# {"role": "user", "content": "What's next for the API migration?"}
|
|
@@ -110,13 +106,11 @@ class ContextBuilder:
|
|
|
110
106
|
|
|
111
107
|
Session History (ALWAYS loaded with compression):
|
|
112
108
|
- If session_id provided, session history is ALWAYS loaded using SessionMessageStore
|
|
113
|
-
- Compression keeps
|
|
114
|
-
- Example: "... [Message truncated - REM LOOKUP session-{id}-msg-{index}] ..."
|
|
115
|
-
- Agent can retrieve full content on-demand using REM LOOKUP
|
|
109
|
+
- Compression keeps context efficient
|
|
116
110
|
|
|
117
111
|
User Context (on-demand by default):
|
|
118
|
-
- System message includes
|
|
119
|
-
-
|
|
112
|
+
- System message includes user email: "User: {email}"
|
|
113
|
+
- Fails silently if user not found - agent proceeds without user context
|
|
120
114
|
|
|
121
115
|
User Context (auto-inject when enabled):
|
|
122
116
|
- Set CHAT__AUTO_INJECT_USER_CONTEXT=true
|
|
@@ -137,9 +131,9 @@ class ContextBuilder:
|
|
|
137
131
|
|
|
138
132
|
# messages structure:
|
|
139
133
|
# [
|
|
140
|
-
# {"role": "system", "content": "Today's date: 2025-11-22\nUser: sarah@example.com
|
|
134
|
+
# {"role": "system", "content": "Today's date: 2025-11-22\n\nUser: sarah@example.com"},
|
|
141
135
|
# {"role": "user", "content": "Previous message"},
|
|
142
|
-
# {"role": "assistant", "content": "
|
|
136
|
+
# {"role": "assistant", "content": "Previous response"},
|
|
143
137
|
# {"role": "user", "content": "New message"}
|
|
144
138
|
# ]
|
|
145
139
|
"""
|
|
@@ -158,6 +152,7 @@ class ContextBuilder:
|
|
|
158
152
|
default_model=context.default_model,
|
|
159
153
|
agent_schema_uri=context.agent_schema_uri,
|
|
160
154
|
is_eval=context.is_eval,
|
|
155
|
+
client_id=context.client_id,
|
|
161
156
|
)
|
|
162
157
|
|
|
163
158
|
# Initialize DB if not provided and needed (for user context or session history)
|
|
@@ -177,6 +172,10 @@ class ContextBuilder:
|
|
|
177
172
|
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
178
173
|
context_hint = f"Today's date: {today}."
|
|
179
174
|
|
|
175
|
+
# Add client identifier if present
|
|
176
|
+
if context.client_id:
|
|
177
|
+
context_hint += f"\nClient: {context.client_id}"
|
|
178
|
+
|
|
180
179
|
# Add user context (auto-inject or on-demand hint)
|
|
181
180
|
if settings.chat.auto_inject_user_context and context.user_id and db:
|
|
182
181
|
# Auto-inject: Load and include user profile
|
|
@@ -189,18 +188,18 @@ class ContextBuilder:
|
|
|
189
188
|
context_hint += f"\n\nUser Context (auto-injected):\n{user_context_content}"
|
|
190
189
|
else:
|
|
191
190
|
context_hint += "\n\nNo user context available (anonymous or new user)."
|
|
192
|
-
elif context.user_id:
|
|
193
|
-
# On-demand:
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
191
|
+
elif context.user_id and db:
|
|
192
|
+
# On-demand: Show user email for context (no REM LOOKUP - it requires exact user_id match)
|
|
193
|
+
# Fail silently if user lookup fails - just proceed without user context
|
|
194
|
+
try:
|
|
195
|
+
user_repo = Repository(User, "users", db=db)
|
|
196
|
+
user = await user_repo.get_by_id(context.user_id, context.tenant_id)
|
|
197
|
+
if user and user.email:
|
|
198
|
+
context_hint += f"\n\nUser: {user.email}"
|
|
199
|
+
# If user not found, just proceed without adding user context
|
|
200
|
+
except Exception as e:
|
|
201
|
+
# Fail silently - don't block agent execution if user lookup fails
|
|
202
|
+
logger.debug(f"Could not load user context: {e}")
|
|
204
203
|
|
|
205
204
|
# Add system context hint
|
|
206
205
|
messages.append(ContextMessage(role="system", content=context_hint))
|
|
@@ -318,6 +317,7 @@ class ContextBuilder:
|
|
|
318
317
|
session_id: str | None = None,
|
|
319
318
|
message: str = "Hello",
|
|
320
319
|
model: str | None = None,
|
|
320
|
+
client_id: str | None = None,
|
|
321
321
|
) -> tuple[AgentContext, list[ContextMessage]]:
|
|
322
322
|
"""
|
|
323
323
|
Build context for testing (no database lookup).
|
|
@@ -325,7 +325,7 @@ class ContextBuilder:
|
|
|
325
325
|
Creates minimal context with:
|
|
326
326
|
- Test user (test@rem.ai)
|
|
327
327
|
- Test tenant
|
|
328
|
-
- Context hint with date
|
|
328
|
+
- Context hint with date and client
|
|
329
329
|
- Single user message
|
|
330
330
|
|
|
331
331
|
Args:
|
|
@@ -334,6 +334,7 @@ class ContextBuilder:
|
|
|
334
334
|
session_id: Optional session ID
|
|
335
335
|
message: User message content
|
|
336
336
|
model: Optional model override
|
|
337
|
+
client_id: Optional client identifier (e.g., "cli", "test")
|
|
337
338
|
|
|
338
339
|
Returns:
|
|
339
340
|
Tuple of (AgentContext, messages list)
|
|
@@ -341,7 +342,8 @@ class ContextBuilder:
|
|
|
341
342
|
Example:
|
|
342
343
|
context, messages = await ContextBuilder.build_from_test(
|
|
343
344
|
user_id="test@rem.ai",
|
|
344
|
-
message="What's the weather like?"
|
|
345
|
+
message="What's the weather like?",
|
|
346
|
+
client_id="cli"
|
|
345
347
|
)
|
|
346
348
|
"""
|
|
347
349
|
from ..settings import settings
|
|
@@ -352,11 +354,15 @@ class ContextBuilder:
|
|
|
352
354
|
tenant_id=tenant_id,
|
|
353
355
|
session_id=session_id,
|
|
354
356
|
default_model=model or settings.llm.default_model,
|
|
357
|
+
client_id=client_id,
|
|
355
358
|
)
|
|
356
359
|
|
|
357
360
|
# Build minimal messages
|
|
358
361
|
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
359
|
-
context_hint = f"Today's date: {today}
|
|
362
|
+
context_hint = f"Today's date: {today}."
|
|
363
|
+
if client_id:
|
|
364
|
+
context_hint += f"\nClient: {client_id}"
|
|
365
|
+
context_hint += f"\n\nTest user context: {user_id} (test mode, no profile loaded)."
|
|
360
366
|
|
|
361
367
|
messages = [
|
|
362
368
|
ContextMessage(role="system", content=context_hint),
|
rem/agentic/mcp/tool_wrapper.py
CHANGED
|
@@ -116,7 +116,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
|
|
|
116
116
|
the artificial MCP distinction between tools and resources.
|
|
117
117
|
|
|
118
118
|
Supports both:
|
|
119
|
-
- Concrete URIs: "rem://
|
|
119
|
+
- Concrete URIs: "rem://agents" -> tool with no parameters
|
|
120
120
|
- Template URIs: "patient-profile://field/{field_key}" -> tool with field_key parameter
|
|
121
121
|
|
|
122
122
|
Args:
|
|
@@ -131,7 +131,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
|
|
|
131
131
|
|
|
132
132
|
Example:
|
|
133
133
|
# Concrete URI -> no-param tool
|
|
134
|
-
tool = create_resource_tool("rem://
|
|
134
|
+
tool = create_resource_tool("rem://agents", "List all agent schemas")
|
|
135
135
|
|
|
136
136
|
# Template URI -> parameterized tool
|
|
137
137
|
tool = create_resource_tool("patient-profile://field/{field_key}", "Get field definition", mcp_server=mcp)
|
|
@@ -357,6 +357,9 @@ def _convert_properties_to_prompt(properties: dict[str, Any]) -> str:
|
|
|
357
357
|
definition into natural language guidance that informs the agent
|
|
358
358
|
about the expected response structure without forcing JSON output.
|
|
359
359
|
|
|
360
|
+
IMPORTANT: The 'answer' field is the OUTPUT to the user. All other
|
|
361
|
+
fields are INTERNAL tracking that should NOT appear in the output.
|
|
362
|
+
|
|
360
363
|
Args:
|
|
361
364
|
properties: JSON Schema properties dict
|
|
362
365
|
|
|
@@ -368,45 +371,59 @@ def _convert_properties_to_prompt(properties: dict[str, Any]) -> str:
|
|
|
368
371
|
"answer": {"type": "string", "description": "The answer"},
|
|
369
372
|
"confidence": {"type": "number", "description": "Confidence 0-1"}
|
|
370
373
|
}
|
|
371
|
-
# Returns
|
|
372
|
-
# "## Response Structure\n\nYour response should include:\n- **answer**: The answer\n..."
|
|
374
|
+
# Returns guidance that only answer should be output
|
|
373
375
|
"""
|
|
374
376
|
if not properties:
|
|
375
377
|
return ""
|
|
376
378
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
for
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
379
|
+
# Separate answer (output) from other fields (internal tracking)
|
|
380
|
+
answer_field = properties.get("answer")
|
|
381
|
+
internal_fields = {k: v for k, v in properties.items() if k != "answer"}
|
|
382
|
+
|
|
383
|
+
lines = ["## Internal Thinking Structure (DO NOT output these labels)"]
|
|
384
|
+
lines.append("")
|
|
385
|
+
lines.append("Use this structure to organize your thinking, but ONLY output the answer content:")
|
|
386
|
+
lines.append("")
|
|
387
|
+
|
|
388
|
+
# If there's an answer field, emphasize it's the ONLY output
|
|
389
|
+
if answer_field:
|
|
390
|
+
answer_desc = answer_field.get("description", "Your response")
|
|
391
|
+
lines.append(f"**OUTPUT (what the user sees):** {answer_desc}")
|
|
392
|
+
lines.append("")
|
|
393
|
+
|
|
394
|
+
# Document internal fields for tracking/thinking
|
|
395
|
+
if internal_fields:
|
|
396
|
+
lines.append("**INTERNAL (for your tracking only - do NOT include in output):**")
|
|
397
|
+
for field_name, field_def in internal_fields.items():
|
|
398
|
+
field_type = field_def.get("type", "any")
|
|
399
|
+
description = field_def.get("description", "")
|
|
400
|
+
|
|
401
|
+
# Format based on type
|
|
402
|
+
if field_type == "array":
|
|
403
|
+
type_hint = "list"
|
|
404
|
+
elif field_type == "number":
|
|
405
|
+
type_hint = "number"
|
|
406
|
+
if "minimum" in field_def or "maximum" in field_def:
|
|
407
|
+
min_val = field_def.get("minimum", "")
|
|
408
|
+
max_val = field_def.get("maximum", "")
|
|
409
|
+
if min_val != "" and max_val != "":
|
|
410
|
+
type_hint = f"number ({min_val}-{max_val})"
|
|
411
|
+
elif field_type == "boolean":
|
|
412
|
+
type_hint = "yes/no"
|
|
413
|
+
else:
|
|
414
|
+
type_hint = field_type
|
|
398
415
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
field_line += f": {description}"
|
|
416
|
+
field_line = f"- {field_name}"
|
|
417
|
+
if type_hint and type_hint != "string":
|
|
418
|
+
field_line += f" ({type_hint})"
|
|
419
|
+
if description:
|
|
420
|
+
field_line += f": {description}"
|
|
405
421
|
|
|
406
|
-
|
|
422
|
+
lines.append(field_line)
|
|
407
423
|
|
|
408
424
|
lines.append("")
|
|
409
|
-
lines.append("
|
|
425
|
+
lines.append("⚠️ CRITICAL: Your response must be ONLY the conversational answer text.")
|
|
426
|
+
lines.append("Do NOT output field names like 'answer:' or 'diverge_output:' - just the response itself.")
|
|
410
427
|
|
|
411
428
|
return "\n".join(lines)
|
|
412
429
|
|
|
@@ -664,26 +681,26 @@ async def create_agent(
|
|
|
664
681
|
|
|
665
682
|
set_agent_resource_attributes(agent_schema=agent_schema)
|
|
666
683
|
|
|
667
|
-
# Extract schema metadata for search_rem tool description suffix
|
|
668
|
-
# This allows entity schemas to add context-specific notes to the search_rem tool
|
|
669
|
-
search_rem_suffix = None
|
|
670
|
-
if metadata:
|
|
671
|
-
# Check for default_search_table in metadata (set by entity schemas)
|
|
672
|
-
extra = agent_schema.get("json_schema_extra", {}) if agent_schema else {}
|
|
673
|
-
default_table = extra.get("default_search_table")
|
|
674
|
-
has_embeddings = extra.get("has_embeddings", False)
|
|
675
|
-
|
|
676
|
-
if default_table:
|
|
677
|
-
# Build description suffix for search_rem
|
|
678
|
-
search_rem_suffix = f"\n\nFor this schema, use `search_rem` to query `{default_table}`. "
|
|
679
|
-
if has_embeddings:
|
|
680
|
-
search_rem_suffix += f"SEARCH works well on {default_table} (has embeddings). "
|
|
681
|
-
search_rem_suffix += f"Example: `SEARCH \"your query\" FROM {default_table} LIMIT 10`"
|
|
682
|
-
|
|
683
684
|
# Add tools from MCP server (in-process, no subprocess)
|
|
684
685
|
# Track loaded MCP servers for resource resolution
|
|
685
686
|
loaded_mcp_server = None
|
|
686
687
|
|
|
688
|
+
# Build map of tool_name → schema description from agent schema tools section
|
|
689
|
+
# This allows agent-specific tool guidance to override/augment MCP tool descriptions
|
|
690
|
+
schema_tool_descriptions: dict[str, str] = {}
|
|
691
|
+
tool_configs = metadata.tools if metadata and hasattr(metadata, 'tools') else []
|
|
692
|
+
for tool_config in tool_configs:
|
|
693
|
+
if hasattr(tool_config, 'name'):
|
|
694
|
+
t_name = tool_config.name
|
|
695
|
+
t_desc = tool_config.description or ""
|
|
696
|
+
else:
|
|
697
|
+
t_name = tool_config.get("name", "")
|
|
698
|
+
t_desc = tool_config.get("description", "")
|
|
699
|
+
# Skip resource URIs (handled separately below)
|
|
700
|
+
if t_name and "://" not in t_name and t_desc:
|
|
701
|
+
schema_tool_descriptions[t_name] = t_desc
|
|
702
|
+
logger.debug(f"Schema tool description for '{t_name}': {len(t_desc)} chars")
|
|
703
|
+
|
|
687
704
|
for server_config in mcp_server_configs:
|
|
688
705
|
server_type = server_config.get("type")
|
|
689
706
|
server_id = server_config.get("id", "mcp-server")
|
|
@@ -708,8 +725,8 @@ async def create_agent(
|
|
|
708
725
|
mcp_tools_dict = await mcp_server.get_tools()
|
|
709
726
|
|
|
710
727
|
for tool_name, tool_func in mcp_tools_dict.items():
|
|
711
|
-
#
|
|
712
|
-
tool_suffix =
|
|
728
|
+
# Get schema description suffix if agent schema defines one for this tool
|
|
729
|
+
tool_suffix = schema_tool_descriptions.get(tool_name)
|
|
713
730
|
|
|
714
731
|
wrapped_tool = create_mcp_tool_wrapper(
|
|
715
732
|
tool_name,
|
|
@@ -718,7 +735,7 @@ async def create_agent(
|
|
|
718
735
|
description_suffix=tool_suffix,
|
|
719
736
|
)
|
|
720
737
|
tools.append(wrapped_tool)
|
|
721
|
-
logger.debug(f"Loaded MCP tool: {tool_name}" + (" (with schema
|
|
738
|
+
logger.debug(f"Loaded MCP tool: {tool_name}" + (" (with schema desc)" if tool_suffix else ""))
|
|
722
739
|
|
|
723
740
|
logger.info(f"Loaded {len(mcp_tools_dict)} tools from MCP server: {server_id} (in-process)")
|
|
724
741
|
|
|
@@ -732,7 +749,7 @@ async def create_agent(
|
|
|
732
749
|
# the artificial MCP distinction between tools and resources
|
|
733
750
|
#
|
|
734
751
|
# Supports both concrete and template URIs:
|
|
735
|
-
# - Concrete: "rem://
|
|
752
|
+
# - Concrete: "rem://agents" -> no-param tool
|
|
736
753
|
# - Template: "patient-profile://field/{field_key}" -> tool with field_key param
|
|
737
754
|
from ..mcp.tool_wrapper import create_resource_tool
|
|
738
755
|
|
rem/agentic/schema.py
CHANGED
|
@@ -79,7 +79,7 @@ class MCPResourceReference(BaseModel):
|
|
|
79
79
|
|
|
80
80
|
Example (exact URI):
|
|
81
81
|
{
|
|
82
|
-
"uri": "rem://
|
|
82
|
+
"uri": "rem://agents",
|
|
83
83
|
"name": "Agent Schemas",
|
|
84
84
|
"description": "List all available agent schemas"
|
|
85
85
|
}
|
|
@@ -96,7 +96,7 @@ class MCPResourceReference(BaseModel):
|
|
|
96
96
|
default=None,
|
|
97
97
|
description=(
|
|
98
98
|
"Exact resource URI or URI with query parameters. "
|
|
99
|
-
"Examples: 'rem://
|
|
99
|
+
"Examples: 'rem://agents', 'rem://resources?category=drug.*'"
|
|
100
100
|
)
|
|
101
101
|
)
|
|
102
102
|
|