mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
init
This commit is contained in:
338
negot8/backend/features/trip.py
Normal file
338
negot8/backend/features/trip.py
Normal file
@@ -0,0 +1,338 @@
|
||||
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
|
||||
Reference in New Issue
Block a user