Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -0,0 +1,13 @@
|
|||
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,130 @@
|
|||
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import json
|
||||
import time
|
||||
from collections import deque
|
||||
from collections.abc import Iterator
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
import httpx
|
||||
|
||||
from ..utils._headers import build_hf_headers
|
||||
from ..utils._http import hf_raise_for_status
|
||||
from .sse_client import SSEClient
|
||||
from .types import ApiGetReloadEventSourceData, ApiGetReloadRequest
|
||||
|
||||
|
||||
HOT_RELOADING_PORT = 7887
|
||||
CLIENT_TIMEOUT = 20
|
||||
|
||||
|
||||
class MultiReplicaStreamWarning(TypedDict):
|
||||
kind: Literal["warning"]
|
||||
message: str
|
||||
|
||||
|
||||
class MultiReplicaStreamEvent(TypedDict):
|
||||
kind: Literal["event"]
|
||||
event: ApiGetReloadEventSourceData
|
||||
|
||||
|
||||
class MultiReplicaStreamReplicaHash(TypedDict):
|
||||
kind: Literal["replicaHash"]
|
||||
hash: str
|
||||
|
||||
|
||||
class MultiReplicaStreamFullMatch(TypedDict):
|
||||
kind: Literal["fullMatch"]
|
||||
|
||||
|
||||
class ReloadClient:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
host: str,
|
||||
subdomain: str,
|
||||
replica_hash: str,
|
||||
token: str | None,
|
||||
):
|
||||
base_host = host.replace(subdomain, f"{subdomain}--{HOT_RELOADING_PORT}")
|
||||
self.replica_hash = replica_hash
|
||||
self.client = httpx.Client(
|
||||
base_url=f"{base_host}/--replicas/+{replica_hash}",
|
||||
headers=build_hf_headers(token=token),
|
||||
timeout=CLIENT_TIMEOUT,
|
||||
)
|
||||
|
||||
def get_reload(self, reload_id: str) -> Iterator[ApiGetReloadEventSourceData] | int:
|
||||
req = ApiGetReloadRequest(reloadId=reload_id)
|
||||
with self.client.stream("POST", "/get-reload", json=req) as res:
|
||||
if res.status_code != 200:
|
||||
return res.status_code
|
||||
hf_raise_for_status(res)
|
||||
for event in SSEClient(res.iter_bytes()).events():
|
||||
if event.event == "message":
|
||||
yield json.loads(event.data)
|
||||
return None
|
||||
|
||||
|
||||
def multi_replica_reload_events(
|
||||
commit_sha: str,
|
||||
host: str,
|
||||
subdomain: str,
|
||||
replica_hashes: list[str],
|
||||
token: str | None,
|
||||
max_retries: int = 10,
|
||||
) -> Iterator[
|
||||
MultiReplicaStreamWarning | MultiReplicaStreamEvent | MultiReplicaStreamReplicaHash | MultiReplicaStreamFullMatch
|
||||
]:
|
||||
clients = [
|
||||
ReloadClient(
|
||||
host=host,
|
||||
subdomain=subdomain,
|
||||
replica_hash=hash,
|
||||
token=token,
|
||||
)
|
||||
for hash in replica_hashes
|
||||
]
|
||||
|
||||
first_client_events: dict[int, ApiGetReloadEventSourceData] = {}
|
||||
for client_index, client in enumerate(clients):
|
||||
if len(clients) > 1:
|
||||
yield {"kind": "replicaHash", "hash": client.replica_hash}
|
||||
|
||||
retries = 0
|
||||
while isinstance((events := client.get_reload(commit_sha)), int):
|
||||
if (retries := retries + 1) > max_retries:
|
||||
raise Exception("Too many retries reached")
|
||||
if (status_code := events) not in (200, 204):
|
||||
raise Exception(f"Unexpected {status_code=} on `ReloadClient.get_reload`")
|
||||
subject = "reloadId" if status_code == 204 else "replica"
|
||||
yield {"kind": "warning", "message": f"Retrying on unexpected {subject} not found"}
|
||||
time.sleep(2)
|
||||
|
||||
full_match = True
|
||||
replay: deque[ApiGetReloadEventSourceData] = deque()
|
||||
for event_index, event in enumerate(events):
|
||||
if client_index == 0:
|
||||
first_client_events[event_index] = event
|
||||
elif full_match := full_match and first_client_events.get(event_index) == event:
|
||||
replay.append(event)
|
||||
continue
|
||||
while replay:
|
||||
yield {"kind": "event", "event": replay.popleft()}
|
||||
yield {"kind": "event", "event": event}
|
||||
|
||||
if client_index > 0 and full_match:
|
||||
yield {"kind": "fullMatch"}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
"""
|
||||
Vendored file: Server Side Events (SSE) client for Python.
|
||||
|
||||
Source:
|
||||
- Author: Maxime Petazzoni <maxime.petazzoni@bulix.org>
|
||||
- Repository: https://github.com/mpetazzoni/sseclient
|
||||
- File: https://github.com/mpetazzoni/sseclient/blob/main/sseclient/__init__.py
|
||||
|
||||
License:
|
||||
- Apache-2.0 (from upstream project)
|
||||
|
||||
Provides a generator of SSE received through an existing HTTP response.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
__author__ = 'Maxime Petazzoni <maxime.petazzoni@bulix.org>'
|
||||
__email__ = 'maxime.petazzoni@bulix.org'
|
||||
__all__ = ['SSEClient']
|
||||
|
||||
_FIELD_SEPARATOR = ':'
|
||||
|
||||
|
||||
class SSEClient:
|
||||
"""Implementation of a SSE client.
|
||||
|
||||
See http://www.w3.org/TR/2009/WD-eventsource-20091029/ for the
|
||||
specification.
|
||||
"""
|
||||
|
||||
def __init__(self, event_source, char_enc='utf-8'):
|
||||
"""Initialize the SSE client over an existing, ready to consume
|
||||
event source.
|
||||
|
||||
The event source is expected to be a binary stream and have a close()
|
||||
method. That would usually be something that implements
|
||||
io.BinaryIOBase, like an httplib or urllib3 HTTPResponse object.
|
||||
"""
|
||||
self._logger = logging.getLogger(self.__class__.__module__)
|
||||
self._logger.debug('Initialized SSE client from event source %s',
|
||||
event_source)
|
||||
self._event_source = event_source
|
||||
self._char_enc = char_enc
|
||||
|
||||
def _read(self):
|
||||
"""Read the incoming event source stream and yield event chunks.
|
||||
|
||||
Unfortunately it is possible for some servers to decide to break an
|
||||
event into multiple HTTP chunks in the response. It is thus necessary
|
||||
to correctly stitch together consecutive response chunks and find the
|
||||
SSE delimiter (empty new line) to yield full, correct event chunks."""
|
||||
data = b''
|
||||
for chunk in self._event_source:
|
||||
for line in chunk.splitlines(True):
|
||||
data += line
|
||||
if data.endswith((b'\r\r', b'\n\n', b'\r\n\r\n')):
|
||||
yield data
|
||||
data = b''
|
||||
if data:
|
||||
yield data
|
||||
|
||||
def events(self):
|
||||
for chunk in self._read():
|
||||
event = Event()
|
||||
# Split before decoding so splitlines() only uses \r and \n
|
||||
for line in chunk.splitlines():
|
||||
# Decode the line.
|
||||
line = line.decode(self._char_enc)
|
||||
|
||||
# Lines starting with a separator are comments and are to be
|
||||
# ignored.
|
||||
if not line.strip() or line.startswith(_FIELD_SEPARATOR):
|
||||
continue
|
||||
|
||||
data = line.split(_FIELD_SEPARATOR, 1)
|
||||
field = data[0]
|
||||
|
||||
# Ignore unknown fields.
|
||||
if field not in event.__dict__:
|
||||
self._logger.debug('Saw invalid field %s while parsing '
|
||||
'Server Side Event', field)
|
||||
continue
|
||||
|
||||
if len(data) > 1:
|
||||
# From the spec:
|
||||
# "If value starts with a single U+0020 SPACE character,
|
||||
# remove it from value."
|
||||
if data[1].startswith(' '):
|
||||
value = data[1][1:]
|
||||
else:
|
||||
value = data[1]
|
||||
else:
|
||||
# If no value is present after the separator,
|
||||
# assume an empty value.
|
||||
value = ''
|
||||
|
||||
# The data field may come over multiple lines and their values
|
||||
# are concatenated with each other.
|
||||
if field == 'data':
|
||||
event.__dict__[field] += value + '\n'
|
||||
else:
|
||||
event.__dict__[field] = value
|
||||
|
||||
# Events with no data are not dispatched.
|
||||
if not event.data:
|
||||
continue
|
||||
|
||||
# If the data field ends with a newline, remove it.
|
||||
if event.data.endswith('\n'):
|
||||
event.data = event.data[0:-1]
|
||||
|
||||
# Empty event names default to 'message'
|
||||
event.event = event.event or 'message'
|
||||
|
||||
# Dispatch the event
|
||||
self._logger.debug('Dispatching %s...', event)
|
||||
yield event
|
||||
|
||||
def close(self):
|
||||
"""Manually close the event source stream."""
|
||||
self._event_source.close()
|
||||
|
||||
|
||||
class Event:
|
||||
"""Representation of an event from the event stream."""
|
||||
|
||||
def __init__(self, id=None, event='message', data='', retry=None):
|
||||
self.id = id
|
||||
self.event = event
|
||||
self.data = data
|
||||
self.retry = retry
|
||||
|
||||
def __str__(self):
|
||||
s = f'{self.event} event'
|
||||
if self.id:
|
||||
s += f' #{self.id}'
|
||||
if self.data:
|
||||
s += ', {} byte{}'.format(len(self.data),
|
||||
's' if len(self.data) else '')
|
||||
else:
|
||||
s += ', no data'
|
||||
if self.retry:
|
||||
s += f', retry in {self.retry}ms'
|
||||
return s
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
|
||||
class ReloadRegion(TypedDict):
|
||||
startLine: int
|
||||
startCol: int
|
||||
endLine: int
|
||||
endCol: int
|
||||
|
||||
|
||||
class ReloadOperationObject(TypedDict):
|
||||
kind: Literal["add", "update", "delete"]
|
||||
region: ReloadRegion
|
||||
objectType: str
|
||||
objectName: str
|
||||
|
||||
|
||||
class ReloadOperationRun(TypedDict):
|
||||
kind: Literal["run"]
|
||||
region: ReloadRegion
|
||||
codeLines: str
|
||||
stdout: NotRequired[str]
|
||||
stderr: NotRequired[str]
|
||||
|
||||
|
||||
class ReloadOperationException(TypedDict):
|
||||
kind: Literal["exception"]
|
||||
region: ReloadRegion
|
||||
traceback: str
|
||||
|
||||
|
||||
class ReloadOperationError(TypedDict):
|
||||
kind: Literal["error"]
|
||||
traceback: str
|
||||
|
||||
|
||||
class ReloadOperationUI(TypedDict):
|
||||
kind: Literal["ui"]
|
||||
updated: bool
|
||||
|
||||
|
||||
class ReloadOperationFile(TypedDict):
|
||||
kind: Literal["file"]
|
||||
created: bool
|
||||
|
||||
|
||||
class ApiCreateReloadRequest(TypedDict):
|
||||
filepath: str
|
||||
contents: str
|
||||
reloadId: NotRequired[str]
|
||||
|
||||
|
||||
class ApiCreateReloadResponseSuccess(TypedDict):
|
||||
status: Literal["created"]
|
||||
reloadId: str
|
||||
|
||||
|
||||
class ApiCreateReloadResponseError(TypedDict):
|
||||
status: Literal["alreadyReloading", "fileNotFound"]
|
||||
|
||||
|
||||
class ApiCreateReloadResponse(TypedDict):
|
||||
res: ApiCreateReloadResponseError | ApiCreateReloadResponseSuccess
|
||||
|
||||
|
||||
class ApiGetReloadRequest(TypedDict):
|
||||
reloadId: str
|
||||
|
||||
|
||||
class ApiGetReloadEventSourceData(TypedDict):
|
||||
data: (
|
||||
ReloadOperationError
|
||||
| ReloadOperationException
|
||||
| ReloadOperationObject
|
||||
| ReloadOperationRun
|
||||
| ReloadOperationUI
|
||||
| ReloadOperationFile
|
||||
)
|
||||
|
||||
|
||||
class ApiGetStatusRequest(TypedDict):
|
||||
revision: str
|
||||
|
||||
|
||||
class ApiGetStatusResponse(TypedDict):
|
||||
reloading: bool
|
||||
uncommited: list[str]
|
||||
|
||||
|
||||
class ApiFetchContentsRequest(TypedDict):
|
||||
filepath: str
|
||||
|
||||
|
||||
class ApiFetchContentsResponseError(TypedDict):
|
||||
status: Literal["fileNotFound"]
|
||||
|
||||
|
||||
class ApiFetchContentsResponseSuccess(TypedDict):
|
||||
status: Literal["ok"]
|
||||
contents: str
|
||||
|
||||
|
||||
class ApiFetchContentsResponse(TypedDict):
|
||||
res: ApiFetchContentsResponseError | ApiFetchContentsResponseSuccess
|
||||
Loading…
Add table
Add a link
Reference in a new issue