Open-Source AI Фитнес-Тренер: 27 MCP-инструментов, 3 провайдера и граф упражнений

Дисклеймер: это 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

Open-Source AI Фитнес-Тренер: 27 MCP-инструментов, 3 провайдера и граф упражнений

Почему не взять готовое?

Просто так захотелось. Использовать 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

api.openai.com

Оригинал, GPT-4o/4.1

Groq

api.groq.com

Llama 3.3 70B за 0.5 сек

Together AI

api.together.xyz

Дешёвые open-source модели

DeepSeek

api.deepseek.com

DeepSeek-V3, дёшево и качественно

OpenRouter

openrouter.ai

100+ моделей, один API key

Fireworks

api.fireworks.ai

Быстрый 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\ndаta: {json.dumps({'content': event.content})}\n\n"

case "thinking":
# Extended Thinking (Claude Agent SDK v2)
yield f"event: thinking\ndаta: {json.dumps({'thought': event.content})}\n\n"

case "tool_start":
yield f"event: tool_start\ndаta: {json.dumps({'tool': event.tool, 'input': event.input})}\n\n"

case "tool_result":
yield f"event: tool_result\ndаta: {json.dumps({'tool': event.tool, 'result': event.result})}\n\n"

case "done":
yield f"event: done\ndаta: {json.dumps({'status': 'completed'})}\n\n"

case "error":
yield f"event: error\ndаta: {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
Объяснить код с

http://localhost:8000.

Docker Compose

services:
postgres:
image: pgvector/pgvector:pg16
volumes:
- postgres_dаta:/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

Что работает

  1. Batch-инструменты - один вызов вместо десятков

  2. Граф упражнений - семантические связи лучше SQL joins

  3. SSE - проще WebSocket для однонаправленного потока

  4. Protocol-based providers - легко добавить нового провайдера

  5. pgvector - векторный поиск без отдельной базы

Чего избегать

  1. Mega-промпты - дорого и медленно

  2. Жёсткая привязка к провайдеру - API падают

  3. Синхронные tool calls - asyncio.gather спасает

  4. Хранение всей истории в контексте - 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, архитектурные решения - человек.


Внимание!

Официальный сайт Perplexity AI доступен по ссылке ниже.

Официальный сайт