mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +02:00
Add SaaS AI engine (#5907)
This commit is contained in:
11
engine/tests/conftest.py
Normal file
11
engine/tests/conftest.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from pytest import Config
|
||||
|
||||
|
||||
def pytest_configure(config: Config) -> None:
|
||||
# Set required env vars in case there is no .env file
|
||||
os.environ.setdefault("STIRLING_OPENAI_API_KEY", "test")
|
||||
os.environ.setdefault("STIRLING_POSTHOG_API_KEY", "test")
|
||||
160
engine/tests/test_chat_router.py
Normal file
160
engine/tests/test_chat_router.py
Normal file
@@ -0,0 +1,160 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from chat_router import classify_chat_route
|
||||
from models import (
|
||||
ChatRouteRequest,
|
||||
ChatRouteResponse,
|
||||
CreateIntentHint,
|
||||
EditIntentHint,
|
||||
SmartFolderIntentHint,
|
||||
)
|
||||
|
||||
|
||||
def test_classify_chat_route_handles_smart_folder_intent(monkeypatch: MonkeyPatch):
|
||||
request = ChatRouteRequest(
|
||||
message="Create a workflow that batches PDFs overnight",
|
||||
history=[],
|
||||
has_files=False,
|
||||
has_create_session=False,
|
||||
has_edit_session=False,
|
||||
last_route="none",
|
||||
)
|
||||
expected_response = ChatRouteResponse(
|
||||
intent="smart_folder",
|
||||
smart_folder_intent=SmartFolderIntentHint(action="create"),
|
||||
reason="User wants to automate PDFs",
|
||||
)
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("chat_router.run_ai", lambda *args, **kwargs: expected_response)
|
||||
response = classify_chat_route(request)
|
||||
|
||||
assert response.intent == "smart_folder"
|
||||
assert response.smart_folder_intent == expected_response.smart_folder_intent
|
||||
|
||||
|
||||
def test_greeting_without_files(monkeypatch: MonkeyPatch):
|
||||
"""Greetings should route to edit/info, not create"""
|
||||
request = ChatRouteRequest(
|
||||
message="Hello",
|
||||
has_files=False,
|
||||
has_create_session=False,
|
||||
has_edit_session=False,
|
||||
last_route="none",
|
||||
history=[],
|
||||
)
|
||||
expected_response = ChatRouteResponse(
|
||||
intent="edit",
|
||||
edit_intent=EditIntentHint(mode="info", requires_file_context=False),
|
||||
reason="Conversational greeting",
|
||||
)
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("chat_router.run_ai", lambda *args, **kwargs: expected_response)
|
||||
response = classify_chat_route(request)
|
||||
|
||||
assert response.intent == "edit"
|
||||
assert response.edit_intent is not None
|
||||
assert response.edit_intent.mode == "info"
|
||||
|
||||
|
||||
def test_capability_question_without_files(monkeypatch: MonkeyPatch):
|
||||
"""'What can you do?' should route to edit/info"""
|
||||
request = ChatRouteRequest(
|
||||
message="What can you do?",
|
||||
has_files=False,
|
||||
has_create_session=False,
|
||||
has_edit_session=False,
|
||||
last_route="none",
|
||||
history=[],
|
||||
)
|
||||
expected_response = ChatRouteResponse(
|
||||
intent="edit",
|
||||
edit_intent=EditIntentHint(mode="info", requires_file_context=False),
|
||||
reason="User asking about capabilities",
|
||||
)
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("chat_router.run_ai", lambda *args, **kwargs: expected_response)
|
||||
response = classify_chat_route(request)
|
||||
|
||||
assert response.intent == "edit"
|
||||
assert response.edit_intent is not None
|
||||
assert response.edit_intent.mode == "info"
|
||||
|
||||
|
||||
def test_help_request(monkeypatch: MonkeyPatch):
|
||||
"""Help requests should route to edit/info"""
|
||||
request = ChatRouteRequest(
|
||||
message="help",
|
||||
has_files=False,
|
||||
has_create_session=False,
|
||||
has_edit_session=False,
|
||||
last_route="none",
|
||||
history=[],
|
||||
)
|
||||
expected_response = ChatRouteResponse(
|
||||
intent="edit",
|
||||
edit_intent=EditIntentHint(mode="info", requires_file_context=False),
|
||||
reason="Help request",
|
||||
)
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("chat_router.run_ai", lambda *args, **kwargs: expected_response)
|
||||
response = classify_chat_route(request)
|
||||
|
||||
assert response.intent == "edit"
|
||||
assert response.edit_intent is not None
|
||||
assert response.edit_intent.mode == "info"
|
||||
|
||||
|
||||
def test_actual_document_creation(monkeypatch: MonkeyPatch):
|
||||
"""Explicit creation requests should still route to create"""
|
||||
request = ChatRouteRequest(
|
||||
message="Create a business proposal document",
|
||||
has_files=False,
|
||||
has_create_session=False,
|
||||
has_edit_session=False,
|
||||
last_route="none",
|
||||
history=[],
|
||||
)
|
||||
expected_response = ChatRouteResponse(
|
||||
intent="create",
|
||||
create_intent=CreateIntentHint(action="start"),
|
||||
reason="User wants to create new document",
|
||||
)
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("chat_router.run_ai", lambda *args, **kwargs: expected_response)
|
||||
response = classify_chat_route(request)
|
||||
|
||||
assert response.intent == "create"
|
||||
assert response.create_intent is not None
|
||||
assert response.create_intent.action == "start"
|
||||
|
||||
|
||||
def test_edit_command_with_files(monkeypatch: MonkeyPatch):
|
||||
"""Edit commands should still route to edit/command"""
|
||||
request = ChatRouteRequest(
|
||||
message="Compress this PDF",
|
||||
has_files=True,
|
||||
has_create_session=False,
|
||||
has_edit_session=False,
|
||||
last_route="none",
|
||||
history=[],
|
||||
)
|
||||
expected_response = ChatRouteResponse(
|
||||
intent="edit",
|
||||
edit_intent=EditIntentHint(mode="command", requires_file_context=False),
|
||||
reason="User wants to compress PDF",
|
||||
)
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("chat_router.run_ai", lambda *args, **kwargs: expected_response)
|
||||
response = classify_chat_route(request)
|
||||
|
||||
assert response.intent == "edit"
|
||||
assert response.edit_intent is not None
|
||||
assert response.edit_intent.mode == "command"
|
||||
52
engine/tests/test_conversational_info.py
Normal file
52
engine/tests/test_conversational_info.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from editing.decisions import answer_conversational_info
|
||||
from file_processing_agent import ToolCatalogService
|
||||
|
||||
|
||||
def test_answer_conversational_info_greeting(monkeypatch: MonkeyPatch):
|
||||
"""Test handling of greeting without files"""
|
||||
|
||||
class MockResponse:
|
||||
message = "Hello! I can help you with PDF operations like compress, merge, split, and more."
|
||||
|
||||
def mock_run_ai(*args, **kwargs):
|
||||
return MockResponse()
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("editing.decisions.run_ai", mock_run_ai)
|
||||
|
||||
tool_catalog = ToolCatalogService()
|
||||
result = answer_conversational_info(
|
||||
message="Hello",
|
||||
history=[],
|
||||
tool_catalog=tool_catalog,
|
||||
)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_answer_conversational_info_capabilities(monkeypatch: MonkeyPatch):
|
||||
"""Test handling of capability questions"""
|
||||
|
||||
class MockResponse:
|
||||
message = "I can help with compress, merge, split, rotate, watermark, OCR, and many other PDF operations."
|
||||
|
||||
def mock_run_ai(*args, **kwargs):
|
||||
return MockResponse()
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("editing.decisions.run_ai", mock_run_ai)
|
||||
|
||||
tool_catalog = ToolCatalogService()
|
||||
result = answer_conversational_info(
|
||||
message="What can you do?",
|
||||
history=[],
|
||||
tool_catalog=tool_catalog,
|
||||
)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
31
engine/tests/test_env.py
Normal file
31
engine/tests/test_env.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import dotenv_values
|
||||
|
||||
ENGINE_ROOT = Path(__file__).parent.parent
|
||||
SRC_DIR = ENGINE_ROOT / "src"
|
||||
EXAMPLE_FILE = ENGINE_ROOT / "config" / ".env.example"
|
||||
|
||||
|
||||
def _parse_example_keys() -> set[str]:
|
||||
return set(dotenv_values(EXAMPLE_FILE).keys())
|
||||
|
||||
|
||||
def _find_stirling_env_vars() -> set[str]:
|
||||
env_vars: set[str] = set()
|
||||
for path in SRC_DIR.rglob("*.py"):
|
||||
for match in re.finditer(r"\b(STIRLING_\w+)\b", path.read_text()):
|
||||
env_vars.add(match.group(1))
|
||||
return env_vars
|
||||
|
||||
|
||||
def test_every_stirling_env_var_is_in_example_file():
|
||||
example_keys = _parse_example_keys()
|
||||
source_vars = _find_stirling_env_vars()
|
||||
missing = sorted(source_vars - example_keys)
|
||||
assert not missing, "env vars used in src/ but missing from config/.env.example:\n" + "\n".join(
|
||||
f" {v}" for v in missing
|
||||
)
|
||||
45
engine/tests/test_smart_folder_creator.py
Normal file
45
engine/tests/test_smart_folder_creator.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from models import (
|
||||
AvailableTool,
|
||||
SmartFolderAutomation,
|
||||
SmartFolderConfig,
|
||||
SmartFolderCreateRequest,
|
||||
SmartFolderCreateResponse,
|
||||
SmartFolderOperation,
|
||||
)
|
||||
from smart_folder_creator import create_smart_folder_config
|
||||
|
||||
|
||||
def _build_sample_response() -> SmartFolderCreateResponse:
|
||||
return SmartFolderCreateResponse(
|
||||
assistant_message="I will build that folder for you.",
|
||||
smart_folder_config=SmartFolderConfig(
|
||||
name="Email Prep",
|
||||
description="Compress and split for email",
|
||||
automation=SmartFolderAutomation(
|
||||
name="Email Cleanup",
|
||||
description="Email prep steps",
|
||||
operations=[SmartFolderOperation(operation="compress-pdf", parameters='{"compressionLevel": 3}')],
|
||||
),
|
||||
icon="mail",
|
||||
accent_color="#0ea5e9",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_create_smart_folder_config_calls_ai_and_returns_response(monkeypatch: MonkeyPatch):
|
||||
request = SmartFolderCreateRequest(
|
||||
message="Create a folder that zips attachments",
|
||||
history=[],
|
||||
available_tools=[AvailableTool(id="compress-pdf", name="Compress PDFs")],
|
||||
)
|
||||
response_value = _build_sample_response()
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("smart_folder_creator.run_ai", lambda *args, **kwargs: response_value)
|
||||
result = create_smart_folder_config(request)
|
||||
|
||||
assert result == response_value
|
||||
Reference in New Issue
Block a user