feat(kabelfachmann-mcp): add local Ollama support for KABELFACHMANN_LLM_PROVIDER
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m4s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m53s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m2s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m4s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m53s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m2s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
This commit is contained in:
@@ -3,7 +3,7 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import express from "express";
|
||||
import { z } from "zod";
|
||||
import { QdrantMemoryService } from "./qdrant.js";
|
||||
import { askOpenRouter } from "./llm.js";
|
||||
import { askKabelfachmannLLM } from "./llm.js";
|
||||
|
||||
async function main() {
|
||||
const server = new McpServer({
|
||||
@@ -57,7 +57,7 @@ Hier ist der Kontext aus dem Handbuch:
|
||||
${contextText}`;
|
||||
|
||||
try {
|
||||
const answer = await askOpenRouter(systemPrompt, args.query);
|
||||
const answer = await askKabelfachmannLLM(systemPrompt, args.query);
|
||||
return {
|
||||
content: [{ type: "text", text: answer }],
|
||||
};
|
||||
|
||||
@@ -1,71 +1,118 @@
|
||||
import fs from "fs";
|
||||
import fsPromises from "fs/promises";
|
||||
import path from "path";
|
||||
import pdf from "pdf-parse";
|
||||
import { QdrantMemoryService } from "./qdrant.js";
|
||||
|
||||
async function start() {
|
||||
const qdrant = new QdrantMemoryService(
|
||||
process.env.QDRANT_URL || "http://localhost:6333",
|
||||
async function findPdfs(dir: string): Promise<string[]> {
|
||||
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
||||
const files = await Promise.all(
|
||||
entries.map((entry) => {
|
||||
const res = path.resolve(dir, entry.name);
|
||||
return entry.isDirectory() ? findPdfs(res) : res;
|
||||
}),
|
||||
);
|
||||
return Array.prototype
|
||||
.concat(...files)
|
||||
.filter((file: string) => file.toLowerCase().endsWith(".pdf"));
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const qdrantUrl = process.env.QDRANT_URL || "http://localhost:6333";
|
||||
console.error(`Initializing Qdrant at ${qdrantUrl}...`);
|
||||
const qdrant = new QdrantMemoryService(qdrantUrl);
|
||||
await qdrant.initialize();
|
||||
|
||||
const pdfPath = path.join(process.cwd(), "data", "pdf", "kabelhandbuch.pdf");
|
||||
console.error(`Reading PDF from ${pdfPath}...`);
|
||||
const dataDir =
|
||||
process.env.PDF_DATA_DIR || path.join(process.cwd(), "data", "pdf");
|
||||
console.error(`Scanning for PDFs in ${dataDir}...`);
|
||||
|
||||
let dataBuffer;
|
||||
let pdfPaths: string[] = [];
|
||||
try {
|
||||
dataBuffer = fs.readFileSync(pdfPath);
|
||||
pdfPaths = await findPdfs(dataDir);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"PDF file not found. Ensure it exists at data/pdf/kabelhandbuch.pdf",
|
||||
);
|
||||
console.error(`Failed to read directory ${dataDir}. Error:`, e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = await pdf(dataBuffer);
|
||||
const text = data.text;
|
||||
|
||||
// chunk text
|
||||
// A simple chunking strategy by paragraph or chunks of ~1000 characters
|
||||
const paragraphs = text
|
||||
.split(/\n\s*\n/)
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length > 50);
|
||||
|
||||
let currentChunk = "";
|
||||
const chunks: string[] = [];
|
||||
const MAX_CHUNK_LENGTH = 1500;
|
||||
|
||||
for (const p of paragraphs) {
|
||||
if (currentChunk.length + p.length > MAX_CHUNK_LENGTH) {
|
||||
chunks.push(currentChunk);
|
||||
currentChunk = p;
|
||||
} else {
|
||||
currentChunk += (currentChunk.length ? "\n\n" : "") + p;
|
||||
}
|
||||
}
|
||||
if (currentChunk.length > 0) {
|
||||
chunks.push(currentChunk);
|
||||
if (pdfPaths.length === 0) {
|
||||
console.error(`No PDFs found in ${dataDir}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error(
|
||||
`Split PDF into ${chunks.length} chunks. Ingesting to Qdrant...`,
|
||||
);
|
||||
console.error(`Found ${pdfPaths.length} PDFs. Starting ingestion...`);
|
||||
|
||||
let successCount = 0;
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i];
|
||||
const success = await qdrant.storeMemory(`Handbuch Teil ${i + 1}`, chunk);
|
||||
if (success) {
|
||||
successCount++;
|
||||
let totalSuccess = 0;
|
||||
let totalChunks = 0;
|
||||
|
||||
for (const pdfPath of pdfPaths) {
|
||||
console.error(`\nProcessing: ${pdfPath}`);
|
||||
const filename = path.basename(pdfPath);
|
||||
|
||||
let dataBuffer;
|
||||
try {
|
||||
dataBuffer = fs.readFileSync(pdfPath);
|
||||
} catch (e) {
|
||||
console.error(`Failed to read ${pdfPath}. Skipping...`);
|
||||
continue;
|
||||
}
|
||||
if ((i + 1) % 10 === 0) {
|
||||
console.error(`Ingested ${i + 1}/${chunks.length} chunks...`);
|
||||
|
||||
try {
|
||||
const data = await pdf(dataBuffer);
|
||||
const text = data.text;
|
||||
|
||||
// chunk text
|
||||
// A simple chunking strategy by paragraph or chunks of ~1000 characters
|
||||
const paragraphs = text
|
||||
.split(/\n\s*\n/)
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length > 50);
|
||||
|
||||
let currentChunk = "";
|
||||
const chunks: string[] = [];
|
||||
const MAX_CHUNK_LENGTH = 1500;
|
||||
|
||||
for (const p of paragraphs) {
|
||||
if (currentChunk.length + p.length > MAX_CHUNK_LENGTH) {
|
||||
chunks.push(currentChunk);
|
||||
currentChunk = p;
|
||||
} else {
|
||||
currentChunk += (currentChunk.length ? "\n\n" : "") + p;
|
||||
}
|
||||
}
|
||||
if (currentChunk.length > 0) {
|
||||
chunks.push(currentChunk);
|
||||
}
|
||||
|
||||
console.error(
|
||||
`Split ${filename} into ${chunks.length} chunks. Ingesting to Qdrant...`,
|
||||
);
|
||||
|
||||
let fileSuccessCount = 0;
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i];
|
||||
const success = await qdrant.storeMemory(
|
||||
`${filename} - Teil ${i + 1}`,
|
||||
chunk,
|
||||
);
|
||||
if (success) {
|
||||
fileSuccessCount++;
|
||||
totalSuccess++;
|
||||
}
|
||||
if ((i + 1) % 10 === 0) {
|
||||
console.error(`Ingested ${i + 1}/${chunks.length} chunks for ${filename}...`);
|
||||
}
|
||||
}
|
||||
totalChunks += chunks.length;
|
||||
|
||||
console.error(`Finished ${filename}: stored ${fileSuccessCount}/${chunks.length} chunks.`);
|
||||
} catch (e) {
|
||||
console.error(`Error processing ${pdfPath}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
`Ingestion complete! Successfully stored ${successCount}/${chunks.length} chunks.`,
|
||||
`\nIngestion complete! Successfully stored ${totalSuccess}/${totalChunks} chunks across ${pdfPaths.length} files.`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,55 @@
|
||||
import fetch from "node-fetch";
|
||||
|
||||
export async function askOpenRouter(
|
||||
export async function askKabelfachmannLLM(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
): Promise<string> {
|
||||
const provider = process.env.KABELFACHMANN_LLM_PROVIDER || "openrouter";
|
||||
|
||||
if (provider === "ollama") {
|
||||
return askOllama(systemPrompt, userPrompt);
|
||||
} else {
|
||||
return askOpenRouter(systemPrompt, userPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
async function askOllama(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
): Promise<string> {
|
||||
const host = process.env.KABELFACHMANN_OLLAMA_HOST || "http://127.0.0.1:11434";
|
||||
const model = process.env.KABELFACHMANN_OLLAMA_MODEL || "qwen2.5:32b";
|
||||
|
||||
const response = await fetch(`${host}/api/chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
],
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`Ollama API error: ${response.status} ${response.statusText} - ${text}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as any;
|
||||
if (!data.message || !data.message.content) {
|
||||
throw new Error("Invalid response from Ollama API");
|
||||
}
|
||||
return data.message.content;
|
||||
}
|
||||
|
||||
async function askOpenRouter(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
): Promise<string> {
|
||||
|
||||
Reference in New Issue
Block a user