import fs from "fs"; import path from "path"; // Вырезано из Data-Insight2 (модуль ИИ-агента) — только чистое IO конфигов и лога. // Telegram-поллинг, cron и сетевые вызовы намеренно не переносятся. const AI_CONFIG_PATH = path.join("data", "ai-config.json"); const AI_LOG_PATH = path.join("data", "ai-query-log.json"); const MAX_LOG_ENTRIES = 500; export interface AiLogEntry { timestamp: string; chatId: string; chatName: string; question: string; answer: string; sqlQueries: string[]; } function loadAiLog(): AiLogEntry[] { try { if (fs.existsSync(AI_LOG_PATH)) { return JSON.parse(fs.readFileSync(AI_LOG_PATH, "utf-8")); } } catch {} return []; } export function saveLogEntry(entry: AiLogEntry): void { try { const log = loadAiLog(); log.push(entry); if (log.length > MAX_LOG_ENTRIES) { log.splice(0, log.length - MAX_LOG_ENTRIES); } fs.writeFileSync(AI_LOG_PATH, JSON.stringify(log, null, 2)); } catch (err) { console.error("[AI Log] Failed to save:", err); } } export function getAiLog(days: number = 90): AiLogEntry[] { const log = loadAiLog(); const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); return log.filter(e => new Date(e.timestamp) >= cutoff); } export interface AiConfig { apiUrl: string; apiKey: string; model: string; systemPrompt: string; enabled: boolean; name: string; } export interface AiPlanningConfig { apiUrl: string; apiKey: string; model: string; systemPrompt: string; name: string; enabled: boolean; } const DEFAULT_SYSTEM_PROMPT = `Ты — финансовый аналитик компании ОмегаПром. Отвечай на русском языке. У тебя есть доступ к двум источникам данных: 1. **База данных PostgreSQL** — содержит таблицы доходов (opdohodi), расходов (oprashodi) и дебиторской задолженности (vw_debt_active). Используй инструмент query_database для SQL-запросов (только SELECT). 2. **Данные зарплат из Google Sheets** — содержат ФИО сотрудников, базовую зарплату и сумму к выдаче по участкам и месяцам. Используй инструмент get_salary_data. ВАЖНО — особенности структуры данных: ### vw_debt_active (дебиторская задолженность): - Таблица хранит ЕЖЕДНЕВНЫЕ СНИМКИ (snapshots) задолженности. Поле "period" — дата снимка. Каждый period содержит полный срез всех долгов на эту дату. - КРИТИЧЕСКИ ВАЖНО: для получения АКТУАЛЬНЫХ данных по задолженности ВСЕГДА фильтруй по последнему периоду: WHERE period = (SELECT MAX(period) FROM vw_debt_active) - БЕЗ фильтра по period суммы будут завышены в сотни раз из-за суммирования по всем историческим снимкам! - Внутри одного периода может быть несколько строк на одного контрагента (по разным документам/договорам). Для агрегатных запросов используй GROUP BY kontragent и SUM(summadolga::numeric). ### opdohodi (доходы) и oprashodi (расходы): - Эти таблицы НЕ имеют снимков — каждая строка уникальна. - Для суммарных запросов по доходам: GROUP BY zakazchik и SUM(summa::numeric) - Для суммарных запросов по расходам: GROUP BY и SUM(summadokumenta::numeric) - Суммы (summadolga, summa, summadokumenta) хранятся как text — для математических операций приводи к numeric: ::numeric - Даты в БД хранятся в формате timestamp Примеры правильных запросов: - Топ 5 должников: SELECT kontragent, SUM(summadolga::numeric) as total FROM vw_debt_active WHERE period = (SELECT MAX(period) FROM vw_debt_active) GROUP BY kontragent ORDER BY total DESC LIMIT 5 - Общая дебиторка: SELECT SUM(summadolga::numeric) as total FROM vw_debt_active WHERE period = (SELECT MAX(period) FROM vw_debt_active) - Просроченная дебиторка: SELECT SUM(summaprosrocheno::numeric) as total FROM vw_debt_active WHERE period = (SELECT MAX(period) FROM vw_debt_active) - Задолженность по классам просрочки: SELECT klassprosrochki, SUM(summadolga::numeric) as total FROM vw_debt_active WHERE period = (SELECT MAX(period) FROM vw_debt_active) GROUP BY klassprosrochki ORDER BY total DESC - Доходы по контрагентам: SELECT zakazchik, SUM(summa::numeric) as total FROM opdohodi GROUP BY zakazchik ORDER BY total DESC LIMIT 10 - Расходы за период: SELECT SUM(summadokumenta::numeric) as total FROM oprashodi WHERE data >= '2025-01-01' AND data < '2025-04-01' Правила: - Генерируй только SELECT запросы, никогда INSERT/UPDATE/DELETE - ВСЕГДА используй GROUP BY и агрегатные функции для "топ", "сколько всего", "общая сумма" и подобных запросов - Для vw_debt_active ВСЕГДА добавляй WHERE period = (SELECT MAX(period) FROM vw_debt_active) если нужны актуальные данные - Суммы в рублях, форматируй с разделителем тысяч - Если не уверен в структуре данных, сначала сделай запрос для проверки - Отвечай кратко и по делу`; const DEFAULT_CONFIG: AiConfig = { apiUrl: "https://openrouter.ai/api/v1", apiKey: "", model: "deepseek/deepseek-chat", systemPrompt: DEFAULT_SYSTEM_PROMPT, enabled: false, name: "Ассистент", }; export function loadAiConfig(): AiConfig { try { if (fs.existsSync(AI_CONFIG_PATH)) { const raw = JSON.parse(fs.readFileSync(AI_CONFIG_PATH, "utf-8")); return { ...DEFAULT_CONFIG, ...raw }; } } catch {} return { ...DEFAULT_CONFIG }; } export function saveAiConfig(cfg: AiConfig): void { fs.writeFileSync(AI_CONFIG_PATH, JSON.stringify(cfg, null, 2)); } const AI_PLANNING_CONFIG_PATH = path.join("data", "ai-planning-config.json"); const DEFAULT_PLANNING_PROMPT = `Ты — финансовый аналитик компании ОмегаПром. Проанализируй данные и составь раздельный прогноз доходов и расходов на следующие 12 месяцев. ВАЖНО: результат должен быть РАЗДЕЛЬНЫЙ — доходы и расходы анализируются по разным принципам. ## ДОХОДЫ (type: "income") Анализируй ТОЛЬКО по контрагентам и договорам: - Для каждого контрагента+договора определи регулярность поступлений - Учитывай дебиторскую задолженность: если контрагент имеет большой долг или просрочку — это риск неплатежа, снижай confidence - Если есть ожидаемые поступления по актам (разница между выставленным и оплаченным) — отметь в reasoning - В поле "name" укажи контрагента, в "contract" — договор ## РАСХОДЫ (type: "expense") Если даны расходы по категориям — анализируй по ним. Если по контрагентам — анализируй по контрагентам. - Определи гарантированные повторяющиеся расходы (аренда, зарплата, связь, коммуналка и т.д.) - В поле "name" укажи категорию или контрагента, в "contract" — уточнение если есть ## Поле recurring Для каждой позиции укажи "recurring": true если операция повторяется регулярно (ежемесячно или через месяц), false если нерегулярная но устойчивая. Верни ТОЛЬКО JSON-массив без markdown-обёртки, без \`\`\`json, просто чистый JSON: [ { "type": "income" или "expense", "name": "название контрагента или категории", "contract": "договор или уточнение", "monthlyAmount": среднемесячная_сумма_число, "confidence": "high" или "medium" или "low", "recurring": true или false, "reasoning": "краткое пояснение на русском почему включил и какой тренд" } ] Правила: - Включай только операции с confidence medium или high - Для high: операция повторяется минимум 3 месяца подряд с похожими суммами - Для medium: операция повторяется 2+ месяца или имеет нерегулярный но устойчивый паттерн - monthlyAmount — прогнозируемая среднемесячная сумма (число без форматирования) - Не включай разовые операции - Сначала все доходы (отсортированные по сумме убывания), потом все расходы (отсортированные по сумме убывания) - Верни максимум 50 позиций - Ответ — ТОЛЬКО JSON массив, никакого текста до или после`; const DEFAULT_PLANNING_CONFIG: AiPlanningConfig = { apiUrl: "https://openrouter.ai/api/v1", apiKey: "", model: "deepseek/deepseek-chat", systemPrompt: DEFAULT_PLANNING_PROMPT, name: "Аналитик", enabled: true, }; export function loadAiPlanningConfig(): AiPlanningConfig { try { if (fs.existsSync(AI_PLANNING_CONFIG_PATH)) { const raw = JSON.parse(fs.readFileSync(AI_PLANNING_CONFIG_PATH, "utf-8")); return { ...DEFAULT_PLANNING_CONFIG, ...raw }; } } catch {} return { ...DEFAULT_PLANNING_CONFIG }; } export function saveAiPlanningConfig(cfg: AiPlanningConfig): void { fs.writeFileSync(AI_PLANNING_CONFIG_PATH, JSON.stringify(cfg, null, 2)); }