chore: initialize V2 backend foundation
This commit is contained in:
@@ -2,6 +2,11 @@ backend/.venv/
|
||||
frontend/.next/
|
||||
frontend/.next-dev-log/
|
||||
frontend/node_modules/
|
||||
services/*/.venv/
|
||||
services/*/.pytest_cache/
|
||||
apps/*/.next/
|
||||
apps/*/node_modules/
|
||||
.pytest_cache/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
@@ -26,6 +31,8 @@ backend/project-docs/qa-artifacts/
|
||||
output/
|
||||
frontend/next-env.d.ts
|
||||
frontend/tsconfig.tsbuildinfo
|
||||
apps/*/next-env.d.ts
|
||||
apps/*/tsconfig.tsbuildinfo
|
||||
|
||||
# Temporary directories
|
||||
temp-*/
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# V2 Local Development
|
||||
|
||||
## Run Tests
|
||||
|
||||
```powershell
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
## Run APIs Without Docker
|
||||
|
||||
Contract API:
|
||||
|
||||
```powershell
|
||||
$env:PYTHONPATH='services/contract-api/src'
|
||||
python -m uvicorn yuqei_contract_api.main:app --host 0.0.0.0 --port 8100
|
||||
```
|
||||
|
||||
AI Platform API:
|
||||
|
||||
```powershell
|
||||
$env:PYTHONPATH='services/ai-platform-api/src'
|
||||
python -m uvicorn yuqei_ai_platform_api.main:app --host 0.0.0.0 --port 8101
|
||||
```
|
||||
|
||||
Worker health:
|
||||
|
||||
```powershell
|
||||
$env:PYTHONPATH='services/worker/src'
|
||||
python -m yuqei_worker.main --health
|
||||
```
|
||||
|
||||
## Run Infrastructure With Docker
|
||||
|
||||
```powershell
|
||||
Copy-Item infra/compose/.env.example infra/compose/.env -ErrorAction SilentlyContinue
|
||||
docker compose --env-file infra/compose/.env -f infra/compose/docker-compose.yml up --build
|
||||
```
|
||||
|
||||
Health checks:
|
||||
|
||||
- Contract API: `http://localhost:8100/health`
|
||||
- AI Platform API: `http://localhost:8101/health`
|
||||
- MinIO Console: `http://localhost:9001`
|
||||
@@ -0,0 +1,5 @@
|
||||
POSTGRES_USER=yuqei
|
||||
POSTGRES_PASSWORD=yuqei-local
|
||||
POSTGRES_DB=yuqei_v2
|
||||
MINIO_ROOT_USER=yuqei-minio
|
||||
MINIO_ROOT_PASSWORD=yuqei-minio-local
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
name: yuqei-contract-v2
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-yuqei}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-yuqei-local}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-yuqei_v2}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-yuqei} -d $${POSTGRES_DB:-yuqei_v2}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-yuqei-minio}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-yuqei-minio-local}
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
contract-api:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: infra/docker/contract-api.Dockerfile
|
||||
environment:
|
||||
CONTRACT_API_ENVIRONMENT: local
|
||||
CONTRACT_API_DATABASE_URL: postgresql+psycopg://yuqei:yuqei-local@postgres:5432/yuqei_v2
|
||||
CONTRACT_API_REDIS_URL: redis://redis:6379/0
|
||||
CONTRACT_API_AI_PLATFORM_BASE_URL: http://ai-platform-api:8101
|
||||
ports:
|
||||
- "8100:8100"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
ai-platform-api:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: infra/docker/ai-platform-api.Dockerfile
|
||||
environment:
|
||||
AI_PLATFORM_ENVIRONMENT: local
|
||||
AI_PLATFORM_DATABASE_URL: postgresql+psycopg://yuqei:yuqei-local@postgres:5432/yuqei_v2
|
||||
AI_PLATFORM_REDIS_URL: redis://redis:6379/1
|
||||
AI_PLATFORM_PROVIDER_CONFIG_SOURCE: database
|
||||
ports:
|
||||
- "8101:8101"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: infra/docker/worker.Dockerfile
|
||||
environment:
|
||||
YUQEI_WORKER_ENVIRONMENT: local
|
||||
YUQEI_WORKER_REDIS_URL: redis://redis:6379/2
|
||||
YUQEI_WORKER_OBJECT_STORAGE_ENDPOINT: http://minio:9000
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
minio-data:
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONPATH=/app/services/ai-platform-api/src
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY services/ai-platform-api/pyproject.toml /tmp/ai-platform-api-pyproject.toml
|
||||
RUN pip install --no-cache-dir fastapi pydantic pydantic-settings sqlalchemy uvicorn
|
||||
|
||||
COPY services/ai-platform-api services/ai-platform-api
|
||||
|
||||
EXPOSE 8101
|
||||
CMD ["python", "-m", "uvicorn", "yuqei_ai_platform_api.main:app", "--host", "0.0.0.0", "--port", "8101"]
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONPATH=/app/services/contract-api/src
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY services/contract-api/pyproject.toml /tmp/contract-api-pyproject.toml
|
||||
RUN pip install --no-cache-dir fastapi pydantic pydantic-settings sqlalchemy uvicorn
|
||||
|
||||
COPY services/contract-api services/contract-api
|
||||
|
||||
EXPOSE 8100
|
||||
CMD ["python", "-m", "uvicorn", "yuqei_contract_api.main:app", "--host", "0.0.0.0", "--port", "8100"]
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONPATH=/app/services/worker/src
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY services/worker/pyproject.toml /tmp/worker-pyproject.toml
|
||||
RUN pip install --no-cache-dir pydantic pydantic-settings
|
||||
|
||||
COPY services/worker services/worker
|
||||
|
||||
CMD ["python", "-m", "yuqei_worker.main", "--health"]
|
||||
@@ -0,0 +1,108 @@
|
||||
# V2 后端基础工程与本地开发底座记录
|
||||
|
||||
日期:2026-06-22
|
||||
|
||||
## 1. 本轮目标
|
||||
|
||||
承接 V2 重构任务编排中的 T0-4 和 T0-6,先建立可运行、可测试、可部署演进的后端基础工程,避免直接进入业务页面和 AI 复杂能力开发。
|
||||
|
||||
## 2. 已完成内容
|
||||
|
||||
### Contract API
|
||||
|
||||
目录:`services/contract-api`
|
||||
|
||||
已建立:
|
||||
|
||||
- FastAPI 应用入口。
|
||||
- `/health` 和 `/api/v1/health`。
|
||||
- Pydantic Settings 配置加载。
|
||||
- 基础日志配置。
|
||||
- `.env.example`。
|
||||
- 单元测试。
|
||||
- 服务 README。
|
||||
|
||||
### AI Platform API
|
||||
|
||||
目录:`services/ai-platform-api`
|
||||
|
||||
已建立:
|
||||
|
||||
- FastAPI 应用入口。
|
||||
- `/health` 和 `/api/v1/health`。
|
||||
- Pydantic Settings 配置加载。
|
||||
- 基础日志配置。
|
||||
- `.env.example`。
|
||||
- 单元测试。
|
||||
- 服务 README。
|
||||
|
||||
### Worker
|
||||
|
||||
目录:`services/worker`
|
||||
|
||||
已建立:
|
||||
|
||||
- worker 配置加载。
|
||||
- 基础日志配置。
|
||||
- `--health` 健康输出。
|
||||
- `.env.example`。
|
||||
- 单元测试。
|
||||
- 服务 README。
|
||||
|
||||
### 本地开发底座
|
||||
|
||||
已建立:
|
||||
|
||||
- 根目录 `pyproject.toml`,统一 pytest 入口。
|
||||
- `infra/compose/docker-compose.yml`。
|
||||
- `infra/compose/.env.example`。
|
||||
- 三个服务 Dockerfile。
|
||||
- `docs/architecture/local-development.md`。
|
||||
- V2 目录相关 `.gitignore` 规则。
|
||||
|
||||
## 3. 验证结果
|
||||
|
||||
已通过:
|
||||
|
||||
```powershell
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
6 passed
|
||||
```
|
||||
|
||||
已通过:
|
||||
|
||||
```powershell
|
||||
git diff --check
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 当前机器未安装 Docker CLI,所以本轮未执行 `docker compose config` 或实际容器启动。
|
||||
- Docker/Compose 文件已作为开发底座提交,后续在具备 Docker 的机器或服务器上执行启动验证。
|
||||
|
||||
## 4. 当前边界
|
||||
|
||||
本轮只建立工程底座,不包含:
|
||||
|
||||
- 数据库表结构和 Alembic migration。
|
||||
- AI provider 真实调用。
|
||||
- 法律知识库检索。
|
||||
- 合同业务 API。
|
||||
- 前端 Next.js 工程。
|
||||
|
||||
## 5. 下一步建议
|
||||
|
||||
继续执行 T0-5:初始化前端基础工程和设计系统底座。
|
||||
|
||||
优先顺序:
|
||||
|
||||
1. `apps/contract-web` 初始化 Next.js/React/TypeScript。
|
||||
2. `packages/design-system` 建立 tokens、主导航、页面壳、工作台布局规范。
|
||||
3. 建立前端本地启动、lint、typecheck、build 命令。
|
||||
4. 前端健康页对接 `contract-api` 的 `/health`。
|
||||
5. 再进入 S2/S3:AI Platform MVP 和 SDK MVP。
|
||||
@@ -0,0 +1,12 @@
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = [
|
||||
"services/contract-api/tests",
|
||||
"services/ai-platform-api/tests",
|
||||
"services/worker/tests",
|
||||
]
|
||||
pythonpath = [
|
||||
"services/contract-api/src",
|
||||
"services/ai-platform-api/src",
|
||||
"services/worker/src",
|
||||
]
|
||||
addopts = "-q --import-mode=importlib"
|
||||
@@ -0,0 +1,5 @@
|
||||
AI_PLATFORM_ENVIRONMENT=local
|
||||
AI_PLATFORM_LOG_LEVEL=INFO
|
||||
AI_PLATFORM_DATABASE_URL=postgresql+psycopg://ai:ai@localhost:5432/yuqei_ai
|
||||
AI_PLATFORM_REDIS_URL=redis://localhost:6379/1
|
||||
AI_PLATFORM_PROVIDER_CONFIG_SOURCE=database
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# AI Platform API
|
||||
|
||||
FastAPI service for V2 shared AI capabilities.
|
||||
|
||||
## Local Run
|
||||
|
||||
```powershell
|
||||
cd services/ai-platform-api
|
||||
python -m uvicorn yuqei_ai_platform_api.main:app --app-dir src --reload --port 8101
|
||||
```
|
||||
|
||||
Health endpoints:
|
||||
|
||||
- `GET /health`
|
||||
- `GET /api/v1/health`
|
||||
|
||||
## Test
|
||||
|
||||
From repository root:
|
||||
|
||||
```powershell
|
||||
python -m pytest services/ai-platform-api/tests
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "yuqei-ai-platform-api"
|
||||
version = "0.1.0"
|
||||
description = "Yuqei shared AI platform V2 API"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastapi>=0.115",
|
||||
"pydantic>=2.10",
|
||||
"pydantic-settings>=2.7",
|
||||
"sqlalchemy>=2.0",
|
||||
"uvicorn>=0.34",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,25 @@
|
||||
from functools import lru_cache
|
||||
from typing import Literal
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class AiPlatformSettings(BaseSettings):
|
||||
service_name: str = "ai-platform-api"
|
||||
environment: Literal["local", "test", "staging", "production"] = "local"
|
||||
log_level: str = "INFO"
|
||||
api_prefix: str = "/api/v1"
|
||||
database_url: str = "postgresql+psycopg://ai:ai@localhost:5432/yuqei_ai"
|
||||
redis_url: str = "redis://localhost:6379/1"
|
||||
provider_config_source: Literal["database", "environment"] = "database"
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_prefix="AI_PLATFORM_",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> AiPlatformSettings:
|
||||
return AiPlatformSettings()
|
||||
@@ -0,0 +1,27 @@
|
||||
import logging
|
||||
from logging.config import dictConfig
|
||||
|
||||
|
||||
def configure_logging(level: str) -> None:
|
||||
dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "default",
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": level.upper(),
|
||||
},
|
||||
}
|
||||
)
|
||||
logging.getLogger("uvicorn.access").setLevel(level.upper())
|
||||
@@ -0,0 +1,47 @@
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
from yuqei_ai_platform_api import __version__
|
||||
from yuqei_ai_platform_api.config import AiPlatformSettings, get_settings
|
||||
from yuqei_ai_platform_api.logging import configure_logging
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: Literal["ok"] = "ok"
|
||||
service: str
|
||||
version: str
|
||||
environment: str
|
||||
provider_config_source: str
|
||||
|
||||
|
||||
def create_app(settings: AiPlatformSettings | None = None) -> FastAPI:
|
||||
resolved_settings = settings or get_settings()
|
||||
configure_logging(resolved_settings.log_level)
|
||||
|
||||
app = FastAPI(
|
||||
title="Yuqei AI Platform API",
|
||||
version=__version__,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
openapi_url="/openapi.json",
|
||||
)
|
||||
|
||||
@app.get("/health", response_model=HealthResponse, tags=["system"])
|
||||
def health() -> HealthResponse:
|
||||
return HealthResponse(
|
||||
service=resolved_settings.service_name,
|
||||
version=__version__,
|
||||
environment=resolved_settings.environment,
|
||||
provider_config_source=resolved_settings.provider_config_source,
|
||||
)
|
||||
|
||||
@app.get(f"{resolved_settings.api_prefix}/health", response_model=HealthResponse, tags=["system"])
|
||||
def api_health() -> HealthResponse:
|
||||
return health()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -0,0 +1 @@
|
||||
"""AI Platform API test package."""
|
||||
@@ -0,0 +1,30 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from yuqei_ai_platform_api.config import AiPlatformSettings
|
||||
from yuqei_ai_platform_api.main import create_app
|
||||
|
||||
|
||||
def test_health_endpoint_returns_ai_platform_status() -> None:
|
||||
app = create_app(AiPlatformSettings(environment="test"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"status": "ok",
|
||||
"service": "ai-platform-api",
|
||||
"version": "0.1.0",
|
||||
"environment": "test",
|
||||
"provider_config_source": "database",
|
||||
}
|
||||
|
||||
|
||||
def test_versioned_health_endpoint_matches_ai_platform_prefix() -> None:
|
||||
app = create_app(AiPlatformSettings(environment="test"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/v1/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["service"] == "ai-platform-api"
|
||||
@@ -0,0 +1,5 @@
|
||||
CONTRACT_API_ENVIRONMENT=local
|
||||
CONTRACT_API_LOG_LEVEL=INFO
|
||||
CONTRACT_API_DATABASE_URL=postgresql+psycopg://contract:contract@localhost:5432/yuqei_contract
|
||||
CONTRACT_API_REDIS_URL=redis://localhost:6379/0
|
||||
CONTRACT_API_AI_PLATFORM_BASE_URL=http://localhost:8101
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Contract API
|
||||
|
||||
FastAPI service for V2 contract workflows.
|
||||
|
||||
## Local Run
|
||||
|
||||
```powershell
|
||||
cd services/contract-api
|
||||
python -m uvicorn yuqei_contract_api.main:app --app-dir src --reload --port 8100
|
||||
```
|
||||
|
||||
Health endpoints:
|
||||
|
||||
- `GET /health`
|
||||
- `GET /api/v1/health`
|
||||
|
||||
## Test
|
||||
|
||||
From repository root:
|
||||
|
||||
```powershell
|
||||
python -m pytest services/contract-api/tests
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "yuqei-contract-api"
|
||||
version = "0.1.0"
|
||||
description = "Yuqei AI Contract Platform V2 contract API"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastapi>=0.115",
|
||||
"pydantic>=2.10",
|
||||
"pydantic-settings>=2.7",
|
||||
"sqlalchemy>=2.0",
|
||||
"uvicorn>=0.34",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,25 @@
|
||||
from functools import lru_cache
|
||||
from typing import Literal
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class ContractApiSettings(BaseSettings):
|
||||
service_name: str = "contract-api"
|
||||
environment: Literal["local", "test", "staging", "production"] = "local"
|
||||
log_level: str = "INFO"
|
||||
api_prefix: str = "/api/v1"
|
||||
database_url: str = "postgresql+psycopg://contract:contract@localhost:5432/yuqei_contract"
|
||||
redis_url: str = "redis://localhost:6379/0"
|
||||
ai_platform_base_url: str = "http://localhost:8101"
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_prefix="CONTRACT_API_",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> ContractApiSettings:
|
||||
return ContractApiSettings()
|
||||
@@ -0,0 +1,27 @@
|
||||
import logging
|
||||
from logging.config import dictConfig
|
||||
|
||||
|
||||
def configure_logging(level: str) -> None:
|
||||
dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "default",
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": level.upper(),
|
||||
},
|
||||
}
|
||||
)
|
||||
logging.getLogger("uvicorn.access").setLevel(level.upper())
|
||||
@@ -0,0 +1,45 @@
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
from yuqei_contract_api import __version__
|
||||
from yuqei_contract_api.config import ContractApiSettings, get_settings
|
||||
from yuqei_contract_api.logging import configure_logging
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: Literal["ok"] = "ok"
|
||||
service: str
|
||||
version: str
|
||||
environment: str
|
||||
|
||||
|
||||
def create_app(settings: ContractApiSettings | None = None) -> FastAPI:
|
||||
resolved_settings = settings or get_settings()
|
||||
configure_logging(resolved_settings.log_level)
|
||||
|
||||
app = FastAPI(
|
||||
title="Yuqei Contract API",
|
||||
version=__version__,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
openapi_url="/openapi.json",
|
||||
)
|
||||
|
||||
@app.get("/health", response_model=HealthResponse, tags=["system"])
|
||||
def health() -> HealthResponse:
|
||||
return HealthResponse(
|
||||
service=resolved_settings.service_name,
|
||||
version=__version__,
|
||||
environment=resolved_settings.environment,
|
||||
)
|
||||
|
||||
@app.get(f"{resolved_settings.api_prefix}/health", response_model=HealthResponse, tags=["system"])
|
||||
def api_health() -> HealthResponse:
|
||||
return health()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -0,0 +1 @@
|
||||
"""Contract API test package."""
|
||||
@@ -0,0 +1,29 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from yuqei_contract_api.config import ContractApiSettings
|
||||
from yuqei_contract_api.main import create_app
|
||||
|
||||
|
||||
def test_health_endpoint_returns_contract_api_status() -> None:
|
||||
app = create_app(ContractApiSettings(environment="test"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"status": "ok",
|
||||
"service": "contract-api",
|
||||
"version": "0.1.0",
|
||||
"environment": "test",
|
||||
}
|
||||
|
||||
|
||||
def test_versioned_health_endpoint_matches_contract_api_prefix() -> None:
|
||||
app = create_app(ContractApiSettings(environment="test"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/v1/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["service"] == "contract-api"
|
||||
@@ -0,0 +1,5 @@
|
||||
YUQEI_WORKER_ENVIRONMENT=local
|
||||
YUQEI_WORKER_LOG_LEVEL=INFO
|
||||
YUQEI_WORKER_REDIS_URL=redis://localhost:6379/2
|
||||
YUQEI_WORKER_QUEUE_NAME=yuqei-v2
|
||||
YUQEI_WORKER_OBJECT_STORAGE_ENDPOINT=http://localhost:9000
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Worker
|
||||
|
||||
Python worker process for V2 background tasks.
|
||||
|
||||
Initial responsibilities:
|
||||
|
||||
- Document extraction
|
||||
- Knowledge indexing
|
||||
- Export generation
|
||||
- Reminder jobs
|
||||
- Long-running AI jobs
|
||||
|
||||
## Local Health Check
|
||||
|
||||
```powershell
|
||||
cd services/worker
|
||||
$env:PYTHONPATH='src'
|
||||
python -m yuqei_worker.main --health
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
From repository root:
|
||||
|
||||
```powershell
|
||||
python -m pytest services/worker/tests
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "yuqei-worker"
|
||||
version = "0.1.0"
|
||||
description = "Yuqei AI Contract Platform V2 worker"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"pydantic>=2.10",
|
||||
"pydantic-settings>=2.7",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,24 @@
|
||||
from functools import lru_cache
|
||||
from typing import Literal
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class WorkerSettings(BaseSettings):
|
||||
service_name: str = "worker"
|
||||
environment: Literal["local", "test", "staging", "production"] = "local"
|
||||
log_level: str = "INFO"
|
||||
redis_url: str = "redis://localhost:6379/2"
|
||||
queue_name: str = "yuqei-v2"
|
||||
object_storage_endpoint: str = "http://localhost:9000"
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_prefix="YUQEI_WORKER_",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> WorkerSettings:
|
||||
return WorkerSettings()
|
||||
@@ -0,0 +1,26 @@
|
||||
import logging
|
||||
from logging.config import dictConfig
|
||||
|
||||
|
||||
def configure_logging(level: str) -> None:
|
||||
dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "default",
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": level.upper(),
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
import argparse
|
||||
import json
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
from yuqei_worker import __version__
|
||||
from yuqei_worker.config import WorkerSettings, get_settings
|
||||
from yuqei_worker.logging import configure_logging
|
||||
|
||||
|
||||
class WorkerHealth(TypedDict):
|
||||
status: Literal["ok"]
|
||||
service: str
|
||||
version: str
|
||||
environment: str
|
||||
queue_name: str
|
||||
|
||||
|
||||
def health_snapshot(settings: WorkerSettings | None = None) -> WorkerHealth:
|
||||
resolved_settings = settings or get_settings()
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": resolved_settings.service_name,
|
||||
"version": __version__,
|
||||
"environment": resolved_settings.environment,
|
||||
"queue_name": resolved_settings.queue_name,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Yuqei V2 worker")
|
||||
parser.add_argument("--health", action="store_true", help="Print worker health JSON and exit.")
|
||||
args = parser.parse_args()
|
||||
|
||||
settings = get_settings()
|
||||
configure_logging(settings.log_level)
|
||||
|
||||
if args.health:
|
||||
print(json.dumps(health_snapshot(settings), ensure_ascii=False))
|
||||
return
|
||||
|
||||
raise SystemExit("Worker loop is not implemented yet. Use --health for the current V2 baseline.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
"""Worker test package."""
|
||||
@@ -0,0 +1,14 @@
|
||||
from yuqei_worker.config import WorkerSettings
|
||||
from yuqei_worker.main import health_snapshot
|
||||
|
||||
|
||||
def test_worker_health_snapshot_returns_worker_status() -> None:
|
||||
health = health_snapshot(WorkerSettings(environment="test", queue_name="test-queue"))
|
||||
|
||||
assert health == {
|
||||
"status": "ok",
|
||||
"service": "worker",
|
||||
"version": "0.1.0",
|
||||
"environment": "test",
|
||||
"queue_name": "test-queue",
|
||||
}
|
||||
Reference in New Issue
Block a user