diff --git a/AGENTS.md b/AGENTS.md index 4151b35..855987c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,21 +5,27 @@ ## 1. 当前项目状态 -当前仓库是 Aurask 的 **Python 模块化单体 MVP**。 +当前仓库是 Aurask 的 **Python 模块化单体 MVP**,根目录按产品边界划分为: + +- `api`:后端服务、PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 桥接配置,以及前端到后端请求契约。 +- `protal`:用户前端使用面板。目录名按当前需求保留 `protal` 拼写。 +- `manager`:管理员前端使用面板。 +- `deploy`:k3s 与后续部署配置。 已实现能力: -- 租户、用户与 API Key:`src/aurask/auth.py` -- HTTP 网关:`src/aurask/api.py` -- 套餐与商品目录:`src/aurask/plans.py` -- 订单、订阅与权益发放:`src/aurask/billing.py` -- TBU 额度、预扣、结算与账本:`src/aurask/quota.py` -- USDT-TRC20 支付匹配:`src/aurask/payments.py` -- 模板工作流编排:`src/aurask/orchestrator.py` -- AnythingLLM Workspace / 文档接入适配:`src/aurask/knowledge_base.py` -- 审计事件:`src/aurask/audit.py` -- MVP JSON 持久化:`src/aurask/repository.py` -- CLI:`src/aurask/cli.py` +- 租户、用户与 API Key:`api/aurask/auth.py` +- HTTP 网关:`api/aurask/api.py` +- 套餐与商品目录:`api/aurask/plans.py` +- 订单、订阅与权益发放:`api/aurask/billing.py` +- TBU 额度、预扣、结算与账本:`api/aurask/quota.py` +- USDT-TRC20 支付匹配:`api/aurask/payments.py` +- 模板工作流编排:`api/aurask/orchestrator.py` +- AnythingLLM Workspace / 文档接入适配:`api/aurask/knowledge_base.py` +- PostgreSQL / PGVector / Redis / AnythingLLM / Langflow 桥接:`api/aurask/bridges/` +- 审计事件:`api/aurask/audit.py` +- MVP JSON 持久化:`api/aurask/repository.py` +- CLI:`api/aurask/cli.py` - 测试:`tests/test_mvp.py` - k3s 部署规划:`deploy/k3s/README.md` @@ -43,11 +49,15 @@ 当前目录职责如下: ```text -src/aurask/ +api/ + README.md # API 与外部组件桥接说明 + requests/ # 前端到后端请求样例 + aurask/ __init__.py # 包入口,导出 CLI main __main__.py # python -m aurask app.py # 应用装配,不写具体业务规则 api.py # HTTP 网关与路由映射 + bridge_status.py # 组件桥接状态 cli.py # 命令行入口 repository.py # MVP 持久化层 errors.py # 统一错误类型 @@ -60,6 +70,11 @@ src/aurask/ payments.py # 支付匹配、tx_hash 幂等 orchestrator.py # 工作流模板与运行编排 knowledge_base.py # Workspace 与文档接入 + bridges/ # PostgreSQL/PGVector/Redis/AnythingLLM/Langflow 桥接 +protal/ + index.html # 用户面板 +manager/ + index.html # 管理员面板 tests/ test_mvp.py # 核心闭环测试 deploy/k3s/ @@ -163,6 +178,15 @@ Aurask 目标架构: - 允许为演示补功能,但不要把它描述为生产数据库。 - 生产化时应新增 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 约束 ### Langflow @@ -220,7 +244,7 @@ uv run aurask demo --reset 如果当前环境缺少 `uv`,可使用: ```bash -$env:PYTHONPATH='src'; py -3 -m aurask demo --reset +$env:PYTHONPATH='api'; py -3 -m aurask demo --reset ``` ## 10. 文档同步要求 diff --git a/Aurask_Technical_Operations_Plan.md b/Aurask_Technical_Operations_Plan.md index 31acf07..54f986c 100644 --- a/Aurask_Technical_Operations_Plan.md +++ b/Aurask_Technical_Operations_Plan.md @@ -9,6 +9,8 @@ Aurask 当前已完成 **可运行 MVP 后端骨架**: - 使用 Python 模块化单体实现首版领域闭环。 - 已具备 `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 网关。 - 已新增面向 `300` 名月度活跃用户的 `k3s` 部署规划:`deploy/k3s/README.md`。 @@ -35,11 +37,16 @@ Aurask 是面向海外个人开发者、学生、独立创业者和小团队的 ### 2.1 当前目录结构 ```text -src/aurask/ +api/ + README.md # API 与外部组件桥接说明 + requests/ + aurask-api.http # 前端到后端请求样例 + aurask/ __init__.py # 包入口,导出 CLI main __main__.py # python -m aurask 入口 app.py # 应用装配与 demo bootstrap api.py # 标准库 HTTP 网关 + bridge_status.py # 组件桥接状态 cli.py # aurask demo / aurask serve repository.py # MVP JSON 持久化 errors.py # 应用错误类型 @@ -52,6 +59,21 @@ src/aurask/ payments.py # USDT-TRC20 支付匹配 orchestrator.py # 模板工作流编排与 Langflow Runtime 适配 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/ test_mvp.py # MVP 核心流程测试 deploy/k3s/ @@ -92,13 +114,48 @@ python -m unittest discover -s tests -v | --- | --- | --- | | 持久化 | 本地 JSON 文件 | PostgreSQL + PGVector | | 队列 | 同进程同步执行 | Redis / RabbitMQ / NATS 队列 | -| Langflow | `LangflowRuntimeAdapter` 模拟适配层 | 内部 `ClusterIP` Langflow Runtime Pool | -| AnythingLLM | `AnythingLLMAdapter` 模拟适配层 | 内部 `ClusterIP` AnythingLLM API | +| Langflow | 默认模拟适配层,可用 `LangflowBridge` 接真实服务 | 内部 `ClusterIP` Langflow Runtime Pool | +| AnythingLLM | 默认模拟适配层,可用 `AnythingLLMBridge` 接真实服务 | 内部 `ClusterIP` AnythingLLM API | | 网关 | Python 标准库 HTTP Server | FastAPI / ASGI + Ingress + HPA | | 支付 | 人工提交 tx hash 匹配 | TronGrid / Tronscan / 自建节点监听 | | 观测 | 审计事件写入 store | Prometheus + Loki + Grafana + Alertmanager | | 部署 | 本地运行 | 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= +AURASK_LANGFLOW_BASE_URL=http://langflow-runtime.aurask-runtime.svc.cluster.local:7860 +AURASK_LANGFLOW_API_KEY= +``` + +桥接模块: + +- `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. 目标技术架构 ```text @@ -205,10 +262,10 @@ AnythingLLM 负责 Workspace、文档、RAG 和聊天历史。 ### 5.3 当前代码对应 -- 套餐定义:`src/aurask/plans.py` -- 额度账户:`src/aurask/quota.py` -- 订单与权益:`src/aurask/billing.py` -- 使用记录:`src/aurask/orchestrator.py` +- 套餐定义:`api/aurask/plans.py` +- 额度账户:`api/aurask/quota.py` +- 订单与权益:`api/aurask/billing.py` +- 使用记录:`api/aurask/orchestrator.py` ## 6. 支付与订单闭环 diff --git a/README.md b/README.md index ab75c61..6cb648e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Aurask -Aurask 首版已按 `Aurask_Technical_Operations_Plan.md` 落地为一个可运行的模块化单体后端,覆盖以下核心边界: +Aurask 首版已按 `Aurask_Technical_Operations_Plan.md` 落地为一个可运行的模块化单体后端,并按产品边界拆分为 `api`、`protal`、`manager`、`deploy` 四个根目录。 - `Auth + API Gateway` - `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`,可用: + +```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 `aurask demo` 会自动完成: @@ -59,6 +67,7 @@ uv run aurask serve --reset --host 127.0.0.1 --port 8080 - `POST /payments/match` - `POST /workflow-runs` - `GET /workflow-runs/{run_id}` +- `GET /admin/bridge-status` 鉴权方式: @@ -110,17 +119,22 @@ python -m unittest discover -s tests -v ### Deployment - `deploy/k3s/README.md`: 面向 `300` 名月度活跃用户的 `k3s` 部署方案 +- `api/README.md`: PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 桥接配置 +- `api/requests/aurask-api.http`: 前端到后端请求样例 ### Project layout - `AGENTS.md`: 项目级实现约束 - `Aurask_Technical_Operations_Plan.md`: 技术与运营方案 -- `src/aurask/app.py`: 应用装配 -- `src/aurask/api.py`: HTTP 网关 -- `src/aurask/auth.py`: 租户、用户与 API Key -- `src/aurask/billing.py`: 套餐、订单与权益发放 -- `src/aurask/quota.py`: TBU 预扣、结算与额度账本 -- `src/aurask/orchestrator.py`: 模板工作流编排 -- `src/aurask/knowledge_base.py`: Workspace 与文档接入 -- `src/aurask/payments.py`: USDT-TRC20 支付匹配 +- `api/aurask/app.py`: 应用装配 +- `api/aurask/api.py`: HTTP 网关 +- `api/aurask/bridges/`: PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 桥接配置 +- `api/aurask/auth.py`: 租户、用户与 API Key +- `api/aurask/billing.py`: 套餐、订单与权益发放 +- `api/aurask/quota.py`: TBU 预扣、结算与额度账本 +- `api/aurask/orchestrator.py`: 模板工作流编排 +- `api/aurask/knowledge_base.py`: Workspace 与文档接入 +- `api/aurask/payments.py`: USDT-TRC20 支付匹配 +- `protal/`: 用户前端使用面板 +- `manager/`: 管理员前端使用面板 - `tests/`: 单元测试 diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..ec80c77 --- /dev/null +++ b/api/README.md @@ -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="" +$env:AURASK_LANGFLOW_BASE_URL="http://langflow-runtime.aurask-runtime.svc.cluster.local:7860" +$env:AURASK_LANGFLOW_API_KEY="" +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 +``` diff --git a/src/aurask/__init__.py b/api/aurask/__init__.py similarity index 100% rename from src/aurask/__init__.py rename to api/aurask/__init__.py diff --git a/src/aurask/__main__.py b/api/aurask/__main__.py similarity index 100% rename from src/aurask/__main__.py rename to api/aurask/__main__.py diff --git a/src/aurask/api.py b/api/aurask/api.py similarity index 97% rename from src/aurask/api.py rename to api/aurask/api.py index 61f9883..584bb2d 100644 --- a/src/aurask/api.py +++ b/api/aurask/api.py @@ -8,6 +8,7 @@ from typing import Any from urllib.parse import urlparse from aurask.app import AuraskApp +from aurask.bridge_status import bridge_status from aurask.errors import AuraskError @@ -55,6 +56,9 @@ def make_handler(app: AuraskApp): tenant_id = context["tenant"]["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": self._send(200, app.quota.get_account(tenant_id)) return diff --git a/src/aurask/app.py b/api/aurask/app.py similarity index 72% rename from src/aurask/app.py rename to api/aurask/app.py index d015409..aff026c 100644 --- a/src/aurask/app.py +++ b/api/aurask/app.py @@ -2,12 +2,16 @@ from __future__ import annotations +import os from dataclasses import dataclass from pathlib import Path from aurask.audit import AuditService from aurask.auth import AuthService 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.orchestrator import WorkflowOrchestrator 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) if reset: 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) auth = AuthService(store, audit) quota = QuotaService(store, audit) billing = BillingService(store, quota, audit) payments = PaymentService(store, billing, audit) - knowledge = KnowledgeBaseService(store, quota, audit) - orchestrator = WorkflowOrchestrator(store, quota, audit) + anythingllm_adapter = AnythingLLMBridge(bridge_config.anythingllm) if use_external_bridges else None + 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) orchestrator.seed_templates() return AuraskApp( diff --git a/src/aurask/audit.py b/api/aurask/audit.py similarity index 100% rename from src/aurask/audit.py rename to api/aurask/audit.py diff --git a/src/aurask/auth.py b/api/aurask/auth.py similarity index 100% rename from src/aurask/auth.py rename to api/aurask/auth.py diff --git a/src/aurask/billing.py b/api/aurask/billing.py similarity index 100% rename from src/aurask/billing.py rename to api/aurask/billing.py diff --git a/api/aurask/bridge_status.py b/api/aurask/bridge_status.py new file mode 100644 index 0000000..ebfed58 --- /dev/null +++ b/api/aurask/bridge_status.py @@ -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), + }, + } diff --git a/api/aurask/bridges/__init__.py b/api/aurask/bridges/__init__.py new file mode 100644 index 0000000..509198e --- /dev/null +++ b/api/aurask/bridges/__init__.py @@ -0,0 +1,5 @@ +"""External service bridge adapters for Aurask production integration.""" + +from aurask.bridges.config import BridgeConfig + +__all__ = ["BridgeConfig"] diff --git a/api/aurask/bridges/anythingllm.py b/api/aurask/bridges/anythingllm.py new file mode 100644 index 0000000..01a84a0 --- /dev/null +++ b/api/aurask/bridges/anythingllm.py @@ -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"), + } diff --git a/api/aurask/bridges/config.py b/api/aurask/bridges/config.py new file mode 100644 index 0000000..9e7d535 --- /dev/null +++ b/api/aurask/bridges/config.py @@ -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")), + ), + ) diff --git a/api/aurask/bridges/http.py b/api/aurask/bridges/http.py new file mode 100644 index 0000000..1dd8a75 --- /dev/null +++ b/api/aurask/bridges/http.py @@ -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 diff --git a/api/aurask/bridges/langflow.py b/api/aurask/bridges/langflow.py new file mode 100644 index 0000000..267107f --- /dev/null +++ b/api/aurask/bridges/langflow.py @@ -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) diff --git a/api/aurask/bridges/pgvector.py b/api/aurask/bridges/pgvector.py new file mode 100644 index 0000000..b75ebb6 --- /dev/null +++ b/api/aurask/bridges/pgvector.py @@ -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"], + } diff --git a/api/aurask/bridges/postgres.py b/api/aurask/bridges/postgres.py new file mode 100644 index 0000000..e739131 --- /dev/null +++ b/api/aurask/bridges/postgres.py @@ -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(), + } diff --git a/api/aurask/bridges/redis_bridge.py b/api/aurask/bridges/redis_bridge.py new file mode 100644 index 0000000..61b3418 --- /dev/null +++ b/api/aurask/bridges/redis_bridge.py @@ -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}" diff --git a/src/aurask/cli.py b/api/aurask/cli.py similarity index 100% rename from src/aurask/cli.py rename to api/aurask/cli.py diff --git a/src/aurask/errors.py b/api/aurask/errors.py similarity index 100% rename from src/aurask/errors.py rename to api/aurask/errors.py diff --git a/src/aurask/ids.py b/api/aurask/ids.py similarity index 100% rename from src/aurask/ids.py rename to api/aurask/ids.py diff --git a/src/aurask/knowledge_base.py b/api/aurask/knowledge_base.py similarity index 100% rename from src/aurask/knowledge_base.py rename to api/aurask/knowledge_base.py diff --git a/src/aurask/orchestrator.py b/api/aurask/orchestrator.py similarity index 100% rename from src/aurask/orchestrator.py rename to api/aurask/orchestrator.py diff --git a/src/aurask/payments.py b/api/aurask/payments.py similarity index 100% rename from src/aurask/payments.py rename to api/aurask/payments.py diff --git a/src/aurask/plans.py b/api/aurask/plans.py similarity index 100% rename from src/aurask/plans.py rename to api/aurask/plans.py diff --git a/src/aurask/quota.py b/api/aurask/quota.py similarity index 100% rename from src/aurask/quota.py rename to api/aurask/quota.py diff --git a/src/aurask/repository.py b/api/aurask/repository.py similarity index 100% rename from src/aurask/repository.py rename to api/aurask/repository.py diff --git a/api/requests/aurask-api.http b/api/requests/aurask-api.http new file mode 100644 index 0000000..3d60178 --- /dev/null +++ b/api/requests/aurask-api.http @@ -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}} diff --git a/manager/README.md b/manager/README.md new file mode 100644 index 0000000..194aa34 --- /dev/null +++ b/manager/README.md @@ -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. diff --git a/manager/index.html b/manager/index.html new file mode 100644 index 0000000..c7ef4b3 --- /dev/null +++ b/manager/index.html @@ -0,0 +1,31 @@ + + + + + + Aurask Manager + + + +
+
+

Aurask Manager

+

Operate tenant-safe AI workflow infrastructure

+

Inspect bridge configuration and verify production integration readiness.

+
+ +
+

Admin Connection

+ + + +
+ +
+

Bridge Status

+

+      
+
+ + + diff --git a/manager/main.js b/manager/main.js new file mode 100644 index 0000000..83ea6d9 --- /dev/null +++ b/manager/main.js @@ -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); diff --git a/manager/styles.css b/manager/styles.css new file mode 100644 index 0000000..08852d9 --- /dev/null +++ b/manager/styles.css @@ -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; +} diff --git a/protal/README.md b/protal/README.md new file mode 100644 index 0000000..c340151 --- /dev/null +++ b/protal/README.md @@ -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 +``` diff --git a/protal/index.html b/protal/index.html new file mode 100644 index 0000000..386efad --- /dev/null +++ b/protal/index.html @@ -0,0 +1,49 @@ + + + + + + Aurask User Protal + + + +
+
+

Aurask User Protal

+

Launch safe AI employee workflows

+

Bootstrap a tenant, create a knowledge workspace, and run an approved template through the Aurask gateway.

+
+ +
+

Connection

+ + + +
+ +
+
+

Quota

+ +

+        
+
+

Workspace

+ + +

+        
+
+ +
+

Run Template

+ + + + +

+      
+
+ + + diff --git a/protal/main.js b/protal/main.js new file mode 100644 index 0000000..a469668 --- /dev/null +++ b/protal/main.js @@ -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) })); +}); diff --git a/protal/styles.css b/protal/styles.css new file mode 100644 index 0000000..54f50c4 --- /dev/null +++ b/protal/styles.css @@ -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; +} diff --git a/pyproject.toml b/pyproject.toml index 18f0c4e..c8694ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,3 +15,6 @@ aurask = "aurask:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["api/aurask"] diff --git a/tests/test_bridges.py b/tests/test_bridges.py new file mode 100644 index 0000000..220ca19 --- /dev/null +++ b/tests/test_bridges.py @@ -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() diff --git a/tests/test_mvp.py b/tests/test_mvp.py index 48cc3ea..e49e4ed 100644 --- a/tests/test_mvp.py +++ b/tests/test_mvp.py @@ -1,6 +1,10 @@ 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.app import create_app from aurask.errors import QuotaError