Files
dify/api/services/dify_builder/native_launch_observation.py
T

114 lines
4.0 KiB
Python

"""Observe the native worker boundary, without granting execution authority.
Only instrumented synchronous generation can prove preparation failed before
worker dispatch. Missing instrumentation and interrupted processes stay unknown.
"""
import re
from collections.abc import Generator
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import Literal
FailureResource = tuple[Literal["model", "tool"], str, str]
@dataclass
class NativeLaunchObservation:
preparing: bool = False
dispatched: bool = False
failure_resource: FailureResource | None = None
_resource_error: Exception | None = field(default=None, repr=False)
def resource_for(self, error: Exception) -> FailureResource | None:
current: BaseException | None = error
for _ in range(8):
if current is None:
break
if current is self._resource_error:
return self.failure_resource
current = current.__cause__ or current.__context__
return None
@contextmanager
def track(self) -> Generator[None]:
token = _active.set(self)
try:
yield
finally:
_active.reset(token)
@property
def failed_before_dispatch(self) -> bool:
return self.preparing and not self.dispatched
_active: ContextVar[NativeLaunchObservation | None] = ContextVar("builder_native_launch", default=None)
def begin_preparation() -> None:
if (observation := _active.get()) is not None:
observation.preparing = True
def mark_dispatched() -> None:
from services.dify_builder.task_controls import check_current
check_current()
# Mark before Thread.start: a start acknowledgement failure is conservative.
if (observation := _active.get()) is not None:
observation.dispatched = True
def _public_id(value: str) -> bool:
return bool(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_./:@+-]{0,127}", value))
@contextmanager
def observe_resource(kind: Literal["model", "tool"], provider: str, name: str = "") -> Generator[None]:
"""Attribute only a failed metadata read, never successful earlier reads."""
try:
yield
except Exception as exc:
observation = _active.get()
if observation is not None:
from services.dify_builder import trace_diagnostics
if trace_diagnostics.plugin_activation_unavailable(exc):
# Worker context is copied before dispatch. Observe the typed
# cause here, before native response handling converts it to
# text; this grants no execution or retry authority.
trace_diagnostics.report(
"native_preparation_failed" if observation.failed_before_dispatch else "native_outcome_unknown",
error=exc,
provider=provider,
model=name if kind == "model" else None,
)
if observation is not None and _public_id(provider) and (not name or _public_id(name)):
observation.failure_resource = (kind, provider, name)
observation._resource_error = exc
raise
def resource_node_ids(graph: dict, resource: FailureResource | None) -> tuple[str, ...]:
if resource is None:
return ()
kind, provider, name = resource
result = []
for node in graph.get("nodes", []):
data = node.get("data") or {}
if kind == "model":
config = data.get("model") or {}
matches = data.get("type") == "llm" and config.get("provider") == provider and config.get("name") == name
else:
matches = (
data.get("type") == "tool"
and data.get("provider_id") == provider
and (not name or data.get("tool_name") == name)
)
node_id = node.get("id")
if matches and isinstance(node_id, str) and _public_id(node_id):
result.append(node_id)
return tuple(sorted(set(result)))