152 lines
4.3 KiB
JavaScript
152 lines
4.3 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
/**
|
|
* Lectura directa de los markdown del knowledge base desde el filesystem.
|
|
*
|
|
* El MCP server corre dentro del container `agentic` junto al FastAPI, asi
|
|
* que los .md viven en `/app/docs/` (la imagen los copia ahi).
|
|
*
|
|
* En caso de override por entorno, respeta `ACAI_DOCS_DIR`. En desarrollo
|
|
* fuera del container, fallback a paths relativos al cwd.
|
|
*/
|
|
|
|
function resolveDocsDir() {
|
|
const override = process.env.ACAI_DOCS_DIR;
|
|
if (override) return override;
|
|
// Container path
|
|
return "/app/docs";
|
|
}
|
|
|
|
export async function listDocs() {
|
|
const dir = resolveDocsDir();
|
|
let files;
|
|
try {
|
|
files = await fs.readdir(dir);
|
|
} catch (err) {
|
|
const error = new Error(`No se pudo leer el directorio de docs (${dir}): ${err.message}`);
|
|
error.code = "DOCS_DIR_NOT_FOUND";
|
|
throw error;
|
|
}
|
|
|
|
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
const docs = [];
|
|
|
|
for (const file of mdFiles) {
|
|
const id = file.replace(/\.md$/i, "");
|
|
const filePath = path.join(dir, file);
|
|
const content = await fs.readFile(filePath, "utf8");
|
|
const lines = content.split("\n");
|
|
const titleLine = lines.find((l) => l.trim().startsWith("# ")) || id;
|
|
const title = titleLine.replace(/^#+/, "").trim();
|
|
|
|
// Summary: primeras 30 lineas no-heading, capeado a 500 chars
|
|
const summaryLines = [];
|
|
for (const line of lines.slice(0, 30)) {
|
|
const t = line.trim();
|
|
if (t && !t.startsWith("#")) summaryLines.push(t);
|
|
if (summaryLines.join(" ").length > 500) break;
|
|
}
|
|
const summary = summaryLines.join(" ").slice(0, 500);
|
|
|
|
docs.push({
|
|
id,
|
|
title,
|
|
summary,
|
|
chars: content.length,
|
|
});
|
|
}
|
|
return docs;
|
|
}
|
|
|
|
export async function readDoc(name, section) {
|
|
const dir = resolveDocsDir();
|
|
const safeName = String(name).replace(/\.md$/i, "").replace(/[\/\\]/g, "");
|
|
const filePath = path.join(dir, `${safeName}.md`);
|
|
|
|
let content;
|
|
try {
|
|
content = await fs.readFile(filePath, "utf8");
|
|
} catch (err) {
|
|
const error = new Error(`Doc '${safeName}' no encontrado en ${dir}`);
|
|
error.code = "DOC_NOT_FOUND";
|
|
throw error;
|
|
}
|
|
|
|
const titleLine = content.split("\n").find((l) => l.trim().startsWith("# ")) || safeName;
|
|
const title = titleLine.replace(/^#+/, "").trim();
|
|
const availableSections = listSections(content);
|
|
|
|
if (section) {
|
|
const sectionContent = extractSection(content, section);
|
|
if (sectionContent === null) {
|
|
return {
|
|
id: safeName,
|
|
title,
|
|
section_requested: section,
|
|
section_found: false,
|
|
available_sections: availableSections,
|
|
chars: 0,
|
|
content: "",
|
|
};
|
|
}
|
|
return {
|
|
id: safeName,
|
|
title,
|
|
section,
|
|
section_found: true,
|
|
chars: sectionContent.length,
|
|
content: sectionContent,
|
|
};
|
|
}
|
|
|
|
return {
|
|
id: safeName,
|
|
title,
|
|
section: null,
|
|
section_found: true,
|
|
chars: content.length,
|
|
available_sections: availableSections,
|
|
content,
|
|
};
|
|
}
|
|
|
|
function listSections(content) {
|
|
const sections = [];
|
|
for (const line of content.split("\n")) {
|
|
const t = line.trimStart();
|
|
if (t.startsWith("## ") && !t.startsWith("### ")) {
|
|
sections.push(t.slice(3).trim());
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
|
|
function extractSection(content, query) {
|
|
const q = String(query).toLowerCase().trim();
|
|
if (!q) return null;
|
|
const lines = content.split("\n");
|
|
const captured = [];
|
|
let capturing = false;
|
|
|
|
for (const line of lines) {
|
|
const t = line.trimStart();
|
|
const isH2 = t.startsWith("## ") && !t.startsWith("### ");
|
|
|
|
if (isH2) {
|
|
const heading = t.slice(3).trim();
|
|
if (capturing) break;
|
|
if (heading.toLowerCase().includes(q)) {
|
|
capturing = true;
|
|
captured.push(line);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (capturing) captured.push(line);
|
|
}
|
|
|
|
if (captured.length === 0) return null;
|
|
return captured.join("\n").trimEnd();
|
|
}
|