Skip to content
SHASHWAT // SYSTEM ARCHIVE
SYSTEM.ARTICLE

Building a Social-to-Lead AI Agent with LangGraph

avatarShashwat Sharma
8 min read

Building a Social-to-Lead AI Agent with LangGraph

Most inbound interest from social media dies in a DM thread because no one follows up fast enough or asks the right questions in the right order. I built AutoStream — a conversational AI agent that qualifies visitors and captures their contact details through natural multi-turn dialogue, without a human sales rep on the other end. It runs on a LangGraph state graph, a local RAG pipeline, and Llama 3.3 70B served through Groq.


What It Does and Who It's For

AutoStream is a SaaS product (fictional, built for an ML engineering assignment) that automates video editing for content creators. The agent's job is to sit at the top of the funnel — on a website chat widget, a WhatsApp number, or a social DM — and convert interest into a captured lead record.

The agent classifies each incoming message into one of three intents: greeting, product inquiry, or high-intent purchase signal. Depending on the classification, it either answers a product question by searching a knowledge base, or starts collecting the three fields needed to capture a lead: name, email, and content platform.

It only fires the lead-capture tool once all three fields are confirmed. No partial captures, no duplicate entries.

The target user is a small SaaS or creator-tools company that gets inbound interest through social channels but doesn't have a sales team responding in real time.

The Problem That Made Me Build It

The assignment brief was to build a single-agent workflow that could handle a complete sales qualification loop — intent detection, product Q&A, data collection, and tool execution — without losing state between turns.

The interesting constraint was that the agent had to behave correctly across an arbitrary number of turns. It couldn't forget that it had already asked for a name. It couldn't fire the capture tool before all fields were in hand. And it couldn't fire it twice.

A simple prompt-and-reply loop with no persistent state would break on the second or third message. I needed a structure that made the flow explicit rather than hoping the LLM would track it from context alone.

Architecture: How It Works

The core is a LangGraph StateGraph with two nodes: an agent node and a tool node.

START[agent node] → has tool calls?[tool node][agent node]END
                     ↘ no tool calls  → END

The agent node sends the current state — including the full message history — to the LLM. If the LLM emits tool calls, the graph routes to the tool node, which executes them and appends the results back to the message list. The agent node then runs again and generates the final reply. If there are no tool calls, the graph exits.

State is a typed dictionary called AgentState:

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    intent: str
    lead_name: str
    lead_email: str
    lead_platform: str
    lead_captured: bool

The add_messages reducer appends each new message rather than overwriting the list. This means the LLM always receives the full conversation history on every turn — no summarization, no windowing.

The RAG pipeline lives in rag.py. At query time it chunks the knowledge base JSON into topic segments — pricing, policies, FAQs — and scores each chunk by keyword overlap with the user's message. The top 3 chunks are returned as context to the LLM through a search_knowledge_base tool call.

📝Note

There's no vector database here. The knowledge base is small enough that keyword overlap scoring is accurate and the latency of a local JSON scan is negligible. For a production KB with hundreds of articles, you'd want embeddings.

The web UI is a Gradio interface that wraps the agent in a chat component. It runs at localhost:7860 and calls the same LangGraph logic as the CLI mode.

The Hardest Technical Decision

The choice between LangGraph and AutoGen.

AutoGen is built around multi-agent conversations where several agents talk to each other to solve a problem. That model is powerful when the problem genuinely requires collaborative reasoning — planning, critique, verification across agents. But this workflow is linear and single-agent: classify intent, optionally search the KB, collect fields one at a time, capture the lead.

Plugging that into AutoGen's conversation model would mean engineering around a framework that wasn't designed for it. You'd be managing which synthetic "agent" speaks when, suppressing unnecessary back-and-forth, and trying to make something stateful out of a system that treats each exchange as a new negotiation.

LangGraph gives you explicit nodes and edges. You define exactly when the tool node runs, when the agent node runs, and what conditions cause the graph to exit. The routing logic is code, not a prompt.

💡Insight

The rule I've started applying: use AutoGen (or similar) when you want agents to discover a solution through dialogue. Use LangGraph when you know the steps and want to enforce them deterministically.

The lead_captured boolean was a direct product of this thinking. Once the capture tool fires, that flag is set in state. The conditional edge checks it before allowing another tool call cycle. The LLM cannot trigger a second capture regardless of what it generates.

What I Measured

The evaluation checklist for the assignment gave me concrete pass/fail criteria rather than continuous metrics.

The agent maintained coherent state across 6 turns in every test conversation I ran — the minimum the spec required was 5. It never asked for a field it had already received. It never fired mock_lead_capture() before all three fields were present. It never fired it twice in the same session.

RAG retrieval returned the correct chunk category (pricing, policy, or FAQ) for every test query against the 3-category knowledge base. With only 3 categories and keyword scoring, that's not a surprising result — but it confirms the pipeline isn't misrouting.

Intent classification was accurate across the test cases: greetings routed to welcome responses, pricing questions triggered KB search, and explicit purchase signals triggered the lead collection flow.

⚠️Warning

These are pass/fail checks on a small, controlled test set — not benchmarks. A real deployment would need an eval set of at least a few hundred conversations with labelled intents to measure classification accuracy meaningfully.

End-to-end latency for a single turn, including the Groq API call, was consistently under 2 seconds in local testing. Groq's inference speed for Llama 3.3 70B was the biggest factor here — it's noticeably faster than running the same model through a slower API.

What I'd Do Differently

The knowledge base is a flat JSON file with keyword scoring. It works for a small, stable KB. But as soon as the product has more than a few dozen articles, keyword overlap fails — it can't handle synonyms, paraphrasing, or queries that don't share exact words with the document. I'd swap in a proper embedding-based retrieval layer from the start.

The intent classifier is the LLM itself, via the system prompt. That means intent detection costs a full API round-trip even for a simple "hi." A lightweight local classifier — even a fine-tuned BERT with three output classes — would cut latency and cost on the classification step while reserving the 70B call for generation.

The state is in-memory and lives only for the duration of the Python process. For a real deployment on WhatsApp or any webhook-driven channel, the conversation state needs to be persisted externally, keyed by user ID. The README sketches a Redis approach, but the implementation isn't there. I'd build that before connecting any real messaging channel.

There's also no retry logic on tool failures, no handling for malformed LLM outputs, and no fallback if the Groq API is down. Those would all need to be production concerns.

Conclusion

The project delivered what it set out to: a single-agent workflow that holds conversation state, answers product questions from a knowledge base, and captures a lead only when all required fields are present. The LangGraph model made the routing explicit and the lead_captured gate made the capture idempotent.

The interesting engineering was less about the LLM and more about the state machine around it — deciding which conditions should be in code versus which should be left to the model's judgment. For anything involving order of operations or preventing duplicate side effects, the answer was code.

The WhatsApp deployment path is straightforward from here: replace the CLI I/O with a Meta webhook handler, persist AgentState in Redis keyed by phone number, and the rest of the graph logic runs unchanged.