Livrare LOT 1 - Didi

This commit is contained in:
Dezvoltari Evotech 2026-06-25 14:13:25 -07:00
commit 5380c3fc63
990 changed files with 133308 additions and 0 deletions

405
ai_platform/bootstrap.sh Normal file
View 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