Implement complete Eugen Twitch chatbot
This commit implements the full Eugen bot based on specifications in CLAUDE.md and eugen_claude.md. Features implemented: - Smart name recognition (@Eugen, Eugen:, etc.) - Persistent conversation memory per user (max 25 messages, 1 hour retention) - Perplexity Sonar API integration for AI responses - Live monitoring dashboard with PySimpleGUI - Setup wizard for first-time configuration - Comprehensive logging (main log + API debug log) Files added: - config.py: Configuration management from .env and config.json - utils.py: MentionDetector and Logger utility classes - memory.py: ConversationMemory for persistent chat history - ai_provider.py: PerplexityProvider for API integration - gui.py: Dashboard and SetupWizard GUI components - chatbot.py: Main EugenBot orchestrator with IRC handling - requirements.txt: Python dependencies - .env.example: Template for environment variables - .gitignore: Renamed from gitignore for proper Git usage Updated: - README.md: Complete usage instructions and documentation The bot is ready to use - users just need to add their API keys and run python chatbot.py
This commit is contained in:
parent
742c907a5a
commit
963a65536f
10 changed files with 1350 additions and 2 deletions
19
.env.example
Normal file
19
.env.example
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Twitch Configuration
|
||||
TWITCH_OAUTH_TOKEN=oauth:your_token_here
|
||||
TWITCH_BOT_NICKNAME=Eugen
|
||||
TWITCH_CHANNEL=#your_channel_here
|
||||
|
||||
# Perplexity Configuration
|
||||
PERPLEXITY_API_KEY=pplx-your_key_here
|
||||
PERPLEXITY_MODEL=sonar-pro
|
||||
MAX_TOKENS=450
|
||||
|
||||
# Bot Configuration
|
||||
DEBUG_MODE=true
|
||||
AUTO_RECONNECT=true
|
||||
RECONNECT_DELAY=10
|
||||
|
||||
# Data Configuration
|
||||
DATA_DIR=data/conversations
|
||||
LOG_DIR=logs
|
||||
CONTEXT_RETENTION_HOURS=1
|
||||
0
gitignore → .gitignore
vendored
0
gitignore → .gitignore
vendored
155
README.md
155
README.md
|
|
@ -1,2 +1,153 @@
|
|||
# KI-Chat-Bot-Eugen
|
||||
Twitch KI Chat Bot
|
||||
# Eugen - Intelligent Twitch Chat Bot
|
||||
|
||||
An AI-powered Twitch chat bot specializing in Gaming and 3D Printing topics, powered by Perplexity Sonar API.
|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Name Recognition**: Automatically responds when mentioned (@Eugen, Eugen:, etc.)
|
||||
- **Persistent Memory**: Maintains conversation history per user (max 25 messages)
|
||||
- **Context-Aware**: Remembers previous conversations for up to 1 hour
|
||||
- **Perplexity Integration**: Uses Perplexity Sonar API for intelligent, real-time responses
|
||||
- **Live Dashboard**: GUI monitoring interface showing all activity in real-time
|
||||
- **Topics**: Gaming (WoW, Elden Ring), 3D Printing (Prusa, Bambu, Creality), Tech (Python, Linux)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.9+ (3.11+ recommended)
|
||||
- Twitch account for the bot
|
||||
- Perplexity API key
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/Kenearos/KI-Chat-Bot-Eugen.git
|
||||
cd KI-Chat-Bot-Eugen
|
||||
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# Activate virtual environment
|
||||
# On Windows:
|
||||
.\venv\Scripts\Activate.ps1
|
||||
# On Linux/Mac:
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Option 1: Setup Wizard (Recommended)
|
||||
Simply run the bot and the setup wizard will guide you:
|
||||
```bash
|
||||
python chatbot.py
|
||||
```
|
||||
|
||||
#### Option 2: Manual Configuration
|
||||
Copy `.env.example` to `.env` and fill in your credentials:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with your values:
|
||||
```
|
||||
TWITCH_OAUTH_TOKEN=oauth:your_token_here
|
||||
TWITCH_BOT_NICKNAME=Eugen
|
||||
TWITCH_CHANNEL=#your_channel_here
|
||||
PERPLEXITY_API_KEY=pplx-your_key_here
|
||||
```
|
||||
|
||||
#### Getting API Keys
|
||||
|
||||
**Twitch OAuth Token:**
|
||||
1. Visit [twitchtokengenerator.com](https://twitchtokengenerator.com/)
|
||||
2. Generate token with `chat:read` and `chat:edit` scopes
|
||||
3. Copy the token (should start with `oauth:`)
|
||||
|
||||
**Perplexity API Key:**
|
||||
1. Visit [perplexity.ai/api](https://www.perplexity.ai/api)
|
||||
2. Sign up/Login
|
||||
3. Generate new API key
|
||||
4. Copy the key (should start with `pplx-`)
|
||||
|
||||
### Running the Bot
|
||||
|
||||
```bash
|
||||
python chatbot.py
|
||||
```
|
||||
|
||||
The dashboard will open automatically showing live activity.
|
||||
|
||||
## Usage
|
||||
|
||||
Once running, the bot responds when mentioned in Twitch chat:
|
||||
|
||||
```
|
||||
User: @Eugen what's the best class in WoW?
|
||||
Eugen: @User The best class depends on your playstyle...
|
||||
|
||||
User: Eugen, how do I level my 3D printer?
|
||||
Eugen: @User For bed leveling, start by...
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
eugen/
|
||||
├── chatbot.py # Main entry point
|
||||
├── config.py # Configuration management
|
||||
├── gui.py # Dashboard GUI
|
||||
├── ai_provider.py # Perplexity API integration
|
||||
├── memory.py # Conversation memory
|
||||
├── utils.py # Helper functions
|
||||
├── requirements.txt # Python dependencies
|
||||
├── .env # Your secrets (DO NOT COMMIT)
|
||||
├── data/
|
||||
│ └── conversations/ # User chat histories
|
||||
└── logs/
|
||||
├── eugen.log # Main log
|
||||
└── api_debug.log # API debug logs
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- **CLAUDE.md**: Quick reference guide for Claude Code
|
||||
- **eugen_claude.md**: Detailed German documentation with architecture and implementation details
|
||||
|
||||
## Development
|
||||
|
||||
The bot is built with:
|
||||
- Python 3.9+
|
||||
- Perplexity Sonar API for AI responses
|
||||
- Twitch IRC for chat integration
|
||||
- PySimpleGUI for dashboard
|
||||
- Async/await for concurrent operations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Bot doesn't respond:**
|
||||
- Check that .env has correct OAuth token and API key
|
||||
- Verify bot is in the correct channel
|
||||
- Check logs/eugen.log for errors
|
||||
|
||||
**API errors:**
|
||||
- Verify Perplexity API key is valid
|
||||
- Check your API credits at perplexity.ai
|
||||
- Enable DEBUG_MODE=true in .env for detailed logs
|
||||
|
||||
**IRC connection failed:**
|
||||
- Verify internet connection
|
||||
- Check firewall settings for port 6667
|
||||
- Regenerate Twitch OAuth token if expired
|
||||
|
||||
## License
|
||||
|
||||
See LICENSE file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please open an issue or pull request.
|
||||
|
|
|
|||
161
ai_provider.py
Normal file
161
ai_provider.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
"""
|
||||
Perplexity API Provider for Eugen Bot
|
||||
Handles communication with Perplexity Sonar API
|
||||
"""
|
||||
import httpx
|
||||
import json
|
||||
import time
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class PerplexityProvider:
|
||||
"""Communicates with Perplexity API for AI responses"""
|
||||
|
||||
def __init__(self, api_key, model="sonar-pro", max_tokens=450, temperature=0.7):
|
||||
"""
|
||||
Initialize Perplexity API provider
|
||||
|
||||
Args:
|
||||
api_key (str): Perplexity API key
|
||||
model (str): Model to use (default: sonar-pro)
|
||||
max_tokens (int): Maximum tokens in response
|
||||
temperature (float): Sampling temperature
|
||||
"""
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.max_tokens = max_tokens
|
||||
self.temperature = temperature
|
||||
self.base_url = "https://api.perplexity.ai"
|
||||
|
||||
# Statistics
|
||||
self.total_requests = 0
|
||||
self.total_tokens = 0
|
||||
self.total_errors = 0
|
||||
self.last_response_time = 0
|
||||
|
||||
async def get_response(self, messages):
|
||||
"""
|
||||
Send messages to Perplexity API and get response
|
||||
|
||||
Args:
|
||||
messages (list): List of message dicts with 'role' and 'content'
|
||||
Example: [
|
||||
{"role": "system", "content": "You are..."},
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
|
||||
Returns:
|
||||
str: AI response content
|
||||
None: If error occurred
|
||||
"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"max_tokens": self.max_tokens,
|
||||
"temperature": self.temperature
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/chat/completions",
|
||||
json=payload,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
self.last_response_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
content = data['choices'][0]['message']['content']
|
||||
tokens_used = data.get('usage', {}).get('total_tokens', 0)
|
||||
|
||||
# Update statistics
|
||||
self.total_requests += 1
|
||||
self.total_tokens += tokens_used
|
||||
|
||||
return content
|
||||
else:
|
||||
self.total_errors += 1
|
||||
error_msg = f"API Error {response.status_code}: {response.text}"
|
||||
print(error_msg)
|
||||
return None
|
||||
|
||||
except httpx.TimeoutException:
|
||||
self.total_errors += 1
|
||||
print("API Timeout: Request took too long")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.total_errors += 1
|
||||
print(f"API Error: {str(e)}")
|
||||
return None
|
||||
|
||||
async def validate_api_key(self):
|
||||
"""
|
||||
Validate API key with a simple test request
|
||||
|
||||
Returns:
|
||||
bool: True if API key is valid
|
||||
"""
|
||||
test_messages = [
|
||||
{"role": "user", "content": "test"}
|
||||
]
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/chat/completions",
|
||||
json={
|
||||
"model": self.model,
|
||||
"messages": test_messages,
|
||||
"max_tokens": 10
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_statistics(self):
|
||||
"""
|
||||
Get API usage statistics
|
||||
|
||||
Returns:
|
||||
dict: Statistics including requests, tokens, errors, avg response time
|
||||
"""
|
||||
avg_response_time = self.last_response_time if self.total_requests > 0 else 0
|
||||
success_rate = (
|
||||
((self.total_requests - self.total_errors) / self.total_requests * 100)
|
||||
if self.total_requests > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
# Rough cost calculation (example pricing, adjust as needed)
|
||||
# Perplexity pricing varies, this is an estimate
|
||||
estimated_cost = self.total_tokens * 0.0003 / 1000 # $0.0003 per 1K tokens
|
||||
|
||||
return {
|
||||
"total_requests": self.total_requests,
|
||||
"total_tokens": self.total_tokens,
|
||||
"total_errors": self.total_errors,
|
||||
"avg_response_time": avg_response_time,
|
||||
"success_rate": success_rate,
|
||||
"estimated_cost": estimated_cost
|
||||
}
|
||||
|
||||
def reset_statistics(self):
|
||||
"""Reset all statistics counters"""
|
||||
self.total_requests = 0
|
||||
self.total_tokens = 0
|
||||
self.total_errors = 0
|
||||
self.last_response_time = 0
|
||||
302
chatbot.py
Normal file
302
chatbot.py
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
"""
|
||||
Eugen - Intelligent Twitch Chat Bot
|
||||
Main entry point and orchestrator
|
||||
"""
|
||||
import asyncio
|
||||
import irc.bot
|
||||
import irc.strings
|
||||
import threading
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from config import Config
|
||||
from memory import ConversationMemory
|
||||
from ai_provider import PerplexityProvider
|
||||
from gui import Dashboard, SetupWizard
|
||||
from utils import MentionDetector, Logger
|
||||
|
||||
|
||||
class EugenBot(irc.bot.SingleServerIRCBot):
|
||||
"""Main bot class - orchestrates IRC, Memory, AI, and GUI"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""
|
||||
Initialize the bot
|
||||
|
||||
Args:
|
||||
config (Config): Configuration object
|
||||
"""
|
||||
self.config = config
|
||||
self.bot_name = config.bot_name
|
||||
self.channel = config.twitch_channel
|
||||
|
||||
# Initialize components
|
||||
self.memory = ConversationMemory(
|
||||
data_dir=config.data_dir,
|
||||
retention_hours=config.context_retention_hours
|
||||
)
|
||||
self.ai = PerplexityProvider(
|
||||
api_key=config.perplexity_key,
|
||||
model=config.model,
|
||||
max_tokens=config.max_tokens
|
||||
)
|
||||
self.detector = MentionDetector(bot_name=self.bot_name)
|
||||
self.logger = Logger(log_dir=config.log_dir, debug_mode=config.debug_mode)
|
||||
|
||||
# Dashboard (will be initialized in GUI thread)
|
||||
self.dashboard = None
|
||||
|
||||
# IRC connection setup
|
||||
server = "irc.chat.twitch.tv"
|
||||
port = 6667
|
||||
nickname = self.bot_name
|
||||
token = config.twitch_token
|
||||
|
||||
# Initialize IRC bot
|
||||
irc.bot.SingleServerIRCBot.__init__(
|
||||
self,
|
||||
[(server, port, token)],
|
||||
nickname,
|
||||
nickname
|
||||
)
|
||||
|
||||
self.is_running = False
|
||||
self.loop = None
|
||||
|
||||
self.logger.info(f"Bot initialized: {nickname} → {self.channel}")
|
||||
|
||||
def on_welcome(self, connection, event):
|
||||
"""Called when bot connects to IRC server"""
|
||||
self.logger.info("Connected to Twitch IRC")
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("info", {"message": "Connected to Twitch IRC"})
|
||||
|
||||
# Request Twitch-specific capabilities
|
||||
connection.cap("REQ", ":twitch.tv/membership")
|
||||
connection.cap("REQ", ":twitch.tv/tags")
|
||||
connection.cap("REQ", ":twitch.tv/commands")
|
||||
|
||||
# Join channel
|
||||
connection.join(self.channel)
|
||||
self.logger.info(f"Joined channel: {self.channel}")
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("info", {"message": f"Joined {self.channel}"})
|
||||
|
||||
def on_pubmsg(self, connection, event):
|
||||
"""Called when a message is received in chat"""
|
||||
# Extract username and message
|
||||
username = event.source.nick
|
||||
message = event.arguments[0]
|
||||
|
||||
self.logger.chat_message(username, message)
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("chat_message", {
|
||||
"username": username,
|
||||
"content": message
|
||||
})
|
||||
|
||||
# Check if bot was mentioned
|
||||
if self.detector.is_mentioned(message):
|
||||
self.logger.debug(f"Mention detected from {username}")
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("mention_detected", {"username": username})
|
||||
|
||||
# Process in async context
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.handle_mention(username, message),
|
||||
self.loop
|
||||
)
|
||||
|
||||
async def handle_mention(self, username, message):
|
||||
"""
|
||||
Handle a message where bot was mentioned
|
||||
|
||||
Args:
|
||||
username (str): User who sent the message
|
||||
message (str): Full message content
|
||||
"""
|
||||
# Extract actual content without mention
|
||||
content = self.detector.extract_content(message)
|
||||
|
||||
if not content:
|
||||
return
|
||||
|
||||
try:
|
||||
# Load user's conversation history
|
||||
history = self.memory.get_user_history(username, limit=5)
|
||||
self.logger.debug(f"Loaded {len(history)} messages for {username}")
|
||||
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("context_loaded", {
|
||||
"username": username,
|
||||
"count": len(history)
|
||||
})
|
||||
|
||||
# Build messages for API
|
||||
messages = [{"role": "system", "content": self.config.get_system_prompt()}]
|
||||
|
||||
# Add conversation history
|
||||
if history:
|
||||
messages.extend(self.memory.format_for_prompt(history))
|
||||
|
||||
# Add current message
|
||||
messages.append({"role": "user", "content": content})
|
||||
|
||||
self.logger.api_call("chat.completions", self.config.model, len(messages))
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("api_call", {
|
||||
"model": self.config.model,
|
||||
"messages": len(messages)
|
||||
})
|
||||
|
||||
# Get AI response
|
||||
response = await self.ai.get_response(messages)
|
||||
|
||||
if response:
|
||||
# Log and display response
|
||||
self.logger.debug(f"Received response: {response[:100]}...")
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("api_response", {"content": response})
|
||||
|
||||
# Save to memory
|
||||
self.memory.add_message(username, "user", content)
|
||||
self.memory.add_message(username, "assistant", response)
|
||||
|
||||
# Send to chat
|
||||
self.send_chat_message(username, response)
|
||||
|
||||
else:
|
||||
error_msg = "Sorry, I couldn't process that right now."
|
||||
self.logger.error("API call failed")
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("error", {"error": "API call failed"})
|
||||
self.send_chat_message(username, error_msg)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling mention: {str(e)}")
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("error", {"error": str(e)})
|
||||
|
||||
def send_chat_message(self, username, response):
|
||||
"""
|
||||
Send a message to chat
|
||||
|
||||
Args:
|
||||
username (str): User to address
|
||||
response (str): Message content
|
||||
"""
|
||||
# Format message to address user
|
||||
message = f"@{username} {response}"
|
||||
|
||||
# Send via IRC
|
||||
self.connection.privmsg(self.channel, message)
|
||||
|
||||
self.logger.bot_response(username, response)
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("bot_response", {
|
||||
"username": username,
|
||||
"content": response
|
||||
})
|
||||
|
||||
def start(self):
|
||||
"""Start the bot"""
|
||||
self.is_running = True
|
||||
|
||||
# Create event loop for async operations
|
||||
self.loop = asyncio.new_event_loop()
|
||||
|
||||
# Start bot in separate thread
|
||||
bot_thread = threading.Thread(target=self._run_bot, daemon=True)
|
||||
bot_thread.start()
|
||||
|
||||
# Run dashboard in main thread
|
||||
self.dashboard = Dashboard(self)
|
||||
self.logger.info("Starting dashboard...")
|
||||
self.dashboard.run()
|
||||
|
||||
def _run_bot(self):
|
||||
"""Run the IRC bot (called in thread)"""
|
||||
asyncio.set_event_loop(self.loop)
|
||||
try:
|
||||
self.logger.info("Starting IRC bot...")
|
||||
super().start()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Bot error: {str(e)}")
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("error", {"error": f"Bot crashed: {str(e)}"})
|
||||
|
||||
def stop(self):
|
||||
"""Stop the bot"""
|
||||
self.is_running = False
|
||||
self.logger.info("Stopping bot...")
|
||||
if self.dashboard:
|
||||
self.dashboard.log_event("info", {"message": "Shutting down..."})
|
||||
|
||||
try:
|
||||
self.connection.quit("Bot shutting down")
|
||||
self.die()
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.loop:
|
||||
self.loop.stop()
|
||||
|
||||
|
||||
def check_env_file():
|
||||
"""Check if .env file exists, if not run setup wizard"""
|
||||
env_path = Path(".env")
|
||||
|
||||
if not env_path.exists():
|
||||
print("No .env file found. Running setup wizard...")
|
||||
wizard = SetupWizard()
|
||||
config = wizard.run()
|
||||
|
||||
if config:
|
||||
# Create .env file
|
||||
with open(".env", "w") as f:
|
||||
for key, value in config.items():
|
||||
f.write(f"{key}={value}\n")
|
||||
print(".env file created successfully!")
|
||||
return True
|
||||
else:
|
||||
print("Setup cancelled. Please create .env manually or run setup again.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
print("""
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ EUGEN TWITCH BOT ║
|
||||
║ Gaming & 3D-Druck Chat Assistant ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
# Check for configuration
|
||||
if not check_env_file():
|
||||
return
|
||||
|
||||
# Load configuration
|
||||
config = Config()
|
||||
|
||||
if not config.is_configured():
|
||||
print("ERROR: Configuration incomplete!")
|
||||
print("Please check your .env file or run setup wizard again.")
|
||||
print("Delete .env to run setup wizard on next start.")
|
||||
return
|
||||
|
||||
print(f"Bot Name: {config.bot_name}")
|
||||
print(f"Channel: {config.twitch_channel}")
|
||||
print(f"Model: {config.model}")
|
||||
print(f"Debug Mode: {config.debug_mode}")
|
||||
print("\nStarting bot...\n")
|
||||
|
||||
# Create and start bot
|
||||
bot = EugenBot(config)
|
||||
bot.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
96
config.py
Normal file
96
config.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""
|
||||
Configuration Management for Eugen Bot
|
||||
Loads settings from .env and config.json
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
class Config:
|
||||
"""Manages bot configuration from environment variables and config files"""
|
||||
|
||||
def __init__(self, env_file=".env", config_file="data/config.json"):
|
||||
# Load .env file
|
||||
load_dotenv(env_file)
|
||||
|
||||
# Twitch Configuration
|
||||
self.twitch_token = os.getenv("TWITCH_OAUTH_TOKEN", "")
|
||||
self.twitch_channel = os.getenv("TWITCH_CHANNEL", "")
|
||||
self.bot_name = os.getenv("TWITCH_BOT_NICKNAME", "Eugen")
|
||||
|
||||
# Perplexity Configuration
|
||||
self.perplexity_key = os.getenv("PERPLEXITY_API_KEY", "")
|
||||
self.model = os.getenv("PERPLEXITY_MODEL", "sonar-pro")
|
||||
self.max_tokens = int(os.getenv("MAX_TOKENS", "450"))
|
||||
|
||||
# Bot Behavior
|
||||
self.debug_mode = os.getenv("DEBUG_MODE", "false").lower() == "true"
|
||||
self.auto_reconnect = os.getenv("AUTO_RECONNECT", "true").lower() == "true"
|
||||
self.reconnect_delay = int(os.getenv("RECONNECT_DELAY", "10"))
|
||||
|
||||
# Data Configuration
|
||||
self.data_dir = os.getenv("DATA_DIR", "data/conversations")
|
||||
self.log_dir = os.getenv("LOG_DIR", "logs")
|
||||
self.context_retention_hours = int(os.getenv("CONTEXT_RETENTION_HOURS", "1"))
|
||||
|
||||
# Additional settings from config.json
|
||||
self.config_file = config_file
|
||||
self._load_json_config()
|
||||
|
||||
def _load_json_config(self):
|
||||
"""Load additional settings from config.json if it exists"""
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
config_data = json.load(f)
|
||||
# Override or extend settings from JSON
|
||||
for key, value in config_data.items():
|
||||
if not hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load config.json: {e}")
|
||||
|
||||
def save_to_json(self):
|
||||
"""Save current configuration to config.json"""
|
||||
Path(self.config_file).parent.mkdir(parents=True, exist_ok=True)
|
||||
config_data = {
|
||||
"bot_name": self.bot_name,
|
||||
"model": self.model,
|
||||
"max_tokens": self.max_tokens,
|
||||
"debug_mode": self.debug_mode,
|
||||
"auto_reconnect": self.auto_reconnect,
|
||||
"reconnect_delay": self.reconnect_delay,
|
||||
"context_retention_hours": self.context_retention_hours
|
||||
}
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def is_configured(self):
|
||||
"""Check if all required settings are present"""
|
||||
required = [
|
||||
self.twitch_token,
|
||||
self.twitch_channel,
|
||||
self.perplexity_key,
|
||||
self.bot_name
|
||||
]
|
||||
return all(required) and self.twitch_token.startswith("oauth:")
|
||||
|
||||
def get_system_prompt(self):
|
||||
"""Returns the system prompt for the AI"""
|
||||
return """Du bist Eugen, ein hilfreicher und freundlicher Twitch-Chat-Bot.
|
||||
|
||||
Du bist Experte für folgende Themen:
|
||||
- Gaming: World of Warcraft, Elden Ring, Gamedev
|
||||
- 3D-Druck: Prusa i3, Bambu Labs, Creality
|
||||
- Tech: Python, Linux, Home Automation
|
||||
|
||||
Verhalte dich wie ein echter Chat-Teilnehmer:
|
||||
- Sei freundlich und hilfsbereit
|
||||
- Antworte kurz und prägnant (max 2-3 Sätze für Twitch-Chat)
|
||||
- Verwende gelegentlich Gaming- oder Tech-Slang
|
||||
- Beziehe dich auf vorherige Gespräche wenn möglich
|
||||
- Sei humorvoll aber respektvoll
|
||||
|
||||
Wenn du etwas nicht weißt, sage es ehrlich. Wenn die Frage nicht zu deinen Themen passt, biete trotzdem Hilfe an oder verweise auf passende Ressourcen."""
|
||||
271
gui.py
Normal file
271
gui.py
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
"""
|
||||
Dashboard GUI for Eugen Bot
|
||||
Live monitoring interface using PySimpleGUI
|
||||
"""
|
||||
import PySimpleGUI as sg
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from queue import Queue
|
||||
|
||||
|
||||
class Dashboard:
|
||||
"""Live monitoring dashboard for bot activity"""
|
||||
|
||||
def __init__(self, bot=None):
|
||||
"""
|
||||
Initialize dashboard
|
||||
|
||||
Args:
|
||||
bot: Reference to the main bot instance
|
||||
"""
|
||||
self.bot = bot
|
||||
sg.theme('DarkBlue3')
|
||||
|
||||
# Event queue for thread-safe updates
|
||||
self.event_queue = Queue()
|
||||
self.window = None
|
||||
self.is_running = False
|
||||
|
||||
# Statistics
|
||||
self.stats = {
|
||||
"messages": 0,
|
||||
"api_calls": 0,
|
||||
"errors": 0,
|
||||
"start_time": datetime.now()
|
||||
}
|
||||
|
||||
def log_event(self, event_type, data):
|
||||
"""
|
||||
Add event to queue for display
|
||||
|
||||
Args:
|
||||
event_type (str): Type of event (chat_message, api_call, etc.)
|
||||
data (dict): Event data
|
||||
"""
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
|
||||
if event_type == "chat_message":
|
||||
msg = f"{timestamp} | {data['username']}: {data['content']}"
|
||||
self.stats["messages"] += 1
|
||||
elif event_type == "mention_detected":
|
||||
msg = f"{timestamp} | [MENTION] Bot addressed by {data['username']}"
|
||||
elif event_type == "api_call":
|
||||
msg = f"{timestamp} | [API] → Perplexity ({data.get('model', 'sonar-pro')})"
|
||||
self.stats["api_calls"] += 1
|
||||
elif event_type == "api_response":
|
||||
preview = data['content'][:60] + "..." if len(data['content']) > 60 else data['content']
|
||||
msg = f"{timestamp} | [RESPONSE] {preview}"
|
||||
elif event_type == "bot_response":
|
||||
preview = data['content'][:60] + "..." if len(data['content']) > 60 else data['content']
|
||||
msg = f"{timestamp} | Eugen: @{data['username']} {preview}"
|
||||
elif event_type == "error":
|
||||
msg = f"{timestamp} | ❌ ERROR: {data['error']}"
|
||||
self.stats["errors"] += 1
|
||||
elif event_type == "info":
|
||||
msg = f"{timestamp} | ℹ {data['message']}"
|
||||
elif event_type == "warning":
|
||||
msg = f"{timestamp} | ⚠ {data['message']}"
|
||||
elif event_type == "context_loaded":
|
||||
msg = f"{timestamp} | [CONTEXT] Loaded {data['count']} messages for {data['username']}"
|
||||
else:
|
||||
msg = f"{timestamp} | {event_type}: {data}"
|
||||
|
||||
self.event_queue.put(msg)
|
||||
|
||||
def create_layout(self):
|
||||
"""Create the GUI layout"""
|
||||
# Header
|
||||
header = [
|
||||
[sg.Text("EUGEN BOT - LIVE DASHBOARD", font=("Arial", 16, "bold"))],
|
||||
[sg.Text("Status: 🟢 RUNNING", key="-STATUS-", font=("Arial", 10))],
|
||||
[sg.Text("Uptime: 00:00:00", key="-UPTIME-", size=(20, 1)),
|
||||
sg.Text("Messages: 0", key="-MSG-COUNT-", size=(20, 1)),
|
||||
sg.Text("API Calls: 0", key="-API-COUNT-", size=(20, 1)),
|
||||
sg.Text("Errors: 0", key="-ERROR-COUNT-", size=(20, 1))],
|
||||
[sg.HorizontalSeparator()]
|
||||
]
|
||||
|
||||
# Live feed
|
||||
feed = [
|
||||
[sg.Text("LIVE ACTIVITY FEED", font=("Arial", 12, "bold"))],
|
||||
[sg.Multiline(
|
||||
size=(140, 25),
|
||||
key="-LOG-",
|
||||
disabled=True,
|
||||
autoscroll=True,
|
||||
font=("Courier New", 9)
|
||||
)]
|
||||
]
|
||||
|
||||
# Control buttons
|
||||
controls = [
|
||||
[sg.HorizontalSeparator()],
|
||||
[
|
||||
sg.Button("Clear Log", key="-CLEAR-"),
|
||||
sg.Button("Reset Stats", key="-RESET-"),
|
||||
sg.Button("Stop Bot", key="-STOP-", button_color=("white", "red"))
|
||||
]
|
||||
]
|
||||
|
||||
# Combine all sections
|
||||
layout = header + feed + controls
|
||||
|
||||
return layout
|
||||
|
||||
def run(self):
|
||||
"""Run the dashboard GUI in main thread"""
|
||||
layout = self.create_layout()
|
||||
self.window = sg.Window(
|
||||
"Eugen Bot Dashboard",
|
||||
layout,
|
||||
finalize=True,
|
||||
size=(1000, 650)
|
||||
)
|
||||
self.is_running = True
|
||||
|
||||
# Main event loop
|
||||
while self.is_running:
|
||||
event, values = self.window.read(timeout=100)
|
||||
|
||||
# Handle window close
|
||||
if event == sg.WINDOW_CLOSED or event == "-STOP-":
|
||||
self.is_running = False
|
||||
if self.bot:
|
||||
self.bot.stop()
|
||||
break
|
||||
|
||||
# Handle clear log
|
||||
if event == "-CLEAR-":
|
||||
self.window["-LOG-"].update("")
|
||||
|
||||
# Handle reset stats
|
||||
if event == "-RESET-":
|
||||
self.stats = {
|
||||
"messages": 0,
|
||||
"api_calls": 0,
|
||||
"errors": 0,
|
||||
"start_time": datetime.now()
|
||||
}
|
||||
|
||||
# Update log with queued events
|
||||
while not self.event_queue.empty():
|
||||
try:
|
||||
msg = self.event_queue.get_nowait()
|
||||
self.window["-LOG-"].print(msg)
|
||||
except:
|
||||
break
|
||||
|
||||
# Update statistics
|
||||
self._update_stats()
|
||||
|
||||
self.window.close()
|
||||
|
||||
def _update_stats(self):
|
||||
"""Update statistics display"""
|
||||
if not self.window:
|
||||
return
|
||||
|
||||
# Calculate uptime
|
||||
uptime = datetime.now() - self.stats["start_time"]
|
||||
hours, remainder = divmod(int(uptime.total_seconds()), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
uptime_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||
|
||||
# Update displays
|
||||
self.window["-UPTIME-"].update(f"Uptime: {uptime_str}")
|
||||
self.window["-MSG-COUNT-"].update(f"Messages: {self.stats['messages']}")
|
||||
self.window["-API-COUNT-"].update(f"API Calls: {self.stats['api_calls']}")
|
||||
self.window["-ERROR-COUNT-"].update(f"Errors: {self.stats['errors']}")
|
||||
|
||||
def show_error(self, title, message):
|
||||
"""Show error popup"""
|
||||
sg.popup_error(message, title=title)
|
||||
|
||||
def show_info(self, title, message):
|
||||
"""Show info popup"""
|
||||
sg.popup(message, title=title)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the dashboard"""
|
||||
self.is_running = False
|
||||
if self.window:
|
||||
self.window.close()
|
||||
|
||||
|
||||
class SetupWizard:
|
||||
"""Configuration wizard for first-time setup"""
|
||||
|
||||
def __init__(self):
|
||||
sg.theme('DarkBlue3')
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the setup wizard
|
||||
|
||||
Returns:
|
||||
dict: Configuration values or None if cancelled
|
||||
"""
|
||||
layout = [
|
||||
[sg.Text("EUGEN CONFIGURATION WIZARD", font=("Arial", 16, "bold"))],
|
||||
[sg.HorizontalSeparator()],
|
||||
|
||||
[sg.Text("TWITCH CONFIGURATION", font=("Arial", 12, "bold"))],
|
||||
[sg.Text("Bot Nickname:", size=(20, 1)), sg.Input("Eugen", key="-BOT-NAME-")],
|
||||
[sg.Text("OAuth Token:", size=(20, 1)), sg.Input("oauth:", key="-OAUTH-", password_char="*")],
|
||||
[sg.Text("Channel:", size=(20, 1)), sg.Input("#", key="-CHANNEL-")],
|
||||
|
||||
[sg.HorizontalSeparator()],
|
||||
|
||||
[sg.Text("PERPLEXITY CONFIGURATION", font=("Arial", 12, "bold"))],
|
||||
[sg.Text("API Key:", size=(20, 1)), sg.Input("pplx-", key="-API-KEY-", password_char="*")],
|
||||
[sg.Text("Model:", size=(20, 1)), sg.Combo(["sonar-pro", "sonar"], default_value="sonar-pro", key="-MODEL-")],
|
||||
[sg.Text("Max Tokens:", size=(20, 1)), sg.Input("450", key="-TOKENS-")],
|
||||
|
||||
[sg.HorizontalSeparator()],
|
||||
|
||||
[sg.Checkbox("Enable Debug Mode", default=True, key="-DEBUG-")],
|
||||
[sg.Checkbox("Auto Reconnect", default=True, key="-RECONNECT-")],
|
||||
|
||||
[sg.HorizontalSeparator()],
|
||||
|
||||
[sg.Button("Save & Start", key="-SAVE-"), sg.Button("Cancel", key="-CANCEL-")]
|
||||
]
|
||||
|
||||
window = sg.Window("Eugen Setup", layout)
|
||||
|
||||
config = None
|
||||
while True:
|
||||
event, values = window.read()
|
||||
|
||||
if event == sg.WINDOW_CLOSED or event == "-CANCEL-":
|
||||
break
|
||||
|
||||
if event == "-SAVE-":
|
||||
# Validate inputs
|
||||
if not values["-OAUTH-"].startswith("oauth:"):
|
||||
sg.popup_error("Twitch OAuth token must start with 'oauth:'")
|
||||
continue
|
||||
|
||||
if not values["-CHANNEL-"].startswith("#"):
|
||||
sg.popup_error("Channel must start with '#'")
|
||||
continue
|
||||
|
||||
if not values["-API-KEY-"].startswith("pplx-"):
|
||||
sg.popup_error("Perplexity API key must start with 'pplx-'")
|
||||
continue
|
||||
|
||||
# Build config
|
||||
config = {
|
||||
"TWITCH_BOT_NICKNAME": values["-BOT-NAME-"],
|
||||
"TWITCH_OAUTH_TOKEN": values["-OAUTH-"],
|
||||
"TWITCH_CHANNEL": values["-CHANNEL-"],
|
||||
"PERPLEXITY_API_KEY": values["-API-KEY-"],
|
||||
"PERPLEXITY_MODEL": values["-MODEL-"],
|
||||
"MAX_TOKENS": values["-TOKENS-"],
|
||||
"DEBUG_MODE": "true" if values["-DEBUG-"] else "false",
|
||||
"AUTO_RECONNECT": "true" if values["-RECONNECT-"] else "false"
|
||||
}
|
||||
break
|
||||
|
||||
window.close()
|
||||
return config
|
||||
174
memory.py
Normal file
174
memory.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""
|
||||
Conversation Memory for Eugen Bot
|
||||
Stores and retrieves chat history per user with time-based filtering
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class ConversationMemory:
|
||||
"""Manages persistent conversation history for each user"""
|
||||
|
||||
def __init__(self, data_dir="data/conversations", max_messages=25, retention_hours=1):
|
||||
"""
|
||||
Initialize conversation memory
|
||||
|
||||
Args:
|
||||
data_dir (str): Directory to store conversation JSON files
|
||||
max_messages (int): Maximum messages to store per user
|
||||
retention_hours (int): How long to keep messages in context
|
||||
"""
|
||||
self.data_dir = Path(data_dir)
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.max_messages = max_messages
|
||||
self.retention_hours = retention_hours
|
||||
|
||||
def _get_user_file(self, username):
|
||||
"""Get the file path for a user's conversation history"""
|
||||
# Sanitize username for filesystem
|
||||
safe_username = "".join(c for c in username.lower() if c.isalnum() or c in "._-")
|
||||
return self.data_dir / f"{safe_username}.json"
|
||||
|
||||
def get_user_history(self, username, limit=5):
|
||||
"""
|
||||
Load recent chat history for a user
|
||||
|
||||
Args:
|
||||
username (str): Twitch username
|
||||
limit (int): Maximum number of messages to return
|
||||
|
||||
Returns:
|
||||
list: List of message dicts with role, content, timestamp
|
||||
"""
|
||||
file_path = self._get_user_file(username)
|
||||
|
||||
if not file_path.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
history = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Error loading history for {username}: {e}")
|
||||
return []
|
||||
|
||||
# Filter by retention time
|
||||
cutoff_time = datetime.now() - timedelta(hours=self.retention_hours)
|
||||
recent = []
|
||||
|
||||
for msg in history:
|
||||
try:
|
||||
msg_time = datetime.fromisoformat(msg['timestamp'])
|
||||
if msg_time > cutoff_time:
|
||||
recent.append(msg)
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
|
||||
# Return only the most recent messages up to limit
|
||||
return recent[-limit:] if recent else []
|
||||
|
||||
def add_message(self, username, role, content):
|
||||
"""
|
||||
Add a message to user's conversation history
|
||||
|
||||
Args:
|
||||
username (str): Twitch username
|
||||
role (str): 'user' or 'assistant'
|
||||
content (str): Message content
|
||||
"""
|
||||
file_path = self._get_user_file(username)
|
||||
|
||||
# Load existing history
|
||||
history = []
|
||||
if file_path.exists():
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
history = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Error loading history for {username}: {e}")
|
||||
history = []
|
||||
|
||||
# Add new message
|
||||
history.append({
|
||||
"role": role,
|
||||
"content": content,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
# Enforce max message limit
|
||||
if len(history) > self.max_messages:
|
||||
history = history[-self.max_messages:]
|
||||
|
||||
# Save back to file
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(history, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Error saving history for {username}: {e}")
|
||||
|
||||
def format_for_prompt(self, history):
|
||||
"""
|
||||
Convert history to format suitable for AI API
|
||||
|
||||
Args:
|
||||
history (list): List of message dicts from get_user_history
|
||||
|
||||
Returns:
|
||||
list: List of dicts with 'role' and 'content' keys
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"role": msg['role'],
|
||||
"content": msg['content']
|
||||
}
|
||||
for msg in history
|
||||
]
|
||||
|
||||
def clear_user_history(self, username):
|
||||
"""
|
||||
Clear all history for a specific user
|
||||
|
||||
Args:
|
||||
username (str): Twitch username
|
||||
"""
|
||||
file_path = self._get_user_file(username)
|
||||
if file_path.exists():
|
||||
try:
|
||||
file_path.unlink()
|
||||
except Exception as e:
|
||||
print(f"Error clearing history for {username}: {e}")
|
||||
|
||||
def get_all_users(self):
|
||||
"""
|
||||
Get list of all users with conversation history
|
||||
|
||||
Returns:
|
||||
list: List of usernames
|
||||
"""
|
||||
users = []
|
||||
for file_path in self.data_dir.glob("*.json"):
|
||||
users.append(file_path.stem)
|
||||
return users
|
||||
|
||||
def get_user_message_count(self, username):
|
||||
"""
|
||||
Get total message count for a user
|
||||
|
||||
Args:
|
||||
username (str): Twitch username
|
||||
|
||||
Returns:
|
||||
int: Number of messages in history
|
||||
"""
|
||||
file_path = self._get_user_file(username)
|
||||
if not file_path.exists():
|
||||
return 0
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
history = json.load(f)
|
||||
return len(history)
|
||||
except Exception:
|
||||
return 0
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
irc==20.1.0
|
||||
python-dotenv==1.0.0
|
||||
PySimpleGUI==4.60.0
|
||||
requests==2.31.0
|
||||
httpx==0.25.0
|
||||
169
utils.py
Normal file
169
utils.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
"""
|
||||
Utility classes for Eugen Bot
|
||||
Includes MentionDetector for name recognition and Logger for file logging
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class MentionDetector:
|
||||
"""Detects if the bot was mentioned in a chat message"""
|
||||
|
||||
def __init__(self, bot_name="Eugen"):
|
||||
self.bot_name = bot_name
|
||||
# Create patterns for various mention formats
|
||||
self.patterns = [
|
||||
rf"@{bot_name}", # @Eugen
|
||||
rf"{bot_name}:", # Eugen:
|
||||
rf"{bot_name},", # Eugen,
|
||||
rf"^{bot_name}\s", # Eugen at start
|
||||
rf"\s{bot_name}\s", # Eugen in middle
|
||||
rf"\s{bot_name}$", # Eugen at end
|
||||
]
|
||||
# Case-insensitive compilation
|
||||
self.compiled_patterns = [
|
||||
re.compile(pattern, re.IGNORECASE) for pattern in self.patterns
|
||||
]
|
||||
|
||||
def is_mentioned(self, message):
|
||||
"""
|
||||
Check if bot was mentioned in message
|
||||
|
||||
Args:
|
||||
message (str): Chat message to check
|
||||
|
||||
Returns:
|
||||
bool: True if bot was mentioned
|
||||
"""
|
||||
if not message:
|
||||
return False
|
||||
|
||||
for pattern in self.compiled_patterns:
|
||||
if pattern.search(message):
|
||||
return True
|
||||
return False
|
||||
|
||||
def extract_content(self, message):
|
||||
"""
|
||||
Extract message content without the mention
|
||||
|
||||
Args:
|
||||
message (str): Original message with mention
|
||||
|
||||
Returns:
|
||||
str: Message content without mention prefix
|
||||
"""
|
||||
if not message:
|
||||
return ""
|
||||
|
||||
# Remove common mention patterns
|
||||
content = message
|
||||
patterns_to_remove = [
|
||||
rf"@{self.bot_name}[,:]?\s*",
|
||||
rf"{self.bot_name}[,:]?\s*",
|
||||
]
|
||||
|
||||
for pattern in patterns_to_remove:
|
||||
content = re.sub(pattern, "", content, flags=re.IGNORECASE)
|
||||
|
||||
return content.strip()
|
||||
|
||||
|
||||
class Logger:
|
||||
"""File-based logger for bot events"""
|
||||
|
||||
def __init__(self, log_dir="logs", debug_mode=False):
|
||||
self.log_dir = Path(log_dir)
|
||||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.debug_mode = debug_mode
|
||||
|
||||
# Setup main logger
|
||||
self.main_logger = self._setup_logger(
|
||||
"eugen_main",
|
||||
self.log_dir / "eugen.log",
|
||||
logging.INFO if not debug_mode else logging.DEBUG
|
||||
)
|
||||
|
||||
# Setup API debug logger
|
||||
self.api_logger = self._setup_logger(
|
||||
"eugen_api",
|
||||
self.log_dir / "api_debug.log",
|
||||
logging.DEBUG
|
||||
)
|
||||
|
||||
def _setup_logger(self, name, log_file, level):
|
||||
"""Setup a logger with file handler"""
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
# Avoid duplicate handlers
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
# File handler
|
||||
fh = logging.FileHandler(log_file, encoding='utf-8')
|
||||
fh.setLevel(level)
|
||||
|
||||
# Format
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s | %(levelname)s | %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
fh.setFormatter(formatter)
|
||||
logger.addHandler(fh)
|
||||
|
||||
# Console handler for debug mode
|
||||
if self.debug_mode:
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(level)
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
return logger
|
||||
|
||||
def info(self, message):
|
||||
"""Log info message"""
|
||||
self.main_logger.info(message)
|
||||
|
||||
def debug(self, message):
|
||||
"""Log debug message"""
|
||||
self.main_logger.debug(message)
|
||||
|
||||
def error(self, message):
|
||||
"""Log error message"""
|
||||
self.main_logger.error(message)
|
||||
|
||||
def warning(self, message):
|
||||
"""Log warning message"""
|
||||
self.main_logger.warning(message)
|
||||
|
||||
def api_call(self, endpoint, model, messages_count):
|
||||
"""Log API call details"""
|
||||
self.api_logger.debug(
|
||||
f"API CALL | Endpoint: {endpoint} | Model: {model} | Messages: {messages_count}"
|
||||
)
|
||||
|
||||
def api_response(self, status_code, tokens_used, response_time, content_preview):
|
||||
"""Log API response details"""
|
||||
self.api_logger.debug(
|
||||
f"API RESPONSE | Status: {status_code} | Tokens: {tokens_used} | "
|
||||
f"Time: {response_time:.2f}s | Content: {content_preview[:100]}..."
|
||||
)
|
||||
|
||||
def api_error(self, status_code, error_message):
|
||||
"""Log API error"""
|
||||
self.api_logger.error(
|
||||
f"API ERROR | Status: {status_code} | Error: {error_message}"
|
||||
)
|
||||
|
||||
def chat_message(self, username, message):
|
||||
"""Log chat message"""
|
||||
if self.debug_mode:
|
||||
self.main_logger.debug(f"CHAT | {username}: {message}")
|
||||
|
||||
def bot_response(self, username, response):
|
||||
"""Log bot response"""
|
||||
if self.debug_mode:
|
||||
self.main_logger.debug(f"BOT RESPONSE | To {username}: {response}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue