From c351e3453f024cbe1fa836b935a02e2062929e2a Mon Sep 17 00:00:00 2001 From: root Date: Wed, 26 Aug 2026 19:19:01 -0400 Subject: [PATCH] Route Agent D.R.E chatbot through admin-ai (dedicated dre-chatbot virtual key, $5/day budget) instead of native DeepSeek key --- backend/chatbot.py | 32 +++++++-------- backend/scripts/create-chatbot-key.sh | 58 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 backend/scripts/create-chatbot-key.sh diff --git a/backend/chatbot.py b/backend/chatbot.py index 4d30f87..bfabbb1 100644 --- a/backend/chatbot.py +++ b/backend/chatbot.py @@ -1,6 +1,6 @@ """Agent D.R.E chatbot — public FAQ assistant. -Backed by DeepSeek (deepseek-v4-flash, thinking disabled) via native API. +Backed by deepseek-v4-flash via the admin-ai LiteLLM proxy (dedicated virtual key, budget capped). Stdlib-only (urllib + json) so no new venv dependencies. Public endpoint: POST /api/chat @@ -31,10 +31,10 @@ logger = logging.getLogger("dre.chatbot") router = APIRouter() -DEEPSEEK_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com").rstrip("/") -DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") +CHATBOT_BASE_URL = os.environ.get("DRE_CHATBOT_BASE_URL", "https://admin-ai.itpropartner.com/v1").rstrip("/") +CHATBOT_API_KEY = os.environ.get("DRE_CHATBOT_API_KEY", "") CHATBOT_MODEL = os.environ.get("DRE_CHATBOT_MODEL", "deepseek-v4-flash") -CHATBOT_MAX_TOKENS = int(os.environ.get("DRE_CHATBOT_MAX_TOKENS", "300")) +CHATBOT_MAX_TOKENS = int(os.environ.get("DRE_CHATBOT_MAX_TOKENS", "400")) CHATBOT_TIMEOUT = float(os.environ.get("DRE_CHATBOT_TIMEOUT", "30")) MAX_MESSAGE_CHARS = 1000 @@ -97,9 +97,9 @@ def _is_private_topic(text: str) -> bool: return any(m in low for m in PRIVATE_TOPIC_MARKERS) -def _call_deepseek(system_prompt: str, history: list[dict], message: str) -> str | None: - if not DEEPSEEK_API_KEY: - logger.error("DEEPSEEK_API_KEY not set; chatbot cannot call LLM") +def _call_llm(system_prompt: str, history: list[dict], message: str) -> str | None: + if not CHATBOT_API_KEY: + logger.error("DRE_CHATBOT_API_KEY not set; chatbot cannot call LLM") return None messages = [{"role": "system", "content": system_prompt}] @@ -115,15 +115,14 @@ def _call_deepseek(system_prompt: str, history: list[dict], message: str) -> str "messages": messages, "max_tokens": CHATBOT_MAX_TOKENS, "temperature": 0.3, - "thinking": {"type": "disabled"}, } - url = f"{DEEPSEEK_BASE_URL}/v1/chat/completions" + url = f"{CHATBOT_BASE_URL}/chat/completions" req = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), headers={ "Content-Type": "application/json", - "Authorization": f"Bearer {DEEPSEEK_API_KEY}", + "Authorization": f"Bearer {CHATBOT_API_KEY}", }, method="POST", ) @@ -131,20 +130,21 @@ def _call_deepseek(system_prompt: str, history: list[dict], message: str) -> str with urllib.request.urlopen(req, timeout=CHATBOT_TIMEOUT) as resp: data = json.loads(resp.read().decode("utf-8")) except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError, json.JSONDecodeError) as exc: - logger.error("deepseek call failed: %s", exc) + logger.error("LLM call failed: %s", exc) return None try: content = data["choices"][0]["message"].get("content") or "" content = content.strip() except (KeyError, IndexError, TypeError): - logger.error("unexpected deepseek response shape: %s", data) + logger.error("unexpected LLM response shape: %s", data) return None - # If thinking was not actually disabled, content can come back empty. Do not - # surface the model's reasoning; treat it as a failed call instead. + # deepseek-v4-flash reasons even through LiteLLM; with max_tokens set high + # enough, content is still populated. If content is empty, never surface the + # model's reasoning_content (chain-of-thought); treat it as a failed call. if not content: - logger.error("deepseek returned empty content (thinking may not be disabled)") + logger.error("LLM returned empty content") return None return content @@ -193,7 +193,7 @@ async def chat(req: ChatRequest, request: Request): return {"reply": FALLBACK_OUT_OF_SCOPE} # 2. LLM answer from public FAQ only - reply = _call_deepseek(_system_prompt(), req.history, message) + reply = _call_llm(_system_prompt(), req.history, message) if reply is None: return {"reply": FALLBACK_ERROR} diff --git a/backend/scripts/create-chatbot-key.sh b/backend/scripts/create-chatbot-key.sh new file mode 100644 index 0000000..2e79fcf --- /dev/null +++ b/backend/scripts/create-chatbot-key.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Idempotent: ensure a single dre-chatbot virtual key exists on admin-ai. +# LiteLLM facts (learned 2026-08-26): +# - /key/list -> {"keys": [hash...], total_count, total_pages} (paginated) +# - /key/info?key= -> {"key": "", "info": {key_alias, models, ...}} +# - /key/generate -> {"key": "sk-...", "token": "", "token_id": ""} +# - raw "sk-" key is ONLY visible in the /key/generate response; never stored +# in plaintext afterward (key_name is masked). So we must regenerate to get it. +set -euo pipefail + +APP1_KEY=/root/.ssh/itpp-infra +APP1_HOST=root@152.53.36.131 +ENV_FILE=/opt/dre-portal/.env +ALIAS=dre-chatbot +MODEL=deepseek-v4-flash +BUDGET=5.0 +BASE="https://admin-ai.itpropartner.com" + +MK=$(ssh -i "$APP1_KEY" "$APP1_HOST" "grep LITELLM_MASTER_KEY /root/docker/litellm/.env | cut -d= -f2" 2>/dev/null) +[ -n "$MK" ] || { echo "ERROR: no master key"; exit 1; } + +# --- 1. Delete any existing keys carrying the dre-chatbot alias (junk from bad runs) +curl -s -H "Authorization: Bearer $MK" "$BASE/key/list" > /tmp/dre_keylist.json +HASHES=$(python3 -c "import json; print('\n'.join(json.load(open('/tmp/dre_keylist.json')).get('keys', [])))") +DEL="" +for h in $HASHES; do + [ -z "$h" ] && continue + alias=$(curl -s -H "Authorization: Bearer $MK" "$BASE/key/info?key=$h" \ + | python3 -c "import sys,json; print((json.load(sys.stdin).get('info') or {}).get('key_alias',''))" 2>/dev/null || echo "") + if [ "$alias" = "$ALIAS" ] || [ "$alias" = "dre-chatbot-inspect" ]; then + DEL="$DEL $h" + fi +done +if [ -n "$DEL" ]; then + curl -s -X POST "$BASE/key/delete" -H "Authorization: Bearer $MK" -H "Content-Type: application/json" \ + -d "$(python3 -c "import json,sys; print(json.dumps({'keys': sys.argv[1].split()}))" "$DEL")" >/dev/null + echo "deleted stale keys:$DEL" +fi + +# --- 2. Generate a fresh key, capture the RAW sk- token from the 'key' field +RESP=$(curl -s -X POST "$BASE/key/generate" -H "Authorization: Bearer $MK" -H "Content-Type: application/json" \ + -d "{\"key_alias\":\"$ALIAS\",\"models\":[\"$MODEL\"],\"max_budget\":$BUDGET,\"budget_duration\":\"daily\",\"metadata\":{\"purpose\":\"Agent DRE public FAQ chatbot\",\"service\":\"dre-chatbot\"}}") +KEY=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('key',''))") +case "$KEY" in + sk-*) ;; + *) echo "ERROR: generated key not sk- (got '${KEY:0:8}')"; echo "$RESP" | head -c 500; exit 1;; +esac + +# --- 3. Write to .env (never print the key) +if grep -q '^DRE_CHATBOT_API_KEY=' "$ENV_FILE"; then + sed -i "s|^DRE_CHATBOT_API_KEY=.*|DRE_CHATBOT_API_KEY=$KEY|" "$ENV_FILE" +else + echo "DRE_CHATBOT_API_KEY=$KEY" >> "$ENV_FILE" +fi +grep -q '^DRE_CHATBOT_BASE_URL=' "$ENV_FILE" || echo "DRE_CHATBOT_BASE_URL=https://admin-ai.itpropartner.com/v1" >> "$ENV_FILE" +grep -q '^DRE_CHATBOT_MODEL=' "$ENV_FILE" || echo "DRE_CHATBOT_MODEL=$MODEL" >> "$ENV_FILE" + +echo "DONE: alias=$ALIAS model=$MODEL budget=\$$BUDGET/day keylen=${#KEY}"