This commit is contained in:
2026-04-05 00:43:23 +05:30
commit 8be37d3e92
425 changed files with 101853 additions and 0 deletions

View File

View 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)}

View 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
)

View 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

View 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."""
)

View 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