Skip to content
77 changes: 77 additions & 0 deletions src/chat-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { HttpRequests } from "./http-requests.js";
import type {
ChatWorkspaceSettings,
ChatCompletionRequest,
} from "./types/types.js";

/**
* Class for handling chat workspaces.
*
* @see {@link https://www.meilisearch.com/docs/reference/api/chats}
*/
export class ChatWorkspace {
readonly #httpRequest: HttpRequests;
readonly #workspace: string;

constructor(httpRequests: HttpRequests, workspace: string) {
this.#httpRequest = httpRequests;
this.#workspace = workspace;
}

/**
* Get the settings of a chat workspace.
*
* @experimental
* @see {@link https://www.meilisearch.com/docs/reference/api/chats#get-chat-workspace-settings}
*/
async get(): Promise<ChatWorkspaceSettings> {
return await this.#httpRequest.get({
path: `chats/${this.#workspace}/settings`,
});
}

/**
* Update the settings of a chat workspace.
*
* @experimental
* @see {@link https://www.meilisearch.com/docs/reference/api/chats#update-chat-workspace-settings}
*/
async update(
settings: Partial<ChatWorkspaceSettings>,
): Promise<ChatWorkspaceSettings> {
return await this.#httpRequest.patch({
path: `chats/${this.#workspace}/settings`,
body: settings,
});
}

/**
* Reset the settings of a chat workspace.
*
* @experimental
* @see {@link https://www.meilisearch.com/docs/reference/api/chats#reset-chat-workspace-settings}
*/
async reset(): Promise<void> {
await this.#httpRequest.delete({
path: `chats/${this.#workspace}/settings`,
});
}

/**
* Create a chat completion using an OpenAI-compatible interface.
*
* @experimental
* @see {@link https://www.meilisearch.com/docs/reference/api/chats#chat-completions}
*/
async streamCompletion(
completion: ChatCompletionRequest,
): Promise<ReadableStream<Uint8Array>> {
if (!completion.stream) {
throw new Error("The SDK only supports streaming");
}
return await this.#httpRequest.postStream({
path: `chats/${this.#workspace}/chat/completions`,
body: completion,
});
}
}
87 changes: 82 additions & 5 deletions src/http-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,19 +190,18 @@ export class HttpRequests {
}

/**
* Sends a request with {@link fetch} or a custom HTTP client, combining
* parameters and class properties.
* Prepares common request parameters (URL and RequestInit).
*
* @returns A promise containing the response
* @returns Object containing the prepared URL and RequestInit
*/
async #request<T = unknown>({
#prepareRequest({
path,
method,
params,
contentType,
body,
extraRequestInit,
}: MainRequestOptions): Promise<T> {
}: MainRequestOptions): { url: URL; init: RequestInit } {
const url = new URL(path, this.#url);
if (params !== undefined) {
appendRecordToURLSearchParams(url.searchParams, params);
Expand All @@ -219,6 +218,18 @@ export class HttpRequests {
headers: this.#getHeaders(extraRequestInit?.headers, contentType),
};

return { url, init };
}

/**
* Sends a request with {@link fetch} or a custom HTTP client, combining
* parameters and class properties.
*
* @returns A promise containing the response
*/
async #request<T = unknown>(options: MainRequestOptions): Promise<T> {
const { url, init } = this.#prepareRequest(options);

const startTimeout =
this.#requestTimeout !== undefined
? getTimeoutFn(init, this.#requestTimeout)
Expand Down Expand Up @@ -286,4 +297,70 @@ export class HttpRequests {
delete<T = unknown>(options: RequestOptions): Promise<T> {
return this.#request<T>({ ...options, method: "DELETE" });
}

/** Request with POST that returns a stream. */
postStream(options: RequestOptions): Promise<ReadableStream<Uint8Array>> {
return this.#requestStream({ ...options, method: "POST" });
}

/**
* Sends a request that returns a ReadableStream for streaming responses.
*
* @returns A promise containing the response stream
*/
async #requestStream(
options: MainRequestOptions,
): Promise<ReadableStream<Uint8Array>> {
const { url, init } = this.#prepareRequest(options);

const startTimeout =
this.#requestTimeout !== undefined
? getTimeoutFn(init, this.#requestTimeout)
: null;

const stopTimeout = startTimeout?.();

let response: Response;
try {
if (this.#customRequestFn !== undefined) {
const result = await this.#customRequestFn(url, init);
if (!(result instanceof ReadableStream)) {
throw new MeiliSearchError(
"Custom HTTP client must return a ReadableStream for streaming requests",
);
}
return result as ReadableStream<Uint8Array>;
}

response = await fetch(url, init);
} catch (error) {
throw new MeiliSearchRequestError(
url.toString(),
Object.is(error, TIMEOUT_ID)
? new MeiliSearchRequestTimeOutError(this.#requestTimeout!, init)
: error,
);
} finally {
stopTimeout?.();
}

if (!response.ok) {
// For error responses, we still need to read the body to get error details
const responseBody = await response.text();
const parsedResponse =
responseBody === ""
? undefined
: (JSON.parse(responseBody) as MeiliSearchErrorResponse);

throw new MeiliSearchApiError(response, parsedResponse);
}

if (!response.body) {
throw new MeiliSearchError(
"Response body is null - server did not return a readable stream",
);
}

return response.body;
}
}
30 changes: 30 additions & 0 deletions src/indexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import type {
PrefixSearch,
RecordAny,
EnqueuedTaskPromise,
ChatSettings,
ChatSettingsPayload,
} from "./types/index.js";
import { HttpRequests } from "./http-requests.js";
import {
Expand Down Expand Up @@ -1377,4 +1379,32 @@ export class Index<T extends RecordAny = RecordAny> {
path: `indexes/${this.uid}/settings/prefix-search`,
});
}

///
/// CHAT SETTINGS
///

/**
* Get the index's chat settings.
*
* @returns Promise containing a ChatSettings object
*/
async getChat(): Promise<ChatSettings> {
return await this.httpRequest.get<ChatSettings>({
path: `indexes/${this.uid}/settings/chat`,
});
}

/**
* Update the index's chat settings.
*
* @param chatSettings - ChatSettingsPayload object
* @returns Promise containing an EnqueuedTask
*/
updateChat(chatSettings: ChatSettingsPayload): EnqueuedTaskPromise {
return this.#httpRequestsWithTask.patch({
path: `indexes/${this.uid}/settings/chat`,
body: chatSettings,
});
}
}
27 changes: 27 additions & 0 deletions src/meilisearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
Network,
RecordAny,
RuntimeTogglableFeatures,
ResourceResults,
Webhook,
ResultsWrapper,
WebhookCreatePayload,
Expand All @@ -42,6 +43,7 @@ import {
type HttpRequestsWithEnqueuedTaskPromise,
} from "./task.js";
import { BatchClient } from "./batch.js";
import { ChatWorkspace } from "./chat-workspace.js";
import type { MeiliSearchApiError } from "./errors/index.js";

export class MeiliSearch {
Expand Down Expand Up @@ -278,6 +280,31 @@ export class MeiliSearch {
});
}

///
/// CHATS
///

/**
* Get a chat workspace instance
*
* @param workspace - The chat workspace UID
* @returns Instance of ChatWorkspace
*/
chat(workspace: string): ChatWorkspace {
return new ChatWorkspace(this.httpRequest, workspace);
}

/**
* Get all chat workspaces
*
* @returns Promise returning an array of chat workspaces UIDs
*/
async getChatWorkspaces(): Promise<ResourceResults<{ uid: string }[]>> {
return await this.httpRequest.get({
path: "chats",
});
}

///
/// WEBHOOKS
///
Expand Down
41 changes: 41 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,47 @@ export type Stats = {
};
};

/*
** CHATS
*/

/** @see https://www.meilisearch.com/docs/reference/api/chats#settings-parameters */
export type ChatWorkspaceSettings = {
source: "openAi" | "azureOpenAi" | "mistral" | "gemini" | "vLlm";
orgId?: string;
projectId?: string;
apiVersion?: string;
deploymentId?: string;
baseUrl?: string;
apiKey: string;
prompts: {
system: string;
};
};

export type ChatCompletionRequest = {
model: string;
messages: {
role: "user" | "assistant" | "system";
content: string;
}[];
stream: boolean;
};

export type ChatSettings = {
description: string;
documentTemplate: string;
documentTemplateMaxBytes: number;
searchParameters: SearchParams;
};

export type ChatSettingsPayload = {
description?: string;
documentTemplate?: string;
documentTemplateMaxBytes?: number;
searchParameters?: Partial<SearchParams>;
};

/*
** Keys
*/
Expand Down
Loading