Skip to content

Commit 5eecbb6

Browse files
feat: Add voice agent SDK module and CLI template
Add comprehensive voice agent support to the Agentex SDK: ## New SDK Module (src/agentex/voice/) - VoiceAgentBase: Base class with state management, interruption handling, guardrail execution, and streaming support - AgentState/AgentResponse: Pydantic models for conversation state - Guardrail/LLMGuardrail: Abstract base classes for implementing guardrails ## New CLI Template (agentex init --voice) - Full project scaffolding for voice agents - Multi-provider LLM support (OpenAI, Azure, SGP, Vertex AI, Mock) - Docker + Kubernetes deployment configuration - Example guardrail structure ## CLI Changes - Added hidden --voice flag to 'agentex init' command - Generates voice-specific project structure ## Dependencies - Added partial-json-parser for streaming JSON parsing - Added google-auth for Vertex AI authentication
1 parent a277f10 commit 5eecbb6

File tree

20 files changed

+2579
-36
lines changed

20 files changed

+2579
-36
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ Brewfile.lock.json
1919
examples/**/uv.lock
2020

2121
# Claude workspace directories
22-
.claude-workspace/
22+
.claude-workspace/
23+
# Internal testing guide (not for PR)
24+
VOICE_AGENT_TESTING_GUIDE.md

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ dependencies = [
4848
"yaspin>=3.1.0",
4949
"claude-agent-sdk>=0.1.0",
5050
"anthropic>=0.40.0",
51+
"partial-json-parser>=0.2.1", # For voice agent streaming JSON parsing
52+
"google-auth>=2.0.0", # For Vertex AI authentication in voice agents
5153
]
5254

5355
requires-python = ">= 3.12,<4"

src/agentex/lib/cli/commands/init.py

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import annotations
22

33
from enum import Enum
4-
from typing import Any, Dict
4+
from typing import Any, Dict, Optional
55
from pathlib import Path
66

7+
import typer
78
import questionary
89
from jinja2 import Environment, FileSystemLoader
910
from rich.rule import Rule
@@ -27,6 +28,7 @@ class TemplateType(str, Enum):
2728
DEFAULT = "default"
2829
SYNC = "sync"
2930
SYNC_OPENAI_AGENTS = "sync-openai-agents"
31+
VOICE = "voice"
3032

3133

3234
def render_template(
@@ -60,6 +62,7 @@ def create_project_structure(
6062
TemplateType.DEFAULT: ["acp.py"],
6163
TemplateType.SYNC: ["acp.py"],
6264
TemplateType.SYNC_OPENAI_AGENTS: ["acp.py"],
65+
TemplateType.VOICE: ["acp.py"],
6366
}[template_type]
6467

6568
# Create project/code files
@@ -102,9 +105,15 @@ def get_project_context(answers: Dict[str, Any], project_path: Path, manifest_ro
102105
# Now, this is actually the exact same as the project_name because we changed the build root to be ../
103106
project_path_from_build_root = project_name
104107

108+
# Create PascalCase class name from agent name
109+
agent_class_name = "".join(
110+
word.capitalize() for word in answers["agent_name"].split("-")
111+
)
112+
105113
return {
106114
**answers,
107115
"project_name": project_name,
116+
"agent_class_name": agent_class_name,
108117
"workflow_class": "".join(
109118
word.capitalize() for word in answers["agent_name"].split("-")
110119
)
@@ -115,7 +124,14 @@ def get_project_context(answers: Dict[str, Any], project_path: Path, manifest_ro
115124
}
116125

117126

118-
def init():
127+
def init(
128+
voice: bool = typer.Option(
129+
False,
130+
"--voice",
131+
hidden=True,
132+
help="Create a voice agent template (LiveKit + Gemini)",
133+
),
134+
):
119135
"""Initialize a new agent project"""
120136
console.print(
121137
Panel.fit(
@@ -124,43 +140,48 @@ def init():
124140
)
125141
)
126142

127-
# Use a Rich table for template descriptions
128-
table = Table(show_header=True, header_style="bold blue")
129-
table.add_column("Template", style="cyan", no_wrap=True)
130-
table.add_column("Description", style="white")
131-
table.add_row(
132-
"[bold cyan]Async - ACP Only[/bold cyan]",
133-
"Asynchronous, non-blocking agent that can process multiple concurrent requests. Best for straightforward asynchronous agents that don't need durable execution. Good for asynchronous workflows, stateful applications, and multi-step analysis.",
134-
)
135-
table.add_row(
136-
"[bold cyan]Async - Temporal[/bold cyan]",
137-
"Asynchronous, non-blocking agent with durable execution for all steps. Best for production-grade agents that require complex multi-step tool calls, human-in-the-loop approvals, and long-running processes that require transactional reliability.",
138-
)
139-
table.add_row(
140-
"[bold cyan]Sync ACP[/bold cyan]",
141-
"Synchronous agent that processes one request per task with a simple request-response pattern. Best for low-latency use cases, FAQ bots, translation services, and data lookups.",
142-
)
143-
console.print()
144-
console.print(table)
145-
console.print()
143+
# If --voice flag is passed, skip the menu and use voice template
144+
if voice:
145+
console.print("[bold cyan]Creating Voice Agent template...[/bold cyan]\n")
146+
template_type = TemplateType.VOICE
147+
else:
148+
# Use a Rich table for template descriptions
149+
table = Table(show_header=True, header_style="bold blue")
150+
table.add_column("Template", style="cyan", no_wrap=True)
151+
table.add_column("Description", style="white")
152+
table.add_row(
153+
"[bold cyan]Async - ACP Only[/bold cyan]",
154+
"Asynchronous, non-blocking agent that can process multiple concurrent requests. Best for straightforward asynchronous agents that don't need durable execution. Good for asynchronous workflows, stateful applications, and multi-step analysis.",
155+
)
156+
table.add_row(
157+
"[bold cyan]Async - Temporal[/bold cyan]",
158+
"Asynchronous, non-blocking agent with durable execution for all steps. Best for production-grade agents that require complex multi-step tool calls, human-in-the-loop approvals, and long-running processes that require transactional reliability.",
159+
)
160+
table.add_row(
161+
"[bold cyan]Sync ACP[/bold cyan]",
162+
"Synchronous agent that processes one request per task with a simple request-response pattern. Best for low-latency use cases, FAQ bots, translation services, and data lookups.",
163+
)
164+
console.print()
165+
console.print(table)
166+
console.print()
167+
168+
# Gather project information
169+
template_type = questionary.select(
170+
"What type of template would you like to create?",
171+
choices=[
172+
{"name": "Async - ACP Only", "value": TemplateType.DEFAULT},
173+
{"name": "Async - Temporal", "value": "temporal_submenu"},
174+
{"name": "Sync ACP", "value": "sync_submenu"},
175+
],
176+
).ask()
146177

147178
def validate_agent_name(text: str) -> bool | str:
148179
"""Validate agent name follows required format"""
149180
is_valid = len(text) >= 1 and text.replace("-", "").isalnum() and text.islower()
150181
if not is_valid:
151182
return "Invalid name. Use only lowercase letters, numbers, and hyphens. Examples: 'my-agent', 'newsbot'"
152183
return True
153-
154-
# Gather project information
155-
template_type = questionary.select(
156-
"What type of template would you like to create?",
157-
choices=[
158-
{"name": "Async - ACP Only", "value": TemplateType.DEFAULT},
159-
{"name": "Async - Temporal", "value": "temporal_submenu"},
160-
{"name": "Sync ACP", "value": "sync_submenu"},
161-
],
162-
).ask()
163-
if not template_type:
184+
if template_type is None:
164185
return
165186

166187
# If Temporal was selected, show sub-menu for Temporal variants
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
eggs/
12+
.eggs/
13+
lib/
14+
lib64/
15+
parts/
16+
sdist/
17+
var/
18+
wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
23+
# Environments
24+
.env**
25+
.venv
26+
env/
27+
venv/
28+
ENV/
29+
env.bak/
30+
venv.bak/
31+
32+
# IDE
33+
.idea/
34+
.vscode/
35+
*.swp
36+
*.swo
37+
38+
# Git
39+
.git
40+
.gitignore
41+
42+
# Misc
43+
.DS_Store
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# syntax=docker/dockerfile:1.3
2+
FROM python:3.12-slim
3+
COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/
4+
5+
# Install system dependencies
6+
RUN apt-get update && apt-get install -y \
7+
htop \
8+
vim \
9+
curl \
10+
tar \
11+
python3-dev \
12+
postgresql-client \
13+
build-essential \
14+
libpq-dev \
15+
gcc \
16+
cmake \
17+
netcat-openbsd \
18+
nodejs \
19+
npm \
20+
&& apt-get clean \
21+
&& rm -rf /var/lib/apt/lists/**
22+
23+
RUN uv pip install --system --upgrade pip setuptools wheel
24+
25+
ENV UV_HTTP_TIMEOUT=1000
26+
27+
# Copy just the pyproject.toml file to optimize caching
28+
COPY {{ project_path_from_build_root }}/pyproject.toml /app/{{ project_path_from_build_root }}/pyproject.toml
29+
30+
WORKDIR /app/{{ project_path_from_build_root }}
31+
32+
# Install the required Python packages using uv
33+
RUN uv pip install --system .
34+
35+
# Copy the project code
36+
COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project
37+
38+
# Set environment variables
39+
ENV PYTHONPATH=/app
40+
41+
# Run the agent using uvicorn
42+
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# syntax=docker/dockerfile:1.3
2+
FROM python:3.12-slim
3+
COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/
4+
5+
# Install system dependencies
6+
RUN apt-get update && apt-get install -y \
7+
htop \
8+
vim \
9+
curl \
10+
tar \
11+
python3-dev \
12+
postgresql-client \
13+
build-essential \
14+
libpq-dev \
15+
gcc \
16+
cmake \
17+
netcat-openbsd \
18+
node \
19+
npm \
20+
&& apt-get clean \
21+
&& rm -rf /var/lib/apt/lists/*
22+
23+
RUN uv pip install --system --upgrade pip setuptools wheel
24+
25+
ENV UV_HTTP_TIMEOUT=1000
26+
27+
# Copy just the requirements file to optimize caching
28+
COPY {{ project_path_from_build_root }}/requirements.txt /app/{{ project_path_from_build_root }}/requirements.txt
29+
30+
WORKDIR /app/{{ project_path_from_build_root }}
31+
32+
# Install the required Python packages
33+
RUN uv pip install --system -r requirements.txt
34+
35+
# Copy the project code
36+
COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project
37+
38+
39+
# Set environment variables
40+
ENV PYTHONPATH=/app
41+
42+
# Run the agent using uvicorn
43+
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]

0 commit comments

Comments
 (0)