Files
awoooi/scripts/generate-schemas.py
OG T 8235f91bc6
Some checks failed
Type Sync Check / check-type-sync (push) Failing after 56s
fix(scripts): generate-schemas 同時加入 apps/api 和 apps/api/src 到 sys.path
問題: CI type-sync-check 持續失敗
原因: 只加 apps/api/src 不夠,模型檔內部用 from src.utils.X import Y
     需要 apps/api 在 path 才能解析 src 套件
結果: 51 個型別全部正確生成

# 2026-04-06 ogt: fix CI type-sync blocking deployment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 12:00:18 +08:00

296 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""
Pydantic → JSON Schema 生成器
=============================
Phase 14.3: 共用型別系統
功能:
- 從 Pydantic 模型生成 JSON Schema
- 正確處理 Pydantic 2.x 的 $defs 格式
- 輸出到 packages/shared-types/schemas/
使用方式:
cd apps/api
python ../../scripts/generate-schemas.py
建立: 2026-03-31 (台北時區)
建立者: Claude Code (Phase 14.3)
"""
import json
import sys
from pathlib import Path
from typing import Any
# 加入 apps/api 和 apps/api/src 到 Python path
# - apps/api/src: script 內部用 `from models.X import Y`
# - apps/api: 模型檔案內部用 `from src.utils.X import Y`
# 2026-04-06 ogt: 兩個都需要,否則跨模組 import 失敗導致 CI 產生空 schema
api_root = Path(__file__).parent.parent / "apps" / "api"
api_src = api_root / "src"
sys.path.insert(0, str(api_src))
sys.path.insert(0, str(api_root))
# 輸出目錄
OUTPUT_DIR = Path(__file__).parent.parent / "packages" / "shared-types" / "schemas"
def extract_and_merge_defs(schema: dict[str, Any], all_defs: dict[str, Any]) -> dict[str, Any]:
"""
從單一模型 schema 中提取 $defs 並合併到全局 $defs
同時修正 $ref 路徑
"""
# 提取並合併 $defs
if "$defs" in schema:
for def_name, def_schema in schema["$defs"].items():
if def_name not in all_defs:
# 遞迴處理巢狀 $defs
cleaned_def = extract_and_merge_defs(def_schema.copy(), all_defs)
all_defs[def_name] = cleaned_def
# 移除 schema 中的 $defs (已合併到全局)
cleaned_schema = {k: v for k, v in schema.items() if k != "$defs"}
return cleaned_schema
def fix_refs(obj: Any) -> Any:
"""遞迴修正所有 $ref 路徑從 #/$defs/X 到 #/definitions/X"""
if isinstance(obj, dict):
result = {}
for key, value in obj.items():
if key == "$ref" and isinstance(value, str):
# 修正 ref 路徑
result[key] = value.replace("#/$defs/", "#/definitions/")
else:
result[key] = fix_refs(value)
return result
elif isinstance(obj, list):
return [fix_refs(item) for item in obj]
else:
return obj
def generate_schemas():
"""生成所有模型的 JSON Schema"""
# 確保輸出目錄存在
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 收集所有 definitions
all_definitions: dict[str, Any] = {}
model_count = 0
print("📦 Approval Models:")
# === Approval Models ===
try:
from models.approval import (
ApprovalRequest,
ApprovalRequestCreate,
ApprovalRequestResponse,
BlastRadius,
DryRunCheck,
PendingApprovalsResponse,
RejectRequest,
Signature,
SignRequest,
SignResponse,
)
approval_models = [
ApprovalRequest,
ApprovalRequestCreate,
ApprovalRequestResponse,
BlastRadius,
DryRunCheck,
PendingApprovalsResponse,
RejectRequest,
Signature,
SignRequest,
SignResponse,
]
for model in approval_models:
schema = model.model_json_schema()
cleaned = extract_and_merge_defs(schema, all_definitions)
all_definitions[model.__name__] = cleaned
model_count += 1
print(f"{model.__name__}")
except ImportError as e:
print(f" ⚠ Import failed: {e}")
print("\n📦 Incident Models:")
# === Incident Models ===
try:
from models.incident import (
AIDecisionChain,
Incident,
IncidentCreate,
IncidentOutcome,
IncidentResponse,
IncidentUpdate,
Signal,
)
incident_models = [
AIDecisionChain,
Incident,
IncidentCreate,
IncidentOutcome,
IncidentResponse,
IncidentUpdate,
Signal,
]
for model in incident_models:
schema = model.model_json_schema()
cleaned = extract_and_merge_defs(schema, all_definitions)
all_definitions[model.__name__] = cleaned
model_count += 1
print(f"{model.__name__}")
except ImportError as e:
print(f" ⚠ Import failed: {e}")
print("\n📦 Terminal Models:")
# === Terminal Models ===
try:
from models.terminal import (
SpatialContext,
TerminalAbortRequest,
TerminalAbortResponse,
TerminalIntentRequest,
TerminalIntentResponse,
TerminalStatusResponse,
TerminalThoughtEvent,
)
terminal_models = [
TerminalIntentRequest,
TerminalIntentResponse,
TerminalStatusResponse,
TerminalAbortRequest,
TerminalAbortResponse,
TerminalThoughtEvent,
SpatialContext,
]
for model in terminal_models:
schema = model.model_json_schema()
cleaned = extract_and_merge_defs(schema, all_definitions)
all_definitions[model.__name__] = cleaned
model_count += 1
print(f"{model.__name__}")
except ImportError as e:
print(f" ⚠ Import failed: {e}")
print("\n📦 Playbook Models:")
# === Playbook Models ===
try:
from models.playbook import (
Playbook,
PlaybookCreateRequest,
PlaybookListResponse,
PlaybookRecommendation,
PlaybookResponse,
PlaybookUpdateRequest,
RepairStep,
SymptomPattern,
)
playbook_models = [
Playbook,
PlaybookResponse,
PlaybookListResponse,
PlaybookCreateRequest,
PlaybookUpdateRequest,
PlaybookRecommendation,
SymptomPattern,
RepairStep,
]
for model in playbook_models:
schema = model.model_json_schema()
cleaned = extract_and_merge_defs(schema, all_definitions)
all_definitions[model.__name__] = cleaned
model_count += 1
print(f"{model.__name__}")
except ImportError as e:
print(f" ⚠ Import failed: {e}")
print("\n📦 NVIDIA Models:")
# === NVIDIA Models (ADR-036) ===
try:
from models.nvidia import (
NvidiaChoice,
NvidiaMessage,
NvidiaResponse,
NvidiaUsage,
ToolCall,
ToolDefinition,
ToolFunction,
)
nvidia_models = [
ToolFunction,
ToolCall,
NvidiaMessage,
NvidiaChoice,
NvidiaUsage,
NvidiaResponse,
ToolDefinition,
]
for model in nvidia_models:
schema = model.model_json_schema()
cleaned = extract_and_merge_defs(schema, all_definitions)
all_definitions[model.__name__] = cleaned
model_count += 1
print(f"{model.__name__}")
except ImportError as e:
print(f" ⚠ NVIDIA import failed: {e}")
print("\n📦 Other Models:")
# === CSRF Models ===
try:
from models.csrf import CSRFTokenResponse
schema = CSRFTokenResponse.model_json_schema()
cleaned = extract_and_merge_defs(schema, all_definitions)
all_definitions["CSRFTokenResponse"] = cleaned
model_count += 1
print(" ✓ CSRFTokenResponse")
except ImportError as e:
print(f" ⚠ CSRF import failed: {e}")
# 修正所有 $ref 路徑
all_definitions = fix_refs(all_definitions)
# 組裝最終 schema
final_schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AWOOOI API Types",
"description": "Auto-generated from Pydantic models - DO NOT EDIT",
"definitions": all_definitions,
}
# 寫入檔案
output_file = OUTPUT_DIR / "api-types.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump(final_schema, f, indent=2, ensure_ascii=False)
print(f"\n✅ Schema 生成完成: {output_file}")
print(f"{len(all_definitions)} 個型別定義")
return final_schema
if __name__ == "__main__":
print("🔄 開始生成 JSON Schema...\n")
generate_schemas()