Files
B.Tech-Project-III/negot8/docs/milestone.md
2026-04-05 00:43:23 +05:30

58 KiB
Raw Blame History

negoT8 — Milestone Execution Guide

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 (NEGOT8_BUILD_GUIDE.md) you need. Open both docs side by side.


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)
□ 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
□ Laptop charger, phone chargers, hotspot backup
□ This guide + Build Guide 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

mkdir -p negot8/backend/{agents,protocol,features,telegram,tools}
cd negot8
python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

Step 2: Install dependencies

pip install fastapi uvicorn "python-telegram-bot[all]" google-generativeai httpx pydantic python-dotenv aiosqlite python-socketio

Step 3: Create .env file

# Create .env in project root (negot8/.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
NEXT_PUBLIC_API_URL=http://localhost:8000
DATABASE_PATH=negot8.db

Step 4: Create config.py

# 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")
DATABASE_PATH = os.getenv("DATABASE_PATH", "negot8.db")
API_URL = os.getenv("NEXT_PUBLIC_API_URL", "http://localhost:8000")

Step 5: Initialize databaseReference: Build Guide Section 5 (Database Schema)

# 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 '{}'
            );
            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
            );
            CREATE TABLE IF NOT EXISTS participants (
                negotiation_id TEXT,
                user_id INTEGER,
                preferences_json TEXT,
                satisfaction_score REAL,
                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,
                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
            );
        ''')
        await db.commit()
    print("✅ Database initialized")

# Helper functions (add more as needed)
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 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):
    async with aiosqlite.connect(DATABASE_PATH) as db:
        await db.execute(
            "INSERT OR REPLACE INTO participants (negotiation_id, user_id, preferences_json) VALUES (?, ?, ?)",
            (negotiation_id, user_id, json.dumps(preferences))
        )
        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):
    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)
               VALUES (?, ?, ?, ?, ?, ?, ?)""",
            (negotiation_id, round_number, proposer_id, json.dumps(proposal), response_type, json.dumps(response) if response else None, reasoning)
        )
        await db.commit()

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

Step 6: Verify Gemini API works

# test_gemini.py (run this standalone)
import google.generativeai as genai
import os
from dotenv import load_dotenv

load_dotenv()
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 a JSON object with keys "status" and "message". Status should be "ok" and message should be "Gemini is working"'
)

print(response.text)
python test_gemini.py

DEV B Tasks (Frontend + Telegram Shell)

Step 1: Create Next.js dashboard

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

// dashboard/app/layout.tsx — just make sure it loads
// dashboard/app/page.tsx — simple "negoT8 Dashboard" heading

Step 3: Verify Telegram bot responds

# 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()
python test_telegram.py
# Open Telegram, find your bot, send /start

MILESTONE 1 SUCCESS TEST

□ python test_gemini.py → prints {"status": "ok", "message": "Gemini is working"}
□ python test_telegram.py → bot responds to /start on Telegram
□ Database file negot8.db exists with all 5 tables
□ Next.js dashboard loads at http://localhost:3000
□ .env has all 4 keys filled in

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


MILESTONE 2: Base Agent + Preference Extraction

Time: Hour 1 → Hour 3 (120 min) 👤 Who: DEV A builds agents, DEV B builds Telegram integration

DEV A: Build the Agent System

Reference: Build Guide Section 6 (System Prompts) + Section 12 (Gemini Wrapper)

Step 1: Create BaseAgent class

# 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 Section 6 (Personal Agent System Prompt)

# 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 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: Test PersonalAgent with real examples

# test_personal_agent.py
import asyncio
from agents.personal_agent import PersonalAgent

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
    r2 = await agent.extract_preferences("Split our Goa trip costs. I paid 12K hotel, 3K fuel. Fuel should be 60-40 since I drove.")
    print("TEST 2 (expenses):", r2.get("feature_type"), "✅" if r2.get("feature_type") == "expenses" else "❌")

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

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

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

import json
asyncio.run(test())
cd backend
python test_personal_agent.py

Expected output: All 4 tests show correct feature_type. Full output of Test 2 should have constraints (hard budget limits), preferences (60-40 split preference), and raw_details (hotel: 12000, fuel: 3000).

If feature_type is wrong: Tweak the FEATURE TYPE CLASSIFICATION section in the prompt. Add more examples for the confused category.

If JSON parsing fails: Make sure response_mime_type="application/json" is set. If still failing, the fallback JSON extraction in BaseAgent should catch it.

DEV B: Telegram Bot with Preference Flow

Reference: Build Guide Section 9 (Telegram Bot Implementation)

Step 1: Build full Telegram bot

# backend/telegram/bot.py
import asyncio
from telegram import Update, BotCommand
from telegram.ext import (
    Application, CommandHandler, MessageHandler,
    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
# In production, this would be a shared database
pending_coordinations = {}  # {counterparty_username: {negotiation_id, initiator_id, feature_type, preferences}}

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"
        "Try: `/coordinate @friend_username`\n"
        "Then just tell me what you need in plain English.",
        parse_mode="Markdown"
    )

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\n"
            "• Negotiate project scope and budget\n"
            "• Plan a weekend trip\n"
            "• Decide where to eat tonight\n"
            "• Resolve the parking spot issue\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.",
        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")

    # Show "typing" indicator while agent processes
    await update.message.chat.send_action("typing")

    # Extract preferences using Personal Agent
    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")

    # Create negotiation
    negotiation_id = await db.create_negotiation(feature_type, user.id)
    await db.add_participant(negotiation_id, user.id, preferences)

    # Store for counterparty pickup
    pending_coordinations[counterparty] = {
        "negotiation_id": negotiation_id,
        "initiator_id": user.id,
        "initiator_name": user.first_name,
        "feature_type": feature_type,
        "preferences_a": preferences
    }

    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"
    }

    await update.message.reply_text(
        f"✅ *Preferences captured!*\n\n"
        f"📋 Type: {feature_labels.get(feature_type, '🤝 Coordination')}\n"
        f"🤝 With: @{counterparty}\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 as we go!_ 🤖↔️🤖",
        parse_mode="Markdown"
    )
    return ConversationHandler.END

async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text("📊 Checking your active negotiations...")
    # TODO: Query DB for user's negotiations

async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        "🤖 *negoT8 — How it works*\n\n"
        "1⃣ You: `/coordinate @person`\n"
        "2⃣ You: Describe what you need\n"
        "3⃣ Their agent asks them for their side\n"
        "4⃣ 🤖↔️🤖 Agents negotiate (you watch live)\n"
        "5⃣ ✅ Both get the agreed resolution\n\n"
        "*Works for:*\n"
        "Scheduling, expenses, trip planning, buying/selling, "
        "restaurant picks, roommate decisions, conflict resolution, "
        "and literally anything else that needs agreement!",
        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(conv_handler)
    app.add_handler(CommandHandler("status", status_command))
    app.add_handler(CommandHandler("help", help_command))

    return app

Step 2: Test the full preference extraction flow via Telegram

# run_bot_a.py
import asyncio
from telegram.bot import create_bot
from config import TELEGRAM_BOT_TOKEN_A
import database as db

async def main():
    await db.init_db()
    bot = create_bot(TELEGRAM_BOT_TOKEN_A)
    print("🤖 Bot A running...")
    await bot.run_polling()

asyncio.run(main())

MILESTONE 2 SUCCESS TEST

□ PersonalAgent correctly classifies all 4 test scenarios (scheduling, expenses, generic, marketplace)
□ PersonalAgent extracts actual numbers/dates/constraints from messages
□ Telegram Bot A responds to /start, /coordinate @someone, and captures preferences
□ Preferences stored in SQLite database (check with: sqlite3 negot8.db "SELECT * FROM participants;")
□ pending_coordinations dict has the stored coordination data

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

🚫 If Telegram bot crashes on message: Check that ConversationHandler states are correct. Most common bug: returning wrong state integer.


MILESTONE 3: Negotiator Agent + Inter-Agent Protocol

Time: Hour 3 → Hour 6 (180 min) 👤 Who: DEV A builds negotiation engine, DEV B builds counterparty flow + connects bots

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

DEV A: Build the Negotiation Engine

Reference: Build Guide Section 6 (Negotiator Agent Prompt) + Section 7 (Protocol)

Step 1: Create Pydantic message models

# backend/protocol/messages.py
from pydantic import BaseModel
from typing import Optional, List
from enum import Enum
from datetime import datetime

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        # telegram_id of sender's human
    receiver_id: int      # telegram_id of receiver's human
    round_number: int
    payload: dict         # feature-specific content
    reasoning: str = ""
    satisfaction_score: float = 0.0
    concessions_made: List[str] = []
    concessions_requested: List[str] = []
    timestamp: str = ""

class NegotiationState(BaseModel):
    negotiation_id: str
    feature_type: str
    status: str           # pending, active, resolved, escalated
    current_round: int = 0
    max_rounds: int = 5
    participants: List[dict] = []
    rounds: List[dict] = []
    resolution: Optional[dict] = None

Step 2: Create NegotiatorAgent

# backend/agents/negotiator_agent.py
from agents.base_agent import BaseAgent
import json

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

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.

For EACH round, you receive:
- Your human's preferences (constraints + preferences + goal)
- The other party's latest proposal (or nothing if you go first)
- Round number (1-5)

You MUST respond with this exact JSON:
{
  "action": "propose|counter|accept|escalate",
  "proposal": {
    "summary": "one-line description of proposal",
    "details": { ... feature-specific proposal 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

For GENERIC (non-standard) negotiations:
- Identify what both parties want and where they conflict
- Find creative compromises that reframe the problem
- If the domain is unfamiliar, focus on the underlying interests not positions

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):
        super().__init__(system_prompt=NEGOTIATOR_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. This is 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 the proposal 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 the Negotiation Engine (orchestrator)

# backend/protocol/negotiation.py
import asyncio
import json
from agents.negotiator_agent import NegotiatorAgent
import database as db

negotiator = NegotiatorAgent()

async def run_negotiation(negotiation_id: str, preferences_a: dict, preferences_b: dict,
                          user_a_id: int, user_b_id: int, feature_type: str,
                          on_round_update=None, on_resolution=None):
    """
    Main negotiation loop. Runs the full negotiation between two agents.

    on_round_update: callback(round_data) — called after each round
    on_resolution: callback(resolution_data) — called when negotiation ends
    """

    await db.update_negotiation_status(negotiation_id, "active")

    current_proposal = None
    max_rounds = 5

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

        if round_num == 1:
            # Agent A proposes first
            response = await negotiator.generate_initial_proposal(
                my_preferences=preferences_a,
                feature_type=feature_type
            )
            proposer_id = user_a_id
        elif round_num % 2 == 0:
            # Agent B's turn to respond
            response = await negotiator.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:
            # Agent A's turn to respond
            response = await negotiator.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 from agent
        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, continuing negotiation",
                "concessions_made": [],
                "concessions_requested": []
            }

        action = response.get("action", "counter")
        current_proposal = response.get("proposal", {})

        # Save round to database
        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", "")
        )

        # Notify via callback
        round_data = {
            "negotiation_id": negotiation_id,
            "round_number": round_num,
            "action": action,
            "proposal": current_proposal,
            "satisfaction_score": response.get("satisfaction_score", 0),
            "reasoning": response.get("reasoning", ""),
            "proposer_id": proposer_id
        }

        if on_round_update:
            await on_round_update(round_data)

        # Check if negotiation is complete
        if action == "accept":
            resolution = {
                "status": "resolved",
                "final_proposal": current_proposal,
                "rounds_taken": round_num,
                "summary": current_proposal.get("summary", "Agreement reached"),
            }
            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 presented for human decision.",
            }
            await db.update_negotiation_status(negotiation_id, "escalated", resolution)
            if on_resolution:
                await on_resolution(resolution)
            return resolution

    # If we exhausted all rounds without resolution
    resolution = {
        "status": "escalated",
        "final_proposal": current_proposal,
        "rounds_taken": max_rounds,
        "summary": "Maximum rounds reached. Best proposal presented for human decision.",
    }
    await db.update_negotiation_status(negotiation_id, "escalated", resolution)
    if on_resolution:
        await on_resolution(resolution)
    return resolution

Step 4: TEST THE NEGOTIATION ENGINE STANDALONE

# test_negotiation.py — THIS IS THE MOST IMPORTANT TEST
import asyncio
import json
from protocol.negotiation import run_negotiation

async def on_round(data):
    print(f"\n🔄 Round {data['round_number']}: {data['action']}")
    print(f"   Satisfaction: {data['satisfaction_score']}%")
    print(f"   Reasoning: {data['reasoning']}")
    print(f"   Proposal: {json.dumps(data['proposal'], indent=2)[:200]}")

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"   Final: {json.dumps(data['final_proposal'], indent=2)[:300]}")

async def test():
    # Test: Restaurant decision (simplest scenario)
    prefs_a = {
        "feature_type": "collaborative",
        "goal": "Find a restaurant for dinner tonight",
        "constraints": [{"type": "budget", "value": 1500, "description": "Max ₹1500 for two", "hard": True}],
        "preferences": [
            {"type": "cuisine", "value": "Thai", "priority": "high", "description": "Craving Thai food"},
            {"type": "cuisine", "value": "not Chinese", "priority": "medium", "description": "No Chinese"},
            {"type": "vibe", "value": "casual", "priority": "low", "description": "Casual atmosphere"}
        ],
        "raw_details": {"location": "Bandra", "time": "tonight 8pm"}
    }

    prefs_b = {
        "feature_type": "collaborative",
        "goal": "Find a restaurant for dinner tonight",
        "constraints": [{"type": "budget", "value": 1200, "description": "Max ₹1200 for two", "hard": True}],
        "preferences": [
            {"type": "cuisine", "value": "Mediterranean or Thai", "priority": "high", "description": "Light food"},
            {"type": "dietary", "value": "vegetarian options", "priority": "high", "description": "Need veg options"},
            {"type": "location", "value": "western suburbs", "priority": "medium", "description": "Anywhere western suburbs"}
        ],
        "raw_details": {"location": "western suburbs"}
    }

    import database as db
    await db.init_db()
    neg_id = await db.create_negotiation("collaborative", 111)
    await db.add_participant(neg_id, 111, prefs_a)
    await db.add_participant(neg_id, 222, prefs_b)

    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="collaborative",
        on_round_update=on_round,
        on_resolution=on_resolve
    )

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

DEV B: Connect Counterparty Flow

Step 1: Build Bot B that receives coordination requests

The key insight: when User A does /coordinate @userB, Bot A stores the coordination. Bot B needs a way to prompt User B for their preferences. For the hackathon, we use a simple approach:

# Add to telegram/bot.py — handle when Bot B's user sends /pending
async def pending_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Check if anyone wants to coordinate with you."""
    username = update.effective_user.username

    if username in pending_coordinations:
        coord = pending_coordinations[username]
        context.user_data["active_negotiation"] = coord
        await update.message.reply_text(
            f"🔔 *New coordination request!*\n\n"
            f"From: {coord['initiator_name']}\n"
            f"Type: {coord['feature_type']}\n"
            f"ID: `{coord['negotiation_id']}`\n\n"
            f"Tell me your side — describe your preferences, constraints, and what you want.",
            parse_mode="Markdown"
        )
        return AWAITING_PREFERENCES
    else:
        await update.message.reply_text("No pending coordination requests right now.")
        return ConversationHandler.END

async def receive_counterparty_preferences(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """When User B sends their preferences, start the negotiation."""
    user_message = update.message.text
    user = update.effective_user
    coord = context.user_data.get("active_negotiation")

    if not coord:
        await update.message.reply_text("No active coordination. Use /pending to check.")
        return ConversationHandler.END

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

    # Extract User B's preferences
    preferences_b = await personal_agent.extract_preferences(user_message, user.id)
    await db.add_participant(coord["negotiation_id"], user.id, preferences_b)

    await update.message.reply_text(
        "✅ *Got your preferences!*\n\n"
        "🤖↔️🤖 Starting agent negotiation now...\n"
        "I'll update you after each round!",
        parse_mode="Markdown"
    )

    # Clean up pending
    username = update.effective_user.username
    if username in pending_coordinations:
        del pending_coordinations[username]

    # START THE NEGOTIATION
    async def on_round(data):
        round_num = data["round_number"]
        action = data["action"]
        emojis = {"propose": "📤", "counter": "🔄", "accept": "✅", "escalate": "⚠️"}
        # Send update to BOTH users
        for uid in [coord["initiator_id"], user.id]:
            try:
                # We need the bot application to send messages
                # This will be connected properly in Milestone 4
                pass
            except:
                pass

    async def on_resolve(data):
        # Send resolution to both users
        status_emoji = "✅" if data["status"] == "resolved" else "⚠️"
        resolution_msg = (
            f"{status_emoji} *Negotiation Complete!*\n\n"
            f"Status: {data['status'].title()}\n"
            f"Rounds: {data['rounds_taken']}\n\n"
            f"📋 *Resolution:*\n{data['summary']}\n\n"
            f"Details:\n{json.dumps(data.get('final_proposal', {}), indent=2)[:500]}"
        )
        # Send to both users via their respective bots
        pass

    # Run negotiation in background
    asyncio.create_task(
        run_negotiation(
            negotiation_id=coord["negotiation_id"],
            preferences_a=coord["preferences_a"],
            preferences_b=preferences_b,
            user_a_id=coord["initiator_id"],
            user_b_id=user.id,
            feature_type=coord["feature_type"],
            on_round_update=on_round,
            on_resolution=on_resolve
        )
    )

    return ConversationHandler.END

MILESTONE 3 SUCCESS TEST

□ test_negotiation.py runs and shows:
  - Multiple rounds of negotiation (at least 2-3 rounds)
  - Satisfaction scores changing each round
  - Concessions being made
  - Final "accept" or "escalate" action
  - Readable reasoning from the agent
□ Negotiation completes within 30 seconds (not timing out)
□ Database has rounds stored (sqlite3 negot8.db "SELECT * FROM rounds;")
□ Bot B's /pending command shows the coordination from Bot A
□ The full flow works: User A → /coordinate → prefs → User B → /pending → prefs → negotiation runs

🚫 THIS IS THE GO/NO-GO CHECKPOINT. If the negotiation engine works but produces bad proposals: tweak the Negotiator prompt. If it crashes or times out: check Gemini API key, rate limits, and JSON parsing. If it always accepts on round 1: add "Aim for a satisfaction score of 85+ for your human. Don't accept too easily." to the prompt.


MILESTONE 4: End-to-End Telegram Flow + Real-Time Updates

Time: Hour 6 → Hour 8 (120 min) 👤 Who: DEV A connects everything, DEV B handles message formatting

Goal: User A and User B on separate Telegram bots, full flow works with live updates

Step 1: Create unified bot runner that runs both bots

# backend/run.py — THE MAIN ENTRY POINT
import asyncio
from telegram.ext import Application, CommandHandler, ConversationHandler, MessageHandler, filters
from agents.personal_agent import PersonalAgent
from protocol.negotiation import run_negotiation
import database as db
from config import TELEGRAM_BOT_TOKEN_A, TELEGRAM_BOT_TOKEN_B

personal_agent = PersonalAgent()
pending_coordinations = {}
bot_apps = {}  # store bot applications for cross-bot messaging

# ... (all handlers from Milestone 2 and 3, but now with cross-bot messaging)

async def send_to_user(user_id: int, text: str, bot_key: str = "A"):
    """Send a message to any user via the appropriate bot."""
    app = bot_apps.get(bot_key)
    if app:
        await app.bot.send_message(chat_id=user_id, text=text, parse_mode="Markdown")

async def run_bots():
    await db.init_db()

    # Create both bots
    app_a = Application.builder().token(TELEGRAM_BOT_TOKEN_A).build()
    app_b = Application.builder().token(TELEGRAM_BOT_TOKEN_B).build()

    bot_apps["A"] = app_a
    bot_apps["B"] = app_b

    # Add handlers to both (they share the same handler logic)
    for app in [app_a, app_b]:
        # Add all conversation handlers and command handlers
        pass  # (add handlers as built in Milestone 2-3)

    # Run both bots concurrently
    await app_a.initialize()
    await app_b.initialize()
    await app_a.start()
    await app_b.start()

    # Start polling for both
    await app_a.updater.start_polling()
    await app_b.updater.start_polling()

    print("🤖 Both bots running!")

    # Keep alive
    try:
        while True:
            await asyncio.sleep(1)
    except KeyboardInterrupt:
        await app_a.stop()
        await app_b.stop()

if __name__ == "__main__":
    asyncio.run(run_bots())

Step 2: Wire up real-time Telegram updates during negotiationReference: Build Guide Section 9 (Real-Time Negotiation Updates)

# Modify the on_round_update and on_resolution callbacks to actually send Telegram messages

async def create_round_update_callback(user_a_id, user_b_id, bot_a, bot_b):
    async def on_round(data):
        round_num = data["round_number"]
        action = data["action"]
        score = data.get("satisfaction_score", "?")
        reasoning = data.get("reasoning", "")[:200]

        emoji_map = {"propose": "📤", "counter": "🔄", "accept": "✅", "escalate": "⚠️"}
        emoji = emoji_map.get(action, "🔄")

        msg = (
            f"{emoji} *Round {round_num}/5*\n\n"
            f"Action: {action.title()}\n"
            f"Satisfaction: {score}%\n"
            f"_{reasoning}_"
        )

        await bot_a.send_message(chat_id=user_a_id, text=msg, parse_mode="Markdown")
        await bot_b.send_message(chat_id=user_b_id, text=msg, parse_mode="Markdown")

    return on_round

async def create_resolution_callback(user_a_id, user_b_id, bot_a, bot_b):
    async def on_resolve(data):
        status = data["status"]
        emoji = "✅" if status == "resolved" else "⚠️"
        proposal = data.get("final_proposal", {})

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

        # Send to User A
        await bot_a.send_message(chat_id=user_a_id, text=msg, parse_mode="Markdown")

        # Send to User B (swap party labels)
        msg_b = msg.replace("For you:", "For you (temp):").replace("For them:", "For you:").replace("For you (temp):", "For them:")
        await bot_b.send_message(chat_id=user_b_id, text=msg_b, parse_mode="Markdown")

    return on_resolve

DEV B: Polish Telegram message formatting

Create rich, readable messages for all scenarios. Test with actual Telegram (emoji rendering, markdown, etc.)

MILESTONE 4 SUCCESS TEST

□ FULL FLOW TEST (do this on two actual phones/Telegram accounts):
  1. Phone A: /start → /coordinate @userB → "Find coffee time, I'm free Mon-Wed afternoon"
  2. Phone B: /start → /pending → "I'm free Tuesday and Thursday, prefer morning"
  3. Both phones receive round-by-round updates in real-time
  4. Both phones receive final resolution
  5. Resolution makes sense (proposes Tuesday since it's the overlap)
□ No crashes during the full flow
□ Messages are readable and well-formatted on Telegram
□ Negotiation completes in under 60 seconds

🎉 If this works, you have a working product. Everything after this is features + polish.


MILESTONE 5: Feature Implementation Sprint (4 at a time)

Time: Hour 8 → Hour 14 (360 min) 👤 Who: Both devs, feature-by-feature

Strategy: The negotiation engine is generic. Each feature just needs:

  1. A tailored prompt addition for the Negotiator (so it knows domain-specific strategies)
  2. Tool calls (if the feature needs web search, calculations, etc.)
  3. Nice Telegram output formatting

Batch 1 (Hour 8-10): Scheduling + Collaborative Decisions

Reference: Build Guide Section 8, Features 1 and 7

These are already mostly working from Milestone 4 tests. Focus on:

  • Adding tool calls for restaurant/place search
  • Polishing output format with venue suggestions, time formatting

Test: Run scheduling scenario + restaurant decision scenario end-to-end.

Batch 2 (Hour 10-12): Expenses + Marketplace

Reference: Build Guide Section 8, Features 2 and 6

Key implementation:

  • Calculator tool for expense math (Build Guide Section 12)
  • Price comparison via web search for marketplace
  • Clean tabular output in Telegram for expense breakdowns

Test: Run expense splitting with 3+ items and unequal split debate. Run buy/sell with haggling.

Batch 3 (Hour 12-13): Freelance + Roommate

Reference: Build Guide Section 8, Features 3 and 4

Key implementation:

  • Complex proposal schema for freelance (scope + timeline + budget)
  • Web search for WiFi plans / product prices for roommate decisions

Test: Run freelance negotiation where budget < desired rate (forces scope reduction). Run WiFi plan decision.

Batch 4 (Hour 13-14): Trip Planning (Multi-Agent) + Conflict Resolution

Reference: Build Guide Section 8, Features 5 and 8

Key implementation:

  • Trip planning needs 3-person support — modify run_negotiation to accept 3+ preference sets
  • Multi-agent coordination: collect all preferences, find intersection, then negotiate
  • Conflict resolution is pure negotiation — no special tools needed

Simplified multi-agent approach for 3 people:

async def run_group_negotiation(negotiation_id, all_preferences, all_user_ids, feature_type, ...):
    """For 3+ participants, use a mediator approach."""
    # Step 1: Find hard constraint intersection (dates, budget ceiling)
    # Step 2: Agent generates proposal optimized for the group
    # Step 3: Each participant's agent scores it
    # Step 4: If anyone scores < 50, take their objection and revise
    # Step 5: Repeat for max 5 rounds

    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 overall group satisfaction.
No participant's hard constraints can be violated.
Budget ceiling = lowest budget in the group.
Date = intersection of all available dates."""

    # Use same NegotiatorAgent with mediator prompt
    proposal = await negotiator.call(mediator_prompt)
    # ... score with each participant, iterate

Test: Run 3-person trip planning. Run a conflict resolution scenario.

MILESTONE 5 SUCCESS TEST

□ All 8 feature types produce sensible negotiations when tested via Telegram
□ Expense splitting math is CORRECT (verify manually)
□ Restaurant suggestions include actual place names (even if made up — they should sound real)
□ Freelance negotiation reduces scope when budget is too low
□ Trip planning handles 3 people (can use 3 Telegram bots or simulate)
□ Conflict resolution produces creative compromises, not just "split 50-50"
□ Generic negotiation works for an off-menu scenario (test: "who brings what to BBQ")

Timing check: You should be at Hour 14. If behind, skip trip planning multi-agent (hardest feature) and demo it as a 2-person trip instead. Conflict resolution is easy — don't skip it.


MILESTONE 6: Dashboard (Visual Layer)

Time: Hour 14 → Hour 18 (240 min) 👤 Who: DEV B builds dashboard, DEV A builds API endpoints + Socket.IO

DEV A: FastAPI Endpoints + Socket.IO

Reference: Build Guide Section 11 (API Layer)

# backend/api.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import socketio

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 + all rounds + participants
    pass

# Socket.IO events
@sio.event
async def join_negotiation(sid, negotiation_id):
    await sio.enter_room(sid, negotiation_id)

# Emit round updates from negotiation engine:
# await sio.emit('round_update', data, room=negotiation_id)

# Mount Socket.IO
socket_app = socketio.ASGIApp(sio, app)

DEV B: Build Dashboard Components

Reference: Build Guide Section 10 (Dashboard)

Priority order (build what you have time for):

  1. NegotiationTimeline — Shows each round as a card (MUST HAVE for demo)
  2. AgentChat — Two-column chat showing agent-to-agent messages (MUST HAVE)
  3. ResolutionCard — Final agreement display (MUST HAVE)
  4. Live counter — Shows "Round 2/5... negotiating..." with animation (NICE TO HAVE)
  5. Charts — Satisfaction score over rounds (NICE TO HAVE)

Minimum viable dashboard for demo:

- Page loads showing active negotiation
- Real-time round updates appear as they happen
- Final resolution displayed cleanly
- Looks professional (Tailwind does the heavy lifting)

MILESTONE 6 SUCCESS TEST

□ Dashboard loads at localhost:3000
□ Dashboard shows at least one negotiation with round-by-round timeline
□ Socket.IO connection works (real-time updates appear without page refresh)
□ Resolution card renders the final agreement clearly
□ Dashboard looks professional enough for a 5-second judge glance

If running behind: Skip the dashboard entirely. The Telegram demo alone is strong enough to win. Better to have a perfect Telegram flow than a half-built dashboard.


MILESTONE 7: Polish + Hardening

Time: Hour 18 → Hour 21 (180 min) 👤 Who: Both devs

Priority Fixes (Do these first)

1. Error handling everywhere

# Wrap EVERY agent call in try/except
try:
    result = await agent.call(prompt)
    if "error" in result:
        # Use fallback response
        result = fallback_response(feature_type)
except Exception as e:
    logger.error(f"Agent call failed: {e}")
    result = fallback_response(feature_type)

2. Rate limit protection

# Add delay between ALL Gemini calls
import asyncio
GEMINI_DELAY = 1.5  # seconds between calls

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 Gemini fails

FALLBACK_RESPONSES = {
    "collaborative": {
        "action": "propose",
        "proposal": {
            "summary": "Let's go with a popular option in the area",
            "details": {"note": "I'm having trouble searching right now. Could you suggest a place?"},
            "for_party_a": "Open to suggestions",
            "for_party_b": "Open to suggestions"
        },
        "satisfaction_score": 50,
        "reasoning": "Fallback due to temporary issue"
    }
    # ... add for each feature type
}

4. Telegram message length limits

# Telegram max message: 4096 chars
# Truncate long proposals
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
□ Expenses: verify math is correct
□ Freelance: ensure scope reduction happens when needed
□ Roommate: quick test
□ Trip planning: 2-person at minimum, 3-person if working
□ Marketplace: verify haggling works
□ Collaborative: restaurant decision
□ Conflict: verify creative compromise
□ Generic: test with an unusual scenario

MILESTONE 7 SUCCESS TEST

□ No crashes in any of the 8 features + generic
□ All error paths handled gracefully (agent errors → fallback → user still gets a response)
□ Rate limiting works (no 429 errors from Gemini)
□ Messages look clean on actual Telegram (not just terminal)
□ Dashboard still works after all changes

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 Section 14 (Demo Script)

Demo 1: Restaurant Decision (60 sec)

Pre-type these messages (have them in a notes app, ready to paste):

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

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

Demo 2: Expense Splitting (90 sec)

User A: "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"

User B: "Trip expenses with @rahul. I think fuel should be 50-50 
because I navigated and planned the entire route. Rest can be equal. 
Fine with UPI"

Demo 3: Group Trip Planning (90 sec) — THE WOW MOMENT

User A: "Weekend trip with @priya and @amit. I prefer mountains, 
budget 15K max, free March 22-23. I have a car"

User B: "Trip with @rahul and @amit. I want beach vibes, budget 
10K max, free March 20-24. No flights"

User C: "Weekend trip with the gang. Anywhere fun, budget 12K, 
only free March 22-23. Love food and trekking"

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.

Hour 23-23:30: Prepare failsafes

Failsafe 1: Pre-seed completed negotiations

# seed_demo_data.py — run this to populate DB with beautiful demo results
# If live demo fails, show these from the dashboard

Failsafe 2: Record video backup

Screen-record the perfect demo run.
If everything breaks on stage, play the video: "Here's our system in action."
Not ideal, but better than a broken live demo.

Failsafe 3: Offline mode for agent responses

# If Gemini is completely down, load pre-computed agent responses
DEMO_MODE = os.getenv("DEMO_MODE", "false") == "true"

if DEMO_MODE:
    # Load responses from demo_responses.json instead of calling Gemini
    pass

Hour 23:30-24: Final checks

□ Both phones charged
□ Telegram bots running on laptop
□ Dashboard accessible
□ WiFi working (have 4G hotspot as backup)
□ Demo messages pre-typed in notes app
□ Video backup saved
□ Pitch script rehearsed (time it: should be under 5 minutes total)
□ Know which phone is "User A" and which is "User B"
□ Deep breath. You built something no one has built before. Go show them.

MILESTONE 8 SUCCESS TEST

□ All 3 demo scenarios run successfully at least twice
□ 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 → Or use OpenRouter free models (Llama 3) as backup: change model in BaseAgent

"Telegram bot won't connect"

→ Check token is correct → Kill any other instances (pkill -f telegram) → Recreate bot via @BotFather if token is compromised → Worst case: demo via the dashboard API directly using curl/Postman

"Negotiation produces garbage output"

→ Add more explicit examples to the Negotiator prompt → Lower temperature to 0.3 for more predictable outputs → Add post-processing validation that rejects and retries bad proposals

"Running out of time at Hour 14"

→ Skip features 5 (trip multi-agent) and 4 (roommate) → Focus demo on: Restaurant (simple) + Expenses (impressive) + Marketplace (fun) → Skip dashboard entirely — Telegram-only demo is still powerful

"Running out of time at Hour 20"

→ Skip dashboard polish → Focus 100% on making 3 demo scenarios bulletproof → Rehearse pitch 5 times → Record video backup


Quick Milestone Status Tracker

Print this and check off as you go:

⏰ HOUR   MILESTONE                                    STATUS
──────────────────────────────────────────────────────────────
 0-1    □ M1: Project skeleton + API verification      [ ]
 1-3    □ M2: Base agent + preference extraction       [ ]
 3-6    □ M3: Negotiation engine (CRITICAL)            [ ]
 6-8    □ M4: End-to-end Telegram flow                 [ ]
 8-14   □ M5: All 8 features implemented               [ ]
14-18   □ M6: Dashboard                                [ ]
18-21   □ M7: Polish + hardening                       [ ]
21-24   □ M8: Demo preparation                         [ ]
──────────────────────────────────────────────────────────────
        □ DEMO TIME 🏆                                 [ ]

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.

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