remdb 0.3.180__py3-none-any.whl → 0.3.258__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rem/agentic/README.md +36 -2
- rem/agentic/__init__.py +10 -1
- rem/agentic/context.py +185 -1
- rem/agentic/context_builder.py +56 -35
- rem/agentic/mcp/tool_wrapper.py +2 -2
- rem/agentic/providers/pydantic_ai.py +303 -111
- rem/agentic/schema.py +2 -2
- rem/api/main.py +1 -1
- rem/api/mcp_router/resources.py +223 -0
- rem/api/mcp_router/server.py +4 -0
- rem/api/mcp_router/tools.py +608 -166
- rem/api/routers/admin.py +30 -4
- rem/api/routers/auth.py +219 -20
- rem/api/routers/chat/child_streaming.py +393 -0
- rem/api/routers/chat/completions.py +77 -40
- rem/api/routers/chat/sse_events.py +7 -3
- rem/api/routers/chat/streaming.py +381 -291
- rem/api/routers/chat/streaming_utils.py +325 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +7 -1
- rem/api/routers/feedback.py +11 -3
- rem/api/routers/messages.py +176 -38
- rem/api/routers/models.py +9 -1
- rem/api/routers/query.py +17 -15
- rem/api/routers/shared_sessions.py +16 -0
- rem/auth/jwt.py +19 -4
- rem/auth/middleware.py +42 -28
- rem/cli/README.md +62 -0
- rem/cli/commands/ask.py +205 -114
- rem/cli/commands/db.py +55 -31
- rem/cli/commands/experiments.py +1 -1
- rem/cli/commands/process.py +179 -43
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/session.py +117 -0
- rem/cli/main.py +2 -0
- rem/models/core/experiment.py +1 -1
- rem/models/entities/ontology.py +18 -20
- rem/models/entities/session.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +1 -1
- rem/schemas/agents/rem.yaml +1 -1
- rem/schemas/agents/test_orchestrator.yaml +42 -0
- rem/schemas/agents/test_structured_output.yaml +52 -0
- rem/services/content/providers.py +151 -49
- rem/services/content/service.py +18 -5
- rem/services/embeddings/worker.py +26 -12
- rem/services/postgres/__init__.py +28 -3
- rem/services/postgres/diff_service.py +57 -5
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +2 -2
- rem/services/postgres/register_type.py +11 -10
- rem/services/postgres/repository.py +39 -28
- rem/services/postgres/schema_generator.py +5 -5
- rem/services/postgres/sql_builder.py +6 -5
- rem/services/rem/README.md +4 -3
- rem/services/rem/parser.py +7 -10
- rem/services/rem/service.py +47 -0
- rem/services/session/__init__.py +8 -1
- rem/services/session/compression.py +47 -5
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +2 -1
- rem/settings.py +92 -7
- rem/sql/migrations/001_install.sql +125 -7
- rem/sql/migrations/002_install_models.sql +159 -149
- rem/sql/migrations/004_cache_system.sql +10 -276
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/schema_loader.py +180 -120
- {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/METADATA +7 -6
- {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/RECORD +70 -61
- {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/WHEEL +0 -0
- {remdb-0.3.180.dist-info → remdb-0.3.258.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/__init__.py
CHANGED
|
@@ -15,7 +15,13 @@ from .schema import (
|
|
|
15
15
|
validate_agent_schema,
|
|
16
16
|
create_agent_schema,
|
|
17
17
|
)
|
|
18
|
-
from .providers.pydantic_ai import
|
|
18
|
+
from .providers.pydantic_ai import (
|
|
19
|
+
create_agent_from_schema_file,
|
|
20
|
+
create_agent,
|
|
21
|
+
AgentRuntime,
|
|
22
|
+
clear_agent_cache,
|
|
23
|
+
get_agent_cache_stats,
|
|
24
|
+
)
|
|
19
25
|
from .query_helper import ask_rem, REMQueryOutput
|
|
20
26
|
from .llm_provider_models import (
|
|
21
27
|
ModelInfo,
|
|
@@ -41,6 +47,9 @@ __all__ = [
|
|
|
41
47
|
"create_agent_from_schema_file",
|
|
42
48
|
"create_agent",
|
|
43
49
|
"AgentRuntime",
|
|
50
|
+
# Agent Cache Management
|
|
51
|
+
"clear_agent_cache",
|
|
52
|
+
"get_agent_cache_stats",
|
|
44
53
|
# REM Query Helpers
|
|
45
54
|
"ask_rem",
|
|
46
55
|
"REMQueryOutput",
|
rem/agentic/context.py
CHANGED
|
@@ -16,20 +16,160 @@ 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
|
|
22
23
|
- Enables session tracking across API, CLI, and test execution
|
|
23
24
|
- Supports header-based configuration override (model, schema URI)
|
|
24
25
|
- Clean separation: context (who/what) vs agent (how)
|
|
26
|
+
|
|
27
|
+
Multi-Agent Context Propagation:
|
|
28
|
+
- ContextVar (_current_agent_context) threads context through nested agent calls
|
|
29
|
+
- Parent context is automatically available to child agents via get_current_context()
|
|
30
|
+
- Use agent_context_scope() context manager for scoped context setting
|
|
31
|
+
- Child agents inherit user_id, tenant_id, session_id, is_eval from parent
|
|
25
32
|
"""
|
|
26
33
|
|
|
34
|
+
import asyncio
|
|
35
|
+
from contextlib import contextmanager
|
|
36
|
+
from contextvars import ContextVar
|
|
37
|
+
from typing import Any, Generator
|
|
38
|
+
|
|
27
39
|
from loguru import logger
|
|
28
40
|
from pydantic import BaseModel, Field
|
|
29
41
|
|
|
30
42
|
from ..settings import settings
|
|
31
43
|
|
|
32
44
|
|
|
45
|
+
# Thread-local context for current agent execution
|
|
46
|
+
# This enables context propagation through nested agent calls (multi-agent)
|
|
47
|
+
_current_agent_context: ContextVar["AgentContext | None"] = ContextVar(
|
|
48
|
+
"current_agent_context", default=None
|
|
49
|
+
)
|
|
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
|
+
|
|
58
|
+
|
|
59
|
+
def get_current_context() -> "AgentContext | None":
|
|
60
|
+
"""
|
|
61
|
+
Get the current agent context from context var.
|
|
62
|
+
|
|
63
|
+
Used by MCP tools (like ask_agent) to inherit context from parent agent.
|
|
64
|
+
Returns None if no context is set (e.g., direct CLI invocation without context).
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
# In an MCP tool
|
|
68
|
+
parent_context = get_current_context()
|
|
69
|
+
if parent_context:
|
|
70
|
+
# Inherit user_id, session_id, etc. from parent
|
|
71
|
+
child_context = parent_context.child_context(agent_schema_uri="child-agent")
|
|
72
|
+
"""
|
|
73
|
+
return _current_agent_context.get()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def set_current_context(ctx: "AgentContext | None") -> None:
|
|
77
|
+
"""
|
|
78
|
+
Set the current agent context.
|
|
79
|
+
|
|
80
|
+
Called by streaming layer before agent execution.
|
|
81
|
+
Should be cleared (set to None) after execution completes.
|
|
82
|
+
"""
|
|
83
|
+
_current_agent_context.set(ctx)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@contextmanager
|
|
87
|
+
def agent_context_scope(ctx: "AgentContext") -> Generator["AgentContext", None, None]:
|
|
88
|
+
"""
|
|
89
|
+
Context manager for scoped context setting.
|
|
90
|
+
|
|
91
|
+
Automatically restores previous context when exiting scope.
|
|
92
|
+
Safe for nested agent calls - each level preserves its parent's context.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
context = AgentContext(user_id="user-123")
|
|
96
|
+
with agent_context_scope(context):
|
|
97
|
+
# Context is available via get_current_context()
|
|
98
|
+
result = await agent.run(...)
|
|
99
|
+
# Previous context (or None) is restored
|
|
100
|
+
"""
|
|
101
|
+
previous = _current_agent_context.get()
|
|
102
|
+
_current_agent_context.set(ctx)
|
|
103
|
+
try:
|
|
104
|
+
yield ctx
|
|
105
|
+
finally:
|
|
106
|
+
_current_agent_context.set(previous)
|
|
107
|
+
|
|
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
|
+
|
|
33
173
|
class AgentContext(BaseModel):
|
|
34
174
|
"""
|
|
35
175
|
Session and configuration context for agent execution.
|
|
@@ -83,8 +223,48 @@ class AgentContext(BaseModel):
|
|
|
83
223
|
description="Whether this is an evaluation session (set via X-Is-Eval header)",
|
|
84
224
|
)
|
|
85
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
|
+
|
|
86
231
|
model_config = {"populate_by_name": True}
|
|
87
232
|
|
|
233
|
+
def child_context(
|
|
234
|
+
self,
|
|
235
|
+
agent_schema_uri: str | None = None,
|
|
236
|
+
model_override: str | None = None,
|
|
237
|
+
) -> "AgentContext":
|
|
238
|
+
"""
|
|
239
|
+
Create a child context for nested agent calls.
|
|
240
|
+
|
|
241
|
+
Inherits user_id, tenant_id, session_id, is_eval, client_id from parent.
|
|
242
|
+
Allows overriding agent_schema_uri and default_model for the child.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
agent_schema_uri: Agent schema for the child agent (required for lineage)
|
|
246
|
+
model_override: Optional model override for child agent
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
New AgentContext for the child agent
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
parent_context = get_current_context()
|
|
253
|
+
child_context = parent_context.child_context(
|
|
254
|
+
agent_schema_uri="sentiment-analyzer"
|
|
255
|
+
)
|
|
256
|
+
agent = await create_agent(context=child_context)
|
|
257
|
+
"""
|
|
258
|
+
return AgentContext(
|
|
259
|
+
user_id=self.user_id,
|
|
260
|
+
tenant_id=self.tenant_id,
|
|
261
|
+
session_id=self.session_id,
|
|
262
|
+
default_model=model_override or self.default_model,
|
|
263
|
+
agent_schema_uri=agent_schema_uri or self.agent_schema_uri,
|
|
264
|
+
is_eval=self.is_eval,
|
|
265
|
+
client_id=self.client_id,
|
|
266
|
+
)
|
|
267
|
+
|
|
88
268
|
@staticmethod
|
|
89
269
|
def get_user_id_or_default(
|
|
90
270
|
user_id: str | None,
|
|
@@ -201,6 +381,7 @@ class AgentContext(BaseModel):
|
|
|
201
381
|
default_model=normalized.get("x-model-name") or settings.llm.default_model,
|
|
202
382
|
agent_schema_uri=normalized.get("x-agent-schema"),
|
|
203
383
|
is_eval=is_eval,
|
|
384
|
+
client_id=normalized.get("x-client-id"),
|
|
204
385
|
)
|
|
205
386
|
|
|
206
387
|
@classmethod
|
|
@@ -218,6 +399,7 @@ class AgentContext(BaseModel):
|
|
|
218
399
|
- X-Model-Name: Model override
|
|
219
400
|
- X-Agent-Schema: Agent schema URI
|
|
220
401
|
- X-Is-Eval: Whether this is an evaluation session (true/false)
|
|
402
|
+
- X-Client-Id: Client identifier (e.g., "web", "mobile", "cli")
|
|
221
403
|
|
|
222
404
|
Args:
|
|
223
405
|
headers: Dictionary of HTTP headers (case-insensitive)
|
|
@@ -231,7 +413,8 @@ class AgentContext(BaseModel):
|
|
|
231
413
|
"X-Tenant-Id": "acme-corp",
|
|
232
414
|
"X-Session-Id": "sess-456",
|
|
233
415
|
"X-Model-Name": "anthropic:claude-opus-4-20250514",
|
|
234
|
-
"X-Is-Eval": "true"
|
|
416
|
+
"X-Is-Eval": "true",
|
|
417
|
+
"X-Client-Id": "web"
|
|
235
418
|
}
|
|
236
419
|
context = AgentContext.from_headers(headers)
|
|
237
420
|
"""
|
|
@@ -249,4 +432,5 @@ class AgentContext(BaseModel):
|
|
|
249
432
|
default_model=normalized.get("x-model-name") or settings.llm.default_model,
|
|
250
433
|
agent_schema_uri=normalized.get("x-agent-schema"),
|
|
251
434
|
is_eval=is_eval,
|
|
435
|
+
client_id=normalized.get("x-client-id"),
|
|
252
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))
|
|
@@ -217,11 +216,26 @@ class ContextBuilder:
|
|
|
217
216
|
)
|
|
218
217
|
|
|
219
218
|
# Convert to ContextMessage format
|
|
219
|
+
# For tool messages, wrap content with clear markers so the agent
|
|
220
|
+
# can see previous tool results when the prompt is concatenated
|
|
220
221
|
for msg_dict in session_history:
|
|
222
|
+
role = msg_dict["role"]
|
|
223
|
+
content = msg_dict.get("content")
|
|
224
|
+
|
|
225
|
+
# Skip messages with null/empty content (common in tool messages)
|
|
226
|
+
if content is None or content == "":
|
|
227
|
+
logger.debug(f"Skipping {role} message with null/empty content")
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if role == "tool":
|
|
231
|
+
# Wrap tool results with clear markers for visibility
|
|
232
|
+
tool_name = msg_dict.get("tool_name", "unknown")
|
|
233
|
+
content = f"[TOOL RESULT: {tool_name}]\n{content}\n[/TOOL RESULT]"
|
|
234
|
+
|
|
221
235
|
messages.append(
|
|
222
236
|
ContextMessage(
|
|
223
|
-
role=
|
|
224
|
-
content=
|
|
237
|
+
role=role,
|
|
238
|
+
content=content,
|
|
225
239
|
)
|
|
226
240
|
)
|
|
227
241
|
|
|
@@ -308,6 +322,7 @@ class ContextBuilder:
|
|
|
308
322
|
session_id: str | None = None,
|
|
309
323
|
message: str = "Hello",
|
|
310
324
|
model: str | None = None,
|
|
325
|
+
client_id: str | None = None,
|
|
311
326
|
) -> tuple[AgentContext, list[ContextMessage]]:
|
|
312
327
|
"""
|
|
313
328
|
Build context for testing (no database lookup).
|
|
@@ -315,7 +330,7 @@ class ContextBuilder:
|
|
|
315
330
|
Creates minimal context with:
|
|
316
331
|
- Test user (test@rem.ai)
|
|
317
332
|
- Test tenant
|
|
318
|
-
- Context hint with date
|
|
333
|
+
- Context hint with date and client
|
|
319
334
|
- Single user message
|
|
320
335
|
|
|
321
336
|
Args:
|
|
@@ -324,6 +339,7 @@ class ContextBuilder:
|
|
|
324
339
|
session_id: Optional session ID
|
|
325
340
|
message: User message content
|
|
326
341
|
model: Optional model override
|
|
342
|
+
client_id: Optional client identifier (e.g., "cli", "test")
|
|
327
343
|
|
|
328
344
|
Returns:
|
|
329
345
|
Tuple of (AgentContext, messages list)
|
|
@@ -331,7 +347,8 @@ class ContextBuilder:
|
|
|
331
347
|
Example:
|
|
332
348
|
context, messages = await ContextBuilder.build_from_test(
|
|
333
349
|
user_id="test@rem.ai",
|
|
334
|
-
message="What's the weather like?"
|
|
350
|
+
message="What's the weather like?",
|
|
351
|
+
client_id="cli"
|
|
335
352
|
)
|
|
336
353
|
"""
|
|
337
354
|
from ..settings import settings
|
|
@@ -342,11 +359,15 @@ class ContextBuilder:
|
|
|
342
359
|
tenant_id=tenant_id,
|
|
343
360
|
session_id=session_id,
|
|
344
361
|
default_model=model or settings.llm.default_model,
|
|
362
|
+
client_id=client_id,
|
|
345
363
|
)
|
|
346
364
|
|
|
347
365
|
# Build minimal messages
|
|
348
366
|
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
349
|
-
context_hint = f"Today's date: {today}
|
|
367
|
+
context_hint = f"Today's date: {today}."
|
|
368
|
+
if client_id:
|
|
369
|
+
context_hint += f"\nClient: {client_id}"
|
|
370
|
+
context_hint += f"\n\nTest user context: {user_id} (test mode, no profile loaded)."
|
|
350
371
|
|
|
351
372
|
messages = [
|
|
352
373
|
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)
|