remdb 0.3.226__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 +22 -248
- rem/agentic/context.py +13 -2
- rem/agentic/context_builder.py +39 -33
- rem/agentic/providers/pydantic_ai.py +67 -50
- rem/api/mcp_router/resources.py +223 -0
- rem/api/mcp_router/tools.py +25 -9
- rem/api/routers/auth.py +112 -9
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/streaming.py +166 -357
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/query.py +5 -14
- rem/cli/commands/ask.py +144 -33
- rem/cli/commands/process.py +9 -1
- 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/services/postgres/repository.py +7 -17
- rem/services/rem/service.py +47 -0
- rem/services/session/compression.py +7 -3
- rem/services/session/pydantic_messages.py +45 -11
- rem/services/session/reload.py +2 -1
- rem/settings.py +43 -0
- rem/sql/migrations/004_cache_system.sql +3 -1
- rem/utils/schema_loader.py +99 -99
- {remdb-0.3.226.dist-info → remdb-0.3.245.dist-info}/METADATA +2 -2
- {remdb-0.3.226.dist-info → remdb-0.3.245.dist-info}/RECORD +29 -26
- {remdb-0.3.226.dist-info → remdb-0.3.245.dist-info}/WHEEL +0 -0
- {remdb-0.3.226.dist-info → remdb-0.3.245.dist-info}/entry_points.txt +0 -0
rem/agentic/README.md
CHANGED
|
@@ -718,263 +718,37 @@ See `rem/api/README.md` for full SSE event protocol documentation.
|
|
|
718
718
|
|
|
719
719
|
## Multi-Agent Orchestration
|
|
720
720
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
### Architecture
|
|
724
|
-
|
|
725
|
-
```mermaid
|
|
726
|
-
sequenceDiagram
|
|
727
|
-
participant User
|
|
728
|
-
participant API as Chat API
|
|
729
|
-
participant Orchestrator as Orchestrator Agent
|
|
730
|
-
participant EventSink as Event Sink (Queue)
|
|
731
|
-
participant Child as Child Agent
|
|
732
|
-
participant DB as PostgreSQL
|
|
733
|
-
|
|
734
|
-
User->>API: POST /chat/completions (stream=true)
|
|
735
|
-
API->>API: Create event sink (asyncio.Queue)
|
|
736
|
-
API->>Orchestrator: agent.iter(prompt)
|
|
737
|
-
|
|
738
|
-
loop Streaming Loop
|
|
739
|
-
Orchestrator->>API: PartDeltaEvent (text)
|
|
740
|
-
API->>User: SSE: data: {"delta": {"content": "..."}}
|
|
741
|
-
end
|
|
742
|
-
|
|
743
|
-
Orchestrator->>Orchestrator: Decides to call ask_agent
|
|
744
|
-
Orchestrator->>API: ToolCallPart (ask_agent)
|
|
745
|
-
API->>User: SSE: event: tool_call
|
|
746
|
-
|
|
747
|
-
API->>Child: ask_agent("child_name", input)
|
|
748
|
-
Child->>EventSink: push_event(child_content)
|
|
749
|
-
EventSink->>API: Consume child events
|
|
750
|
-
API->>User: SSE: data: {"delta": {"content": "..."}}
|
|
751
|
-
|
|
752
|
-
Child->>Child: Completes
|
|
753
|
-
Child-->>Orchestrator: Return result
|
|
754
|
-
|
|
755
|
-
Orchestrator->>API: Final response
|
|
756
|
-
API->>DB: Save tool calls
|
|
757
|
-
API->>DB: Save assistant message
|
|
758
|
-
API->>User: SSE: data: [DONE]
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
### Event Sink Pattern
|
|
762
|
-
|
|
763
|
-
When an agent delegates to a child via `ask_agent`, the child's streaming events need to bubble up to the parent's stream. This is achieved through an **event sink** pattern using Python's `ContextVar`:
|
|
764
|
-
|
|
765
|
-
```python
|
|
766
|
-
# context.py
|
|
767
|
-
from contextvars import ContextVar
|
|
768
|
-
|
|
769
|
-
_parent_event_sink: ContextVar["asyncio.Queue | None"] = ContextVar(
|
|
770
|
-
"parent_event_sink", default=None
|
|
771
|
-
)
|
|
772
|
-
|
|
773
|
-
async def push_event(event: Any) -> bool:
|
|
774
|
-
"""Push event to parent's event sink if available."""
|
|
775
|
-
sink = _parent_event_sink.get()
|
|
776
|
-
if sink is not None:
|
|
777
|
-
await sink.put(event)
|
|
778
|
-
return True
|
|
779
|
-
return False
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
The streaming controller sets up the event sink before agent execution:
|
|
783
|
-
|
|
784
|
-
```python
|
|
785
|
-
# streaming.py
|
|
786
|
-
child_event_sink: asyncio.Queue = asyncio.Queue()
|
|
787
|
-
set_event_sink(child_event_sink)
|
|
788
|
-
|
|
789
|
-
async for node in agent.iter(prompt):
|
|
790
|
-
# Process agent events...
|
|
791
|
-
|
|
792
|
-
# Consume any child events that arrived
|
|
793
|
-
while not child_event_sink.empty():
|
|
794
|
-
child_event = child_event_sink.get_nowait()
|
|
795
|
-
if child_event["type"] == "child_content":
|
|
796
|
-
yield format_sse_content_delta(child_event["content"])
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
### ask_agent Tool Implementation
|
|
800
|
-
|
|
801
|
-
The `ask_agent` tool in `mcp_router/tools.py` uses Pydantic AI's streaming iteration:
|
|
802
|
-
|
|
803
|
-
```python
|
|
804
|
-
async def ask_agent(agent_name: str, input_text: str, ...):
|
|
805
|
-
"""Delegate work to another agent."""
|
|
806
|
-
|
|
807
|
-
# Load and create child agent
|
|
808
|
-
schema = await load_agent_schema_async(agent_name, user_id)
|
|
809
|
-
child_agent = await create_agent(context=context, agent_schema_override=schema)
|
|
810
|
-
|
|
811
|
-
# Stream child agent with event proxying
|
|
812
|
-
async with child_agent.iter(prompt) as agent_run:
|
|
813
|
-
async for node in agent_run:
|
|
814
|
-
if Agent.is_model_request_node(node):
|
|
815
|
-
async with node.stream(agent_run.ctx) as request_stream:
|
|
816
|
-
async for event in request_stream:
|
|
817
|
-
if isinstance(event, PartDeltaEvent):
|
|
818
|
-
# Push content to parent's event sink
|
|
819
|
-
await push_event({
|
|
820
|
-
"type": "child_content",
|
|
821
|
-
"agent_name": agent_name,
|
|
822
|
-
"content": event.delta.content_delta,
|
|
823
|
-
})
|
|
824
|
-
|
|
825
|
-
return agent_run.result
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
### Pydantic AI Features Used
|
|
721
|
+
Agents can delegate work to other agents via the `ask_agent` tool. This enables orchestrator patterns where a parent agent routes to specialists.
|
|
829
722
|
|
|
830
|
-
|
|
723
|
+
### How It Works
|
|
831
724
|
|
|
832
|
-
|
|
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
|
|
833
729
|
|
|
834
|
-
|
|
835
|
-
async with agent.iter(prompt) as agent_run:
|
|
836
|
-
async for node in agent_run:
|
|
837
|
-
if Agent.is_model_request_node(node):
|
|
838
|
-
# Model is generating - stream the response
|
|
839
|
-
async with node.stream(agent_run.ctx) as stream:
|
|
840
|
-
async for event in stream:
|
|
841
|
-
if isinstance(event, PartStartEvent):
|
|
842
|
-
# Tool call starting
|
|
843
|
-
elif isinstance(event, PartDeltaEvent):
|
|
844
|
-
# Content chunk
|
|
845
|
-
elif Agent.is_call_tools_node(node):
|
|
846
|
-
# Tools are being executed
|
|
847
|
-
async with node.stream(agent_run.ctx) as stream:
|
|
848
|
-
async for event in stream:
|
|
849
|
-
if isinstance(event, FunctionToolResultEvent):
|
|
850
|
-
# Tool completed
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
#### 2. Node Types
|
|
854
|
-
|
|
855
|
-
- **`ModelRequestNode`**: The model is generating a response (text or tool calls)
|
|
856
|
-
- **`CallToolsNode`**: Tools are being executed
|
|
857
|
-
- **`End`**: Agent execution complete
|
|
858
|
-
|
|
859
|
-
#### 3. Event Types
|
|
730
|
+
### Key Components
|
|
860
731
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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 |
|
|
866
737
|
|
|
867
|
-
###
|
|
868
|
-
|
|
869
|
-
All messages are persisted to PostgreSQL for session continuity:
|
|
870
|
-
|
|
871
|
-
```python
|
|
872
|
-
# streaming.py - after agent completes
|
|
873
|
-
async def save_session_messages(...):
|
|
874
|
-
store = SessionMessageStore(user_id=user_id)
|
|
875
|
-
|
|
876
|
-
# Save each tool call as a tool message
|
|
877
|
-
for tool_call in tool_calls:
|
|
878
|
-
await store.save_message(
|
|
879
|
-
session_id=session_id,
|
|
880
|
-
role="tool",
|
|
881
|
-
content=tool_call.result,
|
|
882
|
-
tool_name=tool_call.name,
|
|
883
|
-
tool_call_id=tool_call.id,
|
|
884
|
-
)
|
|
885
|
-
|
|
886
|
-
# Save the final assistant response
|
|
887
|
-
await store.save_message(
|
|
888
|
-
session_id=session_id,
|
|
889
|
-
role="assistant",
|
|
890
|
-
content=accumulated_content,
|
|
891
|
-
)
|
|
892
|
-
```
|
|
738
|
+
### Event Types
|
|
893
739
|
|
|
894
|
-
|
|
895
|
-
- **Embeddings**: For semantic search across conversation history
|
|
896
|
-
- **Compression**: Long conversations are summarized to manage context window
|
|
897
|
-
- **Session isolation**: Each session maintains its own message history
|
|
740
|
+
Child agents emit events that the parent streams to the client:
|
|
898
741
|
|
|
899
|
-
|
|
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)
|
|
900
745
|
|
|
901
|
-
|
|
746
|
+
### Testing
|
|
902
747
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
async def test_ask_agent_streams_and_saves(self, session_id, user_id):
|
|
908
|
-
"""Test delegation via ask_agent."""
|
|
909
|
-
# Uses test_orchestrator which always delegates to test_responder
|
|
910
|
-
agent = await create_agent(context=context, agent_schema_override=schema)
|
|
911
|
-
|
|
912
|
-
chunks = []
|
|
913
|
-
async for chunk in stream_openai_response_with_save(
|
|
914
|
-
agent=agent,
|
|
915
|
-
prompt="Hello, please delegate this",
|
|
916
|
-
...
|
|
917
|
-
):
|
|
918
|
-
chunks.append(chunk)
|
|
919
|
-
|
|
920
|
-
# Verify streaming worked
|
|
921
|
-
assert len(content_chunks) > 0
|
|
922
|
-
|
|
923
|
-
# Verify persistence
|
|
924
|
-
messages = await store.load_session_messages(session_id)
|
|
925
|
-
assert len([m for m in messages if m["role"] == "assistant"]) == 1
|
|
926
|
-
assert len([m for m in messages if m["tool_name"] == "ask_agent"]) >= 1
|
|
927
|
-
|
|
928
|
-
async def test_multi_turn_saves_all_assistant_messages(self, session_id, user_id):
|
|
929
|
-
"""Test that each turn saves its own assistant message.
|
|
930
|
-
|
|
931
|
-
This catches scoping bugs like accumulated_content not being
|
|
932
|
-
properly scoped per-turn.
|
|
933
|
-
"""
|
|
934
|
-
turn_prompts = [
|
|
935
|
-
"Hello, how are you?",
|
|
936
|
-
"Tell me something interesting",
|
|
937
|
-
"Thanks for chatting!",
|
|
938
|
-
]
|
|
939
|
-
|
|
940
|
-
for prompt in turn_prompts:
|
|
941
|
-
async for chunk in stream_openai_response_with_save(...):
|
|
942
|
-
pass
|
|
943
|
-
|
|
944
|
-
# Each turn should save an assistant message
|
|
945
|
-
messages = await store.load_session_messages(session_id)
|
|
946
|
-
assistant_msgs = [m for m in messages if m["role"] == "assistant"]
|
|
947
|
-
assert len(assistant_msgs) == 3
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
#### Test Agent Schemas
|
|
951
|
-
|
|
952
|
-
Test agents are defined in `tests/data/schemas/agents/`:
|
|
953
|
-
|
|
954
|
-
- **`test_orchestrator.yaml`**: Always delegates via `ask_agent`
|
|
955
|
-
- **`test_responder.yaml`**: Simple agent that responds directly
|
|
956
|
-
|
|
957
|
-
```yaml
|
|
958
|
-
# test_orchestrator.yaml
|
|
959
|
-
type: object
|
|
960
|
-
description: |
|
|
961
|
-
You are a TEST ORCHESTRATOR that ALWAYS delegates to another agent.
|
|
962
|
-
Call ask_agent with agent_name="test_responder" on EVERY turn.
|
|
963
|
-
json_schema_extra:
|
|
964
|
-
kind: agent
|
|
965
|
-
name: test_orchestrator
|
|
966
|
-
tools:
|
|
967
|
-
- name: ask_agent
|
|
968
|
-
mcp_server: rem
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
#### Running Integration Tests
|
|
972
|
-
|
|
973
|
-
```bash
|
|
974
|
-
# Run individually (recommended due to async isolation)
|
|
975
|
-
POSTGRES__CONNECTION_STRING="postgresql://rem:rem@localhost:5050/rem" \
|
|
976
|
-
uv run pytest tests/integration/test_ask_agent_streaming.py::TestAskAgentStreaming::test_multi_turn_saves_all_assistant_messages -v -s
|
|
977
|
-
```
|
|
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
|
|
978
752
|
|
|
979
753
|
## Future Work
|
|
980
754
|
|
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
|
|
@@ -222,6 +223,11 @@ class AgentContext(BaseModel):
|
|
|
222
223
|
description="Whether this is an evaluation session (set via X-Is-Eval header)",
|
|
223
224
|
)
|
|
224
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
|
+
|
|
225
231
|
model_config = {"populate_by_name": True}
|
|
226
232
|
|
|
227
233
|
def child_context(
|
|
@@ -232,7 +238,7 @@ class AgentContext(BaseModel):
|
|
|
232
238
|
"""
|
|
233
239
|
Create a child context for nested agent calls.
|
|
234
240
|
|
|
235
|
-
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.
|
|
236
242
|
Allows overriding agent_schema_uri and default_model for the child.
|
|
237
243
|
|
|
238
244
|
Args:
|
|
@@ -256,6 +262,7 @@ class AgentContext(BaseModel):
|
|
|
256
262
|
default_model=model_override or self.default_model,
|
|
257
263
|
agent_schema_uri=agent_schema_uri or self.agent_schema_uri,
|
|
258
264
|
is_eval=self.is_eval,
|
|
265
|
+
client_id=self.client_id,
|
|
259
266
|
)
|
|
260
267
|
|
|
261
268
|
@staticmethod
|
|
@@ -374,6 +381,7 @@ class AgentContext(BaseModel):
|
|
|
374
381
|
default_model=normalized.get("x-model-name") or settings.llm.default_model,
|
|
375
382
|
agent_schema_uri=normalized.get("x-agent-schema"),
|
|
376
383
|
is_eval=is_eval,
|
|
384
|
+
client_id=normalized.get("x-client-id"),
|
|
377
385
|
)
|
|
378
386
|
|
|
379
387
|
@classmethod
|
|
@@ -391,6 +399,7 @@ class AgentContext(BaseModel):
|
|
|
391
399
|
- X-Model-Name: Model override
|
|
392
400
|
- X-Agent-Schema: Agent schema URI
|
|
393
401
|
- X-Is-Eval: Whether this is an evaluation session (true/false)
|
|
402
|
+
- X-Client-Id: Client identifier (e.g., "web", "mobile", "cli")
|
|
394
403
|
|
|
395
404
|
Args:
|
|
396
405
|
headers: Dictionary of HTTP headers (case-insensitive)
|
|
@@ -404,7 +413,8 @@ class AgentContext(BaseModel):
|
|
|
404
413
|
"X-Tenant-Id": "acme-corp",
|
|
405
414
|
"X-Session-Id": "sess-456",
|
|
406
415
|
"X-Model-Name": "anthropic:claude-opus-4-20250514",
|
|
407
|
-
"X-Is-Eval": "true"
|
|
416
|
+
"X-Is-Eval": "true",
|
|
417
|
+
"X-Client-Id": "web"
|
|
408
418
|
}
|
|
409
419
|
context = AgentContext.from_headers(headers)
|
|
410
420
|
"""
|
|
@@ -422,4 +432,5 @@ class AgentContext(BaseModel):
|
|
|
422
432
|
default_model=normalized.get("x-model-name") or settings.llm.default_model,
|
|
423
433
|
agent_schema_uri=normalized.get("x-agent-schema"),
|
|
424
434
|
is_eval=is_eval,
|
|
435
|
+
client_id=normalized.get("x-client-id"),
|
|
425
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),
|