Add service bridges and frontend panel structure

This commit is contained in:
Aaron 2026-04-19 16:23:39 +08:00
parent 0a3f0585f4
commit 66af3b44d9
41 changed files with 1096 additions and 33 deletions

View File

@ -5,21 +5,27 @@
## 1. 当前项目状态 ## 1. 当前项目状态
当前仓库是 Aurask 的 **Python 模块化单体 MVP** 当前仓库是 Aurask 的 **Python 模块化单体 MVP**,根目录按产品边界划分为:
- `api`后端服务、PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 桥接配置,以及前端到后端请求契约。
- `protal`:用户前端使用面板。目录名按当前需求保留 `protal` 拼写。
- `manager`:管理员前端使用面板。
- `deploy`k3s 与后续部署配置。
已实现能力: 已实现能力:
- 租户、用户与 API Key`src/aurask/auth.py` - 租户、用户与 API Key`api/aurask/auth.py`
- HTTP 网关:`src/aurask/api.py` - HTTP 网关:`api/aurask/api.py`
- 套餐与商品目录:`src/aurask/plans.py` - 套餐与商品目录:`api/aurask/plans.py`
- 订单、订阅与权益发放:`src/aurask/billing.py` - 订单、订阅与权益发放:`api/aurask/billing.py`
- TBU 额度、预扣、结算与账本:`src/aurask/quota.py` - TBU 额度、预扣、结算与账本:`api/aurask/quota.py`
- USDT-TRC20 支付匹配:`src/aurask/payments.py` - USDT-TRC20 支付匹配:`api/aurask/payments.py`
- 模板工作流编排:`src/aurask/orchestrator.py` - 模板工作流编排:`api/aurask/orchestrator.py`
- AnythingLLM Workspace / 文档接入适配:`src/aurask/knowledge_base.py` - AnythingLLM Workspace / 文档接入适配:`api/aurask/knowledge_base.py`
- 审计事件:`src/aurask/audit.py` - PostgreSQL / PGVector / Redis / AnythingLLM / Langflow 桥接:`api/aurask/bridges/`
- MVP JSON 持久化:`src/aurask/repository.py` - 审计事件:`api/aurask/audit.py`
- CLI`src/aurask/cli.py` - MVP JSON 持久化:`api/aurask/repository.py`
- CLI`api/aurask/cli.py`
- 测试:`tests/test_mvp.py` - 测试:`tests/test_mvp.py`
- k3s 部署规划:`deploy/k3s/README.md` - k3s 部署规划:`deploy/k3s/README.md`
@ -43,11 +49,15 @@
当前目录职责如下: 当前目录职责如下:
```text ```text
src/aurask/ api/
README.md # API 与外部组件桥接说明
requests/ # 前端到后端请求样例
aurask/
__init__.py # 包入口,导出 CLI main __init__.py # 包入口,导出 CLI main
__main__.py # python -m aurask __main__.py # python -m aurask
app.py # 应用装配,不写具体业务规则 app.py # 应用装配,不写具体业务规则
api.py # HTTP 网关与路由映射 api.py # HTTP 网关与路由映射
bridge_status.py # 组件桥接状态
cli.py # 命令行入口 cli.py # 命令行入口
repository.py # MVP 持久化层 repository.py # MVP 持久化层
errors.py # 统一错误类型 errors.py # 统一错误类型
@ -60,6 +70,11 @@ src/aurask/
payments.py # 支付匹配、tx_hash 幂等 payments.py # 支付匹配、tx_hash 幂等
orchestrator.py # 工作流模板与运行编排 orchestrator.py # 工作流模板与运行编排
knowledge_base.py # Workspace 与文档接入 knowledge_base.py # Workspace 与文档接入
bridges/ # PostgreSQL/PGVector/Redis/AnythingLLM/Langflow 桥接
protal/
index.html # 用户面板
manager/
index.html # 管理员面板
tests/ tests/
test_mvp.py # 核心闭环测试 test_mvp.py # 核心闭环测试
deploy/k3s/ deploy/k3s/
@ -163,6 +178,15 @@ Aurask 目标架构:
- 允许为演示补功能,但不要把它描述为生产数据库。 - 允许为演示补功能,但不要把它描述为生产数据库。
- 生产化时应新增 PostgreSQL repository而不是把 JSON store 继续扩展成复杂数据库。 - 生产化时应新增 PostgreSQL repository而不是把 JSON store 继续扩展成复杂数据库。
### 6.10 `bridges/`
- `config.py` 统一读取环境变量。
- `postgres.py` 只维护 PostgreSQL schema / migration contract不直接混入业务服务。
- `pgvector.py` 必须强制 `tenant_id`、`workspace_id` 过滤契约。
- `redis_bridge.py` 统一队列、缓存、幂等和限流 key 规则。
- `anythingllm.py` 只封装 AnythingLLM API不绕过 Aurask Workspace 绑定。
- `langflow.py` 只执行审核模板,不开放任意代码执行。
## 7. Langflow 与 AnythingLLM 约束 ## 7. Langflow 与 AnythingLLM 约束
### Langflow ### Langflow
@ -220,7 +244,7 @@ uv run aurask demo --reset
如果当前环境缺少 `uv`,可使用: 如果当前环境缺少 `uv`,可使用:
```bash ```bash
$env:PYTHONPATH='src'; py -3 -m aurask demo --reset $env:PYTHONPATH='api'; py -3 -m aurask demo --reset
``` ```
## 10. 文档同步要求 ## 10. 文档同步要求

View File

@ -9,6 +9,8 @@ Aurask 当前已完成 **可运行 MVP 后端骨架**
- 使用 Python 模块化单体实现首版领域闭环。 - 使用 Python 模块化单体实现首版领域闭环。
- 已具备 `Auth + API Gateway`、`Billing + Quota + TBU Ledger`、`Workflow Orchestrator`、`Knowledge Base`、`USDT-TRC20 Payment`、`Audit` 等核心边界。 - 已具备 `Auth + API Gateway`、`Billing + Quota + TBU Ledger`、`Workflow Orchestrator`、`Knowledge Base`、`USDT-TRC20 Payment`、`Audit` 等核心边界。
- 已按产品职责划分根目录:`api`、`protal`、`manager`、`deploy`。
- 已补充 PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 的桥接配置与接口契约。
- 已提供 CLI 演示与标准库 HTTP 网关。 - 已提供 CLI 演示与标准库 HTTP 网关。
- 已新增面向 `300` 名月度活跃用户的 `k3s` 部署规划:`deploy/k3s/README.md`。 - 已新增面向 `300` 名月度活跃用户的 `k3s` 部署规划:`deploy/k3s/README.md`。
@ -35,11 +37,16 @@ Aurask 是面向海外个人开发者、学生、独立创业者和小团队的
### 2.1 当前目录结构 ### 2.1 当前目录结构
```text ```text
src/aurask/ api/
README.md # API 与外部组件桥接说明
requests/
aurask-api.http # 前端到后端请求样例
aurask/
__init__.py # 包入口,导出 CLI main __init__.py # 包入口,导出 CLI main
__main__.py # python -m aurask 入口 __main__.py # python -m aurask 入口
app.py # 应用装配与 demo bootstrap app.py # 应用装配与 demo bootstrap
api.py # 标准库 HTTP 网关 api.py # 标准库 HTTP 网关
bridge_status.py # 组件桥接状态
cli.py # aurask demo / aurask serve cli.py # aurask demo / aurask serve
repository.py # MVP JSON 持久化 repository.py # MVP JSON 持久化
errors.py # 应用错误类型 errors.py # 应用错误类型
@ -52,6 +59,21 @@ src/aurask/
payments.py # USDT-TRC20 支付匹配 payments.py # USDT-TRC20 支付匹配
orchestrator.py # 模板工作流编排与 Langflow Runtime 适配 orchestrator.py # 模板工作流编排与 Langflow Runtime 适配
knowledge_base.py # AnythingLLM Workspace / 文档接入适配 knowledge_base.py # AnythingLLM Workspace / 文档接入适配
bridges/
config.py # PostgreSQL/PGVector/Redis/AnythingLLM/Langflow 环境配置
postgres.py # PostgreSQL schema contract
pgvector.py # PGVector tenant-filtered collection contract
redis_bridge.py # Redis queue/cache/idempotency key contract
anythingllm.py # AnythingLLM API bridge
langflow.py # Langflow Runtime bridge
protal/
index.html # 用户前端使用面板
main.js
styles.css
manager/
index.html # 管理员前端使用面板
main.js
styles.css
tests/ tests/
test_mvp.py # MVP 核心流程测试 test_mvp.py # MVP 核心流程测试
deploy/k3s/ deploy/k3s/
@ -92,13 +114,48 @@ python -m unittest discover -s tests -v
| --- | --- | --- | | --- | --- | --- |
| 持久化 | 本地 JSON 文件 | PostgreSQL + PGVector | | 持久化 | 本地 JSON 文件 | PostgreSQL + PGVector |
| 队列 | 同进程同步执行 | Redis / RabbitMQ / NATS 队列 | | 队列 | 同进程同步执行 | Redis / RabbitMQ / NATS 队列 |
| Langflow | `LangflowRuntimeAdapter` 模拟适配层 | 内部 `ClusterIP` Langflow Runtime Pool | | Langflow | 默认模拟适配层,可用 `LangflowBridge` 接真实服务 | 内部 `ClusterIP` Langflow Runtime Pool |
| AnythingLLM | `AnythingLLMAdapter` 模拟适配层 | 内部 `ClusterIP` AnythingLLM API | | AnythingLLM | 默认模拟适配层,可用 `AnythingLLMBridge` 接真实服务 | 内部 `ClusterIP` AnythingLLM API |
| 网关 | Python 标准库 HTTP Server | FastAPI / ASGI + Ingress + HPA | | 网关 | Python 标准库 HTTP Server | FastAPI / ASGI + Ingress + HPA |
| 支付 | 人工提交 tx hash 匹配 | TronGrid / Tronscan / 自建节点监听 | | 支付 | 人工提交 tx hash 匹配 | TronGrid / Tronscan / 自建节点监听 |
| 观测 | 审计事件写入 store | Prometheus + Loki + Grafana + Alertmanager | | 观测 | 审计事件写入 store | Prometheus + Loki + Grafana + Alertmanager |
| 部署 | 本地运行 | k3s`aurask-api` + `aurask-worker` | | 部署 | 本地运行 | k3s`aurask-api` + `aurask-worker` |
### 2.4 根目录职责
| 根目录 | 职责 |
| --- | --- |
| `api` | 后端服务、外部组件桥接配置、前端请求契约 |
| `protal` | 用户前端使用面板。目录名按当前需求使用 `protal` 拼写 |
| `manager` | 管理员前端使用面板 |
| `deploy` | k3s 与后续部署配置 |
### 2.5 外部组件桥接
生产桥接通过环境变量启用:
```bash
AURASK_USE_EXTERNAL_BRIDGES=true
AURASK_DATABASE_URL=postgresql://aurask:secret@postgres:5432/aurask
AURASK_REDIS_URL=redis://redis:6379/0
AURASK_ANYTHINGLLM_BASE_URL=http://anythingllm.aurask-runtime.svc.cluster.local:3001
AURASK_ANYTHINGLLM_API_KEY=<secret>
AURASK_LANGFLOW_BASE_URL=http://langflow-runtime.aurask-runtime.svc.cluster.local:7860
AURASK_LANGFLOW_API_KEY=<secret>
```
桥接模块:
- `api/aurask/bridges/postgres.py`PostgreSQL schema contract。
- `api/aurask/bridges/pgvector.py`PGVector collection 与租户过滤契约。
- `api/aurask/bridges/redis_bridge.py`:队列、缓存、幂等键、限流键约定。
- `api/aurask/bridges/anythingllm.py`AnythingLLM Workspace / 文档入库桥接。
- `api/aurask/bridges/langflow.py`Langflow 安全模板运行桥接。
管理员可通过鉴权接口查看桥接配置状态:
- `GET /admin/bridge-status`
## 3. 目标技术架构 ## 3. 目标技术架构
```text ```text
@ -205,10 +262,10 @@ AnythingLLM 负责 Workspace、文档、RAG 和聊天历史。
### 5.3 当前代码对应 ### 5.3 当前代码对应
- 套餐定义:`src/aurask/plans.py` - 套餐定义:`api/aurask/plans.py`
- 额度账户:`src/aurask/quota.py` - 额度账户:`api/aurask/quota.py`
- 订单与权益:`src/aurask/billing.py` - 订单与权益:`api/aurask/billing.py`
- 使用记录:`src/aurask/orchestrator.py` - 使用记录:`api/aurask/orchestrator.py`
## 6. 支付与订单闭环 ## 6. 支付与订单闭环

View File

@ -1,6 +1,6 @@
## Aurask ## Aurask
Aurask 首版已按 `Aurask_Technical_Operations_Plan.md` 落地为一个可运行的模块化单体后端,覆盖以下核心边界: Aurask 首版已按 `Aurask_Technical_Operations_Plan.md` 落地为一个可运行的模块化单体后端,并按产品边界拆分为 `api`、`protal`、`manager`、`deploy` 四个根目录。
- `Auth + API Gateway` - `Auth + API Gateway`
- `Billing + Quota + TBU Ledger` - `Billing + Quota + TBU Ledger`
@ -30,6 +30,14 @@ uv run aurask demo --reset
uv run aurask serve --reset --host 127.0.0.1 --port 8080 uv run aurask serve --reset --host 127.0.0.1 --port 8080
``` ```
如果本机未安装 `uv`,可用:
```powershell
$env:PYTHONPATH='api'
py -3 -m aurask demo --reset
py -3 -m aurask serve --reset --host 127.0.0.1 --port 8080
```
### Demo flow ### Demo flow
`aurask demo` 会自动完成: `aurask demo` 会自动完成:
@ -59,6 +67,7 @@ uv run aurask serve --reset --host 127.0.0.1 --port 8080
- `POST /payments/match` - `POST /payments/match`
- `POST /workflow-runs` - `POST /workflow-runs`
- `GET /workflow-runs/{run_id}` - `GET /workflow-runs/{run_id}`
- `GET /admin/bridge-status`
鉴权方式: 鉴权方式:
@ -110,17 +119,22 @@ python -m unittest discover -s tests -v
### Deployment ### Deployment
- `deploy/k3s/README.md`: 面向 `300` 名月度活跃用户的 `k3s` 部署方案 - `deploy/k3s/README.md`: 面向 `300` 名月度活跃用户的 `k3s` 部署方案
- `api/README.md`: PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 桥接配置
- `api/requests/aurask-api.http`: 前端到后端请求样例
### Project layout ### Project layout
- `AGENTS.md`: 项目级实现约束 - `AGENTS.md`: 项目级实现约束
- `Aurask_Technical_Operations_Plan.md`: 技术与运营方案 - `Aurask_Technical_Operations_Plan.md`: 技术与运营方案
- `src/aurask/app.py`: 应用装配 - `api/aurask/app.py`: 应用装配
- `src/aurask/api.py`: HTTP 网关 - `api/aurask/api.py`: HTTP 网关
- `src/aurask/auth.py`: 租户、用户与 API Key - `api/aurask/bridges/`: PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 桥接配置
- `src/aurask/billing.py`: 套餐、订单与权益发放 - `api/aurask/auth.py`: 租户、用户与 API Key
- `src/aurask/quota.py`: TBU 预扣、结算与额度账本 - `api/aurask/billing.py`: 套餐、订单与权益发放
- `src/aurask/orchestrator.py`: 模板工作流编排 - `api/aurask/quota.py`: TBU 预扣、结算与额度账本
- `src/aurask/knowledge_base.py`: Workspace 与文档接入 - `api/aurask/orchestrator.py`: 模板工作流编排
- `src/aurask/payments.py`: USDT-TRC20 支付匹配 - `api/aurask/knowledge_base.py`: Workspace 与文档接入
- `api/aurask/payments.py`: USDT-TRC20 支付匹配
- `protal/`: 用户前端使用面板
- `manager/`: 管理员前端使用面板
- `tests/`: 单元测试 - `tests/`: 单元测试

50
api/README.md Normal file
View File

@ -0,0 +1,50 @@
# Aurask API
This directory contains the Python backend package and bridge configuration for:
- PostgreSQL
- PGVector
- Redis
- AnythingLLM
- Langflow
- Frontend-to-backend request contracts
## Runtime modes
Default local mode uses `JsonStore` and simulated bridges:
```bash
uv run aurask demo --reset
uv run aurask serve --reset --host 127.0.0.1 --port 8080
```
Production bridge mode can be enabled with:
```bash
$env:AURASK_USE_EXTERNAL_BRIDGES="true"
$env:AURASK_DATABASE_URL="postgresql://aurask:secret@postgres:5432/aurask"
$env:AURASK_REDIS_URL="redis://redis:6379/0"
$env:AURASK_ANYTHINGLLM_BASE_URL="http://anythingllm.aurask-runtime.svc.cluster.local:3001"
$env:AURASK_ANYTHINGLLM_API_KEY="<secret>"
$env:AURASK_LANGFLOW_BASE_URL="http://langflow-runtime.aurask-runtime.svc.cluster.local:7860"
$env:AURASK_LANGFLOW_API_KEY="<secret>"
uv run aurask serve --host 0.0.0.0 --port 8080
```
## Bridge modules
- `aurask.bridges.config`: environment-driven configuration
- `aurask.bridges.postgres`: PostgreSQL schema contract
- `aurask.bridges.pgvector`: PGVector tenant-filtered collection contract
- `aurask.bridges.redis_bridge`: Redis queue/cache/idempotency key contract
- `aurask.bridges.anythingllm`: AnythingLLM API bridge
- `aurask.bridges.langflow`: Langflow runtime bridge
## Admin bridge status
Authenticated admin status endpoint:
```http
GET /admin/bridge-status
Authorization: Bearer <api_key>
```

View File

@ -8,6 +8,7 @@ from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from aurask.app import AuraskApp from aurask.app import AuraskApp
from aurask.bridge_status import bridge_status
from aurask.errors import AuraskError from aurask.errors import AuraskError
@ -55,6 +56,9 @@ def make_handler(app: AuraskApp):
tenant_id = context["tenant"]["id"] tenant_id = context["tenant"]["id"]
user_id = context["user"]["id"] user_id = context["user"]["id"]
if method == "GET" and path == "/admin/bridge-status":
self._send(200, bridge_status())
return
if method == "GET" and path == "/quota": if method == "GET" and path == "/quota":
self._send(200, app.quota.get_account(tenant_id)) self._send(200, app.quota.get_account(tenant_id))
return return

View File

@ -2,12 +2,16 @@
from __future__ import annotations from __future__ import annotations
import os
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from aurask.audit import AuditService from aurask.audit import AuditService
from aurask.auth import AuthService from aurask.auth import AuthService
from aurask.billing import BillingService from aurask.billing import BillingService
from aurask.bridges.anythingllm import AnythingLLMBridge
from aurask.bridges.config import BridgeConfig
from aurask.bridges.langflow import LangflowBridge
from aurask.knowledge_base import KnowledgeBaseService from aurask.knowledge_base import KnowledgeBaseService
from aurask.orchestrator import WorkflowOrchestrator from aurask.orchestrator import WorkflowOrchestrator
from aurask.payments import PaymentService from aurask.payments import PaymentService
@ -41,17 +45,22 @@ class AuraskApp:
} }
def create_app(data_path: str | Path | None = None, *, reset: bool = False) -> AuraskApp: def create_app(data_path: str | Path | None = None, *, reset: bool = False, use_external_bridges: bool | None = None) -> AuraskApp:
store = JsonStore(data_path) store = JsonStore(data_path)
if reset: if reset:
store.delete_all() store.delete_all()
if use_external_bridges is None:
use_external_bridges = os.getenv("AURASK_USE_EXTERNAL_BRIDGES", "false").lower() in {"1", "true", "yes"}
bridge_config = BridgeConfig.from_env()
audit = AuditService(store) audit = AuditService(store)
auth = AuthService(store, audit) auth = AuthService(store, audit)
quota = QuotaService(store, audit) quota = QuotaService(store, audit)
billing = BillingService(store, quota, audit) billing = BillingService(store, quota, audit)
payments = PaymentService(store, billing, audit) payments = PaymentService(store, billing, audit)
knowledge = KnowledgeBaseService(store, quota, audit) anythingllm_adapter = AnythingLLMBridge(bridge_config.anythingllm) if use_external_bridges else None
orchestrator = WorkflowOrchestrator(store, quota, audit) langflow_runtime = LangflowBridge(bridge_config.langflow) if use_external_bridges else None
knowledge = KnowledgeBaseService(store, quota, audit, adapter=anythingllm_adapter)
orchestrator = WorkflowOrchestrator(store, quota, audit, runtime=langflow_runtime)
seed_plan_catalog(store) seed_plan_catalog(store)
orchestrator.seed_templates() orchestrator.seed_templates()
return AuraskApp( return AuraskApp(

View File

@ -0,0 +1,37 @@
"""Bridge configuration status for admin and deployment checks."""
from __future__ import annotations
from aurask.bridges.config import BridgeConfig
from aurask.bridges.pgvector import PGVectorBridge
from aurask.bridges.postgres import PostgresBridge
from aurask.bridges.redis_bridge import RedisBridge
def bridge_status(config: BridgeConfig | None = None) -> dict:
config = config or BridgeConfig.from_env()
return {
"postgres": {
"database_url_configured": bool(config.postgres.database_url),
"min_connections": config.postgres.min_connections,
"max_connections": config.postgres.max_connections,
"migration_available": bool(PostgresBridge(config.postgres).migration_plan()["schema_sql"]),
},
"pgvector": {
"table": config.pgvector.collection_table,
"embedding_dimension": config.pgvector.embedding_dimension,
"tenant_filters_required": PGVectorBridge(config.pgvector).search_contract("tenant", "workspace")["required"],
},
"redis": {
"redis_url_configured": bool(config.redis.redis_url),
"workflow_queue": RedisBridge(config.redis).workflow_queue_key(),
},
"anythingllm": {
"base_url": config.anythingllm.base_url,
"api_key_configured": bool(config.anythingllm.api_key),
},
"langflow": {
"base_url": config.langflow.base_url,
"api_key_configured": bool(config.langflow.api_key),
},
}

View File

@ -0,0 +1,5 @@
"""External service bridge adapters for Aurask production integration."""
from aurask.bridges.config import BridgeConfig
__all__ = ["BridgeConfig"]

View File

@ -0,0 +1,46 @@
"""AnythingLLM production bridge."""
from __future__ import annotations
from dataclasses import dataclass
from aurask.bridges.config import AnythingLLMConfig
from aurask.bridges.http import JsonHttpClient
@dataclass
class AnythingLLMBridge:
config: AnythingLLMConfig
def __post_init__(self) -> None:
self.client = JsonHttpClient(
self.config.base_url,
api_key=self.config.api_key,
timeout_seconds=self.config.request_timeout_seconds,
)
def create_workspace(self, tenant_id: str, name: str) -> str:
payload = {"name": name, "metadata": {"tenant_id": tenant_id}}
response = self.client.post("/api/v1/workspace/new", payload)
return str(response.get("workspace", {}).get("slug") or response.get("slug") or f"anythingllm-{tenant_id}-{name}")
def ingest_document(self, workspace_external_id: str, document: dict) -> dict:
payload = {
"workspace": workspace_external_id,
"document": {
"id": document["id"],
"filename": document["filename"],
"content_hash": document["content_hash"],
"storage_path": document["storage_path"],
"metadata": {
"tenant_id": document["tenant_id"],
"workspace_id": document["workspace_id"],
"document_id": document["id"],
},
},
}
response = self.client.post("/api/v1/document/ingest", payload)
return {
"external_document_id": str(response.get("documentId") or response.get("id") or document["id"]),
"status": str(response.get("status") or "queued"),
}

View File

@ -0,0 +1,80 @@
"""Environment-driven bridge configuration."""
from __future__ import annotations
import os
from dataclasses import dataclass
@dataclass(frozen=True)
class PostgresConfig:
database_url: str
min_connections: int = 1
max_connections: int = 10
@dataclass(frozen=True)
class PGVectorConfig:
collection_table: str = "aurask_vectors"
embedding_dimension: int = 1536
@dataclass(frozen=True)
class RedisConfig:
redis_url: str
queue_name: str = "aurask:workflow-runs"
lock_prefix: str = "aurask:lock:"
cache_prefix: str = "aurask:cache:"
@dataclass(frozen=True)
class AnythingLLMConfig:
base_url: str
api_key: str
request_timeout_seconds: int = 30
@dataclass(frozen=True)
class LangflowConfig:
base_url: str
api_key: str
request_timeout_seconds: int = 180
@dataclass(frozen=True)
class BridgeConfig:
postgres: PostgresConfig
pgvector: PGVectorConfig
redis: RedisConfig
anythingllm: AnythingLLMConfig
langflow: LangflowConfig
@classmethod
def from_env(cls) -> "BridgeConfig":
return cls(
postgres=PostgresConfig(
database_url=os.getenv("AURASK_DATABASE_URL", "postgresql://aurask:aurask@postgres:5432/aurask"),
min_connections=int(os.getenv("AURASK_POSTGRES_MIN_CONNECTIONS", "1")),
max_connections=int(os.getenv("AURASK_POSTGRES_MAX_CONNECTIONS", "10")),
),
pgvector=PGVectorConfig(
collection_table=os.getenv("AURASK_PGVECTOR_TABLE", "aurask_vectors"),
embedding_dimension=int(os.getenv("AURASK_PGVECTOR_DIMENSION", "1536")),
),
redis=RedisConfig(
redis_url=os.getenv("AURASK_REDIS_URL", "redis://redis:6379/0"),
queue_name=os.getenv("AURASK_REDIS_WORKFLOW_QUEUE", "aurask:workflow-runs"),
lock_prefix=os.getenv("AURASK_REDIS_LOCK_PREFIX", "aurask:lock:"),
cache_prefix=os.getenv("AURASK_REDIS_CACHE_PREFIX", "aurask:cache:"),
),
anythingllm=AnythingLLMConfig(
base_url=os.getenv("AURASK_ANYTHINGLLM_BASE_URL", "http://anythingllm.aurask-runtime.svc.cluster.local:3001"),
api_key=os.getenv("AURASK_ANYTHINGLLM_API_KEY", ""),
request_timeout_seconds=int(os.getenv("AURASK_ANYTHINGLLM_TIMEOUT_SECONDS", "30")),
),
langflow=LangflowConfig(
base_url=os.getenv("AURASK_LANGFLOW_BASE_URL", "http://langflow-runtime.aurask-runtime.svc.cluster.local:7860"),
api_key=os.getenv("AURASK_LANGFLOW_API_KEY", ""),
request_timeout_seconds=int(os.getenv("AURASK_LANGFLOW_TIMEOUT_SECONDS", "180")),
),
)

View File

@ -0,0 +1,41 @@
"""Small stdlib JSON HTTP client used by bridge adapters."""
from __future__ import annotations
import json
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from aurask.errors import ValidationError
class JsonHttpClient:
def __init__(self, base_url: str, *, api_key: str = "", timeout_seconds: int = 30) -> None:
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.timeout_seconds = timeout_seconds
def get(self, path: str) -> dict[str, Any]:
return self._request("GET", path)
def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
return self._request("POST", path, payload)
def _request(self, method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
body = None if payload is None else json.dumps(payload).encode("utf-8")
request = Request(f"{self.base_url}{path}", data=body, method=method)
request.add_header("Accept", "application/json")
if body is not None:
request.add_header("Content-Type", "application/json")
if self.api_key:
request.add_header("Authorization", f"Bearer {self.api_key}")
try:
with urlopen(request, timeout=self.timeout_seconds) as response:
data = response.read().decode("utf-8")
return json.loads(data) if data else {}
except HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise ValidationError("upstream HTTP bridge returned an error", details={"status": exc.code, "body": detail}) from exc
except URLError as exc:
raise ValidationError("upstream HTTP bridge is unavailable", details={"reason": str(exc.reason)}) from exc

View File

@ -0,0 +1,49 @@
"""Langflow production runtime bridge."""
from __future__ import annotations
from dataclasses import dataclass
from aurask.bridges.config import LangflowConfig
from aurask.bridges.http import JsonHttpClient
@dataclass
class LangflowBridge:
config: LangflowConfig
def __post_init__(self) -> None:
self.client = JsonHttpClient(
self.config.base_url,
api_key=self.config.api_key,
timeout_seconds=self.config.request_timeout_seconds,
)
def execute_template_flow(self, template: dict, inputs: dict, *, estimated_tbu: int) -> dict:
flow_id = template.get("langflow_flow_id") or template["id"]
payload = {
"input_value": inputs,
"output_type": "chat",
"input_type": "json",
"tweaks": {
"aurask": {
"template_id": template["id"],
"estimated_tbu": estimated_tbu,
"safe_template": True,
}
},
}
response = self.client.post(f"/api/v1/run/{flow_id}", payload)
actual_tbu = int(response.get("actual_tbu") or max(1, estimated_tbu))
return {
"status": "completed",
"actual_tbu": actual_tbu,
"output": {
"template_id": template["id"],
"summary": response.get("summary") or "Langflow template executed",
"raw_result_ref": response.get("run_id") or response.get("id"),
},
}
def execute(self, template: dict, inputs: dict, *, estimated_tbu: int) -> dict:
return self.execute_template_flow(template, inputs, estimated_tbu=estimated_tbu)

View File

@ -0,0 +1,43 @@
"""PGVector bridge contract for tenant-filtered vector retrieval."""
from __future__ import annotations
from dataclasses import dataclass
from aurask.bridges.config import PGVectorConfig
@dataclass(frozen=True)
class PGVectorBridge:
config: PGVectorConfig
def create_collection_sql(self) -> str:
table = self.config.collection_table
dimension = self.config.embedding_dimension
return f"""
CREATE TABLE IF NOT EXISTS {table} (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
workspace_id TEXT NOT NULL,
document_id TEXT NOT NULL,
chunk_hash TEXT NOT NULL,
content_summary TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{{}}'::jsonb,
embedding vector({dimension}) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS {table}_tenant_workspace_idx
ON {table} (tenant_id, workspace_id);
CREATE INDEX IF NOT EXISTS {table}_embedding_idx
ON {table} USING ivfflat (embedding vector_cosine_ops);
""".strip()
def search_contract(self, tenant_id: str, workspace_id: str, top_k: int = 5) -> dict:
return {
"table": self.config.collection_table,
"filters": {"tenant_id": tenant_id, "workspace_id": workspace_id},
"top_k": top_k,
"required": ["tenant_id", "workspace_id", "query_embedding"],
}

View File

@ -0,0 +1,99 @@
"""PostgreSQL bridge contract.
The MVP still uses ``JsonStore``. This module centralizes the production
PostgreSQL contract so repository migration can happen without changing domain
services.
"""
from __future__ import annotations
from dataclasses import dataclass
from aurask.bridges.config import PostgresConfig
SCHEMA_SQL = """
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS tenants (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
region TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
email TEXT NOT NULL,
role TEXT NOT NULL,
status TEXT NOT NULL,
api_key_hash TEXT,
created_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS quota_accounts (
tenant_id TEXT PRIMARY KEY REFERENCES tenants(id),
plan_code TEXT NOT NULL,
available_tbu INTEGER NOT NULL,
reserved_tbu INTEGER NOT NULL,
workflow_slots INTEGER NOT NULL,
knowledge_bases INTEGER NOT NULL,
storage_mb INTEGER NOT NULL,
used_storage_mb INTEGER NOT NULL,
active_workflow_runs INTEGER NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS quota_ledger (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
entry_type TEXT NOT NULL,
amount INTEGER NOT NULL,
unit TEXT NOT NULL,
reason TEXT NOT NULL,
source_id TEXT NOT NULL,
reservation_id TEXT,
created_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
user_id TEXT NOT NULL REFERENCES users(id),
product_code TEXT NOT NULL,
amount_usdt NUMERIC(18, 6) NOT NULL,
chain TEXT NOT NULL,
currency TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
paid_at TIMESTAMPTZ,
fulfilled_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS audit_events (
id TEXT PRIMARY KEY,
tenant_id TEXT,
user_id TEXT,
event_type TEXT NOT NULL,
resource_type TEXT,
resource_id TEXT,
summary TEXT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL
);
"""
@dataclass(frozen=True)
class PostgresBridge:
config: PostgresConfig
def migration_plan(self) -> dict:
return {
"database_url": self.config.database_url,
"min_connections": self.config.min_connections,
"max_connections": self.config.max_connections,
"schema_sql": SCHEMA_SQL.strip(),
}

View File

@ -0,0 +1,25 @@
"""Redis bridge contract for queue, cache, idempotency, and rate-limit keys."""
from __future__ import annotations
from dataclasses import dataclass
from aurask.bridges.config import RedisConfig
@dataclass(frozen=True)
class RedisBridge:
config: RedisConfig
def workflow_queue_key(self) -> str:
return self.config.queue_name
def idempotency_key(self, tenant_id: str, scope: str, key: str) -> str:
return f"{self.config.lock_prefix}{tenant_id}:{scope}:{key}"
def cache_key(self, tenant_id: str, scope: str, key: str) -> str:
return f"{self.config.cache_prefix}{tenant_id}:{scope}:{key}"
def rate_limit_key(self, tenant_id: str, user_id: str, route: str) -> str:
normalized_route = route.strip("/").replace("/", ":")
return f"aurask:rate:{tenant_id}:{user_id}:{normalized_route}"

View File

@ -0,0 +1,86 @@
### Health
GET http://127.0.0.1:8080/health
### Plans
GET http://127.0.0.1:8080/plans
### Demo bootstrap
POST http://127.0.0.1:8080/demo/bootstrap
Content-Type: application/json
{
"tenant_name": "Aurask Demo",
"email": "owner@example.com"
}
### Set API key after bootstrap
@api_key = ak_replace_me
### Quota
GET http://127.0.0.1:8080/quota
Authorization: Bearer {{api_key}}
### Workflow templates
GET http://127.0.0.1:8080/workflow-templates
Authorization: Bearer {{api_key}}
### Create workspace
POST http://127.0.0.1:8080/workspaces
Authorization: Bearer {{api_key}}
Content-Type: application/json
{
"name": "Support KB"
}
### Upload document metadata
POST http://127.0.0.1:8080/documents
Authorization: Bearer {{api_key}}
Content-Type: application/json
{
"workspace_id": "ws_replace_me",
"filename": "support-guide.pdf",
"size_bytes": 1048576,
"content_type": "application/pdf",
"content_preview": "Support guide hash input"
}
### Run workflow
POST http://127.0.0.1:8080/workflow-runs
Authorization: Bearer {{api_key}}
Content-Type: application/json
{
"template_id": "tpl_knowledge_qa",
"workspace_id": "ws_replace_me",
"inputs": {
"question": "How can Aurask help my support team?"
}
}
### Create TBU order
POST http://127.0.0.1:8080/orders
Authorization: Bearer {{api_key}}
Content-Type: application/json
{
"product_code": "tbu_1000",
"quantity": 1
}
### Match USDT-TRC20 payment
POST http://127.0.0.1:8080/payments/match
Authorization: Bearer {{api_key}}
Content-Type: application/json
{
"order_id": "order_replace_me",
"tx_hash": "tron_tx_hash_replace_me",
"amount_usdt": 21.87,
"confirmations": 20
}
### Admin bridge status
GET http://127.0.0.1:8080/admin/bridge-status
Authorization: Bearer {{api_key}}

17
manager/README.md Normal file
View File

@ -0,0 +1,17 @@
# Aurask Manager
`manager` is the admin-facing panel directory.
Current MVP:
- Static HTML panel
- Uses Aurask admin endpoints
- Shows bridge configuration status for PostgreSQL, PGVector, Redis, AnythingLLM, and Langflow
Start the API first:
```bash
uv run aurask serve --reset --host 127.0.0.1 --port 8080
```
Then open `index.html` in a browser.

31
manager/index.html Normal file
View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aurask Manager</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main class="shell">
<section class="hero">
<p class="eyebrow">Aurask Manager</p>
<h1>Operate tenant-safe AI workflow infrastructure</h1>
<p>Inspect bridge configuration and verify production integration readiness.</p>
</section>
<section class="card">
<h2>Admin Connection</h2>
<label>API Base URL <input id="apiBase" value="http://127.0.0.1:8080" /></label>
<label>Admin API Key <input id="apiKey" placeholder="Use owner API key from bootstrap" /></label>
<button id="statusBtn">Load Bridge Status</button>
</section>
<section class="card">
<h2>Bridge Status</h2>
<pre id="statusOutput"></pre>
</section>
</main>
<script src="./main.js"></script>
</body>
</html>

19
manager/main.js Normal file
View File

@ -0,0 +1,19 @@
const apiBaseInput = document.querySelector("#apiBase");
const apiKeyInput = document.querySelector("#apiKey");
function apiBase() {
return apiBaseInput.value.replace(/\/$/, "");
}
async function loadStatus() {
const response = await fetch(`${apiBase()}/admin/bridge-status`, {
headers: {
Authorization: `Bearer ${apiKeyInput.value.trim()}`,
Accept: "application/json",
},
});
const payload = await response.json();
document.querySelector("#statusOutput").textContent = JSON.stringify(payload, null, 2);
}
document.querySelector("#statusBtn").addEventListener("click", loadStatus);

72
manager/styles.css Normal file
View File

@ -0,0 +1,72 @@
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #020617;
color: #e2e8f0;
}
body {
margin: 0;
}
.shell {
max-width: 1040px;
margin: 0 auto;
padding: 40px 20px;
}
.hero,
.card {
border: 1px solid #1e293b;
border-radius: 22px;
padding: 26px;
margin-bottom: 20px;
background: rgba(15, 23, 42, 0.88);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
}
.hero {
background: linear-gradient(135deg, #111827, #7c2d12);
}
.eyebrow {
color: #fdba74;
text-transform: uppercase;
letter-spacing: 0.14em;
}
label {
display: grid;
gap: 8px;
margin: 12px 0;
font-weight: 700;
}
input {
border: 1px solid #334155;
border-radius: 12px;
background: #020617;
color: #e2e8f0;
padding: 12px;
font: inherit;
}
button {
border: 0;
border-radius: 999px;
background: #ea580c;
color: white;
padding: 12px 18px;
font-weight: 800;
cursor: pointer;
}
pre {
overflow: auto;
background: #020617;
color: #bfdbfe;
border: 1px solid #1e293b;
border-radius: 14px;
padding: 16px;
min-height: 140px;
}

15
protal/README.md Normal file
View File

@ -0,0 +1,15 @@
# Aurask Protal
`protal` is the user-facing panel directory. The name follows the requested root directory spelling.
Current MVP:
- Static HTML panel
- Talks directly to Aurask API
- Supports demo bootstrap, quota lookup, workspace creation, and template workflow run
Open `index.html` in a browser after starting:
```bash
uv run aurask serve --reset --host 127.0.0.1 --port 8080
```

49
protal/index.html Normal file
View File

@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aurask User Protal</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main class="shell">
<section class="hero">
<p class="eyebrow">Aurask User Protal</p>
<h1>Launch safe AI employee workflows</h1>
<p>Bootstrap a tenant, create a knowledge workspace, and run an approved template through the Aurask gateway.</p>
</section>
<section class="card">
<h2>Connection</h2>
<label>API Base URL <input id="apiBase" value="http://127.0.0.1:8080" /></label>
<label>API Key <input id="apiKey" placeholder="Bootstrap to fill automatically" /></label>
<button id="bootstrapBtn">Bootstrap Demo</button>
</section>
<section class="grid">
<div class="card">
<h2>Quota</h2>
<button id="quotaBtn">Load Quota</button>
<pre id="quotaOutput"></pre>
</div>
<div class="card">
<h2>Workspace</h2>
<label>Name <input id="workspaceName" value="Support KB" /></label>
<button id="workspaceBtn">Create Workspace</button>
<pre id="workspaceOutput"></pre>
</div>
</section>
<section class="card">
<h2>Run Template</h2>
<label>Template ID <input id="templateId" value="tpl_email_assistant" /></label>
<label>Workspace ID <input id="workspaceId" placeholder="Required for RAG templates" /></label>
<label>Prompt <textarea id="prompt">Draft a friendly refund policy reply.</textarea></label>
<button id="runBtn">Run Workflow</button>
<pre id="runOutput"></pre>
</section>
</main>
<script src="./main.js"></script>
</body>
</html>

55
protal/main.js Normal file
View File

@ -0,0 +1,55 @@
const apiBaseInput = document.querySelector("#apiBase");
const apiKeyInput = document.querySelector("#apiKey");
function apiBase() {
return apiBaseInput.value.replace(/\/$/, "");
}
async function request(path, options = {}) {
const headers = new Headers(options.headers || {});
headers.set("Content-Type", "application/json");
const apiKey = apiKeyInput.value.trim();
if (apiKey) headers.set("Authorization", `Bearer ${apiKey}`);
const response = await fetch(`${apiBase()}${path}`, { ...options, headers });
const payload = await response.json();
if (!response.ok) throw new Error(JSON.stringify(payload, null, 2));
return payload;
}
function render(target, payload) {
document.querySelector(target).textContent = JSON.stringify(payload, null, 2);
}
document.querySelector("#bootstrapBtn").addEventListener("click", async () => {
const payload = await request("/demo/bootstrap", {
method: "POST",
body: JSON.stringify({ tenant_name: "Aurask Protal Demo", email: "owner@example.com" }),
});
apiKeyInput.value = payload.api_key;
document.querySelector("#workspaceId").value = payload.workspace.id;
render("#quotaOutput", payload.quota);
render("#workspaceOutput", payload.workspace);
});
document.querySelector("#quotaBtn").addEventListener("click", async () => {
render("#quotaOutput", await request("/quota"));
});
document.querySelector("#workspaceBtn").addEventListener("click", async () => {
const payload = await request("/workspaces", {
method: "POST",
body: JSON.stringify({ name: document.querySelector("#workspaceName").value }),
});
document.querySelector("#workspaceId").value = payload.id;
render("#workspaceOutput", payload);
});
document.querySelector("#runBtn").addEventListener("click", async () => {
const workspaceId = document.querySelector("#workspaceId").value.trim();
const body = {
template_id: document.querySelector("#templateId").value,
inputs: { prompt: document.querySelector("#prompt").value },
};
if (workspaceId) body.workspace_id = workspaceId;
render("#runOutput", await request("/workflow-runs", { method: "POST", body: JSON.stringify(body) }));
});

83
protal/styles.css Normal file
View File

@ -0,0 +1,83 @@
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f6f7fb;
color: #182033;
}
body {
margin: 0;
}
.shell {
max-width: 1120px;
margin: 0 auto;
padding: 40px 20px;
}
.hero {
padding: 32px;
border-radius: 24px;
background: linear-gradient(135deg, #172554, #2563eb);
color: white;
margin-bottom: 24px;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.14em;
opacity: 0.8;
}
.grid {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 20px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.06);
}
label {
display: grid;
gap: 8px;
margin: 12px 0;
font-weight: 600;
}
input,
textarea {
border: 1px solid #cbd5e1;
border-radius: 12px;
padding: 12px;
font: inherit;
}
textarea {
min-height: 100px;
}
button {
border: 0;
border-radius: 999px;
background: #2563eb;
color: white;
padding: 12px 18px;
font-weight: 700;
cursor: pointer;
}
pre {
overflow: auto;
background: #0f172a;
color: #dbeafe;
border-radius: 14px;
padding: 16px;
min-height: 72px;
}

View File

@ -15,3 +15,6 @@ aurask = "aurask:main"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["api/aurask"]

46
tests/test_bridges.py Normal file
View File

@ -0,0 +1,46 @@
from __future__ import annotations
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "api"))
from aurask.bridge_status import bridge_status
from aurask.bridges.config import BridgeConfig
from aurask.bridges.pgvector import PGVectorBridge
from aurask.bridges.postgres import PostgresBridge
from aurask.bridges.redis_bridge import RedisBridge
class BridgeContractTests(unittest.TestCase):
def test_bridge_status_reports_all_required_components(self) -> None:
status = bridge_status()
self.assertIn("postgres", status)
self.assertIn("pgvector", status)
self.assertIn("redis", status)
self.assertIn("anythingllm", status)
self.assertIn("langflow", status)
def test_pgvector_contract_requires_tenant_workspace_filters(self) -> None:
bridge = PGVectorBridge(BridgeConfig.from_env().pgvector)
contract = bridge.search_contract("tenant_1", "workspace_1")
self.assertEqual(contract["filters"]["tenant_id"], "tenant_1")
self.assertEqual(contract["filters"]["workspace_id"], "workspace_1")
self.assertIn("query_embedding", contract["required"])
def test_redis_keys_are_tenant_scoped(self) -> None:
bridge = RedisBridge(BridgeConfig.from_env().redis)
key = bridge.idempotency_key("tenant_1", "payment", "tx_hash")
self.assertIn("tenant_1", key)
self.assertIn("payment", key)
def test_postgres_schema_includes_vector_extension(self) -> None:
bridge = PostgresBridge(BridgeConfig.from_env().postgres)
schema = bridge.migration_plan()["schema_sql"]
self.assertIn("CREATE EXTENSION IF NOT EXISTS vector", schema)
self.assertIn("quota_ledger", schema)
if __name__ == "__main__":
unittest.main()

View File

@ -1,6 +1,10 @@
from __future__ import annotations from __future__ import annotations
import sys
import unittest import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "api"))
from aurask.app import create_app from aurask.app import create_app
from aurask.errors import QuotaError from aurask.errors import QuotaError