mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-14 16:08:41 +00:00
* feat: add opt-in task notes and compacted history recall * fix: validate task continuity state and preserve user answers Honor explicit opt-out, preserve clarification replies and capture failure statuses, validate notebook writes, and clear branch archive references. Update the config version and audit optional LLM credentials, with regression and integration evidence. * fix: align Helm config version with task continuity schema * fix: preserve mixed task history and declare continuity policies * fix: recover malformed history and evict archives atomically
117 lines
7.0 KiB
Python
117 lines
7.0 KiB
Python
"""Manual live-model recovery check using production middleware and tools.
|
|
|
|
Run from backend with --endpoints /path/to/private.json --output /tmp/check.json.
|
|
The private file contains llm_base, llm_model and optional llm_key. No network
|
|
calls occur on import. Uses synthetic history only; no endpoint or response body
|
|
is retained in the public result. This is a controlled integration check, not a
|
|
production success-rate benchmark. The summary prompt intentionally omits codes
|
|
so successful recovery must exercise source recall.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from langchain.agents import create_agent
|
|
from langchain.tools import tool
|
|
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
from langchain_openai import ChatOpenAI
|
|
from langgraph.checkpoint.memory import InMemorySaver
|
|
|
|
from deerflow.agents.middlewares.durable_context_middleware import DurableContextMiddleware
|
|
from deerflow.agents.middlewares.summarization_middleware import DeerFlowSummarizationMiddleware
|
|
from deerflow.agents.task_continuity import archive
|
|
from deerflow.agents.task_continuity.tools import history_read, history_search, task_note
|
|
from deerflow.agents.thread_state import ThreadState
|
|
from deerflow.config.paths import Paths
|
|
from deerflow.config.task_continuity_config import TaskContinuityConfig
|
|
|
|
|
|
async def run(args):
|
|
private = json.loads(Path(args.endpoints).read_text())
|
|
model = ChatOpenAI(model=private["llm_model"], base_url=private["llm_base"], api_key=private.get("llm_key", "unused"), temperature=0, max_tokens=2048, timeout=180, max_retries=1, extra_body={"reasoning_effort": "none"})
|
|
results = []
|
|
with tempfile.TemporaryDirectory(prefix="deerflow-continuity-") as directory:
|
|
root = Path(directory)
|
|
original_paths = archive.get_paths
|
|
archive.get_paths = lambda: Paths(base_dir=root)
|
|
try:
|
|
for index in range(3):
|
|
code = f"CIT-{731 + index * 37}-B"
|
|
output = root / f"artifact-{index}.json"
|
|
|
|
@tool
|
|
def write_manifest(batch_code: str, keep_backups: bool) -> str:
|
|
"""Write the requested final manifest using recovered task facts."""
|
|
output.write_text(json.dumps({"batch_code": batch_code, "keep_backups": keep_backups}))
|
|
return "Manifest written."
|
|
|
|
tools = [task_note, history_search, history_read, write_manifest]
|
|
saver = InMemorySaver()
|
|
context = {"thread_id": f"live-{index}", "user_id": "continuity-check"}
|
|
config = {"configurable": {"thread_id": context["thread_id"]}, "recursion_limit": 30}
|
|
middleware = DeerFlowSummarizationMiddleware(
|
|
model=model,
|
|
trigger=("messages", 4),
|
|
keep=("messages", 2),
|
|
summary_prompt="Summarize the project purpose in one short sentence. Omit all batch identifiers and exact values. Historical data: {messages}",
|
|
task_continuity_config=TaskContinuityConfig(enabled=True),
|
|
)
|
|
# First invocation archives old source messages through the real graph.
|
|
graph = create_agent(model, tools=tools, middleware=[DurableContextMiddleware(task_continuity_enabled=True), middleware], state_schema=ThreadState, checkpointer=saver)
|
|
history = [
|
|
HumanMessage(content="Citrine project: inspect the latest approved batch.", id="u1"),
|
|
AIMessage(content="", tool_calls=[{"name": "inspect_batch", "id": "inspection", "args": {"project": "Citrine"}}], id="a1"),
|
|
ToolMessage(content=f"Citrine approved batch_code={code}; keep_backups=true. Older batch is retired.", tool_call_id="inspection", id="t1"),
|
|
AIMessage(content="Inspection completed.", id="a2"),
|
|
HumanMessage(content="Pause this task. Reply only 'paused'.", id="pause"),
|
|
]
|
|
record = {"case": index, "model": private["llm_model"]}
|
|
try:
|
|
paused = await graph.ainvoke({"messages": history}, config=config, context=context)
|
|
record["archived"] = bool(paused.get("task_history", {}).get("batches"))
|
|
record["code_absent_from_active_context"] = code not in paused.get("summary_text", "") and all(code not in str(m.content) for m in paused["messages"])
|
|
# Rebuild against the existing checkpoint and recover with native tool calls.
|
|
resumed = create_agent(model, tools=tools, middleware=[DurableContextMiddleware(task_continuity_enabled=True)], state_schema=ThreadState, checkpointer=saver)
|
|
state = await resumed.ainvoke(
|
|
{
|
|
"messages": [
|
|
HumanMessage(
|
|
content=(
|
|
"Resume Citrine. Search historical sources for the approved batch, read the exact source, "
|
|
"save a task note with its source ID, and write the final manifest. Preserve the backup decision. Do not guess missing values."
|
|
)
|
|
)
|
|
]
|
|
},
|
|
config=config,
|
|
context=context,
|
|
)
|
|
calls = [call["name"] for message in state["messages"] if isinstance(message, AIMessage) for call in message.tool_calls]
|
|
record["tools_used"] = sorted(set(calls))
|
|
record["notes_saved"] = bool(state.get("task_notes"))
|
|
actual = json.loads(output.read_text()) if output.exists() else None
|
|
record["artifact_verified"] = actual == {"batch_code": code, "keep_backups": True}
|
|
record["artifact_sha256"] = hashlib.sha256(output.read_bytes()).hexdigest() if output.exists() else None
|
|
record["passed"] = all(record.get(k) for k in ("archived", "code_absent_from_active_context", "notes_saved", "artifact_verified")) and {"history_search", "history_read", "task_note", "write_manifest"} <= set(calls)
|
|
except Exception as exc:
|
|
record.update({"passed": False, "error_type": type(exc).__name__})
|
|
results.append(record)
|
|
Path(args.output).write_text(json.dumps({"scope": "controlled production-middleware integration; synthetic input; summary intentionally drops exact codes", "cases": results}, indent=2))
|
|
print(json.dumps(record), flush=True)
|
|
finally:
|
|
archive.get_paths = original_paths
|
|
return all(row["passed"] for row in results)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--endpoints", required=True)
|
|
parser.add_argument("--output", required=True)
|
|
raise SystemExit(0 if asyncio.run(run(parser.parse_args())) else 1)
|