mirror of
https://github.com/docmost/docmost.git
synced 2026-08-25 15:57:11 +08:00
* feat(ai): add AI_VECTOR_DRIVER and turbopuffer configuration * feat(ai): carry workspace and target space ids on vector lifecycle events * feat(ai): add vector driver interface and turbopuffer request helpers * feat(ai): add pgvector driver behind the vector driver interface * refactor(ai): route vector reads and writes through the vector driver * feat(ai): add turbopuffer vector driver * feat(ai): rebuild turbopuffer namespaces when the embedding model changes * feat(ai): warm the vector namespace cache on session start * fix(ai): harden turbopuffer misconfiguration and reset failure paths * fix(ai): collapse blank-line runs in extracted page text * fix(ai): skip full re-embed when ai search is re-enabled within the delete grace window * sync * fix(ai): store real embedding dimensions instead of serialized vector length * fix(ai): filter search hits by the page's current space at query time * fix(ai): retry the page moved-to-space vector patch job * sync * feat(ai): pre-warm the vector namespace * fix(ai): pass AI_VECTOR_DRIVER through the client build config
92 lines
2.2 KiB
TypeScript
92 lines
2.2 KiB
TypeScript
import api from "@/lib/api-client.ts";
|
|
import { IPageSearchParams } from "@/features/search/types/search.types.ts";
|
|
|
|
export interface IAiSearchResponse {
|
|
answer: string;
|
|
sources?: Array<{
|
|
pageId: string;
|
|
title: string;
|
|
slugId: string;
|
|
spaceSlug: string;
|
|
similarity: number;
|
|
distance: number;
|
|
chunkIndex: number;
|
|
excerpt: string;
|
|
}>;
|
|
}
|
|
|
|
export async function hintVectorCache(): Promise<void> {
|
|
try {
|
|
await api.post("/ai/vector-cache-hint");
|
|
} catch {
|
|
// best-effort cache hint
|
|
}
|
|
}
|
|
|
|
export async function aiAnswers(
|
|
params: IPageSearchParams,
|
|
onChunk?: (chunk: { content?: string; sources?: any[] }) => void,
|
|
): Promise<IAiSearchResponse> {
|
|
const response = await fetch("/api/ai/answers", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
credentials: "include",
|
|
body: JSON.stringify(params),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const reader = response.body?.getReader();
|
|
const decoder = new TextDecoder();
|
|
|
|
let answer = "";
|
|
let sources: any[] = [];
|
|
let buffer = "";
|
|
|
|
if (reader) {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split("\n");
|
|
|
|
// Keep the last incomplete line in the buffer
|
|
buffer = lines.pop() || "";
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith("data: ")) {
|
|
const data = line.slice(6);
|
|
if (data === "[DONE]") break;
|
|
|
|
try {
|
|
const parsed = JSON.parse(data);
|
|
if (parsed.error) {
|
|
throw new Error(parsed.error);
|
|
}
|
|
if (parsed.content) {
|
|
answer += parsed.content;
|
|
onChunk?.({ content: parsed.content });
|
|
}
|
|
if (parsed.sources) {
|
|
sources = parsed.sources;
|
|
onChunk?.({ sources: parsed.sources });
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
throw e;
|
|
}
|
|
// Skip invalid JSON
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { answer, sources };
|
|
}
|