196 lines
7.3 KiB
Python
Executable File
196 lines
7.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Needle 3 AI Telemetry Triage Sidecar Service
|
|
Accepts OTel incident summaries and uses Cactus-Compute/needle3 to classify and extract actionable insights.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import time
|
|
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
|
import needle
|
|
|
|
PORT = int(os.environ.get("NEEDLE_PORT", 5005))
|
|
HOST = os.environ.get("NEEDLE_HOST", "0.0.0.0")
|
|
|
|
# --- Define Triage Tools for Needle 3 ---
|
|
@needle.tool
|
|
def report_client_javascript_crash(service: str, message: str, url: str):
|
|
"""Report client-side JavaScript crash or error logged by browser RUM."""
|
|
return {"action": "client_js_crash", "service": service, "message": message, "url": url}
|
|
|
|
@needle.tool
|
|
def alert_vulnerability_probe(scanner: str, target: str, status_code: int):
|
|
"""Alert on malicious vulnerability scans or exploit crawlers."""
|
|
return {"action": "security_alert", "scanner": scanner, "target": target, "status_code": status_code}
|
|
|
|
@needle.tool
|
|
def record_frontend_web_vital(service: str, metric_name: str, duration_ms: int):
|
|
"""Record RUM performance metrics like TTFB or FCP."""
|
|
return {"action": "record_vital", "service": service, "metric": metric_name, "ms": duration_ms}
|
|
|
|
@needle.tool
|
|
def log_missing_resource_404(path: str, host: str):
|
|
"""Log benign 404 Not Found requests for missing pages or git commits."""
|
|
return {"action": "log_404", "path": path, "host": host}
|
|
|
|
@needle.tool
|
|
def report_database_error(service: str, table: str, error_message: str):
|
|
"""Report database query errors, deadlocks, or connection pool timeouts."""
|
|
return {"action": "database_error", "service": service, "table": table, "error": error_message}
|
|
|
|
@needle.tool
|
|
def report_auth_failure(service: str, user_or_token: str, reason: str):
|
|
"""Report authentication/authorization failure or invalid credentials."""
|
|
return {"action": "auth_failure", "service": service, "user_or_token": user_or_token, "reason": reason}
|
|
|
|
@needle.tool
|
|
def escalate_backend_outage(service: str, error_message: str):
|
|
"""Escalate severe backend 5xx crash or unhandled panic to on-call."""
|
|
return {"action": "escalate_outage", "service": service, "error": error_message}
|
|
|
|
# Initialize Needle Agent once
|
|
print(f"Loading Needle 3 model (Cactus-Compute/needle3)...")
|
|
t0 = time.perf_counter()
|
|
agent = needle.Needle(tools=[
|
|
report_client_javascript_crash,
|
|
alert_vulnerability_probe,
|
|
record_frontend_web_vital,
|
|
log_missing_resource_404,
|
|
report_database_error,
|
|
report_auth_failure,
|
|
escalate_backend_outage
|
|
])
|
|
print(f"Needle 3 loaded in {(time.perf_counter() - t0):.2f}s")
|
|
|
|
|
|
class NeedleHandler(BaseHTTPRequestHandler):
|
|
def _send_cors(self):
|
|
self.send_header("Access-Control-Allow-Origin", "*")
|
|
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Otel-Agent-Token")
|
|
|
|
def do_OPTIONS(self):
|
|
self.send_response(204)
|
|
self._send_cors()
|
|
self.end_headers()
|
|
|
|
def do_GET(self):
|
|
if self.path == "/health" or self.path == "/api/needle/status":
|
|
self.send_response(200)
|
|
self._send_cors()
|
|
self.send_header("Content-Type", "application/json")
|
|
self.end_headers()
|
|
resp = {
|
|
"status": "ok",
|
|
"available": True,
|
|
"model": "Cactus-Compute/needle3",
|
|
"tools": [
|
|
"client_js_crash",
|
|
"security_alert",
|
|
"record_vital",
|
|
"log_404",
|
|
"database_error",
|
|
"auth_failure",
|
|
"escalate_outage"
|
|
]
|
|
}
|
|
self.wfile.write(json.dumps(resp).encode("utf-8"))
|
|
else:
|
|
self.send_response(404)
|
|
self._send_cors()
|
|
self.end_headers()
|
|
|
|
def do_POST(self):
|
|
if self.path in ("/classify", "/api/needle/classify"):
|
|
content_len = int(self.headers.get("Content-Length", 0))
|
|
body = self.rfile.read(content_len)
|
|
try:
|
|
data = json.loads(body) if body else {}
|
|
except Exception as e:
|
|
self.send_response(400)
|
|
self._send_cors()
|
|
self.send_header("Content-Type", "application/json")
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({"error": f"Invalid JSON: {e}"}).encode("utf-8"))
|
|
return
|
|
|
|
text = data.get("text", "").strip()
|
|
# If text is not provided, build from structured fields:
|
|
if not text:
|
|
parts = []
|
|
if data.get("service"):
|
|
parts.append(f"Service: {data['service']}")
|
|
if data.get("kind"):
|
|
parts.append(f"Kind: {data['kind']}")
|
|
if data.get("title"):
|
|
parts.append(f"Title: {data['title']}")
|
|
if data.get("message"):
|
|
parts.append(f"Message: {data['message']}")
|
|
if data.get("url"):
|
|
parts.append(f"URL: {data['url']}")
|
|
if data.get("status_code"):
|
|
parts.append(f"Status: {data['status_code']}")
|
|
text = " | ".join(parts)
|
|
|
|
if not text:
|
|
self.send_response(400)
|
|
self._send_cors()
|
|
self.send_header("Content-Type", "application/json")
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({"error": "Missing 'text' or telemetry fields"}).encode("utf-8"))
|
|
return
|
|
|
|
t_start = time.perf_counter()
|
|
agent.reset()
|
|
res = agent.run(text, strict=False)
|
|
latency_ms = (time.perf_counter() - t_start) * 1000
|
|
|
|
results = res.get("results", [])
|
|
first_result = results[0] if results and isinstance(results[0], dict) else {}
|
|
action = first_result.get("action", "unclassified")
|
|
|
|
calls = res.get("function_calls", [])
|
|
tool_name = calls[0].get("name") if calls else "triage_tool"
|
|
|
|
out = {
|
|
"success": True,
|
|
"input_text": text,
|
|
"action": action,
|
|
"tool": tool_name,
|
|
"arguments": first_result,
|
|
"confidence": float(res.get("confidence", 0.0)),
|
|
"reasoning": res.get("reasoning", ""),
|
|
"latency_ms": round(latency_ms, 2),
|
|
"model": "Cactus-Compute/needle3"
|
|
}
|
|
|
|
self.send_response(200)
|
|
self._send_cors()
|
|
self.send_header("Content-Type", "application/json")
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(out).encode("utf-8"))
|
|
else:
|
|
self.send_response(404)
|
|
self._send_cors()
|
|
self.end_headers()
|
|
|
|
def log_message(self, format, *args):
|
|
# Concise logging
|
|
print(f"[needle-sidecar] {args[0]} {args[1]} -> {args[2]}")
|
|
|
|
|
|
def run():
|
|
server = ThreadingHTTPServer((HOST, PORT), NeedleHandler)
|
|
print(f"[needle-sidecar] Listening on http://{HOST}:{PORT}")
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down needle sidecar...")
|
|
server.server_close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|