#!/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 import re import hashlib 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} @needle.tool def report_unclassified(service: str, summary: str): """Fallback when the event matches none of the other categories: retries, cancellations, restarts, OOM, health checks, or anything unclear.""" return {"action": "unclassified", "service": service, "summary": summary} # --- Redaction: nothing PII-bearing should reach the model or the cache --- _REDACTIONS = [ (re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"), ""), (re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b"), ""), (re.compile(r"\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b"), ""), (re.compile(r"\beyJ[\w-]+\.[\w-]+\.[\w-]+\b"), ""), (re.compile(r"\b[0-9a-fA-F]{16,}\b"), ""), (re.compile(r"([?&][\w.-]+=)[^&\s|]+"), r"\1"), ] def redact(text: str) -> str: for rx, sub in _REDACTIONS: text = rx.sub(sub, text) return text[:1000] # Cache keyed on the redacted text: repeated signatures cost nothing. CACHE_MAX = int(os.environ.get("NEEDLE_CACHE_MAX", 2000)) _cache = {} _stats = {"hits": 0, "misses": 0} # 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, report_unclassified ]) 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", "cache": {**_stats, "size": len(_cache)}, "tools": [ "client_js_crash", "security_alert", "record_vital", "log_404", "database_error", "auth_failure", "escalate_outage", "unclassified" ] } 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 text = redact(text) key = hashlib.sha256(text.encode()).hexdigest() cached = _cache.get(key) if cached is not None: _stats["hits"] += 1 out = {**cached, "cached": True} 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")) return _stats["misses"] += 1 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" } if len(_cache) >= CACHE_MAX: _cache.pop(next(iter(_cache))) _cache[key] = out 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()