Files
B.Tech-Project-III/negot8/backend/features/trip.py
2026-04-05 00:43:23 +05:30

339 lines
14 KiB
Python

import asyncio
import json
from features.base_feature import BaseFeature
from tools.tavily_search import TavilySearchTool
from tools.calculator import CalculatorTool
_tavily = TavilySearchTool()
_calc = CalculatorTool()
class TripFeature(BaseFeature):
async def get_context(self, preferences_a: dict, preferences_b: dict, user_a_id: int = None, user_b_id: int = None) -> str:
"""
Compute date intersection across both parties.
Fetch real destination options via Tavily.
"""
raw_a = preferences_a.get("raw_details", {})
raw_b = preferences_b.get("raw_details", {})
dates_a = raw_a.get("available_dates") or raw_a.get("dates") or []
dates_b = raw_b.get("available_dates") or raw_b.get("dates") or []
budget_a = raw_a.get("budget_per_person") or raw_a.get("budget") or ""
budget_b = raw_b.get("budget_per_person") or raw_b.get("budget") or ""
dest_pref_a = raw_a.get("destination_preference") or raw_a.get("destination") or ""
dest_pref_b = raw_b.get("destination_preference") or raw_b.get("destination") or ""
from_city = raw_a.get("origin") or raw_b.get("origin") or raw_a.get("city") or "Mumbai"
accom_a = raw_a.get("accommodation_type") or ""
accom_b = raw_b.get("accommodation_type") or ""
# Compute date overlap (simple string intersection)
dates_a_set = set(str(d).lower() for d in (dates_a if isinstance(dates_a, list) else [dates_a]))
dates_b_set = set(str(d).lower() for d in (dates_b if isinstance(dates_b, list) else [dates_b]))
common_dates = dates_a_set & dates_b_set
# Budget ceiling = lower budget
budget_ceiling = ""
if budget_a and budget_b:
try:
ba = float(str(budget_a).replace(",", ""))
bb = float(str(budget_b).replace(",", ""))
budget_ceiling = f"{min(ba, bb):,.0f}/person"
except (ValueError, TypeError):
budget_ceiling = f"{budget_a} or {budget_b} (take lower)"
elif budget_a or budget_b:
budget_ceiling = f"{budget_a or budget_b}/person"
# Destination type combined
dest_type = " or ".join(filter(None, [dest_pref_a, dest_pref_b])) or "weekend getaway"
# Tavily: real destination options
search_text = ""
try:
query = f"weekend getaway destinations from {from_city} {dest_type} budget India 2026"
result = await _tavily.execute(query)
answer = result.get("answer", "")
results = result.get("results", [])[:4]
parts = []
if answer:
parts.append(f"Destination summary: {answer[:300]}")
for r in results:
title = r.get("title", "")
content = r.get("content", "")[:150]
if title:
parts.append(f"{title}: {content}")
search_text = "\n".join(parts)
except Exception as e:
search_text = f"Search unavailable. Use your knowledge of destinations from {from_city}."
lines = [
"TRIP PLANNING DOMAIN RULES:",
"• Date overlap is PRIORITY #1 — only propose dates both parties are available.",
"• Budget ceiling = LOWEST budget in the group. No one should overspend.",
"• Destination must satisfy at least one preference from each party.",
"• Accommodation type: prefer the more comfortable option if budget allows.",
"• If no date overlap: escalate immediately with adjusted date suggestions.",
]
if from_city:
lines.append(f"\nOrigin city: {from_city}")
if dates_a:
lines.append(f"Person A available: {dates_a}")
if dates_b:
lines.append(f"Person B available: {dates_b}")
if common_dates:
lines.append(f"✅ OVERLAPPING DATES: {', '.join(common_dates)}")
else:
lines.append("⚠️ No exact date overlap found — propose closest alternatives.")
if budget_ceiling:
lines.append(f"Budget ceiling: {budget_ceiling}")
if dest_pref_a:
lines.append(f"Person A wants: {dest_pref_a}")
if dest_pref_b:
lines.append(f"Person B wants: {dest_pref_b}")
if accom_a or accom_b:
lines.append(f"Accommodation preferences: {accom_a or ''} / {accom_b or ''}")
if search_text:
lines.append(f"\nREAL DESTINATION OPTIONS:\n{search_text}")
return "\n".join(lines)
def format_resolution(
self, resolution: dict, preferences_a: dict, preferences_b: dict
) -> str:
status = resolution.get("status", "resolved")
final = resolution.get("final_proposal", {})
details = final.get("details", {})
rounds = resolution.get("rounds_taken", "?")
summary = resolution.get("summary", "")
if status == "escalated":
return (
f"⚠️ *Trip Planning — Human Decision Needed*\n\n"
f"_{summary}_\n\n"
f"Agents proposed options but couldn't finalize in {rounds} round(s). "
f"Please agree on dates and destination directly."
)
destination = details.get("destination") or details.get("place") or final.get("summary", "")
dates = details.get("dates") or details.get("travel_dates") or details.get("date") or ""
budget = details.get("budget_per_person") or details.get("budget") or ""
accommodation = details.get("accommodation") or details.get("stay") or ""
activities = details.get("activities") or details.get("things_to_do") or []
duration = details.get("duration") or details.get("nights") or ""
lines = ["✈️ *Trip Planned!*\n"]
if destination:
lines.append(f"🗺 *Destination:* {destination}")
if dates:
lines.append(f"📅 *Dates:* {dates}")
if duration:
lines.append(f"⏱ *Duration:* {duration}")
if accommodation:
lines.append(f"🏨 *Stay:* {accommodation}")
if budget:
lines.append(f"💰 *Budget/person:* ₹{budget}")
if activities and isinstance(activities, list):
lines.append("🎯 *Activities:*")
for act in activities[:4]:
lines.append(f"{act}")
elif activities:
lines.append(f"🎯 *Activities:* {activities}")
lines.append(f"\n⏱ Planned in {rounds} round(s)")
if summary and summary != "Agreement reached":
lines.append(f"_{summary}_")
return "\n".join(lines)
# ────────────────────────────────────────────────────────────────────────────
# Group negotiation for 3+ participants (trip planning)
# ────────────────────────────────────────────────────────────────────────────
async def run_group_negotiation(
negotiation_id: str,
all_preferences: list,
all_user_ids: list,
feature_type: str = "trip",
personalities: list = None,
on_round_update=None,
on_resolution=None,
) -> dict:
"""
Multi-agent group negotiation using a mediator approach.
One NegotiatorAgent acts as mediator, sees all preferences, proposes.
Each participant's NegotiatorAgent scores the proposal.
Iterates up to 5 rounds; escalates if no full agreement.
"""
import database as db
from agents.negotiator_agent import NegotiatorAgent
if personalities is None:
personalities = ["balanced"] * len(all_preferences)
await db.update_negotiation_status(negotiation_id, "active")
# Create mediator (uses balanced personality)
mediator = NegotiatorAgent(personality="balanced")
# Create per-participant evaluators
evaluators = [
NegotiatorAgent(personality=p)
for p in personalities
]
# Pre-fetch trip context
feature = TripFeature()
feature_context = ""
if len(all_preferences) >= 2:
try:
feature_context = await feature.get_context(all_preferences[0], all_preferences[1])
except Exception:
feature_context = ""
max_rounds = 5
current_proposal = None
satisfaction_timeline = []
for round_num in range(1, max_rounds + 1):
await asyncio.sleep(1.5)
# Mediator generates/refines proposal
mediator_prompt = f"""You are MEDIATING a group {feature_type} negotiation with {len(all_preferences)} participants.
{"DOMAIN CONTEXT:" + chr(10) + feature_context if feature_context else ""}
ALL PARTICIPANTS' PREFERENCES:
{json.dumps(all_preferences, indent=2)}
{"PREVIOUS PROPOSAL (refine based on feedback below):" + chr(10) + json.dumps(current_proposal, indent=2) if current_proposal else ""}
Round {round_num} of {max_rounds}. Generate a proposal that maximizes GROUP satisfaction.
Rules:
- Budget ceiling = LOWEST budget among all participants.
- Dates = intersection of all available dates (or closest compromise).
- Every participant must get at least one preference honored.
- Return the standard proposal JSON format."""
try:
mediated = await mediator.call(user_prompt=mediator_prompt)
except Exception:
mediated = {
"action": "propose",
"proposal": {"summary": "Group proposal", "details": {}},
"satisfaction_score": 60,
"reasoning": "Mediator generated group proposal",
"concessions_made": [],
"concessions_requested": [],
}
current_proposal = mediated.get("proposal", {})
# Score with each participant
scores = []
low_scorer = None
low_score = 100
for i, (prefs, evaluator) in enumerate(zip(all_preferences, evaluators)):
eval_prompt = f"""Evaluate this group {feature_type} proposal for Participant {i+1}.
PROPOSAL:
{json.dumps(current_proposal, indent=2)}
YOUR PREFERENCES:
{json.dumps(prefs, indent=2)}
Score it and decide: accept (>= 65), counter (40-64), or escalate (< 40 after round 3).
Return standard JSON format."""
try:
eval_response = await evaluator.call(user_prompt=eval_prompt)
except Exception:
eval_response = {"action": "accept", "satisfaction_score": 65, "reasoning": ""}
score = eval_response.get("satisfaction_score", 65)
scores.append(score)
if score < low_score:
low_score = score
low_scorer = i
avg_score = sum(scores) / len(scores)
sat_entry = {"round": round_num}
for i, s in enumerate(scores):
sat_entry[f"score_{chr(65+i)}"] = s
satisfaction_timeline.append(sat_entry)
round_data = {
"negotiation_id": negotiation_id,
"round_number": round_num,
"action": "counter" if avg_score < 65 else "accept",
"proposal": current_proposal,
"satisfaction_score": avg_score,
"reasoning": mediated.get("reasoning", "")[:200],
"group_scores": scores,
"satisfaction_a": scores[0] if scores else 0,
"satisfaction_b": scores[1] if len(scores) > 1 else 0,
}
# Save round
try:
await db.save_round(
negotiation_id=negotiation_id,
round_number=round_num,
proposer_id=all_user_ids[0],
proposal=mediated,
response_type=round_data["action"],
reasoning=round_data["reasoning"],
satisfaction_a=scores[0] if scores else 0,
satisfaction_b=scores[1] if len(scores) > 1 else 0,
concessions_made=[],
)
except Exception:
pass
if on_round_update:
await on_round_update(round_data)
# All participants satisfied
if all(s >= 65 for s in scores):
resolution = {
"status": "resolved",
"final_proposal": current_proposal,
"rounds_taken": round_num,
"summary": current_proposal.get("summary", "Group trip planned!"),
"satisfaction_timeline": satisfaction_timeline,
"group_scores": scores,
}
await db.update_negotiation_status(negotiation_id, "resolved", resolution)
if on_resolution:
await on_resolution(resolution)
return resolution
# After round 3, if any score < 40, escalate
if round_num >= 3 and low_score < 40:
resolution = {
"status": "escalated",
"final_proposal": current_proposal,
"rounds_taken": round_num,
"summary": f"Participant {low_scorer+1} couldn't agree. Human decision needed.",
"satisfaction_timeline": satisfaction_timeline,
"group_scores": scores,
}
await db.update_negotiation_status(negotiation_id, "escalated", resolution)
if on_resolution:
await on_resolution(resolution)
return resolution
# Max rounds exhausted
resolution = {
"status": "escalated",
"final_proposal": current_proposal,
"rounds_taken": max_rounds,
"summary": "Max rounds reached. Best group proposal for human review.",
"satisfaction_timeline": satisfaction_timeline,
"group_scores": scores if scores else [],
}
await db.update_negotiation_status(negotiation_id, "escalated", resolution)
if on_resolution:
await on_resolution(resolution)
return resolution