mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
17
Commits
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# Knowledge Embedding Model Configuration
|
||||
|
||||
Knowledge / RAG search uses vector **embeddings**. The default embedding model is
|
||||
`text-embedding-3-small`. Many OpenAI keys or projects do not have access to that
|
||||
model (organization tier, model allowlist, or regional restrictions), which caused
|
||||
knowledge search to silently degrade to local substring matching.
|
||||
|
||||
PraisonAIUI now makes this failure mode configurable and observable.
|
||||
|
||||
## Configure the embedding model
|
||||
|
||||
Set an accessible model via environment variable:
|
||||
|
||||
```bash
|
||||
export AIUI_EMBEDDING_MODEL=text-embedding-ada-002
|
||||
```
|
||||
|
||||
`OPENAI_EMBEDDING_MODEL` is also honoured as a fallback if `AIUI_EMBEDDING_MODEL`
|
||||
is not set.
|
||||
|
||||
## Fallback chain
|
||||
|
||||
The OpenAI-compatible `/v1/embeddings` endpoint tries the requested (or default)
|
||||
model first, then each model in the fallback chain if the primary returns a
|
||||
`model_not_found` / 403 access error:
|
||||
|
||||
```bash
|
||||
export AIUI_EMBEDDING_FALLBACK_MODELS="text-embedding-ada-002,text-embedding-3-small"
|
||||
```
|
||||
|
||||
If every candidate is inaccessible, the endpoint returns `503` with an actionable
|
||||
`model_not_found` error instead of a generic `500`.
|
||||
|
||||
## Observing degraded search
|
||||
|
||||
`GET /api/knowledge/status` reports embedding availability:
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 12,
|
||||
"backend": "SDKKnowledgeManager",
|
||||
"status": "degraded",
|
||||
"embedding": {"available": false, "error": "Project does not have access to model `text-embedding-3-small`"},
|
||||
"warnings": ["Embedding model unavailable; knowledge search degraded to local text matching. Set AIUI_EMBEDDING_MODEL to an accessible model."]
|
||||
}
|
||||
```
|
||||
|
||||
`POST /api/knowledge/search` indicates whether results came from vector search or a
|
||||
degraded local fallback:
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [],
|
||||
"count": 0,
|
||||
"search_mode": "fallback",
|
||||
"warning": "Vector search unavailable: Project does not have access to model text-embedding-3-small. Set AIUI_EMBEDDING_MODEL=text-embedding-ada-002 or enable embedding models in your provider project."
|
||||
}
|
||||
```
|
||||
|
||||
A dashboard can show a warning banner whenever `search_mode == "fallback"` or
|
||||
`embedding.available == false`.
|
||||
|
||||
## Model reference
|
||||
|
||||
| Model | Notes |
|
||||
|-------|-------|
|
||||
| `text-embedding-3-small` | Default — best cost/quality when available |
|
||||
| `text-embedding-3-large` | Higher quality — may also be restricted |
|
||||
| `text-embedding-ada-002` | Legacy — widely available on older projects |
|
||||
@@ -44,15 +44,18 @@ import sys
|
||||
# ── Import praisonaiui ──────────────────────────────────────
|
||||
import praisonaiui as aiui
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check gateway availability ──────────────────────────────
|
||||
try:
|
||||
from praisonaiui.integration import AIUIGateway
|
||||
from praisonaiagents import Agent
|
||||
GATEWAY_OK = True
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Gateway dependencies missing: {e}")
|
||||
print(" Install with: pip install praisonai praisonaiagents")
|
||||
print(" Falling back to standalone mode.")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Gateway dependencies missing: {e}")
|
||||
safe_print(" Install with: pip install praisonai praisonaiagents")
|
||||
safe_print(" Falling back to standalone mode.")
|
||||
GATEWAY_OK = False
|
||||
|
||||
# ── Set dashboard style ─────────────────────────────────────
|
||||
@@ -149,15 +152,15 @@ def _register_agents_in_dashboard():
|
||||
"model": agent_def["model"],
|
||||
"icon": agent_def["icon"],
|
||||
})
|
||||
print(f" ✓ Dashboard: {agent_def['icon']} {agent_def['name']}")
|
||||
safe_print(f" {icon('✓', '[OK]')} Dashboard: {agent_def['name']}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────
|
||||
async def main():
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
if GATEWAY_OK:
|
||||
print("🚀 Starting with Gateway mode (full execution + streaming)")
|
||||
print()
|
||||
safe_print(f"{icon('🚀', '[START]')} Starting with Gateway mode (full execution + streaming)")
|
||||
safe_print()
|
||||
|
||||
# Create the gateway
|
||||
gateway = AIUIGateway(
|
||||
@@ -174,22 +177,22 @@ async def main():
|
||||
self_reflect=False,
|
||||
)
|
||||
gateway.register_agent(agent, agent_id=agent_def["agent_id"])
|
||||
print(f" ✓ Gateway: {agent_def['icon']} {agent_def['name']} ({agent_def['model']})")
|
||||
safe_print(f" {icon('✓', '[OK]')} Gateway: {agent_def['name']} ({agent_def['model']})")
|
||||
|
||||
# ── Register agents in dashboard CRUD storage ───────
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:8083")
|
||||
print(f" WebSocket: ws://localhost:8083/ws")
|
||||
print(f" Inspector: http://localhost:8083 → 🔍 Feature Inspector")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(" Dashboard: http://localhost:8083")
|
||||
safe_print(" WebSocket: ws://localhost:8083/ws")
|
||||
safe_print(f" Inspector: http://localhost:8083 {icon('→', '->')} {icon('🔍', '')} Feature Inspector")
|
||||
safe_print()
|
||||
|
||||
# Start the gateway — this runs the full server
|
||||
await gateway.start()
|
||||
else:
|
||||
# Fallback to standalone mode (same as example 15)
|
||||
print("📦 Starting in standalone mode (CRUD only, no agent execution)")
|
||||
safe_print(f"{icon('📦', '[INFO]')} Starting in standalone mode (CRUD only, no agent execution)")
|
||||
from praisonaiui.server import create_app
|
||||
import uvicorn
|
||||
|
||||
|
||||
@@ -26,15 +26,18 @@ import sys
|
||||
|
||||
import praisonaiui as aiui
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check gateway availability ──────────────────────────────
|
||||
try:
|
||||
from praisonaiui.integration import AIUIGateway
|
||||
from praisonaiagents import Agent
|
||||
GATEWAY_OK = True
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Gateway dependencies not found: {e}")
|
||||
print(" Install with: pip install praisonai praisonaiagents")
|
||||
print(" Falling back to standalone mode (REST APIs only).\n")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Gateway dependencies not found: {e}")
|
||||
safe_print(" Install with: pip install praisonai praisonaiagents")
|
||||
safe_print(" Falling back to standalone mode (REST APIs only).\n")
|
||||
GATEWAY_OK = False
|
||||
|
||||
aiui.set_style("dashboard")
|
||||
@@ -115,7 +118,7 @@ def _register_agents_in_dashboard():
|
||||
"model": agent_def["model"],
|
||||
"icon": agent_def["icon"],
|
||||
})
|
||||
print(f" ✓ Agent: {agent_def['icon']} {agent_def['name']}")
|
||||
safe_print(f" {icon('✓', '[OK]')} Agent: {agent_def['name']}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────
|
||||
@@ -125,8 +128,8 @@ async def main():
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
|
||||
if GATEWAY_OK:
|
||||
print("🚀 Starting with Gateway mode (full execution + streaming)")
|
||||
print()
|
||||
safe_print(f"{icon('🚀', '[START]')} Starting with Gateway mode (full execution + streaming)")
|
||||
safe_print()
|
||||
|
||||
gateway = AIUIGateway(host=host, port=port)
|
||||
|
||||
@@ -139,32 +142,32 @@ async def main():
|
||||
reflection=False,
|
||||
)
|
||||
gateway.register_agent(agent, agent_id=agent_def["agent_id"])
|
||||
print(f" ✓ Gateway: {agent_def['icon']} {agent_def['name']} ({agent_def['model']})")
|
||||
safe_print(f" {icon('✓', '[OK]')} Gateway: {agent_def['name']} ({agent_def['model']})")
|
||||
|
||||
# Also register in dashboard CRUD
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:{port}")
|
||||
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
|
||||
print(f" WebSocket: ws://localhost:{port}/ws")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(f" Dashboard: http://localhost:{port}")
|
||||
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
|
||||
safe_print(f" WebSocket: ws://localhost:{port}/ws")
|
||||
safe_print()
|
||||
|
||||
await gateway.start()
|
||||
else:
|
||||
# Standalone: REST APIs only, no agent execution
|
||||
print("📦 Starting in standalone mode (REST APIs only, no agent execution)")
|
||||
safe_print(f"{icon('📦', '[INFO]')} Starting in standalone mode (REST APIs only, no agent execution)")
|
||||
from praisonaiui.server import create_app
|
||||
import uvicorn
|
||||
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
app = create_app()
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:{port}")
|
||||
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
|
||||
print(f" Note: POST endpoints may fail without gateway")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(f" Dashboard: http://localhost:{port}")
|
||||
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
|
||||
safe_print(" Note: POST endpoints may fail without gateway")
|
||||
safe_print()
|
||||
|
||||
config = uvicorn.Config(app, host=host, port=port, log_level="info")
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
@@ -25,9 +25,13 @@ Usage:
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import praisonaiui as aiui
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check gateway availability ──────────────────────────────
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
@@ -35,9 +39,9 @@ try:
|
||||
from praisonaiui.integration import AIUIGateway
|
||||
GATEWAY_OK = True
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Gateway dependencies not found: {e}")
|
||||
print(" Install with: pip install praisonai praisonaiagents")
|
||||
print(" Falling back to standalone mode (REST APIs only).\n")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Gateway dependencies not found: {e}")
|
||||
safe_print(" Install with: pip install praisonai praisonaiagents")
|
||||
safe_print(" Falling back to standalone mode (REST APIs only).\n")
|
||||
GATEWAY_OK = False
|
||||
|
||||
aiui.set_style("dashboard")
|
||||
@@ -127,7 +131,7 @@ def _register_agents_in_dashboard():
|
||||
"model": agent_def["model"],
|
||||
"icon": agent_def["icon"],
|
||||
})
|
||||
print(f" ✓ Agent: {agent_def['icon']} {agent_def['name']}")
|
||||
safe_print(f" {icon('✓', '[OK]')} Agent: {agent_def['name']}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────
|
||||
@@ -137,8 +141,8 @@ async def main():
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
|
||||
if GATEWAY_OK:
|
||||
print("🚀 Starting with Gateway mode (full execution + streaming)")
|
||||
print()
|
||||
safe_print(f"{icon('🚀', '[START]')} Starting with Gateway mode (full execution + streaming)")
|
||||
safe_print()
|
||||
|
||||
gateway = AIUIGateway(host=host, port=port)
|
||||
|
||||
@@ -151,21 +155,21 @@ async def main():
|
||||
reflection=False,
|
||||
)
|
||||
gateway.register_agent(agent, agent_id=agent_def["agent_id"])
|
||||
print(f" ✓ Gateway: {agent_def['icon']} {agent_def['name']} ({agent_def['model']})")
|
||||
safe_print(f" {icon('✓', '[OK]')} Gateway: {agent_def['name']} ({agent_def['model']})")
|
||||
|
||||
# Also register in dashboard CRUD
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:{port}")
|
||||
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
|
||||
print(f" WebSocket: ws://localhost:{port}/ws")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(f" Dashboard: http://localhost:{port}")
|
||||
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
|
||||
safe_print(f" WebSocket: ws://localhost:{port}/ws")
|
||||
safe_print()
|
||||
|
||||
await gateway.start()
|
||||
else:
|
||||
# Standalone: REST APIs only, no agent execution
|
||||
print("📦 Starting in standalone mode (REST APIs only, no agent execution)")
|
||||
safe_print(f"{icon('📦', '[INFO]')} Starting in standalone mode (REST APIs only, no agent execution)")
|
||||
import uvicorn
|
||||
|
||||
from praisonaiui.server import create_app
|
||||
@@ -173,11 +177,11 @@ async def main():
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
app = create_app()
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:{port}")
|
||||
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
|
||||
print(" Note: POST endpoints may fail without gateway")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(f" Dashboard: http://localhost:{port}")
|
||||
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
|
||||
safe_print(" Note: POST endpoints may fail without gateway")
|
||||
safe_print()
|
||||
|
||||
config = uvicorn.Config(app, host=host, port=port, log_level="info")
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
@@ -32,6 +32,9 @@ import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check prerequisites ─────────────────────────────────────────────
|
||||
|
||||
try:
|
||||
@@ -40,8 +43,8 @@ try:
|
||||
SDK_OK = True
|
||||
except ImportError:
|
||||
SDK_OK = False
|
||||
print("⚠️ Missing dependencies. Install with:")
|
||||
print(" pip install praisonai[email] praisonaiagents")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Missing dependencies. Install with:")
|
||||
safe_print(" pip install praisonai[email] praisonaiagents")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -68,8 +71,8 @@ agent = Agent(
|
||||
async def run_simple():
|
||||
"""Start email bot with automatic env var resolution."""
|
||||
bot = Bot("email", agent=agent)
|
||||
print(f"📧 Starting email bot for: {os.getenv('EMAIL_ADDRESS', '(not set)')}")
|
||||
print(" Listening for new emails... (Ctrl+C to stop)")
|
||||
safe_print(f"{icon('📧', '[EMAIL]')} Starting email bot for: {os.getenv('EMAIL_ADDRESS', '(not set)')}")
|
||||
safe_print(" Listening for new emails... (Ctrl+C to stop)")
|
||||
await bot.start()
|
||||
|
||||
|
||||
@@ -96,9 +99,9 @@ async def run_with_handlers():
|
||||
@bot.on_message
|
||||
async def log_email(message):
|
||||
subject = message.metadata.get("subject", "(no subject)")
|
||||
print(f"📨 From: {message.sender.username}")
|
||||
print(f" Subject: {subject}")
|
||||
print(f" Preview: {message.text[:100]}...")
|
||||
safe_print(f"{icon('📨', '[MSG]')} From: {message.sender.username}")
|
||||
safe_print(f" Subject: {subject}")
|
||||
safe_print(f" Preview: {message.text[:100]}...")
|
||||
|
||||
# Custom command: emails with subject starting with "/status"
|
||||
@bot.on_command("status")
|
||||
@@ -118,11 +121,11 @@ async def run_with_handlers():
|
||||
reply_to=message.message_id,
|
||||
)
|
||||
|
||||
print(f"📧 Starting email bot with custom handlers")
|
||||
print(f" Email: {bot._email_address}")
|
||||
print(f" Poll interval: {config.polling_interval}s")
|
||||
print(f" Allowed senders: {'all' if not config.allowed_users else config.allowed_users}")
|
||||
print(" Listening... (Ctrl+C to stop)")
|
||||
safe_print(f"{icon('📧', '[EMAIL]')} Starting email bot with custom handlers")
|
||||
safe_print(f" Email: {bot._email_address}")
|
||||
safe_print(f" Poll interval: {config.polling_interval}s")
|
||||
safe_print(f" Allowed senders: {'all' if not config.allowed_users else config.allowed_users}")
|
||||
safe_print(" Listening... (Ctrl+C to stop)")
|
||||
await bot.start()
|
||||
|
||||
|
||||
@@ -131,17 +134,17 @@ async def run_with_handlers():
|
||||
async def run_probe():
|
||||
"""Test email server connectivity."""
|
||||
bot = Bot("email", agent=agent)
|
||||
print("🔍 Testing email server connectivity...")
|
||||
safe_print(f"{icon('🔍', '[PROBE]')} Testing email server connectivity...")
|
||||
result = await bot.probe()
|
||||
|
||||
print(f"\n Connection: {'✅ OK' if result.ok else '❌ FAILED'}")
|
||||
print(f" Platform: {result.platform}")
|
||||
safe_print(f"\n Connection: {icon('✅', '[OK]') + ' OK' if result.ok else icon('❌', '[FAIL]') + ' FAILED'}")
|
||||
safe_print(f" Platform: {result.platform}")
|
||||
if result.bot_username:
|
||||
print(f" Email: {result.bot_username}")
|
||||
safe_print(f" Email: {result.bot_username}")
|
||||
if result.elapsed_ms:
|
||||
print(f" Latency: {result.elapsed_ms:.0f}ms")
|
||||
safe_print(f" Latency: {result.elapsed_ms:.0f}ms")
|
||||
if result.error:
|
||||
print(f" Error: {result.error}")
|
||||
safe_print(f" Error: {result.error}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -42,7 +42,9 @@ import sys
|
||||
# ── Imports ─────────────────────────────────────────────────────────
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "src"))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from _shared.console import icon, safe_print
|
||||
from praisonaiui.server import create_app
|
||||
import uvicorn
|
||||
|
||||
@@ -91,11 +93,11 @@ def seed_email_channel():
|
||||
})
|
||||
if r.status_code in (200, 201):
|
||||
channel_id = r.json().get("id", "unknown")
|
||||
print(f" ✅ Email channel registered: {channel_id}")
|
||||
safe_print(f" {icon('✅', '[OK]')} Email channel registered: {channel_id}")
|
||||
else:
|
||||
print(f" ⚠️ Channel registration: {r.status_code} — {r.json()}")
|
||||
safe_print(f" {icon('⚠️', '[WARN]')} Channel registration: {r.status_code} - {r.json()}")
|
||||
else:
|
||||
print(" ✓ Email channel already exists")
|
||||
safe_print(f" {icon('✓', '[OK]')} Email channel already exists")
|
||||
|
||||
# Also seed other channels for comparison in dashboard
|
||||
for name, platform in [("Discord #general", "discord"), ("Telegram Support", "telegram")]:
|
||||
@@ -106,7 +108,7 @@ def seed_email_channel():
|
||||
"platform": platform,
|
||||
"config": {},
|
||||
})
|
||||
print(f" ✓ Seeded {platform} channel")
|
||||
safe_print(f" {icon('✓', '[OK]')} Seeded {platform} channel")
|
||||
|
||||
|
||||
# ── Verify email platform support ──────────────────────────────────
|
||||
@@ -122,8 +124,8 @@ def verify_email_support():
|
||||
if r.status_code == 200:
|
||||
platforms = r.json().get("platforms", [])
|
||||
email_supported = "email" in platforms
|
||||
print(f" Supported platforms: {platforms}")
|
||||
print(f" Email supported: {'✅ Yes' if email_supported else '❌ No — add email to SUPPORTED_PLATFORMS'}")
|
||||
safe_print(f" Supported platforms: {platforms}")
|
||||
safe_print(f" Email supported: {icon('✅', '[OK]') + ' Yes' if email_supported else icon('❌', '[X]') + ' No - add email to SUPPORTED_PLATFORMS'}")
|
||||
return email_supported
|
||||
return False
|
||||
|
||||
@@ -137,51 +139,51 @@ def test_channel_api():
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
|
||||
print("\n📋 Channel API Test")
|
||||
print("=" * 40)
|
||||
safe_print(f"\n{icon('📋', '[TEST]')} Channel API Test")
|
||||
safe_print("=" * 40)
|
||||
|
||||
# List channels
|
||||
r = client.get("/api/channels")
|
||||
channels = r.json().get("channels", [])
|
||||
print(f" GET /api/channels: {len(channels)} channels")
|
||||
safe_print(f" GET /api/channels: {len(channels)} channels")
|
||||
for ch in channels:
|
||||
icon = "📧" if ch.get("platform") == "email" else "💬"
|
||||
print(f" {icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
|
||||
ch_icon = icon("📧", "[email]") if ch.get("platform") == "email" else icon("💬", "[chat]")
|
||||
safe_print(f" {ch_icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
|
||||
|
||||
# Get email channel status
|
||||
email_ch = next((ch for ch in channels if ch.get("platform") == "email"), None)
|
||||
if email_ch:
|
||||
ch_id = email_ch.get("id")
|
||||
r = client.get(f"/api/channels/{ch_id}/status")
|
||||
print(f" GET /api/channels/{ch_id}/status: {r.json()}")
|
||||
safe_print(f" GET /api/channels/{ch_id}/status: {r.json()}")
|
||||
|
||||
print()
|
||||
safe_print()
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("📧 PraisonAIUI — Email Channel Integration")
|
||||
print("=" * 50)
|
||||
print()
|
||||
safe_print(f"{icon('📧', '[EMAIL]')} PraisonAIUI - Email Channel Integration")
|
||||
safe_print("=" * 50)
|
||||
safe_print()
|
||||
|
||||
if "--test" in sys.argv:
|
||||
# Test mode: verify APIs + seed data
|
||||
print("🔍 Verifying email platform support...")
|
||||
safe_print(f"{icon('🔍', '[VERIFY]')} Verifying email platform support...")
|
||||
verify_email_support()
|
||||
print()
|
||||
print("🌱 Seeding email channel...")
|
||||
safe_print()
|
||||
safe_print(f"{icon('🌱', '[SEED]')} Seeding email channel...")
|
||||
seed_email_channel()
|
||||
test_channel_api()
|
||||
else:
|
||||
# Run the dashboard
|
||||
print("🌱 Seeding channels...")
|
||||
safe_print(f"{icon('🌱', '[SEED]')} Seeding channels...")
|
||||
seed_email_channel()
|
||||
print()
|
||||
safe_print()
|
||||
|
||||
app = create_app()
|
||||
print(f"✅ Dashboard at http://localhost:8084")
|
||||
print(f" Channels: http://localhost:8084/api/channels")
|
||||
print(f" Platforms: http://localhost:8084/api/channels/platforms")
|
||||
safe_print(f"{icon('✅', '[OK]')} Dashboard at http://localhost:8084")
|
||||
safe_print(" Channels: http://localhost:8084/api/channels")
|
||||
safe_print(" Platforms: http://localhost:8084/api/channels/platforms")
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
uvicorn.run(app, host=host, port=8084, log_level="info")
|
||||
@@ -33,6 +33,9 @@ import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check prerequisites ─────────────────────────────────────────────
|
||||
|
||||
try:
|
||||
@@ -41,8 +44,8 @@ try:
|
||||
SDK_OK = True
|
||||
except ImportError:
|
||||
SDK_OK = False
|
||||
print("⚠️ Missing dependencies. Install with:")
|
||||
print(" pip install praisonai agentmail praisonaiagents")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Missing dependencies. Install with:")
|
||||
safe_print(" pip install praisonai agentmail praisonaiagents")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -69,9 +72,9 @@ agent = Agent(
|
||||
async def run_simple():
|
||||
"""Start agentmail bot with automatic env var resolution."""
|
||||
bot = Bot("agentmail", agent=agent)
|
||||
print(f"📩 Starting AgentMail bot")
|
||||
print(f" API key: {'✅ set' if os.getenv('AGENTMAIL_API_KEY') else '❌ not set'}")
|
||||
print(" Listening for new emails... (Ctrl+C to stop)")
|
||||
safe_print(f"{icon('📩', '[MAIL]')} Starting AgentMail bot")
|
||||
safe_print(f" API key: {icon('✅', '[OK]') + ' set' if os.getenv('AGENTMAIL_API_KEY') else icon('❌', '[X]') + ' not set'}")
|
||||
safe_print(" Listening for new emails... (Ctrl+C to stop)")
|
||||
await bot.start()
|
||||
|
||||
|
||||
@@ -99,9 +102,9 @@ async def run_with_handlers():
|
||||
@bot.on_message
|
||||
async def log_email(message):
|
||||
subject = message.metadata.get("subject", "(no subject)")
|
||||
print(f"📨 From: {message.sender.username}")
|
||||
print(f" Subject: {subject}")
|
||||
print(f" Preview: {message.text[:100]}...")
|
||||
safe_print(f"{icon('📨', '[MSG]')} From: {message.sender.username}")
|
||||
safe_print(f" Subject: {subject}")
|
||||
safe_print(f" Preview: {message.text[:100]}...")
|
||||
|
||||
# Custom command: emails with subject starting with "/status"
|
||||
@bot.on_command("status")
|
||||
@@ -121,10 +124,10 @@ async def run_with_handlers():
|
||||
reply_to=message.message_id,
|
||||
)
|
||||
|
||||
print(f"📩 Starting AgentMail bot with custom handlers")
|
||||
print(f" Email: {bot.email_address or '(will be assigned on start)'}")
|
||||
print(f" Poll interval: {config.polling_interval}s")
|
||||
print(" Listening... (Ctrl+C to stop)")
|
||||
safe_print(f"{icon('📩', '[MAIL]')} Starting AgentMail bot with custom handlers")
|
||||
safe_print(f" Email: {bot.email_address or '(will be assigned on start)'}")
|
||||
safe_print(f" Poll interval: {config.polling_interval}s")
|
||||
safe_print(" Listening... (Ctrl+C to stop)")
|
||||
await bot.start()
|
||||
|
||||
|
||||
@@ -136,30 +139,30 @@ async def run_inbox_demo():
|
||||
token=os.environ.get("AGENTMAIL_API_KEY", ""),
|
||||
)
|
||||
|
||||
print("📩 AgentMail Inbox Lifecycle Demo")
|
||||
print("=" * 50)
|
||||
safe_print(f"{icon('📩', '[MAIL]')} AgentMail Inbox Lifecycle Demo")
|
||||
safe_print("=" * 50)
|
||||
|
||||
# Create inbox
|
||||
print("\n1. Creating inbox...")
|
||||
safe_print("\n1. Creating inbox...")
|
||||
inbox = await bot.create_inbox(domain=os.getenv("AGENTMAIL_DOMAIN"))
|
||||
print(f" ✅ Created: {inbox.get('email_address', 'unknown')}")
|
||||
print(f" ID: {inbox.get('id', 'unknown')}")
|
||||
safe_print(f" {icon('✅', '[OK]')} Created: {inbox.get('email_address', 'unknown')}")
|
||||
safe_print(f" ID: {inbox.get('id', 'unknown')}")
|
||||
|
||||
# List inboxes
|
||||
print("\n2. Listing inboxes...")
|
||||
safe_print("\n2. Listing inboxes...")
|
||||
inboxes = await bot.list_inboxes()
|
||||
print(f" Found: {len(inboxes)} inbox(es)")
|
||||
safe_print(f" Found: {len(inboxes)} inbox(es)")
|
||||
for ib in inboxes:
|
||||
print(f" 📧 {ib.get('email_address', '?')} (id: {ib.get('id', '?')})")
|
||||
safe_print(f" {icon('📧', '[EMAIL]')} {ib.get('email_address', '?')} (id: {ib.get('id', '?')})")
|
||||
|
||||
# Delete inbox
|
||||
inbox_id = inbox.get("id", "")
|
||||
if inbox_id:
|
||||
print(f"\n3. Deleting inbox {inbox_id}...")
|
||||
safe_print(f"\n3. Deleting inbox {inbox_id}...")
|
||||
deleted = await bot.delete_inbox(inbox_id)
|
||||
print(f" {'✅ Deleted' if deleted else '❌ Failed to delete'}")
|
||||
safe_print(f" {icon('✅', '[OK]') + ' Deleted' if deleted else icon('❌', '[X]') + ' Failed to delete'}")
|
||||
|
||||
print("\n✅ Inbox lifecycle complete")
|
||||
safe_print(f"\n{icon('✅', '[OK]')} Inbox lifecycle complete")
|
||||
|
||||
|
||||
# ── Probe: Test connectivity ────────────────────────────────────────
|
||||
@@ -167,17 +170,17 @@ async def run_inbox_demo():
|
||||
async def run_probe():
|
||||
"""Test AgentMail API connectivity."""
|
||||
bot = Bot("agentmail", agent=agent)
|
||||
print("🔍 Testing AgentMail API connectivity...")
|
||||
safe_print(f"{icon('🔍', '[PROBE]')} Testing AgentMail API connectivity...")
|
||||
result = await bot.probe()
|
||||
|
||||
print(f"\n Connection: {'✅ OK' if result.ok else '❌ FAILED'}")
|
||||
print(f" Platform: {result.platform}")
|
||||
safe_print(f"\n Connection: {icon('✅', '[OK]') + ' OK' if result.ok else icon('❌', '[FAIL]') + ' FAILED'}")
|
||||
safe_print(f" Platform: {result.platform}")
|
||||
if result.bot_username:
|
||||
print(f" Email: {result.bot_username}")
|
||||
safe_print(f" Email: {result.bot_username}")
|
||||
if result.elapsed_ms:
|
||||
print(f" Latency: {result.elapsed_ms:.0f}ms")
|
||||
safe_print(f" Latency: {result.elapsed_ms:.0f}ms")
|
||||
if result.error:
|
||||
print(f" Error: {result.error}")
|
||||
safe_print(f" Error: {result.error}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -40,7 +40,9 @@ import sys
|
||||
# ── Imports ─────────────────────────────────────────────────────────
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "src"))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from _shared.console import icon, safe_print
|
||||
from praisonaiui.server import create_app
|
||||
import uvicorn
|
||||
|
||||
@@ -88,11 +90,11 @@ def seed_agentmail_channel():
|
||||
})
|
||||
if r.status_code in (200, 201):
|
||||
channel_id = r.json().get("id", "unknown")
|
||||
print(f" ✅ AgentMail channel registered: {channel_id}")
|
||||
safe_print(f" {icon('✅', '[OK]')} AgentMail channel registered: {channel_id}")
|
||||
else:
|
||||
print(f" ⚠️ Channel registration: {r.status_code} — {r.json()}")
|
||||
safe_print(f" {icon('⚠️', '[WARN]')} Channel registration: {r.status_code} - {r.json()}")
|
||||
else:
|
||||
print(" ✓ AgentMail channel already exists")
|
||||
safe_print(f" {icon('✓', '[OK]')} AgentMail channel already exists")
|
||||
|
||||
# Also seed email + telegram for comparison in dashboard
|
||||
for name, platform in [("Support Email", "email"), ("Telegram Support", "telegram")]:
|
||||
@@ -103,7 +105,7 @@ def seed_agentmail_channel():
|
||||
"platform": platform,
|
||||
"config": {},
|
||||
})
|
||||
print(f" ✓ Seeded {platform} channel")
|
||||
safe_print(f" {icon('✓', '[OK]')} Seeded {platform} channel")
|
||||
|
||||
|
||||
# ── Verify agentmail platform support ──────────────────────────────
|
||||
@@ -120,9 +122,9 @@ def verify_agentmail_support():
|
||||
platforms = r.json().get("platforms", [])
|
||||
am_supported = "agentmail" in platforms
|
||||
email_supported = "email" in platforms
|
||||
print(f" Supported platforms: {platforms}")
|
||||
print(f" email supported: {'✅ Yes' if email_supported else '❌ No'}")
|
||||
print(f" agentmail supported: {'✅ Yes' if am_supported else '❌ No'}")
|
||||
safe_print(f" Supported platforms: {platforms}")
|
||||
safe_print(f" email supported: {icon('✅', '[OK]') + ' Yes' if email_supported else icon('❌', '[X]') + ' No'}")
|
||||
safe_print(f" agentmail supported: {icon('✅', '[OK]') + ' Yes' if am_supported else icon('❌', '[X]') + ' No'}")
|
||||
return am_supported
|
||||
return False
|
||||
|
||||
@@ -136,51 +138,51 @@ def test_channel_api():
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
|
||||
print("\n📋 Channel API Test")
|
||||
print("=" * 40)
|
||||
safe_print(f"\n{icon('📋', '[TEST]')} Channel API Test")
|
||||
safe_print("=" * 40)
|
||||
|
||||
# List channels
|
||||
r = client.get("/api/channels")
|
||||
channels = r.json().get("channels", [])
|
||||
print(f" GET /api/channels: {len(channels)} channels")
|
||||
safe_print(f" GET /api/channels: {len(channels)} channels")
|
||||
for ch in channels:
|
||||
icon = {"email": "📧", "agentmail": "📩"}.get(ch.get("platform", ""), "💬")
|
||||
print(f" {icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
|
||||
ch_icon = {"email": icon("📧", "[email]"), "agentmail": icon("📩", "[mail]")}.get(ch.get("platform", ""), icon("💬", "[chat]"))
|
||||
safe_print(f" {ch_icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
|
||||
|
||||
# Get agentmail channel status
|
||||
am_ch = next((ch for ch in channels if ch.get("platform") == "agentmail"), None)
|
||||
if am_ch:
|
||||
ch_id = am_ch.get("id")
|
||||
r = client.get(f"/api/channels/{ch_id}/status")
|
||||
print(f" GET /api/channels/{ch_id}/status: {r.json()}")
|
||||
safe_print(f" GET /api/channels/{ch_id}/status: {r.json()}")
|
||||
|
||||
print()
|
||||
safe_print()
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("📩 PraisonAIUI — AgentMail Channel Integration")
|
||||
print("=" * 50)
|
||||
print()
|
||||
safe_print(f"{icon('📩', '[MAIL]')} PraisonAIUI - AgentMail Channel Integration")
|
||||
safe_print("=" * 50)
|
||||
safe_print()
|
||||
|
||||
if "--test" in sys.argv:
|
||||
# Test mode: verify APIs + seed data
|
||||
print("🔍 Verifying agentmail platform support...")
|
||||
safe_print(f"{icon('🔍', '[VERIFY]')} Verifying agentmail platform support...")
|
||||
verify_agentmail_support()
|
||||
print()
|
||||
print("🌱 Seeding agentmail channel...")
|
||||
safe_print()
|
||||
safe_print(f"{icon('🌱', '[SEED]')} Seeding agentmail channel...")
|
||||
seed_agentmail_channel()
|
||||
test_channel_api()
|
||||
else:
|
||||
# Run the dashboard
|
||||
print("🌱 Seeding channels...")
|
||||
safe_print(f"{icon('🌱', '[SEED]')} Seeding channels...")
|
||||
seed_agentmail_channel()
|
||||
print()
|
||||
safe_print()
|
||||
|
||||
app = create_app()
|
||||
print(f"✅ Dashboard at http://localhost:8084")
|
||||
print(f" Channels: http://localhost:8084/api/channels")
|
||||
print(f" Platforms: http://localhost:8084/api/channels/platforms")
|
||||
safe_print(f"{icon('✅', '[OK]')} Dashboard at http://localhost:8084")
|
||||
safe_print(" Channels: http://localhost:8084/api/channels")
|
||||
safe_print(" Platforms: http://localhost:8084/api/channels/platforms")
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
uvicorn.run(app, host=host, port=8084, log_level="info")
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Cross-platform console output for examples.
|
||||
|
||||
Legacy Windows consoles default stdout to a non-UTF code page (cp1252/cp437)
|
||||
that cannot encode emoji such as U+1F680. Calling ``print`` with those
|
||||
characters raises ``UnicodeEncodeError`` and crashes the example before the
|
||||
server starts. These helpers mirror the ``_supports_unicode``/``_icon`` pattern
|
||||
in ``praisonaiui.cli`` so examples degrade to ASCII instead of crashing while
|
||||
preserving emoji on UTF-aware terminals.
|
||||
|
||||
Usage:
|
||||
from _shared.console import safe_print, icon
|
||||
|
||||
safe_print(f"{icon('🚀', '[START]')} Starting gateway...")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def supports_unicode() -> bool:
|
||||
try:
|
||||
encoding = sys.stdout.encoding or ""
|
||||
except Exception:
|
||||
return False
|
||||
return "utf" in encoding.lower()
|
||||
|
||||
|
||||
def icon(symbol: str, fallback: str) -> str:
|
||||
return symbol if supports_unicode() else fallback
|
||||
|
||||
|
||||
def safe_print(*args: Any, **kwargs: Any) -> None:
|
||||
sep = kwargs.pop("sep", " ")
|
||||
text = sep.join(str(a) for a in args)
|
||||
try:
|
||||
print(text, **kwargs)
|
||||
except UnicodeEncodeError:
|
||||
print(text.encode("ascii", errors="replace").decode("ascii"), **kwargs)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "aiui"
|
||||
version = "0.3.134"
|
||||
version = "0.3.136"
|
||||
description = "YAML-driven website generator - CLI and compiler"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Version information."""
|
||||
|
||||
__version__ = "0.3.134"
|
||||
__version__ = "0.3.136"
|
||||
@@ -27,6 +27,17 @@ from ._base import BaseFeatureProtocol
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_embedding_unavailable(err: BaseException) -> bool:
|
||||
"""Detect an embedding model access / not-found failure."""
|
||||
msg = str(err).lower()
|
||||
return (
|
||||
"does not have access" in msg
|
||||
or "model_not_found" in msg
|
||||
or "does not exist" in msg
|
||||
or "notfounderror" in msg
|
||||
)
|
||||
|
||||
|
||||
# ── Knowledge Protocol ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -95,6 +106,10 @@ class KnowledgeProtocol(ABC):
|
||||
"""Health check for this backend."""
|
||||
return {"status": "ok", "provider": self.__class__.__name__}
|
||||
|
||||
def last_search_meta(self) -> Dict[str, Any]:
|
||||
"""Metadata about the most recent search (mode, error)."""
|
||||
return {"mode": "vector", "error": None}
|
||||
|
||||
|
||||
# ── Simple Knowledge Manager (Default, no deps) ─────────────────────
|
||||
|
||||
@@ -171,8 +186,12 @@ class SimpleKnowledgeManager(KnowledgeProtocol):
|
||||
"status": "ok",
|
||||
"provider": "SimpleKnowledgeManager",
|
||||
"total_entries": len(self._entries),
|
||||
"embedding": {"available": False, "reason": "no_vector_backend"},
|
||||
}
|
||||
|
||||
def last_search_meta(self) -> Dict[str, Any]:
|
||||
return {"mode": "local", "error": None}
|
||||
|
||||
|
||||
# ── SDK Knowledge Manager (wraps praisonaiagents.knowledge) ──────────
|
||||
|
||||
@@ -191,6 +210,9 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
self._sdk_knowledge = None
|
||||
self._sdk_probed = False # True once we've verified SDK actually works
|
||||
self._local_index: Dict[str, Dict[str, Any]] = {}
|
||||
self._embedding_unavailable = False
|
||||
self._embedding_error: Optional[str] = None
|
||||
self._last_search_meta: Dict[str, Any] = {"mode": "vector", "error": None}
|
||||
|
||||
def _get_sdk_knowledge(self) -> Any:
|
||||
"""Lazy-init SDK Knowledge with health probe.
|
||||
@@ -242,6 +264,10 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
"using local in-memory index"
|
||||
)
|
||||
self._sdk_knowledge = None
|
||||
elif _is_embedding_unavailable(e):
|
||||
self._embedding_unavailable = True
|
||||
self._embedding_error = err_msg
|
||||
logger.warning("Embedding model unavailable: %s", e)
|
||||
else:
|
||||
# Other errors (e.g. empty DB) are fine — SDK works
|
||||
pass
|
||||
@@ -294,6 +320,7 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
try:
|
||||
results = sdk.search(query, limit=limit, user_id="praisonaiui")
|
||||
if isinstance(results, list) and results:
|
||||
self._last_search_meta = {"mode": "vector", "error": None}
|
||||
return [
|
||||
{
|
||||
"id": str(r.get("id", i)),
|
||||
@@ -305,6 +332,9 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
]
|
||||
except BaseException as e:
|
||||
logger.warning("SDK knowledge search failed: %s; falling back to local", e)
|
||||
if _is_embedding_unavailable(e):
|
||||
self._embedding_unavailable = True
|
||||
self._embedding_error = str(e)
|
||||
|
||||
# Fallback to local text search
|
||||
query_lower = query.lower()
|
||||
@@ -314,8 +344,26 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
results.append(e)
|
||||
if len(results) >= limit:
|
||||
break
|
||||
|
||||
if self._embedding_unavailable:
|
||||
self._last_search_meta = {
|
||||
"mode": "fallback",
|
||||
"error": (
|
||||
"Vector search unavailable: "
|
||||
f"{self._embedding_error or 'embedding model not accessible'}. "
|
||||
"Set AIUI_EMBEDDING_MODEL=text-embedding-ada-002 or enable embedding "
|
||||
"models in your provider project."
|
||||
),
|
||||
}
|
||||
elif sdk is not None:
|
||||
self._last_search_meta = {"mode": "vector", "error": None}
|
||||
else:
|
||||
self._last_search_meta = {"mode": "local", "error": None}
|
||||
return results
|
||||
|
||||
def last_search_meta(self) -> Dict[str, Any]:
|
||||
return dict(self._last_search_meta)
|
||||
|
||||
def add_file(
|
||||
self,
|
||||
file_path: str,
|
||||
@@ -453,11 +501,25 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
|
||||
def health(self) -> Dict[str, Any]:
|
||||
sdk = self._get_sdk_knowledge()
|
||||
embedding_available = sdk is not None and not self._embedding_unavailable
|
||||
warnings: List[str] = []
|
||||
embedding: Dict[str, Any] = {"available": embedding_available}
|
||||
if self._embedding_unavailable:
|
||||
embedding["error"] = self._embedding_error
|
||||
warnings.append(
|
||||
"Embedding model unavailable; knowledge search degraded to local text "
|
||||
"matching. Set AIUI_EMBEDDING_MODEL to an accessible model."
|
||||
)
|
||||
status = "ok"
|
||||
if sdk is None or self._embedding_unavailable:
|
||||
status = "degraded"
|
||||
return {
|
||||
"status": "ok" if sdk is not None else "degraded",
|
||||
"status": status,
|
||||
"provider": "SDKKnowledgeManager",
|
||||
"sdk_available": sdk is not None,
|
||||
"total_indexed": len(self._local_index),
|
||||
"embedding": embedding,
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
|
||||
@@ -626,7 +688,15 @@ class KnowledgeFeature(BaseFeatureProtocol):
|
||||
query=query,
|
||||
limit=body.get("limit", 10),
|
||||
)
|
||||
return JSONResponse({"results": results, "count": len(results)})
|
||||
meta = mgr.last_search_meta()
|
||||
return JSONResponse(
|
||||
{
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
"search_mode": meta.get("mode", "unknown"),
|
||||
"warning": meta.get("error"),
|
||||
}
|
||||
)
|
||||
|
||||
async def _add_file(self, request: Request) -> JSONResponse:
|
||||
"""POST /api/knowledge/add-file — ingest a file."""
|
||||
@@ -675,6 +745,8 @@ class KnowledgeFeature(BaseFeatureProtocol):
|
||||
"files": stats.get("files", 0),
|
||||
"backend": h.get("provider", "unknown"),
|
||||
"status": h.get("status", "ok"),
|
||||
"embedding": h.get("embedding", {"available": True}),
|
||||
"warnings": h.get("warnings", []),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Wraps praisonai.capabilities functions as HTTP endpoints.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import asdict
|
||||
@@ -17,6 +18,35 @@ from starlette.routing import Route
|
||||
|
||||
from ._base import BaseFeatureProtocol
|
||||
|
||||
|
||||
def _default_embedding_model() -> str:
|
||||
"""Resolve the default embedding model from environment."""
|
||||
return os.environ.get(
|
||||
"AIUI_EMBEDDING_MODEL",
|
||||
os.environ.get("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small"),
|
||||
)
|
||||
|
||||
|
||||
def _embedding_fallback_models() -> list[str]:
|
||||
"""Resolve the embedding fallback chain from environment."""
|
||||
raw = os.environ.get(
|
||||
"AIUI_EMBEDDING_FALLBACK_MODELS",
|
||||
"text-embedding-ada-002,text-embedding-3-small",
|
||||
)
|
||||
return [m.strip() for m in raw.split(",") if m.strip()]
|
||||
|
||||
|
||||
def _is_model_not_found(err: BaseException) -> bool:
|
||||
"""Heuristically detect embedding model access / not-found errors."""
|
||||
msg = str(err).lower()
|
||||
return (
|
||||
"does not have access" in msg
|
||||
or "model_not_found" in msg
|
||||
or "does not exist" in msg
|
||||
or "notfounderror" in msg
|
||||
)
|
||||
|
||||
|
||||
# Lazy import capabilities
|
||||
_capabilities = None
|
||||
|
||||
@@ -280,29 +310,57 @@ class OpenAIAPIFeature(BaseFeatureProtocol):
|
||||
|
||||
body = await request.json()
|
||||
input_text = body.get("input", "")
|
||||
model = body.get("model", "text-embedding-3-small")
|
||||
model = body.get("model") or _default_embedding_model()
|
||||
|
||||
try:
|
||||
result = await caps.aembed(
|
||||
input=input_text,
|
||||
model=model,
|
||||
)
|
||||
candidates = [model]
|
||||
for fallback in _embedding_fallback_models():
|
||||
if fallback not in candidates:
|
||||
candidates.append(fallback)
|
||||
|
||||
data = _result_to_dict(result)
|
||||
last_err: BaseException | None = None
|
||||
for candidate in candidates:
|
||||
try:
|
||||
result = await caps.aembed(
|
||||
input=input_text,
|
||||
model=candidate,
|
||||
)
|
||||
data = _result_to_dict(result)
|
||||
return JSONResponse(
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"object": "embedding",
|
||||
"index": 0,
|
||||
"embedding": data.get("embedding", []),
|
||||
}
|
||||
],
|
||||
"model": candidate,
|
||||
"usage": data.get("usage", {"prompt_tokens": 0, "total_tokens": 0}),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
if not _is_model_not_found(e):
|
||||
break
|
||||
|
||||
if last_err is not None and _is_model_not_found(last_err):
|
||||
return JSONResponse(
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{"object": "embedding", "index": 0, "embedding": data.get("embedding", [])}
|
||||
],
|
||||
"model": model,
|
||||
"usage": data.get("usage", {"prompt_tokens": 0, "total_tokens": 0}),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
{"error": {"message": str(e), "type": "api_error"}}, status_code=500
|
||||
"error": {
|
||||
"message": (
|
||||
f"Embedding model unavailable: {last_err}. "
|
||||
"Set AIUI_EMBEDDING_MODEL to a model your project can access "
|
||||
"(e.g. text-embedding-ada-002)."
|
||||
),
|
||||
"type": "model_not_found",
|
||||
}
|
||||
},
|
||||
status_code=503,
|
||||
)
|
||||
return JSONResponse(
|
||||
{"error": {"message": str(last_err), "type": "api_error"}}, status_code=500
|
||||
)
|
||||
|
||||
async def _image_generate(self, request: Request) -> JSONResponse:
|
||||
"""POST /v1/images/generations — Generate images."""
|
||||
|
||||
@@ -2860,7 +2860,14 @@ def create_app(
|
||||
require_auth: bool = False,
|
||||
config_path: Optional[Path] = None,
|
||||
) -> Starlette:
|
||||
"""Create the Starlette application."""
|
||||
"""Create the Starlette application.
|
||||
|
||||
URL-token auth (``TokenQueryMiddleware``) is opt-in: it activates only
|
||||
when ``AIUI_REQUIRE_TOKEN`` is truthy, YAML ``auth.requireToken`` is set,
|
||||
or ``require_auth=True``. A bare ``GATEWAY_AUTH_TOKEN`` /
|
||||
``AIUI_URL_TOKEN`` env var (e.g. leaked into a developer shell after a
|
||||
gateway run) no longer forces 401 on standalone ``/api/*`` (#238, #251).
|
||||
"""
|
||||
from praisonaiui.auth import (
|
||||
AuthMiddleware,
|
||||
login_handler,
|
||||
@@ -2874,10 +2881,13 @@ def create_app(
|
||||
config = load_config_from_yaml(config_path)
|
||||
|
||||
# Extract auth settings from config
|
||||
_config_requires_token = False
|
||||
if config:
|
||||
auth_config = config.get("auth", {})
|
||||
if auth_config.get("requireAuth") or auth_config.get("require_auth"):
|
||||
require_auth = True
|
||||
if auth_config.get("requireToken") or auth_config.get("require_token"):
|
||||
_config_requires_token = True
|
||||
|
||||
middleware = [
|
||||
Middleware(
|
||||
@@ -2891,10 +2901,20 @@ def create_app(
|
||||
# Add optional AUTH_ENFORCE middleware (env var driven)
|
||||
middleware.append(Middleware(AuthEnforcementMiddleware))
|
||||
|
||||
# URL-token auth — opt-in via env. Enables query-param / cookie auth so
|
||||
# dashboards can be shared by URL (Jupyter model).
|
||||
# URL-token auth — opt-in. Enables query-param / cookie auth so dashboards
|
||||
# can be shared by URL (Jupyter model). The mere presence of a token env
|
||||
# var (e.g. GATEWAY_AUTH_TOKEN leaking into a developer shell after a
|
||||
# gateway run) must NOT force auth on standalone create_app() — that broke
|
||||
# ad-hoc TestClient scripts (#238, #251). Activation now requires an
|
||||
# explicit opt-in: AIUI_REQUIRE_TOKEN=1, YAML auth.requireToken, or the
|
||||
# require_auth flag.
|
||||
_tok = os.environ.get("AIUI_URL_TOKEN") or os.environ.get("GATEWAY_AUTH_TOKEN")
|
||||
if _tok:
|
||||
_token_auth_opt_in = (
|
||||
os.environ.get("AIUI_REQUIRE_TOKEN", "").lower() in ("true", "1", "yes")
|
||||
or _config_requires_token
|
||||
or require_auth
|
||||
)
|
||||
if _tok and _token_auth_opt_in:
|
||||
from praisonaiui.auth import TokenQueryMiddleware
|
||||
middleware.append(
|
||||
Middleware(TokenQueryMiddleware, expected_token=_tok)
|
||||
|
||||
@@ -49,13 +49,16 @@ templates:
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
"""
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
docs_dir = tmp_path / "docs"
|
||||
docs_dir.mkdir()
|
||||
(docs_dir / "index.md").write_text("# Welcome\n\nHello from integration test.")
|
||||
(docs_dir / "guide.md").write_text("# Guide\n\nA test guide.")
|
||||
(docs_dir / "index.md").write_text(
|
||||
"# Welcome\n\nHello from integration test.", encoding="utf-8"
|
||||
)
|
||||
(docs_dir / "guide.md").write_text("# Guide\n\nA test guide.", encoding="utf-8")
|
||||
|
||||
return tmp_path
|
||||
|
||||
@@ -116,7 +119,7 @@ class TestBuildCommand:
|
||||
|
||||
def test_build_invalid_config(self, tmp_path: Path):
|
||||
config = tmp_path / "bad.yaml"
|
||||
config.write_text("not: valid: yaml: config")
|
||||
config.write_text("not: valid: yaml: config", encoding="utf-8")
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["build", "--config", str(config), "--output", str(tmp_path / "out")],
|
||||
@@ -141,7 +144,8 @@ templates:
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
"""
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = runner.invoke(
|
||||
app,
|
||||
|
||||
@@ -488,12 +488,16 @@ class TestTelemetryAPI:
|
||||
|
||||
class TestTracingAPI:
|
||||
def test_list_traces(self, client):
|
||||
r = client.get("/api/tracing")
|
||||
r = client.get("/api/traces")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert isinstance(data, (list, dict))
|
||||
|
||||
def test_tracing_status(self, client):
|
||||
r = client.get("/api/tracing/status")
|
||||
r = client.get("/api/traces/status")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert "status" in data or "feature" in data
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Tests for the shared examples console helper.
|
||||
|
||||
These verify that examples degrade emoji to ASCII on legacy Windows code pages
|
||||
(cp1252) instead of raising ``UnicodeEncodeError`` while preserving emoji on
|
||||
UTF-aware terminals, mirroring the CLI ``_icon``/``_supports_unicode`` pattern.
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "examples", "python", "_shared"))
|
||||
from console import icon, safe_print, supports_unicode
|
||||
|
||||
|
||||
class TestSupportsUnicode:
|
||||
def test_utf8_encoding_is_supported(self):
|
||||
mock_stdout = mock.MagicMock()
|
||||
mock_stdout.encoding = "utf-8"
|
||||
with mock.patch("sys.stdout", mock_stdout):
|
||||
assert supports_unicode() is True
|
||||
|
||||
def test_cp1252_encoding_is_not_supported(self):
|
||||
mock_stdout = mock.MagicMock()
|
||||
mock_stdout.encoding = "cp1252"
|
||||
with mock.patch("sys.stdout", mock_stdout):
|
||||
assert supports_unicode() is False
|
||||
|
||||
def test_missing_encoding_falls_back_to_ascii(self):
|
||||
mock_stdout = mock.MagicMock()
|
||||
mock_stdout.encoding = None
|
||||
with mock.patch("sys.stdout", mock_stdout):
|
||||
assert supports_unicode() is False
|
||||
|
||||
|
||||
class TestIcon:
|
||||
def test_returns_symbol_on_utf8(self):
|
||||
mock_stdout = mock.MagicMock()
|
||||
mock_stdout.encoding = "utf-8"
|
||||
with mock.patch("sys.stdout", mock_stdout):
|
||||
assert icon("🚀", "[START]") == "🚀"
|
||||
|
||||
def test_returns_fallback_on_cp1252(self):
|
||||
mock_stdout = mock.MagicMock()
|
||||
mock_stdout.encoding = "cp1252"
|
||||
with mock.patch("sys.stdout", mock_stdout):
|
||||
assert icon("🚀", "[START]") == "[START]"
|
||||
|
||||
|
||||
class TestSafePrint:
|
||||
def test_never_raises_on_cp1252(self):
|
||||
buffer = io.BytesIO()
|
||||
stream = io.TextIOWrapper(buffer, encoding="cp1252", errors="strict")
|
||||
with mock.patch("sys.stdout", stream):
|
||||
safe_print("🚀 Starting")
|
||||
stream.flush()
|
||||
assert buffer.getvalue()
|
||||
|
||||
def test_preserves_unicode_on_utf8(self):
|
||||
buffer = io.StringIO()
|
||||
with mock.patch("sys.stdout", buffer):
|
||||
safe_print("🚀 Starting")
|
||||
assert "🚀" in buffer.getvalue()
|
||||
|
||||
def test_respects_sep(self):
|
||||
buffer = io.StringIO()
|
||||
with mock.patch("sys.stdout", buffer):
|
||||
safe_print("a", "b", sep="-")
|
||||
assert buffer.getvalue().strip() == "a-b"
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Unit tests for knowledge embedding fallback + degraded-mode reporting."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from praisonaiui.features import _features, register_feature
|
||||
from praisonaiui.features import knowledge as knowledge_mod
|
||||
from praisonaiui.features.knowledge import (
|
||||
KnowledgeFeature,
|
||||
SDKKnowledgeManager,
|
||||
SimpleKnowledgeManager,
|
||||
_is_embedding_unavailable,
|
||||
)
|
||||
from praisonaiui.features.openai_api import (
|
||||
OpenAIAPIFeature,
|
||||
_default_embedding_model,
|
||||
_embedding_fallback_models,
|
||||
_is_model_not_found,
|
||||
)
|
||||
from praisonaiui.server import create_app
|
||||
|
||||
|
||||
class _FakeSDK:
|
||||
def __init__(self, search_error: BaseException | None = None) -> None:
|
||||
self._search_error = search_error
|
||||
|
||||
def search(self, query, limit=1, user_id=None):
|
||||
if query == "__probe__":
|
||||
return []
|
||||
if self._search_error is not None:
|
||||
raise self._search_error
|
||||
return []
|
||||
|
||||
|
||||
def test_is_model_not_found_detects_403():
|
||||
assert _is_model_not_found(Exception("Project does not have access to model x"))
|
||||
assert _is_model_not_found(Exception("litellm.NotFoundError: nope"))
|
||||
assert not _is_model_not_found(Exception("timeout"))
|
||||
|
||||
|
||||
def test_is_embedding_unavailable_detects_403():
|
||||
assert _is_embedding_unavailable(Exception("model_not_found"))
|
||||
assert not _is_embedding_unavailable(Exception("connection reset"))
|
||||
|
||||
|
||||
def test_default_embedding_model_env(monkeypatch):
|
||||
monkeypatch.delenv("AIUI_EMBEDDING_MODEL", raising=False)
|
||||
monkeypatch.delenv("OPENAI_EMBEDDING_MODEL", raising=False)
|
||||
assert _default_embedding_model() == "text-embedding-3-small"
|
||||
monkeypatch.setenv("AIUI_EMBEDDING_MODEL", "text-embedding-ada-002")
|
||||
assert _default_embedding_model() == "text-embedding-ada-002"
|
||||
|
||||
|
||||
def test_fallback_models_env(monkeypatch):
|
||||
monkeypatch.setenv("AIUI_EMBEDDING_FALLBACK_MODELS", "a, b ,,c")
|
||||
assert _embedding_fallback_models() == ["a", "b", "c"]
|
||||
|
||||
|
||||
def test_simple_manager_reports_local_mode():
|
||||
mgr = SimpleKnowledgeManager()
|
||||
mgr.store("hello world")
|
||||
mgr.search("hello")
|
||||
assert mgr.last_search_meta()["mode"] == "local"
|
||||
assert mgr.health()["embedding"]["available"] is False
|
||||
|
||||
|
||||
def test_sdk_search_reports_fallback_on_embedding_error():
|
||||
mgr = SDKKnowledgeManager()
|
||||
err = Exception("Project does not have access to model text-embedding-3-small")
|
||||
mgr._sdk_knowledge = _FakeSDK(search_error=err)
|
||||
mgr._sdk_probed = True
|
||||
|
||||
mgr.store("PraisonAIUI is a YAML-driven UI generator.")
|
||||
mgr.search("documentation generator")
|
||||
|
||||
meta = mgr.last_search_meta()
|
||||
assert meta["mode"] == "fallback"
|
||||
assert "text-embedding" in meta["error"].lower()
|
||||
|
||||
health = mgr.health()
|
||||
assert health["embedding"]["available"] is False
|
||||
assert health["status"] == "degraded"
|
||||
assert health["warnings"]
|
||||
|
||||
|
||||
def test_sdk_search_vector_mode_when_healthy():
|
||||
class _OkSDK:
|
||||
def search(self, query, limit=1, user_id=None):
|
||||
if query == "__probe__":
|
||||
return []
|
||||
return [{"id": "1", "memory": "hit", "score": 0.9}]
|
||||
|
||||
mgr = SDKKnowledgeManager()
|
||||
mgr._sdk_knowledge = _OkSDK()
|
||||
mgr._sdk_probed = True
|
||||
results = mgr.search("anything")
|
||||
assert results
|
||||
assert mgr.last_search_meta()["mode"] == "vector"
|
||||
|
||||
|
||||
def test_probe_flags_embedding_unavailable():
|
||||
mgr = SDKKnowledgeManager()
|
||||
|
||||
class _Kn:
|
||||
def search(self, query, limit=1):
|
||||
raise Exception("model_not_found: no access")
|
||||
|
||||
with patch("praisonaiagents.knowledge.Knowledge", return_value=_Kn(), create=True):
|
||||
mgr._get_sdk_knowledge()
|
||||
assert mgr._embedding_unavailable is True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
saved = dict(_features)
|
||||
saved_mgr = knowledge_mod._knowledge_manager
|
||||
_features.clear()
|
||||
register_feature(KnowledgeFeature())
|
||||
register_feature(OpenAIAPIFeature())
|
||||
yield TestClient(create_app())
|
||||
_features.clear()
|
||||
_features.update(saved)
|
||||
knowledge_mod._knowledge_manager = saved_mgr
|
||||
|
||||
|
||||
def test_knowledge_search_reports_fallback(client):
|
||||
mgr = SDKKnowledgeManager()
|
||||
err = Exception("Project does not have access to model text-embedding-3-small")
|
||||
mgr._sdk_knowledge = _FakeSDK(search_error=err)
|
||||
mgr._sdk_probed = True
|
||||
mgr.store("PraisonAIUI is a YAML-driven documentation site generator.")
|
||||
knowledge_mod._knowledge_manager = mgr
|
||||
|
||||
r = client.post("/api/knowledge/search", json={"query": "documentation generator YAML"})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["search_mode"] == "fallback"
|
||||
assert body["warning"]
|
||||
assert "embedding" in body["warning"].lower()
|
||||
|
||||
|
||||
def test_knowledge_status_reports_embedding(client):
|
||||
mgr = SDKKnowledgeManager()
|
||||
mgr._sdk_knowledge = _FakeSDK()
|
||||
mgr._sdk_probed = True
|
||||
mgr._embedding_unavailable = True
|
||||
mgr._embedding_error = "no access"
|
||||
knowledge_mod._knowledge_manager = mgr
|
||||
|
||||
r = client.get("/api/knowledge/status")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["embedding"]["available"] is False
|
||||
assert body["warnings"]
|
||||
|
||||
|
||||
def test_embeddings_endpoint_fallback_models(client):
|
||||
call_models: list[str] = []
|
||||
|
||||
async def _aembed(input, model):
|
||||
call_models.append(model)
|
||||
if model == "text-embedding-3-small":
|
||||
raise Exception("Project does not have access to model text-embedding-3-small")
|
||||
return {"embedding": [0.1, 0.2], "usage": {"prompt_tokens": 1, "total_tokens": 1}}
|
||||
|
||||
fake_caps = type("Caps", (), {"aembed": staticmethod(AsyncMock(side_effect=_aembed))})
|
||||
with patch(
|
||||
"praisonaiui.features.openai_api._get_capabilities", return_value=fake_caps
|
||||
):
|
||||
r = client.post(
|
||||
"/v1/embeddings",
|
||||
json={"input": "test", "model": "text-embedding-3-small"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["model"] == "text-embedding-ada-002"
|
||||
assert call_models[0] == "text-embedding-3-small"
|
||||
|
||||
|
||||
def test_embeddings_endpoint_all_fail_returns_503(client):
|
||||
async def _aembed(input, model):
|
||||
raise Exception("model_not_found: no access to any embedding model")
|
||||
|
||||
fake_caps = type("Caps", (), {"aembed": staticmethod(AsyncMock(side_effect=_aembed))})
|
||||
with patch(
|
||||
"praisonaiui.features.openai_api._get_capabilities", return_value=fake_caps
|
||||
):
|
||||
r = client.post("/v1/embeddings", json={"input": "test"})
|
||||
assert r.status_code == 503
|
||||
assert r.json()["error"]["type"] == "model_not_found"
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Regression tests for opt-in token auth activation (#238, #251).
|
||||
|
||||
The mere presence of GATEWAY_AUTH_TOKEN / AIUI_URL_TOKEN in the environment
|
||||
(e.g. leaking into a developer shell after a gateway run) must not force 401
|
||||
on standalone create_app() + TestClient. Token auth activates only when
|
||||
explicitly opted in via AIUI_REQUIRE_TOKEN, YAML auth.requireToken, or
|
||||
require_auth=True.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from praisonaiui import server
|
||||
from praisonaiui.server import create_app
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_state():
|
||||
server.reset_state()
|
||||
yield
|
||||
server.reset_state()
|
||||
|
||||
|
||||
def test_env_token_alone_does_not_force_401(monkeypatch):
|
||||
monkeypatch.setenv("GATEWAY_AUTH_TOKEN", "test-secret-token")
|
||||
monkeypatch.delenv("AIUI_REQUIRE_TOKEN", raising=False)
|
||||
client = TestClient(create_app())
|
||||
assert client.get("/api/agents").status_code == 200
|
||||
|
||||
|
||||
def test_aiui_url_token_alone_does_not_force_401(monkeypatch):
|
||||
monkeypatch.setenv("AIUI_URL_TOKEN", "test-secret-token")
|
||||
monkeypatch.delenv("AIUI_REQUIRE_TOKEN", raising=False)
|
||||
client = TestClient(create_app())
|
||||
assert client.get("/api/agents").status_code == 200
|
||||
|
||||
|
||||
def test_require_token_env_opt_in_enforces_401(monkeypatch):
|
||||
monkeypatch.setenv("GATEWAY_AUTH_TOKEN", "test-secret-token")
|
||||
monkeypatch.setenv("AIUI_REQUIRE_TOKEN", "1")
|
||||
client = TestClient(create_app())
|
||||
assert client.get("/api/agents").status_code == 401
|
||||
|
||||
|
||||
def test_require_token_env_opt_in_allows_bearer(monkeypatch):
|
||||
monkeypatch.setenv("GATEWAY_AUTH_TOKEN", "test-secret-token")
|
||||
monkeypatch.setenv("AIUI_REQUIRE_TOKEN", "1")
|
||||
client = TestClient(create_app())
|
||||
resp = client.get(
|
||||
"/api/agents",
|
||||
headers={"Authorization": "Bearer test-secret-token"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_config_require_token_opt_in_enforces_401(monkeypatch):
|
||||
monkeypatch.setenv("AIUI_URL_TOKEN", "test-secret-token")
|
||||
monkeypatch.delenv("AIUI_REQUIRE_TOKEN", raising=False)
|
||||
client = TestClient(create_app(config={"auth": {"requireToken": True}}))
|
||||
assert client.get("/api/agents").status_code == 401
|
||||
|
||||
|
||||
def test_require_auth_opt_in_enforces_401(monkeypatch):
|
||||
monkeypatch.setenv("AIUI_URL_TOKEN", "test-secret-token")
|
||||
monkeypatch.delenv("AIUI_REQUIRE_TOKEN", raising=False)
|
||||
client = TestClient(create_app(require_auth=True))
|
||||
assert client.get("/api/agents").status_code == 401
|
||||
Reference in new issue
Block a user