From a3f06768c1eb5037e9799c28286f50a9ba84abe8 Mon Sep 17 00:00:00 2001 From: Sternakt Date: Thu, 12 Dec 2024 12:50:37 +0100 Subject: [PATCH 01/51] WIP --- autogen/agentchat/realtime_agent/__init__.py | 9 ++ .../realtime_agent/function_observer.py | 55 +++++++ .../realtime_agent/realtime_agent.py | 103 ++++++++++++ .../realtime_agent/twilio_observer.py | 148 ++++++++++++++++++ notebook/realtime_agent_twillio.ipynb | 129 +++++++++++++++ setup.py | 2 + 6 files changed, 446 insertions(+) create mode 100644 autogen/agentchat/realtime_agent/__init__.py create mode 100644 autogen/agentchat/realtime_agent/function_observer.py create mode 100644 autogen/agentchat/realtime_agent/realtime_agent.py create mode 100644 autogen/agentchat/realtime_agent/twilio_observer.py create mode 100644 notebook/realtime_agent_twillio.ipynb diff --git a/autogen/agentchat/realtime_agent/__init__.py b/autogen/agentchat/realtime_agent/__init__.py new file mode 100644 index 0000000000..fe3572874b --- /dev/null +++ b/autogen/agentchat/realtime_agent/__init__.py @@ -0,0 +1,9 @@ +from .function_observer import FunctionObserver +from .realtime_agent import RealtimeAgent +from .twilio_observer import TwilioAudioAdapter + +__all__ = [ + "RealtimeAgent", + "FunctionObserver", + "TwilioAudioAdapter", +] diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py new file mode 100644 index 0000000000..7d0f2fe9c3 --- /dev/null +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 - 2024, Owners of https://github.com/ag2ai +# +# SPDX-License-Identifier: Apache-2.0 +# +# Portions derived from https://github.com/microsoft/autogen are under the MIT License. +# SPDX-License-Identifier: MIT + +import json + +from .realtime_agent import RealtimeObserver + + +class FunctionObserver(RealtimeObserver): + def __init__(self): + super().__init__() + + async def update(self, response): + if response.get("type") == "response.function_call_arguments.done": + print("!" * 50) + print(f"Received event: {response['type']}", response) + await self.call_function(response["call_id"], **json.loads(response["arguments"])) + + async def call_function(self, call_id, location): + function_result = { + "type": "conversation.item.create", + "item": { + "type": "function_call_output", + "call_id": call_id, + "output": "The weather is cloudy." if location == "Seattle" else "The weather is sunny.", + }, + } + await self.client.openai_ws.send(json.dumps(function_result)) + await self.client.openai_ws.send(json.dumps({"type": "response.create"})) + + async def run(self, openai_ws): + await self.initialize_session(openai_ws) + + async def initialize_session(self, openai_ws): + """Add tool to OpenAI.""" + session_update = { + "type": "session.update", + "session": { + "tools": [ + { + "name": "get_weather", + "description": "Get the current weather", + "parameters": {"type": "object", "properties": {"location": {"type": "string"}}}, + "type": "function", + } + ], + "tool_choice": "auto", + }, + } + print("Sending session update:", json.dumps(session_update)) + await openai_ws.send(json.dumps(session_update)) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py new file mode 100644 index 0000000000..4124bf6052 --- /dev/null +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -0,0 +1,103 @@ +# Copyright (c) 2023 - 2024, Owners of https://github.com/ag2ai +# +# SPDX-License-Identifier: Apache-2.0 +# +# Portions derived from https://github.com/microsoft/autogen are under the MIT License. +# SPDX-License-Identifier: MIT + +import asyncio +import json +from abc import ABC, abstractmethod +from typing import Any, Callable, Dict, List, Literal, Optional, Union + +import websockets + +from autogen.agentchat.agent import Agent, LLMAgent + + +class RealtimeObserver(ABC): + def __init__(self): + self.client = None + + def register_client(self, client): + self.client = client + + @abstractmethod + async def run(self, openai_ws): + pass + + @abstractmethod + async def update(self, message): + pass + + +class RealtimeAgent(LLMAgent): + def __init__( + self, + name: str, + audio_adapter: RealtimeObserver, + system_message: Optional[Union[str, List]] = "You are a helpful AI Assistant.", + is_termination_msg: Optional[Callable[[Dict], bool]] = None, + max_consecutive_auto_reply: Optional[int] = None, + human_input_mode: Literal["ALWAYS", "NEVER", "TERMINATE"] = "TERMINATE", + function_map: Optional[Dict[str, Callable]] = None, + code_execution_config: Union[Dict, Literal[False]] = False, + llm_config: Optional[Union[Dict, Literal[False]]] = None, + default_auto_reply: Union[str, Dict] = "", + description: Optional[str] = None, + chat_messages: Optional[Dict[Agent, List[Dict]]] = None, + silent: Optional[bool] = None, + context_variables: Optional[Dict[str, Any]] = None, + voice: str = "alloy", + ): + self.llm_config = llm_config + self.system_message = system_message + self.voice = voice + self.observers = [] + self.openai_ws = None + + self.register(audio_adapter) + + def register(self, observer): + observer.register_client(self) + self.observers.append(observer) + + async def notify_observers(self, message): + for observer in self.observers: + await observer.update(message) + + async def _read_from_client(self, openai_ws): + try: + async for openai_message in openai_ws: + response = json.loads(openai_message) + await self.notify_observers(response) + except Exception as e: + print(f"Error in _read_from_client: {e}") + + async def run(self): + async with websockets.connect( + "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01", + extra_headers={"Authorization": f"Bearer {self.llm_config[0]['api_key']}", "OpenAI-Beta": "realtime=v1"}, + ) as openai_ws: + self.openai_ws = openai_ws + self.initialize_session() + await asyncio.gather( + self._read_from_client(openai_ws), *[observer.run(openai_ws) for observer in self.observers] + ) + + async def initialize_session(self): + """Control initial session with OpenAI.""" + session_update = { + "type": "session.update", + "session": { + "turn_detection": {"type": "server_vad"}, + "input_audio_format": "g711_ulaw", + "output_audio_format": "g711_ulaw", + "voice": self.voice, + "instructions": self.system_message, + "modalities": ["text", "audio"], + "temperature": 0.8, + }, + } + print("Sending session update:", json.dumps(session_update)) + await self.openai_ws.send(json.dumps(session_update)) diff --git a/autogen/agentchat/realtime_agent/twilio_observer.py b/autogen/agentchat/realtime_agent/twilio_observer.py new file mode 100644 index 0000000000..71352409d4 --- /dev/null +++ b/autogen/agentchat/realtime_agent/twilio_observer.py @@ -0,0 +1,148 @@ +# Copyright (c) 2023 - 2024, Owners of https://github.com/ag2ai +# +# SPDX-License-Identifier: Apache-2.0 +# +# Portions derived from https://github.com/microsoft/autogen are under the MIT License. +# SPDX-License-Identifier: MIT + +import base64 +import json + +from fastapi import WebSocketDisconnect + +from .realtime_agent import RealtimeObserver + +SYSTEM_MESSAGE = ( + "You are a helpful and bubbly AI assistant who loves to chat about weather" + "when the user queries about the waether at some location, call the get_weather tool" + "to get the current weather at that location." +) +VOICE = "alloy" +LOG_EVENT_TYPES = [ + "error", + "response.content.done", + "rate_limits.updated", + "response.done", + "input_audio_buffer.committed", + "input_audio_buffer.speech_stopped", + "input_audio_buffer.speech_started", + "session.created", +] +SHOW_TIMING_MATH = False + + +class TwilioAudioAdapter(RealtimeObserver): + def __init__(self, websocket): + super().__init__() + + self.websocket = websocket + + # Connection specific state + self.stream_sid = None + self.latest_media_timestamp = 0 + self.last_assistant_item = None + self.mark_queue = [] + self.response_start_timestamp_twilio = None + + async def update(self, response): + """Receive events from the OpenAI Realtime API, send audio back to Twilio.""" + if response["type"] in LOG_EVENT_TYPES: + print(f"Received event: {response['type']}", response) + + if response.get("type") == "response.audio.delta" and "delta" in response: + audio_payload = base64.b64encode(base64.b64decode(response["delta"])).decode("utf-8") + audio_delta = {"event": "media", "streamSid": self.stream_sid, "media": {"payload": audio_payload}} + await self.websocket.send_json(audio_delta) + + if self.response_start_timestamp_twilio is None: + self.response_start_timestamp_twilio = self.latest_media_timestamp + if SHOW_TIMING_MATH: + print(f"Setting start timestamp for new response: {self.response_start_timestamp_twilio}ms") + + # Update last_assistant_item safely + if response.get("item_id"): + self.last_assistant_item = response["item_id"] + + await self.send_mark() + + # Trigger an interruption. Your use case might work better using `input_audio_buffer.speech_stopped`, or combining the two. + if response.get("type") == "input_audio_buffer.speech_started": + print("Speech started detected.") + if self.last_assistant_item: + print(f"Interrupting response with id: {self.last_assistant_item}") + await self.handle_speech_started_event() + + async def handle_speech_started_event(self): + """Handle interruption when the caller's speech starts.""" + print("Handling speech started event.") + if self.mark_queue and self.response_start_timestamp_twilio is not None: + elapsed_time = self.latest_media_timestamp - self.response_start_timestamp_twilio + if SHOW_TIMING_MATH: + print( + f"Calculating elapsed time for truncation: {self.latest_media_timestamp} - {self.response_start_timestamp_twilio} = {elapsed_time}ms" + ) + + if self.last_assistant_item: + if SHOW_TIMING_MATH: + print(f"Truncating item with ID: {self.last_assistant_item}, Truncated at: {elapsed_time}ms") + + truncate_event = { + "type": "conversation.item.truncate", + "item_id": self.last_assistant_item, + "content_index": 0, + "audio_end_ms": elapsed_time, + } + await self.client.openai_ws.send(json.dumps(truncate_event)) + + await self.websocket.send_json({"event": "clear", "streamSid": self.stream_sid}) + + self.mark_queue.clear() + self.last_assistant_item = None + self.response_start_timestamp_twilio = None + + async def send_mark(self): + if self.stream_sid: + mark_event = {"event": "mark", "streamSid": self.stream_sid, "mark": {"name": "responsePart"}} + await self.websocket.send_json(mark_event) + self.mark_queue.append("responsePart") + + async def run(self, openai_ws): + # await self.initialize_session(openai_ws) + + try: + async for message in self.websocket.iter_text(): + data = json.loads(message) + if data["event"] == "media" and openai_ws.open: + self.latest_media_timestamp = int(data["media"]["timestamp"]) + audio_append = {"type": "input_audio_buffer.append", "audio": data["media"]["payload"]} + await openai_ws.send(json.dumps(audio_append)) + elif data["event"] == "start": + self.stream_sid = data["start"]["streamSid"] + print(f"Incoming stream has started {self.stream_sid}") + self.response_start_timestamp_twilio = None + self.latest_media_timestamp = 0 + self.last_assistant_item = None + elif data["event"] == "mark": + if self.mark_queue: + self.mark_queue.pop(0) + except WebSocketDisconnect: + print("Client disconnected.") + if openai_ws.open: + await openai_ws.close() + + async def initialize_session(self, openai_ws): + """Control initial session with OpenAI.""" + session_update = { + "type": "session.update", + "session": { + "turn_detection": {"type": "server_vad"}, + "input_audio_format": "g711_ulaw", + "output_audio_format": "g711_ulaw", + "voice": VOICE, + "instructions": SYSTEM_MESSAGE, + "modalities": ["text", "audio"], + "temperature": 0.8, + }, + } + print("Sending session update:", json.dumps(session_update)) + await openai_ws.send(json.dumps(session_update)) diff --git a/notebook/realtime_agent_twillio.ipynb b/notebook/realtime_agent_twillio.ipynb new file mode 100644 index 0000000000..6fd2b0cbf5 --- /dev/null +++ b/notebook/realtime_agent_twillio.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import nest_asyncio\n", + "from fastapi import FastAPI, Request, WebSocket\n", + "from fastapi.responses import HTMLResponse, JSONResponse\n", + "from twilio.twiml.voice_response import Connect, VoiceResponse\n", + "\n", + "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, TwilioAudioAdapter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import uvicorn\n", + "\n", + "# Configuration\n", + "OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\n", + "PORT = int(os.getenv(\"PORT\", 5050))\n", + "\n", + "llm_config = {\n", + " \"timeout\": 600,\n", + " \"cache_seed\": 45, # change the seed for different trials\n", + " \"config_list\": [\n", + " {\n", + " \"model\": \"gpt-4o-realtime-preview-2024-10-01\",\n", + " \"api_key\": OPENAI_API_KEY,\n", + " }\n", + " ],\n", + " \"temperature\": 0.8,\n", + "}\n", + "\n", + "\n", + "app = FastAPI()\n", + "\n", + "if not OPENAI_API_KEY:\n", + " raise ValueError(\"Missing the OpenAI API key. Please set it in the .env file.\")\n", + "\n", + "\n", + "@app.get(\"/\", response_class=JSONResponse)\n", + "async def index_page():\n", + " return {\"message\": \"Twilio Media Stream Server is running!\"}\n", + "\n", + "\n", + "@app.api_route(\"/incoming-call\", methods=[\"GET\", \"POST\"])\n", + "async def handle_incoming_call(request: Request):\n", + " \"\"\"Handle incoming call and return TwiML response to connect to Media Stream.\"\"\"\n", + " response = VoiceResponse()\n", + " # punctuation to improve text-to-speech flow\n", + " response.say(\n", + " \"Please wait while we connect your call to the A. I. voice assistant, powered by Twilio and the Open-A.I. Realtime API\"\n", + " )\n", + " response.pause(length=1)\n", + " response.say(\"O.K. you can start talking!\")\n", + " host = request.url.hostname\n", + " connect = Connect()\n", + " connect.stream(url=f\"wss://{host}/media-stream\")\n", + " response.append(connect)\n", + " return HTMLResponse(content=str(response), media_type=\"application/xml\")\n", + "\n", + "\n", + "@app.websocket(\"/media-stream\")\n", + "async def handle_media_stream(websocket: WebSocket):\n", + " \"\"\"Handle WebSocket connections between Twilio and OpenAI.\"\"\"\n", + " print(\"Client connected\")\n", + " await websocket.accept()\n", + "\n", + " audio_adapter = TwilioAudioAdapter(websocket)\n", + " openai_client = RealtimeAgent(\n", + " system_message=\"Hello there! I am an AI voice assistant powered by Twilio and the OpenAI Realtime API. You can ask me for facts, jokes, or anything you can imagine. How can I help you?\",\n", + " llm_config=llm_config,\n", + " audio_adapter=audio_adapter,\n", + " )\n", + "\n", + " openai_client.add_observer(FunctionObserver())\n", + "\n", + " # @openai_client.register_tool(name=\"get_weather\", description=\"Get the current weather\")\n", + " # def get_weather(location: Annotated[str, \"city\"]) -> str:\n", + " # ...\n", + " # return \"The weather is cloudy.\" if location == \"Seattle\" else \"The weather is sunny.\"\n", + "\n", + " # await openai_client.run()\n", + "\n", + "\n", + "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.20" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/setup.py b/setup.py index 64a4e7cee0..8dfbf1d9f7 100644 --- a/setup.py +++ b/setup.py @@ -121,6 +121,8 @@ "cohere": ["cohere>=5.5.8"], "ollama": ["ollama>=0.3.3", "fix_busted_json>=0.0.18"], "bedrock": ["boto3>=1.34.149"], + "realtime": ["websockets>=13.1"], + "realtime-twilio": ["fastapi>=0.115.0", "uvicorn==0.30.6", "twilio==9.3.2"], } setuptools.setup( From 793bf68ac4ab1654ec95b83b206164ab941969e8 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 12 Dec 2024 13:34:27 +0100 Subject: [PATCH 02/51] WIP: Draft initial version of realtime agent --- .../realtime_agent/realtime_agent.py | 8 ++- .../realtime_agent/twilio_observer.py | 52 +++++++------------ notebook/realtime_agent_twillio.ipynb | 15 ++++-- setup.py | 2 +- 4 files changed, 34 insertions(+), 43 deletions(-) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 4124bf6052..12e8b0f9e3 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -51,7 +51,7 @@ def __init__( voice: str = "alloy", ): self.llm_config = llm_config - self.system_message = system_message + self._oai_system_message = [{"content": system_message, "role": "system"}] self.voice = voice self.observers = [] self.openai_ws = None @@ -77,10 +77,10 @@ async def _read_from_client(self, openai_ws): async def run(self): async with websockets.connect( "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01", - extra_headers={"Authorization": f"Bearer {self.llm_config[0]['api_key']}", "OpenAI-Beta": "realtime=v1"}, + additional_headers={"Authorization": f"Bearer {self.llm_config['config_list'][0]['api_key']}", "OpenAI-Beta": "realtime=v1"}, ) as openai_ws: self.openai_ws = openai_ws - self.initialize_session() + await self.initialize_session() await asyncio.gather( self._read_from_client(openai_ws), *[observer.run(openai_ws) for observer in self.observers] ) @@ -91,8 +91,6 @@ async def initialize_session(self): "type": "session.update", "session": { "turn_detection": {"type": "server_vad"}, - "input_audio_format": "g711_ulaw", - "output_audio_format": "g711_ulaw", "voice": self.voice, "instructions": self.system_message, "modalities": ["text", "audio"], diff --git a/autogen/agentchat/realtime_agent/twilio_observer.py b/autogen/agentchat/realtime_agent/twilio_observer.py index 71352409d4..aa1152febb 100644 --- a/autogen/agentchat/realtime_agent/twilio_observer.py +++ b/autogen/agentchat/realtime_agent/twilio_observer.py @@ -12,12 +12,7 @@ from .realtime_agent import RealtimeObserver -SYSTEM_MESSAGE = ( - "You are a helpful and bubbly AI assistant who loves to chat about weather" - "when the user queries about the waether at some location, call the get_weather tool" - "to get the current weather at that location." -) -VOICE = "alloy" + LOG_EVENT_TYPES = [ "error", "response.content.done", @@ -107,41 +102,32 @@ async def send_mark(self): self.mark_queue.append("responsePart") async def run(self, openai_ws): - # await self.initialize_session(openai_ws) - - try: - async for message in self.websocket.iter_text(): - data = json.loads(message) - if data["event"] == "media" and openai_ws.open: - self.latest_media_timestamp = int(data["media"]["timestamp"]) - audio_append = {"type": "input_audio_buffer.append", "audio": data["media"]["payload"]} - await openai_ws.send(json.dumps(audio_append)) - elif data["event"] == "start": - self.stream_sid = data["start"]["streamSid"] - print(f"Incoming stream has started {self.stream_sid}") - self.response_start_timestamp_twilio = None - self.latest_media_timestamp = 0 - self.last_assistant_item = None - elif data["event"] == "mark": - if self.mark_queue: - self.mark_queue.pop(0) - except WebSocketDisconnect: - print("Client disconnected.") - if openai_ws.open: - await openai_ws.close() + await self.initialize_session(openai_ws) + + async for message in self.websocket.iter_text(): + data = json.loads(message) + if data["event"] == "media": + self.latest_media_timestamp = int(data["media"]["timestamp"]) + audio_append = {"type": "input_audio_buffer.append", "audio": data["media"]["payload"]} + await openai_ws.send(json.dumps(audio_append)) + elif data["event"] == "start": + self.stream_sid = data["start"]["streamSid"] + print(f"Incoming stream has started {self.stream_sid}") + self.response_start_timestamp_twilio = None + self.latest_media_timestamp = 0 + self.last_assistant_item = None + elif data["event"] == "mark": + if self.mark_queue: + self.mark_queue.pop(0) + async def initialize_session(self, openai_ws): """Control initial session with OpenAI.""" session_update = { "type": "session.update", "session": { - "turn_detection": {"type": "server_vad"}, "input_audio_format": "g711_ulaw", "output_audio_format": "g711_ulaw", - "voice": VOICE, - "instructions": SYSTEM_MESSAGE, - "modalities": ["text", "audio"], - "temperature": 0.8, }, } print("Sending session update:", json.dumps(session_update)) diff --git a/notebook/realtime_agent_twillio.ipynb b/notebook/realtime_agent_twillio.ipynb index 6fd2b0cbf5..d60f0e5b77 100644 --- a/notebook/realtime_agent_twillio.ipynb +++ b/notebook/realtime_agent_twillio.ipynb @@ -81,28 +81,35 @@ "@app.websocket(\"/media-stream\")\n", "async def handle_media_stream(websocket: WebSocket):\n", " \"\"\"Handle WebSocket connections between Twilio and OpenAI.\"\"\"\n", - " print(\"Client connected\")\n", " await websocket.accept()\n", "\n", " audio_adapter = TwilioAudioAdapter(websocket)\n", " openai_client = RealtimeAgent(\n", + " name=\"Weather Bot\",\n", " system_message=\"Hello there! I am an AI voice assistant powered by Twilio and the OpenAI Realtime API. You can ask me for facts, jokes, or anything you can imagine. How can I help you?\",\n", " llm_config=llm_config,\n", " audio_adapter=audio_adapter,\n", " )\n", "\n", - " openai_client.add_observer(FunctionObserver())\n", + " openai_client.register(FunctionObserver())\n", "\n", " # @openai_client.register_tool(name=\"get_weather\", description=\"Get the current weather\")\n", " # def get_weather(location: Annotated[str, \"city\"]) -> str:\n", " # ...\n", " # return \"The weather is cloudy.\" if location == \"Seattle\" else \"The weather is sunny.\"\n", "\n", - " # await openai_client.run()\n", + " await openai_client.run()\n", "\n", "\n", "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -121,7 +128,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.20" + "version": "3.10.16" } }, "nbformat": 4, diff --git a/setup.py b/setup.py index 8dfbf1d9f7..dd433d7aad 100644 --- a/setup.py +++ b/setup.py @@ -121,7 +121,7 @@ "cohere": ["cohere>=5.5.8"], "ollama": ["ollama>=0.3.3", "fix_busted_json>=0.0.18"], "bedrock": ["boto3>=1.34.149"], - "realtime": ["websockets>=13.1"], + "realtime": ["websockets>=14.0"], "realtime-twilio": ["fastapi>=0.115.0", "uvicorn==0.30.6", "twilio==9.3.2"], } From 24ea3c2d2f26d3f470404d8dc310378faf064afb Mon Sep 17 00:00:00 2001 From: Davorin Rusevljan Date: Thu, 12 Dec 2024 16:11:13 +0100 Subject: [PATCH 03/51] remove openai_ws from observers (half way through) --- .../realtime_agent/function_observer.py | 42 +++++++------------ .../realtime_agent/realtime_agent.py | 42 +++++++++++++------ .../realtime_agent/twilio_observer.py | 19 ++++----- 3 files changed, 53 insertions(+), 50 deletions(-) diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py index 7d0f2fe9c3..8818e27d4a 100644 --- a/autogen/agentchat/realtime_agent/function_observer.py +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -21,35 +21,23 @@ async def update(self, response): await self.call_function(response["call_id"], **json.loads(response["arguments"])) async def call_function(self, call_id, location): - function_result = { - "type": "conversation.item.create", - "item": { - "type": "function_call_output", - "call_id": call_id, - "output": "The weather is cloudy." if location == "Seattle" else "The weather is sunny.", - }, - } - await self.client.openai_ws.send(json.dumps(function_result)) - await self.client.openai_ws.send(json.dumps({"type": "response.create"})) + result = "The weather is cloudy." if location == "Seattle" else "The weather is sunny." + await self.client.function_result(call_id, result) - async def run(self, openai_ws): - await self.initialize_session(openai_ws) + async def run(self): + await self.initialize_session() - async def initialize_session(self, openai_ws): + async def initialize_session(self): """Add tool to OpenAI.""" session_update = { - "type": "session.update", - "session": { - "tools": [ - { - "name": "get_weather", - "description": "Get the current weather", - "parameters": {"type": "object", "properties": {"location": {"type": "string"}}}, - "type": "function", - } - ], - "tool_choice": "auto", - }, + "tools": [ + { + "name": "get_weather", + "description": "Get the current weather", + "parameters": {"type": "object", "properties": {"location": {"type": "string"}}}, + "type": "function", + } + ], + "tool_choice": "auto", } - print("Sending session update:", json.dumps(session_update)) - await openai_ws.send(json.dumps(session_update)) + await self.client.session_update(session_update) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 12e8b0f9e3..c6dc3ea0e7 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -66,9 +66,21 @@ async def notify_observers(self, message): for observer in self.observers: await observer.update(message) - async def _read_from_client(self, openai_ws): + async def function_result(self, call_id, result): + result_item = { + "type": "conversation.item.create", + "item": { + "type": "function_call_output", + "call_id": call_id, + "output": result, + }, + } + await self.openai_ws.send(json.dumps(result_item)) + await self.openai_ws.send(json.dumps({"type": "response.create"})) + + async def _read_from_client(self): try: - async for openai_message in openai_ws: + async for openai_message in self.openai_ws: response = json.loads(openai_message) await self.notify_observers(response) except Exception as e: @@ -82,20 +94,26 @@ async def run(self): self.openai_ws = openai_ws await self.initialize_session() await asyncio.gather( - self._read_from_client(openai_ws), *[observer.run(openai_ws) for observer in self.observers] + self._read_from_client(), *[observer.run() for observer in self.observers] ) async def initialize_session(self): """Control initial session with OpenAI.""" session_update = { + "turn_detection": {"type": "server_vad"}, + "voice": self.voice, + "instructions": self.system_message, + "modalities": ["text", "audio"], + "temperature": 0.8, + "input_audio_format": "g711_ulaw", + "output_audio_format": "g711_ulaw", + } + await self.session_update(session_update) + + async def session_update(self, session_options): + update = { "type": "session.update", - "session": { - "turn_detection": {"type": "server_vad"}, - "voice": self.voice, - "instructions": self.system_message, - "modalities": ["text", "audio"], - "temperature": 0.8, - }, + "session": session_options } - print("Sending session update:", json.dumps(session_update)) - await self.openai_ws.send(json.dumps(session_update)) + print("Sending session update:", json.dumps(update)) + await self.openai_ws.send(json.dumps(update)) diff --git a/autogen/agentchat/realtime_agent/twilio_observer.py b/autogen/agentchat/realtime_agent/twilio_observer.py index aa1152febb..2082c60976 100644 --- a/autogen/agentchat/realtime_agent/twilio_observer.py +++ b/autogen/agentchat/realtime_agent/twilio_observer.py @@ -29,7 +29,7 @@ class TwilioAudioAdapter(RealtimeObserver): def __init__(self, websocket): super().__init__() - + self.client = None self.websocket = websocket # Connection specific state @@ -101,8 +101,9 @@ async def send_mark(self): await self.websocket.send_json(mark_event) self.mark_queue.append("responsePart") - async def run(self, openai_ws): - await self.initialize_session(openai_ws) + async def run(self): + openai_ws = self.client.openai_ws + await self.initialize_session() async for message in self.websocket.iter_text(): data = json.loads(message) @@ -121,14 +122,10 @@ async def run(self, openai_ws): self.mark_queue.pop(0) - async def initialize_session(self, openai_ws): + async def initialize_session(self): """Control initial session with OpenAI.""" session_update = { - "type": "session.update", - "session": { - "input_audio_format": "g711_ulaw", - "output_audio_format": "g711_ulaw", - }, + "input_audio_format": "g711_ulaw", + "output_audio_format": "g711_ulaw", } - print("Sending session update:", json.dumps(session_update)) - await openai_ws.send(json.dumps(session_update)) + await self.client.session_update(session_update) From 942e3d064da07b4333be75f14b0effbbb9002091 Mon Sep 17 00:00:00 2001 From: Sternakt Date: Fri, 13 Dec 2024 08:50:32 +0100 Subject: [PATCH 04/51] WIP add handover registration to realtime agent --- .../realtime_agent/function_observer.py | 20 +++---- .../realtime_agent/realtime_agent.py | 53 +++++++++++++------ .../realtime_agent/realtime_observer.py | 24 +++++++++ .../realtime_agent/twilio_observer.py | 2 +- 4 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 autogen/agentchat/realtime_agent/realtime_observer.py diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py index 7d0f2fe9c3..796cf062f1 100644 --- a/autogen/agentchat/realtime_agent/function_observer.py +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -7,26 +7,27 @@ import json -from .realtime_agent import RealtimeObserver +from .realtime_observer import RealtimeObserver class FunctionObserver(RealtimeObserver): - def __init__(self): + def __init__(self, registered_functions): super().__init__() + self.registered_functions = registered_functions async def update(self, response): if response.get("type") == "response.function_call_arguments.done": print("!" * 50) print(f"Received event: {response['type']}", response) - await self.call_function(response["call_id"], **json.loads(response["arguments"])) + await self.call_function(response["call_id"], response["name"], json.loads(response["arguments"])) - async def call_function(self, call_id, location): + async def call_function(self, call_id, name, arguments): function_result = { "type": "conversation.item.create", "item": { "type": "function_call_output", "call_id": call_id, - "output": "The weather is cloudy." if location == "Seattle" else "The weather is sunny.", + "output": self.registered_functions[name](**arguments), }, } await self.client.openai_ws.send(json.dumps(function_result)) @@ -40,14 +41,7 @@ async def initialize_session(self, openai_ws): session_update = { "type": "session.update", "session": { - "tools": [ - { - "name": "get_weather", - "description": "Get the current weather", - "parameters": {"type": "object", "properties": {"location": {"type": "string"}}}, - "type": "function", - } - ], + "tools": [schema for schema, _ in self.registered_functions.values()], "tool_choice": "auto", }, } diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 4124bf6052..07eef50d44 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -8,27 +8,17 @@ import asyncio import json from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List, Literal, Optional, Union +from typing import Any, Callable, Dict, List, Literal, Optional, TypeVar, Union import websockets from autogen.agentchat.agent import Agent, LLMAgent +from autogen.function_utils import get_function_schema +from .function_observer import FunctionObserver +from .realtime_observer import RealtimeObserver -class RealtimeObserver(ABC): - def __init__(self): - self.client = None - - def register_client(self, client): - self.client = client - - @abstractmethod - async def run(self, openai_ws): - pass - - @abstractmethod - async def update(self, message): - pass +F = TypeVar("F", bound=Callable[..., Any]) class RealtimeAgent(LLMAgent): @@ -55,6 +45,7 @@ def __init__( self.voice = voice self.observers = [] self.openai_ws = None + self.registered_functions = {} self.register(audio_adapter) @@ -75,6 +66,7 @@ async def _read_from_client(self, openai_ws): print(f"Error in _read_from_client: {e}") async def run(self): + self.register(FunctionObserver(registered_functions=self.registered_functions)) async with websockets.connect( "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01", extra_headers={"Authorization": f"Bearer {self.llm_config[0]['api_key']}", "OpenAI-Beta": "realtime=v1"}, @@ -101,3 +93,34 @@ async def initialize_session(self): } print("Sending session update:", json.dumps(session_update)) await self.openai_ws.send(json.dumps(session_update)) + + def register_handover( + self, + *, + description: str, + name: Optional[str] = None, + ) -> Callable[[F], F]: + def _decorator(func: F, name=name) -> F: + """Decorator for registering a function to be used by an agent. + + Args: + func: the function to be registered. + + Returns: + The function to be registered, with the _description attribute set to the function description. + + Raises: + ValueError: if the function description is not provided and not propagated by a previous decorator. + RuntimeError: if the LLM config is not set up before registering a function. + + """ + # get JSON schema for the function + name = name or func.__name__ + + schema = get_function_schema(func, name=name, description=description) + + self.registered_functions["name"] = (schema, func) + + return func + + return _decorator diff --git a/autogen/agentchat/realtime_agent/realtime_observer.py b/autogen/agentchat/realtime_agent/realtime_observer.py new file mode 100644 index 0000000000..4e542030c1 --- /dev/null +++ b/autogen/agentchat/realtime_agent/realtime_observer.py @@ -0,0 +1,24 @@ +# Copyright (c) 2023 - 2024, Owners of https://github.com/ag2ai +# +# SPDX-License-Identifier: Apache-2.0 +# +# Portions derived from https://github.com/microsoft/autogen are under the MIT License. +# SPDX-License-Identifier: MIT + +from abc import ABC, abstractmethod + + +class RealtimeObserver(ABC): + def __init__(self): + self.client = None + + def register_client(self, client): + self.client = client + + @abstractmethod + async def run(self, openai_ws): + pass + + @abstractmethod + async def update(self, message): + pass diff --git a/autogen/agentchat/realtime_agent/twilio_observer.py b/autogen/agentchat/realtime_agent/twilio_observer.py index 71352409d4..8a2278e919 100644 --- a/autogen/agentchat/realtime_agent/twilio_observer.py +++ b/autogen/agentchat/realtime_agent/twilio_observer.py @@ -10,7 +10,7 @@ from fastapi import WebSocketDisconnect -from .realtime_agent import RealtimeObserver +from .realtime_observer import RealtimeObserver SYSTEM_MESSAGE = ( "You are a helpful and bubbly AI assistant who loves to chat about weather" From 97f12e024c516b091a9fc393339df86f5d0a25a4 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Fri, 13 Dec 2024 09:23:41 +0100 Subject: [PATCH 05/51] Implement function calling and register handover to realtime agent --- .../realtime_agent/function_observer.py | 13 +++++++++++-- .../agentchat/realtime_agent/realtime_agent.py | 5 +++-- notebook/realtime_agent_twillio.ipynb | 18 +++++------------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py index 151a68a14d..93325bb179 100644 --- a/autogen/agentchat/realtime_agent/function_observer.py +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -24,8 +24,9 @@ async def update(self, response): ) async def call_function(self, call_id, name, kwargs): - result = "The weather is cloudy." if kwargs["location"] == "Seattle" else "The weather is sunny." - await self.client.function_result(call_id, result) + #result = "The weather is cloudy." if kwargs["location"] == "Seattle" else "The weather is sunny." + _, func = self.registered_functions[name] + await self.client.function_result(call_id, func(**kwargs)) async def run(self): await self.initialize_session() @@ -34,6 +35,14 @@ async def initialize_session(self): """Add tool to OpenAI.""" session_update = { "tools": [schema for schema, _ in self.registered_functions.values()], + # "tools": [ + # { + # "name": "get_weather", + # "description": "Get the current weather", + # "parameters": {"type": "object", "properties": {"location": {"type": "string"}}}, + # "type": "function", + # }, + # ], "tool_choice": "auto", } await self.client.session_update(session_update) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index eccbd6033b..2ae3495059 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -129,9 +129,10 @@ def _decorator(func: F, name=name) -> F: # get JSON schema for the function name = name or func.__name__ - schema = get_function_schema(func, name=name, description=description) + schema = get_function_schema(func, name=name, description=description)["function"] + schema["type"] = "function" - self.registered_functions["name"] = (schema, func) + self.registered_functions[name] = (schema, func) return func diff --git a/notebook/realtime_agent_twillio.ipynb b/notebook/realtime_agent_twillio.ipynb index d60f0e5b77..4a10966c1b 100644 --- a/notebook/realtime_agent_twillio.ipynb +++ b/notebook/realtime_agent_twillio.ipynb @@ -31,6 +31,7 @@ "metadata": {}, "outputs": [], "source": [ + "from typing import Annotated\n", "import uvicorn\n", "\n", "# Configuration\n", @@ -91,25 +92,16 @@ " audio_adapter=audio_adapter,\n", " )\n", "\n", - " openai_client.register(FunctionObserver())\n", - "\n", - " # @openai_client.register_tool(name=\"get_weather\", description=\"Get the current weather\")\n", - " # def get_weather(location: Annotated[str, \"city\"]) -> str:\n", - " # ...\n", - " # return \"The weather is cloudy.\" if location == \"Seattle\" else \"The weather is sunny.\"\n", + " @openai_client.register_handover(name=\"get_weather\", description=\"Get the current weather\")\n", + " def get_weather(location: Annotated[str, \"city\"]) -> str:\n", + " ...\n", + " return \"The weather is cloudy.\" if location == \"Seattle\" else \"The weather is sunny.\"\n", "\n", " await openai_client.run()\n", "\n", "\n", "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 96cc3bd23c7839aa09678f119ffe9220d9861307 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Fri, 13 Dec 2024 09:26:27 +0100 Subject: [PATCH 06/51] Run pre-commit --- autogen/agentchat/realtime_agent/function_observer.py | 9 --------- notebook/realtime_agent_twillio.ipynb | 1 + 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py index 93325bb179..84409c4ee0 100644 --- a/autogen/agentchat/realtime_agent/function_observer.py +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -24,7 +24,6 @@ async def update(self, response): ) async def call_function(self, call_id, name, kwargs): - #result = "The weather is cloudy." if kwargs["location"] == "Seattle" else "The weather is sunny." _, func = self.registered_functions[name] await self.client.function_result(call_id, func(**kwargs)) @@ -35,14 +34,6 @@ async def initialize_session(self): """Add tool to OpenAI.""" session_update = { "tools": [schema for schema, _ in self.registered_functions.values()], - # "tools": [ - # { - # "name": "get_weather", - # "description": "Get the current weather", - # "parameters": {"type": "object", "properties": {"location": {"type": "string"}}}, - # "type": "function", - # }, - # ], "tool_choice": "auto", } await self.client.session_update(session_update) diff --git a/notebook/realtime_agent_twillio.ipynb b/notebook/realtime_agent_twillio.ipynb index 4a10966c1b..d6031b7554 100644 --- a/notebook/realtime_agent_twillio.ipynb +++ b/notebook/realtime_agent_twillio.ipynb @@ -32,6 +32,7 @@ "outputs": [], "source": [ "from typing import Annotated\n", + "\n", "import uvicorn\n", "\n", "# Configuration\n", From 6145e62fb91b74792a61471c5e34d36fa296066b Mon Sep 17 00:00:00 2001 From: Davorin Rusevljan Date: Fri, 13 Dec 2024 11:21:57 +0100 Subject: [PATCH 07/51] Separated client out from RealtimeAgent --- autogen/agentchat/realtime_agent/client.py | 75 +++++++++++++++++++ .../realtime_agent/function_observer.py | 12 +-- .../realtime_agent/realtime_agent.py | 64 ++-------------- .../realtime_agent/realtime_observer.py | 4 +- .../realtime_agent/twilio_observer.py | 7 +- 5 files changed, 91 insertions(+), 71 deletions(-) create mode 100644 autogen/agentchat/realtime_agent/client.py diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py new file mode 100644 index 0000000000..a90a7fda78 --- /dev/null +++ b/autogen/agentchat/realtime_agent/client.py @@ -0,0 +1,75 @@ +import asyncio +import json +from abc import ABC, abstractmethod + +import websockets + +from .function_observer import FunctionObserver + + +class Client(ABC): + def __init__(self, agent, audio_adapter, function_observer: FunctionObserver): + self._agent = agent + self._observers = [] + self._openai_ws = None # todo factor out to OpenAIClient + self.register(audio_adapter) + self.register(function_observer) + + def register(self, observer): + observer.register_client(self) + self._observers.append(observer) + + async def notify_observers(self, message): + for observer in self._observers: + await observer.update(message) + + async def function_result(self, call_id, result): + result_item = { + "type": "conversation.item.create", + "item": { + "type": "function_call_output", + "call_id": call_id, + "output": result, + }, + } + await self._openai_ws.send(json.dumps(result_item)) + await self._openai_ws.send(json.dumps({"type": "response.create"})) + + # todo override in specific clients + async def initialize_session(self): + """Control initial session with OpenAI.""" + session_update = { + "turn_detection": {"type": "server_vad"}, + "voice": self._agent.voice, + "instructions": self._agent.system_message, + "modalities": ["text", "audio"], + "temperature": 0.8, + } + await self.session_update(session_update) + + # todo override in specific clients + async def session_update(self, session_options): + update = {"type": "session.update", "session": session_options} + print("Sending session update:", json.dumps(update)) + await self._openai_ws.send(json.dumps(update)) + + async def _read_from_client(self): + try: + async for openai_message in self._openai_ws: + response = json.loads(openai_message) + await self.notify_observers(response) + except Exception as e: + print(f"Error in _read_from_client: {e}") + + async def run(self): + + async with websockets.connect( + "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01", + additional_headers={ + "Authorization": f"Bearer {self._agent.llm_config['config_list'][0]['api_key']}", + "OpenAI-Beta": "realtime=v1", + }, + ) as openai_ws: + self._openai_ws = openai_ws + await self.initialize_session() + await asyncio.gather(self._read_from_client(), *[observer.run() for observer in self._observers]) diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py index 84409c4ee0..8d3fcec676 100644 --- a/autogen/agentchat/realtime_agent/function_observer.py +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -11,9 +11,9 @@ class FunctionObserver(RealtimeObserver): - def __init__(self, registered_functions): + def __init__(self, agent): super().__init__() - self.registered_functions = registered_functions + self._agent = agent async def update(self, response): if response.get("type") == "response.function_call_arguments.done": @@ -24,8 +24,8 @@ async def update(self, response): ) async def call_function(self, call_id, name, kwargs): - _, func = self.registered_functions[name] - await self.client.function_result(call_id, func(**kwargs)) + _, func = self._agent.registered_functions[name] + await self._client.function_result(call_id, func(**kwargs)) async def run(self): await self.initialize_session() @@ -33,7 +33,7 @@ async def run(self): async def initialize_session(self): """Add tool to OpenAI.""" session_update = { - "tools": [schema for schema, _ in self.registered_functions.values()], + "tools": [schema for schema, _ in self._agent.registered_functions.values()], "tool_choice": "auto", } - await self.client.session_update(session_update) + await self._client.session_update(session_update) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 2ae3495059..b39f41d5f2 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -15,6 +15,7 @@ from autogen.agentchat.agent import Agent, LLMAgent from autogen.function_utils import get_function_schema +from .client import Client from .function_observer import FunctionObserver from .realtime_observer import RealtimeObserver @@ -40,71 +41,16 @@ def __init__( context_variables: Optional[Dict[str, Any]] = None, voice: str = "alloy", ): + + self._client = Client(self, audio_adapter, FunctionObserver(self)) self.llm_config = llm_config - self._oai_system_message = [{"content": system_message, "role": "system"}] self.voice = voice - self.observers = [] - self.openai_ws = None self.registered_functions = {} - self.register(audio_adapter) - - def register(self, observer): - observer.register_client(self) - self.observers.append(observer) - - async def notify_observers(self, message): - for observer in self.observers: - await observer.update(message) - - async def function_result(self, call_id, result): - result_item = { - "type": "conversation.item.create", - "item": { - "type": "function_call_output", - "call_id": call_id, - "output": result, - }, - } - await self.openai_ws.send(json.dumps(result_item)) - await self.openai_ws.send(json.dumps({"type": "response.create"})) - - async def _read_from_client(self): - try: - async for openai_message in self.openai_ws: - response = json.loads(openai_message) - await self.notify_observers(response) - except Exception as e: - print(f"Error in _read_from_client: {e}") + self._oai_system_message = [{"content": system_message, "role": "system"}] # todo still needed? async def run(self): - self.register(FunctionObserver(registered_functions=self.registered_functions)) - async with websockets.connect( - "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01", - additional_headers={ - "Authorization": f"Bearer {self.llm_config['config_list'][0]['api_key']}", - "OpenAI-Beta": "realtime=v1", - }, - ) as openai_ws: - self.openai_ws = openai_ws - await self.initialize_session() - await asyncio.gather(self._read_from_client(), *[observer.run() for observer in self.observers]) - - async def initialize_session(self): - """Control initial session with OpenAI.""" - session_update = { - "turn_detection": {"type": "server_vad"}, - "voice": self.voice, - "instructions": self.system_message, - "modalities": ["text", "audio"], - "temperature": 0.8, - } - await self.session_update(session_update) - - async def session_update(self, session_options): - update = {"type": "session.update", "session": session_options} - print("Sending session update:", json.dumps(update)) - await self.openai_ws.send(json.dumps(update)) + await self._client.run() def register_handover( self, diff --git a/autogen/agentchat/realtime_agent/realtime_observer.py b/autogen/agentchat/realtime_agent/realtime_observer.py index 4e542030c1..f1b6c88783 100644 --- a/autogen/agentchat/realtime_agent/realtime_observer.py +++ b/autogen/agentchat/realtime_agent/realtime_observer.py @@ -10,10 +10,10 @@ class RealtimeObserver(ABC): def __init__(self): - self.client = None + self._client = None def register_client(self, client): - self.client = client + self._client = client @abstractmethod async def run(self, openai_ws): diff --git a/autogen/agentchat/realtime_agent/twilio_observer.py b/autogen/agentchat/realtime_agent/twilio_observer.py index 168b8e2272..476bf10942 100644 --- a/autogen/agentchat/realtime_agent/twilio_observer.py +++ b/autogen/agentchat/realtime_agent/twilio_observer.py @@ -28,7 +28,6 @@ class TwilioAudioAdapter(RealtimeObserver): def __init__(self, websocket): super().__init__() - self.client = None self.websocket = websocket # Connection specific state @@ -86,7 +85,7 @@ async def handle_speech_started_event(self): "content_index": 0, "audio_end_ms": elapsed_time, } - await self.client.openai_ws.send(json.dumps(truncate_event)) + await self._client.openai_ws.send(json.dumps(truncate_event)) await self.websocket.send_json({"event": "clear", "streamSid": self.stream_sid}) @@ -101,7 +100,7 @@ async def send_mark(self): self.mark_queue.append("responsePart") async def run(self): - openai_ws = self.client.openai_ws + openai_ws = self._client._openai_ws await self.initialize_session() async for message in self.websocket.iter_text(): @@ -126,4 +125,4 @@ async def initialize_session(self): "input_audio_format": "g711_ulaw", "output_audio_format": "g711_ulaw", } - await self.client.session_update(session_update) + await self._client.session_update(session_update) From 3469f13484dafd760b32cbb4e24e502dc37ce271 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Tue, 17 Dec 2024 08:26:25 +0100 Subject: [PATCH 08/51] WIP: Integrate swarm into RealtimeAgent --- autogen/agentchat/realtime_agent/client.py | 15 + .../realtime_agent/function_observer.py | 22 +- .../realtime_agent/realtime_agent.py | 49 +- .../realtime_agent/twilio_observer.py | 2 +- notebook/agentchat_realtime_swarm.ipynb | 476 ++++++++++++++++++ notebook/realtime_agent_twillio.ipynb | 143 +++++- setup.py | 2 +- 7 files changed, 681 insertions(+), 28 deletions(-) create mode 100644 notebook/agentchat_realtime_swarm.ipynb diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py index a90a7fda78..2b1ade89f0 100644 --- a/autogen/agentchat/realtime_agent/client.py +++ b/autogen/agentchat/realtime_agent/client.py @@ -1,3 +1,10 @@ +# Copyright (c) 2023 - 2024, Owners of https://github.com/ag2ai +# +# SPDX-License-Identifier: Apache-2.0 +# +# Portions derived from https://github.com/microsoft/autogen are under the MIT License. +# SPDX-License-Identifier: MIT + import asyncio import json from abc import ABC, abstractmethod @@ -35,6 +42,14 @@ async def function_result(self, call_id, result): await self._openai_ws.send(json.dumps(result_item)) await self._openai_ws.send(json.dumps({"type": "response.create"})) + async def send_text(self, text): + text_item = { + "type": "conversation.item.create", + "item": {"type": "message", "role": "system", "content": [{"type": "input_text", "text": text}]}, + } + await self._openai_ws.send(json.dumps(text_item)) + await self._openai_ws.send(json.dumps({"type": "response.create"})) + # todo override in specific clients async def initialize_session(self): """Control initial session with OpenAI.""" diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py index 8d3fcec676..09a40dc1b9 100644 --- a/autogen/agentchat/realtime_agent/function_observer.py +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -5,8 +5,12 @@ # Portions derived from https://github.com/microsoft/autogen are under the MIT License. # SPDX-License-Identifier: MIT +import asyncio import json +from asyncer import asyncify +from pydantic import BaseModel + from .realtime_observer import RealtimeObserver @@ -24,8 +28,22 @@ async def update(self, response): ) async def call_function(self, call_id, name, kwargs): - _, func = self._agent.registered_functions[name] - await self._client.function_result(call_id, func(**kwargs)) + if name in self._agent.registered_functions: + _, func = self._agent.registered_functions[name] + func = func if asyncio.iscoroutinefunction(func) else asyncify(func) + try: + result = await func(**kwargs) + except Exception: + result = "Function call failed" + + if isinstance(result, BaseModel): + result = result.model_dump_json() + elif not isinstance(result, str): + result = json.dumps(result) + + print("!" * 50) + print(f"Function call result: {result}") + await self._client.function_result(call_id, result) async def run(self): await self.initialize_session() diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index b39f41d5f2..c20dee1961 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -8,11 +8,13 @@ import asyncio import json from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List, Literal, Optional, TypeVar, Union +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, TypeVar, Union +import asyncer import websockets from autogen.agentchat.agent import Agent, LLMAgent +from autogen.agentchat.conversable_agent import ConversableAgent from autogen.function_utils import get_function_schema from .client import Client @@ -22,7 +24,7 @@ F = TypeVar("F", bound=Callable[..., Any]) -class RealtimeAgent(LLMAgent): +class RealtimeAgent(ConversableAgent): def __init__( self, name: str, @@ -30,7 +32,7 @@ def __init__( system_message: Optional[Union[str, List]] = "You are a helpful AI Assistant.", is_termination_msg: Optional[Callable[[Dict], bool]] = None, max_consecutive_auto_reply: Optional[int] = None, - human_input_mode: Literal["ALWAYS", "NEVER", "TERMINATE"] = "TERMINATE", + human_input_mode: Literal["ALWAYS", "NEVER", "TERMINATE"] = "ALWAYS", function_map: Optional[Dict[str, Callable]] = None, code_execution_config: Union[Dict, Literal[False]] = False, llm_config: Optional[Union[Dict, Literal[False]]] = None, @@ -41,13 +43,30 @@ def __init__( context_variables: Optional[Dict[str, Any]] = None, voice: str = "alloy", ): - + super().__init__( + name=name, + is_termination_msg=is_termination_msg, + max_consecutive_auto_reply=max_consecutive_auto_reply, + human_input_mode=human_input_mode, + function_map=function_map, + code_execution_config=code_execution_config, + default_auto_reply=default_auto_reply, + llm_config=llm_config, + system_message=system_message, + description=description, + chat_messages=chat_messages, + silent=silent, + context_variables=context_variables, + ) self._client = Client(self, audio_adapter, FunctionObserver(self)) self.llm_config = llm_config self.voice = voice self.registered_functions = {} self._oai_system_message = [{"content": system_message, "role": "system"}] # todo still needed? + self.register_reply( + [Agent, None], RealtimeAgent.check_termination_and_human_reply, remove_other_reply_funcs=True + ) async def run(self): await self._client.run() @@ -83,3 +102,25 @@ def _decorator(func: F, name=name) -> F: return func return _decorator + + def check_termination_and_human_reply( + self, + messages: Optional[List[Dict]] = None, + sender: Optional[Agent] = None, + config: Optional[Any] = None, + ) -> Tuple[bool, Union[str, None]]: + loop = asyncio.get_event_loop() + self.answer = loop.create_future() + loop.run_until_complete( + self._client.send_text( + ( + f"QUESTION FOR USER (DO NOT ANSWER, ASK THE USER, AND CALL answer_swarm_question AFTER THE USER ANSWERS): '{messages[-1]['content']}'\n\n" + ) + ) + ) + + async def get_input(): + input_text = await self.answer + return input_text + + return True, loop.run_until_complete(get_input()) diff --git a/autogen/agentchat/realtime_agent/twilio_observer.py b/autogen/agentchat/realtime_agent/twilio_observer.py index 476bf10942..04232f13a7 100644 --- a/autogen/agentchat/realtime_agent/twilio_observer.py +++ b/autogen/agentchat/realtime_agent/twilio_observer.py @@ -85,7 +85,7 @@ async def handle_speech_started_event(self): "content_index": 0, "audio_end_ms": elapsed_time, } - await self._client.openai_ws.send(json.dumps(truncate_event)) + await self._client._openai_ws.send(json.dumps(truncate_event)) await self.websocket.send_json({"event": "clear", "streamSid": self.stream_sid}) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb new file mode 100644 index 0000000000..994d78c3b7 --- /dev/null +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -0,0 +1,476 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Swarm Orchestration with AG2 \n", + "\n", + "\n", + "AG2 offers conversable agents, powered by LLMs, tools or a human, that can perform tasks collectively via an automated chat. Recently, OpenAI released a [Swarm](https://github.com/openai/swarm) framework that focuses on making agent coordination and execution lightweight. \n", + "\n", + "Now, AG2 offers a simple programming interface to build and orchestrate a swarm of agents. Please check the [Documentation](https://ag2ai.github.io/ag2/docs/topics/swarm) and [Blog](https://ag2ai.github.io/ag2/blog/2024/11/17/Swarm) for more details.\n", + "\n", + "In this notebook, we implement OpenAI's [airline customer service example](https://github.com/openai/swarm/tree/main/examples/airline) in AG2." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "````{=mdx}\n", + ":::info Requirements\n", + "Install `ag2`:\n", + "```bash\n", + "pip install ag2\n", + "```\n", + "\n", + "For more information, please refer to the [installation guide](/docs/installation/).\n", + ":::\n", + "````" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set your API Endpoint\n", + "\n", + "The [`config_list_from_json`](https://ag2ai.github.io/ag2/docs/reference/oai/openai_utils#config_list_from_json) function loads a list of configurations from an environment variable or a json file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import autogen\n", + "\n", + "config_list = autogen.config_list_from_json(\n", + " \"OAI_CONFIG_LIST\",\n", + " filter_dict={\n", + " \"model\": [\"gpt-4o-mini\"],\n", + " },\n", + ")\n", + "\n", + "llm_config = {\n", + " \"cache_seed\": 42, # change the cache_seed for different trials\n", + " \"temperature\": 1,\n", + " \"config_list\": config_list,\n", + " \"timeout\": 120,\n", + " \"tools\": [],\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prompts & Utility Functions\n", + "\n", + "The prompts and utility functions remain unchanged from the original example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# baggage/policies.py\n", + "LOST_BAGGAGE_POLICY = \"\"\"\n", + "1. Call the 'initiate_baggage_search' function to start the search process.\n", + "2. If the baggage is found:\n", + "2a) Arrange for the baggage to be delivered to the customer's address.\n", + "3. If the baggage is not found:\n", + "3a) Call the 'escalate_to_agent' function.\n", + "4. If the customer has no further questions, call the case_resolved function.\n", + "\n", + "**Case Resolved: When the case has been resolved, ALWAYS call the \"case_resolved\" function**\n", + "\"\"\"\n", + "\n", + "# flight_modification/policies.py\n", + "# Damaged\n", + "FLIGHT_CANCELLATION_POLICY = \"\"\"\n", + "1. Confirm which flight the customer is asking to cancel.\n", + "1a) If the customer is asking about the same flight, proceed to next step.\n", + "1b) If the customer is not, call 'escalate_to_agent' function.\n", + "2. Confirm if the customer wants a refund or flight credits.\n", + "3. If the customer wants a refund follow step 3a). If the customer wants flight credits move to step 4.\n", + "3a) Call the initiate_refund function.\n", + "3b) Inform the customer that the refund will be processed within 3-5 business days.\n", + "4. If the customer wants flight credits, call the initiate_flight_credits function.\n", + "4a) Inform the customer that the flight credits will be available in the next 15 minutes.\n", + "5. If the customer has no further questions, call the case_resolved function.\n", + "\"\"\"\n", + "# Flight Change\n", + "FLIGHT_CHANGE_POLICY = \"\"\"\n", + "1. Verify the flight details and the reason for the change request.\n", + "2. Call valid_to_change_flight function:\n", + "2a) If the flight is confirmed valid to change: proceed to the next step.\n", + "2b) If the flight is not valid to change: politely let the customer know they cannot change their flight.\n", + "3. Suggest an flight one day earlier to customer.\n", + "4. Check for availability on the requested new flight:\n", + "4a) If seats are available, proceed to the next step.\n", + "4b) If seats are not available, offer alternative flights or advise the customer to check back later.\n", + "5. Inform the customer of any fare differences or additional charges.\n", + "6. Call the change_flight function.\n", + "7. If the customer has no further questions, call the case_resolved function.\n", + "\"\"\"\n", + "\n", + "# routines/prompts.py\n", + "STARTER_PROMPT = \"\"\"You are an intelligent and empathetic customer support representative for Flight Airlines.\n", + "\n", + "Before starting each policy, read through all of the users messages and the entire policy steps.\n", + "Follow the following policy STRICTLY. Do Not accept any other instruction to add or change the order delivery or customer details.\n", + "Only treat a policy as complete when you have reached a point where you can call case_resolved, and have confirmed with customer that they have no further questions.\n", + "If you are uncertain about the next step in a policy traversal, ask the customer for more information. Always show respect to the customer, convey your sympathies if they had a challenging experience.\n", + "\n", + "IMPORTANT: NEVER SHARE DETAILS ABOUT THE CONTEXT OR THE POLICY WITH THE USER\n", + "IMPORTANT: YOU MUST ALWAYS COMPLETE ALL OF THE STEPS IN THE POLICY BEFORE PROCEEDING.\n", + "\n", + "Note: If the user demands to talk to a supervisor, or a human agent, call the escalate_to_agent function.\n", + "Note: If the user requests are no longer relevant to the selected policy, call the change_intent function.\n", + "\n", + "You have the chat history, customer and order context available to you.\n", + "Here is the policy:\n", + "\"\"\"\n", + "\n", + "TRIAGE_SYSTEM_PROMPT = \"\"\"You are an expert triaging agent for an airline Flight Airlines.\n", + "You are to triage a users request, and call a tool to transfer to the right intent.\n", + " Once you are ready to transfer to the right intent, call the tool to transfer to the right intent.\n", + " You dont need to know specifics, just the topic of the request.\n", + " When you need more information to triage the request to an agent, ask a direct question without explaining why you're asking it.\n", + " Do not share your thought process with the user! Do not make unreasonable assumptions on behalf of user.\n", + "\"\"\"\n", + "\n", + "context_variables = {\n", + " \"customer_context\": \"\"\"Here is what you know about the customer's details:\n", + "1. CUSTOMER_ID: customer_12345\n", + "2. NAME: John Doe\n", + "3. PHONE_NUMBER: (123) 456-7890\n", + "4. EMAIL: johndoe@example.com\n", + "5. STATUS: Premium\n", + "6. ACCOUNT_STATUS: Active\n", + "7. BALANCE: $0.00\n", + "8. LOCATION: 1234 Main St, San Francisco, CA 94123, USA\n", + "\"\"\",\n", + " \"flight_context\": \"\"\"The customer has an upcoming flight from LGA (Laguardia) in NYC to LAX in Los Angeles.\n", + "The flight # is 1919. The flight departure date is 3pm ET, 5/21/2024.\"\"\",\n", + "}\n", + "\n", + "\n", + "def triage_instructions(context_variables):\n", + " customer_context = context_variables.get(\"customer_context\", None)\n", + " flight_context = context_variables.get(\"flight_context\", None)\n", + " return f\"\"\"You are to triage a users request, and call a tool to transfer to the right intent.\n", + " Once you are ready to transfer to the right intent, call the tool to transfer to the right intent.\n", + " You dont need to know specifics, just the topic of the request.\n", + " When you need more information to triage the request to an agent, ask a direct question without explaining why you're asking it.\n", + " Do not share your thought process with the user! Do not make unreasonable assumptions on behalf of user.\n", + " The customer context is here: {customer_context}, and flight context is here: {flight_context}\"\"\"\n", + "\n", + "\n", + "def valid_to_change_flight() -> str:\n", + " return \"Customer is eligible to change flight\"\n", + "\n", + "\n", + "def change_flight() -> str:\n", + " return \"Flight was successfully changed!\"\n", + "\n", + "\n", + "def initiate_refund() -> str:\n", + " status = \"Refund initiated\"\n", + " return status\n", + "\n", + "\n", + "def initiate_flight_credits() -> str:\n", + " status = \"Successfully initiated flight credits\"\n", + " return status\n", + "\n", + "\n", + "def initiate_baggage_search() -> str:\n", + " return \"Baggage was found!\"\n", + "\n", + "\n", + "def case_resolved() -> str:\n", + " return \"Case resolved. No further questions.\"\n", + "\n", + "\n", + "def escalate_to_agent(reason: str = None) -> str:\n", + " \"\"\"Escalating to human agent to confirm the request.\"\"\"\n", + " return f\"Escalating to agent: {reason}\" if reason else \"Escalating to agent\"\n", + "\n", + "\n", + "def non_flight_enquiry() -> str:\n", + " return \"Sorry, we can't assist with non-flight related enquiries.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define Agents and register functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen import ON_CONDITION, AfterWorkOption, SwarmAgent, initiate_swarm_chat\n", + "\n", + "# Triage Agent\n", + "triage_agent = SwarmAgent(\n", + " name=\"Triage_Agent\",\n", + " system_message=triage_instructions(context_variables=context_variables),\n", + " llm_config=llm_config,\n", + " functions=[non_flight_enquiry],\n", + ")\n", + "\n", + "# Flight Modification Agent\n", + "flight_modification = SwarmAgent(\n", + " name=\"Flight_Modification_Agent\",\n", + " system_message=\"\"\"You are a Flight Modification Agent for a customer service airline.\n", + " Your task is to determine if the user wants to cancel or change their flight.\n", + " Use message history and ask clarifying questions as needed to decide.\n", + " Once clear, call the appropriate transfer function.\"\"\",\n", + " llm_config=llm_config,\n", + ")\n", + "\n", + "# Flight Cancel Agent\n", + "flight_cancel = SwarmAgent(\n", + " name=\"Flight_Cancel_Traversal\",\n", + " system_message=STARTER_PROMPT + FLIGHT_CANCELLATION_POLICY,\n", + " llm_config=llm_config,\n", + " functions=[initiate_refund, initiate_flight_credits, case_resolved, escalate_to_agent],\n", + ")\n", + "\n", + "# Flight Change Agent\n", + "flight_change = SwarmAgent(\n", + " name=\"Flight_Change_Traversal\",\n", + " system_message=STARTER_PROMPT + FLIGHT_CHANGE_POLICY,\n", + " llm_config=llm_config,\n", + " functions=[valid_to_change_flight, change_flight, case_resolved, escalate_to_agent],\n", + ")\n", + "\n", + "# Lost Baggage Agent\n", + "lost_baggage = SwarmAgent(\n", + " name=\"Lost_Baggage_Traversal\",\n", + " system_message=STARTER_PROMPT + LOST_BAGGAGE_POLICY,\n", + " llm_config=llm_config,\n", + " functions=[initiate_baggage_search, case_resolved, escalate_to_agent],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Register Handoffs\n", + "\n", + "Now we register the handoffs for the agents. Note that you don't need to define the transfer functions and pass them in. Instead, you can directly register the handoffs using the `ON_CONDITION` class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Register hand-offs\n", + "triage_agent.register_hand_off(\n", + " [\n", + " ON_CONDITION(flight_modification, \"To modify a flight\"),\n", + " ON_CONDITION(lost_baggage, \"To find lost baggage\"),\n", + " ]\n", + ")\n", + "\n", + "flight_modification.register_hand_off(\n", + " [\n", + " ON_CONDITION(flight_cancel, \"To cancel a flight\"),\n", + " ON_CONDITION(flight_change, \"To change a flight\"),\n", + " ]\n", + ")\n", + "\n", + "transfer_to_triage_description = \"Call this function when a user needs to be transferred to a different agent and a different policy.\\nFor instance, if a user is asking about a topic that is not handled by the current agent, call this function.\"\n", + "for agent in [flight_modification, flight_cancel, flight_change, lost_baggage]:\n", + " agent.register_hand_off(ON_CONDITION(triage_agent, transfer_to_triage_description))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run the code\n", + "\n", + "Finally, call `initiate_swarm_chat` to start the conversation. \n", + "\n", + "For this example, it requires human interaction with the agents. So we pass in a `user` agent, and set the `after_work` parameter to `AfterWorkOption.REVERT_TO_USER`. This means, each time a swarm agent finishes its work, the conversation will be reverted back to the user to give the next input.\n", + "\n", + "> You need to interact with the agents for this example. (You can try different inputs to see how they react!)\n", + "\n", + "Here is a sequence of messages entered in this example:\n", + "\n", + "To initialize the conversation, we pass `I want to cancel flight` to the `initiate_swarm_chat` function.\n", + "1. `please cancel it` (Ask for reason but don't provide one)\n", + "2. `1919` (The flight number)\n", + "3. `I want flight credits`\n", + "4. `No` (No further questions)\n", + "5. `exit` (End the conversation)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "import os\n", + "import time\n", + "from concurrent.futures import ThreadPoolExecutor\n", + "from typing import Annotated, Union\n", + "\n", + "import asyncer\n", + "import nest_asyncio\n", + "import uvicorn\n", + "from fastapi import FastAPI, Request, WebSocket\n", + "from fastapi.responses import HTMLResponse, JSONResponse\n", + "from pydantic import BaseModel\n", + "from twilio.twiml.voice_response import Connect, VoiceResponse\n", + "\n", + "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, TwilioAudioAdapter\n", + "\n", + "# Configuration\n", + "OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\n", + "PORT = int(os.getenv(\"PORT\", 5050))\n", + "\n", + "if not OPENAI_API_KEY:\n", + " raise ValueError(\"Missing the OpenAI API key. Please set it in the .env file.\")\n", + "\n", + "realtime_llm_config = {\n", + " \"timeout\": 600,\n", + " \"cache_seed\": 45, # change the seed for different trials\n", + " \"config_list\": [\n", + " {\n", + " \"model\": \"gpt-4o-realtime-preview-2024-10-01\",\n", + " \"api_key\": OPENAI_API_KEY,\n", + " }\n", + " ],\n", + " \"temperature\": 0.8,\n", + "}\n", + "\n", + "app = FastAPI()\n", + "\n", + "\n", + "@app.get(\"/\", response_class=JSONResponse)\n", + "async def index_page():\n", + " return {\"message\": \"Twilio Media Stream Server is running!\"}\n", + "\n", + "\n", + "@app.api_route(\"/incoming-call\", methods=[\"GET\", \"POST\"])\n", + "async def handle_incoming_call(request: Request):\n", + " \"\"\"Handle incoming call and return TwiML response to connect to Media Stream.\"\"\"\n", + " response = VoiceResponse()\n", + " response.say(\n", + " \"Please wait while we connect your call to the A. I. voice assistant, powered by Twilio and the Open-A.I. Realtime API\"\n", + " )\n", + " response.pause(length=1)\n", + " response.say(\"O.K. you can start talking!\")\n", + " host = request.url.hostname\n", + " connect = Connect()\n", + " connect.stream(url=f\"wss://{host}/media-stream\")\n", + " response.append(connect)\n", + " return HTMLResponse(content=str(response), media_type=\"application/xml\")\n", + "\n", + "\n", + "@app.websocket(\"/media-stream\")\n", + "async def handle_media_stream(websocket: WebSocket):\n", + " \"\"\"Handle WebSocket connections between Twilio and OpenAI.\"\"\"\n", + " await websocket.accept()\n", + "\n", + " audio_adapter = TwilioAudioAdapter(websocket)\n", + " realtime_agent = RealtimeAgent(\n", + " name=\"Customer_service_Bot\",\n", + " system_message=(\n", + " \"You are a helpful voice assistant. You are tasked with delegating tasks to a swarm of agents\\n\"\n", + " \"When a user calls in, you will need to ask the swarm for help to complete the task\\n\"\n", + " \"The swarm will ask questions and provide feedback to help you complete the task\\n\"\n", + " \"You can start the swarm by calling the function `start_swarm`\\n\"\n", + " \"The swarm will sometimes ask questions, you can answer them by calling the function `answer_swarm_question`\\n\"\n", + " \"The questions will start wih the phrase 'QUESTION FOR USER (DO NOT ANSWER, ASK THE USER, AND CALL answer_swarm_question AFTER THE USER ANSWERS):'\\n\"\n", + " \"DO NOT CALL THE FUNCTION UNTIL YOU HAVE THE ANSWER FROM THE USER.\",\n", + " \"Pay close attention to the function signature and the expected input. The function signature is `answer_swarm_question` and the expected input is a string.\",\n", + " ),\n", + " llm_config=realtime_llm_config,\n", + " audio_adapter=audio_adapter,\n", + " )\n", + "\n", + " swarm_started = False\n", + "\n", + " @realtime_agent.register_handover(name=\"start_swarm\", description=\"Start a task using swarm intelligence\")\n", + " async def start_swarm(task_input: str) -> str:\n", + " nonlocal swarm_started\n", + " swarm_started = True\n", + " # chat_history, context_variables, last_agent = await\n", + " asyncio.get_running_loop().run_in_executor(\n", + " None,\n", + " lambda: initiate_swarm_chat(\n", + " initial_agent=triage_agent,\n", + " agents=[triage_agent, flight_modification, flight_cancel, flight_change, lost_baggage],\n", + " user_agent=realtime_agent,\n", + " messages=task_input,\n", + " after_work=AfterWorkOption.REVERT_TO_USER,\n", + " ),\n", + " )\n", + " return \"Swarm started\"\n", + "\n", + " @realtime_agent.register_handover(name=\"answer_swarm_question\", description=\"Answer a question from the swarm\")\n", + " async def answer_swarm_question(answer: str) -> str:\n", + " nonlocal swarm_started\n", + " if not swarm_started:\n", + " return \"Error: Swarm not started, call start_swarm first\"\n", + " realtime_agent.answer.set_result(answer)\n", + " return \"Answered swarm question\"\n", + "\n", + " await realtime_agent.run()\n", + "\n", + "\n", + "nest_asyncio.apply()\n", + "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" + ] + } + ], + "metadata": { + "front_matter": { + "description": "Swarm Ochestration", + "tags": [ + "orchestration", + "group chat", + "swarm" + ] + }, + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.16" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook/realtime_agent_twillio.ipynb b/notebook/realtime_agent_twillio.ipynb index d6031b7554..db93ae5849 100644 --- a/notebook/realtime_agent_twillio.ipynb +++ b/notebook/realtime_agent_twillio.ipynb @@ -6,14 +6,20 @@ "metadata": {}, "outputs": [], "source": [ + "import asyncio\n", "import os\n", + "import time\n", + "from typing import Annotated, Union\n", "\n", "import nest_asyncio\n", + "import uvicorn\n", "from fastapi import FastAPI, Request, WebSocket\n", "from fastapi.responses import HTMLResponse, JSONResponse\n", + "from pydantic import BaseModel\n", "from twilio.twiml.voice_response import Connect, VoiceResponse\n", "\n", - "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, TwilioAudioAdapter" + "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, TwilioAudioAdapter\n", + "from autogen.agentchat.realtime_agent.swarm_observer import SwarmObserver" ] }, { @@ -22,23 +28,13 @@ "metadata": {}, "outputs": [], "source": [ - "nest_asyncio.apply()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "import uvicorn\n", - "\n", "# Configuration\n", "OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\n", "PORT = int(os.getenv(\"PORT\", 5050))\n", "\n", + "if not OPENAI_API_KEY:\n", + " raise ValueError(\"Missing the OpenAI API key. Please set it in the .env file.\")\n", + "\n", "llm_config = {\n", " \"timeout\": 600,\n", " \"cache_seed\": 45, # change the seed for different trials\n", @@ -49,14 +45,24 @@ " }\n", " ],\n", " \"temperature\": 0.8,\n", - "}\n", - "\n", - "\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "app = FastAPI()\n", "\n", - "if not OPENAI_API_KEY:\n", - " raise ValueError(\"Missing the OpenAI API key. Please set it in the .env file.\")\n", - "\n", "\n", "@app.get(\"/\", response_class=JSONResponse)\n", "async def index_page():\n", @@ -103,6 +109,103 @@ "\n", "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## With swarm questions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "app = FastAPI()\n", + "\n", + "\n", + "@app.get(\"/\", response_class=JSONResponse)\n", + "async def index_page():\n", + " return {\"message\": \"Twilio Media Stream Server is running!\"}\n", + "\n", + "\n", + "@app.api_route(\"/incoming-call\", methods=[\"GET\", \"POST\"])\n", + "async def handle_incoming_call(request: Request):\n", + " \"\"\"Handle incoming call and return TwiML response to connect to Media Stream.\"\"\"\n", + " response = VoiceResponse()\n", + " response.say(\n", + " \"Please wait while we connect your call to the A. I. voice assistant, powered by Twilio and the Open-A.I. Realtime API\"\n", + " )\n", + " response.pause(length=1)\n", + " response.say(\"O.K. you can start talking!\")\n", + " host = request.url.hostname\n", + " connect = Connect()\n", + " connect.stream(url=f\"wss://{host}/media-stream\")\n", + " response.append(connect)\n", + " return HTMLResponse(content=str(response), media_type=\"application/xml\")\n", + "\n", + "\n", + "@app.websocket(\"/media-stream\")\n", + "async def handle_media_stream(websocket: WebSocket):\n", + " \"\"\"Handle WebSocket connections between Twilio and OpenAI.\"\"\"\n", + " await websocket.accept()\n", + "\n", + " audio_adapter = TwilioAudioAdapter(websocket)\n", + " realtime_agent = RealtimeAgent(\n", + " name=\"Customer service Bot\",\n", + " system_message=(\n", + " \"You are a helpful voice assistant. You are tasked with delegating tasks to a swarm of agents\\n\"\n", + " \"When a user calls in, you will need to ask the swarm for help to complete the task\\n\"\n", + " \"The swarm will ask questions and provide feedback to help you complete the task\\n\"\n", + " \"You can start the swarm by calling the function `start_swarm`\\n\"\n", + " \"After the swarm asks a question, you can answer it by calling the function `answer_swarm_question`\"\n", + " ),\n", + " llm_config=llm_config,\n", + " audio_adapter=audio_adapter,\n", + " )\n", + "\n", + " class SwarmTaskInput(BaseModel):\n", + " task: str\n", + "\n", + " class TaskEnd(BaseModel):\n", + " result: str\n", + "\n", + " class TaskQuestion(BaseModel):\n", + " question: str\n", + "\n", + " class TaskAnswer(BaseModel):\n", + " answer: str\n", + "\n", + " swarm_started = False\n", + "\n", + " @realtime_agent.register_handover(name=\"start_swarm\", description=\"Start a refund task using swarm intelligence\")\n", + " async def start_swarm(task_input: SwarmTaskInput) -> Union[TaskEnd, TaskQuestion]:\n", + " nonlocal swarm_started\n", + " swarm_started = True\n", + " await asyncio.sleep(2)\n", + " return TaskQuestion(\n", + " question=(\n", + " \"Need more info from user, ask him: 'What is your id number?'\\n\\n\"\n", + " \"After you get the answer from the user, call the function `answer_swarm_question` with the appropriate information to answer the question.\"\n", + " )\n", + " )\n", + "\n", + " @realtime_agent.register_handover(name=\"answer_swarm_question\", description=\"Answer a question from the swarm\")\n", + " async def answer_swarm_question(answer: TaskAnswer) -> TaskEnd:\n", + " nonlocal swarm_started\n", + " if not swarm_started:\n", + " return \"Error: Swarm not started, call start_swarm first\"\n", + " await asyncio.sleep(2)\n", + " time.sleep(2)\n", + " return TaskEnd(result=\"We gave the user a refund for the last purchase.\")\n", + "\n", + " await realtime_agent.run()\n", + "\n", + "\n", + "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" + ] } ], "metadata": { diff --git a/setup.py b/setup.py index dd433d7aad..a9f33aed97 100644 --- a/setup.py +++ b/setup.py @@ -121,7 +121,7 @@ "cohere": ["cohere>=5.5.8"], "ollama": ["ollama>=0.3.3", "fix_busted_json>=0.0.18"], "bedrock": ["boto3>=1.34.149"], - "realtime": ["websockets>=14.0"], + "realtime": ["websockets>=14.0", "asyncer>=0.0.8"], "realtime-twilio": ["fastapi>=0.115.0", "uvicorn==0.30.6", "twilio==9.3.2"], } From 1a26555214db8efc182a7b5127d6d35e8d8fdaaa Mon Sep 17 00:00:00 2001 From: Davor Runje Date: Tue, 17 Dec 2024 10:36:39 +0100 Subject: [PATCH 09/51] wip: refactoring a bit Signed-off-by: Davor Runje --- .../realtime_agent/realtime_agent.py | 2 +- notebook/agentchat_realtime_swarm.ipynb | 127 +++++++++++------- setup.py | 8 +- 3 files changed, 85 insertions(+), 52 deletions(-) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index c20dee1961..899d7bcbef 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -114,7 +114,7 @@ def check_termination_and_human_reply( loop.run_until_complete( self._client.send_text( ( - f"QUESTION FOR USER (DO NOT ANSWER, ASK THE USER, AND CALL answer_swarm_question AFTER THE USER ANSWERS): '{messages[-1]['content']}'\n\n" + f"I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, ASK THE USER AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n" ) ) ) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 994d78c3b7..17ea711773 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -22,7 +22,7 @@ ":::info Requirements\n", "Install `ag2`:\n", "```bash\n", - "pip install ag2\n", + "pip install \"ag2[realtime-twilio]\"\n", "```\n", "\n", "For more information, please refer to the [installation guide](/docs/installation/).\n", @@ -41,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -60,7 +60,40 @@ " \"config_list\": config_list,\n", " \"timeout\": 120,\n", " \"tools\": [],\n", - "}" + "}\n", + "\n", + "assert config_list, \"No LLM found for the given model\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "realtime_config_list = autogen.config_list_from_json(\n", + " \"OAI_CONFIG_LIST\",\n", + " filter_dict={\n", + " \"tags\": [\"realtime\"],\n", + " },\n", + ")\n", + "\n", + "realtime_llm_config = {\n", + " \"timeout\": 600,\n", + " \"cache_seed\": 42, # change the seed for different trials\n", + " \"config_list\": realtime_config_list,\n", + " \"temperature\": 0.8,\n", + "}\n", + "\n", + "assert realtime_config_list, (\n", + " \"No LLM found for the given model, please add the following lines to the OAI_CONFIG_LIST file:\"\n", + " \"\"\"\n", + " {\n", + " \"model\": \"gpt-4o-realtime-preview\",\n", + " \"api_key\": \"sk-***********************...*\",\n", + " \"tags\": [\"gpt-4o-realtime\", \"realtime\"]\n", + " }\"\"\"\n", + ")" ] }, { @@ -74,7 +107,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -216,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -276,7 +309,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -300,6 +333,15 @@ " agent.register_hand_off(ON_CONDITION(triage_agent, transfer_to_triage_description))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Expose your local service to the internet using ngrok\n", + "\n", + "..." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -329,41 +371,16 @@ "outputs": [], "source": [ "import asyncio\n", - "import os\n", - "import time\n", - "from concurrent.futures import ThreadPoolExecutor\n", - "from typing import Annotated, Union\n", "\n", - "import asyncer\n", "import nest_asyncio\n", "import uvicorn\n", "from fastapi import FastAPI, Request, WebSocket\n", "from fastapi.responses import HTMLResponse, JSONResponse\n", - "from pydantic import BaseModel\n", "from twilio.twiml.voice_response import Connect, VoiceResponse\n", "\n", - "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, TwilioAudioAdapter\n", + "from autogen.agentchat.realtime_agent import RealtimeAgent, TwilioAudioAdapter\n", "\n", - "# Configuration\n", - "OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\n", - "PORT = int(os.getenv(\"PORT\", 5050))\n", - "\n", - "if not OPENAI_API_KEY:\n", - " raise ValueError(\"Missing the OpenAI API key. Please set it in the .env file.\")\n", - "\n", - "realtime_llm_config = {\n", - " \"timeout\": 600,\n", - " \"cache_seed\": 45, # change the seed for different trials\n", - " \"config_list\": [\n", - " {\n", - " \"model\": \"gpt-4o-realtime-preview-2024-10-01\",\n", - " \"api_key\": OPENAI_API_KEY,\n", - " }\n", - " ],\n", - " \"temperature\": 0.8,\n", - "}\n", - "\n", - "app = FastAPI()\n", + "app = FastAPI(title=\"Realtime Swarm Agent Chat\", version=\"0.1.0\")\n", "\n", "\n", "@app.get(\"/\", response_class=JSONResponse)\n", @@ -376,7 +393,7 @@ " \"\"\"Handle incoming call and return TwiML response to connect to Media Stream.\"\"\"\n", " response = VoiceResponse()\n", " response.say(\n", - " \"Please wait while we connect your call to the A. I. voice assistant, powered by Twilio and the Open-A.I. Realtime API\"\n", + " \"Please wait while we connect your call to the AI voice assistant, powered by Twilio and the Open-A.I. Realtime API\"\n", " )\n", " response.pause(length=1)\n", " response.say(\"O.K. you can start talking!\")\n", @@ -396,14 +413,10 @@ " realtime_agent = RealtimeAgent(\n", " name=\"Customer_service_Bot\",\n", " system_message=(\n", - " \"You are a helpful voice assistant. You are tasked with delegating tasks to a swarm of agents\\n\"\n", - " \"When a user calls in, you will need to ask the swarm for help to complete the task\\n\"\n", - " \"The swarm will ask questions and provide feedback to help you complete the task\\n\"\n", - " \"You can start the swarm by calling the function `start_swarm`\\n\"\n", - " \"The swarm will sometimes ask questions, you can answer them by calling the function `answer_swarm_question`\\n\"\n", - " \"The questions will start wih the phrase 'QUESTION FOR USER (DO NOT ANSWER, ASK THE USER, AND CALL answer_swarm_question AFTER THE USER ANSWERS):'\\n\"\n", - " \"DO NOT CALL THE FUNCTION UNTIL YOU HAVE THE ANSWER FROM THE USER.\",\n", - " \"Pay close attention to the function signature and the expected input. The function signature is `answer_swarm_question` and the expected input is a string.\",\n", + " \"You are a helpful voice assistant. Your task is to listen to user and to create tasks based on his/her inputs.\\n\"\n", + " \"You have to assume that every task can be successfully completed by the swarm of agents and your only role is to create tasks for them.\\n\"\n", + " \"Do not assume that agents cannot complete the task, unless you create a task and the agents specifically instruct you that the task is impossible to solve by them.\\n\"\n", + " \"Do not create unethical or illegal tasks.\\n\"\n", " ),\n", " llm_config=realtime_llm_config,\n", " audio_adapter=audio_adapter,\n", @@ -411,8 +424,8 @@ "\n", " swarm_started = False\n", "\n", - " @realtime_agent.register_handover(name=\"start_swarm\", description=\"Start a task using swarm intelligence\")\n", - " async def start_swarm(task_input: str) -> str:\n", + " @realtime_agent.register_handover(name=\"create_task\", description=\"Create a task given by the user\")\n", + " async def create_task(task_input: str) -> str:\n", " nonlocal swarm_started\n", " swarm_started = True\n", " # chat_history, context_variables, last_agent = await\n", @@ -428,11 +441,13 @@ " )\n", " return \"Swarm started\"\n", "\n", - " @realtime_agent.register_handover(name=\"answer_swarm_question\", description=\"Answer a question from the swarm\")\n", - " async def answer_swarm_question(answer: str) -> str:\n", + " @realtime_agent.register_handover(\n", + " name=\"answer_question_about_task\", description=\"Answer a question from a swarm agent executing a task\"\n", + " )\n", + " async def answer_question_about_task(answer: str) -> str:\n", " nonlocal swarm_started\n", " if not swarm_started:\n", - " return \"Error: Swarm not started, call start_swarm first\"\n", + " return \"Error: Swarm not started, call create_task first\"\n", " realtime_agent.answer.set_result(answer)\n", " return \"Answered swarm question\"\n", "\n", @@ -440,8 +455,22 @@ "\n", "\n", "nest_asyncio.apply()\n", - "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" + "uvicorn.run(app, host=\"0.0.0.0\", port=5050)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Twilio setup and stuff" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -468,7 +497,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.16" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/setup.py b/setup.py index a9f33aed97..fb8241e250 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,10 @@ "llma-index-core==0.11.8", ] +realtime = ["websockets>=14.0", "asyncer>=0.0.8"] +realtime_twilio = [*realtime, "fastapi>=0.115.0,<1", "uvicorn>=0.30.6,<1", "twilio>=9.3.2"] + + if current_os in ["Windows", "Darwin"]: retrieve_chat_pgvector.extend(["psycopg[binary]>=3.1.18"]) elif current_os == "Linux": @@ -121,8 +125,8 @@ "cohere": ["cohere>=5.5.8"], "ollama": ["ollama>=0.3.3", "fix_busted_json>=0.0.18"], "bedrock": ["boto3>=1.34.149"], - "realtime": ["websockets>=14.0", "asyncer>=0.0.8"], - "realtime-twilio": ["fastapi>=0.115.0", "uvicorn==0.30.6", "twilio==9.3.2"], + "realtime": realtime, + "realtime-twilio": realtime_twilio, } setuptools.setup( From 0b3aa5076fc659979d8a95367a01b4d6a92cc0af Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Tue, 17 Dec 2024 11:40:47 +0100 Subject: [PATCH 10/51] WIP --- autogen/agentchat/realtime_agent/client.py | 2 +- .../realtime_agent/realtime_agent.py | 4 +-- notebook/agentchat_realtime_swarm.ipynb | 25 ++++++++----------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py index 2b1ade89f0..fb3bc05be7 100644 --- a/autogen/agentchat/realtime_agent/client.py +++ b/autogen/agentchat/realtime_agent/client.py @@ -57,7 +57,7 @@ async def initialize_session(self): "turn_detection": {"type": "server_vad"}, "voice": self._agent.voice, "instructions": self._agent.system_message, - "modalities": ["text", "audio"], + "modalities": ["audio"], "temperature": 0.8, } await self.session_update(session_update) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 899d7bcbef..a654c7d1d9 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -114,7 +114,7 @@ def check_termination_and_human_reply( loop.run_until_complete( self._client.send_text( ( - f"I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, ASK THE USER AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n" + f"I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, INFORM THE USER **WITH AUDIO** AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n" ) ) ) @@ -123,4 +123,4 @@ async def get_input(): input_text = await self.answer return input_text - return True, loop.run_until_complete(get_input()) + return loop.run_until_complete(get_input()) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 17ea711773..517d2f4e47 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -41,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -67,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -107,7 +107,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -249,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -309,7 +309,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -392,9 +392,7 @@ "async def handle_incoming_call(request: Request):\n", " \"\"\"Handle incoming call and return TwiML response to connect to Media Stream.\"\"\"\n", " response = VoiceResponse()\n", - " response.say(\n", - " \"Please wait while we connect your call to the AI voice assistant, powered by Twilio and the Open-A.I. Realtime API\"\n", - " )\n", + " response.say(\"Please wait while we connect your call to the AI voice assistant.\")\n", " response.pause(length=1)\n", " response.say(\"O.K. you can start talking!\")\n", " host = request.url.hostname\n", @@ -416,6 +414,7 @@ " \"You are a helpful voice assistant. Your task is to listen to user and to create tasks based on his/her inputs.\\n\"\n", " \"You have to assume that every task can be successfully completed by the swarm of agents and your only role is to create tasks for them.\\n\"\n", " \"Do not assume that agents cannot complete the task, unless you create a task and the agents specifically instruct you that the task is impossible to solve by them.\\n\"\n", + " \"If the task is done, you can end the conversation with the task after informing the user that the task is completed.\\n\"\n", " \"Do not create unethical or illegal tasks.\\n\"\n", " ),\n", " llm_config=realtime_llm_config,\n", @@ -441,14 +440,12 @@ " )\n", " return \"Swarm started\"\n", "\n", - " @realtime_agent.register_handover(\n", - " name=\"answer_question_about_task\", description=\"Answer a question from a swarm agent executing a task\"\n", - " )\n", - " async def answer_question_about_task(answer: str) -> str:\n", + " @realtime_agent.register_handover(name=\"answer_question_about_task\", description=\"Answer a question from a task\")\n", + " async def answer_question_about_task(end_conversation: bool, answer: str) -> str:\n", " nonlocal swarm_started\n", " if not swarm_started:\n", " return \"Error: Swarm not started, call create_task first\"\n", - " realtime_agent.answer.set_result(answer)\n", + " realtime_agent.answer.set_result(end_conversation, answer)\n", " return \"Answered swarm question\"\n", "\n", " await realtime_agent.run()\n", @@ -497,7 +494,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.16" } }, "nbformat": 4, From 995f0fd84d1fe65549bd8f3f291066ccda755d6d Mon Sep 17 00:00:00 2001 From: Davor Runje Date: Tue, 17 Dec 2024 15:00:48 +0100 Subject: [PATCH 11/51] wip: refactoring Signed-off-by: Davor Runje --- autogen/agentchat/realtime_agent/client.py | 37 ++++- .../realtime_agent/realtime_agent.py | 128 +++++++++++++++--- notebook/agentchat_realtime_swarm.ipynb | 94 +++++++------ ...io.ipynb => agentchat_realtime_tool.ipynb} | 122 ++++------------- 4 files changed, 222 insertions(+), 159 deletions(-) rename notebook/{realtime_agent_twillio.ipynb => agentchat_realtime_tool.ipynb} (50%) diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py index fb3bc05be7..e8014ede92 100644 --- a/autogen/agentchat/realtime_agent/client.py +++ b/autogen/agentchat/realtime_agent/client.py @@ -5,11 +5,14 @@ # Portions derived from https://github.com/microsoft/autogen are under the MIT License. # SPDX-License-Identifier: MIT -import asyncio +# import asyncio import json from abc import ABC, abstractmethod +from typing import Any, Optional +import anyio import websockets +from asyncer import TaskGroup, asyncify, create_task_group, syncify from .function_observer import FunctionObserver @@ -22,6 +25,17 @@ def __init__(self, agent, audio_adapter, function_observer: FunctionObserver): self.register(audio_adapter) self.register(function_observer) + # LLM config + llm_config = self._agent.llm_config + config = llm_config["config_list"][0] + + self.model = config["model"] + self.temperature = llm_config["temperature"] + self.api_key = config["api_key"] + + # create a task group to manage the tasks + self.tg: Optional[TaskGroup] = None + def register(self, observer): observer.register_client(self) self._observers.append(observer) @@ -42,7 +56,7 @@ async def function_result(self, call_id, result): await self._openai_ws.send(json.dumps(result_item)) await self._openai_ws.send(json.dumps({"type": "response.create"})) - async def send_text(self, text): + async def send_text(self, text: str): text_item = { "type": "conversation.item.create", "item": {"type": "message", "role": "system", "content": [{"type": "input_text", "text": text}]}, @@ -54,6 +68,7 @@ async def send_text(self, text): async def initialize_session(self): """Control initial session with OpenAI.""" session_update = { + # todo: move to config "turn_detection": {"type": "server_vad"}, "voice": self._agent.voice, "instructions": self._agent.system_message, @@ -65,8 +80,9 @@ async def initialize_session(self): # todo override in specific clients async def session_update(self, session_options): update = {"type": "session.update", "session": session_options} - print("Sending session update:", json.dumps(update)) + print("Sending session update:", json.dumps(update), flush=True) await self._openai_ws.send(json.dumps(update)) + print("Sending session update finished", flush=True) async def _read_from_client(self): try: @@ -77,14 +93,21 @@ async def _read_from_client(self): print(f"Error in _read_from_client: {e}") async def run(self): - async with websockets.connect( - "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01", + f"wss://api.openai.com/v1/realtime?model={self.model}", additional_headers={ - "Authorization": f"Bearer {self._agent.llm_config['config_list'][0]['api_key']}", + "Authorization": f"Bearer {self.api_key}", "OpenAI-Beta": "realtime=v1", }, ) as openai_ws: self._openai_ws = openai_ws await self.initialize_session() - await asyncio.gather(self._read_from_client(), *[observer.run() for observer in self._observers]) + # await asyncio.gather(self._read_from_client(), *[observer.run() for observer in self._observers]) + async with create_task_group() as tg: + self.tg = tg + self.tg.soonify(self._read_from_client)() + for observer in self._observers: + self.tg.soonify(observer.run)() + + def run_task(self, task, *args: Any, **kwargs: Any): + self.tg.soonify(task)(*args, **kwargs) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index a654c7d1d9..646d7388a5 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -5,14 +5,17 @@ # Portions derived from https://github.com/microsoft/autogen are under the MIT License. # SPDX-License-Identifier: MIT -import asyncio +# import asyncio import json +import logging from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, TypeVar, Union +from typing import Any, Callable, Dict, Generator, List, Literal, Optional, Tuple, TypeVar, Union -import asyncer +import anyio import websockets +from asyncer import TaskGroup, asyncify, create_task_group, syncify +from autogen import ON_CONDITION, AfterWorkOption, SwarmAgent, initiate_swarm_chat from autogen.agentchat.agent import Agent, LLMAgent from autogen.agentchat.conversable_agent import ConversableAgent from autogen.function_utils import get_function_schema @@ -23,10 +26,23 @@ F = TypeVar("F", bound=Callable[..., Any]) +logger = logging.getLogger(__name__) + +SWARM_SYSTEM_MESSAGE = ( + "You are a helpful voice assistant. Your task is to listen to user and to create tasks based on his/her inputs. E.g. if a user wishes to make change to his flight, you should create a task for it.\n" + "DO NOT ask any additional information about the task from the user. You should assume that the user has provided all the necessary information and that the agents executing the task have all the contextual information needed.\n" + "You have to assume that every task can be successfully completed by the swarm of agents and your only role is to create tasks for them.\n" + "While the task is being executed, please keep the user on the line and inform him/her about the progress by calling the 'get_task_status' function. You might also get additional questions or status reports from the agents working on the task.\n" + "Do not assume that agents cannot complete the task, unless you create a task and the agents specifically instruct you that the task is impossible to solve by them.\n" + "Once the task is done, inform the user that the task is completed and ask if you can help with anything else.\n" + "Do not create unethical or illegal tasks.\n" +) + class RealtimeAgent(ConversableAgent): def __init__( self, + *, name: str, audio_adapter: RealtimeObserver, system_message: Optional[Union[str, List]] = "You are a helpful AI Assistant.", @@ -68,6 +84,51 @@ def __init__( [Agent, None], RealtimeAgent.check_termination_and_human_reply, remove_other_reply_funcs=True ) + self._answer_event: anyio.Event = anyio.Event() + self._answer: str = "" + self.register_handover(name="Answer question", description="Answer question from the task")(self.set_answer) + + def register_swarm( + self, + *, + initial_agent: SwarmAgent, + agents: List[SwarmAgent], + system_message: Optional[str] = None, + ) -> None: + if not system_message: + if self.system_message != "You are a helpful AI Assistant.": + logger.warning( + "Overriding system message set up in `__init__`, please use `system_message` parameter of the `register_swarm` function instead." + ) + system_message = SWARM_SYSTEM_MESSAGE + + self._oai_system_message = [{"content": system_message, "role": "system"}] + + @self.register_handover(name="create_task", description="Create a task given by the user") + async def create_task(task_input: str) -> str: + self._client.run_task( + asyncify(initiate_swarm_chat), + initial_agent=initial_agent, + agents=agents, + user_agent=self, + messages=task_input, + after_work=AfterWorkOption.REVERT_TO_USER, + ) + + def _get_task_status(task_id: str) -> Generator[None, str, None]: + while True: + for s in [ + "The task is in progress, agents are working on it. ETA is 1 minute", + "The task is successfully completed.", + ]: + yield s + + it = _get_task_status("task_id") + + @self.register_handover(name="get_task_status", description="Get the status of the task") + async def get_task_status(task_id: str) -> str: + return next(it) + async def run(self): await self._client.run() @@ -103,24 +164,59 @@ def _decorator(func: F, name=name) -> F: return _decorator + def reset_answer(self) -> None: + self._answer_event = anyio.Event() + + def set_answer(self, answer: str) -> None: + self._answer = answer + self._answer_event.set() + + async def get_answer(self) -> str: + await self._answer_event.wait() + return self._answer + def check_termination_and_human_reply( self, messages: Optional[List[Dict]] = None, sender: Optional[Agent] = None, config: Optional[Any] = None, ) -> Tuple[bool, Union[str, None]]: - loop = asyncio.get_event_loop() - self.answer = loop.create_future() - loop.run_until_complete( - self._client.send_text( - ( - f"I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, INFORM THE USER **WITH AUDIO** AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n" - ) - ) - ) + print("check_termination_and_human_reply() entering") async def get_input(): - input_text = await self.answer - return input_text - - return loop.run_until_complete(get_input()) + async with create_task_group() as tg: + self.reset_answer() + tg.soonify(self._client.send_text)( + "I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, " + "INFORM THE USER **WITH AUDIO** AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE " + f"USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n", + ) + await self.get_answer() + + print("check_termination_and_human_reply() exiting") + + syncify(get_input)() + + return True, {"role": "user", "content": self._answer} + + # loop = asyncio.get_event_loop() + # self.answer = loop.create_future() + # # loop.run_until_complete( + # # self._client.send_text( + # # ( + # # f"I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, INFORM THE USER **WITH AUDIO** AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n" + # # ) + # # ) + # # ) + # self.client.run_task( + # self._client.send_text, + # "I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, " + # "INFORM THE USER **WITH AUDIO** AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE " + # f"USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n" + # ) + + # async def get_input(): + # input_text = await self.answer + # return input_text + + # return loop.run_until_complete(get_input()) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 517d2f4e47..98f5300faa 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -41,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -67,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -107,7 +107,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -249,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -309,7 +309,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -370,10 +370,13 @@ "metadata": {}, "outputs": [], "source": [ - "import asyncio\n", + "# import asyncio\n", + "from datetime import datetime\n", + "from time import time\n", "\n", "import nest_asyncio\n", "import uvicorn\n", + "from asyncer import asyncify, create_task_group, syncify\n", "from fastapi import FastAPI, Request, WebSocket\n", "from fastapi.responses import HTMLResponse, JSONResponse\n", "from twilio.twiml.voice_response import Connect, VoiceResponse\n", @@ -410,43 +413,14 @@ " audio_adapter = TwilioAudioAdapter(websocket)\n", " realtime_agent = RealtimeAgent(\n", " name=\"Customer_service_Bot\",\n", - " system_message=(\n", - " \"You are a helpful voice assistant. Your task is to listen to user and to create tasks based on his/her inputs.\\n\"\n", - " \"You have to assume that every task can be successfully completed by the swarm of agents and your only role is to create tasks for them.\\n\"\n", - " \"Do not assume that agents cannot complete the task, unless you create a task and the agents specifically instruct you that the task is impossible to solve by them.\\n\"\n", - " \"If the task is done, you can end the conversation with the task after informing the user that the task is completed.\\n\"\n", - " \"Do not create unethical or illegal tasks.\\n\"\n", - " ),\n", " llm_config=realtime_llm_config,\n", " audio_adapter=audio_adapter,\n", " )\n", "\n", - " swarm_started = False\n", - "\n", - " @realtime_agent.register_handover(name=\"create_task\", description=\"Create a task given by the user\")\n", - " async def create_task(task_input: str) -> str:\n", - " nonlocal swarm_started\n", - " swarm_started = True\n", - " # chat_history, context_variables, last_agent = await\n", - " asyncio.get_running_loop().run_in_executor(\n", - " None,\n", - " lambda: initiate_swarm_chat(\n", - " initial_agent=triage_agent,\n", - " agents=[triage_agent, flight_modification, flight_cancel, flight_change, lost_baggage],\n", - " user_agent=realtime_agent,\n", - " messages=task_input,\n", - " after_work=AfterWorkOption.REVERT_TO_USER,\n", - " ),\n", - " )\n", - " return \"Swarm started\"\n", - "\n", - " @realtime_agent.register_handover(name=\"answer_question_about_task\", description=\"Answer a question from a task\")\n", - " async def answer_question_about_task(end_conversation: bool, answer: str) -> str:\n", - " nonlocal swarm_started\n", - " if not swarm_started:\n", - " return \"Error: Swarm not started, call create_task first\"\n", - " realtime_agent.answer.set_result(end_conversation, answer)\n", - " return \"Answered swarm question\"\n", + " realtime_agent.register_swarm(\n", + " initial_agent=triage_agent,\n", + " agents=[triage_agent, flight_modification, flight_cancel, flight_change, lost_baggage],\n", + " )\n", "\n", " await realtime_agent.run()\n", "\n", @@ -455,6 +429,41 @@ "uvicorn.run(app, host=\"0.0.0.0\", port=5050)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -462,6 +471,13 @@ "## Twilio setup and stuff" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, diff --git a/notebook/realtime_agent_twillio.ipynb b/notebook/agentchat_realtime_tool.ipynb similarity index 50% rename from notebook/realtime_agent_twillio.ipynb rename to notebook/agentchat_realtime_tool.ipynb index db93ae5849..5b16186660 100644 --- a/notebook/realtime_agent_twillio.ipynb +++ b/notebook/agentchat_realtime_tool.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -19,12 +19,13 @@ "from twilio.twiml.voice_response import Connect, VoiceResponse\n", "\n", "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, TwilioAudioAdapter\n", - "from autogen.agentchat.realtime_agent.swarm_observer import SwarmObserver" + "\n", + "# from autogen.agentchat.realtime_agent.swarm_observer import SwarmObserver" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -50,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -58,8 +59,25 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 8, "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: Started server process [3628527]\n", + "INFO: Waiting for application startup.\n", + "INFO: Application startup complete.\n", + "INFO: Uvicorn running on http://0.0.0.0:5050 (Press CTRL+C to quit)\n", + "INFO: Shutting down\n", + "INFO: Waiting for application shutdown.\n", + "INFO: Application shutdown complete.\n", + "INFO: Finished server process [3628527]\n" + ] + } + ], "source": [ "app = FastAPI()\n", "\n", @@ -110,102 +128,12 @@ "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## With swarm questions" - ] - }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "app = FastAPI()\n", - "\n", - "\n", - "@app.get(\"/\", response_class=JSONResponse)\n", - "async def index_page():\n", - " return {\"message\": \"Twilio Media Stream Server is running!\"}\n", - "\n", - "\n", - "@app.api_route(\"/incoming-call\", methods=[\"GET\", \"POST\"])\n", - "async def handle_incoming_call(request: Request):\n", - " \"\"\"Handle incoming call and return TwiML response to connect to Media Stream.\"\"\"\n", - " response = VoiceResponse()\n", - " response.say(\n", - " \"Please wait while we connect your call to the A. I. voice assistant, powered by Twilio and the Open-A.I. Realtime API\"\n", - " )\n", - " response.pause(length=1)\n", - " response.say(\"O.K. you can start talking!\")\n", - " host = request.url.hostname\n", - " connect = Connect()\n", - " connect.stream(url=f\"wss://{host}/media-stream\")\n", - " response.append(connect)\n", - " return HTMLResponse(content=str(response), media_type=\"application/xml\")\n", - "\n", - "\n", - "@app.websocket(\"/media-stream\")\n", - "async def handle_media_stream(websocket: WebSocket):\n", - " \"\"\"Handle WebSocket connections between Twilio and OpenAI.\"\"\"\n", - " await websocket.accept()\n", - "\n", - " audio_adapter = TwilioAudioAdapter(websocket)\n", - " realtime_agent = RealtimeAgent(\n", - " name=\"Customer service Bot\",\n", - " system_message=(\n", - " \"You are a helpful voice assistant. You are tasked with delegating tasks to a swarm of agents\\n\"\n", - " \"When a user calls in, you will need to ask the swarm for help to complete the task\\n\"\n", - " \"The swarm will ask questions and provide feedback to help you complete the task\\n\"\n", - " \"You can start the swarm by calling the function `start_swarm`\\n\"\n", - " \"After the swarm asks a question, you can answer it by calling the function `answer_swarm_question`\"\n", - " ),\n", - " llm_config=llm_config,\n", - " audio_adapter=audio_adapter,\n", - " )\n", - "\n", - " class SwarmTaskInput(BaseModel):\n", - " task: str\n", - "\n", - " class TaskEnd(BaseModel):\n", - " result: str\n", - "\n", - " class TaskQuestion(BaseModel):\n", - " question: str\n", - "\n", - " class TaskAnswer(BaseModel):\n", - " answer: str\n", - "\n", - " swarm_started = False\n", - "\n", - " @realtime_agent.register_handover(name=\"start_swarm\", description=\"Start a refund task using swarm intelligence\")\n", - " async def start_swarm(task_input: SwarmTaskInput) -> Union[TaskEnd, TaskQuestion]:\n", - " nonlocal swarm_started\n", - " swarm_started = True\n", - " await asyncio.sleep(2)\n", - " return TaskQuestion(\n", - " question=(\n", - " \"Need more info from user, ask him: 'What is your id number?'\\n\\n\"\n", - " \"After you get the answer from the user, call the function `answer_swarm_question` with the appropriate information to answer the question.\"\n", - " )\n", - " )\n", - "\n", - " @realtime_agent.register_handover(name=\"answer_swarm_question\", description=\"Answer a question from the swarm\")\n", - " async def answer_swarm_question(answer: TaskAnswer) -> TaskEnd:\n", - " nonlocal swarm_started\n", - " if not swarm_started:\n", - " return \"Error: Swarm not started, call start_swarm first\"\n", - " await asyncio.sleep(2)\n", - " time.sleep(2)\n", - " return TaskEnd(result=\"We gave the user a refund for the last purchase.\")\n", - "\n", - " await realtime_agent.run()\n", - "\n", - "\n", - "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" - ] + "source": [] } ], "metadata": { @@ -224,7 +152,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.16" + "version": "3.10.13" } }, "nbformat": 4, From 85fa9079d362eec410141301e69de40d6c997c53 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Tue, 17 Dec 2024 16:20:49 +0100 Subject: [PATCH 12/51] Rework into anyio --- .../realtime_agent/realtime_agent.py | 42 +++++-------------- notebook/agentchat_realtime_swarm.ipynb | 10 ++--- 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 646d7388a5..f35d892b06 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -30,11 +30,10 @@ SWARM_SYSTEM_MESSAGE = ( "You are a helpful voice assistant. Your task is to listen to user and to create tasks based on his/her inputs. E.g. if a user wishes to make change to his flight, you should create a task for it.\n" - "DO NOT ask any additional information about the task from the user. You should assume that the user has provided all the necessary information and that the agents executing the task have all the contextual information needed.\n" + "DO NOT ask any additional information about the task from the user. Start the task as soon as possible.\n" "You have to assume that every task can be successfully completed by the swarm of agents and your only role is to create tasks for them.\n" - "While the task is being executed, please keep the user on the line and inform him/her about the progress by calling the 'get_task_status' function. You might also get additional questions or status reports from the agents working on the task.\n" - "Do not assume that agents cannot complete the task, unless you create a task and the agents specifically instruct you that the task is impossible to solve by them.\n" - "Once the task is done, inform the user that the task is completed and ask if you can help with anything else.\n" + # "While the task is being executed, please keep the user on the line and inform him/her about the progress by calling the 'get_task_status' function. You might also get additional questions or status reports from the agents working on the task.\n" + # "Once the task is done, inform the user that the task is completed and ask if you can help with anything else.\n" "Do not create unethical or illegal tasks.\n" ) @@ -86,7 +85,6 @@ def __init__( self._answer_event: anyio.Event = anyio.Event() self._answer: str = "" - self.register_handover(name="Answer question", description="Answer question from the task")(self.set_answer) def register_swarm( self, @@ -129,6 +127,10 @@ def _get_task_status(task_id: str) -> Generator[None, str, None]: async def get_task_status(task_id: str) -> str: return next(it) + self.register_handover(name="answer_task_question", description="Answer question from the task")( + self.set_answer + ) + async def run(self): await self._client.run() @@ -150,7 +152,6 @@ def _decorator(func: F, name=name) -> F: Raises: ValueError: if the function description is not provided and not propagated by a previous decorator. RuntimeError: if the LLM config is not set up before registering a function. - """ # get JSON schema for the function name = name or func.__name__ @@ -167,9 +168,10 @@ def _decorator(func: F, name=name) -> F: def reset_answer(self) -> None: self._answer_event = anyio.Event() - def set_answer(self, answer: str) -> None: + def set_answer(self, answer: str) -> str: self._answer = answer self._answer_event.set() + return "Answer set successfully." async def get_answer(self) -> str: await self._answer_event.wait() @@ -187,8 +189,8 @@ async def get_input(): async with create_task_group() as tg: self.reset_answer() tg.soonify(self._client.send_text)( - "I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, " - "INFORM THE USER **WITH AUDIO** AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE " + "I have a question/information for the user from the agent working on a task. DO NOT ANSWER YOURSELF, " + "INFORM THE USER **WITH AUDIO OUTPUT** AND THEN CALL 'answer_task_question' TO PROPAGETE THE " f"USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n", ) await self.get_answer() @@ -198,25 +200,3 @@ async def get_input(): syncify(get_input)() return True, {"role": "user", "content": self._answer} - - # loop = asyncio.get_event_loop() - # self.answer = loop.create_future() - # # loop.run_until_complete( - # # self._client.send_text( - # # ( - # # f"I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, INFORM THE USER **WITH AUDIO** AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n" - # # ) - # # ) - # # ) - # self.client.run_task( - # self._client.send_text, - # "I have a question for the user from the agent working on a task. DO NOT ANSWER YOURSELF, " - # "INFORM THE USER **WITH AUDIO** AND THEN CALL 'answer_question_about_task' TO PROPAGETE THE " - # f"USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n" - # ) - - # async def get_input(): - # input_text = await self.answer - # return input_text - - # return loop.run_until_complete(get_input()) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 98f5300faa..60353105ff 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -41,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -67,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -107,7 +107,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -249,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -309,7 +309,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ From 99ed59552e766bf2ac599b36075488eb4de1f6a7 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Tue, 17 Dec 2024 18:48:51 +0100 Subject: [PATCH 13/51] WIP: Cleanup example notebook --- .../realtime_agent/function_observer.py | 2 - .../realtime_agent/realtime_agent.py | 6 +-- notebook/agentchat_realtime_swarm.ipynb | 43 ++++++++++++------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py index 09a40dc1b9..b4855d63ab 100644 --- a/autogen/agentchat/realtime_agent/function_observer.py +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -21,7 +21,6 @@ def __init__(self, agent): async def update(self, response): if response.get("type") == "response.function_call_arguments.done": - print("!" * 50) print(f"Received event: {response['type']}", response) await self.call_function( call_id=response["call_id"], name=response["name"], kwargs=json.loads(response["arguments"]) @@ -41,7 +40,6 @@ async def call_function(self, call_id, name, kwargs): elif not isinstance(result, str): result = json.dumps(result) - print("!" * 50) print(f"Function call result: {result}") await self._client.function_result(call_id, result) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index f35d892b06..cc6deefd56 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -113,6 +113,8 @@ async def create_task(task_input: str) -> str: after_work=AfterWorkOption.REVERT_TO_USER, ) + return "Task created successfully." + def _get_task_status(task_id: str) -> Generator[None, str, None]: while True: for s in [ @@ -183,8 +185,6 @@ def check_termination_and_human_reply( sender: Optional[Agent] = None, config: Optional[Any] = None, ) -> Tuple[bool, Union[str, None]]: - print("check_termination_and_human_reply() entering") - async def get_input(): async with create_task_group() as tg: self.reset_answer() @@ -195,8 +195,6 @@ async def get_input(): ) await self.get_answer() - print("check_termination_and_human_reply() exiting") - syncify(get_input)() return True, {"role": "user", "content": self._answer} diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 60353105ff..bfdb070cd6 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -4,14 +4,27 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Swarm Orchestration with AG2 \n", + "# Swarm Orchestration with AG2 and RealtimeAgent\n", "\n", "\n", - "AG2 offers conversable agents, powered by LLMs, tools or a human, that can perform tasks collectively via an automated chat. Recently, OpenAI released a [Swarm](https://github.com/openai/swarm) framework that focuses on making agent coordination and execution lightweight. \n", + "AG2 now supports **RealtimeAgent**, a powerful agent type that connects seamlessly to OpenAI's [Realtime API](https://openai.com/index/introducing-the-realtime-api). With RealtimeAgent, you can add voice interaction and listening capabilities to your swarms, enabling dynamic and natural communication.\n", "\n", - "Now, AG2 offers a simple programming interface to build and orchestrate a swarm of agents. Please check the [Documentation](https://ag2ai.github.io/ag2/docs/topics/swarm) and [Blog](https://ag2ai.github.io/ag2/blog/2024/11/17/Swarm) for more details.\n", + "AG2 provides an intuitive programming interface to build and orchestrate swarms of agents. With RealtimeAgent, you can enhance swarm functionality, integrating real-time interactions alongside task automation. Check the [Documentation](https://ag2ai.github.io/ag2/docs/topics/swarm) and [Blog](https://ag2ai.github.io/ag2/blog/2024/11/17/Swarm) for further insights.\n", "\n", - "In this notebook, we implement OpenAI's [airline customer service example](https://github.com/openai/swarm/tree/main/examples/airline) in AG2." + "In this notebook, we implement OpenAI's [airline customer service example](https://github.com/openai/swarm/tree/main/examples/airline) in AG2 using the RealtimeAgent for enhanced interaction." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install AG2 with realtime-twillio dependencies\n", + "\n", + "To use the realtime agent we will connect it to twillio service, this tutorial was inspired by [twillio tutorial](https://www.twilio.com/en-us/blog/voice-ai-assistant-openai-realtime-api-node) for connecting to OpenAPI real-time agent.\n", + "\n", + "We have prepared a `TwillioAdapter` to enable you to connect your realtime agent to twillio service.\n", + "\n", + "To be able to run this notebook, you will need to install ag2 with additional realtime and twillio dependencies." ] }, { @@ -34,14 +47,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Set your API Endpoint\n", + "## Prepare your `llm_config` and `realtime_llm_config`\n", "\n", "The [`config_list_from_json`](https://ag2ai.github.io/ag2/docs/reference/oai/openai_utils#config_list_from_json) function loads a list of configurations from an environment variable or a json file." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -67,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -107,7 +120,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -249,11 +262,11 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ - "from autogen import ON_CONDITION, AfterWorkOption, SwarmAgent, initiate_swarm_chat\n", + "from autogen import ON_CONDITION, SwarmAgent\n", "\n", "# Triage Agent\n", "triage_agent = SwarmAgent(\n", @@ -309,7 +322,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -430,11 +443,11 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "## Contact your service over twillio" + ] }, { "cell_type": "code", From 1681bb825928e2599073e005eee2621af7a91d7e Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Tue, 17 Dec 2024 18:49:27 +0100 Subject: [PATCH 14/51] WIP: Cleanup example notebook --- notebook/agentchat_realtime_swarm.ipynb | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index bfdb070cd6..f2f2c18838 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -470,27 +470,6 @@ "outputs": [], "source": [] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Twilio setup and stuff" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "code", "execution_count": null, From 75af6b8ee74756a862cd482448d3ab482f66fe40 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 18 Dec 2024 09:49:03 +0100 Subject: [PATCH 15/51] WIP --- autogen/agentchat/realtime_agent/client.py | 10 +++ .../realtime_agent/realtime_agent.py | 68 ++++++++++--------- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py index e8014ede92..a4c78edd83 100644 --- a/autogen/agentchat/realtime_agent/client.py +++ b/autogen/agentchat/realtime_agent/client.py @@ -14,6 +14,8 @@ import websockets from asyncer import TaskGroup, asyncify, create_task_group, syncify +from autogen.agentchat.contrib.swarm_agent import AfterWorkOption, initiate_swarm_chat + from .function_observer import FunctionObserver @@ -108,6 +110,14 @@ async def run(self): self.tg.soonify(self._read_from_client)() for observer in self._observers: self.tg.soonify(observer.run)() + if self._agent._start_swarm_chat: + self.tg.soonify(asyncify(initiate_swarm_chat))( + initial_agent=self._agent._initial_agent, + agents=self._agent._agents, + user_agent=self._agent, + messages="Find out what the user wants.", + after_work=AfterWorkOption.REVERT_TO_USER, + ) def run_task(self, task, *args: Any, **kwargs: Any): self.tg.soonify(task)(*args, **kwargs) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index cc6deefd56..223e3f182e 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -6,6 +6,7 @@ # SPDX-License-Identifier: MIT # import asyncio +import asyncio import json import logging from abc import ABC, abstractmethod @@ -29,12 +30,17 @@ logger = logging.getLogger(__name__) SWARM_SYSTEM_MESSAGE = ( - "You are a helpful voice assistant. Your task is to listen to user and to create tasks based on his/her inputs. E.g. if a user wishes to make change to his flight, you should create a task for it.\n" - "DO NOT ask any additional information about the task from the user. Start the task as soon as possible.\n" - "You have to assume that every task can be successfully completed by the swarm of agents and your only role is to create tasks for them.\n" + "You are a helpful voice assistant. Your task is to listen to user and to coordinate the swarm of agents based on his/her inputs." + "The swarm will have questions for the user, and you will be responsible for asking the user these questions and relaying the answers back to the agents." + "The questions will start with 'I have a question/information for the user from the agent working on a task." + "When you get a question, inform the user with audio output and then call 'answer_task_question' to propagate the user answer to the agent working on the task." + "You can communicate and will communicate using audio output only." + # "You are a helpful voice assistant. Your task is to listen to user and to create tasks based on his/her inputs. E.g. if a user wishes to make change to his flight, you should create a task for it.\n" + # "DO NOT ask any additional information about the task from the user. Start the task as soon as possible.\n" + # "You have to assume that every task can be successfully completed by the swarm of agents and your only role is to create tasks for them.\n" # "While the task is being executed, please keep the user on the line and inform him/her about the progress by calling the 'get_task_status' function. You might also get additional questions or status reports from the agents working on the task.\n" # "Once the task is done, inform the user that the task is completed and ask if you can help with anything else.\n" - "Do not create unethical or illegal tasks.\n" + # "Do not create unethical or illegal tasks.\n" ) @@ -85,6 +91,9 @@ def __init__( self._answer_event: anyio.Event = anyio.Event() self._answer: str = "" + self._start_swarm_chat = False + self._initial_agent = None + self._agents = None def register_swarm( self, @@ -102,32 +111,23 @@ def register_swarm( self._oai_system_message = [{"content": system_message, "role": "system"}] - @self.register_handover(name="create_task", description="Create a task given by the user") - async def create_task(task_input: str) -> str: - self._client.run_task( - asyncify(initiate_swarm_chat), - initial_agent=initial_agent, - agents=agents, - user_agent=self, - messages=task_input, - after_work=AfterWorkOption.REVERT_TO_USER, - ) - - return "Task created successfully." - - def _get_task_status(task_id: str) -> Generator[None, str, None]: - while True: - for s in [ - "The task is in progress, agents are working on it. ETA is 1 minute", - "The task is successfully completed.", - ]: - yield s - - it = _get_task_status("task_id") - - @self.register_handover(name="get_task_status", description="Get the status of the task") - async def get_task_status(task_id: str) -> str: - return next(it) + self._start_swarm_chat = True + self._initial_agent = initial_agent + self._agents = agents + + # def _get_task_status(task_id: str) -> Generator[None, str, None]: + # while True: + # for s in [ + # "The task is in progress, agents are working on it. ETA is 1 minute", + # "The task is successfully completed.", + # ]: + # yield s + + # it = _get_task_status("task_id") + + # @self.register_handover(name="get_task_status", description="Get the status of the task") + # async def get_task_status(task_id: str) -> str: + # return next(it) self.register_handover(name="answer_task_question", description="Answer question from the task")( self.set_answer @@ -179,6 +179,12 @@ async def get_answer(self) -> str: await self._answer_event.wait() return self._answer + async def ask_question(self, question: str) -> str: + self.reset_answer() + while not self._answer_event.is_set(): + await self._client.send_text(question) + await asyncio.sleep(5) + def check_termination_and_human_reply( self, messages: Optional[List[Dict]] = None, @@ -188,7 +194,7 @@ def check_termination_and_human_reply( async def get_input(): async with create_task_group() as tg: self.reset_answer() - tg.soonify(self._client.send_text)( + tg.soonify(self.ask_question)( "I have a question/information for the user from the agent working on a task. DO NOT ANSWER YOURSELF, " "INFORM THE USER **WITH AUDIO OUTPUT** AND THEN CALL 'answer_task_question' TO PROPAGETE THE " f"USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n", From 5bd052d55456135089fc728715c7cdf36306a35d Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 18 Dec 2024 11:03:39 +0100 Subject: [PATCH 16/51] Add question polling --- autogen/agentchat/realtime_agent/client.py | 5 ++-- .../realtime_agent/realtime_agent.py | 28 +++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py index a4c78edd83..a92b84f2d8 100644 --- a/autogen/agentchat/realtime_agent/client.py +++ b/autogen/agentchat/realtime_agent/client.py @@ -59,9 +59,10 @@ async def function_result(self, call_id, result): await self._openai_ws.send(json.dumps({"type": "response.create"})) async def send_text(self, text: str): + # await self._openai_ws.send(json.dumps({"type": "response.cancel"})) text_item = { "type": "conversation.item.create", - "item": {"type": "message", "role": "system", "content": [{"type": "input_text", "text": text}]}, + "item": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}, } await self._openai_ws.send(json.dumps(text_item)) await self._openai_ws.send(json.dumps({"type": "response.create"})) @@ -74,7 +75,7 @@ async def initialize_session(self): "turn_detection": {"type": "server_vad"}, "voice": self._agent.voice, "instructions": self._agent.system_message, - "modalities": ["audio"], + "modalities": ["audio", "text"], "temperature": 0.8, } await self.session_update(session_update) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 223e3f182e..687dd91410 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -31,9 +31,9 @@ SWARM_SYSTEM_MESSAGE = ( "You are a helpful voice assistant. Your task is to listen to user and to coordinate the swarm of agents based on his/her inputs." - "The swarm will have questions for the user, and you will be responsible for asking the user these questions and relaying the answers back to the agents." - "The questions will start with 'I have a question/information for the user from the agent working on a task." - "When you get a question, inform the user with audio output and then call 'answer_task_question' to propagate the user answer to the agent working on the task." + # "The swarm will have questions for the user, and you will be responsible for asking the user these questions and relaying the answers back to the agents." + # "The questions will start with 'I have a question/information for the user from the agent working on a task." + # "When you get a question, inform the user with audio output and then call 'answer_task_question' to propagate the user answer to the agent working on the task." "You can communicate and will communicate using audio output only." # "You are a helpful voice assistant. Your task is to listen to user and to create tasks based on his/her inputs. E.g. if a user wishes to make change to his flight, you should create a task for it.\n" # "DO NOT ask any additional information about the task from the user. Start the task as soon as possible.\n" @@ -183,7 +183,7 @@ async def ask_question(self, question: str) -> str: self.reset_answer() while not self._answer_event.is_set(): await self._client.send_text(question) - await asyncio.sleep(5) + await asyncio.sleep(20) def check_termination_and_human_reply( self, @@ -193,14 +193,24 @@ def check_termination_and_human_reply( ) -> Tuple[bool, Union[str, None]]: async def get_input(): async with create_task_group() as tg: - self.reset_answer() tg.soonify(self.ask_question)( - "I have a question/information for the user from the agent working on a task. DO NOT ANSWER YOURSELF, " - "INFORM THE USER **WITH AUDIO OUTPUT** AND THEN CALL 'answer_task_question' TO PROPAGETE THE " - f"USER ANSWER TO THE AGENT WORKING ON THE TASK. The question is: '{messages[-1]['content']}'\n\n", + "I have a question/information for the myself. DO NOT ANSWER YOURSELF, " + "repeat the question to me **WITH AUDIO OUTPUT** and then call 'answer_task_question' AFTER YOU GET THE ANSWER FROM ME\n\n" + f"The question is: '{messages[-1]['content']}'\n\n", ) - await self.get_answer() syncify(get_input)() + # async def get_input(): + # self._client.run_task(self.ask_question, ( + # "I have a question for the myself. DO NOT ANSWER YOURSELF, " + # "repeat the question to me **WITH AUDIO OUTPUT** and then call 'answer_task_question' AFTER YOU GET THE ANSWER FROM ME\n" + # "Do not conclude anything about my answer, just call the answer_task_question function after you get the answer from me.\n\n" + # f"The question is: '{messages[-1]['content']}'\n\n", + # ) + # ) + + # syncify(get_input)() + # answer = syncify(self.get_answer)() + return True, {"role": "user", "content": self._answer} From e8fc5a24cf7cce53032fa9838f7d9037d8226011 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 18 Dec 2024 12:58:19 +0100 Subject: [PATCH 17/51] Sync question asking --- autogen/agentchat/realtime_agent/client.py | 9 ++-- .../realtime_agent/realtime_agent.py | 54 ++++++++----------- .../realtime_agent/twilio_observer.py | 5 +- notebook/agentchat_realtime_swarm.ipynb | 2 +- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py index a92b84f2d8..5b371b1a92 100644 --- a/autogen/agentchat/realtime_agent/client.py +++ b/autogen/agentchat/realtime_agent/client.py @@ -58,11 +58,11 @@ async def function_result(self, call_id, result): await self._openai_ws.send(json.dumps(result_item)) await self._openai_ws.send(json.dumps({"type": "response.create"})) - async def send_text(self, text: str): - # await self._openai_ws.send(json.dumps({"type": "response.cancel"})) + async def send_text(self, *, role: str, text: str): + await self._openai_ws.send(json.dumps({"type": "response.cancel"})) text_item = { "type": "conversation.item.create", - "item": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}, + "item": {"type": "message", "role": role, "content": [{"type": "input_text", "text": text}]}, } await self._openai_ws.send(json.dumps(text_item)) await self._openai_ws.send(json.dumps({"type": "response.create"})) @@ -119,6 +119,3 @@ async def run(self): messages="Find out what the user wants.", after_work=AfterWorkOption.REVERT_TO_USER, ) - - def run_task(self, task, *args: Any, **kwargs: Any): - self.tg.soonify(task)(*args, **kwargs) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 687dd91410..b497298a5f 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -30,17 +30,16 @@ logger = logging.getLogger(__name__) SWARM_SYSTEM_MESSAGE = ( - "You are a helpful voice assistant. Your task is to listen to user and to coordinate the swarm of agents based on his/her inputs." - # "The swarm will have questions for the user, and you will be responsible for asking the user these questions and relaying the answers back to the agents." - # "The questions will start with 'I have a question/information for the user from the agent working on a task." - # "When you get a question, inform the user with audio output and then call 'answer_task_question' to propagate the user answer to the agent working on the task." + "You are a helpful voice assistant. Your task is to listen to user and to coordinate the tasks based on his/her inputs." + "Only call the 'answer_task_question' function when you have the answer from the user." "You can communicate and will communicate using audio output only." - # "You are a helpful voice assistant. Your task is to listen to user and to create tasks based on his/her inputs. E.g. if a user wishes to make change to his flight, you should create a task for it.\n" - # "DO NOT ask any additional information about the task from the user. Start the task as soon as possible.\n" - # "You have to assume that every task can be successfully completed by the swarm of agents and your only role is to create tasks for them.\n" - # "While the task is being executed, please keep the user on the line and inform him/her about the progress by calling the 'get_task_status' function. You might also get additional questions or status reports from the agents working on the task.\n" - # "Once the task is done, inform the user that the task is completed and ask if you can help with anything else.\n" - # "Do not create unethical or illegal tasks.\n" +) + +QUESTION_ROLE = "user" +QUESTION_MESSAGE = ( + "I have a question/information for the myself. DO NOT ANSWER YOURSELF, GET THE ANSWER FROM ME. " + "repeat the question to me **WITH AUDIO OUTPUT** and then call 'answer_task_question' AFTER YOU GET THE ANSWER FROM ME\n\n" + "The question is: '{}'\n\n" ) @@ -179,11 +178,20 @@ async def get_answer(self) -> str: await self._answer_event.wait() return self._answer - async def ask_question(self, question: str) -> str: + async def ask_question(self, question: str, question_timeout: int) -> str: self.reset_answer() - while not self._answer_event.is_set(): - await self._client.send_text(question) - await asyncio.sleep(20) + await anyio.sleep(1) + await self._client.send_text(role=QUESTION_ROLE, text=question) + + async def _check_event_set(timeout: int = question_timeout) -> None: + for _ in range(timeout): + if self._answer_event.is_set(): + return True + await anyio.sleep(1) + return False + + while not await _check_event_set(): + await self._client.send_text(role=QUESTION_ROLE, text=question) def check_termination_and_human_reply( self, @@ -193,24 +201,8 @@ def check_termination_and_human_reply( ) -> Tuple[bool, Union[str, None]]: async def get_input(): async with create_task_group() as tg: - tg.soonify(self.ask_question)( - "I have a question/information for the myself. DO NOT ANSWER YOURSELF, " - "repeat the question to me **WITH AUDIO OUTPUT** and then call 'answer_task_question' AFTER YOU GET THE ANSWER FROM ME\n\n" - f"The question is: '{messages[-1]['content']}'\n\n", - ) + tg.soonify(self.ask_question)(QUESTION_MESSAGE.format(messages[-1]["content"]), 20) syncify(get_input)() - # async def get_input(): - # self._client.run_task(self.ask_question, ( - # "I have a question for the myself. DO NOT ANSWER YOURSELF, " - # "repeat the question to me **WITH AUDIO OUTPUT** and then call 'answer_task_question' AFTER YOU GET THE ANSWER FROM ME\n" - # "Do not conclude anything about my answer, just call the answer_task_question function after you get the answer from me.\n\n" - # f"The question is: '{messages[-1]['content']}'\n\n", - # ) - # ) - - # syncify(get_input)() - # answer = syncify(self.get_answer)() - return True, {"role": "user", "content": self._answer} diff --git a/autogen/agentchat/realtime_agent/twilio_observer.py b/autogen/agentchat/realtime_agent/twilio_observer.py index 04232f13a7..da950f27d6 100644 --- a/autogen/agentchat/realtime_agent/twilio_observer.py +++ b/autogen/agentchat/realtime_agent/twilio_observer.py @@ -26,7 +26,7 @@ class TwilioAudioAdapter(RealtimeObserver): - def __init__(self, websocket): + def __init__(self, websocket, log_events=False): super().__init__() self.websocket = websocket @@ -36,10 +36,11 @@ def __init__(self, websocket): self.last_assistant_item = None self.mark_queue = [] self.response_start_timestamp_twilio = None + self.log_events = log_events async def update(self, response): """Receive events from the OpenAI Realtime API, send audio back to Twilio.""" - if response["type"] in LOG_EVENT_TYPES: + if response["type"] in LOG_EVENT_TYPES and self.log_events: print(f"Received event: {response['type']}", response) if response.get("type") == "response.audio.delta" and "delta" in response: diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index f2f2c18838..3697b185e2 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -423,7 +423,7 @@ " \"\"\"Handle WebSocket connections between Twilio and OpenAI.\"\"\"\n", " await websocket.accept()\n", "\n", - " audio_adapter = TwilioAudioAdapter(websocket)\n", + " audio_adapter = TwilioAudioAdapter(websocket, log_events=False)\n", " realtime_agent = RealtimeAgent(\n", " name=\"Customer_service_Bot\",\n", " llm_config=realtime_llm_config,\n", From 1201f0cf19e6c8140ef37c032b099c852ad7394d Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 18 Dec 2024 13:58:52 +0100 Subject: [PATCH 18/51] Replace prints with logging --- autogen/agentchat/realtime_agent/client.py | 9 ++++--- .../realtime_agent/function_observer.py | 7 ++++-- .../realtime_agent/twilio_observer.py | 24 ++++++++++--------- notebook/agentchat_realtime_swarm.ipynb | 3 ++- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py index 5b371b1a92..86c65a3452 100644 --- a/autogen/agentchat/realtime_agent/client.py +++ b/autogen/agentchat/realtime_agent/client.py @@ -7,6 +7,7 @@ # import asyncio import json +import logging from abc import ABC, abstractmethod from typing import Any, Optional @@ -18,6 +19,8 @@ from .function_observer import FunctionObserver +logger = logging.getLogger(__name__) + class Client(ABC): def __init__(self, agent, audio_adapter, function_observer: FunctionObserver): @@ -83,9 +86,9 @@ async def initialize_session(self): # todo override in specific clients async def session_update(self, session_options): update = {"type": "session.update", "session": session_options} - print("Sending session update:", json.dumps(update), flush=True) + logger.info("Sending session update:", json.dumps(update)) await self._openai_ws.send(json.dumps(update)) - print("Sending session update finished", flush=True) + logger.info("Sending session update finished") async def _read_from_client(self): try: @@ -93,7 +96,7 @@ async def _read_from_client(self): response = json.loads(openai_message) await self.notify_observers(response) except Exception as e: - print(f"Error in _read_from_client: {e}") + logger.warning(f"Error in _read_from_client: {e}") async def run(self): async with websockets.connect( diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py index b4855d63ab..777fbf993e 100644 --- a/autogen/agentchat/realtime_agent/function_observer.py +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -7,12 +7,15 @@ import asyncio import json +import logging from asyncer import asyncify from pydantic import BaseModel from .realtime_observer import RealtimeObserver +logger = logging.getLogger(__name__) + class FunctionObserver(RealtimeObserver): def __init__(self, agent): @@ -21,7 +24,7 @@ def __init__(self, agent): async def update(self, response): if response.get("type") == "response.function_call_arguments.done": - print(f"Received event: {response['type']}", response) + logger.info(f"Received event: {response['type']}", response) await self.call_function( call_id=response["call_id"], name=response["name"], kwargs=json.loads(response["arguments"]) ) @@ -34,13 +37,13 @@ async def call_function(self, call_id, name, kwargs): result = await func(**kwargs) except Exception: result = "Function call failed" + logger.warning(f"Function call failed: {name}") if isinstance(result, BaseModel): result = result.model_dump_json() elif not isinstance(result, str): result = json.dumps(result) - print(f"Function call result: {result}") await self._client.function_result(call_id, result) async def run(self): diff --git a/autogen/agentchat/realtime_agent/twilio_observer.py b/autogen/agentchat/realtime_agent/twilio_observer.py index da950f27d6..6364fa99be 100644 --- a/autogen/agentchat/realtime_agent/twilio_observer.py +++ b/autogen/agentchat/realtime_agent/twilio_observer.py @@ -7,6 +7,7 @@ import base64 import json +import logging from fastapi import WebSocketDisconnect @@ -24,9 +25,11 @@ ] SHOW_TIMING_MATH = False +logger = logging.getLogger(__name__) + class TwilioAudioAdapter(RealtimeObserver): - def __init__(self, websocket, log_events=False): + def __init__(self, websocket): super().__init__() self.websocket = websocket @@ -36,12 +39,11 @@ def __init__(self, websocket, log_events=False): self.last_assistant_item = None self.mark_queue = [] self.response_start_timestamp_twilio = None - self.log_events = log_events async def update(self, response): """Receive events from the OpenAI Realtime API, send audio back to Twilio.""" - if response["type"] in LOG_EVENT_TYPES and self.log_events: - print(f"Received event: {response['type']}", response) + if response["type"] in LOG_EVENT_TYPES: + logger.info(f"Received event: {response['type']}", response) if response.get("type") == "response.audio.delta" and "delta" in response: audio_payload = base64.b64encode(base64.b64decode(response["delta"])).decode("utf-8") @@ -51,7 +53,7 @@ async def update(self, response): if self.response_start_timestamp_twilio is None: self.response_start_timestamp_twilio = self.latest_media_timestamp if SHOW_TIMING_MATH: - print(f"Setting start timestamp for new response: {self.response_start_timestamp_twilio}ms") + logger.info(f"Setting start timestamp for new response: {self.response_start_timestamp_twilio}ms") # Update last_assistant_item safely if response.get("item_id"): @@ -61,24 +63,24 @@ async def update(self, response): # Trigger an interruption. Your use case might work better using `input_audio_buffer.speech_stopped`, or combining the two. if response.get("type") == "input_audio_buffer.speech_started": - print("Speech started detected.") + logger.info("Speech started detected.") if self.last_assistant_item: - print(f"Interrupting response with id: {self.last_assistant_item}") + logger.info(f"Interrupting response with id: {self.last_assistant_item}") await self.handle_speech_started_event() async def handle_speech_started_event(self): """Handle interruption when the caller's speech starts.""" - print("Handling speech started event.") + logger.info("Handling speech started event.") if self.mark_queue and self.response_start_timestamp_twilio is not None: elapsed_time = self.latest_media_timestamp - self.response_start_timestamp_twilio if SHOW_TIMING_MATH: - print( + logger.info( f"Calculating elapsed time for truncation: {self.latest_media_timestamp} - {self.response_start_timestamp_twilio} = {elapsed_time}ms" ) if self.last_assistant_item: if SHOW_TIMING_MATH: - print(f"Truncating item with ID: {self.last_assistant_item}, Truncated at: {elapsed_time}ms") + logger.info(f"Truncating item with ID: {self.last_assistant_item}, Truncated at: {elapsed_time}ms") truncate_event = { "type": "conversation.item.truncate", @@ -112,7 +114,7 @@ async def run(self): await openai_ws.send(json.dumps(audio_append)) elif data["event"] == "start": self.stream_sid = data["start"]["streamSid"] - print(f"Incoming stream has started {self.stream_sid}") + logger.info(f"Incoming stream has started {self.stream_sid}") self.response_start_timestamp_twilio = None self.latest_media_timestamp = 0 self.last_assistant_item = None diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 3697b185e2..d36996c1ab 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -384,6 +384,7 @@ "outputs": [], "source": [ "# import asyncio\n", + "import logging\n", "from datetime import datetime\n", "from time import time\n", "\n", @@ -423,7 +424,7 @@ " \"\"\"Handle WebSocket connections between Twilio and OpenAI.\"\"\"\n", " await websocket.accept()\n", "\n", - " audio_adapter = TwilioAudioAdapter(websocket, log_events=False)\n", + " audio_adapter = TwilioAudioAdapter(websocket)\n", " realtime_agent = RealtimeAgent(\n", " name=\"Customer_service_Bot\",\n", " llm_config=realtime_llm_config,\n", From 90e0516c351975d1b70f9f0ec7c380e552bed8e4 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 18 Dec 2024 15:45:54 +0000 Subject: [PATCH 19/51] Init RealtimeAgent blogpost --- notebook/agentchat_realtime_swarm.ipynb | 64 ++---- .../blog/2024-12-18-RealtimeAgent/index.mdx | 215 ++++++++++++++++++ website/blog/authors.yml | 6 + 3 files changed, 236 insertions(+), 49 deletions(-) create mode 100644 website/blog/2024-12-18-RealtimeAgent/index.mdx diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index d36996c1ab..0351ab22e4 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -7,7 +7,7 @@ "# Swarm Orchestration with AG2 and RealtimeAgent\n", "\n", "\n", - "AG2 now supports **RealtimeAgent**, a powerful agent type that connects seamlessly to OpenAI's [Realtime API](https://openai.com/index/introducing-the-realtime-api). With RealtimeAgent, you can add voice interaction and listening capabilities to your swarms, enabling dynamic and natural communication.\n", + "AG2 supports **RealtimeAgent**, a powerful agent type that connects seamlessly to OpenAI's [Realtime API](https://openai.com/index/introducing-the-realtime-api). With RealtimeAgent, you can add voice interaction and listening capabilities to your swarms, enabling dynamic and natural communication.\n", "\n", "AG2 provides an intuitive programming interface to build and orchestrate swarms of agents. With RealtimeAgent, you can enhance swarm functionality, integrating real-time interactions alongside task automation. Check the [Documentation](https://ag2ai.github.io/ag2/docs/topics/swarm) and [Blog](https://ag2ai.github.io/ag2/blog/2024/11/17/Swarm) for further insights.\n", "\n", @@ -359,22 +359,25 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Run the code\n", + "# Running the Code\n", "\n", - "Finally, call `initiate_swarm_chat` to start the conversation. \n", + "This code sets up the FastAPI server for the RealtimeAgent, enabling it to handle real-time voice interactions through Twilio. By executing this code, you’ll start the server and make it accessible for testing voice calls.\n", "\n", - "For this example, it requires human interaction with the agents. So we pass in a `user` agent, and set the `after_work` parameter to `AfterWorkOption.REVERT_TO_USER`. This means, each time a swarm agent finishes its work, the conversation will be reverted back to the user to give the next input.\n", + "Here’s what happens when you run the code: \n", "\n", - "> You need to interact with the agents for this example. (You can try different inputs to see how they react!)\n", + "1. **Server Initialization**: A FastAPI application is started, ready to process requests and WebSocket connections. \n", + "2. **Incoming Call Handling**: The `/incoming-call` route processes incoming calls from Twilio, providing a TwiML response to connect the call to a real-time AI assistant. \n", + "3. **WebSocket Integration**: The `/media-stream` WebSocket endpoint bridges the connection between Twilio’s media stream and OpenAI’s Realtime API through the RealtimeAgent. \n", + "4. **RealtimeAgent Configuration**: The RealtimeAgent registers a swarm of agents (e.g., `triage_agent`, `flight_modification`) to handle complex tasks during the call. \n", "\n", - "Here is a sequence of messages entered in this example:\n", "\n", - "To initialize the conversation, we pass `I want to cancel flight` to the `initiate_swarm_chat` function.\n", - "1. `please cancel it` (Ask for reason but don't provide one)\n", - "2. `1919` (The flight number)\n", - "3. `I want flight credits`\n", - "4. `No` (No further questions)\n", - "5. `exit` (End the conversation)" + "# How to Execute\n", + "\n", + "1. **Run the Code**: Execute the provided code block in your Python environment (such as a Jupyter Notebook or directly in a Python script). \n", + "2. **Start the Server**: The server will listen for requests on port `5050`. You can access the root URL (`http://localhost:5050/`) to confirm the server is running. \n", + "3. **Connect Twilio**: Use a tool like **ngrok** to expose the server to the public internet, then configure Twilio to route calls to the public URL. \n", + "\n", + "Once the server is running, you can connect a Twilio phone call to the AI assistant and test the RealtimeAgent’s capabilities!" ] }, { @@ -383,8 +386,6 @@ "metadata": {}, "outputs": [], "source": [ - "# import asyncio\n", - "import logging\n", "from datetime import datetime\n", "from time import time\n", "\n", @@ -442,41 +443,6 @@ "nest_asyncio.apply()\n", "uvicorn.run(app, host=\"0.0.0.0\", port=5050)" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Contact your service over twillio" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx new file mode 100644 index 0000000000..a80aa27df8 --- /dev/null +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -0,0 +1,215 @@ +--- +title: Introducing RealtimeAgent Capabilities in AG2 +authors: + - marklysze + - sternakt + - davorrunje + - davorinrusevljan +tags: [Realtime API, Voice Agents, Swarm Teams, Twilio, AI Tools] + +--- + +**TL;DR:** +- **RealtimeAgent** is coming in the next AG2 release, enabling real-time conversational AI. +- Features include real-time voice interactions, seamless task delegation to Swarm teams, and Twilio-based telephony integration. +- Learn how to integrate twillio and RealtimeAgent into your swarm in this blogpost + +### **Realtime API Support: What's New?** + +We're thrilled to announce the release of **RealtimeAgent**, extending AG2's capabilities to support **real-time conversational AI tasks**. This new feature makes it possible for developers to build agents capable of handling voice-based interactions with minimal latency, integrating OpenAI’s Realtime API, Twilio for telephony, and AutoGen’s Swarm task orchestration. + +### **Why Realtime API Support Matters** + +Traditionally, conversational AI tasks have focused on asynchronous interactions, such as text-based chats. However, the demand for **real-time voice agents** has surged in domains like customer support, healthcare, and virtual assistance. With this update, AG2 takes a leap forward by enabling agents to: + +1. **Support Real-Time Voice Interactions** + Engage in real-time conversations with users through seamless integration with Twilio telephony. + +2. **Leverage Swarm Teams for Task Delegation** + Delegate complex tasks to AutoGen Swarm teams during a voice interaction, ensuring efficient task management. + +3. **Provide Developer-Friendly Integration** + Tutorial and streamlined API to make setting up real-time agents more straightforward for developers. + + +### **Key Features of RealtimeAgent and RealtimeOpenAIClient** + +#### **1. RealtimeAgent** +- Acts as the central interface for handling real-time interactions. +- Bridges voice input/output with AutoGen’s task-handling capabilities. + +#### **3. TwilioPhoneConnector** +- Connects agents to Twilio for telephony support. +- Simplifies the process of handling voice calls with clear API methods. + + +### **Real-World Applications** + +Here’s how RealtimeAgent transforms use cases: + +- **Customer Support**: Enable agents to answer customer queries in real time while delegating complex tasks to a Swarm team. +- **Healthcare**: Facilitate real-time interactions between patients and medical AI assistants with immediate task handoffs. +- **Virtual Assistance**: Create voice-activated personal assistants that can handle scheduling, reminders, and more. + + +### **Tutorial: Integrating RealtimeAgent with Swarm Teams** + +In this tutorial, we’ll demonstrate how to implement OpenAI's [Airline Customer Service Example](https://github.com/openai/swarm/tree/main/examples/airline) using AG2's new **RealtimeAgent**. By leveraging real-time capabilities, you can create a seamless voice-driven experience for customer service tasks, enhanced with Swarm's task orchestration for efficient problem-solving. + +This guide will walk you through the setup, implementation, and connection process, ensuring you’re ready to build real-time conversational agents with ease. + + +#### **Prerequisites** +Before we begin, ensure you have the following set up: + +1. **Ngrok**: Exposing your local service to the web for Twilio integration. +2. **Twilio**: Setting up Twilio for telephony connectivity. + +#### Ngrok setup + +To enable Twilio to interact with your local server, you’ll need to expose it to the public internet. Twilio requires a public URL to send requests to your server and receive real-time instructions. + +For this guide, we’ll use **ngrok**, a popular tunneling service, to make your local server accessible. Alternatively, you can use other reverse proxy or tunneling options, such as a virtual private server (VPS). + +##### **Step 1: Install Ngrok** +If you haven’t already, download and install **ngrok** from their [official website](https://ngrok.com/download). Follow the instructions for your operating system to set it up. + +##### **Step 2: Start the Tunnel** +Run the following command to expose your local server on port `5050` (or the port your server uses): + +```bash +ngrok http 5050 +``` + +If you’ve configured your server to use a different port, replace `5050` with the correct port number in the command. + +##### **Step 3: Retrieve Your Public URL** +After running the command, **ngrok** will display a public URL in your terminal. It will look something like this: + +```plaintext +Forwarding https://abc123.ngrok.io -> http://localhost:5050 +``` + +Copy this public URL (e.g., `https://abc123.ngrok.io`). You will use it in Twilio’s configuration to route incoming requests to your server. + +With your public URL set up, you’re ready to configure Twilio to send requests to your server! + +#### Twilio setup + +### **Swarm Implementation for Airline Customer Service** +Here, we will build and configure a Swarm to manage common airline customer service tasks, such as booking changes, seat upgrades, or handling complaints. + +### **Connecting the Swarm to the RealtimeAgent** + +In this section, we will connect the Swarm to the **RealtimeAgent**, enabling real-time voice interaction and task delegation. To achieve this, we use FastAPI to create a lightweight server that acts as a bridge between Twilio and the RealtimeAgent. + +The key functionalities of this implementation include: + +1. **Setting Up a REST Endpoint** + We define a REST API endpoint, `/incoming-call`, to handle incoming voice calls from Twilio. This endpoint returns a Twilio Markup Language (TwiML) response, connecting the call to Twilio’s Media Stream for real-time audio data transfer. + +```python +app.api_route("/incoming-call", methods=["GET", "POST"]) +async def handle_incoming_call(request: Request): + """Handle incoming call and return TwiML response to connect to Media Stream.""" + response = VoiceResponse() + response.say("Please wait while we connect your call to the AI voice assistant.") + response.pause(length=1) + response.say("O.K. you can start talking!") + host = request.url.hostname + connect = Connect() + connect.stream(url=f"wss://{host}/media-stream") + response.append(connect) + return HTMLResponse(content=str(response), media_type="application/xml") +``` +2. **WebSocket Media Stream** + A WebSocket endpoint, `/media-stream`, is established to manage real-time audio communication between Twilio and OpenAI's API. This allows audio data to flow seamlessly, enabling the RealtimeAgent to process and respond to user queries. +```python +@app.websocket("/media-stream") +async def handle_media_stream(websocket: WebSocket): + """Handle WebSocket connections between Twilio and OpenAI.""" + await websocket.accept() + ... +``` +3. **Initializing the RealtimeAgent** + Inside the WebSocket handler, we instantiate the **RealtimeAgent** with the following components: + - **Name**: The identifier for the agent (e.g., `Customer_service_Bot`). + - **LLM Configuration**: The configuration for the underlying OpenAI model that powers the agent. + - **Audio Adapter**: A TwilioAudioAdapter is used to handle audio streaming between Twilio and the agent. +```python + audio_adapter = TwilioAudioAdapter(websocket) + + realtime_agent = RealtimeAgent( + name="Customer_service_Bot", + llm_config=realtime_llm_config, + audio_adapter=audio_adapter, + ) +``` +4. **Registering the Swarm** + The RealtimeAgent is linked to a Swarm of agents responsible for different customer service tasks. + - `initial_agent`: The first agent to process incoming queries (e.g., a triage agent). + - `agents`: A list of specialized agents for handling specific tasks like flight modifications, cancellations, or lost baggage. +```python + realtime_agent.register_swarm( + initial_agent=triage_agent, + agents=[triage_agent, flight_modification, flight_cancel, flight_change, lost_baggage], + ) +``` +5. **Running the RealtimeAgent** + The `run()` method is invoked to start the RealtimeAgent, enabling it to handle real-time voice interactions and delegate tasks to the registered Swarm agents. +```python + await realtime_agent.run() +``` +Here is the full code for this integration: + +```python +app = FastAPI(title="Realtime Swarm Agent Chat", version="0.1.0") + + +@app.get("/", response_class=JSONResponse) +async def index_page(): + return {"message": "Twilio Media Stream Server is running!"} + + +@app.api_route("/incoming-call", methods=["GET", "POST"]) +async def handle_incoming_call(request: Request): + """Handle incoming call and return TwiML response to connect to Media Stream.""" + response = VoiceResponse() + response.say("Please wait while we connect your call to the AI voice assistant.") + response.pause(length=1) + response.say("O.K. you can start talking!") + host = request.url.hostname + connect = Connect() + connect.stream(url=f"wss://{host}/media-stream") + response.append(connect) + return HTMLResponse(content=str(response), media_type="application/xml") + + +@app.websocket("/media-stream") +async def handle_media_stream(websocket: WebSocket): + """Handle WebSocket connections between Twilio and OpenAI.""" + await websocket.accept() + + audio_adapter = TwilioAudioAdapter(websocket) + + realtime_agent = RealtimeAgent( + name="Customer_service_Bot", + llm_config=realtime_llm_config, + audio_adapter=audio_adapter, + ) + + realtime_agent.register_swarm( + initial_agent=triage_agent, + agents=[triage_agent, flight_modification, flight_cancel, flight_change, lost_baggage], + ) + + await realtime_agent.run() +``` + +### **Results: Running the Service** +Finally, we’ll test the implementation, showcasing how real-time interactions with **RealtimeAgent** and task delegation to a Swarm create an efficient and engaging customer service solution. + + +For more updates, tutorials, and discussions, join our [Discord community](https://discord.com/invite/pAbnFJrkgZ). + +--- diff --git a/website/blog/authors.yml b/website/blog/authors.yml index f93f3aee16..17fa9ea8f2 100644 --- a/website/blog/authors.yml +++ b/website/blog/authors.yml @@ -188,3 +188,9 @@ AgentGenie: title: AG2 Contributor url: https://github.com/AgentGenie image_url: https://github.com/AgentGenie.png + +davorinrusevljan: + name: Davorin Ruševljan + title: Developer + url: https://github.com/davorinrusevljan + image_url: https://github.com/davorinrusevljan.png From 39d5a0ec2e46d00bd2dcab2aa4f20d402ea9ecbf Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 18 Dec 2024 23:17:44 +0100 Subject: [PATCH 20/51] Add realtime agent swarm image --- .../img/realtime_agent_swarm.png | Bin 0 -> 300823 bytes .../blog/2024-12-18-RealtimeAgent/index.mdx | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 website/blog/2024-12-18-RealtimeAgent/img/realtime_agent_swarm.png diff --git a/website/blog/2024-12-18-RealtimeAgent/img/realtime_agent_swarm.png b/website/blog/2024-12-18-RealtimeAgent/img/realtime_agent_swarm.png new file mode 100644 index 0000000000000000000000000000000000000000..03676fb495bc5df8b4f603511b1da7a9304e0f9a GIT binary patch literal 300823 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�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ฏt&b0Hgu#?*aMG|x-ZmmrY6cJo=q(;hII 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

I2yg+KVqnuaEGT=kJw?py?&&Mb1iGhBegqRw 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 SRFsba4^34)6}0l>uzvtHRIX$I literal 0 HcmV?d00001 diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index a80aa27df8..41fedfafd1 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -9,6 +9,8 @@ tags: [Realtime API, Voice Agents, Swarm Teams, Twilio, AI Tools] --- +![Realtime Agent Swarm](img/realtime_agent_swarm.png) + **TL;DR:** - **RealtimeAgent** is coming in the next AG2 release, enabling real-time conversational AI. - Features include real-time voice interactions, seamless task delegation to Swarm teams, and Twilio-based telephony integration. From d3e1864ecbcdd688e724ec3d41f59826e9ddbe36 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 19 Dec 2024 11:00:48 +0000 Subject: [PATCH 21/51] WIP: blog --- .../img/1_service_running.png | 3 + .../img/2_incoming_call.png | 3 + .../img/3_request_for_flight_cancellation.png | 3 + .../img/4_flight_number_name.png | 3 + .../img/5_refund_policy.png | 3 + .../img/6_flight_refunded.png | 3 + .../img/realtime_agent_swarm.png | Bin 300823 -> 131 bytes .../blog/2024-12-18-RealtimeAgent/index.mdx | 79 +++++++++++++++++- 8 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 website/blog/2024-12-18-RealtimeAgent/img/1_service_running.png create mode 100644 website/blog/2024-12-18-RealtimeAgent/img/2_incoming_call.png create mode 100644 website/blog/2024-12-18-RealtimeAgent/img/3_request_for_flight_cancellation.png create mode 100644 website/blog/2024-12-18-RealtimeAgent/img/4_flight_number_name.png create mode 100644 website/blog/2024-12-18-RealtimeAgent/img/5_refund_policy.png create mode 100644 website/blog/2024-12-18-RealtimeAgent/img/6_flight_refunded.png diff --git a/website/blog/2024-12-18-RealtimeAgent/img/1_service_running.png b/website/blog/2024-12-18-RealtimeAgent/img/1_service_running.png new file mode 100644 index 0000000000..3d3593e77a --- /dev/null +++ b/website/blog/2024-12-18-RealtimeAgent/img/1_service_running.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:078387f868d157f6e7bc91b44487cb95bc36a74392e47927c4b7b0fbf64ae494 +size 37760 diff --git a/website/blog/2024-12-18-RealtimeAgent/img/2_incoming_call.png b/website/blog/2024-12-18-RealtimeAgent/img/2_incoming_call.png new file mode 100644 index 0000000000..57baac9afe --- /dev/null +++ b/website/blog/2024-12-18-RealtimeAgent/img/2_incoming_call.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbf0ed3cb33625a4784164f505be1bd0bbc303b7d48e4470a89e46bd10c4ebb2 +size 93384 diff --git a/website/blog/2024-12-18-RealtimeAgent/img/3_request_for_flight_cancellation.png b/website/blog/2024-12-18-RealtimeAgent/img/3_request_for_flight_cancellation.png new file mode 100644 index 0000000000..aaf697d298 --- /dev/null +++ b/website/blog/2024-12-18-RealtimeAgent/img/3_request_for_flight_cancellation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a5bc811db8b7db89f70c4fd6dc49d1d18fa580f19f057dfd87e66fb932cdbb2 +size 31315 diff --git a/website/blog/2024-12-18-RealtimeAgent/img/4_flight_number_name.png b/website/blog/2024-12-18-RealtimeAgent/img/4_flight_number_name.png new file mode 100644 index 0000000000..e808e06770 --- /dev/null +++ b/website/blog/2024-12-18-RealtimeAgent/img/4_flight_number_name.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f03a5224d63c54b6e9f2d095dc08851427d1196a0e4df8ab50ecbffceebf6681 +size 77047 diff --git a/website/blog/2024-12-18-RealtimeAgent/img/5_refund_policy.png b/website/blog/2024-12-18-RealtimeAgent/img/5_refund_policy.png new file mode 100644 index 0000000000..d169997e3b --- /dev/null +++ b/website/blog/2024-12-18-RealtimeAgent/img/5_refund_policy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:463af4d2a03c831e83e2a8d92167b8d61131469d75d226a11f5ed7a1ff24bb3e +size 73380 diff --git a/website/blog/2024-12-18-RealtimeAgent/img/6_flight_refunded.png b/website/blog/2024-12-18-RealtimeAgent/img/6_flight_refunded.png new file mode 100644 index 0000000000..63e54e7e5a --- /dev/null +++ b/website/blog/2024-12-18-RealtimeAgent/img/6_flight_refunded.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937e498137d076d4b6350cf9db3726e51d97bf7db11fe96b9285ded799d60fa5 +size 141098 diff --git a/website/blog/2024-12-18-RealtimeAgent/img/realtime_agent_swarm.png b/website/blog/2024-12-18-RealtimeAgent/img/realtime_agent_swarm.png index 03676fb495bc5df8b4f603511b1da7a9304e0f9a..1b8d80aa3ba388676fb3a74a87fc11f943a79850 100644 GIT binary patch literal 131 zcmWN?%MrpL5CG6SRnUNeEU+x;hUFv7sAL3luzG!$ca`_-<0ac#=RB0U_jP;Jx&3b+ z-qLugd6uNEU5uXPY!MtDMstNk(Pv{3wat+$Ic?AYF%UXmg7uJ{)8rW)VX(oc!eO^i O1BuzcMH;OMjQ9cKnI>oe literal 300823 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�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ฏt&b0Hgu#?*aMG|x-ZmmrY6cJo=q(;hII zKtQq#|3dVI<=>J3>yB8WyX(5*AH3j7*@5A|1EeH(MWda8{UD(P$7-e)BN>O3eNe;s zp|e=}6BK1}30XKWD)b-r(0?uk2^3&VFqNSEE9)3|kJM_b75;?DDShuBmMR|H{7fsz zPiRyO#}VTFg9<*o{f>;oA}TpHT)mUPy%M@wgE}~uVY22x@tt?x#Y@{+4trVhk+O&M z7_(6R=UF7&Nku`S2A8SQPV(UBGwcpF?#Tk#0=O9_15bq#!SnDphw9bReA6TQ)TFvAOxAaMyvYCPK>vvCNgP6{?tO-8g#Y-#MH}9jUXq(syAv4@~a(MMf-3#U5J9 z-Qv*V0It{`ug^fRx#=`Va(|o8(BCDXxV2^y5G8 znhPI{=%3O4yxr{ma%})rWTxE1lEh+|4L){Fsb>Zwaq$Z>BhP8|O@vxAOVY8~xC*uW z#!f2`h00^%H=>(VPnSSKvFTkoAvQRH+kU0SZ%@Zfyrac)Fr=u8twc^SppuMxEQH8^sz;CBfmeaXxbO^3pfj*gxx5+e=NjUX`vq}% z8WqHlz$%Rx+BWyH8ok{a1@n)preG^c6utKKYkg!x zfJE=VQ@T!VF~L{K_93sK4Pcjm3`xK+2A&OaXk$9mvny{#e=gV&t4(u9XHxtGsp+~O zKtDQ6vM%;Y>ZN618;(XE(a{={;^=DxQhjt&kcaA5r zoiUpOKd)Y3QmEfHQ$ZSjZ?ZKky)DzJN$;`_P^5W<#h91j|C;fs^me+h%QiuVkj|ef z-W}Q^sA>(No*`fo8>#?h-s0|XVT38Lk{)x$e%-5Jf!`VCqn^3Z)GUUd=}U86k<-{| z$d8q3%a-E9#kwPT<%2pxzfDTwHsQ6f2YK{)xfD{t(QXO?k?SX7_xxFJI)AfyB(y6A z=Bfv_vkvn+B6CHc2AFoAY!T;Z?mb*%HC4Ex{1|7bT&@Vl5Y?$@JWR0-9)O_+%V^c( z_w?e{4nX)z^WBrqJ?E$$eT?H^gSY%`u4t#$geGs8k#}3qou1!M z_aNihY%jjjn_o^DkE-2?QL7~Kh_=+1Q6bVs>Hl(zI)kK|a#-gH^@g)ts+BvmY$H-c zJ1SBrP`KF}juuAW*mdNK8I(fOt+T@F58Ul`K-dvf4Esr+9b zBo~KLu<5HJ*O81+RT=#=qN~6P(H|YcTj;E2g8FYd|kv!`-8hsY7b;0hibn`e4Woq{%n%DaG^`%Xv{ zp`=Jr-Hz}Qyt@mxZ>XooQVQaZ%p#|v1_75VNq>lN#w#?DfU@Z13P3_lAT@JRE_O@C z{c+t_;*aeAOMdfXiT+M z`-Bz*57A%L`qn9A7>5$~EbPv$xZ2wERUz*9FF8TMFf=q$PO-`XnSWm$01h04&gs5O zAe~Yb#BQ-Dw6SrjBqBkgR4syjxROH{M}r$1GzL0gvs{1U&jcg@Ciw1hvns^fMR8eN z@&_bcjLARZK>d6N0qYw%SvY|yfsgI^+H3N7-`%i4$jIoNW&)8G4nR^+tpxv}zy_H^ zmD`Sfo2MenEJprDYX~m{qJZmEcxr3V-E-TdTp)VU9XupUu#-BB|D!+;pwcK1pPRscD3)XAe3afKWhw*3Q27s_HKo!eWAxSc5B zzI$s8RpNn@CC{nZ6W2nI6|>KI4;PE0U%&w0|vJ)I1ue(<=lmTTP)+ z9IYlr6`Sl|+w-s8(1Qm~Ny<4JCkg$}<^VbXm=JQ$9vqG#%C6M)E=1hhHmC`COAXdj zkpg3xc|)@$wum_8nUZmmigh$Qr7zzo9^p!tDpw2uw({-dn&(=U>lwCW59E7wG$;(T zKX9}{V12kYB!*hhc%Gtbu~yM1hbTku3ucOvJ+6QP+2u-qx*BIbdiqV{i2_~YOjx_ zaX^vG;MeYAr${2yhFkyUD70h$&&mBK1EYrXg8ME0>EC5F@N>O;f;zo@#Y>H_>7oLw z4egp7)oIC$PMau^QTNNm*WNgl%N82e;x^$!l($h|Bj2F0`o-(al`rS9Y35=IM&*hy zgp{(}K1W)9dDZxmP@$D`eK0BC!XD0BuMd8>jhx~*sZWtEo60RcSH7CL++-)GQ=eb1 zU4wybQ)zTl?58@M-s1PV(Hs>}Z19v{+>c9?*`OJ7p z+l3IIcnHVeko<`9>}6S=%2g;Q$Zw@8ji~AB-+BH`g#VjGH`l>@4P~Oy@T($&#uRg2 zh(c5Khf>2Nbf`^CaI}sh1;hvZx%Y_R1am=>?*F}||2I_r=Pzm^aA`+~ped=4)vwrQ z+@s*fSaSL4e6B*4lkB}k^B4|`!~TQKoP`oX3=luMtnJ{xna4>Q@=8nq=w_rxI%A5} zYO5$X^py*GXNx1TQvDQ&EbZ%Jn7`DZN^U{=ewi%>Pu#?jM;fc_$fpe?(rZ<#sniG0 zgAt-B6)9VDDMywIPSZh%I`@&qGN_qy-2ODJ=+7r7rHeCFPHQ9-A{>*M;O<2_E5$B# zb65`^4!p1Z?$?i+WCx=HZ{ztRE!Y>~^QoB>HpHa z|7Lst{SbrzfqgQ(OZP3FA{H5aKun+(4zQ1X~( zZF7s1*FC0RCgzHS6&ZN+N-~GZ5ut#dj10);;t8CMA*i$9@$T~8;{dbwWwF2swyiK< z4hE9Qz5_W*}VcgOG775O~MV8a2<8Q`~LZgt5e;JD1{qc9&foWwGDbF^`o-U+lOpU&f5|Pw0joso6;2wcuZlj#?a>f8ct$XSXoq_wuTx-b8K&fb+NY=j-!cJ+5WC% zj1sDc)+txnMZ{J49lUNRz3ta*IY=Cp7+yKPORWfAZn)cMKFqp3<(s>#Ad0}%_Ddy`F|EA+EWZa0)yKMKz*6)U-l=4x)4IGQ?AMTSc4Dux@D(Sw_W zjBJ20I+S=LQiv*io*K2 zLY-fvF-#KV%n(1=Jq7g3N^{H$aHeZNTMmwGv$fB$;AX77E1ynrmuX;fOn&YKf$#nJ|A7^V{s0BMx<1g99pppLz-uROPf>y;c+qxs;ThS)7k(6w&4}zke9(Z1aFp!@z$PNCw#cCqe zb=#1i8OVr@`sLi(4P~Yf4&ZVn1uHIfm^J#rgWi{zI2iOtzyy_7gv!|XIX zPi>}}YB%+}j<77#VFl>^b~&zw-()KwaTMyNS*^5N&R`O_4KLEO0Ou;I8W8omr`LHK zP&A%AWTQ_B5GV5RDpo1lzS;*G$OV4@r~0{K^)#a6sVQUig>^B#)n|X+>cN}yk1DJ| zFnn*dt~rrS8)WU-27M7FWrT}$`6?e%9;k2|N*b(|gfftvb}~$6bJ6);a`*a68~Ku_ zDgH^C|My@|h=Pd(e|{x}CF5S7aJGiI9 zo%1*xen(i5$uzeLsn5B`|}Gl~TCV<*KGd?U>enS$ws|Ol8A(x}^PZ zRjrr2`TWnSC4xOtb;Ub$5btl#USax|B1&b#t#U~@)Kgdcqf_bA_N>bumJo-`T~m2H zqkR@O@xZH9V)mNscJyFks2fBO(sxi?w&bM2;ATEs9y!&V$NaCV0swI8;plXl zQ&->LlDP9coQ|<;asxo`WcML{I zzwM<`ytl&?U3ll5k^Te@4#VJnhVlWmxVHq$D0F4o1m!9BC+((+s!aIxY>OqU-MF^B zbXTASt8l28Z_iXmYWA35>P~WK=WUmI6f~+AK#vYpyLx9OtFKy?qcL+ygtNp} z+%(-eZxuha!^G$Qm|CY68Q0f2M6H0!HJMg#{#;eYP0S&kJWaF5U@MeDT$p>~Ah^IH z1@(lWd%Hk|T4J-EI|rWE;P+QiGuol&^Q9Gp79FWQGa_(vVH*~!W9pBvbFZI}9<#cp z^nh7!z1FKjQ|H!I?eK(hGcM{)r_QXH#GL0AAAW?~8w#ziygIU_0UL+=@8$1=g`icf z$YWZItf?ohSP7EVy;W#dgZ?J=-%0+rAh7-@;XvV;By1pi0NN@+*K|S<0t+1pgV zF;h@SRYu3^ol?auSW%7Di=#$^4Nn@kgUU^uppZ(ToQK(TCd@{vy;RaGtpEALVIr3? z(?{K=;4;+`nry1#ddtP?FNCz$BJWEwyNiHq8R6?>BXY@hW)}-2R3B2kxl9^2Hs5Ro z&&j;ASEYc3wzsT&y|X_cL4&M%Dr2Wx@Swgt%UEC1_TwOAG3@B}rD z2$^{GD(nRJMaagT>CMnhMTSPzXz_U;deXf-8 zK6;$Hyn&kGT7<;@7TpO(5uG~LU4+tRwR0$4s)m|nq0UmWJmb=q-X-?q^^A7<>)2@- zHKiub5rAN%Hup`O)<}tbQrDmY3236ha&H#hEdd1{3G9)4d!&9t=u_4EwNO9|Pl(Vc zbzQ5;ZjWKh8@s59ywa|;z&87;l)Ov{?wW@7UKM~3r4!v5icGFPh>XBMbbHV#<* z8Jc5at|4dn5dt_sFZVf^@l~gnb#m2};cawHp`~x;Cd^dxFKj`A;7?Q+oC7CwCq;|_ zMP!?No5^joyt^zcHB+XUI)oEFi7#6s{ouu6ZL?DiCdXE+oW<&Nkypj^R0o9(pBws) zLEiteJlci&)Mo(xZ;+W>5^A<8Gyu1f0+Gvpc_Lz%j`AuP-`jky+>UaIEhAiN2OU#hKuc-BWVlGYTb|0n5tf-L<}n;de7spTVyaJw zSqN&g_v)M=m)VOL!2(B8xDT!HxKNnePy>KCLktVZU)4OBdi>hRCp_QP>M-|soe$3j zSMl+?)kuCkoprclbXI!nMey14 zVFXH)Cgn($aiPIzMa6&VYK&h(F6~Ty8mSi(+{TDgJ2_ee z_jiG>oltPYJG2~=Q9_Xl^yR93mZS2ILw*Z~>ElO?y?nv0h6B1s`wIn6jK5AP+T{!Q zUxO8mW81iI!PEQg5Fj{A$O0jsHy$nyIpa7U1*iCqV zEOGYxlBOFEi<+xk-|_x^R5~n%Ns4J2=6rFGaryb#)KRw>4Z+CGXLq7`LFh6A)RLr& zl{x*}1_LxWiA$end}A1o76(B(tf`=)(Ahfk=oa-K~H$Lx-f&0z(WiG)RMVcXxM-AdSS(A>G||KlgU; zbI!f@pPAnb%)H+`-<#`MYdxFJm|(uhKWG>p!gIL%Y~IbyGoIiRP7FWBiINIENE7AkGO_gpU&%|oz9dabRJk~)y1(S5^-PxVhtaz- zAyfiBR0K3hF8nq=dz0*2H>V85P8l^emA&7%2c5)eRjR!*%T4GZG{GvW-NmW?BS?`E zDwYPE(uwhwdA35>Ju@Rt)+=YtKqx0f*TOpW7NW=}o9fr>@voGzPAr7m>uWxOV7@V2 zmy@TLV9Q*zN(1^K)m~ridadYL3(iGbuMu;DP}OTtWtCX#j_e0?BM;VMcb=-Jc;D80 zyL*3{Q?^U?g;=2UDM(5?rF0weG2D{yob3a3C3w^#n}l*3gx)s_DVY3hyAO2n5|u8} zXbx8CvN57%cq@8_4i(zqVd~jgySvskVVf058Ig!yQ2NSa7&5C?s8W67fNgMijud-k zZg3|_*~&9r4v)U!DsBESP)^TqLX;KG_jl^Ya@8c3Eqa~j8le3_sA_?9LR;E4@Q4GSW6;oZAYtz}}^OjB(adJdhw z-pqUob$#o3vmE`Juo7Lpf*yVkbn=U0;@XqhZrD&$P~t~8{ELn!*EV1;UT(z8vA}ew zIpE35VVV(l*D5um(!*` zpPH>Pl7ad13qLjMW$M0T(&cSeeO(SF=#M|sre{X-UJLIaq4>gOe35=f>d7vm%3qD% zx3Kx?EA#g>+c!VXS@2d|T_Zac_BoNdlw&cmvGa#?>Q4)DUk$6;NrPhu`qI-WS>UaR zRugwSwv`~{Xk*Kf5`sPlujZbRucJaiOdyso!9>jc>?;Sjkxeb*j_CEP{pzr{g-x}V z47(%~r1o2A?FoCv1DUVRtKw*6u{Sx{S8b@mJyz?sX7ve-1`F^=XVS*W1 z)a5F&d&Idu$s@xcwG`1lnBlzWt`NIWyPJ{Zqf0JWkuDqWPoQ2LqfXdLBYj`OVvq?% zp3!Lr(bBDQ=pel+Q5}e5EZaFcr$l37Ai$;ieFFd1AOECSed2hk*`Uz-dMP4V_7IbF z>s&X`beo0-LjOM8}UMkGtVm>z*E+Dwxb-Zcn0+=C; z0O{1rx!O-A*1kUYJB!=ZgpA@=7cI2my@(&4>}Ad(Wu`D1qZDJ^${VgA_(f1}Cc&K(7YI#s-mLJ5_7z|`_FMI=`CHSSqnmw4w#*uYO+7A>TyhHA) z%UiDes97s14}K(BZQjjRu*dj-yKGO~uk|5EB~ByOt=hKtE_tK5 z^*&;{f0H1vuA@Z6);<=+EZytz!br&`tWA()!yi{tpy_`N9}0(yf#AZ9%>?%@Ty)V; z$V;?z`B9W&x0Cg`yVqg*yXU+1bl!z9+Ka+Zk9&$bD-DM9UZ+wR4IK}g!v-h$B)uG0 zn4>$Z9Hzm_BL~)G1HW}FxinthazKx*N3nGWN*JFV2il$PU#V+gx@!vlZDXSb^&WvEIGC3EG; zP(nqcLKWuDn~w45RX-Lm_lj++&~AMTihgra{92G{)a{#Er_c4Ozu{oy3&c6R*zD3d zT|P=tSpnRRynBb({#a_EdMQt~+|TXuYJ9wBjXR9eJwkS#*gMmUm%i=~han&L`4t7; zw~FM^1`~x5!UK{f;Txj~9;?-@8{>%*zzNb)EgR}_xQW|&gU#Uf1a@*finr6`E9w-S z_nzdZafG1cc7kr)KOA5_G?V_ApheG3@Ie8eSR&_rzO;9KN;Um?i!(%{n}2$A1#$P= zwI_>4sw4KS2hVudl!6VgfLLG6I=uVE)?zKK4!y04AIV!e`mRRo&2;4J zkS_zy(c$|^+9s*MVQdBvi@0cvdkYStt+^cxBUK1R_Fi53GspUKFCP91fs?*@gAw|z z8I#3HN3Ym?CS_IMFw9 zzP45V)C`P&9m0QB+u?4671_-Q?UkWf@9@HJCD{G)wHcvItSlFl%8r@ zF-6(*Ox&#$fVO0AO#m~%Ysh2vhzrsdvszWoY2Oe*@zf}fG1T!T4_HVpj!mN<_QAGg zFq`at*Dsm#(pmckW^WmjTqdV@3RIjpziCM50vt^{0A6&@Tq{r%Xi@3&5 z_6&O#bkVt;@fdF(wm)9X6ak#Q9Ep;}?_d~Th&y2%<{xnE$5VJmIwU0gjK_ztR14Zd zUzL0((^wv?O>gs!!U~7}q@$_QxfXYsLX*)aQ-SZ6zfmY@EE}WP8}tOg1wah(Bc`Nc z3@3k}9)1spV;kv5W{9=nXp7Y(e-=+(vf;WkGW^3FdeXie3D&bz@&q>E{~lwei1@l-Fk@Rqt~7rd#>wE#&JRAmWC8G!W#pX zH>!9YoaXFw?(|jFT#c$$W~Sxg&#cI8JPrI>`OpmSNmXNlPD61m&WzmOXE0!6x96(0 z74$MbmQyJC>~Fv!vg~#^8^UALy_JQRK@jSL3!dZ14Y{@wwmOa)C-W}+REtTihi%v4 zHBmZu!T+Fdv$85hc-1To^L5r|xSgxBS!)=`mD7kBmxvD^I!rnxM8V$fSn_-3BP#yF zt5fR&jYYSwO*YQ>xAoI&BsRL{79g%ZlAw-I zne#a#d)$3lN79tTpCDbKeYVRU*v$MT0l+5(Js!K+39ohX9N$5%GH++5D$q6;8@^>9 za33-J{Is)9WO8zcj6P%ZJ+-~NSlqbhSMI=siZp0#s#Hf#i>^EGilZ1Lp_r0&my%4^ zmIdT~&laZBC$?~>Nz&WxqF6~6uGtNxJeUU0?YiYW&WmEnlTTnLu@q~z{T31mwWg%!clveH#E0(De|zUD`?%i)78XZYXK<{alAqsN^9;g)>A{zEGgjr zvBZ%pSIG{gAAX|h^ROOY4RAO=W2H07;sRx}3YwPOtqU}2iJnLN2G9Bo7;b*c|R{5AkTQv1Tq%d5(@+B76cHoVnDVchP;Nm;OYJeUMP%uiJ;Gf)V ze&yOhWL}UZkW*#{)p@sseM%8Hqy{-`+EciMLr$xVlu_-!fr^Xbbyag$F z{n9_Bih1SoqBWD@oC!aKz&Fh^L3qu*mVSEndN|b+9~Xn;YV%1ozSB_hlybkL2gQa5 znflyVNkv7qK2k;DV5rpi;Z5289QVq@q_tx*YIGOqp*5l{xAz8oS)qw~9VXaE^#ov{ zSzu3#Ca<+88jxwmv+B*OJQ^*1?FpGKUaqmVF+whrR??FY!m4@(Uv-jy%&E2Y%xJ&( zhpVL&b&6K|hvrEpjEgTUhRU|M9af0e&pNISB>c-iQ@q}xCpqcLzDwSI(7}GX2c)2I zXI@O$GEXc%*Yvq^y<<5~o+_1-!=^ogr@%Poqg?IsA7G-t)+ra3{BNtGg`ZNszB{3yk(SUpz15#F=6lvuWU@M z`8^zh&Vj*PFqJAoIpF2DPJxTkyqcfpqGtPROrN+w?4JKFgHc{WA1YC(Ig%#OoBiB1 z$F!{l#5Wjv|GC9!;N@vvpG3gb(oTF@Hc~W16=@_!{zQov@3Z~qqOWb?mUu}J!^Wz|p zk0tF^cv|N$ukR;4#bIyuMb+5Ec9}>)-bjjE#rwKdU%}3+gs7l>jBGhL-%yyV1XB#V z96Pp0XLnko>-cwk{O51WThG&|_nl8X>@W8CDDIQz1mA>_^Q6fjHA#cwm~{dcAsz+b z1&4K`+k%TyM#?4Tba!qb@9L%AyFXVK%mJ(H6*Kv(TEcI7(c3&UlYuyYL%G0VF-=x# zKy|~4WJjiV=vZR$j%|&7aW&OG%6hs+0&SVM$smX1uK@@!CL*)9Qe?>Z$@&YiWQ(o~ zeiLP4m=L(~=m@iCQRhBfTVO{@^Dl?vswXrCf56l$v-&Kawp3~boOtvL9Po-^k>BC3 zy)&PNzq^w7`EXg@?8ywaAnG)s7$~dZs_*G(X=f}QY~y+Fx4oLL7(cwjIkx-D!lIt< zLjBMhgt*($f#kUudr+Z7Rri4cR)T(c9(aglX5_#ef?LO2rL&9-kOuoxeB zrZN4(oE>2)h{Fn#LY)^EwmqI7VNoyHNZwAPD7YBicC(=|DSRhP#l!vOySr5<^mb~= z@#DYxB{`yikyo#-_ZminMSimzpkRG|8lM_m)Bc0W zJ@Vo?(lgBhFBL<}J!+s--1+uo8h79R?Il< zMyFMa43M9<@Cpazdji~n^{to?)U%E*p%_$-mCNlp_P`!Doi#Y0j_wp`1lKCn+#h1Z zU$<;Mj9P=m;g))svt{-RB}kBL<&*6011u*fFNXGfby;(QKCz|q>~_TdC3s> zXcF9F3pkk`esF-_kZ2#j!effhsoLy^cq_!of$6)e27l$`xh#m-U>UcQ2dL0H87vBB zAq$CBvh1GK>XpDgJE8JCzxkD-*kBe`RAX~bSIfk^7KsXMzEheh*1kPIsyPMAked#~ zWC8hs3QU@lz`YE*u$|rRKDB?4zPDpQg}XxVzzre1`xn+DYV_$#>$Q2s&KEMV{%`o7 zeHahGy{a(w8%h`Pj=B36#T*y;Locs_GPB^WQrP?Ci_?79=z=V9b-nO|+<89yTTF+U>OKIKmF11{hPX{gX zyTw`*yAoGzyd$nXI1P)v;pu8j#PjI)l=~}}=d-Q!2}?i3CmJ(-nR(`dK;<9>y;E`l zY|S;K3GP1hEoM9Y=OtddXw=>O>>zzWBTV=6G>Oan^_=xY{G32O)4%rCm+7oV8!^1UMJFIoc}WHEHFzJUk_(#$pMKc4z%HP0-WM+5|7AdY z;}_3W+1lbUbKa($K(RCa4pYDTko#o*U{x@KYJH<(=T#oktGA*IBqP=KLmDaTmx$zA z)mQX%qwA|iNj&9q8)_oasXLS0W7Bas`;HdTjaX}BeX6>g3=-+CRS}JRQ>V_SYkXemZrfB`|At zM1H;1`dF#$(nRvbcjzr8eq1yLp;>3BAYFw)4&*9w3Zy zb{~WjOO0)>2X>(DG?6bPAhNOipRmq?Y^{t`+dYNeSMvGn?SAn>CEio&nztF=%`zQm zu%@2^bQJcZ;+aV_GERDu?{ClAMvP~4y=u#`G@nRZZ!;IEm35HIby{$KG4{-o+q`%p z{gCCs#aDhff(KQ7@BfU@_4dFtnPytUYhnVv`C~qs(C02~O!e||N@A0ze8lALx%!++?&gj5QYym~K;zk>t8HG1S$gaHda-}{$# zZ4TIfC(-|6ql(%~chqEdqXAwEudWz8u7R}QwrSiVRM+r8uoj*_0ICm)Xircn5srP$TsG0h`+3AvM1{G?@!lY~#X95k{`Xs& z#O0FtvL*mf*KmG3%J|e7FLV?J&s_bR(I`eI02Y2_B@u{@HYzqER1yM_7n_A%pO=F+ zQx*#nTkP%@MxtWc8-9Q&1PDHL+tR-CYx zro5$i@OCH*pPK=$_6r@AptG0#enS)Xhslot!tA>FkC>2e-6ua;cs!=knXq5*+hlSR zKlJdHyOC_x8Lm8~V9`!ZgZ_d_ffB%?>lx*VjTEO98sOQ0TapXczeT$*kq}JzB3@c? z9M1(URS+=qsALBmoGF-JUt@%<2SsmGl4@F8OlAptj9Bm^O_);Y4fS=HbYMdDS9nG! zsQgp=>U@w-in3hjywJ)98$I^ni&dr@x`K(hg8>kb$xQ`A@aa>G45&wQk)CL9Mi^_- zWG<1xbxh>avk{v7l=_D)!Xp+WZ~MLJcTSYe8TQz7a{*$C{0L+x3p>`wT>jQPU+S4l zKF+m(dGg0BthE+)jDOu_MoKTIBJh1#<+$tQC|X^C`dFMY7>C`$nZ$d*y-*vh&_H2C zKEdYheqQrji^`nk7YcSs_k+_pr&aU5w=>IhkngC+=Og1_D@499H$2J$h1yy0$fBVX z9MNF9Mr*In$G~R`?>bwSBi8%*kX@@krw%$>yODnV#ew31#cmKB$EK5yA{LN_rkcZq zvbH`FABY-?uf}I|T(-^Z^_!|wdYF8#-_u4F`PnmO13s$_VO<(}!6bmkrhJL_5ZCB> zTBp+3CEljD8-R3s!`^l$)oPrbwmYA`!<KC-tfIoBm7ltITigm z81bd4RG4bkdgzPW-N6J zR%|Bl*CV*I^Au>j$8`C2$bbOr(eU!4|h+=GLZnxp8M6$&Q|%2qStV({go$K^BltQ9^qx^;-JO%hhpQWG4z;*F;iL_)Sm# z%7y@J6^1O$TaJmV!&$#rxXo1M7TTmHF=;R}iH3vRpK{KiS}w~H4B(Z7eL&r3mPc?I zS@byj#_8)$pxzH^JwE$%7%$Q~l(G@-r^G$b0~0JM_6?|yD_-~7E>)`D zfA(FCV^9vItm7j~Hhfm5OiRI2pU&&rU)eTY_GXtAfBDuFrTjl}jkSN6)B@hH4mP>iXICxcv^szO0uW6Kz0nCK?mTf-WnO0xy-ovB7zt$XP!UQyH(ZBWT<0Z}RA0l840H3~MyH#kYo zvs=O`WcrhXA;X$kNx$0!7t+T%ngA@ZG=hJZ-hGgzpXv5nCPLxaK}-|aZ}qQ zC+2S0ka)awQByD2r@DY#TG_^eEzo1S9*|1fjGy)KS&j&VK#oW+cjOfLL4W|fPJcI< zr&R7rl0KvVJAGAOx3AgRtu)5|J%wVswb-!GT`{;jWKR>v>} z5%rf-7SV3`r0dxjf6JGGu;DM0^?q6EoY*LN#8)Tg>>I$R|)Z!VOzG*{shC{Gf$x;WUD$ty>SJ6^WBqxQI^k+ZZicCpFRbQ<_ z*q!^rZy+sII?bO~=^|Rv4yQjLuG2~f(Tu(e999pVn(`hF95bdXO3_oQH$=Mh%BLJn zUUi(!(h@p&USdAk2%gh7B@G~;)wDkAY-ZP-%SwXh(@Yl#9}?Z%{h}io4vRQA6@Pf^NR~xIu?hAfu%`K}FPP8))<4|_AH;;Q7$Tzi z!_Q_5oQhF-@;FsdlU5woB%;Tjkowhz(}h{$3iBHxJ{$xt6-X863tulwwZ6;yE4nX9 zkTMY5{0M4Rp=p_Etej@-AW#~W^YN{InkqV(2t}T~R8m*~sgIC_db#K+)Ib5y#byA= zTpYm%t{5EzU9rI>cL$LID}g#oS9dKTJ4KI%l#u^1C}kB{4cA+(hj(%#F}} z$(#>_4Qpvwn~zpJ;FST+VXl`XX_Nzj?=Zjf$M5&nGZVrMD6^Hh4!vekOicUx@A?Ov zdcfjoaz++%;=jC%X~KT}rWLWJCh7Q^?_G%!mvn!^DPN3>$-8inm$i7I%~wvHl1{tsRU%K=9dZpF-CL+8)mg304Zo$18zNE|KliUg0nq%7Z+skV*`<)f}9yjH%R#rdFjd z-(QM~``k&viD>qHDqeLeB`pkB!ifS>F*Rt0IE`j^Z=iXmKY|oI*0d)2iFMf>b;Waq z?D|k;xsZJb*BDP``FfhdY|d@pgC42vH~TYVDyh;x>|)lUTP>yhR>w?PIHq+O;TGW{ zJz#z8_=fOyo@dESJ?T0ypf=tT<&ufN1-D`l@so8L(eQaHPkOf4H@Q3ZIu{!u9h z^s8YrphB;v3>BQcYKc>f>L^4NEynTD=eg<2ze4l22(2TSf~m0FuzVng2!-WSPeHi@ zQ^NL#Y!xym}aCOZO7GXjbYtLggdv}C=zmu9KqVeqyt4_&25h~=Vd$%J#$sJ$*1g}1E}n+oS2^j5@? z3kY;p*o@>hlX+|l;h#3KEP4WKSw0HU4md%aG5qrDheMBClxec={8QFb@&=9N6DCdIzu>mr6 zcNa4D1~OsSpSEx=gUZCN)@Mzlo8-`m<-%{~L=KX`Tak~V_UAi%NAb|T&a(NFZ-53* zzP6m`e-u6(&Y*^6n9$-h`?%JCv zEUVDUjh-T=O(dKAmoSUl`GQ{l+79A|+g>clePfGWiXW!#XWD47pobbLEG;9X`?6 zyRFO*oo{%_YZ7!lX!SD6^FE%*Q;+?o(qO8dLoR!fjg?JAXmnIBt_@jV zckq&^;1auji#R)`t8OX>3WCRAA&J?LmiR3;1xS*)Byqs5Oo72XqOw+G@x>k=`>}kl zEic;KWXd5?urW>z;H3iM+dd}M4~uQOaCw+PEmSC#m0fw-g;R!Om(#IK#t^@Mk$+{4 z`h?$lgBn^#rXfk$PXlPJGKVJ@xiL1nmb2DVD2Yd~$u{|!Q=&X;5GWDXDq+GU6jE64 zC5O@Mby=T3PHs3!Xjypnas;~!b;%N9vE<4jyIOZUU^cRSxFjR951n&WX=RSxY`M5n zD^#0ezGR<&KRTSS(SbCBUWNa3+pet)pxde6JG!KgZXPnLOW!w5PTcAOqC%lzj8HOyj+w!=Q^E!{nfuWaha+2d5KQD9;UQp@@K^!c&vW_osh3P8+XK%VHMA3fA|LP-r`-^|a z(!smXFxfI|abaz8R4kNTt=_`M){l{LLmB8kYxkDK&iqoLXSTC7xSh{PyaAilp2>W! z=q~nF?ohUBFgriv<=%qg`A!onEEuWDfZBO~wwRyma6P_ujj(olgZMfM*ZBKGPTm#v zE=`CR91mYh(3A7JQk^T7HrHKlOC5KBjS|LVe;L5pc*xv)3KWN(2HCe;s1EH+7PuT| zfo=uMZYkOh+->Iv>IDD3dn`s)_iTVeX{+Tm8P{8vxdDgUaw{tX;mVHs@uQ`$Tq zE5a3!mV(sE|Lwa(h-UZ-tB>LRTfS8M#YEwIyDyMBUouMz&82L?zHhf~wa&8uj6CBp z%W51bOCVbuu{taankv!?x)ou%p5AFCv4)$m!d`n5&O*Xd@(w8XT2T%aek!*mbK`WH z&lJIdeGHe|hhRfBR%sY8haz10q~Y6is!^5M53==Zk;(S>k#3)l;3C*27SzhiXDmK= z3|8CKn&qHy`x&hmZLjb6m_9@40_^U25zpZxDv#5#oXC>MSp*#L_m>yu-9=zvqoW~T z<-3Eesy~LHFL2&MtqjC7wl-gf5EC|09RbBVhX|qTtrv?!Ib?OauP(=Tp%13i3oT$d zR&io&AeYtpBWa`Gf6P-#{XnOr#cnKBf^dKe`S|CT78F1;oRL_)p)zvYWE8E@p+NG| zVqunx(V(qGe-7{T=^FeL(kp&>uw>jLk*S?EG+9uOV5#$BHZX#0UAZdFM36fmRak^tH!w{BqP5>#KzjbkvXmcEyD7ww--j|$3PIt|-)Qu9=+SDO)}$J@*lJMS!w zpC*XubCKsw9sq(_p}h`AV)+RB2MuziHQ}p1Imqjuh3o?FTDnVCQp)AxOS6UCe1HSg z&wi8aCA0(>`qW@$t3D~lt@+yXH;6^d1*wL#SYLTV35+h_3G*=#xG9~#_P!ZB^)dZu zH8POyl=Gh1)9A)GI_7sR@3k;E{O0}HH=tnY=`Gnm^!S(LNRc+k{a1B@N2b@oVKv4u z_`0R`422BCPWOJMc7VO>=iTyWHZVh4D?e>^0UJuqHz<3=#uEkJ8%@-|F*$-`(#flt z3ZDHfIcBJn$e z?XR?FB}p57X{qYWoUnQX!Hc-NK4~xu7Xfy5~vN4Wa=e$6W}e`XUDlyFu+3Jv;_!O9B!L-ah=v zCBU#gu866Nk6NF8fkQ|*zDsc6vJ3`*o85@OG{CETQ*$8UHR>~~>6&q7*g`C`jSio+ z*nzTlI{NE&?9`Egk#5=7f8$f$Xh{{`ozH6ZD(lnJ7Et zzVs3;Xj4K$@u!1tfXaJNlMjn@TLORtu&lH1`0STQe{FEczepjnYAr$}S60m%`ZfQ` zvQWP8YfMlnn~yJcv0#JJ6Pn26mqjVxiVUf5us!WMQCyFInu3ZgChgXfDnVBXt{IBh zKCPT~Z545KlZD=CK_W1Zw#L~GYtmd6W?A&$Vn6Y0A_;-m20%Ka|*C0Ays_(7HQdQyf=`Mah z`L0BOq(DIVEk8eC0G9gg(U{=KO1sCR{AaFQ(JhDxfNKSAB1 ziDWO&pF_p3Xml`-fJ*(ai?B)6(MCE72os3?Nz^#J2k-Wgyl+3SrToE9m7*mmdQ%6; zZS(9@pPNKW*D_%15YQY_*ke;egpYhg0wX!F(B>@*Kp*6`%hlQQ6r`jQKb58EVC zl2(5wF2q^q3ne%s&WSnm>f^e*^~yx1fWC)U<6_d`IY~Xh7325Kks5TGh zY+JI_zFEQ=CxkzgSo8o2{EcFEY&wR(XzdP^Szowt>c?{7_7>NZ(ZIvj9?9L0QdiW@ zOF(XSQYO9=f9IgnH!ZizA8~MU>JIQb;nVo0WjQ0f+E2gINFJwr;dOJn&ts4|wgeqX z9nqP%2Slr1f*>ObQjQnrjqRvh7D^-f{@MZOmlZIBJW42u;!PX;XREKhklv0C@w1x& ziLYeG<#X)+xr3vnax$|6g-A#4ouW)@ssr&n8{9Qxwqm~)5F2%i)!4cZ5pLCJ@<((f zeNit?E$pZ^na88}R8b&D58;s4v*V`Bx;8JTaeGhV<-;$$RgSPW&JnIjl6PwtD?Bey zetJus9Nd?$h$rTF; zejlF+v(l!*KH8dMzLC2?E41qy+HiYEGI@s%p5a({V#!64?4 z1f7-H6nE4O_5`E6R)!}3%$Fl#MYmjA>^TQAz}{7$0Bs`P?6fSY5T#EbIp_U(Zw|9F zk2b2W%8HH%m%ggYyzE#|z_?mVS|>47_!}bHaB6$TUbC2F_empa1W-aoAb$K{stM3S zj%RG9fw*CP=Pm$aGXxF@MEW=mrAeII+Eb@>puu!?+ElTQqTLbD`dXKt!KI#T~Qh)21lt+q;2!x@u_QG z={MTHCm8C2^$=a7CvW+E0X({N0x>r)XL3k3&iU=nUbdsBha6|1M#CLkDrpp{Wya93 ztj-WZb}w2j=DrU#7pRBafEw?v7ZBMZdoS@fpRh%WMAR-UB~nYxAEx&8@ulwLY@p90O91amb;_E}lUU~dU|!Wc`7s;)eQhq@bSunSN~@d?w;0sK zuHN@yFW51T>bLY*Q915cu>*Rp61EfekcZJU=?t12`LtoXox(y-(i=hvXqKGVwWD`*BieC9#NzDcvdZx&-w@)t}w>~}81>ik7g8tYuY8P9QoabmD!Q#xd zn%qPZFeZgg0wY7e#7hu>VW3WVMa1PnG& zz=tL#ZD(m+KL;FANv;r-eLF4ZhXjUY`#Yt%vNrcuB;4O=wg^~inJ_OrZ>JH zal@}*U0WCYb~(Q)%ZZS63lRRcDASVQk*nGLtAqo%a;Iy11#KKRj-i*|j1M{?0?yqDU0U$2xlVV3ZG}A+!!>n=etu z&v)7ADfK>FmX)5duw@&j>%_TLOJR-Sy*sR3SvLfq8-Oh6ic_@zu;Lxwc94E$5kK)?o?QRr z4!o#v9?>R%GtzABr<@0Fg(1&)=k1BKBG56}NAkzgw{0>w$aW{Kb@pce`NVHdz_Fib zZg6g)WTog+VMlp#_(m^Oq05Tiu_6$oTo}nj20<}*;duo&QmWYP2b>rbN0q zf(EKuw>ne&WQfI6`&zAN{+go^bqnH)t_sY6D!iOenhA0<+m(3ix`saN8fAMF#hDyw zoVJC%{0=MB(W$KzWL9M=ry?GYnTphSlFop67=c(&mnWN+1fZeM?D9Wl<~k}=HGgsr zy*O4<2weFT4HV!I43=+n5Bg!Be-V0nfJkw)C2$@9dvev7MXkeyXc<}hgz+NRoO8~*ZkZ?M3t7%N8m zYufAiHa+x#!s861;iyF-aI^tb=c8_!Z^z}`sg|)WKU}kQZy{|z zD|lP0k$i40d_ZO6K!ZG^EyJAt)EB#bNZ*R3yv$r=v)Hiy;bClPfL-{})_DHFCjACS z6b>HgKMzbGQQK6<^JVBW62cKsj13fj5YCB{Ve*9+90D5E?+2tnuXC1Ve+$B+10mEs z6ri|Et^N49##<9fKjl1go}G-!#nVatt76|DBf{YHE#g|*=r#k?W0rU9)h*_SVl%}) z#*nETK<-`>^RX5jNNYXaFl7I+jW9ML(ULG_J)_`i@D_c^ZegAKeroln+sksD_DQsd zot^r_3TzNzQ-f_Ky}56A!@UJDbezh8*ixw2b+=CM`)L){1GkluPMX_JI`{3infYd4 z5?Dums0vBB^wLqkYR~aVQzOfFH(O@Gx;oGf3iY;{ZINK`99 z8qPvZcRx>RxS^1)+J9*O;IOPfq03|C6_$;4wSz5Jq8@jnNiVY`qQKp~{&ztBipuW? zVWy0Fg`ab&U+0-|4iizgU z{-eA85bOci(!CK9bR66Kxm)9oyHFcY++hysH7j_sJbp*plUfmZytv5Rm{^gV%Xor+ zd!_~ARIH$0oU+K-Xf}j@n$~3Dy8rcEMEz3kCiBP!hlUV4`Nv5E`iaE>h#(}+X@r72 zM=qb+EOJ_v4M)nV;r1KE0~3WT~alXd(pB1>SqprnoV9@2307EiAVyY%}NL z$w8r6vGcx_2%DT9M7ny>S3AGolYP#Lu^KwL6s@JjCk?W&2nI)W8{Y>$3>ZHu4xv20 z2bJj32Trh9E-1v1G+)>(mP92HFHNkpno$4S&Hvn0FJo_o5;1gJn2an@tZOtxc@u26rT9`(TT2@!N+<1 z@0j}C*a6Pf&76VBMG9>$==`+YGojN!Xg6WmwoncK-yZg-!{MkQnCp+Q7BD<#`7_yRW;5Kgm zXBxmcdhI=0@oLQNa>wwHXX%kiJ+wo>4+bqJeu+=G0e-b9R`M)3i z{hNJsyo9`bZ?8Ea|1{T;*W`8USxgb9zm)UNws zj2q9tK3g^Il8cbdm5nl^Kt-ckCikA7kLGZG;LxhHZ{3@56IDp*i``)iQYi+3KaDpP z0>J370C^_C|J|10OHsJ#`wABLeWmD z#^wF~_^sgTKr`z&$^YD$-`(n}7!Kah)5HFZ^q+2Y6e%iOwXaeIq5PSXz^mc722OQ(JLQak{wO@$w963guDLFw02OuZxa&qd*&VR!>NE7?_P%+OB1 zfPnrm7^a%sa22YZp`f0$_doOZe>#&|OGjL(!1^RCzdtuUb1>x+WQ zW~q95dh#Tdm80%zAz>568oh(79bkBbjiSlerHg z(;s+7u4dbe>w9l?rcf>pgg)vQ#n;mXsTJWfV$VB=_yzhClc_FV%LC)u9|edTgSr=- zUE!PmXYTyZ1i$Qt!2kAU=(3%dT&{$W-M6e^dRt-maaGavF}pmp+|;w|^9@vSVYF38 zC+G_rPa7L=dvtyhfc>bds;>PM77B&t=bL}g$4c@8LKQ=YqpuBr{Xd=Ie@7h9949oz z`XIt>X!4$xgX8BV|F%&C#9%_N$=SL*u9mP+CATW17HPxUx4cCXn%w$!=s}3F{jN?a z0HaB5KQ&m{P}o}9U-TL&0bW;FlBxxCfM(J=e}4?u4DM9#Kd~{SKe|nE<!n9J$(-|cmd1~RiDS5wCX<;ml(9GV^Q)k zt*oqkO^Ko6S+dK`$$2?DaeQ9i-WL^W%=> zpvflHa}|teeZcBk+1}M`J&29^I?Ww z8!e4Ud&VIAvQ&shKPnSd>gPA(_RYslkD^VG1uLZYq&y}6hpn%Ui?Vy(h7|-1KoAj; zPz0nEB%~FkOS(}h=@O7!LLWrwSOg@bySo-eI;D}=Rhp%wbN4+9$n$(ZzxN;iQ1^~= z=3Fy#%{6ngk)SOr)l?6dqG7M01|EC*qZOFYF2=F)RTw_&-SJ2G--mO@*ab&&cUU<& zaP(3kQ#_{KFeUgW7JU47rux;B>N)&3=Bo4ESL+Rule0g~mem`RNRUeg$pjJs9tM`-uM_Z7G4?j8~3Zf)IkmqLcT`#8w_P2)|* zeVr4iZSZOM_EgTwp}(JJfH|t)YF)ry;d#tH6=sb=Nv2WK+N3#9J74{q{(sLMX(i$loEl6n|nCmI&@QF$No-*n(t+0}d&@?zu#y7s$DU9+vN6`NxVc50wOiY;~A{Pq}5!;)*gkGGfMh#_|lP?WAiLCFknPm6$ zNxpiO`9Q;{By}(iZ;NB5_O`dU-GzJS1OyUme%?X94W#0KbW~g@^X=PXlU|DL#QF;= zu@~sJ1L?d}Y>RE9ea`K-1r3wrtf704ZOsQsE-j;2PE~3U%*v9rsU$U~*BKD+O}y`g zZsOHh?UDVA~(lm-`=xr?+FmPSeiZaaOt!J@0MtT5XU=-$s=WWOaOV ztI*xF!_5s3hE<2Mz_s45p4RR0B0qAZ76vpx1M5aDadUCU{%RY81=aj!qg2pnF(b-j zF~FUXYjwb32v(d3J(2)y41b4ti~+(GT0KuyAy}p^_aLMeC1?c86&>Dl+d~23L^iSA zbg)P4T7Kw7H)rndJWNV-YIobi7sdXEHEd5p10BN@L!% z1pZt@U09Y$jgw9W?58MPszs$B4F}ioS})HI9KE7RM07d&i>Ce;*i){9@YQ-@PQ!U> z+~UW-4t!e;|s4gKgzlvzDoGLIbr|FPZu7q2VN}Ivx{Hr`@yVhEb5b9 zh4>L%e&D4vQf*!Hr)A;tutB~kQ8#Tvx3GuQBSX}I`_4)lZ2oqpjrF(t|M9r!yGaOL z{W??Cm>n3$>UEMdy8Ai3B{H5Y3*HN2l9TjqAwP?m>LTsiu2PeODqi568_A0fhc37B z70;c?SLD}+9}U-gy0v$g)~%yfKL%EBvT8D(Yw#zgxerS%f8gAbCy}=Ebb>v@0*;Wb zQo%QOqINU2HJ4eX%Z*MhPetWZJuguzaPB*}fpTlk(f%grDKy1jwHZ#EZ1ysmaWG%M zv36?b=#7}e)j8F~ayKN}Jym~fC`~`x>xVYmltIF)NW+nwjB&S) z%V`F3j}{;#qH83svsIKPjm=%_Gs6hEJe{mXa>oqlkGn^TvM1ioE<_wSwA!^L@?x_d zn*a@{i_pH`xCjTn28Y?jeVvf)#V?vVi4KAPb;e$Ill}RAH2l~bM?xwsDWM_-G}DsNZ^bpkc8kA z#?c?8-8h0K7(x>*Ust%6qyx@n_;V48St&uSL;3TAd=_g%JURTmJ^b0Ss;VlOl|lT~ z_sVjQC!q-Gy&Z{=tQpDRjsrRmb`ISK5pUm^5W5ks?_>m0X}-+;*w^Qa3<_!d8&>6*bTOq z+THQ46Xq0pqEgF)abHaDM#C)9(y!w_*Uyjk<+Q{RU|rlSslbbqv-I=8^H5JxM&{M9V+>}72bab@93hbt1zAGy=HFY@~P zAXwE|Wwc&>nx2`FGw|}tGS(h_XsRvWxxfhP>g1-?A_(tz^U;3v54nkkKnE$=yhi5cnatlmY1VatH|rcBk+^$(7othEZQQ zgAH7Vv%WA+>VT4(+IlF_@$jwd%}xS*i-&V_hM!-ahekw@4b<1vr5YVvBcr2Z3k$n) zzNZ~N)NyfYk~6B^O~X2PP#&dXdp9jfB~7{{)iFb(ukxA5#-Tic!hpjM69wj6-=?A& zLXGG$k$j1U2#A}^MyRqce1(q*XUXOnU7l@YvqE^eWyO!0Y9~v%BR8kIpZzbl0#a8^ z4_{gRrb&h+*6S(Ekg&}}$cSZW7ciqIC3gGdIQgQvAEugqTh#6p7A%$D&dww|fA(eD&vYH{IF=L3)^tzR*Vl3wJPllW8ZM9 z$ljMM>52uqC7eD!TVgVCd1p+qZ9Aoqk`X zcv6nR|An$OZAIpsZx7AMFU-j(4P0c+9Iv#O0BX#*n_~8l%dI9%H#166ZBnNRQV&rj z17YWSPlCJXz<*qjcLtsZ<8Dri8O>5htAIxSEq{Oim>r>#!$Q}Hkeh&*o>-E}fhkx8 zc0!z#wcgA!mgHzEJ>W8|+~um;UhMcDH4}vz&M_G(F<1>*k~qt(t3170@h{d~m$M&mD5K^&B*hNv>5m%(U}KXx0Db zV%Vk5f+-QJ^8|4$)U2AxYHVjE6*F=%Ol1lme3Rq*im0sA`tj8SWg@s4_}jif{(%{z z?h4ml^@}M}bwr2h+VH&JAiEh&E_&%>K@g5pn|sgeNqPJv4)d)WEj|`<4-5WUnKmvB zp4YFvADq13)_XXM?8*NV{ZOzX<-QtTepa)+Aj2xuw+-t^uO18>ju-0C)T|Oz>5khX z>xI2uL%!(v(uHb^a9tm)5Q}roaOveONd>8##bBGNkB)v1-Nooc&QifxA=|9Mn4>Sm zk98NZ$&f+~W5Ljjk)f9q$P?9z=L6@2`nNyLlv&236YlXm6vy#NqNK;UiHCqN4{|r@ zmRs>k{VaUpxiMl$lAMwWmK!C%H!bF-qFKv zimPiVur;ML^Im5~J0tbG#d>E%NPCScQ+rN9U>FG^{!YbZ__J?lv`>NA1*q5*Gir>6 z7RC5Q$Yt^Q=JHg?Bn+eIu1O2sd?X_yTIbO^fbm#do9yGJ=F7CbmZx3wRe1K)tngSR z8wTm`b%<$pZX~P!hf2|-SSv#o&`NdPXtNlrQC)R4H8t%BbLfQRN5#JE%hj&X_S#z> z*mj3?*4BEA!Z8DV?J!{%_+p(^NwI@fz2bDjgSa276)^UmGS*>AP)}*8^=eFCwvel% zyCVi~XzkbCS*U~3xv#5_)|zCv#&6M9!#Y#H<1IKnGV92acSDiKh`0n?&E&wm?kf;G z6p+eOdDvLn{KLmfQj@rLggq8L$nJVj*V9yuaMSx3og4lvg}HT|0yQ0w!JIuvr?k^k zTNQZ@|`SRcCk=?g$j*$a|qg7+Q12s$L?q(jI23%Yj&^LLFY&$rmdYnZVgKJz%AC zeL6VX9oaPy68CAFn~a-1uSE|Xp#wQr`I z-a^$+3at}U9c^A`FyxZT+Y!iDdAVKesv2;v&a8xS!87PDqALG^kchla^vKuP)~a%F ziV@;@Ej?P{bJsCV@4YuAz8Bibr_^#x-t!Ij*N=I9SN|Ra<@=EO>xX61^WKQhaI-J@ zh(h&l)X=2L^nhi*BXvN6NVR*9*OVwt>fvG>CQF9bK0M2}*zKBC?N3#gc50=E>IFQM zD4h9(Is&;m6SVJ4GCWr`qIqk&Dr1$uBsO78`37$;gKkB9?9+omK&b5Sn8^jkK1tna z?@}*;%B3?{5Sbj&+P@!U4d0J_bk4owcG;n$bGN-w+beK#=Z?4n8xDRkNHH?cJ!#9n znLq+4I_+|gpru`R(=y_GjHp@BKwd;PEQ!G4>FI2+52y*%jcpFaBm8~k@l3eu*@<%7)+?0l)XjvvR}*zG+Vk7vKQGYqy)# z9@^a#DI`Em&wu#vVY++_^Gj__6I!{{&c@|f%*ZL#P=Pm;hq%qK>MnUSw)W5A{=qkH z_}+|eN)rFyD7j%2-{awKjyob>fya>)8c7%&i*F*(d(b!O)N(INXJ(VneZfDZorzb~ z8RPSsMFm$yq*1Hq!M2);fz75ROgrKA9Nz1C@qdg>#Blj%O_7Ng?9$Tg7`Qdnbr~f0lM)Z z@v<$i?}GClJMqJ@InQ9ZIn#TKywqKz-@9UYi$}W)r^`Mwc#E%LSqQ>}I;Y;=!BKdu z3|Ea_nQW9w(`N2aE_=@rx3Wbe=e5#TUi*}caun4Vpd)@JChi!-UZ+&ZIFrMu)E)VF z&!L9K-Yc1cJ z)@LJKx_Xn(*DjIPvC7&AOA(jL;Z$fKlslI$Bd!yB-2^bn_X%h)V85ZOE;(L zbTyaaKC2p92k^`09qyL=^mnd!mVvHeR#do?t(%d4#VR0uDn+HIc_$`|gq1UV(2_4pyNO^Ryz3nqH92FR zP6AtXVnDzjl~mtC!Cm+C)Mz^|gRXlCJ1vP8>C1dFJli_NVFR&keJSn)Ms%|TbW~<) z&#F|?RC4-ODG;xZC@lVQ7SjF#`!ZDGwUa&CEtsU2r}Ozirlu}ubz#mgJqt!+@EvU( z^Vw}wfB%VZD7=Q`dTkkmBhua0c3A&vh7P|!#T4mK zVQY%=qz1pg4L=gq1apY8y>=5Gw(2H4(<1Qy_(9f7F>f=L;r8vo22fdFjEsrOf;6^VO~-={ zv|6DNexpn$`es4FtJea}hNWpM-uY0I}JOIkwc zNPMqETfeuhJF2HT9JKA+UizWXr8S=oLu4}j`D4Er@%mSIf|u3`W+%lYr&mIgA{(dP zWV^uTF;|nq$>{@G6JWn0x2XBr%0Ygkh7T=(Z#%>tg$R$9a}DXkkgGH<<5tdf#-xB< zP`E)15l;f-I`O%NA&GwXJTP}}wK;myU!mv1$`{BtH!)tazpfGa5rV#UIQY-L$hWb7 zNMM?ViP^p|W|Q`s#5TVj#Nx+VuR-SJz2U{6v5me0L*tv>P^0qA~wq zB`Sd1rn6tAz=kHIj7pw@9EaF9gNBmQGI)LW5BY2nKgniXiyIN~QAeaAvxAb_yl6R= zlyMhe=iX0-DvRxbiCdfFOwCnW(A;S2^6s74KSViZ=?AI{mkwm#lo*!2J7x?kA@_g$ z;GP}*QEa)x84$nOa zcP)OV`V<)!PR<39@WNt3FP>)p4UG^q2qi*6aa$3_uAf$3shee>`7wdUZ7?cY>w~M) zBYH;0AC+wUJE?!%SFt5grvY}v=g;KDP|Pi(Hfy;u%}bTiH~7{RlVn%T5BF0;g3?ox zvs>Sf`D<5yWrKKuig$B49(0R-)BWBlS)~+-C96EEVn;q*FL7@Prr`;xb4`j4mY2k{*9~hvf zzd1~x3J-U-j-~pF3|`iK&*d@SH(F#Ta%xO?|9FcLKsl72PrpaqnR=D3(_|j!vEL+L zwkYdc1o@cuS#u80rf!e*hx=_PnErq*+HPtvQxC2d4zb+6t(RYW_>$`NI~U*OmXNvj zh@M(bQ17*HP)-%zhq}nBsQ5ZX3rIF_;ad!6zZhN>EE|<w%2bRz`Azh z%9UGtdwbztyYG#2Ja4i7Cf{LUX*0#q;$j<3@)8khj)La#?VD*LXD^}B3QJ0ggPs?9 zrPLVj|7yWRb*}I4b(iX!#%1v?V;()Y2(p(G(5N7)ym46ex`7NYLCDr8_R;dLge}a{ z^=ShF54g@56`T?7?r-02gNQ9}e)OwCcUFPu-5cgCzbVczCsv#u56Y`yZ2VUEa8+o4 zJDcu?9A%4c-uVUIP8-7sVT{< z|IEx$%Q6<8s~mZ?H|)tz4VQ4wv%m4U!4IM@R1sDmy&0P5l>k1YLL7#B#1#eV?Cyn4 z$n01gJ5piWqkw&;xd-88D8B0{6EQHXU)k7i?J*yhK5Ec2D?K<0q)U^i;Zvz|`!E`k z)&c9FjzIACh`9C^KJqMaqq&;6o!B#7ZRs!j_UDdfhJ?i`qDK^?GMmY7&B3C{L7ivD zjDo(ejd5NQ7O6=#xwtCEZyOv{erja+KI_1$jCvm-<814j9opF-xL{fW$GN;{Cr+h4 z+s{F?P8Hu<5-0Q1g}3av&QK{(JP8l{EFYkIpxc|bZd{I8ZitE^#<*bz!tYty4M&_J zh+S4GUYja)IGrbo!!OM~#jSbCxI#^qk#HAa@?^c+D>0(WLD89+cj*??pTG9Q0h*1L zlHg?Zj#*QjH%I?;QIO%#+Kr~I_vac86jLz=#|C$4_XHOH0}J%==P7IgG2azRh^xT%~7mL!VpV49b3V^mBce!9_;?5_G%< zuT|kR;8o`I5x5R_FpGHTC%5^ve&IA`_b5`j-YSXbDm2;OA8>7mXu^b%L;qv6*c69n>YW|pE_bKsJOpR-YMhZ)vl}qD4TXjF! ztxpsnq>+#nV)S{&+kV71aO*CWvMqdW-WupcgyBK;zfRGf=FdRVk#ru~aarO-(t*p| za0%({o*GOrkooK!!<$O$G2+oQ(~3n;6Vzbd)b7iiaX}{wY913O4UfZ^oq~QIqpe&W zFgh*AvKDsOvnNtR?T+z6&kPqE&2c__gG`#Q^NHiNzD*-k8qpmum}vrohpnyMn;HV# zYn=M`tRpt|E&f-}YoK;<8XSylLb@E}&kthG*S)BmNtnJf=1KMYrO^yvA=FF4p;3$- z=tw|(1rS3}&LF3IvcFm1Tgk5&WZ9EYCn)IaX9JSXq>MIhd=g?q!;E*lHizME*X-Owb~4>(_e*B z9cFkilS^u8dS@(FKJig!Crd`()?sHGHZzC|gms_11bWU-#zP z>Rb@^L+-+!K3*kd#=S%R)(Kf&hKnJ@e3%VG66%ta#>Q{PeW(KI$KPW7*352Do1B=H z0GIOJrl&F8h*>ii(!;G10!bIo$Dryo+wGnZ+1a_oy~QklBzkbzkBR)}ayRCJi>&^f^}>KM&!Zm&%Cd${4Tk@wveN zCk;-8lGWAVP%J*mYr}ImTAPC%81(8l z7(%=fkeXE)9jNADBAXt;7X-IwTud-%36rYNmM6#pE>S^5dgw&WNO8<{?}h;>n3$W^ z%yPDY*Y3)d3aUZyQ2<0ldMz_!^aD;?>hV=t9|kFIU z3|Q3iP6Ud-?G1u?s~z13wya0{1~13UUraIW?TRHn)Wsim*P;g@f&3={?c!oZjtu_a z+q!pHS=$@)R8GBTIizF`eM?DfCtTjcF%~53BjxvSx(U(e8zhVgDL8wpXR1`PRE)EZ z>`I4M0b4K=L&?;3WlQ!fI}>IRUmK379tgt5K1dV<&)f5e$TbNH#2!JVw!_VtuFaj= z>su8(!o3&Nt6A@vj=%Zw)ivIx{*TT=Z+%{lx;@1%SV<|V8KO7qmE97CyHb{wAD%2#8j;mx zu{xjaq{TJ=OcEx2rx`{vnq>r>i}a9}j)j1*QbD|D?Fj9UXj+fyyXCjv>eP7t)0UK! zluQj@m+{x+C<{g(4<19hn?kTOjTf~k-@a*DEMC{tt zvIdEVbbHG?uHmF~Tv_0w2zq9aTobUI+v4$5!CpKS_9^GVIeoP8t!$sJays=6SLqY- z)8l{bRjQYmR-I|Q@l6po3-u3{=QBBobOh>0lpKiA`)z*7abC=%R8%bBD@oVtR2rNX z5g)#?;^=WVHA_;MTUa8^J?BDJvs0bm8MjNJDA>`k}nlMY! zx%3+6v@C7-?Lo5N!lRKbD5hPM{mUDK4n8qrT@+iRDhnMcGpT*&Az;*gch@0pKR~-~={KmE#-hQ7Gfzas5v)(CD+oJ`$WK zTkXh7Y{_*^!6!d{tTioq^PXTv1$*6LaeZICJ~d(~mu~w5W75&ummel)J0t{o2ehCI z@#*8^vf93Imr2);ak^=OK;DZLADJYMHKE>yM8z*JB9b!>a^&(s1FJnY1-hg!)B41?V2`e?R zDX9ZOgkL&yQn>O-tPnJ&_TcL#S=Hw=iFgJ+Av&QPNr-(X7hIZM#$6OTLmH@s(+A#T zVeL4kQho$3$wru9dd2QhlKZf;!*KktKXvc&tQC^1wdST+7724@JQ)zED8o_B60G zdCKkeQ;4B&0E>Qp_nhgQ<7c3~LbxNHr}p7&xsk&V$2pEr#9U{@Ze_LuR%yvt726Z! zk1XgBArpKnUWK2pQQ{p!*TcJw;m{rxT7}*MC1Qt1J&KGU1x~AWJ~$L$0h35zFOHq{ z8$J3|Eyj?F$LIciZ!dC<)oYJNs-2Q(Xo&0CZTPy>EFaRA+s0qg8O`QHB*2i_N5vQ58&T^RZxl*`3UaPphWloK|N;qzhQeJR1AgF2nFfbR*T>nr3 z#$jlrDfw6Bg%=FAu)!4;h<9vPFYyR*Gy{e5&a7CJE%fu}SN#>Fyi=U_?@uxnBr6i4 zH6n$VZ-a_(Z?AIITs{gka|}6Oro48oZFg8S5(xf&Mu7?YCx(mW0%nOxyv2aEuXff# z<53IO&454Livea}paQ(zje0oh5s>0ig;pLv0oFzdnU00(e*0U2w@@=01=P+l1DOeG zzwp3OIlKZ?ZMCdPehrw~9VT-`d6j+*P#SXE9J&|V9BI^WbDx?*0~DCT{%T3e99Sst z5(rQ%hHDxeiI>}qrD-ILu}&Gp+%>Op7Sg>u9DN($W?Dn&akPOwPk1HbKzh3LGm7c{ zvnG{1ou(c%<1Te4A}CN~1Rn$sCQBWwt+8cs%Xe8d^Y4I`A&wyG`2F4w6Zpob=g}Jh z40ZuggpeqRFF>Wvq(Ie)#YMWZGPnT}?_ArHz4+E+0(tow-xE!SErjy|FoBl{! zaWRwg{@zf+CxF_=>Z%pu9u%VNfPZjBH&qz71dBMEoZSqAy;-8!Qzh)pT!;n~^K3NL zmvyq7U5zW5z)|AmUpWh{NY{LEc%XIO^5lfOKHdon2d$V1mY^F$MJQ{vEyXQTa`LBZ!{^rUK)Z-+)ZgQT1IfP# zLh*@Up(}*veJU%jQyq;+%gD%-Qrm#Gmi5gITUihR{Kw8*Kv1*aU6RCnd<7pCaU6kr ziEJrn3x22qp3+d1PcX=P7(biwds{?`r0mPGH#OxXH-)V_@wz)Z%l~z{tOmdpHi?~c zJH~a=T>z=)FjPE`&s-iC_I6E1$q|IRf#RUdW5i@v8+%2MdwJAdIe~^pyVx-}n*H~2 z;R>83R%LKG{NMc#u%}U6nTQmJIJ>3m`7z334+;YK0#sbjJA+#w%I3Aa(?OoHqH*x_ z>8O*F*-r!47fZShzn|p)#~M7r4o&iBJU#Iw6rdtr+mjG>CIk_^1&%v2?YVgI23vff z!eYT{{MGVwMr|(XmZ!CSik=LMVBYx8o~(krXDfl1;}}ZmlkgvZQ1^u#`>B&ZERU1L z&s7_YqWRf+BB_a~nQ0m#VT5S6$5Db;2o-%4;#^uCEk@_`2)<5gaKyDTkWAjXa^=_8 zfX2*|x6Bj=k4*7isUM32iG-k!Vpm4#C?gKf`)E%4hw9nFvOz9Va)kXUP~aZx`-=E8 zMGI?fmFDT8Api{#U!-XW>7R2H+8kYT{nb>#$R&ml91{Q%k}Bahm^?CCTGM|K^^GLh zZ!^6tEN4de0OSTM(B%KyAB6U8;yV(1GymRdGTbymbCTQ_K0`6>g-2?4r?*zy$aM{u zV)}9k$vUTDks^~~EYm)M?k~^%2AD{}T#z~L@e29$0mzPKk`O(?TM+c6LfT{k_!vlt zh{C>G*z>v0JiN85ODnSS6vwB8sMLBw4ef;JBD-OMnOjM2g0`hrh+c7iEEsot=Hv)O zf>UL?cUXySLn6H9lh&cpk@gBK@>1Z*p0w_MQf+^0iOFzq)8>E)&e!X)qV9_}{V5DmV zKA%3lqB=NK4KH`}|NbOr6dv%$W`XakVZ}WaP>+gymgHXH93CCb6)}naz7h7pT zLqc_JZW{l)ZR7 z==m}A`x_ZmDlFW6S~Cs#p?`h8oetRBe&g|DFH|O;H?ZhDRS>1q&lz%Q7=Y~0|3UJ* zDZqDgKenfW5Wabm1nOJU&8YK&xd*6KudoHfuKxsTk@{dyNAoJluqK0y6tCd4o(cb0 z6L&Lu3&j9Bc6fUp-c1yjE|Q(e@vS)n2Rw*sQsFp{Hxc^$tjqb7#|?D&=}-}@_93D% zAK}p$bu3+lg$11Ydb4Zvpv+GvdwhXz_RttGPVNg)a>X5nDkX26!UOg!&<1y6Glchi z*730y9;rCj_=p|{PxR zya0uO1iE1d;a*Z|+25I8o0O#7T(_Q(izpmC^VYuVz@`dCU7Jrxnh?mXCe}TpmIa=_ z%joSrhc|mvq>L3${IORz9trgd|E!lc&TqF*(;S$MTgR;|j{wq0+8(qfVHKwAr z=5ko+k8PsrEV)Sv#4>9j1wgZh#<{B`K>u)zR+`sL+ymVxtq~y;3|pwd;(zZ(3hs7E zDds-*ZWL@Fb~-};|G@j{f51CKi4grW49gCAu_l@Ai()H0jpv<~Bts({A16@rXgjYy ze13*AJ^*I;dlT^h zP5?20w|^_?8F;%~xJ3IvnZrqEI8%iHT2NOF2mRz$e2>ZVpsR9@8wU3_VK<(c=58j`->r&Yh+^FWw1``@UyIk6Y;;D!AVgpZSOGiV4Jw}I$Ng5VTHKN0^Wr*;WmD}9ycf@fj z<>mFxQdf|;^FU8poqeCtgKSfHZ$yNQj?#$#aKfFq)e)>?gyXlf9Xtd(SO6D&xQ{(m zyBXrc-PWm~C!@0)NEHHJ0|Arp9LTfTarOV(;s?=6QvUFf`yJ92N9^T4fUYs47%6j4 z6wZSAb$5%}xL8j~It7XNfwG&`nH{3 zga4swnA}PSSOAa>i@*igz;kdaxdbdRu8&}uW&2dfIHNiZo@<4bLPQZ4oPk+s)-}l_ z`y@VZ5CUjYHzm(@yvxD(FO5=&(1PhKnw+>uP(?UH3DQa|*zSySa|6^-OnTub$i4XM zib|DA>O>HV-zp-y8O{va7S6IAEE^03Z14}UlPA+72cY}ENz`*Z;DlNG(>-VHK1VRu z-DZ%yKPf8mh>}zgNbEUK5}>j9;|7esdbaq)hh&aFn9N{Pb zfWv8UWq!OU>d9p{5w7T0GtBFz7Vi!6VJQiw(UQm1<5-`Wr(K1?+U0 zU-=8#Yb-t`_)xCOwU<<)hi-|BJn`7JK95JpZx^dutt&1$k3><4b%%j&!iB;>`IG&w zsv5BS5yX}Z(tm8DKs06J_ z4vl5eLQ9iC`oa!SV&>n=GJ?zM_LZLQ%qrpA14YgGQ!x{Gd~bgIcCth3kzKtMIl$!B zj`IG*2B3$s&yn6AQ-S&X3Pki|VD4bYY@j{4)^hIuIF|9* zm#ULp-fc%8zYvskcQ;~nriKdsaA`5VLZ73KYjO7Mg@m!07#as*s3=G|HfBi3) z?1D|HEADO`2S$~;*x=f9O>;O8>YS%l^#LeCl8A6>h{7$-EEaki;9P0O>QT8soezDF zeIL6xyMYaBaRuSEhaP*N!!}NIp{asNVXxqc}^k2w+2#h=#WC&wFWMo3#oHy-rJ+G-O!!UF_^b>Y4SD^*fE+s^uZa&dLr zvk$cEU*?^q!Icu=91C5|I%dins38*r1V(ukZ41LYWDC6sBpuZq319*RqYSptadOg0 zE>>PUA0!Uhxb7yIg6~5j@^r9dk&X}wNma!LLR#sM~p91j7 zhWYv2C1sLi_m9*{IOr?;cVEmvNGQnGymkDX^>;|gVv%waMDObh@m`AyOP>Er;t>j5 zvlH1xRf;+sN1*MTq_ni0B$Lpb(pQrsE9pQhPPNfD1`b>7*%Apb0P~~b$Ung(dI3On z-MrZo^9&nTfxC&5UWgRi?YM-bKP-RIdhAg^zxn$32n&c7h--dgWkjkW2)3=o^s}hA zVHA%Q3=V=eFN=lRIP-Aw>5!5A?sP=?F;KPK;o%wU%+_YmW>x?2pvUtMa_v`2cvQU6 zYmPg2o-7V!vFs00vxvdP%U4G@Kp}_9I(OuFy_OjR=-SVZ#6pOuq z8-o!~3{^`^i{HL`=bG}d6uS(S3ScmSP09nd+c;J#b|)_Z{u(b>d=JP-eR1rN9#Z!} zAtCdX&s2+2D<~RVd!9tGkYMR-wm$<*Nrc@KETv zdmg7xLU;++4mkR?tatTABmiFnBNj)R8;1(_Q%$TtQIe`_>lHk9D`n?`m+UgpO=@y$)#hX8C2n^SPhw3IqhDe=VLDo|8ft8unHXjP<~L?$H!o6g z=zccZ;ct6Z=GzR$bp_g9s{8${B>i&G6p)M?H#S>`$3%eyDZRZ);fD0*eNqi`nSzEQ z*AB(em~u_B!JY-Cxq)c6MNyYK6D?6WeD7^iDl@ef!S{GJVWe5Ss#e)tckhM<#W`n+ zE)}(rT)uoC=C095PO~K9C9J79JsUgx{fTDO7Iit*oRh_H=!ljV^q#!{Hs18r0_#C4 z=J8MPF%XaJR$d+|ytNy3X$f>r!EVI?*t zL*9|pCu1`STg(gOIq`0)-?BbbUdP5&$Urb8r-$6);ruIJ#|-12MUElDnBQ01xA6Uvh3DYN7uK?3AqB@a|VzF^%?}yQeq;{HUoe5^^q$n zlmfS68Istlo^w;7WX$VV$%0sWDaOl;@^Ekm%!z!0)+^GW6O7+PnA5K7W0m$ce;ZhIpIvD@bKBXke#io&eA@arEcL7iYArJ9U#Qv9K zRO-I?<~Hb#xc{bCT)V<6WoaZPT|@6;m}S8V7q7(~A;hDTTBSgaw*h1wxHRr<^q{Sx zZU%>LzwwgXYHOzdYN_se-@f2Uhbu(0A<-AU&%&-%+PCYl9ng7bA6!&gG*h2$(KbuG ztSkNHLMr;O1H!PI)3Mppu5QWK2I|E+oMoCixt_-d;FUFd)sG$e~6DNSMLln)%{!j9-_;KJLqxXh*W264Ev1f~vvG)a^JoT2rP=mR%q zBAkPL@?_{=jQ2i=tAr1AQY|XF&%OOF0~C*zEii4QGP3e^z2}OL!;5i7ppc}Yn5Ovx z^Gsl`V&1UI)(5jz0;w_eK<7Eec-vR(m$lt7R6+C_5P+<9y$Lo$CR#BsXeNkF{X=96 zUELU=)uQ0k)Jv0gp~Tq1MDz27lY+lSnO6aRs^;8S8033>Zzw!?-t!$sQ4F*1KRuIp zff$wB9k0`NZfD}QpjM8m-+tM2qdDSwOZTDK>ZD(Dr(uVPptz7u-tLzjiXoXDOjVKU zlU3E+2!lS?p&qC-rhcx2++v)X-#VIkK;BHQI#ZL9ihEwI_h-0E%Y@P=UFp;zdA^mB z$UoQ`@b1S3pg!nC7Oa$@t1`@R$#I-}$c>3!?Fgt`JTW~Gi16Yn0g));JO(m z9mDOY2vOL+VC=!oVdAtdSPL`nY7mPybQxJrAo^_ zR`EI&78afsH~B4pDt~MI)|t2i8*Ol=JPwZV3bEXDkjbAduLZ#^lzV^<8g7jfGEyVK zfU~4;3+5f1=8k%RYesR?@MGgiFNWT71s-gYYyzfueL%Y)1Q$Fhwz$Jx5UzPeQZ-Ig zEyv5@a|_w?)$j=KrBU*eRom6OO_{}geMLsv+>*=g1v`H1F+i+k1tT1EKY+nlvl1hQ z7Uaj6jWKm31@)rr>;u0Daorn+Ziojf=|XQ~fl=_P%rS@%8#I4gM*@E7hzrKbAydU&KxUzoC&4X9-vBl)(W{IF8lf#m5dD(~17#AuG#4N{vE`%NQ?x zdR5Fu-e2X#p&I=sKX_FYv&qqZzdLf#w#HrMmTltrp)pq}DdYCizyK#MK7I?5lL6#a>q9k(qnjPG zzj;0bS%&0h@gVF0^sB?3O|$Ln5OI!%Z6p{`Fo_KG0yHZU>2b6{G0^6 zXqIdCdr{;K82uV3l~8V(D2yJk>?`!L#`aT!@edXod7&e#L!|Lg0i)#G^p!ST2RPHzmKr=*Jr})z5d4)j(#Y5gu(<&ri>J?7No5m z($ZkZUBOfhP6Befk2pNu6W^p6|Kf2-X? z1osnG%?}S&2cZ7T@?(~GPzkjf@Nx5d9iJao{$Bq$1EVGw9Z!y~=A=5b9zDESHN!N9 zvaUICH4vW@Dkll0*c3u!W6 zoLJqMhcel{SI5we^+GP#!2mU{{;2GEb_)lDkZL$1g(408e@ijeU0BOja@Xyiym4sLC1kdtCyzaY5wy%a0Z9q&u zvbojpwJ~`IdHa}PQmX~J7NOtUFB}XioSoxYRKETip8Ukxp_%`;td#T{ShL0Y1}?4-XYSw`yV$U|_6{KeQ(umS8)c~Sk*!?WC|Pc|0crDr z!ZTUO!1Vp+UlunVB25FG)2I=nZ776JNDd`4jR1j%tq`Rd%|8w!9-}krPwVridF0b&+ z%$alc*=Nt02dEz}-H{a7?x7F*_>HWli^2N#oyIppCgh+6$g%SvurptB#ls~sgUenq zlk_@qS7t@&#D(iY^`r|})g8FEsSj3npyfZ7dR8)!Uodenb9kZI4e;+YW{$vrz6tiSrKas5nfd=ALLqp89B@p;@5cTS zhv`Bc%z^Euc&MTe188H6NycNA<9CVPG|rMPxP>2qNdKixR9*ghs9r!@HxRka{?%;S zfTkO}E{b&jL|<=!o%30LL4PpLb6B8|k4)*XuNroV+?%h_1Lamx2ZuLP{|OKmTR<$> z82J2G(##8~=+?B2{bdz|fIgKojFevlLVbG)lCiUkLJFM_w>+5!S}OhkZGmC|G5MEp z`ae1f@LyV}(G*K#&r@?L|2jaxbix3+jkb378T84- z-u+qLs9dD@i+7`=8KPtaDKDSTv&r(Mb=mu@)?=vmA1#(I@?zd#kn#CboXjJNHU%Jn zhDyX}A53noXr2Wa?!yG=n17_9LkKvT_uOv{Mk5m>yG?U9VD_ZsOIqE#%T1c`wqCgY zO+|6;%c>MC_LTIsjg5_oGYu3p8hC!BKy@ypUuNUL3PM4iBe9O~^z_@~39Pas87LUL zOobeB1V5<%p4CV{zOZ(Xl;wy@Cw{QiaBXT=!+r9UA*-P)7aIZVk-w~#%W!Cxm#tGi z{IyfQ?zz)TI##|9sOAEM=d8e6jn=*kLG}onZirA=yGA9-&nmnTWo~`ZVThtqGf4B| z`3vJKt5jCkqMVzRbKL@|gs)FGxM+M}jz z8k=xyprM=mwOM8s6dMByjC5i-YjgiHr6oXJ4%NXClj>N$`Ett2TwUFI1mTOu%sVy_<7I^q;@pdaF^Xz`l{v3Tn;Dkck!B?Y}uw+9`CONCgPrb71^9zxX%YX2Vvb8M1(0! z|0d_+Vd)`MGBsS>8e{9}mWc1(?2MC03~7bisa_s#jw}zX8J}A_C7R@#f8&vnZIIM_>W{ zy)gYQ(rn*u{+%Cg4E1&c_<@v$_~+Q}@>%oeir_*Q2dn7b7EC{=bXQd~#Q(h=4&qeY z1wGR4uUtawg~o^|VxF7{PupmonC8*t3~>W{%pX5~WK;G{5HiujB-GSkhL#OH9d9x3 zv%x@Ja-br_=?uJ`IvjOc?}ro7k33o>JjZ$&O^Jn_37gJYFO@^^EIoZq3Ha3Z21)=b9BH-zuS~c-U2h|%t4k7ctkaBoL{z;#m!sq8;OfdcX1ijU)$D9- zYHHe)Yb0%wh>bwEw9T&BYJ!x(>m0?T_a@u#$=by=pK=*DFT>P}C>OgA&ta@)(-TA* z2drh?iN(#n6=X=guu^ZwVgHnMOuB2oTgK>~emdb7d@Ub#=s}^%7iF%Rl}U2+A~KCD za`jruwolbc{@FoCx93iT{Zq)SqR0W8_$B}Y^=C2UJ7HICm*v_nhF@&+?0d^(`P?wU z!5ie(LramPkU`Nts;@(~@TenrV)t{Y%E`EN@4(36Hp%em*r@lGu zd8IFn45W@x`nFe}kb2&|>p5Q;StI>$gQS6ex@*Ffw zFjA+Rw4D`-O}p4ZGgSctLNyx-nDQjypVcPMzW&W?}TBd5iDx@KBLt@QP!)>fmy7 zbU(6l;T<|To8g82(ShA$>QXBnqSNlr0Fr&riZ1%Wl;(JFpxLwvB^cs77bRd9FLxwd z2`%9s0vOF3m>hB;lO!ZigP<1?`hp+`#e~Os0`u^XU|#I(U{Wu5oVS%(F~FZroANcU zus}e-dhZuj;ZZ?IYRhaf`-#I=IV`X8XX$}F?$nHP_u#D4!=YB9xRg#=QEtMh9v`0+_Dz%PZPCv8R4au+S{Ecz zg)iI~Vi{_FU9k9QZ@4U*T7fCd$HluBruMi*yJ)~@o1XlthS^uuXs#vY^)XIq9{;(s z!bXAK>&m-}+a)QQgqVrMqsKefVRp^8iJutVJt(uP!bW&pNq=%Gb;l-Me2+|9?Fl%+ z8MH6ea373dFl?M!a7@H=mz4go3%|c#%&x*Pe|tdq?eYZw{?qIi0$Y);vcL}&DC;-f z8&b92Jd0EV?} zi5&`LI$dPJbm}bVGNAmKPAs&TfzBbmxg@2bS&?$%UdP_&H{R;;4R9nKqLP?K{(qh2 z6d#yQ7vG4g4n-;bp6XLE*0_VA)H?IIpw4668UUn5LD;VyedaCzs$w6^Gz(qWotX?P zS}Z0@4Y7(Zxgu(R{b~gO%dKaf>aG_rc1OLvt!(DJILCGGC9YpriR*sbH>vk?u8W@x zp0LR76tTQ5zw&w3F+W*TlaoOd<~E%`Mx zHNq%%Xf1lY&PI$tm_ExwH)c3kiiP93xmW7xJ<)OZd6el3;LZSuISE6|Y1X_H^UwI% z&}j7LNtY|IZD4u*%Al}C{)LArYUof*UrH1Dr?67c(1#Y-6>6>4vn>%E*1wEtFy!V} zSFxO#7D}X(Y{?F-80mN%&jNa+lnVsHhlKp1O0f!TQ+KMuYL{C*7Z+K^ zT?jXzZ6=%WGx(C&snHG3e-AyaXo)z6qGmx_%1*(#yEMEpE|X|gGm$eKK+@C$cLQSe z5U|J1b}4*Vnl4)6qsy`!e#N9!$UD=9tS`?MpsA=M7!rKL8r?*^%z|Exi3b)K$f$qhD++xqi>F z-I;*2Lwf(me9I!!=ov|2DeU02#L>wUg2TgKo@Ce83GCLHJB@A!7BH19JICaq!H*TL z>scN{yKy3Qs5v*j>$`G`TcyY8li<7?tdFlyn{IRrvuq9DsH8lm@6ddk?!415n%Nra zrmMn;98rxyaq#;7dd%br`qn8`V_~;k>L4NLT^Ym|3Nb%FKFAoNU9m0yddY4IFa(x; z9@T%SF9sDRistSK2_ko2*7-NV?AM4C)YPNJ>{>^B@I9x^fye9Hj?f#PXgZ-!h+?H| zm+fcrl`OqK3|A3u$P5ftW9rmcGOsOv;WE)EZJ&*XT1to{VRqBo~IDr(M0YnxR{ zPO*G0?3UAcQ9=mkr>rQ$M6SbA&((Jv`8nS3DDR^$k&A6iOX^N8_Y251IoW(`{Gev0 zvsFmzJ92HL$JRd}?;nUkNhWX|%6sXD;SYM%Td?6?o*zUU#%-iFFB-< z-CyRu@2?d9M7xp2QK#%v&wqCVKV3J&3twlhl^zIL?J_j5+VrkZj$!{=><@bX8{eat z$q&PSeAYt}VwDcnsYlrEpWLQ{rf&5Njeo3=er`S|zpE}h`EvV*9I`XmiaF8@V~O+| zrZjz99phc1GX>%(O~mMz`X(K7fkOG3gTTO}}3n~Xwn=qC40^zO-97iZV& zsTi{PEDjw82jAfIInrYtDmxr*%ko<8%s$E0uCPIVIiRu&dPtvYkWI+6@EF_FY%q<# zQ}4h25b|_a#Mwe^<*w@F_c`m<^yzC6?-Cs3p3q<;X{wdAu41)F#Q_rcO`8NQE=I<< zo%ajt3MDv^Kl=JunZISrvS8TR+vnhB{uU?ox*@hUwe@Jt&&wNu&+?Lcu174)UM#*- zV)p=iImCe@8<=}3gZJRbItCF|PxqyGwCcx`jBXG^$C*Qs{+CJmS)nSQwsO(t$rBe;Ik9`R2J4X}+SdK}Y2i|ZuP{hbe-^7> zoPLD9-D6G1JtJeIh}jzp9(=Q?-PJy%DL-W}ydVX;`pf+?7&?S#Tdo$S0v1*^Yf*Yyc};m^Y%(=pt&@CY7tcxK<{H6e#h z4!96-n9!&#&vLSS=76|s%sQcCWbOO1b=-Gnhf}QIfbtl2=n7PCD?B!Q*+$unmz6@@ zV7*gtGWCs50#XL3yf5^h59yC`IFNK2ROoqcNxsDX&R91fr(uhe%|5Job$h)awdS6l#qzWM z0^xIGW6?&o=D0*ngp}J^)xPXUOrkrYKLduO!)0?U{eRNo4jV*4==$C=Q3SIR{jThP z1Fn|>1J-M_Em5t=egD36p$!wGe7K(Yy2!cYd%cGii!C9g-EyY}wu?rZ59o$d`^{=$ zrrt(4BB`7m!&@$czv5`)F8ToByDU{| znwy=ofBblndcQ#T%H@Wwt4l{wWpB?$x#A7`_OBI`ge-MS=6!$v?~@5P#c=Efl@_w9 zKDvho<1$}qx;w`T;~KucmPS|zLAEd-7u$ciu*9%8cCcQ*2>hrJq4v3|L76@Q|^ab?GxYAKI&do=P+r&5@HIg2N)NN&Fw!pP{r zu5!o7J~EoWd>2K+W6Z{lh?x!} ztRcd!)dio+jAD;E8jA&v5Q^f6+~n6vnxD<;G;Lh(_^Q1~l;RM&DO*0+pJ$3VrE_Zb z?CVDoju*Tn$`DaZS6*Mxs%sx&(rqdADU6G7T9=^z+VrAce~IpuFMrgtXTn8Bo+|?h z?)YjSgMRF%zIbr={;`ycouqrl-c4%Nj$i)Axj>?t;LSj%z)pqlW|E#SXA^F&IDZ$p zUO<_{kB%x zJd0^vO&E6ddKu73qeY_|{U&6jX6@|T_-ouYs^Jez6^$r_sMmboB%Z}^4NYe2hwm+{{+&pO5Bn`Y0fd~Lxs=QFk)_|R?JvXH zq0{R>>ZF;B?PT&u+-3f3QCwN6_*tlTCj+j6s-Ayq<+TbxV&KINh!n9Qd4M(;<1G*> z(*4zPNxylWb(h`QnU?R7nM{P>C%8pkX}n`YAYlHAn9J!ssDDV>wjPt~Nd9>LUB$!1 zyqVQL-Hou`Gx~+&KTJRmXei64>bxWi1h=nBLTns=tvCp@C zYH+fywepno$nr(!$`0e*f$vjKnClu&?gzq83KO7>iDtrO#;eg)0osg5vQn?YL5~E5 zP?rJ85d||UU&nAkF868Be81at#isyK@BPF^R(_AbLTLGWp z0TR06a;ddnpcZNfqvkPQ5-a`6my+U4|@Mn41zttJjF85vA*q%vaHG5cHZ| z3)#7IJcr|x?KFpK#l_b;dHM-2I8 z@SNz<4;%&>4LZyhA!s5L54=bZPa;YRZPwIpJxUv9yVr4>vS(P|;fKY{>|kKXz=1%p zm2j`_9*J-D|5A7U@IBH(sixr6p+$d)q+#?XxI{VaxC(b*I+KS~yf3k$aMaXrdw<(l(cWhjTZ3K+9Dj|DC~DzN4jPr5*e48Q&ljQa;JMHe=dXuY?oS z-XpMY7iLI23H+U2sCI-aEx^gi`2-0$Z-%EfPYl`8eBk|j{%sTT%+|#rtJ9VPjA#(N zl9Mv5b8CrC4sR-Exo&RJsOe||y~cg{pwS1f+wZ@Lgn8tH{W(&9l*IwBcye8OxxJg& zY+HCxsh?}&c}8NwRI<5RR1+T`FQdYRYCkIG{7G`9>Ew8<{j>FBzMst{Qt*ajALQPO zOr|$VZ!IpZIPGZ@IijeXXB%CwQz!^^zLi0|_1e9lb!O6|@;Q!p)%J&|x25TZhD`k? zqEl-Pqvs9Dv!0~A-PPBqCtF6uRnPH>C%{kPzf)CovnUW7qmj)4V-eq#$zniQ>B5F6%+Z$7& zqdC2=mP(X!TC!4-4t9MLf4)ySreS9vFiQpnTsjuF@l?i>7oj3JACy?psgla3z^!;< zbkGKXj?u*KvbP8j5r6aZD1mV6vhZ4Ql*$>|*}*OoP9qD_WkFF(w;*dPOjVFQ$65PGI=RPaNC_&Mf)rD zwYygP=RIbL@F>KE=K^<$Vl6Kv+?DZFmSG{7wNwUd^#tezg1eKH;}9g#pisOh$5#l| zi|8Kw98m=W7ojh?qU{9Xe8EL#&%-Be6cuAJXzz`N=ErK;u5O3!r+6CR!edGs0&i@i zlHFG3hpY0hTuqM?=XQx((qiM3l=qgXB6ngiJ(I)3Kk+7V(uH4?1Q(X;)V$2#a!*WT zS`dsK5q#ZGmq0(9k)c__5Bdu)kll$Lwfnj{N?0BS@OiCL+;V$@?CKe);`)qbqhhv>kwevnW1oj8*gRgd=K*Lh^PfsVmTSq8@N&u!0g{<>0lmGvjJ_Q+RRQuTx znMbIAMAC~GkGRS>Qs1-Ztw+n4tK09Q$xf^wxr9C+#Q3`>oZce(GpsK!%OpB*r`8Yc z-7LV-CtT1QmcEWgKpNobPRCt^V@K{@b*Ph-BL=i!HD90Rv9XMck0D z^2fZOME*gNGC)@pXSp4HpMN66NSFX%TTbtq=CbJZY{Wi6IW0^j!q?+<({+Y&rDnQT zelLXk=Jk(P5(_4~I=q0>b&A{Ibf4Bc899E`zn-M!|GM97fhh!T4f8Ry`d9hMiZt$q z*{igPXrrM8z%*kEUkAGC=7AKpI2>RIAw~lBb!m}3lhL+mcaT|aHWreyB5bsJIv3!E z`hNQAV>$ortI)dlrBr|rl+6Oi_n+c~Nc#}yA43dn?fAy!l!h4(dLzJDg!!L^qg6tt z$a+81;QOCIRWFon64}d^E;|3xxL;JaXY2G=l0Y27%Livt$pHzdR93ugi7*fd2#wSq*I0((LQBSu_(0ub{V1w6LG;S%n%S}SLoNCpd z;|Q}Ks+Aypl8@Tkccy07H)yc=Z9x?KDnYe$Hjg(Ze*lz0(#qJMJ$vm=B7GYY$@6Px z%1tCb9=A|(Z&j&~wY4U=gzC=15v{7lI;~*ZynbIK)2l<`hcn19lTA|GGi~Q(=nkDt zcg{|hNAS6R-9lvU-C>qV`v_C5{`_5=ozr!97k%xaA&&eh$J+MoCj1{e41-J@= z?NfL@&sm|Pk=@-!_1`gPq+A8MApu(OSK41U-1cIra`JW&LVYJ>f~=$`!dgGoX}2bB zi?Y=_3G^i$v$Ekvn}R_2jT_|wH3-7Kz8`r1Q!jgO^KA+gtbU*NAD97IUwv^9t#M9F zmK*j1VnqZXW?-ibgAscSTzOUZzMP2xrCP;YFDXV+4!N9zZif~(9&rNZ1c09MCy@Wi zH`?-^hTCrI0)JeY_e}|a#7QL$mo5X(6>gM{o!=Y2p#E+KdN|S9{xe3S4$C z%tbyFnU>~!T@~y!8=&&$5#i}S)0s}ngYw!{pCC95DD(O1-ykf!(|6KME|hqDE|qNa zrzRt~MGXc0`DEdFKiUHm0Kr6Pkk|NMtxFIZLE`g3VF-UeN&Bb@zxXX0U@he{h2>7# z<@DY^lx1kJz!C?4Rr@qJ`~e-@fW^=L9b^SZ6i{-RXPm^Vylt`44O&Sq%89LE(be7C zX|~bShlArXoikwv`o$WpFE5jh^S4;b&klTr1+O^QYX60Wbli1nx)F;T3h)X ztd+EpYa7#anVTKTbPR+&ER7b4AKo~76;cM_KKuOgugUo(fK-Q(YY`k_8d-2$w|A>c zqr+7LNC=MfDc_$^PZTAXHj@fwAM5I>d7*!z4P;dg8>pwG-k(D?h#%-pK>U;4ttY7u z*V|6iGCjC*r`do$;_`un%! zb6xyWTTQxX5fK>V<9E$&;Q;}3c!PC;NcKs!A}tVpORxt+(21<%K)mJg|H;xRA<4>@ z{f^+z5o|{TRkaSMnz42nx!>Qf+-TuG;U@zZ_CY3xi^3lQNO7Al?teTkZo*} z6^dqk`Gk4|_oNB5w;{- zvoVpwYSzHTzEKYpxP*NN25%afxjmbmY;vdPWTZH1+r2J}m(+QPzOR@|R;r^M%_)ztFX zFF%#d;Ma<*^X~R}f^d+uj8TJj<}fkw*=vnz#8bX6?+%oFff7PK@7U3K;0Z^n-Koci zsK_sWBKMIT*-oLu-StQKzvd4-?kf;*kbI?YZg2Y^AD$nx(~CSHVK4`)!Gc31uJSKK zZ*IINU~1M!xw3Mbo703~Us(D*7k$QDEk+$#jj z+Kg4=T)KHGc->%6*BdcsaHnCbY^LNy(-^1}Fd8J@dUW0eM~@wY=CpZhE#ImMyS$u>dYTPd9v!#K(Xy>)AUf^yx!T<=OH4Q1Bb2Gvg)iF# zi?>ynQj4ncX7JS8z5#5K(q%+w`yI>arVmd7^S*eq`ykL?7bxR2cJ1v=2(2}%QBz*5 z`Omu!*IQqY5eVF@dRl;*VsJ3*@bF3@q+lq@&)PVd>V47UO40xJc``pyoociz#CFhN zj_~JaeUoZNzqs5rkR_9NuR=e0`T|u0RTB8j8 z$*NV5)j_S)VzR(|_-T~FpEV(h70kMB`ZODL->+p-e@E(*na@v?ggWt%CB_hfjn;@x zmvVKt3%9$WmIw%S3kw2b(`a?~lvHMTSNv$-22+w{tSHe>@NpXIXDO+An{Hw^5Ee zw4h>HPs8Fl(!l!Ed0MD&MvIn3gNM22{2hAE$wb=FPw}ifL0s<1!}{OXW76xZYg)bQon|+L5l@rXYl2t0Kyat^rgE%a4vNi@hh+Ns0=R2 zEFZ2!ZJXe&hLwEWz7cJ)8tlFVJM_Q16D0A^z_>->)xLhNvl$OaWwYpLo+TXRb9H6w zZC7zp`zrAV97Vkr_&Xy_grM{|-$T1D&E9MoERl^uGNKE4>|90tb`$@jvoWR#&Q%F}p`8@m|K$F1E6Yy}B{aWA6&J!c>eNN+PGOFtlQY+-i@>^KKT1 zvoM1#xO*82^j9NqE;mlu=gG!E8My!XC_cdw*$`~Hn;P~A{RTcR!M~zrych)QyQ;k$ zVLo2Ga31yuVtEL7o?%HmGt1#fo=H*<{ZuU@Ts5RJmQb@ zKAAVIXg>m60zKFhDy^rrgsmgaGa@2bW$*4DQBI}Re|bo})#EzU^!gUy2%jd!L~`i) zp5iB*_Do+Ge(DrzL*&|MhJOWK|G!6&_8IW|SaVNh;QrgD4&-DcIsmQ4Xx6!SnsQfm$DV{K*Juqt*^{@fq%eP0L5ey7z{@T0$ zG9}1sL9rRy%KQ6gn1}H53>w{$Vlo%ULvct@IHV1vR>ge=6c0`T>JWDbBGl-(CiRup zs^3${ARH?ZLM9wjbWc6s<$Il0zgy6kgsFV7uBPV^yY0yLhkOh0^Qw29XSCY74k z)um*CYivVpYt~&K|9C>djyA}CZM#N-{(PanA_dvwi(#;_adBIK zL&@R0MoD8_=o<|bYvq4WC$7d;0tr-pw-9df{v40KmLK}k)ddI!YOTBZo9xIlSFejL z5+sUs8Q+1nd{RpWUna6(%O6av5nUg83K7Q6CjLkV6`E_|2k5Z*-#cVeAg)r~MUe_* zh9DEF#wyiTSejDEi;1Ormm!uwKV&5|DzG;5G9Mcj(j?$fi8<4fqN8ymNiNw`it+xF zUS31i(`TPi^?}U=<5-7PhjIylujEepn!try&hsD0fLNvAAvy>XUZr*P#&6BU-QfRA ze0IeBWRG}nFq&qx1$bTVSt#Y}^(Ug|67;$I2INn~I7 z(+p^vV646}yh_AFeE1%jI82+dx7k=&4i*NH{sa?9&M^F$bz7|d8wnX?Bzw3CAD-<-}ffy_eA>pqofTH>3bhzwp zF;~n|7iKR1*2swLzrFP%TyVBanr*wx9t+pCr&L4Tj4aY;1^?)r{=aFQfh?8FKC#Jz zW(6!&FxBEJkYMTtlRSQe2UL|-{f|ulL>us#9|n+LY$N^=^ep}J`8-VOH~3TsAfOhh zM2Lkr68-08{MZrGqa$+_jJ^-&_aT}j6)CgC&wJg{(rHc1l%l|apHmMhss5g0?f+9! z1&#u;FW^7KnJS=!S0zIU3$C#I;eEj;O#)br#93=@kKNB7F6dc7V-BMpSp>B|LJK=C z2rpqg>yxQ&OgQ!hr*fak}nHh<6%`sTC~4Q!I$-+>TLHaw$Td zF8h7X$1%;#gmq^4BGKo*T$A-tk~R7zAs1RNY&QRTqO;e6KBR7+_NB$7j}8wDNZT{? zl|$kJFgx_b;Y>p_|LrDG2?%BLmEmKKu!sl^g}MWC0oHIB-ecG&;`)GGW%C(fi+!Qs zAuTDH&uYs2X)@J$<`bw-do2CBQ_cZxb5mrAA|`i%PS)(|J@-lHN^6nvaY%@J7MFbp zq*i!$w{9?v94U1n__}Gx4Yy%^jdm3MLz_0@fQCbt&;98B>eAkuiq@Au1UIJZUUw-z z0olP9`KU)b7*q9ptktjQrd5u;b5hyq=s%p_Mz>R5W=_rk*t>abAGhTTFbe|C0qjK0 zmnv@mprDdk!%GXzDu+n-e97?WXw(E+B{9SfDitn9G7wVXU_)*GZa!1k)$_CLjw38j z^7+c@n8i}r<`7{btYj#|DJ(qVSO43N*wB|`2$}7oqBUmSLYosG*TRRNywRV(!9Mvy zy@21j5O#pJJyOJu4A5iyUg2OvgW(6}QKj3(oRt%xN|At7(yBS$9p~Me!H>5)ZMqz@ zChI>1RRZK?+w1y)&0hx5BctC)Xjv5YW=KuIVQle!jgNU#h+%5iIp3(qoWX6n3fbU8 zD?`_-yj@C3|5ZnbERXruye+!)3UlH?AwfAoj_Ds%0l?3=yHPr99xA)tZ$slO<1O2g zFXqtyx3#Cpp(Q!jwI;O67_x=k32s(bTt4^4YGLsDNk{RO|Do|gNbZ1L4b1krz0!Dk z&ESW2*7%k*PK%aT+6Cit(>F|@=%sm`T}d-9TYgILv`>8U@U>+N0X6hFE&+E17(ig>8A*qtkg~I`y zq|JOwkwm9no6KP5-l~S~*Ad5Zhd1Q=o%i)zJ^mlKKQuH@ABu=>4CyVJ(y`)JhK9mR zx(Tb_f%rQ>%3S+b8YfOB->iy#wcV%SM7~D_7aF95pXWq7GGjxU6@*2L@$@fjAd}aI zb-?tcmPz(YZ|`zut!|F;n(oPp{3-}wir4Y2?c<6Op+|d#QN)_+=}}(t0yka$Y|8^f zl0iS>3I~Tl39JY=qF|yDRDZ#dwhtRQlw-F8X+4+Qxumn-zGbW?q=iqBH^%bu9rJTq z%Um`xR0OQwzL6SyiVvFoeJFvV)^*<6!k4NXEze4F?SZCu=PomNNnLfb8jG!4RAjE2 zrldPbd#=sFv&sp36iLW=d|QgON@BK@ML5Bnd|r_$5|R9v&FaUu-W9b>*H%W2`pj9b zxBGr~=UWl>yB)BdqvtdGRHN<3IK^4gZ|?Y4M~cy%-LjGL3-dl$;&UFA$ontoNZxZv zV#S}VD!*H3%_8x3ig`vuezCvxE0V-JFZvPY!2QVD#wzYg;ZIO!WOO0ntwengXT8Gu zx03*>y_{ivmd?4(EmY31&4c_=R2FOHpWbve+|LUwK5m1#bAi{O*5mnM2+N(>+;&nY zoM1CJ@iR)AXO1KQG>4_yABS2kG0h3V~R*Y3JL7U@v>asP0eYIIbI`%kP%`Jn_U8iKy zMuFVJcyP%;XzY`Lwa39-x%*Le<|}WGq1zW3T+XUG>mB~NY|D{DV}~)+q}fuus8z6M z{dd2kFDd4;H;kC8r(%V2$f1?%q$v2N3hCjL(V|-zv>H7yZ+UzvMPCQW1D^Efkkb9L zo*k_exOI*O_Iw-Hx;~3ip6MoQ=7H56v*TZVjp4@p{Kl2BC$V-Y2&QtsE>}(|6cqA@Rh>#Z)RWqc7OccP z_NgL^^U6a~Yy#cO8qAF@r!Jrh*oMa`>MqI8w~BscaKtcOcb(n4Gh1rTclv(S_<83U z@%4BmNU=sx^%hs^%hD8I+H%DpKP%wKY8Rh#i}Q0E_$DaG;j+8n>X%rdUZ#JcQvRB; zsiE)yCz3WYi=5LHwHS@yP5CZgxtd=_dOGdeH1f3b5f-CXo75@tlPB4ak$0di_#tir zo`LO;fFBAPo{}t!C*`}Ly*!p^lhx!iV&uSoR_iB70{Ql%JjIdMLFjl0@e~jWmVk+Q zDf&1zsR;-q-bKA5>w#loJmRvnp0P3N zv_tM>D0ugx$pXga z%>*E+ovtgk7=L%0q$f-6zIQ^kv(Q`d`=<0{&$-aU{1gzG)p3qJ-xvw;JDI-gQC;5c zo^SGP^A<8cEA2imc*J}rRiVoMvasN?IBuUCeZ@ERC`qIJ*FA&6M-M;@CXeTIR3ZEr zQUl&upu6kk!sT-@9JkA~w3S!KE}Z=F99IjTZ!yy`ef0_)i(cEsWNn$7Z;1(@5=Fe7 zurQ93eft(Kfq5fI#cWAJ{oC;*3?9}$6T(A}S!mW5m4kw@8)oea`v=Ro(PB^o7BABt zWh<&uwg-v8Fd1`peJRMreNA7DYo;rU>6d?Njzi53%~f$LJ2P-^n;>*k);n*6Dlnzv z-t!KR<94!9ib1>fEu&-x&PUA_ZT78t0fO&Su-BkUx9 z5nUsHhQGmq?U{Y9Gv@bgZT$@TT21fX5Y3mc$~E83UX^CQ;`K<+=-slHRFVYmGtra1 zpM$f3nw@&)z5+WU75T8ChOU-jo z!g;Sicq<8ovfSV{g+K0gl{P< zgk0{$gl_6~#GxV}=NZA{|B(_3sOsG336TG9Nd{UPWVxCj6{q_SwA{?wMb7vI4hgUv z*X5QT!Mqp!l0HE)AY5Q_xGSoOC;izY(qu5TA|%W(y0Bs1D>1lJ#&*DA_m<6Lbrez~ zpjdJ%tU;j6$)Hr*=jWfaT3k6Nbhv+afZs#@LC*~poxQOgn2kMf4)P?}Inm~m%=-}<|NQ;MiQ3nF3}?nlocXXQ!4>pKeU z5w)+p?I-Qf#btNW3BB0nyPKf&Np=N(qKc3CG5V0+t=Gwll*HbL3S@RStcXuQW+3Zf z)SpzG5>zt*tB*+Qb23G%a))3G^Ajn8n47n;-~M7Jiw6CoVPS(l+U~aQ-L|^07ilv) z>S4fmOQdLe+1KS}x3_)kj}Cn%9aJLJ8bIg2xYL%aqqvZ*Fh8XlvD&7MW%m2gc!90l zb6t+(o5b0^{rB!s0o+e*d}J{F0F+BT7%G@dltslrxrzq(rtD)8k(5s6AydW-#5?f= zrZzOllZ1!k*c8v+MsG;Wx#-JiwBz8yTZiK_a#ECUSzicWIn+OOSZ*V~si`~Vsi;hP z0UD*7!KA6$=7>JZRMhzz~haxh+L$+RriHkzLv zF*Pa!H3dB`P>`Iy3-?o3A9Wy%z-^!$ngg9FX@*tGpBv_+n35z#Or_%5j@}H`S*Mvi zK2+PGC#ihq@=o%H&A^21R1V6my4mmfx>i*yH(Ud~mgug|<{3@Z5K&s&vm5NV*@0zi zrum*!WbY%PtEHW)8;|bFLqXoMI*w@GF&#?_xAWEOxdgt{E^uUuw?noJdt-30;(~su z;k>H5KG}^}tB>UKJ=1ok?3+zU!Rg3fYiAm-!3jNP<7T-_y4HH;&FOTVCo|%s6Cp;A?a{84Nm+Qe&8Ub zS-(Z8%Cs$(`BJ5<$Ti)*jz~o5BlKW7J6*#|d|f>0RiciU zQR>7f&KYy5I}_z%pf7<^X1YC5y(aUr^nyG}jRi91kfro`_mD1w=QpWjZ1_EVr??hlDO zSY1Z+((3yTc)vg*hH_cN=*))t-?XSOJ{pEY1V4Z-Q-qxS`xujRKF>-?6 zUV?8sE$no1K}&Ght52v2U0}fs+naRG${;E{`YzO1clRk$M(P^1YvqLaJrk*oWxdno z36M z0@I|9kM>hmTd|m&h3ZH1yy8vA%^O_q5ghiWEt%Zn3$M7369rCfgiC;Trc_GNJ+h< zb6mw_ys4pmBnz8}%>GMM0I%PD&vyUkNG<)~AJNGlS6BKP3Z^ol!%+P{QWeU-AINMAUv_PYi#o#L}glX)0g_Xzv-IGzsdDt6=dh6A9wFx=1u1YOmW`R|v(IBg13eG&b zhQsF-adT^Mvc}9? zY9R?(y@KbKYf3rh75!Wj63%I@E0alSPH*_ZO_tMIk#o{U6YsY+p+n%V7n1Gl3tuW@ zUNto}50A~~bZlpucO<{+jVh+K$%g7z*Jo?plX{_VmUx&ytUjKYpe5;Xb+O3UiKkO} z%5JHIW@{pHr*{^{j1qUv1{91^u)59s$EKk!I_`J;_eZ84YhBb_t8Rz=;>{fM(#-yB z^AaD>T!l8aEO{r>eQfX>YVTRSFHA?r-yxJPyvVNzy(lU4|KHhhR+wsd=A9v%8Vf9} z_fW8l0rvk>>iKIUbR2Wy3g)*=XC2RF@WR~;M%XQf4$)XXb_kNTNPt3dZW4xdj4r!DgU3~A>Q%FOZmdsrFDTt!y6Z6uo z#&0zO=l?PFl~GlFU)O*%N_TfR(nxm;(hbtxT`JPuEg;?9UDDm1SGv32!%zJ`?-=(3 zV{p-P&faJ3wdR~_F7SoLJUC%g_*Rz@+S#@=1FuH4vA3RaB6j)XKw0!|$ANgGK;ZdM z!s94ji1t(SM+$jB=pZi6lbd^flUaFTi;1xM32ca5ANQG?NgL2k1)wheV^54w=K*cU zz}j64Q6y*Gs+b%ur;{wQs;2X59go)DCiXu|7C|8O5PYnCdRRQE4CJyhGf`Iu{5)c! zQWE%JtbKFUh0LiG%hHHyKclG2@|os8v!-6Is&9(XH98C2ZAy3;_w?6T^+Ml2=L>xD zg!@W2R<_#G!Z%$AYm`U1;WYO9{HMj0LDO;*fSOE%gs);TXvjRlw28lX-hI$?9XdFD zr3gAm$o6KUGmB2ECxsw?zv_E)kZyheiDEI!XT~Z-W1cLb_R(SKi*f_{{a_01R{2c> ztATI9CNWn4fUu;UWTQx9b7?=FuH4KX7mUcnfvDW8ZtV~-LEl7ks zquI@kNepv17o(8QYZ5JII4k-Syqr@RQs!8`1CNc<=Try%(G`PZ?U#ffCSQzm9vDC3 z0$v)@V{g5u#~)9lWo_LUe9K{f#0)LQJn>xUUE#N1?gx?+^`Xz!RrWWl1Nw;2r!{0a zgJ+WNm0r0h?%x8unU;jzUdwPwqO258+*adl;AMp5Cd+8v^pOBEI zb89MNT?@3R;KKbUJNu7o2dJ89DWkPaQNUj<|AXhv#R*7nJgYn41+V9lC&CL6d_MYw zfA$~crus?%!Yj(?|N31ZiF>mE^Zq_O@z$HX*7$@4KsL~UIhx00FIw)hVrJe4>E+bN zn-uH>DDFV>s~oDy?QJmICD4(9Elj_*()vcFRB6C$@wuLZ!wMw8dIgc_wi}Vjm410= zQ5%6u%Ryby!V|wT5QvyLUNqgj9gse(JGi;wk#JEWJ#WFX@SddWuyQNkyt^;GNcMqL zIBQr<)?r+lU!R2&ajaNFTYDF`)=}oILgKj>9{jF z;rpQn+oQai#WGUI%S2Q!1H3I$Zxb=E+n=AAq*nDl)yv(n!m%C~UA&3SRdWNCH|Xm#H8JG}PpXr8Xo=CE>4 z@J07ps?WoTv=Z>AT!l$vJ5HOU2_%-v! ziE)Zllndg^mlBrz)3JoU*xyLN^u)SP0j&Q{C2(ZL=ukih|CW5)Hsu{2gKY!H`!1n2 z*w5ZgQROyf|AYBoiE!fQ6t>D8R0RbE#7K?U?f~7|9b^4t7hu4vhHZTHMYn>lVn&rG z7uM<2SgV|YK_@y2VM)uKEEKbbi@u=y4nxTfe>^KnC;8j5`3EBJAtL!X(lf4=$^$?6 zqheawwxFn`lZlMW2~&ZBmTWQ!Um?;K>G_pzd>?lJarnO6sdHi_fT=_TZ z(?<(iE~p!)m&xioPz;W-4sh(PhC7t(30}xAfa8g@?7G=p9S(zb)%PApm%G7ma^2ER za0ZCygi=q*<3jcHMHTKE2P9{0bUm-T1r%QAd%j>bOK!|XiMK@B>sff`PnLa+zMNH5 z)qE1HCV1Bk@$X=l=HF;7eIh1_e7HQ0$v=2+t*v&I_&6n#P@2PLI$FFuA>qE&vDzBQ zAfAh1y9yK<1uN$cnSuPg-*KzQN%1gTD!~};s(QBCwi-&kh_b8U0mjZ8F-AarN|_9h2Li-}f`;8mH6 zv>=K{dTDwKBu?YouNabd$hYmafbWPuQZ%{KZGNG%owkepjr@24oAAN^@w;zA0YT0h z;aDnMX5Kme$eaJKWb=R^=>)%5cE(4=|F01W?X?kVA&csNnP>!{Eg^W@%(JZ^4W$3@ zu%Aj%rHI13{Zrl-<(QBO&j8|Kvz6~h(t@&vw)`w8o1&r^56As$@x)Exy{aRC^KOa5 z*@9>2+&znMiJYWZ!)O{XFnaz1MiSlY`jr&0-yrQ7|{K+7+TyN~H`SRYi z>SYomlT~d|^yRkSY6(q40Y7Qg)A^D%MwKR-Z?lVe@p+}i9QuTus09n0GM%?5@yLM3 z%2Y7n;gpJw1YZP2eZ{tzZk=Hc>ZRy9;nkvIND%CRId<`Eu;~gN6JOT(`=J1!NmPu@ zWOOTP)e)#8zrhhcZ*~YHMBWEDna!ouyaOZU2?gB*@VTiT^fQ0(0rDfj1P8N7y)Wfz zPB?{si>MaSopI;Ra|6H>y$Q`1XEqz%;E+7BBT3Y*+TKXad{`bxMeTVtnL-_u;}v+h zG(y*)k;Mq@FLUcxcyTU64d@loX!+LPc83odzJBl2dN5iy{Zg4Bjh%3k?2A?M=}U!M z!BAspXf#}g zcM`Vm(U|YYci#CjSd%)u#dX&*7U~ih`gLxb9@LT0wFEjEH2X&u6YG~levUOu9JPKs zWI03neX({Z4Bwc#M<@%H>x`+fuQD5+9iQiqC>O{OxL%ck*Pe51tV+SG>)=-HuJasf zltk3PKyNYc3i^yN(u$B@pOPI9>NZb1H{Gcre$(2JsRC@<&PKl85s;d2jg1@dbQ&bR zS+J~94IiA2`@qYj*Cvg}aq=n4fp^V5wJnq`|E^mdva|c} zXs_2WA-6F&vH06t%q-0u z6QhFjF;Q~Ih@roGhxl`s<%+=tMabvnIVU6#w%frMkTc~$x=kUMpeCrdNE*{gM;;R( zY4mo59J~? z3hJQFcGMLXNR4n!5MdqSqcF9_4TQytv~Mi4lD#!uWsO;Gt*oNvNeM7ZsASiO{?o}Zs^R?FK<*9={7alQZIJ_56kK5?{+77 zS6uZ35HY4+gVTEEzsIqrfA)6Q%df4XFEldb7BYW*WJ&TucR84{E0tJ2K1yUdSr@GQsrdJAN!$vFM;_@Em0YkL6hZ`Z|12VS0sY`ygiT&R?&r^T z8KYs42<+4l{*wyp;~QI-H)#~KqPDVxP{Wh$w68r9KbRpIb((~2t6)pqHxBwy?H?H6 z)P_>u>(|K$a)2K#RA*u`=$2tdr=pwGVL7(8HseB-A#9)zLR?jV zmoEl+U-Q$KFN{c}c0vPLME@9hUT~^av%t0XQu{;_Fs-k&egEzah6K1b6r11W8LkykS8h8+gC@~F9a1WX~J;?@b;N5c^ zrGhDv>h{?0B-!{}74}6Mz@1P1aKR6|8wHSQAB-&HMMS0E!IIKfkx=VfMHvwWp~hN3Y!wWieAEhJu2^UG`?v zHo{Huf4+W-gK&~ui=66^gd-UpaA_cQ+ZYMY!v{UO==q@`5xoE-JaS@Tn`BnwUm^~V zRl&@Mq$x~+uA`Dt%EIMrpqRv+UCb45`fx`F^r|dz9vd0pD`T@c9n# zLH<*3a=rB*0>z($@O3DZ}oGfEF< z@Z7)BzIV+Mw6&GqS^HY9Et@r8+Pvqnse<_CSZt?AE#K3_NyWQYWa`Fk&lC6FegY35EJT4C{V zai#ylS~#^6@_ms+3fJUd(9iVcra-REs?i$VrC_-ctQras|lO z!xdnw-{e+e6%+y;5#KTh@0o*PVXFxsNFU{I_jyxzUHwyP+I^ucCbH22zN;#6`vS&T z%Iu>D2SBeF4$dCB?;bn>mtE52f(&2=6d!@VAagLsw0Yq~3p!C~J#J3D=fD1O1V+6s zTYuIU_`ws2EZ-LVE(`Bpz!fBfUGs*JX4o?ph-aLR%nyXbGiQJCb~H_m7Q^?!pLx`sC(6e9{=xwQU#mz-FLKT;90U~ zEkDD9_5r$}(i|e5R(#z1X4P6<s1a3bmT~OLkoPicYiI=h^Vw8 zLJTY4z{|V?KU<&CWn?U)e{8BAKNvM=)lo6CRa3+Ap~C#4^T|D#l~DTBbwe1%m+hfS z`0ZcH^uLRye((QkvMVMtI|%?3zcbwte&=WAV|mEF)t3dFE&BNE{?$~75`tX|DHbU-il zO77wO=lZkaG}YqN)U~hr```1GBHLSA-Gm)9kH@{6HstQ^?$lW~fUgpd*KNT$Gyq=g zKDJZI9BDGM{NJ%rAqGK1Q~0XJ&nX0kk|je6EUW?BI^eZY%IL* z7>SAfjvGOQGAh*7A`qROLg?tfN9fqbkgNlTdSm{+W*vg-5fP=-G&BJ|8TW}J^Gqf^@hXan8dp<=5}A+V9ztF6$A`hv_-4Q2y(4+lhRj<+B@ z3o$_LLTxZFo)BpMKb@GLU?G)aUKriWu}TDEj}GfF`+M>pyRfg>B)|omaT%ijI|ei$ z?%uuNV_``gE!6|FWD-7#ihdg@!$TVbQmg#oG&lDX|9^CrJ`rTo%1lH|OssjI?Zq(W z+alSVh`ub|NLTXw`?c(PC;!Sk8dt#X zbMtaZ=Z(NqR+mt{EtZjL;FlO+Wn^(IFsG8rbVn|K1AV2^0JNCr2r_g`Pb)5_6BAbj z97hfc>t1tU`4!E^YmtBd?SHC+Ww-`F%{e}PzKo2b=8zx%f{E;RG4}Suzinj$yUqDn zz1Kwok&0S5=6Dv=xN~cmiO^H@_Y1^GkoO-JV;v!=rm`t4M>G{4UA~_~0y@3u*N2wi z7Z}L9GIwCJ?L9^EYwF)?3Bv?B$UfV?(hlro;gQeN1Gil`~7|F6^LHTMvlK#m$q ztr4UbVYq|hj9_X2hvnkt2Ia7)>;{7U+6}~9{CAcSpil#4 zVJ9*UjucbtC2FZ&dj=+hCL~Z1WxT7-U4Ns%M*i16-A)#Q6ao?N5xLfEwd1YdcF5D= zP^S4(gg3xV0ybHfhBnY^_3zztb3a1o!^_La(v~^?XhF42HZ;sysEl^$z%A#uzC+&KzhK5lfM!i$3CKlhZpQgJb)qBSJ z=l{|ZQAj1_so#Lt#81EnCpG+cU%NyEHh+-CToKUb^JSBY*cXjnr!K_hcsYkLBPLB6 zG^&?lITxdGDVGb6@uff~mG}0<-+>-7e=>QBYN6XR`W)TG**VW)r-holNA2lpeP>51 z9FP4+0z)dsi?E&8{}(w$Amm4P4((KOjiiwMWcs~3=>I$rcqu_hojW@TUD9*iMu&h4 zkMta1h~-2ynS>dTy4JX!=<;~q*Pwps6%qNi3e3DMQZ2=0x0;VCQY)7SstI%;%@Pt4 zO6wnyYhC6RO$+M1{!AJK0|RpbqczaTh>&et7IfGqBrZ<-5gO+m;l__TNT^&6jw?0m? z^)|z5zx#Xs0ylmb z>a24_0uiI*02iANOO?6h+vV|dk9QX;yeQsp-n;>9i&Ajkn+cAjuq&ScV>p0Ldbw*- zXb|QzPPT1bWO%mU&&z3gsdgpw`>48i?>ps50rUo}@=B_tKtMob!x}Pq>I~@) zl^r0bJM8Ghs>i)X z)6V3@z2-jp(6(%@38796uOOH&pU!~+1N}WU;74@yM-!HxnVE^l525Adi74JGH!7Cv zk{DjCPWI+)x?5zxIqZRGlRXp`n3Z<~Zb=4?O8n>JKP&BboQZcassv^u+eDuVDd`6Z zSk%MnpF?#WFI1cE0Ix|Rg-pWl!N=N|xVX>DyqrIk)O)(5u`_3i)$oS&)CUpqIAkg1 zQpk<^BjRmVnhJ9TfvF6&%K|x>UguKmvgy383EYmm^=X|}Pf1iE1 z4wD}$CzkI1A0QZ9cNcZX+%o>$@eGRJDZ<<+Fjh1=00C<-Khwve#0O=nz7o|C4k!$V z67_s&)_fjH?NPj^}1fsY&_pJpR}?4qQ2*twUs}hc73`Z!PsQ- zfK*F7;IzpHTjsH0@)Uv~7la^RCxs9a@;?t;Tch#4dysM1Hgm{Ts&;e;=XwN59LL`_ z%u9nBruZNyr<8YcB3BcPhcJs&Yzu3`t*OwcWD|b{!P3m$grE)d8{dTlP}ayM_d)Wz z97?|1w7aNDuZ;nNi~92sJRlVWZ(up0f?DtUIaDR}-viJV?%1z;XOk{)yZ*-iYJVa} zLPyEP8{U8X?b%(q?^AKh!*MgKU&~a6)Dn!=2Y(1 zVEA`8U~o045KvG7tS33Pp8;E{$y!Ub#;d%8nO~GY<1a_On}opUy^o=^g#Wu7#W((6 zzCalH^V+WR-QBdmSZ`jCleUmkg%0@KS*JfAa3$&4v7u?PX%bcN?J2>z^EeD5;UU$gfeNqU?VZEwRc%W5hGiR?32U4c{Xnq@!h^XrvPKNe(u^W*5 z0TzCt(kQUydeIU$^MEW}f51MoZ)PGUDG587#pwI8!w>^2;8zcafKW`vsSqtVM5?I! z-8Zn8p?JdT-=2Ao=C7Quw6h0VP4GN!65?@{H_KCX6R)iRrjU3iK0ci&Sjw*LD{J@m z_Es5zY~Jm}d;*6QwoZm`m+b7!n#^@nfICHqQ%WZt8X1TplY((Von$?()O<9&rza*B zY;r!JZ}qq?pdNAoP8-O^tq3i4QCX&??q$mVMOJXs3X;=bnJ*Nc*92u;lXcc5EtikV z7!S*K@4(?Z$~Jx}{RvCjf?%<hNKBz7+{26QBCLbZa&~o?BXm&mlz5@1Hm3 z4SIPqAf8_)nA4WCF_m~)R-YctmDlsVEN`)z3>TynZLs`1310t23b6Vy1-^^CH2!qP ziHVQr9k#~_86TQr!$aZ5p=v_?tGNDr8w9KW<#4)CxeCZj-S7AEcTP7hiaXNiCLAzS zZyp{V5WZ;~a8d#UOp$K`77Z;eE0HYW3_HuVB=0a}9Q-^D4Gm8aH-qEuaP;l1Tc4LB zB;iE^x4)(rNt1i=0^i>&GGl~5_f3sxK|C61SY4>(TZO}ya}FMG!|Hf|`L|nMUn0RH zNZ>peYR|a)n#Qe6|8auF=}DWn2s%9rY?;`rCF#En-hly0+(RhfqYcag`fvvlcriD8 z;n8BG)c*423pp#R5-BOE6NW|&FcebC%Bo}m5e|-}Y)O%y(qmosemI#eCMCrJ(#*=# z#3V(*#-?oQ%W1|3&+P@%%=nTL`o_Jy)b}nH67ta8p30>r`11d}0Nm44k=H-xr7pvA zeEs{H@kM}%&c(%jqMB5a&H zFkM7|`4br@#wYBxHO0=Ji-?etvmb>qntaeKrQJqCM2z9E&Zb*bWk5svzkuQwh}?m1 zW3_~R#9%zw*R}8JRNSBMKv3DmY&6Ww<3TMzH9ME2 z$|sDTTIMhYiDOy3PDe4bAE9Ve(H7k`y-mK+fV{Sl_N-=+vPCnXKpU{ z3ZweVs>eYoF2W(=-gIH?9xi2tJ^|Q}k(bdVymbc*ng}*nXDL2?!kNDePq7w{>HH}! zsvVsFx$qOgh@;d;`frf?GQ?DzW3m;z9y*GOKRkRYT0~8ZEgvn?D}Z|}aZA9&O8o8E z-ofYC-6f3AP&ezfu+X@RLx_$2W6Zi;^WyBACEWUnnUGMl+k;#2Z&dA%Zpy1jVMY!P z^kaSF
c7~xyca8Z8Bz|lJP>%`}VYN6HCw)UX4rGsKt-+Kx~@6-1Xx~m(D#zYDSF0ogMlOC~ICSrd= zqY9kh>9-{i=bG1)`ZFdv`lk^?S0c<-I5;>~{SL5XW`i$bc%1S;)G;aXJJR)H{A$eZ zQ-Kl;Ozrr=(%gq_fBh?(j1uvz%6RDWR$q8tX$4}L0+0Lo&&P+g7mn=icr{&f$S;?d zPGq!Oc$i(ttp+1t2H0J;f`WeEzJHf?b8FCqtzqfnpiiodL>d*s7H>|L{ILf+4$-iu zUY%D2`3$;ID5{6<&upL5W78>1pFEVeegWIj~B0*u$=FwL3#PUxm?2K5~C? zFA~Ns(<~=@KnAmIK8U0|aCXceDxss~cF+*Zm?|{=yp?yvYQ29OVzg`*))7eLOQ*;5 zs4Fb|W&3E3u(;uuhd^1fWAbnYd*h~?krh5y61lI}rMvQFj&FZu!q zdPB8$KjT7;xePldw!qCHi=>JCK_P;$SJ-bnG&~wOf~_$9P!>lgr=WgM2jM zUvUSwt%|KsJ8G~U`7z2Aa+y13{e{Z?JL_3SRlY9~7Pb11ggHi@VRy;@%Nl_n1pOT4 z!tXp^bE>^6tN#L&za}6)3uxX!5_lYMkS;5~MF>?_e-J)x-3+1o{CImNPF-5t<&n_= zDL0n!VFXlETowax0s~ljULbN48-=p3RQP>qQ zbfRJ#GHYvedo)Ny5>DnT5bdor*E=0AYb@4TTP|yM@38l-FH)a348iJm3LUkVF&2M^ ziWUrq)IM!CeQ2p&DXVOwi z`9zm!-0xUh@eXhZNUDirZJ=-Ju;mqul|l9KGA>?Yw1f|L?!`d|MvHp~TjeK|{+gBz zA;Z{#Sdwc#Z^%hmhl9G1y%7+mry@goeA@Q+`UPZX%e0u<_-FFBD!^*JAGoAsOCD^K zwirx+QFs5MiJd7#nyJNMv$A*3!aRGG*Pq`+Qu#I1+Z8^}8$|-hNJuvyu}zoQ#ZICg zWBr;>Gm1Y!+dLFbcURZv-Qi^5AxOA`(p|TU%iAV{i?*(+pk0L6!mdsM*Aqyh8l&|N ze^&cV(JctPE@neQHj@fE$?C8{$lnS1tVqY~F#bV7%s4Dw`d^C)*$t!CyG8#L0)&w8 zpU9s!hiqpoM|~}<6!rBh!tfm9wfQ)Nbf^=9^!zfN4$+y~e4fS4-XS2w*y!*kS(04b z7uFLKVQ3p9hX(NT^Xr;4_jMiEBO60O-4%m*>1s!Wz<)y6RwDZV!m3G(&_Q$dw=A2b zqkYDdIzLlZ(pE32a!yW@ zLN?qRL=DEtVUVEkr(OsKE79B#z5{krf=;uG3Xj@cL}q5@<-wf7am$5#<5?e`9R70C zIp-|5?EPCDR#CNGDgf(A-vOR%g7?EUyQ;Ixkl5P|pdKDtthEg1wT7LZ%9CR&%O-)G zBAsUxHnH;)9NhbqeA7u~ah1Wi)rXh}hZX6xPHRE}enSzJm{{IEwy?Q#L~&+cIfEP0 zuF~v66sJsMzIg&FX1wK1^+Kjb4xR6x(^{+zFLX)264YGO;I^Zda&S2R=~*8!St-(@ z`90mYQ4H3vrsnOy5EFCakLB&{Dct5sE&Uzs%x_%%W|rdO3E!d3$X1%1qADt)G(&&= z8lG)ZBEQ=<#~_xbpk)<)n3G~SAT82~&B(|KO$#AcVp#a{_3Jd5nE#=G+jI@E73(Hz z9kZW5l(jOV2k{{OW(NfO%+Ah-*m`7+mOH5G&S;?7j1GoW3uI4Xg+wz6!1R~H<99CoxWqctwsN|m!B)hHwyT@ z*9!Z#hj_gC3ULYVdo`?VST;Nd64R(G|L#M>F;){7hgM}e2JT(At452%Yj1CTWjnlJ z(eM5b8vvrbCY8VAoBG)W?)u*tQ2O-^@2ntSb0#-L@CG@bm!})!jjjMhR~}|&#qUT& zjO{N^C*ciyrvc${C}+MX)!1c8Ioa96kjOn>c4Z5EPgYycr%!|QuhS%*e8%F=ZqTlb(Izjc?aDTi4 z>t?Te3RX}NO?<}F!!=QK{o0)@Vv(N)yMg^woJ{|(t2tRU6sXMriVtVQ2bw-r%XUX^ z2S!E`nc8%93mIXFxj2m$d*k=JX1KW<&o}8VKKGYBiQjUm{k+31R9$X=?8SfL#WnF% z2Y7A9mzzF`nD}(9JvTeMAIoQYjyu^1fuDGehe!K>+1WF5wGkz0(urGyS#n0=NTp8r zyvV^o-`)p}hphx@P&R3|@hnCxEx{Hd5Uw2|Pw~4*lQd&3%6lm&`tGqXJ923sNoULE z8hhC>Z-xPhRBgfpH5Gj}v#1$)3##BSBOM+4!#pDc!(5$(mHvnC_9{|R(cEfRFxYly zCMvYc|LT2Ivrh9ao5}${YSR%A1&D@zg5kr@Vd>=nz(yJ5e`99UAaoJ~2>^s)!!*(k0oqa1&uN1av zyEAFLoRkS)vQYoX7A@!vhtK6!ApBUX`>=Iiitbm+e7jNv+p&q!Wwpy~U*YYg1!ROV z0K0%krxjG@oIBR8scO$dNja#FVC0WT@15zkE9ZV0=~}#FCgDFn&(A*_G~C-u(NxeS zQRo?vrOM9zQ=Dq7`FrLej^PK${kKgtoQv zp2!%ZYY(u2J+BHpU*bVb;J`U|^57(NAS;tZl|O3619oV!Xy7%5Fo((k{Z-L4Zp9qim}CpB5L z$_LmjL1wMcK7Y5+<8=yRQyn***}pG+@iIw`Ujo($5!H{`2h~+H$fn>4HTh1vNt_yJv+{U-&|mz z?vk&wKKS_P#zY}oXAB_w4du4WX7YoUn-v?;3q#HLw$Q(Le1c7gi#6S8y%FHh@ed3j z`3h5%>j>le8+Yk{$cG>VBZXoa_e2jv4$Zj73)d#}L+ z`j;IbiP^UGeMk+%X31tr*|AH$ySwXiz5?=0Vy2eUFHJT}TNOb^b`*@mK~l9qN{Lgv z4mbl1l-94(euaK#!7;bMc!(QnVF&@h;i=04g$ln4t9xk#zC?%GB^Hz{$~9(6ceRZ=~V!9s6r zWI4RF)=Rdxt;o4FP(KeDG0CLBFJ$^we#(<4q>HvNez@w27cU8rd<4*@j|RbEq3afR zNRfQSs^%0`PQ5&ZQeccqp|yy?_U8wlHXO$#XMF0l9yu+y`+UkLF}aqsa2-r7k3Ipn z2*sOpjG1N`WXKVEz6VBRd4xdWU^#1@Ak!Ba7rST!+aYO}_@uQ1gEb{-I%+Z~5dQ>d zWR3md8;lrnHSI?~+nq`b?!CWrdnS#=`*LUN>s{QGU}Ji{!T*p9dSR$|{vLm!UJD^V z{Y>iJZt<1=*`{`(&2C%2@uBSCQ_Xcba(lbRZ7X*_5wQKP$ox!4$F%B(aw7R{7%wjn zq6|d(1`dm#WkQ@$g6sPZt;@?6$a1=_*0c&Wwqug!RWnb(m2~r_0(i83=h*vHGDATy zU$2ndSp@lwE6Qja%6LNjluJdXp;MdEe`*(@a?V`K$*O$G zk7(Q+pg}GK618*(wKpI>R8R(7(M`gIUo4H+0Il3)A=zoEGn#X>&xQi>TR2*!K0}{S z78=Z(?c3jS4od=?{iD=lFN^vcB;0wtRPj&Ze49mZ5JM}jI<`F;Y2Eyr{(8&}eJ++^XnERsN_JFwl)Us7n>b~XrKyp}nurETncIUr0k5g_BUKZ(&*qVm|w zUP1W0@}~WLSs@DA?G#i*IGJ6k_5JOXSl>PNY;WBr)46C79cb<8n!_mhU9pL}qSjOu zhCs>)Y-^%EVC4vRTy8tjM%>2+U|NDtaQY-t0Kf#WP}5#9F%iWuUzLI*!e-nytf$4i zN_uL7XF#^RPdzfSvb<<(_%{1rry0C{cqj=z>NUvrZDM+kf$r(gWu!#RoI2*_c zW?Y1nvxpF}rVA`DiZ#Z^(Up`1i$xKHbyY{Y1(er{!%hz(X`LH@8CtGpwJg9@ybT*I zhFA(03zezdPdj0DHba@pJes7g4TM77dVvEnNKA4Aha@0mMMJl_5%Z_6oUbzTovBVB+qjyjTLURi3R#loS;YyZ$@13>q zI^&ZGolaINi<1<~>1J?5sv;0Jb{A3@Q(0%5G|pDati!^vITUA~K5-*ji9xUYE;~*q zxnHE@TQe~CxOZv_A#ktDPg?{Y^Q3$Od?}4CLxfPp_gtU2kbKGVw$lO>l<|ZjxY>Gt zaM0GXa8PNA{zzCakxJ|g9#~2KSv~YlclP{!eKBPOvXjBlTRO|QP^VMHn7iYZ;$nUy z`h@n~jz%JV9>LN)POHU@D2%$J+F5H$f)Ac9WfhOL8Qrex>OsKZI+^v&)oWK0sfVwS zGRD3y`&sCDdK|hLzGphIz#IiXNPH?kP=K>gK%;af31wY*-wneoD5{)ChQr>`dF@>G zjK4l;LhFX0Jb%wo)i$lR^;TT(G{gUMFoG4%1sSxidvYJgY$UsX^aJeDtm@exa__%< zH;u~grZ;T(P#Ws7#W{62i9dIe<+$NH7?l2;-DjFV8^Ia5Li6SMa!SE@o5z=yhey+< zaR)OQ=r+0gDpwOAm7JD_zffs9YyjJa+4T03Po&0n)?dmGY`xMJ% zo{D7eZ7D4T8tjJ1{7%_ZEM}3f+I!plk$powb9JSNa5gEJ@)Yzf*xua~-Sq;%A*-I45lcpNIZ;97z5^gm9DS=~~Rr z-VD(u;7RipDB=3`E)n?$*db~dfXcpp^9Kh+67u(@jhi*!HQDRwgfS03!O|%4NJ0kJ z6=${B(AWz3@6Dz?cLApe#TpXy38mKM_u;{(KYkE9A7SR$aBuy5g|J@nd6g_~T zw}dnZYPZu1gw#z!|7wOfft4^7h)59g4v*tknNG9pnzW=@!qe4Gb1jo%uDA7&^1(Q;IZ2 zv?7;$GG{zC$`(^fgv*`V_c_kxPti;l_qm>|h5tJGl5O)N@wruJioQ`Q~o z^1j{vlm%XzYdAqLe*4Xz5h8jlm@UX&50{OCWhw5>nva-x{+zl2BT4hr-js3hgiI3? zS+rlej+(A;K6Z=r`^?zHz8RDC^=gYWdiW+}ztm@&4OZ zK>_tEae&eTz?vRp(6%d0Gq6*Zvn4ED%stM`WeQ*19tY{lu@4G)Pg<0DT#sc@8u-CJ zaF;hxl_Y(=`iyw!SxkrpWCHpUC0<+Q)j8|&8Jjqvmwtz*L>}=I2+tmPr z1I!s`4XoV6wg0+N)@0Q0wl~TBao6`b-OGdVxHKHoQY)-`DJb7kPT#!Q_Jhp>0gAou z`*yQw{p8%U#3Kq%VXv735>d|s>^H(}o2X*|+H8?v@ep)pd0@&xm2YwaQ)YLdQ|drS z?5`>f7K&d<{04#k>G9DyO{?V0OQg5`r{tgt*t=6CWFicS-07t-fRH;3Pa8H#1Tbd;=q6bEa*^26EckOVXhlntSDN z&0_y}((yq!Z7zq3w?sUO@F5W7^p}qh`gA8Q)lQKRcGdc(x&NKe+>q;ax3AzGT$opL zIwB%s!roN=(CyjQ(XO)?v^pvAC=;`<@GcuL2;83+Bo>N^Q;BOILZUy{|Ka_zY&*cgTBxAV*RQyw(qJ8# zlKS(&#i_Y+YMJ_Eko#kb66;dUM#ICy?}$2tkk^zSFW%_C!>o~85o;h_t$zu!HxFw! zVs8|Fej<7)EEAIfs(hxX)qRd7+UJNyvx4P|;)7xx$4yabF-}Djk(kor?d{k?cK8k= zugmb@TD)?`YTp-udKuYN$yZAHBd;2dvz4CF)dLbfyL}kS+WbB{Ft&mnzo)ARyLA*6 zL@Pa~3Pb<%;{1HZQ6jF8jE6^wxPeSOKHgwv)0!cpIAe<+z;k9ls+YKy+H4w}S%@lN z@Y|PYXR4^D8T4h%3nUVKDL0<7cv{jl({xu~_;JFOCtT0(Q#*&r*!0!97Ng9~L@V!3 zkn0B*E}zbW!-9%d8=lN;4grpCt<~gIIGoC<;PZTEiWWGu;9*dS;Ey@mHmB=Z8ivn3 z8PqwNpf=~6bbCBgoOlP|3nKh4kB%A@`u=?|OXk`lOY-nDDv0#{dOml?TiN$b2W78H zsOk80kR(`j7!O-;aNjHk&QPih1Zns8>sTa#J7AD(H}3V^&9XM!P(=EJ+~cw|3|_;V zArA7TTZ!us(Z^*XRoiK|EB7joBNH4>6ZKpfL;`-wTVlBew>TT3axMWw^%r=OP7mbc zEY~A`{gwDhX!wr0&Hd~XRjD=p_$?GHti)d_&Z#>!eNO>Np^$W+VLS9&T-OYQN=%o& zsrULmFUctVoc)R%s^{?>?GZ|-Q;P`O5#-;ZC7`lJo~bmq82Nl8=E#(tL%O}=`@vA% z`DjsyYz0c`S2hrw7stv64^_nzH@8p?&dQ-AG3Cq z86U2xpz_qQjS=v@%gOF`-uPVMj(AWE5H}A=EgWKXRdP(ua7UK+Z%OKFKI!Or#_zHC z>U`Uzn6U0LKL1IVt5DvFi>9t4@entlW$Rs#={mwzp>O`AFr2ehhivXWbut8O8tS-BLGX6nI$A&F4ctoi{$}bWUOBR81!E5HzA192*JP} zHyrN`WU`_%jhkxuGS4B5LzK}}sq8tKkQblbX+d9e78&n_OyCyKg(82@2Y`<@)D~BrkVA?!!{z%kvx-z ze?&0;X<3#*nfmE-?`9=P&r9OC`GIl|NT#_1Ep>9?jIU%8Z{rWj(&fM#uAW0GO#jA?aSu4+9&F_tv>#y0 zzk@WuUw2fE55&U40t5+)O)!eY4WLGg0mylTZ+bnqRf!dw1he^ocEa95X-@Z!D-z?% zjLh)2&*g-~`3UD4cJyMjrRCt8VyK?q1kCz=!+<8jWF*CyJ}I^P9o{0VVGrzAhJv6P(x1D@#*$_9eD9cWCHPBb4T)D*J*Md0j{{u8 zhPK=JuB&P+m&DI&-!9$u*1v38kWs=wK}%{i2&hgt17ngLmePIWGCuGO?Y*a;mFBz> z+#VgN{bu3!Z~(Mp9amUde@jQ3(_z!8v(O2;y6fy;`!%;#hTd~j>zv4Ecnj9(_6V!2 z)Q@=PFoO}hJq0BbMsW&&mz|ezq?Q(o3n7d5H%BB>c@gFF5+?pIOgjv&d9AI?$cl z14@n`Jm6uW2=MUJMnYvMew9&OYigxhDsLI*9DRVnVWHMeYVBgBx*LQ|*GcWarf`)4 z5-q22oX<-LsxRAR+lOW*)%*(%Oo=j;!h;V@Hz~T`Aoo3rf!sDzgmvZz|4?cF9V-m6 zU{LFM4Yufhsb5$RUfDk&7Xci_tNRw6fcB{K71yQJR)^qk|^br1U8 zem_$lhWrG`0A*&&bS$-Xc$Gj!DuAZ?W-amd_SOWgiol%eqTvlV;^fK(qnkFKHuIis zvtYl=QiAd4&kc`vz9;P2JG0iF6dJvX!-FC}(pZiR7piZf=ylrUzsy($uoYUgCMskS z17t+ub{-Cdl@0{+J^?@;0#mT`hC<6*m@1;=xT&@`@#(+Vw)o_QHU2v1WS3<4MfiAR zeLiSOl;p84?%vNBIz!t7kd`UgjN%eQM=$@6sjH5Ps{7glf|61K3P^(jLrQl@r$`Ll z-74K6At53qN=Zm}4Lx)zpmaA#cZc6WU;TcrS^hIDn7Q}defD|w6IY=uWQ^~ftt$n* z#vu2WUx|~ZxL>;NpX;Q4fHmRD6&f?Vj$9k;=uOgk&KQS-ISodRJvI(LPz!b5s;!u& zw*>Tx^(sZ*1xsv;KIXlsOp)GHn0(D&=dxzF?uS~b4v{906|h2H|L}##y+l+n^^4*| z3*%e9*TWnpbV;u^Gx;8nljpI=IlSvkITb0#K?-?;BA5&zsxZ8aj4dWd4G5H=<)od0 zv)#qQ;znkDtpK~!zWxR?zA~PCkvM!_&ELIXEEFPI!^~wx(V)J2tQviBdC>A>_632l zM4g4pUhBT--blm`t(Zib_t{Pu^{dwxfs$1?K)<=mOT=yBw3ugm{yRf1AfS z1{B`vp?j_q^3(e;sZWZ}NPjOJBz#Lk6fi+w`uROW>QM4h3k~5|xM>>}5fR&Txy<@v zr{RQO=V|09Ro~8Mi4fD}p16+bC(sxp^iA6UGh0B35zO$Aqrt_OIJSy>0hWy7-!4=Mw=JW2ea&NSMp zs@AV-2HwqiIRDxlS6EcIH>wpRbos64S-7S`fyAL?fT90sg~mx2Z?HFQ{?F6Hmk!6?1n-T^ENB!0Lj6g4A>^rv1`BeEMWjV7fTb zt}$dGWH9QQr+rK=6c2Tr#_FAJdkqisna~dzZOo948J>QX_ojMW?4Z(NRtfbM?BknK za+dE+(Map@Bt*?JB@;}lxBVIKNvP#~v|fO5DZf2tVqh4@Kj5LC2wmf1FVPQiNmrKb zRF+Gbp@FT@Gwyuc7I^)o{u0@Q`40QY$AS}@r<1mxB897p53RMlHpyJus5^wPu?u}1 z3P$Hh1@GoKyM38)Xyfl_fuL!P%$B0r-6}ZzAcJ_0Zfb7cJ*eD7>3O{Q1nu%FFw0KC zp>p>~bGk~GZG?<{ojQ(t7`39K7F!Jm^^^x=p7^7qo7+3#m|NBn7va0~o~{o^gOblT z?Q;);&Uqd&GAh)%L_O)B=_z;8U)hl{H}^k&)~Vftw!_?B%a)y!2}^XBwO4v;J6Y8FY`K@BvQ@=x_x5)G&>M<_#-4%)+3K)b#JF) z&#J2Bs<78O+YKhYH$0EDZgfP@;SU+tqZVU~S#q z-5rESArwB3BOs-x%$J0nj`2J*>Z^_pVRpeXXL`E zPy}x4R#rs?rx=+LJUW<*`%&o@-a-9=NP#|+fp>$-C6~tZaCvr5nty{34zlMI6s4uL zFWmFUPE-w>-I?l#)J7+Fb1kvGe?|rk<3g);kin8fl71VqRKTglC+ZEy@d&TxX8#ov z&&VzA*H0}7+T^`9>0lww+ewDPT+5Y&Dut_C=;>Te75X+;!H(guI`0cu)X-I=3*e-5 z>Ib;JRvtFp@%g|X`#{%Z^pSt(97&2N*VWtMY~2+){}FLWIEO~@*|S+Z?vE^$c9Z(4 z*-vO0Ii%!z#3VSdJyL{;SZTXhPkOXX|h9TI*Cn!UP~{uWW!#fvYcQ_hyn z{bfbpg)N;^6tz-+*%msNNeY|O4kHsT(-gkA)N`&*wNDA!J%uRUbU6RL=B`n+*TaNQ z9@$KagEpNL{-;=|&>)3|0xG7IiZ_^Jns&dZ!!A3mEJV?$yrJxaQ~e~li#2Kf5`gth z7I0dJ55Oeb>a}4u#$Y&`EJ3EdaGmo8JxD-_-6(Eqltj-U)_40X2<8FnKCA{B#rj(+ z3UfCNZA%d5HzEH@0B^(4H)qN<_1)IEHi`uIzLM?_iIB&lR8pE!`;P$eQ;Cct_GRCN zL67gOQ{gMM&&0mBjQB1_d{;AvU1>8VC&OT61g7ppb4jycLRRtJN{>oE^K`SYq{obYf; zADwdpdzuODg-dN1%p*7`XO&${v}&m|iMbkvzOkSQe-r${{%C#RJ-npw-^EOk676&) zAqvdhLsx;=dvz4}-+*M=oJg?2?b)W;wvZs_pWQL>aMz;sdj%oq$6NXK#Qi$AhN^89 zQcTUv5?y>QwyKdgmH^RYxcYZaXjieBnfth`}Z&zuy;uJ+)Vs$VgcNHWslU#RK z1yb}hNPsyJQXut6S5Pj#`0|;_GI`7|_frOc`B~LumHuwN9Z93@q+I&j4)WVflS()1 zq=*vIymz?9=^UP77ACJOlEIQ3I29DL2p zol0?T-_HXKVG^IQJBvS5nV$6W%y?7MU!330nY4A|*d5`xtPrFauii=9Te!6+$iX2l z^t3H0qnq_Fzy-z*#K_DJ&{dYD^~t*!h$Fg=%bUA1Cx(J6K;-oD*IBUtRC#F0*{8^9gvSy7x6HPuYF?ZzC&<^iFi*BPogc1s zIJ~ zH2Qa<`E-ude3Cb3Fc!El%hknE^u>0~b7H-a>WS-GiO!n|qn_dK2_pg?sUoUh`9VG%X1{K+Tfs|2kagl5f5G*4Qt95 z>o+7Z_Z1ce=G>H{j~8eg`Y`#?I76%5VNHW>R-Zl08vr>mEssg0-~G1#o{2PR{(Cc~ zMxN*IH1*kKJx+Vl+y>g{!2RlWO@x`B$YD)Ew``RcHS z(A5o-`k|$zCCk(JdxWxA5z9L%LUqI1F4A%NI{ihd27XH`@l3_r9Y1(zX}ezAqhme& zCI9WwPW`Pg>xXx;b8=kpb=54N9-4G=m<`gqT{4U;Mst{U`B{(d{@ql;8sQ}N2UHz- zCE8T(VTZ}a=QC~_58dZ@PB=AR3a0C0Ajq9Qjm!QXz9m80MBvIk^FhQBOmknqDs-~C zP(RYBZ5@8$P4HBqjV9Q6{|&*Tbub_BDQJv(`v?UWLfni)x|Z}2{BXjRsVU90Zk;ijk$K`?W>QQt0U zUoMYJjteQ&q}ZrBB8Qz6x}W{)bVZTB$x@0mr2)Q76^LrT8d3D&!3WZcNR;^Y8zq#e zJT1f@f21)=71$}@-cp{r$p|jsYszgqMhPaExG+tc6gFK098&JDFws^*7n5vqA&)K) zj}(FM1-={n`4XR+F8AeY0BjmbYPn&mnoPWxDUFMUJq#NoF)6CQ^okIU`pGLG`*&|1 zphPB#9|3>KOFOf^-s_0vy?ufI{5~(kT25vr8CSZal~pMV?5mxM_qGqauEFZESxHdah`?zZI-IwazCU0vNeYG6dX%BnATx`-w)!t{3f;bNQazl}>#g7WcW z6@FK1R_V9&nrdU^(kEed_MCTbE*K!l{=Q)0G?357peXLQ5h&m9Rfgq`@H9Kq7g(^u z=a9=^`~s??QXLeH(>r;d@N?PRw8-f$1@HTKCi6nHCyiIHm zE{oLkf*mJI-h2^B#KFMrQtB!I9-7mKcPTtNSDSf0PgR<_Vb;e9Slvlx zaFSM*M)6F?^j zu0a<&9$-qbHoC!sP1meX4I6TzSc&GfpaIkMp0>a1Hdw^qrAF=e%e%S4DfS1JrWkXP(zimG$%qc zB2L?IRqaf6j9V-~)R^_!J6e%nm*u5*gE6gPDAJOWe5ygu-3NR_XK=Hk$oNz(9{g-cy4he@;IbS zkCmuzgoAUaJWI{fqWXQg2R!ykuhxfTA2EfQ{kpy~clH>*EE)?jv||k}v=hQjq@ku} zEUG@ zz%a`c2~BLMS)FcAZ_hX3eabJ)#@EJs*$JA755h0b&ut#y`VBXfcC;QXz5dJ%J3T{v zOi{A`B%mgh;>)(&H@^BiRQ{!BNsEzN?^V${vQ`9r(xCMJ)6Xy=e^55I=0FB84;B~G zZ)w*FEd6@=n;Af`o+MXhz1 zQ&-txqM|!=(76&vTv*qx+F^DEURcD$+V75!1ig(&Yxo+&|E=*qWvQPu0m8GjNYB%( zwIdZfBp^V%`x{e?&z022_P72>*ChOzfls9?WA=Zj?u}k?+tIYo@`#Q`=3-hV`%l1~ zLhyJ>-`fJ_bgkv97(5>;!!OyXQRu60(&XjBoNaw{^-LUa+n#nHkEHs(9SJH9sx`=4 zDEjH?U)@#_$&FQBev3}vkxtkVe+FE>v@sQ1{gpocy`|!!%<|<(o>{KPcm{5Bem)+> z9-h?N$BDo1;mvJSL_=hdJs-pa=<2Hit>3~i_(LQG+5&?n{S?n_z8eBd>sH_ZX8!D? zecd-$KKGYi;}BAHIvQilR_02szMW7!8d`4dDmj+XO>+>Z!qbQA10HJYDFd252e8-P zjH^H0ZwPI|pYM;v=u?Hcm{N;?c+cjstPHJ%gIhb zq$MMlWY2gX|6yey4$$rq6884&XxfZ@NKjlgYM5-eP{B6hXof?N4^Co~9@fda$-rat z=&TJ@{DKYM46Av4ws0aP*x}>kGznfZ#)FI)}xZ7g0Z9t*u{Ppqb!Th3g{J;HmrD>^J<{$f4j3rclr zh5j*bAI1{;diD$r6zLUA0qOHSKRdUVf90Y;AB6t%y{KMZ$B-HdVAB&?jY!-S7JDg3 z3=|wH_b(6nNr~Iua^sX5g1W)rI_q8i=Ch5>(T3%N$V8m@Zd?z%RpL^6s#m~1;Vdc3 z-^M1mD;(A-9}i_O^}*{UHg=RCCy{aU?HPio?S>p+_cDFXB5%L7`5HTasb zsOUQuxY|7XV+GV6<307yJO~k$OE2&C(O%8V;W+lU^S8ncdXH)t4_g$S5=|{u<&>0y z`DU~ehqDdlo~y+=2*dC0aE*`i1rrnmQo%hh+;YvF{qVjw43qGRK9EnW=77feAw;pM z2-2w?6FFFhUdSztl@bKA&ajr0#Ju^tH2qLMCP4H=dFXsG5Q?&&oPVoNNb&qItA7)v zu8ws=bO>`|Fm7~`cgWpDZ{v_}?m8L3aO1OZ_`&{l+TP2|5s=SK)3=r#RiV8|*eMY!4DVJTKOGM|vYZ zmA<>TT;qXdS6H6q8;D&`(i}jt3B`11{zax~W*wTkXj_95TI01%K34xJt>ELfp1eid z@xL9s0tyPQ%)8t*w0ACprRIvi_XTd05=Jjjn)boX=Bvjii_Zqk`S=iW51I3mogHrP z+zDJ*?%f9*ZEfd;9({xtFg_ffeq0~T0vJ^Y4#f!Jaik5ZyTmx*N|@w@4x<^ zTeOMW)!iM-tfO(iR}HO0OKZ&>iYc$@*)78P^!5~+4mJLQ`W!TFYH+P=YdR`Rr7(1w zPx6(*yTF`)@TSV7Bog7n(u&> z@BR-rOB)vnYFaL`Z6o&Z#l;s@h|7ge+wGeI?6-lAC_4niN^a^BnyiaF(rO^`hBuss zH*$`wyTC<*>y`joptSPs)1x}q9TyqF5XYGeX&i#6n~CEJzeRx+Uh#t?3K5?v7^-rL z;P_MwfO_-ts|}^?O)3RJ3!E@8Fks!8Pwpq0^zkF3kmqqGM3?Yw$o9@urL?}!5jJ1) zNBUdmC$Y0rFUwyCeV2rXT=SRQ(5-J?h6#{}X9ox6W~+VJKoU-pZvd5Rzdstil!4iI z-j_)A<`(O_g?(yS0XI<>ugjoVGjo9#?&ZtqZ|!JqIn+y04+r8HUy|!qi6474cA?cC zlc=k_%hnG$&LLgBMQ>wd6wK1tE5t+Y8u6)t|3BA;-y8MEw~+SPFD7=mZ;&0I6IgiP ze3pT)C~VyU@G{3=o1t}F1zk;Z0hke|;qcOwcm!q(^OTV9!8e{V&^?qpMBRcns~^Cf z7MIF;Sz^Gz1XGu=LwMAlD!QU4-1h~~3wch3fEHzl6|05+Zwj4jNtqYc9MKKIr zalT@?ckf=#ve?JOv{qvXeLuZQK~r`A^@dp`Mn4B7@Y{a0<9ix6<$O_@f*suOS6P5V zIafex#L5b%m$IEeCS*U@Q0$TK9Pu&wyg)&~B1|WVZ%CBV9M%>Y!`V)t+PT&P|8<=LKg%s`a(s7iiU*`YXIkb4M2y z^#W=yT0XvUdkMB?;N0jbCa4D|JI;KKP7G_LmSTCT1NWQ(bUq0U47-VnXn@|5&b$7| zl>Jet!{blKhVsfwD&oG8u1v;eZ0x=4l61QgsbIQ&aJI6X5|09C)D)|{7u!XvKKqdH!;M2}k9%LHgc8)x*b z=hJsoXL@8lTNUV=W&g^(Tjaf8XzBW(kIxcS?^mOHqNkZ>e}Re56pvSd*M71cBRhg(U+5}h{2N;;qkv$Jyi{WNOxA=zxs0vglA=aiQ8!}E>e1aL&G zCyx=hOSmz_$g>T^w=M6;~;*wDC4`Z!i?6V zKt#YO7;rNhFp+M2es#1K4Q6;4m{w6o0IY0fB@9xL%|GR*Kzyu)V6SrNQrh#|w zA<4Ooy?$Z@g>M`S7L?rt%yf5dG6Kav-qlPiWsL|6y=RE3jwstnk*;lkNshX-oJ|e6XJmnEtu{J(kAwXYMh-F2< zYGfv+y1w=3nMAN$cX*Vntks)X!h7A{hd0b7FAg@I2idM=6 zIFEV%V8!6m6&zmm;v1J{_gr(z>U=kn-`79R9XBu@HS*aR>VDf3hpwXOJI?+|3192I zK2AAK53?{Pw~x_)lNLGEZ$}P)ji+fjIWoc*n@{+F=h3vjmY&G0Q|H54@!luKo&o(} z1)Ht?$U=@LD~cZ+9Na1kP?-U~A^uD&xzDqEfMA*q^mHQ|l=b3}?H^g;rMLAfETctP0KsPjMmcY4| zn(?~O<15IjS!dY503(ham{Btdn_H#*010|6;BxueYkzI^i$!DO zHOYu+4i*Wg5;2bUYYhW~CaFFAMS?k2befPJzi~~gud~SLD&Z4%k)rXL8&$sqx6RLg z|8WwQtC|*%(5G8|!Ru0qfhgv=c!(B?7E?sfAsZ4+li8LJOIJHo+>LstvKNmJyY2e( z@q=>noX{=0cC8xK51IqOCN3hXVQ5e}BG~f{hXSs$TYpOGVflBEOZZ)}BckCQpqc&Z zC_(zKun*LtL-lJrLYscl38n&F4iCq9di0N)=baIohEDYmg(0r#`d?}RpWd{vijU&t2m?o}5RA{HIRVfPOVP{$fU z4QpSI=YK4!u3V>Qb%^DtV>j`Tgj$%Sj@a(_`*FtfdYkrTy|)6HA;ngzrd8tUDNkQT z5>fF9y66yUCA|a4fNMC?Zvo6GR}#+4KW0ET8&xQZs_KutxRm!VJ|a7EF-E>jbCUWP zIj9jY_j^+vfFR@U0*e=#J9o)SbTD+0X_U4IHFb@69OV1_WTOD=XR=o3I4^Dz1a^FV z1{Rj8NoF{V9ApAH9eDvthaAwJM{*TTW9FOsg<>TPl$(E3wFwjZqJY+n>p1u$59fWJ zuKprRLHu_L7-}msXTf_arOx(t^|E8hUk?dg9#9lW4GCJBYqOV*)o&BI)BGve;Qu5V zzfwaD4VTDy(Xgq{v`v_TN z;4W1|QZEbBBc=z}G z!O3}BrOul17XTjio9|F`^^5$neY)MtQuh_@!jhHVWw*|67PBq5Dq&7;0#Dw^b9vWm zg9doOVDyivigHmoV?+vlVtx}7i&8~RRSF86Yk{yvyDolV{OjLU$62sy`$^(d0#TXmMCQN zrB7g#yr}Q{i^P6*u&4foA_vmZ5kCk2{BW4Ima;JtJ2u`jhcEgzS?u?ZdrAIZJi=9hL0_5 zHBwSlcfwyDEG?}(B>GN<$ujKiqOD(aF2|rQzk<(K7~d@2Xz-}rW2&MqFSp3fC=!mX zIWSqhljlVBSiEiS3V~NF6FK&j=pT|jAcq!lUsz?8zthrz*LP^BI`)5BSuB)Y*E@ac*9Z5uG7!Y3uQ0VSXZ!la4TfZL3>>e?Pg_+qIm+M1<*^9X&}cCt!QAK+^* zujmqVsVm#ZDm69qR!xkJP4}0&rE(q$d%*$Yv>zT6+-q&I;4Jo2($)m6d*l>xa|n21 zJ#5s^NEVJvO+&*}6IGPICAg}E0wqGE3|kz>|Lxf@ks_{))^sKV5G%S6Ska<={xo}m zS;Np(dGV=yO;iWf@!(ji2SLw~Q0K=^y=~bn7n{X;FFe#S&H7=Ru+yK?LfM?Ib$i18 zCm$!a1z-ygfnCGD>O!If>799e&7EX&C7%Y-^);{mElkrUL#iL6Au6EBhf^JjLF^S7 za>xYs4+UIe zYhJEmKm(|BGp7aafN3a99H$KO#xHMnyf7R%u_|`6 z^%*|zS);uZ997~0x);ZwXJ>Lbz`e&W$k^BWoN2{dH7V{r=^_O8nJS!OKHiD zuvA|^<09y1ldlx_?|;FR#*u-+a_|CLTU$BR6g`7qmtJKpasOSiYRJzbmM7YQ6 z`g4Hl1_}dC8{=^GV9#YLiQxM-TAt2Bt6sn-M%1vQp=P%Qa~V+1If%YGZfu6;7*w(- z90OBnQN62&uev_oFHHmD4tXZOJjxme35Z0G@h9&axyygY5*q)nU!m^%-K%C-!Qafu z?&FiuFLvrtHH$^7+Gi+utSuC&GQ?b6RQGnKHqLD7F?4mkkMs3Wy$lGfOs38XLRC!* z7o6tLE&5pXs|4yZr@TOr7T;^_r1K$bUNa2iQ^PpujX2JJlzLRGmXNt1MGKYtl|8vly^gqX&JN@iaubc2`Nw4!g-ESCX62Ggj z@5bF>+>Qce4#0M`f<=YXVR}hO$k|+dtOe$vJG6PN?Kv!Z8kDs$@L3`Y{A`)iq7>ji zMGcuTW64i3>r~yB*IoiKB6)a$c+z}8ps62s#E(-w1>7NGfOkF#)qU?k9Se9>FlO~R zy=Q#W$UZ&+!5Z=eM&K+L0m8cP1qOnvb^h6ZP_ZKgGTN2X9KuNNe$6+|JjGW6iNefE z6YaDzxsi~9FDX(bf-y6fBTbe$rH~oVE^kr(G5j`&6x7639*Bz0X9%)28m56<){TO3 za5nJr(pFEV&Vb`d|4#wMv{xGw?Vp1(wnvn!{w?yfGjunr$5>xhs6_X_wf=Dz6pg|2 zV?$#az8TRJ5-`=`IaJYQ1Gl}!Z4#m)a1P?usCCxb*S;Vr7(pqSN#-yM!*g6*SWw{8 zh$h?%*pMrM0^8CoyogaCIe9$+<{k1t`m9^4N*==z%9r%+o#BLl?HJpja%lfs7|-Z% z$V6YWdPRQw=EVBK9`Npv=!y4O9kc&)LHcRZTDuR>#T{}x0*B!hxYvyMZ#O(qgcS;- zAW6cDjR~3!J04`!Q^!?{SAqXiT#{(g7SUKcemTF@Yb{=|)oXoFhVpx&YJ-gHhgW{n zY1#`=1+>&w&^-!G!}Xy;S+)Ag?1{c-2YM`)7KVS5Vs>Bk&Sg6 z*d&}Xi-l)(Wis)@J!qL(7I~k}oZk-!Yn-2KjQ4nSZvT0F-idC|%`Ik-I~vnu>3NYd zF!QlOOr|izcm){>j$CE}Jy$jsnWODmXq9&A3ae=>8{B*q1WevsZ8V3DIscnAB;fj; z_O_d{h^;-Py62bd*CBsXa?|MW{2se6w4v9r)UkCF5E9bu?d>t=dJa!uRJ(u^e6CCV zIug7t_|vKrn3c`D-i)U}w=Oph4!0;H#h|2e+JB~kvT9d!wLO>enxDX5*l$@r26s1- z)C%k|X6z`FySuv+EHBr2d+JUX!ckeWsW>Ha^h=F{XzA#31*-A0dny_p)6%w8kUE6| z?{YC=FP@Tdog?S#R&UxHPi=_lw^75unu9 z1D~I;1b7&bKWavs^4%O2=Dwhm1PE|(4cl+9b>Ctd2AD>jyAH4ndcwi+qm5rma2lLZ z?ne`E7u(1?`eKca0krHM46O-(vSnbbL{l)jtWQGJ<@n7D)YV0$2@;C?%W!R#px$+$ zpp*k+Ewi>zJgFT029L_j?CekKmz77~W}GSriGDcmm1^GZueL`{^}cq_LSGCD*X)i< zP#*HNDAL;;sAVW~{d+DGL}BB;i*=mv?p>OkZ~sxrTdH2?ui6*)YuvTJYU_z1dV6&w z>QCiW9MeuuQ>uHKy;+~h?}`eA%wqq~^vMzzOn|XlMhC+A-a9-N)Bo*>W3L44SROoh z;G(m(wPmME(Yjt)Nsm@@>o(TA?wCitW(j)qj&@j>n$qvpc{?gg(1Izks(W$WsLDp- z45RBoD>vTY=^7--ws7kaJv|0AS})EKhiWAK=8Q+8IaZ=QQ}135m=wI~*gY7Sn2RXy zo*xP>QtMRv|71_$gSoZ*<3JB^ptS=Ptx2m~2e zB<}P7Ti&<`!SCsG`E+M$dAgj|5m+Q+sfHilKv3t*k^8NGGZS;sL?fJNuURKY&d%yr5jm6Fhf`hlB@1X4!51*rzGs9u z{n`vu5`|I-c~sn3Dit*xYil)n)p!A-C%v$vn7;d`ylhc`BySN$*_i;dP&5ylYl_FrOX-_n<}UWAk&xL)wZaphD;Ik*Iyu0Z3wN0H(eFy9VF=~ zEEDl&mmk@D6Ki?=ROU1(d%%^-zRdvc5!_x(L$*E{JU3@}(*Rb0!7gmt2V5);-)k@b zWm~;Hf1@{;;fAo)ZIJFj2{8b2UiuC;UG+K+u>x{l$23YMHe-|~P_CtNT!u()Qf2m| zd@m(xUAC;xC+?H!jT^)dAEMN{ZZMvP-peOcYGu~_*m$QP`oAVwFa|P$(t-hp!-u?d z7z_pN_*N%M?y}&qt7jf+6bm-9+tJbrcfc-#LyMn!23a3ePNUM{<4Zg6K$`w&7=bxGAVs^ipgKzeeKr_xB@gRzUNU2y3`$;(%qH z468HEs@!KnmPJYkV9E}*p#@9s{K%5uc%@@!$8*~*lqSL^#ccsSd)h*$chS{`9`V`C zrm=35^mkt@x&fLPSaK$TSl2z);vRD2o?h>-DEbnfpzPws7(K)2eIAh zxkhZpaz-1+5$YB;(tWc8uIPZ=MN_og_=%nYiD$M8>OMtL6IH?VHX?i2=J!Vfb4P2+ z3RgFt>22w)tImJ7Z)fR)P`Lm- z+l%EmO*zZQ1(1&{M#x!!J^p8%;_Fj4)UMJ@0_!7P7D7U`F;CFGRooAp zBG&ivnsB(PW2FBLNMsQ4h)Uq0nG;PzyZq`4aHf-P`RL{|QpKFosE9N+=zD9-_eL9} z3_)g_qL`G&r5gP4BSO;hbuGJ1F1kiJF83$x%gSX!Knpbt-8rXK*aSX#U0}pz229FU zNbNrZ@u5vmzOt6{a*!mau9yfgaG{?BAw?`&TE~m;lZ8A-;8Eo4#;=`FvGbAW@pI(T zboJO;2s!`R@cLa4JcdH!fb;%_Cxj?qqSiCG(kc-!!hWnNeUN->QI@GNjnzuY&B-Z4 zY^86}!}J_;I*y6>0n%6^#=i63Qk8pZPHv%g>-nkNLKkM{4s-eN^{8h3Wlxsf4*3i9 z&KX7l9{qqN?VQ&6K(VYBuovnf$I*x7R%KhgK`)G3HQX}dIS8A>hZm!sFwoMfNxdb@ z{%rguH+&n}Kh0;|^kdQp&inPb)yBg3$q|R%y;>h*r`7KnofQ#t)-57Lx4{|hYiO@? zT=UESuOKR<{=kVsHi4xGJ@A>%LqYpg&>mhyeE99;3kcu&8wQ@qh;U&PLpZg4H!|D# zn?_DoYZ+3|&_vQXT3hD<5at`~-YhUO;dfkL;MlN5TLw*HED(4OgkImwjHae^>&9@4 zPa95Vo-DAxcDcE*2>HulWcsFcqwRl_ayXqohY1pfjdEz&DnY#mC3nd|cr6<>t2&dq zS3-i;l;|B)Dwd?`+}{5FUtZ2f)2f<3P#=u1Fgl1B5NfGY4htFRrNQzrb-a-;>1f$1 zzBBuI51o~}x;MtgE`O>GtK43w_pQM=^GIpP2m)q`$+IGsg_K6J@gY3aIk)#lw`Yr z(t5;Uj3QwCT~D)SsOSBdb}29fM!9f^JHoMymw< z`+j{V8S4}7Dzqr2p&YX~LLzh&*w6SZMtx#&OPBPVb6wS(xVU)YpV>03&x0A=Bt)IL zsffy>v^g^f3^O?D8H(%chaw6QU)z3Fo_ZIlDS8v7Exe#`PT1NAAM0PW@2mOY*xyjk zYo&wE;`$HgS{FkUle;HYvfqda{>Cp%`9bu5;z@IJb8Eb%5Ye{?rY>h^piw(E`COr}Cx8JuO>ym@jvr5Fq8MwBR*YjL98b87adlgcO zPnD?j1YX8#D0nGijBHk~(+QegwTD+10yY0~elphH6P1^X5;a=B1!wiUigV#!pk09lYI?!Y0J;dj-WAVKbE02%w-+ zKNd05T4rbwl9uKn96gH)EtxutO-@!`?WcXFy5Nkhyjk~zfxsI3%R!n3rdg+?AW9Ek z@Rj$iHdowZ{YK5xOSkaaeK+4(Qf!_7l6D}-eUgy*RJ%1V2}o5|Ig-vz*bPDs1@NB7 z%jf{>@%auG+51RI1ST;{=qH@-9+TR6?;W6zsiK*=z7$5arq@SBMF~Cv0XIXRCGMOb z^owNW^yoLA%K;M^EKKj^9>Rrp-+*8?hLt>9kjIKQb!V*9BHF=H~k1D7&t7iNAo0C^ZD1ev|N93&%@UeY1XT z{Oj@4m+j0TyE!XDcR^Bj-(BVH{KLP?3W5?rPf7hDZS1c93thd*K59es-#L4JKFDM% z)gWc%uoXw-KGu%sVh1*0RhrbG7&bZ~`10NKiQ;-0y)qV#woX1+)^4ztLWp$WS&SG>5@}mH^e!Eph65qe-ad9_8yg^|1`?CqdLs zLfxaW#w2_CP$5;DVT{4?N*Onr*;?n`If*cSfyn`}>%YcpWB8^TMr{f#)DurJsNnPli1^ZPhn7tAxDq+jasL-4b+f1-NJ>Wj z#sVmmkjcIpKf0R*Xvl@UX*3B&1sGUZw0EfriUFf%09dJ80u%40<>jnmt3+q0ZE#4) zb4$y|baZra<8K`pK?61L$)Qm;O87kI9)%W~b5#q~hs04oq+6ykt3VSl0Az1jJoZXT zN{PI@7Ecb$*gptW17{x0Vd)xJzSV z5Hhj?rGP+)PZD?de)}vw%c}9B)%PEjoK}~MFDOL*eIMHpQL*NSwMnlMIXGUvdhN%O zK4in7!J#4Pjw<tic zj{7?R^{HOZl=5^5ayBWG$gg3t0)mHvQ`tLcR1etfbL5Y{*rGx-bwOdv|+`U-_CDcgAeOQ>7 zG1NC6(}#U+%Jbm#9}E0ws1AnK?R2H+Lhu*QU_;6L&ejhK1!7j1D7gR{s~=#wnl-KhfDYFa z_rvp3Dt|D`z+DYkQJFi(2zjI5Qn8ynEmlvhwBC*uN3{6iWI~HUiiuMZy8|@pLc@KWn zxsB21uR}_r%oKCxzN=C1IUTM04}JL-DE{*Zzn-t+prPf$%E~=9vHPpuYm4J#RqDNl zs^#g0gOucC#kB`k0lyj!^f~O=&CD#;d2yDL39rP!s{Q>Uix%Asa;tS?i2lfFFOG-T z_!j=OPLIICR0`>LK%^rw@@4E9P;W$PTWM>PfP|}jU8)h@dwZF(F%kIDXK#Li&`a6x zXEDmcpMeSevz6A$8SFG*gZq&#)u4hMIP0b7AXM(I#qiOi&IVL`zN5dTw2G*#vNywH2|Ppa$6 zy9^`wnyh=K-e_50zN`l|e-5g)o0QV2vi+2i3?1CoL!s-V#jG!RAJNkO zY`}9pKeXyD0cJ_&A!g<~8$c`DJJka4Qjfj;vjsk2diO>`E5pz4)9Gg8)rbwJ?>s)< z!WE;?&}X`%ok>)~f6uR|XgI{PzAU?Rz)SL3cT)9P%Z7RaL`%0IJtxQVdl78`rAP%V zD=d7om@5Z+XZOf^y7rlIGA((L&2(Ft7OM09jANV z`NE^bTjz+~R*eND@5c2~l;13F(IC9JPqfkEnH$q}Jm0_+c&dPJ;N&lq(CZ{>8FfEwKN7J92W@iMI^EAKfJbV8e)``kV@o4X4)3?FK2O zvgP@CG3WK6&j5={0{UM?U@NuIp)!63AG7&*R`$W(9%g=AZ^&wR{FIv(vbwrD2pC## z4u~WmF7I)&@Old#kcbyIT&CM<=|WrX6D1_~PK3@6p#0qx5lHB}T#qp8bk*{V@!rFh z>c1^+O#PtFk<2bfGt3zCsXVEfDpS5CbxW1W;6$yf(#S|rEev|m-kMRg$lm`6k z6n(GHvcH~X_CtKaz+Axlw5~sdKcb^fcclE{vyQo1YBzccHk+!$i`dR>=xuI`W1; zDb%l1v>SORc57&IEXNBuc!yTAF|y=O)j^1(@Is%2JU8FztF##k@$KNFvd|ZEz<46gBL0WRk%Go4b*1XAV#l%>QJNR>VxTo49 z`@C@fR5%bs=O>j zm!9pxnvL{-;y5wY652CX;QWtVS8(g1>aTXFIWkaPzp( zT>d?pNx=v>dz32B)I1gz4*JMrLv5f`?j`_xjAAIjE6av=vo4{sh}qdxdZnbt2X!k; zKTmp0zVCso!Nco|9eFBVYU*^|FT0VnL_~Zdc5ZIG&f8OoV6zejZC2cT$`N+oObZlJ zF1_x5>rUI?!MWWi579an!Q9K?B0Yg~P>QiJ5J}HJ8j$5p+-EgwEzbzbz9{yxn~LKcyrK{nh%|8-C}djXU>r zKw$6KNFUnf@7uscVCet$U=LV+wpJLoV+58v@y_4u(SX(`lj?JxJAUTJ^~l&MT0;D# zj}Z|zAN0_n>59G~0DC#gCInqvCM_*=m93lQ=&*kJNf0ujT@*tBSfa~VDd zy*GCps~{>14U6@(B`^Nl26A+fXJ&>i_m{GV*9=vk7k|fOy_RsZ(;YE_l^$LXJ@c1v zQKIDG2?yS4w1R$7Z{jQsQsbURT1)=ATovi=%>H;e(~h&JJxMy%7v19Z-!FAkMZQFc zv>E6Koq;~(=qrNd=8Y0+|MlVg!P)C$o{5^5kEZM02j>)^yQK+5si_oKAW(HOcIm2hfpMc%Ik?y4&*Idl2S~{yHe=ncwbRSp|h5&jV%j!Ec}E z1qB5QZ0<=_8z56-aT3yKR$Qfs!v4hQ&Y_{e+Va(z)Ee&b@bFOG&VH`_ar@3S<$3Ni zzMh)r^5^fT^M)fqg1c=jb|&Q=l#Cd5}V%E zN=7@zI=gqxNymshS)ivc^$kqyFF}W@zKTujsV`z%4Mp-|D1jhAraFsfm~gsq}Upv{|H=;Nnd2&m?w+{r6NM-lMd6D-(J`GR7E@+#r+diT--O zJ8&~(5d6VG%ogH013;nX@l*O{P;V>&KFsm!|LFP(sH(Q_Yq+8yT_Vz;NP{39f}|4C z-6GN*(jg@vpwbP}-JR0X-7Vd5>H7Aye(#O{`^I1}u6mJs&e?nIwdR_0t{p;)TJl;% z;|E&?rnm4UCelIGmVJ1uWNaV!puM7JBv9w{h5|VUH9LC*fTllgza6lT%Yd~jY(e1{ z7_2_vRaH~dB{M@0XM_IMbs1DR`m@4=V;FY5!i|b9%$Si0t?%ND22|*N-^_7Vcz%BE z2iWH;L9r)%wH=UoACySL=X zYAuU-#mR~EC*hD0gca@Ap&&Y}NMoTQz|NITtW3EyWZ?S|1mmAQu%Gx8neqCrpBJpr z-Hpbh--$~A$Y+=~^cUWoQ%V`Hh>_9J(*eg?4j|D+pwR*wVcUutze_Jz{G!hB@GW3W z#JP~N8HeG9t* z+LDr}d?1px2tM(PDt}`>3#-8s-LGltf`L>S48NYU@6@OaCbQqX;7!iIM2INRplj78 z@fQ-mKA&PS9!t-vTqjbw{>BXXv-CU^k=8olw-y&^>z`V>skr%1TTru0c=X-7?`BY> zT{3^J8G5JuZ&4vho@BlMB#moQ2^*FoB9IKhMm>AvVd^lK0@pa0=s}`y1f1*yW(3Yt z#g^Swo`A<902|+gTBA{8ZicwpHJr_3L|~w>BypV6tpKz~tT_nH?k8mo^DjGy5Fg2070& z$aiJA((J&N-PNGj3UW9T*>C87wkJA#aK})(9m@InW=NXs?c49xy4M{hOANwaT**o* zW>?D2R0m~=2B|GBFbaDW#Xl80NS<9c)uRV#+<0xYJMV?^7&iY(wkK3bw1%iyq+K0o$#}^+Aig|12 z^p5EYdIbK1j)zOTyB4xP&a#dGQ`N{oK_Lvf5&z7!8Y(b)zdpvt7%$*tYeHzNs0NI5 zrBQ;xdSE8A`IjKxK9F|5NtgpcT0)nLimE-8c`J)KQC-jaLMcaS`&j|RS&hoH?3pL} z_S8WA72*iaZG=k;1mlVZamNK$=@3KO@uxsd4Wp|f(E zDB47-JKU|XWa_sKyEl0YV(GKk_U32Ys6pT?U~=w^Q6HaT2W(5HCNK{%g57-j1q>U4 zj_U8iOW<*E9J6QA9p2(F?Dqg1cA-E#hOeM_SdaHT9De-xu{a^y;jMQY!~Up_g~dp~ zA>`hJ_vfJLlzzC{hZC3*OvD{j=MMaX^%Y*2;0;Rng%3Hi$gJBUaKGCSsHy@p4#u<& zj*q{}N<%3zFdTw;W)R_jk7+KXgSZgyuCCIVy?s7Dq9}59Mv3)Wffk)c&J~Ls``}Yq8UmV(Hh%eLDsi#@l@29AYZ&HlrdiOJ$*%qCe ze>-X|em^Ci|87pAqQ0U4`wDrt{GP${Ql`5-nv#x=RRdtscMLk8I|O ztgysi0%NVkU%dE4@x*xos9Z{h0d91F?yBv6%niU_U>P^)%~|@?&*vH_0+*&w*hV=U zDTV>N2;h1sBcP9%_A>PG_afGqEDlu!P=j#wB=E%slkkbh-HhWGs2suBSARSEs5(3r zevt?L!uhc>&F6a1MrZE&E_7!k{Qo3DystY1-xOs&l#Ptt6a_bv9Jm$n``AfaNQUy8?z4*7$P#orN$m z>T)SKNTFFGsR|l1~7;+i?INmqf92g(30n);ZQY*lM!+r38W=h|{pz8d@!`&#Kpk zy2x91Zc&UI7#L~F?Rw7Y3aaculW39owKYRE;FOGd2Ggcm)Z?>6`SgP$>Sc0lV)p9U zwkq5+z+RT!JU%)~+uO6m!tQtDeS#Vg_MPNOhnO;aDf8{$=SWyA9RJq+ zdy0{O=DwuDVKEB`T@~^g(=_`xj^Fp_*$=wLUyDx->cu>fTF(!!E+}MaLy~V|wNX#w zD_JmGe*WtC+H8>vl3bJ)HOmf3X1n& zKG`ea&Hfd1iiUv4D}6n?>!Er|aNm8uE_MaZOB)#(Omm6{5TEl4BwQTltEK68M2LS& zU(-;yK;Hn%wS@)7{LsLU;`d+2=2$W2s6dY}wZ^Fb?lCeQbkD{{8K*Y_m0X$w8DEeD z;PM~sMd88Xo~aL9U@X0@b>&+j5l6W=A<5&Xi7+x3xhb!r~9WwOTN?vbNTH(?c)^! z$0ZedMjHj|lun-r))tp!d2-Wxj#WJXhRTCdh-9&ing5ODm|4d#d_T#bNSri?WQmZ_ z*tnrkV_!L=eW66qf*ivM+?p1#qG&s8C4be_u(TKjK|xS_>3)DFR^x+`oT1x#u#Bnk z0Ac(vT!_fVOZJUI^kE_TT{)~p`;vRe+6F(HS6EyVK<2LPpfh?=_ z(hCAsLrkf7?r?M>&Lo9cXzw9y)jNJ5vs5d28w9;W!fi_pLS!^+L~`3!yiAHH+6Ev$ zqV{#0M6sDf;1w?`!vuy8(b5J?Pg{~3nye1EZOQrNZ7nSeyAMr*(T>5yv}{GWkE?t0zTzPyd`q|B zeJ7Ji5TIBcoiwROgzU1<-Qexkr-KEBjMq$pxqmOXpWxv?U+3U>oD_^;sarvi8kM{Q zlYed$EhK!L>*%p2t%Ni_EiQW7&C$H|A!+__E_?UOeK9Z>oSr^b-rB|{je|L`l#s(b z6#%%tu#qA{oUu*>UWsv@w#)_vidoY@YvJyoh^mpVqxj-?+vfy ze^>l|DX zJh^($0m45E=@-oT_j{uYq~pru84uywUHI`kuOO1J%k}JhtdpyvuH?Xh=X2}Ut@Y1h zB+0;z!vo+DqQLqkQa7SUm2j$gwi-a686YSm!ocr7z7rxe{3ng68YRG+7We*rv4-pO z9O$c+CnIg%Y;R+^hK~w$n_(?Rmg%R?{3T>tYx?_(t++TiLTyM(aTk$%DO{!O$VgEV z^DX}J#`}@&9X(wcL9zBjLwbHOJde)*ma>0~fL|}d0zGnGO!zpvEeooAk0*yyKDgTm zg#`i-(?0ghIf*%JtfQcGlO%6Zc73H;?Ysg=<2slRn*zqHltg^DnE{wBFq;$yy^vG@ z=|v^D9;0Ugj1HzqxU9V+ zfHzLq2@^kRqhk^mDJwEkn4d>Jq6BY_Q<%XE=sz&%e=Z};C!ZV7#vNjqo$sclYGoX6 zs|_vr_eCvz)LXPQG}i~1d>$%(Gb{!1$ivau_Yrv|Mce5h6~meg zt|RsiIuUi9CszfN-#hjU%MTk%OK;SQ-`w3EFY*GkFdC5+r=yJ-4kDB~di=g9X0LLK zIYvHz)k<~+6$FdX+y_!|oS%WwisbX>&tKVjf?=qPhn$I=R`17cCT08s0=_IHa^3%V zO#+82QdvF@twj7Zes_`fQl8b3(Dr>=L-~V_{7FTB2VotE=jA2j92yuMmXJW>Y&KV= zEc^mBi8)0y>IrhpB&_@{w`JEsfn^X15O_ROuIZxWCWs|y<;~Q)#&Yf!BsVR5xdnlw za+ulLvZ6g;@lJmgFAn(WEW>5pPIyU7n$^N{?l*kE?pvExw+SJdbiBRYPvz0u{Vz6% z1!`phpnQ1Ty1{~2qyB9ZOlQhq)@ysA8!e*G#KeTPD6F5E(>z>~aRf};J{&%@Su@i^ zJ_J-Yn}EXcwmkvD!@HFM4fk$bHrcqXS$zjvR|_;_+mw&Z?G!nHejPN~SuJLHX)tXPw9O4NEl*$f zs&@-ogI?x`W`3JzdIofK%K524k|i%E_eFMkyv7;ef?py_ikh7Aggi*`ji%B^X1)bx zRZ>ft5?&LR2eaYd`|pd13ls<<82R2e7CX4(1b4WxITmQkh=g9QhSZ2|FIYqbTgy+v z;ZT?Wq<{LIr2ZTkr3IKKf`vS_X_8*jJLKdDdHBZ*U?>#iocic+WS`<9`q*xA% z4O5FXGUvSFNT$^@01ehb;a2wg+Ps-o3R~mEq7Lu=%n?`#V-Y7lX^Pj?Ibd2$l?TDx z4?vb&D$Did?6B$V^BFnZEJ%U6aAfXcTxKuodYg7(1Y!M!d;X?)zh3M-oZs0IEofj# zaEq${U_MG(oAt~5&+p8MinD_l%WeVO%J%hQ>%51m$`4c3aW;d8KLs!$MhdLNiiH3%uG z^WTBBypY%Zjyt=x~)rCA<^(#b17>8Ro2hHj@27W8ksQ zv;W$_KT-7jrUkFtI=EsFJ$S1A_dG&9{QMA)Hplvs;JTY_@C}ch%6#E+qxz1$0nW@^ z0d65n#ST-$B`}HjE{=0lQ&GWjv^F5Las~*4PpuNPP5i^RmU+Njt7x$kkma-VS%7(G zlX!aUoV;uWeFQ$Om%!O!vgREV?8GbuW>7Sj)+)yGw~beT2#HhIA}4`$-P z#)I(9wTiRA<*ynos~2UFiOGnQZ0~YKUbFqJvHtZ!<^clcc@;~haz`9s6CgZ&{1a=Q zqH0`-Qx(={n%o>K*kr3rN)kYB-MZc7Xb(86-Ak8^U`?gN#)`wbJKz!GnT;1dpX^{S z89M+XQ^Wa2Ph5O_LpI&;+Y0E1-LoCM?n+7#FO##_9uyV!EQCqX(9aUkvAP3?@xh@; zA16GopzRDJ43zra+Cy|SV(u^(S68;2LNv%d(IgpOjUtD2mLnID#Sn?(S-LrJX(iF(6sJ-dG1muW;F>QB- zW-^;uO%uHGJdka{NSQQuGE!49S846vy^CbGAx4Rq?j7`L-GNQ|3t0sLO1?KXHrC{r zH7>CdFiQq&Cc>Vpc&zO8__k=^1^oE&1Fd|BgTwh6sQSAGFSyCb1hnOcJv|if;sV7a z@jg$cs^rm|v8_O}-)qeB4J0o85#=&>m6BE3r+ERn!S|d8F-7eET!KSL^lHTs>}E3( z2_!pE==i{Jxs=J!9RZUX7>Z^7>K^LPTy+y?zP|D*p7&m-?nAYQOs{M0G;G zr~_leukDCNqw8E$!%T-WKNF3qB`2u9S+pDi7 zH?K5@JXHfjo`(;c*Ur%VTh3RI@8119J9^2b(>sbea?}&TjoJUGQS18Z{5YL$lRXmy zV{1&RE(q<qO_+_?FtA=aI6QpnNPX<^7DLA*0&vM-1WFnqvyy)F{jr!* z=JFzhHa@m51Z<`P`*$O=tZZ)?Pt=yLsae}hjWhTEVO;|nT+=cgbiiO}eWj=LA|jHB zWV9BnWY?2sUbTxcbf;yDAJo)HpEY-G>9={t*I>wtzKz(bdN(JF)cEef%F@!{#G#dS zUyYh#K(_X(Ioj;soXFG%5bO{bO@OZRjwJACNWOq&)RunqAsUr1)Ovw2qyERq03<^z3?ziebOvrLAkWb_4sprTZ_)cjPp_vc_6KuXb}L zpx=0yqKSOkMn^R#jx{Qy1YntXtkr1;%>%CQF&G)yR3-ap*R`(Dbz38S@4aU?i$fe{ z)32j`;t(K|^@=5F%^*$I+9f(Ixb#u-)-J|54ddvg2*OZ4%H}%Oq}Y`pw()Ow2=|7}i549}T32ejE{FAu$;aB-x>MgMwIicjH%#m! z{&T=Uc!XY{nZRp$pHZVQ0FH=&fcEwVIAG*}weOZz3)9$3A%hFDqyw^h%P3eX?omz` zDyX?XGbQji4w2|pN@GCU%E06N;nK{ap4~Hs%`B5`e1*Z;=sdS|@uvHAK%n$j?bW;_ z|31_)V=v+%q%9uIlo4`uhq?q##{{)9(_A|up8thzoj?SqldTt#I0b|03^m`!+GD|z z3=n+pWjdfR{staomke-NLa z$1u5TK*N0|tw+6P-5lS$89+%1191m|;5hAs31wp$%WVR^m(K;Hr=8v1tYf(s=Z+2z z9Ws4Pbx>>=If_$~t=4U~+51+xxWF>ySAmSFY`elgeWl!2^GjKW8(eKc53CbUyTbXL zZIhPhl#N<97>c*+uiDAraHXcxN~L7~ABk86 z`}cUCrstr7tE2PcUio=@3905cHpDfnZ9kLbg791-J6eo%IjU~)eYzG9>7Qmx>$3y4 z7Fg!)rwfQQwl>e z;IYG^{R5SZ;Joj1bILgh?>IU3{XloBj2ax9^;Hndm0m46mbveRA*#2n_h%*XPI0KkOUB8vLbsX=UYZNJO1=jZ%+J zw(8Rl_Pbny(|toj@oE*uf|@)Pp+xMalc1)}8mv#3`kLuHcI-s@Vabk}pK(E#oWi)@ zu3BE@1tLcLr>5#64A;kx9t~wZz3(lxPq87B-`z8vuBc~BGakp~EW5NI5oTlSQb-9} zzg=dU{HHZtcL6)W?{vkbZ)fqFJo=^z2TL5m-BHEtuq(|sKUVlaS|&YGn=N-@zP-FB z780M{{N6c0MUU-sGe@q{2i)=$VHGP)O4&jlJR4EXRt!Sw<>PHswL1IQ;vBaVti-JR zv}f`g;{R(ftHgT*kY?Vu8j*?ffF80qU;IYkIkQ zV1KVbZSKu`efmT6C~Hl0YrqB6UeP6|z)kUsR+(u*m5&7!*l!1bQ$W}Y5hHybux#JQ z@+9QD0efg1jy`d7_VsLz6@0%WH!w-uXu8s>rHRRzcFMW;t=?!5JG!I*Qlo+f8J^+w zZo#dgZ-c*rI8hWFN{M2NkSqN&YhA2R5lv0Xq-s($(n-JK;Mv1AqAW<{?DNYDp8oxUDzI~saHGgj>gPd4=h)A znyo|IosQBn(k%0bjp}b6_u}{!9Pj6g0_o}1NCp3pPhcclY{xdG`=yAh^SiGZ?dWw( z*#k7+WOOyFA3hyZY+prySQ@E~BmT+{Jdr7q7EQP-kYiAX#vw3l$`ViK7VL?Slfk1S zYBadHI-ShC)92LF6PByb!XhP=zBzHf5d#gIlsofQbmvZl>)_W3oR$-BmV47ee-mp z(0v>K5b05D4{2oGPzhaKtav_me>GN(dR=cdkLCswr=bU1(VhV>yb0TbPmGe*2(sfQk z^(CKrOk_J3QZkm(t!MI6-6yKA;P_aQ5J!SDvsg!*=I8_s>C!Z(R+>`Ag^(LvKB^L6 zdoY%%MVU8;ZPU)IWNdp?yKexy@GMz@Im{`Zca+e={ngFY7>a(rU;EqS zWBy%Nx_Xn;?^)v{$9HL~W3m0r)GKr|UL4^_f@bVzvoO-`k>S^`Naa@rod2e|ng}g7 z`-2zC%EZB3w6ILZ>21+3mLnW|eHCt6$~$XiNOcO^zro-C(Kd!=6=!kM3~T-f=rMwo7bDlGJ)>_n)+{CDrp+Q$^WuG z0KgsoH~YIxJ{cC;3z`$XnTD*gM>{@8LHEi1r^=0J&0b?nRaOd@xn_9d=OQ%{S#vUH z3B%aMR#wBW(N4QP9VK1d++7Z=(oM5Ly9x1WJ?M%~~w>JfFRE=hv6rqbY zDIXdqA1<4K(B_UUsXt)1Bain!h+(UU8<5+%T}}&eJxLvxuyXuU^(VrjqiKXx+`*PFwOM@?&5_IN z=*H!44pM=2kn=pmQwEc2b7S};>mon`%p{}~Sth#7ImPaH@P+`$#*u!EdUGN&qtx#m zDgq_pV`YTO(`P#>AwB^IWXO$ z`y!5Ci6%u1{p%~@De9U79yq@%20XF!SFubXHXU!|kyqDTx0ek%Y6w>Ip5_k+p@uFp zRjgnR#0@E775brWP?CJIjbtiU#PTT6Zb-A)b0t_voGLSWJOzwM;x~r#MY?yh_t=9$ ze%ZmT%YAWkz3Ozd@7r{7u5H0b$M+%J%awG8ehUTfHI%jNMIH#secGjJ+j(7Me7iR% zlvZ`x)+T*3GeMIdh}w4>4&9*}go*v}JU+It6#a1wUrQJ5C70tx5KYTfc++fmK?84s z*7ZfZbzE*PQFLe(eUaw6-@Cv2T>TN5Ab-xz6~9FwUen`EOu#W9&i{pF6Zyi;;R7T; z1<XjEjp)u91k}t+qSlq6viw)45x%#xg-0F98e?76nwxuVBg^(9*mN zX0=&G2leSf0+nPGnTQUa23s9TH?gudKe}oUR}@!nc``N@7WwuM-iAFWn=r~zu&~bT zd&*L$p0`zxmhWsK?$&R;kZ4i+U1kYTc~X&*3VdUOq(&MziM~uw$J;13onq2{E|^5b zomv4Mq<5WBQ}nh}LKrypespvi^PY*c){^|WQ*5zHwnO8={@{CxzybM?h%B0WQbW`UX>>x!w>0w`V+D@DtoC0DK-$^K(z<@~yY~sXFJqqPQInGBF=~Q{vwW>qDO`#kb28>$f&2t8v z9K~|!k}iS_*R<`Y@qA&0I}IQM_R-I1>cn7^M3@NRvv~Hg%=XkfIU_Q(O*{IesFQ9>m!5dqxaurAeUA z_E`mqEv>Mf!A~cF!-52661H-C7N~Qp4tF<-$$5BUVLdwci>bM_{dUqw-NXr;D&9pV z=fMLN7|RZW|MJt5(<-Wb^wKm2lDg^m%NOu7^sjGRU zvOO8$-Mez@g00qj1g*t)rTP}+GSjidH)cd_>U^+Mqsm&!>4&u++-Lsi<`8B0jv!~6aa@32rMx(fMYba^S5Ho7sN}_j#c6yA4H;9KnN!YC4ob-*-2B5-^?^gl58)pTAt9U;Tk)7F zyLhra;T&dtC@ws6xVVss@htuoY269S*|`JVOsewQ==Q~Ov+vzV`AnrAGP`T--Z|&z zAIhVnCn!*Db8#8&VazwexgXrWncJXcZW^*X9IeBBXwGY`ErIKy{r46iQ+~lJ=&(&aO z$I`LhUb`S5;7odeS6vz9<&g3hfgfk_49}_PpFX1-e*BEj9v5wEPOL5jZIuGxPF1}e zNRzl;Do>%~3tTL!^WW7H? z(;DiYh;Fv!&M;x3{OLjgR9xTd>vVW(z?6IxYfo2$eptWT$b6(`J}aJ~ zPsg5p;+P^H?u_BjQtVIu-Tt&+=&wj1|KszgH5H)DG)z{binT zl6fxEaTw(ppLMcfdX#Pa^tPs( zRl)jiBy?;VB^gvdp=Qkpg^mzVHBGN?3m*$iZJ+#j}Q9?KDCW8XVjYSj~mT%uiI3Sfg@-9iY%$2N^oHiZC zbyo*-IQgpisoQlebX!Aho5I{xm#Q_R$0m2_0}{EMU-F?ja~|g87E613GBUt92kc6V zf3MjL&)bSU$bS&@p{`K9`~!P-O~V{tfkg^;64uYa4MT%wu&Lu;HHrAp7uq|sFz9e1 z=%5I+V8SkA-V{M4Y#V4T0rmfrmgDlh#kl%q8v6O-cTD&3yj4#nB_+d}3Md+BAxsT) zQ_IovB}{=6Fd?B<4?rlwn+jIY@OsKeXJ_kOYfwXbbn}8}wywI^OALb{kY35gIqe_s zlA zn~B;NukuF+Qk?O8@B9~;r~)_lAzYF}ZL$aPw{;lf5FNV8uZ(x+Gtcdota?ZZ*>~i2 zpK+YM+!)w?epJ8W8yFaXjIvD?w&y9-E_J^1ZK2#E(6T>A7CUVEeexd?%-(Xu$>av3TaV+5>)e6(|7F4$va|68RHn zT~9R6fv1^dHP`D7Bom~%wADA{H2_+Nf~@65!P{f*eENk*Gal=u58XxGv;8h!y)yey zXet<0-Suh2ck(Lgj^)fl3ieH)ClbD_u$)Q0U1y&(ECX)tnx0<2w3ORel(zKicp$ z39^z{N~yEIir{TKwA-E3%9%}gcf1oK8+&tFSkH35_}rtd?F;|)*B=zS%2m^Y$HKTqw% zTzs4bQD_->v6d>0x<+0i2EZi<4p-!Wao<;a>|mI&nKm`>M>u5jT#;S0ycmAVqbC#8IR#bi6VK8LInl6Gl0E;VG?~(pOhBLHkE4#~zMg1p7?;85ci4ljj>N}0Ol(+9|_sQDplWH#v)<8njfL+EzZ8C-*3|uw zoOUkjEmqg-5!2{IRaY6)y=iTir+%CYr|pSX!kcVOHYVjIPCw7p)e~0sN8#p~;gE{R9wCc4= zqu^Nvkf z0k!5kiqy=^;a>_g6fxET{G;V!U+SbifrPirKUxNEDP z%EaosdiL$XDh9L3Vr7!qqD@R0>N-euV5F!WK$Z5%_JF9@(uAznWQ14$fQeyGN*>Y4)tr@;d zE_2^b(qlHz@36#kD@fHyNnKJnU#nBjG*7k1-w!CpIQ*S)d8A-dW4X6_?NO5@J%=M{ zBzRJI_vihRc?O9)@#iyz@khHNnKFRzESlSX>qX32QYVMdnUgkfCIORA1@7Lx8_szB z*1%v0jPG3wSJr}MxcCg$ES}H3?hF8Wt#EGv!Sb#5vt;wA2*RfiU@l zCyr--KiTOE*u1+#D89p2C$ zv9T_ zEe4PuF5u7j+}@le^-W!Puv(vMr20hqH@ zdqQwq+dWNmej7?HbN!zPpxAo`j86FUk=cxQ)$#wi0nv2V%ssG zwEr$-;SjnhlNO;wlmjZ=#E>hZP>)TXubeA`g|t75a?ehRMlCqNuuTEtMsYyZGMnlo z!~z{s91`RcYj4F;SHPjR-I?Z^1YR0Q3o8PmZCm8M53qD<3PdM!Cn4Y3z?`>gm=v#P3c zEiN=|uo~)MfEjvJgVt2&G~MK`sxnJXy?*M|dq?ge=G#7}t4wX3kI{Es#2m`213xte zc^hnfShzBpjuOi%%zP2x!FxZjD^6Zn`6X_`S0dUdo7r-#g>Xl6y-s2)O=4$qtz$i= zA@;nO%$nAADja9A!>BEhoF$f55?y3?eP<4b>1b&(32`pri^9(3CCQcMYcreVN1Xa* zh4%{w{e;N*hm*^c9(ocgGB;DMe6>kUj1>r5E==pHir*Dnr zh#?{mBDLqCw!LOq^>jC>NQbniY0|bfmmAKWU@JN^*kCe=Jt-p{#D@E+KQ4- zQ?uaOw_}E^(2mHh?I2d-bI!j;VIQevL^PBQGftn>GpTWN!-rQUi5{Sd@pq|^!XA_Y z3w-@qXl$$mXceY{Cp`qn-ai3VlB#go)AW}>tHB5Kg-~!z%L8U3ZOcc%78JLb1Fo(^ zz&jvytbpbQ&ClDrA54D|0i*;hjup$HRKZsSkFAg(B`e)&cAFeoV@*}a4VPOC@f?LC zL4S!J_AdnXuSzjIx5<|d;4@p3s#T8K` zb1I6^#|Beff#KY!s(iqq)p#^FTG2cPG=*SHW!lxLB0a>kw6q5rdh2DUXJ-Qq*GEhn zpvBlSH)vUi0l9+|$L*?=Bl2Z$L$`qNxbpd*Tt4SfH;o}btlLoq=X|}|`Ys*7x(G{P-3kwRoOwc=ot>S7*7ue`17ifZJaVYZ{ryA~ zG1|ZkKLZfOx-18X%A+q}E#ShzIK8O4cW3wT@u}NiTTG5=IR~k{U{6~Lw0sLhXs{W8 zgMiYM1z7T|z%L(6Q^qN(Fmpt9=1t^x*K*vEVv0-KUhjQW*xjp+%K)L zl$aWacMLHgucVUzRg+>@<7>>V)$|{PIscIL$Y3F$lW;$O`j+W)g<)c?lNZVQoMso# z`QGPHXjiSiu{`(eJ^vBJzl(@$ei(ssvK^6BI%PcY?w97#ZN#-BWnN(?pYm5dw*$wd z;mDJY4xj(5I6VYzW6Lxp75U{C=W+3&eCrHMe6(uDNRJI7q*OpE@F5-AdOcVk8%wB| zrxxDS+?)mA5jkLoSF@$x-SiZts2X{y6T4|)K0>e%SlHnvPIyH%bCtBS1U3*Uacd$;#T`az2YfQiXXfn#sL?v?=mo6Q z%-a?=?Lz#Rmi7>zI6~=5w?Hb%%qW%?=a<2J2CMIn{wnoU5*yPQoY%%2yJ)DW3cA0; zU~4AH2O^(8JILfozR3J@^;C~650wb`E*5#z=ynI{Q26-u-;XkHd3ZbqJ);;_KpBIj za5`fM<3kaL$PI_RJfL|?8<_9Ys?&O*vfrfNr_C79pVMVBq4AIt;Rp~MjKH9o!CR`) zL(}{H7zhGMtngSyNz|G|^k;mGnt`r=fose{R?m+g=@&H`#ezYyld8~(WuynNIvQSj zZ#7m6VT=PfM>$c#QVHoKO~B! zBRvh{!2u7swLqr&PmYd38!F#}IHzsFOtZBSW$iRTmx+kBsr=H@0wtSQK?pz*7G9HV zSHn@d^*fLK*RP)^18TI(BPrpta z7tln|P2bU@`iL#PBNg?;6NnyOE-Wmt+!PkM4geNaQXE3Sp*NCgjXP;)6}Rp0hVLI* z*sm2)C;<_?RCL72${tTUdQxo-*uD80*HOPL3L85SI3GblW6%(^f1p<Q8R7kN-;| z;Nh{3fQ*bx4bF!M;5!Hg;U)uWDoRMh>mkHqnUKat0VlxKG};<3Vwo-}C}64rBh?LU zQNdjV+Qh}L^pjO481Y=rp8-c#YNn=K4BgyD;Ol?#f(5FOG8MF%4#!e7^pZ+!fJk#| zdX+Ti=iSM8^eG}j6g<&1m@X0Ys6_y@d#~x+$VfQg@f!iv9CRsETCOXC;qz*RV22(y z=9A#Y#k4NN@4H>YL5R?8ap@4^?Ai7^ztghcX*oC+etrRNMRv9W+04+!kPf8%F#th} z`k0h{5wVi|1hljgy0KsuTma`Jjt5E<;EjC*eNq=t=LAgfUcw9=K^86x2E8rUuRlx% zSb;~`T}j7l{c6X}awh@|jCbdDqf~nkC7f^n`2jpA@8aU&4d(NSQ8O@jP4f502?{o~ zxFRiFfy!7Wx^h{|Af1)p)w=~Ny8rJN z{mc1g>GXl39|p^xcFfJ8-<)s0R)4mm=epfyEeDuN@z55%iC;V5{O;1~syqWhs zE{GpGX&O^~f_U6h;7{k^)@isgm{ry@ao6krem3woTZxTnmBLPkiQ0w?sbUqq(905> zvS3|Agl-snlE`@N*G`_tMF0+=X(t5-y0JW+jtuxW<FU0?kyCTq-ez&ESa)9vWQlqOaVWwu#M!wC2xEUQO zN+>CSE50A`_YcK+0%{)8)E24%feC`AGlDU~9qfj%uh#om7_LZImG~5eO$fXHiZXw; z-aMe$pM^fW4H_yvMU;Qv8P=9%OJBIxeXHx*-Cjp09gvCD4E=e2wQ% zlK;>&|Mi|mT!^`Xq@vN>b#qum#ACVcvh*Z^6B2O_{YRy@JqJP1j(q(7A7=sxLewHj zQ6lq;l2*7BmNcy6|EU^#^4UXtnNfqaPM9h6^HVJ17{Zs0tsVA;f2@gqUEV7Rfi!D! zbVRA8jZ)M?%m1A`ze3&5?-H1nIMo(x%$P0O68$as)ga!D_S_cx%}@BXJd=R70M}D7 zn|9-ehzJo0!QW2OzgI6RP;h962UaZj8p4C^;{m8|2p@L-d8d0tlcJ8dW}cV0YY9Gx zRvpgLBXqJK#8F7=leGXGKziWQBY2gQ!D*~`jQh!g;_K$18v?PF3 zSY!;H@OP(889h)?Q4yYcN0O>&Qo+@8UL^CM*WHW&^ccHr?@ygWZkN+~!%gGT{^6?s ze!nFy%!!4%AxXf5%gV}M?PB~-_WZB^(DD^zwYPnsg8x!fxnMTQ7mZ>)JThGdNT%X( z?kH&*A*`DZbL%kOW5C#i{jV-5Pj4T%(NPtbS+f zKbKJ96VI(JqH4FRh|x?yIxagdx&^uS93-rC?^#yTSv=7X$1 zSUeHD+zC^hp3at0$@^5CZB_dP$#k|l#D3?QFqL%cU~GXD*}Hcq;>8F*U4!6|)~M!z zh2sH7%wZP<$ZmxhV%XA{Qxf~qYgRsi@Quh%xwSWYiR=x4l zJdL{L>*IGJrx$1a()_i`Z|jl+mX>mCuT~h0Wg5^G^EQk!t6Dea&R6e;Y0HqcUl?tG zIqAD5MMcb?e&DE-nDbl}nF>Xt9332_CK|YCysfKKOa*9K&X|Q9fJj=jRFBn+M^L|{ z9|}E5bm#M%b+DsQ%vv-q=<=L_t-kpr1dW%=i$dhz+*s_wycn^&igCcDZZ8Rclle8F?lGyf)9V#n@CTbg5wYrJH);`vf=R%ql}zat@Yi<03-K ztp&!BmjB1rS3p(Ou3H0w2uPQ7cL^xnCEaW~q(i!6lhWN?N_Tg6gQRqBX^@uw7oL01 zJ>MC>V+{v5aI@ZgYCdxo(T7JzKnr&f0z(+j4HEdD^OYlPa4}Eh^x!s(EEm~(FrJfx z|KVVG^Isf9T(6*$F%%3&Fv#toA%G?J>cIv&8YqF(lNprn$G@$3Xe|3xtp(z&keJn27`qmA_^X8o>KrO?XF!dUk zzY^FA-A-XZwX9Gq7L)Aiz!|-U$(NkGr|9vOwYWfqV(EmnLI4RjzG(6mM&2;$&4IkY zYuK+Mn+Vkob6T6|xsUy8Q)!WS%Tw=6<|;K?vm7qYPfK0wl|tLB{jfl_1x*&X(!|8X zH}%z!S7sAgm}KxbNfw4~8s*ZszB2$vroNB1P~*&Wv3j)4Y<2G|lcmE5>_J2Mslaf1 zhkgDQ*Mftmr`N1UEA`10daV_;M8Sroc=+_sNpRutP&zs~P7Ov1`Pf@GbyZc8W?)Tt zPnT125&osq$kLj5h})prmP`((Zj)1zz}=3v^yCn^D<`MN(_-wUV(@%aqfXhImrdk^ zw()#eFsUOZzLh!3ZWJ6AlV7kN#swgZ`91Dk#x0IgKF!^?b#EqX*N4pW4DbTCLmbOE zBFgd?1n4FJ%dtcCYaD0GF&!j#r1fATV1Fjnr#G(y+>xSCNtA>a@Vo2GnF04p_30dB zQ2O$+Nzv8R6Y1S*Pm|}ZWFwy#F+!y2R(qs1pUvpweCnsdsg3SE1XAv zz3w;3B#Pt$()=WSYj}5Ig=jwa;THmbVxM-7$3Lrl>1-@6xfoSq+GPDy;<6*Sg41H^&nk06#dvaaB)_>ft z46QLeZrR`^Doz=4IG#&Rmtu-Ct_{22*wv4^P(W$bZIoxC_w7|4;lmSnKBxKqARdiY zdDhFij&BvIi1~_5VR8}kMOZ_U-;Zi^{+-On+1WA+e&AGVh0zBp55=Jzvk!`KXt4YI zK5=x|*!WDRThcVg&kShUu%Sb{!Tk~1_Bvhnk(5qmjt%ulIgT|KHmjvF%rjmp0s^5?#QSzT5cd}ILF=2WVaR8fU^TZ91bom z*y#*<`i9xIZVP91ODYLE$G9q<*WB85d{)&2bOB&Uz*`d5B^oAf0-3q|%V+R;Af0k6 z2)P+jAUsS*9D_Sgmn3%hioRdQU^1&6`0!%Kt2>A9P}lP@m-y5+fN)Lvbc3@@^J&&{ z{KQ!MVq6!1d+-{#Iz7X5xIW^(a*0`>$s~9Yn~5PpcsM^a_S9Hyt*c&$Z>{;H+o%?^`RST+V|dSCX#*w3Akr0JL`{AO9v=Z)U&Jovq(oe2!u z61pWP@qkw)V_KqZK2*0~o2QjHGCde5uIJLv;d#QBoy(-(p==b9nUIn)6~?DAE$}?H zeSkA^sC7MCJR=}I{8{uw%}QUN0YK)~2w3MY41{bdl&0%OBWY{&9V=5UKEQ>~9{}n4{2q-g-T_$ic&3B9@c2o#-sR1y=likXg{ItKg^;acoSDo2Y{Ojv97{Z zK*h>%o`ULZf_^U6+QUC>y{mYa%A~;bKGyg;WYB;TEG(L> za_-gk;B-5Bc*IiD{4W>48HRX%@c^&&a!WPiJlJa9hV2S~5P+nUXp@Xr-I7u`jQkjN z>XcoM*GidfjXtzVOF3t2)8rwoUJ*3x^-bryw70r?ifSJ%`zUt&xij<3uJ_-)5oqVp zNZ)Va%Olb1_Uaw5uCbqS`GHG-T&W~Muf|A2v;-(acRJmM)z>&JYAsefMjPn-E>Nv8 zE}JdVlUi!DrgtMoqFR6YU6?VC#sN3SuJr7!yR*`KG2-6AsM#Xna=K`y6HPE7cYx2i zk$YfYu-$uhj(up{J}}TQfQ_rZM69l=DqwaUbGU~awSI?OWR{M=u9WYPp*l}8LD6{i z^$Te#FQ`n1MJA~nr%7YI_0;!_UJWs$R4vYB1dmMFAK?zgieBdWgx|b;1Q6-=&k7Di z!cOo8^DN$$VcUt5h#*j}BhUWa97fOJd`G}#yV@|*hgzYP@2^^_CA{%kp!lZb)F;h= z&j7M;a}E92ntB0$HilaYIbm*J`}XbpfHOvUx=G2}V!rR5d9%O=c@6^hgJHc}^AZ6a zfMIXO*%teH7RUdj{?)=|OY0I;`L#5nU)SzF>Y~|XSA15L`~&>ilX4>o3cJfjij@Q* zMkozkh_I@9-9zweqt%S`N%JAdId#+idw5gQ$CGkX!ya-D>K#vK%vH5SP^vC^ifPOP zeHy!~pTY>Y8XA*MotS=}_H7<7XBWV+WH~_zzPP0zX$zz$s6$vX=o++$8D3tz`@P;Xu)K`Io%JiHjAw;`?R-4hDRk?Z@SlSi_3G0K+NrXp;#qVHfY_?VrXvtA889oBUHdc=(YC_cf`RLNr7CXw3M zDu-b(kR&ORK3-l>n#E0e6#Z$p3r*8QG@z~VanWGEcar4H*>EX^8hF0D5yYo?mc7c{ zK?vM9;+B~um{?bCvV&GbvHdVk8fCAHxIr{>xWBs&iWc?+x4`Ak>0_EnA0|caR4iA{ zA%u5i`{uZz^y5kA8{Cf+^R7lHLiOe!r=NOP#Or~32g~GD!+f5%Vey58Afn+_zju*Djuf8(Ry0yXQkJt2hx&wI|eO z+Zci9B>NNjWamDVCxbCg(`kR7SEC=GGX}wx8MZCT__L@PurP3TRgTEcWF0P5;3i^! zI$!k)Es_tNbCr90Uc<^z+iA}ZM=TCcUTtcDN=uUq7nsvR*j=vc_L+K>-Rs3Zx5p&k zbHrOiFa-|kpJzf}t%c$g89_>r*@P=qRwz!JFFdwESo@o>h|U=_8%W*m@^E%smMedC zUFm3iPZ#|#7@6MVsz`2#pbt6c;+wVtHiSF5D=vT368CcU?P1q=QnYxXeCGk-F?3v@9JVRmisl<} z2($7b1zRW1;HPtP*Jv|we3sf{u~SJWf;)-wyg4#OwceJ9h6H4y8N%TtbNvEv zvopAt7i11pnGwH>(!S=KChs1(2Gh>kV6H+ zpIG${jBY%pl8Ep^2z;$sl5{u8Ott+vp5_%{sES3& z5<)2?z2OLF>e--bKONEcyu2%7b+_cuMz(B;Ugs-91ZW}xmfRkq)TdR;@@NFXeXgBLt} zf}ahCrapa2m12;(+T9tRF^vmy&f4_2>6G7{(5L3QiKp235_;inm2dyoFQJTa0k~Q% zzhgcjE@Feq6!q|n$&dhtgdKW)KRdO#@I?YssDZ4?jqVq;XI|t$-&YgE_kJ>~%$dAH z>PDUHt+`(Qhgvc@G5B7ix|YI;q;sWeiO(o4^7>N)`1x^(@FU6Rme$UTzE9Ht8!!X+ z4u*5L$svhld85@0F)xYUIXys?NOHn5r#b3;?6ixR5E$@!owwGL86+X}B_Ya!$h4uJX z)8$_gA=}26L%ZeUzK{YrZR4Nc6;dxJGM7f__}5KK6B*q^0WYDV`papIZRhi_#$~p4mPD+o2bt)FN-jNK*Tws>vl6tz43?w~-(7ym#%` z2m!4>As`GJm6X{?n=hX&)z%eoyW!403b66+1HFlI^Og)6Ty6jJjd{8n?3gVeoXUPM z$0}g4$$`VHS4>RSmh@vhZ9e|6qD69~#DpnJbhkui{YLOk0*z&@d;<@;BCE2wCfg(J zw}zL^diAF@?^C300lce+oTPmI*2XIYvYBEdr0z)!4KxA4E&f`~X61~w4_+bl482Kq z1W#8@>qplHpWSC7UmgD`w;Q}axz|60?HH#}fHM6|8m`fKw=s)Eljvn*!oqkrdT=SXq z@hU5}uL!t^{r-$^wN^RsRFX8zd3X6;H=Wu)o60WyY1Yv;b-c+Te$!7AU5r?e;KY7xzNxSN<~aER@@EKf+Zpu((F0MV>t#Hl?hh-BI>Q>#X# zJe%$%hIxrWNXfIkdzr{7KJhWsnohERrd?{VNi^bDE5I}XrK7-_Lua5UYEl8)W*XMu z%1kQ$J$pVrC%{nyF!M#KjWM3(_9{!3Yj+Bp38UU9J&|Kx1I*95df;s$KYu1VKdOz| zUj3x!`%4+%G8!S3U|Mo}>Q_3TKKj(EYX(L*Ly)))uLDYhhi`WquOJV#P!N{u-u9zc zy~N1?;?Rg)Ds{W+(t5Qf@Pnn|`i`LUU7bBmn(-!M%gjD8+S!!& z2#GdVB+PyBaTjN6kMYl`1+2}#ZZj@CZ;q2cWxLIPx(&a3|6_B!aUInX;q9mwVI}H- z*8vb)b33+ie2GJ6=`_VUlOkf-QIN53@v-^Q7V6D+)wJ9joGBjO9ZVvzFYOp{lVe-oo#4I8R-2%O1tTW?7LpszGfGm-t@}uaA2CaH# zCs*WZs?P28z2GsqA~lLqKV?EE8s95X_p3e8#uk_4NQX?d(EF*F^e+&W=^QT4-yPiz z_2gJ;iil<|w|D95{T`rlmDUoz%Z+g$(glBNoBb5LN?yelxIw7hKEF-BOiB4<-$D}H zGY-ri4(*l=u~(pOUt~U`&ZWTRh8*yJ0yiCQ)>>_^`29Q;fje)aW_C-s8uOCrFIf?wZ=fUr zeU8XO2n|WAsKfqO$B^efl=!_H`1lWYCt4%=N6fm?H1h|yMw;u}9Ts%6k74kueLu?t zx~x5mTz7KUYU6c@NW4hodgpf9n zg0hg~g(j$~6cRrrsg|gAYazEpgfo;r8&gD_BiQ8R()!mMy!Y#Q&1%ByY+efLhckW* zij?qkr2i4gQU(y#m6$WQWjG6MUXEDVE>;+vo#o3EqTD&)(AY7(4LtTxQxwkSJ6R^V z67O`1J9FJ^z!PkXs5k#Ht3mr6S%bAV4bws7@Nuiu-eG~Lk*js9)_bE%`1_EUp?xH@ zR{A?(OzL_^9426|UHF0FUKO@oByiM0C@8NeYP3lM@ zaoJR@5IWnd;|NULi%4~pEw5@ihS-H$e^le+nAf_}1WjMGDTz*sL0iF>X(^dW!{QuP zd3{>3Xku%GwYS-v$8qj8BnUIJd&$Gh79SI=A6_-C%|t7d$|@07MtShr@o%{?X*6k6 z@=r!M6GkiNOQ!@|lxALP0gQUGpz?EBMq$P;eOvwD*Y<^ERVXgo++A|d9D9$w7#fKx zlBB-A_6X(Czbuh6V$dp|Ddb1AoR>AEb&=V!w4s=&ftUth`lwQ^qwLgT<)QhbxzBBl zAkz)R+e^K~ZWaom5fOEb<%yPR43ha_CURv;3{1t4#8;l_uGG(bRjS2mwKZLGOva5` zrSnC4!)0azf&d#>d}=Muk}iRpRCLan(`W4EVo{`9YNb?^f{@CC1YX!)OBz)32(X%7ZBdox>#gF0sz2zic^}T* z@tvkt5lH_)q^i1-N5x(Ey@Nf1q><*r9GKV*v*9yE%OPG}0WivgR_|AsjRbhq z3aJD+P_8&%^L?uBNw?tD)lE7ecu>~7y1cxHzx6)>u?%B0^e5*nC~SHik(LWvQYAooq4-$rpY6%Y3?wF&+PWx}!ryG@1eu(PM6 z_Vu;8hp_w2RKU$$)9_M@bt~UZ&!}arXJ5iIu=@h&eK(VUyQ7@c{IqzGm~8|UpX2lT z^3;k^E*I@<#QinAMLBam#Z@3f0@i#JS~UL$%ABm~^#=o;-ES+i`?Wjs>9%i+XI-?O zMG#)NCH$1!dgeZ0WriqZs0zyFGG3Y`YI%q*T1puSic?S1$fp@H_!xcr4jCQ{lA0?? zEGKuce-=1DqtSoox+wV|p^GhAefaEW?cXcsaAT7RNqW9(0Uwsv-eoOZ3@Yo$r?c{| zVs*`xfY2Wp{T!B*p^}y1I94!Q6ZQC85WVI7l&^o#fOYA3Zi2N2DUo&5(VC1&ye@Pb z;12m+*GlHG>c<3hjDYq2a0rwau9Tn@lo<@e$qct%agCxTZxAHCb0 z%-2EZdq;Pd3GPTZOQ8D{Nk3FtWc*q7+B^G7ttPk7YEyY@!KXhLw%zSR7`x4uFTOf4 zjK{<$tZ6AMhFQZWrfquk{VJaBU>(Gl=-!wLMo~;->vUjVdeTCM!JBaac~p=VED}Cg z^ZwHNG|NDm%E}fY($i`fTCKyxbg|tm`|-}o2*w2g}Fd_Px6s6 zp?bBYE;enB|MX)D>@TdQIsqSnHA-wXfhzxjuhj<1N;GI1HJ?^33zNIZm+7jq`16>P z(Z6$3?uyBnn>CNsp>S>|eXs>hHayf%XN)puYAqseWVW_JGTh6q_}oQ1mp2}&`<@@F zr$w32qk+}wbs)44`yaQan9_jS{8M2K1`drMk^<@F%wXUtf#-|4MMlVQX%Z0Ek>w1N zQHaovhWX?4#7lFL^$)F&A8NJyDF{iM){iXYD(p`|RbKs0U5oOQ$SRb!!^{SO z*EI!ESg$ar4h7OI%y~{rn1t6xpk@8fwp30~tlFB^S|zJ;=CQ_pGRj`ze4G#d=qHVu z9z(W@j2pPsk>LCp{_;7HaJ@}Meo_&`oCA7Na;e&9twwokvueg_lzi_KzAPV6v$?rB zlt_f|Gm&ANW~d;D%|b<{*MK8Pa199RxOk5xA|H<`6?@&Cd#}_>P4+Ps8+|&D)CQ`4 zeOWCD)J0)Xyf)f`%Jb=5Gu?j(@Thpl)dkYyDd)<(t-0`s?Fl^Igv?kqn8)_~$`B{7 zV38R~IP-p}rv73;Qsyss|6s+CzEZRht8yDo^vB%&v0fct#y|(vj*8!G;(@aK=onF@ znoWDTD$iYIvYxOU@xa5`ia^FpgN8+=6gG@8__q@nbI z=y?#a?uv1=c^aE8)>~z-w%d7y70pq4OCe+pnifHh^L&22(IG1=+>!FC6ef7ea6LZn zBOefmKsv3+r7X6$UTm3Rc9AR}hF^UsYR{YvyJ(Z4*O$K8T(u6;vI(fg9_7e)HVJYF z8T=xZ%0_&1G~e)CQ)bjzo8i-8w=A$5O?k@Wyr$1|v$46f?ftj50gdX%?U-ER+MkcV z-dKgN=LCxOy+dXs?>u>XIVECB8H0_W2%Ui)jejo-v-njAz`K}w$6cGu*pt*QXm#6e9sBPBebWcC)$I|FzdTj= zqtrbRby`^zoCc$;T7_R#dh%;5YgbgT+IL05xLuaohMLuc6)rRtYMdRG*{c@oNWDgF znP1w8gB8KxvY#&fAWYqd`Bht%-dB`-8wDsG$x4!T=l19REF_lb?BEI6o%&qo^ zJF}17qO=3tWKYbbnqqWXDt(_r_EdgXsVPebnwGU|A3dZr?!?efAC5w(0()r7%e}gj zJZC@JN%wK%QBXnR=>x3pBO3*8?B$NfvSTwcj|)tRMx14OGK1}&tr4;ox_Eq^kjz+3 z&l>LffM5p-hyyieuy$>sMXw8_pTt<7tuFA}&nEL+SdL@eok9@dl22e4v8iRwD^RW8@K zyJ!B>XQkEaw`SFioYrfGmBGAvR`V4zO){mkJ@+{+jJ$a`OT=(*S5lOe<-6S59K?4p z`nFK0(i`2V#SH{kBnt~kPpYoEQRBr7R=-HGXBwPsrYMa6;&ZO`4)+$49jPa0%lh$W z=MP-G*X{X%?DX37^S3p!)pmG>!MY3?DAv6}uZL#j9L@OG14e#JSc?`fWfoGuG5H)t z;ZsK)TF&}Z@LP*j!9J0OzNW!?j;0T=8zaCy_{{&gyukbx2=Ko^Y)@8Vr#q}w_E)O& z6I7FbhBR9S1~uOWku_*Tus-g5Q-r?aF_y9%fMmRRjk{&7#o16I=b%YTL_-kqcyDWHd-?pp@fR87X1O@R|%2o zx@&`L9f}O{6E`dFnRS+ndm_$M<4evh&M3PAo|P0G4yn^rJ@WW+wemdlB0uqEGB9n+ zKD^YL0Yl)AX(vA>it;WMi(Zx1&9op+2!~1t&JKs)?uY}ySPiD?Td&|u@298~GD_@c z9K84GS6^E!>+=-5^S1dkZy%E9-`eJBgM2_)+o2~G<<^a(mrf?Z0!$02&q-~MC-DCI zw0`MJWd%^gQ=OIe&DN0!WWybgYugpKt*3(o>MPx4%rob1z#0onv^}(BNJYqH%8*>6 z>2rf<-eNH+i_y^RB+VR*9444)66yPdJM@{$zQvv}tk zN9pNrVs(Wa9}lQG6>+4pugw*zv^ zdD4(`mo!I{vpBW$n;lJRpkME`<>vC(8ZzXrTQuutJY}yaW<;OQ)>KSmhb_N-5d7@d zTjSjeDhAd2uj=oP#O0To^yH;d7mL@vp3cp5)k&mJsIRqI?$B;H6{5{mcF~fk^sW9F z3yoQ?-C+K%4k^eH2)QzLa~;{-rbcGSs&zD3W&j+;r~#Pd?MHO*K$*A;Dsx|b#Qgzl zaJ^TqYKwd$d@uHk1s%GEbr^=cVoAg<}PP6d-=XAczVns{YqfK(vQGK5Ua>R$ewE8^9K^L~Y z*RCfCo6?N!#vLt%{61^#ho3dr^}rC5{khDy#f=K5C+DM;KIUbkGc}G2HZKKc?;#7K zsfG);&(CrJwr_?o8OeU0dB`LlB%L>E&PX_XZ<%J+?781WxDwiwg|mDA?PNM^(4(@F zqg-pKoO6Azgp|>IMh1sTFO3n`$#IuCL3lwwUT^htJ7tV4%mjsS;)Sqne-!Uzo7Mf{ItRN={G9CH4XuWsHVErV&IFyX5GXSB~Pi(;uG4~BaB8RS$1_nU%Na350t z+z1vf-llyl-kqs8><`;(b+k}>IN{ho_2B{m$<2x!P~jV+i(B>W@sGQz-NlvZcM2QWsa9mTep3VCA>B*CY`)n+O!m8fxim=25oRmZ2P+(|Gu*E|suH;` z+H7JDEeY2XMheEYkH0-3c7$QK)mFx`NEkUb-)@Qvv>RU&o;H=Lm(nw(aU$G>&*L4V z^>H>B5*DgwPq*sz-5;-&*RH-T^DR%-zV` zLj5-%0t0t;_O<{5q;67RoZv_x4Roc$kyFU;MH>yU%EEh!kh^^O*`yZKc`0}ta6g>m z5?HJg8p&kH5c7VGHQNg}rzv30tjg?CmBSjh^SUHF0!e~$EUQ+mz0Pa97ba<#1pJU$ z{?IQL@cIfY>YSh^vsKhLCCt@f+ws^;_~SV?smt=}F?wKFecQV}Zj6n;9LV;UWb;>W zUnHenXAZs#J7DEumXV6E|9DA&%j141r~VVH*X|O}_qd|nfL&4l$|gM9^Ni^BIJ)?J zzEyuHdi%M-WpUJQmwipJM;EusJUH+p?CrYSJ$vTf@rKt%j91a%=|!b?!*ahgSE*i~ z`9$_Q0Skdnj)BVv89!=(TnY+Dg`o{JP=6=7vtV&q!|&<| zbF%GooA_q}oKQ2*$}2+xKg`PUc7@V*fdUSa{b813Dr9DGq%jt4qf_$Xr!V)LqEQ= znH;?3ZFir^(=Ym6gF9yOLal28p`T=QPz`bmleecY0**HQ1ukQpsZ?G4GoWYysxzNz z$hazSB4T-#Nn&mc7j%2pRc8E(6xX=^sAxu{!0EpE`VJ^)EkbIxM#LXI_D8S89)H@+ z)7AkCP2%r$Dkd= zjI=Xp+ZfC>si{T}x|FZ_S??KLeFX3gm<(+aa=A%{VbL+h9xw~=!|u-ah|3(6oVM<| zU@9!j+m(40+2lTFydTc%LVi)^CGu>RhDr4m5+ep%i${D@Ils@w+=-v`nT^gp^N`0) zrXgt;<=2@4t5I`FAHg8(n8REh&c}qtn-RQbP`8a?&eS&HLZ!X+zFWL_=F`}&I&XEE zn)1~(S|d3z`Uq_4I&-~s+VG9n?_j8i7~e90Xw4FIlAO(ig4sx+t4~{;zR>xY`;b1% zW}0D@+uP^Mt-GAA(R_*~N{;O%Nsvw)$(MFj+&M2&jiC!_x8?_sxLUhmC=21+4x#<4Bo9gkUNmzgCYi))4Rz|l+^Gm}-&`Nh347my!-~%UAOh@}Bg0|Sx499IcKWB!!dAn_jXL?K?*q%_JT-w5 z@cv9~Ko<-?4ApPl8-5xtiGHK-v`_<}Ri*x-H;qF>XU*2%Wn!FYk-&1g3s;gV`6xLTj?t5@??82ljbjqk<+ zYn*PiiqX_1CJuIxGlD-|wt%1ZJ@X~5q^YB?cJE1OhMG>FsyJRHRiU7|w0GXj&)h|w zLL#r9Q`yw#E7lDs5`k-h#Q;<;4YybsX5D1h2n%WqsOani{niF*jXKp1?W+-vJ&~D1 ze{DA@G*!-X;_TLf##0GNO zD_6f_r2r!c*qkRuGb9^($YftlRF+Oq zrv?7(VcNH3HJpFu2LJlfeDf3x>5r|nEX(xCY@PlB{;@?R0Ud_;IrE+LZTO?S_(!ZR zO-YYe(8;t)I)a97MxVi#KH0%_6B!>pvsNy;de0xizt09HyK?a0+qJ-~ z*P2y+`B^n=v!Latc2#QIW@McPapzxP6Uo5DzP5aRuSCv(xK+r7-C-o~q{F1m0Z%lN z(TudS22UKf8x;Mvi3XM_#=kFr6cl2z3hjECFO&FIQ^uvWLj)y(w}#8_O??A51&PmH zfYOuU`_`MIWZMqZ$20bD)Ra{kzzY%q?)K6OG9A8bqF+oJI0kZQ=%S(O;hhO@7~wMG zKvzN**L@U?1nPsJ$CS1Jw?@DxG4Ol96-)j4qysk5SDQ$zYB6QxNv_nOv0AX^04gCr z>!`rlGP$w@57{zQu6Z<+Z^{I7_jJn6?3o}eIy~zx7iv73b)l>3yNCln%A|C85#}2_ zZ;0Ou%G@HEw5hnad5lZ&`goMB*sVe}?+^G1Y-<@sGAP|z4LHvv3a|oV9c+6+GCLgO zm_*0+lgLyISaDUvF7!U7s!{CH%4SCvrOa)Dg<=n*y4+Qb3K0;;J8#s9q(j%b9{|i0 zTzEyVu3nBZc^fwBA?2=SsqG(%KdN4V`P=^-x5iy2M;-e4NfXk`^f>w%qgy3En;Qxn2Mq8dK*SX}N=d67 z%y-#zd9>Ew_iC-yHCy({EbTZ8mqf~@>7=BJM0mjaB7knD-O4O-QAKgy_NRH7)I#M= zhk@Qor|p!74gLd(X0=1T(p#cgx9(`Zr!pNcB@(~oeL&j#9fb4_2}sPG#hDC*RTArs8T^SPhD3q12sGw50R$kC8O=a1V%GWdMJ-nzQ(Jg*@5Y`Q|i zyS(?KJbZIkc67+u?%8uoa3DW3#1 zV4edU>~G&6rp8n1T|gIX1V3t1=+OLSk&T)KtM|{F%OfNft1pFm9wSfO$yZM0GT6 zJkas8C7;P}?}y=C*!eX(c+^9ys@}7-Z*62vH~=1ncD2=oZQ0@${;Y<{;zwC=J-(Bs z`eY%XJ(DTvAia%qf~N64`G>}i1s&Io_n&=r=lnhAi@j&)gWdHfMI_2fajvD)rTp&( zJ+OfQxV?Jhk?9N2eIm>C;(*(ABVbMM?#(k19I{`+fF}g3697e(53)*~T8&@wCcO(~ z7a#DMd&-3nf@+4*!H-qCWZIDF65?5S+*(vYw_4dOwolof97W1j-vw+lShEO#{-5ul z#N!<)XO?gTezd-Ut7MLHg8kNnK_;83N6=HrL0ZF=(&V4df%x)Mjmi3ire!Rjxm&Fk z^tS!+nB7Mc=;)j+)jpnm>79#TdTjToTL^7cGw$xet`Sb$2K4pV)ePSP4UY)|mU0A) z6_s-~dGyi2$jwr}Sag_G3dcD|u3lNXhdI_*CZ11gGG!8Q6q%G&s+XN>DB;ke8DTdj z15OXyE;9yWU}^hIwQ{=_)%^owWl~8lY|?PmR%WL`vzZPL$B*XMHiIT}NLF{RkuC8V z3~5C+4T<6jS%Z`h5c0sw1^y{@&2#l8?~7Hdb!sY+W&_NO3VkmEZcyi4x3LEdz3!v< zVWI^#bn@A_8mER)>LaOcYOt;3l=>EsX}AjY@df%cWLah%4GOg$dlDVrPV`Pw&FY$A zTU3OZ`1nzQ9V%UC(T_Qq^B}|V+BW!J%|YQ7q*XRSp4a8tuBlS5xPmvXn^l5wgew&i zf;YTfAO1+y5@QnGbp*CVQE&5z2ae9tFO?7+bJ4J;yuSSl{d7Qset5=vng%3E%~u-r z@t^V%SoF!@l{OTmNMnS4({y)v$^Ok;eV~F$p1iRdn~VLf$4OHD8TzF{8_>YI7i4vr zU+p~wehQr_n8bMSM<_#EZnVY*T4ANnfQtCcJ1LTPrAAGmfjtxY7QsC!x}M8SoYEe& zK9QbUYSdaVu8UGq2`xwg)0l{YZxT#_)C>EenbTYS==-&@;5+uGoBAFWsR{ONsV zu%Z)DjxJPw$_cu=L~E3xIocSMb>BGx};VJU@}(2N#qj24aOA#gKApZT5HPn7Tj4xuu|;*wxYP5}&< zT+Il8O=mFhdGv5DhC<1SbHsd~n{OC+F0jU+MWBGtlm%w`>A*}Cl>z$#XWUzE4N!vm z{&?0Y%Z3=6a0vm`yUB*uS+9z<$5KAZ9v7_Dl?+-Uonisx{2YP|<64H()Qq%Dp$xcO z(8v|htCEA)<5%tZ&FN(>#IzmEL{qB|M}0_DFjQ3D#j&Px2gOu&Ip3yAY76O?&yc99 z*jy-ddFrtqR^F`WY$t2^jqb{@oMe5o%y}=nZyaHO%weIU&hO&RRPlJ>q%NcN5J#b? zhuTzBdfUvjP$T}Tn}rFEwcv4Ewzd2@=~-`+-b%V+Ci~LcQmRuRvrO%88jJK3Mwb|+ zn`$+z{ZTTbt~gv3+}BY;nXc_{x

I2yg+KVqnuaEGT=kJw?py?&&Mb1iGhBegqRw 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 SRFsba4^34)6}0l>uzvtHRIX$I diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index 41fedfafd1..b5fa003134 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -96,10 +96,30 @@ Copy this public URL (e.g., `https://abc123.ngrok.io`). You will use it in Twili With your public URL set up, you’re ready to configure Twilio to send requests to your server! -#### Twilio setup +#### **Twilio Setup** + +To connect Twilio with your RealtimeAgent, follow these steps: + +1. **Create a Twilio Account** + If you don’t already have a Twilio account, you can [sign up here](https://login.twilio.com/u/signup). Twilio offers trial accounts, which are perfect for testing purposes. + +2. **Access Your Voice-Enabled Number** + Log in to the **Twilio Console** and select your **Voice-enabled phone number**. + +3. **Configure the Webhook** + - Navigate to the **Voice & Fax** section under your number’s settings. + - Set the **A CALL COMES IN** webhook to your public **ngrok** URL. + - Append `/incoming-call` to the URL. For example: + - If your ngrok URL is `https://abc123.ngrok.app`, the webhook URL should be: + `https://abc123.ngrok.app/incoming-call` + +4. **Save Changes** + Once the webhook URL is set, save your changes. + +You’re now ready to test the integration between Twilio and your RealtimeAgent! ### **Swarm Implementation for Airline Customer Service** -Here, we will build and configure a Swarm to manage common airline customer service tasks, such as booking changes, seat upgrades, or handling complaints. +Here, we will build and configure a Swarm to manage airline customer service tasks, such as flight changes and cancellations. ### **Connecting the Swarm to the RealtimeAgent** @@ -209,8 +229,61 @@ async def handle_media_stream(websocket: WebSocket): ``` ### **Results: Running the Service** -Finally, we’ll test the implementation, showcasing how real-time interactions with **RealtimeAgent** and task delegation to a Swarm create an efficient and engaging customer service solution. +With everything set up, it’s time to put your RealtimeAgent to the test! Follow these steps to make your first call and interact with the AI system: + +1. **Ensure Everything is Running** + - Verify that your **ngrok** session is still active and providing a public URL. + - Confirm that your FastAPI server is up and running, as outlined in the previous chapters. + +2. **Place a Call** + - Use a cell phone or landline to call your **Twilio number**. + +3. **Watch the Magic Happen** + - Start speaking! You’ll hear the AI system’s initial message and then be able to interact with it in real-time. + +#### **Realtime Agent and Swarm Workflow in Action** + +The following images showcase the seamless interaction between the **RealtimeAgent** and the Swarm of agents as they work together to handle a live customer request. Here's how the process unfolds: + +1. **Service Initialization** + Our service starts successfully, ready to handle incoming calls and process real-time interactions. +![Realtime Agent Swarm](img/1_service_running.png) + +2. **Incoming Call** + A call comes in, and the **RealtimeAgent** greets us with an audio prompt: + *“What do you need assistance with today?”* +![Realtime Agent Swarm](img/2_incoming_call.png) + +3. **Request Relay to Swarm** + We respond via audio, requesting to cancel our flight. The **RealtimeAgent** processes this request and relays it to the Swarm team for further action. +![Realtime Agent Swarm](img/3_request_for_flight_cancellation.png) + +4. **Clarification from Swarm** + The Swarm requires additional information, asking for the flight number we want to cancel. The **RealtimeAgent** gathers this detail from us and passes it back to the Swarm. +![Realtime Agent Swarm](img/4_flight_number_name.png) + +5. **Policy Confirmation** + The Swarm then queries us about the refund policy preference (e.g., refund vs. flight credits). The **RealtimeAgent** conveys this question, and after receiving our preference (flight credits), it informs the Swarm. +![Realtime Agent Swarm](img/5_refund_policy.png) + +6. **Successful Resolution** + The Swarm successfully processes the cancellation and initiates the refund. The **RealtimeAgent** communicates this resolution to us over audio: + *“Your refund has been successfully initiated.”* +![Realtime Agent Swarm](img/6_flight_refunded.png) + +This flow highlights the power of integrating real-time audio interaction with the collaborative capabilities of a Swarm of AI agents, providing efficient and user-friendly solutions to complex tasks. + +### **Caveats and Future Improvements** + +While the RealtimeAgent and Swarm integration is a powerful tool, there are a few things to keep in mind as we continue to refine the system: + +- **Work in Progress**: The agent is still evolving, and we’re actively polishing its details for a smoother experience in the coming weeks. +- **Transcription Challenges**: Occasionally, the agent may mishear inputs, particularly when dictating complex information like flight numbers or letters. +- **Response Misdirection**: At times, the agent might respond directly instead of relaying information to the customer. We’re addressing this with prompt optimizations. +- **Simpler Setup Coming Soon**: Setting up Twilio can be time-consuming, but we’re developing a **LocalAdapter** that will let you interact with the agent directly via your web browser audio—perfect for quick testing without the long setup. + +We’re excited about what’s to come and look forward to your feedback as we refine and expand these capabilities! For more updates, tutorials, and discussions, join our [Discord community](https://discord.com/invite/pAbnFJrkgZ). From 147f171f588c22be9738bccba96546b523ad79ca Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 19 Dec 2024 11:41:20 +0000 Subject: [PATCH 22/51] Prepare blogpost --- notebook/agentchat_realtime_swarm.ipynb | 9 --- .../blog/2024-12-18-RealtimeAgent/index.mdx | 65 ++++++++++++++++++- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 0351ab22e4..64af9c151c 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -346,15 +346,6 @@ " agent.register_hand_off(ON_CONDITION(triage_agent, transfer_to_triage_description))" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Expose your local service to the internet using ngrok\n", - "\n", - "..." - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index b5fa003134..f5d7436155 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -119,7 +119,70 @@ To connect Twilio with your RealtimeAgent, follow these steps: You’re now ready to test the integration between Twilio and your RealtimeAgent! ### **Swarm Implementation for Airline Customer Service** -Here, we will build and configure a Swarm to manage airline customer service tasks, such as flight changes and cancellations. +In this section, we’ll configure a Swarm to handle airline customer service tasks, such as flight changes and cancellations. This implementation builds upon the [original Swarm example notebook](/docs/notebooks/agentchat_swarm), which we’ve adapted to work seamlessly with the RealtimeAgent acting as a `UserProxyAgent`. + +You can explore and run the complete implementation of the RealtimeAgent demonstrated here by visiting [this notebook](/docs/notebooks/agentchat_realtime_swarm). + +For the sake of brevity, we’ll focus on the key sections of the Swarm setup in this blog post, highlighting the essential components. + +Below are the key parts of the Swarm setup, accompanied by concise comments to clarify their purpose and functionality. + +#### **Policy Definitions** +```python +FLIGHT_CANCELLATION_POLICY = """ +1. Confirm which flight the customer is asking to cancel. +2. Confirm refund or flight credits and proceed accordingly. +... +""" +``` +- **Purpose:** Defines the detailed step-by-step process for specific tasks like flight cancellations. +- **Usage:** Used as part of the agent's `system_message` to guide its behavior. + +#### **Agents Definition** +```python +triage_agent = SwarmAgent( + name="Triage_Agent", + system_message=triage_instructions(context_variables=context_variables), + llm_config=llm_config, + functions=[non_flight_enquiry], +) +``` +- **Triage Agent:** Routes the user's request to the appropriate specialized agent based on the topic. + +```python +flight_cancel = SwarmAgent( + name="Flight_Cancel_Traversal", + system_message=STARTER_PROMPT + FLIGHT_CANCELLATION_POLICY, + llm_config=llm_config, + functions=[initiate_refund, initiate_flight_credits, case_resolved, escalate_to_agent], +) +``` +- **Flight Cancel Agent:** Handles cancellations, including refunds and flight credits, while ensuring policy steps are strictly followed. + +```python +flight_modification.register_hand_off( + [ + ON_CONDITION(flight_cancel, "To cancel a flight"), + ON_CONDITION(flight_change, "To change a flight"), + ] +) +``` +- **Nested Handoffs:** Further refines routing, enabling deeper task-specific flows like cancellations or changes. + +#### **Utility Functions** +```python +def escalate_to_agent(reason: str = None) -> str: + """Escalates the interaction to a human agent if required.""" + return f"Escalating to agent: {reason}" if reason else "Escalating to agent" +``` +- **Purpose:** Ensures seamless fallback to human agents when automated handling is insufficient. + +```python +def initiate_refund() -> str: + """Handles initiating a refund process.""" + return "Refund initiated" +``` +- **Task-Specific:** Simplifies complex actions into modular, reusable functions. ### **Connecting the Swarm to the RealtimeAgent** From ff2f47abcc6245e303d54b9e05511cad680e9918 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak <117077296+sternakt@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:08:47 +0100 Subject: [PATCH 23/51] Update autogen/agentchat/realtime_agent/realtime_agent.py Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com> --- autogen/agentchat/realtime_agent/realtime_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index b497298a5f..691bd0c517 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -37,7 +37,7 @@ QUESTION_ROLE = "user" QUESTION_MESSAGE = ( - "I have a question/information for the myself. DO NOT ANSWER YOURSELF, GET THE ANSWER FROM ME. " + "I have a question/information for myself. DO NOT ANSWER YOURSELF, GET THE ANSWER FROM ME. " "repeat the question to me **WITH AUDIO OUTPUT** and then call 'answer_task_question' AFTER YOU GET THE ANSWER FROM ME\n\n" "The question is: '{}'\n\n" ) From e2153fb7b9fed272a742c58f7f89e829bb885ae6 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak <117077296+sternakt@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:15:48 +0100 Subject: [PATCH 24/51] Update website/blog/2024-12-18-RealtimeAgent/index.mdx Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com> --- website/blog/2024-12-18-RealtimeAgent/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index f5d7436155..6336e7279a 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -14,7 +14,7 @@ tags: [Realtime API, Voice Agents, Swarm Teams, Twilio, AI Tools] **TL;DR:** - **RealtimeAgent** is coming in the next AG2 release, enabling real-time conversational AI. - Features include real-time voice interactions, seamless task delegation to Swarm teams, and Twilio-based telephony integration. -- Learn how to integrate twillio and RealtimeAgent into your swarm in this blogpost +- Learn how to integrate twillio and RealtimeAgent into your swarm in this blogpost. ### **Realtime API Support: What's New?** From f04deb2b646c0a3ce03384acc25fab9b5ac5138f Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak <117077296+sternakt@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:16:03 +0100 Subject: [PATCH 25/51] Update website/blog/2024-12-18-RealtimeAgent/index.mdx Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com> --- website/blog/2024-12-18-RealtimeAgent/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index 6336e7279a..6660d74975 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -18,7 +18,7 @@ tags: [Realtime API, Voice Agents, Swarm Teams, Twilio, AI Tools] ### **Realtime API Support: What's New?** -We're thrilled to announce the release of **RealtimeAgent**, extending AG2's capabilities to support **real-time conversational AI tasks**. This new feature makes it possible for developers to build agents capable of handling voice-based interactions with minimal latency, integrating OpenAI’s Realtime API, Twilio for telephony, and AutoGen’s Swarm task orchestration. +We're thrilled to announce the release of **RealtimeAgent**, extending AG2's capabilities to support **real-time conversational AI tasks**. This new experimental feature makes it possible for developers to build agents capable of handling voice-based interactions with minimal latency, integrating OpenAI’s Realtime API, Twilio for telephony, and AG2’s Swarm orchestration. ### **Why Realtime API Support Matters** From 6e4ed4f4ec39d9839f3c5860eff0fe3453248723 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak <117077296+sternakt@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:16:15 +0100 Subject: [PATCH 26/51] Update website/blog/2024-12-18-RealtimeAgent/index.mdx Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com> --- website/blog/2024-12-18-RealtimeAgent/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index 6660d74975..2eb46c838d 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -28,7 +28,7 @@ Traditionally, conversational AI tasks have focused on asynchronous interactions Engage in real-time conversations with users through seamless integration with Twilio telephony. 2. **Leverage Swarm Teams for Task Delegation** - Delegate complex tasks to AutoGen Swarm teams during a voice interaction, ensuring efficient task management. + Delegate complex tasks to AG2 Swarm teams during a voice interaction, ensuring efficient task management. 3. **Provide Developer-Friendly Integration** Tutorial and streamlined API to make setting up real-time agents more straightforward for developers. From da27ec89bb0f175cb32e2da8b7cf92ca8b9c4739 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak <117077296+sternakt@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:17:17 +0100 Subject: [PATCH 27/51] Update website/blog/2024-12-18-RealtimeAgent/index.mdx Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com> --- website/blog/2024-12-18-RealtimeAgent/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index 2eb46c838d..f784de239e 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -208,7 +208,7 @@ async def handle_incoming_call(request: Request): return HTMLResponse(content=str(response), media_type="application/xml") ``` 2. **WebSocket Media Stream** - A WebSocket endpoint, `/media-stream`, is established to manage real-time audio communication between Twilio and OpenAI's API. This allows audio data to flow seamlessly, enabling the RealtimeAgent to process and respond to user queries. + A WebSocket endpoint, `/media-stream`, is established to manage real-time audio communication between Twilio and a realtime model inference client such as OpenAI's realtime API. This allows audio data to flow seamlessly, enabling the RealtimeAgent to process and respond to user queries. ```python @app.websocket("/media-stream") async def handle_media_stream(websocket: WebSocket): From 3c5eb4d2d2f0ce67054ebc9d893d5d44c9201c89 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak <117077296+sternakt@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:18:31 +0100 Subject: [PATCH 28/51] Update website/blog/2024-12-18-RealtimeAgent/index.mdx Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com> --- website/blog/2024-12-18-RealtimeAgent/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index f784de239e..e0c1761514 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -272,7 +272,7 @@ async def handle_incoming_call(request: Request): @app.websocket("/media-stream") async def handle_media_stream(websocket: WebSocket): - """Handle WebSocket connections between Twilio and OpenAI.""" + """Handle WebSocket connections between Twilio and realtime model inference client.""" await websocket.accept() audio_adapter = TwilioAudioAdapter(websocket) From 1ac6dbc0f50a67a822c474da4fabfc8a187c6fe0 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak <117077296+sternakt@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:19:41 +0100 Subject: [PATCH 29/51] Update notebook/agentchat_realtime_swarm.ipynb Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com> --- notebook/agentchat_realtime_swarm.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 64af9c151c..b628858d76 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -7,7 +7,7 @@ "# Swarm Orchestration with AG2 and RealtimeAgent\n", "\n", "\n", - "AG2 supports **RealtimeAgent**, a powerful agent type that connects seamlessly to OpenAI's [Realtime API](https://openai.com/index/introducing-the-realtime-api). With RealtimeAgent, you can add voice interaction and listening capabilities to your swarms, enabling dynamic and natural communication.\n", + "AG2 supports **RealtimeAgent**, a powerful agent type that connects seamlessly to realtime clients such as [OpenAI's Realtime API](https://openai.com/index/introducing-the-realtime-api). With RealtimeAgent, you can add voice interaction and listening capabilities to your swarms, enabling dynamic and natural communication.\n", "\n", "AG2 provides an intuitive programming interface to build and orchestrate swarms of agents. With RealtimeAgent, you can enhance swarm functionality, integrating real-time interactions alongside task automation. Check the [Documentation](https://ag2ai.github.io/ag2/docs/topics/swarm) and [Blog](https://ag2ai.github.io/ag2/blog/2024/11/17/Swarm) for further insights.\n", "\n", From 4b6229e48f0faf8c7a5261f68c9307604c7cd068 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak <117077296+sternakt@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:20:05 +0100 Subject: [PATCH 30/51] Update website/blog/2024-12-18-RealtimeAgent/index.mdx Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com> --- website/blog/2024-12-18-RealtimeAgent/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index e0c1761514..94538b3c44 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -212,7 +212,7 @@ async def handle_incoming_call(request: Request): ```python @app.websocket("/media-stream") async def handle_media_stream(websocket: WebSocket): - """Handle WebSocket connections between Twilio and OpenAI.""" + """Handle WebSocket connections between Twilio and realtime model inference client.""" await websocket.accept() ... ``` From 71a1aa6dba265d0970c673e27676642bd9f8ef61 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak <117077296+sternakt@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:20:40 +0100 Subject: [PATCH 31/51] Update website/blog/2024-12-18-RealtimeAgent/index.mdx Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com> --- website/blog/2024-12-18-RealtimeAgent/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index 94538b3c44..b387d0e1d5 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -219,7 +219,7 @@ async def handle_media_stream(websocket: WebSocket): 3. **Initializing the RealtimeAgent** Inside the WebSocket handler, we instantiate the **RealtimeAgent** with the following components: - **Name**: The identifier for the agent (e.g., `Customer_service_Bot`). - - **LLM Configuration**: The configuration for the underlying OpenAI model that powers the agent. + - **LLM Configuration**: The configuration for the underlying realtime model inference client that powers the agent. - **Audio Adapter**: A TwilioAudioAdapter is used to handle audio streaming between Twilio and the agent. ```python audio_adapter = TwilioAudioAdapter(websocket) From 425218c4b3be7771f4b310404687585dd23d7113 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 19 Dec 2024 13:27:53 +0000 Subject: [PATCH 32/51] Revise agentchat_realtime_swarm.ipynb --- notebook/agentchat_realtime_swarm.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index b628858d76..c57ae5ab2d 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -93,7 +93,6 @@ "\n", "realtime_llm_config = {\n", " \"timeout\": 600,\n", - " \"cache_seed\": 42, # change the seed for different trials\n", " \"config_list\": realtime_config_list,\n", " \"temperature\": 0.8,\n", "}\n", @@ -438,11 +437,12 @@ ], "metadata": { "front_matter": { - "description": "Swarm Ochestration", + "description": "Realtime Swarm Orchestration with AG2 and RealtimeAgent", "tags": [ "orchestration", "group chat", - "swarm" + "swarm", + "realtime" ] }, "kernelspec": { From 1cc619ac5ba5ee218c60ae28e79aa46bb5ae6ae6 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 19 Dec 2024 13:44:27 +0000 Subject: [PATCH 33/51] Refactor RealtimeAgent.ask_question --- .../realtime_agent/realtime_agent.py | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 691bd0c517..60bae22f63 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -5,7 +5,6 @@ # Portions derived from https://github.com/microsoft/autogen are under the MIT License. # SPDX-License-Identifier: MIT -# import asyncio import asyncio import json import logging @@ -41,6 +40,7 @@ "repeat the question to me **WITH AUDIO OUTPUT** and then call 'answer_task_question' AFTER YOU GET THE ANSWER FROM ME\n\n" "The question is: '{}'\n\n" ) +QUESTION_TIMEOUT_SECONDS = 20 class RealtimeAgent(ConversableAgent): @@ -114,20 +114,6 @@ def register_swarm( self._initial_agent = initial_agent self._agents = agents - # def _get_task_status(task_id: str) -> Generator[None, str, None]: - # while True: - # for s in [ - # "The task is in progress, agents are working on it. ETA is 1 minute", - # "The task is successfully completed.", - # ]: - # yield s - - # it = _get_task_status("task_id") - - # @self.register_handover(name="get_task_status", description="Get the status of the task") - # async def get_task_status(task_id: str) -> str: - # return next(it) - self.register_handover(name="answer_task_question", description="Answer question from the task")( self.set_answer ) @@ -179,8 +165,16 @@ async def get_answer(self) -> str: return self._answer async def ask_question(self, question: str, question_timeout: int) -> str: + """ + Send a question for the user to the agent and wait for the answer. + If the answer is not received within the timeout, the question is repeated. + + Args: + question: The question to ask the user. + question_timeout: The time in seconds to wait for the answer. + """ + self.reset_answer() - await anyio.sleep(1) await self._client.send_text(role=QUESTION_ROLE, text=question) async def _check_event_set(timeout: int = question_timeout) -> None: @@ -201,7 +195,10 @@ def check_termination_and_human_reply( ) -> Tuple[bool, Union[str, None]]: async def get_input(): async with create_task_group() as tg: - tg.soonify(self.ask_question)(QUESTION_MESSAGE.format(messages[-1]["content"]), 20) + tg.soonify(self.ask_question)( + QUESTION_MESSAGE.format(messages[-1]["content"]), + question_timeout=QUESTION_TIMEOUT_SECONDS, + ) syncify(get_input)() From 4cab66e4f8e516398e8bcc426e183f9d691d10e7 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 19 Dec 2024 13:51:55 +0000 Subject: [PATCH 34/51] Remove RealtimeOpenAIClient mention from the blogpost --- website/blog/2024-12-18-RealtimeAgent/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index b387d0e1d5..7ebf8567b8 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -34,7 +34,7 @@ Traditionally, conversational AI tasks have focused on asynchronous interactions Tutorial and streamlined API to make setting up real-time agents more straightforward for developers. -### **Key Features of RealtimeAgent and RealtimeOpenAIClient** +### **Key Features of RealtimeAgent** #### **1. RealtimeAgent** - Acts as the central interface for handling real-time interactions. From acef7b45f3c2efa9d6f0063776b53d4abaa4fcf5 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 19 Dec 2024 15:09:35 +0000 Subject: [PATCH 35/51] Add docs --- autogen/agentchat/realtime_agent/client.py | 25 +++++- .../realtime_agent/function_observer.py | 22 ++++- .../realtime_agent/realtime_agent.py | 89 +++++++++++++------ .../realtime_agent/realtime_observer.py | 5 ++ .../realtime_agent/twilio_observer.py | 13 +++ notebook/agentchat_realtime_swarm.ipynb | 32 ++++++- notebook/agentchat_realtime_tool.ipynb | 2 +- 7 files changed, 149 insertions(+), 39 deletions(-) diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py index 86c65a3452..83e029d1d9 100644 --- a/autogen/agentchat/realtime_agent/client.py +++ b/autogen/agentchat/realtime_agent/client.py @@ -8,7 +8,6 @@ # import asyncio import json import logging -from abc import ABC, abstractmethod from typing import Any, Optional import anyio @@ -22,8 +21,20 @@ logger = logging.getLogger(__name__) -class Client(ABC): +class OpenAIRealtimeClient: + """(Experimental) Client for OpenAI Realtime API.""" + def __init__(self, agent, audio_adapter, function_observer: FunctionObserver): + """(Experimental) Client for OpenAI Realtime API. + + args: + agent: Agent instance + the agent to be used for the conversation + audio_adapter: RealtimeObserver + adapter for streaming the audio from the client + function_observer: FunctionObserver + observer for handling function calls + """ self._agent = agent self._observers = [] self._openai_ws = None # todo factor out to OpenAIClient @@ -32,6 +43,9 @@ def __init__(self, agent, audio_adapter, function_observer: FunctionObserver): # LLM config llm_config = self._agent.llm_config + + print("!" * 100) + print(llm_config) config = llm_config["config_list"][0] self.model = config["model"] @@ -42,14 +56,17 @@ def __init__(self, agent, audio_adapter, function_observer: FunctionObserver): self.tg: Optional[TaskGroup] = None def register(self, observer): + """Register an observer to the client.""" observer.register_client(self) self._observers.append(observer) async def notify_observers(self, message): + """Notify all observers of a message from the OpenAI Realtime API.""" for observer in self._observers: await observer.update(message) async def function_result(self, call_id, result): + """Send the result of a function call to the OpenAI Realtime API.""" result_item = { "type": "conversation.item.create", "item": { @@ -62,6 +79,7 @@ async def function_result(self, call_id, result): await self._openai_ws.send(json.dumps({"type": "response.create"})) async def send_text(self, *, role: str, text: str): + """Send a text message to the OpenAI Realtime API.""" await self._openai_ws.send(json.dumps({"type": "response.cancel"})) text_item = { "type": "conversation.item.create", @@ -85,12 +103,14 @@ async def initialize_session(self): # todo override in specific clients async def session_update(self, session_options): + """Send a session update to the OpenAI Realtime API.""" update = {"type": "session.update", "session": session_options} logger.info("Sending session update:", json.dumps(update)) await self._openai_ws.send(json.dumps(update)) logger.info("Sending session update finished") async def _read_from_client(self): + """Read messages from the OpenAI Realtime API.""" try: async for openai_message in self._openai_ws: response = json.loads(openai_message) @@ -99,6 +119,7 @@ async def _read_from_client(self): logger.warning(f"Error in _read_from_client: {e}") async def run(self): + """Run the client.""" async with websockets.connect( f"wss://api.openai.com/v1/realtime?model={self.model}", additional_headers={ diff --git a/autogen/agentchat/realtime_agent/function_observer.py b/autogen/agentchat/realtime_agent/function_observer.py index 777fbf993e..14c70bca62 100644 --- a/autogen/agentchat/realtime_agent/function_observer.py +++ b/autogen/agentchat/realtime_agent/function_observer.py @@ -18,11 +18,20 @@ class FunctionObserver(RealtimeObserver): + """Observer for handling function calls from the OpenAI Realtime API.""" + def __init__(self, agent): + """Observer for handling function calls from the OpenAI Realtime API. + + Args: + agent: Agent instance + the agent to be used for the conversation + """ super().__init__() self._agent = agent async def update(self, response): + """Handle function call events from the OpenAI Realtime API.""" if response.get("type") == "response.function_call_arguments.done": logger.info(f"Received event: {response['type']}", response) await self.call_function( @@ -30,8 +39,9 @@ async def update(self, response): ) async def call_function(self, call_id, name, kwargs): - if name in self._agent.registered_functions: - _, func = self._agent.registered_functions[name] + """Call a function registered with the agent.""" + if name in self._agent.realtime_functions: + _, func = self._agent.realtime_functions[name] func = func if asyncio.iscoroutinefunction(func) else asyncify(func) try: result = await func(**kwargs) @@ -47,12 +57,16 @@ async def call_function(self, call_id, name, kwargs): await self._client.function_result(call_id, result) async def run(self): + """Run the observer. + + Initialize the session with the OpenAI Realtime API. + """ await self.initialize_session() async def initialize_session(self): - """Add tool to OpenAI.""" + """Add registered tools to OpenAI with a session update.""" session_update = { - "tools": [schema for schema, _ in self._agent.registered_functions.values()], + "tools": [schema for schema, _ in self._agent.realtime_functions.values()], "tool_choice": "auto", } await self._client.session_update(session_update) diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index 60bae22f63..c0de704d44 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -20,7 +20,7 @@ from autogen.agentchat.conversable_agent import ConversableAgent from autogen.function_utils import get_function_schema -from .client import Client +from .client import OpenAIRealtimeClient from .function_observer import FunctionObserver from .realtime_observer import RealtimeObserver @@ -44,44 +44,50 @@ class RealtimeAgent(ConversableAgent): + """(Experimental) Agent for interacting with the Realtime Clients.""" + def __init__( self, *, name: str, audio_adapter: RealtimeObserver, system_message: Optional[Union[str, List]] = "You are a helpful AI Assistant.", - is_termination_msg: Optional[Callable[[Dict], bool]] = None, - max_consecutive_auto_reply: Optional[int] = None, - human_input_mode: Literal["ALWAYS", "NEVER", "TERMINATE"] = "ALWAYS", - function_map: Optional[Dict[str, Callable]] = None, - code_execution_config: Union[Dict, Literal[False]] = False, llm_config: Optional[Union[Dict, Literal[False]]] = None, - default_auto_reply: Union[str, Dict] = "", - description: Optional[str] = None, - chat_messages: Optional[Dict[Agent, List[Dict]]] = None, - silent: Optional[bool] = None, - context_variables: Optional[Dict[str, Any]] = None, voice: str = "alloy", ): + """(Experimental) Agent for interacting with the Realtime Clients. + + Args: + name: str + the name of the agent + audio_adapter: RealtimeObserver + adapter for streaming the audio from the client + system_message: str or list + the system message for the client + llm_config: dict or False + the config for the LLM + voice: str + the voice to be used for the agent + """ super().__init__( name=name, - is_termination_msg=is_termination_msg, - max_consecutive_auto_reply=max_consecutive_auto_reply, - human_input_mode=human_input_mode, - function_map=function_map, - code_execution_config=code_execution_config, - default_auto_reply=default_auto_reply, - llm_config=llm_config, - system_message=system_message, - description=description, - chat_messages=chat_messages, - silent=silent, - context_variables=context_variables, + is_termination_msg=None, + max_consecutive_auto_reply=None, + human_input_mode="ALWAYS", + function_map=None, + code_execution_config=False, + default_auto_reply="", + description=None, + chat_messages=None, + silent=None, + context_variables=None, ) - self._client = Client(self, audio_adapter, FunctionObserver(self)) + print("!" * 100) + print(llm_config) self.llm_config = llm_config + self._client = OpenAIRealtimeClient(self, audio_adapter, FunctionObserver(self)) self.voice = voice - self.registered_functions = {} + self.realtime_functions = {} self._oai_system_message = [{"content": system_message, "role": "system"}] # todo still needed? self.register_reply( @@ -101,6 +107,16 @@ def register_swarm( agents: List[SwarmAgent], system_message: Optional[str] = None, ) -> None: + """Register a swarm of agents with the Realtime Agent. + + Args: + initial_agent: SwarmAgent + the initial agent in the swarm + agents: list of SwarmAgent + the agents in the swarm + system_message: str + the system message for the client + """ if not system_message: if self.system_message != "You are a helpful AI Assistant.": logger.warning( @@ -114,14 +130,15 @@ def register_swarm( self._initial_agent = initial_agent self._agents = agents - self.register_handover(name="answer_task_question", description="Answer question from the task")( + self.register_realtime_function(name="answer_task_question", description="Answer question from the task")( self.set_answer ) async def run(self): + """Run the agent.""" await self._client.run() - def register_handover( + def register_realtime_function( self, *, description: str, @@ -146,21 +163,24 @@ def _decorator(func: F, name=name) -> F: schema = get_function_schema(func, name=name, description=description)["function"] schema["type"] = "function" - self.registered_functions[name] = (schema, func) + self.realtime_functions[name] = (schema, func) return func return _decorator def reset_answer(self) -> None: + """Reset the answer event.""" self._answer_event = anyio.Event() def set_answer(self, answer: str) -> str: + """Set the answer to the question.""" self._answer = answer self._answer_event.set() return "Answer set successfully." async def get_answer(self) -> str: + """Get the answer to the question.""" await self._answer_event.wait() return self._answer @@ -193,6 +213,19 @@ def check_termination_and_human_reply( sender: Optional[Agent] = None, config: Optional[Any] = None, ) -> Tuple[bool, Union[str, None]]: + """Check if the conversation should be terminated and if the agent should reply. + + Called when its agents turn in the chat conversation. + + Args: + messages: list of dict + the messages in the conversation + sender: Agent + the agent sending the message + config: any + the config for the agent + """ + async def get_input(): async with create_task_group() as tg: tg.soonify(self.ask_question)( diff --git a/autogen/agentchat/realtime_agent/realtime_observer.py b/autogen/agentchat/realtime_agent/realtime_observer.py index f1b6c88783..80d59de95c 100644 --- a/autogen/agentchat/realtime_agent/realtime_observer.py +++ b/autogen/agentchat/realtime_agent/realtime_observer.py @@ -9,16 +9,21 @@ class RealtimeObserver(ABC): + """Observer for the OpenAI Realtime API.""" + def __init__(self): self._client = None def register_client(self, client): + """Register a client with the observer.""" self._client = client @abstractmethod async def run(self, openai_ws): + """Run the observer.""" pass @abstractmethod async def update(self, message): + """Update the observer with a message from the OpenAI Realtime API.""" pass diff --git a/autogen/agentchat/realtime_agent/twilio_observer.py b/autogen/agentchat/realtime_agent/twilio_observer.py index 6364fa99be..4d6c793898 100644 --- a/autogen/agentchat/realtime_agent/twilio_observer.py +++ b/autogen/agentchat/realtime_agent/twilio_observer.py @@ -29,7 +29,15 @@ class TwilioAudioAdapter(RealtimeObserver): + """Adapter for streaming audio from Twilio to OpenAI Realtime API and vice versa.""" + def __init__(self, websocket): + """Adapter for streaming audio from Twilio to OpenAI Realtime API and vice versa. + + Args: + websocket: WebSocket + the websocket connection to the Twilio service + """ super().__init__() self.websocket = websocket @@ -97,12 +105,17 @@ async def handle_speech_started_event(self): self.response_start_timestamp_twilio = None async def send_mark(self): + """Send a mark of audio interruption to the Twilio websocket.""" if self.stream_sid: mark_event = {"event": "mark", "streamSid": self.stream_sid, "mark": {"name": "responsePart"}} await self.websocket.send_json(mark_event) self.mark_queue.append("responsePart") async def run(self): + """Run the adapter. + + Start reading messages from the Twilio websocket and send audio to OpenAI. + """ openai_ws = self._client._openai_ws await self.initialize_session() diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index c57ae5ab2d..18d2ab92b1 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -7,7 +7,7 @@ "# Swarm Orchestration with AG2 and RealtimeAgent\n", "\n", "\n", - "AG2 supports **RealtimeAgent**, a powerful agent type that connects seamlessly to realtime clients such as [OpenAI's Realtime API](https://openai.com/index/introducing-the-realtime-api). With RealtimeAgent, you can add voice interaction and listening capabilities to your swarms, enabling dynamic and natural communication.\n", + "AG2 supports **RealtimeAgent**, a powerful agent type that connects seamlessly to OpenAI's [Realtime API](https://openai.com/index/introducing-the-realtime-api). With RealtimeAgent, you can add voice interaction and listening capabilities to your swarms, enabling dynamic and natural communication.\n", "\n", "AG2 provides an intuitive programming interface to build and orchestrate swarms of agents. With RealtimeAgent, you can enhance swarm functionality, integrating real-time interactions alongside task automation. Check the [Documentation](https://ag2ai.github.io/ag2/docs/topics/swarm) and [Blog](https://ag2ai.github.io/ag2/blog/2024/11/17/Swarm) for further insights.\n", "\n", @@ -345,6 +345,15 @@ " agent.register_hand_off(ON_CONDITION(triage_agent, transfer_to_triage_description))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Expose your local service to the internet using ngrok\n", + "\n", + "..." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -433,16 +442,31 @@ "nest_asyncio.apply()\n", "uvicorn.run(app, host=\"0.0.0.0\", port=5050)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "realtime_llm_config" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "front_matter": { - "description": "Realtime Swarm Orchestration with AG2 and RealtimeAgent", + "description": "Swarm Ochestration", "tags": [ "orchestration", "group chat", - "swarm", - "realtime" + "swarm" ] }, "kernelspec": { diff --git a/notebook/agentchat_realtime_tool.ipynb b/notebook/agentchat_realtime_tool.ipynb index 5b16186660..77e7bea86c 100644 --- a/notebook/agentchat_realtime_tool.ipynb +++ b/notebook/agentchat_realtime_tool.ipynb @@ -117,7 +117,7 @@ " audio_adapter=audio_adapter,\n", " )\n", "\n", - " @openai_client.register_handover(name=\"get_weather\", description=\"Get the current weather\")\n", + " @openai_client.register_realtime_function(name=\"get_weather\", description=\"Get the current weather\")\n", " def get_weather(location: Annotated[str, \"city\"]) -> str:\n", " ...\n", " return \"The weather is cloudy.\" if location == \"Seattle\" else \"The weather is sunny.\"\n", From c193859b1aa47d80e363d3d8dc2fa5d21ca806a2 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 19 Dec 2024 15:21:43 +0000 Subject: [PATCH 36/51] Remove prints --- autogen/agentchat/realtime_agent/client.py | 2 -- autogen/agentchat/realtime_agent/realtime_agent.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/autogen/agentchat/realtime_agent/client.py b/autogen/agentchat/realtime_agent/client.py index 83e029d1d9..ac2ed1674a 100644 --- a/autogen/agentchat/realtime_agent/client.py +++ b/autogen/agentchat/realtime_agent/client.py @@ -44,8 +44,6 @@ def __init__(self, agent, audio_adapter, function_observer: FunctionObserver): # LLM config llm_config = self._agent.llm_config - print("!" * 100) - print(llm_config) config = llm_config["config_list"][0] self.model = config["model"] diff --git a/autogen/agentchat/realtime_agent/realtime_agent.py b/autogen/agentchat/realtime_agent/realtime_agent.py index c0de704d44..09dadab27e 100644 --- a/autogen/agentchat/realtime_agent/realtime_agent.py +++ b/autogen/agentchat/realtime_agent/realtime_agent.py @@ -82,8 +82,6 @@ def __init__( silent=None, context_variables=None, ) - print("!" * 100) - print(llm_config) self.llm_config = llm_config self._client = OpenAIRealtimeClient(self, audio_adapter, FunctionObserver(self)) self.voice = voice From f361341cf8dbacc5c2bfa7db3cf6a1591b0d30bf Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 19 Dec 2024 16:55:02 +0000 Subject: [PATCH 37/51] Blog polishing --- website/blog/2024-12-18-RealtimeAgent/index.mdx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/website/blog/2024-12-18-RealtimeAgent/index.mdx b/website/blog/2024-12-18-RealtimeAgent/index.mdx index 7ebf8567b8..d41f357bd9 100644 --- a/website/blog/2024-12-18-RealtimeAgent/index.mdx +++ b/website/blog/2024-12-18-RealtimeAgent/index.mdx @@ -12,9 +12,9 @@ tags: [Realtime API, Voice Agents, Swarm Teams, Twilio, AI Tools] ![Realtime Agent Swarm](img/realtime_agent_swarm.png) **TL;DR:** -- **RealtimeAgent** is coming in the next AG2 release, enabling real-time conversational AI. +- **RealtimeAgent** is coming in the AG2 0.6 release, enabling real-time conversational AI. - Features include real-time voice interactions, seamless task delegation to Swarm teams, and Twilio-based telephony integration. -- Learn how to integrate twillio and RealtimeAgent into your swarm in this blogpost. +- Learn how to integrate Twilio and RealtimeAgent into your swarm in this blogpost. ### **Realtime API Support: What's New?** @@ -25,7 +25,7 @@ We're thrilled to announce the release of **RealtimeAgent**, extending AG2's cap Traditionally, conversational AI tasks have focused on asynchronous interactions, such as text-based chats. However, the demand for **real-time voice agents** has surged in domains like customer support, healthcare, and virtual assistance. With this update, AG2 takes a leap forward by enabling agents to: 1. **Support Real-Time Voice Interactions** - Engage in real-time conversations with users through seamless integration with Twilio telephony. + Engage in real-time conversations with users. 2. **Leverage Swarm Teams for Task Delegation** Delegate complex tasks to AG2 Swarm teams during a voice interaction, ensuring efficient task management. @@ -38,9 +38,12 @@ Traditionally, conversational AI tasks have focused on asynchronous interactions #### **1. RealtimeAgent** - Acts as the central interface for handling real-time interactions. -- Bridges voice input/output with AutoGen’s task-handling capabilities. +- Bridges voice input/output with AG2’s task-handling capabilities. -#### **3. TwilioPhoneConnector** +#### **2. RealtimeAgent swarm integration** +- Seamless integration of RealtimeAgent into Swarm + +#### **3. TwilioAdapter** - Connects agents to Twilio for telephony support. - Simplifies the process of handling voice calls with clear API methods. From 21e0c680ca545e0b944f17dfdad37551415a9bf2 Mon Sep 17 00:00:00 2001 From: Mark Sze <66362098+marklysze@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:26:47 +1100 Subject: [PATCH 38/51] Update notebook title --- notebook/agentchat_realtime_swarm.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 06c90cc4a7..6f690b847d 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Swarm Orchestration with AG2 and RealtimeAgent\n", + "# RealtimeAgent in a Swarm Orchestration\n", "\n", "\n", "AG2 supports **RealtimeAgent**, a powerful agent type that connects seamlessly to OpenAI's [Realtime API](https://openai.com/index/introducing-the-realtime-api). With RealtimeAgent, you can add voice interaction and listening capabilities to your swarms, enabling dynamic and natural communication.\n", From 7290c06f11afd6bc227e4c311c709cd9e0afc3ab Mon Sep 17 00:00:00 2001 From: Mark Sze <66362098+marklysze@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:29:54 +1100 Subject: [PATCH 39/51] Twilio spelling corrected --- notebook/agentchat_realtime_swarm.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 6f690b847d..dc9365b82b 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -18,13 +18,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Install AG2 with realtime-twillio dependencies\n", + "## Install AG2 with realtime-twilio dependencies\n", "\n", - "To use the realtime agent we will connect it to twillio service, this tutorial was inspired by [twillio tutorial](https://www.twilio.com/en-us/blog/voice-ai-assistant-openai-realtime-api-node) for connecting to OpenAPI real-time agent.\n", + "To use the realtime agent we will connect it to twilio service, this tutorial was inspired by [twilio tutorial](https://www.twilio.com/en-us/blog/voice-ai-assistant-openai-realtime-api-node) for connecting to OpenAPI real-time agent.\n", "\n", - "We have prepared a `TwillioAdapter` to enable you to connect your realtime agent to twillio service.\n", + "We have prepared a `TwilioAdapter` to enable you to connect your realtime agent to twilio service.\n", "\n", - "To be able to run this notebook, you will need to install ag2 with additional realtime and twillio dependencies." + "To be able to run this notebook, you will need to install ag2 with additional realtime and twilio dependencies." ] }, { From 449ad8ca50731169d94f6640860d089e1e7f9adc Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Thu, 19 Dec 2024 20:46:29 +0000 Subject: [PATCH 40/51] Updated setups for ag2 and autogen packages --- setup_ag2.py | 1 + setup_autogen.py | 1 + 2 files changed, 2 insertions(+) diff --git a/setup_ag2.py b/setup_ag2.py index fdcdfd6e90..7107fc0dd6 100644 --- a/setup_ag2.py +++ b/setup_ag2.py @@ -54,6 +54,7 @@ "cohere": ["pyautogen[cohere]==" + __version__], "ollama": ["pyautogen[ollama]==" + __version__], "bedrock": ["pyautogen[bedrock]==" + __version__], + "twilio": ["pyautogen[twilio]==" + __version__], "neo4j": ["pyautogen[neo4j]==" + __version__], }, url="https://github.com/ag2ai/ag2", diff --git a/setup_autogen.py b/setup_autogen.py index ac65e29eec..16f1cfa3f9 100644 --- a/setup_autogen.py +++ b/setup_autogen.py @@ -54,6 +54,7 @@ "cohere": ["pyautogen[cohere]==" + __version__], "ollama": ["pyautogen[ollama]==" + __version__], "bedrock": ["pyautogen[bedrock]==" + __version__], + "twilio": ["pyautogen[twilio]==" + __version__], "neo4j": ["pyautogen[neo4j]==" + __version__], }, url="https://github.com/ag2ai/ag2", From ea01e7123d79eb2358f821e5fccd94843833419d Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Thu, 19 Dec 2024 21:11:06 +0000 Subject: [PATCH 41/51] Notebook text tweaks --- notebook/agentchat_realtime_swarm.ipynb | 27 ++----------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index dc9365b82b..59db0bcb26 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -345,21 +345,14 @@ " agent.register_hand_off(ON_CONDITION(triage_agent, transfer_to_triage_description))" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Expose your local service to the internet using ngrok\n", - "\n", - "..." - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Running the Code\n", "\n", + "**Note**: You may need to expose your machine to the internet through a tunnel, such as one provided by [ngrok](https://ngrok.com/).\n", + "\n", "This code sets up the FastAPI server for the RealtimeAgent, enabling it to handle real-time voice interactions through Twilio. By executing this code, you’ll start the server and make it accessible for testing voice calls.\n", "\n", "Here’s what happens when you run the code: \n", @@ -442,22 +435,6 @@ "nest_asyncio.apply()\n", "uvicorn.run(app, host=\"0.0.0.0\", port=5050)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "realtime_llm_config" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From c68633edc75f4a13e8d6a0e2b28e3c803af5970b Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Thu, 19 Dec 2024 21:16:12 +0000 Subject: [PATCH 42/51] Update response message to match video --- notebook/agentchat_realtime_swarm.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/agentchat_realtime_swarm.ipynb b/notebook/agentchat_realtime_swarm.ipynb index 59db0bcb26..909f5ad95d 100644 --- a/notebook/agentchat_realtime_swarm.ipynb +++ b/notebook/agentchat_realtime_swarm.ipynb @@ -402,7 +402,7 @@ "async def handle_incoming_call(request: Request):\n", " \"\"\"Handle incoming call and return TwiML response to connect to Media Stream.\"\"\"\n", " response = VoiceResponse()\n", - " response.say(\"Please wait while we connect your call to the AI voice assistant.\")\n", + " response.say(\"Welcome to Agentic Airways, please wait while we connect your call to the AI voice assistant.\")\n", " response.pause(length=1)\n", " response.say(\"O.K. you can start talking!\")\n", " host = request.url.hostname\n", From 6c01a66fd905211da461376706fb395517117801 Mon Sep 17 00:00:00 2001 From: Davorin Rusevljan Date: Thu, 19 Dec 2024 22:53:01 +0100 Subject: [PATCH 43/51] websocket realtime wip(1) --- autogen/agentchat/realtime_agent/__init__.py | 2 + .../realtime_agent/websocket_observer.py | 130 ++ notebook/static/Audio.js | 255 ++++ notebook/static/AudioCapture.js | 100 ++ notebook/static/AudioPlayer.js | 101 ++ notebook/static/main.js | 6 + notebook/static/wavtools.js | 1244 +++++++++++++++++ notebook/templates/chat.html | 23 + 8 files changed, 1861 insertions(+) create mode 100644 autogen/agentchat/realtime_agent/websocket_observer.py create mode 100644 notebook/static/Audio.js create mode 100644 notebook/static/AudioCapture.js create mode 100644 notebook/static/AudioPlayer.js create mode 100644 notebook/static/main.js create mode 100644 notebook/static/wavtools.js create mode 100644 notebook/templates/chat.html diff --git a/autogen/agentchat/realtime_agent/__init__.py b/autogen/agentchat/realtime_agent/__init__.py index fe3572874b..d5cd2f30e7 100644 --- a/autogen/agentchat/realtime_agent/__init__.py +++ b/autogen/agentchat/realtime_agent/__init__.py @@ -1,9 +1,11 @@ from .function_observer import FunctionObserver from .realtime_agent import RealtimeAgent from .twilio_observer import TwilioAudioAdapter +from .websocket_observer import WebsocketAudioAdapter __all__ = [ "RealtimeAgent", "FunctionObserver", "TwilioAudioAdapter", + "WebsocketAudioAdapter" ] diff --git a/autogen/agentchat/realtime_agent/websocket_observer.py b/autogen/agentchat/realtime_agent/websocket_observer.py new file mode 100644 index 0000000000..a25137c7c3 --- /dev/null +++ b/autogen/agentchat/realtime_agent/websocket_observer.py @@ -0,0 +1,130 @@ +# Copyright (c) 2023 - 2024, Owners of https://github.com/ag2ai +# +# SPDX-License-Identifier: Apache-2.0 +# +# Portions derived from https://github.com/microsoft/autogen are under the MIT License. +# SPDX-License-Identifier: MIT + +import base64 +import json + +from fastapi import WebSocketDisconnect + +from .realtime_observer import RealtimeObserver + +LOG_EVENT_TYPES = [ + "error", + "response.content.done", + "rate_limits.updated", + "response.done", + "input_audio_buffer.committed", + "input_audio_buffer.speech_stopped", + "input_audio_buffer.speech_started", + "session.created", +] +SHOW_TIMING_MATH = False + + +class WebsocketAudioAdapter(RealtimeObserver): + def __init__(self, websocket): + super().__init__() + self.websocket = websocket + + # Connection specific state + self.stream_sid = None + self.latest_media_timestamp = 0 + self.last_assistant_item = None + self.mark_queue = [] + self.response_start_timestamp_twilio = None + + async def update(self, response): + """Receive events from the OpenAI Realtime API, send audio back to websocket.""" + if response["type"] in LOG_EVENT_TYPES: + print(f"Received event: {response['type']}", response) + + if response.get("type") == "response.audio.delta" and "delta" in response: + audio_payload = base64.b64encode(base64.b64decode(response["delta"])).decode("utf-8") + audio_delta = {"event": "media", "streamSid": self.stream_sid, "media": {"payload": audio_payload}} + await self.websocket.send_json(audio_delta) + + if self.response_start_timestamp_twilio is None: + self.response_start_timestamp_twilio = self.latest_media_timestamp + if SHOW_TIMING_MATH: + print(f"Setting start timestamp for new response: {self.response_start_timestamp_twilio}ms") + + # Update last_assistant_item safely + if response.get("item_id"): + self.last_assistant_item = response["item_id"] + + await self.send_mark() + + # Trigger an interruption. Your use case might work better using `input_audio_buffer.speech_stopped`, or combining the two. + if response.get("type") == "input_audio_buffer.speech_started": + print("Speech started detected.") + if self.last_assistant_item: + print(f"Interrupting response with id: {self.last_assistant_item}") + await self.handle_speech_started_event() + + async def handle_speech_started_event(self): + """Handle interruption when the caller's speech starts.""" + print("Handling speech started event.") + if self.mark_queue and self.response_start_timestamp_twilio is not None: + elapsed_time = self.latest_media_timestamp - self.response_start_timestamp_twilio + if SHOW_TIMING_MATH: + print( + f"Calculating elapsed time for truncation: {self.latest_media_timestamp} - {self.response_start_timestamp_twilio} = {elapsed_time}ms" + ) + + if self.last_assistant_item: + if SHOW_TIMING_MATH: + print(f"Truncating item with ID: {self.last_assistant_item}, Truncated at: {elapsed_time}ms") + + truncate_event = { + "type": "conversation.item.truncate", + "item_id": self.last_assistant_item, + "content_index": 0, + "audio_end_ms": elapsed_time, + } + await self._client._openai_ws.send(json.dumps(truncate_event)) + + await self.websocket.send_json({"event": "clear", "streamSid": self.stream_sid}) + + self.mark_queue.clear() + self.last_assistant_item = None + self.response_start_timestamp_twilio = None + + async def send_mark(self): + if self.stream_sid: + mark_event = {"event": "mark", "streamSid": self.stream_sid, "mark": {"name": "responsePart"}} + await self.websocket.send_json(mark_event) + self.mark_queue.append("responsePart") + + async def run(self): + openai_ws = self._client._openai_ws + await self.initialize_session() + + async for message in self.websocket.iter_text(): + data = json.loads(message) + if data["event"] == "media": + self.latest_media_timestamp = int(data["media"]["timestamp"]) + audio_append = {"type": "input_audio_buffer.append", "audio": data["media"]["payload"]} + #await openai_ws.send(json.dumps(audio_append)) + audio_delta = {"event": "media", "streamSid": self.stream_sid, "media": {"payload": data["media"]["payload"]}} + await self.websocket.send_json(audio_delta) + elif data["event"] == "start": + self.stream_sid = data["start"]["streamSid"] + print(f"Incoming stream has started {self.stream_sid}") + self.response_start_timestamp_twilio = None + self.latest_media_timestamp = 0 + self.last_assistant_item = None + elif data["event"] == "mark": + if self.mark_queue: + self.mark_queue.pop(0) + + async def initialize_session(self): + """Control initial session with OpenAI.""" + session_update = { + "input_audio_format": "pcm16", # g711_ulaw + "output_audio_format": "pcm16" # "g711_ulaw", + } + await self._client.session_update(session_update) diff --git a/notebook/static/Audio.js b/notebook/static/Audio.js new file mode 100644 index 0000000000..7612f16e73 --- /dev/null +++ b/notebook/static/Audio.js @@ -0,0 +1,255 @@ +// AudioPlayer.js + +export class Audio { + constructor(webSocketUrl) { + this.webSocketUrl = webSocketUrl; + this.socket = null; + // audio out + this.outAudioContext = null; + this.sourceNode = null; + this.bufferQueue = []; // Queue to store audio buffers + this.isPlaying = false; // Flag to check if audio is playing + // audio in + this.inAudioContext = null; + this.processorNode = null; + this.stream = null; + this.bufferSize = 8192; // Define the buffer size for capturing chunks + } + + // Initialize WebSocket and start receiving audio data + async start() { + try { + // Initialize WebSocket connection + this.socket = new WebSocket(this.webSocketUrl); + + this.socket.onopen = () => { + console.log("WebSocket connected."); + const sessionStarted = { + event: "start", + start: { + streamSid:"dsfstreamSidsdf", + } + } + this.socket.send(JSON.stringify(sessionStarted)) + console.log("sent session start") + }; + + this.socket.onclose = () => { + console.log("WebSocket disconnected."); + }; + + this.socket.onmessage = async (event) => { + console.log("Received web socket message") + const message = JSON.parse(event.data) + if (message.event == "media") { + console.log("got media payload..") + + const bufferString = atob(message.media.payload); // Decode base64 to binary string + const byteArray = new Uint8Array(bufferString.length); + for (let i = 0; i < bufferString.length; i++) { + byteArray[i] = bufferString.charCodeAt(i); //Create a byte array + } + //const payload = base64.decode(message.media.payload) + // Ensure the data is an ArrayBuffer, if it's a Blob, convert it + //const pcmData = event.data instanceof ArrayBuffer ? event.data : await event.data.arrayBuffer(); + // + + this.queuePcmData(byteArray.buffer); // Push the received data into the buffer queue + if (!this.isPlaying) { + this.playFromQueue(); // Start playing if not already playing + } + } + }; + this.outAudioContext = new (window.AudioContext || window.webkitAudioContext)(); + console.log("Audio player initialized."); + + /* + await wavRecorder.begin() + await wavRecorder.record((data) => { + try { + const { mono, raw } = data; + console.log("rec:", mono) + console.log("rec:", mono.length) + const pcmBuffer = new ArrayBuffer(mono.length * 2); // 2 bytes per sample + const pcmView = new DataView(pcmBuffer); + + for (let i = 0; i < mono.length; i++) { + pcmView.setInt16(i * 2, mono[i], true); // true means little-endian + } + + const byteArray = new Uint8Array(pcmView); // Create a Uint8Array view + const bufferString = String.fromCharCode(...byteArray); // convert each byte of the buffer to a character + const audioBase64String = btoa(bufferString); // Apply base64 + + + if (this.socket.readyState === WebSocket.OPEN) { + const audioMessage = { + 'event': "media", + 'media': { + 'timestamp': Date.now(), + 'payload': audioBase64String + } + } + console.log("sendin voice ..", audioMessage); + this.socket.send(JSON.stringify(audioMessage)); + } + } catch (ex) { + console.log("napaka", ex) + } + }); + */ + + // audio in + // Get user media (microphone access) + + + const stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate:24000} }); + this.stream = stream; + console.log("Audio tracks", stream.getAudioTracks()) + console.log('Sample rate :', stream.getAudioTracks()[0].getSettings().sampleRate) + this.inAudioContext = new (window.AudioContext || window.webkitAudioContext)(); + + // Create an AudioNode to capture the microphone stream + const sourceNode = this.inAudioContext.createMediaStreamSource(stream); + + // Create a ScriptProcessorNode (or AudioWorkletProcessor for better performance) + this.processorNode = this.inAudioContext.createScriptProcessor(this.bufferSize, 1, 1); + + // Process audio data when available + this.processorNode.onaudioprocess = (event) => { + const inputBuffer = event.inputBuffer; + + // Extract PCM 16-bit data from input buffer (mono channel) + const audioData = this.extractPcm16Data(inputBuffer); + const byteArray = new Uint8Array(audioData); // Create a Uint8Array view + const bufferString = String.fromCharCode(...byteArray); // convert each byte of the buffer to a character + const audioBase64String = btoa(bufferString); // Apply base64 + // Send the PCM data over the WebSocket + if (this.socket.readyState === WebSocket.OPEN) { + const audioMessage = { + 'event': "media", + 'media': { + 'timestamp': Date.now(), + 'payload': audioBase64String + } + } + //console.log("sendin voice ..", audioMessage); + this.socket.send(JSON.stringify(audioMessage)); + } + }; + + // Connect the source node to the processor node and the processor node to the destination (speakers) + sourceNode.connect(this.processorNode); + this.processorNode.connect(this.inAudioContext.destination); + + console.log("Audio capture started."); + } catch (err) { + console.error("Error initializing audio player:", err); + } + } + + // Stop receiving and playing audio + stop() { + this.stop_out() + this.stop_in() + } + + stop_out() { + if (this.socket) { + this.socket.close(); + } + if (this.outAudioContext) { + this.outAudioContext.close(); + } + console.log("Audio player stopped."); + } + + stop_in() { + if (this.processorNode) { + this.processorNode.disconnect(); + } + if (this.inAudioContext) { + this.inAudioContext.close(); + } + if (this.socket) { + this.socket.close(); + } + if (this.stream) { + this.stream.getTracks().forEach(track => track.stop()); + } + console.log("Audio capture stopped."); + } + + // Queue PCM data for later playback + queuePcmData(pcmData) { + this.bufferQueue.push(pcmData); + } + + // Play audio from the queue + async playFromQueue() { + if (this.bufferQueue.length === 0) { + this.isPlaying = false; // No more data to play + return; + } + + this.isPlaying = true; + const pcmData = this.bufferQueue.shift(); // Get the next chunk from the queue + + // Convert PCM 16-bit data to ArrayBuffer + const audioBuffer = await this.decodePcm16Data(pcmData); + + // Create an audio source and play it + const source = this.outAudioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(this.outAudioContext.destination); + source.onended = () => { + // Play the next chunk after the current one ends + this.playFromQueue(); + }; + source.start(); + } + + // Decode PCM 16-bit data into AudioBuffer + async decodePcm16Data(pcmData) { + const audioData = new Float32Array(pcmData.byteLength / 2); + + // Convert PCM 16-bit to Float32Array + const dataView = new DataView(pcmData); + for (let i = 0; i < audioData.length; i++) { + const pcm16 = dataView.getInt16(i * 2, true); // true means little-endian + audioData[i] = pcm16 / 32768; // Convert to normalized float (-1 to 1) + } + + // Create an audio buffer from the Float32Array + console.log("sample rate is ", this.outAudioContext.sampleRate) + //const audioBuffer = this.outAudioContext.createBuffer(1, audioData.length, 24000); + const audioBuffer = this.outAudioContext.createBuffer(1, audioData.length, 41000); + audioBuffer.getChannelData(0).set(audioData); + + return audioBuffer; + } + + // Convert audio buffer to PCM 16-bit data + extractPcm16Data(buffer) { + const sampleRate = buffer.sampleRate; + const length = buffer.length; + const pcmData = new Int16Array(length); + + // Convert the float samples to PCM 16-bit (scaled between -32768 and 32767) + for (let i = 0; i < length; i++) { + pcmData[i] = Math.max(-32768, Math.min(32767, buffer.getChannelData(0)[i] * 32767)); + } + + // Convert Int16Array to a binary buffer (ArrayBuffer) + const pcmBuffer = new ArrayBuffer(pcmData.length * 2); // 2 bytes per sample + const pcmView = new DataView(pcmBuffer); + + for (let i = 0; i < pcmData.length; i++) { + pcmView.setInt16(i * 2, pcmData[i], true); // true means little-endian + } + + return pcmBuffer; + } + + } + \ No newline at end of file diff --git a/notebook/static/AudioCapture.js b/notebook/static/AudioCapture.js new file mode 100644 index 0000000000..a4532d19ec --- /dev/null +++ b/notebook/static/AudioCapture.js @@ -0,0 +1,100 @@ +export class AudioCapture { + constructor(webSocketUrl) { + this.webSocketUrl = webSocketUrl; + this.socket = null; + this.audioContext = null; + this.processorNode = null; + this.stream = null; + this.bufferSize = 8192; // Define the buffer size for capturing chunks + } + + // Initialize WebSocket and start capturing audio + async start() { + try { + // Initialize WebSocket connection + this.socket = new WebSocket(this.webSocketUrl); + + this.socket.onopen = () => { + console.log("WebSocket connected."); + }; + + this.socket.onclose = () => { + console.log("WebSocket disconnected."); + }; + + // Get user media (microphone access) + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + this.stream = stream; + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + + // Create an AudioNode to capture the microphone stream + const sourceNode = this.audioContext.createMediaStreamSource(stream); + + // Create a ScriptProcessorNode (or AudioWorkletProcessor for better performance) + this.processorNode = this.audioContext.createScriptProcessor(this.bufferSize, 1, 1); + + // Process audio data when available + this.processorNode.onaudioprocess = (event) => { + const inputBuffer = event.inputBuffer; + const outputBuffer = event.outputBuffer; + + // Extract PCM 16-bit data from input buffer (mono channel) + const audioData = this.extractPcm16Data(inputBuffer); + + // Send the PCM data over the WebSocket + if (this.socket.readyState === WebSocket.OPEN) { + this.socket.send(audioData); + } + }; + + // Connect the source node to the processor node and the processor node to the destination (speakers) + sourceNode.connect(this.processorNode); + this.processorNode.connect(this.audioContext.destination); + + console.log("Audio capture started."); + } catch (err) { + console.error("Error capturing audio:", err); + } + } + + // Stop capturing audio and close the WebSocket connection + stop() { + if (this.processorNode) { + this.processorNode.disconnect(); + } + if (this.audioContext) { + this.audioContext.close(); + } + if (this.socket) { + this.socket.close(); + } + if (this.stream) { + this.stream.getTracks().forEach(track => track.stop()); + } + + console.log("Audio capture stopped."); + } + + // Convert audio buffer to PCM 16-bit data + extractPcm16Data(buffer) { + const sampleRate = buffer.sampleRate; + const length = buffer.length; + const pcmData = new Int16Array(length); + + // Convert the float samples to PCM 16-bit (scaled between -32768 and 32767) + for (let i = 0; i < length; i++) { + pcmData[i] = Math.max(-32768, Math.min(32767, buffer.getChannelData(0)[i] * 32767)); + } + + // Convert Int16Array to a binary buffer (ArrayBuffer) + const pcmBuffer = new ArrayBuffer(pcmData.length * 2); // 2 bytes per sample + const pcmView = new DataView(pcmBuffer); + + for (let i = 0; i < pcmData.length; i++) { + pcmView.setInt16(i * 2, pcmData[i], true); // true means little-endian + } + + return pcmBuffer; + } + } + \ No newline at end of file diff --git a/notebook/static/AudioPlayer.js b/notebook/static/AudioPlayer.js new file mode 100644 index 0000000000..319a768355 --- /dev/null +++ b/notebook/static/AudioPlayer.js @@ -0,0 +1,101 @@ +// AudioPlayer.js + +export class AudioPlayer { + constructor(webSocketUrl) { + this.webSocketUrl = webSocketUrl; + this.socket = null; + this.audioContext = null; + this.sourceNode = null; + this.bufferQueue = []; // Queue to store audio buffers + this.isPlaying = false; // Flag to check if audio is playing + } + + // Initialize WebSocket and start receiving audio data + async start() { + try { + // Initialize WebSocket connection + this.socket = new WebSocket(this.webSocketUrl); + + this.socket.onopen = () => { + console.log("WebSocket connected."); + }; + + this.socket.onclose = () => { + console.log("WebSocket disconnected."); + }; + + this.socket.onmessage = async (event) => { + // Ensure the data is an ArrayBuffer, if it's a Blob, convert it + const pcmData = event.data instanceof ArrayBuffer ? event.data : await event.data.arrayBuffer(); + this.queuePcmData(pcmData); // Push the received data into the buffer queue + if (!this.isPlaying) { + this.playFromQueue(); // Start playing if not already playing + } + }; + + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + console.log("Audio player initialized."); + } catch (err) { + console.error("Error initializing audio player:", err); + } + } + + // Stop receiving and playing audio + stop() { + if (this.socket) { + this.socket.close(); + } + if (this.audioContext) { + this.audioContext.close(); + } + console.log("Audio player stopped."); + } + + // Queue PCM data for later playback + queuePcmData(pcmData) { + this.bufferQueue.push(pcmData); + } + + // Play audio from the queue + async playFromQueue() { + if (this.bufferQueue.length === 0) { + this.isPlaying = false; // No more data to play + return; + } + + this.isPlaying = true; + const pcmData = this.bufferQueue.shift(); // Get the next chunk from the queue + + // Convert PCM 16-bit data to ArrayBuffer + const audioBuffer = await this.decodePcm16Data(pcmData); + + // Create an audio source and play it + const source = this.audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(this.audioContext.destination); + source.onended = () => { + // Play the next chunk after the current one ends + this.playFromQueue(); + }; + source.start(); + } + + // Decode PCM 16-bit data into AudioBuffer + async decodePcm16Data(pcmData) { + const audioData = new Float32Array(pcmData.byteLength / 2); + + // Convert PCM 16-bit to Float32Array + const dataView = new DataView(pcmData); + for (let i = 0; i < audioData.length; i++) { + const pcm16 = dataView.getInt16(i * 2, true); // true means little-endian + audioData[i] = pcm16 / 32768; // Convert to normalized float (-1 to 1) + } + + // Create an audio buffer from the Float32Array + const audioBuffer = this.audioContext.createBuffer(1, audioData.length, this.audioContext.sampleRate); + audioBuffer.getChannelData(0).set(audioData); + + return audioBuffer; + } + } + \ No newline at end of file diff --git a/notebook/static/main.js b/notebook/static/main.js new file mode 100644 index 0000000000..e9563aa301 --- /dev/null +++ b/notebook/static/main.js @@ -0,0 +1,6 @@ +import { Audio } from './Audio.js'; + +// Create an instance of AudioPlayer with the WebSocket URL +const audio = new Audio(socketUrl); +// Start receiving and playing audio +audio.start(); \ No newline at end of file diff --git a/notebook/static/wavtools.js b/notebook/static/wavtools.js new file mode 100644 index 0000000000..9d21d048ea --- /dev/null +++ b/notebook/static/wavtools.js @@ -0,0 +1,1244 @@ +(() => { + // lib/wav_packer.js + var WavPacker = class { + /** + * Converts Float32Array of amplitude data to ArrayBuffer in Int16Array format + * @param {Float32Array} float32Array + * @returns {ArrayBuffer} + */ + static floatTo16BitPCM(float32Array) { + const buffer = new ArrayBuffer(float32Array.length * 2); + const view = new DataView(buffer); + let offset = 0; + for (let i = 0; i < float32Array.length; i++, offset += 2) { + let s = Math.max(-1, Math.min(1, float32Array[i])); + view.setInt16(offset, s < 0 ? s * 32768 : s * 32767, true); + } + return buffer; + } + /** + * Concatenates two ArrayBuffers + * @param {ArrayBuffer} leftBuffer + * @param {ArrayBuffer} rightBuffer + * @returns {ArrayBuffer} + */ + static mergeBuffers(leftBuffer, rightBuffer) { + const tmpArray = new Uint8Array( + leftBuffer.byteLength + rightBuffer.byteLength + ); + tmpArray.set(new Uint8Array(leftBuffer), 0); + tmpArray.set(new Uint8Array(rightBuffer), leftBuffer.byteLength); + return tmpArray.buffer; + } + /** + * Packs data into an Int16 format + * @private + * @param {number} size 0 = 1x Int16, 1 = 2x Int16 + * @param {number} arg value to pack + * @returns + */ + _packData(size, arg) { + return [ + new Uint8Array([arg, arg >> 8]), + new Uint8Array([arg, arg >> 8, arg >> 16, arg >> 24]) + ][size]; + } + /** + * Packs audio into "audio/wav" Blob + * @param {number} sampleRate + * @param {{bitsPerSample: number, channels: Array, data: Int16Array}} audio + * @returns {WavPackerAudioType} + */ + pack(sampleRate, audio) { + if (!audio?.bitsPerSample) { + throw new Error(`Missing "bitsPerSample"`); + } else if (!audio?.channels) { + throw new Error(`Missing "channels"`); + } else if (!audio?.data) { + throw new Error(`Missing "data"`); + } + const { bitsPerSample, channels, data } = audio; + const output = [ + // Header + "RIFF", + this._packData( + 1, + 4 + (8 + 24) + (8 + 8) + /* chunk 2 length */ + ), + // Length + "WAVE", + // chunk 1 + "fmt ", + // Sub-chunk identifier + this._packData(1, 16), + // Chunk length + this._packData(0, 1), + // Audio format (1 is linear quantization) + this._packData(0, channels.length), + this._packData(1, sampleRate), + this._packData(1, sampleRate * channels.length * bitsPerSample / 8), + // Byte rate + this._packData(0, channels.length * bitsPerSample / 8), + this._packData(0, bitsPerSample), + // chunk 2 + "data", + // Sub-chunk identifier + this._packData( + 1, + channels[0].length * channels.length * bitsPerSample / 8 + ), + // Chunk length + data + ]; + const blob = new Blob(output, { type: "audio/mpeg" }); + const url = URL.createObjectURL(blob); + return { + blob, + url, + channelCount: channels.length, + sampleRate, + duration: data.byteLength / (channels.length * sampleRate * 2) + }; + } + }; + globalThis.WavPacker = WavPacker; + + // lib/analysis/constants.js + var octave8Frequencies = [ + 4186.01, + 4434.92, + 4698.63, + 4978.03, + 5274.04, + 5587.65, + 5919.91, + 6271.93, + 6644.88, + 7040, + 7458.62, + 7902.13 + ]; + var octave8FrequencyLabels = [ + "C", + "C#", + "D", + "D#", + "E", + "F", + "F#", + "G", + "G#", + "A", + "A#", + "B" + ]; + var noteFrequencies = []; + var noteFrequencyLabels = []; + for (let i = 1; i <= 8; i++) { + for (let f = 0; f < octave8Frequencies.length; f++) { + const freq = octave8Frequencies[f]; + noteFrequencies.push(freq / Math.pow(2, 8 - i)); + noteFrequencyLabels.push(octave8FrequencyLabels[f] + i); + } + } + var voiceFrequencyRange = [32, 2e3]; + var voiceFrequencies = noteFrequencies.filter((_, i) => { + return noteFrequencies[i] > voiceFrequencyRange[0] && noteFrequencies[i] < voiceFrequencyRange[1]; + }); + var voiceFrequencyLabels = noteFrequencyLabels.filter((_, i) => { + return noteFrequencies[i] > voiceFrequencyRange[0] && noteFrequencies[i] < voiceFrequencyRange[1]; + }); + + // lib/analysis/audio_analysis.js + var AudioAnalysis = class _AudioAnalysis { + /** + * Retrieves frequency domain data from an AnalyserNode adjusted to a decibel range + * returns human-readable formatting and labels + * @param {AnalyserNode} analyser + * @param {number} sampleRate + * @param {Float32Array} [fftResult] + * @param {"frequency"|"music"|"voice"} [analysisType] + * @param {number} [minDecibels] default -100 + * @param {number} [maxDecibels] default -30 + * @returns {AudioAnalysisOutputType} + */ + static getFrequencies(analyser, sampleRate, fftResult, analysisType = "frequency", minDecibels = -100, maxDecibels = -30) { + if (!fftResult) { + fftResult = new Float32Array(analyser.frequencyBinCount); + analyser.getFloatFrequencyData(fftResult); + } + const nyquistFrequency = sampleRate / 2; + const frequencyStep = 1 / fftResult.length * nyquistFrequency; + let outputValues; + let frequencies; + let labels; + if (analysisType === "music" || analysisType === "voice") { + const useFrequencies = analysisType === "voice" ? voiceFrequencies : noteFrequencies; + const aggregateOutput = Array(useFrequencies.length).fill(minDecibels); + for (let i = 0; i < fftResult.length; i++) { + const frequency = i * frequencyStep; + const amplitude = fftResult[i]; + for (let n = useFrequencies.length - 1; n >= 0; n--) { + if (frequency > useFrequencies[n]) { + aggregateOutput[n] = Math.max(aggregateOutput[n], amplitude); + break; + } + } + } + outputValues = aggregateOutput; + frequencies = analysisType === "voice" ? voiceFrequencies : noteFrequencies; + labels = analysisType === "voice" ? voiceFrequencyLabels : noteFrequencyLabels; + } else { + outputValues = Array.from(fftResult); + frequencies = outputValues.map((_, i) => frequencyStep * i); + labels = frequencies.map((f) => `${f.toFixed(2)} Hz`); + } + const normalizedOutput = outputValues.map((v) => { + return Math.max( + 0, + Math.min((v - minDecibels) / (maxDecibels - minDecibels), 1) + ); + }); + const values = new Float32Array(normalizedOutput); + return { + values, + frequencies, + labels + }; + } + /** + * Creates a new AudioAnalysis instance for an HTMLAudioElement + * @param {HTMLAudioElement} audioElement + * @param {AudioBuffer|null} [audioBuffer] If provided, will cache all frequency domain data from the buffer + * @returns {AudioAnalysis} + */ + constructor(audioElement, audioBuffer = null) { + this.fftResults = []; + if (audioBuffer) { + const { length, sampleRate } = audioBuffer; + const offlineAudioContext = new OfflineAudioContext({ + length, + sampleRate + }); + const source = offlineAudioContext.createBufferSource(); + source.buffer = audioBuffer; + const analyser = offlineAudioContext.createAnalyser(); + analyser.fftSize = 8192; + analyser.smoothingTimeConstant = 0.1; + source.connect(analyser); + const renderQuantumInSeconds = 1 / 60; + const durationInSeconds = length / sampleRate; + const analyze = (index) => { + const suspendTime = renderQuantumInSeconds * index; + if (suspendTime < durationInSeconds) { + offlineAudioContext.suspend(suspendTime).then(() => { + const fftResult = new Float32Array(analyser.frequencyBinCount); + analyser.getFloatFrequencyData(fftResult); + this.fftResults.push(fftResult); + analyze(index + 1); + }); + } + if (index === 1) { + offlineAudioContext.startRendering(); + } else { + offlineAudioContext.resume(); + } + }; + source.start(0); + analyze(1); + this.audio = audioElement; + this.context = offlineAudioContext; + this.analyser = analyser; + this.sampleRate = sampleRate; + this.audioBuffer = audioBuffer; + } else { + const audioContext = new AudioContext(); + const track = audioContext.createMediaElementSource(audioElement); + const analyser = audioContext.createAnalyser(); + analyser.fftSize = 8192; + analyser.smoothingTimeConstant = 0.1; + track.connect(analyser); + analyser.connect(audioContext.destination); + this.audio = audioElement; + this.context = audioContext; + this.analyser = analyser; + this.sampleRate = this.context.sampleRate; + this.audioBuffer = null; + } + } + /** + * Gets the current frequency domain data from the playing audio track + * @param {"frequency"|"music"|"voice"} [analysisType] + * @param {number} [minDecibels] default -100 + * @param {number} [maxDecibels] default -30 + * @returns {AudioAnalysisOutputType} + */ + getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) { + let fftResult = null; + if (this.audioBuffer && this.fftResults.length) { + const pct = this.audio.currentTime / this.audio.duration; + const index = Math.min( + pct * this.fftResults.length | 0, + this.fftResults.length - 1 + ); + fftResult = this.fftResults[index]; + } + return _AudioAnalysis.getFrequencies( + this.analyser, + this.sampleRate, + fftResult, + analysisType, + minDecibels, + maxDecibels + ); + } + /** + * Resume the internal AudioContext if it was suspended due to the lack of + * user interaction when the AudioAnalysis was instantiated. + * @returns {Promise} + */ + async resumeIfSuspended() { + if (this.context.state === "suspended") { + await this.context.resume(); + } + return true; + } + }; + globalThis.AudioAnalysis = AudioAnalysis; + + // lib/worklets/stream_processor.js + var StreamProcessorWorklet = ` + class StreamProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.hasStarted = false; + this.hasInterrupted = false; + this.outputBuffers = []; + this.bufferLength = 128; + this.write = { buffer: new Float32Array(this.bufferLength), trackId: null }; + this.writeOffset = 0; + this.trackSampleOffsets = {}; + this.port.onmessage = (event) => { + if (event.data) { + const payload = event.data; + if (payload.event === 'write') { + const int16Array = payload.buffer; + const float32Array = new Float32Array(int16Array.length); + for (let i = 0; i < int16Array.length; i++) { + float32Array[i] = int16Array[i] / 0x8000; // Convert Int16 to Float32 + } + this.writeData(float32Array, payload.trackId); + } else if ( + payload.event === 'offset' || + payload.event === 'interrupt' + ) { + const requestId = payload.requestId; + const trackId = this.write.trackId; + const offset = this.trackSampleOffsets[trackId] || 0; + this.port.postMessage({ + event: 'offset', + requestId, + trackId, + offset, + }); + if (payload.event === 'interrupt') { + this.hasInterrupted = true; + } + } else { + throw new Error(\`Unhandled event "\${payload.event}"\`); + } + } + }; + } + + writeData(float32Array, trackId = null) { + let { buffer } = this.write; + let offset = this.writeOffset; + for (let i = 0; i < float32Array.length; i++) { + buffer[offset++] = float32Array[i]; + if (offset >= buffer.length) { + this.outputBuffers.push(this.write); + this.write = { buffer: new Float32Array(this.bufferLength), trackId }; + buffer = this.write.buffer; + offset = 0; + } + } + this.writeOffset = offset; + return true; + } + + process(inputs, outputs, parameters) { + const output = outputs[0]; + const outputChannelData = output[0]; + const outputBuffers = this.outputBuffers; + if (this.hasInterrupted) { + this.port.postMessage({ event: 'stop' }); + return false; + } else if (outputBuffers.length) { + this.hasStarted = true; + const { buffer, trackId } = outputBuffers.shift(); + for (let i = 0; i < outputChannelData.length; i++) { + outputChannelData[i] = buffer[i] || 0; + } + if (trackId) { + this.trackSampleOffsets[trackId] = + this.trackSampleOffsets[trackId] || 0; + this.trackSampleOffsets[trackId] += buffer.length; + } + return true; + } else if (this.hasStarted) { + this.port.postMessage({ event: 'stop' }); + return false; + } else { + return true; + } + } + } + + registerProcessor('stream_processor', StreamProcessor); + `; + var script = new Blob([StreamProcessorWorklet], { + type: "application/javascript" + }); + var src = URL.createObjectURL(script); + var StreamProcessorSrc = src; + + // lib/wav_stream_player.js + var WavStreamPlayer = class { + /** + * Creates a new WavStreamPlayer instance + * @param {{sampleRate?: number}} options + * @returns {WavStreamPlayer} + */ + constructor({ sampleRate = 44100 } = {}) { + this.scriptSrc = StreamProcessorSrc; + this.sampleRate = sampleRate; + this.context = null; + this.stream = null; + this.analyser = null; + this.trackSampleOffsets = {}; + this.interruptedTrackIds = {}; + } + /** + * Connects the audio context and enables output to speakers + * @returns {Promise} + */ + async connect() { + this.context = new AudioContext({ sampleRate: this.sampleRate }); + if (this.context.state === "suspended") { + await this.context.resume(); + } + try { + await this.context.audioWorklet.addModule(this.scriptSrc); + } catch (e) { + console.error(e); + throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`); + } + const analyser = this.context.createAnalyser(); + analyser.fftSize = 8192; + analyser.smoothingTimeConstant = 0.1; + this.analyser = analyser; + return true; + } + /** + * Gets the current frequency domain data from the playing track + * @param {"frequency"|"music"|"voice"} [analysisType] + * @param {number} [minDecibels] default -100 + * @param {number} [maxDecibels] default -30 + * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType} + */ + getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) { + if (!this.analyser) { + throw new Error("Not connected, please call .connect() first"); + } + return AudioAnalysis.getFrequencies( + this.analyser, + this.sampleRate, + null, + analysisType, + minDecibels, + maxDecibels + ); + } + /** + * Starts audio streaming + * @private + * @returns {Promise} + */ + _start() { + const streamNode = new AudioWorkletNode(this.context, "stream_processor"); + streamNode.connect(this.context.destination); + streamNode.port.onmessage = (e) => { + const { event } = e.data; + if (event === "stop") { + streamNode.disconnect(); + this.stream = null; + } else if (event === "offset") { + const { requestId, trackId, offset } = e.data; + const currentTime = offset / this.sampleRate; + this.trackSampleOffsets[requestId] = { trackId, offset, currentTime }; + } + }; + this.analyser.disconnect(); + streamNode.connect(this.analyser); + this.stream = streamNode; + return true; + } + /** + * Adds 16BitPCM data to the currently playing audio stream + * You can add chunks beyond the current play point and they will be queued for play + * @param {ArrayBuffer|Int16Array} arrayBuffer + * @param {string} [trackId] + * @returns {Int16Array} + */ + add16BitPCM(arrayBuffer, trackId = "default") { + if (typeof trackId !== "string") { + throw new Error(`trackId must be a string`); + } else if (this.interruptedTrackIds[trackId]) { + return; + } + if (!this.stream) { + this._start(); + } + let buffer; + if (arrayBuffer instanceof Int16Array) { + buffer = arrayBuffer; + } else if (arrayBuffer instanceof ArrayBuffer) { + buffer = new Int16Array(arrayBuffer); + } else { + throw new Error(`argument must be Int16Array or ArrayBuffer`); + } + this.stream.port.postMessage({ event: "write", buffer, trackId }); + return buffer; + } + /** + * Gets the offset (sample count) of the currently playing stream + * @param {boolean} [interrupt] + * @returns {{trackId: string|null, offset: number, currentTime: number}} + */ + async getTrackSampleOffset(interrupt = false) { + if (!this.stream) { + return null; + } + const requestId = crypto.randomUUID(); + this.stream.port.postMessage({ + event: interrupt ? "interrupt" : "offset", + requestId + }); + let trackSampleOffset; + while (!trackSampleOffset) { + trackSampleOffset = this.trackSampleOffsets[requestId]; + await new Promise((r) => setTimeout(() => r(), 1)); + } + const { trackId } = trackSampleOffset; + if (interrupt && trackId) { + this.interruptedTrackIds[trackId] = true; + } + return trackSampleOffset; + } + /** + * Strips the current stream and returns the sample offset of the audio + * @param {boolean} [interrupt] + * @returns {{trackId: string|null, offset: number, currentTime: number}} + */ + async interrupt() { + return this.getTrackSampleOffset(true); + } + }; + globalThis.WavStreamPlayer = WavStreamPlayer; + + // lib/worklets/audio_processor.js + var AudioProcessorWorklet = ` + class AudioProcessor extends AudioWorkletProcessor { + + constructor() { + super(); + this.port.onmessage = this.receive.bind(this); + this.initialize(); + } + + initialize() { + this.foundAudio = false; + this.recording = false; + this.chunks = []; + } + + /** + * Concatenates sampled chunks into channels + * Format is chunk[Left[], Right[]] + */ + readChannelData(chunks, channel = -1, maxChannels = 9) { + let channelLimit; + if (channel !== -1) { + if (chunks[0] && chunks[0].length - 1 < channel) { + throw new Error( + \`Channel \${channel} out of range: max \${chunks[0].length}\` + ); + } + channelLimit = channel + 1; + } else { + channel = 0; + channelLimit = Math.min(chunks[0] ? chunks[0].length : 1, maxChannels); + } + const channels = []; + for (let n = channel; n < channelLimit; n++) { + const length = chunks.reduce((sum, chunk) => { + return sum + chunk[n].length; + }, 0); + const buffers = chunks.map((chunk) => chunk[n]); + const result = new Float32Array(length); + let offset = 0; + for (let i = 0; i < buffers.length; i++) { + result.set(buffers[i], offset); + offset += buffers[i].length; + } + channels[n] = result; + } + return channels; + } + + /** + * Combines parallel audio data into correct format, + * channels[Left[], Right[]] to float32Array[LRLRLRLR...] + */ + formatAudioData(channels) { + if (channels.length === 1) { + // Simple case is only one channel + const float32Array = channels[0].slice(); + const meanValues = channels[0].slice(); + return { float32Array, meanValues }; + } else { + const float32Array = new Float32Array( + channels[0].length * channels.length + ); + const meanValues = new Float32Array(channels[0].length); + for (let i = 0; i < channels[0].length; i++) { + const offset = i * channels.length; + let meanValue = 0; + for (let n = 0; n < channels.length; n++) { + float32Array[offset + n] = channels[n][i]; + meanValue += channels[n][i]; + } + meanValues[i] = meanValue / channels.length; + } + return { float32Array, meanValues }; + } + } + + /** + * Converts 32-bit float data to 16-bit integers + */ + floatTo16BitPCM(float32Array) { + const buffer = new ArrayBuffer(float32Array.length * 2); + const view = new DataView(buffer); + let offset = 0; + for (let i = 0; i < float32Array.length; i++, offset += 2) { + let s = Math.max(-1, Math.min(1, float32Array[i])); + view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); + } + return buffer; + } + + /** + * Retrieves the most recent amplitude values from the audio stream + * @param {number} channel + */ + getValues(channel = -1) { + const channels = this.readChannelData(this.chunks, channel); + const { meanValues } = this.formatAudioData(channels); + return { meanValues, channels }; + } + + /** + * Exports chunks as an audio/wav file + */ + export() { + const channels = this.readChannelData(this.chunks); + const { float32Array, meanValues } = this.formatAudioData(channels); + const audioData = this.floatTo16BitPCM(float32Array); + return { + meanValues: meanValues, + audio: { + bitsPerSample: 16, + channels: channels, + data: audioData, + }, + }; + } + + receive(e) { + const { event, id } = e.data; + let receiptData = {}; + switch (event) { + case 'start': + this.recording = true; + break; + case 'stop': + this.recording = false; + break; + case 'clear': + this.initialize(); + break; + case 'export': + receiptData = this.export(); + break; + case 'read': + receiptData = this.getValues(); + break; + default: + break; + } + // Always send back receipt + this.port.postMessage({ event: 'receipt', id, data: receiptData }); + } + + sendChunk(chunk) { + const channels = this.readChannelData([chunk]); + const { float32Array, meanValues } = this.formatAudioData(channels); + const rawAudioData = this.floatTo16BitPCM(float32Array); + const monoAudioData = this.floatTo16BitPCM(meanValues); + this.port.postMessage({ + event: 'chunk', + data: { + mono: monoAudioData, + raw: rawAudioData, + }, + }); + } + + process(inputList, outputList, parameters) { + // Copy input to output (e.g. speakers) + // Note that this creates choppy sounds with Mac products + const sourceLimit = Math.min(inputList.length, outputList.length); + for (let inputNum = 0; inputNum < sourceLimit; inputNum++) { + const input = inputList[inputNum]; + const output = outputList[inputNum]; + const channelCount = Math.min(input.length, output.length); + for (let channelNum = 0; channelNum < channelCount; channelNum++) { + input[channelNum].forEach((sample, i) => { + output[channelNum][i] = sample; + }); + } + } + const inputs = inputList[0]; + // There's latency at the beginning of a stream before recording starts + // Make sure we actually receive audio data before we start storing chunks + let sliceIndex = 0; + if (!this.foundAudio) { + for (const channel of inputs) { + sliceIndex = 0; // reset for each channel + if (this.foundAudio) { + break; + } + if (channel) { + for (const value of channel) { + if (value !== 0) { + // find only one non-zero entry in any channel + this.foundAudio = true; + break; + } else { + sliceIndex++; + } + } + } + } + } + if (inputs && inputs[0] && this.foundAudio && this.recording) { + // We need to copy the TypedArray, because the \`process\` + // internals will reuse the same buffer to hold each input + const chunk = inputs.map((input) => input.slice(sliceIndex)); + this.chunks.push(chunk); + this.sendChunk(chunk); + } + return true; + } + } + + registerProcessor('audio_processor', AudioProcessor); + `; + var script2 = new Blob([AudioProcessorWorklet], { + type: "application/javascript" + }); + var src2 = URL.createObjectURL(script2); + var AudioProcessorSrc = src2; + + // lib/wav_recorder.js + var WavRecorder = class { + /** + * Create a new WavRecorder instance + * @param {{sampleRate?: number, outputToSpeakers?: boolean, debug?: boolean}} [options] + * @returns {WavRecorder} + */ + constructor({ + sampleRate = 44100, + outputToSpeakers = false, + debug = false + } = {}) { + this.scriptSrc = AudioProcessorSrc; + this.sampleRate = sampleRate; + this.outputToSpeakers = outputToSpeakers; + this.debug = !!debug; + this._deviceChangeCallback = null; + this._devices = []; + this.stream = null; + this.processor = null; + this.source = null; + this.node = null; + this.recording = false; + this._lastEventId = 0; + this.eventReceipts = {}; + this.eventTimeout = 5e3; + this._chunkProcessor = () => { + }; + this._chunkProcessorSize = void 0; + this._chunkProcessorBuffer = { + raw: new ArrayBuffer(0), + mono: new ArrayBuffer(0) + }; + } + /** + * Decodes audio data from multiple formats to a Blob, url, Float32Array and AudioBuffer + * @param {Blob|Float32Array|Int16Array|ArrayBuffer|number[]} audioData + * @param {number} sampleRate + * @param {number} fromSampleRate + * @returns {Promise} + */ + static async decode(audioData, sampleRate = 44100, fromSampleRate = -1) { + const context = new AudioContext({ sampleRate }); + let arrayBuffer; + let blob; + if (audioData instanceof Blob) { + if (fromSampleRate !== -1) { + throw new Error( + `Can not specify "fromSampleRate" when reading from Blob` + ); + } + blob = audioData; + arrayBuffer = await blob.arrayBuffer(); + } else if (audioData instanceof ArrayBuffer) { + if (fromSampleRate !== -1) { + throw new Error( + `Can not specify "fromSampleRate" when reading from ArrayBuffer` + ); + } + arrayBuffer = audioData; + blob = new Blob([arrayBuffer], { type: "audio/wav" }); + } else { + let float32Array; + let data; + if (audioData instanceof Int16Array) { + data = audioData; + float32Array = new Float32Array(audioData.length); + for (let i = 0; i < audioData.length; i++) { + float32Array[i] = audioData[i] / 32768; + } + } else if (audioData instanceof Float32Array) { + float32Array = audioData; + } else if (audioData instanceof Array) { + float32Array = new Float32Array(audioData); + } else { + throw new Error( + `"audioData" must be one of: Blob, Float32Arrray, Int16Array, ArrayBuffer, Array` + ); + } + if (fromSampleRate === -1) { + throw new Error( + `Must specify "fromSampleRate" when reading from Float32Array, In16Array or Array` + ); + } else if (fromSampleRate < 3e3) { + throw new Error(`Minimum "fromSampleRate" is 3000 (3kHz)`); + } + if (!data) { + data = WavPacker.floatTo16BitPCM(float32Array); + } + const audio = { + bitsPerSample: 16, + channels: [float32Array], + data + }; + const packer = new WavPacker(); + const result = packer.pack(fromSampleRate, audio); + blob = result.blob; + arrayBuffer = await blob.arrayBuffer(); + } + const audioBuffer = await context.decodeAudioData(arrayBuffer); + const values = audioBuffer.getChannelData(0); + const url = URL.createObjectURL(blob); + return { + blob, + url, + values, + audioBuffer + }; + } + /** + * Logs data in debug mode + * @param {...any} arguments + * @returns {true} + */ + log() { + if (this.debug) { + this.log(...arguments); + } + return true; + } + /** + * Retrieves the current sampleRate for the recorder + * @returns {number} + */ + getSampleRate() { + return this.sampleRate; + } + /** + * Retrieves the current status of the recording + * @returns {"ended"|"paused"|"recording"} + */ + getStatus() { + if (!this.processor) { + return "ended"; + } else if (!this.recording) { + return "paused"; + } else { + return "recording"; + } + } + /** + * Sends an event to the AudioWorklet + * @private + * @param {string} name + * @param {{[key: string]: any}} data + * @param {AudioWorkletNode} [_processor] + * @returns {Promise<{[key: string]: any}>} + */ + async _event(name, data = {}, _processor = null) { + _processor = _processor || this.processor; + if (!_processor) { + throw new Error("Can not send events without recording first"); + } + const message = { + event: name, + id: this._lastEventId++, + data + }; + _processor.port.postMessage(message); + const t0 = (/* @__PURE__ */ new Date()).valueOf(); + while (!this.eventReceipts[message.id]) { + if ((/* @__PURE__ */ new Date()).valueOf() - t0 > this.eventTimeout) { + throw new Error(`Timeout waiting for "${name}" event`); + } + await new Promise((res) => setTimeout(() => res(true), 1)); + } + const payload = this.eventReceipts[message.id]; + delete this.eventReceipts[message.id]; + return payload; + } + /** + * Sets device change callback, remove if callback provided is `null` + * @param {(Array): void|null} callback + * @returns {true} + */ + listenForDeviceChange(callback) { + if (callback === null && this._deviceChangeCallback) { + navigator.mediaDevices.removeEventListener( + "devicechange", + this._deviceChangeCallback + ); + this._deviceChangeCallback = null; + } else if (callback !== null) { + let lastId = 0; + let lastDevices = []; + const serializeDevices = (devices) => devices.map((d) => d.deviceId).sort().join(","); + const cb = async () => { + let id = ++lastId; + const devices = await this.listDevices(); + if (id === lastId) { + if (serializeDevices(lastDevices) !== serializeDevices(devices)) { + lastDevices = devices; + callback(devices.slice()); + } + } + }; + navigator.mediaDevices.addEventListener("devicechange", cb); + cb(); + this._deviceChangeCallback = cb; + } + return true; + } + /** + * Manually request permission to use the microphone + * @returns {Promise} + */ + async requestPermission() { + const permissionStatus = await navigator.permissions.query({ + name: "microphone" + }); + if (permissionStatus.state === "denied") { + window.alert("You must grant microphone access to use this feature."); + } else if (permissionStatus.state === "prompt") { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true + }); + const tracks = stream.getTracks(); + tracks.forEach((track) => track.stop()); + } catch (e) { + window.alert("You must grant microphone access to use this feature."); + } + } + return true; + } + /** + * List all eligible devices for recording, will request permission to use microphone + * @returns {Promise>} + */ + async listDevices() { + if (!navigator.mediaDevices || !("enumerateDevices" in navigator.mediaDevices)) { + throw new Error("Could not request user devices"); + } + await this.requestPermission(); + const devices = await navigator.mediaDevices.enumerateDevices(); + const audioDevices = devices.filter( + (device) => device.kind === "audioinput" + ); + const defaultDeviceIndex = audioDevices.findIndex( + (device) => device.deviceId === "default" + ); + const deviceList = []; + if (defaultDeviceIndex !== -1) { + let defaultDevice = audioDevices.splice(defaultDeviceIndex, 1)[0]; + let existingIndex = audioDevices.findIndex( + (device) => device.groupId === defaultDevice.groupId + ); + if (existingIndex !== -1) { + defaultDevice = audioDevices.splice(existingIndex, 1)[0]; + } + defaultDevice.default = true; + deviceList.push(defaultDevice); + } + return deviceList.concat(audioDevices); + } + /** + * Begins a recording session and requests microphone permissions if not already granted + * Microphone recording indicator will appear on browser tab but status will be "paused" + * @param {string} [deviceId] if no device provided, default device will be used + * @returns {Promise} + */ + async begin(deviceId) { + if (this.processor) { + throw new Error( + `Already connected: please call .end() to start a new session` + ); + } + if (!navigator.mediaDevices || !("getUserMedia" in navigator.mediaDevices)) { + throw new Error("Could not request user media"); + } + try { + const config = { audio: true }; + if (deviceId) { + config.audio = { deviceId: { exact: deviceId } }; + } + this.stream = await navigator.mediaDevices.getUserMedia(config); + } catch (err) { + throw new Error("Could not start media stream"); + } + const context = new AudioContext({ sampleRate: this.sampleRate }); + const source = context.createMediaStreamSource(this.stream); + try { + await context.audioWorklet.addModule(this.scriptSrc); + } catch (e) { + console.error(e); + throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`); + } + const processor = new AudioWorkletNode(context, "audio_processor"); + processor.port.onmessage = (e) => { + const { event, id, data } = e.data; + if (event === "receipt") { + this.eventReceipts[id] = data; + } else if (event === "chunk") { + if (this._chunkProcessorSize) { + const buffer = this._chunkProcessorBuffer; + this._chunkProcessorBuffer = { + raw: WavPacker.mergeBuffers(buffer.raw, data.raw), + mono: WavPacker.mergeBuffers(buffer.mono, data.mono) + }; + if (this._chunkProcessorBuffer.mono.byteLength >= this._chunkProcessorSize) { + this._chunkProcessor(this._chunkProcessorBuffer); + this._chunkProcessorBuffer = { + raw: new ArrayBuffer(0), + mono: new ArrayBuffer(0) + }; + } + } else { + this._chunkProcessor(data); + } + } + }; + const node = source.connect(processor); + const analyser = context.createAnalyser(); + analyser.fftSize = 8192; + analyser.smoothingTimeConstant = 0.1; + node.connect(analyser); + if (this.outputToSpeakers) { + console.warn( + "Warning: Output to speakers may affect sound quality,\nespecially due to system audio feedback preventative measures.\nuse only for debugging" + ); + analyser.connect(context.destination); + } + this.source = source; + this.node = node; + this.analyser = analyser; + this.processor = processor; + return true; + } + /** + * Gets the current frequency domain data from the recording track + * @param {"frequency"|"music"|"voice"} [analysisType] + * @param {number} [minDecibels] default -100 + * @param {number} [maxDecibels] default -30 + * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType} + */ + getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) { + if (!this.processor) { + throw new Error("Session ended: please call .begin() first"); + } + return AudioAnalysis.getFrequencies( + this.analyser, + this.sampleRate, + null, + analysisType, + minDecibels, + maxDecibels + ); + } + /** + * Pauses the recording + * Keeps microphone stream open but halts storage of audio + * @returns {Promise} + */ + async pause() { + if (!this.processor) { + throw new Error("Session ended: please call .begin() first"); + } else if (!this.recording) { + throw new Error("Already paused: please call .record() first"); + } + if (this._chunkProcessorBuffer.raw.byteLength) { + this._chunkProcessor(this._chunkProcessorBuffer); + } + this.log("Pausing ..."); + await this._event("stop"); + this.recording = false; + return true; + } + /** + * Start recording stream and storing to memory from the connected audio source + * @param {(data: { mono: Int16Array; raw: Int16Array }) => any} [chunkProcessor] + * @param {number} [chunkSize] chunkProcessor will not be triggered until this size threshold met in mono audio + * @returns {Promise} + */ + async record(chunkProcessor = () => { + }, chunkSize = 8192) { + if (!this.processor) { + throw new Error("Session ended: please call .begin() first"); + } else if (this.recording) { + throw new Error("Already recording: please call .pause() first"); + } else if (typeof chunkProcessor !== "function") { + throw new Error(`chunkProcessor must be a function`); + } + this._chunkProcessor = chunkProcessor; + this._chunkProcessorSize = chunkSize; + this._chunkProcessorBuffer = { + raw: new ArrayBuffer(0), + mono: new ArrayBuffer(0) + }; + this.log("Recording ..."); + await this._event("start"); + this.recording = true; + return true; + } + /** + * Clears the audio buffer, empties stored recording + * @returns {Promise} + */ + async clear() { + if (!this.processor) { + throw new Error("Session ended: please call .begin() first"); + } + await this._event("clear"); + return true; + } + /** + * Reads the current audio stream data + * @returns {Promise<{meanValues: Float32Array, channels: Array}>} + */ + async read() { + if (!this.processor) { + throw new Error("Session ended: please call .begin() first"); + } + this.log("Reading ..."); + const result = await this._event("read"); + return result; + } + /** + * Saves the current audio stream to a file + * @param {boolean} [force] Force saving while still recording + * @returns {Promise} + */ + async save(force = false) { + if (!this.processor) { + throw new Error("Session ended: please call .begin() first"); + } + if (!force && this.recording) { + throw new Error( + "Currently recording: please call .pause() first, or call .save(true) to force" + ); + } + this.log("Exporting ..."); + const exportData = await this._event("export"); + const packer = new WavPacker(); + const result = packer.pack(this.sampleRate, exportData.audio); + return result; + } + /** + * Ends the current recording session and saves the result + * @returns {Promise} + */ + async end() { + if (!this.processor) { + throw new Error("Session ended: please call .begin() first"); + } + const _processor = this.processor; + this.log("Stopping ..."); + await this._event("stop"); + this.recording = false; + const tracks = this.stream.getTracks(); + tracks.forEach((track) => track.stop()); + this.log("Exporting ..."); + const exportData = await this._event("export", {}, _processor); + this.processor.disconnect(); + this.source.disconnect(); + this.node.disconnect(); + this.analyser.disconnect(); + this.stream = null; + this.processor = null; + this.source = null; + this.node = null; + const packer = new WavPacker(); + const result = packer.pack(this.sampleRate, exportData.audio); + return result; + } + /** + * Performs a full cleanup of WavRecorder instance + * Stops actively listening via microphone and removes existing listeners + * @returns {Promise} + */ + async quit() { + this.listenForDeviceChange(null); + if (this.processor) { + await this.end(); + } + return true; + } + }; + globalThis.WavRecorder = WavRecorder; + })(); \ No newline at end of file diff --git a/notebook/templates/chat.html b/notebook/templates/chat.html new file mode 100644 index 0000000000..7a930a1f2e --- /dev/null +++ b/notebook/templates/chat.html @@ -0,0 +1,23 @@ + + + + + + Audio Chat + + + + + + +

Audio Chat

+

Ensure microphone and speaker access is enabled.

+ + From 0b023a137bfee93e45104376358885b9dbfe635c Mon Sep 17 00:00:00 2001 From: Davorin Rusevljan Date: Thu, 19 Dec 2024 23:26:16 +0100 Subject: [PATCH 44/51] websocket realtime wip(2) --- autogen/agentchat/realtime_agent/websocket_observer.py | 6 +++--- notebook/static/Audio.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/autogen/agentchat/realtime_agent/websocket_observer.py b/autogen/agentchat/realtime_agent/websocket_observer.py index a25137c7c3..ab0ab28fce 100644 --- a/autogen/agentchat/realtime_agent/websocket_observer.py +++ b/autogen/agentchat/realtime_agent/websocket_observer.py @@ -108,9 +108,9 @@ async def run(self): if data["event"] == "media": self.latest_media_timestamp = int(data["media"]["timestamp"]) audio_append = {"type": "input_audio_buffer.append", "audio": data["media"]["payload"]} - #await openai_ws.send(json.dumps(audio_append)) - audio_delta = {"event": "media", "streamSid": self.stream_sid, "media": {"payload": data["media"]["payload"]}} - await self.websocket.send_json(audio_delta) + await openai_ws.send(json.dumps(audio_append)) + #audio_delta = {"event": "media", "streamSid": self.stream_sid, "media": {"payload": data["media"]["payload"]}} + #await self.websocket.send_json(audio_delta) elif data["event"] == "start": self.stream_sid = data["start"]["streamSid"] print(f"Incoming stream has started {self.stream_sid}") diff --git a/notebook/static/Audio.js b/notebook/static/Audio.js index 7612f16e73..47f229b06e 100644 --- a/notebook/static/Audio.js +++ b/notebook/static/Audio.js @@ -107,7 +107,8 @@ export class Audio { this.stream = stream; console.log("Audio tracks", stream.getAudioTracks()) console.log('Sample rate :', stream.getAudioTracks()[0].getSettings().sampleRate) - this.inAudioContext = new (window.AudioContext || window.webkitAudioContext)(); + this.inAudioContext = new AudioContext({ sampleRate: 24000 }); + //this.inAudioContext = new (window.AudioContext || window.webkitAudioContext)(); // Create an AudioNode to capture the microphone stream const sourceNode = this.inAudioContext.createMediaStreamSource(stream); @@ -222,8 +223,7 @@ export class Audio { // Create an audio buffer from the Float32Array console.log("sample rate is ", this.outAudioContext.sampleRate) - //const audioBuffer = this.outAudioContext.createBuffer(1, audioData.length, 24000); - const audioBuffer = this.outAudioContext.createBuffer(1, audioData.length, 41000); + const audioBuffer = this.outAudioContext.createBuffer(1, audioData.length, 24000); audioBuffer.getChannelData(0).set(audioData); return audioBuffer; From 4ae306ef81f799364e379139dc0803a44305c756 Mon Sep 17 00:00:00 2001 From: Davorin Rusevljan Date: Thu, 19 Dec 2024 23:34:17 +0100 Subject: [PATCH 45/51] websocket realtime wip(3) --- .../realtime_agent/websocket_observer.py | 2 - notebook/static/Audio.js | 42 +- notebook/static/AudioCapture.js | 100 -- notebook/static/AudioPlayer.js | 101 -- notebook/static/wavtools.js | 1244 ----------------- 5 files changed, 1 insertion(+), 1488 deletions(-) delete mode 100644 notebook/static/AudioCapture.js delete mode 100644 notebook/static/AudioPlayer.js delete mode 100644 notebook/static/wavtools.js diff --git a/autogen/agentchat/realtime_agent/websocket_observer.py b/autogen/agentchat/realtime_agent/websocket_observer.py index ab0ab28fce..ebcb92852c 100644 --- a/autogen/agentchat/realtime_agent/websocket_observer.py +++ b/autogen/agentchat/realtime_agent/websocket_observer.py @@ -109,8 +109,6 @@ async def run(self): self.latest_media_timestamp = int(data["media"]["timestamp"]) audio_append = {"type": "input_audio_buffer.append", "audio": data["media"]["payload"]} await openai_ws.send(json.dumps(audio_append)) - #audio_delta = {"event": "media", "streamSid": self.stream_sid, "media": {"payload": data["media"]["payload"]}} - #await self.websocket.send_json(audio_delta) elif data["event"] == "start": self.stream_sid = data["start"]["streamSid"] print(f"Incoming stream has started {self.stream_sid}") diff --git a/notebook/static/Audio.js b/notebook/static/Audio.js index 47f229b06e..289be6cea0 100644 --- a/notebook/static/Audio.js +++ b/notebook/static/Audio.js @@ -1,4 +1,4 @@ -// AudioPlayer.js +// Audio.js export class Audio { constructor(webSocketUrl) { @@ -62,53 +62,15 @@ export class Audio { }; this.outAudioContext = new (window.AudioContext || window.webkitAudioContext)(); console.log("Audio player initialized."); - - /* - await wavRecorder.begin() - await wavRecorder.record((data) => { - try { - const { mono, raw } = data; - console.log("rec:", mono) - console.log("rec:", mono.length) - const pcmBuffer = new ArrayBuffer(mono.length * 2); // 2 bytes per sample - const pcmView = new DataView(pcmBuffer); - - for (let i = 0; i < mono.length; i++) { - pcmView.setInt16(i * 2, mono[i], true); // true means little-endian - } - - const byteArray = new Uint8Array(pcmView); // Create a Uint8Array view - const bufferString = String.fromCharCode(...byteArray); // convert each byte of the buffer to a character - const audioBase64String = btoa(bufferString); // Apply base64 - - - if (this.socket.readyState === WebSocket.OPEN) { - const audioMessage = { - 'event': "media", - 'media': { - 'timestamp': Date.now(), - 'payload': audioBase64String - } - } - console.log("sendin voice ..", audioMessage); - this.socket.send(JSON.stringify(audioMessage)); - } - } catch (ex) { - console.log("napaka", ex) - } - }); - */ // audio in // Get user media (microphone access) - const stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate:24000} }); this.stream = stream; console.log("Audio tracks", stream.getAudioTracks()) console.log('Sample rate :', stream.getAudioTracks()[0].getSettings().sampleRate) this.inAudioContext = new AudioContext({ sampleRate: 24000 }); - //this.inAudioContext = new (window.AudioContext || window.webkitAudioContext)(); // Create an AudioNode to capture the microphone stream const sourceNode = this.inAudioContext.createMediaStreamSource(stream); @@ -134,7 +96,6 @@ export class Audio { 'payload': audioBase64String } } - //console.log("sendin voice ..", audioMessage); this.socket.send(JSON.stringify(audioMessage)); } }; @@ -142,7 +103,6 @@ export class Audio { // Connect the source node to the processor node and the processor node to the destination (speakers) sourceNode.connect(this.processorNode); this.processorNode.connect(this.inAudioContext.destination); - console.log("Audio capture started."); } catch (err) { console.error("Error initializing audio player:", err); diff --git a/notebook/static/AudioCapture.js b/notebook/static/AudioCapture.js deleted file mode 100644 index a4532d19ec..0000000000 --- a/notebook/static/AudioCapture.js +++ /dev/null @@ -1,100 +0,0 @@ -export class AudioCapture { - constructor(webSocketUrl) { - this.webSocketUrl = webSocketUrl; - this.socket = null; - this.audioContext = null; - this.processorNode = null; - this.stream = null; - this.bufferSize = 8192; // Define the buffer size for capturing chunks - } - - // Initialize WebSocket and start capturing audio - async start() { - try { - // Initialize WebSocket connection - this.socket = new WebSocket(this.webSocketUrl); - - this.socket.onopen = () => { - console.log("WebSocket connected."); - }; - - this.socket.onclose = () => { - console.log("WebSocket disconnected."); - }; - - // Get user media (microphone access) - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - this.stream = stream; - this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); - - // Create an AudioNode to capture the microphone stream - const sourceNode = this.audioContext.createMediaStreamSource(stream); - - // Create a ScriptProcessorNode (or AudioWorkletProcessor for better performance) - this.processorNode = this.audioContext.createScriptProcessor(this.bufferSize, 1, 1); - - // Process audio data when available - this.processorNode.onaudioprocess = (event) => { - const inputBuffer = event.inputBuffer; - const outputBuffer = event.outputBuffer; - - // Extract PCM 16-bit data from input buffer (mono channel) - const audioData = this.extractPcm16Data(inputBuffer); - - // Send the PCM data over the WebSocket - if (this.socket.readyState === WebSocket.OPEN) { - this.socket.send(audioData); - } - }; - - // Connect the source node to the processor node and the processor node to the destination (speakers) - sourceNode.connect(this.processorNode); - this.processorNode.connect(this.audioContext.destination); - - console.log("Audio capture started."); - } catch (err) { - console.error("Error capturing audio:", err); - } - } - - // Stop capturing audio and close the WebSocket connection - stop() { - if (this.processorNode) { - this.processorNode.disconnect(); - } - if (this.audioContext) { - this.audioContext.close(); - } - if (this.socket) { - this.socket.close(); - } - if (this.stream) { - this.stream.getTracks().forEach(track => track.stop()); - } - - console.log("Audio capture stopped."); - } - - // Convert audio buffer to PCM 16-bit data - extractPcm16Data(buffer) { - const sampleRate = buffer.sampleRate; - const length = buffer.length; - const pcmData = new Int16Array(length); - - // Convert the float samples to PCM 16-bit (scaled between -32768 and 32767) - for (let i = 0; i < length; i++) { - pcmData[i] = Math.max(-32768, Math.min(32767, buffer.getChannelData(0)[i] * 32767)); - } - - // Convert Int16Array to a binary buffer (ArrayBuffer) - const pcmBuffer = new ArrayBuffer(pcmData.length * 2); // 2 bytes per sample - const pcmView = new DataView(pcmBuffer); - - for (let i = 0; i < pcmData.length; i++) { - pcmView.setInt16(i * 2, pcmData[i], true); // true means little-endian - } - - return pcmBuffer; - } - } - \ No newline at end of file diff --git a/notebook/static/AudioPlayer.js b/notebook/static/AudioPlayer.js deleted file mode 100644 index 319a768355..0000000000 --- a/notebook/static/AudioPlayer.js +++ /dev/null @@ -1,101 +0,0 @@ -// AudioPlayer.js - -export class AudioPlayer { - constructor(webSocketUrl) { - this.webSocketUrl = webSocketUrl; - this.socket = null; - this.audioContext = null; - this.sourceNode = null; - this.bufferQueue = []; // Queue to store audio buffers - this.isPlaying = false; // Flag to check if audio is playing - } - - // Initialize WebSocket and start receiving audio data - async start() { - try { - // Initialize WebSocket connection - this.socket = new WebSocket(this.webSocketUrl); - - this.socket.onopen = () => { - console.log("WebSocket connected."); - }; - - this.socket.onclose = () => { - console.log("WebSocket disconnected."); - }; - - this.socket.onmessage = async (event) => { - // Ensure the data is an ArrayBuffer, if it's a Blob, convert it - const pcmData = event.data instanceof ArrayBuffer ? event.data : await event.data.arrayBuffer(); - this.queuePcmData(pcmData); // Push the received data into the buffer queue - if (!this.isPlaying) { - this.playFromQueue(); // Start playing if not already playing - } - }; - - this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); - console.log("Audio player initialized."); - } catch (err) { - console.error("Error initializing audio player:", err); - } - } - - // Stop receiving and playing audio - stop() { - if (this.socket) { - this.socket.close(); - } - if (this.audioContext) { - this.audioContext.close(); - } - console.log("Audio player stopped."); - } - - // Queue PCM data for later playback - queuePcmData(pcmData) { - this.bufferQueue.push(pcmData); - } - - // Play audio from the queue - async playFromQueue() { - if (this.bufferQueue.length === 0) { - this.isPlaying = false; // No more data to play - return; - } - - this.isPlaying = true; - const pcmData = this.bufferQueue.shift(); // Get the next chunk from the queue - - // Convert PCM 16-bit data to ArrayBuffer - const audioBuffer = await this.decodePcm16Data(pcmData); - - // Create an audio source and play it - const source = this.audioContext.createBufferSource(); - source.buffer = audioBuffer; - source.connect(this.audioContext.destination); - source.onended = () => { - // Play the next chunk after the current one ends - this.playFromQueue(); - }; - source.start(); - } - - // Decode PCM 16-bit data into AudioBuffer - async decodePcm16Data(pcmData) { - const audioData = new Float32Array(pcmData.byteLength / 2); - - // Convert PCM 16-bit to Float32Array - const dataView = new DataView(pcmData); - for (let i = 0; i < audioData.length; i++) { - const pcm16 = dataView.getInt16(i * 2, true); // true means little-endian - audioData[i] = pcm16 / 32768; // Convert to normalized float (-1 to 1) - } - - // Create an audio buffer from the Float32Array - const audioBuffer = this.audioContext.createBuffer(1, audioData.length, this.audioContext.sampleRate); - audioBuffer.getChannelData(0).set(audioData); - - return audioBuffer; - } - } - \ No newline at end of file diff --git a/notebook/static/wavtools.js b/notebook/static/wavtools.js deleted file mode 100644 index 9d21d048ea..0000000000 --- a/notebook/static/wavtools.js +++ /dev/null @@ -1,1244 +0,0 @@ -(() => { - // lib/wav_packer.js - var WavPacker = class { - /** - * Converts Float32Array of amplitude data to ArrayBuffer in Int16Array format - * @param {Float32Array} float32Array - * @returns {ArrayBuffer} - */ - static floatTo16BitPCM(float32Array) { - const buffer = new ArrayBuffer(float32Array.length * 2); - const view = new DataView(buffer); - let offset = 0; - for (let i = 0; i < float32Array.length; i++, offset += 2) { - let s = Math.max(-1, Math.min(1, float32Array[i])); - view.setInt16(offset, s < 0 ? s * 32768 : s * 32767, true); - } - return buffer; - } - /** - * Concatenates two ArrayBuffers - * @param {ArrayBuffer} leftBuffer - * @param {ArrayBuffer} rightBuffer - * @returns {ArrayBuffer} - */ - static mergeBuffers(leftBuffer, rightBuffer) { - const tmpArray = new Uint8Array( - leftBuffer.byteLength + rightBuffer.byteLength - ); - tmpArray.set(new Uint8Array(leftBuffer), 0); - tmpArray.set(new Uint8Array(rightBuffer), leftBuffer.byteLength); - return tmpArray.buffer; - } - /** - * Packs data into an Int16 format - * @private - * @param {number} size 0 = 1x Int16, 1 = 2x Int16 - * @param {number} arg value to pack - * @returns - */ - _packData(size, arg) { - return [ - new Uint8Array([arg, arg >> 8]), - new Uint8Array([arg, arg >> 8, arg >> 16, arg >> 24]) - ][size]; - } - /** - * Packs audio into "audio/wav" Blob - * @param {number} sampleRate - * @param {{bitsPerSample: number, channels: Array, data: Int16Array}} audio - * @returns {WavPackerAudioType} - */ - pack(sampleRate, audio) { - if (!audio?.bitsPerSample) { - throw new Error(`Missing "bitsPerSample"`); - } else if (!audio?.channels) { - throw new Error(`Missing "channels"`); - } else if (!audio?.data) { - throw new Error(`Missing "data"`); - } - const { bitsPerSample, channels, data } = audio; - const output = [ - // Header - "RIFF", - this._packData( - 1, - 4 + (8 + 24) + (8 + 8) - /* chunk 2 length */ - ), - // Length - "WAVE", - // chunk 1 - "fmt ", - // Sub-chunk identifier - this._packData(1, 16), - // Chunk length - this._packData(0, 1), - // Audio format (1 is linear quantization) - this._packData(0, channels.length), - this._packData(1, sampleRate), - this._packData(1, sampleRate * channels.length * bitsPerSample / 8), - // Byte rate - this._packData(0, channels.length * bitsPerSample / 8), - this._packData(0, bitsPerSample), - // chunk 2 - "data", - // Sub-chunk identifier - this._packData( - 1, - channels[0].length * channels.length * bitsPerSample / 8 - ), - // Chunk length - data - ]; - const blob = new Blob(output, { type: "audio/mpeg" }); - const url = URL.createObjectURL(blob); - return { - blob, - url, - channelCount: channels.length, - sampleRate, - duration: data.byteLength / (channels.length * sampleRate * 2) - }; - } - }; - globalThis.WavPacker = WavPacker; - - // lib/analysis/constants.js - var octave8Frequencies = [ - 4186.01, - 4434.92, - 4698.63, - 4978.03, - 5274.04, - 5587.65, - 5919.91, - 6271.93, - 6644.88, - 7040, - 7458.62, - 7902.13 - ]; - var octave8FrequencyLabels = [ - "C", - "C#", - "D", - "D#", - "E", - "F", - "F#", - "G", - "G#", - "A", - "A#", - "B" - ]; - var noteFrequencies = []; - var noteFrequencyLabels = []; - for (let i = 1; i <= 8; i++) { - for (let f = 0; f < octave8Frequencies.length; f++) { - const freq = octave8Frequencies[f]; - noteFrequencies.push(freq / Math.pow(2, 8 - i)); - noteFrequencyLabels.push(octave8FrequencyLabels[f] + i); - } - } - var voiceFrequencyRange = [32, 2e3]; - var voiceFrequencies = noteFrequencies.filter((_, i) => { - return noteFrequencies[i] > voiceFrequencyRange[0] && noteFrequencies[i] < voiceFrequencyRange[1]; - }); - var voiceFrequencyLabels = noteFrequencyLabels.filter((_, i) => { - return noteFrequencies[i] > voiceFrequencyRange[0] && noteFrequencies[i] < voiceFrequencyRange[1]; - }); - - // lib/analysis/audio_analysis.js - var AudioAnalysis = class _AudioAnalysis { - /** - * Retrieves frequency domain data from an AnalyserNode adjusted to a decibel range - * returns human-readable formatting and labels - * @param {AnalyserNode} analyser - * @param {number} sampleRate - * @param {Float32Array} [fftResult] - * @param {"frequency"|"music"|"voice"} [analysisType] - * @param {number} [minDecibels] default -100 - * @param {number} [maxDecibels] default -30 - * @returns {AudioAnalysisOutputType} - */ - static getFrequencies(analyser, sampleRate, fftResult, analysisType = "frequency", minDecibels = -100, maxDecibels = -30) { - if (!fftResult) { - fftResult = new Float32Array(analyser.frequencyBinCount); - analyser.getFloatFrequencyData(fftResult); - } - const nyquistFrequency = sampleRate / 2; - const frequencyStep = 1 / fftResult.length * nyquistFrequency; - let outputValues; - let frequencies; - let labels; - if (analysisType === "music" || analysisType === "voice") { - const useFrequencies = analysisType === "voice" ? voiceFrequencies : noteFrequencies; - const aggregateOutput = Array(useFrequencies.length).fill(minDecibels); - for (let i = 0; i < fftResult.length; i++) { - const frequency = i * frequencyStep; - const amplitude = fftResult[i]; - for (let n = useFrequencies.length - 1; n >= 0; n--) { - if (frequency > useFrequencies[n]) { - aggregateOutput[n] = Math.max(aggregateOutput[n], amplitude); - break; - } - } - } - outputValues = aggregateOutput; - frequencies = analysisType === "voice" ? voiceFrequencies : noteFrequencies; - labels = analysisType === "voice" ? voiceFrequencyLabels : noteFrequencyLabels; - } else { - outputValues = Array.from(fftResult); - frequencies = outputValues.map((_, i) => frequencyStep * i); - labels = frequencies.map((f) => `${f.toFixed(2)} Hz`); - } - const normalizedOutput = outputValues.map((v) => { - return Math.max( - 0, - Math.min((v - minDecibels) / (maxDecibels - minDecibels), 1) - ); - }); - const values = new Float32Array(normalizedOutput); - return { - values, - frequencies, - labels - }; - } - /** - * Creates a new AudioAnalysis instance for an HTMLAudioElement - * @param {HTMLAudioElement} audioElement - * @param {AudioBuffer|null} [audioBuffer] If provided, will cache all frequency domain data from the buffer - * @returns {AudioAnalysis} - */ - constructor(audioElement, audioBuffer = null) { - this.fftResults = []; - if (audioBuffer) { - const { length, sampleRate } = audioBuffer; - const offlineAudioContext = new OfflineAudioContext({ - length, - sampleRate - }); - const source = offlineAudioContext.createBufferSource(); - source.buffer = audioBuffer; - const analyser = offlineAudioContext.createAnalyser(); - analyser.fftSize = 8192; - analyser.smoothingTimeConstant = 0.1; - source.connect(analyser); - const renderQuantumInSeconds = 1 / 60; - const durationInSeconds = length / sampleRate; - const analyze = (index) => { - const suspendTime = renderQuantumInSeconds * index; - if (suspendTime < durationInSeconds) { - offlineAudioContext.suspend(suspendTime).then(() => { - const fftResult = new Float32Array(analyser.frequencyBinCount); - analyser.getFloatFrequencyData(fftResult); - this.fftResults.push(fftResult); - analyze(index + 1); - }); - } - if (index === 1) { - offlineAudioContext.startRendering(); - } else { - offlineAudioContext.resume(); - } - }; - source.start(0); - analyze(1); - this.audio = audioElement; - this.context = offlineAudioContext; - this.analyser = analyser; - this.sampleRate = sampleRate; - this.audioBuffer = audioBuffer; - } else { - const audioContext = new AudioContext(); - const track = audioContext.createMediaElementSource(audioElement); - const analyser = audioContext.createAnalyser(); - analyser.fftSize = 8192; - analyser.smoothingTimeConstant = 0.1; - track.connect(analyser); - analyser.connect(audioContext.destination); - this.audio = audioElement; - this.context = audioContext; - this.analyser = analyser; - this.sampleRate = this.context.sampleRate; - this.audioBuffer = null; - } - } - /** - * Gets the current frequency domain data from the playing audio track - * @param {"frequency"|"music"|"voice"} [analysisType] - * @param {number} [minDecibels] default -100 - * @param {number} [maxDecibels] default -30 - * @returns {AudioAnalysisOutputType} - */ - getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) { - let fftResult = null; - if (this.audioBuffer && this.fftResults.length) { - const pct = this.audio.currentTime / this.audio.duration; - const index = Math.min( - pct * this.fftResults.length | 0, - this.fftResults.length - 1 - ); - fftResult = this.fftResults[index]; - } - return _AudioAnalysis.getFrequencies( - this.analyser, - this.sampleRate, - fftResult, - analysisType, - minDecibels, - maxDecibels - ); - } - /** - * Resume the internal AudioContext if it was suspended due to the lack of - * user interaction when the AudioAnalysis was instantiated. - * @returns {Promise} - */ - async resumeIfSuspended() { - if (this.context.state === "suspended") { - await this.context.resume(); - } - return true; - } - }; - globalThis.AudioAnalysis = AudioAnalysis; - - // lib/worklets/stream_processor.js - var StreamProcessorWorklet = ` - class StreamProcessor extends AudioWorkletProcessor { - constructor() { - super(); - this.hasStarted = false; - this.hasInterrupted = false; - this.outputBuffers = []; - this.bufferLength = 128; - this.write = { buffer: new Float32Array(this.bufferLength), trackId: null }; - this.writeOffset = 0; - this.trackSampleOffsets = {}; - this.port.onmessage = (event) => { - if (event.data) { - const payload = event.data; - if (payload.event === 'write') { - const int16Array = payload.buffer; - const float32Array = new Float32Array(int16Array.length); - for (let i = 0; i < int16Array.length; i++) { - float32Array[i] = int16Array[i] / 0x8000; // Convert Int16 to Float32 - } - this.writeData(float32Array, payload.trackId); - } else if ( - payload.event === 'offset' || - payload.event === 'interrupt' - ) { - const requestId = payload.requestId; - const trackId = this.write.trackId; - const offset = this.trackSampleOffsets[trackId] || 0; - this.port.postMessage({ - event: 'offset', - requestId, - trackId, - offset, - }); - if (payload.event === 'interrupt') { - this.hasInterrupted = true; - } - } else { - throw new Error(\`Unhandled event "\${payload.event}"\`); - } - } - }; - } - - writeData(float32Array, trackId = null) { - let { buffer } = this.write; - let offset = this.writeOffset; - for (let i = 0; i < float32Array.length; i++) { - buffer[offset++] = float32Array[i]; - if (offset >= buffer.length) { - this.outputBuffers.push(this.write); - this.write = { buffer: new Float32Array(this.bufferLength), trackId }; - buffer = this.write.buffer; - offset = 0; - } - } - this.writeOffset = offset; - return true; - } - - process(inputs, outputs, parameters) { - const output = outputs[0]; - const outputChannelData = output[0]; - const outputBuffers = this.outputBuffers; - if (this.hasInterrupted) { - this.port.postMessage({ event: 'stop' }); - return false; - } else if (outputBuffers.length) { - this.hasStarted = true; - const { buffer, trackId } = outputBuffers.shift(); - for (let i = 0; i < outputChannelData.length; i++) { - outputChannelData[i] = buffer[i] || 0; - } - if (trackId) { - this.trackSampleOffsets[trackId] = - this.trackSampleOffsets[trackId] || 0; - this.trackSampleOffsets[trackId] += buffer.length; - } - return true; - } else if (this.hasStarted) { - this.port.postMessage({ event: 'stop' }); - return false; - } else { - return true; - } - } - } - - registerProcessor('stream_processor', StreamProcessor); - `; - var script = new Blob([StreamProcessorWorklet], { - type: "application/javascript" - }); - var src = URL.createObjectURL(script); - var StreamProcessorSrc = src; - - // lib/wav_stream_player.js - var WavStreamPlayer = class { - /** - * Creates a new WavStreamPlayer instance - * @param {{sampleRate?: number}} options - * @returns {WavStreamPlayer} - */ - constructor({ sampleRate = 44100 } = {}) { - this.scriptSrc = StreamProcessorSrc; - this.sampleRate = sampleRate; - this.context = null; - this.stream = null; - this.analyser = null; - this.trackSampleOffsets = {}; - this.interruptedTrackIds = {}; - } - /** - * Connects the audio context and enables output to speakers - * @returns {Promise} - */ - async connect() { - this.context = new AudioContext({ sampleRate: this.sampleRate }); - if (this.context.state === "suspended") { - await this.context.resume(); - } - try { - await this.context.audioWorklet.addModule(this.scriptSrc); - } catch (e) { - console.error(e); - throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`); - } - const analyser = this.context.createAnalyser(); - analyser.fftSize = 8192; - analyser.smoothingTimeConstant = 0.1; - this.analyser = analyser; - return true; - } - /** - * Gets the current frequency domain data from the playing track - * @param {"frequency"|"music"|"voice"} [analysisType] - * @param {number} [minDecibels] default -100 - * @param {number} [maxDecibels] default -30 - * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType} - */ - getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) { - if (!this.analyser) { - throw new Error("Not connected, please call .connect() first"); - } - return AudioAnalysis.getFrequencies( - this.analyser, - this.sampleRate, - null, - analysisType, - minDecibels, - maxDecibels - ); - } - /** - * Starts audio streaming - * @private - * @returns {Promise} - */ - _start() { - const streamNode = new AudioWorkletNode(this.context, "stream_processor"); - streamNode.connect(this.context.destination); - streamNode.port.onmessage = (e) => { - const { event } = e.data; - if (event === "stop") { - streamNode.disconnect(); - this.stream = null; - } else if (event === "offset") { - const { requestId, trackId, offset } = e.data; - const currentTime = offset / this.sampleRate; - this.trackSampleOffsets[requestId] = { trackId, offset, currentTime }; - } - }; - this.analyser.disconnect(); - streamNode.connect(this.analyser); - this.stream = streamNode; - return true; - } - /** - * Adds 16BitPCM data to the currently playing audio stream - * You can add chunks beyond the current play point and they will be queued for play - * @param {ArrayBuffer|Int16Array} arrayBuffer - * @param {string} [trackId] - * @returns {Int16Array} - */ - add16BitPCM(arrayBuffer, trackId = "default") { - if (typeof trackId !== "string") { - throw new Error(`trackId must be a string`); - } else if (this.interruptedTrackIds[trackId]) { - return; - } - if (!this.stream) { - this._start(); - } - let buffer; - if (arrayBuffer instanceof Int16Array) { - buffer = arrayBuffer; - } else if (arrayBuffer instanceof ArrayBuffer) { - buffer = new Int16Array(arrayBuffer); - } else { - throw new Error(`argument must be Int16Array or ArrayBuffer`); - } - this.stream.port.postMessage({ event: "write", buffer, trackId }); - return buffer; - } - /** - * Gets the offset (sample count) of the currently playing stream - * @param {boolean} [interrupt] - * @returns {{trackId: string|null, offset: number, currentTime: number}} - */ - async getTrackSampleOffset(interrupt = false) { - if (!this.stream) { - return null; - } - const requestId = crypto.randomUUID(); - this.stream.port.postMessage({ - event: interrupt ? "interrupt" : "offset", - requestId - }); - let trackSampleOffset; - while (!trackSampleOffset) { - trackSampleOffset = this.trackSampleOffsets[requestId]; - await new Promise((r) => setTimeout(() => r(), 1)); - } - const { trackId } = trackSampleOffset; - if (interrupt && trackId) { - this.interruptedTrackIds[trackId] = true; - } - return trackSampleOffset; - } - /** - * Strips the current stream and returns the sample offset of the audio - * @param {boolean} [interrupt] - * @returns {{trackId: string|null, offset: number, currentTime: number}} - */ - async interrupt() { - return this.getTrackSampleOffset(true); - } - }; - globalThis.WavStreamPlayer = WavStreamPlayer; - - // lib/worklets/audio_processor.js - var AudioProcessorWorklet = ` - class AudioProcessor extends AudioWorkletProcessor { - - constructor() { - super(); - this.port.onmessage = this.receive.bind(this); - this.initialize(); - } - - initialize() { - this.foundAudio = false; - this.recording = false; - this.chunks = []; - } - - /** - * Concatenates sampled chunks into channels - * Format is chunk[Left[], Right[]] - */ - readChannelData(chunks, channel = -1, maxChannels = 9) { - let channelLimit; - if (channel !== -1) { - if (chunks[0] && chunks[0].length - 1 < channel) { - throw new Error( - \`Channel \${channel} out of range: max \${chunks[0].length}\` - ); - } - channelLimit = channel + 1; - } else { - channel = 0; - channelLimit = Math.min(chunks[0] ? chunks[0].length : 1, maxChannels); - } - const channels = []; - for (let n = channel; n < channelLimit; n++) { - const length = chunks.reduce((sum, chunk) => { - return sum + chunk[n].length; - }, 0); - const buffers = chunks.map((chunk) => chunk[n]); - const result = new Float32Array(length); - let offset = 0; - for (let i = 0; i < buffers.length; i++) { - result.set(buffers[i], offset); - offset += buffers[i].length; - } - channels[n] = result; - } - return channels; - } - - /** - * Combines parallel audio data into correct format, - * channels[Left[], Right[]] to float32Array[LRLRLRLR...] - */ - formatAudioData(channels) { - if (channels.length === 1) { - // Simple case is only one channel - const float32Array = channels[0].slice(); - const meanValues = channels[0].slice(); - return { float32Array, meanValues }; - } else { - const float32Array = new Float32Array( - channels[0].length * channels.length - ); - const meanValues = new Float32Array(channels[0].length); - for (let i = 0; i < channels[0].length; i++) { - const offset = i * channels.length; - let meanValue = 0; - for (let n = 0; n < channels.length; n++) { - float32Array[offset + n] = channels[n][i]; - meanValue += channels[n][i]; - } - meanValues[i] = meanValue / channels.length; - } - return { float32Array, meanValues }; - } - } - - /** - * Converts 32-bit float data to 16-bit integers - */ - floatTo16BitPCM(float32Array) { - const buffer = new ArrayBuffer(float32Array.length * 2); - const view = new DataView(buffer); - let offset = 0; - for (let i = 0; i < float32Array.length; i++, offset += 2) { - let s = Math.max(-1, Math.min(1, float32Array[i])); - view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); - } - return buffer; - } - - /** - * Retrieves the most recent amplitude values from the audio stream - * @param {number} channel - */ - getValues(channel = -1) { - const channels = this.readChannelData(this.chunks, channel); - const { meanValues } = this.formatAudioData(channels); - return { meanValues, channels }; - } - - /** - * Exports chunks as an audio/wav file - */ - export() { - const channels = this.readChannelData(this.chunks); - const { float32Array, meanValues } = this.formatAudioData(channels); - const audioData = this.floatTo16BitPCM(float32Array); - return { - meanValues: meanValues, - audio: { - bitsPerSample: 16, - channels: channels, - data: audioData, - }, - }; - } - - receive(e) { - const { event, id } = e.data; - let receiptData = {}; - switch (event) { - case 'start': - this.recording = true; - break; - case 'stop': - this.recording = false; - break; - case 'clear': - this.initialize(); - break; - case 'export': - receiptData = this.export(); - break; - case 'read': - receiptData = this.getValues(); - break; - default: - break; - } - // Always send back receipt - this.port.postMessage({ event: 'receipt', id, data: receiptData }); - } - - sendChunk(chunk) { - const channels = this.readChannelData([chunk]); - const { float32Array, meanValues } = this.formatAudioData(channels); - const rawAudioData = this.floatTo16BitPCM(float32Array); - const monoAudioData = this.floatTo16BitPCM(meanValues); - this.port.postMessage({ - event: 'chunk', - data: { - mono: monoAudioData, - raw: rawAudioData, - }, - }); - } - - process(inputList, outputList, parameters) { - // Copy input to output (e.g. speakers) - // Note that this creates choppy sounds with Mac products - const sourceLimit = Math.min(inputList.length, outputList.length); - for (let inputNum = 0; inputNum < sourceLimit; inputNum++) { - const input = inputList[inputNum]; - const output = outputList[inputNum]; - const channelCount = Math.min(input.length, output.length); - for (let channelNum = 0; channelNum < channelCount; channelNum++) { - input[channelNum].forEach((sample, i) => { - output[channelNum][i] = sample; - }); - } - } - const inputs = inputList[0]; - // There's latency at the beginning of a stream before recording starts - // Make sure we actually receive audio data before we start storing chunks - let sliceIndex = 0; - if (!this.foundAudio) { - for (const channel of inputs) { - sliceIndex = 0; // reset for each channel - if (this.foundAudio) { - break; - } - if (channel) { - for (const value of channel) { - if (value !== 0) { - // find only one non-zero entry in any channel - this.foundAudio = true; - break; - } else { - sliceIndex++; - } - } - } - } - } - if (inputs && inputs[0] && this.foundAudio && this.recording) { - // We need to copy the TypedArray, because the \`process\` - // internals will reuse the same buffer to hold each input - const chunk = inputs.map((input) => input.slice(sliceIndex)); - this.chunks.push(chunk); - this.sendChunk(chunk); - } - return true; - } - } - - registerProcessor('audio_processor', AudioProcessor); - `; - var script2 = new Blob([AudioProcessorWorklet], { - type: "application/javascript" - }); - var src2 = URL.createObjectURL(script2); - var AudioProcessorSrc = src2; - - // lib/wav_recorder.js - var WavRecorder = class { - /** - * Create a new WavRecorder instance - * @param {{sampleRate?: number, outputToSpeakers?: boolean, debug?: boolean}} [options] - * @returns {WavRecorder} - */ - constructor({ - sampleRate = 44100, - outputToSpeakers = false, - debug = false - } = {}) { - this.scriptSrc = AudioProcessorSrc; - this.sampleRate = sampleRate; - this.outputToSpeakers = outputToSpeakers; - this.debug = !!debug; - this._deviceChangeCallback = null; - this._devices = []; - this.stream = null; - this.processor = null; - this.source = null; - this.node = null; - this.recording = false; - this._lastEventId = 0; - this.eventReceipts = {}; - this.eventTimeout = 5e3; - this._chunkProcessor = () => { - }; - this._chunkProcessorSize = void 0; - this._chunkProcessorBuffer = { - raw: new ArrayBuffer(0), - mono: new ArrayBuffer(0) - }; - } - /** - * Decodes audio data from multiple formats to a Blob, url, Float32Array and AudioBuffer - * @param {Blob|Float32Array|Int16Array|ArrayBuffer|number[]} audioData - * @param {number} sampleRate - * @param {number} fromSampleRate - * @returns {Promise} - */ - static async decode(audioData, sampleRate = 44100, fromSampleRate = -1) { - const context = new AudioContext({ sampleRate }); - let arrayBuffer; - let blob; - if (audioData instanceof Blob) { - if (fromSampleRate !== -1) { - throw new Error( - `Can not specify "fromSampleRate" when reading from Blob` - ); - } - blob = audioData; - arrayBuffer = await blob.arrayBuffer(); - } else if (audioData instanceof ArrayBuffer) { - if (fromSampleRate !== -1) { - throw new Error( - `Can not specify "fromSampleRate" when reading from ArrayBuffer` - ); - } - arrayBuffer = audioData; - blob = new Blob([arrayBuffer], { type: "audio/wav" }); - } else { - let float32Array; - let data; - if (audioData instanceof Int16Array) { - data = audioData; - float32Array = new Float32Array(audioData.length); - for (let i = 0; i < audioData.length; i++) { - float32Array[i] = audioData[i] / 32768; - } - } else if (audioData instanceof Float32Array) { - float32Array = audioData; - } else if (audioData instanceof Array) { - float32Array = new Float32Array(audioData); - } else { - throw new Error( - `"audioData" must be one of: Blob, Float32Arrray, Int16Array, ArrayBuffer, Array` - ); - } - if (fromSampleRate === -1) { - throw new Error( - `Must specify "fromSampleRate" when reading from Float32Array, In16Array or Array` - ); - } else if (fromSampleRate < 3e3) { - throw new Error(`Minimum "fromSampleRate" is 3000 (3kHz)`); - } - if (!data) { - data = WavPacker.floatTo16BitPCM(float32Array); - } - const audio = { - bitsPerSample: 16, - channels: [float32Array], - data - }; - const packer = new WavPacker(); - const result = packer.pack(fromSampleRate, audio); - blob = result.blob; - arrayBuffer = await blob.arrayBuffer(); - } - const audioBuffer = await context.decodeAudioData(arrayBuffer); - const values = audioBuffer.getChannelData(0); - const url = URL.createObjectURL(blob); - return { - blob, - url, - values, - audioBuffer - }; - } - /** - * Logs data in debug mode - * @param {...any} arguments - * @returns {true} - */ - log() { - if (this.debug) { - this.log(...arguments); - } - return true; - } - /** - * Retrieves the current sampleRate for the recorder - * @returns {number} - */ - getSampleRate() { - return this.sampleRate; - } - /** - * Retrieves the current status of the recording - * @returns {"ended"|"paused"|"recording"} - */ - getStatus() { - if (!this.processor) { - return "ended"; - } else if (!this.recording) { - return "paused"; - } else { - return "recording"; - } - } - /** - * Sends an event to the AudioWorklet - * @private - * @param {string} name - * @param {{[key: string]: any}} data - * @param {AudioWorkletNode} [_processor] - * @returns {Promise<{[key: string]: any}>} - */ - async _event(name, data = {}, _processor = null) { - _processor = _processor || this.processor; - if (!_processor) { - throw new Error("Can not send events without recording first"); - } - const message = { - event: name, - id: this._lastEventId++, - data - }; - _processor.port.postMessage(message); - const t0 = (/* @__PURE__ */ new Date()).valueOf(); - while (!this.eventReceipts[message.id]) { - if ((/* @__PURE__ */ new Date()).valueOf() - t0 > this.eventTimeout) { - throw new Error(`Timeout waiting for "${name}" event`); - } - await new Promise((res) => setTimeout(() => res(true), 1)); - } - const payload = this.eventReceipts[message.id]; - delete this.eventReceipts[message.id]; - return payload; - } - /** - * Sets device change callback, remove if callback provided is `null` - * @param {(Array): void|null} callback - * @returns {true} - */ - listenForDeviceChange(callback) { - if (callback === null && this._deviceChangeCallback) { - navigator.mediaDevices.removeEventListener( - "devicechange", - this._deviceChangeCallback - ); - this._deviceChangeCallback = null; - } else if (callback !== null) { - let lastId = 0; - let lastDevices = []; - const serializeDevices = (devices) => devices.map((d) => d.deviceId).sort().join(","); - const cb = async () => { - let id = ++lastId; - const devices = await this.listDevices(); - if (id === lastId) { - if (serializeDevices(lastDevices) !== serializeDevices(devices)) { - lastDevices = devices; - callback(devices.slice()); - } - } - }; - navigator.mediaDevices.addEventListener("devicechange", cb); - cb(); - this._deviceChangeCallback = cb; - } - return true; - } - /** - * Manually request permission to use the microphone - * @returns {Promise} - */ - async requestPermission() { - const permissionStatus = await navigator.permissions.query({ - name: "microphone" - }); - if (permissionStatus.state === "denied") { - window.alert("You must grant microphone access to use this feature."); - } else if (permissionStatus.state === "prompt") { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - audio: true - }); - const tracks = stream.getTracks(); - tracks.forEach((track) => track.stop()); - } catch (e) { - window.alert("You must grant microphone access to use this feature."); - } - } - return true; - } - /** - * List all eligible devices for recording, will request permission to use microphone - * @returns {Promise>} - */ - async listDevices() { - if (!navigator.mediaDevices || !("enumerateDevices" in navigator.mediaDevices)) { - throw new Error("Could not request user devices"); - } - await this.requestPermission(); - const devices = await navigator.mediaDevices.enumerateDevices(); - const audioDevices = devices.filter( - (device) => device.kind === "audioinput" - ); - const defaultDeviceIndex = audioDevices.findIndex( - (device) => device.deviceId === "default" - ); - const deviceList = []; - if (defaultDeviceIndex !== -1) { - let defaultDevice = audioDevices.splice(defaultDeviceIndex, 1)[0]; - let existingIndex = audioDevices.findIndex( - (device) => device.groupId === defaultDevice.groupId - ); - if (existingIndex !== -1) { - defaultDevice = audioDevices.splice(existingIndex, 1)[0]; - } - defaultDevice.default = true; - deviceList.push(defaultDevice); - } - return deviceList.concat(audioDevices); - } - /** - * Begins a recording session and requests microphone permissions if not already granted - * Microphone recording indicator will appear on browser tab but status will be "paused" - * @param {string} [deviceId] if no device provided, default device will be used - * @returns {Promise} - */ - async begin(deviceId) { - if (this.processor) { - throw new Error( - `Already connected: please call .end() to start a new session` - ); - } - if (!navigator.mediaDevices || !("getUserMedia" in navigator.mediaDevices)) { - throw new Error("Could not request user media"); - } - try { - const config = { audio: true }; - if (deviceId) { - config.audio = { deviceId: { exact: deviceId } }; - } - this.stream = await navigator.mediaDevices.getUserMedia(config); - } catch (err) { - throw new Error("Could not start media stream"); - } - const context = new AudioContext({ sampleRate: this.sampleRate }); - const source = context.createMediaStreamSource(this.stream); - try { - await context.audioWorklet.addModule(this.scriptSrc); - } catch (e) { - console.error(e); - throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`); - } - const processor = new AudioWorkletNode(context, "audio_processor"); - processor.port.onmessage = (e) => { - const { event, id, data } = e.data; - if (event === "receipt") { - this.eventReceipts[id] = data; - } else if (event === "chunk") { - if (this._chunkProcessorSize) { - const buffer = this._chunkProcessorBuffer; - this._chunkProcessorBuffer = { - raw: WavPacker.mergeBuffers(buffer.raw, data.raw), - mono: WavPacker.mergeBuffers(buffer.mono, data.mono) - }; - if (this._chunkProcessorBuffer.mono.byteLength >= this._chunkProcessorSize) { - this._chunkProcessor(this._chunkProcessorBuffer); - this._chunkProcessorBuffer = { - raw: new ArrayBuffer(0), - mono: new ArrayBuffer(0) - }; - } - } else { - this._chunkProcessor(data); - } - } - }; - const node = source.connect(processor); - const analyser = context.createAnalyser(); - analyser.fftSize = 8192; - analyser.smoothingTimeConstant = 0.1; - node.connect(analyser); - if (this.outputToSpeakers) { - console.warn( - "Warning: Output to speakers may affect sound quality,\nespecially due to system audio feedback preventative measures.\nuse only for debugging" - ); - analyser.connect(context.destination); - } - this.source = source; - this.node = node; - this.analyser = analyser; - this.processor = processor; - return true; - } - /** - * Gets the current frequency domain data from the recording track - * @param {"frequency"|"music"|"voice"} [analysisType] - * @param {number} [minDecibels] default -100 - * @param {number} [maxDecibels] default -30 - * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType} - */ - getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) { - if (!this.processor) { - throw new Error("Session ended: please call .begin() first"); - } - return AudioAnalysis.getFrequencies( - this.analyser, - this.sampleRate, - null, - analysisType, - minDecibels, - maxDecibels - ); - } - /** - * Pauses the recording - * Keeps microphone stream open but halts storage of audio - * @returns {Promise} - */ - async pause() { - if (!this.processor) { - throw new Error("Session ended: please call .begin() first"); - } else if (!this.recording) { - throw new Error("Already paused: please call .record() first"); - } - if (this._chunkProcessorBuffer.raw.byteLength) { - this._chunkProcessor(this._chunkProcessorBuffer); - } - this.log("Pausing ..."); - await this._event("stop"); - this.recording = false; - return true; - } - /** - * Start recording stream and storing to memory from the connected audio source - * @param {(data: { mono: Int16Array; raw: Int16Array }) => any} [chunkProcessor] - * @param {number} [chunkSize] chunkProcessor will not be triggered until this size threshold met in mono audio - * @returns {Promise} - */ - async record(chunkProcessor = () => { - }, chunkSize = 8192) { - if (!this.processor) { - throw new Error("Session ended: please call .begin() first"); - } else if (this.recording) { - throw new Error("Already recording: please call .pause() first"); - } else if (typeof chunkProcessor !== "function") { - throw new Error(`chunkProcessor must be a function`); - } - this._chunkProcessor = chunkProcessor; - this._chunkProcessorSize = chunkSize; - this._chunkProcessorBuffer = { - raw: new ArrayBuffer(0), - mono: new ArrayBuffer(0) - }; - this.log("Recording ..."); - await this._event("start"); - this.recording = true; - return true; - } - /** - * Clears the audio buffer, empties stored recording - * @returns {Promise} - */ - async clear() { - if (!this.processor) { - throw new Error("Session ended: please call .begin() first"); - } - await this._event("clear"); - return true; - } - /** - * Reads the current audio stream data - * @returns {Promise<{meanValues: Float32Array, channels: Array}>} - */ - async read() { - if (!this.processor) { - throw new Error("Session ended: please call .begin() first"); - } - this.log("Reading ..."); - const result = await this._event("read"); - return result; - } - /** - * Saves the current audio stream to a file - * @param {boolean} [force] Force saving while still recording - * @returns {Promise} - */ - async save(force = false) { - if (!this.processor) { - throw new Error("Session ended: please call .begin() first"); - } - if (!force && this.recording) { - throw new Error( - "Currently recording: please call .pause() first, or call .save(true) to force" - ); - } - this.log("Exporting ..."); - const exportData = await this._event("export"); - const packer = new WavPacker(); - const result = packer.pack(this.sampleRate, exportData.audio); - return result; - } - /** - * Ends the current recording session and saves the result - * @returns {Promise} - */ - async end() { - if (!this.processor) { - throw new Error("Session ended: please call .begin() first"); - } - const _processor = this.processor; - this.log("Stopping ..."); - await this._event("stop"); - this.recording = false; - const tracks = this.stream.getTracks(); - tracks.forEach((track) => track.stop()); - this.log("Exporting ..."); - const exportData = await this._event("export", {}, _processor); - this.processor.disconnect(); - this.source.disconnect(); - this.node.disconnect(); - this.analyser.disconnect(); - this.stream = null; - this.processor = null; - this.source = null; - this.node = null; - const packer = new WavPacker(); - const result = packer.pack(this.sampleRate, exportData.audio); - return result; - } - /** - * Performs a full cleanup of WavRecorder instance - * Stops actively listening via microphone and removes existing listeners - * @returns {Promise} - */ - async quit() { - this.listenForDeviceChange(null); - if (this.processor) { - await this.end(); - } - return true; - } - }; - globalThis.WavRecorder = WavRecorder; - })(); \ No newline at end of file From ba151320f9f7c06f33c7388b6355139f0068ac29 Mon Sep 17 00:00:00 2001 From: Davorin Rusevljan Date: Thu, 19 Dec 2024 23:46:53 +0100 Subject: [PATCH 46/51] websocket realtime wip(4) --- notebook/agentchat_realtime_websocket.ipynb | 846 ++++++++++++++++++ .../static/Audio.js | 0 .../static/main.js | 0 .../templates/chat.html | 0 4 files changed, 846 insertions(+) create mode 100644 notebook/agentchat_realtime_websocket.ipynb rename notebook/{ => agentchat_realtime_websocket}/static/Audio.js (100%) rename notebook/{ => agentchat_realtime_websocket}/static/main.js (100%) rename notebook/{ => agentchat_realtime_websocket}/templates/chat.html (100%) diff --git a/notebook/agentchat_realtime_websocket.ipynb b/notebook/agentchat_realtime_websocket.ipynb new file mode 100644 index 0000000000..6370b6f9c9 --- /dev/null +++ b/notebook/agentchat_realtime_websocket.ipynb @@ -0,0 +1,846 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "import os\n", + "from typing import Annotated, Union\n", + "from pathlib import Path\n", + "\n", + "import nest_asyncio\n", + "import uvicorn\n", + "from fastapi import FastAPI, Request, WebSocket\n", + "from fastapi.responses import HTMLResponse, JSONResponse\n", + "from fastapi.templating import Jinja2Templates\n", + "from fastapi.staticfiles import StaticFiles\n", + "\n", + "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, WebsocketAudioAdapter\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Configuration\n", + "OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\n", + "PORT = int(os.getenv(\"PORT\", 5050))\n", + "\n", + "if not OPENAI_API_KEY:\n", + " raise ValueError(\"Missing the OpenAI API key. Please set it in the .env file.\")\n", + "\n", + "llm_config = {\n", + " \"timeout\": 600,\n", + " \"cache_seed\": 45, # change the seed for different trials\n", + " \"config_list\": [\n", + " {\n", + " \"model\": \"gpt-4o-realtime-preview-2024-10-01\",\n", + " \"api_key\": OPENAI_API_KEY,\n", + " }\n", + " ],\n", + " \"temperature\": 0.8,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: Started server process [60435]\n", + "INFO: Waiting for application startup.\n", + "INFO: Application startup complete.\n", + "INFO: Uvicorn running on http://0.0.0.0:5050 (Press CTRL+C to quit)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 127.0.0.1:51198 - \"GET /start-chat HTTP/1.1\" 307 Temporary Redirect\n", + "INFO: 127.0.0.1:51198 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:51198 - \"GET /static/wavtools.js HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:51204 - \"GET /static/main.js HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:51204 - \"GET /static/Audio.js HTTP/1.1\" 200 OK\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: ('127.0.0.1', 51216) - \"WebSocket /media-stream\" [accepted]\n", + "INFO: connection open\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 127.0.0.1:51204 - \"GET /favicon.ico HTTP/1.1\" 404 Not Found\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", + "Sending session update finished\n", + "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIuWHBuzW59zezATXxJ5', 'session': {'id': 'sess_AgIuVZh1p6dyoyNEqVSuQ', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647631, 'modalities': ['audio', 'text'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.8999999761581421, 'prefix_padding_ms': 300, 'silence_duration_ms': 500, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", + "Sending session update finished\n", + "Incoming stream has started dsfstreamSidsdf\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", + "Sending session update finished\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgIuWM0MX2EsxnPqZfZhh', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", + "INFO: 127.0.0.1:43640 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:43640 - \"GET /static/Audio.js HTTP/1.1\" 304 Not Modified\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: ('127.0.0.1', 43670) - \"WebSocket /media-stream\" [accepted]\n", + "INFO: connection open\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", + "Sending session update finished\n", + "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIutGsw5qf3WwveWikTy', 'session': {'id': 'sess_AgIut2eUGPpxXodLrAE93', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647655, 'modalities': ['text', 'audio'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.5, 'prefix_padding_ms': 300, 'silence_duration_ms': 200, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", + "Sending session update finished\n", + "Incoming stream has started dsfstreamSidsdf\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", + "Sending session update finished\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgIutKeqWiiguz6JyJWjK', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv1v0Ixv7fDrgPsV549', 'audio_start_ms': 8288, 'item_id': 'item_AgIv11q2rT8VMahU5Ipc3'}\n", + "Speech started detected.\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv1PlvDuWUDebfOld0v', 'audio_end_ms': 8544, 'item_id': 'item_AgIv11q2rT8VMahU5Ipc3'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv1bQwEDqP5IEaIb27C', 'previous_item_id': None, 'item_id': 'item_AgIv11q2rT8VMahU5Ipc3'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIv2SG0D48umgWtY2Jwd', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995335, 'reset_seconds': 0.139}]}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv2X6rm9fi8IpgsUciQ', 'audio_start_ms': 9088, 'item_id': 'item_AgIv2FxXnsyLaS0j6Aaka'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv2h5tbQBTVwG9xe93G', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv1ZzYIc7XDEvYgCQTI', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIv1Zf020SRd2tXt0wUp', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': '¡Hola! ¿En qué puedo'}]}], 'usage': {'total_tokens': 170, 'input_tokens': 158, 'output_tokens': 12, 'input_token_details': {'text_tokens': 155, 'audio_tokens': 3, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 11, 'audio_tokens': 1}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv2leTMzPB3DUvmyrHV', 'audio_end_ms': 9344, 'item_id': 'item_AgIv2FxXnsyLaS0j6Aaka'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv21GOL0XgWEsti0xh5', 'previous_item_id': 'item_AgIv1Zf020SRd2tXt0wUp', 'item_id': 'item_AgIv2FxXnsyLaS0j6Aaka'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv2ZAYxBsfOplYL4pup', 'audio_start_ms': 9600, 'item_id': 'item_AgIv2KKyEKdWrMhL5reeb'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv203CwpQoRaDmWmvfE', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv2JNLPHKhxNLR5MQhT', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "INFO: 127.0.0.1:43654 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: connection closed\n", + "INFO: ('127.0.0.1', 37832) - \"WebSocket /media-stream\" [accepted]\n", + "INFO: connection open\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", + "Sending session update finished\n", + "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIv4LEN3eKdUMGopSU1q', 'session': {'id': 'sess_AgIv3xr2ZDNfzTCZ2GADs', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647665, 'modalities': ['audio', 'text'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.5, 'prefix_padding_ms': 300, 'silence_duration_ms': 200, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", + "Sending session update finished\n", + "Incoming stream has started dsfstreamSidsdf\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", + "Sending session update finished\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgIv47gZ824nWl07EizAQ', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv4r3z0lakPrfJzTD40', 'audio_start_ms': 928, 'item_id': 'item_AgIv4T3kcUJbZcb4V8zRc'}\n", + "Speech started detected.\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv6tDVD3MEBDXGh7Slu', 'audio_end_ms': 3040, 'item_id': 'item_AgIv4T3kcUJbZcb4V8zRc'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv63TsfTqsuWGnf00dC', 'previous_item_id': None, 'item_id': 'item_AgIv4T3kcUJbZcb4V8zRc'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIv6G7odSIaE5R2SAPva', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995335, 'reset_seconds': 0.139}]}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv78Vl2HBKWtgdL15vl', 'audio_start_ms': 3552, 'item_id': 'item_AgIv7TV1NmcVgnWJyxSft'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgIv6SAScZX28fon1H5hc\n", + "Handling speech started event.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv7uGl2TwTPvsyORYCD', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv6Iu1BUB5lAECX2SKL', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIv6SAScZX28fon1H5hc', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': '¡Hola! ¿En qué puedo ayudarte hoy?'}]}], 'usage': {'total_tokens': 214, 'input_tokens': 176, 'output_tokens': 38, 'input_token_details': {'text_tokens': 155, 'audio_tokens': 21, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 16, 'audio_tokens': 22}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv7HErXauigNREaYgoA', 'audio_end_ms': 4000, 'item_id': 'item_AgIv7TV1NmcVgnWJyxSft'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv7i0yGFGWlL6CsnSfU', 'previous_item_id': 'item_AgIv6SAScZX28fon1H5hc', 'item_id': 'item_AgIv7TV1NmcVgnWJyxSft'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIv7ltszsDLrnvjIQMMd', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995319, 'reset_seconds': 0.14}]}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv7A6oBgsY1D5JVhaPo', 'audio_start_ms': 4384, 'item_id': 'item_AgIv72u8No6pIczdWPLj5'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv76Bz5RHrz6kmy5EjS', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv7ftze76cWRHDWnzqU', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIv7XqXuv2zgMWdTNug3', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': []}], 'usage': {'total_tokens': 199, 'input_tokens': 196, 'output_tokens': 3, 'input_token_details': {'text_tokens': 171, 'audio_tokens': 25, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 3, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv9rBCHgWPE24HapcdM', 'audio_end_ms': 6112, 'item_id': 'item_AgIv72u8No6pIczdWPLj5'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv9M6JkjgrgMX79O9c9', 'previous_item_id': 'item_AgIv7XqXuv2zgMWdTNug3', 'item_id': 'item_AgIv72u8No6pIczdWPLj5'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv94UbUvvpC1NqqJygm', 'audio_start_ms': 6240, 'item_id': 'item_AgIv912do71I6O1Go8ALM'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv9ABJDlRKBdi2DqpRA', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv9JwRGjIPAvY6HrPBm', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvAf8OQ7ShYL9TkdvBU', 'audio_end_ms': 6560, 'item_id': 'item_AgIv912do71I6O1Go8ALM'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvAFMYzhqEPFu84ImUh', 'previous_item_id': 'item_AgIv72u8No6pIczdWPLj5', 'item_id': 'item_AgIv912do71I6O1Go8ALM'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvA5ftb0Ft7XkOI4uNI', 'audio_start_ms': 6624, 'item_id': 'item_AgIvA36DvS98pcOn8lfRQ'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvAdMbiSJM3NpWq87Br', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvA42OmEqV0I00Bltig', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvBW5OtfP5wccnECK3i', 'audio_end_ms': 8384, 'item_id': 'item_AgIvA36DvS98pcOn8lfRQ'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvB9ZUNSD3G7RWY19N2', 'previous_item_id': 'item_AgIv912do71I6O1Go8ALM', 'item_id': 'item_AgIvA36DvS98pcOn8lfRQ'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvCSEwOnL2ZCo6uywf2', 'audio_start_ms': 8608, 'item_id': 'item_AgIvC1qUXCvUdR32ICfvM'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvCBzCehRGc9oPxxLOs', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvBpFXLRm9MmUp6EhdQ', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvDw0EYNJ0cvtZbTESN', 'audio_end_ms': 10016, 'item_id': 'item_AgIvC1qUXCvUdR32ICfvM'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvDOcT82SI1EmjGRXTw', 'previous_item_id': 'item_AgIvA36DvS98pcOn8lfRQ', 'item_id': 'item_AgIvC1qUXCvUdR32ICfvM'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvD3lR5Thof58qYHlgj', 'audio_start_ms': 10560, 'item_id': 'item_AgIvDkJoKMBviJ2QrbtfX'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvEtqS07ZUSahr6BRT9', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvDlVCjnB0lmbxvTx7Y', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvF0y2riRR7xXZdinrV', 'audio_end_ms': 12192, 'item_id': 'item_AgIvDkJoKMBviJ2QrbtfX'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvFWAixFr4rQM0wcoWt', 'previous_item_id': 'item_AgIvC1qUXCvUdR32ICfvM', 'item_id': 'item_AgIvDkJoKMBviJ2QrbtfX'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvGGLiSgxqwLbVO7Dfq', 'audio_start_ms': 12320, 'item_id': 'item_AgIvGfI0nsIfUM8Kr5J4c'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvGDGkRYxYigQZQj0iC', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvFmcuMunpwKR0X4bIy', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvGJihcEDybKjF5L1zn', 'audio_end_ms': 12864, 'item_id': 'item_AgIvGfI0nsIfUM8Kr5J4c'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvGdce6XZwOH8rlePt1', 'previous_item_id': 'item_AgIvDkJoKMBviJ2QrbtfX', 'item_id': 'item_AgIvGfI0nsIfUM8Kr5J4c'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvGPnjMc60vMEEXQiQE', 'audio_start_ms': 13184, 'item_id': 'item_AgIvGIVh0oiN4pY09TzgT'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvGBjCL5TlVHTagFk4B', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvGJc94bVNVJoZKYjM3', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvIxn2IrKYnlZw6stKg', 'audio_end_ms': 14816, 'item_id': 'item_AgIvGIVh0oiN4pY09TzgT'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvIiADD15tyF64rQ6Xp', 'previous_item_id': 'item_AgIvGfI0nsIfUM8Kr5J4c', 'item_id': 'item_AgIvGIVh0oiN4pY09TzgT'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvIA9FbfUmBGxKIWomT', 'audio_start_ms': 15008, 'item_id': 'item_AgIvIfkL36zDgpW94kavF'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvIokvA3thBbVaZRZZj', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvIZSEkMh9C2DVFOVgE', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvIOdhp2eL2tdrx6mQv', 'audio_end_ms': 15264, 'item_id': 'item_AgIvIfkL36zDgpW94kavF'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvIZQEL1PhSd9Sk9BCv', 'previous_item_id': 'item_AgIvGIVh0oiN4pY09TzgT', 'item_id': 'item_AgIvIfkL36zDgpW94kavF'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvJIGV3OgJzAWT4NXLB', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995251, 'reset_seconds': 0.142}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvKNQsqsf1COXXeAtF5', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvImTaAvAyoNmfMrztE', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIvIF63nPIokhVyFFdrH', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Δεν είμαι σε θέση να συνδέσω φωνές με συγκεκριμένα πρόσωπα. Πώς θα μπορούσα να σας βοηθήσω αλλιώς;'}]}], 'usage': {'total_tokens': 518, 'input_tokens': 352, 'output_tokens': 166, 'input_token_details': {'text_tokens': 239, 'audio_tokens': 113, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 49, 'audio_tokens': 117}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvLOdBzRaiHdkT6FuMm', 'audio_start_ms': 17728, 'item_id': 'item_AgIvLYI9iu7MpaQ2SJtDv'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgIvIF63nPIokhVyFFdrH\n", + "Handling speech started event.\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvMmSWQnibCpQJQ60Zx', 'audio_end_ms': 18912, 'item_id': 'item_AgIvLYI9iu7MpaQ2SJtDv'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvMWfqgfMYmjcBKriMp', 'previous_item_id': 'item_AgIvIF63nPIokhVyFFdrH', 'item_id': 'item_AgIvLYI9iu7MpaQ2SJtDv'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvMvoqd55c3Y1WN93w1', 'audio_start_ms': 18976, 'item_id': 'item_AgIvM2lLuGR9NyFx3MESp'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvMp5JW0UYpZWAvhFhJ', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvMajQgdlBdcZ1hD9sL', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvMdm870cJAviOhgZDw', 'audio_end_ms': 19232, 'item_id': 'item_AgIvM2lLuGR9NyFx3MESp'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvMo91AgklnIWSPaDv4', 'previous_item_id': 'item_AgIvLYI9iu7MpaQ2SJtDv', 'item_id': 'item_AgIvM2lLuGR9NyFx3MESp'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvMcmCAKF2kzGOz22vU', 'audio_start_ms': 19296, 'item_id': 'item_AgIvM49OggE9jktZcpZq1'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvMs5hLbnOZzz4jVnxM', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvMlcEBGSJxDVfruoq3', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvNk7ivGbvIz7OCQ4UL', 'audio_end_ms': 19584, 'item_id': 'item_AgIvM49OggE9jktZcpZq1'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvNleYAkH4NPqhQh7HC', 'previous_item_id': 'item_AgIvM2lLuGR9NyFx3MESp', 'item_id': 'item_AgIvM49OggE9jktZcpZq1'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvNQ4kwOiWaWDDfjhsH', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995192, 'reset_seconds': 0.144}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvOTosnHQO5lHeK6F7z', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvNveOAUf03h8NHg89O', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIvNE6uhqw0vyTc3KVwD', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Извините, не понял вопрос. Можете, пожалуйста, повторить?'}]}], 'usage': {'total_tokens': 540, 'input_tokens': 427, 'output_tokens': 113, 'input_token_details': {'text_tokens': 271, 'audio_tokens': 156, 'cached_tokens': 384, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 128}}, 'output_token_details': {'text_tokens': 28, 'audio_tokens': 85}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvOklb0MU3ywfGyQDL7', 'audio_start_ms': 21376, 'item_id': 'item_AgIvOwZ316LZcF9v1bfQR'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgIvNE6uhqw0vyTc3KVwD\n", + "Handling speech started event.\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvPYYEXNnTeYBz6aIVr', 'audio_end_ms': 21696, 'item_id': 'item_AgIvOwZ316LZcF9v1bfQR'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvPB3H9MhDuDIye0axS', 'previous_item_id': 'item_AgIvNE6uhqw0vyTc3KVwD', 'item_id': 'item_AgIvOwZ316LZcF9v1bfQR'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvPVfhHXE3eVb6UmrD4', 'audio_start_ms': 21792, 'item_id': 'item_AgIvP5vicb2s4v4a8KuFN'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvP3JaU4jQW6ftj1PTe', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvP2F8uQTI32kKi4uAp', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvQjxTvwlZSIT9oglRN', 'audio_end_ms': 22816, 'item_id': 'item_AgIvP5vicb2s4v4a8KuFN'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvQSV3umPgBNYk2jl7t', 'previous_item_id': 'item_AgIvOwZ316LZcF9v1bfQR', 'item_id': 'item_AgIvP5vicb2s4v4a8KuFN'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvQwanpPPZxOipmEylg', 'audio_start_ms': 23008, 'item_id': 'item_AgIvQAmNYrarPSElCGJLE'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvQBeXQSoSSBGTYw4Qw', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvQW6HnYMbJdnzNgcei', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvSDoCc7uXeNTaj4MRj', 'audio_end_ms': 24736, 'item_id': 'item_AgIvQAmNYrarPSElCGJLE'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvS93qoUx4cT04LZXXH', 'previous_item_id': 'item_AgIvP5vicb2s4v4a8KuFN', 'item_id': 'item_AgIvQAmNYrarPSElCGJLE'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvSBc7UHHWNRMBd7O7I', 'audio_start_ms': 25056, 'item_id': 'item_AgIvSxtJLXtvObujRrUHZ'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvSfnr5lDjv6tVKsYIy', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvSrYovoHYkWrruAqKX', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvSYzX4PXxdVri5mIOW', 'audio_end_ms': 25408, 'item_id': 'item_AgIvSxtJLXtvObujRrUHZ'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvTURDUP2bIYetIKDUD', 'previous_item_id': 'item_AgIvQAmNYrarPSElCGJLE', 'item_id': 'item_AgIvSxtJLXtvObujRrUHZ'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvT6gKLvh9bQVkyX9kf', 'audio_start_ms': 25504, 'item_id': 'item_AgIvTUH0xhqJ5Fldtt3cc'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvTslO1wg00wIxbzB85', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvTvdzBINN4MGdLqcky', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvUBy3v7Z16nyZ3WG9O', 'audio_end_ms': 26400, 'item_id': 'item_AgIvTUH0xhqJ5Fldtt3cc'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvUxjrVy9mGEheaFzxy', 'previous_item_id': 'item_AgIvSxtJLXtvObujRrUHZ', 'item_id': 'item_AgIvTUH0xhqJ5Fldtt3cc'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvUcIrG7sNPz2b5bneC', 'audio_start_ms': 26848, 'item_id': 'item_AgIvUkhALhph174x8AOLS'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvUvPqfvVdJA5kj9XLs', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvULH6BXIfJBj8nDtt8', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvUt8oF2LXKrsbEVRMW', 'audio_end_ms': 27168, 'item_id': 'item_AgIvUkhALhph174x8AOLS'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvUIOnaITb7SNi5HrNB', 'previous_item_id': 'item_AgIvTUH0xhqJ5Fldtt3cc', 'item_id': 'item_AgIvUkhALhph174x8AOLS'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvVK1SQ0hNUrnxPVYnD', 'audio_start_ms': 27648, 'item_id': 'item_AgIvVzXvWfuTYV6Vt4dex'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvVbdDqkb7UmWzXWoi7', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvUfxIU5Osy3bxidK23', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvWx1UnBMU7hbcenYF4', 'audio_end_ms': 28704, 'item_id': 'item_AgIvVzXvWfuTYV6Vt4dex'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvW9EiL5ieFSIzTbFws', 'previous_item_id': 'item_AgIvUkhALhph174x8AOLS', 'item_id': 'item_AgIvVzXvWfuTYV6Vt4dex'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvWVNwXcoTybtzG3c5H', 'audio_start_ms': 28800, 'item_id': 'item_AgIvWIXVQjPGkDFSrcTg7'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvWimnUjqyqBNT9B3tT', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvWSd9qJnipkz6kOuer', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvWTRLbVqhJhexOEFM4', 'audio_end_ms': 29312, 'item_id': 'item_AgIvWIXVQjPGkDFSrcTg7'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvWgswMPYF1mQ9ecehg', 'previous_item_id': 'item_AgIvVzXvWfuTYV6Vt4dex', 'item_id': 'item_AgIvWIXVQjPGkDFSrcTg7'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvX56uDZFHS4r6SOqyj', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995100, 'reset_seconds': 0.147}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvXNCLhLupqNanxdFu8', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvWHv9dVWwCyYNzPiOr', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIvWGctF58HOZ61udhGn', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Может быть.'}]}], 'usage': {'total_tokens': 601, 'input_tokens': 577, 'output_tokens': 24, 'input_token_details': {'text_tokens': 343, 'audio_tokens': 234, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 192}}, 'output_token_details': {'text_tokens': 10, 'audio_tokens': 14}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvbmPMYsYvaPl4FWBm2', 'audio_start_ms': 33984, 'item_id': 'item_AgIvbVxG1xIGd9afRu5i1'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgIvWGctF58HOZ61udhGn\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgIvbblsCxw5yrg88xIx2', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 700ms is already shorter than 4101ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvfQOC79peQAZk2N0ao', 'audio_end_ms': 38208, 'item_id': 'item_AgIvbVxG1xIGd9afRu5i1'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvfb6uMbuAJ9dWf4LKI', 'previous_item_id': 'item_AgIvWGctF58HOZ61udhGn', 'item_id': 'item_AgIvbVxG1xIGd9afRu5i1'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvg8qSMpyFzDVKyQWls', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995070, 'reset_seconds': 0.147}]}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvgdzs7xDXqzhu9VnmX', 'audio_start_ms': 39136, 'item_id': 'item_AgIvg45WJBJ8YiPbDbUy9'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgIvfLkRPGs4uTBkOi64O\n", + "Handling speech started event.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvgMxBZ5ZbaPF8NMRTl', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvfDrDm63dS5rcwLkol', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIvfLkRPGs4uTBkOi64O', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Pjesërisht e saktë. Në fakt, ngjy'}]}], 'usage': {'total_tokens': 713, 'input_tokens': 644, 'output_tokens': 69, 'input_token_details': {'text_tokens': 359, 'audio_tokens': 285, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 192}}, 'output_token_details': {'text_tokens': 24, 'audio_tokens': 45}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvlsaRSpK1MNj6yNrxJ', 'audio_end_ms': 43840, 'item_id': 'item_AgIvg45WJBJ8YiPbDbUy9'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvlbvpTVbmurKj5lOCX', 'previous_item_id': 'item_AgIvfLkRPGs4uTBkOi64O', 'item_id': 'item_AgIvg45WJBJ8YiPbDbUy9'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvlpdPzriwHULxCNNon', 'audio_start_ms': 44352, 'item_id': 'item_AgIvlWWeTxC18pVRPLKDP'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvlBeRih35LlakCD8TZ', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvlQLGFSa1WypZEHemZ', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvpVNHxT2kM5A8tdeiz', 'audio_end_ms': 47648, 'item_id': 'item_AgIvlWWeTxC18pVRPLKDP'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvp5KpTdTPZMpaXhPhx', 'previous_item_id': 'item_AgIvg45WJBJ8YiPbDbUy9', 'item_id': 'item_AgIvlWWeTxC18pVRPLKDP'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvpz296B6paBO91P6Jz', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995039, 'reset_seconds': 0.148}]}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvqny8LqwCltvQvp4UW', 'audio_start_ms': 49280, 'item_id': 'item_AgIvqDccaZzV9zEAhAgP9'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgIvp8EjFbfm4DDBI4Bs8\n", + "Handling speech started event.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvqQNQUAhXk5cs6xkhg', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvplM3FV0H8EoFZoRJF', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIvp8EjFbfm4DDBI4Bs8', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Maalesef, ben sesleri tanıyamam veya ses üzerinden kimlik tespiti yapamam. Başka bir konuda yardımcı olabilirsem memnun olurum!'}]}], 'usage': {'total_tokens': 921, 'input_tokens': 747, 'output_tokens': 174, 'input_token_details': {'text_tokens': 383, 'audio_tokens': 364, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 192}}, 'output_token_details': {'text_tokens': 49, 'audio_tokens': 125}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvuccH3qvzyTLdmSCgl', 'audio_end_ms': 53504, 'item_id': 'item_AgIvqDccaZzV9zEAhAgP9'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvuiFVi3kfFcVOfRCRv', 'previous_item_id': 'item_AgIvp8EjFbfm4DDBI4Bs8', 'item_id': 'item_AgIvqDccaZzV9zEAhAgP9'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvv1e1iN7XQ6UbNJUWR', 'audio_start_ms': 53792, 'item_id': 'item_AgIvv5UoT8f7WHGeMAKge'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvvtzve7MPvLdZN2gbK', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvuqYpHCO8WTh0NyXK0', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvwzEvW8Ugw2tcIqFBe', 'audio_end_ms': 54720, 'item_id': 'item_AgIvv5UoT8f7WHGeMAKge'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvwiYtpMsTc4gqkhVFc', 'previous_item_id': 'item_AgIvqDccaZzV9zEAhAgP9', 'item_id': 'item_AgIvv5UoT8f7WHGeMAKge'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvwJfL5JKNFSsRDDynY', 'audio_start_ms': 54912, 'item_id': 'item_AgIvwsOMr9E9Et13jFiTZ'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvwFhfWudQWJX9aEwRt', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvwNnCgs6ZuiNfuXAC7', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvxrSvFcoASkzrp66jK', 'audio_end_ms': 56128, 'item_id': 'item_AgIvwsOMr9E9Et13jFiTZ'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvxB2JEmOWlKZWfDezy', 'previous_item_id': 'item_AgIvv5UoT8f7WHGeMAKge', 'item_id': 'item_AgIvwsOMr9E9Et13jFiTZ'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvydO1vBoRIIXDaxojs', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994987, 'reset_seconds': 0.15}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvzX7fShrl6NC2ESYPx', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvxwo1lpX8O8rpIbvny', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIvxjgOAprnIeem74fk1', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Skat, men du må gerne prøve igen, så ser vi, om det hjælper!'}]}], 'usage': {'total_tokens': 960, 'input_tokens': 853, 'output_tokens': 107, 'input_token_details': {'text_tokens': 415, 'audio_tokens': 438, 'cached_tokens': 768, 'cached_tokens_details': {'text_tokens': 384, 'audio_tokens': 384}}, 'output_token_details': {'text_tokens': 31, 'audio_tokens': 76}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIwNm2UKQSE4WLbzqGiE', 'audio_start_ms': 81760, 'item_id': 'item_AgIwNuJXeJcfZTElQ1jDS'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgIvxjgOAprnIeem74fk1\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgIwN6MPF9KdQccG0h5ay', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 3800ms is already shorter than 24975ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIwOQK0djEVxaOS9Kvbc', 'audio_end_ms': 82720, 'item_id': 'item_AgIwNuJXeJcfZTElQ1jDS'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIwOw05mMqfRmeG6voaG', 'previous_item_id': 'item_AgIvxjgOAprnIeem74fk1', 'item_id': 'item_AgIwNuJXeJcfZTElQ1jDS'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIwOSFcaoh2ze1cZB3oi', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994896, 'reset_seconds': 0.153}]}\n", + "INFO: 127.0.0.1:33508 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n", + "INFO: 127.0.0.1:33508 - \"GET /static/Audio.js HTTP/1.1\" 304 Not Modified\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: ('127.0.0.1', 33534) - \"WebSocket /media-stream\" [accepted]\n", + "INFO: connection open\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", + "Sending session update finished\n", + "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIwz80goWYq1dsKVkpcN', 'session': {'id': 'sess_AgIwzi90bTt5bge8Xp4qA', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647785, 'modalities': ['text', 'audio'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.5, 'prefix_padding_ms': 300, 'silence_duration_ms': 200, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", + "Sending session update finished\n", + "Incoming stream has started dsfstreamSidsdf\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", + "Sending session update finished\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgIx0zO0dsVMze7HvemBw', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIx0KUWUFYhJCiR7c2vT', 'audio_start_ms': 4128, 'item_id': 'item_AgIx0BKJA1RwyWoLb8Sc3'}\n", + "Speech started detected.\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIx0lITRoh0l5kvSvo1t', 'audio_end_ms': 5792, 'item_id': 'item_AgIx0BKJA1RwyWoLb8Sc3'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIx0EStGx0JspR5VcgQj', 'previous_item_id': None, 'item_id': 'item_AgIx0BKJA1RwyWoLb8Sc3'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIx0VvnLI1UBx75gXzRP', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995335, 'reset_seconds': 0.139}]}\n", + "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgIx1MLNF3OGjzoY8W83M', 'response_id': 'resp_AgIx0gifsEM0EqjTCqDhU', 'item_id': 'item_AgIx1dOL6Ajnt8geHJSYL', 'output_index': 1, 'call_id': 'call_TuJC6HD3Baet5Y3t', 'name': 'get_weather', 'arguments': '{\"location\":\"Paris\"}'}\n", + "Function call result: The weather is sunny.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIx15AiZ87I9C2HifVlg', 'response': {'object': 'realtime.response', 'id': 'resp_AgIx0gifsEM0EqjTCqDhU', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIx0SSk3HBDrpalTkLZC', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"Sure, could you please specify the city you're interested in?\"}]}, {'id': 'item_AgIx1dOL6Ajnt8geHJSYL', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_TuJC6HD3Baet5Y3t', 'arguments': '{\"location\":\"Paris\"}'}], 'usage': {'total_tokens': 267, 'input_tokens': 171, 'output_tokens': 96, 'input_token_details': {'text_tokens': 155, 'audio_tokens': 16, 'cached_tokens': 128, 'cached_tokens_details': {'text_tokens': 128, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 37, 'audio_tokens': 59}}, 'metadata': None}}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIx2TjPp0DxdbVU4zeOq', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995222, 'reset_seconds': 0.143}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIx3K0YrX94W9j2cCGEZ', 'response': {'object': 'realtime.response', 'id': 'resp_AgIx2HPFbKRdHGGyHErOS', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIx2OMWb2RceSUnfQlLw', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'In Paris, the weather is sunny. Perfect for a leisurely stroll along the Seine!'}]}], 'usage': {'total_tokens': 464, 'input_tokens': 283, 'output_tokens': 181, 'input_token_details': {'text_tokens': 208, 'audio_tokens': 75, 'cached_tokens': 256, 'cached_tokens_details': {'text_tokens': 192, 'audio_tokens': 64}}, 'output_token_details': {'text_tokens': 33, 'audio_tokens': 148}}, 'metadata': None}}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "ERROR: Exception in ASGI application\n", + "Traceback (most recent call last):\n", + " File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 793, in __aexit__\n", + " await _wait(self._tasks)\n", + " File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 758, in _wait\n", + " await waiter\n", + " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 287, in __await__\n", + " yield self # This tells Task to wait for completion.\n", + " ^^^^^^^^^^\n", + " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 339, in __wakeup\n", + " future.result()\n", + " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 198, in result\n", + " raise exc\n", + "asyncio.exceptions.CancelledError: Cancelled by cancel scope 75421d523150\n", + "\n", + "During handling of the above exception, another exception occurred:\n", + "\n", + " + Exception Group Traceback (most recent call last):\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/uvicorn/protocols/websockets/websockets_impl.py\", line 244, in run_asgi\n", + " | result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value]\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py\", line 70, in __call__\n", + " | return await self.app(scope, receive, send)\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/fastapi/applications.py\", line 1054, in __call__\n", + " | await super().__call__(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/applications.py\", line 113, in __call__\n", + " | await self.middleware_stack(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/middleware/errors.py\", line 152, in __call__\n", + " | await self.app(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/middleware/exceptions.py\", line 62, in __call__\n", + " | await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 53, in wrapped_app\n", + " | raise exc\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 42, in wrapped_app\n", + " | await app(scope, receive, sender)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 715, in __call__\n", + " | await self.middleware_stack(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 735, in app\n", + " | await route.handle(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 362, in handle\n", + " | await self.app(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 95, in app\n", + " | await wrap_app_handling_exceptions(app, session)(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 53, in wrapped_app\n", + " | raise exc\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 42, in wrapped_app\n", + " | await app(scope, receive, sender)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 93, in app\n", + " | await func(session)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/fastapi/routing.py\", line 383, in app\n", + " | await dependant.call(**solved_result.values)\n", + " | File \"/tmp/ipykernel_60435/3022857786.py\", line 74, in handle_media_stream\n", + " | await openai_client.run()\n", + " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/realtime_agent.py\", line 137, in run\n", + " | await self._client.run()\n", + " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/client.py\", line 106, in run\n", + " | async with create_task_group() as tg:\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 815, in __aexit__\n", + " | raise BaseExceptionGroup(\n", + " | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)\n", + " +-+---------------- 1 ----------------\n", + " | Traceback (most recent call last):\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 891, in send_context\n", + " | await self.drain()\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 1052, in drain\n", + " | await waiter\n", + " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 287, in __await__\n", + " | yield self # This tells Task to wait for completion.\n", + " | ^^^^^^^^^^\n", + " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 339, in __wakeup\n", + " | future.result()\n", + " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 203, in result\n", + " | raise self._exception.with_traceback(self._exception_tb)\n", + " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/selector_events.py\", line 970, in _read_ready__get_buffer\n", + " | nbytes = self._sock.recv_into(buf)\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | ConnectionResetError: [Errno 104] Connection reset by peer\n", + " | \n", + " | The above exception was the direct cause of the following exception:\n", + " | \n", + " | Traceback (most recent call last):\n", + " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 269, in __step\n", + " | result = coro.throw(exc)\n", + " | ^^^^^^^^^^^^^^^\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/asyncer/_main.py\", line 169, in value_wrapper\n", + " | value = await partial_f()\n", + " | ^^^^^^^^^^^^^^^^^\n", + " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/websocket_observer.py\", line 111, in run\n", + " | await openai_ws.send(json.dumps(audio_append))\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 458, in send\n", + " | async with self.send_context():\n", + " | File \"/home/davorin/anaconda3/lib/python3.11/contextlib.py\", line 211, in __aexit__\n", + " | await anext(self.gen)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 933, in send_context\n", + " | raise self.protocol.close_exc from original_exc\n", + " | websockets.exceptions.ConnectionClosedError: no close frame received or sent\n", + " +------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Error in _read_from_client: no close frame received or sent\n", + "Error in _read_from_client: sent 1011 (internal error) keepalive ping timeout; no close frame received\n", + "Error in _read_from_client: sent 1011 (internal error) keepalive ping timeout; no close frame received\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: connection closed\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIxUU4lXFZQSCMoSwaQ3', 'audio_start_ms': 36192, 'item_id': 'item_AgIxULDzyUMGwF3VS9VEL'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgIx2OMWb2RceSUnfQlLw\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgIxUJgyio4YmMMwUjU47', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 7400ms is already shorter than 29716ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIxWBkcvhdAAsEhV3TMT', 'audio_end_ms': 37888, 'item_id': 'item_AgIxULDzyUMGwF3VS9VEL'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIxWaFseiJpm1CGfHJca', 'previous_item_id': 'item_AgIx2OMWb2RceSUnfQlLw', 'item_id': 'item_AgIxULDzyUMGwF3VS9VEL'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIxWAxTV3Ym5W0wWWLom', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995059, 'reset_seconds': 0.148}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIxXbsQF0moIVwkOD2fi', 'response': {'object': 'realtime.response', 'id': 'resp_AgIxWgsjHjBq0lput4jJI', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIxWqrz3BYiCckp0zGOs', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"Could you tell me which city you're asking about?\"}]}], 'usage': {'total_tokens': 533, 'input_tokens': 462, 'output_tokens': 71, 'input_token_details': {'text_tokens': 224, 'audio_tokens': 238, 'cached_tokens': 128, 'cached_tokens_details': {'text_tokens': 128, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 20, 'audio_tokens': 51}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIxadspBGeWjcUgdxrjU', 'audio_start_ms': 41920, 'item_id': 'item_AgIxacvWr5898i8VHn81G'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgIxWqrz3BYiCckp0zGOs\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgIxavintZQpfHoMyGHqw', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 2550ms is already shorter than 3762ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIxanZtyR23SQfKAtpAj', 'audio_end_ms': 42528, 'item_id': 'item_AgIxacvWr5898i8VHn81G'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIxa59jewIY5Ht4Gd9OX', 'previous_item_id': 'item_AgIxWqrz3BYiCckp0zGOs', 'item_id': 'item_AgIxacvWr5898i8VHn81G'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIxbRduPwi78n3N1NLOR', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994992, 'reset_seconds': 0.15}]}\n", + "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgIxbZAwfw3kGK5TjmRnf', 'response_id': 'resp_AgIxatopD0Z148W16VgVO', 'item_id': 'item_AgIxaCRePUE5NAj8tps27', 'output_index': 0, 'call_id': 'call_HK9NKE0YJ5ynQCzp', 'name': 'get_weather', 'arguments': '{\"location\":\"Seattle\"}'}\n", + "Function call result: The weather is cloudy.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIxbrmNKd2TxHpYIAh1Z', 'response': {'object': 'realtime.response', 'id': 'resp_AgIxatopD0Z148W16VgVO', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIxaCRePUE5NAj8tps27', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_HK9NKE0YJ5ynQCzp', 'arguments': '{\"location\":\"Seattle\"}'}], 'usage': {'total_tokens': 550, 'input_tokens': 535, 'output_tokens': 15, 'input_token_details': {'text_tokens': 240, 'audio_tokens': 295, 'cached_tokens': 320, 'cached_tokens_details': {'text_tokens': 192, 'audio_tokens': 128}}, 'output_token_details': {'text_tokens': 15, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIxb5zlKNC7Rj3SpMSUY', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994964, 'reset_seconds': 0.151}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIxcyNra4xN6XUPZu2ub', 'response': {'object': 'realtime.response', 'id': 'resp_AgIxbvLNrgu9spmCsWGxt', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIxbbg4cHk27R5Tq7C8w', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'In Seattle, the weather is cloudy. A typical day in the Pacific Northwest!'}]}], 'usage': {'total_tokens': 681, 'input_tokens': 562, 'output_tokens': 119, 'input_token_details': {'text_tokens': 267, 'audio_tokens': 295, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 256}}, 'output_token_details': {'text_tokens': 28, 'audio_tokens': 91}}, 'metadata': None}}\n", + "INFO: 127.0.0.1:33524 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: connection closed\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 127.0.0.1:33524 - \"GET /static/Audio.js HTTP/1.1\" 304 Not Modified\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: ('127.0.0.1', 49718) - \"WebSocket /media-stream\" [accepted]\n", + "INFO: connection open\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", + "Sending session update finished\n", + "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIxqzPdEpGeINATkNRdN', 'session': {'id': 'sess_AgIxqOTBa59WVMwlzwwSx', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647838, 'modalities': ['audio', 'text'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.5, 'prefix_padding_ms': 300, 'silence_duration_ms': 200, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", + "Sending session update finished\n", + "Incoming stream has started dsfstreamSidsdf\n", + "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", + "Sending session update finished\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgIxqUvbpqrCP28JJygmy', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ0D21t8h1VSpqI0ENv4', 'audio_start_ms': 146688, 'item_id': 'item_AgJ0DW2wC4SKz3StwQ92n'}\n", + "Speech started detected.\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ0FfeVjLn3deJU0RdLf', 'audio_end_ms': 148608, 'item_id': 'item_AgJ0DW2wC4SKz3StwQ92n'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ0Fuc00yRlJ9Eg14Mtq', 'previous_item_id': None, 'item_id': 'item_AgJ0DW2wC4SKz3StwQ92n'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ0FQiMg0pE9QI36s3jr', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995335, 'reset_seconds': 0.139}]}\n", + "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgJ0FErzkQZFAGF6LxxJJ', 'response_id': 'resp_AgJ0FVDaFCTTUusG4p00a', 'item_id': 'item_AgJ0FdT0Vh3fe72jKRlO9', 'output_index': 0, 'call_id': 'call_K14PfcRwkaY73PEF', 'name': 'get_weather', 'arguments': '{\"location\":\"Paris\"}'}\n", + "Function call result: The weather is sunny.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ0FHXKsrwbDjqmzEk9h', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ0FVDaFCTTUusG4p00a', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ0FdT0Vh3fe72jKRlO9', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_K14PfcRwkaY73PEF', 'arguments': '{\"location\":\"Paris\"}'}], 'usage': {'total_tokens': 189, 'input_tokens': 174, 'output_tokens': 15, 'input_token_details': {'text_tokens': 155, 'audio_tokens': 19, 'cached_tokens': 128, 'cached_tokens_details': {'text_tokens': 128, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 15, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ0GU0NGxyikkeKwELAg', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995307, 'reset_seconds': 0.14}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ0HAunq1nNUfSslUDQQ', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ0FYGpMPpqNH93giajQ', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ0FTHE0vFhclbGYfPQJ', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"It's sunny in Paris today! Perfect weather for a stroll along the Seine or a visit to the Eiffel Tower.\"}]}], 'usage': {'total_tokens': 346, 'input_tokens': 201, 'output_tokens': 145, 'input_token_details': {'text_tokens': 182, 'audio_tokens': 19, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 36, 'audio_tokens': 109}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ0Vhj8lsh4LnaFEQVx7', 'audio_start_ms': 165408, 'item_id': 'item_AgJ0VzJmhadkiRGPBPzdz'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ0FTHE0vFhclbGYfPQJ\n", + "Handling speech started event.\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ0WuSBuBpDMxyNvMe1Y', 'audio_end_ms': 165760, 'item_id': 'item_AgJ0VzJmhadkiRGPBPzdz'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ0WnUjpWGcQxrzkoMdI', 'previous_item_id': 'item_AgJ0FTHE0vFhclbGYfPQJ', 'item_id': 'item_AgJ0VzJmhadkiRGPBPzdz'}\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJ0WOJ6yvwnMP7WvLTxk', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 5450ms is already shorter than 16034ms', 'param': None, 'event_id': None}}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ0WhbgRKH5gidKQsJJF', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995153, 'reset_seconds': 0.145}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ0Xy0D4eqD1DDvFop5e', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ0WHGqpWIbnCK37VYPs', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ0WuhSSkhTZNzQ9VWp7', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"You're welcome! If you need anything else, just let me know!\"}]}], 'usage': {'total_tokens': 460, 'input_tokens': 360, 'output_tokens': 100, 'input_token_details': {'text_tokens': 228, 'audio_tokens': 132, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 26, 'audio_tokens': 74}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ5y25f8hb2TNXy8OkuQ', 'audio_start_ms': 503552, 'item_id': 'item_AgJ5yuXfPQRvATmSPcbXz'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ0WuhSSkhTZNzQ9VWp7\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJ5yIAbD1uF53iyWWzgo', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 3700ms is already shorter than 337436ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ5z4KgnuNmp8MurF5Be', 'audio_end_ms': 505312, 'item_id': 'item_AgJ5yuXfPQRvATmSPcbXz'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ5zQigtN9BCZLidPhMU', 'previous_item_id': 'item_AgJ0WuhSSkhTZNzQ9VWp7', 'item_id': 'item_AgJ5yuXfPQRvATmSPcbXz'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ60AfeBPiiGiN4YlUUK', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995093, 'reset_seconds': 0.147}]}\n", + "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgJ60WTrdzv2meASXpZsG', 'response_id': 'resp_AgJ5zNOYVtR78tRx3RvVR', 'item_id': 'item_AgJ5zYtn2RlMyY8ZaLOoM', 'output_index': 0, 'call_id': 'call_1MGA3C3GDZDQXWOq', 'name': 'get_weather', 'arguments': '{\"location\":\"Venice\"}'}\n", + "Function call result: The weather is sunny.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ60xV8kFbe45KDyLVYX', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ5zNOYVtR78tRx3RvVR', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ5zYtn2RlMyY8ZaLOoM', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_1MGA3C3GDZDQXWOq', 'arguments': '{\"location\":\"Venice\"}'}], 'usage': {'total_tokens': 452, 'input_tokens': 436, 'output_tokens': 16, 'input_token_details': {'text_tokens': 214, 'audio_tokens': 222, 'cached_tokens': 128, 'cached_tokens_details': {'text_tokens': 128, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 16, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ60mHVzYdYvu14YYFQH', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995064, 'reset_seconds': 0.148}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ61TN5ajpAD1gL6QM1x', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ60GKJE6m94CtcpNw4e', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ60X4C0exXd2Fn8PT5i', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"In Venice, it's sunny today. Perfect weather for a gondola ride!\"}]}], 'usage': {'total_tokens': 582, 'input_tokens': 464, 'output_tokens': 118, 'input_token_details': {'text_tokens': 242, 'audio_tokens': 222, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 29, 'audio_tokens': 89}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6QQpDfFhhulVA7xhyY', 'audio_start_ms': 531648, 'item_id': 'item_AgJ6QJaQAEyhDKreXW3RP'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ60X4C0exXd2Fn8PT5i\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJ6QoDIm9UTVFWCxN8pD', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 4450ms is already shorter than 25304ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6Sym3PXvmfzX39uE1u', 'audio_end_ms': 533888, 'item_id': 'item_AgJ6QJaQAEyhDKreXW3RP'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6SJAXUHfUKoRsgs1Xv', 'previous_item_id': 'item_AgJ60X4C0exXd2Fn8PT5i', 'item_id': 'item_AgJ6QJaQAEyhDKreXW3RP'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6Ta4ZpArIlMCOZ59Sv', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994960, 'reset_seconds': 0.151}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6TL89NPplWWnqxVis3', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6SkOmkeUKaVzgqRYQx', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ6SLoUBqFSAM4kpPIse', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'The weather in Venice is sunny today. Perfect for a gondola ride!'}]}], 'usage': {'total_tokens': 684, 'input_tokens': 591, 'output_tokens': 93, 'input_token_details': {'text_tokens': 258, 'audio_tokens': 333, 'cached_tokens': 320, 'cached_tokens_details': {'text_tokens': 192, 'audio_tokens': 128}}, 'output_token_details': {'text_tokens': 27, 'audio_tokens': 66}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6VDlaKillQDJTHIhj6', 'audio_start_ms': 536448, 'item_id': 'item_AgJ6VZLJhKE9Xz0PHnDCY'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ6SLoUBqFSAM4kpPIse\n", + "Handling speech started event.\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6VOh9AOcuhBAaoiU4H', 'audio_end_ms': 536736, 'item_id': 'item_AgJ6VZLJhKE9Xz0PHnDCY'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6VhsyAORkw1ezPxCCz', 'previous_item_id': 'item_AgJ6SLoUBqFSAM4kpPIse', 'item_id': 'item_AgJ6VZLJhKE9Xz0PHnDCY'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6Vqv3hnpOhNMgJPdiS', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994910, 'reset_seconds': 0.152}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6VeKuTDq8IQHEiri4i', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6VRNuaJB7b7XyA0l0e', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ6VlUy2kUZl0YQYnRNp', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"You're welcome!\"}]}], 'usage': {'total_tokens': 673, 'input_tokens': 644, 'output_tokens': 29, 'input_token_details': {'text_tokens': 274, 'audio_tokens': 370, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 256}}, 'output_token_details': {'text_tokens': 9, 'audio_tokens': 20}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6kzocqKZVoHuz3IHws', 'audio_start_ms': 552192, 'item_id': 'item_AgJ6kviG3H90kK8P9cPw2'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ6VlUy2kUZl0YQYnRNp\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJ6kMMQ8aGkKi92abI53', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 1000ms is already shorter than 14686ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6lpZSJfD3OfNPPvmPy', 'audio_end_ms': 553248, 'item_id': 'item_AgJ6kviG3H90kK8P9cPw2'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6lqf7MK6QbLNPQJM7k', 'previous_item_id': 'item_AgJ6VlUy2kUZl0YQYnRNp', 'item_id': 'item_AgJ6kviG3H90kK8P9cPw2'}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6myCCdvmbV1V15kmI3', 'audio_start_ms': 553408, 'item_id': 'item_AgJ6mn5fLIh8NxAkTtpPg'}\n", + "Speech started detected.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6mG3gpO5OoqJzJ8tDl', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6lrkace62o4pUhBk3S', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6mnjsYs2BupFLwtD37', 'audio_end_ms': 553664, 'item_id': 'item_AgJ6mn5fLIh8NxAkTtpPg'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6mx6eTkzFizoFatwd8', 'previous_item_id': 'item_AgJ6kviG3H90kK8P9cPw2', 'item_id': 'item_AgJ6mn5fLIh8NxAkTtpPg'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6meLJStlTjfrShdQc4', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994865, 'reset_seconds': 0.154}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6nDNdR71yEvl3aYiVo', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6mreeZSbpBORcFuUn8', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ6muD8kVyqFqUexUqQg', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Nie ma sprawy!'}]}], 'usage': {'total_tokens': 737, 'input_tokens': 701, 'output_tokens': 36, 'input_token_details': {'text_tokens': 298, 'audio_tokens': 403, 'cached_tokens': 576, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 320}}, 'output_token_details': {'text_tokens': 11, 'audio_tokens': 25}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6qJDYYP5j20qiohXew', 'audio_start_ms': 557984, 'item_id': 'item_AgJ6qPkWR7Nc5DrKUYuOy'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ6muD8kVyqFqUexUqQg\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJ6qu105LfgjuBiKILgS', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 1250ms is already shorter than 3399ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6rkA966bLO16xxBqZl', 'audio_end_ms': 559136, 'item_id': 'item_AgJ6qPkWR7Nc5DrKUYuOy'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6rEk9iJMmWYgM5OqUH', 'previous_item_id': 'item_AgJ6muD8kVyqFqUexUqQg', 'item_id': 'item_AgJ6qPkWR7Nc5DrKUYuOy'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6sgqhZgnmAHiZzXt1H', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994825, 'reset_seconds': 0.155}]}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6sjrahODcZEOQblH3r', 'audio_start_ms': 560192, 'item_id': 'item_AgJ6sgaWYDl8PQl74cO0V'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ6rDHccABfykseOdrlG\n", + "Handling speech started event.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6ssSonwi44Ai4Ri3TH', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6rESyM3sEw3zW1vBIO', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgJ6rDHccABfykseOdrlG', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"Can you please tell me which location you'd like the weather for?\"}]}], 'usage': {'total_tokens': 836, 'input_tokens': 754, 'output_tokens': 82, 'input_token_details': {'text_tokens': 314, 'audio_tokens': 440, 'cached_tokens': 576, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 320}}, 'output_token_details': {'text_tokens': 24, 'audio_tokens': 58}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6tqlNhn5BsDbuyZToq', 'audio_end_ms': 560448, 'item_id': 'item_AgJ6sgaWYDl8PQl74cO0V'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6tPBnW9Qafa0dvA4hP', 'previous_item_id': 'item_AgJ6rDHccABfykseOdrlG', 'item_id': 'item_AgJ6sgaWYDl8PQl74cO0V'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6tfL0Risf0w2MW78Vh', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994730, 'reset_seconds': 0.158}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6udHaBJJ9Qbt1Aenpf', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6t0x7qCTbuzJJLkS4I', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ6tlnDkvBiVGNpE1cYc', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"I'm sorry, I didn't quite get that. Could you please repeat the location for the weather update?\"}]}], 'usage': {'total_tokens': 995, 'input_tokens': 851, 'output_tokens': 144, 'input_token_details': {'text_tokens': 350, 'audio_tokens': 501, 'cached_tokens': 768, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 448}}, 'output_token_details': {'text_tokens': 35, 'audio_tokens': 109}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ7OPszlNNCIl7jLG1tv', 'audio_start_ms': 591776, 'item_id': 'item_AgJ7OzsBYes9SMruvIKlp'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ6tlnDkvBiVGNpE1cYc\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJ7OjGNJjbTB61aS2258', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 5450ms is already shorter than 30764ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ7QSXDVVmriq8dF8Ihs', 'audio_end_ms': 593632, 'item_id': 'item_AgJ7OzsBYes9SMruvIKlp'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ7Q6fQP8RjgItYqtGLX', 'previous_item_id': 'item_AgJ6tlnDkvBiVGNpE1cYc', 'item_id': 'item_AgJ7OzsBYes9SMruvIKlp'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ7QktJEWLDmLxAB4zCW', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994678, 'reset_seconds': 0.159}]}\n", + "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgJ7RcElep57CweJQYXkv', 'response_id': 'resp_AgJ7QuwQPUxx9L7oJi6LM', 'item_id': 'item_AgJ7QE3t6XId0CzhXglsD', 'output_index': 0, 'call_id': 'call_75jfkZ0uM1oOjzry', 'name': 'get_weather', 'arguments': '{\"location\":\"Washington\"}'}\n", + "Function call result: The weather is sunny.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ7RFv9HCszBxbM3WPlu', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ7QuwQPUxx9L7oJi6LM', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ7QE3t6XId0CzhXglsD', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_75jfkZ0uM1oOjzry', 'arguments': '{\"location\":\"Washington\"}'}], 'usage': {'total_tokens': 935, 'input_tokens': 920, 'output_tokens': 15, 'input_token_details': {'text_tokens': 346, 'audio_tokens': 574, 'cached_tokens': 768, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 448}}, 'output_token_details': {'text_tokens': 15, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ7RLigioAK54CGuHdRs', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994651, 'reset_seconds': 0.16}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ7SNQpmqxT3sffFeJTw', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ7RNNqor7Qa1SPkQeuU', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ7RDo9DvUoOkq26Rm01', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"In Washington, it's currently sunny. Perfect weather to enjoy the capital!\"}]}], 'usage': {'total_tokens': 1076, 'input_tokens': 947, 'output_tokens': 129, 'input_token_details': {'text_tokens': 373, 'audio_tokens': 574, 'cached_tokens': 832, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 512}}, 'output_token_details': {'text_tokens': 28, 'audio_tokens': 101}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ7jaVDQSZZ8Te5fVCZl', 'audio_start_ms': 612384, 'item_id': 'item_AgJ7jQoBwK0jUC3APofYA'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ7RDo9DvUoOkq26Rm01\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJ7j1UMcp6UCy8StkCEo', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 5050ms is already shorter than 17759ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ7kwUdWktcDHrq1lIHU', 'audio_end_ms': 613536, 'item_id': 'item_AgJ7jQoBwK0jUC3APofYA'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ7kLy5OIz2extl9kChb', 'previous_item_id': 'item_AgJ7RDo9DvUoOkq26Rm01', 'item_id': 'item_AgJ7jQoBwK0jUC3APofYA'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ7kWhlH6CfDJXSQCg3C', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994534, 'reset_seconds': 0.163}]}\n", + "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgJ7kcNEhs0yU0n5jE5hb', 'response_id': 'resp_AgJ7kPHurDk5OqFbtcAif', 'item_id': 'item_AgJ7k0NvrgBJg0m61JKTX', 'output_index': 0, 'call_id': 'call_JqWG2iBT7nmJ26ak', 'name': 'get_weather', 'arguments': '{\"location\":\"Washington State\"}'}\n", + "Function call result: The weather is sunny.\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ7kwkCvieTrYnApJBAh', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ7kPHurDk5OqFbtcAif', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ7k0NvrgBJg0m61JKTX', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_JqWG2iBT7nmJ26ak', 'arguments': '{\"location\":\"Washington State\"}'}], 'usage': {'total_tokens': 1091, 'input_tokens': 1075, 'output_tokens': 16, 'input_token_details': {'text_tokens': 389, 'audio_tokens': 686, 'cached_tokens': 832, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 512}}, 'output_token_details': {'text_tokens': 16, 'audio_tokens': 0}}, 'metadata': None}}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ7lgQK8laznOJ6hdVfP', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994505, 'reset_seconds': 0.164}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ7mEJOrJmNB1WTFWYnA', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ7kgHQmjuHW42TupboB', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ7lJlVLMf0RsJuQjIUh', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"In Washington State, it's currently sunny. Perfect for enjoying the great outdoors!\"}]}], 'usage': {'total_tokens': 1239, 'input_tokens': 1103, 'output_tokens': 136, 'input_token_details': {'text_tokens': 417, 'audio_tokens': 686, 'cached_tokens': 1024, 'cached_tokens_details': {'text_tokens': 384, 'audio_tokens': 640}}, 'output_token_details': {'text_tokens': 30, 'audio_tokens': 106}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ9JXZMDStM6UxyyjjVz', 'audio_start_ms': 710752, 'item_id': 'item_AgJ9JAw7003hQsQSZMn8X'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ7lJlVLMf0RsJuQjIUh\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJ9JhJ7qk6rYoZQwduMJ', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 5300ms is already shorter than 95999ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ9KJGkKeIyYJlhJ4D8O', 'audio_end_ms': 711360, 'item_id': 'item_AgJ9JAw7003hQsQSZMn8X'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ9KxilY5uWJiclFThvB', 'previous_item_id': 'item_AgJ7lJlVLMf0RsJuQjIUh', 'item_id': 'item_AgJ9JAw7003hQsQSZMn8X'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ9KRRWRYOBImEigGwco', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994384, 'reset_seconds': 0.168}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ9L4nOADOmkqK8ydnQE', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ9K0HqWc0tB9bkDssCj', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ9KUOGCWXZSMLZgEUwV', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"You're welcome! If you have any more questions, feel free to ask!\"}]}], 'usage': {'total_tokens': 1332, 'input_tokens': 1231, 'output_tokens': 101, 'input_token_details': {'text_tokens': 433, 'audio_tokens': 798, 'cached_tokens': 320, 'cached_tokens_details': {'text_tokens': 192, 'audio_tokens': 128}}, 'output_token_details': {'text_tokens': 27, 'audio_tokens': 74}}, 'metadata': None}}\n", + "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJAJlMUUSZNnGX66iIME', 'audio_start_ms': 772480, 'item_id': 'item_AgJAJZeXX63cubvQYKqak'}\n", + "Speech started detected.\n", + "Interrupting response with id: item_AgJ9KUOGCWXZSMLZgEUwV\n", + "Handling speech started event.\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJAJIuACVcZF89FThqIc', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 3700ms is already shorter than 60476ms', 'param': None, 'event_id': None}}\n", + "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJAJuSBKoNhiUxMFj0qF', 'audio_end_ms': 772832, 'item_id': 'item_AgJAJZeXX63cubvQYKqak'}\n", + "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJAJepe5rZA5aK9sYLWo', 'previous_item_id': 'item_AgJ9KUOGCWXZSMLZgEUwV', 'item_id': 'item_AgJAJZeXX63cubvQYKqak'}\n", + "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJAK7JywPb2DZFz7Q3vU', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994294, 'reset_seconds': 0.171}]}\n", + "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJAKxxzKBqS8OrBITb0d', 'response': {'object': 'realtime.response', 'id': 'resp_AgJAJqofLEXY5jawsVsZg', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJAJeDfmDt2RyN5UXJdG', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"You're welcome! If you need anything else, just let me know!\"}]}], 'usage': {'total_tokens': 1423, 'input_tokens': 1325, 'output_tokens': 98, 'input_token_details': {'text_tokens': 449, 'audio_tokens': 876, 'cached_tokens': 1152, 'cached_tokens_details': {'text_tokens': 448, 'audio_tokens': 704}}, 'output_token_details': {'text_tokens': 26, 'audio_tokens': 72}}, 'metadata': None}}\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJQ165lRYZ4V3LOqcwjo', 'error': {'type': 'invalid_request_error', 'code': 'session_expired', 'message': 'Your session hit the maximum duration of 30 minutes.', 'param': None, 'event_id': None}}\n", + "Received event: error {'type': 'error', 'event_id': 'event_AgJQsgWk3vfUaBG4x5hv5', 'error': {'type': 'invalid_request_error', 'code': 'session_expired', 'message': 'Your session hit the maximum duration of 30 minutes.', 'param': None, 'event_id': None}}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "ERROR: Exception in ASGI application\n", + "Traceback (most recent call last):\n", + " File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 793, in __aexit__\n", + " await _wait(self._tasks)\n", + " File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 758, in _wait\n", + " await waiter\n", + " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 287, in __await__\n", + " yield self # This tells Task to wait for completion.\n", + " ^^^^^^^^^^\n", + " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 339, in __wakeup\n", + " future.result()\n", + " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 198, in result\n", + " raise exc\n", + "asyncio.exceptions.CancelledError: Cancelled by cancel scope 75421c0a0c90\n", + "\n", + "During handling of the above exception, another exception occurred:\n", + "\n", + " + Exception Group Traceback (most recent call last):\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/uvicorn/protocols/websockets/websockets_impl.py\", line 244, in run_asgi\n", + " | result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value]\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py\", line 70, in __call__\n", + " | return await self.app(scope, receive, send)\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/fastapi/applications.py\", line 1054, in __call__\n", + " | await super().__call__(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/applications.py\", line 113, in __call__\n", + " | await self.middleware_stack(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/middleware/errors.py\", line 152, in __call__\n", + " | await self.app(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/middleware/exceptions.py\", line 62, in __call__\n", + " | await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 53, in wrapped_app\n", + " | raise exc\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 42, in wrapped_app\n", + " | await app(scope, receive, sender)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 715, in __call__\n", + " | await self.middleware_stack(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 735, in app\n", + " | await route.handle(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 362, in handle\n", + " | await self.app(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 95, in app\n", + " | await wrap_app_handling_exceptions(app, session)(scope, receive, send)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 53, in wrapped_app\n", + " | raise exc\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 42, in wrapped_app\n", + " | await app(scope, receive, sender)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 93, in app\n", + " | await func(session)\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/fastapi/routing.py\", line 383, in app\n", + " | await dependant.call(**solved_result.values)\n", + " | File \"/tmp/ipykernel_60435/3022857786.py\", line 74, in handle_media_stream\n", + " | await openai_client.run()\n", + " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/realtime_agent.py\", line 137, in run\n", + " | await self._client.run()\n", + " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/client.py\", line 106, in run\n", + " | async with create_task_group() as tg:\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 815, in __aexit__\n", + " | raise BaseExceptionGroup(\n", + " | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)\n", + " +-+---------------- 1 ----------------\n", + " | Traceback (most recent call last):\n", + " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 267, in __step\n", + " | result = coro.send(None)\n", + " | ^^^^^^^^^^^^^^^\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/asyncer/_main.py\", line 169, in value_wrapper\n", + " | value = await partial_f()\n", + " | ^^^^^^^^^^^^^^^^^\n", + " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/websocket_observer.py\", line 111, in run\n", + " | await openai_ws.send(json.dumps(audio_append))\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 458, in send\n", + " | async with self.send_context():\n", + " | File \"/home/davorin/anaconda3/lib/python3.11/contextlib.py\", line 204, in __aenter__\n", + " | return await anext(self.gen)\n", + " | ^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 933, in send_context\n", + " | raise self.protocol.close_exc from original_exc\n", + " | websockets.exceptions.ConnectionClosedOK: received 1001 (going away) Your session hit the maximum duration of 30 minutes.; then sent 1001 (going away) Your session hit the maximum duration of 30 minutes.\n", + " +------------------------------------\n", + "INFO: connection closed\n" + ] + } + ], + "source": [ + "app = FastAPI()\n", + "\n", + "notebook_path=os.getcwd()\n", + "\n", + "app.mount(\"/static\", StaticFiles(directory=Path(notebook_path) / \"agentchat_realtime_websocket\" / \"static\"), name=\"static\")\n", + "\n", + "# Templates for HTML responses\n", + "\n", + "templates = Jinja2Templates(directory=Path(notebook_path) / \"agentchat_realtime_websocket\" / \"templates\")\n", + "\n", + "@app.get(\"/\", response_class=JSONResponse)\n", + "async def index_page():\n", + " return {\"message\": \"Websocket Audio Stream Server is running!\"}\n", + "\n", + "@app.get(\"/start-chat/\", response_class=HTMLResponse)\n", + "async def start_chat(request: Request):\n", + " \"\"\"Endpoint to return the HTML page for audio chat.\"\"\"\n", + " port = PORT # Extract the client's port\n", + " return templates.TemplateResponse(\"chat.html\", {\"request\": request, \"port\": port})\n", + "\n", + "@app.websocket(\"/media-stream\")\n", + "async def handle_media_stream(websocket: WebSocket):\n", + " \"\"\"Handle WebSocket connections providing audio stream and OpenAI.\"\"\"\n", + " await websocket.accept()\n", + "\n", + " audio_adapter = WebsocketAudioAdapter(websocket)\n", + " openai_client = RealtimeAgent(\n", + " name=\"Weather Bot\",\n", + " system_message=\"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\",\n", + " llm_config=llm_config,\n", + " audio_adapter=audio_adapter,\n", + " )\n", + "\n", + " @openai_client.register_handover(name=\"get_weather\", description=\"Get the current weather\")\n", + " def get_weather(location: Annotated[str, \"city\"]) -> str:\n", + " ...\n", + " return \"The weather is cloudy.\" if location == \"Seattle\" else \"The weather is sunny.\"\n", + "\n", + " await openai_client.run()\n", + "\n", + "\n", + "uvicorn.run(app, host=\"0.0.0.0\", port=PORT)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook/static/Audio.js b/notebook/agentchat_realtime_websocket/static/Audio.js similarity index 100% rename from notebook/static/Audio.js rename to notebook/agentchat_realtime_websocket/static/Audio.js diff --git a/notebook/static/main.js b/notebook/agentchat_realtime_websocket/static/main.js similarity index 100% rename from notebook/static/main.js rename to notebook/agentchat_realtime_websocket/static/main.js diff --git a/notebook/templates/chat.html b/notebook/agentchat_realtime_websocket/templates/chat.html similarity index 100% rename from notebook/templates/chat.html rename to notebook/agentchat_realtime_websocket/templates/chat.html From 972b53bcc73624adbdbfa3aa8413e309aefa3fef Mon Sep 17 00:00:00 2001 From: Davorin Rusevljan Date: Thu, 19 Dec 2024 23:50:52 +0100 Subject: [PATCH 47/51] websocket realtime wip(5) --- .../realtime_agent/websocket_observer.py | 18 +- notebook/agentchat_realtime_websocket.ipynb | 721 +----------------- 2 files changed, 15 insertions(+), 724 deletions(-) diff --git a/autogen/agentchat/realtime_agent/websocket_observer.py b/autogen/agentchat/realtime_agent/websocket_observer.py index ebcb92852c..5f40cde5d8 100644 --- a/autogen/agentchat/realtime_agent/websocket_observer.py +++ b/autogen/agentchat/realtime_agent/websocket_observer.py @@ -35,7 +35,7 @@ def __init__(self, websocket): self.latest_media_timestamp = 0 self.last_assistant_item = None self.mark_queue = [] - self.response_start_timestamp_twilio = None + self.response_start_timestamp_socket = None async def update(self, response): """Receive events from the OpenAI Realtime API, send audio back to websocket.""" @@ -47,10 +47,10 @@ async def update(self, response): audio_delta = {"event": "media", "streamSid": self.stream_sid, "media": {"payload": audio_payload}} await self.websocket.send_json(audio_delta) - if self.response_start_timestamp_twilio is None: - self.response_start_timestamp_twilio = self.latest_media_timestamp + if self.response_start_timestamp_socket is None: + self.response_start_timestamp_socket = self.latest_media_timestamp if SHOW_TIMING_MATH: - print(f"Setting start timestamp for new response: {self.response_start_timestamp_twilio}ms") + print(f"Setting start timestamp for new response: {self.response_start_timestamp_socket}ms") # Update last_assistant_item safely if response.get("item_id"): @@ -68,11 +68,11 @@ async def update(self, response): async def handle_speech_started_event(self): """Handle interruption when the caller's speech starts.""" print("Handling speech started event.") - if self.mark_queue and self.response_start_timestamp_twilio is not None: - elapsed_time = self.latest_media_timestamp - self.response_start_timestamp_twilio + if self.mark_queue and self.response_start_timestamp_socket is not None: + elapsed_time = self.latest_media_timestamp - self.response_start_timestamp_socket if SHOW_TIMING_MATH: print( - f"Calculating elapsed time for truncation: {self.latest_media_timestamp} - {self.response_start_timestamp_twilio} = {elapsed_time}ms" + f"Calculating elapsed time for truncation: {self.latest_media_timestamp} - {self.response_start_timestamp_socket} = {elapsed_time}ms" ) if self.last_assistant_item: @@ -91,7 +91,7 @@ async def handle_speech_started_event(self): self.mark_queue.clear() self.last_assistant_item = None - self.response_start_timestamp_twilio = None + self.response_start_timestamp_socket = None async def send_mark(self): if self.stream_sid: @@ -112,7 +112,7 @@ async def run(self): elif data["event"] == "start": self.stream_sid = data["start"]["streamSid"] print(f"Incoming stream has started {self.stream_sid}") - self.response_start_timestamp_twilio = None + self.response_start_timestamp_socket = None self.latest_media_timestamp = 0 self.last_assistant_item = None elif data["event"] == "mark": diff --git a/notebook/agentchat_realtime_websocket.ipynb b/notebook/agentchat_realtime_websocket.ipynb index 6370b6f9c9..3b5ca3987b 100644 --- a/notebook/agentchat_realtime_websocket.ipynb +++ b/notebook/agentchat_realtime_websocket.ipynb @@ -2,11 +2,10 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import asyncio\n", "import os\n", "from typing import Annotated, Union\n", "from pathlib import Path\n", @@ -18,13 +17,12 @@ "from fastapi.templating import Jinja2Templates\n", "from fastapi.staticfiles import StaticFiles\n", "\n", - "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, WebsocketAudioAdapter\n", - "\n" + "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, WebsocketAudioAdapter\n" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -50,7 +48,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -59,716 +57,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO: Started server process [60435]\n", - "INFO: Waiting for application startup.\n", - "INFO: Application startup complete.\n", - "INFO: Uvicorn running on http://0.0.0.0:5050 (Press CTRL+C to quit)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: 127.0.0.1:51198 - \"GET /start-chat HTTP/1.1\" 307 Temporary Redirect\n", - "INFO: 127.0.0.1:51198 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n", - "INFO: 127.0.0.1:51198 - \"GET /static/wavtools.js HTTP/1.1\" 200 OK\n", - "INFO: 127.0.0.1:51204 - \"GET /static/main.js HTTP/1.1\" 200 OK\n", - "INFO: 127.0.0.1:51204 - \"GET /static/Audio.js HTTP/1.1\" 200 OK\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO: ('127.0.0.1', 51216) - \"WebSocket /media-stream\" [accepted]\n", - "INFO: connection open\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: 127.0.0.1:51204 - \"GET /favicon.ico HTTP/1.1\" 404 Not Found\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", - "Sending session update finished\n", - "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIuWHBuzW59zezATXxJ5', 'session': {'id': 'sess_AgIuVZh1p6dyoyNEqVSuQ', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647631, 'modalities': ['audio', 'text'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.8999999761581421, 'prefix_padding_ms': 300, 'silence_duration_ms': 500, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", - "Sending session update finished\n", - "Incoming stream has started dsfstreamSidsdf\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", - "Sending session update finished\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgIuWM0MX2EsxnPqZfZhh', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", - "INFO: 127.0.0.1:43640 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n", - "INFO: 127.0.0.1:43640 - \"GET /static/Audio.js HTTP/1.1\" 304 Not Modified\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO: ('127.0.0.1', 43670) - \"WebSocket /media-stream\" [accepted]\n", - "INFO: connection open\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", - "Sending session update finished\n", - "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIutGsw5qf3WwveWikTy', 'session': {'id': 'sess_AgIut2eUGPpxXodLrAE93', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647655, 'modalities': ['text', 'audio'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.5, 'prefix_padding_ms': 300, 'silence_duration_ms': 200, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", - "Sending session update finished\n", - "Incoming stream has started dsfstreamSidsdf\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", - "Sending session update finished\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgIutKeqWiiguz6JyJWjK', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv1v0Ixv7fDrgPsV549', 'audio_start_ms': 8288, 'item_id': 'item_AgIv11q2rT8VMahU5Ipc3'}\n", - "Speech started detected.\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv1PlvDuWUDebfOld0v', 'audio_end_ms': 8544, 'item_id': 'item_AgIv11q2rT8VMahU5Ipc3'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv1bQwEDqP5IEaIb27C', 'previous_item_id': None, 'item_id': 'item_AgIv11q2rT8VMahU5Ipc3'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIv2SG0D48umgWtY2Jwd', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995335, 'reset_seconds': 0.139}]}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv2X6rm9fi8IpgsUciQ', 'audio_start_ms': 9088, 'item_id': 'item_AgIv2FxXnsyLaS0j6Aaka'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv2h5tbQBTVwG9xe93G', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv1ZzYIc7XDEvYgCQTI', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIv1Zf020SRd2tXt0wUp', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': '¡Hola! ¿En qué puedo'}]}], 'usage': {'total_tokens': 170, 'input_tokens': 158, 'output_tokens': 12, 'input_token_details': {'text_tokens': 155, 'audio_tokens': 3, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 11, 'audio_tokens': 1}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv2leTMzPB3DUvmyrHV', 'audio_end_ms': 9344, 'item_id': 'item_AgIv2FxXnsyLaS0j6Aaka'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv21GOL0XgWEsti0xh5', 'previous_item_id': 'item_AgIv1Zf020SRd2tXt0wUp', 'item_id': 'item_AgIv2FxXnsyLaS0j6Aaka'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv2ZAYxBsfOplYL4pup', 'audio_start_ms': 9600, 'item_id': 'item_AgIv2KKyEKdWrMhL5reeb'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv203CwpQoRaDmWmvfE', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv2JNLPHKhxNLR5MQhT', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "INFO: 127.0.0.1:43654 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO: connection closed\n", - "INFO: ('127.0.0.1', 37832) - \"WebSocket /media-stream\" [accepted]\n", - "INFO: connection open\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", - "Sending session update finished\n", - "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIv4LEN3eKdUMGopSU1q', 'session': {'id': 'sess_AgIv3xr2ZDNfzTCZ2GADs', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647665, 'modalities': ['audio', 'text'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.5, 'prefix_padding_ms': 300, 'silence_duration_ms': 200, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", - "Sending session update finished\n", - "Incoming stream has started dsfstreamSidsdf\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", - "Sending session update finished\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgIv47gZ824nWl07EizAQ', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv4r3z0lakPrfJzTD40', 'audio_start_ms': 928, 'item_id': 'item_AgIv4T3kcUJbZcb4V8zRc'}\n", - "Speech started detected.\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv6tDVD3MEBDXGh7Slu', 'audio_end_ms': 3040, 'item_id': 'item_AgIv4T3kcUJbZcb4V8zRc'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv63TsfTqsuWGnf00dC', 'previous_item_id': None, 'item_id': 'item_AgIv4T3kcUJbZcb4V8zRc'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIv6G7odSIaE5R2SAPva', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995335, 'reset_seconds': 0.139}]}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv78Vl2HBKWtgdL15vl', 'audio_start_ms': 3552, 'item_id': 'item_AgIv7TV1NmcVgnWJyxSft'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgIv6SAScZX28fon1H5hc\n", - "Handling speech started event.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv7uGl2TwTPvsyORYCD', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv6Iu1BUB5lAECX2SKL', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIv6SAScZX28fon1H5hc', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': '¡Hola! ¿En qué puedo ayudarte hoy?'}]}], 'usage': {'total_tokens': 214, 'input_tokens': 176, 'output_tokens': 38, 'input_token_details': {'text_tokens': 155, 'audio_tokens': 21, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 16, 'audio_tokens': 22}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv7HErXauigNREaYgoA', 'audio_end_ms': 4000, 'item_id': 'item_AgIv7TV1NmcVgnWJyxSft'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv7i0yGFGWlL6CsnSfU', 'previous_item_id': 'item_AgIv6SAScZX28fon1H5hc', 'item_id': 'item_AgIv7TV1NmcVgnWJyxSft'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIv7ltszsDLrnvjIQMMd', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995319, 'reset_seconds': 0.14}]}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv7A6oBgsY1D5JVhaPo', 'audio_start_ms': 4384, 'item_id': 'item_AgIv72u8No6pIczdWPLj5'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv76Bz5RHrz6kmy5EjS', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv7ftze76cWRHDWnzqU', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIv7XqXuv2zgMWdTNug3', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': []}], 'usage': {'total_tokens': 199, 'input_tokens': 196, 'output_tokens': 3, 'input_token_details': {'text_tokens': 171, 'audio_tokens': 25, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 3, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIv9rBCHgWPE24HapcdM', 'audio_end_ms': 6112, 'item_id': 'item_AgIv72u8No6pIczdWPLj5'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIv9M6JkjgrgMX79O9c9', 'previous_item_id': 'item_AgIv7XqXuv2zgMWdTNug3', 'item_id': 'item_AgIv72u8No6pIczdWPLj5'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIv94UbUvvpC1NqqJygm', 'audio_start_ms': 6240, 'item_id': 'item_AgIv912do71I6O1Go8ALM'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIv9ABJDlRKBdi2DqpRA', 'response': {'object': 'realtime.response', 'id': 'resp_AgIv9JwRGjIPAvY6HrPBm', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvAf8OQ7ShYL9TkdvBU', 'audio_end_ms': 6560, 'item_id': 'item_AgIv912do71I6O1Go8ALM'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvAFMYzhqEPFu84ImUh', 'previous_item_id': 'item_AgIv72u8No6pIczdWPLj5', 'item_id': 'item_AgIv912do71I6O1Go8ALM'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvA5ftb0Ft7XkOI4uNI', 'audio_start_ms': 6624, 'item_id': 'item_AgIvA36DvS98pcOn8lfRQ'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvAdMbiSJM3NpWq87Br', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvA42OmEqV0I00Bltig', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvBW5OtfP5wccnECK3i', 'audio_end_ms': 8384, 'item_id': 'item_AgIvA36DvS98pcOn8lfRQ'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvB9ZUNSD3G7RWY19N2', 'previous_item_id': 'item_AgIv912do71I6O1Go8ALM', 'item_id': 'item_AgIvA36DvS98pcOn8lfRQ'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvCSEwOnL2ZCo6uywf2', 'audio_start_ms': 8608, 'item_id': 'item_AgIvC1qUXCvUdR32ICfvM'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvCBzCehRGc9oPxxLOs', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvBpFXLRm9MmUp6EhdQ', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvDw0EYNJ0cvtZbTESN', 'audio_end_ms': 10016, 'item_id': 'item_AgIvC1qUXCvUdR32ICfvM'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvDOcT82SI1EmjGRXTw', 'previous_item_id': 'item_AgIvA36DvS98pcOn8lfRQ', 'item_id': 'item_AgIvC1qUXCvUdR32ICfvM'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvD3lR5Thof58qYHlgj', 'audio_start_ms': 10560, 'item_id': 'item_AgIvDkJoKMBviJ2QrbtfX'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvEtqS07ZUSahr6BRT9', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvDlVCjnB0lmbxvTx7Y', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvF0y2riRR7xXZdinrV', 'audio_end_ms': 12192, 'item_id': 'item_AgIvDkJoKMBviJ2QrbtfX'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvFWAixFr4rQM0wcoWt', 'previous_item_id': 'item_AgIvC1qUXCvUdR32ICfvM', 'item_id': 'item_AgIvDkJoKMBviJ2QrbtfX'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvGGLiSgxqwLbVO7Dfq', 'audio_start_ms': 12320, 'item_id': 'item_AgIvGfI0nsIfUM8Kr5J4c'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvGDGkRYxYigQZQj0iC', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvFmcuMunpwKR0X4bIy', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvGJihcEDybKjF5L1zn', 'audio_end_ms': 12864, 'item_id': 'item_AgIvGfI0nsIfUM8Kr5J4c'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvGdce6XZwOH8rlePt1', 'previous_item_id': 'item_AgIvDkJoKMBviJ2QrbtfX', 'item_id': 'item_AgIvGfI0nsIfUM8Kr5J4c'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvGPnjMc60vMEEXQiQE', 'audio_start_ms': 13184, 'item_id': 'item_AgIvGIVh0oiN4pY09TzgT'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvGBjCL5TlVHTagFk4B', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvGJc94bVNVJoZKYjM3', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvIxn2IrKYnlZw6stKg', 'audio_end_ms': 14816, 'item_id': 'item_AgIvGIVh0oiN4pY09TzgT'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvIiADD15tyF64rQ6Xp', 'previous_item_id': 'item_AgIvGfI0nsIfUM8Kr5J4c', 'item_id': 'item_AgIvGIVh0oiN4pY09TzgT'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvIA9FbfUmBGxKIWomT', 'audio_start_ms': 15008, 'item_id': 'item_AgIvIfkL36zDgpW94kavF'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvIokvA3thBbVaZRZZj', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvIZSEkMh9C2DVFOVgE', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvIOdhp2eL2tdrx6mQv', 'audio_end_ms': 15264, 'item_id': 'item_AgIvIfkL36zDgpW94kavF'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvIZQEL1PhSd9Sk9BCv', 'previous_item_id': 'item_AgIvGIVh0oiN4pY09TzgT', 'item_id': 'item_AgIvIfkL36zDgpW94kavF'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvJIGV3OgJzAWT4NXLB', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995251, 'reset_seconds': 0.142}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvKNQsqsf1COXXeAtF5', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvImTaAvAyoNmfMrztE', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIvIF63nPIokhVyFFdrH', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Δεν είμαι σε θέση να συνδέσω φωνές με συγκεκριμένα πρόσωπα. Πώς θα μπορούσα να σας βοηθήσω αλλιώς;'}]}], 'usage': {'total_tokens': 518, 'input_tokens': 352, 'output_tokens': 166, 'input_token_details': {'text_tokens': 239, 'audio_tokens': 113, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 49, 'audio_tokens': 117}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvLOdBzRaiHdkT6FuMm', 'audio_start_ms': 17728, 'item_id': 'item_AgIvLYI9iu7MpaQ2SJtDv'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgIvIF63nPIokhVyFFdrH\n", - "Handling speech started event.\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvMmSWQnibCpQJQ60Zx', 'audio_end_ms': 18912, 'item_id': 'item_AgIvLYI9iu7MpaQ2SJtDv'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvMWfqgfMYmjcBKriMp', 'previous_item_id': 'item_AgIvIF63nPIokhVyFFdrH', 'item_id': 'item_AgIvLYI9iu7MpaQ2SJtDv'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvMvoqd55c3Y1WN93w1', 'audio_start_ms': 18976, 'item_id': 'item_AgIvM2lLuGR9NyFx3MESp'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvMp5JW0UYpZWAvhFhJ', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvMajQgdlBdcZ1hD9sL', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvMdm870cJAviOhgZDw', 'audio_end_ms': 19232, 'item_id': 'item_AgIvM2lLuGR9NyFx3MESp'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvMo91AgklnIWSPaDv4', 'previous_item_id': 'item_AgIvLYI9iu7MpaQ2SJtDv', 'item_id': 'item_AgIvM2lLuGR9NyFx3MESp'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvMcmCAKF2kzGOz22vU', 'audio_start_ms': 19296, 'item_id': 'item_AgIvM49OggE9jktZcpZq1'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvMs5hLbnOZzz4jVnxM', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvMlcEBGSJxDVfruoq3', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvNk7ivGbvIz7OCQ4UL', 'audio_end_ms': 19584, 'item_id': 'item_AgIvM49OggE9jktZcpZq1'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvNleYAkH4NPqhQh7HC', 'previous_item_id': 'item_AgIvM2lLuGR9NyFx3MESp', 'item_id': 'item_AgIvM49OggE9jktZcpZq1'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvNQ4kwOiWaWDDfjhsH', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995192, 'reset_seconds': 0.144}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvOTosnHQO5lHeK6F7z', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvNveOAUf03h8NHg89O', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIvNE6uhqw0vyTc3KVwD', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Извините, не понял вопрос. Можете, пожалуйста, повторить?'}]}], 'usage': {'total_tokens': 540, 'input_tokens': 427, 'output_tokens': 113, 'input_token_details': {'text_tokens': 271, 'audio_tokens': 156, 'cached_tokens': 384, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 128}}, 'output_token_details': {'text_tokens': 28, 'audio_tokens': 85}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvOklb0MU3ywfGyQDL7', 'audio_start_ms': 21376, 'item_id': 'item_AgIvOwZ316LZcF9v1bfQR'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgIvNE6uhqw0vyTc3KVwD\n", - "Handling speech started event.\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvPYYEXNnTeYBz6aIVr', 'audio_end_ms': 21696, 'item_id': 'item_AgIvOwZ316LZcF9v1bfQR'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvPB3H9MhDuDIye0axS', 'previous_item_id': 'item_AgIvNE6uhqw0vyTc3KVwD', 'item_id': 'item_AgIvOwZ316LZcF9v1bfQR'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvPVfhHXE3eVb6UmrD4', 'audio_start_ms': 21792, 'item_id': 'item_AgIvP5vicb2s4v4a8KuFN'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvP3JaU4jQW6ftj1PTe', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvP2F8uQTI32kKi4uAp', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvQjxTvwlZSIT9oglRN', 'audio_end_ms': 22816, 'item_id': 'item_AgIvP5vicb2s4v4a8KuFN'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvQSV3umPgBNYk2jl7t', 'previous_item_id': 'item_AgIvOwZ316LZcF9v1bfQR', 'item_id': 'item_AgIvP5vicb2s4v4a8KuFN'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvQwanpPPZxOipmEylg', 'audio_start_ms': 23008, 'item_id': 'item_AgIvQAmNYrarPSElCGJLE'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvQBeXQSoSSBGTYw4Qw', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvQW6HnYMbJdnzNgcei', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvSDoCc7uXeNTaj4MRj', 'audio_end_ms': 24736, 'item_id': 'item_AgIvQAmNYrarPSElCGJLE'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvS93qoUx4cT04LZXXH', 'previous_item_id': 'item_AgIvP5vicb2s4v4a8KuFN', 'item_id': 'item_AgIvQAmNYrarPSElCGJLE'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvSBc7UHHWNRMBd7O7I', 'audio_start_ms': 25056, 'item_id': 'item_AgIvSxtJLXtvObujRrUHZ'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvSfnr5lDjv6tVKsYIy', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvSrYovoHYkWrruAqKX', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvSYzX4PXxdVri5mIOW', 'audio_end_ms': 25408, 'item_id': 'item_AgIvSxtJLXtvObujRrUHZ'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvTURDUP2bIYetIKDUD', 'previous_item_id': 'item_AgIvQAmNYrarPSElCGJLE', 'item_id': 'item_AgIvSxtJLXtvObujRrUHZ'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvT6gKLvh9bQVkyX9kf', 'audio_start_ms': 25504, 'item_id': 'item_AgIvTUH0xhqJ5Fldtt3cc'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvTslO1wg00wIxbzB85', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvTvdzBINN4MGdLqcky', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvUBy3v7Z16nyZ3WG9O', 'audio_end_ms': 26400, 'item_id': 'item_AgIvTUH0xhqJ5Fldtt3cc'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvUxjrVy9mGEheaFzxy', 'previous_item_id': 'item_AgIvSxtJLXtvObujRrUHZ', 'item_id': 'item_AgIvTUH0xhqJ5Fldtt3cc'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvUcIrG7sNPz2b5bneC', 'audio_start_ms': 26848, 'item_id': 'item_AgIvUkhALhph174x8AOLS'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvUvPqfvVdJA5kj9XLs', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvULH6BXIfJBj8nDtt8', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvUt8oF2LXKrsbEVRMW', 'audio_end_ms': 27168, 'item_id': 'item_AgIvUkhALhph174x8AOLS'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvUIOnaITb7SNi5HrNB', 'previous_item_id': 'item_AgIvTUH0xhqJ5Fldtt3cc', 'item_id': 'item_AgIvUkhALhph174x8AOLS'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvVK1SQ0hNUrnxPVYnD', 'audio_start_ms': 27648, 'item_id': 'item_AgIvVzXvWfuTYV6Vt4dex'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvVbdDqkb7UmWzXWoi7', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvUfxIU5Osy3bxidK23', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvWx1UnBMU7hbcenYF4', 'audio_end_ms': 28704, 'item_id': 'item_AgIvVzXvWfuTYV6Vt4dex'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvW9EiL5ieFSIzTbFws', 'previous_item_id': 'item_AgIvUkhALhph174x8AOLS', 'item_id': 'item_AgIvVzXvWfuTYV6Vt4dex'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvWVNwXcoTybtzG3c5H', 'audio_start_ms': 28800, 'item_id': 'item_AgIvWIXVQjPGkDFSrcTg7'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvWimnUjqyqBNT9B3tT', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvWSd9qJnipkz6kOuer', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvWTRLbVqhJhexOEFM4', 'audio_end_ms': 29312, 'item_id': 'item_AgIvWIXVQjPGkDFSrcTg7'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvWgswMPYF1mQ9ecehg', 'previous_item_id': 'item_AgIvVzXvWfuTYV6Vt4dex', 'item_id': 'item_AgIvWIXVQjPGkDFSrcTg7'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvX56uDZFHS4r6SOqyj', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995100, 'reset_seconds': 0.147}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvXNCLhLupqNanxdFu8', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvWHv9dVWwCyYNzPiOr', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIvWGctF58HOZ61udhGn', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Может быть.'}]}], 'usage': {'total_tokens': 601, 'input_tokens': 577, 'output_tokens': 24, 'input_token_details': {'text_tokens': 343, 'audio_tokens': 234, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 192}}, 'output_token_details': {'text_tokens': 10, 'audio_tokens': 14}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvbmPMYsYvaPl4FWBm2', 'audio_start_ms': 33984, 'item_id': 'item_AgIvbVxG1xIGd9afRu5i1'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgIvWGctF58HOZ61udhGn\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgIvbblsCxw5yrg88xIx2', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 700ms is already shorter than 4101ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvfQOC79peQAZk2N0ao', 'audio_end_ms': 38208, 'item_id': 'item_AgIvbVxG1xIGd9afRu5i1'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvfb6uMbuAJ9dWf4LKI', 'previous_item_id': 'item_AgIvWGctF58HOZ61udhGn', 'item_id': 'item_AgIvbVxG1xIGd9afRu5i1'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvg8qSMpyFzDVKyQWls', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995070, 'reset_seconds': 0.147}]}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvgdzs7xDXqzhu9VnmX', 'audio_start_ms': 39136, 'item_id': 'item_AgIvg45WJBJ8YiPbDbUy9'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgIvfLkRPGs4uTBkOi64O\n", - "Handling speech started event.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvgMxBZ5ZbaPF8NMRTl', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvfDrDm63dS5rcwLkol', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIvfLkRPGs4uTBkOi64O', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Pjesërisht e saktë. Në fakt, ngjy'}]}], 'usage': {'total_tokens': 713, 'input_tokens': 644, 'output_tokens': 69, 'input_token_details': {'text_tokens': 359, 'audio_tokens': 285, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 192}}, 'output_token_details': {'text_tokens': 24, 'audio_tokens': 45}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvlsaRSpK1MNj6yNrxJ', 'audio_end_ms': 43840, 'item_id': 'item_AgIvg45WJBJ8YiPbDbUy9'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvlbvpTVbmurKj5lOCX', 'previous_item_id': 'item_AgIvfLkRPGs4uTBkOi64O', 'item_id': 'item_AgIvg45WJBJ8YiPbDbUy9'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvlpdPzriwHULxCNNon', 'audio_start_ms': 44352, 'item_id': 'item_AgIvlWWeTxC18pVRPLKDP'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvlBeRih35LlakCD8TZ', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvlQLGFSa1WypZEHemZ', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvpVNHxT2kM5A8tdeiz', 'audio_end_ms': 47648, 'item_id': 'item_AgIvlWWeTxC18pVRPLKDP'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvp5KpTdTPZMpaXhPhx', 'previous_item_id': 'item_AgIvg45WJBJ8YiPbDbUy9', 'item_id': 'item_AgIvlWWeTxC18pVRPLKDP'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvpz296B6paBO91P6Jz', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995039, 'reset_seconds': 0.148}]}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvqny8LqwCltvQvp4UW', 'audio_start_ms': 49280, 'item_id': 'item_AgIvqDccaZzV9zEAhAgP9'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgIvp8EjFbfm4DDBI4Bs8\n", - "Handling speech started event.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvqQNQUAhXk5cs6xkhg', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvplM3FV0H8EoFZoRJF', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgIvp8EjFbfm4DDBI4Bs8', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Maalesef, ben sesleri tanıyamam veya ses üzerinden kimlik tespiti yapamam. Başka bir konuda yardımcı olabilirsem memnun olurum!'}]}], 'usage': {'total_tokens': 921, 'input_tokens': 747, 'output_tokens': 174, 'input_token_details': {'text_tokens': 383, 'audio_tokens': 364, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 192}}, 'output_token_details': {'text_tokens': 49, 'audio_tokens': 125}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvuccH3qvzyTLdmSCgl', 'audio_end_ms': 53504, 'item_id': 'item_AgIvqDccaZzV9zEAhAgP9'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvuiFVi3kfFcVOfRCRv', 'previous_item_id': 'item_AgIvp8EjFbfm4DDBI4Bs8', 'item_id': 'item_AgIvqDccaZzV9zEAhAgP9'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvv1e1iN7XQ6UbNJUWR', 'audio_start_ms': 53792, 'item_id': 'item_AgIvv5UoT8f7WHGeMAKge'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvvtzve7MPvLdZN2gbK', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvuqYpHCO8WTh0NyXK0', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvwzEvW8Ugw2tcIqFBe', 'audio_end_ms': 54720, 'item_id': 'item_AgIvv5UoT8f7WHGeMAKge'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvwiYtpMsTc4gqkhVFc', 'previous_item_id': 'item_AgIvqDccaZzV9zEAhAgP9', 'item_id': 'item_AgIvv5UoT8f7WHGeMAKge'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIvwJfL5JKNFSsRDDynY', 'audio_start_ms': 54912, 'item_id': 'item_AgIvwsOMr9E9Et13jFiTZ'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvwFhfWudQWJX9aEwRt', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvwNnCgs6ZuiNfuXAC7', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIvxrSvFcoASkzrp66jK', 'audio_end_ms': 56128, 'item_id': 'item_AgIvwsOMr9E9Et13jFiTZ'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIvxB2JEmOWlKZWfDezy', 'previous_item_id': 'item_AgIvv5UoT8f7WHGeMAKge', 'item_id': 'item_AgIvwsOMr9E9Et13jFiTZ'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIvydO1vBoRIIXDaxojs', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994987, 'reset_seconds': 0.15}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIvzX7fShrl6NC2ESYPx', 'response': {'object': 'realtime.response', 'id': 'resp_AgIvxwo1lpX8O8rpIbvny', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIvxjgOAprnIeem74fk1', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Skat, men du må gerne prøve igen, så ser vi, om det hjælper!'}]}], 'usage': {'total_tokens': 960, 'input_tokens': 853, 'output_tokens': 107, 'input_token_details': {'text_tokens': 415, 'audio_tokens': 438, 'cached_tokens': 768, 'cached_tokens_details': {'text_tokens': 384, 'audio_tokens': 384}}, 'output_token_details': {'text_tokens': 31, 'audio_tokens': 76}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIwNm2UKQSE4WLbzqGiE', 'audio_start_ms': 81760, 'item_id': 'item_AgIwNuJXeJcfZTElQ1jDS'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgIvxjgOAprnIeem74fk1\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgIwN6MPF9KdQccG0h5ay', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 3800ms is already shorter than 24975ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIwOQK0djEVxaOS9Kvbc', 'audio_end_ms': 82720, 'item_id': 'item_AgIwNuJXeJcfZTElQ1jDS'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIwOw05mMqfRmeG6voaG', 'previous_item_id': 'item_AgIvxjgOAprnIeem74fk1', 'item_id': 'item_AgIwNuJXeJcfZTElQ1jDS'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIwOSFcaoh2ze1cZB3oi', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994896, 'reset_seconds': 0.153}]}\n", - "INFO: 127.0.0.1:33508 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n", - "INFO: 127.0.0.1:33508 - \"GET /static/Audio.js HTTP/1.1\" 304 Not Modified\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO: ('127.0.0.1', 33534) - \"WebSocket /media-stream\" [accepted]\n", - "INFO: connection open\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", - "Sending session update finished\n", - "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIwz80goWYq1dsKVkpcN', 'session': {'id': 'sess_AgIwzi90bTt5bge8Xp4qA', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647785, 'modalities': ['text', 'audio'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.5, 'prefix_padding_ms': 300, 'silence_duration_ms': 200, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", - "Sending session update finished\n", - "Incoming stream has started dsfstreamSidsdf\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", - "Sending session update finished\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgIx0zO0dsVMze7HvemBw', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIx0KUWUFYhJCiR7c2vT', 'audio_start_ms': 4128, 'item_id': 'item_AgIx0BKJA1RwyWoLb8Sc3'}\n", - "Speech started detected.\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIx0lITRoh0l5kvSvo1t', 'audio_end_ms': 5792, 'item_id': 'item_AgIx0BKJA1RwyWoLb8Sc3'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIx0EStGx0JspR5VcgQj', 'previous_item_id': None, 'item_id': 'item_AgIx0BKJA1RwyWoLb8Sc3'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIx0VvnLI1UBx75gXzRP', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995335, 'reset_seconds': 0.139}]}\n", - "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgIx1MLNF3OGjzoY8W83M', 'response_id': 'resp_AgIx0gifsEM0EqjTCqDhU', 'item_id': 'item_AgIx1dOL6Ajnt8geHJSYL', 'output_index': 1, 'call_id': 'call_TuJC6HD3Baet5Y3t', 'name': 'get_weather', 'arguments': '{\"location\":\"Paris\"}'}\n", - "Function call result: The weather is sunny.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIx15AiZ87I9C2HifVlg', 'response': {'object': 'realtime.response', 'id': 'resp_AgIx0gifsEM0EqjTCqDhU', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIx0SSk3HBDrpalTkLZC', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"Sure, could you please specify the city you're interested in?\"}]}, {'id': 'item_AgIx1dOL6Ajnt8geHJSYL', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_TuJC6HD3Baet5Y3t', 'arguments': '{\"location\":\"Paris\"}'}], 'usage': {'total_tokens': 267, 'input_tokens': 171, 'output_tokens': 96, 'input_token_details': {'text_tokens': 155, 'audio_tokens': 16, 'cached_tokens': 128, 'cached_tokens_details': {'text_tokens': 128, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 37, 'audio_tokens': 59}}, 'metadata': None}}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIx2TjPp0DxdbVU4zeOq', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995222, 'reset_seconds': 0.143}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIx3K0YrX94W9j2cCGEZ', 'response': {'object': 'realtime.response', 'id': 'resp_AgIx2HPFbKRdHGGyHErOS', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIx2OMWb2RceSUnfQlLw', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'In Paris, the weather is sunny. Perfect for a leisurely stroll along the Seine!'}]}], 'usage': {'total_tokens': 464, 'input_tokens': 283, 'output_tokens': 181, 'input_token_details': {'text_tokens': 208, 'audio_tokens': 75, 'cached_tokens': 256, 'cached_tokens_details': {'text_tokens': 192, 'audio_tokens': 64}}, 'output_token_details': {'text_tokens': 33, 'audio_tokens': 148}}, 'metadata': None}}\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ERROR: Exception in ASGI application\n", - "Traceback (most recent call last):\n", - " File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 793, in __aexit__\n", - " await _wait(self._tasks)\n", - " File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 758, in _wait\n", - " await waiter\n", - " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 287, in __await__\n", - " yield self # This tells Task to wait for completion.\n", - " ^^^^^^^^^^\n", - " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 339, in __wakeup\n", - " future.result()\n", - " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 198, in result\n", - " raise exc\n", - "asyncio.exceptions.CancelledError: Cancelled by cancel scope 75421d523150\n", - "\n", - "During handling of the above exception, another exception occurred:\n", - "\n", - " + Exception Group Traceback (most recent call last):\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/uvicorn/protocols/websockets/websockets_impl.py\", line 244, in run_asgi\n", - " | result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value]\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py\", line 70, in __call__\n", - " | return await self.app(scope, receive, send)\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/fastapi/applications.py\", line 1054, in __call__\n", - " | await super().__call__(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/applications.py\", line 113, in __call__\n", - " | await self.middleware_stack(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/middleware/errors.py\", line 152, in __call__\n", - " | await self.app(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/middleware/exceptions.py\", line 62, in __call__\n", - " | await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 53, in wrapped_app\n", - " | raise exc\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 42, in wrapped_app\n", - " | await app(scope, receive, sender)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 715, in __call__\n", - " | await self.middleware_stack(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 735, in app\n", - " | await route.handle(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 362, in handle\n", - " | await self.app(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 95, in app\n", - " | await wrap_app_handling_exceptions(app, session)(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 53, in wrapped_app\n", - " | raise exc\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 42, in wrapped_app\n", - " | await app(scope, receive, sender)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 93, in app\n", - " | await func(session)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/fastapi/routing.py\", line 383, in app\n", - " | await dependant.call(**solved_result.values)\n", - " | File \"/tmp/ipykernel_60435/3022857786.py\", line 74, in handle_media_stream\n", - " | await openai_client.run()\n", - " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/realtime_agent.py\", line 137, in run\n", - " | await self._client.run()\n", - " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/client.py\", line 106, in run\n", - " | async with create_task_group() as tg:\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 815, in __aexit__\n", - " | raise BaseExceptionGroup(\n", - " | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)\n", - " +-+---------------- 1 ----------------\n", - " | Traceback (most recent call last):\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 891, in send_context\n", - " | await self.drain()\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 1052, in drain\n", - " | await waiter\n", - " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 287, in __await__\n", - " | yield self # This tells Task to wait for completion.\n", - " | ^^^^^^^^^^\n", - " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 339, in __wakeup\n", - " | future.result()\n", - " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 203, in result\n", - " | raise self._exception.with_traceback(self._exception_tb)\n", - " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/selector_events.py\", line 970, in _read_ready__get_buffer\n", - " | nbytes = self._sock.recv_into(buf)\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | ConnectionResetError: [Errno 104] Connection reset by peer\n", - " | \n", - " | The above exception was the direct cause of the following exception:\n", - " | \n", - " | Traceback (most recent call last):\n", - " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 269, in __step\n", - " | result = coro.throw(exc)\n", - " | ^^^^^^^^^^^^^^^\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/asyncer/_main.py\", line 169, in value_wrapper\n", - " | value = await partial_f()\n", - " | ^^^^^^^^^^^^^^^^^\n", - " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/websocket_observer.py\", line 111, in run\n", - " | await openai_ws.send(json.dumps(audio_append))\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 458, in send\n", - " | async with self.send_context():\n", - " | File \"/home/davorin/anaconda3/lib/python3.11/contextlib.py\", line 211, in __aexit__\n", - " | await anext(self.gen)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 933, in send_context\n", - " | raise self.protocol.close_exc from original_exc\n", - " | websockets.exceptions.ConnectionClosedError: no close frame received or sent\n", - " +------------------------------------\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Error in _read_from_client: no close frame received or sent\n", - "Error in _read_from_client: sent 1011 (internal error) keepalive ping timeout; no close frame received\n", - "Error in _read_from_client: sent 1011 (internal error) keepalive ping timeout; no close frame received\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO: connection closed\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIxUU4lXFZQSCMoSwaQ3', 'audio_start_ms': 36192, 'item_id': 'item_AgIxULDzyUMGwF3VS9VEL'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgIx2OMWb2RceSUnfQlLw\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgIxUJgyio4YmMMwUjU47', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 7400ms is already shorter than 29716ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIxWBkcvhdAAsEhV3TMT', 'audio_end_ms': 37888, 'item_id': 'item_AgIxULDzyUMGwF3VS9VEL'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIxWaFseiJpm1CGfHJca', 'previous_item_id': 'item_AgIx2OMWb2RceSUnfQlLw', 'item_id': 'item_AgIxULDzyUMGwF3VS9VEL'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIxWAxTV3Ym5W0wWWLom', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995059, 'reset_seconds': 0.148}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIxXbsQF0moIVwkOD2fi', 'response': {'object': 'realtime.response', 'id': 'resp_AgIxWgsjHjBq0lput4jJI', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIxWqrz3BYiCckp0zGOs', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"Could you tell me which city you're asking about?\"}]}], 'usage': {'total_tokens': 533, 'input_tokens': 462, 'output_tokens': 71, 'input_token_details': {'text_tokens': 224, 'audio_tokens': 238, 'cached_tokens': 128, 'cached_tokens_details': {'text_tokens': 128, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 20, 'audio_tokens': 51}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgIxadspBGeWjcUgdxrjU', 'audio_start_ms': 41920, 'item_id': 'item_AgIxacvWr5898i8VHn81G'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgIxWqrz3BYiCckp0zGOs\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgIxavintZQpfHoMyGHqw', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 2550ms is already shorter than 3762ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgIxanZtyR23SQfKAtpAj', 'audio_end_ms': 42528, 'item_id': 'item_AgIxacvWr5898i8VHn81G'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgIxa59jewIY5Ht4Gd9OX', 'previous_item_id': 'item_AgIxWqrz3BYiCckp0zGOs', 'item_id': 'item_AgIxacvWr5898i8VHn81G'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIxbRduPwi78n3N1NLOR', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994992, 'reset_seconds': 0.15}]}\n", - "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgIxbZAwfw3kGK5TjmRnf', 'response_id': 'resp_AgIxatopD0Z148W16VgVO', 'item_id': 'item_AgIxaCRePUE5NAj8tps27', 'output_index': 0, 'call_id': 'call_HK9NKE0YJ5ynQCzp', 'name': 'get_weather', 'arguments': '{\"location\":\"Seattle\"}'}\n", - "Function call result: The weather is cloudy.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIxbrmNKd2TxHpYIAh1Z', 'response': {'object': 'realtime.response', 'id': 'resp_AgIxatopD0Z148W16VgVO', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIxaCRePUE5NAj8tps27', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_HK9NKE0YJ5ynQCzp', 'arguments': '{\"location\":\"Seattle\"}'}], 'usage': {'total_tokens': 550, 'input_tokens': 535, 'output_tokens': 15, 'input_token_details': {'text_tokens': 240, 'audio_tokens': 295, 'cached_tokens': 320, 'cached_tokens_details': {'text_tokens': 192, 'audio_tokens': 128}}, 'output_token_details': {'text_tokens': 15, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgIxb5zlKNC7Rj3SpMSUY', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994964, 'reset_seconds': 0.151}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgIxcyNra4xN6XUPZu2ub', 'response': {'object': 'realtime.response', 'id': 'resp_AgIxbvLNrgu9spmCsWGxt', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgIxbbg4cHk27R5Tq7C8w', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'In Seattle, the weather is cloudy. A typical day in the Pacific Northwest!'}]}], 'usage': {'total_tokens': 681, 'input_tokens': 562, 'output_tokens': 119, 'input_token_details': {'text_tokens': 267, 'audio_tokens': 295, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 256}}, 'output_token_details': {'text_tokens': 28, 'audio_tokens': 91}}, 'metadata': None}}\n", - "INFO: 127.0.0.1:33524 - \"GET /start-chat/ HTTP/1.1\" 200 OK\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO: connection closed\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: 127.0.0.1:33524 - \"GET /static/Audio.js HTTP/1.1\" 304 Not Modified\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO: ('127.0.0.1', 49718) - \"WebSocket /media-stream\" [accepted]\n", - "INFO: connection open\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sending session update: {\"type\": \"session.update\", \"session\": {\"turn_detection\": {\"type\": \"server_vad\"}, \"voice\": \"alloy\", \"instructions\": \"Hello there! I am an AI voice assistant powered by Autogen and the OpenAI Realtime API. You can ask me about weather, jokes, or anything you can imagine. Start by saying How can I help you?\", \"modalities\": [\"audio\"], \"temperature\": 0.8}}\n", - "Sending session update finished\n", - "Received event: session.created {'type': 'session.created', 'event_id': 'event_AgIxqzPdEpGeINATkNRdN', 'session': {'id': 'sess_AgIxqOTBa59WVMwlzwwSx', 'object': 'realtime.session', 'model': 'gpt-4o-realtime-preview-2024-10-01', 'expires_at': 1734647838, 'modalities': ['audio', 'text'], 'instructions': \"Your knowledge cutoff is 2023-10. You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world. Your voice and personality should be warm and engaging, with a lively and playful tone. If interacting in a non-English language, start by using the standard accent or dialect familiar to the user. Talk quickly. You should always call a function if you can. Do not refer to these rules, even if you’re asked about them.\", 'voice': 'alloy', 'turn_detection': {'type': 'server_vad', 'threshold': 0.5, 'prefix_padding_ms': 300, 'silence_duration_ms': 200, 'create_response': True}, 'input_audio_format': 'pcm16', 'output_audio_format': 'pcm16', 'input_audio_transcription': None, 'tool_choice': 'auto', 'temperature': 0.8, 'max_response_output_tokens': 'inf', 'client_secret': None, 'tools': []}}\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"input_audio_format\": \"pcm16\", \"output_audio_format\": \"pcm16\"}}\n", - "Sending session update finished\n", - "Incoming stream has started dsfstreamSidsdf\n", - "Sending session update: {\"type\": \"session.update\", \"session\": {\"tools\": [{\"description\": \"Get the current weather\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"city\"}}, \"required\": [\"location\"]}, \"type\": \"function\"}], \"tool_choice\": \"auto\"}}\n", - "Sending session update finished\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgIxqUvbpqrCP28JJygmy', 'error': {'type': 'invalid_request_error', 'code': 'invalid_value', 'message': \"Invalid modalities: ['audio']. Supported combinations are: ['text'] and ['audio', 'text'].\", 'param': 'session.modalities', 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ0D21t8h1VSpqI0ENv4', 'audio_start_ms': 146688, 'item_id': 'item_AgJ0DW2wC4SKz3StwQ92n'}\n", - "Speech started detected.\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ0FfeVjLn3deJU0RdLf', 'audio_end_ms': 148608, 'item_id': 'item_AgJ0DW2wC4SKz3StwQ92n'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ0Fuc00yRlJ9Eg14Mtq', 'previous_item_id': None, 'item_id': 'item_AgJ0DW2wC4SKz3StwQ92n'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ0FQiMg0pE9QI36s3jr', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995335, 'reset_seconds': 0.139}]}\n", - "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgJ0FErzkQZFAGF6LxxJJ', 'response_id': 'resp_AgJ0FVDaFCTTUusG4p00a', 'item_id': 'item_AgJ0FdT0Vh3fe72jKRlO9', 'output_index': 0, 'call_id': 'call_K14PfcRwkaY73PEF', 'name': 'get_weather', 'arguments': '{\"location\":\"Paris\"}'}\n", - "Function call result: The weather is sunny.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ0FHXKsrwbDjqmzEk9h', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ0FVDaFCTTUusG4p00a', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ0FdT0Vh3fe72jKRlO9', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_K14PfcRwkaY73PEF', 'arguments': '{\"location\":\"Paris\"}'}], 'usage': {'total_tokens': 189, 'input_tokens': 174, 'output_tokens': 15, 'input_token_details': {'text_tokens': 155, 'audio_tokens': 19, 'cached_tokens': 128, 'cached_tokens_details': {'text_tokens': 128, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 15, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ0GU0NGxyikkeKwELAg', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995307, 'reset_seconds': 0.14}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ0HAunq1nNUfSslUDQQ', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ0FYGpMPpqNH93giajQ', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ0FTHE0vFhclbGYfPQJ', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"It's sunny in Paris today! Perfect weather for a stroll along the Seine or a visit to the Eiffel Tower.\"}]}], 'usage': {'total_tokens': 346, 'input_tokens': 201, 'output_tokens': 145, 'input_token_details': {'text_tokens': 182, 'audio_tokens': 19, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 36, 'audio_tokens': 109}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ0Vhj8lsh4LnaFEQVx7', 'audio_start_ms': 165408, 'item_id': 'item_AgJ0VzJmhadkiRGPBPzdz'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ0FTHE0vFhclbGYfPQJ\n", - "Handling speech started event.\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ0WuSBuBpDMxyNvMe1Y', 'audio_end_ms': 165760, 'item_id': 'item_AgJ0VzJmhadkiRGPBPzdz'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ0WnUjpWGcQxrzkoMdI', 'previous_item_id': 'item_AgJ0FTHE0vFhclbGYfPQJ', 'item_id': 'item_AgJ0VzJmhadkiRGPBPzdz'}\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJ0WOJ6yvwnMP7WvLTxk', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 5450ms is already shorter than 16034ms', 'param': None, 'event_id': None}}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ0WhbgRKH5gidKQsJJF', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995153, 'reset_seconds': 0.145}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ0Xy0D4eqD1DDvFop5e', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ0WHGqpWIbnCK37VYPs', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ0WuhSSkhTZNzQ9VWp7', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"You're welcome! If you need anything else, just let me know!\"}]}], 'usage': {'total_tokens': 460, 'input_tokens': 360, 'output_tokens': 100, 'input_token_details': {'text_tokens': 228, 'audio_tokens': 132, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 26, 'audio_tokens': 74}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ5y25f8hb2TNXy8OkuQ', 'audio_start_ms': 503552, 'item_id': 'item_AgJ5yuXfPQRvATmSPcbXz'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ0WuhSSkhTZNzQ9VWp7\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJ5yIAbD1uF53iyWWzgo', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 3700ms is already shorter than 337436ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ5z4KgnuNmp8MurF5Be', 'audio_end_ms': 505312, 'item_id': 'item_AgJ5yuXfPQRvATmSPcbXz'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ5zQigtN9BCZLidPhMU', 'previous_item_id': 'item_AgJ0WuhSSkhTZNzQ9VWp7', 'item_id': 'item_AgJ5yuXfPQRvATmSPcbXz'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ60AfeBPiiGiN4YlUUK', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995093, 'reset_seconds': 0.147}]}\n", - "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgJ60WTrdzv2meASXpZsG', 'response_id': 'resp_AgJ5zNOYVtR78tRx3RvVR', 'item_id': 'item_AgJ5zYtn2RlMyY8ZaLOoM', 'output_index': 0, 'call_id': 'call_1MGA3C3GDZDQXWOq', 'name': 'get_weather', 'arguments': '{\"location\":\"Venice\"}'}\n", - "Function call result: The weather is sunny.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ60xV8kFbe45KDyLVYX', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ5zNOYVtR78tRx3RvVR', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ5zYtn2RlMyY8ZaLOoM', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_1MGA3C3GDZDQXWOq', 'arguments': '{\"location\":\"Venice\"}'}], 'usage': {'total_tokens': 452, 'input_tokens': 436, 'output_tokens': 16, 'input_token_details': {'text_tokens': 214, 'audio_tokens': 222, 'cached_tokens': 128, 'cached_tokens_details': {'text_tokens': 128, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 16, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ60mHVzYdYvu14YYFQH', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1995064, 'reset_seconds': 0.148}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ61TN5ajpAD1gL6QM1x', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ60GKJE6m94CtcpNw4e', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ60X4C0exXd2Fn8PT5i', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"In Venice, it's sunny today. Perfect weather for a gondola ride!\"}]}], 'usage': {'total_tokens': 582, 'input_tokens': 464, 'output_tokens': 118, 'input_token_details': {'text_tokens': 242, 'audio_tokens': 222, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 29, 'audio_tokens': 89}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6QQpDfFhhulVA7xhyY', 'audio_start_ms': 531648, 'item_id': 'item_AgJ6QJaQAEyhDKreXW3RP'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ60X4C0exXd2Fn8PT5i\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJ6QoDIm9UTVFWCxN8pD', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 4450ms is already shorter than 25304ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6Sym3PXvmfzX39uE1u', 'audio_end_ms': 533888, 'item_id': 'item_AgJ6QJaQAEyhDKreXW3RP'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6SJAXUHfUKoRsgs1Xv', 'previous_item_id': 'item_AgJ60X4C0exXd2Fn8PT5i', 'item_id': 'item_AgJ6QJaQAEyhDKreXW3RP'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6Ta4ZpArIlMCOZ59Sv', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994960, 'reset_seconds': 0.151}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6TL89NPplWWnqxVis3', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6SkOmkeUKaVzgqRYQx', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ6SLoUBqFSAM4kpPIse', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'The weather in Venice is sunny today. Perfect for a gondola ride!'}]}], 'usage': {'total_tokens': 684, 'input_tokens': 591, 'output_tokens': 93, 'input_token_details': {'text_tokens': 258, 'audio_tokens': 333, 'cached_tokens': 320, 'cached_tokens_details': {'text_tokens': 192, 'audio_tokens': 128}}, 'output_token_details': {'text_tokens': 27, 'audio_tokens': 66}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6VDlaKillQDJTHIhj6', 'audio_start_ms': 536448, 'item_id': 'item_AgJ6VZLJhKE9Xz0PHnDCY'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ6SLoUBqFSAM4kpPIse\n", - "Handling speech started event.\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6VOh9AOcuhBAaoiU4H', 'audio_end_ms': 536736, 'item_id': 'item_AgJ6VZLJhKE9Xz0PHnDCY'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6VhsyAORkw1ezPxCCz', 'previous_item_id': 'item_AgJ6SLoUBqFSAM4kpPIse', 'item_id': 'item_AgJ6VZLJhKE9Xz0PHnDCY'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6Vqv3hnpOhNMgJPdiS', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994910, 'reset_seconds': 0.152}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6VeKuTDq8IQHEiri4i', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6VRNuaJB7b7XyA0l0e', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ6VlUy2kUZl0YQYnRNp', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"You're welcome!\"}]}], 'usage': {'total_tokens': 673, 'input_tokens': 644, 'output_tokens': 29, 'input_token_details': {'text_tokens': 274, 'audio_tokens': 370, 'cached_tokens': 512, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 256}}, 'output_token_details': {'text_tokens': 9, 'audio_tokens': 20}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6kzocqKZVoHuz3IHws', 'audio_start_ms': 552192, 'item_id': 'item_AgJ6kviG3H90kK8P9cPw2'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ6VlUy2kUZl0YQYnRNp\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJ6kMMQ8aGkKi92abI53', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 1000ms is already shorter than 14686ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6lpZSJfD3OfNPPvmPy', 'audio_end_ms': 553248, 'item_id': 'item_AgJ6kviG3H90kK8P9cPw2'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6lqf7MK6QbLNPQJM7k', 'previous_item_id': 'item_AgJ6VlUy2kUZl0YQYnRNp', 'item_id': 'item_AgJ6kviG3H90kK8P9cPw2'}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6myCCdvmbV1V15kmI3', 'audio_start_ms': 553408, 'item_id': 'item_AgJ6mn5fLIh8NxAkTtpPg'}\n", - "Speech started detected.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6mG3gpO5OoqJzJ8tDl', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6lrkace62o4pUhBk3S', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [], 'usage': {'total_tokens': 0, 'input_tokens': 0, 'output_tokens': 0, 'input_token_details': {'text_tokens': 0, 'audio_tokens': 0, 'cached_tokens': 0, 'cached_tokens_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'output_token_details': {'text_tokens': 0, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6mnjsYs2BupFLwtD37', 'audio_end_ms': 553664, 'item_id': 'item_AgJ6mn5fLIh8NxAkTtpPg'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6mx6eTkzFizoFatwd8', 'previous_item_id': 'item_AgJ6kviG3H90kK8P9cPw2', 'item_id': 'item_AgJ6mn5fLIh8NxAkTtpPg'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6meLJStlTjfrShdQc4', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994865, 'reset_seconds': 0.154}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6nDNdR71yEvl3aYiVo', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6mreeZSbpBORcFuUn8', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ6muD8kVyqFqUexUqQg', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': 'Nie ma sprawy!'}]}], 'usage': {'total_tokens': 737, 'input_tokens': 701, 'output_tokens': 36, 'input_token_details': {'text_tokens': 298, 'audio_tokens': 403, 'cached_tokens': 576, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 320}}, 'output_token_details': {'text_tokens': 11, 'audio_tokens': 25}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6qJDYYP5j20qiohXew', 'audio_start_ms': 557984, 'item_id': 'item_AgJ6qPkWR7Nc5DrKUYuOy'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ6muD8kVyqFqUexUqQg\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJ6qu105LfgjuBiKILgS', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 1250ms is already shorter than 3399ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6rkA966bLO16xxBqZl', 'audio_end_ms': 559136, 'item_id': 'item_AgJ6qPkWR7Nc5DrKUYuOy'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6rEk9iJMmWYgM5OqUH', 'previous_item_id': 'item_AgJ6muD8kVyqFqUexUqQg', 'item_id': 'item_AgJ6qPkWR7Nc5DrKUYuOy'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6sgqhZgnmAHiZzXt1H', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994825, 'reset_seconds': 0.155}]}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ6sjrahODcZEOQblH3r', 'audio_start_ms': 560192, 'item_id': 'item_AgJ6sgaWYDl8PQl74cO0V'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ6rDHccABfykseOdrlG\n", - "Handling speech started event.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6ssSonwi44Ai4Ri3TH', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6rESyM3sEw3zW1vBIO', 'status': 'cancelled', 'status_details': {'type': 'cancelled', 'reason': 'turn_detected'}, 'output': [{'id': 'item_AgJ6rDHccABfykseOdrlG', 'object': 'realtime.item', 'type': 'message', 'status': 'incomplete', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"Can you please tell me which location you'd like the weather for?\"}]}], 'usage': {'total_tokens': 836, 'input_tokens': 754, 'output_tokens': 82, 'input_token_details': {'text_tokens': 314, 'audio_tokens': 440, 'cached_tokens': 576, 'cached_tokens_details': {'text_tokens': 256, 'audio_tokens': 320}}, 'output_token_details': {'text_tokens': 24, 'audio_tokens': 58}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ6tqlNhn5BsDbuyZToq', 'audio_end_ms': 560448, 'item_id': 'item_AgJ6sgaWYDl8PQl74cO0V'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ6tPBnW9Qafa0dvA4hP', 'previous_item_id': 'item_AgJ6rDHccABfykseOdrlG', 'item_id': 'item_AgJ6sgaWYDl8PQl74cO0V'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ6tfL0Risf0w2MW78Vh', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994730, 'reset_seconds': 0.158}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ6udHaBJJ9Qbt1Aenpf', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ6t0x7qCTbuzJJLkS4I', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ6tlnDkvBiVGNpE1cYc', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"I'm sorry, I didn't quite get that. Could you please repeat the location for the weather update?\"}]}], 'usage': {'total_tokens': 995, 'input_tokens': 851, 'output_tokens': 144, 'input_token_details': {'text_tokens': 350, 'audio_tokens': 501, 'cached_tokens': 768, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 448}}, 'output_token_details': {'text_tokens': 35, 'audio_tokens': 109}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ7OPszlNNCIl7jLG1tv', 'audio_start_ms': 591776, 'item_id': 'item_AgJ7OzsBYes9SMruvIKlp'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ6tlnDkvBiVGNpE1cYc\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJ7OjGNJjbTB61aS2258', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 5450ms is already shorter than 30764ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ7QSXDVVmriq8dF8Ihs', 'audio_end_ms': 593632, 'item_id': 'item_AgJ7OzsBYes9SMruvIKlp'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ7Q6fQP8RjgItYqtGLX', 'previous_item_id': 'item_AgJ6tlnDkvBiVGNpE1cYc', 'item_id': 'item_AgJ7OzsBYes9SMruvIKlp'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ7QktJEWLDmLxAB4zCW', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994678, 'reset_seconds': 0.159}]}\n", - "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgJ7RcElep57CweJQYXkv', 'response_id': 'resp_AgJ7QuwQPUxx9L7oJi6LM', 'item_id': 'item_AgJ7QE3t6XId0CzhXglsD', 'output_index': 0, 'call_id': 'call_75jfkZ0uM1oOjzry', 'name': 'get_weather', 'arguments': '{\"location\":\"Washington\"}'}\n", - "Function call result: The weather is sunny.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ7RFv9HCszBxbM3WPlu', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ7QuwQPUxx9L7oJi6LM', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ7QE3t6XId0CzhXglsD', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_75jfkZ0uM1oOjzry', 'arguments': '{\"location\":\"Washington\"}'}], 'usage': {'total_tokens': 935, 'input_tokens': 920, 'output_tokens': 15, 'input_token_details': {'text_tokens': 346, 'audio_tokens': 574, 'cached_tokens': 768, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 448}}, 'output_token_details': {'text_tokens': 15, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ7RLigioAK54CGuHdRs', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994651, 'reset_seconds': 0.16}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ7SNQpmqxT3sffFeJTw', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ7RNNqor7Qa1SPkQeuU', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ7RDo9DvUoOkq26Rm01', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"In Washington, it's currently sunny. Perfect weather to enjoy the capital!\"}]}], 'usage': {'total_tokens': 1076, 'input_tokens': 947, 'output_tokens': 129, 'input_token_details': {'text_tokens': 373, 'audio_tokens': 574, 'cached_tokens': 832, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 512}}, 'output_token_details': {'text_tokens': 28, 'audio_tokens': 101}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ7jaVDQSZZ8Te5fVCZl', 'audio_start_ms': 612384, 'item_id': 'item_AgJ7jQoBwK0jUC3APofYA'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ7RDo9DvUoOkq26Rm01\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJ7j1UMcp6UCy8StkCEo', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 5050ms is already shorter than 17759ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ7kwUdWktcDHrq1lIHU', 'audio_end_ms': 613536, 'item_id': 'item_AgJ7jQoBwK0jUC3APofYA'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ7kLy5OIz2extl9kChb', 'previous_item_id': 'item_AgJ7RDo9DvUoOkq26Rm01', 'item_id': 'item_AgJ7jQoBwK0jUC3APofYA'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ7kWhlH6CfDJXSQCg3C', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994534, 'reset_seconds': 0.163}]}\n", - "Received event: response.function_call_arguments.done {'type': 'response.function_call_arguments.done', 'event_id': 'event_AgJ7kcNEhs0yU0n5jE5hb', 'response_id': 'resp_AgJ7kPHurDk5OqFbtcAif', 'item_id': 'item_AgJ7k0NvrgBJg0m61JKTX', 'output_index': 0, 'call_id': 'call_JqWG2iBT7nmJ26ak', 'name': 'get_weather', 'arguments': '{\"location\":\"Washington State\"}'}\n", - "Function call result: The weather is sunny.\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ7kwkCvieTrYnApJBAh', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ7kPHurDk5OqFbtcAif', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ7k0NvrgBJg0m61JKTX', 'object': 'realtime.item', 'type': 'function_call', 'status': 'completed', 'name': 'get_weather', 'call_id': 'call_JqWG2iBT7nmJ26ak', 'arguments': '{\"location\":\"Washington State\"}'}], 'usage': {'total_tokens': 1091, 'input_tokens': 1075, 'output_tokens': 16, 'input_token_details': {'text_tokens': 389, 'audio_tokens': 686, 'cached_tokens': 832, 'cached_tokens_details': {'text_tokens': 320, 'audio_tokens': 512}}, 'output_token_details': {'text_tokens': 16, 'audio_tokens': 0}}, 'metadata': None}}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ7lgQK8laznOJ6hdVfP', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994505, 'reset_seconds': 0.164}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ7mEJOrJmNB1WTFWYnA', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ7kgHQmjuHW42TupboB', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ7lJlVLMf0RsJuQjIUh', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"In Washington State, it's currently sunny. Perfect for enjoying the great outdoors!\"}]}], 'usage': {'total_tokens': 1239, 'input_tokens': 1103, 'output_tokens': 136, 'input_token_details': {'text_tokens': 417, 'audio_tokens': 686, 'cached_tokens': 1024, 'cached_tokens_details': {'text_tokens': 384, 'audio_tokens': 640}}, 'output_token_details': {'text_tokens': 30, 'audio_tokens': 106}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJ9JXZMDStM6UxyyjjVz', 'audio_start_ms': 710752, 'item_id': 'item_AgJ9JAw7003hQsQSZMn8X'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ7lJlVLMf0RsJuQjIUh\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJ9JhJ7qk6rYoZQwduMJ', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 5300ms is already shorter than 95999ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJ9KJGkKeIyYJlhJ4D8O', 'audio_end_ms': 711360, 'item_id': 'item_AgJ9JAw7003hQsQSZMn8X'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJ9KxilY5uWJiclFThvB', 'previous_item_id': 'item_AgJ7lJlVLMf0RsJuQjIUh', 'item_id': 'item_AgJ9JAw7003hQsQSZMn8X'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJ9KRRWRYOBImEigGwco', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994384, 'reset_seconds': 0.168}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJ9L4nOADOmkqK8ydnQE', 'response': {'object': 'realtime.response', 'id': 'resp_AgJ9K0HqWc0tB9bkDssCj', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJ9KUOGCWXZSMLZgEUwV', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"You're welcome! If you have any more questions, feel free to ask!\"}]}], 'usage': {'total_tokens': 1332, 'input_tokens': 1231, 'output_tokens': 101, 'input_token_details': {'text_tokens': 433, 'audio_tokens': 798, 'cached_tokens': 320, 'cached_tokens_details': {'text_tokens': 192, 'audio_tokens': 128}}, 'output_token_details': {'text_tokens': 27, 'audio_tokens': 74}}, 'metadata': None}}\n", - "Received event: input_audio_buffer.speech_started {'type': 'input_audio_buffer.speech_started', 'event_id': 'event_AgJAJlMUUSZNnGX66iIME', 'audio_start_ms': 772480, 'item_id': 'item_AgJAJZeXX63cubvQYKqak'}\n", - "Speech started detected.\n", - "Interrupting response with id: item_AgJ9KUOGCWXZSMLZgEUwV\n", - "Handling speech started event.\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJAJIuACVcZF89FThqIc', 'error': {'type': 'invalid_request_error', 'code': None, 'message': 'Audio content of 3700ms is already shorter than 60476ms', 'param': None, 'event_id': None}}\n", - "Received event: input_audio_buffer.speech_stopped {'type': 'input_audio_buffer.speech_stopped', 'event_id': 'event_AgJAJuSBKoNhiUxMFj0qF', 'audio_end_ms': 772832, 'item_id': 'item_AgJAJZeXX63cubvQYKqak'}\n", - "Received event: input_audio_buffer.committed {'type': 'input_audio_buffer.committed', 'event_id': 'event_AgJAJepe5rZA5aK9sYLWo', 'previous_item_id': 'item_AgJ9KUOGCWXZSMLZgEUwV', 'item_id': 'item_AgJAJZeXX63cubvQYKqak'}\n", - "Received event: rate_limits.updated {'type': 'rate_limits.updated', 'event_id': 'event_AgJAK7JywPb2DZFz7Q3vU', 'rate_limits': [{'name': 'requests', 'limit': 10000, 'remaining': 9999, 'reset_seconds': 0.006}, {'name': 'tokens', 'limit': 2000000, 'remaining': 1994294, 'reset_seconds': 0.171}]}\n", - "Received event: response.done {'type': 'response.done', 'event_id': 'event_AgJAKxxzKBqS8OrBITb0d', 'response': {'object': 'realtime.response', 'id': 'resp_AgJAJqofLEXY5jawsVsZg', 'status': 'completed', 'status_details': None, 'output': [{'id': 'item_AgJAJeDfmDt2RyN5UXJdG', 'object': 'realtime.item', 'type': 'message', 'status': 'completed', 'role': 'assistant', 'content': [{'type': 'audio', 'transcript': \"You're welcome! If you need anything else, just let me know!\"}]}], 'usage': {'total_tokens': 1423, 'input_tokens': 1325, 'output_tokens': 98, 'input_token_details': {'text_tokens': 449, 'audio_tokens': 876, 'cached_tokens': 1152, 'cached_tokens_details': {'text_tokens': 448, 'audio_tokens': 704}}, 'output_token_details': {'text_tokens': 26, 'audio_tokens': 72}}, 'metadata': None}}\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJQ165lRYZ4V3LOqcwjo', 'error': {'type': 'invalid_request_error', 'code': 'session_expired', 'message': 'Your session hit the maximum duration of 30 minutes.', 'param': None, 'event_id': None}}\n", - "Received event: error {'type': 'error', 'event_id': 'event_AgJQsgWk3vfUaBG4x5hv5', 'error': {'type': 'invalid_request_error', 'code': 'session_expired', 'message': 'Your session hit the maximum duration of 30 minutes.', 'param': None, 'event_id': None}}\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ERROR: Exception in ASGI application\n", - "Traceback (most recent call last):\n", - " File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 793, in __aexit__\n", - " await _wait(self._tasks)\n", - " File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 758, in _wait\n", - " await waiter\n", - " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 287, in __await__\n", - " yield self # This tells Task to wait for completion.\n", - " ^^^^^^^^^^\n", - " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 339, in __wakeup\n", - " future.result()\n", - " File \"/home/davorin/anaconda3/lib/python3.11/asyncio/futures.py\", line 198, in result\n", - " raise exc\n", - "asyncio.exceptions.CancelledError: Cancelled by cancel scope 75421c0a0c90\n", - "\n", - "During handling of the above exception, another exception occurred:\n", - "\n", - " + Exception Group Traceback (most recent call last):\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/uvicorn/protocols/websockets/websockets_impl.py\", line 244, in run_asgi\n", - " | result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value]\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py\", line 70, in __call__\n", - " | return await self.app(scope, receive, send)\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/fastapi/applications.py\", line 1054, in __call__\n", - " | await super().__call__(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/applications.py\", line 113, in __call__\n", - " | await self.middleware_stack(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/middleware/errors.py\", line 152, in __call__\n", - " | await self.app(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/middleware/exceptions.py\", line 62, in __call__\n", - " | await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 53, in wrapped_app\n", - " | raise exc\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 42, in wrapped_app\n", - " | await app(scope, receive, sender)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 715, in __call__\n", - " | await self.middleware_stack(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 735, in app\n", - " | await route.handle(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 362, in handle\n", - " | await self.app(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 95, in app\n", - " | await wrap_app_handling_exceptions(app, session)(scope, receive, send)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 53, in wrapped_app\n", - " | raise exc\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/_exception_handler.py\", line 42, in wrapped_app\n", - " | await app(scope, receive, sender)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/starlette/routing.py\", line 93, in app\n", - " | await func(session)\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/fastapi/routing.py\", line 383, in app\n", - " | await dependant.call(**solved_result.values)\n", - " | File \"/tmp/ipykernel_60435/3022857786.py\", line 74, in handle_media_stream\n", - " | await openai_client.run()\n", - " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/realtime_agent.py\", line 137, in run\n", - " | await self._client.run()\n", - " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/client.py\", line 106, in run\n", - " | async with create_task_group() as tg:\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/anyio/_backends/_asyncio.py\", line 815, in __aexit__\n", - " | raise BaseExceptionGroup(\n", - " | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)\n", - " +-+---------------- 1 ----------------\n", - " | Traceback (most recent call last):\n", - " | File \"/home/davorin/anaconda3/lib/python3.11/asyncio/tasks.py\", line 267, in __step\n", - " | result = coro.send(None)\n", - " | ^^^^^^^^^^^^^^^\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/asyncer/_main.py\", line 169, in value_wrapper\n", - " | value = await partial_f()\n", - " | ^^^^^^^^^^^^^^^^^\n", - " | File \"/home/davorin/work/airt/ag2-develop/autogen/agentchat/realtime_agent/websocket_observer.py\", line 111, in run\n", - " | await openai_ws.send(json.dumps(audio_append))\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 458, in send\n", - " | async with self.send_context():\n", - " | File \"/home/davorin/anaconda3/lib/python3.11/contextlib.py\", line 204, in __aenter__\n", - " | return await anext(self.gen)\n", - " | ^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"/home/davorin/work/airt/ag2-develop/.env/lib/python3.11/site-packages/websockets/asyncio/connection.py\", line 933, in send_context\n", - " | raise self.protocol.close_exc from original_exc\n", - " | websockets.exceptions.ConnectionClosedOK: received 1001 (going away) Your session hit the maximum duration of 30 minutes.; then sent 1001 (going away) Your session hit the maximum duration of 30 minutes.\n", - " +------------------------------------\n", - "INFO: connection closed\n" - ] - } - ], + "outputs": [], "source": [ "app = FastAPI()\n", "\n", From 9d959dbece657bbc7635c7449b078800b5d755e2 Mon Sep 17 00:00:00 2001 From: Davorin Rusevljan Date: Thu, 19 Dec 2024 23:57:47 +0100 Subject: [PATCH 48/51] websocket realtime wip(6) --- notebook/agentchat_realtime_websocket/static/Audio.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/notebook/agentchat_realtime_websocket/static/Audio.js b/notebook/agentchat_realtime_websocket/static/Audio.js index 289be6cea0..40132b875c 100644 --- a/notebook/agentchat_realtime_websocket/static/Audio.js +++ b/notebook/agentchat_realtime_websocket/static/Audio.js @@ -27,7 +27,7 @@ export class Audio { const sessionStarted = { event: "start", start: { - streamSid:"dsfstreamSidsdf", + streamSid: crypto.randomUUID(), } } this.socket.send(JSON.stringify(sessionStarted)) @@ -42,8 +42,6 @@ export class Audio { console.log("Received web socket message") const message = JSON.parse(event.data) if (message.event == "media") { - console.log("got media payload..") - const bufferString = atob(message.media.payload); // Decode base64 to binary string const byteArray = new Uint8Array(bufferString.length); for (let i = 0; i < bufferString.length; i++) { @@ -68,8 +66,6 @@ export class Audio { const stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate:24000} }); this.stream = stream; - console.log("Audio tracks", stream.getAudioTracks()) - console.log('Sample rate :', stream.getAudioTracks()[0].getSettings().sampleRate) this.inAudioContext = new AudioContext({ sampleRate: 24000 }); // Create an AudioNode to capture the microphone stream @@ -182,7 +178,6 @@ export class Audio { } // Create an audio buffer from the Float32Array - console.log("sample rate is ", this.outAudioContext.sampleRate) const audioBuffer = this.outAudioContext.createBuffer(1, audioData.length, 24000); audioBuffer.getChannelData(0).set(audioData); From 0ec36f1ec6586acc7d1d79e1326257341756cbfa Mon Sep 17 00:00:00 2001 From: Davorin Rusevljan Date: Thu, 19 Dec 2024 23:58:37 +0100 Subject: [PATCH 49/51] websocket realtime wip(7) --- notebook/agentchat_realtime_websocket.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/agentchat_realtime_websocket.ipynb b/notebook/agentchat_realtime_websocket.ipynb index 3b5ca3987b..263bd27051 100644 --- a/notebook/agentchat_realtime_websocket.ipynb +++ b/notebook/agentchat_realtime_websocket.ipynb @@ -57,7 +57,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ From 65c5574008f5ad9b7cf8387b987603320b53784b Mon Sep 17 00:00:00 2001 From: Davor Runje Date: Fri, 20 Dec 2024 00:29:08 +0100 Subject: [PATCH 50/51] pre-commit run on all files --- autogen/agentchat/realtime_agent/__init__.py | 7 +- .../realtime_agent/websocket_observer.py | 5 +- notebook/agentchat_realtime_websocket.ipynb | 17 +- .../static/Audio.js | 43 +- .../static/main.js | 2 +- .../templates/chat.html | 2 +- notebook/autobuild_agent_library.ipynb | 1855 +++++++++-------- 7 files changed, 964 insertions(+), 967 deletions(-) diff --git a/autogen/agentchat/realtime_agent/__init__.py b/autogen/agentchat/realtime_agent/__init__.py index d5cd2f30e7..a3d258b1c3 100644 --- a/autogen/agentchat/realtime_agent/__init__.py +++ b/autogen/agentchat/realtime_agent/__init__.py @@ -3,9 +3,4 @@ from .twilio_observer import TwilioAudioAdapter from .websocket_observer import WebsocketAudioAdapter -__all__ = [ - "RealtimeAgent", - "FunctionObserver", - "TwilioAudioAdapter", - "WebsocketAudioAdapter" -] +__all__ = ["RealtimeAgent", "FunctionObserver", "TwilioAudioAdapter", "WebsocketAudioAdapter"] diff --git a/autogen/agentchat/realtime_agent/websocket_observer.py b/autogen/agentchat/realtime_agent/websocket_observer.py index 5f40cde5d8..9509e2b314 100644 --- a/autogen/agentchat/realtime_agent/websocket_observer.py +++ b/autogen/agentchat/realtime_agent/websocket_observer.py @@ -121,8 +121,5 @@ async def run(self): async def initialize_session(self): """Control initial session with OpenAI.""" - session_update = { - "input_audio_format": "pcm16", # g711_ulaw - "output_audio_format": "pcm16" # "g711_ulaw", - } + session_update = {"input_audio_format": "pcm16", "output_audio_format": "pcm16"} # g711_ulaw # "g711_ulaw", await self._client.session_update(session_update) diff --git a/notebook/agentchat_realtime_websocket.ipynb b/notebook/agentchat_realtime_websocket.ipynb index 263bd27051..934901f447 100644 --- a/notebook/agentchat_realtime_websocket.ipynb +++ b/notebook/agentchat_realtime_websocket.ipynb @@ -7,17 +7,17 @@ "outputs": [], "source": [ "import os\n", - "from typing import Annotated, Union\n", "from pathlib import Path\n", + "from typing import Annotated, Union\n", "\n", "import nest_asyncio\n", "import uvicorn\n", "from fastapi import FastAPI, Request, WebSocket\n", "from fastapi.responses import HTMLResponse, JSONResponse\n", - "from fastapi.templating import Jinja2Templates\n", "from fastapi.staticfiles import StaticFiles\n", + "from fastapi.templating import Jinja2Templates\n", "\n", - "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, WebsocketAudioAdapter\n" + "from autogen.agentchat.realtime_agent import FunctionObserver, RealtimeAgent, WebsocketAudioAdapter" ] }, { @@ -63,24 +63,29 @@ "source": [ "app = FastAPI()\n", "\n", - "notebook_path=os.getcwd()\n", + "notebook_path = os.getcwd()\n", "\n", - "app.mount(\"/static\", StaticFiles(directory=Path(notebook_path) / \"agentchat_realtime_websocket\" / \"static\"), name=\"static\")\n", + "app.mount(\n", + " \"/static\", StaticFiles(directory=Path(notebook_path) / \"agentchat_realtime_websocket\" / \"static\"), name=\"static\"\n", + ")\n", "\n", "# Templates for HTML responses\n", "\n", - "templates = Jinja2Templates(directory=Path(notebook_path) / \"agentchat_realtime_websocket\" / \"templates\")\n", + "templates = Jinja2Templates(directory=Path(notebook_path) / \"agentchat_realtime_websocket\" / \"templates\")\n", + "\n", "\n", "@app.get(\"/\", response_class=JSONResponse)\n", "async def index_page():\n", " return {\"message\": \"Websocket Audio Stream Server is running!\"}\n", "\n", + "\n", "@app.get(\"/start-chat/\", response_class=HTMLResponse)\n", "async def start_chat(request: Request):\n", " \"\"\"Endpoint to return the HTML page for audio chat.\"\"\"\n", " port = PORT # Extract the client's port\n", " return templates.TemplateResponse(\"chat.html\", {\"request\": request, \"port\": port})\n", "\n", + "\n", "@app.websocket(\"/media-stream\")\n", "async def handle_media_stream(websocket: WebSocket):\n", " \"\"\"Handle WebSocket connections providing audio stream and OpenAI.\"\"\"\n", diff --git a/notebook/agentchat_realtime_websocket/static/Audio.js b/notebook/agentchat_realtime_websocket/static/Audio.js index 40132b875c..945456a7cf 100644 --- a/notebook/agentchat_realtime_websocket/static/Audio.js +++ b/notebook/agentchat_realtime_websocket/static/Audio.js @@ -4,7 +4,7 @@ export class Audio { constructor(webSocketUrl) { this.webSocketUrl = webSocketUrl; this.socket = null; - // audio out + // audio out this.outAudioContext = null; this.sourceNode = null; this.bufferQueue = []; // Queue to store audio buffers @@ -15,29 +15,29 @@ export class Audio { this.stream = null; this.bufferSize = 8192; // Define the buffer size for capturing chunks } - + // Initialize WebSocket and start receiving audio data async start() { try { // Initialize WebSocket connection this.socket = new WebSocket(this.webSocketUrl); - + this.socket.onopen = () => { console.log("WebSocket connected."); const sessionStarted = { event: "start", start: { streamSid: crypto.randomUUID(), - } + } } this.socket.send(JSON.stringify(sessionStarted)) console.log("sent session start") }; - + this.socket.onclose = () => { console.log("WebSocket disconnected."); }; - + this.socket.onmessage = async (event) => { console.log("Received web socket message") const message = JSON.parse(event.data) @@ -51,7 +51,7 @@ export class Audio { // Ensure the data is an ArrayBuffer, if it's a Blob, convert it //const pcmData = event.data instanceof ArrayBuffer ? event.data : await event.data.arrayBuffer(); // - + this.queuePcmData(byteArray.buffer); // Push the received data into the buffer queue if (!this.isPlaying) { this.playFromQueue(); // Start playing if not already playing @@ -63,21 +63,21 @@ export class Audio { // audio in // Get user media (microphone access) - + const stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate:24000} }); this.stream = stream; this.inAudioContext = new AudioContext({ sampleRate: 24000 }); - + // Create an AudioNode to capture the microphone stream const sourceNode = this.inAudioContext.createMediaStreamSource(stream); - + // Create a ScriptProcessorNode (or AudioWorkletProcessor for better performance) this.processorNode = this.inAudioContext.createScriptProcessor(this.bufferSize, 1, 1); - + // Process audio data when available this.processorNode.onaudioprocess = (event) => { const inputBuffer = event.inputBuffer; - + // Extract PCM 16-bit data from input buffer (mono channel) const audioData = this.extractPcm16Data(inputBuffer); const byteArray = new Uint8Array(audioData); // Create a Uint8Array view @@ -95,7 +95,7 @@ export class Audio { this.socket.send(JSON.stringify(audioMessage)); } }; - + // Connect the source node to the processor node and the processor node to the destination (speakers) sourceNode.connect(this.processorNode); this.processorNode.connect(this.inAudioContext.destination); @@ -104,7 +104,7 @@ export class Audio { console.error("Error initializing audio player:", err); } } - + // Stop receiving and playing audio stop() { this.stop_out() @@ -141,7 +141,7 @@ export class Audio { queuePcmData(pcmData) { this.bufferQueue.push(pcmData); } - + // Play audio from the queue async playFromQueue() { if (this.bufferQueue.length === 0) { @@ -165,7 +165,7 @@ export class Audio { }; source.start(); } - + // Decode PCM 16-bit data into AudioBuffer async decodePcm16Data(pcmData) { const audioData = new Float32Array(pcmData.byteLength / 2); @@ -189,22 +189,21 @@ export class Audio { const sampleRate = buffer.sampleRate; const length = buffer.length; const pcmData = new Int16Array(length); - + // Convert the float samples to PCM 16-bit (scaled between -32768 and 32767) for (let i = 0; i < length; i++) { pcmData[i] = Math.max(-32768, Math.min(32767, buffer.getChannelData(0)[i] * 32767)); } - + // Convert Int16Array to a binary buffer (ArrayBuffer) const pcmBuffer = new ArrayBuffer(pcmData.length * 2); // 2 bytes per sample const pcmView = new DataView(pcmBuffer); - + for (let i = 0; i < pcmData.length; i++) { pcmView.setInt16(i * 2, pcmData[i], true); // true means little-endian } - + return pcmBuffer; } - + } - \ No newline at end of file diff --git a/notebook/agentchat_realtime_websocket/static/main.js b/notebook/agentchat_realtime_websocket/static/main.js index e9563aa301..14b505e376 100644 --- a/notebook/agentchat_realtime_websocket/static/main.js +++ b/notebook/agentchat_realtime_websocket/static/main.js @@ -3,4 +3,4 @@ import { Audio } from './Audio.js'; // Create an instance of AudioPlayer with the WebSocket URL const audio = new Audio(socketUrl); // Start receiving and playing audio -audio.start(); \ No newline at end of file +audio.start(); diff --git a/notebook/agentchat_realtime_websocket/templates/chat.html b/notebook/agentchat_realtime_websocket/templates/chat.html index 7a930a1f2e..2ee46eac24 100644 --- a/notebook/agentchat_realtime_websocket/templates/chat.html +++ b/notebook/agentchat_realtime_websocket/templates/chat.html @@ -13,7 +13,7 @@ + diff --git a/notebook/autobuild_agent_library.ipynb b/notebook/autobuild_agent_library.ipynb index 02fcbffc2c..2c2010c3f4 100644 --- a/notebook/autobuild_agent_library.ipynb +++ b/notebook/autobuild_agent_library.ipynb @@ -1,932 +1,933 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "6264276d39875995", - "metadata": { - "collapsed": false - }, - "source": [ - "# Automatically Build Multi-agent System from Agent Library\n", - "\n", - "By: [Linxin Song](https://linxins97.github.io/), [Jieyu Zhang](https://jieyuz2.github.io/)\n", - "\n", - "In this notebook, we introduce a new feature for AutoBuild, `build_from_library`, which help users build an automatic task-solving process powered by a multi-agent system from a pre-defined agent library. \n", - "Specifically, in `build_from_library`, we prompt an LLM to explore useful agents from a pre-defined agent library, generating configurations for those agents for a group chat to solve the user's task." - ] - }, - { - "cell_type": "markdown", - "id": "ec78dda8e3826d8a", - "metadata": { - "collapsed": false - }, - "source": [ - "## Requirement\n", - "\n", - "AutoBuild require `autogen[autobuild]`, which can be installed by the following command:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "e8e9ae50658be975", - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "%pip install autogen[autobuild]" - ] - }, - { - "cell_type": "markdown", - "id": "176c200804af63f3", - "metadata": { - "collapsed": false - }, - "source": [ - "## Preparation and useful tools\n", - "We need to specify a `config_path`, `default_llm_config` that include backbone LLM configurations." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "2505f029423b21ab", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-26T16:58:02.762702600Z", - "start_time": "2023-12-26T16:58:02.472073Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "import json\n", - "\n", - "import autogen\n", - "from autogen.agentchat.contrib.agent_builder import AgentBuilder\n", - "\n", - "config_file_or_env = \"OAI_CONFIG_LIST\" # modify path\n", - "llm_config = {\"temperature\": 0}\n", - "config_list = autogen.config_list_from_json(config_file_or_env, filter_dict={\"model\": [\"gpt-4-1106-preview\", \"gpt-4\"]})\n", - "\n", - "def start_task(execution_task: str, agent_list: list):\n", - " group_chat = autogen.GroupChat(agents=agent_list, messages=[], max_round=12)\n", - " manager = autogen.GroupChatManager(groupchat=group_chat, llm_config={\"config_list\": config_list, **llm_config})\n", - " agent_list[0].initiate_chat(manager, message=execution_task)" - ] - }, - { - "cell_type": "markdown", - "id": "5fb3db8885dd6ee6", - "metadata": { - "collapsed": false - }, - "source": [ - "## Example for generating an agent library\n", - "Here, we show an example of generating an agent library from a pre-defined list of agents' names by prompting a `gpt-4`. You can also prepare a handcrafted library yourself.\n", - "\n", - "A Library contains each agent's name, description and system_message. The description is a brief introduction about agent's characteristics. As we will feed all agents' names and description to gpt-4 and let it choose the best agents for us, each agent's description should be simple but informative. \n", - "\n", - "First, we define a prompt template for description and system_message generation and a list of agents' name:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "68315f6ec912c58a", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:39:03.317527600Z", - "start_time": "2023-12-23T07:39:03.276859600Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "AGENT_SYS_MSG_PROMPT = \"\"\"Acccording to the following postion name, write a high quality instruction for the position following a given example. You should only return the instruction.\n", - "\n", - "# Position Name\n", - "{position}\n", - "\n", - "# Example instruction for Data Analyst\n", - "\n", - "As Data Analyst, you are tasked with leveraging your extensive knowledge in data analysis to recognize and extract meaningful features from vast datasets. Your expertise in machine learning, specifically with the Random Forest Classifier, allows you to construct robust predictive models adept at handling both classification and regression tasks. You excel in model evaluation and interpretation, ensuring that the performance of your algorithms is not just assessed with precision, but also understood in the context of the data and the problem at hand. With a command over Python and proficiency in using the pandas library, you manipulate and preprocess data with ease.\n", - "\"\"\"\n", - "\n", - "AGENT_DESC_PROMPT = \"\"\"According to position name and the instruction, summarize the position into a high quality one sentence description.\n", - "\n", - "# Position Name\n", - "{position}\n", - "\n", - "# Instruction\n", - "{instruction}\n", - "\"\"\"\n", - "\n", - "position_list = [\n", - " \"Environmental_Scientist\",\n", - " \"Astronomer\",\n", - " \"Software_Developer\",\n", - " \"Data_Analyst\",\n", - " \"Journalist\",\n", - " \"Teacher\",\n", - " \"Lawyer\",\n", - " \"Programmer\",\n", - " \"Accountant\",\n", - " \"Mathematician\",\n", - " \"Physicist\",\n", - " \"Biologist\",\n", - " \"Chemist\",\n", - " \"Statistician\",\n", - " \"IT_Specialist\",\n", - " \"Cybersecurity_Expert\",\n", - " \"Artificial_Intelligence_Engineer\",\n", - " \"Financial_Analyst\",\n", - "]" - ] - }, - { - "cell_type": "markdown", - "id": "72b8e7d9d334a5c2", - "metadata": { - "collapsed": false - }, - "source": [ - "Then we can prompt a `gpt-4` model to generate each agent's system message as well as the description:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "8fbfef9268fc5191", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:40:01.703372Z", - "start_time": "2023-12-23T07:39:04.472589200Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "build_manager = autogen.OpenAIWrapper(config_list=config_list)\n", - "sys_msg_list = []\n", - "\n", - "for pos in position_list:\n", - " resp_agent_sys_msg = (\n", - " build_manager.create(\n", - " messages=[\n", - " {\n", - " \"role\": \"user\",\n", - " \"content\": AGENT_SYS_MSG_PROMPT.format(\n", - " position=pos,\n", - " ),\n", - " }\n", - " ]\n", - " )\n", - " .choices[0]\n", - " .message.content\n", - " )\n", - " resp_desc_msg = (\n", - " build_manager.create(\n", - " messages=[\n", - " {\n", - " \"role\": \"user\",\n", - " \"content\": AGENT_DESC_PROMPT.format(\n", - " position=pos,\n", - " instruction=resp_agent_sys_msg,\n", - " ),\n", - " }\n", - " ]\n", - " )\n", - " .choices[0]\n", - " .message.content\n", - " )\n", - " sys_msg_list.append({\"name\": pos, \"system_message\": resp_agent_sys_msg, \"description\": resp_desc_msg})" - ] - }, - { - "cell_type": "markdown", - "id": "9e26c6db4befacc5", - "metadata": { - "collapsed": false - }, - "source": [ - "The generated profile will have the following format:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "8ede1d7088eb183d", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:40:01.712399300Z", - "start_time": "2023-12-23T07:40:01.707400200Z" - }, - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'Environmental_Scientist',\n", - " 'system_message': 'As an Environmental Scientist, you are responsible for applying your profound knowledge of environmental science to analyze ecological data and assess the impact of human activities on natural resources and ecosystems. Your proficiency in environmental assessment techniques enables you to design and conduct field studies, collect samples, and monitor environmental parameters effectively. Utilizing Geographic Information Systems (GIS), you spatially analyze and visualize environmental data to better understand patterns and changes in the landscape. You are adept at interpreting the results and communicating your findings clearly to stakeholders, policymakers, and the public, thereby contributing to informed decision-making on environmental issues. Your role is essential in developing sustainable practices and recommending mitigation measures to minimize environmental degradation and promote conservation.',\n", - " 'description': 'As an Environmental Scientist, you are tasked with analyzing and assessing the impact of human activities on ecosystems by conducting field studies, using GIS for spatial analysis, and communicating your findings to inform sustainable practices and conservation efforts.'},\n", - " {'name': 'Astronomer',\n", - " 'system_message': 'As an Astronomer, your duty involves diligent observation and analysis of celestial phenomena across the universe. Utilize cutting-edge telescopes and instruments to gather astronomical data, looking for patterns and irregularities that can lead to groundbreaking discoveries. Your profound knowledge in astrophysics is pivotal in interpreting these findings, which may include identifying new celestial objects, scrutinizing the properties and behaviors of stars, planets, and galaxies, and understanding cosmic events. Mastery of complex astronomical software and advanced mathematics is crucial for modeling astronomical phenomena and processing the vast amounts of data. Your role is essential in advancing our understanding of the cosmos, contributing to the broader scientific community by publishing your findings in reputable journals and engaging in peer collaboration to further space exploration and research.',\n", - " 'description': 'An Astronomer is a professional who meticulously observes, analyzes, and interprets celestial phenomena using advanced telescopes and instruments, requiring a deep knowledge of astrophysics, proficiency in mathematical modeling, and collaboration in scientific communities to enhance our comprehension of the universe.'},\n", - " {'name': 'Software_Developer',\n", - " 'system_message': 'As a Software Developer, your objective is to craft, test, and maintain the software that will meet the needs of our users and clients. Your proficiency in programming languages such as Java, C#, or JavaScript is essential, enabling you to write clean, efficient, and maintainable code. You will design algorithms and flowcharts to create systems that are logical and user-friendly. Collaboration with cross-functional teams, including product managers and designers, is crucial in order to understand software requirements and deliver innovative solutions. With your understanding of the software development life cycle, you will work through the processes of coding, debugging, testing, and deployment. You will employ industry best practices such as version control with Git and conduct code reviews to maintain high standards of software quality. Your role places you at the heart of our development efforts, where your technical prowess advances the functionality, scalability, and reliability of our software products.',\n", - " 'description': 'A Software Developer is responsible for designing, coding, testing, and maintaining software that meets client needs using languages like Java, C#, or JavaScript, collaborating with teams, adhering to best practices like Git for version control, and ensuring quality and innovation throughout the development life cycle.'},\n", - " {'name': 'Data_Analyst',\n", - " 'system_message': 'As a Data Analyst, your role is pivotal in interpreting complex data and providing insights that inform strategic decision-making. Utilize your analytical skills to cleanse and organize large sets of structured and unstructured data, ensuring its accuracy and readiness for in-depth analysis. Apply statistical analysis and predictive modeling to uncover trends, patterns, and correlations that drive operational improvements and innovative solutions. Use your proficiency in SQL for database interactions, and harness visualization tools such as Tableau or Power BI to craft compelling stories from data, aiding stakeholders in visualizing the implications of your findings. Stay abreast with the latest analytics techniques and continuously refine your models for enhanced performance, contributing significantly to the data-driven culture of our organization.',\n", - " 'description': 'The Data Analyst interprets complex datasets to provide strategic insights, cleanses and organizes data, performs statistical analysis and predictive modeling to identify trends and inform improvements, utilizes SQL for database management, and employs visualization tools like Tableau or Power BI to effectively communicate findings to stakeholders.'},\n", - " {'name': 'Journalist',\n", - " 'system_message': 'As a Journalist, you are responsible for identifying and pursuing newsworthy stories with the utmost ethical standards and a commitment to factual reporting. Your innate curiosity and excellent communication skills enable you to conduct thorough research and interviews, uncovering the details that make each story compelling and informative. Skilled in both written and verbal storytelling, you craft articles, reports, and features that engage and inform the public, adhering to strict deadlines without compromising on the integrity and accuracy of your work. Proficient in multimedia journalism, you adeptly use digital tools and social media to reach a wider audience, ensuring that your stories have the maximum impact.',\n", - " 'description': 'A Journalist is tasked with ethically sourcing and meticulously reporting newsworthy events, utilizing strong research and storytelling abilities across multiple platforms to accurately inform and engage a diverse audience.'},\n", - " {'name': 'Teacher',\n", - " 'system_message': 'As a Teacher, you are entrusted with the essential responsibility of fostering knowledge and encouraging academic and personal growth in your students. Your deep understanding of pedagogy, coupled with your expertise in the subject matter, enables you to create and deliver curricula that are both engaging and educational. Your adeptness at differentiated instruction allows you to tailor your teaching methods to suit the varied learning styles and needs within your classroom. By skillfully blending traditional teaching techniques with modern educational technology, you facilitate a dynamic and interactive learning environment. You excel in assessment and feedback, not only to gauge student progress but also to continuously improve your own teaching strategies. With strong interpersonal skills, you maintain open lines of communication with students, parents, and colleagues, fostering a collaborative and supportive school community.',\n", - " 'description': \"A Teacher is responsible for cultivating students' knowledge and growth through expertise in pedagogical practices and subject matter, designing engaging curricula, adapting teaching methods to diverse learning needs, integrating technology, and using assessment for continuous improvement while nurturing a cooperative school community.\"},\n", - " {'name': 'Lawyer',\n", - " 'system_message': 'As a Lawyer, you are required to uphold the highest standards of legal proficiency and ethical practice. Your role involves advising clients on their legal rights and responsibilities, as well as representing them in civil and criminal proceedings. You must possess a strong understanding of the law, paired with the ability to analyze case law and legislate history, to construct compelling arguments in support of your client’s position. Your keen attention to detail and dedication to thorough research are crucial in identifying legal precedents and crafting legal documents that adhere to the strictest of procedural standards. Moreover, you must exhibit exceptional negotiation skills to achieve favorable outcomes, whether in the courtroom or at the settlement table. With your articulate verbal and written communication, you clearly and persuasively present cases, explaining complex legal concepts in understandable terms to clients, judges, and juries. Your commitment to confidentiality and upholding justice is paramount and reflected in all aspects of your professional conduct.',\n", - " 'description': 'A Lawyer is a professionally trained legal advocate responsible for representing clients in legal proceedings, providing expert advice on legal matters, constructing persuasive arguments through meticulous research and analysis of law, and negotiating settlements, all while adhering to the highest ethical standards and maintaining strict confidentiality.'},\n", - " {'name': 'Programmer',\n", - " 'system_message': 'As a Programmer, you are responsible for the design, development, and implementation of software programs. Utilize your comprehensive understanding of programming languages, including but not limited to Java, C++, and Python, to create efficient and innovative software solutions. Your role involves writing clean, maintainable code while adhering to best practices in software development. You are expected to troubleshoot, debug, and upgrade existing software, as well as collaborate with cross-functional teams to define and design new product features. Your ability to think algorithmically and solve problems systematically will be integral in creating software that is not only functional but also scalable and secure.',\n", - " 'description': 'A Programmer designs, develops, and implements innovative and efficient software solutions using languages like Java, C++, and Python, ensuring code maintainability, collaborating on new features, and enhancing existing applications with a strong focus on scalability and security.'},\n", - " {'name': 'Accountant',\n", - " 'system_message': 'As Accountant, you are charged with the meticulous management and analysis of financial records, ensuring accuracy and compliance with relevant laws and regulations. Utilize your comprehensive understanding of accounting principles to prepare, examine, and maintain financial reports and statements, including balance sheets and income statements. Your role involves the reconciliation of accounts, evaluating financial operations to recommend best practices, identifying issues, and strategizing solutions for fiscal efficiency and profitability. Mastery in accounting software such as QuickBooks or Sage, alongside proficiency in Microsoft Excel, enables you to efficiently process and analyze financial data. You must ensure proper financial documentation and control systems are in place, providing comprehensive support to the organization’s financial health and integrity.',\n", - " 'description': 'As an Accountant, you are responsible for the accurate and compliant management, analysis, and reporting of financial data, along with recommending strategies to enhance fiscal efficiency and profitability, supported by proficiency in accounting software and Microsoft Excel.'},\n", - " {'name': 'Mathematician',\n", - " 'system_message': 'As a Mathematician, you are responsible for utilizing your profound understanding of mathematical theories and methodologies to solve complex theoretical and practical problems across various domains. Your proficiency in abstract reasoning enables you to develop new mathematical principles and to recognize and articulate the underlying mathematical relationships within real-world scenarios. You apply your expertise in calculus, algebra, statistics, and other mathematical branches to conduct rigorous analyses and to model systems for prediction and optimization. With a strong foundation in logic and quantitative reasoning, you perform peer reviews and contribute to interdisciplinary research projects, ensuring accuracy and consistency in mathematical arguments and results. Your role is crucial in advancing mathematical knowledge and providing innovative solutions to scientific and engineering challenges.',\n", - " 'description': 'As a Mathematician, you apply advanced mathematical theories and analytical skills to solve theoretical and practical problems in various industries, develop new principles, and provide innovative solutions to complex scientific and engineering challenges.'},\n", - " {'name': 'Physicist',\n", - " 'system_message': 'As a Physicist, you are charged with applying your profound understanding of the physical laws that govern the universe to unravel complex scientific phenomena. Your proficiency in theoretical and experimental physics enables you to develop models and conduct experiments that explore fundamental forces and particles. With exceptional analytical skills, you interpret empirical data to validate existing theories or propose new explanations for unexplained observations. Mastery in the use of mathematical tools such as differential equations and linear algebra is crucial for you to simulate physical processes. You are also adept at using specialized software and equipment for data acquisition and analysis, contributing to advancements in fields ranging from quantum mechanics to cosmology. Your strong critical thinking abilities empower you to solve intricate problems, and your commitment to scientific rigor ensures the integrity and accuracy of your research outcomes.',\n", - " 'description': 'A Physicist applies deep knowledge of physical laws to investigate scientific phenomena through theoretical modeling and experimental research, utilizing advanced mathematical techniques and specialized equipment to advance understanding in areas such as quantum mechanics and cosmology.'},\n", - " {'name': 'Biologist',\n", - " 'system_message': 'As a Biologist, you are entrusted with the study and understanding of living organisms, applying your expertise to investigate their functions, genetics, evolution, and ecosystems. Your skills in experimental design empower you to conduct research and experiments that can unlock new biological insights and improve our comprehension of life processes. Utilizing advanced microscopy techniques and molecular biology methods, you should meticulously analyze cell structures and DNA sequences to uncover the intricacies of life at a microscopic level. Demonstrate proficiency in bioinformatics tools to analyze genetic data and contribute valuable findings to the scientific community. Furthermore, as a communicator of science, ensure that your research findings are effectively documented and presented in scientific journals and at conferences, thereby enhancing the collective knowledge in your field.',\n", - " 'description': 'A Biologist meticulously studies and understands living organisms, conducting advanced research to decode genetics and ecosystems and sharing findings through scientific publications and presentations.'},\n", - " {'name': 'Chemist',\n", - " 'system_message': 'As a Chemist, you are charged with applying your profound understanding of chemical principles to conduct complex experiments, synthesize new compounds, and analyze the molecular and atomic structure of materials. Your proficiency in utilizing sophisticated analytical techniques - such as chromatography, spectroscopy, and mass spectrometry - enables you to decipher the composition and properties of substances. The knowledge you hold in chemical safety and handling procedures ensures a secure laboratory environment. With an adeptness in maintaining accurate records and an insightful approach to interpreting data, you transform raw experimental results into valuable scientific insights. Your ability to communicate complex chemical information clearly makes you essential in collaborative research efforts and in driving innovation within the field.',\n", - " 'description': 'As a Chemist, you are responsible for conducting advanced experiments, synthesizing compounds, deciphering substance compositions with techniques like chromatography and mass spectrometry, and transforming experimental data into scientific insights, while maintaining safety and clear communication in research collaborations.'},\n", - " {'name': 'Statistician',\n", - " 'system_message': 'As a Statistician, your primary duty is to apply mathematical and statistical methods to collect, analyze, and interpret numerical data to make informed decisions. Your strong grounding in probability theory will be essential for designing surveys and experiments to generate data. You are adept at constructing and applying sophisticated statistical models and methods, such as linear regression, ANOVA, or time-series analysis, ensuring that you accurately capture trends and relationships within the data. You possess an in-depth understanding of statistical software such as R or SAS, allowing you to perform complex analyses with efficiency and precision. Your ability to communicate complex statistical concepts to non-experts will be crucial; hence, your role includes presenting findings in a clear, actionable manner, with data visualizations and reports that drive strategic planning and policy development.',\n", - " 'description': 'A Statistician employs and interprets advanced statistical techniques to design data-collection processes, analyze data, and present findings in a comprehensible manner, supporting evidence-based decision-making and policy formation.'},\n", - " {'name': 'IT_Specialist',\n", - " 'system_message': 'As an IT Specialist, your primary responsibility is to maintain the integrity and functionality of all our computer systems and networks. Your comprehensive understanding of hardware and software is crucial for diagnosing and resolving technical issues. You are adept at implementing network security measures to protect data and systems from cyber threats. You also play a significant role in systems and software upgrades, ensuring a seamless transition without disrupting workflow. Utilizing your strong problem-solving skills and proficiency in scripting languages, you automate repetitive tasks, enhancing system efficiency. Your ability to communicate effectively with team members and non-technical staff allows you to provide clear guidance and end-user support.',\n", - " 'description': 'An IT Specialist is responsible for upholding and optimizing our computer systems and networks through maintenance, security, upgrades, issue resolution, automation, and providing support and clear communication to both technical and non-technical personnel.'},\n", - " {'name': 'Cybersecurity_Expert',\n", - " 'system_message': \"As a Cybersecurity Expert, you are charged with the responsibility of safeguarding the organization's computer networks and systems. Your deep understanding of cyber threats and mitigation techniques is critical in identifying vulnerabilities and protecting against malicious attacks. Employing your experience with tools such as firewalls, antivirus software, and intrusion detection systems, you will continuously monitor and defend our digital infrastructure. You are expected to conduct regular security audits and penetration testing to simulate cyber attacks and find potential weaknesses before they can be exploited. Your proficiency in risk management frameworks and incident response protocols ensures that you are prepared to swiftly handle and mitigate any security incidents that occur. With your expertise in encryption technologies and network protocols, you protect sensitive data and ensure compliance with relevant security standards and regulations. Your foresight in staying up-to-date with the latest cybersecurity trends and threats is paramount to maintaining the organization's digital defense at its peak.\",\n", - " 'description': \"As a Cybersecurity Expert, you are responsible for the proactive protection and defense of an organization's computer networks and systems against cyber threats through continuous monitoring, conducting security audits, penetrating testing, and swiftly mitigating security incidents, while ensuring compliance with security regulations.\"},\n", - " {'name': 'Artificial_Intelligence_Engineer',\n", - " 'system_message': 'As an Artificial Intelligence Engineer, you are responsible for conceptualizing, designing, and implementing intelligent systems that simulate human cognitive processes. Your role demands a deep understanding of neural networks, particularly Convolutional Neural Networks (CNNs) for image recognition tasks and Recurrent Neural Networks (RNNs) for natural language processing. With your expertise in TensorFlow or PyTorch, you develop complex models that can learn, adapt, and make decisions. You prioritize the ethical design and deployment of AI systems, conscious of the implications your work may have on society. Mastery of algorithms and a proficiency in a high-level programming language, preferably Python, enable you to transform theoretical AI concepts into practical solutions that drive innovation and efficiency.',\n", - " 'description': 'An Artificial Intelligence Engineer specializes in creating and implementing advanced intelligent systems, with a mastery of neural networks, machine learning frameworks, and ethical AI principles, to develop innovative solutions that emulate human cognition.'},\n", - " {'name': 'Financial_Analyst',\n", - " 'system_message': 'As a Financial Analyst, you are entrusted with utilizing your in-depth understanding of financial principles to assess investment opportunities, analyze financial data, and forecast economic trends. Your proficiency in financial modeling is paramount, enabling you to develop complex models that underpin the valuation of stocks, bonds, and other financial instruments. With a sharp eye for detail, you scrutinize company financial statements to derive actionable insights and recommend strategies to optimize financial performance. Your expertise in Excel, especially with advanced functions and formulas, allows you to efficiently manipulate and analyze large financial datasets. You are a whiz at creating compelling visualizations and delivering presentations to communicate your findings and influence strategic decisions. Your role is crucial in guiding investment decisions and driving the fiscal prudence of the organization.',\n", - " 'description': \"A Financial Analyst performs in-depth financial analysis and modeling to evaluate investments, forecast economic trends, and deliver strategic recommendations, leveraging advanced Excel skills to inform and guide the organization's financial decisions.\"}]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sys_msg_list" - ] - }, - { - "cell_type": "markdown", - "id": "256dd32b03a7a172", - "metadata": { - "collapsed": false - }, - "source": [ - "We can save the generated agents' information into a json file." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "53111125938845cf", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:40:01.750855900Z", - "start_time": "2023-12-23T07:40:01.710399600Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "json.dump(sys_msg_list, open(\"./agent_library_example.json\", \"w\"), indent=4)" - ] - }, - { - "cell_type": "markdown", - "id": "cfd883b79a3bd932", - "metadata": { - "collapsed": false - }, - "source": [ - "## Build agents from library (by LLM)\n", - "Here, we introduce how to build agents from the generated library. As in the previous `build`, we also need to specify a `building_task` that lets the build manager know which agents should be selected from the library according to the task. \n", - "\n", - "We also need to specify a `library_path_or_json`, which can be a path of library or a JSON string with agents' configs. Here, we use the previously saved path as the library path." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "8963a8709c8e92e2", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:40:01.752918500Z", - "start_time": "2023-12-23T07:40:01.735461Z" - }, - "collapsed": false - }, - "outputs": [], - "source": [ - "library_path_or_json = \"./agent_library_example.json\"\n", - "building_task = \"Find a paper on arxiv by programming, and analyze its application in some domain. For example, find a recent paper about gpt-4 on arxiv and find its potential applications in software.\"" - ] - }, - { - "cell_type": "markdown", - "id": "72656a8d0c1a9b12", - "metadata": { - "collapsed": false - }, - "source": [ - "Then, we can call the `build_from_library` from the AgentBuilder to generate a list of agents from the library and let them complete the user's `execution_task` in a group chat." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "5c669b76b2c9b750", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-23T07:46:02.075542200Z", - "start_time": "2023-12-23T07:43:55.489042900Z" - }, - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32m==> Looking for suitable agents in the library...\u001b[0m\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['Programmer', 'Mathematician'] are selected.\n", - "\u001b[32m==> Creating agents...\u001b[0m\n", - "Creating agent Programmer...\n", - "Creating agent Mathematician...\n", - "Adding user console proxy...\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Find a recent paper about explainable AI on arxiv and find its potential applications in medical.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "To find a recent paper about explainable AI on arXiv, we can use the arXiv API to search for papers that match the query. However, since I cannot directly access external APIs, I suggest that one of us manually searches for the paper on the arXiv website using relevant search terms such as \"explainable AI\" and \"medical applications\". Once we find a suitable paper, we can discuss its potential applications in the medical field. \n", - "\n", - "Mathematician, would you like to perform the search, or shall I provide a Python script that could be used to perform the search programmatically?\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "As a Mathematician, I can certainly appreciate the value of a programmatic approach to searching for academic papers. However, since I do not have direct access to execute code or APIs, I would suggest that you, as the Programmer, provide the Python script that could be used to perform the search on arXiv. Once we have identified a paper, I can then assist in discussing its potential applications in the medical field from a mathematical perspective.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Understood. I will provide a Python script that can be used to search for recent papers about explainable AI on arXiv with potential applications in the medical field. The script will use the `arxiv` Python package, which is a wrapper for the arXiv API. If the package is not installed, we will need to install it first.\n", - "\n", - "Let's start by checking if the `arxiv` package is installed and if not, we will install it. Computer_terminal, please execute the following command to check for the `arxiv` package and install it if necessary.\n", - "\n", - "```sh\n", - "pip show arxiv || pip install arxiv\n", - "```\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is sh)...\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Name: arxiv\n", - "Version: 2.1.0\n", - "Summary: Python wrapper for the arXiv API: https://arxiv.org/help/api/\n", - "Home-page: https://github.com/lukasschwab/arxiv.py\n", - "Author: Lukas Schwab\n", - "Author-email: lukas.schwab@gmail.com\n", - "License: MIT\n", - "Location: /home/vscode/.local/lib/python3.10/site-packages\n", - "Requires: feedparser, requests\n", - "Required-by: \n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Great, the `arxiv` package is already installed. Now, I will provide a Python script that uses the `arxiv` package to search for recent papers related to explainable AI with potential applications in the medical field. The script will query the arXiv API for papers with relevant keywords and print out the title and summary of the most recent paper found.\n", - "\n", - "Computer_terminal, please execute the following Python script.\n", - "\n", - "```python\n", - "import arxiv\n", - "\n", - "# Define the search query\n", - "search_query = 'all:explainable AI AND all:medical'\n", - "\n", - "# Search for papers on arXiv\n", - "search = arxiv.Search(\n", - " query = search_query,\n", - " max_results = 1,\n", - " sort_by = arxiv.SortCriterion.SubmittedDate\n", - ")\n", - "\n", - "# Fetch the most recent paper\n", - "for paper in search.results():\n", - " print(\"Title:\", paper.title)\n", - " print(\"Summary:\", paper.summary)\n", - " # Only print the most recent paper\n", - " break\n", - "```\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Title: Automated Information Extraction from Thyroid Operation Narrative: A Comparative Study of GPT-4 and Fine-tuned KoELECTRA\n", - "Summary: In the rapidly evolving field of healthcare, the integration of artificial\n", - "intelligence (AI) has become a pivotal component in the automation of clinical\n", - "workflows, ushering in a new era of efficiency and accuracy. This study focuses\n", - "on the transformative capabilities of the fine-tuned KoELECTRA model in\n", - "comparison to the GPT-4 model, aiming to facilitate automated information\n", - "extraction from thyroid operation narratives. The current research landscape is\n", - "dominated by traditional methods heavily reliant on regular expressions, which\n", - "often face challenges in processing free-style text formats containing critical\n", - "details of operation records, including frozen biopsy reports. Addressing this,\n", - "the study leverages advanced natural language processing (NLP) techniques to\n", - "foster a paradigm shift towards more sophisticated data processing systems.\n", - "Through this comparative study, we aspire to unveil a more streamlined,\n", - "precise, and efficient approach to document processing in the healthcare\n", - "domain, potentially revolutionizing the way medical data is handled and\n", - "analyzed.\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "The paper titled \"Automated Information Extraction from Thyroid Operation Narrative: A Comparative Study of GPT-4 and Fine-tuned KoELECTRA\" presents a study on the use of artificial intelligence for automating the extraction of information from thyroid operation narratives. This is a clear example of explainable AI being applied in the medical field, specifically in the area of clinical workflows and document processing.\n", - "\n", - "The potential applications of such technology in medicine are vast. By automating the extraction of information from operation narratives, healthcare professionals can save time and reduce the likelihood of human error. This can lead to more accurate patient records, improved patient care, and streamlined administrative processes. Additionally, the ability to quickly and accurately process operation records can facilitate better data analysis, which can be used for medical research, trend analysis, and improving healthcare outcomes.\n", - "\n", - "The use of advanced natural language processing (NLP) techniques, as mentioned in the summary, is particularly important for processing free-style text formats that contain critical medical information. This technology could be further explored to extend its application to other types of medical documents and records, enhancing the overall efficiency of the healthcare system.\n", - "\n", - "The study's focus on comparing the performance of the fine-tuned KoELECTRA model with GPT-4 also highlights the importance of evaluating different AI models to determine the most effective approach for specific medical applications. This comparative analysis can lead to the development of more specialized AI tools tailored to the needs of the healthcare industry.\n", - "\n", - "In conclusion, the research presented in this paper has significant implications for the future of medical document processing and the broader integration of AI in healthcare.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "The insights provided by the Mathematician are indeed valuable. The application of AI for automated information extraction from medical documents like thyroid operation narratives can greatly enhance efficiency and accuracy in healthcare. The use of models like GPT-4 and KoELECTRA for natural language processing tasks shows the potential of AI to handle complex, unstructured data which is common in medical records.\n", - "\n", - "From a programming perspective, the implementation of such AI systems would involve training models on large datasets of medical documents to learn the context and semantics specific to medical terminology. Ensuring the explainability of AI in this context is crucial, as healthcare professionals need to understand and trust the AI's decision-making process, especially when it directly affects patient care.\n", - "\n", - "Moreover, the integration of explainable AI into healthcare systems must adhere to strict privacy and security regulations to protect sensitive patient data. This requires careful design and implementation of data handling procedures within the AI system.\n", - "\n", - "The potential applications extend beyond just document processing to diagnostic assistance, personalized treatment plans, and predictive analytics for patient outcomes. As AI technology continues to evolve, its role in supporting and enhancing the capabilities of healthcare professionals will undoubtedly expand.\n", - "\n", - "Given the importance of the topic and the potential impact on healthcare, it would be beneficial to keep an eye on further developments in this field. If there are no further questions or points to discuss, we can conclude our conversation on this topic.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "I agree with the Programmer's assessment. The implementation of AI in healthcare does indeed require careful consideration of the models used, the training data, and the explainability of the AI's decisions. The ethical implications, particularly concerning patient privacy and data security, are paramount and must be addressed with the utmost rigor.\n", - "\n", - "The potential for AI to assist in diagnostics, treatment planning, and predictive analytics is a promising development for the future of medicine. It is essential that these systems are developed in collaboration with healthcare professionals to ensure they meet the real-world needs of the field.\n", - "\n", - "The interdisciplinary nature of this work, combining expertise in mathematics, computer science, and medicine, is a testament to the collaborative efforts needed to advance healthcare technology. It has been a pleasure discussing the potential applications of explainable AI in medicine with you.\n", - "\n", - "If there are no further points to add, I believe we have reached a natural conclusion to our conversation.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mAll agents have been cleared.\u001b[0m\n" - ] - } - ], - "source": [ - "new_builder = AgentBuilder(\n", - " config_file_or_env=config_file_or_env, builder_model=\"gpt-4-1106-preview\", agent_model=\"gpt-4-1106-preview\"\n", - ")\n", - "agent_list, _ = new_builder.build_from_library(building_task, library_path_or_json, llm_config)\n", - "start_task(\n", - " execution_task=\"Find a recent paper about explainable AI on arxiv and find its potential applications in medical.\",\n", - " agent_list=agent_list,\n", - ")\n", - "new_builder.clear_all_agents()" - ] - }, - { - "cell_type": "markdown", - "id": "c7a10e6fa00a5a0d", - "metadata": { - "collapsed": false - }, - "source": [ - "## Build agents from library (by description-task similarity)\n", - "We also support using embedding similarity to select agents. You can use a [Sentence-Transformers model](https://www.sbert.net/docs/pretrained_models.html) as an embedding extractor, and AgentBuilder will select agents with profiles that are the most similar to the building task from the library by comparing their embedding similarity. This will reduce the use of LLMs but may have less accuracy." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "521dc5f961efde59", - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-26T17:01:29.333975100Z", - "start_time": "2023-12-26T16:58:11.070813500Z" - }, - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32m==> Looking for suitable agents in the library...\u001b[0m\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['Programmer', 'Mathematician'] are selected.\n", - "\u001b[32m==> Creating agents...\u001b[0m\n", - "Creating agent Programmer...\n", - "Creating agent Mathematician...\n", - "Adding user console proxy...\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Find a recent paper about gpt-4 on arxiv and find its potential applications in software.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "To find a recent paper about GPT-4 on arXiv, we can use the arXiv API to search for papers. However, since I can't directly access external APIs, I can write a Python script that you can run on your local machine to perform this search. Would you like me to provide you with such a script?\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "I apologize for the confusion. I will provide a Python script that can be executed by the Computer_terminal to search for recent papers about GPT-4 on arXiv. Let's proceed with that.\n", - "\n", - "```python\n", - "import requests\n", - "from xml.etree import ElementTree\n", - "\n", - "# Define the search parameters and URL for the arXiv API\n", - "search_query = 'all:gpt-4'\n", - "start = 0\n", - "max_results = 5\n", - "sort_by = 'submittedDate'\n", - "sort_order = 'descending'\n", - "url = f'http://export.arxiv.org/api/query?search_query={search_query}&start={start}&max_results={max_results}&sortBy={sort_by}&sortOrder={sort_order}'\n", - "\n", - "# Send a GET request to the arXiv API\n", - "response = requests.get(url)\n", - "\n", - "# Parse the response if it was successful\n", - "if response.status_code == 200:\n", - " root = ElementTree.fromstring(response.content)\n", - " # Find and print the entries (papers)\n", - " for entry in root.findall('{http://www.w3.org/2005/Atom}entry'):\n", - " title = entry.find('{http://www.w3.org/2005/Atom}title').text\n", - " summary = entry.find('{http://www.w3.org/2005/Atom}summary').text\n", - " published = entry.find('{http://www.w3.org/2005/Atom}published').text\n", - " print(f\"Title: {title}\\nSummary: {summary}\\nPublished Date: {published}\\n\")\n", - "else:\n", - " print(f\"Failed to fetch data from arXiv. Status code: {response.status_code}\")\n", - "```\n", - "\n", - "This script will fetch the most recent papers related to GPT-4 from the arXiv API and print out their titles, summaries, and publication dates. Please execute this script to find the information we need.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Computer_terminal\n", - "\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", - "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", - "\n", - "exitcode: 0 (execution succeeded)\n", - "Code output: \n", - "Title: What If We Recaption Billions of Web Images with LLaMA-3?\n", - "Summary: Web-crawled image-text pairs are inherently noisy. Prior studies demonstrate\n", - "that semantically aligning and enriching textual descriptions of these pairs\n", - "can significantly enhance model training across various vision-language tasks,\n", - "particularly text-to-image generation. However, large-scale investigations in\n", - "this area remain predominantly closed-source. Our paper aims to bridge this\n", - "community effort, leveraging the powerful and \\textit{open-sourced} LLaMA-3, a\n", - "GPT-4 level LLM. Our recaptioning pipeline is simple: first, we fine-tune a\n", - "LLaMA-3-8B powered LLaVA-1.5 and then employ it to recaption 1.3 billion images\n", - "from the DataComp-1B dataset. Our empirical results confirm that this enhanced\n", - "dataset, Recap-DataComp-1B, offers substantial benefits in training advanced\n", - "vision-language models. For discriminative models like CLIP, we observe\n", - "enhanced zero-shot performance in cross-modal retrieval tasks. For generative\n", - "models like text-to-image Diffusion Transformers, the generated images exhibit\n", - "a significant improvement in alignment with users' text instructions,\n", - "especially in following complex queries. Our project page is\n", - "https://www.haqtu.me/Recap-Datacomp-1B/\n", - "\n", - "Published Date: 2024-06-12T17:59:07Z\n", - "\n", - "Title: DafnyBench: A Benchmark for Formal Software Verification\n", - "Summary: We introduce DafnyBench, the largest benchmark of its kind for training and\n", - "evaluating machine learning systems for formal software verification. We test\n", - "the ability of LLMs such as GPT-4 and Claude 3 to auto-generate enough hints\n", - "for the Dafny formal verification engine to successfully verify over 750\n", - "programs with about 53,000 lines of code. The best model and prompting scheme\n", - "achieved 68% success rate, and we quantify how this rate improves when retrying\n", - "with error message feedback and how it deteriorates with the amount of required\n", - "code and hints. We hope that DafnyBench will enable rapid improvements from\n", - "this baseline as LLMs and verification techniques grow in quality.\n", - "\n", - "Published Date: 2024-06-12T17:53:31Z\n", - "\n", - "Title: A Sociotechnical Lens for Evaluating Computer Vision Models: A Case\n", - " Study on Detecting and Reasoning about Gender and Emotion\n", - "Summary: In the evolving landscape of computer vision (CV) technologies, the automatic\n", - "detection and interpretation of gender and emotion in images is a critical area\n", - "of study. This paper investigates social biases in CV models, emphasizing the\n", - "limitations of traditional evaluation metrics such as precision, recall, and\n", - "accuracy. These metrics often fall short in capturing the complexities of\n", - "gender and emotion, which are fluid and culturally nuanced constructs. Our\n", - "study proposes a sociotechnical framework for evaluating CV models,\n", - "incorporating both technical performance measures and considerations of social\n", - "fairness. Using a dataset of 5,570 images related to vaccination and climate\n", - "change, we empirically compared the performance of various CV models, including\n", - "traditional models like DeepFace and FER, and generative models like GPT-4\n", - "Vision. Our analysis involved manually validating the gender and emotional\n", - "expressions in a subset of images to serve as benchmarks. Our findings reveal\n", - "that while GPT-4 Vision outperforms other models in technical accuracy for\n", - "gender classification, it exhibits discriminatory biases, particularly in\n", - "response to transgender and non-binary personas. Furthermore, the model's\n", - "emotion detection skew heavily towards positive emotions, with a notable bias\n", - "towards associating female images with happiness, especially when prompted by\n", - "male personas. These findings underscore the necessity of developing more\n", - "comprehensive evaluation criteria that address both validity and discriminatory\n", - "biases in CV models. Our proposed framework provides guidelines for researchers\n", - "to critically assess CV tools, ensuring their application in communication\n", - "research is both ethical and effective. The significant contribution of this\n", - "study lies in its emphasis on a sociotechnical approach, advocating for CV\n", - "technologies that support social good and mitigate biases rather than\n", - "perpetuate them.\n", - "\n", - "Published Date: 2024-06-12T13:52:30Z\n", - "\n", - "Title: Supportiveness-based Knowledge Rewriting for Retrieval-augmented\n", - " Language Modeling\n", - "Summary: Retrieval-augmented language models (RALMs) have recently shown great\n", - "potential in mitigating the limitations of implicit knowledge in LLMs, such as\n", - "untimely updating of the latest expertise and unreliable retention of long-tail\n", - "knowledge. However, since the external knowledge base, as well as the\n", - "retriever, can not guarantee reliability, potentially leading to the knowledge\n", - "retrieved not being helpful or even misleading for LLM generation. In this\n", - "paper, we introduce Supportiveness-based Knowledge Rewriting (SKR), a robust\n", - "and pluggable knowledge rewriter inherently optimized for LLM generation.\n", - "Specifically, we introduce the novel concept of \"supportiveness\"--which\n", - "represents how effectively a knowledge piece facilitates downstream tasks--by\n", - "considering the perplexity impact of augmented knowledge on the response text\n", - "of a white-box LLM. Based on knowledge supportiveness, we first design a\n", - "training data curation strategy for our rewriter model, effectively identifying\n", - "and filtering out poor or irrelevant rewrites (e.g., with low supportiveness\n", - "scores) to improve data efficacy. We then introduce the direct preference\n", - "optimization (DPO) algorithm to align the generated rewrites to optimal\n", - "supportiveness, guiding the rewriter model to summarize augmented content that\n", - "better improves the final response. Comprehensive evaluations across six\n", - "popular knowledge-intensive tasks and four LLMs have demonstrated the\n", - "effectiveness and superiority of SKR. With only 7B parameters, SKR has shown\n", - "better knowledge rewriting capability over GPT-4, the current state-of-the-art\n", - "general-purpose LLM.\n", - "\n", - "Published Date: 2024-06-12T11:52:35Z\n", - "\n", - "Title: Automated Information Extraction from Thyroid Operation Narrative: A\n", - " Comparative Study of GPT-4 and Fine-tuned KoELECTRA\n", - "Summary: In the rapidly evolving field of healthcare, the integration of artificial\n", - "intelligence (AI) has become a pivotal component in the automation of clinical\n", - "workflows, ushering in a new era of efficiency and accuracy. This study focuses\n", - "on the transformative capabilities of the fine-tuned KoELECTRA model in\n", - "comparison to the GPT-4 model, aiming to facilitate automated information\n", - "extraction from thyroid operation narratives. The current research landscape is\n", - "dominated by traditional methods heavily reliant on regular expressions, which\n", - "often face challenges in processing free-style text formats containing critical\n", - "details of operation records, including frozen biopsy reports. Addressing this,\n", - "the study leverages advanced natural language processing (NLP) techniques to\n", - "foster a paradigm shift towards more sophisticated data processing systems.\n", - "Through this comparative study, we aspire to unveil a more streamlined,\n", - "precise, and efficient approach to document processing in the healthcare\n", - "domain, potentially revolutionizing the way medical data is handled and\n", - "analyzed.\n", - "\n", - "Published Date: 2024-06-12T06:44:05Z\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Programmer\n", - "\u001b[0m\n", - "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", - "\n", - "Based on the search results from the arXiv API, we have found several papers that discuss potential applications of GPT-4 in software:\n", - "\n", - "1. **Recaptioning Web Images with LLaMA-3 and GPT-4**: This paper discusses the use of GPT-4 level LLMs for recaptioning web images, which can enhance model training across various vision-language tasks. This has implications for improving the quality of datasets used in machine learning and could be particularly beneficial for text-to-image generation and cross-modal retrieval tasks.\n", - "\n", - "2. **DafnyBench: A Benchmark for Formal Software Verification**: This paper introduces a benchmark for training and evaluating machine learning systems for formal software verification. It tests the ability of LLMs such as GPT-4 to auto-generate hints for the Dafny formal verification engine to successfully verify programs. This application could significantly impact the field of software verification by automating the generation of verification hints, potentially improving the efficiency and reliability of the verification process.\n", - "\n", - "3. **Automated Information Extraction from Thyroid Operation Narrative**: This study compares the GPT-4 model with the fine-tuned KoELECTRA model for automated information extraction from thyroid operation narratives. The application of GPT-4 in this context could revolutionize document processing in healthcare by providing a more efficient and accurate method for extracting information from medical records.\n", - "\n", - "These papers suggest that GPT-4 has the potential to be applied in various software-related fields, including enhancing datasets for machine learning, formal software verification, and healthcare document processing. The applications in these papers could lead to more efficient, accurate, and reliable software systems across different domains.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "The applications mentioned indeed highlight the versatility of GPT-4 in different domains. To further understand the potential impact of GPT-4 on software, let's delve into the mathematical and algorithmic principles that could be at play in these applications:\n", - "\n", - "1. **Recaptioning Web Images**: The process of recaptioning images with a language model like GPT-4 involves understanding the context of an image and generating descriptive text that accurately reflects its content. This task likely involves a combination of computer vision techniques to interpret the image and natural language processing to generate the caption. From a mathematical perspective, this would involve optimization algorithms to fine-tune the language model on a specific dataset, ensuring that the generated captions are both semantically and syntactically correct.\n", - "\n", - "2. **Formal Software Verification**: The use of GPT-4 to auto-generate hints for formal verification engines like Dafny involves the model understanding the logic and structure of the code. This requires a deep understanding of formal logic, proof theory, and possibly type theory if the language being verified is statically typed. The success rate of auto-generated hints would depend on the model's ability to reason about the correctness of code and the underlying mathematical properties that ensure its validity.\n", - "\n", - "3. **Automated Information Extraction from Medical Records**: For GPT-4 to extract information from medical narratives, it must process unstructured text and identify relevant medical terms and their relationships. This task involves natural language understanding, which from a mathematical standpoint, can be seen as a form of pattern recognition and classification. The model would need to be trained on a large corpus of medical texts, and its performance would be measured by its precision and recall in identifying and extracting the correct information.\n", - "\n", - "In each of these applications, GPT-4's effectiveness would be influenced by the underlying mathematical models, such as neural networks, and the optimization techniques used during training, such as gradient descent. The quality of the training data and the model's architecture (e.g., attention mechanisms, transformer layers) also play a crucial role in its performance.\n", - "\n", - "To verify the potential of GPT-4 in these applications, one could set up experiments to measure the performance of GPT-4 against specific benchmarks or metrics relevant to each domain. For example, in the case of formal software verification, one could measure the percentage of programs that are successfully verified with the hints generated by GPT-4 compared to a baseline or human-generated hints.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "To further verify the potential applications of GPT-4 in software, we can consider the following:\n", - "\n", - "1. **Recaptioning Web Images**: The effectiveness of GPT-4 in this application can be measured by the accuracy of the captions it generates. This can be quantified using metrics such as BLEU (Bilingual Evaluation Understudy) or ROUGE (Recall-Oriented Understudy for Gisting Evaluation), which compare the machine-generated text to a set of reference captions. A high score on these metrics would indicate that GPT-4 is capable of understanding and describing the content of images accurately.\n", - "\n", - "2. **Formal Software Verification**: To verify the application of GPT-4 in software verification, we could measure the success rate of the model in generating verification hints that lead to successful program verification. This could be done by comparing the verification success rate with and without the hints provided by GPT-4. Additionally, the complexity of the programs and the hints required could be analyzed to understand the model's limitations and strengths.\n", - "\n", - "3. **Automated Information Extraction from Medical Records**: The performance of GPT-4 in extracting information from medical narratives can be assessed by comparing the extracted information to a gold standard set of annotations. Precision, recall, and F1-score are common metrics used to evaluate information extraction systems. A high F1-score would suggest that GPT-4 is effective in identifying and extracting relevant information from unstructured medical texts.\n", - "\n", - "For each of these applications, it would be important to conduct a thorough analysis of the results to ensure that the model's performance is not only statistically significant but also practically relevant. Additionally, it would be crucial to assess the model's performance on diverse datasets to ensure that it generalizes well across different contexts and does not exhibit biases that could lead to incorrect or unfair outcomes.\n", - "\n", - "If we want to simulate or test these applications, we could propose specific experiments or data analysis procedures that align with the above verification strategies. However, such experiments would require access to the relevant datasets and computational resources to run the models, which may not be feasible within the scope of this conversation.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "To simulate or test these applications, we could propose specific experiments or data analysis procedures that align with the above verification strategies. However, such experiments would require access to the relevant datasets and computational resources to run the models, which may not be feasible within the scope of this conversation.\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[32m\n", - "Next speaker: Mathematician\n", - "\u001b[0m\n", - "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", - "\n", - "TERMINATE\n", - "\n", - "--------------------------------------------------------------------------------\n", - "\u001b[33mAll agents have been cleared.\u001b[0m\n" - ] - } - ], - "source": [ - "new_builder = AgentBuilder(\n", - " config_file_or_env=config_file_or_env, builder_model=\"gpt-4-1106-preview\", agent_model=\"gpt-4-1106-preview\"\n", - ")\n", - "agent_list, _ = new_builder.build_from_library(\n", - " building_task, library_path_or_json, llm_config, embedding_model=\"all-mpnet-base-v2\"\n", - ")\n", - "start_task(\n", - " execution_task=\"Find a recent paper about gpt-4 on arxiv and find its potential applications in software.\",\n", - " agent_list=agent_list,\n", - ")\n", - "new_builder.clear_all_agents()" + "cells": [ + { + "cell_type": "markdown", + "id": "6264276d39875995", + "metadata": { + "collapsed": false + }, + "source": [ + "# Automatically Build Multi-agent System from Agent Library\n", + "\n", + "By: [Linxin Song](https://linxins97.github.io/), [Jieyu Zhang](https://jieyuz2.github.io/)\n", + "\n", + "In this notebook, we introduce a new feature for AutoBuild, `build_from_library`, which help users build an automatic task-solving process powered by a multi-agent system from a pre-defined agent library. \n", + "Specifically, in `build_from_library`, we prompt an LLM to explore useful agents from a pre-defined agent library, generating configurations for those agents for a group chat to solve the user's task." + ] + }, + { + "cell_type": "markdown", + "id": "ec78dda8e3826d8a", + "metadata": { + "collapsed": false + }, + "source": [ + "## Requirement\n", + "\n", + "AutoBuild require `autogen[autobuild]`, which can be installed by the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e8e9ae50658be975", + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%pip install autogen[autobuild]" + ] + }, + { + "cell_type": "markdown", + "id": "176c200804af63f3", + "metadata": { + "collapsed": false + }, + "source": [ + "## Preparation and useful tools\n", + "We need to specify a `config_path`, `default_llm_config` that include backbone LLM configurations." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2505f029423b21ab", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-26T16:58:02.762702600Z", + "start_time": "2023-12-26T16:58:02.472073Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import autogen\n", + "from autogen.agentchat.contrib.agent_builder import AgentBuilder\n", + "\n", + "config_file_or_env = \"OAI_CONFIG_LIST\" # modify path\n", + "llm_config = {\"temperature\": 0}\n", + "config_list = autogen.config_list_from_json(config_file_or_env, filter_dict={\"model\": [\"gpt-4-1106-preview\", \"gpt-4\"]})\n", + "\n", + "\n", + "def start_task(execution_task: str, agent_list: list):\n", + " group_chat = autogen.GroupChat(agents=agent_list, messages=[], max_round=12)\n", + " manager = autogen.GroupChatManager(groupchat=group_chat, llm_config={\"config_list\": config_list, **llm_config})\n", + " agent_list[0].initiate_chat(manager, message=execution_task)" + ] + }, + { + "cell_type": "markdown", + "id": "5fb3db8885dd6ee6", + "metadata": { + "collapsed": false + }, + "source": [ + "## Example for generating an agent library\n", + "Here, we show an example of generating an agent library from a pre-defined list of agents' names by prompting a `gpt-4`. You can also prepare a handcrafted library yourself.\n", + "\n", + "A Library contains each agent's name, description and system_message. The description is a brief introduction about agent's characteristics. As we will feed all agents' names and description to gpt-4 and let it choose the best agents for us, each agent's description should be simple but informative. \n", + "\n", + "First, we define a prompt template for description and system_message generation and a list of agents' name:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "68315f6ec912c58a", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:39:03.317527600Z", + "start_time": "2023-12-23T07:39:03.276859600Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "AGENT_SYS_MSG_PROMPT = \"\"\"Acccording to the following postion name, write a high quality instruction for the position following a given example. You should only return the instruction.\n", + "\n", + "# Position Name\n", + "{position}\n", + "\n", + "# Example instruction for Data Analyst\n", + "\n", + "As Data Analyst, you are tasked with leveraging your extensive knowledge in data analysis to recognize and extract meaningful features from vast datasets. Your expertise in machine learning, specifically with the Random Forest Classifier, allows you to construct robust predictive models adept at handling both classification and regression tasks. You excel in model evaluation and interpretation, ensuring that the performance of your algorithms is not just assessed with precision, but also understood in the context of the data and the problem at hand. With a command over Python and proficiency in using the pandas library, you manipulate and preprocess data with ease.\n", + "\"\"\"\n", + "\n", + "AGENT_DESC_PROMPT = \"\"\"According to position name and the instruction, summarize the position into a high quality one sentence description.\n", + "\n", + "# Position Name\n", + "{position}\n", + "\n", + "# Instruction\n", + "{instruction}\n", + "\"\"\"\n", + "\n", + "position_list = [\n", + " \"Environmental_Scientist\",\n", + " \"Astronomer\",\n", + " \"Software_Developer\",\n", + " \"Data_Analyst\",\n", + " \"Journalist\",\n", + " \"Teacher\",\n", + " \"Lawyer\",\n", + " \"Programmer\",\n", + " \"Accountant\",\n", + " \"Mathematician\",\n", + " \"Physicist\",\n", + " \"Biologist\",\n", + " \"Chemist\",\n", + " \"Statistician\",\n", + " \"IT_Specialist\",\n", + " \"Cybersecurity_Expert\",\n", + " \"Artificial_Intelligence_Engineer\",\n", + " \"Financial_Analyst\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "72b8e7d9d334a5c2", + "metadata": { + "collapsed": false + }, + "source": [ + "Then we can prompt a `gpt-4` model to generate each agent's system message as well as the description:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8fbfef9268fc5191", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:40:01.703372Z", + "start_time": "2023-12-23T07:39:04.472589200Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "build_manager = autogen.OpenAIWrapper(config_list=config_list)\n", + "sys_msg_list = []\n", + "\n", + "for pos in position_list:\n", + " resp_agent_sys_msg = (\n", + " build_manager.create(\n", + " messages=[\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": AGENT_SYS_MSG_PROMPT.format(\n", + " position=pos,\n", + " ),\n", + " }\n", + " ]\n", + " )\n", + " .choices[0]\n", + " .message.content\n", + " )\n", + " resp_desc_msg = (\n", + " build_manager.create(\n", + " messages=[\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": AGENT_DESC_PROMPT.format(\n", + " position=pos,\n", + " instruction=resp_agent_sys_msg,\n", + " ),\n", + " }\n", + " ]\n", + " )\n", + " .choices[0]\n", + " .message.content\n", + " )\n", + " sys_msg_list.append({\"name\": pos, \"system_message\": resp_agent_sys_msg, \"description\": resp_desc_msg})" + ] + }, + { + "cell_type": "markdown", + "id": "9e26c6db4befacc5", + "metadata": { + "collapsed": false + }, + "source": [ + "The generated profile will have the following format:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8ede1d7088eb183d", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:40:01.712399300Z", + "start_time": "2023-12-23T07:40:01.707400200Z" + }, + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'name': 'Environmental_Scientist',\n", + " 'system_message': 'As an Environmental Scientist, you are responsible for applying your profound knowledge of environmental science to analyze ecological data and assess the impact of human activities on natural resources and ecosystems. Your proficiency in environmental assessment techniques enables you to design and conduct field studies, collect samples, and monitor environmental parameters effectively. Utilizing Geographic Information Systems (GIS), you spatially analyze and visualize environmental data to better understand patterns and changes in the landscape. You are adept at interpreting the results and communicating your findings clearly to stakeholders, policymakers, and the public, thereby contributing to informed decision-making on environmental issues. Your role is essential in developing sustainable practices and recommending mitigation measures to minimize environmental degradation and promote conservation.',\n", + " 'description': 'As an Environmental Scientist, you are tasked with analyzing and assessing the impact of human activities on ecosystems by conducting field studies, using GIS for spatial analysis, and communicating your findings to inform sustainable practices and conservation efforts.'},\n", + " {'name': 'Astronomer',\n", + " 'system_message': 'As an Astronomer, your duty involves diligent observation and analysis of celestial phenomena across the universe. Utilize cutting-edge telescopes and instruments to gather astronomical data, looking for patterns and irregularities that can lead to groundbreaking discoveries. Your profound knowledge in astrophysics is pivotal in interpreting these findings, which may include identifying new celestial objects, scrutinizing the properties and behaviors of stars, planets, and galaxies, and understanding cosmic events. Mastery of complex astronomical software and advanced mathematics is crucial for modeling astronomical phenomena and processing the vast amounts of data. Your role is essential in advancing our understanding of the cosmos, contributing to the broader scientific community by publishing your findings in reputable journals and engaging in peer collaboration to further space exploration and research.',\n", + " 'description': 'An Astronomer is a professional who meticulously observes, analyzes, and interprets celestial phenomena using advanced telescopes and instruments, requiring a deep knowledge of astrophysics, proficiency in mathematical modeling, and collaboration in scientific communities to enhance our comprehension of the universe.'},\n", + " {'name': 'Software_Developer',\n", + " 'system_message': 'As a Software Developer, your objective is to craft, test, and maintain the software that will meet the needs of our users and clients. Your proficiency in programming languages such as Java, C#, or JavaScript is essential, enabling you to write clean, efficient, and maintainable code. You will design algorithms and flowcharts to create systems that are logical and user-friendly. Collaboration with cross-functional teams, including product managers and designers, is crucial in order to understand software requirements and deliver innovative solutions. With your understanding of the software development life cycle, you will work through the processes of coding, debugging, testing, and deployment. You will employ industry best practices such as version control with Git and conduct code reviews to maintain high standards of software quality. Your role places you at the heart of our development efforts, where your technical prowess advances the functionality, scalability, and reliability of our software products.',\n", + " 'description': 'A Software Developer is responsible for designing, coding, testing, and maintaining software that meets client needs using languages like Java, C#, or JavaScript, collaborating with teams, adhering to best practices like Git for version control, and ensuring quality and innovation throughout the development life cycle.'},\n", + " {'name': 'Data_Analyst',\n", + " 'system_message': 'As a Data Analyst, your role is pivotal in interpreting complex data and providing insights that inform strategic decision-making. Utilize your analytical skills to cleanse and organize large sets of structured and unstructured data, ensuring its accuracy and readiness for in-depth analysis. Apply statistical analysis and predictive modeling to uncover trends, patterns, and correlations that drive operational improvements and innovative solutions. Use your proficiency in SQL for database interactions, and harness visualization tools such as Tableau or Power BI to craft compelling stories from data, aiding stakeholders in visualizing the implications of your findings. Stay abreast with the latest analytics techniques and continuously refine your models for enhanced performance, contributing significantly to the data-driven culture of our organization.',\n", + " 'description': 'The Data Analyst interprets complex datasets to provide strategic insights, cleanses and organizes data, performs statistical analysis and predictive modeling to identify trends and inform improvements, utilizes SQL for database management, and employs visualization tools like Tableau or Power BI to effectively communicate findings to stakeholders.'},\n", + " {'name': 'Journalist',\n", + " 'system_message': 'As a Journalist, you are responsible for identifying and pursuing newsworthy stories with the utmost ethical standards and a commitment to factual reporting. Your innate curiosity and excellent communication skills enable you to conduct thorough research and interviews, uncovering the details that make each story compelling and informative. Skilled in both written and verbal storytelling, you craft articles, reports, and features that engage and inform the public, adhering to strict deadlines without compromising on the integrity and accuracy of your work. Proficient in multimedia journalism, you adeptly use digital tools and social media to reach a wider audience, ensuring that your stories have the maximum impact.',\n", + " 'description': 'A Journalist is tasked with ethically sourcing and meticulously reporting newsworthy events, utilizing strong research and storytelling abilities across multiple platforms to accurately inform and engage a diverse audience.'},\n", + " {'name': 'Teacher',\n", + " 'system_message': 'As a Teacher, you are entrusted with the essential responsibility of fostering knowledge and encouraging academic and personal growth in your students. Your deep understanding of pedagogy, coupled with your expertise in the subject matter, enables you to create and deliver curricula that are both engaging and educational. Your adeptness at differentiated instruction allows you to tailor your teaching methods to suit the varied learning styles and needs within your classroom. By skillfully blending traditional teaching techniques with modern educational technology, you facilitate a dynamic and interactive learning environment. You excel in assessment and feedback, not only to gauge student progress but also to continuously improve your own teaching strategies. With strong interpersonal skills, you maintain open lines of communication with students, parents, and colleagues, fostering a collaborative and supportive school community.',\n", + " 'description': \"A Teacher is responsible for cultivating students' knowledge and growth through expertise in pedagogical practices and subject matter, designing engaging curricula, adapting teaching methods to diverse learning needs, integrating technology, and using assessment for continuous improvement while nurturing a cooperative school community.\"},\n", + " {'name': 'Lawyer',\n", + " 'system_message': 'As a Lawyer, you are required to uphold the highest standards of legal proficiency and ethical practice. Your role involves advising clients on their legal rights and responsibilities, as well as representing them in civil and criminal proceedings. You must possess a strong understanding of the law, paired with the ability to analyze case law and legislate history, to construct compelling arguments in support of your client’s position. Your keen attention to detail and dedication to thorough research are crucial in identifying legal precedents and crafting legal documents that adhere to the strictest of procedural standards. Moreover, you must exhibit exceptional negotiation skills to achieve favorable outcomes, whether in the courtroom or at the settlement table. With your articulate verbal and written communication, you clearly and persuasively present cases, explaining complex legal concepts in understandable terms to clients, judges, and juries. Your commitment to confidentiality and upholding justice is paramount and reflected in all aspects of your professional conduct.',\n", + " 'description': 'A Lawyer is a professionally trained legal advocate responsible for representing clients in legal proceedings, providing expert advice on legal matters, constructing persuasive arguments through meticulous research and analysis of law, and negotiating settlements, all while adhering to the highest ethical standards and maintaining strict confidentiality.'},\n", + " {'name': 'Programmer',\n", + " 'system_message': 'As a Programmer, you are responsible for the design, development, and implementation of software programs. Utilize your comprehensive understanding of programming languages, including but not limited to Java, C++, and Python, to create efficient and innovative software solutions. Your role involves writing clean, maintainable code while adhering to best practices in software development. You are expected to troubleshoot, debug, and upgrade existing software, as well as collaborate with cross-functional teams to define and design new product features. Your ability to think algorithmically and solve problems systematically will be integral in creating software that is not only functional but also scalable and secure.',\n", + " 'description': 'A Programmer designs, develops, and implements innovative and efficient software solutions using languages like Java, C++, and Python, ensuring code maintainability, collaborating on new features, and enhancing existing applications with a strong focus on scalability and security.'},\n", + " {'name': 'Accountant',\n", + " 'system_message': 'As Accountant, you are charged with the meticulous management and analysis of financial records, ensuring accuracy and compliance with relevant laws and regulations. Utilize your comprehensive understanding of accounting principles to prepare, examine, and maintain financial reports and statements, including balance sheets and income statements. Your role involves the reconciliation of accounts, evaluating financial operations to recommend best practices, identifying issues, and strategizing solutions for fiscal efficiency and profitability. Mastery in accounting software such as QuickBooks or Sage, alongside proficiency in Microsoft Excel, enables you to efficiently process and analyze financial data. You must ensure proper financial documentation and control systems are in place, providing comprehensive support to the organization’s financial health and integrity.',\n", + " 'description': 'As an Accountant, you are responsible for the accurate and compliant management, analysis, and reporting of financial data, along with recommending strategies to enhance fiscal efficiency and profitability, supported by proficiency in accounting software and Microsoft Excel.'},\n", + " {'name': 'Mathematician',\n", + " 'system_message': 'As a Mathematician, you are responsible for utilizing your profound understanding of mathematical theories and methodologies to solve complex theoretical and practical problems across various domains. Your proficiency in abstract reasoning enables you to develop new mathematical principles and to recognize and articulate the underlying mathematical relationships within real-world scenarios. You apply your expertise in calculus, algebra, statistics, and other mathematical branches to conduct rigorous analyses and to model systems for prediction and optimization. With a strong foundation in logic and quantitative reasoning, you perform peer reviews and contribute to interdisciplinary research projects, ensuring accuracy and consistency in mathematical arguments and results. Your role is crucial in advancing mathematical knowledge and providing innovative solutions to scientific and engineering challenges.',\n", + " 'description': 'As a Mathematician, you apply advanced mathematical theories and analytical skills to solve theoretical and practical problems in various industries, develop new principles, and provide innovative solutions to complex scientific and engineering challenges.'},\n", + " {'name': 'Physicist',\n", + " 'system_message': 'As a Physicist, you are charged with applying your profound understanding of the physical laws that govern the universe to unravel complex scientific phenomena. Your proficiency in theoretical and experimental physics enables you to develop models and conduct experiments that explore fundamental forces and particles. With exceptional analytical skills, you interpret empirical data to validate existing theories or propose new explanations for unexplained observations. Mastery in the use of mathematical tools such as differential equations and linear algebra is crucial for you to simulate physical processes. You are also adept at using specialized software and equipment for data acquisition and analysis, contributing to advancements in fields ranging from quantum mechanics to cosmology. Your strong critical thinking abilities empower you to solve intricate problems, and your commitment to scientific rigor ensures the integrity and accuracy of your research outcomes.',\n", + " 'description': 'A Physicist applies deep knowledge of physical laws to investigate scientific phenomena through theoretical modeling and experimental research, utilizing advanced mathematical techniques and specialized equipment to advance understanding in areas such as quantum mechanics and cosmology.'},\n", + " {'name': 'Biologist',\n", + " 'system_message': 'As a Biologist, you are entrusted with the study and understanding of living organisms, applying your expertise to investigate their functions, genetics, evolution, and ecosystems. Your skills in experimental design empower you to conduct research and experiments that can unlock new biological insights and improve our comprehension of life processes. Utilizing advanced microscopy techniques and molecular biology methods, you should meticulously analyze cell structures and DNA sequences to uncover the intricacies of life at a microscopic level. Demonstrate proficiency in bioinformatics tools to analyze genetic data and contribute valuable findings to the scientific community. Furthermore, as a communicator of science, ensure that your research findings are effectively documented and presented in scientific journals and at conferences, thereby enhancing the collective knowledge in your field.',\n", + " 'description': 'A Biologist meticulously studies and understands living organisms, conducting advanced research to decode genetics and ecosystems and sharing findings through scientific publications and presentations.'},\n", + " {'name': 'Chemist',\n", + " 'system_message': 'As a Chemist, you are charged with applying your profound understanding of chemical principles to conduct complex experiments, synthesize new compounds, and analyze the molecular and atomic structure of materials. Your proficiency in utilizing sophisticated analytical techniques - such as chromatography, spectroscopy, and mass spectrometry - enables you to decipher the composition and properties of substances. The knowledge you hold in chemical safety and handling procedures ensures a secure laboratory environment. With an adeptness in maintaining accurate records and an insightful approach to interpreting data, you transform raw experimental results into valuable scientific insights. Your ability to communicate complex chemical information clearly makes you essential in collaborative research efforts and in driving innovation within the field.',\n", + " 'description': 'As a Chemist, you are responsible for conducting advanced experiments, synthesizing compounds, deciphering substance compositions with techniques like chromatography and mass spectrometry, and transforming experimental data into scientific insights, while maintaining safety and clear communication in research collaborations.'},\n", + " {'name': 'Statistician',\n", + " 'system_message': 'As a Statistician, your primary duty is to apply mathematical and statistical methods to collect, analyze, and interpret numerical data to make informed decisions. Your strong grounding in probability theory will be essential for designing surveys and experiments to generate data. You are adept at constructing and applying sophisticated statistical models and methods, such as linear regression, ANOVA, or time-series analysis, ensuring that you accurately capture trends and relationships within the data. You possess an in-depth understanding of statistical software such as R or SAS, allowing you to perform complex analyses with efficiency and precision. Your ability to communicate complex statistical concepts to non-experts will be crucial; hence, your role includes presenting findings in a clear, actionable manner, with data visualizations and reports that drive strategic planning and policy development.',\n", + " 'description': 'A Statistician employs and interprets advanced statistical techniques to design data-collection processes, analyze data, and present findings in a comprehensible manner, supporting evidence-based decision-making and policy formation.'},\n", + " {'name': 'IT_Specialist',\n", + " 'system_message': 'As an IT Specialist, your primary responsibility is to maintain the integrity and functionality of all our computer systems and networks. Your comprehensive understanding of hardware and software is crucial for diagnosing and resolving technical issues. You are adept at implementing network security measures to protect data and systems from cyber threats. You also play a significant role in systems and software upgrades, ensuring a seamless transition without disrupting workflow. Utilizing your strong problem-solving skills and proficiency in scripting languages, you automate repetitive tasks, enhancing system efficiency. Your ability to communicate effectively with team members and non-technical staff allows you to provide clear guidance and end-user support.',\n", + " 'description': 'An IT Specialist is responsible for upholding and optimizing our computer systems and networks through maintenance, security, upgrades, issue resolution, automation, and providing support and clear communication to both technical and non-technical personnel.'},\n", + " {'name': 'Cybersecurity_Expert',\n", + " 'system_message': \"As a Cybersecurity Expert, you are charged with the responsibility of safeguarding the organization's computer networks and systems. Your deep understanding of cyber threats and mitigation techniques is critical in identifying vulnerabilities and protecting against malicious attacks. Employing your experience with tools such as firewalls, antivirus software, and intrusion detection systems, you will continuously monitor and defend our digital infrastructure. You are expected to conduct regular security audits and penetration testing to simulate cyber attacks and find potential weaknesses before they can be exploited. Your proficiency in risk management frameworks and incident response protocols ensures that you are prepared to swiftly handle and mitigate any security incidents that occur. With your expertise in encryption technologies and network protocols, you protect sensitive data and ensure compliance with relevant security standards and regulations. Your foresight in staying up-to-date with the latest cybersecurity trends and threats is paramount to maintaining the organization's digital defense at its peak.\",\n", + " 'description': \"As a Cybersecurity Expert, you are responsible for the proactive protection and defense of an organization's computer networks and systems against cyber threats through continuous monitoring, conducting security audits, penetrating testing, and swiftly mitigating security incidents, while ensuring compliance with security regulations.\"},\n", + " {'name': 'Artificial_Intelligence_Engineer',\n", + " 'system_message': 'As an Artificial Intelligence Engineer, you are responsible for conceptualizing, designing, and implementing intelligent systems that simulate human cognitive processes. Your role demands a deep understanding of neural networks, particularly Convolutional Neural Networks (CNNs) for image recognition tasks and Recurrent Neural Networks (RNNs) for natural language processing. With your expertise in TensorFlow or PyTorch, you develop complex models that can learn, adapt, and make decisions. You prioritize the ethical design and deployment of AI systems, conscious of the implications your work may have on society. Mastery of algorithms and a proficiency in a high-level programming language, preferably Python, enable you to transform theoretical AI concepts into practical solutions that drive innovation and efficiency.',\n", + " 'description': 'An Artificial Intelligence Engineer specializes in creating and implementing advanced intelligent systems, with a mastery of neural networks, machine learning frameworks, and ethical AI principles, to develop innovative solutions that emulate human cognition.'},\n", + " {'name': 'Financial_Analyst',\n", + " 'system_message': 'As a Financial Analyst, you are entrusted with utilizing your in-depth understanding of financial principles to assess investment opportunities, analyze financial data, and forecast economic trends. Your proficiency in financial modeling is paramount, enabling you to develop complex models that underpin the valuation of stocks, bonds, and other financial instruments. With a sharp eye for detail, you scrutinize company financial statements to derive actionable insights and recommend strategies to optimize financial performance. Your expertise in Excel, especially with advanced functions and formulas, allows you to efficiently manipulate and analyze large financial datasets. You are a whiz at creating compelling visualizations and delivering presentations to communicate your findings and influence strategic decisions. Your role is crucial in guiding investment decisions and driving the fiscal prudence of the organization.',\n", + " 'description': \"A Financial Analyst performs in-depth financial analysis and modeling to evaluate investments, forecast economic trends, and deliver strategic recommendations, leveraging advanced Excel skills to inform and guide the organization's financial decisions.\"}]" ] - } - ], - "metadata": { - "front_matter": { - "description": "Automatically build multi-agent system from agent library", - "tags": [ - "autobuild" - ] - }, - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.14" - } + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys_msg_list" + ] + }, + { + "cell_type": "markdown", + "id": "256dd32b03a7a172", + "metadata": { + "collapsed": false + }, + "source": [ + "We can save the generated agents' information into a json file." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "53111125938845cf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:40:01.750855900Z", + "start_time": "2023-12-23T07:40:01.710399600Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "json.dump(sys_msg_list, open(\"./agent_library_example.json\", \"w\"), indent=4)" + ] + }, + { + "cell_type": "markdown", + "id": "cfd883b79a3bd932", + "metadata": { + "collapsed": false + }, + "source": [ + "## Build agents from library (by LLM)\n", + "Here, we introduce how to build agents from the generated library. As in the previous `build`, we also need to specify a `building_task` that lets the build manager know which agents should be selected from the library according to the task. \n", + "\n", + "We also need to specify a `library_path_or_json`, which can be a path of library or a JSON string with agents' configs. Here, we use the previously saved path as the library path." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8963a8709c8e92e2", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:40:01.752918500Z", + "start_time": "2023-12-23T07:40:01.735461Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "library_path_or_json = \"./agent_library_example.json\"\n", + "building_task = \"Find a paper on arxiv by programming, and analyze its application in some domain. For example, find a recent paper about gpt-4 on arxiv and find its potential applications in software.\"" + ] + }, + { + "cell_type": "markdown", + "id": "72656a8d0c1a9b12", + "metadata": { + "collapsed": false + }, + "source": [ + "Then, we can call the `build_from_library` from the AgentBuilder to generate a list of agents from the library and let them complete the user's `execution_task` in a group chat." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5c669b76b2c9b750", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-23T07:46:02.075542200Z", + "start_time": "2023-12-23T07:43:55.489042900Z" + }, + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m==> Looking for suitable agents in the library...\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['Programmer', 'Mathematician'] are selected.\n", + "\u001b[32m==> Creating agents...\u001b[0m\n", + "Creating agent Programmer...\n", + "Creating agent Mathematician...\n", + "Adding user console proxy...\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Find a recent paper about explainable AI on arxiv and find its potential applications in medical.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "To find a recent paper about explainable AI on arXiv, we can use the arXiv API to search for papers that match the query. However, since I cannot directly access external APIs, I suggest that one of us manually searches for the paper on the arXiv website using relevant search terms such as \"explainable AI\" and \"medical applications\". Once we find a suitable paper, we can discuss its potential applications in the medical field. \n", + "\n", + "Mathematician, would you like to perform the search, or shall I provide a Python script that could be used to perform the search programmatically?\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "As a Mathematician, I can certainly appreciate the value of a programmatic approach to searching for academic papers. However, since I do not have direct access to execute code or APIs, I would suggest that you, as the Programmer, provide the Python script that could be used to perform the search on arXiv. Once we have identified a paper, I can then assist in discussing its potential applications in the medical field from a mathematical perspective.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Understood. I will provide a Python script that can be used to search for recent papers about explainable AI on arXiv with potential applications in the medical field. The script will use the `arxiv` Python package, which is a wrapper for the arXiv API. If the package is not installed, we will need to install it first.\n", + "\n", + "Let's start by checking if the `arxiv` package is installed and if not, we will install it. Computer_terminal, please execute the following command to check for the `arxiv` package and install it if necessary.\n", + "\n", + "```sh\n", + "pip show arxiv || pip install arxiv\n", + "```\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is sh)...\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "Name: arxiv\n", + "Version: 2.1.0\n", + "Summary: Python wrapper for the arXiv API: https://arxiv.org/help/api/\n", + "Home-page: https://github.com/lukasschwab/arxiv.py\n", + "Author: Lukas Schwab\n", + "Author-email: lukas.schwab@gmail.com\n", + "License: MIT\n", + "Location: /home/vscode/.local/lib/python3.10/site-packages\n", + "Requires: feedparser, requests\n", + "Required-by: \n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Great, the `arxiv` package is already installed. Now, I will provide a Python script that uses the `arxiv` package to search for recent papers related to explainable AI with potential applications in the medical field. The script will query the arXiv API for papers with relevant keywords and print out the title and summary of the most recent paper found.\n", + "\n", + "Computer_terminal, please execute the following Python script.\n", + "\n", + "```python\n", + "import arxiv\n", + "\n", + "# Define the search query\n", + "search_query = 'all:explainable AI AND all:medical'\n", + "\n", + "# Search for papers on arXiv\n", + "search = arxiv.Search(\n", + " query = search_query,\n", + " max_results = 1,\n", + " sort_by = arxiv.SortCriterion.SubmittedDate\n", + ")\n", + "\n", + "# Fetch the most recent paper\n", + "for paper in search.results():\n", + " print(\"Title:\", paper.title)\n", + " print(\"Summary:\", paper.summary)\n", + " # Only print the most recent paper\n", + " break\n", + "```\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "Title: Automated Information Extraction from Thyroid Operation Narrative: A Comparative Study of GPT-4 and Fine-tuned KoELECTRA\n", + "Summary: In the rapidly evolving field of healthcare, the integration of artificial\n", + "intelligence (AI) has become a pivotal component in the automation of clinical\n", + "workflows, ushering in a new era of efficiency and accuracy. This study focuses\n", + "on the transformative capabilities of the fine-tuned KoELECTRA model in\n", + "comparison to the GPT-4 model, aiming to facilitate automated information\n", + "extraction from thyroid operation narratives. The current research landscape is\n", + "dominated by traditional methods heavily reliant on regular expressions, which\n", + "often face challenges in processing free-style text formats containing critical\n", + "details of operation records, including frozen biopsy reports. Addressing this,\n", + "the study leverages advanced natural language processing (NLP) techniques to\n", + "foster a paradigm shift towards more sophisticated data processing systems.\n", + "Through this comparative study, we aspire to unveil a more streamlined,\n", + "precise, and efficient approach to document processing in the healthcare\n", + "domain, potentially revolutionizing the way medical data is handled and\n", + "analyzed.\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "The paper titled \"Automated Information Extraction from Thyroid Operation Narrative: A Comparative Study of GPT-4 and Fine-tuned KoELECTRA\" presents a study on the use of artificial intelligence for automating the extraction of information from thyroid operation narratives. This is a clear example of explainable AI being applied in the medical field, specifically in the area of clinical workflows and document processing.\n", + "\n", + "The potential applications of such technology in medicine are vast. By automating the extraction of information from operation narratives, healthcare professionals can save time and reduce the likelihood of human error. This can lead to more accurate patient records, improved patient care, and streamlined administrative processes. Additionally, the ability to quickly and accurately process operation records can facilitate better data analysis, which can be used for medical research, trend analysis, and improving healthcare outcomes.\n", + "\n", + "The use of advanced natural language processing (NLP) techniques, as mentioned in the summary, is particularly important for processing free-style text formats that contain critical medical information. This technology could be further explored to extend its application to other types of medical documents and records, enhancing the overall efficiency of the healthcare system.\n", + "\n", + "The study's focus on comparing the performance of the fine-tuned KoELECTRA model with GPT-4 also highlights the importance of evaluating different AI models to determine the most effective approach for specific medical applications. This comparative analysis can lead to the development of more specialized AI tools tailored to the needs of the healthcare industry.\n", + "\n", + "In conclusion, the research presented in this paper has significant implications for the future of medical document processing and the broader integration of AI in healthcare.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "The insights provided by the Mathematician are indeed valuable. The application of AI for automated information extraction from medical documents like thyroid operation narratives can greatly enhance efficiency and accuracy in healthcare. The use of models like GPT-4 and KoELECTRA for natural language processing tasks shows the potential of AI to handle complex, unstructured data which is common in medical records.\n", + "\n", + "From a programming perspective, the implementation of such AI systems would involve training models on large datasets of medical documents to learn the context and semantics specific to medical terminology. Ensuring the explainability of AI in this context is crucial, as healthcare professionals need to understand and trust the AI's decision-making process, especially when it directly affects patient care.\n", + "\n", + "Moreover, the integration of explainable AI into healthcare systems must adhere to strict privacy and security regulations to protect sensitive patient data. This requires careful design and implementation of data handling procedures within the AI system.\n", + "\n", + "The potential applications extend beyond just document processing to diagnostic assistance, personalized treatment plans, and predictive analytics for patient outcomes. As AI technology continues to evolve, its role in supporting and enhancing the capabilities of healthcare professionals will undoubtedly expand.\n", + "\n", + "Given the importance of the topic and the potential impact on healthcare, it would be beneficial to keep an eye on further developments in this field. If there are no further questions or points to discuss, we can conclude our conversation on this topic.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "I agree with the Programmer's assessment. The implementation of AI in healthcare does indeed require careful consideration of the models used, the training data, and the explainability of the AI's decisions. The ethical implications, particularly concerning patient privacy and data security, are paramount and must be addressed with the utmost rigor.\n", + "\n", + "The potential for AI to assist in diagnostics, treatment planning, and predictive analytics is a promising development for the future of medicine. It is essential that these systems are developed in collaboration with healthcare professionals to ensure they meet the real-world needs of the field.\n", + "\n", + "The interdisciplinary nature of this work, combining expertise in mathematics, computer science, and medicine, is a testament to the collaborative efforts needed to advance healthcare technology. It has been a pleasure discussing the potential applications of explainable AI in medicine with you.\n", + "\n", + "If there are no further points to add, I believe we have reached a natural conclusion to our conversation.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mAll agents have been cleared.\u001b[0m\n" + ] + } + ], + "source": [ + "new_builder = AgentBuilder(\n", + " config_file_or_env=config_file_or_env, builder_model=\"gpt-4-1106-preview\", agent_model=\"gpt-4-1106-preview\"\n", + ")\n", + "agent_list, _ = new_builder.build_from_library(building_task, library_path_or_json, llm_config)\n", + "start_task(\n", + " execution_task=\"Find a recent paper about explainable AI on arxiv and find its potential applications in medical.\",\n", + " agent_list=agent_list,\n", + ")\n", + "new_builder.clear_all_agents()" + ] + }, + { + "cell_type": "markdown", + "id": "c7a10e6fa00a5a0d", + "metadata": { + "collapsed": false + }, + "source": [ + "## Build agents from library (by description-task similarity)\n", + "We also support using embedding similarity to select agents. You can use a [Sentence-Transformers model](https://www.sbert.net/docs/pretrained_models.html) as an embedding extractor, and AgentBuilder will select agents with profiles that are the most similar to the building task from the library by comparing their embedding similarity. This will reduce the use of LLMs but may have less accuracy." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "521dc5f961efde59", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-26T17:01:29.333975100Z", + "start_time": "2023-12-26T16:58:11.070813500Z" + }, + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m==> Looking for suitable agents in the library...\u001b[0m\n" + ] }, - "nbformat": 4, - "nbformat_minor": 5 + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['Programmer', 'Mathematician'] are selected.\n", + "\u001b[32m==> Creating agents...\u001b[0m\n", + "Creating agent Programmer...\n", + "Creating agent Mathematician...\n", + "Adding user console proxy...\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Find a recent paper about gpt-4 on arxiv and find its potential applications in software.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "To find a recent paper about GPT-4 on arXiv, we can use the arXiv API to search for papers. However, since I can't directly access external APIs, I can write a Python script that you can run on your local machine to perform this search. Would you like me to provide you with such a script?\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "There is no code from the last 1 message for me to execute. Group chat manager should let other participants to continue the conversation. If the group chat manager want to end the conversation, you should let other participant reply me only with \"TERMINATE\"\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "I apologize for the confusion. I will provide a Python script that can be executed by the Computer_terminal to search for recent papers about GPT-4 on arXiv. Let's proceed with that.\n", + "\n", + "```python\n", + "import requests\n", + "from xml.etree import ElementTree\n", + "\n", + "# Define the search parameters and URL for the arXiv API\n", + "search_query = 'all:gpt-4'\n", + "start = 0\n", + "max_results = 5\n", + "sort_by = 'submittedDate'\n", + "sort_order = 'descending'\n", + "url = f'http://export.arxiv.org/api/query?search_query={search_query}&start={start}&max_results={max_results}&sortBy={sort_by}&sortOrder={sort_order}'\n", + "\n", + "# Send a GET request to the arXiv API\n", + "response = requests.get(url)\n", + "\n", + "# Parse the response if it was successful\n", + "if response.status_code == 200:\n", + " root = ElementTree.fromstring(response.content)\n", + " # Find and print the entries (papers)\n", + " for entry in root.findall('{http://www.w3.org/2005/Atom}entry'):\n", + " title = entry.find('{http://www.w3.org/2005/Atom}title').text\n", + " summary = entry.find('{http://www.w3.org/2005/Atom}summary').text\n", + " published = entry.find('{http://www.w3.org/2005/Atom}published').text\n", + " print(f\"Title: {title}\\nSummary: {summary}\\nPublished Date: {published}\\n\")\n", + "else:\n", + " print(f\"Failed to fetch data from arXiv. Status code: {response.status_code}\")\n", + "```\n", + "\n", + "This script will fetch the most recent papers related to GPT-4 from the arXiv API and print out their titles, summaries, and publication dates. Please execute this script to find the information we need.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Computer_terminal\n", + "\u001b[0m\n", + "\u001b[31m\n", + ">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n", + "\u001b[33mComputer_terminal\u001b[0m (to chat_manager):\n", + "\n", + "exitcode: 0 (execution succeeded)\n", + "Code output: \n", + "Title: What If We Recaption Billions of Web Images with LLaMA-3?\n", + "Summary: Web-crawled image-text pairs are inherently noisy. Prior studies demonstrate\n", + "that semantically aligning and enriching textual descriptions of these pairs\n", + "can significantly enhance model training across various vision-language tasks,\n", + "particularly text-to-image generation. However, large-scale investigations in\n", + "this area remain predominantly closed-source. Our paper aims to bridge this\n", + "community effort, leveraging the powerful and \\textit{open-sourced} LLaMA-3, a\n", + "GPT-4 level LLM. Our recaptioning pipeline is simple: first, we fine-tune a\n", + "LLaMA-3-8B powered LLaVA-1.5 and then employ it to recaption 1.3 billion images\n", + "from the DataComp-1B dataset. Our empirical results confirm that this enhanced\n", + "dataset, Recap-DataComp-1B, offers substantial benefits in training advanced\n", + "vision-language models. For discriminative models like CLIP, we observe\n", + "enhanced zero-shot performance in cross-modal retrieval tasks. For generative\n", + "models like text-to-image Diffusion Transformers, the generated images exhibit\n", + "a significant improvement in alignment with users' text instructions,\n", + "especially in following complex queries. Our project page is\n", + "https://www.haqtu.me/Recap-Datacomp-1B/\n", + "\n", + "Published Date: 2024-06-12T17:59:07Z\n", + "\n", + "Title: DafnyBench: A Benchmark for Formal Software Verification\n", + "Summary: We introduce DafnyBench, the largest benchmark of its kind for training and\n", + "evaluating machine learning systems for formal software verification. We test\n", + "the ability of LLMs such as GPT-4 and Claude 3 to auto-generate enough hints\n", + "for the Dafny formal verification engine to successfully verify over 750\n", + "programs with about 53,000 lines of code. The best model and prompting scheme\n", + "achieved 68% success rate, and we quantify how this rate improves when retrying\n", + "with error message feedback and how it deteriorates with the amount of required\n", + "code and hints. We hope that DafnyBench will enable rapid improvements from\n", + "this baseline as LLMs and verification techniques grow in quality.\n", + "\n", + "Published Date: 2024-06-12T17:53:31Z\n", + "\n", + "Title: A Sociotechnical Lens for Evaluating Computer Vision Models: A Case\n", + " Study on Detecting and Reasoning about Gender and Emotion\n", + "Summary: In the evolving landscape of computer vision (CV) technologies, the automatic\n", + "detection and interpretation of gender and emotion in images is a critical area\n", + "of study. This paper investigates social biases in CV models, emphasizing the\n", + "limitations of traditional evaluation metrics such as precision, recall, and\n", + "accuracy. These metrics often fall short in capturing the complexities of\n", + "gender and emotion, which are fluid and culturally nuanced constructs. Our\n", + "study proposes a sociotechnical framework for evaluating CV models,\n", + "incorporating both technical performance measures and considerations of social\n", + "fairness. Using a dataset of 5,570 images related to vaccination and climate\n", + "change, we empirically compared the performance of various CV models, including\n", + "traditional models like DeepFace and FER, and generative models like GPT-4\n", + "Vision. Our analysis involved manually validating the gender and emotional\n", + "expressions in a subset of images to serve as benchmarks. Our findings reveal\n", + "that while GPT-4 Vision outperforms other models in technical accuracy for\n", + "gender classification, it exhibits discriminatory biases, particularly in\n", + "response to transgender and non-binary personas. Furthermore, the model's\n", + "emotion detection skew heavily towards positive emotions, with a notable bias\n", + "towards associating female images with happiness, especially when prompted by\n", + "male personas. These findings underscore the necessity of developing more\n", + "comprehensive evaluation criteria that address both validity and discriminatory\n", + "biases in CV models. Our proposed framework provides guidelines for researchers\n", + "to critically assess CV tools, ensuring their application in communication\n", + "research is both ethical and effective. The significant contribution of this\n", + "study lies in its emphasis on a sociotechnical approach, advocating for CV\n", + "technologies that support social good and mitigate biases rather than\n", + "perpetuate them.\n", + "\n", + "Published Date: 2024-06-12T13:52:30Z\n", + "\n", + "Title: Supportiveness-based Knowledge Rewriting for Retrieval-augmented\n", + " Language Modeling\n", + "Summary: Retrieval-augmented language models (RALMs) have recently shown great\n", + "potential in mitigating the limitations of implicit knowledge in LLMs, such as\n", + "untimely updating of the latest expertise and unreliable retention of long-tail\n", + "knowledge. However, since the external knowledge base, as well as the\n", + "retriever, can not guarantee reliability, potentially leading to the knowledge\n", + "retrieved not being helpful or even misleading for LLM generation. In this\n", + "paper, we introduce Supportiveness-based Knowledge Rewriting (SKR), a robust\n", + "and pluggable knowledge rewriter inherently optimized for LLM generation.\n", + "Specifically, we introduce the novel concept of \"supportiveness\"--which\n", + "represents how effectively a knowledge piece facilitates downstream tasks--by\n", + "considering the perplexity impact of augmented knowledge on the response text\n", + "of a white-box LLM. Based on knowledge supportiveness, we first design a\n", + "training data curation strategy for our rewriter model, effectively identifying\n", + "and filtering out poor or irrelevant rewrites (e.g., with low supportiveness\n", + "scores) to improve data efficacy. We then introduce the direct preference\n", + "optimization (DPO) algorithm to align the generated rewrites to optimal\n", + "supportiveness, guiding the rewriter model to summarize augmented content that\n", + "better improves the final response. Comprehensive evaluations across six\n", + "popular knowledge-intensive tasks and four LLMs have demonstrated the\n", + "effectiveness and superiority of SKR. With only 7B parameters, SKR has shown\n", + "better knowledge rewriting capability over GPT-4, the current state-of-the-art\n", + "general-purpose LLM.\n", + "\n", + "Published Date: 2024-06-12T11:52:35Z\n", + "\n", + "Title: Automated Information Extraction from Thyroid Operation Narrative: A\n", + " Comparative Study of GPT-4 and Fine-tuned KoELECTRA\n", + "Summary: In the rapidly evolving field of healthcare, the integration of artificial\n", + "intelligence (AI) has become a pivotal component in the automation of clinical\n", + "workflows, ushering in a new era of efficiency and accuracy. This study focuses\n", + "on the transformative capabilities of the fine-tuned KoELECTRA model in\n", + "comparison to the GPT-4 model, aiming to facilitate automated information\n", + "extraction from thyroid operation narratives. The current research landscape is\n", + "dominated by traditional methods heavily reliant on regular expressions, which\n", + "often face challenges in processing free-style text formats containing critical\n", + "details of operation records, including frozen biopsy reports. Addressing this,\n", + "the study leverages advanced natural language processing (NLP) techniques to\n", + "foster a paradigm shift towards more sophisticated data processing systems.\n", + "Through this comparative study, we aspire to unveil a more streamlined,\n", + "precise, and efficient approach to document processing in the healthcare\n", + "domain, potentially revolutionizing the way medical data is handled and\n", + "analyzed.\n", + "\n", + "Published Date: 2024-06-12T06:44:05Z\n", + "\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Programmer\n", + "\u001b[0m\n", + "\u001b[33mProgrammer\u001b[0m (to chat_manager):\n", + "\n", + "Based on the search results from the arXiv API, we have found several papers that discuss potential applications of GPT-4 in software:\n", + "\n", + "1. **Recaptioning Web Images with LLaMA-3 and GPT-4**: This paper discusses the use of GPT-4 level LLMs for recaptioning web images, which can enhance model training across various vision-language tasks. This has implications for improving the quality of datasets used in machine learning and could be particularly beneficial for text-to-image generation and cross-modal retrieval tasks.\n", + "\n", + "2. **DafnyBench: A Benchmark for Formal Software Verification**: This paper introduces a benchmark for training and evaluating machine learning systems for formal software verification. It tests the ability of LLMs such as GPT-4 to auto-generate hints for the Dafny formal verification engine to successfully verify programs. This application could significantly impact the field of software verification by automating the generation of verification hints, potentially improving the efficiency and reliability of the verification process.\n", + "\n", + "3. **Automated Information Extraction from Thyroid Operation Narrative**: This study compares the GPT-4 model with the fine-tuned KoELECTRA model for automated information extraction from thyroid operation narratives. The application of GPT-4 in this context could revolutionize document processing in healthcare by providing a more efficient and accurate method for extracting information from medical records.\n", + "\n", + "These papers suggest that GPT-4 has the potential to be applied in various software-related fields, including enhancing datasets for machine learning, formal software verification, and healthcare document processing. The applications in these papers could lead to more efficient, accurate, and reliable software systems across different domains.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "The applications mentioned indeed highlight the versatility of GPT-4 in different domains. To further understand the potential impact of GPT-4 on software, let's delve into the mathematical and algorithmic principles that could be at play in these applications:\n", + "\n", + "1. **Recaptioning Web Images**: The process of recaptioning images with a language model like GPT-4 involves understanding the context of an image and generating descriptive text that accurately reflects its content. This task likely involves a combination of computer vision techniques to interpret the image and natural language processing to generate the caption. From a mathematical perspective, this would involve optimization algorithms to fine-tune the language model on a specific dataset, ensuring that the generated captions are both semantically and syntactically correct.\n", + "\n", + "2. **Formal Software Verification**: The use of GPT-4 to auto-generate hints for formal verification engines like Dafny involves the model understanding the logic and structure of the code. This requires a deep understanding of formal logic, proof theory, and possibly type theory if the language being verified is statically typed. The success rate of auto-generated hints would depend on the model's ability to reason about the correctness of code and the underlying mathematical properties that ensure its validity.\n", + "\n", + "3. **Automated Information Extraction from Medical Records**: For GPT-4 to extract information from medical narratives, it must process unstructured text and identify relevant medical terms and their relationships. This task involves natural language understanding, which from a mathematical standpoint, can be seen as a form of pattern recognition and classification. The model would need to be trained on a large corpus of medical texts, and its performance would be measured by its precision and recall in identifying and extracting the correct information.\n", + "\n", + "In each of these applications, GPT-4's effectiveness would be influenced by the underlying mathematical models, such as neural networks, and the optimization techniques used during training, such as gradient descent. The quality of the training data and the model's architecture (e.g., attention mechanisms, transformer layers) also play a crucial role in its performance.\n", + "\n", + "To verify the potential of GPT-4 in these applications, one could set up experiments to measure the performance of GPT-4 against specific benchmarks or metrics relevant to each domain. For example, in the case of formal software verification, one could measure the percentage of programs that are successfully verified with the hints generated by GPT-4 compared to a baseline or human-generated hints.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "To further verify the potential applications of GPT-4 in software, we can consider the following:\n", + "\n", + "1. **Recaptioning Web Images**: The effectiveness of GPT-4 in this application can be measured by the accuracy of the captions it generates. This can be quantified using metrics such as BLEU (Bilingual Evaluation Understudy) or ROUGE (Recall-Oriented Understudy for Gisting Evaluation), which compare the machine-generated text to a set of reference captions. A high score on these metrics would indicate that GPT-4 is capable of understanding and describing the content of images accurately.\n", + "\n", + "2. **Formal Software Verification**: To verify the application of GPT-4 in software verification, we could measure the success rate of the model in generating verification hints that lead to successful program verification. This could be done by comparing the verification success rate with and without the hints provided by GPT-4. Additionally, the complexity of the programs and the hints required could be analyzed to understand the model's limitations and strengths.\n", + "\n", + "3. **Automated Information Extraction from Medical Records**: The performance of GPT-4 in extracting information from medical narratives can be assessed by comparing the extracted information to a gold standard set of annotations. Precision, recall, and F1-score are common metrics used to evaluate information extraction systems. A high F1-score would suggest that GPT-4 is effective in identifying and extracting relevant information from unstructured medical texts.\n", + "\n", + "For each of these applications, it would be important to conduct a thorough analysis of the results to ensure that the model's performance is not only statistically significant but also practically relevant. Additionally, it would be crucial to assess the model's performance on diverse datasets to ensure that it generalizes well across different contexts and does not exhibit biases that could lead to incorrect or unfair outcomes.\n", + "\n", + "If we want to simulate or test these applications, we could propose specific experiments or data analysis procedures that align with the above verification strategies. However, such experiments would require access to the relevant datasets and computational resources to run the models, which may not be feasible within the scope of this conversation.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "To simulate or test these applications, we could propose specific experiments or data analysis procedures that align with the above verification strategies. However, such experiments would require access to the relevant datasets and computational resources to run the models, which may not be feasible within the scope of this conversation.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[32m\n", + "Next speaker: Mathematician\n", + "\u001b[0m\n", + "\u001b[33mMathematician\u001b[0m (to chat_manager):\n", + "\n", + "TERMINATE\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mAll agents have been cleared.\u001b[0m\n" + ] + } + ], + "source": [ + "new_builder = AgentBuilder(\n", + " config_file_or_env=config_file_or_env, builder_model=\"gpt-4-1106-preview\", agent_model=\"gpt-4-1106-preview\"\n", + ")\n", + "agent_list, _ = new_builder.build_from_library(\n", + " building_task, library_path_or_json, llm_config, embedding_model=\"all-mpnet-base-v2\"\n", + ")\n", + "start_task(\n", + " execution_task=\"Find a recent paper about gpt-4 on arxiv and find its potential applications in software.\",\n", + " agent_list=agent_list,\n", + ")\n", + "new_builder.clear_all_agents()" + ] + } + ], + "metadata": { + "front_matter": { + "description": "Automatically build multi-agent system from agent library", + "tags": [ + "autobuild" + ] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } From 1342f7572ebfa1f6e019e32fc5d24c0ddae774e8 Mon Sep 17 00:00:00 2001 From: Davor Runje Date: Fri, 20 Dec 2024 00:38:28 +0100 Subject: [PATCH 51/51] websockets upgraded to 14.x --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 4bef018b03..7492699d17 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ "pydantic>=1.10,<3,!=2.6.0", # could be both V1 and V2 "docker", "packaging", - "websockets>=14.0", + "websockets>=14,<15", "asyncer>=0.0.8", ] @@ -120,7 +120,7 @@ "websurfer": ["beautifulsoup4", "markdownify", "pdfminer.six", "pathvalidate"], "redis": ["redis"], "cosmosdb": ["azure-cosmos>=4.2.0"], - "websockets": ["websockets>=12.0,<13"], + "websockets": ["websockets>=14.0,<15"], "jupyter-executor": jupyter_executor, "types": types, "long-context": ["llmlingua<0.3"],