mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
init
This commit is contained in:
0
negot8/backend/agents/__init__.py
Normal file
0
negot8/backend/agents/__init__.py
Normal file
57
negot8/backend/agents/base_agent.py
Normal file
57
negot8/backend/agents/base_agent.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
import json
|
||||
from config import GEMINI_API_KEY
|
||||
|
||||
_client = genai.Client(api_key=GEMINI_API_KEY)
|
||||
_DEFAULT_MODEL = "gemini-3-flash-preview"
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
def __init__(self, system_prompt: str, model_name: str = _DEFAULT_MODEL):
|
||||
self.system_prompt = system_prompt
|
||||
self.model_name = model_name
|
||||
|
||||
async def call(self, user_prompt: str, context: dict = None) -> dict:
|
||||
"""Call Gemini asynchronously and always return a dict."""
|
||||
full_prompt = user_prompt
|
||||
if context:
|
||||
full_prompt = f"CONTEXT:\n{json.dumps(context, indent=2)}\n\nTASK:\n{user_prompt}"
|
||||
|
||||
try:
|
||||
response = await _client.aio.models.generate_content(
|
||||
model=self.model_name,
|
||||
contents=full_prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
system_instruction=self.system_prompt,
|
||||
response_mime_type="application/json",
|
||||
temperature=0.7,
|
||||
),
|
||||
)
|
||||
text = response.text.strip()
|
||||
|
||||
# Fast path: response is already valid JSON
|
||||
try:
|
||||
result = json.loads(text)
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
return {"error": "Gemini returned non-dict JSON", "raw": text[:200]}
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Fallback: extract the first {...} block from the text
|
||||
start = text.find("{")
|
||||
end = text.rfind("}") + 1
|
||||
if start != -1 and end > start:
|
||||
try:
|
||||
result = json.loads(text[start:end])
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return {"error": "Could not parse JSON from Gemini response", "raw": text[:500]}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[BaseAgent] Gemini call failed: {e}")
|
||||
return {"error": str(e)}
|
||||
111
negot8/backend/agents/matching_agent.py
Normal file
111
negot8/backend/agents/matching_agent.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
backend/agents/matching_agent.py
|
||||
|
||||
Scores how well an applicant's preferences match an open contract's
|
||||
requirements using a single Gemini call. Returns a 0-100 score and
|
||||
a short explanation — used to rank applicants before the poster picks one.
|
||||
"""
|
||||
|
||||
import json
|
||||
from agents.base_agent import BaseAgent
|
||||
|
||||
_MATCHING_PROMPT = """You are a matching agent. Your job is to score how well an applicant's
|
||||
preferences align with an open contract's requirements.
|
||||
|
||||
Contract type: {contract_type}
|
||||
|
||||
CONTRACT REQUIREMENTS (what the poster needs):
|
||||
{requirements}
|
||||
|
||||
APPLICANT PREFERENCES (what the applicant brings / wants):
|
||||
{preferences}
|
||||
|
||||
Evaluate on these axes:
|
||||
1. Core match — does the applicant fundamentally fit what the contract needs?
|
||||
2. Budget/rate alignment — are financial expectations compatible?
|
||||
3. Timeline/availability — do schedules overlap?
|
||||
4. Skills/criteria — do they have what is required?
|
||||
5. Flexibility potential — could a negotiation bridge remaining gaps?
|
||||
|
||||
RESPOND ONLY with valid JSON in this exact format:
|
||||
{{
|
||||
"match_score": <integer 0-100>,
|
||||
"match_reasoning": "<one concise sentence explaining the score>",
|
||||
"key_alignments": ["<thing 1 that matches well>", "<thing 2>"],
|
||||
"key_gaps": ["<gap 1>", "<gap 2>"]
|
||||
}}
|
||||
|
||||
Scoring guide:
|
||||
90-100 → Nearly perfect fit, minimal negotiation needed
|
||||
70-89 → Good fit, small gaps bridgeable in negotiation
|
||||
50-69 → Moderate fit, notable differences but workable
|
||||
30-49 → Weak fit, significant gaps — negotiation will be hard
|
||||
0-29 → Poor fit, fundamental incompatibility
|
||||
"""
|
||||
|
||||
|
||||
class MatchingAgent(BaseAgent):
|
||||
def __init__(self):
|
||||
super().__init__(system_prompt="You are a concise JSON-only matching agent.")
|
||||
|
||||
async def score_applicant(
|
||||
self,
|
||||
contract_requirements: dict,
|
||||
applicant_preferences: dict,
|
||||
contract_type: str,
|
||||
) -> dict:
|
||||
"""
|
||||
Score how well an applicant matches a contract.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"match_score": int (0-100),
|
||||
"match_reasoning": str,
|
||||
"key_alignments": list[str],
|
||||
"key_gaps": list[str],
|
||||
}
|
||||
On failure returns a safe default with score=0.
|
||||
"""
|
||||
prompt = _MATCHING_PROMPT.format(
|
||||
contract_type=contract_type,
|
||||
requirements=json.dumps(contract_requirements, indent=2),
|
||||
preferences=json.dumps(applicant_preferences, indent=2),
|
||||
)
|
||||
try:
|
||||
# self.call() returns a dict — BaseAgent already handles JSON parsing
|
||||
result = await self.call(prompt)
|
||||
if "error" in result:
|
||||
raise ValueError(result["error"])
|
||||
# Clamp score to 0-100
|
||||
result["match_score"] = max(0, min(100, int(result.get("match_score", 0))))
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"[MatchingAgent] score_applicant failed: {e}")
|
||||
return {
|
||||
"match_score": 0,
|
||||
"match_reasoning": "Could not compute score.",
|
||||
"key_alignments": [],
|
||||
"key_gaps": [],
|
||||
}
|
||||
|
||||
|
||||
# Module-level singleton
|
||||
_agent = None
|
||||
|
||||
|
||||
def get_matching_agent() -> MatchingAgent:
|
||||
global _agent
|
||||
if _agent is None:
|
||||
_agent = MatchingAgent()
|
||||
return _agent
|
||||
|
||||
|
||||
async def score_applicant(
|
||||
contract_requirements: dict,
|
||||
applicant_preferences: dict,
|
||||
contract_type: str,
|
||||
) -> dict:
|
||||
"""Convenience wrapper — use the module-level singleton."""
|
||||
return await get_matching_agent().score_applicant(
|
||||
contract_requirements, applicant_preferences, contract_type
|
||||
)
|
||||
125
negot8/backend/agents/negotiation.py
Normal file
125
negot8/backend/agents/negotiation.py
Normal file
@@ -0,0 +1,125 @@
|
||||
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,
|
||||
feature_context: str = ""):
|
||||
"""
|
||||
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 = 7
|
||||
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,
|
||||
feature_context=feature_context
|
||||
)
|
||||
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,
|
||||
feature_context=feature_context
|
||||
)
|
||||
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,
|
||||
feature_context=feature_context
|
||||
)
|
||||
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
|
||||
97
negot8/backend/agents/negotiator_agent.py
Normal file
97
negot8/backend/agents/negotiator_agent.py
Normal file
@@ -0,0 +1,97 @@
|
||||
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. HARD CONSTRAINT FIRST: If the received proposal satisfies ALL of your human's hard constraints, you MUST "accept" — even if satisfaction < 70%. Meeting someone's stated floor/ceiling/limit IS a valid deal.
|
||||
EXAMPLE: Seller's hard constraint is "minimum price ≥ 1,000,000". Buyer offers exactly 1,000,000 → accept.
|
||||
EXAMPLE: Buyer's hard constraint is "budget ≤ 1,000,000". Seller offers exactly 1,000,000 → accept.
|
||||
6. Only use satisfaction thresholds when no hard constraints are involved: Accept if >= 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, feature_context: str = ""
|
||||
) -> dict:
|
||||
context_block = (
|
||||
f"\n\nDOMAIN CONTEXT (use this real-world data in your proposal):\n{feature_context}"
|
||||
if feature_context else ""
|
||||
)
|
||||
human_name = my_preferences.get("human_name", "my human")
|
||||
return await self.call(
|
||||
user_prompt=f"""Generate the FIRST proposal for this {feature_type} negotiation.{context_block}
|
||||
|
||||
You represent {human_name}. Always refer to them by name (not as "my human" or "my client") in your reasoning field.
|
||||
{human_name}'s preferences:
|
||||
{json.dumps(my_preferences, indent=2)}
|
||||
|
||||
This is Round 1. Propose {human_name}'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, feature_context: str = ""
|
||||
) -> dict:
|
||||
context_block = (
|
||||
f"\n\nDOMAIN CONTEXT (use this real-world data when evaluating):\n{feature_context}"
|
||||
if feature_context else ""
|
||||
)
|
||||
human_name = my_preferences.get("human_name", "my human")
|
||||
return await self.call(
|
||||
user_prompt=f"""Evaluate this proposal and respond. Round {round_number} of a {feature_type} negotiation.{context_block}
|
||||
|
||||
You represent {human_name}. Always refer to them by name in your reasoning field.
|
||||
|
||||
RECEIVED PROPOSAL FROM OTHER AGENT:
|
||||
{json.dumps(received_proposal, indent=2)}
|
||||
|
||||
{human_name.upper()}'S PREFERENCES:
|
||||
{json.dumps(my_preferences, indent=2)}
|
||||
|
||||
Evaluate against my human's preferences using this STRICT decision order:
|
||||
1. CHECK HARD CONSTRAINTS FIRST: Does the received proposal satisfy ALL items where "hard": true?
|
||||
- If YES → your action MUST be "accept". Do NOT counter. Do NOT escalate. Hard constraints met = deal is done.
|
||||
- If NO → continue to step 2.
|
||||
2. If a hard constraint is violated: counter (round < 4) or escalate (round >= 4 with < 40% satisfaction).
|
||||
3. If there are no hard constraints: accept if satisfaction >= 70, counter if 40-69, escalate if < 40 and round >= 3.
|
||||
|
||||
CRITICAL: A proposal that meets someone's stated minimum/maximum is ALWAYS acceptable to them. Never counter when all hard constraints are satisfied.
|
||||
If countering, make a strategic concession while protecting high-priority items."""
|
||||
)
|
||||
51
negot8/backend/agents/personal_agent.py
Normal file
51
negot8/backend/agents/personal_agent.py
Normal file
@@ -0,0 +1,51 @@
|
||||
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
|
||||
)
|
||||
# Guard: must always be a dict — never let a string propagate downstream
|
||||
if not isinstance(result, dict):
|
||||
return {"error": f"extract_preferences got non-dict: {type(result).__name__}", "raw": str(result)[:200]}
|
||||
return result
|
||||
Reference in New Issue
Block a user