- BrainTools - https://www.braintools.ru -
Дисклеймер: это open source, в нем могут быть недостатки, заходите, предлагайте идеи, исправления. Публикую тут в ознакомительных и образовательных целях. Выпилил этот кусок в open source из части личного проекта, о котором писал тут [1]. Весь код писал полностью Claude Code на Opus 4.5 с thinking режимом.
Выделили из production-проекта и открыли в open-source PWA-приложение для персонального фитнес-коучинга с AI. Пользователь общается с тренером через чат, а тот создаёт программы тренировок, отслеживает прогресс, предлагает альтернативные упражнения.
В статье:
Multi-provider AI (Claude, GPT, Ollama) – переключается одной переменной
27 MCP-инструментов для управления тренировками
Knowledge Graph упражнений (NetworkX / Neo4j)
RAG-память с pgvector для долгосрочного контекста
PWA с offline-режимом
GitHub: https://github.com/gmen1057/fitness-coach [2] Лицензия: MIT

Просто так захотелось. Использовать 12+ агентов с Claude Code CLI и сделать что-то для себя. По токенам не скажу, все в рамках подписки за 200$.
Нужно было:
AI-тренер, который помнит историю тренировок
Возможность модифицировать программу через естественный язык
Приватность (для параноиков) – опция запускать AI локально (Ollama)
Граф упражнений – “болит колено, чем заменить приседания?”
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Frontend │────>│ Backend │────>│ PostgreSQL │
│ (Next.js) │ SSE │ (FastAPI) │ │ + pgvector │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
┌────────────┼────────────┐
v v v
┌──────────┐ ┌──────���───┐ ┌──────────┐
│ Anthropic│ │ OpenAI │ │ Ollama │
│ Claude │ │ GPT │ │ (Local) │
└──────────┘ └──────────┘ └──────────┘
Пользователь: "Составь мне план для набора массы, 4 дня в неделю"
│
v
┌─────────────────────────────────────────────────────────────────┐
│ AI AGENT (Claude/GPT/Ollama) │
│ │
│ 1. Анализирует запрос │
│ 2. Выбирает инструмент: create_full_plan │
│ 3. Генерирует параметры: │
│ {name: "Mass Building", goal: "hypertrophy", │
│ weeks: 8, days_per_week: 4} │
└─────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ MCP TOOL: create_full_plan │
│ │
│ - Создаёт план в PostgreSQL │
│ - Генерирует 8 недель × 4 дня = 32 тренировки │
│ - Каждая тренировка: 5-8 упражнений из Knowledge Graph │
│ - Возвращает структуру плана AI │
└─────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ AI RESPONSE │
│ │
│ "Создал план 'Mass Building' на 8 недель: │
│ - Понедельник: Грудь + Трицепс (8 упражнений) │
│ - Вторник: Спина + Бицепс (7 упражнений) │
│ - Четверг: Плечи + Пресс (6 упражнений) │
│ - Пятница: Ноги (8 упражнений) │
│ │
│ Хочешь посмотреть детали первой недели?" │
└─────────────────────────────────────────────────────────────────┘
Больше было лень
|
Провайдер |
Лучше для |
Стоимость |
Приватность |
|---|---|---|---|
|
Claude Sonnet 4.5 |
Качество, reasoning |
$3-15/1M токенов |
Облако |
|
GPT-4o |
Совместимость |
$2.50-10/1M токенов |
Облако |
|
Ollama (llama3.3) |
Приватность, offline |
Бесплатно |
100% локально |
OpenAI API – это стандарт. Один клиент:
┌─────────────────────────────────────────────────────────────────┐
│ OpenAI-совместимый клиент │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────��
│ │ │
v v v
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ OpenAI │ │ Облачные │ │ Локальные │
│ GPT-4o/4.1 │ │ провайдеры │ │ серверы │
└───────────────┘ └───────────────┘ └───────────────┘
│ │
┌────────────┼────────────┐ │
v v v v
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Groq │ │Together│ │ Open │ │ vLLM │
│(быстро)│ │ AI │ │ Router │ │LMStudio│
└────────┘ └────────┘ └────────┘ └────────┘
│ │ │
v v v
┌────────┐ ┌────────┐ ┌────────┐
│DeepSeek│ │Fireworks│ │100+ моделей│
│Mistral │ │Perplexity│ │через один│
└────────┘ └────────┘ │ endpoint │
└────────┘
Что это значит на практике:
|
Провайдер |
base_url |
Зачем |
|---|---|---|
|
OpenAI |
api.openai.com [3] |
Оригинал, GPT-4o/4.1 |
|
Groq |
api.groq.com [4] |
Llama 3.3 70B за 0.5 сек |
|
Together AI |
api.together.xyz [5] |
Дешёвые open-source модели |
|
DeepSeek |
api.deepseek.com [6] |
DeepSeek-V3, дёшево и качественно |
|
OpenRouter |
openrouter.ai [7] |
100+ моделей, один API key |
|
Fireworks |
api.fireworks.ai [8] |
Быстрый inference |
|
vLLM |
localhost:8000 |
Свой сервер с любой моделью |
|
LM Studio |
localhost:1234 |
Desktop app, GPU inference |
Меняем две переменные – base_url и api_key:
# Вариант 1: Anthropic (рекомендуется)
ANTHROPIC_API_KEY=sk-ant-api03-xxx
# Вариант 2: OpenAI
OPENAI_API_KEY=sk-proj-xxx
# Вариант 3: Локально
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama3.3
from typing import Protocol, AsyncIterator
class AIProvider(Protocol):
"""Протокол для AI-провайдеров"""
async def chat_stream(
self,
messages: list[dict],
tools: list[dict]
) -> AsyncIterator[StreamEvent]:
"""Стриминговый ответ с поддержкой tool calls"""
...
class AnthropicProvider:
def __init__(self):
self.client = Anthropic(api_key=settings.ANTHROPIC_API_KEY)
self.model = settings.ANTHROPIC_MODEL or "claude-sonnet-4-5"
async def chat_stream(self, messages, tools):
async with self.client.messages.stream(
model=self.model,
messages=messages,
tools=tools,
max_tokens=4096
) as stream:
async for event in stream:
yield self._convert_event(event)
class OllamaProvider:
def __init__(self):
self.base_url = settings.OLLAMA_BASE_URL
self.model = settings.OLLAMA_MODEL or "llama3.3"
async def chat_stream(self, messages, tools):
# Ollama не поддерживает native tool calling
# Эмулируем через structured prompting
enhanced_prompt = self._inject_tools_into_prompt(messages, tools)
async for chunk in self._stream_completion(enhanced_prompt):
yield self._parse_tool_calls(chunk)
Если основной провайдер недоступен – автоматический fallback:
class AIRouter:
def __init__(self):
self.providers = [
AnthropicProvider(),
OpenAIProvider(),
OllamaProvider()
]
async def chat_stream(self, messages, tools):
for provider in self.providers:
try:
async for event in provider.chat_stream(messages, tools):
yield event
return
except (APIError, ConnectionError) as e:
logger.warning(f"{provider.__class__.__name__} failed: {e}")
continue
raise AllProvidersFailedError("No AI providers available")
Первая версия имела 6 инструментов. Проблема: AI делал 6-10 последовательных вызовов для создания одной программы:
P.S. На своем личном я использую Claude Agent SDK, там работает немного иначе
v1.0 (6 инструментов):
create_plan → create_week → create_day → create_exercise → create_exercise → ...
Итого: 20-30 tool calls для одной программы
Время: 15-25 секунд
Добавили batch-инструменты:
v2.0 (27 инструментов):
create_full_plan (создаёт всё за 1 вызов)
Итого: 1-3 tool calls
Время: 3-5 секунд
# 1. БАЗОВЫЕ (7 штук) - чтение данных
tools_basic = [
"get_workout_plans", # Список планов пользователя
"get_plan_details", # Детали конкретного плана
"get_current_workout", # Сегодняшняя тренировка
"get_workout_stats", # Статистика (streaks, completion rate)
"get_week_details", # Детали недели
"get_day_exercises", # Упражнения дня
"get_workout_history", # История выполненных тренировок
]
# 2. CRUD (8 штук) - создание и редактирование
tools_crud = [
"create_workout_plan", # Создать пустой план
"edit_workout_plan", # Изменить название/цель
"create_week", # Добавить неделю
"create_day", # Добавить день
"add_exercise", # Добавить упражнение
"edit_exercise", # Изменить упражнение
"delete_exercise", # Удалить упражнение
"reorder_exercises", # Изменить порядок
]
# 3. BATCH (2 штуки) - массовые операции
tools_batch = [
"create_full_plan", # Создать полный план за 1 вызов
"create_full_week", # Создать полную неделю за 1 вызов
]
# 4. GRAPH (4 штуки) - работа с графом упражнений
tools_graph = [
"get_exercise_alternatives", # "Чем заменить жим лёжа?"
"get_exercise_progressions", # "Как усложнить отжимания?"
"get_exercises_for_muscle", # "Упражнения на бицепс"
"get_exercise_info", # Детали упражнения
]
# 5. RAG (2 штуки) - долгосрочная память
tools_rag = [
"search_workout_memory", # "Что я делал в прошлом месяце?"
"store_training_insight", # Сохранить инсайт
]
# 6. STATUS (3 штуки) - логирование
tools_status = [
"complete_workout_day", # Отметить день выполненным
"skip_workout_day", # Пропустить с причиной
"add_exercise_note", # Добавить заметку
]
# 7. PROGRAMS (1 штука)
tools_programs = [
"get_training_programs", # Библиотека готовых программ
]
Когда AI вызывает несколько инструментов – выполняем параллельно:
async def execute_tool_calls(tool_calls: list[ToolCall]) -> list[ToolResult]:
"""Параллельное выполнение инструментов через asyncio.gather"""
tasks = [execute_single_tool(tc) for tc in tool_calls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [
ToolResult(
tool_use_id=tc.id,
content=str(r) if not isinstance(r, Exception) else f"Error: {r}"
)
for tc, r in zip(tool_calls, results)
]
Это важно для операций типа “покажи мою статистику и текущую тренировку”:
Последовательно: get_stats (200ms) + get_current_workout (150ms) = 350ms
Параллельно: max(200ms, 150ms) = 200ms
Простая база упражнений не отвечает на вопросы:
“Чем заменить приседания, если болит колено?”
“Как усложнить отжимания, когда стало легко?”
“Какие упражнения нагружают нижнюю часть груди?”
Граф хранит связи между упражнениями:
┌─────────────┐ TARGETS ┌─────────────┐
│ Bench │─────────────────>│ Chest │
│ Press │ │ (Muscle) │
└─────────────┘ └─────────────┘
│ ^
│ ALTERNATIVE │
v │ TARGETS
┌─────────────┐ ┌─────────────┐
│ Dumbbell │──────────────────│ Triceps │
│ Press │ │ (Muscle) │
└─────────────┘ └─────────────┘
│
│ PROGRESSION_TO
v
┌─────────────┐
│ Incline │
│ Press │
└─────────────┘
# Development: In-memory граф (NetworkX)
class InMemoryExerciseGraph:
def __init__(self):
self.graph = nx.DiGraph()
self._load_exercises()
def get_alternatives(self, exercise_id: str) -> list[Exercise]:
"""Найти альтернативные упражнения"""
alternatives = []
for neighbor in self.graph.neighbors(exercise_id):
edge = self.graph.edges[exercise_id, neighbor]
if edge.get("relation") == "ALTERNATIVE":
alternatives.append(self._get_exercise(neighbor))
return alternatives
def get_progressions(self, exercise_id: str) -> list[Exercise]:
"""Найти прогрессии (усложнения)"""
return [
self._get_exercise(n)
for n in self.graph.neighbors(exercise_id)
if self.graph.edges[exercise_id, n].get("relation") == "PROGRESSION_TO"
]
def get_exercises_for_muscle(self, muscle: str) -> list[Exercise]:
"""Найти все упражнения для мышцы"""
return [
self._get_exercise(n)
for n in self.graph.predecessors(muscle)
if self.graph.edges[n, muscle].get("relation") == "TARGETS"
]
# Production: Neo4j для персистентности и масштабирования
class Neo4jExerciseGraph:
def __init__(self):
self.driver = neo4j.GraphDatabase.driver(
settings.NEO4J_URI,
auth=(settings.NEO4J_USER, settings.NEO4J_PASSWORD)
)
def get_alternatives(self, exercise_id: str) -> list[Exercise]:
query = """
MATCH (e:Exercise {id: $exercise_id})-[:ALTERNATIVE]->(alt:Exercise)
RETURN alt
"""
with self.driver.session() as session:
result = session.run(query, exercise_id=exercise_id)
return [Exercise(**record["alt"]) for record in result]
Пользователь: "Болит плечо, чем заменить жим штанги стоя?"
AI внутренне вызывает:
get_exercise_alternatives(exercise="overhead_press")
Граф возвращает:
[
{"name": "Landmine Press", "reason": "меньше нагрузка на плечевой сустав"},
{"name": "Arnold Press", "reason": "контролируемое движение"},
{"name": "Cable Lateral Raise", "reason": "изоляция без компрессии"}
]
AI отвечает:
"При боли в плече могу предложить замены:
1. Landmine Press - меньше нагружает плечевой сустав
2. Arnold Press - более контролируемая амплитуда
3. Cable Lateral Raise - изоляция без осевой нагрузки
Какой вариант добавить в программу?"
P.S. На своем личном я использую Claude Agent SDK, там работает немного иначе. Я сделал непрерывную сессию с контекстом 1 млн токенов, автокомпактом и построил память [9] на Zep.
LLM имеют ограниченное контекстное окно. Нельзя загрузить всю историю тренировок в каждый запрос.
┌─────────────────────────────────────────────────────────────────┐
│ USER MESSAGE │
│ "Покажи тренировки, где я делал становую тягу с большим весо��" │
└─────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ EMBEDDING │
│ text-embedding-3-small → [0.023, -0.156, 0.089, ...] │
└─────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ PGVECTOR SEARCH │
│ SELECT * FROM workout_memories │
│ ORDER BY embedding <=> $query_embedding │
│ LIMIT 5 │
└─────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ RESULTS │
│ 1. "15 янв: становая 140кг × 5 (PR!)" │
│ 2. "8 янв: становая 130кг × 8" │
│ 3. "2 янв: становая 125кг × 10" │
└─────────────────────────────────────────────────────────────────┘
# Модель для хранения памяти
class WorkoutMemory(Base):
__tablename__ = "workout_memories"
id = Column(UUID, primary_key=True)
user_id = Column(UUID, ForeignKey("users.id"))
content = Column(Text) # "15 янв: становая 140кг × 5"
embedding = Column(Vector(1536)) # pgvector
created_at = Column(DateTime)
__table_args__ = (
Index('ix_memory_embedding', embedding, postgresql_using='ivfflat'),
)
# Сервис RAG
class RAGService:
def __init__(self):
self.embedding_client = OpenAI() # или Ollama
async def search(self, query: str, user_id: str, limit: int = 5):
# 1. Получаем embedding запроса
embedding = await self._get_embedding(query)
# 2. Ищем похожие записи
result = await self.db.execute(
select(WorkoutMemory)
.where(WorkoutMemory.user_id == user_id)
.order_by(WorkoutMemory.embedding.cosine_distance(embedding))
.limit(limit)
)
return result.scalars().all()
async def store(self, content: str, user_id: str):
embedding = await self._get_embedding(content)
memory = WorkoutMemory(
user_id=user_id,
content=content,
embedding=embedding
)
self.db.add(memory)
await self.db.commit()
Не хотите платить OpenAI за embeddings? Ollama поддерживает локальные модели:
# Установка
ollama pull nomic-embed-text
# Конфигурация
FITNESS_EMBEDDING_PROVIDER=ollama
FITNESS_OLLAMA_BASE_URL=http://localhost:11434
class OllamaEmbeddingProvider:
async def get_embedding(self, text: str) -> list[float]:
response = await self.client.post(
f"{self.base_url}/api/embeddings",
json={"model": "nomic-embed-text", "prompt": text}
)
return response.json()["embedding"]
AI нужен контекст текущего плана, но загружать всю структуру (8 недель × 4 дня × 7 упражнений) – это тысячи токенов.
Plan Navigator генерирует компактный индекс (300-500 символов):
class PlanNavigator:
def build_context(self, plan_id: str) -> str:
plan = await self.get_plan(plan_id)
current = await self.get_current_position(plan_id)
return f"""
ПЛАН: {plan.name} ({plan.goal})
ПРОГРЕСС: Неделя {current.week}/{plan.total_weeks}, День {current.day}/{current.days_in_week}
СТАТУС: {current.completed_days} выполнено, {current.skipped_days} пропущено
STREAK: {current.streak} дней подряд
СЕГОДНЯ: {current.today_workout.name if current.today_workout else "Отдых"}
Последние 3 тренировки:
- {current.recent[0].date}: {current.recent[0].name} ({'done' if current.recent[0].completed else 'skip'})
- {current.recent[1].date}: {current.recent[1].name} ({'done' if current.recent[1].completed else 'skip'})
- {current.recent[2].date}: {current.recent[2].name} ({'done' if current.recent[2].completed else 'skip'})
"""
Этот контекст добавляется к каждому запросу AI, давая ему понимание текущего состояния без загрузки всей базы.
|
Критерий |
WebSocket |
SSE |
|---|---|---|
|
Направление |
Bidirectional |
Server → Client only |
|
Сложность |
Выше |
Ниже |
|
Reconnection |
Ручной |
Автоматический |
|
HTTP/2 |
Отдельное соединение |
Мультиплексирование |
|
Для AI чата |
Зачем? |
Нормально |
Для AI-чата нам нужен только поток от сервера к клиенту. SSE проще и надёжнее.
# Backend: FastAPI SSE endpoint
@router.post("/chat")
async def chat_stream(request: ChatRequest):
async def event_generator():
async for event in ai_service.chat_stream(request.message):
match event.type:
case "text":
yield f"event: textndata: {json.dumps({'content': event.content})}nn"
case "thinking":
# Extended Thinking (Claude Agent SDK v2)
yield f"event: thinkingndata: {json.dumps({'thought': event.content})}nn"
case "tool_start":
yield f"event: tool_startndata: {json.dumps({'tool': event.tool, 'input': event.input})}nn"
case "tool_result":
yield f"event: tool_resultndata: {json.dumps({'tool': event.tool, 'result': event.result})}nn"
case "done":
yield f"event: donendata: {json.dumps({'status': 'completed'})}nn"
case "error":
yield f"event: errorndata: {json.dumps({'error': str(event.error)})}nn"
return StreamingResponse(
event_generator(),
media_type="text/event-stream"
)
// Frontend: обработка SSE
const eventSource = new EventSource('/api/fitness/chat');
eventSource.addEventListener('text', (e) => {
const data = JSON.parse(e.data);
appendToMessage(data.content);
});
eventSource.addEventListener('tool_start', (e) => {
const data = JSON.parse(e.data);
showToolIndicator(data.tool, 'loading');
});
eventSource.addEventListener('tool_result', (e) => {
const data = JSON.parse(e.data);
showToolIndicator(data.tool, 'success');
});
eventSource.addEventListener('thinking', (e) => {
// Показываем процесс размышления (Extended Thinking)
const data = JSON.parse(e.data);
showThinkingBubble(data.thought);
});
Использовали Material 3 / Material You:
Большие радиусы скругления (28px для карточек)
Мягкие тени с цветовым оттенком
Градиентные фоны
Минимум 44px для tap targets (мобильная доступность)
// Service Worker стратегии
const CACHE_NAME = 'fitness-coach-v1';
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Статические ресурсы: Cache First
if (url.pathname.match(/.(js|css|png|svg)$/)) {
event.respondWith(cacheFirst(event.request));
return;
}
// API данные: Network First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
return;
}
// Chat: Skip cache (SSE streaming)
if (url.pathname.includes('/chat')) {
event.respondWith(fetch(event.request));
return;
}
});
Когда пользователь offline, действия складываются в очередь:
// stores/workout.ts (Zustand)
interface WorkoutState {
offlineQueue: OfflineAction[];
isOffline: boolean;
completeDay: () => Promise<boolean>;
syncOfflineQueue: () => Promise<void>;
}
const useWorkoutStore = create<WorkoutState>()(
persist(
(set, get) => ({
offlineQueue: [],
isOffline: !navigator.onLine,
completeDay: async () => {
const action = { type: 'COMPLETE_DAY', payload: {...}, timestamp: Date.now() };
if (get().isOffline) {
// Offline: добавляем в очередь
set(state => ({
offlineQueue: [...state.offlineQueue, action]
}));
return true;
}
// Online: выполняем сразу
try {
await api.completeDay(action.payload);
return true;
} catch {
set(state => ({ offlineQueue: [...state.offlineQueue, action] }));
return false;
}
},
syncOfflineQueue: async () => {
const queue = get().offlineQueue;
for (const action of queue) {
await api.executeAction(action);
}
set({ offlineQueue: [] });
}
}),
{ name: 'workout-store' }
)
);
git clone https://github.com/gmen1057/fitness-coach.git
cd fitness-coach/docker
# Настройка
cp .env.example .env
nano .env # Добавить API ключ (Anthropic/OpenAI/Ollama)
# Запуск
docker compose up -d
http://localhost:8000 [10].
services:
postgres:
image: pgvector/pgvector:pg16
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: fitness_coach
backend:
build: ../backend
depends_on:
- postgres
environment:
DATABASE_URL: postgresql+asyncpg://...
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
ports:
- "8000:8000"
frontend:
build: ../frontend
depends_on:
- backend
ports:
- "3000:3000"
|
Метрика |
Значение |
|---|---|
|
Input |
$3 / 1M токенов |
|
Output |
$15 / 1M токенов |
|
Средний запрос |
~800 input + 400 output токенов |
|
Стоимость запроса |
~$0.008 |
|
100 запросов/день |
~$24/месяц |
|
Метрика |
Значение |
|---|---|
|
Input |
$0.15 / 1M токенов |
|
Output |
$0.60 / 1M токенов |
|
Средний запрос |
~800 input + 400 output токенов |
|
Стоимость запроса |
~$0.0004 |
|
100 запросов/день |
~$1.2/месяц |
|
Метрика |
Значение |
|---|---|
|
Стоимость API |
$0 |
|
Требования |
8GB+ RAM, GPU опционально |
|
Качество |
70-80% от Claude |
Проблема: 20-30 tool calls на создание плана
Время: 15-25 секунд
UX: Плохой
Решение: create_full_plan, create_full_week
Tool calls: 1-3
Время: 3-5 секунд
UX: Хороший
Добавлено: Альтернативы, прогрессии, поиск по мышцам
Новые возможности: "чем заменить?", "как усложнить?"
Добавлено: pgvector, долгосрочный контекст
Новые возможности: "что я делал в прошлом месяце?"
Идея: Загрузить всю информацию в system prompt
Проблема: 10K+ токенов, медленно, дорого
Решение: Plan Navigator (300-500 токенов)
Идея: Использовать готовый framework
Проблема: Избыточная абстракция, сложный дебаг
Решение: Прямые вызовы Anthropic SDK
Иде��: Только Claude, без fallback
Проблема: При сбое API – полный downtime
Решение: Multi-provider с автоматическим переключением
Идея: Real-time bidirectional
Проблема: Сложнее SSE, reconnection headache
Решение: SSE с автоматическим reconnection
Batch-инструменты – один вызов вместо десятков
Граф упражнений – семантические связи лучше SQL joins
SSE – проще WebSocket для однонаправленного потока
Protocol-based providers – легко добавить нового провайдера
pgvector – векторный поиск без отдельной базы
Mega-промпты – дорого и медленно
Жёсткая привязка к провайдеру – API падают
Синхронные tool calls – asyncio.gather спасает
Хранение всей истории в контексте – RAG лучше
Сделано:
[x] Multi-provider AI
[x] 27 MCP-инструментов
[x] Knowledge Graph
[x] RAG-память
[x] PWA с offline
[x] Docker Compose
Нет:
мне лень
[ ] Nutrition tracking
[ ] Видео упражнений
[ ] Интеграция с фитнес-трекерами
[ ] Мобильное приложение (React Native)
3 AI-провайдера на выбор (облако или локально)
27 инструментов для управления тренировками
Граф упражнений для умных рекомендаций
RAG-память для долгосрочного контекста
PWA с offline-режимом
GitHub: https://github.com/gmen1057/fitness-coach [2]
Лицензия: MIT – форкайте, модифицируйте, коммерциализируйте.
Anthropic Claude API [11]
OpenAI API [12]
Ollama [13]
pgvector [14]
NetworkX [15]
Проект разработан совместно с Claude Code CLI. Код написан AI, архитектурные решения – человек.
Автор: men10577
Источник [16]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/24915
URLs in this post:
[1] тут: https://habr.com/ru/articles/987872/
[2] https://github.com/gmen1057/fitness-coach: https://github.com/gmen1057/fitness-coach
[3] api.openai.com: http://api.openai.com
[4] api.groq.com: http://api.groq.com
[5] api.together.xyz: http://api.together.xyz
[6] api.deepseek.com: http://api.deepseek.com
[7] openrouter.ai: http://openrouter.ai
[8] api.fireworks.ai: http://api.fireworks.ai
[9] память: http://www.braintools.ru/article/4140
[10] http://localhost:8000: http://localhost:8000
[11] Anthropic Claude API: https://docs.anthropic.com/
[12] OpenAI API: https://platform.openai.com/docs
[13] Ollama: https://ollama.ai/
[14] pgvector: https://github.com/pgvector/pgvector
[15] NetworkX: https://networkx.org/
[16] Источник: https://habr.com/ru/articles/990694/?utm_source=habrahabr&utm_medium=rss&utm_campaign=990694
Нажмите здесь для печати.