remdb 0.3.202__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 +36 -2
- rem/agentic/context.py +86 -3
- rem/agentic/context_builder.py +39 -33
- rem/agentic/mcp/tool_wrapper.py +2 -2
- rem/agentic/providers/pydantic_ai.py +68 -51
- rem/agentic/schema.py +2 -2
- rem/api/mcp_router/resources.py +223 -0
- rem/api/mcp_router/tools.py +170 -18
- rem/api/routers/admin.py +30 -4
- rem/api/routers/auth.py +175 -18
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +24 -29
- rem/api/routers/chat/sse_events.py +5 -1
- rem/api/routers/chat/streaming.py +242 -272
- 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 +80 -15
- rem/api/routers/models.py +9 -1
- rem/api/routers/query.py +17 -15
- rem/api/routers/shared_sessions.py +16 -0
- rem/cli/commands/ask.py +205 -114
- rem/cli/commands/process.py +12 -4
- 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/schemas/agents/rem.yaml +1 -1
- rem/services/postgres/repository.py +7 -7
- rem/services/rem/service.py +47 -0
- rem/services/session/__init__.py +2 -1
- rem/services/session/compression.py +14 -12
- rem/services/session/pydantic_messages.py +111 -11
- rem/services/session/reload.py +2 -1
- rem/settings.py +71 -0
- rem/sql/migrations/001_install.sql +4 -4
- rem/sql/migrations/004_cache_system.sql +3 -1
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/schema_loader.py +139 -111
- {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/METADATA +2 -2
- {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/RECORD +44 -39
- {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/WHEEL +0 -0
- {remdb-0.3.202.dist-info → remdb-0.3.245.dist-info}/entry_points.txt +0 -0
rem/api/mcp_router/resources.py
CHANGED
|
@@ -542,6 +542,227 @@ def register_status_resources(mcp: FastMCP):
|
|
|
542
542
|
"""
|
|
543
543
|
|
|
544
544
|
|
|
545
|
+
def register_session_resources(mcp: FastMCP):
|
|
546
|
+
"""
|
|
547
|
+
Register session resources for loading conversation history.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
mcp: FastMCP server instance
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
@mcp.resource("rem://sessions/{session_id}")
|
|
554
|
+
async def get_session_messages(session_id: str) -> str:
|
|
555
|
+
"""
|
|
556
|
+
Load a conversation session by ID.
|
|
557
|
+
|
|
558
|
+
Returns the full message history including user messages, assistant responses,
|
|
559
|
+
and tool calls. Useful for evaluators and analysis agents.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
session_id: Session UUID or identifier
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Formatted conversation history as markdown string with:
|
|
566
|
+
- Message type (user/assistant/tool)
|
|
567
|
+
- Content
|
|
568
|
+
- Timestamps
|
|
569
|
+
- Tool call details (if any)
|
|
570
|
+
"""
|
|
571
|
+
from ...services.postgres import get_postgres_service
|
|
572
|
+
|
|
573
|
+
pg = get_postgres_service()
|
|
574
|
+
await pg.connect()
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
# Query messages for session
|
|
578
|
+
query = """
|
|
579
|
+
SELECT id, message_type, content, metadata, created_at
|
|
580
|
+
FROM messages
|
|
581
|
+
WHERE session_id = $1
|
|
582
|
+
ORDER BY created_at ASC
|
|
583
|
+
"""
|
|
584
|
+
messages = await pg.fetch(query, session_id)
|
|
585
|
+
|
|
586
|
+
if not messages:
|
|
587
|
+
return f"# Session Not Found\n\nNo messages found for session_id: {session_id}"
|
|
588
|
+
|
|
589
|
+
# Format output
|
|
590
|
+
output = [f"# Session: {session_id}\n"]
|
|
591
|
+
output.append(f"**Total messages:** {len(messages)}\n")
|
|
592
|
+
|
|
593
|
+
for i, msg in enumerate(messages, 1):
|
|
594
|
+
msg_type = msg['message_type']
|
|
595
|
+
content = msg['content'] or "(empty)"
|
|
596
|
+
created = msg['created_at']
|
|
597
|
+
metadata = msg.get('metadata') or {}
|
|
598
|
+
|
|
599
|
+
# Format based on message type
|
|
600
|
+
if msg_type == 'user':
|
|
601
|
+
output.append(f"\n## [{i}] USER ({created})")
|
|
602
|
+
output.append(f"```\n{content[:1000]}{'...' if len(content) > 1000 else ''}\n```")
|
|
603
|
+
elif msg_type == 'assistant':
|
|
604
|
+
output.append(f"\n## [{i}] ASSISTANT ({created})")
|
|
605
|
+
output.append(f"```\n{content[:1000]}{'...' if len(content) > 1000 else ''}\n```")
|
|
606
|
+
elif msg_type == 'tool':
|
|
607
|
+
tool_name = metadata.get('tool_name', 'unknown')
|
|
608
|
+
output.append(f"\n## [{i}] TOOL: {tool_name} ({created})")
|
|
609
|
+
# Truncate tool results more aggressively
|
|
610
|
+
output.append(f"```json\n{content[:500]}{'...' if len(content) > 500 else ''}\n```")
|
|
611
|
+
else:
|
|
612
|
+
output.append(f"\n## [{i}] {msg_type.upper()} ({created})")
|
|
613
|
+
output.append(f"```\n{content[:500]}{'...' if len(content) > 500 else ''}\n```")
|
|
614
|
+
|
|
615
|
+
return "\n".join(output)
|
|
616
|
+
|
|
617
|
+
finally:
|
|
618
|
+
await pg.disconnect()
|
|
619
|
+
|
|
620
|
+
@mcp.resource("rem://sessions")
|
|
621
|
+
async def list_recent_sessions() -> str:
|
|
622
|
+
"""
|
|
623
|
+
List recent sessions with basic info.
|
|
624
|
+
|
|
625
|
+
Returns the most recent 20 sessions with:
|
|
626
|
+
- Session ID
|
|
627
|
+
- First user message (preview)
|
|
628
|
+
- Message count
|
|
629
|
+
- Timestamp
|
|
630
|
+
"""
|
|
631
|
+
from ...services.postgres import get_postgres_service
|
|
632
|
+
|
|
633
|
+
pg = get_postgres_service()
|
|
634
|
+
await pg.connect()
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
# Query recent sessions
|
|
638
|
+
query = """
|
|
639
|
+
SELECT
|
|
640
|
+
session_id,
|
|
641
|
+
MIN(created_at) as started_at,
|
|
642
|
+
COUNT(*) as message_count,
|
|
643
|
+
MIN(CASE WHEN message_type = 'user' THEN content END) as first_message
|
|
644
|
+
FROM messages
|
|
645
|
+
WHERE session_id IS NOT NULL
|
|
646
|
+
GROUP BY session_id
|
|
647
|
+
ORDER BY MIN(created_at) DESC
|
|
648
|
+
LIMIT 20
|
|
649
|
+
"""
|
|
650
|
+
sessions = await pg.fetch(query)
|
|
651
|
+
|
|
652
|
+
if not sessions:
|
|
653
|
+
return "# Recent Sessions\n\nNo sessions found."
|
|
654
|
+
|
|
655
|
+
output = ["# Recent Sessions\n"]
|
|
656
|
+
output.append(f"Showing {len(sessions)} most recent sessions:\n")
|
|
657
|
+
|
|
658
|
+
for session in sessions:
|
|
659
|
+
session_id = session['session_id']
|
|
660
|
+
started = session['started_at']
|
|
661
|
+
count = session['message_count']
|
|
662
|
+
first_msg = session['first_message'] or "(no user message)"
|
|
663
|
+
preview = first_msg[:80] + "..." if len(first_msg) > 80 else first_msg
|
|
664
|
+
|
|
665
|
+
output.append(f"\n## {session_id}")
|
|
666
|
+
output.append(f"- **Started:** {started}")
|
|
667
|
+
output.append(f"- **Messages:** {count}")
|
|
668
|
+
output.append(f"- **First message:** {preview}")
|
|
669
|
+
output.append(f"- **Load:** `rem://sessions/{session_id}`")
|
|
670
|
+
|
|
671
|
+
return "\n".join(output)
|
|
672
|
+
|
|
673
|
+
finally:
|
|
674
|
+
await pg.disconnect()
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def register_user_resources(mcp: FastMCP):
|
|
678
|
+
"""
|
|
679
|
+
Register user profile resources for on-demand profile loading.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
mcp: FastMCP server instance
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
@mcp.resource("user://profile/{user_id}")
|
|
686
|
+
async def get_user_profile(user_id: str) -> str:
|
|
687
|
+
"""
|
|
688
|
+
Load a user's profile by ID.
|
|
689
|
+
|
|
690
|
+
Returns the user's profile information including:
|
|
691
|
+
- Email and name
|
|
692
|
+
- Summary (AI-generated profile summary)
|
|
693
|
+
- Interests and preferred topics
|
|
694
|
+
- Activity level
|
|
695
|
+
|
|
696
|
+
This resource is protected - each user can only access their own profile.
|
|
697
|
+
The user_id should match the authenticated user's ID from the JWT token.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
user_id: User UUID from authentication
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
Formatted user profile as markdown string, or error if not found
|
|
704
|
+
"""
|
|
705
|
+
from ...services.postgres import get_postgres_service
|
|
706
|
+
from ...services.postgres.repository import Repository
|
|
707
|
+
from ...models.entities.user import User
|
|
708
|
+
|
|
709
|
+
pg = get_postgres_service()
|
|
710
|
+
await pg.connect()
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
user_repo = Repository(User, "users", db=pg)
|
|
714
|
+
# Look up user by ID (user_id from JWT is the primary key)
|
|
715
|
+
user = await user_repo.get_by_id(user_id, tenant_id=None)
|
|
716
|
+
|
|
717
|
+
if not user:
|
|
718
|
+
return f"# User Profile Not Found\n\nNo user found with ID: {user_id}"
|
|
719
|
+
|
|
720
|
+
# Build profile output
|
|
721
|
+
output = [f"# User Profile: {user.name or user.email or 'Unknown'}"]
|
|
722
|
+
output.append("")
|
|
723
|
+
|
|
724
|
+
if user.email:
|
|
725
|
+
output.append(f"**Email:** {user.email}")
|
|
726
|
+
|
|
727
|
+
if user.role:
|
|
728
|
+
output.append(f"**Role:** {user.role}")
|
|
729
|
+
|
|
730
|
+
if user.tier:
|
|
731
|
+
output.append(f"**Tier:** {user.tier.value if hasattr(user.tier, 'value') else user.tier}")
|
|
732
|
+
|
|
733
|
+
if user.summary:
|
|
734
|
+
output.append(f"\n## Summary\n{user.summary}")
|
|
735
|
+
|
|
736
|
+
if user.interests:
|
|
737
|
+
output.append(f"\n## Interests\n- " + "\n- ".join(user.interests[:10]))
|
|
738
|
+
|
|
739
|
+
if user.preferred_topics:
|
|
740
|
+
output.append(f"\n## Preferred Topics\n- " + "\n- ".join(user.preferred_topics[:10]))
|
|
741
|
+
|
|
742
|
+
if user.activity_level:
|
|
743
|
+
output.append(f"\n**Activity Level:** {user.activity_level}")
|
|
744
|
+
|
|
745
|
+
if user.last_active_at:
|
|
746
|
+
output.append(f"**Last Active:** {user.last_active_at}")
|
|
747
|
+
|
|
748
|
+
# Add metadata if present (but redact sensitive fields)
|
|
749
|
+
if user.metadata:
|
|
750
|
+
safe_metadata = {k: v for k, v in user.metadata.items()
|
|
751
|
+
if k not in ('login_code', 'password', 'token', 'secret')}
|
|
752
|
+
if safe_metadata:
|
|
753
|
+
output.append(f"\n## Additional Info")
|
|
754
|
+
for key, value in list(safe_metadata.items())[:5]:
|
|
755
|
+
output.append(f"- **{key}:** {value}")
|
|
756
|
+
|
|
757
|
+
return "\n".join(output)
|
|
758
|
+
|
|
759
|
+
except Exception as e:
|
|
760
|
+
return f"# Error Loading Profile\n\nFailed to load user profile: {e}"
|
|
761
|
+
|
|
762
|
+
finally:
|
|
763
|
+
await pg.disconnect()
|
|
764
|
+
|
|
765
|
+
|
|
545
766
|
# Resource dispatcher for read_resource tool
|
|
546
767
|
async def load_resource(uri: str) -> dict | str:
|
|
547
768
|
"""
|
|
@@ -571,6 +792,8 @@ async def load_resource(uri: str) -> dict | str:
|
|
|
571
792
|
register_agent_resources(mcp)
|
|
572
793
|
register_file_resources(mcp)
|
|
573
794
|
register_status_resources(mcp)
|
|
795
|
+
register_session_resources(mcp)
|
|
796
|
+
register_user_resources(mcp)
|
|
574
797
|
|
|
575
798
|
# 1. Try exact match in regular resources
|
|
576
799
|
resources = await mcp.get_resources()
|
rem/api/mcp_router/tools.py
CHANGED
|
@@ -594,15 +594,18 @@ async def read_resource(uri: str) -> dict[str, Any]:
|
|
|
594
594
|
**Available Resources:**
|
|
595
595
|
|
|
596
596
|
Agent Schemas:
|
|
597
|
-
• rem://
|
|
598
|
-
• rem://
|
|
599
|
-
|
|
597
|
+
• rem://agents - List all available agent schemas
|
|
598
|
+
• rem://agents/{agent_name} - Get specific agent schema
|
|
599
|
+
|
|
600
|
+
Documentation:
|
|
601
|
+
• rem://schema/entities - Entity schemas (Resource, Message, User, File, Moment)
|
|
602
|
+
• rem://schema/query-types - REM query type documentation
|
|
600
603
|
|
|
601
604
|
System Status:
|
|
602
605
|
• rem://status - System health and statistics
|
|
603
606
|
|
|
604
607
|
Args:
|
|
605
|
-
uri: Resource URI (e.g., "rem://
|
|
608
|
+
uri: Resource URI (e.g., "rem://agents", "rem://agents/ask_rem")
|
|
606
609
|
|
|
607
610
|
Returns:
|
|
608
611
|
Dict with:
|
|
@@ -611,14 +614,11 @@ async def read_resource(uri: str) -> dict[str, Any]:
|
|
|
611
614
|
- data: Resource data (format depends on resource type)
|
|
612
615
|
|
|
613
616
|
Examples:
|
|
614
|
-
# List all
|
|
615
|
-
read_resource(uri="rem://
|
|
616
|
-
|
|
617
|
-
# Get specific schema
|
|
618
|
-
read_resource(uri="rem://schema/ask_rem")
|
|
617
|
+
# List all agents
|
|
618
|
+
read_resource(uri="rem://agents")
|
|
619
619
|
|
|
620
|
-
# Get
|
|
621
|
-
read_resource(uri="rem://
|
|
620
|
+
# Get specific agent
|
|
621
|
+
read_resource(uri="rem://agents/ask_rem")
|
|
622
622
|
|
|
623
623
|
# Check system status
|
|
624
624
|
read_resource(uri="rem://status")
|
|
@@ -1265,7 +1265,7 @@ async def ask_agent(
|
|
|
1265
1265
|
"""
|
|
1266
1266
|
import asyncio
|
|
1267
1267
|
from ...agentic import create_agent
|
|
1268
|
-
from ...agentic.context import get_current_context, agent_context_scope
|
|
1268
|
+
from ...agentic.context import get_current_context, agent_context_scope, get_event_sink, push_event
|
|
1269
1269
|
from ...agentic.agents.agent_manager import get_agent
|
|
1270
1270
|
from ...utils.schema_loader import load_agent_schema
|
|
1271
1271
|
|
|
@@ -1342,16 +1342,162 @@ async def ask_agent(
|
|
|
1342
1342
|
if input_data:
|
|
1343
1343
|
prompt = f"{input_text}\n\nInput data: {json.dumps(input_data)}"
|
|
1344
1344
|
|
|
1345
|
+
# Load session history for the sub-agent (CRITICAL for multi-turn conversations)
|
|
1346
|
+
# Sub-agents need to see the full conversation context, not just the summary
|
|
1347
|
+
pydantic_message_history = None
|
|
1348
|
+
if child_context.session_id and settings.postgres.enabled:
|
|
1349
|
+
try:
|
|
1350
|
+
from ...services.session import SessionMessageStore, session_to_pydantic_messages
|
|
1351
|
+
from ...agentic.schema import get_system_prompt
|
|
1352
|
+
|
|
1353
|
+
store = SessionMessageStore(user_id=child_context.user_id or "default")
|
|
1354
|
+
raw_session_history = await store.load_session_messages(
|
|
1355
|
+
session_id=child_context.session_id,
|
|
1356
|
+
user_id=child_context.user_id,
|
|
1357
|
+
compress_on_load=False, # Need full data for reconstruction
|
|
1358
|
+
)
|
|
1359
|
+
if raw_session_history:
|
|
1360
|
+
# Extract agent's system prompt from schema
|
|
1361
|
+
agent_system_prompt = get_system_prompt(schema) if schema else None
|
|
1362
|
+
pydantic_message_history = session_to_pydantic_messages(
|
|
1363
|
+
raw_session_history,
|
|
1364
|
+
system_prompt=agent_system_prompt,
|
|
1365
|
+
)
|
|
1366
|
+
logger.debug(
|
|
1367
|
+
f"ask_agent '{agent_name}': loaded {len(raw_session_history)} session messages "
|
|
1368
|
+
f"-> {len(pydantic_message_history)} pydantic-ai messages"
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
# Audit session history if enabled
|
|
1372
|
+
from ...services.session import audit_session_history
|
|
1373
|
+
audit_session_history(
|
|
1374
|
+
session_id=child_context.session_id,
|
|
1375
|
+
agent_name=agent_name,
|
|
1376
|
+
prompt=prompt,
|
|
1377
|
+
raw_session_history=raw_session_history,
|
|
1378
|
+
pydantic_messages_count=len(pydantic_message_history),
|
|
1379
|
+
)
|
|
1380
|
+
except Exception as e:
|
|
1381
|
+
logger.warning(f"ask_agent '{agent_name}': failed to load session history: {e}")
|
|
1382
|
+
# Fall back to running without history
|
|
1383
|
+
|
|
1345
1384
|
# Run agent with timeout and context propagation
|
|
1346
1385
|
logger.info(f"Invoking agent '{agent_name}' with prompt: {prompt[:100]}...")
|
|
1347
1386
|
|
|
1387
|
+
# Check if we have an event sink for streaming
|
|
1388
|
+
push_event = get_event_sink()
|
|
1389
|
+
use_streaming = push_event is not None
|
|
1390
|
+
|
|
1391
|
+
streamed_content = "" # Track if content was streamed
|
|
1392
|
+
|
|
1348
1393
|
try:
|
|
1349
1394
|
# Set child context for nested tool calls
|
|
1350
1395
|
with agent_context_scope(child_context):
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1396
|
+
if use_streaming:
|
|
1397
|
+
# STREAMING MODE: Use iter() and proxy events to parent
|
|
1398
|
+
logger.debug(f"ask_agent '{agent_name}': using streaming mode with event proxying")
|
|
1399
|
+
|
|
1400
|
+
async def run_with_streaming():
|
|
1401
|
+
from pydantic_ai.messages import (
|
|
1402
|
+
PartStartEvent, PartDeltaEvent, PartEndEvent,
|
|
1403
|
+
FunctionToolResultEvent, FunctionToolCallEvent,
|
|
1404
|
+
)
|
|
1405
|
+
from pydantic_ai.agent import Agent
|
|
1406
|
+
|
|
1407
|
+
accumulated_content = []
|
|
1408
|
+
child_tool_calls = []
|
|
1409
|
+
|
|
1410
|
+
# iter() returns an async context manager, not an awaitable
|
|
1411
|
+
iter_kwargs = {"message_history": pydantic_message_history} if pydantic_message_history else {}
|
|
1412
|
+
async with agent_runtime.iter(prompt, **iter_kwargs) as agent_run:
|
|
1413
|
+
async for node in agent_run:
|
|
1414
|
+
if Agent.is_model_request_node(node):
|
|
1415
|
+
async with node.stream(agent_run.ctx) as request_stream:
|
|
1416
|
+
async for event in request_stream:
|
|
1417
|
+
# Proxy part starts (text content only - tool calls handled in is_call_tools_node)
|
|
1418
|
+
if isinstance(event, PartStartEvent):
|
|
1419
|
+
from pydantic_ai.messages import ToolCallPart, TextPart
|
|
1420
|
+
if isinstance(event.part, ToolCallPart):
|
|
1421
|
+
# Track tool call for later (args are incomplete at PartStartEvent)
|
|
1422
|
+
# Full args come via FunctionToolCallEvent in is_call_tools_node
|
|
1423
|
+
child_tool_calls.append({
|
|
1424
|
+
"tool_name": event.part.tool_name,
|
|
1425
|
+
"index": event.index,
|
|
1426
|
+
})
|
|
1427
|
+
elif isinstance(event.part, TextPart):
|
|
1428
|
+
# TextPart may have initial content
|
|
1429
|
+
if event.part.content:
|
|
1430
|
+
accumulated_content.append(event.part.content)
|
|
1431
|
+
await push_event.put({
|
|
1432
|
+
"type": "child_content",
|
|
1433
|
+
"agent_name": agent_name,
|
|
1434
|
+
"content": event.part.content,
|
|
1435
|
+
})
|
|
1436
|
+
# Proxy text content deltas to parent for real-time streaming
|
|
1437
|
+
elif isinstance(event, PartDeltaEvent):
|
|
1438
|
+
if hasattr(event, 'delta') and hasattr(event.delta, 'content_delta'):
|
|
1439
|
+
content = event.delta.content_delta
|
|
1440
|
+
if content:
|
|
1441
|
+
accumulated_content.append(content)
|
|
1442
|
+
# Push content chunk to parent for streaming
|
|
1443
|
+
await push_event.put({
|
|
1444
|
+
"type": "child_content",
|
|
1445
|
+
"agent_name": agent_name,
|
|
1446
|
+
"content": content,
|
|
1447
|
+
})
|
|
1448
|
+
|
|
1449
|
+
elif Agent.is_call_tools_node(node):
|
|
1450
|
+
async with node.stream(agent_run.ctx) as tools_stream:
|
|
1451
|
+
async for tool_event in tools_stream:
|
|
1452
|
+
# FunctionToolCallEvent fires when tool call is parsed
|
|
1453
|
+
# with complete arguments (before execution)
|
|
1454
|
+
if isinstance(tool_event, FunctionToolCallEvent):
|
|
1455
|
+
# Get full arguments from completed tool call
|
|
1456
|
+
tool_args = None
|
|
1457
|
+
if hasattr(tool_event, 'part') and hasattr(tool_event.part, 'args'):
|
|
1458
|
+
raw_args = tool_event.part.args
|
|
1459
|
+
if isinstance(raw_args, str):
|
|
1460
|
+
try:
|
|
1461
|
+
tool_args = json.loads(raw_args)
|
|
1462
|
+
except json.JSONDecodeError:
|
|
1463
|
+
tool_args = {"raw": raw_args}
|
|
1464
|
+
elif isinstance(raw_args, dict):
|
|
1465
|
+
tool_args = raw_args
|
|
1466
|
+
# Push tool start with full arguments
|
|
1467
|
+
await push_event.put({
|
|
1468
|
+
"type": "child_tool_start",
|
|
1469
|
+
"agent_name": agent_name,
|
|
1470
|
+
"tool_name": tool_event.part.tool_name if hasattr(tool_event, 'part') else "unknown",
|
|
1471
|
+
"arguments": tool_args,
|
|
1472
|
+
})
|
|
1473
|
+
elif isinstance(tool_event, FunctionToolResultEvent):
|
|
1474
|
+
result_content = tool_event.result.content if hasattr(tool_event.result, 'content') else tool_event.result
|
|
1475
|
+
# Push tool result to parent
|
|
1476
|
+
await push_event.put({
|
|
1477
|
+
"type": "child_tool_result",
|
|
1478
|
+
"agent_name": agent_name,
|
|
1479
|
+
"result": result_content,
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
# Get final result (inside context manager)
|
|
1483
|
+
return agent_run.result, "".join(accumulated_content), child_tool_calls
|
|
1484
|
+
|
|
1485
|
+
result, streamed_content, tool_calls = await asyncio.wait_for(
|
|
1486
|
+
run_with_streaming(),
|
|
1487
|
+
timeout=timeout_seconds
|
|
1488
|
+
)
|
|
1489
|
+
else:
|
|
1490
|
+
# NON-STREAMING MODE: Use run() for backwards compatibility
|
|
1491
|
+
if pydantic_message_history:
|
|
1492
|
+
result = await asyncio.wait_for(
|
|
1493
|
+
agent_runtime.run(prompt, message_history=pydantic_message_history),
|
|
1494
|
+
timeout=timeout_seconds
|
|
1495
|
+
)
|
|
1496
|
+
else:
|
|
1497
|
+
result = await asyncio.wait_for(
|
|
1498
|
+
agent_runtime.run(prompt),
|
|
1499
|
+
timeout=timeout_seconds
|
|
1500
|
+
)
|
|
1355
1501
|
except asyncio.TimeoutError:
|
|
1356
1502
|
return {
|
|
1357
1503
|
"status": "error",
|
|
@@ -1365,14 +1511,20 @@ async def ask_agent(
|
|
|
1365
1511
|
|
|
1366
1512
|
logger.info(f"Agent '{agent_name}' completed successfully")
|
|
1367
1513
|
|
|
1368
|
-
|
|
1514
|
+
response = {
|
|
1369
1515
|
"status": "success",
|
|
1370
1516
|
"output": output,
|
|
1371
|
-
"text_response": str(result.output),
|
|
1372
1517
|
"agent_schema": agent_name,
|
|
1373
1518
|
"input_text": input_text,
|
|
1374
1519
|
}
|
|
1375
1520
|
|
|
1521
|
+
# Only include text_response if content was NOT streamed
|
|
1522
|
+
# When streaming, child_content events already delivered the content
|
|
1523
|
+
if not use_streaming or not streamed_content:
|
|
1524
|
+
response["text_response"] = str(result.output)
|
|
1525
|
+
|
|
1526
|
+
return response
|
|
1527
|
+
|
|
1376
1528
|
|
|
1377
1529
|
# =============================================================================
|
|
1378
1530
|
# Test/Debug Tools (for development only)
|
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:
|