Route Agent D.R.E chatbot through admin-ai (dedicated dre-chatbot virtual key, $5/day budget) instead of native DeepSeek key
This commit is contained in:
+16
-16
@@ -1,6 +1,6 @@
|
|||||||
"""Agent D.R.E chatbot — public FAQ assistant.
|
"""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.
|
Stdlib-only (urllib + json) so no new venv dependencies.
|
||||||
Public endpoint: POST /api/chat
|
Public endpoint: POST /api/chat
|
||||||
|
|
||||||
@@ -31,10 +31,10 @@ logger = logging.getLogger("dre.chatbot")
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
DEEPSEEK_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com").rstrip("/")
|
CHATBOT_BASE_URL = os.environ.get("DRE_CHATBOT_BASE_URL", "https://admin-ai.itpropartner.com/v1").rstrip("/")
|
||||||
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
CHATBOT_API_KEY = os.environ.get("DRE_CHATBOT_API_KEY", "")
|
||||||
CHATBOT_MODEL = os.environ.get("DRE_CHATBOT_MODEL", "deepseek-v4-flash")
|
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"))
|
CHATBOT_TIMEOUT = float(os.environ.get("DRE_CHATBOT_TIMEOUT", "30"))
|
||||||
|
|
||||||
MAX_MESSAGE_CHARS = 1000
|
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)
|
return any(m in low for m in PRIVATE_TOPIC_MARKERS)
|
||||||
|
|
||||||
|
|
||||||
def _call_deepseek(system_prompt: str, history: list[dict], message: str) -> str | None:
|
def _call_llm(system_prompt: str, history: list[dict], message: str) -> str | None:
|
||||||
if not DEEPSEEK_API_KEY:
|
if not CHATBOT_API_KEY:
|
||||||
logger.error("DEEPSEEK_API_KEY not set; chatbot cannot call LLM")
|
logger.error("DRE_CHATBOT_API_KEY not set; chatbot cannot call LLM")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
messages = [{"role": "system", "content": system_prompt}]
|
messages = [{"role": "system", "content": system_prompt}]
|
||||||
@@ -115,15 +115,14 @@ def _call_deepseek(system_prompt: str, history: list[dict], message: str) -> str
|
|||||||
"messages": messages,
|
"messages": messages,
|
||||||
"max_tokens": CHATBOT_MAX_TOKENS,
|
"max_tokens": CHATBOT_MAX_TOKENS,
|
||||||
"temperature": 0.3,
|
"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(
|
req = urllib.request.Request(
|
||||||
url,
|
url,
|
||||||
data=json.dumps(payload).encode("utf-8"),
|
data=json.dumps(payload).encode("utf-8"),
|
||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
|
"Authorization": f"Bearer {CHATBOT_API_KEY}",
|
||||||
},
|
},
|
||||||
method="POST",
|
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:
|
with urllib.request.urlopen(req, timeout=CHATBOT_TIMEOUT) as resp:
|
||||||
data = json.loads(resp.read().decode("utf-8"))
|
data = json.loads(resp.read().decode("utf-8"))
|
||||||
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError, json.JSONDecodeError) as exc:
|
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
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content = data["choices"][0]["message"].get("content") or ""
|
content = data["choices"][0]["message"].get("content") or ""
|
||||||
content = content.strip()
|
content = content.strip()
|
||||||
except (KeyError, IndexError, TypeError):
|
except (KeyError, IndexError, TypeError):
|
||||||
logger.error("unexpected deepseek response shape: %s", data)
|
logger.error("unexpected LLM response shape: %s", data)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# If thinking was not actually disabled, content can come back empty. Do not
|
# deepseek-v4-flash reasons even through LiteLLM; with max_tokens set high
|
||||||
# surface the model's reasoning; treat it as a failed call instead.
|
# 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:
|
if not content:
|
||||||
logger.error("deepseek returned empty content (thinking may not be disabled)")
|
logger.error("LLM returned empty content")
|
||||||
return None
|
return None
|
||||||
return content
|
return content
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ async def chat(req: ChatRequest, request: Request):
|
|||||||
return {"reply": FALLBACK_OUT_OF_SCOPE}
|
return {"reply": FALLBACK_OUT_OF_SCOPE}
|
||||||
|
|
||||||
# 2. LLM answer from public FAQ only
|
# 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:
|
if reply is None:
|
||||||
return {"reply": FALLBACK_ERROR}
|
return {"reply": FALLBACK_ERROR}
|
||||||
|
|
||||||
|
|||||||
@@ -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=<hash> -> {"key": "<hash>", "info": {key_alias, models, ...}}
|
||||||
|
# - /key/generate -> {"key": "sk-...", "token": "<hash>", "token_id": "<hash>"}
|
||||||
|
# - 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}"
|
||||||
Reference in New Issue
Block a user