RandomZ / app /optimization /ai_phases.py
StormShadow308's picture
Fix HF startup when OPENAI_API_KEY secret is missing or only in Space env.
970bad4
Raw
History Blame Contribute Delete
11 kB
"""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