Phase 3.9: Developer Tools Enhancement - Webhooks, SDK Center, API Docs, Usage Dashboard
This commit is contained in:
1 parent
065d2f05f6
commit
49dcea0a11
6 files changed
+1460
No files matched your search
@@ -0,0 +1,899 @@
|
||||
"""
|
||||
Developer Tools Enhancement - Webhooks, SDK Center, API Docs, Usage Dashboard.
|
||||
Phase 3.9: Extends the developer module with webhook management,
|
||||
SDK download center, interactive API documentation, and enhanced analytics.
|
||||
"""
|
||||
import json
|
||||
import secrets
|
||||
import hashlib
|
||||
import hmac
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...auth import get_current_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
CST_TZ = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
def cst_now():
|
||||
return datetime.now(CST_TZ)
|
||||
|
||||
|
||||
def fmt_time(dt):
|
||||
if dt is None:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=CST_TZ).isoformat()
|
||||
return dt.isoformat()
|
||||
|
||||
|
||||
def _execute_raw(db, sql, params=None):
|
||||
"""Execute raw SQL and return list of dicts."""
|
||||
result = db.execute(text(sql), params or {})
|
||||
rows = []
|
||||
for row in result:
|
||||
rows.append(dict(row._mapping))
|
||||
return rows
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Pydantic Models
|
||||
# ============================================================
|
||||
|
||||
class WebhookCreate(BaseModel):
|
||||
name: str
|
||||
url: str
|
||||
events: list = []
|
||||
secret: str = ""
|
||||
headers: dict = {}
|
||||
|
||||
|
||||
class WebhookUpdate(BaseModel):
|
||||
name: str = None
|
||||
url: str = None
|
||||
events: list = None
|
||||
secret: str = None
|
||||
headers: dict = None
|
||||
status: str = None
|
||||
|
||||
|
||||
class WebhookTest(BaseModel):
|
||||
event: str = "test"
|
||||
payload: dict = {"message": "Test webhook delivery"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Webhook Management
|
||||
# ============================================================
|
||||
|
||||
SUPPORTED_EVENTS = [
|
||||
{"value": "app.created", "label": "App Created", "desc": "Triggered when a new AI app is created"},
|
||||
{"value": "app.published", "label": "App Published", "desc": "Triggered when an app is published to market"},
|
||||
{"value": "agent.created", "label": "Agent Created", "desc": "Triggered when a new agent is created"},
|
||||
{"value": "knowledge.updated", "label": "Knowledge Updated", "desc": "Triggered when knowledge base is updated"},
|
||||
{"value": "api_key.created", "label": "API Key Created", "desc": "Triggered when a new API key is generated"},
|
||||
{"value": "api_key.revoked", "label": "API Key Revoked", "desc": "Triggered when an API key is revoked"},
|
||||
{"value": "user.registered", "label": "User Registered", "desc": "Triggered when a new user registers"},
|
||||
{"value": "model.connected", "label": "Model Connected", "desc": "Triggered when a model connection test passes"},
|
||||
{"value": "workflow.completed", "label": "Workflow Completed", "desc": "Triggered when a workflow execution finishes"},
|
||||
{"value": "storage.threshold", "label": "Storage Threshold", "desc": "Triggered when storage usage exceeds threshold"},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/webhooks", summary="List webhooks")
|
||||
async def list_webhooks(
|
||||
status: str = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List all webhook configurations."""
|
||||
where_clause = "WHERE 1=1"
|
||||
params = {}
|
||||
if status:
|
||||
where_clause += " AND status = :status"
|
||||
params["status"] = status
|
||||
|
||||
count_sql = f"SELECT COUNT(*) as cnt FROM webhook_configs {where_clause}"
|
||||
count_rows = _execute_raw(db, count_sql, params)
|
||||
total = count_rows[0]["cnt"] if count_rows else 0
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
list_sql = f"""SELECT id, name, url, events, secret, headers, status,
|
||||
last_triggered_at, last_status, total_deliveries, success_count, fail_count,
|
||||
created_at, updated_at
|
||||
FROM webhook_configs {where_clause}
|
||||
ORDER BY created_at DESC LIMIT :limit OFFSET :offset"""
|
||||
list_params = dict(params)
|
||||
list_params["limit"] = page_size
|
||||
list_params["offset"] = offset
|
||||
rows = _execute_raw(db, list_sql, list_params)
|
||||
|
||||
items = []
|
||||
for r in rows:
|
||||
events_raw = r.get("events")
|
||||
if isinstance(events_raw, str):
|
||||
try:
|
||||
events_val = json.loads(events_raw)
|
||||
except Exception:
|
||||
events_val = []
|
||||
else:
|
||||
events_val = events_raw or []
|
||||
|
||||
headers_raw = r.get("headers")
|
||||
if isinstance(headers_raw, str):
|
||||
try:
|
||||
headers_val = json.loads(headers_raw)
|
||||
except Exception:
|
||||
headers_val = {}
|
||||
else:
|
||||
headers_val = headers_raw or {}
|
||||
|
||||
success_rate = 0
|
||||
if r.get("total_deliveries", 0) > 0:
|
||||
success_rate = round(r["success_count"] / r["total_deliveries"] * 100, 1)
|
||||
|
||||
items.append({
|
||||
"id": r["id"],
|
||||
"name": r["name"],
|
||||
"url": r["url"],
|
||||
"events": events_val,
|
||||
"has_secret": bool(r.get("secret")),
|
||||
"headers": headers_val,
|
||||
"status": r["status"],
|
||||
"last_triggered_at": fmt_time(r.get("last_triggered_at")),
|
||||
"last_status": r.get("last_status"),
|
||||
"total_deliveries": r.get("total_deliveries", 0),
|
||||
"success_count": r.get("success_count", 0),
|
||||
"fail_count": r.get("fail_count", 0),
|
||||
"success_rate": success_rate,
|
||||
"created_at": fmt_time(r.get("created_at")),
|
||||
"updated_at": fmt_time(r.get("updated_at")),
|
||||
})
|
||||
|
||||
return {"code": 0, "data": {"total": total, "page": page, "page_size": page_size, "items": items}}
|
||||
|
||||
|
||||
@router.get("/webhooks/events", summary="List supported webhook events")
|
||||
async def list_webhook_events(
|
||||
admin_user=Depends(get_current_admin),
|
||||
):
|
||||
"""List all supported webhook event types."""
|
||||
return {"code": 0, "data": {"events": SUPPORTED_EVENTS}}
|
||||
|
||||
|
||||
@router.post("/webhooks/", summary="Create webhook")
|
||||
async def create_webhook(
|
||||
body: WebhookCreate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Create a new webhook configuration."""
|
||||
webhook_id = "wh_" + secrets.token_hex(12)
|
||||
now = cst_now()
|
||||
|
||||
secret_val = body.secret if body.secret else "whsec_" + secrets.token_hex(16)
|
||||
|
||||
insert_sql = """INSERT INTO webhook_configs
|
||||
(id, name, url, events, secret, headers, status, last_triggered_at,
|
||||
last_status, total_deliveries, success_count, fail_count, created_at, updated_at)
|
||||
VALUES (:id, :name, :url, :events, :secret, :headers, 'active',
|
||||
NULL, NULL, 0, 0, 0, :now, :now)"""
|
||||
params = {
|
||||
"id": webhook_id,
|
||||
"name": body.name,
|
||||
"url": body.url,
|
||||
"events": json.dumps(body.events),
|
||||
"secret": secret_val,
|
||||
"headers": json.dumps(body.headers),
|
||||
"now": now,
|
||||
}
|
||||
db.execute(text(insert_sql), params)
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "data": {"id": webhook_id, "name": body.name, "secret": secret_val, "message": "Webhook created"}}
|
||||
|
||||
|
||||
@router.put("/webhooks/{webhook_id}", summary="Update webhook")
|
||||
async def update_webhook(
|
||||
webhook_id: str,
|
||||
body: WebhookUpdate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update an existing webhook configuration."""
|
||||
check_sql = "SELECT id FROM webhook_configs WHERE id = :id"
|
||||
rows = _execute_raw(db, check_sql, {"id": webhook_id})
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail="Webhook not found")
|
||||
|
||||
updates = []
|
||||
params = {"id": webhook_id, "now": cst_now()}
|
||||
if body.name is not None:
|
||||
updates.append("name = :name")
|
||||
params["name"] = body.name
|
||||
if body.url is not None:
|
||||
updates.append("url = :url")
|
||||
params["url"] = body.url
|
||||
if body.events is not None:
|
||||
updates.append("events = :events")
|
||||
params["events"] = json.dumps(body.events)
|
||||
if body.secret is not None:
|
||||
updates.append("secret = :secret")
|
||||
params["secret"] = body.secret
|
||||
if body.headers is not None:
|
||||
updates.append("headers = :headers")
|
||||
params["headers"] = json.dumps(body.headers)
|
||||
if body.status is not None:
|
||||
updates.append("status = :status")
|
||||
params["status"] = body.status
|
||||
|
||||
if not updates:
|
||||
return {"code": 0, "message": "No changes"}
|
||||
|
||||
updates.append("updated_at = :now")
|
||||
update_sql = f"UPDATE webhook_configs SET {', '.join(updates)} WHERE id = :id"
|
||||
db.execute(text(update_sql), params)
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "Webhook updated"}
|
||||
|
||||
|
||||
@router.delete("/webhooks/{webhook_id}", summary="Delete webhook")
|
||||
async def delete_webhook(
|
||||
webhook_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Delete a webhook configuration."""
|
||||
check_sql = "SELECT id FROM webhook_configs WHERE id = :id"
|
||||
rows = _execute_raw(db, check_sql, {"id": webhook_id})
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail="Webhook not found")
|
||||
|
||||
db.execute(text("DELETE FROM webhook_configs WHERE id = :id"), {"id": webhook_id})
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "Webhook deleted"}
|
||||
|
||||
|
||||
@router.post("/webhooks/{webhook_id}/test", summary="Test webhook delivery")
|
||||
async def test_webhook(
|
||||
webhook_id: str,
|
||||
body: WebhookTest = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Send a test payload to the webhook endpoint."""
|
||||
select_sql = "SELECT * FROM webhook_configs WHERE id = :id"
|
||||
rows = _execute_raw(db, select_sql, {"id": webhook_id})
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail="Webhook not found")
|
||||
|
||||
wh = rows[0]
|
||||
url = wh["url"]
|
||||
secret = wh.get("secret", "")
|
||||
|
||||
test_event = body.event if body else "test"
|
||||
test_payload = body.payload if body else {"message": "Test webhook delivery"}
|
||||
full_payload = {
|
||||
"event": test_event,
|
||||
"timestamp": cst_now().isoformat(),
|
||||
"webhook_id": webhook_id,
|
||||
"data": test_payload,
|
||||
}
|
||||
payload_bytes = json.dumps(full_payload).encode("utf-8")
|
||||
|
||||
# Build headers
|
||||
headers = {"Content-Type": "application/json"}
|
||||
headers_raw = wh.get("headers")
|
||||
if isinstance(headers_raw, str):
|
||||
try:
|
||||
headers.update(json.loads(headers_raw))
|
||||
except Exception:
|
||||
pass
|
||||
elif isinstance(headers_raw, dict):
|
||||
headers.update(headers_raw)
|
||||
|
||||
# HMAC signature
|
||||
if secret:
|
||||
signature = hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest()
|
||||
headers["X-Webhook-Signature"] = signature
|
||||
|
||||
# Send request
|
||||
delivery_id = "dlv_" + secrets.token_hex(12)
|
||||
now = cst_now()
|
||||
response_code = None
|
||||
response_body = None
|
||||
response_time_ms = None
|
||||
status = "failed"
|
||||
error_message = None
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, data=payload_bytes, headers=headers, method="POST")
|
||||
start_time = datetime.now()
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
end_time = datetime.now()
|
||||
response_code = resp.getcode()
|
||||
response_body = resp.read().decode("utf-8", errors="replace")[:2000]
|
||||
response_time_ms = int((end_time - start_time).total_seconds() * 1000)
|
||||
if 200 <= response_code < 300:
|
||||
status = "success"
|
||||
except urllib.error.HTTPError as e:
|
||||
response_code = e.code
|
||||
try:
|
||||
response_body = e.read().decode("utf-8", errors="replace")[:2000]
|
||||
except Exception:
|
||||
response_body = str(e)
|
||||
error_message = f"HTTP {e.code}: {str(e)[:200]}"
|
||||
except Exception as e:
|
||||
error_message = str(e)[:500]
|
||||
|
||||
# Record delivery
|
||||
insert_sql = """INSERT INTO webhook_deliveries
|
||||
(id, webhook_id, event, payload, status, response_code, response_body,
|
||||
response_time_ms, attempts, error_message, created_at)
|
||||
VALUES (:id, :wid, :event, :payload, :status, :rcode, :rbody,
|
||||
:rtime, 1, :err, :now)"""
|
||||
db.execute(text(insert_sql), {
|
||||
"id": delivery_id, "wid": webhook_id, "event": test_event,
|
||||
"payload": json.dumps(full_payload), "status": status,
|
||||
"rcode": response_code, "rbody": response_body,
|
||||
"rtime": response_time_ms, "err": error_message, "now": now,
|
||||
})
|
||||
|
||||
# Update webhook stats
|
||||
total_deliveries = (wh.get("total_deliveries") or 0) + 1
|
||||
success_count = (wh.get("success_count") or 0) + (1 if status == "success" else 0)
|
||||
fail_count = (wh.get("fail_count") or 0) + (1 if status != "success" else 0)
|
||||
db.execute(text("""UPDATE webhook_configs SET
|
||||
total_deliveries = :td, success_count = :sc, fail_count = :fc,
|
||||
last_triggered_at = :now, last_status = :ls, updated_at = :now
|
||||
WHERE id = :id"""), {
|
||||
"td": total_deliveries, "sc": success_count, "fc": fail_count,
|
||||
"now": now, "ls": status, "id": webhook_id,
|
||||
})
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"delivery_id": delivery_id,
|
||||
"status": status,
|
||||
"response_code": response_code,
|
||||
"response_time_ms": response_time_ms,
|
||||
"response_body": response_body[:500] if response_body else None,
|
||||
"error_message": error_message,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/webhooks/{webhook_id}/deliveries", summary="Webhook delivery history")
|
||||
async def webhook_deliveries(
|
||||
webhook_id: str,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get delivery history for a specific webhook."""
|
||||
count_sql = "SELECT COUNT(*) as cnt FROM webhook_deliveries WHERE webhook_id = :wid"
|
||||
count_rows = _execute_raw(db, count_sql, {"wid": webhook_id})
|
||||
total = count_rows[0]["cnt"] if count_rows else 0
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
list_sql = """SELECT id, webhook_id, event, payload, status, response_code,
|
||||
response_body, response_time_ms, attempts, error_message, created_at
|
||||
FROM webhook_deliveries WHERE webhook_id = :wid
|
||||
ORDER BY created_at DESC LIMIT :limit OFFSET :offset"""
|
||||
rows = _execute_raw(db, list_sql, {"wid": webhook_id, "limit": page_size, "offset": offset})
|
||||
|
||||
items = []
|
||||
for r in rows:
|
||||
payload_raw = r.get("payload")
|
||||
if isinstance(payload_raw, str):
|
||||
try:
|
||||
payload_val = json.loads(payload_raw)
|
||||
except Exception:
|
||||
payload_val = payload_raw
|
||||
else:
|
||||
payload_val = payload_raw
|
||||
|
||||
items.append({
|
||||
"id": r["id"],
|
||||
"event": r["event"],
|
||||
"status": r["status"],
|
||||
"response_code": r.get("response_code"),
|
||||
"response_time_ms": r.get("response_time_ms"),
|
||||
"attempts": r.get("attempts", 0),
|
||||
"error_message": r.get("error_message"),
|
||||
"payload": payload_val,
|
||||
"response_body": (r.get("response_body") or "")[:500],
|
||||
"created_at": fmt_time(r.get("created_at")),
|
||||
})
|
||||
|
||||
return {"code": 0, "data": {"total": total, "page": page, "page_size": page_size, "items": items}}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SDK Download Center
|
||||
# ============================================================
|
||||
|
||||
SDK_PACKAGES = [
|
||||
{
|
||||
"language": "python",
|
||||
"name": "Python SDK",
|
||||
"version": "1.2.0",
|
||||
"description": "Official Python SDK for Qihu-AIFlow platform. Supports chat completions, knowledge base, agent execution, and file upload.",
|
||||
"install_cmd": "pip install qihu-aiflow",
|
||||
"download_url": "https://pypi.org/project/qihu-aiflow/",
|
||||
"github_url": "https://github.com/qihuyun/qihu-aiflow-sdk-python",
|
||||
"min_version": "Python 3.8+",
|
||||
"features": ["Chat completions (streaming)", "Knowledge base queries", "Agent execution", "File upload", "Webhook verification", "Async support"],
|
||||
"code_example": '''from qihu_aiflow import QihuClient
|
||||
|
||||
client = QihuClient(api_key="sk-your-api-key", base_url="http://210.16.161.218:18080")
|
||||
|
||||
# Chat completion
|
||||
response = client.chat.create(
|
||||
model="deepseek-ai/DeepSeek-V4-Flash",
|
||||
messages=[{"role": "user", "content": "Hello!"}]
|
||||
)
|
||||
print(response.choices[0].message.content)
|
||||
|
||||
# Streaming
|
||||
for chunk in client.chat.create(
|
||||
model="deepseek-ai/DeepSeek-V4-Flash",
|
||||
messages=[{"role": "user", "content": "Tell me a story"}],
|
||||
stream=True
|
||||
):
|
||||
print(chunk.choices[0].delta.content, end="")''',
|
||||
},
|
||||
{
|
||||
"language": "javascript",
|
||||
"name": "JavaScript SDK",
|
||||
"version": "1.1.0",
|
||||
"description": "Official JavaScript/TypeScript SDK for browser and Node.js. Full TypeScript support with type definitions.",
|
||||
"install_cmd": "npm install @qihu/aiflow",
|
||||
"download_url": "https://www.npmjs.com/package/@qihu/aiflow",
|
||||
"github_url": "https://github.com/qihuyun/qihu-aiflow-sdk-js",
|
||||
"min_version": "Node.js 16+ / Browser",
|
||||
"features": ["Chat completions (streaming)", "Knowledge base queries", "Agent execution", "File upload", "WebSocket support", "TypeScript types"],
|
||||
"code_example": '''import { QihuClient } from "@qihu/aiflow";
|
||||
|
||||
const client = new QihuClient({
|
||||
apiKey: "sk-your-api-key",
|
||||
baseUrl: "http://210.16.161.218:18080",
|
||||
});
|
||||
|
||||
// Chat completion
|
||||
const response = await client.chat.create({
|
||||
model: "deepseek-ai/DeepSeek-V4-Flash",
|
||||
messages: [{ role: "user", content: "Hello!" }],
|
||||
});
|
||||
console.log(response.choices[0].message.content);
|
||||
|
||||
// Streaming
|
||||
const stream = await client.chat.create({
|
||||
model: "deepseek-ai/DeepSeek-V4-Flash",
|
||||
messages: [{ role: "user", content: "Tell me a story" }],
|
||||
stream: true,
|
||||
});
|
||||
for await (const chunk of stream) {
|
||||
process.stdout.write(chunk.choices[0].delta.content || "");
|
||||
}''',
|
||||
},
|
||||
{
|
||||
"language": "go",
|
||||
"name": "Go SDK",
|
||||
"version": "1.0.0",
|
||||
"description": "Official Go SDK for Qihu-AIFlow. Lightweight and fast, suitable for microservices and CLI tools.",
|
||||
"install_cmd": "go get github.com/qihuyun/qihu-aiflow-sdk-go",
|
||||
"download_url": "https://github.com/qihuyun/qihu-aiflow-sdk-go/releases",
|
||||
"github_url": "https://github.com/qihuyun/qihu-aiflow-sdk-go",
|
||||
"min_version": "Go 1.19+",
|
||||
"features": ["Chat completions", "Knowledge base queries", "Agent execution", "Context cancellation", "Retry logic", "Connection pooling"],
|
||||
"code_example": '''package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/qihuyun/qihu-aiflow-sdk-go/qihu"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := qihu.NewClient("sk-your-api-key", "http://210.16.161.218:18080")
|
||||
|
||||
resp, err := client.Chat.Create(context.Background(), &qihu.ChatRequest{
|
||||
Model: "deepseek-ai/DeepSeek-V4-Flash",
|
||||
Messages: []qihu.Message{
|
||||
{Role: "user", Content: "Hello!"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(resp.Choices[0].Message.Content)
|
||||
}''',
|
||||
},
|
||||
{
|
||||
"language": "curl",
|
||||
"name": "REST API (cURL)",
|
||||
"version": "1.0.0",
|
||||
"description": "Direct REST API access without SDK. Use any HTTP client to interact with the platform.",
|
||||
"install_cmd": "# No installation needed - use curl directly",
|
||||
"download_url": "https://docs.qihuyun.com/api-reference",
|
||||
"github_url": "",
|
||||
"min_version": "Any HTTP client",
|
||||
"features": ["Chat completions", "Knowledge base queries", "Agent execution", "File upload", "Webhook management", "No dependencies"],
|
||||
"code_example": '''# Chat completion
|
||||
curl -X POST http://210.16.161.218:18080/app/api/chat/completions \\
|
||||
-H "Authorization: Bearer sk-your-api-key" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"model": "deepseek-ai/DeepSeek-V4-Flash",
|
||||
"messages": [{"role": "user", "content": "Hello!"}]
|
||||
}'
|
||||
|
||||
# List knowledge bases
|
||||
curl http://210.16.161.218:18080/app/api/knowledge-bases \\
|
||||
-H "Authorization: Bearer sk-your-api-key"''',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/sdk/packages", summary="List SDK packages")
|
||||
async def list_sdk_packages(
|
||||
admin_user=Depends(get_current_admin),
|
||||
):
|
||||
"""List all available SDK packages with download info and code examples."""
|
||||
return {"code": 0, "data": {"packages": SDK_PACKAGES}}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Interactive API Documentation
|
||||
# ============================================================
|
||||
|
||||
API_DOC_SECTIONS = [
|
||||
{
|
||||
"section": "Authentication",
|
||||
"icon": "lock",
|
||||
"description": "All API requests require authentication via Bearer token.",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/app/api/auth/login",
|
||||
"title": "User Login",
|
||||
"description": "Authenticate and obtain an access token.",
|
||||
"params": [
|
||||
{"name": "username", "type": "string", "required": True, "desc": "Username"},
|
||||
{"name": "password", "type": "string", "required": True, "desc": "Password"},
|
||||
],
|
||||
"example_request": '{"username": "admin", "password": "admin123"}',
|
||||
"example_response": '{"code": 0, "data": {"access_token": "eyJ...", "token_type": "bearer", "user": {"id": "admin-001", "username": "admin"}}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"section": "Chat & Completions",
|
||||
"icon": "chat",
|
||||
"description": "AI chat completion endpoints for conversational AI.",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/app/api/chat/completions",
|
||||
"title": "Chat Completion",
|
||||
"description": "Create a chat completion with the specified model.",
|
||||
"params": [
|
||||
{"name": "model", "type": "string", "required": True, "desc": "Model ID (e.g. deepseek-ai/DeepSeek-V4-Flash)"},
|
||||
{"name": "messages", "type": "array", "required": True, "desc": "Array of message objects"},
|
||||
{"name": "stream", "type": "boolean", "required": False, "desc": "Enable streaming response"},
|
||||
{"name": "temperature", "type": "number", "required": False, "desc": "Sampling temperature (0-2)"},
|
||||
{"name": "max_tokens", "type": "integer", "required": False, "desc": "Maximum tokens to generate"},
|
||||
],
|
||||
"example_request": '{"model": "deepseek-ai/DeepSeek-V4-Flash", "messages": [{"role": "user", "content": "Hello"}]}',
|
||||
"example_response": '{"code": 0, "data": {"id": "chat-xxx", "choices": [{"message": {"role": "assistant", "content": "Hi there!"}}]}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"section": "Knowledge Base",
|
||||
"icon": "book",
|
||||
"description": "Manage knowledge bases and documents for RAG.",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/app/api/knowledge-bases",
|
||||
"title": "List Knowledge Bases",
|
||||
"description": "Get all knowledge bases for the current user.",
|
||||
"params": [],
|
||||
"example_request": "",
|
||||
"example_response": '{"code": 0, "data": [{"id": "kb-001", "name": "Product Docs", "document_count": 15}]}',
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/app/api/knowledge-bases/{kb_id}/documents",
|
||||
"title": "Upload Document",
|
||||
"description": "Upload a document to a knowledge base for processing.",
|
||||
"params": [
|
||||
{"name": "file", "type": "file", "required": True, "desc": "Document file (PDF, TXT, DOCX, MD)"},
|
||||
],
|
||||
"example_request": "multipart/form-data: file=@document.pdf",
|
||||
"example_response": '{"code": 0, "data": {"document_id": "doc-001", "status": "processing"}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"section": "Agents",
|
||||
"icon": "robot",
|
||||
"description": "Agent management and execution endpoints.",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/app/api/agents",
|
||||
"title": "List Agents",
|
||||
"description": "Get all available agents.",
|
||||
"params": [],
|
||||
"example_request": "",
|
||||
"example_response": '{"code": 0, "data": [{"id": "agent-001", "name": "Code Assistant", "type": "agent"}]}',
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/app/api/agents/{agent_id}/execute",
|
||||
"title": "Execute Agent",
|
||||
"description": "Execute an agent with a given input.",
|
||||
"params": [
|
||||
{"name": "input", "type": "string", "required": True, "desc": "Input text for the agent"},
|
||||
{"name": "context", "type": "object", "required": False, "desc": "Additional context"},
|
||||
],
|
||||
"example_request": '{"input": "Help me write a Python function"}',
|
||||
"example_response": '{"code": 0, "data": {"output": "Here is a Python function...", "tool_calls": []}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"section": "Workflows",
|
||||
"icon": "workflow",
|
||||
"description": "DAG workflow orchestration endpoints.",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/app/api/admin/workflows/templates",
|
||||
"title": "List Workflow Templates",
|
||||
"description": "Get predefined workflow templates.",
|
||||
"params": [],
|
||||
"example_request": "",
|
||||
"example_response": '{"code": 0, "data": [{"id": "tpl-001", "name": "Q&A Pipeline", "nodes": [...]}]}',
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/app/api/admin/workflows/{id}/execute",
|
||||
"title": "Execute Workflow",
|
||||
"description": "Execute a workflow with input data.",
|
||||
"params": [
|
||||
{"name": "input", "type": "string", "required": True, "desc": "Input for the workflow"},
|
||||
],
|
||||
"example_request": '{"input": "Summarize this article"}',
|
||||
"example_response": '{"code": 0, "data": {"execution_id": "exec-001", "status": "completed", "result": "..."}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"section": "File Storage",
|
||||
"icon": "storage",
|
||||
"description": "File upload and storage management.",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/app/api/files/upload",
|
||||
"title": "Upload File",
|
||||
"description": "Upload a file to storage (MinIO/OSS).",
|
||||
"params": [
|
||||
{"name": "file", "type": "file", "required": True, "desc": "File to upload"},
|
||||
{"name": "category", "type": "string", "required": False, "desc": "File category"},
|
||||
],
|
||||
"example_request": "multipart/form-data: file=@image.png",
|
||||
"example_response": '{"code": 0, "data": {"url": "https://...", "size": 102400, "file_type": "image"}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"section": "Webhooks",
|
||||
"icon": "webhook",
|
||||
"description": "Webhook configuration and management.",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/app/api/admin/developer-tools/webhooks",
|
||||
"title": "List Webhooks",
|
||||
"description": "List all configured webhooks.",
|
||||
"params": [],
|
||||
"example_request": "",
|
||||
"example_response": '{"code": 0, "data": {"total": 2, "items": [{"id": "wh_xxx", "name": "My Webhook", "url": "https://...", "status": "active"}]}}',
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/app/api/admin/developer-tools/webhooks/",
|
||||
"title": "Create Webhook",
|
||||
"description": "Create a new webhook endpoint.",
|
||||
"params": [
|
||||
{"name": "name", "type": "string", "required": True, "desc": "Webhook name"},
|
||||
{"name": "url", "type": "string", "required": True, "desc": "Webhook URL"},
|
||||
{"name": "events", "type": "array", "required": True, "desc": "Events to subscribe"},
|
||||
],
|
||||
"example_request": '{"name": "My Webhook", "url": "https://example.com/webhook", "events": ["app.created"]}',
|
||||
"example_response": '{"code": 0, "data": {"id": "wh_xxx", "secret": "whsec_..."}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/api-docs", summary="Interactive API documentation")
|
||||
async def get_api_docs(
|
||||
admin_user=Depends(get_current_admin),
|
||||
):
|
||||
"""Get structured API documentation for the interactive docs viewer."""
|
||||
total_endpoints = sum(len(s["endpoints"]) for s in API_DOC_SECTIONS)
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"sections": API_DOC_SECTIONS,
|
||||
"total_sections": len(API_DOC_SECTIONS),
|
||||
"total_endpoints": total_endpoints,
|
||||
"base_url": "http://210.16.161.218:18080",
|
||||
"api_version": "v1",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Enhanced Usage Dashboard
|
||||
# ============================================================
|
||||
|
||||
@router.get("/usage/dashboard", summary="Enhanced usage dashboard")
|
||||
async def usage_dashboard(
|
||||
days: int = Query(7, ge=1, le=90),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Enhanced usage statistics dashboard with trend data."""
|
||||
now = cst_now()
|
||||
start_date = now - timedelta(days=days)
|
||||
|
||||
# API call trends (daily)
|
||||
trend_sql = """SELECT DATE(created_at) as date, COUNT(*) as calls,
|
||||
SUM(CASE WHEN error_count > 0 THEN 1 ELSE 0 END) as errors,
|
||||
SUM(CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) as avg_time
|
||||
FROM api_call_stats
|
||||
WHERE created_at >= :start
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date"""
|
||||
trend_rows = _execute_raw(db, trend_sql, {"start": start_date})
|
||||
|
||||
trend = []
|
||||
for r in trend_rows:
|
||||
trend.append({
|
||||
"date": r["date"].strftime("%Y-%m-%d") if r.get("date") else "",
|
||||
"calls": r.get("calls", 0),
|
||||
"errors": r.get("errors", 0),
|
||||
"avg_time": round(r.get("avg_time", 0), 2) if r.get("avg_time") else 0,
|
||||
})
|
||||
|
||||
# Top endpoints by call count
|
||||
top_sql = """SELECT endpoint, method, call_count, error_count,
|
||||
last_called_at, total_response_time
|
||||
FROM api_call_stats
|
||||
ORDER BY call_count DESC
|
||||
LIMIT 10"""
|
||||
top_rows = _execute_raw(db, top_sql)
|
||||
|
||||
top_endpoints = []
|
||||
for r in top_rows:
|
||||
top_endpoints.append({
|
||||
"endpoint": r.get("endpoint", ""),
|
||||
"method": r.get("method", ""),
|
||||
"call_count": r.get("call_count", 0),
|
||||
"error_count": r.get("error_count", 0),
|
||||
"error_rate": round(r.get("error_count", 0) / max(r.get("call_count", 1), 1) * 100, 2) if r.get("call_count", 0) > 0 else 0,
|
||||
"avg_response_time": round(r.get("total_response_time", 0) / max(r.get("call_count", 1), 1), 2) if r.get("call_count", 0) > 0 else 0,
|
||||
"last_called_at": fmt_time(r.get("last_called_at")),
|
||||
})
|
||||
|
||||
# Error distribution by status code
|
||||
error_sql = """SELECT
|
||||
CASE
|
||||
WHEN error_count > 0 THEN 'has_errors'
|
||||
ELSE 'success'
|
||||
END as status_group,
|
||||
COUNT(*) as count,
|
||||
SUM(call_count) as total_calls
|
||||
FROM api_call_stats
|
||||
GROUP BY status_group"""
|
||||
error_rows = _execute_raw(db, error_sql)
|
||||
|
||||
error_distribution = {"success": 0, "has_errors": 0, "success_calls": 0, "error_calls": 0}
|
||||
for r in error_rows:
|
||||
if r["status_group"] == "success":
|
||||
error_distribution["success"] = r["count"]
|
||||
error_distribution["success_calls"] = r.get("total_calls", 0)
|
||||
else:
|
||||
error_distribution["has_errors"] = r["count"]
|
||||
error_distribution["error_calls"] = r.get("total_calls", 0)
|
||||
|
||||
# Response time distribution
|
||||
latency_sql = """SELECT
|
||||
CASE
|
||||
WHEN (CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) < 100 THEN 'fast'
|
||||
WHEN (CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) < 500 THEN 'normal'
|
||||
WHEN (CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) < 1000 THEN 'slow'
|
||||
ELSE 'very_slow'
|
||||
END as latency_bucket,
|
||||
COUNT(*) as count,
|
||||
SUM(CASE WHEN call_count > 0 THEN total_response_time / call_count ELSE 0 END) as avg_time
|
||||
FROM api_call_stats
|
||||
WHERE total_response_time IS NOT NULL AND call_count > 0
|
||||
GROUP BY latency_bucket"""
|
||||
latency_rows = _execute_raw(db, latency_sql)
|
||||
|
||||
latency_distribution = {"fast": 0, "normal": 0, "slow": 0, "very_slow": 0}
|
||||
latency_avg = {"fast": 0, "normal": 0, "slow": 0, "very_slow": 0}
|
||||
for r in latency_rows:
|
||||
bucket = r["latency_bucket"]
|
||||
latency_distribution[bucket] = r["count"]
|
||||
latency_avg[bucket] = round(r.get("avg_time", 0), 2) if r.get("avg_time") else 0
|
||||
|
||||
# API key usage distribution
|
||||
key_sql = """SELECT k.name, k.status, COUNT(s.id) as endpoint_count,
|
||||
COALESCE(SUM(s.call_count), 0) as total_calls,
|
||||
COALESCE(SUM(s.error_count), 0) as total_errors
|
||||
FROM api_keys k
|
||||
LEFT JOIN api_call_stats s ON s.endpoint LIKE CONCAT('%api-key%', k.id, '%')
|
||||
GROUP BY k.id, k.name, k.status
|
||||
ORDER BY total_calls DESC
|
||||
LIMIT 10"""
|
||||
key_rows = _execute_raw(db, key_sql)
|
||||
|
||||
key_usage = []
|
||||
for r in key_rows:
|
||||
key_usage.append({
|
||||
"name": r.get("name", "Unknown"),
|
||||
"status": r.get("status", "unknown"),
|
||||
"total_calls": r.get("total_calls", 0),
|
||||
"total_errors": r.get("total_errors", 0),
|
||||
})
|
||||
|
||||
# Summary
|
||||
total_calls = sum(t["calls"] for t in trend)
|
||||
total_errors = sum(t["errors"] for t in trend)
|
||||
avg_response = sum(t["avg_time"] for t in trend) / len(trend) if trend else 0
|
||||
error_rate = round(total_errors / total_calls * 100, 2) if total_calls > 0 else 0
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"summary": {
|
||||
"total_calls": total_calls,
|
||||
"total_errors": total_errors,
|
||||
"error_rate": error_rate,
|
||||
"avg_response_time": round(avg_response, 2),
|
||||
"active_api_keys": error_distribution["success"] + error_distribution["has_errors"],
|
||||
},
|
||||
"trend": trend,
|
||||
"top_endpoints": top_endpoints,
|
||||
"error_distribution": error_distribution,
|
||||
"latency_distribution": latency_distribution,
|
||||
"latency_avg": latency_avg,
|
||||
"key_usage": key_usage,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user