remdb 0.3.200__py3-none-any.whl → 0.3.226__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 +262 -2
- rem/agentic/context.py +73 -1
- rem/agentic/mcp/tool_wrapper.py +2 -2
- rem/agentic/providers/pydantic_ai.py +1 -1
- rem/agentic/schema.py +2 -2
- rem/api/mcp_router/tools.py +154 -18
- rem/api/routers/admin.py +30 -4
- rem/api/routers/auth.py +106 -10
- rem/api/routers/chat/completions.py +24 -29
- rem/api/routers/chat/sse_events.py +5 -1
- rem/api/routers/chat/streaming.py +163 -2
- 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 +12 -1
- rem/api/routers/shared_sessions.py +16 -0
- rem/auth/jwt.py +19 -4
- rem/cli/commands/ask.py +61 -81
- rem/cli/commands/process.py +3 -3
- rem/models/entities/ontology.py +18 -20
- rem/schemas/agents/rem.yaml +1 -1
- rem/services/postgres/repository.py +14 -4
- rem/services/session/__init__.py +2 -1
- rem/services/session/compression.py +40 -2
- rem/services/session/pydantic_messages.py +66 -0
- rem/settings.py +28 -0
- rem/sql/migrations/001_install.sql +13 -3
- rem/sql/migrations/002_install_models.sql +20 -22
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/schema_loader.py +73 -45
- {remdb-0.3.200.dist-info → remdb-0.3.226.dist-info}/METADATA +1 -1
- {remdb-0.3.200.dist-info → remdb-0.3.226.dist-info}/RECORD +36 -34
- {remdb-0.3.200.dist-info → remdb-0.3.226.dist-info}/WHEEL +0 -0
- {remdb-0.3.200.dist-info → remdb-0.3.226.dist-info}/entry_points.txt +0 -0
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,
|
rem/api/routers/messages.py
CHANGED
|
@@ -16,6 +16,7 @@ Endpoints:
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
from datetime import datetime
|
|
19
|
+
from enum import Enum
|
|
19
20
|
from typing import Literal
|
|
20
21
|
from uuid import UUID
|
|
21
22
|
|
|
@@ -23,6 +24,8 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
|
|
|
23
24
|
from loguru import logger
|
|
24
25
|
from pydantic import BaseModel, Field
|
|
25
26
|
|
|
27
|
+
from .common import ErrorResponse
|
|
28
|
+
|
|
26
29
|
from ..deps import (
|
|
27
30
|
get_current_user,
|
|
28
31
|
get_user_filter,
|
|
@@ -38,6 +41,18 @@ from ...utils.date_utils import parse_iso, utc_now
|
|
|
38
41
|
router = APIRouter(prefix="/api/v1")
|
|
39
42
|
|
|
40
43
|
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Enums
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SortOrder(str, Enum):
|
|
50
|
+
"""Sort order for list queries."""
|
|
51
|
+
|
|
52
|
+
ASC = "asc"
|
|
53
|
+
DESC = "desc"
|
|
54
|
+
|
|
55
|
+
|
|
41
56
|
# =============================================================================
|
|
42
57
|
# Request/Response Models
|
|
43
58
|
# =============================================================================
|
|
@@ -134,7 +149,14 @@ class SessionsQueryResponse(BaseModel):
|
|
|
134
149
|
# =============================================================================
|
|
135
150
|
|
|
136
151
|
|
|
137
|
-
@router.get(
|
|
152
|
+
@router.get(
|
|
153
|
+
"/messages",
|
|
154
|
+
response_model=MessageListResponse,
|
|
155
|
+
tags=["messages"],
|
|
156
|
+
responses={
|
|
157
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
158
|
+
},
|
|
159
|
+
)
|
|
138
160
|
async def list_messages(
|
|
139
161
|
request: Request,
|
|
140
162
|
mine: bool = Query(default=False, description="Only show my messages (uses JWT identity)"),
|
|
@@ -151,6 +173,7 @@ async def list_messages(
|
|
|
151
173
|
),
|
|
152
174
|
limit: int = Query(default=50, ge=1, le=100, description="Max results to return"),
|
|
153
175
|
offset: int = Query(default=0, ge=0, description="Offset for pagination"),
|
|
176
|
+
sort: SortOrder = Query(default=SortOrder.DESC, description="Sort order by created_at (asc or desc)"),
|
|
154
177
|
) -> MessageListResponse:
|
|
155
178
|
"""
|
|
156
179
|
List messages with optional filters.
|
|
@@ -166,8 +189,9 @@ async def list_messages(
|
|
|
166
189
|
- session_id: Filter by conversation session
|
|
167
190
|
- start_date/end_date: Filter by creation time range (ISO 8601 format)
|
|
168
191
|
- message_type: Filter by role (user, assistant, system, tool)
|
|
192
|
+
- sort: Sort order by created_at (asc or desc, default: desc)
|
|
169
193
|
|
|
170
|
-
Returns paginated results ordered by created_at
|
|
194
|
+
Returns paginated results ordered by created_at.
|
|
171
195
|
"""
|
|
172
196
|
if not settings.postgres.enabled:
|
|
173
197
|
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
@@ -189,6 +213,7 @@ async def list_messages(
|
|
|
189
213
|
|
|
190
214
|
# Apply optional filters
|
|
191
215
|
if session_id:
|
|
216
|
+
# session_id is the session UUID - use directly
|
|
192
217
|
filters["session_id"] = session_id
|
|
193
218
|
if message_type:
|
|
194
219
|
filters["message_type"] = message_type
|
|
@@ -200,12 +225,15 @@ async def list_messages(
|
|
|
200
225
|
f"filters={filters}"
|
|
201
226
|
)
|
|
202
227
|
|
|
228
|
+
# Build order_by clause based on sort parameter
|
|
229
|
+
order_by = f"created_at {sort.value.upper()}"
|
|
230
|
+
|
|
203
231
|
# For date filtering, we need custom SQL (not supported by basic Repository)
|
|
204
232
|
# For now, fetch all matching base filters and filter in Python
|
|
205
233
|
# TODO: Extend Repository to support date range filters
|
|
206
234
|
messages = await repo.find(
|
|
207
235
|
filters,
|
|
208
|
-
order_by=
|
|
236
|
+
order_by=order_by,
|
|
209
237
|
limit=limit + 1, # Fetch one extra to determine has_more
|
|
210
238
|
offset=offset,
|
|
211
239
|
)
|
|
@@ -241,7 +269,16 @@ async def list_messages(
|
|
|
241
269
|
return MessageListResponse(data=messages, total=total, has_more=has_more)
|
|
242
270
|
|
|
243
271
|
|
|
244
|
-
@router.get(
|
|
272
|
+
@router.get(
|
|
273
|
+
"/messages/{message_id}",
|
|
274
|
+
response_model=Message,
|
|
275
|
+
tags=["messages"],
|
|
276
|
+
responses={
|
|
277
|
+
403: {"model": ErrorResponse, "description": "Access denied: not owner"},
|
|
278
|
+
404: {"model": ErrorResponse, "description": "Message not found"},
|
|
279
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
280
|
+
},
|
|
281
|
+
)
|
|
245
282
|
async def get_message(
|
|
246
283
|
request: Request,
|
|
247
284
|
message_id: str,
|
|
@@ -287,7 +324,14 @@ async def get_message(
|
|
|
287
324
|
# =============================================================================
|
|
288
325
|
|
|
289
326
|
|
|
290
|
-
@router.get(
|
|
327
|
+
@router.get(
|
|
328
|
+
"/sessions",
|
|
329
|
+
response_model=SessionsQueryResponse,
|
|
330
|
+
tags=["sessions"],
|
|
331
|
+
responses={
|
|
332
|
+
503: {"model": ErrorResponse, "description": "Database not enabled or connection failed"},
|
|
333
|
+
},
|
|
334
|
+
)
|
|
291
335
|
async def list_sessions(
|
|
292
336
|
request: Request,
|
|
293
337
|
user_id: str | None = Query(default=None, description="Filter by user ID (admin only for cross-user)"),
|
|
@@ -400,7 +444,15 @@ async def list_sessions(
|
|
|
400
444
|
)
|
|
401
445
|
|
|
402
446
|
|
|
403
|
-
@router.post(
|
|
447
|
+
@router.post(
|
|
448
|
+
"/sessions",
|
|
449
|
+
response_model=Session,
|
|
450
|
+
status_code=201,
|
|
451
|
+
tags=["sessions"],
|
|
452
|
+
responses={
|
|
453
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
454
|
+
},
|
|
455
|
+
)
|
|
404
456
|
async def create_session(
|
|
405
457
|
request_body: SessionCreateRequest,
|
|
406
458
|
user: dict = Depends(require_admin),
|
|
@@ -452,7 +504,16 @@ async def create_session(
|
|
|
452
504
|
return result # type: ignore
|
|
453
505
|
|
|
454
506
|
|
|
455
|
-
@router.get(
|
|
507
|
+
@router.get(
|
|
508
|
+
"/sessions/{session_id}",
|
|
509
|
+
response_model=Session,
|
|
510
|
+
tags=["sessions"],
|
|
511
|
+
responses={
|
|
512
|
+
403: {"model": ErrorResponse, "description": "Access denied: not owner"},
|
|
513
|
+
404: {"model": ErrorResponse, "description": "Session not found"},
|
|
514
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
515
|
+
},
|
|
516
|
+
)
|
|
456
517
|
async def get_session(
|
|
457
518
|
request: Request,
|
|
458
519
|
session_id: str,
|
|
@@ -465,7 +526,7 @@ async def get_session(
|
|
|
465
526
|
- Admin users: Can access any session
|
|
466
527
|
|
|
467
528
|
Args:
|
|
468
|
-
session_id: UUID
|
|
529
|
+
session_id: UUID of the session
|
|
469
530
|
|
|
470
531
|
Returns:
|
|
471
532
|
Session object if found
|
|
@@ -481,12 +542,7 @@ async def get_session(
|
|
|
481
542
|
session = await repo.get_by_id(session_id)
|
|
482
543
|
|
|
483
544
|
if not session:
|
|
484
|
-
|
|
485
|
-
sessions = await repo.find({"name": session_id}, limit=1)
|
|
486
|
-
if sessions:
|
|
487
|
-
session = sessions[0]
|
|
488
|
-
else:
|
|
489
|
-
raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
|
|
545
|
+
raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
|
|
490
546
|
|
|
491
547
|
# Check access: admin or owner
|
|
492
548
|
current_user = get_current_user(request)
|
|
@@ -498,7 +554,16 @@ async def get_session(
|
|
|
498
554
|
return session
|
|
499
555
|
|
|
500
556
|
|
|
501
|
-
@router.put(
|
|
557
|
+
@router.put(
|
|
558
|
+
"/sessions/{session_id}",
|
|
559
|
+
response_model=Session,
|
|
560
|
+
tags=["sessions"],
|
|
561
|
+
responses={
|
|
562
|
+
403: {"model": ErrorResponse, "description": "Access denied: not owner"},
|
|
563
|
+
404: {"model": ErrorResponse, "description": "Session not found"},
|
|
564
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
565
|
+
},
|
|
566
|
+
)
|
|
502
567
|
async def update_session(
|
|
503
568
|
request: Request,
|
|
504
569
|
session_id: str,
|
rem/api/routers/models.py
CHANGED
|
@@ -15,6 +15,8 @@ from typing import Literal
|
|
|
15
15
|
from fastapi import APIRouter, HTTPException
|
|
16
16
|
from pydantic import BaseModel, Field
|
|
17
17
|
|
|
18
|
+
from .common import ErrorResponse
|
|
19
|
+
|
|
18
20
|
from rem.agentic.llm_provider_models import (
|
|
19
21
|
ModelInfo,
|
|
20
22
|
AVAILABLE_MODELS,
|
|
@@ -57,7 +59,13 @@ async def list_models() -> ModelsResponse:
|
|
|
57
59
|
return ModelsResponse(data=AVAILABLE_MODELS)
|
|
58
60
|
|
|
59
61
|
|
|
60
|
-
@router.get(
|
|
62
|
+
@router.get(
|
|
63
|
+
"/models/{model_id:path}",
|
|
64
|
+
response_model=ModelInfo,
|
|
65
|
+
responses={
|
|
66
|
+
404: {"model": ErrorResponse, "description": "Model not found"},
|
|
67
|
+
},
|
|
68
|
+
)
|
|
61
69
|
async def get_model(model_id: str) -> ModelInfo:
|
|
62
70
|
"""
|
|
63
71
|
Get information about a specific model.
|
rem/api/routers/query.py
CHANGED
|
@@ -86,6 +86,8 @@ from fastapi import APIRouter, Header, HTTPException
|
|
|
86
86
|
from loguru import logger
|
|
87
87
|
from pydantic import BaseModel, Field
|
|
88
88
|
|
|
89
|
+
from .common import ErrorResponse
|
|
90
|
+
|
|
89
91
|
from ...services.postgres import get_postgres_service
|
|
90
92
|
from ...services.rem.service import RemService
|
|
91
93
|
from ...services.rem.parser import RemQueryParser
|
|
@@ -213,7 +215,16 @@ class QueryResponse(BaseModel):
|
|
|
213
215
|
)
|
|
214
216
|
|
|
215
217
|
|
|
216
|
-
@router.post(
|
|
218
|
+
@router.post(
|
|
219
|
+
"/query",
|
|
220
|
+
response_model=QueryResponse,
|
|
221
|
+
responses={
|
|
222
|
+
400: {"model": ErrorResponse, "description": "Invalid query or missing required fields"},
|
|
223
|
+
500: {"model": ErrorResponse, "description": "Query execution failed"},
|
|
224
|
+
501: {"model": ErrorResponse, "description": "Feature not yet implemented"},
|
|
225
|
+
503: {"model": ErrorResponse, "description": "Database not configured or unavailable"},
|
|
226
|
+
},
|
|
227
|
+
)
|
|
217
228
|
async def execute_query(
|
|
218
229
|
request: QueryRequest,
|
|
219
230
|
x_user_id: str | None = Header(default=None, description="User ID for query isolation (optional, uses default if not provided)"),
|
|
@@ -18,6 +18,8 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
|
|
|
18
18
|
from loguru import logger
|
|
19
19
|
from pydantic import BaseModel, Field
|
|
20
20
|
|
|
21
|
+
from .common import ErrorResponse
|
|
22
|
+
|
|
21
23
|
from ..deps import get_current_user, require_auth
|
|
22
24
|
from ...models.entities import (
|
|
23
25
|
Message,
|
|
@@ -83,6 +85,10 @@ class ShareSessionResponse(BaseModel):
|
|
|
83
85
|
response_model=ShareSessionResponse,
|
|
84
86
|
status_code=201,
|
|
85
87
|
tags=["sessions"],
|
|
88
|
+
responses={
|
|
89
|
+
400: {"model": ErrorResponse, "description": "Session already shared with this user"},
|
|
90
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
91
|
+
},
|
|
86
92
|
)
|
|
87
93
|
async def share_session(
|
|
88
94
|
request: Request,
|
|
@@ -175,6 +181,10 @@ async def share_session(
|
|
|
175
181
|
"/sessions/{session_id}/share/{shared_with_user_id}",
|
|
176
182
|
status_code=200,
|
|
177
183
|
tags=["sessions"],
|
|
184
|
+
responses={
|
|
185
|
+
404: {"model": ErrorResponse, "description": "Share not found"},
|
|
186
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
187
|
+
},
|
|
178
188
|
)
|
|
179
189
|
async def remove_session_share(
|
|
180
190
|
request: Request,
|
|
@@ -250,6 +260,9 @@ async def remove_session_share(
|
|
|
250
260
|
"/sessions/shared-with-me",
|
|
251
261
|
response_model=SharedWithMeResponse,
|
|
252
262
|
tags=["sessions"],
|
|
263
|
+
responses={
|
|
264
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
265
|
+
},
|
|
253
266
|
)
|
|
254
267
|
async def get_shared_with_me(
|
|
255
268
|
request: Request,
|
|
@@ -328,6 +341,9 @@ async def get_shared_with_me(
|
|
|
328
341
|
"/sessions/shared-with-me/{owner_user_id}/messages",
|
|
329
342
|
response_model=SharedMessagesResponse,
|
|
330
343
|
tags=["sessions"],
|
|
344
|
+
responses={
|
|
345
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
346
|
+
},
|
|
331
347
|
)
|
|
332
348
|
async def get_shared_messages(
|
|
333
349
|
request: Request,
|
rem/auth/jwt.py
CHANGED
|
@@ -260,12 +260,16 @@ class JWTService:
|
|
|
260
260
|
"tenant_id": payload.get("tenant_id", "default"),
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
def refresh_access_token(
|
|
263
|
+
def refresh_access_token(
|
|
264
|
+
self, refresh_token: str, user_override: dict | None = None
|
|
265
|
+
) -> dict | None:
|
|
264
266
|
"""
|
|
265
267
|
Create new access token using refresh token.
|
|
266
268
|
|
|
267
269
|
Args:
|
|
268
270
|
refresh_token: Valid refresh token
|
|
271
|
+
user_override: Optional dict with user fields to override defaults
|
|
272
|
+
(e.g., role, roles, tier, name from database lookup)
|
|
269
273
|
|
|
270
274
|
Returns:
|
|
271
275
|
New token dict or None if refresh token is invalid
|
|
@@ -285,8 +289,7 @@ class JWTService:
|
|
|
285
289
|
logger.debug("Refresh token expired")
|
|
286
290
|
return None
|
|
287
291
|
|
|
288
|
-
#
|
|
289
|
-
# In production, you'd look up the full user from database
|
|
292
|
+
# Build user dict with defaults
|
|
290
293
|
user = {
|
|
291
294
|
"id": payload.get("sub"),
|
|
292
295
|
"email": payload.get("email"),
|
|
@@ -294,16 +297,28 @@ class JWTService:
|
|
|
294
297
|
"provider": "email",
|
|
295
298
|
"tenant_id": "default",
|
|
296
299
|
"tier": "free",
|
|
300
|
+
"role": "user",
|
|
297
301
|
"roles": ["user"],
|
|
298
302
|
}
|
|
299
303
|
|
|
304
|
+
# Apply overrides from database lookup if provided
|
|
305
|
+
if user_override:
|
|
306
|
+
if user_override.get("role"):
|
|
307
|
+
user["role"] = user_override["role"]
|
|
308
|
+
if user_override.get("roles"):
|
|
309
|
+
user["roles"] = user_override["roles"]
|
|
310
|
+
if user_override.get("tier"):
|
|
311
|
+
user["tier"] = user_override["tier"]
|
|
312
|
+
if user_override.get("name"):
|
|
313
|
+
user["name"] = user_override["name"]
|
|
314
|
+
|
|
300
315
|
# Only return new access token, keep same refresh token
|
|
301
316
|
now = int(time.time())
|
|
302
317
|
access_payload = {
|
|
303
318
|
"sub": user["id"],
|
|
304
319
|
"email": user["email"],
|
|
305
320
|
"name": user["name"],
|
|
306
|
-
"role": user
|
|
321
|
+
"role": user["role"],
|
|
307
322
|
"tier": user["tier"],
|
|
308
323
|
"roles": user["roles"],
|
|
309
324
|
"provider": user["provider"],
|
rem/cli/commands/ask.py
CHANGED
|
@@ -71,16 +71,18 @@ async def run_agent_streaming(
|
|
|
71
71
|
max_turns: int = 10,
|
|
72
72
|
context: AgentContext | None = None,
|
|
73
73
|
max_iterations: int | None = None,
|
|
74
|
+
user_message: str | None = None,
|
|
74
75
|
) -> None:
|
|
75
76
|
"""
|
|
76
|
-
Run agent in streaming mode using
|
|
77
|
+
Run agent in streaming mode using the SAME code path as the API.
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
This uses stream_openai_response_with_save from the API to ensure:
|
|
80
|
+
1. Tool calls are saved as separate "tool" messages (not embedded in content)
|
|
81
|
+
2. Assistant response is clean text only (no [Calling: ...] markers)
|
|
82
|
+
3. CLI testing is equivalent to API testing
|
|
83
|
+
|
|
84
|
+
The CLI displays tool calls as [Calling: tool_name] for visibility,
|
|
85
|
+
but these are NOT saved to the database.
|
|
84
86
|
|
|
85
87
|
Args:
|
|
86
88
|
agent: Pydantic AI agent
|
|
@@ -88,88 +90,66 @@ async def run_agent_streaming(
|
|
|
88
90
|
max_turns: Maximum turns for agent execution (not used in current API)
|
|
89
91
|
context: Optional AgentContext for session persistence
|
|
90
92
|
max_iterations: Maximum iterations/requests (from agent schema or settings)
|
|
93
|
+
user_message: The user's original message (for database storage)
|
|
91
94
|
"""
|
|
92
|
-
|
|
93
|
-
from rem.
|
|
95
|
+
import json
|
|
96
|
+
from rem.api.routers.chat.streaming import stream_openai_response_with_save, save_user_message
|
|
94
97
|
|
|
95
98
|
logger.info("Running agent in streaming mode...")
|
|
96
99
|
|
|
97
100
|
try:
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# Accumulate assistant response for session persistence
|
|
103
|
-
assistant_response_parts = []
|
|
104
|
-
|
|
105
|
-
# Use agent.iter() to get complete execution with tool calls
|
|
106
|
-
usage_limits = UsageLimits(request_limit=max_iterations) if max_iterations else None
|
|
107
|
-
async with agent.iter(prompt, usage_limits=usage_limits) as agent_run:
|
|
108
|
-
async for node in agent_run:
|
|
109
|
-
# Check if this is a model request node (includes tool calls and text)
|
|
110
|
-
if PydanticAgent.is_model_request_node(node):
|
|
111
|
-
# Stream events from model request
|
|
112
|
-
request_stream: Any
|
|
113
|
-
async with node.stream(agent_run.ctx) as request_stream:
|
|
114
|
-
async for event in request_stream:
|
|
115
|
-
# Tool call start event
|
|
116
|
-
if isinstance(event, PartStartEvent) and isinstance(
|
|
117
|
-
event.part, ToolCallPart
|
|
118
|
-
):
|
|
119
|
-
tool_marker = f"\n[Calling: {event.part.tool_name}]"
|
|
120
|
-
print(tool_marker, flush=True)
|
|
121
|
-
assistant_response_parts.append(tool_marker)
|
|
122
|
-
|
|
123
|
-
# Text content delta
|
|
124
|
-
elif isinstance(event, PartDeltaEvent) and isinstance(
|
|
125
|
-
event.delta, TextPartDelta
|
|
126
|
-
):
|
|
127
|
-
print(event.delta.content_delta, end="", flush=True)
|
|
128
|
-
assistant_response_parts.append(event.delta.content_delta)
|
|
129
|
-
|
|
130
|
-
print("\n") # Final newline after streaming
|
|
131
|
-
|
|
132
|
-
# Get final result from agent_run
|
|
133
|
-
result = agent_run.result
|
|
134
|
-
if hasattr(result, "output"):
|
|
135
|
-
logger.info("Final structured result:")
|
|
136
|
-
output = result.output
|
|
137
|
-
from rem.agentic.serialization import serialize_agent_result
|
|
138
|
-
output_json = json.dumps(serialize_agent_result(output), indent=2)
|
|
139
|
-
print(output_json)
|
|
140
|
-
assistant_response_parts.append(f"\n{output_json}")
|
|
141
|
-
|
|
142
|
-
# Save session messages (if session_id provided and postgres enabled)
|
|
143
|
-
if context and context.session_id and settings.postgres.enabled:
|
|
144
|
-
from ...services.session.compression import SessionMessageStore
|
|
145
|
-
|
|
146
|
-
# Extract just the user query from prompt
|
|
147
|
-
# Prompt format from ContextBuilder: system + history + user message
|
|
148
|
-
# We need to extract the last user message
|
|
149
|
-
user_message_content = prompt.split("\n\n")[-1] if "\n\n" in prompt else prompt
|
|
150
|
-
|
|
151
|
-
user_message = {
|
|
152
|
-
"role": "user",
|
|
153
|
-
"content": user_message_content,
|
|
154
|
-
"timestamp": to_iso_with_z(utc_now()),
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
assistant_message = {
|
|
158
|
-
"role": "assistant",
|
|
159
|
-
"content": "".join(assistant_response_parts),
|
|
160
|
-
"timestamp": to_iso_with_z(utc_now()),
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
# Store messages with compression
|
|
164
|
-
store = SessionMessageStore(user_id=context.user_id or settings.test.effective_user_id)
|
|
165
|
-
await store.store_session_messages(
|
|
101
|
+
# Save user message BEFORE streaming (same as API, using shared utility)
|
|
102
|
+
if context and context.session_id and user_message:
|
|
103
|
+
await save_user_message(
|
|
166
104
|
session_id=context.session_id,
|
|
167
|
-
messages=[user_message, assistant_message],
|
|
168
105
|
user_id=context.user_id,
|
|
169
|
-
|
|
106
|
+
content=user_message,
|
|
170
107
|
)
|
|
171
108
|
|
|
172
|
-
|
|
109
|
+
# Use the API streaming code path for consistency
|
|
110
|
+
# This properly handles tool calls and message persistence
|
|
111
|
+
model_name = getattr(agent, 'model', 'unknown')
|
|
112
|
+
if hasattr(model_name, 'model_name'):
|
|
113
|
+
model_name = model_name.model_name
|
|
114
|
+
elif hasattr(model_name, 'name'):
|
|
115
|
+
model_name = model_name.name
|
|
116
|
+
else:
|
|
117
|
+
model_name = str(model_name)
|
|
118
|
+
|
|
119
|
+
async for chunk in stream_openai_response_with_save(
|
|
120
|
+
agent=agent.agent if hasattr(agent, 'agent') else agent,
|
|
121
|
+
prompt=prompt,
|
|
122
|
+
model=model_name,
|
|
123
|
+
session_id=context.session_id if context else None,
|
|
124
|
+
user_id=context.user_id if context else None,
|
|
125
|
+
agent_context=context,
|
|
126
|
+
):
|
|
127
|
+
# Parse SSE chunks for CLI display
|
|
128
|
+
if chunk.startswith("event: tool_call"):
|
|
129
|
+
# Extract tool call info from next data line
|
|
130
|
+
continue
|
|
131
|
+
elif chunk.startswith("data: ") and not chunk.startswith("data: [DONE]"):
|
|
132
|
+
try:
|
|
133
|
+
data_str = chunk[6:].strip()
|
|
134
|
+
if data_str:
|
|
135
|
+
data = json.loads(data_str)
|
|
136
|
+
# Check for tool_call event
|
|
137
|
+
if data.get("type") == "tool_call":
|
|
138
|
+
tool_name = data.get("tool_name", "tool")
|
|
139
|
+
status = data.get("status", "")
|
|
140
|
+
if status == "started":
|
|
141
|
+
print(f"\n[Calling: {tool_name}]", flush=True)
|
|
142
|
+
# Check for text content (OpenAI format)
|
|
143
|
+
elif "choices" in data and data["choices"]:
|
|
144
|
+
delta = data["choices"][0].get("delta", {})
|
|
145
|
+
content = delta.get("content")
|
|
146
|
+
if content:
|
|
147
|
+
print(content, end="", flush=True)
|
|
148
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
print("\n") # Final newline after streaming
|
|
152
|
+
logger.info("Final structured result:")
|
|
173
153
|
|
|
174
154
|
except Exception as e:
|
|
175
155
|
logger.error(f"Agent execution failed: {e}")
|
|
@@ -549,7 +529,7 @@ async def _ask_async(
|
|
|
549
529
|
|
|
550
530
|
# Run agent with session persistence
|
|
551
531
|
if stream:
|
|
552
|
-
await run_agent_streaming(agent, prompt, max_turns=max_turns, context=context)
|
|
532
|
+
await run_agent_streaming(agent, prompt, max_turns=max_turns, context=context, user_message=query)
|
|
553
533
|
else:
|
|
554
534
|
await run_agent_non_streaming(
|
|
555
535
|
agent,
|
rem/cli/commands/process.py
CHANGED
|
@@ -206,9 +206,9 @@ def process_ingest(
|
|
|
206
206
|
if category:
|
|
207
207
|
entity_data["category"] = category
|
|
208
208
|
|
|
209
|
-
# Scoping: user_id for private data,
|
|
210
|
-
# tenant_id=
|
|
211
|
-
entity_data["tenant_id"] = user_id
|
|
209
|
+
# Scoping: user_id for private data, "public" for shared
|
|
210
|
+
# tenant_id="public" is the default for shared knowledge bases
|
|
211
|
+
entity_data["tenant_id"] = user_id or "public"
|
|
212
212
|
entity_data["user_id"] = user_id # None = public/shared
|
|
213
213
|
|
|
214
214
|
# For ontologies, add URI
|
rem/models/entities/ontology.py
CHANGED
|
@@ -103,32 +103,30 @@ class Ontology(CoreModel):
|
|
|
103
103
|
tags=["cv", "engineering"]
|
|
104
104
|
)
|
|
105
105
|
|
|
106
|
-
# Direct-loaded:
|
|
107
|
-
|
|
108
|
-
name="
|
|
109
|
-
uri="git://
|
|
110
|
-
content="#
|
|
106
|
+
# Direct-loaded: Knowledge base from git
|
|
107
|
+
api_docs = Ontology(
|
|
108
|
+
name="rest-api-guide",
|
|
109
|
+
uri="git://example-org/docs/api/rest-api-guide.md",
|
|
110
|
+
content="# REST API Guide\\n\\nThis guide covers RESTful API design...",
|
|
111
111
|
extracted_data={
|
|
112
|
-
"type": "
|
|
113
|
-
"category": "
|
|
114
|
-
"
|
|
115
|
-
"dsm5_criteria": ["A", "B", "C", "D"],
|
|
112
|
+
"type": "documentation",
|
|
113
|
+
"category": "api",
|
|
114
|
+
"version": "2.0",
|
|
116
115
|
},
|
|
117
|
-
tags=["
|
|
116
|
+
tags=["api", "rest", "documentation"]
|
|
118
117
|
)
|
|
119
118
|
|
|
120
|
-
# Direct-loaded:
|
|
121
|
-
|
|
122
|
-
name="
|
|
123
|
-
uri="git://
|
|
124
|
-
content="#
|
|
119
|
+
# Direct-loaded: Technical spec from git
|
|
120
|
+
config_spec = Ontology(
|
|
121
|
+
name="config-schema",
|
|
122
|
+
uri="git://example-org/docs/specs/config-schema.md",
|
|
123
|
+
content="# Configuration Schema\\n\\nThis document defines...",
|
|
125
124
|
extracted_data={
|
|
126
|
-
"type": "
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"dsm5_criterion": "Panic Attack Specifier",
|
|
125
|
+
"type": "specification",
|
|
126
|
+
"format": "yaml",
|
|
127
|
+
"version": "1.0",
|
|
130
128
|
},
|
|
131
|
-
tags=["
|
|
129
|
+
tags=["config", "schema", "specification"]
|
|
132
130
|
)
|
|
133
131
|
"""
|
|
134
132
|
|
rem/schemas/agents/rem.yaml
CHANGED
|
@@ -124,7 +124,7 @@ json_schema_extra:
|
|
|
124
124
|
|
|
125
125
|
# Explicit resource declarations for reference data
|
|
126
126
|
resources:
|
|
127
|
-
- uri: rem://
|
|
127
|
+
- uri: rem://agents
|
|
128
128
|
name: Agent Schemas List
|
|
129
129
|
description: List all available agent schemas in the system
|
|
130
130
|
- uri: rem://status
|
|
@@ -31,17 +31,27 @@ if TYPE_CHECKING:
|
|
|
31
31
|
from .service import PostgresService
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
# Singleton instance for connection pool reuse
|
|
35
|
+
_postgres_instance: "PostgresService | None" = None
|
|
36
|
+
|
|
37
|
+
|
|
34
38
|
def get_postgres_service() -> "PostgresService | None":
|
|
35
39
|
"""
|
|
36
|
-
Get PostgresService instance
|
|
40
|
+
Get PostgresService singleton instance.
|
|
37
41
|
|
|
38
42
|
Returns None if Postgres is disabled.
|
|
43
|
+
Uses singleton pattern to prevent connection pool exhaustion.
|
|
39
44
|
"""
|
|
45
|
+
global _postgres_instance
|
|
46
|
+
|
|
40
47
|
if not settings.postgres.enabled:
|
|
41
48
|
return None
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
|
|
50
|
+
if _postgres_instance is None:
|
|
51
|
+
from .service import PostgresService
|
|
52
|
+
_postgres_instance = PostgresService()
|
|
53
|
+
|
|
54
|
+
return _postgres_instance
|
|
45
55
|
|
|
46
56
|
T = TypeVar("T", bound=BaseModel)
|
|
47
57
|
|