<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ayush Singh Tomar</title>
    <description>The latest articles on DEV Community by Ayush Singh Tomar (@ayushsinghtomar).</description>
    <link>https://dev.to/ayushsinghtomar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4003437%2Fe65cd1a7-7525-47b6-9667-cabb87f59d40.jpg</url>
      <title>DEV Community: Ayush Singh Tomar</title>
      <link>https://dev.to/ayushsinghtomar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayushsinghtomar"/>
    <language>en</language>
    <item>
      <title>I Got Tired of Writing Cold Emails. So I Built an AI Agent to Do It for Me.</title>
      <dc:creator>Ayush Singh Tomar</dc:creator>
      <pubDate>Mon, 29 Jun 2026 04:22:18 +0000</pubDate>
      <link>https://dev.to/ayushsinghtomar/i-got-tired-of-writing-cold-emails-so-i-built-an-ai-agent-to-do-it-for-me-2m4h</link>
      <guid>https://dev.to/ayushsinghtomar/i-got-tired-of-writing-cold-emails-so-i-built-an-ai-agent-to-do-it-for-me-2m4h</guid>
      <description>&lt;p&gt;I Got Tired of Writing Cold Emails. So I Built an AI Agent to Do It for Me.&lt;/p&gt;

&lt;p&gt;B2B sales reps spend hours researching a single lead — reading LinkedIn profiles, Googling the company, checking for recent news, then writing a personalized email that doesn't sound like a template. Most of that work is repetitive pattern-matching. I wanted to see if an AI agent could do it better, faster, and without copy-paste.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;SalesAgent&lt;/strong&gt; — paste a LinkedIn URL, get a researched lead profile, an ML-based score (0–100), and a hyper-personalized cold email. End to end in under 45 seconds. No templates. No manual research. Just paste and go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://salesagent-theta.vercel.app" rel="noopener noreferrer"&gt;salesagent-theta.vercel.app&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ayush-s-tomar/salesagent" rel="noopener noreferrer"&gt;github.com/ayush-s-tomar/salesagent&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You paste a LinkedIn profile URL into the React frontend&lt;/li&gt;
&lt;li&gt;The FastAPI backend triggers a LangGraph agent&lt;/li&gt;
&lt;li&gt;The agent runs live Tavily web searches to research the lead and their company&lt;/li&gt;
&lt;li&gt;A scikit-learn model scores the lead 0–100 based on six signals&lt;/li&gt;
&lt;li&gt;Groq's LLaMA generates a personalized cold email referencing real company events&lt;/li&gt;
&lt;li&gt;You get: lead summary, score with breakdown, and a ready-to-send email&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what it looks like in action — I ran it on Satya Nadella's LinkedIn profile. It found Microsoft's recent AI keynote announcements and referenced them directly in the email: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Subject:&lt;/strong&gt; Microsoft's Quantum Leap: Can You Keep Up with the AI Revolution?&lt;/p&gt;

&lt;p&gt;Dear Satya, I was excited to watch your recent keynote on Microsoft's AI advancements, including the launch of seven new MAI models and the introduction of Majorana 2, your quantum computer...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not a template. The agent found that news in real time and wrote around it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jcc3wu5mo44ljcbfd6b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jcc3wu5mo44ljcbfd6b.png" alt="SalesAgent Architecture Diagram" width="597" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yd24n9gw2ru7foshvdh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yd24n9gw2ru7foshvdh.png" alt="SalesAgent running on Satya Nadella LinkedIn profile" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three nodes. Each one enriches the context for the next. The scoring node doesn't call an LLM — it runs a trained ML model, which is faster and more deterministic for a classification task like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; LangGraph · FastAPI · React · scikit-learn · Groq · Tavily · Render · Vercel&lt;/p&gt;


&lt;h2&gt;
  
  
  How Each Part Works
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Research Node — Tavily + LangGraph
&lt;/h3&gt;

&lt;p&gt;The agent calls Tavily's search API twice per lead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search 1:&lt;/strong&gt; &lt;code&gt;"{name} {company} LinkedIn"&lt;/code&gt; — pulls profile signals (title, summary, skills)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search 2:&lt;/strong&gt; &lt;code&gt;"{company} news funding jobs 2024"&lt;/code&gt; — checks for recent company activity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tavily returns structured results with titles, URLs, and content snippets. The LangGraph research node processes these into six binary/numeric signals that feed the scorer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# Is company name known?
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# Is job title known?
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skills_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# Number of skills (0–15)
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# Does profile have a summary?
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_news&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# Did Tavily find company news?
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# Did Tavily find job postings?
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;has_news&lt;/code&gt; and &lt;code&gt;has_jobs&lt;/code&gt; are the most valuable signals — they tell you whether the company is active and growing right now. That matters more than whether a LinkedIn summary exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Scoring Node — scikit-learn
&lt;/h3&gt;

&lt;p&gt;The scorer uses a &lt;strong&gt;Gradient Boosting Classifier&lt;/strong&gt; trained on 500 synthetic samples generated with numpy. Labels were assigned using a weighted formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;has_news&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;   &lt;span class="c1"&gt;# Company is in the news = hot lead
&lt;/span&gt;    &lt;span class="n"&gt;has_jobs&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;   &lt;span class="c1"&gt;# Hiring = growing, budget exists
&lt;/span&gt;    &lt;span class="n"&gt;has_title&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;   &lt;span class="c1"&gt;# We know who we're targeting
&lt;/span&gt;    &lt;span class="n"&gt;has_summary&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;   &lt;span class="c1"&gt;# They invest in their profile
&lt;/span&gt;    &lt;span class="n"&gt;skills_count&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="c1"&gt;# Proxy for profile completeness
&lt;/span&gt;    &lt;span class="n"&gt;has_company&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;     &lt;span class="c1"&gt;# Basic data quality check
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why ML instead of just an LLM scoring the lead? Two reasons: speed and determinism. An LLM call adds 2–3 seconds and gives you a different score every run. A trained classifier runs in milliseconds and gives you the same score for the same inputs every time — which matters when you're building something people actually use.&lt;/p&gt;

&lt;p&gt;In production, you'd retrain on real CRM data — won vs lost deals — with richer features like funding stage, company size, industry vertical, and email response rate. But for a portfolio project with no CRM access, synthetic training with domain-informed weights gets you a working, explainable scorer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Email Generation Node — Groq + LLaMA
&lt;/h3&gt;

&lt;p&gt;The email node takes the full lead context — name, title, company, recent news, job postings — and injects it into a structured prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System: You are an expert B2B sales copywriter. Write emails that are 
specific, short, and reference real context. Never use generic openers.

User: Lead: {name}, {title} at {company}
Recent news: {news_snippet}
Score: {score}/100
Write a 3-paragraph cold email referencing the news above.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key constraint is &lt;strong&gt;"never use generic openers"&lt;/strong&gt; — without this, LLaMA defaults to "I hope this email finds you well." With it, every email opens with a specific reference to something real about the company.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke (The Honest Part)
&lt;/h2&gt;

&lt;p&gt;This is where I spent most of my time. Real projects break in ways tutorials never show you.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Groq Model Deprecations — Three Times
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt; failed. Switched to &lt;code&gt;llama3-70b-8192&lt;/code&gt;. That was decommissioned. Tried &lt;code&gt;llama3-groq-70b-8192-tool-use-preview&lt;/code&gt; — tool-calling didn't work properly. Ended up on &lt;code&gt;llama-3.1-8b-instant&lt;/code&gt;, which is smaller but stable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; never hardcode a model string. In a production system, this belongs in a config file or environment variable so you can swap it without touching code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Tool-Calling Schema Bug — 400 Failed Generation
&lt;/h3&gt;

&lt;p&gt;Groq was rejecting my tool schemas with a &lt;code&gt;failed_generation&lt;/code&gt; 400 error. After multiple attempts to isolate it, the issue was that I was passing &lt;code&gt;input_schema&lt;/code&gt; directly instead of extracting &lt;code&gt;properties&lt;/code&gt; and &lt;code&gt;required&lt;/code&gt; separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_schema&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Right:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This took longer than it should have because the error message (&lt;code&gt;failed_generation&lt;/code&gt;) gave no hint about the schema structure. If you're hitting this — check your tool schema first.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Interface Mismatch Between graph.py and llm.py
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;graph.py&lt;/code&gt; was calling &lt;code&gt;run_with_tools(prompt=..., system=...)&lt;/code&gt; and expecting a &lt;code&gt;(text, tool_log)&lt;/code&gt; tuple back. &lt;code&gt;llm.py&lt;/code&gt; was written to accept &lt;code&gt;messages=[]&lt;/code&gt; and return a dict. Classic interface mismatch between two files written in isolation.&lt;/p&gt;

&lt;p&gt;Every bug from this — the &lt;code&gt;prompt&lt;/code&gt; vs &lt;code&gt;messages&lt;/code&gt; confusion, the &lt;code&gt;system&lt;/code&gt; kwarg error, the tuple vs dict return type — cost me hours of debugging that a typed interface contract would have caught in seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Python 3.14 on Render
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pydantic-core&lt;/code&gt; failed to build because no wheel exists for Python 3.14. Fix: force &lt;code&gt;PYTHON_VERSION=3.11.9&lt;/code&gt; in Render's environment variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're deploying to Render: always pin your Python version explicitly.&lt;/strong&gt; Don't trust Render's default.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Cached ML Model on Render
&lt;/h3&gt;

&lt;p&gt;After rebalancing the scoring weights in &lt;code&gt;scorer.py&lt;/code&gt;, the old &lt;code&gt;model.pkl&lt;/code&gt; was still cached on disk. The score stayed stuck at 19/100 until I added &lt;code&gt;rm -f ml/model.pkl&lt;/code&gt; to the build command to force a retrain on every deploy.&lt;/p&gt;

&lt;p&gt;This one was subtle. The code was right. The model was wrong. Nothing in the logs told me the model was stale.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Define the LLM interface contract on day one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The biggest source of bugs was &lt;code&gt;graph.py&lt;/code&gt; and &lt;code&gt;llm.py&lt;/code&gt; making different assumptions about function signatures, return types, and argument names — and those assumptions were never written down anywhere.&lt;/p&gt;

&lt;p&gt;If I rebuilt SalesAgent today, the first file I'd create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# contracts.py — written before any other code
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_with_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run LLM with tool-calling. Returns (response_text, tool_call_log).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Simple chat completion. Returns response string.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One typed file, agreed upfront. Every bug from the interface mismatch would have been caught before a single line of agent logic was written.&lt;/p&gt;

&lt;p&gt;Beyond that — in a production version, I'd:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;strong&gt;conversation memory&lt;/strong&gt; so the agent learns from past outreach (what worked, what didn't)&lt;/li&gt;
&lt;li&gt;Replace synthetic training data with &lt;strong&gt;real CRM data&lt;/strong&gt; (won/lost deals) for the scorer&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;email open tracking&lt;/strong&gt; to close the feedback loop and retrain the scorer on outcomes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://salesagent-theta.vercel.app" rel="noopener noreferrer"&gt;salesagent-theta.vercel.app&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ayush-s-tomar/salesagent" rel="noopener noreferrer"&gt;github.com/ayush-s-tomar/salesagent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Paste any LinkedIn URL and see what it generates. The email quality varies with how much Tavily finds — the more public news about a company, the better the output.&lt;/p&gt;

&lt;p&gt;If you're building something similar or have feedback on the ML scoring approach, I'd genuinely like to hear it — connect with me on &lt;a href="https://linkedin.com/in/ayushsinghtomar" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stack: LangGraph · FastAPI · React · scikit-learn · Groq LLaMA 3.1 · Tavily · Render · Vercel&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: #ai #python #machinelearning #langchain #buildinpublic&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>langchain</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
