|
@@ -55,9 +55,11 @@ function stableUuid(seed) {
|
|
|
export async function ingestDocuments(documents) {
|
|
export async function ingestDocuments(documents) {
|
|
|
const collectionName = config.qdrant.collection;
|
|
const collectionName = config.qdrant.collection;
|
|
|
const allChunks = [];
|
|
const allChunks = [];
|
|
|
|
|
+ const ingestedAt = new Date().toISOString();
|
|
|
|
|
|
|
|
for (const doc of documents) {
|
|
for (const doc of documents) {
|
|
|
- const baseSeed = doc.id ?? `${doc.source ?? "doc"}:${doc.text.slice(0, 64)}`;
|
|
|
|
|
|
|
+ const docHash = createHash("sha256").update(doc.text).digest("hex");
|
|
|
|
|
+ const baseSeed = doc.id ?? docHash;
|
|
|
const chunks = chunkText(doc.text, {
|
|
const chunks = chunkText(doc.text, {
|
|
|
chunkSize: config.rag.chunkSize,
|
|
chunkSize: config.rag.chunkSize,
|
|
|
chunkOverlap: config.rag.chunkOverlap
|
|
chunkOverlap: config.rag.chunkOverlap
|
|
@@ -66,9 +68,11 @@ export async function ingestDocuments(documents) {
|
|
|
allChunks.push({
|
|
allChunks.push({
|
|
|
id: stableUuid(`${baseSeed}:${idx}`),
|
|
id: stableUuid(`${baseSeed}:${idx}`),
|
|
|
source: doc.source ?? null,
|
|
source: doc.source ?? null,
|
|
|
|
|
+ title: doc.title ?? doc.source ?? null,
|
|
|
metadata: doc.metadata ?? null,
|
|
metadata: doc.metadata ?? null,
|
|
|
chunkIndex: idx,
|
|
chunkIndex: idx,
|
|
|
- text: chunk
|
|
|
|
|
|
|
+ text: chunk,
|
|
|
|
|
+ documentHash: docHash
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -90,9 +94,12 @@ export async function ingestDocuments(documents) {
|
|
|
vector: vectors[idx],
|
|
vector: vectors[idx],
|
|
|
payload: {
|
|
payload: {
|
|
|
source: c.source,
|
|
source: c.source,
|
|
|
|
|
+ title: c.title,
|
|
|
chunkIndex: c.chunkIndex,
|
|
chunkIndex: c.chunkIndex,
|
|
|
text: c.text,
|
|
text: c.text,
|
|
|
- metadata: c.metadata
|
|
|
|
|
|
|
+ metadata: c.metadata,
|
|
|
|
|
+ documentHash: c.documentHash,
|
|
|
|
|
+ ingestedAt
|
|
|
}
|
|
}
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
@@ -104,6 +111,64 @@ export async function ingestDocuments(documents) {
|
|
|
return { upserted: points.length };
|
|
return { upserted: points.length };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function isPrivateUrl(urlStr) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { hostname } = new URL(urlStr);
|
|
|
|
|
+ return (
|
|
|
|
|
+ hostname === "localhost" ||
|
|
|
|
|
+ /^127\./.test(hostname) ||
|
|
|
|
|
+ /^10\./.test(hostname) ||
|
|
|
|
|
+ /^192\.168\./.test(hostname) ||
|
|
|
|
|
+ /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
|
|
|
|
|
+ hostname === "0.0.0.0" ||
|
|
|
|
|
+ hostname.endsWith(".local")
|
|
|
|
|
+ );
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function fetchUrlText(urlStr) {
|
|
|
|
|
+ if (!urlStr.startsWith("https://")) {
|
|
|
|
|
+ const err = new Error("url_must_be_https");
|
|
|
|
|
+ err.statusCode = 400;
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isPrivateUrl(urlStr)) {
|
|
|
|
|
+ const err = new Error("url_private_not_allowed");
|
|
|
|
|
+ err.statusCode = 400;
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const res = await fetch(urlStr, {
|
|
|
|
|
+ headers: { "User-Agent": "Mozilla/5.0 star-oraculo/1.0" },
|
|
|
|
|
+ redirect: "follow",
|
|
|
|
|
+ signal: AbortSignal.timeout(15_000)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ const err = new Error(`url_fetch_error:${res.status}`);
|
|
|
|
|
+ err.statusCode = 502;
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const html = await res.text();
|
|
|
|
|
+ const text = html
|
|
|
|
|
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, " ")
|
|
|
|
|
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, " ")
|
|
|
|
|
+ .replace(/<[^>]+>/g, " ")
|
|
|
|
|
+ .replace(/ /g, " ")
|
|
|
|
|
+ .replace(/&/g, "&")
|
|
|
|
|
+ .replace(/</g, "<")
|
|
|
|
|
+ .replace(/>/g, ">")
|
|
|
|
|
+ .replace(/"/g, '"')
|
|
|
|
|
+ .replace(/'/g, "'")
|
|
|
|
|
+ .replace(/\s+/g, " ")
|
|
|
|
|
+ .trim();
|
|
|
|
|
+
|
|
|
|
|
+ return text;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function guessFileKind({ mimeType, filename }) {
|
|
function guessFileKind({ mimeType, filename }) {
|
|
|
const name = String(filename ?? "").toLowerCase();
|
|
const name = String(filename ?? "").toLowerCase();
|
|
|
const mt = String(mimeType ?? "").toLowerCase();
|
|
const mt = String(mimeType ?? "").toLowerCase();
|