mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
copilot: rename FixContext -> CopilotContext + additive Build fields
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
32077736ba
commit
4be0e77ade
23 files changed
+195
-148
No files matched your search
@@ -41,7 +41,7 @@ from core.workflow_copilot.models import (
|
||||
ChangeSet,
|
||||
ChecklistError,
|
||||
ConversationItem,
|
||||
FixContext,
|
||||
CopilotContext,
|
||||
NodeOutput,
|
||||
Run,
|
||||
Session,
|
||||
@@ -76,7 +76,7 @@ __all__ = [
|
||||
# ---- helpers ---------------------------------------------------------------
|
||||
|
||||
|
||||
def append_item(fc: FixContext, kind: str, payload: dict[str, Any]) -> list[ConversationItem]:
|
||||
def append_item(fc: CopilotContext, kind: str, payload: dict[str, Any]) -> list[ConversationItem]:
|
||||
"""Append a conversation item stamped with the next sequence number.
|
||||
|
||||
``fc.next_seq`` is the seq authority (port of ``handlers_fix.go:9-13``):
|
||||
@@ -90,7 +90,7 @@ def append_item(fc: FixContext, kind: str, payload: dict[str, Any]) -> list[Conv
|
||||
return [item]
|
||||
|
||||
|
||||
def append_card(fc: FixContext, card) -> list[ConversationItem]:
|
||||
def append_card(fc: CopilotContext, card) -> list[ConversationItem]:
|
||||
"""Stamp a typed card into the conversation at the next seq (mirrors
|
||||
append_item's seq authority; at_version preserved as the prior code's 0).
|
||||
"""
|
||||
@@ -123,7 +123,7 @@ def _mode_or_default(mode: str) -> str:
|
||||
# ---- handlers ---------------------------------------------------------------
|
||||
|
||||
|
||||
def handle_diagnose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_diagnose(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(working) Read the failed run's node outputs, diagnose, capture the
|
||||
pre-repair checkpoint. Port of ``handlers_fix.go:38``."""
|
||||
failed = env.repo.get_run(fc.failed_run_id)
|
||||
@@ -153,7 +153,7 @@ def handle_diagnose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepRes
|
||||
)
|
||||
|
||||
|
||||
def handle_propose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_propose(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(working) Ask the agent for repair intents + risk, stage them, branch
|
||||
on risk. Port of ``handlers_fix.go:97``. Also serves as
|
||||
``checklist.propose`` (Task 7 reuses this handler, as Go does)."""
|
||||
@@ -179,7 +179,7 @@ def handle_propose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResu
|
||||
return StepResult(next=next_state, context=fc, items=items)
|
||||
|
||||
|
||||
def handle_await_approval(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_await_approval(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(waiting) High-risk gate. Only an explicit ``approve_repair`` advances
|
||||
to ``fix.apply``; any other/absent action kind is a no-op that stays at
|
||||
the gate. Port of ``handlers_fix.go:123``."""
|
||||
@@ -193,7 +193,7 @@ def handle_await_approval(env: Env, turn: Turn, s: Session, fc: FixContext) -> S
|
||||
return StepResult(next=PcState.FIX_AWAIT_APPROVAL, context=fc)
|
||||
|
||||
|
||||
def handle_apply(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_apply(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(working) Apply the staged repair to the real draft. The next state
|
||||
branches on ``fc.source``: the run-fix path always lands at
|
||||
``fix.await_verify``; the checklist-fix path lands at
|
||||
@@ -222,7 +222,7 @@ def handle_apply(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult
|
||||
return StepResult(next=next_state, context=fc, items=items)
|
||||
|
||||
|
||||
def handle_await_verify(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_await_verify(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(waiting) The user runs verification or undoes. Port of
|
||||
``handlers_fix.go:163``."""
|
||||
if turn.action is not None and turn.action.kind == "undo":
|
||||
@@ -234,7 +234,7 @@ def handle_await_verify(env: Env, turn: Turn, s: Session, fc: FixContext) -> Ste
|
||||
return StepResult(next=PcState.FIX_VERIFY, context=fc)
|
||||
|
||||
|
||||
def handle_await_testdata(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_await_testdata(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(waiting) Prepare inputs for the verify run. Port of
|
||||
``handlers_fix.go:176``."""
|
||||
mode, _ = action_string(turn, "mode")
|
||||
@@ -254,7 +254,7 @@ def handle_await_testdata(env: Env, turn: Turn, s: Session, fc: FixContext) -> S
|
||||
return StepResult(next=PcState.FIX_VERIFY, context=fc)
|
||||
|
||||
|
||||
def handle_verify(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_verify(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(working) Run the repaired draft, mint a NEW immutable Run. Advances
|
||||
to ``fix.await_decision`` on both pass and fail; never touches the
|
||||
original failed run. Port of ``handlers_fix.go:206``."""
|
||||
@@ -304,7 +304,7 @@ def handle_verify(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResul
|
||||
)
|
||||
|
||||
|
||||
def handle_await_decision(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_await_decision(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(waiting) Terminal choice. Port of ``handlers_fix.go:239``."""
|
||||
kind = turn.action.kind if turn.action is not None else ""
|
||||
if kind == "publish":
|
||||
@@ -329,7 +329,7 @@ def handle_await_decision(env: Env, turn: Turn, s: Session, fc: FixContext) -> S
|
||||
return StepResult(next=PcState.SUCCESS, context=fc, items=items)
|
||||
|
||||
|
||||
def handle_publish(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_publish(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(working) Publish the repaired workflow. Port of
|
||||
``handlers_fix.go:316``."""
|
||||
env.dify.publish(s.app_id, turn.actor)
|
||||
@@ -340,7 +340,7 @@ def handle_publish(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResu
|
||||
# ---- checklist handlers -----------------------------------------------------
|
||||
|
||||
|
||||
def handle_checklist_diagnose(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_checklist_diagnose(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(working) Mirrors ``handle_diagnose`` but diagnoses from the
|
||||
pre-publish checklist's config errors instead of a failed run's node
|
||||
outputs — there is no ``fc.failed_run_id`` on this path, so no run
|
||||
@@ -370,7 +370,7 @@ def handle_checklist_diagnose(env: Env, turn: Turn, s: Session, fc: FixContext)
|
||||
)
|
||||
|
||||
|
||||
def handle_await_recheck(env: Env, turn: Turn, s: Session, fc: FixContext) -> StepResult:
|
||||
def handle_await_recheck(env: Env, turn: Turn, s: Session, fc: CopilotContext) -> StepResult:
|
||||
"""(waiting) The checklist-fix counterpart of ``handle_await_verify``:
|
||||
instead of running the workflow, the caller re-runs the pre-publish
|
||||
checklist and reports the result via a ``"recheck"`` action. On ``undo``
|
||||
|
||||
@@ -14,13 +14,13 @@ Deltas from the Go source (per the P1 port plan's Global Constraints / ADR):
|
||||
Go test suite constructs these structs: almost every struct literal in
|
||||
``*_test.go`` sets only a handful of fields and relies on Go's implicit
|
||||
zero-initialization for the rest (e.g. ``&Session{AppID: "app", ...}``,
|
||||
``&FixContext{}``, ``&FixContext{FailedRunID: "TR-1"}``). The excepted
|
||||
``&CopilotContext{}``, ``&CopilotContext{FailedRunID: "TR-1"}``). The excepted
|
||||
fields have no meaningful "empty" value (an empty string is not a valid
|
||||
``PcState``/``EntryMode`` member) and are always set explicitly in the Go
|
||||
suite, so they stay required here.
|
||||
- ``Diagnosis`` / ``Risk`` / ``MutationIntent`` / ``ChangeSet`` / ``NodeEvent``
|
||||
/ ``ApplyResult`` (Go: ``ports.go``) live here rather than in ``ports.py``.
|
||||
``FixContext`` embeds ``Diagnosis``/``Risk``/``ChangeSet``/
|
||||
``CopilotContext`` embeds ``Diagnosis``/``Risk``/``ChangeSet``/
|
||||
``MutationIntent`` directly, and defining them in ``ports.py`` (which
|
||||
imports the rest of these models) would create a models<->ports import
|
||||
cycle; ``ports.py`` imports them back out of this module instead.
|
||||
@@ -205,9 +205,16 @@ class ApplyResult:
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class FixContext:
|
||||
"""Per-session working state persisted as the commit context."""
|
||||
class CopilotContext:
|
||||
"""Per-session working state persisted as the commit context.
|
||||
|
||||
One context type for all entry modes (Fix, checklist, Build, Edit).
|
||||
Fix-specific and Build-specific fields coexist; each scenario touches
|
||||
only its own, all default-valued (serialized as a JSON blob -- no
|
||||
migration on rename or on adding fields).
|
||||
"""
|
||||
|
||||
# -- Fix / checklist fields --
|
||||
failed_run_id: str = ""
|
||||
diagnosis: Diagnosis | None = None
|
||||
staged_repair: list[MutationIntent] = field(default_factory=list)
|
||||
@@ -222,6 +229,14 @@ class FixContext:
|
||||
source: str = ""
|
||||
checklist_errors: list[ChecklistError] = field(default_factory=list)
|
||||
|
||||
# -- Build fields (Slice 2, additive) --
|
||||
goal_text: str = ""
|
||||
requirements: dict[str, Any] = field(default_factory=dict)
|
||||
plan_items: list[str] = field(default_factory=list)
|
||||
plan_version_tag: str = ""
|
||||
resource_selection: dict[str, Any] = field(default_factory=dict)
|
||||
built_node_ids: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Action:
|
||||
|
||||
@@ -11,7 +11,7 @@ Deltas from the Go source (per the P1 port plan's Global Constraints / ADR):
|
||||
- The value types Go defines in this file (``Diagnosis``, ``Risk``,
|
||||
``MutationIntent``, ``ChangeSet``, ``NodeEvent``, ``ApplyResult``) live in
|
||||
``models.py`` instead, alongside the rest of the domain dataclasses —
|
||||
``FixContext`` embeds several of them directly, and defining them here
|
||||
``CopilotContext`` embeds several of them directly, and defining them here
|
||||
(where the models are imported) would create a models<->ports import
|
||||
cycle. They are re-exported from here for convenience.
|
||||
- ``interface`` becomes ``typing.Protocol``, marked ``@runtime_checkable`` so
|
||||
@@ -29,8 +29,8 @@ from core.workflow_copilot.models import (
|
||||
ChecklistError,
|
||||
Checkpoint,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
Diagnosis,
|
||||
FixContext,
|
||||
Graph,
|
||||
Inputs,
|
||||
MutationIntent,
|
||||
@@ -109,16 +109,16 @@ class Repository(Protocol):
|
||||
``base_version``, else raises ``ConflictError``.
|
||||
"""
|
||||
|
||||
def create_session(self, session: Session, initial_fc: FixContext, items: list[ConversationItem]) -> None: ...
|
||||
def create_session(self, session: Session, initial_fc: CopilotContext, items: list[ConversationItem]) -> None: ...
|
||||
|
||||
def get_session(self, id: str) -> tuple[Session, FixContext]: ...
|
||||
def get_session(self, id: str) -> tuple[Session, CopilotContext]: ...
|
||||
|
||||
def compare_and_advance(
|
||||
self,
|
||||
session_id: str,
|
||||
base_version: int,
|
||||
next: PcState,
|
||||
fc: FixContext,
|
||||
fc: CopilotContext,
|
||||
items: list[ConversationItem],
|
||||
) -> int: ...
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from core.workflow_copilot.errors import ConflictError
|
||||
from core.workflow_copilot.models import (
|
||||
Checkpoint,
|
||||
ConversationItem,
|
||||
FixContext,
|
||||
CopilotContext,
|
||||
NodeEvent,
|
||||
Run,
|
||||
Session,
|
||||
@@ -61,14 +61,14 @@ class Env:
|
||||
@dataclass
|
||||
class StepResult:
|
||||
next: PcState
|
||||
context: FixContext | None = None
|
||||
context: CopilotContext | None = None
|
||||
items: list[ConversationItem] = field(default_factory=list)
|
||||
checkpoint: Snapshot | None = None # if set, persist a checkpoint from this snapshot before advancing
|
||||
run: Run | None = None # if set, persist this run before advancing
|
||||
run_id_sink: list[str] | None = None # internal: runner writes the saved run id to run_id_sink[0]
|
||||
|
||||
|
||||
Handler = Callable[[Env, Turn, Session, FixContext], StepResult]
|
||||
Handler = Callable[[Env, Turn, Session, CopilotContext], StepResult]
|
||||
|
||||
|
||||
class Runner:
|
||||
|
||||
@@ -46,7 +46,7 @@ class CopilotSessionCommit(Base):
|
||||
Copilot Session Commit Model
|
||||
|
||||
One immutable row per session version, carrying the serialized
|
||||
`FixContext` (`context`) as of that version. The latest commit by
|
||||
`CopilotContext` (`context`) as of that version. The latest commit by
|
||||
`version` for a given `session_id` is the session's current context.
|
||||
"""
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ from core.workflow_copilot.errors import ConflictError, NotFoundError
|
||||
from core.workflow_copilot.models import (
|
||||
Checkpoint,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
EntryMode,
|
||||
FixContext,
|
||||
NodeOutput,
|
||||
Run,
|
||||
Snapshot,
|
||||
@@ -50,7 +50,7 @@ from models.workflow_copilot import (
|
||||
CopilotSnapshot,
|
||||
CopilotTestInput,
|
||||
)
|
||||
from services.workflow_copilot.serde import fix_context_from_dict, fix_context_to_dict
|
||||
from services.workflow_copilot.serde import context_from_dict, context_to_dict
|
||||
|
||||
__all__ = ["SqlCopilotRepository"]
|
||||
|
||||
@@ -63,7 +63,7 @@ class SqlCopilotRepository:
|
||||
|
||||
# -- sessions --
|
||||
|
||||
def create_session(self, session: DomainSession, initial_fc: FixContext, items: list[ConversationItem]) -> None:
|
||||
def create_session(self, session: DomainSession, initial_fc: CopilotContext, items: list[ConversationItem]) -> None:
|
||||
with self._session_factory() as db_session, db_session.begin():
|
||||
row = CopilotSession(
|
||||
app_id=session.app_id,
|
||||
@@ -93,12 +93,12 @@ class SqlCopilotRepository:
|
||||
session_id=row.id,
|
||||
version=1,
|
||||
state=str(session.current_state),
|
||||
context=fix_context_to_dict(initial_fc),
|
||||
context=context_to_dict(initial_fc),
|
||||
actor="",
|
||||
)
|
||||
)
|
||||
|
||||
def get_session(self, id: str) -> tuple[DomainSession, FixContext]:
|
||||
def get_session(self, id: str) -> tuple[DomainSession, CopilotContext]:
|
||||
with self._session_factory() as db_session, db_session.begin():
|
||||
row = db_session.get(CopilotSession, id)
|
||||
if row is None:
|
||||
@@ -114,7 +114,7 @@ class SqlCopilotRepository:
|
||||
if commit is None:
|
||||
raise NotFoundError(f"no commits found for session {id}")
|
||||
|
||||
fc = fix_context_from_dict(commit.context)
|
||||
fc = context_from_dict(commit.context)
|
||||
return self._to_domain_session(row), fc
|
||||
|
||||
def compare_and_advance(
|
||||
@@ -122,7 +122,7 @@ class SqlCopilotRepository:
|
||||
session_id: str,
|
||||
base_version: int,
|
||||
next: PcState,
|
||||
fc: FixContext,
|
||||
fc: CopilotContext,
|
||||
items: list[ConversationItem],
|
||||
) -> int:
|
||||
new_version = base_version + 1
|
||||
@@ -145,7 +145,7 @@ class SqlCopilotRepository:
|
||||
session_id=session_id,
|
||||
version=new_version,
|
||||
state=str(next),
|
||||
context=fix_context_to_dict(fc),
|
||||
context=context_to_dict(fc),
|
||||
actor="",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ def map_run_result(data: Mapping[str, Any], node_execs: Sequence[Any]) -> Run:
|
||||
"""Map a blocking ``AppGenerateService.generate`` result into a ``Run``.
|
||||
|
||||
``Run.id`` is deliberately left ``""`` — the caller (``handle_verify``)
|
||||
assigns the id and sets ``FixContext.verify_run_id``, not this function.
|
||||
assigns the id and sets ``CopilotContext.verify_run_id``, not this function.
|
||||
"""
|
||||
if _is_paused_shape(data):
|
||||
status = "failed"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Pure JSON (de)serialization for the domain ``FixContext``.
|
||||
"""Pure JSON (de)serialization for the domain ``CopilotContext``.
|
||||
|
||||
``FixContext`` (see ``core.workflow_copilot.models``) is the per-session
|
||||
``CopilotContext`` (see ``core.workflow_copilot.models``) is the per-session
|
||||
working state persisted, one row per version, as the ``context`` JSONB
|
||||
column on ``workflow_copilot_session_commits`` (P3a Task 4). This module
|
||||
has no I/O and no SQLAlchemy dependency -- it only converts between the
|
||||
dataclass and a JSON-safe ``dict``, so the SQL repository can hand the dict
|
||||
straight to an ``AdjustedJSON`` column and get it back unchanged.
|
||||
column on ``workflow_copilot_session_commits``. This module has no I/O and
|
||||
no SQLAlchemy dependency -- it only converts between the dataclass and a
|
||||
JSON-safe ``dict``, so the SQL repository can hand the dict straight to an
|
||||
``AdjustedJSON`` column and get it back unchanged. The on-disk key names are
|
||||
unchanged from the pre-rename dataclass form so existing rows deserialize
|
||||
as-is.
|
||||
"""
|
||||
|
||||
from dataclasses import asdict
|
||||
@@ -14,34 +16,31 @@ from typing import Any
|
||||
from core.workflow_copilot.models import (
|
||||
ChangeSet,
|
||||
ChecklistError,
|
||||
CopilotContext,
|
||||
Diagnosis,
|
||||
FixContext,
|
||||
MutationIntent,
|
||||
Risk,
|
||||
)
|
||||
|
||||
|
||||
def fix_context_to_dict(fc: FixContext) -> dict[str, Any]:
|
||||
"""Serialize a ``FixContext`` to a JSON-safe dict.
|
||||
def context_to_dict(fc: CopilotContext) -> dict[str, Any]:
|
||||
"""Serialize a ``CopilotContext`` to a JSON-safe dict.
|
||||
|
||||
``dataclasses.asdict`` recurses into the nested dataclasses
|
||||
(``Diagnosis``, ``MutationIntent``, ``Risk``, ``ChangeSet``,
|
||||
``ChecklistError``), turning each into a plain dict, and every field on
|
||||
``FixContext`` and its nested dataclasses is already a
|
||||
``ChecklistError``), turning each into a plain dict; every field is a
|
||||
str/int/bool/list/dict/None -- so the result is JSON-safe as-is.
|
||||
"""
|
||||
return asdict(fc)
|
||||
|
||||
|
||||
def fix_context_from_dict(d: dict[str, Any]) -> FixContext:
|
||||
"""Rebuild a ``FixContext`` from a dict produced by ``fix_context_to_dict``.
|
||||
def context_from_dict(d: dict[str, Any]) -> CopilotContext:
|
||||
"""Rebuild a ``CopilotContext`` from a dict produced by ``context_to_dict``.
|
||||
|
||||
Reconstructs the nested dataclasses explicitly rather than splatting
|
||||
``d`` into ``FixContext(**d)`` -- the nested values are plain dicts (and
|
||||
lists of dicts) after a JSON round-trip and must become the right
|
||||
dataclass instances. Every lookup is defensive (``.get`` with a default
|
||||
matching the ``FixContext``/nested-dataclass field default) so an
|
||||
older or partial dict still yields a valid ``FixContext``.
|
||||
Reconstructs the nested dataclasses explicitly (they are plain dicts
|
||||
after a JSON round-trip). Every lookup is defensive (``.get`` with a
|
||||
default matching the field default) so an older or partial dict still
|
||||
yields a valid ``CopilotContext``.
|
||||
"""
|
||||
diagnosis = Diagnosis(**d["diagnosis"]) if d.get("diagnosis") else None
|
||||
staged_repair = [MutationIntent(**m) for m in d.get("staged_repair", [])]
|
||||
@@ -49,7 +48,7 @@ def fix_context_from_dict(d: dict[str, Any]) -> FixContext:
|
||||
change_set = ChangeSet(**d["change_set"]) if d.get("change_set") else None
|
||||
checklist_errors = [ChecklistError(**e) for e in d.get("checklist_errors", [])]
|
||||
|
||||
return FixContext(
|
||||
return CopilotContext(
|
||||
failed_run_id=d.get("failed_run_id", ""),
|
||||
diagnosis=diagnosis,
|
||||
staged_repair=staged_repair,
|
||||
@@ -62,4 +61,10 @@ def fix_context_from_dict(d: dict[str, Any]) -> FixContext:
|
||||
next_seq=d.get("next_seq", 0),
|
||||
source=d.get("source", ""),
|
||||
checklist_errors=checklist_errors,
|
||||
goal_text=d.get("goal_text", ""),
|
||||
requirements=dict(d.get("requirements") or {}),
|
||||
plan_items=list(d.get("plan_items") or []),
|
||||
plan_version_tag=d.get("plan_version_tag", ""),
|
||||
resource_selection=dict(d.get("resource_selection") or {}),
|
||||
built_node_ids=list(d.get("built_node_ids") or []),
|
||||
)
|
||||
@@ -21,8 +21,8 @@ from core.workflow_copilot.models import (
|
||||
Actor,
|
||||
ChecklistError,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
EntryMode,
|
||||
FixContext,
|
||||
Run,
|
||||
Session,
|
||||
)
|
||||
@@ -223,7 +223,7 @@ class WorkflowCopilotService:
|
||||
failed_run: Run | None = None
|
||||
if checklist_errors:
|
||||
entry_mode, state = EntryMode.FIX_CHECKLIST, PcState.CHECKLIST_DIAGNOSE
|
||||
fc = FixContext(source="checklist", checklist_errors=checklist_errors)
|
||||
fc = CopilotContext(source="checklist", checklist_errors=checklist_errors)
|
||||
else:
|
||||
entry_mode, state = EntryMode.FIX, PcState.FIX_DIAGNOSE
|
||||
failed_run = Run(
|
||||
@@ -233,7 +233,7 @@ class WorkflowCopilotService:
|
||||
status="failed",
|
||||
immutable=True,
|
||||
)
|
||||
fc = FixContext(failed_run_id=failed_run.id, source="run")
|
||||
fc = CopilotContext(failed_run_id=failed_run.id, source="run")
|
||||
|
||||
s = Session(
|
||||
app_id=app_id,
|
||||
|
||||
@@ -42,8 +42,8 @@ from core.workflow_copilot.models import (
|
||||
ChecklistError,
|
||||
Checkpoint,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
Diagnosis,
|
||||
FixContext,
|
||||
Graph,
|
||||
Inputs,
|
||||
MutationIntent,
|
||||
@@ -75,7 +75,7 @@ class InMemoryRepository:
|
||||
def __init__(self) -> None:
|
||||
self._lock = threading.Lock()
|
||||
self._sessions: dict[str, Session] = {}
|
||||
self._contexts: dict[str, FixContext] = {}
|
||||
self._contexts: dict[str, CopilotContext] = {}
|
||||
self._commits: dict[str, list[int]] = {}
|
||||
self._checkpoints: dict[str, Checkpoint] = {}
|
||||
self._snapshots: dict[str, Snapshot] = {}
|
||||
@@ -86,7 +86,7 @@ class InMemoryRepository:
|
||||
|
||||
# -- sessions --
|
||||
|
||||
def create_session(self, session: Session, initial_fc: FixContext, items: list[ConversationItem]) -> None:
|
||||
def create_session(self, session: Session, initial_fc: CopilotContext, items: list[ConversationItem]) -> None:
|
||||
with self._lock:
|
||||
if not session.id:
|
||||
session.id = str(uuid.uuid4())
|
||||
@@ -109,7 +109,7 @@ class InMemoryRepository:
|
||||
self._items[session.id] = copy.deepcopy(items)
|
||||
self._used_seqs[session.id] = seqs
|
||||
|
||||
def get_session(self, id: str) -> tuple[Session, FixContext]:
|
||||
def get_session(self, id: str) -> tuple[Session, CopilotContext]:
|
||||
with self._lock:
|
||||
session = self._sessions.get(id)
|
||||
if session is None:
|
||||
@@ -121,7 +121,7 @@ class InMemoryRepository:
|
||||
session_id: str,
|
||||
base_version: int,
|
||||
next: PcState,
|
||||
fc: FixContext,
|
||||
fc: CopilotContext,
|
||||
items: list[ConversationItem],
|
||||
) -> int:
|
||||
with self._lock:
|
||||
|
||||
@@ -16,9 +16,9 @@ from core.workflow_copilot.models import (
|
||||
Actor,
|
||||
Checkpoint,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
Diagnosis,
|
||||
EntryMode,
|
||||
FixContext,
|
||||
MutationIntent,
|
||||
Run,
|
||||
Session,
|
||||
@@ -62,7 +62,7 @@ def test_create_session_assigns_id_and_starts_at_version_1():
|
||||
repo = InMemoryRepository()
|
||||
s = _session()
|
||||
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
assert s.id != ""
|
||||
assert s.version == 1
|
||||
@@ -75,7 +75,7 @@ def test_create_session_assigns_id_and_starts_at_version_1():
|
||||
def test_create_session_sets_next_seq_to_seed_item_count():
|
||||
repo = InMemoryRepository()
|
||||
s = _session()
|
||||
fc = FixContext()
|
||||
fc = CopilotContext()
|
||||
items = [
|
||||
ConversationItem(seq=0, kind="run-context", at_version=1),
|
||||
ConversationItem(seq=1, kind="user", at_version=1),
|
||||
@@ -93,7 +93,7 @@ def test_create_session_sets_next_seq_to_seed_item_count():
|
||||
def test_create_session_does_not_downgrade_an_already_larger_next_seq():
|
||||
repo = InMemoryRepository()
|
||||
s = _session()
|
||||
fc = FixContext(next_seq=5)
|
||||
fc = CopilotContext(next_seq=5)
|
||||
|
||||
repo.create_session(s, fc, [])
|
||||
|
||||
@@ -109,7 +109,7 @@ def test_create_session_rejects_duplicate_seq_within_seed_items():
|
||||
]
|
||||
|
||||
with pytest.raises(ConflictError):
|
||||
repo.create_session(s, FixContext(), items)
|
||||
repo.create_session(s, CopilotContext(), items)
|
||||
|
||||
|
||||
# ---- compare_and_advance: version CAS -------------------------------------
|
||||
@@ -118,10 +118,10 @@ def test_create_session_rejects_duplicate_seq_within_seed_items():
|
||||
def test_compare_and_advance_rejects_stale_base_version():
|
||||
repo = InMemoryRepository()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
with pytest.raises(ConflictError):
|
||||
repo.compare_and_advance(s.id, 0, PcState.FIX_PROPOSE, FixContext(), [])
|
||||
repo.compare_and_advance(s.id, 0, PcState.FIX_PROPOSE, CopilotContext(), [])
|
||||
|
||||
# Nothing applied.
|
||||
stored, _ = repo.get_session(s.id)
|
||||
@@ -132,13 +132,13 @@ def test_compare_and_advance_rejects_stale_base_version():
|
||||
def test_compare_and_advance_accepts_current_version_and_bumps_it():
|
||||
repo = InMemoryRepository()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
new_version = repo.compare_and_advance(
|
||||
s.id,
|
||||
1,
|
||||
PcState.FIX_PROPOSE,
|
||||
FixContext(diagnosis=Diagnosis(culprit_node_id="n1")),
|
||||
CopilotContext(diagnosis=Diagnosis(culprit_node_id="n1")),
|
||||
[],
|
||||
)
|
||||
|
||||
@@ -154,15 +154,15 @@ def test_compare_and_advance_on_unknown_session_raises_not_found():
|
||||
repo = InMemoryRepository()
|
||||
|
||||
with pytest.raises(NotFoundError):
|
||||
repo.compare_and_advance("nope", 1, PcState.FIX_PROPOSE, FixContext(), [])
|
||||
repo.compare_and_advance("nope", 1, PcState.FIX_PROPOSE, CopilotContext(), [])
|
||||
|
||||
|
||||
def test_compare_and_advance_deep_copies_the_stored_context():
|
||||
repo = InMemoryRepository()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
fc = FixContext(staged_repair=[MutationIntent(op="set_node_config")])
|
||||
fc = CopilotContext(staged_repair=[MutationIntent(op="set_node_config")])
|
||||
repo.compare_and_advance(s.id, 1, PcState.FIX_APPLY, fc, [])
|
||||
|
||||
# Mutating the caller's fc after the commit must not affect stored state.
|
||||
@@ -179,7 +179,7 @@ def test_compare_and_advance_deep_copies_the_stored_context():
|
||||
def test_compare_and_advance_rejects_duplicate_seq_within_one_batch():
|
||||
repo = InMemoryRepository()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
dup_items = [
|
||||
ConversationItem(seq=0, kind="a", at_version=2),
|
||||
@@ -187,7 +187,7 @@ def test_compare_and_advance_rejects_duplicate_seq_within_one_batch():
|
||||
]
|
||||
|
||||
with pytest.raises(ConflictError):
|
||||
repo.compare_and_advance(s.id, 1, PcState.FIX_PROPOSE, FixContext(), dup_items)
|
||||
repo.compare_and_advance(s.id, 1, PcState.FIX_PROPOSE, CopilotContext(), dup_items)
|
||||
|
||||
# Nothing applied: neither version nor conversation items advanced.
|
||||
stored, _ = repo.get_session(s.id)
|
||||
@@ -198,14 +198,14 @@ def test_compare_and_advance_rejects_duplicate_seq_within_one_batch():
|
||||
def test_compare_and_advance_rejects_seq_reused_from_a_previous_commit():
|
||||
repo = InMemoryRepository()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [ConversationItem(seq=0, kind="run-context", at_version=1)])
|
||||
repo.create_session(s, CopilotContext(), [ConversationItem(seq=0, kind="run-context", at_version=1)])
|
||||
|
||||
with pytest.raises(ConflictError):
|
||||
repo.compare_and_advance(
|
||||
s.id,
|
||||
1,
|
||||
PcState.FIX_PROPOSE,
|
||||
FixContext(),
|
||||
CopilotContext(),
|
||||
[ConversationItem(seq=0, kind="user", at_version=2)],
|
||||
)
|
||||
|
||||
@@ -217,13 +217,13 @@ def test_compare_and_advance_rejects_seq_reused_from_a_previous_commit():
|
||||
def test_compare_and_advance_accepts_the_next_seq_in_order():
|
||||
repo = InMemoryRepository()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [ConversationItem(seq=0, kind="run-context", at_version=1)])
|
||||
repo.create_session(s, CopilotContext(), [ConversationItem(seq=0, kind="run-context", at_version=1)])
|
||||
|
||||
repo.compare_and_advance(
|
||||
s.id,
|
||||
1,
|
||||
PcState.FIX_PROPOSE,
|
||||
FixContext(),
|
||||
CopilotContext(),
|
||||
[ConversationItem(seq=1, kind="user", at_version=2)],
|
||||
)
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ from core.workflow_copilot.models import (
|
||||
Actor,
|
||||
ChecklistError,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
EntryMode,
|
||||
FixContext,
|
||||
Run,
|
||||
Session,
|
||||
Turn,
|
||||
@@ -66,12 +66,12 @@ def _fix_session(**overrides) -> Session:
|
||||
|
||||
def _seed_fix_session(repo: InMemoryRepository) -> Session:
|
||||
"""Seed a session at fix.diagnose with a run-context item and a
|
||||
FixContext pointing at a pre-existing failed run. The Fix flow diagnoses
|
||||
CopilotContext pointing at a pre-existing failed run. The Fix flow diagnoses
|
||||
from this already-ingested "red" run; it never re-runs it."""
|
||||
s = _fix_session()
|
||||
repo.create_session(
|
||||
s,
|
||||
FixContext(failed_run_id="TR-1"),
|
||||
CopilotContext(failed_run_id="TR-1"),
|
||||
[ConversationItem(kind="run-context", seq=0, payload={"failed_run_id": "TR-1"})],
|
||||
)
|
||||
repo.save_run(s.id, Run(id="TR-1", kind="original-failed", status="failed", immutable=True))
|
||||
@@ -219,7 +219,7 @@ def _seed_checklist_session(repo: InMemoryRepository, errors: list[ChecklistErro
|
||||
s = _fix_session(entry_mode=EntryMode.FIX_CHECKLIST, current_state=PcState.CHECKLIST_DIAGNOSE)
|
||||
repo.create_session(
|
||||
s,
|
||||
FixContext(source="checklist", checklist_errors=errors),
|
||||
CopilotContext(source="checklist", checklist_errors=errors),
|
||||
[ConversationItem(kind="run-context", seq=0)],
|
||||
)
|
||||
return s
|
||||
|
||||
@@ -24,8 +24,8 @@ from core.workflow_copilot.models import (
|
||||
Actor,
|
||||
ChecklistError,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
EntryMode,
|
||||
FixContext,
|
||||
Run,
|
||||
Session,
|
||||
Turn,
|
||||
@@ -81,7 +81,7 @@ def _run_fix_registry() -> dict[PcState, object]:
|
||||
|
||||
def _seed_diagnose_session(repo: InMemoryRepository) -> Session:
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
|
||||
repo.create_session(s, CopilotContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
|
||||
repo.save_run(s.id, Run(id="TR-1", kind="original-failed", status="failed", immutable=True))
|
||||
return s
|
||||
|
||||
@@ -374,7 +374,7 @@ def _seed_checklist_session(repo: InMemoryRepository, errors: list[ChecklistErro
|
||||
s = _session(entry_mode=EntryMode.FIX_CHECKLIST, current_state=PcState.CHECKLIST_DIAGNOSE)
|
||||
repo.create_session(
|
||||
s,
|
||||
FixContext(source="checklist", checklist_errors=errors),
|
||||
CopilotContext(source="checklist", checklist_errors=errors),
|
||||
[ConversationItem(kind="run-context", seq=0)],
|
||||
)
|
||||
return s
|
||||
|
||||
@@ -6,7 +6,7 @@ these structs (see e.g. handlers_fix_test.go, runner_test.go): almost every
|
||||
struct literal in that suite sets only a handful of fields and relies on
|
||||
Go's implicit zero-initialization for the rest (``&Session{AppID: "app",
|
||||
TenantID: "t", OwnerAccountID: "u", EntryMode: EntryModeFix, CurrentState:
|
||||
FixDiagnose}``, ``&FixContext{}``, ``&FixContext{FailedRunID: "TR-1"}``).
|
||||
FixDiagnose}``, ``&CopilotContext{}``, ``&CopilotContext{FailedRunID: "TR-1"}``).
|
||||
These tests port that same terse-construction ergonomics and assert the
|
||||
Python defaults match the Go zero values field-for-field.
|
||||
"""
|
||||
@@ -19,8 +19,8 @@ from core.workflow_copilot.models import (
|
||||
ChecklistError,
|
||||
Checkpoint,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
EntryMode,
|
||||
FixContext,
|
||||
NodeOutput,
|
||||
Run,
|
||||
Session,
|
||||
@@ -162,8 +162,8 @@ def test_conversation_item_defaults():
|
||||
|
||||
|
||||
def test_fix_context_round_trip_defaults_empty():
|
||||
# Mirrors runner_test.go: repo.CreateSession(..., &FixContext{}, nil)
|
||||
fc = FixContext()
|
||||
# Mirrors runner_test.go: repo.CreateSession(..., &CopilotContext{}, nil)
|
||||
fc = CopilotContext()
|
||||
assert fc.failed_run_id == ""
|
||||
assert fc.diagnosis is None
|
||||
assert fc.staged_repair == []
|
||||
@@ -179,14 +179,14 @@ def test_fix_context_round_trip_defaults_empty():
|
||||
|
||||
|
||||
def test_fix_context_partial_construction():
|
||||
# Mirrors handlers_fix_test.go: &FixContext{FailedRunID: "TR-1"}
|
||||
fc = FixContext(failed_run_id="TR-1")
|
||||
# Mirrors handlers_fix_test.go: &CopilotContext{FailedRunID: "TR-1"}
|
||||
fc = CopilotContext(failed_run_id="TR-1")
|
||||
assert fc.failed_run_id == "TR-1"
|
||||
assert fc.source == ""
|
||||
|
||||
# Mirrors handlers_fix_test.go: &FixContext{Source: "checklist", ChecklistErrors: errs}
|
||||
# Mirrors handlers_fix_test.go: &CopilotContext{Source: "checklist", ChecklistErrors: errs}
|
||||
errs = [ChecklistError(node_id="n1", unconnected=True)]
|
||||
fc2 = FixContext(source="checklist", checklist_errors=errs)
|
||||
fc2 = CopilotContext(source="checklist", checklist_errors=errs)
|
||||
assert fc2.source == "checklist"
|
||||
assert fc2.checklist_errors == errs
|
||||
assert fc2.failed_run_id == ""
|
||||
|
||||
@@ -14,8 +14,8 @@ from core.workflow_copilot.models import (
|
||||
Actor,
|
||||
ApplyResult,
|
||||
ChangeSet,
|
||||
CopilotContext,
|
||||
Diagnosis,
|
||||
FixContext,
|
||||
MutationIntent,
|
||||
NodeEvent,
|
||||
Risk,
|
||||
@@ -86,7 +86,7 @@ def test_apply_result_defaults_and_changed_nodes_not_shared():
|
||||
|
||||
|
||||
def test_fix_context_embeds_the_tightened_value_types():
|
||||
fc = FixContext(
|
||||
fc = CopilotContext(
|
||||
diagnosis=Diagnosis(culprit_node_id="n1"),
|
||||
staged_repair=[MutationIntent(op="set_node_config")],
|
||||
risk=Risk(level="low"),
|
||||
|
||||
@@ -10,7 +10,7 @@ from datetime import datetime
|
||||
import pytest
|
||||
|
||||
from core.workflow_copilot.errors import ConflictError
|
||||
from core.workflow_copilot.models import Action, Actor, EntryMode, FixContext, Session, Turn
|
||||
from core.workflow_copilot.models import Action, Actor, CopilotContext, EntryMode, Session, Turn
|
||||
from core.workflow_copilot.runner import Env, Runner, StepResult
|
||||
from core.workflow_copilot.state import PcState
|
||||
from tests.unit_tests.core.workflow_copilot.fakes import FakeDifyPort, InMemoryRepository, StubAgent
|
||||
@@ -36,7 +36,7 @@ def _toy_registry() -> dict[PcState, object]:
|
||||
"""A working -> waiting toy machine: FIX_DIAGNOSE auto-advances to
|
||||
FIX_AWAIT_APPROVAL (a waiting state), where the loop stops."""
|
||||
|
||||
def diagnose(_env: Env, _turn: Turn, _s: Session, fc: FixContext) -> StepResult:
|
||||
def diagnose(_env: Env, _turn: Turn, _s: Session, fc: CopilotContext) -> StepResult:
|
||||
return StepResult(next=PcState.FIX_AWAIT_APPROVAL, context=fc)
|
||||
|
||||
return {PcState.FIX_DIAGNOSE: diagnose}
|
||||
@@ -56,7 +56,7 @@ def _new_env() -> tuple[Env, InMemoryRepository]:
|
||||
def test_advance_commits_each_transition_and_stops_at_waiting():
|
||||
env, repo = _new_env()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
runner = Runner(env, _toy_registry())
|
||||
turn = Turn(action=Action(kind="request_fix", base_version=1), actor=_actor())
|
||||
@@ -73,7 +73,7 @@ def test_advance_commits_each_transition_and_stops_at_waiting():
|
||||
def test_advance_stale_base_version_raises_conflict_with_nothing_applied():
|
||||
env, repo = _new_env()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
runner = Runner(env, _toy_registry())
|
||||
turn = Turn(action=Action(kind="request_fix", base_version=99), actor=_actor())
|
||||
@@ -90,7 +90,7 @@ def test_advance_stale_base_version_raises_conflict_with_nothing_applied():
|
||||
def test_advance_missing_handler_raises():
|
||||
env, repo = _new_env()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
runner = Runner(env, {}) # empty registry: no handler for FIX_DIAGNOSE
|
||||
turn = Turn(action=Action(kind="request_fix", base_version=1), actor=_actor())
|
||||
@@ -102,7 +102,7 @@ def test_advance_missing_handler_raises():
|
||||
def test_advance_missing_handler_error_message_names_the_state():
|
||||
env, repo = _new_env()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
runner = Runner(env, {})
|
||||
turn = Turn(action=Action(kind="request_fix", base_version=1), actor=_actor())
|
||||
@@ -114,7 +114,7 @@ def test_advance_missing_handler_error_message_names_the_state():
|
||||
def test_advance_waiting_state_first_turn_no_action_returns_session_unchanged():
|
||||
env, repo = _new_env()
|
||||
s = _session(current_state=PcState.FIX_AWAIT_APPROVAL)
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
runner = Runner(env, {}) # never consulted: loop returns before handler lookup
|
||||
turn = Turn(actor=_actor()) # no action
|
||||
@@ -127,7 +127,7 @@ def test_advance_waiting_state_first_turn_no_action_returns_session_unchanged():
|
||||
def test_advance_terminal_state_returns_session_unchanged():
|
||||
env, repo = _new_env()
|
||||
s = _session(current_state=PcState.SUCCESS)
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
runner = Runner(env, {}) # never consulted: terminal short-circuits first
|
||||
turn = Turn(actor=_actor())
|
||||
@@ -143,15 +143,15 @@ def test_advance_passes_full_turn_to_first_step_and_actor_only_turn_to_subsequen
|
||||
handler must see a Turn with Action consumed (None) but the same Actor."""
|
||||
env, repo = _new_env()
|
||||
s = _session()
|
||||
repo.create_session(s, FixContext(), [])
|
||||
repo.create_session(s, CopilotContext(), [])
|
||||
|
||||
seen_turns: list[Turn] = []
|
||||
|
||||
def diagnose(_env: Env, turn: Turn, _s: Session, fc: FixContext) -> StepResult:
|
||||
def diagnose(_env: Env, turn: Turn, _s: Session, fc: CopilotContext) -> StepResult:
|
||||
seen_turns.append(turn)
|
||||
return StepResult(next=PcState.FIX_PROPOSE, context=fc)
|
||||
|
||||
def propose(_env: Env, turn: Turn, _s: Session, fc: FixContext) -> StepResult:
|
||||
def propose(_env: Env, turn: Turn, _s: Session, fc: CopilotContext) -> StepResult:
|
||||
seen_turns.append(turn)
|
||||
return StepResult(next=PcState.FIX_AWAIT_APPROVAL, context=fc)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
import tasks.workflow_copilot_advance_task as mod
|
||||
from core.workflow_copilot.models import ConversationItem, EntryMode, FixContext, Run, Session
|
||||
from core.workflow_copilot.models import ConversationItem, CopilotContext, EntryMode, Run, Session
|
||||
from core.workflow_copilot.state import PcState
|
||||
from models.base import Base
|
||||
from services.workflow_copilot.repository import SqlCopilotRepository
|
||||
@@ -55,7 +55,7 @@ def _seed_fix_session(repo: SqlCopilotRepository) -> Session:
|
||||
)
|
||||
repo.create_session(
|
||||
s,
|
||||
FixContext(failed_run_id="TR-1"),
|
||||
CopilotContext(failed_run_id="TR-1"),
|
||||
[ConversationItem(kind="run-context", seq=0)],
|
||||
)
|
||||
repo.save_run(s.id, Run(id="TR-1", kind="original-failed", status="failed", immutable=True))
|
||||
|
||||
@@ -29,7 +29,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
|
||||
import tasks.workflow_copilot_advance_task as task_mod
|
||||
from core.workflow_copilot.errors import BusyError
|
||||
from core.workflow_copilot.models import Action, Actor, ConversationItem, EntryMode, FixContext, Run, Session
|
||||
from core.workflow_copilot.models import Action, Actor, ConversationItem, CopilotContext, EntryMode, Run, Session
|
||||
from core.workflow_copilot.state import PcState
|
||||
from models.base import Base
|
||||
from services.workflow_copilot import session_lock
|
||||
@@ -170,7 +170,7 @@ def test_submit_action_while_lock_held_raises_busy_eager_async(
|
||||
entry_mode=EntryMode.FIX,
|
||||
current_state=PcState.FIX_DIAGNOSE,
|
||||
)
|
||||
repo.create_session(s, FixContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
|
||||
repo.create_session(s, CopilotContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
|
||||
|
||||
# Manually hold the lock, simulating an in-flight advance on another
|
||||
# worker.
|
||||
|
||||
@@ -28,8 +28,8 @@ from core.workflow_copilot.models import (
|
||||
Actor,
|
||||
ChecklistError,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
EntryMode,
|
||||
FixContext,
|
||||
Run,
|
||||
Session,
|
||||
Turn,
|
||||
@@ -87,13 +87,13 @@ def _fix_session(**overrides) -> Session:
|
||||
|
||||
def _seed_fix_session(repo: SqlCopilotRepository) -> Session:
|
||||
"""Seed a session at fix.diagnose with a run-context item and a
|
||||
FixContext pointing at a pre-existing failed run -- mirrors the P1
|
||||
CopilotContext pointing at a pre-existing failed run -- mirrors the P1
|
||||
full-flow acceptance's seed, persisted through the SQL repo instead of
|
||||
the in-memory fake."""
|
||||
s = _fix_session()
|
||||
repo.create_session(
|
||||
s,
|
||||
FixContext(failed_run_id="TR-1"),
|
||||
CopilotContext(failed_run_id="TR-1"),
|
||||
[ConversationItem(kind="run-context", seq=0, payload={"failed_run_id": "TR-1"})],
|
||||
)
|
||||
repo.save_run(s.id, Run(id="TR-1", kind="original-failed", status="failed", immutable=True))
|
||||
@@ -244,7 +244,7 @@ def _seed_checklist_session(repo: SqlCopilotRepository, errors: list[ChecklistEr
|
||||
s = _fix_session(entry_mode=EntryMode.FIX_CHECKLIST, current_state=PcState.CHECKLIST_DIAGNOSE)
|
||||
repo.create_session(
|
||||
s,
|
||||
FixContext(source="checklist", checklist_errors=errors),
|
||||
CopilotContext(source="checklist", checklist_errors=errors),
|
||||
[ConversationItem(kind="run-context", seq=0)],
|
||||
)
|
||||
return s
|
||||
|
||||
@@ -24,7 +24,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
|
||||
import schedule.workflow_copilot_reconcile_task as mod
|
||||
from configs import dify_config
|
||||
from core.workflow_copilot.models import ConversationItem, EntryMode, FixContext, Session
|
||||
from core.workflow_copilot.models import ConversationItem, CopilotContext, EntryMode, Session
|
||||
from core.workflow_copilot.state import PcState
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models.base import Base
|
||||
@@ -63,7 +63,7 @@ def _seed(repo: SqlCopilotRepository, state: PcState) -> Session:
|
||||
entry_mode=EntryMode.FIX,
|
||||
current_state=state,
|
||||
)
|
||||
repo.create_session(s, FixContext(), [ConversationItem(kind="run-context", seq=0)])
|
||||
repo.create_session(s, CopilotContext(), [ConversationItem(kind="run-context", seq=0)])
|
||||
return s
|
||||
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ from core.workflow_copilot.errors import ConflictError, NotFoundError
|
||||
from core.workflow_copilot.models import (
|
||||
Checkpoint,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
EntryMode,
|
||||
FixContext,
|
||||
MutationIntent,
|
||||
NodeOutput,
|
||||
Run,
|
||||
@@ -67,7 +67,7 @@ def _make_domain_session(**overrides) -> DomainSession:
|
||||
|
||||
def test_create_session_then_get_session_round_trips(repo: SqlCopilotRepository) -> None:
|
||||
domain_session = _make_domain_session()
|
||||
fc = FixContext(failed_run_id="TR-1")
|
||||
fc = CopilotContext(failed_run_id="TR-1")
|
||||
items = [
|
||||
ConversationItem(seq=0, kind="run-context", payload={"a": 1}, at_version=1),
|
||||
ConversationItem(seq=1, kind="assistant-turn", payload={"b": 2}, at_version=1),
|
||||
@@ -90,7 +90,7 @@ def test_create_session_then_get_session_round_trips(repo: SqlCopilotRepository)
|
||||
assert loaded_session.entry_mode == EntryMode.FIX
|
||||
assert loaded_session.current_state == PcState.FIX_DIAGNOSE
|
||||
assert loaded_session.version == 1
|
||||
assert loaded_fc == FixContext(failed_run_id="TR-1", next_seq=2)
|
||||
assert loaded_fc == CopilotContext(failed_run_id="TR-1", next_seq=2)
|
||||
|
||||
|
||||
def test_get_session_missing_id_raises_not_found(repo: SqlCopilotRepository) -> None:
|
||||
@@ -100,10 +100,10 @@ def test_get_session_missing_id_raises_not_found(repo: SqlCopilotRepository) ->
|
||||
|
||||
def test_compare_and_advance_bumps_version_and_appends_commit_and_items(repo: SqlCopilotRepository) -> None:
|
||||
domain_session = _make_domain_session()
|
||||
fc = FixContext(failed_run_id="TR-1")
|
||||
fc = CopilotContext(failed_run_id="TR-1")
|
||||
repo.create_session(domain_session, fc, [])
|
||||
|
||||
next_fc = FixContext(failed_run_id="TR-1", staged_repair=[MutationIntent(op="set_node_config")])
|
||||
next_fc = CopilotContext(failed_run_id="TR-1", staged_repair=[MutationIntent(op="set_node_config")])
|
||||
new_items = [ConversationItem(seq=0, kind="diagnosis", payload={"x": 1}, at_version=2)]
|
||||
|
||||
new_version = repo.compare_and_advance(
|
||||
@@ -122,7 +122,7 @@ def test_compare_and_advance_stale_version_raises_conflict_and_leaves_row_unchan
|
||||
repo: SqlCopilotRepository,
|
||||
) -> None:
|
||||
domain_session = _make_domain_session()
|
||||
fc = FixContext(failed_run_id="TR-1")
|
||||
fc = CopilotContext(failed_run_id="TR-1")
|
||||
repo.create_session(domain_session, fc, [])
|
||||
|
||||
with pytest.raises(ConflictError):
|
||||
@@ -130,7 +130,7 @@ def test_compare_and_advance_stale_version_raises_conflict_and_leaves_row_unchan
|
||||
domain_session.id,
|
||||
base_version=999,
|
||||
next=PcState.FIX_PROPOSE,
|
||||
fc=FixContext(),
|
||||
fc=CopilotContext(),
|
||||
items=[],
|
||||
)
|
||||
|
||||
@@ -146,14 +146,14 @@ def test_compare_and_advance_missing_session_raises_not_found(repo: SqlCopilotRe
|
||||
"does-not-exist",
|
||||
base_version=1,
|
||||
next=PcState.FIX_PROPOSE,
|
||||
fc=FixContext(),
|
||||
fc=CopilotContext(),
|
||||
items=[],
|
||||
)
|
||||
|
||||
|
||||
def test_compare_and_advance_duplicate_seq_raises_conflict(repo: SqlCopilotRepository) -> None:
|
||||
domain_session = _make_domain_session()
|
||||
fc = FixContext(failed_run_id="TR-1")
|
||||
fc = CopilotContext(failed_run_id="TR-1")
|
||||
seed_items = [ConversationItem(seq=0, kind="run-context", payload={}, at_version=1)]
|
||||
repo.create_session(domain_session, fc, seed_items)
|
||||
|
||||
@@ -162,7 +162,7 @@ def test_compare_and_advance_duplicate_seq_raises_conflict(repo: SqlCopilotRepos
|
||||
domain_session.id,
|
||||
base_version=1,
|
||||
next=PcState.FIX_PROPOSE,
|
||||
fc=FixContext(),
|
||||
fc=CopilotContext(),
|
||||
items=[ConversationItem(seq=0, kind="diagnosis", payload={}, at_version=2)],
|
||||
)
|
||||
|
||||
@@ -177,7 +177,7 @@ def test_compare_and_advance_duplicate_seq_raises_conflict(repo: SqlCopilotRepos
|
||||
|
||||
def test_create_checkpoint_then_get_checkpoint_round_trips(repo: SqlCopilotRepository) -> None:
|
||||
domain_session = _make_domain_session()
|
||||
fc = FixContext(failed_run_id="TR-1")
|
||||
fc = CopilotContext(failed_run_id="TR-1")
|
||||
repo.create_session(domain_session, fc, [])
|
||||
|
||||
snap = Snapshot(session_id=domain_session.id, hash="h1", graph={"nodes": [1, 2, 3]})
|
||||
@@ -290,7 +290,7 @@ def test_get_test_input_missing_id_raises_not_found(repo: SqlCopilotRepository)
|
||||
|
||||
def test_list_conversation_returns_items_ordered_by_seq(repo: SqlCopilotRepository) -> None:
|
||||
domain_session = _make_domain_session()
|
||||
fc = FixContext(failed_run_id="TR-1")
|
||||
fc = CopilotContext(failed_run_id="TR-1")
|
||||
items = [
|
||||
ConversationItem(seq=0, kind="run-context", payload={"a": 1}, at_version=1),
|
||||
ConversationItem(seq=1, kind="assistant-turn", payload={"b": 2}, at_version=1),
|
||||
@@ -299,7 +299,7 @@ def test_list_conversation_returns_items_ordered_by_seq(repo: SqlCopilotReposito
|
||||
|
||||
more_items = [ConversationItem(seq=2, kind="diagnosis", payload={"c": 3}, at_version=2)]
|
||||
repo.compare_and_advance(
|
||||
domain_session.id, base_version=1, next=PcState.FIX_PROPOSE, fc=FixContext(), items=more_items
|
||||
domain_session.id, base_version=1, next=PcState.FIX_PROPOSE, fc=CopilotContext(), items=more_items
|
||||
)
|
||||
|
||||
conv = repo.list_conversation(domain_session.id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Round-trip tests for the ``FixContext`` JSON (de)serializer.
|
||||
"""Round-trip tests for the ``CopilotContext`` JSON (de)serializer.
|
||||
|
||||
``FixContext`` is the per-session working state persisted as the
|
||||
``CopilotContext`` is the per-session working state persisted as the
|
||||
``context`` JSONB column on ``workflow_copilot_session_commits`` (P3a Task 4).
|
||||
This module verifies the pure (de)serializer is lossless and produces a
|
||||
JSON-safe dict, independent of any database concern.
|
||||
@@ -11,16 +11,16 @@ import json
|
||||
from core.workflow_copilot.models import (
|
||||
ChangeSet,
|
||||
ChecklistError,
|
||||
CopilotContext,
|
||||
Diagnosis,
|
||||
FixContext,
|
||||
MutationIntent,
|
||||
Risk,
|
||||
)
|
||||
from services.workflow_copilot.serde import fix_context_from_dict, fix_context_to_dict
|
||||
from services.workflow_copilot.serde import context_from_dict, context_to_dict
|
||||
|
||||
|
||||
def _full_fix_context() -> FixContext:
|
||||
return FixContext(
|
||||
def _full_fix_context() -> CopilotContext:
|
||||
return CopilotContext(
|
||||
failed_run_id="TR-1",
|
||||
diagnosis=Diagnosis(
|
||||
culprit_node_id="n1",
|
||||
@@ -57,15 +57,15 @@ def _full_fix_context() -> FixContext:
|
||||
def test_fully_populated_fix_context_round_trips():
|
||||
fc = _full_fix_context()
|
||||
|
||||
result = fix_context_from_dict(fix_context_to_dict(fc))
|
||||
result = context_from_dict(context_to_dict(fc))
|
||||
|
||||
assert result == fc
|
||||
|
||||
|
||||
def test_empty_fix_context_round_trips():
|
||||
fc = FixContext()
|
||||
fc = CopilotContext()
|
||||
|
||||
result = fix_context_from_dict(fix_context_to_dict(fc))
|
||||
result = context_from_dict(context_to_dict(fc))
|
||||
|
||||
assert result == fc
|
||||
|
||||
@@ -73,7 +73,7 @@ def test_empty_fix_context_round_trips():
|
||||
def test_to_dict_output_is_json_serializable():
|
||||
fc = _full_fix_context()
|
||||
|
||||
d = fix_context_to_dict(fc)
|
||||
d = context_to_dict(fc)
|
||||
|
||||
# Must not raise -- this is what actually lands in the JSONB column.
|
||||
json.dumps(d)
|
||||
@@ -83,6 +83,33 @@ def test_from_dict_tolerates_missing_optional_keys():
|
||||
# An older/partial persisted dict -- only a subset of keys present.
|
||||
d = {"failed_run_id": "TR-9"}
|
||||
|
||||
fc = fix_context_from_dict(d)
|
||||
fc = context_from_dict(d)
|
||||
|
||||
assert fc == FixContext(failed_run_id="TR-9")
|
||||
assert fc == CopilotContext(failed_run_id="TR-9")
|
||||
|
||||
|
||||
def test_context_round_trips_build_fields():
|
||||
from core.workflow_copilot.models import CopilotContext
|
||||
from services.workflow_copilot.serde import context_from_dict, context_to_dict
|
||||
|
||||
fc = CopilotContext(
|
||||
goal_text="Build a quarterly report workflow",
|
||||
requirements={"currency": "USD", "prefer_audited": True},
|
||||
plan_items=["Retrieve", "Summarize", "Emit"],
|
||||
plan_version_tag="v2",
|
||||
resource_selection={"resource_ids": ["kb-company"], "conflict_policy": "audited"},
|
||||
built_node_ids=["start", "llm", "end"],
|
||||
)
|
||||
out = context_from_dict(context_to_dict(fc))
|
||||
assert out == fc
|
||||
|
||||
|
||||
def test_context_from_dict_defaults_build_fields_when_absent():
|
||||
from core.workflow_copilot.models import CopilotContext
|
||||
from services.workflow_copilot.serde import context_from_dict
|
||||
|
||||
fc = context_from_dict({"failed_run_id": "TR-9"})
|
||||
assert fc == CopilotContext(failed_run_id="TR-9")
|
||||
assert fc.goal_text == ""
|
||||
assert fc.requirements == {}
|
||||
assert fc.plan_items == []
|
||||
@@ -21,8 +21,8 @@ from core.workflow_copilot.models import (
|
||||
Actor,
|
||||
ChecklistError,
|
||||
ConversationItem,
|
||||
CopilotContext,
|
||||
EntryMode,
|
||||
FixContext,
|
||||
Session,
|
||||
)
|
||||
from core.workflow_copilot.state import PcState
|
||||
@@ -251,7 +251,7 @@ def _seed_free_session(repo: SqlCopilotRepository) -> Session:
|
||||
entry_mode=EntryMode.FIX,
|
||||
current_state=PcState.FIX_DIAGNOSE,
|
||||
)
|
||||
repo.create_session(s, FixContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
|
||||
repo.create_session(s, CopilotContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
|
||||
return s
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ def _seed_session_at(repo: SqlCopilotRepository, state: PcState) -> Session:
|
||||
entry_mode=EntryMode.FIX,
|
||||
current_state=state,
|
||||
)
|
||||
repo.create_session(s, FixContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
|
||||
repo.create_session(s, CopilotContext(failed_run_id="TR-1"), [ConversationItem(kind="run-context", seq=0)])
|
||||
return s
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user