mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-14 04:33:45 +00:00
Major refactoring of deerflow/runtime/: - runs/callbacks/ - new callback system (builder, events, title, tokens) - runs/internal/ - execution internals (executor, supervisor, stream_logic, registry) - runs/internal/execution/ - execution artifacts and events handling - runs/facade.py - high-level run facade - runs/observer.py - run observation protocol - runs/types.py - type definitions - runs/store/ - simplified store interfaces (create, delete, query, event) Refactor stream_bridge/: - Replace old providers with contract.py and exceptions.py - Remove async_provider.py, base.py, memory.py Add documentation: - README.md and README_zh.md for runtime module Remove deprecated: - manager.py moved to internal/ - worker.py, schemas.py - user_context.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
139 lines
4.1 KiB
Python
139 lines
4.1 KiB
Python
"""Callback assembly for runs execution."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from langchain_core.callbacks import BaseCallbackHandler
|
|
|
|
from ..store import RunEventStore
|
|
from ..types import RunRecord
|
|
from .events import RunEventCallback
|
|
from .title import RunTitleCallback
|
|
from .tokens import RunCompletionData, RunTokenCallback
|
|
|
|
|
|
@dataclass
|
|
class RunCallbackArtifacts:
|
|
"""Callbacks plus handles used by the executor after callbacks run."""
|
|
|
|
callbacks: list[BaseCallbackHandler]
|
|
event_callback: RunEventCallback | None = None
|
|
token_callback: RunTokenCallback | None = None
|
|
title_callback: RunTitleCallback | None = None
|
|
|
|
async def flush(self) -> None:
|
|
for callback in self.callbacks:
|
|
flush = getattr(callback, "flush", None)
|
|
if flush is None:
|
|
continue
|
|
result = flush()
|
|
if hasattr(result, "__await__"):
|
|
await result
|
|
|
|
def completion_data(self) -> RunCompletionData:
|
|
if self.token_callback is None:
|
|
return RunCompletionData()
|
|
return self.token_callback.completion_data()
|
|
|
|
def title(self) -> str | None:
|
|
if self.title_callback is None:
|
|
return None
|
|
return self.title_callback.title()
|
|
|
|
|
|
def build_run_callbacks(
|
|
*,
|
|
record: RunRecord,
|
|
graph_input: dict[str, Any],
|
|
event_store: RunEventStore | None,
|
|
existing_callbacks: Iterable[BaseCallbackHandler] = (),
|
|
) -> RunCallbackArtifacts:
|
|
"""Build execution callbacks for a run.
|
|
|
|
Reference callbacks are intentionally not assembled here yet; they remain
|
|
in the existing artifacts path until that integration is migrated.
|
|
"""
|
|
callbacks = list(existing_callbacks)
|
|
|
|
event_callback = None
|
|
if event_store is not None:
|
|
event_callback = RunEventCallback(
|
|
run_id=record.run_id,
|
|
thread_id=record.thread_id,
|
|
event_store=event_store,
|
|
)
|
|
callbacks.append(event_callback)
|
|
|
|
token_callback = RunTokenCallback(track_token_usage=True)
|
|
_set_first_human_message(token_callback, graph_input)
|
|
callbacks.append(token_callback)
|
|
|
|
title_callback = RunTitleCallback()
|
|
callbacks.append(title_callback)
|
|
|
|
return RunCallbackArtifacts(
|
|
callbacks=callbacks,
|
|
event_callback=event_callback,
|
|
token_callback=token_callback,
|
|
title_callback=title_callback,
|
|
)
|
|
|
|
|
|
def _set_first_human_message(token_callback: RunTokenCallback, graph_input: dict[str, Any]) -> None:
|
|
messages = graph_input.get("messages")
|
|
if not isinstance(messages, list) or not messages:
|
|
return
|
|
|
|
first = messages[0]
|
|
content = _extract_first_human_text(first)
|
|
if content:
|
|
token_callback.set_first_human_message(content)
|
|
|
|
|
|
def _extract_first_human_text(message: Any) -> str | None:
|
|
if isinstance(message, str):
|
|
return message
|
|
|
|
content = getattr(message, "content", None)
|
|
if content is not None:
|
|
return _extract_text_content(content)
|
|
|
|
if isinstance(message, dict):
|
|
return _extract_text_content(message.get("content"))
|
|
|
|
return None
|
|
|
|
|
|
def _extract_text_content(content: Any) -> str | None:
|
|
if isinstance(content, str):
|
|
return content
|
|
|
|
if isinstance(content, list):
|
|
parts: list[str] = []
|
|
for item in content:
|
|
if isinstance(item, str):
|
|
parts.append(item)
|
|
continue
|
|
if not isinstance(item, dict):
|
|
continue
|
|
if item.get("type") == "text" and isinstance(item.get("text"), str):
|
|
parts.append(item["text"])
|
|
continue
|
|
if isinstance(item.get("content"), str):
|
|
parts.append(item["content"])
|
|
joined = "".join(parts).strip()
|
|
return joined or None
|
|
|
|
if isinstance(content, dict):
|
|
text = content.get("text")
|
|
if isinstance(text, str):
|
|
return text
|
|
nested = content.get("content")
|
|
if isinstance(nested, str):
|
|
return nested
|
|
|
|
return None
|