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

568
negot8/backend/database.py Normal file
View File

@@ -0,0 +1,568 @@
import aiosqlite
import json
import uuid
from datetime import datetime
from config import DATABASE_PATH
async def init_db():
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute("""
CREATE TABLE IF NOT EXISTS users (
telegram_id INTEGER PRIMARY KEY,
username TEXT,
display_name TEXT,
personality TEXT DEFAULT 'balanced',
voice_id TEXT DEFAULT 'pNInz6obpgDQGcFmaJgB',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS negotiations (
id TEXT PRIMARY KEY,
feature_type TEXT,
status TEXT DEFAULT 'pending',
initiator_id INTEGER,
resolution TEXT,
voice_summary_file TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS participants (
negotiation_id TEXT REFERENCES negotiations(id),
user_id INTEGER,
preferences TEXT,
personality_used TEXT DEFAULT 'balanced',
PRIMARY KEY (negotiation_id, user_id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS rounds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
negotiation_id TEXT REFERENCES negotiations(id),
round_number INTEGER,
proposer_id INTEGER,
proposal TEXT,
response_type TEXT,
response TEXT,
reasoning TEXT,
satisfaction_a REAL,
satisfaction_b REAL,
concessions_made TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS tool_calls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
negotiation_id TEXT REFERENCES negotiations(id),
tool_name TEXT,
tool_input TEXT,
tool_output TEXT,
called_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS negotiation_analytics (
negotiation_id TEXT PRIMARY KEY REFERENCES negotiations(id),
satisfaction_timeline TEXT,
concession_log TEXT,
fairness_score REAL,
total_concessions_a INTEGER DEFAULT 0,
total_concessions_b INTEGER DEFAULT 0,
computed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS user_calendar_tokens (
telegram_id INTEGER PRIMARY KEY,
token_json TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS blockchain_proofs (
negotiation_id TEXT PRIMARY KEY REFERENCES negotiations(id),
tx_hash TEXT,
block_number INTEGER,
agreement_hash TEXT,
explorer_url TEXT,
gas_used INTEGER DEFAULT 0,
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# ─── Open Contracts (public marketplace) ─────────────────────────────────
await db.execute("""
CREATE TABLE IF NOT EXISTS open_contracts (
id TEXT PRIMARY KEY,
poster_id INTEGER NOT NULL,
contract_type TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
requirements TEXT,
status TEXT DEFAULT 'open',
matched_applicant_id INTEGER,
negotiation_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS contract_applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contract_id TEXT REFERENCES open_contracts(id),
applicant_id INTEGER NOT NULL,
preferences TEXT,
match_score REAL DEFAULT 0,
match_reasoning TEXT DEFAULT '',
status TEXT DEFAULT 'pending',
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.commit()
print("✅ Database initialized (v5 schema — open contracts + applications added)")
# ─── User helpers ───────────────────────────────────────────────────────────
async def create_user(telegram_id: int, username: str, display_name: str):
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""INSERT OR IGNORE INTO users (telegram_id, username, display_name)
VALUES (?, ?, ?)""",
(telegram_id, username or "", display_name or ""),
)
await db.commit()
async def get_user(telegram_id: int):
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM users WHERE telegram_id = ?", (telegram_id,)
) as cursor:
return await cursor.fetchone()
async def update_user_personality(telegram_id: int, personality: str):
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"UPDATE users SET personality = ? WHERE telegram_id = ?",
(personality, telegram_id),
)
await db.commit()
# ─── Negotiation helpers ─────────────────────────────────────────────────────
async def create_negotiation(feature_type: str, initiator_id: int) -> str:
neg_id = str(uuid.uuid4())[:8]
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""INSERT INTO negotiations (id, feature_type, initiator_id, status)
VALUES (?, ?, ?, 'pending')""",
(neg_id, feature_type, initiator_id),
)
await db.commit()
return neg_id
async def add_participant(
negotiation_id: str,
user_id: int,
preferences: dict,
personality_used: str = "balanced",
):
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""INSERT OR REPLACE INTO participants
(negotiation_id, user_id, preferences, personality_used)
VALUES (?, ?, ?, ?)""",
(negotiation_id, user_id, json.dumps(preferences), personality_used),
)
await db.commit()
async def get_participants(negotiation_id: str):
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM participants WHERE negotiation_id = ?", (negotiation_id,)
) as cursor:
rows = await cursor.fetchall()
return [dict(r) for r in rows]
async def save_round(
negotiation_id: str,
round_number: int,
proposer_id: int,
proposal: dict,
response_type: str = None,
response: dict = None,
reasoning: str = None,
satisfaction_a: float = None,
satisfaction_b: float = None,
concessions_made: list = None,
):
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""INSERT INTO rounds
(negotiation_id, round_number, proposer_id, proposal,
response_type, response, reasoning,
satisfaction_a, satisfaction_b, concessions_made)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
negotiation_id,
round_number,
proposer_id,
json.dumps(proposal),
response_type,
json.dumps(response) if response else None,
reasoning,
satisfaction_a,
satisfaction_b,
json.dumps(concessions_made or []),
),
)
await db.commit()
async def get_rounds(negotiation_id: str):
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM rounds WHERE negotiation_id = ? ORDER BY round_number",
(negotiation_id,),
) as cursor:
rows = await cursor.fetchall()
return [dict(r) for r in rows]
async def update_negotiation_status(
negotiation_id: str,
status: str,
resolution: dict = None,
voice_file: str = None,
):
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""UPDATE negotiations
SET status = ?,
resolution = ?,
voice_summary_file = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?""",
(
status,
json.dumps(resolution) if resolution else None,
voice_file,
negotiation_id,
),
)
await db.commit()
async def store_analytics(analytics: dict):
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""INSERT OR REPLACE INTO negotiation_analytics
(negotiation_id, satisfaction_timeline, concession_log,
fairness_score, total_concessions_a, total_concessions_b)
VALUES (?, ?, ?, ?, ?, ?)""",
(
analytics["negotiation_id"],
analytics.get("satisfaction_timeline", "[]"),
analytics.get("concession_log", "[]"),
analytics.get("fairness_score", 0),
analytics.get("total_concessions_a", 0),
analytics.get("total_concessions_b", 0),
),
)
await db.commit()
# ─── Google Calendar token helpers ─────────────────────────────────────────
async def save_calendar_token(telegram_id: int, token_json: str):
"""Upsert a user's Google OAuth2 credentials (serialized JSON)."""
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""INSERT INTO user_calendar_tokens (telegram_id, token_json, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(telegram_id) DO UPDATE
SET token_json = excluded.token_json,
updated_at = CURRENT_TIMESTAMP""",
(telegram_id, token_json),
)
await db.commit()
async def get_calendar_token(telegram_id: int) -> str:
"""Return stored token JSON for a user, or None if not connected."""
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT token_json FROM user_calendar_tokens WHERE telegram_id = ?",
(telegram_id,),
) as cursor:
row = await cursor.fetchone()
return row["token_json"] if row else None
async def get_analytics(negotiation_id: str):
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM negotiation_analytics WHERE negotiation_id = ?",
(negotiation_id,),
) as cursor:
row = await cursor.fetchone()
return dict(row) if row else None
# ─── Blockchain proof helpers ────────────────────────────────────────────────
async def store_blockchain_proof(
negotiation_id: str,
tx_hash: str,
block_number: int,
agreement_hash: str,
explorer_url: str,
gas_used: int = 0,
):
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""INSERT OR REPLACE INTO blockchain_proofs
(negotiation_id, tx_hash, block_number, agreement_hash, explorer_url, gas_used)
VALUES (?, ?, ?, ?, ?, ?)""",
(negotiation_id, tx_hash, block_number, agreement_hash, explorer_url, gas_used),
)
await db.commit()
async def get_blockchain_proof(negotiation_id: str):
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM blockchain_proofs WHERE negotiation_id = ?",
(negotiation_id,),
) as cursor:
row = await cursor.fetchone()
return dict(row) if row else None
async def get_negotiation(negotiation_id: str):
"""Return a single negotiation row by ID, or None if not found."""
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM negotiations WHERE id = ?",
(negotiation_id,),
) as cursor:
row = await cursor.fetchone()
return dict(row) if row else None
async def get_latest_negotiation_for_user(telegram_id: int):
"""Return the most recent resolved negotiation_id that the given user participated in."""
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
# First try: find a resolved negotiation where the user is the initiator
async with db.execute(
"""SELECT n.id FROM negotiations n
JOIN participants p ON p.negotiation_id = n.id
WHERE p.user_id = ? AND n.status = 'resolved'
ORDER BY n.created_at DESC LIMIT 1""",
(telegram_id,),
) as cursor:
row = await cursor.fetchone()
return row["id"] if row else None
# ─── Open Contracts helpers ──────────────────────────────────────────────────
async def create_open_contract(
poster_id: int,
contract_type: str,
title: str,
description: str,
requirements: dict,
) -> str:
"""Create a new open contract. Returns the 8-char UUID id."""
contract_id = str(uuid.uuid4())[:8]
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""INSERT INTO open_contracts
(id, poster_id, contract_type, title, description, requirements, status)
VALUES (?, ?, ?, ?, ?, ?, 'open')""",
(contract_id, poster_id, contract_type, title, description, json.dumps(requirements)),
)
await db.commit()
return contract_id
async def get_open_contracts(status: str = "open") -> list:
"""Return all contracts with the given status, newest first."""
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"""SELECT oc.*,
u.username AS poster_username,
u.display_name AS poster_name,
COUNT(ca.id) AS application_count
FROM open_contracts oc
LEFT JOIN users u ON u.telegram_id = oc.poster_id
LEFT JOIN contract_applications ca ON ca.contract_id = oc.id
WHERE oc.status = ?
GROUP BY oc.id
ORDER BY oc.created_at DESC""",
(status,),
) as cur:
rows = await cur.fetchall()
result = []
for r in rows:
d = dict(r)
if isinstance(d.get("requirements"), str):
try:
d["requirements"] = json.loads(d["requirements"])
except Exception:
pass
result.append(d)
return result
async def get_open_contract(contract_id: str):
"""Return a single open contract by ID, or None."""
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"""SELECT oc.*, u.username AS poster_username, u.display_name AS poster_name
FROM open_contracts oc
LEFT JOIN users u ON u.telegram_id = oc.poster_id
WHERE oc.id = ?""",
(contract_id,),
) as cur:
row = await cur.fetchone()
if not row:
return None
d = dict(row)
if isinstance(d.get("requirements"), str):
try:
d["requirements"] = json.loads(d["requirements"])
except Exception:
pass
return d
async def add_application(contract_id: str, applicant_id: int, preferences: dict) -> int:
"""Add an application to an open contract. Returns the new application row id."""
async with aiosqlite.connect(DATABASE_PATH) as db:
cursor = await db.execute(
"""INSERT INTO contract_applications
(contract_id, applicant_id, preferences, status)
VALUES (?, ?, ?, 'pending')""",
(contract_id, applicant_id, json.dumps(preferences)),
)
await db.commit()
return cursor.lastrowid
async def get_applications(contract_id: str) -> list:
"""Return all applications for a contract, sorted by match_score descending."""
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"""SELECT ca.*, u.username, u.display_name
FROM contract_applications ca
LEFT JOIN users u ON u.telegram_id = ca.applicant_id
WHERE ca.contract_id = ?
ORDER BY ca.match_score DESC""",
(contract_id,),
) as cur:
rows = await cur.fetchall()
result = []
for r in rows:
d = dict(r)
if isinstance(d.get("preferences"), str):
try:
d["preferences"] = json.loads(d["preferences"])
except Exception:
pass
result.append(d)
return result
async def update_application_match_score(app_id: int, score: float, reasoning: str):
"""Update the AI match score + reasoning for an application row."""
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""UPDATE contract_applications
SET match_score = ?, match_reasoning = ?
WHERE id = ?""",
(score, reasoning, app_id),
)
await db.commit()
async def claim_contract(contract_id: str, applicant_id: int, negotiation_id: str):
"""Mark a contract as 'negotiating', lock in the matched applicant, and link the negotiation."""
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"""UPDATE open_contracts
SET status = 'negotiating',
matched_applicant_id = ?,
negotiation_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?""",
(applicant_id, negotiation_id, contract_id),
)
await db.execute(
"UPDATE contract_applications SET status = 'selected' WHERE contract_id = ? AND applicant_id = ?",
(contract_id, applicant_id),
)
await db.execute(
"UPDATE contract_applications SET status = 'rejected' WHERE contract_id = ? AND applicant_id != ?",
(contract_id, applicant_id),
)
await db.commit()
async def close_contract(contract_id: str):
"""Mark a contract as resolved/closed after negotiation completes."""
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"UPDATE open_contracts SET status = 'resolved', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
(contract_id,),
)
await db.commit()
async def get_contracts_by_poster(poster_id: int) -> list:
"""Return all open contracts created by a given poster, newest first."""
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(
"""SELECT oc.*,
COUNT(ca.id) AS application_count,
MAX(ca.match_score) AS best_score
FROM open_contracts oc
LEFT JOIN contract_applications ca ON ca.contract_id = oc.id
WHERE oc.poster_id = ?
GROUP BY oc.id
ORDER BY oc.created_at DESC""",
(poster_id,),
) as cur:
rows = await cur.fetchall()
result = []
for r in rows:
d = dict(r)
if isinstance(d.get("requirements"), str):
try:
d["requirements"] = json.loads(d["requirements"])
except Exception:
pass
result.append(d)
return result