mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
339 lines
14 KiB
Python
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
|