local-ai-platform/src/answer.ts
Brandon Wolfgang c2b47e7645 feat: complete Phase 3 retrieval-augmented generation pipeline
- 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
2026-10-22 15:17:02 -05:00

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