mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-16 23:08:38 +02:00
Redesign Python AI engine (#5991)
# Description of Changes Redesign the Python AI engine to be properly agentic and make use of `pydantic-ai` instead of `langchain` for correctness and ergonomics. This should be a good foundation for us to build our AI engine on going forwards.
This commit is contained in:
160
engine/tests/test_pdf_edit_agent.py
Normal file
160
engine/tests/test_pdf_edit_agent.py
Normal file
@@ -0,0 +1,160 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
from stirling.agents import PdfEditAgent, PdfEditParameterSelector, PdfEditPlanSelection
|
||||
from stirling.config import AppSettings
|
||||
from stirling.contracts import (
|
||||
EditCannotDoResponse,
|
||||
EditClarificationRequest,
|
||||
EditPlanResponse,
|
||||
PdfEditRequest,
|
||||
ToolOperationStep,
|
||||
)
|
||||
from stirling.models.tool_models import CompressParams, OperationId, RotateParams
|
||||
from stirling.services import build_runtime
|
||||
|
||||
|
||||
def build_test_settings() -> AppSettings:
|
||||
return AppSettings(
|
||||
smart_model_name="test",
|
||||
fast_model_name="test",
|
||||
smart_model_max_tokens=8192,
|
||||
fast_model_max_tokens=2048,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParameterSelectorCall:
|
||||
request: PdfEditRequest
|
||||
operation_plan: list[OperationId]
|
||||
operation_index: int
|
||||
generated_steps: list[ToolOperationStep]
|
||||
|
||||
|
||||
class RecordingParameterSelector:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[ParameterSelectorCall] = []
|
||||
|
||||
async def select(
|
||||
self,
|
||||
request: PdfEditRequest,
|
||||
operation_plan: list[OperationId],
|
||||
operation_index: int,
|
||||
generated_steps: list[ToolOperationStep],
|
||||
) -> RotateParams | CompressParams:
|
||||
self.calls.append(
|
||||
ParameterSelectorCall(
|
||||
request=request,
|
||||
operation_plan=operation_plan,
|
||||
operation_index=operation_index,
|
||||
generated_steps=list(generated_steps),
|
||||
)
|
||||
)
|
||||
if operation_index == 0:
|
||||
return RotateParams(angle=90)
|
||||
return CompressParams(compression_level=5)
|
||||
|
||||
|
||||
class StubPdfEditAgent(PdfEditAgent):
|
||||
def __init__(
|
||||
self,
|
||||
selection: PdfEditPlanSelection | EditClarificationRequest | EditCannotDoResponse,
|
||||
parameter_selector: RecordingParameterSelector | PdfEditParameterSelector | None = None,
|
||||
) -> None:
|
||||
super().__init__(build_runtime(build_test_settings()))
|
||||
self.selection = selection
|
||||
if parameter_selector is not None:
|
||||
self.parameter_selector = parameter_selector
|
||||
|
||||
async def _select_plan(
|
||||
self,
|
||||
request: PdfEditRequest,
|
||||
) -> PdfEditPlanSelection | EditClarificationRequest | EditCannotDoResponse:
|
||||
return self.selection
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pdf_edit_agent_builds_multi_step_plan() -> None:
|
||||
parameter_selector = RecordingParameterSelector()
|
||||
agent = StubPdfEditAgent(
|
||||
PdfEditPlanSelection(
|
||||
operations=[OperationId.ROTATE, OperationId.COMPRESS],
|
||||
summary="Rotate the PDF, then compress it.",
|
||||
rationale="The pages need reorientation before reducing file size.",
|
||||
),
|
||||
parameter_selector=parameter_selector,
|
||||
)
|
||||
|
||||
response = await agent.handle(
|
||||
PdfEditRequest(
|
||||
user_message="Rotate the PDF clockwise and then compress it.",
|
||||
file_names=["scan.pdf"],
|
||||
)
|
||||
)
|
||||
|
||||
assert isinstance(response, EditPlanResponse)
|
||||
assert response.summary == "Rotate the PDF, then compress it."
|
||||
assert response.rationale == "The pages need reorientation before reducing file size."
|
||||
assert [step.tool for step in response.steps] == [OperationId.ROTATE, OperationId.COMPRESS]
|
||||
assert isinstance(response.steps[0].parameters, RotateParams)
|
||||
assert isinstance(response.steps[1].parameters, CompressParams)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pdf_edit_agent_passes_previous_steps_to_parameter_selector() -> None:
|
||||
parameter_selector = RecordingParameterSelector()
|
||||
agent = StubPdfEditAgent(
|
||||
PdfEditPlanSelection(
|
||||
operations=[OperationId.ROTATE, OperationId.COMPRESS],
|
||||
summary="Rotate the PDF, then compress it.",
|
||||
),
|
||||
parameter_selector=parameter_selector,
|
||||
)
|
||||
|
||||
request = PdfEditRequest(
|
||||
user_message="Rotate the PDF clockwise and then compress it.",
|
||||
file_names=["scan.pdf"],
|
||||
)
|
||||
response = await agent.handle(request)
|
||||
|
||||
assert isinstance(response, EditPlanResponse)
|
||||
assert len(parameter_selector.calls) == 2
|
||||
assert parameter_selector.calls[0].operation_index == 0
|
||||
assert parameter_selector.calls[0].generated_steps == []
|
||||
assert parameter_selector.calls[1].operation_index == 1
|
||||
assert parameter_selector.calls[1].generated_steps == [
|
||||
ToolOperationStep(
|
||||
tool=OperationId.ROTATE,
|
||||
parameters=RotateParams(angle=90),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pdf_edit_agent_returns_clarification_without_partial_plan() -> None:
|
||||
agent = StubPdfEditAgent(
|
||||
EditClarificationRequest(
|
||||
question="Which pages should be rotated?",
|
||||
reason="The request does not say which pages to change.",
|
||||
)
|
||||
)
|
||||
|
||||
response = await agent.handle(PdfEditRequest(user_message="Rotate some pages."))
|
||||
|
||||
assert isinstance(response, EditClarificationRequest)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pdf_edit_agent_returns_cannot_do_without_partial_plan() -> None:
|
||||
agent = StubPdfEditAgent(
|
||||
EditCannotDoResponse(
|
||||
reason="This request requires OCR, which is not part of PDF edit planning.",
|
||||
)
|
||||
)
|
||||
|
||||
response = await agent.handle(PdfEditRequest(user_message="Read this scan and summarize it."))
|
||||
|
||||
assert isinstance(response, EditCannotDoResponse)
|
||||
Reference in New Issue
Block a user