From a6157fcaf87e50326d0581514b43f08907b3f3d9 Mon Sep 17 00:00:00 2001 From: yuqei Date: Mon, 22 Jun 2026 18:19:59 +0800 Subject: [PATCH] chore: initialize V2 backend foundation --- .gitignore | 7 ++ docs/architecture/local-development.md | 43 +++++++ infra/compose/.env.example | 5 + infra/compose/.gitkeep | 1 - infra/compose/docker-compose.yml | 97 ++++++++++++++++ infra/docker/.gitkeep | 1 - infra/docker/ai-platform-api.Dockerfile | 15 +++ infra/docker/contract-api.Dockerfile | 15 +++ infra/docker/worker.Dockerfile | 14 +++ ...Ž端基础工程与本地开发底座记录-2026-06-22.md | 108 ++++++++++++++++++ pyproject.toml | 12 ++ services/ai-platform-api/.env.example | 5 + services/ai-platform-api/.gitkeep | 1 - services/ai-platform-api/README.md | 23 ++++ services/ai-platform-api/pyproject.toml | 16 +++ .../src/yuqei_ai_platform_api/__init__.py | 1 + .../src/yuqei_ai_platform_api/config.py | 25 ++++ .../src/yuqei_ai_platform_api/logging.py | 27 +++++ .../src/yuqei_ai_platform_api/main.py | 47 ++++++++ services/ai-platform-api/tests/__init__.py | 1 + services/ai-platform-api/tests/test_health.py | 30 +++++ services/contract-api/.env.example | 5 + services/contract-api/.gitkeep | 1 - services/contract-api/README.md | 23 ++++ services/contract-api/pyproject.toml | 16 +++ .../src/yuqei_contract_api/__init__.py | 1 + .../src/yuqei_contract_api/config.py | 25 ++++ .../src/yuqei_contract_api/logging.py | 27 +++++ .../src/yuqei_contract_api/main.py | 45 ++++++++ services/contract-api/tests/__init__.py | 1 + services/contract-api/tests/test_health.py | 29 +++++ services/worker/.env.example | 5 + services/worker/.gitkeep | 1 - services/worker/README.md | 27 +++++ services/worker/pyproject.toml | 13 +++ services/worker/src/yuqei_worker/__init__.py | 1 + services/worker/src/yuqei_worker/config.py | 24 ++++ services/worker/src/yuqei_worker/logging.py | 26 +++++ services/worker/src/yuqei_worker/main.py | 45 ++++++++ services/worker/tests/__init__.py | 1 + services/worker/tests/test_health.py | 14 +++ 41 files changed, 819 insertions(+), 5 deletions(-) create mode 100644 docs/architecture/local-development.md create mode 100644 infra/compose/.env.example delete mode 100644 infra/compose/.gitkeep create mode 100644 infra/compose/docker-compose.yml delete mode 100644 infra/docker/.gitkeep create mode 100644 infra/docker/ai-platform-api.Dockerfile create mode 100644 infra/docker/contract-api.Dockerfile create mode 100644 infra/docker/worker.Dockerfile create mode 100644 project-docs/V2后端基础工程与本地开发底座记录-2026-06-22.md create mode 100644 pyproject.toml create mode 100644 services/ai-platform-api/.env.example delete mode 100644 services/ai-platform-api/.gitkeep create mode 100644 services/ai-platform-api/README.md create mode 100644 services/ai-platform-api/pyproject.toml create mode 100644 services/ai-platform-api/src/yuqei_ai_platform_api/__init__.py create mode 100644 services/ai-platform-api/src/yuqei_ai_platform_api/config.py create mode 100644 services/ai-platform-api/src/yuqei_ai_platform_api/logging.py create mode 100644 services/ai-platform-api/src/yuqei_ai_platform_api/main.py create mode 100644 services/ai-platform-api/tests/__init__.py create mode 100644 services/ai-platform-api/tests/test_health.py create mode 100644 services/contract-api/.env.example delete mode 100644 services/contract-api/.gitkeep create mode 100644 services/contract-api/README.md create mode 100644 services/contract-api/pyproject.toml create mode 100644 services/contract-api/src/yuqei_contract_api/__init__.py create mode 100644 services/contract-api/src/yuqei_contract_api/config.py create mode 100644 services/contract-api/src/yuqei_contract_api/logging.py create mode 100644 services/contract-api/src/yuqei_contract_api/main.py create mode 100644 services/contract-api/tests/__init__.py create mode 100644 services/contract-api/tests/test_health.py create mode 100644 services/worker/.env.example delete mode 100644 services/worker/.gitkeep create mode 100644 services/worker/README.md create mode 100644 services/worker/pyproject.toml create mode 100644 services/worker/src/yuqei_worker/__init__.py create mode 100644 services/worker/src/yuqei_worker/config.py create mode 100644 services/worker/src/yuqei_worker/logging.py create mode 100644 services/worker/src/yuqei_worker/main.py create mode 100644 services/worker/tests/__init__.py create mode 100644 services/worker/tests/test_health.py diff --git a/.gitignore b/.gitignore index 6a78e976..08353997 100644 --- a/.gitignore +++ b/.gitignore @@ -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-*/ diff --git a/docs/architecture/local-development.md b/docs/architecture/local-development.md new file mode 100644 index 00000000..cca82e3c --- /dev/null +++ b/docs/architecture/local-development.md @@ -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` diff --git a/infra/compose/.env.example b/infra/compose/.env.example new file mode 100644 index 00000000..c0b03070 --- /dev/null +++ b/infra/compose/.env.example @@ -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 diff --git a/infra/compose/.gitkeep b/infra/compose/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/infra/compose/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/infra/compose/docker-compose.yml b/infra/compose/docker-compose.yml new file mode 100644 index 00000000..0bf29b42 --- /dev/null +++ b/infra/compose/docker-compose.yml @@ -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: diff --git a/infra/docker/.gitkeep b/infra/docker/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/infra/docker/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/infra/docker/ai-platform-api.Dockerfile b/infra/docker/ai-platform-api.Dockerfile new file mode 100644 index 00000000..e1ae2f3d --- /dev/null +++ b/infra/docker/ai-platform-api.Dockerfile @@ -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"] diff --git a/infra/docker/contract-api.Dockerfile b/infra/docker/contract-api.Dockerfile new file mode 100644 index 00000000..fdfd150f --- /dev/null +++ b/infra/docker/contract-api.Dockerfile @@ -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"] diff --git a/infra/docker/worker.Dockerfile b/infra/docker/worker.Dockerfile new file mode 100644 index 00000000..4503ac32 --- /dev/null +++ b/infra/docker/worker.Dockerfile @@ -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"] diff --git a/project-docs/V2后端基础工程与本地开发底座记录-2026-06-22.md b/project-docs/V2后端基础工程与本地开发底座记录-2026-06-22.md new file mode 100644 index 00000000..0596b11b --- /dev/null +++ b/project-docs/V2后端基础工程与本地开发底座记录-2026-06-22.md @@ -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。 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..397e0ebe --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/services/ai-platform-api/.env.example b/services/ai-platform-api/.env.example new file mode 100644 index 00000000..ecca7bd3 --- /dev/null +++ b/services/ai-platform-api/.env.example @@ -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 diff --git a/services/ai-platform-api/.gitkeep b/services/ai-platform-api/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/services/ai-platform-api/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/services/ai-platform-api/README.md b/services/ai-platform-api/README.md new file mode 100644 index 00000000..bbf40669 --- /dev/null +++ b/services/ai-platform-api/README.md @@ -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 +``` diff --git a/services/ai-platform-api/pyproject.toml b/services/ai-platform-api/pyproject.toml new file mode 100644 index 00000000..88cb86e0 --- /dev/null +++ b/services/ai-platform-api/pyproject.toml @@ -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" diff --git a/services/ai-platform-api/src/yuqei_ai_platform_api/__init__.py b/services/ai-platform-api/src/yuqei_ai_platform_api/__init__.py new file mode 100644 index 00000000..3dc1f76b --- /dev/null +++ b/services/ai-platform-api/src/yuqei_ai_platform_api/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/services/ai-platform-api/src/yuqei_ai_platform_api/config.py b/services/ai-platform-api/src/yuqei_ai_platform_api/config.py new file mode 100644 index 00000000..c9926043 --- /dev/null +++ b/services/ai-platform-api/src/yuqei_ai_platform_api/config.py @@ -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() diff --git a/services/ai-platform-api/src/yuqei_ai_platform_api/logging.py b/services/ai-platform-api/src/yuqei_ai_platform_api/logging.py new file mode 100644 index 00000000..06a77ae0 --- /dev/null +++ b/services/ai-platform-api/src/yuqei_ai_platform_api/logging.py @@ -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()) diff --git a/services/ai-platform-api/src/yuqei_ai_platform_api/main.py b/services/ai-platform-api/src/yuqei_ai_platform_api/main.py new file mode 100644 index 00000000..da02b548 --- /dev/null +++ b/services/ai-platform-api/src/yuqei_ai_platform_api/main.py @@ -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() diff --git a/services/ai-platform-api/tests/__init__.py b/services/ai-platform-api/tests/__init__.py new file mode 100644 index 00000000..68c3c318 --- /dev/null +++ b/services/ai-platform-api/tests/__init__.py @@ -0,0 +1 @@ +"""AI Platform API test package.""" diff --git a/services/ai-platform-api/tests/test_health.py b/services/ai-platform-api/tests/test_health.py new file mode 100644 index 00000000..055b00c5 --- /dev/null +++ b/services/ai-platform-api/tests/test_health.py @@ -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" diff --git a/services/contract-api/.env.example b/services/contract-api/.env.example new file mode 100644 index 00000000..f3fc9b4c --- /dev/null +++ b/services/contract-api/.env.example @@ -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 diff --git a/services/contract-api/.gitkeep b/services/contract-api/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/services/contract-api/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/services/contract-api/README.md b/services/contract-api/README.md new file mode 100644 index 00000000..29e280aa --- /dev/null +++ b/services/contract-api/README.md @@ -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 +``` diff --git a/services/contract-api/pyproject.toml b/services/contract-api/pyproject.toml new file mode 100644 index 00000000..01f1d78f --- /dev/null +++ b/services/contract-api/pyproject.toml @@ -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" diff --git a/services/contract-api/src/yuqei_contract_api/__init__.py b/services/contract-api/src/yuqei_contract_api/__init__.py new file mode 100644 index 00000000..3dc1f76b --- /dev/null +++ b/services/contract-api/src/yuqei_contract_api/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/services/contract-api/src/yuqei_contract_api/config.py b/services/contract-api/src/yuqei_contract_api/config.py new file mode 100644 index 00000000..0f24d8cb --- /dev/null +++ b/services/contract-api/src/yuqei_contract_api/config.py @@ -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() diff --git a/services/contract-api/src/yuqei_contract_api/logging.py b/services/contract-api/src/yuqei_contract_api/logging.py new file mode 100644 index 00000000..06a77ae0 --- /dev/null +++ b/services/contract-api/src/yuqei_contract_api/logging.py @@ -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()) diff --git a/services/contract-api/src/yuqei_contract_api/main.py b/services/contract-api/src/yuqei_contract_api/main.py new file mode 100644 index 00000000..4735bcce --- /dev/null +++ b/services/contract-api/src/yuqei_contract_api/main.py @@ -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() diff --git a/services/contract-api/tests/__init__.py b/services/contract-api/tests/__init__.py new file mode 100644 index 00000000..aa5c3f3b --- /dev/null +++ b/services/contract-api/tests/__init__.py @@ -0,0 +1 @@ +"""Contract API test package.""" diff --git a/services/contract-api/tests/test_health.py b/services/contract-api/tests/test_health.py new file mode 100644 index 00000000..5c3ddf95 --- /dev/null +++ b/services/contract-api/tests/test_health.py @@ -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" diff --git a/services/worker/.env.example b/services/worker/.env.example new file mode 100644 index 00000000..c0f9cdd3 --- /dev/null +++ b/services/worker/.env.example @@ -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 diff --git a/services/worker/.gitkeep b/services/worker/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/services/worker/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/services/worker/README.md b/services/worker/README.md new file mode 100644 index 00000000..3881e37c --- /dev/null +++ b/services/worker/README.md @@ -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 +``` diff --git a/services/worker/pyproject.toml b/services/worker/pyproject.toml new file mode 100644 index 00000000..75bd38b5 --- /dev/null +++ b/services/worker/pyproject.toml @@ -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" diff --git a/services/worker/src/yuqei_worker/__init__.py b/services/worker/src/yuqei_worker/__init__.py new file mode 100644 index 00000000..3dc1f76b --- /dev/null +++ b/services/worker/src/yuqei_worker/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/services/worker/src/yuqei_worker/config.py b/services/worker/src/yuqei_worker/config.py new file mode 100644 index 00000000..090588c3 --- /dev/null +++ b/services/worker/src/yuqei_worker/config.py @@ -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() diff --git a/services/worker/src/yuqei_worker/logging.py b/services/worker/src/yuqei_worker/logging.py new file mode 100644 index 00000000..03248440 --- /dev/null +++ b/services/worker/src/yuqei_worker/logging.py @@ -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(), + }, + } + ) diff --git a/services/worker/src/yuqei_worker/main.py b/services/worker/src/yuqei_worker/main.py new file mode 100644 index 00000000..19b9915d --- /dev/null +++ b/services/worker/src/yuqei_worker/main.py @@ -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() diff --git a/services/worker/tests/__init__.py b/services/worker/tests/__init__.py new file mode 100644 index 00000000..ddc576c0 --- /dev/null +++ b/services/worker/tests/__init__.py @@ -0,0 +1 @@ +"""Worker test package.""" diff --git a/services/worker/tests/test_health.py b/services/worker/tests/test_health.py new file mode 100644 index 00000000..99f26372 --- /dev/null +++ b/services/worker/tests/test_health.py @@ -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", + }