mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
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:
1 parent
4254283d14
commit
6ff6883994
7 files changed
+528
-31
No files matched your search
@@ -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
@@ -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
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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]")
|
||||
|
||||
Reference in new issue
Block a user