Files

437 lines
14 KiB
Python

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""
Harvey State Manager - Track conversation sessions with the invisible rabbit.
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
# State file location
STATE_DIR = Path(__file__).parent.parent / "state"
STATE_FILE = STATE_DIR / "harvey_session.json"
# Default session timeout (2 hours)
SESSION_TIMEOUT_HOURS = 2
MODES = {
"bored": {
"name": "Langeweile-Modus",
"emoji": "🎭",
"response_delay": 0,
"message_length": "medium",
"depth": "medium"
},
"restaurant": {
"name": "Restaurant-Modus",
"emoji": "🍽️",
"response_delay": 45, # seconds
"message_length": "short",
"depth": "light"
},
"waiting": {
"name": "Warte-Modus",
"emoji": "⏳",
"response_delay": 0,
"message_length": "short",
"depth": "light"
},
"walk": {
"name": "Begleiter-Modus",
"emoji": "🚶",
"response_delay": 0,
"message_length": "long",
"depth": "deep"
}
}
def load_state() -> dict:
"""Load current Harvey session state."""
if not STATE_FILE.exists():
return {"active": False}
try:
with open(STATE_FILE) as f:
state = json.load(f)
# Check if session has timed out
if state.get("active") and state.get("last_activity"):
last_activity = datetime.fromisoformat(state["last_activity"])
now = datetime.now(timezone.utc)
hours_inactive = (now - last_activity).total_seconds() / 3600
if hours_inactive > SESSION_TIMEOUT_HOURS:
state["active"] = False
state["timeout"] = True
save_state(state)
return state
except (json.JSONDecodeError, KeyError):
return {"active": False}
def save_state(state: dict) -> None:
"""Save Harvey session state."""
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=2, default=str)
def start_session(mode: str = "bored") -> dict:
"""Start a new Harvey session."""
if mode not in MODES:
mode = "bored"
now = datetime.now(timezone.utc).isoformat()
state = {
"active": True,
"mode": mode,
"mode_info": MODES[mode],
"started_at": now,
"last_activity": now,
"topics_discussed": [],
"user_mentions": {},
"message_count": 0
}
save_state(state)
return state
def end_session(reason: str = "user_ended") -> dict:
"""End the current Harvey session."""
state = load_state()
state["active"] = False
state["ended_at"] = datetime.now(timezone.utc).isoformat()
state["end_reason"] = reason
save_state(state)
return state
def update_session(topic: str = None, mention_key: str = None, mention_value: str = None) -> dict:
"""Update session with new activity."""
state = load_state()
if not state.get("active"):
return state
state["last_activity"] = datetime.now(timezone.utc).isoformat()
state["message_count"] = state.get("message_count", 0) + 1
if topic and topic not in state.get("topics_discussed", []):
state.setdefault("topics_discussed", []).append(topic)
if mention_key and mention_value:
state.setdefault("user_mentions", {})[mention_key] = mention_value
# Track if it's time to offer a game (every 7+ messages if no game offered yet)
msg_count = state.get("message_count", 0)
game_offered = state.get("game_offered", False)
if msg_count >= 7 and not game_offered:
state["suggest_game"] = True
save_state(state)
return state
def mark_game_offered() -> dict:
"""Mark that a game has been offered in this session."""
state = load_state()
if state.get("active"):
state["game_offered"] = True
state["suggest_game"] = False
save_state(state)
return state
def should_suggest_game() -> bool:
"""Check if Harvey should suggest a game."""
state = load_state()
return state.get("suggest_game", False)
# ============================================================================
# Game & Score Tracking
# ============================================================================
DIFFICULTY_LEVELS = ["easy", "medium", "hard"]
def start_game(game_type: str, difficulty: str = "medium") -> dict:
"""Start a new game within the Harvey session."""
state = load_state()
if not state.get("active"):
return {"error": "No active Harvey session"}
if difficulty not in DIFFICULTY_LEVELS:
difficulty = "medium"
state["current_game"] = {
"type": game_type,
"difficulty": difficulty,
"started_at": datetime.now(timezone.utc).isoformat(),
"questions_asked": 0,
"correct_answers": 0,
"hints_used": 0
}
state["game_offered"] = True
state["suggest_game"] = False
save_state(state)
return state["current_game"]
def update_game_score(correct: bool, hint_used: bool = False) -> dict:
"""Update the current game score."""
state = load_state()
if not state.get("active") or not state.get("current_game"):
return {"error": "No active game"}
game = state["current_game"]
game["questions_asked"] = game.get("questions_asked", 0) + 1
if correct:
game["correct_answers"] = game.get("correct_answers", 0) + 1
if hint_used:
game["hints_used"] = game.get("hints_used", 0) + 1
save_state(state)
return game
def end_game() -> dict:
"""End the current game and return final score."""
state = load_state()
if not state.get("active") or not state.get("current_game"):
return {"error": "No active game"}
game = state["current_game"]
game["ended_at"] = datetime.now(timezone.utc).isoformat()
# Calculate score
questions = game.get("questions_asked", 0)
correct = game.get("correct_answers", 0)
hints = game.get("hints_used", 0)
if questions > 0:
base_score = (correct / questions) * 100
hint_penalty = hints * 5
game["final_score"] = max(0, base_score - hint_penalty)
game["accuracy"] = f"{correct}/{questions}"
else:
game["final_score"] = 0
game["accuracy"] = "0/0"
# Save to game history
if "game_history" not in state:
state["game_history"] = []
state["game_history"].append(game)
# Keep only last 10 games
state["game_history"] = state["game_history"][-10:]
# Clear current game
state["current_game"] = None
save_state(state)
return game
def get_game_stats() -> dict:
"""Get overall game statistics."""
state = load_state()
history = state.get("game_history", [])
if not history:
return {
"games_played": 0,
"message": "Noch keine Spiele gespielt! 🎮"
}
total_games = len(history)
total_correct = sum(g.get("correct_answers", 0) for g in history)
total_questions = sum(g.get("questions_asked", 0) for g in history)
avg_score = sum(g.get("final_score", 0) for g in history) / total_games
# Find favorite game type
game_types = {}
for g in history:
gt = g.get("type", "unknown")
game_types[gt] = game_types.get(gt, 0) + 1
favorite = max(game_types, key=game_types.get) if game_types else "none"
return {
"games_played": total_games,
"total_correct": total_correct,
"total_questions": total_questions,
"average_score": round(avg_score, 1),
"favorite_game": favorite,
"message": f"🎮 {total_games} Spiele | ⭐ {avg_score:.0f}% avg | 🏆 Favorit: {favorite}"
}
def adjust_difficulty(direction: str = "auto") -> str:
"""Adjust game difficulty based on performance or manual input."""
state = load_state()
current = state.get("difficulty", "medium")
current_idx = DIFFICULTY_LEVELS.index(current) if current in DIFFICULTY_LEVELS else 1
if direction == "up" and current_idx < 2:
new_difficulty = DIFFICULTY_LEVELS[current_idx + 1]
elif direction == "down" and current_idx > 0:
new_difficulty = DIFFICULTY_LEVELS[current_idx - 1]
elif direction == "auto":
# Auto-adjust based on recent performance
history = state.get("game_history", [])[-5:] # Last 5 games
if len(history) >= 3:
avg_score = sum(g.get("final_score", 0) for g in history) / len(history)
if avg_score > 80 and current_idx < 2:
new_difficulty = DIFFICULTY_LEVELS[current_idx + 1]
elif avg_score < 40 and current_idx > 0:
new_difficulty = DIFFICULTY_LEVELS[current_idx - 1]
else:
new_difficulty = current
else:
new_difficulty = current
else:
new_difficulty = current
state["difficulty"] = new_difficulty
save_state(state)
return new_difficulty
def change_mode(mode: str) -> dict:
"""Change the current session mode."""
state = load_state()
if not state.get("active"):
return start_session(mode)
if mode in MODES:
state["mode"] = mode
state["mode_info"] = MODES[mode]
state["last_activity"] = datetime.now(timezone.utc).isoformat()
save_state(state)
return state
def get_status() -> dict:
"""Get current Harvey status."""
state = load_state()
if not state.get("active"):
return {
"active": False,
"message": "Harvey schläft gerade. Sag 'Hey Harvey' um ihn zu wecken! 🐰💤"
}
mode_info = MODES.get(state.get("mode", "bored"), MODES["bored"])
duration_min = 0
if state.get("started_at"):
started = datetime.fromisoformat(state["started_at"])
now = datetime.now(timezone.utc)
duration_min = int((now - started).total_seconds() / 60)
return {
"active": True,
"mode": state.get("mode"),
"mode_name": mode_info["name"],
"mode_emoji": mode_info["emoji"],
"duration_minutes": duration_min,
"message_count": state.get("message_count", 0),
"topics": state.get("topics_discussed", []),
"message": f"Harvey ist wach {mode_info['emoji']} | {mode_info['name']} | {duration_min} Min | {state.get('message_count', 0)} Nachrichten"
}
def main():
parser = argparse.ArgumentParser(description="Harvey Session Manager")
parser.add_argument("action", choices=[
"start", "end", "status", "update", "mode",
"game-start", "game-score", "game-end", "game-stats", "difficulty"
], help="Action to perform")
parser.add_argument("--mode", "-m", choices=list(MODES.keys()), default="bored",
help="Session mode")
parser.add_argument("--topic", "-t", help="Add a discussed topic")
parser.add_argument("--mention-key", help="Key for user mention")
parser.add_argument("--mention-value", help="Value for user mention")
parser.add_argument("--reason", default="user_ended", help="Reason for ending session")
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
# Game arguments
parser.add_argument("--game-type", "-g", help="Type of game (trivia, riddle, what_if, etc.)")
parser.add_argument("--difficulty", "-d", choices=DIFFICULTY_LEVELS, default="medium")
parser.add_argument("--correct", action="store_true", help="Mark answer as correct")
parser.add_argument("--hint", action="store_true", help="Mark that hint was used")
parser.add_argument("--direction", choices=["up", "down", "auto"], default="auto",
help="Difficulty adjustment direction")
args = parser.parse_args()
if args.action == "start":
result = start_session(args.mode)
msg = f"🐰 Harvey ist wach! Modus: {MODES[args.mode]['name']} {MODES[args.mode]['emoji']}"
elif args.action == "end":
result = end_session(args.reason)
msg = "🐰💤 Harvey schläft jetzt. Bis später!"
elif args.action == "status":
result = get_status()
msg = result.get("message", "")
elif args.action == "update":
result = update_session(args.topic, args.mention_key, args.mention_value)
msg = "Session aktualisiert"
elif args.action == "mode":
result = change_mode(args.mode)
msg = f"Modus gewechselt zu: {MODES[args.mode]['name']} {MODES[args.mode]['emoji']}"
# Game actions
elif args.action == "game-start":
game_type = args.game_type or "trivia"
result = start_game(game_type, args.difficulty)
msg = f"🎮 Spiel gestartet: {game_type} ({args.difficulty})"
elif args.action == "game-score":
result = update_game_score(args.correct, args.hint)
correct = result.get("correct_answers", 0)
total = result.get("questions_asked", 0)
msg = f"📊 Score: {correct}/{total}"
elif args.action == "game-end":
result = end_game()
if "error" not in result:
msg = f"🏁 Spiel beendet! Score: {result.get('final_score', 0):.0f}% ({result.get('accuracy', '?')})"
else:
msg = result["error"]
elif args.action == "game-stats":
result = get_game_stats()
msg = result.get("message", "")
elif args.action == "difficulty":
result = adjust_difficulty(args.direction)
msg = f"🎚️ Schwierigkeit: {result}"
else:
result = {"error": "Unknown action"}
msg = "Unbekannte Aktion"
if args.json:
print(json.dumps(result, indent=2, default=str))
else:
print(msg)
if args.action == "status" and result.get("active"):
print(f" Topics: {', '.join(result.get('topics', [])) or 'noch keine'}")
if __name__ == "__main__":
main()