remdb 0.3.171__py3-none-any.whl → 0.3.230__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/context.py +173 -0
- rem/agentic/context_builder.py +12 -2
- rem/agentic/mcp/tool_wrapper.py +39 -16
- rem/agentic/providers/pydantic_ai.py +78 -45
- rem/agentic/schema.py +6 -5
- rem/agentic/tools/rem_tools.py +11 -0
- rem/api/main.py +1 -1
- rem/api/mcp_router/resources.py +75 -14
- rem/api/mcp_router/server.py +31 -24
- rem/api/mcp_router/tools.py +621 -166
- rem/api/routers/admin.py +30 -4
- rem/api/routers/auth.py +114 -15
- rem/api/routers/chat/child_streaming.py +379 -0
- rem/api/routers/chat/completions.py +74 -37
- rem/api/routers/chat/sse_events.py +7 -3
- rem/api/routers/chat/streaming.py +352 -257
- 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 +176 -38
- rem/api/routers/models.py +9 -1
- rem/api/routers/query.py +12 -1
- 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 +61 -81
- rem/cli/commands/db.py +148 -70
- rem/cli/commands/process.py +171 -43
- rem/models/entities/ontology.py +91 -101
- rem/schemas/agents/rem.yaml +1 -1
- rem/services/content/service.py +18 -5
- rem/services/email/service.py +11 -2
- 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 +12 -11
- rem/services/postgres/repository.py +39 -29
- rem/services/postgres/schema_generator.py +5 -5
- rem/services/postgres/sql_builder.py +6 -5
- rem/services/session/__init__.py +8 -1
- rem/services/session/compression.py +40 -2
- rem/services/session/pydantic_messages.py +292 -0
- rem/settings.py +34 -0
- rem/sql/background_indexes.sql +5 -0
- rem/sql/migrations/001_install.sql +157 -10
- rem/sql/migrations/002_install_models.sql +160 -132
- rem/sql/migrations/004_cache_system.sql +7 -275
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/model_helpers.py +101 -0
- rem/utils/schema_loader.py +79 -51
- {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/METADATA +2 -2
- {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/RECORD +59 -53
- {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/WHEEL +0 -0
- {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Streaming Utilities.
|
|
3
|
+
|
|
4
|
+
Pure functions and data structures for SSE streaming.
|
|
5
|
+
No I/O, no database calls - just data transformation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
18
|
+
from .models import (
|
|
19
|
+
ChatCompletionMessageDelta,
|
|
20
|
+
ChatCompletionStreamChoice,
|
|
21
|
+
ChatCompletionStreamResponse,
|
|
22
|
+
)
|
|
23
|
+
from .sse_events import (
|
|
24
|
+
MetadataEvent,
|
|
25
|
+
ProgressEvent,
|
|
26
|
+
ReasoningEvent,
|
|
27
|
+
ToolCallEvent,
|
|
28
|
+
format_sse_event,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# =============================================================================
|
|
33
|
+
# STREAMING STATE
|
|
34
|
+
# =============================================================================
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class StreamingState:
|
|
38
|
+
"""
|
|
39
|
+
Tracks state during SSE streaming.
|
|
40
|
+
|
|
41
|
+
This is a pure data container - no methods that do I/O.
|
|
42
|
+
"""
|
|
43
|
+
request_id: str
|
|
44
|
+
created_at: int
|
|
45
|
+
model: str
|
|
46
|
+
start_time: float = field(default_factory=time.time)
|
|
47
|
+
|
|
48
|
+
# Content tracking
|
|
49
|
+
is_first_chunk: bool = True
|
|
50
|
+
token_count: int = 0
|
|
51
|
+
|
|
52
|
+
# Child agent tracking - KEY FOR DUPLICATION FIX
|
|
53
|
+
child_content_streamed: bool = False
|
|
54
|
+
responding_agent: str | None = None
|
|
55
|
+
|
|
56
|
+
# Tool tracking
|
|
57
|
+
active_tool_calls: dict = field(default_factory=dict) # index -> (name, id)
|
|
58
|
+
pending_tool_completions: list = field(default_factory=list) # FIFO queue
|
|
59
|
+
pending_tool_data: dict = field(default_factory=dict) # tool_id -> data
|
|
60
|
+
|
|
61
|
+
# Reasoning tracking
|
|
62
|
+
reasoning_step: int = 0
|
|
63
|
+
|
|
64
|
+
# Progress tracking
|
|
65
|
+
current_step: int = 0
|
|
66
|
+
total_steps: int = 3
|
|
67
|
+
|
|
68
|
+
# Metadata tracking
|
|
69
|
+
metadata_registered: bool = False
|
|
70
|
+
|
|
71
|
+
# Trace context (captured from OTEL)
|
|
72
|
+
trace_id: str | None = None
|
|
73
|
+
span_id: str | None = None
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def create(cls, model: str, request_id: str | None = None) -> "StreamingState":
|
|
77
|
+
"""Create a new streaming state."""
|
|
78
|
+
return cls(
|
|
79
|
+
request_id=request_id or f"chatcmpl-{uuid.uuid4().hex[:24]}",
|
|
80
|
+
created_at=int(time.time()),
|
|
81
|
+
model=model,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def latency_ms(self) -> int:
|
|
85
|
+
"""Calculate latency since start."""
|
|
86
|
+
return int((time.time() - self.start_time) * 1000)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# =============================================================================
|
|
90
|
+
# SSE CHUNK BUILDERS
|
|
91
|
+
# =============================================================================
|
|
92
|
+
|
|
93
|
+
def build_content_chunk(state: StreamingState, content: str) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Build an SSE content chunk in OpenAI format.
|
|
96
|
+
|
|
97
|
+
Updates state.is_first_chunk and state.token_count.
|
|
98
|
+
"""
|
|
99
|
+
state.token_count += len(content.split())
|
|
100
|
+
|
|
101
|
+
chunk = ChatCompletionStreamResponse(
|
|
102
|
+
id=state.request_id,
|
|
103
|
+
created=state.created_at,
|
|
104
|
+
model=state.model,
|
|
105
|
+
choices=[
|
|
106
|
+
ChatCompletionStreamChoice(
|
|
107
|
+
index=0,
|
|
108
|
+
delta=ChatCompletionMessageDelta(
|
|
109
|
+
role="assistant" if state.is_first_chunk else None,
|
|
110
|
+
content=content,
|
|
111
|
+
),
|
|
112
|
+
finish_reason=None,
|
|
113
|
+
)
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
state.is_first_chunk = False
|
|
117
|
+
return f"data: {chunk.model_dump_json()}\n\n"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_final_chunk(state: StreamingState) -> str:
|
|
121
|
+
"""Build the final SSE chunk with finish_reason=stop."""
|
|
122
|
+
chunk = ChatCompletionStreamResponse(
|
|
123
|
+
id=state.request_id,
|
|
124
|
+
created=state.created_at,
|
|
125
|
+
model=state.model,
|
|
126
|
+
choices=[
|
|
127
|
+
ChatCompletionStreamChoice(
|
|
128
|
+
index=0,
|
|
129
|
+
delta=ChatCompletionMessageDelta(),
|
|
130
|
+
finish_reason="stop",
|
|
131
|
+
)
|
|
132
|
+
],
|
|
133
|
+
)
|
|
134
|
+
return f"data: {chunk.model_dump_json()}\n\n"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_reasoning_event(state: StreamingState, content: str) -> str:
|
|
138
|
+
"""Build a reasoning SSE event."""
|
|
139
|
+
return format_sse_event(ReasoningEvent(
|
|
140
|
+
content=content,
|
|
141
|
+
step=state.reasoning_step,
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_progress_event(
|
|
146
|
+
step: int,
|
|
147
|
+
total_steps: int,
|
|
148
|
+
label: str,
|
|
149
|
+
status: str = "in_progress",
|
|
150
|
+
) -> str:
|
|
151
|
+
"""Build a progress SSE event."""
|
|
152
|
+
return format_sse_event(ProgressEvent(
|
|
153
|
+
step=step,
|
|
154
|
+
total_steps=total_steps,
|
|
155
|
+
label=label,
|
|
156
|
+
status=status,
|
|
157
|
+
))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def build_tool_start_event(
|
|
161
|
+
tool_name: str,
|
|
162
|
+
tool_id: str,
|
|
163
|
+
arguments: dict | None = None,
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Build a tool call started SSE event."""
|
|
166
|
+
return format_sse_event(ToolCallEvent(
|
|
167
|
+
tool_name=tool_name,
|
|
168
|
+
tool_id=tool_id,
|
|
169
|
+
status="started",
|
|
170
|
+
arguments=arguments,
|
|
171
|
+
))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_tool_complete_event(
|
|
175
|
+
tool_name: str,
|
|
176
|
+
tool_id: str,
|
|
177
|
+
arguments: dict | None = None,
|
|
178
|
+
result: Any = None,
|
|
179
|
+
) -> str:
|
|
180
|
+
"""Build a tool call completed SSE event."""
|
|
181
|
+
result_str = None
|
|
182
|
+
if result is not None:
|
|
183
|
+
result_str = str(result)
|
|
184
|
+
if len(result_str) > 200:
|
|
185
|
+
result_str = result_str[:200] + "..."
|
|
186
|
+
|
|
187
|
+
return format_sse_event(ToolCallEvent(
|
|
188
|
+
tool_name=tool_name,
|
|
189
|
+
tool_id=tool_id,
|
|
190
|
+
status="completed",
|
|
191
|
+
arguments=arguments,
|
|
192
|
+
result=result_str,
|
|
193
|
+
))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_metadata_event(
|
|
197
|
+
message_id: str | None = None,
|
|
198
|
+
in_reply_to: str | None = None,
|
|
199
|
+
session_id: str | None = None,
|
|
200
|
+
agent_schema: str | None = None,
|
|
201
|
+
responding_agent: str | None = None,
|
|
202
|
+
confidence: float | None = None,
|
|
203
|
+
sources: list | None = None,
|
|
204
|
+
model_version: str | None = None,
|
|
205
|
+
latency_ms: int | None = None,
|
|
206
|
+
token_count: int | None = None,
|
|
207
|
+
trace_id: str | None = None,
|
|
208
|
+
span_id: str | None = None,
|
|
209
|
+
extra: dict | None = None,
|
|
210
|
+
) -> str:
|
|
211
|
+
"""Build a metadata SSE event."""
|
|
212
|
+
return format_sse_event(MetadataEvent(
|
|
213
|
+
message_id=message_id,
|
|
214
|
+
in_reply_to=in_reply_to,
|
|
215
|
+
session_id=session_id,
|
|
216
|
+
agent_schema=agent_schema,
|
|
217
|
+
responding_agent=responding_agent,
|
|
218
|
+
confidence=confidence,
|
|
219
|
+
sources=sources,
|
|
220
|
+
model_version=model_version,
|
|
221
|
+
latency_ms=latency_ms,
|
|
222
|
+
token_count=token_count,
|
|
223
|
+
trace_id=trace_id,
|
|
224
|
+
span_id=span_id,
|
|
225
|
+
extra=extra,
|
|
226
|
+
))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# =============================================================================
|
|
230
|
+
# TOOL ARGUMENT EXTRACTION
|
|
231
|
+
# =============================================================================
|
|
232
|
+
|
|
233
|
+
def extract_tool_args(part) -> dict | None:
|
|
234
|
+
"""
|
|
235
|
+
Extract arguments from a ToolCallPart.
|
|
236
|
+
|
|
237
|
+
Handles various formats:
|
|
238
|
+
- ArgsDict object with args_dict attribute
|
|
239
|
+
- Plain dict
|
|
240
|
+
- JSON string
|
|
241
|
+
"""
|
|
242
|
+
if part.args is None:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
if hasattr(part.args, 'args_dict'):
|
|
246
|
+
return part.args.args_dict
|
|
247
|
+
|
|
248
|
+
if isinstance(part.args, dict):
|
|
249
|
+
return part.args
|
|
250
|
+
|
|
251
|
+
if isinstance(part.args, str) and part.args:
|
|
252
|
+
try:
|
|
253
|
+
return json.loads(part.args)
|
|
254
|
+
except json.JSONDecodeError:
|
|
255
|
+
logger.warning(f"Failed to parse tool args: {part.args[:100]}")
|
|
256
|
+
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def log_tool_call(tool_name: str, args_dict: dict | None) -> None:
|
|
261
|
+
"""Log a tool call with key parameters."""
|
|
262
|
+
if args_dict and tool_name == "search_rem":
|
|
263
|
+
query_type = args_dict.get("query_type", "?")
|
|
264
|
+
limit = args_dict.get("limit", 20)
|
|
265
|
+
table = args_dict.get("table", "")
|
|
266
|
+
query_text = args_dict.get("query_text", args_dict.get("entity_key", ""))
|
|
267
|
+
if query_text and len(str(query_text)) > 50:
|
|
268
|
+
query_text = str(query_text)[:50] + "..."
|
|
269
|
+
logger.info(f"🔧 {tool_name} {query_type.upper()} '{query_text}' table={table} limit={limit}")
|
|
270
|
+
else:
|
|
271
|
+
logger.info(f"🔧 {tool_name}")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def log_tool_result(tool_name: str, result_content: Any) -> None:
|
|
275
|
+
"""Log a tool result with key metrics."""
|
|
276
|
+
if tool_name == "search_rem" and isinstance(result_content, dict):
|
|
277
|
+
results = result_content.get("results", {})
|
|
278
|
+
if isinstance(results, dict):
|
|
279
|
+
count = results.get("count", len(results.get("results", [])))
|
|
280
|
+
query_type = results.get("query_type", "?")
|
|
281
|
+
query_text = results.get("query_text", results.get("key", ""))
|
|
282
|
+
table = results.get("table_name", "")
|
|
283
|
+
elif isinstance(results, list):
|
|
284
|
+
count = len(results)
|
|
285
|
+
query_type = "?"
|
|
286
|
+
query_text = ""
|
|
287
|
+
table = ""
|
|
288
|
+
else:
|
|
289
|
+
count = "?"
|
|
290
|
+
query_type = "?"
|
|
291
|
+
query_text = ""
|
|
292
|
+
table = ""
|
|
293
|
+
|
|
294
|
+
if query_text and len(str(query_text)) > 40:
|
|
295
|
+
query_text = str(query_text)[:40] + "..."
|
|
296
|
+
logger.info(f" ↳ {tool_name} {query_type} '{query_text}' table={table} → {count} results")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# =============================================================================
|
|
300
|
+
# METADATA EXTRACTION
|
|
301
|
+
# =============================================================================
|
|
302
|
+
|
|
303
|
+
def extract_metadata_from_result(result_content: Any) -> dict | None:
|
|
304
|
+
"""
|
|
305
|
+
Extract metadata from a register_metadata tool result.
|
|
306
|
+
|
|
307
|
+
Returns dict with extracted fields or None if not a metadata event.
|
|
308
|
+
"""
|
|
309
|
+
if not isinstance(result_content, dict):
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
if not result_content.get("_metadata_event"):
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"confidence": result_content.get("confidence"),
|
|
317
|
+
"sources": result_content.get("sources"),
|
|
318
|
+
"references": result_content.get("references"),
|
|
319
|
+
"flags": result_content.get("flags"),
|
|
320
|
+
"session_name": result_content.get("session_name"),
|
|
321
|
+
"risk_level": result_content.get("risk_level"),
|
|
322
|
+
"risk_score": result_content.get("risk_score"),
|
|
323
|
+
"risk_reasoning": result_content.get("risk_reasoning"),
|
|
324
|
+
"recommended_action": result_content.get("recommended_action"),
|
|
325
|
+
"agent_schema": result_content.get("agent_schema"),
|
|
326
|
+
"extra": result_content.get("extra"),
|
|
327
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common models shared across API routers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ErrorResponse(BaseModel):
|
|
9
|
+
"""Standard error response format for HTTPException errors.
|
|
10
|
+
|
|
11
|
+
This is different from FastAPI's HTTPValidationError which is used
|
|
12
|
+
for Pydantic validation failures (422 errors with loc/msg/type array).
|
|
13
|
+
|
|
14
|
+
HTTPException errors return this simpler format:
|
|
15
|
+
{"detail": "Error message here"}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
detail: str = Field(description="Error message describing what went wrong")
|
rem/api/routers/dev.py
CHANGED
|
@@ -11,6 +11,7 @@ Endpoints:
|
|
|
11
11
|
from fastapi import APIRouter, HTTPException, Request
|
|
12
12
|
from loguru import logger
|
|
13
13
|
|
|
14
|
+
from .common import ErrorResponse
|
|
14
15
|
from ...settings import settings
|
|
15
16
|
|
|
16
17
|
router = APIRouter(prefix="/api/dev", tags=["dev"])
|
|
@@ -45,7 +46,12 @@ def verify_dev_token(token: str) -> bool:
|
|
|
45
46
|
return token == expected
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
@router.get(
|
|
49
|
+
@router.get(
|
|
50
|
+
"/token",
|
|
51
|
+
responses={
|
|
52
|
+
401: {"model": ErrorResponse, "description": "Dev tokens not available in production"},
|
|
53
|
+
},
|
|
54
|
+
)
|
|
49
55
|
async def get_dev_token(request: Request):
|
|
50
56
|
"""
|
|
51
57
|
Get a development token for testing (non-production only).
|
rem/api/routers/feedback.py
CHANGED
|
@@ -63,6 +63,8 @@ from fastapi import APIRouter, Header, HTTPException, Request, Response
|
|
|
63
63
|
from loguru import logger
|
|
64
64
|
from pydantic import BaseModel, Field
|
|
65
65
|
|
|
66
|
+
from .common import ErrorResponse
|
|
67
|
+
|
|
66
68
|
from ..deps import get_user_id_from_request
|
|
67
69
|
from ...models.entities import Feedback
|
|
68
70
|
from ...services.postgres import Repository
|
|
@@ -121,7 +123,13 @@ class FeedbackResponse(BaseModel):
|
|
|
121
123
|
# =============================================================================
|
|
122
124
|
|
|
123
125
|
|
|
124
|
-
@router.post(
|
|
126
|
+
@router.post(
|
|
127
|
+
"/messages/feedback",
|
|
128
|
+
response_model=FeedbackResponse,
|
|
129
|
+
responses={
|
|
130
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
131
|
+
},
|
|
132
|
+
)
|
|
125
133
|
async def submit_feedback(
|
|
126
134
|
request: Request,
|
|
127
135
|
response: Response,
|