Resolution favours main for all conflicts (new orchestrator shape with artifacts/file_names/conversation_history/resume_with, WorkflowOutcome enum, RAG, ledger agent, deleted engine/config/.env.example). Re-wires our three form-fill agents (FormAnalyserAgent, FormFillerAgent, DocumentExtractorAgent) into the new app layout: - contracts/__init__.py: adds form-fill exports to __all__ - api/dependencies.py: adds get_form_analyser_agent / get_form_filler_agent / get_document_extractor_agent - api/app.py: instantiates the three agents in lifespan and registers form_fill_router - api/routes/__init__.py: exports form_fill_router - tests/test_stirling_api.py: imports and registers form-fill stubs - tests/test_stirling_contracts.py: adds back KnowledgeUpdateResponse discriminator test Form fill remains accessible via its own endpoints (/api/v1/form/ai/analyse, /fill-batch, /extract). Not wired as an orchestrator delegate — following main's pattern where orchestrator only routes pdf_edit/pdf_question/user_spec /math_auditor. Follow-ups still needed: - Thread conversation_history into form-fill agent prompts - Align form-fill response outcomes with WorkflowOutcome enum - Decide whether engine should return ToolOperationStep plans (main's new pattern per #6116) or keep returning fill values directly All 127 engine tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
258 lines
8.3 KiB
Python
258 lines
8.3 KiB
Python
from conftest import build_app_settings
|
|
from fastapi.testclient import TestClient
|
|
|
|
from stirling.api import app
|
|
from stirling.api.dependencies import (
|
|
get_document_extractor_agent,
|
|
get_execution_planning_agent,
|
|
get_form_analyser_agent,
|
|
get_form_filler_agent,
|
|
get_orchestrator_agent,
|
|
get_pdf_edit_agent,
|
|
get_pdf_question_agent,
|
|
get_user_spec_agent,
|
|
)
|
|
from stirling.config import load_settings
|
|
from stirling.contracts import (
|
|
AgentDraft,
|
|
AgentDraftRequest,
|
|
AgentDraftResponse,
|
|
AgentExecutionRequest,
|
|
AgentRevisionRequest,
|
|
AgentRevisionResponse,
|
|
AnalysedFileResult,
|
|
CannotContinueExecutionAction,
|
|
CrossFileRole,
|
|
DetectedRole,
|
|
DocumentExtractionRequest,
|
|
EditCannotDoResponse,
|
|
FileFillResult,
|
|
FormAnalysisRequest,
|
|
FormAnalysisResponse,
|
|
FormFillBatchRequest,
|
|
FormFillBatchResponse,
|
|
KnowledgeUpdateResponse,
|
|
OrchestratorRequest,
|
|
PdfEditRequest,
|
|
PdfQuestionNeedContentResponse,
|
|
PdfQuestionNotFoundResponse,
|
|
PdfQuestionRequest,
|
|
)
|
|
from stirling.models.tool_models import Angle, RotatePdfParams
|
|
|
|
|
|
class StubOrchestratorAgent:
|
|
async def handle(self, request: OrchestratorRequest) -> PdfQuestionNeedContentResponse:
|
|
return PdfQuestionNeedContentResponse(reason=request.user_message, files=[], max_pages=1, max_characters=1000)
|
|
|
|
|
|
class StubPdfEditAgent:
|
|
async def handle(self, request: PdfEditRequest) -> EditCannotDoResponse:
|
|
return EditCannotDoResponse(reason=request.user_message)
|
|
|
|
|
|
class StubPdfQuestionAgent:
|
|
async def handle(self, request: PdfQuestionRequest) -> PdfQuestionNotFoundResponse:
|
|
return PdfQuestionNotFoundResponse(reason=request.question)
|
|
|
|
|
|
class StubUserSpecAgent:
|
|
async def draft(self, request: AgentDraftRequest) -> AgentDraftResponse:
|
|
return AgentDraftResponse(
|
|
draft=AgentDraft(
|
|
name="Drafted",
|
|
description="Route wiring test",
|
|
objective=request.user_message,
|
|
steps=[],
|
|
)
|
|
)
|
|
|
|
async def revise(self, request: AgentRevisionRequest) -> AgentRevisionResponse:
|
|
return AgentRevisionResponse(draft=request.current_draft)
|
|
|
|
|
|
class StubFormAnalyserAgent:
|
|
async def analyse(self, request: FormAnalysisRequest) -> FormAnalysisResponse:
|
|
return FormAnalysisResponse(
|
|
per_file=[
|
|
AnalysedFileResult(
|
|
file_id=f.file_id,
|
|
file_name=f.file_name,
|
|
detected_roles=[
|
|
DetectedRole(
|
|
role_label="Primary", field_names=[ff.name for ff in f.form_fields], is_primary_person=True
|
|
)
|
|
],
|
|
)
|
|
for f in request.files
|
|
],
|
|
cross_file_roles=[
|
|
CrossFileRole(
|
|
role_label="Primary",
|
|
file_ids=[f.file_id for f in request.files],
|
|
field_names_by_file={f.file_id: [ff.name for ff in f.form_fields] for f in request.files},
|
|
is_primary_person=True,
|
|
)
|
|
],
|
|
message="Stub analysis.",
|
|
)
|
|
|
|
|
|
class StubFormFillerAgent:
|
|
async def fill_batch(self, request: FormFillBatchRequest) -> FormFillBatchResponse:
|
|
return FormFillBatchResponse(
|
|
per_file=[FileFillResult(file_id=f.file_id, filled_fields=[]) for f in request.files],
|
|
message="Stub fill.",
|
|
)
|
|
|
|
|
|
class StubDocumentExtractorAgent:
|
|
async def extract_multiple(self, request: DocumentExtractionRequest) -> KnowledgeUpdateResponse:
|
|
return KnowledgeUpdateResponse(proposed_entries=[], message="Stub extraction.")
|
|
|
|
|
|
class StubExecutionPlanningAgent:
|
|
async def next_action(self, request: AgentExecutionRequest) -> CannotContinueExecutionAction:
|
|
return CannotContinueExecutionAction(reason=str(request.current_step_index))
|
|
|
|
|
|
app.dependency_overrides[load_settings] = build_app_settings
|
|
app.dependency_overrides[get_orchestrator_agent] = lambda: StubOrchestratorAgent()
|
|
app.dependency_overrides[get_pdf_edit_agent] = lambda: StubPdfEditAgent()
|
|
app.dependency_overrides[get_pdf_question_agent] = lambda: StubPdfQuestionAgent()
|
|
app.dependency_overrides[get_user_spec_agent] = lambda: StubUserSpecAgent()
|
|
app.dependency_overrides[get_execution_planning_agent] = lambda: StubExecutionPlanningAgent()
|
|
app.dependency_overrides[get_form_analyser_agent] = lambda: StubFormAnalyserAgent()
|
|
app.dependency_overrides[get_form_filler_agent] = lambda: StubFormFillerAgent()
|
|
app.dependency_overrides[get_document_extractor_agent] = lambda: StubDocumentExtractorAgent()
|
|
|
|
client: TestClient = TestClient(app)
|
|
|
|
|
|
def test_health_route() -> None:
|
|
response = client.get("/health")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "ok"
|
|
|
|
|
|
def test_orchestrator_route() -> None:
|
|
response = client.post("/api/v1/orchestrator", json={"userMessage": "route this", "fileNames": ["test.pdf"]})
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["outcome"] == "need_content"
|
|
|
|
|
|
def test_pdf_edit_route() -> None:
|
|
response = client.post("/api/v1/pdf/edit", json={"userMessage": "rotate this"})
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["outcome"] == "cannot_do"
|
|
|
|
|
|
def test_pdf_questions_route() -> None:
|
|
response = client.post(
|
|
"/api/v1/pdf/questions",
|
|
json={
|
|
"question": "what is this?",
|
|
"fileNames": ["test.pdf"],
|
|
"pageText": [{"fileName": "test.pdf", "pages": [{"pageNumber": 1, "text": "Example"}]}],
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["outcome"] == "not_found"
|
|
|
|
|
|
def test_agent_draft_route() -> None:
|
|
response = client.post("/api/v1/agents/draft", json={"userMessage": "build me an agent"})
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["outcome"] == "draft"
|
|
|
|
|
|
def test_agent_revise_route() -> None:
|
|
response = client.post(
|
|
"/api/v1/agents/revise",
|
|
json={
|
|
"userMessage": "revise it",
|
|
"currentDraft": {
|
|
"name": "Drafted",
|
|
"description": "Route wiring test",
|
|
"objective": "build me an agent",
|
|
"steps": [
|
|
{
|
|
"kind": "tool",
|
|
"tool": "/api/v1/general/rotate-pdf",
|
|
"parameters": RotatePdfParams(angle=Angle(90)).model_dump(by_alias=True),
|
|
}
|
|
],
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["outcome"] == "draft"
|
|
|
|
|
|
def test_form_analyse_route() -> None:
|
|
response = client.post(
|
|
"/api/v1/form/ai/analyse",
|
|
json={
|
|
"files": [
|
|
{
|
|
"fileId": "f1",
|
|
"fileName": "test.pdf",
|
|
"formFields": [{"name": "name", "type": "text"}],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert len(response.json()["crossFileRoles"]) > 0
|
|
|
|
|
|
def test_form_fill_batch_route() -> None:
|
|
response = client.post(
|
|
"/api/v1/form/ai/fill-batch",
|
|
json={
|
|
"files": [
|
|
{
|
|
"fileId": "f1",
|
|
"formFields": [{"name": "name", "type": "text"}],
|
|
"roleLabel": "Primary",
|
|
}
|
|
],
|
|
"knowledge": {"first_name": "John"},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["outcome"] == "batch_fill_result"
|
|
|
|
|
|
def test_next_action_route() -> None:
|
|
response = client.post(
|
|
"/api/v1/agents/next-action",
|
|
json={
|
|
"agentSpec": {
|
|
"name": "Drafted",
|
|
"description": "Route wiring test",
|
|
"objective": "build me an agent",
|
|
"steps": [
|
|
{
|
|
"kind": "tool",
|
|
"tool": "/api/v1/general/rotate-pdf",
|
|
"parameters": RotatePdfParams(angle=Angle(90)).model_dump(by_alias=True),
|
|
}
|
|
],
|
|
},
|
|
"currentStepIndex": 0,
|
|
"executionContext": {"inputFiles": ["input.pdf"], "metadata": {}},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["outcome"] == "cannot_continue"
|