feat(stage12): implement Research Orchestrator
This commit is contained in:
206
src/nsct/orchestration/state.py
Normal file
206
src/nsct/orchestration/state.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
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.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
|
||||
Reference in New Issue
Block a user