UNPKG

venice-dev-tools

Version:

unOfficial SDK for the Venice AI API

595 lines (520 loc) 21.5 kB
// Chat commands implementation import { Command } from 'commander'; import * as inquirer from 'inquirer'; import * as chalk from 'chalk'; import * as path from 'path'; import ora from 'ora'; import { VeniceNode } from '../../venice-node'; import type { ChatCompletionRequest as BaseChatCompletionRequest, ChatCompletionResponse as BaseChatCompletionResponse } from '@venice-dev-tools/core'; import type { ContentItem, TextContent } from '@venice-dev-tools/core/src/types/multimodal'; import { processFile } from '../../utils'; // Define ChatMessage interface locally to avoid import issues interface ChatMessage { role: 'system' | 'user' | 'assistant'; content: string | ContentItem[]; } // Extend the ChatCompletionRequest interface to include venice_parameters interface ChatCompletionRequest extends BaseChatCompletionRequest { venice_parameters?: { enable_web_search?: 'auto' | 'on' | 'off'; character_slug?: string; include_venice_system_prompt?: boolean; }; } // Extend the ChatCompletionResponse interface to include venice_parameters interface ChatCompletionResponse extends BaseChatCompletionResponse { venice_parameters?: { web_search_citations?: Array<{ title: string; url: string; }>; }; } /** * Register chat-related commands with the CLI */ export function registerChatCommands(program: Command, venice: VeniceNode): void { const chat = program .command('chat') .description('Chat with Venice AI models'); // Chat completion command chat .command('completion') .description('Create a chat completion') .option('-m, --model <model>', 'Model to use', 'llama-3.3-70b') .option('-p, --prompt <prompt>', 'Chat prompt message') .option('-s, --system <system>', 'System message for the chat') .option('-t, --temperature <temperature>', 'Temperature for sampling (0.0-2.0)', '0.7') .option('--max-tokens <maxTokens>', 'Maximum tokens to generate') .option('--stream', 'Stream the response', false) .option('--web-search', 'Enable web search', false) .option('--character <character>', 'Character slug to use') .option('--attach <files>', 'Attach files to the message (comma-separated paths)') .option('--pdf-mode <mode>', 'How to process PDF files (image, text, or both)', 'image') .option('--raw', 'Output raw text without formatting (useful for piping to other commands)', false) .option('--json', 'Output response as JSON (useful for programmatic use)', false) .action(async (options) => { // If no prompt is provided, start interactive mode if (!options.prompt) { return startInteractiveChat(venice, options); } const messages: ChatMessage[] = []; // Add system message if provided if (options.system) { messages.push({ role: 'system', content: options.system }); } // Handle file attachments if (options.attach) { const fileList = options.attach.split(',').map((f: string) => f.trim()); try { // Process all files const processedFiles = await Promise.all(fileList.map(async (filePath: string) => { try { return await processFile(filePath, { pdfMode: options.pdfMode }); } catch (error) { console.error(chalk.red(`Error processing file ${filePath}: ${(error as Error).message}`)); throw error; } })); // Create content array for multimodal input const contentArray = [ { type: 'text', text: options.prompt || 'Please analyze these files.' }, ...processedFiles ]; // Use content array instead of string content messages.push({ role: 'user', content: contentArray }); // Auto-select vision model if needed if ( processedFiles.some((file: ContentItem) => file.type === 'image_url') && options.model === 'llama-3.3-70b' // default model ) { // Use the default vision model options.model = 'qwen-2.5-vl'; console.log(chalk.yellow(`Using vision model: ${options.model} for image analysis`)); } } catch (error) { console.error(chalk.red(`Error processing files: ${(error as Error).message}`)); process.exit(1); } } else { // Regular text-only message messages.push({ role: 'user', content: options.prompt }); } // Create request payload const request: ChatCompletionRequest = { model: options.model, messages: messages as any, // Type assertion to avoid TypeScript errors temperature: parseFloat(options.temperature), venice_parameters: {} }; // Set max tokens if provided if (options.maxTokens) { request.max_tokens = parseInt(options.maxTokens, 10); } // Set web search if enabled if (options.webSearch) { request.venice_parameters!.enable_web_search = 'auto'; } // Set character if provided if (options.character) { request.venice_parameters!.character_slug = options.character; } // Check if both raw and json options are provided if (options.raw && options.json) { console.error(chalk.red('Error: Cannot use both --raw and --json options together')); process.exit(1); } try { const spinner = ora('Generating response...').start(); if (options.stream) { spinner.stop(); // Prepare for streaming let responseText = ''; // Execute the stream using the async generator pattern try { // Set up for different output formats if (!options.raw && !options.json) { if (options.attach) { // For multimodal content, show the prompt and file info console.log(chalk.cyan('User: ') + options.prompt); console.log(chalk.cyan('Files: ') + options.attach); } else { // For text-only content, just show the prompt console.log(chalk.cyan('User: ') + options.prompt); } console.log(chalk.green('Venice AI: ')); } // Get the stream generator const streamGenerator = venice.chat.streamCompletion({ ...request, stream: true } as any); // Process the stream for await (const chunk of streamGenerator) { const content = chunk.choices[0]?.delta?.content; if (content) { // Handle content based on output format if (options.raw || !options.json) { // For raw mode or standard mode, write to stdout process.stdout.write(content); } // Always collect the content for potential JSON output responseText += content; } } // Handle completion based on output format if (options.json) { // For JSON mode, output the collected response const jsonOutput: { prompt: string; response: string; model: string; stream: boolean; files?: string[]; } = { prompt: options.prompt, response: responseText, model: options.model, stream: true }; // Add file information if files were attached if (options.attach) { jsonOutput.files = options.attach.split(',').map((f: string) => f.trim()); } console.log(JSON.stringify(jsonOutput, null, 2)); } else { // For raw mode, just add a newline // For standard mode, add two newlines process.stdout.write(options.raw ? '\n' : '\n\n'); } } catch (error) { throw error; } } else { const response = await venice.chat.createCompletion(request as any) as unknown as ChatCompletionResponse; spinner.stop(); if (options.json) { // Output as JSON console.log(JSON.stringify({ prompt: options.prompt, response: response.choices[0]?.message?.content, model: options.model, usage: response.usage, web_search_citations: (response as any).venice_parameters?.web_search_citations || [] }, null, 2)); } else if (options.raw) { // Output raw text only console.log(response.choices[0]?.message?.content); } else { // Standard formatted output console.log(chalk.cyan('User: ') + options.prompt); console.log(chalk.green('Venice AI: ') + response.choices[0]?.message?.content); // Display web search citations if available if ((response as any).venice_parameters?.web_search_citations?.length) { console.log('\n' + chalk.yellow('Citations:')); (response as any).venice_parameters.web_search_citations.forEach((citation: any, i: number) => { console.log(`${i + 1}. ${chalk.blue(citation.title)}: ${citation.url}`); }); } // Display usage statistics console.log('\n' + chalk.dim(`Tokens: ${response.usage.total_tokens} (${response.usage.prompt_tokens} prompt, ${response.usage.completion_tokens} completion)`)); } } } catch (error) { const spinner = ora(); spinner.stop(); const errorMessage = (error as Error).message; const isAuthError = (error as Error).name === 'VeniceAuthError' || errorMessage.includes('API key'); if (options.json) { // Output error as JSON console.error(JSON.stringify({ error: errorMessage, type: isAuthError ? 'auth_error' : 'api_error', status: 'error' }, null, 2)); } else if (options.raw) { // Output raw error message console.error(errorMessage); } else { // Standard formatted error if (isAuthError) { console.error(chalk.red('Error: API key not found or invalid.')); console.error(chalk.yellow('Please provide an API key using one of these methods:')); console.error(chalk.yellow('1. Use the --api-key or -k option: venice -k YOUR_API_KEY ...')); console.error(chalk.yellow('2. Set the VENICE_API_KEY environment variable')); console.error(chalk.yellow('3. Save your API key using: venice set-key YOUR_API_KEY')); } else { ora().fail(`Error: ${errorMessage}`); } } process.exit(1); } }); // Interactive chat command chat .command('interactive') .alias('i') .description('Start an interactive chat session') .option('-m, --model <model>', 'Model to use', 'llama-3.3-70b') .option('-s, --system <system>', 'System message for the chat') .option('-t, --temperature <temperature>', 'Temperature for sampling (0.0-2.0)', '0.7') .option('--stream', 'Stream the response', false) .option('--web-search', 'Enable web search', false) .option('--character <character>', 'Character slug to use') .option('--pdf-mode <mode>', 'How to process PDF files (image, text, or both)', 'image') .option('--raw', 'Output raw text without formatting (ignored in interactive mode)', false) .option('--json', 'Output response as JSON (ignored in interactive mode)', false) .action(async (options) => { await startInteractiveChat(venice, options); }); } /** * Start an interactive chat session */ async function startInteractiveChat(venice: VeniceNode, options: any): Promise<void> { console.log(chalk.bold('Venice AI Interactive Chat')); console.log(chalk.dim(`Model: ${options.model}`)); console.log(chalk.dim(`Mode: ${options.stream ? 'Streaming' : 'Standard'}`)); console.log(chalk.dim('Type "exit" or Ctrl+C to end the session')); console.log(chalk.dim('Type "/attach [filepath]" to include a file\n')); // Notify if raw or json options are provided if (options.raw || options.json) { console.log(chalk.yellow('Note: --raw and --json options are ignored in interactive mode\n')); } const messages: ChatMessage[] = []; // Add system message if provided if (options.system) { messages.push({ role: 'system', content: options.system }); console.log(chalk.yellow('System: ') + options.system + '\n'); } // Interactive chat loop while (true) { let userInput; try { const response = await inquirer.prompt([ { type: 'input', name: 'userInput', message: chalk.cyan('You:'), prefix: '' } ]); userInput = response.userInput; } catch (error) { // Handle case where stdin is not a TTY (e.g., piped input) console.log(chalk.dim('Ending chat session (non-interactive input)')); break; } // Exit condition if (!userInput || userInput.toLowerCase() === 'exit') { console.log(chalk.dim('Ending chat session')); break; } // Check for attach command if (userInput.startsWith('/attach ')) { const filePath = userInput.substring(8).trim(); try { const processedFile = await processFile(filePath, { pdfMode: options.pdfMode }); // Prompt for a message to accompany the file const messageResponse = await inquirer.prompt([{ type: 'input', name: 'message', message: chalk.cyan('Enter a message to accompany the file:'), default: 'Please analyze this file.' }]); // Create content array const contentArray: ContentItem[] = [ { type: 'text', text: messageResponse.message } as TextContent ]; // Add the processed file(s) to the content array if (Array.isArray(processedFile)) { contentArray.push(...processedFile); } else { contentArray.push(processedFile); } // Add to messages messages.push({ role: 'user', content: contentArray }); // Auto-select vision model if needed const hasImage = Array.isArray(processedFile) ? processedFile.some(item => item.type === 'image_url') : processedFile.type === 'image_url'; if (hasImage && options.model === 'llama-3.3-70b') { options.model = 'qwen-2.5-vl'; console.log(chalk.yellow(`Switched to vision model: ${options.model}`)); } console.log(chalk.green(`File attached: ${path.basename(filePath)}`)); continue; // Skip to next iteration } catch (error) { console.error(chalk.red(`Error attaching file: ${(error as Error).message}`)); continue; // Skip to next iteration } } // Add user message (for regular text messages) messages.push({ role: 'user', content: userInput }); // Let's try a completely different approach // Instead of trying to reuse the file message, let's create a new multimodal message // that combines the image and the current text input console.log(chalk.dim(`Debug: Message history has ${messages.length} messages`)); // Find the last file attachment message to extract the image const fileMessage = messages.find(msg => msg.role === 'user' && typeof msg.content !== 'string' && Array.isArray(msg.content) ); // Create a new message array for the API const apiMessages = []; // Add system message if present const systemMessage = messages.find(msg => msg.role === 'system'); if (systemMessage) { apiMessages.push({ role: 'system', content: typeof systemMessage.content === 'string' ? systemMessage.content : JSON.stringify(systemMessage.content) }); } // If we have a file message with an image, create a new multimodal message if (fileMessage && fileMessage.content && Array.isArray(fileMessage.content)) { // Find the image content item const imageItem = fileMessage.content.find(item => item.type === 'image_url' && item.image_url && item.image_url.url ); if (imageItem) { // Create a new multimodal message with both the image and the current text apiMessages.push({ role: 'user', content: [ { type: 'text', text: userInput }, imageItem ] }); console.log(chalk.dim(`Debug: Created new multimodal message with image and text: "${userInput}"`)); } else { // No image found, just add the text apiMessages.push({ role: 'user', content: userInput }); } } else { // No file message, just add the text apiMessages.push({ role: 'user', content: userInput }); } // Create the request with the new message array const request: ChatCompletionRequest = { model: options.model, messages: apiMessages as any, // Type assertion to avoid TypeScript errors temperature: parseFloat(options.temperature), venice_parameters: {} }; console.log(chalk.dim(`Debug: Sending ${apiMessages.length} messages to API`)); console.log(chalk.dim(`Debug: Using model: ${options.model}`)); // Set web search if enabled if (options.webSearch) { request.venice_parameters!.enable_web_search = 'auto'; } // Set character if provided if (options.character) { request.venice_parameters!.character_slug = options.character; } try { // Check if streaming is enabled if (options.stream) { console.log(chalk.green('Venice AI: ')); // Prepare for streaming let responseText = ''; try { // Get the stream generator const streamGenerator = venice.chat.streamCompletion({ ...request, stream: true } as any); // Process the stream for await (const chunk of streamGenerator) { const content = chunk.choices[0]?.delta?.content; if (content) { // Write to stdout process.stdout.write(content); // Collect the content responseText += content; } } // Add a newline after streaming process.stdout.write('\n\n'); // Add assistant message to history messages.push({ role: 'assistant', content: responseText }); } catch (error) { console.error(chalk.red(`Error during streaming: ${(error as Error).message}\n`)); } } else { // Non-streaming mode const spinner = ora('Thinking...').start(); const response = await venice.chat.createCompletion(request as any) as unknown as ChatCompletionResponse; spinner.stop(); // Get assistant response const assistantResponse = response.choices[0]?.message; if (assistantResponse && assistantResponse.content) { // For display purposes, convert content to string if it's not already const displayContent = typeof assistantResponse.content === 'string' ? assistantResponse.content : JSON.stringify(assistantResponse.content); console.log(chalk.green('Venice AI: ') + displayContent + '\n'); // Add assistant message to history - always use string content for assistant responses // This ensures compatibility with future messages messages.push({ role: 'assistant', content: typeof assistantResponse.content === 'string' ? assistantResponse.content : JSON.stringify(assistantResponse.content) }); // Display web search citations if available if ((response as any).venice_parameters?.web_search_citations?.length) { console.log(chalk.yellow('Citations:')); (response as any).venice_parameters.web_search_citations.forEach((citation: any, i: number) => { console.log(`${i + 1}. ${chalk.blue(citation.title)}: ${citation.url}`); }); console.log(''); } } else { console.log(chalk.red('No response generated.\n')); } } } catch (error) { console.error(chalk.red(`Error: ${(error as Error).message}\n`)); } } }