Implement AI orchestration wedding demo
This commit is contained in:
29
backend/tests/test_integration_flow.py
Normal file
29
backend/tests/test_integration_flow.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.infrastructure.database import PendingAction, SessionLocal, init_db
|
||||
from app.main import app
|
||||
|
||||
|
||||
def test_websocket_ingestion_creates_pending_action():
|
||||
init_db()
|
||||
session = SessionLocal()
|
||||
session.query(PendingAction).delete()
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/ws/transcript") as socket:
|
||||
socket.send_json(
|
||||
{
|
||||
"speaker": "Planner",
|
||||
"timestamp": "2026-06-26T12:00:00Z",
|
||||
"text": "Create a task for Maria to confirm the photographer.",
|
||||
}
|
||||
)
|
||||
response = socket.receive_json()
|
||||
|
||||
assert response["ok"] is True
|
||||
assert response["pending_action_id"] is not None
|
||||
|
||||
dashboard = client.get("/dashboard").json()
|
||||
assert any(action["tool"] == "create_task" for action in dashboard["pending_actions"])
|
||||
71
backend/tests/test_memory_executor.py
Normal file
71
backend/tests/test_memory_executor.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import pytest
|
||||
|
||||
from app.ai.tool_registry import ToolRegistry
|
||||
from app.domain.schemas import ActionStatus
|
||||
from app.domain.schemas import ToolDefinition
|
||||
from app.executor.action_executor import ActionExecutor
|
||||
from app.infrastructure.database import PendingAction
|
||||
from app.memory.memory_manager import MemoryManager
|
||||
|
||||
|
||||
def test_memory_retrieves_semantic_items():
|
||||
memory = MemoryManager()
|
||||
memory.upsert("preference", "Customer prefers white flowers")
|
||||
|
||||
results = memory.retrieve("white flower preference")
|
||||
|
||||
assert results[0]["type"] == "preference"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_executor_rejects_unapproved_action():
|
||||
registry = ToolRegistry(
|
||||
[
|
||||
ToolDefinition(
|
||||
name="create_task",
|
||||
description="Create task",
|
||||
method="POST",
|
||||
path="/tasks",
|
||||
schema={"type": "object", "properties": {"title": {"type": "string"}}},
|
||||
)
|
||||
]
|
||||
)
|
||||
action = PendingAction(tool="create_task", status="pending_review", payload={"title": "x"})
|
||||
|
||||
with pytest.raises(ValueError, match="Only approved"):
|
||||
await ActionExecutor(registry).execute(None, action) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_executor_generates_call_without_http_request():
|
||||
registry = ToolRegistry(
|
||||
[
|
||||
ToolDefinition(
|
||||
name="update_reservation",
|
||||
description="Update reservation",
|
||||
method="PATCH",
|
||||
path="/reservations/{reservation_id}",
|
||||
path_params=["reservation_id"],
|
||||
schema={"type": "object", "properties": {"reservation_id": {"type": "string"}, "status": {"type": "string"}}},
|
||||
)
|
||||
]
|
||||
)
|
||||
action = PendingAction(tool="update_reservation", status=ActionStatus.APPROVED, payload={"reservation_id": "abc", "status": "ACCEPTED"})
|
||||
|
||||
class FakeSession:
|
||||
def add(self, _action):
|
||||
return None
|
||||
|
||||
def commit(self):
|
||||
return None
|
||||
|
||||
def refresh(self, _action):
|
||||
return None
|
||||
|
||||
result = await ActionExecutor(registry).execute(FakeSession(), action) # type: ignore[arg-type]
|
||||
|
||||
assert result.status == ActionStatus.APPROVED
|
||||
assert result.execution_result["generated_only"] is True
|
||||
assert result.execution_result["method"] == "PATCH"
|
||||
assert result.execution_result["path"] == "/reservations/abc"
|
||||
assert result.execution_result["json"] == {"status": "ACCEPTED"}
|
||||
59
backend/tests/test_planner_validation_policy.py
Normal file
59
backend/tests/test_planner_validation_policy.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.ai.tool_registry import ToolRegistry
|
||||
from app.domain.schemas import PlannerDecision, ToolDefinition, TranscriptChunk
|
||||
from app.planner.llm_planner import HybridPlanner
|
||||
from app.validation.action_validator import ActionValidator
|
||||
from app.validation.policy_engine import PolicyEngine
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fallback_planner_extracts_task():
|
||||
planner = HybridPlanner()
|
||||
chunk = TranscriptChunk(speaker="Bride", timestamp=datetime(2026, 6, 26, 12, 0), text="Create a task for Maria to confirm the photographer.")
|
||||
|
||||
decision = await planner.plan(chunk, {}, [], [])
|
||||
|
||||
assert decision.tool == "create_task"
|
||||
assert decision.extracted_entities["assignee"] == "Maria"
|
||||
assert decision.arguments["is_completed"] is False
|
||||
assert "Suggested owner: Maria" in decision.arguments["description"]
|
||||
assert "photographer" in decision.arguments["title"].lower()
|
||||
|
||||
|
||||
def test_validator_rejects_hallucinated_parameters():
|
||||
registry = ToolRegistry(
|
||||
[
|
||||
ToolDefinition(
|
||||
name="create_task",
|
||||
description="Create task",
|
||||
method="POST",
|
||||
path="/tasks",
|
||||
schema={"type": "object", "required": ["title"], "properties": {"title": {"type": "string"}}},
|
||||
required=["title"],
|
||||
)
|
||||
]
|
||||
)
|
||||
decision = PlannerDecision(
|
||||
intent="create_task",
|
||||
confidence=0.9,
|
||||
reasoning="test",
|
||||
tool="create_task",
|
||||
arguments={"title": "Confirm photographer", "unexpected": True},
|
||||
)
|
||||
|
||||
result = ActionValidator(registry).validate(decision)
|
||||
|
||||
assert not result.valid
|
||||
assert "Hallucinated" in result.errors[0]
|
||||
|
||||
|
||||
def test_policy_rejects_low_confidence():
|
||||
decision = PlannerDecision(intent="create_note", confidence=0.2, reasoning="weak", tool="create_note", arguments={"content": "x"})
|
||||
|
||||
result = PolicyEngine().evaluate(decision)
|
||||
|
||||
assert not result.allowed
|
||||
assert "below threshold" in result.reasons[0]
|
||||
17
backend/tests/test_soniox_grouping.py
Normal file
17
backend/tests/test_soniox_grouping.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from app.infrastructure.soniox import group_by_speaker
|
||||
|
||||
|
||||
def test_group_by_speaker_flushes_on_sentence_and_speaker_change():
|
||||
segments = group_by_speaker(
|
||||
[
|
||||
{"speaker": "Bride", "text": "Create ", "start_ms": 0, "end_ms": 100},
|
||||
{"speaker": "Bride", "text": "a task.", "start_ms": 100, "end_ms": 500},
|
||||
{"speaker": "Planner", "text": "Add ", "start_ms": 600, "end_ms": 700},
|
||||
{"speaker": "Planner", "text": "a note.", "start_ms": 700, "end_ms": 1000},
|
||||
]
|
||||
)
|
||||
|
||||
assert len(segments) == 2
|
||||
assert segments[0].speaker == "Bride"
|
||||
assert segments[0].edited_text == "Create a task."
|
||||
assert segments[1].speaker == "Planner"
|
||||
63
backend/tests/test_tool_generation.py
Normal file
63
backend/tests/test_tool_generation.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from app.ai.tool_generator import OpenAPIToolGenerator
|
||||
|
||||
|
||||
def test_generates_tools_from_openapi_spec():
|
||||
spec = {
|
||||
"paths": {
|
||||
"/tasks": {
|
||||
"post": {
|
||||
"operationId": "create_task",
|
||||
"summary": "Create task",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/TaskCreate"}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"TaskCreate": {
|
||||
"type": "object",
|
||||
"required": ["title"],
|
||||
"properties": {"title": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
tools = OpenAPIToolGenerator(spec).generate()
|
||||
|
||||
assert tools[0].name == "create_task"
|
||||
assert tools[0].required == ["title"]
|
||||
assert tools[0].path == "/tasks"
|
||||
|
||||
|
||||
def test_generates_path_params_for_patch_tools():
|
||||
spec = {
|
||||
"paths": {
|
||||
"/reservations/{reservation_id}": {
|
||||
"patch": {
|
||||
"operationId": "update_reservation",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"status": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tools = OpenAPIToolGenerator(spec).generate()
|
||||
|
||||
assert tools[0].path_params == ["reservation_id"]
|
||||
assert "reservation_id" in tools[0].schema["required"]
|
||||
Reference in New Issue
Block a user