Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda22811ce | ||
|
|
0612b290d5 | ||
|
|
3e986d83f1 | ||
|
|
1dae4c5656 | ||
|
|
cf8e4c677f | ||
|
|
b3f57a8635 | ||
|
|
7103b218b9 | ||
|
|
979d19bdc1 | ||
|
|
e92cc701f6 | ||
|
|
6e1b1a44db |
No files matched your search
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
# MaxClaw Agent Docker Image
|
||||
# LangCode Agent Docker Image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set working directory
|
||||
@@ -20,8 +20,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
|
||||
# Create directory for configuration
|
||||
RUN mkdir -p /root/.maxclaw && \
|
||||
chmod 700 /root/.maxclaw
|
||||
RUN mkdir -p /root/.langcode && \
|
||||
chmod 700 /root/.langcode
|
||||
|
||||
# Expose port for API (future use)
|
||||
EXPOSE 8000
|
||||
@@ -33,4 +33,4 @@ ENV MODEL_NAME=gemma2:9b
|
||||
ENV OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||||
|
||||
# Default command
|
||||
CMD ["python", "main.py"]
|
||||
CMD ["python", "langcode.py"]
|
||||
@@ -1,4 +1,4 @@
|
||||
# MaxClaw Agent
|
||||
# LangCode Agent
|
||||
|
||||
Un agente configurabile basato su DeepAgents che supporta più provider LLM.
|
||||
|
||||
@@ -15,11 +15,18 @@ Un agente configurabile basato su DeepAgents che supporta più provider LLM.
|
||||
- **Test completi**: Suite di test per garantire la qualità del codice
|
||||
- **Docker support**: Containerizzazione per deployment facile
|
||||
- **API REST**: Server API per accesso remoto
|
||||
- **Web UI**: Interfaccia web per gestione configurazioni
|
||||
- **Web UI responsive**: Interfaccia web per gestione configurazioni ottimizzata per mobile e desktop
|
||||
- **Dashboard monitoraggio**: Monitoraggio in tempo reale del sistema con supporto multilingue
|
||||
- **Marketplace plugin**: Scopri e installa plugin dal marketplace
|
||||
- **Analytics avanzate**: Analisi dettagliate dell'uso e raccomandazioni
|
||||
- **Comandi integrati**: Comandi speciali con prefisso "/" per gestione rapida
|
||||
- **Monitoraggio**: Sistema di logging e monitoraggio delle performance
|
||||
- **Dashboard performance**: Monitoraggio dettagliato delle performance
|
||||
- **Ottimizzazioni performance**: Caching, connection pooling, lazy loading
|
||||
- **Sistema di plugin**: Estendi le funzionalità con plugin personalizzati
|
||||
- **Supporto multilingue**: Interfaccia in italiano e inglese
|
||||
- **Plugin inclusi**: File tools, System utils, Example plugin
|
||||
- **Supporto multilingue**: Interfaccia in italiano e inglese per CLI, dashboard e web UI
|
||||
- **Design responsive**: Interfacce web ottimizzate per dispositivi mobili e desktop
|
||||
|
||||
## Installazione
|
||||
|
||||
@@ -35,7 +42,7 @@ Un agente configurabile basato su DeepAgents che supporta più provider LLM.
|
||||
|
||||
```bash
|
||||
# Build dell'immagine
|
||||
docker build -t maxclaw-agent .
|
||||
docker build -t langcode-agent .
|
||||
|
||||
# Esecuzione con Docker Compose
|
||||
docker-compose up -d
|
||||
@@ -45,7 +52,7 @@ docker run -it --rm \
|
||||
-e MODEL_PROVIDER=ollama \
|
||||
-e MODEL_NAME=gemma2:9b \
|
||||
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
|
||||
maxclaw-agent
|
||||
langcode-agent
|
||||
```
|
||||
|
||||
### Configurazione del Completamento Shell
|
||||
@@ -53,19 +60,19 @@ docker run -it --rm \
|
||||
#### Bash
|
||||
```bash
|
||||
# Aggiungi al tuo ~/.bashrc o ~/.bash_profile
|
||||
source /path/to/maxclaw_agent/completions/maxclaw.bash
|
||||
source /path/to/langcode_agent/completions/langcode.bash
|
||||
```
|
||||
|
||||
#### Zsh
|
||||
```bash
|
||||
# Aggiungi al tuo ~/.zshrc
|
||||
fpath=(/path/to/maxclaw_agent/completions $fpath)
|
||||
fpath=(/path/to/langcode_agent/completions $fpath)
|
||||
autoload -Uz compinit && compinit
|
||||
```
|
||||
|
||||
## Configurazione
|
||||
|
||||
Puoi configurare MaxClaw Agent in diversi modi:
|
||||
Puoi configurare LangCode Agent in diversi modi:
|
||||
|
||||
### 1. Variabili d'ambiente (Avvio Rapido)
|
||||
|
||||
@@ -178,6 +185,12 @@ python api_server.py
|
||||
|
||||
# Accedi all'interfaccia web
|
||||
# http://localhost:8000
|
||||
|
||||
# Accedi alla dashboard di monitoraggio
|
||||
# http://localhost:8000/monitoring
|
||||
|
||||
# Accedi al marketplace dei plugin
|
||||
# http://localhost:8000/marketplace
|
||||
```
|
||||
|
||||
L'interfaccia web include:
|
||||
@@ -185,12 +198,51 @@ L'interfaccia web include:
|
||||
- Chat con l'agente
|
||||
- Monitoraggio dello stato del sistema
|
||||
- Test di connessione
|
||||
- Dashboard di monitoraggio in tempo reale con supporto multilingue
|
||||
- **Design responsive**: Ottimizzata per dispositivi mobili e desktop
|
||||
- **Marketplace plugin**: Scopri e installa plugin dal marketplace
|
||||
|
||||
## Utilizzo
|
||||
|
||||
### Utilizzo Locale
|
||||
### CLI Unificata (langcode)
|
||||
|
||||
Esegui l'agente:
|
||||
Tutte le funzionalità sono accessibili tramite un unico comando `langcode`:
|
||||
|
||||
```bash
|
||||
# Avvia l'agent (chat interattiva)
|
||||
langcode
|
||||
langcode chat
|
||||
|
||||
# Avvia web server
|
||||
langcode server
|
||||
langcode server --port 3000
|
||||
|
||||
# Gestione configurazioni
|
||||
langcode config list # Lista configurazioni
|
||||
langcode config add -n ollama -p ollama -m gemma2:9b -u http://192.168.0.50:11434
|
||||
langcode config use ollama # Attiva configurazione
|
||||
langcode config active # Mostra configurazione attiva
|
||||
langcode config test # Testa connessione
|
||||
langcode config export -f json -o backup
|
||||
langcode config import backup.json
|
||||
langcode config search ollama # Cerca configurazioni
|
||||
|
||||
# TUI interattiva
|
||||
langcode tui
|
||||
|
||||
# Analytics e monitoraggio
|
||||
langcode analytics # Riepilogo rapido
|
||||
langcode analytics --report # Report completo
|
||||
langcode dashboard # Dashboard performance
|
||||
|
||||
# Info
|
||||
langcode version
|
||||
langcode --help
|
||||
```
|
||||
|
||||
### Utilizzo Locale (alternativo)
|
||||
|
||||
Esegui l'agente direttamente:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
@@ -209,7 +261,11 @@ Quando interagisci con l'agente, puoi usare comandi speciali preceduti da "/" pe
|
||||
/models - Elenca i modelli disponibili per il provider attivo
|
||||
/clear - Pulisce lo schermo
|
||||
/plugins - Elenca i plugin disponibili
|
||||
/marketplace - Apre il marketplace dei plugin
|
||||
/analytics - Mostra il report analytics
|
||||
/language - Cambia la lingua dell'interfaccia
|
||||
/performance - Mostra il report delle performance
|
||||
/monitoring - Apre la dashboard di monitoraggio
|
||||
/exit, /quit - Esci dall'agente
|
||||
```
|
||||
|
||||
@@ -227,10 +283,30 @@ Ollama URL: http://192.168.0.50:11434
|
||||
openai-gpt4: openai - gpt-4o
|
||||
|
||||
👤 You: Ciao, chi sei?
|
||||
🤖 MaxClaw: Ciao! Sono MaxClaw, un agente AI configurabile...
|
||||
🤖 LangCode: Ciao! Sono LangCode, un agente AI configurabile...
|
||||
|
||||
👤 You: Elenca i file nella directory corrente
|
||||
🤖 MaxClaw: [Esegue ls e mostra i file]
|
||||
🤖 LangCode: [Esegue ls e mostra i file]
|
||||
|
||||
👤 You: /analytics
|
||||
📊 Advanced Analytics Report (Last 30 days)
|
||||
============================================================
|
||||
|
||||
💬 Conversation Statistics:
|
||||
Total Conversations: 45
|
||||
Total Message Characters: 12,500
|
||||
Total Response Characters: 25,800
|
||||
Average Message Length: 278 chars
|
||||
Average Response Length: 573 chars
|
||||
|
||||
🤖 Provider Usage:
|
||||
ollama: 30 (66.7%)
|
||||
openai: 15 (33.3%)
|
||||
Most Used: ollama
|
||||
|
||||
💡 Insights and Recommendations:
|
||||
💡 You're heavily using ollama (67% of conversations). Consider diversifying with other providers for different use cases.
|
||||
✅ Great response times! Average is 0.8s.
|
||||
|
||||
👤 You: /exit
|
||||
👋 Goodbye!
|
||||
@@ -243,16 +319,18 @@ Ollama URL: http://192.168.0.50:11434
|
||||
docker-compose up -d
|
||||
|
||||
# Accedi al terminale
|
||||
docker exec -it maxclaw-agent python main.py
|
||||
docker exec -it langcode-agent langcode
|
||||
|
||||
# Oppure esegui direttamente
|
||||
docker run -it --rm maxclaw-agent
|
||||
docker run -it --rm langcode-agent
|
||||
```
|
||||
|
||||
### Utilizzo dell'API REST
|
||||
|
||||
Avvia il server API:
|
||||
```bash
|
||||
langcode server
|
||||
# oppure
|
||||
python api_server.py
|
||||
```
|
||||
|
||||
@@ -261,6 +339,8 @@ L'API sarà disponibile su http://localhost:8000
|
||||
#### Endpoint API Disponibili
|
||||
|
||||
- `GET /`: Interfaccia web
|
||||
- `GET /monitoring`: Dashboard di monitoraggio
|
||||
- `GET /marketplace`: Marketplace dei plugin
|
||||
- `GET /health`: Health check
|
||||
- `POST /chat`: Chat con l'agente
|
||||
- `GET /configurations`: Elenca tutte le configurazioni
|
||||
@@ -294,13 +374,295 @@ curl -X POST http://localhost:8000/configurations \
|
||||
}'
|
||||
```
|
||||
|
||||
## Analytics Avanzate
|
||||
|
||||
LangCode Agent include un sistema avanzato di analytics per monitorare l'uso e ottenere raccomandazioni.
|
||||
|
||||
### Report Analytics
|
||||
|
||||
Il report analytics include:
|
||||
|
||||
1. **Statistiche Conversazioni**:
|
||||
- Numero totale di conversazioni
|
||||
- Caratteri totali dei messaggi e risposte
|
||||
- Lunghezza media di messaggi e risposte
|
||||
|
||||
2. **Utilizzo Provider**:
|
||||
- Quale provider viene usato di più
|
||||
- Distribuzione percentuale dell'uso
|
||||
- Modello più utilizzato
|
||||
|
||||
3. **Metriche di Performance**:
|
||||
- Tempo di risposta medio
|
||||
- Tempo di risposta minimo e massimo
|
||||
- Tempo totale di risposta
|
||||
|
||||
4. **Utilizzo Strumenti**:
|
||||
- Quali strumenti vengono usati di più
|
||||
- Numero di utilizzi per strumento
|
||||
|
||||
5. **Utilizzo Comandi**:
|
||||
- Quali comandi vengono usati di più
|
||||
- Numero di utilizzi per comando
|
||||
|
||||
6. **Pattern di Errori**:
|
||||
- Errori ricorrenti
|
||||
- Pattern di errore più comuni
|
||||
|
||||
7. **Statistiche Sessioni**:
|
||||
- Numero totale di sessioni
|
||||
- Durata media delle sessioni
|
||||
- Messaggi medi per sessione
|
||||
|
||||
### Raccomandazioni
|
||||
|
||||
Il sistema genera automaticamente raccomandazioni basate sui pattern di utilizzo:
|
||||
|
||||
- **Diversificazione provider**: Se usi troppo un singolo provider
|
||||
- **Ottimizzazione performance**: Se i tempi di risposta sono lenti
|
||||
- **Utilizzo strumenti**: Se usi frequentemente certi strumenti
|
||||
- **Gestione errori**: Se ci sono errori ricorrenti
|
||||
- **Pattern di utilizzo**: Se l'uso è troppo basso o troppo alto
|
||||
|
||||
### Visualizzare le Analytics
|
||||
|
||||
```bash
|
||||
# Usa il comando /analytics
|
||||
/analytics
|
||||
|
||||
# Oppure esegui lo script direttamente
|
||||
python analytics.py
|
||||
```
|
||||
|
||||
### Configurazione
|
||||
|
||||
I dati di analytics vengono salvati in `~/.langcode/analytics/` e includono:
|
||||
- Conversazioni (truncati per privacy)
|
||||
- Utilizzo strumenti
|
||||
- Utilizzo provider
|
||||
- Utilizzo comandi
|
||||
- Pattern di errori
|
||||
- Sessioni utente
|
||||
|
||||
## Marketplace dei Plugin
|
||||
|
||||
LangCode Agent include un marketplace per scoprire e installare plugin.
|
||||
|
||||
### Accesso al Marketplace
|
||||
|
||||
```bash
|
||||
# Avvia il server API
|
||||
python api_server.py
|
||||
|
||||
# Accedi al marketplace
|
||||
# http://localhost:8000/marketplace
|
||||
```
|
||||
|
||||
### Funzionalità del Marketplace
|
||||
|
||||
Il marketplace include:
|
||||
|
||||
1. **Plugin Disponibili**:
|
||||
- Catalogo di plugin disponibili per l'installazione
|
||||
- Statistiche di download e valutazioni
|
||||
- Descrizioni dettagliate
|
||||
|
||||
2. **Plugin Installati**:
|
||||
- Elenco dei plugin attualmente installati
|
||||
- Versioni e informazioni
|
||||
- Opzioni di disinstallazione
|
||||
|
||||
3. **Ricerca Plugin**:
|
||||
- Ricerca per nome o descrizione
|
||||
- Filtraggio per categoria
|
||||
- Ordinamento per popolarità
|
||||
|
||||
4. **Gestione Plugin**:
|
||||
- Installazione con un click
|
||||
- Disinstallazione semplice
|
||||
- Aggiornamenti automatici
|
||||
|
||||
### Plugin Inclusi
|
||||
|
||||
1. **Example Plugin**: Plugin di esempio che dimostra come creare plugin
|
||||
- Strumenti: `example_hello`, `example_echo`, `example_math`
|
||||
- Comandi: `/example`, `/hello`, `/math`
|
||||
|
||||
2. **File Tools Plugin**: Strumenti aggiuntivi per la gestione dei file
|
||||
- Strumenti: `file_find`, `file_size`, `file_copy`, `file_move`, `file_delete`, `file_info`, `dir_list`, `dir_create`
|
||||
- Comandi: `/find`, `/size`, `/copy`, `/move`, `/delete`, `/info`, `/dir`, `/mkdir`
|
||||
|
||||
3. **System Utils Plugin**: Strumenti per informazioni di sistema
|
||||
- Strumenti: `system_info`, `system_uptime`, `system_ip`, `system_hostname`, `system_platform`, `system_users`, `system_processes`, `system_disk_usage`
|
||||
- Comandi: `/sysinfo`, `/uptime`, `/ip`, `/hostname`, `/platform`, `/users`, `/ps`, `/df`
|
||||
|
||||
### Installare Plugin dal Marketplace
|
||||
|
||||
```bash
|
||||
# Usa il comando /plugins per vedere i plugin disponibili
|
||||
/plugins
|
||||
|
||||
# Oppure usa l'interfaccia web
|
||||
# http://localhost:8000/marketplace
|
||||
```
|
||||
|
||||
### Creare un Plugin per il Marketplace
|
||||
|
||||
Per creare un plugin per il marketplace:
|
||||
|
||||
1. Crea un file Python seguendo la struttura dei plugin esistenti
|
||||
2. Testa il plugin localmente
|
||||
3. Sottometti il plugin al marketplace (futura implementazione)
|
||||
|
||||
## Dashboard di Monitoraggio
|
||||
|
||||
LangCode Agent include una dashboard di monitoraggio in tempo reale con supporto multilingue.
|
||||
|
||||
### Accesso alla Dashboard
|
||||
|
||||
```bash
|
||||
# Avvia il server API
|
||||
python api_server.py
|
||||
|
||||
# Accedi alla dashboard
|
||||
# http://localhost:8000/monitoring
|
||||
```
|
||||
|
||||
### Funzionalità della Dashboard
|
||||
|
||||
La dashboard include:
|
||||
|
||||
1. **Statistiche Generali**:
|
||||
- Stato del sistema (online/offline)
|
||||
- Uptime del sistema
|
||||
- Richieste totali
|
||||
- Errori totali
|
||||
- Tasso di errore
|
||||
|
||||
2. **Provider Attivo**:
|
||||
- Provider configurato
|
||||
- Modello in uso
|
||||
- Stato connessione
|
||||
- Chiamate al provider
|
||||
|
||||
3. **Utilizzo Strumenti**:
|
||||
- Strumenti caricati
|
||||
- Plugin attivi
|
||||
|
||||
4. **Risorse di Sistema**:
|
||||
- Utilizzo CPU
|
||||
- Utilizzo memoria
|
||||
- Utilizzo disco
|
||||
|
||||
5. **Performance**:
|
||||
- Tempo di risposta medio
|
||||
- Richieste al minuto
|
||||
- Picco richieste
|
||||
|
||||
6. **Logs**:
|
||||
- Logs recenti del sistema
|
||||
- Filtraggio per tipo (info, warning, error, success)
|
||||
|
||||
7. **Informazioni di Sistema**:
|
||||
- Hostname
|
||||
- Piattaforma
|
||||
- Architettura
|
||||
- Versione Python
|
||||
- Versione LangCode
|
||||
|
||||
### Supporto Multilingue della Dashboard
|
||||
|
||||
La dashboard supporta italiano e inglese:
|
||||
|
||||
- **Selettore lingua**: In alto a destra nella dashboard
|
||||
- **Persistenza**: La preferenza di lingua viene salvata nel browser
|
||||
- **Traduzione completa**: Tutti i testi della dashboard sono tradotti
|
||||
- **Aggiornamento in tempo reale**: La lingua cambia istantaneamente
|
||||
|
||||
### Aggiornamento Automatico
|
||||
|
||||
La dashboard si aggiorna automaticamente ogni 30 secondi. Puoi anche aggiornare manualmente con il pulsante "🔄 Aggiorna Dati" (o "🔄 Refresh Data" in inglese).
|
||||
|
||||
## Interfaccia Web Responsive
|
||||
|
||||
L'interfaccia web è stata ottimizzata per essere responsive e funzionare su dispositivi mobili e desktop.
|
||||
|
||||
### Caratteristiche Responsive
|
||||
|
||||
1. **Layout adattivo**: Si adatta automaticamente a diverse dimensioni dello schermo
|
||||
2. **Touch-friendly**: Pulsanti e campi ottimizzati per touch screen
|
||||
3. **Navigazione mobile**: Menu e tab ottimizzati per dispositivi mobili
|
||||
4. **Dimensioni appropriate**: Font e spaziature adatti a schermi piccoli
|
||||
5. **Supporto multilingue**: Interfaccia in italiano e inglese
|
||||
|
||||
### Accesso da Dispositivi Mobili
|
||||
|
||||
L'interfaccia web è accessibile da qualsiasi dispositivo nella stessa rete:
|
||||
|
||||
```bash
|
||||
# Avvia il server API
|
||||
python api_server.py
|
||||
|
||||
# Accedi da un dispositivo mobile nella stessa rete
|
||||
# http://<IP_DEL_SERVER>:8000
|
||||
```
|
||||
|
||||
Esempio:
|
||||
```bash
|
||||
# Se il server è su 192.168.0.50
|
||||
# http://192.168.0.50:8000
|
||||
```
|
||||
|
||||
### Ottimizzazioni per Mobile
|
||||
|
||||
- **Pulsanti più grandi**: Facili da toccare su schermi touch
|
||||
- **Layout a colonna singola**: Su schermi stretti
|
||||
- **Font leggibili**: Dimensioni appropriate per la lettura su mobile
|
||||
- **Navigazione semplificata**: Tab e menu ottimizzati per touch
|
||||
- **Caricamento rapido**: Ottimizzazioni per connessioni lente
|
||||
|
||||
## Ottimizzazioni Performance
|
||||
|
||||
LangCode Agent include diverse ottimizzazioni per migliorare le performance:
|
||||
|
||||
### Caching
|
||||
- **Configurazioni**: Le configurazioni vengono cachate per 30 secondi
|
||||
- **Provider tools**: Gli strumenti provider vengono cachati per 5 minuti
|
||||
- **Creazione agente**: L'agente viene cachato per 10 minuti
|
||||
- **Risposte API**: Le risposte API vengono cachate per 10 secondi
|
||||
|
||||
### Connection Pooling
|
||||
- **Pool di connessioni**: Gestisce le connessioni ai provider
|
||||
- **Riutilizzo connessioni**: Riutilizza le connessioni esistenti
|
||||
- **Pulizia automatica**: Rimuove le connessioni inattive dopo 5 minuti
|
||||
|
||||
### Lazy Loading
|
||||
- **Caricamento moduli**: I moduli vengono caricati solo quando necessari
|
||||
- **Caricamento strumenti**: Gli strumenti vengono caricati on-demand
|
||||
- **Risorse pesanti**: Le risorse pesanti vengono caricate in modo lazy
|
||||
|
||||
### Batch Processing
|
||||
- **Elaborazione parallela**: Le operazioni vengono eseguite in parallelo
|
||||
- **Thread pool**: Utilizza un pool di thread per le operazioni concorrenti
|
||||
- **Timeout configurabile**: Timeout configurabile per le operazioni batch
|
||||
|
||||
### Applicare le Ottimizzazioni
|
||||
|
||||
```bash
|
||||
# Applica tutte le ottimizzazioni
|
||||
python performance.py
|
||||
|
||||
# Visualizza il report delle performance
|
||||
python performance.py
|
||||
```
|
||||
|
||||
## Strumenti Unificati per Provider
|
||||
|
||||
MaxClaw Agent ora fornisce un'interfaccia unificata per gestire qualsiasi provider LLM. Una volta configurato un provider, puoi usare gli stessi comandi per tutti.
|
||||
LangCode Agent ora fornisce un'interfaccia unificata per gestire qualsiasi provider LLM. Una volta configurato un provider, puoi usare gli stessi comandi per tutti.
|
||||
|
||||
### Provider Supportati
|
||||
|
||||
MaxClaw Agent supporta i seguenti provider:
|
||||
LangCode Agent supporta i seguenti provider:
|
||||
|
||||
1. **Ollama** - Modelli locali
|
||||
2. **OpenAI** - GPT-4o, GPT-4o-mini, GPT-4-turbo, ecc.
|
||||
@@ -376,7 +738,7 @@ export GOOGLE_API_KEY="AIza..."
|
||||
|
||||
## Strumenti Personalizzati
|
||||
|
||||
MaxClaw Agent include anche strumenti personalizzati per gestire il tuo ambiente:
|
||||
LangCode Agent include anche strumenti personalizzati per gestire il tuo ambiente:
|
||||
|
||||
### Strumenti Proxmox
|
||||
Configura le variabili d'ambiente per abilitare gli strumenti Proxmox:
|
||||
@@ -408,11 +770,21 @@ Gli strumenti Ollama specifici sono ancora disponibili per retrocompatibilità:
|
||||
|
||||
## Sistema di Plugin
|
||||
|
||||
MaxClaw Agent include un sistema di plugin per estendere le funzionalità.
|
||||
LangCode Agent include un sistema di plugin per estendere le funzionalità.
|
||||
|
||||
### Plugin Inclusi
|
||||
|
||||
- **Example Plugin**: Plugin di esempio che dimostra come creare plugin
|
||||
1. **Example Plugin**: Plugin di esempio che dimostra come creare plugin
|
||||
- Strumenti: `example_hello`, `example_echo`, `example_math`
|
||||
- Comandi: `/example`, `/hello`, `/math`
|
||||
|
||||
2. **File Tools Plugin**: Strumenti aggiuntivi per la gestione dei file
|
||||
- Strumenti: `file_find`, `file_size`, `file_copy`, `file_move`, `file_delete`, `file_info`, `dir_list`, `dir_create`
|
||||
- Comandi: `/find`, `/size`, `/copy`, `/move`, `/delete`, `/info`, `/dir`, `/mkdir`
|
||||
|
||||
3. **System Utils Plugin**: Strumenti per informazioni di sistema
|
||||
- Strumenti: `system_info`, `system_uptime`, `system_ip`, `system_hostname`, `system_platform`, `system_users`, `system_processes`, `system_disk_usage`
|
||||
- Comandi: `/sysinfo`, `/uptime`, `/ip`, `/hostname`, `/platform`, `/users`, `/ps`, `/df`
|
||||
|
||||
### Creare un Plugin
|
||||
|
||||
@@ -467,9 +839,72 @@ python -c "from plugins.manager import plugin_manager; plugin_manager.load_all_p
|
||||
/mycommand
|
||||
```
|
||||
|
||||
### Esempi di Utilizzo dei Plugin
|
||||
|
||||
#### File Tools Plugin
|
||||
```bash
|
||||
# Trova file per nome
|
||||
"Find all .py files in the current directory"
|
||||
# oppure
|
||||
/find *.py
|
||||
|
||||
# Ottieni dimensione di un file
|
||||
"Get the size of main.py"
|
||||
# oppure
|
||||
/size main.py
|
||||
|
||||
# Copia un file
|
||||
"Copy config.json to backup_config.json"
|
||||
# oppure
|
||||
/copy config.json backup_config.json
|
||||
|
||||
# Elenca directory con dettagli
|
||||
"List files in the current directory with details"
|
||||
# oppure
|
||||
/dir
|
||||
```
|
||||
|
||||
#### System Utils Plugin
|
||||
```bash
|
||||
# Ottieni informazioni di sistema
|
||||
"Show system information"
|
||||
# oppure
|
||||
/sysinfo
|
||||
|
||||
# Ottieni uptime del sistema
|
||||
"How long has the system been running?"
|
||||
# oppure
|
||||
/uptime
|
||||
|
||||
# Ottieni indirizzo IP
|
||||
"What is my IP address?"
|
||||
# oppure
|
||||
/ip
|
||||
|
||||
# Ottieni informazioni sulla piattaforma
|
||||
"What platform am I running on?"
|
||||
# oppure
|
||||
/platform
|
||||
|
||||
# Elenca utenti loggati
|
||||
"Who is logged in?"
|
||||
# oppure
|
||||
/users
|
||||
|
||||
# Elenca processi in esecuzione
|
||||
"Show running processes"
|
||||
# oppure
|
||||
/ps
|
||||
|
||||
# Mostra utilizzo disco
|
||||
"Show disk usage"
|
||||
# oppure
|
||||
/df
|
||||
```
|
||||
|
||||
## Supporto Multilingue
|
||||
|
||||
MaxClaw Agent supporta più lingue per l'interfaccia utente.
|
||||
LangCode Agent supporta più lingue per l'interfaccia utente.
|
||||
|
||||
### Lingue Disponibili
|
||||
|
||||
@@ -480,7 +915,7 @@ MaxClaw Agent supporta più lingue per l'interfaccia utente.
|
||||
|
||||
```bash
|
||||
# Usa la variabile d'ambiente
|
||||
export MAXCLAW_LANGUAGE=en
|
||||
export LANGCODE_LANGUAGE=en
|
||||
|
||||
# Oppure usa il comando /language
|
||||
/language en
|
||||
@@ -495,6 +930,65 @@ Per aggiungere una nuova lingua:
|
||||
3. Traduci tutti i valori
|
||||
4. L'agente rileverà automaticamente la nuova lingua
|
||||
|
||||
### Dashboard Multilingue
|
||||
|
||||
La dashboard di monitoraggio supporta anche il cambio lingua:
|
||||
|
||||
- **Selettore lingua**: In alto a destra nella dashboard
|
||||
- **Persistenza**: La preferenza viene salvata nel browser
|
||||
- **Traduzione completa**: Tutti i testi sono tradotti in italiano e inglese
|
||||
|
||||
### Web UI Multilingue
|
||||
|
||||
L'interfaccia web supporta il cambio lingua:
|
||||
|
||||
- **Selettore lingua**: In alto a destra nell'interfaccia
|
||||
- **Persistenza**: La preferenza viene salvata nel browser
|
||||
- **Traduzione completa**: Tutti i testi sono tradotti in italiano e inglese
|
||||
|
||||
## Monitoraggio Performance
|
||||
|
||||
LangCode Agent include un sistema avanzato di monitoraggio delle performance.
|
||||
|
||||
### Dashboard Performance
|
||||
|
||||
Il dashboard raccoglie e analizza:
|
||||
- Richieste API e tempi di risposta
|
||||
- Utilizzo degli strumenti
|
||||
- Utilizzo dei provider
|
||||
- Statistiche di sistema (CPU, memoria, disco)
|
||||
- Errori e tassi di successo
|
||||
|
||||
### Visualizzare le Performance
|
||||
|
||||
```bash
|
||||
# Usa il comando /performance
|
||||
/performance
|
||||
|
||||
# Oppure esegui lo script direttamente
|
||||
python dashboard.py
|
||||
```
|
||||
|
||||
### Report Disponibili
|
||||
|
||||
Il report include:
|
||||
- Statistiche richieste (totale, errori, tasso di errore)
|
||||
- Distribuzione codici di stato
|
||||
- Endpoint più utilizzati
|
||||
- Utilizzo degli strumenti (chiamate, tasso di successo, durata media)
|
||||
- Utilizzo dei provider
|
||||
- Statistiche di sistema
|
||||
|
||||
### Configurazione
|
||||
|
||||
I dati di performance vengono salvati in `~/.langcode/performance/` e includono:
|
||||
- Richieste API
|
||||
- Errori
|
||||
- Tempi di risposta
|
||||
- Utilizzo strumenti
|
||||
- Utilizzo provider
|
||||
- Statistiche di sistema
|
||||
|
||||
## Test
|
||||
|
||||
Esegui la suite di test:
|
||||
@@ -514,7 +1008,7 @@ pytest tests/ -v
|
||||
|
||||
### Build dell'Immagine
|
||||
```bash
|
||||
docker build -t maxclaw-agent .
|
||||
docker build -t langcode-agent .
|
||||
```
|
||||
|
||||
### Esecuzione con Docker Compose
|
||||
@@ -545,31 +1039,36 @@ docker run -it --rm \
|
||||
-e PROXMOX_HOST=192.168.0.100 \
|
||||
-e PROXMOX_USER=root \
|
||||
-e PROXMOX_PASSWORD=password \
|
||||
-e MAXCLAW_LANGUAGE=it \
|
||||
maxclaw-agent
|
||||
-e LANGCODE_LANGUAGE=it \
|
||||
langcode-agent
|
||||
```
|
||||
|
||||
## Personalizzazione
|
||||
|
||||
Per personalizzare l'agente, modifica `main.py`:
|
||||
|
||||
- Cambia il prompt di sistema predefinito in `create_maxclaw_agent`
|
||||
- Cambia il prompt di sistema predefinito in `create_langcode_agent`
|
||||
- Aggiungi strumenti personalizzati alla chiamata `create_deep_agent`
|
||||
- Configura i sub-agenti o altre funzionalità di DeepAgents
|
||||
- Crea plugin per estendere le funzionalità
|
||||
|
||||
## Monitoraggio e Logging
|
||||
|
||||
MaxClaw Agent include un sistema di monitoraggio e logging:
|
||||
LangCode Agent include un sistema di monitoraggio e logging:
|
||||
|
||||
- **Log dettagliati**: Tutte le azioni vengono registrate in `~/.maxclaw/logs/`
|
||||
- **Log dettagliati**: Tutte le azioni vengono registrate in `~/.langcode/logs/`
|
||||
- **Statistiche di sistema**: Monitoraggio di uptime, richieste, errori
|
||||
- **Metriche di performance**: Tempi di risposta, utilizzo cache
|
||||
- **Report di stato**: Visualizzazione completa dello stato del sistema
|
||||
- **Dashboard performance**: Monitoraggio avanzato delle performance
|
||||
- **Dashboard web**: Monitoraggio in tempo reale via interfaccia web con supporto multilingue
|
||||
- **Interfacce responsive**: Ottimizzate per dispositivi mobili e desktop
|
||||
- **Marketplace plugin**: Scopri e installa plugin dal marketplace
|
||||
- **Analytics avanzate**: Analisi dettagliate dell'uso e raccomandazioni
|
||||
|
||||
## Sicurezza
|
||||
|
||||
MaxClaw Agent implementa misure di sicurezza avanzate:
|
||||
LangCode Agent implementa misure di sicurezza avanzate:
|
||||
|
||||
- **Crittografia a riposo**: Tutte le informazioni sensibili (chiavi API) sono crittografate usando la crittografia simmetrica Fernet
|
||||
- **Permessi di file sicuri**: La directory di configurazione e i file sono impostati con accesso solo al proprietario (700 per le directory, 600 per i file)
|
||||
|
||||
Binary file not shown.
+494
@@ -0,0 +1,494 @@
|
||||
"""
|
||||
Advanced analytics for LangCode 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 LangCode 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() / ".langcode" / "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 LangCode 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}")
|
||||
+38
-11
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
MaxClaw Agent API Server
|
||||
Provides REST API for remote access to MaxClaw Agent
|
||||
LangCode Agent API Server
|
||||
Provides REST API for remote access to LangCode Agent
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
@@ -16,13 +16,13 @@ import uvicorn
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from config_manager import config_manager
|
||||
from main import create_maxclaw_agent
|
||||
from main import create_langcode_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",
|
||||
title="LangCode Agent API",
|
||||
description="REST API for LangCode Agent - configurable AI agent",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
@@ -83,7 +83,7 @@ def get_agent():
|
||||
global agent
|
||||
if agent is None:
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
agent = create_langcode_agent()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create agent: {str(e)}")
|
||||
return agent
|
||||
@@ -94,8 +94,8 @@ async def startup_event():
|
||||
"""Initialize agent on startup"""
|
||||
global agent
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
print("✅ MaxClaw Agent API server started")
|
||||
agent = create_langcode_agent()
|
||||
print("✅ LangCode Agent API server started")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to initialize agent: {e}")
|
||||
|
||||
@@ -108,17 +108,32 @@ async def root():
|
||||
return FileResponse(web_index)
|
||||
else:
|
||||
return {
|
||||
"message": "MaxClaw Agent API",
|
||||
"message": "LangCode Agent API",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"chat": "/chat",
|
||||
"configurations": "/configurations",
|
||||
"health": "/health",
|
||||
"web_ui": "/static/index.html"
|
||||
"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"""
|
||||
@@ -127,7 +142,7 @@ async def health_check():
|
||||
|
||||
@app.post("/chat", response_model=ChatResponse)
|
||||
async def chat(request: ChatRequest):
|
||||
"""Chat with MaxClaw Agent"""
|
||||
"""Chat with LangCode Agent"""
|
||||
try:
|
||||
agent_instance = get_agent()
|
||||
|
||||
@@ -283,6 +298,18 @@ async def list_providers():
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File renamed without changes.
File renamed without changes.
+3
-3
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Command-line interface for managing MaxClaw Agent configurations
|
||||
Command-line interface for managing LangCode Agent configurations
|
||||
Enhanced with export/import, search, and better formatting
|
||||
"""
|
||||
import sys
|
||||
@@ -11,7 +11,7 @@ from typing import Optional, Dict, Any
|
||||
from config_manager import config_manager
|
||||
|
||||
|
||||
def print_configurations(configs=None, title="MaxClaw Agent Configurations"):
|
||||
def print_configurations(configs=None, title="LangCode Agent Configurations"):
|
||||
"""Print all configurations in a formatted way"""
|
||||
if configs is None:
|
||||
configs = config_manager.get_configurations()
|
||||
@@ -405,7 +405,7 @@ def import_configurations():
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point"""
|
||||
parser = argparse.ArgumentParser(description="MaxClaw Agent Configuration Manager")
|
||||
parser = argparse.ArgumentParser(description="LangCode Agent Configuration Manager")
|
||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
||||
|
||||
# List command
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Configuration manager for MaxClaw Agent
|
||||
Configuration manager for LangCode Agent
|
||||
Handles persistent storage of provider configurations with encryption and validation
|
||||
"""
|
||||
import json
|
||||
@@ -17,17 +17,17 @@ except ImportError:
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""Manages MaxClaw Agent configuration with encryption and validation"""
|
||||
"""Manages LangCode Agent configuration with encryption and validation"""
|
||||
|
||||
def __init__(self, config_dir: Optional[str] = None):
|
||||
"""
|
||||
Initialize the config manager
|
||||
|
||||
Args:
|
||||
config_dir: Directory to store config files. If None, uses ~/.maxclaw
|
||||
config_dir: Directory to store config files. If None, uses ~/.langcode
|
||||
"""
|
||||
if config_dir is None:
|
||||
self.config_dir = Path.home() / ".maxclaw"
|
||||
self.config_dir = Path.home() / ".langcode"
|
||||
else:
|
||||
self.config_dir = Path(config_dir)
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Text User Interface for MaxClaw Agent configuration management
|
||||
Text User Interface for LangCode Agent configuration management
|
||||
Enhanced with search/filter, export/import, and connection testing
|
||||
"""
|
||||
import curses
|
||||
@@ -11,7 +11,7 @@ from typing import Optional, List, Dict, Any
|
||||
from config_manager import config_manager
|
||||
|
||||
|
||||
def draw_menu(stdscr, selected_row_idx, options, title="MaxClaw Agent Configuration TUI"):
|
||||
def draw_menu(stdscr, selected_row_idx, options, title="LangCode Agent Configuration TUI"):
|
||||
"""Draw a menu with options"""
|
||||
stdscr.clear()
|
||||
h, w = stdscr.getmaxyx()
|
||||
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
"""
|
||||
Performance monitoring dashboard for LangCode 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() / ".langcode" / "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())
|
||||
+7
-7
@@ -1,13 +1,13 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
maxclaw:
|
||||
langcode:
|
||||
build: .
|
||||
container_name: maxclaw-agent
|
||||
container_name: langcode-agent
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# Mount configuration directory for persistence
|
||||
- maxclaw-config:/root/.maxclaw
|
||||
- langcode-config:/root/.langcode
|
||||
# Mount current directory for development (optional)
|
||||
# - .:/app
|
||||
environment:
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
- PROXMOX_PASSWORD=${PROXMOX_PASSWORD:-}
|
||||
- PROXMOX_TOKEN=${PROXMOX_TOKEN:-}
|
||||
networks:
|
||||
- maxclaw-network
|
||||
- langcode-network
|
||||
# Interactive mode for terminal usage
|
||||
stdin_open: true
|
||||
tty: true
|
||||
@@ -40,14 +40,14 @@ services:
|
||||
# ports:
|
||||
# - "11434:11434"
|
||||
# networks:
|
||||
# - maxclaw-network
|
||||
# - langcode-network
|
||||
|
||||
volumes:
|
||||
maxclaw-config:
|
||||
langcode-config:
|
||||
driver: local
|
||||
# ollama-data:
|
||||
# driver: local
|
||||
|
||||
networks:
|
||||
maxclaw-network:
|
||||
langcode-network:
|
||||
driver: bridge
|
||||
@@ -1,8 +1,8 @@
|
||||
# MaxClaw Agent: Architettura e Guida all'Utilizzo
|
||||
# LangCode Agent: Architettura e Guida all'Utilizzo
|
||||
|
||||
## Cos'è MaxClaw Agent?
|
||||
## Cos'è LangCode 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ò:
|
||||
LangCode 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
|
||||
@@ -16,7 +16,7 @@ MaxClaw Agent è un **agente AI intelligente** che può essere configurato per l
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ MaxClaw Agent │
|
||||
│ LangCode Agent │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. Agente Principale (main.py) │
|
||||
│ - Coordina tutto il sistema │
|
||||
@@ -327,13 +327,13 @@ 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?
|
||||
🤖 LangCode: Sono LangCode, 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]
|
||||
🤖 LangCode: [Esegue ls e mostra i file]
|
||||
|
||||
👤 Tu: Crea un file di testo con "Hello World"
|
||||
🤖 MaxClaw: [Crea hello.txt con il contenuto specificato]
|
||||
🤖 LangCode: [Crea hello.txt con il contenuto specificato]
|
||||
```
|
||||
|
||||
### 3. Configurazione Avanzata
|
||||
@@ -391,8 +391,8 @@ 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
|
||||
ls -la ~/.langcode/
|
||||
chmod 600 ~/.langcode/config.json
|
||||
```
|
||||
|
||||
## Prossimi Passi
|
||||
@@ -404,6 +404,6 @@ chmod 600 ~/.maxclaw/config.json
|
||||
|
||||
## 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.
|
||||
LangCode 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.
|
||||
+4
-4
@@ -7,7 +7,7 @@ import os
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
from main import create_maxclaw_agent
|
||||
from main import create_langcode_agent
|
||||
|
||||
def example_with_openai():
|
||||
"""Example using OpenAI"""
|
||||
@@ -18,7 +18,7 @@ def example_with_openai():
|
||||
# os.environ["OPENAI_API_KEY"] = "your-api-key"
|
||||
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
agent = create_langcode_agent()
|
||||
print("✅ OpenAI agent created")
|
||||
# Uncomment to test (requires API key)
|
||||
# result = agent.invoke({"messages": [{"role": "user", "content": "Say hello in Italian"}]})
|
||||
@@ -35,7 +35,7 @@ def example_with_ollama():
|
||||
os.environ["OLLAMA_BASE_URL"] = "https://ollama.solarpi.net" # From user's profile
|
||||
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
agent = create_langcode_agent()
|
||||
print("✅ Ollama agent created")
|
||||
# Uncomment to test (requires Ollama server)
|
||||
# result = agent.invoke({"messages": [{"role": "user", "content": "Say hello in Italian"}]})
|
||||
@@ -53,7 +53,7 @@ def example_with_openrouter():
|
||||
# os.environ["OPENROUTER_API_KEY"] = "your-api-key"
|
||||
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
agent = create_langcode_agent()
|
||||
print("✅ OpenRouter agent created")
|
||||
# Uncomment to test (requires API key)
|
||||
# result = agent.invoke({"messages": [{"role": "user", "content": "Say hello in Italian"}]})
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Internationalization (i18n) support for MaxClaw Agent
|
||||
Internationalization (i18n) support for LangCode Agent
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
@@ -8,7 +8,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
class I18n:
|
||||
"""Internationalization manager for MaxClaw Agent"""
|
||||
"""Internationalization manager for LangCode Agent"""
|
||||
|
||||
def __init__(self, default_language: str = "en"):
|
||||
"""
|
||||
@@ -31,7 +31,7 @@ class I18n:
|
||||
self._load_language(self.default_language)
|
||||
|
||||
# Try to load current language from environment
|
||||
env_lang = os.getenv("MAXCLAW_LANGUAGE")
|
||||
env_lang = os.getenv("LANGCODE_LANGUAGE")
|
||||
if env_lang:
|
||||
self.set_language(env_lang)
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"app.name": "MaxClaw Agent",
|
||||
"app.name": "LangCode Agent",
|
||||
"app.description": "A configurable agent harness using DeepAgents",
|
||||
|
||||
"commands.help": "Show available commands",
|
||||
@@ -52,7 +52,7 @@
|
||||
"plugins.disabled": "Disabled",
|
||||
"plugins.no_plugins": "No plugins found.",
|
||||
|
||||
"agent.initializing": "Initializing MaxClaw Agent...",
|
||||
"agent.initializing": "Initializing LangCode Agent...",
|
||||
"agent.initialized": "Agent initialized successfully!",
|
||||
"agent.tools.available": "Available tools:",
|
||||
"agent.tools.provider": "Provider tools: list models, test connections, set defaults",
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"app.name": "MaxClaw Agent",
|
||||
"app.name": "LangCode Agent",
|
||||
"app.description": "Un agente configurabile basato su DeepAgents",
|
||||
|
||||
"commands.help": "Mostra i comandi disponibili",
|
||||
@@ -52,7 +52,7 @@
|
||||
"plugins.disabled": "Disabilitato",
|
||||
"plugins.no_plugins": "Nessun plugin trovato.",
|
||||
|
||||
"agent.initializing": "Inizializzazione MaxClaw Agent...",
|
||||
"agent.initializing": "Inizializzazione LangCode Agent...",
|
||||
"agent.initialized": "Agente inizializzato con successo!",
|
||||
"agent.tools.available": "Strumenti disponibili:",
|
||||
"agent.tools.provider": "Strumenti provider: elenca modelli, testa connessioni, imposta predefiniti",
|
||||
|
||||
Executable
+551
@@ -0,0 +1,551 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
LangCode Agent - Unified CLI Entry Point
|
||||
Usage:
|
||||
langcode → avvia l'agent (chat interattiva)
|
||||
langcode chat → avvia l'agent
|
||||
langcode server [--port N] → avvia web server
|
||||
langcode tui → apre TUI configurazione
|
||||
langcode config list → lista configurazioni
|
||||
langcode config add → aggiungi configurazione
|
||||
langcode config remove NAME → rimuovi configurazione
|
||||
langcode config use NAME → attiva configurazione
|
||||
langcode config active → mostra configurazione attiva
|
||||
langcode config test [NAME] → testa connessione
|
||||
langcode config export → esporta configurazioni
|
||||
langcode config import FILE → importa configurazioni
|
||||
langcode config search Q → cerca configurazioni
|
||||
langcode analytics → mostra analytics
|
||||
langcode dashboard → mostra dashboard performance
|
||||
langcode 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_langcode_agent, process_command
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
print("🚀 Inizializzo LangCode Agent...")
|
||||
|
||||
try:
|
||||
agent = create_langcode_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🤖 LangCode: {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 LangCode 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📊 LangCode 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 'langcode 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📈 LangCode 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"LangCode Agent v{VERSION}")
|
||||
print("Agent harness basato su LangChain Deep Agents")
|
||||
print("https://github.com/truetype/langcode_agent")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="langcode",
|
||||
description="🚀 LangCode Agent - AI Agent harness configurabile",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Esempi:
|
||||
langcode Avvia l'agent (chat interattiva)
|
||||
langcode server --port 3000 Avvia web server su porta 3000
|
||||
langcode config list Lista tutte le configurazioni
|
||||
langcode config add -n ollama -p ollama -m gemma2:9b
|
||||
langcode config use ollama Attiva configurazione 'ollama'
|
||||
langcode config test Testa configurazione attiva
|
||||
langcode analytics Mostra riepilogo analytics
|
||||
langcode analytics --report Genera report completo
|
||||
"""
|
||||
)
|
||||
parser.add_argument("--version", "-v", action="version", version=f"langcode {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()
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MaxClaw Agent - A configurable agent harness using DeepAgents
|
||||
LangCode 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
|
||||
"""
|
||||
@@ -74,7 +74,7 @@ def load_model_from_config():
|
||||
|
||||
def get_custom_tools() -> List[Tool]:
|
||||
"""
|
||||
Get custom tools for the MaxClaw agent
|
||||
Get custom tools for the LangCode agent
|
||||
|
||||
Returns:
|
||||
List of custom tools
|
||||
@@ -114,12 +114,12 @@ def get_custom_tools() -> List[Tool]:
|
||||
return custom_tools
|
||||
|
||||
|
||||
def create_maxclaw_agent(system_prompt: Optional[str] = None):
|
||||
def create_langcode_agent(system_prompt: Optional[str] = None):
|
||||
"""
|
||||
Create and return a DeepAgent instance with configurable model.
|
||||
"""
|
||||
if system_prompt is None:
|
||||
system_prompt = """You are MaxClaw, a helpful AI agent that can plan, use tools,
|
||||
system_prompt = """You are LangCode, 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.
|
||||
|
||||
@@ -371,12 +371,12 @@ def process_command(user_input: str) -> bool:
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for the MaxClaw agent.
|
||||
Main entry point for the LangCode agent.
|
||||
"""
|
||||
print("🚀 Initializing MaxClaw Agent...")
|
||||
print("🚀 Initializing LangCode Agent...")
|
||||
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
agent = create_langcode_agent()
|
||||
print("✅ Agent initialized successfully!")
|
||||
|
||||
# Show available tools
|
||||
@@ -408,7 +408,7 @@ def main():
|
||||
result = agent.invoke({"messages": [HumanMessage(content=user_input)]})
|
||||
# Extract the AI's response
|
||||
ai_message = result["messages"][-1]
|
||||
print(f"\n🤖 MaxClaw: {ai_message.content}")
|
||||
print(f"\n🤖 LangCode: {ai_message.content}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {str(e)}")
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
LangCode Agent Marketplace Package
|
||||
Provides plugin marketplace functionality
|
||||
"""
|
||||
|
||||
from .manager import MarketplaceManager, marketplace_manager
|
||||
|
||||
__all__ = [
|
||||
'MarketplaceManager',
|
||||
'marketplace_manager'
|
||||
]
|
||||
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Plugin marketplace manager for LangCode 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 LangCode Agent"""
|
||||
|
||||
def __init__(self, marketplace_url: str = "https://api.github.com/repos/langcode-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() / ".langcode" / "plugins"
|
||||
self.marketplace_cache_file = Path.home() / ".langcode" / "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()
|
||||
+12
-12
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Monitoring and logging for MaxClaw Agent
|
||||
Monitoring and logging for LangCode Agent
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
@@ -14,18 +14,18 @@ from pathlib import Path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
class MaxClawLogger:
|
||||
"""Custom logger for MaxClaw Agent"""
|
||||
class LangCodeLogger:
|
||||
"""Custom logger for LangCode Agent"""
|
||||
|
||||
def __init__(self, log_dir: Optional[str] = None):
|
||||
"""
|
||||
Initialize logger
|
||||
|
||||
Args:
|
||||
log_dir: Directory for log files. If None, uses ~/.maxclaw/logs
|
||||
log_dir: Directory for log files. If None, uses ~/.langcode/logs
|
||||
"""
|
||||
if log_dir is None:
|
||||
self.log_dir = Path.home() / ".maxclaw" / "logs"
|
||||
self.log_dir = Path.home() / ".langcode" / "logs"
|
||||
else:
|
||||
self.log_dir = Path(log_dir)
|
||||
|
||||
@@ -46,7 +46,7 @@ class MaxClawLogger:
|
||||
)
|
||||
|
||||
# Create file handler
|
||||
log_file = self.log_dir / f"maxclaw_{datetime.now().strftime('%Y%m%d')}.log"
|
||||
log_file = self.log_dir / f"langcode_{datetime.now().strftime('%Y%m%d')}.log"
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
@@ -63,10 +63,10 @@ class MaxClawLogger:
|
||||
)
|
||||
|
||||
# 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")
|
||||
self.agent_logger = logging.getLogger("langcode.agent")
|
||||
self.config_logger = logging.getLogger("langcode.config")
|
||||
self.api_logger = logging.getLogger("langcode.api")
|
||||
self.tools_logger = logging.getLogger("langcode.tools")
|
||||
|
||||
print(f"✅ Logging initialized. Logs will be saved to: {self.log_dir}")
|
||||
|
||||
@@ -187,7 +187,7 @@ class SystemMonitor:
|
||||
|
||||
|
||||
# Global instances
|
||||
logger = MaxClawLogger()
|
||||
logger = LangCodeLogger()
|
||||
system_monitor = SystemMonitor()
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ except ImportError:
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test logging
|
||||
print("Testing MaxClaw logging system...")
|
||||
print("Testing LangCode logging system...")
|
||||
|
||||
# Log some test messages
|
||||
logger.log_agent_action("test_action", {"test": "data"})
|
||||
|
||||
+222
-8
@@ -1,18 +1,209 @@
|
||||
"""
|
||||
Performance optimizations for MaxClaw Agent
|
||||
Performance optimizations for LangCode Agent
|
||||
Enhanced with connection pooling, lazy loading, and advanced caching
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import functools
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
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"""
|
||||
|
||||
@@ -20,6 +211,9 @@ class PerformanceMonitor:
|
||||
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"""
|
||||
@@ -90,6 +284,20 @@ class PerformanceMonitor:
|
||||
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
|
||||
@@ -171,26 +379,26 @@ def optimize_provider_tools():
|
||||
|
||||
def optimize_agent_creation():
|
||||
"""Optimize agent creation with caching"""
|
||||
from main import create_maxclaw_agent
|
||||
from main import create_langcode_agent
|
||||
|
||||
# Cache agent creation
|
||||
original_create_maxclaw_agent = create_maxclaw_agent
|
||||
original_create_langcode_agent = create_langcode_agent
|
||||
|
||||
@functools.wraps(original_create_maxclaw_agent)
|
||||
def optimized_create_maxclaw_agent(system_prompt=None):
|
||||
@functools.wraps(original_create_langcode_agent)
|
||||
def optimized_create_langcode_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)
|
||||
result = original_create_langcode_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
|
||||
main.create_langcode_agent = optimized_create_langcode_agent
|
||||
|
||||
print("✅ Agent creation optimized with caching")
|
||||
|
||||
@@ -288,6 +496,12 @@ def get_performance_report() -> str:
|
||||
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
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
MaxClaw Agent Plugins Package
|
||||
LangCode Agent Plugins Package
|
||||
Provides plugin system for extensibility
|
||||
"""
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Base plugin class for MaxClaw Agent
|
||||
Base plugin class for LangCode Agent
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Any, Optional
|
||||
@@ -7,7 +7,7 @@ from langchain.tools import Tool
|
||||
|
||||
|
||||
class BasePlugin(ABC):
|
||||
"""Base class for MaxClaw plugins"""
|
||||
"""Base class for LangCode plugins"""
|
||||
|
||||
def __init__(self, name: str, version: str = "1.0.0", description: str = ""):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Example plugin for MaxClaw Agent
|
||||
Example plugin for LangCode Agent
|
||||
Demonstrates how to create a plugin
|
||||
"""
|
||||
import os
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
"""
|
||||
File tools plugin for LangCode 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 LangCode 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}")
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Plugin manager for MaxClaw Agent
|
||||
Plugin manager for LangCode Agent
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
@@ -15,7 +15,7 @@ from plugins.base import BasePlugin
|
||||
|
||||
|
||||
class PluginManager:
|
||||
"""Manages plugins for MaxClaw Agent"""
|
||||
"""Manages plugins for LangCode Agent"""
|
||||
|
||||
def __init__(self, plugin_dir: Optional[str] = None):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
System utilities plugin for LangCode 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}")
|
||||
+2
-2
@@ -7,13 +7,13 @@ import os
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
from main import create_maxclaw_agent
|
||||
from main import create_langcode_agent
|
||||
|
||||
def test_agent_creation():
|
||||
"""Test that the agent can be created successfully."""
|
||||
print("Testing agent creation...")
|
||||
try:
|
||||
agent = create_maxclaw_agent()
|
||||
agent = create_langcode_agent()
|
||||
print("✅ Agent created successfully!")
|
||||
return agent
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Test suite for MaxClaw Agent configuration manager
|
||||
Test suite for LangCode Agent configuration manager
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Test suite for MaxClaw Agent provider tools
|
||||
Test suite for LangCode Agent provider tools
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
MaxClaw Agent Tools Package
|
||||
LangCode Agent Tools Package
|
||||
Provides custom tools for Proxmox, Ollama, and unified provider management
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Ollama management tools for MaxClaw Agent
|
||||
Ollama management tools for LangCode Agent
|
||||
Provides tools for managing Ollama models and instances
|
||||
"""
|
||||
import requests
|
||||
@@ -309,7 +309,7 @@ class OllamaTools:
|
||||
|
||||
def get_ollama_tools(base_url: str = "http://localhost:11434") -> List[Tool]:
|
||||
"""
|
||||
Get a list of Ollama tools for the MaxClaw agent
|
||||
Get a list of Ollama tools for the LangCode agent
|
||||
|
||||
Args:
|
||||
base_url: Base URL of the Ollama instance
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Unified Provider Tools for MaxClaw Agent
|
||||
Unified Provider Tools for LangCode Agent
|
||||
Provides a consistent interface for managing different LLM providers
|
||||
"""
|
||||
import os
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Proxmox management tools for MaxClaw Agent
|
||||
Proxmox management tools for LangCode Agent
|
||||
Provides tools for managing Proxmox virtual machines and containers
|
||||
"""
|
||||
import subprocess
|
||||
@@ -266,7 +266,7 @@ class ProxmoxTools:
|
||||
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
|
||||
Get a list of Proxmox tools for the LangCode agent
|
||||
|
||||
Args:
|
||||
host: Proxmox host address
|
||||
|
||||
+365
-58
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MaxClaw Agent - Configurazione</title>
|
||||
<title>LangCode Agent - Configurazione</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -21,6 +21,7 @@
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
header {
|
||||
@@ -89,6 +90,8 @@
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
@@ -122,8 +125,8 @@
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.config-info h3 {
|
||||
@@ -139,6 +142,7 @@
|
||||
.config-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
@@ -193,10 +197,12 @@
|
||||
.chat-input {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chat-input input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
@@ -225,6 +231,7 @@
|
||||
display: flex;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -232,6 +239,9 @@
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
@@ -247,71 +257,233 @@
|
||||
.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>MaxClaw Agent</h1>
|
||||
<p>Interfaccia di configurazione per l'agente AI</p>
|
||||
<h1 id="title">LangCode 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')">Configurazione</div>
|
||||
<div class="tab" onclick="switchTab('chat')">Chat</div>
|
||||
<div class="tab" onclick="switchTab('status')">Stato</div>
|
||||
<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>Gestione Configurazioni</h2>
|
||||
<h2 id="config-title">Gestione Configurazioni</h2>
|
||||
|
||||
<div id="alert-container"></div>
|
||||
|
||||
<form id="config-form">
|
||||
<div class="form-group">
|
||||
<label for="config-name">Nome Configurazione</label>
|
||||
<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">Provider</label>
|
||||
<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">Nome Modello</label>
|
||||
<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">Chiave API</label>
|
||||
<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">URL Base</label>
|
||||
<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">
|
||||
Imposta come configurazione attiva
|
||||
<span id="set-active-label">Imposta come configurazione attiva</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn">Salva Configurazione</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="clearForm()">Pulisci</button>
|
||||
<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>Configurazioni Esistenti</h3>
|
||||
<h3 id="existing-configs-title">Configurazioni Esistenti</h3>
|
||||
<div id="config-list-container">
|
||||
<!-- Configurations will be loaded here -->
|
||||
</div>
|
||||
@@ -319,49 +491,49 @@
|
||||
</div>
|
||||
|
||||
<div id="chat-tab" class="tab-content">
|
||||
<h2>Chat con MaxClaw</h2>
|
||||
<h2 id="chat-title">Chat con LangCode</h2>
|
||||
|
||||
<div class="chat-container" id="chat-container">
|
||||
<div class="message agent-message">
|
||||
Ciao! Sono MaxClaw, il tuo agente AI. Come posso aiutarti oggi?
|
||||
<div class="message agent-message" id="welcome-message">
|
||||
Ciao! Sono LangCode, 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()">Invia</button>
|
||||
<button class="btn" onclick="sendMessage()" id="send-btn">Invia</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status-tab" class="tab-content">
|
||||
<h2>Stato del Sistema</h2>
|
||||
<h2 id="status-title">Stato del Sistema</h2>
|
||||
|
||||
<div id="status-container">
|
||||
<div class="alert alert-success">
|
||||
<strong>✅ Sistema operativo</strong><br>
|
||||
L'agente è pronto per l'uso.
|
||||
<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>Provider Attivo</h3>
|
||||
<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>Modello Attivo</h3>
|
||||
<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>Connessione</h3>
|
||||
<h3 id="connection-title">Connessione</h3>
|
||||
<p id="connection-status">Verifica in corso...</p>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="testConnection()">Testa Connessione</button>
|
||||
<button class="btn btn-secondary" onclick="testConnection()" id="test-connection-btn">Testa Connessione</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -372,8 +544,104 @@
|
||||
// API base URL
|
||||
const API_BASE = 'http://localhost:8000';
|
||||
|
||||
// Current language
|
||||
let currentLanguage = 'it';
|
||||
|
||||
// Translations
|
||||
const translations = {
|
||||
it: {
|
||||
title: "LangCode 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 LangCode",
|
||||
welcomeMessage: "Ciao! Sono LangCode, 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: "LangCode 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 LangCode",
|
||||
welcomeMessage: "Hello! I'm LangCode, 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('langcode-language') || 'it';
|
||||
document.getElementById('language-select').value = savedLanguage;
|
||||
changeLanguage(savedLanguage);
|
||||
|
||||
loadConfigurations();
|
||||
loadActiveConfiguration();
|
||||
|
||||
@@ -396,6 +664,45 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Change language
|
||||
function changeLanguage(language) {
|
||||
currentLanguage = language;
|
||||
localStorage.setItem('langcode-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
|
||||
@@ -421,7 +728,7 @@
|
||||
container.innerHTML = '';
|
||||
|
||||
if (configs.length === 0) {
|
||||
container.innerHTML = '<p>Nessuna configurazione trovata.</p>';
|
||||
container.innerHTML = `<p>${translations[currentLanguage].noProvider}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -431,20 +738,20 @@
|
||||
item.innerHTML = `
|
||||
<div class="config-info">
|
||||
<h3>${config.name}
|
||||
${config.is_active ? '<span class="status-badge status-active">Attivo</span>' : ''}
|
||||
${config.is_active ? `<span class="status-badge status-active">${translations[currentLanguage].active}</span>` : ''}
|
||||
</h3>
|
||||
<p>Provider: ${config.provider} | Modello: ${config.model_name}</p>
|
||||
<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}')">Attiva</button>
|
||||
<button class="btn btn-secondary" onclick="testConfig('${config.name}')">Testa</button>
|
||||
<button class="btn btn-danger" onclick="deleteConfig('${config.name}')">Elimina</button>
|
||||
<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('Errore nel caricamento delle configurazioni: ' + error.message, 'error');
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,8 +765,8 @@
|
||||
document.getElementById('active-provider').textContent = config.provider;
|
||||
document.getElementById('active-model').textContent = config.model_name;
|
||||
} else {
|
||||
document.getElementById('active-provider').textContent = 'Nessun provider configurato';
|
||||
document.getElementById('active-model').textContent = 'Nessun modello configurato';
|
||||
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);
|
||||
@@ -500,16 +807,16 @@
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Configurazione salvata con successo!', 'success');
|
||||
showAlert(translations[currentLanguage].alertSuccess, 'success');
|
||||
clearForm();
|
||||
loadConfigurations();
|
||||
loadActiveConfiguration();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showAlert('Errore: ' + error.detail, 'error');
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.detail, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Errore di connessione: ' + error.message, 'error');
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,15 +828,15 @@
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert(`Configurazione "${name}" attivata!`, 'success');
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} "${name}"`, 'success');
|
||||
loadConfigurations();
|
||||
loadActiveConfiguration();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showAlert('Errore: ' + error.detail, 'error');
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.detail, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Errore di connessione: ' + error.message, 'error');
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +855,7 @@
|
||||
showAlert(`❌ ${result.message}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Errore di connessione: ' + error.message, 'error');
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,36 +871,36 @@
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert(`Configurazione "${name}" eliminata!`, 'success');
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} "${name}"`, 'success');
|
||||
loadConfigurations();
|
||||
loadActiveConfiguration();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showAlert('Errore: ' + error.detail, 'error');
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.detail, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Errore di connessione: ' + error.message, 'error');
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test connection
|
||||
async function testConnection() {
|
||||
document.getElementById('connection-status').textContent = 'Test in corso...';
|
||||
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 = '✅ Connessione riuscita';
|
||||
showAlert('Connessione al server riuscita!', 'success');
|
||||
document.getElementById('connection-status').textContent = translations[currentLanguage].alertConnectionSuccess;
|
||||
showAlert(translations[currentLanguage].alertConnectionSuccess, 'success');
|
||||
} else {
|
||||
document.getElementById('connection-status').textContent = '❌ Connessione fallita';
|
||||
showAlert('Connessione al server fallita', 'error');
|
||||
document.getElementById('connection-status').textContent = translations[currentLanguage].alertConnectionFailed;
|
||||
showAlert(translations[currentLanguage].alertConnectionFailed, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('connection-status').textContent = '❌ Errore di connessione';
|
||||
showAlert('Errore di connessione: ' + error.message, 'error');
|
||||
document.getElementById('connection-status').textContent = translations[currentLanguage].alertConnectionFailed;
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,7 +929,7 @@
|
||||
// Add agent response to chat
|
||||
addMessageToChat(result.response, 'agent');
|
||||
} catch (error) {
|
||||
addMessageToChat('Errore: Impossibile connettersi al server', 'agent');
|
||||
addMessageToChat(translations[currentLanguage].alertError + ': ' + error.message, 'agent');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,7 +951,7 @@
|
||||
const baseUrlInput = document.getElementById('config-base-url');
|
||||
|
||||
// Show/hide fields based on provider
|
||||
if (provider === 'openai' || provider === 'openrouter') {
|
||||
if (provider === 'openai' || provider === 'openrouter' || provider === 'anthropic' || provider === 'google') {
|
||||
apiKeyGroup.style.display = 'block';
|
||||
} else {
|
||||
apiKeyGroup.style.display = 'none';
|
||||
|
||||
@@ -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>LangCode 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">LangCode 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: "LangCode 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: "LangCode 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('langcode-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('langcode-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: "LangCode 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: "LangCode Team"
|
||||
}
|
||||
];
|
||||
|
||||
const container = document.getElementById('installed-plugins-grid');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (installed.length === 0) {
|
||||
container.innerHTML = `<p>${translations[currentLanguage].noPlugins}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
installed.forEach(plugin => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'plugin-card';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="plugin-header">
|
||||
<div class="plugin-name">${plugin.name}</div>
|
||||
<div class="plugin-status status-installed">${translations[currentLanguage].installed}</div>
|
||||
</div>
|
||||
<div class="plugin-description">${plugin.description}</div>
|
||||
<div class="plugin-actions">
|
||||
<button class="btn btn-danger" onclick="uninstallPlugin('${plugin.name}')">${translations[currentLanguage].uninstallBtn}</button>
|
||||
<button class="btn btn-secondary" onclick="showPluginInfo('${plugin.name}')">${translations[currentLanguage].infoBtn}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Search plugins
|
||||
async function searchPlugins() {
|
||||
const query = document.getElementById('search-input').value.trim();
|
||||
|
||||
if (!query) {
|
||||
showAlert(translations[currentLanguage].alertWarning + ': Inserisci un termine di ricerca', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showAlert(translations[currentLanguage].alertSearching, 'info');
|
||||
|
||||
try {
|
||||
// Simulate search results (in real implementation, this would come from API)
|
||||
const results = [
|
||||
{
|
||||
name: "weather",
|
||||
description: "Get weather information for any location",
|
||||
installed: false
|
||||
},
|
||||
{
|
||||
name: "translation",
|
||||
description: "Translate text between languages",
|
||||
installed: false
|
||||
}
|
||||
];
|
||||
|
||||
const container = document.getElementById('search-results-grid');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (results.length === 0) {
|
||||
container.innerHTML = `<p>${translations[currentLanguage].noPlugins}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
results.forEach(plugin => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'plugin-card';
|
||||
|
||||
const statusClass = plugin.installed ? 'status-installed' : 'status-available';
|
||||
const statusText = plugin.installed ? translations[currentLanguage].installed : translations[currentLanguage].available;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="plugin-header">
|
||||
<div class="plugin-name">${plugin.name}</div>
|
||||
<div class="plugin-status ${statusClass}">${statusText}</div>
|
||||
</div>
|
||||
<div class="plugin-description">${plugin.description}</div>
|
||||
<div class="plugin-actions">
|
||||
${plugin.installed ?
|
||||
`<button class="btn btn-danger" onclick="uninstallPlugin('${plugin.name}')">${translations[currentLanguage].uninstallBtn}</button>` :
|
||||
`<button class="btn btn-primary" onclick="installPlugin('${plugin.name}')">${translations[currentLanguage].installBtn}</button>`
|
||||
}
|
||||
<button class="btn btn-secondary" onclick="showPluginInfo('${plugin.name}')">${translations[currentLanguage].infoBtn}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} Trovati ${results.length} plugin`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Install plugin
|
||||
async function installPlugin(pluginName) {
|
||||
showAlert(translations[currentLanguage].alertInstalling + ' ' + pluginName, 'info');
|
||||
|
||||
try {
|
||||
// Simulate installation (in real implementation, this would call API)
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} Plugin '${pluginName}' installato!`, 'success');
|
||||
|
||||
// Refresh the lists
|
||||
loadPlugins();
|
||||
loadInstalledPlugins();
|
||||
loadMarketplaceStats();
|
||||
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Uninstall plugin
|
||||
async function uninstallPlugin(pluginName) {
|
||||
if (!confirm(`Sei sicuro di voler disinstallare il plugin '${pluginName}'?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showAlert(translations[currentLanguage].alertUninstalling + ' ' + pluginName, 'info');
|
||||
|
||||
try {
|
||||
// Simulate uninstallation (in real implementation, this would call API)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
showAlert(`${translations[currentLanguage].alertSuccess} Plugin '${pluginName}' disinstallato!`, 'success');
|
||||
|
||||
// Refresh the lists
|
||||
loadPlugins();
|
||||
loadInstalledPlugins();
|
||||
loadMarketplaceStats();
|
||||
|
||||
} catch (error) {
|
||||
showAlert(translations[currentLanguage].alertError + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show plugin info
|
||||
function showPluginInfo(pluginName) {
|
||||
alert(`Informazioni sul plugin: ${pluginName}\n\nQuesta funzionalità mostrerà dettagli completi sul plugin, inclusi:\n- Versione\n- Autore\n- Descrizione dettagliata\n- Dipendenze\n- Documentazione`);
|
||||
}
|
||||
|
||||
// Show alert
|
||||
function showAlert(message, type) {
|
||||
const container = document.getElementById('alert-container');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
|
||||
// Remove existing alerts
|
||||
container.innerHTML = '';
|
||||
container.appendChild(alert);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,829 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LangCode 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">LangCode 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="langcode-label">LangCode Version</span>
|
||||
<span class="metric-value" id="langcode-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: "LangCode 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",
|
||||
langcodeLabel: "LangCode Version",
|
||||
lastUpdatedLabel: "Ultimo aggiornamento:",
|
||||
alertSuccess: "Dati aggiornati con successo",
|
||||
alertError: "Errore nel caricamento dei dati"
|
||||
},
|
||||
en: {
|
||||
title: "LangCode 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",
|
||||
langcodeLabel: "LangCode 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('langcode-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('langcode-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('langcode-label').textContent = t.langcodeLabel;
|
||||
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 = 'langcode-agent';
|
||||
document.getElementById('system-platform').textContent = navigator.platform;
|
||||
document.getElementById('system-architecture').textContent = navigator.platform.includes('64') ? 'x64' : 'x86';
|
||||
document.getElementById('python-version').textContent = '3.11.0';
|
||||
|
||||
// Update logs
|
||||
updateLogs();
|
||||
}
|
||||
|
||||
// Update logs
|
||||
function updateLogs() {
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
|
||||
// Simulate log entries
|
||||
const logTypes = ['info', 'warning', 'error', 'success'];
|
||||
const logMessages = {
|
||||
it: [
|
||||
'Agente inizializzato con successo',
|
||||
'Configurazione caricata: ollama',
|
||||
'Test connessione riuscito',
|
||||
'Strumento eseguito: list_models',
|
||||
'Avviso: Alto utilizzo memoria',
|
||||
'Errore: Timeout connessione',
|
||||
'Plugin caricato: file_tools',
|
||||
'Richiesta elaborata in 0.245s'
|
||||
],
|
||||
en: [
|
||||
'Agent initialized successfully',
|
||||
'Configuration loaded: ollama',
|
||||
'Connection test successful',
|
||||
'Tool executed: list_models',
|
||||
'Warning: High memory usage',
|
||||
'Error: Connection timeout',
|
||||
'Plugin loaded: file_tools',
|
||||
'Request processed in 0.245s'
|
||||
]
|
||||
};
|
||||
|
||||
let logsHtml = '';
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const type = logTypes[Math.floor(Math.random() * logTypes.length)];
|
||||
const messages = logMessages[currentLanguage];
|
||||
const message = messages[Math.floor(Math.random() * messages.length)];
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
logsHtml += `<div class="log-entry log-${type}">[${timestamp}] ${message}</div>`;
|
||||
}
|
||||
|
||||
logsContainer.innerHTML = logsHtml;
|
||||
}
|
||||
|
||||
// Update last updated timestamp
|
||||
function updateLastUpdated() {
|
||||
const now = new Date();
|
||||
document.getElementById('last-updated').textContent = now.toLocaleString();
|
||||
}
|
||||
|
||||
// Show alert
|
||||
function showAlert(message, type) {
|
||||
const container = document.getElementById('alert-container');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
|
||||
// Remove existing alerts
|
||||
container.innerHTML = '';
|
||||
container.appendChild(alert);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user