235 lines
8.3 KiB
JavaScript
235 lines
8.3 KiB
JavaScript
import http from "node:http";
|
|
|
|
const port = Number(process.env.SLICE4_SHARED_AI_STUB_PORT || 18011);
|
|
const knowledgeBases = [];
|
|
const documentsByKnowledgeBase = new Map();
|
|
|
|
function sendJson(response, status, payload) {
|
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
response.end(`${JSON.stringify(payload)}\n`);
|
|
}
|
|
|
|
async function readJson(request) {
|
|
const chunks = [];
|
|
for await (const chunk of request) {
|
|
chunks.push(chunk);
|
|
}
|
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
return text ? JSON.parse(text) : {};
|
|
}
|
|
|
|
async function readBuffer(request) {
|
|
const chunks = [];
|
|
for await (const chunk of request) {
|
|
chunks.push(chunk);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
function nowIso() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function parseMultipartFile(buffer, contentType = "") {
|
|
const text = buffer.toString("latin1");
|
|
const fileNameMatch = text.match(/filename="([^"]+)"/i);
|
|
const mimeTypeMatch = text.match(/Content-Type:\s*([^\r\n]+)/i);
|
|
return {
|
|
fileName: fileNameMatch?.[1] || "document.txt",
|
|
mimeType: mimeTypeMatch?.[1] || contentType || "application/octet-stream",
|
|
fileSize: buffer.length,
|
|
};
|
|
}
|
|
|
|
function buildReviewPayload(body) {
|
|
const title = body.title || "Local shared-ai stub review";
|
|
const contractType = body.contract_type || "procurement";
|
|
const text = body.text || "";
|
|
const quote = text.slice(0, 160) || "Generated contract content";
|
|
|
|
return {
|
|
basic_info: {
|
|
contract_type: contractType,
|
|
contract_title: title,
|
|
party_a: "Party A",
|
|
party_b: "Party B",
|
|
amount: "Pending extraction",
|
|
term: "Pending extraction",
|
|
},
|
|
contract_structure: {
|
|
title,
|
|
detected_contract_type: contractType,
|
|
detected_contract_type_label: contractType,
|
|
clause_count: 2,
|
|
payment_milestones: [],
|
|
clause_relationships: [],
|
|
missing_sections: ["acceptance clause", "breach clause", "dispute clause"],
|
|
clause_type_counts: { payment: 1, confidentiality: 1 },
|
|
summary: "Stub review extracted payment and confidentiality clauses and marked the contract as high risk.",
|
|
},
|
|
risk_summary: { high: 1, medium: 0, low: 1, overall: "high" },
|
|
clauses: [
|
|
{
|
|
title: "Payment Clause",
|
|
content: quote,
|
|
clause_type: "payment",
|
|
clause_label: "Payment Clause",
|
|
summary: "Payment arrangement detected; acceptance trigger and invoice dependency should be clarified.",
|
|
},
|
|
{
|
|
title: "Confidentiality Clause",
|
|
content: quote,
|
|
clause_type: "confidentiality",
|
|
clause_label: "Confidentiality Clause",
|
|
summary: "Confidentiality obligation detected; duration and scope should be clarified.",
|
|
},
|
|
],
|
|
risk_items: [
|
|
{
|
|
title: "Payment trigger needs clarification",
|
|
level: "high",
|
|
description: "The payment section still depends on vague acceptance and invoice conditions.",
|
|
suggestion: "Add explicit acceptance criteria, invoice dependency, and due date for payment.",
|
|
evidence: { quote, location: "generated_contract" },
|
|
},
|
|
{
|
|
title: "Confidentiality scope should be refined",
|
|
level: "low",
|
|
description: "The confidentiality clause needs a clearer scope and survival period.",
|
|
suggestion: "Specify confidential information scope, carve-outs, and survival term.",
|
|
evidence: { quote, location: "generated_contract" },
|
|
},
|
|
],
|
|
suggestions: [
|
|
"Add explicit acceptance criteria, invoice dependency, and due date for payment.",
|
|
"Specify confidential information scope, carve-outs, and survival term.",
|
|
],
|
|
legal_basis: [],
|
|
review_opinion: "Stub review completed. Manual review is recommended before delivery.",
|
|
recommendation: "manual_review",
|
|
confidence_summary: {
|
|
provider: "slice4-shared-ai-stub",
|
|
knowledge_source: "qa-fixture",
|
|
structured_analysis: { stage: "slice4-auto-review-stub" },
|
|
rendered_report: [
|
|
`Review task: ${title}`,
|
|
"Structured summary: local shared-ai stub review",
|
|
"Risk summary: high risk 1, low risk 1",
|
|
"Opinion: manual review is recommended before delivery.",
|
|
].join("\n"),
|
|
},
|
|
rendered_report: [
|
|
`Review task: ${title}`,
|
|
"Structured summary: local shared-ai stub review",
|
|
"Risk summary: high risk 1, low risk 1",
|
|
"Opinion: manual review is recommended before delivery.",
|
|
].join("\n"),
|
|
};
|
|
}
|
|
|
|
const server = http.createServer(async (request, response) => {
|
|
try {
|
|
if (request.method === "GET" && request.url === "/health") {
|
|
sendJson(response, 200, { code: 0, data: { status: "ok" } });
|
|
return;
|
|
}
|
|
|
|
if (request.method === "POST" && request.url === "/api/v0.1/domain-packs/legal-review/reviews/run") {
|
|
const body = await readJson(request);
|
|
sendJson(response, 200, { code: 0, data: buildReviewPayload(body) });
|
|
return;
|
|
}
|
|
|
|
if (request.method === "GET" && request.url?.startsWith("/api/v0.2/knowledge-bases")) {
|
|
const match = request.url.match(/^\/api\/v0\.2\/knowledge-bases\/([^/?]+)\/documents/);
|
|
if (match) {
|
|
const knowledgeBaseId = decodeURIComponent(match[1]);
|
|
const items = documentsByKnowledgeBase.get(knowledgeBaseId) || [];
|
|
sendJson(response, 200, {
|
|
code: 0,
|
|
data: { items, total: items.length, page: 1, page_size: 20 },
|
|
});
|
|
return;
|
|
}
|
|
|
|
sendJson(response, 200, {
|
|
code: 0,
|
|
data: { items: knowledgeBases, total: knowledgeBases.length, page: 1, page_size: 20 },
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (request.method === "POST" && request.url === "/api/v0.2/knowledge-bases") {
|
|
const body = await readJson(request);
|
|
const existing = knowledgeBases.find((item) => item.name === body.name);
|
|
if (existing) {
|
|
sendJson(response, 200, { code: 0, data: existing });
|
|
return;
|
|
}
|
|
const knowledgeBase = {
|
|
id: `kb-${knowledgeBases.length + 1}`,
|
|
name: body.name || "Demo Knowledge Base",
|
|
description: body.description || null,
|
|
status: "active",
|
|
document_count: 0,
|
|
retrieval_mode: body.retrieval_mode || "hybrid",
|
|
created_at: nowIso(),
|
|
updated_at: nowIso(),
|
|
};
|
|
knowledgeBases.push(knowledgeBase);
|
|
documentsByKnowledgeBase.set(knowledgeBase.id, []);
|
|
sendJson(response, 200, { code: 0, data: knowledgeBase });
|
|
return;
|
|
}
|
|
|
|
if (request.method === "POST") {
|
|
const uploadMatch = request.url?.match(/^\/api\/v0\.2\/knowledge-bases\/([^/?]+)\/documents$/);
|
|
if (uploadMatch) {
|
|
const knowledgeBaseId = decodeURIComponent(uploadMatch[1]);
|
|
const buffer = await readBuffer(request);
|
|
const file = parseMultipartFile(buffer, request.headers["content-type"]);
|
|
const items = documentsByKnowledgeBase.get(knowledgeBaseId) || [];
|
|
const document = {
|
|
id: `kbdoc-${knowledgeBaseId}-${items.length + 1}`,
|
|
knowledge_base_id: knowledgeBaseId,
|
|
file_name: file.fileName,
|
|
mime_type: file.mimeType,
|
|
file_size: file.fileSize,
|
|
status: "indexed",
|
|
content_excerpt: "Pilot demo knowledge document indexed by local shared-ai stub.",
|
|
error_message: null,
|
|
extracted_text_length: file.fileSize,
|
|
preview_truncated: false,
|
|
current_stage: "indexed",
|
|
current_stage_label: "Indexed",
|
|
status_detail: "Document indexed",
|
|
parse_status: "parsed",
|
|
index_status: "indexed",
|
|
failure_reason: null,
|
|
next_action: "ready",
|
|
retryable: false,
|
|
created_at: nowIso(),
|
|
updated_at: nowIso(),
|
|
};
|
|
items.push(document);
|
|
documentsByKnowledgeBase.set(knowledgeBaseId, items);
|
|
const knowledgeBase = knowledgeBases.find((item) => item.id === knowledgeBaseId);
|
|
if (knowledgeBase) {
|
|
knowledgeBase.document_count = items.length;
|
|
knowledgeBase.updated_at = nowIso();
|
|
}
|
|
sendJson(response, 200, { code: 0, data: document });
|
|
return;
|
|
}
|
|
}
|
|
|
|
sendJson(response, 404, { code: 404, message: "not found" });
|
|
} catch (error) {
|
|
sendJson(response, 500, { code: 500, message: error.message });
|
|
}
|
|
});
|
|
|
|
server.listen(port, "127.0.0.1", () => {
|
|
console.log(`slice4 shared-ai stub listening on http://127.0.0.1:${port}`);
|
|
});
|