diff --git a/services/bstein-dev-home/backend-deployment.yaml b/services/bstein-dev-home/backend-deployment.yaml index ecf478c..26c99e1 100644 --- a/services/bstein-dev-home/backend-deployment.yaml +++ b/services/bstein-dev-home/backend-deployment.yaml @@ -28,6 +28,7 @@ spec: {{ with secret "kv/data/atlas/shared/chat-ai-keys-runtime" }} export CHAT_KEY_MATRIX="{{ .Data.data.matrix }}" export CHAT_KEY_HOMEPAGE="{{ .Data.data.homepage }}" + export AI_ATLASBOT_TOKEN="{{ .Data.data.homepage }}" {{ end }} {{ with secret "kv/data/atlas/shared/portal-e2e-client" }} export PORTAL_E2E_CLIENT_ID="{{ .Data.data.client_id }}" @@ -66,6 +67,10 @@ spec: value: qwen2.5-coder:7b-instruct-q4_0 - name: AI_CHAT_TIMEOUT_SEC value: "480" + - name: AI_ATLASBOT_ENDPOINT + value: http://atlasbot.comms.svc.cluster.local:8090/v1/answer + - name: AI_ATLASBOT_TIMEOUT_SEC + value: "5" - name: AI_NODE_NAME valueFrom: fieldRef: diff --git a/services/comms/atlasbot-deployment.yaml b/services/comms/atlasbot-deployment.yaml index d195e89..c0596b6 100644 --- a/services/comms/atlasbot-deployment.yaml +++ b/services/comms/atlasbot-deployment.yaml @@ -16,7 +16,7 @@ spec: labels: app: atlasbot annotations: - checksum/atlasbot-configmap: manual-atlasbot-24 + checksum/atlasbot-configmap: manual-atlasbot-25 vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "comms" vault.hashicorp.com/agent-inject-secret-turn-secret: "kv/data/atlas/comms/turn-shared-secret" @@ -87,6 +87,11 @@ spec: value: "480" - name: ATLASBOT_THINKING_INTERVAL_SEC value: "120" + - name: ATLASBOT_HTTP_PORT + value: "8090" + ports: + - name: http + containerPort: 8090 resources: requests: cpu: 100m diff --git a/services/comms/atlasbot-service.yaml b/services/comms/atlasbot-service.yaml new file mode 100644 index 0000000..c8b3570 --- /dev/null +++ b/services/comms/atlasbot-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: atlasbot + namespace: comms + labels: + app: atlasbot +spec: + selector: + app: atlasbot + ports: + - name: http + port: 8090 + targetPort: 8090 + type: ClusterIP diff --git a/services/comms/kustomization.yaml b/services/comms/kustomization.yaml index 37f681d..410f2a6 100644 --- a/services/comms/kustomization.yaml +++ b/services/comms/kustomization.yaml @@ -14,6 +14,7 @@ resources: - guest-register-deployment.yaml - guest-register-service.yaml - atlasbot-deployment.yaml + - atlasbot-service.yaml - wellknown.yaml - atlasbot-rbac.yaml - mas-secrets-ensure-rbac.yaml diff --git a/services/comms/scripts/atlasbot/bot.py b/services/comms/scripts/atlasbot/bot.py index 987df7a..deb8e62 100644 --- a/services/comms/scripts/atlasbot/bot.py +++ b/services/comms/scripts/atlasbot/bot.py @@ -5,6 +5,7 @@ import re import ssl import threading import time +from http.server import BaseHTTPRequestHandler, HTTPServer from typing import Any from urllib import error, parse, request @@ -1089,6 +1090,62 @@ def _normalize_reply(value: Any) -> str: return text +# Internal HTTP endpoint for cluster answers (website uses this). +class _AtlasbotHandler(BaseHTTPRequestHandler): + server_version = "AtlasbotHTTP/1.0" + + def _write_json(self, status: int, payload: dict[str, Any]): + body = json.dumps(payload).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _authorized(self) -> bool: + if not ATLASBOT_INTERNAL_TOKEN: + return True + token = self.headers.get("X-Internal-Token", "") + return token == ATLASBOT_INTERNAL_TOKEN + + def do_GET(self): # noqa: N802 + if self.path == "/health": + self._write_json(200, {"status": "ok"}) + return + self._write_json(404, {"error": "not_found"}) + + def do_POST(self): # noqa: N802 + if self.path != "/v1/answer": + self._write_json(404, {"error": "not_found"}) + return + if not self._authorized(): + self._write_json(401, {"error": "unauthorized"}) + return + try: + length = int(self.headers.get("Content-Length", "0")) + except ValueError: + length = 0 + raw = self.rfile.read(length) if length > 0 else b"" + try: + payload = json.loads(raw.decode("utf-8")) if raw else {} + except json.JSONDecodeError: + self._write_json(400, {"error": "invalid_json"}) + return + prompt = str(payload.get("prompt") or payload.get("question") or "").strip() + if not prompt: + self._write_json(400, {"error": "missing_prompt"}) + return + inventory = node_inventory_live() + answer = structured_answer(prompt, inventory=inventory, metrics_summary="") + self._write_json(200, {"answer": answer}) + + +def _start_http_server(): + server = HTTPServer(("0.0.0.0", ATLASBOT_HTTP_PORT), _AtlasbotHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + + # Conversation state. history = collections.defaultdict(list) # (room_id, sender|None) -> list[str] (short transcript) @@ -1326,6 +1383,7 @@ def login_with_retry(): def main(): load_kb() + _start_http_server() token = login_with_retry() try: room_id = resolve_alias(token, ROOM_ALIAS)