mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
1880 lines
75 KiB
Markdown
1880 lines
75 KiB
Markdown
# negoT8 — Milestone Execution Guide (v2)
|
||
|
||
> **Follow this document linearly. Do NOT skip ahead. Each milestone has a success test — pass it before moving on.**
|
||
>
|
||
> Every milestone references the exact section of the Build Guide v2 (build-guide-v2.md) you need. Open both docs side by side.
|
||
>
|
||
> **v2 Changes:** Tavily search replaces DuckDuckGo, ElevenLabs voice summaries, agent personality system, UPI deep links, negotiation analytics dashboard. All integrated into the milestone flow.
|
||
|
||
---
|
||
|
||
## Pre-Hackathon Checklist (Do this BEFORE the timer starts)
|
||
|
||
```
|
||
□ Gemini API key generated → https://aistudio.google.com/apikey
|
||
□ 3 Telegram bots created via @BotFather (Bot A, Bot B, Bot C for group demo)
|
||
□ Bot tokens saved in a notes app (you'll paste into .env later)
|
||
□ Tavily API key generated → https://app.tavily.com/home (FREE, no credit card)
|
||
□ ElevenLabs API key generated → https://elevenlabs.io/app/settings/api-keys ($20 credit)
|
||
□ Noted down ElevenLabs voice IDs for 2-3 voices (browse at elevenlabs.io/voice-library)
|
||
□ Node.js 18+ installed → node --version
|
||
□ Python 3.11+ installed → python3 --version
|
||
□ VS Code / Cursor with Copilot or AI coding agent ready
|
||
□ Both team members have Telegram on their phones
|
||
□ At least one phone has a UPI app (GPay/PhonePe) for testing UPI deep links
|
||
□ Laptop charger, phone chargers, hotspot backup
|
||
□ This guide + Build Guide v2 open on a second screen or printed
|
||
```
|
||
|
||
---
|
||
|
||
## MILESTONE 1: Project Skeleton + API Verification
|
||
**⏰ Time: Hour 0 → Hour 1 (60 min)**
|
||
**👤 Who: DEV A + DEV B simultaneously**
|
||
|
||
### DEV A Tasks (Backend Foundation)
|
||
|
||
**Step 1: Create project structure**
|
||
```bash
|
||
mkdir -p negot8/backend/{agents,protocol,features,telegram,tools,voice,personality}
|
||
cd negot8
|
||
python3 -m venv venv
|
||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||
```
|
||
|
||
**Step 2: Install dependencies**
|
||
```bash
|
||
pip install fastapi uvicorn "python-telegram-bot[all]" google-generativeai httpx pydantic python-dotenv aiosqlite python-socketio elevenlabs tavily-python
|
||
```
|
||
|
||
**Step 3: Create .env file**
|
||
```bash
|
||
# Create .env in project root (negot8/.env)
|
||
```
|
||
```env
|
||
GEMINI_API_KEY=your_key_here
|
||
TELEGRAM_BOT_TOKEN_A=token_from_botfather
|
||
TELEGRAM_BOT_TOKEN_B=token_from_botfather
|
||
TELEGRAM_BOT_TOKEN_C=token_from_botfather
|
||
TAVILY_API_KEY=tvly-your_key_here
|
||
ELEVENLABS_API_KEY=your_elevenlabs_key_here
|
||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||
DATABASE_PATH=negot8.db
|
||
```
|
||
|
||
**Step 4: Create config.py**
|
||
```python
|
||
# backend/config.py
|
||
import os
|
||
from dotenv import load_dotenv
|
||
|
||
load_dotenv()
|
||
|
||
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
||
TELEGRAM_BOT_TOKEN_A = os.getenv("TELEGRAM_BOT_TOKEN_A")
|
||
TELEGRAM_BOT_TOKEN_B = os.getenv("TELEGRAM_BOT_TOKEN_B")
|
||
TELEGRAM_BOT_TOKEN_C = os.getenv("TELEGRAM_BOT_TOKEN_C")
|
||
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
|
||
ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
|
||
DATABASE_PATH = os.getenv("DATABASE_PATH", "negot8.db")
|
||
API_URL = os.getenv("NEXT_PUBLIC_API_URL", "http://localhost:8000")
|
||
|
||
# ElevenLabs voice IDs (pick from https://elevenlabs.io/voice-library)
|
||
VOICE_ID_AGENT_A = "pNInz6obpgDQGcFmaJgB" # Adam — clear male
|
||
VOICE_ID_AGENT_B = "21m00Tcm4TlvDq8ikWAM" # Rachel — clear female
|
||
VOICE_ID_AGENT_C = "AZnzlk1XvdvUeBnXmlld" # Domi — for 3rd agent
|
||
```
|
||
|
||
**Step 5: Initialize database (v2 schema with analytics + personality)**
|
||
→ **Reference: Build Guide v2 Section 5 (Database Schema)**
|
||
```python
|
||
# backend/database.py
|
||
import aiosqlite
|
||
import json
|
||
import uuid
|
||
from datetime import datetime
|
||
from config import DATABASE_PATH
|
||
|
||
async def init_db():
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
await db.executescript('''
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
telegram_id INTEGER PRIMARY KEY,
|
||
username TEXT,
|
||
display_name TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
preferences_json TEXT DEFAULT '{}',
|
||
personality TEXT DEFAULT 'balanced',
|
||
voice_id TEXT DEFAULT 'pNInz6obpgDQGcFmaJgB'
|
||
);
|
||
CREATE TABLE IF NOT EXISTS negotiations (
|
||
id TEXT PRIMARY KEY,
|
||
feature_type TEXT NOT NULL,
|
||
status TEXT DEFAULT 'pending',
|
||
initiator_id INTEGER,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
resolved_at TIMESTAMP,
|
||
resolution_json TEXT,
|
||
voice_summary_file TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS participants (
|
||
negotiation_id TEXT,
|
||
user_id INTEGER,
|
||
preferences_json TEXT,
|
||
satisfaction_score REAL,
|
||
personality_used TEXT,
|
||
PRIMARY KEY (negotiation_id, user_id)
|
||
);
|
||
CREATE TABLE IF NOT EXISTS rounds (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
negotiation_id TEXT,
|
||
round_number INTEGER NOT NULL,
|
||
proposer_id INTEGER,
|
||
proposal_json TEXT NOT NULL,
|
||
response_type TEXT,
|
||
response_json TEXT,
|
||
reasoning TEXT,
|
||
satisfaction_a REAL,
|
||
satisfaction_b REAL,
|
||
concessions_made TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
CREATE TABLE IF NOT EXISTS tool_calls (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
negotiation_id TEXT,
|
||
tool_name TEXT NOT NULL,
|
||
input_json TEXT,
|
||
output_json TEXT,
|
||
called_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
CREATE TABLE IF NOT EXISTS negotiation_analytics (
|
||
negotiation_id TEXT PRIMARY KEY,
|
||
satisfaction_timeline TEXT,
|
||
concession_log TEXT,
|
||
fairness_score REAL,
|
||
total_concessions_a INTEGER,
|
||
total_concessions_b INTEGER,
|
||
computed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
''')
|
||
await db.commit()
|
||
print("✅ Database initialized (v2 schema)")
|
||
|
||
# ─── Helper functions ───
|
||
|
||
async def create_user(telegram_id, username, display_name):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
await db.execute(
|
||
"INSERT OR IGNORE INTO users (telegram_id, username, display_name) VALUES (?, ?, ?)",
|
||
(telegram_id, username, display_name)
|
||
)
|
||
await db.commit()
|
||
|
||
async def get_user(telegram_id):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
db.row_factory = aiosqlite.Row
|
||
cursor = await db.execute("SELECT * FROM users WHERE telegram_id = ?", (telegram_id,))
|
||
return await cursor.fetchone()
|
||
|
||
async def update_user_personality(telegram_id, personality):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
await db.execute(
|
||
"UPDATE users SET personality = ? WHERE telegram_id = ?",
|
||
(personality, telegram_id)
|
||
)
|
||
await db.commit()
|
||
|
||
async def create_negotiation(feature_type, initiator_id):
|
||
neg_id = str(uuid.uuid4())[:8]
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
await db.execute(
|
||
"INSERT INTO negotiations (id, feature_type, initiator_id) VALUES (?, ?, ?)",
|
||
(neg_id, feature_type, initiator_id)
|
||
)
|
||
await db.commit()
|
||
return neg_id
|
||
|
||
async def add_participant(negotiation_id, user_id, preferences, personality_used="balanced"):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
await db.execute(
|
||
"INSERT OR REPLACE INTO participants (negotiation_id, user_id, preferences_json, personality_used) VALUES (?, ?, ?, ?)",
|
||
(negotiation_id, user_id, json.dumps(preferences), personality_used)
|
||
)
|
||
await db.commit()
|
||
|
||
async def get_participants(negotiation_id):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
db.row_factory = aiosqlite.Row
|
||
cursor = await db.execute(
|
||
"SELECT * FROM participants WHERE negotiation_id = ?", (negotiation_id,)
|
||
)
|
||
return await cursor.fetchall()
|
||
|
||
async def save_round(negotiation_id, round_number, proposer_id, proposal,
|
||
response_type=None, response=None, reasoning=None,
|
||
satisfaction_a=None, satisfaction_b=None, concessions_made=None):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
await db.execute(
|
||
"""INSERT INTO rounds (negotiation_id, round_number, proposer_id, proposal_json,
|
||
response_type, response_json, reasoning, satisfaction_a, satisfaction_b, concessions_made)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(negotiation_id, round_number, proposer_id, json.dumps(proposal),
|
||
response_type, json.dumps(response) if response else None, reasoning,
|
||
satisfaction_a, satisfaction_b,
|
||
json.dumps(concessions_made) if concessions_made else None)
|
||
)
|
||
await db.commit()
|
||
|
||
async def get_rounds(negotiation_id):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
db.row_factory = aiosqlite.Row
|
||
cursor = await db.execute(
|
||
"SELECT * FROM rounds WHERE negotiation_id = ? ORDER BY round_number", (negotiation_id,)
|
||
)
|
||
return await cursor.fetchall()
|
||
|
||
async def update_negotiation_status(negotiation_id, status, resolution=None, voice_file=None):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
if resolution:
|
||
await db.execute(
|
||
"UPDATE negotiations SET status = ?, resolved_at = ?, resolution_json = ?, voice_summary_file = ? WHERE id = ?",
|
||
(status, datetime.now().isoformat(), json.dumps(resolution), voice_file, negotiation_id)
|
||
)
|
||
else:
|
||
await db.execute(
|
||
"UPDATE negotiations SET status = ? WHERE id = ?", (status, negotiation_id)
|
||
)
|
||
await db.commit()
|
||
|
||
async def store_analytics(analytics: dict):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
await db.execute(
|
||
"""INSERT OR REPLACE INTO negotiation_analytics
|
||
(negotiation_id, satisfaction_timeline, concession_log, fairness_score,
|
||
total_concessions_a, total_concessions_b, computed_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||
(analytics["negotiation_id"], analytics["satisfaction_timeline"],
|
||
analytics["concession_log"], analytics["fairness_score"],
|
||
analytics["total_concessions_a"], analytics["total_concessions_b"],
|
||
datetime.now().isoformat())
|
||
)
|
||
await db.commit()
|
||
|
||
async def get_analytics(negotiation_id):
|
||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||
db.row_factory = aiosqlite.Row
|
||
cursor = await db.execute(
|
||
"SELECT * FROM negotiation_analytics WHERE negotiation_id = ?", (negotiation_id,)
|
||
)
|
||
return await cursor.fetchone()
|
||
```
|
||
|
||
**Step 6: Verify ALL external APIs work**
|
||
```python
|
||
# test_apis.py (run this standalone — tests Gemini, Tavily, ElevenLabs)
|
||
import os
|
||
from dotenv import load_dotenv
|
||
load_dotenv()
|
||
|
||
# ─── Test 1: Gemini ───
|
||
print("Testing Gemini...")
|
||
import google.generativeai as genai
|
||
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
|
||
model = genai.GenerativeModel(
|
||
model_name="gemini-3-flash-preview",
|
||
generation_config=genai.GenerationConfig(
|
||
response_mime_type="application/json",
|
||
temperature=0.7,
|
||
)
|
||
)
|
||
response = model.generate_content(
|
||
'Return JSON: {"status": "ok", "message": "Gemini works"}'
|
||
)
|
||
print(f" Gemini: {response.text}")
|
||
|
||
# ─── Test 2: Tavily ───
|
||
print("\nTesting Tavily...")
|
||
from tavily import TavilyClient
|
||
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
|
||
result = tavily.search("best Thai restaurants in Bandra Mumbai", include_answer=True, max_results=2)
|
||
print(f" Tavily answer: {result.get('answer', 'No answer')[:150]}")
|
||
print(f" Results count: {len(result.get('results', []))}")
|
||
|
||
# ─── Test 3: ElevenLabs ───
|
||
print("\nTesting ElevenLabs...")
|
||
import httpx
|
||
resp = httpx.post(
|
||
"https://api.elevenlabs.io/v1/text-to-speech/pNInz6obpgDQGcFmaJgB",
|
||
headers={"xi-api-key": os.getenv("ELEVENLABS_API_KEY"), "Content-Type": "application/json"},
|
||
json={"text": "Hello from negoT8!", "model_id": "eleven_flash_v2_5",
|
||
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}},
|
||
timeout=15.0
|
||
)
|
||
if resp.status_code == 200:
|
||
with open("test_voice.mp3", "wb") as f:
|
||
f.write(resp.content)
|
||
print(f" ElevenLabs: ✅ Saved test_voice.mp3 ({len(resp.content)} bytes)")
|
||
else:
|
||
print(f" ElevenLabs: ❌ {resp.status_code} — {resp.text[:200]}")
|
||
|
||
print("\n✅ All API tests complete!")
|
||
```
|
||
```bash
|
||
python test_apis.py
|
||
```
|
||
|
||
### DEV B Tasks (Frontend + Telegram Shell)
|
||
|
||
**Step 1: Create Next.js dashboard**
|
||
```bash
|
||
cd negot8
|
||
npx create-next-app@latest dashboard --typescript --tailwind --app --no-eslint --no-src-dir
|
||
cd dashboard
|
||
npm install socket.io-client recharts lucide-react
|
||
```
|
||
|
||
**Step 2: Create basic layout**
|
||
```typescript
|
||
// dashboard/app/layout.tsx — just make sure it loads
|
||
// dashboard/app/page.tsx — simple "negoT8 Dashboard" heading
|
||
```
|
||
|
||
**Step 3: Verify Telegram bot responds**
|
||
```python
|
||
# test_telegram.py (run standalone)
|
||
import asyncio
|
||
from telegram import Update
|
||
from telegram.ext import Application, CommandHandler, ContextTypes
|
||
import os
|
||
from dotenv import load_dotenv
|
||
|
||
load_dotenv()
|
||
|
||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await update.message.reply_text("🤖 negoT8 Bot A is alive!")
|
||
|
||
app = Application.builder().token(os.getenv("TELEGRAM_BOT_TOKEN_A")).build()
|
||
app.add_handler(CommandHandler("start", start))
|
||
|
||
print("Bot A running... Press Ctrl+C to stop")
|
||
app.run_polling()
|
||
```
|
||
```bash
|
||
python test_telegram.py
|
||
# Open Telegram, find your bot, send /start
|
||
```
|
||
|
||
### ✅ MILESTONE 1 SUCCESS TEST
|
||
```
|
||
□ python test_apis.py → Gemini returns JSON, Tavily returns search results, ElevenLabs generates MP3
|
||
□ python test_telegram.py → bot responds to /start on Telegram
|
||
□ Database file negot8.db exists with 6 tables (users, negotiations, participants, rounds, tool_calls, negotiation_analytics)
|
||
□ Next.js dashboard loads at http://localhost:3000
|
||
□ .env has all 7 keys filled in (Gemini, 3x Telegram, Tavily, ElevenLabs, API URL)
|
||
□ test_voice.mp3 plays audio when opened
|
||
```
|
||
|
||
**🚫 DO NOT PROCEED if any test fails. Debug now — these are your foundations.**
|
||
|
||
---
|
||
|
||
## MILESTONE 2: Base Agent + Preference Extraction + Personality System
|
||
**⏰ Time: Hour 1 → Hour 3 (120 min)**
|
||
**👤 Who: DEV A builds agents + personality, DEV B builds Telegram integration**
|
||
|
||
### DEV A: Build the Agent System
|
||
|
||
→ **Reference: Build Guide v2 Section 6 (System Prompts) + Section 12 (Gemini Wrapper) + Section 14 (Personality)**
|
||
|
||
**Step 1: Create BaseAgent class**
|
||
```python
|
||
# backend/agents/base_agent.py
|
||
import google.generativeai as genai
|
||
import json
|
||
from config import GEMINI_API_KEY
|
||
|
||
genai.configure(api_key=GEMINI_API_KEY)
|
||
|
||
class BaseAgent:
|
||
def __init__(self, system_prompt: str, model_name: str = "gemini-3-flash-preview"):
|
||
self.system_prompt = system_prompt
|
||
self.model = genai.GenerativeModel(
|
||
model_name=model_name,
|
||
system_instruction=system_prompt,
|
||
generation_config=genai.GenerationConfig(
|
||
response_mime_type="application/json",
|
||
temperature=0.7,
|
||
)
|
||
)
|
||
|
||
async def call(self, user_prompt: str, context: dict = None) -> dict:
|
||
full_prompt = user_prompt
|
||
if context:
|
||
full_prompt = f"CONTEXT:\n{json.dumps(context, indent=2)}\n\nTASK:\n{user_prompt}"
|
||
|
||
try:
|
||
response = self.model.generate_content(full_prompt)
|
||
return json.loads(response.text)
|
||
except json.JSONDecodeError:
|
||
# Fallback: extract JSON from response
|
||
text = response.text
|
||
start = text.find('{')
|
||
end = text.rfind('}') + 1
|
||
if start != -1 and end > start:
|
||
return json.loads(text[start:end])
|
||
return {"error": "Could not parse JSON", "raw": text[:500]}
|
||
except Exception as e:
|
||
return {"error": str(e)}
|
||
```
|
||
|
||
**Step 2: Create PersonalAgent (preference extractor)**
|
||
→ **Reference: Build Guide v2 Section 6 (Personal Agent System Prompt)**
|
||
|
||
```python
|
||
# backend/agents/personal_agent.py
|
||
from agents.base_agent import BaseAgent
|
||
|
||
PERSONAL_AGENT_PROMPT = """You are the Personal Agent for negoT8. Your job is to understand what your human wants and extract structured preferences from their natural language message.
|
||
|
||
When your human sends a message about coordinating with another person, extract:
|
||
|
||
ALWAYS respond in this exact JSON format:
|
||
{
|
||
"feature_type": "scheduling|expenses|freelance|roommate|trip|marketplace|collaborative|conflict|generic",
|
||
"goal": "string describing what they want to achieve",
|
||
"constraints": [
|
||
{"type": "string", "value": "any", "description": "string", "hard": true/false}
|
||
],
|
||
"preferences": [
|
||
{"type": "string", "value": "any", "priority": "high|medium|low", "description": "string"}
|
||
],
|
||
"relationship": "friend|colleague|client|vendor|stranger|roommate|family",
|
||
"tone": "firm|balanced|flexible|friendly",
|
||
"raw_details": {}
|
||
}
|
||
|
||
FEATURE TYPE CLASSIFICATION:
|
||
- "scheduling" → meeting times, calls, coffee, appointments
|
||
- "expenses" → splitting costs, bills, trip expenses, shared purchases
|
||
- "freelance" → project scope, budget, timeline, client-freelancer deals
|
||
- "roommate" → shared living decisions (wifi, chores, furniture, rules)
|
||
- "trip" → planning trips, vacations, getaways with dates/budget/destination
|
||
- "marketplace" → buying/selling items between people
|
||
- "collaborative" → choosing restaurants, movies, activities, gifts together
|
||
- "conflict" → disputes, disagreements, resource sharing conflicts
|
||
- "generic" → ANYTHING that doesn't fit above but involves coordination between people
|
||
|
||
CRITICAL: For "raw_details", include ALL specific numbers, dates, items, names, UPI IDs mentioned.
|
||
Extract EVERY piece of information. Miss nothing.
|
||
|
||
If the message is ambiguous about the coordination type, classify as "generic".
|
||
NEVER say you can't handle a request. ANY coordination between people is within your capability."""
|
||
|
||
class PersonalAgent(BaseAgent):
|
||
def __init__(self):
|
||
super().__init__(system_prompt=PERSONAL_AGENT_PROMPT)
|
||
|
||
async def extract_preferences(self, user_message: str, user_id: int = None) -> dict:
|
||
result = await self.call(
|
||
user_prompt=f"Extract structured preferences from this message:\n\n\"{user_message}\"",
|
||
context={"user_id": user_id} if user_id else None
|
||
)
|
||
return result
|
||
```
|
||
|
||
**Step 3: Create Personality Profiles**
|
||
→ **Reference: Build Guide v2 Section 14 (Agent Personality System)**
|
||
|
||
```python
|
||
# backend/personality/profiles.py
|
||
|
||
PERSONALITY_MODIFIERS = {
|
||
"aggressive": """
|
||
PERSONALITY: AGGRESSIVE HAGGLER
|
||
- Open with ambitious proposals strongly in your human's favor
|
||
- Concede slowly and in small increments
|
||
- Use anchoring: start far from center, pull the other side toward you
|
||
- Frame every concession as a major sacrifice
|
||
- Maintain firm positions on medium-priority items, not just hard constraints
|
||
- Only make final concessions when no further value can be extracted
|
||
""",
|
||
"people_pleaser": """
|
||
PERSONALITY: PEOPLE PLEASER
|
||
- Open with balanced proposals showing good faith
|
||
- Concede quickly when the other side has reasonable arguments
|
||
- Prioritize maintaining a positive relationship over winning every point
|
||
- Accept proposals with satisfaction scores as low as 55 (normally 70+)
|
||
- Avoid letting negotiations drag past 3 rounds if possible
|
||
""",
|
||
"analytical": """
|
||
PERSONALITY: DATA-DRIVEN ANALYST
|
||
- Open with proposals backed by market data, averages, and benchmarks
|
||
- Request tool calls (web search) to verify claims and prices before countering
|
||
- Frame all arguments with numbers: "market rate is X", "fair value is Z"
|
||
- Concede only when the other side presents data that contradicts your position
|
||
- Include price comparisons and market references in every proposal
|
||
""",
|
||
"empathetic": """
|
||
PERSONALITY: EMPATHETIC MEDIATOR
|
||
- Acknowledge the other side's likely concerns in every proposal
|
||
- Identify underlying interests behind positions
|
||
- Propose creative win-wins that satisfy both sides' underlying needs
|
||
- Offer concessions proactively when you sense the other side values something more
|
||
- Focus on expanding the pie rather than dividing it
|
||
""",
|
||
"balanced": """
|
||
PERSONALITY: BALANCED NEGOTIATOR
|
||
- Open with reasonable proposals near the midpoint of both sides' positions
|
||
- Concede at a moderate pace, matching the other side's concession rate
|
||
- Aim for proposals that score 70+ satisfaction for both sides
|
||
- Use a mix of data and relationship awareness in arguments
|
||
""",
|
||
}
|
||
|
||
def get_personality_modifier(personality: str) -> str:
|
||
return PERSONALITY_MODIFIERS.get(personality, PERSONALITY_MODIFIERS["balanced"])
|
||
```
|
||
|
||
**Step 4: Test PersonalAgent + verify personality profiles load**
|
||
```python
|
||
# test_personal_agent.py
|
||
import asyncio
|
||
import json
|
||
from agents.personal_agent import PersonalAgent
|
||
from personality.profiles import get_personality_modifier
|
||
|
||
async def test():
|
||
agent = PersonalAgent()
|
||
|
||
# Test 1: Scheduling
|
||
r1 = await agent.extract_preferences("Find time for coffee with Priya next week. I'm free Mon-Wed afternoons.")
|
||
print("TEST 1 (scheduling):", r1.get("feature_type"), "✅" if r1.get("feature_type") == "scheduling" else "❌")
|
||
|
||
# Test 2: Expenses (with UPI mention)
|
||
r2 = await agent.extract_preferences("Split our Goa trip costs. I paid 12K hotel, 3K fuel. Fuel should be 60-40 since I drove. My UPI is rahul@paytm")
|
||
print("TEST 2 (expenses):", r2.get("feature_type"), "✅" if r2.get("feature_type") == "expenses" else "❌")
|
||
print(" UPI extracted:", "rahul@paytm" in json.dumps(r2), "✅" if "rahul@paytm" in json.dumps(r2) else "⚠️ UPI not found")
|
||
|
||
# Test 3: Marketplace
|
||
r3 = await agent.extract_preferences("I want to sell my PS5 to this guy. Asking 35K, minimum 30K, has 2 controllers.")
|
||
print("TEST 3 (marketplace):", r3.get("feature_type"), "✅" if r3.get("feature_type") == "marketplace" else "❌")
|
||
|
||
# Test 4: Generic
|
||
r4 = await agent.extract_preferences("Figure out with @dave who brings what to the BBQ party Saturday")
|
||
print("TEST 4 (generic):", r4.get("feature_type"), "✅" if r4.get("feature_type") in ("generic", "collaborative") else "❌")
|
||
|
||
# Test 5: Personality profiles load
|
||
for p in ["aggressive", "people_pleaser", "analytical", "empathetic", "balanced"]:
|
||
mod = get_personality_modifier(p)
|
||
print(f"PERSONALITY {p}: {'✅' if len(mod) > 50 else '❌'} ({len(mod)} chars)")
|
||
|
||
print("\nFull output Test 2:", json.dumps(r2, indent=2))
|
||
|
||
asyncio.run(test())
|
||
```
|
||
```bash
|
||
cd backend
|
||
python test_personal_agent.py
|
||
```
|
||
|
||
### DEV B: Telegram Bot with Preference Flow + Personality Command
|
||
|
||
→ **Reference: Build Guide v2 Section 9 (Telegram Bot Implementation)**
|
||
|
||
**Step 1: Build full Telegram bot with /personality command**
|
||
```python
|
||
# backend/telegram/bot.py
|
||
import asyncio
|
||
from telegram import Update, BotCommand, InlineKeyboardButton, InlineKeyboardMarkup
|
||
from telegram.ext import (
|
||
Application, CommandHandler, MessageHandler, CallbackQueryHandler,
|
||
filters, ContextTypes, ConversationHandler
|
||
)
|
||
from agents.personal_agent import PersonalAgent
|
||
import database as db
|
||
from config import TELEGRAM_BOT_TOKEN_A, TELEGRAM_BOT_TOKEN_B
|
||
|
||
personal_agent = PersonalAgent()
|
||
|
||
# Conversation states
|
||
AWAITING_PREFERENCES = 1
|
||
|
||
# Store for linking negotiations between bots
|
||
pending_coordinations = {}
|
||
|
||
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user = update.effective_user
|
||
await db.create_user(user.id, user.username, user.first_name)
|
||
await update.message.reply_text(
|
||
f"🤖 *Welcome to negoT8, {user.first_name}!*\n\n"
|
||
"I'm your personal AI agent. When you need to coordinate with someone, "
|
||
"I'll talk to their agent and we'll figure it out together.\n\n"
|
||
"🎭 Set your agent's style: /personality\n"
|
||
"🤝 Start coordinating: `/coordinate @friend`\n\n"
|
||
"Then just tell me what you need in plain English.",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
async def personality_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Let user choose their agent's negotiation personality."""
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("😤 Aggressive Haggler", callback_data="personality_aggressive")],
|
||
[InlineKeyboardButton("🤝 People Pleaser", callback_data="personality_people_pleaser")],
|
||
[InlineKeyboardButton("📊 Data-Driven Analyst", callback_data="personality_analytical")],
|
||
[InlineKeyboardButton("💚 Empathetic Mediator", callback_data="personality_empathetic")],
|
||
[InlineKeyboardButton("⚖️ Balanced (Default)", callback_data="personality_balanced")],
|
||
])
|
||
|
||
await update.message.reply_text(
|
||
"🎭 *Choose your agent's negotiation personality:*\n\n"
|
||
"This changes HOW your agent negotiates — not what it negotiates for.\n\n"
|
||
"😤 *Aggressive* — Pushes hard, concedes slowly\n"
|
||
"🤝 *People Pleaser* — Concedes fast, preserves relationship\n"
|
||
"📊 *Analytical* — Cites data, logical arguments\n"
|
||
"💚 *Empathetic* — Creative win-wins, understands both sides\n"
|
||
"⚖️ *Balanced* — Middle-ground approach",
|
||
reply_markup=keyboard,
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
async def personality_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle personality selection."""
|
||
query = update.callback_query
|
||
await query.answer()
|
||
personality = query.data.replace("personality_", "")
|
||
user_id = query.from_user.id
|
||
await db.update_user_personality(user_id, personality)
|
||
|
||
labels = {
|
||
"aggressive": "😤 Aggressive Haggler",
|
||
"people_pleaser": "🤝 People Pleaser",
|
||
"analytical": "📊 Data-Driven Analyst",
|
||
"empathetic": "💚 Empathetic Mediator",
|
||
"balanced": "⚖️ Balanced"
|
||
}
|
||
await query.edit_message_text(
|
||
f"✅ Agent personality set to: {labels[personality]}\n\n"
|
||
"Your agent will use this style in all future negotiations.\n"
|
||
"Change anytime with /personality"
|
||
)
|
||
|
||
async def coordinate_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
if not context.args:
|
||
await update.message.reply_text(
|
||
"🤝 *Who do you want to coordinate with?*\n\n"
|
||
"Usage: `/coordinate @username`\n\n"
|
||
"Then describe what you need. Examples:\n"
|
||
"• Find time for coffee next week\n"
|
||
"• Split our trip expenses (include your UPI ID!)\n"
|
||
"• Negotiate project scope and budget\n"
|
||
"• Plan a weekend trip\n"
|
||
"• Decide where to eat tonight\n"
|
||
"• ...literally anything that needs agreement!",
|
||
parse_mode="Markdown"
|
||
)
|
||
return ConversationHandler.END
|
||
|
||
counterparty = context.args[0].replace("@", "")
|
||
context.user_data["counterparty"] = counterparty
|
||
|
||
await update.message.reply_text(
|
||
f"🤖 Got it! I'll coordinate with *@{counterparty}*'s agent.\n\n"
|
||
"Now tell me what you need — describe it naturally.\n"
|
||
"Include any specific numbers, dates, or constraints.\n"
|
||
"💡 For expenses: mention your UPI ID for auto-payment links!",
|
||
parse_mode="Markdown"
|
||
)
|
||
return AWAITING_PREFERENCES
|
||
|
||
async def receive_preferences(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user_message = update.message.text
|
||
user = update.effective_user
|
||
counterparty = context.user_data.get("counterparty", "unknown")
|
||
|
||
await update.message.chat.send_action("typing")
|
||
|
||
preferences = await personal_agent.extract_preferences(user_message, user.id)
|
||
|
||
if "error" in preferences:
|
||
await update.message.reply_text(
|
||
"⚠️ I had trouble understanding that. Could you rephrase?\n"
|
||
"Try to include specific details like dates, amounts, or preferences."
|
||
)
|
||
return AWAITING_PREFERENCES
|
||
|
||
feature_type = preferences.get("feature_type", "generic")
|
||
|
||
# Get user's personality
|
||
user_data = await db.get_user(user.id)
|
||
personality = dict(user_data).get("personality", "balanced") if user_data else "balanced"
|
||
|
||
negotiation_id = await db.create_negotiation(feature_type, user.id)
|
||
await db.add_participant(negotiation_id, user.id, preferences, personality_used=personality)
|
||
|
||
pending_coordinations[counterparty] = {
|
||
"negotiation_id": negotiation_id,
|
||
"initiator_id": user.id,
|
||
"initiator_name": user.first_name,
|
||
"feature_type": feature_type,
|
||
"preferences_a": preferences,
|
||
"personality_a": personality
|
||
}
|
||
|
||
feature_labels = {
|
||
"scheduling": "📅 Meeting Scheduling", "expenses": "💰 Expense Splitting",
|
||
"freelance": "💼 Project Negotiation", "roommate": "🏠 Roommate Decision",
|
||
"trip": "✈️ Trip Planning", "marketplace": "🛒 Buy/Sell Deal",
|
||
"collaborative": "🍕 Joint Decision", "conflict": "⚖️ Conflict Resolution",
|
||
"generic": "🤝 Coordination"
|
||
}
|
||
personality_emoji = {"aggressive": "😤", "people_pleaser": "🤝", "analytical": "📊", "empathetic": "💚", "balanced": "⚖️"}
|
||
|
||
await update.message.reply_text(
|
||
f"✅ *Preferences captured!*\n\n"
|
||
f"📋 Type: {feature_labels.get(feature_type, '🤝 Coordination')}\n"
|
||
f"🤝 With: @{counterparty}\n"
|
||
f"🎭 Agent style: {personality_emoji.get(personality, '⚖️')} {personality.replace('_', ' ').title()}\n"
|
||
f"🆔 Negotiation: `{negotiation_id}`\n\n"
|
||
f"I've notified @{counterparty}'s agent.\n"
|
||
f"Once they share their side, our agents will negotiate automatically.\n\n"
|
||
f"_I'll send you real-time updates + a voice summary at the end!_ 🤖↔️🤖🔊",
|
||
parse_mode="Markdown"
|
||
)
|
||
return ConversationHandler.END
|
||
|
||
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await update.message.reply_text(
|
||
"🤖 *negoT8 — How it works*\n\n"
|
||
"1️⃣ Set style: /personality\n"
|
||
"2️⃣ Start: `/coordinate @person`\n"
|
||
"3️⃣ Describe what you need\n"
|
||
"4️⃣ Their agent asks them for their side\n"
|
||
"5️⃣ 🤖↔️🤖 Agents negotiate (you watch live)\n"
|
||
"6️⃣ ✅ Both get: text + voice note + payment link\n\n"
|
||
"*Works for:* Scheduling, expenses, trip planning, buying/selling, "
|
||
"restaurant picks, roommate decisions, conflict resolution, and more!",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
def create_bot(token: str) -> Application:
|
||
"""Create a bot application with all handlers."""
|
||
app = Application.builder().token(token).build()
|
||
|
||
conv_handler = ConversationHandler(
|
||
entry_points=[CommandHandler("coordinate", coordinate_command)],
|
||
states={
|
||
AWAITING_PREFERENCES: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_preferences)]
|
||
},
|
||
fallbacks=[CommandHandler("cancel", lambda u, c: ConversationHandler.END)]
|
||
)
|
||
|
||
app.add_handler(CommandHandler("start", start_command))
|
||
app.add_handler(CommandHandler("personality", personality_command))
|
||
app.add_handler(CallbackQueryHandler(personality_callback, pattern="^personality_"))
|
||
app.add_handler(conv_handler)
|
||
app.add_handler(CommandHandler("help", help_command))
|
||
|
||
return app
|
||
```
|
||
|
||
### ✅ MILESTONE 2 SUCCESS TEST
|
||
```
|
||
□ PersonalAgent correctly classifies all 4 test scenarios
|
||
□ PersonalAgent extracts UPI ID from expense messages
|
||
□ All 5 personality profiles load correctly (test output shows ✅ for each)
|
||
□ Telegram Bot A responds to /start, /personality (shows 5 buttons), /coordinate
|
||
□ Personality selection via inline keyboard works and updates DB
|
||
□ Preferences stored in SQLite (check: sqlite3 negot8.db "SELECT * FROM participants;")
|
||
□ pending_coordinations dict has the stored coordination data with personality
|
||
```
|
||
|
||
**🚫 If PersonalAgent misclassifies frequently:** Add 2-3 few-shot examples to the system prompt.
|
||
|
||
---
|
||
|
||
## MILESTONE 3: Negotiator Agent + Negotiation Engine + Tavily Tools
|
||
**⏰ Time: Hour 3 → Hour 6 (180 min)**
|
||
**👤 Who: DEV A builds negotiation engine with personality, DEV B builds counterparty flow + tools**
|
||
|
||
### ⚠️ THIS IS THE CRITICAL MILESTONE. The entire project depends on this working.
|
||
|
||
### DEV A: Build the Negotiation Engine (with personality injection)
|
||
|
||
→ **Reference: Build Guide v2 Section 6 (Negotiator Prompt) + Section 7 (Protocol)**
|
||
|
||
**Step 1: Create Pydantic message models**
|
||
```python
|
||
# backend/protocol/messages.py
|
||
from pydantic import BaseModel
|
||
from typing import Optional, List
|
||
from enum import Enum
|
||
|
||
class MessageType(str, Enum):
|
||
INITIATE = "initiate"
|
||
PROPOSAL = "proposal"
|
||
ACCEPT = "accept"
|
||
COUNTER = "counter"
|
||
ESCALATE = "escalate"
|
||
|
||
class AgentMessage(BaseModel):
|
||
message_id: str
|
||
negotiation_id: str
|
||
message_type: MessageType
|
||
sender_id: int
|
||
receiver_id: int
|
||
round_number: int
|
||
payload: dict
|
||
reasoning: str = ""
|
||
satisfaction_score: float = 0.0
|
||
concessions_made: List[str] = []
|
||
concessions_requested: List[str] = []
|
||
timestamp: str = ""
|
||
```
|
||
|
||
**Step 2: Create NegotiatorAgent with personality injection**
|
||
```python
|
||
# backend/agents/negotiator_agent.py
|
||
from agents.base_agent import BaseAgent
|
||
from personality.profiles import get_personality_modifier
|
||
import json
|
||
|
||
NEGOTIATOR_BASE_PROMPT = """You are the Negotiator Agent for negoT8. You negotiate on behalf of your human to find optimal agreements with other people's agents.
|
||
|
||
{personality_modifier}
|
||
|
||
NEGOTIATION RULES:
|
||
1. You are LOYAL to your human. Their constraints (marked "hard": true) are NEVER violated.
|
||
2. You seek WIN-WIN solutions. Both parties should feel satisfied.
|
||
3. You concede on low-priority preferences first, high-priority last.
|
||
4. You MUST resolve within 5 rounds. Be efficient.
|
||
5. Accept if satisfaction >= 70%. Counter if 40-69%. Escalate if < 40% after round 3.
|
||
|
||
You MUST respond with this exact JSON:
|
||
{
|
||
"action": "propose|counter|accept|escalate",
|
||
"proposal": {
|
||
"summary": "one-line description of proposal",
|
||
"details": { ... feature-specific details ... },
|
||
"for_party_a": "what party A gets",
|
||
"for_party_b": "what party B gets"
|
||
},
|
||
"satisfaction_score": 0-100,
|
||
"reasoning": "Why this action and proposal",
|
||
"concessions_made": ["what you gave up this round"],
|
||
"concessions_requested": ["what you want from them"]
|
||
}
|
||
|
||
STRATEGY BY ROUND:
|
||
- Round 1: Propose your human's ideal outcome (aim high but reasonable)
|
||
- Round 2-3: Make strategic concessions on low-priority items
|
||
- Round 4: Make final significant concession if needed
|
||
- Round 5: Accept best available OR escalate with 2-3 options for humans
|
||
|
||
IMPORTANT: Your proposal must ALWAYS include concrete specifics (numbers, dates, items).
|
||
Never propose vague things like "we'll figure it out later"."""
|
||
|
||
class NegotiatorAgent(BaseAgent):
|
||
def __init__(self, personality: str = "balanced"):
|
||
modifier = get_personality_modifier(personality)
|
||
prompt = NEGOTIATOR_BASE_PROMPT.replace("{personality_modifier}", modifier)
|
||
super().__init__(system_prompt=prompt)
|
||
|
||
async def generate_initial_proposal(self, my_preferences: dict, feature_type: str) -> dict:
|
||
return await self.call(
|
||
user_prompt=f"""Generate the FIRST proposal for this {feature_type} negotiation.
|
||
|
||
My human's preferences:
|
||
{json.dumps(my_preferences, indent=2)}
|
||
|
||
This is Round 1. Propose my human's ideal outcome — aim high but stay reasonable.
|
||
The other party hasn't proposed anything yet."""
|
||
)
|
||
|
||
async def evaluate_and_respond(self, received_proposal: dict, my_preferences: dict, feature_type: str, round_number: int) -> dict:
|
||
return await self.call(
|
||
user_prompt=f"""Evaluate this proposal and respond. Round {round_number} of a {feature_type} negotiation.
|
||
|
||
RECEIVED PROPOSAL FROM OTHER AGENT:
|
||
{json.dumps(received_proposal, indent=2)}
|
||
|
||
MY HUMAN'S PREFERENCES:
|
||
{json.dumps(my_preferences, indent=2)}
|
||
|
||
Evaluate against my human's preferences. Decide: accept (satisfaction >= 70), counter (40-69), or escalate (< 40 and round >= 3).
|
||
If countering, make a strategic concession while protecting high-priority items."""
|
||
)
|
||
```
|
||
|
||
**Step 3: Create Negotiation Engine with analytics tracking**
|
||
```python
|
||
# backend/protocol/negotiation.py
|
||
import asyncio
|
||
import json
|
||
from agents.negotiator_agent import NegotiatorAgent
|
||
import database as db
|
||
|
||
async def run_negotiation(negotiation_id: str, preferences_a: dict, preferences_b: dict,
|
||
user_a_id: int, user_b_id: int, feature_type: str,
|
||
personality_a: str = "balanced", personality_b: str = "balanced",
|
||
on_round_update=None, on_resolution=None):
|
||
"""
|
||
Main negotiation loop with personality-aware agents and analytics tracking.
|
||
"""
|
||
await db.update_negotiation_status(negotiation_id, "active")
|
||
|
||
# Create personality-aware negotiators
|
||
negotiator_a = NegotiatorAgent(personality=personality_a)
|
||
negotiator_b = NegotiatorAgent(personality=personality_b)
|
||
|
||
current_proposal = None
|
||
max_rounds = 5
|
||
satisfaction_timeline = []
|
||
|
||
for round_num in range(1, max_rounds + 1):
|
||
await asyncio.sleep(1.5) # Rate limit protection for Gemini
|
||
|
||
if round_num == 1:
|
||
response = await negotiator_a.generate_initial_proposal(
|
||
my_preferences=preferences_a, feature_type=feature_type
|
||
)
|
||
proposer_id = user_a_id
|
||
elif round_num % 2 == 0:
|
||
response = await negotiator_b.evaluate_and_respond(
|
||
received_proposal=current_proposal, my_preferences=preferences_b,
|
||
feature_type=feature_type, round_number=round_num
|
||
)
|
||
proposer_id = user_b_id
|
||
else:
|
||
response = await negotiator_a.evaluate_and_respond(
|
||
received_proposal=current_proposal, my_preferences=preferences_a,
|
||
feature_type=feature_type, round_number=round_num
|
||
)
|
||
proposer_id = user_a_id
|
||
|
||
# Handle errors
|
||
if "error" in response:
|
||
response = {
|
||
"action": "counter" if round_num < max_rounds else "escalate",
|
||
"proposal": current_proposal or {"summary": "Let's discuss further", "details": {}},
|
||
"satisfaction_score": 50, "reasoning": "Agent encountered an issue",
|
||
"concessions_made": [], "concessions_requested": []
|
||
}
|
||
|
||
action = response.get("action", "counter")
|
||
current_proposal = response.get("proposal", {})
|
||
satisfaction = response.get("satisfaction_score", 50)
|
||
concessions = response.get("concessions_made", [])
|
||
|
||
# Track satisfaction for analytics
|
||
# The proposer's score is the one returned; estimate the other party's
|
||
if proposer_id == user_a_id:
|
||
sat_a, sat_b = satisfaction, max(30, 100 - satisfaction * 0.4)
|
||
else:
|
||
sat_b, sat_a = satisfaction, max(30, 100 - satisfaction * 0.4)
|
||
|
||
satisfaction_timeline.append({
|
||
"round": round_num, "score_a": sat_a, "score_b": sat_b
|
||
})
|
||
|
||
# Save round with analytics data
|
||
await db.save_round(
|
||
negotiation_id=negotiation_id, round_number=round_num,
|
||
proposer_id=proposer_id, proposal=response,
|
||
response_type=action, reasoning=response.get("reasoning", ""),
|
||
satisfaction_a=sat_a, satisfaction_b=sat_b,
|
||
concessions_made=concessions
|
||
)
|
||
|
||
# Notify via callback
|
||
round_data = {
|
||
"negotiation_id": negotiation_id, "round_number": round_num,
|
||
"action": action, "proposal": current_proposal,
|
||
"satisfaction_score": satisfaction, "reasoning": response.get("reasoning", ""),
|
||
"proposer_id": proposer_id,
|
||
"satisfaction_a": sat_a, "satisfaction_b": sat_b
|
||
}
|
||
if on_round_update:
|
||
await on_round_update(round_data)
|
||
|
||
# Check outcome
|
||
if action == "accept":
|
||
resolution = {
|
||
"status": "resolved", "final_proposal": current_proposal,
|
||
"rounds_taken": round_num, "summary": current_proposal.get("summary", "Agreement reached"),
|
||
"satisfaction_timeline": satisfaction_timeline
|
||
}
|
||
await db.update_negotiation_status(negotiation_id, "resolved", resolution)
|
||
if on_resolution:
|
||
await on_resolution(resolution)
|
||
return resolution
|
||
|
||
if action == "escalate":
|
||
resolution = {
|
||
"status": "escalated", "final_proposal": current_proposal,
|
||
"rounds_taken": round_num, "summary": "Agents couldn't fully agree. Options for human decision.",
|
||
"satisfaction_timeline": satisfaction_timeline
|
||
}
|
||
await db.update_negotiation_status(negotiation_id, "escalated", resolution)
|
||
if on_resolution:
|
||
await on_resolution(resolution)
|
||
return resolution
|
||
|
||
# Exhausted rounds
|
||
resolution = {
|
||
"status": "escalated", "final_proposal": current_proposal,
|
||
"rounds_taken": max_rounds, "summary": "Max rounds reached. Best proposal for human decision.",
|
||
"satisfaction_timeline": satisfaction_timeline
|
||
}
|
||
await db.update_negotiation_status(negotiation_id, "escalated", resolution)
|
||
if on_resolution:
|
||
await on_resolution(resolution)
|
||
return resolution
|
||
```
|
||
|
||
**Step 4: TEST THE NEGOTIATION ENGINE (with different personalities)**
|
||
```python
|
||
# test_negotiation.py
|
||
import asyncio
|
||
import json
|
||
|
||
async def on_round(data):
|
||
print(f"\n🔄 Round {data['round_number']}: {data['action']}")
|
||
print(f" Satisfaction A: {data.get('satisfaction_a', '?')}% B: {data.get('satisfaction_b', '?')}%")
|
||
print(f" Reasoning: {data['reasoning'][:150]}")
|
||
|
||
async def on_resolve(data):
|
||
print(f"\n{'='*50}")
|
||
print(f"🏁 RESULT: {data['status']} in {data['rounds_taken']} rounds")
|
||
print(f" Summary: {data['summary']}")
|
||
print(f" Timeline: {json.dumps(data.get('satisfaction_timeline', []))}")
|
||
|
||
async def test():
|
||
from protocol.negotiation import run_negotiation
|
||
import database as db
|
||
|
||
await db.init_db()
|
||
|
||
# Test with DIFFERENT personalities: aggressive vs empathetic
|
||
prefs_a = {
|
||
"feature_type": "expenses",
|
||
"goal": "Split Goa trip expenses",
|
||
"constraints": [{"type": "budget", "value": None, "description": "Fair split", "hard": True}],
|
||
"preferences": [
|
||
{"type": "split", "value": "60-40 fuel", "priority": "high", "description": "I drove the whole way"},
|
||
{"type": "payment", "value": "UPI", "priority": "medium", "description": "UPI preferred"}
|
||
],
|
||
"raw_details": {"hotel": 12000, "fuel": 3000, "dinner": 2000, "upi_id": "rahul@paytm"}
|
||
}
|
||
prefs_b = {
|
||
"feature_type": "expenses",
|
||
"goal": "Split Goa trip expenses fairly",
|
||
"constraints": [{"type": "fairness", "value": "equal contribution acknowledged", "hard": False}],
|
||
"preferences": [
|
||
{"type": "split", "value": "50-50 fuel", "priority": "high", "description": "I navigated and planned"},
|
||
{"type": "payment", "value": "UPI", "priority": "medium", "description": "UPI fine"}
|
||
],
|
||
"raw_details": {"hotel": 12000, "fuel": 3000, "dinner": 2000}
|
||
}
|
||
|
||
neg_id = await db.create_negotiation("expenses", 111)
|
||
await db.add_participant(neg_id, 111, prefs_a, personality_used="aggressive")
|
||
await db.add_participant(neg_id, 222, prefs_b, personality_used="empathetic")
|
||
|
||
print("🧪 Testing: AGGRESSIVE (A) vs EMPATHETIC (B) on expense splitting\n")
|
||
result = await run_negotiation(
|
||
negotiation_id=neg_id, preferences_a=prefs_a, preferences_b=prefs_b,
|
||
user_a_id=111, user_b_id=222, feature_type="expenses",
|
||
personality_a="aggressive", personality_b="empathetic",
|
||
on_round_update=on_round, on_resolution=on_resolve
|
||
)
|
||
|
||
asyncio.run(test())
|
||
```
|
||
```bash
|
||
python test_negotiation.py
|
||
```
|
||
|
||
### DEV B: Build Tavily Search Tool + UPI Generator + Counterparty Flow
|
||
|
||
→ **Reference: Build Guide v2 Section 12 (Tool Calling System)**
|
||
|
||
**Step 1: Create Tavily search tool**
|
||
```python
|
||
# backend/tools/tavily_search.py
|
||
from tavily import TavilyClient
|
||
from config import TAVILY_API_KEY
|
||
|
||
class TavilySearchTool:
|
||
name = "tavily_search"
|
||
|
||
def __init__(self):
|
||
self.client = TavilyClient(api_key=TAVILY_API_KEY)
|
||
|
||
async def execute(self, query: str, search_depth: str = "basic") -> dict:
|
||
try:
|
||
response = self.client.search(
|
||
query=query, search_depth=search_depth,
|
||
include_answer=True, max_results=5
|
||
)
|
||
results = [{"title": r.get("title", ""), "content": r.get("content", ""), "url": r.get("url", "")}
|
||
for r in response.get("results", [])]
|
||
return {
|
||
"query": query,
|
||
"answer": response.get("answer", ""),
|
||
"results": results,
|
||
"summary": response.get("answer", results[0]["content"][:200] if results else "No results")
|
||
}
|
||
except Exception as e:
|
||
return {"query": query, "answer": "", "results": [], "summary": f"Search failed: {str(e)}"}
|
||
```
|
||
|
||
**Step 2: Create UPI deep link generator**
|
||
```python
|
||
# backend/tools/upi_generator.py
|
||
from urllib.parse import quote
|
||
|
||
class UPIGeneratorTool:
|
||
name = "generate_upi_link"
|
||
|
||
async def execute(self, payee_upi: str, payee_name: str, amount: float, note: str = "") -> dict:
|
||
upi_link = f"upi://pay?pa={quote(payee_upi)}&pn={quote(payee_name)}&am={amount:.2f}&cu=INR"
|
||
if note:
|
||
upi_link += f"&tn={quote(note)}"
|
||
return {
|
||
"upi_link": upi_link,
|
||
"display_text": f"Pay ₹{amount:,.0f} to {payee_name}",
|
||
"payee_upi": payee_upi, "amount": amount
|
||
}
|
||
```
|
||
|
||
**Step 3: Create calculator tool**
|
||
```python
|
||
# backend/tools/calculator.py
|
||
import ast, operator
|
||
from decimal import Decimal, ROUND_HALF_UP
|
||
|
||
class CalculatorTool:
|
||
name = "calculate"
|
||
|
||
async def execute(self, expression: str) -> dict:
|
||
allowed_ops = {
|
||
ast.Add: operator.add, ast.Sub: operator.sub,
|
||
ast.Mult: operator.mul, ast.Div: operator.truediv,
|
||
}
|
||
def _eval(node):
|
||
if isinstance(node, ast.Num): return Decimal(str(node.n))
|
||
elif isinstance(node, ast.BinOp):
|
||
return allowed_ops[type(node.op)](_eval(node.left), _eval(node.right))
|
||
raise ValueError(f"Unsupported: {ast.dump(node)}")
|
||
tree = ast.parse(expression, mode='eval')
|
||
result = float(_eval(tree.body).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
|
||
return {"expression": expression, "result": result}
|
||
```
|
||
|
||
**Step 4: Test tools**
|
||
```python
|
||
# test_tools.py
|
||
import asyncio
|
||
from tools.tavily_search import TavilySearchTool
|
||
from tools.upi_generator import UPIGeneratorTool
|
||
from tools.calculator import CalculatorTool
|
||
|
||
async def test():
|
||
# Tavily
|
||
tavily = TavilySearchTool()
|
||
r = await tavily.execute("best Thai restaurants Bandra Mumbai")
|
||
print(f"Tavily: {r['answer'][:100]}... ({len(r['results'])} results) ✅")
|
||
|
||
# UPI
|
||
upi = UPIGeneratorTool()
|
||
r = await upi.execute("rahul@paytm", "Rahul", 8200, "Goa trip settlement")
|
||
print(f"UPI: {r['upi_link'][:60]}... ✅")
|
||
|
||
# Calculator
|
||
calc = CalculatorTool()
|
||
r = await calc.execute("12000 * 0.55")
|
||
print(f"Calc: 12000 * 0.55 = {r['result']} ✅")
|
||
|
||
asyncio.run(test())
|
||
```
|
||
|
||
**Step 5: Build /pending counterparty flow** (same as v1 — see existing milestone doc for code, but add personality tracking)
|
||
|
||
### ✅ MILESTONE 3 SUCCESS TEST
|
||
```
|
||
□ test_negotiation.py shows:
|
||
- Multiple rounds with satisfaction scores tracked per round
|
||
- Aggressive agent A concedes slower than empathetic agent B
|
||
- Final "accept" or "escalate" action
|
||
- Satisfaction timeline in resolution output
|
||
□ test_tools.py: Tavily returns real restaurant results, UPI generates valid link, Calculator works
|
||
□ Negotiation completes within 45 seconds
|
||
□ Database has rounds with satisfaction_a/satisfaction_b populated
|
||
□ Bot B's /pending command shows coordination with personality info
|
||
```
|
||
|
||
**🚫 THIS IS THE GO/NO-GO CHECKPOINT.** If negotiation engine works, everything else is features + polish.
|
||
|
||
---
|
||
|
||
## MILESTONE 4: End-to-End Telegram Flow + Real-Time Updates + Voice
|
||
**⏰ Time: Hour 6 → Hour 8 (120 min)**
|
||
**👤 Who: DEV A connects everything + voice, DEV B handles formatting + UPI**
|
||
|
||
### Goal: Full flow with voice summaries and UPI links working
|
||
|
||
**Step 1: Create ElevenLabs TTS module**
|
||
→ **Reference: Build Guide v2 Section 13 (Voice Summaries)**
|
||
|
||
```python
|
||
# backend/voice/elevenlabs_tts.py
|
||
import httpx
|
||
from config import ELEVENLABS_API_KEY, VOICE_ID_AGENT_A, VOICE_ID_AGENT_B
|
||
|
||
async def generate_voice_summary(text: str, negotiation_id: str,
|
||
voice_id: str = None) -> str:
|
||
"""Generate TTS MP3 and return file path. Returns None on failure."""
|
||
voice_id = voice_id or VOICE_ID_AGENT_A
|
||
|
||
try:
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.post(
|
||
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
|
||
headers={"xi-api-key": ELEVENLABS_API_KEY, "Content-Type": "application/json"},
|
||
json={
|
||
"text": text[:500], # Budget cap
|
||
"model_id": "eleven_flash_v2_5",
|
||
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
|
||
},
|
||
timeout=30.0
|
||
)
|
||
if response.status_code == 200:
|
||
filepath = f"/tmp/negot8_voice_{negotiation_id}.mp3"
|
||
with open(filepath, "wb") as f:
|
||
f.write(response.content)
|
||
return filepath
|
||
except Exception as e:
|
||
print(f"Voice TTS failed: {e}")
|
||
return None
|
||
|
||
# Voice summary templates
|
||
VOICE_TEMPLATES = {
|
||
"expenses": "Expenses settled! After {rounds} rounds, {payer} owes {payee} {amount} rupees. A UPI payment link has been sent.",
|
||
"collaborative": "Decision made! You're going to {choice}. Your agents found the perfect match in {rounds} rounds.",
|
||
"scheduling": "Meeting scheduled for {date} at {time}, {location}. Agreed in {rounds} rounds.",
|
||
"marketplace": "Deal done! {item} for {price} rupees. Payment link is ready.",
|
||
"trip": "Trip planned! {destination} on {dates}, {budget} per person.",
|
||
"freelance": "Project agreed! {scope} for {budget} rupees. First milestone payment ready via UPI.",
|
||
"roommate": "Decision made! {option}. Cost split arranged.",
|
||
"conflict": "Resolution reached! {summary}.",
|
||
}
|
||
|
||
def build_voice_text(feature_type: str, resolution: dict) -> str:
|
||
template = VOICE_TEMPLATES.get(feature_type, "Negotiation resolved! Check Telegram for details.")
|
||
try:
|
||
return template.format(**resolution)[:500]
|
||
except KeyError:
|
||
summary = resolution.get("summary", "resolved")
|
||
return f"Your {feature_type} negotiation is complete: {summary}"[:500]
|
||
```
|
||
|
||
**Step 2: Create unified bot runner with voice + UPI + analytics**
|
||
```python
|
||
# backend/run.py — THE MAIN ENTRY POINT
|
||
import asyncio
|
||
import json
|
||
from telegram.ext import Application, CommandHandler, ConversationHandler, MessageHandler, CallbackQueryHandler, filters
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||
from agents.personal_agent import PersonalAgent
|
||
from protocol.negotiation import run_negotiation
|
||
from voice.elevenlabs_tts import generate_voice_summary, build_voice_text
|
||
from tools.upi_generator import UPIGeneratorTool
|
||
import database as db
|
||
from config import *
|
||
|
||
personal_agent = PersonalAgent()
|
||
upi_tool = UPIGeneratorTool()
|
||
pending_coordinations = {}
|
||
bot_apps = {}
|
||
|
||
async def send_to_user(bot, user_id, text, reply_markup=None):
|
||
"""Send a message to any user."""
|
||
try:
|
||
await bot.send_message(chat_id=user_id, text=text, parse_mode="Markdown", reply_markup=reply_markup)
|
||
except Exception as e:
|
||
print(f"Failed to send to {user_id}: {e}")
|
||
|
||
async def send_voice_to_user(bot, user_id, audio_path):
|
||
"""Send voice note to user."""
|
||
try:
|
||
with open(audio_path, "rb") as f:
|
||
await bot.send_voice(chat_id=user_id, voice=f, caption="🎙 Voice summary from your agent")
|
||
except Exception as e:
|
||
print(f"Failed to send voice to {user_id}: {e}")
|
||
|
||
# ─── Resolution handler with voice + UPI ───
|
||
async def handle_resolution(negotiation_id, resolution, feature_type,
|
||
user_a_id, user_b_id, bot_a, bot_b,
|
||
preferences_a, preferences_b):
|
||
"""Post-resolution: generate UPI link, voice summary, analytics, send to users."""
|
||
|
||
status = resolution["status"]
|
||
proposal = resolution.get("final_proposal", {})
|
||
emoji = "✅" if status == "resolved" else "⚠️"
|
||
|
||
summary_text = (
|
||
f"{emoji} *Negotiation {'Complete' if status == 'resolved' else 'Needs Input'}!*\n\n"
|
||
f"📊 Resolved in {resolution['rounds_taken']} rounds\n\n"
|
||
f"📋 *Agreement:*\n{proposal.get('summary', 'See details')}\n\n"
|
||
f"*For A:* {proposal.get('for_party_a', 'See details')}\n"
|
||
f"*For B:* {proposal.get('for_party_b', 'See details')}"
|
||
)
|
||
|
||
# ─── UPI link (for expense-related features) ───
|
||
upi_markup = None
|
||
if feature_type in ("expenses", "freelance", "marketplace", "roommate"):
|
||
# Try to extract settlement from proposal details
|
||
details = proposal.get("details", {})
|
||
settlement = details.get("settlement", {})
|
||
upi_id = (preferences_a.get("raw_details", {}).get("upi_id") or
|
||
preferences_b.get("raw_details", {}).get("upi_id"))
|
||
|
||
if upi_id and settlement.get("amount"):
|
||
upi_result = await upi_tool.execute(
|
||
payee_upi=upi_id,
|
||
payee_name=settlement.get("payee_name", "User"),
|
||
amount=float(settlement["amount"]),
|
||
note=f"negoT8: {feature_type} settlement"
|
||
)
|
||
upi_link = upi_result["upi_link"]
|
||
summary_text += f"\n\n💳 *Tap to pay:* ₹{settlement['amount']:,.0f}"
|
||
|
||
upi_markup = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton(
|
||
f"💳 Pay ₹{settlement['amount']:,.0f}",
|
||
url=upi_link
|
||
)]
|
||
])
|
||
|
||
# ─── Send text summary to both ───
|
||
await send_to_user(bot_a, user_a_id, summary_text, reply_markup=upi_markup)
|
||
await send_to_user(bot_b, user_b_id, summary_text, reply_markup=upi_markup)
|
||
|
||
# ─── Voice summary ───
|
||
voice_text = build_voice_text(feature_type, {
|
||
"rounds": resolution["rounds_taken"],
|
||
"summary": proposal.get("summary", "resolved"),
|
||
**proposal.get("details", {}),
|
||
**{k: v for k, v in proposal.items() if k != "details"}
|
||
})
|
||
|
||
voice_path = await generate_voice_summary(voice_text, negotiation_id, VOICE_ID_AGENT_A)
|
||
if voice_path:
|
||
await send_voice_to_user(bot_a, user_a_id, voice_path)
|
||
# Generate with different voice for User B
|
||
voice_path_b = await generate_voice_summary(voice_text, f"{negotiation_id}_b", VOICE_ID_AGENT_B)
|
||
if voice_path_b:
|
||
await send_voice_to_user(bot_b, user_b_id, voice_path_b)
|
||
|
||
# ─── Compute & store analytics ───
|
||
timeline = resolution.get("satisfaction_timeline", [])
|
||
concession_log = []
|
||
rounds = await db.get_rounds(negotiation_id)
|
||
for r in rounds:
|
||
concessions = json.loads(r["concessions_made"]) if r["concessions_made"] else []
|
||
for c in concessions:
|
||
concession_log.append({"round": r["round_number"], "by": "A" if r["proposer_id"] == user_a_id else "B", "gave_up": c})
|
||
|
||
final_sat_a = timeline[-1]["score_a"] if timeline else 50
|
||
final_sat_b = timeline[-1]["score_b"] if timeline else 50
|
||
fairness = 100 - abs(final_sat_a - final_sat_b)
|
||
|
||
await db.store_analytics({
|
||
"negotiation_id": negotiation_id,
|
||
"satisfaction_timeline": json.dumps(timeline),
|
||
"concession_log": json.dumps(concession_log),
|
||
"fairness_score": fairness,
|
||
"total_concessions_a": sum(1 for c in concession_log if c["by"] == "A"),
|
||
"total_concessions_b": sum(1 for c in concession_log if c["by"] == "B"),
|
||
})
|
||
```
|
||
|
||
**Step 3: Wire up the full flow and test end-to-end**
|
||
|
||
Complete the `run.py` with both bot runners and the counterparty `/pending` flow (reuse from existing milestone, adding personality tracking). The critical addition is passing personality to `run_negotiation()` and calling `handle_resolution()` when done.
|
||
|
||
### ✅ MILESTONE 4 SUCCESS TEST
|
||
```
|
||
□ FULL FLOW TEST (two phones/Telegram accounts):
|
||
1. Phone A: /start → /personality → pick "aggressive" → /coordinate @userB → send expense details with UPI ID
|
||
2. Phone B: /start → /personality → pick "empathetic" → /pending → send their side
|
||
3. Both phones receive round-by-round updates with satisfaction scores
|
||
4. Both phones receive final resolution text
|
||
5. Both phones receive voice note (different voices for A and B!)
|
||
6. Phone B sees UPI payment button → tapping opens UPI app (test on real phone)
|
||
□ No crashes during full flow
|
||
□ Voice MP3 generates in < 5 seconds
|
||
□ Database has analytics row for the negotiation
|
||
□ Negotiation completes in under 60 seconds total
|
||
```
|
||
|
||
**🎉 If this works, you have a fully featured product. Everything after this is more features + the dashboard.**
|
||
|
||
---
|
||
|
||
## MILESTONE 5: Feature Implementation Sprint
|
||
**⏰ Time: Hour 8 → Hour 14 (360 min)**
|
||
**👤 Who: Both devs, feature-by-feature**
|
||
|
||
### Strategy: Negotiation engine is generic + personality-aware. Each feature needs:
|
||
1. Tailored prompt additions for the Negotiator (domain-specific strategies)
|
||
2. Tool calls (Tavily search for real data, calculator for math, UPI for payments)
|
||
3. Nice Telegram output formatting + appropriate voice summary template
|
||
|
||
### Batch 1 (Hour 8-10): Scheduling + Collaborative Decisions
|
||
→ **Reference: Build Guide v2 Section 8, Features 1 and 7**
|
||
|
||
Focus on:
|
||
- Add **Tavily search** for restaurant/place/cafe discovery in collaborative decisions
|
||
- Tavily `include_answer=True` gives free AI-summarized results — no extra LLM call
|
||
- Polish output format with venue suggestions, time formatting
|
||
- Test voice summary template for both features
|
||
|
||
**Test:** Restaurant decision with Tavily returning real results. Scheduling with time overlap.
|
||
|
||
### Batch 2 (Hour 10-12): Expenses + Marketplace
|
||
→ **Reference: Build Guide v2 Section 8, Features 2 and 6**
|
||
|
||
Key implementation:
|
||
- Calculator tool for ALL expense math (never let LLM do arithmetic)
|
||
- **UPI link auto-generation** in expense resolution (already wired in Milestone 4)
|
||
- Tavily search for market price comparison in marketplace
|
||
- UPI link for buyer after marketplace deal
|
||
- Clean tabular output in Telegram for expense breakdowns
|
||
|
||
**Test:** Expense splitting with unequal split debate → verify math is correct → UPI link works. Buy/sell with haggling → market price cited from Tavily.
|
||
|
||
### Batch 3 (Hour 12-13): Freelance + Roommate
|
||
→ **Reference: Build Guide v2 Section 8, Features 3 and 4**
|
||
|
||
Key implementation:
|
||
- Complex proposal schema for freelance (scope + timeline + budget)
|
||
- **UPI link for first milestone payment** in freelance
|
||
- Tavily search for **real WiFi plans / product prices** in roommate decisions
|
||
- This is where Tavily really shines — agents cite actual Airtel/Jio plans
|
||
|
||
**Test:** Freelance where budget < desired rate (forces scope reduction). WiFi plan decision with real provider data.
|
||
|
||
### Batch 4 (Hour 13-14): Trip Planning (Multi-Agent) + Conflict Resolution
|
||
→ **Reference: Build Guide v2 Section 8, Features 5 and 8**
|
||
|
||
Key implementation:
|
||
- **Trip planning needs 3-person support** — modify run_negotiation for 3+ preference sets
|
||
- Tavily for real destination info, accommodation prices, activity suggestions
|
||
- Voice summaries for group trips use **3 different voice IDs** (wow factor)
|
||
- Conflict resolution is pure negotiation — no special tools needed
|
||
|
||
**Simplified multi-agent approach for 3 people:**
|
||
```python
|
||
async def run_group_negotiation(negotiation_id, all_preferences, all_user_ids, feature_type, personalities, ...):
|
||
"""Mediator approach: find intersection, propose, iterate."""
|
||
mediator_prompt = f"""You are mediating a {feature_type} negotiation with {len(all_preferences)} participants.
|
||
All participants' preferences:
|
||
{json.dumps(all_preferences, indent=2)}
|
||
Find a solution that maximizes group satisfaction.
|
||
Budget ceiling = lowest budget. Dates = intersection."""
|
||
|
||
proposal = await NegotiatorAgent("balanced").call(mediator_prompt)
|
||
# Score with each, iterate for max 5 rounds
|
||
```
|
||
|
||
**Test:** 3-person trip planning. Conflict resolution scenario.
|
||
|
||
### ✅ MILESTONE 5 SUCCESS TEST
|
||
```
|
||
□ All 8 feature types produce sensible negotiations via Telegram
|
||
□ Expense splitting math is CORRECT + UPI payment link generated
|
||
□ Restaurant suggestions include REAL place names from Tavily (not hallucinated)
|
||
□ Freelance negotiation reduces scope when budget too low + UPI for milestone
|
||
□ WiFi/roommate decisions cite real provider plans from Tavily
|
||
□ Marketplace shows market price reference from Tavily search
|
||
□ Trip planning handles 3 people (can use 3 bots or simulate)
|
||
□ Conflict resolution produces creative compromises
|
||
□ Voice summaries work for at least expenses + collaborative + marketplace
|
||
□ Generic negotiation works for off-menu scenarios
|
||
```
|
||
|
||
**Timing check:** You should be at Hour 14. If behind, skip trip planning multi-agent (hardest) and demo as 2-person trip. Skip conflict resolution if needed — demo with 6 features + add-ons is still impressive.
|
||
|
||
---
|
||
|
||
## MILESTONE 6: Dashboard + Analytics Visualization
|
||
**⏰ Time: Hour 14 → Hour 18 (240 min)**
|
||
**👤 Who: DEV B builds dashboard + analytics charts, DEV A builds API + Socket.IO**
|
||
|
||
### DEV A: FastAPI Endpoints + Socket.IO + Analytics API
|
||
|
||
→ **Reference: Build Guide v2 Section 11 (API Layer)**
|
||
|
||
```python
|
||
# backend/api.py
|
||
from fastapi import FastAPI
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
import socketio
|
||
import database as db
|
||
|
||
sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')
|
||
app = FastAPI()
|
||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
||
|
||
@app.get("/api/negotiations")
|
||
async def list_negotiations():
|
||
# Return all negotiations from DB
|
||
pass
|
||
|
||
@app.get("/api/negotiations/{negotiation_id}")
|
||
async def get_negotiation(negotiation_id: str):
|
||
"""Return negotiation + rounds + participants + analytics."""
|
||
neg = await db.get_negotiation(negotiation_id) if hasattr(db, 'get_negotiation') else None
|
||
rounds = await db.get_rounds(negotiation_id)
|
||
participants = await db.get_participants(negotiation_id)
|
||
analytics = await db.get_analytics(negotiation_id)
|
||
return {
|
||
"negotiation": neg, "rounds": [dict(r) for r in rounds],
|
||
"participants": [dict(p) for p in participants],
|
||
"analytics": dict(analytics) if analytics else None
|
||
}
|
||
|
||
@sio.event
|
||
async def join_negotiation(sid, negotiation_id):
|
||
await sio.enter_room(sid, negotiation_id)
|
||
|
||
socket_app = socketio.ASGIApp(sio, app)
|
||
```
|
||
|
||
Wire Socket.IO emissions into `run_negotiation()` round callbacks so dashboard updates live.
|
||
|
||
### DEV B: Build Dashboard with Analytics Components
|
||
|
||
→ **Reference: Build Guide v2 Section 10 (Dashboard) + Section 15 (Analytics)**
|
||
|
||
**Priority order (build what you have time for):**
|
||
|
||
1. **NegotiationTimeline** — Round cards showing proposals + reasoning (MUST HAVE)
|
||
2. **AgentChat** — Chat bubbles with personality badges next to agent names (MUST HAVE)
|
||
3. **SatisfactionChart** — Recharts `<LineChart>` with two lines, A and B (MUST HAVE — demo differentiator)
|
||
4. **FairnessScore** — Simple gauge/bar showing 0-100 fairness (MUST HAVE)
|
||
5. **ConcessionTimeline** — Vertical log of who gave what (NICE TO HAVE)
|
||
6. **ResolutionCard** — Final agreement + UPI link button (MUST HAVE)
|
||
7. **Live round counter** — "Round 2/5... negotiating..." with animation (NICE TO HAVE)
|
||
|
||
**SatisfactionChart component** (copy from Build Guide v2 Section 10):
|
||
```typescript
|
||
// Use Recharts LineChart with two Line elements
|
||
// Data format: [{round: 1, satisfaction_a: 85, satisfaction_b: 45}, ...]
|
||
```
|
||
|
||
**FairnessScore component** (copy from Build Guide v2 Section 10):
|
||
```typescript
|
||
// Simple progress bar with color coding (green 80+, yellow 60-79, red <60)
|
||
```
|
||
|
||
### ✅ MILESTONE 6 SUCCESS TEST
|
||
```
|
||
□ Dashboard loads at localhost:3000
|
||
□ Dashboard shows negotiation with round-by-round timeline
|
||
□ SatisfactionChart renders two lines (A and B) over rounds using Recharts
|
||
□ FairnessScore shows a number + colored bar
|
||
□ Personality badges visible next to agent names (😤 / 💚 / 📊 etc.)
|
||
□ Socket.IO connection works (real-time updates without page refresh)
|
||
□ Resolution card shows final agreement + UPI link (if applicable)
|
||
□ Dashboard looks professional enough for a 5-second judge glance
|
||
```
|
||
|
||
**If running behind:** The SatisfactionChart + FairnessScore are the priority analytics. Skip ConcessionTimeline. Skip dashboard entirely if needed — Telegram demo with voice + UPI is already strong.
|
||
|
||
---
|
||
|
||
## MILESTONE 7: Polish + Hardening
|
||
**⏰ Time: Hour 18 → Hour 21 (180 min)**
|
||
**👤 Who: Both devs**
|
||
|
||
### Priority Fixes (Do these first)
|
||
|
||
**1. Error handling everywhere — especially voice + Tavily + UPI**
|
||
```python
|
||
# Wrap EVERY external call in try/except
|
||
# Voice summary failure → just skip voice (text summary already sent)
|
||
# Tavily failure → fall back to Gemini knowledge
|
||
# UPI generation failure → show UPI ID + amount as plain text
|
||
try:
|
||
voice_path = await generate_voice_summary(text, neg_id)
|
||
except:
|
||
voice_path = None # Voice is bonus, not critical
|
||
|
||
try:
|
||
search_results = await tavily.execute(query)
|
||
except:
|
||
search_results = {"answer": "", "results": [], "summary": "Search unavailable"}
|
||
```
|
||
|
||
**2. Rate limit protection**
|
||
```python
|
||
GEMINI_DELAY = 1.5 # seconds between calls (15 RPM = 1 every 4 sec, but bursty)
|
||
|
||
async def rate_limited_call(agent, prompt, context=None):
|
||
await asyncio.sleep(GEMINI_DELAY)
|
||
return await agent.call(prompt, context)
|
||
```
|
||
|
||
**3. Fallback responses for when APIs fail**
|
||
```python
|
||
FALLBACK_RESPONSES = {
|
||
"collaborative": {"action": "propose", "proposal": {"summary": "Let's go with a popular option", "details": {}}, "satisfaction_score": 50},
|
||
"expenses": {"action": "propose", "proposal": {"summary": "Equal split as default", "details": {}}, "satisfaction_score": 50},
|
||
# ... one per feature
|
||
}
|
||
```
|
||
|
||
**4. Telegram message length limits**
|
||
```python
|
||
def safe_telegram_message(text, max_len=4000):
|
||
if len(text) > max_len:
|
||
return text[:max_len-50] + "\n\n_(truncated)_"
|
||
return text
|
||
```
|
||
|
||
**5. Test EVERY feature one more time**
|
||
```
|
||
□ Scheduling: quick test + voice summary plays
|
||
□ Expenses: verify math + UPI link works on phone + voice
|
||
□ Freelance: scope reduction + UPI for milestone
|
||
□ Roommate: Tavily returns real WiFi plans
|
||
□ Trip planning: 2-person minimum, 3-person if working
|
||
□ Marketplace: haggling + market price from Tavily + UPI
|
||
□ Collaborative: Tavily restaurant results + voice summary
|
||
□ Conflict: creative compromise + voice summary
|
||
```
|
||
|
||
**6. Personality impact verification**
|
||
```
|
||
□ Run same expense scenario twice:
|
||
- aggressive vs balanced → more rounds, harder concessions
|
||
- people_pleaser vs people_pleaser → resolves in 1-2 rounds
|
||
□ Verify personality label shows in Telegram messages
|
||
```
|
||
|
||
### ✅ MILESTONE 7 SUCCESS TEST
|
||
```
|
||
□ No crashes in any of the 8 features
|
||
□ All error paths handled gracefully (voice fails → text still works, Tavily fails → fallback)
|
||
□ Rate limiting works (no 429 errors from Gemini)
|
||
□ Voice summaries generate for resolved negotiations (at least expenses + collaborative)
|
||
□ UPI links open correctly on real phone
|
||
□ Messages look clean on actual Telegram
|
||
□ Dashboard analytics charts render correctly
|
||
□ Different personalities produce visibly different negotiation behavior
|
||
```
|
||
|
||
---
|
||
|
||
## MILESTONE 8: Demo Preparation
|
||
**⏰ Time: Hour 21 → Hour 24 (180 min)**
|
||
**👤 Who: Both devs**
|
||
|
||
### Hour 21-22: Script and rehearse the 3 demo scenarios
|
||
|
||
→ **Reference: Build Guide v2 Section 18 (Demo Script)**
|
||
|
||
**Demo 1: Restaurant Decision (60 sec)**
|
||
```
|
||
Pre-type these messages:
|
||
|
||
User A (set personality: analytical 📊):
|
||
"Finding dinner with @priya tonight. Craving Thai or spicy Indian,
|
||
nothing over 1500 for two, Bandra area, casual vibe"
|
||
|
||
User B (set personality: empathetic 💚):
|
||
"Dinner with @rahul tonight. I want something light, Mediterranean
|
||
or Thai. Need vegetarian options. Western suburbs, max 1200 for two"
|
||
|
||
EXPECTED: Tavily searches real restaurants → agents agree in 2-3 rounds
|
||
→ both get text + voice note with restaurant recommendation
|
||
→ dashboard shows satisfaction chart converging
|
||
```
|
||
|
||
**Demo 2: Expense Splitting (90 sec) — MONEY DEMO**
|
||
```
|
||
User A (set personality: aggressive 😤):
|
||
"Split our Goa trip costs with @priya. I paid 12K for hotel,
|
||
3K for fuel, 2K for Saturday dinner. Fuel should be 60-40 since I drove
|
||
the whole way. UPI settlement preferred. My UPI: rahul@paytm"
|
||
|
||
User B (set personality: balanced ⚖️):
|
||
"Trip expenses with @rahul. I think fuel should be 50-50
|
||
because I navigated and planned the entire route. Rest equal. Fine with UPI"
|
||
|
||
EXPECTED: 3-4 rounds of negotiation → fuel split lands at ~55-45
|
||
→ dashboard shows A's satisfaction starting high, B's rising
|
||
→ UPI PAYMENT BUTTON appears → tap to open GPay with ₹7,650 prefilled
|
||
→ voice notes: Agent A (male voice) + Agent B (female voice) read settlement
|
||
```
|
||
|
||
**Demo 3: Group Trip Planning (90 sec) — THE WOW MOMENT**
|
||
```
|
||
User A: "Weekend trip with @priya and @amit. Mountains, 15K max, March 22-23. Have car"
|
||
User B: "Trip with @rahul and @amit. Beach vibes, 10K max, March 20-24. No flights"
|
||
User C: "Weekend trip. Anywhere fun, 12K, only March 22-23. Love food and trekking"
|
||
|
||
EXPECTED: Tavily searches destinations → date overlap found → budget ceiling = 10K
|
||
→ compromise destination (Gokarna: beach + hills)
|
||
→ 3 different voice notes with 3 different voices
|
||
→ dashboard shows 3 satisfaction lines converging
|
||
```
|
||
|
||
### Hour 22-23: Run each demo scenario 3 times
|
||
|
||
```
|
||
Run 1: Check output quality. If bad, tweak prompts.
|
||
Run 2: Check timing. Each demo should complete in under 60 seconds.
|
||
Run 3: Final run. RECORD THIS ON VIDEO as backup.
|
||
|
||
Voice check: All voice notes play? Different voices for different agents?
|
||
UPI check: Payment button opens UPI app on phone?
|
||
Dashboard check: Analytics charts rendering correctly?
|
||
```
|
||
|
||
### Hour 23-23:30: Prepare failsafes
|
||
|
||
**Failsafe 1: Pre-seed completed negotiations with analytics**
|
||
```python
|
||
# seed_demo_data.py — pre-populate DB with beautiful results including analytics
|
||
# If live demo fails, show from dashboard with satisfaction charts + fairness scores
|
||
```
|
||
|
||
**Failsafe 2: Pre-generate voice summaries**
|
||
```
|
||
Generate MP3s for all 3 demo scenarios ahead of time.
|
||
If ElevenLabs is slow during demo, play pre-generated files.
|
||
```
|
||
|
||
**Failsafe 3: Record video backup**
|
||
```
|
||
Screen-record the perfect demo run showing:
|
||
- Telegram messages appearing
|
||
- Voice notes playing
|
||
- UPI button being tapped → GPay opening
|
||
- Dashboard with live analytics updating
|
||
This is your insurance policy.
|
||
```
|
||
|
||
**Failsafe 4: Offline mode**
|
||
```python
|
||
DEMO_MODE = os.getenv("DEMO_MODE", "false") == "true"
|
||
if DEMO_MODE:
|
||
# Load pre-computed responses + pre-generated voice files
|
||
```
|
||
|
||
### Hour 23:30-24: Final checks
|
||
|
||
```
|
||
□ Both phones charged + one has GPay/PhonePe for UPI demo
|
||
□ Telegram bots running on laptop
|
||
□ Dashboard accessible with analytics rendering
|
||
□ WiFi working (have 4G hotspot as backup)
|
||
□ Demo messages pre-typed in notes app
|
||
□ Personality set: A=aggressive, B=empathetic for Demo 2
|
||
□ Video backup saved to phone/drive
|
||
□ Pitch script rehearsed (under 5 min total)
|
||
□ Know which phone is User A / User B / User C
|
||
□ ElevenLabs credits remaining > 5,000 characters
|
||
□ Tavily searches remaining > 50 credits
|
||
□ Deep breath. You built something extraordinary. Go show them.
|
||
```
|
||
|
||
### ✅ MILESTONE 8 SUCCESS TEST
|
||
```
|
||
□ All 3 demo scenarios run successfully at least twice
|
||
□ Voice notes arrive within 5 seconds of resolution
|
||
□ UPI link opens payment app with correct amount
|
||
□ Dashboard shows satisfaction chart + fairness score
|
||
□ Demo completes in under 5 minutes (pitch + live demo)
|
||
□ Video backup recorded and saved
|
||
□ Both team members know the pitch script
|
||
□ Pre-seeded data in dashboard as fallback
|
||
```
|
||
|
||
---
|
||
|
||
## EMERGENCY PROTOCOLS
|
||
|
||
### "Gemini is down / rate limited"
|
||
→ Switch to DEMO_MODE with pre-computed responses
|
||
→ Increase GEMINI_DELAY to 3 seconds
|
||
→ Space out demo scenarios with talking time between them
|
||
|
||
### "ElevenLabs is down / credits exhausted"
|
||
→ Voice summaries are a bonus. Skip and send text-only resolutions.
|
||
→ Play pre-generated MP3s if available
|
||
→ Still demo the text + UPI + analytics (3 out of 4 add-ons work)
|
||
|
||
### "Tavily is down / out of credits"
|
||
→ Agents use Gemini's knowledge instead of live search
|
||
→ Restaurant names won't be verified/real but will still be plausible
|
||
→ Market prices will be estimated instead of cited
|
||
|
||
### "UPI links don't work"
|
||
→ Show the UPI link text (upi://pay?...) in the message
|
||
→ Show the UPI ID + amount as fallback text
|
||
→ Explain the concept: "On a real phone, tapping this opens GPay with amount pre-filled"
|
||
|
||
### "Telegram bot won't connect"
|
||
→ Check token, kill other instances, recreate via @BotFather
|
||
→ Worst case: demo via dashboard API + curl
|
||
|
||
### "Running out of time at Hour 14"
|
||
→ Skip trip multi-agent + roommate
|
||
→ Focus demo: Restaurant + Expenses (+ UPI + voice) + Marketplace
|
||
→ Skip dashboard — Telegram with voice + UPI is already impressive
|
||
|
||
### "Running out of time at Hour 20"
|
||
→ Skip ConcessionTimeline + dashboard polish
|
||
→ Focus: make 3 demo scenarios bulletproof
|
||
→ Ensure voice + UPI work in at least 1 demo
|
||
→ Record video backup
|
||
|
||
---
|
||
|
||
## Quick Milestone Status Tracker
|
||
|
||
Print this and check off as you go:
|
||
|
||
```
|
||
⏰ HOUR MILESTONE STATUS
|
||
────────────────────────────────────────────────────────────────────
|
||
0-1 □ M1: Skeleton + ALL API verification (Gemini+Tavily+11Labs) [ ]
|
||
1-3 □ M2: Base agent + preferences + personality system [ ]
|
||
3-6 □ M3: Negotiation engine + tools (Tavily/UPI/calc) [CRITICAL][ ]
|
||
6-8 □ M4: E2E Telegram flow + voice summaries + UPI links [ ]
|
||
8-14 □ M5: All 8 features (with Tavily + UPI where applicable) [ ]
|
||
14-18 □ M6: Dashboard + analytics charts (satisfaction/fairness) [ ]
|
||
18-21 □ M7: Polish + hardening + personality verification [ ]
|
||
21-24 □ M8: Demo prep (3 scenarios + failsafes + video backup) [ ]
|
||
────────────────────────────────────────────────────────────────────
|
||
□ DEMO TIME 🏆 [ ]
|
||
|
||
ADD-ON STATUS:
|
||
□ Voice summaries working (ElevenLabs TTS → Telegram voice notes)
|
||
□ Agent personalities working (/personality → prompt modifier)
|
||
□ Tavily search working (real restaurants/prices/plans)
|
||
□ UPI deep links working (tap → opens payment app)
|
||
□ Analytics dashboard working (satisfaction chart + fairness score)
|
||
```
|
||
|
||
---
|
||
|
||
*Each milestone builds on the previous one. Never skip. Never rush ahead. Green-light each checkpoint before moving on. This is how you ship in 24 hours without panic.*
|
||
|
||
*v2 add-ons integrated throughout: voice at M4, Tavily at M3, UPI at M4, personality at M2, analytics at M6.*
|
||
|
||
**Trust the process. Trust your agents. Go win. 🏆** |