Skip to content

Commit 7b67ddf

Browse files
committed
Add ExecutiveAssistant agent and Deepgram voice support
1 parent 5d2d616 commit 7b67ddf

File tree

11 files changed

+279
-0
lines changed

11 files changed

+279
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from __future__ import annotations
2+
3+
# mypy: ignore-errors
4+
5+
"""Executive Assistant agent.
6+
7+
This agent orchestrates other agents and provides voice capabilities using
8+
Deepgram STT and TTS models. It maintains short-term and long-term memory and
9+
can retrieve information via a simple RAG component.
10+
"""
11+
from agents import Agent # noqa: E402
12+
13+
from .memory import LongTermMemory, ShortTermMemory # noqa: E402
14+
from .rag import Retriever # noqa: E402
15+
from .tools import get_calendar_events, send_email # noqa: E402
16+
17+
18+
class ExecutiveAssistantState:
19+
"""Holds resources used by the Executive Assistant."""
20+
21+
def __init__(self, memory_path: str = "memory.json") -> None:
22+
self.short_memory = ShortTermMemory()
23+
self.long_memory = LongTermMemory(memory_path)
24+
self.retriever = Retriever()
25+
26+
27+
executive_assistant_agent = Agent(
28+
name="ExecutiveAssistant",
29+
instructions=(
30+
"You are an executive assistant. Use the available tools to help the user. "
31+
"Remember important facts during the conversation for later retrieval."
32+
),
33+
model="gpt-4o-mini",
34+
tools=[get_calendar_events, send_email],
35+
)
36+
37+
__all__ = ["ExecutiveAssistantState", "executive_assistant_agent"]

agents/executive_assistant/memory.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
from typing import Any
6+
7+
8+
class ShortTermMemory:
9+
"""In-memory store for conversation turns."""
10+
11+
def __init__(self) -> None:
12+
self._messages: list[dict[str, str]] = []
13+
14+
def add(self, role: str, content: str) -> None:
15+
"""Add a message to memory."""
16+
self._messages.append({"role": role, "content": content})
17+
18+
def to_list(self) -> list[dict[str, str]]:
19+
"""Return the last 20 messages."""
20+
return self._messages[-20:]
21+
22+
23+
class LongTermMemory:
24+
"""Simple file backed memory store."""
25+
26+
def __init__(self, path: str | Path) -> None:
27+
self._path = Path(path)
28+
if self._path.exists():
29+
self._data = json.loads(self._path.read_text())
30+
else:
31+
self._data = []
32+
33+
def add(self, item: Any) -> None:
34+
"""Persist an item to disk."""
35+
self._data.append(item)
36+
self._path.write_text(json.dumps(self._data))
37+
38+
def all(self) -> list[Any]:
39+
"""Return all persisted items."""
40+
return list(self._data)

agents/executive_assistant/rag.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterable
4+
5+
6+
class Retriever:
7+
"""Very small RAG retriever stub."""
8+
9+
def __init__(self, corpus: Iterable[str] | None = None) -> None:
10+
self._corpus = list(corpus or [])
11+
12+
def add(self, document: str) -> None:
13+
"""Add a document to the corpus."""
14+
self._corpus.append(document)
15+
16+
def search(self, query: str) -> list[str]:
17+
"""Return documents containing the query string."""
18+
return [doc for doc in self._corpus if query.lower() in doc.lower()]

agents/executive_assistant/tools.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from __future__ import annotations
2+
3+
from agents import function_tool
4+
5+
6+
@function_tool
7+
def get_calendar_events(date: str) -> str:
8+
"""Retrieve calendar events for a given date."""
9+
# TODO: Integrate with calendar API.
10+
return f"No events found for {date}."
11+
12+
13+
@function_tool
14+
def send_email(recipient: str, subject: str, body: str) -> str:
15+
"""Send a simple email."""
16+
# TODO: Integrate with email service.
17+
return "Email sent."

agents/pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[project]
2+
name = "custom-agents"
3+
version = "0.0.0"
4+
requires-python = ">=3.9"
5+
6+
[tool.hatch.build.targets.wheel]
7+
packages = ["executive_assistant"]

src/agents/voice/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
TTSVoice,
1111
VoiceModelProvider,
1212
)
13+
from .models.deepgram_model_provider import DeepgramVoiceModelProvider
14+
from .models.deepgram_stt import DeepgramSTTModel
15+
from .models.deepgram_tts import DeepgramTTSModel
1316
from .models.openai_model_provider import OpenAIVoiceModelProvider
1417
from .models.openai_stt import OpenAISTTModel, OpenAISTTTranscriptionSession
1518
from .models.openai_tts import OpenAITTSModel
@@ -38,6 +41,9 @@
3841
"OpenAIVoiceModelProvider",
3942
"OpenAISTTModel",
4043
"OpenAITTSModel",
44+
"DeepgramVoiceModelProvider",
45+
"DeepgramSTTModel",
46+
"DeepgramTTSModel",
4147
"VoiceStreamEventAudio",
4248
"VoiceStreamEventLifecycle",
4349
"VoiceStreamEvent",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from __future__ import annotations
2+
3+
import httpx # type: ignore
4+
5+
from ..model import STTModel, TTSModel, VoiceModelProvider
6+
from .deepgram_stt import DeepgramSTTModel
7+
from .deepgram_tts import DeepgramTTSModel
8+
9+
DEFAULT_STT_MODEL = "nova-3"
10+
DEFAULT_TTS_MODEL = "aura-2"
11+
12+
13+
class DeepgramVoiceModelProvider(VoiceModelProvider):
14+
"""Voice model provider for Deepgram APIs."""
15+
16+
def __init__(self, api_key: str, *, client: httpx.AsyncClient | None = None) -> None:
17+
self._api_key = api_key
18+
self._client = client
19+
20+
def _get_client(self) -> httpx.AsyncClient:
21+
if self._client is None:
22+
self._client = httpx.AsyncClient()
23+
return self._client
24+
25+
def get_stt_model(self, model_name: str | None) -> STTModel:
26+
return DeepgramSTTModel(
27+
model_name or DEFAULT_STT_MODEL, self._api_key, client=self._get_client()
28+
)
29+
30+
def get_tts_model(self, model_name: str | None) -> TTSModel:
31+
return DeepgramTTSModel(
32+
model_name or DEFAULT_TTS_MODEL, self._api_key, client=self._get_client()
33+
)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
import httpx # type: ignore
6+
7+
from ..input import AudioInput, StreamedAudioInput
8+
from ..model import StreamedTranscriptionSession, STTModel, STTModelSettings
9+
10+
11+
class DeepgramSTTModel(STTModel):
12+
"""Speech-to-text model using Deepgram Nova 3."""
13+
14+
def __init__(
15+
self, model: str, api_key: str, *, client: httpx.AsyncClient | None = None
16+
) -> None:
17+
self.model = model
18+
self.api_key = api_key
19+
self._client = client or httpx.AsyncClient()
20+
21+
@property
22+
def model_name(self) -> str:
23+
return self.model
24+
25+
async def transcribe(
26+
self,
27+
input: AudioInput,
28+
settings: STTModelSettings,
29+
trace_include_sensitive_data: bool,
30+
trace_include_sensitive_audio_data: bool,
31+
) -> str:
32+
url = f"https://api.deepgram.com/v1/listen?model={self.model}"
33+
headers = {"Authorization": f"Token {self.api_key}"}
34+
filename, data, content_type = input.to_audio_file()
35+
response = await self._client.post(url, headers=headers, content=data.getvalue())
36+
payload: dict[str, Any] = response.json()
37+
return (
38+
payload.get("results", {})
39+
.get("channels", [{}])[0]
40+
.get("alternatives", [{}])[0]
41+
.get("transcript", "")
42+
)
43+
44+
async def create_session(
45+
self,
46+
input: StreamedAudioInput,
47+
settings: STTModelSettings,
48+
trace_include_sensitive_data: bool,
49+
trace_include_sensitive_audio_data: bool,
50+
) -> StreamedTranscriptionSession:
51+
raise NotImplementedError("Streaming transcription is not implemented.")
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import AsyncIterator
4+
5+
import httpx # type: ignore
6+
7+
from ..model import TTSModel, TTSModelSettings
8+
9+
10+
class DeepgramTTSModel(TTSModel):
11+
"""Text-to-speech model using Deepgram Aura 2."""
12+
13+
def __init__(
14+
self, model: str, api_key: str, *, client: httpx.AsyncClient | None = None
15+
) -> None:
16+
self.model = model
17+
self.api_key = api_key
18+
self._client = client or httpx.AsyncClient()
19+
20+
@property
21+
def model_name(self) -> str:
22+
return self.model
23+
24+
async def run(self, text: str, settings: TTSModelSettings) -> AsyncIterator[bytes]:
25+
url = "https://api.deepgram.com/v1/speak"
26+
headers = {"Authorization": f"Token {self.api_key}", "Content-Type": "application/json"}
27+
payload = {"text": text, "model": self.model, "voice": settings.voice or "aura-2"}
28+
response = await self._client.post(url, headers=headers, json=payload)
29+
yield response.content
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from agents import Agent, Runner
6+
from agents.agent import ToolsToFinalOutputResult
7+
from agents.executive_assistant import executive_assistant_agent
8+
from tests.fake_model import FakeModel
9+
10+
11+
@pytest.mark.asyncio
12+
async def test_agent_runs_with_fake_model() -> None:
13+
model = FakeModel()
14+
agent = Agent(
15+
name=executive_assistant_agent.name,
16+
instructions=executive_assistant_agent.instructions,
17+
tools=executive_assistant_agent.tools,
18+
model=model,
19+
)
20+
model.set_next_output(
21+
[
22+
{"role": "assistant", "content": "Hello"},
23+
]
24+
)
25+
26+
result: ToolsToFinalOutputResult = await Runner.run(agent, "hi")
27+
assert result.final_output == "Hello"

tests/voice/test_deepgram_models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from agents.voice import DeepgramVoiceModelProvider
6+
7+
8+
@pytest.mark.asyncio
9+
async def test_provider_returns_models() -> None:
10+
provider = DeepgramVoiceModelProvider(api_key="key")
11+
stt = provider.get_stt_model(None)
12+
tts = provider.get_tts_model(None)
13+
assert stt.model_name == "nova-3"
14+
assert tts.model_name == "aura-2"

0 commit comments

Comments
 (0)