gendesign/backend/app/services/llm/__init__.py
Light1YT 4af7ba5a40
All checks were successful
CI / changes (pull_request) Successful in 5s
CI / backend-tests (push) Successful in 6m25s
CI / backend-tests (pull_request) Successful in 6m20s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 2m42s
Deploy / deploy (push) Successful in 1m13s
CI / changes (push) Successful in 5s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
feat(llm): foundational LLM infra package with §19 redaction + deterministic fallback (#960)
Optional external-OpenAI layer over the deterministic forecasting engine. Gated by
llm_enabled (default False) so prod makes no network calls until deliberately enabled.
Allowlist-first SafePayload contract + is_confidential hard-block + RU-PII regex scrub
(mandatory on the external path). Abstract LLMProvider seam (is_external) for a future
RU-hosted provider. Sync httpx core (Celery-friendly); tool/function-calling pass-through;
timeout + bounded 429/5xx retry + per-request call cap, all degrading to fallback.
Raw httpx (no openai SDK -> no pyproject/lock drift). 47 tests, ruff + mypy clean.

Refs #960
2026-06-08 15:44:16 +05:00

77 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""LLM infrastructure (#960) — ОПЦИОНАЛЬНЫЙ слой поверх детерминированного движка.
Foundational-пакет. Консьюмеры приедут позже: #956 граддок-extraction (sync Celery)
и #957 chat (async FastAPI). Этот пакет их НЕ реализует — только инфраструктуру.
═══════════════════════════════════════════════════════════════════════════════
КОНТРАКТ БЕЗОПАСНОСТИ — обязателен к прочтению авторами консьюмеров
═══════════════════════════════════════════════════════════════════════════════
1. Детерминированный fallback ВСЕГДА. ``complete`` НИКОГДА не бросает из-за LLM:
при ``LLMResult.ok is False`` вы ОБЯЗАНЫ подставить свой детерминированный
результат. Forecasting-движок остаётся детерминированным; LLM ничего не «ломает».
res = complete(system_prompt=..., payload=safe)
if not res.ok:
return deterministic_result() # обязательно
use(res.content, res.tool_calls)
2. Allowlist-first (§19 data-residency). Провайдер ВНЕШНИЙ (OpenAI) → данные
покидают РФ. Поэтому в ``complete`` нельзя передать сырые строки/DB-записи —
только ЯВНО собранный ``SafePayload`` из проверенных не-чувствительных полей:
payload = SafePayload(
text=public_regulation_text, # публичный/безопасный текст
fields={"zone_index": "Ц-1", "floors": 16}, # агрегаты/коды
is_confidential=insight.is_confidential, # прокинуть флаг источника!
)
НЕ собирайте ``SafePayload`` из сырой строки insight/лида без проверки полей.
3. Hard-block конфиденциального. Если ``SafePayload.is_confidential is True`` —
для внешнего провайдера ``complete`` НЕ отправит данные, вернёт fallback
(reason="redaction_refused"). Всегда прокидывайте confidential-флаг источника.
4. Regex-scrub — вторичная защита (belt-and-suspenders): из ``text``/``fields``
автоматически вырезаются RU PII (телефон/email/СНИЛС/ИНН/паспорт/ФИО) перед
отправкой. Это НЕ заменяет пункт 2 — не полагайтесь на scrub как на гарантию.
5. ``llm_enabled`` default False. В проде (до настройки секретов) сеть НЕ дёргается —
``complete`` сразу возвращает fallback. Включение — осознанное действие.
"""
from __future__ import annotations
from .client import LLMResult, LLMUnavailableError, complete, complete_or_raise
from .prompts import PromptNotFoundError, render
from .provider import (
LLMProvider,
LLMProviderError,
LLMRateLimitedError,
LLMTimeoutError,
OpenAIProvider,
ToolCall,
)
from .redaction import RedactionRefusedError, SafePayload, scrub_safe_payload, scrub_text
# Сортировка isort-style (RUF022). Группировка по слоям — в docstring выше:
# orchestration (complete/complete_or_raise/LLMResult), redaction (SafePayload/...),
# provider abstraction (LLMProvider/OpenAIProvider/... — шов под RU-hosted), prompts.
__all__ = [
"LLMProvider",
"LLMProviderError",
"LLMRateLimitedError",
"LLMResult",
"LLMTimeoutError",
"LLMUnavailableError",
"OpenAIProvider",
"PromptNotFoundError",
"RedactionRefusedError",
"SafePayload",
"ToolCall",
"complete",
"complete_or_raise",
"render",
"scrub_safe_payload",
"scrub_text",
]