Skip to content
67 changes: 57 additions & 10 deletions src/query/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ it("search-only mode success: caches searches and sends on subsequent request",
}),
} as unknown as WeaviateClient;

const capturedBodies: ApiSearchModeResponse<undefined>[] = [];
const capturedBodies: ApiSearchModeResponse[] = [];

const apiSuccess: ApiSearchModeResponse<undefined> = {
const apiSuccess: ApiSearchModeResponse = {
original_query: "Test this search only mode!",
searches: [
{
Expand Down Expand Up @@ -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",
},
],
},
Expand All @@ -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<undefined>,
JSON.parse(init.body as string) as ApiSearchModeResponse,
);
}
return Promise.resolve({
Expand Down Expand Up @@ -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");

Expand Down
6 changes: 3 additions & 3 deletions src/query/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = undefined>(
async search(
query: string,
{ limit = 20, collections }: QueryAgentSearchOnlyOptions = {},
): Promise<SearchModeResponse<T>> {
const searcher = new QueryAgentSearcher<T>(this.client, query, {
): Promise<SearchModeResponse> {
const searcher = new QueryAgentSearcher(this.client, query, {
collections: collections ?? this.collections,
systemPrompt: this.systemPrompt,
agentsHost: this.agentsHost,
Expand Down
37 changes: 34 additions & 3 deletions src/query/response/api-response.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WeaviateReturn } from "weaviate-client";
import { Vectors, WeaviateField } from "weaviate-client";

import {
NumericMetrics,
Expand Down Expand Up @@ -180,10 +180,41 @@ export type ApiSource = {
collection: string;
};

export type ApiSearchModeResponse<T> = {
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<string, WeaviateField>;
/** The returned metadata of the object. */
metadata: ApiReturnMetadata;
/** The returned references of the object. */
references: null;
/** The UUID of the object. */
uuid: string;
/** The returned vectors of the object. */
vector: Vectors;
/** The collection this object belongs to. */
collection: string;
};

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<T>;
search_results: ApiWeaviateReturn;
};
67 changes: 61 additions & 6 deletions src/query/response/response-mapping.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ReturnMetadata } from "weaviate-client";

import {
QueryAgentResponse,
SearchResult,
Expand All @@ -9,7 +11,9 @@ import {
StreamedTokens,
ProgressMessage,
DateFilterValue,
MappedSearchModeResponse,
WeaviateObjectWithCollection,
WeaviateReturnWithCollection,
SearchModeResponse,
} from "./response.js";

import {
Expand All @@ -22,6 +26,8 @@ import {
ApiSource,
ApiDateFilterValue,
ApiSearchModeResponse,
ApiWeaviateObject,
ApiWeaviateReturn,
} from "./api-response.js";

import { ServerSentEvent } from "./server-sent-events.js";
Expand Down Expand Up @@ -302,19 +308,68 @@ export const mapResponseFromSSE = (
};
};

export const mapSearchOnlyResponse = <T>(
response: ApiSearchModeResponse<T>,
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,
Copy link
Collaborator

Choose a reason for hiding this comment

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

nitpick can omit variable when key has the same name. metadata,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice catch - fixed.

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<T>;
mappedResponse: Omit<SearchModeResponse, "next">;
apiSearches: ApiSearchResult[] | undefined;
} => {
const apiSearches = response.searches;
const mappedResponse: MappedSearchModeResponse<T> = {
const mappedResponse: Omit<SearchModeResponse, "next"> = {
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 };
};
23 changes: 14 additions & 9 deletions src/query/response/response.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WeaviateReturn } from "weaviate-client";
import { WeaviateReturn, WeaviateObject } from "weaviate-client";

export type QueryAgentResponse = {
outputType: "finalState";
Expand Down Expand Up @@ -263,12 +263,12 @@ export type StreamedTokens = {
delta: string;
};

export type MappedSearchModeResponse<T> = {
originalQuery: string;
searches?: SearchResult[];
usage: Usage;
totalTime: number;
searchResults: WeaviateReturn<T>;
export type WeaviateObjectWithCollection = WeaviateObject<undefined> & {
collection: string;
};

export type WeaviateReturnWithCollection = WeaviateReturn<undefined> & {
objects: WeaviateObjectWithCollection[];
};

/** Options for the executing a prepared QueryAgent search. */
Expand All @@ -279,6 +279,11 @@ export type SearchExecutionOptions = {
offset?: number;
};

export type SearchModeResponse<T> = MappedSearchModeResponse<T> & {
next: (options: SearchExecutionOptions) => Promise<SearchModeResponse<T>>;
export type SearchModeResponse = {
originalQuery: string;
searches?: SearchResult[];
usage: Usage;
totalTime: number;
searchResults: WeaviateReturnWithCollection;
next: (options: SearchExecutionOptions) => Promise<SearchModeResponse>;
};
23 changes: 11 additions & 12 deletions src/query/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,17 @@ 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.
* 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<T> {
export class QueryAgentSearcher {
private agentsHost: string;
private query: string;
private collections: (string | QueryAgentCollectionConfig)[];
Expand Down Expand Up @@ -103,9 +101,10 @@ export class QueryAgentSearcher<T> {
* @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<T>
> {
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.");
}
Expand All @@ -121,9 +120,9 @@ export class QueryAgentSearcher<T> {
if (!response.ok) {
await handleError(await response.text());
}
const parsedResponse = (await response.json()) as ApiSearchModeResponse<T>;
const parsedResponse = (await response.json()) as ApiSearchModeResponse;
const { mappedResponse, apiSearches } =
mapSearchOnlyResponse<T>(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
Expand All @@ -132,7 +131,7 @@ export class QueryAgentSearcher<T> {
}
return {
...mappedResponse,
next: async (options: SearchExecutionOptions = {}) => this.run(options),
next: async (options: SearchExecutionOptions) => this.run(options),
};
}
}