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.
Files changed (59) hide show
  1. rem/agentic/README.md +36 -2
  2. rem/agentic/context.py +173 -0
  3. rem/agentic/context_builder.py +12 -2
  4. rem/agentic/mcp/tool_wrapper.py +39 -16
  5. rem/agentic/providers/pydantic_ai.py +78 -45
  6. rem/agentic/schema.py +6 -5
  7. rem/agentic/tools/rem_tools.py +11 -0
  8. rem/api/main.py +1 -1
  9. rem/api/mcp_router/resources.py +75 -14
  10. rem/api/mcp_router/server.py +31 -24
  11. rem/api/mcp_router/tools.py +621 -166
  12. rem/api/routers/admin.py +30 -4
  13. rem/api/routers/auth.py +114 -15
  14. rem/api/routers/chat/child_streaming.py +379 -0
  15. rem/api/routers/chat/completions.py +74 -37
  16. rem/api/routers/chat/sse_events.py +7 -3
  17. rem/api/routers/chat/streaming.py +352 -257
  18. rem/api/routers/chat/streaming_utils.py +327 -0
  19. rem/api/routers/common.py +18 -0
  20. rem/api/routers/dev.py +7 -1
  21. rem/api/routers/feedback.py +9 -1
  22. rem/api/routers/messages.py +176 -38
  23. rem/api/routers/models.py +9 -1
  24. rem/api/routers/query.py +12 -1
  25. rem/api/routers/shared_sessions.py +16 -0
  26. rem/auth/jwt.py +19 -4
  27. rem/auth/middleware.py +42 -28
  28. rem/cli/README.md +62 -0
  29. rem/cli/commands/ask.py +61 -81
  30. rem/cli/commands/db.py +148 -70
  31. rem/cli/commands/process.py +171 -43
  32. rem/models/entities/ontology.py +91 -101
  33. rem/schemas/agents/rem.yaml +1 -1
  34. rem/services/content/service.py +18 -5
  35. rem/services/email/service.py +11 -2
  36. rem/services/embeddings/worker.py +26 -12
  37. rem/services/postgres/__init__.py +28 -3
  38. rem/services/postgres/diff_service.py +57 -5
  39. rem/services/postgres/programmable_diff_service.py +635 -0
  40. rem/services/postgres/pydantic_to_sqlalchemy.py +2 -2
  41. rem/services/postgres/register_type.py +12 -11
  42. rem/services/postgres/repository.py +39 -29
  43. rem/services/postgres/schema_generator.py +5 -5
  44. rem/services/postgres/sql_builder.py +6 -5
  45. rem/services/session/__init__.py +8 -1
  46. rem/services/session/compression.py +40 -2
  47. rem/services/session/pydantic_messages.py +292 -0
  48. rem/settings.py +34 -0
  49. rem/sql/background_indexes.sql +5 -0
  50. rem/sql/migrations/001_install.sql +157 -10
  51. rem/sql/migrations/002_install_models.sql +160 -132
  52. rem/sql/migrations/004_cache_system.sql +7 -275
  53. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  54. rem/utils/model_helpers.py +101 -0
  55. rem/utils/schema_loader.py +79 -51
  56. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/METADATA +2 -2
  57. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/RECORD +59 -53
  58. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/WHEEL +0 -0
  59. {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("/token")
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).
@@ -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("/messages/feedback", response_model=FeedbackResponse)
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,