-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathvoice_agent.py
More file actions
262 lines (209 loc) · 8.97 KB
/
Copy pathvoice_agent.py
File metadata and controls
262 lines (209 loc) · 8.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
"""
Voice AI Agent
==============
Speak to Groq, get spoken responses back.
Uses:
- SpeechRecognition (mic → text)
- pyttsx3 (text → speech, offline, no API key)
- groq (Groq API — free tier)
Install:
pip install groq speechrecognition pyttsx3 pyaudio
Set your free Groq API key (https://console.groq.com/keys):
export GROQ_API_KEY=... # macOS / Linux
setx GROQ_API_KEY "..." # Windows
On macOS you may need: brew install portaudio
On Linux: sudo apt install portaudio19-dev python3-pyaudio espeak
"""
import io
import os
import sys
import groq
from groq import Groq
from dotenv import load_dotenv
import speech_recognition as sr
try:
import pyttsx3
_TTS_AVAILABLE = True
except ImportError:
_TTS_AVAILABLE = False
# Load GROQ_API_KEY (and any other vars) from a local .env file, if present.
load_dotenv()
# ─── Configuration ────────────────────────────────────────────────────────────
AGENT_INSTRUCTIONS = """
You are an EBT application assistant helping clients apply for food assistance benefits.
Keep every reply under 3 sentences — responses will be spoken aloud.
Never use bullet points, markdown, or special characters.
Speak naturally, like a conversation.
Do not ask for or repeat back sensitive personal information such as Social Security numbers, bank account details, or passwords.
If user asks to switch language, please return responses in that language.
"""
# Groq model — swap for "llama-3.1-8b-instant" (faster) or "openai/gpt-oss-20b", etc.
MODEL = "llama-3.3-70b-versatile"
SPEECH_RATE = 175 # words per minute (pyttsx3)
SPEECH_VOLUME = 1.0 # 0.0 – 1.0
# ─── Setup ────────────────────────────────────────────────────────────────────
client = None
recognizer = sr.Recognizer()
conversation_history = []
def get_client() -> Groq:
"""
Create and return the Groq client.
We do this lazily so voice_agent.py can be safely imported by the
Streamlit frontend. If the API key is missing, the frontend can show
a friendly error instead of the whole app exiting during import.
"""
global client
if client is not None:
return client
if not os.environ.get("GROQ_API_KEY"):
raise RuntimeError(
"GROQ_API_KEY is not set. Copy .env.example to .env and add your Groq API key."
)
client = Groq()
return client
def reset_conversation() -> None:
"""Clear the in-memory conversation history."""
conversation_history.clear()
# ─── Helpers ──────────────────────────────────────────────────────────────────
def speak(text: str) -> None:
"""Convert text to speech and play it (CLI only; not used by the web app)."""
print(f"\n🤖 Agent: {text}\n")
if not _TTS_AVAILABLE:
return
engine = pyttsx3.init()
engine.setProperty("rate", SPEECH_RATE)
engine.setProperty("volume", SPEECH_VOLUME)
engine.say(text)
engine.runAndWait()
engine.stop()
def listen(timeout: int = 8, phrase_limit: int = 15) -> str | None:
"""
Record from the default microphone and return transcribed text.
Returns None if nothing was heard or recognition failed.
"""
with sr.Microphone() as source:
print("🎙 Listening… (speak now)")
recognizer.adjust_for_ambient_noise(source, duration=0.5)
try:
audio = recognizer.listen(
source,
timeout=timeout,
phrase_time_limit=phrase_limit,
)
except sr.WaitTimeoutError:
print(" (no speech detected, try again)")
return None
print(" (processing…)")
try:
text = recognizer.recognize_google(audio)
print(f"👤 You: {text}")
return text
except sr.UnknownValueError:
print(" (could not understand audio)")
return None
except sr.RequestError as e:
print(f" (Google Speech API error: {e})")
return None
def transcribe_audio_bytes(audio_bytes: bytes) -> str | None:
"""
Transcribe browser-recorded audio from the Streamlit frontend.
Streamlit's st.audio_input gives us audio bytes. We wrap those bytes
in a file-like object and let SpeechRecognition process them as an
audio file.
"""
try:
with sr.AudioFile(io.BytesIO(audio_bytes)) as source:
audio = recognizer.record(source)
text = recognizer.recognize_google(audio)
return text
except sr.UnknownValueError:
return None
except sr.RequestError as e:
print(f"Google Speech API error: {e}")
return None
except Exception as e:
print(f"Audio transcription error: {e}")
return None
def chat(user_text: str) -> str:
"""Send user_text to Groq and return the assistant reply."""
groq_client = get_client()
conversation_history.append({"role": "user", "content": user_text})
response = groq_client.chat.completions.create(
model=MODEL,
max_tokens=512,
messages=[{"role": "system", "content": AGENT_INSTRUCTIONS.strip()}]
+ conversation_history,
)
reply = response.choices[0].message.content.strip()
conversation_history.append({"role": "assistant", "content": reply})
return reply
def update_instructions(new_instructions: str) -> None:
"""
Hot-swap the agent's instructions at runtime.
Clears conversation history so the new persona starts fresh.
"""
global AGENT_INSTRUCTIONS
AGENT_INSTRUCTIONS = new_instructions
conversation_history.clear()
print("\n✅ Instructions updated. Conversation reset.\n")
# ─── Main loop ────────────────────────────────────────────────────────────────
def run():
try:
get_client()
except RuntimeError as e:
print(e)
sys.exit(1)
print("=" * 55)
print(" Voice AI Agent — powered by Groq")
print("=" * 55)
print(" Say 'quit' or 'exit' to stop.")
print(" Say 'change instructions' to update the agent.")
print(" Current instructions:")
print(AGENT_INSTRUCTIONS.strip())
print("=" * 55)
speak(
"Hello! I'm your EBT application assistant, here to help you apply for food assistance benefits. "
"Please do not share sensitive personal information such as Social Security numbers, "
"bank account details, or passwords during our conversation. "
"You can request to switch language at any time."
"How can I help you today?"
)
while True:
user_input = listen()
if user_input is None:
continue
lower = user_input.lower().strip()
# ── Exit commands ──────────────────────────────────────────────────
if any(lower.startswith(w) for w in ("quit", "exit", "stop", "goodbye")):
speak("Goodbye! Have a great day.")
sys.exit(0)
# ── Update instructions at runtime ─────────────────────────────────
if "change instructions" in lower or "update instructions" in lower:
speak("Sure. Please type your new instructions in the terminal.")
print("\nEnter new instructions (blank line to finish):\n")
lines = []
while True:
line = input()
if line == "":
break
lines.append(line)
if lines:
update_instructions("\n".join(lines))
speak("Got it. Instructions updated. Let's continue.")
else:
speak("No changes made.")
continue
# ── Normal conversation ────────────────────────────────────────────
try:
reply = chat(user_input)
speak(reply)
except groq.APIError as e:
print(f" (API error: {e})")
speak("Sorry, I had trouble reaching the AI. Please try again.")
# ─── Entry point ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
# Optional: accept instructions as a CLI argument
# Usage: python voice_agent.py "You are a pirate assistant."
if len(sys.argv) > 1:
update_instructions(" ".join(sys.argv[1:]))
run()