File size: 10,965 Bytes
be9fd4a
 
 
 
 
 
 
 
 
 
 
 
 
970bad4
be9fd4a
 
 
 
 
 
 
 
970bad4
be9fd4a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"""AI feature phases only (no Redis, job queue, Temporal, or other backend scale).

Phase 1 — Core RAG generation (standard pipeline)
Phase 2 — Agentic inspector, vision, optional LLM validators / ingest sanitiser
Phase 3 — AI latency & retrieval quality (async LLM, speculation, prompt cache, hybrid RAG)
"""

from __future__ import annotations

from typing import Any

from app.agentic.runtime_status import inspector_public_status, is_openai_inspector_live
from app.agentic.speculative_executor import speculative_execution_active
from app.config import effective_openai_api_key, settings
from app.db.database import (
    is_sqlite_database,
    multi_section_parallel_enabled,
    parallel_section_writes_safe,
)
from app.llm.prompt_cache import prompt_caching_active
from app.retrieval.semantic_cache import is_semantic_cache_active
def _openai_configured() -> bool:
    return bool(effective_openai_api_key())


def ai_phase1_status() -> dict[str, Any]:
    """Embeddings, retrieval, standard generate / proofread / enhance."""
    backend = (settings.vectorstore_backend or "faiss").strip().lower()
    return {
        "name": "core_rag_generation",
        "openai_required": True,
        "openai_configured": _openai_configured(),
        "live": _openai_configured(),
        "capabilities": {
            "generate_proofread_enhance": _openai_configured(),
            "notes_expansion_llm": _openai_configured(),
            "style_analyzer_llm": _openai_configured(),
            "postprocess_verify_llm": _openai_configured(),
            "embeddings": {
                "prefer_local": bool(settings.prefer_local_embeddings),
                "local_model": settings.local_embedding_model,
                "openai_model": settings.embedding_model if not settings.prefer_local_embeddings else None,
            },
            "vector_retrieval": {
                "backend": backend,
                "hierarchical_rag": bool(settings.hierarchical_rag_enabled),
                "hybrid_retrieval": bool(settings.enable_hybrid_retrieval) and backend == "qdrant",
                "semantic_cache": is_semantic_cache_active(),
            },
            "personalised_style_rag": bool(settings.personalised_style_rag_enabled),
            "rag_upload_sanitisation": bool(settings.enable_rag_upload_sanitisation),
            "rag_sanitisation_regex_only": bool(settings.enable_rag_upload_sanitisation)
            and not bool(getattr(settings, "rag_sanitisation_use_llm", False)),
            "notes_only_generation": bool(settings.notes_only_generation),
            "interference_levels": True,
            "llm_section_validator": bool(settings.llm_section_validator_enabled),
        },
        "flags": {
            "chat_model": (settings.chat_model or "").strip() or None,
            "prefer_local_embeddings": bool(settings.prefer_local_embeddings),
            "vectorstore_backend": backend,
            "personalised_style_rag_enabled": bool(settings.personalised_style_rag_enabled),
            "enable_rag_upload_sanitisation": bool(settings.enable_rag_upload_sanitisation),
            "notes_only_generation": bool(settings.notes_only_generation),
        },
    }


def ai_phase2_status() -> dict[str, Any]:
    """Inspector tool loop, agentic generate, section photo vision."""
    key_ok = _openai_configured()
    vision_on = bool(settings.section_photo_vision_enabled)
    notes_only = bool(settings.notes_only_generation)
    agentic_on_notes = bool(settings.agentic_inspector_when_notes_only)
    post_generate_inspector = (not notes_only or agentic_on_notes) and is_openai_inspector_live()

    return {
        "name": "agentic_inspector_and_vision",
        "openai_required": True,
        "openai_configured": key_ok,
        "live": key_ok and (is_openai_inspector_live() or vision_on),
        "capabilities": {
            "inspector_tool_agent": bool(settings.inspector_tool_agent),
            "inspector_live": is_openai_inspector_live(),
            "post_generate_uses_inspector": post_generate_inspector,
            "agentic_generate_endpoint": key_ok and is_openai_inspector_live(),
            "section_photo_vision": key_ok and vision_on,
            "section_photo_analyze_on_upload": bool(
                getattr(settings, "section_photo_analyze_on_upload", False)
            ),
            "rag_sanitisation_llm_ingest": bool(
                settings.enable_rag_upload_sanitisation
                and getattr(settings, "rag_sanitisation_use_llm", False)
            ),
            "llm_section_validator": bool(settings.llm_section_validator_enabled),
        },
        "flags": {
            "inspector_tool_agent": bool(settings.inspector_tool_agent),
            "inspector_body_model": (settings.inspector_body_model or "").strip() or None,
            "section_photo_vision_model": (
                (getattr(settings, "section_photo_vision_model", None) or "").strip()
                or (settings.chat_model or "").strip()
                or "gpt-4o"
            ),
            "agentic_inspector_when_notes_only": agentic_on_notes,
            "primary_generate_pipeline": (settings.primary_generate_pipeline or "agentic")
            .strip()
            .lower(),
            "production_ai_profile": bool(getattr(settings, "production_ai_profile", False)),
        },
        "rics_inspector": inspector_public_status(),
    }


def ai_phase3_status() -> dict[str, Any]:
    """Faster / cheaper OpenAI usage and stronger retrieval — not deployment queues."""
    async_on = bool(settings.enable_async_pipeline)
    backend = (settings.vectorstore_backend or "faiss").strip().lower()
    sla_s = int(getattr(settings, "generation_sla_seconds", 600))
    return {
        "name": "ai_latency_and_retrieval_quality",
        "openai_required": True,
        "openai_configured": _openai_configured(),
        "live": _openai_configured() and async_on,
        "generation_sla_seconds": sla_s,
        "generation_timeout_seconds": int(getattr(settings, "generation_timeout_seconds", 720)),
        "capabilities": {
            "async_llm_pipeline": async_on,
            "parallel_multi_section": multi_section_parallel_enabled(),
            "speculative_inspector_tools": speculative_execution_active(),
            "openai_prompt_cache": prompt_caching_active(),
            "hybrid_retrieval_active": bool(settings.enable_hybrid_retrieval)
            and backend == "qdrant",
            "semantic_retrieval_cache_active": is_semantic_cache_active(),
        },
        "flags": {
            "enable_async_pipeline": async_on,
            "enable_speculative_executor": bool(settings.enable_speculative_executor),
            "enable_prompt_caching": bool(settings.enable_prompt_caching),
            "max_concurrent_llm_calls": int(settings.max_concurrent_llm_calls),
            "vectorstore_backend": backend,
            "enable_hybrid_retrieval": bool(settings.enable_hybrid_retrieval),
            "semantic_cache_enabled": bool(settings.semantic_cache_enabled),
        },
    }


def collect_ai_phases() -> dict[str, Any]:
    return {
        "phase1": ai_phase1_status(),
        "phase2": ai_phase2_status(),
        "phase3": ai_phase3_status(),
    }


def collect_ai_phase_warnings() -> list[str]:
    """Misconfigurations that weaken AI behaviour (excludes Redis/Temporal/job queue)."""
    warnings: list[str] = []
    key_ok = _openai_configured()

    if not key_ok:
        warnings.append(
            "OPENAI_API_KEY unset: all LLM phases (generation, inspector, vision) are off."
        )
    if key_ok and not settings.section_photo_vision_enabled:
        warnings.append(
            "SECTION_PHOTO_VISION_ENABLED=false: photos are stored but not analysed by vision at generate."
        )
    if key_ok and not settings.inspector_tool_agent:
        warnings.append(
            "INSPECTOR_TOOL_AGENT=false: agentic paths use the legacy fixed pipeline, not tool-calling."
        )
    if (
        key_ok
        and settings.notes_only_generation
        and not settings.agentic_inspector_when_notes_only
    ):
        warnings.append(
            "Phase 2 inactive on POST /generate: NOTES_ONLY_GENERATION=true and "
            "AGENTIC_INSPECTOR_WHEN_NOTES_ONLY=false. Use agentic/generate or enable "
            "AGENTIC_INSPECTOR_WHEN_NOTES_ONLY."
        )
    if key_ok and settings.enable_rag_upload_sanitisation and getattr(
        settings, "rag_sanitisation_use_llm", False
    ):
        warnings.append(
            "RAG_SANITISATION_USE_LLM=true: ingest competes with generation/vision quota; "
            "use regex-only in production (auto-off on HF via production_ai_profile)."
        )
    if not settings.enable_rag_upload_sanitisation and settings.personalised_style_rag_enabled:
        warnings.append(
            "ENABLE_RAG_UPLOAD_SANITISATION=false with personalised RAG: uploads indexed without PII redaction."
        )
    if settings.enable_speculative_executor and not speculative_execution_active():
        warnings.append(
            "Phase 3: ENABLE_SPECULATIVE_EXECUTOR=true but speculation inactive "
            "(requires ENABLE_ASYNC_PIPELINE=true and Phase 2 inspector path)."
        )
    if settings.enable_prompt_caching and not prompt_caching_active():
        warnings.append(
            "Phase 3: ENABLE_PROMPT_CACHING=true requires ENABLE_ASYNC_PIPELINE=true."
        )
    if settings.enable_async_pipeline and not key_ok:
        warnings.append("Phase 3: ENABLE_ASYNC_PIPELINE=true has no effect without OPENAI_API_KEY.")
    backend = (settings.vectorstore_backend or "faiss").strip().lower()
    if settings.enable_hybrid_retrieval and backend != "qdrant":
        warnings.append(
            "Phase 3: ENABLE_HYBRID_RETRIEVAL=true needs VECTORSTORE_BACKEND=qdrant."
        )
    if settings.semantic_cache_enabled and not is_semantic_cache_active():
        warnings.append(
            "Phase 3: SEMANTIC_CACHE_ENABLED=true needs VECTORSTORE_BACKEND=qdrant."
        )
    sla_s = int(getattr(settings, "generation_sla_seconds", 600))
    if key_ok and not multi_section_parallel_enabled():
        warnings.append(
            f"Full-report SLA ({sla_s}s): ENABLE_ASYNC_PIPELINE=true and parallel sections "
            "are required; sequential generation often exceeds 10 minutes."
        )
    elif key_ok and is_sqlite_database() and not parallel_section_writes_safe():
        warnings.append(
            f"Full-report SLA ({sla_s}s): set ALLOW_SQLITE_PARALLEL_SECTIONS=true or use PostgreSQL."
        )
    if (
        settings.enable_speculative_executor
        and settings.enable_async_pipeline
        and settings.notes_only_generation
        and not settings.agentic_inspector_when_notes_only
    ):
        warnings.append(
            "Phase 3 speculation enabled but POST /generate skips the inspector (Phase 2 off on that path)."
        )
    return warnings