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