UNPKG

solver-sdk

Version:

SDK for WorkAI API - AI-powered code analysis with WorkCoins billing system

439 lines 21.1 kB
/** * Помощники для создания complex content сообщений согласно Anthropic API */ import { ThinkingBlockManager } from './thinking-block-manager'; /** * Создает сообщение assistant с thinking и tool_use блоками * @param thinkingContent Содержимое мышления * @param signature Криптографическая подпись * @param toolCall Вызов инструмента * @returns Сообщение assistant с complex content */ export function createAssistantMessageWithThinking(thinkingContent, toolCall, signature // ✅ ЦЕНТРАЛИЗОВАННОЕ: signature опциональная (последний параметр) ) { // 🏗️ ЦЕНТРАЛИЗОВАННОЕ создание thinking блока const thinkingBlock = ThinkingBlockManager.createThinkingBlock(thinkingContent, signature); const content = [ thinkingBlock, { type: 'tool_use', id: toolCall.id, name: toolCall.name, input: toolCall.input } ]; return { role: 'assistant', content: content }; } /** * Создает простое текстовое сообщение * @param role Роль отправителя * @param text Текст сообщения * @returns Простое сообщение */ export function createTextMessage(role, text) { return { role: role, content: text }; } /** * Создает сообщение assistant с текстом и опциональным мышлением * @param text Основной текст ответа * @param thinkingContent Содержимое мышления (опционально) * @param signature Подпись мышления (опционально) * @returns Сообщение assistant с complex content */ export function createAssistantMessage(text, thinkingContent, signature) { const content = []; // Добавляем thinking блок если есть if (thinkingContent) { // 🏗️ ЦЕНТРАЛИЗОВАННОЕ создание thinking блока const thinkingBlock = ThinkingBlockManager.createThinkingBlock(thinkingContent, signature); content.push(thinkingBlock); } // Добавляем текстовый блок content.push({ type: 'text', text: text }); return { role: 'assistant', content: content }; } /** * Извлекает thinking блоки из сообщения * @param message Сообщение с complex content * @returns Массив thinking блоков */ export function extractThinkingBlocks(message) { const content = Array.isArray(message) ? message : (Array.isArray(message.content) ? message.content : []); return content.filter(block => block.type === 'thinking' || block.type === 'redacted_thinking'); } /** * Извлекает tool_use блоки из сообщения * @param message Сообщение с complex content * @returns Массив tool_use блоков */ export function extractToolUseBlocks(message) { const content = Array.isArray(message) ? message : (Array.isArray(message.content) ? message.content : []); return content.filter(block => block.type === 'tool_use'); } /** * Извлекает tool_result блоки из сообщения * @param message Сообщение с complex content * @returns Массив tool_result блоков */ export function extractToolResultBlocks(message) { const content = Array.isArray(message) ? message : (Array.isArray(message.content) ? message.content : []); return content.filter(block => block.type === 'tool_result'); } /** * Извлекает текстовое содержимое из сообщения * @param message Сообщение * @returns Текстовое содержимое */ export function extractTextContent(message) { if (typeof message.content === 'string') { return message.content; } const textBlocks = message.content.filter(block => block.type === 'text'); return textBlocks.map(block => block.text).join(''); } /** * Создает определение инструмента для Anthropic API * @param name Название инструмента * @param description Описание функциональности * @param properties Свойства входных параметров * @param required Обязательные поля * @returns Определение инструмента */ export function createToolDefinition(name, description, properties, required = []) { return { name: name, description: description, input_schema: { type: 'object', properties: properties, required: required } }; } /** * Получает схемы инструментов с backend сервера * @param backendUrl URL backend сервера (по умолчанию из SDK опций) * @returns Массив инструментов от backend */ export async function createStandardDevelopmentTools(backendUrl) { const url = `${backendUrl || 'http://localhost:3000'}/api/v1/tools/schemas`; const response = await fetch(url); if (!response.ok) { throw new Error(`Backend недоступен: HTTP ${response.status} - ${response.statusText}`); } const data = await response.json(); if (!data.tools || !Array.isArray(data.tools)) { throw new Error('Неверный формат ответа от backend'); } console.log(`Загружено ${data.count} схем инструментов с backend`); return data.tools; } /** * Создает сообщение с контентом мышления * @param thinking Текст мышления * @param signature Криптографическая подпись (опциональная) * @returns Блок контента с мышлением */ export function createThinkingBlock(thinking, signature) { // 🏗️ ЦЕНТРАЛИЗОВАННОЕ создание через ThinkingBlockManager return ThinkingBlockManager.createThinkingBlock(thinking, signature); } /** * Создает user message с результатами выполнения инструментов * * ⚠️ КРИТИЧНО: Anthropic API требует, чтобы ВСЕ tool_result для одного assistant message * были отправлены ВМЕСТЕ в одном user message! * * @example * // Один результат: * messages.push(createBatchToolResultMessage([ * { toolUseId: 'toolu_123', content: 'File content...' } * ])); * * // Множество результатов (ПРАВИЛЬНО): * const toolResults = await Promise.all( * toolUses.map(async tool => ({ * toolUseId: tool.id, * content: await executeTool(tool), * })) * ); * messages.push(createBatchToolResultMessage(toolResults)); * * // С обработкой ошибок: * messages.push(createBatchToolResultMessage([ * { toolUseId: 'toolu_123', content: 'Success result' }, * { toolUseId: 'toolu_456', content: 'Error: file not found', isError: true } * ])); * * @param toolResults Массив результатов инструментов (минимум 1) * @returns User message со всеми tool_result блоками * @throws {Error} Если массив toolResults пустой */ export function createBatchToolResultMessage(toolResults) { if (toolResults.length === 0) { throw new Error('createBatchToolResultMessage: toolResults не может быть пустым'); } return { role: 'user', content: toolResults.map(result => ({ type: 'tool_result', tool_use_id: result.toolUseId, content: result.content, is_error: result.isError || false })) }; } /** * Создает блок использования инструмента * @param id ID вызова инструмента * @param name Название инструмента * @param input Параметры инструмента * @returns Блок использования инструмента */ export function createToolUseBlock(id, name, input) { return { type: 'tool_use', id: id, name: name, input: input }; } /** * Создает complex content сообщение, комбинируя разные блоки * @param thinkingBlocks Блоки мышления * @param toolUseBlocks Блоки использования инструментов * @param textBlocks Текстовые блоки (опционально) * @returns Сообщение ассистента с complex content */ export function createComplexAssistantMessage(thinkingBlocks = [], toolUseBlocks = [], textBlocks = []) { return { role: 'assistant', content: [...thinkingBlocks, ...toolUseBlocks, ...textBlocks] }; } /** * ✅ КРИТИЧНО: Сортирует блоки в assistant message согласно требованиям Anthropic API * При thinking mode: thinking блоки ВСЕГДА ПЕРВЫМИ! */ export function ensureCorrectBlockOrder(content) { if (!Array.isArray(content) || content.length === 0) { return content; } // Разделяем блоки по типам const thinkingBlocks = content.filter(b => b.type === 'thinking' || b.type === 'redacted_thinking'); const toolUseBlocks = content.filter(b => b.type === 'tool_use'); const textBlocks = content.filter(b => b.type === 'text'); const otherBlocks = content.filter(b => b.type !== 'thinking' && b.type !== 'redacted_thinking' && b.type !== 'tool_use' && b.type !== 'text'); // ✅ ПРАВИЛЬНЫЙ ПОРЯДОК согласно Anthropic API: // 1. thinking ПЕРВЫМИ (ОБЯЗАТЕЛЬНО если есть tool_use) // 2. tool_use // 3. text // 4. остальные return [ ...thinkingBlocks, ...toolUseBlocks, ...textBlocks, ...otherBlocks ]; } /** * ✅ Валидирует структуру assistant message перед отправкой * 🚨 ERROR FIRST: Только валидация, БЕЗ автоисправлений! * Кидает exception с детальной диагностикой и инструкциями по исправлению */ export function validateAndFixAssistantMessage(message) { // Проверяем что это assistant message if (message.role !== 'assistant') { return message; } // Если content - строка, оставляем как есть if (typeof message.content === 'string') { return message; } // Если content - массив, валидируем структуру if (Array.isArray(message.content)) { const blockTypes = message.content.map(b => b.type); const structure = blockTypes.join(' → '); const hasToolUse = message.content.some(b => b.type === 'tool_use'); const hasThinking = message.content.some(b => b.type === 'thinking' || b.type === 'redacted_thinking'); const hasText = message.content.some(b => b.type === 'text'); // ❌ КРИТИЧЕСКАЯ ОШИБКА #1: tool_use без thinking if (hasToolUse && !hasThinking) { console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.error('❌ [SDK VALIDATION ERROR] Anthropic API отклонит этот запрос!'); console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.error(''); console.error('📋 ПРОБЛЕМА:'); console.error(' Assistant message содержит tool_use БЕЗ thinking блока'); console.error(''); console.error('📊 ТЕКУЩАЯ СТРУКТУРА:'); console.error(` ${structure}`); console.error(''); console.error('✅ ТРЕБУЕМАЯ СТРУКТУРА:'); console.error(' thinking → tool_use → [text]'); console.error(''); console.error('🔍 ДЕТАЛИ:'); console.error(` - Блоков thinking: ${hasThinking ? 'есть' : '❌ НЕТ'}`); console.error(` - Блоков tool_use: ${message.content.filter(b => b.type === 'tool_use').length}`); console.error(` - Блоков text: ${message.content.filter(b => b.type === 'text').length}`); console.error(''); console.error('🔧 КАК ИСПРАВИТЬ В ВАШЕМ КОДЕ:'); console.error(''); console.error(' 1️⃣ Убедитесь что вы накапливаете thinking из SSE:'); console.error(''); console.error(' let thinkingBlock = { type: "thinking", thinking: "" };'); console.error(' '); console.error(' stream.on("content_block_start", (event) => {'); console.error(' if (event.content_block.type === "thinking") {'); console.error(' thinkingBlock.thinking = "";'); console.error(' }'); console.error(' });'); console.error(''); console.error(' stream.on("content_block_delta", (event) => {'); console.error(' if (event.delta.type === "thinking_delta") {'); console.error(' thinkingBlock.thinking += event.delta.thinking;'); console.error(' }'); console.error(' });'); console.error(''); console.error(' 2️⃣ При формировании assistant message добавьте thinking ПЕРВЫМ:'); console.error(''); console.error(' const message = {'); console.error(' role: "assistant",'); console.error(' content: ['); console.error(' thinkingBlock, // ← thinking ПЕРВЫМ!'); console.error(' toolUseBlock'); console.error(' ]'); console.error(' };'); console.error(''); console.error('📚 Документация Anthropic:'); console.error(' https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking'); console.error(''); console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); throw new Error(`[SDK VALIDATION] Assistant message с tool_use ОБЯЗАН содержать thinking блок! Структура: ${structure}`); } // ❌ КРИТИЧЕСКАЯ ОШИБКА #2: Неправильный порядок блоков const firstBlock = message.content[0]; if (hasThinking && firstBlock.type !== 'thinking' && firstBlock.type !== 'redacted_thinking') { console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.error('❌ [SDK VALIDATION ERROR] Anthropic API отклонит этот запрос!'); console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.error(''); console.error('📋 ПРОБЛЕМА:'); console.error(' Неправильный порядок блоков в assistant message'); console.error(''); console.error('📊 ТЕКУЩАЯ СТРУКТУРА:'); console.error(` ${structure}`); console.error(''); console.error('❌ ПЕРВЫЙ БЛОК:'); console.error(` ${firstBlock.type} (НЕПРАВИЛЬНО!)`); console.error(''); console.error('✅ ОЖИДАЕТСЯ:'); console.error(' thinking (должен быть ПЕРВЫМ!)'); console.error(''); console.error('🔍 ДЕТАЛИ БЛОКОВ:'); message.content.forEach((block, idx) => { const emoji = idx === 0 ? (block.type === 'thinking' ? '✅' : '❌') : ' '; console.error(` ${emoji} [${idx}] ${block.type}`); if (block.type === 'thinking') { const preview = block.thinking?.substring(0, 50) || ''; const thinkingLength = block.thinking?.length || 0; console.error(` └─ "${preview}${thinkingLength > 50 ? '...' : ''}"`); } if (block.type === 'tool_use') { console.error(` └─ tool: ${block.name}, id: ${block.id?.substring(0, 12)}...`); } }); console.error(''); console.error('🔧 КАК ИСПРАВИТЬ В ВАШЕМ КОДЕ:'); console.error(''); console.error(' ❌ НЕ ДЕЛАЙТЕ ТАК:'); console.error(' const content = [textBlock, thinkingBlock, toolUseBlock]; // НЕПРАВИЛЬНО!'); console.error(''); console.error(' ✅ ДЕЛАЙТЕ ТАК:'); console.error(' const content = ['); console.error(' ...thinkingBlocks, // thinking ВСЕГДА первыми!'); console.error(' ...toolUseBlocks,'); console.error(' ...textBlocks'); console.error(' ];'); console.error(''); console.error(' Или используйте helper функцию:'); console.error(' import { ensureCorrectBlockOrder } from "solver-sdk";'); console.error(' const sortedContent = ensureCorrectBlockOrder(blocks);'); console.error(''); console.error('📚 Документация Anthropic:'); console.error(' https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking'); console.error(''); console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); throw new Error(`[SDK VALIDATION] Assistant message должен начинаться с thinking блока! Первый блок: ${firstBlock.type}, структура: ${structure}`); } // ✅ ФИКС: Очищаем невалидные signature из thinking блоков перед отправкой на бэкенд // Anthropic API требует либо валидную signature, либо вообще не иметь этого поля const cleanedContent = message.content.map((block) => { if ((block.type === 'thinking' || block.type === 'redacted_thinking') && block.hasOwnProperty('signature')) { const hasValidSignature = block.signature && typeof block.signature === 'string' && block.signature.length > 0; if (!hasValidSignature) { // Удаляем невалидную signature (пустая строка или undefined) const { signature, ...blockWithoutSignature } = block; return blockWithoutSignature; } } return block; }); return { ...message, content: cleanedContent }; } return message; } /** * Конвертирует текст в ContentBlock * @param text Текст сообщения * @returns Блок текстового контента */ export function createTextBlock(text) { return { type: 'text', text: text }; } /** * Создает сообщение пользователя с текстом * @param text Текст сообщения * @returns Сообщение пользователя */ export function createUserMessage(text) { return { role: 'user', content: text }; } /** * Создает системное сообщение * @param text Текст системного сообщения * @returns Системное сообщение */ export function createSystemMessage(text) { return { role: 'system', content: text }; } //# sourceMappingURL=message-helpers.js.map