- Add document ingestion with text chunking, embedding generation, and PostgreSQL persistence - Configure PostgreSQL with pgvector, document schemas, and database migrations - Implement semantic search using Ollama embeddings and pgvector cosine similarity - Add POST /documents, POST /search, and POST /ask endpoints with validation and sanitized error handling - Generate context-grounded answers with source attribution - Filter retrieved chunks using a minimum similarity threshold of 0.5 and return fallback responses when no relevant context is available - Add unit and integration tests for ingestion, search, question answering, validation, and failure handling - Update README with Phase 3 capabilities, setup instructions, and API documentation
74 lines
1.8 KiB
TypeScript
74 lines
1.8 KiB
TypeScript
import { generateResponse } from "./ollama.js";
|
|
import { searchDocuments, type SearchResult } from "./search.js";
|
|
|
|
export interface AnswerResult {
|
|
answer: string;
|
|
sources: {
|
|
documentId: string;
|
|
title: string;
|
|
content: string;
|
|
similarity: number;
|
|
}[];
|
|
}
|
|
|
|
interface AnswerDependencies {
|
|
search: (query: string, limit?: number) => Promise<SearchResult[]>;
|
|
generate: (prompt: string) => Promise<string>;
|
|
}
|
|
|
|
const defaultDependencies: AnswerDependencies = {
|
|
search: searchDocuments,
|
|
generate: generateResponse,
|
|
};
|
|
|
|
export const answerQuestion = async (
|
|
question: string,
|
|
dependencies: AnswerDependencies = defaultDependencies,
|
|
): Promise<AnswerResult> => {
|
|
const normalizedQuestion = question.trim();
|
|
|
|
if (!normalizedQuestion) {
|
|
throw new Error("Question cannot be empty");
|
|
}
|
|
|
|
const results = await dependencies.search(normalizedQuestion, 5);
|
|
const relevantResults = results.filter((result) => result.similarity >= 0.5);
|
|
|
|
if (relevantResults.length === 0) {
|
|
return {
|
|
answer:
|
|
"I couldn't find relevant information in the available documents.",
|
|
sources: [],
|
|
};
|
|
}
|
|
|
|
const context = relevantResults
|
|
.map(
|
|
(result, index) =>
|
|
`[Source ${index + 1}: ${result.title}]\n${result.content}`,
|
|
)
|
|
.join("\n\n");
|
|
|
|
const prompt = `Answer the question using only the context provided below.
|
|
If the context does not contain the answer, say that you don't have enough information.
|
|
Do not invent facts or claim that the context supports something it does not.
|
|
|
|
Context:
|
|
${context}
|
|
|
|
Question: ${normalizedQuestion}
|
|
|
|
Answer:`;
|
|
|
|
const answer = await dependencies.generate(prompt);
|
|
|
|
return {
|
|
answer,
|
|
sources: relevantResults.map((result) => ({
|
|
documentId: result.documentId,
|
|
title: result.title,
|
|
content: result.content,
|
|
similarity: result.similarity,
|
|
})),
|
|
};
|
|
};
|