mirror of
https://18126008609:longquanjian123@gitee.com/feigong123/aurask.git
synced 2026-04-19 15:00:35 +00:00
Add service bridges and frontend panel structure
This commit is contained in:
parent
0a3f0585f4
commit
66af3b44d9
52
AGENTS.md
52
AGENTS.md
@ -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. 文档同步要求
|
||||||
|
|||||||
@ -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. 支付与订单闭环
|
||||||
|
|
||||||
|
|||||||
32
README.md
32
README.md
@ -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
50
api/README.md
Normal 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>
|
||||||
|
```
|
||||||
@ -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
|
||||||
@ -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(
|
||||||
37
api/aurask/bridge_status.py
Normal file
37
api/aurask/bridge_status.py
Normal 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),
|
||||||
|
},
|
||||||
|
}
|
||||||
5
api/aurask/bridges/__init__.py
Normal file
5
api/aurask/bridges/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""External service bridge adapters for Aurask production integration."""
|
||||||
|
|
||||||
|
from aurask.bridges.config import BridgeConfig
|
||||||
|
|
||||||
|
__all__ = ["BridgeConfig"]
|
||||||
46
api/aurask/bridges/anythingllm.py
Normal file
46
api/aurask/bridges/anythingllm.py
Normal 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"),
|
||||||
|
}
|
||||||
80
api/aurask/bridges/config.py
Normal file
80
api/aurask/bridges/config.py
Normal 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")),
|
||||||
|
),
|
||||||
|
)
|
||||||
41
api/aurask/bridges/http.py
Normal file
41
api/aurask/bridges/http.py
Normal 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
|
||||||
49
api/aurask/bridges/langflow.py
Normal file
49
api/aurask/bridges/langflow.py
Normal 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)
|
||||||
43
api/aurask/bridges/pgvector.py
Normal file
43
api/aurask/bridges/pgvector.py
Normal 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"],
|
||||||
|
}
|
||||||
99
api/aurask/bridges/postgres.py
Normal file
99
api/aurask/bridges/postgres.py
Normal 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(),
|
||||||
|
}
|
||||||
25
api/aurask/bridges/redis_bridge.py
Normal file
25
api/aurask/bridges/redis_bridge.py
Normal 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}"
|
||||||
86
api/requests/aurask-api.http
Normal file
86
api/requests/aurask-api.http
Normal 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
17
manager/README.md
Normal 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
31
manager/index.html
Normal 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
19
manager/main.js
Normal 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
72
manager/styles.css
Normal 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
15
protal/README.md
Normal 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
49
protal/index.html
Normal 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
55
protal/main.js
Normal 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
83
protal/styles.css
Normal 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;
|
||||||
|
}
|
||||||
@ -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
46
tests/test_bridges.py
Normal 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()
|
||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user