feat(agent99): establish enterprise AI automation work ledger
This commit is contained in:
@@ -35,6 +35,9 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from src.core.logging import get_logger
|
||||
from src.core.sse import get_publisher
|
||||
from src.services.agent99_enterprise_ai_automation_work_items import (
|
||||
load_latest_agent99_enterprise_ai_automation_work_items,
|
||||
)
|
||||
from src.services.agent_market_governance_snapshot import (
|
||||
load_latest_agent_market_governance_snapshot,
|
||||
)
|
||||
@@ -1133,6 +1136,40 @@ async def get_delivery_closure_workbench() -> dict[str, Any]:
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agent99-enterprise-ai-automation-work-items",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 Agent99 全域 AI 自動化工作總帳",
|
||||
description=(
|
||||
"讀取 Agent99 對全部主機、產品、網站、服務、工具、套件與 AI 控制面"
|
||||
"的唯一工作總帳,包含 current P0、完整排序、完成契約、verifier、rollback "
|
||||
"與 coverage。此端點只讀 committed snapshot,不連主機、不執行修復、不讀 secret、"
|
||||
"不呼叫 GitHub,也不觸發 workflow。"
|
||||
),
|
||||
)
|
||||
async def get_agent99_enterprise_ai_automation_work_items() -> dict[str, Any]:
|
||||
"""回傳 Agent99 全域 AI 自動化工作總帳。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(
|
||||
load_latest_agent99_enterprise_ai_automation_work_items
|
||||
)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
logger.error(
|
||||
"agent99_enterprise_ai_automation_work_items_invalid",
|
||||
error=str(exc),
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Agent99 全域 AI 自動化工作總帳無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/awoooi-priority-work-order-readback",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Agent99 enterprise AI-automation work-item readback."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from src.services.snapshot_paths import default_operations_dir
|
||||
|
||||
_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__))
|
||||
_SNAPSHOT_FILE = "agent99-enterprise-ai-automation-work-items.snapshot.json"
|
||||
_SCHEMA_VERSION = "agent99_enterprise_ai_automation_work_items_v1"
|
||||
_VALID_PRIORITIES = {"P0", "P1", "P2"}
|
||||
_VALID_STATUSES = {"planned", "in_progress", "in_progress_blocked", "complete"}
|
||||
|
||||
|
||||
def load_latest_agent99_enterprise_ai_automation_work_items(
|
||||
operations_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Load and validate the canonical Agent99 enterprise work ledger."""
|
||||
directory = operations_dir or _DEFAULT_OPERATIONS_DIR
|
||||
path = directory / _SNAPSHOT_FILE
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"{path}: expected JSON object")
|
||||
if payload.get("schema_version") != _SCHEMA_VERSION:
|
||||
raise ValueError(f"{path}: unexpected schema_version")
|
||||
|
||||
coverage = _require_dict(payload, "coverage", path)
|
||||
hosts = _require_list(coverage, "hosts", path)
|
||||
products = _require_list(coverage, "products", path)
|
||||
public_surfaces = _require_list(coverage, "public_surfaces", path)
|
||||
work_items = _require_list(payload, "work_items", path)
|
||||
summary = _require_dict(payload, "summary", path)
|
||||
current_p0 = _require_dict(payload, "current_p0", path)
|
||||
promotion_contract = _require_dict(payload, "promotion_contract", path)
|
||||
required_p0_fields = _require_list(
|
||||
promotion_contract,
|
||||
"required_item_fields",
|
||||
path,
|
||||
)
|
||||
|
||||
_require_count(summary, "host_count", len(hosts), path)
|
||||
_require_count(summary, "product_count", len(products), path)
|
||||
_require_count(summary, "public_surface_count", len(public_surfaces), path)
|
||||
_require_count(summary, "work_item_count", len(work_items), path)
|
||||
|
||||
ids: list[str] = []
|
||||
orders: list[int] = []
|
||||
priority_counts = {priority: 0 for priority in _VALID_PRIORITIES}
|
||||
status_counts = {status: 0 for status in _VALID_STATUSES}
|
||||
p0_ids: list[str] = []
|
||||
|
||||
for index, item in enumerate(work_items, start=1):
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError(f"{path}: work_items[{index - 1}] must be an object")
|
||||
item_id = item.get("id")
|
||||
order = item.get("order")
|
||||
priority = item.get("priority")
|
||||
item_status = item.get("status")
|
||||
if not isinstance(item_id, str) or not item_id:
|
||||
raise ValueError(f"{path}: work_items[{index - 1}].id is required")
|
||||
if not isinstance(order, int):
|
||||
raise ValueError(f"{path}: {item_id}.order must be an integer")
|
||||
if priority not in _VALID_PRIORITIES:
|
||||
raise ValueError(f"{path}: {item_id}.priority is invalid")
|
||||
if item_status not in _VALID_STATUSES:
|
||||
raise ValueError(f"{path}: {item_id}.status is invalid")
|
||||
if order != index:
|
||||
raise ValueError(f"{path}: {item_id}.order must equal {index}")
|
||||
|
||||
ids.append(item_id)
|
||||
orders.append(order)
|
||||
priority_counts[priority] += 1
|
||||
status_counts[item_status] += 1
|
||||
if priority == "P0":
|
||||
p0_ids.append(item_id)
|
||||
missing = [field for field in required_p0_fields if item.get(field) is None]
|
||||
if missing:
|
||||
raise ValueError(f"{path}: {item_id} missing P0 fields: {missing}")
|
||||
|
||||
if len(ids) != len(set(ids)):
|
||||
raise ValueError(f"{path}: duplicate work item id")
|
||||
if orders != list(range(1, len(work_items) + 1)):
|
||||
raise ValueError(f"{path}: work item order is not contiguous")
|
||||
|
||||
current_p0_id = current_p0.get("id")
|
||||
if current_p0_id not in p0_ids:
|
||||
raise ValueError(f"{path}: current_p0.id is not a P0 work item")
|
||||
if not p0_ids or p0_ids[0] != current_p0_id:
|
||||
raise ValueError(f"{path}: current_p0 must be the first ordered P0")
|
||||
if payload.get("next_execution_order") != p0_ids:
|
||||
raise ValueError(f"{path}: next_execution_order must equal ordered P0 ids")
|
||||
|
||||
_require_count(summary, "p0_count", priority_counts["P0"], path)
|
||||
_require_count(summary, "p1_count", priority_counts["P1"], path)
|
||||
_require_count(summary, "p2_count", priority_counts["P2"], path)
|
||||
_require_count(summary, "planned_count", status_counts["planned"], path)
|
||||
_require_count(
|
||||
summary,
|
||||
"in_progress_count",
|
||||
status_counts["in_progress"],
|
||||
path,
|
||||
)
|
||||
_require_count(
|
||||
summary,
|
||||
"in_progress_blocked_count",
|
||||
status_counts["in_progress_blocked"],
|
||||
path,
|
||||
)
|
||||
_require_count(summary, "complete_count", status_counts["complete"], path)
|
||||
_require_count(summary, "current_p0_count", 1, path)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def build_agent99_enterprise_priority_projection(
|
||||
payload: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Build the bounded projection embedded in the main priority endpoint."""
|
||||
coverage = payload["coverage"]
|
||||
summary = payload["summary"]
|
||||
return {
|
||||
"schema_version": payload["schema_version"],
|
||||
"generated_at": payload["generated_at"],
|
||||
"status": payload["status"],
|
||||
"scope_complete": payload["scope_complete"],
|
||||
"current_p0": payload["current_p0"],
|
||||
"coverage": {
|
||||
"host_count": len(coverage["hosts"]),
|
||||
"product_count": len(coverage["products"]),
|
||||
"public_surface_count": len(coverage["public_surfaces"]),
|
||||
"service_domain_count": len(coverage["service_domains"]),
|
||||
"package_domain_count": len(coverage["package_domains"]),
|
||||
},
|
||||
"work_item_counts": {
|
||||
"total": summary["work_item_count"],
|
||||
"p0": summary["p0_count"],
|
||||
"p1": summary["p1_count"],
|
||||
"p2": summary["p2_count"],
|
||||
"in_progress": summary["in_progress_count"],
|
||||
"in_progress_blocked": summary["in_progress_blocked_count"],
|
||||
"planned": summary["planned_count"],
|
||||
"complete": summary["complete_count"],
|
||||
},
|
||||
"next_execution_order": payload["next_execution_order"],
|
||||
"full_readback_api": "/api/v1/agents/agent99-enterprise-ai-automation-work-items",
|
||||
}
|
||||
|
||||
|
||||
def _require_dict(payload: dict[str, Any], key: str, path: Path) -> dict[str, Any]:
|
||||
value = payload.get(key)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path}: {key} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _require_list(payload: dict[str, Any], key: str, path: Path) -> list[Any]:
|
||||
value = payload.get(key)
|
||||
if not isinstance(value, list):
|
||||
raise ValueError(f"{path}: {key} must be an array")
|
||||
return value
|
||||
|
||||
|
||||
def _require_count(
|
||||
summary: dict[str, Any],
|
||||
key: str,
|
||||
expected: int,
|
||||
path: Path,
|
||||
) -> None:
|
||||
if summary.get(key) != expected:
|
||||
raise ValueError(
|
||||
f"{path}: summary.{key}={summary.get(key)!r}, expected {expected}"
|
||||
)
|
||||
@@ -17,6 +17,10 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from src.services.agent99_enterprise_ai_automation_work_items import (
|
||||
build_agent99_enterprise_priority_projection,
|
||||
load_latest_agent99_enterprise_ai_automation_work_items,
|
||||
)
|
||||
from src.services.snapshot_paths import default_operations_dir
|
||||
|
||||
_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__))
|
||||
@@ -870,11 +874,38 @@ def load_latest_awoooi_priority_work_order_readback(
|
||||
)
|
||||
_require_mainline_consistency(payload, str(path))
|
||||
_apply_commander_inserted_requirement_work_items(payload)
|
||||
_apply_agent99_enterprise_ai_automation_projection(payload, directory)
|
||||
_mark_runtime_generated_at(payload)
|
||||
apply_ai_automation_node_receipts(payload)
|
||||
return payload
|
||||
|
||||
|
||||
def _apply_agent99_enterprise_ai_automation_projection(
|
||||
payload: dict[str, Any],
|
||||
operations_dir: Path,
|
||||
) -> None:
|
||||
"""Attach a bounded projection while keeping fixture-only snapshots usable."""
|
||||
try:
|
||||
enterprise = load_latest_agent99_enterprise_ai_automation_work_items(
|
||||
operations_dir
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
payload["agent99_enterprise_ai_automation"] = (
|
||||
build_agent99_enterprise_priority_projection(enterprise)
|
||||
)
|
||||
source_refs = payload.setdefault("source_refs", {})
|
||||
if isinstance(source_refs, dict):
|
||||
source_refs["agent99_enterprise_ai_automation_work_items"] = (
|
||||
"docs/operations/"
|
||||
"agent99-enterprise-ai-automation-work-items.snapshot.json"
|
||||
)
|
||||
source_refs["agent99_enterprise_ai_automation_work_items_api"] = (
|
||||
"/api/v1/agents/agent99-enterprise-ai-automation-work-items"
|
||||
)
|
||||
|
||||
|
||||
def load_controlled_cd_lane_live_metric_readback(
|
||||
metrics_url: str = _CONTROLLED_CD_LANE_NODE_EXPORTER_URL,
|
||||
prometheus_query_url: str = _CONTROLLED_CD_LANE_PROMETHEUS_QUERY_URL,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# ruff: noqa: E402
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.v1.agents import router
|
||||
from src.services.agent99_enterprise_ai_automation_work_items import (
|
||||
build_agent99_enterprise_priority_projection,
|
||||
load_latest_agent99_enterprise_ai_automation_work_items,
|
||||
)
|
||||
from src.services.awoooi_priority_work_order_readback import (
|
||||
load_latest_awoooi_priority_work_order_readback,
|
||||
)
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
_OPERATIONS_DIR = _REPO_ROOT / "docs" / "operations"
|
||||
|
||||
|
||||
def test_agent99_enterprise_work_items_loader_returns_complete_scope() -> None:
|
||||
payload = load_latest_agent99_enterprise_ai_automation_work_items()
|
||||
|
||||
assert payload["schema_version"] == (
|
||||
"agent99_enterprise_ai_automation_work_items_v1"
|
||||
)
|
||||
assert payload["scope_complete"] is False
|
||||
assert payload["current_p0"]["id"] == "AG99-P0-001"
|
||||
assert payload["summary"] == {
|
||||
"host_count": 7,
|
||||
"product_count": 12,
|
||||
"public_surface_count": 23,
|
||||
"work_item_count": 25,
|
||||
"p0_count": 14,
|
||||
"p1_count": 8,
|
||||
"p2_count": 3,
|
||||
"in_progress_count": 5,
|
||||
"in_progress_blocked_count": 3,
|
||||
"planned_count": 17,
|
||||
"complete_count": 0,
|
||||
"current_p0_count": 1,
|
||||
}
|
||||
assert payload["next_execution_order"][0] == "AG99-P0-001"
|
||||
assert payload["next_execution_order"][-1] == "AG99-P0-014"
|
||||
|
||||
|
||||
def test_agent99_enterprise_projection_is_bounded() -> None:
|
||||
payload = load_latest_agent99_enterprise_ai_automation_work_items()
|
||||
projection = build_agent99_enterprise_priority_projection(payload)
|
||||
|
||||
assert projection["current_p0"]["id"] == "AG99-P0-001"
|
||||
assert projection["coverage"] == {
|
||||
"host_count": 7,
|
||||
"product_count": 12,
|
||||
"public_surface_count": 23,
|
||||
"service_domain_count": 12,
|
||||
"package_domain_count": 7,
|
||||
}
|
||||
assert projection["work_item_counts"]["total"] == 25
|
||||
assert "work_items" not in projection
|
||||
assert projection["full_readback_api"] == (
|
||||
"/api/v1/agents/agent99-enterprise-ai-automation-work-items"
|
||||
)
|
||||
|
||||
|
||||
def test_priority_readback_projects_agent99_enterprise_order() -> None:
|
||||
payload = load_latest_awoooi_priority_work_order_readback()
|
||||
projection = payload["agent99_enterprise_ai_automation"]
|
||||
|
||||
assert projection["current_p0"]["id"] == "AG99-P0-001"
|
||||
assert projection["work_item_counts"]["p0"] == 14
|
||||
assert (
|
||||
payload["source_refs"]["agent99_enterprise_ai_automation_work_items_api"]
|
||||
== "/api/v1/agents/agent99-enterprise-ai-automation-work-items"
|
||||
)
|
||||
|
||||
|
||||
def test_agent99_enterprise_loader_rejects_duplicate_ids(tmp_path: Path) -> None:
|
||||
source = _OPERATIONS_DIR / (
|
||||
"agent99-enterprise-ai-automation-work-items.snapshot.json"
|
||||
)
|
||||
payload = json.loads(source.read_text(encoding="utf-8"))
|
||||
payload["work_items"][1]["id"] = payload["work_items"][0]["id"]
|
||||
target = tmp_path / source.name
|
||||
target.write_text(json.dumps(payload), encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="duplicate work item id"):
|
||||
load_latest_agent99_enterprise_ai_automation_work_items(tmp_path)
|
||||
|
||||
|
||||
def test_agent99_enterprise_work_items_endpoint_returns_snapshot() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/v1/agents")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/v1/agents/agent99-enterprise-ai-automation-work-items")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["current_p0"]["id"] == "AG99-P0-001"
|
||||
assert payload["summary"]["host_count"] == 7
|
||||
assert payload["summary"]["product_count"] == 12
|
||||
assert payload["summary"]["public_surface_count"] == 23
|
||||
Reference in New Issue
Block a user