Files
NSCT---Neutral-Search-Crawl…/src/nsct/orchestration/state.py
2026-09-06 18:32:42 +02:00

207 lines
6.3 KiB
Python

"""
State Machine Model für den NSCT Research Orchestrator.
Definiert die Lebenszyklus-Zustände eines Research-Run und validiert
erlaubte Zustandübergänge entlang der Pipeline.
"""
from __future__ import annotations
import logging
from enum import Enum
from typing import ClassVar, FrozenSet
logger = logging.getLogger(__name__)
class ResearchRunState(str, Enum):
"""Mögliche Zustände eines Research-Laufs entlang der Pipeline."""
# --- Initialisierung ---
CREATED = "created"
# --- Pipeline-Schritte ---
PLANNING = "planning"
SEARCHING = "searching"
FETCHING = "fetching"
EXTRACTING = "extracting"
ANALYZING = "analyzing"
EXPANDING = "expanding"
COMPARING = "comparing"
SYNTHESIZING = "synthesizing"
# --- Endzustände ---
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
# Übergangsmatrix: Quelle -> zulässige Ziele (frozenset für Immutable)
_VALID_TRANSITIONS: dict[ResearchRunState, FrozenSet[ResearchRunState]] = {
ResearchRunState.CREATED: frozenset(
(ResearchRunState.PLANNING, ResearchRunState.FAILED)
),
ResearchRunState.PLANNING: frozenset(
(ResearchRunState.SEARCHING, ResearchRunState.FAILED)
),
ResearchRunState.SEARCHING: frozenset(
(ResearchRunState.FETCHING, ResearchRunState.FAILED)
),
ResearchRunState.FETCHING: frozenset(
(ResearchRunState.EXTRACTING, ResearchRunState.FAILED)
),
ResearchRunState.EXTRACTING: frozenset(
(ResearchRunState.ANALYZING, ResearchRunState.FAILED)
),
ResearchRunState.ANALYZING: frozenset(
(
ResearchRunState.EXPANDING,
ResearchRunState.COMPARING,
ResearchRunState.FAILED,
)
),
ResearchRunState.EXPANDING: frozenset(
(
ResearchRunState.ANALYZING, # zurück zur Analyse nach Expansion
ResearchRunState.COMPARING,
ResearchRunState.FAILED,
)
),
ResearchRunState.COMPARING: frozenset(
(ResearchRunState.SYNTHESIZING, ResearchRunState.FAILED)
),
ResearchRunState.SYNTHESIZING: frozenset(
(ResearchRunState.COMPLETED, ResearchRunState.FAILED)
),
# Terminal-States: keine ausgehenden Übergänge.
ResearchRunState.COMPLETED: frozenset(),
ResearchRunState.FAILED: frozenset(),
ResearchRunState.CANCELLED: frozenset(),
}
class StateMachine:
"""Endlicher Automat für den Lebenszyklus eines Research-Runs.
Parameters
----------
initial_state : ResearchRunState
Der Startzustand der Maschine (default: CREATED).
Attributes
----------
current_state : ResearchRunState
Der aktuell eingestellte Zustand.
"""
# Die erlaubten Übergänge sind klassengebunden und typstarr.
_TRANSITIONS: ClassVar[dict[ResearchRunState, FrozenSet[ResearchRunState]]] = _VALID_TRANSITIONS
def __init__(self, initial_state: ResearchRunState = ResearchRunState.CREATED) -> None:
self.current_state: ResearchRunState = initial_state
def transition(self, new_state: str) -> bool:
"""Führt einen Zustandübergang aus, wenn er erlaubt ist.
Parameters
----------
new_state : str
Der gewünschte Zielzustand als String (z. B. ``"planning"``).
Returns
-------
bool
``True`` wenn der Übergang erfolgreich war, ``False`` sonst
(z. B. bei einem unerlaubten Übergang oder Terminal-State).
"""
target: ResearchRunState | None = None
try:
target = ResearchRunState(new_state)
except ValueError:
logger.info(
"Unknown state '%s' — transition %s -> %s rejected",
self.current_state.value,
new_state,
)
return False
return self._apply(self.current_state, target)
def is_terminal(self) -> bool:
"""Prüft, ob die Maschine sich in einem Terminal-State befindet."""
return self.current_state in (
ResearchRunState.COMPLETED,
ResearchRunState.FAILED,
ResearchRunState.CANCELLED,
)
def reset(self, state: ResearchRunState | None = None) -> None:
"""Setzt die Maschine zurück (z. B. vor einem neuen Run).
Parameters
----------
state : ResearchRunState | None
Zielzustand; default: CREATED.
"""
self.current_state = state or ResearchRunState.CREATED
def _apply(self, from_state: ResearchRunState, to_state: ResearchRunState) -> bool:
"""Interne Übergangslogik mit Logging.
Parameters
----------
from_state : ResearchRunState
Der aktuelle Zustand (zur Validierung).
to_state : ResearchRunState
Der gewünschte neue Zustand.
Returns
-------
bool
``True`` wenn der Übergang ausgeführt wurde, ``False`` sonst.
"""
allowed: FrozenSet[ResearchRunState] = _VALID_TRANSITIONS.get(from_state)
if allowed is None:
raise ValueError(
f"No outgoing transitions defined for state {from_state.value!r}"
)
# Terminal-States blockieren jeglichen Ausgang
if from_state in (
ResearchRunState.COMPLETED,
ResearchRunState.FAILED,
ResearchRunState.CANCELLED,
):
logger.info(
"Terminal state %s reached — transition %s -> %s rejected",
from_state.value,
from_state.value,
to_state.value,
)
return False
# Selber-Stehen ist kein Fehler
if from_state == to_state:
logger.info(
"No-op: state already %s — skipping transition",
from_state.value,
)
return False
if to_state not in allowed:
logger.info(
"Transition %s -> %s rejected — not allowed",
from_state.value,
to_state.value,
)
return False
# Erlaubt: tatsächlich durchführen
self.current_state = to_state # type: ignore[assignment] # instance attr
logger.info(
"Transition %s -> %s accepted",
from_state.value,
to_state.value,
)
return True