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/app/api/source-uploads/blob/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe("POST /api/source-uploads/blob", () => {
mocks.getCurrentUser.mockResolvedValue({ id: "user_1" });
});

it("generates a public client-upload token capped at the 100 MB document limit", async () => {
it("generates a public client-upload token capped at the document upload limit", async () => {
mocks.handleUpload.mockImplementation(async (options) => {
const tokenOptions = await options.onBeforeGenerateToken(
"source-uploads/upload_1/document.pdf",
Expand Down
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
4 changes: 3 additions & 1 deletion src/components/chunks-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import { useChunksPanelWorkflow } from "@/components/chunks-panel-workflow";
import { ParsedChunkCard } from "@/components/parsed-chunk-card";
import { chunksPanelState } from "@/components/chunks-panel-state";
import { MAX_UPLOAD_MB } from "@/domains/sources/validation";
import { useSourceOriginalPreviewWarmup } from "@/components/source-original-preview-warmup";
import { sourceOriginalPreviewModel } from "@/components/source-original-preview-model";
import type { ParsedChunkView } from "@/domains/chunks/types";
Expand Down Expand Up @@ -1006,7 +1007,8 @@ function EmptySourceUploadState({
parsed chunks and chat.
</span>
<span className="rounded-md border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-muted-foreground shadow-sm">
PDF, DOCX, TXT, MD, spreadsheets, slides, and images up to 100 MB
PDF, DOCX, TXT, MD, spreadsheets, slides, and images up to{" "}
{MAX_UPLOAD_MB} MB
</span>
</button>
)}
Expand Down
6 changes: 4 additions & 2 deletions src/components/source-upload-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "@/components/ui/dialog";
import { useSourceUploadDialogWorkflow } from "@/components/source-upload-dialog-workflow";
import type { SourceView } from "@/domains/sources/types";
import { MAX_UPLOAD_MB } from "@/domains/sources/validation";

export type SourceUploadDialogProps = {
readonly onSourceUploaded?: (source: SourceView) => void;
Expand Down Expand Up @@ -84,7 +85,8 @@ export function SourceUploadDialog({
<DialogTitle>Add source</DialogTitle>
<DialogDescription>
Add a document to your notebook. Notebook accepts PDF, DOC, DOCX,
TXT, MD, XLS, XLSX, PPTX, images, and more files up to 100 MB.
TXT, MD, XLS, XLSX, PPTX, images, and more files up to{" "}
{MAX_UPLOAD_MB} MB.
</DialogDescription>
</DialogHeader>
<form
Expand Down Expand Up @@ -117,7 +119,7 @@ export function SourceUploadDialog({
Click to select or drag and drop a document
</p>
<p className="mt-2 rounded-md border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-muted-foreground shadow-sm">
Max size: 100 MB
Max size: {MAX_UPLOAD_MB} MB
</p>
{selectedFileName && (
<p className="mt-3 max-w-full truncate text-xs font-medium text-foreground">
Expand Down
15 changes: 7 additions & 8 deletions src/components/sources-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { SourcesPanel } from "./sources-panel";

const C = SourcesPanel as React.FC<Record<string, unknown>>;
const originalResizeObserver = globalThis.ResizeObserver;
const fileTooLargeMessage =
"File is too large. Upload a document up to 300 MB. " +
"For larger files, contact team@knowhereto.ai or open an issue at https://github.com/Ontos-AI/knowhere/issues.";

describe("SourcesPanel", () => {
beforeEach(() => {
Expand Down Expand Up @@ -131,10 +134,10 @@ describe("SourcesPanel", () => {

expect(
screen.getByText(
/Notebook accepts PDF, DOC, DOCX, TXT, MD, XLS, XLSX, PPTX, images, and more files up to 100 MB/,
/Notebook accepts PDF, DOC, DOCX, TXT, MD, XLS, XLSX, PPTX, images, and more files up to 300 MB/,
),
).toBeTruthy();
expect(screen.getByText("Max size: 100 MB")).toBeTruthy();
expect(screen.getByText("Max size: 300 MB")).toBeTruthy();
expect(opened.container.textContent).not.toMatch(/Knowhere|parsing|indexing/i);
});

Expand Down Expand Up @@ -383,7 +386,7 @@ describe("SourcesPanel", () => {
}

return Response.json(
{ message: "File is too large. Upload a document up to 100 MB." },
{ message: fileTooLargeMessage },
{ status: 400 },
);
});
Expand All @@ -407,11 +410,7 @@ describe("SourcesPanel", () => {
}
fireEvent.submit(form);

expect(
await screen.findByText(
"File is too large. Upload a document up to 100 MB.",
),
).toBeTruthy();
expect(await screen.findByText(fileTooLargeMessage)).toBeTruthy();
expect(screen.getByRole("button", { name: "Confirm" })).toBeTruthy();
});
});
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
Loading
Loading