AI Chat — модуль чата с AI-ассистентом
Архитектура
Бэкенд (cryptodiary)
Точки входа
AiChatController— CRUD чатов + отправка сообщений (SSE stream)POST /api/ai-chat— создать чат (context_type,context_id)POST /api/ai-chat/{id}/messages— отправить сообщение (message,context_data)
Сервисы
AiChatService— стриминг ответа через OpenAI API. Собирает системный промпт (черезChatContextProvider), историю (до 50 сообщений) и отправляет в OpenAI.ChatContextRegistry— реестр провайдеров контекста. РезолвитChatContextProviderпоcontext_type.AiChatLimitService— лимиты бесплатных сообщений.
Контексты (ChatContextProvider)
Интерфейс: App\Services\AI\Chat\Contracts\ChatContextProvider
contextType()— строковый идентификатор (совпадает сcontext_typeчата)getSystemPrompt(AiChat $chat, array $contextData)— формирует системный промптgetModel()— модель OpenAIgetDailyFreeLimit()/getLimitPeriod()— настройки лимитов
Реализации:
PortfolioHealthChatContext(portfolio_health) — анализ портфеля. Загружает данные портфеля с бэкенда черезPortfolioStatsCacheService(state-кеш 7 дней → hydrated-кеш 50 с → расчёт с нуля).context_id= ID портфеля. Фильтрует тикеры, убирая thumb_url, currency, assetable, xirr flows. Фронтенд может дополнительно передавать метрики волатильности/просадки (рассчитываются на клиенте по графику).SupportChatContext(support) — чат поддержки.
Модели
AiChat—user_id,context_type,context_id,titleAiChatMessage—ai_chat_id,role,content,rating,rating_comment
Фронтенд (hadl-front)
Composable & Store
composables/aiChat.js(useAiChat) — работа с API: CRUD чатов, отправка сообщений (SSE), стриминг ответа, лимиты.stores/aiChat.js(useAiChatStore) — состояние виджета:isWidgetOpen,currentContextType,currentContextId,contextData. МетодopenWidget(contextType, contextId, contextData)открывает виджет с нужным контекстом.
Компоненты
AiChatWidget.vue— основной виджет чата (fullscreen/mini, sidebar, messages, input).AiChatToggleButton.vue— кнопка открытия чата (принимаетcontextType,contextId,contextDataпропсами).AiChatMessages.vue/AiChatInput.vue/AiChatSidebar.vue— внутренние компоненты.
Вызов из PortfolioHealth
В PortfolioHealthOverview.vue и PortfolioHealthSwipeCard.vue:
js
aiChatStore.openWidget('portfolio_health', portfolioId, insightsPayload)insightsPayload формируется в composables/portfolioHealth.js — health-скоры, волатильность, просадка, XIRR. Эти данные отправляются как context_data при каждом сообщении.
Поток данных для portfolio_health
- Фронт вызывает
openWidget('portfolio_health', portfolioId, insightsPayload) - При
sendMessage(text)→POST /api/ai-chat/{id}/messagesс{ message, context_data } - Бэкенд в
PortfolioHealthChatContext::getSystemPrompt:- Загружает статистику портфеля из кеша
PortfolioStatsCacheService(приоритет: hydrated → state → расчёт с нуля) - Фильтрует тикеры: оставляет ticker, title, type, symbol, totalQuantity, currentPrice, currentNetworth, currentIncome, spent, fixed, accruals, expenses, commissions, taxes, averageWeightedPrice, price_change_percentage_24h
- Добавляет итоговые показатели: totalSpent, totalIncome, totalCurrentNetworth, xirr и т.д.
- Добавляет health-метрики: diversificationScore, concentrationScore, assetClasses, topPositions
- Мержит метрики с фронта: volatility, max_drawdown (рассчитываются на клиенте по графику)
- Загружает статистику портфеля из кеша
- Системный промпт содержит полный JSON портфеля + рыночный контекст (определяется по
portfolio.currency.code) - OpenAI стримит ответ через SSE
Локализация рыночного контекста
Определяется в buildMarketContext(?Portfolio $portfolio) по коду валюты портфеля:
- RUB →
russianMarketContext(): санкции 2022 (заморозка НРД, недоступность иностранных ETF/акций через российских брокеров), доступные инструменты (MOEX, ОФЗ, флоатеры, БПИФы, замещающие облигации, золото, крипта), высокая ставка ЦБ, налоговая специфика (НДФЛ 13%/15%, ЛДВ, ИИС). - Другие валюты → пустая строка (общие советы без ограничений).
Для добавления нового рынка: добавить case 'XXX': в match внутри buildMarketContext и реализовать метод xxxMarketContext().