feat(docker): update Docker configuration for PraisonAIUI deployment

- Modified Dockerfile to enhance deployment process, including new environment variables and installation method for Python packages.
- Updated .dockerignore to exclude additional files and directories.
- Incremented project version in pyproject.toml.
- Streamlined feature usage by removing unused tools from the resolver in channels.py and providers/__init__.py.
- Expanded test coverage in test_runner.py to include more endpoints and health checks.
This commit is contained in:
Mervin Praison committed 2026-03-11 13:45:32 +00:00
1 parent 4254283d14
commit 6ff6883994
7 files changed
+528 -31

No files matched your search

+13
View File
@@ -16,3 +16,16 @@ examples
!README.md
.windsurf
.cursor
.agent
.praisonai
.vscode
_site
site
aiui
uv.lock
prd.md
AGENTS.md
CNAME
mkdocs.yml
.env
.DS_Store
+32 -14
View File
@@ -1,33 +1,51 @@
# Production Dockerfile for AIUI
# Minimal image with health check and configurable port
# Production Dockerfile for PraisonAIUI
# Used for hostaibot managed hosting and standalone deployment
#
# Build: docker build -t praisonaiui:test .
# Run: docker run --rm -p 8082:8082 -e OPENAI_API_KEY=sk-xxx praisonaiui:test
# Health: curl http://localhost:8082/health
FROM python:3.12-slim AS base
# Set environment variables
# Environment
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
AIUI_PORT=8082 \
AIUI_HOST=0.0.0.0
AIUI_HOST=0.0.0.0 \
AIUI_DATA_DIR=/data
WORKDIR /app
# Install system dependencies
# System dependencies
# - curl: for HEALTHCHECK
# - build-essential: for native Python wheel compilation
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy and install Python package
# Install Python package with bot channel extras
# [bot] includes: python-telegram-bot, discord.py, slack_sdk, slack-bolt
COPY pyproject.toml README.md ./
COPY src/ src/
RUN pip install --no-cache-dir .
RUN pip install --no-cache-dir ".[bot]"
# Expose port
EXPOSE ${AIUI_PORT}
# Copy default app for managed hosting
# Required because `aiui run` needs a positional APP_FILE argument
COPY app.py .
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:${AIUI_PORT}/health || exit 1
# Data volume for persistence (sessions, config, memory)
# AIUI reads AIUI_DATA_DIR env var -> /data/config.yaml, /data/sessions/
RUN mkdir -p /data
VOLUME /data
# Entrypoint
# Expose the default AIUI port
EXPOSE 8082
# Health check (hardcoded port for robustness)
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -f http://localhost:8082/health || exit 1
# Entrypoint: aiui run <app_file> [options]
ENTRYPOINT ["aiui", "run"]
CMD ["--host", "0.0.0.0", "--port", "8082"]
CMD ["app.py", "--host", "0.0.0.0", "--port", "8082"]
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "aiui"
version = "0.3.57"
version = "0.3.58"
description = "YAML-driven website generator - CLI and compiler"
readme = "README.md"
license = {text = "MIT"}
+15 -2
View File
@@ -220,7 +220,7 @@ class ChannelsFeature(BaseFeatureProtocol):
try:
from praisonai.tool_resolver import ToolResolver
resolver = ToolResolver()
agent_tools = resolver.resolve_many(["internet_search", "get_current_time"])
agent_tools = resolver.resolve_many(["internet_search"])
except ImportError:
pass # praisonai not installed — no tools
@@ -679,8 +679,21 @@ class ChannelsFeature(BaseFeatureProtocol):
if hasattr(bot, "probe"):
try:
result = await bot.probe()
# Convert dataclass/namedtuple to dict for JSON serialization
if hasattr(result, "__dict__") and not isinstance(result, dict):
try:
import dataclasses
probe_data = dataclasses.asdict(result)
except (TypeError, Exception):
probe_data = result.__dict__
elif hasattr(result, "_asdict"):
probe_data = result._asdict()
elif isinstance(result, dict):
probe_data = result
else:
probe_data = str(result)
_mark_healthy()
return JSONResponse({"success": True, "probe": result})
return JSONResponse({"success": True, "probe": probe_data})
except Exception as e:
return JSONResponse({"success": False, "error": str(e)})
running = bot.is_running if hasattr(bot, "is_running") else False
+1 -1
View File
@@ -216,7 +216,7 @@ class PraisonAIProvider(BaseProvider):
try:
from praisonai.tool_resolver import ToolResolver
resolver = ToolResolver()
default_tools = resolver.resolve_many(["internet_search", "get_current_time"])
default_tools = resolver.resolve_many(["internet_search"])
if default_tools:
kwargs["tools"] = default_tools
except ImportError:
+465 -12
View File
@@ -15,6 +15,7 @@ Usage::
from __future__ import annotations
import json
import os
import time
import uuid
from pathlib import Path
@@ -326,7 +327,7 @@ def test_sessions(server: str = _SERVER_OPT) -> None:
@test_app.command("endpoints")
def test_endpoints(server: str = _SERVER_OPT) -> None:
"""Test all API endpoints: features, sessions, chat, provider."""
"""Test all API endpoints: features, sessions, chat, provider, channels, agents, config, and more."""
r = _Results()
console.print(Panel("[bold]Endpoint Tests[/bold]", border_style="blue"))
@@ -334,19 +335,27 @@ def test_endpoints(server: str = _SERVER_OPT) -> None:
console.print(f"[red]Cannot reach server at {server}[/red]")
raise typer.Exit(code=1)
# Features
# ── Health ───────────────────────────────────────────────────────
console.print("\n[dim]Health[/dim]")
try:
data = _get(server, "/health")
r.check("GET /health", data.get("status") == "ok")
except Exception as e:
r.check("GET /health", False, str(e))
# ── Features ─────────────────────────────────────────────────────
console.print("\n[dim]Features[/dim]")
try:
data = _get(server, "/api/features")
features = data.get("features", [])
r.check("GET /api/features", len(features) > 0, f"Got {len(features)} features")
names = [f.get("name") for f in features]
for needed in ["chat"]:
for needed in ["chat", "channels", "agents_crud", "config_runtime"]:
r.check(f"Feature '{needed}' registered", needed in names)
except Exception as e:
r.check("GET /api/features", False, str(e))
# Sessions
# ── Sessions ─────────────────────────────────────────────────────
console.print("\n[dim]Sessions[/dim]")
try:
data = _get(server, "/api/sessions")
@@ -354,7 +363,7 @@ def test_endpoints(server: str = _SERVER_OPT) -> None:
except Exception as e:
r.check("GET /api/sessions", False, str(e))
# Chat history (non-existent session should return empty)
# ── Chat ─────────────────────────────────────────────────────────
console.print("\n[dim]Chat[/dim]")
try:
data = _get(server, "/api/chat/history/nonexistent-test")
@@ -363,7 +372,6 @@ def test_endpoints(server: str = _SERVER_OPT) -> None:
except Exception as e:
r.check("GET /api/chat/history", False, str(e))
# Send chat
test_sid = f"test-ep-{uuid.uuid4().hex[:8]}"
try:
resp = _post(server, "/api/chat/send", {
@@ -374,7 +382,189 @@ def test_endpoints(server: str = _SERVER_OPT) -> None:
except Exception as e:
r.check("POST /api/chat/send", False, str(e))
# Provider (optional — may not be mounted in all configs)
# ── Channels ─────────────────────────────────────────────────────
console.print("\n[dim]Channels[/dim]")
try:
data = _get(server, "/api/channels")
r.check("GET /api/channels", "channels" in data)
except Exception as e:
r.check("GET /api/channels", False, str(e))
try:
data = _get(server, "/api/channels/platforms")
platforms = data.get("platforms", [])
r.check("GET /api/channels/platforms", len(platforms) > 0, f"Got {len(platforms)}")
except Exception as e:
r.check("GET /api/channels/platforms", False, str(e))
# ── Agents ───────────────────────────────────────────────────────
console.print("\n[dim]Agents[/dim]")
try:
data = _get(server, "/api/agents/definitions")
agents = data.get("agents", [])
r.check("GET /api/agents/definitions", isinstance(agents, list))
except Exception as e:
r.check("GET /api/agents/definitions", False, str(e))
try:
data = _get(server, "/api/agents/models")
r.check("GET /api/agents/models", "models" in data)
except Exception as e:
r.skip("GET /api/agents/models")
# ── Config ───────────────────────────────────────────────────────
console.print("\n[dim]Config[/dim]")
try:
data = _get(server, "/api/config")
r.check("GET /api/config", "config" in data)
except Exception as e:
r.check("GET /api/config", False, str(e))
try:
data = _get(server, "/api/config/runtime")
r.check("GET /api/config/runtime", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/config/runtime")
try:
data = _get(server, "/api/config/schema")
r.check("GET /api/config/schema", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/config/schema")
# ── Skills ───────────────────────────────────────────────────────
console.print("\n[dim]Skills[/dim]")
try:
data = _get(server, "/api/skills")
r.check("GET /api/skills", "skills" in data)
except Exception as e:
r.skip("GET /api/skills")
# ── Memory ───────────────────────────────────────────────────────
console.print("\n[dim]Memory[/dim]")
try:
data = _get(server, "/api/memory")
r.check("GET /api/memory", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/memory")
# ── Schedules ────────────────────────────────────────────────────
console.print("\n[dim]Schedules[/dim]")
try:
data = _get(server, "/api/schedules")
r.check("GET /api/schedules", "schedules" in data)
except Exception as e:
r.skip("GET /api/schedules")
# ── Hooks ────────────────────────────────────────────────────────
console.print("\n[dim]Hooks[/dim]")
try:
data = _get(server, "/api/hooks")
r.check("GET /api/hooks", "hooks" in data)
except Exception as e:
r.skip("GET /api/hooks")
# ── Workflows ────────────────────────────────────────────────────
console.print("\n[dim]Workflows[/dim]")
try:
data = _get(server, "/api/workflows")
r.check("GET /api/workflows", "workflows" in data)
except Exception as e:
r.skip("GET /api/workflows")
# ── Approvals ────────────────────────────────────────────────────
console.print("\n[dim]Approvals[/dim]")
try:
data = _get(server, "/api/approvals")
r.check("GET /api/approvals", "approvals" in data)
except Exception as e:
r.skip("GET /api/approvals")
try:
data = _get(server, "/api/approvals/pending")
r.check("GET /api/approvals/pending", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/approvals/pending")
# ── Auth ─────────────────────────────────────────────────────────
console.print("\n[dim]Auth[/dim]")
try:
data = _get(server, "/api/auth/status")
r.check("GET /api/auth/status", "mode" in data or "status" in data)
except Exception as e:
r.skip("GET /api/auth/status")
try:
data = _get(server, "/api/auth/config")
r.check("GET /api/auth/config", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/auth/config")
# ── Knowledge ────────────────────────────────────────────────────
console.print("\n[dim]Knowledge[/dim]")
try:
data = _get(server, "/api/knowledge")
r.check("GET /api/knowledge", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/knowledge")
# ── Guardrails ───────────────────────────────────────────────────
console.print("\n[dim]Guardrails[/dim]")
try:
data = _get(server, "/api/guardrails")
r.check("GET /api/guardrails", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/guardrails")
# ── i18n ─────────────────────────────────────────────────────────
console.print("\n[dim]i18n[/dim]")
try:
data = _get(server, "/api/i18n/locale")
r.check("GET /api/i18n/locale", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/i18n/locale")
# ── Telemetry ────────────────────────────────────────────────────
console.print("\n[dim]Telemetry[/dim]")
try:
data = _get(server, "/api/telemetry")
r.check("GET /api/telemetry", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/telemetry")
# ── Security ─────────────────────────────────────────────────────
console.print("\n[dim]Security[/dim]")
try:
data = _get(server, "/api/security/status")
r.check("GET /api/security/status", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/security/status")
# ── Logs ─────────────────────────────────────────────────────────
console.print("\n[dim]Logs[/dim]")
try:
data = _get(server, "/api/logs")
r.check("GET /api/logs", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/logs")
# ── Usage ────────────────────────────────────────────────────────
console.print("\n[dim]Usage[/dim]")
try:
data = _get(server, "/api/usage")
r.check("GET /api/usage", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/usage")
# ── Nodes ────────────────────────────────────────────────────────
console.print("\n[dim]Nodes[/dim]")
try:
data = _get(server, "/api/nodes")
r.check("GET /api/nodes", isinstance(data, dict))
except Exception as e:
r.skip("GET /api/nodes")
# ── Provider (optional) ──────────────────────────────────────────
console.print("\n[dim]Provider (optional)[/dim]")
for path, label in [
("/api/provider/health", "GET /api/provider/health"),
@@ -386,13 +576,269 @@ def test_endpoints(server: str = _SERVER_OPT) -> None:
except Exception:
r.skip(label)
# Health
console.print("\n[dim]Health[/dim]")
ok = r.summary()
if not ok:
raise typer.Exit(code=1)
def _put(server: str, path: str, body: dict = None) -> dict:
data = json.dumps(body or {}).encode()
req = Request(
f"{server}{path}", data=data,
headers={"Content-Type": "application/json"}, method="PUT",
)
with urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
def _delete(server: str, path: str) -> dict:
req = Request(f"{server}{path}", method="DELETE")
with urlopen(req, timeout=15) as resp:
return json.loads(resp.read())
@test_app.command("features")
def test_features(server: str = _SERVER_OPT) -> None:
"""Deep functional CRUD tests: guardrails, knowledge, memory, workflows, channels, eval."""
r = _Results()
console.print(Panel("[bold]Feature CRUD Tests[/bold]", border_style="magenta"))
if not _check_server(server):
console.print(f"[red]Cannot reach server at {server}[/red]")
raise typer.Exit(code=1)
# ── Guardrails: register → verify → delete ────────────────
console.print("\n[dim]Guardrails: Create → Verify → Delete[/dim]")
try:
data = _get(server, "/health")
r.check("GET /health", data.get("status") == "ok")
before = _get(server, "/api/guardrails")
before_count = before.get("count", 0)
new_gr = _post(server, "/api/guardrails/register", {
"name": "IntegrationTestGuardrail",
"description": "Test guardrail from aiui test",
"type": "input",
"config": {"block_words": ["blocked"]},
})
gr_id = new_gr.get("registered") or new_gr.get("id") or new_gr.get("guardrail_id")
r.check("Register guardrail", gr_id is not None)
after = _get(server, "/api/guardrails")
r.check("Guardrail appears in list", after.get("count", 0) > before_count)
status = _get(server, "/api/guardrails/status")
r.check("Guardrails status", isinstance(status, dict))
violations = _get(server, "/api/guardrails/violations")
r.check("Guardrails violations", isinstance(violations, dict))
if gr_id:
_delete(server, f"/api/guardrails/{gr_id}")
final = _get(server, "/api/guardrails")
r.check("Delete guardrail", final.get("count", 0) == before_count)
except Exception as e:
r.check("GET /health", False, str(e))
r.check("Guardrails CRUD", False, str(e))
# ── Knowledge: add → search → delete ──────────────────────
console.print("\n[dim]Knowledge: Add → Search → Delete[/dim]")
try:
before = _get(server, "/api/knowledge")
before_count = before.get("count", 0)
unique_fact = f"Capital of Testlandia is Verifytown-{uuid.uuid4().hex[:6]}"
new_k = _post(server, "/api/knowledge", {
"title": "Testlandia Fact",
"text": unique_fact,
"metadata": {"source": "aiui-test"},
})
k_id = new_k.get("id") or new_k.get("entry_id")
r.check("Add knowledge", k_id is not None)
after = _get(server, "/api/knowledge")
r.check("Knowledge count increased", after.get("count", 0) > before_count)
search = _post(server, "/api/knowledge/search", {
"query": "capital of Testlandia",
"top_k": 5,
})
results = search.get("results", search.get("entries", []))
found = any("Testlandia" in str(x) or "Verifytown" in str(x) for x in results)
r.check("Knowledge search returns our entry", found)
status = _get(server, "/api/knowledge/status")
r.check("Knowledge status", isinstance(status, dict))
if k_id:
_delete(server, f"/api/knowledge/{k_id}")
final = _get(server, "/api/knowledge")
r.check("Delete knowledge", final.get("count", 0) == before_count)
except Exception as e:
r.check("Knowledge CRUD", False, str(e))
# ── Memory: store → verify → search → session → delete ───
console.print("\n[dim]Memory: Store → Verify → Search → Delete[/dim]")
try:
before = _get(server, "/api/memory")
before_count = len(before.get("memories", []))
mem_text = f"Test fact: user serial {uuid.uuid4().hex[:8]}"
new_mem = _post(server, "/api/memory", {
"text": mem_text,
"memory_type": "long",
"metadata": {"source": "aiui-test"},
})
mem_id = new_mem.get("id") or new_mem.get("memory_id")
r.check("Store memory", mem_id is not None)
after = _get(server, "/api/memory")
r.check("Memory count increased", len(after.get("memories", [])) > before_count)
search = _post(server, "/api/memory/search", {
"query": "user serial",
"top_k": 5,
})
r.check("Memory search works", isinstance(search, dict))
mstatus = _get(server, "/api/memory/status")
r.check("Memory status", isinstance(mstatus, dict))
if mem_id:
_delete(server, f"/api/memory/{mem_id}")
r.check("Delete memory", True)
except Exception as e:
r.check("Memory CRUD", False, str(e))
# ── Chat: cross-session memory verification ───────────────
console.print("\n[dim]Memory: Cross-session recall via chat[/dim]")
sid_store = f"test-store-{uuid.uuid4().hex[:6]}"
try:
_post(server, "/api/chat/send", {
"message": "My cat is named Thunderpaws. Please remember this.",
"session_id": sid_store,
})
_wait_for_response(server, sid_store)
r.check("Chat: fact stored via conversation", True)
time.sleep(2)
sid_recall = f"test-recall-{uuid.uuid4().hex[:6]}"
_post(server, "/api/chat/send", {
"message": "What is the name of my cat?",
"session_id": sid_recall,
})
recall_msgs = _wait_for_response(server, sid_recall, timeout=25)
asst_msgs = [m for m in recall_msgs if m.get("role") == "assistant"]
if asst_msgs:
content = asst_msgs[-1].get("content", "")
r.check(
"Cross-session: recalls 'Thunderpaws'",
"thunderpaws" in content.lower() or "thunder" in content.lower(),
f"Got: {content[:120]}",
)
else:
r.check("Cross-session recall response", False, "No assistant msg")
except Exception as e:
r.check("Cross-session memory", False, str(e))
# ── Channels: create → test connection → verify → delete ──
console.print("\n[dim]Channels: Create → Test → Verify → Delete[/dim]")
# Load real token from env or .env file
tg_token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
if not tg_token:
# Try loading from .env
for env_path in [Path.cwd() / ".env", Path.home() / ".env"]:
if env_path.exists():
for line in env_path.read_text().splitlines():
if line.startswith("TELEGRAM_BOT_TOKEN="):
tg_token = line.split("=", 1)[1].strip().strip('"').strip("'")
break
if tg_token:
break
if not tg_token:
r.skip("Channels (no TELEGRAM_BOT_TOKEN in env)")
else:
try:
before = _get(server, "/api/channels")
before_count = len(before.get("channels", []))
new_ch = _post(server, "/api/channels", {
"name": "IntTestBot",
"platform": "telegram",
"config": {"bot_token": tg_token},
})
ch_id = new_ch.get("id") or new_ch.get("channel_id")
r.check("Create channel (real token)", ch_id is not None)
after = _get(server, "/api/channels")
r.check("Channel appears in list", len(after.get("channels", [])) > before_count)
platforms = _get(server, "/api/channels/platforms")
r.check("Platforms list populated", len(platforms.get("platforms", [])) > 0)
# Test connection — this calls the Telegram API with the real token
if ch_id:
test_resp = _post(server, f"/api/channels/{ch_id}/test")
r.check(
"Channel test (Telegram API)",
test_resp.get("success") is True,
str(test_resp)[:150],
)
if ch_id:
_delete(server, f"/api/channels/{ch_id}")
final = _get(server, "/api/channels")
r.check("Delete channel", len(final.get("channels", [])) == before_count)
except Exception as e:
r.check("Channels CRUD", False, str(e))
# ── Workflows: create → verify → run → delete ─────────────
console.print("\n[dim]Workflows: Create → Run → Delete[/dim]")
try:
before = _get(server, "/api/workflows")
before_count = len(before.get("workflows", []))
new_wf = _post(server, "/api/workflows", {
"name": "test-integration-wf",
"description": "Integration test workflow",
"steps": [{"type": "log", "message": "Integration test step"}],
})
wf_id = new_wf.get("id") or new_wf.get("workflow_id")
r.check("Create workflow", wf_id is not None)
after = _get(server, "/api/workflows")
r.check("Workflow appears in list", len(after.get("workflows", [])) > before_count)
if wf_id:
run = _post(server, f"/api/workflows/{wf_id}/run")
r.check("Run workflow", isinstance(run, dict))
status = _get(server, f"/api/workflows/{wf_id}/status")
r.check("Workflow status", isinstance(status, dict))
_delete(server, f"/api/workflows/{wf_id}")
final = _get(server, "/api/workflows")
r.check("Delete workflow", len(final.get("workflows", [])) == before_count)
except Exception as e:
r.check("Workflows CRUD", False, str(e))
# ── Eval: status → judges → scores → run ─────────────────
console.print("\n[dim]Eval: Run & Verify[/dim]")
try:
estatus = _get(server, "/api/eval/status")
r.check("Eval status", isinstance(estatus, dict))
judges = _get(server, "/api/eval/judges")
r.check("Eval judges", isinstance(judges, dict))
scores = _get(server, "/api/eval/scores")
r.check("Eval scores", isinstance(scores, dict))
erun = _post(server, "/api/eval/run", {
"prompt": "What is 2+2?",
"expected": "4",
})
r.check("Eval run", isinstance(erun, dict))
except Exception as e:
r.check("Eval", False, str(e))
ok = r.summary()
if not ok:
@@ -444,6 +890,13 @@ def test_all(server: str = _SERVER_OPT) -> None:
if e.code:
failed = True
console.print("\n" + "═" * 50)
try:
test_features(server)
except SystemExit as e:
if e.code:
failed = True
console.print("\n" + "═" * 50)
if failed:
console.print("\n[red bold]Some tests failed.[/red bold]")
Generated
+1 -1
View File
@@ -158,7 +158,7 @@ wheels = [
[[package]]
name = "aiui"
version = "0.3.57"
version = "0.3.58"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },