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
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
77 lines
4.3 KiB
Python
77 lines
4.3 KiB
Python
"""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",
|
||
]
|