Practice Negotiations with an AI Phone Agent Real-Time Roleplay with Telnyx
This article walks through building a 110-line Python app using Telnyx Call Control and AI Inference that lets anyone call a phone number and practice negotiations with an AI. It includes three scenarios (salary, sales deal, vendor contract), voice-driven conversation, and post-call scoring across five dimensions. The guide covers setup, code walkthrough, customization, and production considerations.
Imagine picking up your phone, dialing a number, and practicing a salary negotiation against an AI that plays the hiring manager — one that pushes back on your first offer, counters with budget constraints, and then scores your technique after you hang up. No scheduling a mock interview, no paying a coach, no awkward roleplay with a colleague.
This is the AI Negotiation Practice Phone — a 110-line Python app built with Telnyx Call Control and AI Inference. Three scenarios (salary, sales deal, vendor contract), voice-driven conversation, and a structured performance score delivered after every call. The AI stays in character, adapts to your approach, and gives you actionable feedback.
In this walkthrough, you'll build it from scratch. Clone the repo, configure a phone number, and start practicing in minutes.
What You'll Build
A phone number anyone can call to practice negotiations:
Caller dials in — Telnyx answers and offers a menu
Scenario selection — press 1 for salary negotiation, 2 for sales deal, 3 for vendor contract
AI opens — the AI plays the opposing role (hiring manager, enterprise buyer, or vendor account manager) and makes an opening position
Live negotiation — caller speaks naturally, AI responds in character, pushes back, counters, and adapts
Post-call scoring — on hangup, the AI scores the negotiation across 5 dimensions and returns structured JSON
Session history — every practice session is stored and accessible via GET /sessions
The whole interaction is voice-driven: Text-to-Speech reads the AI's lines, and the caller responds with natural speech. The model (Llama 3.3 70B via Telnyx AI Inference) handles both the real-time roleplay and the post-call evaluation.
Why This Is Interesting
Most negotiation training tools are text-based chatbots or static video courses. Neither captures the pressure of a live conversation — the pauses, the pushback, the moment you have to think on your feet. This one puts you on a real phone call with an AI that has a budget, a role, and a hidden constraint (like "max 15% discount" or "budget is $155K with flexibility to $165K").
The scoring system makes it more than a conversation. After you hang up, the AI evaluates your performance across five dimensions — anchoring, concession strategy, active listening, creativity, and confidence — plus an overall score and specific strengths/improvements. You can practice the same scenario five times and track whether your technique improves.
It also demonstrates the DTMF + speech pattern: the app uses DTMF (keypad input) for scenario selection and speech recognition for the negotiation itself. This two-input pattern shows up in many real IVR workflows.
Prerequisites
Python 3.8+
A Telnyx account with funded balance
A Telnyx API key
A Telnyx phone number with voice enabled
A Call Control Application configured with your webhook URL
ngrok for exposing your local server to Telnyx webhooks
The Architecture
Phone Call │ ▼ Telnyx Call Control (webhook events) │ ▼ Flask app (app.py, 110 lines) │ ├──► call.initiated → answer the call ├──► call.answered → TTS menu (press 1, 2, or 3) ├──► call.speak.ended → gather DTMF (scenario selection) ├──► call.gather.ended → DTMF → pick scenario → AI opening line │ └──► speech → AI inference → TTS response (negotiation loop) └──► call.hangup → score negotiation as JSON → store │ ▼ Telnyx AI Inference (Llama 3.3 70B) │ ▼ Structured score (JSON with 5 dimensions + feedback)
The app is a state machine with two phases: selection (DTMF menu) and negotiating (speech conversation). Each Telnyx webhook event drives the next action. After hangup, the full conversation is sent to the AI with a scoring prompt that returns structured JSON.
Step 1: Clone and Configure
git clone https://github.com/team-telnyx/telnyx-code-examples.git cd telnyx-code-examples/ai-negotiation-practice-phone-python cp .env.example .env pip install -r requirements.txt
Edit .env with your credentials:
TELNYX_API_KEY=your_telnyx_api_key_here TELNYX_PUBLIC_KEY=... AI_MODEL=meta-llama/Llama-3.3-70B-Instruct PRACTICE_NUMBER=+13105551234
Step 2: Understand the Code
Everything lives in app.py (110 lines). Here's what each piece does.
The Scenarios
The app ships with three built-in negotiation scenarios, each with a role and a hidden context:
SCENARIOS = { "1": {"role": "hiring manager", "context": "The candidate wants $180K. Your budget is $155K with flexibility to $165K. Push back on experience level. You can offer equity or signing bonus as alternatives."}, "2": {"role": "enterprise buyer", "context": "You're evaluating their SaaS product at $50K/year. You have a competing offer at $35K. Your budget is $45K. Ask for volume discounts and longer payment terms."}, "3": {"role": "vendor account manager", "context": "The client wants to reduce their contract by 40%. They're a top-10 account. You can offer 15% discount max, or restructure the deal with different terms."} }
The context is injected into the system prompt so the AI knows its constraints — but the caller doesn't know them. You discover the other side's budget and flexibility through the conversation, just like a real negotiation.
The State Machine: Selection → Negotiating
The webhook handler has two states. In select state, it gathers DTMF digits to pick a scenario:
if call["state"] == "select": client.calls.actions.gather(ccid, input_type="dtmf", timeout_secs=10, min_digits=1, max_digits=1)
When the caller presses 1, 2, or 3, the app loads the scenario, builds the system prompt, asks the AI for an opening line, and switches to negotiating state:
if call["state"] == "select": scenario = SCENARIOS.get(digits, SCENARIOS["1"]) call["state"] = "negotiating" call["scenario"] = scenario call["conversation"] = [{"role": "system", "content": f"You are a {scenario['role']} in a negotiation. {scenario['context']} Stay in character. Be firm but fair. Push back on their first offer. Keep responses under 2 sentences. After 6 exchanges, start wrapping up."}] opening = call_inference(call["conversation"] + [{"role": "user", "content": "The negotiation begins. Make your opening position."}]) call["conversation"].append({"role": "assistant", "content": opening}) client.calls.actions.speak(ccid, payload=opening, voice="female", language_code="en-US")
In negotiating state, it gathers speech and runs the conversation loop:
else: client.calls.actions.gather(ccid, input_type="speech", end_silence_timeout_secs=2, timeout_secs=20, language_code="en-US")
The conversation loop: speak → gather → infer → speak. Each turn, the AI sees the full conversation history, so it maintains context and remembers what you offered.
The Inference Helper
def call_inference(messages, max_tokens=200): resp = requests.post(INFERENCE_URL, headers={"Authorization": f"Bearer {TELNYX_API_KEY}", "Content-Type": "application/json"}, json={"model": AI_MODEL, "messages": messages, "max_tokens": max_tokens, "temperature": 0.7}, timeout=15) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"]
OpenAI-compatible chat completions call against Telnyx AI Inference. Temperature 0.7 — higher than the price quote agent (0.5) because you want the negotiator to be a bit more creative and unpredictable, not perfectly consistent every time.
Post-Call Scoring
When the caller hangs up, the app sends the full conversation to the AI with a scoring prompt:
elif event_type == "call.hangup": call = active_calls.pop(ccid, None) if call and len(call.get("conversation", [])) > 3: score_prompt = [{"role": "system", "content": "Score this negotiation practice. Return JSON: anchoring (1-10), concession_strategy (1-10), active_listening (1-10), creativity (1-10), confidence (1-10), overall (1-10), strengths (list), improvements (list), deal_outcome (string)."}, {"role": "user", "content": chr(10).join(f"{m['role']}: {m['content']}" for m in call["conversation"] if m["role"] != "system")}] try: score = json.loads(call_inference(score_prompt, max_tokens=400)) sessions.append({"scenario": call.get("scenario", {}).get("role"), "score": score, "duration": int(time.time() - call["start"])}) except Exception: pass
The AI returns a structured score with five dimensions, a list of strengths, a list of improvements, and the deal outcome. The app stores it alongside the scenario and call duration.
Accessing Sessions
@app.route("/sessions", methods=["GET"]) def list_sessions(): return jsonify({"sessions": sessions[-20:]}), 200
Hit GET /sessions to see the last 20 practice sessions as JSON — track your progress over time, compare scenarios, or pipe into an analytics dashboard.
Memory Cleanup
The app includes a background thread that cleans up stale call state:
def _start_ttl_cleanup(*stores, ttl_seconds=3600, interval=300): def _cleanup(): while True: _ttl_time.sleep(interval) cutoff = _ttl_time.time() - ttl_seconds for store in stores: expired = [k for k, v in store.items() if isinstance(v, dict) and v.get("_ts", _ttl_time.time()) .ngrok.app/webhooks/voice
Assign your Telnyx phone number to this Call Control Application if you haven't already.
Step 4: Call and Practice
Call your Telnyx number from any phone. You'll hear:
"Negotiation Practice! Press 1 for salary negotiation, 2 for sales deal, 3 for vendor contract."
Press 1. The AI (playing a hiring manager) opens with something like:
"Thanks for calling in. I've reviewed your experience, and I have to be honest — we typically bring in candidates at $155K for this level. I know you mentioned $180K, but given the experience gap, I'm not sure I can justify that range. What would make you worth the premium?"
Negotiate. Push back. Make your case. The AI will counter, bring up constraints, and eventually either reach a deal or hold firm.
After you hang up, check your score:
curl http://localhost:5000/sessions | python3 -m json.tool
You'll see a structured score object like:
{ "scenario": "hiring manager", "score": { "anchoring": 7, "concession_strategy": 5, "active_listening": 8, "creativity": 6, "confidence": 7, "overall": 6.6, "strengths": ["Strong opening anchor at $180K", "Asked about equity as alternative to base salary"], "improvements": ["Conceded too quickly on base salary", "Didn't explore signing bonus option"], "deal_outcome": "Settled at $162K base + $10K signing bonus" }, "duration": 184 }
Customizing the Scenarios
The SCENARIOS dict is the control surface. Small changes produce very different practice sessions:
Add a real estate negotiation:
"4": {"role": "seller's agent", "context": "The house is listed at $650K. The seller will accept $620K minimum. The buyer seems interested but price-sensitive. Push for close to asking but signal flexibility on closing dates."}
Make the AI more aggressive:
call["conversation"] = [{"role": "system", "content": f"You are a {scenario['role']} in a negotiation. {scenario['context']} Stay in character. Be aggressive and push hard on the first offer. Never accept the first counter. Keep responses under 2 sentences. After 6 exchanges, start wrapping up."}]
Add multi-round scoring with trend tracking:
After each session, compare to previous sessions in the same scenario
previous = [s for s in sessions if s.get("scenario") == call.get("scenario", {}).get("role")] if previous: last_score = previous[-1]["score"]["overall"] new_score = score["overall"] score["trend"] = "improving" if new_score > last_score else "declining" if new_score < last_score else "stable"
Send the score via SMS after the call:
from telnyx import Message score_text = f"Negotiation score: {score['overall']}/10. Strengths: {', '.join(score['strengths'][:2])}. Improvements: {', '.join(score['improvements'][:2])}." Message.create(to=call["caller"], from_=PRACTICE_NUMBER, text=score_text)
Going to Production
This example uses in-memory storage for simplicity. For a production deployment:
Database — replace the in-memory sessions list with Postg
[truncated for AI cost control]