Compare commits
20
Commits
c789373da9
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0612b290d5 | ||
|
|
3e986d83f1 | ||
|
|
1dae4c5656 | ||
|
|
cf8e4c677f | ||
|
|
b3f57a8635 | ||
|
|
7103b218b9 | ||
|
|
979d19bdc1 | ||
|
|
e92cc701f6 | ||
|
|
6e1b1a44db | ||
|
|
8dbab53c6d | ||
|
|
98b6f10ea4 | ||
|
|
c54f2a33c8 | ||
|
|
c44a4ae92a | ||
|
|
6eace3abc0 | ||
|
|
5541770896 | ||
|
|
b17c180fbe | ||
|
|
5e70e22780 | ||
|
|
aba2fc11f4 | ||
|
|
9f3298fa13 | ||
|
|
f5026965f7 |
No files matched your search
@@ -0,0 +1,77 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
.tox
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.log
|
||||
.git
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.hypothesis
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
.venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Tests
|
||||
tests/
|
||||
test_*.py
|
||||
|
||||
# Completions
|
||||
completions/
|
||||
|
||||
# Configuration examples
|
||||
*.example
|
||||
*.example.*
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
.tmp/
|
||||
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# MaxClaw Agent Docker Image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first for better caching
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create directory for configuration
|
||||
RUN mkdir -p /root/.maxclaw && \
|
||||
chmod 700 /root/.maxclaw
|
||||
|
||||
# Expose port for API (future use)
|
||||
EXPOSE 8000
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV MODEL_PROVIDER=ollama
|
||||
ENV MODEL_NAME=gemma2:9b
|
||||
ENV OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||||
|
||||
# Default command
|
||||
CMD ["python", "maxclaw.py"]
|
||||
@@ -0,0 +1,97 @@
|
||||
# Phase 1 Improvements Summary
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Enhanced Security
|
||||
- **Encryption at rest**: All sensitive data (API keys) is now encrypted using Fernet symmetric encryption
|
||||
- **Secure file permissions**: Configuration directory and files are set to owner-only access (700 for directories, 600 for files)
|
||||
- **Automatic key management**: Encryption keys are automatically generated and stored securely
|
||||
|
||||
### 2. Comprehensive Validation
|
||||
- **Input validation**: All configuration inputs are rigorously validated
|
||||
- **Provider-specific checks**:
|
||||
- OpenAI: Validates API key format (starts with "sk-")
|
||||
- Ollama: Validates URL format (http/https)
|
||||
- OpenRouter: Validates API key and URL format
|
||||
- **Length limits**: Prevents excessively long inputs
|
||||
- **Clear error messages**: Users get specific feedback on what's wrong
|
||||
|
||||
### 3. Improved CLI
|
||||
- **New test command**: Can test connectivity to configured providers
|
||||
- **Better error handling**: Clear error messages for invalid inputs
|
||||
- **Non-interactive mode**: Allows scripting and automation
|
||||
- **Consistent output formatting**: Clean, readable display of configurations
|
||||
|
||||
### 4. Robust Configuration Management
|
||||
- **Backup system**: Automatic backups before configuration changes
|
||||
- **Atomic writes**: Prevents corruption during writes
|
||||
- **Migration ready**: Structure allows for future schema changes
|
||||
- **Deep copying**: Prevents accidental reference sharing
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `config_manager.py` - Core security and validation logic
|
||||
2. `config_cli.py` - Enhanced CLI with testing capabilities
|
||||
3. `config_tui.py` - TUI interface (maintained compatibility)
|
||||
4. `main.py` - Minor updates for consistency
|
||||
5. `README.md` - Updated documentation
|
||||
6. `requirements.txt` - Added cryptography dependency
|
||||
|
||||
## Next Steps for Phase 2
|
||||
|
||||
### User Experience Improvements
|
||||
1. **Enhanced TUI**:
|
||||
- Search/filter configuration lists
|
||||
- Export/import configurations (JSON/YAML)
|
||||
- Connection testing from within TUI
|
||||
- Better keyboard navigation and shortcuts
|
||||
|
||||
2. **CLI Enhancements**:
|
||||
- More subcommands (export, import, migrate)
|
||||
- Better formatting and color output
|
||||
- Shell completion scripts (bash/zsh)
|
||||
- Dry-run mode for safe preview
|
||||
|
||||
### Environment Integration
|
||||
1. **Custom Tools for User's Environment**:
|
||||
- Proxmox VM management tools
|
||||
- Ollama model discovery and management
|
||||
- System information gathering tools
|
||||
|
||||
2. **Auto-discovery**:
|
||||
- Automatic detection of local Ollama instances
|
||||
- Network scanning for common services
|
||||
|
||||
### Testing and Quality
|
||||
1. **Comprehensive Test Suite**:
|
||||
- Unit tests for all components
|
||||
- Integration tests for configuration flows
|
||||
- Security testing for encryption implementation
|
||||
|
||||
2. **Documentation Improvements**:
|
||||
- More detailed usage examples
|
||||
- Troubleshooting guide
|
||||
- API reference for developers
|
||||
|
||||
## Current Status
|
||||
|
||||
The system is now ready for secure daily use:
|
||||
- API keys are encrypted at rest
|
||||
- Configuration is validated before storage
|
||||
- Connections can be tested before use
|
||||
- All file permissions are set appropriately
|
||||
- Backups are automatically created
|
||||
|
||||
To test the current implementation:
|
||||
```bash
|
||||
# Test Ollama connection (adjust URL as needed)
|
||||
python config_cli.py test --name "test-ollama"
|
||||
|
||||
# List configurations
|
||||
python config_cli.py list
|
||||
|
||||
# Launch TUI for interactive management
|
||||
python config_tui.py
|
||||
```
|
||||
|
||||
When ready to continue with Phase 2, we'll focus on enhancing the user experience and integrating more deeply with your specific Proxmox/Ollama environment.
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
"""
|
||||
Advanced analytics for MaxClaw Agent
|
||||
Provides usage analytics, insights, and reporting
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
from collections import defaultdict, Counter
|
||||
|
||||
# Add current directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
class AnalyticsEngine:
|
||||
"""Advanced analytics engine for MaxClaw Agent"""
|
||||
|
||||
def __init__(self, data_dir: Optional[str] = None):
|
||||
"""
|
||||
Initialize analytics engine
|
||||
|
||||
Args:
|
||||
data_dir: Directory for storing analytics data
|
||||
"""
|
||||
if data_dir is None:
|
||||
self.data_dir = Path.home() / ".maxclaw" / "analytics"
|
||||
else:
|
||||
self.data_dir = Path(data_dir)
|
||||
|
||||
# Create data directory
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Analytics data
|
||||
self.usage_data = {
|
||||
"conversations": [],
|
||||
"tool_usage": defaultdict(int),
|
||||
"provider_usage": defaultdict(int),
|
||||
"command_usage": defaultdict(int),
|
||||
"error_patterns": defaultdict(int),
|
||||
"response_times": [],
|
||||
"user_sessions": []
|
||||
}
|
||||
|
||||
# Load existing data
|
||||
self._load_data()
|
||||
|
||||
def _load_data(self):
|
||||
"""Load analytics data from file"""
|
||||
data_file = self.data_dir / "analytics_data.json"
|
||||
|
||||
if data_file.exists():
|
||||
try:
|
||||
with open(data_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
# Convert defaultdicts back
|
||||
self.usage_data["tool_usage"] = defaultdict(int, data.get("tool_usage", {}))
|
||||
self.usage_data["provider_usage"] = defaultdict(int, data.get("provider_usage", {}))
|
||||
self.usage_data["command_usage"] = defaultdict(int, data.get("command_usage", {}))
|
||||
self.usage_data["error_patterns"] = defaultdict(int, data.get("error_patterns", {}))
|
||||
self.usage_data["conversations"] = data.get("conversations", [])
|
||||
self.usage_data["response_times"] = data.get("response_times", [])
|
||||
self.usage_data["user_sessions"] = data.get("user_sessions", [])
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load analytics data: {e}")
|
||||
|
||||
def _save_data(self):
|
||||
"""Save analytics data to file"""
|
||||
data_file = self.data_dir / "analytics_data.json"
|
||||
|
||||
try:
|
||||
# Convert defaultdicts to regular dicts for JSON serialization
|
||||
data_to_save = {
|
||||
"conversations": self.usage_data["conversations"],
|
||||
"tool_usage": dict(self.usage_data["tool_usage"]),
|
||||
"provider_usage": dict(self.usage_data["provider_usage"]),
|
||||
"command_usage": dict(self.usage_data["command_usage"]),
|
||||
"error_patterns": dict(self.usage_data["error_patterns"]),
|
||||
"response_times": self.usage_data["response_times"],
|
||||
"user_sessions": self.usage_data["user_sessions"]
|
||||
}
|
||||
|
||||
with open(data_file, 'w') as f:
|
||||
json.dump(data_to_save, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not save analytics data: {e}")
|
||||
|
||||
def record_conversation(self, user_message: str, agent_response: str,
|
||||
provider: str, model: str, response_time: float):
|
||||
"""Record a conversation for analytics"""
|
||||
conversation = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"user_message": user_message[:100], # Truncate for privacy
|
||||
"agent_response": agent_response[:100], # Truncate for privacy
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"response_time": response_time,
|
||||
"message_length": len(user_message),
|
||||
"response_length": len(agent_response)
|
||||
}
|
||||
|
||||
self.usage_data["conversations"].append(conversation)
|
||||
self.usage_data["response_times"].append(response_time)
|
||||
|
||||
# Keep only last 1000 conversations
|
||||
if len(self.usage_data["conversations"]) > 1000:
|
||||
self.usage_data["conversations"] = self.usage_data["conversations"][-1000:]
|
||||
|
||||
# Keep only last 5000 response times
|
||||
if len(self.usage_data["response_times"]) > 5000:
|
||||
self.usage_data["response_times"] = self.usage_data["response_times"][-5000:]
|
||||
|
||||
self._save_data()
|
||||
|
||||
def record_tool_usage(self, tool_name: str, success: bool, duration: float):
|
||||
"""Record tool usage"""
|
||||
self.usage_data["tool_usage"][tool_name] += 1
|
||||
self._save_data()
|
||||
|
||||
def record_provider_usage(self, provider: str, model: str):
|
||||
"""Record provider usage"""
|
||||
key = f"{provider}:{model}"
|
||||
self.usage_data["provider_usage"][key] += 1
|
||||
self._save_data()
|
||||
|
||||
def record_command_usage(self, command: str):
|
||||
"""Record command usage"""
|
||||
self.usage_data["command_usage"][command] += 1
|
||||
self._save_data()
|
||||
|
||||
def record_error(self, error_type: str, error_message: str):
|
||||
"""Record error patterns"""
|
||||
# Simplify error message for pattern recognition
|
||||
simplified = error_message[:50].lower()
|
||||
self.usage_data["error_patterns"][simplified] += 1
|
||||
self._save_data()
|
||||
|
||||
def record_user_session(self, session_id: str, duration: float,
|
||||
messages_count: int, tools_used: List[str]):
|
||||
"""Record user session"""
|
||||
session = {
|
||||
"session_id": session_id,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"duration": duration,
|
||||
"messages_count": messages_count,
|
||||
"tools_used": tools_used,
|
||||
"tools_count": len(tools_used)
|
||||
}
|
||||
|
||||
self.usage_data["user_sessions"].append(session)
|
||||
|
||||
# Keep only last 100 sessions
|
||||
if len(self.usage_data["user_sessions"]) > 100:
|
||||
self.usage_data["user_sessions"] = self.usage_data["user_sessions"][-100:]
|
||||
|
||||
self._save_data()
|
||||
|
||||
def get_usage_summary(self, days: int = 30) -> Dict[str, Any]:
|
||||
"""
|
||||
Get usage summary for the last N days
|
||||
|
||||
Args:
|
||||
days: Number of days to include
|
||||
|
||||
Returns:
|
||||
Dictionary with usage summary
|
||||
"""
|
||||
cutoff_time = datetime.now() - timedelta(days=days)
|
||||
|
||||
# Filter recent conversations
|
||||
recent_conversations = [
|
||||
c for c in self.usage_data["conversations"]
|
||||
if datetime.fromisoformat(c["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
# Calculate statistics
|
||||
total_conversations = len(recent_conversations)
|
||||
total_messages = sum(c.get("message_length", 0) for c in recent_conversations)
|
||||
total_responses = sum(c.get("response_length", 0) for c in recent_conversations)
|
||||
|
||||
# Provider usage
|
||||
provider_counts = Counter()
|
||||
for conv in recent_conversations:
|
||||
provider = conv.get("provider", "unknown")
|
||||
provider_counts[provider] += 1
|
||||
|
||||
# Model usage
|
||||
model_counts = Counter()
|
||||
for conv in recent_conversations:
|
||||
model = conv.get("model", "unknown")
|
||||
model_counts[model] += 1
|
||||
|
||||
# Response time statistics
|
||||
recent_response_times = [
|
||||
c.get("response_time", 0) for c in recent_conversations
|
||||
if c.get("response_time")
|
||||
]
|
||||
|
||||
if recent_response_times:
|
||||
avg_response_time = sum(recent_response_times) / len(recent_response_times)
|
||||
min_response_time = min(recent_response_times)
|
||||
max_response_time = max(recent_response_times)
|
||||
else:
|
||||
avg_response_time = min_response_time = max_response_time = 0
|
||||
|
||||
# Tool usage
|
||||
tool_usage = dict(self.usage_data["tool_usage"])
|
||||
|
||||
# Command usage
|
||||
command_usage = dict(self.usage_data["command_usage"])
|
||||
|
||||
# Error patterns
|
||||
error_patterns = dict(self.usage_data["error_patterns"])
|
||||
|
||||
# Session statistics
|
||||
recent_sessions = [
|
||||
s for s in self.usage_data["user_sessions"]
|
||||
if datetime.fromisoformat(s["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
total_sessions = len(recent_sessions)
|
||||
total_session_duration = sum(s.get("duration", 0) for s in recent_sessions)
|
||||
avg_session_duration = total_session_duration / total_sessions if total_sessions > 0 else 0
|
||||
|
||||
return {
|
||||
"period_days": days,
|
||||
"conversations": {
|
||||
"total": total_conversations,
|
||||
"total_messages_chars": total_messages,
|
||||
"total_responses_chars": total_responses,
|
||||
"avg_message_length": total_messages / total_conversations if total_conversations > 0 else 0,
|
||||
"avg_response_length": total_responses / total_conversations if total_conversations > 0 else 0
|
||||
},
|
||||
"providers": {
|
||||
"usage": dict(provider_counts),
|
||||
"most_used": provider_counts.most_common(1)[0][0] if provider_counts else "None"
|
||||
},
|
||||
"models": {
|
||||
"usage": dict(model_counts),
|
||||
"most_used": model_counts.most_common(1)[0][0] if model_counts else "None"
|
||||
},
|
||||
"performance": {
|
||||
"avg_response_time": avg_response_time,
|
||||
"min_response_time": min_response_time,
|
||||
"max_response_time": max_response_time,
|
||||
"total_response_time": sum(recent_response_times)
|
||||
},
|
||||
"tools": {
|
||||
"usage": tool_usage,
|
||||
"most_used": max(tool_usage.items(), key=lambda x: x[1])[0] if tool_usage else "None"
|
||||
},
|
||||
"commands": {
|
||||
"usage": command_usage,
|
||||
"most_used": max(command_usage.items(), key=lambda x: x[1])[0] if command_usage else "None"
|
||||
},
|
||||
"errors": {
|
||||
"patterns": error_patterns,
|
||||
"most_common": max(error_patterns.items(), key=lambda x: x[1])[0] if error_patterns else "None"
|
||||
},
|
||||
"sessions": {
|
||||
"total": total_sessions,
|
||||
"total_duration": total_session_duration,
|
||||
"avg_duration": avg_session_duration,
|
||||
"avg_messages_per_session": sum(s.get("messages_count", 0) for s in recent_sessions) / total_sessions if total_sessions > 0 else 0
|
||||
},
|
||||
"generated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def get_detailed_report(self, days: int = 30) -> str:
|
||||
"""
|
||||
Get detailed analytics report
|
||||
|
||||
Args:
|
||||
days: Number of days to include
|
||||
|
||||
Returns:
|
||||
Formatted report string
|
||||
"""
|
||||
summary = self.get_usage_summary(days)
|
||||
|
||||
report = f"📊 Advanced Analytics Report (Last {days} days)\n"
|
||||
report += "=" * 60 + "\n\n"
|
||||
|
||||
# Conversation statistics
|
||||
report += "💬 Conversation Statistics:\n"
|
||||
report += f" Total Conversations: {summary['conversations']['total']}\n"
|
||||
report += f" Total Message Characters: {summary['conversations']['total_messages_chars']:,}\n"
|
||||
report += f" Total Response Characters: {summary['conversations']['total_responses_chars']:,}\n"
|
||||
report += f" Average Message Length: {summary['conversations']['avg_message_length']:.0f} chars\n"
|
||||
report += f" Average Response Length: {summary['conversations']['avg_response_length']:.0f} chars\n\n"
|
||||
|
||||
# Provider usage
|
||||
report += "🤖 Provider Usage:\n"
|
||||
for provider, count in summary['providers']['usage'].items():
|
||||
percentage = (count / summary['conversations']['total'] * 100) if summary['conversations']['total'] > 0 else 0
|
||||
report += f" {provider}: {count} ({percentage:.1f}%)\n"
|
||||
report += f" Most Used: {summary['providers']['most_used']}\n\n"
|
||||
|
||||
# Model usage
|
||||
report += "🧠 Model Usage:\n"
|
||||
for model, count in summary['models']['usage'].items():
|
||||
percentage = (count / summary['conversations']['total'] * 100) if summary['conversations']['total'] > 0 else 0
|
||||
report += f" {model}: {count} ({percentage:.1f}%)\n"
|
||||
report += f" Most Used: {summary['models']['most_used']}\n\n"
|
||||
|
||||
# Performance metrics
|
||||
report += "⚡ Performance Metrics:\n"
|
||||
report += f" Average Response Time: {summary['performance']['avg_response_time']:.3f}s\n"
|
||||
report += f" Minimum Response Time: {summary['performance']['min_response_time']:.3f}s\n"
|
||||
report += f" Maximum Response Time: {summary['performance']['max_response_time']:.3f}s\n"
|
||||
report += f" Total Response Time: {summary['performance']['total_response_time']:.1f}s\n\n"
|
||||
|
||||
# Tool usage
|
||||
report += "🔧 Tool Usage:\n"
|
||||
if summary['tools']['usage']:
|
||||
for tool, count in sorted(summary['tools']['usage'].items(), key=lambda x: x[1], reverse=True)[:10]:
|
||||
report += f" {tool}: {count}\n"
|
||||
report += f" Most Used: {summary['tools']['most_used']}\n"
|
||||
else:
|
||||
report += " No tool usage recorded\n"
|
||||
report += "\n"
|
||||
|
||||
# Command usage
|
||||
report += "⌨️ Command Usage:\n"
|
||||
if summary['commands']['usage']:
|
||||
for command, count in sorted(summary['commands']['usage'].items(), key=lambda x: x[1], reverse=True)[:10]:
|
||||
report += f" {command}: {count}\n"
|
||||
report += f" Most Used: {summary['commands']['most_used']}\n"
|
||||
else:
|
||||
report += " No command usage recorded\n"
|
||||
report += "\n"
|
||||
|
||||
# Error patterns
|
||||
report += "❌ Error Patterns:\n"
|
||||
if summary['errors']['patterns']:
|
||||
for pattern, count in sorted(summary['errors']['patterns'].items(), key=lambda x: x[1], reverse=True)[:5]:
|
||||
report += f" {pattern}: {count}\n"
|
||||
report += f" Most Common: {summary['errors']['most_common']}\n"
|
||||
else:
|
||||
report += " No errors recorded\n"
|
||||
report += "\n"
|
||||
|
||||
# Session statistics
|
||||
report += "👥 Session Statistics:\n"
|
||||
report += f" Total Sessions: {summary['sessions']['total']}\n"
|
||||
report += f" Total Session Duration: {summary['sessions']['total_duration']:.1f}s\n"
|
||||
report += f" Average Session Duration: {summary['sessions']['avg_duration']:.1f}s\n"
|
||||
report += f" Average Messages per Session: {summary['sessions']['avg_messages_per_session']:.1f}\n"
|
||||
|
||||
return report
|
||||
|
||||
def get_insights(self, days: int = 30) -> List[str]:
|
||||
"""
|
||||
Get insights and recommendations based on usage patterns
|
||||
|
||||
Args:
|
||||
days: Number of days to analyze
|
||||
|
||||
Returns:
|
||||
List of insights and recommendations
|
||||
"""
|
||||
summary = self.get_usage_summary(days)
|
||||
insights = []
|
||||
|
||||
# Provider insights
|
||||
if summary['providers']['usage']:
|
||||
most_used_provider = summary['providers']['most_used']
|
||||
provider_count = summary['providers']['usage'].get(most_used_provider, 0)
|
||||
total_conversations = summary['conversations']['total']
|
||||
|
||||
if total_conversations > 0:
|
||||
provider_percentage = (provider_count / total_conversations) * 100
|
||||
if provider_percentage > 80:
|
||||
insights.append(f"💡 You're heavily using {most_used_provider} ({provider_percentage:.0f}% of conversations). Consider diversifying with other providers for different use cases.")
|
||||
elif provider_percentage < 20:
|
||||
insights.append(f"💡 You're barely using {most_used_provider} ({provider_percentage:.0f}% of conversations). Consider if this provider is meeting your needs.")
|
||||
|
||||
# Performance insights
|
||||
avg_response_time = summary['performance']['avg_response_time']
|
||||
if avg_response_time > 5.0:
|
||||
insights.append(f"⚠️ Average response time is high ({avg_response_time:.1f}s). Consider using faster models or optimizing your queries.")
|
||||
elif avg_response_time < 1.0:
|
||||
insights.append(f"✅ Great response times! Average is {avg_response_time:.1f}s.")
|
||||
|
||||
# Tool usage insights
|
||||
if summary['tools']['usage']:
|
||||
most_used_tool = summary['tools']['most_used']
|
||||
tool_count = summary['tools']['usage'].get(most_used_tool, 0)
|
||||
insights.append(f"🔧 You frequently use the '{most_used_tool}' tool ({tool_count} times). Consider creating shortcuts or aliases for it.")
|
||||
|
||||
# Error insights
|
||||
if summary['errors']['patterns']:
|
||||
most_common_error = summary['errors']['most_common']
|
||||
error_count = summary['errors']['patterns'].get(most_common_error, 0)
|
||||
if error_count > 5:
|
||||
insights.append(f"❌ You're encountering a recurring error: '{most_common_error}' ({error_count} times). Consider investigating the root cause.")
|
||||
|
||||
# Session insights
|
||||
avg_session_duration = summary['sessions']['avg_duration']
|
||||
if avg_session_duration > 3600: # More than 1 hour
|
||||
insights.append(f"⏱️ Your average session is long ({avg_session_duration/60:.0f} minutes). Consider breaking work into shorter sessions for better focus.")
|
||||
|
||||
# Usage pattern insights
|
||||
total_conversations = summary['conversations']['total']
|
||||
if total_conversations > 100:
|
||||
insights.append(f"📈 You're a power user with {total_conversations} conversations in {days} days!")
|
||||
elif total_conversations < 10:
|
||||
insights.append(f"💡 You've only had {total_conversations} conversations in {days} days. Consider exploring more features.")
|
||||
|
||||
# Default insight if none generated
|
||||
if not insights:
|
||||
insights.append("📊 Your usage patterns look normal. Keep using MaxClaw Agent!")
|
||||
|
||||
return insights
|
||||
|
||||
def clear_old_data(self, days: int = 90):
|
||||
"""
|
||||
Clear data older than specified days
|
||||
|
||||
Args:
|
||||
days: Number of days to keep data
|
||||
"""
|
||||
cutoff_time = datetime.now() - timedelta(days=days)
|
||||
|
||||
# Filter out old conversations
|
||||
self.usage_data["conversations"] = [
|
||||
c for c in self.usage_data["conversations"]
|
||||
if datetime.fromisoformat(c["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
# Filter out old sessions
|
||||
self.usage_data["user_sessions"] = [
|
||||
s for s in self.usage_data["user_sessions"]
|
||||
if datetime.fromisoformat(s["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
# Save cleaned data
|
||||
self._save_data()
|
||||
|
||||
print(f"✅ Cleared analytics data older than {days} days")
|
||||
|
||||
|
||||
# Global analytics engine instance
|
||||
analytics_engine = AnalyticsEngine()
|
||||
|
||||
|
||||
def get_usage_summary(days: int = 30) -> Dict[str, Any]:
|
||||
"""
|
||||
Get usage summary
|
||||
|
||||
Args:
|
||||
days: Number of days to include
|
||||
|
||||
Returns:
|
||||
Usage summary dictionary
|
||||
"""
|
||||
return analytics_engine.get_usage_summary(days)
|
||||
|
||||
|
||||
def get_analytics_report(days: int = 30) -> str:
|
||||
"""
|
||||
Get formatted analytics report
|
||||
|
||||
Args:
|
||||
days: Number of days to include
|
||||
|
||||
Returns:
|
||||
Formatted report string
|
||||
"""
|
||||
return analytics_engine.get_detailed_report(days)
|
||||
|
||||
|
||||
def get_analytics_insights(days: int = 30) -> List[str]:
|
||||
"""
|
||||
Get analytics insights and recommendations
|
||||
|
||||
Args:
|
||||
days: Number of days to analyze
|
||||
|
||||
Returns:
|
||||
List of insights and recommendations
|
||||
"""
|
||||
return analytics_engine.get_insights(days)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Print analytics report when run directly
|
||||
print(get_analytics_report())
|
||||
print("\n" + "=" * 60)
|
||||
print("💡 Insights and Recommendations:")
|
||||
for insight in get_analytics_insights():
|
||||
print(f" {insight}")
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
"""
|
||||
MaxClaw Agent API Server
|
||||
Provides REST API for remote access to MaxClaw Agent
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, Dict, Any, List
|
||||
from fastapi import FastAPI, HTTPException, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
import uvicorn
|
||||
|
||||
# Add current directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from config_manager import config_manager
|
||||
from main import create_maxclaw_agent
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="MaxClaw Agent API",
|
||||
description="REST API for MaxClaw Agent - configurable AI agent",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Mount static files for web UI
|
||||
web_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web")
|
||||
if os.path.exists(web_dir):
|
||||
app.mount("/static", StaticFiles(directory=web_dir), name="static")
|
||||
|
||||
# Global agent instance
|
||||
agent = None
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
"""Chat request model"""
|
||||
message: str
|
||||
conversation_id: Optional[str] = None
|
||||
provider: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
"""Chat response model"""
|
||||
response: str
|
||||
conversation_id: Optional[str] = None
|
||||
provider: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
|
||||
|
||||
class ConfigRequest(BaseModel):
|
||||
"""Configuration request model"""
|
||||
name: str
|
||||
provider: str
|
||||
model_name: str
|
||||
api_key: Optional[str] = None
|
||||
base_url: Optional[str] = None
|
||||
set_active: bool = False
|
||||
|
||||
|
||||
class ConfigResponse(BaseModel):
|
||||
"""Configuration response model"""
|
||||
name: str
|
||||
provider: str
|
||||
model_name: str
|
||||
is_active: bool
|
||||
|
||||
|
||||
def get_agent():
|
||||
"""Get or create agent instance"""
|
||||
global agent
|
||||
if agent is None:
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create agent: {str(e)}")
|
||||
return agent
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Initialize agent on startup"""
|
||||
global agent
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
print("✅ MaxClaw Agent API server started")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to initialize agent: {e}")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint - serve web UI"""
|
||||
web_index = os.path.join(web_dir, "index.html")
|
||||
if os.path.exists(web_index):
|
||||
return FileResponse(web_index)
|
||||
else:
|
||||
return {
|
||||
"message": "MaxClaw Agent API",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"chat": "/chat",
|
||||
"configurations": "/configurations",
|
||||
"health": "/health",
|
||||
"web_ui": "/static/index.html",
|
||||
"monitoring": "/monitoring"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/monitoring")
|
||||
async def monitoring():
|
||||
"""Monitoring dashboard endpoint"""
|
||||
monitoring_file = os.path.join(web_dir, "monitoring.html")
|
||||
if os.path.exists(monitoring_file):
|
||||
return FileResponse(monitoring_file)
|
||||
else:
|
||||
return {
|
||||
"message": "Monitoring dashboard not available",
|
||||
"version": "1.0.0",
|
||||
"note": "Monitoring dashboard file not found"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "healthy", "agent_initialized": agent is not None}
|
||||
|
||||
|
||||
@app.post("/chat", response_model=ChatResponse)
|
||||
async def chat(request: ChatRequest):
|
||||
"""Chat with MaxClaw Agent"""
|
||||
try:
|
||||
agent_instance = get_agent()
|
||||
|
||||
# Create message
|
||||
message = HumanMessage(content=request.message)
|
||||
|
||||
# Invoke agent
|
||||
result = agent_instance.invoke({"messages": [message]})
|
||||
|
||||
# Extract response
|
||||
ai_message = result["messages"][-1]
|
||||
|
||||
return ChatResponse(
|
||||
response=ai_message.content,
|
||||
conversation_id=request.conversation_id,
|
||||
provider=os.getenv("MODEL_PROVIDER", "unknown"),
|
||||
model=os.getenv("MODEL_NAME", "unknown")
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/configurations", response_model=List[ConfigResponse])
|
||||
async def list_configurations():
|
||||
"""List all configurations"""
|
||||
try:
|
||||
configs = config_manager.get_configurations()
|
||||
active = config_manager.get_active_configuration()
|
||||
|
||||
result = []
|
||||
for name, config in configs.items():
|
||||
result.append(ConfigResponse(
|
||||
name=name,
|
||||
provider=config.get("provider", "unknown"),
|
||||
model_name=config.get("model_name", "unknown"),
|
||||
is_active=(name == active)
|
||||
))
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/configurations", response_model=ConfigResponse)
|
||||
async def add_configuration(request: ConfigRequest):
|
||||
"""Add a new configuration"""
|
||||
try:
|
||||
config = {
|
||||
"provider": request.provider,
|
||||
"model_name": request.model_name
|
||||
}
|
||||
|
||||
if request.api_key:
|
||||
config["api_key"] = request.api_key
|
||||
|
||||
if request.base_url:
|
||||
config["base_url"] = request.base_url
|
||||
|
||||
# Add configuration
|
||||
config_manager.add_configuration(request.name, config)
|
||||
|
||||
# Set as active if requested
|
||||
if request.set_active:
|
||||
config_manager.set_active_configuration(request.name)
|
||||
|
||||
return ConfigResponse(
|
||||
name=request.name,
|
||||
provider=request.provider,
|
||||
model_name=request.model_name,
|
||||
is_active=request.set_active
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.delete("/configurations/{name}")
|
||||
async def delete_configuration(name: str):
|
||||
"""Delete a configuration"""
|
||||
try:
|
||||
if config_manager.remove_configuration(name):
|
||||
return {"message": f"Configuration '{name}' deleted successfully"}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"Configuration '{name}' not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/configurations/{name}/activate")
|
||||
async def activate_configuration(name: str):
|
||||
"""Set a configuration as active"""
|
||||
try:
|
||||
if config_manager.set_active_configuration(name):
|
||||
return {"message": f"Configuration '{name}' set as active"}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"Configuration '{name}' not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/configurations/active")
|
||||
async def get_active_configuration():
|
||||
"""Get the active configuration"""
|
||||
try:
|
||||
active_name = config_manager.get_active_configuration()
|
||||
if not active_name:
|
||||
return {"message": "No active configuration set"}
|
||||
|
||||
config = config_manager.get_configuration(active_name)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail=f"Active configuration '{active_name}' not found")
|
||||
|
||||
return {
|
||||
"name": active_name,
|
||||
"provider": config.get("provider", "unknown"),
|
||||
"model_name": config.get("model_name", "unknown")
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/configurations/{name}/test")
|
||||
async def test_configuration(name: str):
|
||||
"""Test connection to a configuration"""
|
||||
try:
|
||||
success, message = config_manager.test_configuration_connection(name)
|
||||
return {
|
||||
"success": success,
|
||||
"message": message
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/providers")
|
||||
async def list_providers():
|
||||
"""List available providers"""
|
||||
return {
|
||||
"providers": [
|
||||
{
|
||||
"name": "openai",
|
||||
"description": "OpenAI GPT models",
|
||||
"required_env": ["OPENAI_API_KEY"],
|
||||
"optional_env": []
|
||||
},
|
||||
{
|
||||
"name": "ollama",
|
||||
"description": "Local Ollama models",
|
||||
"required_env": [],
|
||||
"optional_env": ["OLLAMA_BASE_URL"]
|
||||
},
|
||||
{
|
||||
"name": "openrouter",
|
||||
"description": "OpenRouter multi-provider access",
|
||||
"required_env": ["OPENROUTER_API_KEY"],
|
||||
"optional_env": ["OPENROUTER_BASE_URL"]
|
||||
},
|
||||
{
|
||||
"name": "anthropic",
|
||||
"description": "Anthropic Claude models",
|
||||
"required_env": ["ANTHROPIC_API_KEY"],
|
||||
"optional_env": []
|
||||
},
|
||||
{
|
||||
"name": "google",
|
||||
"description": "Google Gemini models",
|
||||
"required_env": ["GOOGLE_API_KEY"],
|
||||
"optional_env": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the API server
|
||||
uvicorn.run(
|
||||
"api_server:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
log_level="info"
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# Bash completion for MaxClaw Agent CLI
|
||||
|
||||
_maxclaw_completions()
|
||||
{
|
||||
local cur prev commands
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
# Main commands
|
||||
commands="list search add remove set-active active test export import help"
|
||||
|
||||
# Handle main command completion
|
||||
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
||||
COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Handle subcommand-specific completions
|
||||
case "${prev}" in
|
||||
search)
|
||||
# Search term - no completion
|
||||
return 0
|
||||
;;
|
||||
add)
|
||||
# Add command options
|
||||
local add_options="--non-interactive --name --provider --model --api-key --base-url --set-active"
|
||||
COMPREPLY=( $(compgen -W "${add_options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
remove|set-active|test)
|
||||
# Configuration names - try to get from config list
|
||||
if command -v python3 &> /dev/null; then
|
||||
local configs=$(python3 -c "
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
configs = config_manager.list_configuration_names()
|
||||
print(' '.join(configs))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null)
|
||||
COMPREPLY=( $(compgen -W "${configs}" -- ${cur}) )
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
export)
|
||||
# Export options
|
||||
local export_options="--format --output"
|
||||
COMPREPLY=( $(compgen -W "${export_options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
import)
|
||||
# File completion
|
||||
COMPREPLY=( $(compgen -f -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--provider)
|
||||
# Provider options
|
||||
local providers="openai ollama openrouter"
|
||||
COMPREPLY=( $(compgen -W "${providers}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--format)
|
||||
# Format options
|
||||
local formats="json yaml"
|
||||
COMPREPLY=( $(compgen -W "${formats}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Handle option arguments
|
||||
case "${cur}" in
|
||||
-*)
|
||||
# Option completion
|
||||
local options="--non-interactive --name --provider --model --api-key --base-url --set-active --format --output"
|
||||
COMPREPLY=( $(compgen -W "${options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _maxclaw_completions python3 config_cli.py
|
||||
complete -F _maxclaw_completions python config_cli.py
|
||||
@@ -0,0 +1,79 @@
|
||||
#compdef config_cli.py
|
||||
# Zsh completion for MaxClaw Agent CLI
|
||||
|
||||
_maxclaw() {
|
||||
local -a commands
|
||||
local curcontext="$curcontext" state line
|
||||
typeset -A opt_args
|
||||
|
||||
_arguments -C \
|
||||
'1: :->command' \
|
||||
'*::arg:->args'
|
||||
|
||||
case $state in
|
||||
command)
|
||||
commands=(
|
||||
'list:List all configurations'
|
||||
'search:Search configurations'
|
||||
'add:Add a new configuration'
|
||||
'remove:Remove a configuration'
|
||||
'set-active:Set active configuration'
|
||||
'active:Show active configuration'
|
||||
'test:Test connection to a configuration'
|
||||
'export:Export configurations to JSON/YAML'
|
||||
'import:Import configurations from JSON/YAML'
|
||||
'help:Show help'
|
||||
)
|
||||
_describe -t commands 'MaxClaw commands' commands
|
||||
;;
|
||||
args)
|
||||
case ${line[1]} in
|
||||
search)
|
||||
_message 'search term'
|
||||
;;
|
||||
add)
|
||||
_arguments \
|
||||
'--non-interactive[Non-interactive mode]' \
|
||||
'--name[Configuration name]:name' \
|
||||
'--provider[Provider]:provider:(openai ollama openrouter)' \
|
||||
'--model[Model name]:model' \
|
||||
'--api-key[API key]:api_key' \
|
||||
'--base-url[Base URL]:base_url' \
|
||||
'--set-active[Set as active configuration]'
|
||||
;;
|
||||
remove|set-active|test)
|
||||
# Try to get configuration names
|
||||
local -a configs
|
||||
if (( $+commands[python3] )); then
|
||||
configs=(${(f)"$(python3 -c "
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
configs = config_manager.list_configuration_names()
|
||||
for c in configs:
|
||||
print(c)
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null)"})
|
||||
fi
|
||||
if (( ${#configs} )); then
|
||||
_describe -t configurations 'configurations' configs
|
||||
else
|
||||
_message 'configuration name'
|
||||
fi
|
||||
;;
|
||||
export)
|
||||
_arguments \
|
||||
'--format[Export format]:format:(json yaml)' \
|
||||
'--output[Output filename]:filename'
|
||||
;;
|
||||
import)
|
||||
_files -g '*.json *.yaml *.yml'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_maxclaw "$@"
|
||||
+253
-4
@@ -1,22 +1,27 @@
|
||||
"""
|
||||
Command-line interface for managing MaxClaw Agent configurations
|
||||
Enhanced with export/import, search, and better formatting
|
||||
"""
|
||||
import sys
|
||||
import argparse
|
||||
from typing import Optional
|
||||
import json
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
from config_manager import config_manager
|
||||
|
||||
|
||||
def print_configurations():
|
||||
def print_configurations(configs=None, title="MaxClaw Agent Configurations"):
|
||||
"""Print all configurations in a formatted way"""
|
||||
configs = config_manager.get_configurations()
|
||||
if configs is None:
|
||||
configs = config_manager.get_configurations()
|
||||
active = config_manager.get_active_configuration()
|
||||
|
||||
if not configs:
|
||||
print("No configurations found.")
|
||||
return
|
||||
|
||||
print("\nMaxClaw Agent Configurations:")
|
||||
print(f"\n{title}:")
|
||||
print("=" * 50)
|
||||
|
||||
for name, config in configs.items():
|
||||
@@ -41,6 +46,41 @@ def print_configurations():
|
||||
print()
|
||||
|
||||
|
||||
def filter_configurations(configs: Dict[str, Any], search_term: str) -> Dict[str, Any]:
|
||||
"""Filter configurations based on search term"""
|
||||
if not search_term:
|
||||
return configs
|
||||
|
||||
search_term = search_term.lower()
|
||||
filtered = {}
|
||||
|
||||
for name, config in configs.items():
|
||||
# Search in name
|
||||
if search_term in name.lower():
|
||||
filtered[name] = config
|
||||
continue
|
||||
|
||||
# Search in provider
|
||||
provider = config.get("provider", "").lower()
|
||||
if search_term in provider:
|
||||
filtered[name] = config
|
||||
continue
|
||||
|
||||
# Search in model name
|
||||
model_name = config.get("model_name", "").lower()
|
||||
if search_term in model_name:
|
||||
filtered[name] = config
|
||||
continue
|
||||
|
||||
# Search in base URL
|
||||
base_url = config.get("base_url", "").lower()
|
||||
if search_term in base_url:
|
||||
filtered[name] = config
|
||||
continue
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def add_configuration_interactive():
|
||||
"""Interactively add a new configuration"""
|
||||
print("\nAdd New Configuration")
|
||||
@@ -247,6 +287,122 @@ def test_configuration():
|
||||
print(f"❌ {message}")
|
||||
|
||||
|
||||
def search_configurations():
|
||||
"""Search/filter configurations"""
|
||||
configs = config_manager.get_configurations()
|
||||
if not configs:
|
||||
print("No configurations to search.")
|
||||
return
|
||||
|
||||
search_term = input("Search term (name, provider, model, URL): ").strip()
|
||||
if not search_term:
|
||||
print("Search cancelled.")
|
||||
return
|
||||
|
||||
filtered = filter_configurations(configs, search_term)
|
||||
|
||||
if not filtered:
|
||||
print(f"No configurations found matching '{search_term}'.")
|
||||
else:
|
||||
print_configurations(filtered, f"Search Results for '{search_term}'")
|
||||
|
||||
|
||||
def export_configurations():
|
||||
"""Export configurations to JSON or YAML"""
|
||||
configs = config_manager.get_configurations()
|
||||
if not configs:
|
||||
print("No configurations to export.")
|
||||
return
|
||||
|
||||
print("\nExport Configurations")
|
||||
print("=" * 30)
|
||||
|
||||
# Select format
|
||||
print("Select export format:")
|
||||
print("1. JSON")
|
||||
print("2. YAML")
|
||||
|
||||
format_choice = input("Enter choice (1-2): ").strip()
|
||||
if format_choice not in ["1", "2"]:
|
||||
print("Invalid choice.")
|
||||
return
|
||||
|
||||
# Get filename
|
||||
filename = input("Filename (without extension): ").strip()
|
||||
if not filename:
|
||||
print("Cancelled.")
|
||||
return
|
||||
|
||||
# Export configurations
|
||||
try:
|
||||
if format_choice == "1": # JSON
|
||||
export_file = Path(f"{filename}.json")
|
||||
with open(export_file, 'w') as f:
|
||||
json.dump(configs, f, indent=2)
|
||||
else: # YAML
|
||||
export_file = Path(f"{filename}.yaml")
|
||||
with open(export_file, 'w') as f:
|
||||
yaml.dump(configs, f, default_flow_style=False)
|
||||
|
||||
print(f"✅ Configurations exported to {export_file}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error exporting configurations: {e}")
|
||||
|
||||
|
||||
def import_configurations():
|
||||
"""Import configurations from JSON or YAML"""
|
||||
print("\nImport Configurations")
|
||||
print("=" * 30)
|
||||
|
||||
# Get filename
|
||||
filename = input("Filename (with extension .json or .yaml): ").strip()
|
||||
if not filename:
|
||||
print("Cancelled.")
|
||||
return
|
||||
|
||||
# Check if file exists
|
||||
file_path = Path(filename)
|
||||
if not file_path.exists():
|
||||
print(f"❌ Error: File '{filename}' not found.")
|
||||
return
|
||||
|
||||
# Import configurations
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
if filename.endswith('.json'):
|
||||
imported_configs = json.load(f)
|
||||
elif filename.endswith(('.yaml', '.yml')):
|
||||
imported_configs = yaml.safe_load(f)
|
||||
else:
|
||||
print("❌ Error: Unsupported file format. Use .json or .yaml")
|
||||
return
|
||||
|
||||
# Validate imported configurations
|
||||
if not isinstance(imported_configs, dict):
|
||||
print("❌ Error: Invalid configuration format.")
|
||||
return
|
||||
|
||||
# Ask for confirmation
|
||||
confirm = input(f"Import {len(imported_configs)} configurations? (y/N): ").strip().lower()
|
||||
if confirm != 'y':
|
||||
print("Cancelled.")
|
||||
return
|
||||
|
||||
# Import configurations
|
||||
imported_count = 0
|
||||
for name, config in imported_configs.items():
|
||||
try:
|
||||
config_manager.add_configuration(name, config)
|
||||
imported_count += 1
|
||||
print(f"✅ Imported configuration: {name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Warning: Could not import '{name}': {e}")
|
||||
|
||||
print(f"\n✅ Successfully imported {imported_count} configurations.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error importing configurations: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point"""
|
||||
parser = argparse.ArgumentParser(description="MaxClaw Agent Configuration Manager")
|
||||
@@ -255,6 +411,10 @@ def main():
|
||||
# List command
|
||||
subparsers.add_parser('list', help='List all configurations')
|
||||
|
||||
# Search command
|
||||
search_parser = subparsers.add_parser('search', help='Search configurations')
|
||||
search_parser.add_argument('term', nargs='?', help='Search term')
|
||||
|
||||
# Add command
|
||||
add_parser = subparsers.add_parser('add', help='Add a new configuration')
|
||||
add_parser.add_argument('--non-interactive', action='store_true',
|
||||
@@ -281,11 +441,33 @@ def main():
|
||||
test_parser = subparsers.add_parser('test', help='Test connection to a configuration')
|
||||
test_parser.add_argument('name', nargs='?', help='Configuration name to test')
|
||||
|
||||
# Export command
|
||||
export_parser = subparsers.add_parser('export', help='Export configurations to JSON/YAML')
|
||||
export_parser.add_argument('--format', choices=['json', 'yaml'], help='Export format')
|
||||
export_parser.add_argument('--output', help='Output filename (without extension)')
|
||||
|
||||
# Import command
|
||||
import_parser = subparsers.add_parser('import', help='Import configurations from JSON/YAML')
|
||||
import_parser.add_argument('file', nargs='?', help='File to import')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'list':
|
||||
print_configurations()
|
||||
|
||||
elif args.command == 'search':
|
||||
if args.term:
|
||||
# Non-interactive search
|
||||
configs = config_manager.get_configurations()
|
||||
filtered = filter_configurations(configs, args.term)
|
||||
if not filtered:
|
||||
print(f"No configurations found matching '{args.term}'.")
|
||||
else:
|
||||
print_configurations(filtered, f"Search Results for '{args.term}'")
|
||||
else:
|
||||
# Interactive search
|
||||
search_configurations()
|
||||
|
||||
elif args.command == 'add':
|
||||
if args.non_interactive:
|
||||
# Non-interactive mode
|
||||
@@ -368,6 +550,73 @@ def main():
|
||||
# Interactive test
|
||||
test_configuration()
|
||||
|
||||
elif args.command == 'export':
|
||||
if args.format and args.output:
|
||||
# Non-interactive export
|
||||
configs = config_manager.get_configurations()
|
||||
if not configs:
|
||||
print("No configurations to export.")
|
||||
return 1
|
||||
|
||||
try:
|
||||
if args.format == 'json':
|
||||
export_file = Path(f"{args.output}.json")
|
||||
with open(export_file, 'w') as f:
|
||||
json.dump(configs, f, indent=2)
|
||||
else: # yaml
|
||||
export_file = Path(f"{args.output}.yaml")
|
||||
with open(export_file, 'w') as f:
|
||||
yaml.dump(configs, f, default_flow_style=False)
|
||||
|
||||
print(f"✅ Configurations exported to {export_file}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error exporting configurations: {e}")
|
||||
return 1
|
||||
else:
|
||||
# Interactive export
|
||||
export_configurations()
|
||||
|
||||
elif args.command == 'import':
|
||||
if args.file:
|
||||
# Non-interactive import
|
||||
file_path = Path(args.file)
|
||||
if not file_path.exists():
|
||||
print(f"❌ Error: File '{args.file}' not found.")
|
||||
return 1
|
||||
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
if args.file.endswith('.json'):
|
||||
imported_configs = json.load(f)
|
||||
elif args.file.endswith(('.yaml', '.yml')):
|
||||
imported_configs = yaml.safe_load(f)
|
||||
else:
|
||||
print("❌ Error: Unsupported file format. Use .json or .yaml")
|
||||
return 1
|
||||
|
||||
# Validate imported configurations
|
||||
if not isinstance(imported_configs, dict):
|
||||
print("❌ Error: Invalid configuration format.")
|
||||
return 1
|
||||
|
||||
# Import configurations
|
||||
imported_count = 0
|
||||
for name, config in imported_configs.items():
|
||||
try:
|
||||
config_manager.add_configuration(name, config)
|
||||
imported_count += 1
|
||||
print(f"✅ Imported configuration: {name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Warning: Could not import '{name}': {e}")
|
||||
|
||||
print(f"\n✅ Successfully imported {imported_count} configurations.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error importing configurations: {e}")
|
||||
return 1
|
||||
else:
|
||||
# Interactive import
|
||||
import_configurations()
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
+289
-10
@@ -1,19 +1,22 @@
|
||||
"""
|
||||
Text User Interface for MaxClaw Agent configuration management
|
||||
Enhanced with search/filter, export/import, and connection testing
|
||||
"""
|
||||
import curses
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
from config_manager import config_manager
|
||||
|
||||
|
||||
def draw_menu(stdscr, selected_row_idx, options):
|
||||
def draw_menu(stdscr, selected_row_idx, options, title="MaxClaw Agent Configuration TUI"):
|
||||
"""Draw a menu with options"""
|
||||
stdscr.clear()
|
||||
h, w = stdscr.getmaxyx()
|
||||
|
||||
# Title
|
||||
title = "MaxClaw Agent Configuration TUI"
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, (w - len(title)) // 2, title)
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
@@ -87,20 +90,61 @@ def show_message(stdscr, message, wait_for_key=True):
|
||||
stdscr.getch()
|
||||
|
||||
|
||||
def filter_configurations(configs: Dict[str, Any], search_term: str) -> Dict[str, Any]:
|
||||
"""Filter configurations based on search term"""
|
||||
if not search_term:
|
||||
return configs
|
||||
|
||||
search_term = search_term.lower()
|
||||
filtered = {}
|
||||
|
||||
for name, config in configs.items():
|
||||
# Search in name
|
||||
if search_term in name.lower():
|
||||
filtered[name] = config
|
||||
continue
|
||||
|
||||
# Search in provider
|
||||
provider = config.get("provider", "").lower()
|
||||
if search_term in provider:
|
||||
filtered[name] = config
|
||||
continue
|
||||
|
||||
# Search in model name
|
||||
model_name = config.get("model_name", "").lower()
|
||||
if search_term in model_name:
|
||||
filtered[name] = config
|
||||
continue
|
||||
|
||||
# Search in base URL
|
||||
base_url = config.get("base_url", "").lower()
|
||||
if search_term in base_url:
|
||||
filtered[name] = config
|
||||
continue
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def main_tui(stdscr):
|
||||
"""Main TUI function"""
|
||||
# Initialize colors if available
|
||||
if curses.has_colors():
|
||||
curses.start_color()
|
||||
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
||||
curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||||
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||
|
||||
# Main menu options
|
||||
main_options = [
|
||||
"View Configurations",
|
||||
"Search Configurations",
|
||||
"Add Configuration",
|
||||
"Remove Configuration",
|
||||
"Set Active Configuration",
|
||||
"View Active Configuration",
|
||||
"Test Configuration",
|
||||
"Export Configurations",
|
||||
"Import Configurations",
|
||||
"Exit"
|
||||
]
|
||||
|
||||
@@ -119,30 +163,38 @@ def main_tui(stdscr):
|
||||
# Enter key pressed
|
||||
if current_selection == 0: # View Configurations
|
||||
view_configurations_tui(stdscr)
|
||||
elif current_selection == 1: # Add Configuration
|
||||
elif current_selection == 1: # Search Configurations
|
||||
search_configurations_tui(stdscr)
|
||||
elif current_selection == 2: # Add Configuration
|
||||
add_configuration_tui(stdscr)
|
||||
elif current_selection == 2: # Remove Configuration
|
||||
elif current_selection == 3: # Remove Configuration
|
||||
remove_configuration_tui(stdscr)
|
||||
elif current_selection == 3: # Set Active Configuration
|
||||
elif current_selection == 4: # Set Active Configuration
|
||||
set_active_configuration_tui(stdscr)
|
||||
elif current_selection == 4: # View Active Configuration
|
||||
elif current_selection == 5: # View Active Configuration
|
||||
view_active_configuration_tui(stdscr)
|
||||
elif current_selection == 5: # Exit
|
||||
elif current_selection == 6: # Test Configuration
|
||||
test_configuration_tui(stdscr)
|
||||
elif current_selection == 7: # Export Configurations
|
||||
export_configurations_tui(stdscr)
|
||||
elif current_selection == 8: # Import Configurations
|
||||
import_configurations_tui(stdscr)
|
||||
elif current_selection == 9: # Exit
|
||||
break
|
||||
elif key == ord('q') or key == ord('Q'):
|
||||
break
|
||||
|
||||
|
||||
def view_configurations_tui(stdscr):
|
||||
def view_configurations_tui(stdscr, configs=None, title="Configurations"):
|
||||
"""View all configurations in TUI"""
|
||||
configs = config_manager.get_configurations()
|
||||
if configs is None:
|
||||
configs = config_manager.get_configurations()
|
||||
active = config_manager.get_active_configuration()
|
||||
|
||||
stdscr.clear()
|
||||
h, w = stdscr.getmaxyx()
|
||||
|
||||
# Title
|
||||
title = "Configurations"
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, (w - len(title)) // 2, title)
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
@@ -174,6 +226,35 @@ def view_configurations_tui(stdscr):
|
||||
stdscr.getch()
|
||||
|
||||
|
||||
def search_configurations_tui(stdscr):
|
||||
"""Search/filter configurations"""
|
||||
stdscr.clear()
|
||||
h, w = stdscr.getmaxyx()
|
||||
|
||||
# Title
|
||||
title = "Search Configurations"
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, (w - len(title)) // 2, title)
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
|
||||
# Get search term
|
||||
search_term = get_string_input(stdscr, "Search term (name, provider, model, URL): ", 2, 0, 50)
|
||||
|
||||
if not search_term:
|
||||
show_message(stdscr, "Search cancelled.")
|
||||
return
|
||||
|
||||
# Get all configurations and filter
|
||||
all_configs = config_manager.get_configurations()
|
||||
filtered_configs = filter_configurations(all_configs, search_term)
|
||||
|
||||
# Show filtered results
|
||||
if not filtered_configs:
|
||||
show_message(stdscr, f"No configurations found matching '{search_term}'.")
|
||||
else:
|
||||
view_configurations_tui(stdscr, filtered_configs, f"Search Results for '{search_term}'")
|
||||
|
||||
|
||||
def add_configuration_tui(stdscr):
|
||||
"""Add a new configuration via TUI"""
|
||||
stdscr.clear()
|
||||
@@ -480,6 +561,204 @@ def view_active_configuration_tui(stdscr):
|
||||
stdscr.getch()
|
||||
|
||||
|
||||
def test_configuration_tui(stdscr):
|
||||
"""Test connection to a configuration"""
|
||||
configs = config_manager.get_configurations()
|
||||
if not configs:
|
||||
show_message(stdscr, "No configurations to test.")
|
||||
return
|
||||
|
||||
stdscr.clear()
|
||||
h, w = stdscr.getmaxyx()
|
||||
|
||||
# Title
|
||||
title = "Test Configuration Connection"
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, (w - len(title)) // 2, title)
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
|
||||
# List configurations
|
||||
config_names = list(configs.keys())
|
||||
selection = 0
|
||||
|
||||
while True:
|
||||
stdscr.clear()
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, (w - len(title)) // 2, title)
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
|
||||
stdscr.addstr(2, 0, "Select configuration to test:")
|
||||
|
||||
for idx, name in enumerate(config_names):
|
||||
y = 3 + idx
|
||||
if y >= h - 4: # Prevent overflow
|
||||
break
|
||||
|
||||
if idx == selection:
|
||||
stdscr.attron(curses.A_REVERSE)
|
||||
stdscr.addstr(y, 2, f"> {name}")
|
||||
stdscr.attroff(curses.A_REVERSE)
|
||||
else:
|
||||
stdscr.addstr(y, 2, f" {name}")
|
||||
|
||||
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to test, 'c' to cancel")
|
||||
stdscr.refresh()
|
||||
|
||||
key = stdscr.getch()
|
||||
if key == curses.KEY_UP and selection > 0:
|
||||
selection -= 1
|
||||
elif key == curses.KEY_DOWN and selection < len(config_names) - 1:
|
||||
selection += 1
|
||||
elif key == curses.KEY_ENTER or key in [10, 13]:
|
||||
# Test configuration
|
||||
name_to_test = config_names[selection]
|
||||
show_message(stdscr, f"Testing connection to '{name_to_test}'...", False)
|
||||
|
||||
success, message = config_manager.test_configuration_connection(name_to_test)
|
||||
|
||||
if success:
|
||||
show_message(stdscr, f"✅ {message}")
|
||||
else:
|
||||
show_message(stdscr, f"❌ {message}")
|
||||
return
|
||||
elif key == ord('c') or key == ord('C'):
|
||||
show_message(stdscr, "Cancelled.")
|
||||
return
|
||||
|
||||
|
||||
def export_configurations_tui(stdscr):
|
||||
"""Export configurations to JSON or YAML"""
|
||||
configs = config_manager.get_configurations()
|
||||
if not configs:
|
||||
show_message(stdscr, "No configurations to export.")
|
||||
return
|
||||
|
||||
stdscr.clear()
|
||||
h, w = stdscr.getmaxyx()
|
||||
|
||||
# Title
|
||||
title = "Export Configurations"
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, (w - len(title)) // 2, title)
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
|
||||
# Select format
|
||||
formats = ["JSON", "YAML"]
|
||||
format_selection = 0
|
||||
|
||||
while True:
|
||||
stdscr.clear()
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, (w - len(title)) // 2, title)
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
|
||||
stdscr.addstr(2, 0, "Select export format:")
|
||||
|
||||
for idx, fmt in enumerate(formats):
|
||||
y = 3 + idx
|
||||
if idx == format_selection:
|
||||
stdscr.attron(curses.A_REVERSE)
|
||||
stdscr.addstr(y, 2, f"> {fmt}")
|
||||
stdscr.attroff(curses.A_REVERSE)
|
||||
else:
|
||||
stdscr.addstr(y, 2, f" {fmt}")
|
||||
|
||||
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm, 'c' to cancel")
|
||||
stdscr.refresh()
|
||||
|
||||
key = stdscr.getch()
|
||||
if key == curses.KEY_UP and format_selection > 0:
|
||||
format_selection -= 1
|
||||
elif key == curses.KEY_DOWN and format_selection < len(formats) - 1:
|
||||
format_selection += 1
|
||||
elif key == curses.KEY_ENTER or key in [10, 13]:
|
||||
break
|
||||
elif key == ord('c') or key == ord('C'):
|
||||
show_message(stdscr, "Cancelled.")
|
||||
return
|
||||
|
||||
# Get filename
|
||||
filename = get_string_input(stdscr, "Filename (without extension): ", 5, 0, 30)
|
||||
if not filename:
|
||||
show_message(stdscr, "Cancelled.")
|
||||
return
|
||||
|
||||
# Export configurations
|
||||
try:
|
||||
if format_selection == 0: # JSON
|
||||
export_file = Path(f"{filename}.json")
|
||||
with open(export_file, 'w') as f:
|
||||
json.dump(configs, f, indent=2)
|
||||
else: # YAML
|
||||
export_file = Path(f"{filename}.yaml")
|
||||
with open(export_file, 'w') as f:
|
||||
yaml.dump(configs, f, default_flow_style=False)
|
||||
|
||||
show_message(stdscr, f"Configurations exported to {export_file}")
|
||||
except Exception as e:
|
||||
show_message(stdscr, f"Error exporting configurations: {e}")
|
||||
|
||||
|
||||
def import_configurations_tui(stdscr):
|
||||
"""Import configurations from JSON or YAML"""
|
||||
stdscr.clear()
|
||||
h, w = stdscr.getmaxyx()
|
||||
|
||||
# Title
|
||||
title = "Import Configurations"
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, (w - len(title)) // 2, title)
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
|
||||
# Get filename
|
||||
filename = get_string_input(stdscr, "Filename (with extension .json or .yaml): ", 2, 0, 50)
|
||||
if not filename:
|
||||
show_message(stdscr, "Cancelled.")
|
||||
return
|
||||
|
||||
# Check if file exists
|
||||
file_path = Path(filename)
|
||||
if not file_path.exists():
|
||||
show_message(stdscr, f"Error: File '{filename}' not found.")
|
||||
return
|
||||
|
||||
# Import configurations
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
if filename.endswith('.json'):
|
||||
imported_configs = json.load(f)
|
||||
elif filename.endswith(('.yaml', '.yml')):
|
||||
imported_configs = yaml.safe_load(f)
|
||||
else:
|
||||
show_message(stdscr, "Error: Unsupported file format. Use .json or .yaml")
|
||||
return
|
||||
|
||||
# Validate imported configurations
|
||||
if not isinstance(imported_configs, dict):
|
||||
show_message(stdscr, "Error: Invalid configuration format.")
|
||||
return
|
||||
|
||||
# Ask for confirmation
|
||||
confirm_prompt = f"Import {len(imported_configs)} configurations? (y/n): "
|
||||
confirm = get_string_input(stdscr, confirm_prompt, 4, 0, 1)
|
||||
if confirm.lower() != 'y':
|
||||
show_message(stdscr, "Cancelled.")
|
||||
return
|
||||
|
||||
# Import configurations
|
||||
imported_count = 0
|
||||
for name, config in imported_configs.items():
|
||||
try:
|
||||
config_manager.add_configuration(name, config)
|
||||
imported_count += 1
|
||||
except Exception as e:
|
||||
show_message(stdscr, f"Warning: Could not import '{name}': {e}")
|
||||
|
||||
show_message(stdscr, f"Successfully imported {imported_count} configurations.")
|
||||
except Exception as e:
|
||||
show_message(stdscr, f"Error importing configurations: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the TUI"""
|
||||
curses.wrapper(main_tui)
|
||||
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
"""
|
||||
Performance monitoring dashboard for MaxClaw Agent
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
import threading
|
||||
|
||||
# Add current directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
class PerformanceDashboard:
|
||||
"""Performance monitoring dashboard"""
|
||||
|
||||
def __init__(self, data_dir: Optional[str] = None):
|
||||
"""
|
||||
Initialize performance dashboard
|
||||
|
||||
Args:
|
||||
data_dir: Directory for storing performance data
|
||||
"""
|
||||
if data_dir is None:
|
||||
self.data_dir = Path.home() / ".maxclaw" / "performance"
|
||||
else:
|
||||
self.data_dir = Path(data_dir)
|
||||
|
||||
# Create data directory
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Performance metrics
|
||||
self.metrics = {
|
||||
"requests": [],
|
||||
"errors": [],
|
||||
"response_times": [],
|
||||
"tool_usage": {},
|
||||
"provider_usage": {},
|
||||
"system_stats": []
|
||||
}
|
||||
|
||||
# Load existing data
|
||||
self._load_data()
|
||||
|
||||
# Start background monitoring
|
||||
self._start_monitoring()
|
||||
|
||||
def _load_data(self):
|
||||
"""Load existing performance data"""
|
||||
data_file = self.data_dir / "performance_data.json"
|
||||
|
||||
if data_file.exists():
|
||||
try:
|
||||
with open(data_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
self.metrics.update(data)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load performance data: {e}")
|
||||
|
||||
def _save_data(self):
|
||||
"""Save performance data to file"""
|
||||
data_file = self.data_dir / "performance_data.json"
|
||||
|
||||
try:
|
||||
with open(data_file, 'w') as f:
|
||||
json.dump(self.metrics, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not save performance data: {e}")
|
||||
|
||||
def _start_monitoring(self):
|
||||
"""Start background monitoring thread"""
|
||||
def monitor():
|
||||
while True:
|
||||
time.sleep(60) # Collect data every minute
|
||||
self._collect_system_stats()
|
||||
self._save_data()
|
||||
|
||||
thread = threading.Thread(target=monitor, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def _collect_system_stats(self):
|
||||
"""Collect system statistics"""
|
||||
try:
|
||||
import psutil
|
||||
|
||||
stats = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"cpu_percent": psutil.cpu_percent(interval=1),
|
||||
"memory_percent": psutil.virtual_memory().percent,
|
||||
"disk_usage": psutil.disk_usage('/').percent,
|
||||
"network_connections": len(psutil.net_connections())
|
||||
}
|
||||
|
||||
self.metrics["system_stats"].append(stats)
|
||||
|
||||
# Keep only last 1000 entries
|
||||
if len(self.metrics["system_stats"]) > 1000:
|
||||
self.metrics["system_stats"] = self.metrics["system_stats"][-1000:]
|
||||
|
||||
except ImportError:
|
||||
# psutil not available, skip system stats
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not collect system stats: {e}")
|
||||
|
||||
def record_request(self, endpoint: str, method: str, status_code: int, duration: float):
|
||||
"""Record an API request"""
|
||||
request = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"endpoint": endpoint,
|
||||
"method": method,
|
||||
"status_code": status_code,
|
||||
"duration": duration
|
||||
}
|
||||
|
||||
self.metrics["requests"].append(request)
|
||||
|
||||
# Keep only last 10000 requests
|
||||
if len(self.metrics["requests"]) > 10000:
|
||||
self.metrics["requests"] = self.metrics["requests"][-10000:]
|
||||
|
||||
def record_error(self, error_type: str, error_message: str, context: Optional[str] = None):
|
||||
"""Record an error"""
|
||||
error = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"type": error_type,
|
||||
"message": error_message,
|
||||
"context": context
|
||||
}
|
||||
|
||||
self.metrics["errors"].append(error)
|
||||
|
||||
# Keep only last 1000 errors
|
||||
if len(self.metrics["errors"]) > 1000:
|
||||
self.metrics["errors"] = self.metrics["errors"][-1000:]
|
||||
|
||||
def record_response_time(self, operation: str, duration: float):
|
||||
"""Record response time for an operation"""
|
||||
response_time = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"operation": operation,
|
||||
"duration": duration
|
||||
}
|
||||
|
||||
self.metrics["response_times"].append(response_time)
|
||||
|
||||
# Keep only last 5000 response times
|
||||
if len(self.metrics["response_times"]) > 5000:
|
||||
self.metrics["response_times"] = self.metrics["response_times"][-5000:]
|
||||
|
||||
def record_tool_usage(self, tool_name: str, success: bool, duration: float):
|
||||
"""Record tool usage"""
|
||||
if tool_name not in self.metrics["tool_usage"]:
|
||||
self.metrics["tool_usage"][tool_name] = {
|
||||
"total_calls": 0,
|
||||
"successful_calls": 0,
|
||||
"failed_calls": 0,
|
||||
"total_duration": 0,
|
||||
"avg_duration": 0
|
||||
}
|
||||
|
||||
tool_stats = self.metrics["tool_usage"][tool_name]
|
||||
tool_stats["total_calls"] += 1
|
||||
tool_stats["total_duration"] += duration
|
||||
|
||||
if success:
|
||||
tool_stats["successful_calls"] += 1
|
||||
else:
|
||||
tool_stats["failed_calls"] += 1
|
||||
|
||||
tool_stats["avg_duration"] = tool_stats["total_duration"] / tool_stats["total_calls"]
|
||||
|
||||
def record_provider_usage(self, provider: str, model: str, success: bool):
|
||||
"""Record provider usage"""
|
||||
key = f"{provider}:{model}"
|
||||
|
||||
if key not in self.metrics["provider_usage"]:
|
||||
self.metrics["provider_usage"][key] = {
|
||||
"total_calls": 0,
|
||||
"successful_calls": 0,
|
||||
"failed_calls": 0
|
||||
}
|
||||
|
||||
provider_stats = self.metrics["provider_usage"][key]
|
||||
provider_stats["total_calls"] += 1
|
||||
|
||||
if success:
|
||||
provider_stats["successful_calls"] += 1
|
||||
else:
|
||||
provider_stats["failed_calls"] += 1
|
||||
|
||||
def get_summary(self, hours: int = 24) -> Dict[str, Any]:
|
||||
"""
|
||||
Get performance summary for the last N hours
|
||||
|
||||
Args:
|
||||
hours: Number of hours to include in summary
|
||||
|
||||
Returns:
|
||||
Dictionary with performance summary
|
||||
"""
|
||||
cutoff_time = datetime.now() - timedelta(hours=hours)
|
||||
|
||||
# Filter recent data
|
||||
recent_requests = [
|
||||
r for r in self.metrics["requests"]
|
||||
if datetime.fromisoformat(r["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
recent_errors = [
|
||||
e for e in self.metrics["errors"]
|
||||
if datetime.fromisoformat(e["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
recent_response_times = [
|
||||
rt for rt in self.metrics["response_times"]
|
||||
if datetime.fromisoformat(rt["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
# Calculate statistics
|
||||
total_requests = len(recent_requests)
|
||||
total_errors = len(recent_errors)
|
||||
|
||||
if total_requests > 0:
|
||||
error_rate = total_errors / total_requests
|
||||
avg_response_time = sum(r["duration"] for r in recent_requests) / total_requests
|
||||
else:
|
||||
error_rate = 0
|
||||
avg_response_time = 0
|
||||
|
||||
# Status code distribution
|
||||
status_codes = {}
|
||||
for request in recent_requests:
|
||||
code = request["status_code"]
|
||||
status_codes[code] = status_codes.get(code, 0) + 1
|
||||
|
||||
# Endpoint usage
|
||||
endpoints = {}
|
||||
for request in recent_requests:
|
||||
endpoint = request["endpoint"]
|
||||
endpoints[endpoint] = endpoints.get(endpoint, 0) + 1
|
||||
|
||||
# Tool usage summary
|
||||
tool_summary = {}
|
||||
for tool_name, stats in self.metrics["tool_usage"].items():
|
||||
tool_summary[tool_name] = {
|
||||
"total_calls": stats["total_calls"],
|
||||
"success_rate": stats["successful_calls"] / stats["total_calls"] if stats["total_calls"] > 0 else 0,
|
||||
"avg_duration": stats["avg_duration"]
|
||||
}
|
||||
|
||||
# Provider usage summary
|
||||
provider_summary = {}
|
||||
for provider_key, stats in self.metrics["provider_usage"].items():
|
||||
provider_summary[provider_key] = {
|
||||
"total_calls": stats["total_calls"],
|
||||
"success_rate": stats["successful_calls"] / stats["total_calls"] if stats["total_calls"] > 0 else 0
|
||||
}
|
||||
|
||||
# System stats (if available)
|
||||
system_stats = {}
|
||||
if self.metrics["system_stats"]:
|
||||
latest_stats = self.metrics["system_stats"][-1]
|
||||
system_stats = {
|
||||
"cpu_percent": latest_stats.get("cpu_percent", 0),
|
||||
"memory_percent": latest_stats.get("memory_percent", 0),
|
||||
"disk_usage": latest_stats.get("disk_usage", 0)
|
||||
}
|
||||
|
||||
return {
|
||||
"period_hours": hours,
|
||||
"total_requests": total_requests,
|
||||
"total_errors": total_errors,
|
||||
"error_rate": error_rate,
|
||||
"avg_response_time": avg_response_time,
|
||||
"status_codes": status_codes,
|
||||
"endpoints": endpoints,
|
||||
"tool_usage": tool_summary,
|
||||
"provider_usage": provider_summary,
|
||||
"system_stats": system_stats,
|
||||
"generated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def get_detailed_report(self, hours: int = 24) -> str:
|
||||
"""
|
||||
Get detailed performance report
|
||||
|
||||
Args:
|
||||
hours: Number of hours to include in report
|
||||
|
||||
Returns:
|
||||
Formatted report string
|
||||
"""
|
||||
summary = self.get_summary(hours)
|
||||
|
||||
report = f"📊 Performance Report (Last {hours} hours)\n"
|
||||
report += "=" * 50 + "\n\n"
|
||||
|
||||
# Request statistics
|
||||
report += f"📈 Request Statistics:\n"
|
||||
report += f" Total Requests: {summary['total_requests']}\n"
|
||||
report += f" Total Errors: {summary['total_errors']}\n"
|
||||
report += f" Error Rate: {summary['error_rate']:.2%}\n"
|
||||
report += f" Avg Response Time: {summary['avg_response_time']:.3f}s\n\n"
|
||||
|
||||
# Status codes
|
||||
if summary['status_codes']:
|
||||
report += f"📋 Status Code Distribution:\n"
|
||||
for code, count in sorted(summary['status_codes'].items()):
|
||||
report += f" {code}: {count}\n"
|
||||
report += "\n"
|
||||
|
||||
# Top endpoints
|
||||
if summary['endpoints']:
|
||||
report += f"🔗 Top Endpoints:\n"
|
||||
sorted_endpoints = sorted(summary['endpoints'].items(), key=lambda x: x[1], reverse=True)[:10]
|
||||
for endpoint, count in sorted_endpoints:
|
||||
report += f" {endpoint}: {count}\n"
|
||||
report += "\n"
|
||||
|
||||
# Tool usage
|
||||
if summary['tool_usage']:
|
||||
report += f"🔧 Tool Usage:\n"
|
||||
for tool_name, stats in summary['tool_usage'].items():
|
||||
report += f" {tool_name}:\n"
|
||||
report += f" Calls: {stats['total_calls']}\n"
|
||||
report += f" Success Rate: {stats['success_rate']:.2%}\n"
|
||||
report += f" Avg Duration: {stats['avg_duration']:.3f}s\n"
|
||||
report += "\n"
|
||||
|
||||
# Provider usage
|
||||
if summary['provider_usage']:
|
||||
report += f"🤖 Provider Usage:\n"
|
||||
for provider_key, stats in summary['provider_usage'].items():
|
||||
report += f" {provider_key}:\n"
|
||||
report += f" Calls: {stats['total_calls']}\n"
|
||||
report += f" Success Rate: {stats['success_rate']:.2%}\n"
|
||||
report += "\n"
|
||||
|
||||
# System stats
|
||||
if summary['system_stats']:
|
||||
report += f"💻 System Statistics:\n"
|
||||
report += f" CPU Usage: {summary['system_stats']['cpu_percent']}%\n"
|
||||
report += f" Memory Usage: {summary['system_stats']['memory_percent']}%\n"
|
||||
report += f" Disk Usage: {summary['system_stats']['disk_usage']}%\n"
|
||||
|
||||
return report
|
||||
|
||||
def clear_old_data(self, days: int = 30):
|
||||
"""
|
||||
Clear data older than specified days
|
||||
|
||||
Args:
|
||||
days: Number of days to keep data
|
||||
"""
|
||||
cutoff_time = datetime.now() - timedelta(days=days)
|
||||
|
||||
# Filter out old data
|
||||
self.metrics["requests"] = [
|
||||
r for r in self.metrics["requests"]
|
||||
if datetime.fromisoformat(r["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
self.metrics["errors"] = [
|
||||
e for e in self.metrics["errors"]
|
||||
if datetime.fromisoformat(e["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
self.metrics["response_times"] = [
|
||||
rt for rt in self.metrics["response_times"]
|
||||
if datetime.fromisoformat(rt["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
self.metrics["system_stats"] = [
|
||||
s for s in self.metrics["system_stats"]
|
||||
if datetime.fromisoformat(s["timestamp"]) > cutoff_time
|
||||
]
|
||||
|
||||
# Save cleaned data
|
||||
self._save_data()
|
||||
|
||||
print(f"✅ Cleared data older than {days} days")
|
||||
|
||||
|
||||
# Global performance dashboard instance
|
||||
performance_dashboard = PerformanceDashboard()
|
||||
|
||||
|
||||
def get_performance_summary(hours: int = 24) -> Dict[str, Any]:
|
||||
"""
|
||||
Get performance summary
|
||||
|
||||
Args:
|
||||
hours: Number of hours to include
|
||||
|
||||
Returns:
|
||||
Performance summary dictionary
|
||||
"""
|
||||
return performance_dashboard.get_summary(hours)
|
||||
|
||||
|
||||
def get_performance_report(hours: int = 24) -> str:
|
||||
"""
|
||||
Get formatted performance report
|
||||
|
||||
Args:
|
||||
hours: Number of hours to include
|
||||
|
||||
Returns:
|
||||
Formatted report string
|
||||
"""
|
||||
return performance_dashboard.get_detailed_report(hours)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Print performance report when run directly
|
||||
print(get_performance_report())
|
||||
@@ -0,0 +1,53 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
maxclaw:
|
||||
build: .
|
||||
container_name: maxclaw-agent
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# Mount configuration directory for persistence
|
||||
- maxclaw-config:/root/.maxclaw
|
||||
# Mount current directory for development (optional)
|
||||
# - .:/app
|
||||
environment:
|
||||
# Model configuration
|
||||
- MODEL_PROVIDER=${MODEL_PROVIDER:-ollama}
|
||||
- MODEL_NAME=${MODEL_NAME:-gemma2:9b}
|
||||
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
|
||||
- OPENROUTER_BASE_URL=${OPENROUTER_BASE_URL:-https://openrouter.ai/api/v1}
|
||||
|
||||
# Proxmox configuration (optional)
|
||||
- PROXMOX_HOST=${PROXMOX_HOST:-}
|
||||
- PROXMOX_USER=${PROXMOX_USER:-root}
|
||||
- PROXMOX_PASSWORD=${PROXMOX_PASSWORD:-}
|
||||
- PROXMOX_TOKEN=${PROXMOX_TOKEN:-}
|
||||
networks:
|
||||
- maxclaw-network
|
||||
# Interactive mode for terminal usage
|
||||
stdin_open: true
|
||||
tty: true
|
||||
|
||||
# Optional: Ollama service (if you want to run Ollama in Docker)
|
||||
# ollama:
|
||||
# image: ollama/ollama:latest
|
||||
# container_name: ollama
|
||||
# restart: unless-stopped
|
||||
# volumes:
|
||||
# - ollama-data:/root/.ollama
|
||||
# ports:
|
||||
# - "11434:11434"
|
||||
# networks:
|
||||
# - maxclaw-network
|
||||
|
||||
volumes:
|
||||
maxclaw-config:
|
||||
driver: local
|
||||
# ollama-data:
|
||||
# driver: local
|
||||
|
||||
networks:
|
||||
maxclaw-network:
|
||||
driver: bridge
|
||||
Whitespace-only changes.
@@ -0,0 +1,409 @@
|
||||
# MaxClaw Agent: Architettura e Guida all'Utilizzo
|
||||
|
||||
## Cos'è MaxClaw Agent?
|
||||
|
||||
MaxClaw Agent è un **agente AI intelligente** che può essere configurato per lavorare con diversi modelli di intelligenza artificiale (come GPT-4o di OpenAI, modelli locali con Ollama, o Claude tramite OpenRouter). In pratica, è come avere un assistente virtuale programmabile che può:
|
||||
|
||||
- **Leggere e scrivere file** sul tuo computer
|
||||
- **Eseguire comandi** di sistema
|
||||
- **Pianificare attività complesse** scomponendole in passi più semplici
|
||||
- **Delegare compiti** a sotto-agenti specializzati
|
||||
- **Gestire configurazioni** in modo sicuro
|
||||
|
||||
## Architettura del Sistema
|
||||
|
||||
### Componenti Principali
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ MaxClaw Agent │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. Agente Principale (main.py) │
|
||||
│ - Coordina tutto il sistema │
|
||||
│ - Gestisce le conversazioni │
|
||||
│ - Carica la configurazione │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 2. Sistema di Configurazione │
|
||||
│ - config_manager.py: Gestisce le configurazioni │
|
||||
│ - config_cli.py: Interfaccia a riga di comando │
|
||||
│ - config_tui.py: Interfaccia grafica testuale │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 3. Strumenti (Tools) │
|
||||
│ - Strumenti unificati per provider │
|
||||
│ - Strumenti per Proxmox │
|
||||
│ - Strumenti per Ollama │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 4. Provider LLM │
|
||||
│ - OpenAI (GPT-4o, GPT-4o-mini, ecc.) │
|
||||
│ - Ollama (modelli locali) │
|
||||
│ - OpenRouter (Claude, altri modelli) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Come Funziona il Flusso
|
||||
|
||||
1. **L'utente scrive un messaggio** (es. "Elenca i file nella directory corrente")
|
||||
2. **L'agente analizza la richiesta** e decide quali strumenti usare
|
||||
3. **Esegue gli strumenti necessari** (es. ls, read_file, ecc.)
|
||||
4. **Elabora i risultati** e genera una risposta
|
||||
5. **Restituisce la risposta** all'utente
|
||||
|
||||
## Esempi Concreti di Utilizzo
|
||||
|
||||
### Esempio 1: Gestione Base dei File
|
||||
|
||||
**Scenario**: Vuoi organizzare i file nella tua directory Downloads.
|
||||
|
||||
**Domanda**: "Analizza i file nella mia directory Downloads e suggeriscimi come organizzarli per tipo"
|
||||
|
||||
**Cosa fa l'agente**:
|
||||
1. Usa lo strumento `ls` per elencare i file
|
||||
2. Usa lo strumento `read_file` per leggere alcuni file
|
||||
3. Analizza i tipi di file (documenti, immagini, video, ecc.)
|
||||
4. Suggerisce una struttura di cartelle
|
||||
5. Può anche creare le cartelle e spostare i file se autorizzato
|
||||
|
||||
**Risposta dell'agente**:
|
||||
```
|
||||
Ho analizzato la tua directory Downloads. Ecco cosa ho trovato:
|
||||
|
||||
📁 File trovati:
|
||||
- 15 documenti PDF
|
||||
- 8 immagini JPG
|
||||
- 3 video MP4
|
||||
- 5 file ZIP
|
||||
- 2 file eseguibili
|
||||
|
||||
💡 Suggerimento di organizzazione:
|
||||
Downloads/
|
||||
├── Documenti/
|
||||
├── Immagini/
|
||||
├── Video/
|
||||
├── Archivi/
|
||||
└── Software/
|
||||
|
||||
Vuoi che crei questa struttura e sposti i file?
|
||||
```
|
||||
|
||||
### Esempio 2: Analisi di Codice
|
||||
|
||||
**Scenario**: Hai un progetto Python e vuoi trovare errori.
|
||||
|
||||
**Domanda**: "Controlla se ci sono errori di sintassi nei file Python nella directory src/"
|
||||
|
||||
**Cosa fa l'agente**:
|
||||
1. Usa `ls` per trovare tutti i file .py
|
||||
2. Usa `read_file` per leggere ogni file
|
||||
3. Analizza la sintassi di ogni file
|
||||
4. Segnala eventuali errori
|
||||
|
||||
**Risposta dell'agente**:
|
||||
```
|
||||
Ho analizzato 12 file Python nella directory src/.
|
||||
|
||||
✅ File senza errori:
|
||||
- main.py
|
||||
- utils.py
|
||||
- config.py
|
||||
|
||||
❌ File con errori:
|
||||
- database.py: Errore di sintassi alla riga 45 (mancano due punti)
|
||||
- api.py: Indentazione errata alla riga 78
|
||||
|
||||
Vuoi che corregga questi errori automaticamente?
|
||||
```
|
||||
|
||||
### Esempio 3: Gestione di Server con Proxmox
|
||||
|
||||
**Scenario**: Vuoi controllare le tue macchine virtuali su Proxmox.
|
||||
|
||||
**Domanda**: "Mostrami lo stato di tutte le VM nel mio cluster Proxmox"
|
||||
|
||||
**Cosa fa l'agente**:
|
||||
1. Usa lo strumento `proxmox_list_vms`
|
||||
2. Recupera informazioni su tutte le VM
|
||||
3. Formatta i dati in modo leggibile
|
||||
|
||||
**Risposta dell'agente**:
|
||||
```
|
||||
Ecco lo stato delle tue VM nel cluster Proxmox:
|
||||
|
||||
🖥️ VM 100: web-server
|
||||
- Stato: ✅ In esecuzione
|
||||
- CPU: 15%
|
||||
- Memoria: 2.1 GB / 4 GB
|
||||
- Uptime: 5 giorni, 3 ore
|
||||
|
||||
🖥️ VM 101: database-server
|
||||
- Stato: ✅ In esecuzione
|
||||
- CPU: 8%
|
||||
- Memoria: 1.5 GB / 2 GB
|
||||
- Uptime: 5 giorni, 3 ore
|
||||
|
||||
🖥️ VM 102: dev-machine
|
||||
- Stato: ⏸️ Sospesa
|
||||
- CPU: 0%
|
||||
- Memoria: 0 GB / 8 GB
|
||||
|
||||
Vuoi avviare la VM 102 o vedere dettagli su una specifica VM?
|
||||
```
|
||||
|
||||
### Esempio 4: Gestione Modelli AI con Ollama
|
||||
|
||||
**Scenario**: Vuoi vedere quali modelli AI hai disponibili localmente.
|
||||
|
||||
**Domanda**: "Quali modelli ho su Ollama? Scarica anche il modello llama2:13b"
|
||||
|
||||
**Cosa fa l'agente**:
|
||||
1. Usa `ollama_list_models` per vedere i modelli disponibili
|
||||
2. Usa `ollama_pull_model` per scaricare il nuovo modello
|
||||
3. Mostra il progresso del download
|
||||
|
||||
**Risposta dell'agente**:
|
||||
```
|
||||
Modelli disponibili su Ollama (http://192.168.0.50:11434):
|
||||
|
||||
📦 Modelli installati:
|
||||
- gemma2:9b (5.2 GB)
|
||||
- llama2:7b (3.8 GB)
|
||||
- mistral:7b (4.1 GB)
|
||||
|
||||
⬇️ Download di llama2:13b in corso...
|
||||
[██████████░░░░░░░░░░] 45% - 2.3 GB / 5.1 GB
|
||||
|
||||
Il download richiederà circa 5 minuti.
|
||||
Vuoi che ti avvisi quando è completato?
|
||||
```
|
||||
|
||||
### Esempio 5: Creazione di uno Script Automatico
|
||||
|
||||
**Scenario**: Vuoi creare uno script che faccia backup automatici.
|
||||
|
||||
**Domanda**: "Crea uno script bash che faccia backup incrementali della directory /home/user/documents ogni giorno alle 2:00"
|
||||
|
||||
**Cosa fa l'agente**:
|
||||
1. Usa `write_file` per creare lo script
|
||||
2. Usa `execute` per renderlo eseguibile
|
||||
3. Fornisce istruzioni per l'automazione
|
||||
|
||||
**Risposta dell'agente**:
|
||||
```
|
||||
Ho creato lo script di backup:
|
||||
|
||||
📁 File creati:
|
||||
- backup_script.sh (script principale)
|
||||
- backup_cron.txt (configurazione per cron)
|
||||
|
||||
📋 Contenuto di backup_script.sh:
|
||||
#!/bin/bash
|
||||
# Script di backup incrementale
|
||||
SOURCE="/home/user/documents"
|
||||
DEST="/backup/documents"
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
rsync -avz --delete "$SOURCE" "$DEST/$DATE/"
|
||||
|
||||
🔧 Per automatizzare:
|
||||
1. chmod +x backup_script.sh
|
||||
2. crontab -e
|
||||
3. Aggiungi: 0 2 * * * /path/to/backup_script.sh
|
||||
|
||||
Vuoi che esegua il backup adesso per testarlo?
|
||||
```
|
||||
|
||||
### Esempio 6: Ricerca e Analisi di Dati
|
||||
|
||||
**Scenario**: Hai un file CSV con dati di vendite e vuoi analizzarlo.
|
||||
|
||||
**Domanda**: "Analizza il file vendite_2024.csv e dimmi quali sono i prodotti più venduti"
|
||||
|
||||
**Cosa fa l'agente**:
|
||||
1. Usa `read_file` per leggere il file CSV
|
||||
2. Analizza i dati con Python
|
||||
3. Calcola statistiche e crea grafici
|
||||
|
||||
**Risposta dell'agente**:
|
||||
```
|
||||
Ho analizzato il file vendite_2024.csv (1.234 righe).
|
||||
|
||||
📊 Top 5 prodotti più venduti:
|
||||
1. Smartphone X - 150 unità - €75.000
|
||||
2. Laptop Pro - 120 unità - €180.000
|
||||
3. Cuffie Wireless - 200 unità - €10.000
|
||||
4. Tablet Mini - 80 unità - €32.000
|
||||
5. Smartwatch - 95 unità - €19.000
|
||||
|
||||
📈 Statistiche:
|
||||
- Vendite totali: €316.000
|
||||
- Media per prodotto: €63.200
|
||||
- Mese migliore: Dicembre (€45.000)
|
||||
|
||||
Ho creato un grafico in vendite_analysis.png.
|
||||
Vuoi che esporti questi dati in un report PDF?
|
||||
```
|
||||
|
||||
### Esempio 7: Configurazione Multi-Provider
|
||||
|
||||
**Scenario**: Vuoi confrontare le risposte di diversi modelli AI.
|
||||
|
||||
**Domanda**: "Confronta come GPT-4o e Claude 3.5 rispondono alla domanda 'Spiega la relatività in modo semplice'"
|
||||
|
||||
**Cosa fa l'agente**:
|
||||
1. Usa `ollama_set_default_model` per configurare Ollama
|
||||
2. Usa `openai_set_default_model` per configurare OpenAI
|
||||
3. Invia la stessa domanda a entrambi
|
||||
4. Confronta le risposte
|
||||
|
||||
**Risposta dell'agente**:
|
||||
```
|
||||
Ho configurato i due provider e inviato la domanda.
|
||||
|
||||
🤖 Risposta di GPT-4o (OpenAI):
|
||||
"La relatività è come avere due orologi: uno sulla Terra e uno su un'astronave veloce. Quando l'astronave viaggia vicino alla velocità della luce, il suo orologio rallenta rispetto a quello sulla Terra. Questo perché il tempo è relativo alla velocità..."
|
||||
|
||||
🤖 Risposta di Claude 3.5 (OpenRouter):
|
||||
"Immagina di essere su un treno che passa vicino a un altro treno fermo. Se il tuo treno si muove, sembra che l'altro treno si stia muovendo al contrario. La relatività dice che il movimento è sempre relativo a chi osserva..."
|
||||
|
||||
📊 Confronto:
|
||||
- GPT-4o: Più tecnico, usa esempi pratici
|
||||
- Claude 3.5: Più intuitivo, usa analogie semplici
|
||||
|
||||
Vuoi che approfondisca qualche aspetto specifico?
|
||||
```
|
||||
|
||||
### Esempio 8: Automazione di Task Complessi
|
||||
|
||||
**Scenario**: Vuoi preparare un ambiente di sviluppo completo.
|
||||
|
||||
**Domanda**: "Prepara un ambiente di sviluppo Python con virtualenv, installa le dipendenze, e configura Git"
|
||||
|
||||
**Cosa fa l'agente**:
|
||||
1. Usa `execute` per creare virtualenv
|
||||
2. Usa `write_file` per creare requirements.txt
|
||||
3. Usa `execute` per installare pacchetti
|
||||
4. Configura Git con i tuoi dati
|
||||
|
||||
**Risposta dell'agente**:
|
||||
```
|
||||
Ho preparato l'ambiente di sviluppo:
|
||||
|
||||
✅ Virtualenv creato: .venv
|
||||
✅ Requirements.txt creato con:
|
||||
- flask==2.3.0
|
||||
- requests==2.31.0
|
||||
- pandas==2.0.0
|
||||
- pytest==7.4.0
|
||||
|
||||
✅ Dipendenze installate (15 pacchetti)
|
||||
✅ Git configurato:
|
||||
- Nome: Mario Rossi
|
||||
- Email: mario@example.com
|
||||
- Branch predefinito: main
|
||||
|
||||
📁 Struttura creata:
|
||||
progetto/
|
||||
├── .venv/
|
||||
├── src/
|
||||
├── tests/
|
||||
├── requirements.txt
|
||||
└── .gitignore
|
||||
|
||||
Per attivare l'ambiente: source .venv/bin/activate
|
||||
Vuoi che crei anche un file README.md?
|
||||
```
|
||||
|
||||
## Come Iniziare
|
||||
|
||||
### 1. Configurazione Base
|
||||
```bash
|
||||
# Configura Ollama (modello locale)
|
||||
export MODEL_PROVIDER=ollama
|
||||
export MODEL_NAME=gemma2:9b
|
||||
export OLLAMA_BASE_URL=http://192.168.0.50:11434
|
||||
|
||||
# Avvia l'agente
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 2. Prima Conversazione
|
||||
```
|
||||
👤 Tu: Ciao, chi sei?
|
||||
🤖 MaxClaw: Sono MaxClaw, un agente AI configurabile. Posso aiutarti a gestire file, eseguire comandi, analizzare dati e molto altro. Come posso aiutarti oggi?
|
||||
|
||||
👤 Tu: Mostrami i file nella directory corrente
|
||||
🤖 MaxClaw: [Esegue ls e mostra i file]
|
||||
|
||||
👤 Tu: Crea un file di testo con "Hello World"
|
||||
🤖 MaxClaw: [Crea hello.txt con il contenuto specificato]
|
||||
```
|
||||
|
||||
### 3. Configurazione Avanzata
|
||||
```bash
|
||||
# Configurazione persistente
|
||||
python config_cli.py add --name "mio-ollama" \
|
||||
--provider "ollama" \
|
||||
--model "gemma2:9b" \
|
||||
--base-url "http://192.168.0.50:11434" \
|
||||
--set-active
|
||||
|
||||
# Testa la connessione
|
||||
python config_cli.py test "mio-ollama"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Sicurezza
|
||||
- **Non condividere mai le chiavi API** in chiaro
|
||||
- **Usa la configurazione persistente** per evitare di esporre credenziali
|
||||
- **Testa prima di eseguire** comandi distruttivi
|
||||
|
||||
### 2. Performance
|
||||
- **Usa modelli locali** (Ollama) per task semplici
|
||||
- **Usa modelli cloud** (OpenAI, OpenRouter) per task complessi
|
||||
- **Configura timeout** appropriati per le connessioni
|
||||
|
||||
### 3. Organizzazione
|
||||
- **Usa configurazioni nominate** per diversi ambienti
|
||||
- **Esporta regolarmente** le configurazioni come backup
|
||||
- **Documenta le tue configurazioni** personalizzate
|
||||
|
||||
## Risoluzione Problemi
|
||||
|
||||
### Problema: "Connessione rifiutata"
|
||||
**Soluzione**: Verifica che il servizio sia in esecuzione
|
||||
```bash
|
||||
# Per Ollama
|
||||
curl http://192.168.0.50:11434
|
||||
|
||||
# Per Proxmox
|
||||
ping 192.168.0.100
|
||||
```
|
||||
|
||||
### Problema: "Modello non trovato"
|
||||
**Soluzione**: Verifica che il modello sia disponibile
|
||||
```bash
|
||||
# Per Ollama
|
||||
ollama list
|
||||
|
||||
# Per OpenAI
|
||||
python config_cli.py test "openai-config"
|
||||
```
|
||||
|
||||
### Problema: "Permessi insufficienti"
|
||||
**Soluzione**: Verifica i permessi dei file
|
||||
```bash
|
||||
ls -la ~/.maxclaw/
|
||||
chmod 600 ~/.maxclaw/config.json
|
||||
```
|
||||
|
||||
## Prossimi Passi
|
||||
|
||||
1. **Sperimenta con diversi provider**: Prova OpenAI, Ollama, OpenRouter
|
||||
2. **Crea strumenti personalizzati**: Aggiungi funzionalità specifiche per il tuo ambiente
|
||||
3. **Automatizza task ripetitivi**: Usa l'agente per attività quotidiane
|
||||
4. **Contribuisci al progetto**: Segnala bug, suggerisci miglioramenti
|
||||
|
||||
## Conclusione
|
||||
|
||||
MaxClaw Agent è uno strumento potente e flessibile che può adattarsi a molteplici esigenze. Che tu voglia gestire file, analizzare dati, amministrare server o sperimentare con modelli AI, questo agente può aiutarti a automatizzare e semplificare il tuo lavoro.
|
||||
|
||||
La chiave è iniziare con task semplici e gradualmente esplorare funzionalità più avanzate man mano che acquisisci familiarità con il sistema.
|
||||
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
Internationalization (i18n) support for MaxClaw Agent
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class I18n:
|
||||
"""Internationalization manager for MaxClaw Agent"""
|
||||
|
||||
def __init__(self, default_language: str = "en"):
|
||||
"""
|
||||
Initialize i18n manager
|
||||
|
||||
Args:
|
||||
default_language: Default language code (e.g., "en", "it", "es")
|
||||
"""
|
||||
self.default_language = default_language
|
||||
self.current_language = default_language
|
||||
self.translations: Dict[str, Dict[str, str]] = {}
|
||||
self.i18n_dir = Path(__file__).parent
|
||||
|
||||
# Load translations
|
||||
self._load_translations()
|
||||
|
||||
def _load_translations(self):
|
||||
"""Load all translation files"""
|
||||
# Load default language
|
||||
self._load_language(self.default_language)
|
||||
|
||||
# Try to load current language from environment
|
||||
env_lang = os.getenv("MAXCLAW_LANGUAGE")
|
||||
if env_lang:
|
||||
self.set_language(env_lang)
|
||||
|
||||
def _load_language(self, language: str) -> bool:
|
||||
"""
|
||||
Load translations for a specific language
|
||||
|
||||
Args:
|
||||
language: Language code
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
translation_file = self.i18n_dir / f"{language}.json"
|
||||
|
||||
if not translation_file.exists():
|
||||
print(f"⚠️ Translation file not found: {translation_file}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(translation_file, 'r', encoding='utf-8') as f:
|
||||
self.translations[language] = json.load(f)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Error loading translations for {language}: {e}")
|
||||
return False
|
||||
|
||||
def set_language(self, language: str) -> bool:
|
||||
"""
|
||||
Set the current language
|
||||
|
||||
Args:
|
||||
language: Language code
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Load translations if not already loaded
|
||||
if language not in self.translations:
|
||||
if not self._load_language(language):
|
||||
return False
|
||||
|
||||
self.current_language = language
|
||||
return True
|
||||
|
||||
def get_language(self) -> str:
|
||||
"""
|
||||
Get the current language
|
||||
|
||||
Returns:
|
||||
Current language code
|
||||
"""
|
||||
return self.current_language
|
||||
|
||||
def get_available_languages(self) -> list:
|
||||
"""
|
||||
Get list of available languages
|
||||
|
||||
Returns:
|
||||
List of language codes
|
||||
"""
|
||||
languages = []
|
||||
|
||||
# Find all JSON files in i18n directory
|
||||
for file_path in self.i18n_dir.glob("*.json"):
|
||||
language = file_path.stem
|
||||
languages.append(language)
|
||||
|
||||
return languages
|
||||
|
||||
def translate(self, key: str, **kwargs) -> str:
|
||||
"""
|
||||
Translate a key to the current language
|
||||
|
||||
Args:
|
||||
key: Translation key
|
||||
**kwargs: Format arguments
|
||||
|
||||
Returns:
|
||||
Translated string
|
||||
"""
|
||||
# Get translation from current language
|
||||
if self.current_language in self.translations:
|
||||
translation = self.translations[self.current_language].get(key)
|
||||
if translation:
|
||||
# Format with kwargs if provided
|
||||
if kwargs:
|
||||
try:
|
||||
return translation.format(**kwargs)
|
||||
except KeyError:
|
||||
return translation
|
||||
return translation
|
||||
|
||||
# Fallback to default language
|
||||
if self.default_language in self.translations:
|
||||
translation = self.translations[self.default_language].get(key)
|
||||
if translation:
|
||||
if kwargs:
|
||||
try:
|
||||
return translation.format(**kwargs)
|
||||
except KeyError:
|
||||
return translation
|
||||
return translation
|
||||
|
||||
# Return key if no translation found
|
||||
return key
|
||||
|
||||
def t(self, key: str, **kwargs) -> str:
|
||||
"""
|
||||
Shorthand for translate()
|
||||
|
||||
Args:
|
||||
key: Translation key
|
||||
**kwargs: Format arguments
|
||||
|
||||
Returns:
|
||||
Translated string
|
||||
"""
|
||||
return self.translate(key, **kwargs)
|
||||
|
||||
|
||||
# Global i18n instance
|
||||
i18n = I18n()
|
||||
|
||||
|
||||
def set_language(language: str) -> bool:
|
||||
"""
|
||||
Set the global language
|
||||
|
||||
Args:
|
||||
language: Language code
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
return i18n.set_language(language)
|
||||
|
||||
|
||||
def get_language() -> str:
|
||||
"""
|
||||
Get the current global language
|
||||
|
||||
Returns:
|
||||
Current language code
|
||||
"""
|
||||
return i18n.get_language()
|
||||
|
||||
|
||||
def translate(key: str, **kwargs) -> str:
|
||||
"""
|
||||
Translate a key using the global i18n instance
|
||||
|
||||
Args:
|
||||
key: Translation key
|
||||
**kwargs: Format arguments
|
||||
|
||||
Returns:
|
||||
Translated string
|
||||
"""
|
||||
return i18n.translate(key, **kwargs)
|
||||
|
||||
|
||||
def t(key: str, **kwargs) -> str:
|
||||
"""
|
||||
Shorthand for translate()
|
||||
|
||||
Args:
|
||||
key: Translation key
|
||||
**kwargs: Format arguments
|
||||
|
||||
Returns:
|
||||
Translated string
|
||||
"""
|
||||
return i18n.t(key, **kwargs)
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"app.name": "MaxClaw Agent",
|
||||
"app.description": "A configurable agent harness using DeepAgents",
|
||||
|
||||
"commands.help": "Show available commands",
|
||||
"commands.status": "Show system status",
|
||||
"commands.config.list": "List all configurations",
|
||||
"commands.config.active": "Show active configuration",
|
||||
"commands.config.test": "Test connection to active configuration",
|
||||
"commands.config.use": "Change active configuration",
|
||||
"commands.models": "List available models for current provider",
|
||||
"commands.clear": "Clear the screen",
|
||||
"commands.plugins": "List available plugins",
|
||||
"commands.exit": "Exit the agent",
|
||||
|
||||
"status.title": "System Status",
|
||||
"status.provider": "Provider",
|
||||
"status.model": "Model",
|
||||
"status.ollama_url": "Ollama URL",
|
||||
"status.active_config": "Active configuration",
|
||||
"status.total_configs": "Total configurations",
|
||||
"status.custom_tools": "Custom tools",
|
||||
"status.available": "Available",
|
||||
"status.not_available": "Not available",
|
||||
|
||||
"config.title": "Available Configurations",
|
||||
"config.active_marker": "✅",
|
||||
"config.inactive_marker": " ",
|
||||
"config.no_configs": "No configurations found.",
|
||||
"config.active_title": "Active Configuration",
|
||||
"config.no_active": "No active configuration set.",
|
||||
"config.test_title": "Testing connection to",
|
||||
"config.test_success": "Connection successful",
|
||||
"config.test_failed": "Connection failed",
|
||||
"config.changed": "Configuration changed to",
|
||||
"config.not_found": "Configuration not found",
|
||||
|
||||
"models.title": "Available Models",
|
||||
"models.ollama_hint": "To list Ollama models, ask the agent:",
|
||||
"models.ollama_example": "List available models on Ollama",
|
||||
"models.openai_models": "Common OpenAI models:",
|
||||
"models.openrouter_hint": "To list OpenRouter models, ask the agent:",
|
||||
"models.openrouter_example": "List available models on OpenRouter",
|
||||
"models.unknown_provider": "Unknown provider",
|
||||
|
||||
"plugins.title": "Available Plugins",
|
||||
"plugins.name": "Name",
|
||||
"plugins.version": "Version",
|
||||
"plugins.description": "Description",
|
||||
"plugins.status": "Status",
|
||||
"plugins.enabled": "Enabled",
|
||||
"plugins.disabled": "Disabled",
|
||||
"plugins.no_plugins": "No plugins found.",
|
||||
|
||||
"agent.initializing": "Initializing MaxClaw Agent...",
|
||||
"agent.initialized": "Agent initialized successfully!",
|
||||
"agent.tools.available": "Available tools:",
|
||||
"agent.tools.provider": "Provider tools: list models, test connections, set defaults",
|
||||
"agent.tools.proxmox": "Proxmox tools: manage VMs, snapshots, resources",
|
||||
"agent.tools.standard": "Standard tools: planning, filesystem, shell, subagents",
|
||||
"agent.chat.ready": "You can now interact with the agent.",
|
||||
"agent.chat.commands": "Special commands (prefixed with /):",
|
||||
"agent.chat.hint": "To chat, simply write your message.",
|
||||
"agent.error.init": "Failed to initialize agent",
|
||||
"agent.error.chat": "Error",
|
||||
|
||||
"goodbye": "Goodbye!",
|
||||
|
||||
"errors.config_not_available": "Configuration manager not available.",
|
||||
"errors.no_active_config": "No active configuration to test.",
|
||||
"errors.config_not_found": "Configuration not found.",
|
||||
"errors.unknown_command": "Unknown command",
|
||||
"errors.command_help": "Type /help to see available commands.",
|
||||
"errors.subcommand_required": "Specify a subcommand: list, active, test, use <name>",
|
||||
"errors.unknown_subcommand": "Unknown subcommand",
|
||||
"errors.subcommands_available": "Available subcommands: list, active, test, use <name>"
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"app.name": "MaxClaw Agent",
|
||||
"app.description": "Un agente configurabile basato su DeepAgents",
|
||||
|
||||
"commands.help": "Mostra i comandi disponibili",
|
||||
"commands.status": "Mostra lo stato del sistema",
|
||||
"commands.config.list": "Elenca tutte le configurazioni",
|
||||
"commands.config.active": "Mostra la configurazione attiva",
|
||||
"commands.config.test": "Testa la connessione alla configurazione attiva",
|
||||
"commands.config.use": "Cambia la configurazione attiva",
|
||||
"commands.models": "Elenca i modelli disponibili per il provider attivo",
|
||||
"commands.clear": "Pulisce lo schermo",
|
||||
"commands.plugins": "Elenca i plugin disponibili",
|
||||
"commands.exit": "Esci dall'agente",
|
||||
|
||||
"status.title": "Stato del Sistema",
|
||||
"status.provider": "Provider",
|
||||
"status.model": "Modello",
|
||||
"status.ollama_url": "URL Ollama",
|
||||
"status.active_config": "Configurazione attiva",
|
||||
"status.total_configs": "Configurazioni totali",
|
||||
"status.custom_tools": "Strumenti personalizzati",
|
||||
"status.available": "Disponibili",
|
||||
"status.not_available": "Non disponibili",
|
||||
|
||||
"config.title": "Configurazioni disponibili",
|
||||
"config.active_marker": "✅",
|
||||
"config.inactive_marker": " ",
|
||||
"config.no_configs": "Nessuna configurazione trovata.",
|
||||
"config.active_title": "Configurazione attiva",
|
||||
"config.no_active": "Nessuna configurazione attiva impostata.",
|
||||
"config.test_title": "Test connessione a",
|
||||
"config.test_success": "Connessione riuscita",
|
||||
"config.test_failed": "Connessione fallita",
|
||||
"config.changed": "Configurazione cambiata in",
|
||||
"config.not_found": "Configurazione non trovata",
|
||||
|
||||
"models.title": "Modelli disponibili",
|
||||
"models.ollama_hint": "Per elencare i modelli Ollama, chiedi all'agente:",
|
||||
"models.ollama_example": "Elenca i modelli disponibili su Ollama",
|
||||
"models.openai_models": "Modelli OpenAI comuni:",
|
||||
"models.openrouter_hint": "Per elencare i modelli OpenRouter, chiedi all'agente:",
|
||||
"models.openrouter_example": "Elenca i modelli disponibili su OpenRouter",
|
||||
"models.unknown_provider": "Provider sconosciuto",
|
||||
|
||||
"plugins.title": "Plugin disponibili",
|
||||
"plugins.name": "Nome",
|
||||
"plugins.version": "Versione",
|
||||
"plugins.description": "Descrizione",
|
||||
"plugins.status": "Stato",
|
||||
"plugins.enabled": "Abilitato",
|
||||
"plugins.disabled": "Disabilitato",
|
||||
"plugins.no_plugins": "Nessun plugin trovato.",
|
||||
|
||||
"agent.initializing": "Inizializzazione MaxClaw Agent...",
|
||||
"agent.initialized": "Agente inizializzato con successo!",
|
||||
"agent.tools.available": "Strumenti disponibili:",
|
||||
"agent.tools.provider": "Strumenti provider: elenca modelli, testa connessioni, imposta predefiniti",
|
||||
"agent.tools.proxmox": "Strumenti Proxmox: gestisci VM, snapshot, risorse",
|
||||
"agent.tools.standard": "Strumenti standard: pianificazione, filesystem, shell, subagenti",
|
||||
"agent.chat.ready": "Ora puoi interagire con l'agente.",
|
||||
"agent.chat.commands": "Comandi speciali (prefissati con /):",
|
||||
"agent.chat.hint": "Per chattare, scrivi semplicemente il tuo messaggio.",
|
||||
"agent.error.init": "Impossibile inizializzare l'agente",
|
||||
"agent.error.chat": "Errore",
|
||||
|
||||
"goodbye": "Arrivederci!",
|
||||
|
||||
"errors.config_not_available": "Gestore configurazioni non disponibile.",
|
||||
"errors.no_active_config": "Nessuna configurazione attiva da testare.",
|
||||
"errors.config_not_found": "Configurazione non trovata.",
|
||||
"errors.unknown_command": "Comando sconosciuto",
|
||||
"errors.command_help": "Digita /help per vedere i comandi disponibili.",
|
||||
"errors.subcommand_required": "Specifica un sottocomando: list, active, test, use <name>",
|
||||
"errors.unknown_subcommand": "Sottocomando sconosciuto",
|
||||
"errors.subcommands_available": "Sottocomandi disponibili: list, active, test, use <name>"
|
||||
}
|
||||
@@ -2,13 +2,32 @@
|
||||
"""
|
||||
MaxClaw Agent - A configurable agent harness using DeepAgents
|
||||
Supports multiple LLM providers via LangChain's init_chat_model
|
||||
Enhanced with unified provider tools and custom tools
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
from typing import Optional, List, Dict, Any
|
||||
from deepagents import create_deep_agent
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from langchain.tools import Tool
|
||||
|
||||
# Import custom tools
|
||||
try:
|
||||
from tools import get_proxmox_tools
|
||||
from tools.provider_tools import get_provider_tools, get_all_provider_tools
|
||||
TOOLS_AVAILABLE = True
|
||||
except ImportError:
|
||||
TOOLS_AVAILABLE = False
|
||||
print("Warning: Custom tools not available. Install required dependencies.")
|
||||
|
||||
# Import configuration manager
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
CONFIG_AVAILABLE = True
|
||||
except ImportError:
|
||||
CONFIG_AVAILABLE = False
|
||||
print("Warning: Configuration manager not available.")
|
||||
|
||||
|
||||
def load_model_from_config():
|
||||
@@ -53,6 +72,48 @@ def load_model_from_config():
|
||||
return init_chat_model(f"openai:{model_name}")
|
||||
|
||||
|
||||
def get_custom_tools() -> List[Tool]:
|
||||
"""
|
||||
Get custom tools for the MaxClaw agent
|
||||
|
||||
Returns:
|
||||
List of custom tools
|
||||
"""
|
||||
custom_tools = []
|
||||
|
||||
if not TOOLS_AVAILABLE:
|
||||
return custom_tools
|
||||
|
||||
# Add Proxmox tools if configured
|
||||
proxmox_host = os.getenv("PROXMOX_HOST")
|
||||
if proxmox_host:
|
||||
proxmox_user = os.getenv("PROXMOX_USER", "root")
|
||||
proxmox_password = os.getenv("PROXMOX_PASSWORD")
|
||||
proxmox_token = os.getenv("PROXMOX_TOKEN")
|
||||
|
||||
try:
|
||||
proxmox_tools = get_proxmox_tools(
|
||||
host=proxmox_host,
|
||||
user=proxmox_user,
|
||||
password=proxmox_password,
|
||||
api_token=proxmox_token
|
||||
)
|
||||
custom_tools.extend(proxmox_tools)
|
||||
print(f"✅ Added {len(proxmox_tools)} Proxmox tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load Proxmox tools: {e}")
|
||||
|
||||
# Add unified provider tools for all configured providers
|
||||
try:
|
||||
provider_tools = get_all_provider_tools()
|
||||
custom_tools.extend(provider_tools)
|
||||
print(f"✅ Added {len(provider_tools)} provider tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load provider tools: {e}")
|
||||
|
||||
return custom_tools
|
||||
|
||||
|
||||
def create_maxclaw_agent(system_prompt: Optional[str] = None):
|
||||
"""
|
||||
Create and return a DeepAgent instance with configurable model.
|
||||
@@ -60,16 +121,254 @@ def create_maxclaw_agent(system_prompt: Optional[str] = None):
|
||||
if system_prompt is None:
|
||||
system_prompt = """You are MaxClaw, a helpful AI agent that can plan, use tools,
|
||||
manage context, and delegate to subagents. You are designed to be configurable
|
||||
to work with various LLM providers."""
|
||||
to work with various LLM providers.
|
||||
|
||||
You have access to:
|
||||
1. Unified provider tools for managing LLM providers (list models, test connections, etc.)
|
||||
2. Custom tools for managing Proxmox virtual machines
|
||||
3. Standard DeepAgents tools (planning, filesystem, shell, subagents)
|
||||
|
||||
Use these tools when asked about:
|
||||
- LLM provider management (listing models, testing connections, setting defaults)
|
||||
- System administration and virtualization
|
||||
- AI model management and configuration"""
|
||||
|
||||
model = load_model_from_config()
|
||||
|
||||
# Get custom tools
|
||||
custom_tools = get_custom_tools()
|
||||
|
||||
# Create the agent with custom tools
|
||||
agent = create_deep_agent(
|
||||
model=model,
|
||||
tools=custom_tools, # Add custom tools to the agent
|
||||
system_prompt=system_prompt
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
def show_help():
|
||||
"""Show available commands"""
|
||||
print("""
|
||||
📖 Comandi disponibili (prefissati con /):
|
||||
|
||||
/help, /? - Mostra questo messaggio di aiuto
|
||||
/status - Mostra lo stato del sistema
|
||||
/config list - Elenca tutte le configurazioni
|
||||
/config active - Mostra la configurazione attiva
|
||||
/config test - Testa la connessione alla configurazione attiva
|
||||
/config use <name> - Cambia la configurazione attiva
|
||||
/models - Elenca i modelli disponibili per il provider attivo
|
||||
/clear - Pulisce lo schermo
|
||||
/exit, /quit - Esci dall'agente
|
||||
|
||||
💡 Per chattare con l'agente, scrivi semplicemente il tuo messaggio senza "/".
|
||||
Esempio: "Ciao, chi sei?" oppure "Elenca i file nella directory corrente"
|
||||
""")
|
||||
|
||||
|
||||
def show_status():
|
||||
"""Show system status"""
|
||||
print("\n📊 Stato del Sistema:")
|
||||
print("=" * 40)
|
||||
|
||||
# Show provider info
|
||||
provider = os.getenv("MODEL_PROVIDER", "non configurato")
|
||||
model = os.getenv("MODEL_NAME", "non configurato")
|
||||
print(f"Provider: {provider}")
|
||||
print(f"Modello: {model}")
|
||||
|
||||
# Show Ollama URL if configured
|
||||
if provider == "ollama":
|
||||
ollama_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
||||
print(f"Ollama URL: {ollama_url}")
|
||||
|
||||
# Show configuration status
|
||||
if CONFIG_AVAILABLE:
|
||||
active_config = config_manager.get_active_configuration()
|
||||
if active_config:
|
||||
print(f"Configurazione attiva: {active_config}")
|
||||
else:
|
||||
print("Configurazione attiva: Nessuna")
|
||||
|
||||
configs = config_manager.get_configurations()
|
||||
print(f"Configurazioni totali: {len(configs)}")
|
||||
|
||||
# Show tools status
|
||||
if TOOLS_AVAILABLE:
|
||||
print("Strumenti personalizzati: Disponibili")
|
||||
else:
|
||||
print("Strumenti personalizzati: Non disponibili")
|
||||
|
||||
|
||||
def list_configurations():
|
||||
"""List all configurations"""
|
||||
if not CONFIG_AVAILABLE:
|
||||
print("❌ Gestore configurazioni non disponibile.")
|
||||
return
|
||||
|
||||
configs = config_manager.get_configurations()
|
||||
active = config_manager.get_active_configuration()
|
||||
|
||||
if not configs:
|
||||
print("📭 Nessuna configurazione trovata.")
|
||||
return
|
||||
|
||||
print("\n📋 Configurazioni disponibili:")
|
||||
print("=" * 50)
|
||||
|
||||
for name, config in configs.items():
|
||||
marker = "✅" if name == active else " "
|
||||
provider = config.get("provider", "unknown")
|
||||
model = config.get("model_name", "unknown")
|
||||
print(f"{marker} {name}: {provider} - {model}")
|
||||
|
||||
|
||||
def show_active_configuration():
|
||||
"""Show active configuration"""
|
||||
if not CONFIG_AVAILABLE:
|
||||
print("❌ Gestore configurazioni non disponibile.")
|
||||
return
|
||||
|
||||
active_name = config_manager.get_active_configuration()
|
||||
if not active_name:
|
||||
print("📭 Nessuna configurazione attiva.")
|
||||
return
|
||||
|
||||
config = config_manager.get_configuration(active_name)
|
||||
if not config:
|
||||
print(f"❌ Configurazione '{active_name}' non trovata.")
|
||||
return
|
||||
|
||||
print(f"\n✅ Configurazione attiva: {active_name}")
|
||||
print(f" Provider: {config.get('provider', 'unknown')}")
|
||||
print(f" Modello: {config.get('model_name', 'unknown')}")
|
||||
|
||||
if config.get('base_url'):
|
||||
print(f" URL: {config.get('base_url')}")
|
||||
|
||||
|
||||
def test_active_configuration():
|
||||
"""Test connection to active configuration"""
|
||||
if not CONFIG_AVAILABLE:
|
||||
print("❌ Gestore configurazioni non disponibile.")
|
||||
return
|
||||
|
||||
active_name = config_manager.get_active_configuration()
|
||||
if not active_name:
|
||||
print("📭 Nessuna configurazione attiva da testare.")
|
||||
return
|
||||
|
||||
print(f"\n🔍 Test connessione a '{active_name}'...")
|
||||
success, message = config_manager.test_configuration_connection(active_name)
|
||||
|
||||
if success:
|
||||
print(f"✅ {message}")
|
||||
else:
|
||||
print(f"❌ {message}")
|
||||
|
||||
|
||||
def use_configuration(name: str):
|
||||
"""Set active configuration"""
|
||||
if not CONFIG_AVAILABLE:
|
||||
print("❌ Gestore configurazioni non disponibile.")
|
||||
return
|
||||
|
||||
if not config_manager.configuration_exists(name):
|
||||
print(f"❌ Configurazione '{name}' non trovata.")
|
||||
print("Usa '/config list' per vedere le configurazioni disponibili.")
|
||||
return
|
||||
|
||||
if config_manager.set_active_configuration(name):
|
||||
print(f"✅ Configurazione '{name}' impostata come attiva.")
|
||||
else:
|
||||
print(f"❌ Errore nell'impostare la configurazione '{name}'.")
|
||||
|
||||
|
||||
def list_models():
|
||||
"""List available models for current provider"""
|
||||
provider = os.getenv("MODEL_PROVIDER", "").lower()
|
||||
|
||||
if not provider:
|
||||
print("❌ Nessun provider configurato.")
|
||||
return
|
||||
|
||||
print(f"\n📋 Modelli disponibili per {provider}:")
|
||||
print("=" * 40)
|
||||
|
||||
# This is a simplified version - in a real implementation,
|
||||
# you would call the provider's API to list models
|
||||
if provider == "ollama":
|
||||
print("Per elencare i modelli Ollama, chiedi all'agente:")
|
||||
print(" 'Elenca i modelli disponibili su Ollama'")
|
||||
elif provider == "openai":
|
||||
print("Modelli OpenAI comuni:")
|
||||
print(" - gpt-4o")
|
||||
print(" - gpt-4o-mini")
|
||||
print(" - gpt-4-turbo")
|
||||
print(" - gpt-3.5-turbo")
|
||||
elif provider == "openrouter":
|
||||
print("Per elencare i modelli OpenRouter, chiedi all'agente:")
|
||||
print(" 'Elenca i modelli disponibili su OpenRouter'")
|
||||
else:
|
||||
print(f"Provider '{provider}' non riconosciuto.")
|
||||
|
||||
|
||||
def clear_screen():
|
||||
"""Clear the screen"""
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
|
||||
def process_command(user_input: str) -> bool:
|
||||
"""
|
||||
Process a command starting with "/"
|
||||
|
||||
Returns:
|
||||
True if it was a command, False otherwise
|
||||
"""
|
||||
if not user_input.startswith('/'):
|
||||
return False
|
||||
|
||||
# Remove the "/" and split into command and arguments
|
||||
command_line = user_input[1:].strip()
|
||||
parts = command_line.split()
|
||||
command = parts[0].lower() if parts else ""
|
||||
args = parts[1:] if len(parts) > 1 else []
|
||||
|
||||
# Process commands
|
||||
if command in ['help', '?']:
|
||||
show_help()
|
||||
elif command == 'status':
|
||||
show_status()
|
||||
elif command == 'config':
|
||||
if not args:
|
||||
print("❌ Specifica un sottocomando: list, active, test, use <name>")
|
||||
elif args[0] == 'list':
|
||||
list_configurations()
|
||||
elif args[0] == 'active':
|
||||
show_active_configuration()
|
||||
elif args[0] == 'test':
|
||||
test_active_configuration()
|
||||
elif args[0] == 'use' and len(args) > 1:
|
||||
use_configuration(args[1])
|
||||
else:
|
||||
print(f"❌ Sottocomando sconosciuto: {args[0]}")
|
||||
print("Sottocomandi disponibili: list, active, test, use <name>")
|
||||
elif command == 'models':
|
||||
list_models()
|
||||
elif command == 'clear':
|
||||
clear_screen()
|
||||
elif command in ['exit', 'quit']:
|
||||
print("👋 Goodbye!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"❌ Comando sconosciuto: /{command}")
|
||||
print("Digita /help per vedere i comandi disponibili.")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for the MaxClaw agent.
|
||||
@@ -80,18 +379,31 @@ def main():
|
||||
agent = create_maxclaw_agent()
|
||||
print("✅ Agent initialized successfully!")
|
||||
|
||||
# Simple test interaction
|
||||
print("\n💬 You can now interact with the agent. Type 'exit' to quit.")
|
||||
# Show available tools
|
||||
print("\n🔧 Available tools:")
|
||||
print(" • Provider tools: list models, test connections, set defaults")
|
||||
print(" • Proxmox tools: manage VMs, snapshots, resources")
|
||||
print(" • Standard tools: planning, filesystem, shell, subagents")
|
||||
|
||||
print("\n💬 You can now interact with the agent.")
|
||||
print("📌 Comandi speciali (prefissati con /):")
|
||||
print(" /help - Mostra i comandi disponibili")
|
||||
print(" /status - Mostra lo stato del sistema")
|
||||
print(" /config - Gestisci le configurazioni")
|
||||
print(" /exit - Esci dall'agente")
|
||||
print("\n💡 Per chattare, scrivi semplicemente il tuo messaggio.")
|
||||
|
||||
while True:
|
||||
user_input = input("\n👤 You: ").strip()
|
||||
if user_input.lower() in ['exit', 'quit']:
|
||||
print("👋 Goodbye!")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
# Invoke the agent
|
||||
|
||||
# Check if it's a command (starts with /)
|
||||
if process_command(user_input):
|
||||
continue
|
||||
|
||||
# Otherwise, treat it as a chat message
|
||||
try:
|
||||
result = agent.invoke({"messages": [HumanMessage(content=user_input)]})
|
||||
# Extract the AI's response
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
MaxClaw Agent Marketplace Package
|
||||
Provides plugin marketplace functionality
|
||||
"""
|
||||
|
||||
from .manager import MarketplaceManager, marketplace_manager
|
||||
|
||||
__all__ = [
|
||||
'MarketplaceManager',
|
||||
'marketplace_manager'
|
||||
]
|
||||
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Plugin marketplace manager for MaxClaw Agent
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class MarketplaceManager:
|
||||
"""Manages plugin marketplace for MaxClaw Agent"""
|
||||
|
||||
def __init__(self, marketplace_url: str = "https://api.github.com/repos/maxclaw-agent/plugins/contents"):
|
||||
"""
|
||||
Initialize marketplace manager
|
||||
|
||||
Args:
|
||||
marketplace_url: URL to the plugin marketplace
|
||||
"""
|
||||
self.marketplace_url = marketplace_url
|
||||
self.local_plugins_dir = Path.home() / ".maxclaw" / "plugins"
|
||||
self.marketplace_cache_file = Path.home() / ".maxclaw" / "marketplace_cache.json"
|
||||
|
||||
# Ensure directories exist
|
||||
self.local_plugins_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Load cache
|
||||
self.cache = self._load_cache()
|
||||
|
||||
def _load_cache(self) -> Dict[str, Any]:
|
||||
"""Load marketplace cache"""
|
||||
if self.marketplace_cache_file.exists():
|
||||
try:
|
||||
with open(self.marketplace_cache_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {"plugins": {}, "last_updated": None}
|
||||
|
||||
def _save_cache(self):
|
||||
"""Save marketplace cache"""
|
||||
try:
|
||||
with open(self.marketplace_cache_file, 'w') as f:
|
||||
json.dump(self.cache, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not save marketplace cache: {e}")
|
||||
|
||||
def fetch_marketplace_plugins(self, force_refresh: bool = False) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch plugins from marketplace
|
||||
|
||||
Args:
|
||||
force_refresh: Force refresh of cache
|
||||
|
||||
Returns:
|
||||
List of plugin information
|
||||
"""
|
||||
# Check cache first
|
||||
if not force_refresh and self.cache.get("plugins"):
|
||||
return list(self.cache["plugins"].values())
|
||||
|
||||
try:
|
||||
# Fetch from GitHub API (example)
|
||||
response = requests.get(self.marketplace_url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
plugins = []
|
||||
for item in response.json():
|
||||
if item.get("type") == "file" and item.get("name", "").endswith(".py"):
|
||||
plugin_info = {
|
||||
"name": item["name"].replace(".py", ""),
|
||||
"download_url": item.get("download_url", ""),
|
||||
"size": item.get("size", 0),
|
||||
"description": f"Plugin from marketplace: {item['name']}"
|
||||
}
|
||||
plugins.append(plugin_info)
|
||||
|
||||
# Update cache
|
||||
self.cache["plugins"] = {p["name"]: p for p in plugins}
|
||||
self.cache["last_updated"] = str(Path(__file__).stat().st_mtime)
|
||||
self._save_cache()
|
||||
|
||||
return plugins
|
||||
else:
|
||||
print(f"Warning: Could not fetch marketplace plugins: HTTP {response.status_code}")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not fetch marketplace plugins: {e}")
|
||||
return []
|
||||
|
||||
def get_installed_plugins(self) -> List[str]:
|
||||
"""
|
||||
Get list of installed plugins
|
||||
|
||||
Returns:
|
||||
List of installed plugin names
|
||||
"""
|
||||
installed = []
|
||||
|
||||
# Check local plugins directory
|
||||
for file_path in self.local_plugins_dir.glob("*.py"):
|
||||
if file_path.name != "__init__.py":
|
||||
installed.append(file_path.stem)
|
||||
|
||||
# Check built-in plugins directory
|
||||
builtin_dir = Path(__file__).parent.parent / "plugins"
|
||||
if builtin_dir.exists():
|
||||
for file_path in builtin_dir.glob("*.py"):
|
||||
if file_path.name not in ["__init__.py", "base.py", "manager.py"]:
|
||||
installed.append(file_path.stem)
|
||||
|
||||
return installed
|
||||
|
||||
def install_plugin(self, plugin_name: str) -> bool:
|
||||
"""
|
||||
Install a plugin from marketplace
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to install
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Check if already installed
|
||||
if plugin_name in self.get_installed_plugins():
|
||||
print(f"Plugin '{plugin_name}' is already installed")
|
||||
return True
|
||||
|
||||
# Get plugin info from marketplace
|
||||
plugins = self.fetch_marketplace_plugins()
|
||||
plugin_info = None
|
||||
|
||||
for plugin in plugins:
|
||||
if plugin["name"] == plugin_name:
|
||||
plugin_info = plugin
|
||||
break
|
||||
|
||||
if not plugin_info:
|
||||
print(f"Plugin '{plugin_name}' not found in marketplace")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Download plugin
|
||||
download_url = plugin_info.get("download_url")
|
||||
if not download_url:
|
||||
print(f"No download URL for plugin '{plugin_name}'")
|
||||
return False
|
||||
|
||||
response = requests.get(download_url, timeout=30)
|
||||
if response.status_code == 200:
|
||||
# Save plugin file
|
||||
plugin_file = self.local_plugins_dir / f"{plugin_name}.py"
|
||||
with open(plugin_file, 'w') as f:
|
||||
f.write(response.text)
|
||||
|
||||
print(f"✅ Plugin '{plugin_name}' installed successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"Failed to download plugin: HTTP {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error installing plugin: {e}")
|
||||
return False
|
||||
|
||||
def uninstall_plugin(self, plugin_name: str) -> bool:
|
||||
"""
|
||||
Uninstall a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to uninstall
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Check if it's a built-in plugin
|
||||
builtin_dir = Path(__file__).parent.parent / "plugins"
|
||||
builtin_plugins = []
|
||||
if builtin_dir.exists():
|
||||
for file_path in builtin_dir.glob("*.py"):
|
||||
if file_path.name not in ["__init__.py", "base.py", "manager.py"]:
|
||||
builtin_plugins.append(file_path.stem)
|
||||
|
||||
if plugin_name in builtin_plugins:
|
||||
print(f"Cannot uninstall built-in plugin '{plugin_name}'")
|
||||
return False
|
||||
|
||||
# Check if installed in local directory
|
||||
plugin_file = self.local_plugins_dir / f"{plugin_name}.py"
|
||||
if not plugin_file.exists():
|
||||
print(f"Plugin '{plugin_name}' is not installed")
|
||||
return False
|
||||
|
||||
try:
|
||||
plugin_file.unlink()
|
||||
print(f"✅ Plugin '{plugin_name}' uninstalled successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error uninstalling plugin: {e}")
|
||||
return False
|
||||
|
||||
def get_plugin_info(self, plugin_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get information about a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin
|
||||
|
||||
Returns:
|
||||
Plugin information or None if not found
|
||||
"""
|
||||
# Check marketplace
|
||||
plugins = self.fetch_marketplace_plugins()
|
||||
for plugin in plugins:
|
||||
if plugin["name"] == plugin_name:
|
||||
return plugin
|
||||
|
||||
# Check local installation
|
||||
local_file = self.local_plugins_dir / f"{plugin_name}.py"
|
||||
if local_file.exists():
|
||||
return {
|
||||
"name": plugin_name,
|
||||
"installed": True,
|
||||
"location": str(local_file),
|
||||
"description": f"Locally installed plugin: {plugin_name}"
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def search_plugins(self, query: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search for plugins in marketplace
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
|
||||
Returns:
|
||||
List of matching plugins
|
||||
"""
|
||||
plugins = self.fetch_marketplace_plugins()
|
||||
results = []
|
||||
|
||||
query_lower = query.lower()
|
||||
for plugin in plugins:
|
||||
if (query_lower in plugin["name"].lower() or
|
||||
query_lower in plugin.get("description", "").lower()):
|
||||
results.append(plugin)
|
||||
|
||||
return results
|
||||
|
||||
def get_marketplace_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get marketplace statistics
|
||||
|
||||
Returns:
|
||||
Dictionary with marketplace stats
|
||||
"""
|
||||
plugins = self.fetch_marketplace_plugins()
|
||||
installed = self.get_installed_plugins()
|
||||
|
||||
return {
|
||||
"total_plugins": len(plugins),
|
||||
"installed_plugins": len(installed),
|
||||
"available_plugins": len(plugins) - len(installed),
|
||||
"last_updated": self.cache.get("last_updated", "Never")
|
||||
}
|
||||
|
||||
|
||||
# Global marketplace manager instance
|
||||
marketplace_manager = MarketplaceManager()
|
||||
Executable
+551
@@ -0,0 +1,551 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MaxClaw Agent - Unified CLI Entry Point
|
||||
Usage:
|
||||
maxclaw → avvia l'agent (chat interattiva)
|
||||
maxclaw chat → avvia l'agent
|
||||
maxclaw server [--port N] → avvia web server
|
||||
maxclaw tui → apre TUI configurazione
|
||||
maxclaw config list → lista configurazioni
|
||||
maxclaw config add → aggiungi configurazione
|
||||
maxclaw config remove NAME → rimuovi configurazione
|
||||
maxclaw config use NAME → attiva configurazione
|
||||
maxclaw config active → mostra configurazione attiva
|
||||
maxclaw config test [NAME] → testa connessione
|
||||
maxclaw config export → esporta configurazioni
|
||||
maxclaw config import FILE → importa configurazioni
|
||||
maxclaw config search Q → cerca configurazioni
|
||||
maxclaw analytics → mostra analytics
|
||||
maxclaw dashboard → mostra dashboard performance
|
||||
maxclaw version → mostra versione
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
|
||||
# Add current directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
VERSION = "1.0.0"
|
||||
|
||||
|
||||
def cmd_chat(args):
|
||||
"""Avvia l'agent in modalità chat interattiva"""
|
||||
from main import create_maxclaw_agent, process_command
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
print("🚀 Inizializzo MaxClaw Agent...")
|
||||
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
print("✅ Agent inizializzato con successo!")
|
||||
|
||||
print("\n🔧 Strumenti disponibili:")
|
||||
print(" • Provider tools: list models, test connections, set defaults")
|
||||
print(" • Proxmox tools: manage VMs, snapshots, resources")
|
||||
print(" • Standard tools: planning, filesystem, shell, subagents")
|
||||
|
||||
print("\n💬 Puoi interagire con l'agent.")
|
||||
print("📌 Comandi speciali (prefissati con /):")
|
||||
print(" /help - Mostra i comandi disponibili")
|
||||
print(" /status - Mostra lo stato del sistema")
|
||||
print(" /config - Gestisci le configurazioni")
|
||||
print(" /exit - Esci dall'agente")
|
||||
print("\n💡 Per chattare, scrivi semplicemente il tuo messaggio.")
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n👤 You: ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\n👋 Arrivederci!")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
# Check if it's a command (starts with /)
|
||||
if process_command(user_input):
|
||||
continue
|
||||
|
||||
# Otherwise, treat it as a chat message
|
||||
try:
|
||||
result = agent.invoke({"messages": [HumanMessage(content=user_input)]})
|
||||
ai_message = result["messages"][-1]
|
||||
print(f"\n🤖 MaxClaw: {ai_message.content}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Errore: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Inizializzazione fallita: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_server(args):
|
||||
"""Avvia il web server"""
|
||||
import uvicorn
|
||||
from api_server import app
|
||||
|
||||
host = args.host or "0.0.0.0"
|
||||
port = args.port or 8000
|
||||
|
||||
print(f"🌐 Avvio MaxClaw Web Server su http://{host}:{port}")
|
||||
print(f"📊 Dashboard: http://{host}:{port}/monitoring")
|
||||
print(f"🛒 Marketplace: http://{host}:{port}/marketplace")
|
||||
print(f"📡 API Docs: http://{host}:{port}/docs")
|
||||
print("Premi Ctrl+C per fermare il server\n")
|
||||
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
|
||||
def cmd_tui(args):
|
||||
"""Apre la TUI per gestione configurazioni"""
|
||||
from config_tui import main as tui_main
|
||||
tui_main()
|
||||
|
||||
|
||||
def cmd_config_list(args):
|
||||
"""Lista tutte le configurazioni"""
|
||||
from config_cli import print_configurations
|
||||
from config_manager import config_manager
|
||||
|
||||
configs = config_manager.get_configurations()
|
||||
if args.search:
|
||||
from config_cli import filter_configurations
|
||||
configs = filter_configurations(configs, args.search)
|
||||
print_configurations(configs, title=f"Configurazioni trovate per '{args.search}'")
|
||||
else:
|
||||
print_configurations(configs)
|
||||
|
||||
|
||||
def cmd_config_add(args):
|
||||
"""Aggiunge una nuova configurazione"""
|
||||
from config_manager import config_manager
|
||||
|
||||
if args.name and args.provider:
|
||||
# Modalità non-interattiva
|
||||
config_data = {"provider": args.provider}
|
||||
|
||||
if args.model:
|
||||
config_data["model_name"] = args.model
|
||||
else:
|
||||
# Default model per provider
|
||||
defaults = {
|
||||
"openai": "gpt-4o",
|
||||
"ollama": "gemma2:9b",
|
||||
"openrouter": "anthropic/claude-3.5-sonnet",
|
||||
"anthropic": "claude-3-5-sonnet-20241022",
|
||||
"google": "gemini-1.5-pro"
|
||||
}
|
||||
config_data["model_name"] = defaults.get(args.provider, "gpt-4o")
|
||||
|
||||
if args.api_key:
|
||||
config_data["api_key"] = args.api_key
|
||||
if args.base_url:
|
||||
config_data["base_url"] = args.base_url
|
||||
|
||||
try:
|
||||
config_manager.add_configuration(args.name, config_data)
|
||||
print(f"✅ Configurazione '{args.name}' aggiunta con successo!")
|
||||
if args.activate:
|
||||
config_manager.set_active_configuration(args.name)
|
||||
print(f"✅ Configurazione '{args.name}' attivata!")
|
||||
except Exception as e:
|
||||
print(f"❌ Errore: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Modalità interattiva
|
||||
from config_cli import add_configuration
|
||||
add_configuration()
|
||||
|
||||
|
||||
def cmd_config_remove(args):
|
||||
"""Rimuove una configurazione"""
|
||||
from config_manager import config_manager
|
||||
|
||||
name = args.name
|
||||
if not name:
|
||||
name = input("Nome configurazione da rimuovere: ").strip()
|
||||
|
||||
if not name:
|
||||
print("❌ Nome richiesto.")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
config_manager.remove_configuration(name)
|
||||
print(f"✅ Configurazione '{name}' rimossa!")
|
||||
except Exception as e:
|
||||
print(f"❌ Errore: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_config_use(args):
|
||||
"""Attiva una configurazione"""
|
||||
from config_manager import config_manager
|
||||
|
||||
try:
|
||||
config_manager.set_active_configuration(args.name)
|
||||
print(f"✅ Configurazione '{args.name}' attivata!")
|
||||
except Exception as e:
|
||||
print(f"❌ Errore: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_config_active(args):
|
||||
"""Mostra la configurazione attiva"""
|
||||
from config_manager import config_manager
|
||||
|
||||
active = config_manager.get_active_configuration()
|
||||
if active:
|
||||
config = config_manager.get_configuration(active)
|
||||
print(f"\n📌 Configurazione attiva: {active}")
|
||||
print(f" Provider: {config.get('provider', 'unknown')}")
|
||||
print(f" Model: {config.get('model_name', 'unknown')}")
|
||||
if config.get('base_url'):
|
||||
print(f" Base URL: {config.get('base_url')}")
|
||||
else:
|
||||
print("⚠️ Nessuna configurazione attiva.")
|
||||
|
||||
|
||||
def cmd_config_test(args):
|
||||
"""Testa una configurazione"""
|
||||
from config_manager import config_manager
|
||||
|
||||
name = args.name or config_manager.get_active_configuration()
|
||||
if not name:
|
||||
print("❌ Nessuna configurazione specificata o attiva.")
|
||||
sys.exit(1)
|
||||
|
||||
config = config_manager.get_configuration(name)
|
||||
if not config:
|
||||
print(f"❌ Configurazione '{name}' non trovata.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"🔍 Test connessione per '{name}'...")
|
||||
provider = config.get("provider", "")
|
||||
|
||||
try:
|
||||
if provider == "ollama":
|
||||
import requests
|
||||
base_url = config.get("base_url", "http://localhost:11434")
|
||||
resp = requests.get(f"{base_url}/api/tags", timeout=10)
|
||||
if resp.status_code == 200:
|
||||
models = resp.json().get("models", [])
|
||||
print(f"✅ Ollama connesso! {len(models)} modelli disponibili.")
|
||||
for m in models[:5]:
|
||||
print(f" • {m['name']}")
|
||||
else:
|
||||
print(f"❌ Errore: HTTP {resp.status_code}")
|
||||
elif provider in ("openai", "openrouter", "anthropic"):
|
||||
api_key = config.get("api_key", "")
|
||||
if not api_key:
|
||||
print("❌ API key non configurata.")
|
||||
sys.exit(1)
|
||||
print("✅ API key presente (test completo richiede una chiamata API)")
|
||||
elif provider == "google":
|
||||
api_key = config.get("api_key", "")
|
||||
if not api_key:
|
||||
print("❌ API key non configurata.")
|
||||
sys.exit(1)
|
||||
print("✅ API key presente (test completo richiede una chiamata API)")
|
||||
else:
|
||||
print(f"⚠️ Test non supportato per provider '{provider}'")
|
||||
except Exception as e:
|
||||
print(f"❌ Errore connessione: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_config_export(args):
|
||||
"""Esporta configurazioni"""
|
||||
from config_cli import export_configurations
|
||||
if args.format and args.output:
|
||||
import json
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from config_manager import config_manager
|
||||
|
||||
configs = config_manager.get_configurations()
|
||||
if not configs:
|
||||
print("❌ Nessuna configurazione da esportare.")
|
||||
return
|
||||
|
||||
try:
|
||||
if args.format == 'json':
|
||||
export_file = Path(f"{args.output}.json")
|
||||
with open(export_file, 'w') as f:
|
||||
json.dump(configs, f, indent=2)
|
||||
else:
|
||||
export_file = Path(f"{args.output}.yaml")
|
||||
with open(export_file, 'w') as f:
|
||||
yaml.dump(configs, f, default_flow_style=False)
|
||||
print(f"✅ Configurazioni esportate in {export_file}")
|
||||
except Exception as e:
|
||||
print(f"❌ Errore: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
export_configurations()
|
||||
|
||||
|
||||
def cmd_config_import(args):
|
||||
"""Importa configurazioni"""
|
||||
if args.file:
|
||||
import json
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from config_manager import config_manager
|
||||
|
||||
file_path = Path(args.file)
|
||||
if not file_path.exists():
|
||||
print(f"❌ File '{args.file}' non trovato.")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
if args.file.endswith('.json'):
|
||||
imported = json.load(f)
|
||||
elif args.file.endswith(('.yaml', '.yml')):
|
||||
imported = yaml.safe_load(f)
|
||||
else:
|
||||
print("❌ Formato non supportato. Usa .json o .yaml")
|
||||
sys.exit(1)
|
||||
|
||||
if not isinstance(imported, dict):
|
||||
print("❌ Formato configurazione non valido.")
|
||||
sys.exit(1)
|
||||
|
||||
count = 0
|
||||
for name, config in imported.items():
|
||||
try:
|
||||
config_manager.add_configuration(name, config)
|
||||
count += 1
|
||||
print(f"✅ Importata: {name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ '{name}': {e}")
|
||||
|
||||
print(f"\n✅ {count} configurazioni importate.")
|
||||
except Exception as e:
|
||||
print(f"❌ Errore: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
from config_cli import import_configurations
|
||||
import_configurations()
|
||||
|
||||
|
||||
def cmd_config_search(args):
|
||||
"""Cerca configurazioni"""
|
||||
from config_cli import print_configurations, filter_configurations
|
||||
from config_manager import config_manager
|
||||
|
||||
configs = config_manager.get_configurations()
|
||||
filtered = filter_configurations(configs, args.query)
|
||||
print_configurations(filtered, title=f"Risultati per '{args.query}'")
|
||||
|
||||
|
||||
def cmd_analytics(args):
|
||||
"""Mostra analytics"""
|
||||
from analytics import AnalyticsEngine
|
||||
|
||||
engine = AnalyticsEngine()
|
||||
|
||||
if args.report:
|
||||
report = engine.get_detailed_report()
|
||||
print(report)
|
||||
elif args.clear:
|
||||
engine.clear_old_data()
|
||||
print("✅ Dati analytics cancellati.")
|
||||
else:
|
||||
# Mostra riepilogo rapido
|
||||
summary = engine.get_usage_summary()
|
||||
insights = engine.get_insights()
|
||||
|
||||
print("\n📊 MaxClaw Analytics")
|
||||
print("=" * 40)
|
||||
|
||||
if summary.get("total_conversations", 0) > 0:
|
||||
print(f"\n💬 Conversazioni totali: {summary['total_conversations']}")
|
||||
print(f"📝 Messaggi totali: {summary.get('total_messages', 0)}")
|
||||
print(f"⏱️ Tempo medio risposta: {summary.get('avg_response_time', 0):.2f}s")
|
||||
|
||||
if summary.get("provider_usage"):
|
||||
print("\n🔌 Utilizzo provider:")
|
||||
for provider, count in summary["provider_usage"].items():
|
||||
print(f" • {provider}: {count} volte")
|
||||
|
||||
if summary.get("tool_usage"):
|
||||
print("\n🔧 Utilizzo strumenti:")
|
||||
for tool, count in sorted(summary["tool_usage"].items(), key=lambda x: -x[1])[:5]:
|
||||
print(f" • {tool}: {count} volte")
|
||||
else:
|
||||
print("\n📭 Nessun dato analytics ancora.")
|
||||
print(" Usa l'agent per generare dati!")
|
||||
|
||||
if insights:
|
||||
print("\n💡 Insights:")
|
||||
for insight in insights[:3]:
|
||||
print(f" • {insight}")
|
||||
|
||||
print(f"\n📁 Dati salvati in: {engine.data_dir}")
|
||||
print(" Usa 'maxclaw analytics --report' per report completo")
|
||||
|
||||
|
||||
def cmd_dashboard(args):
|
||||
"""Mostra dashboard performance"""
|
||||
from dashboard import PerformanceDashboard
|
||||
|
||||
dashboard = PerformanceDashboard()
|
||||
|
||||
if args.clear:
|
||||
dashboard.clear_old_data()
|
||||
print("✅ Dati performance cancellati.")
|
||||
else:
|
||||
report = dashboard.get_summary()
|
||||
print("\n📈 MaxClaw Performance Dashboard")
|
||||
print("=" * 40)
|
||||
|
||||
if report.get("total_requests", 0) > 0:
|
||||
print(f"\n📊 Richieste totali: {report['total_requests']}")
|
||||
print(f"⏱️ Tempo medio risposta: {report.get('avg_response_time', 0):.2f}s")
|
||||
print(f"✅ Tasso successo: {report.get('success_rate', 0):.1f}%")
|
||||
|
||||
if report.get("slowest_operations"):
|
||||
print("\n🐌 Operazioni più lente:")
|
||||
for op in report["slowest_operations"][:3]:
|
||||
print(f" • {op['name']}: {op['time']:.2f}s")
|
||||
|
||||
if report.get("memory_usage"):
|
||||
mem = report["memory_usage"]
|
||||
print(f"\n💾 Memoria: {mem.get('current', 0):.1f}MB (picco: {mem.get('peak', 0):.1f}MB)")
|
||||
else:
|
||||
print("\n📭 Nessun dato performance ancora.")
|
||||
|
||||
print(f"\n📁 Dati salvati in: {dashboard.data_dir}")
|
||||
|
||||
|
||||
def cmd_version(args):
|
||||
"""Mostra versione"""
|
||||
print(f"MaxClaw Agent v{VERSION}")
|
||||
print("Agent harness basato su LangChain Deep Agents")
|
||||
print("https://github.com/truetype/maxclaw_agent")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="maxclaw",
|
||||
description="🚀 MaxClaw Agent - AI Agent harness configurabile",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Esempi:
|
||||
maxclaw Avvia l'agent (chat interattiva)
|
||||
maxclaw server --port 3000 Avvia web server su porta 3000
|
||||
maxclaw config list Lista tutte le configurazioni
|
||||
maxclaw config add -n ollama -p ollama -m gemma2:9b
|
||||
maxclaw config use ollama Attiva configurazione 'ollama'
|
||||
maxclaw config test Testa configurazione attiva
|
||||
maxclaw analytics Mostra riepilogo analytics
|
||||
maxclaw analytics --report Genera report completo
|
||||
"""
|
||||
)
|
||||
parser.add_argument("--version", "-v", action="version", version=f"maxclaw {VERSION}")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Comandi disponibili")
|
||||
|
||||
# === CHAT (default) ===
|
||||
chat_parser = subparsers.add_parser("chat", help="Avvia l'agent in modalità chat")
|
||||
chat_parser.set_defaults(func=cmd_chat)
|
||||
|
||||
# === SERVER ===
|
||||
server_parser = subparsers.add_parser("server", help="Avvia il web server")
|
||||
server_parser.add_argument("--port", "-p", type=int, default=8000, help="Porta del server (default: 8000)")
|
||||
server_parser.add_argument("--host", "-H", default="0.0.0.0", help="Host del server (default: 0.0.0.0)")
|
||||
server_parser.set_defaults(func=cmd_server)
|
||||
|
||||
# === TUI ===
|
||||
tui_parser = subparsers.add_parser("tui", help="Apre TUI per gestione configurazioni")
|
||||
tui_parser.set_defaults(func=cmd_tui)
|
||||
|
||||
# === CONFIG (nested subcommands) ===
|
||||
config_parser = subparsers.add_parser("config", help="Gestione configurazioni")
|
||||
config_subparsers = config_parser.add_subparsers(dest="config_command", help="Sottocomandi config")
|
||||
|
||||
# config list
|
||||
cl = config_subparsers.add_parser("list", help="Lista configurazioni")
|
||||
cl.add_argument("--search", "-s", help="Filtra per termine di ricerca")
|
||||
cl.set_defaults(func=cmd_config_list)
|
||||
|
||||
# config add
|
||||
ca = config_subparsers.add_parser("add", help="Aggiungi configurazione")
|
||||
ca.add_argument("--name", "-n", help="Nome configurazione")
|
||||
ca.add_argument("--provider", "-p", choices=["openai", "ollama", "openrouter", "anthropic", "google"], help="Provider")
|
||||
ca.add_argument("--model", "-m", help="Nome modello")
|
||||
ca.add_argument("--api-key", "-k", help="API key")
|
||||
ca.add_argument("--base-url", "-u", help="Base URL")
|
||||
ca.add_argument("--activate", "-a", action="store_true", help="Attiva dopo la creazione")
|
||||
ca.set_defaults(func=cmd_config_add)
|
||||
|
||||
# config remove
|
||||
cr = config_subparsers.add_parser("remove", help="Rimuovi configurazione")
|
||||
cr.add_argument("name", help="Nome configurazione da rimuovere")
|
||||
cr.set_defaults(func=cmd_config_remove)
|
||||
|
||||
# config use
|
||||
cu = config_subparsers.add_parser("use", help="Attiva configurazione")
|
||||
cu.add_argument("name", help="Nome configurazione da attivare")
|
||||
cu.set_defaults(func=cmd_config_use)
|
||||
|
||||
# config active
|
||||
cv = config_subparsers.add_parser("active", help="Mostra configurazione attiva")
|
||||
cv.set_defaults(func=cmd_config_active)
|
||||
|
||||
# config test
|
||||
ct = config_subparsers.add_parser("test", help="Testa connessione configurazione")
|
||||
ct.add_argument("name", nargs="?", help="Nome configurazione (default: attiva)")
|
||||
ct.set_defaults(func=cmd_config_test)
|
||||
|
||||
# config export
|
||||
ce = config_subparsers.add_parser("export", help="Esporta configurazioni")
|
||||
ce.add_argument("--format", "-f", choices=["json", "yaml"], help="Formato export")
|
||||
ce.add_argument("--output", "-o", help="Nome file output (senza estensione)")
|
||||
ce.set_defaults(func=cmd_config_export)
|
||||
|
||||
# config import
|
||||
ci = config_subparsers.add_parser("import", help="Importa configurazioni")
|
||||
ci.add_argument("file", nargs="?", help="File da importare (.json o .yaml)")
|
||||
ci.set_defaults(func=cmd_config_import)
|
||||
|
||||
# config search
|
||||
cs = config_subparsers.add_parser("search", help="Cerca configurazioni")
|
||||
cs.add_argument("query", help="Termine di ricerca")
|
||||
cs.set_defaults(func=cmd_config_search)
|
||||
|
||||
# === ANALYTICS ===
|
||||
analytics_parser = subparsers.add_parser("analytics", help="Mostra analytics")
|
||||
analytics_parser.add_argument("--report", "-r", action="store_true", help="Genera report completo")
|
||||
analytics_parser.add_argument("--clear", "-c", action="store_true", help="Cancella dati analytics")
|
||||
analytics_parser.set_defaults(func=cmd_analytics)
|
||||
|
||||
# === DASHBOARD ===
|
||||
dashboard_parser = subparsers.add_parser("dashboard", help="Mostra dashboard performance")
|
||||
dashboard_parser.add_argument("--clear", "-c", action="store_true", help="Cancella dati performance")
|
||||
dashboard_parser.set_defaults(func=cmd_dashboard)
|
||||
|
||||
# === VERSION ===
|
||||
version_parser = subparsers.add_parser("version", help="Mostra versione")
|
||||
version_parser.set_defaults(func=cmd_version)
|
||||
|
||||
# Parse arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
# Se nessun comando, avvia chat (default)
|
||||
if not args.command:
|
||||
args.func = cmd_chat
|
||||
args.func(args)
|
||||
return
|
||||
|
||||
# Esegui il comando
|
||||
if hasattr(args, 'func'):
|
||||
args.func(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Monitoring and logging for MaxClaw Agent
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
# Add current directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
class MaxClawLogger:
|
||||
"""Custom logger for MaxClaw Agent"""
|
||||
|
||||
def __init__(self, log_dir: Optional[str] = None):
|
||||
"""
|
||||
Initialize logger
|
||||
|
||||
Args:
|
||||
log_dir: Directory for log files. If None, uses ~/.maxclaw/logs
|
||||
"""
|
||||
if log_dir is None:
|
||||
self.log_dir = Path.home() / ".maxclaw" / "logs"
|
||||
else:
|
||||
self.log_dir = Path(log_dir)
|
||||
|
||||
# Create log directory
|
||||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Set up logging
|
||||
self.setup_logging()
|
||||
|
||||
def setup_logging(self):
|
||||
"""Set up logging configuration"""
|
||||
# Create formatters
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
console_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Create file handler
|
||||
log_file = self.log_dir / f"maxclaw_{datetime.now().strftime('%Y%m%d')}.log"
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
|
||||
# Create console handler
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
|
||||
# Configure root logger
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
handlers=[file_handler, console_handler]
|
||||
)
|
||||
|
||||
# Create specific loggers
|
||||
self.agent_logger = logging.getLogger("maxclaw.agent")
|
||||
self.config_logger = logging.getLogger("maxclaw.config")
|
||||
self.api_logger = logging.getLogger("maxclaw.api")
|
||||
self.tools_logger = logging.getLogger("maxclaw.tools")
|
||||
|
||||
print(f"✅ Logging initialized. Logs will be saved to: {self.log_dir}")
|
||||
|
||||
def log_agent_action(self, action: str, details: Optional[Dict[str, Any]] = None):
|
||||
"""Log agent actions"""
|
||||
message = f"Agent action: {action}"
|
||||
if details:
|
||||
message += f" - {json.dumps(details)}"
|
||||
self.agent_logger.info(message)
|
||||
|
||||
def log_config_change(self, action: str, config_name: str, details: Optional[Dict[str, Any]] = None):
|
||||
"""Log configuration changes"""
|
||||
message = f"Config {action}: {config_name}"
|
||||
if details:
|
||||
message += f" - {json.dumps(details)}"
|
||||
self.config_logger.info(message)
|
||||
|
||||
def log_api_request(self, method: str, endpoint: str, status_code: int, duration: float):
|
||||
"""Log API requests"""
|
||||
message = f"API {method} {endpoint} - Status: {status_code} - Duration: {duration:.3f}s"
|
||||
self.api_logger.info(message)
|
||||
|
||||
def log_tool_usage(self, tool_name: str, success: bool, duration: float, details: Optional[Dict[str, Any]] = None):
|
||||
"""Log tool usage"""
|
||||
status = "SUCCESS" if success else "FAILED"
|
||||
message = f"Tool {tool_name} - {status} - Duration: {duration:.3f}s"
|
||||
if details:
|
||||
message += f" - {json.dumps(details)}"
|
||||
self.tools_logger.info(message)
|
||||
|
||||
def log_error(self, error: Exception, context: Optional[str] = None):
|
||||
"""Log errors"""
|
||||
message = f"Error: {str(error)}"
|
||||
if context:
|
||||
message = f"{context} - {message}"
|
||||
self.agent_logger.error(message, exc_info=True)
|
||||
|
||||
def get_log_files(self):
|
||||
"""Get list of log files"""
|
||||
return list(self.log_dir.glob("*.log"))
|
||||
|
||||
def get_recent_logs(self, lines: int = 100):
|
||||
"""Get recent log entries"""
|
||||
log_files = self.get_log_files()
|
||||
if not log_files:
|
||||
return "No log files found."
|
||||
|
||||
# Get the most recent log file
|
||||
latest_log = max(log_files, key=lambda f: f.stat().st_mtime)
|
||||
|
||||
try:
|
||||
with open(latest_log, 'r') as f:
|
||||
all_lines = f.readlines()
|
||||
return ''.join(all_lines[-lines:])
|
||||
except Exception as e:
|
||||
return f"Error reading log file: {e}"
|
||||
|
||||
|
||||
class SystemMonitor:
|
||||
"""Monitor system resources and performance"""
|
||||
|
||||
def __init__(self):
|
||||
self.start_time = time.time()
|
||||
self.request_count = 0
|
||||
self.error_count = 0
|
||||
self.response_times = []
|
||||
|
||||
def record_request(self, duration: float, success: bool = True):
|
||||
"""Record an API request"""
|
||||
self.request_count += 1
|
||||
self.response_times.append(duration)
|
||||
|
||||
if not success:
|
||||
self.error_count += 1
|
||||
|
||||
# Keep only last 1000 response times
|
||||
if len(self.response_times) > 1000:
|
||||
self.response_times = self.response_times[-1000:]
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get system statistics"""
|
||||
uptime = time.time() - self.start_time
|
||||
|
||||
stats = {
|
||||
"uptime_seconds": uptime,
|
||||
"uptime_formatted": self._format_uptime(uptime),
|
||||
"total_requests": self.request_count,
|
||||
"total_errors": self.error_count,
|
||||
"error_rate": self.error_count / self.request_count if self.request_count > 0 else 0,
|
||||
"avg_response_time": sum(self.response_times) / len(self.response_times) if self.response_times else 0,
|
||||
"min_response_time": min(self.response_times) if self.response_times else 0,
|
||||
"max_response_time": max(self.response_times) if self.response_times else 0,
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def _format_uptime(self, seconds: float) -> str:
|
||||
"""Format uptime in human-readable format"""
|
||||
days = int(seconds // (24 * 3600))
|
||||
hours = int((seconds % (24 * 3600)) // 3600)
|
||||
minutes = int((seconds % 3600) // 60)
|
||||
seconds = int(seconds % 60)
|
||||
|
||||
if days > 0:
|
||||
return f"{days}d {hours}h {minutes}m {seconds}s"
|
||||
elif hours > 0:
|
||||
return f"{hours}h {minutes}m {seconds}s"
|
||||
elif minutes > 0:
|
||||
return f"{minutes}m {seconds}s"
|
||||
else:
|
||||
return f"{seconds}s"
|
||||
|
||||
def reset_stats(self):
|
||||
"""Reset statistics"""
|
||||
self.request_count = 0
|
||||
self.error_count = 0
|
||||
self.response_times = []
|
||||
|
||||
|
||||
# Global instances
|
||||
logger = MaxClawLogger()
|
||||
system_monitor = SystemMonitor()
|
||||
|
||||
|
||||
def get_system_status() -> Dict[str, Any]:
|
||||
"""Get comprehensive system status"""
|
||||
from config_manager import config_manager
|
||||
|
||||
# Get configuration status
|
||||
configs = config_manager.get_configurations()
|
||||
active_config = config_manager.get_active_configuration()
|
||||
|
||||
# Get system stats
|
||||
stats = system_monitor.get_stats()
|
||||
|
||||
# Get log files
|
||||
log_files = logger.get_log_files()
|
||||
|
||||
status = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"system": stats,
|
||||
"configurations": {
|
||||
"total": len(configs),
|
||||
"active": active_config,
|
||||
"providers": list(set(config.get("provider", "unknown") for config in configs.values()))
|
||||
},
|
||||
"logging": {
|
||||
"log_dir": str(logger.log_dir),
|
||||
"log_files": len(log_files),
|
||||
"recent_logs": logger.get_recent_logs(10)
|
||||
},
|
||||
"performance": {
|
||||
"cache_size": len(performance_monitor.cache) if 'performance_monitor' in globals() else 0
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def log_system_status():
|
||||
"""Log current system status"""
|
||||
status = get_system_status()
|
||||
logger.log_agent_action("system_status_check", status)
|
||||
|
||||
|
||||
# Import performance monitor if available
|
||||
try:
|
||||
from performance import performance_monitor
|
||||
except ImportError:
|
||||
performance_monitor = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test logging
|
||||
print("Testing MaxClaw logging system...")
|
||||
|
||||
# Log some test messages
|
||||
logger.log_agent_action("test_action", {"test": "data"})
|
||||
logger.log_config_change("add", "test-config", {"provider": "ollama"})
|
||||
logger.log_api_request("GET", "/health", 200, 0.05)
|
||||
logger.log_tool_usage("ollama_list_models", True, 0.1)
|
||||
|
||||
# Get system status
|
||||
status = get_system_status()
|
||||
print("\nSystem Status:")
|
||||
print(json.dumps(status, indent=2))
|
||||
|
||||
# Get recent logs
|
||||
print("\nRecent Logs:")
|
||||
print(logger.get_recent_logs(5))
|
||||
+513
@@ -0,0 +1,513 @@
|
||||
"""
|
||||
Performance optimizations for MaxClaw Agent
|
||||
Enhanced with connection pooling, lazy loading, and advanced caching
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import functools
|
||||
import threading
|
||||
from typing import Dict, Any, Optional, Callable, List
|
||||
from pathlib import Path
|
||||
import json
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
# Add current directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
class ConnectionPool:
|
||||
"""Connection pool for managing API connections"""
|
||||
|
||||
def __init__(self, max_connections: int = 10):
|
||||
"""
|
||||
Initialize connection pool
|
||||
|
||||
Args:
|
||||
max_connections: Maximum number of connections in the pool
|
||||
"""
|
||||
self.max_connections = max_connections
|
||||
self.connections = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def get_connection(self, provider: str, **kwargs):
|
||||
"""
|
||||
Get a connection from the pool
|
||||
|
||||
Args:
|
||||
provider: Provider name
|
||||
**kwargs: Connection parameters
|
||||
|
||||
Returns:
|
||||
Connection object
|
||||
"""
|
||||
with self.lock:
|
||||
key = f"{provider}:{str(sorted(kwargs.items()))}"
|
||||
|
||||
if key not in self.connections:
|
||||
# Create new connection
|
||||
if provider == "ollama":
|
||||
import requests
|
||||
base_url = kwargs.get("base_url", "http://localhost:11434")
|
||||
session = requests.Session()
|
||||
session.headers.update({"Content-Type": "application/json"})
|
||||
self.connections[key] = {
|
||||
"session": session,
|
||||
"base_url": base_url,
|
||||
"last_used": time.time()
|
||||
}
|
||||
elif provider in ["openai", "openrouter", "anthropic", "google"]:
|
||||
import requests
|
||||
session = requests.Session()
|
||||
api_key = kwargs.get("api_key", "")
|
||||
if api_key:
|
||||
session.headers.update({"Authorization": f"Bearer {api_key}"})
|
||||
self.connections[key] = {
|
||||
"session": session,
|
||||
"last_used": time.time()
|
||||
}
|
||||
|
||||
# Update last used time
|
||||
if key in self.connections:
|
||||
self.connections[key]["last_used"] = time.time()
|
||||
|
||||
return self.connections.get(key)
|
||||
|
||||
def cleanup_idle_connections(self, max_idle_time: int = 300):
|
||||
"""
|
||||
Clean up idle connections
|
||||
|
||||
Args:
|
||||
max_idle_time: Maximum idle time in seconds
|
||||
"""
|
||||
with self.lock:
|
||||
current_time = time.time()
|
||||
keys_to_remove = []
|
||||
|
||||
for key, conn in self.connections.items():
|
||||
if current_time - conn["last_used"] > max_idle_time:
|
||||
keys_to_remove.append(key)
|
||||
|
||||
for key in keys_to_remove:
|
||||
del self.connections[key]
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get connection pool statistics"""
|
||||
with self.lock:
|
||||
return {
|
||||
"total_connections": len(self.connections),
|
||||
"max_connections": self.max_connections,
|
||||
"connections": list(self.connections.keys())
|
||||
}
|
||||
|
||||
|
||||
class LazyLoader:
|
||||
"""Lazy loading for heavy resources"""
|
||||
|
||||
def __init__(self):
|
||||
self.loaded_modules = {}
|
||||
self.loaded_tools = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def load_module(self, module_name: str, import_path: str):
|
||||
"""
|
||||
Lazily load a module
|
||||
|
||||
Args:
|
||||
module_name: Name to store the module under
|
||||
import_path: Import path for the module
|
||||
|
||||
Returns:
|
||||
Loaded module
|
||||
"""
|
||||
with self.lock:
|
||||
if module_name not in self.loaded_modules:
|
||||
try:
|
||||
module = __import__(import_path, fromlist=[''])
|
||||
self.loaded_modules[module_name] = module
|
||||
except ImportError as e:
|
||||
print(f"Warning: Could not load module {module_name}: {e}")
|
||||
self.loaded_modules[module_name] = None
|
||||
|
||||
return self.loaded_modules.get(module_name)
|
||||
|
||||
def load_tool(self, tool_name: str, loader_func: Callable):
|
||||
"""
|
||||
Lazily load a tool
|
||||
|
||||
Args:
|
||||
tool_name: Name to store the tool under
|
||||
loader_func: Function to load the tool
|
||||
|
||||
Returns:
|
||||
Loaded tool
|
||||
"""
|
||||
with self.lock:
|
||||
if tool_name not in self.loaded_tools:
|
||||
try:
|
||||
tool = loader_func()
|
||||
self.loaded_tools[tool_name] = tool
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load tool {tool_name}: {e}")
|
||||
self.loaded_tools[tool_name] = None
|
||||
|
||||
return self.loaded_tools.get(tool_name)
|
||||
|
||||
def clear(self):
|
||||
"""Clear all loaded resources"""
|
||||
with self.lock:
|
||||
self.loaded_modules.clear()
|
||||
self.loaded_tools.clear()
|
||||
|
||||
|
||||
class BatchProcessor:
|
||||
"""Batch processing for multiple operations"""
|
||||
|
||||
def __init__(self, max_workers: int = 5):
|
||||
"""
|
||||
Initialize batch processor
|
||||
|
||||
Args:
|
||||
max_workers: Maximum number of worker threads
|
||||
"""
|
||||
self.max_workers = max_workers
|
||||
self.executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||
|
||||
def process_batch(self, tasks: List[Callable], timeout: int = 30) -> List[Any]:
|
||||
"""
|
||||
Process a batch of tasks in parallel
|
||||
|
||||
Args:
|
||||
tasks: List of callable tasks
|
||||
timeout: Timeout in seconds
|
||||
|
||||
Returns:
|
||||
List of results
|
||||
"""
|
||||
futures = [self.executor.submit(task) for task in tasks]
|
||||
results = []
|
||||
|
||||
try:
|
||||
for future in as_completed(futures, timeout=timeout):
|
||||
try:
|
||||
result = future.result()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
results.append({"error": str(e)})
|
||||
except Exception as e:
|
||||
print(f"Batch processing error: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown the executor"""
|
||||
self.executor.shutdown(wait=True)
|
||||
|
||||
|
||||
class PerformanceMonitor:
|
||||
"""Monitor and optimize performance"""
|
||||
|
||||
def __init__(self):
|
||||
self.metrics = {}
|
||||
self.cache = {}
|
||||
self.cache_ttl = 300 # 5 minutes default TTL
|
||||
self.connection_pool = ConnectionPool()
|
||||
self.lazy_loader = LazyLoader()
|
||||
self.batch_processor = BatchProcessor()
|
||||
|
||||
def timing(self, func: Callable) -> Callable:
|
||||
"""Decorator to measure function execution time"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
result = func(*args, **kwargs)
|
||||
end_time = time.time()
|
||||
|
||||
# Store timing metric
|
||||
func_name = func.__name__
|
||||
if func_name not in self.metrics:
|
||||
self.metrics[func_name] = []
|
||||
|
||||
self.metrics[func_name].append(end_time - start_time)
|
||||
|
||||
# Keep only last 100 measurements
|
||||
if len(self.metrics[func_name]) > 100:
|
||||
self.metrics[func_name] = self.metrics[func_name][-100:]
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
def cache_result(self, ttl: Optional[int] = None):
|
||||
"""Decorator to cache function results"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Create cache key
|
||||
cache_key = f"{func.__name__}:{str(args)}:{str(kwargs)}"
|
||||
|
||||
# Check cache
|
||||
if cache_key in self.cache:
|
||||
cached_time, cached_result = self.cache[cache_key]
|
||||
if time.time() - cached_time < (ttl or self.cache_ttl):
|
||||
return cached_result
|
||||
|
||||
# Execute function
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
# Cache result
|
||||
self.cache[cache_key] = (time.time(), result)
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def get_metrics(self) -> Dict[str, Any]:
|
||||
"""Get performance metrics"""
|
||||
metrics_summary = {}
|
||||
|
||||
for func_name, timings in self.metrics.items():
|
||||
if timings:
|
||||
metrics_summary[func_name] = {
|
||||
"count": len(timings),
|
||||
"avg_time": sum(timings) / len(timings),
|
||||
"min_time": min(timings),
|
||||
"max_time": max(timings),
|
||||
"total_time": sum(timings)
|
||||
}
|
||||
|
||||
return metrics_summary
|
||||
|
||||
def clear_cache(self):
|
||||
"""Clear the cache"""
|
||||
self.cache.clear()
|
||||
|
||||
def clear_metrics(self):
|
||||
"""Clear performance metrics"""
|
||||
self.metrics.clear()
|
||||
|
||||
def get_connection_pool_stats(self) -> Dict[str, Any]:
|
||||
"""Get connection pool statistics"""
|
||||
return self.connection_pool.get_stats()
|
||||
|
||||
def cleanup_resources(self):
|
||||
"""Clean up resources"""
|
||||
self.connection_pool.cleanup_idle_connections()
|
||||
self.clear_cache()
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown all resources"""
|
||||
self.batch_processor.shutdown()
|
||||
self.cleanup_resources()
|
||||
|
||||
|
||||
# Global performance monitor instance
|
||||
performance_monitor = PerformanceMonitor()
|
||||
|
||||
|
||||
def optimize_config_manager():
|
||||
"""Optimize configuration manager with caching"""
|
||||
from config_manager import config_manager
|
||||
|
||||
# Cache configuration loading
|
||||
original_load_config = config_manager._load_config
|
||||
|
||||
@functools.wraps(original_load_config)
|
||||
def optimized_load_config():
|
||||
# Check if we have a cached version
|
||||
cache_key = "config_manager:load_config"
|
||||
if cache_key in performance_monitor.cache:
|
||||
cached_time, cached_result = performance_monitor.cache[cache_key]
|
||||
if time.time() - cached_time < 60: # Cache for 1 minute
|
||||
return cached_result
|
||||
|
||||
# Load configuration
|
||||
result = original_load_config()
|
||||
|
||||
# Cache result
|
||||
performance_monitor.cache[cache_key] = (time.time(), result)
|
||||
|
||||
return result
|
||||
|
||||
# Replace method
|
||||
config_manager._load_config = optimized_load_config
|
||||
|
||||
# Cache configuration retrieval
|
||||
original_get_configurations = config_manager.get_configurations
|
||||
|
||||
@functools.wraps(original_get_configurations)
|
||||
def optimized_get_configurations():
|
||||
cache_key = "config_manager:get_configurations"
|
||||
if cache_key in performance_monitor.cache:
|
||||
cached_time, cached_result = performance_monitor.cache[cache_key]
|
||||
if time.time() - cached_time < 30: # Cache for 30 seconds
|
||||
return cached_result
|
||||
|
||||
result = original_get_configurations()
|
||||
performance_monitor.cache[cache_key] = (time.time(), result)
|
||||
return result
|
||||
|
||||
config_manager.get_configurations = optimized_get_configurations
|
||||
|
||||
print("✅ Configuration manager optimized with caching")
|
||||
|
||||
|
||||
def optimize_provider_tools():
|
||||
"""Optimize provider tools with connection pooling"""
|
||||
from tools.provider_tools import get_provider_tools
|
||||
|
||||
# Cache provider tools
|
||||
original_get_provider_tools = get_provider_tools
|
||||
|
||||
@functools.wraps(original_get_provider_tools)
|
||||
def optimized_get_provider_tools(provider: str, **kwargs):
|
||||
cache_key = f"provider_tools:{provider}:{str(kwargs)}"
|
||||
if cache_key in performance_monitor.cache:
|
||||
cached_time, cached_result = performance_monitor.cache[cache_key]
|
||||
if time.time() - cached_time < 300: # Cache for 5 minutes
|
||||
return cached_result
|
||||
|
||||
result = original_get_provider_tools(provider, **kwargs)
|
||||
performance_monitor.cache[cache_key] = (time.time(), result)
|
||||
return result
|
||||
|
||||
# Replace function
|
||||
import tools.provider_tools
|
||||
tools.provider_tools.get_provider_tools = optimized_get_provider_tools
|
||||
|
||||
print("✅ Provider tools optimized with caching")
|
||||
|
||||
|
||||
def optimize_agent_creation():
|
||||
"""Optimize agent creation with caching"""
|
||||
from main import create_maxclaw_agent
|
||||
|
||||
# Cache agent creation
|
||||
original_create_maxclaw_agent = create_maxclaw_agent
|
||||
|
||||
@functools.wraps(original_create_maxclaw_agent)
|
||||
def optimized_create_maxclaw_agent(system_prompt=None):
|
||||
cache_key = f"agent:{system_prompt}"
|
||||
if cache_key in performance_monitor.cache:
|
||||
cached_time, cached_result = performance_monitor.cache[cache_key]
|
||||
if time.time() - cached_time < 600: # Cache for 10 minutes
|
||||
return cached_result
|
||||
|
||||
result = original_create_maxclaw_agent(system_prompt)
|
||||
performance_monitor.cache[cache_key] = (time.time(), result)
|
||||
return result
|
||||
|
||||
# Replace function
|
||||
import main
|
||||
main.create_maxclaw_agent = optimized_create_maxclaw_agent
|
||||
|
||||
print("✅ Agent creation optimized with caching")
|
||||
|
||||
|
||||
def optimize_api_server():
|
||||
"""Optimize API server with response caching"""
|
||||
try:
|
||||
from api_server import app, list_configurations, get_active_configuration
|
||||
|
||||
# Cache configuration endpoints
|
||||
original_list_configurations = list_configurations
|
||||
original_get_active_configuration = get_active_configuration
|
||||
|
||||
@functools.wraps(original_list_configurations)
|
||||
async def optimized_list_configurations():
|
||||
cache_key = "api:list_configurations"
|
||||
if cache_key in performance_monitor.cache:
|
||||
cached_time, cached_result = performance_monitor.cache[cache_key]
|
||||
if time.time() - cached_time < 10: # Cache for 10 seconds
|
||||
return cached_result
|
||||
|
||||
result = await original_list_configurations()
|
||||
performance_monitor.cache[cache_key] = (time.time(), result)
|
||||
return result
|
||||
|
||||
@functools.wraps(original_get_active_configuration)
|
||||
async def optimized_get_active_configuration():
|
||||
cache_key = "api:get_active_configuration"
|
||||
if cache_key in performance_monitor.cache:
|
||||
cached_time, cached_result = performance_monitor.cache[cache_key]
|
||||
if time.time() - cached_time < 10: # Cache for 10 seconds
|
||||
return cached_result
|
||||
|
||||
result = await original_get_active_configuration()
|
||||
performance_monitor.cache[cache_key] = (time.time(), result)
|
||||
return result
|
||||
|
||||
# Replace functions
|
||||
import api_server
|
||||
api_server.list_configurations = optimized_list_configurations
|
||||
api_server.get_active_configuration = optimized_get_active_configuration
|
||||
|
||||
print("✅ API server optimized with response caching")
|
||||
except ImportError:
|
||||
print("⚠️ API server not available for optimization")
|
||||
|
||||
|
||||
def apply_all_optimizations():
|
||||
"""Apply all performance optimizations"""
|
||||
print("🚀 Applying performance optimizations...")
|
||||
|
||||
try:
|
||||
optimize_config_manager()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not optimize config manager: {e}")
|
||||
|
||||
try:
|
||||
optimize_provider_tools()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not optimize provider tools: {e}")
|
||||
|
||||
try:
|
||||
optimize_agent_creation()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not optimize agent creation: {e}")
|
||||
|
||||
try:
|
||||
optimize_api_server()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not optimize API server: {e}")
|
||||
|
||||
print("✅ All optimizations applied")
|
||||
|
||||
|
||||
def get_performance_report() -> str:
|
||||
"""Get a performance report"""
|
||||
metrics = performance_monitor.get_metrics()
|
||||
|
||||
if not metrics:
|
||||
return "No performance metrics collected yet."
|
||||
|
||||
report = "Performance Report:\n"
|
||||
report += "=" * 50 + "\n"
|
||||
|
||||
for func_name, stats in metrics.items():
|
||||
report += f"\n{func_name}:\n"
|
||||
report += f" Calls: {stats['count']}\n"
|
||||
report += f" Avg Time: {stats['avg_time']:.4f}s\n"
|
||||
report += f" Min Time: {stats['min_time']:.4f}s\n"
|
||||
report += f" Max Time: {stats['max_time']:.4f}s\n"
|
||||
report += f" Total Time: {stats['total_time']:.4f}s\n"
|
||||
|
||||
# Cache statistics
|
||||
cache_size = len(performance_monitor.cache)
|
||||
report += f"\nCache Statistics:\n"
|
||||
report += f" Cached Items: {cache_size}\n"
|
||||
|
||||
# Connection pool statistics
|
||||
pool_stats = performance_monitor.get_connection_pool_stats()
|
||||
report += f"\nConnection Pool:\n"
|
||||
report += f" Total Connections: {pool_stats['total_connections']}\n"
|
||||
report += f" Max Connections: {pool_stats['max_connections']}\n"
|
||||
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Apply optimizations when run directly
|
||||
apply_all_optimizations()
|
||||
|
||||
# Print performance report
|
||||
print("\n" + get_performance_report())
|
||||
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
MaxClaw Agent Plugins Package
|
||||
Provides plugin system for extensibility
|
||||
"""
|
||||
|
||||
from .base import BasePlugin
|
||||
from .manager import PluginManager, plugin_manager
|
||||
|
||||
__all__ = [
|
||||
'BasePlugin',
|
||||
'PluginManager',
|
||||
'plugin_manager'
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Base plugin class for MaxClaw Agent
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Any, Optional
|
||||
from langchain.tools import Tool
|
||||
|
||||
|
||||
class BasePlugin(ABC):
|
||||
"""Base class for MaxClaw plugins"""
|
||||
|
||||
def __init__(self, name: str, version: str = "1.0.0", description: str = ""):
|
||||
"""
|
||||
Initialize plugin
|
||||
|
||||
Args:
|
||||
name: Plugin name
|
||||
version: Plugin version
|
||||
description: Plugin description
|
||||
"""
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.description = description
|
||||
self.enabled = True
|
||||
|
||||
@abstractmethod
|
||||
def get_tools(self) -> List[Tool]:
|
||||
"""
|
||||
Get tools provided by this plugin
|
||||
|
||||
Returns:
|
||||
List of tools
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_commands(self) -> Dict[str, callable]:
|
||||
"""
|
||||
Get commands provided by this plugin
|
||||
|
||||
Returns:
|
||||
Dictionary mapping command names to functions
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_load(self):
|
||||
"""Called when plugin is loaded"""
|
||||
pass
|
||||
|
||||
def on_unload(self):
|
||||
"""Called when plugin is unloaded"""
|
||||
pass
|
||||
|
||||
def get_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get plugin information
|
||||
|
||||
Returns:
|
||||
Dictionary with plugin info
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"version": self.version,
|
||||
"description": self.description,
|
||||
"enabled": self.enabled,
|
||||
"tools_count": len(self.get_tools()),
|
||||
"commands_count": len(self.get_commands())
|
||||
}
|
||||
|
||||
def enable(self):
|
||||
"""Enable the plugin"""
|
||||
self.enabled = True
|
||||
|
||||
def disable(self):
|
||||
"""Disable the plugin"""
|
||||
self.enabled = False
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""Check if plugin is enabled"""
|
||||
return self.enabled
|
||||
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
Example plugin for MaxClaw Agent
|
||||
Demonstrates how to create a plugin
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Dict, Any
|
||||
from langchain.tools import Tool
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from plugins.base import BasePlugin
|
||||
|
||||
|
||||
class ExamplePlugin(BasePlugin):
|
||||
"""Example plugin that provides simple tools and commands"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize example plugin"""
|
||||
super().__init__(
|
||||
name="example",
|
||||
version="1.0.0",
|
||||
description="Example plugin demonstrating plugin system capabilities"
|
||||
)
|
||||
|
||||
def get_tools(self) -> List[Tool]:
|
||||
"""Get tools provided by this plugin"""
|
||||
return [
|
||||
Tool(
|
||||
name="example_hello",
|
||||
func=self.hello_tool,
|
||||
description="Say hello from the example plugin"
|
||||
),
|
||||
Tool(
|
||||
name="example_echo",
|
||||
func=self.echo_tool,
|
||||
description="Echo back the input message"
|
||||
),
|
||||
Tool(
|
||||
name="example_math",
|
||||
func=self.math_tool,
|
||||
description="Perform basic math operations"
|
||||
)
|
||||
]
|
||||
|
||||
def get_commands(self) -> Dict[str, callable]:
|
||||
"""Get commands provided by this plugin"""
|
||||
return {
|
||||
"/example": self.example_command,
|
||||
"/hello": self.hello_command,
|
||||
"/math": self.math_command
|
||||
}
|
||||
|
||||
def hello_tool(self) -> str:
|
||||
"""Hello tool implementation"""
|
||||
return "👋 Hello from the Example Plugin! This is a tool demonstration."
|
||||
|
||||
def echo_tool(self, message: str) -> str:
|
||||
"""Echo tool implementation"""
|
||||
return f"🔊 Echo: {message}"
|
||||
|
||||
def math_tool(self, operation: str, a: float, b: float) -> str:
|
||||
"""Math tool implementation"""
|
||||
try:
|
||||
if operation == "add":
|
||||
result = a + b
|
||||
return f"➕ {a} + {b} = {result}"
|
||||
elif operation == "subtract":
|
||||
result = a - b
|
||||
return f"➖ {a} - {b} = {result}"
|
||||
elif operation == "multiply":
|
||||
result = a * b
|
||||
return f"✖️ {a} × {b} = {result}"
|
||||
elif operation == "divide":
|
||||
if b == 0:
|
||||
return "❌ Error: Division by zero"
|
||||
result = a / b
|
||||
return f"➗ {a} ÷ {b} = {result}"
|
||||
else:
|
||||
return f"❌ Unknown operation: {operation}"
|
||||
except Exception as e:
|
||||
return f"❌ Error: {str(e)}"
|
||||
|
||||
def example_command(self, args: List[str] = None) -> str:
|
||||
"""Example command implementation"""
|
||||
if args:
|
||||
return f"📋 Example command executed with args: {', '.join(args)}"
|
||||
else:
|
||||
return "📋 Example command executed without arguments"
|
||||
|
||||
def hello_command(self, args: List[str] = None) -> str:
|
||||
"""Hello command implementation"""
|
||||
if args:
|
||||
name = " ".join(args)
|
||||
return f"👋 Hello, {name}! This is a command from the Example Plugin."
|
||||
else:
|
||||
return "👋 Hello! This is a command from the Example Plugin."
|
||||
|
||||
def math_command(self, args: List[str] = None) -> str:
|
||||
"""Math command implementation"""
|
||||
if not args or len(args) < 3:
|
||||
return "❌ Usage: /math <operation> <a> <b>\nOperations: add, subtract, multiply, divide"
|
||||
|
||||
try:
|
||||
operation = args[0].lower()
|
||||
a = float(args[1])
|
||||
b = float(args[2])
|
||||
|
||||
if operation == "add":
|
||||
result = a + b
|
||||
return f"➕ {a} + {b} = {result}"
|
||||
elif operation == "subtract":
|
||||
result = a - b
|
||||
return f"➖ {a} - {b} = {result}"
|
||||
elif operation == "multiply":
|
||||
result = a * b
|
||||
return f"✖️ {a} × {b} = {result}"
|
||||
elif operation == "divide":
|
||||
if b == 0:
|
||||
return "❌ Error: Division by zero"
|
||||
result = a / b
|
||||
return f"➗ {a} ÷ {b} = {result}"
|
||||
else:
|
||||
return f"❌ Unknown operation: {operation}"
|
||||
except ValueError:
|
||||
return "❌ Error: Invalid numbers"
|
||||
except Exception as e:
|
||||
return f"❌ Error: {str(e)}"
|
||||
|
||||
def on_load(self):
|
||||
"""Called when plugin is loaded"""
|
||||
print(f"📦 Example plugin loaded: {self.name} v{self.version}")
|
||||
|
||||
def on_unload(self):
|
||||
"""Called when plugin is unloaded"""
|
||||
print(f"🗑️ Example plugin unloaded: {self.name}")
|
||||
@@ -0,0 +1,441 @@
|
||||
"""
|
||||
File tools plugin for MaxClaw Agent
|
||||
Provides additional file management tools
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from typing import List, Dict, Any
|
||||
from pathlib import Path
|
||||
from langchain.tools import Tool
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from plugins.base import BasePlugin
|
||||
|
||||
|
||||
class FileToolsPlugin(BasePlugin):
|
||||
"""Plugin providing additional file management tools"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize file tools plugin"""
|
||||
super().__init__(
|
||||
name="file_tools",
|
||||
version="1.0.0",
|
||||
description="Additional file management tools for MaxClaw Agent"
|
||||
)
|
||||
|
||||
def get_tools(self) -> List[Tool]:
|
||||
"""Get tools provided by this plugin"""
|
||||
return [
|
||||
Tool(
|
||||
name="file_find",
|
||||
func=self.find_files,
|
||||
description="Find files by name pattern"
|
||||
),
|
||||
Tool(
|
||||
name="file_size",
|
||||
func=self.get_file_size,
|
||||
description="Get file or directory size"
|
||||
),
|
||||
Tool(
|
||||
name="file_copy",
|
||||
func=self.copy_file,
|
||||
description="Copy a file to a new location"
|
||||
),
|
||||
Tool(
|
||||
name="file_move",
|
||||
func=self.move_file,
|
||||
description="Move a file to a new location"
|
||||
),
|
||||
Tool(
|
||||
name="file_delete",
|
||||
func=self.delete_file,
|
||||
description="Delete a file or directory"
|
||||
),
|
||||
Tool(
|
||||
name="file_info",
|
||||
func=self.get_file_info,
|
||||
description="Get detailed file information"
|
||||
),
|
||||
Tool(
|
||||
name="dir_list",
|
||||
func=self.list_directory,
|
||||
description="List directory contents with details"
|
||||
),
|
||||
Tool(
|
||||
name="dir_create",
|
||||
func=self.create_directory,
|
||||
description="Create a new directory"
|
||||
)
|
||||
]
|
||||
|
||||
def get_commands(self) -> Dict[str, callable]:
|
||||
"""Get commands provided by this plugin"""
|
||||
return {
|
||||
"/find": self.find_command,
|
||||
"/size": self.size_command,
|
||||
"/copy": self.copy_command,
|
||||
"/move": self.move_command,
|
||||
"/delete": self.delete_command,
|
||||
"/info": self.info_command,
|
||||
"/dir": self.dir_command,
|
||||
"/mkdir": self.mkdir_command
|
||||
}
|
||||
|
||||
def find_files(self, pattern: str, directory: str = ".") -> str:
|
||||
"""Find files by name pattern"""
|
||||
try:
|
||||
path = Path(directory)
|
||||
if not path.exists():
|
||||
return f"❌ Directory not found: {directory}"
|
||||
|
||||
matches = list(path.rglob(pattern))
|
||||
|
||||
if not matches:
|
||||
return f"No files found matching '{pattern}' in {directory}"
|
||||
|
||||
output = f"🔍 Found {len(matches)} files matching '{pattern}':\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for match in matches[:20]: # Limit to 20 results
|
||||
if match.is_file():
|
||||
size = match.stat().st_size
|
||||
if size > 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024):.1f} MB"
|
||||
elif size > 1024:
|
||||
size_str = f"{size / 1024:.1f} KB"
|
||||
else:
|
||||
size_str = f"{size} B"
|
||||
|
||||
output += f"📄 {match.relative_to(path)} ({size_str})\n"
|
||||
|
||||
if len(matches) > 20:
|
||||
output += f"\n... and {len(matches) - 20} more files"
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error finding files: {str(e)}"
|
||||
|
||||
def get_file_size(self, path: str) -> str:
|
||||
"""Get file or directory size"""
|
||||
try:
|
||||
path_obj = Path(path)
|
||||
|
||||
if not path_obj.exists():
|
||||
return f"❌ Path not found: {path}"
|
||||
|
||||
if path_obj.is_file():
|
||||
size = path_obj.stat().st_size
|
||||
if size > 1024 * 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024 * 1024):.2f} GB"
|
||||
elif size > 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024):.2f} MB"
|
||||
elif size > 1024:
|
||||
size_str = f"{size / 1024:.2f} KB"
|
||||
else:
|
||||
size_str = f"{size} bytes"
|
||||
|
||||
return f"📄 {path}: {size_str}"
|
||||
|
||||
elif path_obj.is_dir():
|
||||
total_size = 0
|
||||
file_count = 0
|
||||
|
||||
for item in path_obj.rglob('*'):
|
||||
if item.is_file():
|
||||
total_size += item.stat().st_size
|
||||
file_count += 1
|
||||
|
||||
if total_size > 1024 * 1024 * 1024:
|
||||
size_str = f"{total_size / (1024 * 1024 * 1024):.2f} GB"
|
||||
elif total_size > 1024 * 1024:
|
||||
size_str = f"{total_size / (1024 * 1024):.2f} MB"
|
||||
elif total_size > 1024:
|
||||
size_str = f"{total_size / 1024:.2f} KB"
|
||||
else:
|
||||
size_str = f"{total_size} bytes"
|
||||
|
||||
return f"📁 {path}: {size_str} ({file_count} files)"
|
||||
|
||||
else:
|
||||
return f"❌ Unknown path type: {path}"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting size: {str(e)}"
|
||||
|
||||
def copy_file(self, source: str, destination: str) -> str:
|
||||
"""Copy a file to a new location"""
|
||||
try:
|
||||
source_path = Path(source)
|
||||
dest_path = Path(destination)
|
||||
|
||||
if not source_path.exists():
|
||||
return f"❌ Source not found: {source}"
|
||||
|
||||
# If destination is a directory, copy into it
|
||||
if dest_path.is_dir():
|
||||
dest_path = dest_path / source_path.name
|
||||
|
||||
# Create parent directories if needed
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if source_path.is_file():
|
||||
shutil.copy2(source_path, dest_path)
|
||||
return f"✅ File copied: {source} → {destination}"
|
||||
elif source_path.is_dir():
|
||||
shutil.copytree(source_path, dest_path)
|
||||
return f"✅ Directory copied: {source} → {destination}"
|
||||
else:
|
||||
return f"❌ Unknown source type: {source}"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error copying: {str(e)}"
|
||||
|
||||
def move_file(self, source: str, destination: str) -> str:
|
||||
"""Move a file to a new location"""
|
||||
try:
|
||||
source_path = Path(source)
|
||||
dest_path = Path(destination)
|
||||
|
||||
if not source_path.exists():
|
||||
return f"❌ Source not found: {source}"
|
||||
|
||||
# If destination is a directory, move into it
|
||||
if dest_path.is_dir():
|
||||
dest_path = dest_path / source_path.name
|
||||
|
||||
# Create parent directories if needed
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
shutil.move(str(source_path), str(dest_path))
|
||||
return f"✅ Moved: {source} → {destination}"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error moving: {str(e)}"
|
||||
|
||||
def delete_file(self, path: str, confirm: bool = True) -> str:
|
||||
"""Delete a file or directory"""
|
||||
try:
|
||||
path_obj = Path(path)
|
||||
|
||||
if not path_obj.exists():
|
||||
return f"❌ Path not found: {path}"
|
||||
|
||||
if confirm:
|
||||
return f"⚠️ To delete {path}, use the command with confirm=True"
|
||||
|
||||
if path_obj.is_file():
|
||||
path_obj.unlink()
|
||||
return f"✅ File deleted: {path}"
|
||||
elif path_obj.is_dir():
|
||||
shutil.rmtree(path_obj)
|
||||
return f"✅ Directory deleted: {path}"
|
||||
else:
|
||||
return f"❌ Unknown path type: {path}"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error deleting: {str(e)}"
|
||||
|
||||
def get_file_info(self, path: str) -> str:
|
||||
"""Get detailed file information"""
|
||||
try:
|
||||
path_obj = Path(path)
|
||||
|
||||
if not path_obj.exists():
|
||||
return f"❌ Path not found: {path}"
|
||||
|
||||
stat = path_obj.stat()
|
||||
|
||||
# Format size
|
||||
size = stat.st_size
|
||||
if size > 1024 * 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024 * 1024):.2f} GB"
|
||||
elif size > 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024):.2f} MB"
|
||||
elif size > 1024:
|
||||
size_str = f"{size / 1024:.2f} KB"
|
||||
else:
|
||||
size_str = f"{size} bytes"
|
||||
|
||||
# Format dates
|
||||
from datetime import datetime
|
||||
created = datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
modified = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
accessed = datetime.fromtimestamp(stat.st_atime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# File type
|
||||
if path_obj.is_file():
|
||||
file_type = "File"
|
||||
elif path_obj.is_dir():
|
||||
file_type = "Directory"
|
||||
elif path_obj.is_symlink():
|
||||
file_type = "Symbolic Link"
|
||||
else:
|
||||
file_type = "Unknown"
|
||||
|
||||
output = f"📋 File Information: {path}\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += f"Type: {file_type}\n"
|
||||
output += f"Size: {size_str}\n"
|
||||
output += f"Created: {created}\n"
|
||||
output += f"Modified: {modified}\n"
|
||||
output += f"Accessed: {accessed}\n"
|
||||
output += f"Permissions: {oct(stat.st_mode)[-3:]}\n"
|
||||
|
||||
if path_obj.is_file():
|
||||
output += f"Extension: {path_obj.suffix}\n"
|
||||
output += f"Name: {path_obj.name}\n"
|
||||
output += f"Parent: {path_obj.parent}\n"
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting file info: {str(e)}"
|
||||
|
||||
def list_directory(self, path: str = ".", show_hidden: bool = False) -> str:
|
||||
"""List directory contents with details"""
|
||||
try:
|
||||
path_obj = Path(path)
|
||||
|
||||
if not path_obj.exists():
|
||||
return f"❌ Directory not found: {path}"
|
||||
|
||||
if not path_obj.is_dir():
|
||||
return f"❌ Not a directory: {path}"
|
||||
|
||||
items = list(path_obj.iterdir())
|
||||
|
||||
if not show_hidden:
|
||||
items = [item for item in items if not item.name.startswith('.')]
|
||||
|
||||
if not items:
|
||||
return f"📁 Directory is empty: {path}"
|
||||
|
||||
output = f"📁 Directory: {path}\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
# Sort items: directories first, then files
|
||||
dirs = [item for item in items if item.is_dir()]
|
||||
files = [item for item in items if item.is_file()]
|
||||
|
||||
# List directories
|
||||
for dir_item in sorted(dirs, key=lambda x: x.name.lower()):
|
||||
try:
|
||||
size = sum(f.stat().st_size for f in dir_item.rglob('*') if f.is_file())
|
||||
if size > 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024):.1f} MB"
|
||||
elif size > 1024:
|
||||
size_str = f"{size / 1024:.1f} KB"
|
||||
else:
|
||||
size_str = f"{size} B"
|
||||
|
||||
output += f"📁 {dir_item.name}/ ({size_str})\n"
|
||||
except:
|
||||
output += f"📁 {dir_item.name}/\n"
|
||||
|
||||
# List files
|
||||
for file_item in sorted(files, key=lambda x: x.name.lower()):
|
||||
try:
|
||||
size = file_item.stat().st_size
|
||||
if size > 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024):.1f} MB"
|
||||
elif size > 1024:
|
||||
size_str = f"{size / 1024:.1f} KB"
|
||||
else:
|
||||
size_str = f"{size} B"
|
||||
|
||||
output += f"📄 {file_item.name} ({size_str})\n"
|
||||
except:
|
||||
output += f"📄 {file_item.name}\n"
|
||||
|
||||
output += f"\nTotal: {len(dirs)} directories, {len(files)} files"
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error listing directory: {str(e)}"
|
||||
|
||||
def create_directory(self, path: str, parents: bool = True) -> str:
|
||||
"""Create a new directory"""
|
||||
try:
|
||||
path_obj = Path(path)
|
||||
|
||||
if path_obj.exists():
|
||||
return f"⚠️ Path already exists: {path}"
|
||||
|
||||
path_obj.mkdir(parents=parents, exist_ok=True)
|
||||
return f"✅ Directory created: {path}"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error creating directory: {str(e)}"
|
||||
|
||||
# Command implementations
|
||||
def find_command(self, args: List[str] = None) -> str:
|
||||
"""Find command implementation"""
|
||||
if not args:
|
||||
return "❌ Usage: /find <pattern> [directory]"
|
||||
|
||||
pattern = args[0]
|
||||
directory = args[1] if len(args) > 1 else "."
|
||||
|
||||
return self.find_files(pattern, directory)
|
||||
|
||||
def size_command(self, args: List[str] = None) -> str:
|
||||
"""Size command implementation"""
|
||||
if not args:
|
||||
return "❌ Usage: /size <path>"
|
||||
|
||||
return self.get_file_size(args[0])
|
||||
|
||||
def copy_command(self, args: List[str] = None) -> str:
|
||||
"""Copy command implementation"""
|
||||
if not args or len(args) < 2:
|
||||
return "❌ Usage: /copy <source> <destination>"
|
||||
|
||||
return self.copy_file(args[0], args[1])
|
||||
|
||||
def move_command(self, args: List[str] = None) -> str:
|
||||
"""Move command implementation"""
|
||||
if not args or len(args) < 2:
|
||||
return "❌ Usage: /move <source> <destination>"
|
||||
|
||||
return self.move_file(args[0], args[1])
|
||||
|
||||
def delete_command(self, args: List[str] = None) -> str:
|
||||
"""Delete command implementation"""
|
||||
if not args:
|
||||
return "❌ Usage: /delete <path>"
|
||||
|
||||
return self.delete_file(args[0], confirm=False)
|
||||
|
||||
def info_command(self, args: List[str] = None) -> str:
|
||||
"""Info command implementation"""
|
||||
if not args:
|
||||
return "❌ Usage: /info <path>"
|
||||
|
||||
return self.get_file_info(args[0])
|
||||
|
||||
def dir_command(self, args: List[str] = None) -> str:
|
||||
"""Dir command implementation"""
|
||||
path = args[0] if args else "."
|
||||
show_hidden = "-a" in args if args else False
|
||||
|
||||
return self.list_directory(path, show_hidden)
|
||||
|
||||
def mkdir_command(self, args: List[str] = None) -> str:
|
||||
"""Mkdir command implementation"""
|
||||
if not args:
|
||||
return "❌ Usage: /mkdir <path>"
|
||||
|
||||
return self.create_directory(args[0])
|
||||
|
||||
def on_load(self):
|
||||
"""Called when plugin is loaded"""
|
||||
print(f"📦 File tools plugin loaded: {self.name} v{self.version}")
|
||||
|
||||
def on_unload(self):
|
||||
"""Called when plugin is unloaded"""
|
||||
print(f"🗑️ File tools plugin unloaded: {self.name}")
|
||||
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
Plugin manager for MaxClaw Agent
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
import inspect
|
||||
from typing import Dict, List, Optional, Any, Type
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from plugins.base import BasePlugin
|
||||
|
||||
|
||||
class PluginManager:
|
||||
"""Manages plugins for MaxClaw Agent"""
|
||||
|
||||
def __init__(self, plugin_dir: Optional[str] = None):
|
||||
"""
|
||||
Initialize plugin manager
|
||||
|
||||
Args:
|
||||
plugin_dir: Directory containing plugins. If None, uses ./plugins
|
||||
"""
|
||||
if plugin_dir is None:
|
||||
self.plugin_dir = Path(__file__).parent
|
||||
else:
|
||||
self.plugin_dir = Path(plugin_dir)
|
||||
|
||||
self.plugins: Dict[str, BasePlugin] = {}
|
||||
self.loaded_modules = {}
|
||||
|
||||
def discover_plugins(self) -> List[str]:
|
||||
"""
|
||||
Discover available plugins
|
||||
|
||||
Returns:
|
||||
List of plugin module names
|
||||
"""
|
||||
plugin_files = []
|
||||
|
||||
# Look for Python files in plugin directory
|
||||
for file_path in self.plugin_dir.glob("*.py"):
|
||||
if file_path.name != "__init__.py" and file_path.name != "base.py" and file_path.name != "manager.py":
|
||||
module_name = file_path.stem
|
||||
plugin_files.append(module_name)
|
||||
|
||||
return plugin_files
|
||||
|
||||
def load_plugin(self, module_name: str) -> bool:
|
||||
"""
|
||||
Load a plugin from module name
|
||||
|
||||
Args:
|
||||
module_name: Name of the module to load
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Import the module
|
||||
module_path = f"plugins.{module_name}"
|
||||
module = importlib.import_module(module_path)
|
||||
|
||||
# Find plugin classes in the module
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if (inspect.isclass(obj) and
|
||||
issubclass(obj, BasePlugin) and
|
||||
obj != BasePlugin):
|
||||
|
||||
# Instantiate the plugin
|
||||
plugin = obj()
|
||||
|
||||
# Check if plugin with same name already exists
|
||||
if plugin.name in self.plugins:
|
||||
print(f"⚠️ Plugin '{plugin.name}' already loaded")
|
||||
return False
|
||||
|
||||
# Load the plugin
|
||||
plugin.on_load()
|
||||
self.plugins[plugin.name] = plugin
|
||||
self.loaded_modules[plugin.name] = module
|
||||
|
||||
print(f"✅ Loaded plugin: {plugin.name} v{plugin.version}")
|
||||
return True
|
||||
|
||||
print(f"⚠️ No plugin class found in module: {module_name}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error loading plugin {module_name}: {e}")
|
||||
return False
|
||||
|
||||
def load_all_plugins(self):
|
||||
"""Load all discovered plugins"""
|
||||
plugin_modules = self.discover_plugins()
|
||||
|
||||
for module_name in plugin_modules:
|
||||
self.load_plugin(module_name)
|
||||
|
||||
def unload_plugin(self, plugin_name: str) -> bool:
|
||||
"""
|
||||
Unload a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to unload
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if plugin_name not in self.plugins:
|
||||
print(f"⚠️ Plugin '{plugin_name}' not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Unload the plugin
|
||||
plugin = self.plugins[plugin_name]
|
||||
plugin.on_unload()
|
||||
|
||||
# Remove from dictionaries
|
||||
del self.plugins[plugin_name]
|
||||
if plugin_name in self.loaded_modules:
|
||||
del self.loaded_modules[plugin_name]
|
||||
|
||||
print(f"✅ Unloaded plugin: {plugin_name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error unloading plugin {plugin_name}: {e}")
|
||||
return False
|
||||
|
||||
def get_plugin(self, plugin_name: str) -> Optional[BasePlugin]:
|
||||
"""
|
||||
Get a plugin by name
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin
|
||||
|
||||
Returns:
|
||||
Plugin instance or None if not found
|
||||
"""
|
||||
return self.plugins.get(plugin_name)
|
||||
|
||||
def get_all_plugins(self) -> List[BasePlugin]:
|
||||
"""
|
||||
Get all loaded plugins
|
||||
|
||||
Returns:
|
||||
List of plugin instances
|
||||
"""
|
||||
return list(self.plugins.values())
|
||||
|
||||
def get_enabled_plugins(self) -> List[BasePlugin]:
|
||||
"""
|
||||
Get all enabled plugins
|
||||
|
||||
Returns:
|
||||
List of enabled plugin instances
|
||||
"""
|
||||
return [plugin for plugin in self.plugins.values() if plugin.is_enabled()]
|
||||
|
||||
def get_all_tools(self) -> List:
|
||||
"""
|
||||
Get all tools from enabled plugins
|
||||
|
||||
Returns:
|
||||
List of tools
|
||||
"""
|
||||
all_tools = []
|
||||
|
||||
for plugin in self.get_enabled_plugins():
|
||||
try:
|
||||
tools = plugin.get_tools()
|
||||
all_tools.extend(tools)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error getting tools from plugin {plugin.name}: {e}")
|
||||
|
||||
return all_tools
|
||||
|
||||
def get_all_commands(self) -> Dict[str, callable]:
|
||||
"""
|
||||
Get all commands from enabled plugins
|
||||
|
||||
Returns:
|
||||
Dictionary mapping command names to functions
|
||||
"""
|
||||
all_commands = {}
|
||||
|
||||
for plugin in self.get_enabled_plugins():
|
||||
try:
|
||||
commands = plugin.get_commands()
|
||||
all_commands.update(commands)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error getting commands from plugin {plugin.name}: {e}")
|
||||
|
||||
return all_commands
|
||||
|
||||
def enable_plugin(self, plugin_name: str) -> bool:
|
||||
"""
|
||||
Enable a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to enable
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
plugin = self.get_plugin(plugin_name)
|
||||
if not plugin:
|
||||
print(f"⚠️ Plugin '{plugin_name}' not found")
|
||||
return False
|
||||
|
||||
plugin.enable()
|
||||
print(f"✅ Enabled plugin: {plugin_name}")
|
||||
return True
|
||||
|
||||
def disable_plugin(self, plugin_name: str) -> bool:
|
||||
"""
|
||||
Disable a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to disable
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
plugin = self.get_plugin(plugin_name)
|
||||
if not plugin:
|
||||
print(f"⚠️ Plugin '{plugin_name}' not found")
|
||||
return False
|
||||
|
||||
plugin.disable()
|
||||
print(f"✅ Disabled plugin: {plugin_name}")
|
||||
return True
|
||||
|
||||
def get_plugin_info(self, plugin_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get information about a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin
|
||||
|
||||
Returns:
|
||||
Dictionary with plugin info or None if not found
|
||||
"""
|
||||
plugin = self.get_plugin(plugin_name)
|
||||
if not plugin:
|
||||
return None
|
||||
|
||||
return plugin.get_info()
|
||||
|
||||
def get_all_plugins_info(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get information about all plugins
|
||||
|
||||
Returns:
|
||||
List of dictionaries with plugin info
|
||||
"""
|
||||
return [plugin.get_info() for plugin in self.plugins.values()]
|
||||
|
||||
def reload_plugin(self, plugin_name: str) -> bool:
|
||||
"""
|
||||
Reload a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to reload
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Find the module name for this plugin
|
||||
module_name = None
|
||||
for name, module in self.loaded_modules.items():
|
||||
if name == plugin_name:
|
||||
# Get the module name from the module object
|
||||
module_name = module.__name__.split('.')[-1]
|
||||
break
|
||||
|
||||
if not module_name:
|
||||
print(f"⚠️ Could not find module for plugin '{plugin_name}'")
|
||||
return False
|
||||
|
||||
# Unload the plugin
|
||||
if not self.unload_plugin(plugin_name):
|
||||
return False
|
||||
|
||||
# Reload the module
|
||||
try:
|
||||
module_path = f"plugins.{module_name}"
|
||||
if module_path in sys.modules:
|
||||
importlib.reload(sys.modules[module_path])
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error reloading module: {e}")
|
||||
|
||||
# Load the plugin again
|
||||
return self.load_plugin(module_name)
|
||||
|
||||
|
||||
# Global plugin manager instance
|
||||
plugin_manager = PluginManager()
|
||||
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
System utilities plugin for MaxClaw Agent
|
||||
Provides system information and utility tools
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import socket
|
||||
import subprocess
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime
|
||||
from langchain.tools import Tool
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from plugins.base import BasePlugin
|
||||
|
||||
|
||||
class SystemUtilsPlugin(BasePlugin):
|
||||
"""Plugin providing system utilities and information"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize system utilities plugin"""
|
||||
super().__init__(
|
||||
name="system_utils",
|
||||
version="1.0.0",
|
||||
description="System information and utility tools"
|
||||
)
|
||||
|
||||
def get_tools(self) -> List[Tool]:
|
||||
"""Get tools provided by this plugin"""
|
||||
return [
|
||||
Tool(
|
||||
name="system_info",
|
||||
func=self.get_system_info,
|
||||
description="Get comprehensive system information"
|
||||
),
|
||||
Tool(
|
||||
name="system_uptime",
|
||||
func=self.get_system_uptime,
|
||||
description="Get system uptime"
|
||||
),
|
||||
Tool(
|
||||
name="system_ip",
|
||||
func=self.get_ip_address,
|
||||
description="Get system IP addresses"
|
||||
),
|
||||
Tool(
|
||||
name="system_hostname",
|
||||
func=self.get_hostname,
|
||||
description="Get system hostname"
|
||||
),
|
||||
Tool(
|
||||
name="system_platform",
|
||||
func=self.get_platform_info,
|
||||
description="Get platform information"
|
||||
),
|
||||
Tool(
|
||||
name="system_users",
|
||||
func=self.get_logged_users,
|
||||
description="Get logged in users"
|
||||
),
|
||||
Tool(
|
||||
name="system_processes",
|
||||
func=self.get_running_processes,
|
||||
description="Get running processes"
|
||||
),
|
||||
Tool(
|
||||
name="system_disk_usage",
|
||||
func=self.get_disk_usage,
|
||||
description="Get disk usage information"
|
||||
)
|
||||
]
|
||||
|
||||
def get_commands(self) -> Dict[str, callable]:
|
||||
"""Get commands provided by this plugin"""
|
||||
return {
|
||||
"/sysinfo": self.sysinfo_command,
|
||||
"/uptime": self.uptime_command,
|
||||
"/ip": self.ip_command,
|
||||
"/hostname": self.hostname_command,
|
||||
"/platform": self.platform_command,
|
||||
"/users": self.users_command,
|
||||
"/ps": self.ps_command,
|
||||
"/df": self.df_command
|
||||
}
|
||||
|
||||
def get_system_info(self) -> str:
|
||||
"""Get comprehensive system information"""
|
||||
try:
|
||||
info = {
|
||||
"System": platform.system(),
|
||||
"Node Name": platform.node(),
|
||||
"Release": platform.release(),
|
||||
"Version": platform.version(),
|
||||
"Machine": platform.machine(),
|
||||
"Processor": platform.processor(),
|
||||
"Python Version": platform.python_version(),
|
||||
"Hostname": socket.gethostname(),
|
||||
"IP Address": socket.gethostbyname(socket.gethostname()),
|
||||
"Current User": os.getlogin(),
|
||||
"Current Directory": os.getcwd(),
|
||||
"Home Directory": os.path.expanduser("~"),
|
||||
"Timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
output = "💻 System Information:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for key, value in info.items():
|
||||
output += f"{key}: {value}\n"
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting system info: {str(e)}"
|
||||
|
||||
def get_system_uptime(self) -> str:
|
||||
"""Get system uptime"""
|
||||
try:
|
||||
if platform.system() == "Linux":
|
||||
with open('/proc/uptime', 'r') as f:
|
||||
uptime_seconds = float(f.readline().split()[0])
|
||||
|
||||
days = int(uptime_seconds // (24 * 3600))
|
||||
hours = int((uptime_seconds % (24 * 3600)) // 3600)
|
||||
minutes = int((uptime_seconds % 3600) // 60)
|
||||
seconds = int(uptime_seconds % 60)
|
||||
|
||||
if days > 0:
|
||||
return f"⏱️ System uptime: {days} days, {hours} hours, {minutes} minutes, {seconds} seconds"
|
||||
elif hours > 0:
|
||||
return f"⏱️ System uptime: {hours} hours, {minutes} minutes, {seconds} seconds"
|
||||
elif minutes > 0:
|
||||
return f"⏱️ System uptime: {minutes} minutes, {seconds} seconds"
|
||||
else:
|
||||
return f"⏱️ System uptime: {seconds} seconds"
|
||||
|
||||
elif platform.system() == "Darwin": # macOS
|
||||
result = subprocess.run(['uptime'], capture_output=True, text=True)
|
||||
return f"⏱️ {result.stdout.strip()}"
|
||||
|
||||
elif platform.system() == "Windows":
|
||||
result = subprocess.run(['net', 'stats', 'srv'], capture_output=True, text=True)
|
||||
# Parse Windows uptime from output
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'Statistics since' in line:
|
||||
return f"⏱️ {line.strip()}"
|
||||
|
||||
return "⏱️ Uptime information not available"
|
||||
|
||||
else:
|
||||
return "⏱️ Uptime information not available for this platform"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting uptime: {str(e)}"
|
||||
|
||||
def get_ip_address(self) -> str:
|
||||
"""Get system IP addresses"""
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
|
||||
# Get all IP addresses
|
||||
ip_addresses = []
|
||||
|
||||
# Get hostname IP
|
||||
try:
|
||||
host_ip = socket.gethostbyname(hostname)
|
||||
ip_addresses.append(("Hostname", host_ip))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get all network interfaces (if available)
|
||||
try:
|
||||
import netifaces
|
||||
for interface in netifaces.interfaces():
|
||||
addrs = netifaces.ifaddresses(interface)
|
||||
if netifaces.AF_INET in addrs:
|
||||
for addr in addrs[netifaces.AF_INET]:
|
||||
ip_addresses.append((interface, addr['addr']))
|
||||
except ImportError:
|
||||
# netifaces not available, use alternative method
|
||||
pass
|
||||
|
||||
if not ip_addresses:
|
||||
return "❌ No IP addresses found"
|
||||
|
||||
output = "🌐 IP Addresses:\n"
|
||||
output += "=" * 30 + "\n"
|
||||
|
||||
for interface, ip in ip_addresses:
|
||||
output += f"{interface}: {ip}\n"
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting IP addresses: {str(e)}"
|
||||
|
||||
def get_hostname(self) -> str:
|
||||
"""Get system hostname"""
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
fqdn = socket.getfqdn()
|
||||
|
||||
output = "🏠 Hostname Information:\n"
|
||||
output += "=" * 30 + "\n"
|
||||
output += f"Hostname: {hostname}\n"
|
||||
output += f"FQDN: {fqdn}\n"
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting hostname: {str(e)}"
|
||||
|
||||
def get_platform_info(self) -> str:
|
||||
"""Get platform information"""
|
||||
try:
|
||||
info = {
|
||||
"System": platform.system(),
|
||||
"Node": platform.node(),
|
||||
"Release": platform.release(),
|
||||
"Version": platform.version(),
|
||||
"Machine": platform.machine(),
|
||||
"Processor": platform.processor(),
|
||||
"Python Build": platform.python_build(),
|
||||
"Python Compiler": platform.python_compiler(),
|
||||
"Python Implementation": platform.python_implementation(),
|
||||
"Python Version": platform.python_version()
|
||||
}
|
||||
|
||||
output = "🖥️ Platform Information:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for key, value in info.items():
|
||||
output += f"{key}: {value}\n"
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting platform info: {str(e)}"
|
||||
|
||||
def get_logged_users(self) -> str:
|
||||
"""Get logged in users"""
|
||||
try:
|
||||
if platform.system() in ["Linux", "Darwin"]:
|
||||
result = subprocess.run(['who'], capture_output=True, text=True)
|
||||
users = result.stdout.strip()
|
||||
|
||||
if not users:
|
||||
return "👥 No users currently logged in"
|
||||
|
||||
output = "👥 Logged in users:\n"
|
||||
output += "=" * 30 + "\n"
|
||||
output += users
|
||||
|
||||
return output
|
||||
|
||||
elif platform.system() == "Windows":
|
||||
result = subprocess.run(['query', 'user'], capture_output=True, text=True)
|
||||
users = result.stdout.strip()
|
||||
|
||||
if not users:
|
||||
return "👥 No users currently logged in"
|
||||
|
||||
output = "👥 Logged in users:\n"
|
||||
output += "=" * 30 + "\n"
|
||||
output += users
|
||||
|
||||
return output
|
||||
|
||||
else:
|
||||
return "👥 User information not available for this platform"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting logged users: {str(e)}"
|
||||
|
||||
def get_running_processes(self, limit: int = 20) -> str:
|
||||
"""Get running processes"""
|
||||
try:
|
||||
if platform.system() in ["Linux", "Darwin"]:
|
||||
result = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
|
||||
processes = result.stdout.strip().split('\n')
|
||||
|
||||
if len(processes) <= 1:
|
||||
return "🔄 No processes found"
|
||||
|
||||
output = f"🔄 Running processes (top {limit}):\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
# Header
|
||||
output += processes[0] + "\n"
|
||||
output += "-" * 50 + "\n"
|
||||
|
||||
# Processes (limit to specified number)
|
||||
for process in processes[1:limit+1]:
|
||||
output += process + "\n"
|
||||
|
||||
if len(processes) > limit+1:
|
||||
output += f"\n... and {len(processes) - limit - 1} more processes"
|
||||
|
||||
return output
|
||||
|
||||
elif platform.system() == "Windows":
|
||||
result = subprocess.run(['tasklist'], capture_output=True, text=True)
|
||||
processes = result.stdout.strip().split('\n')
|
||||
|
||||
if len(processes) <= 3:
|
||||
return "🔄 No processes found"
|
||||
|
||||
output = f"🔄 Running processes (top {limit}):\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
# Header (first 3 lines in Windows)
|
||||
for i in range(3):
|
||||
if i < len(processes):
|
||||
output += processes[i] + "\n"
|
||||
|
||||
output += "-" * 50 + "\n"
|
||||
|
||||
# Processes (limit to specified number)
|
||||
for process in processes[3:limit+3]:
|
||||
output += process + "\n"
|
||||
|
||||
if len(processes) > limit+3:
|
||||
output += f"\n... and {len(processes) - limit - 3} more processes"
|
||||
|
||||
return output
|
||||
|
||||
else:
|
||||
return "🔄 Process information not available for this platform"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting processes: {str(e)}"
|
||||
|
||||
def get_disk_usage(self, path: str = "/") -> str:
|
||||
"""Get disk usage information"""
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
# For Windows, use different drive letters
|
||||
if path == "/":
|
||||
path = "C:\\"
|
||||
|
||||
import shutil
|
||||
total, used, free = shutil.disk_usage(path)
|
||||
|
||||
# Convert to human readable
|
||||
def format_bytes(bytes):
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes < 1024.0:
|
||||
return f"{bytes:.2f} {unit}"
|
||||
bytes /= 1024.0
|
||||
return f"{bytes:.2f} PB"
|
||||
|
||||
total_str = format_bytes(total)
|
||||
used_str = format_bytes(used)
|
||||
free_str = format_bytes(free)
|
||||
percent_used = (used / total) * 100
|
||||
|
||||
output = f"💾 Disk Usage for {path}:\n"
|
||||
output += "=" * 40 + "\n"
|
||||
output += f"Total: {total_str}\n"
|
||||
output += f"Used: {used_str} ({percent_used:.1f}%)\n"
|
||||
output += f"Free: {free_str}\n"
|
||||
|
||||
# Create a simple progress bar
|
||||
bar_length = 30
|
||||
filled_length = int(bar_length * percent_used // 100)
|
||||
bar = '█' * filled_length + '░' * (bar_length - filled_length)
|
||||
output += f"\n[{bar}] {percent_used:.1f}%\n"
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ Error getting disk usage: {str(e)}"
|
||||
|
||||
# Command implementations
|
||||
def sysinfo_command(self, args: List[str] = None) -> str:
|
||||
"""Sysinfo command implementation"""
|
||||
return self.get_system_info()
|
||||
|
||||
def uptime_command(self, args: List[str] = None) -> str:
|
||||
"""Uptime command implementation"""
|
||||
return self.get_system_uptime()
|
||||
|
||||
def ip_command(self, args: List[str] = None) -> str:
|
||||
"""IP command implementation"""
|
||||
return self.get_ip_address()
|
||||
|
||||
def hostname_command(self, args: List[str] = None) -> str:
|
||||
"""Hostname command implementation"""
|
||||
return self.get_hostname()
|
||||
|
||||
def platform_command(self, args: List[str] = None) -> str:
|
||||
"""Platform command implementation"""
|
||||
return self.get_platform_info()
|
||||
|
||||
def users_command(self, args: List[str] = None) -> str:
|
||||
"""Users command implementation"""
|
||||
return self.get_logged_users()
|
||||
|
||||
def ps_command(self, args: List[str] = None) -> str:
|
||||
"""PS command implementation"""
|
||||
limit = 20
|
||||
if args and args[0].isdigit():
|
||||
limit = int(args[0])
|
||||
|
||||
return self.get_running_processes(limit)
|
||||
|
||||
def df_command(self, args: List[str] = None) -> str:
|
||||
"""DF command implementation"""
|
||||
path = args[0] if args else "/"
|
||||
return self.get_disk_usage(path)
|
||||
|
||||
def on_load(self):
|
||||
"""Called when plugin is loaded"""
|
||||
print(f"📦 System utils plugin loaded: {self.name} v{self.version}")
|
||||
|
||||
def on_unload(self):
|
||||
"""Called when plugin is unloaded"""
|
||||
print(f"🗑️ System utils plugin unloaded: {self.name}")
|
||||
+8
-1
@@ -1,4 +1,11 @@
|
||||
deepagents>=0.0.34
|
||||
langchain>=0.3.0
|
||||
langchain-community>=0.3.0
|
||||
langchain-openai>=0.1.0
|
||||
langchain-openai>=0.1.0
|
||||
cryptography>=41.0.0
|
||||
requests>=2.31.0
|
||||
pyyaml>=6.0
|
||||
fastapi>=0.104.0
|
||||
uvicorn>=0.24.0
|
||||
pydantic>=2.5.0
|
||||
pytest>=7.4.0
|
||||
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Test suite for MaxClaw Agent configuration manager
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import tempfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config_manager import ConfigManager
|
||||
|
||||
|
||||
class TestConfigManager:
|
||||
"""Test cases for ConfigManager"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.config_manager = ConfigManager(self.temp_dir)
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test environment"""
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test ConfigManager initialization"""
|
||||
assert self.config_manager.config_dir.exists()
|
||||
assert self.config_manager.config_file.exists()
|
||||
assert self.config_manager.key_file.exists()
|
||||
assert self.config_manager.config == {"configurations": {}, "active_configuration": None, "version": "1.0"}
|
||||
|
||||
def test_add_configuration(self):
|
||||
"""Test adding a configuration"""
|
||||
config = {
|
||||
"provider": "ollama",
|
||||
"model_name": "gemma2:9b",
|
||||
"base_url": "http://localhost:11434"
|
||||
}
|
||||
|
||||
self.config_manager.add_configuration("test-config", config)
|
||||
|
||||
# Verify configuration was added
|
||||
assert self.config_manager.configuration_exists("test-config")
|
||||
retrieved = self.config_manager.get_configuration("test-config")
|
||||
assert retrieved["provider"] == "ollama"
|
||||
assert retrieved["model_name"] == "gemma2:9b"
|
||||
assert retrieved["base_url"] == "http://localhost:11434"
|
||||
|
||||
def test_remove_configuration(self):
|
||||
"""Test removing a configuration"""
|
||||
config = {
|
||||
"provider": "openai",
|
||||
"model_name": "gpt-4o",
|
||||
"api_key": "sk-test123"
|
||||
}
|
||||
|
||||
self.config_manager.add_configuration("test-openai", config)
|
||||
assert self.config_manager.configuration_exists("test-openai")
|
||||
|
||||
# Remove configuration
|
||||
result = self.config_manager.remove_configuration("test-openai")
|
||||
assert result is True
|
||||
assert not self.config_manager.configuration_exists("test-openai")
|
||||
|
||||
def test_set_active_configuration(self):
|
||||
"""Test setting active configuration"""
|
||||
config = {
|
||||
"provider": "ollama",
|
||||
"model_name": "gemma2:9b",
|
||||
"base_url": "http://localhost:11434"
|
||||
}
|
||||
|
||||
self.config_manager.add_configuration("test-config", config)
|
||||
|
||||
# Set as active
|
||||
result = self.config_manager.set_active_configuration("test-config")
|
||||
assert result is True
|
||||
assert self.config_manager.get_active_configuration() == "test-config"
|
||||
|
||||
def test_validation(self):
|
||||
"""Test configuration validation"""
|
||||
# Valid configuration
|
||||
valid_config = {
|
||||
"provider": "ollama",
|
||||
"model_name": "gemma2:9b",
|
||||
"base_url": "http://localhost:11434"
|
||||
}
|
||||
|
||||
is_valid, errors = self.config_manager.validate_configuration(valid_config)
|
||||
assert is_valid is True
|
||||
assert len(errors) == 0
|
||||
|
||||
# Invalid configuration - missing provider
|
||||
invalid_config = {
|
||||
"model_name": "gemma2:9b",
|
||||
"base_url": "http://localhost:11434"
|
||||
}
|
||||
|
||||
is_valid, errors = self.config_manager.validate_configuration(invalid_config)
|
||||
assert is_valid is False
|
||||
assert "Provider is required" in errors
|
||||
|
||||
# Invalid configuration - OpenAI without API key
|
||||
invalid_openai = {
|
||||
"provider": "openai",
|
||||
"model_name": "gpt-4o"
|
||||
}
|
||||
|
||||
is_valid, errors = self.config_manager.validate_configuration(invalid_openai)
|
||||
assert is_valid is False
|
||||
assert "API key is required for OpenAI provider" in errors
|
||||
|
||||
def test_encryption(self):
|
||||
"""Test that API keys are encrypted"""
|
||||
config = {
|
||||
"provider": "openai",
|
||||
"model_name": "gpt-4o",
|
||||
"api_key": "sk-test123456789"
|
||||
}
|
||||
|
||||
self.config_manager.add_configuration("test-openai", config)
|
||||
|
||||
# Read the raw config file
|
||||
with open(self.config_manager.config_file, 'r') as f:
|
||||
raw_config = json.load(f)
|
||||
|
||||
# Check that API key is encrypted in the file
|
||||
stored_config = raw_config["configurations"]["test-openai"]
|
||||
assert "api_key" in stored_config
|
||||
assert stored_config["api_key"] != "sk-test123456789" # Should be encrypted
|
||||
assert len(stored_config["api_key"]) > 20 # Encrypted data is longer
|
||||
|
||||
# But when retrieved, it should be decrypted
|
||||
retrieved = self.config_manager.get_configuration("test-openai")
|
||||
assert retrieved["api_key"] == "sk-test123456789"
|
||||
|
||||
def test_list_configurations(self):
|
||||
"""Test listing configurations"""
|
||||
# Add multiple configurations
|
||||
configs = [
|
||||
("config1", {"provider": "ollama", "model_name": "gemma2:9b", "base_url": "http://localhost:11434"}),
|
||||
("config2", {"provider": "openai", "model_name": "gpt-4o", "api_key": "sk-test"}),
|
||||
("config3", {"provider": "openrouter", "model_name": "claude-3.5-sonnet", "api_key": "sk-or-test", "base_url": "https://openrouter.ai/api/v1"})
|
||||
]
|
||||
|
||||
for name, config in configs:
|
||||
self.config_manager.add_configuration(name, config)
|
||||
|
||||
# List configurations
|
||||
config_names = self.config_manager.list_configuration_names()
|
||||
assert len(config_names) == 3
|
||||
assert "config1" in config_names
|
||||
assert "config2" in config_names
|
||||
assert "config3" in config_names
|
||||
|
||||
def test_backup_creation(self):
|
||||
"""Test that backups are created"""
|
||||
config = {
|
||||
"provider": "ollama",
|
||||
"model_name": "gemma2:9b",
|
||||
"base_url": "http://localhost:11434"
|
||||
}
|
||||
|
||||
# Add first configuration
|
||||
self.config_manager.add_configuration("config1", config)
|
||||
|
||||
# Add second configuration (should create backup)
|
||||
config2 = {
|
||||
"provider": "openai",
|
||||
"model_name": "gpt-4o",
|
||||
"api_key": "sk-test"
|
||||
}
|
||||
self.config_manager.add_configuration("config2", config2)
|
||||
|
||||
# Check that backup file exists
|
||||
backup_file = self.config_manager.config_file.with_suffix('.json.bak')
|
||||
assert backup_file.exists()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
Test suite for MaxClaw Agent provider tools
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from tools.provider_tools import (
|
||||
OllamaProviderTools,
|
||||
OpenAIProviderTools,
|
||||
OpenRouterProviderTools,
|
||||
get_provider_tools
|
||||
)
|
||||
|
||||
|
||||
class TestOllamaProviderTools:
|
||||
"""Test cases for OllamaProviderTools"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment"""
|
||||
self.tools = OllamaProviderTools(base_url="http://localhost:11434")
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test OllamaProviderTools initialization"""
|
||||
assert self.tools.provider_name == "ollama"
|
||||
assert self.tools.base_url == "http://localhost:11434"
|
||||
assert self.tools.api_url == "http://localhost:11434/api"
|
||||
|
||||
@patch('requests.get')
|
||||
def test_list_models_success(self, mock_get):
|
||||
"""Test successful model listing"""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"models": [
|
||||
{"name": "gemma2:9b", "size": 5000000000},
|
||||
{"name": "llama2:7b", "size": 4000000000}
|
||||
]
|
||||
}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = self.tools.list_models()
|
||||
|
||||
assert "Available Ollama Models" in result
|
||||
assert "gemma2:9b" in result
|
||||
assert "llama2:7b" in result
|
||||
|
||||
@patch('requests.get')
|
||||
def test_list_models_failure(self, mock_get):
|
||||
"""Test failed model listing"""
|
||||
mock_get.side_effect = Exception("Connection failed")
|
||||
|
||||
result = self.tools.list_models()
|
||||
|
||||
assert "Error listing models" in result
|
||||
|
||||
@patch('requests.post')
|
||||
def test_get_model_info_success(self, mock_post):
|
||||
"""Test successful model info retrieval"""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"license": "Apache 2.0",
|
||||
"parameters": "9B",
|
||||
"details": {
|
||||
"parameter_size": "9B",
|
||||
"quantization_level": "Q4_0"
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
result = self.tools.get_model_info("gemma2:9b")
|
||||
|
||||
assert "Model Information: gemma2:9b" in result
|
||||
assert "License: Apache 2.0" in result
|
||||
assert "Parameter Size: 9B" in result
|
||||
|
||||
@patch('requests.get')
|
||||
def test_test_connection_success(self, mock_get):
|
||||
"""Test successful connection test"""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = self.tools.test_connection()
|
||||
|
||||
assert "✅ Ollama instance at http://localhost:11434 is running" in result
|
||||
|
||||
@patch('requests.get')
|
||||
def test_test_connection_failure(self, mock_get):
|
||||
"""Test failed connection test"""
|
||||
mock_get.side_effect = Exception("Connection failed")
|
||||
|
||||
result = self.tools.test_connection()
|
||||
|
||||
assert "❌ Cannot connect to Ollama instance" in result
|
||||
|
||||
def test_get_tools(self):
|
||||
"""Test tool generation"""
|
||||
tools = self.tools.get_tools()
|
||||
|
||||
assert len(tools) == 6
|
||||
tool_names = [tool.name for tool in tools]
|
||||
assert "ollama_list_models" in tool_names
|
||||
assert "ollama_model_info" in tool_names
|
||||
assert "ollama_test_connection" in tool_names
|
||||
assert "ollama_get_default_model" in tool_names
|
||||
assert "ollama_set_default_model" in tool_names
|
||||
assert "ollama_info" in tool_names
|
||||
|
||||
|
||||
class TestOpenAIProviderTools:
|
||||
"""Test cases for OpenAIProviderTools"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment"""
|
||||
self.tools = OpenAIProviderTools(api_key="sk-test123")
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test OpenAIProviderTools initialization"""
|
||||
assert self.tools.provider_name == "openai"
|
||||
assert self.tools.api_key == "sk-test123"
|
||||
assert self.tools.api_url == "https://api.openai.com/v1"
|
||||
|
||||
@patch('requests.get')
|
||||
def test_list_models_success(self, mock_get):
|
||||
"""Test successful model listing"""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"data": [
|
||||
{"id": "gpt-4o"},
|
||||
{"id": "gpt-4o-mini"},
|
||||
{"id": "gpt-3.5-turbo"}
|
||||
]
|
||||
}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = self.tools.list_models()
|
||||
|
||||
assert "Available OpenAI Models" in result
|
||||
assert "gpt-4o" in result
|
||||
assert "gpt-4o-mini" in result
|
||||
|
||||
@patch('requests.get')
|
||||
def test_test_connection_success(self, mock_get):
|
||||
"""Test successful connection test"""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"data": []}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = self.tools.test_connection()
|
||||
|
||||
assert "✅ OpenAI API connection successful" in result
|
||||
|
||||
def test_get_tools(self):
|
||||
"""Test tool generation"""
|
||||
tools = self.tools.get_tools()
|
||||
|
||||
assert len(tools) == 6
|
||||
tool_names = [tool.name for tool in tools]
|
||||
assert "openai_list_models" in tool_names
|
||||
assert "openai_model_info" in tool_names
|
||||
assert "openai_test_connection" in tool_names
|
||||
|
||||
|
||||
class TestOpenRouterProviderTools:
|
||||
"""Test cases for OpenRouterProviderTools"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment"""
|
||||
self.tools = OpenRouterProviderTools(
|
||||
api_key="sk-or-test123",
|
||||
base_url="https://openrouter.ai/api/v1"
|
||||
)
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test OpenRouterProviderTools initialization"""
|
||||
assert self.tools.provider_name == "openrouter"
|
||||
assert self.tools.api_key == "sk-or-test123"
|
||||
assert self.tools.base_url == "https://openrouter.ai/api/v1"
|
||||
|
||||
@patch('requests.get')
|
||||
def test_list_models_success(self, mock_get):
|
||||
"""Test successful model listing"""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"data": [
|
||||
{"id": "anthropic/claude-3.5-sonnet", "name": "Claude 3.5 Sonnet"},
|
||||
{"id": "openai/gpt-4o", "name": "GPT-4o"}
|
||||
]
|
||||
}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = self.tools.list_models()
|
||||
|
||||
assert "Available OpenRouter Models" in result
|
||||
assert "anthropic/claude-3.5-sonnet" in result
|
||||
assert "openai/gpt-4o" in result
|
||||
|
||||
def test_get_tools(self):
|
||||
"""Test tool generation"""
|
||||
tools = self.tools.get_tools()
|
||||
|
||||
assert len(tools) == 6
|
||||
tool_names = [tool.name for tool in tools]
|
||||
assert "openrouter_list_models" in tool_names
|
||||
assert "openrouter_model_info" in tool_names
|
||||
assert "openrouter_test_connection" in tool_names
|
||||
|
||||
|
||||
class TestGetProviderTools:
|
||||
"""Test cases for get_provider_tools function"""
|
||||
|
||||
@patch.dict(os.environ, {"OLLAMA_BASE_URL": "http://localhost:11434"})
|
||||
def test_get_ollama_tools(self):
|
||||
"""Test getting Ollama tools"""
|
||||
tools = get_provider_tools("ollama")
|
||||
|
||||
assert len(tools) == 6
|
||||
tool_names = [tool.name for tool in tools]
|
||||
assert "ollama_list_models" in tool_names
|
||||
|
||||
@patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test123"})
|
||||
def test_get_openai_tools(self):
|
||||
"""Test getting OpenAI tools"""
|
||||
tools = get_provider_tools("openai")
|
||||
|
||||
assert len(tools) == 6
|
||||
tool_names = [tool.name for tool in tools]
|
||||
assert "openai_list_models" in tool_names
|
||||
|
||||
@patch.dict(os.environ, {"OPENROUTER_API_KEY": "sk-or-test123"})
|
||||
def test_get_openrouter_tools(self):
|
||||
"""Test getting OpenRouter tools"""
|
||||
tools = get_provider_tools("openrouter")
|
||||
|
||||
assert len(tools) == 6
|
||||
tool_names = [tool.name for tool in tools]
|
||||
assert "openrouter_list_models" in tool_names
|
||||
|
||||
def test_get_unknown_provider_tools(self):
|
||||
"""Test getting tools for unknown provider"""
|
||||
tools = get_provider_tools("unknown")
|
||||
|
||||
assert len(tools) == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
MaxClaw Agent Tools Package
|
||||
Provides custom tools for Proxmox, Ollama, and unified provider management
|
||||
"""
|
||||
|
||||
from .proxmox_tools import get_proxmox_tools, ProxmoxTools
|
||||
from .ollama_tools import get_ollama_tools, OllamaTools
|
||||
from .provider_tools import (
|
||||
get_provider_tools,
|
||||
get_all_provider_tools,
|
||||
BaseProviderTools,
|
||||
OllamaProviderTools,
|
||||
OpenAIProviderTools,
|
||||
OpenRouterProviderTools
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Proxmox tools
|
||||
'get_proxmox_tools',
|
||||
'ProxmoxTools',
|
||||
|
||||
# Ollama tools (legacy)
|
||||
'get_ollama_tools',
|
||||
'OllamaTools',
|
||||
|
||||
# Unified provider tools
|
||||
'get_provider_tools',
|
||||
'get_all_provider_tools',
|
||||
'BaseProviderTools',
|
||||
'OllamaProviderTools',
|
||||
'OpenAIProviderTools',
|
||||
'OpenRouterProviderTools'
|
||||
]
|
||||
@@ -0,0 +1,360 @@
|
||||
"""
|
||||
Ollama management tools for MaxClaw Agent
|
||||
Provides tools for managing Ollama models and instances
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
from typing import Dict, List, Optional, Any
|
||||
from langchain.tools import Tool
|
||||
|
||||
|
||||
class OllamaTools:
|
||||
"""Tools for managing Ollama instances and models"""
|
||||
|
||||
def __init__(self, base_url: str = "http://localhost:11434"):
|
||||
"""
|
||||
Initialize Ollama tools
|
||||
|
||||
Args:
|
||||
base_url: Base URL of the Ollama instance
|
||||
"""
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_url = f"{self.base_url}/api"
|
||||
|
||||
def _make_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Make a request to the Ollama API
|
||||
|
||||
Args:
|
||||
endpoint: API endpoint
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
data: Optional data for POST requests
|
||||
|
||||
Returns:
|
||||
Dictionary with response data
|
||||
"""
|
||||
try:
|
||||
url = f"{self.api_url}/{endpoint}"
|
||||
|
||||
if method.upper() == "GET":
|
||||
response = requests.get(url, timeout=10)
|
||||
elif method.upper() == "POST":
|
||||
response = requests.post(url, json=data, timeout=30)
|
||||
else:
|
||||
return {"success": False, "error": f"Unsupported method: {method}"}
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return {"success": True, "data": response.json()}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "data": response.text}
|
||||
else:
|
||||
return {"success": False, "error": f"HTTP {response.status_code}: {response.text}"}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return {"success": False, "error": "Request timed out"}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {"success": False, "error": f"Connection failed to {self.base_url}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def list_models(self) -> str:
|
||||
"""
|
||||
List all available models on the Ollama instance
|
||||
|
||||
Returns:
|
||||
Formatted string with model information
|
||||
"""
|
||||
result = self._make_request("tags")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error listing models: {result['error']}"
|
||||
|
||||
models = result["data"].get("models", [])
|
||||
if not models:
|
||||
return "No models found on the Ollama instance."
|
||||
|
||||
output = "Available Ollama Models:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for model in models:
|
||||
name = model.get("name", "Unknown")
|
||||
size = model.get("size", 0)
|
||||
modified = model.get("modified_at", "")
|
||||
|
||||
# Convert size to human readable format
|
||||
if size > 1024 * 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024 * 1024):.1f} GB"
|
||||
elif size > 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024):.1f} MB"
|
||||
else:
|
||||
size_str = f"{size / 1024:.1f} KB"
|
||||
|
||||
# Format modification date
|
||||
if modified:
|
||||
from datetime import datetime
|
||||
try:
|
||||
dt = datetime.fromisoformat(modified.replace('Z', '+00:00'))
|
||||
modified_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except:
|
||||
modified_str = modified
|
||||
else:
|
||||
modified_str = "N/A"
|
||||
|
||||
output += f"Name: {name}\n"
|
||||
output += f"Size: {size_str}\n"
|
||||
output += f"Modified: {modified_str}\n"
|
||||
output += "-" * 30 + "\n"
|
||||
|
||||
return output
|
||||
|
||||
def get_model_info(self, model_name: str) -> str:
|
||||
"""
|
||||
Get detailed information about a specific model
|
||||
|
||||
Args:
|
||||
model_name: Name of the model
|
||||
|
||||
Returns:
|
||||
Formatted string with model details
|
||||
"""
|
||||
result = self._make_request("show", method="POST", data={"name": model_name})
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error getting model info: {result['error']}"
|
||||
|
||||
model_info = result["data"]
|
||||
|
||||
output = f"Model Information: {model_name}\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
# Basic information
|
||||
if "license" in model_info:
|
||||
output += f"License: {model_info['license']}\n"
|
||||
|
||||
if "modelfile" in model_info:
|
||||
output += f"Modelfile: {model_info['modelfile'][:100]}...\n"
|
||||
|
||||
if "parameters" in model_info:
|
||||
output += f"Parameters: {model_info['parameters']}\n"
|
||||
|
||||
if "template" in model_info:
|
||||
output += f"Template: {model_info['template'][:100]}...\n"
|
||||
|
||||
# System information
|
||||
if "system" in model_info:
|
||||
output += f"System: {model_info['system'][:100]}...\n"
|
||||
|
||||
# Details
|
||||
if "details" in model_info:
|
||||
details = model_info["details"]
|
||||
output += "\nModel Details:\n"
|
||||
output += "-" * 30 + "\n"
|
||||
|
||||
if "parent_model" in details:
|
||||
output += f"Parent Model: {details['parent_model']}\n"
|
||||
|
||||
if "format" in details:
|
||||
output += f"Format: {details['format']}\n"
|
||||
|
||||
if "family" in details:
|
||||
output += f"Family: {details['family']}\n"
|
||||
|
||||
if "families" in details:
|
||||
output += f"Families: {', '.join(details['families'])}\n"
|
||||
|
||||
if "parameter_size" in details:
|
||||
output += f"Parameter Size: {details['parameter_size']}\n"
|
||||
|
||||
if "quantization_level" in details:
|
||||
output += f"Quantization Level: {details['quantization_level']}\n"
|
||||
|
||||
return output
|
||||
|
||||
def pull_model(self, model_name: str) -> str:
|
||||
"""
|
||||
Pull a model from the Ollama library
|
||||
|
||||
Args:
|
||||
model_name: Name of the model to pull
|
||||
|
||||
Returns:
|
||||
Result message
|
||||
"""
|
||||
result = self._make_request("pull", method="POST", data={"name": model_name})
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ Model '{model_name}' pulled successfully."
|
||||
else:
|
||||
return f"❌ Failed to pull model '{model_name}': {result['error']}"
|
||||
|
||||
def delete_model(self, model_name: str) -> str:
|
||||
"""
|
||||
Delete a model from the Ollama instance
|
||||
|
||||
Args:
|
||||
model_name: Name of the model to delete
|
||||
|
||||
Returns:
|
||||
Result message
|
||||
"""
|
||||
result = self._make_request("delete", method="POST", data={"name": model_name})
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ Model '{model_name}' deleted successfully."
|
||||
else:
|
||||
return f"❌ Failed to delete model '{model_name}': {result['error']}"
|
||||
|
||||
def generate_completion(self, model: str, prompt: str, system: Optional[str] = None) -> str:
|
||||
"""
|
||||
Generate a completion using a model
|
||||
|
||||
Args:
|
||||
model: Model name
|
||||
prompt: Prompt text
|
||||
system: Optional system prompt
|
||||
|
||||
Returns:
|
||||
Generated text
|
||||
"""
|
||||
data = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
if system:
|
||||
data["system"] = system
|
||||
|
||||
result = self._make_request("generate", method="POST", data=data)
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error generating completion: {result['error']}"
|
||||
|
||||
response = result["data"]
|
||||
return response.get("response", "No response generated")
|
||||
|
||||
def check_ollama_status(self) -> str:
|
||||
"""
|
||||
Check if the Ollama instance is running and responsive
|
||||
|
||||
Returns:
|
||||
Status message
|
||||
"""
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/", timeout=5)
|
||||
if response.status_code == 200:
|
||||
return f"✅ Ollama instance at {self.base_url} is running and responsive."
|
||||
else:
|
||||
return f"⚠️ Ollama instance at {self.base_url} returned status {response.status_code}."
|
||||
except requests.exceptions.ConnectionError:
|
||||
return f"❌ Cannot connect to Ollama instance at {self.base_url}."
|
||||
except requests.exceptions.Timeout:
|
||||
return f"❌ Connection to Ollama instance at {self.base_url} timed out."
|
||||
except Exception as e:
|
||||
return f"❌ Error checking Ollama status: {str(e)}"
|
||||
|
||||
def get_running_models(self) -> str:
|
||||
"""
|
||||
Get information about currently running models
|
||||
|
||||
Returns:
|
||||
Formatted string with running model information
|
||||
"""
|
||||
result = self._make_request("ps")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error getting running models: {result['error']}"
|
||||
|
||||
running = result["data"].get("models", [])
|
||||
if not running:
|
||||
return "No models currently running."
|
||||
|
||||
output = "Currently Running Models:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for model in running:
|
||||
name = model.get("name", "Unknown")
|
||||
size = model.get("size", 0)
|
||||
digest = model.get("digest", "")
|
||||
expires_at = model.get("expires_at", "")
|
||||
|
||||
# Convert size to human readable format
|
||||
if size > 1024 * 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024 * 1024):.1f} GB"
|
||||
elif size > 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024):.1f} MB"
|
||||
else:
|
||||
size_str = f"{size / 1024:.1f} KB"
|
||||
|
||||
# Format expiration time
|
||||
if expires_at:
|
||||
from datetime import datetime
|
||||
try:
|
||||
dt = datetime.fromisoformat(expires_at.replace('Z', '+00:00'))
|
||||
expires_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except:
|
||||
expires_str = expires_at
|
||||
else:
|
||||
expires_str = "N/A"
|
||||
|
||||
output += f"Name: {name}\n"
|
||||
output += f"Size: {size_str}\n"
|
||||
output += f"Digest: {digest[:20]}...\n"
|
||||
output += f"Expires: {expires_str}\n"
|
||||
output += "-" * 30 + "\n"
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def get_ollama_tools(base_url: str = "http://localhost:11434") -> List[Tool]:
|
||||
"""
|
||||
Get a list of Ollama tools for the MaxClaw agent
|
||||
|
||||
Args:
|
||||
base_url: Base URL of the Ollama instance
|
||||
|
||||
Returns:
|
||||
List of LangChain tools for Ollama management
|
||||
"""
|
||||
ollama = OllamaTools(base_url)
|
||||
|
||||
tools = [
|
||||
Tool(
|
||||
name="ollama_list_models",
|
||||
func=lambda: ollama.list_models(),
|
||||
description="List all available models on the Ollama instance"
|
||||
),
|
||||
Tool(
|
||||
name="ollama_model_info",
|
||||
func=lambda model_name: ollama.get_model_info(model_name),
|
||||
description="Get detailed information about a specific model"
|
||||
),
|
||||
Tool(
|
||||
name="ollama_pull_model",
|
||||
func=lambda model_name: ollama.pull_model(model_name),
|
||||
description="Pull a model from the Ollama library"
|
||||
),
|
||||
Tool(
|
||||
name="ollama_delete_model",
|
||||
func=lambda model_name: ollama.delete_model(model_name),
|
||||
description="Delete a model from the Ollama instance"
|
||||
),
|
||||
Tool(
|
||||
name="ollama_generate",
|
||||
func=lambda model, prompt, system="": ollama.generate_completion(model, prompt, system if system else None),
|
||||
description="Generate a completion using a model with optional system prompt"
|
||||
),
|
||||
Tool(
|
||||
name="ollama_status",
|
||||
func=lambda: ollama.check_ollama_status(),
|
||||
description="Check if the Ollama instance is running and responsive"
|
||||
),
|
||||
Tool(
|
||||
name="ollama_running_models",
|
||||
func=lambda: ollama.get_running_models(),
|
||||
description="Get information about currently running models"
|
||||
)
|
||||
]
|
||||
|
||||
return tools
|
||||
@@ -0,0 +1,991 @@
|
||||
"""
|
||||
Unified Provider Tools for MaxClaw Agent
|
||||
Provides a consistent interface for managing different LLM providers
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from langchain.tools import Tool
|
||||
|
||||
|
||||
class BaseProviderTools(ABC):
|
||||
"""Base class for provider-specific tools"""
|
||||
|
||||
def __init__(self, provider_name: str, **kwargs):
|
||||
"""
|
||||
Initialize provider tools
|
||||
|
||||
Args:
|
||||
provider_name: Name of the provider (e.g., "ollama", "openai", "openrouter")
|
||||
**kwargs: Provider-specific configuration
|
||||
"""
|
||||
self.provider_name = provider_name
|
||||
self.config = kwargs
|
||||
|
||||
@abstractmethod
|
||||
def list_models(self) -> str:
|
||||
"""List available models for this provider"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_model_info(self, model_name: str) -> str:
|
||||
"""Get detailed information about a specific model"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def test_connection(self) -> str:
|
||||
"""Test connection to the provider"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_default_model(self) -> str:
|
||||
"""Get the default model for this provider"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_default_model(self, model_name: str) -> str:
|
||||
"""Set the default model for this provider"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_provider_info(self) -> str:
|
||||
"""Get information about the provider configuration"""
|
||||
pass
|
||||
|
||||
def get_tools(self) -> List[Tool]:
|
||||
"""
|
||||
Get a list of LangChain tools for this provider
|
||||
|
||||
Returns:
|
||||
List of tools
|
||||
"""
|
||||
return [
|
||||
Tool(
|
||||
name=f"{self.provider_name}_list_models",
|
||||
func=self.list_models,
|
||||
description=f"List all available models for {self.provider_name}"
|
||||
),
|
||||
Tool(
|
||||
name=f"{self.provider_name}_model_info",
|
||||
func=self.get_model_info,
|
||||
description=f"Get detailed information about a specific {self.provider_name} model"
|
||||
),
|
||||
Tool(
|
||||
name=f"{self.provider_name}_test_connection",
|
||||
func=self.test_connection,
|
||||
description=f"Test connection to {self.provider_name} provider"
|
||||
),
|
||||
Tool(
|
||||
name=f"{self.provider_name}_get_default_model",
|
||||
func=self.get_default_model,
|
||||
description=f"Get the default model for {self.provider_name}"
|
||||
),
|
||||
Tool(
|
||||
name=f"{self.provider_name}_set_default_model",
|
||||
func=self.set_default_model,
|
||||
description=f"Set the default model for {self.provider_name}"
|
||||
),
|
||||
Tool(
|
||||
name=f"{self.provider_name}_info",
|
||||
func=self.get_provider_info,
|
||||
description=f"Get information about {self.provider_name} configuration"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class OllamaProviderTools(BaseProviderTools):
|
||||
"""Ollama provider tools implementation"""
|
||||
|
||||
def __init__(self, base_url: str = "http://localhost:11434", **kwargs):
|
||||
"""
|
||||
Initialize Ollama tools
|
||||
|
||||
Args:
|
||||
base_url: Base URL of the Ollama instance
|
||||
**kwargs: Additional configuration
|
||||
"""
|
||||
super().__init__("ollama", base_url=base_url, **kwargs)
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_url = f"{self.base_url}/api"
|
||||
|
||||
def _make_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Make a request to the Ollama API"""
|
||||
try:
|
||||
url = f"{self.api_url}/{endpoint}"
|
||||
|
||||
if method.upper() == "GET":
|
||||
response = requests.get(url, timeout=10)
|
||||
elif method.upper() == "POST":
|
||||
response = requests.post(url, json=data, timeout=30)
|
||||
else:
|
||||
return {"success": False, "error": f"Unsupported method: {method}"}
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return {"success": True, "data": response.json()}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "data": response.text}
|
||||
else:
|
||||
return {"success": False, "error": f"HTTP {response.status_code}: {response.text}"}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return {"success": False, "error": "Request timed out"}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {"success": False, "error": f"Connection failed to {self.base_url}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def list_models(self) -> str:
|
||||
"""List all available models on the Ollama instance"""
|
||||
result = self._make_request("tags")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error listing models: {result['error']}"
|
||||
|
||||
models = result["data"].get("models", [])
|
||||
if not models:
|
||||
return "No models found on the Ollama instance."
|
||||
|
||||
output = f"Available Ollama Models ({self.base_url}):\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for model in models:
|
||||
name = model.get("name", "Unknown")
|
||||
size = model.get("size", 0)
|
||||
|
||||
# Convert size to human readable format
|
||||
if size > 1024 * 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024 * 1024):.1f} GB"
|
||||
elif size > 1024 * 1024:
|
||||
size_str = f"{size / (1024 * 1024):.1f} MB"
|
||||
else:
|
||||
size_str = f"{size / 1024:.1f} KB"
|
||||
|
||||
output += f"• {name} ({size_str})\n"
|
||||
|
||||
return output
|
||||
|
||||
def get_model_info(self, model_name: str) -> str:
|
||||
"""Get detailed information about a specific model"""
|
||||
result = self._make_request("show", method="POST", data={"name": model_name})
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error getting model info: {result['error']}"
|
||||
|
||||
model_info = result["data"]
|
||||
|
||||
output = f"Model Information: {model_name}\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
if "license" in model_info:
|
||||
output += f"License: {model_info['license']}\n"
|
||||
|
||||
if "parameters" in model_info:
|
||||
output += f"Parameters: {model_info['parameters']}\n"
|
||||
|
||||
if "details" in model_info:
|
||||
details = model_info["details"]
|
||||
if "parameter_size" in details:
|
||||
output += f"Parameter Size: {details['parameter_size']}\n"
|
||||
if "quantization_level" in details:
|
||||
output += f"Quantization Level: {details['quantization_level']}\n"
|
||||
|
||||
return output
|
||||
|
||||
def test_connection(self) -> str:
|
||||
"""Test connection to Ollama"""
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/", timeout=5)
|
||||
if response.status_code == 200:
|
||||
return f"✅ Ollama instance at {self.base_url} is running and responsive."
|
||||
else:
|
||||
return f"⚠️ Ollama instance at {self.base_url} returned status {response.status_code}."
|
||||
except requests.exceptions.ConnectionError:
|
||||
return f"❌ Cannot connect to Ollama instance at {self.base_url}."
|
||||
except requests.exceptions.Timeout:
|
||||
return f"❌ Connection to Ollama instance at {self.base_url} timed out."
|
||||
except Exception as e:
|
||||
return f"❌ Error checking Ollama status: {str(e)}"
|
||||
|
||||
def get_default_model(self) -> str:
|
||||
"""Get the default model for Ollama"""
|
||||
# Ollama doesn't have a built-in default model concept
|
||||
# We'll return the first available model or a message
|
||||
result = self._make_request("tags")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error getting models: {result['error']}"
|
||||
|
||||
models = result["data"].get("models", [])
|
||||
if not models:
|
||||
return "No models available on Ollama instance."
|
||||
|
||||
# Return the first model as default
|
||||
first_model = models[0].get("name", "Unknown")
|
||||
return f"Default model (first available): {first_model}"
|
||||
|
||||
def set_default_model(self, model_name: str) -> str:
|
||||
"""Set the default model for Ollama"""
|
||||
# Ollama doesn't have a built-in default model concept
|
||||
# We'll just verify the model exists
|
||||
result = self._make_request("show", method="POST", data={"name": model_name})
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ Model '{model_name}' exists and can be used as default."
|
||||
else:
|
||||
return f"❌ Model '{model_name}' not found on Ollama instance."
|
||||
|
||||
def get_provider_info(self) -> str:
|
||||
"""Get information about Ollama configuration"""
|
||||
output = "Ollama Provider Configuration:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += f"Base URL: {self.base_url}\n"
|
||||
output += f"API URL: {self.api_url}\n"
|
||||
|
||||
# Test connection
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/", timeout=5)
|
||||
if response.status_code == 200:
|
||||
output += "Status: ✅ Connected\n"
|
||||
else:
|
||||
output += f"Status: ⚠️ HTTP {response.status_code}\n"
|
||||
except:
|
||||
output += "Status: ❌ Not connected\n"
|
||||
|
||||
# Get model count
|
||||
result = self._make_request("tags")
|
||||
if result["success"]:
|
||||
models = result["data"].get("models", [])
|
||||
output += f"Available Models: {len(models)}\n"
|
||||
else:
|
||||
output += "Available Models: Unknown\n"
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class OpenAIProviderTools(BaseProviderTools):
|
||||
"""OpenAI provider tools implementation"""
|
||||
|
||||
def __init__(self, api_key: str, **kwargs):
|
||||
"""
|
||||
Initialize OpenAI tools
|
||||
|
||||
Args:
|
||||
api_key: OpenAI API key
|
||||
**kwargs: Additional configuration
|
||||
"""
|
||||
super().__init__("openai", api_key=api_key, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.api_url = "https://api.openai.com/v1"
|
||||
|
||||
def _make_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Make a request to the OpenAI API"""
|
||||
try:
|
||||
url = f"{self.api_url}/{endpoint}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
if method.upper() == "GET":
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
elif method.upper() == "POST":
|
||||
response = requests.post(url, headers=headers, json=data, timeout=30)
|
||||
else:
|
||||
return {"success": False, "error": f"Unsupported method: {method}"}
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return {"success": True, "data": response.json()}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "data": response.text}
|
||||
else:
|
||||
return {"success": False, "error": f"HTTP {response.status_code}: {response.text}"}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return {"success": False, "error": "Request timed out"}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {"success": False, "error": "Connection failed to OpenAI API"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def list_models(self) -> str:
|
||||
"""List all available models on OpenAI"""
|
||||
result = self._make_request("models")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error listing models: {result['error']}"
|
||||
|
||||
models = result["data"].get("data", [])
|
||||
if not models:
|
||||
return "No models found on OpenAI."
|
||||
|
||||
# Filter to show only common models
|
||||
common_models = ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo", "text-embedding-ada-002"]
|
||||
|
||||
output = "Available OpenAI Models:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for model in models:
|
||||
model_id = model.get("id", "")
|
||||
if model_id in common_models or "gpt" in model_id or "text-embedding" in model_id:
|
||||
output += f"• {model_id}\n"
|
||||
|
||||
return output
|
||||
|
||||
def get_model_info(self, model_name: str) -> str:
|
||||
"""Get detailed information about a specific model"""
|
||||
result = self._make_request(f"models/{model_name}")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error getting model info: {result['error']}"
|
||||
|
||||
model_info = result["data"]
|
||||
|
||||
output = f"Model Information: {model_name}\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += f"ID: {model_info.get('id', 'Unknown')}\n"
|
||||
output += f"Object: {model_info.get('object', 'Unknown')}\n"
|
||||
output += f"Created: {model_info.get('created', 'Unknown')}\n"
|
||||
output += f"Owned by: {model_info.get('owned_by', 'Unknown')}\n"
|
||||
|
||||
return output
|
||||
|
||||
def test_connection(self) -> str:
|
||||
"""Test connection to OpenAI"""
|
||||
try:
|
||||
# Try to list models as a connection test
|
||||
result = self._make_request("models")
|
||||
if result["success"]:
|
||||
return "✅ OpenAI API connection successful."
|
||||
else:
|
||||
return f"❌ OpenAI API connection failed: {result['error']}"
|
||||
except Exception as e:
|
||||
return f"❌ Error connecting to OpenAI: {str(e)}"
|
||||
|
||||
def get_default_model(self) -> str:
|
||||
"""Get the default model for OpenAI"""
|
||||
# OpenAI doesn't have a built-in default model concept
|
||||
# Return a common default
|
||||
return "Default model: gpt-4o (recommended for most tasks)"
|
||||
|
||||
def set_default_model(self, model_name: str) -> str:
|
||||
"""Set the default model for OpenAI"""
|
||||
# OpenAI doesn't have a built-in default model concept
|
||||
# Just verify the model exists
|
||||
result = self._make_request(f"models/{model_name}")
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ Model '{model_name}' exists and can be used as default."
|
||||
else:
|
||||
return f"❌ Model '{model_name}' not found on OpenAI."
|
||||
|
||||
def get_provider_info(self) -> str:
|
||||
"""Get information about OpenAI configuration"""
|
||||
output = "OpenAI Provider Configuration:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += f"API URL: {self.api_url}\n"
|
||||
output += f"API Key: {'Set' if self.api_key else 'Not set'}\n"
|
||||
|
||||
# Test connection
|
||||
try:
|
||||
result = self._make_request("models")
|
||||
if result["success"]:
|
||||
output += "Status: ✅ Connected\n"
|
||||
else:
|
||||
output += f"Status: ❌ Connection failed\n"
|
||||
except:
|
||||
output += "Status: ❌ Not connected\n"
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class OpenRouterProviderTools(BaseProviderTools):
|
||||
"""OpenRouter provider tools implementation"""
|
||||
|
||||
def __init__(self, api_key: str, base_url: str = "https://openrouter.ai/api/v1", **kwargs):
|
||||
"""
|
||||
Initialize OpenRouter tools
|
||||
|
||||
Args:
|
||||
api_key: OpenRouter API key
|
||||
base_url: Base URL for OpenRouter API
|
||||
**kwargs: Additional configuration
|
||||
"""
|
||||
super().__init__("openrouter", api_key=api_key, base_url=base_url, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_url = f"{self.base_url}"
|
||||
|
||||
def _make_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Make a request to the OpenRouter API"""
|
||||
try:
|
||||
url = f"{self.api_url}/{endpoint}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
if method.upper() == "GET":
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
elif method.upper() == "POST":
|
||||
response = requests.post(url, headers=headers, json=data, timeout=30)
|
||||
else:
|
||||
return {"success": False, "error": f"Unsupported method: {method}"}
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return {"success": True, "data": response.json()}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "data": response.text}
|
||||
else:
|
||||
return {"success": False, "error": f"HTTP {response.status_code}: {response.text}"}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return {"success": False, "error": "Request timed out"}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {"success": False, "error": f"Connection failed to {self.base_url}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def list_models(self) -> str:
|
||||
"""List all available models on OpenRouter"""
|
||||
result = self._make_request("models")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error listing models: {result['error']}"
|
||||
|
||||
models = result["data"].get("data", [])
|
||||
if not models:
|
||||
return "No models found on OpenRouter."
|
||||
|
||||
output = "Available OpenRouter Models:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for model in models[:20]: # Limit to first 20 models
|
||||
model_id = model.get("id", "")
|
||||
name = model.get("name", "")
|
||||
description = model.get("description", "")
|
||||
|
||||
output += f"• {model_id}\n"
|
||||
if name:
|
||||
output += f" Name: {name}\n"
|
||||
if description:
|
||||
output += f" Description: {description[:100]}...\n"
|
||||
output += "\n"
|
||||
|
||||
return output
|
||||
|
||||
def get_model_info(self, model_name: str) -> str:
|
||||
"""Get detailed information about a specific model"""
|
||||
result = self._make_request(f"models/{model_name}")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error getting model info: {result['error']}"
|
||||
|
||||
model_info = result["data"]
|
||||
|
||||
output = f"Model Information: {model_name}\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += f"ID: {model_info.get('id', 'Unknown')}\n"
|
||||
output += f"Name: {model_info.get('name', 'Unknown')}\n"
|
||||
output += f"Description: {model_info.get('description', 'Unknown')}\n"
|
||||
|
||||
if "pricing" in model_info:
|
||||
pricing = model_info["pricing"]
|
||||
output += f"Pricing: {pricing}\n"
|
||||
|
||||
return output
|
||||
|
||||
def test_connection(self) -> str:
|
||||
"""Test connection to OpenRouter"""
|
||||
try:
|
||||
# Try to list models as a connection test
|
||||
result = self._make_request("models")
|
||||
if result["success"]:
|
||||
return "✅ OpenRouter API connection successful."
|
||||
else:
|
||||
return f"❌ OpenRouter API connection failed: {result['error']}"
|
||||
except Exception as e:
|
||||
return f"❌ Error connecting to OpenRouter: {str(e)}"
|
||||
|
||||
def get_default_model(self) -> str:
|
||||
"""Get the default model for OpenRouter"""
|
||||
# OpenRouter doesn't have a built-in default model concept
|
||||
# Return a common default
|
||||
return "Default model: anthropic/claude-3.5-sonnet (recommended for most tasks)"
|
||||
|
||||
def set_default_model(self, model_name: str) -> str:
|
||||
"""Set the default model for OpenRouter"""
|
||||
# OpenRouter doesn't have a built-in default model concept
|
||||
# Just verify the model exists
|
||||
result = self._make_request(f"models/{model_name}")
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ Model '{model_name}' exists and can be used as default."
|
||||
else:
|
||||
return f"❌ Model '{model_name}' not found on OpenRouter."
|
||||
|
||||
def get_provider_info(self) -> str:
|
||||
"""Get information about OpenRouter configuration"""
|
||||
output = "OpenRouter Provider Configuration:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += f"API URL: {self.api_url}\n"
|
||||
output += f"API Key: {'Set' if self.api_key else 'Not set'}\n"
|
||||
|
||||
# Test connection
|
||||
try:
|
||||
result = self._make_request("models")
|
||||
if result["success"]:
|
||||
output += "Status: ✅ Connected\n"
|
||||
else:
|
||||
output += f"Status: ❌ Connection failed\n"
|
||||
except:
|
||||
output += "Status: ❌ Not connected\n"
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class AnthropicProviderTools(BaseProviderTools):
|
||||
"""Anthropic provider tools implementation"""
|
||||
|
||||
def __init__(self, api_key: str, **kwargs):
|
||||
"""
|
||||
Initialize Anthropic tools
|
||||
|
||||
Args:
|
||||
api_key: Anthropic API key
|
||||
**kwargs: Additional configuration
|
||||
"""
|
||||
super().__init__("anthropic", api_key=api_key, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.api_url = "https://api.anthropic.com/v1"
|
||||
|
||||
def _make_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Make a request to the Anthropic API"""
|
||||
try:
|
||||
url = f"{self.api_url}/{endpoint}"
|
||||
headers = {
|
||||
"x-api-key": self.api_key,
|
||||
"Content-Type": "application/json",
|
||||
"anthropic-version": "2023-06-01"
|
||||
}
|
||||
|
||||
if method.upper() == "GET":
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
elif method.upper() == "POST":
|
||||
response = requests.post(url, headers=headers, json=data, timeout=30)
|
||||
else:
|
||||
return {"success": False, "error": f"Unsupported method: {method}"}
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return {"success": True, "data": response.json()}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "data": response.text}
|
||||
else:
|
||||
return {"success": False, "error": f"HTTP {response.status_code}: {response.text}"}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return {"success": False, "error": "Request timed out"}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {"success": False, "error": "Connection failed to Anthropic API"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def list_models(self) -> str:
|
||||
"""List available Anthropic models"""
|
||||
# Anthropic doesn't have a public models endpoint
|
||||
# Return known models
|
||||
output = "Available Anthropic Models:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += "• claude-3-5-sonnet-20241022\n"
|
||||
output += "• claude-3-opus-20240229\n"
|
||||
output += "• claude-3-sonnet-20240229\n"
|
||||
output += "• claude-3-haiku-20240307\n"
|
||||
output += "• claude-2.1\n"
|
||||
output += "• claude-2.0\n"
|
||||
output += "• claude-instant-1.2\n"
|
||||
return output
|
||||
|
||||
def get_model_info(self, model_name: str) -> str:
|
||||
"""Get detailed information about a specific model"""
|
||||
# Anthropic doesn't have a public model info endpoint
|
||||
# Return basic information
|
||||
output = f"Model Information: {model_name}\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
if "claude-3-5-sonnet" in model_name:
|
||||
output += "Description: Most intelligent model\n"
|
||||
output += "Context Window: 200K tokens\n"
|
||||
output += "Best for: Complex tasks, analysis, coding\n"
|
||||
elif "claude-3-opus" in model_name:
|
||||
output += "Description: Most powerful model\n"
|
||||
output += "Context Window: 200K tokens\n"
|
||||
output += "Best for: Maximum intelligence\n"
|
||||
elif "claude-3-sonnet" in model_name:
|
||||
output += "Description: Balanced performance\n"
|
||||
output += "Context Window: 200K tokens\n"
|
||||
output += "Best for: Balance of intelligence and speed\n"
|
||||
elif "claude-3-haiku" in model_name:
|
||||
output += "Description: Fastest model\n"
|
||||
output += "Context Window: 200K tokens\n"
|
||||
output += "Best for: Quick responses, simple tasks\n"
|
||||
else:
|
||||
output += "Description: Claude model\n"
|
||||
output += "Context Window: Varies\n"
|
||||
|
||||
return output
|
||||
|
||||
def test_connection(self) -> str:
|
||||
"""Test connection to Anthropic"""
|
||||
try:
|
||||
# Try to make a simple request
|
||||
headers = {
|
||||
"x-api-key": self.api_key,
|
||||
"Content-Type": "application/json",
|
||||
"anthropic-version": "2023-06-01"
|
||||
}
|
||||
|
||||
# Test with a minimal completion request
|
||||
data = {
|
||||
"model": "claude-3-haiku-20240307",
|
||||
"max_tokens": 10,
|
||||
"messages": [{"role": "user", "content": "Hi"}]
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{self.api_url}/messages",
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return "✅ Anthropic API connection successful."
|
||||
else:
|
||||
return f"❌ Anthropic API connection failed: HTTP {response.status_code}"
|
||||
except Exception as e:
|
||||
return f"❌ Error connecting to Anthropic: {str(e)}"
|
||||
|
||||
def get_default_model(self) -> str:
|
||||
"""Get the default model for Anthropic"""
|
||||
return "Default model: claude-3-5-sonnet-20241022 (recommended for most tasks)"
|
||||
|
||||
def set_default_model(self, model_name: str) -> str:
|
||||
"""Set the default model for Anthropic"""
|
||||
# Anthropic doesn't have a built-in default model concept
|
||||
# Just verify the model name is valid
|
||||
known_models = [
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-2.1",
|
||||
"claude-2.0",
|
||||
"claude-instant-1.2"
|
||||
]
|
||||
|
||||
if model_name in known_models:
|
||||
return f"✅ Model '{model_name}' is a valid Anthropic model."
|
||||
else:
|
||||
return f"⚠️ Model '{model_name}' may not be a valid Anthropic model."
|
||||
|
||||
def get_provider_info(self) -> str:
|
||||
"""Get information about Anthropic configuration"""
|
||||
output = "Anthropic Provider Configuration:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += f"API URL: {self.api_url}\n"
|
||||
output += f"API Key: {'Set' if self.api_key else 'Not set'}\n"
|
||||
|
||||
# Test connection
|
||||
try:
|
||||
headers = {
|
||||
"x-api-key": self.api_key,
|
||||
"Content-Type": "application/json",
|
||||
"anthropic-version": "2023-06-01"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{self.api_url}/messages",
|
||||
headers=headers,
|
||||
json={"model": "claude-3-haiku-20240307", "max_tokens": 1, "messages": [{"role": "user", "content": "test"}]},
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
output += "Status: ✅ Connected\n"
|
||||
else:
|
||||
output += f"Status: ❌ Connection failed (HTTP {response.status_code})\n"
|
||||
except:
|
||||
output += "Status: ❌ Not connected\n"
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class GoogleProviderTools(BaseProviderTools):
|
||||
"""Google provider tools implementation"""
|
||||
|
||||
def __init__(self, api_key: str, **kwargs):
|
||||
"""
|
||||
Initialize Google tools
|
||||
|
||||
Args:
|
||||
api_key: Google API key
|
||||
**kwargs: Additional configuration
|
||||
"""
|
||||
super().__init__("google", api_key=api_key, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.api_url = "https://generativelanguage.googleapis.com/v1beta"
|
||||
|
||||
def _make_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Make a request to the Google API"""
|
||||
try:
|
||||
url = f"{self.api_url}/{endpoint}?key={self.api_key}"
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
if method.upper() == "GET":
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
elif method.upper() == "POST":
|
||||
response = requests.post(url, headers=headers, json=data, timeout=30)
|
||||
else:
|
||||
return {"success": False, "error": f"Unsupported method: {method}"}
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return {"success": True, "data": response.json()}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "data": response.text}
|
||||
else:
|
||||
return {"success": False, "error": f"HTTP {response.status_code}: {response.text}"}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return {"success": False, "error": "Request timed out"}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {"success": False, "error": "Connection failed to Google API"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def list_models(self) -> str:
|
||||
"""List available Google models"""
|
||||
result = self._make_request("models")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error listing models: {result['error']}"
|
||||
|
||||
models = result["data"].get("models", [])
|
||||
if not models:
|
||||
return "No models found on Google."
|
||||
|
||||
output = "Available Google Models:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for model in models:
|
||||
name = model.get("name", "")
|
||||
display_name = model.get("displayName", "")
|
||||
description = model.get("description", "")
|
||||
|
||||
if name:
|
||||
output += f"• {name}\n"
|
||||
if display_name:
|
||||
output += f" Display Name: {display_name}\n"
|
||||
if description:
|
||||
output += f" Description: {description[:100]}...\n"
|
||||
output += "\n"
|
||||
|
||||
return output
|
||||
|
||||
def get_model_info(self, model_name: str) -> str:
|
||||
"""Get detailed information about a specific model"""
|
||||
# Extract model name from full path if needed
|
||||
if "/" in model_name:
|
||||
model_name = model_name.split("/")[-1]
|
||||
|
||||
result = self._make_request(f"models/{model_name}")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error getting model info: {result['error']}"
|
||||
|
||||
model_info = result["data"]
|
||||
|
||||
output = f"Model Information: {model_name}\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += f"Name: {model_info.get('name', 'Unknown')}\n"
|
||||
output += f"Display Name: {model_info.get('displayName', 'Unknown')}\n"
|
||||
output += f"Description: {model_info.get('description', 'Unknown')}\n"
|
||||
|
||||
if "inputTokenLimit" in model_info:
|
||||
output += f"Input Token Limit: {model_info['inputTokenLimit']}\n"
|
||||
|
||||
if "outputTokenLimit" in model_info:
|
||||
output += f"Output Token Limit: {model_info['outputTokenLimit']}\n"
|
||||
|
||||
return output
|
||||
|
||||
def test_connection(self) -> str:
|
||||
"""Test connection to Google"""
|
||||
try:
|
||||
# Try to list models as a connection test
|
||||
result = self._make_request("models")
|
||||
if result["success"]:
|
||||
return "✅ Google API connection successful."
|
||||
else:
|
||||
return f"❌ Google API connection failed: {result['error']}"
|
||||
except Exception as e:
|
||||
return f"❌ Error connecting to Google: {str(e)}"
|
||||
|
||||
def get_default_model(self) -> str:
|
||||
"""Get the default model for Google"""
|
||||
return "Default model: gemini-1.5-pro (recommended for most tasks)"
|
||||
|
||||
def set_default_model(self, model_name: str) -> str:
|
||||
"""Set the default model for Google"""
|
||||
# Google doesn't have a built-in default model concept
|
||||
# Just verify the model exists
|
||||
result = self._make_request(f"models/{model_name}")
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ Model '{model_name}' exists and can be used as default."
|
||||
else:
|
||||
return f"❌ Model '{model_name}' not found on Google."
|
||||
|
||||
def get_provider_info(self) -> str:
|
||||
"""Get information about Google configuration"""
|
||||
output = "Google Provider Configuration:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
output += f"API URL: {self.api_url}\n"
|
||||
output += f"API Key: {'Set' if self.api_key else 'Not set'}\n"
|
||||
|
||||
# Test connection
|
||||
try:
|
||||
result = self._make_request("models")
|
||||
if result["success"]:
|
||||
output += "Status: ✅ Connected\n"
|
||||
else:
|
||||
output += f"Status: ❌ Connection failed\n"
|
||||
except:
|
||||
output += "Status: ❌ Not connected\n"
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def get_provider_tools(provider: str, **kwargs) -> List[Tool]:
|
||||
"""
|
||||
Get tools for a specific provider
|
||||
|
||||
Args:
|
||||
provider: Provider name ("ollama", "openai", "openrouter", "anthropic", "google")
|
||||
**kwargs: Provider-specific configuration
|
||||
|
||||
Returns:
|
||||
List of tools for the provider
|
||||
"""
|
||||
provider = provider.lower()
|
||||
|
||||
if provider == "ollama":
|
||||
base_url = kwargs.get("base_url", os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"))
|
||||
tools = OllamaProviderTools(base_url=base_url)
|
||||
return tools.get_tools()
|
||||
|
||||
elif provider == "openai":
|
||||
api_key = kwargs.get("api_key", os.getenv("OPENAI_API_KEY"))
|
||||
if not api_key:
|
||||
print("Warning: OpenAI API key not provided")
|
||||
return []
|
||||
tools = OpenAIProviderTools(api_key=api_key)
|
||||
return tools.get_tools()
|
||||
|
||||
elif provider == "openrouter":
|
||||
api_key = kwargs.get("api_key", os.getenv("OPENROUTER_API_KEY"))
|
||||
base_url = kwargs.get("base_url", os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"))
|
||||
if not api_key:
|
||||
print("Warning: OpenRouter API key not provided")
|
||||
return []
|
||||
tools = OpenRouterProviderTools(api_key=api_key, base_url=base_url)
|
||||
return tools.get_tools()
|
||||
|
||||
elif provider == "anthropic":
|
||||
api_key = kwargs.get("api_key", os.getenv("ANTHROPIC_API_KEY"))
|
||||
if not api_key:
|
||||
print("Warning: Anthropic API key not provided")
|
||||
return []
|
||||
tools = AnthropicProviderTools(api_key=api_key)
|
||||
return tools.get_tools()
|
||||
|
||||
elif provider == "google":
|
||||
api_key = kwargs.get("api_key", os.getenv("GOOGLE_API_KEY"))
|
||||
if not api_key:
|
||||
print("Warning: Google API key not provided")
|
||||
return []
|
||||
tools = GoogleProviderTools(api_key=api_key)
|
||||
return tools.get_tools()
|
||||
|
||||
else:
|
||||
print(f"Warning: Unknown provider '{provider}'")
|
||||
return []
|
||||
|
||||
|
||||
def get_all_provider_tools() -> List[Tool]:
|
||||
"""
|
||||
Get tools for all configured providers
|
||||
|
||||
Returns:
|
||||
List of tools for all configured providers
|
||||
"""
|
||||
all_tools = []
|
||||
|
||||
# Check for Ollama configuration
|
||||
ollama_url = os.getenv("OLLAMA_BASE_URL")
|
||||
if ollama_url:
|
||||
try:
|
||||
ollama_tools = get_provider_tools("ollama", base_url=ollama_url)
|
||||
all_tools.extend(ollama_tools)
|
||||
print(f"✅ Added {len(ollama_tools)} Ollama tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load Ollama tools: {e}")
|
||||
|
||||
# Check for OpenAI configuration
|
||||
openai_key = os.getenv("OPENAI_API_KEY")
|
||||
if openai_key:
|
||||
try:
|
||||
openai_tools = get_provider_tools("openai", api_key=openai_key)
|
||||
all_tools.extend(openai_tools)
|
||||
print(f"✅ Added {len(openai_tools)} OpenAI tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load OpenAI tools: {e}")
|
||||
|
||||
# Check for OpenRouter configuration
|
||||
openrouter_key = os.getenv("OPENROUTER_API_KEY")
|
||||
if openrouter_key:
|
||||
try:
|
||||
openrouter_tools = get_provider_tools("openrouter", api_key=openrouter_key)
|
||||
all_tools.extend(openrouter_tools)
|
||||
print(f"✅ Added {len(openrouter_tools)} OpenRouter tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load OpenRouter tools: {e}")
|
||||
|
||||
# Check for Anthropic configuration
|
||||
anthropic_key = os.getenv("ANTHROPIC_API_KEY")
|
||||
if anthropic_key:
|
||||
try:
|
||||
anthropic_tools = get_provider_tools("anthropic", api_key=anthropic_key)
|
||||
all_tools.extend(anthropic_tools)
|
||||
print(f"✅ Added {len(anthropic_tools)} Anthropic tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load Anthropic tools: {e}")
|
||||
|
||||
# Check for Google configuration
|
||||
google_key = os.getenv("GOOGLE_API_KEY")
|
||||
if google_key:
|
||||
try:
|
||||
google_tools = get_provider_tools("google", api_key=google_key)
|
||||
all_tools.extend(google_tools)
|
||||
print(f"✅ Added {len(google_tools)} Google tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load Google tools: {e}")
|
||||
|
||||
return all_tools
|
||||
@@ -0,0 +1,320 @@
|
||||
"""
|
||||
Proxmox management tools for MaxClaw Agent
|
||||
Provides tools for managing Proxmox virtual machines and containers
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
from typing import Dict, List, Optional, Any
|
||||
from langchain.tools import Tool
|
||||
|
||||
|
||||
class ProxmoxTools:
|
||||
"""Tools for managing Proxmox virtual environment"""
|
||||
|
||||
def __init__(self, host: str = "192.168.0.100", user: str = "root",
|
||||
password: Optional[str] = None, api_token: Optional[str] = None):
|
||||
"""
|
||||
Initialize Proxmox tools
|
||||
|
||||
Args:
|
||||
host: Proxmox host address
|
||||
user: Username for authentication
|
||||
password: Password for authentication (optional if using API token)
|
||||
api_token: API token for authentication (optional if using password)
|
||||
"""
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.api_token = api_token
|
||||
self.base_url = f"https://{host}:8006/api2/json"
|
||||
|
||||
def _run_pvesh_command(self, command: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Run a pvesh command and return the result
|
||||
|
||||
Args:
|
||||
command: The pvesh command to run
|
||||
|
||||
Returns:
|
||||
Dictionary with command result
|
||||
"""
|
||||
try:
|
||||
# Build the full command
|
||||
full_command = f"pvesh {command}"
|
||||
|
||||
# Add authentication if needed
|
||||
if self.api_token:
|
||||
full_command = f"pvesh {command} --apitoken {self.api_token}"
|
||||
elif self.password:
|
||||
# Note: In production, use proper authentication methods
|
||||
full_command = f"pvesh {command} --username {self.user} --password {self.password}"
|
||||
|
||||
# Run the command
|
||||
result = subprocess.run(
|
||||
full_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
return {"success": True, "data": json.loads(result.stdout)}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "data": result.stdout}
|
||||
else:
|
||||
return {"success": False, "error": result.stderr}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "error": "Command timed out"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def list_vms(self) -> str:
|
||||
"""
|
||||
List all virtual machines in the Proxmox cluster
|
||||
|
||||
Returns:
|
||||
Formatted string with VM information
|
||||
"""
|
||||
result = self._run_pvesh_command("get /cluster/resources --type vm")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error listing VMs: {result['error']}"
|
||||
|
||||
vms = result["data"]
|
||||
if not vms:
|
||||
return "No virtual machines found."
|
||||
|
||||
output = "Virtual Machines:\n"
|
||||
output += "=" * 50 + "\n"
|
||||
|
||||
for vm in vms:
|
||||
vmid = vm.get("vmid", "N/A")
|
||||
name = vm.get("name", "Unknown")
|
||||
status = vm.get("status", "Unknown")
|
||||
node = vm.get("node", "Unknown")
|
||||
cpu = vm.get("cpu", 0) * 100
|
||||
mem = vm.get("mem", 0) / (1024 * 1024) # Convert to MB
|
||||
maxmem = vm.get("maxmem", 0) / (1024 * 1024 * 1024) # Convert to GB
|
||||
|
||||
output += f"VMID: {vmid}\n"
|
||||
output += f"Name: {name}\n"
|
||||
output += f"Status: {status}\n"
|
||||
output += f"Node: {node}\n"
|
||||
output += f"CPU Usage: {cpu:.1f}%\n"
|
||||
output += f"Memory: {mem:.1f} MB / {maxmem:.1f} GB\n"
|
||||
output += "-" * 30 + "\n"
|
||||
|
||||
return output
|
||||
|
||||
def get_vm_status(self, vmid: int) -> str:
|
||||
"""
|
||||
Get the status of a specific virtual machine
|
||||
|
||||
Args:
|
||||
vmid: Virtual machine ID
|
||||
|
||||
Returns:
|
||||
Formatted string with VM status
|
||||
"""
|
||||
result = self._run_pvesh_command(f"get /nodes/proxmox/qemu/{vmid}/status/current")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error getting VM status: {result['error']}"
|
||||
|
||||
vm = result["data"]
|
||||
status = vm.get("status", "Unknown")
|
||||
cpu = vm.get("cpu", 0) * 100
|
||||
mem = vm.get("mem", 0) / (1024 * 1024) # Convert to MB
|
||||
maxmem = vm.get("maxmem", 0) / (1024 * 1024 * 1024) # Convert to GB
|
||||
uptime = vm.get("uptime", 0)
|
||||
|
||||
# Convert uptime to human readable format
|
||||
days = uptime // (24 * 3600)
|
||||
hours = (uptime % (24 * 3600)) // 3600
|
||||
minutes = (uptime % 3600) // 60
|
||||
|
||||
output = f"VM {vmid} Status:\n"
|
||||
output += "=" * 30 + "\n"
|
||||
output += f"Status: {status}\n"
|
||||
output += f"CPU Usage: {cpu:.1f}%\n"
|
||||
output += f"Memory: {mem:.1f} MB / {maxmem:.1f} GB\n"
|
||||
output += f"Uptime: {days} days, {hours} hours, {minutes} minutes\n"
|
||||
|
||||
return output
|
||||
|
||||
def start_vm(self, vmid: int) -> str:
|
||||
"""
|
||||
Start a virtual machine
|
||||
|
||||
Args:
|
||||
vmid: Virtual machine ID
|
||||
|
||||
Returns:
|
||||
Result message
|
||||
"""
|
||||
result = self._run_pvesh_command(f"create /nodes/proxmox/qemu/{vmid}/status/start")
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ VM {vmid} started successfully."
|
||||
else:
|
||||
return f"❌ Failed to start VM {vmid}: {result['error']}"
|
||||
|
||||
def stop_vm(self, vmid: int) -> str:
|
||||
"""
|
||||
Stop a virtual machine
|
||||
|
||||
Args:
|
||||
vmid: Virtual machine ID
|
||||
|
||||
Returns:
|
||||
Result message
|
||||
"""
|
||||
result = self._run_pvesh_command(f"create /nodes/proxmox/qemu/{vmid}/status/stop")
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ VM {vmid} stopped successfully."
|
||||
else:
|
||||
return f"❌ Failed to stop VM {vmid}: {result['error']}"
|
||||
|
||||
def restart_vm(self, vmid: int) -> str:
|
||||
"""
|
||||
Restart a virtual machine
|
||||
|
||||
Args:
|
||||
vmid: Virtual machine ID
|
||||
|
||||
Returns:
|
||||
Result message
|
||||
"""
|
||||
result = self._run_pvesh_command(f"create /nodes/proxmox/qemu/{vmid}/status/reboot")
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ VM {vmid} restarted successfully."
|
||||
else:
|
||||
return f"❌ Failed to restart VM {vmid}: {result['error']}"
|
||||
|
||||
def create_snapshot(self, vmid: int, snapname: str, description: str = "") -> str:
|
||||
"""
|
||||
Create a snapshot of a virtual machine
|
||||
|
||||
Args:
|
||||
vmid: Virtual machine ID
|
||||
snapname: Snapshot name
|
||||
description: Optional description
|
||||
|
||||
Returns:
|
||||
Result message
|
||||
"""
|
||||
command = f"create /nodes/proxmox/qemu/{vmid}/snapshot"
|
||||
command += f" --snapname {snapname}"
|
||||
if description:
|
||||
command += f" --description \"{description}\""
|
||||
|
||||
result = self._run_pvesh_command(command)
|
||||
|
||||
if result["success"]:
|
||||
return f"✅ Snapshot '{snapname}' created for VM {vmid}."
|
||||
else:
|
||||
return f"❌ Failed to create snapshot for VM {vmid}: {result['error']}"
|
||||
|
||||
def list_snapshots(self, vmid: int) -> str:
|
||||
"""
|
||||
List all snapshots for a virtual machine
|
||||
|
||||
Args:
|
||||
vmid: Virtual machine ID
|
||||
|
||||
Returns:
|
||||
Formatted string with snapshot information
|
||||
"""
|
||||
result = self._run_pvesh_command(f"get /nodes/proxmox/qemu/{vmid}/snapshot")
|
||||
|
||||
if not result["success"]:
|
||||
return f"Error listing snapshots: {result['error']}"
|
||||
|
||||
snapshots = result["data"]
|
||||
if not snapshots:
|
||||
return f"No snapshots found for VM {vmid}."
|
||||
|
||||
output = f"Snapshots for VM {vmid}:\n"
|
||||
output += "=" * 40 + "\n"
|
||||
|
||||
for snap in snapshots:
|
||||
name = snap.get("name", "Unknown")
|
||||
description = snap.get("description", "")
|
||||
snaptime = snap.get("snaptime", 0)
|
||||
|
||||
# Convert timestamp to readable format
|
||||
if snaptime:
|
||||
from datetime import datetime
|
||||
dt = datetime.fromtimestamp(snaptime)
|
||||
time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
time_str = "N/A"
|
||||
|
||||
output += f"Name: {name}\n"
|
||||
output += f"Description: {description}\n"
|
||||
output += f"Created: {time_str}\n"
|
||||
output += "-" * 30 + "\n"
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def get_proxmox_tools(host: str = "192.168.0.100", user: str = "root",
|
||||
password: Optional[str] = None, api_token: Optional[str] = None) -> List[Tool]:
|
||||
"""
|
||||
Get a list of Proxmox tools for the MaxClaw agent
|
||||
|
||||
Args:
|
||||
host: Proxmox host address
|
||||
user: Username for authentication
|
||||
password: Password for authentication
|
||||
api_token: API token for authentication
|
||||
|
||||
Returns:
|
||||
List of LangChain tools for Proxmox management
|
||||
"""
|
||||
proxmox = ProxmoxTools(host, user, password, api_token)
|
||||
|
||||
tools = [
|
||||
Tool(
|
||||
name="proxmox_list_vms",
|
||||
func=lambda: proxmox.list_vms(),
|
||||
description="List all virtual machines in the Proxmox cluster with their status and resource usage"
|
||||
),
|
||||
Tool(
|
||||
name="proxmox_vm_status",
|
||||
func=lambda vmid: proxmox.get_vm_status(int(vmid)),
|
||||
description="Get detailed status of a specific virtual machine by VMID"
|
||||
),
|
||||
Tool(
|
||||
name="proxmox_start_vm",
|
||||
func=lambda vmid: proxmox.start_vm(int(vmid)),
|
||||
description="Start a virtual machine by VMID"
|
||||
),
|
||||
Tool(
|
||||
name="proxmox_stop_vm",
|
||||
func=lambda vmid: proxmox.stop_vm(int(vmid)),
|
||||
description="Stop a virtual machine by VMID"
|
||||
),
|
||||
Tool(
|
||||
name="proxmox_restart_vm",
|
||||
func=lambda vmid: proxmox.restart_vm(int(vmid)),
|
||||
description="Restart a virtual machine by VMID"
|
||||
),
|
||||
Tool(
|
||||
name="proxmox_create_snapshot",
|
||||
func=lambda vmid, snapname, description="": proxmox.create_snapshot(int(vmid), snapname, description),
|
||||
description="Create a snapshot of a virtual machine with optional description"
|
||||
),
|
||||
Tool(
|
||||
name="proxmox_list_snapshots",
|
||||
func=lambda vmid: proxmox.list_snapshots(int(vmid)),
|
||||
description="List all snapshots for a specific virtual machine"
|
||||
)
|
||||
]
|
||||
|
||||
return tools
|
||||
+998
@@ -0,0 +1,998 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MaxClaw Agent - Configurazione</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.config-list {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.config-info h3 {
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.config-info p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: 400px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px 15px;
|
||||
border-radius: 18px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.agent-message {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chat-input input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-bottom-color: #667eea;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.language-selector select {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: 300px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 90%;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input input {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 15px;
|
||||
border-bottom: none;
|
||||
border-left: 3px solid transparent;
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-bottom: none;
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
position: static;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch-friendly improvements */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.btn:hover {
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
background: #5a6fd8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="language-selector">
|
||||
<select id="language-select" onchange="changeLanguage(this.value)">
|
||||
<option value="it">Italiano</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<h1 id="title">MaxClaw Agent</h1>
|
||||
<p id="subtitle">Interfaccia di configurazione per l'agente AI</p>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchTab('config')" id="tab-config">Configurazione</div>
|
||||
<div class="tab" onclick="switchTab('chat')" id="tab-chat">Chat</div>
|
||||
<div class="tab" onclick="switchTab('status')" id="tab-status">Stato</div>
|
||||
</div>
|
||||
|
||||
<div id="config-tab" class="tab-content active">
|
||||
<h2 id="config-title">Gestione Configurazioni</h2>
|
||||
|
||||
<div id="alert-container"></div>
|
||||
|
||||
<form id="config-form">
|
||||
<div class="form-group">
|
||||
<label for="config-name" id="name-label">Nome Configurazione</label>
|
||||
<input type="text" id="config-name" placeholder="es. mio-ollama" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="config-provider" id="provider-label">Provider</label>
|
||||
<select id="config-provider" required>
|
||||
<option value="">Seleziona un provider</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="google">Google</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="config-model" id="model-label">Nome Modello</label>
|
||||
<input type="text" id="config-model" placeholder="es. gemma2:9b" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="api-key-group">
|
||||
<label for="config-api-key" id="api-key-label">Chiave API</label>
|
||||
<input type="password" id="config-api-key" placeholder="Inserisci la chiave API">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="base-url-group">
|
||||
<label for="config-base-url" id="base-url-label">URL Base</label>
|
||||
<input type="text" id="config-base-url" placeholder="http://localhost:11434">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="config-set-active">
|
||||
<span id="set-active-label">Imposta come configurazione attiva</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="save-btn">Salva Configurazione</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="clearForm()" id="clear-btn">Pulisci</button>
|
||||
</form>
|
||||
|
||||
<div class="config-list">
|
||||
<h3 id="existing-configs-title">Configurazioni Esistenti</h3>
|
||||
<div id="config-list-container">
|
||||
<!-- Configurations will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chat-tab" class="tab-content">
|
||||
<h2 id="chat-title">Chat con MaxClaw</h2>
|
||||
|
||||
<div class="chat-container" id="chat-container">
|
||||
<div class="message agent-message" id="welcome-message">
|
||||
Ciao! Sono MaxClaw, il tuo agente AI. Come posso aiutarti oggi?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<input type="text" id="chat-input" placeholder="Scrivi un messaggio...">
|
||||
<button class="btn" onclick="sendMessage()" id="send-btn">Invia</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status-tab" class="tab-content">
|
||||
<h2 id="status-title">Stato del Sistema</h2>
|
||||
|
||||
<div id="status-container">
|
||||
<div class="alert alert-success" id="system-status-alert">
|
||||
<strong id="system-ok">✅ Sistema operativo</strong><br>
|
||||
<span id="system-ready">L'agente è pronto per l'uso.</span>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<div class="config-info">
|
||||
<h3 id="active-provider-title">Provider Attivo</h3>
|
||||
<p id="active-provider">Nessun provider configurato</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<div class="config-info">
|
||||
<h3 id="active-model-title">Modello Attivo</h3>
|
||||
<p id="active-model">Nessun modello configurato</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<div class="config-info">
|
||||
<h3 id="connection-title">Connessione</h3>
|
||||
<p id="connection-status">Verifica in corso...</p>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="testConnection()" id="test-connection-btn">Testa Connessione</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// API base URL
|
||||
const API_BASE = 'http://localhost:8000';
|
||||
|
||||
// Current language
|
||||
let currentLanguage = 'it';
|
||||
|
||||
// Translations
|
||||
const translations = {
|
||||
it: {
|
||||
title: "MaxClaw Agent",
|
||||
subtitle: "Interfaccia di configurazione per l'agente AI",
|
||||
tabConfig: "Configurazione",
|
||||
tabChat: "Chat",
|
||||
tabStatus: "Stato",
|
||||
configTitle: "Gestione Configurazioni",
|
||||
nameLabel: "Nome Configurazione",
|
||||
providerLabel: "Provider",
|
||||
modelLabel: "Nome Modello",
|
||||
apiKeyLabel: "Chiave API",
|
||||
baseUrlLabel: "URL Base",
|
||||
setActiveLabel: "Imposta come configurazione attiva",
|
||||
saveBtn: "Salva Configurazione",
|
||||
clearBtn: "Pulisci",
|
||||
existingConfigsTitle: "Configurazioni Esistenti",
|
||||
chatTitle: "Chat con MaxClaw",
|
||||
welcomeMessage: "Ciao! Sono MaxClaw, il tuo agente AI. Come posso aiutarti oggi?",
|
||||
chatPlaceholder: "Scrivi un messaggio...",
|
||||
sendBtn: "Invia",
|
||||
statusTitle: "Stato del Sistema",
|
||||
systemOk: "✅ Sistema operativo",
|
||||
systemReady: "L'agente è pronto per l'uso.",
|
||||
activeProviderTitle: "Provider Attivo",
|
||||
activeModelTitle: "Modello Attivo",
|
||||
connectionTitle: "Connessione",
|
||||
testConnectionBtn: "Testa Connessione",
|
||||
alertSuccess: "Configurazione salvata con successo!",
|
||||
alertError: "Errore",
|
||||
alertCancelled: "Annullato.",
|
||||
alertTesting: "Test in corso...",
|
||||
alertConnectionSuccess: "Connessione riuscita!",
|
||||
alertConnectionFailed: "Connessione fallita",
|
||||
noProvider: "Nessun provider configurato",
|
||||
noModel: "Nessun modello configurato",
|
||||
checking: "Verifica in corso...",
|
||||
active: "Attivo",
|
||||
inactive: "Inattivo",
|
||||
test: "Testa",
|
||||
delete: "Elimina",
|
||||
activate: "Attiva"
|
||||
},
|
||||
en: {
|
||||
title: "MaxClaw Agent",
|
||||
subtitle: "Configuration interface for AI agent",
|
||||
tabConfig: "Configuration",
|
||||
tabChat: "Chat",
|
||||
tabStatus: "Status",
|
||||
configTitle: "Configuration Management",
|
||||
nameLabel: "Configuration Name",
|
||||
providerLabel: "Provider",
|
||||
modelLabel: "Model Name",
|
||||
apiKeyLabel: "API Key",
|
||||
baseUrlLabel: "Base URL",
|
||||
setActiveLabel: "Set as active configuration",
|
||||
saveBtn: "Save Configuration",
|
||||
clearBtn: "Clear",
|
||||
existingConfigsTitle: "Existing Configurations",
|
||||
chatTitle: "Chat with MaxClaw",
|
||||
welcomeMessage: "Hello! I'm MaxClaw, your AI agent. How can I help you today?",
|
||||
chatPlaceholder: "Type a message...",
|
||||
sendBtn: "Send",
|
||||
statusTitle: "System Status",
|
||||
systemOk: "✅ System operational",
|
||||
systemReady: "The agent is ready to use.",
|
||||
activeProviderTitle: "Active Provider",
|
||||
activeModelTitle: "Active Model",
|
||||
connectionTitle: "Connection",
|
||||
testConnectionBtn: "Test Connection",
|
||||
alertSuccess: "Configuration saved successfully!",
|
||||
alertError: "Error",
|
||||
alertCancelled: "Cancelled.",
|
||||
alertTesting: "Testing...",
|
||||
alertConnectionSuccess: "Connection successful!",
|
||||
alertConnectionFailed: "Connection failed",
|
||||
noProvider: "No provider configured",
|
||||
noModel: "No model configured",
|
||||
checking: "Checking...",
|
||||
active: "Active",
|
||||
inactive: "Inactive",
|
||||
test: "Test",
|
||||
delete: "Delete",
|
||||
activate: "Activate"
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load saved language preference
|
||||
const savedLanguage = localStorage.getItem('maxclaw-language') || 'it';
|
||||
document.getElementById('language-select').value = savedLanguage;
|
||||
changeLanguage(savedLanguage);
|
||||
|
||||
loadConfigurations();
|
||||
loadActiveConfiguration();
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('config-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveConfiguration();
|
||||
});
|
||||
|
||||
// Handle provider change
|
||||
document.getElementById('config-provider').addEventListener('change', function() {
|
||||
updateFormFields();
|
||||
});
|
||||
|
||||
// Handle chat input
|
||||
document.getElementById('chat-input').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Change language
|
||||
function changeLanguage(language) {
|
||||
currentLanguage = language;
|
||||
localStorage.setItem('maxclaw-language', language);
|
||||
|
||||
const t = translations[language];
|
||||
|
||||
// Update all translatable elements
|
||||
document.getElementById('title').textContent = t.title;
|
||||
document.getElementById('subtitle').textContent = t.subtitle;
|
||||
document.getElementById('tab-config').textContent = t.tabConfig;
|
||||
document.getElementById('tab-chat').textContent = t.tabChat;
|
||||
document.getElementById('tab-status').textContent = t.tabStatus;
|
||||
document.getElementById('config-title').textContent = t.configTitle;
|
||||
document.getElementById('name-label').textContent = t.nameLabel;
|
||||
document.getElementById('provider-label').textContent = t.providerLabel;
|
||||
document.getElementById('model-label').textContent = t.modelLabel;
|
||||
document.getElementById('api-key-label').textContent = t.apiKeyLabel;
|
||||
document.getElementById('base-url-label').textContent = t.baseUrlLabel;
|
||||
document.getElementById('set-active-label').textContent = t.setActiveLabel;
|
||||
document.getElementById('save-btn').textContent = t.saveBtn;
|
||||
document.getElementById('clear-btn').textContent = t.clearBtn;
|
||||
document.getElementById('existing-configs-title').textContent = t.existingConfigsTitle;
|
||||
document.getElementById('chat-title').textContent = t.chatTitle;
|
||||
document.getElementById('welcome-message').textContent = t.welcomeMessage;
|
||||
document.getElementById('chat-input').placeholder = t.chatPlaceholder;
|
||||
document.getElementById('send-btn').textContent = t.sendBtn;
|
||||
document.getElementById('status-title').textContent = t.statusTitle;
|
||||
document.getElementById('system-ok').textContent = t.systemOk;
|
||||
document.getElementById('system-ready').textContent = t.systemReady;
|
||||
document.getElementById('active-provider-title').textContent = t.activeProviderTitle;
|
||||
document.getElementById('active-model-title').textContent = t.activeModelTitle;
|
||||
document.getElementById('connection-title').textContent = t.connectionTitle;
|
||||
document.getElementById('test-connection-btn').textContent = t.testConnectionBtn;
|
||||
|
||||
// Update HTML lang attribute
|
||||
document.documentElement.lang = language;
|
||||
}
|
||||
|
||||
// Switch tabs
|
||||
function switchTab(tabName) {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Update tab content
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(tabName + '-tab').classList.add('active');
|
||||
}
|
||||
|
||||
// Load configurations
|
||||
async function loadConfigurations() {
|
||||
try {
|
||||
const response = await fetch(API_BASE + '/configurations');
|
||||
const configs = await response.json();
|
||||
|
||||
const container = document.getElementById('config-list-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (configs.length === 0) {
|
||||
container.innerHTML = `<p>${translations[currentLanguage].noProvider}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
configs.forEach(config => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'config-item';
|
||||
item.innerHTML = `
|
||||
<div class="config-info">
|
||||
<h3>${config.name}
|
||||
${config.is_active ? `<span class="status-badge status-active">${translations[currentLanguage].active}</span>` : ''}
|
||||
</h3>
|
||||
<p>${translations[currentLanguage].providerLabel}: ${config.provider} | ${translations[currentLanguage].modelLabel}: ${config.model_name}</p>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-secondary" onclick="activateConfig('${config.name}')">${translations[currentLanguage].activate}</button>
|
||||
<button class="btn btn-secondary" onclick="testConfig('${config.name}')">${translations[currentLanguage].test}</button>
|
||||
<button class="btn btn-danger" onclick="deleteConfig('${config.name}')">${translations[currentLanguage].delete}</button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
});
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load active configuration
|
||||
async function loadActiveConfiguration() {
|
||||
try {
|
||||
const response = await fetch(API_BASE + '/configurations/active');
|
||||
const config = await response.json();
|
||||
|
||||
if (config.name) {
|
||||
document.getElementById('active-provider').textContent = config.provider;
|
||||
document.getElementById('active-model').textContent = config.model_name;
|
||||
} else {
|
||||
document.getElementById('active-provider').textContent = translations[currentLanguage].noProvider;
|
||||
document.getElementById('active-model').textContent = translations[currentLanguage].noModel;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading active configuration:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
async function saveConfiguration() {
|
||||
const name = document.getElementById('config-name').value;
|
||||
const provider = document.getElementById('config-provider').value;
|
||||
const model = document.getElementById('config-model').value;
|
||||
const apiKey = document.getElementById('config-api-key').value;
|
||||
const baseUrl = document.getElementById('config-base-url').value;
|
||||
const setActive = document.getElementById('config-set-active').checked;
|
||||
|
||||
const config = {
|
||||
name: name,
|
||||
provider: provider,
|
||||
model_name: model,
|
||||
set_active: setActive
|
||||
};
|
||||
|
||||
if (apiKey) {
|
||||
config.api_key = apiKey;
|
||||
}
|
||||
|
||||
if (baseUrl) {
|
||||
config.base_url = baseUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(API_BASE + '/configurations', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert(translations[currentLanguage].alertSuccess, 'success');
|
||||
clearForm();
|
||||
loadConfigurations();
|
||||
loadActiveConfiguration();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.detail, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Activate configuration
|
||||
async function activateConfig(name) {
|
||||
try {
|
||||
const response = await fetch(API_BASE + `/configurations/${name}/activate`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} "${name}"`, 'success');
|
||||
loadConfigurations();
|
||||
loadActiveConfiguration();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.detail, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test configuration
|
||||
async function testConfig(name) {
|
||||
try {
|
||||
const response = await fetch(API_BASE + `/configurations/${name}/test`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showAlert(`✅ ${result.message}`, 'success');
|
||||
} else {
|
||||
showAlert(`❌ ${result.message}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete configuration
|
||||
async function deleteConfig(name) {
|
||||
if (!confirm(`Sei sicuro di voler eliminare la configurazione "${name}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(API_BASE + `/configurations/${name}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} "${name}"`, 'success');
|
||||
loadConfigurations();
|
||||
loadActiveConfiguration();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.detail, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test connection
|
||||
async function testConnection() {
|
||||
document.getElementById('connection-status').textContent = translations[currentLanguage].alertTesting;
|
||||
|
||||
try {
|
||||
const response = await fetch(API_BASE + '/health');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'healthy') {
|
||||
document.getElementById('connection-status').textContent = translations[currentLanguage].alertConnectionSuccess;
|
||||
showAlert(translations[currentLanguage].alertConnectionSuccess, 'success');
|
||||
} else {
|
||||
document.getElementById('connection-status').textContent = translations[currentLanguage].alertConnectionFailed;
|
||||
showAlert(translations[currentLanguage].alertConnectionFailed, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('connection-status').textContent = translations[currentLanguage].alertConnectionFailed;
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Send chat message
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById('chat-input');
|
||||
const message = input.value.trim();
|
||||
|
||||
if (!message) return;
|
||||
|
||||
// Add user message to chat
|
||||
addMessageToChat(message, 'user');
|
||||
input.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch(API_BASE + '/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ message: message })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Add agent response to chat
|
||||
addMessageToChat(result.response, 'agent');
|
||||
} catch (error) {
|
||||
addMessageToChat(translations[currentLanguage].alertError + ': ' + error.message, 'agent');
|
||||
}
|
||||
}
|
||||
|
||||
// Add message to chat
|
||||
function addMessageToChat(message, sender) {
|
||||
const container = document.getElementById('chat-container');
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${sender}-message`;
|
||||
messageDiv.textContent = message;
|
||||
container.appendChild(messageDiv);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// Update form fields based on provider
|
||||
function updateFormFields() {
|
||||
const provider = document.getElementById('config-provider').value;
|
||||
const apiKeyGroup = document.getElementById('api-key-group');
|
||||
const baseUrlGroup = document.getElementById('base-url-group');
|
||||
const baseUrlInput = document.getElementById('config-base-url');
|
||||
|
||||
// Show/hide fields based on provider
|
||||
if (provider === 'openai' || provider === 'openrouter' || provider === 'anthropic' || provider === 'google') {
|
||||
apiKeyGroup.style.display = 'block';
|
||||
} else {
|
||||
apiKeyGroup.style.display = 'none';
|
||||
}
|
||||
|
||||
if (provider === 'ollama' || provider === 'openrouter') {
|
||||
baseUrlGroup.style.display = 'block';
|
||||
if (provider === 'ollama') {
|
||||
baseUrlInput.placeholder = 'http://192.168.0.50:11434';
|
||||
} else {
|
||||
baseUrlInput.placeholder = 'https://openrouter.ai/api/v1';
|
||||
}
|
||||
} else {
|
||||
baseUrlGroup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Clear form
|
||||
function clearForm() {
|
||||
document.getElementById('config-form').reset();
|
||||
updateFormFields();
|
||||
}
|
||||
|
||||
// Show alert
|
||||
function showAlert(message, type) {
|
||||
const container = document.getElementById('alert-container');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
|
||||
// Remove existing alerts
|
||||
container.innerHTML = '';
|
||||
container.appendChild(alert);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,906 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MaxClaw Agent - Marketplace</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.language-selector select {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 12px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.plugins-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.plugin-card {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.plugin-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.plugin-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.plugin-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.plugin-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-installed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-available {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.plugin-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.plugin-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-bottom-color: #667eea;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.plugins-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.plugin-card {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.plugin-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 15px;
|
||||
border-bottom: none;
|
||||
border-left: 3px solid transparent;
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-bottom: none;
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
position: static;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch-friendly improvements */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.btn:hover {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.plugin-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.plugin-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="language-selector">
|
||||
<select id="language-select" onchange="changeLanguage(this.value)">
|
||||
<option value="it">Italiano</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<h1 id="title">MaxClaw Agent - Marketplace</h1>
|
||||
<p id="subtitle">Scopri e installa plugin per estendere le funzionalità</p>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchTab('marketplace')" id="tab-marketplace">Marketplace</div>
|
||||
<div class="tab" onclick="switchTab('installed')" id="tab-installed">Plugin Installati</div>
|
||||
<div class="tab" onclick="switchTab('search')" id="tab-search">Cerca</div>
|
||||
</div>
|
||||
|
||||
<div id="marketplace-tab" class="tab-content active">
|
||||
<h2 id="marketplace-title">🏪 Plugin Disponibili</h2>
|
||||
|
||||
<div id="alert-container"></div>
|
||||
|
||||
<div class="stats-container">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="total-plugins">0</div>
|
||||
<div class="stat-label" id="total-plugins-label">Plugin Totali</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="installed-plugins">0</div>
|
||||
<div class="stat-label" id="installed-plugins-label">Plugin Installati</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="available-plugins">0</div>
|
||||
<div class="stat-label" id="available-plugins-label">Plugin Disponibili</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugins-grid" id="plugins-grid">
|
||||
<!-- Plugins will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="installed-tab" class="tab-content">
|
||||
<h2 id="installed-title">📦 Plugin Installati</h2>
|
||||
|
||||
<div class="plugins-grid" id="installed-plugins-grid">
|
||||
<!-- Installed plugins will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="search-tab" class="tab-content">
|
||||
<h2 id="search-title">🔍 Cerca Plugin</h2>
|
||||
|
||||
<div class="search-container">
|
||||
<input type="text" class="search-input" id="search-input" placeholder="Cerca plugin...">
|
||||
<button class="search-btn" onclick="searchPlugins()" id="search-btn">Cerca</button>
|
||||
</div>
|
||||
|
||||
<div class="plugins-grid" id="search-results-grid">
|
||||
<!-- Search results will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// API base URL
|
||||
const API_BASE = 'http://localhost:8000';
|
||||
|
||||
// Current language
|
||||
let currentLanguage = 'it';
|
||||
|
||||
// Translations
|
||||
const translations = {
|
||||
it: {
|
||||
title: "MaxClaw Agent - Marketplace",
|
||||
subtitle: "Scopri e installa plugin per estendere le funzionalità",
|
||||
tabMarketplace: "Marketplace",
|
||||
tabInstalled: "Plugin Installati",
|
||||
tabSearch: "Cerca",
|
||||
marketplaceTitle: "🏪 Plugin Disponibili",
|
||||
totalPluginsLabel: "Plugin Totali",
|
||||
installedPluginsLabel: "Plugin Installati",
|
||||
availablePluginsLabel: "Plugin Disponibili",
|
||||
installedTitle: "📦 Plugin Installati",
|
||||
searchTitle: "🔍 Cerca Plugin",
|
||||
searchPlaceholder: "Cerca plugin...",
|
||||
searchBtn: "Cerca",
|
||||
installBtn: "Installa",
|
||||
uninstallBtn: "Disinstalla",
|
||||
infoBtn: "Info",
|
||||
alertSuccess: "Operazione completata con successo!",
|
||||
alertError: "Errore",
|
||||
alertWarning: "Avviso",
|
||||
alertSearching: "Ricerca in corso...",
|
||||
alertInstalling: "Installazione in corso...",
|
||||
alertUninstalling: "Disinstallazione in corso...",
|
||||
noPlugins: "Nessun plugin trovato",
|
||||
installed: "Installato",
|
||||
available: "Disponibile",
|
||||
refreshBtn: "Aggiorna"
|
||||
},
|
||||
en: {
|
||||
title: "MaxClaw Agent - Marketplace",
|
||||
subtitle: "Discover and install plugins to extend functionality",
|
||||
tabMarketplace: "Marketplace",
|
||||
tabInstalled: "Installed Plugins",
|
||||
tabSearch: "Search",
|
||||
marketplaceTitle: "🏪 Available Plugins",
|
||||
totalPluginsLabel: "Total Plugins",
|
||||
installedPluginsLabel: "Installed Plugins",
|
||||
availablePluginsLabel: "Available Plugins",
|
||||
installedTitle: "📦 Installed Plugins",
|
||||
searchTitle: "🔍 Search Plugins",
|
||||
searchPlaceholder: "Search plugins...",
|
||||
searchBtn: "Search",
|
||||
installBtn: "Install",
|
||||
uninstallBtn: "Uninstall",
|
||||
infoBtn: "Info",
|
||||
alertSuccess: "Operation completed successfully!",
|
||||
alertError: "Error",
|
||||
alertWarning: "Warning",
|
||||
alertSearching: "Searching...",
|
||||
alertInstalling: "Installing...",
|
||||
alertUninstalling: "Uninstalling...",
|
||||
noPlugins: "No plugins found",
|
||||
installed: "Installed",
|
||||
available: "Available",
|
||||
refreshBtn: "Refresh"
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load saved language preference
|
||||
const savedLanguage = localStorage.getItem('maxclaw-language') || 'it';
|
||||
document.getElementById('language-select').value = savedLanguage;
|
||||
changeLanguage(savedLanguage);
|
||||
|
||||
loadMarketplaceStats();
|
||||
loadPlugins();
|
||||
loadInstalledPlugins();
|
||||
|
||||
// Handle search input
|
||||
document.getElementById('search-input').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchPlugins();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Change language
|
||||
function changeLanguage(language) {
|
||||
currentLanguage = language;
|
||||
localStorage.setItem('maxclaw-language', language);
|
||||
|
||||
const t = translations[language];
|
||||
|
||||
// Update all translatable elements
|
||||
document.getElementById('title').textContent = t.title;
|
||||
document.getElementById('subtitle').textContent = t.subtitle;
|
||||
document.getElementById('tab-marketplace').textContent = t.tabMarketplace;
|
||||
document.getElementById('tab-installed').textContent = t.tabInstalled;
|
||||
document.getElementById('tab-search').textContent = t.tabSearch;
|
||||
document.getElementById('marketplace-title').textContent = t.marketplaceTitle;
|
||||
document.getElementById('total-plugins-label').textContent = t.totalPluginsLabel;
|
||||
document.getElementById('installed-plugins-label').textContent = t.installedPluginsLabel;
|
||||
document.getElementById('available-plugins-label').textContent = t.availablePluginsLabel;
|
||||
document.getElementById('installed-title').textContent = t.installedTitle;
|
||||
document.getElementById('search-title').textContent = t.searchTitle;
|
||||
document.getElementById('search-input').placeholder = t.searchPlaceholder;
|
||||
document.getElementById('search-btn').textContent = t.searchBtn;
|
||||
|
||||
// Update HTML lang attribute
|
||||
document.documentElement.lang = language;
|
||||
}
|
||||
|
||||
// Switch tabs
|
||||
function switchTab(tabName) {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Update tab content
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(tabName + '-tab').classList.add('active');
|
||||
}
|
||||
|
||||
// Load marketplace stats
|
||||
async function loadMarketplaceStats() {
|
||||
try {
|
||||
// Simulate stats (in real implementation, this would come from API)
|
||||
const stats = {
|
||||
total_plugins: 15,
|
||||
installed_plugins: 3,
|
||||
available_plugins: 12
|
||||
};
|
||||
|
||||
document.getElementById('total-plugins').textContent = stats.total_plugins;
|
||||
document.getElementById('installed-plugins').textContent = stats.installed_plugins;
|
||||
document.getElementById('available-plugins').textContent = stats.available_plugins;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading marketplace stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load plugins
|
||||
async function loadPlugins() {
|
||||
try {
|
||||
// Simulate plugins (in real implementation, this would come from API)
|
||||
const plugins = [
|
||||
{
|
||||
name: "weather",
|
||||
description: "Get weather information for any location",
|
||||
installed: false,
|
||||
downloads: 1250,
|
||||
rating: 4.5
|
||||
},
|
||||
{
|
||||
name: "calendar",
|
||||
description: "Manage calendar events and reminders",
|
||||
installed: true,
|
||||
downloads: 980,
|
||||
rating: 4.2
|
||||
},
|
||||
{
|
||||
name: "translation",
|
||||
description: "Translate text between languages",
|
||||
installed: false,
|
||||
downloads: 2100,
|
||||
rating: 4.7
|
||||
},
|
||||
{
|
||||
name: "image_recognition",
|
||||
description: "Analyze and describe images",
|
||||
installed: false,
|
||||
downloads: 750,
|
||||
rating: 4.0
|
||||
},
|
||||
{
|
||||
name: "code_analysis",
|
||||
description: "Analyze code quality and suggest improvements",
|
||||
installed: true,
|
||||
downloads: 1500,
|
||||
rating: 4.8
|
||||
},
|
||||
{
|
||||
name: "database_tools",
|
||||
description: "Connect to and query databases",
|
||||
installed: false,
|
||||
downloads: 620,
|
||||
rating: 4.3
|
||||
}
|
||||
];
|
||||
|
||||
const container = document.getElementById('plugins-grid');
|
||||
container.innerHTML = '';
|
||||
|
||||
plugins.forEach(plugin => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'plugin-card';
|
||||
|
||||
const statusClass = plugin.installed ? 'status-installed' : 'status-available';
|
||||
const statusText = plugin.installed ? translations[currentLanguage].installed : translations[currentLanguage].available;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="plugin-header">
|
||||
<div class="plugin-name">${plugin.name}</div>
|
||||
<div class="plugin-status ${statusClass}">${statusText}</div>
|
||||
</div>
|
||||
<div class="plugin-description">${plugin.description}</div>
|
||||
<div class="plugin-actions">
|
||||
${plugin.installed ?
|
||||
`<button class="btn btn-danger" onclick="uninstallPlugin('${plugin.name}')">${translations[currentLanguage].uninstallBtn}</button>` :
|
||||
`<button class="btn btn-primary" onclick="installPlugin('${plugin.name}')">${translations[currentLanguage].installBtn}</button>`
|
||||
}
|
||||
<button class="btn btn-secondary" onclick="showPluginInfo('${plugin.name}')">${translations[currentLanguage].infoBtn}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load installed plugins
|
||||
async function loadInstalledPlugins() {
|
||||
try {
|
||||
// Simulate installed plugins (in real implementation, this would come from API)
|
||||
const installed = [
|
||||
{
|
||||
name: "calendar",
|
||||
description: "Manage calendar events and reminders",
|
||||
version: "1.0.0",
|
||||
author: "MaxClaw Team"
|
||||
},
|
||||
{
|
||||
name: "code_analysis",
|
||||
description: "Analyze code quality and suggest improvements",
|
||||
version: "1.2.0",
|
||||
author: "Community"
|
||||
},
|
||||
{
|
||||
name: "file_tools",
|
||||
description: "Additional file management tools",
|
||||
version: "1.0.0",
|
||||
author: "MaxClaw Team"
|
||||
}
|
||||
];
|
||||
|
||||
const container = document.getElementById('installed-plugins-grid');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (installed.length === 0) {
|
||||
container.innerHTML = `<p>${translations[currentLanguage].noPlugins}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
installed.forEach(plugin => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'plugin-card';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="plugin-header">
|
||||
<div class="plugin-name">${plugin.name}</div>
|
||||
<div class="plugin-status status-installed">${translations[currentLanguage].installed}</div>
|
||||
</div>
|
||||
<div class="plugin-description">${plugin.description}</div>
|
||||
<div class="plugin-actions">
|
||||
<button class="btn btn-danger" onclick="uninstallPlugin('${plugin.name}')">${translations[currentLanguage].uninstallBtn}</button>
|
||||
<button class="btn btn-secondary" onclick="showPluginInfo('${plugin.name}')">${translations[currentLanguage].infoBtn}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Search plugins
|
||||
async function searchPlugins() {
|
||||
const query = document.getElementById('search-input').value.trim();
|
||||
|
||||
if (!query) {
|
||||
showAlert(translations[currentLanguage].alertWarning + ': Inserisci un termine di ricerca', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showAlert(translations[currentLanguage].alertSearching, 'info');
|
||||
|
||||
try {
|
||||
// Simulate search results (in real implementation, this would come from API)
|
||||
const results = [
|
||||
{
|
||||
name: "weather",
|
||||
description: "Get weather information for any location",
|
||||
installed: false
|
||||
},
|
||||
{
|
||||
name: "translation",
|
||||
description: "Translate text between languages",
|
||||
installed: false
|
||||
}
|
||||
];
|
||||
|
||||
const container = document.getElementById('search-results-grid');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (results.length === 0) {
|
||||
container.innerHTML = `<p>${translations[currentLanguage].noPlugins}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
results.forEach(plugin => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'plugin-card';
|
||||
|
||||
const statusClass = plugin.installed ? 'status-installed' : 'status-available';
|
||||
const statusText = plugin.installed ? translations[currentLanguage].installed : translations[currentLanguage].available;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="plugin-header">
|
||||
<div class="plugin-name">${plugin.name}</div>
|
||||
<div class="plugin-status ${statusClass}">${statusText}</div>
|
||||
</div>
|
||||
<div class="plugin-description">${plugin.description}</div>
|
||||
<div class="plugin-actions">
|
||||
${plugin.installed ?
|
||||
`<button class="btn btn-danger" onclick="uninstallPlugin('${plugin.name}')">${translations[currentLanguage].uninstallBtn}</button>` :
|
||||
`<button class="btn btn-primary" onclick="installPlugin('${plugin.name}')">${translations[currentLanguage].installBtn}</button>`
|
||||
}
|
||||
<button class="btn btn-secondary" onclick="showPluginInfo('${plugin.name}')">${translations[currentLanguage].infoBtn}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} Trovati ${results.length} plugin`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Install plugin
|
||||
async function installPlugin(pluginName) {
|
||||
showAlert(translations[currentLanguage].alertInstalling + ' ' + pluginName, 'info');
|
||||
|
||||
try {
|
||||
// Simulate installation (in real implementation, this would call API)
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} Plugin '${pluginName}' installato!`, 'success');
|
||||
|
||||
// Refresh the lists
|
||||
loadPlugins();
|
||||
loadInstalledPlugins();
|
||||
loadMarketplaceStats();
|
||||
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Uninstall plugin
|
||||
async function uninstallPlugin(pluginName) {
|
||||
if (!confirm(`Sei sicuro di voler disinstallare il plugin '${pluginName}'?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showAlert(translations[currentLanguage].alertUninstalling + ' ' + pluginName, 'info');
|
||||
|
||||
try {
|
||||
// Simulate uninstallation (in real implementation, this would call API)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} Plugin '${pluginName}' disinstallato!`, 'success');
|
||||
|
||||
// Refresh the lists
|
||||
loadPlugins();
|
||||
loadInstalledPlugins();
|
||||
loadMarketplaceStats();
|
||||
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show plugin info
|
||||
function showPluginInfo(pluginName) {
|
||||
alert(`Informazioni sul plugin: ${pluginName}\n\nQuesta funzionalità mostrerà dettagli completi sul plugin, inclusi:\n- Versione\n- Autore\n- Descrizione dettagliata\n- Dipendenze\n- Documentazione`);
|
||||
}
|
||||
|
||||
// Show alert
|
||||
function showAlert(message, type) {
|
||||
const container = document.getElementById('alert-container');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
|
||||
// Remove existing alerts
|
||||
container.innerHTML = '';
|
||||
container.appendChild(alert);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,829 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MaxClaw Agent - Monitoring</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.language-selector select {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.metric:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.metric-value.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.metric-value.warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.metric-value.danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #e9ecef;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 200px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background: #28a745;
|
||||
box-shadow: 0 0 8px #28a745;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background: #dc3545;
|
||||
box-shadow: 0 0 8px #dc3545;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: #ffc107;
|
||||
box-shadow: 0 0 8px #ffc107;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
text-align: right;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 5px;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.log-warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.log-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.log-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-bottom-color: #667eea;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
position: static;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="language-selector">
|
||||
<select id="language-select" onchange="changeLanguage(this.value)">
|
||||
<option value="it">Italiano</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<h1 id="title">MaxClaw Agent - Monitoraggio</h1>
|
||||
<p id="subtitle">Dashboard di monitoraggio in tempo reale</p>
|
||||
</header>
|
||||
|
||||
<button class="refresh-btn" onclick="refreshData()" id="refresh-btn">🔄 Aggiorna Dati</button>
|
||||
|
||||
<div id="alert-container"></div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchTab('dashboard')" id="tab-dashboard">Dashboard</div>
|
||||
<div class="tab" onclick="switchTab('logs')" id="tab-logs">Logs</div>
|
||||
<div class="tab" onclick="switchTab('performance')" id="tab-performance">Performance</div>
|
||||
<div class="tab" onclick="switchTab('system')" id="tab-system">Sistema</div>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-tab" class="tab-content active">
|
||||
<div class="dashboard">
|
||||
<div class="card">
|
||||
<h2 id="stats-title">📊 Statistiche Generali</h2>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="status-label">Stato Sistema</span>
|
||||
<span class="metric-value success" id="system-status">
|
||||
<span class="status-indicator status-online"></span><span id="status-online">Online</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="uptime-label">Uptime</span>
|
||||
<span class="metric-value" id="system-uptime">0 giorni, 0 ore</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="requests-label">Richieste Totali</span>
|
||||
<span class="metric-value" id="total-requests">0</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="errors-label">Errori Totali</span>
|
||||
<span class="metric-value" id="total-errors">0</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="error-rate-label">Tasso di Errore</span>
|
||||
<span class="metric-value" id="error-rate">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 id="provider-title">🤖 Provider Attivo</h2>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="provider-label">Provider</span>
|
||||
<span class="metric-value" id="active-provider">Nessuno</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="model-label">Modello</span>
|
||||
<span class="metric-value" id="active-model">Nessuno</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="connection-label">Stato Connessione</span>
|
||||
<span class="metric-value" id="connection-status">
|
||||
<span class="status-indicator status-offline"></span><span id="status-offline">Non connesso</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="calls-label">Chiamate Provider</span>
|
||||
<span class="metric-value" id="provider-calls">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 id="tools-title">🔧 Utilizzo Strumenti</h2>
|
||||
<div id="tools-usage">
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="tools-loaded-label">Strumenti Caricati</span>
|
||||
<span class="metric-value" id="tools-loaded">0</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="plugins-label">Plugin Attivi</span>
|
||||
<span class="metric-value" id="plugins-active">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 id="resources-title">💻 Risorse di Sistema</h2>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="cpu-label">CPU</span>
|
||||
<span class="metric-value" id="cpu-usage">0%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="cpu-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="memory-label">Memoria</span>
|
||||
<span class="metric-value" id="memory-usage">0%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="memory-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="disk-label">Disco</span>
|
||||
<span class="metric-value" id="disk-usage">0%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="disk-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="logs-tab" class="tab-content">
|
||||
<div class="card">
|
||||
<h2 id="logs-title">📋 Logs Recenti</h2>
|
||||
<div class="log-container" id="logs-container">
|
||||
<!-- Logs will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="performance-tab" class="tab-content">
|
||||
<div class="card">
|
||||
<h2 id="performance-title">📈 Performance</h2>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="response-time-label">Tempo di Risposta Medio</span>
|
||||
<span class="metric-value" id="avg-response-time">0s</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="rpm-label">Richieste al Minuto</span>
|
||||
<span class="metric-value" id="requests-per-minute">0</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="peak-label">Picco Richieste</span>
|
||||
<span class="metric-value" id="peak-requests">0</span>
|
||||
</div>
|
||||
<div class="chart-container" id="performance-chart">
|
||||
<!-- Chart will be rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="system-tab" class="tab-content">
|
||||
<div class="card">
|
||||
<h2 id="system-info-title">🖥️ Informazioni di Sistema</h2>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="hostname-label">Hostname</span>
|
||||
<span class="metric-value" id="system-hostname">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="platform-label">Piattaforma</span>
|
||||
<span class="metric-value" id="system-platform">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="architecture-label">Architettura</span>
|
||||
<span class="metric-value" id="system-architecture">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="python-label">Python Version</span>
|
||||
<span class="metric-value" id="python-version">-</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label" id="maxclaw-label">MaxClaw Version</span>
|
||||
<span class="metric-value" id="maxclaw-version">1.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="last-updated">
|
||||
<span id="last-updated-label">Ultimo aggiornamento:</span> <span id="last-updated">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// API base URL
|
||||
const API_BASE = 'http://localhost:8000';
|
||||
|
||||
// Auto-refresh interval (in milliseconds)
|
||||
const REFRESH_INTERVAL = 30000; // 30 seconds
|
||||
|
||||
// Current language
|
||||
let currentLanguage = 'it';
|
||||
|
||||
// Translations
|
||||
const translations = {
|
||||
it: {
|
||||
title: "MaxClaw Agent - Monitoraggio",
|
||||
subtitle: "Dashboard di monitoraggio in tempo reale",
|
||||
refreshBtn: "🔄 Aggiorna Dati",
|
||||
tabDashboard: "Dashboard",
|
||||
tabLogs: "Logs",
|
||||
tabPerformance: "Performance",
|
||||
tabSystem: "Sistema",
|
||||
statsTitle: "📊 Statistiche Generali",
|
||||
statusLabel: "Stato Sistema",
|
||||
statusOnline: "Online",
|
||||
statusOffline: "Non connesso",
|
||||
uptimeLabel: "Uptime",
|
||||
requestsLabel: "Richieste Totali",
|
||||
errorsLabel: "Errori Totali",
|
||||
errorRateLabel: "Tasso di Errore",
|
||||
providerTitle: "🤖 Provider Attivo",
|
||||
providerLabel: "Provider",
|
||||
modelLabel: "Modello",
|
||||
connectionLabel: "Stato Connessione",
|
||||
callsLabel: "Chiamate Provider",
|
||||
toolsTitle: "🔧 Utilizzo Strumenti",
|
||||
toolsLoadedLabel: "Strumenti Caricati",
|
||||
pluginsLabel: "Plugin Attivi",
|
||||
resourcesTitle: "💻 Risorse di Sistema",
|
||||
cpuLabel: "CPU",
|
||||
memoryLabel: "Memoria",
|
||||
diskLabel: "Disco",
|
||||
logsTitle: "📋 Logs Recenti",
|
||||
performanceTitle: "📈 Performance",
|
||||
responseTimeLabel: "Tempo di Risposta Medio",
|
||||
rpmLabel: "Richieste al Minuto",
|
||||
peakLabel: "Picco Richieste",
|
||||
systemInfoTitle: "🖥️ Informazioni di Sistema",
|
||||
hostnameLabel: "Hostname",
|
||||
platformLabel: "Piattaforma",
|
||||
architectureLabel: "Architettura",
|
||||
pythonLabel: "Python Version",
|
||||
maxclawLabel: "MaxClaw Version",
|
||||
lastUpdatedLabel: "Ultimo aggiornamento:",
|
||||
alertSuccess: "Dati aggiornati con successo",
|
||||
alertError: "Errore nel caricamento dei dati"
|
||||
},
|
||||
en: {
|
||||
title: "MaxClaw Agent - Monitoring",
|
||||
subtitle: "Real-time monitoring dashboard",
|
||||
refreshBtn: "🔄 Refresh Data",
|
||||
tabDashboard: "Dashboard",
|
||||
tabLogs: "Logs",
|
||||
tabPerformance: "Performance",
|
||||
tabSystem: "System",
|
||||
statsTitle: "📊 General Statistics",
|
||||
statusLabel: "System Status",
|
||||
statusOnline: "Online",
|
||||
statusOffline: "Offline",
|
||||
uptimeLabel: "Uptime",
|
||||
requestsLabel: "Total Requests",
|
||||
errorsLabel: "Total Errors",
|
||||
errorRateLabel: "Error Rate",
|
||||
providerTitle: "🤖 Active Provider",
|
||||
providerLabel: "Provider",
|
||||
modelLabel: "Model",
|
||||
connectionLabel: "Connection Status",
|
||||
callsLabel: "Provider Calls",
|
||||
toolsTitle: "🔧 Tool Usage",
|
||||
toolsLoadedLabel: "Loaded Tools",
|
||||
pluginsLabel: "Active Plugins",
|
||||
resourcesTitle: "💻 System Resources",
|
||||
cpuLabel: "CPU",
|
||||
memoryLabel: "Memory",
|
||||
diskLabel: "Disk",
|
||||
logsTitle: "📋 Recent Logs",
|
||||
performanceTitle: "📈 Performance",
|
||||
responseTimeLabel: "Average Response Time",
|
||||
rpmLabel: "Requests per Minute",
|
||||
peakLabel: "Peak Requests",
|
||||
systemInfoTitle: "🖥️ System Information",
|
||||
hostnameLabel: "Hostname",
|
||||
platformLabel: "Platform",
|
||||
architectureLabel: "Architecture",
|
||||
pythonLabel: "Python Version",
|
||||
maxclawLabel: "MaxClaw Version",
|
||||
lastUpdatedLabel: "Last updated:",
|
||||
alertSuccess: "Data refreshed successfully",
|
||||
alertError: "Error loading data"
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load saved language preference
|
||||
const savedLanguage = localStorage.getItem('maxclaw-language') || 'it';
|
||||
document.getElementById('language-select').value = savedLanguage;
|
||||
changeLanguage(savedLanguage);
|
||||
|
||||
refreshData();
|
||||
|
||||
// Set up auto-refresh
|
||||
setInterval(refreshData, REFRESH_INTERVAL);
|
||||
});
|
||||
|
||||
// Change language
|
||||
function changeLanguage(language) {
|
||||
currentLanguage = language;
|
||||
localStorage.setItem('maxclaw-language', language);
|
||||
|
||||
const t = translations[language];
|
||||
|
||||
// Update all translatable elements
|
||||
document.getElementById('title').textContent = t.title;
|
||||
document.getElementById('subtitle').textContent = t.subtitle;
|
||||
document.getElementById('refresh-btn').textContent = t.refreshBtn;
|
||||
document.getElementById('tab-dashboard').textContent = t.tabDashboard;
|
||||
document.getElementById('tab-logs').textContent = t.tabLogs;
|
||||
document.getElementById('tab-performance').textContent = t.tabPerformance;
|
||||
document.getElementById('tab-system').textContent = t.tabSystem;
|
||||
document.getElementById('stats-title').textContent = t.statsTitle;
|
||||
document.getElementById('status-label').textContent = t.statusLabel;
|
||||
document.getElementById('status-online').textContent = t.statusOnline;
|
||||
document.getElementById('status-offline').textContent = t.statusOffline;
|
||||
document.getElementById('uptime-label').textContent = t.uptimeLabel;
|
||||
document.getElementById('requests-label').textContent = t.requestsLabel;
|
||||
document.getElementById('errors-label').textContent = t.errorsLabel;
|
||||
document.getElementById('error-rate-label').textContent = t.errorRateLabel;
|
||||
document.getElementById('provider-title').textContent = t.providerTitle;
|
||||
document.getElementById('provider-label').textContent = t.providerLabel;
|
||||
document.getElementById('model-label').textContent = t.modelLabel;
|
||||
document.getElementById('connection-label').textContent = t.connectionLabel;
|
||||
document.getElementById('calls-label').textContent = t.callsLabel;
|
||||
document.getElementById('tools-title').textContent = t.toolsTitle;
|
||||
document.getElementById('tools-loaded-label').textContent = t.toolsLoadedLabel;
|
||||
document.getElementById('plugins-label').textContent = t.pluginsLabel;
|
||||
document.getElementById('resources-title').textContent = t.resourcesTitle;
|
||||
document.getElementById('cpu-label').textContent = t.cpuLabel;
|
||||
document.getElementById('memory-label').textContent = t.memoryLabel;
|
||||
document.getElementById('disk-label').textContent = t.diskLabel;
|
||||
document.getElementById('logs-title').textContent = t.logsTitle;
|
||||
document.getElementById('performance-title').textContent = t.performanceTitle;
|
||||
document.getElementById('response-time-label').textContent = t.responseTimeLabel;
|
||||
document.getElementById('rpm-label').textContent = t.rpmLabel;
|
||||
document.getElementById('peak-label').textContent = t.peakLabel;
|
||||
document.getElementById('system-info-title').textContent = t.systemInfoTitle;
|
||||
document.getElementById('hostname-label').textContent = t.hostnameLabel;
|
||||
document.getElementById('platform-label').textContent = t.platformLabel;
|
||||
document.getElementById('architecture-label').textContent = t.architectureLabel;
|
||||
document.getElementById('python-label').textContent = t.pythonLabel;
|
||||
document.getElementById('maxclaw-label').textContent = t.maxclawLabel;
|
||||
document.getElementById('last-updated-label').textContent = t.lastUpdatedLabel;
|
||||
|
||||
// Update HTML lang attribute
|
||||
document.documentElement.lang = language;
|
||||
}
|
||||
|
||||
// Switch tabs
|
||||
function switchTab(tabName) {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Update tab content
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(tabName + '-tab').classList.add('active');
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
async function refreshData() {
|
||||
try {
|
||||
// Fetch health status
|
||||
const healthResponse = await fetch(API_BASE + '/health');
|
||||
const healthData = await healthResponse.json();
|
||||
|
||||
// Fetch configurations
|
||||
const configResponse = await fetch(API_BASE + '/configurations');
|
||||
const configs = await configResponse.json();
|
||||
|
||||
// Fetch active configuration
|
||||
const activeResponse = await fetch(API_BASE + '/configurations/active');
|
||||
const activeConfig = await activeResponse.json();
|
||||
|
||||
// Update UI
|
||||
updateDashboard(healthData, configs, activeConfig);
|
||||
updateLastUpdated();
|
||||
|
||||
// Show success alert
|
||||
showAlert(translations[currentLanguage].alertSuccess, 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error refreshing data:', error);
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Update dashboard
|
||||
function updateDashboard(healthData, configs, activeConfig) {
|
||||
const t = translations[currentLanguage];
|
||||
|
||||
// System status
|
||||
const systemStatus = document.getElementById('system-status');
|
||||
if (healthData.status === 'healthy') {
|
||||
systemStatus.innerHTML = `<span class="status-indicator status-online"></span>${t.statusOnline}`;
|
||||
systemStatus.className = 'metric-value success';
|
||||
} else {
|
||||
systemStatus.innerHTML = `<span class="status-indicator status-offline"></span>${t.statusOffline}`;
|
||||
systemStatus.className = 'metric-value danger';
|
||||
}
|
||||
|
||||
// Active provider
|
||||
if (activeConfig.name) {
|
||||
document.getElementById('active-provider').textContent = activeConfig.provider;
|
||||
document.getElementById('active-model').textContent = activeConfig.model_name;
|
||||
|
||||
const connectionStatus = document.getElementById('connection-status');
|
||||
connectionStatus.innerHTML = `<span class="status-indicator status-online"></span>${t.statusOnline}`;
|
||||
connectionStatus.className = 'metric-value success';
|
||||
} else {
|
||||
document.getElementById('active-provider').textContent = 'Nessuno';
|
||||
document.getElementById('active-model').textContent = 'Nessuno';
|
||||
|
||||
const connectionStatus = document.getElementById('connection-status');
|
||||
connectionStatus.innerHTML = `<span class="status-indicator status-offline"></span>${t.statusOffline}`;
|
||||
connectionStatus.className = 'metric-value danger';
|
||||
}
|
||||
|
||||
// Configurations count
|
||||
document.getElementById('total-requests').textContent = configs.length;
|
||||
|
||||
// Simulate some metrics (in a real implementation, these would come from the API)
|
||||
const uptime = Math.floor(Math.random() * 30) + 1;
|
||||
document.getElementById('system-uptime').textContent = `${uptime} giorni, ${Math.floor(Math.random() * 24)} ore`;
|
||||
|
||||
const totalRequests = Math.floor(Math.random() * 1000) + 100;
|
||||
document.getElementById('total-requests').textContent = totalRequests;
|
||||
|
||||
const totalErrors = Math.floor(Math.random() * 10);
|
||||
document.getElementById('total-errors').textContent = totalErrors;
|
||||
|
||||
const errorRate = ((totalErrors / totalRequests) * 100).toFixed(2);
|
||||
document.getElementById('error-rate').textContent = `${errorRate}%`;
|
||||
|
||||
// System resources (simulated)
|
||||
const cpuUsage = Math.floor(Math.random() * 100);
|
||||
const memoryUsage = Math.floor(Math.random() * 100);
|
||||
const diskUsage = Math.floor(Math.random() * 100);
|
||||
|
||||
document.getElementById('cpu-usage').textContent = `${cpuUsage}%`;
|
||||
document.getElementById('memory-usage').textContent = `${memoryUsage}%`;
|
||||
document.getElementById('disk-usage').textContent = `${diskUsage}%`;
|
||||
|
||||
document.getElementById('cpu-bar').style.width = `${cpuUsage}%`;
|
||||
document.getElementById('memory-bar').style.width = `${memoryUsage}%`;
|
||||
document.getElementById('disk-bar').style.width = `${diskUsage}%`;
|
||||
|
||||
// Tools and plugins
|
||||
document.getElementById('tools-loaded').textContent = Math.floor(Math.random() * 20) + 5;
|
||||
document.getElementById('plugins-active').textContent = Math.floor(Math.random() * 5) + 1;
|
||||
|
||||
// Performance metrics
|
||||
const avgResponseTime = (Math.random() * 2).toFixed(3);
|
||||
document.getElementById('avg-response-time').textContent = `${avgResponseTime}s`;
|
||||
|
||||
const requestsPerMinute = Math.floor(Math.random() * 50) + 10;
|
||||
document.getElementById('requests-per-minute').textContent = requestsPerMinute;
|
||||
|
||||
const peakRequests = Math.floor(Math.random() * 100) + 50;
|
||||
document.getElementById('peak-requests').textContent = peakRequests;
|
||||
|
||||
// System info
|
||||
document.getElementById('system-hostname').textContent = 'maxclaw-agent';
|
||||
document.getElementById('system-platform').textContent = navigator.platform;
|
||||
document.getElementById('system-architecture').textContent = navigator.platform.includes('64') ? 'x64' : 'x86';
|
||||
document.getElementById('python-version').textContent = '3.11.0';
|
||||
|
||||
// Update logs
|
||||
updateLogs();
|
||||
}
|
||||
|
||||
// Update logs
|
||||
function updateLogs() {
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
|
||||
// Simulate log entries
|
||||
const logTypes = ['info', 'warning', 'error', 'success'];
|
||||
const logMessages = {
|
||||
it: [
|
||||
'Agente inizializzato con successo',
|
||||
'Configurazione caricata: ollama',
|
||||
'Test connessione riuscito',
|
||||
'Strumento eseguito: list_models',
|
||||
'Avviso: Alto utilizzo memoria',
|
||||
'Errore: Timeout connessione',
|
||||
'Plugin caricato: file_tools',
|
||||
'Richiesta elaborata in 0.245s'
|
||||
],
|
||||
en: [
|
||||
'Agent initialized successfully',
|
||||
'Configuration loaded: ollama',
|
||||
'Connection test successful',
|
||||
'Tool executed: list_models',
|
||||
'Warning: High memory usage',
|
||||
'Error: Connection timeout',
|
||||
'Plugin loaded: file_tools',
|
||||
'Request processed in 0.245s'
|
||||
]
|
||||
};
|
||||
|
||||
let logsHtml = '';
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const type = logTypes[Math.floor(Math.random() * logTypes.length)];
|
||||
const messages = logMessages[currentLanguage];
|
||||
const message = messages[Math.floor(Math.random() * messages.length)];
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
logsHtml += `<div class="log-entry log-${type}">[${timestamp}] ${message}</div>`;
|
||||
}
|
||||
|
||||
logsContainer.innerHTML = logsHtml;
|
||||
}
|
||||
|
||||
// Update last updated timestamp
|
||||
function updateLastUpdated() {
|
||||
const now = new Date();
|
||||
document.getElementById('last-updated').textContent = now.toLocaleString();
|
||||
}
|
||||
|
||||
// Show alert
|
||||
function showAlert(message, type) {
|
||||
const container = document.getElementById('alert-container');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
|
||||
// Remove existing alerts
|
||||
container.innerHTML = '';
|
||||
container.appendChild(alert);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user