Compare commits
78
Commits
8dbab53c6d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
442c91ee66 | ||
|
|
858b469def | ||
|
|
2e4175f26b | ||
|
|
e72905fd78 | ||
|
|
aec5736394 | ||
|
|
e69a289361 | ||
|
|
b6e8864829 | ||
|
|
39b9288773 | ||
|
|
20233db282 | ||
|
|
c76beb764c | ||
|
|
6cb514ec13 | ||
|
|
5d9bd48013 | ||
|
|
453156148c | ||
|
|
e75a75e16c | ||
|
|
f1d1248d4b | ||
|
|
3197dedd4c | ||
|
|
918b1f3a5b | ||
|
|
c8e7489c26 | ||
|
|
82b70f88d1 | ||
|
|
3a042355b8 | ||
|
|
9fa564e12b | ||
|
|
4a0342dcb7 | ||
|
|
32ed76e5be | ||
|
|
fc60fc1fd0 | ||
|
|
cce8709149 | ||
|
|
f847b92b0a | ||
|
|
1c7781a68f | ||
|
|
e5ce70c68e | ||
|
|
4fef38b740 | ||
|
|
f9def3e389 | ||
|
|
462f3d1785 | ||
|
|
57c43d4738 | ||
|
|
f678a6f20d | ||
|
|
34d6623cb2 | ||
|
|
f8ebfb65ab | ||
|
|
1e49eff703 | ||
|
|
9cccbabef1 | ||
|
|
cffac60211 | ||
|
|
d16102587d | ||
|
|
a0ce57132d | ||
|
|
fbc19aef23 | ||
|
|
e7cd792954 | ||
|
|
b22297a5d4 | ||
|
|
2253fbad13 | ||
|
|
658f7cd580 | ||
|
|
8528068995 | ||
|
|
7fd3bfd26b | ||
|
|
52925d80ce | ||
|
|
ecf4648393 | ||
|
|
7c645bff78 | ||
|
|
a2f933edee | ||
|
|
9970bba6c7 | ||
|
|
2fa06ec0af | ||
|
|
a309c7e27c | ||
|
|
2077618f52 | ||
|
|
0b5c6a874e | ||
|
|
8acbeb7dfe | ||
|
|
b25aa8fbad | ||
|
|
4de50a1712 | ||
|
|
122ad6930b | ||
|
|
05fc002cb4 | ||
|
|
d055204067 | ||
|
|
56f2819a92 | ||
|
|
5a8fcda993 | ||
|
|
d68417bbc8 | ||
|
|
6e7ae3a959 | ||
|
|
753c7d491e | ||
|
|
55a31278a8 | ||
|
|
eda22811ce | ||
|
|
0612b290d5 | ||
|
|
3e986d83f1 | ||
|
|
1dae4c5656 | ||
|
|
cf8e4c677f | ||
|
|
b3f57a8635 | ||
|
|
7103b218b9 | ||
|
|
979d19bdc1 | ||
|
|
e92cc701f6 | ||
|
|
6e1b1a44db |
No files matched your search
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"conversations": [],
|
||||
"facts": [
|
||||
{
|
||||
"timestamp": "2026-03-22T20:18:14.550293",
|
||||
"fact": "LangCode Agent supports MCP integration",
|
||||
"category": "features"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-22T20:27:22.386697",
|
||||
"fact": "LangCode Agent supports MCP integration",
|
||||
"category": "features"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-22T21:16:16.644866",
|
||||
"fact": "\"LangCode Agent supports MCP integration\"",
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-22T21:19:31.649087",
|
||||
"fact": "\"LangCode Agent supports MCP integration\"",
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-22T21:25:33.013155",
|
||||
"fact": "\"LangCode Agent supports MCP integration\"",
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-22T21:30:17.895357",
|
||||
"fact": "\"LangCode Agent supports MCP integration\"",
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-22T22:34:46.997791",
|
||||
"fact": "\"LangCode Agent supports MCP integration\"",
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-23T07:14:33.532609",
|
||||
"fact": "\"LangCode Agent supports MCP integration\"",
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-23T09:14:30.538647",
|
||||
"fact": "\"Il mio colore preferito \u00e8 il verde\"",
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-23T09:36:02.053792",
|
||||
"fact": "Questo \u00e8 un test senza virgolette",
|
||||
"category": "general"
|
||||
}
|
||||
],
|
||||
"preferences": {
|
||||
"model_provider": "ollama"
|
||||
},
|
||||
"skills_used": [],
|
||||
"last_updated": "2026-03-23T09:36:02.053800"
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# Stato Debug LangCode Agent - 23 Marzo 2026
|
||||
|
||||
## Situazione Attuale
|
||||
|
||||
L'agente **funziona** a livello di esecuzione tool, ma **non mostra il risultato** all'utente.
|
||||
|
||||
## Cosa Funziona
|
||||
|
||||
1. **Intent Detection**: Rileva correttamente "che SO sto usando" come richiesta di system_info
|
||||
2. **Tool Execution**: Il tool `system_info` viene eseguito e restituisce il risultato corretto
|
||||
3. **Response Generation**: `response_text` contiene i dati corretti
|
||||
|
||||
### Debug Output Osservato (CORRETTO):
|
||||
```
|
||||
OLLAMA_AGENT: user_content='che so sto usando'
|
||||
OLLAMA_AGENT: detected_tool=ToolCall(id='intent', name='system_info', arguments={})
|
||||
_execute_tool: name=system_info, args={}
|
||||
_execute_tool: got result type=<class 'str'>, value=OS: Linux 6.17.4-2-pve...
|
||||
OLLAMA_AGENT: result type=<class 'dict'>, value={'result': 'OS: Linux 6.17.4-2-pve...'}
|
||||
OLLAMA_AGENT: response_text=OS: Linux 6.17.4-2-pve
|
||||
Python: 3.12.3
|
||||
Current dir: /root/tryout/langcode_agent
|
||||
User: root
|
||||
```
|
||||
|
||||
## Problema
|
||||
|
||||
Nonostante `response_text` sia corretto, l'output mostra:
|
||||
```
|
||||
🤖 LangCode: che so sto usando
|
||||
```
|
||||
|
||||
Invece di:
|
||||
```
|
||||
🤖 LangCode: OS: Linux 6.17.4-2-pve
|
||||
Python: 3.12.3...
|
||||
```
|
||||
|
||||
## Ipotesi
|
||||
|
||||
1. **L'agente viene chiamato due volte**: Prima con SimpleAgent (fallback) e poi con OllamaToolAgent
|
||||
2. **L'eccezione viene mangiata**: Il blocco try/except in main.py potrebbe nascondere l'errore
|
||||
3. **Bufferizzazione output**: I print() non vengono visualizzati prima dell'eco
|
||||
|
||||
## Codicemain.py (main loop)
|
||||
|
||||
```python
|
||||
while True:
|
||||
user_input = input("\n👤 You: ").strip()
|
||||
|
||||
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:
|
||||
print("MAIN: before invoke", flush=True)
|
||||
result = agent.invoke({"messages": [HumanMessage(content=user_input)]})
|
||||
print(f"MAIN: after invoke, result type={type(result)}", flush=True)
|
||||
|
||||
# Extract the AI's response
|
||||
if result and "response" in result:
|
||||
response_text = result["response"]
|
||||
else:
|
||||
ai_message = result["messages"][-1]
|
||||
response_text = ai_message.content if hasattr(ai_message, 'content') else str(ai_message)
|
||||
|
||||
print(f"\n🤖 LangCode: {response_text}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Errore: {str(e)}")
|
||||
```
|
||||
|
||||
## Osservazioni Critiche
|
||||
|
||||
1. `MAIN: before invoke` NON viene mai visualizzato
|
||||
2. Questo significa che `agent.invoke()` potrebbe:
|
||||
- Essere chiamato ma stampare prima di return
|
||||
- Sollevare un'eccezione che viene catturata
|
||||
- Restituire qualcosa di inaspettato
|
||||
|
||||
3. L'eco `🤖 LangCode: che so sto usando` è identico all'input `👤 You: che so sto usando`
|
||||
- Questo suggerisce che l'agent sta restituendo l'input invece del risultato
|
||||
|
||||
## Prossimi Passi Suggeriti
|
||||
|
||||
1. **Verificare quale agente viene creato**: Aggiungere print in `create_langcode_agent()`
|
||||
2. **Verificare il tipo di agent restituito**: Stampare `type(agent)` dopo la creazione
|
||||
3. **Forzare output non bufferizzato**: Usare `sys.stdout.flush()` dopo ogni print
|
||||
4. **Verificare se l'eccezione viene mangiata**: Stampare SEMPRE l'eccezione, anche se vuota
|
||||
|
||||
## File Principali
|
||||
|
||||
- `main.py` - Entry point, main loop
|
||||
- `ollama_agent.py` - OllamaToolAgent con intent detection
|
||||
- `langcode.py` - CLI entry point
|
||||
|
||||
## Comandi di Test
|
||||
|
||||
```bash
|
||||
# Sul server
|
||||
cd ~/tryout/langcode_agent
|
||||
source venv/bin/activate
|
||||
python langcode.py
|
||||
|
||||
# Testare: "che SO sto usando"
|
||||
# Osservare: Il debug output dell'agente MA NON l'output MAIN
|
||||
```
|
||||
+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"]
|
||||
@@ -0,0 +1,199 @@
|
||||
# LangCode Agent Enhancement Implementation Plan
|
||||
Based on LangChain Deep Agents Architecture Analysis
|
||||
|
||||
## Overview
|
||||
|
||||
This plan outlines the integration of enhancements inspired by LangChain's Deep Agents architecture into the LangCode Agent. The enhancements focus on:
|
||||
|
||||
1. **Advanced Memory Systems** - Persistent, semantic, and summary-based memory
|
||||
2. **Enhanced Planning & Reasoning** - Sophisticated task planning and execution tracking
|
||||
3. **Production Observability** - Monitoring, tracing, and debugging capabilities
|
||||
4. **Agent Hierarchies** - Specialist agents and routing capabilities
|
||||
|
||||
## Phase 1: Foundation Enhancements (Immediate Integration)
|
||||
|
||||
### 1.1 Simple Persistent Memory System
|
||||
**File:** `enhancements/simple_memory.py`
|
||||
**Status:** Implemented and tested
|
||||
**Integration Points:**
|
||||
- Import in `main.py`: `from enhancements.simple_memory import simple_memory, create_simple_memory_tools`
|
||||
- Add to `get_custom_tools()`: Extend with memory tools
|
||||
- Update system prompt to mention memory capabilities
|
||||
- Add CLI commands: `/memory remember`, `/memory recall`, `/memory preference`, `/memory stats`
|
||||
|
||||
### 1.2 Enhanced Planning System
|
||||
**File:** `enhancements/planning_enhancement.py`
|
||||
**Status:** Implemented and tested
|
||||
**Integration Points:**
|
||||
- Import in `main.py`: `from enhancements.planning_enhancement import enhanced_planner`
|
||||
- Add to `get_custom_tools()`: Extend with planning tools
|
||||
- Update system prompt to mention planning capabilities
|
||||
- Enhance existing `/todo` commands or add `/plan` subcommands
|
||||
|
||||
## Phase 2: Advanced Features (Next Iteration)
|
||||
|
||||
### 2.1 Advanced Memory with Vector Storage
|
||||
**File:** `enhancements/memory_enhancement.py`
|
||||
**Dependencies:**
|
||||
- `langchain-community` for OllamaEmbeddings and FAISS
|
||||
- `sentence-transformers` for embeddings
|
||||
**Integration Points:**
|
||||
- Similar to simple memory but with semantic search capabilities
|
||||
- Add background processing for conversation summarization
|
||||
- Implement memory consolidation during idle periods
|
||||
|
||||
### 2.2 Agent Hierarchies and Routing
|
||||
**New File:** `enhancements/agent_hierarchy.py`
|
||||
**Features:**
|
||||
- Specialist agents for different domains (coding, research, system admin)
|
||||
- Intelligent routing based on task analysis
|
||||
- Agent-to-agent communication patterns
|
||||
- Load balancing and specialization
|
||||
|
||||
### 2.3 Production Observability
|
||||
**New File:** `enhancements/observability.py`
|
||||
**Features:**
|
||||
- LangChain-style callbacks for tracing
|
||||
- Tool usage monitoring and metrics
|
||||
- Performance profiling
|
||||
- Error tracking and alerting
|
||||
- Integration with LangSmith or custom monitoring
|
||||
|
||||
## Phase 3: Deep Agents Alignment (Future Work)
|
||||
|
||||
### 3.1 Context Management Enhancements
|
||||
- Implement automatic summarization at 85% context usage
|
||||
- Add file-based storage for large tool outputs
|
||||
- Implement pointer-based context management
|
||||
- Add context compression techniques
|
||||
|
||||
### 3.2 Subagent Spawning Improvements
|
||||
- Enhance the `task` tool with better isolation
|
||||
- Add per-subagent model configuration
|
||||
- Implement subagent pooling for efficiency
|
||||
- Add inter-subagent communication mechanisms
|
||||
|
||||
### 3.3 Advanced Planning Features
|
||||
- Implement plan-and-execute agent pattern
|
||||
- Add self-reflection and critique mechanisms
|
||||
- Enable dynamic replanning based on execution results
|
||||
- Add contingency planning and error recovery
|
||||
|
||||
## Integration Strategy
|
||||
|
||||
### Step-by-Step Integration:
|
||||
|
||||
1. **Start with Simple Enhancements** (Phase 1)
|
||||
- Integrate `simple_memory.py` and `planning_enhancement.py`
|
||||
- Test thoroughly with existing functionality
|
||||
- Gather user feedback
|
||||
|
||||
2. **Update Documentation**
|
||||
- Update `docs/plans/` with enhancement designs
|
||||
- Add usage examples to README
|
||||
- Create tutorial examples
|
||||
|
||||
3. **Gradual Rollout of Advanced Features** (Phase 2)
|
||||
- Add one enhancement at a time
|
||||
- Maintain backward compatibility
|
||||
- Provide migration paths
|
||||
|
||||
4. **Performance Optimization**
|
||||
- Profile and optimize memory usage
|
||||
- Implement caching where appropriate
|
||||
- Add configuration options to enable/disable features
|
||||
|
||||
## CLI Integration Examples
|
||||
|
||||
### Memory Commands:
|
||||
```
|
||||
/memory remember "LangCode uses Ollama for local models" --category architecture
|
||||
/memory recall "Ollama"
|
||||
/memory preference set model_provider ollama
|
||||
/memory preference get model_provider
|
||||
/memory stats
|
||||
```
|
||||
|
||||
### Planning Commands:
|
||||
```
|
||||
/plan create "Implement user authentication" --tasks "Design DB schema" "Create API endpoints" "Write frontend" "Add tests"
|
||||
/plan start auth-plan
|
||||
/plan list
|
||||
/plan progress auth-plan
|
||||
/task list
|
||||
/task start task_0000
|
||||
/task complete task_0000 "Authentication system implemented"
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Add to configuration system:
|
||||
```json
|
||||
{
|
||||
"enhancements": {
|
||||
"memory": {
|
||||
"type": "simple|advanced",
|
||||
"persist_directory": "./.langcode/memory",
|
||||
"max_conversations": 100,
|
||||
"max_facts": 50
|
||||
},
|
||||
"planning": {
|
||||
"enabled": true,
|
||||
"auto_create_todos": true,
|
||||
"dependency_tracking": true
|
||||
},
|
||||
"observability": {
|
||||
"enabled": false,
|
||||
"trace_level": "info",
|
||||
"metrics_endpoint": "http://localhost:9090/metrics"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Backward Compatibility Strategy
|
||||
|
||||
All enhancements are designed to be:
|
||||
- **Optional**: Can be enabled/disabled via configuration
|
||||
- **Additive**: Don't modify existing core functionality
|
||||
- **Testable**: Include comprehensive test suites
|
||||
- **Documented**: Clear usage instructions and examples
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
1. **Dependency Management**:
|
||||
- Enhancements with heavy dependencies are isolated
|
||||
- Fallback to simpler implementations when deps missing
|
||||
- Clear error messages when features unavailable
|
||||
|
||||
2. **Performance Impact**:
|
||||
- Lazy initialization of heavy components
|
||||
- Configurable feature flags
|
||||
- Performance monitoring built-in
|
||||
|
||||
3. **Data Safety**:
|
||||
- Memory data stored locally by default
|
||||
- No automatic data transmission
|
||||
- User control over what gets remembered
|
||||
|
||||
## Success Metrics
|
||||
|
||||
After implementation, measure:
|
||||
- User adoption of new features
|
||||
- Reduction in repeated context requests
|
||||
- Improvement in complex task completion rates
|
||||
- User satisfaction scores
|
||||
- System performance impact
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review and approve this implementation plan
|
||||
2. Begin Phase 1 integration (simple memory and planning)
|
||||
3. Conduct internal testing
|
||||
4. Gather feedback from pilot users
|
||||
5. Iterate based on feedback
|
||||
6. Proceed to Phase 2 enhancements
|
||||
|
||||
---
|
||||
|
||||
*This plan brings LangCode Agent closer to the production-grade architecture patterns revealed in LangChain's Deep Agents while maintaining the project's core strengths in configurability and modularity.*
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
# LangCode Agent - Guida Rapida
|
||||
|
||||
## Installazione
|
||||
|
||||
```bash
|
||||
# 1. Clona il repository
|
||||
git clone http://192.168.0.183:8085/truetype/langcode_agent.git
|
||||
cd langcode_agent
|
||||
|
||||
# 2. Crea e attiva virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 3. Installa dipendenze
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configurazione Rapida
|
||||
|
||||
```bash
|
||||
# Imposta variabili d'ambiente
|
||||
export MODEL_PROVIDER=ollama
|
||||
export MODEL_NAME=gemma2:9b
|
||||
export OLLAMA_BASE_URL=http://192.168.0.50:11434
|
||||
```
|
||||
|
||||
Oppure usa la CLI interattiva per salvare configurazioni persistenti:
|
||||
|
||||
```bash
|
||||
langcode config add -n ollama -p ollama -m gemma2:9b -u http://192.168.0.50:11434 --set-active
|
||||
langcode config test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modalità di Utilizzo
|
||||
|
||||
### 1. Chat Interattiva (EnhancedCLI)
|
||||
|
||||
```bash
|
||||
langcode chat
|
||||
```
|
||||
|
||||
Chatta con l'agent AI in tempo reale con UI moderna:
|
||||
- **Colors**: User (cyan), Agent (green), Error (red)
|
||||
- **History**: Cronologia comandi persistente (↑↓)
|
||||
- **Autocomplete**: Tab per completare comandi
|
||||
- **4 Temi**: default, dark, light, monokai
|
||||
- **Syntax Highlighting**: Code blocks colorati
|
||||
- **Keyboard shortcuts**: Ctrl+A/E, Ctrl+K/U, Ctrl+W
|
||||
|
||||
**Comandi CLI interni:**
|
||||
```
|
||||
/help - Mostra aiuto
|
||||
/status - Stato sistema
|
||||
/clear - Pulisci schermo (Ctrl+L)
|
||||
/theme-* - Cambia tema
|
||||
/timestamps - Toggle timestamps
|
||||
/syntax - Toggle syntax highlighting
|
||||
/history - Mostra cronologia
|
||||
/exit - Esci
|
||||
```
|
||||
|
||||
### 2. Server Web (API + Dashboard)
|
||||
|
||||
```bash
|
||||
langcode server
|
||||
langcode server --port 8080
|
||||
langcode server --host 0.0.0.0 --port 3000
|
||||
```
|
||||
|
||||
Accesso dopo l'avvio:
|
||||
- 🌐 **Web UI**: http://localhost:8000
|
||||
- 📊 **Dashboard**: http://localhost:8000/monitoring
|
||||
- 🛒 **Marketplace**: http://localhost:8000/marketplace
|
||||
- 📖 **API Docs**: http://localhost:8000/docs
|
||||
|
||||
### 3. CLI Configurazione
|
||||
|
||||
```bash
|
||||
langcode config list # Lista configurazioni
|
||||
langcode config add -n nome -p ollama -m gemma2:9b -u http://... # Aggiungi
|
||||
langcode config use nome # Attiva configurazione
|
||||
langcode config active # Mostra attiva
|
||||
langcode config test # Testa connessione
|
||||
langcode config search ollama # Cerca
|
||||
langcode config export -f json # Esporta
|
||||
langcode config import backup.json # Importa
|
||||
```
|
||||
|
||||
### 4. TUI Interattiva
|
||||
|
||||
```bash
|
||||
langcode tui
|
||||
```
|
||||
|
||||
Interfaccia testuale per gestire configurazioni.
|
||||
|
||||
### 5. Analytics
|
||||
|
||||
```bash
|
||||
langcode analytics # Riepilogo rapido
|
||||
langcode analytics --report # Report completo
|
||||
langcode dashboard # Dashboard performance
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comandi Interni (nella Chat)
|
||||
|
||||
Durante la chat interattiva (`langcode chat`):
|
||||
|
||||
```
|
||||
/help # Aiuto
|
||||
/status # Stato sistema
|
||||
/config list # Lista configurazioni
|
||||
/config use <nome> # Cambia configurazione
|
||||
/models # Elenca modelli
|
||||
/clear # Pulisci schermo
|
||||
/exit # Esci
|
||||
```
|
||||
|
||||
### Memory (Memoria Persistente)
|
||||
|
||||
```
|
||||
/memory remember <testo> # Salva un fatto
|
||||
/memory recall <query> # Cerca fatti
|
||||
/memory preference set <key> <value> # Imposta preferenza
|
||||
/memory preference get <key> # Leggi preferenza
|
||||
/memory stats # Statistiche
|
||||
```
|
||||
|
||||
### Planning (Pianificazione)
|
||||
|
||||
```
|
||||
/plan create "<nome piano>" "<task1; task2; task3>" # Crea piano
|
||||
/plan list # Elenca piani
|
||||
/plan progress <plan_id> # Mostra progresso
|
||||
/plan start <task_id> # Inizia task
|
||||
/plan complete <task_id> # Completa task
|
||||
```
|
||||
|
||||
### Skills (Gestione Skills)
|
||||
|
||||
```
|
||||
/skills list # Skills installate
|
||||
/skills search <query> # Cerca skills
|
||||
/skills install <nome> # Installa skill
|
||||
/skills details <nome> # Dettagli skill
|
||||
/skills uninstall <nome> # Disinstalla
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Struttura
|
||||
|
||||
```
|
||||
langcode_agent/
|
||||
├── langcode.py # CLI unificato (langcode)
|
||||
├── main.py # Agent principale
|
||||
├── api_server.py # Server web + API
|
||||
├── config_manager.py # Gestione configurazioni
|
||||
├── config_cli.py # CLI configurazione
|
||||
├── config_tui.py # TUI configurazione
|
||||
├── skill_manager.py # Gestione skills
|
||||
├── mcp_client.py # Client MCP
|
||||
├── analytics.py # Analytics
|
||||
├── dashboard.py # Dashboard performance
|
||||
├── monitoring.py # Monitoraggio
|
||||
├── enhancements/ # Memory e Planning
|
||||
│ ├── simple_memory.py
|
||||
│ └── planning_enhancement.py
|
||||
├── web/ # Frontend web
|
||||
├── plugins/ # Plugin sistema
|
||||
└── venv/ # Virtual environment
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Provider Supportati
|
||||
|
||||
| Provider | Variabili necessarie |
|
||||
|-------------|---------------------|
|
||||
| Ollama | `OLLAMA_BASE_URL` |
|
||||
| OpenAI | `OPENAI_API_KEY` |
|
||||
| OpenRouter | `OPENROUTER_API_KEY` |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` |
|
||||
| Google | `GOOGLE_API_KEY` |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Agent non si avvia:**
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**Ollama non risponde:**
|
||||
```bash
|
||||
curl http://192.168.0.50:11434/api/tags
|
||||
```
|
||||
|
||||
**Porta già in uso:**
|
||||
```bash
|
||||
langcode server --port 8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Tempo installazione**: ~5 minuti
|
||||
Binary file not shown.
Binary file not shown.
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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
# Bash completion for LangCode Agent CLI
|
||||
|
||||
_langcode_completions()
|
||||
{
|
||||
local cur prev commands subcommands
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
# Main commands
|
||||
commands="chat server tui config analytics dashboard version"
|
||||
|
||||
# Config subcommands
|
||||
config_subcommands="list add remove use active test export import search"
|
||||
|
||||
# Handle main command completion
|
||||
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
||||
COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Handle config subcommand completion
|
||||
if [[ "${COMP_WORDS[1]}" == "config" && ${COMP_CWORD} -eq 2 ]]; then
|
||||
COMPREPLY=( $(compgen -W "${config_subcommands}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Handle subcommand-specific completions
|
||||
case "${prev}" in
|
||||
server)
|
||||
local server_options="--port --host"
|
||||
COMPREPLY=( $(compgen -W "${server_options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
analytics)
|
||||
local analytics_options="--report --clear"
|
||||
COMPREPLY=( $(compgen -W "${analytics_options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
dashboard)
|
||||
local dashboard_options="--clear"
|
||||
COMPREPLY=( $(compgen -W "${dashboard_options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
search)
|
||||
# Search term - no completion
|
||||
return 0
|
||||
;;
|
||||
add)
|
||||
# Add command options
|
||||
local add_options="--name --provider --model --api-key --base-url --activate"
|
||||
COMPREPLY=( $(compgen -W "${add_options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
remove|use|test)
|
||||
# Configuration names - try to get from config list
|
||||
if command -v python3 &> /dev/null; then
|
||||
local configs=$(python3 -c "
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
configs = config_manager.list_configuration_names()
|
||||
print(' '.join(configs))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null)
|
||||
COMPREPLY=( $(compgen -W "${configs}" -- ${cur}) )
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
export)
|
||||
# Export options
|
||||
local export_options="--format --output"
|
||||
COMPREPLY=( $(compgen -W "${export_options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
import)
|
||||
# File completion
|
||||
COMPREPLY=( $(compgen -f -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--provider|-p)
|
||||
# Provider options
|
||||
local providers="openai ollama openrouter anthropic google"
|
||||
COMPREPLY=( $(compgen -W "${providers}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--format|-f)
|
||||
# Format options
|
||||
local formats="json yaml"
|
||||
COMPREPLY=( $(compgen -W "${formats}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--port|-p)
|
||||
# Port suggestions
|
||||
local ports="8000 3000 8080 5000"
|
||||
COMPREPLY=( $(compgen -W "${ports}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Handle option arguments
|
||||
case "${cur}" in
|
||||
-*)
|
||||
# Option completion based on context
|
||||
if [[ "${COMP_WORDS[1]}" == "server" ]]; then
|
||||
local options="--port --host"
|
||||
elif [[ "${COMP_WORDS[1]}" == "config" && "${COMP_WORDS[2]}" == "add" ]]; then
|
||||
local options="--name --provider --model --api-key --base-url --activate"
|
||||
elif [[ "${COMP_WORDS[1]}" == "config" && "${COMP_WORDS[2]}" == "export" ]]; then
|
||||
local options="--format --output"
|
||||
elif [[ "${COMP_WORDS[1]}" == "analytics" ]]; then
|
||||
local options="--report --clear"
|
||||
elif [[ "${COMP_WORDS[1]}" == "dashboard" ]]; then
|
||||
local options="--clear"
|
||||
else
|
||||
local options="--help --version"
|
||||
fi
|
||||
COMPREPLY=( $(compgen -W "${options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _langcode_completions langcode
|
||||
complete -F _langcode_completions langcode.py
|
||||
@@ -0,0 +1,107 @@
|
||||
#compdef langcode
|
||||
# Zsh completion for LangCode Agent CLI
|
||||
|
||||
_langcode() {
|
||||
local -a commands config_subcommands
|
||||
local curcontext="$curcontext" state line
|
||||
typeset -A opt_args
|
||||
|
||||
_arguments -C \
|
||||
'1: :->command' \
|
||||
'*::arg:->args'
|
||||
|
||||
case $state in
|
||||
command)
|
||||
commands=(
|
||||
'chat:Avvia l'\''agent in modalità chat'
|
||||
'server:Avvia il web server'
|
||||
'tui:Apre TUI per gestione configurazioni'
|
||||
'config:Gestione configurazioni'
|
||||
'analytics:Mostra analytics'
|
||||
'dashboard:Mostra dashboard performance'
|
||||
'version:Mostra versione'
|
||||
)
|
||||
_describe -t commands 'LangCode commands' commands
|
||||
;;
|
||||
args)
|
||||
case ${line[1]} in
|
||||
config)
|
||||
config_subcommands=(
|
||||
'list:Lista configurazioni'
|
||||
'add:Aggiungi configurazione'
|
||||
'remove:Rimuovi configurazione'
|
||||
'use:Attiva configurazione'
|
||||
'active:Mostra configurazione attiva'
|
||||
'test:Testa connessione configurazione'
|
||||
'export:Esporta configurazioni'
|
||||
'import:Importa configurazioni'
|
||||
'search:Cerca configurazioni'
|
||||
)
|
||||
_describe -t config_commands 'config commands' config_subcommands
|
||||
;;
|
||||
server)
|
||||
_arguments \
|
||||
'--port[Porta del server]:port:(8000 3000 8080 5000)' \
|
||||
'--host[Host del server]:host'
|
||||
;;
|
||||
analytics)
|
||||
_arguments \
|
||||
'--report[Genera report completo]' \
|
||||
'--clear[Cancella dati analytics]'
|
||||
;;
|
||||
dashboard)
|
||||
_arguments \
|
||||
'--clear[Cancella dati performance]'
|
||||
;;
|
||||
config)
|
||||
case ${line[2]} in
|
||||
search)
|
||||
_message 'termine di ricerca'
|
||||
;;
|
||||
add)
|
||||
_arguments \
|
||||
'--name[Nome configurazione]:name' \
|
||||
'--provider[Provider]:provider:(openai ollama openrouter anthropic google)' \
|
||||
'--model[Modello]:model' \
|
||||
'--api-key[API key]:api_key' \
|
||||
'--base-url[Base URL]:base_url' \
|
||||
'--activate[Attiva dopo la creazione]'
|
||||
;;
|
||||
remove|use|test)
|
||||
# Try to get configuration names
|
||||
local -a configs
|
||||
if (( $+commands[python3] )); then
|
||||
configs=(${(f)"$(python3 -c "
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
configs = config_manager.list_configuration_names()
|
||||
for c in configs:
|
||||
print(c)
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null)"})
|
||||
fi
|
||||
if (( ${#configs} )); then
|
||||
_describe -t configurations 'configurazioni' configs
|
||||
else
|
||||
_message 'nome configurazione'
|
||||
fi
|
||||
;;
|
||||
export)
|
||||
_arguments \
|
||||
'--format[Formato export]:format:(json yaml)' \
|
||||
'--output[File output]:filename'
|
||||
;;
|
||||
import)
|
||||
_files -g '*.json *.yaml *.yml'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_langcode "$@"
|
||||
@@ -1,88 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Bash completion for MaxClaw Agent CLI
|
||||
|
||||
_maxclaw_completions()
|
||||
{
|
||||
local cur prev commands
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
# Main commands
|
||||
commands="list search add remove set-active active test export import help"
|
||||
|
||||
# Handle main command completion
|
||||
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
||||
COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Handle subcommand-specific completions
|
||||
case "${prev}" in
|
||||
search)
|
||||
# Search term - no completion
|
||||
return 0
|
||||
;;
|
||||
add)
|
||||
# Add command options
|
||||
local add_options="--non-interactive --name --provider --model --api-key --base-url --set-active"
|
||||
COMPREPLY=( $(compgen -W "${add_options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
remove|set-active|test)
|
||||
# Configuration names - try to get from config list
|
||||
if command -v python3 &> /dev/null; then
|
||||
local configs=$(python3 -c "
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
configs = config_manager.list_configuration_names()
|
||||
print(' '.join(configs))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null)
|
||||
COMPREPLY=( $(compgen -W "${configs}" -- ${cur}) )
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
export)
|
||||
# Export options
|
||||
local export_options="--format --output"
|
||||
COMPREPLY=( $(compgen -W "${export_options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
import)
|
||||
# File completion
|
||||
COMPREPLY=( $(compgen -f -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--provider)
|
||||
# Provider options
|
||||
local providers="openai ollama openrouter"
|
||||
COMPREPLY=( $(compgen -W "${providers}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--format)
|
||||
# Format options
|
||||
local formats="json yaml"
|
||||
COMPREPLY=( $(compgen -W "${formats}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Handle option arguments
|
||||
case "${cur}" in
|
||||
-*)
|
||||
# Option completion
|
||||
local options="--non-interactive --name --provider --model --api-key --base-url --set-active --format --output"
|
||||
COMPREPLY=( $(compgen -W "${options}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _maxclaw_completions python3 config_cli.py
|
||||
complete -F _maxclaw_completions python config_cli.py
|
||||
@@ -1,79 +0,0 @@
|
||||
#compdef config_cli.py
|
||||
# Zsh completion for MaxClaw Agent CLI
|
||||
|
||||
_maxclaw() {
|
||||
local -a commands
|
||||
local curcontext="$curcontext" state line
|
||||
typeset -A opt_args
|
||||
|
||||
_arguments -C \
|
||||
'1: :->command' \
|
||||
'*::arg:->args'
|
||||
|
||||
case $state in
|
||||
command)
|
||||
commands=(
|
||||
'list:List all configurations'
|
||||
'search:Search configurations'
|
||||
'add:Add a new configuration'
|
||||
'remove:Remove a configuration'
|
||||
'set-active:Set active configuration'
|
||||
'active:Show active configuration'
|
||||
'test:Test connection to a configuration'
|
||||
'export:Export configurations to JSON/YAML'
|
||||
'import:Import configurations from JSON/YAML'
|
||||
'help:Show help'
|
||||
)
|
||||
_describe -t commands 'MaxClaw commands' commands
|
||||
;;
|
||||
args)
|
||||
case ${line[1]} in
|
||||
search)
|
||||
_message 'search term'
|
||||
;;
|
||||
add)
|
||||
_arguments \
|
||||
'--non-interactive[Non-interactive mode]' \
|
||||
'--name[Configuration name]:name' \
|
||||
'--provider[Provider]:provider:(openai ollama openrouter)' \
|
||||
'--model[Model name]:model' \
|
||||
'--api-key[API key]:api_key' \
|
||||
'--base-url[Base URL]:base_url' \
|
||||
'--set-active[Set as active configuration]'
|
||||
;;
|
||||
remove|set-active|test)
|
||||
# Try to get configuration names
|
||||
local -a configs
|
||||
if (( $+commands[python3] )); then
|
||||
configs=(${(f)"$(python3 -c "
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
configs = config_manager.list_configuration_names()
|
||||
for c in configs:
|
||||
print(c)
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null)"})
|
||||
fi
|
||||
if (( ${#configs} )); then
|
||||
_describe -t configurations 'configurations' configs
|
||||
else
|
||||
_message 'configuration name'
|
||||
fi
|
||||
;;
|
||||
export)
|
||||
_arguments \
|
||||
'--format[Export format]:format:(json yaml)' \
|
||||
'--output[Output filename]:filename'
|
||||
;;
|
||||
import)
|
||||
_files -g '*.json *.yaml *.yml'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_maxclaw "$@"
|
||||
+40
-5
@@ -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()
|
||||
@@ -42,6 +42,13 @@ def print_configurations(configs=None, title="MaxClaw Agent Configurations"):
|
||||
print(f" API Key: {'Set' if config.get('api_key') else 'Not set'}")
|
||||
base_url = config.get("base_url", "https://openrouter.ai/api/v1")
|
||||
print(f" Base URL: {base_url}")
|
||||
elif provider == "mcp":
|
||||
server_url = config.get("server_url", "Not set")
|
||||
print(f" Server URL: {server_url}")
|
||||
api_key_status = 'Set' if config.get('api_key') else 'Not set'
|
||||
print(f" API Key: {api_key_status}")
|
||||
timeout = config.get("timeout", 30)
|
||||
print(f" Timeout: {timeout}s")
|
||||
|
||||
print()
|
||||
|
||||
@@ -101,12 +108,14 @@ def add_configuration_interactive():
|
||||
print("1. OpenAI")
|
||||
print("2. Ollama")
|
||||
print("3. OpenRouter")
|
||||
print("4. MCP (Model Context Protocol)")
|
||||
|
||||
provider_choice = input("Enter choice (1-3): ").strip()
|
||||
provider_choice = input("Enter choice (1-4): ").strip()
|
||||
provider_map = {
|
||||
"1": "openai",
|
||||
"2": "ollama",
|
||||
"3": "openrouter"
|
||||
"3": "openrouter",
|
||||
"4": "mcp"
|
||||
}
|
||||
|
||||
if provider_choice not in provider_map:
|
||||
@@ -146,6 +155,32 @@ def add_configuration_interactive():
|
||||
if not base_url:
|
||||
base_url = "https://openrouter.ai/api/v1"
|
||||
config["base_url"] = base_url
|
||||
|
||||
elif provider == "mcp":
|
||||
server_url = input("MCP server URL (e.g., https://skills.sh/mcp): ").strip()
|
||||
if not server_url:
|
||||
print("Error: Server URL is required for MCP")
|
||||
return
|
||||
config["server_url"] = server_url
|
||||
|
||||
api_key = input("MCP API key (optional, press Enter to skip): ").strip()
|
||||
if api_key:
|
||||
config["api_key"] = api_key
|
||||
|
||||
timeout_input = input(f"MCP request timeout in seconds (default: 30): ").strip()
|
||||
if timeout_input:
|
||||
try:
|
||||
timeout = int(timeout_input)
|
||||
if timeout <= 0:
|
||||
print("Warning: Timeout must be positive, using default 30")
|
||||
config["timeout"] = "30"
|
||||
else:
|
||||
config["timeout"] = str(timeout)
|
||||
except ValueError:
|
||||
print("Warning: Invalid timeout value, using default 30")
|
||||
config["timeout"] = "30"
|
||||
else:
|
||||
config["timeout"] = "30"
|
||||
|
||||
# Ask if this should be the active configuration
|
||||
set_active = input("Set as active configuration? (y/N): ").strip().lower()
|
||||
@@ -405,7 +440,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
|
||||
|
||||
+35
-6
@@ -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)
|
||||
|
||||
@@ -121,7 +121,7 @@ class ConfigManager:
|
||||
if isinstance(config_data, dict):
|
||||
# Decrypt API keys for relevant providers
|
||||
provider = config_data.get("provider", "").lower()
|
||||
if provider in ["openai", "openrouter"] and "api_key" in config_data:
|
||||
if provider in ["openai", "openrouter", "mcp"] and "api_key" in config_data:
|
||||
config_data["api_key"] = self._decrypt_data(config_data["api_key"])
|
||||
|
||||
return result
|
||||
@@ -207,7 +207,7 @@ class ConfigManager:
|
||||
|
||||
# Validate provider
|
||||
provider = config.get("provider", "").lower()
|
||||
valid_providers = ["openai", "ollama", "openrouter"]
|
||||
valid_providers = ["openai", "ollama", "openrouter", "mcp"]
|
||||
|
||||
if not provider:
|
||||
errors.append("Provider is required")
|
||||
@@ -262,6 +262,35 @@ class ConfigManager:
|
||||
if base_url.endswith("/"):
|
||||
config["base_url"] = base_url.rstrip("/")
|
||||
|
||||
elif provider == "mcp":
|
||||
server_url = config.get("server_url", "").strip()
|
||||
if not server_url:
|
||||
errors.append("Server URL is required for MCP provider")
|
||||
else:
|
||||
if not server_url.startswith(("http://", "https://")):
|
||||
errors.append("MCP server URL must start with http:// or https://")
|
||||
else:
|
||||
# Remove trailing slash for consistency
|
||||
if server_url.endswith("/"):
|
||||
config["server_url"] = server_url.rstrip("/")
|
||||
|
||||
# API key is optional for MCP (some servers might not require auth)
|
||||
api_key = config.get("api_key", "").strip()
|
||||
if api_key and len(api_key) < 10:
|
||||
errors.append("MCP API key appears too short if provided")
|
||||
|
||||
# Timeout is optional, but if provided must be positive integer
|
||||
timeout = config.get("timeout")
|
||||
if timeout is not None:
|
||||
try:
|
||||
timeout_int = int(timeout)
|
||||
if timeout_int <= 0:
|
||||
errors.append("MCP timeout must be a positive integer")
|
||||
else:
|
||||
config["timeout"] = timeout_int
|
||||
except ValueError:
|
||||
errors.append("MCP timeout must be a valid integer")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def get_configurations(self) -> Dict[str, Any]:
|
||||
|
||||
+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()
|
||||
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Terminal UI con scrollback usando Curses
|
||||
Scrollback vero con frecce e PgUp/PgDn
|
||||
"""
|
||||
import sys
|
||||
import signal
|
||||
import curses
|
||||
from typing import Optional, Callable
|
||||
|
||||
try:
|
||||
import curses
|
||||
CURSES_AVAILABLE = True
|
||||
except ImportError:
|
||||
CURSES_AVAILABLE = False
|
||||
|
||||
|
||||
class CursesUI:
|
||||
"""
|
||||
Terminal UI con scrollback usando curses.
|
||||
|
||||
Features:
|
||||
- Scrollback completo con PgUp/PgDn
|
||||
- Frecce su/giù per navigare
|
||||
- Resize automatico
|
||||
- Colors (se supportati)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scrollback: int = 10000,
|
||||
prompt: str = "❯ "
|
||||
):
|
||||
"""
|
||||
Inizializza il terminale UI con curses.
|
||||
|
||||
Args:
|
||||
scrollback: Numero di righe nello scrollback buffer
|
||||
prompt: Prompt da mostrare
|
||||
"""
|
||||
if not CURSES_AVAILABLE:
|
||||
raise ImportError("curses non è disponibile su questo sistema")
|
||||
|
||||
self.scrollback = scrollback
|
||||
self.prompt = prompt
|
||||
|
||||
# Buffer per scrollback
|
||||
self.lines: list = []
|
||||
|
||||
# Callback per l'agent
|
||||
self.agent_callback: Optional[Callable[[str], str]] = None
|
||||
|
||||
def _init_colors(self, stdscr):
|
||||
"""Inizializza i colori"""
|
||||
if curses.has_colors():
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
# Colori: 1=cyan, 2=green, 3=yellow, 4=red, 5=magenta, 6=blue
|
||||
curses.init_pair(1, curses.COLOR_CYAN, -1) # Header
|
||||
curses.init_pair(2, curses.COLOR_GREEN, -1) # Agent output
|
||||
curses.init_pair(3, curses.COLOR_YELLOW, -1) # Thinking
|
||||
curses.init_pair(4, curses.COLOR_RED, -1) # Error
|
||||
curses.init_pair(5, curses.COLOR_MAGENTA, -1) # Prompt
|
||||
curses.init_pair(6, curses.COLOR_BLUE, -1) # User input
|
||||
|
||||
def _add_line(self, text: str, color: int = 2):
|
||||
"""Aggiunge una linea al buffer"""
|
||||
self.lines.append((text, color))
|
||||
# Limita la dimensione del buffer
|
||||
if len(self.lines) > self.scrollback:
|
||||
self.lines.pop(0)
|
||||
|
||||
def _draw(self, stdscr):
|
||||
"""Disegna lo schermo"""
|
||||
stdscr.clear()
|
||||
|
||||
# Ottieni dimensioni
|
||||
max_y, max_x = stdscr.getmaxyx()
|
||||
|
||||
# Header
|
||||
header = "═" * (max_x - 4)
|
||||
stdscr.addstr(0, 0, f"╔{header}╗", curses.color_pair(1) | curses.A_BOLD)
|
||||
title = " LangCode Agent - Curses UI "
|
||||
stdscr.addstr(1, (max_x - len(title)) // 2, title, curses.color_pair(1) | curses.A_BOLD)
|
||||
stdscr.addstr(2, 0, f"╚{header}╝", curses.color_pair(1) | curses.A_BOLD)
|
||||
|
||||
# Calcola area visibile
|
||||
start_row = 4
|
||||
visible_rows = max_y - start_row - 2 # -2 per status e prompt
|
||||
|
||||
# Mostra linee visibili
|
||||
total_lines = len(self.lines)
|
||||
|
||||
# Scroll offset (0 = showing latest)
|
||||
# La scrolling position va da 0 a max(0, total_lines - visible_rows)
|
||||
self.scroll_offset = max(0, total_lines - visible_rows)
|
||||
|
||||
# Disegna le linee visibili
|
||||
for i in range(min(visible_rows, total_lines)):
|
||||
line_idx = self.scroll_offset + i
|
||||
if line_idx < total_lines:
|
||||
text, color = self.lines[line_idx]
|
||||
# Tronca se troppo lunga
|
||||
display_text = text[:max_x - 2] if len(text) > max_x - 2 else text
|
||||
try:
|
||||
stdscr.addstr(start_row + i, 0, display_text, curses.color_pair(color))
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
# Status bar
|
||||
status_y = max_y - 2
|
||||
scroll_info = f"Linee: {total_lines} | Scroll: {self.scroll_offset} | PgUp/PgDn=Frecce=Scroll | q=quit | Ctrl+C=exit"
|
||||
stdscr.addstr(status_y, 0, "─" * (max_x - len(scroll_info) - 1), curses.A_DIM)
|
||||
stdscr.addstr(status_y, max_x - len(scroll_info) - 1, scroll_info, curses.A_DIM)
|
||||
|
||||
# Prompt
|
||||
stdscr.addstr(max_y - 1, 0, f"\033[1;35m{self.prompt}\033[0m ", curses.color_pair(5) | curses.A_BOLD)
|
||||
|
||||
stdscr.refresh()
|
||||
|
||||
def _handle_input(self, stdscr):
|
||||
"""Gestisce l'input"""
|
||||
curses.curs_set(1) # Mostra cursore
|
||||
|
||||
# Abilita arrow keys e PgUp/PgDn
|
||||
stdscr.keypad(True)
|
||||
|
||||
# Configura timeout per read non-blocking
|
||||
stdscr.timeout(100) # 100ms timeout
|
||||
|
||||
input_buffer = ""
|
||||
while True:
|
||||
self._draw(stdscr)
|
||||
|
||||
try:
|
||||
key = stdscr.getch()
|
||||
except curses.error:
|
||||
continue
|
||||
|
||||
if key == -1:
|
||||
# Timeout - continua
|
||||
continue
|
||||
|
||||
elif key in [curses.KEY_ENTER, 10, 13]:
|
||||
# Enter - processa comando
|
||||
if input_buffer.strip():
|
||||
return input_buffer.strip()
|
||||
input_buffer = ""
|
||||
|
||||
elif key in [curses.KEY_UP]:
|
||||
# Scroll up nel buffer
|
||||
if self.scroll_offset > 0:
|
||||
self.scroll_offset -= 1
|
||||
|
||||
elif key in [curses.KEY_DOWN]:
|
||||
# Scroll down nel buffer
|
||||
max_y, max_x = stdscr.getmaxyx()
|
||||
visible_rows = max_y - 6
|
||||
if self.scroll_offset < max(0, len(self.lines) - visible_rows):
|
||||
self.scroll_offset += 1
|
||||
|
||||
elif key in [curses.KEY_PPAGE]:
|
||||
# PgUp - scroll su di una pagina
|
||||
max_y, _ = stdscr.getmaxyx()
|
||||
self.scroll_offset = max(0, self.scroll_offset - (max_y - 6))
|
||||
|
||||
elif key in [curses.KEY_NPAGE]:
|
||||
# PgDn - scroll giu di una pagina
|
||||
max_y, max_x = stdscr.getmaxyx()
|
||||
visible_rows = max_y - 6
|
||||
self.scroll_offset = min(max(0, len(self.lines) - visible_rows),
|
||||
self.scroll_offset + visible_rows)
|
||||
|
||||
elif key == 27: # ESC
|
||||
return "/exit"
|
||||
|
||||
elif key in [3, 26]: # Ctrl+C
|
||||
return "/exit"
|
||||
|
||||
elif key in [curses.KEY_BACKSPACE, 127, 8]:
|
||||
# Backspace
|
||||
input_buffer = input_buffer[:-1]
|
||||
|
||||
elif 32 <= key <= 126:
|
||||
# Carattere stampabile
|
||||
input_buffer += chr(key)
|
||||
|
||||
# Mostra input corrente da qualche parte se necessario
|
||||
|
||||
def start(self, agent_callback: Callable[[str], str]):
|
||||
"""
|
||||
Avvia il terminale interattivo con curses.
|
||||
"""
|
||||
self.agent_callback = agent_callback
|
||||
|
||||
def handle_sigint(signum, frame):
|
||||
sys.exit(0)
|
||||
|
||||
old_handler = signal.signal(signal.SIGINT, handle_sigint)
|
||||
|
||||
try:
|
||||
curses.wrapper(self._main_loop)
|
||||
finally:
|
||||
signal.signal(signal.SIGINT, old_handler)
|
||||
|
||||
def _main_loop(self, stdscr):
|
||||
"""Loop principale curses"""
|
||||
self._init_colors(stdscr)
|
||||
curses.cbreak()
|
||||
curses.noecho()
|
||||
|
||||
# Aggiungi header
|
||||
self._add_line("╔═══════════════════════════════════════════════════════╗", 1)
|
||||
self._add_line("║ LangCode Agent - Curses UI con Scrollback ║", 1)
|
||||
self._add_line("╚═══════════════════════════════════════════════════════╝", 1)
|
||||
self._add_line("", 0)
|
||||
self._add_line("Scrivi un messaggio e premi Enter. Usa PgUp/PgDn per scrollare.", 6)
|
||||
self._add_line("Comandi: /help, /clear, /exit", 6)
|
||||
self._add_line("", 0)
|
||||
|
||||
while True:
|
||||
# Ottieni input
|
||||
stdscr.timeout(-1) # Bloccante per l'input
|
||||
command = self._handle_input(stdscr)
|
||||
|
||||
if command.lower() in ['/exit', '/quit']:
|
||||
break
|
||||
|
||||
if not command:
|
||||
continue
|
||||
|
||||
if command == '/help':
|
||||
self._add_line("╔═══════════════════════════════════════════════════════╗", 1)
|
||||
self._add_line("║ COMANDI ║", 1)
|
||||
self._add_line("╠═══════════════════════════════════════════════════════╣", 1)
|
||||
self._add_line("║ /help - Mostra questo messaggio ║", 1)
|
||||
self._add_line("║ /clear - Pulisci lo schermo ║", 1)
|
||||
self._add_line("║ /exit - Esci dall'agente ║", 1)
|
||||
self._add_line("║ PgUp - Scroll su ║", 1)
|
||||
self._add_line("║ PgDn - Scroll giu ║", 1)
|
||||
self._add_line("║ Frecce - Naviga nel scrollback ║", 1)
|
||||
self._add_line("╚═══════════════════════════════════════════════════════╝", 1)
|
||||
continue
|
||||
|
||||
if command == '/clear':
|
||||
self.lines.clear()
|
||||
continue
|
||||
|
||||
# Mostra comando utente
|
||||
self._add_line("", 0)
|
||||
self._add_line(f"👤 {command}", 6)
|
||||
|
||||
# Chiamata all'agent
|
||||
self._add_line("", 0)
|
||||
self._add_line("🤔 Pensando...", 3)
|
||||
|
||||
try:
|
||||
response = agent_callback(command)
|
||||
|
||||
# Rimuovi "thinking" e mostra risposta
|
||||
if self.lines and "🤔 Pensando..." in self.lines[-1][0]:
|
||||
self.lines.pop()
|
||||
|
||||
# Aggiungi risposta
|
||||
for line in response.split('\n'):
|
||||
self._add_line(f"🤖 {line}", 2)
|
||||
except Exception as e:
|
||||
self._add_line(f"❌ Errore: {str(e)}", 4)
|
||||
|
||||
self._add_line("", 0)
|
||||
|
||||
|
||||
def create_curses_ui(
|
||||
scrollback: int = 10000,
|
||||
prompt: str = "❯ "
|
||||
) -> CursesUI:
|
||||
"""Factory function per creare CursesUI"""
|
||||
return CursesUI(
|
||||
scrollback=scrollback,
|
||||
prompt=prompt
|
||||
)
|
||||
|
||||
|
||||
# Test standalone
|
||||
if __name__ == "__main__":
|
||||
def test_callback(text):
|
||||
return f"Ricevuto: {text}\n\nRisposta multi-linea:\n1. Prima linea\n2. Seconda\n3. Terza\n4. Quarta\n5. Quinta"
|
||||
|
||||
print("Avvio CursesUI di test...")
|
||||
ui = create_curses_ui()
|
||||
try:
|
||||
ui.start(test_callback)
|
||||
except KeyboardInterrupt:
|
||||
print("\nUscito")
|
||||
+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
|
||||
@@ -0,0 +1,271 @@
|
||||
# Da Chatbot ad Agente AI: Gap Analysis e Roadmap
|
||||
|
||||
## Stato Attuale del Progetto
|
||||
|
||||
**LangCode Agent è ora un Agente AI completo** con loop operativo, pianificazione, memoria persistente e UI moderna.
|
||||
|
||||
L'agente può:
|
||||
- Eseguire goal in autonomia attraverso il loop Pensa→Agisci→Osserva
|
||||
- Pianificare e tracciare task multipli
|
||||
- Salvare e recuperare fatti e preferenze
|
||||
- Interagire con l'utente attraverso una UI moderna a colori
|
||||
|
||||
---
|
||||
|
||||
## Chatbot vs Agente: Gap Analysis
|
||||
|
||||
| Caratteristica | Chatbot (Attuale) | Agente AI (Obiettivo) | Stato Attuale |
|
||||
|----------------|-------------------|----------------------|---------------|
|
||||
| **Input/Output** | Turn-by-turn | Goal-oriented | ✅ Goal-oriented |
|
||||
| **Planning** | Nessuno | Scomposizione in task | ✅ Integrato |
|
||||
| **Loop operativo** | Singola risposta | Pensa → Agisci → Osserva → Ripianifica | ✅ Funzionante |
|
||||
| **Memoria** | Solo cronologia sessione | A lungo termine, persistente | ✅ Integrato |
|
||||
| **Autonomia** | Dipende da input utente | Esegue autonomamente | ✅ Parziale |
|
||||
| **Tool use** | Arricchisci risposta | Agisci sull'ambiente | ✅ Funzionante |
|
||||
| **Goal tracking** | Nessuno | Tiene traccia obiettivi | ✅ Integrato nel planner |
|
||||
|
||||
---
|
||||
|
||||
## Componenti Implementati
|
||||
|
||||
### 1. Planning System (`enhancements/planning_enhancement.py`)
|
||||
- **Integrato in**: `ReActAgent`
|
||||
- **Funzionalità**: Task con priorità, dipendenze, progress tracking
|
||||
- **API**: `run()`, `run_plan()`, `get_plan_progress()`
|
||||
|
||||
### 2. Simple Memory (`enhancements/simple_memory.py`)
|
||||
- **Integrato in**: `ReActAgent`
|
||||
- **Funzionalità**: Memorizzazione fatti, preferenze, cronologia
|
||||
- **Tool**: `remember`, `recall`, `preference`, `memory_stats`
|
||||
|
||||
### 3. Enhanced Memory (`enhancements/memory_enhancement.py`)
|
||||
- **Esiste**: Vector store con FAISS, semantic search
|
||||
- **Dipendenza**: Richiede `nomic-embed-text` in Ollama
|
||||
- **Stato**: Parzialmente funzionante
|
||||
|
||||
---
|
||||
|
||||
## Cosa Manca per Diventare Agente
|
||||
|
||||
### ✅ 1. **Agent Loop (COMPLETATO)**
|
||||
Implementato in `enhancements/react_agent.py`:
|
||||
- Loop Pensa → Agisci → Osserva
|
||||
- Massimo 10 iterazioni configurabili
|
||||
- Supporto per tutti i tool built-in
|
||||
|
||||
### ✅ 2. **Goal Decomposition (COMPLETATO)**
|
||||
- `decompose_goal()` per scomposizione automatica
|
||||
- `run_plan()` per esecuzione sequenziale di goal multipli
|
||||
|
||||
### 3. **Self-Reflection**
|
||||
- L'agente deve valutare se il risultato è soddisfacente
|
||||
- Se un tool fallisce, provare approccio alternativo
|
||||
- **Stato**: Da implementare
|
||||
|
||||
### 4. **Enhanced Memory (Vector Store)**
|
||||
- Integrato ma richiede `nomic-embed-text` in Ollama
|
||||
- **Stato**: Dipendenza esterna necessaria
|
||||
|
||||
### 4. **Persistent Memory Integration**
|
||||
- Memoria deve essere доступна in ogni conversazione
|
||||
- L'agente deve poter salvare e recuperare informazioni
|
||||
|
||||
### 5. **Multi-Step Execution Context**
|
||||
- L'agente deve mantenere stato tra diverse chiamate tool
|
||||
- Non ripetere azioni già completate
|
||||
|
||||
---
|
||||
|
||||
## Roadmap di Implementazione
|
||||
|
||||
### ✅ Fase 1: Agent Loop Base (COMPLETATO)
|
||||
**Obiettivo**: Abilitare esecuzione multi-step
|
||||
|
||||
Implementato in `enhancements/react_agent.py`:
|
||||
- ThinkStep: LLM decide prossima azione (via intent detection)
|
||||
- ActStep: Esegui tool
|
||||
- ObserveStep: Leggi risultato
|
||||
- EvaluateStep: Goal raggiunto?
|
||||
- ReplanStep: Ripianifica se fallito
|
||||
|
||||
**Modifiche a `ollama_agent.py`**:
|
||||
- Aggiunto metodo `run()` con loop
|
||||
- Aggiunto parametro `max_iterations`
|
||||
- Integrato `enhanced_planner` per task tracking
|
||||
|
||||
### ✅ Fase 2: Planning Integration (COMPLETATO)
|
||||
**Obiettivo**: Planning system parte dell'agente
|
||||
|
||||
- `run()` crea e traccia task nel planner
|
||||
- `run_plan()` esegue sequenze di goal
|
||||
- `decompose_goal()` scompone goal complessi via LLM
|
||||
- Task tracciati con stato: pending->in_progress->completed/failed
|
||||
|
||||
### ✅ Fase 3: Memory Integration (COMPLETATO)
|
||||
**Obiettivo**: Rendere memoria parte dell'agente
|
||||
|
||||
- Integrato `simple_memory` in `ReActAgent`
|
||||
- Aggiunti tool: `remember`, `recall`, `preference`, `memory_stats`
|
||||
- Memoria persistente tra sessioni (file `.langcode/simple_memory.json`)
|
||||
- Metodi: `remember()`, `recall()`, `get_memory_stats()`
|
||||
- Totale: 10 tool disponibili (6 built-in + 4 memory)
|
||||
|
||||
### ✅ Fase 4: Enhanced CLI (COMPLETATO)
|
||||
**Obiettivo**: Migliorare l'esperienza utente con UI moderna
|
||||
|
||||
- `enhanced_cli.py`: UI moderna con prompt_toolkit
|
||||
- **Colors**: User (cyan), Agent (green), System (yellow), Error (red)
|
||||
- **History**: Cronologia persistente in `.langcode/.cli_history`
|
||||
- **Autocomplete**: Comandi `/help`, `/status`, `/clear`, `/theme`, `/exit`
|
||||
- **4 Temi**: default, dark, light, monokai
|
||||
- **Syntax Highlighting**: Code blocks con pygments
|
||||
- **Keyboard shortcuts**: Ctrl+A/E, Ctrl+K/U, Ctrl+W
|
||||
- **Toggle**: `/timestamps`, `/syntax`
|
||||
- Integrato in `langcode.py chat`
|
||||
|
||||
---
|
||||
|
||||
## Implementazione Proposta: Nuovo `ReActAgent`
|
||||
|
||||
```python
|
||||
# enhancements/react_agent.py (nuovo)
|
||||
|
||||
class ReActAgent:
|
||||
"""
|
||||
Agente con Loop Pensa→Agisci→Osserva→Ripianifica
|
||||
"""
|
||||
|
||||
def __init__(self, llm, tools, max_iterations=10):
|
||||
self.llm = llm
|
||||
self.tools = {t.name: t for t in tools}
|
||||
self.max_iterations = max_iterations
|
||||
self.planner = EnhancedPlanner() # Esistente
|
||||
self.memory = SimplePersistentMemory() # Esistente
|
||||
|
||||
def run(self, goal: str) -> Dict:
|
||||
"""
|
||||
Esegue un goal attraverso il loop ReAct.
|
||||
"""
|
||||
context = f"Goal: {goal}\n\n"
|
||||
|
||||
for i in range(self.max_iterations):
|
||||
# 1. THINK: LLM decide prossima azione
|
||||
thought = self._think(context, goal)
|
||||
|
||||
# 2. ACT: Esegui tool se necessario
|
||||
if thought.get("action"):
|
||||
result = self._act(thought["action"], thought["args"])
|
||||
context += f"\nAzione: {thought['action']}\nRisultato: {result}\n"
|
||||
|
||||
# Salva in memoria se rilevante
|
||||
if thought["action"] in ["read_file", "execute"]:
|
||||
self.memory.add_fact(f"Azione completata: {thought['action']}")
|
||||
|
||||
# 3. OBSERVE & EVALUATE
|
||||
if self._goal_achieved(context, goal):
|
||||
return {"success": True, "result": context}
|
||||
|
||||
return {"success": False, "result": context, "error": "Max iterations"}
|
||||
|
||||
def _think(self, context, goal) -> Dict:
|
||||
"""LLM decide prossima azione basata su contesto"""
|
||||
# Prompt ingegnerizzato per decision-making
|
||||
...
|
||||
|
||||
def _act(self, action_name, args) -> str:
|
||||
"""Esegue azione tramite tool"""
|
||||
if action_name in self.tools:
|
||||
return self.tools[action_name].invoke(args)
|
||||
return f"Tool {action_name} non trovato"
|
||||
|
||||
def _goal_achieved(self, context, goal) -> bool:
|
||||
"""Valuta se goal è stato raggiunto"""
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utilizzo Attuale
|
||||
|
||||
### Test Semplice:
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
./venv/bin/python -c "
|
||||
from enhancements.react_agent import ReActAgent
|
||||
from ollama_agent import get_builtin_tools
|
||||
from langchain_ollama import ChatOllama
|
||||
|
||||
llm = ChatOllama(model='qwen3.5:4b', base_url='http://192.168.0.50:11434')
|
||||
agent = ReActAgent(llm=llm, tools=get_builtin_tools())
|
||||
|
||||
result = agent.run('Che OS stai usando?')
|
||||
print(result['result'])
|
||||
"
|
||||
```
|
||||
|
||||
### Test con Piano (multi-goal):
|
||||
```python
|
||||
goals = [
|
||||
'Che OS stai usando?',
|
||||
'Elenca i file',
|
||||
'Leggi requirements.txt'
|
||||
]
|
||||
result = agent.run_plan(goals, 'test_plan')
|
||||
print(f\"Completati: {result['completed']}/{result['total_goals']}\")
|
||||
```
|
||||
|
||||
### Verifica Task nel Planner:
|
||||
```python
|
||||
print(agent.get_plan_progress())
|
||||
# {'total_tasks': 3, 'plans': 1}
|
||||
```
|
||||
|
||||
### Test Memory:
|
||||
```python
|
||||
# Ricorda un fatto
|
||||
agent.remember("L'utente preferisce il tema scuro", "preferenza")
|
||||
|
||||
# Cerca fatti
|
||||
print(agent.recall("tema"))
|
||||
# Trovati 1 ricordi:
|
||||
# - L'utente preferisce il tema scuro (preferenza)
|
||||
|
||||
# Statistiche memoria
|
||||
print(agent.get_memory_stats())
|
||||
# {'total_conversations': 0, 'total_facts': 11, 'total_preferences': 1, ...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commit Log
|
||||
|
||||
| Data | Commit | Descrizione |
|
||||
|------|--------|-------------|
|
||||
| 24 Mar 2026 | `e5ce70c` | fix: migliora intent detection e parsing message dict |
|
||||
| 24 Mar 2026 | `1c778a` | feat: aggiungi ReAct Agent Loop |
|
||||
| 24 Mar 2026 | `f847b92` | feat: integra Planning system nel ReActAgent |
|
||||
| 24 Mar 2026 | `fc60fc1` | feat: integra Simple Memory nel ReActAgent |
|
||||
| 24 Mar 2026 | `4a0342d` | feat: aggiungi EnhancedCLI con prompt_toolkit |
|
||||
| 24 Mar 2026 | `9fa564e` | feat: potenzia EnhancedCLI con funzionalità avanzate |
|
||||
|
||||
---
|
||||
|
||||
## Conclusione
|
||||
|
||||
**Il progetto è progredito da chatbot ad agente con UI moderna.**
|
||||
|
||||
Tutte le fasi della roadmap sono completate:
|
||||
- ✅ Agent Loop (Pensa→Agisci→Osserva)
|
||||
- ✅ Planning Integration
|
||||
- ✅ Memory Integration
|
||||
- ✅ Enhanced CLI (UI moderna con colors, history, autocomplete, temi)
|
||||
|
||||
Il progetto è ora un **Agente AI completo** con:
|
||||
- Loop operativo autonomo
|
||||
- Pianificazione task
|
||||
- Memoria persistente
|
||||
- UI moderna e user-friendly
|
||||
|
||||
---
|
||||
|
||||
*Documento aggiornato: 24 Marzo 2026*
|
||||
@@ -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.
|
||||
@@ -0,0 +1,50 @@
|
||||
# MCP Integration Design for LangCode Agent
|
||||
|
||||
## Section 1: MCP Client and Tool Conversion (Previously discussed and validated)
|
||||
|
||||
## Section 2: Integration with LangCode Agent Architecture (Previously discussed and validated)
|
||||
|
||||
## Section 3: Specific Implementation Details, Data Flow, and Error Handling
|
||||
|
||||
### Implementation Files to Create/Modify:
|
||||
1. **New file**: `mcp_client.py` - Contains the MCPClient class and tool conversion functions
|
||||
2. **Modified file**: `main.py` - Add `get_mcp_tools()` function and integrate MCP tools into `get_custom_tools()`
|
||||
3. **Modified file**: `config_manager.py` - Add methods to manage MCP server configurations (if not already present)
|
||||
4. **Modified file**: `config_cli.py` - Add CLI commands for MCP server management (list, add, remove, use, etc.)
|
||||
|
||||
### Data Flow:
|
||||
1. **Startup**: When the agent initializes, `get_custom_tools()` is called
|
||||
2. **MCP Tool Discovery**: `get_mcp_tools()` reads all configured MCP servers from config_manager
|
||||
3. **Server Connection**: For each MCP server, an MCPClient instance connects to the server URL
|
||||
4. **Tool Enumeration**: The client calls the MCP server's `tools/list` endpoint to get available tools
|
||||
5. **Tool Conversion**: Each tool specification is converted to a LangChain Tool with:
|
||||
- Same name and description as the MCP tool
|
||||
- A function that serializes arguments and calls the MCP server's `tools/call` endpoint
|
||||
- Proper error handling for network issues, server errors, or invalid responses
|
||||
6. **Tool Aggregation**: All MCP-derived tools are combined with existing Proxmox and provider tools
|
||||
7. **Agent Initialization**: The DeepAgent is created with the complete toolset
|
||||
|
||||
### Error Handling Strategy:
|
||||
- **Connection Failures**: If an MCP server is unreachable during startup, log a warning and continue (don't fail agent initialization)
|
||||
- **Tool Call Failures**: If a tool call to an MCP server fails, return a descriptive error message as the tool result
|
||||
- **Schema Validation**: Validate tool arguments against the MCP tool's input schema before making the call
|
||||
- **Timeouts**: Implement reasonable timeouts for MCP server connections and calls
|
||||
- **Retry Logic**: Consider implementing retry logic for transient network failures
|
||||
|
||||
### Testing Approach:
|
||||
1. **Unit Tests**:
|
||||
- Test MCPClient connection and tool listing with mock servers
|
||||
- Test tool conversion function with various tool specifications
|
||||
- Test error handling scenarios (timeouts, invalid responses, etc.)
|
||||
|
||||
2. **Integration Tests**:
|
||||
- Test with a real MCP server (could be a local test server or skills.sh if accessible)
|
||||
- Verify that tools appear in the agent's tool list and can be invoked successfully
|
||||
- Test end-to-end flows: user asks agent to use an MCP-derived tool, agent executes it correctly
|
||||
|
||||
3. **Manual Testing**:
|
||||
- Add an MCP server configuration pointing to skills.sh (if it offers MCP endpoints)
|
||||
- Verify that skills from skills.sh appear as available tools in the agent
|
||||
- Test invoking a few different skills to ensure they work correctly
|
||||
|
||||
Are these implementation details, data flow, error handling, and testing approaches correct and complete? Do you see any gaps or areas that need adjustment?
|
||||
@@ -0,0 +1,300 @@
|
||||
# Skill Management Design for LangCode Agent
|
||||
|
||||
## Overview
|
||||
|
||||
The Skill Management feature enables LangCode Agent to discover, install, uninstall, and manage agent skills from skill registries like skills.sh. This functionality allows users to extend the agent's capabilities by leveraging community-developed skills that encapsulate domain expertise, specialized tools, and reusable workflows.
|
||||
|
||||
## Architecture
|
||||
|
||||
The skill management system consists of three main components:
|
||||
|
||||
1. **Skill Manager (`skill_manager.py`)**:
|
||||
- Handles all interactions with skill registries (primarily skills.sh CLI)
|
||||
- Provides functions for searching, installing, uninstalling, listing, and getting details about skills
|
||||
- Includes fallback mechanisms when the skills.sh CLI is not available
|
||||
- Manages the local skills directory (`~/.langcode/skills/`)
|
||||
|
||||
2. **Skill Tools (`main.py`)**:
|
||||
- Converts skill management functions into LangChain Tool objects
|
||||
- Makes skill management capabilities available to the LLM agent
|
||||
- Includes tools for: search_skills, install_skill, uninstall_skill, list_installed_skills, get_skill_details
|
||||
|
||||
3. **CLI Integration (`main.py` and `config_cli.py`)**:
|
||||
- Adds `/skills` subcommand to the main LangCode CLI
|
||||
- Provides direct access to skill management functions via terminal commands
|
||||
- Integrates with the existing configuration and command processing systems
|
||||
|
||||
## CLI Usage
|
||||
|
||||
The skill management functionality is accessible through the `/skills` subcommand:
|
||||
|
||||
```
|
||||
langcode skills search <query> [--limit <n>]
|
||||
langcode skills install <skill_name> [--version <version>]
|
||||
langcode skills uninstall <skill_name>
|
||||
langcode skills list
|
||||
langcode skills details <skill_name>
|
||||
```
|
||||
|
||||
### Command Details
|
||||
|
||||
#### Search for Skills
|
||||
```
|
||||
langcode skills search "web scraping" --limit 5
|
||||
```
|
||||
Searches for skills matching the query and displays results with name, description, version, and author information.
|
||||
|
||||
#### Install a Skill
|
||||
```
|
||||
langcode skills install brave-search
|
||||
```
|
||||
Installs the specified skill from the skill registry. Optionally specify a version with `--version`.
|
||||
|
||||
#### Uninstall a Skill
|
||||
```
|
||||
langcode skills uninstall brave-search
|
||||
```
|
||||
Removes the specified skill from the local skills installation.
|
||||
|
||||
#### List Installed Skills
|
||||
```
|
||||
langcode skills list
|
||||
```
|
||||
Displays all currently installed skills with their names, descriptions, versions, authors, and installation paths.
|
||||
|
||||
#### Get Skill Details
|
||||
```
|
||||
langcode skills details brave-search
|
||||
```
|
||||
Shows detailed information about a specific skill including description, version, author, category, repository, and homepage.
|
||||
|
||||
## Natural Language Usage
|
||||
|
||||
Once the LangCode Agent is running, users can interact with skill management features using natural language:
|
||||
|
||||
### Examples:
|
||||
- "Search for skills related to web scraping"
|
||||
- "Install the brave-search skill"
|
||||
- "What skills do I have installed?"
|
||||
- "Show me details about the mcp-builder skill"
|
||||
- "Uninstall the github-api skill"
|
||||
|
||||
The agent will use its skill management tools to perform these operations and provide feedback on the results.
|
||||
|
||||
## Skill Management Tools Available to the Agent
|
||||
|
||||
When the agent initializes with skill management enabled, it gains access to these LangChain Tools:
|
||||
|
||||
1. **search_skills**: Search for agent skills in the skills.sh registry
|
||||
- Input: query (string), limit (integer, optional, default 10)
|
||||
- Output: Formatted list of matching skills with details
|
||||
|
||||
2. **install_skill**: Install a skill from the skills.sh registry
|
||||
- Input: skill_name (string)
|
||||
- Output: Success or error message
|
||||
|
||||
3. **uninstall_skill**: Uninstall an installed skill
|
||||
- Input: skill_name (string)
|
||||
- Output: Success or error message
|
||||
|
||||
4. **list_installed_skills**: List all currently installed skills
|
||||
- Input: None
|
||||
- Output: Formatted list of installed skills with details
|
||||
|
||||
5. **get_skill_details**: Get detailed information about a specific skill
|
||||
- Input: skill_name (string)
|
||||
- Output: Detailed skill information
|
||||
|
||||
## Workflow Examples
|
||||
|
||||
### Discovering and Installing a New Skill
|
||||
1. User: "Search for skills about GitHub integration"
|
||||
2. Agent: Uses search_skills tool to find relevant skills
|
||||
3. Agent: Returns list of GitHub-related skills (e.g., github-api, github-actions)
|
||||
4. User: "Install the github-api skill"
|
||||
5. Agent: Uses install_skill tool to install the skill
|
||||
6. Agent: Confirms successful installation
|
||||
7. User: Now the agent can use GitHub API tools through the newly installed skill
|
||||
|
||||
### Managing Existing Skills
|
||||
1. User: "What skills do I have installed?"
|
||||
2. Agent: Uses list_installed_skills tool to show current skills
|
||||
3. User: "Show me details about the brave-search skill"
|
||||
4. Agent: Uses get_skill_details tool to display comprehensive information
|
||||
5. User: "Uninstall the brave-search skill"
|
||||
6. Agent: Uses uninstall_skill tool to remove the skill
|
||||
7. Agent: Confirms successful removal
|
||||
|
||||
## Technical Implementation Details
|
||||
|
||||
### Skill Manager Class
|
||||
The `SkillManager` class in `skill_manager.py` provides the core functionality:
|
||||
|
||||
- **search_skills(query, limit)**: Uses `skills search` CLI command with JSON output
|
||||
- **get_skill_details(skill_name)**: Uses `skills show` CLI command with JSON output
|
||||
- **install_skill(skill_name, version)**: Uses `skills add` CLI command
|
||||
- **uninstall_skill(skill_name)**: Uses `skills rm` CLI command
|
||||
- **list_installed_skills()**: Uses `skills list` CLI command
|
||||
- **skill_is_installed(skill_name)**: Checks if a skill is in the installed list
|
||||
- **get_skill_path(skill_name)**: Returns filesystem path to installed skill
|
||||
|
||||
### Fallback Mechanisms
|
||||
When the skills.sh CLI is not available:
|
||||
- Search functions return empty results
|
||||
- Installation/uninstallation functions return error messages
|
||||
- Local skills directory checking provides basic installed skill information
|
||||
|
||||
### Integration with Agent System
|
||||
- Skill tools are added to the agent's toolset in `get_custom_tools()` alongside MCP, Proxmox, and provider tools
|
||||
- The agent's system prompt is updated to inform users about skill management capabilities
|
||||
- CLI commands are integrated into the main command processing flow in `process_command()`
|
||||
|
||||
## Configuration
|
||||
|
||||
Skill management uses the local skills directory by default:
|
||||
- Location: `~/.langcode/skills/`
|
||||
- Automatically created when first needed
|
||||
- Each skill is installed in its own subdirectory
|
||||
- Skills consist of a SKILL.md file and any supporting files/scripts
|
||||
|
||||
No additional configuration is required to use skill management - it works out of the box with the skills.sh CLI when available.
|
||||
|
||||
## Dependencies
|
||||
|
||||
The skill management feature requires:
|
||||
1. **skills.sh CLI** (primary method): For full functionality including search, install, uninstall
|
||||
- Install via: `npm install -g @openai/codex` (includes skills.sh)
|
||||
- Or follow skills.sh installation instructions
|
||||
|
||||
2. **Python subprocess module**: For executing skills.sh commands
|
||||
- Included in standard Python library
|
||||
|
||||
When skills.sh is not available:
|
||||
- Basic functionality is limited to checking locally installed skills
|
||||
- Search, install, and uninstall operations will return appropriate error messages
|
||||
- The system gracefully degrades rather than failing completely
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Skill installation executes code from external sources - users should review skills before installing
|
||||
- The skill manager does not automatically execute skill code - it only manages installation
|
||||
- Skills are installed to a local directory under user control
|
||||
- Users should only install skills from trusted sources
|
||||
- The system does not grant additional permissions to installed skills beyond what the agent already has
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "skills.sh command not found"
|
||||
- Install skills.sh using: `npm install -g @openai/codex`
|
||||
- Ensure the npm global bin directory is in your PATH
|
||||
- Verify installation with: `skills --version`
|
||||
|
||||
### Skill installation fails
|
||||
- Check your internet connection
|
||||
- Verify the skill name is correct
|
||||
- Some skills may have specific dependencies or requirements
|
||||
- Try installing without specifying a version to get the latest compatible version
|
||||
|
||||
### No skills found in search
|
||||
- Try different or more general search terms
|
||||
- Check that skills.sh is properly installed and authenticated
|
||||
- Some skills may not be publicly available or may require specific access
|
||||
|
||||
### Skill not found after installation
|
||||
- Verify installation completed successfully
|
||||
- Check the local skills directory (`~/.langcode/skills/`)
|
||||
- Use `/skills list` to confirm the skill appears in the installed list
|
||||
- Restart the agent if necessary to reload available tools
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Search before installing**: Use search to find the most relevant skill for your needs
|
||||
2. **Check skill details**: Review description, version, and author before installing
|
||||
3. **Start with popular skills**: Well-maintained skills with good documentation are safer choices
|
||||
4. **Keep skills updated**: Periodically check for updates to installed skills
|
||||
5. **Remove unused skills**: Keep your skills directory clean by uninstalling skills you no longer need
|
||||
6. **Review skill content**: For security, examine SKILL.md and supporting files before relying on a skill
|
||||
7. **Combine with MCP**: Many skills work best when combined with MCP servers for external service access
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements to the skill management system include:
|
||||
- Integration with multiple skill registries beyond skills.sh
|
||||
- Skill version management and upgrading
|
||||
- Skill dependency resolution
|
||||
- Skill categorization and tagging systems
|
||||
- Offline skill distribution and sharing
|
||||
- Skill validation and safety scanning
|
||||
- Integration with agent configuration for skill-specific settings
|
||||
- Web interface for skill management
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
$ langcode
|
||||
🚀 Initializing LangCode Agent...
|
||||
✅ Agent initialized successfully!
|
||||
|
||||
🔧 Available tools:
|
||||
• Provider tools: list models, test connections, set defaults
|
||||
• Proxmox tools: manage VMs, snapshots, resources
|
||||
• MCP tools: accessing external services and skills registries (like skills.sh)
|
||||
• Skill management tools: discovering, installing, and managing agent skills
|
||||
• Standard tools: planning, filesystem, shell, subagents
|
||||
|
||||
💬 You can now interact with the agent.
|
||||
📌 Comandi speciali (prefissati con /):
|
||||
/help - Mostra i comandi disponibili
|
||||
/status - Mostra lo stato del sistema
|
||||
/config - Gestisci le configurazioni
|
||||
/skills - Gestione skill agent
|
||||
/exit - Esci dall'agente
|
||||
|
||||
💡 Per chattare, scrivi semplicemente il tuo messaggio.
|
||||
|
||||
👤 You: Search for skills about web scraping
|
||||
🤖 LangCode:
|
||||
Found 3 skills:
|
||||
1. brave-search
|
||||
Description: Helps you structured brave web search queries and summarizer workflows
|
||||
Version: 1.0.0
|
||||
Author: skillmd.ai
|
||||
|
||||
2. scrape-web
|
||||
Description: Skill for scraping web content with various options
|
||||
Version: 0.5.0
|
||||
Author: webtools.dev
|
||||
|
||||
3. html-parser
|
||||
Description: Parse and extract data from HTML documents
|
||||
Version: 2.1.0
|
||||
Author: dataparsers.org
|
||||
|
||||
👤 You: Install the brave-search skill
|
||||
🤖 LangCode: ✅ Skill 'brave-search' installed successfully
|
||||
|
||||
👤 You: What skills do I have installed?
|
||||
🤖 LangCode:
|
||||
Installed skills (1):
|
||||
1. brave-search
|
||||
Description: Helps you structured brave web search queries and summarizer workflows
|
||||
Version: 1.0.0
|
||||
Author: skillmd.ai
|
||||
Path: /home/user/.langcode/skills/brave-search
|
||||
|
||||
👤 You: Show me details about the brave-search skill
|
||||
🤖 LangCode:
|
||||
Skill Details: brave-search
|
||||
==================================================
|
||||
Description: Helps you structured brave web search queries and summarizer workflows
|
||||
Version: 1.0.0
|
||||
Author: skillmd.ai
|
||||
Category: development
|
||||
Repository: https://github.com/skillmd/brave-search
|
||||
Homepage: https://skillmd.ai/how-to-build/brave-search-13/
|
||||
|
||||
👤 You: Now I want to scrape a website and summarize the content
|
||||
🤖 LangCode: I can help you with that! First, let me use the brave-search skill to search for information about the website you're interested in. What specific website or topic would you like me to research?
|
||||
```
|
||||
|
||||
This demonstrates the complete workflow from discovering skills to installing them and using their capabilities through natural language interaction with the LangCode Agent.
|
||||
+734
@@ -0,0 +1,734 @@
|
||||
"""
|
||||
Enhanced CLI for LangCode Agent
|
||||
Provides modern terminal UI with colors, history, autocomplete, and more.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from typing import Optional, List, Dict, Any, Callable
|
||||
from datetime import datetime
|
||||
|
||||
# prompt_toolkit imports
|
||||
from prompt_toolkit import PromptSession, HTML
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.completion import WordCompleter, NestedCompleter, PathCompleter
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
from prompt_toolkit.cursor_shapes import CursorShape
|
||||
|
||||
# For syntax highlighting
|
||||
try:
|
||||
from pygments.lexers import get_lexer_by_name, get_lexer_for_filename
|
||||
from pygments.token import Token
|
||||
from pygments import highlight as pygments_highlight
|
||||
PYGMENTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYGMENTS_AVAILABLE = False
|
||||
get_lexer_by_name = None
|
||||
get_lexer_for_filename = None
|
||||
pygments_highlight = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# STYLING
|
||||
# =============================================================================
|
||||
|
||||
class Colors:
|
||||
"""ANSI color codes for terminal output"""
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
ITALIC = "\033[3m"
|
||||
|
||||
BLACK = "\033[30m"
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
BLUE = "\033[34m"
|
||||
MAGENTA = "\033[35m"
|
||||
CYAN = "\033[36m"
|
||||
WHITE = "\033[37m"
|
||||
|
||||
BRIGHT_BLACK = "\033[90m"
|
||||
BRIGHT_RED = "\033[91m"
|
||||
BRIGHT_GREEN = "\033[92m"
|
||||
BRIGHT_YELLOW = "\033[93m"
|
||||
BRIGHT_BLUE = "\033[94m"
|
||||
BRIGHT_MAGENTA = "\033[95m"
|
||||
BRIGHT_CYAN = "\033[96m"
|
||||
BRIGHT_WHITE = "\033[97m"
|
||||
|
||||
|
||||
class Theme:
|
||||
"""Color themes for the CLI"""
|
||||
|
||||
@staticmethod
|
||||
def get_style(theme_name: str) -> Style:
|
||||
"""Get prompt_toolkit style from theme name"""
|
||||
themes = {
|
||||
"default": {
|
||||
"user": "ansicyan bold",
|
||||
"agent": "ansigreen",
|
||||
"system": "ansiyellow",
|
||||
"error": "ansired bold",
|
||||
"info": "ansiblue",
|
||||
"muted": "ansiblack",
|
||||
"prompt": "ansimagenta bold",
|
||||
"code": "ansiyellow",
|
||||
"url": "ansiblue underline",
|
||||
},
|
||||
"dark": {
|
||||
"user": "cyan bold",
|
||||
"agent": "lightgreen",
|
||||
"system": "yellow",
|
||||
"error": "lightred bold",
|
||||
"info": "lightblue",
|
||||
"muted": "gray",
|
||||
"prompt": "lightmagenta bold",
|
||||
"code": "yellow",
|
||||
"url": "lightblue underline",
|
||||
},
|
||||
"light": {
|
||||
"user": "blue bold",
|
||||
"agent": "darkgreen",
|
||||
"system": "darkyellow",
|
||||
"error": "darkred bold",
|
||||
"info": "darkblue",
|
||||
"muted": "gray",
|
||||
"prompt": "darkmagenta bold",
|
||||
"code": "darkyellow",
|
||||
"url": "darkblue underline",
|
||||
},
|
||||
"monokai": {
|
||||
"user": "cyan bold",
|
||||
"agent": "green",
|
||||
"system": "yellow",
|
||||
"error": "red bold",
|
||||
"info": "blue",
|
||||
"muted": "gray",
|
||||
"prompt": "magenta bold",
|
||||
"code": "yellow",
|
||||
"url": "cyan underline",
|
||||
},
|
||||
}
|
||||
theme_dict = themes.get(theme_name, themes["default"])
|
||||
return Style.from_dict(theme_dict)
|
||||
|
||||
@staticmethod
|
||||
def get_ansi_theme(theme_name: str) -> Dict[str, str]:
|
||||
"""Get ANSI color theme for non-prompt_toolkit output"""
|
||||
themes = {
|
||||
"default": {
|
||||
"user": f"{Colors.CYAN}{Colors.BOLD}",
|
||||
"agent": Colors.GREEN,
|
||||
"system": Colors.YELLOW,
|
||||
"error": f"{Colors.RED}{Colors.BOLD}",
|
||||
"info": Colors.BLUE,
|
||||
"muted": Colors.BRIGHT_BLACK,
|
||||
"prompt": f"{Colors.MAGENTA}{Colors.BOLD}",
|
||||
"code": Colors.YELLOW,
|
||||
"reset": Colors.RESET,
|
||||
},
|
||||
"dark": {
|
||||
"user": Colors.BRIGHT_CYAN,
|
||||
"agent": Colors.BRIGHT_GREEN,
|
||||
"system": Colors.BRIGHT_YELLOW,
|
||||
"error": f"{Colors.BRIGHT_RED}{Colors.BOLD}",
|
||||
"info": Colors.BRIGHT_BLUE,
|
||||
"muted": Colors.BRIGHT_BLACK,
|
||||
"prompt": Colors.BRIGHT_MAGENTA,
|
||||
"code": Colors.YELLOW,
|
||||
"reset": Colors.RESET,
|
||||
},
|
||||
"light": {
|
||||
"user": Colors.BLUE,
|
||||
"agent": "\033[32m", # darkgreen
|
||||
"system": "\033[33m", # darkyellow
|
||||
"error": "\033[31m", # darkred
|
||||
"info": "\033[34m", # darkblue
|
||||
"muted": "\033[90m", # brightblack/gray
|
||||
"prompt": "\033[35m", # darkmagenta
|
||||
"code": "\033[33m",
|
||||
"reset": Colors.RESET,
|
||||
},
|
||||
"monokai": {
|
||||
"user": Colors.CYAN,
|
||||
"agent": Colors.GREEN,
|
||||
"system": Colors.YELLOW,
|
||||
"error": Colors.RED,
|
||||
"info": Colors.BLUE,
|
||||
"muted": Colors.BRIGHT_BLACK,
|
||||
"prompt": Colors.MAGENTA,
|
||||
"code": Colors.YELLOW,
|
||||
"reset": Colors.RESET,
|
||||
},
|
||||
}
|
||||
return themes.get(theme_name, themes["default"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SYNTAX HIGHLIGHTING
|
||||
# =============================================================================
|
||||
|
||||
class SyntaxHighlighter:
|
||||
"""Simple syntax highlighter for code blocks"""
|
||||
|
||||
# Language aliases
|
||||
LANG_ALIASES = {
|
||||
"py": "python",
|
||||
"js": "javascript",
|
||||
"ts": "typescript",
|
||||
"rb": "ruby",
|
||||
"sh": "bash",
|
||||
"zsh": "bash",
|
||||
"ps": "powershell",
|
||||
"yml": "yaml",
|
||||
"md": "markdown",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_lexer(cls, lang: str):
|
||||
"""Get pygments lexer for language"""
|
||||
if not PYGMENTS_AVAILABLE:
|
||||
return None
|
||||
|
||||
lang = cls.LANG_ALIASES.get(lang.lower(), lang.lower())
|
||||
try:
|
||||
return get_lexer_by_name(lang)
|
||||
except:
|
||||
try:
|
||||
return get_lexer_for_filename(f"file.{lang}")
|
||||
except:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def highlight(cls, code: str, lang: str = "text") -> str:
|
||||
"""Apply syntax highlighting to code"""
|
||||
if not PYGMENTS_AVAILABLE:
|
||||
return code
|
||||
|
||||
lexer = cls.get_lexer(lang)
|
||||
if not lexer:
|
||||
return code
|
||||
|
||||
from pygments import highlight as pygments_highlight
|
||||
from pygments.formatters import Terminal256Formatter
|
||||
formatter = Terminal256Formatter()
|
||||
|
||||
try:
|
||||
highlighted = pygments_highlight(code, lexer, formatter)
|
||||
return highlighted.rstrip()
|
||||
except:
|
||||
return code
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN ENHANCED CLI CLASS
|
||||
# =============================================================================
|
||||
|
||||
class EnhancedCLI:
|
||||
"""
|
||||
Enhanced Command Line Interface for LangCode Agent.
|
||||
|
||||
Features:
|
||||
- Colored input/output with themes
|
||||
- Command history (persistent)
|
||||
- Autocomplete for commands and paths
|
||||
- Multi-line input support
|
||||
- Code syntax highlighting
|
||||
- Mouse support for scrolling
|
||||
- Copy/paste support
|
||||
- Signal handling (Ctrl+C, Ctrl+L)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
history_file: str = "./.langcode/.cli_history",
|
||||
theme: str = "default",
|
||||
welcome_message: Optional[str] = None,
|
||||
show_timestamps: bool = True,
|
||||
enable_mouse: bool = True,
|
||||
enable_syntax_highlight: bool = True,
|
||||
terminal_width: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Initialize Enhanced CLI.
|
||||
|
||||
Args:
|
||||
history_file: Path to persist command history
|
||||
theme: Color theme (default, dark, light, monokai)
|
||||
welcome_message: Initial message to show
|
||||
show_timestamps: Show timestamps in output
|
||||
enable_mouse: Enable mouse for scrolling
|
||||
enable_syntax_highlight: Highlight code blocks
|
||||
terminal_width: Force terminal width
|
||||
"""
|
||||
self.history_file = history_file
|
||||
self.theme = theme
|
||||
self.show_timestamps = show_timestamps
|
||||
self.enable_mouse = enable_mouse
|
||||
self.enable_syntax_highlight = enable_syntax_highlight and PYGMENTS_AVAILABLE
|
||||
|
||||
# Get terminal size
|
||||
self.terminal_width = terminal_width or shutil.get_terminal_size().columns
|
||||
|
||||
# ANSI theme for non-prompt output
|
||||
self.ansi_theme = Theme.get_ansi_theme(theme)
|
||||
|
||||
# Welcome message
|
||||
self.welcome_message = welcome_message or self._default_welcome()
|
||||
|
||||
# Ensure history directory exists
|
||||
os.makedirs(os.path.dirname(history_file) or ".", exist_ok=True)
|
||||
|
||||
# Command definitions
|
||||
self.commands = {
|
||||
"/help": "Mostra aiuto",
|
||||
"/status": "Stato sistema",
|
||||
"/clear": "Pulisci schermo",
|
||||
"/theme": "Cicla tra i temi",
|
||||
"/history": "Mostra cronologia",
|
||||
"/theme-default": "Tema default",
|
||||
"/theme-dark": "Tema dark",
|
||||
"/theme-light": "Tema light",
|
||||
"/theme-monokai": "Tema monokai",
|
||||
"/timestamps": "Toggle timestamps",
|
||||
"/syntax": "Toggle syntax highlighting",
|
||||
"/width": "Show terminal width",
|
||||
"/exit": "Esci",
|
||||
"/quit": "Esci",
|
||||
}
|
||||
|
||||
# Path completer for commands that need paths
|
||||
self.path_completer = PathCompleter()
|
||||
|
||||
# Create key bindings
|
||||
self.key_bindings = self._create_key_bindings()
|
||||
|
||||
# Create prompt session
|
||||
self.session = self._create_session()
|
||||
|
||||
# Current theme name
|
||||
self.current_theme = theme
|
||||
|
||||
# Statistics
|
||||
self.stats = {
|
||||
"commands_executed": 0,
|
||||
"session_start": datetime.now(),
|
||||
}
|
||||
|
||||
# Callback for Ctrl+L (clear)
|
||||
self._on_clear: Optional[Callable] = None
|
||||
|
||||
def _default_welcome(self) -> str:
|
||||
"""Generate default welcome message"""
|
||||
theme = self.ansi_theme
|
||||
w = self.terminal_width
|
||||
|
||||
# Box drawing
|
||||
top = f"{theme['prompt']}╔{'═' * (w - 4)}╗{theme['reset']}"
|
||||
bottom = f"{theme['prompt']}╚{'═' * (w - 4)}╝{theme['reset']}"
|
||||
|
||||
lines = [
|
||||
f"{theme['prompt']}║{theme['reset']} {theme['agent']}{Colors.BOLD}LangCode Agent - Enhanced CLI{theme['reset']}" + " " * (w - 43) + f"{theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']}" + " " * (w - 2) + f"{theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']} {theme['system']}Comandi:{theme['reset']}" + " " * (w - 17) + f"{theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']} {theme['user']}/help{theme['reset']} - Mostra aiuto {theme['user']}/clear{theme['reset']} - Pulisci {theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']} {theme['user']}/status{theme['reset']} - Stato sistema {theme['user']}/theme{theme['reset']} - Cambia tema {theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']} {theme['user']}/exit{theme['reset']} - Esci {theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']}" + " " * (w - 2) + f"{theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']} {theme['info']}Funzionalità:{theme['reset']}" + " " * (w - 22) + f"{theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']} • History (↑↓) • Autocomplete (Tab) • Colors {theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']} • Syntax highlighting • Mouse scroll • Multiline {theme['prompt']}║{theme['reset']}",
|
||||
f"{theme['prompt']}║{theme['reset']}" + " " * (w - 2) + f"{theme['prompt']}║{theme['reset']}",
|
||||
]
|
||||
|
||||
return "\n".join([top] + lines + [bottom])
|
||||
|
||||
def _create_key_bindings(self) -> KeyBindings:
|
||||
"""Create custom key bindings"""
|
||||
kb = KeyBindings()
|
||||
|
||||
@kb.add("c-c")
|
||||
def _(event):
|
||||
"""Handle Ctrl+C - interrupt or exit"""
|
||||
# If there's text, clear it; otherwise exit
|
||||
if event.app.current_buffer.text:
|
||||
event.app.current_buffer.text = ""
|
||||
else:
|
||||
event.app.exit(exception=KeyboardInterrupt)
|
||||
|
||||
@kb.add("c-l")
|
||||
def _(event):
|
||||
"""Handle Ctrl+L - clear screen"""
|
||||
os.system("clear" if os.name != "nt" else "cls")
|
||||
if self._on_clear:
|
||||
self._on_clear()
|
||||
|
||||
@kb.add("c-a")
|
||||
def _(event):
|
||||
"""Handle Ctrl+A - go to start of line"""
|
||||
buffer = event.app.current_buffer
|
||||
buffer.cursor_position = 0
|
||||
|
||||
@kb.add("c-e")
|
||||
def _(event):
|
||||
"""Handle Ctrl+E - go to end of line"""
|
||||
buffer = event.app.current_buffer
|
||||
buffer.cursor_position = len(buffer.text)
|
||||
|
||||
@kb.add("c-k")
|
||||
def _(event):
|
||||
"""Handle Ctrl+K - kill line to end"""
|
||||
buffer = event.app.current_buffer
|
||||
buffer.text = buffer.text[:buffer.cursor_position]
|
||||
|
||||
@kb.add("c-u")
|
||||
def _(event):
|
||||
"""Handle Ctrl+U - kill line from start"""
|
||||
buffer = event.app.current_buffer
|
||||
buffer.text = buffer.text[buffer.cursor_position:]
|
||||
buffer.cursor_position = 0
|
||||
|
||||
@kb.add("c-w")
|
||||
def _(event):
|
||||
"""Handle Ctrl+W - kill word before cursor"""
|
||||
buffer = event.app.current_buffer
|
||||
text = buffer.text[:buffer.cursor_position]
|
||||
if text and text[-1] == " ":
|
||||
buffer.text = text[:-1] + buffer.text[buffer.cursor_position:]
|
||||
buffer.cursor_position = len(buffer.text) - (len(buffer.text) - buffer.cursor_position + 1)
|
||||
else:
|
||||
words = text.split()
|
||||
if words:
|
||||
remaining = " ".join(words[:-1]) + (" " if len(words) > 1 else "")
|
||||
buffer.text = remaining + buffer.text[buffer.cursor_position:]
|
||||
buffer.cursor_position = len(remaining)
|
||||
|
||||
# Note: Mouse support is enabled via mouse_support parameter in PromptSession
|
||||
# Scroll events are handled automatically by prompt_toolkit
|
||||
|
||||
return kb
|
||||
|
||||
def _create_session(self) -> PromptSession:
|
||||
"""Create prompt session with history and autocomplete"""
|
||||
|
||||
# Create nested completer for commands
|
||||
command_completer = NestedCompleter.from_nested_dict({
|
||||
cmd: None for cmd in self.commands.keys()
|
||||
})
|
||||
|
||||
return PromptSession(
|
||||
history=FileHistory(self.history_file),
|
||||
completer=command_completer,
|
||||
key_bindings=self.key_bindings,
|
||||
auto_suggest=AutoSuggestFromHistory(),
|
||||
multiline=True,
|
||||
mouse_support=self.enable_mouse,
|
||||
)
|
||||
|
||||
def _get_prompt_tokens(self) -> FormattedText:
|
||||
"""Create prompt tokens with colors and optional info"""
|
||||
theme = self.ansi_theme
|
||||
timestamp = datetime.now().strftime("%H:%M") if self.show_timestamps else None
|
||||
|
||||
tokens = []
|
||||
|
||||
# Prompt symbol with theme
|
||||
tokens.append(("class:prompt", f"{theme['prompt']}❯ {theme['reset']}"))
|
||||
tokens.append(("class:user", f"{theme['user']}TUO{theme['reset']}"))
|
||||
|
||||
if timestamp:
|
||||
tokens.append(("class:muted", f" [{timestamp}]"))
|
||||
|
||||
tokens.append(("class:prompt", f" {theme['prompt']}»{theme['reset']} "))
|
||||
|
||||
return FormattedText(tokens)
|
||||
|
||||
def _print_with_timestamp(self, message: str, prefix: str = "") -> None:
|
||||
"""Print message with optional timestamp"""
|
||||
if self.show_timestamps:
|
||||
ts = datetime.now().strftime("%H:%M:%S")
|
||||
print(f"{self.ansi_theme['muted']}[{ts}]{self.ansi_theme['reset']} {prefix}{message}")
|
||||
else:
|
||||
print(f"{prefix}{message}")
|
||||
|
||||
def _print_formatted(self, message: str, level: str = "info") -> None:
|
||||
"""Print formatted message with color and optional timestamp"""
|
||||
theme = self.ansi_theme
|
||||
colors = {
|
||||
"user": theme["user"],
|
||||
"agent": theme["agent"],
|
||||
"system": theme["system"],
|
||||
"error": theme["error"],
|
||||
"info": theme["info"],
|
||||
}
|
||||
color = colors.get(level, theme["reset"])
|
||||
prefix_map = {
|
||||
"user": f"{color}👤{theme['reset']} ",
|
||||
"agent": f"{color}🤖{theme['reset']} ",
|
||||
"system": f"{color}🔧{theme['reset']} ",
|
||||
"error": f"{color}❌{theme['reset']} ",
|
||||
"info": f"{color}ℹ️{theme['reset']} ",
|
||||
}
|
||||
prefix = prefix_map.get(level, "")
|
||||
self._print_with_timestamp(message, prefix)
|
||||
|
||||
def _print_user(self, message: str) -> None:
|
||||
"""Print user message"""
|
||||
self._print_formatted(message, "user")
|
||||
|
||||
def _print_agent(self, message: str) -> None:
|
||||
"""Print agent response with optional code highlighting"""
|
||||
if self.enable_syntax_highlight:
|
||||
message = self._highlight_code_blocks(message)
|
||||
self._print_formatted(message, "agent")
|
||||
|
||||
def _print_system(self, message: str) -> None:
|
||||
"""Print system message"""
|
||||
self._print_formatted(message, "system")
|
||||
|
||||
def _print_error(self, message: str) -> None:
|
||||
"""Print error message"""
|
||||
self._print_formatted(message, "error")
|
||||
|
||||
def _print_info(self, message: str) -> None:
|
||||
"""Print info message"""
|
||||
self._print_formatted(message, "info")
|
||||
|
||||
def _highlight_code_blocks(self, text: str) -> str:
|
||||
"""Apply syntax highlighting to code blocks in text"""
|
||||
if not self.enable_syntax_highlight:
|
||||
return text
|
||||
|
||||
# Pattern: ```language\ncode\n```
|
||||
code_block_pattern = r"```(\w+)?\n(.*?)```"
|
||||
|
||||
def replace_block(match):
|
||||
lang = match.group(1) or "text"
|
||||
code = match.group(2)
|
||||
|
||||
if PYGMENTS_AVAILABLE and lang.lower() not in ["text", "txt", "ansi"]:
|
||||
try:
|
||||
lexer = SyntaxHighlighter.get_lexer(lang)
|
||||
if lexer:
|
||||
from pygments import highlight as pygments_highlight
|
||||
from pygments.formatters import Terminal256Formatter
|
||||
highlighted = pygments_highlight(code, lexer, Terminal256Formatter())
|
||||
return highlighted.rstrip()
|
||||
except:
|
||||
pass
|
||||
|
||||
# No highlighting available, just return code with a header
|
||||
return f"{self.ansi_theme['code']}--- {lang or 'code'} ---\n{code}{self.ansi_theme['reset']}"
|
||||
|
||||
return re.sub(code_block_pattern, replace_block, text, flags=re.DOTALL)
|
||||
|
||||
def _handle_command(self, command: str) -> Optional[bool]:
|
||||
"""Handle special commands (/command)."""
|
||||
cmd_lower = command.lower().strip()
|
||||
|
||||
if cmd_lower == "/help":
|
||||
self._print_system("""
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ COMANDI DISPONIBILI ║
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
║ /help - Mostra questo messaggio ║
|
||||
║ /status - Mostra stato del sistema ║
|
||||
║ /clear, Ctrl+L - Pulisci lo schermo ║
|
||||
║ /theme - Cicla tra i temi ║
|
||||
║ /theme-default - Tema default ║
|
||||
║ /theme-dark - Tema scuro ║
|
||||
║ /theme-light - Tema chiaro ║
|
||||
║ /theme-monokai - Tema monokai ║
|
||||
║ /timestamps - Toggle timestamps ║
|
||||
║ /syntax - Toggle syntax highlighting ║
|
||||
║ /width - Mostra larghezza terminale ║
|
||||
║ /history - Mostra cronologia comandi ║
|
||||
║ /exit, Ctrl+C - Esci dall'agente ║
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
║ SHORTCUTS: ║
|
||||
║ Ctrl+A/E - Inizio/fine riga ║
|
||||
║ Ctrl+K/U - Kill riga da cursore/fino a cursore ║
|
||||
║ Ctrl+W - Kill parola precedente ║
|
||||
║ ↑/↓ - History navigation ║
|
||||
║ Tab - Autocomplete ║
|
||||
║ Shift+Tab - Show suggestions ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
return None
|
||||
|
||||
elif cmd_lower == "/status":
|
||||
uptime = datetime.now() - self.stats["session_start"]
|
||||
self._print_info(f"Theme: {self.current_theme}")
|
||||
self._print_info(f"Timestamps: {'On' if self.show_timestamps else 'Off'}")
|
||||
self._print_info(f"Syntax highlighting: {'On' if self.enable_syntax_highlight else 'Off'}")
|
||||
self._print_info(f"Commands executed: {self.stats['commands_executed']}")
|
||||
self._print_info(f"Session uptime: {uptime}")
|
||||
self._print_info(f"Terminal width: {self.terminal_width}")
|
||||
self._print_info(f"History file: {self.history_file}")
|
||||
return None
|
||||
|
||||
elif cmd_lower == "/clear":
|
||||
os.system("clear" if os.name != "nt" else "cls")
|
||||
return None
|
||||
|
||||
elif cmd_lower in ["/theme", "/theme-default"]:
|
||||
self.current_theme = "default"
|
||||
self.ansi_theme = Theme.get_ansi_theme("default")
|
||||
self._print_info("Tema cambiato a: default")
|
||||
return None
|
||||
|
||||
elif cmd_lower == "/theme-dark":
|
||||
self.current_theme = "dark"
|
||||
self.ansi_theme = Theme.get_ansi_theme("dark")
|
||||
self._print_info("Tema cambiato a: dark")
|
||||
return None
|
||||
|
||||
elif cmd_lower == "/theme-light":
|
||||
self.current_theme = "light"
|
||||
self.ansi_theme = Theme.get_ansi_theme("light")
|
||||
self._print_info("Tema cambiato a: light")
|
||||
return None
|
||||
|
||||
elif cmd_lower == "/theme-monokai":
|
||||
self.current_theme = "monokai"
|
||||
self.ansi_theme = Theme.get_ansi_theme("monokai")
|
||||
self._print_info("Tema cambiato a: monokai")
|
||||
return None
|
||||
|
||||
elif cmd_lower == "/timestamps":
|
||||
self.show_timestamps = not self.show_timestamps
|
||||
self._print_info(f"Timestamps: {'On' if self.show_timestamps else 'Off'}")
|
||||
return None
|
||||
|
||||
elif cmd_lower == "/syntax":
|
||||
self.enable_syntax_highlight = not self.enable_syntax_highlight
|
||||
self._print_info(f"Syntax highlighting: {'On' if self.enable_syntax_highlight else 'Off'}")
|
||||
return None
|
||||
|
||||
elif cmd_lower == "/width":
|
||||
self.terminal_width = shutil.get_terminal_size().columns
|
||||
self._print_info(f"Terminal width: {self.terminal_width}")
|
||||
return None
|
||||
|
||||
elif cmd_lower in ["/exit", "/quit"]:
|
||||
return True
|
||||
|
||||
elif cmd_lower == "/history":
|
||||
try:
|
||||
with open(self.history_file, "r") as f:
|
||||
lines = f.readlines()
|
||||
if lines:
|
||||
self._print_system(f"Ultimi {min(20, len(lines))} comandi:")
|
||||
for i, line in enumerate(lines[-20:], 1):
|
||||
print(f" {i:3d}. {line.strip()}")
|
||||
else:
|
||||
self._print_info("Nessuna cronologia")
|
||||
except FileNotFoundError:
|
||||
self._print_info("Nessuna cronologia")
|
||||
return None
|
||||
|
||||
return None # Command not recognized
|
||||
|
||||
def prompt(self) -> str:
|
||||
"""Get input from user with enhanced UI"""
|
||||
try:
|
||||
text = self.session.prompt(
|
||||
self._get_prompt_tokens(),
|
||||
style=Theme.get_style(self.current_theme),
|
||||
)
|
||||
return text
|
||||
except KeyboardInterrupt:
|
||||
return ""
|
||||
except EOFError:
|
||||
return "/exit"
|
||||
|
||||
def run(self, agent_callback: Callable[[str], str]):
|
||||
"""
|
||||
Run the interactive CLI loop.
|
||||
|
||||
Args:
|
||||
agent_callback: Function that takes user input and returns agent response
|
||||
"""
|
||||
import signal
|
||||
import sys
|
||||
|
||||
# Register signal handler for Ctrl+C
|
||||
def handle_sigint(signum, frame):
|
||||
print("\n\n👋 Arrivederci!")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, handle_sigint)
|
||||
|
||||
# Print welcome
|
||||
print(self.welcome_message)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Get user input
|
||||
user_input = self.prompt().strip()
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
# Show what user typed
|
||||
self._print_user(user_input)
|
||||
self.stats["commands_executed"] += 1
|
||||
|
||||
# Check for commands
|
||||
if user_input.startswith("/"):
|
||||
should_exit = self._handle_command(user_input)
|
||||
if should_exit:
|
||||
break
|
||||
continue
|
||||
|
||||
# Call agent and show response
|
||||
try:
|
||||
response = agent_callback(user_input)
|
||||
self._print_agent(response)
|
||||
except KeyboardInterrupt:
|
||||
self._print_system("\nInterruzione... Riprova.")
|
||||
except Exception as e:
|
||||
self._print_error(f"Errore: {str(e)}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self._print_system("\n👋 Arrivederci!")
|
||||
break
|
||||
except EOFError:
|
||||
break
|
||||
|
||||
self._print_system("Sessione terminata.")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FACTORY FUNCTION
|
||||
# =============================================================================
|
||||
|
||||
def create_enhanced_cli(
|
||||
history_file: str = "./.langcode/.cli_history",
|
||||
theme: str = "default",
|
||||
**kwargs
|
||||
) -> EnhancedCLI:
|
||||
"""Create an EnhancedCLI instance"""
|
||||
return EnhancedCLI(
|
||||
history_file=history_file,
|
||||
theme=theme,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# STANDALONE TEST
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
def test_callback(text):
|
||||
return f"Hai detto: {text}\n\n```python\nprint('Hello!')\n```"
|
||||
|
||||
cli = create_enhanced_cli(theme="monokai")
|
||||
cli.run(test_callback)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Enhancements for LangCode Agent based on LangChain Deep Agents analysis
|
||||
|
||||
This directory contains planned enhancements to bring LangCode Agent closer to the architecture patterns revealed in LangChain's Deep Agents framework.
|
||||
|
||||
## Planned Enhancements
|
||||
|
||||
1. **Enhanced Memory Systems**
|
||||
- Add VectorStoreRetrieverMemory for semantic recall
|
||||
- Implement SummaryBufferMemory for conversation summarization
|
||||
- Add persistent memory storage for cross-session learning
|
||||
|
||||
2. **Advanced Planning & Reasoning**
|
||||
- Implement plan-and-execute agent pattern
|
||||
- Add self-reflection capabilities
|
||||
- Improve task decomposition and tracking
|
||||
|
||||
3. **Production Observability**
|
||||
- Add LangChain-style callbacks for tracing
|
||||
- Implement tool usage monitoring
|
||||
- Add performance metrics collection
|
||||
|
||||
4. **Advanced Tool Composition**
|
||||
- Implement tool chaining capabilities
|
||||
- Add async tool execution for parallelism
|
||||
- Enhance input validation and error handling
|
||||
|
||||
5. **Agent Hierarchies**
|
||||
- Develop specialist subagents for specific domains
|
||||
- Implement agent routing based on task analysis
|
||||
- Add agent-to-agent communication patterns
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
Each enhancement will be implemented as a separate module that can be optionally integrated into the main agent, maintaining backward compatibility while extending capabilities.
|
||||
|
||||
The enhancements will follow the same patterns already established in the codebase:
|
||||
- Modular design
|
||||
- Configuration-driven behavior
|
||||
- Clear separation of concerns
|
||||
- Comprehensive error handling
|
||||
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
Enhanced memory systems for LangCode Agent
|
||||
Implements advanced memory patterns from LangChain Deep Agents
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
# Try to import from langchain_core, fallback to community if needed
|
||||
try:
|
||||
from langchain_core.memory import BaseMemory
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
from langchain_core.embeddings import Embeddings
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.storage import BaseStore
|
||||
from langchain_core.retrievers import BaseRetriever
|
||||
LANGCHAIN_CORE_AVAILABLE = True
|
||||
except ImportError:
|
||||
LANGCHAIN_CORE_AVAILABLE = False
|
||||
# We'll handle this gracefully in the implementation
|
||||
|
||||
# Try to import OllamaEmbeddings
|
||||
try:
|
||||
from langchain_community.embeddings import OllamaEmbeddings
|
||||
OLLAMA_AVAILABLE = True
|
||||
except ImportError:
|
||||
OLLAMA_AVAILABLE = False
|
||||
OllamaEmbeddings = None # type: ignore
|
||||
|
||||
# Try to import FAISS for vector store
|
||||
try:
|
||||
from langchain_community.vectorstores import FAISS
|
||||
FAISS_AVAILABLE = True
|
||||
except ImportError:
|
||||
FAISS_AVAILABLE = False
|
||||
FAISS = None # type: ignore
|
||||
|
||||
# Try to import memory components
|
||||
try:
|
||||
from langchain_core.memory import VectorStoreRetrieverMemory, SummaryBufferMemory
|
||||
MEMORY_COMPONENTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
MEMORY_COMPONENTS_AVAILABLE = False
|
||||
VectorStoreRetrieverMemory = None # type: ignore
|
||||
SummaryBufferMemory = None # type: ignore
|
||||
|
||||
from langchain_core.tools import Tool
|
||||
|
||||
class EnhancedMemoryManager:
|
||||
"""Manages enhanced memory systems for the LangCode Agent"""
|
||||
|
||||
def __init__(self,
|
||||
embedding_model: str = "nomic-embed-text",
|
||||
ollama_base_url: str = "http://localhost:11434",
|
||||
persist_directory: str = "./.langcode/memory"):
|
||||
"""
|
||||
Initialize the enhanced memory manager
|
||||
|
||||
Args:
|
||||
embedding_model: Name of the embedding model to use
|
||||
ollama_base_url: URL for Ollama service
|
||||
persist_directory: Directory to persist memory data
|
||||
"""
|
||||
self.embedding_model = embedding_model
|
||||
self.ollama_base_url = ollama_base_url
|
||||
self.persist_directory = persist_directory
|
||||
|
||||
# Ensure persist directory exists
|
||||
os.makedirs(persist_directory, exist_ok=True)
|
||||
|
||||
# Initialize components if available
|
||||
self.embeddings = None
|
||||
self.vectorstore = None
|
||||
self.summary_memory = None
|
||||
self.vector_memory = None
|
||||
self.docstore = None
|
||||
|
||||
self._initialize_memory_systems()
|
||||
|
||||
def _initialize_memory_systems(self):
|
||||
"""Initialize all memory systems if dependencies are available"""
|
||||
if not (OLLAMA_AVAILABLE and FAISS_AVAILABLE and MEMORY_COMPONENTS_AVAILABLE):
|
||||
missing = []
|
||||
if not OLLAMA_AVAILABLE:
|
||||
missing.append("Ollama embeddings")
|
||||
if not FAISS_AVAILABLE:
|
||||
missing.append("FAISS vector store")
|
||||
if not MEMORY_COMPONENTS_AVAILABLE:
|
||||
missing.append("Memory components")
|
||||
print(f"Warning: Enhanced memory features limited due to missing: {', '.join(missing)}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Initialize embeddings
|
||||
self.embeddings = OllamaEmbeddings(
|
||||
model=self.embedding_model,
|
||||
base_url=self.ollama_base_url
|
||||
)
|
||||
|
||||
# Initialize docstore for storing documents
|
||||
from langchain.storage import InMemoryStore
|
||||
self.docstore = InMemoryStore()
|
||||
|
||||
# Initialize vector store for semantic memory
|
||||
self.vectorstore = FAISS(
|
||||
embedding_function=self.embeddings,
|
||||
index=None,
|
||||
docstore=self.docstore,
|
||||
index_to_docstore_id={}
|
||||
)
|
||||
|
||||
# Initialize vector store memory
|
||||
self.vector_memory = VectorStoreRetrieverMemory(
|
||||
retriever=self.vectorstore.as_retriever(search_kwargs={"k": 3})
|
||||
)
|
||||
|
||||
# Initialize summary memory for conversation compression
|
||||
self.summary_memory = SummaryBufferMemory(
|
||||
max_token_limit=1000
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to initialize enhanced memory systems: {e}")
|
||||
# Reset to None if initialization failed
|
||||
self.embeddings = None
|
||||
self.vectorstore = None
|
||||
self.summary_memory = None
|
||||
self.vector_memory = None
|
||||
self.docstore = None
|
||||
|
||||
def set_llm_for_summary_memory(self, llm):
|
||||
"""Set the LLM for summary memory"""
|
||||
if self.summary_memory and hasattr(self.summary_memory, 'llm'):
|
||||
self.summary_memory.llm = llm
|
||||
|
||||
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Save context to all memory systems
|
||||
|
||||
Args:
|
||||
inputs: Input values from the conversation
|
||||
outputs: Output values from the conversation
|
||||
"""
|
||||
try:
|
||||
# Save to vector memory
|
||||
if self.vector_memory:
|
||||
self.vector_memory.save_context(inputs, outputs)
|
||||
|
||||
# Save to summary memory
|
||||
if self.summary_memory:
|
||||
self.summary_memory.save_context(inputs, outputs)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to save context to memory: {e}")
|
||||
|
||||
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Load memory variables from all memory systems
|
||||
|
||||
Args:
|
||||
inputs: Input values for context retrieval
|
||||
|
||||
Returns:
|
||||
Dictionary containing memory variables
|
||||
"""
|
||||
result = {}
|
||||
|
||||
try:
|
||||
# Load from vector memory
|
||||
if self.vector_memory:
|
||||
vector_result = self.vector_memory.load_memory_variables(inputs)
|
||||
result.update(vector_result)
|
||||
|
||||
# Load from summary memory
|
||||
if self.summary_memory:
|
||||
summary_result = self.summary_memory.load_memory_variables(inputs)
|
||||
result.update(summary_result)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load memory variables: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def add_document(self, content: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""
|
||||
Add a document to the vector store for long-term memory
|
||||
|
||||
Args:
|
||||
content: Document content to store
|
||||
metadata: Optional metadata for the document
|
||||
"""
|
||||
if not self.vectorstore:
|
||||
return
|
||||
|
||||
try:
|
||||
doc = Document(page_content=content, metadata=metadata or {})
|
||||
self.vectorstore.add_documents([doc])
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to add document to vector store: {e}")
|
||||
|
||||
def search_similar(self, query: str, k: int = 3) -> List[Document]:
|
||||
"""
|
||||
Search for similar documents in vector store
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
k: Number of results to return
|
||||
|
||||
Returns:
|
||||
List of similar documents
|
||||
"""
|
||||
if not self.vectorstore:
|
||||
return []
|
||||
|
||||
try:
|
||||
return self.vectorstore.similarity_search(query, k=k)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to search vector store: {e}")
|
||||
return []
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all memory systems"""
|
||||
try:
|
||||
if self.vector_memory:
|
||||
self.vector_memory.clear()
|
||||
if self.summary_memory:
|
||||
self.summary_memory.clear()
|
||||
# Reinitialize vector store
|
||||
self._initialize_memory_systems()
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to clear memory systems: {e}")
|
||||
|
||||
def create_memory_tools(memory_manager: EnhancedMemoryManager) -> List[Tool]:
|
||||
"""
|
||||
Create LangChain tools for the enhanced memory system
|
||||
|
||||
Args:
|
||||
memory_manager: Instance of EnhancedMemoryManager
|
||||
|
||||
Returns:
|
||||
List of memory tools
|
||||
"""
|
||||
|
||||
def add_memory_func(content: str, metadata: str = "{}") -> str:
|
||||
try:
|
||||
metadata_dict = json.loads(metadata) if metadata.strip() else {}
|
||||
memory_manager.add_document(content, metadata_dict)
|
||||
return f"Added content to memory: {content[:50]}{'...' if len(content) > 50 else ''}"
|
||||
except Exception as e:
|
||||
return f"Error adding to memory: {str(e)}"
|
||||
|
||||
def search_memory_func(query: str, k: str = "3") -> str:
|
||||
try:
|
||||
k_int = int(k)
|
||||
results = memory_manager.search_similar(query, k_int)
|
||||
if not results:
|
||||
return "No similar memories found."
|
||||
|
||||
result = f"Found {len(results)} similar memories:\n"
|
||||
for i, doc in enumerate(results, 1):
|
||||
result += f"{i}. {doc.page_content[:100]}{'...' if len(doc.page_content) > 100 else ''}\n"
|
||||
if doc.metadata:
|
||||
result += f" Metadata: {doc.metadata}\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error searching memory: {str(e)}"
|
||||
|
||||
def clear_memory_func() -> str:
|
||||
try:
|
||||
memory_manager.clear()
|
||||
return "Memory cleared successfully."
|
||||
except Exception as e:
|
||||
return f"Error clearing memory: {str(e)}"
|
||||
|
||||
def get_memory_stats_func() -> str:
|
||||
try:
|
||||
stats = []
|
||||
if memory_manager.vectorstore:
|
||||
# Try to get vector count
|
||||
try:
|
||||
count = memory_manager.vectorstore.index.ntotal if hasattr(memory_manager.vectorstore.index, 'ntotal') else 'unknown'
|
||||
stats.append(f"Vector store: {count} vectors")
|
||||
except:
|
||||
stats.append("Vector store: active")
|
||||
if memory_manager.summary_memory:
|
||||
stats.append("Summary memory: active")
|
||||
if memory_manager.vector_memory:
|
||||
stats.append("Vector memory: active")
|
||||
return "Memory Systems:\n" + "\n".join(f" - {s}" for s in stats)
|
||||
except Exception as e:
|
||||
return f"Error getting memory stats: {str(e)}"
|
||||
|
||||
return [
|
||||
Tool(
|
||||
name="add_to_memory",
|
||||
description="Add content to long-term memory. Provide content and optional metadata as JSON string.",
|
||||
func=add_memory_func
|
||||
),
|
||||
Tool(
|
||||
name="search_memory",
|
||||
description="Search long-term memory for similar content. Provide query and optional k (number of results, default 3).",
|
||||
func=search_memory_func
|
||||
),
|
||||
Tool(
|
||||
name="clear_memory",
|
||||
description="Clear all long-term memory.",
|
||||
func=clear_memory_func
|
||||
),
|
||||
Tool(
|
||||
name="memory_stats",
|
||||
description="Get statistics about the memory systems.",
|
||||
func=get_memory_stats_func
|
||||
)
|
||||
]
|
||||
|
||||
# Global instance for easy access
|
||||
enhanced_memory_manager = EnhancedMemoryManager()
|
||||
@@ -0,0 +1,364 @@
|
||||
"""
|
||||
Enhanced planning systems for LangCode Agent
|
||||
Implements advanced planning patterns from LangChain Deep Agents
|
||||
"""
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from langchain_core.tools import Tool
|
||||
|
||||
class TaskStatus(Enum):
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
CANCELLED = "cancelled"
|
||||
FAILED = "failed"
|
||||
|
||||
class TaskPriority(Enum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
URGENT = "urgent"
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
"""Represents a task in the planning system"""
|
||||
id: str
|
||||
description: str
|
||||
status: TaskStatus
|
||||
priority: TaskPriority
|
||||
created_at: datetime
|
||||
dependencies: List[str] = field(default_factory=list) # List of task IDs that must be completed first
|
||||
started_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
result: Optional[Any] = None
|
||||
error: Optional[str] = None
|
||||
subtasks: List[str] = field(default_factory=list) # List of subtask IDs
|
||||
|
||||
class EnhancedPlanner:
|
||||
"""Enhanced planning system for LangCode Agent"""
|
||||
|
||||
def __init__(self):
|
||||
self.tasks: Dict[str, Task] = {}
|
||||
self.task_counter = 0
|
||||
self.plans: Dict[str, List[str]] = {} # plan_id -> list of task IDs
|
||||
|
||||
def create_task(self,
|
||||
description: str,
|
||||
priority: TaskPriority = TaskPriority.MEDIUM,
|
||||
dependencies: Optional[List[str]] = None) -> str:
|
||||
"""
|
||||
Create a new task
|
||||
|
||||
Args:
|
||||
description: Task description
|
||||
priority: Task priority level
|
||||
dependencies: List of task IDs that must be completed first
|
||||
|
||||
Returns:
|
||||
Task ID
|
||||
"""
|
||||
if dependencies is None:
|
||||
dependencies = []
|
||||
|
||||
task_id = f"task_{self.task_counter:04d}"
|
||||
self.task_counter += 1
|
||||
|
||||
task = Task(
|
||||
id=task_id,
|
||||
description=description,
|
||||
status=TaskStatus.PENDING,
|
||||
priority=priority,
|
||||
dependencies=dependencies,
|
||||
created_at=datetime.now()
|
||||
)
|
||||
|
||||
self.tasks[task_id] = task
|
||||
return task_id
|
||||
|
||||
def create_plan(self, name: str, task_descriptions: List[str]) -> str:
|
||||
"""
|
||||
Create a plan with multiple tasks
|
||||
|
||||
Args:
|
||||
name: Plan name
|
||||
task_descriptions: List of task descriptions
|
||||
|
||||
Returns:
|
||||
Plan ID
|
||||
"""
|
||||
plan_id = f"plan_{len(self.plans):04d}"
|
||||
task_ids = []
|
||||
|
||||
for desc in task_descriptions:
|
||||
task_id = self.create_task(desc)
|
||||
task_ids.append(task_id)
|
||||
|
||||
self.plans[plan_id] = task_ids
|
||||
return plan_id
|
||||
|
||||
def start_task(self, task_id: str) -> bool:
|
||||
"""
|
||||
Start working on a task
|
||||
|
||||
Args:
|
||||
task_id: ID of the task to start
|
||||
|
||||
Returns:
|
||||
True if task started successfully, False otherwise
|
||||
"""
|
||||
if task_id not in self.tasks:
|
||||
return False
|
||||
|
||||
task = self.tasks[task_id]
|
||||
|
||||
# Check if dependencies are completed
|
||||
for dep_id in task.dependencies:
|
||||
if dep_id not in self.tasks or self.tasks[dep_id].status != TaskStatus.COMPLETED:
|
||||
return False # Dependencies not met
|
||||
|
||||
task.status = TaskStatus.IN_PROGRESS
|
||||
task.started_at = datetime.now()
|
||||
return True
|
||||
|
||||
def complete_task(self, task_id: str, result: Any = None) -> bool:
|
||||
"""
|
||||
Mark a task as completed
|
||||
|
||||
Args:
|
||||
task_id: ID of the task to complete
|
||||
result: Result of the task execution
|
||||
|
||||
Returns:
|
||||
True if task completed successfully, False otherwise
|
||||
"""
|
||||
if task_id not in self.tasks:
|
||||
return False
|
||||
|
||||
task = self.tasks[task_id]
|
||||
if task.status != TaskStatus.IN_PROGRESS:
|
||||
return False
|
||||
|
||||
task.status = TaskStatus.COMPLETED
|
||||
task.completed_at = datetime.now()
|
||||
task.result = result
|
||||
return True
|
||||
|
||||
def fail_task(self, task_id: str, error: str) -> bool:
|
||||
"""
|
||||
Mark a task as failed
|
||||
|
||||
Args:
|
||||
task_id: ID of the task to fail
|
||||
error: Error message
|
||||
|
||||
Returns:
|
||||
True if task marked as failed, False otherwise
|
||||
"""
|
||||
if task_id not in self.tasks:
|
||||
return False
|
||||
|
||||
task = self.tasks[task_id]
|
||||
if task.status != TaskStatus.IN_PROGRESS:
|
||||
return False
|
||||
|
||||
task.status = TaskStatus.FAILED
|
||||
task.completed_at = datetime.now()
|
||||
task.error = error
|
||||
return True
|
||||
|
||||
def get_ready_tasks(self) -> List[Task]:
|
||||
"""
|
||||
Get tasks that are ready to be started (dependencies met)
|
||||
|
||||
Returns:
|
||||
List of ready tasks
|
||||
"""
|
||||
ready_tasks = []
|
||||
for task in self.tasks.values():
|
||||
if task.status == TaskStatus.PENDING:
|
||||
# Check if all dependencies are completed
|
||||
deps_met = True
|
||||
for dep_id in task.dependencies:
|
||||
if dep_id not in self.tasks or self.tasks[dep_id].status != TaskStatus.COMPLETED:
|
||||
deps_met = False
|
||||
break
|
||||
if deps_met:
|
||||
ready_tasks.append(task)
|
||||
return ready_tasks
|
||||
|
||||
def get_task_status(self, task_id: str) -> Optional[TaskStatus]:
|
||||
"""
|
||||
Get the status of a task
|
||||
|
||||
Args:
|
||||
task_id: ID of the task
|
||||
|
||||
Returns:
|
||||
Task status or None if task not found
|
||||
"""
|
||||
if task_id in self.tasks:
|
||||
return self.tasks[task_id].status
|
||||
return None
|
||||
|
||||
def get_plan_progress(self, plan_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get progress information for a plan
|
||||
|
||||
Args:
|
||||
plan_id: ID of the plan
|
||||
|
||||
Returns:
|
||||
Dictionary with progress information
|
||||
"""
|
||||
if plan_id not in self.plans:
|
||||
return {"error": "Plan not found"}
|
||||
|
||||
task_ids = self.plans[plan_id]
|
||||
total_tasks = len(task_ids)
|
||||
completed_tasks = 0
|
||||
failed_tasks = 0
|
||||
in_progress_tasks = 0
|
||||
|
||||
for task_id in task_ids:
|
||||
if task_id in self.tasks:
|
||||
task = self.tasks[task_id]
|
||||
if task.status == TaskStatus.COMPLETED:
|
||||
completed_tasks += 1
|
||||
elif task.status == TaskStatus.FAILED:
|
||||
failed_tasks += 1
|
||||
elif task.status == TaskStatus.IN_PROGRESS:
|
||||
in_progress_tasks += 1
|
||||
|
||||
return {
|
||||
"plan_id": plan_id,
|
||||
"total_tasks": total_tasks,
|
||||
"completed_tasks": completed_tasks,
|
||||
"failed_tasks": failed_tasks,
|
||||
"in_progress_tasks": in_progress_tasks,
|
||||
"pending_tasks": total_tasks - completed_tasks - failed_tasks - in_progress_tasks,
|
||||
"progress_percentage": (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
|
||||
}
|
||||
|
||||
def create_planning_tools(self) -> List[Tool]:
|
||||
"""
|
||||
Create LangChain tools for the planning system
|
||||
|
||||
Returns:
|
||||
List of planning tools
|
||||
"""
|
||||
def create_task_func(description: str, priority: str = "medium", dependencies: str = "") -> str:
|
||||
try:
|
||||
priority_enum = TaskPriority(priority.lower())
|
||||
dep_list = [d.strip() for d in dependencies.split(",") if d.strip()] if dependencies else []
|
||||
task_id = self.create_task(description, priority_enum, dep_list)
|
||||
return f"Created task '{description}' with ID: {task_id}"
|
||||
except Exception as e:
|
||||
return f"Error creating task: {str(e)}"
|
||||
|
||||
def start_task_func(task_id: str) -> str:
|
||||
try:
|
||||
if self.start_task(task_id):
|
||||
return f"Started task {task_id}"
|
||||
else:
|
||||
return f"Failed to start task {task_id} - check dependencies or task ID"
|
||||
except Exception as e:
|
||||
return f"Error starting task: {str(e)}"
|
||||
|
||||
def complete_task_func(task_id: str, result: str = "") -> str:
|
||||
try:
|
||||
if self.complete_task(task_id, result if result else None):
|
||||
return f"Completed task {task_id}"
|
||||
else:
|
||||
return f"Failed to complete task {task_id} - check task ID or status"
|
||||
except Exception as e:
|
||||
return f"Error completing task: {str(e)}"
|
||||
|
||||
def list_tasks_func(status: str = "") -> str:
|
||||
try:
|
||||
if status:
|
||||
status_enum = TaskStatus(status.lower())
|
||||
filtered_tasks = [t for t in self.tasks.values() if t.status == status_enum]
|
||||
else:
|
||||
filtered_tasks = list(self.tasks.values())
|
||||
|
||||
if not filtered_tasks:
|
||||
return "No tasks found"
|
||||
|
||||
result = f"Tasks ({len(filtered_tasks)}):\n"
|
||||
for task in sorted(filtered_tasks, key=lambda t: t.created_at):
|
||||
result += f"- [{task.status.value}] {task.id}: {task.description}\n"
|
||||
if task.dependencies:
|
||||
result += f" Dependencies: {', '.join(task.dependencies)}\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error listing tasks: {str(e)}"
|
||||
|
||||
def get_ready_tasks_func() -> str:
|
||||
try:
|
||||
ready_tasks = self.get_ready_tasks()
|
||||
if not ready_tasks:
|
||||
return "No tasks ready to start"
|
||||
|
||||
result = f"Ready tasks ({len(ready_tasks)}):\n"
|
||||
for task in ready_tasks:
|
||||
result += f"- {task.id}: {task.description}\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error getting ready tasks: {str(e)}"
|
||||
|
||||
def get_plan_progress_func(plan_id: str) -> str:
|
||||
try:
|
||||
progress = self.get_plan_progress(plan_id)
|
||||
if "error" in progress:
|
||||
return progress["error"]
|
||||
|
||||
result = f"Plan {plan_id} Progress:\n"
|
||||
result += f" Total: {progress['total_tasks']}\n"
|
||||
result += f" Completed: {progress['completed_tasks']}\n"
|
||||
result += f" Failed: {progress['failed_tasks']}\n"
|
||||
result += f" In Progress: {progress['in_progress_tasks']}\n"
|
||||
result += f" Pending: {progress['pending_tasks']}\n"
|
||||
result += f" Progress: {progress['progress_percentage']:.1f}%\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error getting plan progress: {str(e)}"
|
||||
|
||||
return [
|
||||
Tool(
|
||||
name="create_task",
|
||||
description="Create a new task. Provide description, priority (low/medium/high/urgent), and optional dependencies (comma-separated task IDs)",
|
||||
func=create_task_func
|
||||
),
|
||||
Tool(
|
||||
name="start_task",
|
||||
description="Start working on a task. Provide task ID",
|
||||
func=start_task_func
|
||||
),
|
||||
Tool(
|
||||
name="complete_task",
|
||||
description="Mark a task as completed. Provide task ID and optional result",
|
||||
func=complete_task_func
|
||||
),
|
||||
Tool(
|
||||
name="list_tasks",
|
||||
description="List all tasks. Optionally filter by status (pending, in_progress, completed, failed)",
|
||||
func=list_tasks_func
|
||||
),
|
||||
Tool(
|
||||
name="get_ready_tasks",
|
||||
description="Get tasks that are ready to be started (dependencies met)",
|
||||
func=get_ready_tasks_func
|
||||
),
|
||||
Tool(
|
||||
name="get_plan_progress",
|
||||
description="Get progress information for a plan. Provide plan ID",
|
||||
func=get_plan_progress_func
|
||||
)
|
||||
]
|
||||
|
||||
# Global planner instance
|
||||
enhanced_planner = EnhancedPlanner()
|
||||
@@ -0,0 +1,638 @@
|
||||
"""
|
||||
ReAct Agent - Agent Loop Implementation
|
||||
|
||||
Questo modulo implementa un agente con loop Pensa→Agisci→Osserva→Ripianifica.
|
||||
Ispirato al pattern ReAct (Reasoning + Acting) di Yao et al.
|
||||
|
||||
L'agente:
|
||||
1. THINK: Analizza il goal e decide la prossima azione
|
||||
2. ACT: Esegue l'azione tramite tool
|
||||
3. OBSERVE: Legge il risultato
|
||||
4. EVALUATE: Verifica se il goal è raggiunto
|
||||
5. REPLAN: Se fallito, ripianifica
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import List, Dict, Any, Optional, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
|
||||
from langchain_core.tools import Tool
|
||||
|
||||
from enhancements.planning_enhancement import EnhancedPlanner, TaskPriority
|
||||
from enhancements.simple_memory import SimplePersistentMemory, create_simple_memory_tools
|
||||
|
||||
|
||||
class AgentState(Enum):
|
||||
"""Stati dell'agente nel loop"""
|
||||
THINKING = "thinking"
|
||||
ACTING = "acting"
|
||||
OBSERVING = "observing"
|
||||
EVALUATING = "evaluating"
|
||||
REPLANNING = "replanning"
|
||||
DONE = "done"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionResult:
|
||||
"""Risultato di un'azione"""
|
||||
action_name: str
|
||||
args: Dict[str, Any]
|
||||
result: str
|
||||
success: bool
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Step:
|
||||
"""Singolo step nel loop dell'agente"""
|
||||
step_number: int
|
||||
state: AgentState
|
||||
thought: str
|
||||
action: Optional[ActionResult] = None
|
||||
observation: Optional[str] = None
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
|
||||
|
||||
class ReActAgent:
|
||||
"""
|
||||
Agente con loop ReAct (Reasoning + Acting).
|
||||
|
||||
Esegue un goal attraverso iterazioni di:
|
||||
- THINK: Decide cosa fare
|
||||
- ACT: Esegue l'azione
|
||||
- OBSERVE: Legge il risultato
|
||||
- EVALUATE: Verifica se goal raggiunto
|
||||
- REPLAN: Ripianifica se necessario
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm,
|
||||
tools: List[Tool],
|
||||
max_iterations: int = 10,
|
||||
planner: Optional[EnhancedPlanner] = None,
|
||||
memory: Optional[SimplePersistentMemory] = None,
|
||||
verbose: bool = False,
|
||||
include_memory_tools: bool = True
|
||||
):
|
||||
"""
|
||||
Inizializza il ReActAgent.
|
||||
|
||||
Args:
|
||||
llm: Modello LLM da usare
|
||||
tools: Lista di tool disponibili
|
||||
max_iterations: Massimo numero di iterazioni
|
||||
planner: Istanza EnhancedPlanner (creato se None)
|
||||
memory: Istanza SimplePersistentMemory (creato se None)
|
||||
verbose: Se True, stampa debug info
|
||||
include_memory_tools: Se True, aggiunge tool memory (remember, recall, preference)
|
||||
"""
|
||||
self.llm = llm
|
||||
self.max_iterations = max_iterations
|
||||
self.verbose = verbose
|
||||
|
||||
# Planning e memory esistenti
|
||||
self.planner = planner or EnhancedPlanner()
|
||||
self.memory = memory or SimplePersistentMemory()
|
||||
|
||||
# Aggiungi memory tools se richiesto
|
||||
if include_memory_tools:
|
||||
memory_tools = create_simple_memory_tools(self.memory)
|
||||
tools = tools + memory_tools
|
||||
|
||||
self.tools = {tool.name: tool for tool in tools}
|
||||
|
||||
# Stato interno
|
||||
self.steps: List[Step] = []
|
||||
self.current_goal: Optional[str] = None
|
||||
self.context: str = ""
|
||||
self.conversation_history: List[Dict[str, str]] = []
|
||||
|
||||
# System prompt per il loop
|
||||
self._system_prompt = self._build_system_prompt()
|
||||
|
||||
def _build_system_prompt(self) -> str:
|
||||
"""Costruisce il system prompt per il reasoning"""
|
||||
tool_descriptions = []
|
||||
for name, tool in self.tools.items():
|
||||
desc = tool.description or "No description"
|
||||
tool_descriptions.append(f"- {name}: {desc}")
|
||||
|
||||
tools_list = "\n".join(tool_descriptions)
|
||||
|
||||
return f"""Sei un AI Agent che ragiona e agisce per raggiungere obiettivi.
|
||||
|
||||
TOOLS DISPONIBILI:
|
||||
{tools_list}
|
||||
|
||||
MODO DI OPERARE:
|
||||
1. ANALIZZA il goal e il contesto attuale
|
||||
2. DECIDI la prossima azione (usa un tool se necessario)
|
||||
3. Se il goal è raggiunto, rispondi con SUCCESS
|
||||
4. Se serve un'azione, rispondi con: TOOL:<nome_tool>|arg1=val1|arg2=val2
|
||||
5. Se il goal non può essere raggiunto, rispondi con: FAIL:<reason>
|
||||
|
||||
ESEMPI:
|
||||
Goal: Elenca i file nella directory
|
||||
→ TOOL:ls|path=.
|
||||
|
||||
Goal: Leggi il file config.txt
|
||||
→ TOOL:read_file|path=config.txt
|
||||
|
||||
Goal: Mostra info sistema
|
||||
→ TOOL:system_info
|
||||
|
||||
Goal: Crea un file prova.txt con "hello"
|
||||
→ TOOL:write_file|path=prova.txt|content=hello
|
||||
|
||||
Goal: Qual è il contenuto del file?
|
||||
(analizza il contesto - il file è già stato letto)
|
||||
→ SUCCESS: Il file contiene XYZ
|
||||
|
||||
RISPOSTA:"""
|
||||
|
||||
def run(self, goal: str, track_in_planner: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
Esegue un goal attraverso il loop ReAct.
|
||||
|
||||
Args:
|
||||
goal: Goal da raggiungere
|
||||
track_in_planner: Se True, traccia il task nel planner
|
||||
|
||||
Returns:
|
||||
Dict con success, result, steps, iterations
|
||||
"""
|
||||
self.current_goal = goal
|
||||
self.context = f"GOAL: {goal}\n"
|
||||
self.steps = []
|
||||
|
||||
# Traccia nel planner se richiesto
|
||||
task_id = None
|
||||
if track_in_planner and self.planner:
|
||||
task_id = self.planner.create_task(
|
||||
description=goal,
|
||||
priority=TaskPriority.MEDIUM
|
||||
)
|
||||
self.planner.start_task(task_id)
|
||||
if self.verbose:
|
||||
print(f"📋 Task creato: {task_id}")
|
||||
|
||||
if self.verbose:
|
||||
print(f"\n🎯 GOAL: {goal}")
|
||||
print("="*60)
|
||||
|
||||
# Esegui prima azione
|
||||
current_input = goal
|
||||
success = False
|
||||
final_result = ""
|
||||
|
||||
for iteration in range(1, self.max_iterations + 1):
|
||||
# 1. THINK - analizza input corrente
|
||||
thought = self._think(current_input)
|
||||
|
||||
if self.verbose:
|
||||
print(f"\n📝 Step {iteration} - THINK:")
|
||||
print(f" {thought[:200]}...")
|
||||
|
||||
# Check per terminazione
|
||||
if thought.strip().upper().startswith("SUCCESS"):
|
||||
result = thought[len("SUCCESS"):].strip()
|
||||
self._add_step(AgentState.DONE, thought)
|
||||
success = True
|
||||
final_result = result
|
||||
break
|
||||
|
||||
if thought.strip().upper().startswith("FAIL"):
|
||||
reason = thought[len("FAIL:"):].strip()
|
||||
self._add_step(AgentState.FAILED, thought)
|
||||
final_result = self.context
|
||||
if task_id:
|
||||
self.planner.fail_task(task_id, reason)
|
||||
return self._build_result(False, self.context, reason)
|
||||
|
||||
# 2. ACT - parse e esegui tool
|
||||
action_result = self._act(thought)
|
||||
self._add_step(AgentState.ACTING, thought, action_result)
|
||||
|
||||
if self.verbose:
|
||||
print(f" 🔧 ACTION: {action_result.action_name}")
|
||||
print(f" 📤 RESULT: {action_result.result[:100]}...")
|
||||
|
||||
# 3. OBSERVE
|
||||
observation = f"[{action_result.action_name}] {action_result.result}"
|
||||
self.context += f"\n{observation}"
|
||||
self._add_step(AgentState.OBSERVING, thought, action_result, observation)
|
||||
|
||||
# Se l'azione era un tool, goal semplice raggiunto
|
||||
if action_result.action_name != "respond":
|
||||
success = True
|
||||
final_result = action_result.result
|
||||
break
|
||||
|
||||
# 4. EVALUATE - solo per risposte dirette
|
||||
self._add_step(AgentState.EVALUATING, thought, action_result, observation)
|
||||
current_input = f"Risultato: {action_result.result}\n\nGoal: {goal}\nGoal raggiunto?"
|
||||
|
||||
# Aggiorna planner
|
||||
if task_id:
|
||||
if success:
|
||||
self.planner.complete_task(task_id, final_result)
|
||||
if self.verbose:
|
||||
print(f"✅ Task {task_id} completato")
|
||||
else:
|
||||
self.planner.fail_task(task_id, "Max iterations reached")
|
||||
|
||||
if success:
|
||||
return self._build_result(
|
||||
True,
|
||||
final_result,
|
||||
f"Goal raggiunto in {iteration} step"
|
||||
)
|
||||
else:
|
||||
return self._build_result(
|
||||
False,
|
||||
self.context,
|
||||
f"Raggiunto massimo numero di iterazioni ({self.max_iterations})"
|
||||
)
|
||||
|
||||
def _think(self, goal: str) -> str:
|
||||
"""LLM decide la prossima azione basata su contesto e goal"""
|
||||
|
||||
# Usa intent detection esistente (già testata e funzionante)
|
||||
from ollama_agent import OllamaToolAgent
|
||||
|
||||
# Crea un agent temporaneo per detect
|
||||
temp_agent = OllamaToolAgent(llm=self.llm, tools=list(self.tools.values()))
|
||||
detected = temp_agent._detect_intent(goal)
|
||||
|
||||
if detected:
|
||||
# Formatta come TOOL:...|
|
||||
args_str = "|".join(f"{k}={v}" for k, v in detected.arguments.items())
|
||||
result = f"TOOL:{detected.name}"
|
||||
if args_str:
|
||||
result += f"|{args_str}"
|
||||
if self.verbose:
|
||||
print(f" [DEBUG] Detected intent: {detected.name}, args: {detected.arguments}")
|
||||
return result
|
||||
|
||||
# Se non c'è intent detection, chiedi al LLM
|
||||
tool_names = ", ".join(self.tools.keys())
|
||||
user_prompt = f"Goal: {goal}\nTool: {tool_names}\nRispondi con TOOL:nome|arg=val oppure SUCCESS:risposta"
|
||||
|
||||
try:
|
||||
messages = [
|
||||
SystemMessage(content=self._system_prompt),
|
||||
HumanMessage(content=user_prompt)
|
||||
]
|
||||
response = self.llm.invoke(messages)
|
||||
if hasattr(response, 'content'):
|
||||
return response.content.strip()
|
||||
return str(response).strip()
|
||||
except Exception as e:
|
||||
return f"FAIL: Errore LLM: {str(e)}"
|
||||
|
||||
def _act(self, thought: str) -> ActionResult:
|
||||
"""Esegue l'azione decisa da _think"""
|
||||
# Parse TOOL:toolname|arg1=val1|...
|
||||
match = re.match(r'^TOOL:(\w+)(?:\|(.+))?$', thought.strip())
|
||||
|
||||
if not match:
|
||||
# Non è un'azione tool valida, ritorna come thought
|
||||
return ActionResult(
|
||||
action_name="respond",
|
||||
args={},
|
||||
result=thought,
|
||||
success=True
|
||||
)
|
||||
|
||||
tool_name = match.group(1)
|
||||
args_str = match.group(2) or "" if match.group(2) is not None else ""
|
||||
|
||||
# Parse args
|
||||
args = {}
|
||||
if args_str:
|
||||
for arg_part in args_str.split("|"):
|
||||
if '=' in arg_part:
|
||||
key, val = arg_part.split('=', 1)
|
||||
args[key.strip()] = val.strip()
|
||||
|
||||
# Esegui tool
|
||||
if tool_name not in self.tools:
|
||||
return ActionResult(
|
||||
action_name=tool_name,
|
||||
args=args,
|
||||
result=f"Tool '{tool_name}' non trovato",
|
||||
success=False,
|
||||
error=f"Unknown tool: {tool_name}"
|
||||
)
|
||||
|
||||
try:
|
||||
tool = self.tools[tool_name]
|
||||
if args:
|
||||
result = tool.invoke(args)
|
||||
else:
|
||||
result = tool.invoke({})
|
||||
|
||||
result_str = result if isinstance(result, str) else json.dumps(result, ensure_ascii=False)
|
||||
|
||||
return ActionResult(
|
||||
action_name=tool_name,
|
||||
args=args,
|
||||
result=result_str,
|
||||
success=True
|
||||
)
|
||||
except Exception as e:
|
||||
return ActionResult(
|
||||
action_name=tool_name,
|
||||
args=args,
|
||||
result=f"Errore nell'esecuzione: {str(e)}",
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
def _add_step(
|
||||
self,
|
||||
state: AgentState,
|
||||
thought: str,
|
||||
action: Optional[ActionResult] = None,
|
||||
observation: Optional[str] = None
|
||||
):
|
||||
"""Aggiunge uno step alla cronologia"""
|
||||
step = Step(
|
||||
step_number=len(self.steps) + 1,
|
||||
state=state,
|
||||
thought=thought,
|
||||
action=action,
|
||||
observation=observation
|
||||
)
|
||||
self.steps.append(step)
|
||||
|
||||
def _build_result(self, success: bool, result: str, error: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Costruisce il risultato finale"""
|
||||
return {
|
||||
"success": success,
|
||||
"result": result,
|
||||
"error": error,
|
||||
"iterations": len(self.steps),
|
||||
"steps": [
|
||||
{
|
||||
"step": s.step_number,
|
||||
"state": s.state.value,
|
||||
"thought": s.thought[:200] if s.thought else None,
|
||||
"action": s.action.action_name if s.action else None,
|
||||
"action_result": s.action.result[:100] if s.action and s.action.result else None,
|
||||
"observation": s.observation[:200] if s.observation else None
|
||||
}
|
||||
for s in self.steps
|
||||
]
|
||||
}
|
||||
|
||||
def get_plan_progress(self) -> Dict[str, Any]:
|
||||
"""Restituisce statistiche sullo stato del planner"""
|
||||
return {
|
||||
"total_tasks": len(self.planner.tasks),
|
||||
"plans": len(self.planner.plans)
|
||||
}
|
||||
|
||||
def run_plan(self, goals: List[str], plan_name: str = "multi_step_plan") -> Dict[str, Any]:
|
||||
"""
|
||||
Esegue un piano con più goal in sequenza.
|
||||
|
||||
Args:
|
||||
goals: Lista di goal da eseguire in ordine
|
||||
plan_name: Nome del piano
|
||||
|
||||
Returns:
|
||||
Dict con risultati per ogni goal e progresso complessivo
|
||||
"""
|
||||
# Crea piano nel planner
|
||||
plan_id = self.planner.create_plan(plan_name, goals)
|
||||
|
||||
if self.verbose:
|
||||
print(f"\n📋 PIANO: {plan_name}")
|
||||
print(f" Goal: {len(goals)}")
|
||||
print("="*60)
|
||||
|
||||
results = []
|
||||
completed = 0
|
||||
failed = 0
|
||||
|
||||
for i, goal in enumerate(goals):
|
||||
if self.verbose:
|
||||
print(f"\n--- Goal {i+1}/{len(goals)} ---")
|
||||
|
||||
result = self.run(goal, track_in_planner=True)
|
||||
results.append({
|
||||
"goal": goal,
|
||||
"success": result["success"],
|
||||
"result": result["result"][:200] if result["result"] else None,
|
||||
"error": result.get("error")
|
||||
})
|
||||
|
||||
if result["success"]:
|
||||
completed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# Risultato complessivo
|
||||
overall_success = failed == 0
|
||||
|
||||
if self.verbose:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"📊 PIANO COMPLETATO")
|
||||
print(f" Totale: {len(goals)}")
|
||||
print(f" Completati: {completed}")
|
||||
print(f" Falliti: {failed}")
|
||||
|
||||
return {
|
||||
"success": overall_success,
|
||||
"plan_id": plan_id,
|
||||
"total_goals": len(goals),
|
||||
"completed": completed,
|
||||
"failed": failed,
|
||||
"results": results,
|
||||
"plan_progress": self.planner.get_plan_progress(plan_id)
|
||||
}
|
||||
|
||||
def decompose_goal(self, complex_goal: str) -> List[str]:
|
||||
"""
|
||||
Decompone un goal complesso in sotto-goal usando LLM.
|
||||
|
||||
Args:
|
||||
complex_goal: Goal complesso da decomporre
|
||||
|
||||
Returns:
|
||||
Lista di sotto-goal
|
||||
"""
|
||||
prompt = f"""Scomponi questo goal in passi sequenziali semplici.
|
||||
|
||||
Goal complesso: {complex_goal}
|
||||
|
||||
Elenca i passi necessari, uno per riga. Ogni passo deve essere un'azione concreta.
|
||||
Non usare numeri o bullet, solo testo puro.
|
||||
|
||||
ESEMPIO:
|
||||
Goal: Prepara report vendite mensile
|
||||
Output:
|
||||
Leggi i dati di vendita dal database
|
||||
Analizza i dati e calcola totali
|
||||
Genera il report in formato markdown
|
||||
Salva il report come report_mese.md"""
|
||||
|
||||
try:
|
||||
response = self.llm.invoke([HumanMessage(content=prompt)])
|
||||
content = response.content if hasattr(response, 'content') else str(response)
|
||||
|
||||
# Parse output - una linea per passo
|
||||
lines = [l.strip() for l in content.split('\n') if l.strip()]
|
||||
# Filtra linee vuote e possibili header
|
||||
steps = [l for l in lines if len(l) > 10 and not l.startswith('#')]
|
||||
|
||||
if self.verbose:
|
||||
print(f"\n🔬 Goal decomposed into {len(steps)} steps")
|
||||
|
||||
return steps
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"⚠️ Decomposition failed: {e}")
|
||||
# Fallback: ritorna goal originale
|
||||
return [complex_goal]
|
||||
|
||||
def get_relevant_facts(self, query: str) -> List[str]:
|
||||
"""
|
||||
Cerca fatti rilevanti nella memoria.
|
||||
|
||||
Args:
|
||||
query: Query di ricerca
|
||||
|
||||
Returns:
|
||||
Lista di fatti rilevanti
|
||||
"""
|
||||
facts = self.memory.search_facts(query)
|
||||
return [f"{f['fact']} ({f['category']})" for f in facts]
|
||||
|
||||
def save_conversation(self, user_input: str, agent_response: str) -> None:
|
||||
"""
|
||||
Salva una conversazione nella memoria.
|
||||
|
||||
Args:
|
||||
user_input: Input dell'utente
|
||||
agent_response: Risposta dell'agente
|
||||
"""
|
||||
self.memory.add_conversation(user_input, agent_response)
|
||||
|
||||
def remember(self, fact: str, category: str = "general") -> str:
|
||||
"""
|
||||
Ricorda un fatto.
|
||||
|
||||
Args:
|
||||
fact: Il fatto da ricordare
|
||||
category: Categoria del fatto
|
||||
|
||||
Returns:
|
||||
Messaggio di conferma
|
||||
"""
|
||||
self.memory.add_fact(fact, category)
|
||||
return f"Ricordato: {fact}"
|
||||
|
||||
def recall(self, query: str) -> str:
|
||||
"""
|
||||
Cerca fatti nella memoria.
|
||||
|
||||
Args:
|
||||
query: Query di ricerca
|
||||
|
||||
Returns:
|
||||
Risultati della ricerca
|
||||
"""
|
||||
facts = self.memory.search_facts(query)
|
||||
if not facts:
|
||||
return f"Nessun ricordo trovato per: {query}"
|
||||
|
||||
result = f"Trovati {len(facts)} ricordi:\n"
|
||||
for f in facts[:5]:
|
||||
result += f"- {f['fact']} ({f['category']})\n"
|
||||
return result
|
||||
|
||||
def get_memory_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Restituisce statistiche della memoria.
|
||||
|
||||
Returns:
|
||||
Statistiche memoria
|
||||
"""
|
||||
return self.memory.get_memory_stats()
|
||||
|
||||
def add_preference(self, key: str, value: Any) -> str:
|
||||
"""
|
||||
Imposta una preferenza.
|
||||
|
||||
Args:
|
||||
key: Chiave preferenza
|
||||
value: Valore preferenza
|
||||
|
||||
Returns:
|
||||
Messaggio di conferma
|
||||
"""
|
||||
self.memory.set_preference(key, value)
|
||||
return f"Preferenza impostata: {key} = {value}"
|
||||
|
||||
def get_preference(self, key: str) -> str:
|
||||
"""
|
||||
Recupera una preferenza.
|
||||
|
||||
Args:
|
||||
key: Chiave preferenza
|
||||
|
||||
Returns:
|
||||
Valore della preferenza
|
||||
"""
|
||||
val = self.memory.get_preference(key)
|
||||
if val is None:
|
||||
return f"Preferenza '{key}' non trovata"
|
||||
return f"{key} = {val}"
|
||||
|
||||
|
||||
def create_react_agent(
|
||||
llm,
|
||||
tools: Optional[List[Tool]] = None,
|
||||
include_builtin_tools: bool = True,
|
||||
include_memory_tools: bool = True,
|
||||
max_iterations: int = 10,
|
||||
verbose: bool = False
|
||||
) -> ReActAgent:
|
||||
"""
|
||||
Factory function per creare un ReActAgent.
|
||||
|
||||
Args:
|
||||
llm: Modello LLM
|
||||
tools: Lista tool personalizzati
|
||||
include_builtin_tools: Se True, aggiunge tool built-in
|
||||
include_memory_tools: Se True, aggiunge tool memory
|
||||
max_iterations: Massimo iterazioni
|
||||
verbose: Debug output
|
||||
|
||||
Returns:
|
||||
ReActAgent instance
|
||||
"""
|
||||
if tools is None:
|
||||
tools = []
|
||||
|
||||
# Import built-in tools
|
||||
if include_builtin_tools:
|
||||
from ollama_agent import get_builtin_tools
|
||||
builtin = get_builtin_tools()
|
||||
tools = builtin + tools
|
||||
|
||||
return ReActAgent(
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
max_iterations=max_iterations,
|
||||
verbose=verbose,
|
||||
include_memory_tools=include_memory_tools
|
||||
)
|
||||
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Simple memory enhancement for LangCode Agent
|
||||
Implements basic persistent memory without complex dependencies
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
class SimplePersistentMemory:
|
||||
"""Simple persistent memory system for LangCode Agent"""
|
||||
|
||||
def __init__(self, memory_file: str = "./.langcode/simple_memory.json"):
|
||||
"""
|
||||
Initialize the simple persistent memory
|
||||
|
||||
Args:
|
||||
memory_file: Path to the memory file
|
||||
"""
|
||||
self.memory_file = memory_file
|
||||
self.memory_data = {
|
||||
"conversations": [],
|
||||
"facts": [],
|
||||
"preferences": {},
|
||||
"skills_used": [],
|
||||
"last_updated": None
|
||||
}
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(memory_file), exist_ok=True)
|
||||
|
||||
# Load existing memory
|
||||
self.load()
|
||||
|
||||
def load(self) -> None:
|
||||
"""Load memory from file"""
|
||||
try:
|
||||
if os.path.exists(self.memory_file):
|
||||
with open(self.memory_file, 'r') as f:
|
||||
self.memory_data = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load memory: {e}")
|
||||
# Reset to default if load fails
|
||||
self.memory_data = {
|
||||
"conversations": [],
|
||||
"facts": [],
|
||||
"preferences": {},
|
||||
"skills_used": [],
|
||||
"last_updated": None
|
||||
}
|
||||
|
||||
def save(self) -> None:
|
||||
"""Save memory to file"""
|
||||
try:
|
||||
self.memory_data["last_updated"] = datetime.now().isoformat()
|
||||
with open(self.memory_file, 'w') as f:
|
||||
json.dump(self.memory_data, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to save memory: {e}")
|
||||
|
||||
def add_conversation(self, user_input: str, agent_response: str) -> None:
|
||||
"""
|
||||
Add a conversation exchange to memory
|
||||
|
||||
Args:
|
||||
user_input: User's input
|
||||
agent_response: Agent's response
|
||||
"""
|
||||
self.memory_data["conversations"].append({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"user": user_input,
|
||||
"agent": agent_response
|
||||
})
|
||||
|
||||
# Keep only last 100 conversations to prevent file from growing too large
|
||||
if len(self.memory_data["conversations"]) > 100:
|
||||
self.memory_data["conversations"] = self.memory_data["conversations"][-100:]
|
||||
|
||||
self.save()
|
||||
|
||||
def add_fact(self, fact: str, category: str = "general") -> None:
|
||||
"""
|
||||
Add a fact to memory
|
||||
|
||||
Args:
|
||||
fact: The fact to remember
|
||||
category: Category for the fact
|
||||
"""
|
||||
self.memory_data["facts"].append({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"fact": fact,
|
||||
"category": category
|
||||
})
|
||||
|
||||
# Keep only last 50 facts
|
||||
if len(self.memory_data["facts"]) > 50:
|
||||
self.memory_data["facts"] = self.memory_data["facts"][-50:]
|
||||
|
||||
self.save()
|
||||
|
||||
def set_preference(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
Set a user preference
|
||||
|
||||
Args:
|
||||
key: Preference key
|
||||
value: Preference value
|
||||
"""
|
||||
self.memory_data["preferences"][key] = value
|
||||
self.save()
|
||||
|
||||
def get_preference(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Get a user preference
|
||||
|
||||
Args:
|
||||
key: Preference key
|
||||
default: Default value if key not found
|
||||
|
||||
Returns:
|
||||
Preference value or default
|
||||
"""
|
||||
return self.memory_data["preferences"].get(key, default)
|
||||
|
||||
def add_skill_used(self, skill_name: str) -> None:
|
||||
"""
|
||||
Record that a skill was used
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill used
|
||||
"""
|
||||
if skill_name not in self.memory_data["skills_used"]:
|
||||
self.memory_data["skills_used"].append(skill_name)
|
||||
self.save()
|
||||
|
||||
def get_recent_conversations(self, limit: int = 5) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get recent conversations
|
||||
|
||||
Args:
|
||||
limit: Maximum number of conversations to return
|
||||
|
||||
Returns:
|
||||
List of recent conversations
|
||||
"""
|
||||
return self.memory_data["conversations"][-limit:] if self.memory_data["conversations"] else []
|
||||
|
||||
def search_facts(self, query: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search facts by query
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
|
||||
Returns:
|
||||
List of matching facts
|
||||
"""
|
||||
query_lower = query.lower()
|
||||
return [
|
||||
fact for fact in self.memory_data["facts"]
|
||||
if query_lower in fact["fact"].lower()
|
||||
]
|
||||
|
||||
def get_memory_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get memory statistics
|
||||
|
||||
Returns:
|
||||
Dictionary with memory statistics
|
||||
"""
|
||||
return {
|
||||
"total_conversations": len(self.memory_data["conversations"]),
|
||||
"total_facts": len(self.memory_data["facts"]),
|
||||
"total_preferences": len(self.memory_data["preferences"]),
|
||||
"total_skills_used": len(self.memory_data["skills_used"]),
|
||||
"last_updated": self.memory_data["last_updated"]
|
||||
}
|
||||
|
||||
def create_simple_memory_tools(memory: SimplePersistentMemory) -> List:
|
||||
"""
|
||||
Create simple tools for the persistent memory system
|
||||
|
||||
Args:
|
||||
memory: Instance of SimplePersistentMemory
|
||||
|
||||
Returns:
|
||||
List of memory tools
|
||||
"""
|
||||
from langchain_core.tools import Tool
|
||||
|
||||
def remember_func(fact: str, category: str = "general") -> str:
|
||||
try:
|
||||
memory.add_fact(fact, category)
|
||||
return f"Remembered: {fact}"
|
||||
except Exception as e:
|
||||
return f"Error remembering: {str(e)}"
|
||||
|
||||
def recall_func(query: str) -> str:
|
||||
try:
|
||||
results = memory.search_facts(query)
|
||||
if not results:
|
||||
return f"No memories found for: {query}"
|
||||
|
||||
result = f"Found {len(results)} memories:\n"
|
||||
for i, fact in enumerate(results[:5], 1): # Limit to 5 results
|
||||
result += f"{i}. {fact['fact']} ({fact['category']})\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error recalling: {str(e)}"
|
||||
|
||||
def preference_func(action: str, key: str = "", value: str = "") -> str:
|
||||
try:
|
||||
if action.lower() == "set" and key and value:
|
||||
# Try to parse value as JSON for complex types
|
||||
try:
|
||||
parsed_value = json.loads(value)
|
||||
except:
|
||||
parsed_value = value
|
||||
memory.set_preference(key, parsed_value)
|
||||
return f"Set preference '{key}' = {parsed_value}"
|
||||
elif action.lower() == "get" and key:
|
||||
val = memory.get_preference(key)
|
||||
return f"Preference '{key}': {val}"
|
||||
else:
|
||||
return "Usage: preference set <key> <value> or preference get <key>"
|
||||
except Exception as e:
|
||||
return f"Error with preference: {str(e)}"
|
||||
|
||||
def stats_func() -> str:
|
||||
try:
|
||||
stats = memory.get_memory_stats()
|
||||
result = "Memory Statistics:\n"
|
||||
result += f" Conversations: {stats['total_conversations']}\n"
|
||||
result += f" Facts: {stats['total_facts']}\n"
|
||||
result += f" Preferences: {stats['total_preferences']}\n"
|
||||
result += f" Skills Used: {stats['total_skills_used']}\n"
|
||||
result += f" Last Updated: {stats['last_updated'] or 'Never'}\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error getting memory stats: {str(e)}"
|
||||
|
||||
return [
|
||||
Tool(
|
||||
name="remember",
|
||||
description="Remember a fact for future reference. Provide fact and optional category.",
|
||||
func=remember_func
|
||||
),
|
||||
Tool(
|
||||
name="recall",
|
||||
description="Recall facts by searching memory. Provide search query.",
|
||||
func=recall_func
|
||||
),
|
||||
Tool(
|
||||
name="preference",
|
||||
description="Get or set user preferences. Usage: preference set <key> <value> or preference get <key>",
|
||||
func=preference_func
|
||||
),
|
||||
Tool(
|
||||
name="memory_stats",
|
||||
description="Get statistics about the memory system.",
|
||||
func=stats_func
|
||||
)
|
||||
]
|
||||
|
||||
# Global instance
|
||||
simple_memory = SimplePersistentMemory()
|
||||
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
Test integration of enhancements with LangCode Agent
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
def test_enhancements_import():
|
||||
"""Test that our enhancements can be imported"""
|
||||
print("Testing enhancement imports...")
|
||||
|
||||
try:
|
||||
from enhancements.simple_memory import simple_memory, create_simple_memory_tools
|
||||
print("✓ Simple memory enhancement imported successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to import simple memory enhancement: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
from enhancements.planning_enhancement import enhanced_planner
|
||||
print("✓ Planning enhancement imported successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to import planning enhancement: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test_simple_memory():
|
||||
"""Test simple memory functionality"""
|
||||
print("\nTesting simple memory...")
|
||||
|
||||
try:
|
||||
from enhancements.simple_memory import simple_memory
|
||||
|
||||
# Test adding a fact
|
||||
simple_memory.add_fact("LangCode Agent supports MCP integration", "features")
|
||||
print("✓ Added fact to memory")
|
||||
|
||||
# Test recalling
|
||||
results = simple_memory.search_facts("MCP")
|
||||
if results:
|
||||
print(f"✓ Recalled {len(results)} facts about MCP")
|
||||
else:
|
||||
print("✗ Failed to recall facts about MCP")
|
||||
return False
|
||||
|
||||
# Test preference
|
||||
simple_memory.set_preference("model_provider", "ollama")
|
||||
provider = simple_memory.get_preference("model_provider")
|
||||
if provider == "ollama":
|
||||
print("✓ Preference system works")
|
||||
else:
|
||||
print(f"✗ Preference system failed: got {provider}")
|
||||
return False
|
||||
|
||||
# Test stats
|
||||
stats = simple_memory.get_memory_stats()
|
||||
if isinstance(stats, dict) and "total_facts" in stats:
|
||||
print(f"✓ Memory stats work: {stats['total_facts']} facts")
|
||||
else:
|
||||
print("✗ Memory stats failed")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Simple memory test failed: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test_planning():
|
||||
"""Test planning functionality"""
|
||||
print("\nTesting planning...")
|
||||
|
||||
try:
|
||||
from enhancements.planning_enhancement import enhanced_planner, TaskPriority
|
||||
|
||||
# Test creating a task
|
||||
task_id = enhanced_planner.create_task("Test task", TaskPriority.HIGH)
|
||||
if task_id:
|
||||
print(f"✓ Created task: {task_id}")
|
||||
else:
|
||||
print("✗ Failed to create task")
|
||||
return False
|
||||
|
||||
# Test starting task
|
||||
if enhanced_planner.start_task(task_id):
|
||||
print("✓ Started task successfully")
|
||||
else:
|
||||
print("✗ Failed to start task")
|
||||
return False
|
||||
|
||||
# Test completing task
|
||||
if enhanced_planner.complete_task(task_id, "Task completed successfully"):
|
||||
print("✓ Completed task successfully")
|
||||
else:
|
||||
print("✗ Failed to complete task")
|
||||
return False
|
||||
|
||||
# Test listing tasks by getting the tool and calling it
|
||||
planning_tools = enhanced_planner.create_planning_tools()
|
||||
list_tasks_tool = None
|
||||
for tool in planning_tools:
|
||||
if tool.name == "list_tasks":
|
||||
list_tasks_tool = tool
|
||||
break
|
||||
|
||||
if list_tasks_tool and list_tasks_tool.func:
|
||||
tasks_output = list_tasks_tool.func("")
|
||||
if "Test task" in tasks_output and "completed" in tasks_output:
|
||||
print("✓ Task listing works")
|
||||
else:
|
||||
print("✗ Task listing failed")
|
||||
print(f" Output: {tasks_output}")
|
||||
return False
|
||||
else:
|
||||
print("✗ Could not find or call list_tasks tool")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Planning test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test_tool_creation():
|
||||
"""Test that our enhancements create valid LangChain tools"""
|
||||
print("\nTesting tool creation...")
|
||||
|
||||
try:
|
||||
from enhancements.simple_memory import create_simple_memory_tools, simple_memory
|
||||
from enhancements.planning_enhancement import enhanced_planner
|
||||
|
||||
# Test simple memory tools
|
||||
memory_tools = create_simple_memory_tools(simple_memory)
|
||||
if len(memory_tools) == 4:
|
||||
print(f"✓ Simple memory tools created: {len(memory_tools)} tools")
|
||||
tool_names = [tool.name for tool in memory_tools]
|
||||
expected = ["remember", "recall", "preference", "memory_stats"]
|
||||
if all(name in tool_names for name in expected):
|
||||
print("✓ All expected memory tools present")
|
||||
else:
|
||||
print(f"✗ Missing some memory tools. Got: {tool_names}")
|
||||
return False
|
||||
else:
|
||||
print(f"✗ Expected 4 memory tools, got {len(memory_tools)}")
|
||||
return False
|
||||
|
||||
# Test planning tools
|
||||
planning_tools = enhanced_planner.create_planning_tools()
|
||||
if len(planning_tools) == 6:
|
||||
print(f"✓ Planning tools created: {len(planning_tools)} tools")
|
||||
tool_names = [tool.name for tool in planning_tools]
|
||||
expected = ["create_task", "start_task", "complete_task", "list_tasks", "get_ready_tasks", "get_plan_progress"]
|
||||
if all(name in tool_names for name in expected):
|
||||
print("✓ All expected planning tools present")
|
||||
else:
|
||||
print(f"✗ Missing some planning tools. Got: {tool_names}")
|
||||
return False
|
||||
else:
|
||||
print(f"✗ Expected 6 planning tools, got {len(planning_tools)}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Tool creation test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("=== LangCode Agent Enhancements Integration Test ===\n")
|
||||
|
||||
all_passed = True
|
||||
|
||||
all_passed &= test_enhancements_import()
|
||||
all_passed &= test_simple_memory()
|
||||
all_passed &= test_planning()
|
||||
all_passed &= test_tool_creation()
|
||||
|
||||
print("\n=== Test Results ===")
|
||||
if all_passed:
|
||||
print("✓ All tests passed! Enhancements are ready for integration.")
|
||||
return 0
|
||||
else:
|
||||
print("✗ Some tests failed. Please review the implementation.")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+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
+730
@@ -0,0 +1,730 @@
|
||||
#!/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 strip_ansi(text: str) -> str:
|
||||
"""Rimuove codici ANSI escape sequences per terminali semplici"""
|
||||
import re
|
||||
ansi_pattern = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
||||
return ansi_pattern.sub('', text)
|
||||
|
||||
|
||||
def cmd_chat(args):
|
||||
"""Avvia l'agent in modalità chat interattiva"""
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from main import create_langcode_agent, process_command
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
# Signal handler for Ctrl+C
|
||||
def handle_sigint(signum, frame):
|
||||
print("\n\n👋 Arrivederci!")
|
||||
sys.exit(0)
|
||||
|
||||
# Register signal handler
|
||||
signal.signal(signal.SIGINT, handle_sigint)
|
||||
|
||||
# Thread-safe flag for thinking indicator
|
||||
thinking_active = threading.Event()
|
||||
thinking_active.set()
|
||||
|
||||
def thinking_indicator():
|
||||
"""Mostra indicatore di pensiero mentre aspetta"""
|
||||
dots = [" ", ". ", ".. ", "..."]
|
||||
idx = 0
|
||||
while thinking_active.is_set():
|
||||
print(f"\r🤖 Pensando{dots[idx % 4]} ", end="", flush=True)
|
||||
idx += 1
|
||||
time.sleep(0.5)
|
||||
print("\r" + " " * 20 + "\r", end="") # Clear line
|
||||
|
||||
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.")
|
||||
|
||||
# Callback per processare i messaggi con l'agente
|
||||
def agent_callback(user_input: str) -> str:
|
||||
# Check if it's a command (starts with /)
|
||||
if process_command(user_input):
|
||||
return "Comando eseguito."
|
||||
|
||||
# Otherwise, treat it as a chat message
|
||||
try:
|
||||
result = agent.invoke({"messages": [HumanMessage(content=user_input)]})
|
||||
|
||||
# Extract response - support both simple agent (messages[-1]) and tool agent (response)
|
||||
if "response" in result:
|
||||
return result["response"]
|
||||
else:
|
||||
ai_message = result["messages"][-1]
|
||||
return ai_message.content if hasattr(ai_message, 'content') else str(ai_message)
|
||||
except Exception as e:
|
||||
return f"Errore: {str(e)}"
|
||||
|
||||
# Default: CLI semplice, EnhancedCLI/TerminalUI con flag
|
||||
use_enhanced = getattr(args, 'enhanced', False)
|
||||
use_terminal = getattr(args, 'terminal', False)
|
||||
|
||||
if use_terminal:
|
||||
try:
|
||||
from terminal_ui import create_terminal_ui
|
||||
term = create_terminal_ui(
|
||||
scrollback=10000,
|
||||
prompt="❯ "
|
||||
)
|
||||
term.start(agent_callback)
|
||||
return
|
||||
except ImportError:
|
||||
print("⚠️ pyte non installato. Esegui: pip install pyte")
|
||||
print("📝 Uso CLI semplice...")
|
||||
except Exception as e:
|
||||
print(f"⚠️ TerminalUI errore: {e}")
|
||||
print("📝 Uso CLI semplice...")
|
||||
|
||||
if use_enhanced:
|
||||
import errno
|
||||
import fcntl
|
||||
import select
|
||||
import threading
|
||||
|
||||
def timeout_handler():
|
||||
print("\n⚠️ EnhancedCLI timeout - uso CLI semplice")
|
||||
|
||||
try:
|
||||
from enhanced_cli import create_enhanced_cli
|
||||
cli = create_enhanced_cli(
|
||||
history_file="./.langcode/.cli_history",
|
||||
theme="default"
|
||||
)
|
||||
|
||||
# Run with timeout detection
|
||||
timeout_seconds = 30
|
||||
timer = threading.Timer(timeout_seconds, timeout_handler)
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
|
||||
cli.run(agent_callback)
|
||||
timer.cancel()
|
||||
return
|
||||
except (KeyboardInterrupt, EOFError, OSError) as e:
|
||||
# EnhancedCLI hang - fall back to simple
|
||||
print(f"⚠️ EnhancedCLI bloccato: {e}")
|
||||
print("📝 Uso CLI semplice...")
|
||||
except Exception as e:
|
||||
print(f"⚠️ EnhancedCLI errore: {e}")
|
||||
print("📝 Uso CLI semplice...")
|
||||
|
||||
# CLI semplice con input()
|
||||
# Reset terminal state dopo eventuale EnhancedCLI fallito
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(["stty", "sane"], check=False, capture_output=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" CLI SEMPLICE - digita /exit per uscire")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
sys.stdout.write("❯ ")
|
||||
sys.stdout.flush()
|
||||
user_input = sys.stdin.readline().strip()
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
if user_input.lower() in ["/exit", "/quit"]:
|
||||
print("👋 Arrivederci!")
|
||||
break
|
||||
|
||||
if user_input.startswith("/"):
|
||||
if user_input == "/help":
|
||||
print("""
|
||||
Comandi disponibili:
|
||||
/help - Mostra questo messaggio
|
||||
/status - Mostra stato del sistema
|
||||
/exit - Esci
|
||||
""")
|
||||
continue
|
||||
elif user_input == "/status":
|
||||
print("Agent: in esecuzione")
|
||||
continue
|
||||
else:
|
||||
print(f"Comando sconosciuto: {user_input}")
|
||||
continue
|
||||
|
||||
# Mostra input utente
|
||||
print(f"👤 {user_input}\n")
|
||||
|
||||
# Chiamata all'agent CON indicatore di pensiero
|
||||
thinking_active.set()
|
||||
thinker = threading.Thread(target=thinking_indicator, daemon=True)
|
||||
thinker.start()
|
||||
|
||||
try:
|
||||
response = agent_callback(user_input)
|
||||
finally:
|
||||
thinking_active.clear()
|
||||
thinker.join(timeout=1)
|
||||
|
||||
# Strip ANSI codes per terminali semplici
|
||||
clean_response = strip_ansi(response)
|
||||
|
||||
# Output con scrollback via less
|
||||
lines = clean_response.split('\n')
|
||||
if len(lines) > 10:
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_file = f".langcode/output_{timestamp}.txt"
|
||||
|
||||
# Save to file
|
||||
os.makedirs(".langcode", exist_ok=True)
|
||||
with open(output_file, "w") as f:
|
||||
f.write(clean_response)
|
||||
|
||||
print(f"🤖 Output lungo salvato in: {output_file}")
|
||||
print("💡 Visualizza con: less {0} (usa frecce per scrollare, q per uscire)\n".format(output_file))
|
||||
|
||||
# Try to show with less
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(["less", "-R", output_file], check=False)
|
||||
except FileNotFoundError:
|
||||
# less not available, just cat first part
|
||||
print("🤖 " + "\n🤖 ".join(lines[:20]))
|
||||
if len(lines) > 20:
|
||||
print(f"\n... (altre {len(lines)-20} righe nel file {output_file})")
|
||||
print()
|
||||
else:
|
||||
print(f"🤖 {clean_response}\n")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Arrivederci!")
|
||||
break
|
||||
except EOFError:
|
||||
break
|
||||
|
||||
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.add_argument("--enhanced", "-e", action="store_true", help="Usa EnhancedCLI (sperimentale)")
|
||||
chat_parser.add_argument("--terminal", "-t", action="store_true", help="Usa TerminalUI con scrollback (pyte)")
|
||||
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,16 +1,17 @@
|
||||
#!/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
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from deepagents import create_deep_agent
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from langchain.tools import Tool
|
||||
from langchain_core.tools import Tool
|
||||
|
||||
# Import custom tools
|
||||
try:
|
||||
@@ -29,22 +30,73 @@ except ImportError:
|
||||
CONFIG_AVAILABLE = False
|
||||
print("Warning: Configuration manager not available.")
|
||||
|
||||
# Import skill manager
|
||||
try:
|
||||
from skill_manager import skill_manager
|
||||
SKILL_MANAGER_AVAILABLE = True
|
||||
except ImportError:
|
||||
SKILL_MANAGER_AVAILABLE = False
|
||||
print("Warning: Skill manager not available.")
|
||||
|
||||
# Import enhancement modules
|
||||
try:
|
||||
from enhancements.simple_memory import simple_memory, create_simple_memory_tools
|
||||
from enhancements.planning_enhancement import enhanced_planner
|
||||
ENHANCEMENTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
ENHANCEMENTS_AVAILABLE = False
|
||||
print("Warning: Enhancement modules not available.")
|
||||
|
||||
# Import json for memory preference handling
|
||||
import json
|
||||
|
||||
|
||||
def load_model_from_config():
|
||||
"""
|
||||
Load the LLM model based on configuration.
|
||||
Configuration can come from environment variables or a config file.
|
||||
For simplicity, we'll use environment variables.
|
||||
Priority: 1) config_manager active config, 2) environment variables
|
||||
|
||||
Expected environment variables:
|
||||
Expected environment variables (fallback):
|
||||
- OPENAI_API_KEY: For OpenAI models
|
||||
- OLLAMA_BASE_URL: For Ollama (e.g., https://ollama.solarpi.net)
|
||||
- MODEL_PROVIDER: One of "openai", "ollama", "openrouter", etc.
|
||||
- MODEL_NAME: The specific model name (e.g., "gpt-4o", "gemma2:9b")
|
||||
"""
|
||||
# Try to use config_manager first
|
||||
if CONFIG_AVAILABLE:
|
||||
active_name = config_manager.get_active_configuration()
|
||||
if active_name:
|
||||
config = config_manager.get_configuration(active_name)
|
||||
if config:
|
||||
provider = config.get("provider", "ollama").lower()
|
||||
model_name = config.get("model_name", "gemma2:9b")
|
||||
base_url = config.get("base_url", "http://localhost:11434")
|
||||
api_key = config.get("api_key")
|
||||
|
||||
print(f"🔗 Caricamento modello da config_manager: {active_name}")
|
||||
print(f" Provider: {provider}")
|
||||
print(f" Modello: {model_name}")
|
||||
print(f" URL: {base_url}")
|
||||
|
||||
if provider == "ollama":
|
||||
return init_chat_model(f"ollama:{model_name}", base_url=base_url)
|
||||
elif provider == "openai":
|
||||
return init_chat_model(f"openai:{model_name}", api_key=api_key)
|
||||
elif provider == "openrouter":
|
||||
return init_chat_model(
|
||||
f"openai:{model_name}",
|
||||
base_url=base_url,
|
||||
api_key=api_key
|
||||
)
|
||||
|
||||
# Fallback to environment variables
|
||||
provider = os.getenv("MODEL_PROVIDER", "openai").lower()
|
||||
model_name = os.getenv("MODEL_NAME", "gpt-4o")
|
||||
|
||||
print(f"⚠️ Usando variabili d'ambiente (nessuna config attiva)")
|
||||
print(f" Provider: {provider}")
|
||||
print(f" Modello: {model_name}")
|
||||
|
||||
# Configure based on provider
|
||||
if provider == "openai":
|
||||
# Uses OPENAI_API_KEY from environment
|
||||
@@ -72,9 +124,220 @@ def load_model_from_config():
|
||||
return init_chat_model(f"openai:{model_name}")
|
||||
|
||||
|
||||
def get_mcp_tools() -> List[Tool]:
|
||||
"""
|
||||
Get MCP tools from configured MCP servers
|
||||
|
||||
Returns:
|
||||
List of MCP-derived LangChain Tool objects
|
||||
"""
|
||||
from mcp_client import MCPClient, get_mcp_tools_from_client
|
||||
|
||||
mcp_tools = []
|
||||
|
||||
if not CONFIG_AVAILABLE:
|
||||
return mcp_tools
|
||||
|
||||
try:
|
||||
# Get all MCP server configurations
|
||||
# We'll look for configurations with provider="mcp"
|
||||
all_configs = config_manager.get_configurations()
|
||||
mcp_configs = {
|
||||
name: config for name, config in all_configs.items()
|
||||
if config.get("provider") == "mcp"
|
||||
}
|
||||
|
||||
for name, config in mcp_configs.items():
|
||||
try:
|
||||
server_url = config.get("server_url")
|
||||
if not server_url:
|
||||
print(f"⚠️ MCP configuration '{name}' missing server_url")
|
||||
continue
|
||||
|
||||
headers = {}
|
||||
api_key = config.get("api_key")
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
mcp_client = MCPClient(
|
||||
server_url=server_url,
|
||||
headers=headers,
|
||||
timeout=config.get("timeout", 30)
|
||||
)
|
||||
|
||||
tools = get_mcp_tools_from_client(mcp_client)
|
||||
mcp_tools.extend(tools)
|
||||
print(f"✅ Added {len(tools)} tools from MCP server '{name}'")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load tools from MCP server '{name}': {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error loading MCP tools: {e}")
|
||||
|
||||
return mcp_tools
|
||||
|
||||
|
||||
def get_skill_tools() -> List[Tool]:
|
||||
"""
|
||||
Get skill management tools for the LangCode agent
|
||||
|
||||
Returns:
|
||||
List of skill management LangChain Tool objects
|
||||
"""
|
||||
from skill_manager import skill_manager
|
||||
|
||||
skill_tools = []
|
||||
|
||||
# Search for skills
|
||||
def search_skills_func(query: str, limit: int = 10) -> str:
|
||||
try:
|
||||
skills = skill_manager.search_skills(query, limit)
|
||||
if not skills:
|
||||
return f"No skills found for query: '{query}'"
|
||||
|
||||
result = f"Found {len(skills)} skills:\n"
|
||||
for i, skill in enumerate(skills, 1):
|
||||
result += f"{i}. {skill.get('name', 'Unknown')}: {skill.get('description', 'No description')}\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error searching skills: {str(e)}"
|
||||
|
||||
# Install a skill
|
||||
def install_skill_func(skill_name: str) -> str:
|
||||
try:
|
||||
success, message = skill_manager.install_skill(skill_name)
|
||||
return message
|
||||
except Exception as e:
|
||||
return f"Error installing skill: {str(e)}"
|
||||
|
||||
# Uninstall a skill
|
||||
def uninstall_skill_func(skill_name: str) -> str:
|
||||
try:
|
||||
success, message = skill_manager.uninstall_skill(skill_name)
|
||||
return message
|
||||
except Exception as e:
|
||||
return f"Error uninstalling skill: {str(e)}"
|
||||
|
||||
# List installed skills
|
||||
def list_installed_skills_func() -> str:
|
||||
try:
|
||||
skills = skill_manager.list_installed_skills()
|
||||
if not skills:
|
||||
return "No skills installed"
|
||||
|
||||
result = f"Installed skills ({len(skills)}):\n"
|
||||
for i, skill in enumerate(skills, 1):
|
||||
result += f"{i}. {skill.get('name', 'Unknown')}: {skill.get('description', 'No description')}\n"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error listing installed skills: {str(e)}"
|
||||
|
||||
# Get skill details
|
||||
def get_skill_details_func(skill_name: str) -> str:
|
||||
try:
|
||||
details = skill_manager.get_skill_details(skill_name)
|
||||
if not details:
|
||||
return f"Skill '{skill_name}' not found or no details available"
|
||||
|
||||
# Format the details nicely
|
||||
result = f"Skill: {details.get('name', 'Unknown')}\n"
|
||||
result += f"Description: {details.get('description', 'No description')}\n"
|
||||
|
||||
# Add other relevant details if available
|
||||
if 'version' in details:
|
||||
result += f"Version: {details['version']}\n"
|
||||
if 'author' in details:
|
||||
result += f"Author: {details['author']}\n"
|
||||
if 'category' in details:
|
||||
result += f"Category: {details['category']}\n"
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error getting skill details: {str(e)}"
|
||||
|
||||
# Create skill management tools
|
||||
skill_tools.extend([
|
||||
Tool(
|
||||
name="search_skills",
|
||||
description="Search for agent skills in the skills.sh registry. Use this to find skills by keyword or description.",
|
||||
func=search_skills_func
|
||||
),
|
||||
Tool(
|
||||
name="install_skill",
|
||||
description="Install a skill from the skills.sh registry. Provide the skill name to install.",
|
||||
func=install_skill_func
|
||||
),
|
||||
Tool(
|
||||
name="uninstall_skill",
|
||||
description="Uninstall an installed skill. Provide the skill name to remove.",
|
||||
func=uninstall_skill_func
|
||||
),
|
||||
Tool(
|
||||
name="list_installed_skills",
|
||||
description="List all currently installed skills in the local skills directory.",
|
||||
func=list_installed_skills_func
|
||||
),
|
||||
Tool(
|
||||
name="get_skill_details",
|
||||
description="Get detailed information about a specific skill. Provide the skill name to get details.",
|
||||
func=get_skill_details_func
|
||||
)
|
||||
])
|
||||
|
||||
return skill_tools
|
||||
|
||||
|
||||
def create_simple_agent(system_prompt: Optional[str] = None):
|
||||
"""
|
||||
Create a simple chat agent for providers without full tool-calling support (e.g., Ollama).
|
||||
This agent just uses direct chat without advanced tool calling features.
|
||||
"""
|
||||
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
model = load_model_from_config()
|
||||
|
||||
class SimpleAgent:
|
||||
"""Simple agent wrapper for Ollama and similar providers."""
|
||||
|
||||
def __init__(self, llm, prompt):
|
||||
self.llm = llm
|
||||
self.messages: List = []
|
||||
if prompt:
|
||||
self.messages.append(SystemMessage(content=prompt))
|
||||
|
||||
def invoke(self, input_dict):
|
||||
user_message = input_dict.get("messages", [])[-1]
|
||||
if hasattr(user_message, 'content'):
|
||||
user_content = user_message.content
|
||||
else:
|
||||
user_content = str(user_message)
|
||||
|
||||
# Add to history
|
||||
self.messages.append(HumanMessage(content=user_content))
|
||||
|
||||
# Get response
|
||||
response = self.llm.invoke(self.messages)
|
||||
self.messages.append(response)
|
||||
|
||||
return {"messages": self.messages}
|
||||
|
||||
def clear_history(self):
|
||||
"""Clear conversation history but keep system prompt."""
|
||||
system_msg = self.messages[0] if self.messages and isinstance(self.messages[0], SystemMessage) else None
|
||||
self.messages = [system_msg] if system_msg else []
|
||||
|
||||
if system_prompt is None:
|
||||
system_prompt = """You are LangCode, a helpful AI assistant.
|
||||
You can answer questions, write code, explain concepts, and help with programming tasks.
|
||||
Be concise and helpful. Respond in Italian when the user writes in Italian."""
|
||||
|
||||
return SimpleAgent(model, system_prompt)
|
||||
|
||||
|
||||
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
|
||||
@@ -111,22 +374,114 @@ def get_custom_tools() -> List[Tool]:
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load provider tools: {e}")
|
||||
|
||||
# Add MCP tools from configured servers
|
||||
try:
|
||||
mcp_tools = get_mcp_tools()
|
||||
custom_tools.extend(mcp_tools)
|
||||
print(f"✅ Added {len(mcp_tools)} MCP tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load MCP tools: {e}")
|
||||
|
||||
# Add skill management tools
|
||||
try:
|
||||
skill_tools = get_skill_tools()
|
||||
custom_tools.extend(skill_tools)
|
||||
print(f"✅ Added {len(skill_tools)} skill management tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load skill tools: {e}")
|
||||
|
||||
# Add enhancement tools
|
||||
try:
|
||||
if ENHANCEMENTS_AVAILABLE:
|
||||
# Add simple memory tools
|
||||
memory_tools = create_simple_memory_tools(simple_memory)
|
||||
custom_tools.extend(memory_tools)
|
||||
print(f"✅ Added {len(memory_tools)} simple memory tools")
|
||||
|
||||
# Add planning tools
|
||||
planning_tools = enhanced_planner.create_planning_tools()
|
||||
custom_tools.extend(planning_tools)
|
||||
print(f"✅ Added {len(planning_tools)} planning tools")
|
||||
else:
|
||||
print("⚠️ Enhancement modules not available, skipping enhancement tools")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load enhancement tools: {e}")
|
||||
|
||||
return custom_tools
|
||||
|
||||
|
||||
def create_maxclaw_agent(system_prompt: Optional[str] = None):
|
||||
def create_langcode_agent(system_prompt: Optional[str] = None, use_simple_agent: bool = False):
|
||||
"""
|
||||
Create and return a DeepAgent instance with configurable model.
|
||||
Create and return an agent instance with configurable model.
|
||||
|
||||
For Ollama uses custom OllamaToolAgent with tool calling support.
|
||||
For other providers with full tool support uses deepagents.
|
||||
"""
|
||||
# Try to get config info for Ollama detection
|
||||
is_ollama = False
|
||||
if CONFIG_AVAILABLE:
|
||||
active_name = config_manager.get_active_configuration()
|
||||
if active_name:
|
||||
config = config_manager.get_configuration(active_name)
|
||||
if config and config.get("provider") == "ollama":
|
||||
is_ollama = True
|
||||
|
||||
# Use OllamaToolAgent for Ollama with tool calling
|
||||
if is_ollama and not use_simple_agent:
|
||||
try:
|
||||
# Force reload the module
|
||||
import importlib
|
||||
import ollama_agent
|
||||
importlib.reload(ollama_agent)
|
||||
from ollama_agent import create_ollama_tool_agent
|
||||
|
||||
model = load_model_from_config()
|
||||
custom_tools = get_custom_tools()
|
||||
|
||||
# Enhance with memory and planning tools if available
|
||||
if ENHANCEMENTS_AVAILABLE:
|
||||
try:
|
||||
memory_tools = create_simple_memory_tools(simple_memory)
|
||||
custom_tools.extend(memory_tools)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
planning_tools = enhanced_planner.create_planning_tools()
|
||||
custom_tools.extend(planning_tools)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print("🔧 Using OllamaToolAgent with tool calling")
|
||||
return create_ollama_tool_agent(
|
||||
model=model,
|
||||
tools=custom_tools,
|
||||
system_prompt=system_prompt,
|
||||
include_builtin_tools=True
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not load OllamaToolAgent: {e}")
|
||||
print("⚠️ Falling back to simple agent")
|
||||
use_simple_agent = True
|
||||
elif is_ollama:
|
||||
print("⚠️ Using simple chat agent (Ollama detected)")
|
||||
use_simple_agent = True
|
||||
|
||||
if use_simple_agent:
|
||||
return create_simple_agent(system_prompt)
|
||||
|
||||
# Use deepagents for providers with full tool-calling support
|
||||
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.
|
||||
|
||||
You have access to:
|
||||
1. Unified provider tools for managing LLM providers (list models, test connections, etc.)
|
||||
2. Custom tools for managing Proxmox virtual machines
|
||||
3. Standard DeepAgents tools (planning, filesystem, shell, subagents)
|
||||
3. MCP tools for accessing external services and skills registries (like skills.sh)
|
||||
4. Skill management tools for discovering, installing, and managing agent skills
|
||||
5. Enhancement tools for persistent memory and advanced planning
|
||||
6. Standard DeepAgents tools (planning, filesystem, shell, subagents)
|
||||
|
||||
Use these tools when asked about:
|
||||
- LLM provider management (listing models, testing connections, setting defaults)
|
||||
@@ -320,6 +675,287 @@ def clear_screen():
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
|
||||
def skills_search(query: str):
|
||||
"""Search for skills"""
|
||||
from skill_manager import skill_manager
|
||||
|
||||
try:
|
||||
skills = skill_manager.search_skills(query)
|
||||
if not skills:
|
||||
print(f"No skills found for query: '{query}'")
|
||||
return
|
||||
|
||||
print(f"\nFound {len(skills)} skills:")
|
||||
print("=" * 50)
|
||||
for i, skill in enumerate(skills, 1):
|
||||
print(f"{i}. {skill.get('name', 'Unknown')}")
|
||||
print(f" Description: {skill.get('description', 'No description')}")
|
||||
if 'version' in skill:
|
||||
print(f" Version: {skill['version']}")
|
||||
if 'author' in skill:
|
||||
print(f" Author: {skill['author']}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"Error searching skills: {str(e)}")
|
||||
|
||||
|
||||
def skills_install(skill_name: str):
|
||||
"""Install a skill"""
|
||||
from skill_manager import skill_manager
|
||||
|
||||
try:
|
||||
success, message = skill_manager.install_skill(skill_name)
|
||||
if success:
|
||||
print(f"✅ {message}")
|
||||
else:
|
||||
print(f"❌ {message}")
|
||||
except Exception as e:
|
||||
print(f"Error installing skill: {str(e)}")
|
||||
|
||||
|
||||
def skills_uninstall(skill_name: str):
|
||||
"""Uninstall a skill"""
|
||||
from skill_manager import skill_manager
|
||||
|
||||
try:
|
||||
success, message = skill_manager.uninstall_skill(skill_name)
|
||||
if success:
|
||||
print(f"✅ {message}")
|
||||
else:
|
||||
print(f"❌ {message}")
|
||||
except Exception as e:
|
||||
print(f"Error uninstalling skill: {str(e)}")
|
||||
|
||||
|
||||
def skills_list():
|
||||
"""List installed skills"""
|
||||
from skill_manager import skill_manager
|
||||
|
||||
try:
|
||||
skills = skill_manager.list_installed_skills()
|
||||
if not skills:
|
||||
print("No skills installed")
|
||||
return
|
||||
|
||||
print(f"\nInstalled skills ({len(skills)}):")
|
||||
print("=" * 50)
|
||||
for i, skill in enumerate(skills, 1):
|
||||
print(f"{i}. {skill.get('name', 'Unknown')}")
|
||||
print(f" Description: {skill.get('description', 'No description')}")
|
||||
if 'version' in skill:
|
||||
print(f" Version: {skill['version']}")
|
||||
if 'author' in skill:
|
||||
print(f" Author: {skill['author']}")
|
||||
print(f" Path: {skill.get('path', 'Unknown')}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"Error listing installed skills: {str(e)}")
|
||||
|
||||
|
||||
def skills_details(skill_name: str):
|
||||
"""Show skill details"""
|
||||
from skill_manager import skill_manager
|
||||
|
||||
try:
|
||||
details = skill_manager.get_skill_details(skill_name)
|
||||
if not details:
|
||||
print(f"Skill '{skill_name}' not found or no details available")
|
||||
return
|
||||
|
||||
print(f"\nSkill Details: {details.get('name', 'Unknown')}")
|
||||
print("=" * 50)
|
||||
print(f"Description: {details.get('description', 'No description')}")
|
||||
if 'version' in details:
|
||||
print(f"Version: {details['version']}")
|
||||
if 'author' in details:
|
||||
print(f"Author: {details['author']}")
|
||||
if 'category' in details:
|
||||
print(f"Category: {details['category']}")
|
||||
if 'repository' in details:
|
||||
print(f"Repository: {details['repository']}")
|
||||
if 'homepage' in details:
|
||||
print(f"Homepage: {details['homepage']}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"Error getting skill details: {str(e)}")
|
||||
|
||||
|
||||
def memory_remember(text: str):
|
||||
"""Remember a fact for future reference"""
|
||||
from enhancements.simple_memory import simple_memory
|
||||
try:
|
||||
# Clean the text - remove any quotes
|
||||
clean_text = _clean_text(text)
|
||||
if not clean_text:
|
||||
print("❌ Errore: il testo non può essere vuoto")
|
||||
return
|
||||
# Default category is "general"
|
||||
simple_memory.add_fact(clean_text, "general")
|
||||
print(f"✅ Remembered: {clean_text[:100]}{'...' if len(clean_text) > 100 else ''}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error remembering: {e}")
|
||||
|
||||
def memory_recall(query: str):
|
||||
"""Recall facts by searching memory"""
|
||||
from enhancements.simple_memory import simple_memory
|
||||
try:
|
||||
results = simple_memory.search_facts(query)
|
||||
if not results:
|
||||
print(f"No memories found for: {query}")
|
||||
return
|
||||
|
||||
print(f"\nFound {len(results)} memories:")
|
||||
print("=" * 50)
|
||||
for i, fact in enumerate(results[:10], 1): # Limit to 10 results
|
||||
print(f"{i}. {fact['fact']}")
|
||||
print(f" Category: {fact['category']}")
|
||||
print(f" Time: {fact['timestamp']}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"Error recalling: {e}")
|
||||
|
||||
def memory_set_preference(key: str, value: str):
|
||||
"""Set a user preference"""
|
||||
from enhancements.simple_memory import simple_memory
|
||||
try:
|
||||
# Try to parse value as JSON for complex types
|
||||
try:
|
||||
parsed_val = json.loads(value)
|
||||
except:
|
||||
parsed_val = value
|
||||
simple_memory.set_preference(key, parsed_val)
|
||||
print(f"✅ Set preference '{key}' = {parsed_val}")
|
||||
except Exception as e:
|
||||
print(f"Error setting preference: {e}")
|
||||
|
||||
def memory_preference_get(key: str):
|
||||
"""Get a user preference"""
|
||||
from enhancements.simple_memory import simple_memory
|
||||
try:
|
||||
val = simple_memory.get_preference(key)
|
||||
if val is not None:
|
||||
print(f"Preference '{key}': {val}")
|
||||
else:
|
||||
print(f"Preference '{key}' not found")
|
||||
except Exception as e:
|
||||
print(f"Error getting preference: {e}")
|
||||
|
||||
def memory_stats():
|
||||
"""Get memory statistics"""
|
||||
from enhancements.simple_memory import simple_memory
|
||||
try:
|
||||
stats = simple_memory.get_memory_stats()
|
||||
print("\nMemory Statistics:")
|
||||
print("=" * 30)
|
||||
print(f"Total Conversations: {stats['total_conversations']}")
|
||||
print(f"Total Facts: {stats['total_facts']}")
|
||||
print(f"Total Preferences: {stats['total_preferences']}")
|
||||
print(f"Total Skills Used: {stats['total_skills_used']}")
|
||||
print(f"Last Updated: {stats['last_updated'] or 'Never'}")
|
||||
except Exception as e:
|
||||
print(f"Error getting memory stats: {e}")
|
||||
|
||||
def _strip_quotes(s: str) -> str:
|
||||
"""Strip surrounding quotes from a string"""
|
||||
s = s.strip()
|
||||
if len(s) >= 2:
|
||||
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
|
||||
return s[1:-1]
|
||||
return s
|
||||
|
||||
def _clean_text(s: str) -> str:
|
||||
"""Remove all quotes (single and double) from a string"""
|
||||
return s.replace('"', '').replace("'", '').strip()
|
||||
|
||||
def plan_create(name: str, tasks_string: str):
|
||||
"""Create a plan with multiple tasks"""
|
||||
from enhancements.planning_enhancement import enhanced_planner
|
||||
try:
|
||||
# Strip quotes from name
|
||||
name = _strip_quotes(name)
|
||||
# Split tasks by semicolon and clean up
|
||||
task_descriptions = [_strip_quotes(t.strip()) for t in tasks_string.split(';') if t.strip()]
|
||||
if not task_descriptions:
|
||||
print("❌ No tasks provided")
|
||||
return
|
||||
|
||||
plan_id = enhanced_planner.create_plan(name, task_descriptions)
|
||||
print(f"✅ Created plan '{name}' with ID: {plan_id}")
|
||||
print(f" Tasks: {len(task_descriptions)}")
|
||||
for i, desc in enumerate(task_descriptions, 1):
|
||||
print(f" {i}. {desc}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating plan: {e}")
|
||||
|
||||
def plan_list():
|
||||
"""List all plans"""
|
||||
from enhancements.planning_enhancement import enhanced_planner
|
||||
try:
|
||||
if not enhanced_planner.plans:
|
||||
print("No plans created yet")
|
||||
return
|
||||
|
||||
print(f"\nPlans ({len(enhanced_planner.plans)}):")
|
||||
print("=" * 50)
|
||||
for plan_id, task_ids in enhanced_planner.plans.items():
|
||||
task_count = len(task_ids)
|
||||
print(f"Plan ID: {plan_id}")
|
||||
print(f" Tasks: {task_count}")
|
||||
# Show first few tasks
|
||||
for i, task_id in enumerate(task_ids[:3], 1):
|
||||
if task_id in enhanced_planner.tasks:
|
||||
task = enhanced_planner.tasks[task_id]
|
||||
print(f" {i}. [{task.status.value}] {task.description}")
|
||||
else:
|
||||
print(f" {i}. [unknown] Task {task_id} not found")
|
||||
if len(task_ids) > 3:
|
||||
print(f" ... and {len(task_ids) - 3} more tasks")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"Error listing plans: {e}")
|
||||
|
||||
def plan_progress(plan_id: str):
|
||||
"""Get progress information for a plan"""
|
||||
from enhancements.planning_enhancement import enhanced_planner
|
||||
try:
|
||||
progress = enhanced_planner.get_plan_progress(plan_id)
|
||||
if "error" in progress:
|
||||
print(f"❌ {progress['error']}")
|
||||
return
|
||||
|
||||
print(f"\nPlan {plan_id} Progress:")
|
||||
print("=" * 40)
|
||||
print(f"Total Tasks: {progress['total_tasks']}")
|
||||
print(f"Completed: {progress['completed_tasks']}")
|
||||
print(f"Failed: {progress['failed_tasks']}")
|
||||
print(f"In Progress: {progress['in_progress_tasks']}")
|
||||
print(f"Pending: {progress['pending_tasks']}")
|
||||
print(f"Progress: {progress['progress_percentage']:.1f}%")
|
||||
except Exception as e:
|
||||
print(f"Error getting plan progress: {e}")
|
||||
|
||||
def plan_start(task_id: str):
|
||||
"""Start working on a task"""
|
||||
from enhancements.planning_enhancement import enhanced_planner
|
||||
try:
|
||||
if enhanced_planner.start_task(task_id):
|
||||
print(f"✅ Started task {task_id}")
|
||||
else:
|
||||
print(f"❌ Failed to start task {task_id} - check dependencies or task ID")
|
||||
except Exception as e:
|
||||
print(f"Error starting task: {e}")
|
||||
|
||||
def plan_complete(task_id: str):
|
||||
"""Mark a task as completed"""
|
||||
from enhancements.planning_enhancement import enhanced_planner
|
||||
try:
|
||||
if enhanced_planner.complete_task(task_id):
|
||||
print(f"✅ Completed task {task_id}")
|
||||
else:
|
||||
print(f"❌ Failed to complete task {task_id} - check task ID or status")
|
||||
except Exception as e:
|
||||
print(f"Error completing task: {e}")
|
||||
|
||||
def process_command(user_input: str) -> bool:
|
||||
"""
|
||||
Process a command starting with "/"
|
||||
@@ -355,10 +991,105 @@ def process_command(user_input: str) -> bool:
|
||||
else:
|
||||
print(f"❌ Sottocomando sconosciuto: {args[0]}")
|
||||
print("Sottocomandi disponibili: list, active, test, use <name>")
|
||||
elif command == 'skills':
|
||||
if not args:
|
||||
print("❌ Specifica un sottocomando: search, install, uninstall, list, details")
|
||||
elif args[0] == 'search' and len(args) > 1:
|
||||
skills_search(' '.join(args[1:]))
|
||||
elif args[0] == 'install' and len(args) > 1:
|
||||
skills_install(args[1])
|
||||
elif args[0] == 'uninstall' and len(args) > 1:
|
||||
skills_uninstall(args[1])
|
||||
elif args[0] == 'list':
|
||||
skills_list()
|
||||
elif args[0] == 'details' and len(args) > 1:
|
||||
skills_details(args[1])
|
||||
else:
|
||||
print("❌ Sottocomando sconosciuto o argomenti mancanti")
|
||||
print("Sottocomandi disponibili: search <query>, install <name>, uninstall <name>, list, details <name>")
|
||||
elif command == 'memory':
|
||||
if not args:
|
||||
print("❌ Specifica un sottocomando: remember, recall, preference, stats")
|
||||
elif args[0] == 'remember' and len(args) > 1:
|
||||
memory_remember(' '.join(args[1:]))
|
||||
elif args[0] == 'recall' and len(args) > 1:
|
||||
memory_recall(' '.join(args[1:]))
|
||||
elif args[0] == 'preference':
|
||||
if len(args) >= 4 and args[1] == 'set':
|
||||
# Join everything after 'set' as the value (key is args[2], value is args[3:])
|
||||
key = args[2]
|
||||
value = ' '.join(args[3:])
|
||||
memory_set_preference(key, value)
|
||||
elif len(args) == 3 and args[1] == 'get':
|
||||
key = args[2]
|
||||
memory_preference_get(key)
|
||||
else:
|
||||
print("❌ Usage: preference set <key> <value> or preference get <key>")
|
||||
elif args[0] == 'stats':
|
||||
memory_stats()
|
||||
else:
|
||||
print("❌ Sottocomando sconosciuto o argomenti mancanti")
|
||||
print("Sottocomandi disponibili: remember <text>, recall <query>, preference set <key> <value>, preference get <key>, stats")
|
||||
elif command == 'plan':
|
||||
if not args:
|
||||
print("❌ Specifica un sottocomando: create, list, progress, start, complete")
|
||||
elif args[0] == 'create':
|
||||
# Handle plan creation with special parsing for quoted strings
|
||||
# Format: /plan create "Plan Name" "Task 1; Task 2; Task 3"
|
||||
# Or: /plan create PlanName "Task1; Task2; Task3"
|
||||
import re
|
||||
# Get the raw part after "plan create"
|
||||
raw_part = command_line[len("plan create "):]
|
||||
|
||||
# Find all quoted strings (handles both " and ' quotes)
|
||||
quoted_pattern = r'"([^"]*)"|\'([^\']*)\''
|
||||
matches = re.findall(quoted_pattern, raw_part)
|
||||
|
||||
if len(matches) >= 2:
|
||||
# Both name and tasks are quoted
|
||||
name = matches[0][0] or matches[0][1]
|
||||
tasks = matches[1][0] or matches[1][1]
|
||||
plan_create(name, tasks)
|
||||
elif len(matches) == 1:
|
||||
# Only one quoted string - it's the tasks, name is the first unquoted word
|
||||
tasks = matches[0][0] or matches[0][1]
|
||||
# Find the name as the first word before the quoted part
|
||||
unquoted_start = raw_part.find('"') if '"' in raw_part else raw_part.find("'")
|
||||
name = raw_part[:unquoted_start].strip()
|
||||
if name and tasks:
|
||||
plan_create(name, tasks)
|
||||
else:
|
||||
print("❌ Usage: /plan create <nome> \"<task1; task2; ...>\"")
|
||||
else:
|
||||
# No quotes - use simple split
|
||||
if len(args) >= 3:
|
||||
name = args[1]
|
||||
tasks = ' '.join(args[2:])
|
||||
plan_create(name, tasks)
|
||||
else:
|
||||
print("❌ Usage: /plan create <nome> \"<task1; task2; ...>\"")
|
||||
elif args[0] == 'list':
|
||||
plan_list()
|
||||
elif args[0] == 'progress' and len(args) > 1:
|
||||
plan_progress(args[1])
|
||||
elif args[0] == 'start' and len(args) > 1:
|
||||
plan_start(args[1])
|
||||
elif args[0] == 'complete' and len(args) > 1:
|
||||
plan_complete(args[1])
|
||||
else:
|
||||
print("❌ Sottocomando sconosciuto o argomenti mancanti")
|
||||
print("Sottocomandi disponibili: create <name> <task1> <task2> ..., list, progress <plan_id>, start <task_id>, complete <task_id>")
|
||||
elif command == 'models':
|
||||
list_models()
|
||||
elif command == 'clear':
|
||||
clear_screen()
|
||||
elif command == 'reset':
|
||||
# Reset conversation history (for simple agent)
|
||||
if hasattr(agent, 'clear_history'):
|
||||
agent.clear_history()
|
||||
print("✅ Cronologia conversazione pulita!")
|
||||
else:
|
||||
print("ℹ️ Reset non disponibile per questo tipo di agent.")
|
||||
elif command in ['exit', 'quit']:
|
||||
print("👋 Goodbye!")
|
||||
sys.exit(0)
|
||||
@@ -371,12 +1102,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
|
||||
@@ -405,12 +1136,38 @@ def main():
|
||||
|
||||
# Otherwise, treat it as a chat message
|
||||
try:
|
||||
print("MAIN: before invoke", flush=True)
|
||||
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"MAIN: after invoke, result type={type(result)}", flush=True)
|
||||
|
||||
# Extract the AI's response (works for both simple and tool agent)
|
||||
print(f"MAIN: result keys={result.keys() if hasattr(result, 'keys') else 'NO KEYS'}")
|
||||
if result and "response" in result:
|
||||
# OllamaToolAgent returns a separate response field
|
||||
response_text = result["response"]
|
||||
else:
|
||||
# Simple agent returns the last message
|
||||
ai_message = result["messages"][-1]
|
||||
response_text = ai_message.content if hasattr(ai_message, 'content') else str(ai_message)
|
||||
print(f"MAIN: response_text={response_text[:100] if response_text else 'EMPTY'}...")
|
||||
|
||||
print(f"\n🤖 LangCode: {response_text}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {str(e)}")
|
||||
error_msg = str(e)
|
||||
if not error_msg:
|
||||
error_msg = f"{type(e).__name__} (nessun messaggio)"
|
||||
print(f"\n❌ Errore: {error_msg}")
|
||||
|
||||
# Suggerimenti basati sul tipo di errore
|
||||
exc_type = type(e).__name__
|
||||
if "Connection" in exc_type or "Connect" in str(e):
|
||||
print("💡 Suggerimento: Verifica che il server Ollama sia raggiungibile.")
|
||||
print(f" URL: {os.getenv('OLLAMA_BASE_URL', 'http://localhost:11434')}")
|
||||
elif "Timeout" in exc_type or "timeout" in str(e).lower():
|
||||
print("💡 Suggerimento: Il server è lento. Prova con un modello più piccolo.")
|
||||
elif "model" in str(e).lower() or "Model" in exc_type:
|
||||
print("💡 Suggerimento: Il modello specificato potrebbe non esistere.")
|
||||
print(f" Modello: {os.getenv('MODEL_NAME', 'non impostato')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to initialize agent: {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()
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
MCP (Model Context Protocol) client for LangCode Agent
|
||||
Enables integration with MCP servers like skills.sh to discover and use tools
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
from typing import Dict, List, Any, Optional
|
||||
from langchain_core.tools import Tool
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MCPClient:
|
||||
"""Client for communicating with MCP (Model Context Protocol) servers"""
|
||||
|
||||
def __init__(self, server_url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 30):
|
||||
"""
|
||||
Initialize MCP client
|
||||
|
||||
Args:
|
||||
server_url: URL of the MCP server
|
||||
headers: Optional headers for authentication
|
||||
timeout: Request timeout in seconds
|
||||
"""
|
||||
self.server_url = server_url.rstrip('/')
|
||||
self.headers = headers or {}
|
||||
self.timeout = timeout
|
||||
self._available_tools: Optional[List[Dict[str, Any]]] = None
|
||||
|
||||
def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Make a JSON-RPC request to the MCP server
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
endpoint: API endpoint
|
||||
data: Optional data to send
|
||||
|
||||
Returns:
|
||||
Response data as dictionary
|
||||
|
||||
Raises:
|
||||
Exception: If request fails
|
||||
"""
|
||||
url = f"{self.server_url}{endpoint}"
|
||||
|
||||
# For MCP, we typically use JSON-RPC over HTTP POST
|
||||
if method.upper() == "POST":
|
||||
# JSON-RPC 2.0 format
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": endpoint.lstrip('/'), # Remove leading slash for method name
|
||||
"params": data or {},
|
||||
"id": 1
|
||||
}
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={**self.headers, "Content-Type": "application/json"},
|
||||
timeout=self.timeout
|
||||
)
|
||||
else:
|
||||
response = requests.request(
|
||||
method,
|
||||
url,
|
||||
headers=self.headers,
|
||||
params=data,
|
||||
timeout=self.timeout
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# Handle JSON-RPC response
|
||||
if method.upper() == "POST":
|
||||
result = response.json()
|
||||
if "error" in result:
|
||||
raise Exception(f"MCP error: {result['error']}")
|
||||
return result.get("result", {})
|
||||
else:
|
||||
return response.json()
|
||||
|
||||
def list_tools(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List available tools from the MCP server
|
||||
|
||||
Returns:
|
||||
List of tool specifications
|
||||
"""
|
||||
if self._available_tools is not None:
|
||||
return self._available_tools
|
||||
|
||||
try:
|
||||
# MCP standard method for listing tools
|
||||
result = self._make_request("POST", "/tools/list")
|
||||
self._available_tools = result.get("tools", [])
|
||||
logger.info(f"Discovered {len(self._available_tools)} tools from MCP server {self.server_url}")
|
||||
return self._available_tools
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list tools from MCP server {self.server_url}: {e}")
|
||||
self._available_tools = []
|
||||
return []
|
||||
|
||||
def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
Call a specific tool on the MCP server
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to call
|
||||
arguments: Arguments to pass to the tool
|
||||
|
||||
Returns:
|
||||
Tool execution result
|
||||
"""
|
||||
try:
|
||||
result = self._make_request("POST", "/tools/call", {
|
||||
"name": tool_name,
|
||||
"arguments": arguments
|
||||
})
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to call tool {tool_name} on MCP server {self.server_url}: {e}")
|
||||
raise Exception(f"MCP tool call failed: {e}")
|
||||
|
||||
def refresh_tools(self):
|
||||
"""Refresh the cached list of available tools"""
|
||||
self._available_tools = None
|
||||
self.list_tools()
|
||||
|
||||
|
||||
def mcp_to_langchain_tool(mcp_client: MCPClient, tool_spec: Dict[str, Any]) -> Tool:
|
||||
"""
|
||||
Convert an MCP tool specification to a LangChain Tool
|
||||
|
||||
Args:
|
||||
mcp_client: MCP client instance
|
||||
tool_spec: Tool specification from MCP server
|
||||
|
||||
Returns:
|
||||
LangChain Tool object
|
||||
"""
|
||||
def tool_function(*args, **kwargs):
|
||||
# Handle both positional and keyword arguments
|
||||
if args and len(args) == 1 and isinstance(args[0], dict):
|
||||
# Single dict argument passed positionally
|
||||
tool_args = args[0]
|
||||
elif kwargs:
|
||||
# Keyword arguments
|
||||
tool_args = kwargs
|
||||
else:
|
||||
# No arguments or multiple positional args (shouldn't happen for MCP tools)
|
||||
tool_args = {}
|
||||
|
||||
try:
|
||||
result = mcp_client.call_tool(tool_spec["name"], tool_args)
|
||||
# Convert result to string if it isn't already
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
else:
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error calling MCP tool {tool_spec['name']}: {str(e)}"
|
||||
|
||||
# Extract description, default to empty string if not provided
|
||||
description = tool_spec.get("description", "")
|
||||
if not description:
|
||||
description = f"MCP tool: {tool_spec['name']}"
|
||||
|
||||
return Tool(
|
||||
name=tool_spec["name"],
|
||||
description=description,
|
||||
func=tool_function
|
||||
)
|
||||
|
||||
|
||||
def get_mcp_tools_from_client(mcp_client: MCPClient) -> List[Tool]:
|
||||
"""
|
||||
Get all available tools from an MCP client as LangChain Tools
|
||||
|
||||
Args:
|
||||
mcp_client: Configured MCP client
|
||||
|
||||
Returns:
|
||||
List of LangChain Tool objects
|
||||
"""
|
||||
tools = []
|
||||
try:
|
||||
tool_specs = mcp_client.list_tools()
|
||||
for tool_spec in tool_specs:
|
||||
langchain_tool = mcp_to_langchain_tool(mcp_client, tool_spec)
|
||||
tools.append(langchain_tool)
|
||||
logger.info(f"Converted {len(tools)} MCP tools to LangChain Tools")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get tools from MCP client: {e}")
|
||||
|
||||
return tools
|
||||
+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"})
|
||||
|
||||
+528
@@ -0,0 +1,528 @@
|
||||
"""
|
||||
Ollama Agent with Tool Calling Support
|
||||
|
||||
Questo modulo implementa un agente che supporta tool calling per Ollama
|
||||
usando un approccio di parsing delle risposte invece di bind_tools.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
import subprocess
|
||||
from typing import List, Dict, Any, Optional, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
|
||||
from langchain_core.tools import tool, Tool
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TOOL CALL PATTERNS
|
||||
# =============================================================================
|
||||
|
||||
# Old format: [TOOL_CALL]...[/TOOL_CALL]
|
||||
TOOL_CALL_PATTERN_OLD = r'\[TOOL_CALL\]\s*(.*?)\s*\[\/TOOL_CALL\]'
|
||||
|
||||
# New format: TOOL:toolname|arg1=val1|arg2=val2
|
||||
TOOL_CALL_PATTERN_NEW = r'^TOOL:(\w+)(?:\|(.*))?$'
|
||||
|
||||
TOOL_RESULT_PATTERN = r'\[TOOL_RESULT\]\s*(.*?)\s*\[\/TOOL_RESULT\]'
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BASIC BUILT-IN TOOLS
|
||||
# =============================================================================
|
||||
|
||||
def get_builtin_tools() -> List[Tool]:
|
||||
"""
|
||||
Restituisce i tool built-in disponibili per l'agente.
|
||||
"""
|
||||
tools = []
|
||||
|
||||
# Tool: Info sistema - usa @tool decorator
|
||||
@tool
|
||||
def system_info() -> str:
|
||||
"""Get system information (OS, Python version, current directory)."""
|
||||
import platform
|
||||
import os
|
||||
info = []
|
||||
info.append(f"OS: {platform.system()} {platform.release()}")
|
||||
info.append(f"Python: {platform.python_version()}")
|
||||
info.append(f"Current dir: {os.getcwd()}")
|
||||
info.append(f"User: {os.getenv('USER', 'unknown')}")
|
||||
return "\n".join(info)
|
||||
|
||||
tools.append(system_info)
|
||||
|
||||
# Tool: Lista file
|
||||
@tool
|
||||
def ls(path: str) -> str:
|
||||
"""List files in a directory."""
|
||||
try:
|
||||
dir_path = Path(path).expanduser()
|
||||
if not dir_path.exists():
|
||||
return f"Error: Directory '{path}' not found"
|
||||
if not dir_path.is_dir():
|
||||
return f"Error: '{path}' is not a directory"
|
||||
|
||||
items = []
|
||||
for item in sorted(dir_path.iterdir()):
|
||||
item_type = "DIR" if item.is_dir() else "FILE"
|
||||
items.append(f"{item_type}: {item.name}")
|
||||
return "\n".join(items) if items else "Directory is empty"
|
||||
except Exception as e:
|
||||
return f"Error listing directory: {str(e)}"
|
||||
|
||||
tools.append(ls)
|
||||
|
||||
# Tool: Esegui comando shell
|
||||
@tool
|
||||
def execute(command: str) -> str:
|
||||
"""Execute a shell command. Use this for file operations, running scripts, git commands, etc."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
output = []
|
||||
if result.stdout:
|
||||
output.append(f"STDOUT:\n{result.stdout}")
|
||||
if result.stderr:
|
||||
output.append(f"STDERR:\n{result.stderr}")
|
||||
if result.returncode != 0:
|
||||
output.append(f"Exit code: {result.returncode}")
|
||||
return "\n".join(output) if output else "Command executed with no output"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Command timed out after 30 seconds"
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
tools.append(execute)
|
||||
|
||||
# Tool: Leggi file
|
||||
@tool
|
||||
def read_file(path: str) -> str:
|
||||
"""Read the contents of a file."""
|
||||
try:
|
||||
file_path = Path(path).expanduser()
|
||||
if not file_path.exists():
|
||||
return f"Error: File '{path}' not found"
|
||||
if file_path.is_dir():
|
||||
return f"Error: '{path}' is a directory, not a file"
|
||||
return file_path.read_text(encoding='utf-8')
|
||||
except Exception as e:
|
||||
return f"Error reading file: {str(e)}"
|
||||
|
||||
tools.append(read_file)
|
||||
|
||||
# Tool: Scrivi file
|
||||
@tool
|
||||
def write_file(path: str, content: str) -> str:
|
||||
"""Write content to a file. WARNING: overwrites existing files!"""
|
||||
try:
|
||||
file_path = Path(path).expanduser()
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content, encoding='utf-8')
|
||||
return f"Successfully wrote to {path}"
|
||||
except Exception as e:
|
||||
return f"Error writing file: {str(e)}"
|
||||
|
||||
tools.append(write_file)
|
||||
|
||||
# Tool: Cerca file
|
||||
@tool
|
||||
def search(directory: str, pattern: str) -> str:
|
||||
"""Search for files by name pattern."""
|
||||
try:
|
||||
dir_path = Path(directory).expanduser()
|
||||
if not dir_path.exists():
|
||||
return f"Error: Directory '{directory}' not found"
|
||||
|
||||
matches = list(dir_path.rglob(pattern))
|
||||
if not matches:
|
||||
return f"No files matching '{pattern}' found in {directory}"
|
||||
|
||||
results = []
|
||||
for match in matches[:50]:
|
||||
results.append(str(match))
|
||||
|
||||
return f"Found {len(matches)} matches (showing first 50):\n" + "\n".join(results)
|
||||
except Exception as e:
|
||||
return f"Error searching files: {str(e)}"
|
||||
|
||||
tools.append(search)
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
"""Rappresenta una chiamata a tool"""
|
||||
id: str
|
||||
name: str
|
||||
arguments: Dict[str, Any]
|
||||
|
||||
|
||||
class OllamaToolAgent:
|
||||
"""
|
||||
Agente che supporta tool calling per Ollama e provider simili.
|
||||
|
||||
Strategia: Il modello risponde con marcatori speciali per le chiamate tool.
|
||||
Noi parsiamo, eseguiamo, e riportiamo i risultati.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm,
|
||||
tools: List[Tool],
|
||||
system_prompt: Optional[str] = None,
|
||||
max_iterations: int = 10
|
||||
):
|
||||
self.llm = llm
|
||||
self.tools = {tool.name: tool for tool in tools}
|
||||
self.max_iterations = max_iterations
|
||||
|
||||
# Costruisci il system prompt con la lista dei tool
|
||||
if system_prompt is None:
|
||||
system_prompt = self._default_system_prompt()
|
||||
else:
|
||||
system_prompt = system_prompt + "\n\n" + self._tool_instructions()
|
||||
|
||||
self.messages: List[BaseMessage] = [SystemMessage(content=system_prompt)]
|
||||
|
||||
# Tool per la memoria
|
||||
self.short_term_memory: List[Dict] = []
|
||||
|
||||
def _default_system_prompt(self) -> str:
|
||||
"""System prompt di default"""
|
||||
return """You are a helpful AI assistant. When users ask about:
|
||||
- system info, OS, operating system, what computer: respond with TOOL:system_info
|
||||
- list files, directory contents, ls: respond with TOOL:ls|path=.
|
||||
|
||||
Example: User asks "what OS am I using?" -> You respond: TOOL:system_info
|
||||
Example: User asks "list files" -> You respond: TOOL:ls|path=.
|
||||
|
||||
For other questions, just answer normally without tools."""
|
||||
|
||||
def _tool_instructions(self) -> str:
|
||||
"""Genera le istruzioni per i tool disponibili"""
|
||||
instructions = []
|
||||
instructions.append("\n\n# AVAILABLE TOOLS\n")
|
||||
instructions.append("You have access to the following tools:\n\n")
|
||||
|
||||
for name, tool in self.tools.items():
|
||||
# Estrai la descrizione dal docstring del tool
|
||||
desc = tool.description or "No description"
|
||||
# Pulisci la descrizione da caratteri speciali
|
||||
desc = desc.replace('"', '\\"').replace('\n', ' ')
|
||||
instructions.append(f"## {name}\n")
|
||||
instructions.append(f"Description: {desc}\n")
|
||||
|
||||
# Parsa i parametri se presenti
|
||||
if hasattr(tool, 'args_schema') and tool.args_schema:
|
||||
instructions.append("Parameters:\n")
|
||||
schema = tool.args_schema
|
||||
# Handle both dict and BaseModel schemas
|
||||
try:
|
||||
model_json_schema = getattr(schema, 'model_json_schema', None) # type: ignore
|
||||
if callable(model_json_schema):
|
||||
schema = model_json_schema()
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(schema, dict) and 'properties' in schema:
|
||||
for param_name, param_info in schema['properties'].items():
|
||||
param_type = param_info.get('type', 'any')
|
||||
param_desc = param_info.get('description', '')
|
||||
required = param_name in schema.get('required', [])
|
||||
req_marker = "(required)" if required else "(optional)"
|
||||
instructions.append(f" - {param_name}: {param_type} {req_marker} - {param_desc}\n")
|
||||
|
||||
instructions.append("\n")
|
||||
|
||||
instructions.append("\n# IMPORTANT RULES:\n")
|
||||
instructions.append("1. When you need to use a tool, respond ONLY with [TOOL_CALL] blocks\n")
|
||||
instructions.append("2. After receiving the result, continue your response\n")
|
||||
instructions.append("3. For simple questions, respond directly without tools\n")
|
||||
instructions.append("4. Use valid JSON in tool calls\n")
|
||||
instructions.append("5. If a tool fails, explain the error to the user\n")
|
||||
|
||||
return ''.join(instructions)
|
||||
|
||||
def _parse_tool_calls(self, text: str) -> List[ToolCall]:
|
||||
"""Parse le chiamate tool dalla risposta del modello"""
|
||||
tool_calls = []
|
||||
|
||||
# Try new format first: TOOL:toolname|arg1=val1
|
||||
lines = text.strip().split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('TOOL:'):
|
||||
# Parse: TOOL:toolname|arg1=val1|arg2=val2
|
||||
match = re.match(r'^TOOL:(\w+)(?:\|(.+))?$', line)
|
||||
if match:
|
||||
tool_name = match.group(1)
|
||||
if tool_name in self.tools:
|
||||
args = {}
|
||||
if match.group(2):
|
||||
# Parse arguments: arg1=val1|arg2=val2
|
||||
for arg_part in match.group(2).split('|'):
|
||||
if '=' in arg_part:
|
||||
key, val = arg_part.split('=', 1)
|
||||
args[key] = val
|
||||
tool_calls.append(ToolCall(
|
||||
id=str(uuid.uuid4())[:8],
|
||||
name=tool_name,
|
||||
arguments=args
|
||||
))
|
||||
# If we found a valid tool call, return only that one
|
||||
return tool_calls
|
||||
|
||||
# Try old format: [TOOL_CALL]{...}[/TOOL_CALL]
|
||||
matches = re.finditer(TOOL_CALL_PATTERN_OLD, text, re.DOTALL)
|
||||
for match in matches:
|
||||
try:
|
||||
data = json.loads(match.group(1))
|
||||
tool_name = data.get('tool')
|
||||
if tool_name and tool_name in self.tools:
|
||||
tool_calls.append(ToolCall(
|
||||
id=str(uuid.uuid4())[:8],
|
||||
name=tool_name,
|
||||
arguments=data.get('arguments', {})
|
||||
))
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
continue
|
||||
|
||||
return tool_calls
|
||||
|
||||
def _execute_tool(self, tool_call: ToolCall) -> Any:
|
||||
"""Esegui un singolo tool"""
|
||||
if tool_call.name not in self.tools:
|
||||
available = ", ".join(self.tools.keys())
|
||||
return {
|
||||
"error": f"Tool '{tool_call.name}' not found",
|
||||
"available_tools": available
|
||||
}
|
||||
|
||||
try:
|
||||
tool = self.tools[tool_call.name]
|
||||
|
||||
# Per tool senza argomenti, non passare arguments
|
||||
if tool_call.arguments:
|
||||
result = tool.invoke(tool_call.arguments)
|
||||
else:
|
||||
result = tool.invoke({})
|
||||
|
||||
# Converti result in stringa per il modello
|
||||
if result is None:
|
||||
return {"result": "Tool executed successfully"}
|
||||
elif isinstance(result, (dict, list)):
|
||||
return {"result": json.dumps(result, ensure_ascii=False, indent=2)}
|
||||
else:
|
||||
return {"result": str(result)}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def _format_tool_result(self, tool_call: ToolCall, result: Any) -> str:
|
||||
"""Formatta il risultato di un tool per il modello"""
|
||||
return f"\n[TOOL_RESULT]\nTool: {tool_call.name}\nResult: {json.dumps(result, ensure_ascii=False)}\n[/TOOL_RESULT]\n"
|
||||
|
||||
def _detect_intent(self, text: str) -> Optional[ToolCall]:
|
||||
"""Detect intents using both patterns and LLM interpretation"""
|
||||
import re
|
||||
|
||||
text_lower = text.lower().strip()
|
||||
|
||||
# Extract potential file paths (case insensitive)
|
||||
path_match = re.search(r'([/\w.-]+\.\w+)', text, re.IGNORECASE)
|
||||
file_path = path_match.group(1) if path_match else None
|
||||
|
||||
# Execute command patterns - check FIRST for shell commands with flags
|
||||
# Shell commands like "ls -lha", "git status", "pwd", "echo" should use execute
|
||||
shell_commands = ['ls', 'git', 'cat', 'grep', 'find', 'pwd', 'echo', 'cd',
|
||||
'mkdir', 'rm', 'cp', 'mv', 'chmod', 'head', 'tail', 'sort', 'uniq']
|
||||
# Check for command alone or with arguments
|
||||
for cmd in shell_commands:
|
||||
if text_lower.strip() == cmd or text_lower.startswith(cmd + ' '):
|
||||
return ToolCall(id='intent', name='execute', arguments={'command': text.strip()})
|
||||
|
||||
# System info patterns (more comprehensive)
|
||||
system_patterns = [
|
||||
'sistema operativo', 'so sto', 'che os', 'che sistema', 'che so',
|
||||
'sto usando', 'che computer', 'che tipo di sistema',
|
||||
'versione python', 'che versione', 'dettagli sistema',
|
||||
'operating system', 'what os', 'which system', 'what system',
|
||||
'which computer', 'system info', 'python version'
|
||||
]
|
||||
if any(p in text_lower for p in system_patterns):
|
||||
return ToolCall(id='intent', name='system_info', arguments={})
|
||||
|
||||
# List files patterns
|
||||
list_patterns = [
|
||||
'lista', 'listare', 'listami', 'elenco', 'mostrami i file', 'mostrami',
|
||||
'file nella cartella', 'file del', 'cartella corrente', 'mostra i file',
|
||||
'cosa c\'è', 'cosa c\'e', 'cosa contiene', 'cosa trovo',
|
||||
'list files', 'list directory', 'ls', 'show files', 'show me',
|
||||
'what\'s in', 'what is in', 'files here', 'directory contents'
|
||||
]
|
||||
if any(p in text_lower for p in list_patterns):
|
||||
return ToolCall(id='intent', name='ls', arguments={'path': '.'})
|
||||
|
||||
# Search patterns
|
||||
search_patterns = [
|
||||
'cerca', 'cercare', 'trova', 'trovare', 'dove si trova',
|
||||
'search', 'find', 'file', '*.py', '*.md', '*.txt'
|
||||
]
|
||||
# If the message mentions file patterns, use search tool
|
||||
if 'cerca' in text_lower or 'search' in text_lower or 'find' in text_lower:
|
||||
# Extract directory and pattern
|
||||
dir_match = re.search(r'(?:in|alla|nella)\s+([/\w.-]+)', text)
|
||||
pattern_match = re.search(r'\*\.(\w+)', text)
|
||||
directory = dir_match.group(1) if dir_match else '.'
|
||||
pattern = f"*.{pattern_match.group(1)}" if pattern_match else "*"
|
||||
return ToolCall(id='intent', name='search', arguments={'directory': directory, 'pattern': pattern})
|
||||
|
||||
# Read/show file patterns - check for ANY read keyword AND a file path
|
||||
read_keywords = ['leggi', 'leggere', 'mostrami', 'apri', 'aprimi', 'visualizza',
|
||||
'vediamo', 'vedi', 'show me', 'show the', 'read', 'open',
|
||||
'display', 'view', 'contenuto', 'che dice']
|
||||
has_read_keyword = any(k in text_lower for k in read_keywords)
|
||||
|
||||
if has_read_keyword and file_path:
|
||||
return ToolCall(id='intent', name='read_file', arguments={'path': file_path})
|
||||
|
||||
# Write file patterns
|
||||
write_keywords = ['scrivi', 'scrivere', 'crea', 'creare', 'salva', 'salvare',
|
||||
'write', 'create', 'save', 'make file']
|
||||
has_write_keyword = any(k in text_lower for k in write_keywords)
|
||||
|
||||
if has_write_keyword and file_path:
|
||||
# Extract content - try multiple patterns
|
||||
content = None
|
||||
|
||||
# Pattern 1: "con dentro 'testo'" o "con 'testo'"
|
||||
content_match = re.search(r'con\s+(?:dentro\s+)?["\']([^"\']+)["\']', text, re.IGNORECASE)
|
||||
if content_match:
|
||||
content = content_match.group(1)
|
||||
|
||||
# Pattern 2: testo tra virgolette anywhere
|
||||
if not content:
|
||||
content_match = re.search(r'["\']([^"\']+)["\']', text)
|
||||
if content_match:
|
||||
content = content_match.group(1)
|
||||
|
||||
# Pattern 3: contenuto dopo "con" fino alla fine (senza virgolette)
|
||||
if not content:
|
||||
# Match: con <resto della frase senza virgolette iniziali/finali>
|
||||
after_con = re.search(r'con\s+["\']?(.+?)["\']?\s*$', text, re.IGNORECASE | re.DOTALL)
|
||||
if after_con:
|
||||
content = after_con.group(1).strip()
|
||||
|
||||
if not content:
|
||||
content = "Content not specified"
|
||||
|
||||
return ToolCall(id='intent', name='write_file', arguments={'path': file_path, 'content': content})
|
||||
|
||||
exec_patterns = [
|
||||
'esegui', 'eseguire', 'run', 'run command', 'lanciare',
|
||||
'avvia', 'comando', 'terminale', 'bash', 'shell'
|
||||
]
|
||||
if any(p in text_lower for p in exec_patterns):
|
||||
cmd_match = re.search(r'[`"]?([a-zA-Z0-9_.\s-]+)[`"]?\s*$', text)
|
||||
if cmd_match:
|
||||
return ToolCall(id='intent', name='execute', arguments={'command': cmd_match.group(1).strip()})
|
||||
|
||||
return None
|
||||
|
||||
def invoke(self, input_dict: Dict) -> Dict:
|
||||
"""
|
||||
Invoca l'agente con un input.
|
||||
|
||||
L'agente può fare multiple iterazioni tool-call prima di rispondere.
|
||||
"""
|
||||
# Estrai il messaggio utente
|
||||
user_message = input_dict.get("messages", [])[-1]
|
||||
|
||||
# Gestisci diversi formati di messaggio
|
||||
if isinstance(user_message, dict):
|
||||
user_content = user_message.get("content", str(user_message))
|
||||
elif hasattr(user_message, 'content'):
|
||||
user_content = user_message.content
|
||||
else:
|
||||
user_content = str(user_message)
|
||||
|
||||
# Aggiungi il messaggio utente alla cronologia
|
||||
self.messages.append(HumanMessage(content=user_content))
|
||||
|
||||
# Try to detect intent directly first
|
||||
detected_tool = self._detect_intent(user_content)
|
||||
if detected_tool:
|
||||
result = self._execute_tool(detected_tool)
|
||||
|
||||
# Format result as a clean response
|
||||
response_text = ""
|
||||
try:
|
||||
if isinstance(result, str):
|
||||
try:
|
||||
result = json.loads(result)
|
||||
except:
|
||||
response_text = result
|
||||
|
||||
if isinstance(result, dict):
|
||||
if "result" in result:
|
||||
response_text = result["result"]
|
||||
elif "error" in result:
|
||||
response_text = f"Errore: {result['error']}"
|
||||
else:
|
||||
response_text = json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
response_text = str(result) if not response_text else response_text
|
||||
|
||||
return {"messages": self.messages, "response": response_text}
|
||||
|
||||
# Se nessun intent detectato, usa il modello per rispondere normalmente
|
||||
# (senza tool calling per ora - i modelli locali non lo supportano bene)
|
||||
response = self.llm.invoke(self.messages)
|
||||
self.messages.append(response)
|
||||
|
||||
response_text = response.content if hasattr(response, 'content') else str(response)
|
||||
return {"messages": self.messages, "response": response_text}
|
||||
|
||||
def clear_history(self):
|
||||
"""Pulisci la cronologia ma mantieni il system prompt"""
|
||||
system_msg = self.messages[0] if self.messages and isinstance(self.messages[0], SystemMessage) else None
|
||||
self.messages = [system_msg] if system_msg else []
|
||||
|
||||
|
||||
def create_ollama_tool_agent(
|
||||
model,
|
||||
tools: Optional[List[Tool]] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
include_builtin_tools: bool = True
|
||||
) -> OllamaToolAgent:
|
||||
"""
|
||||
Factory function per creare un OllamaToolAgent.
|
||||
|
||||
Args:
|
||||
model: Il modello LLM da usare
|
||||
tools: Lista di tool personalizzati (opzionale)
|
||||
system_prompt: System prompt personalizzato (opzionale)
|
||||
include_builtin_tools: Se True, include i tool built-in (execute, read_file, etc.)
|
||||
"""
|
||||
if tools is None:
|
||||
tools = []
|
||||
|
||||
# Aggiungi i tool built-in
|
||||
if include_builtin_tools:
|
||||
builtin = get_builtin_tools()
|
||||
tools = builtin + tools
|
||||
|
||||
return OllamaToolAgent(
|
||||
llm=model,
|
||||
tools=tools,
|
||||
system_prompt=system_prompt
|
||||
)
|
||||
+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}")
|
||||
@@ -0,0 +1,48 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "langcode-agent"
|
||||
version = "1.0.0"
|
||||
description = "AI Agent harness based on LangChain Deep Agents with memory, planning and MCP support"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "LangCode Team"}
|
||||
]
|
||||
keywords = ["ai", "agent", "langchain", "ollama", "mcp"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"deepagents>=0.0.34",
|
||||
"langchain>=0.3.0",
|
||||
"langchain-community>=0.3.0",
|
||||
"langchain-openai>=0.1.0",
|
||||
"cryptography>=41.0.0",
|
||||
"requests>=2.31.0",
|
||||
"pyyaml>=6.0",
|
||||
"fastapi>=0.104.0",
|
||||
"uvicorn>=0.24.0",
|
||||
"pydantic>=2.5.0",
|
||||
"pytest>=7.4.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
langcode = "langcode:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "http://192.168.0.183:8085/truetype/langcode_agent"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["*"]
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
LangCode Agent - Setup
|
||||
"""
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="langcode-agent",
|
||||
version="1.0.0",
|
||||
description="AI Agent harness based on LangChain Deep Agents",
|
||||
author="LangCode Team",
|
||||
packages=find_packages(),
|
||||
python_requires=">=3.10",
|
||||
install_requires=[
|
||||
"deepagents>=0.0.34",
|
||||
"langchain>=0.3.0",
|
||||
"langchain-community>=0.3.0",
|
||||
"langchain-openai>=0.1.0",
|
||||
"cryptography>=41.0.0",
|
||||
"requests>=2.31.0",
|
||||
"pyyaml>=6.0",
|
||||
"fastapi>=0.104.0",
|
||||
"uvicorn>=0.24.0",
|
||||
"pydantic>=2.5.0",
|
||||
"pytest>=7.4.0",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"langcode=langcode:main",
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Skill manager for LangCode Agent
|
||||
Handles discovery, installation, and management of agent skills from skill registries like skills.sh
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import requests
|
||||
|
||||
from config_manager import config_manager
|
||||
|
||||
class SkillManager:
|
||||
"""Manages agent skills discovery and installation"""
|
||||
|
||||
def __init__(self, skills_dir: Optional[str] = None):
|
||||
"""
|
||||
Initialize the skill manager
|
||||
|
||||
Args:
|
||||
skills_dir: Directory to store skills. If None, uses ~/.langcode/skills
|
||||
"""
|
||||
if skills_dir is None:
|
||||
self.skills_dir = Path.home() / ".langcode" / "skills"
|
||||
else:
|
||||
self.skills_dir = Path(skills_dir)
|
||||
|
||||
# Ensure skills directory exists
|
||||
self.skills_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def search_skills(self, query: str, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search for skills using skills.sh CLI or fallback to manual search
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
limit: Maximum number of results
|
||||
|
||||
Returns:
|
||||
List of skill information dictionaries
|
||||
"""
|
||||
# Try using skills.sh CLI first
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["skills", "search", query, "--limit", str(limit), "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
skills_data = json.loads(result.stdout)
|
||||
return skills_data.get("skills", [])
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError):
|
||||
# Fall back to manual search if skills.sh is not available
|
||||
pass
|
||||
|
||||
# Fallback: try to use known skill registries or return empty
|
||||
return self._fallback_skill_search(query, limit)
|
||||
|
||||
def _fallback_skill_search(self, query: str, limit: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fallback skill search when skills.sh CLI is not available
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
limit: Maximum number of results
|
||||
|
||||
Returns:
|
||||
List of skill information dictionaries
|
||||
"""
|
||||
# For now, return empty list - in a full implementation, this would
|
||||
# query known skill registries via their APIs
|
||||
return []
|
||||
|
||||
def get_skill_details(self, skill_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get detailed information about a specific skill
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill to get details for
|
||||
|
||||
Returns:
|
||||
Skill details dictionary or None if not found
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["skills", "show", skill_name, "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return json.loads(result.stdout)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def install_skill(self, skill_name: str, version: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
Install a skill from the skill registry
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill to install
|
||||
version: Specific version to install (optional)
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
try:
|
||||
cmd = ["skills", "add", skill_name]
|
||||
if version:
|
||||
cmd.extend(["--version", version])
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60 # Skills might take time to install
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Refresh installed skills list
|
||||
self._refresh_installed_skills()
|
||||
return True, f"Skill '{skill_name}' installed successfully"
|
||||
else:
|
||||
return False, f"Failed to install skill: {result.stderr}"
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
||||
return False, f"Error installing skill: {str(e)}"
|
||||
|
||||
def uninstall_skill(self, skill_name: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Uninstall a skill
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill to uninstall
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["skills", "rm", skill_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Refresh installed skills list
|
||||
self._refresh_installed_skills()
|
||||
return True, f"Skill '{skill_name}' uninstalled successfully"
|
||||
else:
|
||||
return False, f"Failed to uninstall skill: {result.stderr}"
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
||||
return False, f"Error uninstalling skill: {str(e)}"
|
||||
|
||||
def list_installed_skills(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all installed skills
|
||||
|
||||
Returns:
|
||||
List of installed skill information dictionaries
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["skills", "list", "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
skills_data = json.loads(result.stdout)
|
||||
return skills_data.get("skills", [])
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
# Fallback: check local skills directory
|
||||
return self._list_local_skills()
|
||||
|
||||
def _list_local_skills(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List skills installed in the local skills directory
|
||||
|
||||
Returns:
|
||||
List of skill information dictionaries
|
||||
"""
|
||||
skills = []
|
||||
|
||||
if not self.skills_dir.exists():
|
||||
return skills
|
||||
|
||||
for skill_dir in self.skills_dir.iterdir():
|
||||
if skill_dir.is_dir():
|
||||
skill_file = skill_dir / "SKILL.md"
|
||||
if skill_file.exists():
|
||||
# Try to read basic info from SKILL.md
|
||||
try:
|
||||
content = skill_file.read_text(encoding='utf-8')
|
||||
# Extract name from first line if it's a markdown header
|
||||
name = skill_dir.name
|
||||
description = "Skill installed locally"
|
||||
|
||||
# Try to extract more info from SKILL.md frontmatter
|
||||
if content.startswith('---'):
|
||||
end_frontmatter = content.find('---', 3)
|
||||
if end_frontmatter != -1:
|
||||
frontmatter = content[3:end_frontmatter]
|
||||
# Simple parsing for name and description
|
||||
for line in frontmatter.split('\n'):
|
||||
if line.startswith('name:'):
|
||||
name = line.split(':', 1)[1].strip()
|
||||
elif line.startswith('description:'):
|
||||
description = line.split(':', 1)[1].strip()
|
||||
|
||||
skills.append({
|
||||
"name": name,
|
||||
"description": description,
|
||||
"path": str(skill_dir),
|
||||
"installed": True
|
||||
})
|
||||
except Exception:
|
||||
# If we can't read the skill properly, still list it
|
||||
skills.append({
|
||||
"name": skill_dir.name,
|
||||
"description": "Skill installed locally",
|
||||
"path": str(skill_dir),
|
||||
"installed": True
|
||||
})
|
||||
|
||||
return skills
|
||||
|
||||
def _refresh_installed_skills(self):
|
||||
"""Refresh the cache of installed skills"""
|
||||
# This would typically update some internal cache
|
||||
# For now, we rely on calling list_installed_skills() which checks fresh
|
||||
pass
|
||||
|
||||
def skill_is_installed(self, skill_name: str) -> bool:
|
||||
"""
|
||||
Check if a skill is installed
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill to check
|
||||
|
||||
Returns:
|
||||
True if skill is installed, False otherwise
|
||||
"""
|
||||
installed_skills = self.list_installed_skills()
|
||||
return any(skill.get("name") == skill_name for skill in installed_skills)
|
||||
|
||||
def get_skill_path(self, skill_name: str) -> Optional[Path]:
|
||||
"""
|
||||
Get the filesystem path where a skill is installed
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill
|
||||
|
||||
Returns:
|
||||
Path to the skill directory or None if not installed
|
||||
"""
|
||||
installed_skills = self.list_installed_skills()
|
||||
for skill in installed_skills:
|
||||
if skill.get("name") == skill_name:
|
||||
return Path(skill.get("path", ""))
|
||||
return None
|
||||
|
||||
|
||||
# Global skill manager instance
|
||||
skill_manager = SkillManager()
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
Terminal UI con scrollback semplice
|
||||
Usa input() standard + visualizzazione scrollback
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
from typing import Optional, Callable
|
||||
|
||||
|
||||
class TerminalUI:
|
||||
"""
|
||||
Terminal UI con scrollback.
|
||||
Uso semplice: input() per input, history buffer per scrollback.
|
||||
"""
|
||||
|
||||
def __init__(self, scrollback: int = 10000, prompt: str = "❯ "):
|
||||
self.scrollback = scrollback
|
||||
self.prompt = prompt
|
||||
|
||||
# History buffer
|
||||
self.history: list = []
|
||||
|
||||
# Callback
|
||||
self.agent_callback: Optional[Callable[[str], str]] = None
|
||||
|
||||
def _print(self, text: str):
|
||||
"""Stampa con colori base"""
|
||||
# Semplice - solo reset e bold
|
||||
if text.startswith("👤"):
|
||||
print(f"\033[1;36m{text}\033[0m")
|
||||
elif text.startswith("🤖"):
|
||||
print(f"\033[1;32m{text}\033[0m")
|
||||
elif text.startswith("🤔"):
|
||||
print(f"\033[1;33m{text}\033[0m")
|
||||
elif text.startswith("❌"):
|
||||
print(f"\033[1;31m{text}\033[0m")
|
||||
else:
|
||||
print(text)
|
||||
|
||||
def _wrap_line(self, text: str, width: int = 80) -> list:
|
||||
"""Wrap linea a width"""
|
||||
lines = []
|
||||
while len(text) > width:
|
||||
lines.append(text[:width])
|
||||
text = text[width:]
|
||||
if text:
|
||||
lines.append(text)
|
||||
return lines
|
||||
|
||||
def _add_history(self, text: str):
|
||||
"""Aggiunge a history"""
|
||||
self.history.append(text)
|
||||
while len(self.history) > self.scrollback:
|
||||
self.history.pop(0)
|
||||
|
||||
def _show_history(self, lines: int = None):
|
||||
"""Mostra ultime N linee di history"""
|
||||
if lines is None:
|
||||
# Determina righe visibili
|
||||
try:
|
||||
size = os.get_terminal_size()
|
||||
lines = size.lines - 5 # spazio per header, status, prompt
|
||||
except:
|
||||
lines = 20
|
||||
|
||||
# Mostra ultime righe
|
||||
start = max(0, len(self.history) - lines)
|
||||
for line in self.history[start:]:
|
||||
self._print(line)
|
||||
|
||||
def _process(self, cmd: str):
|
||||
"""Processa comando"""
|
||||
if not cmd.strip():
|
||||
return
|
||||
|
||||
self._add_history(f"👤 {cmd}")
|
||||
|
||||
# Thinking
|
||||
print("\033[1;33m🤔 Pensando...\033[0m")
|
||||
|
||||
try:
|
||||
resp = self.agent_callback(cmd)
|
||||
|
||||
# Rimuovi thinking (print sopra con risposta)
|
||||
print("\033[2A\033[K", end="") # move up 2, clear line
|
||||
|
||||
# Stampa risposta
|
||||
for line in resp.split('\n'):
|
||||
if line.strip():
|
||||
self._add_history(f"🤖 {line}")
|
||||
self._print(f"🤖 {line}")
|
||||
|
||||
except Exception as e:
|
||||
print("\033[2A\033[K", end="")
|
||||
err_msg = f"❌ {str(e)}"
|
||||
self._add_history(err_msg)
|
||||
self._print(err_msg)
|
||||
|
||||
def _scroll_help(self):
|
||||
"""Mostra aiuto scroll"""
|
||||
print("\n" + "=" * 60)
|
||||
print("SCROLLBACK - Premi q per uscire, frecce per navigare")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Paginator semplice
|
||||
page_size = 30
|
||||
start = 0
|
||||
total = len(self.history)
|
||||
|
||||
while start < total:
|
||||
# Mostra pagina
|
||||
for i in range(start, min(start + page_size, total)):
|
||||
self._print(self.history[i])
|
||||
|
||||
start += page_size
|
||||
|
||||
if start < total:
|
||||
print()
|
||||
print(f"[{start}/{total} righe] - Premi ENTER per continuare, q per uscire")
|
||||
try:
|
||||
choice = input().strip()
|
||||
if choice.lower() == 'q':
|
||||
break
|
||||
except:
|
||||
break
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("FINE SCROLLBACK")
|
||||
print("=" * 60)
|
||||
|
||||
def start(self, agent_callback: Callable[[str], str]):
|
||||
"""Avvia UI"""
|
||||
self.agent_callback = agent_callback
|
||||
|
||||
# Signal handler
|
||||
def sigint_handler(signum, frame):
|
||||
print("\n\033[1;33m👋 Arrivederci!\033[0m\n")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
|
||||
# Header
|
||||
print()
|
||||
print("\033[1;36m" + "=" * 50 + "\033[0m")
|
||||
print("\033[1;36m=\033[0m" + " " * 15 + "\033[1;33mLangCode Agent\033[0m" + " " * 15 + "\033[1;36m=\033[0m")
|
||||
print("\033[1;36m" + "=" * 50 + "\033[0m")
|
||||
print()
|
||||
print("digita /help per aiuto, /exit per uscire")
|
||||
print()
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Input
|
||||
user_input = input(f"\033[1;35m{self.prompt}\033[0m ").strip()
|
||||
except EOFError:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("\n\033[1;33m👋 Arrivederci!\033[0m")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
# Comandi speciali
|
||||
if user_input.lower() == '/exit':
|
||||
print("\033[1;33m👋 Arrivederci!\033[0m")
|
||||
break
|
||||
|
||||
elif user_input.lower() == '/quit':
|
||||
print("\033[1;33m👋 Arrivederci!\033[0m")
|
||||
break
|
||||
|
||||
elif user_input.lower() == '/help':
|
||||
print()
|
||||
print("\033[1;36m" + "=" * 40 + "\033[0m")
|
||||
print("COMANDI:")
|
||||
print(" /help - Questo messaggio")
|
||||
print(" /clear - Pulisci schermo")
|
||||
print(" /history - Mostra cronologia")
|
||||
print(" /scroll - Entra in scrollback")
|
||||
print(" /exit, /quit - Esci")
|
||||
print()
|
||||
print("SCROLLBACK:")
|
||||
print(" /scroll - Mostra tutto lo scrollback")
|
||||
print(" Frecce su/giù - Naviga cronologia (in some terminals)")
|
||||
print()
|
||||
print("\033[1;36m" + "=" * 40 + "\033[0m")
|
||||
print()
|
||||
continue
|
||||
|
||||
elif user_input.lower() == '/clear':
|
||||
os.system('clear' if os.name != 'nt' else 'cls')
|
||||
continue
|
||||
|
||||
elif user_input.lower() == '/history':
|
||||
print()
|
||||
self._show_history(50)
|
||||
print()
|
||||
continue
|
||||
|
||||
elif user_input.lower() == '/scroll':
|
||||
self._scroll_help()
|
||||
continue
|
||||
|
||||
# Processa comando normale
|
||||
self._process(user_input)
|
||||
print()
|
||||
|
||||
|
||||
def create_terminal_ui(scrollback: int = 10000, prompt: str = "❯ ") -> TerminalUI:
|
||||
return TerminalUI(scrollback=scrollback, prompt=prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def test(text):
|
||||
lines = [f"Ricevuto: {text}"]
|
||||
lines += [f"Linea {i}: contenuto di esempio" for i in range(1, 31)]
|
||||
return "\n".join(lines)
|
||||
|
||||
ui = create_terminal_ui()
|
||||
ui.start(test)
|
||||
+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:
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script per debuggare langcode agent
|
||||
"""
|
||||
import sys
|
||||
sys.path.insert(0, '/home/codex/langChainAgent/langcode_agent')
|
||||
|
||||
print("1. Importing modules...", flush=True)
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
print("2. Importing main...", flush=True)
|
||||
from main import create_langcode_agent
|
||||
|
||||
print("3. Creating agent...", flush=True)
|
||||
agent = create_langcode_agent()
|
||||
|
||||
print(f"4. Agent type: {type(agent)}", flush=True)
|
||||
|
||||
print("5. Invoking with test message...", flush=True)
|
||||
try:
|
||||
result = agent.invoke({"messages": [HumanMessage(content="che SO sto usando")]})
|
||||
print(f"6. Result type: {type(result)}", flush=True)
|
||||
print(f"7. Result keys: {result.keys() if hasattr(result, 'keys') else 'NO KEYS'}", flush=True)
|
||||
|
||||
if "response" in result:
|
||||
print(f"8. Response: {result['response'][:200]}...", flush=True)
|
||||
else:
|
||||
print("8. No 'response' key in result!", flush=True)
|
||||
if result.get("messages"):
|
||||
last_msg = result["messages"][-1]
|
||||
print(f" Last message: {last_msg.content if hasattr(last_msg, 'content') else str(last_msg)[:100]}", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {type(e).__name__}: {e}", flush=True)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@@ -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