feat(aiops): structure agent loop shadow output
This commit is contained in:
@@ -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,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user