lotr-sut/sut/backend/services/negotiation_logger.py
Fellowship Scholar f6a5823439 init commit
2026-03-29 20:07:56 +00:00

214 lines
6.8 KiB
Python

"""Logging service for bargaining negotiations (anonymized)."""
import json
import logging
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
import hashlib
import uuid
logger = logging.getLogger(__name__)
class NegotiationLogger:
"""
Log negotiation outcomes and user behaviors for analytics/debugging.
All logs are anonymized - no user identifiers are stored.
Each negotiation gets a unique session ID for tracking.
"""
# In-memory store for simplicity. In production, use a database or CloudWatch Logs.
_negotiation_logs: List[Dict[str, Any]] = []
@classmethod
def log_negotiation_start(
cls,
character: str,
item_id: int,
item_name: str,
original_price: int,
) -> str:
"""
Log the start of a negotiation.
Returns:
session_id: Unique identifier for this negotiation session
"""
session_id = str(uuid.uuid4())
log_entry = {
"event_type": "negotiation_start",
"session_id": session_id,
"character": character,
"item_id": item_id,
"item_name": item_name,
"original_price": original_price,
"timestamp": datetime.utcnow().isoformat(),
}
cls._negotiation_logs.append(log_entry)
logger.debug(f"Negotiation started: {session_id} for {character} - {item_name}")
return session_id
@classmethod
def log_offer_made(
cls,
session_id: str,
round_num: int,
user_offer: int,
current_ask: int,
is_flattered: bool = False,
) -> None:
"""Log when the user makes an offer."""
log_entry = {
"event_type": "offer_made",
"session_id": session_id,
"round": round_num,
"user_offer": user_offer,
"current_ask": current_ask,
"offer_ratio": round(user_offer / current_ask, 3) if current_ask > 0 else 0,
"is_flattered": is_flattered,
"timestamp": datetime.utcnow().isoformat(),
}
cls._negotiation_logs.append(log_entry)
logger.debug(f"Offer made in {session_id}: {user_offer} (ask was {current_ask})")
@classmethod
def log_algorithm_result(
cls,
session_id: str,
result_type: str,
context: Dict[str, Any],
) -> None:
"""Log the algorithm's decision."""
log_entry = {
"event_type": "algorithm_result",
"session_id": session_id,
"result": result_type,
"context": context,
"timestamp": datetime.utcnow().isoformat(),
}
cls._negotiation_logs.append(log_entry)
logger.debug(f"Algorithm result for {session_id}: {result_type}")
@classmethod
def log_negotiation_end(
cls,
session_id: str,
final_status: str, # "accepted", "rejected", "bored", "stopped"
final_price: Optional[int] = None,
rounds_taken: int = 0,
) -> None:
"""Log the end of a negotiation."""
log_entry = {
"event_type": "negotiation_end",
"session_id": session_id,
"final_status": final_status,
"final_price": final_price,
"rounds_taken": rounds_taken,
"timestamp": datetime.utcnow().isoformat(),
}
cls._negotiation_logs.append(log_entry)
logger.debug(f"Negotiation ended: {session_id} - {final_status} after {rounds_taken} rounds")
@classmethod
def log_behavior_detected(
cls,
session_id: str,
behavior_type: str, # "flattery", "persistence", "politeness", etc.
) -> None:
"""Log when a user behavior is detected."""
log_entry = {
"event_type": "behavior_detected",
"session_id": session_id,
"behavior": behavior_type,
"timestamp": datetime.utcnow().isoformat(),
}
cls._negotiation_logs.append(log_entry)
logger.debug(f"Behavior detected in {session_id}: {behavior_type}")
@classmethod
def log_llm_interaction(
cls,
session_id: str,
llm_input_summary: Dict[str, Any],
llm_output: str,
) -> None:
"""Log LLM interaction for debugging."""
log_entry = {
"event_type": "llm_interaction",
"session_id": session_id,
"llm_prompt_fields": list(llm_input_summary.keys()),
"llm_output_length": len(llm_output),
"timestamp": datetime.utcnow().isoformat(),
}
cls._negotiation_logs.append(log_entry)
logger.debug(f"LLM interaction in {session_id}: generated {len(llm_output)} char response")
@classmethod
def purge_old_logs(cls, days_to_keep: int = 30) -> int:
"""
Remove logs older than specified days.
Returns:
Number of logs removed
"""
cutoff_date = (datetime.utcnow() - timedelta(days=days_to_keep)).isoformat()
initial_count = len(cls._negotiation_logs)
cls._negotiation_logs = [
log for log in cls._negotiation_logs
if log.get("timestamp", "") > cutoff_date
]
removed_count = initial_count - len(cls._negotiation_logs)
if removed_count > 0:
logger.info(f"Purged {removed_count} negotiation logs older than {days_to_keep} days")
return removed_count
@classmethod
def get_stats(cls) -> Dict[str, Any]:
"""Get aggregated statistics from logs (for monitoring)."""
if not cls._negotiation_logs:
return {
"total_logs": 0,
"negotiation_sessions": 0,
}
# Count unique sessions
sessions = set()
accepted_count = 0
rejected_count = 0
flattery_count = 0
for log in cls._negotiation_logs:
if log.get("session_id"):
sessions.add(log["session_id"])
if log.get("event_type") == "negotiation_end":
if log.get("final_status") == "accepted":
accepted_count += 1
elif log.get("final_status") == "rejected":
rejected_count += 1
if log.get("is_flattered"):
flattery_count += 1
return {
"total_logs": len(cls._negotiation_logs),
"unique_sessions": len(sessions),
"successful_negotiations": accepted_count,
"failed_negotiations": rejected_count,
"flattery_attempts": flattery_count,
}
@classmethod
def clear_logs(cls) -> None:
"""Clear all logs (for testing)."""
cls._negotiation_logs = []