Skip to content

Commit 9cbf6c2

Browse files
authored
Merge branch 'meilisearch:main' into make-task-results-from-optional
2 parents a79bb85 + d79f4ab commit 9cbf6c2

File tree

9 files changed

+390
-28
lines changed

9 files changed

+390
-28
lines changed

.github/release-draft-template.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ replacers:
3131
replace: ''
3232
- search: '/(?:and )?@dependabot(?:\[bot\])?,?/g'
3333
replace: ''
34-
- search: '/(?:and )?@bors(?:\[bot\])?,?/g'
35-
replace: ''
36-
- search: '/(?:and )?@meili-bors(?:\[bot\])?,?/g'
37-
replace: ''
3834
- search: '/(?:and )?@meili-bot,?/g'
3935
replace: ''
4036
- search: '/(?:and )?@meili-bot(?:\[bot\])?,?/g'

.github/workflows/tests.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ name: Tests
33
on:
44
pull_request:
55
push:
6-
# trying and staging branches are for BORS config
76
branches:
8-
- trying
9-
- staging
107
- main
118
merge_group:
129

CONTRIBUTING.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,19 +108,14 @@ Some notes on GitHub PRs:
108108

109109
- [Convert your PR as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request) if your changes are a work in progress: no one will review it until you pass your PR as ready for review.<br>
110110
The draft PR can be very useful if you want to show that you are working on something and make your work visible.
111-
- The branch related to the PR must be **up-to-date with `main`** before merging. Fortunately, this project [integrates a bot](https://github.com/meilisearch/integration-guides/blob/main/resources/bors.md) to automatically enforce this requirement without the PR author having to do it manually.
111+
- The branch related to the PR must be **up-to-date with `main`** before merging.
112112
- All PRs must be reviewed and approved by at least one maintainer.
113113
- The PR title should be accurate and descriptive of the changes. The title of the PR will be indeed automatically added to the next [release changelogs](https://github.com/meilisearch/meilisearch-python/releases/).
114114

115115
## Release Process (for the internal team only)
116116

117117
Meilisearch tools follow the [Semantic Versioning Convention](https://semver.org/).
118118

119-
### Automation to Rebase and Merge the PRs <!-- omit in toc -->
120-
121-
This project integrates a bot that helps us manage pull requests merging.<br>
122-
_[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/bors.md)._
123-
124119
### Automated Changelogs <!-- omit in toc -->
125120

126121
This project integrates a tool to create automated changelogs.<br>

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
<a href="https://badge.fury.io/py/meilisearch"><img src="https://badge.fury.io/py/meilisearch.svg" alt="PyPI version"></a>
1919
<a href="https://github.com/meilisearch/meilisearch-python/actions"><img src="https://github.com/meilisearch/meilisearch-python/workflows/Tests/badge.svg" alt="Test Status"></a>
2020
<a href="https://github.com/meilisearch/meilisearch-python/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-informational" alt="License"></a>
21-
<a href="https://ms-bors.herokuapp.com/repositories/57"><img src="https://bors.tech/images/badge_small.svg" alt="Bors enabled"></a>
2221
</p>
2322

2423
<p align="center">⚡ The Meilisearch API client written for Python 🐍</p>

bors.toml

Lines changed: 0 additions & 11 deletions
This file was deleted.

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("=", "")

meilisearch/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
__version__ = "0.36.0"
3+
__version__ = "0.37.0"
44

55

66
def qualified_version() -> str:

0 commit comments

Comments
 (0)