Skip to content
FitlegodPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

AI-сервис финансового помощника

Микросервис на FastAPI, реализующий AI-логику финансового помощника для массовой аудитории. Часть хакатон-проекта — работает независимо на порту 8001, не пересекаясь с основным Backend (8000).


Стек

Компонент Технология
Фреймворк FastAPI + Uvicorn (HTTPS)
Граф агентов LangGraph
LLM Ollama · qwen2.5:7b-instruct-q4_K_M
RAG ChromaDB (embedded) · all-MiniLM-L6-v2
База данных MongoDB Atlas (pymongo)
Поиск Serper / Tavily / DuckDuckGo
Контейнеризация Docker + Docker Compose
Конфигурация .env + prompts.yaml

Архитектура

┌─────────────────────────────────────────────────────┐
│                    FastAPI (8001)                    │
│                                                     │
│  /ai/process ──→ LangGraph ──→ Planner              │
│  /ai/stream  ──→  (граф)   ──→ Search (если нужно) │
│                            ──→ RAG (ChromaDB)       │
│                            ──→ Analyst (LLM)        │
│                                                     │
│  /ai/onboarding  ──→ OnboardingFlow ──→ MongoDB     │
│  /ai/bank-offers ──→ Scorer (детерминированный)     │
│  /ai/cashflow    ──→ Calculator (без LLM)           │
│  /ai/patterns    ──→ Calculator + LLM-инсайт        │
│  /ai/daily-action ──→ Calculator + LLM-совет        │
└─────────────────────────────────────────────────────┘

Принцип: калькуляторы считают цифры, LLM только объясняет результаты. LLM никогда не выполняет арифметику.

Очередь: глобальный семафор пропускает к Ollama один LLM-запрос за раз. Детерминированные эндпоинты (/ai/bank-offers, /ai/cashflow/calculate) работают вне очереди и отвечают мгновенно.


Быстрый старт

Требования

  • Python 3.11+
  • Ollama с загруженной моделью qwen2.5:7b-instruct-q4_K_M
  • MongoDB Atlas (опционально — без него онбординг работает в памяти)
  • SSL-сертификат (файлы в certificates/)

Установка

cd ai-service
python -m venv .venv
.venv\Scripts\activate        # Windows
pip install -r requirements.txt

Запуск локально

python -m uvicorn app.main:app \
  --host 0.0.0.0 \
  --port 8001 \
  --ssl-certfile certificates/server.crt \
  --ssl-keyfile  certificates/server.key

Проверка: curl -k https://localhost:8001/health

Запуск через Docker

Ollama запускается на хосте отдельно — в compose не включена.

docker compose up --build

Сервис поднимается на https://localhost:8001. ChromaDB-индекс сохраняется в data/chroma/ (volume ./data:/app/data).

Проверка конфига без запуска: docker compose config


Конфигурация .env

OLLAMA_BASE_URL=http://localhost:11434
PLANNER_MODEL=qwen2.5:7b-instruct-q4_K_M
ANALYST_MODEL=qwen2.5:7b-instruct-q4_K_M
AI_SERVICE_PORT=8001
LOG_LEVEL=INFO

# Поиск (используется в /ai/process при запросах актуальных данных)
SERPER_API_KEY=         # google.serper.dev — основной
TAVILY_API_KEY=         # app.tavily.com    — резерв

# MongoDB (без URI онбординг хранится в памяти процесса)
MONGODB_URI=mongodb+srv://...

Все LLM-промпты — в app/prompts.yaml. Меняются без перезапуска сервиса.

RAG инициализируется при первом запросе: ChromaDB скачивает модель all-MiniLM-L6-v2 (~79 МБ) один раз и кэширует в ~/.cache/chroma/. Повторные старты мгновенные — индекс читается с диска.


Эндпоинты

Основной чат

Метод Путь Очередь Время
POST /ai/process да 15–60 с
POST /ai/stream да первый байт < 1 с, SSE-поток

Пример запроса:

POST /ai/process
{
  "user_id": "uuid",
  "query": "Стоит ли мне брать кредит на машину?",
  "context": {
    "user_profile": { "monthly_income": 150000, "monthly_debt_payments": 10000 },
    "history": []
  }
}

Пример ответа:

{
  "text": "С учётом вашего дохода кредитная нагрузка составит 31.4% — жёлтая зона...",
  "table": { "headers": ["Вариант", "Платёж", "PTI"], "rows": [["...", "...", "..."]] },
  "structured": { "summary": "...", "recommendations": ["..."], "risks": ["..."] },
  "intent": "advice",
  "sources": [],
  "error": null
}

Поле table появляется по решению LLM — когда ответ выигрывает от структуры (сравнение вариантов кредита, разбивка расходов и т.п.). В остальных случаях — null.

Онбординг

Девятишаговый диалог (3 фазы: личность → образ жизни → финансы), который формирует UserFinancialProfile. Профиль сохраняется в MongoDB и используется для персонализации чата.

Метод Путь Описание
POST /ai/onboarding Один шаг диалога
GET /ai/onboarding/{user_id}/status Статус и готовый профиль

Фазы:

Фаза 1 — ЛИЧНОСТЬ    (2–3 вопроса): интересы, образ жизни → доверие
Фаза 2 — ОБРАЗ ЖИЗНИ (3–4 вопроса): кафе, подписки, транспорт → косвенные траты
Фаза 3 — ФИНАНСЫ     (4–5 вопросов): доход, расходы, кредиты, цели → профиль

Пример шага:

POST /ai/onboarding
{ "user_id": "uuid", "message": "Работаю разработчиком, увлекаюсь спортом" }

→ {
    "question": "Как часто ходишь в кафе или рестораны?",
    "suggested_answers": ["Редко", "Пару раз в месяц", "Часто"],
    "phase": 2,
    "complete": false
  }

После complete: true забери профиль через /status и передавай в context.user_profile каждого запроса к /ai/process.

Калькуляторы (без LLM)

Метод Путь Описание
POST /ai/cashflow/calculate Кэшфлоу по балансу и дням до зарплаты
GET /ai/cashflow/{user_id} То же, но баланс берётся из профиля
GET /ai/patterns/{user_id} Паттерн трат + LLM-инсайт
GET /ai/daily-action/{user_id} Персональный совет на сегодня

Кэшфлоу возвращает поднёвной прогноз баланса до зарплаты с выделением рисковых событий:

POST /ai/cashflow/calculate
{ "user_id": "uuid", "current_balance": 15000, "days_to_salary": 18 }

→ {
    "projected_balance": -9000.0,
    "will_be_negative": true,
    "shortage": 9000.0,
    "critical_day": 12,
    "forecast": [
      { "day": 0,  "balance": 15000.0, "event": null },
      { "day": 5,  "balance": 9666.67, "event": "аренда" },
      { "day": 12, "balance": -166.67, "event": null }
    ],
    "risk_events": [{ "day": 5, "balance": 9666.67, "event": "аренда" }]
  }

Подбор кредитных предложений

POST /ai/bank-offers

Детерминированный скоринг без LLM и без внешних запросов. Из базы 48 предложений 28 банков отбирает топ-6 наиболее подходящих под запрос пользователя. Отвечает за < 50 мс.

Запрос:

{
  "user_id": "uuid",
  "loan_amount": 500000,
  "loan_rate": 15.0,
  "loan_months": 24
}

Ответ:

{
  "offers": [
    {
      "bank_name": "Сбербанк",
      "domain": "sber.ru",
      "rate": 14.5,
      "loan_months": 24,
      "monthly_payment": 24124.71,
      "score": 99,
      "logo_url": "https://img.logo.dev/sber.ru?token=free",
      "offer_url": "https://sber.ru/credits/consumer"
    }
  ],
  "search_query": "кредит 500000 ₽ на 24 мес. под 15.0% годовых"
}

Как считается score:

score = max(0, 1 − |offer.rate − loan_rate| / loan_rate)   × 40   ← ставка  (40%)
      + max(0, 1 − |offer.months − loan_months| / loan_months) × 60   ← срок (60%)

Предложение с точным совпадением по обоим параметрам → score = 100.

Покрытие (28 банков, 48 предложений):

Т-Банк · Сбербанк · Альфа-Банк · ВТБ · Газпромбанк · Россельхозбанк · МТС Банк · Почта Банк · Совкомбанк · Росбанк · Уралсиб · Промсвязьбанк · Ренессанс Кредит · МКБ · ОТП Банк · Ак Барс Банк · Банк БСПБ · Абсолют Банк · Хоум Банк · УБРиР · Банк Зенит · СКБ-Банк · Банк ДОМ.РФ · РНКБ Банк · Русский Стандарт · Кредит Европа · Синара Банк · Экспобанк · Банк Авангард · Металлинвестбанк · Кубань Кредит · СДМ-Банк · Банк Центр-Инвест · Левобережный Банк · Инбанк · АТБ


RAG (Retrieval-Augmented Generation)

Перед вызовом LLM в узле analyst граф автоматически ищет релевантные статьи из базы знаний и добавляет их в промпт.

Запрос → ChromaDB (cosine similarity) → top-2 статьи → промпт аналитика → LLM

База знаний — data/knowledge.json, 22 статьи по темам:

Тема ID
PTI и долговая нагрузка pti_explained, pti_calculation
Подушка безопасности emergency_fund, emergency_fund_vs_investing
Аннуитет vs дифференцированный annuity_vs_differential
Инфляция inflation
Вклады и накопительные счета deposits
ОФЗ ofz_basics
Кредитная история и скоринг credit_history
Рефинансирование refinancing
Ипотека mortgage_terms
ETF и индексные фонды etf_index_funds
Налоговый вычет 13% tax_deduction
Правило 50/30/20 50_30_20_rule
Сложный процент compound_interest
Диверсификация diversification
ИИС iis_basics
Управление кэшфлоу, накопления, долги, бюджет, страхование …

Порог фильтрации: cosine distance > 0.625 — результат не добавляется в промпт (нерелевантный запрос).

Индекс хранится в data/chroma/ (в .gitignore). Строится автоматически при первом старте, затем читается с диска.


Docker

ai-service/
├── Dockerfile          # python:3.11-slim, EXPOSE 8001, HTTPS через uvicorn
├── docker-compose.yml  # ai-service, порт 8001, volumes для cert и data
└── .dockerignore       # исключает .venv, .git, tests, data/chroma

Ollama не входит в compose — она работает на хосте Windows. Сервис обращается к ней через host.docker.internal:11434.

# ключевые части docker-compose.yml
environment:
  - OLLAMA_BASE_URL=http://host.docker.internal:11434
volumes:
  - ./certificates:/app/certificates:ro   # SSL
  - ./data:/app/data                       # ChromaDB (без :ro — пишет индекс)
healthcheck:
  test: ["CMD", "curl", "-fk", "https://localhost:8001/health"]

Тесты

pytest tests/test_calculators.py tests/test_onboarding.py tests/test_bank_offers.py -v

Результат: 36 passed

Файл Тестов Что покрыто
test_calculators.py 16 PTI green/yellow/red, zero income; health score critical/medium/good/excellent; savings realistic/unrealistic/no money; cashflow enough/not-enough/danger-day/fixed-payments/zero-balance
test_onboarding.py 12 _count_questions_by_phase (пустая, смешанная история); _next_question (переходы 1→2→3→complete); _build_profile (мёрж, override, defaults); _build_summary (полный/пустой профиль)
test_bank_offers.py 8 score=100 при точном совпадении; ≤6 офферов на выходе; сортировка по убыванию score; обязательные поля в ответе; пустой вход

Фикстуры sample_user_profile и sample_onboarding_history — в tests/conftest.py.


Структура проекта

ai-service/
├── app/
│   ├── main.py           # FastAPI-приложение, все эндпоинты, middleware логирования
│   ├── schemas.py        # Pydantic-модели запросов/ответов
│   ├── graph.py          # LangGraph: Planner → Search → RAG → Analyst
│   ├── rag.py            # FinancialRAG: ChromaDB embedded, lazy singleton
│   ├── llm.py            # OllamaClient с retry-логикой
│   ├── onboarding.py     # Трёхфазный онбординг-диалог
│   ├── calculators.py    # PTI, аннуитет, индексы здоровья, кэшфлоу
│   ├── bank_offers.py    # База 48 офферов + детерминированный скоринг
│   ├── patterns.py       # Анализ паттернов трат
│   ├── daily_action.py   # Ежедневный персональный совет
│   ├── connectors.py     # Serper → Tavily → DuckDuckGo
│   ├── database.py       # MongoDB: сессии онбординга и профили
│   ├── queue.py          # Глобальный семафор для Ollama
│   └── prompts.yaml      # Все LLM-промпты
├── data/
│   ├── knowledge.json    # 22 финансовые статьи для RAG
│   └── chroma/           # ChromaDB-индекс (генерируется, в .gitignore)
├── tests/
│   ├── conftest.py          # Фикстуры pytest
│   ├── test_calculators.py  # 16 тестов калькуляторов
│   ├── test_onboarding.py   # 12 тестов логики онбординга
│   └── test_bank_offers.py  # 8 тестов скоринга офферов
├── docs/
│   └── backend-contract.md  # Полный API-контракт для Backend
├── certificates/
│   ├── server.crt
│   └── server.key
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env
├── requirements.txt
└── README.md

Калькуляторы

PTI (Payment-to-Income)

PTI = ежемесячные_платежи / доход × 100%

Зелёный  PTI < 30%   — нагрузка в норме
Жёлтый   30% ≤ PTI < 50% — повышенная нагрузка
Красный  PTI ≥ 50%   — критическая нагрузка

Аннуитетный платёж

P = S × r(1+r)^n / ((1+r)^n − 1)

S = сумма кредита
r = годовая_ставка / 12 / 100
n = срок в месяцах

Индекс финансового здоровья (0–100)

expense_score  = f(расходы / доход)       → 0 / 10 / 20 / 33
debt_score     = f(долги / доход)          → 0 / 10 / 20 / 33
savings_score  = f(накопления / месяцев)   → 0 / 10 / 20 / 34

Уровни:
  critical  (0–40)   — критическое состояние
  medium   (41–65)   — средний уровень, есть зоны роста
  good     (66–85)   — хорошее состояние
  excellent (86–100) — отличное финансовое здоровье

Логирование

Middleware логирует входной и выходной JSON каждого запроса:

2026-05-30 19:33:40 [INFO] app.main: [IN] POST /ai/bank-offers
{
  "user_id": "alex",
  "loan_amount": 500000,
  "loan_rate": 15.0,
  "loan_months": 24
}

2026-05-30 19:33:40 [INFO] app.main: [OUT] POST /ai/bank-offers
{
  "offers": [...],
  "search_query": "кредит 500000 ₽ на 24 мес. под 15.0% годовых"
}

SSE-стрим (/ai/stream) — логируется только входной JSON (тело ответа нельзя буферизовать без потери стриминга).


Коды ответов

Большинство эндпоинтов всегда возвращают HTTP 200. Ошибки передаются через поле error в теле ответа — никогда через HTTP 500.

Исключение — POST /ai/cashflow/calculate возвращает HTTP 422 если онбординг не пройден.

Подробный контракт со всеми полями, примерами и таймаутами — в docs/backend-contract.md.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages