Livrare LOT 1 - Didi
This commit is contained in:
commit
5380c3fc63
990 changed files with 133308 additions and 0 deletions
405
ai_platform/bootstrap.sh
Normal file
405
ai_platform/bootstrap.sh
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# ml-projects bootstrap
|
||||
#
|
||||
# Provisions a fresh CPU-only machine with:
|
||||
# - dashboard (monitoring + runtime config, PostgreSQL-backed)
|
||||
# - web (search + gather, tier-based routing)
|
||||
#
|
||||
# Assumes you already have:
|
||||
# - SearXNG running (or will deploy ./modules/web/deploy/metasearch)
|
||||
# - An LLM endpoint on a GPU machine (vLLM / llama.cpp) reachable by URL
|
||||
#
|
||||
# Usage:
|
||||
# ./bootstrap.sh # interactive (prompts for everything)
|
||||
# ./bootstrap.sh --non-interactive # no prompts, uses existing .env files
|
||||
# ./bootstrap.sh --help
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# ---------- colors ----------
|
||||
if [[ -t 1 ]]; then
|
||||
C_RESET='\033[0m'
|
||||
C_BOLD='\033[1m'
|
||||
C_RED='\033[31m'
|
||||
C_GREEN='\033[32m'
|
||||
C_YELLOW='\033[33m'
|
||||
C_BLUE='\033[34m'
|
||||
C_CYAN='\033[36m'
|
||||
else
|
||||
C_RESET='' C_BOLD='' C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_CYAN=''
|
||||
fi
|
||||
|
||||
step() { printf "\n${C_BOLD}${C_BLUE}==>${C_RESET} ${C_BOLD}%s${C_RESET}\n" "$1"; }
|
||||
info() { printf " ${C_CYAN}%s${C_RESET}\n" "$1"; }
|
||||
ok() { printf " ${C_GREEN}✓${C_RESET} %s\n" "$1"; }
|
||||
warn() { printf " ${C_YELLOW}⚠${C_RESET} %s\n" "$1"; }
|
||||
fail() { printf " ${C_RED}✗${C_RESET} %s\n" "$1" >&2; }
|
||||
die() { fail "$1"; exit 1; }
|
||||
|
||||
# ---------- args ----------
|
||||
INTERACTIVE=1
|
||||
DEPLOY_SEARXNG=0
|
||||
SKIP_SMOKE=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--non-interactive) INTERACTIVE=0 ;;
|
||||
--deploy-searxng) DEPLOY_SEARXNG=1 ;;
|
||||
--skip-smoke) SKIP_SMOKE=1 ;;
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Options:
|
||||
--non-interactive Skip prompts. All .env files must already be filled.
|
||||
--deploy-searxng Also deploy SearXNG from modules/web/deploy/metasearch.
|
||||
By default, bootstrap assumes SearXNG is already running.
|
||||
--skip-smoke Skip post-deploy smoke tests.
|
||||
-h, --help Show this help and exit.
|
||||
|
||||
Examples:
|
||||
$0 Full interactive bootstrap
|
||||
$0 --deploy-searxng Also deploy SearXNG
|
||||
$0 --non-interactive Just deploy using existing .env files
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*) die "Unknown argument: $arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- prereqs ----------
|
||||
step "Checking prerequisites"
|
||||
|
||||
command -v docker >/dev/null 2>&1 || die "docker not found"
|
||||
docker compose version >/dev/null 2>&1 || die "docker compose v2 not found"
|
||||
ok "docker $(docker --version | awk '{print $3}' | tr -d ',')"
|
||||
ok "docker compose $(docker compose version --short)"
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
die "docker daemon not running or current user has no access"
|
||||
fi
|
||||
ok "docker daemon reachable"
|
||||
|
||||
# ---------- network ----------
|
||||
step "Ensuring 'deploy_default' Docker network exists"
|
||||
|
||||
if docker network inspect deploy_default >/dev/null 2>&1; then
|
||||
ok "deploy_default network exists"
|
||||
else
|
||||
docker network create deploy_default >/dev/null
|
||||
ok "deploy_default network created"
|
||||
fi
|
||||
|
||||
# ---------- helpers ----------
|
||||
gen_password() {
|
||||
# URL-safe 24-char password
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -base64 24 | tr -d '/+=' | head -c 24
|
||||
else
|
||||
head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24
|
||||
fi
|
||||
}
|
||||
|
||||
prompt() {
|
||||
# prompt VAR_NAME "Prompt text" "default"
|
||||
local var="$1" label="$2" default="${3:-}" answer=""
|
||||
if [[ $INTERACTIVE -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "$default" ]]; then
|
||||
read -rp " $label [$default]: " answer
|
||||
answer="${answer:-$default}"
|
||||
else
|
||||
read -rp " $label: " answer
|
||||
fi
|
||||
eval "$var=\"\$answer\""
|
||||
}
|
||||
|
||||
prompt_secret() {
|
||||
# same as prompt but stdin is not echoed
|
||||
local var="$1" label="$2" answer=""
|
||||
if [[ $INTERACTIVE -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
read -rsp " $label: " answer; echo
|
||||
eval "$var=\"\$answer\""
|
||||
}
|
||||
|
||||
render_env() {
|
||||
# render_env <example-path> <output-path> <VAR=VALUE ...>
|
||||
# Rewrites lines matching ^#?\s*KEY= to KEY=VALUE. Leaves other lines alone.
|
||||
local example="$1" output="$2"
|
||||
shift 2
|
||||
cp "$example" "$output"
|
||||
for pair in "$@"; do
|
||||
local key="${pair%%=*}"
|
||||
local val="${pair#*=}"
|
||||
# Escape for sed replacement
|
||||
local esc
|
||||
esc=$(printf '%s' "$val" | sed 's/[&/\]/\\&/g')
|
||||
# Match either "KEY=..." or "# KEY=..."
|
||||
if grep -qE "^#?\s*${key}=" "$output"; then
|
||||
sed -i -E "s|^#?\s*${key}=.*|${key}=${esc}|" "$output"
|
||||
else
|
||||
printf '\n%s=%s\n' "$key" "$val" >> "$output"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ---------- collect config ----------
|
||||
WEB_ENV_PATH="$REPO_ROOT/modules/web/deploy/.env"
|
||||
DASHBOARD_ENV_PATH="$REPO_ROOT/modules/dashboard/deploy/.env"
|
||||
|
||||
if [[ $INTERACTIVE -eq 1 ]]; then
|
||||
step "Collecting configuration"
|
||||
|
||||
info "Press Enter to accept defaults. Leave paid API keys blank to skip."
|
||||
echo
|
||||
|
||||
# LLM on the GPU machine
|
||||
prompt LLM_HOST "GPU host IP or hostname (e.g. 10.11.10.42)" "localhost"
|
||||
prompt LLM_PORT "LLM inference API port" "14011"
|
||||
prompt VLLM_PORT "Raw vLLM port (for dashboard health checks)" "14001"
|
||||
|
||||
LLM_URL="http://${LLM_HOST}:${LLM_PORT}"
|
||||
VLLM_URL="http://${LLM_HOST}:${VLLM_PORT}"
|
||||
ok "LLM URL: $LLM_URL"
|
||||
ok "vLLM URL: $VLLM_URL"
|
||||
|
||||
# SearXNG
|
||||
echo
|
||||
if [[ $DEPLOY_SEARXNG -eq 1 ]]; then
|
||||
SEARXNG_HOST="didiAI-web-searxng"
|
||||
SEARXNG_PORT="8080"
|
||||
info "Will deploy SearXNG internally ($SEARXNG_HOST:$SEARXNG_PORT)"
|
||||
else
|
||||
prompt SEARXNG_HOST "SearXNG container name or host" "didiAI-web-searxng"
|
||||
prompt SEARXNG_PORT "SearXNG port" "8080"
|
||||
fi
|
||||
SEARXNG_URL="http://${SEARXNG_HOST}:${SEARXNG_PORT}"
|
||||
ok "SearXNG URL: $SEARXNG_URL"
|
||||
|
||||
# External URLs
|
||||
echo
|
||||
prompt WEB_EXT "Public-facing Web API URL" "http://localhost:51100"
|
||||
prompt DASH_EXT "Public-facing Dashboard URL" "http://localhost:51300"
|
||||
|
||||
# Paid provider keys
|
||||
echo
|
||||
info "Premium tier API keys (leave blank to skip):"
|
||||
prompt_secret SERPAPI_KEY "SerpAPI key (blank to skip)"
|
||||
prompt_secret TAVILY_KEY "Tavily key"
|
||||
prompt_secret BRAVE_KEY "Brave key"
|
||||
prompt_secret LINKUP_KEY "LinkUp key"
|
||||
prompt_secret OPENROUTER_KEY "OpenRouter key"
|
||||
|
||||
# DB password
|
||||
echo
|
||||
prompt USE_RANDOM_DB_PW "Generate random Postgres password? (y/n)" "y"
|
||||
if [[ "${USE_RANDOM_DB_PW,,}" == "y" ]]; then
|
||||
DB_PASSWORD="$(gen_password)"
|
||||
ok "Generated DB password: $DB_PASSWORD"
|
||||
else
|
||||
prompt_secret DB_PASSWORD "Postgres password for dashboard DB"
|
||||
fi
|
||||
else
|
||||
step "Non-interactive mode — using existing .env files"
|
||||
[[ -f "$WEB_ENV_PATH" ]] || die "Missing $WEB_ENV_PATH"
|
||||
[[ -f "$DASHBOARD_ENV_PATH" ]] || die "Missing $DASHBOARD_ENV_PATH"
|
||||
ok "Existing .env files found"
|
||||
fi
|
||||
|
||||
# ---------- render .env files (interactive only) ----------
|
||||
if [[ $INTERACTIVE -eq 1 ]]; then
|
||||
step "Rendering .env files"
|
||||
|
||||
render_env \
|
||||
"$REPO_ROOT/modules/web/.env.example" \
|
||||
"$WEB_ENV_PATH" \
|
||||
"WEB_SEARXNG_BASE_URL=$SEARXNG_URL" \
|
||||
"WEB_EXTERNAL_URL=$WEB_EXT" \
|
||||
"WEB_LLM_BASE_URL=$LLM_URL" \
|
||||
"WEB_VISION_BASE_URL=$LLM_URL" \
|
||||
"WEB_DASHBOARD_URL=http://didiAI-dashboard:51300" \
|
||||
${SERPAPI_KEY:+"WEB_SERPAPI_API_KEY=$SERPAPI_KEY"} \
|
||||
${TAVILY_KEY:+"WEB_TAVILY_API_KEY=$TAVILY_KEY"} \
|
||||
${BRAVE_KEY:+"WEB_BRAVE_API_KEY=$BRAVE_KEY"} \
|
||||
${LINKUP_KEY:+"WEB_LINKUP_API_KEY=$LINKUP_KEY"} \
|
||||
${OPENROUTER_KEY:+"WEB_OPENROUTER_API_KEY=$OPENROUTER_KEY"}
|
||||
ok "Wrote $WEB_ENV_PATH"
|
||||
|
||||
render_env \
|
||||
"$REPO_ROOT/modules/dashboard/.env.example" \
|
||||
"$DASHBOARD_ENV_PATH" \
|
||||
"DASHBOARD_DB_PASSWORD=$DB_PASSWORD" \
|
||||
"DASHBOARD_EXTERNAL_URL=$DASH_EXT" \
|
||||
"DASHBOARD_SEARXNG_URL=$SEARXNG_URL" \
|
||||
"DASHBOARD_LLM_API_URL=$LLM_URL" \
|
||||
"DASHBOARD_VLLM_QWEN_URL=$VLLM_URL" \
|
||||
${SERPAPI_KEY:+"DASHBOARD_SERPAPI_API_KEY=$SERPAPI_KEY"} \
|
||||
${TAVILY_KEY:+"DASHBOARD_TAVILY_API_KEY=$TAVILY_KEY"} \
|
||||
${BRAVE_KEY:+"DASHBOARD_BRAVE_API_KEY=$BRAVE_KEY"} \
|
||||
${LINKUP_KEY:+"DASHBOARD_LINKUP_API_KEY=$LINKUP_KEY"} \
|
||||
${OPENROUTER_KEY:+"DASHBOARD_OPENROUTER_API_KEY=$OPENROUTER_KEY"}
|
||||
ok "Wrote $DASHBOARD_ENV_PATH"
|
||||
fi
|
||||
|
||||
# ---------- optional SearXNG ----------
|
||||
if [[ $DEPLOY_SEARXNG -eq 1 ]]; then
|
||||
step "Deploying SearXNG"
|
||||
if [[ -d "$REPO_ROOT/modules/web/deploy/metasearch" ]]; then
|
||||
(cd "$REPO_ROOT/modules/web/deploy/metasearch" && docker compose up -d)
|
||||
ok "SearXNG stack up"
|
||||
else
|
||||
warn "modules/web/deploy/metasearch not found — skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- deploy dashboard ----------
|
||||
step "Deploying dashboard (PostgreSQL + UI on port 51300)"
|
||||
|
||||
(cd "$REPO_ROOT/modules/dashboard/deploy" && docker compose --profile dashboard up -d --build)
|
||||
ok "Dashboard containers started"
|
||||
|
||||
info "Waiting for dashboard to become healthy..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sfm 2 http://localhost:51300/health >/dev/null 2>&1; then
|
||||
ok "Dashboard healthy"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
if [[ $i -eq 30 ]]; then
|
||||
fail "Dashboard did not become healthy within 60s"
|
||||
docker logs --tail 50 didiAI-dashboard || true
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------- deploy web ----------
|
||||
step "Deploying web-api (port 51100)"
|
||||
|
||||
(cd "$REPO_ROOT/modules/web/deploy" && docker compose --profile api up -d --build)
|
||||
ok "Web API containers started"
|
||||
|
||||
info "Waiting for web-api to become healthy..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sfm 2 http://localhost:51100/health >/dev/null 2>&1; then
|
||||
ok "Web API healthy"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
if [[ $i -eq 30 ]]; then
|
||||
fail "Web API did not become healthy within 60s"
|
||||
docker logs --tail 50 didiAI-web-api || true
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------- create admin user ----------
|
||||
ADMIN_TOKEN=""
|
||||
if [[ $INTERACTIVE -eq 1 ]]; then
|
||||
step "Creating dashboard admin user"
|
||||
|
||||
prompt ADMIN_NAME "Admin username" "admin"
|
||||
prompt ADMIN_EMAIL "Admin email (optional)" ""
|
||||
|
||||
set +e
|
||||
ADMIN_OUTPUT=$(docker exec -i didiAI-dashboard python -m dashboard.cli \
|
||||
create-user "$ADMIN_NAME" \
|
||||
${ADMIN_EMAIL:+--email "$ADMIN_EMAIL"} \
|
||||
--role admin 2>&1)
|
||||
ADMIN_RC=$?
|
||||
set -e
|
||||
|
||||
if [[ $ADMIN_RC -eq 0 ]]; then
|
||||
ADMIN_TOKEN=$(echo "$ADMIN_OUTPUT" | grep -oE '[A-Za-z0-9_-]{32,}' | tail -1)
|
||||
ok "Admin user '$ADMIN_NAME' created"
|
||||
else
|
||||
warn "Admin user creation failed (maybe already exists). You can create one later:"
|
||||
warn " docker exec didiAI-dashboard python -m dashboard.cli create-user <name>"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- smoke tests ----------
|
||||
if [[ $SKIP_SMOKE -eq 0 ]]; then
|
||||
step "Running smoke tests"
|
||||
|
||||
# Dashboard
|
||||
if curl -sfm 3 http://localhost:51300/health | grep -q '"status":"healthy"'; then
|
||||
ok "Dashboard /health"
|
||||
else
|
||||
warn "Dashboard /health unexpected response"
|
||||
fi
|
||||
|
||||
# Web API
|
||||
if curl -sfm 3 http://localhost:51100/health >/dev/null 2>&1; then
|
||||
ok "Web API /health"
|
||||
else
|
||||
warn "Web API /health failed"
|
||||
fi
|
||||
|
||||
# Providers live endpoint
|
||||
PROV_COUNT=$(curl -sfm 10 http://localhost:51300/api/stats/providers 2>/dev/null |
|
||||
grep -oE '"name"' | wc -l || echo 0)
|
||||
if [[ $PROV_COUNT -gt 0 ]]; then
|
||||
ok "Dashboard sees $PROV_COUNT providers"
|
||||
else
|
||||
warn "Dashboard provider probe returned no data (LLM host may be unreachable)"
|
||||
fi
|
||||
|
||||
# Free tier search — only if SearXNG is reachable
|
||||
FREE_CODE=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 15 \
|
||||
-X POST http://localhost:51100/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"queries":["bootstrap test"],"max_results":3}' || echo "fail")
|
||||
if [[ "$FREE_CODE" == "200" ]]; then
|
||||
ok "Free tier search works"
|
||||
else
|
||||
warn "Free tier search returned $FREE_CODE (check SearXNG connectivity)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- summary ----------
|
||||
step "Bootstrap complete"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
${C_BOLD}Services${C_RESET}
|
||||
────────────────────────────────────────────────────────
|
||||
Dashboard UI http://localhost:51300
|
||||
Web API http://localhost:51100
|
||||
Dashboard DB localhost:15432 (internal only in prod)
|
||||
|
||||
${C_BOLD}Containers${C_RESET}
|
||||
────────────────────────────────────────────────────────
|
||||
$(docker ps --format " {{.Names}} ({{.Status}})" | grep -E "didiAI-(dashboard|web-api)" | sort)
|
||||
|
||||
EOF
|
||||
|
||||
if [[ -n "$ADMIN_TOKEN" ]]; then
|
||||
cat <<EOF
|
||||
${C_BOLD}Admin Bearer token${C_RESET} (save this!)
|
||||
────────────────────────────────────────────────────────
|
||||
${C_GREEN}$ADMIN_TOKEN${C_RESET}
|
||||
|
||||
Usage:
|
||||
curl -H "Authorization: Bearer $ADMIN_TOKEN" \\
|
||||
-X PUT http://localhost:51300/api/config/web.premium.strategy \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"value":"parallel"}'
|
||||
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
${C_BOLD}Next steps${C_RESET}
|
||||
────────────────────────────────────────────────────────
|
||||
1. Open http://localhost:51300 in your browser
|
||||
2. Verify providers on /providers page
|
||||
3. Adjust runtime config on /config page
|
||||
4. Read DEPLOYMENT.md for details + troubleshooting
|
||||
EOF
|
||||
Loading…
Add table
Add a link
Reference in a new issue