feat(telegram): expose alert learning registry readback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m11s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-02 20:20:48 +08:00
parent 0a7edbf3ce
commit 32d0109dcd
9 changed files with 524 additions and 13 deletions

View File

@@ -141,6 +141,7 @@ class AiAlertCardDeliveryItem(BaseModel):
learning_writeback_ready: bool = False
learning_writeback_status: str = "not_evaluated"
learning_writeback_refs: dict[str, Any] = Field(default_factory=dict)
learning_registry_bindings: list[dict[str, Any]] = Field(default_factory=list)
source_refs: dict[str, Any]
run_state: str | None = None
agent_id: str | None = None
@@ -170,17 +171,50 @@ class AiAlertCardDeliverySummary(BaseModel):
mcp_evidence_ref_ready_total: int = 0
post_verifier_ref_ready_total: int = 0
learning_writeback_next_action: str = "none"
learning_registry_target_count: int = 0
learning_registry_ready_target_count: int = 0
learning_registry_missing_target_count: int = 0
learning_registry_next_action: str = "none"
latest_sent_at: datetime | None = None
latest_queued_at: datetime | None = None
production_write_count: int = 0
class AiAlertCardLearningRegistryTarget(BaseModel):
target: str
status: str
consumer_surface: str
source_field: str
ready_receipt_count: int
latest_ref: str | None = None
target_write_performed: bool = False
metadata_only: bool = True
next_action: str
class AiAlertCardLearningRegistry(BaseModel):
schema_version: str
status: str
work_item_id: str
project_id: str
source_endpoint: str
consumer_context_route: str
target_count: int
ready_target_count: int
missing_target_count: int
ready_receipt_ref_count: int
next_action: str
targets: list[AiAlertCardLearningRegistryTarget]
operation_boundaries: dict[str, bool]
class ListAiAlertCardsResponse(BaseModel):
items: list[AiAlertCardDeliveryItem]
total: int
page: int
per_page: int
summary: AiAlertCardDeliverySummary
learning_registry: AiAlertCardLearningRegistry
class OutboundReplyMarkupGapPrefix(BaseModel):

View File

@@ -339,10 +339,11 @@ _COMMANDER_INSERTED_REQUIREMENT_WORK_ITEMS: list[dict[str, Any]] = [
"TelegramGateway / AwoooP outbound receipt pathscripts/ops 4 條 legacy direct Bot API fallback "
"已改為 AWOOI API / TelegramGateway receipt path目前明確讀回 workflow direct send 0、"
"ops script direct gap 0、API direct gap 0/runs/ai-alert-cards 已補 KM / PlayBook / RAG / MCP "
"learning_writeback_refs read model"
"learning_writeback_refs read model,並投影 KM / PlayBook / RAG / MCP / verifier / AI Agent "
"learning_registry consumer bindings。"
),
"acceptance": "Telegram alert matrix 顯示 total、DB receipt、AI route、controlled queue、manual/default gap、direct send gaplearning refs。",
"next_action": "把 Telegram alert learning refs 接到 KM / PlayBook / RAG / MCP registry readback讓 AI Loop Agent 可重用這些 receipt。",
"acceptance": "Telegram alert matrix 顯示 total、DB receipt、AI route、controlled queue、manual/default gap、direct send gaplearning refs 與 registry readback",
"next_action": "把 Telegram alert learning registry 接到 AI Loop Agent consumer context receipt讓後續判斷可直接重用這些 receipt。",
"mapped_workplan_id": "P0-006",
},
{

View File

@@ -99,6 +99,38 @@ _CALLBACK_REPLY_CACHE_TTL_SECONDS = int(
_AI_ALERT_CARD_CACHE_TTL_SECONDS = int(
os.getenv("AWOOOP_AI_ALERT_CARD_CACHE_TTL_SECONDS", "20")
)
_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS: tuple[dict[str, str], ...] = (
{
"target": "km",
"consumer_surface": "knowledge_memory_context",
"source_field": "km_entry_ref",
},
{
"target": "playbook",
"consumer_surface": "playbook_trust_candidate_context",
"source_field": "playbook_candidate_ref",
},
{
"target": "rag",
"consumer_surface": "rag_index_candidate_context",
"source_field": "rag_chunk_ref",
},
{
"target": "mcp",
"consumer_surface": "mcp_audit_feedback_context",
"source_field": "mcp_evidence_ref",
},
{
"target": "verifier",
"consumer_surface": "post_apply_verifier_feedback_context",
"source_field": "post_verifier_ref",
},
{
"target": "ai_agent",
"consumer_surface": "autonomous_runtime_decision_context",
"source_field": "ai_agent_context_ref",
},
)
_INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b")
_REMEDIATION_STATUS_FILTERS = {
"mcp_observed",
@@ -1323,7 +1355,7 @@ async def list_ai_alert_card_delivery_readback(
cache_key,
ttl_seconds=_AI_ALERT_CARD_CACHE_TTL_SECONDS,
)
if cached_response is not None:
if cached_response is not None and "learning_registry" in cached_response:
logger.info(
"operator_ai_alert_card_delivery_readback_cache_hit",
project_id=normalized_project_id,
@@ -1439,12 +1471,26 @@ async def list_ai_alert_card_delivery_readback(
event_type=normalized_event_type or None,
lane=normalized_lane or None,
)
items = [_ai_alert_card_delivery_item(row) for row in rows]
learning_registry = _ai_alert_card_learning_registry_readback(
items=items,
project_id=normalized_project_id,
)
summary["learning_registry_target_count"] = learning_registry["target_count"]
summary["learning_registry_ready_target_count"] = learning_registry[
"ready_target_count"
]
summary["learning_registry_missing_target_count"] = learning_registry[
"missing_target_count"
]
summary["learning_registry_next_action"] = learning_registry["next_action"]
response = {
"items": [_ai_alert_card_delivery_item(row) for row in rows],
"items": items,
"total": summary["total"],
"page": normalized_page,
"per_page": normalized_per_page,
"summary": summary,
"learning_registry": learning_registry,
}
logger.info(
"operator_ai_alert_card_delivery_readback_fetched",
@@ -1519,6 +1565,10 @@ def _ai_alert_card_delivery_summary_from_row(
if total > 0 and learning_writeback_missing_total == 0
else "ensure_ai_alert_cards_include_event_type_lane_and_delivery_receipt"
),
"learning_registry_target_count": 0,
"learning_registry_ready_target_count": 0,
"learning_registry_missing_target_count": 0,
"learning_registry_next_action": "none",
"latest_sent_at": row.get("latest_sent_at"),
"latest_queued_at": row.get("latest_queued_at"),
"production_write_count": 0,
@@ -1572,6 +1622,9 @@ def _ai_alert_card_delivery_item(row: Mapping[str, Any]) -> dict[str, Any]:
),
"learning_writeback_status": learning_writeback_refs["status"],
"learning_writeback_refs": learning_writeback_refs,
"learning_registry_bindings": _ai_alert_card_learning_registry_bindings(
learning_writeback_refs
),
"source_refs": source_refs,
"run_state": row.get("run_state"),
"agent_id": row.get("agent_id"),
@@ -1642,6 +1695,9 @@ def _ai_alert_card_learning_writeback_refs(
"post_verifier_ref": (
f"verifier://{project_id}/telegram-alert-delivery-receipt/{run_ref}"
),
"ai_agent_context_ref": (
f"ai-agent://{project_id}/telegram-alert-learning-context/{run_ref}/{message_ref}"
),
"source_fingerprints": fingerprints if isinstance(fingerprints, list) else [],
"alert_ids": alert_ids if isinstance(alert_ids, list) else [],
"missing_required_fields": missing,
@@ -1655,6 +1711,108 @@ def _ai_alert_card_learning_writeback_refs(
}
def _ai_alert_card_learning_registry_bindings(
learning_refs: Mapping[str, Any],
) -> list[dict[str, Any]]:
"""Project one alert-card receipt into KM / RAG / MCP consumer bindings."""
bindings: list[dict[str, Any]] = []
for target in _AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS:
ref = str(learning_refs.get(target["source_field"]) or "")
ready = learning_refs.get("status") == "ready" and bool(ref)
bindings.append({
"target": target["target"],
"status": "ready_for_consumer_context" if ready else "missing_ref",
"consumer_surface": target["consumer_surface"],
"source_field": target["source_field"],
"ref": ref or None,
"receipt_id": learning_refs.get("receipt_id") or "",
"work_item_id": learning_refs.get("work_item_id") or "CIR-P0-TG-001",
"metadata_only": True,
"target_write_performed": False,
"raw_payload_included": False,
"next_action": (
"consume_metadata_receipt_in_ai_loop_context"
if ready
else "repair_missing_learning_ref_before_consumer_binding"
),
})
return bindings
def _ai_alert_card_learning_registry_readback(
*,
items: list[dict[str, Any]],
project_id: str,
) -> dict[str, Any]:
target_rollups: list[dict[str, Any]] = []
ready_receipt_ref_count = 0
for target in _AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS:
refs = [
binding.get("ref")
for item in items
for binding in item.get("learning_registry_bindings", [])
if binding.get("target") == target["target"] and binding.get("ref")
]
ready_count = len(refs)
ready_receipt_ref_count += ready_count
target_rollups.append({
"target": target["target"],
"status": "ready_for_consumer_context" if ready_count else "waiting_receipt",
"consumer_surface": target["consumer_surface"],
"source_field": target["source_field"],
"ready_receipt_count": ready_count,
"latest_ref": refs[0] if refs else None,
"target_write_performed": False,
"metadata_only": True,
"next_action": (
"consume_metadata_receipt_in_ai_loop_context"
if ready_count
else "wait_for_ai_alert_card_learning_ref"
),
})
ready_target_count = sum(
1 for target in target_rollups if target["status"] == "ready_for_consumer_context"
)
missing_target_count = len(_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS) - ready_target_count
return {
"schema_version": "ai_alert_card_learning_registry_readback_v1",
"status": (
"learning_registry_ready"
if ready_target_count == len(_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS)
and bool(items)
else "waiting_ai_alert_card_receipts"
),
"work_item_id": "CIR-P0-TG-001",
"project_id": project_id,
"source_endpoint": "/api/v1/platform/runs/ai-alert-cards",
"consumer_context_route": "ai_loop_agent.telegram_alert_learning_context",
"target_count": len(_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS),
"ready_target_count": ready_target_count,
"missing_target_count": missing_target_count,
"ready_receipt_ref_count": ready_receipt_ref_count,
"next_action": (
"consume_telegram_alert_learning_registry_in_ai_loop_agent_context"
if ready_target_count == len(_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS)
and bool(items)
else "wait_for_ai_alert_card_learning_refs_before_registry_consume"
),
"targets": target_rollups,
"operation_boundaries": {
"metadata_read_performed": True,
"km_write_performed": False,
"rag_index_write_performed": False,
"playbook_trust_write_performed": False,
"mcp_tool_call_performed": False,
"verifier_execution_performed": False,
"telegram_send_performed": False,
"raw_payload_included": False,
"secret_value_read": False,
"github_api_used": False,
},
}
async def _fetch_callback_reply_audit_summary(
db: Any,
*,

View File

@@ -84,6 +84,9 @@ def load_latest_telegram_alert_ai_automation_matrix(
learning_writeback_refs_present = bool(
source_proof["ai_alert_card_learning_writeback_refs_present"]
)
learning_registry_present = bool(
source_proof["ai_alert_card_learning_registry_readback_present"]
)
direct_gap_count = int(egress_summary["direct_bot_api_call_count"])
direct_gap_file_count = int(egress_summary["direct_bot_api_file_count"])
@@ -242,6 +245,34 @@ def load_latest_telegram_alert_ai_automation_matrix(
"apps/api/src/db/awooop_models.py",
],
},
{
"surface_id": "telegram_alert_learning_registry_readback",
"priority_work_item_id": "CIR-P0-TG-001",
"surface_kind": "learning_registry_readback",
"status": (
"registry_readback_present"
if learning_registry_present
else "registry_readback_missing"
),
"known_item_count": source_proof[
"ai_alert_card_learning_registry_readback_count"
],
"db_or_log_receipt": "ready_awooop_outbound_message_learning_refs",
"ai_route": "ready_ai_loop_agent_consumer_context",
"controlled_queue": "ready_metadata_only_consumer_binding",
"post_verifier": "ready_registry_source_contract_present",
"learning_writeback": (
"ready_km_rag_playbook_mcp_verifier_ai_agent_registry"
if learning_registry_present
else "missing_registry_readback"
),
"manual_default_gap_count": 0,
"direct_send_gap_count": 0,
"evidence_refs": [
"apps/api/src/services/platform_operator_service.py",
"/api/v1/platform/runs/ai-alert-cards",
],
},
]
next_priority_action = _next_priority_action(
@@ -249,6 +280,7 @@ def load_latest_telegram_alert_ai_automation_matrix(
ops_script_gap_count=ops_script_gap_count,
direct_gap_count=direct_gap_count,
learning_writeback_refs_present=learning_writeback_refs_present,
learning_registry_present=learning_registry_present,
)
next_controlled_actions = _ordered_controlled_actions(next_priority_action)
summary = _build_summary(
@@ -306,6 +338,12 @@ def load_latest_telegram_alert_ai_automation_matrix(
(
(
(
"Manual/default semantics are only acceptable for critical/break-glass or historical evidence; "
"KM / PlayBook / RAG / MCP / verifier / AI Agent registry readback is present, "
"so the next P0 step is AI Loop Agent consumer context."
)
if learning_registry_present
else (
"Manual/default semantics are only acceptable for critical/break-glass or historical evidence; "
"learning refs are present, so the next P0 step is KM / PlayBook / RAG / MCP registry readback."
)
@@ -443,6 +481,14 @@ def _inspect_source_contract(repo_root: Path) -> dict[str, int | bool]:
"apps/api/src/api/v1/platform/operator_runs.py",
"learning_writeback_ready_total",
),
"ai_alert_card_learning_registry_readback_present": (
"apps/api/src/services/platform_operator_service.py",
"ai_alert_card_learning_registry_readback_v1",
),
"ai_alert_card_ai_agent_context_ref_present": (
"apps/api/src/services/platform_operator_service.py",
"ai_agent_context_ref",
),
}
result: dict[str, int | bool] = {}
for key, (relative_path, marker) in source_checks.items():
@@ -453,6 +499,10 @@ def _inspect_source_contract(repo_root: Path) -> dict[str, int | bool]:
result["ai_alert_card_delivery_readback_endpoint_count"] = 1 if present else 0
if key == "ai_alert_card_learning_writeback_refs_present":
result["ai_alert_card_learning_writeback_refs_count"] = 1 if present else 0
if key == "ai_alert_card_learning_registry_readback_present":
result["ai_alert_card_learning_registry_readback_count"] = (
1 if present else 0
)
required = [key for key, value in result.items() if key.endswith("_present") and value is not True]
if required:
@@ -478,6 +528,7 @@ def _next_priority_action(
ops_script_gap_count: int,
direct_gap_count: int,
learning_writeback_refs_present: bool,
learning_registry_present: bool,
) -> dict[str, Any]:
if api_direct_gap_count > 0:
return {
@@ -519,14 +570,24 @@ def _next_priority_action(
"requires_runtime_send": False,
"post_verifier": "/api/v1/platform/runs/ai-alert-cards readback shows delivery_receipt_readback_required and learning writeback refs.",
}
if not learning_registry_present:
return {
"action_id": "promote_telegram_alert_learning_refs_to_km_rag_mcp_registry",
"scope": "AI alert card receipt refs -> KM / PlayBook / RAG / MCP registry readback",
"priority": "P0",
"why": "Alert cards now expose learning refs; the next closure is registry/readback integration for AI Loop Agent reuse.",
"requires_secret": False,
"requires_runtime_send": False,
"post_verifier": "KM / PlayBook / RAG / MCP registry readback includes CIR-P0-TG-001 alert-card receipt refs.",
}
return {
"action_id": "promote_telegram_alert_learning_refs_to_km_rag_mcp_registry",
"scope": "AI alert card receipt refs -> KM / PlayBook / RAG / MCP registry readback",
"action_id": "consume_telegram_alert_learning_registry_in_ai_loop_agent_context",
"scope": "KM / PlayBook / RAG / MCP / verifier registry -> AI Loop Agent consumer context",
"priority": "P0",
"why": "Alert cards now expose learning refs; the next closure is registry/readback integration for AI Loop Agent reuse.",
"why": "Telegram alert learning registry is readable; the next closure is AI Loop Agent consumer context reuse and apply receipt.",
"requires_secret": False,
"requires_runtime_send": False,
"post_verifier": "KM / PlayBook / RAG / MCP registry readback includes CIR-P0-TG-001 alert-card receipt refs.",
"post_verifier": "AI Loop Agent context readback references the CIR-P0-TG-001 Telegram alert learning registry.",
}
@@ -589,6 +650,9 @@ def _build_summary(
"ai_alert_card_learning_writeback_refs_count": source_proof[
"ai_alert_card_learning_writeback_refs_count"
],
"ai_alert_card_learning_registry_readback_count": source_proof[
"ai_alert_card_learning_registry_readback_count"
],
"next_priority_action_id": next_priority_action["action_id"],
"next_priority_work_item_id": "CIR-P0-TG-001",
}