Дисклеймер: это 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: 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);
});
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, архитектурные решения – человек.
Автор: men10577


