File size: 2,727 Bytes
569bceb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
"""Canonical paragraph rollout: scan indexed chunks, optionally apply replacement to .docx."""

import logging

from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.rate_limit import check_generate, check_read
from app.db.database import get_db
from app.models.schemas import (
    CanonicalApplyRequest,
    CanonicalApplyResponse,
    CanonicalRolloutRequest,
    CanonicalRolloutResponse,
)
from app.services.canonical_rollout import (
    apply_canonical_replacement,
    scan_canonical_rollout,
)

logger = logging.getLogger(__name__)

router = APIRouter()


@router.post(
    "/content/canonical-scan",
    response_model=CanonicalRolloutResponse,
    summary="Find chunks semantically similar to a canonical paragraph",
)
async def canonical_scan(
    request: Request,
    body: CanonicalRolloutRequest,
    db: AsyncSession = Depends(get_db),
    _: None = Depends(check_read),
) -> CanonicalRolloutResponse:
    """Discover candidate chunks for upgrading to an improved standard paragraph.

    Uses the same embedding search as the rest of the RAG stack, plus token Jaccard
    versus the canonical text to reduce false positives. Proposed text is either
    the canonical paragraph or an LLM merge (``adapt_with_llm``) when an API key is set.

    Review ``compatibility`` (high / review / low) before any automatic apply policy.
    """
    tenant_id: str = request.state.tenant_id
    try:
        return await scan_canonical_rollout(db, tenant_id, body)
    except Exception as exc:  # noqa: BLE001
        logger.exception("canonical-scan failed: %s", exc)
        raise HTTPException(status_code=500, detail="Canonical scan failed") from exc


@router.post(
    "/content/canonical-apply",
    response_model=CanonicalApplyResponse,
    summary="Replace one chunk in a .docx and re-index (destructive layout)",
)
async def canonical_apply(
    request: Request,
    body: CanonicalApplyRequest,
    db: AsyncSession = Depends(get_db),
    _: None = Depends(check_generate),
) -> CanonicalApplyResponse:
    """Apply a replacement string for one indexed chunk.

    Rebuilds the Word file as plain paragraphs (tables/complex layout are not preserved),
    deletes old vector rows for the document, and re-runs ingestion.

    **Requires** ``confirm_destructive_docx: true``.
    """
    tenant_id: str = request.state.tenant_id
    try:
        return await apply_canonical_replacement(db, tenant_id, body)
    except HTTPException:
        raise
    except Exception as exc:  # noqa: BLE001
        logger.exception("canonical-apply failed: %s", exc)
        raise HTTPException(status_code=500, detail="Canonical apply failed") from exc