代码提交
This commit is contained in:
1 parent
f2e81d8875
commit
cebc44ff82
4 files changed
+231
-78
No files matched your search
@@ -64,3 +64,6 @@ ROUNDTABLE_JOB_GATEWAY=inprocess
|
||||
DEEPSEEK_CHAT_API_KEY=sk-686b7bc000be4b7cbed6dccb3897c411
|
||||
GLM5_FP8_API_KEY=modalresearch_gWu46FDpVLYZWPVEA3b_2fq8Jw2UJiKMgEm_Rxmn1SA
|
||||
SILICONFLOW_API_KEY=sk-ighylafootabtuthwueirmoycccyqtdgbifyhozduvolkdyt
|
||||
|
||||
# WeKnora admin credentials (DeerFlow server-side iframe SSO proxy)
|
||||
WEKNORA_ADMIN_PASSWORD=1qaz@WSX
|
||||
+157
-65
@@ -15,10 +15,10 @@ internal checkpoint callbacks that are not exposed in the Python public API.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
import asyncio
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
@@ -28,31 +28,31 @@ from langgraph.checkpoint.base import empty_checkpoint
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
from deerflow.agents.middlewares.title_middleware import (
|
||||
finalize_thread_title,
|
||||
log_title_debug,
|
||||
)
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.runtime.serialization import serialize
|
||||
from deerflow.runtime.stream_bridge import StreamBridge
|
||||
from deerflow.agents.middlewares.title_middleware import (
|
||||
finalize_thread_title,
|
||||
log_title_debug,
|
||||
)
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.runtime.serialization import serialize
|
||||
from deerflow.runtime.stream_bridge import StreamBridge
|
||||
|
||||
from .manager import RunManager, RunRecord
|
||||
from .schemas import RunStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Valid stream_mode values for LangGraph's graph.astream()
|
||||
_VALID_LG_MODES = {"values", "updates", "checkpoints", "tasks", "debug", "messages", "custom"}
|
||||
|
||||
|
||||
def _log_background_title_finalize_failure(thread_id: str):
|
||||
def _done(task: asyncio.Task[bool]) -> None:
|
||||
try:
|
||||
task.result()
|
||||
except Exception:
|
||||
logger.debug("Failed to finalize background title for thread %s", thread_id, exc_info=True)
|
||||
|
||||
return _done
|
||||
_VALID_LG_MODES = {"values", "updates", "checkpoints", "tasks", "debug", "messages", "custom"}
|
||||
|
||||
|
||||
def _log_background_title_finalize_failure(thread_id: str):
|
||||
def _done(task: asyncio.Task[bool]) -> None:
|
||||
try:
|
||||
task.result()
|
||||
except Exception:
|
||||
logger.debug("Failed to finalize background title for thread %s", thread_id, exc_info=True)
|
||||
|
||||
return _done
|
||||
|
||||
|
||||
def _build_runtime_context(
|
||||
@@ -364,7 +364,40 @@ async def run_agent(
|
||||
else:
|
||||
await run_manager.set_status(run_id, RunStatus.interrupted)
|
||||
else:
|
||||
await run_manager.set_status(run_id, RunStatus.success)
|
||||
# LLMErrorHandlingMiddleware intentionally converts an exhausted
|
||||
# provider failure into a user-safe AIMessage instead of raising.
|
||||
# That keeps a malformed upstream response out of the SSE transport,
|
||||
# but previously made this worker record the run as ``success``.
|
||||
# The UI consequently had no error event to show and looked as if
|
||||
# generation had silently stopped halfway through. Inspect the
|
||||
# terminal checkpoint before declaring success so the persisted run
|
||||
# state and the user-visible SSE outcome stay consistent.
|
||||
terminal_llm_error = await _terminal_llm_error_reason(
|
||||
checkpointer=checkpointer,
|
||||
thread_id=thread_id,
|
||||
)
|
||||
if terminal_llm_error is not None:
|
||||
error_message = "本轮模型生成未能完成,已保留当前对话内容,请稍后重试。"
|
||||
logger.warning(
|
||||
"Run %s ended with LLM fallback message (reason=%s)",
|
||||
run_id,
|
||||
terminal_llm_error,
|
||||
)
|
||||
await run_manager.set_status(
|
||||
run_id,
|
||||
RunStatus.error,
|
||||
error=error_message,
|
||||
)
|
||||
await bridge.publish(
|
||||
run_id,
|
||||
"error",
|
||||
{
|
||||
"message": error_message,
|
||||
"name": "ModelGenerationError",
|
||||
},
|
||||
)
|
||||
else:
|
||||
await run_manager.set_status(run_id, RunStatus.success)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
action = record.abort_action
|
||||
@@ -414,47 +447,47 @@ async def run_agent(
|
||||
except Exception:
|
||||
logger.warning("Failed to persist run completion for %s (non-fatal)", run_id, exc_info=True)
|
||||
|
||||
# Sync title from checkpoint to threads_meta.display_name
|
||||
if checkpointer is not None and thread_store is not None:
|
||||
try:
|
||||
ckpt_config = {"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}}
|
||||
ckpt_tuple = await checkpointer.aget_tuple(ckpt_config)
|
||||
if ckpt_tuple is not None:
|
||||
ckpt = getattr(ckpt_tuple, "checkpoint", {}) or {}
|
||||
channel_values = ckpt.get("channel_values", {}) or {}
|
||||
title = channel_values.get("title")
|
||||
if title:
|
||||
await thread_store.update_display_name(thread_id, title)
|
||||
if (
|
||||
record.status == RunStatus.success
|
||||
and channel_values.get("title_provisional") is True
|
||||
):
|
||||
log_title_debug(
|
||||
"Run %s: scheduling background title generation for thread %s",
|
||||
run_id,
|
||||
thread_id,
|
||||
)
|
||||
title_task = asyncio.create_task(
|
||||
finalize_thread_title(
|
||||
checkpointer=checkpointer,
|
||||
thread_store=thread_store,
|
||||
thread_id=thread_id,
|
||||
source_state=copy.deepcopy(channel_values),
|
||||
app_config=ctx.app_config,
|
||||
),
|
||||
name=f"title-finalize:{thread_id}",
|
||||
)
|
||||
title_task.add_done_callback(_log_background_title_finalize_failure(thread_id))
|
||||
elif record.status == RunStatus.success:
|
||||
log_title_debug(
|
||||
"Run %s: background title generation not scheduled for thread %s because title_provisional=%r title=%r",
|
||||
run_id,
|
||||
thread_id,
|
||||
channel_values.get("title_provisional"),
|
||||
channel_values.get("title"),
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Failed to sync title for thread %s (non-fatal)", thread_id)
|
||||
# Sync title from checkpoint to threads_meta.display_name
|
||||
if checkpointer is not None and thread_store is not None:
|
||||
try:
|
||||
ckpt_config = {"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}}
|
||||
ckpt_tuple = await checkpointer.aget_tuple(ckpt_config)
|
||||
if ckpt_tuple is not None:
|
||||
ckpt = getattr(ckpt_tuple, "checkpoint", {}) or {}
|
||||
channel_values = ckpt.get("channel_values", {}) or {}
|
||||
title = channel_values.get("title")
|
||||
if title:
|
||||
await thread_store.update_display_name(thread_id, title)
|
||||
if (
|
||||
record.status == RunStatus.success
|
||||
and channel_values.get("title_provisional") is True
|
||||
):
|
||||
log_title_debug(
|
||||
"Run %s: scheduling background title generation for thread %s",
|
||||
run_id,
|
||||
thread_id,
|
||||
)
|
||||
title_task = asyncio.create_task(
|
||||
finalize_thread_title(
|
||||
checkpointer=checkpointer,
|
||||
thread_store=thread_store,
|
||||
thread_id=thread_id,
|
||||
source_state=copy.deepcopy(channel_values),
|
||||
app_config=ctx.app_config,
|
||||
),
|
||||
name=f"title-finalize:{thread_id}",
|
||||
)
|
||||
title_task.add_done_callback(_log_background_title_finalize_failure(thread_id))
|
||||
elif record.status == RunStatus.success:
|
||||
log_title_debug(
|
||||
"Run %s: background title generation not scheduled for thread %s because title_provisional=%r title=%r",
|
||||
run_id,
|
||||
thread_id,
|
||||
channel_values.get("title_provisional"),
|
||||
channel_values.get("title"),
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Failed to sync title for thread %s (non-fatal)", thread_id)
|
||||
|
||||
# Update threads_meta status based on run outcome
|
||||
if thread_store is not None:
|
||||
@@ -484,6 +517,65 @@ async def _call_checkpointer_method(checkpointer: Any, async_name: str, sync_nam
|
||||
return result
|
||||
|
||||
|
||||
async def _terminal_llm_error_reason(
|
||||
*,
|
||||
checkpointer: Any | None,
|
||||
thread_id: str,
|
||||
) -> str | None:
|
||||
"""Return the terminal LLM fallback reason, if this run ended in one.
|
||||
|
||||
``LLMErrorHandlingMiddleware`` persists its fallback as an AI message with
|
||||
an internal marker. A normal agent graph treats that message as a valid
|
||||
result and completes without an exception, so the worker must check the
|
||||
saved conversation rather than relying solely on ``astream`` raising.
|
||||
"""
|
||||
if checkpointer is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
checkpoint_config = {
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
checkpoint_tuple = await _call_checkpointer_method(
|
||||
checkpointer,
|
||||
"aget_tuple",
|
||||
"get_tuple",
|
||||
checkpoint_config,
|
||||
)
|
||||
if checkpoint_tuple is None:
|
||||
return None
|
||||
checkpoint = getattr(checkpoint_tuple, "checkpoint", {}) or {}
|
||||
channel_values = checkpoint.get("channel_values", {})
|
||||
messages = channel_values.get("messages", []) if isinstance(channel_values, dict) else []
|
||||
if not isinstance(messages, list):
|
||||
return None
|
||||
|
||||
# Walk backwards to the latest assistant message. Tool/human messages
|
||||
# may be appended around it by middleware, but an older fallback should
|
||||
# never turn a later successful response into an error.
|
||||
for message in reversed(messages):
|
||||
message_type = message.get("type") if isinstance(message, dict) else getattr(message, "type", None)
|
||||
if message_type not in {"ai", "assistant"}:
|
||||
continue
|
||||
from deerflow.agents.middlewares.llm_error_handling_middleware import (
|
||||
is_llm_error_message,
|
||||
)
|
||||
|
||||
return is_llm_error_message(message)
|
||||
except Exception:
|
||||
# A status check must never turn a completed answer into a failed run
|
||||
# merely because a custom checkpointer cannot expose its latest tuple.
|
||||
logger.debug(
|
||||
"Could not inspect terminal LLM state for thread %s",
|
||||
thread_id,
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def _rollback_to_pre_run_checkpoint(
|
||||
*,
|
||||
checkpointer: Any,
|
||||
|
||||
+7
-1
@@ -3,7 +3,13 @@ from typing import Literal
|
||||
from langchain.tools import tool
|
||||
|
||||
|
||||
@tool("ask_clarification", parse_docstring=True, return_direct=True)
|
||||
# ClarificationMiddleware owns the stop behaviour: it returns a Command that
|
||||
# writes the ToolMessage and explicitly routes to END. ``return_direct=True``
|
||||
# makes the prebuilt tool node short-circuit before that Command update is
|
||||
# checkpointed in some LangGraph versions, leaving an assistant tool call
|
||||
# without its matching ToolMessage. The next user reply then fails provider
|
||||
# validation with "tool_calls must be followed by tool messages".
|
||||
@tool("ask_clarification", parse_docstring=True, return_direct=False)
|
||||
def ask_clarification_tool(
|
||||
question: str,
|
||||
clarification_type: Literal[
|
||||
|
||||
Reference in new issue
Block a user