- corpus/: Almeida (João Ferreira de Almeida) em USFX, domínio público, 66 livros / 1189 capítulos / 31098 versículos, versionado com SHA-256 - app/corpus.py: parser USFX (texto no tail de <v/>), nomes canônicos dos livros, aliases (abreviaturas e numerais romanos), VerseChunk sem página - indexer: descobre *.xml em CORPUS_DIR, progresso em versículos, lote configurável, manifesto incremental por SHA-256 - remove extraction.py, parallel.py, bible_parser.py, Tesseract e PyMuPDF - campo page removido de Source/SearchResult/StatusResponse e do frontend - embedder: propriedade name; status reporta o embedder em uso, não o do .env - create_app constrói o embedder ANTES do store (o default HashEmbedder do VectorStore impedia o modelo configurado de ser carregado em produção) - fallback do modelo agora aparece em /api/status.errors - torch 2.6.0 + transformers 5.22.1 pinados (transformers 5.x exige torch>=2.6)
89 lines
2.1 KiB
TypeScript
89 lines
2.1 KiB
TypeScript
import { parseSSE, type SSEEvent } from "./lib/sse";
|
|
|
|
export interface Source {
|
|
reference: string;
|
|
text: string;
|
|
book: string;
|
|
chapter: number;
|
|
verse: number;
|
|
file: string;
|
|
score: number;
|
|
}
|
|
|
|
export interface Status {
|
|
state: "ready" | "indexing" | "error";
|
|
sources_total: number;
|
|
sources_indexed: number;
|
|
chunks: number;
|
|
model: string;
|
|
last_indexed_at: string | null;
|
|
errors: string[];
|
|
verses_indexed: number;
|
|
verses_total: number;
|
|
current_file: string | null;
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
role: "user" | "assistant";
|
|
content: string;
|
|
}
|
|
|
|
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "";
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const response = await fetch(`${API_BASE}${path}`, {
|
|
headers: { "Content-Type": "application/json" },
|
|
...init,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let detail = `Erro ${response.status}`;
|
|
try {
|
|
const body = (await response.json()) as { detail?: string };
|
|
if (body.detail) {
|
|
detail = body.detail;
|
|
}
|
|
} catch {
|
|
/* resposta sem corpo JSON */
|
|
}
|
|
throw new Error(detail);
|
|
}
|
|
|
|
return (await response.json()) as T;
|
|
}
|
|
|
|
export function fetchStatus(): Promise<Status> {
|
|
return request<Status>("/api/status");
|
|
}
|
|
|
|
export function reindex(): Promise<{ state: string }> {
|
|
return request<{ state: string }>("/api/reindex", { method: "POST" });
|
|
}
|
|
|
|
/** Envia a mensagem e devolve os eventos do stream. */
|
|
export async function* sendMessage(
|
|
message: string,
|
|
history: ChatMessage[],
|
|
): AsyncGenerator<SSEEvent> {
|
|
const response = await fetch(`${API_BASE}/api/chat`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ message, history }),
|
|
});
|
|
|
|
if (!response.ok || !response.body) {
|
|
let detail = `Erro ${response.status}`;
|
|
try {
|
|
const body = (await response.json()) as { detail?: string };
|
|
if (body.detail) {
|
|
detail = body.detail;
|
|
}
|
|
} catch {
|
|
/* resposta sem corpo JSON */
|
|
}
|
|
throw new Error(detail);
|
|
}
|
|
|
|
yield* parseSSE(response.body);
|
|
}
|