feat(aiops): structure agent loop shadow output
Some checks failed
CD Pipeline / tests (push) Successful in 2m50s
Code Review / ai-code-review (push) Successful in 33s
CD Pipeline / build-and-deploy (push) Failing after 25m48s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-05-01 15:09:57 +08:00
parent f53d7e5584
commit b0da6da1e9
5 changed files with 165 additions and 3 deletions

View File

@@ -1802,6 +1802,7 @@ Focus on:
"task_type": "diagnose",
},
)
structured_shadow = self._parse_agent_loop_shadow_response(result.raw_response or "")
proposal["agent_loop_shadow"] = {
"enabled": True,
"success": result.success,
@@ -1809,6 +1810,9 @@ Focus on:
"tokens": result.tokens,
"latency_ms": round(result.latency_ms, 1),
"error": result.error,
"decision_impact": "none",
"structured": structured_shadow,
"confidence_delta": structured_shadow.get("confidence_delta", 0.0),
"preview": (result.raw_response or "")[:700],
}
logger.info(
@@ -1818,6 +1822,8 @@ Focus on:
success=result.success,
tools_available=len(available_tools),
latency_ms=round(result.latency_ms, 1),
confidence_delta=structured_shadow.get("confidence_delta", 0.0),
parse_status=structured_shadow.get("parse_status"),
)
except Exception as exc:
logger.warning(
@@ -1826,6 +1832,106 @@ Focus on:
error=str(exc),
)
@classmethod
def _parse_agent_loop_shadow_response(cls, raw_response: str) -> dict:
"""
Normalize read-only Agent Loop output into durable metadata.
The shadow result is intentionally non-decisive. Downstream code can
inspect this structure for quality review, but it must not override the
main proposal until ADR-105 canary graduation.
"""
text = (raw_response or "").strip()
if not text:
return {
"parse_status": "empty",
"root_cause_check": "",
"evidence_used": [],
"confidence_delta": 0.0,
"missing_evidence": [],
"human_or_ai_next_step": "",
}
payload = cls._extract_json_object(text)
if not isinstance(payload, dict):
return {
"parse_status": "unparsed",
"root_cause_check": "",
"evidence_used": [],
"confidence_delta": 0.0,
"missing_evidence": [],
"human_or_ai_next_step": "",
"raw_preview": text[:700],
}
return {
"parse_status": "ok",
"root_cause_check": cls._clip_shadow_text(payload.get("root_cause_check"), max_chars=500),
"evidence_used": cls._coerce_shadow_list(payload.get("evidence_used"), max_items=5),
"confidence_delta": cls._coerce_agent_loop_confidence_delta(
payload.get("confidence_delta", 0.0)
),
"missing_evidence": cls._coerce_shadow_list(payload.get("missing_evidence"), max_items=5),
"human_or_ai_next_step": cls._clip_shadow_text(
payload.get("human_or_ai_next_step"), max_chars=500
),
}
@staticmethod
def _extract_json_object(text: str) -> dict | None:
"""Extract the first JSON object from plain or fenced LLM output."""
candidates = [text]
fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL | re.IGNORECASE)
if fenced:
candidates.insert(0, fenced.group(1))
object_match = re.search(r"\{.*\}", text, flags=re.DOTALL)
if object_match:
candidates.append(object_match.group(0))
for candidate in candidates:
try:
parsed = json.loads(candidate)
except (TypeError, json.JSONDecodeError):
continue
if isinstance(parsed, dict):
return parsed
return None
@staticmethod
def _clip_shadow_text(value: object, *, max_chars: int) -> str:
if value is None:
return ""
return str(value).strip()[:max_chars]
@classmethod
def _coerce_shadow_list(cls, value: object, *, max_items: int) -> list[str]:
if value is None:
return []
if isinstance(value, list):
items = value
else:
items = [value]
normalized = []
for item in items:
clipped = cls._clip_shadow_text(item, max_chars=240)
if clipped:
normalized.append(clipped)
if len(normalized) >= max_items:
break
return normalized
@staticmethod
def _coerce_agent_loop_confidence_delta(value: object) -> float:
"""
Keep canary deltas conservative: metadata may lower confidence later,
but positive boosts are recorded as 0 until the shadow path graduates.
"""
try:
delta = float(value)
except (TypeError, ValueError):
return 0.0
return round(max(min(delta, 0.0), -0.15), 3)
def _build_agent_loop_shadow_prompt(
self,
*,