mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
1595 lines
58 KiB
Markdown
1595 lines
58 KiB
Markdown
# 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**
|
||
```bash
|
||
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**
|
||
```bash
|
||
pip install fastapi uvicorn "python-telegram-bot[all]" google-generativeai httpx pydantic python-dotenv aiosqlite python-socketio
|
||
```
|
||
|
||
**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
|
||
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")
|
||
DATABASE_PATH = os.getenv("DATABASE_PATH", "negot8.db")
|
||
API_URL = os.getenv("NEXT_PUBLIC_API_URL", "http://localhost:8000")
|
||
```
|
||
|
||
**Step 5: Initialize database**
|
||
→ **Reference: Build Guide 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 '{}'
|
||
);
|
||
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**
|
||
```python
|
||
# 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)
|
||
```
|
||
```bash
|
||
python test_gemini.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_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**
|
||
```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 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 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**
|
||
```python
|
||
# 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())
|
||
```
|
||
```bash
|
||
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**
|
||
```python
|
||
# 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**
|
||
```python
|
||
# 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**
|
||
```python
|
||
# 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**
|
||
```python
|
||
# 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)**
|
||
```python
|
||
# 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**
|
||
```python
|
||
# 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())
|
||
```
|
||
```bash
|
||
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:
|
||
|
||
```python
|
||
# 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**
|
||
```python
|
||
# 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 negotiation**
|
||
→ **Reference: Build Guide Section 9 (Real-Time Negotiation Updates)**
|
||
|
||
```python
|
||
# 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:**
|
||
```python
|
||
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)**
|
||
|
||
```python
|
||
# 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**
|
||
```python
|
||
# 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**
|
||
```python
|
||
# 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**
|
||
```python
|
||
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**
|
||
```python
|
||
# 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**
|
||
```python
|
||
# 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**
|
||
```python
|
||
# 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. 🏆** |