FastAPI-сервис для первичной обработки и маршрутизации неструктурированных бизнес-запросов: структурирование через LLM, валидация, определение маршрута или необходимости уточнения, формирование ответа заказчику и аудит в SQLite.
Live demo: https://intake.elivcloud.org — access-protected demo; credentials available on request.
- Бизнес-проблема: неструктурированный business intake нужно быстро привести к форме, пригодной для первичной маршрутизации, не теряя запросы с недостаточными данными или неподдерживаемым доменом.
- Решение: LLM выполняет structured analysis, после чего детерминированный Python-слой принимает финальное routing/action решение; при недостатке данных или уверенности используются контролируемые clarification/human-review пути.
- Безопасность архитектуры: customer-facing результат отделен от внутреннего аудита, а ошибки LLM и конфигурации переходят в безопасный
processing_errorвместо падения процесса. Это не позволяет ответу модели напрямую определять итоговый маршрут или раскрывать служебные данные заказчику.
Текущий статус: MVP завершен, функциональность заморожена; post-MVP portfolio/demo deployment развернут.
- проведена ручная приемочная проверка через реальный фронтенд, бэкенд, OpenAI и SQLite-аудит;
- 10 из 10 канонических E2E-сценариев прошли успешно — результаты приемочной проверки;
- проведён независимый финальный аудит проекта;
- проект опубликован на GitHub;
- закрытый live demo работает через HTTPS; доступ защищен, credentials available on request;
- дальнейшие идеи развития вынесены в план развития — новые продуктовые функции в текущей версии не добавляются.
Happy path: структурирование бизнес-запроса и подтвержденная маршрутизация к профильным специалистам.
Журнал: автоматическая маршрутизация и передача координатору на human review с сохранением обоих результатов.
Контейнерный demo deployment использует статический Vite build, который обслуживает Nginx, и FastAPI backend в приватной контейнерной сети. Nginx проксирует API-запросы к backend; backend напрямую наружу не экспонируется. Внешний reverse proxy завершает HTTPS и защищает весь hostname через Basic Auth perimeter.
SQLite хранится в отдельном persistent named volume. Demo database используется только для synthetic portfolio data. OPENAI_API_KEY хранится только в server runtime environment и не включен в репозиторий или frontend build.
Packaging остается компактным и разделен по назначению:
Dockerfileсобирает FastAPI backend;frontend/Dockerfileсоздает Vite build и переносит его в Nginx image;frontend/nginx.confобслуживает SPA и проксирует API к приватному backend;compose.demo.ymlсвязывает контейнеры и persistent SQLite volume для demo deployment.
Неструктурированный бизнес-запрос
│
▼
FastAPI + валидация (IngestRequest)
│
▼
OpenAI Structured Outputs
│
▼
LLMAnalysis
│
▼
Детерминированный слой принятия решения
│
▼
┌───────────────────────┬───────────────────────┬───────────────────────┐
│ Маршрутизация │ Нужны уточнения │ Передача координатору │
│ ready_for_routing │ needs_clarification │ human_review │
└───────────────────────┴───────────────────────┴───────────────────────┘
│
▼
Ответ заказчику + SQLite-аудит
Аварийный путь — при ошибке LLM или конфигурации (например, не задан OPENAI_API_KEY) — обрабатывается отдельно и тоже безопасно, без падения приложения:
Ошибка LLM или конфигурации
│
▼
processing_error
│
▼
Безопасная передача на ручную обработку + SQLite-аудит
Structured Outputs(client.responses.parse(..., text_format=LLMAnalysis)) — строгая схемаLLMAnalysisвместо произвольного текста от LLM (docs/LLM_INTEGRATION.md);- окончательное решение принимает детерминированный Python-слой (
app/services/decision_service.py) — LLM только предлагает, но не решает (docs/DECISION_TABLE.md); - блокирующее уточнение (
needs_clarification, маршрут ещё не назначен) и неблокирующее уточнение (ready_for_routingс уточняющими вопросами при уже подтверждённом маршруте) — два разных случая, не перепутанных между собой (docs/DECISION_TABLE.md); - маршрутизация сразу к нескольким специалистам, если запрос требует нескольких профильных компетенций;
- неподдерживаемый бизнес-домен направляется на
human_review, а не подставляется под похожий по звучанию существующий маршрут; - защита от инъекций инструкций (prompt injection) на уровне системного промпта — пользовательский текст явно помечен как недоверенные данные (
docs/LLM_INTEGRATION.md); - безопасный
processing_errorвместо падения приложения при сбое LLM или отсутствующей конфигурации; - SQLite-аудит каждого обработанного запроса, включая ошибки, без утечки ключей и сырых ответов провайдера (
docs/AUDIT_STORAGE.md); - тонкий фронтенд на TypeScript/Vite поверх того же API и внутренний журнал аудита (
docs/FRONTEND.md).
В самом приложении эндпоинты аудита (GET /records, GET /records/{request_id}) и раздел фронтенда «Журнал» не имеют application-level авторизации. В live demo весь hostname закрыт внешним Basic Auth perimeter; без такого защитного периметра эти данные нельзя публиковать в открытом доступе.
- нет application-level аутентификации и авторизации ни у API бэкенда, ни у фронтенда — закрытый live demo компенсирует это внешним Basic Auth perimeter, но открытая публикация без защитного периметра недопустима;
- нет гарантии экспертного качества решения: LLM классифицирует и предлагает, а не выполняет юридическую, комплаенс- или финансовую экспертизу (
docs/PROCESS_FLOW.md) — итоговая маршрутизация подлежит проверке профильным специалистом или координатором; - ответ заказчику (
ProcessingResult) и внутренний аудит (AuditRecordRead) — сознательно разные проекции одних и тех же данных: заказчик не видитLLMAnalysis,final_routes,reason_codesи детали ошибок (docs/PROCESSING_PIPELINE.md,docs/API_INGEST.md); - один LLM-провайдер (OpenAI), одна SQLite-база без миграций, без ролей и пользователей, без последующей переписки с заказчиком после получения ответа.
Полный план развития (v2/v3/later) — docs/ROADMAP.md. Фактически проведённая ручная приемочная проверка — docs/ACCEPTANCE_SCENARIOS.md.
app/domain/enums.py—RequestCategory,RequestTopic,SpecialistRoute,Priority,Confidence,RecommendedAction,ProcessingStatus,DecisionReason.app/schemas/ingest.py—IngestRequest: валидация входящего неструктурированного запроса.app/schemas/analysis.py—LLMAnalysis: строгая схема результата LLM-анализа с межполевыми бизнес-правилами (пример —docs/example_analysis.json).app/schemas/decision.py—CustomerResponse,ProcessingDecision: итоговое решение и ответ заказчику.app/services/decision_service.py— превращаетLLMAnalysisвProcessingDecision; LLM ничего не решает окончательно, только предлагает.app/services/customer_response_service.py— формирует безопасные тексты для заказчика по контролируемым шаблонам, без утечки внутренних enum и служебных полей.app/services/llm_service.py—RequestAnalyzer(протокол) иOpenAIRequestAnalyzer: получает валидированныйLLMAnalysisот OpenAI через Structured Outputs.app/services/llm_errors.py— собственная иерархия исключений LLM-слоя (LLMServiceErrorи подклассы), независимая от классов исключений OpenAI SDK.app/core/prompts.py— системный промпт (SYSTEM_PROMPT), фиксированная константа без плейсхолдеров.app/db/—ProcessingRecord(SQLAlchemy 2.x модель) иProcessingRecordRepository: SQLite-аудит (подробности —docs/AUDIT_STORAGE.md).app/schemas/audit.py—AuditRecordCreate/AuditRecordRead/AuditRecordSummary: контракт репозитория аудита и его компактная проекция для списка.app/services/processing_service.py—ProcessingService: связываетRequestAnalyzer, детерминированный модуль принятия решения и аудит-репозиторий в единый конвейер обработки (подробности —docs/PROCESSING_PIPELINE.md).app/schemas/processing.py—ProcessingResult: безопасная внешняя проекция результата обработки.app/core/lifespan.py—create_lifespan(): при запуске приложения создает engine, инициализирует таблицы, создает фабрику сессий и создает или регистрируетRequestAnalyzerвapp.state; при остановке освобождает engine. СозданиеOpenAIRequestAnalyzerв lifespan не создает реальный клиент OpenAI: он создается лениво при первомanalyze()через_build_client().app/main.py—create_app(): фабрика приложения с внедряемымиSettings/RequestAnalyzer/Engine. На уровне импорта модуль создаетsettings = Settings()(app/core/config.py) и вызываетapp = create_app()дляuvicorn app.main:app, но сама фабрика не создает engine БД, таблицы или реальный клиент OpenAI. Engine, таблицы иRequestAnalyzerинициализируются в lifespan при реальном запуске, а клиент OpenAI — лениво при первомanalyze().app/api/dependencies.py— зависимости FastAPI в рамках одного запроса:Session,ProcessingRecordRepository,RequestAnalyzer,ProcessingService(ничего не кешируется глобально).app/api/ingest.py—POST /ingest: тонкий HTTP-эндпоинт надProcessingService(подробности и примеры —docs/API_INGEST.md).app/api/records.py—GET /records,GET /records/{request_id}: API аудита только для чтения надProcessingRecordRepository(подробности и примеры —docs/API_RECORDS.md).
Требует реального OPENAI_API_KEY (не хранится в репозитории) и не запускается автоматически:
$env:OPENAI_API_KEY = "..."
.venv\Scripts\python.exe scripts\manual_llm_check.py- Python 3.12
- pip
- Node.js и npm — для запуска и тестирования фронтенда
PowerShell (Windows):
python -m venv .venv
.venv\Scripts\Activate.ps1Bash (Linux/macOS):
python3 -m venv .venv
source .venv/bin/activatepip install -e ".[dev]"Локальный .env создаётся из .env.example:
PowerShell (Windows):
Copy-Item .env.example .envBash (Linux/macOS):
cp .env.example .envДля реального LLM-анализа нужно указать в .env:
OPENAI_API_KEY=...
.env не хранится в Git.
Без OPENAI_API_KEY приложение всё равно запускается: GET /health отвечает штатно, а POST /ingest штатно возвращает результат со статусом processing_error и передачей запроса на ручную обработку — вместо падения с ошибкой.
PowerShell (Windows):
.venv\Scripts\python.exe -m uvicorn app.main:app --reloadBash (Linux/macOS):
uvicorn app.main:app --reloadПриложение будет доступно на http://127.0.0.1:8000, проверка состояния — GET /health.
pytestruff check .
ruff format --check .Тонкий продуктовый фронтенд (frontend/) — чистый TypeScript + Vite, без UI-фреймворков. Подробности архитектуры и ограничений — docs/FRONTEND.md.
cd frontend
npm install
npm run devСервер разработки Vite поднимается на http://localhost:5173 и проксирует запросы /health, /ingest, /records на локальный FastAPI (http://127.0.0.1:8000) — бэкенд должен быть запущен отдельно (см. выше). CORS в бэкенде ради этого не добавлялся: в режиме разработки браузер обращается к тому же источнику (origin) (localhost:5173), а Vite сам перенаправляет запрос на бэкенд.
Два раздела интерфейса:
- Новый запрос — форма отправки неструктурированного запроса (
POST /ingest) и карточка результата с понятным статусом, ответом заказчику и уточняющими вопросами (если требуются); - Журнал — просмотр только для чтения накопленного журнала аудита (
GET /records,GET /records/{request_id}): список записей и полная карточка одной записи с анализом LLM и итоговым решением.
Фронтенд не добавляет аутентификацию или авторизацию. Раздел «Журнал» — это прямой просмотр внутреннего API аудита, который сам по себе не защищен (см. предупреждение выше и docs/API_RECORDS.md). В live demo весь интерфейс закрыт внешним Basic Auth perimeter; открытая публикация без такого защитного слоя недопустима.
Тесты фронтенда (Vitest + jsdom):
cd frontend
npm run test
npm run typecheck
npm run build- автор репозитория — владелец продукта, требований, архитектурных решений и приемки;
- AI-инструменты использовались как помощники при реализации и review;
- рабочий цикл: specification → implementation → independent review → human decision/acceptance;
- критические бизнес-правила и финальные решения не делегируются LLM автоматически.

