Skip to content

Commit 07e6ab7

Browse files
authored
Merge pull request #1133 from EazyAl/development
Added support for conversational search
2 parents e67675f + 6ce0850 commit 07e6ab7

File tree

3 files changed

+388
-2
lines changed

3 files changed

+388
-2
lines changed

meilisearch/_httprequests.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,72 @@ def delete(
146146
) -> Any:
147147
return self.send_request(requests.delete, path, body)
148148

149+
def post_stream(
150+
self,
151+
path: str,
152+
body: Optional[
153+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], bytes, str]
154+
] = None,
155+
content_type: Optional[str] = "application/json",
156+
*,
157+
serializer: Optional[Type[json.JSONEncoder]] = None,
158+
) -> requests.Response:
159+
"""Send a POST request with streaming enabled.
160+
161+
Returns the raw response object for streaming consumption.
162+
"""
163+
if content_type:
164+
self.headers["Content-Type"] = content_type
165+
try:
166+
request_path = self.config.url + "/" + path
167+
168+
if isinstance(body, bytes):
169+
response = requests.post(
170+
request_path,
171+
timeout=self.config.timeout,
172+
headers=self.headers,
173+
data=body,
174+
stream=True,
175+
)
176+
else:
177+
serialize_body = isinstance(body, dict) or body
178+
data = (
179+
json.dumps(body, cls=serializer)
180+
if isinstance(body, bool) or serialize_body
181+
else "" if body == "" else "null"
182+
)
183+
184+
response = requests.post(
185+
request_path,
186+
timeout=self.config.timeout,
187+
headers=self.headers,
188+
data=data,
189+
stream=True,
190+
)
191+
192+
# For streaming responses, we validate status but don't parse JSON
193+
if not response.ok:
194+
response.raise_for_status()
195+
196+
return response
197+
198+
except requests.exceptions.Timeout as err:
199+
raise MeilisearchTimeoutError(str(err)) from err
200+
except requests.exceptions.ConnectionError as err:
201+
raise MeilisearchCommunicationError(str(err)) from err
202+
except requests.exceptions.HTTPError as err:
203+
raise MeilisearchApiError(str(err), response) from err
204+
except requests.exceptions.InvalidSchema as err:
205+
if "://" not in self.config.url:
206+
raise MeilisearchCommunicationError(
207+
f"""
208+
Invalid URL {self.config.url}, no scheme/protocol supplied.
209+
Did you mean https://{self.config.url}?
210+
"""
211+
) from err
212+
213+
raise MeilisearchCommunicationError(str(err)) from err
214+
149215
@staticmethod
150216
def __to_json(request: requests.Response) -> Any:
151217
if request.content == b"":

meilisearch/client.py

Lines changed: 191 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,27 @@
88
import hmac
99
import json
1010
import re
11-
from typing import Any, Dict, List, Mapping, MutableMapping, Optional, Sequence, Tuple, Union
11+
from typing import (
12+
Any,
13+
Dict,
14+
Iterator,
15+
List,
16+
Mapping,
17+
MutableMapping,
18+
Optional,
19+
Sequence,
20+
Tuple,
21+
Union,
22+
)
1223
from urllib import parse
1324

1425
from meilisearch._httprequests import HttpRequests
1526
from meilisearch.config import Config
16-
from meilisearch.errors import MeilisearchError
27+
from meilisearch.errors import ( # noqa: F401
28+
MeilisearchApiError,
29+
MeilisearchCommunicationError,
30+
MeilisearchError,
31+
)
1732
from meilisearch.index import Index
1833
from meilisearch.models.key import Key, KeysResults
1934
from meilisearch.models.task import Batch, BatchResults, Task, TaskInfo, TaskResults
@@ -28,6 +43,9 @@ class Client:
2843
Meilisearch and its permissions.
2944
"""
3045

46+
# Import aliases to satisfy pylint (used in docstrings)
47+
MeilisearchApiError = MeilisearchApiError
48+
3149
def __init__(
3250
self,
3351
url: str,
@@ -795,6 +813,177 @@ def get_all_networks(self) -> Dict[str, str]:
795813
"""
796814
return self.http.get(path=f"{self.config.paths.network}")
797815

816+
def create_chat_completion(
817+
self,
818+
workspace_uid: str,
819+
messages: List[Dict[str, str]],
820+
model: str = "gpt-3.5-turbo",
821+
stream: bool = True,
822+
) -> Iterator[Dict[str, Any]]:
823+
"""Streams a chat completion from the Meilisearch chat API.
824+
825+
Parameters
826+
----------
827+
workspace_uid:
828+
Unique identifier of the chat workspace to use.
829+
messages:
830+
List of message dicts (e.g. {"role": "user", "content": "..."}) comprising the chat history.
831+
model:
832+
The model name to use for completion (should correspond to the LLM in workspace settings).
833+
stream:
834+
Whether to stream the response. Must be True for now (only streaming is supported).
835+
836+
Returns
837+
-------
838+
chunks:
839+
Parsed chunks of the completion as Python dicts. Each chunk is a partial response (in OpenAI format).
840+
Iteration ends when the completion is done.
841+
842+
Raises
843+
------
844+
MeilisearchApiError
845+
An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors
846+
MeilisearchCommunicationError
847+
If a network error occurs.
848+
ValueError
849+
If stream=False is passed (not currently supported), or if workspace_uid is empty or contains path separators.
850+
"""
851+
if not stream:
852+
# The API currently only supports streaming responses:
853+
raise ValueError("Non-streaming chat completions are not supported. Use stream=True.")
854+
855+
# Basic security validation (only what's needed)
856+
if not workspace_uid:
857+
raise ValueError("workspace_uid is required and cannot be empty")
858+
if "/" in workspace_uid or "\\" in workspace_uid:
859+
raise ValueError("Invalid workspace_uid: must not contain path separators")
860+
861+
payload = {"model": model, "messages": messages, "stream": True}
862+
863+
# Construct the URL for the chat completions route.
864+
endpoint = f"chats/{workspace_uid}/chat/completions"
865+
866+
# Initiate the HTTP POST request in streaming mode.
867+
response = self.http.post_stream(endpoint, body=payload)
868+
869+
try:
870+
# Iterate over the streaming response lines
871+
for raw_line in response.iter_lines():
872+
if raw_line is None or raw_line == b"":
873+
continue
874+
875+
line = raw_line.decode("utf-8")
876+
if line.startswith("data: "):
877+
data = line[len("data: ") :]
878+
if data.strip() == "[DONE]":
879+
break
880+
881+
try:
882+
chunk = json.loads(data)
883+
yield chunk
884+
except json.JSONDecodeError as e:
885+
raise MeilisearchCommunicationError(
886+
f"Failed to parse chat chunk: {e}"
887+
) from e
888+
finally:
889+
response.close()
890+
891+
def get_chat_workspaces(
892+
self,
893+
*,
894+
offset: Optional[int] = None,
895+
limit: Optional[int] = None,
896+
) -> Dict[str, Any]:
897+
"""Get all chat workspaces.
898+
899+
Parameters
900+
----------
901+
offset (optional):
902+
Number of workspaces to skip.
903+
limit (optional):
904+
Maximum number of workspaces to return.
905+
906+
Returns
907+
-------
908+
workspaces
909+
Dictionary containing the list of chat workspaces and pagination information.
910+
911+
Raises
912+
------
913+
MeilisearchApiError
914+
An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors
915+
"""
916+
params = {}
917+
if offset is not None:
918+
params["offset"] = offset
919+
if limit is not None:
920+
params["limit"] = limit
921+
path = "chats" + ("?" + parse.urlencode(params) if params else "")
922+
return self.http.get(path)
923+
924+
def get_chat_workspace_settings(self, workspace_uid: str) -> Dict[str, Any]:
925+
"""Get the settings for a specific chat workspace.
926+
927+
Parameters
928+
----------
929+
workspace_uid:
930+
Unique identifier of the chat workspace.
931+
932+
Returns
933+
-------
934+
settings:
935+
Dictionary containing the workspace settings.
936+
937+
Raises
938+
------
939+
MeilisearchApiError
940+
An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors
941+
ValueError
942+
If workspace_uid is empty or contains path separators.
943+
"""
944+
# Basic security validation (only what's needed)
945+
if not workspace_uid:
946+
raise ValueError("workspace_uid is required and cannot be empty")
947+
if "/" in workspace_uid or "\\" in workspace_uid:
948+
raise ValueError("Invalid workspace_uid: must not contain path separators")
949+
950+
return self.http.get(f"chats/{workspace_uid}/settings")
951+
952+
def update_chat_workspace_settings(
953+
self, workspace_uid: str, settings: Mapping[str, Any]
954+
) -> Dict[str, Any]:
955+
"""Update the settings for a specific chat workspace.
956+
957+
Parameters
958+
----------
959+
workspace_uid:
960+
Unique identifier of the chat workspace.
961+
settings:
962+
Dictionary containing the settings to update.
963+
964+
Returns
965+
-------
966+
settings:
967+
Dictionary containing the updated workspace settings.
968+
969+
Raises
970+
------
971+
MeilisearchApiError
972+
An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors
973+
ValueError
974+
If workspace_uid is empty or contains path separators, or if settings is empty.
975+
"""
976+
# Basic security validation (only what's needed)
977+
if not workspace_uid:
978+
raise ValueError("workspace_uid is required and cannot be empty")
979+
if "/" in workspace_uid or "\\" in workspace_uid:
980+
raise ValueError("Invalid workspace_uid: must not contain path separators")
981+
982+
if not settings:
983+
raise ValueError("settings cannot be empty")
984+
985+
return self.http.patch(f"chats/{workspace_uid}/settings", body=settings)
986+
798987
@staticmethod
799988
def _base64url_encode(data: bytes) -> str:
800989
return base64.urlsafe_b64encode(data).decode("utf-8").replace("=", "")

0 commit comments

Comments
 (0)