UNPKG

@alvinveroy/codecompass

Version:

AI-powered MCP server for codebase navigation and LLM prompt optimization

167 lines (151 loc) 7.62 kB
import axios, { AxiosError } from "axios"; // axios will be used by OllamaProvider.generateText import { configService, logger } from "./config-service"; import { OllamaEmbeddingResponse, OllamaGenerateResponse } from "./types"; // OllamaGenerateResponse might be used by OllamaProvider import { preprocessText } from "../utils/text-utils"; import { withRetry } from "../utils/retry-utils"; /** * Check if Ollama server is running and accessible * @returns Promise<boolean> - True if Ollama is accessible, false otherwise */ export async function checkOllama(): Promise<boolean> { const host = configService.OLLAMA_HOST; logger.info(`Checking Ollama at ${host}`); try { await withRetry(async () => { const response = await axios.get(host, { timeout: 10000 }); // Specific timeout for check logger.info(`Ollama status: ${response.status}`); }); return true; } catch (error: unknown) { const err = error instanceof Error ? error : new Error(String(error)); logger.error(`Failed to connect to Ollama: ${err.message}`); return false; } } /** * Check if a specific Ollama model is available * @param model - The model name to check * @param isEmbeddingModel - Whether this is an embedding model * @returns Promise<boolean> - True if model is available, false otherwise */ export async function checkOllamaModel(model: string, isEmbeddingModel: boolean): Promise<boolean> { const host = configService.OLLAMA_HOST; logger.info(`Checking Ollama model: ${model}`); try { if (isEmbeddingModel) { const response = await axios.post<OllamaEmbeddingResponse>( `${host}/api/embeddings`, { model, prompt: "test" }, { timeout: 10000 } ); if (response.status === 200 && response.data.embedding) { logger.info(`Ollama model ${model} is available`); return true; } // If not returned true, it means it's not functional for this path logger.warn(`Ollama embedding model ${model} did not return expected data structure.`); return false; // Explicitly return false } else { const response = await axios.post<OllamaGenerateResponse>( // Add type argument here `${host}/api/generate`, { model, prompt: "test", stream: false }, { timeout: 10000 } ); if (response.status === 200 && response.data && typeof response.data.response === 'string') { // Check response.data and its type logger.info(`Ollama model ${model} is available`); return true; } // If not returned true, it means it's not functional for this path logger.warn(`Ollama generation model ${model} did not return expected data structure.`); return false; // Explicitly return false } // The logic above now ensures a boolean is always returned from the try block's success paths. // The original fall-through to throw an error is no longer needed here, // as we explicitly return false if the checks fail. } catch (error: unknown) { const err = error instanceof Error ? error : new Error(String(error)); const errorDetails: { message: string, response?: { status: number; data: unknown } } = { message: err.message }; if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; errorDetails.response = axiosError.response ? { status: axiosError.response.status, data: axiosError.response.data, } : undefined; } logger.error(`Ollama model check error for ${model}`, errorDetails); // Instead of throwing, we should return false as per the function's Promise<boolean> signature // The caller can decide if this constitutes a critical failure. return false; } } /** * Generate embeddings for text using Ollama * @param text - The text to generate embeddings for * @returns Promise<number[]> - The embedding vector */ export async function generateEmbedding(text: string): Promise<number[]> { const processedText = preprocessText(text); const truncatedText = processedText.length > configService.MAX_INPUT_LENGTH ? processedText.slice(0, configService.MAX_INPUT_LENGTH) : processedText; try { const host = configService.OLLAMA_HOST; const model = configService.EMBEDDING_MODEL; // Use configured embedding model const response = await withRetry(async () => { logger.info(`Generating embedding for text (length: ${truncatedText.length}, snippet: "${truncatedText.slice(0, 100)}...")`); const res = await axios.post<OllamaEmbeddingResponse>( `${host}/api/embeddings`, { model: model, prompt: truncatedText }, { timeout: configService.REQUEST_TIMEOUT } ); if (!res.data.embedding || !Array.isArray(res.data.embedding)) { throw new Error("Invalid embedding response from Ollama API"); } // Ensure the embedding is an array of numbers and has the expected dimension if (!Array.isArray(res.data.embedding) || res.data.embedding.some(isNaN)) { logger.error(`Ollama API returned an invalid embedding vector (not an array of numbers or contains NaN) for model ${model}. Length: ${res.data.embedding?.length}`); throw new Error(`Ollama API returned an invalid embedding vector (contains NaN or not an array of numbers) for model ${model}.`); } // ADD THIS CHECK: const expectedDimension = configService.EMBEDDING_DIMENSION; if (res.data.embedding.some(v => !isFinite(v))) { logger.error(`Ollama API returned an embedding vector with non-finite values (Infinity or -Infinity) for model ${model}.`); throw new Error(`Ollama API returned an embedding vector with non-finite values for model ${model}.`); } if (res.data.embedding.length !== expectedDimension) { logger.error(`Ollama API returned an embedding vector with unexpected dimension for model ${model}. Expected: ${expectedDimension}, Actual: ${res.data.embedding.length}.`); throw new Error(`Ollama API returned an embedding vector with unexpected dimension. Expected: ${expectedDimension}, Actual: ${res.data.embedding.length}`); } return res.data; // Return the whole OllamaEmbeddingResponse object }); return response.embedding; } catch (error: unknown) { const err = error instanceof Error ? error : new Error(String(error)); const errorLogDetails: { // Changed let to const message: string; code?: string; response?: { status: number; data: unknown }; inputLength: number; inputSnippet: string; } = { message: err.message, inputLength: truncatedText.length, inputSnippet: truncatedText.slice(0, 100), }; if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; errorLogDetails.code = axiosError.code; errorLogDetails.response = axiosError.response ? { status: axiosError.response.status, data: axiosError.response.data, } : undefined; } logger.error("Ollama embedding error", errorLogDetails); // It's critical to re-throw here so that the calling function (e.g., in repository.ts) // knows that embedding failed and can skip adding a point with a bad/missing vector. // Or, return a specific marker like null/undefined if the caller is designed to handle it. throw new Error(`Failed to generate embedding with Ollama model ${configService.EMBEDDING_MODEL}: ${err.message}`); } }