Skip to content
Open
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
106 changes: 106 additions & 0 deletions src/query/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,112 @@ it("search-only mode defaults filtering to recall", async () => {
expect(capturedBodies[0].filtering).toBeUndefined();
});

it("search-only mode sends ranking_instructions verbatim and persists through pagination", async () => {
const mockClient = {
getConnectionDetails: jest.fn().mockResolvedValue({
host: "test-cluster",
bearerToken: "test-token",
headers: { "X-Provider": "test-key" },
}),
} as unknown as WeaviateClient;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const capturedBodies: any[] = [];

const apiSuccess: ApiSearchModeResponse = {
searches: [
{
query: "search query",
collection: "test_collection",
},
],
usage: {
model_units: 1,
usage_in_plan: true,
remaining_plan_requests: 2,
},
total_time: 1.0,
search_results: { objects: [] },
};

global.fetch = jest.fn((url, init?: RequestInit) => {
if (init && init.body) {
capturedBodies.push(JSON.parse(init.body as string));
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(apiSuccess),
} as Response);
}) as jest.Mock;

const agent = new QueryAgent(mockClient);

const instructions = "Prioritize the most recent documents.";
const first = await agent.search("test query", {
collections: ["test_collection"],
rankingInstructions: instructions,
});

// First (generation) request should include the instructions verbatim
expect(capturedBodies[0].ranking_instructions).toBe(instructions);

// Paginated (execution) request should also include them
await first.next({ limit: 20, offset: 1 });
expect(capturedBodies[1].ranking_instructions).toBe(instructions);
});

it("search-only mode sends null ranking_instructions when not provided", async () => {
const mockClient = {
getConnectionDetails: jest.fn().mockResolvedValue({
host: "test-cluster",
bearerToken: "test-token",
headers: { "X-Provider": "test-key" },
}),
} as unknown as WeaviateClient;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const capturedBodies: any[] = [];

const apiSuccess: ApiSearchModeResponse = {
searches: [
{
query: "search query",
collection: "test_collection",
},
],
usage: {
model_units: 1,
usage_in_plan: true,
remaining_plan_requests: 2,
},
total_time: 1.0,
search_results: { objects: [] },
};

global.fetch = jest.fn((url, init?: RequestInit) => {
if (init && init.body) {
capturedBodies.push(JSON.parse(init.body as string));
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(apiSuccess),
} as Response);
}) as jest.Mock;

const agent = new QueryAgent(mockClient);

const first = await agent.search("test query", {
collections: ["test_collection"],
});

// The server treats null and absent identically; the client must not invent
// a default (e.g. empty string, which is semantically different server-side)
expect(capturedBodies[0].ranking_instructions).toBeNull();

await first.next({ limit: 20, offset: 1 });
expect(capturedBodies[1].ranking_instructions).toBeNull();
});

it("search-only mode caches empty searches array for precision mode pagination", async () => {
const mockClient = {
getConnectionDetails: jest.fn().mockResolvedValue({
Expand Down
12 changes: 12 additions & 0 deletions src/query/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,10 @@ export class QueryAgent {
* Defaults to no diversity.
* @param options.filtering - The filtering strategy: `"recall"` for broader retrieval or
* `"precision"` for more targeted results. Defaults to `"recall"`.
* @param options.rankingInstructions - Optional natural language instructions for the
* instruction-following reranker, guiding which results to prioritize as most relevant.
* Only affects the ordering of results, not which results are retrieved. Limited to
* roughly 500 tokens, enforced server-side.
* @returns A {@link SearchModeResponse} for the first page of results. Use
* `response.next({ limit, offset })` to paginate.
*
Expand All @@ -624,6 +628,7 @@ export class QueryAgent {
collections,
diversityWeight,
filtering,
rankingInstructions,
}: QueryAgentSearchOnlyOptions = {},
): Promise<SearchModeResponse> {
const searcher = new QueryAgentSearcher(
Expand All @@ -634,6 +639,7 @@ export class QueryAgent {
this.agentsHost,
diversityWeight,
filtering,
rankingInstructions,
);

return searcher.run({ limit, offset: 0 });
Expand Down Expand Up @@ -847,6 +853,12 @@ export type QueryAgentSearchOnlyOptions = {
* (narrower, more targeted retrieval).
*/
filtering?: "recall" | "precision";
/**
* Optional natural language instructions for the instruction-following reranker, guiding
* which results to prioritize as most relevant. Only affects the ordering of results, not
* which results are retrieved. Limited to roughly 500 tokens, enforced server-side.
*/
rankingInstructions?: string;
};

/** Options for {@link QueryAgent.suggestQueries}. */
Expand Down
2 changes: 2 additions & 0 deletions src/query/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class QueryAgentSearcher {
private agentsHost: string,
private diversityWeight: number | undefined,
private filtering: "recall" | "precision" | undefined,
private rankingInstructions: string | undefined,
) {}

private buildRequestBody(
Expand All @@ -43,6 +44,7 @@ export class QueryAgentSearcher {
collections: mapCollections(this.collections),
limit,
offset,
ranking_instructions: this.rankingInstructions ?? null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

additional null conversion doesn't seem necessary

} as const;
if (this.cachedSearches === undefined) {
return {
Expand Down
Loading