Skip to content

Commit db21164

Browse files
committed
Add asyncio event loop
1 parent 92c8e9e commit db21164

3 files changed

Lines changed: 447 additions & 17 deletions

File tree

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,100 @@ async def print_models() -> None:
156156
asyncio.run(print_models())
157157
```
158158

159+
## Conversational AI
160+
161+
Build interactive AI agents with real-time audio capabilities using ElevenLabs Conversational AI.
162+
163+
### Basic Usage
164+
165+
```python
166+
from elevenlabs.client import ElevenLabs
167+
from elevenlabs.conversational_ai.conversation import Conversation, ClientTools
168+
from elevenlabs.conversational_ai.default_audio_interface import DefaultAudioInterface
169+
170+
client = ElevenLabs(api_key="YOUR_API_KEY")
171+
172+
# Create audio interface for real-time audio input/output
173+
audio_interface = DefaultAudioInterface()
174+
175+
# Create conversation
176+
conversation = Conversation(
177+
client=client,
178+
agent_id="your-agent-id",
179+
requires_auth=True,
180+
audio_interface=audio_interface,
181+
)
182+
183+
# Start the conversation
184+
conversation.start_session()
185+
186+
# The conversation runs in background until you call:
187+
conversation.end_session()
188+
```
189+
190+
### Custom Event Loop Support
191+
192+
For advanced use cases involving context propagation, resource reuse, or specific event loop management, `ClientTools` supports custom asyncio event loops:
193+
194+
```python
195+
import asyncio
196+
from elevenlabs.conversational_ai.conversation import ClientTools
197+
198+
async def main():
199+
# Get the current event loop
200+
custom_loop = asyncio.get_running_loop()
201+
202+
# Create ClientTools with custom loop to prevent "different event loop" errors
203+
client_tools = ClientTools(loop=custom_loop)
204+
205+
# Register your tools
206+
async def get_weather(params):
207+
location = params.get("location", "Unknown")
208+
# Your async logic here
209+
return f"Weather in {location}: Sunny, 72°F"
210+
211+
client_tools.register("get_weather", get_weather, is_async=True)
212+
213+
# Use with conversation
214+
conversation = Conversation(
215+
client=client,
216+
agent_id="your-agent-id",
217+
requires_auth=True,
218+
audio_interface=audio_interface,
219+
client_tools=client_tools
220+
)
221+
222+
asyncio.run(main())
223+
```
224+
225+
**Benefits of Custom Event Loop:**
226+
- **Context Propagation**: Maintain request-scoped state across async operations
227+
- **Resource Reuse**: Share existing async resources like HTTP sessions or database pools
228+
- **Loop Management**: Prevent "Task got Future attached to a different event loop" errors
229+
- **Performance**: Better control over async task scheduling and execution
230+
231+
### Tool Registration
232+
233+
Register custom tools that the AI agent can call during conversations:
234+
235+
```python
236+
client_tools = ClientTools()
237+
238+
# Sync tool
239+
def calculate_sum(params):
240+
numbers = params.get("numbers", [])
241+
return sum(numbers)
242+
243+
# Async tool
244+
async def fetch_data(params):
245+
url = params.get("url")
246+
# Your async HTTP request logic
247+
return {"data": "fetched"}
248+
249+
client_tools.register("calculate_sum", calculate_sum, is_async=False)
250+
client_tools.register("fetch_data", fetch_data, is_async=True)
251+
```
252+
159253
## Languages Supported
160254

161255
Explore [all models & languages](https://elevenlabs.io/docs/models).

src/elevenlabs/conversational_ai/conversation.py

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,18 @@ class ClientTools:
155155
156156
Supports both synchronous and asynchronous tools running in a dedicated event loop,
157157
ensuring non-blocking operation of the main conversation thread.
158+
159+
Args:
160+
loop: Optional custom asyncio event loop to use for tool execution. If not provided,
161+
a new event loop will be created and run in a separate thread. Using a custom
162+
loop prevents "different event loop" runtime errors and allows for better
163+
context propagation and resource management.
158164
"""
159165

160-
def __init__(self) -> None:
166+
def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
161167
self.tools: Dict[str, Tuple[Union[Callable[[dict], Any], Callable[[dict], Awaitable[Any]]], bool]] = {}
162168
self.lock = threading.Lock()
169+
self._custom_loop = loop
163170
self._loop = None
164171
self._thread = None
165172
self._running = threading.Event()
@@ -170,27 +177,39 @@ def start(self):
170177
if self._running.is_set():
171178
return
172179

173-
def run_event_loop():
174-
self._loop = asyncio.new_event_loop()
175-
asyncio.set_event_loop(self._loop)
180+
if self._custom_loop is not None:
181+
# Use the provided custom event loop
182+
self._loop = self._custom_loop
176183
self._running.set()
177-
try:
178-
self._loop.run_forever()
179-
finally:
180-
self._running.clear()
181-
self._loop.close()
182-
self._loop = None
184+
else:
185+
# Create and run our own event loop in a separate thread
186+
def run_event_loop():
187+
self._loop = asyncio.new_event_loop()
188+
asyncio.set_event_loop(self._loop)
189+
self._running.set()
190+
try:
191+
self._loop.run_forever()
192+
finally:
193+
self._running.clear()
194+
self._loop.close()
195+
self._loop = None
183196

184-
self._thread = threading.Thread(target=run_event_loop, daemon=True, name="ClientTools-EventLoop")
185-
self._thread.start()
186-
# Wait for loop to be ready
187-
self._running.wait()
197+
self._thread = threading.Thread(target=run_event_loop, daemon=True, name="ClientTools-EventLoop")
198+
self._thread.start()
199+
# Wait for loop to be ready
200+
self._running.wait()
188201

189202
def stop(self):
190203
"""Gracefully stop the event loop and clean up resources."""
191204
if self._loop and self._running.is_set():
192-
self._loop.call_soon_threadsafe(self._loop.stop)
193-
self._thread.join()
205+
if self._custom_loop is not None:
206+
# For custom loops, we don't stop the loop itself, just clear our running flag
207+
self._running.clear()
208+
else:
209+
# For our own loop, stop it and join the thread
210+
self._loop.call_soon_threadsafe(self._loop.stop)
211+
if self._thread:
212+
self._thread.join()
194213
self.thread_pool.shutdown(wait=False)
195214

196215
def register(
@@ -257,7 +276,12 @@ async def _execute_and_callback():
257276
}
258277
callback(response)
259278

260-
asyncio.run_coroutine_threadsafe(_execute_and_callback(), self._loop)
279+
if self._custom_loop is not None:
280+
# For custom loops, schedule the task on the custom loop
281+
self._loop.create_task(_execute_and_callback())
282+
else:
283+
# For our own loop running in a separate thread, use run_coroutine_threadsafe
284+
asyncio.run_coroutine_threadsafe(_execute_and_callback(), self._loop)
261285

262286

263287
class ConversationInitiationData:

0 commit comments

Comments
 (0)