Compare commits

..
20 Commits
Author SHA1 Message Date
truetype 0612b290d5 Phase 19: Unified CLI entry point (maxclaw.py)
- Single 'maxclaw' command for all functionality
- Subcommands: chat, server, tui, config, analytics, dashboard, version
- Nested config subcommands: list, add, remove, use, active, test, export, import, search
- Updated README with unified CLI documentation
- Updated Dockerfile to use maxclaw.py
- Added .gitignore for __pycache__
2026-03-21 08:03:47 +01:00
truetype 3e986d83f1 Phase 18: Advanced analytics and enhanced documentation 2026-03-20 21:10:58 +01:00
truetype 1dae4c5656 Phase 17: Plugin marketplace and enhanced documentation 2026-03-20 20:55:34 +01:00
truetype cf8e4c677f Phase 16: Responsive web design improvements and enhanced documentation 2026-03-20 20:34:23 +01:00
truetype b3f57a8635 Phase 15: Multi-language support for monitoring dashboard 2026-03-20 20:19:31 +01:00
truetype 7103b218b9 Phase 14: Enhanced performance optimizations with connection pooling and lazy loading 2026-03-20 20:01:27 +01:00
truetype 979d19bdc1 Phase 13: Real-time monitoring web interface and enhanced API server 2026-03-20 19:46:41 +01:00
truetype e92cc701f6 Phase 12: Additional plugin examples (File Tools and System Utils) 2026-03-20 19:31:53 +01:00
truetype 6e1b1a44db Phase 11: Performance monitoring dashboard and enhanced documentation 2026-03-20 19:17:07 +01:00
truetype 8dbab53c6d Phase 10: Multi-language support (Italian and English) 2026-03-20 19:07:47 +01:00
truetype 98b6f10ea4 Phase 9: Plugin system for extensibility and advanced customization 2026-03-20 18:58:00 +01:00
truetype c54f2a33c8 Phase 8: Additional provider integrations (Anthropic, Google) and enhanced documentation 2026-03-20 18:47:02 +01:00
truetype c44a4ae92a Phase 7: Performance optimizations, monitoring, and command system with / prefix 2026-03-20 18:36:32 +01:00
truetype 6eace3abc0 Phase 6: Web UI for configuration management and enhanced API server 2026-03-20 18:25:22 +01:00
truetype 5541770896 Phase 5: Docker containerization and API server for remote access 2026-03-20 18:16:11 +01:00
truetype b17c180fbe Add comprehensive architecture and usage documentation in docs/ 2026-03-20 18:07:02 +01:00
truetype 5e70e22780 Phase 4: Shell completion scripts and comprehensive test suite 2026-03-20 17:54:46 +01:00
truetype aba2fc11f4 Phase 3: Unified provider tools system for all LLM providers 2026-03-20 17:45:03 +01:00
truetype 9f3298fa13 Phase 2: Enhanced TUI/CLI with search, export/import, and custom tools for Proxmox and Ollama 2026-03-20 16:08:26 +01:00
truetype f5026965f7 Add docs directory placeholder 2026-03-19 22:31:20 +01:00
40 changed files with 12156 additions and 35 deletions

No files matched your search

+77
View File
@@ -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/
+1
View File
@@ -0,0 +1 @@
__pycache__/
+36
View File
@@ -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"]
+97
View File
@@ -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.
+961 -11
View File
File diff suppressed because it is too large. Load diff
+494
View File
@@ -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
View File
@@ -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"
)
+88
View File
@@ -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
+79
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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())
+53
View File
@@ -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
View File
Whitespace-only changes.
+409
View File
@@ -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.
+208
View File
@@ -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)
+77
View File
@@ -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>"
}
+77
View File
@@ -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>"
}
+321 -9
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
"""
MaxClaw Agent Marketplace Package
Provides plugin marketplace functionality
"""
from .manager import MarketplaceManager, marketplace_manager
__all__ = [
'MarketplaceManager',
'marketplace_manager'
]
+275
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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())
+13
View File
@@ -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'
]
+80
View File
@@ -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
+137
View File
@@ -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}")
+441
View File
@@ -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}")
+302
View File
@@ -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()
+421
View File
@@ -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
View File
@@ -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
+186
View File
@@ -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"])
+256
View File
@@ -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"])
+33
View File
@@ -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'
]
+360
View File
@@ -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
+991
View File
@@ -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
+320
View File
@@ -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
View File
@@ -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>
+906
View File
@@ -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>
+829
View File
@@ -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>