Fix CF-271: Add fallback text search to archive_search
When LiteLLM embedding service is unavailable, archive_search now gracefully falls back to PostgreSQL text search (ILIKE) instead of returning an error. Also adds dotenv support for proper credential loading. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
*.log
|
*.log
|
||||||
|
.env
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"pg": "^8.11.3"
|
"pg": "^8.11.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -734,6 +735,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||||
|
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|||||||
@@ -12,12 +12,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"pg": "^8.11.3"
|
"pg": "^8.11.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.10.9",
|
||||||
"typescript": "^5.3.3",
|
"tsx": "^4.7.0",
|
||||||
"tsx": "^4.7.0"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
// Embeddings via LiteLLM API
|
// Embeddings via LiteLLM API
|
||||||
|
|
||||||
const LLM_API_URL = process.env.LLM_API_URL || 'https://api.agiliton.cloud/llm';
|
|
||||||
const LLM_API_KEY = process.env.LLM_API_KEY || '';
|
|
||||||
|
|
||||||
interface EmbeddingResponse {
|
interface EmbeddingResponse {
|
||||||
data: Array<{
|
data: Array<{
|
||||||
embedding: number[];
|
embedding: number[];
|
||||||
@@ -19,8 +16,13 @@ interface EmbeddingResponse {
|
|||||||
* Generate embedding for text using LiteLLM API
|
* Generate embedding for text using LiteLLM API
|
||||||
*/
|
*/
|
||||||
export async function getEmbedding(text: string): Promise<number[] | null> {
|
export async function getEmbedding(text: string): Promise<number[] | null> {
|
||||||
|
// Read env vars at runtime (after dotenv.config() in index.ts)
|
||||||
|
const LLM_API_URL = process.env.LLM_API_URL || 'https://api.agiliton.cloud/llm';
|
||||||
|
const LLM_API_KEY = process.env.LLM_API_KEY || '';
|
||||||
|
|
||||||
if (!LLM_API_KEY) {
|
if (!LLM_API_KEY) {
|
||||||
console.error('LLM_API_KEY not set, skipping embedding');
|
console.error('LLM_API_KEY not set, skipping embedding (check .env file)');
|
||||||
|
console.error('LLM_API_URL:', LLM_API_URL);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
src/index.ts
19
src/index.ts
@@ -9,6 +9,25 @@
|
|||||||
* ssh -L 5433:localhost:5432 -i ~/.ssh/hetzner_mash_deploy root@46.224.188.157 -N &
|
* ssh -L 5433:localhost:5432 -i ~/.ssh/hetzner_mash_deploy root@46.224.188.157 -N &
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Load environment variables from .env file
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const envPath = join(__dirname, '..', '.env');
|
||||||
|
const result = dotenv.config({ path: envPath, override: true });
|
||||||
|
|
||||||
|
// Log environment loading status (goes to MCP server logs)
|
||||||
|
if (result.error) {
|
||||||
|
console.error('Failed to load .env from:', envPath, result.error);
|
||||||
|
} else {
|
||||||
|
console.error('Loaded .env from:', envPath);
|
||||||
|
console.error('LLM_API_KEY present:', !!process.env.LLM_API_KEY);
|
||||||
|
console.error('LLM_API_URL:', process.env.LLM_API_URL);
|
||||||
|
}
|
||||||
|
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -125,10 +125,54 @@ export async function archiveSearch(args: ArchiveSearchArgs): Promise<string> {
|
|||||||
// Generate embedding for search
|
// Generate embedding for search
|
||||||
const embedding = await getEmbedding(searchQuery);
|
const embedding = await getEmbedding(searchQuery);
|
||||||
|
|
||||||
|
// Fallback to text search if embeddings unavailable
|
||||||
if (!embedding) {
|
if (!embedding) {
|
||||||
return 'Error: Could not generate embedding for search';
|
console.warn('Embeddings unavailable, falling back to text search');
|
||||||
|
|
||||||
|
let whereClause = '(title ILIKE $1 OR content ILIKE $1)';
|
||||||
|
const params: unknown[] = [`%${searchQuery}%`];
|
||||||
|
let paramIndex = 2;
|
||||||
|
|
||||||
|
if (project) {
|
||||||
|
whereClause += ` AND project_key = $${paramIndex++}`;
|
||||||
|
params.push(project);
|
||||||
|
}
|
||||||
|
if (archive_type) {
|
||||||
|
whereClause += ` AND archive_type = $${paramIndex++}`;
|
||||||
|
params.push(archive_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params.push(limit);
|
||||||
|
|
||||||
|
const archives = await query<Archive>(
|
||||||
|
`SELECT id, archive_type, title, original_path, file_size,
|
||||||
|
to_char(archived_at, 'YYYY-MM-DD') as archived_at
|
||||||
|
FROM project_archives
|
||||||
|
WHERE ${whereClause}
|
||||||
|
ORDER BY archived_at DESC
|
||||||
|
LIMIT $${paramIndex}`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
if (archives.length === 0) {
|
||||||
|
return 'No relevant archives found';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = ['Relevant archives (text search - embeddings unavailable):\n'];
|
||||||
|
for (const a of archives) {
|
||||||
|
const sizeStr = a.file_size ? ` (${Math.round(a.file_size / 1024)}KB)` : '';
|
||||||
|
lines.push(`**[${a.archive_type}]** ${a.title}`);
|
||||||
|
lines.push(` Archived: ${a.archived_at}${sizeStr}`);
|
||||||
|
if (a.original_path) {
|
||||||
|
lines.push(` Path: ${a.original_path}`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic search with embeddings
|
||||||
const embeddingStr = formatEmbedding(embedding);
|
const embeddingStr = formatEmbedding(embedding);
|
||||||
|
|
||||||
let whereClause = 'WHERE embedding IS NOT NULL';
|
let whereClause = 'WHERE embedding IS NOT NULL';
|
||||||
|
|||||||
Reference in New Issue
Block a user