limited-pillow-7054
02/22/2026, 8:55 PMtall-airline-16756
02/22/2026, 8:55 PMtall-airline-16756
02/22/2026, 8:56 PMlimited-pillow-7054
02/22/2026, 8:59 PMtall-airline-16756
02/22/2026, 8:59 PMinstructions).
• Session-specific details (user info, job metadata, initial context) are added to the ChatContext at session start.
• During conversation, lifecycle hooks (for example on_user_turn_completed) let you inject or fetch additional context dynamically before the LLM generates a reply.
• Tools are defined and exposed to the agent (via decorators or tool definitions); the agent’s instructions explain when/how to call them.
Recommended prompt structure (modules you can combine)
• Persona prompt (base system): identity, tone, role, goals, and guardrails. Keep it short and authoritative.
• Tools usage prompt: explicit rules about when to call tools, required inputs, and how results should be formatted back to users. Place this with instructions or attach to each tool definition.
• Primary context (important session data): loaded at session start — user profile, active playbook ID, recent important events. Put the most critical facts here.
• Secondary/contextual augmentation (large or rarely-used info): do not preload long playbooks into the initial prompt. Fetch and inject only what is relevant per turn (RAG).
Best practices for handling many different prompts and lots of playbook/context
1. Modularize prompts
◦ Keep persona, tools, goals, and guardrails as separate sections in your system instructions (Markdown works well).
◦ This lets you swap or update the persona without touching RAG or tool logic.
1. Use job/room metadata for initial context
◦ Pass user-specific or session-specific fields (IDs, preferences, playbook references) via job or room metadata and load only essential fields into the ChatContext at start.
1. Offload big playbooks to a vector store and use RAG
◦ Store detailed playbooks in a vector DB. On each user turn, perform a targeted retrieval for the most relevant playbook excerpts and inject only those snippets into the turn via the lifecycle hook. This keeps token usage manageable and the prompt focused.
1. Inject context dynamically with lifecycle hooks
◦ Use on_user_turn_completed (or equivalent pipeline node) to:
◦ Run a vector search or DB lookup for playbook sections relevant to the latest utterance.
◦ Add a concise “Additional info” assistant message or a structured context block to the turn before generating the assistant’s reply.
1. Define clear tool contracts
◦ When exposing tools (APIs, DB lookups, etc.), define their input/output schemas and instruct the agent how to format calls and handle results. This avoids ambiguous or repeated retrievals.
1. Use retrieval + short context snippets, not full docs
◦ Return only the minimal, relevant excerpt plus a short summarizing instruction (e.g., “Use the following playbook excerpt for safety rules; do not quote the full text”). This reduces hallucinations and token costs.
1. Multi-persona and multi-tenant approaches
◦ Single-agent, dynamic persona: Load persona fragments based on the caller (from metadata) when the session starts.
◦ Multi-agent workflows: For complex pipelines, split responsibilities into agents (or workflow tasks) and hand off between them when specialized behavior is required.
1. Testing, guardrails, and observability
◦ Keep test cases for each persona and critical playbook path.
◦ Use the dashboard logs and session analytics to see which retrieved contexts were used and how often, then refine retrieval queries and prompt phrasing.
Practical prompt ordering when mixing types (recommended)
• System/persona (base rules and tone)
• Short user metadata (name, role — from job/room metadata)
• Immediate user request or question
• Injected RAG snippets (clearly labeled and minimal)
• Tool invocation guidance (if agent should call a tool for this task)
When to RAG vs. when to include direct context
• Use direct context when the info is small and crucial (e.g., user preferences, current case ID).
• Use RAG when the info set is large, frequently updated, or seldom needed in full (e.g., full playbooks, compliance manuals).
How LiveKit helps specifically
• Lifecycle hooks to inject per-turn context (so you avoid oversized initial prompts).
• Support for tools and explicit tool definitions so agents can call your retrieval or action endpoints reliably.
• Job/room metadata to pass session-scoped variables and playbook IDs at connection time.
• Observability (session logs, metrics) to iterate on prompt structure and retrieval relevance.
Next steps I can help with
• Walk through a concrete example: show a short persona + tool prompt and an on_user_turn_completed RAG flow.
• Provide a sample implementation (Python or Node) that: 1) initializes persona and metadata, 2) runs a vector search on user turns, and 3) injects retrieved snippets before response generation.
• Help design the prompt sections for one of your playbooks to optimize retrieval and clarity.
Which of those would be most helpful — a conceptual walkthrough, or a runnable code example in Python/Node?
Sources: Prompting guide | LiveKit Documentation | External data and RAG | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentation | Context Variables | LiveKit Documentation | Building the all-in-one platform for voice AI agentslimited-pillow-7054
02/22/2026, 9:06 PMtall-airline-16756
02/22/2026, 9:06 PMon_user_turn_completed RAG flow. Pick A or B — or ask for both and I’ll do A then the code example.
Below is the compact architecture + best practices so you can get going immediately.
1) Architecture (high level)
• Caller dials → SIP trunk → LiveKit telephony bridge → LiveKit Room.
• LiveKit Cloud Agent joins the room and processes audio: STT converts speech to text → LLM (with persona, context, and tool availability) decides next action → Tools may be invoked (CRM, calendar) → LLM response → TTS renders audio back to the caller.
• RAG is executed between STT and LLM: after STT returns text, run an internal retrieval (vector DB or KB) and inject the retrieved snippet into the chat context before LLM generation.
2) How the agent “perceives” prompts & context (voice pipeline specifics)
• System / persona instructions: set as the agent’s core instructions at creation. This controls tone, goals, and guardrails (the “salesy” voice).
• Session metadata: pass lead/session info (playbook id, user region, campaign) via job/room metadata so the agent has immediate, small critical facts.
• Turn-level dynamic injection (RAG): when the user finishes a turn, run retrieval based on the transcribed text, then add that snippet into the ChatContext via the lifecycle hook (e.g., on_user_turn_completed) before LLM replies. This is the key to keeping prompts small but contextually rich.
• Tools: expose function-style tools (booking, CRM update, discount check). The LLM will call them when instructed by its prompt rules.
3) Persona design for Lead Qualifier (prompt template)
• Keep persona concise, actionable, and with clear goals and constraints. Example sections:
◦ Identity + tone: “You are Alex, an energetic sales agent. Speak confidently and briefly.”
◦ Goals: “Qualify the lead (budget, timeline, use case), gather contact info, and attempt to book a demo.”
◦ Output rules: “Ask one question at a time. If qualified, offer 2 demo slots and confirm email/phone.”
◦ Guardrails: “Do not make pricing commitments beyond X; escalate complex objections to human.”
Example (short): "You are Alex, a confident, friendly sales agent for Acme. Goal: qualify leads quickly and book demos. Start with a short hook, ask targeted qualifying questions (budget, timeline, decision maker), and close with a demo booking. Use sales phrasing but never promise discounts unless verified. Keep replies under 20 seconds of speech."
4) Tools you should define (and how to prompt their usage)
• book_demo(date, time, email) — schedule demo in CRM/calendar.
• check_discount(product, region) — returns active promotions.
• update_lead(status, notes) — persists qualification results.
• escalate_to_human(reason) — trigger transfer/alert.
Best practice:
• Include explicit tool-use guidance inside `instructions`: when to call each tool and example input/output. Tools should have strict parameter schemas so the LLM can call them reliably.
5) RAG: where, when, and how to use it in a voice session
• When: Immediately after STT of the user’s utterance and before LLM generation for that turn.
• What to retrieve: short playbook snippets, objection-handling scripts, up-to-date pricing or promotion lines, competitor notes—only the parts relevant to the user’s utterance.
• How: Use a vector DB (or document DB) and run a focused similarity search using the STT text. Return top N small excerpts (or a short synthetic summary).
• Injection: Add retrieval result as a short assistant/internal message labeled like “[Internal Sales Guide]” or add a system instruction block so the LLM treats it as internal context but does not read it verbatim to the customer unless instructed.
• Token hygiene: limit length (e.g., 2–4 short snippets, or one 200–300 token summary). Ask the retriever to summarize long docs before injection.
Example injection pattern (pseudocode):
1. transcribed_text = STT(user_audio)
2. hits = vector_search(transcribed_text, playbooks)
3. rag_snippet = summarize(hits) # keep short
4. turn_ctx.add_message(role="assistant", content=f"[Internal Sales Guide] {rag_snippet}")
5. LLM.generate_reply(turn_ctx)
6) Flow for a single turn (practical)
1. Detect end of user turn (VAD or explicit silence).
2. STT → get text.
3. Run intent classifier (quick) to decide if immediate tool call or RAG needed.
4. If RAG needed, retrieve and inject snippets.
5. LLM runs with persona + injected context + tool definitions.
6. If LLM calls tool: execute tool, return tool result into context, allow LLM to craft final response.
7. TTS speak response back to caller.
8. Optionally persist summary to DB.
7) Example of “what to inject” for lead qualifier
• Top-level qualification summary: “Use these three quick questions: 1) What is your timeline? 2) Who’s the decision maker? 3) What’s your budget range?”
• Objection micro-scripts: brief counters for “too expensive”, “not now”, “we use competitor X”.
• Live promotions or demo slot suggestions pulled from CRM/calendar.
8) Handling long playbooks & many prompts
• Don’t preload whole playbook. Keep persona + key rules in the system prompt. Use RAG to fetch playbook bits on demand.
• Version control playbooks and ensure RAG index is updated whenever playbooks change (CI job or webhook).
• Label RAG inputs as internal guidance so the agent uses them for accuracy but doesn’t necessarily quote them directly.
9) SIP-specific and realtime considerations
• Minimize latency: keep retrieval and summarization fast (millisecond–second range). Cache frequently used snippets.
• Warm agents (paid plans): keep agents warm if you expect immediate answering to calls.
• Interruption & overlap: support short TTS responses and quick turn detection so callers can interrupt. Use short prompts and ask one question at a time to reduce overlap.
• Transfer/DTMF fallback: provide DTMF options if real-time voice recognition fails or the agent should hand off.
10) Observability & safety
• Log RAG retrieval IDs and tool calls so you can audit which playbook snippets influenced responses.
• Monitor session quality, call latency, and common fallback reasons to refine playbook retrieval or persona phrasing.
• Implement guardrails: block PII leakage, do not disclose restricted info, and set thresholds for automatic human handoff.
11) Quick checklist to implement (step-by-step)
1. Create agent with instructions for sales persona and tool descriptions.
2. Implement SIP trunk + dispatch rule to route calls into a LiveKit room.
3. Define tools (book_demo, update_lead, escalate_to_human) with strict schemas.
4. Set up vector DB for playbooks and an efficient retriever (and summarizer).
5. Hook on_user_turn_completed to run retrieval and inject snippet into ChatContext.
6. Implement STT→LLM→TTS pipeline and ensure tool execution returns are re-injected before final response.
7. Test flows: greeting, qualification Qs, objection handling, demo booking, escalation.
8. Monitor logs and iterate retrieval prompts and tool schemas.
If you want a runnable snippet, I can provide:
• A compact Python agent showing persona instructions, a book_demo tool, and an on_user_turn_completed that does vector search + injection, oriented to real-time voice.
• Or a Node example if you prefer.
Which would you like — the architecture checklist covered, or do you want the code example now (Python or Node)?
Sources: Gemini Realtime Agent with Live Vision | LiveKit Documentation | Amazon Nova Sonic integration guide | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentation | Tool definition and use | LiveKit Documentation | Agents telephony integration | LiveKit Documentationlimited-pillow-7054
02/22/2026, 9:13 PMtall-airline-16756
02/22/2026, 9:13 PMlimited-pillow-7054
02/22/2026, 9:25 PMtall-airline-16756
02/22/2026, 9:26 PMlimited-pillow-7054
02/22/2026, 9:40 PMtall-airline-16756
02/22/2026, 9:40 PMlimited-pillow-7054
02/22/2026, 9:41 PMtall-airline-16756
02/22/2026, 9:41 PMtext to TTS immediately (no LLM latency)
B. Compact JSON function schema (example) book_demo_schema = { "name": "book_demo", "description": "Schedule a demo", "parameters": { "type": "object", "properties": { "date": {"type":"string", "format":"date"}, "time": {"type":"string"}, "email": {"type":"string", "format":"email"}, "notes": {"type":"string"} }, "required": ["date","time","email"] } }
Gemini is asked to return JSON that matches this schema when it wants to book.
5) Additional latency tips specific to Gemini Flash 2.5 realtime
• Use streaming token output from Gemini and feed tokens to TTS progressively. This reduces perceived latency significantly.
• Keep injected RAG snippets very short (one 2–3 bullet summary). Precompute summaries for hot playbook sections.
• Co-locate retrieval and agent runtime to reduce network RTT. Put vector DB nodes in the same region/availability zone.
• Warm LLM connections (keep sessions alive) to avoid handshake/cold overhead.
6) Which to choose finally?
• If your priority is the absolute lowest latency and deterministic spoken text: primarily use server-side rendered Jinja2 for speech + compact JSON for tool IO. Use RAG as a short internal snippet injected before LLM generation.
• If you want maximum flexibility and fewer engineering steps: you canlimited-pillow-7054
02/22/2026, 9:59 PMtall-airline-16756
02/22/2026, 10:00 PM# PSEUDOCODE / TEMPLATE - adapt to your infra
def on_user_turn_end(call_audio, session_meta):
# 1) STT finalization (streaming STT recommended)
transcript = finalize_stt(call_audio) # fast streaming STT finalize
# 2) Quick intent check (cheap rules or tiny classifier)
if cheap_intent_is_simple(transcript):
need_rag = False
else:
need_rag = True
# 3) Fast RAG (precomputed embeddings, HNSW; memcached hot cache)
rag_snippet = None
if need_rag:
hits = vector_search(transcript, k=2) # ~10-80ms if colocated
rag_snippet = get_precomputed_summary(hits[0]) # <=150 tokens
# 4) Prepare messages for Gemini Flash 2.5 (streaming)
messages = [
{"role":"system", "content": SYSTEM_PROMPT}, # persona prompt above
{"role":"user", "content": transcript},
]
if rag_snippet:
messages.append({"role":"assistant", "content":"[InternalPlaybook] " + rag_snippet})
# 5) Call Gemini Flash 2.5 realtime (streaming). Provide function JSON schemas for tools.
llm_resp = call_gemini_realtime(messages, function_schemas=[BOOK_DEMO_SCHEMA, ...], stream=True)
# 6) If LLM issues a function call (book_demo), parse & validate JSON
if llm_resp.contains_function_call():
payload = parse_json(llm_resp.function_call.payload)
validate_against_schema(payload, BOOK_DEMO_SCHEMA)
tool_result = execute_book_demo(payload) # keep fast; local cache if possible
# 7) Inject tool result and ask LLM to confirm (quick)
messages.append({"role":"tool", "name":"book_demo", "content": json.dumps(tool_result)})
final_resp = call_gemini_realtime(messages, stream=True)
else:
final_resp = llm_resp
# 8) TTS streaming: start TTS as tokens stream from Gemini (progressive)
stream_tts_to_caller(final_resp.text)
Latency notes in code:
• Use streaming at every stage (STT partials, Gemini streaming tokens, TTS progressive audio).
• Precompute embeddings and summaries; keep vector DB colocated.
• Keep RAG snippet small and use quick intent classifier to avoid unnecessary retrievals.
6) Example persona + injection usage (end-to-end snippet)
• Start call: server-side render the Jinja2 hook and play via TTS immediately.
• After user replies: run STT → quick intent → RAG (if needed) → call Gemini (persona + injected RAG) → tool calls validated via JSON schema → confirm via TTS.
7) Practical tuning tips for <500ms
• Precompute embeddings and short summaries, store hot items in an in-memory cache.
• Co-locate vector DB and Gemini proxy to reduce RTT.
• Use K=1 or 2 for RAG and keep snippet under 150 tokens.
• Warm LLM connections and reuse sessions.
• Stream tokens to TTS to start playback before full LLM completion.
• Use optimistic server-side rendered fillers if a small extra fetch would exceed latency.
8) What I can deliver next (pick one)
• A runnable Python example wired to a specific STT/Vector/LLM SDK (I can produce concrete code for your chosen libs).
• A Node.js equivalent pipeline.
• Expanded Jinja2 template set (greeting, qualification questions, confirm booking) plus JSON schemas for other tools (check_discount, update_lead).
• A short checklist for production hardening (logging, observability, safety guardrails).
Which one would you like next? I can start with a runnable Python snippet wired to Gemini Flash 2.5 and a minimal in-memory vector mock (so you can test the whole flow end-to-end), or provide the Node.js version.limited-pillow-7054
02/22/2026, 10:02 PMtall-airline-16756
02/22/2026, 10:03 PMbook_demo.
2. If function call returned, validate JSON, execute booking, inject tool result and ask Gemini to confirm.
3. Stream TTS back to caller as Gemini emits tokens (start playing partial audio ASAP).
B) Python (concise pseudocode) Same flow as Node but showing Jinja2 render for greeting.
Pseudo:
• render = jinja2.Template(template).render(first_name="Alex", product="Acme CRM")
• tts.play(render) # immediate
• on_turn_end:
◦ transcript = gemini_final_stt()
◦ if need_rag: hits = vector_search(transcript)
◦ messages = [system_prompt, {"role":"user","content":transcript}]
◦ if hits: messages.append({"role":"assistant","content":"[InternalPlaybook] "+summary})
◦ resp = call_gemini_live(messages, functions=[book_demo_schema], stream=True)
◦ if resp.function_call: payload = validate(resp.payload); tool_result = execute(payload); inject tool result and confirm
◦ stream final text → TTS
6) Latency optimizations to meet <500 ms
• Precompute embeddings and short summaries; do not embed at query time.
• Use a colocated vector index (HNSW/Faiss) in memory for <50–120 ms retrieval.
• Cache hot playbook snippets per campaign/region.
• Use K=1 or 2 and limit injected tokens to ~100–200 tokens.
• Use Gemini LIVE streaming tokens → TTS pipeline; start TTS on partial tokens.
• Warm sessions and keep LLM connections alive.
• Run cheap intent classifier to avoid unnecessary RAG calls.
• Make tool calls synchronous but extremely fast (local cache/DB) or return optimistic confirmation then finalize.
7) Examples you can copy now
• Jinja2 snippet (Python)
from jinja2 import Template tpl = Template("Hi {{first_name or 'there'}}, this is Alex from Acme — quick question: are you evaluating solutions for {{product}}?") text = tpl.render(first_name="Sam", product="Acme CRM")
send text to TTS immediately
• Compact JSON (book_demo) — validate server-side before executing.
8) Next steps I can deliver immediately
• A runnable Node.js example using a mock in-memory vector index and pseudo Gemini LIVE client (so you can run locally end-to-end).
• A runnable Python example that wires Jinja2 greeting, mocked RAG, and a simulated Gemini LIVE session flow (useful if you prefer Python).
• Expanded template set (greeting + 3 qualification Qs + booking confirm + escalation) and JSON schemas for additional tools (check_discount, update_lead).
Which do you want next — a runnable Node.js sample or Python sample? I’ll provide the code and explain where to plug in Gemini LIVE SDK calls and your real vector DB.
Sources: Gemini Live API plugin | LiveKit Documentation | Gemini Realtime Agent with Live Vision | LiveKit Documentation | Prompting guide | LiveKit Documentation | Context Variables | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentationlimited-pillow-7054
02/22/2026, 10:08 PMtall-airline-16756
02/22/2026, 10:08 PMimport time
import json
from jinja2 import Environment, FileSystemLoader, select_autoescape
from typing import Optional, Dict, Any
# ===== Mock / Replace these with real implementations =====
def finalize_stt(call_audio: bytes) -> str:
"""
Replace with Gemini LIVE final STT (or LiveKit STT).
Here we simulate a transcript returned quickly.
"""
# Simulated transcript from caller
return "Yes, we are evaluating CRM solutions. Our timeline is three months and budget around 50k."
def cheap_intent_is_simple(transcript: str) -> bool:
"""
Fast rule-based decision to skip RAG when unnecessary.
"""
# If transcript contains clear booking intent or simple reply, skip RAG
tokens = transcript.lower()
if "book" in tokens or "demo" in tokens or "yes" in tokens and "demo" in tokens:
return False # still run RAG for details/confirmation in lead-qualifier
# default: allow RAG
return False
# Mock fast RAG: precomputed summaries for playbook chunks
PRECOMPUTED_RAG = {
"pricing": "No active discounts for region X. Standard starter package is $49/month per seat. Use close: 'limited seats available'.",
"objections_price": "If 'too expensive', ask about business impact and suggest ROI-focused demo. Offer two demo slots."
}
def fast_vector_search(query: str, k: int = 1) -> str:
"""
Replace with real HNSW vector search against precomputed embeddings.
Return a short (<=150 token) summary snippet.
"""
q = query.lower()
if "price" in q or "expensive" in q or "budget" in q:
return PRECOMPUTED_RAG["objections_price"]
return PRECOMPUTED_RAG["pricing"]
def call_gemini_live(messages: list, function_schemas: Optional[list] = None) -> Dict[str, Any]:
"""
Replace with real Gemini LIVE API call that handles STT->LLM->TTS streaming.
Here we simulate an LLM response that decides to book a demo by returning a function call JSON.
"""
# For demo we assume LLM decides to book a demo
function_call = {
"name": "book_demo",
"arguments": {
"date": "2026-03-03",
"time": "15:00",
"email": "<mailto:lead@example.com|lead@example.com>",
"phone": "+1234567890",
"notes": "Qualified: budget ~$50k, timeline 3 months"
}
}
# Simulate small processing latency
time.sleep(0.08)
return {"type": "function_call", "function_call": function_call, "text": None}
def execute_book_demo(payload: dict) -> dict:
"""
Replace with real CRM/calendar integration. Keep this fast (cache where possible).
"""
# Simulate booking latency
time.sleep(0.05)
return {"status": "ok", "confirmation_id": "CONF12345", "date": payload["date"], "time": payload["time"], "email": payload["email"]}
def stream_tts_to_caller(text: str):
"""
Replace with real TTS streaming (Gemini LIVE TTS or other) that starts as soon as tokens stream in.
"""
print("[TTS START] ", text)
# ===== End mock stubs =====
# Setup Jinja2 env
env = Environment(
loader=FileSystemLoader("templates"),
autoescape=select_autoescape(["jinja2", "html", "xml"])
)
SYSTEM_PROMPT = """You are Alex, an energetic, confident lead-qualifier for Acme. Goal: qualify inbound leads quickly and book demos. Speak concisely and persuasively.
Rules:
- Start with a 3–5 second hook and state purpose.
- Ask one qualifying question at a time: budget, timeline, decision maker.
- If qualified, propose two demo slots and confirm email/phone.
- Use [InternalPlaybook] snippets for policy/objection handling (internal guidance only - do not read verbatim unless asked).
- For actions (book_demo, update_lead, escalate_to_human) output exactly one JSON object matching the tool schema.
"""
BOOK_DEMO_SCHEMA = {
"name": "book_demo",
"description": "Schedule a demo",
"parameters": {
"type": "object",
"properties": {
"date": {"type": "string", "format": "date"},
"time": {"type": "string"},
"email": {"type": "string", "format": "email"},
"phone": {"type": "string"},
"notes": {"type": "string"}
},
"required": ["date", "time", "email"]
}
}
def validate_against_schema(payload: dict, schema: dict) -> bool:
"""
Replace with jsonschema.validate in production. Here we do minimal validation.
"""
for r in schema["parameters"].get("required", []):
if r not in payload:
return False
return True
# Main turn handler
def on_user_turn_end(call_audio: bytes, session_meta: dict):
start = time.time()
# 1) STT finalize
transcript = finalize_stt(call_audio)
stt_time = time.time()
# 2
import time
import json
from jinja2 import Environment, FileSystemLoader, select_autoescape
from typing import Optional, Dict, Any
# ===== Mock / Replace these with real implementations =====
def finalize_stt(call_audio: bytes) -> str:
"""
Replace with Gemini LIVE final STT (or LiveKit STT).
Here we simulate a transcript returned quickly.
"""
# Simulated transcript from caller
return "Yes, we are evaluating CRM solutions. Our timeline is three months and budget around 50k."
def cheap_intent_is_simple(transcript: str) -> bool:
"""
Fast rule-based decision to skip RAG when unnecessary.
"""
# If transcript contains clear booking intent or simple reply, skip RAG
tokens = transcript.lower()
if "book" in tokens or "demo" in tokens or "yes" in tokens and "demo" in tokens:
return False # still run RAG for details/confirmation in lead-qualifier
# default: allow RAG
return False
# Mock fast RAG: precomputed summaries for playbook chunks
PRECOMPUTED_RAG = {
"pricing": "No active discounts for region X. Standard starter package is $49/month per seat. Use close: 'limited seats available'.",
"objections_price": "If 'too expensive', ask about business impact and suggest ROI-focused demo. Offer two demo slots."
}
def fast_vector_search(query: str, k: int = 1) -> str:
"""
Replace with real HNSW vector search against precomputed embeddings.
Return a short (<=150 token) summary snippet.
"""
q = query.lower()
if "price" in q or "expensive" in q or "budget" in q:
return PRECOMPUTED_RAG["objections_price"]
return PRECOMPUTED_RAG["pricing"]
def call_gemini_live(messages: list, function_schemas: Optional[list] = None) -> Dict[str, Any]:
"""
Replace with real Gemini LIVE API call that handles STT->LLM->TTS streaming.
Here we simulate an LLM response that decides to book a demo by returning a function call JSON.
"""
# For demo we assume LLM decides to book a demo
function_call = {
"name": "book_demo",
"arguments": {
"date": "2026-03-03",
"time": "15:00",
"email": "lead@example.com",
"phone": "+1234567890",
"notes": "Qualified: budget ~$50k, timeline 3 months"
}
}
# Simulate small processing latency
time.sleep(0.08)
return {"type": "function_call", "function_call": function_call, "text": None}
def execute_book_demo(payload: dict) -> dict:
"""
Replace with real CRM/calendar integration. Keep this fast (cache where possible).
"""
# Simulate booking latency
time.sleep(0.05)
return {"status": "ok", "confirmation_id": "CONF12345", "date": payload["date"], "time": payload["time"], "email": payload["email"]}
def stream_tts_to_caller(text: str):
"""
Replace with real TTS streaming (Gemini LIVE TTS or other) that starts as soon as tokens stream in.
"""
print("[TTS START] ", text)
# ===== End mock stubs =====
# Setup Jinja2 env
env = Environment(
loader=FileSystemLoader("templates"),
autoescape=select_autoescape(["jinja2", "html", "xml"])
)
SYSTEM_PROMPT = """You are Alex, an energetic, confident lead-qualifier for Acme. Goal: qualify inbound leads quickly and book demos. Speak concisely and persuasively.
Rules:
- Start with a 3–5 second hook and state purpose.
- Ask one qualifying question at a time: budget, timeline, decision maker.
- If qualified, propose two demo slots and confirm email/phone.
- Use [InternalPlaybook] snippets for policy/objection handling (internal guidance only - do not read verbatim unless asked).
- For actions (book_demo, update_lead, escalate_to_human) output exactly one JSON object matching the tool schema.
"""
BOOK_DEMO_SCHEMA = {
"name": "book_demo",
"description": "Schedule a demo",
"parameters": {
"type": "object",
"properties": {
"date": {"type": "string", "format": "date"},
"time": {"type": "string"},
"email": {"type": "string", "format": "email"},
"phone": {"type": "string"},
"notes": {"type": "string"}
},
"required": ["date", "time", "email"]
}
}
def validate_against_schema(payload: dict, schema: dict) -> bool:
"""
Replace with jsonschema.validate in production. Here we do minimal validation.
"""
for r in schema["parameters"].get("required", []):
if r not in payload:
return False
return True
# Main turn handler
def on_user_turn_end(call_audio: bytes, session_meta: dict):
start = time.time()
# 1) STT finalize
transcript = finalize_stt(call_audio)
stt_time = time.time()
# 2