Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 51 additions & 10 deletions agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@

load_dotenv()

# Voice configuration mapping
VOICE_CONFIG = {
"FjJJxwBrv1I5sk34AdgP": { # Rayyan (Arabic)
"language": "ar",
"prompt_file": "system_prompt_ar.txt",
"greeting": "مرحباً، كيف يمكنني مساعدتك اليوم؟",
"date_prefix": "للعلم، التاريخ الحالي هو",
"date_format": "%A, %d %B, %Y" # Day before month for Arabic
},
"default": { # English voices
"language": "en",
"prompt_file": "system_prompt.txt",
"greeting": "Hi, how can I help you today?",
"date_prefix": "For your information, the current date is",
"date_format": "%A, %B %d, %Y" # Month before day for English
}
}

# Default voice
DEFAULT_VOICE_ID = "SAz9YHcvj6GT2YYXdXww" # River


def get_api_key_from_env():
"""
Expand All @@ -30,12 +51,20 @@ def get_api_key_from_env():
return None


def get_voice_config(voice_id: str) -> dict:
"""
Get the configuration for a specific voice ID.
Returns default config if voice ID not found.
"""
return VOICE_CONFIG.get(voice_id, VOICE_CONFIG["default"])


async def get_voice_from_participants(ctx: JobContext):
"""
Get the selected voice from participant metadata.
"""
if not ctx.room:
return "SAz9YHcvj6GT2YYXdXww" # Default voice: River
return DEFAULT_VOICE_ID

# Check current participants
all_participants = list(ctx.room.remote_participants.values())
Expand All @@ -51,7 +80,7 @@ async def get_voice_from_participants(ctx: JobContext):
except (json.JSONDecodeError, AttributeError) as e:
print(f"Error parsing participant metadata: {e}")

return "SAz9YHcvj6GT2YYXdXww" # Default voice: River
return DEFAULT_VOICE_ID


def prewarm(proc: JobProcess):
Expand Down Expand Up @@ -314,6 +343,9 @@ async def entrypoint(ctx: JobContext):
if not retrieved_groq_api_key:
print("No GROQ_API_KEY available from environment. Exiting.")
return

# Get ElevenLabs API key (try both common env var names)
elevenlabs_api_key = os.getenv("ELEVEN_API_KEY")

# Connect to the room
await ctx.connect()
Expand All @@ -324,6 +356,7 @@ async def entrypoint(ctx: JobContext):
start_event = asyncio.Event()
agent_ready_confirmed = asyncio.Event()
session_ref = {"session": None} # Use a dict to hold the session reference
pending_voice = {"voice_id": None} # Store voice changes before session is ready

def on_data_received(packet: DataPacket):
# participant is the sender. Should be a remote participant.
Expand All @@ -338,12 +371,14 @@ def on_data_received(packet: DataPacket):
elif payload.get("type") == "agent_ready_received":
agent_ready_confirmed.set()
elif payload.get("type") == "voice_change":
voice = payload.get("voice", "SAz9YHcvj6GT2YYXdXww") # Default: River
voice = payload.get("voice", DEFAULT_VOICE_ID)
pending_voice["voice_id"] = voice # Store for use when session is created
# Update the session's TTS voice if session exists
if session_ref["session"]:
try:
session_ref["session"]._tts = elevenlabs.TTS(
voice_id=voice,
api_key=elevenlabs_api_key,
model="eleven_flash_v2_5",
streaming_latency=1
)
Expand Down Expand Up @@ -382,16 +417,23 @@ def on_data_received(packet: DataPacket):
room=ctx.room
)

# Use pending voice if available, otherwise use selected voice
voice_to_use = pending_voice["voice_id"] or selected_voice

# Get voice configuration
config = get_voice_config(voice_to_use)

# Create the LiveKit session with custom LLM
session = AgentSession(
stt=groq.STT(
model="whisper-large-v3-turbo",
language="en",
language=config["language"],
api_key=retrieved_groq_api_key
),
llm=custom_llm,
tts=elevenlabs.TTS(
voice_id=selected_voice,
voice_id=voice_to_use,
api_key=elevenlabs_api_key,
model="eleven_flash_v2_5",
streaming_latency=1
),
Expand All @@ -402,19 +444,18 @@ def on_data_received(packet: DataPacket):
session_ref["session"] = session

# Start the session with the agent
base_prompt = load_system_prompt()
base_prompt = load_system_prompt(config["prompt_file"])
# Equip the agent with the current date. compound-mini supports a single tool call per request, so we can supply the current date to avoid wasting a tool call
current_date = datetime.datetime.now().strftime("%A, %B %d, %Y")
system_prompt = f"For your information, the current date is {current_date}.\n\n{base_prompt}"
current_date = datetime.datetime.now().strftime(config["date_format"])
system_prompt = f"{config['date_prefix']} {current_date}.\n\n{base_prompt}"

agent = GroqAgent(instructions=system_prompt)
await session.start(agent=agent, room=ctx.room)

time.sleep(0.2)

# Send initial greeting
initial_greeting = "Hi, how can I help you today?"
await session.say(initial_greeting)
await session.say(config["greeting"])

# Notify frontend that agent greeting is finished
await ctx.room.local_participant.publish_data(
Expand Down
36 changes: 36 additions & 0 deletions agent/system_prompt_ar.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
أنت مساعد صوتي مزود بأداة بحث قوية توفر إجابات مباشرة ومحادثة. يجب عليك استخدام قدرات البحث الخاصة بك بشكل استباقي لتقديم معلومات دقيقة ومحدثة مع المصادر.

لقد تم إعطاؤك التاريخ الحالي. عندما يُسأل عن التاريخ، استخدم هذه المعلومات.

قواعد الرد:
* حافظ على الردود أقل من 25 كلمة عندما يكون ذلك ممكناً
* لا تقم بتنسيق إجابتك بنقاط
* قم دائماً بالرد بجمل كاملة
* أعط الإجابة الرئيسية أولاً، ثم توقف
* لا تذكر أبداً "نتائج البحث" في إجابتك النهائية
* كن طبيعياً ومحادثاً
* اسأل المستخدم أحياناً أسئلة متابعة محددة لاستفساره. على سبيل المثال، إذا سأل المستخدم "من فاز في [مباراة رياضية]؟"، تابع أحياناً بـ "[إجابتك]. هل تريد معرفة الفريق الذي سيلعب ضد [الفائز] في المباراة القادمة؟"

البحث:
* **مهم: لديك أداة بحث مدمجة. استخدمها كلما طلب المستخدم معلومات أو مصادر أو استشهادات أو بيانات حالية**
* ابحث دائماً عندما يذكر المستخدم: "أخبرني عن"، "اذكر المصادر"، "ابحث عن معلومات"، "ما الذي يحدث مع"، "آخر أخبار"، "الحالي"، "الأخير"، "اليوم"، "أخبار عن"
* ابحث دائماً عن: نتائج الرياضة، الأحداث الجارية، المعلومات الحديثة، بيانات السوق، الطقس، الأخبار، الاتجاهات
* ابحث دائماً عندما يُطلب منك ذكر المصادر أو تقديم المراجع - ستوفر أداة البحث عناوين URL يمكنك مشاركتها
* لا تبحث عن: الرياضيات الأساسية، التعريفات، المعرفة العامة التي لا تتغير
* بعد البحث، أعط إجابة مباشرة دون ذكر البحث الذي تم إجراؤه

أمثلة على متى يجب البحث:
* "أخبرني عن تغير المناخ" → ابحث عن معلومات المناخ الحالية
* "ما الذي يحدث مع تسلا؟" → ابحث عن أخبار تسلا الأخيرة
* "اشرح الحوسبة الكمومية واذكر المصادر" → ابحث عن معلومات الحوسبة الكمومية مع المصادر
* "آخر التطورات في الذكاء الاصطناعي" → ابحث عن أخبار الذكاء الاصطناعي الأخيرة
* أي طلب للمصادر أو الاستشهادات أو المراجع → ابحث دائماً للحصول على عناوين URL

أسئلة البرمجة:
* لا تقرأ الكود بصوت عالٍ أبداً
* يمكنك شرح الكود دون قراءته صراحة

عام:
* قل فقط "لا أعرف" للأشياء التي لا يمكنك حقاً العثور عليها أو تحديدها
* لا تطلب التوضيح أبداً إلا إذا كان ذلك ضرورياً للغاية
* كن مفيداً ومباشراً
1 change: 1 addition & 0 deletions frontend/src/components/conversation/VoiceSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const VOICE_OPTIONS = [
{ id: "SAz9YHcvj6GT2YYXdXww", label: "River" },
{ id: "CwhRBWXzGAHq8TQ4Fs17", label: "Roger" },
{ id: "IKne3meq5aSn9XLyUdCD", label: "Charlie" },
{ id: "FjJJxwBrv1I5sk34AdgP", label: "Rayyan (Arabic)" },
];

export const VoiceSelector = ({ selectedVoice, onVoiceChange }: VoiceSelectorProps) => {
Expand Down