mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
feat: enhance task management and timeout configurations in multi-agent chat
- Added new environment variables for controlling task execution limits, including `SURFSENSE_SUBAGENT_INVOKE_TIMEOUT_SECONDS`, `SURFSENSE_TASK_BATCH_CONCURRENCY`, and `SURFSENSE_TASK_BATCH_MAX_SIZE`. - Updated documentation to reflect new batch processing capabilities for `task` calls, allowing for concurrent execution of multiple subagent tasks. - Improved error handling and receipt generation for deliverables, ensuring consistent feedback on task status. - Refactored middleware to incorporate search space ID for better task management.
This commit is contained in:
parent
820f541f08
commit
9d6e9b7e2d
66 changed files with 2561 additions and 380 deletions
|
|
@ -6,6 +6,9 @@ import json
|
|||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.messages import ToolMessage
|
||||
from langgraph.types import Command
|
||||
|
||||
from app.tasks.chat.streaming.handlers.tools import (
|
||||
ToolCompletionEmissionContext,
|
||||
iter_tool_completion_emission_frames,
|
||||
|
|
@ -19,6 +22,38 @@ from app.tasks.chat.streaming.relay.task_span import (
|
|||
from app.tasks.chat.streaming.relay.thinking_step_sse import emit_thinking_step_frame
|
||||
|
||||
|
||||
def _unwrap_command_output(raw_output: Any) -> Any:
|
||||
"""Replace a ``Command`` from a tool return with its inner ``ToolMessage``.
|
||||
|
||||
Tools that participate in receipt-style state writes (see
|
||||
``app.agents.shared.receipt_command.with_receipt``) return a
|
||||
``Command(update={"messages": [ToolMessage(...)], "receipts": [...]})``.
|
||||
LangChain's ``on_tool_end`` event surfaces that ``Command`` verbatim as
|
||||
``data.output``, which the rest of this handler can't introspect: it has
|
||||
no ``.content``, isn't a ``dict``, and stringifies to ``"Command(...)"``.
|
||||
That stringified payload reaches the frontend and breaks tool-specific
|
||||
UI components (e.g. the podcast card) that look for ``status`` /
|
||||
``podcast_id`` at the top level.
|
||||
|
||||
We extract the first ``ToolMessage`` from the Command's ``messages`` list
|
||||
so downstream code can read ``.content`` normally. Commands that don't
|
||||
contain a ``ToolMessage`` (rare, e.g. pure state updates) are returned
|
||||
unchanged — the existing ``str(raw_output)`` fallback handles them.
|
||||
"""
|
||||
if not isinstance(raw_output, Command):
|
||||
return raw_output
|
||||
update = raw_output.update
|
||||
if not isinstance(update, dict):
|
||||
return raw_output
|
||||
messages = update.get("messages")
|
||||
if not isinstance(messages, list):
|
||||
return raw_output
|
||||
for msg in messages:
|
||||
if isinstance(msg, ToolMessage):
|
||||
return msg
|
||||
return raw_output
|
||||
|
||||
|
||||
def iter_tool_end_frames(
|
||||
event: dict[str, Any],
|
||||
*,
|
||||
|
|
@ -33,7 +68,7 @@ def iter_tool_end_frames(
|
|||
state.active_tool_depth = max(0, state.active_tool_depth - 1)
|
||||
run_id = event.get("run_id", "")
|
||||
tool_name = event.get("name", "unknown_tool")
|
||||
raw_output = event.get("data", {}).get("output", "")
|
||||
raw_output = _unwrap_command_output(event.get("data", {}).get("output", ""))
|
||||
staged_file_path = state.file_path_by_run.pop(run_id, None) if run_id else None
|
||||
|
||||
if hasattr(raw_output, "content"):
|
||||
|
|
|
|||
|
|
@ -15,12 +15,24 @@ def iter_completion_emission_frames(
|
|||
out = ctx.tool_output
|
||||
payload = out if isinstance(out, dict) else {"result": out}
|
||||
yield ctx.emit_tool_output_card(payload)
|
||||
if isinstance(out, dict) and out.get("status") == "pending":
|
||||
if not isinstance(out, dict):
|
||||
return
|
||||
status = out.get("status")
|
||||
# ``ready`` is the live success status now that the tool waits for the
|
||||
# Celery worker to reach a terminal state. ``pending`` is retained as a
|
||||
# legacy branch for old saved chats that pre-date the wait-for-terminal
|
||||
# change (see ``app.agents.shared.deliverable_wait``).
|
||||
if status == "ready":
|
||||
yield ctx.streaming_service.format_terminal_info(
|
||||
f"Video presentation generated successfully: {out.get('title', 'Presentation')}",
|
||||
"success",
|
||||
)
|
||||
elif status == "pending":
|
||||
yield ctx.streaming_service.format_terminal_info(
|
||||
f"Video presentation queued: {out.get('title', 'Presentation')}",
|
||||
"success",
|
||||
)
|
||||
elif isinstance(out, dict) and out.get("status") == "failed":
|
||||
elif status == "failed":
|
||||
error_msg = out.get("error", "Unknown error")
|
||||
yield ctx.streaming_service.format_terminal_info(
|
||||
f"Presentation generation failed: {error_msg}",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue