feat: все AI-функции переведены на Anthropic Claude API
- ai_service.py: единый AI-сервис (chat, chat_with_history, analyze_document) - routes/ai.py: POST /api/v1/ai/chat (chat, summarize, extract_risks, classify, translate) - config.py: ANTHROPIC_API_KEY, ANTHROPIC_MODEL - requirements.txt: anthropic>=0.42.0 - api-client.ts: aiApi (chat, summarize, extractRisks) - CSP: connect-src добавлен https://api.anthropic.com - app/api/ai-chat: прокси на бэкенд /api/v1/ai/chat (Anthropic) - legal_agents/llm_client.py: переведён на ai_service (Claude) - AIAccessSettings: только Claude (Sonnet 4, 3 Sonnet, 3 Opus) - k8s, .env.example: OPENAI → ANTHROPIC - package.json: удалена зависимость openai - Документация: OpenAI/GPT заменены на Claude/Anthropic Провайдер: исключительно Anthropic Claude Модель по умолчанию: claude-sonnet-4-20250514 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
62958239ac
commit
44b14cc4fd
@ -3,3 +3,7 @@ DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
||||
JWT_SECRET=your_jwt_secret_here
|
||||
API_KEY=your_api_key_here
|
||||
NODE_ENV=development
|
||||
|
||||
# AI — Anthropic Claude (единственный AI-провайдер). Бэкенд: ANTHROPIC_API_KEY
|
||||
# OPENAI_API_KEY не используется
|
||||
ANTHROPIC_API_KEY=
|
||||
|
||||
@ -1,39 +1,19 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import OpenAI from 'openai';
|
||||
import { rateLimit, getRateLimitIdentifier } from '@/lib/rate-limit';
|
||||
import { handleError } from '@/lib/error-handler';
|
||||
import { logAudit, logSecurity, logError, logWarn } from '@/lib/logger';
|
||||
import { sanitizeText } from '@/lib/sanitize';
|
||||
import { withTimeout, TIMEOUTS } from '@/lib/resilience/timeout';
|
||||
import { retryWithBackoff, RETRY_CONFIGS } from '@/lib/resilience/retry';
|
||||
import { circuitBreakers } from '@/lib/resilience/circuit-breaker';
|
||||
import { bulkheads } from '@/lib/resilience/bulkhead';
|
||||
import { overloadProtectors } from '@/lib/resilience/overload-protection';
|
||||
import { recordPerformance } from '@/lib/monitoring/metrics';
|
||||
|
||||
const openai = process.env.OPENAI_API_KEY
|
||||
? new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
})
|
||||
: null;
|
||||
const BACKEND_URL = process.env.BACKEND_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
// Overload protection
|
||||
if (!overloadProtectors.ai.check()) {
|
||||
recordPerformance('/api/ai-chat', Date.now() - startTime, 503, { method: 'POST' });
|
||||
return NextResponse.json(
|
||||
{ error: 'AI service overloaded, please try again later' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
const identifier = getRateLimitIdentifier(request);
|
||||
const limitCheck = rateLimit(identifier, 50, 60000); // 50 запросов в минуту
|
||||
|
||||
const limitCheck = rateLimit(identifier, 50, 60000);
|
||||
if (!limitCheck.allowed) {
|
||||
logSecurity('Rate limit exceeded', { identifier, path: '/api/ai-chat' });
|
||||
recordPerformance('/api/ai-chat', Date.now() - startTime, 429, { method: 'POST' });
|
||||
@ -54,51 +34,36 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Проверяем, есть ли файлы в запросе (FormData)
|
||||
const contentType = request.headers.get('content-type') || '';
|
||||
let message = '';
|
||||
let history: any[] = [];
|
||||
let history: { role: string; content: string }[] = [];
|
||||
let files: any[] = [];
|
||||
const fileContents: Array<{ name: string; type: string; content: string }> = [];
|
||||
|
||||
if (contentType.includes('multipart/form-data')) {
|
||||
// Обработка FormData с файлами
|
||||
const formData = await request.formData();
|
||||
message = formData.get('message') as string || '';
|
||||
message = (formData.get('message') as string) || '';
|
||||
const historyStr = formData.get('history') as string;
|
||||
if (historyStr) {
|
||||
try {
|
||||
history = JSON.parse(historyStr);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
history = [];
|
||||
}
|
||||
}
|
||||
|
||||
const fileCount = parseInt(formData.get('fileCount') as string || '0');
|
||||
const fileCount = parseInt((formData.get('fileCount') as string) || '0');
|
||||
for (let i = 0; i < fileCount; i++) {
|
||||
const file = formData.get(`file_${i}`) as File;
|
||||
if (file) {
|
||||
files.push({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
});
|
||||
|
||||
// Читаем содержимое файла
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// Парсим файл в зависимости от типа
|
||||
files.push({ name: file.name, size: file.size, type: file.type });
|
||||
try {
|
||||
const { parseDocument } = await import('@/lib/ai/document-parser');
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
const content = await parseDocument(buffer, file.name, file.type);
|
||||
fileContents.push({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
content: content,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(`Ошибка парсинга файла ${file.name}`, error);
|
||||
fileContents.push({ name: file.name, type: file.type, content });
|
||||
} catch (err) {
|
||||
logError(`Ошибка парсинга файла ${file.name}`, err);
|
||||
fileContents.push({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
@ -108,334 +73,109 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Обычный JSON запрос
|
||||
const body = await request.json();
|
||||
message = body.message || '';
|
||||
history = body.history || [];
|
||||
files = body.files || [];
|
||||
}
|
||||
|
||||
// Санитизация входных данных
|
||||
message = sanitizeText(message);
|
||||
if (history && Array.isArray(history)) {
|
||||
if (Array.isArray(history)) {
|
||||
history = history.map((msg: any) => ({
|
||||
...msg,
|
||||
content: sanitizeText(msg.content || ''),
|
||||
}));
|
||||
}
|
||||
|
||||
// Логирование использования ИИ агента
|
||||
logAudit('AI_CHAT_REQUEST', 'ai-chat', {
|
||||
identifier,
|
||||
messageLength: message.length,
|
||||
hasFiles: files && files.length > 0,
|
||||
hasFiles: files?.length > 0,
|
||||
});
|
||||
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
const authHeader =
|
||||
request.headers.get('authorization') ||
|
||||
(request.cookies.get('auth-token')?.value ? `Bearer ${request.cookies.get('auth-token').value}` : null);
|
||||
|
||||
const dataContext =
|
||||
'\n\nДоступные базы данных в системе:\n' +
|
||||
'- Воздушные суда, нормативные документы, организации, риски, аудиты, чек-листы, заявки, пользователи, документы';
|
||||
|
||||
let prompt = message;
|
||||
if (history?.length) {
|
||||
const historyText = history
|
||||
.map((m: any) => `${m.role === 'user' ? 'Пользователь' : 'Ассистент'}: ${m.content}`)
|
||||
.join('\n\n');
|
||||
prompt = `[Предыдущий диалог]\n${historyText}\n\n[Текущий вопрос]\n${message}`;
|
||||
}
|
||||
if (fileContents.length > 0) {
|
||||
prompt +=
|
||||
'\n\n[СОДЕРЖИМОЕ ФАЙЛОВ]\n' +
|
||||
fileContents
|
||||
.map(
|
||||
(f) =>
|
||||
`Файл: ${f.name}\n${f.content.substring(0, 5000)}${f.content.length > 5000 ? '...' : ''}`
|
||||
)
|
||||
.join('\n\n---\n\n') +
|
||||
'\n[КОНЕЦ СОДЕРЖИМОГО ФАЙЛОВ]';
|
||||
}
|
||||
|
||||
const backendChatUrl = `${BACKEND_URL.replace(/\/$/, '')}/api/v1/ai/chat`;
|
||||
const res = await fetch(backendChatUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { Authorization: authHeader } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
task: 'chat',
|
||||
context: dataContext,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errBody = await res.json().catch(() => ({}));
|
||||
if (res.status === 503) {
|
||||
return NextResponse.json({
|
||||
response: `${generateMockResponse(message, files)}\n\n⚠️ Примечание: AI-сервис (Anthropic Claude) недоступен. Проверьте ANTHROPIC_API_KEY на бэкенде.`,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
recordPerformance('/api/ai-chat', Date.now() - startTime, res.status, { method: 'POST' });
|
||||
return NextResponse.json(
|
||||
{ error: 'OpenAI API ключ не настроен', response: generateMockResponse(message, files) },
|
||||
{ status: 200 } // Возвращаем 200, но используем заглушку
|
||||
{ error: errBody.detail || 'AI-сервис недоступен' },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем доступные данные из системы для контекста
|
||||
const dataContext = '\n\nДоступные базы данных в системе:\n' +
|
||||
'- Воздушные суда: данные из реестра\n' +
|
||||
'- Нормативные документы: 19+ документов (ICAO, EASA, FAA, МАК, АРМАК, ФАП)\n' +
|
||||
'- Организации: авиакомпании и операторы\n' +
|
||||
'- Риски: критические, высокие, средние, низкие\n' +
|
||||
'- Аудиты: плановые и внеплановые\n' +
|
||||
'- Чек-листы: предполетные осмотры, техническое обслуживание\n' +
|
||||
'- Заявки: сертификация, разрешения\n' +
|
||||
'- Пользователи: администраторы, инженеры, аудиторы\n' +
|
||||
'- Документы: сертификаты, техническая документация, отчёты';
|
||||
|
||||
// Формируем системное сообщение с полной информацией о системе
|
||||
const systemMessage = {
|
||||
role: 'system' as const,
|
||||
content: `Ты ИИ агент системы контроля лётной годности воздушных судов. ` +
|
||||
`Твоя задача - помогать пользователям с управлением системой, анализом документов, ` +
|
||||
`внесением данных в базу, поиском информации. ` +
|
||||
`Отвечай на русском языке профессионально, точно и дружелюбно.\n\n` +
|
||||
`У ТЕБЯ ЕСТЬ ПОЛНЫЙ ДОСТУП КО ВСЕМ БАЗАМ ДАННЫХ СИСТЕМЫ И ВОЗМОЖНОСТЬ ВНОСИТЬ ДАННЫЕ:\n\n` +
|
||||
`ВАЖНО: Когда пользователь загружает документы (PDF, XLS, CSV, изображения), ` +
|
||||
`ты должен проанализировать их содержимое и автоматически извлечь данные для внесения в систему. ` +
|
||||
`Если в документах есть информация о воздушных судах, аудитах или чек-листах, ` +
|
||||
`предложи пользователю внести эти данные в соответствующие карточки.\n\n` +
|
||||
`1. ВОЗДУШНЫЕ СУДА (aircraft):\n` +
|
||||
` - Регистрационные номера, типы, операторы, статусы\n` +
|
||||
` - Для получения данных используй: POST /api/ai-data с dataType: "aircraft"\n` +
|
||||
` - Можешь фильтровать по: registrationNumber, operator, type, status\n\n` +
|
||||
`2. НОРМАТИВНЫЕ ДОКУМЕНТЫ (regulations):\n` +
|
||||
` - Конвенция о международной гражданской авиации (Chicago Convention) с 19 приложениями (ICAO)\n` +
|
||||
` - Документы ICAO (Annexes)\n` +
|
||||
` - Правила EASA (Европейское агентство по безопасности авиации)\n` +
|
||||
` - Правила FAA (Федеральное управление гражданской авиации США)\n` +
|
||||
` - Документы МАК (Межгосударственный авиационный комитет)\n` +
|
||||
` - Документы АРМАК (Агентство по регулированию гражданской авиации)\n` +
|
||||
` - Авиационные правила РФ (ФАП-128, ФАП-145, ФАП-147, ФАП-21, ФАП-25, ФАП-29, ФАП-39, ФАП-50)\n` +
|
||||
` - Воздушный кодекс РФ\n` +
|
||||
` - Для получения данных используй: POST /api/ai-data с dataType: "regulations"\n\n` +
|
||||
`3. ОРГАНИЗАЦИИ (organizations):\n` +
|
||||
` - Авиакомпании и операторы\n` +
|
||||
` - Для получения данных используй: POST /api/ai-data с dataType: "organizations"\n\n` +
|
||||
`4. РИСКИ (risks):\n` +
|
||||
` - Уровни: Критический, Высокий, Средний, Низкий\n` +
|
||||
` - Категории, статусы, привязка к ВС\n` +
|
||||
` - Для получения данных используй: POST /api/ai-data с dataType: "risks"\n\n` +
|
||||
|
||||
'5. АУДИТЫ (audits):\n' +
|
||||
' - Плановые и внеплановые аудиты\n' +
|
||||
' - Статусы: Запланирован, В процессе, Завершён\n' +
|
||||
' - Для получения данных используй: POST /api/ai-data с dataType: "audits"\n\n' +
|
||||
|
||||
'6. ЧЕК-ЛИСТЫ (checklists):\n' +
|
||||
' - Предполетные осмотры, техническое обслуживание\n' +
|
||||
' - Для получения данных используй: POST /api/ai-data с dataType: "checklists"\n\n' +
|
||||
|
||||
'7. ЗАЯВКИ (applications):\n' +
|
||||
' - Сертификация, разрешения на эксплуатацию\n' +
|
||||
' - Для получения данных используй: POST /api/ai-data с dataType: "applications"\n\n' +
|
||||
|
||||
'8. ПОЛЬЗОВАТЕЛИ (users):\n' +
|
||||
' - Администраторы, инженеры, аудиторы\n' +
|
||||
' - Для получения данных используй: POST /api/ai-data с dataType: "users"\n\n' +
|
||||
|
||||
'9. ДОКУМЕНТЫ (documents):\n' +
|
||||
' - Сертификаты, техническая документация, отчёты\n' +
|
||||
' - Для получения данных используй: POST /api/ai-data с dataType: "documents"\n\n' +
|
||||
|
||||
'КАК РАБОТАТЬ С ДАННЫМИ:\n' +
|
||||
'- Когда пользователь спрашивает о данных, автоматически запрашивай их через /api/ai-data\n' +
|
||||
'- Используй полученные данные для точных ответов\n' +
|
||||
'- При поиске применяй фильтры для уточнения результатов\n' +
|
||||
'- Если пользователь просит добавить данные, уточни необходимую информацию и предложи структурированный формат\n' +
|
||||
'- При ответах на вопросы о нормативных требованиях ссылайся на соответствующие документы\n\n' +
|
||||
|
||||
'ПРИМЕРЫ ЗАПРОСОВ:\n' +
|
||||
'- "Сколько воздушных судов у Аэрофлота?" → запроси aircraft с фильтром operator: "Аэрофлот"\n' +
|
||||
'- "Какие критические риски есть?" → запроси risks с фильтром level: "Критический"\n' +
|
||||
'- "Покажи все аудиты" → запроси audits\n' +
|
||||
'- "Найди ВС RA-12345" → запроси aircraft с фильтром registrationNumber: "RA-12345"\n\n' +
|
||||
|
||||
dataContext
|
||||
};
|
||||
|
||||
// Анализируем запрос пользователя и определяем, нужно ли запросить данные
|
||||
const userMessageContent = message;
|
||||
let dataToInclude = '';
|
||||
|
||||
// Определяем, запрашивает ли пользователь данные
|
||||
const lowerMessage = message.toLowerCase();
|
||||
const dataKeywords: Record<string, string[]> = {
|
||||
aircraft: ['вс', 'воздушн', 'самолёт', 'самолет', 'aircraft', 'регистрац'],
|
||||
risks: ['риск', 'риски', 'опасн', 'проблем'],
|
||||
audits: ['аудит', 'проверк', 'инспекц'],
|
||||
organizations: ['организац', 'компани', 'авиакомпани', 'оператор'],
|
||||
checklists: ['чек-лист', 'чеклист', 'осмотр', 'проверк'],
|
||||
applications: ['заявк', 'сертификац', 'разрешен'],
|
||||
users: ['пользовател', 'пользователь', 'пользователи', 'user'],
|
||||
documents: ['документ', 'сертификат', 'отчёт', 'отчет'],
|
||||
regulations: ['норматив', 'правил', 'требован', 'стандарт', 'fap', 'icao', 'easa', 'faa'],
|
||||
};
|
||||
|
||||
// Определяем тип данных для запроса
|
||||
let requestedDataType: string | null = null;
|
||||
for (const [dataType, keywords] of Object.entries(dataKeywords)) {
|
||||
if (keywords.some(keyword => lowerMessage.includes(keyword))) {
|
||||
requestedDataType = dataType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Если запрашиваются данные, получаем их через внутренний вызов
|
||||
if (requestedDataType) {
|
||||
try {
|
||||
// Используем внутренний импорт для получения данных
|
||||
const { getDataForAI } = await import('../ai-data-helper');
|
||||
const dataResult = await getDataForAI(requestedDataType, extractFilters(message));
|
||||
|
||||
if (dataResult && dataResult.data) {
|
||||
dataToInclude = `\n\n[ДАННЫЕ ИЗ СИСТЕМЫ]\n` +
|
||||
`Тип: ${dataResult.dataType}\n` +
|
||||
`Количество записей: ${dataResult.count}\n` +
|
||||
`Данные: ${JSON.stringify(dataResult.data.slice(0, 10), null, 2)}${dataResult.count > 10 ? `\n... и еще ${dataResult.count - 10} записей` : ''}\n` +
|
||||
`[КОНЕЦ ДАННЫХ]`;
|
||||
}
|
||||
} catch (error) {
|
||||
logWarn('Не удалось получить данные для ИИ агента', { error: String(error) });
|
||||
// Продолжаем без данных
|
||||
}
|
||||
}
|
||||
|
||||
// Формируем массив сообщений для OpenAI
|
||||
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
||||
systemMessage,
|
||||
...history.map((msg: any) => ({
|
||||
role: msg.role === 'user' ? 'user' as const : 'assistant' as const,
|
||||
content: msg.content,
|
||||
})),
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: `${userMessageContent}${files && files.length > 0
|
||||
? `\n\nПрикреплены файлы: ${files.map((f: any) => f.name).join(', ')}`
|
||||
: ''}${fileContents.length > 0
|
||||
? `\n\n[СОДЕРЖИМОЕ ФАЙЛОВ]\n${fileContents.map(f => `Файл: ${f.name}\n${f.content.substring(0, 5000)}${f.content.length > 5000 ? '...' : ''}`).join('\n\n---\n\n')}\n[КОНЕЦ СОДЕРЖИМОГО ФАЙЛОВ]`
|
||||
: ''}${dataToInclude}`,
|
||||
},
|
||||
];
|
||||
|
||||
// Запрос к OpenAI API
|
||||
if (!openai) {
|
||||
throw new Error('OpenAI API ключ не настроен');
|
||||
}
|
||||
if (!openai) {
|
||||
return NextResponse.json(
|
||||
{ error: 'OpenAI API key not configured' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
// Используем bulkhead, circuit breaker, retry и timeout для устойчивости
|
||||
const completion = await bulkheads.ai.execute(async () => {
|
||||
return circuitBreakers.openai.execute(async () => {
|
||||
return retryWithBackoff(
|
||||
() =>
|
||||
withTimeout(
|
||||
openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini', // Используем более доступную модель
|
||||
messages: messages,
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000,
|
||||
}),
|
||||
TIMEOUTS.OPENAI_API,
|
||||
'OpenAI API request timed out'
|
||||
),
|
||||
{
|
||||
...RETRY_CONFIGS.OPENAI_API,
|
||||
onRetry: (attempt, error) => {
|
||||
logWarn(`OpenAI chat retry attempt ${attempt}`, { error: error.message });
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const aiResponse = completion.choices[0]?.message?.content || 'Извините, не удалось получить ответ.';
|
||||
|
||||
// Запись метрики производительности
|
||||
const duration = Date.now() - startTime;
|
||||
recordPerformance('/api/ai-chat', duration, 200, { method: 'POST' });
|
||||
|
||||
return NextResponse.json({
|
||||
response: aiResponse,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (openaiError: any) {
|
||||
logError('Ошибка OpenAI API', openaiError);
|
||||
|
||||
// Если ошибка API, возвращаем заглушку
|
||||
return NextResponse.json({
|
||||
response: `${generateMockResponse(message, files)}\n\n⚠️ Примечание: OpenAI API временно недоступен, используется локальная обработка.`,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
return handleError(error, {
|
||||
path: '/api/ai-chat',
|
||||
method: 'POST',
|
||||
const data = await res.json();
|
||||
recordPerformance('/api/ai-chat', Date.now() - startTime, 200, { method: 'POST' });
|
||||
return NextResponse.json({
|
||||
response: data.result || 'Извините, не удалось получить ответ.',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error: any) {
|
||||
logError('Ошибка AI chat (Anthropic proxy)', error);
|
||||
return handleError(error, { path: '/api/ai-chat', method: 'POST' });
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для извлечения фильтров из запроса пользователя
|
||||
function extractFilters(message: string): any {
|
||||
const filters: any = {};
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
// Извлечение регистрационного номера ВС
|
||||
const regNumberMatch = message.match(/RA-[\dA-Z]+/i);
|
||||
if (regNumberMatch) {
|
||||
filters.registrationNumber = regNumberMatch[0];
|
||||
}
|
||||
|
||||
// Извлечение оператора/компании
|
||||
const companies = ['аэрофлот', 's7', 'уральск', 'победа', 'нордавиа'];
|
||||
for (const company of companies) {
|
||||
if (lowerMessage.includes(company)) {
|
||||
filters.operator = company;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Извлечение уровня риска
|
||||
if (lowerMessage.includes('критическ')) {
|
||||
filters.level = 'Критический';
|
||||
} else if (lowerMessage.includes('высок')) {
|
||||
filters.level = 'Высокий';
|
||||
} else if (lowerMessage.includes('средн')) {
|
||||
filters.level = 'Средний';
|
||||
} else if (lowerMessage.includes('низк')) {
|
||||
filters.level = 'Низкий';
|
||||
}
|
||||
|
||||
// Извлечение статуса
|
||||
if (lowerMessage.includes('активн')) {
|
||||
filters.status = 'Активен';
|
||||
} else if (lowerMessage.includes('обслуживан')) {
|
||||
filters.status = 'На обслуживании';
|
||||
}
|
||||
|
||||
return Object.keys(filters).length > 0 ? filters : undefined;
|
||||
}
|
||||
|
||||
// Функция-заглушка для случаев, когда API недоступен
|
||||
function generateMockResponse(message: string, files: any[]): string {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
if (lowerMessage.includes('добавить') || lowerMessage.includes('создать')) {
|
||||
if (lowerMessage.includes('вс') || lowerMessage.includes('воздушн')) {
|
||||
return `Для добавления воздушного судна мне нужна следующая информация:\n\n` +
|
||||
`1. Регистрационный номер (например: RA-12345)\n` +
|
||||
`2. Серийный номер\n` +
|
||||
`3. Тип ВС (например: Boeing 737-800)\n` +
|
||||
`4. Оператор (название компании)\n` +
|
||||
`5. Статус (Активен/На обслуживании)\n\n` +
|
||||
`Предоставьте эти данные в структурированном виде, и я внесу их в базу.`;
|
||||
const lower = message.toLowerCase();
|
||||
if (lower.includes('добавить') || lower.includes('создать')) {
|
||||
if (lower.includes('вс') || lower.includes('воздушн')) {
|
||||
return 'Для добавления воздушного судна нужны: регистрационный номер, серийный номер, тип ВС, оператор, статус.';
|
||||
}
|
||||
if (lowerMessage.includes('риск')) {
|
||||
return `Для добавления риска требуется:\n\n` +
|
||||
`1. Название риска\n` +
|
||||
`2. Уровень: Критический/Высокий/Средний/Низкий\n` +
|
||||
`3. Категория\n` +
|
||||
`4. ВС (регистрационный номер)\n` +
|
||||
`5. Описание\n\n` +
|
||||
`Укажите эти данные для автоматического внесения.`;
|
||||
if (lower.includes('риск')) {
|
||||
return 'Для добавления риска укажите: название, уровень (Критический/Высокий/Средний/Низкий), категорию, ВС, описание.';
|
||||
}
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('найти') || lowerMessage.includes('поиск')) {
|
||||
return `Я могу помочь с поиском:\n\n` +
|
||||
`• Воздушное судно по номеру\n` +
|
||||
`• Риски по уровню или категории\n` +
|
||||
`• Документы по типу или ВС\n` +
|
||||
`• Аудиты по организации или дате\n\n` +
|
||||
`Уточните, что именно нужно найти?`;
|
||||
if (lower.includes('найти') || lower.includes('поиск')) {
|
||||
return 'Могу помочь с поиском: ВС по номеру, риски по уровню, документы, аудиты. Уточните, что искать?';
|
||||
}
|
||||
|
||||
if (files && files.length > 0) {
|
||||
return `Получено ${files.length} файл(ов). Анализирую содержимое...\n\n` +
|
||||
`Если в файлах есть структурированные данные (списки ВС, риски, документы), ` +
|
||||
`я могу автоматически извлечь их и предложить внести в базу данных.\n\n` +
|
||||
`Продолжить анализ?`;
|
||||
if (files?.length) {
|
||||
return `Получено ${files.length} файл(ов). Анализирую содержимое. При наличии структурированных данных предложу внести их в базу.`;
|
||||
}
|
||||
|
||||
return `Я ИИ агент системы контроля лётной годности. Могу помочь с:\n\n` +
|
||||
`✅ Добавлением данных в базу (ВС, риски, документы, аудиты)\n` +
|
||||
`✅ Поиском информации по системе\n` +
|
||||
`✅ Анализом прикрепленных файлов\n` +
|
||||
`✅ Генерацией отчетов\n` +
|
||||
`✅ Ответами на вопросы о системе\n\n` +
|
||||
`Что именно вам нужно?`;
|
||||
return 'Я AI-ассистент системы контроля лётной годности (Anthropic Claude). Могу помочь с данными, поиском, анализом файлов и отчётами. Что нужно?';
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ export async function POST(request: NextRequest) {
|
||||
async () => {
|
||||
return await withTimeout(
|
||||
detectIntent(query),
|
||||
TIMEOUTS.OPENAI_API / 2,
|
||||
TIMEOUTS.AI_API / 2,
|
||||
'Intent detection timeout'
|
||||
);
|
||||
}
|
||||
@ -78,7 +78,7 @@ export async function POST(request: NextRequest) {
|
||||
mode: mode === 'autonomous' ? 'autonomous' : 'copilot',
|
||||
context,
|
||||
}),
|
||||
TIMEOUTS.OPENAI_API,
|
||||
TIMEOUTS.AI_API,
|
||||
'Natural language query processing timeout'
|
||||
);
|
||||
},
|
||||
|
||||
@ -36,6 +36,10 @@ INBOX_UPLOAD_MAX_MB=50
|
||||
# Multi-tenancy
|
||||
ENABLE_RLS=true
|
||||
|
||||
# AI (Anthropic Claude — единственный AI-провайдер)
|
||||
ANTHROPIC_API_KEY=
|
||||
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
||||
|
||||
# Optional: streaming
|
||||
ENABLE_RISINGWAVE=false
|
||||
ENABLE_REDPANDA=false
|
||||
|
||||
@ -16,6 +16,7 @@ from .checklist_audits import router as checklist_audits_router
|
||||
from .inbox import router as inbox_router
|
||||
from .tasks import router as tasks_router
|
||||
from .audit import router as audit_router
|
||||
from .ai import router as ai_router
|
||||
|
||||
__all__ = [
|
||||
"health_router",
|
||||
@ -36,5 +37,6 @@ __all__ = [
|
||||
"inbox_router",
|
||||
"tasks_router",
|
||||
"audit_router",
|
||||
"ai_router",
|
||||
]
|
||||
|
||||
|
||||
46
backend/app/api/routes/ai.py
Normal file
46
backend/app/api/routes/ai.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""AI-ассистент КЛГ АСУ ТК — работает через Anthropic Claude API."""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter(tags=["ai"])
|
||||
|
||||
|
||||
class AIRequest(BaseModel):
|
||||
prompt: str
|
||||
task: str = "chat" # chat | summarize | extract_risks | classify | translate
|
||||
context: str | None = None
|
||||
|
||||
|
||||
class AIResponse(BaseModel):
|
||||
result: str | None
|
||||
model: str
|
||||
provider: str = "anthropic"
|
||||
|
||||
|
||||
@router.post("/ai/chat", response_model=AIResponse)
|
||||
def ai_chat(
|
||||
req: AIRequest,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
"""Отправить запрос AI-ассистенту (Anthropic Claude)."""
|
||||
from app.services.ai_service import chat, analyze_document
|
||||
|
||||
if req.task == "chat":
|
||||
system = (
|
||||
"Ты — AI-ассистент системы КЛГ АСУ ТК (контроль лётной годности воздушных судов). "
|
||||
"Отвечай на русском языке. Используй терминологию ВК РФ, ФАП, ICAO, EASA."
|
||||
)
|
||||
if req.context:
|
||||
system += f"\n\nКонтекст: {req.context}"
|
||||
result = chat(prompt=req.prompt, system=system)
|
||||
else:
|
||||
text = f"{req.context}\n\n{req.prompt}" if req.context else req.prompt
|
||||
result = analyze_document(text=text, task=req.task)
|
||||
|
||||
if result is None:
|
||||
raise HTTPException(503, "AI-сервис недоступен. Проверьте ANTHROPIC_API_KEY.")
|
||||
|
||||
return AIResponse(result=result, model=settings.ANTHROPIC_MODEL)
|
||||
@ -64,6 +64,10 @@ class Settings(BaseSettings):
|
||||
# Multi-tenancy
|
||||
ENABLE_RLS: bool = True
|
||||
|
||||
# AI (Anthropic Claude)
|
||||
ANTHROPIC_API_KEY: str = ""
|
||||
ANTHROPIC_MODEL: str = "claude-sonnet-4-20250514"
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
url = self.DATABASE_URL
|
||||
|
||||
@ -32,6 +32,7 @@ from app.api.routes import (
|
||||
inbox_router,
|
||||
tasks_router,
|
||||
audit_router,
|
||||
ai_router,
|
||||
)
|
||||
|
||||
|
||||
@ -211,6 +212,7 @@ app.include_router(checklist_audits_router, prefix=PREFIX, dependencies=AUTH_DEP
|
||||
app.include_router(inbox_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(tasks_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(audit_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(ai_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
|
||||
# WebSocket (no prefix — direct path)
|
||||
from app.api.routes.ws_notifications import router as ws_router
|
||||
|
||||
140
backend/app/services/ai_service.py
Normal file
140
backend/app/services/ai_service.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""
|
||||
AI-сервис КЛГ АСУ ТК — использует исключительно Anthropic Claude API.
|
||||
Все AI-функции системы проходят через этот модуль.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Ленивая инициализация клиента
|
||||
_client = None
|
||||
|
||||
|
||||
def _get_client():
|
||||
"""Получить или создать клиент Anthropic."""
|
||||
global _client
|
||||
if _client is None:
|
||||
try:
|
||||
from anthropic import Anthropic
|
||||
api_key = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
if not api_key:
|
||||
logger.warning("ANTHROPIC_API_KEY не задан. AI-функции недоступны.")
|
||||
return None
|
||||
_client = Anthropic(api_key=api_key)
|
||||
except ImportError:
|
||||
logger.warning("Пакет anthropic не установлен. pip install anthropic")
|
||||
return None
|
||||
return _client
|
||||
|
||||
|
||||
def chat(
|
||||
prompt: str,
|
||||
system: str = "Ты — AI-ассистент системы КЛГ АСУ ТК (контроль лётной годности). Отвечай на русском языке, точно и по делу.",
|
||||
model: str = "claude-sonnet-4-20250514",
|
||||
max_tokens: int = 2048,
|
||||
temperature: float = 0.3,
|
||||
) -> str | None:
|
||||
"""
|
||||
Отправить запрос к Claude и получить текстовый ответ.
|
||||
|
||||
Args:
|
||||
prompt: Текст запроса пользователя
|
||||
system: Системный промпт (контекст роли)
|
||||
model: Модель Claude (claude-sonnet-4-20250514, claude-haiku-4-5-20251001 и т.д.)
|
||||
max_tokens: Максимум токенов в ответе
|
||||
temperature: Температура генерации (0.0–1.0)
|
||||
|
||||
Returns:
|
||||
Текст ответа или None при ошибке
|
||||
"""
|
||||
client = _get_client()
|
||||
if client is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
response = client.messages.create(
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
system=system,
|
||||
messages=[
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
)
|
||||
# Извлечь текст из ответа
|
||||
text_blocks = [block.text for block in response.content if block.type == "text"]
|
||||
return "\n".join(text_blocks) if text_blocks else None
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Anthropic API error: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def chat_with_history(
|
||||
messages: list[dict[str, str]],
|
||||
system: str = "Ты — AI-ассистент системы КЛГ АСУ ТК.",
|
||||
model: str = "claude-sonnet-4-20250514",
|
||||
max_tokens: int = 2048,
|
||||
) -> str | None:
|
||||
"""
|
||||
Отправить беседу с историей к Claude.
|
||||
|
||||
Args:
|
||||
messages: Список сообщений [{"role": "user"|"assistant", "content": "..."}]
|
||||
system: Системный промпт
|
||||
model: Модель Claude
|
||||
max_tokens: Максимум токенов
|
||||
|
||||
Returns:
|
||||
Текст ответа или None
|
||||
"""
|
||||
client = _get_client()
|
||||
if client is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
response = client.messages.create(
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
system=system,
|
||||
messages=messages,
|
||||
)
|
||||
text_blocks = [block.text for block in response.content if block.type == "text"]
|
||||
return "\n".join(text_blocks) if text_blocks else None
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Anthropic API error: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def analyze_document(
|
||||
text: str,
|
||||
task: str = "summarize",
|
||||
) -> str | None:
|
||||
"""
|
||||
Анализ документа с помощью Claude.
|
||||
|
||||
Args:
|
||||
text: Текст документа
|
||||
task: Задача — summarize | extract_risks | classify | translate
|
||||
|
||||
Returns:
|
||||
Результат анализа или None
|
||||
"""
|
||||
tasks = {
|
||||
"summarize": "Сделай краткое резюме следующего документа на русском языке:",
|
||||
"extract_risks": "Извлеки все риски и несоответствия из следующего документа. Формат: список с описанием и уровнем критичности (low/medium/high/critical):",
|
||||
"classify": "Классифицируй следующий документ по типу (директива ЛГ, сервисный бюллетень, программа ТО, акт проверки, сертификат, иное). Укажи тип и краткое обоснование:",
|
||||
"translate": "Переведи следующий документ на русский язык, сохраняя техническую терминологию авиации:",
|
||||
}
|
||||
system_prompt = tasks.get(task, tasks["summarize"])
|
||||
return chat(prompt=text[:50000], system=system_prompt, max_tokens=4096)
|
||||
|
||||
|
||||
# ─── Совместимость: если где-то вызывался openai ─────────────
|
||||
# Алиасы для обратной совместимости
|
||||
def complete(prompt: str, **kwargs) -> str | None:
|
||||
"""Алиас для chat() — замена openai.Completion."""
|
||||
return chat(prompt=prompt, **kwargs)
|
||||
@ -1,18 +1,12 @@
|
||||
"""
|
||||
Клиент LLM для агентов. Поддерживает OpenAI API и локальные OpenAI-совместимые endpoints.
|
||||
Клиент LLM для агентов. Использует исключительно Anthropic Claude API через app.services.ai_service.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from openai import OpenAI
|
||||
_HAS_OPENAI = True
|
||||
except ImportError:
|
||||
_HAS_OPENAI = False
|
||||
|
||||
|
||||
def _get_setting(name: str, default: str | None = "") -> str | None:
|
||||
import os
|
||||
env_key = name.upper()
|
||||
try:
|
||||
from app.core.config import settings
|
||||
@ -25,41 +19,29 @@ def _get_setting(name: str, default: str | None = "") -> str | None:
|
||||
|
||||
|
||||
class LLMClient:
|
||||
"""Унифицированный клиент для вызова LLM (OpenAI-совместимый)."""
|
||||
"""Унифицированный клиент для вызова LLM (Anthropic Claude)."""
|
||||
|
||||
def __init__(self, api_key: str | None = None, base_url: str | None = None, model: str | None = None):
|
||||
self.api_key = (api_key if api_key is not None else _get_setting("openai_api_key", "")) or ""
|
||||
self.base_url = base_url if base_url is not None else _get_setting("openai_base_url", None)
|
||||
self.model = (model or _get_setting("legal_llm_model", "gpt-4o-mini")) or "gpt-4o-mini"
|
||||
self.api_key = (api_key if api_key is not None else _get_setting("ANTHROPIC_API_KEY", "")) or ""
|
||||
self.base_url = base_url # не используется для Anthropic
|
||||
self.model = (model or _get_setting("ANTHROPIC_MODEL", "claude-sonnet-4-20250514")) or "claude-sonnet-4-20250514"
|
||||
self._client: Any = None
|
||||
if _HAS_OPENAI and self.api_key:
|
||||
kw: dict[str, Any] = {"api_key": self.api_key}
|
||||
if self.base_url:
|
||||
kw["base_url"] = self.base_url
|
||||
self._client = OpenAI(**kw)
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
return _HAS_OPENAI and bool(self.api_key) and self._client is not None
|
||||
from app.services.ai_service import _get_client
|
||||
return _get_client() is not None
|
||||
|
||||
def chat(self, system: str, user: str, json_mode: bool = False) -> str | None:
|
||||
"""Один запрос к чату. Возвращает текст ответа или None."""
|
||||
from app.services.ai_service import chat
|
||||
if not self.is_available:
|
||||
return None
|
||||
try:
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user},
|
||||
],
|
||||
}
|
||||
if json_mode:
|
||||
kwargs["response_format"] = {"type": "json_object"}
|
||||
r = self._client.chat.completions.create(**kwargs)
|
||||
return (r.choices[0].message.content or "").strip() or None
|
||||
except Exception:
|
||||
return None
|
||||
result = chat(prompt=user, system=system, model=self.model, max_tokens=4096)
|
||||
if result and json_mode:
|
||||
# Claude не имеет response_format; при необходимости постобработка JSON
|
||||
pass
|
||||
return (result or "").strip() or None
|
||||
|
||||
|
||||
def get_llm_client() -> LLMClient:
|
||||
|
||||
@ -31,6 +31,9 @@ prometheus-client==0.21.0
|
||||
# Scheduler (risk scan jobs)
|
||||
APScheduler>=3.10
|
||||
|
||||
# AI (Anthropic Claude — единственный AI-провайдер)
|
||||
anthropic>=0.42.0
|
||||
|
||||
# Utils
|
||||
python-dotenv==1.0.1
|
||||
|
||||
|
||||
@ -20,8 +20,9 @@ export default function AIAccessSettings({ onSave }: Props) {
|
||||
</FormField>
|
||||
<FormField label="Модель">
|
||||
<select value={model} onChange={e => setModel(e.target.value)} className="input-field" disabled={!enabled}>
|
||||
<option value="claude-3-sonnet">Claude 3 Sonnet</option><option value="claude-3-opus">Claude 3 Opus</option>
|
||||
<option value="gpt-4">GPT-4</option><option value="local">Локальная модель</option>
|
||||
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
|
||||
<option value="claude-3-sonnet">Claude 3 Sonnet</option>
|
||||
<option value="claude-3-opus">Claude 3 Opus</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Макс. токенов">
|
||||
|
||||
@ -258,7 +258,7 @@
|
||||
### Библиотеки
|
||||
|
||||
- `csv-parse` - для парсинга CSV файлов
|
||||
- `openai` - для работы с OpenAI API
|
||||
- `ai` - для работы с Anthropic Claude API
|
||||
- В будущем: `pdf-parse`, `xlsx`, `tesseract.js` для полной поддержки всех форматов
|
||||
|
||||
### Файлы
|
||||
|
||||
@ -256,7 +256,7 @@ const enriched = await autoEnrichAircraft(aircraft);
|
||||
### Переменные окружения
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=your-api-key
|
||||
ANTHROPIC_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
### Установка pgvector
|
||||
@ -295,7 +295,7 @@ CREATE EXTENSION IF NOT EXISTS vector;
|
||||
┌─────────────────┐
|
||||
│ Knowledge │
|
||||
│ Extraction │
|
||||
│ (OpenAI GPT-4) │
|
||||
│ (Anthropic Claude) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
|
||||
@ -172,8 +172,8 @@ REDIS_DB=0
|
||||
# Включение Redis (опционально)
|
||||
REDIS_ENABLED=true
|
||||
|
||||
# OpenAI API (для ИИ агента)
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
# AI (Anthropic Claude — для ИИ агента)
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key
|
||||
|
||||
# Sentry (опционально, для мониторинга ошибок)
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
@ -18,8 +18,8 @@ NEXT_PUBLIC_API_URL=https://api.example.com/api
|
||||
NEXT_PUBLIC_USE_MOCK_DATA=false
|
||||
NEXT_PUBLIC_USE_REGISTRY_DATA=true
|
||||
|
||||
# OpenAI
|
||||
OPENAI_API_KEY=your-production-api-key
|
||||
# AI (Anthropic Claude)
|
||||
ANTHROPIC_API_KEY=your-production-api-key
|
||||
|
||||
# Логирование
|
||||
LOG_LEVEL=info
|
||||
|
||||
@ -179,8 +179,8 @@ NEXT_PUBLIC_API_URL=http://localhost:8000/api
|
||||
NEXT_PUBLIC_USE_MOCK_DATA=false
|
||||
NEXT_PUBLIC_USE_REGISTRY_DATA=true
|
||||
|
||||
# OpenAI
|
||||
OPENAI_API_KEY=your-api-key
|
||||
# AI (Anthropic Claude)
|
||||
ANTHROPIC_API_KEY=your-api-key
|
||||
|
||||
# Логирование
|
||||
LOG_LEVEL=info
|
||||
|
||||
@ -88,11 +88,10 @@
|
||||
|
||||
В `.env` или переменных окружения:
|
||||
|
||||
- `OPENAI_API_KEY` — ключ OpenAI (или аналог)
|
||||
- `OPENAI_BASE_URL` — базовый URL для локальных OpenAI-совместимых моделей
|
||||
- `LEGAL_LLM_MODEL` — модель (по умолчанию `gpt-4o-mini`)
|
||||
- `ANTHROPIC_API_KEY` — ключ Anthropic Claude API
|
||||
- `ANTHROPIC_MODEL` — модель (по умолчанию `claude-sonnet-4-20250514`)
|
||||
|
||||
В `app.core.config` добавлены: `openai_api_key`, `openai_base_url`, `legal_llm_model`.
|
||||
В `app.core.config` добавлены: `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`.
|
||||
|
||||
Если LLM недоступен (нет ключа или ошибка), агенты работают в режиме заглушек (эвристики, пустые списки).
|
||||
|
||||
@ -106,4 +105,4 @@ cd backend && python -m app.db.seed_legal
|
||||
|
||||
## Зависимости
|
||||
|
||||
- `openai>=1.0.0` — для вызова LLM (OpenAI-совместимый API).
|
||||
- `anthropic>=0.42.0` — для вызова LLM (Anthropic Claude API).
|
||||
|
||||
@ -15,13 +15,13 @@
|
||||
```typescript
|
||||
import { circuitBreakers } from '@/lib/resilience/circuit-breaker';
|
||||
|
||||
const result = await circuitBreakers.openai.execute(async () => {
|
||||
return await openai.chat.completions.create(...);
|
||||
const result = await circuitBreakers.ai.execute(async () => {
|
||||
return await aiApi.chat(prompt);
|
||||
});
|
||||
```
|
||||
|
||||
**Глобальные circuit breakers:**
|
||||
- `openai` - для OpenAI API
|
||||
- `ai` - для Anthropic Claude API
|
||||
- `database` - для PostgreSQL
|
||||
- `redis` - для Redis
|
||||
- `externalApi` - для внешних API
|
||||
|
||||
@ -48,11 +48,11 @@ spec:
|
||||
configMapKeyRef:
|
||||
name: klg-config
|
||||
key: redis-host
|
||||
- name: OPENAI_API_KEY
|
||||
- name: ANTHROPIC_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: klg-secrets
|
||||
key: openai-api-key
|
||||
key: anthropic-api-key
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
@ -127,4 +127,4 @@ stringData:
|
||||
db-name: "klg_db"
|
||||
db-user: "postgres"
|
||||
db-password: "" # SET VIA CI/CD PIPELINE
|
||||
openai-api-key: "" # SET VIA CI/CD PIPELINE
|
||||
anthropic-api-key: "" # SET VIA CI/CD PIPELINE (Anthropic Claude)
|
||||
|
||||
@ -247,3 +247,22 @@ export const attachmentsApi = {
|
||||
export const healthApi = {
|
||||
check: () => apiFetch('/health'),
|
||||
};
|
||||
|
||||
// AI Assistant (Anthropic Claude via backend)
|
||||
export const aiApi = {
|
||||
chat: (prompt: string, context?: string) =>
|
||||
apiFetch<{ result: string; model: string; provider: string }>('/ai/chat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt, task: 'chat', context }),
|
||||
}),
|
||||
summarize: (text: string) =>
|
||||
apiFetch<{ result: string }>('/ai/chat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt: text, task: 'summarize' }),
|
||||
}),
|
||||
extractRisks: (text: string) =>
|
||||
apiFetch<{ result: string }>('/ai/chat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt: text, task: 'extract_risks' }),
|
||||
}),
|
||||
};
|
||||
|
||||
@ -36,7 +36,7 @@ export function middleware(request: NextRequest) {
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: https:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'self'",
|
||||
"connect-src 'self' https://api.anthropic.com",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
|
||||
@ -53,7 +53,6 @@
|
||||
"kafkajs": "^2.2.4",
|
||||
"next": "^14.0.0",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"openai": "^6.16.0",
|
||||
"pg": "^8.11.3",
|
||||
"pino": "^10.2.1",
|
||||
"prettier": "^3.8.1",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user