Дисклеймер: это open source, в нем могут быть недостатки, заходите, предлагайте идеи, исправления. Публикую тут в ознакомительных и образовательных целях. Выпилил этот кусок в open source из части личного проекта, о котором писал тут. Весь код писал полностью 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 Лицензия: 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 упражнений) │ │ │ │ Хочешь посмотреть детали первой недели?" │ └─────────────────────────────────────────────────────────────────┘
Multi-Provider AI
Почему три провайдера?
Больше было лень
Провайдер | Лучше для | Стоимость | Приватность |
|---|---|---|---|
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 | Оригинал, GPT-4o/4.1 | |
Groq | Llama 3.3 70B за 0.5 сек | |
Together AI | Дешёвые open-source модели | |
DeepSeek | DeepSeek-V3, дёшево и качественно | |
OpenRouter | 100+ моделей, один API key | |
Fireworks | Быстрый 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)
Multi-Provider Fallback
Если основной провайдер недоступен - автоматический 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")
27 MCP-инструментов
Зачем?
Первая версия имела 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
Knowledge Graph упражнений
Почему граф?
Простая база упражнений не отвечает на вопросы:
"Чем заменить приседания, если болит колено?"
"Как усложнить отжимания, когда стало легко?"
"Какие упражнения нагружают нижнюю часть груди?"
Граф хранит связи между упражнениями:
┌─────────────┐ TARGETS ┌─────────────┐ │ Bench │─────────────────>│ Chest │ │ Press │ │ (Muscle) │ └─────────────┘ └─────────────┘ │ ^ │ ALTERNATIVE │ v │ TARGETS ┌─────────────┐ ┌─────────────┐ │ Dumbbell │──────────────────│ Triceps │ │ Press │ │ (Muscle) │ └─────────────┘ └─────────────┘ │ │ PROGRESSION_TO v ┌─────────────┐ │ Incline │ │ Press │ └─────────────┘
Реализация: NetworkX или Neo4j
# 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 - изоляция без осевой нагрузки Какой вариант добавить в программу?"
RAG-память с pgvector
P.S. На своем личном я использую Claude Agent SDK, там работает немного иначе. Я сделал непрерывную сессию с контекстом 1 млн токенов, автокомпактом и построил память на Zep.
Проблема контекста
LLM имеют ограниченное контекстное окно. Нельзя загрузить всю историю тренировок в каждый запрос.
Решение: Semantic Search
┌─────────────────────────────────────────────────────────────────┐ │ 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()
Бесплатные embeddings через Ollama
Не хотите платить 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"]
Plan Navigator
Проблема
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, давая ему понимание текущего состояния без загрузки всей базы.
SSE Streaming
Почему SSE, а не WebSocket?
Критерий | 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: text\ndata: {json.dumps({'content': event.content})}\n\n" case "thinking": # Extended Thinking (Claude Agent SDK v2) yield f"event: thinking\ndata: {json.dumps({'thought': event.content})}\n\n" case "tool_start": yield f"event: tool_start\ndata: {json.dumps({'tool': event.tool, 'input': event.input})}\n\n" case "tool_result": yield f"event: tool_result\ndata: {json.dumps({'tool': event.tool, 'result': event.result})}\n\n" case "done": yield f"event: done\ndata: {json.dumps({'status': 'completed'})}\n\n" case "error": yield f"event: error\ndata: {json.dumps({'error': str(event.error)})}\n\n" 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); });
PWA с Material You (потому что у меня Pixel)
Дизайн-система
Использовали Material 3 / Material You:
Большие радиусы скругления (28px для карточек)
Мягкие тени с цветовым оттенком
Градиентные фоны
Минимум 44px для tap targets (мобильная доступность)
Offline Support
// 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 Action Queue
Когда пользователь 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
Docker Compose
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"
Расходы на API (не знаю зачем вам это, но все спрашивают)
Claude Sonnet 4.5
Метрика | Значение |
|---|---|
Input | $3 / 1M токенов |
Output | $15 / 1M токенов |
Средний запрос | ~800 input + 400 output токенов |
Стоимость запроса | ~$0.008 |
100 запросов/день | ~$24/месяц |
OpenAI GPT-4o-mini
Метрика | Значение |
|---|---|
Input | $0.15 / 1M токенов |
Output | $0.60 / 1M токенов |
Средний запрос | ~800 input + 400 output токенов |
Стоимость запроса | ~$0.0004 |
100 запросов/день | ~$1.2/месяц |
Ollama (локально)
Метрика | Значение |
|---|---|
Стоимость API | $0 |
Требования | 8GB+ RAM, GPU опционально |
Качество | 70-80% от Claude |
Эволюция проекта
v1.0: 6 инструментов
Проблема: 20-30 tool calls на создание плана Время: 15-25 секунд UX: Плохой
v2.0: 27 инструментов + batch
Решение: create_full_plan, create_full_week Tool calls: 1-3 Время: 3-5 секунд UX: Хороший
v3.0: + Knowledge Graph
Добавлено: Альтернативы, прогрессии, поиск по мышцам Новые возможности: "чем заменить?", "как усложнить?"
v4.0: + RAG память
Добавлено: pgvector, долгосрочный контекст Новые возможности: "что я делал в прошлом месяце?"
Что еще
1. Единый mega-промпт
Идея: Загрузить всю информацию в system prompt
Проблема: 10K+ токенов, медленно, дорого
Решение: Plan Navigator (300-500 токенов)
2. Langchain
Идея: Использовать готовый framework
Проблема: Избыточная абстракция, сложный дебаг
Решение: Прямые вызовы Anthropic SDK
3. Один провайдер
Идея: Только Claude, без fallback
Проблема: При сбое API - полный downtime
Решение: Multi-provider с автоматическим переключением
4. WebSocket для чата
Идея: Real-time bidirectional
Проблема: Сложнее SSE, reconnection headache
Решение: SSE с автоматическим reconnection
Что работает
Batch-инструменты - один вызов вместо десятков
Граф упражнений - семантические связи лучше SQL joins
SSE - проще WebSocket для однонаправленного потока
Protocol-based providers - легко добавить нового провайдера
pgvector - векторный поиск без отдельной базы
Чего избегать
Mega-промпты - дорого и медленно
Жёсткая привязка к провайдеру - API падают
Синхронные tool calls - asyncio.gather спасает
Хранение всей истории в контексте - RAG лучше
Roadmap
Сделано:
[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
Лицензия: MIT - форкайте, модифицируйте, коммерциализируйте.
Ссылки
Проект разработан совместно с Claude Code CLI. Код написан AI, архитектурные решения - человек.
