forked from templates/template-go-orm
remove docs/ and CLAUDE.md from git tracking
This commit is contained in:
109
CLAUDE.md
109
CLAUDE.md
@@ -1,109 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Ollama Proxy** — Go-сервис, выступающий прокси между Codex CLI и локальной Ollama. Добавляет авторизацию-заглушку и LLM-маршрутизатор: маленькая модель (gemma:1b) классифицирует входящий запрос и перенаправляет его к нужной целевой модели (qwen2.5-coder для кода, gemma для текста).
|
||||
|
||||
Без БД, без Docker на текущем этапе. Чистый Go, stateless.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
Codex CLI → Ollama Proxy (:11435) → Ollama (:11434)
|
||||
│
|
||||
Token check (AUTH_TOKEN из .env)
|
||||
Router LLM: gemma:1b → code / document / general
|
||||
│
|
||||
qwen2.5-coder:1.5b | gemma:1b | gemma:1b
|
||||
```
|
||||
|
||||
**Поток запроса:**
|
||||
1. Codex CLI отправляет запрос с `model: "auto"` на прокси
|
||||
2. Middleware проверяет `Authorization: Bearer <AUTH_TOKEN>`
|
||||
3. Router: если модель не указана явно → gemma:1b классифицирует последнее user-сообщение
|
||||
4. Прокси подменяет поле `model` в запросе и проксирует в реальную Ollama
|
||||
5. Стриминг-ответ (NDJSON) возвращается в Codex CLI построчно
|
||||
|
||||
**Маршрутизация срабатывает только один раз** (при первом запросе с `model: "auto"`). Последующие запросы в той же сессии Codex CLI отправляет уже с конкретным именем модели — прокси пропускает роутер и проксирует напрямую.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
cmd/server/main.go # Точка входа — загрузка конфига, запуск HTTP-сервера
|
||||
internal/
|
||||
config/config.go # Конфигурация из env-переменных
|
||||
model/
|
||||
ollama.go # Типы Ollama API (Chat, Generate, Tags, Message)
|
||||
handler/
|
||||
middleware.go # Проверка AUTH_TOKEN (Bearer token)
|
||||
proxy.go # HTTP-хендлеры: /api/chat, /api/generate, /api/tags
|
||||
service/
|
||||
ollama_client.go # HTTP-клиент к Ollama (streaming + синхронный)
|
||||
router.go # LLM-маршрутизатор через gemma:1b
|
||||
router/
|
||||
router.go # chi-роутер, регистрация маршрутов
|
||||
go.mod / go.sum # Зависимости
|
||||
Makefile # Команды сборки и запуска
|
||||
.env # Локальные переменные окружения (не в git)
|
||||
docs/
|
||||
architecture.md # Подробная архитектура
|
||||
```
|
||||
|
||||
## Ключевые команды
|
||||
|
||||
```bash
|
||||
make build # go build -o ollama-proxy ./cmd/server
|
||||
make run # go run ./cmd/server (читает .env)
|
||||
make test # go test ./...
|
||||
```
|
||||
|
||||
## Переменные окружения (.env)
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|-----------|----------|-------------|
|
||||
| `PROXY_PORT` | Порт прокси | 11435 |
|
||||
| `AUTH_TOKEN` | Токен авторизации (заглушка) | — (обязательна) |
|
||||
| `OLLAMA_URL` | URL реальной Ollama | http://localhost:11434 |
|
||||
| `ROUTER_MODEL` | Модель-классификатор | gemma:1b |
|
||||
| `CODE_MODEL` | Модель для кода | qwen2.5-coder:1.5b |
|
||||
| `DOC_MODEL` | Модель для документов | gemma:1b |
|
||||
| `GENERAL_MODEL` | Общая модель | gemma:1b |
|
||||
|
||||
## Модели Ollama (для тестирования)
|
||||
|
||||
```bash
|
||||
ollama pull gemma:1b # Router LLM + документы + общее
|
||||
ollama pull qwen2.5-coder:1.5b # Целевая модель для кода
|
||||
```
|
||||
|
||||
## Тестирование через curl
|
||||
|
||||
```bash
|
||||
# Здоровье сервиса
|
||||
curl http://localhost:11435/health
|
||||
|
||||
# Без токена → 401
|
||||
curl -X POST http://localhost:11435/api/chat -d '{}'
|
||||
|
||||
# С токеном, auto-маршрутизация (code)
|
||||
curl http://localhost:11435/api/chat \
|
||||
-H "Authorization: Bearer <AUTH_TOKEN>" \
|
||||
-d '{"model":"auto","messages":[{"role":"user","content":"напиши функцию на Go"}]}'
|
||||
|
||||
# С токеном, явная модель (без маршрутизации)
|
||||
curl http://localhost:11435/api/chat \
|
||||
-H "Authorization: Bearer <AUTH_TOKEN>" \
|
||||
-d '{"model":"gemma:1b","messages":[{"role":"user","content":"привет"}]}'
|
||||
|
||||
# Список моделей
|
||||
curl http://localhost:11435/api/tags \
|
||||
-H "Authorization: Bearer <AUTH_TOKEN>"
|
||||
```
|
||||
|
||||
## Конвенции
|
||||
|
||||
- Комментарии в коде — на русском
|
||||
- Переносы строк LF (`.gitattributes`)
|
||||
- Версионирование: bumpversion, `VERSION` файл
|
||||
@@ -1,110 +0,0 @@
|
||||
# Руководство разработчика
|
||||
|
||||
## Требования
|
||||
|
||||
- Go 1.21+
|
||||
- [Ollama](https://ollama.com) — установлен и запущен локально
|
||||
|
||||
## Первый запуск
|
||||
|
||||
### 1. Скачать модели в Ollama
|
||||
|
||||
```bash
|
||||
ollama pull gemma:1b # Router LLM + документы + общее
|
||||
ollama pull qwen2.5-coder:1.5b # Целевая модель для кода
|
||||
```
|
||||
|
||||
Проверить что модели доступны:
|
||||
```bash
|
||||
ollama list
|
||||
```
|
||||
|
||||
### 2. Настроить .env
|
||||
|
||||
```env
|
||||
PROXY_PORT=11435
|
||||
AUTH_TOKEN=my-secret-token
|
||||
OLLAMA_URL=http://localhost:11434
|
||||
ROUTER_MODEL=gemma:1b
|
||||
CODE_MODEL=qwen2.5-coder:1.5b
|
||||
DOC_MODEL=gemma:1b
|
||||
GENERAL_MODEL=gemma:1b
|
||||
```
|
||||
|
||||
### 3. Запустить прокси
|
||||
|
||||
```bash
|
||||
make run
|
||||
# или напрямую:
|
||||
go run ./cmd/server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Команды
|
||||
|
||||
```bash
|
||||
make build # Собрать бинарник ./ollama-proxy
|
||||
make run # go run ./cmd/server (читает .env)
|
||||
make test # go test ./...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Тестирование через curl
|
||||
|
||||
```bash
|
||||
# Проверка работоспособности (без токена)
|
||||
curl http://localhost:11435/health
|
||||
|
||||
# Без токена → 401
|
||||
curl -X POST http://localhost:11435/api/chat \
|
||||
-d '{"model":"auto","messages":[{"role":"user","content":"привет"}]}'
|
||||
|
||||
# Auto-маршрутизация → Router LLM (gemma:1b) выберет модель
|
||||
curl http://localhost:11435/api/chat \
|
||||
-H "Authorization: Bearer my-secret-token" \
|
||||
-d '{"model":"auto","messages":[{"role":"user","content":"напиши функцию на Go"}]}'
|
||||
# → классифицирует как "code" → запрос идёт в qwen2.5-coder:1.5b
|
||||
|
||||
# Явная модель (без маршрутизации)
|
||||
curl http://localhost:11435/api/chat \
|
||||
-H "Authorization: Bearer my-secret-token" \
|
||||
-d '{"model":"gemma:1b","messages":[{"role":"user","content":"расскажи про Go"}]}'
|
||||
|
||||
# Список моделей
|
||||
curl http://localhost:11435/api/tags \
|
||||
-H "Authorization: Bearer my-secret-token"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Настройка Codex CLI
|
||||
|
||||
- **Base URL**: `http://localhost:11435`
|
||||
- **API Key**: значение из `AUTH_TOKEN`
|
||||
- **Model**: `auto` (автомаршрутизация) или конкретная модель
|
||||
|
||||
---
|
||||
|
||||
## Как добавить новую модель
|
||||
|
||||
1. `ollama pull <model-name>`
|
||||
2. Добавить переменную в `.env`: `ANALYSIS_MODEL=llama3.2:3b`
|
||||
3. Добавить поле в `internal/config/config.go`
|
||||
4. Расширить логику в `internal/service/router.go`
|
||||
|
||||
---
|
||||
|
||||
## Структура кода
|
||||
|
||||
| Файл | Ответственность |
|
||||
|------|----------------|
|
||||
| `cmd/server/main.go` | Точка входа, инициализация |
|
||||
| `internal/config/config.go` | Env-переменные |
|
||||
| `internal/model/ollama.go` | Go-типы Ollama API |
|
||||
| `internal/handler/middleware.go` | Проверка токена |
|
||||
| `internal/handler/proxy.go` | HTTP-хендлеры |
|
||||
| `internal/service/ollama_client.go` | Клиент к Ollama (streaming + sync) |
|
||||
| `internal/service/router.go` | LLM-маршрутизатор |
|
||||
| `internal/router/router.go` | chi-роутер, маршруты |
|
||||
Binary file not shown.
@@ -1,277 +0,0 @@
|
||||
# Архитектура Ollama Proxy
|
||||
|
||||
## 1. Общее описание
|
||||
|
||||
**Ollama Proxy** — Go-сервис, прозрачно встающий между Codex CLI и локальной Ollama. Для Codex CLI прокси выглядит как обычная Ollama (те же эндпоинты, тот же формат). Внутри прокси:
|
||||
|
||||
1. Проверяет авторизацию (токен из env)
|
||||
2. Классифицирует запрос через Router LLM (gemma:1b)
|
||||
3. Подменяет модель в запросе на подходящую
|
||||
4. Проксирует запрос в реальную Ollama
|
||||
5. Стримит ответ обратно в Codex CLI
|
||||
|
||||
Сервис **stateless** — нет БД, нет сессий, нет хранения истории. История сообщений хранится на стороне Codex CLI.
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектурная схема
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ CODEX CLI │
|
||||
│ (отправляет запросы как к обычной Ollama) │
|
||||
└────────────────────────────┬────────────────────────────────────────┘
|
||||
│ HTTP model: "auto" или "qwen2.5-coder:1.5b"
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ OLLAMA PROXY (:11435) │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Token Auth Middleware │ │
|
||||
│ │ Authorization: Bearer <AUTH_TOKEN> → 401 если неверный │ │
|
||||
│ └───────────────────────┬──────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────▼─────────────────────────────────────┐ │
|
||||
│ │ Router LLM │ │
|
||||
│ │ │ │
|
||||
│ │ Если model == "auto" или пустой: │ │
|
||||
│ │ → gemma:1b (синхронный вызов, stream=false) │ │
|
||||
│ │ → промпт: "Classify: code / document / general" │ │
|
||||
│ │ → ответ: одно слово │ │
|
||||
│ │ │ │
|
||||
│ │ Если model указана явно → пропустить, использовать как есть │ │
|
||||
│ └────────────────────────┬─────────────────────────────────────┘ │
|
||||
│ │ model подменён на целевую │
|
||||
│ ┌────────────────────────▼─────────────────────────────────────┐ │
|
||||
│ │ Proxy │ │
|
||||
│ │ POST /api/chat → Ollama /api/chat │ │
|
||||
│ │ Стриминг NDJSON построчно (bufio.Scanner + http.Flusher) │ │
|
||||
│ └────────────────────────┬─────────────────────────────────────┘ │
|
||||
└───────────────────────────┼─────────────────────────────────────────┘
|
||||
│ HTTP model: "qwen2.5-coder:1.5b"
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ OLLAMA (:11434) │
|
||||
│ │
|
||||
│ gemma:1b qwen2.5-coder:1.5b (другие модели) │
|
||||
│ (router + docs) (код) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── cmd/server/main.go # Точка входа: загрузка конфига, запуск HTTP-сервера
|
||||
├── internal/
|
||||
│ ├── config/
|
||||
│ │ └── config.go # Парсинг env-переменных (caarlos0/env)
|
||||
│ ├── model/
|
||||
│ │ └── ollama.go # Go-типы Ollama API: ChatRequest, ChatResponse,
|
||||
│ │ # GenerateRequest, GenerateResponse, TagsResponse, Message
|
||||
│ ├── handler/
|
||||
│ │ ├── middleware.go # Проверка Bearer-токена (простое сравнение строк)
|
||||
│ │ └── proxy.go # HTTP-хендлеры: HandleChat, HandleGenerate, HandleTags
|
||||
│ ├── service/
|
||||
│ │ ├── ollama_client.go # HTTP-клиент к Ollama:
|
||||
│ │ │ # ProxyChat — стриминг /api/chat
|
||||
│ │ │ # ProxyGenerate — стриминг /api/generate
|
||||
│ │ │ # GetTags — список моделей
|
||||
│ │ │ # Complete — синхронный вызов (для роутера)
|
||||
│ │ └── router.go # LLM-маршрутизатор:
|
||||
│ │ # Route(ctx, model, messages) → целевая модель
|
||||
├── router/
|
||||
│ └── router.go # chi-роутер, подключение middleware и хендлеров
|
||||
├── go.mod / go.sum # Зависимости
|
||||
├── Makefile # build / run / test
|
||||
├── .env # Локальные переменные (не в git)
|
||||
├── .gitignore
|
||||
├── .gitattributes # LF line endings
|
||||
├── VERSION # Семантическая версия
|
||||
└── docs/
|
||||
└── architecture.md # Этот файл
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Поток обработки запроса
|
||||
|
||||
### Случай 1: `model = "auto"` (первый запрос в сессии)
|
||||
|
||||
```
|
||||
1. Codex CLI → POST /api/chat {"model":"auto", "messages":[...]}
|
||||
|
||||
2. Middleware: проверить Authorization: Bearer <token>
|
||||
→ если неверный: 401 Unauthorized
|
||||
|
||||
3. Handler: декодировать ChatRequest из JSON
|
||||
|
||||
4. Router.Route(ctx, "auto", messages):
|
||||
a. Взять последнее user-сообщение
|
||||
b. POST /api/chat к Ollama с model=gemma:1b, stream=false
|
||||
Промпт: "Classify the following user request into exactly one category:
|
||||
code, document, general.
|
||||
Reply with ONLY the category name, nothing else.
|
||||
User request: {текст}"
|
||||
c. Получить ответ: "code"
|
||||
d. Вернуть CODE_MODEL = "qwen2.5-coder:1.5b"
|
||||
|
||||
5. Подменить req.Model = "qwen2.5-coder:1.5b"
|
||||
|
||||
6. OllamaClient.ProxyChat(ctx, w, req):
|
||||
a. POST /api/chat к Ollama с model=qwen2.5-coder:1.5b
|
||||
b. Читать ответ построчно (bufio.Scanner)
|
||||
c. Каждую строку NDJSON сразу писать в w + Flush()
|
||||
|
||||
7. Codex CLI получает стриминг, видит model="qwen2.5-coder:1.5b" в ответе
|
||||
→ запоминает модель для последующих запросов
|
||||
```
|
||||
|
||||
### Случай 2: `model = "qwen2.5-coder:1.5b"` (последующие запросы)
|
||||
|
||||
```
|
||||
1. Codex CLI → POST /api/chat {"model":"qwen2.5-coder:1.5b", "messages":[...]}
|
||||
|
||||
2. Middleware: проверить токен
|
||||
|
||||
3. Router.Route(ctx, "qwen2.5-coder:1.5b", messages):
|
||||
→ модель указана явно, не "auto" → вернуть как есть
|
||||
|
||||
4. OllamaClient.ProxyChat(ctx, w, req):
|
||||
→ прямой прокси к Ollama, Router LLM не вызывается
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Ollama API — поддерживаемые эндпоинты
|
||||
|
||||
| Метод | URL | Описание |
|
||||
|-------|-----|----------|
|
||||
| GET | `/health` | Проверка работоспособности (без авторизации) |
|
||||
| POST | `/api/chat` | Чат. Streaming NDJSON |
|
||||
| POST | `/api/generate` | Генерация текста. Streaming NDJSON |
|
||||
| GET | `/api/tags` | Список моделей из реальной Ollama |
|
||||
|
||||
### Формат запроса `/api/chat`
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "auto",
|
||||
"messages": [
|
||||
{"role": "user", "content": "напиши функцию на Go"}
|
||||
],
|
||||
"stream": true
|
||||
}
|
||||
```
|
||||
|
||||
### Формат ответа `/api/chat` (NDJSON, одна строка = один чанк)
|
||||
|
||||
```json
|
||||
{"model":"qwen2.5-coder:1.5b","created_at":"2025-01-01T12:00:00Z","message":{"role":"assistant","content":"func"},"done":false}
|
||||
{"model":"qwen2.5-coder:1.5b","created_at":"2025-01-01T12:00:01Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":1234567890,"eval_count":42}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Авторизация
|
||||
|
||||
Простая проверка Bearer-токена:
|
||||
|
||||
```
|
||||
Authorization: Bearer <AUTH_TOKEN>
|
||||
```
|
||||
|
||||
- Токен задаётся в `AUTH_TOKEN` (.env)
|
||||
- Сравнение строк — никакого JWT, bcrypt, БД
|
||||
- Неверный или отсутствующий токен → `401 Unauthorized`
|
||||
- `/health` — без авторизации
|
||||
|
||||
**Это временная заглушка.** В следующем этапе будет полноценная авторизация с JWT и БД.
|
||||
|
||||
---
|
||||
|
||||
## 7. Router LLM — детали реализации
|
||||
|
||||
**Промпт классификатора:**
|
||||
```
|
||||
Classify the following user request into exactly one category: code, document, general.
|
||||
Reply with ONLY the category name, nothing else.
|
||||
|
||||
User request: {последнее user-сообщение}
|
||||
```
|
||||
|
||||
**Вызов:** синхронный (`stream: false`), короткий timeout (5–10 сек).
|
||||
|
||||
**Парсинг ответа:**
|
||||
- Взять ответ, привести к нижнему регистру, убрать пробелы
|
||||
- Если содержит "code" → CODE_MODEL
|
||||
- Если содержит "document" → DOC_MODEL
|
||||
- Иначе → GENERAL_MODEL (fallback)
|
||||
|
||||
**Маппинг категорий → модели:**
|
||||
|
||||
| Категория | Тестовая модель | Production модель |
|
||||
|-----------|----------------|------------------|
|
||||
| code | qwen2.5-coder:1.5b | qwen2.5-coder:32b |
|
||||
| document | gemma:1b | deepseek-r1:70b |
|
||||
| general | gemma:1b | llama3.3:70b |
|
||||
|
||||
---
|
||||
|
||||
## 8. Streaming-прокси — детали реализации
|
||||
|
||||
Ключевые требования:
|
||||
- `http.Flusher` — сбрасывать буфер после каждой строки
|
||||
- `bufio.Scanner` с буфером 1MB (для длинных строк с кодом)
|
||||
- `http.Client` без глобального таймаута (используется контекст запроса)
|
||||
- Отмена через `r.Context()` — если Codex CLI отключился, upstream-запрос к Ollama отменяется
|
||||
|
||||
```go
|
||||
// Псевдокод
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
|
||||
flusher := w.(http.Flusher)
|
||||
for scanner.Scan() {
|
||||
w.Write(scanner.Bytes())
|
||||
w.Write([]byte("\n"))
|
||||
flusher.Flush()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Переменные окружения
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|-----------|----------|-------------|
|
||||
| `PROXY_PORT` | Порт прокси | 11435 |
|
||||
| `AUTH_TOKEN` | Токен авторизации | — (обязательна) |
|
||||
| `OLLAMA_URL` | URL реальной Ollama | http://localhost:11434 |
|
||||
| `ROUTER_MODEL` | Модель-классификатор | gemma:1b |
|
||||
| `CODE_MODEL` | Модель для кода | qwen2.5-coder:1.5b |
|
||||
| `DOC_MODEL` | Модель для документов | gemma:1b |
|
||||
| `GENERAL_MODEL` | Общая модель | gemma:1b |
|
||||
|
||||
---
|
||||
|
||||
## 10. Технологический стек
|
||||
|
||||
| Компонент | Технология |
|
||||
|-----------|-----------|
|
||||
| Язык | Go |
|
||||
| HTTP-фреймворк | chi/v5 |
|
||||
| Конфигурация | caarlos0/env/v11 |
|
||||
| Streaming | bufio.Scanner + http.Flusher |
|
||||
| LLM-инфраструктура | Ollama (локально) |
|
||||
|
||||
---
|
||||
|
||||
## 11. Следующие этапы (после MVP)
|
||||
|
||||
- **Авторизация**: полноценный JWT + PostgreSQL + регистрация/логин
|
||||
- **История чатов**: хранение сообщений в БД (User → Chat → Message)
|
||||
- **Production-модели**: qwen2.5-coder:32b, deepseek-r1:70b
|
||||
- **Codex CLI**: тестирование сквозного потока с реальным клиентом
|
||||
File diff suppressed because one or more lines are too long
@@ -1,36 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>ORM Template — User ERD</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial; padding: 16px; }
|
||||
h2 { margin-bottom: 4px; }
|
||||
h3 { margin-top: 28px; }
|
||||
.hint { color: #666; font-size: 0.9em; }
|
||||
pre.mermaid { border: 1px solid #ddd; border-radius: 8px; padding: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>ORM Template — ER-диаграмма</h2>
|
||||
<div class="hint">Единственная модель: User и ее атрибуты.</div>
|
||||
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
USER {
|
||||
uuid id PK "первичный ключ пользователя"
|
||||
text login "уникальный логин"
|
||||
text password "хэш пароля"
|
||||
text role "роль пользователя"
|
||||
bool is_active "активность"
|
||||
bool is_temporal "пароль временный, хранится в открытом виде"
|
||||
timestamptz created_at "дата создания"
|
||||
}
|
||||
</pre>
|
||||
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({ startOnLoad: true, theme: "neutral" });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,369 +0,0 @@
|
||||
"""Generate architecture.docx from architecture content."""
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.enum.table import WD_TABLE_ALIGNMENT
|
||||
import os
|
||||
|
||||
doc = Document()
|
||||
|
||||
style = doc.styles['Normal']
|
||||
font = style.font
|
||||
font.name = 'Calibri'
|
||||
font.size = Pt(11)
|
||||
|
||||
def add_heading(text, level=1):
|
||||
h = doc.add_heading(text, level=level)
|
||||
for run in h.runs:
|
||||
run.font.color.rgb = RGBColor(0x1A, 0x1A, 0x2E)
|
||||
|
||||
def add_para(text, bold=False):
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run(text)
|
||||
run.bold = bold
|
||||
return p
|
||||
|
||||
def add_bullet(text, bold_prefix=None):
|
||||
p = doc.add_paragraph(style='List Bullet')
|
||||
if bold_prefix:
|
||||
run = p.add_run(bold_prefix)
|
||||
run.bold = True
|
||||
p.add_run(text)
|
||||
else:
|
||||
p.add_run(text)
|
||||
|
||||
def add_table(headers, rows):
|
||||
table = doc.add_table(rows=1, cols=len(headers))
|
||||
table.style = 'Light Grid Accent 1'
|
||||
table.alignment = WD_TABLE_ALIGNMENT.LEFT
|
||||
hdr = table.rows[0]
|
||||
for i, h in enumerate(headers):
|
||||
hdr.cells[i].text = h
|
||||
for p in hdr.cells[i].paragraphs:
|
||||
for run in p.runs:
|
||||
run.bold = True
|
||||
for row_data in rows:
|
||||
row = table.add_row()
|
||||
for i, val in enumerate(row_data):
|
||||
row.cells[i].text = val
|
||||
doc.add_paragraph()
|
||||
|
||||
def add_code(text):
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run(text)
|
||||
run.font.name = 'Consolas'
|
||||
run.font.size = Pt(9)
|
||||
run.font.color.rgb = RGBColor(0x33, 0x33, 0x33)
|
||||
pf = p.paragraph_format
|
||||
pf.space_before = Pt(4)
|
||||
pf.space_after = Pt(4)
|
||||
|
||||
# === TITLE ===
|
||||
title = doc.add_heading('Архитектура AI-платформы', level=0)
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
doc.add_paragraph()
|
||||
|
||||
# === 1. ОБЩЕЕ ОПИСАНИЕ ===
|
||||
add_heading('1. Общее описание')
|
||||
add_para('AI-платформа для интеллектуальной маршрутизации запросов пользователей по различным LLM-моделям. Система состоит из двух основных компонентов:')
|
||||
add_bullet(' — авторизация, управление чатами и сообщениями, REST API', 'Go Backend')
|
||||
add_bullet(' — классификация запросов и маршрутизация к нужной модели', 'Python LLM Orchestrator')
|
||||
|
||||
# === 2. АРХИТЕКТУРНАЯ СХЕМА ===
|
||||
add_heading('2. Архитектурная схема')
|
||||
add_para('Общий поток данных:', bold=True)
|
||||
add_code(
|
||||
'Пользователь → Go Backend (:8080) → Python Orchestrator (:8000) → LLM (Groq API)\n'
|
||||
' ↕ ↕\n'
|
||||
' PostgreSQL Router LLM → Target LLM'
|
||||
)
|
||||
doc.add_paragraph()
|
||||
|
||||
add_para('Компоненты Go Backend:', bold=True)
|
||||
add_bullet(' — /api/auth/register, /api/auth/login', 'Auth Handler')
|
||||
add_bullet(' — CRUD /api/chats', 'Chat Handler')
|
||||
add_bullet(' — POST/GET /api/chats/{id}/messages', 'Message Handler')
|
||||
add_bullet(' — bcrypt + JWT генерация/валидация', 'Auth Service')
|
||||
add_bullet(' — CRUD операции с чатами', 'Chat Service')
|
||||
add_bullet(' — sliding window контекст + вызов оркестратора', 'Message Service')
|
||||
add_bullet(' — HTTP-клиент к Python FastAPI', 'Orchestrator Client')
|
||||
|
||||
doc.add_paragraph()
|
||||
add_para('Компоненты Python Orchestrator:', bold=True)
|
||||
add_bullet(' — маленькая быстрая модель (llama-3.1-8b-instant) классифицирует запрос', 'Router LLM')
|
||||
add_bullet(' — Qwen 2.5 Coder 32B для программирования', 'Code Model')
|
||||
add_bullet(' — DeepSeek R1 70B для анализа документов', 'Document Model')
|
||||
add_bullet(' — Llama 3.3 70B для общих вопросов', 'General Model')
|
||||
|
||||
# === 3. СТРУКТУРА ПРОЕКТА ===
|
||||
add_heading('3. Структура проекта')
|
||||
add_code(
|
||||
'.\n'
|
||||
'├── cmd/server/main.go # Точка входа Go backend\n'
|
||||
'├── internal/\n'
|
||||
'│ ├── config/config.go # Конфигурация (env-переменные)\n'
|
||||
'│ ├── handler/\n'
|
||||
'│ │ ├── auth.go # Хендлеры регистрации и логина\n'
|
||||
'│ │ ├── chat.go # CRUD чатов\n'
|
||||
'│ │ ├── message.go # Отправка/получение сообщений\n'
|
||||
'│ │ └── middleware.go # JWT middleware\n'
|
||||
'│ ├── service/\n'
|
||||
'│ │ ├── auth.go # bcrypt, JWT генерация\n'
|
||||
'│ │ ├── chat.go # Бизнес-логика чатов\n'
|
||||
'│ │ ├── message.go # Контекст + вызов оркестратора\n'
|
||||
'│ │ └── orchestrator.go # HTTP-клиент к Python\n'
|
||||
'│ └── router/router.go # chi router, маршруты\n'
|
||||
'├── ent/schema/\n'
|
||||
'│ ├── common.go # Миксины (PkMixin, RegisteredMixin)\n'
|
||||
'│ ├── user.go # Модель User\n'
|
||||
'│ ├── chat.go # Модель Chat\n'
|
||||
'│ └── message.go # Модель Message\n'
|
||||
'├── python/\n'
|
||||
'│ ├── pyproject.toml\n'
|
||||
'│ ├── app/\n'
|
||||
'│ │ ├── main.py # FastAPI приложение\n'
|
||||
'│ │ ├── config.py # Настройки\n'
|
||||
'│ │ ├── routers/completions.py # Эндпоинт чат-комплишенов\n'
|
||||
'│ │ └── services/\n'
|
||||
'│ │ ├── router_llm.py # Классификатор запросов\n'
|
||||
'│ │ ├── groq_client.py # Клиент Groq API\n'
|
||||
'│ │ └── vllm_client.py # Клиент vLLM (заглушка)\n'
|
||||
'│ └── tests/\n'
|
||||
'├── atlas/migrations/ # SQL-миграции\n'
|
||||
'├── Makefile\n'
|
||||
'└── go.mod / go.sum'
|
||||
)
|
||||
|
||||
# === 4. МОДЕЛЬ ДАННЫХ ===
|
||||
add_heading('4. Модель данных (PostgreSQL)')
|
||||
|
||||
add_para('Таблица USERS:', bold=True)
|
||||
add_table(
|
||||
['Поле', 'Тип', 'Ограничения'],
|
||||
[
|
||||
['id', 'UUID', 'PRIMARY KEY, DEFAULT uuid_generate_v4()'],
|
||||
['login', 'VARCHAR(128)', 'UNIQUE, NOT NULL'],
|
||||
['password_hash', 'VARCHAR(256)', 'NOT NULL (bcrypt)'],
|
||||
['role', 'VARCHAR(64)', "DEFAULT 'user'"],
|
||||
['is_active', 'BOOLEAN', 'DEFAULT true'],
|
||||
['created_at', 'TIMESTAMP', 'IMMUTABLE, DEFAULT now()'],
|
||||
]
|
||||
)
|
||||
|
||||
add_para('Таблица CHATS:', bold=True)
|
||||
add_table(
|
||||
['Поле', 'Тип', 'Ограничения'],
|
||||
[
|
||||
['id', 'UUID', 'PRIMARY KEY'],
|
||||
['title', 'VARCHAR(256)', 'OPTIONAL'],
|
||||
['active_model', 'VARCHAR(128)', 'OPTIONAL'],
|
||||
['created_at', 'TIMESTAMP', 'IMMUTABLE, DEFAULT now()'],
|
||||
['user_chats', 'UUID', 'FK → users.id, NOT NULL'],
|
||||
]
|
||||
)
|
||||
|
||||
add_para('Таблица MESSAGES:', bold=True)
|
||||
add_table(
|
||||
['Поле', 'Тип', 'Ограничения'],
|
||||
[
|
||||
['id', 'UUID', 'PRIMARY KEY'],
|
||||
['role', 'ENUM', 'user | assistant | system'],
|
||||
['content', 'TEXT', 'NOT NULL'],
|
||||
['model_used', 'VARCHAR(128)', 'OPTIONAL'],
|
||||
['token_count', 'INTEGER', 'OPTIONAL'],
|
||||
['created_at', 'TIMESTAMP', 'IMMUTABLE, DEFAULT now()'],
|
||||
['chat_messages', 'UUID', 'FK → chats.id, NOT NULL'],
|
||||
]
|
||||
)
|
||||
|
||||
add_para('Правило изоляции: каждый чат изолирован. История одного чата не передаётся в другой. Новый чат = чистый контекст.')
|
||||
|
||||
# === 5. API ЭНДПОИНТЫ ===
|
||||
add_heading('5. API эндпоинты')
|
||||
|
||||
add_para('Публичные (без авторизации):', bold=True)
|
||||
add_table(
|
||||
['Метод', 'URL', 'Описание'],
|
||||
[
|
||||
['GET', '/health', 'Проверка работоспособности'],
|
||||
['POST', '/api/auth/register', 'Регистрация {login, password} → {token}'],
|
||||
['POST', '/api/auth/login', 'Вход {login, password} → {token}'],
|
||||
]
|
||||
)
|
||||
|
||||
add_para('Защищённые (Bearer JWT):', bold=True)
|
||||
add_table(
|
||||
['Метод', 'URL', 'Описание'],
|
||||
[
|
||||
['POST', '/api/chats', 'Создать чат {title?, active_model?}'],
|
||||
['GET', '/api/chats', 'Список чатов пользователя'],
|
||||
['GET', '/api/chats/{id}', 'Получить чат'],
|
||||
['DELETE', '/api/chats/{id}', 'Удалить чат (каскадно с сообщениями)'],
|
||||
['POST', '/api/chats/{id}/messages', 'Отправить сообщение → получить ответ LLM'],
|
||||
['GET', '/api/chats/{id}/messages', 'История сообщений (пагинация)'],
|
||||
]
|
||||
)
|
||||
|
||||
add_para('Python Orchestrator (внутренний):', bold=True)
|
||||
add_table(
|
||||
['Метод', 'URL', 'Описание'],
|
||||
[
|
||||
['POST', '/api/v1/chat/completions', 'Получить ответ от LLM'],
|
||||
]
|
||||
)
|
||||
|
||||
# === 6. ПОТОК ОБРАБОТКИ ===
|
||||
add_heading('6. Поток обработки сообщения')
|
||||
add_para('1. Пользователь отправляет POST /api/chats/{id}/messages с содержимым сообщения.')
|
||||
add_para('2. Go Backend:')
|
||||
add_bullet('JWT middleware — проверка токена, извлечение user_id')
|
||||
add_bullet('Проверка: чат принадлежит пользователю')
|
||||
add_bullet('Сохранить user-сообщение в БД')
|
||||
add_bullet('Собрать контекст: последние N сообщений из чата (sliding window)')
|
||||
add_bullet('HTTP POST → Python Orchestrator')
|
||||
add_para('3. Python Orchestrator:')
|
||||
add_bullet('Router LLM классифицирует запрос (code / document / general)')
|
||||
add_bullet('Выбирает целевую модель')
|
||||
add_bullet('Отправляет контекст + запрос в модель (через Groq API)')
|
||||
add_bullet('Возвращает: {content, model_used, tokens}')
|
||||
add_para('4. Go Backend:')
|
||||
add_bullet('Сохранить assistant-сообщение в БД (с model_used и token_count)')
|
||||
add_bullet('Вернуть ответ пользователю')
|
||||
|
||||
# === 7. МАРШРУТИЗАЦИЯ ===
|
||||
add_heading('7. Маршрутизация запросов (Router LLM)')
|
||||
add_para('Router LLM — маленькая быстрая модель (llama-3.1-8b-instant), которая анализирует запрос и определяет категорию:')
|
||||
add_table(
|
||||
['Категория', 'Целевая модель (Groq)', 'Когда'],
|
||||
[
|
||||
['code', 'qwen-2.5-coder-32b', 'Программирование, код, отладка'],
|
||||
['document', 'deepseek-r1-distill-llama-70b', 'Анализ документов, резюме, тексты'],
|
||||
['general', 'llama-3.3-70b-versatile', 'Всё остальное'],
|
||||
]
|
||||
)
|
||||
add_para('Если пользователь явно выбрал модель в чате (active_model), маршрутизация пропускается.')
|
||||
|
||||
# === 8. КОНТЕКСТ ===
|
||||
add_heading('8. Управление контекстом')
|
||||
add_bullet('Sliding Window: передаются последние N сообщений (по умолчанию 20)', '')
|
||||
add_bullet('Лимит токенов: максимум MAX_TOKENS (по умолчанию 4096)', '')
|
||||
add_bullet('Приблизительный подсчёт: len(content) / 4 ≈ 1 токен', '')
|
||||
add_bullet('Новый чат: пользователь создаёт новый чат для сброса контекста', '')
|
||||
|
||||
# === 9. АУТЕНТИФИКАЦИЯ ===
|
||||
add_heading('9. Аутентификация')
|
||||
add_bullet('Пароли хешируются bcrypt (golang.org/x/crypto)')
|
||||
add_bullet('JWT токен (HS256): sub (user_id), role, exp, iat')
|
||||
add_bullet('Срок действия: 24 часа (настраивается через JWT_EXPIRE_HOURS)')
|
||||
add_bullet('Заголовок: Authorization: Bearer <token>')
|
||||
|
||||
# === 10. СТЕК ===
|
||||
add_heading('10. Технологический стек')
|
||||
|
||||
add_para('Go Backend:', bold=True)
|
||||
add_table(
|
||||
['Компонент', 'Технология'],
|
||||
[
|
||||
['HTTP-фреймворк', 'chi/v5'],
|
||||
['ORM', 'Ent (entgo.io)'],
|
||||
['Миграции', 'Atlas'],
|
||||
['JWT', 'golang-jwt/jwt/v5'],
|
||||
['Хеширование', 'golang.org/x/crypto (bcrypt)'],
|
||||
['БД-драйвер', 'pgx/v5'],
|
||||
['Конфигурация', 'caarlos0/env/v11'],
|
||||
]
|
||||
)
|
||||
|
||||
add_para('Python Orchestrator:', bold=True)
|
||||
add_table(
|
||||
['Компонент', 'Технология'],
|
||||
[
|
||||
['Web-фреймворк', 'FastAPI'],
|
||||
['ASGI-сервер', 'Uvicorn'],
|
||||
['LLM API', 'Groq SDK'],
|
||||
['HTTP-клиент', 'httpx'],
|
||||
['Конфигурация', 'pydantic-settings'],
|
||||
]
|
||||
)
|
||||
|
||||
# === 11. ENV ===
|
||||
add_heading('11. Переменные окружения')
|
||||
|
||||
add_para('Go Backend:', bold=True)
|
||||
add_table(
|
||||
['Переменная', 'Описание', 'По умолчанию'],
|
||||
[
|
||||
['DB_URL', 'PostgreSQL connection string', '— (обязательна)'],
|
||||
['JWT_SECRET', 'Секрет для подписи JWT', '— (обязательна)'],
|
||||
['PORT', 'Порт сервера', '8080'],
|
||||
['JWT_EXPIRE_HOURS', 'Время жизни токена (часы)', '24'],
|
||||
['ORCHESTRATOR_URL', 'URL Python-оркестратора', 'http://localhost:8000'],
|
||||
['CONTEXT_WINDOW', 'Кол-во сообщений в контексте', '20'],
|
||||
['MAX_TOKENS', 'Максимум токенов контекста', '4096'],
|
||||
]
|
||||
)
|
||||
|
||||
add_para('Python Orchestrator:', bold=True)
|
||||
add_table(
|
||||
['Переменная', 'Описание', 'По умолчанию'],
|
||||
[
|
||||
['GROQ_API_KEY', 'Ключ Groq API', '— (обязательна)'],
|
||||
['ROUTER_MODEL', 'Модель-классификатор', 'llama-3.1-8b-instant'],
|
||||
['PORT', 'Порт оркестратора', '8000'],
|
||||
]
|
||||
)
|
||||
|
||||
# === 12. JSON КОНТРАКТ ===
|
||||
add_heading('12. JSON-контракт Go ↔ Python')
|
||||
|
||||
add_para('Запрос (Go → Python):', bold=True)
|
||||
add_code(
|
||||
'{\n'
|
||||
' "messages": [\n'
|
||||
' {"role": "user", "content": "Напиши функцию..."},\n'
|
||||
' {"role": "assistant", "content": "Вот функция..."},\n'
|
||||
' {"role": "user", "content": "А теперь добавь тесты"}\n'
|
||||
' ],\n'
|
||||
' "active_model": null\n'
|
||||
'}'
|
||||
)
|
||||
|
||||
add_para('Ответ (Python → Go):', bold=True)
|
||||
add_code(
|
||||
'{\n'
|
||||
' "content": "Вот тесты для функции...",\n'
|
||||
' "model_used": "qwen-2.5-coder-32b",\n'
|
||||
' "tokens": 523\n'
|
||||
'}'
|
||||
)
|
||||
|
||||
# === 13. ЭТАПЫ ===
|
||||
add_heading('13. Этапы разработки')
|
||||
|
||||
add_para('Спринт 1: Go-скелет ✓', bold=True)
|
||||
add_bullet('go.mod, зависимости, Ent-схемы (User, Chat, Message), main.go + /health')
|
||||
|
||||
add_para('Спринт 2: Аутентификация', bold=True)
|
||||
add_bullet('Сервис регистрации/логина (bcrypt + JWT), middleware, хендлеры')
|
||||
|
||||
add_para('Спринт 3: CRUD чатов', bold=True)
|
||||
add_bullet('Сервис и REST-хендлеры создания/получения/удаления чатов')
|
||||
|
||||
add_para('Спринт 4: Python-оркестратор', bold=True)
|
||||
add_bullet('FastAPI, Router LLM, Groq API клиент, эндпоинт /api/v1/chat/completions')
|
||||
|
||||
add_para('Спринт 5: Сквозной поток сообщений', bold=True)
|
||||
add_bullet('Go HTTP-клиент к Python, sliding window, хендлеры сообщений, полный цикл')
|
||||
|
||||
# === 14. PRODUCTION ===
|
||||
add_heading('14. Production-сценарий (будущее)')
|
||||
add_para('После проверки маршрутизации через Groq API система переводится на локальные модели:')
|
||||
add_bullet('Каждая модель запускается через vLLM на отдельной GPU')
|
||||
add_bullet('Каждая модель — отдельный сервис (отдельный порт)')
|
||||
add_bullet('Python Orchestrator переключается с Groq API на локальные vLLM-эндпоинты')
|
||||
add_bullet('Внешние API не используются')
|
||||
|
||||
# Save
|
||||
output_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'architecture.docx')
|
||||
doc.save(output_path)
|
||||
print(f"Saved to {output_path}")
|
||||
Reference in New Issue
Block a user