Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion app/ai-service/api/v1/anonymize.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import logging
import uuid

from fastapi import APIRouter, HTTPException

Expand All @@ -18,11 +19,42 @@ async def anonymize_text(request: AnonymizeRequest):
"""Anonymize names, locations, and dates before text is sent to external LLMs."""
import main as _main

trace_id = str(uuid.uuid4())
logger.info("Processing privacy-preserving anonymization request")

try:
result = _main.pii_scrubber_service.anonymize(request.text)
return AnonymizeResponse(success=True, **result)

pii_summary = result.get("pii_summary", {})
total_redacted = (
pii_summary.get("total", 0)
if isinstance(pii_summary, dict)
else getattr(pii_summary, "total", 0)
)

reasons = ["PII scrubbing completed successfully"]
if total_redacted:
reasons.append(f"{total_redacted} PII token(s) redacted")
else:
reasons.append("No PII detected in input")

# Strip envelope keys so our explicit values take precedence.
_ENVELOPE_KEYS = {"result", "confidence", "reasons", "anchor_metadata", "trace_id"}
safe_result = {k: v for k, v in result.items() if k not in _ENVELOPE_KEYS}

return AnonymizeResponse(
success=True,
**safe_result,
# Standard result envelope fields (Issue #609)
result="anonymization_complete",
confidence=None, # rule-based; no probabilistic confidence
reasons=reasons,
anchor_metadata={
"original_length": result.get("original_length"),
"pii_total": total_redacted,
},
trace_id=trace_id,
)
except Exception as e:
logger.error(f"Anonymization failed: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to anonymize text")
34 changes: 33 additions & 1 deletion app/ai-service/api/v1/fraud.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import logging
import uuid

from fastapi import APIRouter, HTTPException

Expand All @@ -23,11 +24,42 @@ async def detect_fraud_endpoint(request: FraudDetectionRequest) -> FraudDetectio
statistical outliers relative to the batch are flagged with
``is_flagged=true``.
"""
trace_id = str(uuid.uuid4())

try:
results = detect_fraud(request.claims)
flagged_count = sum(r.is_flagged for r in results)

# Derive top-level confidence as 1 - mean fraud risk score (ensemble certainty).
if results:
mean_risk = sum(r.fraud_risk_score for r in results) / len(results)
confidence = round(1.0 - mean_risk, 4)
else:
confidence = None

result_label = "fraud_detected" if flagged_count > 0 else "no_fraud_detected"

reasons: list[str] = [f"Analysed {len(results)} claim(s)"]
if flagged_count:
reasons.append(f"{flagged_count} claim(s) flagged as suspicious")
flagged_ids = [r.claim_id for r in results if r.is_flagged]
if flagged_ids:
reasons.append(f"Flagged claim IDs: {', '.join(flagged_ids)}")
else:
reasons.append("No suspicious patterns detected")

return FraudDetectionResponse(
results=results,
flagged_count=sum(r.is_flagged for r in results),
flagged_count=flagged_count,
# Standard result envelope fields (Issue #609)
result=result_label,
confidence=confidence,
reasons=reasons,
anchor_metadata={
"total_claims": len(results),
"flagged_count": flagged_count,
},
trace_id=trace_id,
)
except Exception as exc:
logger.error("Fraud detection failed: %s", exc)
Expand Down
46 changes: 44 additions & 2 deletions app/ai-service/api/v1/humanitarian.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import logging
import uuid

from fastapi import APIRouter

Expand All @@ -23,6 +24,7 @@ async def verify_humanitarian_claim(request: HumanitarianVerificationRequest):
# tests (and any future dependency-injection wiring) works transparently.
import main as _main

trace_id = str(uuid.uuid4())
logger.info("Processing humanitarian verification request")

try:
Expand All @@ -44,7 +46,47 @@ async def verify_humanitarian_claim(request: HumanitarianVerificationRequest):
)
else:
raise exc
return HumanitarianVerificationResponse(success=True, **result)

# Extract envelope fields from the nested verification dict.
verification = result.get("verification") or {}
confidence_raw = verification.get("confidence")
confidence = float(confidence_raw) if confidence_raw is not None else None
verdict = verification.get("verdict")
summary = verification.get("summary")

result_label = f"humanitarian_{verdict}" if verdict else "humanitarian_verified"

reasons: list[str] = []
if verdict:
reasons.append(f"Verdict: {verdict}")
if summary:
reasons.append(summary)

# Strip any envelope-conflicting keys from the service result dict before
# spreading, so our explicit envelope values always take precedence.
_ENVELOPE_KEYS = {"result", "confidence", "reasons", "anchor_metadata", "trace_id"}
safe_result = {k: v for k, v in result.items() if k not in _ENVELOPE_KEYS}

return HumanitarianVerificationResponse(
success=True,
**safe_result,
# Standard result envelope fields (Issue #609)
result=result_label,
confidence=confidence,
reasons=reasons or None,
anchor_metadata={
"provider": result.get("provider"),
"model": result.get("model"),
"prompt_variant": result.get("prompt_variant"),
},
trace_id=trace_id,
)
except Exception as e:
logger.error("Humanitarian verification failed: %s", str(e), exc_info=True)
return HumanitarianVerificationResponse(success=False, error=str(e))
return HumanitarianVerificationResponse(
success=False,
error=str(e),
result="humanitarian_error",
reasons=[str(e)],
trace_id=trace_id,
)
26 changes: 26 additions & 0 deletions app/ai-service/api/v1/ocr.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import io
import time
import uuid
from typing import Annotated

from fastapi import APIRouter, File, HTTPException, Request, UploadFile
Expand Down Expand Up @@ -40,6 +41,7 @@ async def process_ocr(
) -> OCRResponse:
"""Extract text fields from an uploaded document image."""
start_time = time.time()
trace_id = str(uuid.uuid4())

if image.content_type not in ALLOWED_CONTENT_TYPES:
raise HTTPException(
Expand Down Expand Up @@ -87,6 +89,16 @@ async def process_ocr(

processing_time_ms = int((time.time() - start_time) * 1000)

# Derive top-level confidence as the mean of all extracted field confidences.
field_confidences = [f.confidence for f in result.fields.values()]
avg_confidence = (
sum(field_confidences) / len(field_confidences) if field_confidences else None
)

reasons = ["OCR extraction completed successfully"]
if avg_confidence is not None:
reasons.append(f"Average field confidence: {avg_confidence:.2f}")

return OCRResponse(
success=True,
data=OCRData(
Expand All @@ -98,6 +110,15 @@ async def process_ocr(
processing_time_ms=processing_time_ms,
),
processing_time_ms=processing_time_ms,
# Standard result envelope fields (Issue #609)
result="ocr_complete",
confidence=avg_confidence,
reasons=reasons,
anchor_metadata={
"filename": image.filename,
"content_type": image.content_type,
},
trace_id=trace_id,
)

except HTTPException:
Expand All @@ -111,4 +132,9 @@ async def process_ocr(
"message": str(e),
},
processing_time_ms=processing_time_ms,
# Standard result envelope fields (Issue #609)
result="ocr_error",
reasons=[str(e)],
anchor_metadata={"filename": image.filename},
trace_id=trace_id,
)
49 changes: 43 additions & 6 deletions app/ai-service/api/v1/proof_of_life.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
"""

import logging
import uuid
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field

from schemas.envelope import ResultEnvelope

logger = logging.getLogger(__name__)

router = APIRouter(tags=["proof-of-life"])
Expand All @@ -21,14 +24,19 @@ class ProofOfLifeRequest(BaseModel):
confidence_threshold: Optional[float] = Field(default=None, ge=0.0, le=1.0)


class ProofOfLifeResponse(BaseModel):
"""Response model for proof-of-life analysis."""
class ProofOfLifeResponse(ResultEnvelope):
"""
Proof-of-life analysis response – includes the standardised result envelope (Issue #609).

Backward-compatible: existing fields (is_real_person, confidence, threshold,
checks, reason) are preserved alongside the new envelope fields.
"""

is_real_person: bool
confidence: float
confidence: float # overrides Optional[float] from envelope – always present here
threshold: float
checks: Dict[str, Any]
reason: str
reason: str # kept for backward compat; also surfaced as reasons[0]


@router.post("/ai/proof-of-life", response_model=ProofOfLifeResponse)
Expand All @@ -43,15 +51,44 @@ async def analyze_proof_of_life(request: ProofOfLifeRequest):

import main as _main

trace_id = str(uuid.uuid4())
logger.info("Processing proof-of-life verification request")

try:
result = _main.proof_of_life_analyzer.analyze(
raw = _main.proof_of_life_analyzer.analyze(
selfie_image_base64=request.selfie_image_base64,
burst_images_base64=request.burst_images_base64,
confidence_threshold=request.confidence_threshold,
)
return result

# raw may be a dict or a Pydantic model; normalise to dict.
if isinstance(raw, dict):
data = raw
else:
data = raw.model_dump() if hasattr(raw, "model_dump") else dict(raw)

is_real = data.get("is_real_person", False)
reason_str = data.get("reason", "")

result_label = "real_person" if is_real else "not_real_person"
reasons = [reason_str] if reason_str else []

# Strip envelope keys from data so our explicit values take precedence.
_ENVELOPE_KEYS = {"result", "confidence", "reasons", "anchor_metadata", "trace_id"}
safe_data = {k: v for k, v in data.items() if k not in _ENVELOPE_KEYS}

return ProofOfLifeResponse(
**safe_data,
# Standard result envelope fields (Issue #609)
result=result_label,
# confidence is a required field in ProofOfLifeResponse, already in safe_data
reasons=reasons or None,
anchor_metadata={
"threshold": data.get("threshold"),
"checks": data.get("checks"),
},
trace_id=trace_id,
)
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
except Exception as e:
Expand Down
6 changes: 5 additions & 1 deletion app/ai-service/schemas/anonymization.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from pydantic import BaseModel, Field

from schemas.envelope import ResultEnvelope


class AnonymizeRequest(BaseModel):
text: str = Field(min_length=1, description="Input text to anonymize before LLM processing")
Expand All @@ -14,7 +16,9 @@ class PIISummary(BaseModel):
total: int


class AnonymizeResponse(BaseModel):
class AnonymizeResponse(ResultEnvelope):
"""Anonymization endpoint response – includes the standardised result envelope (Issue #609)."""

success: bool
anonymized_text: str
original_length: int
Expand Down
Loading
Loading