Initial commit with CI workflow
All checks were successful
Build Container / build (push) Successful in 1m18s
All checks were successful
Build Container / build (push) Successful in 1m18s
- Flask backend with SSH discovery and Claude AI integration - React/Vite frontend with Tailwind CSS - Docker multi-stage build - Gitea Actions workflow for container builds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
273
backend/services/claude_agent.py
Normal file
273
backend/services/claude_agent.py
Normal file
@@ -0,0 +1,273 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from anthropic import Anthropic
|
||||
|
||||
from config import get_config
|
||||
|
||||
|
||||
SYSTEM_PROMPT = """You are an intelligent monitoring configuration assistant for Uptime Kuma. Your role is to analyze system information from hosts and recommend what should be monitored.
|
||||
|
||||
## Your Capabilities
|
||||
1. Analyze host scan results (OS info, running services, Docker containers, open ports)
|
||||
2. Suggest monitors to create in Uptime Kuma
|
||||
3. Request additional SSH commands to gather more information when needed
|
||||
4. Explain your monitoring recommendations
|
||||
|
||||
## Rules for Suggestions
|
||||
1. **Always explain WHY** you want to monitor something - what failure would it detect?
|
||||
2. **Be specific** with monitor configurations (ports, paths, intervals)
|
||||
3. **Prioritize critical services** - databases, web servers, auth services come first
|
||||
4. **Suggest appropriate intervals** based on criticality:
|
||||
- Critical services (databases, auth): 30-60 seconds
|
||||
- Web services: 60-120 seconds
|
||||
- Background jobs: 300 seconds
|
||||
5. **Look for health endpoints** - prefer /health, /healthz, /status over root paths
|
||||
6. **Consider dependencies** - if a service depends on another, both should be monitored
|
||||
|
||||
## Rules for SSH Commands
|
||||
When you need more information, you can request SSH commands. Follow these rules:
|
||||
1. **Read-only only** - never suggest commands that modify the system
|
||||
2. **Be specific** - explain exactly what information you need and why
|
||||
3. **Safe commands only** - no sudo unless absolutely necessary for reading
|
||||
4. **Examples of acceptable commands:**
|
||||
- `curl -s localhost:8080/health` - check if a service responds
|
||||
- `cat /etc/nginx/nginx.conf` - read configuration
|
||||
- `docker inspect <container>` - get container details
|
||||
- `systemctl status <service>` - check service status
|
||||
|
||||
## Response Format
|
||||
Always respond with valid JSON in this structure:
|
||||
{
|
||||
"analysis": "Your analysis of what you found on the host",
|
||||
"monitors": [
|
||||
{
|
||||
"type": "http|tcp|ping|docker|keyword",
|
||||
"name": "Human-readable monitor name",
|
||||
"target": "URL, hostname, or container name",
|
||||
"port": 80,
|
||||
"interval": 60,
|
||||
"reason": "Why this should be monitored"
|
||||
}
|
||||
],
|
||||
"additional_commands": [
|
||||
{
|
||||
"command": "the SSH command to run",
|
||||
"reason": "why you need this information"
|
||||
}
|
||||
],
|
||||
"questions": ["Any questions for the user about what to monitor"]
|
||||
}
|
||||
|
||||
## Monitor Types
|
||||
- **http**: Web endpoints (provide full URL with protocol)
|
||||
- **tcp**: Port connectivity (provide hostname and port)
|
||||
- **ping**: Host availability (provide hostname)
|
||||
- **docker**: Docker container status (provide container name)
|
||||
- **keyword**: Check for specific text in response (provide URL and keyword)
|
||||
|
||||
Be thorough but not excessive. Quality over quantity - suggest monitors that will actually catch real problems."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonitorSuggestion:
|
||||
"""A suggested monitor configuration."""
|
||||
|
||||
type: str
|
||||
name: str
|
||||
target: str
|
||||
port: Optional[int] = None
|
||||
interval: int = 60
|
||||
reason: str = ""
|
||||
keyword: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandRequest:
|
||||
"""A request to run an SSH command."""
|
||||
|
||||
command: str
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentResponse:
|
||||
"""Response from the Claude agent."""
|
||||
|
||||
analysis: str
|
||||
monitors: list[MonitorSuggestion]
|
||||
additional_commands: list[CommandRequest]
|
||||
questions: list[str]
|
||||
raw_response: str
|
||||
|
||||
|
||||
class ClaudeAgent:
|
||||
"""Claude AI agent for intelligent monitoring suggestions."""
|
||||
|
||||
def __init__(self):
|
||||
config = get_config()
|
||||
self.client = Anthropic(api_key=config.claude_api_key)
|
||||
self.conversation_history: list[dict] = []
|
||||
|
||||
def analyze_host(self, scan_results: dict, hostname: str) -> AgentResponse:
|
||||
"""Analyze host scan results and suggest monitors."""
|
||||
user_message = f"""I've scanned the host '{hostname}' and gathered the following information:
|
||||
|
||||
## System Information
|
||||
```
|
||||
{scan_results.get('system_info', 'Not available')}
|
||||
```
|
||||
|
||||
## OS Release
|
||||
```
|
||||
{scan_results.get('os_release', 'Not available')}
|
||||
```
|
||||
|
||||
## Running Docker Containers
|
||||
```
|
||||
{scan_results.get('docker_containers', 'No Docker or no containers running')}
|
||||
```
|
||||
|
||||
## Running Systemd Services
|
||||
```
|
||||
{scan_results.get('systemd_services', 'Not available')}
|
||||
```
|
||||
|
||||
## Disk Usage
|
||||
```
|
||||
{scan_results.get('disk_usage', 'Not available')}
|
||||
```
|
||||
|
||||
## Memory Usage
|
||||
```
|
||||
{scan_results.get('memory_usage', 'Not available')}
|
||||
```
|
||||
|
||||
## CPU Info
|
||||
```
|
||||
{scan_results.get('cpu_count', 'Not available')} CPU cores
|
||||
```
|
||||
|
||||
## Open Ports (Listening)
|
||||
```
|
||||
{scan_results.get('open_ports', 'Not available')}
|
||||
```
|
||||
|
||||
Please analyze this information and suggest what should be monitored in Uptime Kuma.
|
||||
Respond with JSON as specified in your instructions."""
|
||||
|
||||
return self._send_message(user_message)
|
||||
|
||||
def process_command_results(self, command: str, result: str) -> AgentResponse:
|
||||
"""Process the results of an additional SSH command."""
|
||||
user_message = f"""Here are the results of the command you requested:
|
||||
|
||||
Command: `{command}`
|
||||
|
||||
Output:
|
||||
```
|
||||
{result}
|
||||
```
|
||||
|
||||
Please update your analysis and suggestions based on this new information.
|
||||
Respond with JSON as specified in your instructions."""
|
||||
|
||||
return self._send_message(user_message)
|
||||
|
||||
def answer_question(self, question: str, answer: str) -> AgentResponse:
|
||||
"""Process user's answer to a question."""
|
||||
user_message = f"""You asked: "{question}"
|
||||
|
||||
The user responded: "{answer}"
|
||||
|
||||
Please update your recommendations based on this information.
|
||||
Respond with JSON as specified in your instructions."""
|
||||
|
||||
return self._send_message(user_message)
|
||||
|
||||
def _send_message(self, user_message: str) -> AgentResponse:
|
||||
"""Send a message to Claude and parse the response."""
|
||||
self.conversation_history.append({"role": "user", "content": user_message})
|
||||
|
||||
response = self.client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=4096,
|
||||
system=SYSTEM_PROMPT,
|
||||
messages=self.conversation_history,
|
||||
)
|
||||
|
||||
assistant_message = response.content[0].text
|
||||
self.conversation_history.append({"role": "assistant", "content": assistant_message})
|
||||
|
||||
return self._parse_response(assistant_message)
|
||||
|
||||
def _parse_response(self, response_text: str) -> AgentResponse:
|
||||
"""Parse Claude's JSON response."""
|
||||
# Try to extract JSON from the response
|
||||
try:
|
||||
# Look for JSON block in the response
|
||||
json_start = response_text.find("{")
|
||||
json_end = response_text.rfind("}") + 1
|
||||
|
||||
if json_start != -1 and json_end > json_start:
|
||||
json_str = response_text[json_start:json_end]
|
||||
data = json.loads(json_str)
|
||||
else:
|
||||
# No JSON found, return empty response
|
||||
return AgentResponse(
|
||||
analysis=response_text,
|
||||
monitors=[],
|
||||
additional_commands=[],
|
||||
questions=[],
|
||||
raw_response=response_text,
|
||||
)
|
||||
|
||||
monitors = []
|
||||
for m in data.get("monitors", []):
|
||||
monitors.append(
|
||||
MonitorSuggestion(
|
||||
type=m.get("type", "http"),
|
||||
name=m.get("name", "Unknown"),
|
||||
target=m.get("target", ""),
|
||||
port=m.get("port"),
|
||||
interval=m.get("interval", 60),
|
||||
reason=m.get("reason", ""),
|
||||
keyword=m.get("keyword"),
|
||||
)
|
||||
)
|
||||
|
||||
commands = []
|
||||
for c in data.get("additional_commands", []):
|
||||
commands.append(
|
||||
CommandRequest(
|
||||
command=c.get("command", ""),
|
||||
reason=c.get("reason", ""),
|
||||
)
|
||||
)
|
||||
|
||||
return AgentResponse(
|
||||
analysis=data.get("analysis", ""),
|
||||
monitors=monitors,
|
||||
additional_commands=commands,
|
||||
questions=data.get("questions", []),
|
||||
raw_response=response_text,
|
||||
)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return AgentResponse(
|
||||
analysis=response_text,
|
||||
monitors=[],
|
||||
additional_commands=[],
|
||||
questions=[],
|
||||
raw_response=response_text,
|
||||
)
|
||||
|
||||
def reset_conversation(self) -> None:
|
||||
"""Reset the conversation history."""
|
||||
self.conversation_history = []
|
||||
|
||||
|
||||
def create_agent() -> ClaudeAgent:
|
||||
"""Create a new Claude agent instance."""
|
||||
return ClaudeAgent()
|
||||
Reference in New Issue
Block a user