From dfb7e6b25197ce11011d6d1a2d190ccdc9b028f0 Mon Sep 17 00:00:00 2001 From: Opulence Chuks Date: Fri, 17 Jul 2026 11:54:11 +0100 Subject: [PATCH] feat: expose model_version in responses for humanitarian, anonymize, and ocr schemas --- app/ai-service/api/routes.py | 5 ++ app/ai-service/api/v1/anonymize.py | 6 ++- app/ai-service/api/v1/humanitarian.py | 19 ++++++- app/ai-service/api/v1/ocr.py | 5 ++ app/ai-service/schemas/anonymization.py | 3 +- app/ai-service/schemas/humanitarian.py | 39 ++++++++------- app/ai-service/schemas/ocr.py | 3 ++ app/ai-service/tests/test_schemas.py | 49 +++++++++++++++++++ app/ai-service/tests/test_versioned_routes.py | 9 +++- 9 files changed, 114 insertions(+), 24 deletions(-) diff --git a/app/ai-service/api/routes.py b/app/ai-service/api/routes.py index 2281f5e5..99223a74 100644 --- a/app/ai-service/api/routes.py +++ b/app/ai-service/api/routes.py @@ -76,6 +76,7 @@ async def process_ocr( metrics.logger.info(f"OCR Inference completed in {inference_latency:.4f}s") processing_time_ms = int((time.time() - start_time) * 1000) + model_version = settings.groq_model if settings.get_active_provider() == "groq" else settings.openai_model return OCRResponse( success=True, @@ -88,12 +89,14 @@ async def process_ocr( processing_time_ms=processing_time_ms, ), processing_time_ms=processing_time_ms, + model_version=model_version, ) except HTTPException: raise except Exception as e: processing_time_ms = int((time.time() - start_time) * 1000) + model_version = settings.groq_model if settings.get_active_provider() == "groq" else settings.openai_model return OCRResponse( success=False, error={ @@ -101,4 +104,6 @@ async def process_ocr( "message": str(e), }, processing_time_ms=processing_time_ms, + model_version=model_version, ) + diff --git a/app/ai-service/api/v1/anonymize.py b/app/ai-service/api/v1/anonymize.py index 3ed1e779..ee39c18c 100644 --- a/app/ai-service/api/v1/anonymize.py +++ b/app/ai-service/api/v1/anonymize.py @@ -22,7 +22,11 @@ async def anonymize_text(request: AnonymizeRequest): try: result = _main.pii_scrubber_service.anonymize(request.text) - return AnonymizeResponse(success=True, **result) + from config import settings + active_p = settings.get_active_provider() + model_version = settings.groq_model if active_p == "groq" else settings.openai_model + return AnonymizeResponse(success=True, model_version=model_version, **result) except Exception as e: logger.error(f"Anonymization failed: {str(e)}", exc_info=True) raise HTTPException(status_code=500, detail="Failed to anonymize text") + diff --git a/app/ai-service/api/v1/humanitarian.py b/app/ai-service/api/v1/humanitarian.py index 8d4dac2c..eb8a72bb 100644 --- a/app/ai-service/api/v1/humanitarian.py +++ b/app/ai-service/api/v1/humanitarian.py @@ -44,7 +44,22 @@ async def verify_humanitarian_claim(request: HumanitarianVerificationRequest): ) else: raise exc - return HumanitarianVerificationResponse(success=True, **result) + from config import settings + provider = result.get("provider") + if provider == "openai": + model_version = settings.openai_model + elif provider == "groq": + model_version = settings.groq_model + else: + # Default fallback to active provider model or settings.openai_model + active_p = settings.get_active_provider() + model_version = settings.groq_model if active_p == "groq" else settings.openai_model + + return HumanitarianVerificationResponse(success=True, model_version=model_version, **result) except Exception as e: logger.error("Humanitarian verification failed: %s", str(e), exc_info=True) - return HumanitarianVerificationResponse(success=False, error=str(e)) + from config import settings + active_p = settings.get_active_provider() + model_version = settings.groq_model if active_p == "groq" else settings.openai_model + return HumanitarianVerificationResponse(success=False, error=str(e), model_version=model_version) + diff --git a/app/ai-service/api/v1/ocr.py b/app/ai-service/api/v1/ocr.py index 5fd170ae..08059bf6 100644 --- a/app/ai-service/api/v1/ocr.py +++ b/app/ai-service/api/v1/ocr.py @@ -87,6 +87,7 @@ async def process_ocr( metrics.logger.info(f"OCR Inference completed in {inference_latency:.4f}s") processing_time_ms = int((time.time() - start_time) * 1000) + model_version = settings.groq_model if settings.get_active_provider() == "groq" else settings.openai_model return OCRResponse( success=True, @@ -99,12 +100,14 @@ async def process_ocr( processing_time_ms=processing_time_ms, ), processing_time_ms=processing_time_ms, + model_version=model_version, ) except HTTPException: raise except Exception as e: processing_time_ms = int((time.time() - start_time) * 1000) + model_version = settings.groq_model if settings.get_active_provider() == "groq" else settings.openai_model return OCRResponse( success=False, error={ @@ -112,4 +115,6 @@ async def process_ocr( "message": str(e), }, processing_time_ms=processing_time_ms, + model_version=model_version, ) + diff --git a/app/ai-service/schemas/anonymization.py b/app/ai-service/schemas/anonymization.py index a9bb9c19..2f7ddf40 100644 --- a/app/ai-service/schemas/anonymization.py +++ b/app/ai-service/schemas/anonymization.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, Optional from pydantic import BaseModel, Field @@ -20,3 +20,4 @@ class AnonymizeResponse(BaseModel): original_length: int pii_summary: PIISummary token_counts: Dict[str, int] = Field(default_factory=dict) + model_version: Optional[str] = None diff --git a/app/ai-service/schemas/humanitarian.py b/app/ai-service/schemas/humanitarian.py index 1e8dc493..3fbc1d04 100644 --- a/app/ai-service/schemas/humanitarian.py +++ b/app/ai-service/schemas/humanitarian.py @@ -1,19 +1,20 @@ -from typing import Any, Dict, List, Literal, Optional -from pydantic import BaseModel, Field - - -class HumanitarianVerificationRequest(BaseModel): - aid_claim: str = Field(min_length=10, description="Aid claim to verify") - supporting_evidence: List[str] = Field(default_factory=list) - context_factors: Dict[str, Any] = Field(default_factory=dict) - provider_preference: Literal["auto", "test", "openai", "groq"] = "auto" - timeout: Optional[float] = Field(default=None, description="Request-level timeout in seconds for provider call") - - -class HumanitarianVerificationResponse(BaseModel): - success: bool - provider: Optional[str] = None - model: Optional[str] = None - prompt_variant: Optional[str] = None - verification: Optional[Dict[str, Any]] = None - error: Optional[str] = None \ No newline at end of file +from typing import Any, Dict, List, Literal, Optional +from pydantic import BaseModel, Field + + +class HumanitarianVerificationRequest(BaseModel): + aid_claim: str = Field(min_length=10, description="Aid claim to verify") + supporting_evidence: List[str] = Field(default_factory=list) + context_factors: Dict[str, Any] = Field(default_factory=dict) + provider_preference: Literal["auto", "test", "openai", "groq"] = "auto" + timeout: Optional[float] = Field(default=None, description="Request-level timeout in seconds for provider call") + + +class HumanitarianVerificationResponse(BaseModel): + success: bool + provider: Optional[str] = None + model: Optional[str] = None + prompt_variant: Optional[str] = None + verification: Optional[Dict[str, Any]] = None + error: Optional[str] = None + model_version: Optional[str] = None \ No newline at end of file diff --git a/app/ai-service/schemas/ocr.py b/app/ai-service/schemas/ocr.py index 5432d868..f0ecf4f7 100644 --- a/app/ai-service/schemas/ocr.py +++ b/app/ai-service/schemas/ocr.py @@ -1,3 +1,4 @@ +from typing import Optional from pydantic import BaseModel, Field @@ -17,3 +18,5 @@ class OCRResponse(BaseModel): data: OCRData | None = None error: dict[str, str] | None = None processing_time_ms: int + model_version: Optional[str] = None + diff --git a/app/ai-service/tests/test_schemas.py b/app/ai-service/tests/test_schemas.py index 0c5d3cfd..93a092c8 100644 --- a/app/ai-service/tests/test_schemas.py +++ b/app/ai-service/tests/test_schemas.py @@ -66,3 +66,52 @@ def test_valid_error_detail(self): response = OCRResponse(success=False, error=error, processing_time_ms=0) assert response.error == error assert response.error["code"] == "test_error" + + +class TestSchemaModelVersionSnapshots: + """Pins model_version in expected output schema snapshots.""" + + def test_humanitarian_verification_response_snapshot(self): + from schemas.humanitarian import HumanitarianVerificationResponse + resp = HumanitarianVerificationResponse( + success=True, + provider="openai", + model="gpt-4o-mini", + prompt_variant="primary", + verification={"verdict": "credible", "confidence": 0.95}, + model_version="gpt-4o-mini" + ) + snapshot = resp.model_dump() + assert snapshot["model_version"] == "gpt-4o-mini" + assert snapshot["success"] is True + assert snapshot["provider"] == "openai" + + def test_anonymize_response_snapshot(self): + from schemas.anonymization import AnonymizeResponse, PIISummary + resp = AnonymizeResponse( + success=True, + anonymized_text="Hello [RECIPIENT_NAME]", + original_length=15, + pii_summary=PIISummary(names=1, locations=0, dates=0, total=1), + model_version="gpt-4o-mini" + ) + snapshot = resp.model_dump() + assert snapshot["model_version"] == "gpt-4o-mini" + assert snapshot["success"] is True + + def test_ocr_response_snapshot(self): + from schemas.ocr import OCRResponse, OCRData, OCRFieldResult + resp = OCRResponse( + success=True, + data=OCRData( + fields={"name": OCRFieldResult(value="John", confidence=0.9)}, + raw_text="Name: John", + processing_time_ms=120 + ), + processing_time_ms=120, + model_version="gpt-4o-mini" + ) + snapshot = resp.model_dump() + assert snapshot["model_version"] == "gpt-4o-mini" + assert snapshot["success"] is True + diff --git a/app/ai-service/tests/test_versioned_routes.py b/app/ai-service/tests/test_versioned_routes.py index ce5898ed..cb2b05a9 100644 --- a/app/ai-service/tests/test_versioned_routes.py +++ b/app/ai-service/tests/test_versioned_routes.py @@ -173,7 +173,10 @@ def test_v1_ocr_processing_time_present(self, client): files={"image": ("img.png", buf.getvalue(), "image/png")}, ) assert response.status_code == 200 - assert "processing_time_ms" in response.json() + data = response.json() + assert "processing_time_ms" in data + assert data["model_version"] == "gpt-4o-mini" + # --------------------------------------------------------------------------- @@ -282,6 +285,8 @@ def test_v1_anonymize_success(self, following_client): data = response.json() assert data["success"] is True assert "anonymized_text" in data + assert data["model_version"] == "gpt-4o-mini" + def test_v1_anonymize_empty_text_returns_422(self, following_client): response = following_client.post("/v1/ai/anonymize", json={"text": ""}) @@ -325,6 +330,8 @@ def fake_verify( data = response.json() assert data["success"] is True assert data["verification"]["verdict"] == "credible" + assert data["model_version"] == "gpt-4o-mini" + def test_v1_humanitarian_verify_failure_path(self, following_client, monkeypatch): def fake_verify(