diff --git a/python/.cspell.json b/python/.cspell.json index ea24ad2d7ce4..5a7e0eba650d 100644 --- a/python/.cspell.json +++ b/python/.cspell.json @@ -76,6 +76,7 @@ "SEMANTICKERNEL", "OTEL", "vectorizable", - "desync" + "desync", + "webrtc" ] } \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml index 0dc38b0b57f9..5b9f91d5536b 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -45,6 +45,7 @@ dependencies = [ "pybars4 ~= 0.9", "jinja2 ~= 3.1", "nest-asyncio ~= 1.6", + "taskgroup >= 0.2.2; python_version < '3.11'", ] ### Optional dependencies @@ -61,7 +62,8 @@ chroma = [ ] google = [ "google-cloud-aiplatform ~= 1.60", - "google-generativeai ~= 0.7" + "google-generativeai ~= 0.7", + "google-genai ~= 0.4" ] hugging_face = [ "transformers[torch] ~= 4.28", @@ -123,6 +125,11 @@ dapr = [ "dapr-ext-fastapi>=1.14.0", "flask-dapr>=1.14.0" ] +openai_realtime = [ + "openai[realtime] ~= 1.0", + "aiortc>=1.9.0", + "sounddevice>=0.5.1", +] [tool.uv] prerelease = "if-necessary-or-explicit" @@ -220,5 +227,3 @@ name = "semantic_kernel" [build-system] requires = ["flit-core >= 3.9,<4.0"] build-backend = "flit_core.buildapi" - - diff --git a/python/samples/concepts/realtime/01-chat_with_realtime_webrtc.py b/python/samples/concepts/realtime/01-chat_with_realtime_webrtc.py new file mode 100644 index 000000000000..38d3803a737b --- /dev/null +++ b/python/samples/concepts/realtime/01-chat_with_realtime_webrtc.py @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import logging + +from samples.concepts.realtime.utils import AudioPlayerWebRTC, AudioRecorderWebRTC, check_audio_devices +from semantic_kernel.connectors.ai.open_ai import ( + ListenEvents, + OpenAIRealtime, + OpenAIRealtimeExecutionSettings, + TurnDetection, +) + +logging.basicConfig(level=logging.WARNING) +utils_log = logging.getLogger("samples.concepts.realtime.utils") +utils_log.setLevel(logging.INFO) +aiortc_log = logging.getLogger("aiortc") +aiortc_log.setLevel(logging.WARNING) +aioice_log = logging.getLogger("aioice") +aioice_log.setLevel(logging.WARNING) +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# This simple sample demonstrates how to use the OpenAI Realtime API to create +# a chat bot that can listen and respond directly through audio. +# It requires installing: +# - semantic-kernel[openai_realtime] +# - pyaudio +# - sounddevice +# - pydub +# - aiortc +# e.g. pip install pyaudio sounddevice pydub + +# The characterics of your speaker and microphone are a big factor in a smooth conversation +# so you may need to try out different devices for each. +# you can also play around with the turn_detection settings to get the best results. +# It has device id's set in the AudioRecorderStream and AudioPlayerAsync classes, +# so you may need to adjust these for your system. +# you can check the available devices by uncommenting line below the function +check_audio_devices() + + +async def main() -> None: + # create the realtime client and optionally add the audio output function, this is optional + # you can define the protocol to use, either "websocket" or "webrtc" + # they will behave the same way, even though the underlying protocol is quite different + audio_player = AudioPlayerWebRTC() + realtime_client = OpenAIRealtime( + "webrtc", + audio_output_callback=audio_player.client_callback, + audio_track=AudioRecorderWebRTC(), + ) + # Create the settings for the session + settings = OpenAIRealtimeExecutionSettings( + instructions=""" + You are a chat bot. Your name is Mosscap and + you have one goal: figure out what people need. + Your full name, should you need to know it, is + Splendid Speckled Mosscap. You communicate + effectively, but you tend to answer with long + flowery prose. + """, + voice="alloy", + turn_detection=TurnDetection(type="server_vad", create_response=True, silence_duration_ms=800, threshold=0.8), + ) + # the context manager calls the create_session method on the client and start listening to the audio stream + + print("Mosscap (transcript): ", end="") + async with realtime_client, audio_player: + await realtime_client.update_session(settings=settings, create_response=True) + + async for event in realtime_client.receive(): + match event.event_type: + # case "audio": + # await audio_player.add_audio(event.audio) + case "text": + print(event.text.text, end="") + case "service": + # OpenAI Specific events + if event.service_type == ListenEvents.SESSION_UPDATED: + print("Session updated") + if event.service_type == ListenEvents.RESPONSE_CREATED: + print("") + if event.service_type == ListenEvents.ERROR: + logger.error(event.event) + + +if __name__ == "__main__": + print( + "Instruction: start speaking, when you stop the API should detect you finished and start responding. " + "Press ctrl + c to stop the program." + ) + asyncio.run(main()) diff --git a/python/samples/concepts/realtime/01-chat_with_realtime_websocket.py b/python/samples/concepts/realtime/01-chat_with_realtime_websocket.py new file mode 100644 index 000000000000..e647da6ff4a9 --- /dev/null +++ b/python/samples/concepts/realtime/01-chat_with_realtime_websocket.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import logging + +from samples.concepts.realtime.utils import AudioPlayerWebsocket, AudioRecorderWebsocket, check_audio_devices +from semantic_kernel.connectors.ai.open_ai import ( + ListenEvents, + OpenAIRealtime, + OpenAIRealtimeExecutionSettings, + TurnDetection, +) + +logging.basicConfig(level=logging.WARNING) +utils_log = logging.getLogger("samples.concepts.realtime.utils") +utils_log.setLevel(logging.INFO) +aiortc_log = logging.getLogger("aiortc") +aiortc_log.setLevel(logging.WARNING) +aioice_log = logging.getLogger("aioice") +aioice_log.setLevel(logging.WARNING) +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# This simple sample demonstrates how to use the OpenAI Realtime API to create +# a chat bot that can listen and respond directly through audio. +# It requires installing: +# - semantic-kernel[openai_realtime] +# - pyaudio +# - sounddevice +# - pydub +# - aiortc +# e.g. pip install pyaudio sounddevice pydub + +# The characterics of your speaker and microphone are a big factor in a smooth conversation +# so you may need to try out different devices for each. +# you can also play around with the turn_detection settings to get the best results. +# It has device id's set in the AudioRecorderStream and AudioPlayerAsync classes, +# so you may need to adjust these for your system. +# you can check the available devices by uncommenting line below the function +check_audio_devices() + + +async def main() -> None: + # create the realtime client and optionally add the audio output function, this is optional + # you can define the protocol to use, either "websocket" or "webrtc" + # they will behave the same way, even though the underlying protocol is quite different + audio_player = AudioPlayerWebsocket() + realtime_client = OpenAIRealtime( + "websocket", + audio_output_callback=audio_player.client_callback, + ) + audio_recorder = AudioRecorderWebsocket(realtime_client=realtime_client) + # Create the settings for the session + settings = OpenAIRealtimeExecutionSettings( + instructions=""" + You are a chat bot. Your name is Mosscap and + you have one goal: figure out what people need. + Your full name, should you need to know it, is + Splendid Speckled Mosscap. You communicate + effectively, but you tend to answer with long + flowery prose. + """, + voice="shimmer", + turn_detection=TurnDetection(type="server_vad", create_response=True, silence_duration_ms=800, threshold=0.8), + ) + # the context manager calls the create_session method on the client and start listening to the audio stream + print("Mosscap (transcript): ", end="") + + async with realtime_client, audio_player, audio_recorder: + await realtime_client.update_session(settings=settings, create_response=True) + + async for event in realtime_client.receive(): + match event.event_type: + # this can be used as an alternative to the callback function used above, + # the callback is faster and smoother + # case "audio": + # await audio_player.add_audio(event.audio) + case "text": + print(event.text.text, end="") + case "service": + # OpenAI Specific events + if event.service_type == ListenEvents.SESSION_UPDATED: + print("Session updated") + if event.service_type == ListenEvents.RESPONSE_CREATED: + print("") + if event.service_type == ListenEvents.ERROR: + logger.error(event.event) + + +if __name__ == "__main__": + print( + "Instruction: start speaking, when you stop the API should detect you finished and start responding. " + "Press ctrl + c to stop the program." + ) + asyncio.run(main()) diff --git a/python/samples/concepts/realtime/02-chat_with_function_calling.py b/python/samples/concepts/realtime/02-chat_with_function_calling.py new file mode 100644 index 000000000000..c74b6b583d23 --- /dev/null +++ b/python/samples/concepts/realtime/02-chat_with_function_calling.py @@ -0,0 +1,145 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import logging +from datetime import datetime +from random import randint + +from samples.concepts.realtime.utils import AudioPlayerWebRTC, AudioRecorderWebRTC, check_audio_devices +from semantic_kernel import Kernel +from semantic_kernel.connectors.ai import FunctionChoiceBehavior +from semantic_kernel.connectors.ai.open_ai import ( + ListenEvents, + OpenAIRealtime, + OpenAIRealtimeExecutionSettings, + TurnDetection, +) +from semantic_kernel.contents import ChatHistory +from semantic_kernel.functions import kernel_function + +logging.basicConfig(level=logging.WARNING) +utils_log = logging.getLogger("samples.concepts.realtime.utils") +utils_log.setLevel(logging.INFO) +aiortc_log = logging.getLogger("aiortc") +aiortc_log.setLevel(logging.WARNING) +aioice_log = logging.getLogger("aioice") +aioice_log.setLevel(logging.WARNING) +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# This simple sample demonstrates how to use the OpenAI Realtime API to create +# a chat bot that can listen and respond directly through audio. +# It requires installing: +# - semantic-kernel[openai_realtime] +# - pyaudio +# - sounddevice +# - pydub +# - aiortc +# e.g. pip install pyaudio sounddevice pydub + +# The characterics of your speaker and microphone are a big factor in a smooth conversation +# so you may need to try out different devices for each. +# you can also play around with the turn_detection settings to get the best results. +# It has device id's set in the AudioRecorderStream and AudioPlayerAsync classes, +# so you may need to adjust these for your system. +# you can check the available devices by uncommenting line below the function + + +check_audio_devices() + + +@kernel_function +def get_weather(location: str) -> str: + """Get the weather for a location.""" + weather_conditions = ("sunny", "hot", "cloudy", "raining", "freezing", "snowing") + weather = weather_conditions[randint(0, len(weather_conditions) - 1)] # nosec + logger.info(f"@ Getting weather for {location}: {weather}") + return f"The weather in {location} is {weather}." + + +@kernel_function +def get_date_time() -> str: + """Get the current date and time.""" + logger.info("@ Getting current datetime") + return f"The current date and time is {datetime.now().isoformat()}." + + +@kernel_function +def goodbye(): + """When the user is done, say goodbye and then call this function.""" + logger.info("@ Goodbye has been called!") + raise KeyboardInterrupt + + +async def main() -> None: + print_transcript = True + # create the Kernel and add a simple function for function calling. + kernel = Kernel() + kernel.add_functions(plugin_name="helpers", functions=[goodbye, get_weather, get_date_time]) + + # create the audio player and audio track + # both take a device_id parameter, which is the index of the device to use, if None the default device is used + audio_player = AudioPlayerWebRTC() + audio_track = AudioRecorderWebRTC() + # create the realtime client and optionally add the audio output function, this is optional + # you can define the protocol to use, either "websocket" or "webrtc" + # they will behave the same way, even though the underlying protocol is quite different + realtime_client = OpenAIRealtime( + protocol="webrtc", + audio_output_callback=audio_player.client_callback, + audio_track=audio_track, + ) + + # Create the settings for the session + # The realtime api, does not use a system message, but takes instructions as a parameter for a session + instructions = """ + You are a chat bot. Your name is Mosscap and + you have one goal: figure out what people need. + Your full name, should you need to know it, is + Splendid Speckled Mosscap. You communicate + effectively, but you tend to answer with long + flowery prose. + """ + # the key thing to decide on is to enable the server_vad turn detection + # if turn is turned off (by setting turn_detection=None), you will have to send + # the "input_audio_buffer.commit" and "response.create" event to the realtime api + # to signal the end of the user's turn and start the response. + # manual VAD is not part of this sample + settings = OpenAIRealtimeExecutionSettings( + instructions=instructions, + voice="alloy", + turn_detection=TurnDetection(type="server_vad", create_response=True, silence_duration_ms=800, threshold=0.8), + function_choice_behavior=FunctionChoiceBehavior.Auto(), + ) + # and we can add a chat history to conversation after starting it + chat_history = ChatHistory() + chat_history.add_user_message("Hi there, who are you?") + chat_history.add_assistant_message("I am Mosscap, a chat bot. I'm trying to figure out what people need.") + + # the context manager calls the create_session method on the client and start listening to the audio stream + async with realtime_client, audio_player: + await realtime_client.update_session( + settings=settings, chat_history=chat_history, kernel=kernel, create_response=True + ) + print("Mosscap (transcript): ", end="") + async for event in realtime_client.receive(): + match event.event_type: + case "text": + if print_transcript: + print(event.text.text, end="") + case "service": + # OpenAI Specific events + match event.service_type: + case ListenEvents.RESPONSE_CREATED: + if print_transcript: + print("") + case ListenEvents.ERROR: + logger.error(event.event) + + +if __name__ == "__main__": + print( + "Instruction: start speaking, when you stop the API should detect you finished and start responding. " + "Press ctrl + c to stop the program." + ) + asyncio.run(main()) diff --git a/python/samples/concepts/realtime/utils.py b/python/samples/concepts/realtime/utils.py new file mode 100644 index 000000000000..d7f39369a0d4 --- /dev/null +++ b/python/samples/concepts/realtime/utils.py @@ -0,0 +1,470 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import base64 +import logging +import threading +from typing import Any, ClassVar, Final, cast + +import numpy as np +import numpy.typing as npt +import sounddevice as sd +from aiortc.mediastreams import MediaStreamError, MediaStreamTrack +from av.audio.frame import AudioFrame +from av.frame import Frame +from pydantic import PrivateAttr +from sounddevice import InputStream, OutputStream + +from semantic_kernel.connectors.ai.realtime_client_base import RealtimeClientBase +from semantic_kernel.contents.audio_content import AudioContent +from semantic_kernel.contents.events.realtime_event import AudioEvent +from semantic_kernel.kernel_pydantic import KernelBaseModel + +logger = logging.getLogger(__name__) + +SAMPLE_RATE: Final[int] = 24000 +RECORDER_CHANNELS: Final[int] = 1 +PLAYER_CHANNELS: Final[int] = 1 +FRAME_DURATION: Final[int] = 100 +SAMPLE_RATE_WEBRTC: Final[int] = 48000 +RECORDER_CHANNELS_WEBRTC: Final[int] = 1 +PLAYER_CHANNELS_WEBRTC: Final[int] = 2 +FRAME_DURATION_WEBRTC: Final[int] = 20 +DTYPE: Final[npt.DTypeLike] = np.int16 + + +def check_audio_devices(): + logger.info(sd.query_devices()) + + +# region: Recorders + + +class AudioRecorderWebRTC(KernelBaseModel, MediaStreamTrack): + """A simple class that implements the WebRTC MediaStreamTrack for audio from sounddevice.""" + + kind: ClassVar[str] = "audio" + device: str | int | None = None + sample_rate: int + channels: int + frame_duration: int + dtype: npt.DTypeLike = DTYPE + frame_size: int = 0 + _queue: asyncio.Queue[Frame] = PrivateAttr(default_factory=asyncio.Queue) + _is_recording: bool = False + _stream: InputStream | None = None + _recording_task: asyncio.Task | None = None + _loop: asyncio.AbstractEventLoop | None = None + _pts: int = 0 + + def __init__( + self, + *, + device: str | int | None = None, + sample_rate: int = SAMPLE_RATE_WEBRTC, + channels: int = RECORDER_CHANNELS_WEBRTC, + frame_duration: int = FRAME_DURATION_WEBRTC, + dtype: npt.DTypeLike = DTYPE, + ): + """A simple class that implements the WebRTC MediaStreamTrack for audio from sounddevice. + + Make sure the device is set to the correct device for your system. + + Args: + device: The device id to use for recording audio. + sample_rate: The sample rate for the audio. + channels: The number of channels for the audio. + frame_duration: The duration of each audio frame in milliseconds. + dtype: The data type for the audio. + """ + super().__init__(**{ + "device": device, + "sample_rate": sample_rate, + "channels": channels, + "frame_duration": frame_duration, + "dtype": dtype, + "frame_size": int(sample_rate * frame_duration / 1000), + }) + MediaStreamTrack.__init__(self) + + async def recv(self) -> Frame: + """Receive the next frame of audio data.""" + if not self._recording_task: + self._recording_task = asyncio.create_task(self.start_recording()) + + try: + frame = await self._queue.get() + self._queue.task_done() + return frame + except Exception as e: + logger.error(f"Error receiving audio frame: {e!s}") + raise MediaStreamError("Failed to receive audio frame") + + def _sounddevice_callback(self, indata: np.ndarray, frames: int, time: Any, status: Any) -> None: + if status: + logger.warning(f"Audio input status: {status}") + if self._loop and self._loop.is_running(): + asyncio.run_coroutine_threadsafe(self._queue.put(self._create_frame(indata)), self._loop) + + def _create_frame(self, indata: np.ndarray) -> Frame: + audio_data = indata.copy() + if audio_data.dtype != self.dtype: + audio_data = ( + (audio_data * 32767).astype(self.dtype) if self.dtype == np.int16 else audio_data.astype(self.dtype) + ) + frame = AudioFrame( + format="s16", + layout="mono", + samples=len(audio_data), + ) + frame.rate = self.sample_rate + frame.pts = self._pts + frame.planes[0].update(audio_data.tobytes()) + self._pts += len(audio_data) + return frame + + async def start_recording(self): + """Start recording audio from the input device.""" + if self._is_recording: + return + + self._is_recording = True + self._loop = asyncio.get_running_loop() + self._pts = 0 # Reset pts when starting recording + + try: + self._stream = InputStream( + device=self.device, + channels=self.channels, + samplerate=self.sample_rate, + dtype=self.dtype, + blocksize=self.frame_size, + callback=self._sounddevice_callback, + ) + self._stream.start() + + while self._is_recording: + await asyncio.sleep(0.1) + except asyncio.CancelledError | KeyboardInterrupt: + logger.debug("Recording task was stopped.") + except Exception as e: + logger.error(f"Error in audio recording: {e!s}") + raise + finally: + self._is_recording = False + + +class AudioRecorderWebsocket(KernelBaseModel): + """A simple class that implements a sounddevice for use with websockets.""" + + realtime_client: RealtimeClientBase + device: str | int | None = None + sample_rate: int + channels: int + frame_duration: int + dtype: npt.DTypeLike = DTYPE + frame_size: int = 0 + _stream: InputStream | None = None + _pts: int = 0 + _stream_task: asyncio.Task | None = None + + def __init__( + self, + *, + realtime_client: RealtimeClientBase, + device: str | int | None = None, + sample_rate: int = SAMPLE_RATE, + channels: int = RECORDER_CHANNELS, + frame_duration: int = FRAME_DURATION, + dtype: npt.DTypeLike = DTYPE, + ): + """A simple class that implements the WebRTC MediaStreamTrack for audio from sounddevice. + + Make sure the device is set to the correct device for your system. + + Args: + realtime_client: The RealtimeClientBase to use for streaming audio. + device: The device id to use for recording audio. + sample_rate: The sample rate for the audio. + channels: The number of channels for the audio. + frame_duration: The duration of each audio frame in milliseconds. + dtype: The data type for the audio. + **kwargs: Additional keyword arguments. + """ + super().__init__(**{ + "realtime_client": realtime_client, + "device": device, + "sample_rate": sample_rate, + "channels": channels, + "frame_duration": frame_duration, + "dtype": dtype, + "frame_size": int(sample_rate * frame_duration / 1000), + }) + + async def __aenter__(self): + """Stream audio data to a RealtimeClientBase.""" + if not self._stream_task: + self._stream_task = asyncio.create_task(self._start_stream()) + return self + + async def _start_stream(self): + self._pts = 0 # Reset pts when starting recording + self._stream = InputStream( + device=self.device, + channels=self.channels, + samplerate=self.sample_rate, + dtype=self.dtype, + blocksize=self.frame_size, + ) + self._stream.start() + try: + while True: + if self._stream.read_available < self.frame_size: + await asyncio.sleep(0) + continue + data, _ = self._stream.read(self.frame_size) + + await self.realtime_client.send( + AudioEvent(audio=AudioContent(data=base64.b64encode(cast(Any, data)).decode("utf-8"))) + ) + + await asyncio.sleep(0) + except asyncio.CancelledError: + pass + + async def __aexit__(self, exc_type, exc, tb): + """Stop recording audio.""" + if self._stream_task: + self._stream_task.cancel() + await self._stream_task + if self._stream: + self._stream.stop() + self._stream.close() + + +# region: Players + + +class AudioPlayerWebRTC(KernelBaseModel): + """Simple class that plays audio using sounddevice. + + Make sure the device_id is set to the correct device for your system. + + The sample rate, channels and frame duration + should be set to match the audio you + are receiving. + + Args: + device: The device id to use for playing audio. + sample_rate: The sample rate for the audio. + channels: The number of channels for the audio. + dtype: The data type for the audio. + frame_duration: The duration of each audio frame in milliseconds + + """ + + device: int | None = None + sample_rate: int = SAMPLE_RATE_WEBRTC + channels: int = PLAYER_CHANNELS_WEBRTC + dtype: npt.DTypeLike = DTYPE + frame_duration: int = FRAME_DURATION_WEBRTC + _queue: asyncio.Queue[np.ndarray] | None = PrivateAttr(default=None) + _stream: OutputStream | None = PrivateAttr(default=None) + + async def __aenter__(self): + """Start the audio stream when entering a context.""" + self.start() + return self + + async def __aexit__(self, exc_type, exc, tb): + """Stop the audio stream when exiting a context.""" + self.stop() + + def start(self): + """Start the audio stream.""" + self._queue = asyncio.Queue() + self._stream = OutputStream( + callback=self._sounddevice_callback, + samplerate=self.sample_rate, + channels=self.channels, + dtype=self.dtype, + blocksize=int(self.sample_rate * self.frame_duration / 1000), + device=self.device, + ) + if self._stream and self._queue: + self._stream.start() + + def stop(self): + """Stop the audio stream.""" + if self._stream: + self._stream.stop() + self._stream = None + self._queue = None + + def _sounddevice_callback(self, outdata, frames, time, status): + """This callback is called by sounddevice when it needs more audio data to play.""" + if status: + logger.debug(f"Audio output status: {status}") + if self._queue: + if self._queue.empty(): + return + data = self._queue.get_nowait() + outdata[:] = data.reshape(outdata.shape) + self._queue.task_done() + else: + logger.error( + "Audio queue not initialized, make sure to call start before " + "using the player, or use the context manager." + ) + + async def client_callback(self, content: np.ndarray): + """This function can be passed to the audio_output_callback field of the RealtimeClientBase.""" + if self._queue: + await self._queue.put(content) + else: + logger.error( + "Audio queue not initialized, make sure to call start before " + "using the player, or use the context manager." + ) + + async def add_audio(self, audio_content: AudioContent) -> None: + """This function is used to add audio to the queue for playing. + + It first checks if there is a AudioFrame in the inner_content of the AudioContent. + If not, it checks if the data is a numpy array, bytes, or a string and converts it to a numpy array. + """ + if not self._queue: + logger.error( + "Audio queue not initialized, make sure to call start before " + "using the player, or use the context manager." + ) + return + if audio_content.inner_content and isinstance(audio_content.inner_content, AudioFrame): + await self._queue.put(audio_content.inner_content.to_ndarray()) + return + if isinstance(audio_content.data, np.ndarray): + await self._queue.put(audio_content.data) + return + if isinstance(audio_content.data, bytes): + await self._queue.put(np.frombuffer(audio_content.data, dtype=self.dtype)) + return + if isinstance(audio_content.data, str): + await self._queue.put(np.frombuffer(audio_content.data.encode(), dtype=self.dtype)) + return + logger.error(f"Unknown audio content: {audio_content}") + + +class AudioPlayerWebsocket(KernelBaseModel): + """Simple class that plays audio using sounddevice. + + Make sure the device_id is set to the correct device for your system. + + The sample rate, channels and frame duration + should be set to match the audio you + are receiving. + + Args: + device: The device id to use for playing audio. + sample_rate: The sample rate for the audio. + channels: The number of channels for the audio. + dtype: The data type for the audio. + frame_duration: The duration of each audio frame in milliseconds + + """ + + device: int | None = None + sample_rate: int = SAMPLE_RATE + channels: int = PLAYER_CHANNELS + dtype: npt.DTypeLike = DTYPE + frame_duration: int = FRAME_DURATION + _lock: Any = PrivateAttr(default_factory=threading.Lock) + _queue: list[np.ndarray] = PrivateAttr(default_factory=list) + _stream: OutputStream | None = PrivateAttr(default=None) + _frame_count: int = 0 + + async def __aenter__(self): + """Start the audio stream when entering a context.""" + self.start() + return self + + async def __aexit__(self, exc_type, exc, tb): + """Stop the audio stream when exiting a context.""" + self.stop() + + def start(self): + """Start the audio stream.""" + with self._lock: + self._queue = [] + self._stream = OutputStream( + callback=self._sounddevice_callback, + samplerate=self.sample_rate, + channels=self.channels, + dtype=self.dtype, + blocksize=int(self.sample_rate * self.frame_duration / 1000), + device=self.device, + ) + if self._stream: + self._stream.start() + + def stop(self): + """Stop the audio stream.""" + if self._stream: + self._stream.stop() + self._stream = None + with self._lock: + self._queue = [] + + def _sounddevice_callback(self, outdata, frames, time, status): + """This callback is called by sounddevice when it needs more audio data to play.""" + with self._lock: + if status: + logger.debug(f"Audio output status: {status}") + data = np.empty(0, dtype=np.int16) + + # get next item from queue if there is still space in the buffer + while len(data) < frames and len(self._queue) > 0: + item = self._queue.pop(0) + frames_needed = frames - len(data) + data = np.concatenate((data, item[:frames_needed])) + if len(item) > frames_needed: + self._queue.insert(0, item[frames_needed:]) + + self._frame_count += len(data) + + # fill the rest of the frames with zeros if there is no more data + if len(data) < frames: + data = np.concatenate((data, np.zeros(frames - len(data), dtype=np.int16))) + + outdata[:] = data.reshape(-1, 1) + + def reset_frame_count(self): + self._frame_count = 0 + + def get_frame_count(self): + return self._frame_count + + async def client_callback(self, content: np.ndarray): + """This function can be passed to the audio_output_callback field of the RealtimeClientBase.""" + with self._lock: + self._queue.append(content) + + async def add_audio(self, audio_content: AudioContent) -> None: + """This function is used to add audio to the queue for playing. + + It first checks if there is a AudioFrame in the inner_content of the AudioContent. + If not, it checks if the data is a numpy array, bytes, or a string and converts it to a numpy array. + """ + with self._lock: + if audio_content.inner_content and isinstance(audio_content.inner_content, AudioFrame): + self._queue.append(audio_content.inner_content.to_ndarray()) + return + if isinstance(audio_content.data, np.ndarray): + self._queue.append(audio_content.data) + return + if isinstance(audio_content.data, bytes): + self._queue.append(np.frombuffer(audio_content.data, dtype=self.dtype)) + return + if isinstance(audio_content.data, str): + self._queue.append(np.frombuffer(audio_content.data.encode(), dtype=self.dtype)) + return + logger.error(f"Unknown audio content: {audio_content}") diff --git a/python/semantic_kernel/connectors/ai/chat_completion_client_base.py b/python/semantic_kernel/connectors/ai/chat_completion_client_base.py index 5c527e994564..eaf372b1f858 100644 --- a/python/semantic_kernel/connectors/ai/chat_completion_client_base.py +++ b/python/semantic_kernel/connectors/ai/chat_completion_client_base.py @@ -263,7 +263,9 @@ async def get_streaming_chat_message_contents( for msg in messages: if msg is not None: all_messages.append(msg) - if any(isinstance(item, FunctionCallContent) for item in msg.items): + if not function_call_returned and any( + isinstance(item, FunctionCallContent) for item in msg.items + ): function_call_returned = True yield messages @@ -429,7 +431,10 @@ def _get_ai_model_id(self, settings: "PromptExecutionSettings") -> str: return getattr(settings, "ai_model_id", self.ai_model_id) or self.ai_model_id def _yield_function_result_messages(self, function_result_messages: list) -> bool: - """Determine if the function result messages should be yielded.""" + """Determine if the function result messages should be yielded. + + If there are messages and if the first message has items, then yield the messages. + """ return len(function_result_messages) > 0 and len(function_result_messages[0].items) > 0 # endregion diff --git a/python/semantic_kernel/connectors/ai/function_calling_utils.py b/python/semantic_kernel/connectors/ai/function_calling_utils.py index 7a5c2950c4e0..1cad6d26ce6c 100644 --- a/python/semantic_kernel/connectors/ai/function_calling_utils.py +++ b/python/semantic_kernel/connectors/ai/function_calling_utils.py @@ -1,10 +1,13 @@ # Copyright (c) Microsoft. All rights reserved. from collections import OrderedDict +from collections.abc import Callable +from copy import deepcopy from typing import TYPE_CHECKING, Any from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError +from semantic_kernel.utils.experimental_decorator import experimental_function if TYPE_CHECKING: from semantic_kernel.connectors.ai.function_choice_behavior import ( @@ -15,6 +18,7 @@ from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata + from semantic_kernel.kernel import Kernel def update_settings_from_function_call_configuration( @@ -134,3 +138,49 @@ def merge_streaming_function_results( function_invoke_attempt=function_invoke_attempt, ) ] + + +@experimental_function +def prepare_settings_for_function_calling( + settings: "PromptExecutionSettings", + settings_class: type["PromptExecutionSettings"], + update_settings_callback: Callable[..., None], + kernel: "Kernel", +) -> "PromptExecutionSettings": + """Prepare settings for the service. + + Args: + settings: Prompt execution settings. + settings_class: The settings class. + update_settings_callback: The callback to update the settings. + kernel: Kernel instance. + + Returns: + PromptExecutionSettings of type settings_class. + """ + settings = deepcopy(settings) + if not isinstance(settings, settings_class): + settings = settings_class.from_prompt_execution_settings(settings) + + # For backwards compatibility we need to convert the `FunctionCallBehavior` to `FunctionChoiceBehavior` + # if this method is called with a `FunctionCallBehavior` object as part of the settings + + from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior + from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior + + if hasattr(settings, "function_call_behavior") and isinstance( + settings.function_call_behavior, FunctionCallBehavior + ): + settings.function_choice_behavior = FunctionChoiceBehavior.from_function_call_behavior( + settings.function_call_behavior + ) + + if settings.function_choice_behavior: + # Configure the function choice behavior into the settings object + # that will become part of the request to the AI service + settings.function_choice_behavior.configure( + kernel=kernel, + update_settings_callback=update_settings_callback, + settings=settings, + ) + return settings diff --git a/python/semantic_kernel/connectors/ai/open_ai/__init__.py b/python/semantic_kernel/connectors/ai/open_ai/__init__.py index a3103ae86446..4241ec1e49f3 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/__init__.py +++ b/python/semantic_kernel/connectors/ai/open_ai/__init__.py @@ -22,6 +22,10 @@ OpenAIPromptExecutionSettings, OpenAITextPromptExecutionSettings, ) +from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_realtime_execution_settings import ( + OpenAIRealtimeExecutionSettings, + TurnDetection, +) from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_text_to_audio_execution_settings import ( OpenAITextToAudioExecutionSettings, ) @@ -36,10 +40,12 @@ from semantic_kernel.connectors.ai.open_ai.services.azure_text_to_image import AzureTextToImage from semantic_kernel.connectors.ai.open_ai.services.open_ai_audio_to_text import OpenAIAudioToText from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion +from semantic_kernel.connectors.ai.open_ai.services.open_ai_realtime import OpenAIRealtime from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion import OpenAITextCompletion from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding import OpenAITextEmbedding from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_to_audio import OpenAITextToAudio from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_to_image import OpenAITextToImage +from semantic_kernel.connectors.ai.open_ai.services.realtime.const import ListenEvents, SendEvents from semantic_kernel.connectors.ai.open_ai.settings.azure_open_ai_settings import AzureOpenAISettings from semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings import OpenAISettings @@ -63,12 +69,15 @@ "DataSourceFieldsMapping", "DataSourceFieldsMapping", "ExtraBody", + "ListenEvents", "OpenAIAudioToText", "OpenAIAudioToTextExecutionSettings", "OpenAIChatCompletion", "OpenAIChatPromptExecutionSettings", "OpenAIEmbeddingPromptExecutionSettings", "OpenAIPromptExecutionSettings", + "OpenAIRealtime", + "OpenAIRealtimeExecutionSettings", "OpenAISettings", "OpenAITextCompletion", "OpenAITextEmbedding", @@ -77,4 +86,6 @@ "OpenAITextToAudioExecutionSettings", "OpenAITextToImage", "OpenAITextToImageExecutionSettings", + "SendEvents", + "TurnDetection", ] diff --git a/python/semantic_kernel/connectors/ai/open_ai/prompt_execution_settings/open_ai_realtime_execution_settings.py b/python/semantic_kernel/connectors/ai/open_ai/prompt_execution_settings/open_ai_realtime_execution_settings.py new file mode 100644 index 000000000000..a26237b78b84 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/open_ai/prompt_execution_settings/open_ai_realtime_execution_settings.py @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft. All rights reserved. + +from collections.abc import Sequence +from typing import Annotated, Any, Literal + +from pydantic import Field + +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.kernel_pydantic import KernelBaseModel + + +class InputAudioTranscription(KernelBaseModel): + """Input audio transcription settings.""" + + model: Literal["whisper-1"] | None = None + + +class TurnDetection(KernelBaseModel): + """Turn detection settings.""" + + type: Literal["server_vad"] | None = None + threshold: Annotated[float | None, Field(ge=0, le=1)] = None + prefix_padding_ms: Annotated[int | None, Field(ge=0)] = None + silence_duration_ms: Annotated[int | None, Field(ge=0)] = None + create_response: bool | None = None + + +class OpenAIRealtimeExecutionSettings(PromptExecutionSettings): + """Request settings for OpenAI realtime services.""" + + modalities: Sequence[Literal["audio", "text"]] | None = None + ai_model_id: Annotated[str | None, Field(None, serialization_alias="model")] = None + instructions: str | None = None + voice: str | None = None + input_audio_format: Literal["pcm16", "g711_ulaw", "g711_alaw"] | None = None + output_audio_format: Literal["pcm16", "g711_ulaw", "g711_alaw"] | None = None + input_audio_transcription: InputAudioTranscription | None = None + turn_detection: TurnDetection | None = None + tools: Annotated[ + list[dict[str, Any]] | None, + Field( + description="Do not set this manually. It is set by the service based " + "on the function choice configuration.", + ), + ] = None + tool_choice: Annotated[ + str | None, + Field( + description="Do not set this manually. It is set by the service based " + "on the function choice configuration.", + ), + ] = None + temperature: Annotated[float | None, Field(ge=0.0, le=2.0)] = None + max_response_output_tokens: Annotated[int | Literal["inf"] | None, Field(gt=0)] = None diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_config_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_config_base.py index d3d72795665b..7883be04f4ff 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_config_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_config_base.py @@ -3,6 +3,7 @@ import logging from collections.abc import Mapping from copy import copy +from typing import Any from openai import AsyncOpenAI from pydantic import ConfigDict, Field, validate_call @@ -30,6 +31,7 @@ def __init__( default_headers: Mapping[str, str] | None = None, client: AsyncOpenAI | None = None, instruction_role: str | None = None, + **kwargs: Any, ) -> None: """Initialize a client for OpenAI services. @@ -51,6 +53,7 @@ def __init__( client (AsyncOpenAI): An existing OpenAI client, optional. instruction_role (str): The role to use for 'instruction' messages, for example, summarization prompts could use `developer` or `system`. (Optional) + kwargs: Additional keyword arguments. """ # Merge APP_INFO into the headers if it exists @@ -76,7 +79,7 @@ def __init__( args["service_id"] = service_id if instruction_role: args["instruction_role"] = instruction_role - super().__init__(**args) + super().__init__(**args, **kwargs) def to_dict(self) -> dict[str, str]: """Create a dict of the service settings.""" diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_model_types.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_model_types.py index 7a1f43da234e..ea2e05deead7 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_model_types.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_model_types.py @@ -12,3 +12,4 @@ class OpenAIModelTypes(Enum): TEXT_TO_IMAGE = "text-to-image" AUDIO_TO_TEXT = "audio-to-text" TEXT_TO_AUDIO = "text-to-audio" + REALTIME = "realtime" diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_realtime.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_realtime.py new file mode 100644 index 000000000000..7d6f60eafbd2 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_realtime.py @@ -0,0 +1,150 @@ +# Copyright (c) Microsoft. All rights reserved. + +from collections.abc import Callable, Coroutine, Mapping +from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar + +from numpy import ndarray +from openai import AsyncOpenAI +from pydantic import ValidationError + +from semantic_kernel.connectors.ai.open_ai.services.open_ai_config_base import OpenAIConfigBase +from semantic_kernel.connectors.ai.open_ai.services.open_ai_model_types import OpenAIModelTypes +from semantic_kernel.connectors.ai.open_ai.services.realtime.open_ai_realtime_base import OpenAIRealtimeBase +from semantic_kernel.connectors.ai.open_ai.services.realtime.open_ai_realtime_webrtc import OpenAIRealtimeWebRTCBase +from semantic_kernel.connectors.ai.open_ai.services.realtime.open_ai_realtime_websocket import ( + OpenAIRealtimeWebsocketBase, +) +from semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings import OpenAISettings +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError + +if TYPE_CHECKING: + from aiortc.mediastreams import MediaStreamTrack + + +_T = TypeVar("_T", bound="OpenAIRealtime") + + +__all__ = ["OpenAIRealtime"] + + +class OpenAIRealtime(OpenAIRealtimeBase): + """OpenAI Realtime service.""" + + def __new__(cls: type["_T"], protocol: str, *args: Any, **kwargs: Any) -> "_T": + """Pick the right subclass, based on protocol.""" + subclass_map = {subcl.protocol: subcl for subcl in cls.__subclasses__()} + subclass = subclass_map[protocol] + return super(OpenAIRealtime, subclass).__new__(subclass) + + def __init__( + self, + protocol: Literal["websocket", "webrtc"], + *, + audio_output_callback: Callable[[ndarray], Coroutine[Any, Any, None]] | None = None, + audio_track: "MediaStreamTrack | None" = None, + ai_model_id: str | None = None, + api_key: str | None = None, + org_id: str | None = None, + service_id: str | None = None, + default_headers: Mapping[str, str] | None = None, + client: AsyncOpenAI | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize an OpenAIRealtime service. + + Args: + protocol: The protocol to use, must be either "websocket" or "webrtc". + audio_output_callback: The audio output callback, optional. + This should be a coroutine, that takes a ndarray with audio as input. + The goal of this function is to allow you to play the audio with the + least amount of latency possible. + It is called first in both websockets and webrtc. + Even when passed, the audio content will still be + added to the receiving queue. + audio_track: The audio track to use for the service, only used by WebRTC. + A default is supplied if not provided. + It can be any class that implements the AudioStreamTrack interface. + ai_model_id (str | None): OpenAI model name, see + https://platform.openai.com/docs/models + service_id (str | None): Service ID tied to the execution settings. + api_key (str | None): The optional API key to use. If provided will override, + the env vars or .env file value. + org_id (str | None): The optional org ID to use. If provided will override, + the env vars or .env file value. + default_headers: The default headers mapping of string keys to + string values for HTTP requests. (Optional) + client (Optional[AsyncOpenAI]): An existing client to use. (Optional) + env_file_path (str | None): Use the environment settings file as a fallback to + environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) + kwargs: Additional arguments. + """ + try: + openai_settings = OpenAISettings.create( + api_key=api_key, + org_id=org_id, + realtime_model_id=ai_model_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create OpenAI settings.", ex) from ex + if not openai_settings.realtime_model_id: + raise ServiceInitializationError("The OpenAI text model ID is required.") + if audio_track: + kwargs["audio_track"] = audio_track + super().__init__( + protocol=protocol, + audio_output_callback=audio_output_callback, + ai_model_id=openai_settings.realtime_model_id, + service_id=service_id, + api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None, + org_id=openai_settings.org_id, + ai_model_type=OpenAIModelTypes.REALTIME, + default_headers=default_headers, + client=client, + **kwargs, + ) + + +class OpenAIRealtimeWebRTC(OpenAIRealtime, OpenAIRealtimeWebRTCBase, OpenAIConfigBase): + """OpenAI Realtime service using WebRTC protocol. + + This should not be used directly, use OpenAIRealtime instead. + Set protocol="webrtc" to use this class. + """ + + protocol: ClassVar[Literal["webrtc"]] = "webrtc" + + def __init__( + self, + *args: Any, + **kwargs: Any, + ) -> None: + """Initialize an OpenAIRealtime service using WebRTC protocol.""" + super().__init__( + *args, + **kwargs, + ) + + +class OpenAIRealtimeWebSocket(OpenAIRealtime, OpenAIRealtimeWebsocketBase, OpenAIConfigBase): + """OpenAI Realtime service using WebSocket protocol. + + This should not be used directly, use OpenAIRealtime instead. + Set protocol="websocket" to use this class. + """ + + protocol: ClassVar[Literal["websocket"]] = "websocket" + + def __init__( + self, + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__( + *args, + **kwargs, + ) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/realtime/__init__.py b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/realtime/const.py b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/const.py new file mode 100644 index 000000000000..533e00d24d53 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/const.py @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft. All rights reserved. + +from enum import Enum + +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class SendEvents(str, Enum): + """Events that can be sent.""" + + SESSION_UPDATE = "session.update" + INPUT_AUDIO_BUFFER_APPEND = "input_audio_buffer.append" + INPUT_AUDIO_BUFFER_COMMIT = "input_audio_buffer.commit" + INPUT_AUDIO_BUFFER_CLEAR = "input_audio_buffer.clear" + CONVERSATION_ITEM_CREATE = "conversation.item.create" + CONVERSATION_ITEM_TRUNCATE = "conversation.item.truncate" + CONVERSATION_ITEM_DELETE = "conversation.item.delete" + RESPONSE_CREATE = "response.create" + RESPONSE_CANCEL = "response.cancel" + + +@experimental_class +class ListenEvents(str, Enum): + """Events that can be listened to.""" + + ERROR = "error" + SESSION_CREATED = "session.created" + SESSION_UPDATED = "session.updated" + CONVERSATION_CREATED = "conversation.created" + INPUT_AUDIO_BUFFER_COMMITTED = "input_audio_buffer.committed" + INPUT_AUDIO_BUFFER_CLEARED = "input_audio_buffer.cleared" + INPUT_AUDIO_BUFFER_SPEECH_STARTED = "input_audio_buffer.speech_started" + INPUT_AUDIO_BUFFER_SPEECH_STOPPED = "input_audio_buffer.speech_stopped" + CONVERSATION_ITEM_CREATED = "conversation.item.created" + CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_COMPLETED = "conversation.item.input_audio_transcription.completed" + CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_FAILED = "conversation.item.input_audio_transcription.failed" + CONVERSATION_ITEM_TRUNCATED = "conversation.item.truncated" + CONVERSATION_ITEM_DELETED = "conversation.item.deleted" + RESPONSE_CREATED = "response.created" + RESPONSE_DONE = "response.done" # contains usage info -> log + RESPONSE_OUTPUT_ITEM_ADDED = "response.output_item.added" + RESPONSE_OUTPUT_ITEM_DONE = "response.output_item.done" + RESPONSE_CONTENT_PART_ADDED = "response.content_part.added" + RESPONSE_CONTENT_PART_DONE = "response.content_part.done" + RESPONSE_TEXT_DELTA = "response.text.delta" + RESPONSE_TEXT_DONE = "response.text.done" + RESPONSE_AUDIO_TRANSCRIPT_DELTA = "response.audio_transcript.delta" + RESPONSE_AUDIO_TRANSCRIPT_DONE = "response.audio_transcript.done" + RESPONSE_AUDIO_DELTA = "response.audio.delta" + RESPONSE_AUDIO_DONE = "response.audio.done" + RESPONSE_FUNCTION_CALL_ARGUMENTS_DELTA = "response.function_call_arguments.delta" + RESPONSE_FUNCTION_CALL_ARGUMENTS_DONE = "response.function_call_arguments.done" + RATE_LIMITS_UPDATED = "rate_limits.updated" diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/realtime/open_ai_realtime_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/open_ai_realtime_base.py new file mode 100644 index 000000000000..2789bf0d16e2 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/open_ai_realtime_base.py @@ -0,0 +1,420 @@ +# Copyright (c) Microsoft. All rights reserved. + +import json +import logging +import sys +from collections.abc import AsyncGenerator, Callable +from typing import TYPE_CHECKING, Any, ClassVar, Literal + +from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_realtime_execution_settings import ( + OpenAIRealtimeExecutionSettings, +) + +if sys.version_info >= (3, 12): + from typing import override # pragma: no cover +else: + from typing_extensions import override # pragma: no cover + +from openai.types.beta.realtime import ( + RealtimeClientEvent, + RealtimeServerEvent, + ResponseFunctionCallArgumentsDoneEvent, +) +from pydantic import PrivateAttr + +from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration +from semantic_kernel.connectors.ai.function_calling_utils import ( + prepare_settings_for_function_calling, +) +from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType +from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIHandler +from semantic_kernel.connectors.ai.open_ai.services.realtime.const import ListenEvents, SendEvents +from semantic_kernel.connectors.ai.open_ai.services.realtime.utils import ( + _create_openai_realtime_client_event, + update_settings_from_function_call_configuration, +) +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.connectors.ai.realtime_client_base import RealtimeClientBase +from semantic_kernel.contents.chat_history import ChatHistory +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.events.realtime_event import ( + FunctionCallEvent, + FunctionResultEvent, + RealtimeEvent, + ServiceEvent, + TextEvent, +) +from semantic_kernel.contents.function_call_content import FunctionCallContent +from semantic_kernel.contents.function_result_content import FunctionResultContent +from semantic_kernel.contents.streaming_text_content import StreamingTextContent +from semantic_kernel.contents.text_content import TextContent +from semantic_kernel.kernel import Kernel +from semantic_kernel.utils.experimental_decorator import experimental_class + +if TYPE_CHECKING: + from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings + from semantic_kernel.contents.chat_history import ChatHistory + + +logger: logging.Logger = logging.getLogger(__name__) + + +@experimental_class +class OpenAIRealtimeBase(OpenAIHandler, RealtimeClientBase): + """OpenAI Realtime service.""" + + SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True + protocol: ClassVar[Literal["websocket", "webrtc"]] = "websocket" + kernel: Kernel | None = None + + _current_settings: PromptExecutionSettings | None = PrivateAttr(default=None) + _call_id_to_function_map: dict[str, str] = PrivateAttr(default_factory=dict) + + async def _parse_event(self, event: RealtimeServerEvent) -> AsyncGenerator[RealtimeEvent, None]: + """Handle all events but audio delta. + + Audio delta has to be handled by the implementation of the protocol as some + protocols have different ways of handling audio. + """ + match event.type: + case ListenEvents.RESPONSE_AUDIO_TRANSCRIPT_DELTA.value: + yield TextEvent( + service_type=event.type, + text=StreamingTextContent( + inner_content=event, + text=event.delta, + choice_index=0, + ), + ) + case ListenEvents.RESPONSE_OUTPUT_ITEM_ADDED.value: + if event.item.type == "function_call" and event.item.call_id and event.item.name: + self._call_id_to_function_map[event.item.call_id] = event.item.name + case ListenEvents.RESPONSE_FUNCTION_CALL_ARGUMENTS_DELTA.value: + yield FunctionCallEvent( + service_type=event.type, + function_call=FunctionCallContent( + id=event.item_id, + name=self._call_id_to_function_map[event.call_id], + arguments=event.delta, + index=event.output_index, + metadata={"call_id": event.call_id}, + inner_content=event, + ), + ) + case ListenEvents.RESPONSE_FUNCTION_CALL_ARGUMENTS_DONE.value: + async for parsed_event in self._parse_function_call_arguments_done(event): + if parsed_event: + yield parsed_event + case ListenEvents.ERROR.value: + logger.error("Error received: %s", event.error) + case ListenEvents.SESSION_CREATED.value, ListenEvents.SESSION_UPDATED.value: + logger.info("Session created or updated, session: %s", event.session) + case _: + logger.debug(f"Received event: {event}") + # we put all event in the output buffer, but after the interpreted one. + # so when dealing with them, make sure to check the type of the event, since they + # might be of different types. + yield ServiceEvent(service_type=event.type, event=event) + + @override + async def update_session( + self, + settings: PromptExecutionSettings | None = None, + chat_history: ChatHistory | None = None, + create_response: bool = False, + **kwargs: Any, + ) -> None: + if "kernel" in kwargs: + self.kernel = kwargs["kernel"] + if settings: + self._current_settings = settings + if self._current_settings and self.kernel: + self._current_settings = prepare_settings_for_function_calling( + self._current_settings, + self.get_prompt_execution_settings_class(), + self._update_function_choice_settings_callback(), + kernel=self.kernel, # type: ignore + ) + await self.send( + ServiceEvent( + service_type=SendEvents.SESSION_UPDATE, + event={"settings": self._current_settings}, + ) + ) + if chat_history and len(chat_history) > 0: + for msg in chat_history.messages: + for item in msg.items: + match item: + case TextContent(): + await self.send(TextEvent(service_type=SendEvents.CONVERSATION_ITEM_CREATE, text=item)) + case FunctionCallContent(): + await self.send( + FunctionCallEvent(service_type=SendEvents.CONVERSATION_ITEM_CREATE, function_call=item) + ) + case FunctionResultContent(): + await self.send( + FunctionResultEvent( + service_type=SendEvents.CONVERSATION_ITEM_CREATE, function_result=item + ) + ) + case _: + logger.error("Unsupported item type: %s", item) + if create_response: + await self.send(ServiceEvent(service_type=SendEvents.RESPONSE_CREATE)) + + async def _parse_function_call_arguments_done( + self, + event: ResponseFunctionCallArgumentsDoneEvent, + ) -> AsyncGenerator[RealtimeEvent | None]: + """Handle response function call done.""" + if not self.kernel or ( + self._current_settings + and self._current_settings.function_choice_behavior + and not self._current_settings.function_choice_behavior.auto_invoke_kernel_functions + ): + yield None + return + plugin_name, function_name = self._call_id_to_function_map.pop(event.call_id, "-").split("-", 1) + if not plugin_name or not function_name: + logger.error("Function call needs to have a plugin name and function name") + yield None + return + item = FunctionCallContent( + id=event.item_id, + plugin_name=plugin_name, + function_name=function_name, + arguments=event.arguments, + index=event.output_index, + metadata={"call_id": event.call_id}, + ) + yield FunctionCallEvent(service_type=ListenEvents.RESPONSE_FUNCTION_CALL_ARGUMENTS_DONE, function_call=item) + chat_history = ChatHistory() + await self.kernel.invoke_function_call(item, chat_history) + created_output: FunctionResultContent = chat_history.messages[-1].items[0] # type: ignore + # This returns the output to the service + result = FunctionResultEvent( + service_type=SendEvents.CONVERSATION_ITEM_CREATE, + function_result=created_output, + ) + await self.send(result) + # The model doesn't start responding to the tool call automatically, so triggering it here. + await self.send(ServiceEvent(service_type=SendEvents.RESPONSE_CREATE)) + # This allows a user to have a full conversation in his code + yield result + + async def _send(self, event: RealtimeClientEvent) -> None: + """Send an event to the service.""" + raise NotImplementedError + + @override + async def send(self, event: RealtimeEvent, **kwargs: Any) -> None: + match event.event_type: + case "audio": + await self._send( + _create_openai_realtime_client_event( + event_type=SendEvents.INPUT_AUDIO_BUFFER_APPEND, audio=event.audio.to_base64_bytestring() + ) + ) + case "text": + await self._send( + _create_openai_realtime_client_event( + event_type=SendEvents.CONVERSATION_ITEM_CREATE, + **dict( + type="message", + content=[ + { + "type": "input_text", + "text": event.text.text, + } + ], + role="user", + ), + ) + ) + case "function_call": + await self._send( + _create_openai_realtime_client_event( + event_type=SendEvents.CONVERSATION_ITEM_CREATE, + **dict( + type="function_call", + name=event.function_call.name or event.function_call.function_name, + arguments="" + if not event.function_call.arguments + else event.function_call.arguments + if isinstance(event.function_call.arguments, str) + else json.dumps(event.function_call.arguments), + call_id=event.function_call.metadata.get("call_id"), + ), + ) + ) + case "function_result": + await self._send( + _create_openai_realtime_client_event( + event_type=SendEvents.CONVERSATION_ITEM_CREATE, + **dict( + type="function_call_output", + output=event.function_result.result, + call_id=event.function_result.metadata.get("call_id"), + ), + ) + ) + case "service": + data = event.event + match event.service_type: + case SendEvents.SESSION_UPDATE: + if not data: + logger.error("Event data is empty") + return + settings = data.get("settings", None) + if not settings: + logger.error("Event data does not contain 'settings'") + return + if not isinstance(settings, OpenAIRealtimeExecutionSettings): + try: + settings = self.get_prompt_execution_settings_from_settings(settings) + except Exception as e: + logger.error( + f"Failed to properly create settings from passed settings: {settings}, error: {e}" + ) + return + assert isinstance(settings, OpenAIRealtimeExecutionSettings) # nosec + if not settings.ai_model_id: + settings.ai_model_id = self.ai_model_id + await self._send( + _create_openai_realtime_client_event( + event_type=event.service_type, + session=settings.prepare_settings_dict(), + ) + ) + case SendEvents.INPUT_AUDIO_BUFFER_APPEND: + if not data or "audio" not in data: + logger.error("Event data does not contain 'audio'") + return + await self._send( + _create_openai_realtime_client_event( + event_type=event.service_type, + audio=data["audio"], + ) + ) + case SendEvents.INPUT_AUDIO_BUFFER_COMMIT: + await self._send(_create_openai_realtime_client_event(event_type=event.service_type)) + case SendEvents.INPUT_AUDIO_BUFFER_CLEAR: + await self._send(_create_openai_realtime_client_event(event_type=event.service_type)) + case SendEvents.CONVERSATION_ITEM_CREATE: + if not data or "item" not in data: + logger.error("Event data does not contain 'item'") + return + content = data["item"] + contents = content.items if isinstance(content, ChatMessageContent) else [content] + for item in contents: + match item: + case TextContent(): + await self._send( + _create_openai_realtime_client_event( + event_type=event.service_type, + **dict( + type="message", + content=[ + { + "type": "input_text", + "text": item.text, + } + ], + role="user", + ), + ) + ) + case FunctionCallContent(): + await self._send( + _create_openai_realtime_client_event( + event_type=event.service_type, + **dict( + type="function_call", + name=item.name or item.function_name, + arguments="" + if not item.arguments + else item.arguments + if isinstance(item.arguments, str) + else json.dumps(item.arguments), + call_id=item.metadata.get("call_id"), + ), + ) + ) + + case FunctionResultContent(): + await self._send( + _create_openai_realtime_client_event( + event_type=event.service_type, + **dict( + type="function_call_output", + output=item.result, + call_id=item.metadata.get("call_id"), + ), + ) + ) + case SendEvents.CONVERSATION_ITEM_TRUNCATE: + if not data or "item_id" not in data: + logger.error("Event data does not contain 'item_id'") + return + await self._send( + _create_openai_realtime_client_event( + event_type=event.service_type, + item_id=data["item_id"], + content_index=0, + audio_end_ms=data.get("audio_end_ms", 0), + ) + ) + case SendEvents.CONVERSATION_ITEM_DELETE: + if not data or "item_id" not in data: + logger.error("Event data does not contain 'item_id'") + return + await self._send( + _create_openai_realtime_client_event( + event_type=event.service_type, + item_id=data["item_id"], + ) + ) + case SendEvents.RESPONSE_CREATE: + await self._send( + _create_openai_realtime_client_event( + event_type=event.service_type, event_id=data.get("event_id", None) if data else None + ) + ) + case SendEvents.RESPONSE_CANCEL: + await self._send( + _create_openai_realtime_client_event( + event_type=event.service_type, + response_id=data.get("response_id", None) if data else None, + ) + ) + + @override + def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]: + from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_realtime_execution_settings import ( # noqa + OpenAIRealtimeExecutionSettings, + ) + + return OpenAIRealtimeExecutionSettings + + @override + def _update_function_choice_settings_callback( + self, + ) -> Callable[[FunctionCallChoiceConfiguration, "PromptExecutionSettings", FunctionChoiceType], None]: + return update_settings_from_function_call_configuration + + @override + async def create_session( + self, + settings: "PromptExecutionSettings | None" = None, + chat_history: "ChatHistory | None" = None, + **kwargs: Any, + ) -> None: + pass + + @override + def receive(self, **kwargs: Any) -> AsyncGenerator[RealtimeEvent, None]: + pass + + @override + async def close_session(self) -> None: + pass diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/realtime/open_ai_realtime_webrtc.py b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/open_ai_realtime_webrtc.py new file mode 100644 index 000000000000..2a6bf71dfd68 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/open_ai_realtime_webrtc.py @@ -0,0 +1,211 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import contextlib +import json +import logging +import sys +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast + +if sys.version_info >= (3, 12): + from typing import override # pragma: no cover +else: + from typing_extensions import override # pragma: no cover + +from aiohttp import ClientSession +from aiortc import ( + MediaStreamTrack, + RTCConfiguration, + RTCDataChannel, + RTCIceServer, + RTCPeerConnection, + RTCSessionDescription, +) +from av.audio.frame import AudioFrame +from openai._models import construct_type_unchecked +from openai.types.beta.realtime.realtime_client_event import RealtimeClientEvent +from openai.types.beta.realtime.realtime_server_event import RealtimeServerEvent +from pydantic import PrivateAttr + +from semantic_kernel.connectors.ai.open_ai.services.realtime.const import ListenEvents +from semantic_kernel.connectors.ai.open_ai.services.realtime.open_ai_realtime_base import OpenAIRealtimeBase +from semantic_kernel.contents.audio_content import AudioContent +from semantic_kernel.contents.events import RealtimeEvent +from semantic_kernel.contents.events.realtime_event import AudioEvent +from semantic_kernel.utils.experimental_decorator import experimental_class + +if TYPE_CHECKING: + from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings + from semantic_kernel.contents.chat_history import ChatHistory + + +logger: logging.Logger = logging.getLogger(__name__) + + +@experimental_class +class OpenAIRealtimeWebRTCBase(OpenAIRealtimeBase): + """OpenAI WebRTC Realtime service.""" + + protocol: ClassVar[Literal["webrtc"]] = "webrtc" + peer_connection: RTCPeerConnection | None = None + data_channel: RTCDataChannel | None = None + audio_track: MediaStreamTrack | None = None + _receive_buffer: asyncio.Queue[RealtimeEvent] = PrivateAttr(default_factory=asyncio.Queue) + + @override + async def receive( + self, + **kwargs: Any, + ) -> AsyncGenerator[RealtimeEvent, None]: + while True: + event = await self._receive_buffer.get() + yield event + + async def _send(self, event: RealtimeClientEvent) -> None: + if not self.data_channel: + logger.error("Data channel not initialized") + return + while self.data_channel.readyState != "open": + await asyncio.sleep(0.1) + try: + self.data_channel.send(event.model_dump_json(exclude_none=True)) + except Exception as e: + logger.error(f"Failed to send event {event} with error: {e!s}") + + @override + async def create_session( + self, + settings: "PromptExecutionSettings | None" = None, + chat_history: "ChatHistory | None" = None, + **kwargs: Any, + ) -> None: + """Create a session in the service.""" + if not self.audio_track: + raise Exception("Audio track not initialized") + self.peer_connection = RTCPeerConnection( + configuration=RTCConfiguration(iceServers=[RTCIceServer(urls="stun:stun.l.google.com:19302")]) + ) + + # track is the audio track being returned from the service + self.peer_connection.add_listener("track", self._on_track) + + # data channel is used to send and receive messages + self.data_channel = self.peer_connection.createDataChannel("oai-events", protocol="json") + self.data_channel.add_listener("message", self._on_data) + + # this is the incoming audio, which sends audio to the service + self.peer_connection.addTransceiver(self.audio_track) + + offer = await self.peer_connection.createOffer() + await self.peer_connection.setLocalDescription(offer) + + try: + ephemeral_token = await self._get_ephemeral_token() + headers = {"Authorization": f"Bearer {ephemeral_token}", "Content-Type": "application/sdp"} + + async with ( + ClientSession() as session, + session.post( + f"{self.client.beta.realtime._client.base_url}realtime?model={self.ai_model_id}", + headers=headers, + data=offer.sdp, + ) as response, + ): + if response.status not in [200, 201]: + error_text = await response.text() + raise Exception(f"OpenAI WebRTC error: {error_text}") + + sdp_answer = await response.text() + answer = RTCSessionDescription(sdp=sdp_answer, type="answer") + await self.peer_connection.setRemoteDescription(answer) + logger.info("Connected to OpenAI WebRTC") + + except Exception as e: + logger.error(f"Failed to connect to OpenAI: {e!s}") + raise + + if settings or chat_history or kwargs: + await self.update_session(settings=settings, chat_history=chat_history, **kwargs) + + @override + async def close_session(self) -> None: + """Close the session in the service.""" + if self.peer_connection: + with contextlib.suppress(asyncio.CancelledError): + await self.peer_connection.close() + self.peer_connection = None + if self.data_channel: + with contextlib.suppress(asyncio.CancelledError): + self.data_channel.close() + self.data_channel = None + + async def _on_track(self, track: "MediaStreamTrack") -> None: + logger.info(f"Received {track.kind} track from remote") + if track.kind != "audio": + return + while True: + try: + # This is a MediaStreamTrack, so the type is AudioFrame + # this might need to be updated if video becomes part of this + frame: AudioFrame = await track.recv() # type: ignore + except Exception as e: + logger.error(f"Error getting audio frame: {e!s}") + break + + try: + if self.audio_output_callback: + await self.audio_output_callback(frame.to_ndarray()) + + except Exception as e: + logger.error(f"Error playing remote audio frame: {e!s}") + try: + await self._receive_buffer.put( + AudioEvent( + audio=AudioContent(data=frame.to_ndarray(), data_format="np.int16", inner_content=frame), + service_type=ListenEvents.RESPONSE_AUDIO_DELTA, + ), + ) + except Exception as e: + logger.error(f"Error processing remote audio frame: {e!s}") + await asyncio.sleep(0.01) + + async def _on_data(self, data: str) -> None: + """This method is called whenever a data channel message is received. + + The data is parsed into a RealtimeServerEvent (by OpenAI code) and then processed. + Audio data is not send through this channel, use _on_track for that. + """ + try: + event = cast( + RealtimeServerEvent, + construct_type_unchecked(value=json.loads(data), type_=cast(Any, RealtimeServerEvent)), + ) + except Exception as e: + logger.error(f"Failed to parse event {data} with error: {e!s}") + return + async for parsed_event in self._parse_event(event): + await self._receive_buffer.put(parsed_event) + + async def _get_ephemeral_token(self) -> str: + """Get an ephemeral token from OpenAI.""" + headers = {"Authorization": f"Bearer {self.client.api_key}", "Content-Type": "application/json"} + data = {"model": self.ai_model_id, "voice": "echo"} + + try: + async with ( + ClientSession() as session, + session.post( + f"{self.client.beta.realtime._client.base_url}/realtime/sessions", headers=headers, json=data + ) as response, + ): + if response.status not in [200, 201]: + error_text = await response.text() + raise Exception(f"Failed to get ephemeral token: {error_text}") + + result = await response.json() + return result["client_secret"]["value"] + + except Exception as e: + logger.error(f"Failed to get ephemeral token: {e!s}") + raise diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/realtime/open_ai_realtime_websocket.py b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/open_ai_realtime_websocket.py new file mode 100644 index 000000000000..3b476d96d3c0 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/open_ai_realtime_websocket.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import base64 +import logging +import sys +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING, Any, ClassVar, Literal + +if sys.version_info >= (3, 12): + from typing import override # pragma: no cover +else: + from typing_extensions import override # pragma: no cover + +import numpy as np +from openai.resources.beta.realtime.realtime import AsyncRealtimeConnection +from openai.types.beta.realtime.realtime_client_event import RealtimeClientEvent +from pydantic import Field + +from semantic_kernel.connectors.ai.open_ai.services.realtime.const import ListenEvents +from semantic_kernel.connectors.ai.open_ai.services.realtime.open_ai_realtime_base import OpenAIRealtimeBase +from semantic_kernel.contents.audio_content import AudioContent +from semantic_kernel.contents.events.realtime_event import AudioEvent, RealtimeEvent +from semantic_kernel.utils.experimental_decorator import experimental_class + +if TYPE_CHECKING: + from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings + from semantic_kernel.contents.chat_history import ChatHistory + +logger: logging.Logger = logging.getLogger(__name__) + + +@experimental_class +class OpenAIRealtimeWebsocketBase(OpenAIRealtimeBase): + """OpenAI Realtime service.""" + + protocol: ClassVar[Literal["websocket"]] = "websocket" + connection: AsyncRealtimeConnection | None = None + connected: asyncio.Event = Field(default_factory=asyncio.Event) + + @override + async def receive( + self, + **kwargs: Any, + ) -> AsyncGenerator[RealtimeEvent, None]: + await self.connected.wait() + if not self.connection: + raise ValueError("Connection is not established.") + + async for event in self.connection: + if event.type == ListenEvents.RESPONSE_AUDIO_DELTA.value: + audio_bytes = base64.b64decode(event.delta) + if self.audio_output_callback: + await self.audio_output_callback(np.frombuffer(audio_bytes, dtype=np.int16)) + try: + yield AudioEvent( + audio=AudioContent(data=audio_bytes, data_format="base64", inner_content=event), + service_type=event.type, + ) + except Exception as e: + logger.error(f"Error processing remote audio frame: {e!s}") + else: + async for event in self._parse_event(event): + yield event + + async def _send(self, event: RealtimeClientEvent) -> None: + await self.connected.wait() + if not self.connection: + raise ValueError("Connection is not established.") + try: + await self.connection.send(event) + except Exception as e: + logger.error(f"Error sending response: {e!s}") + + @override + async def create_session( + self, + settings: "PromptExecutionSettings | None" = None, + chat_history: "ChatHistory | None" = None, + **kwargs: Any, + ) -> None: + """Create a session in the service.""" + self.connection = await self.client.beta.realtime.connect(model=self.ai_model_id).enter() + self.connected.set() + if settings or chat_history or kwargs: + await self.update_session(settings=settings, chat_history=chat_history, **kwargs) + + @override + async def close_session(self) -> None: + """Close the session in the service.""" + if self.connected.is_set() and self.connection: + await self.connection.close() + self.connection = None + self.connected.clear() diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/realtime/utils.py b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/utils.py new file mode 100644 index 000000000000..bb815eead6dd --- /dev/null +++ b/python/semantic_kernel/connectors/ai/open_ai/services/realtime/utils.py @@ -0,0 +1,126 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import TYPE_CHECKING, Any + +from openai.types.beta.realtime import ( + ConversationItem, + ConversationItemCreateEvent, + ConversationItemDeleteEvent, + ConversationItemTruncateEvent, + InputAudioBufferAppendEvent, + InputAudioBufferClearEvent, + InputAudioBufferCommitEvent, + RealtimeClientEvent, + ResponseCancelEvent, + ResponseCreateEvent, + SessionUpdateEvent, +) +from openai.types.beta.realtime.response_create_event import Response +from openai.types.beta.realtime.session_update_event import Session + +from semantic_kernel.connectors.ai.open_ai.services.realtime.const import SendEvents + +if TYPE_CHECKING: + from semantic_kernel.connectors.ai.function_choice_behavior import ( + FunctionCallChoiceConfiguration, + FunctionChoiceType, + ) + from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings + from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata + + +def update_settings_from_function_call_configuration( + function_choice_configuration: "FunctionCallChoiceConfiguration", + settings: "PromptExecutionSettings", + type: "FunctionChoiceType", +) -> None: + """Update the settings from a FunctionChoiceConfiguration.""" + if ( + function_choice_configuration.available_functions + and hasattr(settings, "tool_choice") + and hasattr(settings, "tools") + ): + settings.tool_choice = type + settings.tools = [ + kernel_function_metadata_to_function_call_format(f) + for f in function_choice_configuration.available_functions + ] + + +def kernel_function_metadata_to_function_call_format( + metadata: "KernelFunctionMetadata", +) -> dict[str, Any]: + """Convert the kernel function metadata to function calling format.""" + return { + "type": "function", + "name": metadata.fully_qualified_name, + "description": metadata.description or "", + "parameters": { + "type": "object", + "properties": { + param.name: param.schema_data for param in metadata.parameters if param.include_in_function_choices + }, + "required": [p.name for p in metadata.parameters if p.is_required and p.include_in_function_choices], + }, + } + + +def _create_openai_realtime_client_event(event_type: SendEvents, **kwargs: Any) -> RealtimeClientEvent: + match event_type: + case SendEvents.SESSION_UPDATE: + return SessionUpdateEvent( + type=event_type, + session=Session.model_validate(kwargs.pop("session")), + **kwargs, + ) + case SendEvents.INPUT_AUDIO_BUFFER_APPEND: + return InputAudioBufferAppendEvent( + type=event_type, + **kwargs, + ) + case SendEvents.INPUT_AUDIO_BUFFER_COMMIT: + return InputAudioBufferCommitEvent( + type=event_type, + **kwargs, + ) + case SendEvents.INPUT_AUDIO_BUFFER_CLEAR: + return InputAudioBufferClearEvent( + type=event_type, + **kwargs, + ) + case SendEvents.CONVERSATION_ITEM_CREATE: + if "event_id" in kwargs: + event_id = kwargs.pop("event_id") + if "previous_item_id" in kwargs: + previous_item_id = kwargs.pop("previous_item_id") + event_kwargs = {"event_id": event_id} if "event_id" in kwargs else {} + event_kwargs.update({"previous_item_id": previous_item_id} if "previous_item_id" in kwargs else {}) + return ConversationItemCreateEvent( + type=event_type, + item=ConversationItem.model_validate(kwargs), + **event_kwargs, + ) + case SendEvents.CONVERSATION_ITEM_TRUNCATE: + return ConversationItemTruncateEvent( + type=event_type, + **kwargs, + ) + case SendEvents.CONVERSATION_ITEM_DELETE: + return ConversationItemDeleteEvent( + type=event_type, + **kwargs, + ) + case SendEvents.RESPONSE_CREATE: + event_kwargs = {"event_id": kwargs.pop("event_id")} if "event_id" in kwargs else {} + return ResponseCreateEvent( + type=event_type, + response=Response.model_validate(kwargs), + **event_kwargs, + ) + case SendEvents.RESPONSE_CANCEL: + return ResponseCancelEvent( + type=event_type, + **kwargs, + ) + case _: + raise ValueError(f"Unknown event type: {event_type}") diff --git a/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py b/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py index 6423a5385a33..7276af4b1f3b 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py +++ b/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py @@ -32,6 +32,9 @@ class OpenAISettings(KernelBaseSettings): (Env var OPENAI_AUDIO_TO_TEXT_MODEL_ID) - text_to_audio_model_id: str | None - The OpenAI text to audio model ID to use, for example, jukebox-1. (Env var OPENAI_TEXT_TO_AUDIO_MODEL_ID) + - realtime_model_id: str | None - The OpenAI realtime model ID to use, + for example, gpt-4o-realtime-preview-2024-12-17. + (Env var OPENAI_REALTIME_MODEL_ID) - env_file_path: str | None - if provided, the .env settings are read from this file path location """ @@ -45,3 +48,4 @@ class OpenAISettings(KernelBaseSettings): text_to_image_model_id: str | None = None audio_to_text_model_id: str | None = None text_to_audio_model_id: str | None = None + realtime_model_id: str | None = None diff --git a/python/semantic_kernel/connectors/ai/realtime_client_base.py b/python/semantic_kernel/connectors/ai/realtime_client_base.py new file mode 100644 index 000000000000..cc70df1f3c90 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/realtime_client_base.py @@ -0,0 +1,114 @@ +# Copyright (c) Microsoft. All rights reserved. + +import sys +from abc import ABC, abstractmethod +from collections.abc import AsyncGenerator, Callable, Coroutine +from typing import TYPE_CHECKING, Any, ClassVar + +if sys.version_info >= (3, 11): + from typing import Self # pragma: no cover +else: + from typing_extensions import Self # pragma: no cover + +from numpy import ndarray + +from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration +from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType +from semantic_kernel.contents.events.realtime_event import RealtimeEvent +from semantic_kernel.services.ai_service_client_base import AIServiceClientBase +from semantic_kernel.utils.experimental_decorator import experimental_class + +if TYPE_CHECKING: + from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings + from semantic_kernel.contents.chat_history import ChatHistory + + +@experimental_class +class RealtimeClientBase(AIServiceClientBase, ABC): + """Base class for a realtime client.""" + + SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = False + audio_output_callback: Callable[[ndarray], Coroutine[Any, Any, None]] | None = None + + @abstractmethod + async def send(self, event: RealtimeEvent) -> None: + """Send an event to the service. + + Args: + event: The event to send. + kwargs: Additional arguments. + """ + raise NotImplementedError + + @abstractmethod + def receive( + self, + **kwargs: Any, + ) -> AsyncGenerator[RealtimeEvent, None]: + """Starts listening for messages from the service, generates events. + + Args: + kwargs: Additional arguments. + """ + raise NotImplementedError + + @abstractmethod + async def create_session( + self, + settings: "PromptExecutionSettings | None" = None, + chat_history: "ChatHistory | None" = None, + **kwargs: Any, + ) -> None: + """Create a session in the service. + + Args: + settings: Prompt execution settings. + chat_history: Chat history. + kwargs: Additional arguments. + """ + raise NotImplementedError + + @abstractmethod + async def update_session( + self, + settings: "PromptExecutionSettings | None" = None, + chat_history: "ChatHistory | None" = None, + **kwargs: Any, + ) -> None: + """Update a session in the service. + + Can be used when using the context manager instead of calling create_session with these same arguments. + + Args: + settings: Prompt execution settings. + chat_history: Chat history. + kwargs: Additional arguments. + """ + raise NotImplementedError + + @abstractmethod + async def close_session(self) -> None: + """Close the session in the service.""" + pass + + def _update_function_choice_settings_callback( + self, + ) -> Callable[[FunctionCallChoiceConfiguration, "PromptExecutionSettings", FunctionChoiceType], None]: + """Return the callback function to update the settings from a function call configuration. + + Override this method to provide a custom callback function to + update the settings from a function call configuration. + """ + return lambda configuration, settings, choice_type: None + + async def __aenter__(self) -> "Self": + """Enter the context manager. + + Default implementation calls the create session method. + """ + await self.create_session() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + """Exit the context manager.""" + await self.close_session() diff --git a/python/semantic_kernel/contents/audio_content.py b/python/semantic_kernel/contents/audio_content.py index 8ee4197aaa8f..b7e157a242bf 100644 --- a/python/semantic_kernel/contents/audio_content.py +++ b/python/semantic_kernel/contents/audio_content.py @@ -3,9 +3,11 @@ import mimetypes from typing import Any, ClassVar, Literal, TypeVar +from numpy import ndarray from pydantic import Field +from pydantic_core import Url -from semantic_kernel.contents.binary_content import BinaryContent +from semantic_kernel.contents.binary_content import BinaryContent, DataUrl from semantic_kernel.contents.const import AUDIO_CONTENT_TAG, ContentTypes from semantic_kernel.utils.experimental_decorator import experimental_class @@ -41,8 +43,34 @@ class AudioContent(BinaryContent): content_type: Literal[ContentTypes.AUDIO_CONTENT] = Field(AUDIO_CONTENT_TAG, init=False) # type: ignore tag: ClassVar[str] = AUDIO_CONTENT_TAG + def __init__( + self, + uri: Url | str | None = None, + data_uri: DataUrl | str | None = None, + data: str | bytes | ndarray | None = None, + data_format: str | None = None, + mime_type: str | None = None, + **kwargs: Any, + ): + """Create a Audio Content object, either from a data_uri or data. + + Args: + uri (Url | str | None): The reference uri of the content. + data_uri (DataUrl | None): The data uri of the content. + data (str | bytes | ndarray | None): The data of the content. + data_format (str | None): The format of the data (e.g. base64). + mime_type (str | None): The mime type of the image, only used with data. + kwargs (Any): Any additional arguments: + inner_content (Any): The inner content of the response, + this should hold all the information from the response so even + when not creating a subclass a developer can leverage the full thing. + ai_model_id (str | None): The id of the AI model that generated this response. + metadata (dict[str, Any]): Any metadata that should be attached to the response. + """ + super().__init__(uri=uri, data_uri=data_uri, data=data, data_format=data_format, mime_type=mime_type, **kwargs) + @classmethod - def from_audio_file(cls: type[_T], path: str) -> "AudioContent": + def from_audio_file(cls: type[_T], path: str) -> _T: """Create an instance from an audio file.""" mime_type = mimetypes.guess_type(path)[0] with open(path, "rb") as audio_file: @@ -51,3 +79,8 @@ def from_audio_file(cls: type[_T], path: str) -> "AudioContent": def to_dict(self) -> dict[str, Any]: """Convert the instance to a dictionary.""" return {"type": "audio_url", "audio_url": {"uri": str(self)}} + + @classmethod + def from_ndarray(cls: type[_T], data: ndarray, mime_type: str) -> _T: + """Create an instance from an ndarray.""" + return cls(data=data, mime_type=mime_type) diff --git a/python/semantic_kernel/contents/binary_content.py b/python/semantic_kernel/contents/binary_content.py index a36535b0c120..c580830601f1 100644 --- a/python/semantic_kernel/contents/binary_content.py +++ b/python/semantic_kernel/contents/binary_content.py @@ -2,9 +2,11 @@ import logging import os +from base64 import b64encode from typing import Annotated, Any, ClassVar, Literal, TypeVar from xml.etree.ElementTree import Element # nosec +from numpy import ndarray from pydantic import Field, FilePath, UrlConstraints, computed_field from pydantic_core import Url @@ -48,7 +50,7 @@ def __init__( self, uri: Url | str | None = None, data_uri: DataUrl | str | None = None, - data: str | bytes | None = None, + data: str | bytes | ndarray | None = None, data_format: str | None = None, mime_type: str | None = None, **kwargs: Any, @@ -68,22 +70,25 @@ def __init__( ai_model_id (str | None): The id of the AI model that generated this response. metadata (dict[str, Any]): Any metadata that should be attached to the response. """ - _data_uri = None + data_uri_ = None if data_uri: - _data_uri = DataUri.from_data_uri(data_uri, self.default_mime_type) + data_uri_ = DataUri.from_data_uri(data_uri, self.default_mime_type) if "metadata" in kwargs: - kwargs["metadata"].update(_data_uri.parameters) + kwargs["metadata"].update(data_uri_.parameters) else: - kwargs["metadata"] = _data_uri.parameters + kwargs["metadata"] = data_uri_.parameters + elif isinstance(data, ndarray): + data_uri_ = DataUri(data_array=data, mime_type=mime_type or self.default_mime_type) elif data: - if isinstance(data, str): - _data_uri = DataUri( - data_str=data, data_format=data_format, mime_type=mime_type or self.default_mime_type - ) - else: - _data_uri = DataUri( - data_bytes=data, data_format=data_format, mime_type=mime_type or self.default_mime_type - ) + match data: + case str(): + data_uri_ = DataUri( + data_str=data, data_format=data_format, mime_type=mime_type or self.default_mime_type + ) + case bytes(): + data_uri_ = DataUri( + data_bytes=data, data_format=data_format, mime_type=mime_type or self.default_mime_type + ) if uri is not None: if isinstance(uri, str) and os.path.exists(uri): @@ -92,7 +97,7 @@ def __init__( uri = Url(uri) super().__init__(uri=uri, **kwargs) - self._data_uri = _data_uri + self._data_uri = data_uri_ @computed_field # type: ignore @property @@ -109,8 +114,10 @@ def data_uri(self, value: str): self.metadata.update(self._data_uri.parameters) @property - def data(self) -> bytes: + def data(self) -> bytes | ndarray: """Get the data.""" + if self._data_uri and self._data_uri.data_array is not None: + return self._data_uri.data_array if self._data_uri and self._data_uri.data_bytes: return self._data_uri.data_bytes if self._data_uri and self._data_uri.data_str: @@ -118,15 +125,18 @@ def data(self) -> bytes: return b"" @data.setter - def data(self, value: str | bytes): + def data(self, value: str | bytes | ndarray): """Set the data.""" if self._data_uri: self._data_uri.update_data(value) else: - if isinstance(value, str): - self._data_uri = DataUri(data_str=value, mime_type=self.mime_type) - else: - self._data_uri = DataUri(data_bytes=value, mime_type=self.mime_type) + match value: + case str(): + self._data_uri = DataUri(data_str=value, mime_type=self.mime_type) + case bytes(): + self._data_uri = DataUri(data_bytes=value, mime_type=self.mime_type) + case ndarray(): + self._data_uri = DataUri(data_array=value, mime_type=self.mime_type) @property def mime_type(self) -> str: @@ -167,9 +177,22 @@ def from_element(cls: type[_T], element: Element) -> _T: def write_to_file(self, path: str | FilePath) -> None: """Write the data to a file.""" + if isinstance(self.data, ndarray): + self.data.tofile(path) # codespell:ignore tofile + return with open(path, "wb") as file: file.write(self.data) def to_dict(self) -> dict[str, Any]: """Convert the instance to a dictionary.""" return {"type": "binary", "binary": {"uri": str(self)}} + + def to_base64_bytestring(self, encoding: str = "utf-8") -> str: + """Convert the instance to a bytestring.""" + if self._data_uri and self._data_uri.data_array is not None: + return b64encode(self._data_uri.data_array.tobytes()).decode(encoding) + if self._data_uri and self._data_uri.data_bytes: + return self._data_uri.data_bytes.decode(encoding) + if self._data_uri and self._data_uri.data_str: + return self._data_uri.data_str + return "" diff --git a/python/semantic_kernel/contents/chat_message_content.py b/python/semantic_kernel/contents/chat_message_content.py index ce52a7428831..42e50013976c 100644 --- a/python/semantic_kernel/contents/chat_message_content.py +++ b/python/semantic_kernel/contents/chat_message_content.py @@ -10,6 +10,7 @@ from pydantic import Field from semantic_kernel.contents.annotation_content import AnnotationContent +from semantic_kernel.contents.audio_content import AudioContent from semantic_kernel.contents.const import ( ANNOTATION_CONTENT_TAG, CHAT_MESSAGE_CONTENT_TAG, @@ -56,6 +57,7 @@ | FileReferenceContent | StreamingAnnotationContent | StreamingFileReferenceContent + | AudioContent ) logger = logging.getLogger(__name__) diff --git a/python/semantic_kernel/contents/events/__init__.py b/python/semantic_kernel/contents/events/__init__.py new file mode 100644 index 000000000000..432c4a9c0851 --- /dev/null +++ b/python/semantic_kernel/contents/events/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.contents.events.realtime_event import ( + AudioEvent, + FunctionCallEvent, + FunctionResultEvent, + ImageEvent, + RealtimeEvent, + ServiceEvent, + TextEvent, +) + +__all__ = [ + "AudioEvent", + "FunctionCallEvent", + "FunctionResultEvent", + "ImageEvent", + "RealtimeEvent", + "ServiceEvent", + "TextEvent", +] diff --git a/python/semantic_kernel/contents/events/realtime_event.py b/python/semantic_kernel/contents/events/realtime_event.py new file mode 100644 index 000000000000..682c3b4d4e79 --- /dev/null +++ b/python/semantic_kernel/contents/events/realtime_event.py @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Annotated, Any, ClassVar, Literal, TypeAlias, Union + +from pydantic import Field + +from semantic_kernel.contents.audio_content import AudioContent +from semantic_kernel.contents.function_call_content import FunctionCallContent +from semantic_kernel.contents.function_result_content import FunctionResultContent +from semantic_kernel.contents.image_content import ImageContent +from semantic_kernel.contents.text_content import TextContent +from semantic_kernel.kernel_pydantic import KernelBaseModel + +RealtimeEvent: TypeAlias = Annotated[ + Union["ServiceEvent", "AudioEvent", "TextEvent", "FunctionCallEvent", "FunctionResultEvent", "ImageEvent"], + Field(discriminator="event_type"), +] + + +class ServiceEvent(KernelBaseModel): + """Base class for all service events.""" + + event: Any | None = Field(default=None, description="The event content.") + service_type: str + event_type: ClassVar[Literal["service"]] = "service" + + +class AudioEvent(KernelBaseModel): + """Audio event type.""" + + audio: AudioContent = Field(..., description="Audio content.") + service_type: str | None = None + event_type: ClassVar[Literal["audio"]] = "audio" + + +class TextEvent(KernelBaseModel): + """Text event type.""" + + text: TextContent = Field(..., description="Text content.") + service_type: str | None = None + event_type: ClassVar[Literal["text"]] = "text" + + +class FunctionCallEvent(KernelBaseModel): + """Function call event type.""" + + function_call: FunctionCallContent = Field(..., description="Function call content.") + service_type: str | None = None + event_type: ClassVar[Literal["function_call"]] = "function_call" + + +class FunctionResultEvent(KernelBaseModel): + """Function result event type.""" + + function_result: FunctionResultContent = Field(..., description="Function result content.") + service_type: str | None = None + event_type: ClassVar[Literal["function_result"]] = "function_result" + + +class ImageEvent(KernelBaseModel): + """Image event type.""" + + image: ImageContent = Field(..., description="Image content.") + service_type: str | None = None + event_type: ClassVar[Literal["image"]] = "image" diff --git a/python/semantic_kernel/contents/function_call_content.py b/python/semantic_kernel/contents/function_call_content.py index 7067311f4c8a..a8b2509336e1 100644 --- a/python/semantic_kernel/contents/function_call_content.py +++ b/python/semantic_kernel/contents/function_call_content.py @@ -124,6 +124,7 @@ def __add__(self, other: "FunctionCallContent | None") -> "FunctionCallContent": index=self.index or other.index, name=self.name or other.name, arguments=self.combine_arguments(self.arguments, other.arguments), + metadata=self.metadata | other.metadata, ) def combine_arguments( diff --git a/python/semantic_kernel/contents/streaming_chat_message_content.py b/python/semantic_kernel/contents/streaming_chat_message_content.py index be1ca56f113b..926d140ed241 100644 --- a/python/semantic_kernel/contents/streaming_chat_message_content.py +++ b/python/semantic_kernel/contents/streaming_chat_message_content.py @@ -6,6 +6,7 @@ from pydantic import Field +from semantic_kernel.contents.audio_content import AudioContent from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent from semantic_kernel.contents.function_result_content import FunctionResultContent @@ -20,6 +21,7 @@ from semantic_kernel.exceptions import ContentAdditionException ITEM_TYPES = Union[ + AudioContent, ImageContent, StreamingTextContent, FunctionCallContent, diff --git a/python/semantic_kernel/contents/utils/data_uri.py b/python/semantic_kernel/contents/utils/data_uri.py index d49022a6b104..371d64206972 100644 --- a/python/semantic_kernel/contents/utils/data_uri.py +++ b/python/semantic_kernel/contents/utils/data_uri.py @@ -12,6 +12,7 @@ else: from typing import Self # type: ignore # pragma: no cover +from numpy import ndarray from pydantic import Field, ValidationError, field_validator, model_validator from pydantic_core import Url @@ -28,28 +29,43 @@ class DataUri(KernelBaseModel, validate_assignment=True): data_bytes: bytes | None = None data_str: str | None = None + data_array: ndarray | None = None mime_type: str | None = None parameters: dict[str, str] = Field(default_factory=dict) data_format: str | None = None - def update_data(self, value: str | bytes): + def update_data(self, value: str | bytes | ndarray): """Update the data, using either a string or bytes.""" - if isinstance(value, str): - self.data_str = value - else: - self.data_bytes = value + match value: + case str(): + self.data_str = value + case bytes(): + self.data_bytes = value + case ndarray(): + self.data_array = value @model_validator(mode="before") @classmethod def _validate_data(cls, values: Any) -> dict[str, Any]: """Validate the data.""" - if isinstance(values, dict) and not values.get("data_bytes") and not values.get("data_str"): - raise ContentInitializationError("Either data_bytes or data_str must be provided.") + if ( + isinstance(values, dict) + and not values.get("data_bytes") + and not values.get("data_str") + and values.get("data_array") is None + ): + raise ContentInitializationError("Either data_bytes, data_str or data_array must be provided.") return values @model_validator(mode="after") def _parse_data(self) -> Self: - """Parse the data bytes to str.""" + """Parse the data bytes to str. + + Will try to decode the data bytes to a string if it is not already set. + However if the data array is used, it will not be converted to a string. + """ + if self.data_array is not None: + return self if not self.data_str and self.data_bytes: if self.data_format and self.data_format.lower() == "base64": self.data_str = base64.b64encode(self.data_bytes).decode("utf-8") @@ -113,10 +129,12 @@ def from_data_uri(cls: type[_T], data_uri: str | Url, default_mime_type: str = " def to_string(self, metadata: dict[str, str] = {}) -> str: """Return the data uri as a string.""" + if self.data_array: + data_str = self.data_array.tobytes().decode("utf-8") parameters = ";".join([f"{key}={val}" for key, val in metadata.items()]) parameters = f";{parameters}" if parameters else "" data_format = f"{self.data_format}" if self.data_format else "" - return f"data:{self.mime_type or ''}{parameters};{data_format},{self.data_str}" + return f"data:{self.mime_type or ''}{parameters};{data_format},{self.data_str or data_str}" def __eq__(self, value: object) -> bool: """Check if the data uri is equal to another.""" diff --git a/python/uv.lock b/python/uv.lock index 6daaa60a388a..539cd9cfcb1f 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -1,18 +1,18 @@ version = 1 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.13' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version >= '3.13' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'linux'", - "python_full_version >= '3.13' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", ] supported-markers = [ "sys_platform == 'darwin'", @@ -125,6 +125,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/62/c9fa5bafe03186a0e4699150a7fed9b1e73240996d0d2f0e5f70f3fdf471/aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99", size = 436081 }, ] +[[package]] +name = "aioice" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "ifaddr", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/b6/e2b0e48ccb5b04fe29265e93f14a0915f416e359c897ae87d570566c430b/aioice-0.9.0.tar.gz", hash = "sha256:fc2401b1c4b6e19372eaaeaa28fd1bd9cbf6b0e412e48625297c53b495eebd1e", size = 40324 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/35/d21e48d3ba25d32aba5d142d54c4491376c659dd74d052a30dd25198007b/aioice-0.9.0-py3-none-any.whl", hash = "sha256:b609597a3a5a611e0004ff04772e16aceb881d51c25c0afc4ceac05d5e50024e", size = 24177 }, +] + +[[package]] +name = "aiortc" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aioice", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "av", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-crc32c", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyee", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pylibsrtp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyopenssl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/32/e9b01e2271124643e5dc15c273f2bb8155efebf5bc2115407441ac62f4c5/aiortc-1.9.0.tar.gz", hash = "sha256:03faa76d76ef0e5989ac10386898b029369756102217230e2fcd4b029c50b303", size = 1168973 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/01/db89910fc4dfb72ca25fd9a41326762a490d93d39d2fc4aac3f86c05857d/aiortc-1.9.0-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e3e67c1970c2cffacac53c8f161df264efc62b22721c64a621940935028ee087", size = 1216069 }, + { url = "https://files.pythonhosted.org/packages/4c/6d/76ed96521080492c7264eacf73a8cba2202f1ff9f59af1776c5a2532f332/aiortc-1.9.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d893cb3d4ffa0ff4f9bb03a88f0a700cdbcd4c0dc060a46c59a27ccd1c890663", size = 896012 }, + { url = "https://files.pythonhosted.org/packages/8c/87/1f666108764fa5b557bed4f0fd5e2acccd739bb2cca2b766dcacb53e5669/aiortc-1.9.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176b4eb38d833667f87cf719a7a3e105e25a35b138b30893294418c1c96e38db", size = 1779113 }, + { url = "https://files.pythonhosted.org/packages/32/03/f3233e936f7a81549bd95f33f3d304e2a9211cb35d819d74570c0718b1ac/aiortc-1.9.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44b610f36b8d17123855dfbe915fa6874201765b8a2c7fd9cf72d14cf417740", size = 1896322 }, + { url = "https://files.pythonhosted.org/packages/96/99/6672cf57777801c6ddacc13e1ee07f8c2151d0847a4f81455eeec998eaed/aiortc-1.9.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55505adb31d56cba19a1ef8ad6aa9b727ccdba2a83bfbfb4aa79ef3c472026a6", size = 1918600 }, + { url = "https://files.pythonhosted.org/packages/76/e3/bdb76e7e51bc4fc7a5869597de2effad073ccf5ef14de3aed742d7384107/aiortc-1.9.0-cp38-abi3-win32.whl", hash = "sha256:680b703e35870e301535c930bfe32e7d012224a91ce51531aba45a3124ef07cc", size = 923055 }, + { url = "https://files.pythonhosted.org/packages/6a/df/de098b31a3fbf1117f6d4cb84c14518636054e3c95a9d9f693a1123c95b3/aiortc-1.9.0-cp38-abi3-win_amd64.whl", hash = "sha256:de5e7020cfc2d2d9fb95690926ff2e3b3c30cd4f5f5bc68d5b6756a8eebb686e", size = 1009610 }, + { url = "https://files.pythonhosted.org/packages/95/26/c382db590897fe638254f948d8514772d13ff59b5ada0a71d87322f48c52/aiortc-1.9.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34c516ae4e70e8f64494305057af09311444325722fe6938ec38dd1e111adca9", size = 1209093 }, + { url = "https://files.pythonhosted.org/packages/68/48/2fe7de04461fdc4aee8c78c67cfe03579eaa72fb215c4b063acaeb4fd118/aiortc-1.9.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:40e61c1b84914d6f4c2968ff49353a22eed9419de74b151237cdb71af431209c", size = 888818 }, + { url = "https://files.pythonhosted.org/packages/da/d5/94bf7ed6189c316ffef930787cba009387f9bcd2f1c482392b71cca3918d/aiortc-1.9.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1924e130a441507b1315956aff05c504a274f1a09802def225d0f3a3d1870320", size = 1732549 }, + { url = "https://files.pythonhosted.org/packages/e7/0a/6495c696cd7f806bafe511fb27203ce918947c4461398384a4e6bd4b7e57/aiortc-1.9.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb62950e396c311e398925149fa76bc90b8d6525b4eccf28cba704e7ded8bf5", size = 1843911 }, + { url = "https://files.pythonhosted.org/packages/82/36/ffd0f74c73fa6abca0b76bd38473ed7d82dfbada7e57c6efe2a37ee40483/aiortc-1.9.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5234177e8d3126a0190ed9b6f8d0288daedcc0158c45cc279b4e6ac7d97f43f8", size = 1868240 }, + { url = "https://files.pythonhosted.org/packages/fb/46/8cb087a11f2f2d1139bd7e21615cc082097bffc4990d43c9f45f9cf6c8bf/aiortc-1.9.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0e31575eb050aa68e0ea4c519aef101770b2297954f49e64a5c3d73ef27702ea", size = 1004186 }, +] + [[package]] name = "aiosignal" version = "1.3.2" @@ -148,7 +192,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.43.0" +version = "0.42.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -159,9 +203,9 @@ dependencies = [ { name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/973d2ac6c9f7d1be41829c7b878cbe399385b25cc2ebe80ad0eec9999b8c/anthropic-0.43.0.tar.gz", hash = "sha256:06801f01d317a431d883230024318d48981758058bf7e079f33fb11f64b5a5c1", size = 194826 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/7c/91b79f5ae4a52497a4e330d66ea5929aec2878ee2c9f8a998dbe4f4c7f01/anthropic-0.42.0.tar.gz", hash = "sha256:bf8b0ed8c8cb2c2118038f29c58099d2f99f7847296cafdaa853910bfff4edf4", size = 192361 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/88/ded3ba979a2218a448cbc1a1e762d998b92f30529452c5104b35b6cb71f8/anthropic-0.43.0-py3-none-any.whl", hash = "sha256:f748a703f77b3244975e1aace3a935840dc653a4714fb6bba644f97cc76847b4", size = 207867 }, + { url = "https://files.pythonhosted.org/packages/ba/33/b907a6d27dd0d8d3adb4edb5c9e9c85a189719ec6855051cce3814c8ef13/anthropic-0.42.0-py3-none-any.whl", hash = "sha256:46775f65b723c078a2ac9e9de44a46db5c6a4fabeacfd165e5ea78e6817f4eff", size = 203365 }, ] [[package]] @@ -239,6 +283,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/1f/bc95e43ffb57c05b8efcc376dd55a0240bf58f47ddf5a0f92452b6457b75/Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377", size = 223827 }, ] +[[package]] +name = "av" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/f8/5adeeae0c42a7130933d168b8d84a21c98a32cb9fcf9222e2541ed0d9c7b/av-12.3.0.tar.gz", hash = "sha256:04b1892562aff3277efc79f32bd8f1d0cbb64ed011241cb3e96f9ad471816c22", size = 3833953 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/57/414fe243152ef3f5a364f3e0137c16fbfe67c3f096eac1dc49d614de8f98/av-12.3.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b3b1fe6b5ab9af2d09dcdcc5473a3523f7162c3fa0c6b3c379b697fede1e88a5", size = 24663048 }, + { url = "https://files.pythonhosted.org/packages/15/e8/8795c6cf7d4ef34b30690b3e1601982c6ce9ec8c42a681fff5791a4c4ca9/av-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b5f92ba67dca9bac8ce955b09d41e7e92977199adbd0f2aff02653bb40b0ac16", size = 19930356 }, + { url = "https://files.pythonhosted.org/packages/f9/90/6e0340af495b1028be90fae4793900df9853732e38003a795a14bb52dee5/av-12.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3389eebd1f5bb36ebfaa8441c65c14d7433b354d91f9dbb08a6e6225d16a7226", size = 31623727 }, + { url = "https://files.pythonhosted.org/packages/0a/d1/34d69a00405e0c58059431b24e8abbf2861446b740eb1813c1569a0b7467/av-12.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:385b27638bc56fd1560be3b9e86b5cc843cae931503a02e6e504c0357176873e", size = 31126299 }, + { url = "https://files.pythonhosted.org/packages/0a/5f/5ab859d8770ac1203d492e418cf949cfcac5c25994e9754c536fb37578fc/av-12.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0220fce2a62d71cc5e89617419b6224ddb43f1753b00f68b5c9af8b5f41d38c9", size = 33490936 }, + { url = "https://files.pythonhosted.org/packages/39/6f/46a468053c8ae594c91a385f2323ade83746e03ba11ba14fb79db61a23ff/av-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:8328c90f783b3392279a2d3a79789267691f5e5f7c4a160990a41194d268ec59", size = 25973279 }, + { url = "https://files.pythonhosted.org/packages/5d/20/256fa4fc4ef9bb46fdc4be4662e13a30b0334487c955961f3816d94db04b/av-12.3.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:cc06a806419fddc7102150ffe353c7d96b99b95fd12864280c91c851603fd4cb", size = 24658122 }, + { url = "https://files.pythonhosted.org/packages/5d/45/a9d0475539b4f49deb34f3da558de31cefc6be867d5c0603d575a8485069/av-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e2130ff622a574d3d5d6e88ac335efcdd98c375bb341f87d9fe540830a746f5", size = 19923068 }, + { url = "https://files.pythonhosted.org/packages/af/27/1f2b3e46059c6618fd76ba12a96b49dc8515a426cd477032cd33f80505e8/av-12.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8b9bd99f916ff4d1278654e94658e6ace7ca60f6321f254d09c8cd81d9095b", size = 32555100 }, + { url = "https://files.pythonhosted.org/packages/28/34/759741d397a8bdbb8a359b8b5d49832a444b26c9a7f79c0f88be76a6b979/av-12.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e375d1d89a5c6edfd9f66701fdb6cc9161cc1ff99d15ff0bda21ee1ad38e9e0", size = 31936355 }, + { url = "https://files.pythonhosted.org/packages/b4/6e/77426cb92117c941b0f759908bc83f34f259b11b353acb5de95972b452f7/av-12.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef9066fd8d86548e12d587cbfe7b852159e48ff3c732271c3032668d4bd7c599", size = 34416598 }, + { url = "https://files.pythonhosted.org/packages/ff/d3/4b0fddcd54d0a88ee7e035f239ebb56ce139fac8e02ee0942c43746a66ff/av-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfaa9864560e43d45d254ed95f70ab1aab24a2fa0cc35ac99eef362f1453bec0", size = 25975217 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/0636bccf5a1a2c935952614b9d34d8d8aae078c9773a60efb5376702f499/av-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5174e995772ebe33561980dca625f830aea8d39a4338728dedb41ae7dc2605af", size = 24669628 }, + { url = "https://files.pythonhosted.org/packages/ef/7d/9126abdafe20fa73d2c19fd108450363253cfea283c350618cc1434f473c/av-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:028d8b40308536f740dace3efd0178eb96825b414897c9594fb74136532901cb", size = 19928928 }, + { url = "https://files.pythonhosted.org/packages/27/75/c1b9e0aa4bd0d8b8311f366b6b38f6c6600d66baddfe2888accc7f76b1f5/av-12.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b030791ecc6185776d832d19ce196f61daf3e17e591a9bb6fd181280e1754138", size = 32793461 }, + { url = "https://files.pythonhosted.org/packages/5a/06/1364c445f8a8ab4870f0f5c4530b496257ae09de7fa01b6108525abea8b9/av-12.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3703a35481fda5798a27bf6208c1ec3b61c18931625771fb3c9fd870539c7d7", size = 32217647 }, + { url = "https://files.pythonhosted.org/packages/27/08/220d5a1ae7e7830d66d041c71e607c1f5df2e3598b12fb406b0d7c2defa7/av-12.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32f3eef56b2df289db6105f9fe2ebc9a8134a8adbd62190daeb8e22c4ff47794", size = 34746451 }, + { url = "https://files.pythonhosted.org/packages/96/67/9f1c444864d4f3e3773100b9ed20e670f80d5575b7a8fd53cca20de9d681/av-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:62d036ee8321d67190887012c3dbcd1ad83248603cc29ea75fbb75835b8d6e6e", size = 25977611 }, + { url = "https://files.pythonhosted.org/packages/e2/63/e1b22a63404a22bf49a981e2386f33a2d7fd7c1fe1087cca34cc06652b40/av-12.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e47ba817fcd46c9f2c94d638abcdeda120adedcd09605984a5cee844f739a833", size = 24271362 }, + { url = "https://files.pythonhosted.org/packages/64/08/16c8a6a0a1df2a651c0124368e470df85f3086cf98624f6698706f91e717/av-12.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b456cbb7ddd252f0f2db06a09dc10ade201e82e0eb8d3a7b609689907b2802df", size = 19575368 }, + { url = "https://files.pythonhosted.org/packages/eb/6b/18369c3cb78f6aaadcbf7c94683d75c2cefaf79962016ffbf6d0d1b21b22/av-12.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50ccb92605d59732d2a2923786a5dba746a98c5fd6b4d30a5975785673c42c9e", size = 23344574 }, + { url = "https://files.pythonhosted.org/packages/40/61/f26be7deb3675f15925f6006d9f0a2937a5cb15a176b32935eaac8ecaeff/av-12.3.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:061b15203f22e95c60b1cc14702618acbf18e976cf3144298e2f6dc89b7aa993", size = 23272262 }, + { url = "https://files.pythonhosted.org/packages/e9/3f/fb6ac8f1df45ff06155e0850e53d944536966d0564e0b0f5b839e67352cb/av-12.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65849ca4e54f2d50ed263ab488ef051bd973cbdbe2a7c947b31ff965bb7bfddd", size = 25186971 }, + { url = "https://files.pythonhosted.org/packages/94/d7/7b1a9b9c2321cb0dcd093d6dca6a038c5bef27784fb5a58d2798a56459cf/av-12.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:18e915ca9001f9491cb4091fe6ca0744a48da20412be44f71bbfc641efbf518f", size = 25757707 }, +] + [[package]] name = "azure-ai-inference" version = "1.0.0b7" @@ -414,30 +490,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.36.1" +version = "1.35.96" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "jmespath", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "s3transfer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/04/0c6cea060653eee75f4348152dfc0aa0b241f7d1f99a530079ee44d61e4b/boto3-1.36.1.tar.gz", hash = "sha256:258ab77225a81d3cf3029c9afe9920cd9dec317689dfadec6f6f0a23130bb60a", size = 110959 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6c/ea481adf32791472885664224ac8e5269a4429db2e510d9fa56c493407e9/boto3-1.35.96.tar.gz", hash = "sha256:bace02ef2181d176cedc1f8f90c95c301bb7c555db124cf80bc193cbb52a7c64", size = 110999 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/ed/464e1df3901fbfedd5a0786e551240216f0c867440fa6156595178227b3f/boto3-1.36.1-py3-none-any.whl", hash = "sha256:eb21380d73fec6645439c0d802210f72a0cdb3295b02953f246ff53f512faa8f", size = 139163 }, + { url = "https://files.pythonhosted.org/packages/17/07/a1da47e567f7550783a6def2b1840d1b69c1f0cd4933e6f1c5942ff4a6c6/boto3-1.35.96-py3-none-any.whl", hash = "sha256:e6acb2380791b13d8fd55062d9bbc6e27c3ddb3e73cff71c4ca02e6743780c67", size = 139181 }, ] [[package]] name = "botocore" -version = "1.36.1" +version = "1.35.96" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/aa/556720b3ee9629b7c4366b5a0d9797a84e83a97f78435904cbb9bdc41939/botocore-1.36.1.tar.gz", hash = "sha256:f789a6f272b5b3d8f8756495019785e33868e5e00dd9662a3ee7959ac939bb12", size = 13498150 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/b2/9b2558e3f0094eb4829338bca777fc0747ad69fa8fe0b5f692d7e4e86bea/botocore-1.35.96.tar.gz", hash = "sha256:385fd406ed14bdd624e082d3e15dd6575d490d5d7374fb02f0a798c3ca9ea802", size = 13488154 } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/bb/5431f12e2dadd881fd023fb57e7e3ab82f7b697c38dc837fc8d70cca51bd/botocore-1.36.1-py3-none-any.whl", hash = "sha256:dec513b4eb8a847d79bbefdcdd07040ed9d44c20b0001136f0890a03d595705a", size = 13297686 }, + { url = "https://files.pythonhosted.org/packages/65/bc/9ba93a90b3f53afdd5d27c4a0b7bc19b5b9d6ad0e1489b4c5cd47ef6fbe4/botocore-1.35.96-py3-none-any.whl", hash = "sha256:b5f4cf11372aeccf87bb0b6148a020212c4c42fb5bcdebb6590bb10f6612b98e", size = 13289712 }, ] [[package]] @@ -646,7 +722,7 @@ wheels = [ [[package]] name = "chromadb" -version = "0.6.3" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bcrypt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -678,9 +754,9 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/cd/f0f2de3f466ff514fb6b58271c14f6d22198402bb5b71b8d890231265946/chromadb-0.6.3.tar.gz", hash = "sha256:c8f34c0b704b9108b04491480a36d42e894a960429f87c6516027b5481d59ed3", size = 29297929 } +sdist = { url = "https://files.pythonhosted.org/packages/d1/c5/d2b4219fdee424e881608da681c3c63b73d68dc6667bd2df14a4d9bb308d/chromadb-0.6.2.tar.gz", hash = "sha256:e9e11f04d3850796711ee05dad4e918c75ec7b62ab9cbe7b4588b68a26aaea06", size = 19979649 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/8e/5c186c77bf749b6fe0528385e507e463f1667543328d76fd00a49e1a4e6a/chromadb-0.6.3-py3-none-any.whl", hash = "sha256:4851258489a3612b558488d98d09ae0fe0a28d5cad6bd1ba64b96fdc419dc0e5", size = 611129 }, + { url = "https://files.pythonhosted.org/packages/bb/1c/2b77093f4191ad2d1ab70b9215cb6bc9f43350aa3e9e54a44304c8379335/chromadb-0.6.2-py3-none-any.whl", hash = "sha256:77a5e07097e36cdd49d8d2925d0c4d28291cabc9677787423d2cc7c426e8895b", size = 606162 }, ] [[package]] @@ -874,27 +950,27 @@ wheels = [ [[package]] name = "debugpy" -version = "1.8.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/25/c74e337134edf55c4dfc9af579eccb45af2393c40960e2795a94351e8140/debugpy-1.8.12.tar.gz", hash = "sha256:646530b04f45c830ceae8e491ca1c9320a2d2f0efea3141487c82130aba70dce", size = 1641122 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/19/dd58334c0a1ec07babf80bf29fb8daf1a7ca4c1a3bbe61548e40616ac087/debugpy-1.8.12-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:a2ba7ffe58efeae5b8fad1165357edfe01464f9aef25e814e891ec690e7dd82a", size = 2076091 }, - { url = "https://files.pythonhosted.org/packages/4c/37/bde1737da15f9617d11ab7b8d5267165f1b7dae116b2585a6643e89e1fa2/debugpy-1.8.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbbd4149c4fc5e7d508ece083e78c17442ee13b0e69bfa6bd63003e486770f45", size = 3560717 }, - { url = "https://files.pythonhosted.org/packages/d9/ca/bc67f5a36a7de072908bc9e1156c0f0b272a9a2224cf21540ab1ffd71a1f/debugpy-1.8.12-cp310-cp310-win32.whl", hash = "sha256:b202f591204023b3ce62ff9a47baa555dc00bb092219abf5caf0e3718ac20e7c", size = 5180672 }, - { url = "https://files.pythonhosted.org/packages/c1/b9/e899c0a80dfa674dbc992f36f2b1453cd1ee879143cdb455bc04fce999da/debugpy-1.8.12-cp310-cp310-win_amd64.whl", hash = "sha256:9649eced17a98ce816756ce50433b2dd85dfa7bc92ceb60579d68c053f98dff9", size = 5212702 }, - { url = "https://files.pythonhosted.org/packages/af/9f/5b8af282253615296264d4ef62d14a8686f0dcdebb31a669374e22fff0a4/debugpy-1.8.12-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:36f4829839ef0afdfdd208bb54f4c3d0eea86106d719811681a8627ae2e53dd5", size = 2174643 }, - { url = "https://files.pythonhosted.org/packages/ef/31/f9274dcd3b0f9f7d1e60373c3fa4696a585c55acb30729d313bb9d3bcbd1/debugpy-1.8.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a28ed481d530e3138553be60991d2d61103ce6da254e51547b79549675f539b7", size = 3133457 }, - { url = "https://files.pythonhosted.org/packages/ab/ca/6ee59e9892e424477e0c76e3798046f1fd1288040b927319c7a7b0baa484/debugpy-1.8.12-cp311-cp311-win32.whl", hash = "sha256:4ad9a94d8f5c9b954e0e3b137cc64ef3f579d0df3c3698fe9c3734ee397e4abb", size = 5106220 }, - { url = "https://files.pythonhosted.org/packages/d5/1a/8ab508ab05ede8a4eae3b139bbc06ea3ca6234f9e8c02713a044f253be5e/debugpy-1.8.12-cp311-cp311-win_amd64.whl", hash = "sha256:4703575b78dd697b294f8c65588dc86874ed787b7348c65da70cfc885efdf1e1", size = 5130481 }, - { url = "https://files.pythonhosted.org/packages/ba/e6/0f876ecfe5831ebe4762b19214364753c8bc2b357d28c5d739a1e88325c7/debugpy-1.8.12-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:7e94b643b19e8feb5215fa508aee531387494bf668b2eca27fa769ea11d9f498", size = 2500846 }, - { url = "https://files.pythonhosted.org/packages/19/64/33f41653a701f3cd2cbff8b41ebaad59885b3428b5afd0d93d16012ecf17/debugpy-1.8.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:086b32e233e89a2740c1615c2f775c34ae951508b28b308681dbbb87bba97d06", size = 4222181 }, - { url = "https://files.pythonhosted.org/packages/32/a6/02646cfe50bfacc9b71321c47dc19a46e35f4e0aceea227b6d205e900e34/debugpy-1.8.12-cp312-cp312-win32.whl", hash = "sha256:2ae5df899732a6051b49ea2632a9ea67f929604fd2b036613a9f12bc3163b92d", size = 5227017 }, - { url = "https://files.pythonhosted.org/packages/da/a6/10056431b5c47103474312cf4a2ec1001f73e0b63b1216706d5fef2531eb/debugpy-1.8.12-cp312-cp312-win_amd64.whl", hash = "sha256:39dfbb6fa09f12fae32639e3286112fc35ae976114f1f3d37375f3130a820969", size = 5267555 }, - { url = "https://files.pythonhosted.org/packages/cf/4d/7c3896619a8791effd5d8c31f0834471fc8f8fb3047ec4f5fc69dd1393dd/debugpy-1.8.12-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:696d8ae4dff4cbd06bf6b10d671e088b66669f110c7c4e18a44c43cf75ce966f", size = 2485246 }, - { url = "https://files.pythonhosted.org/packages/99/46/bc6dcfd7eb8cc969a5716d858e32485eb40c72c6a8dc88d1e3a4d5e95813/debugpy-1.8.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:898fba72b81a654e74412a67c7e0a81e89723cfe2a3ea6fcd3feaa3395138ca9", size = 4218616 }, - { url = "https://files.pythonhosted.org/packages/03/dd/d7fcdf0381a9b8094da1f6a1c9f19fed493a4f8576a2682349b3a8b20ec7/debugpy-1.8.12-cp313-cp313-win32.whl", hash = "sha256:22a11c493c70413a01ed03f01c3c3a2fc4478fc6ee186e340487b2edcd6f4180", size = 5226540 }, - { url = "https://files.pythonhosted.org/packages/25/bd/ecb98f5b5fc7ea0bfbb3c355bc1dd57c198a28780beadd1e19915bf7b4d9/debugpy-1.8.12-cp313-cp313-win_amd64.whl", hash = "sha256:fdb3c6d342825ea10b90e43d7f20f01535a72b3a1997850c0c3cefa5c27a4a2c", size = 5267134 }, - { url = "https://files.pythonhosted.org/packages/38/c4/5120ad36405c3008f451f94b8f92ef1805b1e516f6ff870f331ccb3c4cc0/debugpy-1.8.12-py2.py3-none-any.whl", hash = "sha256:274b6a2040349b5c9864e475284bce5bb062e63dce368a394b8cc865ae3b00c6", size = 5229490 }, +version = "1.8.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/e7/666f4c9b0e24796af50aadc28d36d21c2e01e831a934535f956e09b3650c/debugpy-1.8.11.tar.gz", hash = "sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57", size = 1640124 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/e6/4cf7422eaa591b4c7d6a9fde224095dac25283fdd99d90164f28714242b0/debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd", size = 2075100 }, + { url = "https://files.pythonhosted.org/packages/83/3a/e163de1df5995d95760a4d748b02fbefb1c1bf19e915b664017c40435dbf/debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f", size = 3559724 }, + { url = "https://files.pythonhosted.org/packages/27/6c/327e19fd1bf428a1efe1a6f97b306689c54c2cebcf871b66674ead718756/debugpy-1.8.11-cp310-cp310-win32.whl", hash = "sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737", size = 5178068 }, + { url = "https://files.pythonhosted.org/packages/49/80/359ff8aa388f0bd4a48f0fa9ce3606396d576657ac149c6fba3cc7de8adb/debugpy-1.8.11-cp310-cp310-win_amd64.whl", hash = "sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1", size = 5210109 }, + { url = "https://files.pythonhosted.org/packages/7c/58/8e3f7ec86c1b7985a232667b5df8f3b1b1c8401028d8f4d75e025c9556cd/debugpy-1.8.11-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296", size = 2173656 }, + { url = "https://files.pythonhosted.org/packages/d2/03/95738a68ade2358e5a4d63a2fd8e7ed9ad911001cfabbbb33a7f81343945/debugpy-1.8.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1", size = 3132464 }, + { url = "https://files.pythonhosted.org/packages/ca/f4/18204891ab67300950615a6ad09b9de236203a9138f52b3b596fa17628ca/debugpy-1.8.11-cp311-cp311-win32.whl", hash = "sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9", size = 5103637 }, + { url = "https://files.pythonhosted.org/packages/3b/90/3775e301cfa573b51eb8a108285681f43f5441dc4c3916feed9f386ef861/debugpy-1.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e", size = 5127862 }, + { url = "https://files.pythonhosted.org/packages/c6/ae/2cf26f3111e9d94384d9c01e9d6170188b0aeda15b60a4ac6457f7c8a26f/debugpy-1.8.11-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308", size = 2498756 }, + { url = "https://files.pythonhosted.org/packages/b0/16/ec551789d547541a46831a19aa15c147741133da188e7e6acf77510545a7/debugpy-1.8.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768", size = 4219136 }, + { url = "https://files.pythonhosted.org/packages/72/6f/b2b3ce673c55f882d27a6eb04a5f0c68bcad6b742ac08a86d8392ae58030/debugpy-1.8.11-cp312-cp312-win32.whl", hash = "sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b", size = 5224440 }, + { url = "https://files.pythonhosted.org/packages/77/09/b1f05be802c1caef5b3efc042fc6a7cadd13d8118b072afd04a9b9e91e06/debugpy-1.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1", size = 5264578 }, + { url = "https://files.pythonhosted.org/packages/2e/66/931dc2479aa8fbf362dc6dcee707d895a84b0b2d7b64020135f20b8db1ed/debugpy-1.8.11-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3", size = 2483651 }, + { url = "https://files.pythonhosted.org/packages/10/07/6c171d0fe6b8d237e35598b742f20ba062511b3a4631938cc78eefbbf847/debugpy-1.8.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e", size = 4213770 }, + { url = "https://files.pythonhosted.org/packages/89/f1/0711da6ac250d4fe3bf7b3e9b14b4a86e82a98b7825075c07e19bab8da3d/debugpy-1.8.11-cp313-cp313-win32.whl", hash = "sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28", size = 5223911 }, + { url = "https://files.pythonhosted.org/packages/56/98/5e27fa39050749ed460025bcd0034a0a5e78a580a14079b164cc3abdeb98/debugpy-1.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1", size = 5264166 }, + { url = "https://files.pythonhosted.org/packages/77/0a/d29a5aacf47b4383ed569b8478c02d59ee3a01ad91224d2cff8562410e43/debugpy-1.8.11-py2.py3-none-any.whl", hash = "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920", size = 5226874 }, ] [[package]] @@ -1207,7 +1283,7 @@ grpc = [ [[package]] name = "google-api-python-client" -version = "2.159.0" +version = "2.158.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1216,9 +1292,9 @@ dependencies = [ { name = "httplib2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uritemplate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/12b58cca5a93d63fd6a7abed570423bdf2db4349eb9361ac5214d42ed7d6/google_api_python_client-2.159.0.tar.gz", hash = "sha256:55197f430f25c907394b44fa078545ffef89d33fd4dca501b7db9f0d8e224bd6", size = 12302576 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/d7/ed1626cb92ffe68a17c5e5b3f0331e20f3ff6ef24deedffd4a70db49e0b0/google_api_python_client-2.158.0.tar.gz", hash = "sha256:b6664597a9955e04977a62752e33fe44cb35c580e190c1cb08a041893172bd67", size = 12277176 } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/ab/d0671375afe79e6e8c51736e115a69bb6b4bcdc80cd5c01bf667486cd24c/google_api_python_client-2.159.0-py2.py3-none-any.whl", hash = "sha256:baef0bb631a60a0bd7c0bf12a5499e3a40cd4388484de7ee55c1950bf820a0cf", size = 12814228 }, + { url = "https://files.pythonhosted.org/packages/b0/91/02f0f4938957892224a1fd8a9c031175a28036d4c8ee538972922a342efd/google_api_python_client-2.158.0-py2.py3-none-any.whl", hash = "sha256:36f8c8d2e79e50f76790ca5946d2f3f8333e210dc8539a6c88e0742416474ad2", size = 12789578 }, ] [[package]] @@ -1250,7 +1326,7 @@ wheels = [ [[package]] name = "google-cloud-aiplatform" -version = "1.77.0" +version = "1.76.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1266,9 +1342,9 @@ dependencies = [ { name = "shapely", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/45/7ffd099ff7554d9f4f3665611afb44d3ea59f8a3dd071e4284381d0ac3c1/google_cloud_aiplatform-1.77.0.tar.gz", hash = "sha256:1e5b77fe6c7f276d7aae65bcf08a273122a71f6c4af1f43cf45821f603a74080", size = 8287282 } +sdist = { url = "https://files.pythonhosted.org/packages/49/61/c3f206a0de113cdba09998b78434c63fcabd8e89607b8fb83cd21a3dffcf/google_cloud_aiplatform-1.76.0.tar.gz", hash = "sha256:910fb7fb6ef7ec73a48523872d669370755f59ac6d764dc8bf2fc91e7c0b2fca", size = 8202679 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/b6/f7a3c8bdb08a3636d216c49768eff3369b5475edd71f6dbe590a942252b9/google_cloud_aiplatform-1.77.0-py2.py3-none-any.whl", hash = "sha256:e9dd1bcb1b9a85eddd452916cd6ad1d9ce2d487772a9e45b1814aa0ac5633689", size = 6939280 }, + { url = "https://files.pythonhosted.org/packages/46/01/651752ae55160f5670c33d8a61de08798212472d11db124cb175ff54bcaa/google_cloud_aiplatform-1.76.0-py2.py3-none-any.whl", hash = "sha256:0b0348525b9528db7b69538ff6e86289ea2ce0d80f3784a42865fc994fe10dd1", size = 6867667 }, ] [[package]] @@ -1361,6 +1437,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/fb/54deefe679b7d1c1cc81d83396fcf28ad1a66d213bddeb275a8d28665918/google_crc32c-1.6.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d", size = 27866 }, ] +[[package]] +name = "google-genai" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/fa/e8c81d37ffe7d8aa05573494735cdc432a97b77f641a08caa959de19523d/google_genai-0.4.0.tar.gz", hash = "sha256:d14ce2e941063092cfc98726aeabcae44f179456e3a4906ee5f28dc91b0663fb", size = 107625 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/ac/cf91960fc842f8c3387be8abeaa01deb0e6b20a72a028b70107f58e13150/google_genai-0.4.0-py3-none-any.whl", hash = "sha256:2cbfea3cb47d4ac54ee3d3f9ecd79ff72298cac13e150828afdc5ed62768ed00", size = 113562 }, +] + [[package]] name = "google-generativeai" version = "0.8.3" @@ -1786,6 +1878,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314 }, +] + [[package]] name = "importlib-metadata" version = "8.5.0" @@ -2240,7 +2341,7 @@ wheels = [ [[package]] name = "mistralai" -version = "1.3.1" +version = "1.2.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "eval-type-backport", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2250,9 +2351,9 @@ dependencies = [ { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-inspect", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/50/59669ee8d21fd27a4f887148b1efb19d9be5ed22ec19c8e6eb842407ac0f/mistralai-1.3.1.tar.gz", hash = "sha256:1c30385656393f993625943045ad20de2aff4c6ab30fc6e8c727d735c22b1c08", size = 133338 } +sdist = { url = "https://files.pythonhosted.org/packages/36/18/53e6bb5c573b130134808236d649748e0280152b4e0c8436f05ff83a86de/mistralai-1.2.6.tar.gz", hash = "sha256:87a2b6fec565e775b0a027474b02be0c219c0a6b787c193ea1c4d12bac08e52e", size = 133264 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/b4/a76b6942b78383d5499f776d880a166296542383f6f952feeef96d0ea692/mistralai-1.3.1-py3-none-any.whl", hash = "sha256:35e74feadf835b7d2145095114b9cf3ba86c4cf1044f28f49b02cd6ddd0a5733", size = 261271 }, + { url = "https://files.pythonhosted.org/packages/22/0e/e16e6fd06f5a6345a1fde3a75653769f46a04f92f10db3bb3028b88eba16/mistralai-1.2.6-py3-none-any.whl", hash = "sha256:d9db22ca3a0e029dc2bf8e9380390168440ae4c19d21d212f53ff0d0bd917447", size = 261307 }, ] [[package]] @@ -2812,15 +2913,15 @@ wheels = [ [[package]] name = "ollama" -version = "0.4.6" +version = "0.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/d6/2bd7cffbabc81282576051ebf66ebfaa97e6b541975cd4e886bfd6c0f83d/ollama-0.4.6.tar.gz", hash = "sha256:b00717651c829f96094ed4231b9f0d87e33cc92dc235aca50aeb5a2a4e6e95b7", size = 12710 } +sdist = { url = "https://files.pythonhosted.org/packages/16/fd/a130173a62fd6dc7f7875919593b1e7a47bf3870a913c35d51ea013cfe41/ollama-0.4.5.tar.gz", hash = "sha256:e7fb71a99147046d028ab8b75e51e09437099aea6f8f9a0d91a71f787e97439e", size = 13104 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/60/ac0e47c4c400fbd1a72a3c6e4a76cf5ef859d60677e7c4b9f0203c5657d3/ollama-0.4.6-py3-none-any.whl", hash = "sha256:cbb4ebe009e10dd12bdd82508ab415fd131945e185753d728a7747c9ebe762e9", size = 13086 }, + { url = "https://files.pythonhosted.org/packages/93/71/44e508b6be7cc12efc498217bf74f443dbc1a31b145c87421d20fe61b70b/ollama-0.4.5-py3-none-any.whl", hash = "sha256:74936de89a41c87c9745f09f2e1db964b4783002188ac21241bfab747f46d925", size = 13205 }, ] [[package]] @@ -2886,7 +2987,7 @@ wheels = [ [[package]] name = "openai" -version = "1.59.7" +version = "1.59.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2898,9 +2999,14 @@ dependencies = [ { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/d5/25cf04789c7929b476c4d9ef711f8979091db63d30bfc093828fe4bf5c72/openai-1.59.7.tar.gz", hash = "sha256:043603def78c00befb857df9f0a16ee76a3af5984ba40cb7ee5e2f40db4646bf", size = 345007 } +sdist = { url = "https://files.pythonhosted.org/packages/2e/7a/07fbe7bdabffd0a5be1bfe5903a02c4fff232e9acbae894014752a8e4def/openai-1.59.6.tar.gz", hash = "sha256:c7670727c2f1e4473f62fea6fa51475c8bc098c9ffb47bfb9eef5be23c747934", size = 344915 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/47/7b92f1731c227f4139ef0025b5996062e44f9a749c54315c8bdb34bad5ec/openai-1.59.7-py3-none-any.whl", hash = "sha256:cfa806556226fa96df7380ab2e29814181d56fea44738c2b0e581b462c268692", size = 454844 }, + { url = "https://files.pythonhosted.org/packages/70/45/6de8e5fd670c804b29c777e4716f1916741c71604d5c7d952eee8432f7d3/openai-1.59.6-py3-none-any.whl", hash = "sha256:b28ed44eee3d5ebe1a3ea045ee1b4b50fea36ecd50741aaa5ce5a5559c900cb6", size = 454817 }, +] + +[package.optional-dependencies] +realtime = [ + { name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [[package]] @@ -3396,7 +3502,7 @@ wheels = [ [[package]] name = "posthog" -version = "3.8.3" +version = "3.7.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3405,9 +3511,9 @@ dependencies = [ { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/5a/057ebd6b279940e2cf2cbe8b10a4b34bc832f6f82b10649dcd12210219e9/posthog-3.8.3.tar.gz", hash = "sha256:263df03ea312d4b47a3d5ea393fdb22ff2ed78140d5ce9af9dd0618ae245a44b", size = 56864 } +sdist = { url = "https://files.pythonhosted.org/packages/58/e9/1cd7492bb58dd255129467e1221e2d6f51aa0c6f3c781ac9ac29cc8a2859/posthog-3.7.5.tar.gz", hash = "sha256:8ba40ab623da35db72715fc87fe7dccb7fc272ced92581fe31db2d4dbe7ad761", size = 50269 } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/3a/ff36f067367de4477d114ab04f42d5830849bad1b0949eb70c9858cdb7e2/posthog-3.8.3-py2.py3-none-any.whl", hash = "sha256:7215c4d7649b0c87905b42f460403311564996d776ab48d39852f46539a50f22", size = 64665 }, + { url = "https://files.pythonhosted.org/packages/76/bd/2d550ac79443cdbb6a5a4193c37820f04df0499e1ecbe8e41c5462cf0c2d/posthog-3.7.5-py2.py3-none-any.whl", hash = "sha256:022132c17069dde03c5c5904e2ae1b9bd68d5059cbc5a8dffc5c1537a1b71cb5", size = 54882 }, ] [[package]] @@ -3853,6 +3959,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, ] +[[package]] +name = "pyee" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/37/8fb6e653597b2b67ef552ed49b438d5398ba3b85a9453f8ada0fd77d455c/pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3", size = 30915 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/68/7e150cba9eeffdeb3c5cecdb6896d70c8edd46ce41c0491e12fb2b2256ff/pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef", size = 15527 }, +] + [[package]] name = "pygments" version = "2.19.1" @@ -3876,6 +3994,29 @@ crypto = [ { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] +[[package]] +name = "pylibsrtp" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/ae/c95199144eed954976223bdce3f94564eb6c43567111aff8048a26a429bd/pylibsrtp-0.10.0.tar.gz", hash = "sha256:d8001912d7f51bd05b4ea3551747930631777fd37892cf3bfe0e541a742e699f", size = 10557 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/d2/ffc24f80e83a54d9b309cdae6b31cf9294b4f3a85ab107827fd272d1e687/pylibsrtp-0.10.0-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6a1121ceea3339e0a84842a4a9da0fcf57cc8f99eb60dbf31a46d978b4170e7c", size = 1704188 }, + { url = "https://files.pythonhosted.org/packages/66/3e/db86a09a5cb290a274f76ce25f4fae3a7e3c4a4dbc64baf7e2aaa57a32bb/pylibsrtp-0.10.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca1994e73c6857b0a695fdde94cc5ac846c1b0d5d8766255a1dc2db40857f667", size = 2028580 }, + { url = "https://files.pythonhosted.org/packages/21/ab/9b2b5ad2ceaa1660de16e0a2e3c54a2043a9c4a3eef7718930c78dc84e77/pylibsrtp-0.10.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb7640b524544603d07bd4373b04c9582c8cfe41d9789d3f492081f053bed9c1", size = 2484470 }, + { url = "https://files.pythonhosted.org/packages/ab/e6/b0a30e79aa2312834b33f5e9c0ad459fc94e195c610634ee9665fafb1fc8/pylibsrtp-0.10.0-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f13aa945e1dcf8c138bf3d4a6e34056c4c2f69bf9934bc53b320ef14c7317ccc", size = 2078367 }, + { url = "https://files.pythonhosted.org/packages/16/78/9ea0c88490ad4fe9683ddf3bbee702c7a2331e83a333bb3aa52e8d7d909b/pylibsrtp-0.10.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b2ef1c32d1145239dd0fe7b7fbe083334d345df6b4597fc66faf914a32682d9", size = 2134898 }, + { url = "https://files.pythonhosted.org/packages/00/f6/c76fa5401f9d95c14db70de0cf4fad922ad61686843bc3e7411178a64bc8/pylibsrtp-0.10.0-cp38-abi3-win32.whl", hash = "sha256:8c6fe2576b2ab13942b47db6c2ffe71f5eb1edc1dc3bdd7283169fecd5249e74", size = 1130881 }, + { url = "https://files.pythonhosted.org/packages/4c/31/85a58625edc0b6967fe0904c9d89d019bcece3f3e3bf775b9151a8cf9d0d/pylibsrtp-0.10.0-cp38-abi3-win_amd64.whl", hash = "sha256:cd965d4b0e9a77b362526cab119f4d9ce39b83f1f20f46c6af8e694b86fa19a7", size = 1448840 }, + { url = "https://files.pythonhosted.org/packages/66/b5/30b57cac6adf93dfee20cceba6cd91e216c81b723df2bc9dcfe781456263/pylibsrtp-0.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:582e9771be7ffd060faea215cb4248afdad1356da473df1b8f35c7e382ca3871", size = 1699981 }, + { url = "https://files.pythonhosted.org/packages/16/e8/3846ac56ae4a2de91e9b3e67dff5363b2b07148616d283416fd8dd8c6ca6/pylibsrtp-0.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70111eeb87e5d3ffb9623e1ea036329dc81fed1282aa93c1f32377862ca0a0d8", size = 2441012 }, + { url = "https://files.pythonhosted.org/packages/b1/9f/c611fc47ef5d84dfffca0292bcfb2d78ee5fc1a98d50cf22dfcda3eee171/pylibsrtp-0.10.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eda06947ab42fd3737f01a7b98537a5d5908434d37c70488d10e7bd2ff0d520c", size = 2019497 }, + { url = "https://files.pythonhosted.org/packages/d8/38/90c897fc2f2929290ada1032fa3e0bd39eca9190503250f6724a7bc22b5b/pylibsrtp-0.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:511158499309c3f7e97e1ebeffbf3dd939e641ea553de43cfc02d3576aad5c15", size = 2074919 }, + { url = "https://files.pythonhosted.org/packages/2c/46/e92f8a8d7cb5c1d68ec85254a8535aad922efa15646c7ba0c7746b42c4ea/pylibsrtp-0.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4033481f332331bf14b9705dca69efd09d3809ba4a2ff69914c53dddf39c20c1", size = 1446426 }, +] + [[package]] name = "pymeta3" version = "0.5.1" @@ -3947,6 +4088,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/36/88d8438699ba09b714dece00a4a7462330c1d316f5eaa28db450572236f6/pymongo-4.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:169b85728cc17800344ba17d736375f400ef47c9fbb4c42910c4b3e7c0247382", size = 975113 }, ] +[[package]] +name = "pyopenssl" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/26/e25b4a374b4639e0c235527bbe31c0524f26eda701d79456a7e1877f4cc5/pyopenssl-25.0.0.tar.gz", hash = "sha256:cd2cef799efa3936bb08e8ccb9433a575722b9dd986023f1cabc4ae64e9dac16", size = 179573 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/d7/eb76863d2060dcbe7c7e6cccfd95ac02ea0b9acc37745a0d99ff6457aefb/pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90", size = 56453 }, +] + [[package]] name = "pyparsing" version = "3.2.1" @@ -4563,27 +4717,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 }, - { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 }, - { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 }, - { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 }, - { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 }, - { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 }, - { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 }, - { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 }, - { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 }, - { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 }, - { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 }, - { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 }, - { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 }, - { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 }, - { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 }, - { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 }, - { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 }, +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/48/385f276f41e89623a5ea8e4eb9c619a44fdfc2a64849916b3584eca6cb9f/ruff-0.9.0.tar.gz", hash = "sha256:143f68fa5560ecf10fc49878b73cee3eab98b777fcf43b0e62d43d42f5ef9d8b", size = 3489167 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/01/e0885e5519212efc7ab9d868bc39cb9781931c4c6f9b17becafa81193ec4/ruff-0.9.0-py3-none-linux_armv6l.whl", hash = "sha256:949b3513f931741e006cf267bf89611edff04e1f012013424022add3ce78f319", size = 10647069 }, + { url = "https://files.pythonhosted.org/packages/dd/69/510a9a5781dcf84c2ad513c2003936fefc802f39c745d5f2355d77fa45fd/ruff-0.9.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:99fbcb8c7fe94ae1e462ab2a1ef17cb20b25fb6438b9f198b1bcf5207a0a7916", size = 10401936 }, + { url = "https://files.pythonhosted.org/packages/07/9f/37fb86bfdf28c4cbfe94cbcc01fb9ab0cb8128548f243f34d5298b212562/ruff-0.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b022afd8eb0fcfce1e0adec84322abf4d6ce3cd285b3b99c4f17aae7decf749", size = 10010347 }, + { url = "https://files.pythonhosted.org/packages/30/0d/b95121f53c7f7bfb7ba427a35d25f983ed3b476620c5cd69f45caa5b294e/ruff-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:336567ce92c9ca8ec62780d07b5fa11fbc881dc7bb40958f93a7d621e7ab4589", size = 10882152 }, + { url = "https://files.pythonhosted.org/packages/d4/0b/a955cb6b19eb900c4c594707ab72132ce2d5cd8b5565137fb8fed21b8f08/ruff-0.9.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d338336c44bda602dc8e8766836ac0441e5b0dfeac3af1bd311a97ebaf087a75", size = 10405502 }, + { url = "https://files.pythonhosted.org/packages/1e/fa/9a6c70af74f20edd2519b89eb3322f4bfa399315cf306383443700f2d6b6/ruff-0.9.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9b3ececf523d733e90b540e7afcc0494189e8999847f8855747acd5a9a8c45f", size = 11465069 }, + { url = "https://files.pythonhosted.org/packages/ee/8b/7effac8915470da496be009fe861060baff2692f92801976b2c01cdc8c54/ruff-0.9.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a11c0872a31232e473e2e0e2107f3d294dbadd2f83fb281c3eb1c22a24866924", size = 12176850 }, + { url = "https://files.pythonhosted.org/packages/bd/ed/626179786889eca47b1e821c1582622ac0c1c8f01d60ac974f8b96867a57/ruff-0.9.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5fd06220c17a9cc0dc7fc6552f2ac4db74e8e8bff9c401d160ac59d00566f54", size = 11700963 }, + { url = "https://files.pythonhosted.org/packages/75/79/094c34ddec47fd3c61a0bc5e83ca164344c592949cff91f05961fd40922e/ruff-0.9.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0457e775c74bf3976243f910805242b7dcd389e1d440deccbd1194ca17a5728c", size = 13096560 }, + { url = "https://files.pythonhosted.org/packages/e7/23/ec85dca0dcb329835197401734501bfa1d39e72343df64628c67b72bcbf5/ruff-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05415599bbcb318f730ea1b46a39e4fbf71f6a63fdbfa1dda92efb55f19d7ecf", size = 11278658 }, + { url = "https://files.pythonhosted.org/packages/6c/17/1b3ea5f06578ea1daa08ac35f9de099d1827eea6e116a8cabbf11235c925/ruff-0.9.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fbf9864b009e43cfc1c8bed1a6a4c529156913105780af4141ca4342148517f5", size = 10879847 }, + { url = "https://files.pythonhosted.org/packages/a6/e5/00bc97d6f419da03c0d898e95cca77311494e7274dc7cc17d94976e32e52/ruff-0.9.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:37b3da222b12e2bb2ce628e02586ab4846b1ed7f31f42a5a0683b213453b2d49", size = 10494220 }, + { url = "https://files.pythonhosted.org/packages/cc/70/d0a23d94f3e40b7ffac0e5506f33bb504672569173781a6c7cab0db6a4ba/ruff-0.9.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:733c0fcf2eb0c90055100b4ed1af9c9d87305b901a8feb6a0451fa53ed88199d", size = 11004182 }, + { url = "https://files.pythonhosted.org/packages/20/8e/367cf8e401890f823d0e4eb33635d0113719d5660b6522b7295376dd95fd/ruff-0.9.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8221a454bfe5ccdf8017512fd6bb60e6ec30f9ea252b8a80e5b73619f6c3cefd", size = 11345761 }, + { url = "https://files.pythonhosted.org/packages/fe/08/4b54e02da73060ebc29368ab15868613f7d2496bde3b01d284d5423646bc/ruff-0.9.0-py3-none-win32.whl", hash = "sha256:d345f2178afd192c7991ddee59155c58145e12ad81310b509bd2e25c5b0247b3", size = 8807005 }, + { url = "https://files.pythonhosted.org/packages/a1/a7/0b422971e897c51bf805f998d75bcfe5d4d858f5002203832875fc91b733/ruff-0.9.0-py3-none-win_amd64.whl", hash = "sha256:0cbc0905d94d21305872f7f8224e30f4bbcd532bc21b2225b2446d8fc7220d19", size = 9689974 }, + { url = "https://files.pythonhosted.org/packages/73/0e/c00f66731e514be3299801b1d9d54efae0abfe8f00a5c14155f2ab9e2920/ruff-0.9.0-py3-none-win_arm64.whl", hash = "sha256:7b1148771c6ca88f820d761350a053a5794bc58e0867739ea93eb5e41ad978cd", size = 9147729 }, ] [[package]] @@ -4660,52 +4814,52 @@ wheels = [ [[package]] name = "scipy" -version = "1.15.1" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/c6/8eb0654ba0c7d0bb1bf67bf8fbace101a8e4f250f7722371105e8b6f68fc/scipy-1.15.1.tar.gz", hash = "sha256:033a75ddad1463970c96a88063a1df87ccfddd526437136b6ee81ff0312ebdf6", size = 59407493 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/53/b204ce5a4433f1864001b9d16f103b9c25f5002a602ae83585d0ea5f9c4a/scipy-1.15.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:c64ded12dcab08afff9e805a67ff4480f5e69993310e093434b10e85dc9d43e1", size = 41414518 }, - { url = "https://files.pythonhosted.org/packages/c7/fc/54ffa7a8847f7f303197a6ba65a66104724beba2e38f328135a78f0dc480/scipy-1.15.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5b190b935e7db569960b48840e5bef71dc513314cc4e79a1b7d14664f57fd4ff", size = 32519265 }, - { url = "https://files.pythonhosted.org/packages/f1/77/a98b8ba03d6f371dc31a38719affd53426d4665729dcffbed4afe296784a/scipy-1.15.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:4b17d4220df99bacb63065c76b0d1126d82bbf00167d1730019d2a30d6ae01ea", size = 24792859 }, - { url = "https://files.pythonhosted.org/packages/a7/78/70bb9f0df7444b18b108580934bfef774822e28fd34a68e5c263c7d2828a/scipy-1.15.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:63b9b6cd0333d0eb1a49de6f834e8aeaefe438df8f6372352084535ad095219e", size = 27886506 }, - { url = "https://files.pythonhosted.org/packages/14/a7/f40f6033e06de4176ddd6cc8c3ae9f10a226c3bca5d6b4ab883bc9914a14/scipy-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f151e9fb60fbf8e52426132f473221a49362091ce7a5e72f8aa41f8e0da4f25", size = 38375041 }, - { url = "https://files.pythonhosted.org/packages/17/03/390a1c5c61fd76b0fa4b3c5aa3bdd7e60f6c46f712924f1a9df5705ec046/scipy-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e10b1dd56ce92fba3e786007322542361984f8463c6d37f6f25935a5a6ef52", size = 40597556 }, - { url = "https://files.pythonhosted.org/packages/4e/70/fa95b3ae026b97eeca58204a90868802e5155ac71b9d7bdee92b68115dd3/scipy-1.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5dff14e75cdbcf07cdaa1c7707db6017d130f0af9ac41f6ce443a93318d6c6e0", size = 42938505 }, - { url = "https://files.pythonhosted.org/packages/d6/07/427859116bdd71847c898180f01802691f203c3e2455a1eb496130ff07c5/scipy-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:f82fcf4e5b377f819542fbc8541f7b5fbcf1c0017d0df0bc22c781bf60abc4d8", size = 43909663 }, - { url = "https://files.pythonhosted.org/packages/8e/2e/7b71312da9c2dabff53e7c9a9d08231bc34d9d8fdabe88a6f1155b44591c/scipy-1.15.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:5bd8d27d44e2c13d0c1124e6a556454f52cd3f704742985f6b09e75e163d20d2", size = 41424362 }, - { url = "https://files.pythonhosted.org/packages/81/8c/ab85f1aa1cc200c796532a385b6ebf6a81089747adc1da7482a062acc46c/scipy-1.15.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:be3deeb32844c27599347faa077b359584ba96664c5c79d71a354b80a0ad0ce0", size = 32535910 }, - { url = "https://files.pythonhosted.org/packages/3b/9c/6f4b787058daa8d8da21ddff881b4320e28de4704a65ec147adb50cb2230/scipy-1.15.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5eb0ca35d4b08e95da99a9f9c400dc9f6c21c424298a0ba876fdc69c7afacedf", size = 24809398 }, - { url = "https://files.pythonhosted.org/packages/16/2b/949460a796df75fc7a1ee1becea202cf072edbe325ebe29f6d2029947aa7/scipy-1.15.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:74bb864ff7640dea310a1377d8567dc2cb7599c26a79ca852fc184cc851954ac", size = 27918045 }, - { url = "https://files.pythonhosted.org/packages/5f/36/67fe249dd7ccfcd2a38b25a640e3af7e59d9169c802478b6035ba91dfd6d/scipy-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:667f950bf8b7c3a23b4199db24cb9bf7512e27e86d0e3813f015b74ec2c6e3df", size = 38332074 }, - { url = "https://files.pythonhosted.org/packages/fc/da/452e1119e6f720df3feb588cce3c42c5e3d628d4bfd4aec097bd30b7de0c/scipy-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395be70220d1189756068b3173853029a013d8c8dd5fd3d1361d505b2aa58fa7", size = 40588469 }, - { url = "https://files.pythonhosted.org/packages/7f/71/5f94aceeac99a4941478af94fe9f459c6752d497035b6b0761a700f5f9ff/scipy-1.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce3a000cd28b4430426db2ca44d96636f701ed12e2b3ca1f2b1dd7abdd84b39a", size = 42965214 }, - { url = "https://files.pythonhosted.org/packages/af/25/caa430865749d504271757cafd24066d596217e83326155993980bc22f97/scipy-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fe1d95944f9cf6ba77aa28b82dd6bb2a5b52f2026beb39ecf05304b8392864b", size = 43896034 }, - { url = "https://files.pythonhosted.org/packages/d8/6e/a9c42d0d39e09ed7fd203d0ac17adfea759cba61ab457671fe66e523dbec/scipy-1.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c09aa9d90f3500ea4c9b393ee96f96b0ccb27f2f350d09a47f533293c78ea776", size = 41478318 }, - { url = "https://files.pythonhosted.org/packages/04/ee/e3e535c81828618878a7433992fecc92fa4df79393f31a8fea1d05615091/scipy-1.15.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0ac102ce99934b162914b1e4a6b94ca7da0f4058b6d6fd65b0cef330c0f3346f", size = 32596696 }, - { url = "https://files.pythonhosted.org/packages/c4/5e/b1b0124be8e76f87115f16b8915003eec4b7060298117715baf13f51942c/scipy-1.15.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:09c52320c42d7f5c7748b69e9f0389266fd4f82cf34c38485c14ee976cb8cb04", size = 24870366 }, - { url = "https://files.pythonhosted.org/packages/14/36/c00cb73eefda85946172c27913ab995c6ad4eee00fa4f007572e8c50cd51/scipy-1.15.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:cdde8414154054763b42b74fe8ce89d7f3d17a7ac5dd77204f0e142cdc9239e9", size = 28007461 }, - { url = "https://files.pythonhosted.org/packages/68/94/aff5c51b3799349a9d1e67a056772a0f8a47db371e83b498d43467806557/scipy-1.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c9d8fc81d6a3b6844235e6fd175ee1d4c060163905a2becce8e74cb0d7554ce", size = 38068174 }, - { url = "https://files.pythonhosted.org/packages/b0/3c/0de11ca154e24a57b579fb648151d901326d3102115bc4f9a7a86526ce54/scipy-1.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb57b30f0017d4afa5fe5f5b150b8f807618819287c21cbe51130de7ccdaed2", size = 40249869 }, - { url = "https://files.pythonhosted.org/packages/15/09/472e8d0a6b33199d1bb95e49bedcabc0976c3724edd9b0ef7602ccacf41e/scipy-1.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491d57fe89927fa1aafbe260f4cfa5ffa20ab9f1435025045a5315006a91b8f5", size = 42629068 }, - { url = "https://files.pythonhosted.org/packages/ff/ba/31c7a8131152822b3a2cdeba76398ffb404d81d640de98287d236da90c49/scipy-1.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:900f3fa3db87257510f011c292a5779eb627043dd89731b9c461cd16ef76ab3d", size = 43621992 }, - { url = "https://files.pythonhosted.org/packages/2b/bf/dd68965a4c5138a630eeed0baec9ae96e5d598887835bdde96cdd2fe4780/scipy-1.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:100193bb72fbff37dbd0bf14322314fc7cbe08b7ff3137f11a34d06dc0ee6b85", size = 41441136 }, - { url = "https://files.pythonhosted.org/packages/ef/5e/4928581312922d7e4d416d74c416a660addec4dd5ea185401df2269ba5a0/scipy-1.15.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:2114a08daec64980e4b4cbdf5bee90935af66d750146b1d2feb0d3ac30613692", size = 32533699 }, - { url = "https://files.pythonhosted.org/packages/32/90/03f99c43041852837686898c66767787cd41c5843d7a1509c39ffef683e9/scipy-1.15.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6b3e71893c6687fc5e29208d518900c24ea372a862854c9888368c0b267387ab", size = 24807289 }, - { url = "https://files.pythonhosted.org/packages/9d/52/bfe82b42ae112eaba1af2f3e556275b8727d55ac6e4932e7aef337a9d9d4/scipy-1.15.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:837299eec3d19b7e042923448d17d95a86e43941104d33f00da7e31a0f715d3c", size = 27929844 }, - { url = "https://files.pythonhosted.org/packages/f6/77/54ff610bad600462c313326acdb035783accc6a3d5f566d22757ad297564/scipy-1.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82add84e8a9fb12af5c2c1a3a3f1cb51849d27a580cb9e6bd66226195142be6e", size = 38031272 }, - { url = "https://files.pythonhosted.org/packages/f1/26/98585cbf04c7cf503d7eb0a1966df8a268154b5d923c5fe0c1ed13154c49/scipy-1.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070d10654f0cb6abd295bc96c12656f948e623ec5f9a4eab0ddb1466c000716e", size = 40210217 }, - { url = "https://files.pythonhosted.org/packages/fd/3f/3d2285eb6fece8bc5dbb2f9f94d61157d61d155e854fd5fea825b8218f12/scipy-1.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55cc79ce4085c702ac31e49b1e69b27ef41111f22beafb9b49fea67142b696c4", size = 42587785 }, - { url = "https://files.pythonhosted.org/packages/48/7d/5b5251984bf0160d6533695a74a5fddb1fa36edd6f26ffa8c871fbd4782a/scipy-1.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:c352c1b6d7cac452534517e022f8f7b8d139cd9f27e6fbd9f3cbd0bfd39f5bef", size = 43640439 }, - { url = "https://files.pythonhosted.org/packages/e7/b8/0e092f592d280496de52e152582030f8a270b194f87f890e1a97c5599b81/scipy-1.15.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0458839c9f873062db69a03de9a9765ae2e694352c76a16be44f93ea45c28d2b", size = 41619862 }, - { url = "https://files.pythonhosted.org/packages/f6/19/0b6e1173aba4db9e0b7aa27fe45019857fb90d6904038b83927cbe0a6c1d/scipy-1.15.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:af0b61c1de46d0565b4b39c6417373304c1d4f5220004058bdad3061c9fa8a95", size = 32610387 }, - { url = "https://files.pythonhosted.org/packages/e7/02/754aae3bd1fa0f2479ade3cfdf1732ecd6b05853f63eee6066a32684563a/scipy-1.15.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:71ba9a76c2390eca6e359be81a3e879614af3a71dfdabb96d1d7ab33da6f2364", size = 24883814 }, - { url = "https://files.pythonhosted.org/packages/1f/ac/d7906201604a2ea3b143bb0de51b3966f66441ba50b7dc182c4505b3edf9/scipy-1.15.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14eaa373c89eaf553be73c3affb11ec6c37493b7eaaf31cf9ac5dffae700c2e0", size = 27944865 }, - { url = "https://files.pythonhosted.org/packages/84/9d/8f539002b5e203723af6a6f513a45e0a7671e9dabeedb08f417ac17e4edc/scipy-1.15.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f735bc41bd1c792c96bc426dece66c8723283695f02df61dcc4d0a707a42fc54", size = 39883261 }, - { url = "https://files.pythonhosted.org/packages/97/c0/62fd3bab828bcccc9b864c5997645a3b86372a35941cdaf677565c25c98d/scipy-1.15.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2722a021a7929d21168830790202a75dbb20b468a8133c74a2c0230c72626b6c", size = 42093299 }, - { url = "https://files.pythonhosted.org/packages/e4/1f/5d46a8d94e9f6d2c913cbb109e57e7eed914de38ea99e2c4d69a9fc93140/scipy-1.15.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc7136626261ac1ed988dca56cfc4ab5180f75e0ee52e58f1e6aa74b5f3eacd5", size = 43181730 }, +sdist = { url = "https://files.pythonhosted.org/packages/d9/7b/2b8ac283cf32465ed08bc20a83d559fe7b174a484781702ba8accea001d6/scipy-1.15.0.tar.gz", hash = "sha256:300742e2cc94e36a2880ebe464a1c8b4352a7b0f3e36ec3d2ac006cdbe0219ac", size = 59407226 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/6a/14ce8d4452acdced1b69ea32b0d304b04b00376deb4f1eb65f946aee41af/scipy-1.15.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:aeac60d3562a7bf2f35549bdfdb6b1751c50590f55ce7322b4b2fc821dc27fca", size = 41413763 }, + { url = "https://files.pythonhosted.org/packages/45/12/570ba186d0ae1d528f8f0524b88fb9a263653ce575ac085edd9c1ef29e9c/scipy-1.15.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5abbdc6ede5c5fed7910cf406a948e2c0869231c0db091593a6b2fa78be77e5d", size = 32518980 }, + { url = "https://files.pythonhosted.org/packages/51/5a/b6ac5aa213cfa196d15db5ee159010aa9b94d0bc2bfa917fb99297701628/scipy-1.15.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:eb1533c59f0ec6c55871206f15a5c72d1fae7ad3c0a8ca33ca88f7c309bbbf8c", size = 24792491 }, + { url = "https://files.pythonhosted.org/packages/35/1f/6af575b77b2ee057551643de75a30252ce32098b2d9fd45bcf969a6fa35b/scipy-1.15.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:de112c2dae53107cfeaf65101419662ac0a54e9a088c17958b51c95dac5de56d", size = 27886039 }, + { url = "https://files.pythonhosted.org/packages/6a/7b/0c261d4857f459de6dffe11b3818583944f8d87716ce0b3b5f058aa34ff3/scipy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2240e1fd0782e62e1aacdc7234212ee271d810f67e9cd3b8d521003a82603ef8", size = 38374628 }, + { url = "https://files.pythonhosted.org/packages/99/17/ca390fbbfea5b34e3a00fc819fcb7c22e8b889360882820030b533d26c01/scipy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d35aef233b098e4de88b1eac29f0df378278e7e250a915766786b773309137c4", size = 40599127 }, + { url = "https://files.pythonhosted.org/packages/1d/65/95d93b1360f5defc1b6bf0963ac4e0d3413c95d8e8d6a1624a256506dfd3/scipy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b29e4fc02e155a5fd1165f1e6a73edfdd110470736b0f48bcbe48083f0eee37", size = 42937900 }, + { url = "https://files.pythonhosted.org/packages/51/8c/c2d371111961f737ae08881f654cf54eca796c42ec0429add2a06df97049/scipy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:0e5b34f8894f9904cc578008d1a9467829c1817e9f9cb45e6d6eeb61d2ab7731", size = 43907603 }, + { url = "https://files.pythonhosted.org/packages/b8/53/7f627c180cdaa211fa537650ca05912f58cb68fc33bb2f9af3d29169913e/scipy-1.15.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:46e91b5b16909ff79224b56e19cbad65ca500b3afda69225820aa3afbf9ec020", size = 41423594 }, + { url = "https://files.pythonhosted.org/packages/c9/ab/f848933c6f656f2c7af2d56d0be44511b730498538fe04db70eb03a6ad86/scipy-1.15.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:82bff2eb01ccf7cea8b6ee5274c2dbeadfdac97919da308ee6d8e5bcbe846443", size = 32535797 }, + { url = "https://files.pythonhosted.org/packages/41/93/266693c471ec1e2e7748c1ee5e867299f3d0ac42e0e63f52649430ec1976/scipy-1.15.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:9c8254fe21dd2c6c8f7757035ec0c31daecf3bb3cffd93bc1ca661b731d28136", size = 24809325 }, + { url = "https://files.pythonhosted.org/packages/f3/55/1acc49a48bc11fb95cf625c0763f2749f8710265d2fecbf6ed6dd618fc54/scipy-1.15.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:c9624eeae79b18cab1a31944b5ef87aa14b125d6ab69b71db22f0dbd962caf1e", size = 27917711 }, + { url = "https://files.pythonhosted.org/packages/e2/f5/15f62812b36f2f94b9d1ca31d3d2bbabfb6979e48a0866041bee7031c461/scipy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d13bbc0658c11f3d19df4138336e4bce2c4fbd78c2755be4bf7b8e235481557f", size = 38331850 }, + { url = "https://files.pythonhosted.org/packages/ad/21/6dc57f6f6c8014dc6d07111e4976422580789fa96c4d7ddf63614939cb6c/scipy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdca4c7bb8dc41307e5f39e9e5d19c707d8e20a29845e7533b3bb20a9d4ccba0", size = 40587953 }, + { url = "https://files.pythonhosted.org/packages/da/dd/26db78c2054f8d81b28ae4688da7930ea3c33e5d1885928aadefeec979f9/scipy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f376d7c767731477bac25a85d0118efdc94a572c6b60decb1ee48bf2391a73b", size = 42963920 }, + { url = "https://files.pythonhosted.org/packages/82/89/eb4aaf929be0e2c03bb5e40ed61427aab9c8ba6c0764aebf82d7302bb3d3/scipy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:61513b989ee8d5218fbeb178b2d51534ecaddba050db949ae99eeb3d12f6825d", size = 43894857 }, + { url = "https://files.pythonhosted.org/packages/35/70/fffb90a725dec6056c9059073856fd99de22a253459a874a63b8b8a012db/scipy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5beb0a2200372b7416ec73fdae94fe81a6e85e44eb49c35a11ac356d2b8eccc6", size = 41475240 }, + { url = "https://files.pythonhosted.org/packages/63/ca/6b838a2e5e6718d879e8522d1155a068c2a769be04f7da8c5179ead32a7b/scipy-1.15.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fde0f3104dfa1dfbc1f230f65506532d0558d43188789eaf68f97e106249a913", size = 32595923 }, + { url = "https://files.pythonhosted.org/packages/b1/07/4e69f6f7185915d77719bf226c1d554a4bb99f27cb92161fdd57b1434343/scipy-1.15.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:35c68f7044b4e7ad73a3e68e513dda946989e523df9b062bd3cf401a1a882192", size = 24869617 }, + { url = "https://files.pythonhosted.org/packages/30/22/e3dadf189dcab215be461efe0fd9d288f4c2d99783c4aec2ce80837800b7/scipy-1.15.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:52475011be29dfcbecc3dfe3060e471ac5155d72e9233e8d5616b84e2b542054", size = 28007674 }, + { url = "https://files.pythonhosted.org/packages/51/0f/71c9ee2acaac0660a79e36424d367ed5737e4ef27b885f96cd439f451467/scipy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5972e3f96f7dda4fd3bb85906a17338e65eaddfe47f750e240f22b331c08858e", size = 38066684 }, + { url = "https://files.pythonhosted.org/packages/fb/77/74a1ceecb205f5d46fe2cd10071383748ee8891a96b7824a372391a6291c/scipy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe00169cf875bed0b3c40e4da45b57037dc21d7c7bf0c85ed75f210c281488f1", size = 40250011 }, + { url = "https://files.pythonhosted.org/packages/8c/9f/f1544110a3d31183034e05422836505beb438aa56183f2ccef6dcd3b4e3f/scipy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:161f80a98047c219c257bf5ce1777c574bde36b9d962a46b20d0d7e531f86863", size = 42625471 }, + { url = "https://files.pythonhosted.org/packages/3f/39/a29b75f9c30084cbafd416bfa00933311a5b7a96be6e88750c98521d2ccb/scipy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:327163ad73e54541a675240708244644294cb0a65cca420c9c79baeb9648e479", size = 43622832 }, + { url = "https://files.pythonhosted.org/packages/4d/46/2fa07d5b53092b73c4bb416954d07d883b53be4a5bd6282c67e03c051225/scipy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0fcb16eb04d84670722ce8d93b05257df471704c913cb0ff9dc5a1c31d1e9422", size = 41438080 }, + { url = "https://files.pythonhosted.org/packages/55/05/77778b1127e170ffb484614691fdd8f9d2640dcf951d515f513debe5d0e0/scipy-1.15.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:767e8cf6562931f8312f4faa7ddea412cb783d8df49e62c44d00d89f41f9bbe8", size = 32532932 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/6de4970a2f524785d94a85f423a53b8c53d84917f2df702733ccdc9afd54/scipy-1.15.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:37ce9394cdcd7c5f437583fc6ef91bd290014993900643fdfc7af9b052d1613b", size = 24806488 }, + { url = "https://files.pythonhosted.org/packages/65/ef/b1c1e2499189bbea109a6b022a6147dd4552d72bed19289b4d4e411c4ce7/scipy-1.15.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6d26f17c64abd6c6c2dfb39920f61518cc9e213d034b45b2380e32ba78fde4c0", size = 27930055 }, + { url = "https://files.pythonhosted.org/packages/24/ec/6e4fe2a34a91102c806ecf9f45426f66bd604a5b5f48e951ce2bd770b2fe/scipy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e2448acd79c6374583581a1ded32ac71a00c2b9c62dfa87a40e1dd2520be111", size = 38031212 }, + { url = "https://files.pythonhosted.org/packages/82/4d/ecef655956ce332edbc411ab64ab843d767dd86e646898ac721dbcc7910e/scipy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36be480e512d38db67f377add5b759fb117edd987f4791cdf58e59b26962bee4", size = 40209536 }, + { url = "https://files.pythonhosted.org/packages/c5/ec/3af823fcd86e3155ad7ed2b684634391e4524ff82735c26abed522fc5405/scipy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ccb6248a9987193fe74363a2d73b93bc2c546e0728bd786050b7aef6e17db03c", size = 42584473 }, + { url = "https://files.pythonhosted.org/packages/23/01/f0ec4236ba8a96353e56694160041d7d9bebd9a0231a1c9beedc6e75cd50/scipy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:952d2e9eaa787f0a9e95b6e85da3654791b57a156c3e6609e65cc5176ccfe6f2", size = 43639460 }, + { url = "https://files.pythonhosted.org/packages/e9/02/c8bccc5c4813eccfeeef6ed0effe42e2cf98199d350ca476c22029569edc/scipy-1.15.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b1432102254b6dc7766d081fa92df87832ac25ff0b3d3a940f37276e63eb74ff", size = 41642304 }, + { url = "https://files.pythonhosted.org/packages/27/7a/9191a8b61f5826f08932b6ae47d44fbf4f473beb307d8ca3ed96a216929f/scipy-1.15.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:4e08c6a36f46abaedf765dd2dfcd3698fa4bd7e311a9abb2d80e33d9b2d72c34", size = 32620019 }, + { url = "https://files.pythonhosted.org/packages/e6/17/9c8452c8a59f1ede4a7ba6ba03b8b44703cdd1f1217b649f470c216f3095/scipy-1.15.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ec915cd26d76f6fc7ae8522f74f5b2accf39546f341c771bb2297f3871934a52", size = 24893299 }, + { url = "https://files.pythonhosted.org/packages/db/73/45c8566538bf9252be1e3e36b149714619c6f4d015a901cd76e257f88a37/scipy-1.15.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:351899dd2a801edd3691622172bc8ea01064b1cada794f8641b89a7dc5418db6", size = 27955764 }, + { url = "https://files.pythonhosted.org/packages/9f/4e/8822a2cafcea8727430e9a0bf785e8f0e81aaaac1048dad764d522f0f1ec/scipy-1.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9baff912ea4f78a543d183ed6f5b3bea9784509b948227daaf6f10727a0e2e5", size = 39879164 }, + { url = "https://files.pythonhosted.org/packages/b1/27/b55549a4aba515d9a19b6384c2c2f976725cd19d5d41c58ffac9a4d98892/scipy-1.15.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cd9d9198a7fd9a77f0eb5105ea9734df26f41faeb2a88a0e62e5245506f7b6df", size = 42091406 }, + { url = "https://files.pythonhosted.org/packages/79/df/989b2fd3f8ead6bcf89fc683fde94741eb3b291e41a3ce70cec08c80aa36/scipy-1.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:129f899ed275c0515d553b8d31696924e2ca87d1972421e46c376b9eb87de3d2", size = 43188844 }, ] [[package]] @@ -4727,6 +4881,7 @@ dependencies = [ { name = "pybars4", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "taskgroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] [package.optional-dependencies] @@ -4753,6 +4908,7 @@ dapr = [ ] google = [ { name = "google-cloud-aiplatform", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-genai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "google-generativeai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] hugging-face = [ @@ -4780,6 +4936,11 @@ ollama = [ onnx = [ { name = "onnxruntime-genai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] +openai-realtime = [ + { name = "aiortc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openai", extra = ["realtime"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "sounddevice", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] pandas = [ { name = "pandas", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -4824,6 +4985,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiohttp", specifier = "~=3.8" }, + { name = "aiortc", marker = "extra == 'openai-realtime'", specifier = ">=1.9.0" }, { name = "anthropic", marker = "extra == 'anthropic'", specifier = "~=0.32" }, { name = "azure-ai-inference", marker = "extra == 'azure'", specifier = ">=1.0.0b6" }, { name = "azure-core-tracing-opentelemetry", marker = "extra == 'azure'", specifier = ">=1.0.0b11" }, @@ -4839,6 +5001,7 @@ requires-dist = [ { name = "defusedxml", specifier = "~=0.7" }, { name = "flask-dapr", marker = "extra == 'dapr'", specifier = ">=1.14.0" }, { name = "google-cloud-aiplatform", marker = "extra == 'google'", specifier = "~=1.60" }, + { name = "google-genai", marker = "extra == 'google'", specifier = "~=0.4" }, { name = "google-generativeai", marker = "extra == 'google'", specifier = "~=0.7" }, { name = "ipykernel", marker = "extra == 'notebooks'", specifier = "~=6.29" }, { name = "jinja2", specifier = "~=3.1" }, @@ -4851,6 +5014,7 @@ requires-dist = [ { name = "ollama", marker = "extra == 'ollama'", specifier = "~=0.4" }, { name = "onnxruntime-genai", marker = "extra == 'onnx'", specifier = "~=0.5" }, { name = "openai", specifier = "~=1.0" }, + { name = "openai", extras = ["realtime"], marker = "extra == 'openai-realtime'", specifier = "~=1.0" }, { name = "openapi-core", specifier = ">=0.18,<0.20" }, { name = "opentelemetry-api", specifier = "~=1.24" }, { name = "opentelemetry-sdk", specifier = "~=1.24" }, @@ -4868,6 +5032,8 @@ requires-dist = [ { name = "redis", extras = ["hiredis"], marker = "extra == 'redis'", specifier = "~=5.0" }, { name = "redisvl", marker = "extra == 'redis'", specifier = ">=0.3.6" }, { name = "sentence-transformers", marker = "extra == 'hugging-face'", specifier = ">=2.2,<4.0" }, + { name = "sounddevice", marker = "extra == 'openai-realtime'", specifier = ">=0.5.1" }, + { name = "taskgroup", marker = "python_full_version < '3.11'", specifier = ">=0.2.2" }, { name = "torch", marker = "extra == 'hugging-face'", specifier = "==2.5.1" }, { name = "transformers", extras = ["torch"], marker = "extra == 'hugging-face'", specifier = "~=4.28" }, { name = "types-redis", marker = "extra == 'redis'", specifier = "~=4.6.0.20240425" }, @@ -5071,6 +5237,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/93/84a16940c44f6ec62cf334f25aed3128a514dffc361397eee09421a1c7f2/snoop-0.6.0-py3-none-any.whl", hash = "sha256:f5ea9060e65594bf404e6841086b4a964cc27bc30569109c91a470f948b0f729", size = 27461 }, ] +[[package]] +name = "sounddevice" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/2d/b04ae180312b81dbb694504bee170eada5372242e186f6298139fd3a0513/sounddevice-0.5.1.tar.gz", hash = "sha256:09ca991daeda8ce4be9ac91e15a9a81c8f81efa6b695a348c9171ea0c16cb041", size = 52896 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/d1/464b5fca3decdd0cfec8c47f7b4161a0b12972453201c1bf03811f367c5e/sounddevice-0.5.1-py3-none-any.whl", hash = "sha256:e2017f182888c3f3c280d9fbac92e5dbddac024a7e3442f6e6116bd79dab8a9c", size = 32276 }, + { url = "https://files.pythonhosted.org/packages/6f/f6/6703fe7cf3d7b7279040c792aeec6334e7305956aba4a80f23e62c8fdc44/sounddevice-0.5.1-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:d16cb23d92322526a86a9490c427bf8d49e273d9ccc0bd096feecd229cde6031", size = 107916 }, + { url = "https://files.pythonhosted.org/packages/57/a5/78a5e71f5ec0faedc54f4053775d61407bfbd7d0c18228c7f3d4252fd276/sounddevice-0.5.1-py3-none-win32.whl", hash = "sha256:d84cc6231526e7a08e89beff229c37f762baefe5e0cc2747cbe8e3a565470055", size = 312494 }, + { url = "https://files.pythonhosted.org/packages/af/9b/15217b04f3b36d30de55fef542389d722de63f1ad81f9c72d8afc98cb6ab/sounddevice-0.5.1-py3-none-win_amd64.whl", hash = "sha256:4313b63f2076552b23ac3e0abd3bcfc0c1c6a696fc356759a13bd113c9df90f1", size = 363634 }, +] + [[package]] name = "soupsieve" version = "2.6" @@ -5127,6 +5308,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, ] +[[package]] +name = "taskgroup" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b1/74babcc824a57904e919f3af16d86c08b524c0691504baf038ef2d7f655c/taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb", size = 14237 }, +] + [[package]] name = "tenacity" version = "9.0.0" @@ -5304,7 +5498,7 @@ wheels = [ [[package]] name = "transformers" -version = "4.48.0" +version = "4.47.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -5318,9 +5512,9 @@ dependencies = [ { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/71/93a6331682d6f15adf7d646956db0c43e5f1759bbbd05f2ef53029bae107/transformers-4.48.0.tar.gz", hash = "sha256:03fdfcbfb8b0367fb6c9fbe9d1c9aa54dfd847618be9b52400b2811d22799cb1", size = 8372101 } +sdist = { url = "https://files.pythonhosted.org/packages/15/1a/936aeb4f88112f670b604f5748034568dbc2b9bbb457a8d4518b1a15510a/transformers-4.47.1.tar.gz", hash = "sha256:6c29c05a5f595e278481166539202bf8641281536df1c42357ee58a45d0a564a", size = 8707421 } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/d6/a69764e89fc5c2c957aa473881527c8c35521108d553df703e9ba703daeb/transformers-4.48.0-py3-none-any.whl", hash = "sha256:6d3de6d71cb5f2a10f9775ccc17abce9620195caaf32ec96542bd2a6937f25b0", size = 9673380 }, + { url = "https://files.pythonhosted.org/packages/f2/3a/8bdab26e09c5a242182b7ba9152e216d5ab4ae2d78c4298eb4872549cd35/transformers-4.47.1-py3-none-any.whl", hash = "sha256:d2f5d19bb6283cd66c893ec7e6d931d6370bbf1cc93633326ff1f41a40046c9c", size = 10133598 }, ] [package.optional-dependencies] @@ -5632,16 +5826,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.29.0" +version = "20.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/5d/8d625ebddf9d31c301f85125b78002d4e4401fe1c15c04dca58a54a3056a/virtualenv-20.29.0.tar.gz", hash = "sha256:6345e1ff19d4b1296954cee076baaf58ff2a12a84a338c62b02eda39f20aa982", size = 7658081 } +sdist = { url = "https://files.pythonhosted.org/packages/50/39/689abee4adc85aad2af8174bb195a819d0be064bf55fcc73b49d2b28ae77/virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329", size = 7650532 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/d3/12687ab375bb0e077ea802a5128f7b45eb5de7a7c6cb576ccf9dd59ff80a/virtualenv-20.29.0-py3-none-any.whl", hash = "sha256:c12311863497992dc4b8644f8ea82d3b35bb7ef8ee82e6630d76d0197c39baf9", size = 4282443 }, + { url = "https://files.pythonhosted.org/packages/51/8f/dfb257ca6b4e27cb990f1631142361e4712badab8e3ca8dc134d96111515/virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb", size = 4276719 }, ] [[package]] @@ -5720,7 +5914,7 @@ wheels = [ [[package]] name = "weaviate-client" -version = "4.10.4" +version = "4.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -5731,9 +5925,9 @@ dependencies = [ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "validators", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ce/e34426eeda39a77b45df86f9ab901a7232096a071ee379a046a8072e2a35/weaviate_client-4.10.4.tar.gz", hash = "sha256:a1e799fc41d9f43a56c95490f6c14f475861f27d2a62b9b6de28a1db5494751d", size = 594549 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/80/5e36a1923d0bc01a6151f1cfb1550da83efec340cded1c4f885615e09575/weaviate_client-4.10.2.tar.gz", hash = "sha256:fde5ad8e36604674d26b115288b58a7e182c91e36c2b41a00d18a36fe4ec7e3f", size = 587835 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/e9/5b6ffbdee0d0f1444d0ce142c70a70bf22ba43bf2d6b35913a8d7e674431/weaviate_client-4.10.4-py3-none-any.whl", hash = "sha256:d9808456ba109fcd99331bc833b61cf520bf6ad9db442db621e12f78c8480c4c", size = 330450 }, + { url = "https://files.pythonhosted.org/packages/80/ca/9f2f1f27a05bfe90cb35a6dacaa547ad5a133211aeca7bb0021e2bbabb06/weaviate_client-4.10.2-py3-none-any.whl", hash = "sha256:e1706438aa7b57be5443bbdebff206cc6688110d1669d54c2721b3aa640b2c4c", size = 325368 }, ] [[package]]