remdb 0.3.172__py3-none-any.whl → 0.3.223__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 +173 -0
- rem/agentic/context_builder.py +12 -2
- rem/agentic/mcp/tool_wrapper.py +39 -16
- rem/agentic/providers/pydantic_ai.py +46 -43
- rem/agentic/schema.py +2 -2
- rem/agentic/tools/rem_tools.py +11 -0
- rem/api/main.py +1 -1
- rem/api/mcp_router/resources.py +64 -8
- 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/completions.py +66 -18
- rem/api/routers/chat/sse_events.py +7 -3
- rem/api/routers/chat/streaming.py +254 -22
- 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 +1 -1
- 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 +46 -25
- 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 +276 -0
- rem/settings.py +28 -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 +6 -6
- {remdb-0.3.172.dist-info → remdb-0.3.223.dist-info}/METADATA +1 -1
- {remdb-0.3.172.dist-info → remdb-0.3.223.dist-info}/RECORD +57 -53
- {remdb-0.3.172.dist-info → remdb-0.3.223.dist-info}/WHEEL +0 -0
- {remdb-0.3.172.dist-info → remdb-0.3.223.dist-info}/entry_points.txt +0 -0
rem/api/routers/admin.py
CHANGED
|
@@ -31,6 +31,8 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Background
|
|
|
31
31
|
from loguru import logger
|
|
32
32
|
from pydantic import BaseModel
|
|
33
33
|
|
|
34
|
+
from .common import ErrorResponse
|
|
35
|
+
|
|
34
36
|
from ..deps import require_admin
|
|
35
37
|
from ...models.entities import Message, Session, SessionMode
|
|
36
38
|
from ...services.postgres import Repository
|
|
@@ -103,7 +105,13 @@ class SystemStats(BaseModel):
|
|
|
103
105
|
# =============================================================================
|
|
104
106
|
|
|
105
107
|
|
|
106
|
-
@router.get(
|
|
108
|
+
@router.get(
|
|
109
|
+
"/users",
|
|
110
|
+
response_model=UserListResponse,
|
|
111
|
+
responses={
|
|
112
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
113
|
+
},
|
|
114
|
+
)
|
|
107
115
|
async def list_all_users(
|
|
108
116
|
user: dict = Depends(require_admin),
|
|
109
117
|
limit: int = Query(default=50, ge=1, le=100),
|
|
@@ -155,7 +163,13 @@ async def list_all_users(
|
|
|
155
163
|
return UserListResponse(data=summaries, total=total, has_more=has_more)
|
|
156
164
|
|
|
157
165
|
|
|
158
|
-
@router.get(
|
|
166
|
+
@router.get(
|
|
167
|
+
"/sessions",
|
|
168
|
+
response_model=SessionListResponse,
|
|
169
|
+
responses={
|
|
170
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
171
|
+
},
|
|
172
|
+
)
|
|
159
173
|
async def list_all_sessions(
|
|
160
174
|
user: dict = Depends(require_admin),
|
|
161
175
|
user_id: str | None = Query(default=None, description="Filter by user ID"),
|
|
@@ -202,7 +216,13 @@ async def list_all_sessions(
|
|
|
202
216
|
return SessionListResponse(data=sessions, total=total, has_more=has_more)
|
|
203
217
|
|
|
204
218
|
|
|
205
|
-
@router.get(
|
|
219
|
+
@router.get(
|
|
220
|
+
"/messages",
|
|
221
|
+
response_model=MessageListResponse,
|
|
222
|
+
responses={
|
|
223
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
224
|
+
},
|
|
225
|
+
)
|
|
206
226
|
async def list_all_messages(
|
|
207
227
|
user: dict = Depends(require_admin),
|
|
208
228
|
user_id: str | None = Query(default=None, description="Filter by user ID"),
|
|
@@ -252,7 +272,13 @@ async def list_all_messages(
|
|
|
252
272
|
return MessageListResponse(data=messages, total=total, has_more=has_more)
|
|
253
273
|
|
|
254
274
|
|
|
255
|
-
@router.get(
|
|
275
|
+
@router.get(
|
|
276
|
+
"/stats",
|
|
277
|
+
response_model=SystemStats,
|
|
278
|
+
responses={
|
|
279
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
280
|
+
},
|
|
281
|
+
)
|
|
256
282
|
async def get_system_stats(
|
|
257
283
|
user: dict = Depends(require_admin),
|
|
258
284
|
) -> SystemStats:
|
rem/api/routers/auth.py
CHANGED
|
@@ -30,14 +30,17 @@ Access Control Flow (send-code):
|
|
|
30
30
|
│ ├── Yes → Check user.tier
|
|
31
31
|
│ │ ├── tier == BLOCKED → Reject "Account is blocked"
|
|
32
32
|
│ │ └── tier != BLOCKED → Allow (send code, existing users grandfathered)
|
|
33
|
-
│ └── No (new user) → Check
|
|
34
|
-
│ ├──
|
|
35
|
-
│
|
|
36
|
-
│
|
|
37
|
-
│
|
|
33
|
+
│ └── No (new user) → Check subscriber list first
|
|
34
|
+
│ ├── Email in subscribers table? → Allow (create user & send code)
|
|
35
|
+
│ └── Not a subscriber → Check EMAIL__TRUSTED_EMAIL_DOMAINS
|
|
36
|
+
│ ├── Setting configured → domain in trusted list?
|
|
37
|
+
│ │ ├── Yes → Create user & send code
|
|
38
|
+
│ │ └── No → Reject "Email domain not allowed for signup"
|
|
39
|
+
│ └── Not configured (empty) → Create user & send code (no restrictions)
|
|
38
40
|
|
|
39
41
|
Key Behaviors:
|
|
40
42
|
- Existing users: Always allowed to login (unless tier=BLOCKED)
|
|
43
|
+
- Subscribers: Always allowed to login (regardless of email domain)
|
|
41
44
|
- New users: Must have email from trusted domain (if EMAIL__TRUSTED_EMAIL_DOMAINS is set)
|
|
42
45
|
- No restrictions: Leave EMAIL__TRUSTED_EMAIL_DOMAINS empty to allow all domains
|
|
43
46
|
|
|
@@ -98,6 +101,8 @@ from authlib.integrations.starlette_client import OAuth
|
|
|
98
101
|
from pydantic import BaseModel, EmailStr
|
|
99
102
|
from loguru import logger
|
|
100
103
|
|
|
104
|
+
from .common import ErrorResponse
|
|
105
|
+
|
|
101
106
|
from ...settings import settings
|
|
102
107
|
from ...services.postgres.service import PostgresService
|
|
103
108
|
from ...services.user_service import UserService
|
|
@@ -156,7 +161,14 @@ class EmailVerifyRequest(BaseModel):
|
|
|
156
161
|
code: str
|
|
157
162
|
|
|
158
163
|
|
|
159
|
-
@router.post(
|
|
164
|
+
@router.post(
|
|
165
|
+
"/email/send-code",
|
|
166
|
+
responses={
|
|
167
|
+
400: {"model": ErrorResponse, "description": "Invalid request or email rejected"},
|
|
168
|
+
500: {"model": ErrorResponse, "description": "Failed to send login code"},
|
|
169
|
+
501: {"model": ErrorResponse, "description": "Email auth or database not configured"},
|
|
170
|
+
},
|
|
171
|
+
)
|
|
160
172
|
async def send_email_code(request: Request, body: EmailSendCodeRequest):
|
|
161
173
|
"""
|
|
162
174
|
Send a login code to an email address.
|
|
@@ -218,7 +230,14 @@ async def send_email_code(request: Request, body: EmailSendCodeRequest):
|
|
|
218
230
|
await db.disconnect()
|
|
219
231
|
|
|
220
232
|
|
|
221
|
-
@router.post(
|
|
233
|
+
@router.post(
|
|
234
|
+
"/email/verify",
|
|
235
|
+
responses={
|
|
236
|
+
400: {"model": ErrorResponse, "description": "Invalid or expired code"},
|
|
237
|
+
500: {"model": ErrorResponse, "description": "Failed to verify login code"},
|
|
238
|
+
501: {"model": ErrorResponse, "description": "Email auth or database not configured"},
|
|
239
|
+
},
|
|
240
|
+
)
|
|
222
241
|
async def verify_email_code(request: Request, body: EmailVerifyRequest):
|
|
223
242
|
"""
|
|
224
243
|
Verify login code and create session with JWT tokens.
|
|
@@ -316,7 +335,13 @@ async def verify_email_code(request: Request, body: EmailVerifyRequest):
|
|
|
316
335
|
# =============================================================================
|
|
317
336
|
|
|
318
337
|
|
|
319
|
-
@router.get(
|
|
338
|
+
@router.get(
|
|
339
|
+
"/{provider}/login",
|
|
340
|
+
responses={
|
|
341
|
+
400: {"model": ErrorResponse, "description": "Unknown OAuth provider"},
|
|
342
|
+
501: {"model": ErrorResponse, "description": "Authentication is disabled"},
|
|
343
|
+
},
|
|
344
|
+
)
|
|
320
345
|
async def login(provider: str, request: Request):
|
|
321
346
|
"""
|
|
322
347
|
Initiate OAuth flow with provider.
|
|
@@ -358,7 +383,13 @@ async def login(provider: str, request: Request):
|
|
|
358
383
|
return await client.authorize_redirect(request, redirect_uri)
|
|
359
384
|
|
|
360
385
|
|
|
361
|
-
@router.get(
|
|
386
|
+
@router.get(
|
|
387
|
+
"/{provider}/callback",
|
|
388
|
+
responses={
|
|
389
|
+
400: {"model": ErrorResponse, "description": "Authentication failed or unknown provider"},
|
|
390
|
+
501: {"model": ErrorResponse, "description": "Authentication is disabled"},
|
|
391
|
+
},
|
|
392
|
+
)
|
|
362
393
|
async def callback(provider: str, request: Request):
|
|
363
394
|
"""
|
|
364
395
|
OAuth callback endpoint.
|
|
@@ -495,7 +526,12 @@ async def logout(request: Request):
|
|
|
495
526
|
return {"message": "Logged out successfully"}
|
|
496
527
|
|
|
497
528
|
|
|
498
|
-
@router.get(
|
|
529
|
+
@router.get(
|
|
530
|
+
"/me",
|
|
531
|
+
responses={
|
|
532
|
+
401: {"model": ErrorResponse, "description": "Not authenticated"},
|
|
533
|
+
},
|
|
534
|
+
)
|
|
499
535
|
async def me(request: Request):
|
|
500
536
|
"""
|
|
501
537
|
Get current user information from session or JWT.
|
|
@@ -533,11 +569,19 @@ class TokenRefreshRequest(BaseModel):
|
|
|
533
569
|
refresh_token: str
|
|
534
570
|
|
|
535
571
|
|
|
536
|
-
@router.post(
|
|
572
|
+
@router.post(
|
|
573
|
+
"/token/refresh",
|
|
574
|
+
responses={
|
|
575
|
+
401: {"model": ErrorResponse, "description": "Invalid or expired refresh token"},
|
|
576
|
+
},
|
|
577
|
+
)
|
|
537
578
|
async def refresh_token(body: TokenRefreshRequest):
|
|
538
579
|
"""
|
|
539
580
|
Refresh access token using refresh token.
|
|
540
581
|
|
|
582
|
+
Fetches the user's current role/tier from the database to ensure
|
|
583
|
+
the new access token reflects their actual permissions.
|
|
584
|
+
|
|
541
585
|
Args:
|
|
542
586
|
body: TokenRefreshRequest with refresh_token
|
|
543
587
|
|
|
@@ -545,7 +589,46 @@ async def refresh_token(body: TokenRefreshRequest):
|
|
|
545
589
|
New access token or 401 if refresh token is invalid
|
|
546
590
|
"""
|
|
547
591
|
jwt_service = get_jwt_service()
|
|
548
|
-
|
|
592
|
+
|
|
593
|
+
# First decode the refresh token to get user_id (without full verification yet)
|
|
594
|
+
payload = jwt_service.decode_without_verification(body.refresh_token)
|
|
595
|
+
if not payload:
|
|
596
|
+
raise HTTPException(
|
|
597
|
+
status_code=401,
|
|
598
|
+
detail="Invalid refresh token format"
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
user_id = payload.get("sub")
|
|
602
|
+
if not user_id:
|
|
603
|
+
raise HTTPException(
|
|
604
|
+
status_code=401,
|
|
605
|
+
detail="Invalid refresh token: missing user ID"
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# Fetch user from database to get current role/tier
|
|
609
|
+
user_override = None
|
|
610
|
+
if settings.postgres.enabled:
|
|
611
|
+
db = PostgresService()
|
|
612
|
+
try:
|
|
613
|
+
await db.connect()
|
|
614
|
+
user_service = UserService(db)
|
|
615
|
+
user_entity = await user_service.get_user_by_id(user_id)
|
|
616
|
+
if user_entity:
|
|
617
|
+
user_override = {
|
|
618
|
+
"role": user_entity.role or "user",
|
|
619
|
+
"roles": [user_entity.role] if user_entity.role else ["user"],
|
|
620
|
+
"tier": user_entity.tier.value if user_entity.tier else "free",
|
|
621
|
+
"name": user_entity.name,
|
|
622
|
+
}
|
|
623
|
+
logger.debug(f"Refresh token: fetched user {user_id} with role={user_override['role']}, tier={user_override['tier']}")
|
|
624
|
+
except Exception as e:
|
|
625
|
+
logger.warning(f"Could not fetch user for token refresh: {e}")
|
|
626
|
+
# Continue without override - will use defaults
|
|
627
|
+
finally:
|
|
628
|
+
await db.disconnect()
|
|
629
|
+
|
|
630
|
+
# Now do the actual refresh with proper verification
|
|
631
|
+
result = jwt_service.refresh_access_token(body.refresh_token, user_override=user_override)
|
|
549
632
|
|
|
550
633
|
if not result:
|
|
551
634
|
raise HTTPException(
|
|
@@ -556,7 +639,12 @@ async def refresh_token(body: TokenRefreshRequest):
|
|
|
556
639
|
return result
|
|
557
640
|
|
|
558
641
|
|
|
559
|
-
@router.post(
|
|
642
|
+
@router.post(
|
|
643
|
+
"/token/verify",
|
|
644
|
+
responses={
|
|
645
|
+
401: {"model": ErrorResponse, "description": "Missing, invalid, or expired token"},
|
|
646
|
+
},
|
|
647
|
+
)
|
|
560
648
|
async def verify_token(request: Request):
|
|
561
649
|
"""
|
|
562
650
|
Verify an access token is valid.
|
|
@@ -620,7 +708,12 @@ def verify_dev_token(token: str) -> bool:
|
|
|
620
708
|
return token == expected
|
|
621
709
|
|
|
622
710
|
|
|
623
|
-
@router.get(
|
|
711
|
+
@router.get(
|
|
712
|
+
"/dev/token",
|
|
713
|
+
responses={
|
|
714
|
+
401: {"model": ErrorResponse, "description": "Dev tokens not available in production"},
|
|
715
|
+
},
|
|
716
|
+
)
|
|
624
717
|
async def get_dev_token(request: Request):
|
|
625
718
|
"""
|
|
626
719
|
Get a development token for testing (non-production only).
|
|
@@ -656,7 +749,13 @@ async def get_dev_token(request: Request):
|
|
|
656
749
|
}
|
|
657
750
|
|
|
658
751
|
|
|
659
|
-
@router.get(
|
|
752
|
+
@router.get(
|
|
753
|
+
"/dev/mock-code/{email}",
|
|
754
|
+
responses={
|
|
755
|
+
401: {"model": ErrorResponse, "description": "Mock codes not available in production"},
|
|
756
|
+
404: {"model": ErrorResponse, "description": "No code found for email"},
|
|
757
|
+
},
|
|
758
|
+
)
|
|
660
759
|
async def get_mock_code(email: str, request: Request):
|
|
661
760
|
"""
|
|
662
761
|
Get the mock login code for testing (non-production only).
|
|
@@ -215,7 +215,7 @@ async def ensure_session_with_metadata(
|
|
|
215
215
|
Merges request metadata with existing session metadata.
|
|
216
216
|
|
|
217
217
|
Args:
|
|
218
|
-
session_id: Session
|
|
218
|
+
session_id: Session UUID from X-Session-Id header
|
|
219
219
|
user_id: User identifier
|
|
220
220
|
tenant_id: Tenant identifier
|
|
221
221
|
is_eval: Whether this is an evaluation session
|
|
@@ -228,12 +228,8 @@ async def ensure_session_with_metadata(
|
|
|
228
228
|
try:
|
|
229
229
|
repo = Repository(Session, table_name="sessions")
|
|
230
230
|
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
filters={"name": session_id, "tenant_id": tenant_id},
|
|
234
|
-
limit=1,
|
|
235
|
-
)
|
|
236
|
-
existing = existing_list[0] if existing_list else None
|
|
231
|
+
# Look up session by UUID (id field)
|
|
232
|
+
existing = await repo.get_by_id(session_id)
|
|
237
233
|
|
|
238
234
|
if existing:
|
|
239
235
|
# Merge metadata if provided
|
|
@@ -254,9 +250,10 @@ async def ensure_session_with_metadata(
|
|
|
254
250
|
await repo.upsert(existing)
|
|
255
251
|
logger.debug(f"Updated session {session_id} (eval={is_eval}, metadata keys={list(merged_metadata.keys())})")
|
|
256
252
|
else:
|
|
257
|
-
# Create new session
|
|
253
|
+
# Create new session with the provided UUID as the id
|
|
258
254
|
session = Session(
|
|
259
|
-
|
|
255
|
+
id=session_id, # Use the provided UUID as session id
|
|
256
|
+
name=session_id, # Default name to UUID, can be updated later with LLM-generated name
|
|
260
257
|
mode=SessionMode.EVALUATION if is_eval else SessionMode.NORMAL,
|
|
261
258
|
user_id=user_id,
|
|
262
259
|
tenant_id=tenant_id,
|
|
@@ -503,16 +500,51 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
|
|
|
503
500
|
logger.error(f"Failed to transcribe audio: {e}")
|
|
504
501
|
# Fall through with original content (will likely fail at agent)
|
|
505
502
|
|
|
506
|
-
# Use ContextBuilder to construct
|
|
507
|
-
#
|
|
508
|
-
# 2. Session history (if session_id provided)
|
|
509
|
-
# 3. New messages from request body (transcribed if audio)
|
|
503
|
+
# Use ContextBuilder to construct context and basic messages
|
|
504
|
+
# Note: We load session history separately for proper pydantic-ai message_history
|
|
510
505
|
context, messages = await ContextBuilder.build_from_headers(
|
|
511
506
|
headers=dict(request.headers),
|
|
512
507
|
new_messages=new_messages,
|
|
513
508
|
user_id=temp_context.user_id, # From JWT token (source of truth)
|
|
514
509
|
)
|
|
515
510
|
|
|
511
|
+
# Load raw session history for proper pydantic-ai message_history format
|
|
512
|
+
# This enables proper tool call/return pairing for LLM API compatibility
|
|
513
|
+
from ....services.session import SessionMessageStore, session_to_pydantic_messages, audit_session_history
|
|
514
|
+
from ....agentic.schema import get_system_prompt
|
|
515
|
+
|
|
516
|
+
pydantic_message_history = None
|
|
517
|
+
if context.session_id and settings.postgres.enabled:
|
|
518
|
+
try:
|
|
519
|
+
store = SessionMessageStore(user_id=context.user_id or settings.test.effective_user_id)
|
|
520
|
+
raw_session_history = await store.load_session_messages(
|
|
521
|
+
session_id=context.session_id,
|
|
522
|
+
user_id=context.user_id,
|
|
523
|
+
compress_on_load=False, # Don't compress - we need full data for reconstruction
|
|
524
|
+
)
|
|
525
|
+
if raw_session_history:
|
|
526
|
+
# CRITICAL: Extract and pass the agent's system prompt
|
|
527
|
+
# pydantic-ai only auto-adds system prompts when message_history is empty
|
|
528
|
+
# When we pass message_history, we must include the system prompt ourselves
|
|
529
|
+
agent_system_prompt = get_system_prompt(agent_schema) if agent_schema else None
|
|
530
|
+
pydantic_message_history = session_to_pydantic_messages(
|
|
531
|
+
raw_session_history,
|
|
532
|
+
system_prompt=agent_system_prompt,
|
|
533
|
+
)
|
|
534
|
+
logger.debug(f"Converted {len(raw_session_history)} session messages to {len(pydantic_message_history)} pydantic-ai messages (with system prompt)")
|
|
535
|
+
|
|
536
|
+
# Audit session history if enabled (for debugging)
|
|
537
|
+
audit_session_history(
|
|
538
|
+
session_id=context.session_id,
|
|
539
|
+
agent_name=schema_name or "default",
|
|
540
|
+
prompt=body.messages[-1].content if body.messages else "",
|
|
541
|
+
raw_session_history=raw_session_history,
|
|
542
|
+
pydantic_messages_count=len(pydantic_message_history),
|
|
543
|
+
)
|
|
544
|
+
except Exception as e:
|
|
545
|
+
logger.warning(f"Failed to load session history for message_history: {e}")
|
|
546
|
+
# Fall back to old behavior (concatenated prompt)
|
|
547
|
+
|
|
516
548
|
logger.info(f"Built context with {len(messages)} total messages (includes history + user context)")
|
|
517
549
|
|
|
518
550
|
# Ensure session exists with metadata and eval mode if applicable
|
|
@@ -533,9 +565,17 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
|
|
|
533
565
|
model_override=body.model, # type: ignore[arg-type]
|
|
534
566
|
)
|
|
535
567
|
|
|
536
|
-
#
|
|
537
|
-
#
|
|
538
|
-
|
|
568
|
+
# Build the prompt for the agent
|
|
569
|
+
# If we have proper message_history, use just the latest user message as prompt
|
|
570
|
+
# Otherwise, fall back to concatenating all messages (legacy behavior)
|
|
571
|
+
if pydantic_message_history:
|
|
572
|
+
# Use the latest user message as the prompt, with history passed separately
|
|
573
|
+
user_prompt = body.messages[-1].content if body.messages else ""
|
|
574
|
+
prompt = user_prompt
|
|
575
|
+
logger.debug(f"Using message_history with {len(pydantic_message_history)} messages")
|
|
576
|
+
else:
|
|
577
|
+
# Legacy: Combine all messages into single prompt for agent
|
|
578
|
+
prompt = "\n".join(msg.content for msg in messages)
|
|
539
579
|
|
|
540
580
|
# Generate OpenAI-compatible request ID
|
|
541
581
|
request_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
|
|
@@ -570,6 +610,8 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
|
|
|
570
610
|
agent_schema=schema_name,
|
|
571
611
|
session_id=context.session_id,
|
|
572
612
|
user_id=context.user_id,
|
|
613
|
+
agent_context=context, # Pass context for multi-agent support
|
|
614
|
+
message_history=pydantic_message_history, # Native pydantic-ai message history
|
|
573
615
|
),
|
|
574
616
|
media_type="text/event-stream",
|
|
575
617
|
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
@@ -592,10 +634,16 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
|
|
|
592
634
|
) as span:
|
|
593
635
|
# Capture trace context from the span we just created
|
|
594
636
|
trace_id, span_id = get_current_trace_context()
|
|
595
|
-
|
|
637
|
+
if pydantic_message_history:
|
|
638
|
+
result = await agent.run(prompt, message_history=pydantic_message_history)
|
|
639
|
+
else:
|
|
640
|
+
result = await agent.run(prompt)
|
|
596
641
|
else:
|
|
597
642
|
# No tracer available, run without tracing
|
|
598
|
-
|
|
643
|
+
if pydantic_message_history:
|
|
644
|
+
result = await agent.run(prompt, message_history=pydantic_message_history)
|
|
645
|
+
else:
|
|
646
|
+
result = await agent.run(prompt)
|
|
599
647
|
|
|
600
648
|
# Determine content format based on response_format request
|
|
601
649
|
if body.response_format and body.response_format.type == "json_object":
|
|
@@ -321,7 +321,11 @@ class MetadataEvent(BaseModel):
|
|
|
321
321
|
# Agent info
|
|
322
322
|
agent_schema: str | None = Field(
|
|
323
323
|
default=None,
|
|
324
|
-
description="Name of the agent schema
|
|
324
|
+
description="Name of the top-level agent schema (e.g., 'siggy', 'rem')"
|
|
325
|
+
)
|
|
326
|
+
responding_agent: str | None = Field(
|
|
327
|
+
default=None,
|
|
328
|
+
description="Name of the agent that produced this response (may differ from agent_schema if delegated via ask_agent)"
|
|
325
329
|
)
|
|
326
330
|
|
|
327
331
|
# Session info
|
|
@@ -409,9 +413,9 @@ class ToolCallEvent(BaseModel):
|
|
|
409
413
|
default=None,
|
|
410
414
|
description="Tool arguments (for 'started' status)"
|
|
411
415
|
)
|
|
412
|
-
result: str | None = Field(
|
|
416
|
+
result: str | dict[str, Any] | None = Field(
|
|
413
417
|
default=None,
|
|
414
|
-
description="Tool result summary
|
|
418
|
+
description="Tool result - full dict for finalize_intake, summary string for others"
|
|
415
419
|
)
|
|
416
420
|
error: str | None = Field(
|
|
417
421
|
default=None,
|