125 lines
6.7 KiB
Python
125 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "backend"))
|
|
|
|
from app.services.agent_service import AgentService # noqa: E402
|
|
from app.services.engine_service import load_engine # noqa: E402
|
|
from app.services.library import CdslLibrary # noqa: E402
|
|
from app.services.part_skills import PartSkillLibrary # noqa: E402
|
|
from app.services.storage import WorkspaceStore # noqa: E402
|
|
from app.settings import ProviderConfig, ProviderModel, Settings # noqa: E402
|
|
from tests.test_design_intent import mounting_plate_cdsl, mounting_plate_intent # noqa: E402
|
|
|
|
|
|
BACKEND = ROOT / "backend"
|
|
PART_SKILL_ROOT = BACKEND / "agent" / "skills" / "cad-engine" / "references" / "part-skills"
|
|
|
|
|
|
class DesignIntentFlowTests(unittest.TestCase):
|
|
def settings(self, root: Path) -> Settings:
|
|
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
|
|
return Settings(
|
|
task_root=root / "tasks",
|
|
conversation_root=root / "conversations",
|
|
library_root=BACKEND / "cdsl_library",
|
|
engine_root=BACKEND / "engine" / "cdsl_engine",
|
|
llm_base_url=provider.base_url,
|
|
llm_api_key=provider.api_key,
|
|
llm_model="test-model",
|
|
llm_timeout_s=1,
|
|
default_provider_id="test",
|
|
providers=(provider,),
|
|
)
|
|
|
|
def test_design_intent_is_required_before_library_or_cdsl(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
settings = self.settings(Path(directory))
|
|
store = WorkspaceStore(settings)
|
|
agent = AgentService(settings, store, CdslLibrary(settings), PartSkillLibrary(PART_SKILL_ROOT))
|
|
state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""}
|
|
searched, _ = asyncio.run(agent._run_tool("search_cdsl_library", {"query": "mounting plate"}, "", "mounting plate", [], intent_state=state))
|
|
generated, _ = asyncio.run(agent._run_tool("generate_cdsl_model", {"design_intent_id": "intent_aaaaaaaaaaaa", "cdsl": {}, "summary": "x", "assumptions": []}, "", "mounting plate", [], intent_state=state))
|
|
|
|
self.assertEqual(searched["code"], "DESIGN_INTENT_REQUIRED")
|
|
self.assertEqual(generated["code"], "DESIGN_INTENT_REQUIRED")
|
|
self.assertEqual(list(settings.task_root.glob("cad_*")), [])
|
|
|
|
def test_ready_plan_persists_before_build_and_links_success_revision(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
settings = self.settings(Path(directory))
|
|
store = WorkspaceStore(settings)
|
|
skills = PartSkillLibrary(PART_SKILL_ROOT)
|
|
agent = AgentService(settings, store, CdslLibrary(settings), skills)
|
|
request = "Create a mounting plate with four holes and a center slot"
|
|
state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""}
|
|
planned, _ = asyncio.run(agent._run_tool(
|
|
"propose_design_intent",
|
|
{"intent": mounting_plate_intent(), "summary": "plate plan", "assumptions": []},
|
|
"", request, [], part_skill_selection=skills.select(request), intent_state=state,
|
|
))
|
|
|
|
self.assertTrue(planned["ok"])
|
|
task = store.read_task(planned["task_id"])
|
|
self.assertEqual(task["current_revision"], "")
|
|
planning_path = store.artifact_path(planned["task_id"], planned["design_intent_path"])
|
|
self.assertTrue(planning_path.is_file())
|
|
saved = json.loads(planning_path.read_text(encoding="utf-8"))
|
|
self.assertEqual(saved["part_skill_ids"], skills.select(request)["skill_ids"])
|
|
|
|
searched, _ = asyncio.run(agent._run_tool("search_cdsl_library", {"query": "plate"}, planned["task_id"], request, [], intent_state=state))
|
|
self.assertTrue(searched["ok"])
|
|
result, built = asyncio.run(agent._run_tool(
|
|
"generate_cdsl_model",
|
|
{"design_intent_id": planned["design_intent_id"], "cdsl": mounting_plate_cdsl(), "summary": "plate", "assumptions": []},
|
|
planned["task_id"], request, [], part_skill_selection=skills.select(request), intent_state=state,
|
|
))
|
|
|
|
self.assertTrue(result["ok"])
|
|
self.assertIsNotNone(built)
|
|
revision = store.read_task(planned["task_id"])["revisions"][-1]
|
|
self.assertEqual(revision["design_intent_id"], planned["design_intent_id"])
|
|
self.assertEqual(revision["design_intent_path"], planned["design_intent_path"])
|
|
audit = json.loads(store.artifact_path(planned["task_id"], revision["part_skills_path"]).read_text(encoding="utf-8"))
|
|
self.assertEqual(audit["design_intent_id"], planned["design_intent_id"])
|
|
self.assertEqual(audit["design_intent_assumptions"], [])
|
|
|
|
def test_blocked_plan_is_persisted_without_a_revision_and_new_plan_supersedes_it(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
settings = self.settings(Path(directory))
|
|
store = WorkspaceStore(settings)
|
|
skills = PartSkillLibrary(PART_SKILL_ROOT)
|
|
agent = AgentService(settings, store, CdslLibrary(settings), skills)
|
|
request = "Create a mounting plate"
|
|
state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""}
|
|
blocked_intent = mounting_plate_intent()
|
|
blocked_intent["open_questions"] = [{"id": "thickness", "question": "Thickness?", "blocking": True}]
|
|
blocked_intent["status"] = "needs_clarification"
|
|
blocked, _ = asyncio.run(agent._run_tool(
|
|
"propose_design_intent",
|
|
{"intent": blocked_intent, "summary": "blocked", "assumptions": []},
|
|
"", request, [], part_skill_selection=skills.select(request), intent_state=state,
|
|
))
|
|
self.assertEqual(blocked["code"], "DESIGN_INTENT_BLOCKED")
|
|
task = store.read_task(blocked["task_id"])
|
|
self.assertEqual(task["revisions"], [])
|
|
self.assertEqual(task["design_intents"][0]["status"], "pending")
|
|
|
|
ready, _ = asyncio.run(agent._run_tool(
|
|
"propose_design_intent",
|
|
{"intent": mounting_plate_intent(), "summary": "ready", "assumptions": []},
|
|
blocked["task_id"], request, [], part_skill_selection=skills.select(request), intent_state=state,
|
|
))
|
|
self.assertTrue(ready["ok"])
|
|
task = store.read_task(blocked["task_id"])
|
|
self.assertEqual(task["design_intents"][0]["status"], "superseded")
|
|
self.assertEqual(task["design_intents"][-1]["status"], "accepted")
|