rayhpeng 9d0a42c1fb refactor(runtime): restructure runs module with new execution architecture
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>
2026-04-22 11:28:01 +08:00

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