Add shared ai-service container as AI provider intermediary
All feature containers now POST messages to ai-service (port 8010) instead of calling AI providers directly. ai-service routes to LM Studio, Ollama, or Anthropic based on /config/ai_service_config.json. doc-service AI providers removed; replaced by httpx ai_client.py. Backend settings restructured to /api/settings/ai. Frontend gets dedicated AIAdminSettingsPage and AI Service card in AppsPage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,12 +75,13 @@ MOCK_AI_RESULT = {
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ai():
|
||||
"""Patch the AI classify_document call to return MOCK_AI_RESULT."""
|
||||
provider_mock = AsyncMock()
|
||||
provider_mock.classify_document = AsyncMock(return_value=MOCK_AI_RESULT)
|
||||
with patch("app.routers.documents.get_provider", return_value=provider_mock):
|
||||
yield provider_mock
|
||||
def mock_ai_service():
|
||||
"""Patch classify_document to return MOCK_AI_RESULT without hitting ai-service."""
|
||||
with patch(
|
||||
"app.services.ai_client.classify_document",
|
||||
new=AsyncMock(return_value=MOCK_AI_RESULT),
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
# ── HTTP client ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -189,7 +189,7 @@ async def test_cannot_assign_other_users_category(client, other_client, minimal_
|
||||
|
||||
# ── AI processing integration (with mock AI) ──────────────────────────────────
|
||||
|
||||
async def test_processing_sets_extracted_data(client, invoice_pdf, mock_ai):
|
||||
async def test_processing_sets_extracted_data(client, invoice_pdf, mock_ai_service):
|
||||
"""Upload + wait for background processing; verify extracted_data is populated."""
|
||||
r = await client.post("/documents/upload", files=_pdf_upload("invoice.pdf", invoice_pdf))
|
||||
assert r.status_code == 202
|
||||
@@ -217,9 +217,35 @@ async def test_processing_sets_extracted_data(client, invoice_pdf, mock_ai):
|
||||
assert len(extracted["suggested_categories"]) > 0
|
||||
|
||||
|
||||
# ── Graceful degradation when ai-service is unavailable ──────────────────────
|
||||
|
||||
async def test_processing_fails_gracefully_when_ai_service_502(client, invoice_pdf):
|
||||
"""When ai-service returns an error, document status should be 'failed', not crash."""
|
||||
from app.services.ai_client import AIServiceError
|
||||
|
||||
with patch(
|
||||
"app.services.ai_client.classify_document",
|
||||
side_effect=AIServiceError("ai-service returned 502"),
|
||||
):
|
||||
r = await client.post("/documents/upload", files=_pdf_upload("fail.pdf", invoice_pdf))
|
||||
assert r.status_code == 202
|
||||
doc_id = r.json()["id"]
|
||||
|
||||
import asyncio
|
||||
for _ in range(20):
|
||||
status_r = await client.get(f"/documents/{doc_id}/status")
|
||||
if status_r.json()["status"] in ("done", "failed"):
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
doc = (await client.get(f"/documents/{doc_id}")).json()
|
||||
assert doc["status"] == "failed"
|
||||
assert "ai-service" in (doc.get("error_message") or "").lower()
|
||||
|
||||
|
||||
# ── Live tests (require real PDFs in tests/pdfs/) ─────────────────────────────
|
||||
|
||||
async def test_live_upload_real_pdf(client, real_pdfs, mock_ai):
|
||||
async def test_live_upload_real_pdf(client, real_pdfs, mock_ai_service):
|
||||
"""Upload each real PDF from tests/pdfs/ and verify it reaches 'done'."""
|
||||
import asyncio
|
||||
for pdf_path in real_pdfs:
|
||||
|
||||
Reference in New Issue
Block a user