feat(decision): add decision persistence
Add decision persistence layer with DecisionRepository, DecisionModel, and Alembic migration. Includes tree queries (BFS traversal via deque, path-to-root), superseded lookup, ordered decision path retrieval, concrete Decision type annotations (via TYPE_CHECKING), and comprehensive test coverage (Behave BDD, Robot Framework, ASV benchmarks). Updated database_schema.md, CHANGELOG.md, and CONTRIBUTORS.md. ISSUES CLOSED: #171
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
"""ASV benchmarks for DecisionRepository persistence operations.
|
||||
|
||||
Measures create, get, get_by_plan, get_tree, and list_by_type performance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.decision import (
|
||||
Decision,
|
||||
DecisionType,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
PlanTimestamps,
|
||||
ProcessingState,
|
||||
)
|
||||
from cleveragents.infrastructure.database.models import Base
|
||||
from cleveragents.infrastructure.database.repositories import (
|
||||
ActionRepository,
|
||||
DecisionRepository,
|
||||
LifecyclePlanRepository,
|
||||
)
|
||||
except ModuleNotFoundError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.decision import (
|
||||
Decision,
|
||||
DecisionType,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
PlanTimestamps,
|
||||
ProcessingState,
|
||||
)
|
||||
from cleveragents.infrastructure.database.models import Base
|
||||
from cleveragents.infrastructure.database.repositories import (
|
||||
ActionRepository,
|
||||
DecisionRepository,
|
||||
LifecyclePlanRepository,
|
||||
)
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
_PLAN_ID = "01HV00000000000000BENCH001"
|
||||
|
||||
|
||||
def _make_db():
|
||||
"""Create an in-memory SQLite DB with all tables and prerequisites."""
|
||||
engine = create_engine("sqlite:///:memory:", echo=False)
|
||||
Base.metadata.create_all(engine)
|
||||
sm = sessionmaker(bind=engine)
|
||||
session = sm()
|
||||
factory = lambda: session # noqa: E731
|
||||
|
||||
# Action prerequisite
|
||||
action_repo = ActionRepository(session_factory=factory)
|
||||
action = Action(
|
||||
namespaced_name=NamespacedName.parse("local/bench-decision-action"),
|
||||
description="Benchmark action",
|
||||
definition_of_done="Done",
|
||||
strategy_actor="local/s",
|
||||
execution_actor="local/e",
|
||||
state=ActionState.AVAILABLE,
|
||||
created_at=datetime(2026, 1, 1, tzinfo=UTC),
|
||||
updated_at=datetime(2026, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
action_repo.create(action)
|
||||
session.commit()
|
||||
|
||||
# Plan prerequisite
|
||||
plan_repo = LifecyclePlanRepository(session_factory=factory)
|
||||
now = datetime(2026, 3, 1, tzinfo=UTC)
|
||||
plan = Plan(
|
||||
identity=PlanIdentity(plan_id=_PLAN_ID, attempt=1),
|
||||
namespaced_name=NamespacedName(namespace="local", name="bench-plan"),
|
||||
action_name="local/bench-decision-action",
|
||||
description="Benchmark plan",
|
||||
definition_of_done="Done",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.PROCESSING,
|
||||
strategy_actor="local/s",
|
||||
execution_actor="local/e",
|
||||
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
||||
created_by="bench",
|
||||
tags=[],
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
)
|
||||
plan_repo.create(plan)
|
||||
session.commit()
|
||||
|
||||
return session, factory
|
||||
|
||||
|
||||
def _make_decision(seq=0, parent_id=None, dtype=DecisionType.PROMPT_DEFINITION):
|
||||
return Decision(
|
||||
plan_id=_PLAN_ID,
|
||||
parent_decision_id=parent_id,
|
||||
sequence_number=seq,
|
||||
decision_type=dtype,
|
||||
question="Benchmark question?",
|
||||
chosen_option="Benchmark option",
|
||||
)
|
||||
|
||||
|
||||
class TimeDecisionCreate:
|
||||
"""Benchmark decision creation."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def setup(self):
|
||||
self.session, self.factory = _make_db()
|
||||
self.repo = DecisionRepository(session_factory=self.factory)
|
||||
self._seq = 100
|
||||
|
||||
def time_create_single(self):
|
||||
self._seq += 1
|
||||
d = _make_decision(seq=self._seq, dtype=DecisionType.STRATEGY_CHOICE)
|
||||
self.repo.create(d)
|
||||
self.session.commit()
|
||||
|
||||
|
||||
class TimeDecisionGet:
|
||||
"""Benchmark decision retrieval."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def setup(self):
|
||||
self.session, self.factory = _make_db()
|
||||
self.repo = DecisionRepository(session_factory=self.factory)
|
||||
d = _make_decision()
|
||||
self.repo.create(d)
|
||||
self.session.commit()
|
||||
self.decision_id = d.decision_id
|
||||
|
||||
def time_get_by_id(self):
|
||||
self.repo.get(self.decision_id)
|
||||
|
||||
|
||||
class TimeDecisionGetByPlan:
|
||||
"""Benchmark get_by_plan with 50 pre-seeded decisions."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def setup(self):
|
||||
self.session, self.factory = _make_db()
|
||||
self.repo = DecisionRepository(session_factory=self.factory)
|
||||
root = _make_decision(seq=0)
|
||||
self.repo.create(root)
|
||||
root_id = root.decision_id
|
||||
for i in range(1, 50):
|
||||
d = _make_decision(
|
||||
seq=i,
|
||||
parent_id=root_id,
|
||||
dtype=DecisionType.STRATEGY_CHOICE,
|
||||
)
|
||||
self.repo.create(d)
|
||||
self.session.commit()
|
||||
|
||||
def time_get_by_plan(self):
|
||||
self.repo.get_by_plan(_PLAN_ID)
|
||||
|
||||
|
||||
class TimeDecisionTree:
|
||||
"""Benchmark get_tree with a 3-level tree."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def setup(self):
|
||||
self.session, self.factory = _make_db()
|
||||
self.repo = DecisionRepository(session_factory=self.factory)
|
||||
|
||||
root = _make_decision(seq=0)
|
||||
self.repo.create(root)
|
||||
self.root_id = root.decision_id
|
||||
|
||||
seq = 1
|
||||
for _ in range(3):
|
||||
child = _make_decision(
|
||||
seq=seq,
|
||||
parent_id=root.decision_id,
|
||||
dtype=DecisionType.STRATEGY_CHOICE,
|
||||
)
|
||||
self.repo.create(child)
|
||||
for _ in range(2):
|
||||
seq += 1
|
||||
gc = _make_decision(
|
||||
seq=seq,
|
||||
parent_id=child.decision_id,
|
||||
dtype=DecisionType.IMPLEMENTATION_CHOICE,
|
||||
)
|
||||
self.repo.create(gc)
|
||||
seq += 1
|
||||
self.session.commit()
|
||||
|
||||
def time_get_tree(self):
|
||||
self.repo.get_tree(self.root_id)
|
||||
|
||||
|
||||
class TimeDecisionListByType:
|
||||
"""Benchmark list_by_type with pre-seeded typed decisions."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def setup(self):
|
||||
self.session, self.factory = _make_db()
|
||||
self.repo = DecisionRepository(session_factory=self.factory)
|
||||
|
||||
root = _make_decision(seq=0)
|
||||
self.repo.create(root)
|
||||
root_id = root.decision_id
|
||||
|
||||
for i in range(1, 20):
|
||||
d = _make_decision(
|
||||
seq=i,
|
||||
parent_id=root_id,
|
||||
dtype=DecisionType.STRATEGY_CHOICE,
|
||||
)
|
||||
self.repo.create(d)
|
||||
for i in range(20, 30):
|
||||
d = _make_decision(
|
||||
seq=i,
|
||||
parent_id=root_id,
|
||||
dtype=DecisionType.IMPLEMENTATION_CHOICE,
|
||||
)
|
||||
self.repo.create(d)
|
||||
self.session.commit()
|
||||
|
||||
def time_list_strategy_choices(self):
|
||||
self.repo.list_by_type(_PLAN_ID, "strategy_choice")
|
||||
|
||||
def time_list_implementation_choices(self):
|
||||
self.repo.list_by_type(_PLAN_ID, "implementation_choice")
|
||||
Reference in New Issue
Block a user