mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-24 00:45:57 +00:00
refactor(gateway): restructure gateway with new dependency injection and services
Reorganize app/gateway/ with: - common/ - lifespan management - dependencies/ - FastAPI dependency injection (db, checkpointer, repositories, stream_bridge) - services/runs/ - run execution services (facade_factory, input adapters, store operations) - registrar.py - router registration - router.py - main router setup Simplify app.py to use the new modular structure. Remove deprecated utils.py. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
"""Gateway service layer."""
|
||||
|
||||
"""Compatibility package for app service submodules."""
|
||||
|
||||
__all__: list[str] = []
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Runs app layer services."""
|
||||
|
||||
from app.infra.storage import StorageRunObserver
|
||||
from .input import (
|
||||
AdaptedRunRequest,
|
||||
RunSpecBuilder,
|
||||
UnsupportedRunFeatureError,
|
||||
adapt_create_run_request,
|
||||
adapt_create_stream_request,
|
||||
adapt_create_wait_request,
|
||||
adapt_join_stream_request,
|
||||
adapt_join_wait_request,
|
||||
)
|
||||
from .store import AppRunCreateStore, AppRunDeleteStore, AppRunQueryStore
|
||||
|
||||
__all__ = [
|
||||
"AdaptedRunRequest",
|
||||
"AppRunCreateStore",
|
||||
"AppRunDeleteStore",
|
||||
"AppRunQueryStore",
|
||||
"RunSpecBuilder",
|
||||
"StorageRunObserver",
|
||||
"UnsupportedRunFeatureError",
|
||||
"adapt_create_run_request",
|
||||
"adapt_create_stream_request",
|
||||
"adapt_create_wait_request",
|
||||
"adapt_join_stream_request",
|
||||
"adapt_join_wait_request",
|
||||
]
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Facade factory - assembles RunsFacade with dependencies."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
from app.gateway.dependencies import get_checkpointer, get_stream_bridge
|
||||
from deerflow.runtime.runs.facade import RunsFacade
|
||||
from deerflow.runtime.runs.facade import RunsRuntime
|
||||
from deerflow.runtime.runs.internal.execution.supervisor import RunSupervisor
|
||||
from deerflow.runtime.runs.internal.planner import ExecutionPlanner
|
||||
from deerflow.runtime.runs.internal.registry import RunRegistry
|
||||
from deerflow.runtime.runs.internal.streams import RunStreamService
|
||||
from deerflow.runtime.runs.internal.wait import RunWaitService
|
||||
|
||||
from app.infra.storage import StorageRunObserver, ThreadMetaStorage
|
||||
from app.infra.storage.runs import RunDeleteRepository, RunReadRepository, RunWriteRepository
|
||||
from .store import AppRunCreateStore, AppRunDeleteStore, AppRunQueryStore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deerflow.runtime.stream_bridge import StreamBridge
|
||||
|
||||
|
||||
type AgentFactory = Callable[..., object]
|
||||
|
||||
|
||||
# Module-level singleton registry (shared across requests)
|
||||
_registry: RunRegistry | None = None
|
||||
_supervisor: RunSupervisor | None = None
|
||||
|
||||
|
||||
def _get_state(request: Request, attr: str, label: str):
|
||||
value = getattr(request.app.state, attr, None)
|
||||
if value is None:
|
||||
raise HTTPException(status_code=503, detail=f"{label} not available")
|
||||
return value
|
||||
|
||||
|
||||
def get_registry() -> RunRegistry:
|
||||
"""Get or create singleton registry."""
|
||||
global _registry
|
||||
if _registry is None:
|
||||
_registry = RunRegistry()
|
||||
return _registry
|
||||
|
||||
|
||||
def get_supervisor() -> RunSupervisor:
|
||||
"""Get or create singleton run supervisor."""
|
||||
global _supervisor
|
||||
if _supervisor is None:
|
||||
_supervisor = RunSupervisor()
|
||||
return _supervisor
|
||||
|
||||
|
||||
def resolve_agent_factory(assistant_id: str | None) -> AgentFactory:
|
||||
"""Resolve the agent factory callable from config."""
|
||||
from deerflow.agents.lead_agent.agent import make_lead_agent
|
||||
|
||||
return make_lead_agent
|
||||
|
||||
|
||||
def build_runs_facade(
|
||||
*,
|
||||
stream_bridge: "StreamBridge",
|
||||
checkpointer: object,
|
||||
store: object | None = None,
|
||||
run_read_repo: RunReadRepository | None = None,
|
||||
run_write_repo: RunWriteRepository | None = None,
|
||||
run_delete_repo: RunDeleteRepository | None = None,
|
||||
thread_meta_storage: ThreadMetaStorage | None = None,
|
||||
run_event_store: object | None = None,
|
||||
) -> RunsFacade:
|
||||
"""
|
||||
Build RunsFacade with all dependencies.
|
||||
|
||||
Args:
|
||||
stream_bridge: StreamBridge instance
|
||||
checkpointer: LangGraph checkpointer
|
||||
store: Optional LangGraph runtime store
|
||||
run_read_repo: Optional run repository for durable reads
|
||||
run_write_repo: Optional run repository for durable writes
|
||||
run_delete_repo: Optional run repository for durable deletes
|
||||
thread_meta_storage: Optional thread metadata storage adapter
|
||||
|
||||
Returns:
|
||||
Configured RunsFacade instance
|
||||
"""
|
||||
registry = get_registry()
|
||||
planner = ExecutionPlanner()
|
||||
supervisor = get_supervisor()
|
||||
|
||||
stream_service = RunStreamService(stream_bridge)
|
||||
wait_service = RunWaitService(stream_service)
|
||||
query_store = AppRunQueryStore(run_read_repo) if run_read_repo else None
|
||||
create_store = (
|
||||
AppRunCreateStore(run_write_repo, thread_meta_storage=thread_meta_storage)
|
||||
if run_write_repo
|
||||
else None
|
||||
)
|
||||
delete_store = AppRunDeleteStore(run_delete_repo) if run_delete_repo else None
|
||||
|
||||
# Build storage observer if repositories provided
|
||||
storage_observer = None
|
||||
if run_write_repo or thread_meta_storage:
|
||||
storage_observer = StorageRunObserver(
|
||||
run_write_repo=run_write_repo,
|
||||
thread_meta_storage=thread_meta_storage,
|
||||
)
|
||||
|
||||
return RunsFacade(
|
||||
registry=registry,
|
||||
planner=planner,
|
||||
supervisor=supervisor,
|
||||
stream_service=stream_service,
|
||||
wait_service=wait_service,
|
||||
runtime=RunsRuntime(
|
||||
bridge=stream_bridge,
|
||||
checkpointer=checkpointer,
|
||||
store=store,
|
||||
event_store=run_event_store,
|
||||
agent_factory_resolver=resolve_agent_factory,
|
||||
),
|
||||
observer=storage_observer,
|
||||
query_store=query_store,
|
||||
create_store=create_store,
|
||||
delete_store=delete_store,
|
||||
)
|
||||
|
||||
|
||||
def build_runs_facade_from_request(request: "Request") -> RunsFacade:
|
||||
"""
|
||||
Build RunsFacade from FastAPI request context.
|
||||
|
||||
Extracts dependencies from request.app.state.
|
||||
"""
|
||||
app_state = request.app.state
|
||||
|
||||
return build_runs_facade(
|
||||
stream_bridge=get_stream_bridge(request),
|
||||
checkpointer=get_checkpointer(request),
|
||||
store=getattr(request.app.state, "store", None),
|
||||
run_read_repo=getattr(app_state, "run_read_repo", None),
|
||||
run_write_repo=getattr(app_state, "run_write_repo", None),
|
||||
run_delete_repo=getattr(app_state, "run_delete_repo", None),
|
||||
thread_meta_storage=getattr(app_state, "thread_meta_storage", None),
|
||||
run_event_store=getattr(app_state, "run_event_store", None),
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Input adapters for app-owned runs entrypoints."""
|
||||
|
||||
from .request_adapter import (
|
||||
AdaptedRunRequest,
|
||||
adapt_create_run_request,
|
||||
adapt_create_stream_request,
|
||||
adapt_create_wait_request,
|
||||
adapt_join_stream_request,
|
||||
adapt_join_wait_request,
|
||||
)
|
||||
from .spec_builder import RunSpecBuilder, UnsupportedRunFeatureError
|
||||
|
||||
__all__ = [
|
||||
"AdaptedRunRequest",
|
||||
"RunSpecBuilder",
|
||||
"UnsupportedRunFeatureError",
|
||||
"adapt_create_run_request",
|
||||
"adapt_create_stream_request",
|
||||
"adapt_create_wait_request",
|
||||
"adapt_join_stream_request",
|
||||
"adapt_join_wait_request",
|
||||
]
|
||||
@@ -0,0 +1,127 @@
|
||||
"""App-owned request adapter for runs entrypoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from deerflow.runtime.stream_bridge import JSONValue
|
||||
from deerflow.runtime.runs.types import RunIntent
|
||||
|
||||
type RequestBody = dict[str, JSONValue]
|
||||
type RequestQuery = dict[str, str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AdaptedRunRequest:
|
||||
"""
|
||||
统一的内部请求 DTO.
|
||||
|
||||
路由层只负责提取 path/query/body,适配器负责转成稳定内部结构。
|
||||
"""
|
||||
|
||||
intent: RunIntent
|
||||
thread_id: str | None
|
||||
run_id: str | None
|
||||
body: RequestBody
|
||||
headers: dict[str, str]
|
||||
query: RequestQuery
|
||||
|
||||
@property
|
||||
def last_event_id(self) -> str | None:
|
||||
"""Extract Last-Event-ID from headers."""
|
||||
return self.headers.get("last-event-id") or self.headers.get("Last-Event-ID")
|
||||
|
||||
@property
|
||||
def is_stateless(self) -> bool:
|
||||
"""Check if this is a stateless request."""
|
||||
return self.thread_id is None
|
||||
|
||||
|
||||
def adapt_create_run_request(
|
||||
*,
|
||||
thread_id: str | None,
|
||||
body: RequestBody,
|
||||
headers: dict[str, str] | None = None,
|
||||
query: RequestQuery | None = None,
|
||||
) -> AdaptedRunRequest:
|
||||
"""Adapt POST /threads/{thread_id}/runs or POST /runs."""
|
||||
return AdaptedRunRequest(
|
||||
intent="create_background",
|
||||
thread_id=thread_id,
|
||||
run_id=None,
|
||||
body=body,
|
||||
headers=headers or {},
|
||||
query=query or {},
|
||||
)
|
||||
|
||||
|
||||
def adapt_create_stream_request(
|
||||
*,
|
||||
thread_id: str | None,
|
||||
body: RequestBody,
|
||||
headers: dict[str, str] | None = None,
|
||||
query: RequestQuery | None = None,
|
||||
) -> AdaptedRunRequest:
|
||||
"""Adapt POST /threads/{thread_id}/runs/stream or POST /runs/stream."""
|
||||
return AdaptedRunRequest(
|
||||
intent="create_and_stream",
|
||||
thread_id=thread_id,
|
||||
run_id=None,
|
||||
body=body,
|
||||
headers=headers or {},
|
||||
query=query or {},
|
||||
)
|
||||
|
||||
|
||||
def adapt_create_wait_request(
|
||||
*,
|
||||
thread_id: str | None,
|
||||
body: RequestBody,
|
||||
headers: dict[str, str] | None = None,
|
||||
query: RequestQuery | None = None,
|
||||
) -> AdaptedRunRequest:
|
||||
"""Adapt POST /threads/{thread_id}/runs/wait or POST /runs/wait."""
|
||||
return AdaptedRunRequest(
|
||||
intent="create_and_wait",
|
||||
thread_id=thread_id,
|
||||
run_id=None,
|
||||
body=body,
|
||||
headers=headers or {},
|
||||
query=query or {},
|
||||
)
|
||||
|
||||
|
||||
def adapt_join_stream_request(
|
||||
*,
|
||||
thread_id: str,
|
||||
run_id: str,
|
||||
headers: dict[str, str] | None = None,
|
||||
query: RequestQuery | None = None,
|
||||
) -> AdaptedRunRequest:
|
||||
"""Adapt GET /threads/{thread_id}/runs/{run_id}/stream."""
|
||||
return AdaptedRunRequest(
|
||||
intent="join_stream",
|
||||
thread_id=thread_id,
|
||||
run_id=run_id,
|
||||
body={},
|
||||
headers=headers or {},
|
||||
query=query or {},
|
||||
)
|
||||
|
||||
|
||||
def adapt_join_wait_request(
|
||||
*,
|
||||
thread_id: str,
|
||||
run_id: str,
|
||||
headers: dict[str, str] | None = None,
|
||||
query: RequestQuery | None = None,
|
||||
) -> AdaptedRunRequest:
|
||||
"""Adapt GET /threads/{thread_id}/runs/{run_id}/join."""
|
||||
return AdaptedRunRequest(
|
||||
intent="join_wait",
|
||||
thread_id=thread_id,
|
||||
run_id=run_id,
|
||||
body={},
|
||||
headers=headers or {},
|
||||
query=query or {},
|
||||
)
|
||||
@@ -0,0 +1,254 @@
|
||||
"""App-owned RunSpec builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
from deerflow.runtime.runs.types import CheckpointRequest, RunScope, RunSpec
|
||||
from deerflow.runtime.stream_bridge import JSONValue
|
||||
|
||||
from .request_adapter import AdaptedRunRequest
|
||||
|
||||
type JSONMapping = dict[str, JSONValue]
|
||||
type GraphInput = dict[str, object]
|
||||
type RunnableConfigDict = dict[str, object]
|
||||
|
||||
|
||||
class UnsupportedRunFeatureError(ValueError):
|
||||
"""Raised when a phase1-unsupported feature is requested."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RunSpecBuilder:
|
||||
"""
|
||||
Build RunSpec from AdaptedRunRequest.
|
||||
|
||||
Phase 1 rules:
|
||||
1. messages-tuple normalized to messages
|
||||
2. enqueue not supported
|
||||
3. rollback not supported
|
||||
4. after_seconds not supported
|
||||
5. stream_resumable accepted
|
||||
6. stateless auto-generates temporary thread
|
||||
"""
|
||||
|
||||
# Phase 1 unsupported features
|
||||
UNSUPPORTED_MULTITASK_STRATEGIES = {"enqueue"}
|
||||
UNSUPPORTED_ACTIONS = {"rollback"}
|
||||
|
||||
# Default stream modes
|
||||
DEFAULT_STREAM_MODES = ["values", "messages"]
|
||||
CONTEXT_CONFIGURABLE_KEYS = frozenset({
|
||||
"model_name",
|
||||
"mode",
|
||||
"thinking_enabled",
|
||||
"reasoning_effort",
|
||||
"is_plan_mode",
|
||||
"subagent_enabled",
|
||||
"max_concurrent_subagents",
|
||||
})
|
||||
DEFAULT_ASSISTANT_ID = "lead_agent"
|
||||
|
||||
@staticmethod
|
||||
def _as_json_mapping(value: JSONValue | None) -> JSONMapping | None:
|
||||
return value if isinstance(value, dict) else None
|
||||
|
||||
@staticmethod
|
||||
def _as_string_list(value: JSONValue | None) -> list[str] | None:
|
||||
if not isinstance(value, list):
|
||||
return None
|
||||
return [item for item in value if isinstance(item, str)]
|
||||
|
||||
def build(self, request: AdaptedRunRequest) -> RunSpec:
|
||||
"""Build RunSpec from adapted request."""
|
||||
body = request.body
|
||||
|
||||
# Validate phase1 constraints
|
||||
self._validate_constraints(body)
|
||||
|
||||
# Build scope
|
||||
scope = self._build_scope(request)
|
||||
|
||||
# Normalize stream modes
|
||||
stream_modes = self._normalize_stream_modes(body.get("stream_mode"))
|
||||
|
||||
# Build checkpoint request
|
||||
checkpoint_request = self._build_checkpoint_request(body)
|
||||
|
||||
config = self._build_runnable_config(
|
||||
thread_id=scope.thread_id,
|
||||
request_config=self._as_json_mapping(body.get("config")),
|
||||
metadata=self._as_json_mapping(body.get("metadata")),
|
||||
assistant_id=body.get("assistant_id"),
|
||||
context=self._as_json_mapping(body.get("context")),
|
||||
)
|
||||
|
||||
return RunSpec(
|
||||
intent=request.intent,
|
||||
scope=scope,
|
||||
assistant_id=body.get("assistant_id") if isinstance(body.get("assistant_id"), str) else None,
|
||||
input=self._normalize_input(self._as_json_mapping(body.get("input"))),
|
||||
command=self._as_json_mapping(body.get("command")),
|
||||
runnable_config=config,
|
||||
context=self._as_json_mapping(body.get("context")),
|
||||
metadata=self._as_json_mapping(body.get("metadata")) or {},
|
||||
stream_modes=stream_modes,
|
||||
stream_subgraphs=bool(body.get("stream_subgraphs", False)),
|
||||
stream_resumable=bool(body.get("stream_resumable", False)),
|
||||
on_disconnect=body.get("on_disconnect", "cancel") if body.get("on_disconnect") in {"cancel", "continue"} else "cancel",
|
||||
on_completion=body.get("on_completion", "keep") if body.get("on_completion") in {"delete", "keep"} else "keep",
|
||||
multitask_strategy=body.get("multitask_strategy", "reject") if body.get("multitask_strategy") in {"reject", "interrupt"} else "reject",
|
||||
interrupt_before="*" if body.get("interrupt_before") == "*" else self._as_string_list(body.get("interrupt_before")),
|
||||
interrupt_after="*" if body.get("interrupt_after") == "*" else self._as_string_list(body.get("interrupt_after")),
|
||||
checkpoint_request=checkpoint_request,
|
||||
follow_up_to_run_id=body.get("follow_up_to_run_id") if isinstance(body.get("follow_up_to_run_id"), str) else None,
|
||||
webhook=body.get("webhook") if isinstance(body.get("webhook"), str) else None,
|
||||
feedback_keys=self._as_string_list(body.get("feedback_keys")),
|
||||
)
|
||||
|
||||
def _validate_constraints(self, body: JSONMapping) -> None:
|
||||
"""Validate phase1 constraints, raise UnsupportedRunFeatureError if violated."""
|
||||
# Check multitask_strategy
|
||||
strategy = body.get("multitask_strategy", "reject")
|
||||
if strategy in self.UNSUPPORTED_MULTITASK_STRATEGIES:
|
||||
raise UnsupportedRunFeatureError(
|
||||
f"multitask_strategy '{strategy}' is not supported in phase1. "
|
||||
f"Supported: reject, interrupt"
|
||||
)
|
||||
|
||||
# Check for rollback action
|
||||
command = self._as_json_mapping(body.get("command")) or {}
|
||||
if command.get("action") in self.UNSUPPORTED_ACTIONS:
|
||||
raise UnsupportedRunFeatureError(
|
||||
f"action '{command.get('action')}' is not supported in phase1"
|
||||
)
|
||||
|
||||
# Check for after_seconds
|
||||
if body.get("after_seconds") is not None:
|
||||
raise UnsupportedRunFeatureError("after_seconds is not supported in phase1")
|
||||
|
||||
def _build_scope(self, request: AdaptedRunRequest) -> RunScope:
|
||||
"""Build RunScope from request."""
|
||||
if request.is_stateless:
|
||||
# Stateless: generate temporary thread
|
||||
return RunScope(
|
||||
kind="stateless",
|
||||
thread_id=str(uuid.uuid4()),
|
||||
temporary=True,
|
||||
)
|
||||
else:
|
||||
assert request.thread_id is not None
|
||||
return RunScope(
|
||||
kind="stateful",
|
||||
thread_id=request.thread_id,
|
||||
temporary=False,
|
||||
)
|
||||
|
||||
def _normalize_stream_modes(self, stream_mode: JSONValue | None) -> list[str]:
|
||||
"""Normalize stream_mode to list, convert messages-tuple to messages."""
|
||||
if stream_mode is None:
|
||||
return self.DEFAULT_STREAM_MODES.copy()
|
||||
|
||||
if isinstance(stream_mode, str):
|
||||
modes = [stream_mode]
|
||||
elif isinstance(stream_mode, list):
|
||||
modes = [mode for mode in stream_mode if isinstance(mode, str)]
|
||||
else:
|
||||
return self.DEFAULT_STREAM_MODES.copy()
|
||||
|
||||
return ["messages" if m == "messages-tuple" else m for m in modes]
|
||||
|
||||
def _build_checkpoint_request(self, body: JSONMapping) -> CheckpointRequest | None:
|
||||
"""Build CheckpointRequest if checkpoint data is provided."""
|
||||
checkpoint_id = body.get("checkpoint_id")
|
||||
checkpoint = self._as_json_mapping(body.get("checkpoint"))
|
||||
|
||||
if not isinstance(checkpoint_id, str) and checkpoint is None:
|
||||
return None
|
||||
|
||||
return CheckpointRequest(
|
||||
checkpoint_id=checkpoint_id if isinstance(checkpoint_id, str) else None,
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
def _normalize_input(self, raw_input: JSONMapping | None) -> GraphInput | None:
|
||||
"""Convert HTTP-friendly message dicts into LangChain message objects."""
|
||||
if raw_input is None:
|
||||
return None
|
||||
|
||||
messages = raw_input.get("messages")
|
||||
if not messages or not isinstance(messages, list):
|
||||
return raw_input
|
||||
|
||||
converted: list[object] = []
|
||||
for msg in messages:
|
||||
if isinstance(msg, dict):
|
||||
role = msg.get("role", msg.get("type", "user"))
|
||||
content = msg.get("content", "")
|
||||
if role in ("user", "human"):
|
||||
converted.append(HumanMessage(content=content))
|
||||
else:
|
||||
converted.append(HumanMessage(content=content))
|
||||
else:
|
||||
converted.append(msg)
|
||||
return {**raw_input, "messages": converted}
|
||||
|
||||
def _build_runnable_config(
|
||||
self,
|
||||
*,
|
||||
thread_id: str,
|
||||
request_config: JSONMapping | None,
|
||||
metadata: JSONMapping | None,
|
||||
assistant_id: str | None,
|
||||
context: JSONMapping | None,
|
||||
) -> RunnableConfigDict:
|
||||
"""Build RunnableConfig from request payload and app-side rules."""
|
||||
config: RunnableConfigDict = {"recursion_limit": 100}
|
||||
|
||||
if request_config:
|
||||
if "context" in request_config:
|
||||
config["context"] = request_config["context"]
|
||||
else:
|
||||
configurable = {"thread_id": thread_id}
|
||||
raw_configurable = request_config.get("configurable")
|
||||
if isinstance(raw_configurable, dict):
|
||||
configurable.update(raw_configurable)
|
||||
config["configurable"] = configurable
|
||||
|
||||
for key, value in request_config.items():
|
||||
if key not in ("configurable", "context"):
|
||||
config[key] = value
|
||||
else:
|
||||
config["configurable"] = {"thread_id": thread_id}
|
||||
|
||||
configurable = config.get("configurable")
|
||||
if (
|
||||
assistant_id
|
||||
and assistant_id != self.DEFAULT_ASSISTANT_ID
|
||||
and isinstance(configurable, dict)
|
||||
and "agent_name" not in configurable
|
||||
):
|
||||
normalized = assistant_id.strip().lower().replace("_", "-")
|
||||
if not normalized or not re.fullmatch(r"[a-z0-9-]+", normalized):
|
||||
raise ValueError(
|
||||
f"Invalid assistant_id {assistant_id!r}: must contain only letters, digits, and hyphens after normalization."
|
||||
)
|
||||
configurable["agent_name"] = normalized
|
||||
|
||||
if metadata:
|
||||
existing_metadata = config.get("metadata")
|
||||
if isinstance(existing_metadata, dict):
|
||||
existing_metadata.update(metadata)
|
||||
else:
|
||||
config["metadata"] = dict(metadata)
|
||||
|
||||
if context and isinstance(configurable, dict):
|
||||
for key in self.CONTEXT_CONFIGURABLE_KEYS:
|
||||
if key in context:
|
||||
configurable.setdefault(key, context[key])
|
||||
|
||||
return config
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Compatibility wrapper for the app-owned storage observer."""
|
||||
|
||||
from app.infra.storage.runs import StorageRunObserver
|
||||
|
||||
__all__ = ["StorageRunObserver"]
|
||||
@@ -0,0 +1,11 @@
|
||||
"""App-owned runs store adapters."""
|
||||
|
||||
from .create_store import AppRunCreateStore
|
||||
from .delete_store import AppRunDeleteStore
|
||||
from .query_store import AppRunQueryStore
|
||||
|
||||
__all__ = [
|
||||
"AppRunCreateStore",
|
||||
"AppRunDeleteStore",
|
||||
"AppRunQueryStore",
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
"""App-owned durable run creation adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from deerflow.runtime.runs.store import RunCreateStore
|
||||
from deerflow.runtime.runs.types import RunRecord
|
||||
|
||||
from app.infra.storage import ThreadMetaStorage
|
||||
from app.infra.storage.runs import RunWriteRepository
|
||||
|
||||
|
||||
class AppRunCreateStore(RunCreateStore):
|
||||
"""Write the initial durable row for a newly created run."""
|
||||
|
||||
def __init__(self, repo: RunWriteRepository, thread_meta_storage: ThreadMetaStorage | None = None) -> None:
|
||||
self._repo = repo
|
||||
self._thread_meta_storage = thread_meta_storage
|
||||
|
||||
async def create_run(self, record: RunRecord) -> None:
|
||||
await self._repo.create(
|
||||
run_id=record.run_id,
|
||||
thread_id=record.thread_id,
|
||||
assistant_id=record.assistant_id,
|
||||
status=str(record.status),
|
||||
metadata=record.metadata,
|
||||
follow_up_to_run_id=record.follow_up_to_run_id,
|
||||
created_at=record.created_at,
|
||||
)
|
||||
if self._thread_meta_storage is not None and record.assistant_id:
|
||||
thread = await self._thread_meta_storage.ensure_thread(
|
||||
thread_id=record.thread_id,
|
||||
assistant_id=record.assistant_id,
|
||||
)
|
||||
if thread.assistant_id != record.assistant_id:
|
||||
await self._thread_meta_storage.sync_thread_assistant_id(
|
||||
thread_id=record.thread_id,
|
||||
assistant_id=record.assistant_id,
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
"""App-owned durable run deletion adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from deerflow.runtime.runs.store import RunDeleteStore
|
||||
|
||||
from app.infra.storage.runs import RunDeleteRepository
|
||||
|
||||
|
||||
class AppRunDeleteStore(RunDeleteStore):
|
||||
"""Delete durable run rows via the app storage adapter."""
|
||||
|
||||
def __init__(self, repo: RunDeleteRepository) -> None:
|
||||
self._repo = repo
|
||||
|
||||
async def delete_run(self, run_id: str) -> bool:
|
||||
return await self._repo.delete(run_id)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""App-owned durable run query adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from deerflow.runtime.runs.store import RunQueryStore
|
||||
from deerflow.runtime.runs.types import RunRecord, RunStatus
|
||||
|
||||
from app.infra.storage.runs import RunReadRepository, RunRow
|
||||
|
||||
|
||||
class AppRunQueryStore(RunQueryStore):
|
||||
"""Map app-side durable run rows into harness RunRecord DTOs."""
|
||||
|
||||
def __init__(self, repo: RunReadRepository) -> None:
|
||||
self._repo = repo
|
||||
|
||||
async def get_run(self, run_id: str) -> RunRecord | None:
|
||||
row = await self._repo.get(run_id)
|
||||
if row is None:
|
||||
return None
|
||||
return self._to_run_record(row)
|
||||
|
||||
async def list_runs(
|
||||
self,
|
||||
thread_id: str,
|
||||
*,
|
||||
limit: int = 100,
|
||||
) -> list[RunRecord]:
|
||||
rows = await self._repo.list_by_thread(thread_id, limit=limit)
|
||||
return [self._to_run_record(row) for row in rows]
|
||||
|
||||
def _to_run_record(self, row: RunRow) -> RunRecord:
|
||||
return RunRecord(
|
||||
run_id=row["run_id"],
|
||||
thread_id=row["thread_id"],
|
||||
assistant_id=row.get("assistant_id"),
|
||||
status=RunStatus(row.get("status", "pending")),
|
||||
temporary=False,
|
||||
multitask_strategy=row.get("multitask_strategy", "reject"),
|
||||
metadata=row.get("metadata", {}),
|
||||
follow_up_to_run_id=row.get("follow_up_to_run_id"),
|
||||
created_at=row.get("created_at", ""),
|
||||
updated_at=row.get("updated_at", ""),
|
||||
started_at=row.get("started_at"),
|
||||
ended_at=row.get("ended_at"),
|
||||
error=row.get("error"),
|
||||
)
|
||||
Reference in New Issue
Block a user