From d9c371da9642b72a3c59d3c5337ac2025e41ccaf Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Tue, 19 Aug 2025 13:03:27 +0100 Subject: [PATCH 01/20] Add new types for search only mode --- src/query/response/api-response.ts | 10 ++++++++ src/query/response/response-mapping.ts | 33 +++++++++++++++++++------- src/query/response/response.ts | 10 ++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/query/response/api-response.ts b/src/query/response/api-response.ts index bf420e7..ca1d922 100644 --- a/src/query/response/api-response.ts +++ b/src/query/response/api-response.ts @@ -1,3 +1,5 @@ +import { WeaviateReturn } from "weaviate-client"; + import { NumericMetrics, TextMetrics, @@ -177,3 +179,11 @@ export type ApiSource = { object_id: string; collection: string; }; + +export type ApiSearchModeResponse = { + original_query: string; + searches?: ApiSearchResult[]; + usage: ApiUsage; + total_time: number; + search_results: WeaviateReturn; +}; diff --git a/src/query/response/response-mapping.ts b/src/query/response/response-mapping.ts index 6cb4e35..d577c2b 100644 --- a/src/query/response/response-mapping.ts +++ b/src/query/response/response-mapping.ts @@ -9,6 +9,7 @@ import { StreamedTokens, ProgressMessage, DateFilterValue, + SearchModeResponse, } from "./response.js"; import { @@ -20,6 +21,7 @@ import { ApiUsage, ApiSource, ApiDateFilterValue, + ApiSearchModeResponse, } from "./api-response.js"; import { ServerSentEvent } from "./server-sent-events.js"; @@ -47,15 +49,16 @@ export const mapResponse = ( }; }; +const mapInnerSearches = (searches: ApiSearchResult[]): SearchResult[] => + searches.map((result) => ({ + collection: result.collection, + queries: result.queries, + filters: result.filters.map(mapPropertyFilters), + filterOperators: result.filter_operators, + })); + const mapSearches = (searches: ApiSearchResult[][]): SearchResult[][] => - searches.map((searchGroup) => - searchGroup.map((result) => ({ - collection: result.collection, - queries: result.queries, - filters: result.filters.map(mapPropertyFilters), - filterOperators: result.filter_operators, - })), - ); + searches.map((searchGroup) => mapInnerSearches(searchGroup)); const mapDatePropertyFilter = ( filterValue: ApiDateFilterValue, @@ -298,3 +301,17 @@ export const mapResponseFromSSE = ( display: () => display(properties), }; }; + +export const mapSearchOnlyResponse = ( + response: ApiSearchModeResponse, +): { mappedResponse: SearchModeResponse, apiSearches: ApiSearchResult[] | undefined } => { + const apiSearches = response.searches; + const mappedResponse: SearchModeResponse = { + originalQuery: response.original_query, + searches: apiSearches ? mapInnerSearches(apiSearches) : undefined, + usage: mapUsage(response.usage), + totalTime: response.total_time, + searchResults: response.search_results, + }; + return {mappedResponse, apiSearches}; +}; diff --git a/src/query/response/response.ts b/src/query/response/response.ts index e0400f7..53544fb 100644 --- a/src/query/response/response.ts +++ b/src/query/response/response.ts @@ -1,3 +1,5 @@ +import { WeaviateReturn } from "weaviate-client"; + export type QueryAgentResponse = { outputType: "finalState"; originalQuery: string; @@ -260,3 +262,11 @@ export type StreamedTokens = { outputType: "streamedTokens"; delta: string; }; + +export type SearchModeResponse = { + originalQuery: string; + searches?: SearchResult[]; + usage: Usage; + totalTime: number; + searchResults: WeaviateReturn; +}; From 21fb90d79a2519a073c110cb311ff26853124850 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Tue, 19 Aug 2025 13:03:58 +0100 Subject: [PATCH 02/20] Add search-only executor class --- src/query/index.ts | 1 + src/query/search.ts | 114 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/query/search.ts diff --git a/src/query/index.ts b/src/query/index.ts index 3ae6fd3..abb4b6c 100644 --- a/src/query/index.ts +++ b/src/query/index.ts @@ -1,3 +1,4 @@ export * from "./agent.js"; export { QueryAgentCollectionConfig } from "./collection.js"; export * from "./response/index.js"; +export * from "./search.js"; diff --git a/src/query/search.ts b/src/query/search.ts new file mode 100644 index 0000000..9990095 --- /dev/null +++ b/src/query/search.ts @@ -0,0 +1,114 @@ +import { WeaviateClient } from "weaviate-client"; +import { SearchModeResponse, SearchResult } from "./response/response.js"; +import { mapSearchOnlyResponse } from "./response/response-mapping.js"; +import { mapCollections, QueryAgentCollectionConfig } from "./collection.js"; +import { handleError } from "./response/error.js"; +import { ApiSearchModeResponse, ApiSearchResult } from "./response/api-response.js"; + + +/** + * A configured searcher for the QueryAgent. + * + * Warning: + * Weaviate Agents - Query Agent is an early stage alpha product. + * The API is subject to breaking changes. Please ensure you are using the latest version of the client. + * + * For more information, see the [Weaviate Query Agent Docs](https://weaviate.io/developers/agents/query) + */ +export class QueryAgentSearcher { + //private headers: Record = {}; + //private connectionHeaders: HeadersInit | undefined; + private agentsHost: string; + private query: string; + private collections: (string | QueryAgentCollectionConfig)[]; + private systemPrompt?: string; + private cachedSearches?: ApiSearchResult[]; + + constructor( + private client: WeaviateClient, + query: string, + { + collections = [], + systemPrompt, + agentsHost = "https://api.agents.weaviate.io", + }: { + collections?: (string | QueryAgentCollectionConfig)[]; + systemPrompt?: string; + agentsHost?: string; + } = {}, + ) { + this.query = query; + this.collections = collections; + this.systemPrompt = systemPrompt; + this.agentsHost = agentsHost; + this.cachedSearches = undefined; + } + + private async getHeaders() { + const { host, bearerToken, headers } = + await this.client.getConnectionDetails(); + const requestHeaders = { + "Content-Type": "application/json", + Authorization: bearerToken!, + "X-Weaviate-Cluster-Url": host, + "X-Agent-Request-Origin": "typescript-client", + }; + const connectionHeaders = headers; + return { requestHeaders, connectionHeaders }; + } + + private buildRequestBody(limit: number, offset: number, connectionHeaders: HeadersInit | undefined) { + const base = { + headers: connectionHeaders, + original_query: this.query, + collections: mapCollections(this.collections), + limit, + offset, + } as const; + if (this.cachedSearches === undefined) { + return { + ...base, + searches: null, + system_prompt: this.systemPrompt || null, + }; + } + return { + ...base, + searches: this.cachedSearches, + }; + } + + async execute(options: SearchExecutionOptions): Promise { + if (!this.collections || this.collections.length === 0) { + throw Error("No collections provided to the query agent."); + } + const { requestHeaders, connectionHeaders } = await this.getHeaders(); + + const { limit = 20, offset = 0 } = options; + const response = await fetch(`${this.agentsHost}/agent/search_only`, { + method: "POST", + headers: requestHeaders, + body: JSON.stringify(this.buildRequestBody(limit, offset, connectionHeaders)), + }); + if (!response.ok) { + await handleError(await response.text()); + } + const parsedResponse = await response.json() as ApiSearchModeResponse; + const {mappedResponse, apiSearches} = mapSearchOnlyResponse(parsedResponse); + // If we successfully mapped the searches, cache them for the next request. + // Since this cache is a private internal value, there's not point in mapping + // back and forth between the exported and API types, so we cache apiSearches + if (mappedResponse.searches) { + this.cachedSearches = apiSearches; + } + return mappedResponse; + } +} + +/** Options for the executing a prepared QueryAgent search. */ +export type SearchExecutionOptions = { + /** The maximum number of results to return. */ + limit?: number; + /** The offset of the results to return, for paginating through query result sets. */ + offset?: number; +}; \ No newline at end of file From 9d5c9d7354a7a609ac5599a5a85ae59c2213c0ff Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Tue, 19 Aug 2025 13:04:10 +0100 Subject: [PATCH 03/20] Add method to agent to build searcher --- src/query/agent.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/query/agent.ts b/src/query/agent.ts index 3bdcb83..eec7d5b 100644 --- a/src/query/agent.ts +++ b/src/query/agent.ts @@ -14,6 +14,7 @@ import { mapApiResponse } from "./response/api-response-mapping.js"; import { fetchServerSentEvents } from "./response/server-sent-events.js"; import { mapCollections, QueryAgentCollectionConfig } from "./collection.js"; import { handleError } from "./response/error.js"; +import { QueryAgentSearcher } from "./search.js"; /** * An agent for executing agentic queries against Weaviate. @@ -185,6 +186,24 @@ export class QueryAgent { yield output; } } + + /** + * Prepare a searcher for the query agent. + * + * @param query - The natural language query string for the agent. + * @param options - Additional options for the searcher. + * @returns The searcher for the query agent. + */ + prepareSearch( + query: string, + { collections }: QueryAgentSearchOnlyOptions = {}, + ): QueryAgentSearcher { + return new QueryAgentSearcher(this.client, query, { + collections: collections ?? this.collections, + systemPrompt: this.systemPrompt, + agentsHost: this.agentsHost, + }); + } } /** Options for the QueryAgent. */ @@ -216,3 +235,10 @@ export type QueryAgentStreamOptions = { /** Include final state in the stream. */ includeFinalState?: boolean; }; + + +/** Options for the QueryAgent search-only run. */ +export type QueryAgentSearchOnlyOptions = { + /** List of collections to query. Will override any collections if passed in the constructor. */ + collections?: (string | QueryAgentCollectionConfig)[]; +}; From 04d81503fe3b722058f7eaa8c1cc4700bd37b0b6 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Tue, 19 Aug 2025 13:10:07 +0100 Subject: [PATCH 04/20] Lint --- src/query/agent.ts | 1 - src/query/response/response-mapping.ts | 7 +++++-- src/query/search.ts | 25 +++++++++++++++++-------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/query/agent.ts b/src/query/agent.ts index eec7d5b..422453c 100644 --- a/src/query/agent.ts +++ b/src/query/agent.ts @@ -236,7 +236,6 @@ export type QueryAgentStreamOptions = { includeFinalState?: boolean; }; - /** Options for the QueryAgent search-only run. */ export type QueryAgentSearchOnlyOptions = { /** List of collections to query. Will override any collections if passed in the constructor. */ diff --git a/src/query/response/response-mapping.ts b/src/query/response/response-mapping.ts index d577c2b..c280be9 100644 --- a/src/query/response/response-mapping.ts +++ b/src/query/response/response-mapping.ts @@ -304,7 +304,10 @@ export const mapResponseFromSSE = ( export const mapSearchOnlyResponse = ( response: ApiSearchModeResponse, -): { mappedResponse: SearchModeResponse, apiSearches: ApiSearchResult[] | undefined } => { +): { + mappedResponse: SearchModeResponse; + apiSearches: ApiSearchResult[] | undefined; +} => { const apiSearches = response.searches; const mappedResponse: SearchModeResponse = { originalQuery: response.original_query, @@ -313,5 +316,5 @@ export const mapSearchOnlyResponse = ( totalTime: response.total_time, searchResults: response.search_results, }; - return {mappedResponse, apiSearches}; + return { mappedResponse, apiSearches }; }; diff --git a/src/query/search.ts b/src/query/search.ts index 9990095..73eabc0 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -1,10 +1,12 @@ import { WeaviateClient } from "weaviate-client"; -import { SearchModeResponse, SearchResult } from "./response/response.js"; +import { SearchModeResponse } from "./response/response.js"; import { mapSearchOnlyResponse } from "./response/response-mapping.js"; import { mapCollections, QueryAgentCollectionConfig } from "./collection.js"; import { handleError } from "./response/error.js"; -import { ApiSearchModeResponse, ApiSearchResult } from "./response/api-response.js"; - +import { + ApiSearchModeResponse, + ApiSearchResult, +} from "./response/api-response.js"; /** * A configured searcher for the QueryAgent. @@ -57,7 +59,11 @@ export class QueryAgentSearcher { return { requestHeaders, connectionHeaders }; } - private buildRequestBody(limit: number, offset: number, connectionHeaders: HeadersInit | undefined) { + private buildRequestBody( + limit: number, + offset: number, + connectionHeaders: HeadersInit | undefined, + ) { const base = { headers: connectionHeaders, original_query: this.query, @@ -88,13 +94,16 @@ export class QueryAgentSearcher { const response = await fetch(`${this.agentsHost}/agent/search_only`, { method: "POST", headers: requestHeaders, - body: JSON.stringify(this.buildRequestBody(limit, offset, connectionHeaders)), + body: JSON.stringify( + this.buildRequestBody(limit, offset, connectionHeaders), + ), }); if (!response.ok) { await handleError(await response.text()); } - const parsedResponse = await response.json() as ApiSearchModeResponse; - const {mappedResponse, apiSearches} = mapSearchOnlyResponse(parsedResponse); + const parsedResponse = (await response.json()) as ApiSearchModeResponse; + const { mappedResponse, apiSearches } = + mapSearchOnlyResponse(parsedResponse); // If we successfully mapped the searches, cache them for the next request. // Since this cache is a private internal value, there's not point in mapping // back and forth between the exported and API types, so we cache apiSearches @@ -111,4 +120,4 @@ export type SearchExecutionOptions = { limit?: number; /** The offset of the results to return, for paginating through query result sets. */ offset?: number; -}; \ No newline at end of file +}; From a7f980c22b53f10a56fb989c8401bfea42cebcc7 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Tue, 19 Aug 2025 14:43:17 +0100 Subject: [PATCH 05/20] Make searcher generic to support generic WeaviateReturn --- src/query/agent.ts | 4 ++-- src/query/response/api-response.ts | 4 ++-- src/query/response/response-mapping.ts | 8 ++++---- src/query/response/response.ts | 4 ++-- src/query/search.ts | 8 ++++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/query/agent.ts b/src/query/agent.ts index 422453c..f0a8ca8 100644 --- a/src/query/agent.ts +++ b/src/query/agent.ts @@ -194,10 +194,10 @@ export class QueryAgent { * @param options - Additional options for the searcher. * @returns The searcher for the query agent. */ - prepareSearch( + prepareSearch( query: string, { collections }: QueryAgentSearchOnlyOptions = {}, - ): QueryAgentSearcher { + ): QueryAgentSearcher { return new QueryAgentSearcher(this.client, query, { collections: collections ?? this.collections, systemPrompt: this.systemPrompt, diff --git a/src/query/response/api-response.ts b/src/query/response/api-response.ts index ca1d922..19ec6a4 100644 --- a/src/query/response/api-response.ts +++ b/src/query/response/api-response.ts @@ -180,10 +180,10 @@ export type ApiSource = { collection: string; }; -export type ApiSearchModeResponse = { +export type ApiSearchModeResponse = { original_query: string; searches?: ApiSearchResult[]; usage: ApiUsage; total_time: number; - search_results: WeaviateReturn; + search_results: WeaviateReturn; }; diff --git a/src/query/response/response-mapping.ts b/src/query/response/response-mapping.ts index c280be9..7b003ab 100644 --- a/src/query/response/response-mapping.ts +++ b/src/query/response/response-mapping.ts @@ -302,14 +302,14 @@ export const mapResponseFromSSE = ( }; }; -export const mapSearchOnlyResponse = ( - response: ApiSearchModeResponse, +export const mapSearchOnlyResponse = ( + response: ApiSearchModeResponse, ): { - mappedResponse: SearchModeResponse; + mappedResponse: SearchModeResponse; apiSearches: ApiSearchResult[] | undefined; } => { const apiSearches = response.searches; - const mappedResponse: SearchModeResponse = { + const mappedResponse: SearchModeResponse = { originalQuery: response.original_query, searches: apiSearches ? mapInnerSearches(apiSearches) : undefined, usage: mapUsage(response.usage), diff --git a/src/query/response/response.ts b/src/query/response/response.ts index 53544fb..b586c56 100644 --- a/src/query/response/response.ts +++ b/src/query/response/response.ts @@ -263,10 +263,10 @@ export type StreamedTokens = { delta: string; }; -export type SearchModeResponse = { +export type SearchModeResponse = { originalQuery: string; searches?: SearchResult[]; usage: Usage; totalTime: number; - searchResults: WeaviateReturn; + searchResults: WeaviateReturn; }; diff --git a/src/query/search.ts b/src/query/search.ts index 73eabc0..7deade1 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -17,7 +17,7 @@ import { * * For more information, see the [Weaviate Query Agent Docs](https://weaviate.io/developers/agents/query) */ -export class QueryAgentSearcher { +export class QueryAgentSearcher { //private headers: Record = {}; //private connectionHeaders: HeadersInit | undefined; private agentsHost: string; @@ -84,7 +84,7 @@ export class QueryAgentSearcher { }; } - async execute(options: SearchExecutionOptions): Promise { + async execute(options: SearchExecutionOptions): Promise> { if (!this.collections || this.collections.length === 0) { throw Error("No collections provided to the query agent."); } @@ -101,9 +101,9 @@ export class QueryAgentSearcher { if (!response.ok) { await handleError(await response.text()); } - const parsedResponse = (await response.json()) as ApiSearchModeResponse; + const parsedResponse = (await response.json()) as ApiSearchModeResponse; const { mappedResponse, apiSearches } = - mapSearchOnlyResponse(parsedResponse); + mapSearchOnlyResponse(parsedResponse); // If we successfully mapped the searches, cache them for the next request. // Since this cache is a private internal value, there's not point in mapping // back and forth between the exported and API types, so we cache apiSearches From 11cef97bc3a30b916ce9897636b2a473ff3dec54 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 20 Aug 2025 12:04:20 +0100 Subject: [PATCH 06/20] Update tests --- src/query/agent.test.ts | 190 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 1 deletion(-) diff --git a/src/query/agent.test.ts b/src/query/agent.test.ts index bcafdc4..25e9785 100644 --- a/src/query/agent.test.ts +++ b/src/query/agent.test.ts @@ -1,7 +1,14 @@ import { WeaviateClient } from "weaviate-client"; import { QueryAgent } from "./agent.js"; import { ApiQueryAgentResponse } from "./response/api-response.js"; -import { QueryAgentResponse } from "./response/response.js"; +import { + QueryAgentResponse, + ComparisonOperator, + SearchModeResponse, +} from "./response/response.js"; +import { QueryAgentSearcher } from "./search.js"; +import { ApiSearchModeResponse } from "./response/api-response.js"; +import { QueryAgentError } from "./response/error.js"; it("runs the query agent", async () => { const mockClient = { @@ -93,3 +100,184 @@ it("runs the query agent", async () => { display: expect.any(Function), }); }); + +it("prepareSearch returns a QueryAgentSearcher", async () => { + const mockClient = { + getConnectionDetails: jest.fn().mockResolvedValue({ + host: "test-cluster", + bearerToken: "test-token", + headers: { "X-Provider": "test-key" }, + }), + } as unknown as WeaviateClient; + + const agent = new QueryAgent(mockClient, { + systemPrompt: "test system prompt", + }); + + const searcher = agent.prepareSearch("test query"); + expect(searcher).toBeInstanceOf(QueryAgentSearcher); +}); + +it("search-only mode success: caches searches and sends on subsequent request", async () => { + const mockClient = { + getConnectionDetails: jest.fn().mockResolvedValue({ + host: "test-cluster", + bearerToken: "test-token", + headers: { "X-Provider": "test-key" }, + }), + } as unknown as WeaviateClient; + + const capturedBodies: ApiSearchModeResponse[] = []; + + const apiSuccess: ApiSearchModeResponse = { + original_query: "Test this search only mode!", + searches: [ + { + queries: ["search query"], + filters: [ + [ + { + filter_type: "integer", + property_name: "test_property", + operator: ComparisonOperator.GreaterThan, + value: 0, + }, + ], + ], + filter_operators: "AND", + collection: "test_collection", + }, + ], + usage: { + requests: 0, + request_tokens: undefined, + response_tokens: undefined, + total_tokens: undefined, + details: undefined, + }, + total_time: 1.5, + search_results: { + objects: [ + { + uuid: "e6dc0a31-76f8-4bd3-b563-677ced6eb557", + metadata: {}, + references: {}, + vectors: {}, + properties: { + test_property: 1.0, + text: "hello", + }, + }, + { + uuid: "cf5401cc-f4f1-4eb9-a6a1-173d34f94339", + metadata: {}, + references: {}, + vectors: {}, + properties: { + test_property: 2.0, + text: "world!", + }, + }, + ], + }, + }; + + // Mock the API response, and capture the request body to assert later + global.fetch = jest.fn((url, init?: RequestInit) => { + if (init && init.body) { + capturedBodies.push( + JSON.parse(init.body as string) as ApiSearchModeResponse, + ); + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(apiSuccess), + } as Response); + }) as jest.Mock; + + const agent = new QueryAgent(mockClient); + const searcher = agent.prepareSearch("test query", { + collections: ["test_collection"], + }); + + const first = await searcher.execute({ limit: 2, offset: 0 }); + + expect(first).toEqual>({ + originalQuery: apiSuccess.original_query, + searches: [ + { + collection: "test_collection", + queries: ["search query"], + filters: [ + [ + { + filterType: "integer", + propertyName: "test_property", + operator: ComparisonOperator.GreaterThan, + value: 0, + }, + ], + ], + filterOperators: "AND", + }, + ], + usage: { + requests: 0, + requestTokens: undefined, + responseTokens: undefined, + totalTokens: undefined, + details: undefined, + }, + totalTime: 1.5, + searchResults: apiSuccess.search_results, + }); + + // First request should have searches: null (generation request) + expect(capturedBodies[0].searches).toBeNull(); + const second = await searcher.execute({ limit: 2, offset: 1 }); + // Second request should include the original searches (execution request) + expect(capturedBodies[1].searches).toEqual(apiSuccess.searches); + // Response mapping should be the same (because response is mocked) + expect(second).toEqual(first); +}); + +it("search-only mode failure propagates QueryAgentError", async () => { + const mockClient = { + getConnectionDetails: jest.fn().mockResolvedValue({ + host: "test-cluster", + bearerToken: "test-token", + headers: { "X-Provider": "test-key" }, + }), + } as unknown as WeaviateClient; + + const errorJson = { + error: { + message: "Test error message", + code: "test_error_code", + details: { info: "test detail" }, + }, + }; + + global.fetch = jest.fn(() => + Promise.resolve({ + ok: false, + text: () => Promise.resolve(JSON.stringify(errorJson)), + } as Response), + ) as jest.Mock; + + const agent = new QueryAgent(mockClient); + const searcher = agent.prepareSearch("test query", { + collections: ["test_collection"], + }); + + try { + await searcher.execute({ limit: 2, offset: 0 }); + } catch (err) { + expect(err).toBeInstanceOf(QueryAgentError); + expect(err).toMatchObject({ + message: "Test error message", + code: "test_error_code", + details: { info: "test detail" }, + }); + } +}); From 019d72f38d0c5ce65b4618da74a646f126b2545b Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 20 Aug 2025 12:04:27 +0100 Subject: [PATCH 07/20] Lint --- src/query/search.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/query/search.ts b/src/query/search.ts index 7deade1..62f6a3a 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -84,7 +84,9 @@ export class QueryAgentSearcher { }; } - async execute(options: SearchExecutionOptions): Promise> { + async execute( + options: SearchExecutionOptions, + ): Promise> { if (!this.collections || this.collections.length === 0) { throw Error("No collections provided to the query agent."); } From 368bda9ea28203d99b52bbdd00d7ce11422e046b Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 20 Aug 2025 12:08:32 +0100 Subject: [PATCH 08/20] Tidy up --- src/query/search.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/query/search.ts b/src/query/search.ts index 62f6a3a..b785133 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -18,8 +18,6 @@ import { * For more information, see the [Weaviate Query Agent Docs](https://weaviate.io/developers/agents/query) */ export class QueryAgentSearcher { - //private headers: Record = {}; - //private connectionHeaders: HeadersInit | undefined; private agentsHost: string; private query: string; private collections: (string | QueryAgentCollectionConfig)[]; From 62dd66d534cf1f1c1c2332d13dbe5abfd9b3c32e Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 20 Aug 2025 16:50:37 +0100 Subject: [PATCH 09/20] Rename methods --- src/query/agent.test.ts | 12 ++++++------ src/query/agent.ts | 16 ++++++++++++---- src/query/search.ts | 22 +++++++++++++++++++--- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/query/agent.test.ts b/src/query/agent.test.ts index 25e9785..c09eba9 100644 --- a/src/query/agent.test.ts +++ b/src/query/agent.test.ts @@ -114,7 +114,7 @@ it("prepareSearch returns a QueryAgentSearcher", async () => { systemPrompt: "test system prompt", }); - const searcher = agent.prepareSearch("test query"); + const searcher = agent.configureSearch("test query"); expect(searcher).toBeInstanceOf(QueryAgentSearcher); }); @@ -196,11 +196,11 @@ it("search-only mode success: caches searches and sends on subsequent request", }) as jest.Mock; const agent = new QueryAgent(mockClient); - const searcher = agent.prepareSearch("test query", { + const searcher = agent.configureSearch("test query", { collections: ["test_collection"], }); - const first = await searcher.execute({ limit: 2, offset: 0 }); + const first = await searcher.run({ limit: 2, offset: 0 }); expect(first).toEqual>({ originalQuery: apiSuccess.original_query, @@ -234,7 +234,7 @@ it("search-only mode success: caches searches and sends on subsequent request", // First request should have searches: null (generation request) expect(capturedBodies[0].searches).toBeNull(); - const second = await searcher.execute({ limit: 2, offset: 1 }); + const second = await searcher.run({ limit: 2, offset: 1 }); // Second request should include the original searches (execution request) expect(capturedBodies[1].searches).toEqual(apiSuccess.searches); // Response mapping should be the same (because response is mocked) @@ -266,12 +266,12 @@ it("search-only mode failure propagates QueryAgentError", async () => { ) as jest.Mock; const agent = new QueryAgent(mockClient); - const searcher = agent.prepareSearch("test query", { + const searcher = agent.configureSearch("test query", { collections: ["test_collection"], }); try { - await searcher.execute({ limit: 2, offset: 0 }); + await searcher.run({ limit: 2, offset: 0 }); } catch (err) { expect(err).toBeInstanceOf(QueryAgentError); expect(err).toMatchObject({ diff --git a/src/query/agent.ts b/src/query/agent.ts index f0a8ca8..222cac1 100644 --- a/src/query/agent.ts +++ b/src/query/agent.ts @@ -188,13 +188,21 @@ export class QueryAgent { } /** - * Prepare a searcher for the query agent. + * Configure a QueryAgentSearcher for the search-only mode of the query agent. + * + * This returns a configured QueryAgentSearcher, but does not send any requests or + * run the agent. To do that, you should call the `run` method on the searcher. + * + * This allows you to paginate through a consistent results set, as calling the + * `run` method on the searcher multiple times will result in the same underlying + * searches being performed each time. * * @param query - The natural language query string for the agent. - * @param options - Additional options for the searcher. - * @returns The searcher for the query agent. + * @param options - Additional options for configuring the searcher. + * @param options.collections - The collections to query. Will override any collections if passed in the constructor. + * @returns A configured QueryAgentSearcher for the search-only mode of the query agent. */ - prepareSearch( + configureSearch( query: string, { collections }: QueryAgentSearchOnlyOptions = {}, ): QueryAgentSearcher { diff --git a/src/query/search.ts b/src/query/search.ts index b785133..55303cc 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -11,6 +11,12 @@ import { /** * A configured searcher for the QueryAgent. * + * This is configured using the `QueryAgent.configureSearch` method, which builds this class + * but does not send any requests and run the agent. The configured search can then be run + * using the `run` method. You can paginate through the results set by running the `run` method + * multiple times on the same searcher instance, but with different `limit` / `offset` values; + * this will result in the same underlying searches being performed each time. + * * Warning: * Weaviate Agents - Query Agent is an early stage alpha product. * The API is subject to breaking changes. Please ensure you are using the latest version of the client. @@ -82,9 +88,19 @@ export class QueryAgentSearcher { }; } - async execute( - options: SearchExecutionOptions, - ): Promise> { + /** + * Run the search-only agent with the given limit and offset values. + * + * Calling this method multiple times on the same QueryAgentSearcher instance will result + * in the same underlying searches being performed each time, allowing you to paginate + * over a consistent results set. + * + * @param options - Options for executing the search + * @param options.limit - The maximum number of results to return. Defaults to 20 if not specified. + * @param options.offset - The offset to start from. If not specified, retrieval begins from the first object. + * @returns A SearchModeResponse object containing the results, usage, and underlying searches performed. + */ + async run(options: SearchExecutionOptions): Promise> { if (!this.collections || this.collections.length === 0) { throw Error("No collections provided to the query agent."); } From 498a65946579600c7185a5c1dafe17a7c2c31cc8 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Fri, 22 Aug 2025 16:05:21 +0100 Subject: [PATCH 10/20] Update to new search/next design --- src/query/agent.test.ts | 49 +++++++++----------------- src/query/agent.ts | 30 +++++++--------- src/query/response/response-mapping.ts | 6 ++-- src/query/response/response.ts | 6 +++- src/query/search.ts | 6 +++- 5 files changed, 42 insertions(+), 55 deletions(-) diff --git a/src/query/agent.test.ts b/src/query/agent.test.ts index c09eba9..79ce269 100644 --- a/src/query/agent.test.ts +++ b/src/query/agent.test.ts @@ -4,9 +4,7 @@ import { ApiQueryAgentResponse } from "./response/api-response.js"; import { QueryAgentResponse, ComparisonOperator, - SearchModeResponse, } from "./response/response.js"; -import { QueryAgentSearcher } from "./search.js"; import { ApiSearchModeResponse } from "./response/api-response.js"; import { QueryAgentError } from "./response/error.js"; @@ -101,23 +99,6 @@ it("runs the query agent", async () => { }); }); -it("prepareSearch returns a QueryAgentSearcher", async () => { - const mockClient = { - getConnectionDetails: jest.fn().mockResolvedValue({ - host: "test-cluster", - bearerToken: "test-token", - headers: { "X-Provider": "test-key" }, - }), - } as unknown as WeaviateClient; - - const agent = new QueryAgent(mockClient, { - systemPrompt: "test system prompt", - }); - - const searcher = agent.configureSearch("test query"); - expect(searcher).toBeInstanceOf(QueryAgentSearcher); -}); - it("search-only mode success: caches searches and sends on subsequent request", async () => { const mockClient = { getConnectionDetails: jest.fn().mockResolvedValue({ @@ -195,14 +176,10 @@ it("search-only mode success: caches searches and sends on subsequent request", } as Response); }) as jest.Mock; - const agent = new QueryAgent(mockClient); - const searcher = agent.configureSearch("test query", { - collections: ["test_collection"], - }); - - const first = await searcher.run({ limit: 2, offset: 0 }); + const agent = new QueryAgent(mockClient) - expect(first).toEqual>({ + const first = await agent.search("test query", { limit: 2, collections: ["test_collection"] }); + expect(first).toMatchObject({ originalQuery: apiSuccess.original_query, searches: [ { @@ -231,14 +208,24 @@ it("search-only mode success: caches searches and sends on subsequent request", totalTime: 1.5, searchResults: apiSuccess.search_results, }); + expect(typeof first.next).toBe("function"); // First request should have searches: null (generation request) expect(capturedBodies[0].searches).toBeNull(); - const second = await searcher.run({ limit: 2, offset: 1 }); + + // Second request uses the next method on the first response + const second = await first.next({ limit: 2, offset: 1 }); // Second request should include the original searches (execution request) expect(capturedBodies[1].searches).toEqual(apiSuccess.searches); // Response mapping should be the same (because response is mocked) - expect(second).toEqual(first); + expect(second).toMatchObject({ + originalQuery: apiSuccess.original_query, + searches: first.searches, + usage: first.usage, + totalTime: first.totalTime, + searchResults: first.searchResults, + }); + expect(typeof second.next).toBe("function"); }); it("search-only mode failure propagates QueryAgentError", async () => { @@ -266,12 +253,8 @@ it("search-only mode failure propagates QueryAgentError", async () => { ) as jest.Mock; const agent = new QueryAgent(mockClient); - const searcher = agent.configureSearch("test query", { - collections: ["test_collection"], - }); - try { - await searcher.run({ limit: 2, offset: 0 }); + await agent.search("test query", { limit: 2, collections: ["test_collection"] }); } catch (err) { expect(err).toBeInstanceOf(QueryAgentError); expect(err).toMatchObject({ diff --git a/src/query/agent.ts b/src/query/agent.ts index 222cac1..7fb8679 100644 --- a/src/query/agent.ts +++ b/src/query/agent.ts @@ -15,6 +15,7 @@ import { fetchServerSentEvents } from "./response/server-sent-events.js"; import { mapCollections, QueryAgentCollectionConfig } from "./collection.js"; import { handleError } from "./response/error.js"; import { QueryAgentSearcher } from "./search.js"; +import { SearchModeResponse } from "./response/response.js"; /** * An agent for executing agentic queries against Weaviate. @@ -188,29 +189,22 @@ export class QueryAgent { } /** - * Configure a QueryAgentSearcher for the search-only mode of the query agent. + * Run the Query Agent search-only mode. * - * This returns a configured QueryAgentSearcher, but does not send any requests or - * run the agent. To do that, you should call the `run` method on the searcher. - * - * This allows you to paginate through a consistent results set, as calling the - * `run` method on the searcher multiple times will result in the same underlying - * searches being performed each time. - * - * @param query - The natural language query string for the agent. - * @param options - Additional options for configuring the searcher. - * @param options.collections - The collections to query. Will override any collections if passed in the constructor. - * @returns A configured QueryAgentSearcher for the search-only mode of the query agent. + * Sends the initial search request and returns the first page of results. + * The returned response includes a `next` method for pagination which + * reuses the same underlying searches to ensure consistency across pages. */ - configureSearch( + async search( query: string, - { collections }: QueryAgentSearchOnlyOptions = {}, - ): QueryAgentSearcher { - return new QueryAgentSearcher(this.client, query, { + { limit, collections }: QueryAgentSearchOnlyOptions = {}, + ): Promise> { + const searcher = new QueryAgentSearcher(this.client, query, { collections: collections ?? this.collections, systemPrompt: this.systemPrompt, agentsHost: this.agentsHost, - }); + }) + return searcher.run({ limit: limit ?? 20, offset: 0 }); } } @@ -246,6 +240,8 @@ export type QueryAgentStreamOptions = { /** Options for the QueryAgent search-only run. */ export type QueryAgentSearchOnlyOptions = { + /** The maximum number of results to return. */ + limit?: number; /** List of collections to query. Will override any collections if passed in the constructor. */ collections?: (string | QueryAgentCollectionConfig)[]; }; diff --git a/src/query/response/response-mapping.ts b/src/query/response/response-mapping.ts index 7b003ab..a994b40 100644 --- a/src/query/response/response-mapping.ts +++ b/src/query/response/response-mapping.ts @@ -9,7 +9,7 @@ import { StreamedTokens, ProgressMessage, DateFilterValue, - SearchModeResponse, + MappedSearchModeResponse, } from "./response.js"; import { @@ -305,11 +305,11 @@ export const mapResponseFromSSE = ( export const mapSearchOnlyResponse = ( response: ApiSearchModeResponse, ): { - mappedResponse: SearchModeResponse; + mappedResponse: MappedSearchModeResponse; apiSearches: ApiSearchResult[] | undefined; } => { const apiSearches = response.searches; - const mappedResponse: SearchModeResponse = { + const mappedResponse: MappedSearchModeResponse = { originalQuery: response.original_query, searches: apiSearches ? mapInnerSearches(apiSearches) : undefined, usage: mapUsage(response.usage), diff --git a/src/query/response/response.ts b/src/query/response/response.ts index b586c56..f6d8147 100644 --- a/src/query/response/response.ts +++ b/src/query/response/response.ts @@ -263,10 +263,14 @@ export type StreamedTokens = { delta: string; }; -export type SearchModeResponse = { +export type MappedSearchModeResponse = { originalQuery: string; searches?: SearchResult[]; usage: Usage; totalTime: number; searchResults: WeaviateReturn; }; + +export type SearchModeResponse = MappedSearchModeResponse & { + next: (options?: { limit?: number; offset?: number }) => Promise>; +}; diff --git a/src/query/search.ts b/src/query/search.ts index 55303cc..6e2d251 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -126,7 +126,11 @@ export class QueryAgentSearcher { if (mappedResponse.searches) { this.cachedSearches = apiSearches; } - return mappedResponse; + return { + ...mappedResponse, + next: async ({ limit: nextLimit = 20, offset: nextOffset = 0 } = {}) => + this.run({ limit: nextLimit, offset: nextOffset }), + }; } } From 9749bf299462b3e04ab77d284ac95ff4e9e4ef37 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Fri, 22 Aug 2025 16:29:00 +0100 Subject: [PATCH 11/20] Lint --- src/query/agent.test.ts | 17 ++++++++++------- src/query/agent.ts | 2 +- src/query/response/response.ts | 5 ++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/query/agent.test.ts b/src/query/agent.test.ts index 79ce269..45080fa 100644 --- a/src/query/agent.test.ts +++ b/src/query/agent.test.ts @@ -1,10 +1,7 @@ import { WeaviateClient } from "weaviate-client"; import { QueryAgent } from "./agent.js"; import { ApiQueryAgentResponse } from "./response/api-response.js"; -import { - QueryAgentResponse, - ComparisonOperator, -} from "./response/response.js"; +import { QueryAgentResponse, ComparisonOperator } from "./response/response.js"; import { ApiSearchModeResponse } from "./response/api-response.js"; import { QueryAgentError } from "./response/error.js"; @@ -176,9 +173,12 @@ it("search-only mode success: caches searches and sends on subsequent request", } as Response); }) as jest.Mock; - const agent = new QueryAgent(mockClient) + const agent = new QueryAgent(mockClient); - const first = await agent.search("test query", { limit: 2, collections: ["test_collection"] }); + const first = await agent.search("test query", { + limit: 2, + collections: ["test_collection"], + }); expect(first).toMatchObject({ originalQuery: apiSuccess.original_query, searches: [ @@ -254,7 +254,10 @@ it("search-only mode failure propagates QueryAgentError", async () => { const agent = new QueryAgent(mockClient); try { - await agent.search("test query", { limit: 2, collections: ["test_collection"] }); + await agent.search("test query", { + limit: 2, + collections: ["test_collection"], + }); } catch (err) { expect(err).toBeInstanceOf(QueryAgentError); expect(err).toMatchObject({ diff --git a/src/query/agent.ts b/src/query/agent.ts index 7fb8679..0013c71 100644 --- a/src/query/agent.ts +++ b/src/query/agent.ts @@ -203,7 +203,7 @@ export class QueryAgent { collections: collections ?? this.collections, systemPrompt: this.systemPrompt, agentsHost: this.agentsHost, - }) + }); return searcher.run({ limit: limit ?? 20, offset: 0 }); } } diff --git a/src/query/response/response.ts b/src/query/response/response.ts index f6d8147..789d41a 100644 --- a/src/query/response/response.ts +++ b/src/query/response/response.ts @@ -272,5 +272,8 @@ export type MappedSearchModeResponse = { }; export type SearchModeResponse = MappedSearchModeResponse & { - next: (options?: { limit?: number; offset?: number }) => Promise>; + next: (options?: { + limit?: number; + offset?: number; + }) => Promise>; }; From f78c5087d581a69649a11dbe234f929641d0c225 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 27 Aug 2025 15:30:39 +0100 Subject: [PATCH 12/20] Address PR comments --- src/query/agent.ts | 4 ++-- src/query/response/response.ts | 13 +++++++++---- src/query/search.ts | 27 +++++++++++---------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/query/agent.ts b/src/query/agent.ts index 0013c71..0cf772e 100644 --- a/src/query/agent.ts +++ b/src/query/agent.ts @@ -197,14 +197,14 @@ export class QueryAgent { */ async search( query: string, - { limit, collections }: QueryAgentSearchOnlyOptions = {}, + { limit = 20, collections }: QueryAgentSearchOnlyOptions = {}, ): Promise> { const searcher = new QueryAgentSearcher(this.client, query, { collections: collections ?? this.collections, systemPrompt: this.systemPrompt, agentsHost: this.agentsHost, }); - return searcher.run({ limit: limit ?? 20, offset: 0 }); + return searcher.run({ limit, offset: 0 }); } } diff --git a/src/query/response/response.ts b/src/query/response/response.ts index 789d41a..d292069 100644 --- a/src/query/response/response.ts +++ b/src/query/response/response.ts @@ -271,9 +271,14 @@ export type MappedSearchModeResponse = { searchResults: WeaviateReturn; }; +/** Options for the executing a prepared QueryAgent search. */ +export type SearchExecutionOptions = { + /** The maximum number of results to return. */ + limit?: number; + /** The offset of the results to return, for paginating through query result sets. */ + offset?: number; +}; + export type SearchModeResponse = MappedSearchModeResponse & { - next: (options?: { - limit?: number; - offset?: number; - }) => Promise>; + next: (options: SearchExecutionOptions) => Promise>; }; diff --git a/src/query/search.ts b/src/query/search.ts index 6e2d251..56fd210 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -1,5 +1,8 @@ import { WeaviateClient } from "weaviate-client"; -import { SearchModeResponse } from "./response/response.js"; +import { + SearchExecutionOptions, + SearchModeResponse, +} from "./response/response.js"; import { mapSearchOnlyResponse } from "./response/response-mapping.js"; import { mapCollections, QueryAgentCollectionConfig } from "./collection.js"; import { handleError } from "./response/error.js"; @@ -95,18 +98,19 @@ export class QueryAgentSearcher { * in the same underlying searches being performed each time, allowing you to paginate * over a consistent results set. * - * @param options - Options for executing the search - * @param options.limit - The maximum number of results to return. Defaults to 20 if not specified. - * @param options.offset - The offset to start from. If not specified, retrieval begins from the first object. + * @param [options] - Options for executing the search + * @param [options.limit] - The maximum number of results to return. Defaults to 20 if not specified. + * @param [options.offset] - The offset to start from. If not specified, retrieval begins from the first object. * @returns A SearchModeResponse object containing the results, usage, and underlying searches performed. */ - async run(options: SearchExecutionOptions): Promise> { + async run({ limit = 20, offset = 0 }: SearchExecutionOptions = {}): Promise< + SearchModeResponse + > { if (!this.collections || this.collections.length === 0) { throw Error("No collections provided to the query agent."); } const { requestHeaders, connectionHeaders } = await this.getHeaders(); - const { limit = 20, offset = 0 } = options; const response = await fetch(`${this.agentsHost}/agent/search_only`, { method: "POST", headers: requestHeaders, @@ -128,16 +132,7 @@ export class QueryAgentSearcher { } return { ...mappedResponse, - next: async ({ limit: nextLimit = 20, offset: nextOffset = 0 } = {}) => - this.run({ limit: nextLimit, offset: nextOffset }), + next: async (options: SearchExecutionOptions = {}) => this.run(options), }; } } - -/** Options for the executing a prepared QueryAgent search. */ -export type SearchExecutionOptions = { - /** The maximum number of results to return. */ - limit?: number; - /** The offset of the results to return, for paginating through query result sets. */ - offset?: number; -}; From 8a0204fde7903f0b262c8a5a084c38fef945181d Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 3 Sep 2025 15:07:32 +0100 Subject: [PATCH 13/20] Add mapping of search results --- src/query/agent.test.ts | 67 ++++++++++++++++++++++---- src/query/agent.ts | 6 +-- src/query/response/api-response.ts | 36 ++++++++++++-- src/query/response/response-mapping.ts | 65 +++++++++++++++++++++++-- src/query/response/response.ts | 18 +++++-- src/query/search.ts | 13 ++--- 6 files changed, 173 insertions(+), 32 deletions(-) diff --git a/src/query/agent.test.ts b/src/query/agent.test.ts index 45080fa..8445c8d 100644 --- a/src/query/agent.test.ts +++ b/src/query/agent.test.ts @@ -105,9 +105,9 @@ it("search-only mode success: caches searches and sends on subsequent request", }), } as unknown as WeaviateClient; - const capturedBodies: ApiSearchModeResponse[] = []; + const capturedBodies: ApiSearchModeResponse[] = []; - const apiSuccess: ApiSearchModeResponse = { + const apiSuccess: ApiSearchModeResponse = { original_query: "Test this search only mode!", searches: [ { @@ -138,23 +138,43 @@ it("search-only mode success: caches searches and sends on subsequent request", objects: [ { uuid: "e6dc0a31-76f8-4bd3-b563-677ced6eb557", - metadata: {}, - references: {}, - vectors: {}, + metadata: { + creation_time: null, + update_time: null, + distance: null, + certainty: null, + score: 0.8, + explain_score: null, + rerank_score: null, + is_consistent: null, + }, + references: null, + vector: {}, properties: { test_property: 1.0, text: "hello", }, + collection: "test_collection", }, { uuid: "cf5401cc-f4f1-4eb9-a6a1-173d34f94339", - metadata: {}, - references: {}, - vectors: {}, + metadata: { + creation_time: null, + update_time: null, + distance: null, + certainty: null, + score: 0.5, + explain_score: null, + rerank_score: null, + is_consistent: null, + }, + references: null, + vector: {}, properties: { test_property: 2.0, text: "world!", }, + collection: "test_collection", }, ], }, @@ -164,7 +184,7 @@ it("search-only mode success: caches searches and sends on subsequent request", global.fetch = jest.fn((url, init?: RequestInit) => { if (init && init.body) { capturedBodies.push( - JSON.parse(init.body as string) as ApiSearchModeResponse, + JSON.parse(init.body as string) as ApiSearchModeResponse, ); } return Promise.resolve({ @@ -206,7 +226,34 @@ it("search-only mode success: caches searches and sends on subsequent request", details: undefined, }, totalTime: 1.5, - searchResults: apiSuccess.search_results, + searchResults: { + objects: [ + { + uuid: "e6dc0a31-76f8-4bd3-b563-677ced6eb557", + metadata: { + score: 0.8, + }, + vectors: {}, + properties: { + test_property: 1.0, + text: "hello", + }, + collection: "test_collection", + }, + { + uuid: "cf5401cc-f4f1-4eb9-a6a1-173d34f94339", + metadata: { + score: 0.5, + }, + vectors: {}, + properties: { + test_property: 2.0, + text: "world!", + }, + collection: "test_collection", + }, + ], + }, }); expect(typeof first.next).toBe("function"); diff --git a/src/query/agent.ts b/src/query/agent.ts index 0cf772e..1583aaa 100644 --- a/src/query/agent.ts +++ b/src/query/agent.ts @@ -195,11 +195,11 @@ export class QueryAgent { * The returned response includes a `next` method for pagination which * reuses the same underlying searches to ensure consistency across pages. */ - async search( + async search( query: string, { limit = 20, collections }: QueryAgentSearchOnlyOptions = {}, - ): Promise> { - const searcher = new QueryAgentSearcher(this.client, query, { + ): Promise { + const searcher = new QueryAgentSearcher(this.client, query, { collections: collections ?? this.collections, systemPrompt: this.systemPrompt, agentsHost: this.agentsHost, diff --git a/src/query/response/api-response.ts b/src/query/response/api-response.ts index 19ec6a4..ae4443c 100644 --- a/src/query/response/api-response.ts +++ b/src/query/response/api-response.ts @@ -1,4 +1,4 @@ -import { WeaviateReturn } from "weaviate-client"; +import { Vectors, WeaviateField } from "weaviate-client"; import { NumericMetrics, @@ -180,10 +180,40 @@ export type ApiSource = { collection: string; }; -export type ApiSearchModeResponse = { +export type ApiReturnMetadata = { + creation_time: Date | null; + update_time: Date | null; + distance: number | null; + certainty: number | null; + score: number | null; + explain_score: string | null; + rerank_score: number | null; + is_consistent: boolean | null; +}; + +export type ApiWeaviateObject = { + /** The returned properties of the object as untyped key-value pairs from the API. */ + properties: Record; + /** The returned metadata of the object. */ + metadata: ApiReturnMetadata; + /** The returned references of the object. */ + references: null; // TODO: QA never requests references, so they're never returned?? Check this + /** The UUID of the object. */ + uuid: string; + /** The returned vectors of the object. */ + vector: Vectors; // TODO: note no s! + collection: string; // NOTE: NEW +}; + +export type ApiWeaviateReturn = { + /** The objects that were found by the query. */ + objects: ApiWeaviateObject[]; +}; + +export type ApiSearchModeResponse = { original_query: string; searches?: ApiSearchResult[]; usage: ApiUsage; total_time: number; - search_results: WeaviateReturn; + search_results: ApiWeaviateReturn; }; diff --git a/src/query/response/response-mapping.ts b/src/query/response/response-mapping.ts index a994b40..4ed7fad 100644 --- a/src/query/response/response-mapping.ts +++ b/src/query/response/response-mapping.ts @@ -1,3 +1,5 @@ +import { ReturnMetadata } from "weaviate-client"; + import { QueryAgentResponse, SearchResult, @@ -10,6 +12,8 @@ import { ProgressMessage, DateFilterValue, MappedSearchModeResponse, + WeaviateObjectWithCollection, + WeaviateReturnWithCollection, } from "./response.js"; import { @@ -22,6 +26,8 @@ import { ApiSource, ApiDateFilterValue, ApiSearchModeResponse, + ApiWeaviateObject, + ApiWeaviateReturn, } from "./api-response.js"; import { ServerSentEvent } from "./server-sent-events.js"; @@ -302,19 +308,68 @@ export const mapResponseFromSSE = ( }; }; -export const mapSearchOnlyResponse = ( - response: ApiSearchModeResponse, +const mapWeaviateObject = ( + object: ApiWeaviateObject, +): WeaviateObjectWithCollection => { + const metadata: ReturnMetadata = { + creationTime: + object.metadata.creation_time !== null + ? object.metadata.creation_time + : undefined, + updateTime: + object.metadata.update_time !== null + ? object.metadata.update_time + : undefined, + distance: + object.metadata.distance !== null ? object.metadata.distance : undefined, + certainty: + object.metadata.certainty !== null + ? object.metadata.certainty + : undefined, + score: object.metadata.score !== null ? object.metadata.score : undefined, + explainScore: + object.metadata.explain_score !== null + ? object.metadata.explain_score + : undefined, + rerankScore: + object.metadata.rerank_score !== null + ? object.metadata.rerank_score + : undefined, + isConsistent: + object.metadata.is_consistent !== null + ? object.metadata.is_consistent + : undefined, + }; + + return { + properties: object.properties, + metadata: metadata, + references: undefined, + uuid: object.uuid, + vectors: object.vector, + collection: object.collection, + }; +}; + +export const mapWeviateSearchResults = ( + response: ApiWeaviateReturn, +): WeaviateReturnWithCollection => ({ + objects: response.objects.map(mapWeaviateObject), +}); + +export const mapSearchOnlyResponse = ( + response: ApiSearchModeResponse, ): { - mappedResponse: MappedSearchModeResponse; + mappedResponse: MappedSearchModeResponse; apiSearches: ApiSearchResult[] | undefined; } => { const apiSearches = response.searches; - const mappedResponse: MappedSearchModeResponse = { + const mappedResponse: MappedSearchModeResponse = { originalQuery: response.original_query, searches: apiSearches ? mapInnerSearches(apiSearches) : undefined, usage: mapUsage(response.usage), totalTime: response.total_time, - searchResults: response.search_results, + searchResults: mapWeviateSearchResults(response.search_results), }; return { mappedResponse, apiSearches }; }; diff --git a/src/query/response/response.ts b/src/query/response/response.ts index d292069..5935f61 100644 --- a/src/query/response/response.ts +++ b/src/query/response/response.ts @@ -1,4 +1,4 @@ -import { WeaviateReturn } from "weaviate-client"; +import { WeaviateReturn, WeaviateObject } from "weaviate-client"; export type QueryAgentResponse = { outputType: "finalState"; @@ -263,12 +263,20 @@ export type StreamedTokens = { delta: string; }; -export type MappedSearchModeResponse = { +export type WeaviateObjectWithCollection = WeaviateObject & { + collection: string; +}; + +export type WeaviateReturnWithCollection = WeaviateReturn & { + objects: WeaviateObjectWithCollection[]; +}; + +export type MappedSearchModeResponse = { originalQuery: string; searches?: SearchResult[]; usage: Usage; totalTime: number; - searchResults: WeaviateReturn; + searchResults: WeaviateReturnWithCollection; }; /** Options for the executing a prepared QueryAgent search. */ @@ -279,6 +287,6 @@ export type SearchExecutionOptions = { offset?: number; }; -export type SearchModeResponse = MappedSearchModeResponse & { - next: (options: SearchExecutionOptions) => Promise>; +export type SearchModeResponse = MappedSearchModeResponse & { + next: (options: SearchExecutionOptions) => Promise; }; diff --git a/src/query/search.ts b/src/query/search.ts index 56fd210..e7aa3f4 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -26,7 +26,7 @@ import { * * For more information, see the [Weaviate Query Agent Docs](https://weaviate.io/developers/agents/query) */ -export class QueryAgentSearcher { +export class QueryAgentSearcher { private agentsHost: string; private query: string; private collections: (string | QueryAgentCollectionConfig)[]; @@ -103,9 +103,10 @@ export class QueryAgentSearcher { * @param [options.offset] - The offset to start from. If not specified, retrieval begins from the first object. * @returns A SearchModeResponse object containing the results, usage, and underlying searches performed. */ - async run({ limit = 20, offset = 0 }: SearchExecutionOptions = {}): Promise< - SearchModeResponse - > { + async run({ + limit = 20, + offset = 0, + }: SearchExecutionOptions = {}): Promise { if (!this.collections || this.collections.length === 0) { throw Error("No collections provided to the query agent."); } @@ -121,9 +122,9 @@ export class QueryAgentSearcher { if (!response.ok) { await handleError(await response.text()); } - const parsedResponse = (await response.json()) as ApiSearchModeResponse; + const parsedResponse = (await response.json()) as ApiSearchModeResponse; const { mappedResponse, apiSearches } = - mapSearchOnlyResponse(parsedResponse); + mapSearchOnlyResponse(parsedResponse); // If we successfully mapped the searches, cache them for the next request. // Since this cache is a private internal value, there's not point in mapping // back and forth between the exported and API types, so we cache apiSearches From 6ae894d502b1e2cf3f473b69724a082c1826274f Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 3 Sep 2025 15:23:38 +0100 Subject: [PATCH 14/20] Remove MappedSearchModeResponse --- src/query/response/response-mapping.ts | 7 ++++--- src/query/response/response.ts | 15 ++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/query/response/response-mapping.ts b/src/query/response/response-mapping.ts index 4ed7fad..a69f358 100644 --- a/src/query/response/response-mapping.ts +++ b/src/query/response/response-mapping.ts @@ -11,9 +11,10 @@ import { StreamedTokens, ProgressMessage, DateFilterValue, - MappedSearchModeResponse, + //MappedSearchModeResponse, WeaviateObjectWithCollection, WeaviateReturnWithCollection, + SearchModeResponse, } from "./response.js"; import { @@ -360,11 +361,11 @@ export const mapWeviateSearchResults = ( export const mapSearchOnlyResponse = ( response: ApiSearchModeResponse, ): { - mappedResponse: MappedSearchModeResponse; + mappedResponse: Omit; apiSearches: ApiSearchResult[] | undefined; } => { const apiSearches = response.searches; - const mappedResponse: MappedSearchModeResponse = { + const mappedResponse: Omit = { originalQuery: response.original_query, searches: apiSearches ? mapInnerSearches(apiSearches) : undefined, usage: mapUsage(response.usage), diff --git a/src/query/response/response.ts b/src/query/response/response.ts index 5935f61..37c77e6 100644 --- a/src/query/response/response.ts +++ b/src/query/response/response.ts @@ -271,14 +271,6 @@ export type WeaviateReturnWithCollection = WeaviateReturn & { objects: WeaviateObjectWithCollection[]; }; -export type MappedSearchModeResponse = { - originalQuery: string; - searches?: SearchResult[]; - usage: Usage; - totalTime: number; - searchResults: WeaviateReturnWithCollection; -}; - /** Options for the executing a prepared QueryAgent search. */ export type SearchExecutionOptions = { /** The maximum number of results to return. */ @@ -287,6 +279,11 @@ export type SearchExecutionOptions = { offset?: number; }; -export type SearchModeResponse = MappedSearchModeResponse & { +export type SearchModeResponse = { + originalQuery: string; + searches?: SearchResult[]; + usage: Usage; + totalTime: number; + searchResults: WeaviateReturnWithCollection; next: (options: SearchExecutionOptions) => Promise; }; From b3f53a837632d68ba6245b8b1d6bf27928586812 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 3 Sep 2025 15:49:35 +0100 Subject: [PATCH 15/20] Remove default options --- src/query/search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/query/search.ts b/src/query/search.ts index e7aa3f4..4e8b3a4 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -133,7 +133,7 @@ export class QueryAgentSearcher { } return { ...mappedResponse, - next: async (options: SearchExecutionOptions = {}) => this.run(options), + next: async (options: SearchExecutionOptions) => this.run(options), }; } } From 85c570220f621f6b5664a20d5a148d19c57fe93e Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 3 Sep 2025 15:58:37 +0100 Subject: [PATCH 16/20] Remove todo comments --- src/query/response/api-response.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/query/response/api-response.ts b/src/query/response/api-response.ts index ae4443c..ab9c1a6 100644 --- a/src/query/response/api-response.ts +++ b/src/query/response/api-response.ts @@ -197,11 +197,11 @@ export type ApiWeaviateObject = { /** The returned metadata of the object. */ metadata: ApiReturnMetadata; /** The returned references of the object. */ - references: null; // TODO: QA never requests references, so they're never returned?? Check this + references: null; /** The UUID of the object. */ uuid: string; /** The returned vectors of the object. */ - vector: Vectors; // TODO: note no s! + vector: Vectors; collection: string; // NOTE: NEW }; From 89eaebfc4e564e4922ddd1ff2dcb8376ec5568a1 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 3 Sep 2025 15:59:54 +0100 Subject: [PATCH 17/20] Update docs --- src/query/response/api-response.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/query/response/api-response.ts b/src/query/response/api-response.ts index ab9c1a6..2b36ea6 100644 --- a/src/query/response/api-response.ts +++ b/src/query/response/api-response.ts @@ -202,7 +202,8 @@ export type ApiWeaviateObject = { uuid: string; /** The returned vectors of the object. */ vector: Vectors; - collection: string; // NOTE: NEW + /** The collection this object belongs to. */ + collection: string; }; export type ApiWeaviateReturn = { From e1815355fcd39067ddcb2d8f210e926fbd299f8f Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 3 Sep 2025 16:00:25 +0100 Subject: [PATCH 18/20] Lint --- src/query/response/response-mapping.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/query/response/response-mapping.ts b/src/query/response/response-mapping.ts index a69f358..20486b3 100644 --- a/src/query/response/response-mapping.ts +++ b/src/query/response/response-mapping.ts @@ -11,7 +11,6 @@ import { StreamedTokens, ProgressMessage, DateFilterValue, - //MappedSearchModeResponse, WeaviateObjectWithCollection, WeaviateReturnWithCollection, SearchModeResponse, From b6c195fb38b0602b0e7764176a8dae51ed3ea136 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 3 Sep 2025 16:02:58 +0100 Subject: [PATCH 19/20] Update searcher docs --- src/query/search.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/query/search.ts b/src/query/search.ts index 4e8b3a4..3710bff 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -14,11 +14,9 @@ import { /** * A configured searcher for the QueryAgent. * - * This is configured using the `QueryAgent.configureSearch` method, which builds this class - * but does not send any requests and run the agent. The configured search can then be run - * using the `run` method. You can paginate through the results set by running the `run` method - * multiple times on the same searcher instance, but with different `limit` / `offset` values; - * this will result in the same underlying searches being performed each time. + * This is used internally by the QueryAgent class to run search-mode queries. + * After the first request is made, the underlying searches are cached and can + * be reused for paginating through the a consistent set of results. * * Warning: * Weaviate Agents - Query Agent is an early stage alpha product. From f43b595371e7da082d8ec75fba3832bd04fa8a4b Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Thu, 4 Sep 2025 12:20:12 +0100 Subject: [PATCH 20/20] PR comments --- src/query/response/response-mapping.ts | 2 +- src/query/response/response.ts | 2 +- src/query/search.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/query/response/response-mapping.ts b/src/query/response/response-mapping.ts index 20486b3..07dd111 100644 --- a/src/query/response/response-mapping.ts +++ b/src/query/response/response-mapping.ts @@ -343,7 +343,7 @@ const mapWeaviateObject = ( return { properties: object.properties, - metadata: metadata, + metadata, references: undefined, uuid: object.uuid, vectors: object.vector, diff --git a/src/query/response/response.ts b/src/query/response/response.ts index 37c77e6..6c64d66 100644 --- a/src/query/response/response.ts +++ b/src/query/response/response.ts @@ -276,7 +276,7 @@ export type SearchExecutionOptions = { /** The maximum number of results to return. */ limit?: number; /** The offset of the results to return, for paginating through query result sets. */ - offset?: number; + offset: number; }; export type SearchModeResponse = { diff --git a/src/query/search.ts b/src/query/search.ts index 3710bff..164345d 100644 --- a/src/query/search.ts +++ b/src/query/search.ts @@ -98,13 +98,13 @@ export class QueryAgentSearcher { * * @param [options] - Options for executing the search * @param [options.limit] - The maximum number of results to return. Defaults to 20 if not specified. - * @param [options.offset] - The offset to start from. If not specified, retrieval begins from the first object. + * @param [options.offset] - The offset to start from. * @returns A SearchModeResponse object containing the results, usage, and underlying searches performed. */ async run({ limit = 20, - offset = 0, - }: SearchExecutionOptions = {}): Promise { + offset, + }: SearchExecutionOptions): Promise { if (!this.collections || this.collections.length === 0) { throw Error("No collections provided to the query agent."); }