Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15,273 changes: 15,273 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@ai-sdk/react": "^3.0.177",
"@effect/platform": "^0.96.1",
"@neondatabase/serverless": "^1.1.0",
"@ontos-ai/knowhere-sdk": "^0.4.0",
"@ontos-ai/knowhere-sdk": "^0.6.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/components/chat-message-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ function getCitationDisplayKey(
]);
}

function getTrimmedCitationField(value: string | undefined): string | null {
function getTrimmedCitationField(value: string | null | undefined): string | null {
const trimmedValue = value?.trim() ?? "";
return trimmedValue.length > 0 ? trimmedValue : null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/chat-panel-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function getCitationDetail(
}

function normalizeCitationLabelPart(
value: string | undefined,
value: string | null | undefined,
sourceName?: string,
): string | undefined {
if (!value) return undefined;
Expand Down
7 changes: 4 additions & 3 deletions src/domains/chat/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { RetrievalQueryParams, RetrievalResult } from "@ontos-ai/knowhere-sdk"
import type { RetrievalQueryParams, RetrievalQueryResponse } from "@ontos-ai/knowhere-sdk"

import type { Source } from "@/infrastructure/db/schema"
import type { ChatCitationView } from "@/domains/chat/types"

export type RetrievalClient = {
query(params: RetrievalQueryParams): Promise<{ results: RetrievalResult[] }>
query(params: RetrievalQueryParams): Promise<RetrievalQueryResponse>
}

export type ChatHistoryMessage = {
Expand All @@ -24,7 +24,7 @@ export type GenerateAnswer = (input: {
question: string
retrievalQuery: string
messages: readonly ChatHistoryMessage[]
results: readonly RetrievalResult[]
evidenceText: string
}) => Promise<string>

export type AnswerQuestionInput = {
Expand All @@ -42,3 +42,4 @@ export type AnswerQuestionResult = {
answer: string
citations: ChatCitationView[]
}

94 changes: 53 additions & 41 deletions src/domains/chat/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ describe("answerQuestionWithRetrieval", () => {
const retrieval = {
query: vi.fn().mockResolvedValue({
results: [makeRetrievalResult()],
evidenceText: "Grounding content from evidence tree",
referencedChunks: [],
namespace: "notebook-workspace",
query: "What does the document say?",
routerUsed: "workflow_single_step",
answerText: null,
}),
};
const generateAnswer = vi.fn().mockResolvedValue("The answer is grounded.");
Expand Down Expand Up @@ -54,13 +60,14 @@ describe("answerQuestionWithRetrieval", () => {
namespace: "notebook-workspace",
query: "What does the document say?",
topK: 8,
useAgentic: true,
excludeDocumentIds: ["doc_excluded"],
});
expect(generateAnswer).toHaveBeenCalledWith({
question: "What does the document say?",
retrievalQuery: "What does the document say?",
messages: [],
results: [makeRetrievalResult()],
evidenceText: "Grounding content from evidence tree",
});
expect(answer).toEqual({
answer: "The answer is grounded.",
Expand All @@ -87,6 +94,12 @@ describe("answerQuestionWithRetrieval", () => {
const retrieval = {
query: vi.fn().mockResolvedValue({
results: [firstResult, secondResult],
evidenceText: "Revenue grew. Gross margin improved.",
referencedChunks: [],
namespace: "notebook-workspace",
query: "What improved?",
routerUsed: "workflow_single_step",
answerText: null,
}),
};
const generateAnswer = vi
Expand Down Expand Up @@ -124,7 +137,15 @@ describe("answerQuestionWithRetrieval", () => {
},
});
const retrieval = {
query: vi.fn().mockResolvedValue({ results: [result] }),
query: vi.fn().mockResolvedValue({
results: [result],
evidenceText: "Tesla invested in xAI.",
referencedChunks: [],
namespace: "notebook-workspace",
query: "Tesla xAI investment",
routerUsed: "workflow_single_step",
answerText: null,
}),
};
const generateAnswer = vi
.fn()
Expand All @@ -149,27 +170,35 @@ describe("answerQuestionWithRetrieval", () => {
}),
);

expect(generateAnswer).toHaveBeenCalledWith({
question: "What does the document say about xAI?",
retrievalQuery: "Tesla xAI investment",
messages: [],
evidenceText: "Tesla invested in xAI.",
});
const expectedResult = {
...result,
source: {
...result.source,
sourceFileName: "TSLA-Q4-2025-Update.pdf",
},
};
expect(generateAnswer).toHaveBeenCalledWith({
question: "What does the document say about xAI?",
retrievalQuery: "Tesla xAI investment",
messages: [],
results: [expectedResult],
});
expect(answer.citations).toEqual([
{ ...expectedResult, description: "xAI investment" },
]);
});

it("returns a deterministic no-results answer without calling the model", async () => {
const retrieval = {
query: vi.fn().mockResolvedValue({ results: [] }),
query: vi.fn().mockResolvedValue({
results: [],
evidenceText: "",
referencedChunks: [],
namespace: "notebook-workspace",
query: "Missing fact?",
routerUsed: "workflow_single_step",
answerText: null,
}),
};
const generateAnswer = vi.fn();
const generateRetrievalQuery = vi.fn().mockResolvedValue("Missing fact?");
Expand Down Expand Up @@ -198,6 +227,12 @@ describe("answerQuestionWithRetrieval", () => {
const retrieval = {
query: vi.fn().mockResolvedValue({
results: [makeRetrievalResult()],
evidenceText: "Energy storage deployments grew significantly.",
referencedChunks: [],
namespace: "notebook-workspace",
query: "Tesla Q4 2025 Update energy generation and storage deployments",
routerUsed: "workflow_single_step",
answerText: null,
}),
};
const generateRetrievalQuery = vi
Expand Down Expand Up @@ -234,21 +269,14 @@ describe("answerQuestionWithRetrieval", () => {
namespace: "notebook-workspace",
query: "Tesla Q4 2025 Update energy generation and storage deployments",
topK: 8,
useAgentic: true,
});
expect(generateAnswer).toHaveBeenCalledWith({
question: "What about energy storage in this document?",
retrievalQuery:
"Tesla Q4 2025 Update energy generation and storage deployments",
messages,
results: [
makeRetrievalResult({
source: {
documentId: "doc_included",
sourceFileName: "TSLA-Q4-2025-Update.pdf",
sectionPath: "Intro",
},
}),
],
evidenceText: "Energy storage deployments grew significantly.",
});
});
});
Expand Down Expand Up @@ -303,11 +331,7 @@ describe("generateGroundedAnswer", () => {
question: "What is PR-E?",
retrievalQuery: "PR-E retrieval",
messages: [],
results: [
makeRetrievalResult({
content: "PR-E wires chat to Knowhere retrieval.",
}),
],
evidenceText: "PR-E wires chat to Knowhere retrieval.",
});

expect(generateText).toHaveBeenCalledWith({
Expand All @@ -319,39 +343,27 @@ describe("generateGroundedAnswer", () => {
});

describe("buildGroundedPrompt", () => {
it("includes retrieved source content and forbids unsupported answers", () => {
it("includes evidence text and uses evidence-based citation format", () => {
const prompt = buildGroundedPrompt({
question: "What is PR-E?",
results: [
makeRetrievalResult({
content: "PR-E wires chat to Knowhere retrieval.",
source: {
documentId: "doc_1",
sourceFileName: "requirements.txt",
sectionPath: "N-005",
},
}),
],
evidenceText: "PR-E wires chat to Knowhere retrieval.\n[Document] requirements.txt\n▸ [L1] N-005",
});

expect(prompt).toContain("What is PR-E?");
expect(prompt).toContain("Retrieval query used: What is PR-E?");
expect(prompt).toContain("PR-E wires chat to Knowhere retrieval.");
expect(prompt).toContain("requirements.txt");
expect(prompt).toContain(
"Use the retrieved source excerpts as helpful context, not as the only allowed information.",
"Use the retrieved evidence as your primary context.",
);
expect(prompt).not.toContain("grounded only in the sources");
expect(prompt).toContain("Retrieved evidence:");
expect(prompt).not.toContain("Source excerpts:");
});

it("asks the model to answer naturally and directly", () => {
const prompt = buildGroundedPrompt({
question: "How about the TBD?",
results: [
makeRetrievalResult({
content: "Roadster location: TBD. Status: Design development.",
}),
],
evidenceText: "Roadster location: TBD. Status: Design development.",
});

expect(prompt).toContain("Answer in a natural, friendly, and direct tone.");
Expand Down
7 changes: 5 additions & 2 deletions src/domains/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ export const answerQuestionWithRetrieval = (
namespace: input.namespace,
query,
topK: DEFAULT_TOP_K,
useAgentic: true,
...excludeDocuments(input.sources, input.excludedSourceIds),
}),
)

if (response.results.length === 0) {
const evidenceText = response.evidenceText ?? ""
if (response.results.length === 0 && !evidenceText) {
return { answer: NO_RESULTS_ANSWER, citations: [] as ChatCitationView[] }
}

Expand All @@ -69,11 +71,12 @@ export const answerQuestionWithRetrieval = (
question,
retrievalQuery: query,
messages: input.messages,
results,
evidenceText,
}),
)
return {
answer,
citations: toChatCitationViews(results, answer),
}
})

27 changes: 7 additions & 20 deletions src/domains/chat/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { RetrievalResult } from "@ontos-ai/knowhere-sdk"
import { generateText } from "ai"
import { Effect } from "effect"

Expand All @@ -23,14 +22,14 @@ type GenerateGroundedAnswerInput = {
question: string
retrievalQuery: string
messages: readonly ChatHistoryMessage[]
results: readonly RetrievalResult[]
evidenceText: string
}

type BuildGroundedPromptInput = {
question: string
retrievalQuery?: string
messages?: readonly ChatHistoryMessage[]
results: readonly RetrievalResult[]
evidenceText: string
}

export const generateContextualRetrievalQueryEffect = (
Expand Down Expand Up @@ -127,40 +126,28 @@ export function buildRetrievalQueryPrompt(
export function buildGroundedPrompt(input: BuildGroundedPromptInput): string {
const retrievalQuery = input.retrievalQuery?.trim() || input.question
const conversationContext = formatConversationContext(input.messages ?? [])
const sources = input.results
.map((result, index) => {
const sourceName = result.source.sourceFileName ?? "Unknown source"
const section = result.source.sectionPath
? ` (${result.source.sectionPath})`
: ""
return [
`[${index + 1}] ${sourceName}${section}`,
result.content,
].join("\n")
})
.join("\n\n")

return [
"You answer user questions.",
"Use the retrieved source excerpts as helpful context, not as the only allowed information.",
"Cite a source when it supports a claim.",
"Use the retrieved evidence as your primary context.",
"Cite document sections (e.g. [文档名 / 章节名]) when they support a claim.",
"If the sources are related but incomplete, answer what you can and briefly say what is not covered.",
"Do not invent document-specific facts that are not in the sources.",
"Use the recent conversation only to resolve references like \"this document\"; do not use it as factual evidence.",
"Answer in a natural, friendly, and direct tone.",
"Start with the answer first. Avoid meta phrases like \"Based on the sources\" or \"Based on the source excerpts\" unless the user asks how you know.",
"Use plain language.",
"Keep answers concise by default: 1-3 short paragraphs unless the user asks for detail.",
"CITATION FORMAT: After each sourced statement include a brief citation label like [Source N: what the source says]. Use only the provided source numbers.",
"CITATION FORMAT: Cite evidence by document and section path, e.g. [文档名 / 章节名].",
"",
`Question: ${input.question}`,
`Retrieval query used: ${retrievalQuery}`,
"",
"Recent conversation:",
conversationContext,
"",
"Source excerpts:",
sources,
"Retrieved evidence:",
input.evidenceText,
].join("\n")
}

Expand Down
Loading
Loading