Build a Sales Experiment Optimizer Agent

Build a Python AI agent that turns sales call data into experiment recommendations.

Introduction

30 Second Summary

Every sales team makes dozens of calls a day, but few know which specific techniques actually move the needle. The difference between a great quarter and a missed target often hides in the patterns across hundreds of conversations.

In this project, you will build a Python-based AI agent that processes a batch of sales call transcripts, classifies the techniques each rep used, correlates those techniques with win rates, and generates a report recommending which experiments to run next.

What You'll Build

A multi-step sales experiment optimizer agent that turns raw call data into actionable experiment recommendations using OpenAI's Chat Completions API and pandas for data analysis.

By the end of this project, you'll have:

  • A technique classifier that uses gpt-4o-mini to identify opening style, discovery approach, objection handling, value proposition, and closing technique for each call.
  • A correlation engine that calculates win rates by technique across dimensions like time of day, location, and deal size.
  • An experiment recommender that identifies statistically meaningful patterns and suggests A/B tests with projected ROI.
  • Secret Mission: Add audio ingestion with speaker diarization to process raw sales call recordings end-to-end.

Are there any prerequisites?

You should have basic Python knowledge and familiarity with APIs and JSON. No prior experience with AI agents or sales analytics is required.

Set Up Your Environment and OpenAI Account

Before you can build your sales experiment optimizer, you need access to OpenAI's API. The agent sends call transcripts to gpt-4o-mini for classification, so a working API key is the first requirement.

In this step, you will create an OpenAI account, configure your API key, install the Python packages the project depends on, and set up sample sales call data to test against.

In this step, get ready to:
  • Create an OpenAI account and set up your API key.
  • Install project dependencies and create your project folder.
  • Add sample sales call data for testing.
Create your OpenAI account and API key

Your agent needs an API key to authenticate with OpenAI's servers. You will create an account, generate a key, and store it as an environment variable so the SDK can find it automatically.

  • Open your browser and go to platform.openai.com.
  • Click Sign up and create a new account (or log in if you already have one).
  • Once logged in, click API keys in the left sidebar.
  • Click Create new secret key.
  • Copy the key immediately and save it somewhere safe (you will not be able to see it again).

Save your key now

OpenAI only shows your full API key once at creation time. Copy it to a secure note or password manager before closing the dialog.

Now set the key as an environment variable so the OpenAI SDK can read it automatically.

macOS

  • Open your terminal and add the key to your shell profile by running this command:
echo 'export OPENAI_API_KEY=your-api-key-here' >> ~/.zshrc
  • Replace your-api-key-here with the key you just copied.
  • Apply the change by running:
source ~/.zshrc

Windows

  • Open PowerShell and set the environment variable permanently by running this command:
setx OPENAI_API_KEY "your-api-key-here"
  • Replace your-api-key-here with the key you just copied.
  • Close and reopen PowerShell for the variable to take effect.

Why set an environment variable?

The OpenAI Python SDK automatically reads from the OPENAI_API_KEY environment variable. Setting it once means you never have to paste the key into your code.

Finally, add prepaid credits so the API accepts your requests.

  • In the OpenAI dashboard, click Settings in the left sidebar.
  • Select Billing.
  • Click Add to balance and add at least $5 in prepaid credits.

How much will this project cost?

The gpt-4o-mini model costs $0.15 per 1M input tokens and $0.60 per 1M output tokens. For 10 sample calls, the entire classification and recommendation pipeline costs well under $0.10.

Prepaid credits do not expire for 1 year.

Create your project folder and install dependencies

Your project needs three Python packages: the OpenAI SDK for API calls, pandas for data analysis, and python-dotenv for loading environment variables from a file.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir sales-optimizer
cd sales-optimizer
  • Install the project dependencies by running:
pip install openai pandas python-dotenv

Seeing a permission error?

If pip fails with a permission error, try adding --user to the install command. This installs packages to your user directory instead of the system directory.

help me fix a pip permission error when installing packages

Now create a .env file as a fallback for your API key. This ensures the key is available even if your system environment variable is not loaded.

  • Open VS Code in your sales-optimizer folder by running:
code .
  • Create a new file called .env in the root of your sales-optimizer folder.
  • Add the following line, replacing the placeholder with your actual API key:
OPENAI_API_KEY=your-api-key-here

What does this file do?

The python-dotenv package reads key-value pairs from .env and loads them into your environment at runtime. Your code calls load_dotenv() at the start, making the key available to the OpenAI client.

  • Save .env.
  • Confirm the file exists by running:
ls

You should see .env listed in the output.

Add sample sales call data

The agent analyzes batches of sales call transcripts. You will create a JSON file with 10 sample calls, each containing the transcript dialogue, outcome (won or lost), deal value, and contextual metadata.

  • In VS Code, create a new file called sample_calls.json in your sales-optimizer folder.
  • Paste the following sample data into the file:
[
  {
    "call_id": "call_001",
    "transcript": "Rep: Hey Sarah, thanks for taking the time today. I saw your team just closed a Series B, congrats! How has scaling the team been going?\nProspect: Thanks! Yeah it's been hectic. We're hiring fast but our onboarding is a mess.\nRep: I hear that a lot from fast-growing teams. What does your onboarding process look like right now?\nProspect: Honestly, it's a Google Doc that nobody updates. New hires take 3 weeks to get productive.\nRep: That's a big cost at your stage. If I told you our clients cut that to 5 days on average, would that be worth exploring?\nProspect: Definitely. What would that look like?\nRep: I can put together a quick pilot plan for your next 5 hires. If the results speak for themselves, we can talk about rolling it out company-wide. Can I send that over by Thursday?\nProspect: Yeah, send it over. I'll review with my co-founder.",
    "outcome": "won",
    "deal_value": 24000,
    "time_of_day": "morning",
    "location": "US-West",
    "rep_name": "Alex",
    "call_duration_minutes": 18
  },
  {
    "call_id": "call_002",
    "transcript": "Rep: Hi Mark, I'm calling from Acme Solutions. We help SaaS companies reduce churn by 30%. Do you have a moment?\nProspect: I'm kind of busy, what's this about?\nRep: I'll be quick. We have a new AI retention tool that predicts which customers will churn 60 days out. Are you seeing churn issues?\nProspect: Everyone has churn. What makes you different?\nRep: Our model has 94% accuracy. We integrate in one day. Here are three features that set us apart: predictive scoring, automated outreach, and health dashboards.\nProspect: Sounds interesting but I need to think about it.\nRep: I understand. Can I follow up next Tuesday?\nProspect: Sure, email me.",
    "outcome": "lost",
    "deal_value": 36000,
    "time_of_day": "afternoon",
    "location": "US-East",
    "rep_name": "Jordan",
    "call_duration_minutes": 8
  },
  {
    "call_id": "call_003",
    "transcript": "Rep: Hi Lisa, Mike from your board suggested I reach out. He mentioned you're looking at revamping your analytics stack.\nProspect: Oh Mike sent you? Yeah we've been struggling with our current setup.\nRep: What's the biggest pain point with your current analytics?\nProspect: It takes our team 2 days to pull a report that should take 2 hours. And the data is never fresh.\nRep: That's exactly the problem we solve. Our clients typically go from 2-day reports to real-time dashboards. What would it mean for your team to have instant access to fresh data?\nProspect: It would be transformative honestly. We're making decisions on stale data right now.\nRep: Based on what you're describing, I think our mid-tier plan would fit. It's $3K/month and pays for itself in the first week based on the time savings alone. Should I set up a technical deep-dive with your data team?\nProspect: Yes, let's do that. Can you do next Wednesday?",
    "outcome": "won",
    "deal_value": 36000,
    "time_of_day": "morning",
    "location": "US-East",
    "rep_name": "Alex",
    "call_duration_minutes": 22
  },
  {
    "call_id": "call_004",
    "transcript": "Rep: Good afternoon David. I wanted to follow up on the proposal I sent last week. Have you had a chance to review it?\nProspect: I looked at it briefly. The price is higher than what we're paying now.\nRep: I appreciate you being direct. Can you help me understand what you're comparing it to?\nProspect: We're paying $1500/month for our current tool. Your proposal is $4000.\nRep: That makes sense. Let me ask this: what's the cost of the problems your current tool isn't solving? You mentioned last time that you're losing about 5 deals a month to slow response times.\nProspect: That's true, but I'm not sure I can justify tripling our spend to my CFO.\nRep: What if we structured it as a 90-day pilot at a reduced rate? That way you can prove the ROI before committing to the full price.\nProspect: That could work. What would the pilot cost?\nRep: $2500/month for 90 days, with a success metric we agree on upfront. If you hit the metric, you roll into the full plan. If not, you walk away. Fair?\nProspect: Let me talk to my CFO and get back to you by Friday.",
    "outcome": "won",
    "deal_value": 48000,
    "time_of_day": "afternoon",
    "location": "US-West",
    "rep_name": "Sam",
    "call_duration_minutes": 25
  },
  {
    "call_id": "call_005",
    "transcript": "Rep: Hi Jennifer, thanks for booking this call. I see from your form submission that you're interested in our enterprise plan. What prompted you to reach out?\nProspect: We've been evaluating tools for the last month. Yours came up in a few reviews.\nRep: Great. What are the top 3 things you're looking for in a solution?\nProspect: Integration with Salesforce, real-time alerts, and the ability to customize workflows.\nRep: We do all three natively. Let me ask: what happens today when a deal goes dark in your pipeline?\nProspect: Honestly, it falls through the cracks. Reps forget to follow up.\nRep: That's the exact problem our alert system solves. In fact, companies like yours see a 25% increase in pipeline velocity within 60 days. Based on your pipeline size, that could mean an extra $200K in closed revenue this quarter alone.\nProspect: Those are big numbers. How do I know it'll work for us?\nRep: Fair question. Let me show you a case study from a company your size in the same industry. Then if it resonates, we can start a free 14-day trial. No commitment. Sound good?\nProspect: Yeah, send me the case study and trial link.",
    "outcome": "won",
    "deal_value": 72000,
    "time_of_day": "morning",
    "location": "US-East",
    "rep_name": "Jordan",
    "call_duration_minutes": 20
  },
  {
    "call_id": "call_006",
    "transcript": "Rep: Hey Tom, quick call today. I know you're busy so I'll cut to the chase. We launched a new feature last week that I think is perfect for your team.\nProspect: What feature?\nRep: Automated pipeline scoring. It ranks your deals by likelihood to close so your team focuses on the right ones.\nProspect: We already use our CRM's built-in scoring.\nRep: How accurate do you find it?\nProspect: It's okay. Maybe 60% accurate.\nRep: Ours hits 89% accuracy because it factors in engagement signals, not just deal stage. Here's what I'd suggest: let me set you up with a free comparison. We score your current pipeline alongside your CRM and you see which is more accurate.\nProspect: I don't have time for another evaluation right now. Maybe next quarter.\nRep: Totally understand. Can I check back in January?\nProspect: Sure.",
    "outcome": "lost",
    "deal_value": 18000,
    "time_of_day": "afternoon",
    "location": "US-West",
    "rep_name": "Alex",
    "call_duration_minutes": 7
  },
  {
    "call_id": "call_007",
    "transcript": "Rep: Good morning Rachel. I noticed your company was featured in TechCrunch last week for your expansion into EMEA. Congratulations!\nProspect: Thanks! Yeah, it's been a big push for us.\nRep: With that kind of growth, I'm curious: how are you handling cross-timezone collaboration for your sales team?\nProspect: It's a nightmare honestly. Our US team and UK team are stepping on each other's toes.\nRep: I've seen that pattern a lot with companies expanding internationally. The ones that solve it fastest usually centralize their deal intelligence so everyone sees the same real-time picture regardless of timezone. Is that something you've explored?\nProspect: We've talked about it but haven't found the right tool.\nRep: Here's what I'd propose: let me map out your current workflow, identify the specific friction points, and show you how two of our clients in similar situations solved it. If the approach makes sense, we can talk about a pilot. If not, at minimum you'll have a clear picture of where the bottlenecks are. Worth 30 minutes next week?\nProspect: Actually yes, that would be really helpful. Book me for Tuesday.",
    "outcome": "won",
    "deal_value": 60000,
    "time_of_day": "morning",
    "location": "US-East",
    "rep_name": "Sam",
    "call_duration_minutes": 15
  },
  {
    "call_id": "call_008",
    "transcript": "Rep: Hi Kevin, I'm following up on our conversation from the conference. You mentioned your team struggles with forecast accuracy.\nProspect: Yeah, we're off by 30-40% every quarter. It's embarrassing.\nRep: That's actually common. Most teams rely on rep gut feel. What if you could get within 5% accuracy consistently?\nProspect: I'd love that but I'm skeptical.\nRep: Fair. Let me share some data: our platform analyzes call sentiment, email engagement, and deal velocity to predict outcomes. Here are the top 5 features...\nProspect: Look, I've heard pitches like this before. They never work as advertised.\nRep: I totally get the skepticism. Rather than take my word for it, would you be open to a blind test? We'll analyze your last quarter's deals retroactively and show you what our model would have predicted vs. what actually happened. Zero risk, and you'll know immediately if it's real or marketing fluff.\nProspect: Okay, that's actually a smart approach. How do we set that up?\nRep: I'll send you a secure link to upload last quarter's CRM export. We'll have results within 48 hours. Deal?\nProspect: Deal.",
    "outcome": "won",
    "deal_value": 42000,
    "time_of_day": "afternoon",
    "location": "US-West",
    "rep_name": "Jordan",
    "call_duration_minutes": 19
  },
  {
    "call_id": "call_009",
    "transcript": "Rep: Hi Amanda. I wanted to check in because your trial ends in 3 days. How has the experience been?\nProspect: It's been fine. The product works but I'm not sure we need it long-term.\nRep: What would need to be true for it to be a must-have?\nProspect: Honestly, I'd need to see a clearer ROI. My boss asks me every week what tools we're paying for and why.\nRep: Makes sense. Let me pull your usage data from the trial. In the last 11 days, your team saved approximately 6 hours per week on reporting. At your average hourly rate, that's about $4800/month in time savings against a $1200/month subscription. Would that kind of data help your conversation with your boss?\nProspect: Actually yes, can you put that in a one-pager I can forward?\nRep: Absolutely. I'll have it in your inbox by end of day. And if your boss has questions, I'm happy to jump on a quick call with both of you.\nProspect: Perfect, send it over.",
    "outcome": "won",
    "deal_value": 14400,
    "time_of_day": "morning",
    "location": "US-West",
    "rep_name": "Sam",
    "call_duration_minutes": 12
  },
  {
    "call_id": "call_010",
    "transcript": "Rep: Hey Brian, I saw you downloaded our whitepaper on sales automation. Wanted to see if you had any questions.\nProspect: I was just doing research. We're not really in the market right now.\nRep: No problem at all. Out of curiosity, what prompted the research?\nProspect: Our VP mentioned wanting to automate some stuff next year.\nRep: Got it. When you say next year, is that Q1 or more like mid-year?\nProspect: Probably Q2. We have budget then.\nRep: Perfect. Here's what I'd suggest: let me send you a 3-minute video overview so when the conversation comes up with your VP, you have something concrete to reference. No obligation, just ammunition for when the timing is right.\nProspect: Sure, send it.\nRep: Will do. I'll also set a reminder to check in with you in February to see if the timeline has firmed up. Sound fair?\nProspect: Yeah that works.",
    "outcome": "lost",
    "deal_value": 30000,
    "time_of_day": "afternoon",
    "location": "US-East",
    "rep_name": "Alex",
    "call_duration_minutes": 9
  }
]

What does this data contain?

Each of the 10 records represents one sales call with metadata the agent will use for analysis:

  • transcript: the full rep and prospect dialogue.
  • outcome: whether the deal was won or lost.
  • deal_value: the dollar amount of the potential deal.
  • time_of_day, location: contextual dimensions the correlation engine uses to find patterns.
  • rep_name and call_duration_minutes: additional metadata for deeper analysis.

The data is designed with clear patterns (e.g., morning calls with rapport openings win more often) so the correlation engine produces meaningful results.

  • Save sample_calls.json.
  • Confirm both files exist by running:
ls

You should see .env and sample_calls.json listed in the output.

Files not showing up?

Make sure your terminal is inside the sales-optimizer folder. Run pwd to check your current directory.

help me find my project files in the terminal

✔️ Awesome, I've got everything!

Great. Double check you have saved both files in your sales-optimizer folder.

ⓧ I'd like to double check the full code

Here is what your .env file should look like (with your real key in place of the placeholder):

OPENAI_API_KEY=your-api-key-here

And here is the full sample_calls.json:

[
  {
    "call_id": "call_001",
    "transcript": "Rep: Hey Sarah, thanks for taking the time today. I saw your team just closed a Series B, congrats! How has scaling the team been going?\nProspect: Thanks! Yeah it's been hectic. We're hiring fast but our onboarding is a mess.\nRep: I hear that a lot from fast-growing teams. What does your onboarding process look like right now?\nProspect: Honestly, it's a Google Doc that nobody updates. New hires take 3 weeks to get productive.\nRep: That's a big cost at your stage. If I told you our clients cut that to 5 days on average, would that be worth exploring?\nProspect: Definitely. What would that look like?\nRep: I can put together a quick pilot plan for your next 5 hires. If the results speak for themselves, we can talk about rolling it out company-wide. Can I send that over by Thursday?\nProspect: Yeah, send it over. I'll review with my co-founder.",
    "outcome": "won",
    "deal_value": 24000,
    "time_of_day": "morning",
    "location": "US-West",
    "rep_name": "Alex",
    "call_duration_minutes": 18
  },
  {
    "call_id": "call_002",
    "transcript": "Rep: Hi Mark, I'm calling from Acme Solutions. We help SaaS companies reduce churn by 30%. Do you have a moment?\nProspect: I'm kind of busy, what's this about?\nRep: I'll be quick. We have a new AI retention tool that predicts which customers will churn 60 days out. Are you seeing churn issues?\nProspect: Everyone has churn. What makes you different?\nRep: Our model has 94% accuracy. We integrate in one day. Here are three features that set us apart: predictive scoring, automated outreach, and health dashboards.\nProspect: Sounds interesting but I need to think about it.\nRep: I understand. Can I follow up next Tuesday?\nProspect: Sure, email me.",
    "outcome": "lost",
    "deal_value": 36000,
    "time_of_day": "afternoon",
    "location": "US-East",
    "rep_name": "Jordan",
    "call_duration_minutes": 8
  },
  {
    "call_id": "call_003",
    "transcript": "Rep: Hi Lisa, Mike from your board suggested I reach out. He mentioned you're looking at revamping your analytics stack.\nProspect: Oh Mike sent you? Yeah we've been struggling with our current setup.\nRep: What's the biggest pain point with your current analytics?\nProspect: It takes our team 2 days to pull a report that should take 2 hours. And the data is never fresh.\nRep: That's exactly the problem we solve. Our clients typically go from 2-day reports to real-time dashboards. What would it mean for your team to have instant access to fresh data?\nProspect: It would be transformative honestly. We're making decisions on stale data right now.\nRep: Based on what you're describing, I think our mid-tier plan would fit. It's $3K/month and pays for itself in the first week based on the time savings alone. Should I set up a technical deep-dive with your data team?\nProspect: Yes, let's do that. Can you do next Wednesday?",
    "outcome": "won",
    "deal_value": 36000,
    "time_of_day": "morning",
    "location": "US-East",
    "rep_name": "Alex",
    "call_duration_minutes": 22
  },
  {
    "call_id": "call_004",
    "transcript": "Rep: Good afternoon David. I wanted to follow up on the proposal I sent last week. Have you had a chance to review it?\nProspect: I looked at it briefly. The price is higher than what we're paying now.\nRep: I appreciate you being direct. Can you help me understand what you're comparing it to?\nProspect: We're paying $1500/month for our current tool. Your proposal is $4000.\nRep: That makes sense. Let me ask this: what's the cost of the problems your current tool isn't solving? You mentioned last time that you're losing about 5 deals a month to slow response times.\nProspect: That's true, but I'm not sure I can justify tripling our spend to my CFO.\nRep: What if we structured it as a 90-day pilot at a reduced rate? That way you can prove the ROI before committing to the full price.\nProspect: That could work. What would the pilot cost?\nRep: $2500/month for 90 days, with a success metric we agree on upfront. If you hit the metric, you roll into the full plan. If not, you walk away. Fair?\nProspect: Let me talk to my CFO and get back to you by Friday.",
    "outcome": "won",
    "deal_value": 48000,
    "time_of_day": "afternoon",
    "location": "US-West",
    "rep_name": "Sam",
    "call_duration_minutes": 25
  },
  {
    "call_id": "call_005",
    "transcript": "Rep: Hi Jennifer, thanks for booking this call. I see from your form submission that you're interested in our enterprise plan. What prompted you to reach out?\nProspect: We've been evaluating tools for the last month. Yours came up in a few reviews.\nRep: Great. What are the top 3 things you're looking for in a solution?\nProspect: Integration with Salesforce, real-time alerts, and the ability to customize workflows.\nRep: We do all three natively. Let me ask: what happens today when a deal goes dark in your pipeline?\nProspect: Honestly, it falls through the cracks. Reps forget to follow up.\nRep: That's the exact problem our alert system solves. In fact, companies like yours see a 25% increase in pipeline velocity within 60 days. Based on your pipeline size, that could mean an extra $200K in closed revenue this quarter alone.\nProspect: Those are big numbers. How do I know it'll work for us?\nRep: Fair question. Let me show you a case study from a company your size in the same industry. Then if it resonates, we can start a free 14-day trial. No commitment. Sound good?\nProspect: Yeah, send me the case study and trial link.",
    "outcome": "won",
    "deal_value": 72000,
    "time_of_day": "morning",
    "location": "US-East",
    "rep_name": "Jordan",
    "call_duration_minutes": 20
  },
  {
    "call_id": "call_006",
    "transcript": "Rep: Hey Tom, quick call today. I know you're busy so I'll cut to the chase. We launched a new feature last week that I think is perfect for your team.\nProspect: What feature?\nRep: Automated pipeline scoring. It ranks your deals by likelihood to close so your team focuses on the right ones.\nProspect: We already use our CRM's built-in scoring.\nRep: How accurate do you find it?\nProspect: It's okay. Maybe 60% accurate.\nRep: Ours hits 89% accuracy because it factors in engagement signals, not just deal stage. Here's what I'd suggest: let me set you up with a free comparison. We score your current pipeline alongside your CRM and you see which is more accurate.\nProspect: I don't have time for another evaluation right now. Maybe next quarter.\nRep: Totally understand. Can I check back in January?\nProspect: Sure.",
    "outcome": "lost",
    "deal_value": 18000,
    "time_of_day": "afternoon",
    "location": "US-West",
    "rep_name": "Alex",
    "call_duration_minutes": 7
  },
  {
    "call_id": "call_007",
    "transcript": "Rep: Good morning Rachel. I noticed your company was featured in TechCrunch last week for your expansion into EMEA. Congratulations!\nProspect: Thanks! Yeah, it's been a big push for us.\nRep: With that kind of growth, I'm curious: how are you handling cross-timezone collaboration for your sales team?\nProspect: It's a nightmare honestly. Our US team and UK team are stepping on each other's toes.\nRep: I've seen that pattern a lot with companies expanding internationally. The ones that solve it fastest usually centralize their deal intelligence so everyone sees the same real-time picture regardless of timezone. Is that something you've explored?\nProspect: We've talked about it but haven't found the right tool.\nRep: Here's what I'd propose: let me map out your current workflow, identify the specific friction points, and show you how two of our clients in similar situations solved it. If the approach makes sense, we can talk about a pilot. If not, at minimum you'll have a clear picture of where the bottlenecks are. Worth 30 minutes next week?\nProspect: Actually yes, that would be really helpful. Book me for Tuesday.",
    "outcome": "won",
    "deal_value": 60000,
    "time_of_day": "morning",
    "location": "US-East",
    "rep_name": "Sam",
    "call_duration_minutes": 15
  },
  {
    "call_id": "call_008",
    "transcript": "Rep: Hi Kevin, I'm following up on our conversation from the conference. You mentioned your team struggles with forecast accuracy.\nProspect: Yeah, we're off by 30-40% every quarter. It's embarrassing.\nRep: That's actually common. Most teams rely on rep gut feel. What if you could get within 5% accuracy consistently?\nProspect: I'd love that but I'm skeptical.\nRep: Fair. Let me share some data: our platform analyzes call sentiment, email engagement, and deal velocity to predict outcomes. Here are the top 5 features...\nProspect: Look, I've heard pitches like this before. They never work as advertised.\nRep: I totally get the skepticism. Rather than take my word for it, would you be open to a blind test? We'll analyze your last quarter's deals retroactively and show you what our model would have predicted vs. what actually happened. Zero risk, and you'll know immediately if it's real or marketing fluff.\nProspect: Okay, that's actually a smart approach. How do we set that up?\nRep: I'll send you a secure link to upload last quarter's CRM export. We'll have results within 48 hours. Deal?\nProspect: Deal.",
    "outcome": "won",
    "deal_value": 42000,
    "time_of_day": "afternoon",
    "location": "US-West",
    "rep_name": "Jordan",
    "call_duration_minutes": 19
  },
  {
    "call_id": "call_009",
    "transcript": "Rep: Hi Amanda. I wanted to check in because your trial ends in 3 days. How has the experience been?\nProspect: It's been fine. The product works but I'm not sure we need it long-term.\nRep: What would need to be true for it to be a must-have?\nProspect: Honestly, I'd need to see a clearer ROI. My boss asks me every week what tools we're paying for and why.\nRep: Makes sense. Let me pull your usage data from the trial. In the last 11 days, your team saved approximately 6 hours per week on reporting. At your average hourly rate, that's about $4800/month in time savings against a $1200/month subscription. Would that kind of data help your conversation with your boss?\nProspect: Actually yes, can you put that in a one-pager I can forward?\nRep: Absolutely. I'll have it in your inbox by end of day. And if your boss has questions, I'm happy to jump on a quick call with both of you.\nProspect: Perfect, send it over.",
    "outcome": "won",
    "deal_value": 14400,
    "time_of_day": "morning",
    "location": "US-West",
    "rep_name": "Sam",
    "call_duration_minutes": 12
  },
  {
    "call_id": "call_010",
    "transcript": "Rep: Hey Brian, I saw you downloaded our whitepaper on sales automation. Wanted to see if you had any questions.\nProspect: I was just doing research. We're not really in the market right now.\nRep: No problem at all. Out of curiosity, what prompted the research?\nProspect: Our VP mentioned wanting to automate some stuff next year.\nRep: Got it. When you say next year, is that Q1 or more like mid-year?\nProspect: Probably Q2. We have budget then.\nRep: Perfect. Here's what I'd suggest: let me send you a 3-minute video overview so when the conversation comes up with your VP, you have something concrete to reference. No obligation, just ammunition for when the timing is right.\nProspect: Sure, send it.\nRep: Will do. I'll also set a reminder to check in with you in February to see if the timeline has firmed up. Sound fair?\nProspect: Yeah that works.",
    "outcome": "lost",
    "deal_value": 30000,
    "time_of_day": "afternoon",
    "location": "US-East",
    "rep_name": "Alex",
    "call_duration_minutes": 9
  }
]

Your environment is ready. Next up, you will build the technique classifier that sends each call transcript to gpt-4o-mini and extracts structured sales methodology data.

Build the Technique Classifier Agent

Your environment is set up and your sample data is ready. Now it's time to build the core intelligence of your agent.

The first module you'll create is a technique classifier. It reads each sales call transcript and uses OpenAI's Chat Completions API to identify which sales methodology the rep used at each stage of the conversation.

In this step, get ready to:
  • Build a prompt that classifies sales calls across 5 technique dimensions.
  • Create a batch processing function that enriches all calls with technique data.
  • Run the classifier on your sample data and verify the results.
Create the classifier file and prompt

Your classifier needs two things: a detailed prompt that tells the AI exactly how to categorize sales techniques, and a function that sends each transcript to the model and parses the structured response.

  • Create a new file called classifier.py in your project folder.
  • Add the imports, client initialization, and classification prompt by pasting this code:
import json
import time
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

client = OpenAI()

CLASSIFICATION_PROMPT = """You are a sales methodology expert. Analyze the following sales call transcript and classify the techniques used by the sales rep.

Return a JSON object with these exact fields:
- opening_style: one of "rapport", "direct", "referral", "question-led"
- discovery_approach: one of "open-ended", "SPIN", "challenger", "consultative"
- objection_handling: one of "acknowledge-redirect", "feel-felt-found", "reframe", "evidence-based", "none" (if no objection occurred)
- value_proposition: one of "feature-led", "outcome-led", "ROI-focused", "story-based"
- closing_technique: one of "assumptive", "summary", "urgency", "next-step", "trial"

Only return the JSON object, no other text.

Transcript:
{transcript}"""

What does this code do?

load_dotenv() reads your .env file so the OPENAI_API_KEY is available as an environment variable. OpenAI() automatically picks up that key and creates a client you can use for API calls.

The CLASSIFICATION_PROMPT is a template string that tells the model exactly which categories to use for each of the 5 technique dimensions. The {transcript} placeholder gets filled in with the actual call text later.

  • Below the prompt, add the classify_call() function that sends a single transcript to the model:
def classify_call(transcript: str) -> dict:
    """Classify a single call transcript into sales techniques."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "You are a sales methodology analyst. Always respond with valid JSON only.",
            },
            {
                "role": "user",
                "content": CLASSIFICATION_PROMPT.format(transcript=transcript),
            },
        ],
        response_format={"type": "json_object"},
        temperature=0.1,
    )
    result = json.loads(response.choices[0].message.content)
    return result

What does this function do?

This function sends the transcript to gpt-4o-mini with two messages: a system message that primes the model as a JSON-only responder, and a user message containing the filled-in classification prompt.

response_format={"type": "json_object"} forces the model to output valid JSON. temperature=0.1 keeps the classifications consistent across runs. The function parses the JSON string into a Python dictionary and returns it.

Why gpt-4o-mini?

At $0.15 per 1M input tokens and $0.60 per 1M output tokens, gpt-4o-mini is extremely cost-effective for classification tasks. Processing all 10 sample calls costs well under $0.01 total.

It supports structured JSON output and performs excellently on extraction and classification tasks where you define clear categories.

  • Save classifier.py.

You won't see output yet. The next chunk adds batch processing so you can run the classifier across all your calls.

Seeing an error about the API key?

If you see an error mentioning that the API key is not set, make sure your .env file contains OPENAI_API_KEY=your-api-key-here with your actual key pasted in place of the placeholder.

Also confirm .env is in the same folder as classifier.py. load_dotenv() looks for .env in the current working directory.

help me fix my OpenAI API key error in classifier.py

Add batch processing

A single-call classifier is useful, but your agent needs to process all 10 calls in one run. The batch function handles iteration, progress feedback, and a short delay between API calls to avoid rate limits.

  • In classifier.py, add the classify_batch() function below classify_call():
def classify_batch(calls: list) -> list:
    """Classify all calls in a batch and return enriched records."""
    enriched_calls = []
    total = len(calls)

    for i, call in enumerate(calls):
        print(f"Classifying call {i + 1}/{total}: {call['call_id']}...")
        techniques = classify_call(call["transcript"])
        enriched_call = {**call, "techniques": techniques}
        enriched_calls.append(enriched_call)
        if i < total - 1:
            time.sleep(0.5)

    return enriched_calls

What does this function do?

The function loops through every call record, sends its transcript to classify_call(), and merges the returned technique classifications back into the original record using {**call, "techniques": techniques}.

time.sleep(0.5) adds a half-second pause between API calls. This avoids triggering rate limits when processing larger batches. The progress indicator prints which call is being processed so you can watch it work.

  • Save classifier.py.

You won't see output yet. The next chunk adds the script entry point that ties everything together.

Test the classifier on your sample data

Now you need an entry point that loads your sample calls, runs the batch classifier, and saves the enriched results to a file.

  • At the bottom of classifier.py, add the main execution block:
if __name__ == "__main__":
    with open("sample_calls.json", "r") as f:
        calls = json.load(f)

    enriched = classify_batch(calls)

    with open("enriched_calls.json", "w") as f:
        json.dump(enriched, f, indent=2)

    print("\nClassification complete! Results saved to enriched_calls.json")
    print("\nSample result:")
    print(json.dumps(enriched[0]["techniques"], indent=2))

What does this block do?

The if __name__ == "__main__": guard means this code only runs when you execute the file directly. It loads your 10 sample calls, classifies all of them, saves the full enriched dataset to enriched_calls.json, and prints the first call's technique breakdown so you can verify it looks right.

  • Save classifier.py.
  • Run the classifier by executing this command in your terminal:
python classifier.py

You should see progress messages printing as each call is classified, followed by a sample result showing the JSON technique breakdown for the first call.

What should I see?

The output shows each call being processed ("Classifying call 1/10: call_001...") with a brief pause between each. After all 10 finish, you'll see a JSON object like:

{
  "opening_style": "rapport",
  "discovery_approach": "consultative",
  "objection_handling": "none",
  "value_proposition": "outcome-led",
  "closing_technique": "next-step"
}

Your exact values may differ slightly. The model interprets each transcript independently, so classifications are reasonable estimates rather than exact answers.

Seeing a rate limit or authentication error?

If you see an authentication error, confirm your API key is correct in your .env file and that you have prepaid credits added to your OpenAI account.

If you see a rate limit error, increase the sleep time in classify_batch() from 0.5 to 2 seconds and try again.

help me debug my classifier.py errors when calling the OpenAI API

  • Confirm that enriched_calls.json was created in your project folder by running:
ls

You should see enriched_calls.json listed alongside classifier.py and sample_calls.json.

✔️ Awesome, I've got everything!

Great. Make sure you've saved classifier.py and that enriched_calls.json exists in your project folder.

ⓧ I'd like to double check the full code

Here is the complete classifier.py file:

import json
import time
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

client = OpenAI()

CLASSIFICATION_PROMPT = """You are a sales methodology expert. Analyze the following sales call transcript and classify the techniques used by the sales rep.

Return a JSON object with these exact fields:
- opening_style: one of "rapport", "direct", "referral", "question-led"
- discovery_approach: one of "open-ended", "SPIN", "challenger", "consultative"
- objection_handling: one of "acknowledge-redirect", "feel-felt-found", "reframe", "evidence-based", "none" (if no objection occurred)
- value_proposition: one of "feature-led", "outcome-led", "ROI-focused", "story-based"
- closing_technique: one of "assumptive", "summary", "urgency", "next-step", "trial"

Only return the JSON object, no other text.

Transcript:
{transcript}"""


def classify_call(transcript: str) -> dict:
    """Classify a single call transcript into sales techniques."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "You are a sales methodology analyst. Always respond with valid JSON only.",
            },
            {
                "role": "user",
                "content": CLASSIFICATION_PROMPT.format(transcript=transcript),
            },
        ],
        response_format={"type": "json_object"},
        temperature=0.1,
    )
    result = json.loads(response.choices[0].message.content)
    return result


def classify_batch(calls: list) -> list:
    """Classify all calls in a batch and return enriched records."""
    enriched_calls = []
    total = len(calls)

    for i, call in enumerate(calls):
        print(f"Classifying call {i + 1}/{total}: {call['call_id']}...")
        techniques = classify_call(call["transcript"])
        enriched_call = {**call, "techniques": techniques}
        enriched_calls.append(enriched_call)
        if i < total - 1:
            time.sleep(0.5)

    return enriched_calls


if __name__ == "__main__":
    with open("sample_calls.json", "r") as f:
        calls = json.load(f)

    enriched = classify_batch(calls)

    with open("enriched_calls.json", "w") as f:
        json.dump(enriched, f, indent=2)

    print("\nClassification complete! Results saved to enriched_calls.json")
    print("\nSample result:")
    print(json.dumps(enriched[0]["techniques"], indent=2))

Your technique classifier is working. Next up, you'll build a correlation engine that finds patterns between these techniques and call outcomes.

Build the Correlation Engine

Your classifier has labeled every call with the exact techniques each rep used. You now have structured data showing which opening style, discovery approach, objection handling, value proposition, and closing technique appeared in each conversation.

The next question is: which of those techniques actually win deals? In this step, you'll build a correlation engine using pandas that calculates win rates across every technique dimension and cross-references them with contextual variables like time of day, location, and deal size.

In this step, get ready to:
  • Load enriched call data into a pandas DataFrame and calculate win rates by technique.
  • Add multi-dimensional correlation that cross-references techniques with time, location, and deal size.
  • Generate a complete correlation summary and run the analysis end-to-end.
Load enriched call data and calculate win rates

The classifier saved your enriched data as a flat JSON file. To run numerical analysis on it, you need to reshape it into a tabular format that pandas can group and aggregate.

  • Create a new file called correlator.py in your project folder.
  • Add the imports and data-loading function by pasting this code:
import json
import pandas as pd


def load_enriched_calls(filepath: str = "enriched_calls.json") -> pd.DataFrame:
    """Load enriched call data into a pandas DataFrame."""
    with open(filepath, "r") as f:
        calls = json.load(f)

    rows = []
    for call in calls:
        row = {
            "call_id": call["call_id"],
            "outcome": call["outcome"],
            "deal_value": call["deal_value"],
            "time_of_day": call["time_of_day"],
            "location": call["location"],
            "rep_name": call["rep_name"],
            "call_duration_minutes": call["call_duration_minutes"],
            "opening_style": call["techniques"]["opening_style"],
            "discovery_approach": call["techniques"]["discovery_approach"],
            "objection_handling": call["techniques"]["objection_handling"],
            "value_proposition": call["techniques"]["value_proposition"],
            "closing_technique": call["techniques"]["closing_technique"],
        }
        rows.append(row)

    return pd.DataFrame(rows)

What does this code do?

The load_enriched_calls() function reads your enriched JSON file and flattens each call record into a single row. The nested techniques dictionary gets unpacked so each technique dimension becomes its own column.

This flat structure is what lets pandas group and aggregate efficiently. Each row is one call with all its metadata and technique labels in separate columns.

Now you need a function that answers the core question: what is the win rate for each technique? This groups calls by technique value and calculates how often each one leads to a win.

  • Add the calculate_win_rates() function below your load_enriched_calls() function:
def calculate_win_rates(df: pd.DataFrame) -> dict:
    """Calculate win rates grouped by each technique dimension."""
    df["won"] = (df["outcome"] == "won").astype(int)
    technique_cols = [
        "opening_style",
        "discovery_approach",
        "objection_handling",
        "value_proposition",
        "closing_technique",
    ]

    win_rates = {}
    for col in technique_cols:
        grouped = df.groupby(col).agg(
            total_calls=("won", "count"),
            wins=("won", "sum"),
            avg_deal_value=("deal_value", "mean"),
        )
        grouped["win_rate"] = (grouped["wins"] / grouped["total_calls"] * 100).round(1)
        win_rates[col] = grouped.to_dict("index")

    return win_rates

What does this code do?

  • It creates a binary won column (1 for won, 0 for lost) so pandas can sum wins easily.
  • For each of the 5 technique dimensions, it groups calls by technique value and calculates total calls, wins, average deal value, and win rate percentage.
  • The result is a nested dictionary. For example, under opening_style you might see that "rapport" has a 75% win rate while "direct" has 33%.
  • Save correlator.py.

Seeing an import error for pandas?

Make sure you installed pandas in Step 1. You can verify by running pip install pandas again. If you are using a virtual environment, confirm it is activated before running the install.

Help me debug a pandas import error in my correlator.py file.

Add multi-dimensional correlation

Knowing that "rapport" openings win more often is useful. But knowing that rapport openings win 80% in the morning and only 30% in the afternoon is actionable. This function cross-references techniques with contextual variables to find those patterns.

  • Add the calculate_contextual_correlations() function below calculate_win_rates():
def calculate_contextual_correlations(df: pd.DataFrame) -> dict:
    """Cross-reference techniques with contextual variables."""
    df["won"] = (df["outcome"] == "won").astype(int)
    df["deal_size_bracket"] = pd.cut(
        df["deal_value"],
        bins=[0, 20000, 40000, 100000],
        labels=["small", "medium", "large"],
    )

    correlations = {}

    # Technique x Time of Day
    time_corr = df.groupby(["opening_style", "time_of_day"]).agg(
        total=("won", "count"), wins=("won", "sum")
    )
    time_corr["win_rate"] = (time_corr["wins"] / time_corr["total"] * 100).round(1)
    correlations["opening_by_time"] = time_corr.to_dict("index")

    # Technique x Location
    loc_corr = df.groupby(["value_proposition", "location"]).agg(
        total=("won", "count"), wins=("won", "sum")
    )
    loc_corr["win_rate"] = (loc_corr["wins"] / loc_corr["total"] * 100).round(1)
    correlations["value_prop_by_location"] = loc_corr.to_dict("index")

    # Technique x Deal Size
    size_corr = df.groupby(["closing_technique", "deal_size_bracket"]).agg(
        total=("won", "count"), wins=("won", "sum")
    )
    size_corr["win_rate"] = (size_corr["wins"] / size_corr["total"] * 100).round(1)
    correlations["closing_by_deal_size"] = size_corr.to_dict("index")

    return correlations

What does this code do?

  • pd.cut() buckets deal values into "small" (under $20K), "medium" ($20K-$40K), and "large" ($40K+) so you can analyze technique effectiveness by deal size.
  • The function runs three cross-tabulations: opening style by time of day, value proposition by location, and closing technique by deal size bracket.
  • Each cross-tab groups by two columns simultaneously, so you get win rates for specific combinations like "rapport opening + morning call" or "next-step close + large deal".
  • Save correlator.py.

Not sure about the deal size bins?

The bins [0, 20000, 40000, 100000] define the boundaries. A deal worth $18,000 falls in "small" (0 to 20000). A deal worth $36,000 falls in "medium" (20000 to 40000). A deal worth $72,000 falls in "large" (40000 to 100000).

Help me understand how pd.cut bins work in my correlator.

Generate the correlation summary and run the analysis

The final piece ties both calculations together into a single summary dictionary. This summary will be what your experiment recommender reads in the next step.

  • Add the generate_correlation_summary() function below calculate_contextual_correlations():
def generate_correlation_summary(df: pd.DataFrame) -> dict:
    """Generate a complete correlation summary."""
    win_rates = calculate_win_rates(df)
    contextual = calculate_contextual_correlations(df)

    # Find top performers
    top_techniques = {}
    for dimension, techniques in win_rates.items():
        best = max(techniques.items(), key=lambda x: x[1]["win_rate"])
        top_techniques[dimension] = {
            "technique": best[0],
            "win_rate": best[1]["win_rate"],
            "sample_size": best[1]["total_calls"],
        }

    summary = {
        "win_rates_by_technique": win_rates,
        "contextual_correlations": contextual,
        "top_performers": top_techniques,
        "total_calls_analyzed": len(df),
        "overall_win_rate": round(df["won"].mean() * 100, 1),
    }

    return summary

What does this code do?

  • It calls both calculate_win_rates() and calculate_contextual_correlations() to gather all the analysis results.
  • The top_performers loop finds the single highest win-rate technique for each of the 5 dimensions. This gives the recommender a quick read on what is working best.
  • The final summary packages everything into one dictionary that the next step's recommender will consume.

Now add the main execution block that loads data, runs the full correlation, saves results, and prints a summary to the console.

  • Add this block at the bottom of correlator.py:
if __name__ == "__main__":
    df = load_enriched_calls()
    summary = generate_correlation_summary(df)

    with open("correlation_summary.json", "w") as f:
        json.dump(summary, f, indent=2, default=str)

    print("Correlation analysis complete!")
    print(f"\nTotal calls analyzed: {summary['total_calls_analyzed']}")
    print(f"Overall win rate: {summary['overall_win_rate']}%")
    print("\nTop performing techniques:")
    for dim, info in summary["top_performers"].items():
        print(f"  {dim}: {info['technique']} ({info['win_rate']}% win rate, n={info['sample_size']})")

What does this code do?

The default=str parameter in json.dump() handles any pandas data types (like tuples from multi-index groupby results) that aren't natively JSON-serializable. It converts them to strings rather than crashing.

  • Save correlator.py.
  • Run the correlation analysis by running this command:
python correlator.py
  • You should see output showing the total calls analyzed, overall win rate percentage, and the top performing technique for each of the 5 dimensions.

What should I see?

Your terminal output will show something like "Total calls analyzed: 10" and "Overall win rate: 70.0%". The top performing techniques will vary slightly depending on how the classifier labeled your calls, but you should see one technique listed per dimension with its win rate and sample size.

A new file called correlation_summary.json should also appear in your project folder.

Seeing a FileNotFoundError?

This means enriched_calls.json does not exist in your project folder. Make sure you ran python classifier.py successfully in Step 2 before running the correlator.

Help me fix a FileNotFoundError in correlator.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved correlator.py and that correlation_summary.json exists in your project folder.

ⓧ I'd like to double check the full code

import json
import pandas as pd


def load_enriched_calls(filepath: str = "enriched_calls.json") -> pd.DataFrame:
    """Load enriched call data into a pandas DataFrame."""
    with open(filepath, "r") as f:
        calls = json.load(f)

    rows = []
    for call in calls:
        row = {
            "call_id": call["call_id"],
            "outcome": call["outcome"],
            "deal_value": call["deal_value"],
            "time_of_day": call["time_of_day"],
            "location": call["location"],
            "rep_name": call["rep_name"],
            "call_duration_minutes": call["call_duration_minutes"],
            "opening_style": call["techniques"]["opening_style"],
            "discovery_approach": call["techniques"]["discovery_approach"],
            "objection_handling": call["techniques"]["objection_handling"],
            "value_proposition": call["techniques"]["value_proposition"],
            "closing_technique": call["techniques"]["closing_technique"],
        }
        rows.append(row)

    return pd.DataFrame(rows)


def calculate_win_rates(df: pd.DataFrame) -> dict:
    """Calculate win rates grouped by each technique dimension."""
    df["won"] = (df["outcome"] == "won").astype(int)
    technique_cols = [
        "opening_style",
        "discovery_approach",
        "objection_handling",
        "value_proposition",
        "closing_technique",
    ]

    win_rates = {}
    for col in technique_cols:
        grouped = df.groupby(col).agg(
            total_calls=("won", "count"),
            wins=("won", "sum"),
            avg_deal_value=("deal_value", "mean"),
        )
        grouped["win_rate"] = (grouped["wins"] / grouped["total_calls"] * 100).round(1)
        win_rates[col] = grouped.to_dict("index")

    return win_rates


def calculate_contextual_correlations(df: pd.DataFrame) -> dict:
    """Cross-reference techniques with contextual variables."""
    df["won"] = (df["outcome"] == "won").astype(int)
    df["deal_size_bracket"] = pd.cut(
        df["deal_value"],
        bins=[0, 20000, 40000, 100000],
        labels=["small", "medium", "large"],
    )

    correlations = {}

    # Technique x Time of Day
    time_corr = df.groupby(["opening_style", "time_of_day"]).agg(
        total=("won", "count"), wins=("won", "sum")
    )
    time_corr["win_rate"] = (time_corr["wins"] / time_corr["total"] * 100).round(1)
    correlations["opening_by_time"] = time_corr.to_dict("index")

    # Technique x Location
    loc_corr = df.groupby(["value_proposition", "location"]).agg(
        total=("won", "count"), wins=("won", "sum")
    )
    loc_corr["win_rate"] = (loc_corr["wins"] / loc_corr["total"] * 100).round(1)
    correlations["value_prop_by_location"] = loc_corr.to_dict("index")

    # Technique x Deal Size
    size_corr = df.groupby(["closing_technique", "deal_size_bracket"]).agg(
        total=("won", "count"), wins=("won", "sum")
    )
    size_corr["win_rate"] = (size_corr["wins"] / size_corr["total"] * 100).round(1)
    correlations["closing_by_deal_size"] = size_corr.to_dict("index")

    return correlations


def generate_correlation_summary(df: pd.DataFrame) -> dict:
    """Generate a complete correlation summary."""
    win_rates = calculate_win_rates(df)
    contextual = calculate_contextual_correlations(df)

    # Find top performers
    top_techniques = {}
    for dimension, techniques in win_rates.items():
        best = max(techniques.items(), key=lambda x: x[1]["win_rate"])
        top_techniques[dimension] = {
            "technique": best[0],
            "win_rate": best[1]["win_rate"],
            "sample_size": best[1]["total_calls"],
        }

    summary = {
        "win_rates_by_technique": win_rates,
        "contextual_correlations": contextual,
        "top_performers": top_techniques,
        "total_calls_analyzed": len(df),
        "overall_win_rate": round(df["won"].mean() * 100, 1),
    }

    return summary


if __name__ == "__main__":
    df = load_enriched_calls()
    summary = generate_correlation_summary(df)

    with open("correlation_summary.json", "w") as f:
        json.dump(summary, f, indent=2, default=str)

    print("Correlation analysis complete!")
    print(f"\nTotal calls analyzed: {summary['total_calls_analyzed']}")
    print(f"Overall win rate: {summary['overall_win_rate']}%")
    print("\nTop performing techniques:")
    for dim, info in summary["top_performers"].items():
        print(f"  {dim}: {info['technique']} ({info['win_rate']}% win rate, n={info['sample_size']})")

Your correlation engine is complete. You now have structured data showing exactly which techniques perform best and in which contexts they shine. Next up, you'll feed these patterns into an AI recommender that suggests specific experiments to run.

Build the Experiment Recommender

Your correlation engine has identified patterns in the data. Now you need something that can reason about those patterns and recommend concrete experiments to test.

In this step, you will build the recommender agent that feeds correlation data into gpt-4o-mini and asks it to propose A/B test experiments with projected ROI. Then you will create the orchestrator that ties the entire pipeline together.

In this step, get ready to:
  • Build a recommendation engine that turns correlation patterns into testable experiment proposals.
  • Add ROI projections that quantify the revenue impact of each experiment.
  • Create an orchestrator that runs the full pipeline end-to-end.
Create the recommendation prompt and OpenAI call

The recommender sends your correlation summary to gpt-4o-mini with a prompt that asks the model to act as a sales operations analyst. The model reasons about the patterns and proposes experiments.

  • Create a new file called recommender.py in your project folder.
  • Add the imports, client setup, and recommendation prompt by pasting this code:
import json
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

client = OpenAI()

RECOMMENDER_PROMPT = """You are a senior sales operations analyst. Based on the following sales call correlation data, identify the top 3 experiments worth running to improve close rates.

For each experiment, provide:
1. hypothesis: A clear testable statement
2. test_group: What the test reps should do differently
3. control_group: What the control reps should continue doing
4. expected_uplift_percent: Your estimated improvement in win rate (as a number)
5. confidence: "high", "medium", or "low" based on sample size and effect magnitude
6. reasoning: 2-3 sentences explaining why this pattern suggests an opportunity

Return a JSON object with a key "experiments" containing an array of 3 experiment objects.

Correlation Data:
{correlation_data}"""

What does this code do?

The setup follows the same pattern as classifier.py. You initialize the OpenAI client using your environment variable.

The RECOMMENDER_PROMPT defines the exact output structure (hypothesis, test group, control group, uplift, confidence, reasoning) so the model returns structured JSON you can process programmatically.

  • Add the function that sends correlation data to the AI by pasting this below the prompt:

def generate_recommendations(correlation_summary: dict) -> dict:
    """Generate experiment recommendations from correlation data."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "You are a sales operations analyst. Always respond with valid JSON only.",
            },
            {
                "role": "user",
                "content": RECOMMENDER_PROMPT.format(
                    correlation_data=json.dumps(correlation_summary, indent=2, default=str)
                ),
            },
        ],
        response_format={"type": "json_object"},
        temperature=0.3,
    )
    result = json.loads(response.choices[0].message.content)
    return result

What does this function do?

  • It serializes the correlation summary to JSON and injects it into the prompt using .format().
  • The temperature=0.3 setting keeps the output focused and consistent. Lower temperature means less creative variation in the recommendations.
  • The response_format={"type": "json_object"} forces the model to return valid JSON so you can parse it reliably.
  • Save recommender.py.

You won't see output yet. The next section adds the ROI calculations and a test runner.

Add ROI projections and test the recommender

Raw experiment recommendations are useful, but stakeholders care about revenue impact. The ROI projection translates each experiment's expected uplift into dollar figures.

  • In recommender.py, add the ROI projection function below generate_recommendations():

def add_roi_projections(
    recommendations: dict, total_deals_per_month: int = 40, avg_deal_value: float = 38000
) -> dict:
    """Add ROI projections to each experiment recommendation."""
    for experiment in recommendations["experiments"]:
        uplift = experiment["expected_uplift_percent"] / 100
        projected_additional_wins = total_deals_per_month * uplift
        projected_monthly_revenue = projected_additional_wins * avg_deal_value
        experiment["roi_projection"] = {
            "additional_wins_per_month": round(projected_additional_wins, 1),
            "projected_monthly_revenue": round(projected_monthly_revenue, 2),
            "projected_annual_revenue": round(projected_monthly_revenue * 12, 2),
        }

    return recommendations

What does this function do?

For each experiment, it calculates: (total_deals_per_month * expected_uplift * avg_deal_value). The defaults assume 40 deals per month at $38,000 average value.

The function attaches a roi_projection dictionary to each experiment showing additional wins, monthly revenue, and annual revenue. This makes each experiment brief actionable for a sales leader.

  • Add the test runner at the bottom of recommender.py so you can run the file standalone:

if __name__ == "__main__":
    with open("correlation_summary.json", "r") as f:
        summary = json.load(f)

    recommendations = generate_recommendations(summary)
    recommendations = add_roi_projections(recommendations)

    with open("recommendations.json", "w") as f:
        json.dump(recommendations, f, indent=2)

    print("Experiment recommendations generated!")
    print("\nTop 3 Experiments:")
    for i, exp in enumerate(recommendations["experiments"], 1):
        print(f"\n--- Experiment {i} ---")
        print(f"Hypothesis: {exp['hypothesis']}")
        print(f"Expected Uplift: {exp['expected_uplift_percent']}%")
        print(f"Confidence: {exp['confidence']}")
        roi = exp["roi_projection"]
        print(f"Projected Monthly Revenue: ${roi['projected_monthly_revenue']:,.0f}")

What does this code do?

  • It loads the correlation_summary.json file that your correlator generated in the previous step.
  • It sends that data to the AI, gets back experiment recommendations, then enriches them with ROI projections.
  • It saves the final output to recommendations.json and prints a summary to your terminal.
  • Save recommender.py.
  • Run the recommender by executing this command in your terminal:
python recommender.py
  • You should see 3 experiment recommendations printed with hypotheses, expected uplift percentages, confidence levels, and projected monthly revenue figures.

Seeing a FileNotFoundError?

  • Make sure you ran correlator.py in the previous step. The recommender needs correlation_summary.json to exist in the same folder.
  • Check that your terminal is in the project folder where your Python files live.

Why is my recommender failing to generate results?

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

import json
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

client = OpenAI()

RECOMMENDER_PROMPT = """You are a senior sales operations analyst. Based on the following sales call correlation data, identify the top 3 experiments worth running to improve close rates.

For each experiment, provide:
1. hypothesis: A clear testable statement
2. test_group: What the test reps should do differently
3. control_group: What the control reps should continue doing
4. expected_uplift_percent: Your estimated improvement in win rate (as a number)
5. confidence: "high", "medium", or "low" based on sample size and effect magnitude
6. reasoning: 2-3 sentences explaining why this pattern suggests an opportunity

Return a JSON object with a key "experiments" containing an array of 3 experiment objects.

Correlation Data:
{correlation_data}"""


def generate_recommendations(correlation_summary: dict) -> dict:
    """Generate experiment recommendations from correlation data."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "You are a sales operations analyst. Always respond with valid JSON only.",
            },
            {
                "role": "user",
                "content": RECOMMENDER_PROMPT.format(
                    correlation_data=json.dumps(correlation_summary, indent=2, default=str)
                ),
            },
        ],
        response_format={"type": "json_object"},
        temperature=0.3,
    )
    result = json.loads(response.choices[0].message.content)
    return result


def add_roi_projections(
    recommendations: dict, total_deals_per_month: int = 40, avg_deal_value: float = 38000
) -> dict:
    """Add ROI projections to each experiment recommendation."""
    for experiment in recommendations["experiments"]:
        uplift = experiment["expected_uplift_percent"] / 100
        projected_additional_wins = total_deals_per_month * uplift
        projected_monthly_revenue = projected_additional_wins * avg_deal_value
        experiment["roi_projection"] = {
            "additional_wins_per_month": round(projected_additional_wins, 1),
            "projected_monthly_revenue": round(projected_monthly_revenue, 2),
            "projected_annual_revenue": round(projected_monthly_revenue * 12, 2),
        }

    return recommendations


if __name__ == "__main__":
    with open("correlation_summary.json", "r") as f:
        summary = json.load(f)

    recommendations = generate_recommendations(summary)
    recommendations = add_roi_projections(recommendations)

    with open("recommendations.json", "w") as f:
        json.dump(recommendations, f, indent=2)

    print("Experiment recommendations generated!")
    print("\nTop 3 Experiments:")
    for i, exp in enumerate(recommendations["experiments"], 1):
        print(f"\n--- Experiment {i} ---")
        print(f"Hypothesis: {exp['hypothesis']}")
        print(f"Expected Uplift: {exp['expected_uplift_percent']}%")
        print(f"Confidence: {exp['confidence']}")
        roi = exp["roi_projection"]
        print(f"Projected Monthly Revenue: ${roi['projected_monthly_revenue']:,.0f}")
Orchestrate the full pipeline with main.py

Each module works independently, but the real power comes from chaining them together. The orchestrator loads raw call data, classifies it, correlates it, generates recommendations, and produces a final report.

  • Create a new file called main.py in your project folder.
  • Paste the full orchestration script:
import json
import os
from dotenv import load_dotenv
from classifier import classify_batch
from correlator import load_enriched_calls, generate_correlation_summary
from recommender import generate_recommendations, add_roi_projections
from report import generate_report, print_report

load_dotenv()


def run_pipeline():
    """Run the complete sales experiment optimization pipeline."""
    print("=" * 60)
    print("  SALES EXPERIMENT OPTIMIZER AGENT")
    print("=" * 60)

    # Step 1: Load raw call data
    print("\n[1/4] Loading sales call data...")
    with open("sample_calls.json", "r") as f:
        calls = json.load(f)
    print(f"  Loaded {len(calls)} calls.")

    # Step 2: Classify techniques
    print("\n[2/4] Classifying sales techniques...")
    enriched_calls = classify_batch(calls)
    with open("enriched_calls.json", "w") as f:
        json.dump(enriched_calls, f, indent=2)
    print("  Classification complete.")

    # Step 3: Correlate techniques with outcomes
    print("\n[3/4] Running correlation analysis...")
    import pandas as pd

    df = load_enriched_calls("enriched_calls.json")
    correlation_summary = generate_correlation_summary(df)
    with open("correlation_summary.json", "w") as f:
        json.dump(correlation_summary, f, indent=2, default=str)
    print(f"  Overall win rate: {correlation_summary['overall_win_rate']}%")

    # Step 4: Generate recommendations
    print("\n[4/4] Generating experiment recommendations...")
    recommendations = generate_recommendations(correlation_summary)
    recommendations = add_roi_projections(recommendations)
    with open("recommendations.json", "w") as f:
        json.dump(recommendations, f, indent=2)

    # Generate final report
    print("\nGenerating final report...")
    report = generate_report(correlation_summary, recommendations)
    with open("report.json", "w") as f:
        json.dump(report, f, indent=2, default=str)

    print_report(report)
    print("\nAll artifacts saved: enriched_calls.json, correlation_summary.json, recommendations.json, report.json")


if __name__ == "__main__":
    run_pipeline()

What does this pipeline do?

  • Stage 1 loads the raw call data from sample_calls.json.
  • Stage 2 classifies each call's sales techniques using the AI classifier.
  • Stage 3 runs correlation analysis to find patterns between techniques and outcomes.
  • Stage 4 generates experiment recommendations with ROI projections.
  • Finally, it generates a full report and prints it to the console. Each intermediate result is saved as a JSON file.

Why does main.py import from report?

The orchestrator imports generate_report and print_report from a report.py module that you have not created yet. You will build that in the next step. For now, save this file but do not run it.

  • Save main.py.

You cannot run main.py yet because it depends on report.py, which you will build in the next step.

Seeing a syntax error when you save?

  • Check that you pasted the full code block including the closing parenthesis on the run_pipeline() call at the bottom.
  • Make sure the import lines at the top match exactly. Each import pulls functions from your existing files.

Help me fix a syntax error in my main.py orchestrator file.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

import json
import os
from dotenv import load_dotenv
from classifier import classify_batch
from correlator import load_enriched_calls, generate_correlation_summary
from recommender import generate_recommendations, add_roi_projections
from report import generate_report, print_report

load_dotenv()


def run_pipeline():
    """Run the complete sales experiment optimization pipeline."""
    print("=" * 60)
    print("  SALES EXPERIMENT OPTIMIZER AGENT")
    print("=" * 60)

    # Step 1: Load raw call data
    print("\n[1/4] Loading sales call data...")
    with open("sample_calls.json", "r") as f:
        calls = json.load(f)
    print(f"  Loaded {len(calls)} calls.")

    # Step 2: Classify techniques
    print("\n[2/4] Classifying sales techniques...")
    enriched_calls = classify_batch(calls)
    with open("enriched_calls.json", "w") as f:
        json.dump(enriched_calls, f, indent=2)
    print("  Classification complete.")

    # Step 3: Correlate techniques with outcomes
    print("\n[3/4] Running correlation analysis...")
    import pandas as pd

    df = load_enriched_calls("enriched_calls.json")
    correlation_summary = generate_correlation_summary(df)
    with open("correlation_summary.json", "w") as f:
        json.dump(correlation_summary, f, indent=2, default=str)
    print(f"  Overall win rate: {correlation_summary['overall_win_rate']}%")

    # Step 4: Generate recommendations
    print("\n[4/4] Generating experiment recommendations...")
    recommendations = generate_recommendations(correlation_summary)
    recommendations = add_roi_projections(recommendations)
    with open("recommendations.json", "w") as f:
        json.dump(recommendations, f, indent=2)

    # Generate final report
    print("\nGenerating final report...")
    report = generate_report(correlation_summary, recommendations)
    with open("report.json", "w") as f:
        json.dump(report, f, indent=2, default=str)

    print_report(report)
    print("\nAll artifacts saved: enriched_calls.json, correlation_summary.json, recommendations.json, report.json")


if __name__ == "__main__":
    run_pipeline()

Your recommender agent is producing actionable experiment briefs with revenue projections. Next up, you will build the trend report that ties all this analysis into a polished output you can run repeatedly.

Generate the Trend Report

Your agent can now classify calls, find correlations, and recommend experiments. The final piece is a structured report that pulls everything together into a format a sales leader can act on.

In this step, you will build a report generator that produces an executive summary, technique rankings, contextual insights, and experiment recommendations with ROI projections. You will also add historical comparison so the agent can track trends over time.

In this step, get ready to:
  • Create a report generator that structures all analysis output into a readable trend report.
  • Add historical comparison capability for tracking win rate trends across analysis runs.
  • Run the full pipeline end-to-end and review the complete optimization report.
Create the report generator function

The report generator takes the correlation summary and experiment recommendations as inputs, then assembles a structured report with multiple sections. It also accepts an optional previous report for trend comparison.

  • Create a new file called report.py in your project folder.
  • Add the imports and the generate_report() function by pasting this code:
import json
import os
from datetime import datetime


def generate_report(
    correlation_summary: dict, recommendations: dict, previous_report: dict = None
) -> dict:
    """Generate a complete trend report with optional historical comparison."""
    report = {
        "generated_at": datetime.now().isoformat(),
        "executive_summary": [],
        "technique_rankings": {},
        "contextual_insights": [],
        "experiments": recommendations.get("experiments", []),
        "trends": None,
    }

    # Executive summary
    overall_wr = correlation_summary["overall_win_rate"]
    total_calls = correlation_summary["total_calls_analyzed"]
    top_technique = list(correlation_summary["top_performers"].values())[0]

    report["executive_summary"] = [
        f"Analyzed {total_calls} sales calls with an overall win rate of {overall_wr}%.",
        f"Top performing technique: {top_technique['technique']} in {list(correlation_summary['top_performers'].keys())[0]} ({top_technique['win_rate']}% win rate).",
        f"Generated {len(report['experiments'])} experiment recommendations with projected ROI.",
    ]

    # Technique rankings
    for dimension, techniques in correlation_summary["win_rates_by_technique"].items():
        ranked = sorted(techniques.items(), key=lambda x: x[1]["win_rate"], reverse=True)
        report["technique_rankings"][dimension] = [
            {"technique": name, "win_rate": data["win_rate"], "sample_size": data["total_calls"]}
            for name, data in ranked
        ]

    # Contextual insights
    contextual = correlation_summary.get("contextual_correlations", {})
    for context_name, data in contextual.items():
        for key, values in data.items():
            if values.get("win_rate", 0) >= 80 and values.get("total", 0) >= 2:
                report["contextual_insights"].append(
                    {
                        "context": context_name,
                        "combination": str(key),
                        "win_rate": values["win_rate"],
                        "sample_size": values["total"],
                        "insight": f"High-performing combination: {key} achieves {values['win_rate']}% win rate (n={values['total']})",
                    }
                )

    # Historical comparison
    if previous_report:
        prev_wr = previous_report.get("overall_win_rate", overall_wr)
        change = round(overall_wr - prev_wr, 1)
        report["trends"] = {
            "win_rate_change": change,
            "direction": "improved" if change > 0 else "declined" if change < 0 else "stable",
            "previous_win_rate": prev_wr,
            "current_win_rate": overall_wr,
        }

    return report

What does this code do?

The generate_report() function assembles all analysis outputs into a single structured dictionary.

  • The executive summary pulls the overall win rate, top technique, and experiment count into 3 human-readable bullet points.
  • Technique rankings sort each dimension by win rate so the best-performing techniques appear first.
  • Contextual insights filter for high-performing combinations where win rate is 80% or higher with at least 2 data points.
  • The historical comparison section only activates when a previous_report is passed in. It calculates the change in win rate and labels the direction as improved, declined, or stable.

You won't see output yet. The next chunk adds a print function that formats the report for the console.

Add the console report printer

The structured report dictionary is great for saving to a file, but you also want a human-readable console output. The print_report() function formats each section with clear headings and indentation.

  • In report.py, add the print_report() function and the __main__ block below the existing code:

def print_report(report: dict):
    """Print a human-readable version of the report."""
    print("\n" + "=" * 60)
    print("   SALES EXPERIMENT OPTIMIZATION REPORT")
    print("=" * 60)
    print(f"\nGenerated: {report['generated_at']}")

    print("\n--- EXECUTIVE SUMMARY ---")
    for point in report["executive_summary"]:
        print(f"  * {point}")

    print("\n--- TECHNIQUE RANKINGS ---")
    for dimension, rankings in report["technique_rankings"].items():
        print(f"\n  {dimension}:")
        for i, item in enumerate(rankings, 1):
            print(f"    {i}. {item['technique']} - {item['win_rate']}% (n={item['sample_size']})")

    if report["contextual_insights"]:
        print("\n--- CONTEXTUAL INSIGHTS ---")
        for insight in report["contextual_insights"]:
            print(f"  * {insight['insight']}")

    print("\n--- EXPERIMENT RECOMMENDATIONS ---")
    for i, exp in enumerate(report["experiments"], 1):
        print(f"\n  Experiment {i}: {exp['hypothesis']}")
        print(f"    Test: {exp['test_group']}")
        print(f"    Control: {exp['control_group']}")
        print(f"    Expected Uplift: {exp['expected_uplift_percent']}%")
        print(f"    Confidence: {exp['confidence']}")
        if "roi_projection" in exp:
            roi = exp["roi_projection"]
            print(f"    Projected Monthly Revenue: ${roi['projected_monthly_revenue']:,.0f}")
            print(f"    Projected Annual Revenue: ${roi['projected_annual_revenue']:,.0f}")

    if report["trends"]:
        print("\n--- TRENDS ---")
        trends = report["trends"]
        print(f"  Win rate {trends['direction']}: {trends['previous_win_rate']}% -> {trends['current_win_rate']}% ({trends['win_rate_change']:+.1f}%)")

    print("\n" + "=" * 60)


if __name__ == "__main__":
    with open("correlation_summary.json", "r") as f:
        correlation_summary = json.load(f)

    with open("recommendations.json", "r") as f:
        recommendations = json.load(f)

    report = generate_report(correlation_summary, recommendations)

    with open("report.json", "w") as f:
        json.dump(report, f, indent=2, default=str)

    print_report(report)

What does this code do?

  • print_report() formats the report dictionary into a sectioned console output with headings for executive summary, technique rankings, contextual insights, experiment recommendations, and trends.
  • The __main__ block loads the previously generated correlation_summary.json and recommendations.json files, generates the report, saves it as report.json, and prints the formatted version to the console.
  • The trends section only prints if a previous report was provided. On the first run, it stays hidden since there is no prior data to compare against.
  • Save report.py.
  • Test the report generator standalone by running:
python report.py
  • You should see a formatted report in your terminal with sections for EXECUTIVE SUMMARY, TECHNIQUE RANKINGS, CONTEXTUAL INSIGHTS, and EXPERIMENT RECOMMENDATIONS. A report.json file should also appear in your project folder.

Seeing a FileNotFoundError?

The report.py script expects correlation_summary.json and recommendations.json to already exist. Make sure you ran correlator.py and recommender.py in previous steps. If those files are missing, re-run them first.

help me debug a FileNotFoundError in report.py

Why structure the report for historical comparison?

Real sales teams run this analysis weekly or monthly. By accepting a previous_report parameter, the agent can show how win rates change after implementing experiment recommendations. This turns a one-time analysis into a continuous improvement loop.

✔️ Awesome, I've got everything!

Great. Double check you have saved report.py.

ⓧ I'd like to double check the full code

import json
import os
from datetime import datetime


def generate_report(
    correlation_summary: dict, recommendations: dict, previous_report: dict = None
) -> dict:
    """Generate a complete trend report with optional historical comparison."""
    report = {
        "generated_at": datetime.now().isoformat(),
        "executive_summary": [],
        "technique_rankings": {},
        "contextual_insights": [],
        "experiments": recommendations.get("experiments", []),
        "trends": None,
    }

    # Executive summary
    overall_wr = correlation_summary["overall_win_rate"]
    total_calls = correlation_summary["total_calls_analyzed"]
    top_technique = list(correlation_summary["top_performers"].values())[0]

    report["executive_summary"] = [
        f"Analyzed {total_calls} sales calls with an overall win rate of {overall_wr}%.",
        f"Top performing technique: {top_technique['technique']} in {list(correlation_summary['top_performers'].keys())[0]} ({top_technique['win_rate']}% win rate).",
        f"Generated {len(report['experiments'])} experiment recommendations with projected ROI.",
    ]

    # Technique rankings
    for dimension, techniques in correlation_summary["win_rates_by_technique"].items():
        ranked = sorted(techniques.items(), key=lambda x: x[1]["win_rate"], reverse=True)
        report["technique_rankings"][dimension] = [
            {"technique": name, "win_rate": data["win_rate"], "sample_size": data["total_calls"]}
            for name, data in ranked
        ]

    # Contextual insights
    contextual = correlation_summary.get("contextual_correlations", {})
    for context_name, data in contextual.items():
        for key, values in data.items():
            if values.get("win_rate", 0) >= 80 and values.get("total", 0) >= 2:
                report["contextual_insights"].append(
                    {
                        "context": context_name,
                        "combination": str(key),
                        "win_rate": values["win_rate"],
                        "sample_size": values["total"],
                        "insight": f"High-performing combination: {key} achieves {values['win_rate']}% win rate (n={values['total']})",
                    }
                )

    # Historical comparison
    if previous_report:
        prev_wr = previous_report.get("overall_win_rate", overall_wr)
        change = round(overall_wr - prev_wr, 1)
        report["trends"] = {
            "win_rate_change": change,
            "direction": "improved" if change > 0 else "declined" if change < 0 else "stable",
            "previous_win_rate": prev_wr,
            "current_win_rate": overall_wr,
        }

    return report


def print_report(report: dict):
    """Print a human-readable version of the report."""
    print("\n" + "=" * 60)
    print("   SALES EXPERIMENT OPTIMIZATION REPORT")
    print("=" * 60)
    print(f"\nGenerated: {report['generated_at']}")

    print("\n--- EXECUTIVE SUMMARY ---")
    for point in report["executive_summary"]:
        print(f"  * {point}")

    print("\n--- TECHNIQUE RANKINGS ---")
    for dimension, rankings in report["technique_rankings"].items():
        print(f"\n  {dimension}:")
        for i, item in enumerate(rankings, 1):
            print(f"    {i}. {item['technique']} - {item['win_rate']}% (n={item['sample_size']})")

    if report["contextual_insights"]:
        print("\n--- CONTEXTUAL INSIGHTS ---")
        for insight in report["contextual_insights"]:
            print(f"  * {insight['insight']}")

    print("\n--- EXPERIMENT RECOMMENDATIONS ---")
    for i, exp in enumerate(report["experiments"], 1):
        print(f"\n  Experiment {i}: {exp['hypothesis']}")
        print(f"    Test: {exp['test_group']}")
        print(f"    Control: {exp['control_group']}")
        print(f"    Expected Uplift: {exp['expected_uplift_percent']}%")
        print(f"    Confidence: {exp['confidence']}")
        if "roi_projection" in exp:
            roi = exp["roi_projection"]
            print(f"    Projected Monthly Revenue: ${roi['projected_monthly_revenue']:,.0f}")
            print(f"    Projected Annual Revenue: ${roi['projected_annual_revenue']:,.0f}")

    if report["trends"]:
        print("\n--- TRENDS ---")
        trends = report["trends"]
        print(f"  Win rate {trends['direction']}: {trends['previous_win_rate']}% -> {trends['current_win_rate']}% ({trends['win_rate_change']:+.1f}%)")

    print("\n" + "=" * 60)


if __name__ == "__main__":
    with open("correlation_summary.json", "r") as f:
        correlation_summary = json.load(f)

    with open("recommendations.json", "r") as f:
        recommendations = json.load(f)

    report = generate_report(correlation_summary, recommendations)

    with open("report.json", "w") as f:
        json.dump(report, f, indent=2, default=str)

    print_report(report)
Run the complete pipeline end-to-end

With report.py in place, your main.py orchestrator now has all four modules available. Running it executes the full agent pipeline from raw call data to final report.

  • Run the complete pipeline by executing:
python main.py

What should I see?

The pipeline runs through four stages in sequence, printing progress as it goes:

  • [1/4] Loading sales call data loads your 10 sample calls.
  • [2/4] Classifying sales techniques sends each call to gpt-4o-mini for classification. This takes about 10-15 seconds due to rate-limit spacing.
  • [3/4] Running correlation analysis crunches win rates across all dimensions.
  • [4/4] Generating experiment recommendations asks the model to identify the top 3 experiments.

After the stages complete, the full SALES EXPERIMENT OPTIMIZATION REPORT prints with technique rankings, contextual insights, and experiment recommendations with ROI projections.

  • Confirm that your project folder now contains these output files: enriched_calls.json, correlation_summary.json, recommendations.json, and report.json.
  • Verify by running:
ls
  • You should see enriched_calls.json, correlation_summary.json, recommendations.json, and report.json alongside your Python files and sample_calls.json.

Pipeline failing partway through?

  • If you see an error about OPENAI_API_KEY, make sure your environment variable is set correctly or your .env file contains your actual API key.
  • If you get a rate limit error, wait 30 seconds and run python main.py again. The sleep between calls helps, but new accounts sometimes have tighter limits.
  • If you see an import error for report, confirm your file is named exactly report.py (not reports.py or Report.py).

help me debug my main.py pipeline run

Your sales experiment optimizer agent is complete. It processes raw call data through four stages and produces a structured report with actionable experiment recommendations. Next up, a Secret Mission that takes the agent even further by processing raw audio files.

Secret mission

Add Audio Ingestion with Speaker Diarization

Your agent is powerful, but it only works with pre-formatted transcripts. Real sales teams have audio recordings, not neat text files. In this secret mission, you'll add an audio ingestion step that uses OpenAI's diarized transcription to process raw call recordings and automatically separate who said what before feeding into your analysis pipeline.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally, so there are no cloud infrastructure costs. The only potential ongoing cost is OpenAI API usage if you run more calls.

Cost warning

OpenAI's auto-recharge is enabled by default when you add prepaid credits. If you run additional API calls beyond this project, your payment method will be charged automatically. Disable auto-recharge in your billing settings if you want to prevent unexpected charges.

Resources you used:

  • OpenAI API account with prepaid credits (credits expire after 1 year).
  • OpenAI API key stored as an environment variable and in a .env file.
  • Local project files (classifier.py, correlator.py, recommender.py, report.py, main.py, sample_calls.json, .env).
  • Python packages: openai, pandas, python-dotenv.

Keep everything running

No action needed. Choose this if you plan to continue experimenting with your sales optimizer agent or want to run it on new call data.

  • Your project files stay on your Desktop in the project folder.
  • Your OpenAI API key remains active. You will only be charged if you run additional API calls.
  • Your prepaid credits remain available for 1 year from the date you added them.

Pause - I'll come back to this later

Prevent accidental charges while keeping your files intact so you can return to the project later.

  • Navigate to Billing on platform.openai.com and disable auto-recharge. This prevents your payment method from being charged if you accidentally run the agent.
  • Your project files and installed packages stay in place for when you return.

Delete - I don't want to use this again

Remove all project resources and revoke API access. Follow each step below.

  • Delete your API key by navigating to API Keys on platform.openai.com, clicking the trash icon next to your key, and confirming deletion.
  • Navigate to Billing on platform.openai.com and disable auto-recharge to prevent future charges.
  • Remove the environment variable by running this command in PowerShell:
setx OPENAI_API_KEY ""
  • Uninstall the Python packages by running:
pip uninstall openai pandas python-dotenv -y
  • Delete your project folder. On macOS/Linux:
rm -rf ~/Desktop/sales-experiment-optimizer

On Windows:

rmdir /s /q %USERPROFILE%\Desktop\sales-experiment-optimizer

Nice Work!

Nice Work!

Congratulations! You built an AI-powered sales experiment optimizer that turns raw call data into actionable business intelligence.

You've learned how to:

  • Built a multi-step AI agent pipeline that processes batch data through classification, correlation, and recommendation stages using OpenAI's Chat Completions API with structured JSON output.
  • Built a correlation engine with pandas that identifies statistically meaningful patterns across multiple dimensions like time of day, location, and deal size.
  • Created an experiment recommender that produces testable hypotheses with projected ROI and a trend report format supporting longitudinal comparison across analysis runs.
  • Secret Mission: Added audio ingestion with speaker diarization to process raw call recordings end-to-end.

Ready to quiz yourself?