# negoT8 — Milestone Execution Guide (v2)

> **Follow this document linearly. Do NOT skip ahead. Each milestone has a success test — pass it before moving on.**
>
> Every milestone references the exact section of the Build Guide v2 (build-guide-v2.md) you need. Open both docs side by side.
>
> **v2 Changes:** Tavily search replaces DuckDuckGo, ElevenLabs voice summaries, agent personality system, UPI deep links, negotiation analytics dashboard. All integrated into the milestone flow.

---

## Pre-Hackathon Checklist (Do this BEFORE the timer starts)

```
□ Gemini API key generated → https://aistudio.google.com/apikey
□ 3 Telegram bots created via @BotFather (Bot A, Bot B, Bot C for group demo)
□ Bot tokens saved in a notes app (you'll paste into .env later)
□ Tavily API key generated → https://app.tavily.com/home (FREE, no credit card)
□ ElevenLabs API key generated → https://elevenlabs.io/app/settings/api-keys ($20 credit)
□ Noted down ElevenLabs voice IDs for 2-3 voices (browse at elevenlabs.io/voice-library)
□ Node.js 18+ installed → node --version
□ Python 3.11+ installed → python3 --version
□ VS Code / Cursor with Copilot or AI coding agent ready
□ Both team members have Telegram on their phones
□ At least one phone has a UPI app (GPay/PhonePe) for testing UPI deep links
□ Laptop charger, phone chargers, hotspot backup
□ This guide + Build Guide v2 open on a second screen or printed
```

---

## MILESTONE 1: Project Skeleton + API Verification
**⏰ Time: Hour 0 → Hour 1 (60 min)**
**👤 Who: DEV A + DEV B simultaneously**

### DEV A Tasks (Backend Foundation)

**Step 1: Create project structure**
```bash
mkdir -p negot8/backend/{agents,protocol,features,telegram,tools,voice,personality}
cd negot8
python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
```

**Step 2: Install dependencies**
```bash
pip install fastapi uvicorn "python-telegram-bot[all]" google-generativeai httpx pydantic python-dotenv aiosqlite python-socketio elevenlabs tavily-python
```

**Step 3: Create .env file**
```bash
# Create .env in project root (negot8/.env)
```
```env
GEMINI_API_KEY=your_key_here
TELEGRAM_BOT_TOKEN_A=token_from_botfather
TELEGRAM_BOT_TOKEN_B=token_from_botfather
TELEGRAM_BOT_TOKEN_C=token_from_botfather
TAVILY_API_KEY=tvly-your_key_here
ELEVENLABS_API_KEY=your_elevenlabs_key_here
NEXT_PUBLIC_API_URL=http://localhost:8000
DATABASE_PATH=negot8.db
```

**Step 4: Create config.py**
```python
# backend/config.py
import os
from dotenv import load_dotenv

load_dotenv()

GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
TELEGRAM_BOT_TOKEN_A = os.getenv("TELEGRAM_BOT_TOKEN_A")
TELEGRAM_BOT_TOKEN_B = os.getenv("TELEGRAM_BOT_TOKEN_B")
TELEGRAM_BOT_TOKEN_C = os.getenv("TELEGRAM_BOT_TOKEN_C")
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
DATABASE_PATH = os.getenv("DATABASE_PATH", "negot8.db")
API_URL = os.getenv("NEXT_PUBLIC_API_URL", "http://localhost:8000")

# ElevenLabs voice IDs (pick from https://elevenlabs.io/voice-library)
VOICE_ID_AGENT_A = "pNInz6obpgDQGcFmaJgB"  # Adam — clear male
VOICE_ID_AGENT_B = "21m00Tcm4TlvDq8ikWAM"  # Rachel — clear female
VOICE_ID_AGENT_C = "AZnzlk1XvdvUeBnXmlld"  # Domi — for 3rd agent
```

**Step 5: Initialize database (v2 schema with analytics + personality)**
→ **Reference: Build Guide v2 Section 5 (Database Schema)**
```python
# backend/database.py
import aiosqlite
import json
import uuid
from datetime import datetime
from config import DATABASE_PATH

async def init_db():
    async with aiosqlite.connect(DATABASE_PATH) as db:
        await db.executescript('''
            CREATE TABLE IF NOT EXISTS users (
                telegram_id INTEGER PRIMARY KEY,
                username TEXT,
                display_name TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                preferences_json TEXT DEFAULT '{}',
                personality TEXT DEFAULT 'balanced',
                voice_id TEXT DEFAULT 'pNInz6obpgDQGcFmaJgB'
            );
            CREATE TABLE IF NOT EXISTS negotiations (
                id TEXT PRIMARY KEY,
                feature_type TEXT NOT NULL,
                status TEXT DEFAULT 'pending',
                initiator_id INTEGER,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                resolved_at TIMESTAMP,
                resolution_json TEXT,
                voice_summary_file TEXT
            );
            CREATE TABLE IF NOT EXISTS participants (
                negotiation_id TEXT,
                user_id INTEGER,
                preferences_json TEXT,
                satisfaction_score REAL,
                personality_used TEXT,
                PRIMARY KEY (negotiation_id, user_id)
            );
            CREATE TABLE IF NOT EXISTS rounds (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                negotiation_id TEXT,
                round_number INTEGER NOT NULL,
                proposer_id INTEGER,
                proposal_json TEXT NOT NULL,
                response_type TEXT,
                response_json TEXT,
                reasoning TEXT,
                satisfaction_a REAL,
                satisfaction_b REAL,
                concessions_made TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );
            CREATE TABLE IF NOT EXISTS tool_calls (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                negotiation_id TEXT,
                tool_name TEXT NOT NULL,
                input_json TEXT,
                output_json TEXT,
                called_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );
            CREATE TABLE IF NOT EXISTS negotiation_analytics (
                negotiation_id TEXT PRIMARY KEY,
                satisfaction_timeline TEXT,
                concession_log TEXT,
                fairness_score REAL,
                total_concessions_a INTEGER,
                total_concessions_b INTEGER,
                computed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );
        ''')
        await db.commit()
    print("✅ Database initialized (v2 schema)")

# ─── Helper functions ───

async def create_user(telegram_id, username, display_name):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        await db.execute(
            "INSERT OR IGNORE INTO users (telegram_id, username, display_name) VALUES (?, ?, ?)",
            (telegram_id, username, display_name)
        )
        await db.commit()

async def get_user(telegram_id):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        db.row_factory = aiosqlite.Row
        cursor = await db.execute("SELECT * FROM users WHERE telegram_id = ?", (telegram_id,))
        return await cursor.fetchone()

async def update_user_personality(telegram_id, personality):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        await db.execute(
            "UPDATE users SET personality = ? WHERE telegram_id = ?",
            (personality, telegram_id)
        )
        await db.commit()

async def create_negotiation(feature_type, initiator_id):
    neg_id = str(uuid.uuid4())[:8]
    async with aiosqlite.connect(DATABASE_PATH) as db:
        await db.execute(
            "INSERT INTO negotiations (id, feature_type, initiator_id) VALUES (?, ?, ?)",
            (neg_id, feature_type, initiator_id)
        )
        await db.commit()
    return neg_id

async def add_participant(negotiation_id, user_id, preferences, personality_used="balanced"):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        await db.execute(
            "INSERT OR REPLACE INTO participants (negotiation_id, user_id, preferences_json, personality_used) VALUES (?, ?, ?, ?)",
            (negotiation_id, user_id, json.dumps(preferences), personality_used)
        )
        await db.commit()

async def get_participants(negotiation_id):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        db.row_factory = aiosqlite.Row
        cursor = await db.execute(
            "SELECT * FROM participants WHERE negotiation_id = ?", (negotiation_id,)
        )
        return await cursor.fetchall()

async def save_round(negotiation_id, round_number, proposer_id, proposal,
                     response_type=None, response=None, reasoning=None,
                     satisfaction_a=None, satisfaction_b=None, concessions_made=None):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        await db.execute(
            """INSERT INTO rounds (negotiation_id, round_number, proposer_id, proposal_json,
               response_type, response_json, reasoning, satisfaction_a, satisfaction_b, concessions_made)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (negotiation_id, round_number, proposer_id, json.dumps(proposal),
             response_type, json.dumps(response) if response else None, reasoning,
             satisfaction_a, satisfaction_b,
             json.dumps(concessions_made) if concessions_made else None)
        )
        await db.commit()

async def get_rounds(negotiation_id):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        db.row_factory = aiosqlite.Row
        cursor = await db.execute(
            "SELECT * FROM rounds WHERE negotiation_id = ? ORDER BY round_number", (negotiation_id,)
        )
        return await cursor.fetchall()

async def update_negotiation_status(negotiation_id, status, resolution=None, voice_file=None):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        if resolution:
            await db.execute(
                "UPDATE negotiations SET status = ?, resolved_at = ?, resolution_json = ?, voice_summary_file = ? WHERE id = ?",
                (status, datetime.now().isoformat(), json.dumps(resolution), voice_file, negotiation_id)
            )
        else:
            await db.execute(
                "UPDATE negotiations SET status = ? WHERE id = ?", (status, negotiation_id)
            )
        await db.commit()

async def store_analytics(analytics: dict):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        await db.execute(
            """INSERT OR REPLACE INTO negotiation_analytics
               (negotiation_id, satisfaction_timeline, concession_log, fairness_score,
                total_concessions_a, total_concessions_b, computed_at)
               VALUES (?, ?, ?, ?, ?, ?, ?)""",
            (analytics["negotiation_id"], analytics["satisfaction_timeline"],
             analytics["concession_log"], analytics["fairness_score"],
             analytics["total_concessions_a"], analytics["total_concessions_b"],
             datetime.now().isoformat())
        )
        await db.commit()

async def get_analytics(negotiation_id):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        db.row_factory = aiosqlite.Row
        cursor = await db.execute(
            "SELECT * FROM negotiation_analytics WHERE negotiation_id = ?", (negotiation_id,)
        )
        return await cursor.fetchone()
```

**Step 6: Verify ALL external APIs work**
```python
# test_apis.py (run this standalone — tests Gemini, Tavily, ElevenLabs)
import os
from dotenv import load_dotenv
load_dotenv()

# ─── Test 1: Gemini ───
print("Testing Gemini...")
import google.generativeai as genai
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
model = genai.GenerativeModel(
    model_name="gemini-3-flash-preview",
    generation_config=genai.GenerationConfig(
        response_mime_type="application/json",
        temperature=0.7,
    )
)
response = model.generate_content(
    'Return JSON: {"status": "ok", "message": "Gemini works"}'
)
print(f"  Gemini: {response.text}")

# ─── Test 2: Tavily ───
print("\nTesting Tavily...")
from tavily import TavilyClient
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
result = tavily.search("best Thai restaurants in Bandra Mumbai", include_answer=True, max_results=2)
print(f"  Tavily answer: {result.get('answer', 'No answer')[:150]}")
print(f"  Results count: {len(result.get('results', []))}")

# ─── Test 3: ElevenLabs ───
print("\nTesting ElevenLabs...")
import httpx
resp = httpx.post(
    "https://api.elevenlabs.io/v1/text-to-speech/pNInz6obpgDQGcFmaJgB",
    headers={"xi-api-key": os.getenv("ELEVENLABS_API_KEY"), "Content-Type": "application/json"},
    json={"text": "Hello from negoT8!", "model_id": "eleven_flash_v2_5",
          "voice_settings": {"stability": 0.5, "similarity_boost": 0.75}},
    timeout=15.0
)
if resp.status_code == 200:
    with open("test_voice.mp3", "wb") as f:
        f.write(resp.content)
    print(f"  ElevenLabs: ✅ Saved test_voice.mp3 ({len(resp.content)} bytes)")
else:
    print(f"  ElevenLabs: ❌ {resp.status_code} — {resp.text[:200]}")

print("\n✅ All API tests complete!")
```
```bash
python test_apis.py
```

### DEV B Tasks (Frontend + Telegram Shell)

**Step 1: Create Next.js dashboard**
```bash
cd negot8
npx create-next-app@latest dashboard --typescript --tailwind --app --no-eslint --no-src-dir
cd dashboard
npm install socket.io-client recharts lucide-react
```

**Step 2: Create basic layout**
```typescript
// dashboard/app/layout.tsx — just make sure it loads
// dashboard/app/page.tsx — simple "negoT8 Dashboard" heading
```

**Step 3: Verify Telegram bot responds**
```python
# test_telegram.py (run standalone)
import asyncio
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
import os
from dotenv import load_dotenv

load_dotenv()

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text("🤖 negoT8 Bot A is alive!")

app = Application.builder().token(os.getenv("TELEGRAM_BOT_TOKEN_A")).build()
app.add_handler(CommandHandler("start", start))

print("Bot A running... Press Ctrl+C to stop")
app.run_polling()
```
```bash
python test_telegram.py
# Open Telegram, find your bot, send /start
```

### ✅ MILESTONE 1 SUCCESS TEST
```
□ python test_apis.py → Gemini returns JSON, Tavily returns search results, ElevenLabs generates MP3
□ python test_telegram.py → bot responds to /start on Telegram
□ Database file negot8.db exists with 6 tables (users, negotiations, participants, rounds, tool_calls, negotiation_analytics)
□ Next.js dashboard loads at http://localhost:3000
□ .env has all 7 keys filled in (Gemini, 3x Telegram, Tavily, ElevenLabs, API URL)
□ test_voice.mp3 plays audio when opened
```

**🚫 DO NOT PROCEED if any test fails. Debug now — these are your foundations.**

---

## MILESTONE 2: Base Agent + Preference Extraction + Personality System
**⏰ Time: Hour 1 → Hour 3 (120 min)**
**👤 Who: DEV A builds agents + personality, DEV B builds Telegram integration**

### DEV A: Build the Agent System

→ **Reference: Build Guide v2 Section 6 (System Prompts) + Section 12 (Gemini Wrapper) + Section 14 (Personality)**

**Step 1: Create BaseAgent class**
```python
# backend/agents/base_agent.py
import google.generativeai as genai
import json
from config import GEMINI_API_KEY

genai.configure(api_key=GEMINI_API_KEY)

class BaseAgent:
    def __init__(self, system_prompt: str, model_name: str = "gemini-3-flash-preview"):
        self.system_prompt = system_prompt
        self.model = genai.GenerativeModel(
            model_name=model_name,
            system_instruction=system_prompt,
            generation_config=genai.GenerationConfig(
                response_mime_type="application/json",
                temperature=0.7,
            )
        )

    async def call(self, user_prompt: str, context: dict = None) -> dict:
        full_prompt = user_prompt
        if context:
            full_prompt = f"CONTEXT:\n{json.dumps(context, indent=2)}\n\nTASK:\n{user_prompt}"

        try:
            response = self.model.generate_content(full_prompt)
            return json.loads(response.text)
        except json.JSONDecodeError:
            # Fallback: extract JSON from response
            text = response.text
            start = text.find('{')
            end = text.rfind('}') + 1
            if start != -1 and end > start:
                return json.loads(text[start:end])
            return {"error": "Could not parse JSON", "raw": text[:500]}
        except Exception as e:
            return {"error": str(e)}
```

**Step 2: Create PersonalAgent (preference extractor)**
→ **Reference: Build Guide v2 Section 6 (Personal Agent System Prompt)**

```python
# backend/agents/personal_agent.py
from agents.base_agent import BaseAgent

PERSONAL_AGENT_PROMPT = """You are the Personal Agent for negoT8. Your job is to understand what your human wants and extract structured preferences from their natural language message.

When your human sends a message about coordinating with another person, extract:

ALWAYS respond in this exact JSON format:
{
  "feature_type": "scheduling|expenses|freelance|roommate|trip|marketplace|collaborative|conflict|generic",
  "goal": "string describing what they want to achieve",
  "constraints": [
    {"type": "string", "value": "any", "description": "string", "hard": true/false}
  ],
  "preferences": [
    {"type": "string", "value": "any", "priority": "high|medium|low", "description": "string"}
  ],
  "relationship": "friend|colleague|client|vendor|stranger|roommate|family",
  "tone": "firm|balanced|flexible|friendly",
  "raw_details": {}
}

FEATURE TYPE CLASSIFICATION:
- "scheduling" → meeting times, calls, coffee, appointments
- "expenses" → splitting costs, bills, trip expenses, shared purchases
- "freelance" → project scope, budget, timeline, client-freelancer deals
- "roommate" → shared living decisions (wifi, chores, furniture, rules)
- "trip" → planning trips, vacations, getaways with dates/budget/destination
- "marketplace" → buying/selling items between people
- "collaborative" → choosing restaurants, movies, activities, gifts together
- "conflict" → disputes, disagreements, resource sharing conflicts
- "generic" → ANYTHING that doesn't fit above but involves coordination between people

CRITICAL: For "raw_details", include ALL specific numbers, dates, items, names, UPI IDs mentioned.
Extract EVERY piece of information. Miss nothing.

If the message is ambiguous about the coordination type, classify as "generic".
NEVER say you can't handle a request. ANY coordination between people is within your capability."""

class PersonalAgent(BaseAgent):
    def __init__(self):
        super().__init__(system_prompt=PERSONAL_AGENT_PROMPT)

    async def extract_preferences(self, user_message: str, user_id: int = None) -> dict:
        result = await self.call(
            user_prompt=f"Extract structured preferences from this message:\n\n\"{user_message}\"",
            context={"user_id": user_id} if user_id else None
        )
        return result
```

**Step 3: Create Personality Profiles**
→ **Reference: Build Guide v2 Section 14 (Agent Personality System)**

```python
# backend/personality/profiles.py

PERSONALITY_MODIFIERS = {
    "aggressive": """
PERSONALITY: AGGRESSIVE HAGGLER
- Open with ambitious proposals strongly in your human's favor
- Concede slowly and in small increments
- Use anchoring: start far from center, pull the other side toward you
- Frame every concession as a major sacrifice
- Maintain firm positions on medium-priority items, not just hard constraints
- Only make final concessions when no further value can be extracted
""",
    "people_pleaser": """
PERSONALITY: PEOPLE PLEASER
- Open with balanced proposals showing good faith
- Concede quickly when the other side has reasonable arguments
- Prioritize maintaining a positive relationship over winning every point
- Accept proposals with satisfaction scores as low as 55 (normally 70+)
- Avoid letting negotiations drag past 3 rounds if possible
""",
    "analytical": """
PERSONALITY: DATA-DRIVEN ANALYST
- Open with proposals backed by market data, averages, and benchmarks
- Request tool calls (web search) to verify claims and prices before countering
- Frame all arguments with numbers: "market rate is X", "fair value is Z"
- Concede only when the other side presents data that contradicts your position
- Include price comparisons and market references in every proposal
""",
    "empathetic": """
PERSONALITY: EMPATHETIC MEDIATOR
- Acknowledge the other side's likely concerns in every proposal
- Identify underlying interests behind positions
- Propose creative win-wins that satisfy both sides' underlying needs
- Offer concessions proactively when you sense the other side values something more
- Focus on expanding the pie rather than dividing it
""",
    "balanced": """
PERSONALITY: BALANCED NEGOTIATOR
- Open with reasonable proposals near the midpoint of both sides' positions
- Concede at a moderate pace, matching the other side's concession rate
- Aim for proposals that score 70+ satisfaction for both sides
- Use a mix of data and relationship awareness in arguments
""",
}

def get_personality_modifier(personality: str) -> str:
    return PERSONALITY_MODIFIERS.get(personality, PERSONALITY_MODIFIERS["balanced"])
```

**Step 4: Test PersonalAgent + verify personality profiles load**
```python
# test_personal_agent.py
import asyncio
import json
from agents.personal_agent import PersonalAgent
from personality.profiles import get_personality_modifier

async def test():
    agent = PersonalAgent()

    # Test 1: Scheduling
    r1 = await agent.extract_preferences("Find time for coffee with Priya next week. I'm free Mon-Wed afternoons.")
    print("TEST 1 (scheduling):", r1.get("feature_type"), "✅" if r1.get("feature_type") == "scheduling" else "❌")

    # Test 2: Expenses (with UPI mention)
    r2 = await agent.extract_preferences("Split our Goa trip costs. I paid 12K hotel, 3K fuel. Fuel should be 60-40 since I drove. My UPI is rahul@paytm")
    print("TEST 2 (expenses):", r2.get("feature_type"), "✅" if r2.get("feature_type") == "expenses" else "❌")
    print("  UPI extracted:", "rahul@paytm" in json.dumps(r2), "✅" if "rahul@paytm" in json.dumps(r2) else "⚠️ UPI not found")

    # Test 3: Marketplace
    r3 = await agent.extract_preferences("I want to sell my PS5 to this guy. Asking 35K, minimum 30K, has 2 controllers.")
    print("TEST 3 (marketplace):", r3.get("feature_type"), "✅" if r3.get("feature_type") == "marketplace" else "❌")

    # Test 4: Generic
    r4 = await agent.extract_preferences("Figure out with @dave who brings what to the BBQ party Saturday")
    print("TEST 4 (generic):", r4.get("feature_type"), "✅" if r4.get("feature_type") in ("generic", "collaborative") else "❌")

    # Test 5: Personality profiles load
    for p in ["aggressive", "people_pleaser", "analytical", "empathetic", "balanced"]:
        mod = get_personality_modifier(p)
        print(f"PERSONALITY {p}: {'✅' if len(mod) > 50 else '❌'} ({len(mod)} chars)")

    print("\nFull output Test 2:", json.dumps(r2, indent=2))

asyncio.run(test())
```
```bash
cd backend
python test_personal_agent.py
```

### DEV B: Telegram Bot with Preference Flow + Personality Command

→ **Reference: Build Guide v2 Section 9 (Telegram Bot Implementation)**

**Step 1: Build full Telegram bot with /personality command**
```python
# backend/telegram/bot.py
import asyncio
from telegram import Update, BotCommand, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
    Application, CommandHandler, MessageHandler, CallbackQueryHandler,
    filters, ContextTypes, ConversationHandler
)
from agents.personal_agent import PersonalAgent
import database as db
from config import TELEGRAM_BOT_TOKEN_A, TELEGRAM_BOT_TOKEN_B

personal_agent = PersonalAgent()

# Conversation states
AWAITING_PREFERENCES = 1

# Store for linking negotiations between bots
pending_coordinations = {}

async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user = update.effective_user
    await db.create_user(user.id, user.username, user.first_name)
    await update.message.reply_text(
        f"🤖 *Welcome to negoT8, {user.first_name}!*\n\n"
        "I'm your personal AI agent. When you need to coordinate with someone, "
        "I'll talk to their agent and we'll figure it out together.\n\n"
        "🎭 Set your agent's style: /personality\n"
        "🤝 Start coordinating: `/coordinate @friend`\n\n"
        "Then just tell me what you need in plain English.",
        parse_mode="Markdown"
    )

async def personality_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Let user choose their agent's negotiation personality."""
    keyboard = InlineKeyboardMarkup([
        [InlineKeyboardButton("😤 Aggressive Haggler", callback_data="personality_aggressive")],
        [InlineKeyboardButton("🤝 People Pleaser", callback_data="personality_people_pleaser")],
        [InlineKeyboardButton("📊 Data-Driven Analyst", callback_data="personality_analytical")],
        [InlineKeyboardButton("💚 Empathetic Mediator", callback_data="personality_empathetic")],
        [InlineKeyboardButton("⚖️ Balanced (Default)", callback_data="personality_balanced")],
    ])

    await update.message.reply_text(
        "🎭 *Choose your agent's negotiation personality:*\n\n"
        "This changes HOW your agent negotiates — not what it negotiates for.\n\n"
        "😤 *Aggressive* — Pushes hard, concedes slowly\n"
        "🤝 *People Pleaser* — Concedes fast, preserves relationship\n"
        "📊 *Analytical* — Cites data, logical arguments\n"
        "💚 *Empathetic* — Creative win-wins, understands both sides\n"
        "⚖️ *Balanced* — Middle-ground approach",
        reply_markup=keyboard,
        parse_mode="Markdown"
    )

async def personality_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle personality selection."""
    query = update.callback_query
    await query.answer()
    personality = query.data.replace("personality_", "")
    user_id = query.from_user.id
    await db.update_user_personality(user_id, personality)

    labels = {
        "aggressive": "😤 Aggressive Haggler",
        "people_pleaser": "🤝 People Pleaser",
        "analytical": "📊 Data-Driven Analyst",
        "empathetic": "💚 Empathetic Mediator",
        "balanced": "⚖️ Balanced"
    }
    await query.edit_message_text(
        f"✅ Agent personality set to: {labels[personality]}\n\n"
        "Your agent will use this style in all future negotiations.\n"
        "Change anytime with /personality"
    )

async def coordinate_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not context.args:
        await update.message.reply_text(
            "🤝 *Who do you want to coordinate with?*\n\n"
            "Usage: `/coordinate @username`\n\n"
            "Then describe what you need. Examples:\n"
            "• Find time for coffee next week\n"
            "• Split our trip expenses (include your UPI ID!)\n"
            "• Negotiate project scope and budget\n"
            "• Plan a weekend trip\n"
            "• Decide where to eat tonight\n"
            "• ...literally anything that needs agreement!",
            parse_mode="Markdown"
        )
        return ConversationHandler.END

    counterparty = context.args[0].replace("@", "")
    context.user_data["counterparty"] = counterparty

    await update.message.reply_text(
        f"🤖 Got it! I'll coordinate with *@{counterparty}*'s agent.\n\n"
        "Now tell me what you need — describe it naturally.\n"
        "Include any specific numbers, dates, or constraints.\n"
        "💡 For expenses: mention your UPI ID for auto-payment links!",
        parse_mode="Markdown"
    )
    return AWAITING_PREFERENCES

async def receive_preferences(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_message = update.message.text
    user = update.effective_user
    counterparty = context.user_data.get("counterparty", "unknown")

    await update.message.chat.send_action("typing")

    preferences = await personal_agent.extract_preferences(user_message, user.id)

    if "error" in preferences:
        await update.message.reply_text(
            "⚠️ I had trouble understanding that. Could you rephrase?\n"
            "Try to include specific details like dates, amounts, or preferences."
        )
        return AWAITING_PREFERENCES

    feature_type = preferences.get("feature_type", "generic")

    # Get user's personality
    user_data = await db.get_user(user.id)
    personality = dict(user_data).get("personality", "balanced") if user_data else "balanced"

    negotiation_id = await db.create_negotiation(feature_type, user.id)
    await db.add_participant(negotiation_id, user.id, preferences, personality_used=personality)

    pending_coordinations[counterparty] = {
        "negotiation_id": negotiation_id,
        "initiator_id": user.id,
        "initiator_name": user.first_name,
        "feature_type": feature_type,
        "preferences_a": preferences,
        "personality_a": personality
    }

    feature_labels = {
        "scheduling": "📅 Meeting Scheduling", "expenses": "💰 Expense Splitting",
        "freelance": "💼 Project Negotiation", "roommate": "🏠 Roommate Decision",
        "trip": "✈️ Trip Planning", "marketplace": "🛒 Buy/Sell Deal",
        "collaborative": "🍕 Joint Decision", "conflict": "⚖️ Conflict Resolution",
        "generic": "🤝 Coordination"
    }
    personality_emoji = {"aggressive": "😤", "people_pleaser": "🤝", "analytical": "📊", "empathetic": "💚", "balanced": "⚖️"}

    await update.message.reply_text(
        f"✅ *Preferences captured!*\n\n"
        f"📋 Type: {feature_labels.get(feature_type, '🤝 Coordination')}\n"
        f"🤝 With: @{counterparty}\n"
        f"🎭 Agent style: {personality_emoji.get(personality, '⚖️')} {personality.replace('_', ' ').title()}\n"
        f"🆔 Negotiation: `{negotiation_id}`\n\n"
        f"I've notified @{counterparty}'s agent.\n"
        f"Once they share their side, our agents will negotiate automatically.\n\n"
        f"_I'll send you real-time updates + a voice summary at the end!_ 🤖↔️🤖🔊",
        parse_mode="Markdown"
    )
    return ConversationHandler.END

async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        "🤖 *negoT8 — How it works*\n\n"
        "1️⃣ Set style: /personality\n"
        "2️⃣ Start: `/coordinate @person`\n"
        "3️⃣ Describe what you need\n"
        "4️⃣ Their agent asks them for their side\n"
        "5️⃣ 🤖↔️🤖 Agents negotiate (you watch live)\n"
        "6️⃣ ✅ Both get: text + voice note + payment link\n\n"
        "*Works for:* Scheduling, expenses, trip planning, buying/selling, "
        "restaurant picks, roommate decisions, conflict resolution, and more!",
        parse_mode="Markdown"
    )

def create_bot(token: str) -> Application:
    """Create a bot application with all handlers."""
    app = Application.builder().token(token).build()

    conv_handler = ConversationHandler(
        entry_points=[CommandHandler("coordinate", coordinate_command)],
        states={
            AWAITING_PREFERENCES: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_preferences)]
        },
        fallbacks=[CommandHandler("cancel", lambda u, c: ConversationHandler.END)]
    )

    app.add_handler(CommandHandler("start", start_command))
    app.add_handler(CommandHandler("personality", personality_command))
    app.add_handler(CallbackQueryHandler(personality_callback, pattern="^personality_"))
    app.add_handler(conv_handler)
    app.add_handler(CommandHandler("help", help_command))

    return app
```

### ✅ MILESTONE 2 SUCCESS TEST
```
□ PersonalAgent correctly classifies all 4 test scenarios
□ PersonalAgent extracts UPI ID from expense messages
□ All 5 personality profiles load correctly (test output shows ✅ for each)
□ Telegram Bot A responds to /start, /personality (shows 5 buttons), /coordinate
□ Personality selection via inline keyboard works and updates DB
□ Preferences stored in SQLite (check: sqlite3 negot8.db "SELECT * FROM participants;")
□ pending_coordinations dict has the stored coordination data with personality
```

**🚫 If PersonalAgent misclassifies frequently:** Add 2-3 few-shot examples to the system prompt.

---

## MILESTONE 3: Negotiator Agent + Negotiation Engine + Tavily Tools
**⏰ Time: Hour 3 → Hour 6 (180 min)**
**👤 Who: DEV A builds negotiation engine with personality, DEV B builds counterparty flow + tools**

### ⚠️ THIS IS THE CRITICAL MILESTONE. The entire project depends on this working.

### DEV A: Build the Negotiation Engine (with personality injection)

→ **Reference: Build Guide v2 Section 6 (Negotiator Prompt) + Section 7 (Protocol)**

**Step 1: Create Pydantic message models**
```python
# backend/protocol/messages.py
from pydantic import BaseModel
from typing import Optional, List
from enum import Enum

class MessageType(str, Enum):
    INITIATE = "initiate"
    PROPOSAL = "proposal"
    ACCEPT = "accept"
    COUNTER = "counter"
    ESCALATE = "escalate"

class AgentMessage(BaseModel):
    message_id: str
    negotiation_id: str
    message_type: MessageType
    sender_id: int
    receiver_id: int
    round_number: int
    payload: dict
    reasoning: str = ""
    satisfaction_score: float = 0.0
    concessions_made: List[str] = []
    concessions_requested: List[str] = []
    timestamp: str = ""
```

**Step 2: Create NegotiatorAgent with personality injection**
```python
# backend/agents/negotiator_agent.py
from agents.base_agent import BaseAgent
from personality.profiles import get_personality_modifier
import json

NEGOTIATOR_BASE_PROMPT = """You are the Negotiator Agent for negoT8. You negotiate on behalf of your human to find optimal agreements with other people's agents.

{personality_modifier}

NEGOTIATION RULES:
1. You are LOYAL to your human. Their constraints (marked "hard": true) are NEVER violated.
2. You seek WIN-WIN solutions. Both parties should feel satisfied.
3. You concede on low-priority preferences first, high-priority last.
4. You MUST resolve within 5 rounds. Be efficient.
5. Accept if satisfaction >= 70%. Counter if 40-69%. Escalate if < 40% after round 3.

You MUST respond with this exact JSON:
{
  "action": "propose|counter|accept|escalate",
  "proposal": {
    "summary": "one-line description of proposal",
    "details": { ... feature-specific details ... },
    "for_party_a": "what party A gets",
    "for_party_b": "what party B gets"
  },
  "satisfaction_score": 0-100,
  "reasoning": "Why this action and proposal",
  "concessions_made": ["what you gave up this round"],
  "concessions_requested": ["what you want from them"]
}

STRATEGY BY ROUND:
- Round 1: Propose your human's ideal outcome (aim high but reasonable)
- Round 2-3: Make strategic concessions on low-priority items
- Round 4: Make final significant concession if needed
- Round 5: Accept best available OR escalate with 2-3 options for humans

IMPORTANT: Your proposal must ALWAYS include concrete specifics (numbers, dates, items).
Never propose vague things like "we'll figure it out later"."""

class NegotiatorAgent(BaseAgent):
    def __init__(self, personality: str = "balanced"):
        modifier = get_personality_modifier(personality)
        prompt = NEGOTIATOR_BASE_PROMPT.replace("{personality_modifier}", modifier)
        super().__init__(system_prompt=prompt)

    async def generate_initial_proposal(self, my_preferences: dict, feature_type: str) -> dict:
        return await self.call(
            user_prompt=f"""Generate the FIRST proposal for this {feature_type} negotiation.

My human's preferences:
{json.dumps(my_preferences, indent=2)}

This is Round 1. Propose my human's ideal outcome — aim high but stay reasonable.
The other party hasn't proposed anything yet."""
        )

    async def evaluate_and_respond(self, received_proposal: dict, my_preferences: dict, feature_type: str, round_number: int) -> dict:
        return await self.call(
            user_prompt=f"""Evaluate this proposal and respond. Round {round_number} of a {feature_type} negotiation.

RECEIVED PROPOSAL FROM OTHER AGENT:
{json.dumps(received_proposal, indent=2)}

MY HUMAN'S PREFERENCES:
{json.dumps(my_preferences, indent=2)}

Evaluate against my human's preferences. Decide: accept (satisfaction >= 70), counter (40-69), or escalate (< 40 and round >= 3).
If countering, make a strategic concession while protecting high-priority items."""
        )
```

**Step 3: Create Negotiation Engine with analytics tracking**
```python
# backend/protocol/negotiation.py
import asyncio
import json
from agents.negotiator_agent import NegotiatorAgent
import database as db

async def run_negotiation(negotiation_id: str, preferences_a: dict, preferences_b: dict,
                          user_a_id: int, user_b_id: int, feature_type: str,
                          personality_a: str = "balanced", personality_b: str = "balanced",
                          on_round_update=None, on_resolution=None):
    """
    Main negotiation loop with personality-aware agents and analytics tracking.
    """
    await db.update_negotiation_status(negotiation_id, "active")

    # Create personality-aware negotiators
    negotiator_a = NegotiatorAgent(personality=personality_a)
    negotiator_b = NegotiatorAgent(personality=personality_b)

    current_proposal = None
    max_rounds = 5
    satisfaction_timeline = []

    for round_num in range(1, max_rounds + 1):
        await asyncio.sleep(1.5)  # Rate limit protection for Gemini

        if round_num == 1:
            response = await negotiator_a.generate_initial_proposal(
                my_preferences=preferences_a, feature_type=feature_type
            )
            proposer_id = user_a_id
        elif round_num % 2 == 0:
            response = await negotiator_b.evaluate_and_respond(
                received_proposal=current_proposal, my_preferences=preferences_b,
                feature_type=feature_type, round_number=round_num
            )
            proposer_id = user_b_id
        else:
            response = await negotiator_a.evaluate_and_respond(
                received_proposal=current_proposal, my_preferences=preferences_a,
                feature_type=feature_type, round_number=round_num
            )
            proposer_id = user_a_id

        # Handle errors
        if "error" in response:
            response = {
                "action": "counter" if round_num < max_rounds else "escalate",
                "proposal": current_proposal or {"summary": "Let's discuss further", "details": {}},
                "satisfaction_score": 50, "reasoning": "Agent encountered an issue",
                "concessions_made": [], "concessions_requested": []
            }

        action = response.get("action", "counter")
        current_proposal = response.get("proposal", {})
        satisfaction = response.get("satisfaction_score", 50)
        concessions = response.get("concessions_made", [])

        # Track satisfaction for analytics
        # The proposer's score is the one returned; estimate the other party's
        if proposer_id == user_a_id:
            sat_a, sat_b = satisfaction, max(30, 100 - satisfaction * 0.4)
        else:
            sat_b, sat_a = satisfaction, max(30, 100 - satisfaction * 0.4)

        satisfaction_timeline.append({
            "round": round_num, "score_a": sat_a, "score_b": sat_b
        })

        # Save round with analytics data
        await db.save_round(
            negotiation_id=negotiation_id, round_number=round_num,
            proposer_id=proposer_id, proposal=response,
            response_type=action, reasoning=response.get("reasoning", ""),
            satisfaction_a=sat_a, satisfaction_b=sat_b,
            concessions_made=concessions
        )

        # Notify via callback
        round_data = {
            "negotiation_id": negotiation_id, "round_number": round_num,
            "action": action, "proposal": current_proposal,
            "satisfaction_score": satisfaction, "reasoning": response.get("reasoning", ""),
            "proposer_id": proposer_id,
            "satisfaction_a": sat_a, "satisfaction_b": sat_b
        }
        if on_round_update:
            await on_round_update(round_data)

        # Check outcome
        if action == "accept":
            resolution = {
                "status": "resolved", "final_proposal": current_proposal,
                "rounds_taken": round_num, "summary": current_proposal.get("summary", "Agreement reached"),
                "satisfaction_timeline": satisfaction_timeline
            }
            await db.update_negotiation_status(negotiation_id, "resolved", resolution)
            if on_resolution:
                await on_resolution(resolution)
            return resolution

        if action == "escalate":
            resolution = {
                "status": "escalated", "final_proposal": current_proposal,
                "rounds_taken": round_num, "summary": "Agents couldn't fully agree. Options for human decision.",
                "satisfaction_timeline": satisfaction_timeline
            }
            await db.update_negotiation_status(negotiation_id, "escalated", resolution)
            if on_resolution:
                await on_resolution(resolution)
            return resolution

    # Exhausted rounds
    resolution = {
        "status": "escalated", "final_proposal": current_proposal,
        "rounds_taken": max_rounds, "summary": "Max rounds reached. Best proposal for human decision.",
        "satisfaction_timeline": satisfaction_timeline
    }
    await db.update_negotiation_status(negotiation_id, "escalated", resolution)
    if on_resolution:
        await on_resolution(resolution)
    return resolution
```

**Step 4: TEST THE NEGOTIATION ENGINE (with different personalities)**
```python
# test_negotiation.py
import asyncio
import json

async def on_round(data):
    print(f"\n🔄 Round {data['round_number']}: {data['action']}")
    print(f"   Satisfaction A: {data.get('satisfaction_a', '?')}%  B: {data.get('satisfaction_b', '?')}%")
    print(f"   Reasoning: {data['reasoning'][:150]}")

async def on_resolve(data):
    print(f"\n{'='*50}")
    print(f"🏁 RESULT: {data['status']} in {data['rounds_taken']} rounds")
    print(f"   Summary: {data['summary']}")
    print(f"   Timeline: {json.dumps(data.get('satisfaction_timeline', []))}")

async def test():
    from protocol.negotiation import run_negotiation
    import database as db

    await db.init_db()

    # Test with DIFFERENT personalities: aggressive vs empathetic
    prefs_a = {
        "feature_type": "expenses",
        "goal": "Split Goa trip expenses",
        "constraints": [{"type": "budget", "value": None, "description": "Fair split", "hard": True}],
        "preferences": [
            {"type": "split", "value": "60-40 fuel", "priority": "high", "description": "I drove the whole way"},
            {"type": "payment", "value": "UPI", "priority": "medium", "description": "UPI preferred"}
        ],
        "raw_details": {"hotel": 12000, "fuel": 3000, "dinner": 2000, "upi_id": "rahul@paytm"}
    }
    prefs_b = {
        "feature_type": "expenses",
        "goal": "Split Goa trip expenses fairly",
        "constraints": [{"type": "fairness", "value": "equal contribution acknowledged", "hard": False}],
        "preferences": [
            {"type": "split", "value": "50-50 fuel", "priority": "high", "description": "I navigated and planned"},
            {"type": "payment", "value": "UPI", "priority": "medium", "description": "UPI fine"}
        ],
        "raw_details": {"hotel": 12000, "fuel": 3000, "dinner": 2000}
    }

    neg_id = await db.create_negotiation("expenses", 111)
    await db.add_participant(neg_id, 111, prefs_a, personality_used="aggressive")
    await db.add_participant(neg_id, 222, prefs_b, personality_used="empathetic")

    print("🧪 Testing: AGGRESSIVE (A) vs EMPATHETIC (B) on expense splitting\n")
    result = await run_negotiation(
        negotiation_id=neg_id, preferences_a=prefs_a, preferences_b=prefs_b,
        user_a_id=111, user_b_id=222, feature_type="expenses",
        personality_a="aggressive", personality_b="empathetic",
        on_round_update=on_round, on_resolution=on_resolve
    )

asyncio.run(test())
```
```bash
python test_negotiation.py
```

### DEV B: Build Tavily Search Tool + UPI Generator + Counterparty Flow

→ **Reference: Build Guide v2 Section 12 (Tool Calling System)**

**Step 1: Create Tavily search tool**
```python
# backend/tools/tavily_search.py
from tavily import TavilyClient
from config import TAVILY_API_KEY

class TavilySearchTool:
    name = "tavily_search"

    def __init__(self):
        self.client = TavilyClient(api_key=TAVILY_API_KEY)

    async def execute(self, query: str, search_depth: str = "basic") -> dict:
        try:
            response = self.client.search(
                query=query, search_depth=search_depth,
                include_answer=True, max_results=5
            )
            results = [{"title": r.get("title", ""), "content": r.get("content", ""), "url": r.get("url", "")}
                       for r in response.get("results", [])]
            return {
                "query": query,
                "answer": response.get("answer", ""),
                "results": results,
                "summary": response.get("answer", results[0]["content"][:200] if results else "No results")
            }
        except Exception as e:
            return {"query": query, "answer": "", "results": [], "summary": f"Search failed: {str(e)}"}
```

**Step 2: Create UPI deep link generator**
```python
# backend/tools/upi_generator.py
from urllib.parse import quote

class UPIGeneratorTool:
    name = "generate_upi_link"

    async def execute(self, payee_upi: str, payee_name: str, amount: float, note: str = "") -> dict:
        upi_link = f"upi://pay?pa={quote(payee_upi)}&pn={quote(payee_name)}&am={amount:.2f}&cu=INR"
        if note:
            upi_link += f"&tn={quote(note)}"
        return {
            "upi_link": upi_link,
            "display_text": f"Pay ₹{amount:,.0f} to {payee_name}",
            "payee_upi": payee_upi, "amount": amount
        }
```

**Step 3: Create calculator tool**
```python
# backend/tools/calculator.py
import ast, operator
from decimal import Decimal, ROUND_HALF_UP

class CalculatorTool:
    name = "calculate"

    async def execute(self, expression: str) -> dict:
        allowed_ops = {
            ast.Add: operator.add, ast.Sub: operator.sub,
            ast.Mult: operator.mul, ast.Div: operator.truediv,
        }
        def _eval(node):
            if isinstance(node, ast.Num): return Decimal(str(node.n))
            elif isinstance(node, ast.BinOp):
                return allowed_ops[type(node.op)](_eval(node.left), _eval(node.right))
            raise ValueError(f"Unsupported: {ast.dump(node)}")
        tree = ast.parse(expression, mode='eval')
        result = float(_eval(tree.body).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
        return {"expression": expression, "result": result}
```

**Step 4: Test tools**
```python
# test_tools.py
import asyncio
from tools.tavily_search import TavilySearchTool
from tools.upi_generator import UPIGeneratorTool
from tools.calculator import CalculatorTool

async def test():
    # Tavily
    tavily = TavilySearchTool()
    r = await tavily.execute("best Thai restaurants Bandra Mumbai")
    print(f"Tavily: {r['answer'][:100]}... ({len(r['results'])} results) ✅")

    # UPI
    upi = UPIGeneratorTool()
    r = await upi.execute("rahul@paytm", "Rahul", 8200, "Goa trip settlement")
    print(f"UPI: {r['upi_link'][:60]}... ✅")

    # Calculator
    calc = CalculatorTool()
    r = await calc.execute("12000 * 0.55")
    print(f"Calc: 12000 * 0.55 = {r['result']} ✅")

asyncio.run(test())
```

**Step 5: Build /pending counterparty flow** (same as v1 — see existing milestone doc for code, but add personality tracking)

### ✅ MILESTONE 3 SUCCESS TEST
```
□ test_negotiation.py shows:
  - Multiple rounds with satisfaction scores tracked per round
  - Aggressive agent A concedes slower than empathetic agent B
  - Final "accept" or "escalate" action
  - Satisfaction timeline in resolution output
□ test_tools.py: Tavily returns real restaurant results, UPI generates valid link, Calculator works
□ Negotiation completes within 45 seconds
□ Database has rounds with satisfaction_a/satisfaction_b populated
□ Bot B's /pending command shows coordination with personality info
```

**🚫 THIS IS THE GO/NO-GO CHECKPOINT.** If negotiation engine works, everything else is features + polish.

---

## MILESTONE 4: End-to-End Telegram Flow + Real-Time Updates + Voice
**⏰ Time: Hour 6 → Hour 8 (120 min)**
**👤 Who: DEV A connects everything + voice, DEV B handles formatting + UPI**

### Goal: Full flow with voice summaries and UPI links working

**Step 1: Create ElevenLabs TTS module**
→ **Reference: Build Guide v2 Section 13 (Voice Summaries)**

```python
# backend/voice/elevenlabs_tts.py
import httpx
from config import ELEVENLABS_API_KEY, VOICE_ID_AGENT_A, VOICE_ID_AGENT_B

async def generate_voice_summary(text: str, negotiation_id: str,
                                  voice_id: str = None) -> str:
    """Generate TTS MP3 and return file path. Returns None on failure."""
    voice_id = voice_id or VOICE_ID_AGENT_A

    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
                headers={"xi-api-key": ELEVENLABS_API_KEY, "Content-Type": "application/json"},
                json={
                    "text": text[:500],  # Budget cap
                    "model_id": "eleven_flash_v2_5",
                    "voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
                },
                timeout=30.0
            )
            if response.status_code == 200:
                filepath = f"/tmp/negot8_voice_{negotiation_id}.mp3"
                with open(filepath, "wb") as f:
                    f.write(response.content)
                return filepath
    except Exception as e:
        print(f"Voice TTS failed: {e}")
    return None

# Voice summary templates
VOICE_TEMPLATES = {
    "expenses": "Expenses settled! After {rounds} rounds, {payer} owes {payee} {amount} rupees. A UPI payment link has been sent.",
    "collaborative": "Decision made! You're going to {choice}. Your agents found the perfect match in {rounds} rounds.",
    "scheduling": "Meeting scheduled for {date} at {time}, {location}. Agreed in {rounds} rounds.",
    "marketplace": "Deal done! {item} for {price} rupees. Payment link is ready.",
    "trip": "Trip planned! {destination} on {dates}, {budget} per person.",
    "freelance": "Project agreed! {scope} for {budget} rupees. First milestone payment ready via UPI.",
    "roommate": "Decision made! {option}. Cost split arranged.",
    "conflict": "Resolution reached! {summary}.",
}

def build_voice_text(feature_type: str, resolution: dict) -> str:
    template = VOICE_TEMPLATES.get(feature_type, "Negotiation resolved! Check Telegram for details.")
    try:
        return template.format(**resolution)[:500]
    except KeyError:
        summary = resolution.get("summary", "resolved")
        return f"Your {feature_type} negotiation is complete: {summary}"[:500]
```

**Step 2: Create unified bot runner with voice + UPI + analytics**
```python
# backend/run.py — THE MAIN ENTRY POINT
import asyncio
import json
from telegram.ext import Application, CommandHandler, ConversationHandler, MessageHandler, CallbackQueryHandler, filters
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from agents.personal_agent import PersonalAgent
from protocol.negotiation import run_negotiation
from voice.elevenlabs_tts import generate_voice_summary, build_voice_text
from tools.upi_generator import UPIGeneratorTool
import database as db
from config import *

personal_agent = PersonalAgent()
upi_tool = UPIGeneratorTool()
pending_coordinations = {}
bot_apps = {}

async def send_to_user(bot, user_id, text, reply_markup=None):
    """Send a message to any user."""
    try:
        await bot.send_message(chat_id=user_id, text=text, parse_mode="Markdown", reply_markup=reply_markup)
    except Exception as e:
        print(f"Failed to send to {user_id}: {e}")

async def send_voice_to_user(bot, user_id, audio_path):
    """Send voice note to user."""
    try:
        with open(audio_path, "rb") as f:
            await bot.send_voice(chat_id=user_id, voice=f, caption="🎙 Voice summary from your agent")
    except Exception as e:
        print(f"Failed to send voice to {user_id}: {e}")

# ─── Resolution handler with voice + UPI ───
async def handle_resolution(negotiation_id, resolution, feature_type,
                             user_a_id, user_b_id, bot_a, bot_b,
                             preferences_a, preferences_b):
    """Post-resolution: generate UPI link, voice summary, analytics, send to users."""

    status = resolution["status"]
    proposal = resolution.get("final_proposal", {})
    emoji = "✅" if status == "resolved" else "⚠️"

    summary_text = (
        f"{emoji} *Negotiation {'Complete' if status == 'resolved' else 'Needs Input'}!*\n\n"
        f"📊 Resolved in {resolution['rounds_taken']} rounds\n\n"
        f"📋 *Agreement:*\n{proposal.get('summary', 'See details')}\n\n"
        f"*For A:* {proposal.get('for_party_a', 'See details')}\n"
        f"*For B:* {proposal.get('for_party_b', 'See details')}"
    )

    # ─── UPI link (for expense-related features) ───
    upi_markup = None
    if feature_type in ("expenses", "freelance", "marketplace", "roommate"):
        # Try to extract settlement from proposal details
        details = proposal.get("details", {})
        settlement = details.get("settlement", {})
        upi_id = (preferences_a.get("raw_details", {}).get("upi_id") or
                  preferences_b.get("raw_details", {}).get("upi_id"))

        if upi_id and settlement.get("amount"):
            upi_result = await upi_tool.execute(
                payee_upi=upi_id,
                payee_name=settlement.get("payee_name", "User"),
                amount=float(settlement["amount"]),
                note=f"negoT8: {feature_type} settlement"
            )
            upi_link = upi_result["upi_link"]
            summary_text += f"\n\n💳 *Tap to pay:* ₹{settlement['amount']:,.0f}"

            upi_markup = InlineKeyboardMarkup([
                [InlineKeyboardButton(
                    f"💳 Pay ₹{settlement['amount']:,.0f}",
                    url=upi_link
                )]
            ])

    # ─── Send text summary to both ───
    await send_to_user(bot_a, user_a_id, summary_text, reply_markup=upi_markup)
    await send_to_user(bot_b, user_b_id, summary_text, reply_markup=upi_markup)

    # ─── Voice summary ───
    voice_text = build_voice_text(feature_type, {
        "rounds": resolution["rounds_taken"],
        "summary": proposal.get("summary", "resolved"),
        **proposal.get("details", {}),
        **{k: v for k, v in proposal.items() if k != "details"}
    })

    voice_path = await generate_voice_summary(voice_text, negotiation_id, VOICE_ID_AGENT_A)
    if voice_path:
        await send_voice_to_user(bot_a, user_a_id, voice_path)
        # Generate with different voice for User B
        voice_path_b = await generate_voice_summary(voice_text, f"{negotiation_id}_b", VOICE_ID_AGENT_B)
        if voice_path_b:
            await send_voice_to_user(bot_b, user_b_id, voice_path_b)

    # ─── Compute & store analytics ───
    timeline = resolution.get("satisfaction_timeline", [])
    concession_log = []
    rounds = await db.get_rounds(negotiation_id)
    for r in rounds:
        concessions = json.loads(r["concessions_made"]) if r["concessions_made"] else []
        for c in concessions:
            concession_log.append({"round": r["round_number"], "by": "A" if r["proposer_id"] == user_a_id else "B", "gave_up": c})

    final_sat_a = timeline[-1]["score_a"] if timeline else 50
    final_sat_b = timeline[-1]["score_b"] if timeline else 50
    fairness = 100 - abs(final_sat_a - final_sat_b)

    await db.store_analytics({
        "negotiation_id": negotiation_id,
        "satisfaction_timeline": json.dumps(timeline),
        "concession_log": json.dumps(concession_log),
        "fairness_score": fairness,
        "total_concessions_a": sum(1 for c in concession_log if c["by"] == "A"),
        "total_concessions_b": sum(1 for c in concession_log if c["by"] == "B"),
    })
```

**Step 3: Wire up the full flow and test end-to-end**

Complete the `run.py` with both bot runners and the counterparty `/pending` flow (reuse from existing milestone, adding personality tracking). The critical addition is passing personality to `run_negotiation()` and calling `handle_resolution()` when done.

### ✅ MILESTONE 4 SUCCESS TEST
```
□ FULL FLOW TEST (two phones/Telegram accounts):
  1. Phone A: /start → /personality → pick "aggressive" → /coordinate @userB → send expense details with UPI ID
  2. Phone B: /start → /personality → pick "empathetic" → /pending → send their side
  3. Both phones receive round-by-round updates with satisfaction scores
  4. Both phones receive final resolution text
  5. Both phones receive voice note (different voices for A and B!)
  6. Phone B sees UPI payment button → tapping opens UPI app (test on real phone)
□ No crashes during full flow
□ Voice MP3 generates in < 5 seconds
□ Database has analytics row for the negotiation
□ Negotiation completes in under 60 seconds total
```

**🎉 If this works, you have a fully featured product. Everything after this is more features + the dashboard.**

---

## MILESTONE 5: Feature Implementation Sprint
**⏰ Time: Hour 8 → Hour 14 (360 min)**
**👤 Who: Both devs, feature-by-feature**

### Strategy: Negotiation engine is generic + personality-aware. Each feature needs:
1. Tailored prompt additions for the Negotiator (domain-specific strategies)
2. Tool calls (Tavily search for real data, calculator for math, UPI for payments)
3. Nice Telegram output formatting + appropriate voice summary template

### Batch 1 (Hour 8-10): Scheduling + Collaborative Decisions
→ **Reference: Build Guide v2 Section 8, Features 1 and 7**

Focus on:
- Add **Tavily search** for restaurant/place/cafe discovery in collaborative decisions
- Tavily `include_answer=True` gives free AI-summarized results — no extra LLM call
- Polish output format with venue suggestions, time formatting
- Test voice summary template for both features

**Test:** Restaurant decision with Tavily returning real results. Scheduling with time overlap.

### Batch 2 (Hour 10-12): Expenses + Marketplace
→ **Reference: Build Guide v2 Section 8, Features 2 and 6**

Key implementation:
- Calculator tool for ALL expense math (never let LLM do arithmetic)
- **UPI link auto-generation** in expense resolution (already wired in Milestone 4)
- Tavily search for market price comparison in marketplace
- UPI link for buyer after marketplace deal
- Clean tabular output in Telegram for expense breakdowns

**Test:** Expense splitting with unequal split debate → verify math is correct → UPI link works. Buy/sell with haggling → market price cited from Tavily.

### Batch 3 (Hour 12-13): Freelance + Roommate
→ **Reference: Build Guide v2 Section 8, Features 3 and 4**

Key implementation:
- Complex proposal schema for freelance (scope + timeline + budget)
- **UPI link for first milestone payment** in freelance
- Tavily search for **real WiFi plans / product prices** in roommate decisions
- This is where Tavily really shines — agents cite actual Airtel/Jio plans

**Test:** Freelance where budget < desired rate (forces scope reduction). WiFi plan decision with real provider data.

### Batch 4 (Hour 13-14): Trip Planning (Multi-Agent) + Conflict Resolution
→ **Reference: Build Guide v2 Section 8, Features 5 and 8**

Key implementation:
- **Trip planning needs 3-person support** — modify run_negotiation for 3+ preference sets
- Tavily for real destination info, accommodation prices, activity suggestions
- Voice summaries for group trips use **3 different voice IDs** (wow factor)
- Conflict resolution is pure negotiation — no special tools needed

**Simplified multi-agent approach for 3 people:**
```python
async def run_group_negotiation(negotiation_id, all_preferences, all_user_ids, feature_type, personalities, ...):
    """Mediator approach: find intersection, propose, iterate."""
    mediator_prompt = f"""You are mediating a {feature_type} negotiation with {len(all_preferences)} participants.
All participants' preferences:
{json.dumps(all_preferences, indent=2)}
Find a solution that maximizes group satisfaction.
Budget ceiling = lowest budget. Dates = intersection."""

    proposal = await NegotiatorAgent("balanced").call(mediator_prompt)
    # Score with each, iterate for max 5 rounds
```

**Test:** 3-person trip planning. Conflict resolution scenario.

### ✅ MILESTONE 5 SUCCESS TEST
```
□ All 8 feature types produce sensible negotiations via Telegram
□ Expense splitting math is CORRECT + UPI payment link generated
□ Restaurant suggestions include REAL place names from Tavily (not hallucinated)
□ Freelance negotiation reduces scope when budget too low + UPI for milestone
□ WiFi/roommate decisions cite real provider plans from Tavily
□ Marketplace shows market price reference from Tavily search
□ Trip planning handles 3 people (can use 3 bots or simulate)
□ Conflict resolution produces creative compromises
□ Voice summaries work for at least expenses + collaborative + marketplace
□ Generic negotiation works for off-menu scenarios
```

**Timing check:** You should be at Hour 14. If behind, skip trip planning multi-agent (hardest) and demo as 2-person trip. Skip conflict resolution if needed — demo with 6 features + add-ons is still impressive.

---

## MILESTONE 6: Dashboard + Analytics Visualization
**⏰ Time: Hour 14 → Hour 18 (240 min)**
**👤 Who: DEV B builds dashboard + analytics charts, DEV A builds API + Socket.IO**

### DEV A: FastAPI Endpoints + Socket.IO + Analytics API

→ **Reference: Build Guide v2 Section 11 (API Layer)**

```python
# backend/api.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import socketio
import database as db

sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

@app.get("/api/negotiations")
async def list_negotiations():
    # Return all negotiations from DB
    pass

@app.get("/api/negotiations/{negotiation_id}")
async def get_negotiation(negotiation_id: str):
    """Return negotiation + rounds + participants + analytics."""
    neg = await db.get_negotiation(negotiation_id) if hasattr(db, 'get_negotiation') else None
    rounds = await db.get_rounds(negotiation_id)
    participants = await db.get_participants(negotiation_id)
    analytics = await db.get_analytics(negotiation_id)
    return {
        "negotiation": neg, "rounds": [dict(r) for r in rounds],
        "participants": [dict(p) for p in participants],
        "analytics": dict(analytics) if analytics else None
    }

@sio.event
async def join_negotiation(sid, negotiation_id):
    await sio.enter_room(sid, negotiation_id)

socket_app = socketio.ASGIApp(sio, app)
```

Wire Socket.IO emissions into `run_negotiation()` round callbacks so dashboard updates live.

### DEV B: Build Dashboard with Analytics Components

→ **Reference: Build Guide v2 Section 10 (Dashboard) + Section 15 (Analytics)**

**Priority order (build what you have time for):**

1. **NegotiationTimeline** — Round cards showing proposals + reasoning (MUST HAVE)
2. **AgentChat** — Chat bubbles with personality badges next to agent names (MUST HAVE)
3. **SatisfactionChart** — Recharts `<LineChart>` with two lines, A and B (MUST HAVE — demo differentiator)
4. **FairnessScore** — Simple gauge/bar showing 0-100 fairness (MUST HAVE)
5. **ConcessionTimeline** — Vertical log of who gave what (NICE TO HAVE)
6. **ResolutionCard** — Final agreement + UPI link button (MUST HAVE)
7. **Live round counter** — "Round 2/5... negotiating..." with animation (NICE TO HAVE)

**SatisfactionChart component** (copy from Build Guide v2 Section 10):
```typescript
// Use Recharts LineChart with two Line elements
// Data format: [{round: 1, satisfaction_a: 85, satisfaction_b: 45}, ...]
```

**FairnessScore component** (copy from Build Guide v2 Section 10):
```typescript
// Simple progress bar with color coding (green 80+, yellow 60-79, red <60)
```

### ✅ MILESTONE 6 SUCCESS TEST
```
□ Dashboard loads at localhost:3000
□ Dashboard shows negotiation with round-by-round timeline
□ SatisfactionChart renders two lines (A and B) over rounds using Recharts
□ FairnessScore shows a number + colored bar
□ Personality badges visible next to agent names (😤 / 💚 / 📊 etc.)
□ Socket.IO connection works (real-time updates without page refresh)
□ Resolution card shows final agreement + UPI link (if applicable)
□ Dashboard looks professional enough for a 5-second judge glance
```

**If running behind:** The SatisfactionChart + FairnessScore are the priority analytics. Skip ConcessionTimeline. Skip dashboard entirely if needed — Telegram demo with voice + UPI is already strong.

---

## MILESTONE 7: Polish + Hardening
**⏰ Time: Hour 18 → Hour 21 (180 min)**
**👤 Who: Both devs**

### Priority Fixes (Do these first)

**1. Error handling everywhere — especially voice + Tavily + UPI**
```python
# Wrap EVERY external call in try/except
# Voice summary failure → just skip voice (text summary already sent)
# Tavily failure → fall back to Gemini knowledge
# UPI generation failure → show UPI ID + amount as plain text
try:
    voice_path = await generate_voice_summary(text, neg_id)
except:
    voice_path = None  # Voice is bonus, not critical

try:
    search_results = await tavily.execute(query)
except:
    search_results = {"answer": "", "results": [], "summary": "Search unavailable"}
```

**2. Rate limit protection**
```python
GEMINI_DELAY = 1.5  # seconds between calls (15 RPM = 1 every 4 sec, but bursty)

async def rate_limited_call(agent, prompt, context=None):
    await asyncio.sleep(GEMINI_DELAY)
    return await agent.call(prompt, context)
```

**3. Fallback responses for when APIs fail**
```python
FALLBACK_RESPONSES = {
    "collaborative": {"action": "propose", "proposal": {"summary": "Let's go with a popular option", "details": {}}, "satisfaction_score": 50},
    "expenses": {"action": "propose", "proposal": {"summary": "Equal split as default", "details": {}}, "satisfaction_score": 50},
    # ... one per feature
}
```

**4. Telegram message length limits**
```python
def safe_telegram_message(text, max_len=4000):
    if len(text) > max_len:
        return text[:max_len-50] + "\n\n_(truncated)_"
    return text
```

**5. Test EVERY feature one more time**
```
□ Scheduling: quick test + voice summary plays
□ Expenses: verify math + UPI link works on phone + voice
□ Freelance: scope reduction + UPI for milestone
□ Roommate: Tavily returns real WiFi plans
□ Trip planning: 2-person minimum, 3-person if working
□ Marketplace: haggling + market price from Tavily + UPI
□ Collaborative: Tavily restaurant results + voice summary
□ Conflict: creative compromise + voice summary
```

**6. Personality impact verification**
```
□ Run same expense scenario twice:
  - aggressive vs balanced → more rounds, harder concessions
  - people_pleaser vs people_pleaser → resolves in 1-2 rounds
□ Verify personality label shows in Telegram messages
```

### ✅ MILESTONE 7 SUCCESS TEST
```
□ No crashes in any of the 8 features
□ All error paths handled gracefully (voice fails → text still works, Tavily fails → fallback)
□ Rate limiting works (no 429 errors from Gemini)
□ Voice summaries generate for resolved negotiations (at least expenses + collaborative)
□ UPI links open correctly on real phone
□ Messages look clean on actual Telegram
□ Dashboard analytics charts render correctly
□ Different personalities produce visibly different negotiation behavior
```

---

## MILESTONE 8: Demo Preparation
**⏰ Time: Hour 21 → Hour 24 (180 min)**
**👤 Who: Both devs**

### Hour 21-22: Script and rehearse the 3 demo scenarios

→ **Reference: Build Guide v2 Section 18 (Demo Script)**

**Demo 1: Restaurant Decision (60 sec)**
```
Pre-type these messages:

User A (set personality: analytical 📊):
"Finding dinner with @priya tonight. Craving Thai or spicy Indian,
nothing over 1500 for two, Bandra area, casual vibe"

User B (set personality: empathetic 💚):
"Dinner with @rahul tonight. I want something light, Mediterranean
or Thai. Need vegetarian options. Western suburbs, max 1200 for two"

EXPECTED: Tavily searches real restaurants → agents agree in 2-3 rounds
→ both get text + voice note with restaurant recommendation
→ dashboard shows satisfaction chart converging
```

**Demo 2: Expense Splitting (90 sec) — MONEY DEMO**
```
User A (set personality: aggressive 😤):
"Split our Goa trip costs with @priya. I paid 12K for hotel,
3K for fuel, 2K for Saturday dinner. Fuel should be 60-40 since I drove
the whole way. UPI settlement preferred. My UPI: rahul@paytm"

User B (set personality: balanced ⚖️):
"Trip expenses with @rahul. I think fuel should be 50-50
because I navigated and planned the entire route. Rest equal. Fine with UPI"

EXPECTED: 3-4 rounds of negotiation → fuel split lands at ~55-45
→ dashboard shows A's satisfaction starting high, B's rising
→ UPI PAYMENT BUTTON appears → tap to open GPay with ₹7,650 prefilled
→ voice notes: Agent A (male voice) + Agent B (female voice) read settlement
```

**Demo 3: Group Trip Planning (90 sec) — THE WOW MOMENT**
```
User A: "Weekend trip with @priya and @amit. Mountains, 15K max, March 22-23. Have car"
User B: "Trip with @rahul and @amit. Beach vibes, 10K max, March 20-24. No flights"
User C: "Weekend trip. Anywhere fun, 12K, only March 22-23. Love food and trekking"

EXPECTED: Tavily searches destinations → date overlap found → budget ceiling = 10K
→ compromise destination (Gokarna: beach + hills)
→ 3 different voice notes with 3 different voices
→ dashboard shows 3 satisfaction lines converging
```

### Hour 22-23: Run each demo scenario 3 times

```
Run 1: Check output quality. If bad, tweak prompts.
Run 2: Check timing. Each demo should complete in under 60 seconds.
Run 3: Final run. RECORD THIS ON VIDEO as backup.

Voice check: All voice notes play? Different voices for different agents?
UPI check: Payment button opens UPI app on phone?
Dashboard check: Analytics charts rendering correctly?
```

### Hour 23-23:30: Prepare failsafes

**Failsafe 1: Pre-seed completed negotiations with analytics**
```python
# seed_demo_data.py — pre-populate DB with beautiful results including analytics
# If live demo fails, show from dashboard with satisfaction charts + fairness scores
```

**Failsafe 2: Pre-generate voice summaries**
```
Generate MP3s for all 3 demo scenarios ahead of time.
If ElevenLabs is slow during demo, play pre-generated files.
```

**Failsafe 3: Record video backup**
```
Screen-record the perfect demo run showing:
- Telegram messages appearing
- Voice notes playing
- UPI button being tapped → GPay opening
- Dashboard with live analytics updating
This is your insurance policy.
```

**Failsafe 4: Offline mode**
```python
DEMO_MODE = os.getenv("DEMO_MODE", "false") == "true"
if DEMO_MODE:
    # Load pre-computed responses + pre-generated voice files
```

### Hour 23:30-24: Final checks

```
□ Both phones charged + one has GPay/PhonePe for UPI demo
□ Telegram bots running on laptop
□ Dashboard accessible with analytics rendering
□ WiFi working (have 4G hotspot as backup)
□ Demo messages pre-typed in notes app
□ Personality set: A=aggressive, B=empathetic for Demo 2
□ Video backup saved to phone/drive
□ Pitch script rehearsed (under 5 min total)
□ Know which phone is User A / User B / User C
□ ElevenLabs credits remaining > 5,000 characters
□ Tavily searches remaining > 50 credits
□ Deep breath. You built something extraordinary. Go show them.
```

### ✅ MILESTONE 8 SUCCESS TEST
```
□ All 3 demo scenarios run successfully at least twice
□ Voice notes arrive within 5 seconds of resolution
□ UPI link opens payment app with correct amount
□ Dashboard shows satisfaction chart + fairness score
□ Demo completes in under 5 minutes (pitch + live demo)
□ Video backup recorded and saved
□ Both team members know the pitch script
□ Pre-seeded data in dashboard as fallback
```

---

## EMERGENCY PROTOCOLS

### "Gemini is down / rate limited"
→ Switch to DEMO_MODE with pre-computed responses
→ Increase GEMINI_DELAY to 3 seconds
→ Space out demo scenarios with talking time between them

### "ElevenLabs is down / credits exhausted"
→ Voice summaries are a bonus. Skip and send text-only resolutions.
→ Play pre-generated MP3s if available
→ Still demo the text + UPI + analytics (3 out of 4 add-ons work)

### "Tavily is down / out of credits"
→ Agents use Gemini's knowledge instead of live search
→ Restaurant names won't be verified/real but will still be plausible
→ Market prices will be estimated instead of cited

### "UPI links don't work"
→ Show the UPI link text (upi://pay?...) in the message
→ Show the UPI ID + amount as fallback text
→ Explain the concept: "On a real phone, tapping this opens GPay with amount pre-filled"

### "Telegram bot won't connect"
→ Check token, kill other instances, recreate via @BotFather
→ Worst case: demo via dashboard API + curl

### "Running out of time at Hour 14"
→ Skip trip multi-agent + roommate
→ Focus demo: Restaurant + Expenses (+ UPI + voice) + Marketplace
→ Skip dashboard — Telegram with voice + UPI is already impressive

### "Running out of time at Hour 20"
→ Skip ConcessionTimeline + dashboard polish
→ Focus: make 3 demo scenarios bulletproof
→ Ensure voice + UPI work in at least 1 demo
→ Record video backup

---

## Quick Milestone Status Tracker

Print this and check off as you go:

```
⏰ HOUR   MILESTONE                                          STATUS
────────────────────────────────────────────────────────────────────
 0-1    □ M1: Skeleton + ALL API verification (Gemini+Tavily+11Labs) [ ]
 1-3    □ M2: Base agent + preferences + personality system          [ ]
 3-6    □ M3: Negotiation engine + tools (Tavily/UPI/calc) [CRITICAL][ ]
 6-8    □ M4: E2E Telegram flow + voice summaries + UPI links       [ ]
 8-14   □ M5: All 8 features (with Tavily + UPI where applicable)   [ ]
14-18   □ M6: Dashboard + analytics charts (satisfaction/fairness)   [ ]
18-21   □ M7: Polish + hardening + personality verification          [ ]
21-24   □ M8: Demo prep (3 scenarios + failsafes + video backup)     [ ]
────────────────────────────────────────────────────────────────────
        □ DEMO TIME 🏆                                               [ ]

ADD-ON STATUS:
  □ Voice summaries working (ElevenLabs TTS → Telegram voice notes)
  □ Agent personalities working (/personality → prompt modifier)
  □ Tavily search working (real restaurants/prices/plans)
  □ UPI deep links working (tap → opens payment app)
  □ Analytics dashboard working (satisfaction chart + fairness score)
```

---

*Each milestone builds on the previous one. Never skip. Never rush ahead. Green-light each checkpoint before moving on. This is how you ship in 24 hours without panic.*

*v2 add-ons integrated throughout: voice at M4, Tavily at M3, UPI at M4, personality at M2, analytics at M6.*

**Trust the process. Trust your agents. Go win. 🏆**