Files
ai-contract-platform/frontend/scripts/slice4-shared-ai-stub.mjs
T

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}`);
});