agent-ablation 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_ablation/__init__.py +76 -0
- agent_ablation/adapters/__init__.py +16 -0
- agent_ablation/adapters/autogen.py +73 -0
- agent_ablation/adapters/crewai.py +111 -0
- agent_ablation/adapters/langgraph.py +109 -0
- agent_ablation/adapters/vercel.py +96 -0
- agent_ablation/core.py +583 -0
- agent_ablation/models.py +162 -0
- agent_ablation/reporters/__init__.py +10 -0
- agent_ablation/reporters/formatters.py +130 -0
- agent_ablation-0.3.0.dist-info/METADATA +275 -0
- agent_ablation-0.3.0.dist-info/RECORD +15 -0
- agent_ablation-0.3.0.dist-info/WHEEL +5 -0
- agent_ablation-0.3.0.dist-info/licenses/LICENSE +21 -0
- agent_ablation-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent-ablation: Leave-one-out ablation testing, backward elimination, and ROI evaluation for multi-agent systems.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from agent_ablation.adapters.autogen import from_autogen_messages
|
|
6
|
+
from agent_ablation.adapters.crewai import from_crewai_tasks
|
|
7
|
+
from agent_ablation.adapters.langgraph import from_langgraph_messages, from_records
|
|
8
|
+
from agent_ablation.adapters.vercel import from_ai_sdk_steps
|
|
9
|
+
from agent_ablation.core import (
|
|
10
|
+
aggregate_batch_results,
|
|
11
|
+
batch_ablation,
|
|
12
|
+
batch_ablation_async,
|
|
13
|
+
majority_vote,
|
|
14
|
+
run_ablation,
|
|
15
|
+
run_ablation_async,
|
|
16
|
+
run_backward_elimination,
|
|
17
|
+
run_backward_elimination_async,
|
|
18
|
+
run_pairwise_ablation,
|
|
19
|
+
)
|
|
20
|
+
from agent_ablation.models import (
|
|
21
|
+
AblationResult,
|
|
22
|
+
AgentRoiMetrics,
|
|
23
|
+
BackwardEliminationResult,
|
|
24
|
+
BackwardEliminationStep,
|
|
25
|
+
BatchAblationSummary,
|
|
26
|
+
Finding,
|
|
27
|
+
GroundTruthSummary,
|
|
28
|
+
PairwiseAblationItem,
|
|
29
|
+
PairwiseAblationResult,
|
|
30
|
+
PerAgentAblation,
|
|
31
|
+
PerAgentStats,
|
|
32
|
+
PruningRecommendation,
|
|
33
|
+
RoiSummary,
|
|
34
|
+
)
|
|
35
|
+
from agent_ablation.reporters.formatters import (
|
|
36
|
+
format_ascii_table,
|
|
37
|
+
format_markdown_report,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = "0.3.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Models
|
|
44
|
+
"Finding",
|
|
45
|
+
"PerAgentAblation",
|
|
46
|
+
"AblationResult",
|
|
47
|
+
"AgentRoiMetrics",
|
|
48
|
+
"PruningRecommendation",
|
|
49
|
+
"RoiSummary",
|
|
50
|
+
"GroundTruthSummary",
|
|
51
|
+
"PerAgentStats",
|
|
52
|
+
"BatchAblationSummary",
|
|
53
|
+
"BackwardEliminationStep",
|
|
54
|
+
"BackwardEliminationResult",
|
|
55
|
+
"PairwiseAblationItem",
|
|
56
|
+
"PairwiseAblationResult",
|
|
57
|
+
# Core Functions
|
|
58
|
+
"run_ablation",
|
|
59
|
+
"run_ablation_async",
|
|
60
|
+
"batch_ablation",
|
|
61
|
+
"batch_ablation_async",
|
|
62
|
+
"aggregate_batch_results",
|
|
63
|
+
"run_backward_elimination",
|
|
64
|
+
"run_backward_elimination_async",
|
|
65
|
+
"run_pairwise_ablation",
|
|
66
|
+
"majority_vote",
|
|
67
|
+
# Adapters
|
|
68
|
+
"from_langgraph_messages",
|
|
69
|
+
"from_records",
|
|
70
|
+
"from_crewai_tasks",
|
|
71
|
+
"from_autogen_messages",
|
|
72
|
+
"from_ai_sdk_steps",
|
|
73
|
+
# Reporters
|
|
74
|
+
"format_markdown_report",
|
|
75
|
+
"format_ascii_table",
|
|
76
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework adapters for agent-ablation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from agent_ablation.adapters.autogen import from_autogen_messages
|
|
6
|
+
from agent_ablation.adapters.crewai import from_crewai_tasks
|
|
7
|
+
from agent_ablation.adapters.langgraph import from_langgraph_messages, from_records
|
|
8
|
+
from agent_ablation.adapters.vercel import from_ai_sdk_steps
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"from_langgraph_messages",
|
|
12
|
+
"from_records",
|
|
13
|
+
"from_crewai_tasks",
|
|
14
|
+
"from_autogen_messages",
|
|
15
|
+
"from_ai_sdk_steps",
|
|
16
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapter for AutoGen multi-agent chat message histories.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
8
|
+
|
|
9
|
+
from agent_ablation.models import Finding
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_field(obj: Any, key: str, default: Any = None) -> Any:
|
|
13
|
+
if isinstance(obj, dict):
|
|
14
|
+
return obj.get(key, default)
|
|
15
|
+
return getattr(obj, key, default)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def from_autogen_messages(
|
|
19
|
+
messages: Sequence[Any],
|
|
20
|
+
score_of: Callable[[Any], float],
|
|
21
|
+
confidence_of: Optional[Callable[[Any], Optional[float]]] = None,
|
|
22
|
+
tokens_of: Optional[Callable[[Any], Optional[int]]] = None,
|
|
23
|
+
cost_of: Optional[Callable[[Any], Optional[float]]] = None,
|
|
24
|
+
) -> List[Finding]:
|
|
25
|
+
"""
|
|
26
|
+
Converts AutoGen conversation history into `Finding` objects.
|
|
27
|
+
"""
|
|
28
|
+
findings: List[Finding] = []
|
|
29
|
+
|
|
30
|
+
for message in messages:
|
|
31
|
+
if message is None:
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
name = _get_field(message, "name")
|
|
35
|
+
role = _get_field(message, "role")
|
|
36
|
+
|
|
37
|
+
agent_id: Optional[str] = None
|
|
38
|
+
if isinstance(name, str) and name.strip():
|
|
39
|
+
agent_id = name.strip()
|
|
40
|
+
elif isinstance(role, str) and role not in ("user", "system") and role.strip():
|
|
41
|
+
agent_id = role.strip()
|
|
42
|
+
|
|
43
|
+
if not agent_id:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
score = float(score_of(message))
|
|
47
|
+
confidence = float(confidence_of(message)) if confidence_of and confidence_of(message) is not None else None
|
|
48
|
+
tokens = int(tokens_of(message)) if tokens_of and tokens_of(message) is not None else None
|
|
49
|
+
cost = float(cost_of(message)) if cost_of and cost_of(message) is not None else None
|
|
50
|
+
|
|
51
|
+
content = _get_field(message, "content")
|
|
52
|
+
context = _get_field(message, "context")
|
|
53
|
+
|
|
54
|
+
metadata: Dict[str, Any] = {}
|
|
55
|
+
if isinstance(content, dict):
|
|
56
|
+
metadata.update(content)
|
|
57
|
+
elif content is not None:
|
|
58
|
+
metadata["raw"] = content
|
|
59
|
+
|
|
60
|
+
if isinstance(context, dict):
|
|
61
|
+
metadata["context"] = context
|
|
62
|
+
|
|
63
|
+
finding = Finding(
|
|
64
|
+
agent_id=agent_id,
|
|
65
|
+
score=score,
|
|
66
|
+
confidence=confidence,
|
|
67
|
+
tokens=tokens,
|
|
68
|
+
cost=cost,
|
|
69
|
+
metadata=metadata if metadata else None,
|
|
70
|
+
)
|
|
71
|
+
findings.append(finding)
|
|
72
|
+
|
|
73
|
+
return findings
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapter for CrewAI task outputs and agent payloads.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
8
|
+
|
|
9
|
+
from agent_ablation.models import Finding
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_field(obj: Any, key: str, default: Any = None) -> Any:
|
|
13
|
+
if isinstance(obj, dict):
|
|
14
|
+
return obj.get(key, default)
|
|
15
|
+
return getattr(obj, key, default)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def from_crewai_tasks(
|
|
19
|
+
outputs: Sequence[Any],
|
|
20
|
+
score_of: Optional[Callable[[Any], float]] = None,
|
|
21
|
+
agent_id_of: Optional[Callable[[Any], str]] = None,
|
|
22
|
+
confidence_of: Optional[Callable[[Any], Optional[float]]] = None,
|
|
23
|
+
cost_of: Optional[Callable[[Any], Optional[float]]] = None,
|
|
24
|
+
tokens_of: Optional[Callable[[Any], Optional[int]]] = None,
|
|
25
|
+
) -> List[Finding]:
|
|
26
|
+
"""
|
|
27
|
+
Converts CrewAI task outputs or agent payloads into `Finding` objects.
|
|
28
|
+
"""
|
|
29
|
+
findings: List[Finding] = []
|
|
30
|
+
|
|
31
|
+
for output in outputs:
|
|
32
|
+
if output is None:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
agent_id: Optional[str] = None
|
|
36
|
+
if agent_id_of is not None:
|
|
37
|
+
agent_id = agent_id_of(output)
|
|
38
|
+
else:
|
|
39
|
+
agent_val = _get_field(output, "agent")
|
|
40
|
+
if isinstance(agent_val, str):
|
|
41
|
+
agent_id = agent_val
|
|
42
|
+
elif agent_val is not None:
|
|
43
|
+
agent_id = _get_field(agent_val, "role") or _get_field(agent_val, "name")
|
|
44
|
+
|
|
45
|
+
if not agent_id or not isinstance(agent_id, str) or not agent_id.strip():
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
agent_id = agent_id.strip()
|
|
49
|
+
|
|
50
|
+
score: float = 0.0
|
|
51
|
+
json_dict = _get_field(output, "json_dict")
|
|
52
|
+
raw = _get_field(output, "raw")
|
|
53
|
+
|
|
54
|
+
if score_of is not None:
|
|
55
|
+
score = float(score_of(output))
|
|
56
|
+
else:
|
|
57
|
+
raw_score = _get_field(output, "score")
|
|
58
|
+
if raw_score is not None:
|
|
59
|
+
score = float(raw_score)
|
|
60
|
+
elif isinstance(json_dict, dict) and "score" in json_dict:
|
|
61
|
+
score = float(json_dict["score"])
|
|
62
|
+
|
|
63
|
+
confidence: Optional[float] = None
|
|
64
|
+
if confidence_of is not None:
|
|
65
|
+
conf_val = confidence_of(output)
|
|
66
|
+
if conf_val is not None:
|
|
67
|
+
confidence = float(conf_val)
|
|
68
|
+
elif isinstance(json_dict, dict) and "confidence" in json_dict:
|
|
69
|
+
confidence = float(json_dict["confidence"])
|
|
70
|
+
|
|
71
|
+
cost: Optional[float] = None
|
|
72
|
+
if cost_of is not None:
|
|
73
|
+
cost_val = cost_of(output)
|
|
74
|
+
if cost_val is not None:
|
|
75
|
+
cost = float(cost_val)
|
|
76
|
+
else:
|
|
77
|
+
cost_val = _get_field(output, "cost")
|
|
78
|
+
if cost_val is not None:
|
|
79
|
+
cost = float(cost_val)
|
|
80
|
+
|
|
81
|
+
tokens: Optional[int] = None
|
|
82
|
+
if tokens_of is not None:
|
|
83
|
+
tokens_val = tokens_of(output)
|
|
84
|
+
if tokens_val is not None:
|
|
85
|
+
tokens = int(tokens_val)
|
|
86
|
+
else:
|
|
87
|
+
tokens_val = _get_field(output, "tokens")
|
|
88
|
+
if tokens_val is not None:
|
|
89
|
+
tokens = int(tokens_val)
|
|
90
|
+
|
|
91
|
+
latency_ms_val = _get_field(output, "latency_ms") or _get_field(output, "latencyMs")
|
|
92
|
+
latency_ms = float(latency_ms_val) if latency_ms_val is not None else None
|
|
93
|
+
|
|
94
|
+
metadata: Dict[str, Any] = {}
|
|
95
|
+
if isinstance(json_dict, dict):
|
|
96
|
+
metadata.update(json_dict)
|
|
97
|
+
if raw:
|
|
98
|
+
metadata["raw"] = raw
|
|
99
|
+
|
|
100
|
+
finding = Finding(
|
|
101
|
+
agent_id=agent_id,
|
|
102
|
+
score=score,
|
|
103
|
+
confidence=confidence,
|
|
104
|
+
cost=cost,
|
|
105
|
+
tokens=tokens,
|
|
106
|
+
latency_ms=latency_ms,
|
|
107
|
+
metadata=metadata if metadata else None,
|
|
108
|
+
)
|
|
109
|
+
findings.append(finding)
|
|
110
|
+
|
|
111
|
+
return findings
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapter for LangGraph / LangChain agent message histories and generic records.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
|
|
8
|
+
|
|
9
|
+
from agent_ablation.models import Finding
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_field(obj: Any, key: str, default: Any = None) -> Any:
|
|
13
|
+
if isinstance(obj, dict):
|
|
14
|
+
return obj.get(key, default)
|
|
15
|
+
return getattr(obj, key, default)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def from_langgraph_messages(
|
|
19
|
+
messages: Sequence[Any],
|
|
20
|
+
score_of: Callable[[Any], float],
|
|
21
|
+
confidence_of: Optional[Callable[[Any], Optional[float]]] = None,
|
|
22
|
+
) -> List[Finding]:
|
|
23
|
+
"""
|
|
24
|
+
Converts a sequence of LangGraph-style agent messages into `Finding` objects for ablation.
|
|
25
|
+
|
|
26
|
+
Rules:
|
|
27
|
+
- Entries without a valid string `name` are ignored.
|
|
28
|
+
- `score_of(message)` is invoked for each named message to extract its score.
|
|
29
|
+
- `confidence_of(message)` is invoked if provided.
|
|
30
|
+
- Object/dict `content` is stored directly as `metadata`.
|
|
31
|
+
- String or other primitive `content` is wrapped inside `{"raw": content}` as `metadata`.
|
|
32
|
+
|
|
33
|
+
:param messages: Sequence of message-like objects or dicts (e.g. `state.messages`).
|
|
34
|
+
:param score_of: Required mapping function to extract the numerical score from each message.
|
|
35
|
+
:param confidence_of: Optional mapping function to extract a numerical confidence.
|
|
36
|
+
:returns: List of `Finding` objects ready for `run_ablation`.
|
|
37
|
+
"""
|
|
38
|
+
findings: List[Finding] = []
|
|
39
|
+
|
|
40
|
+
for message in messages:
|
|
41
|
+
if message is None:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
name = _get_field(message, "name")
|
|
45
|
+
if not isinstance(name, str) or not name.strip():
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
agent_id = name.strip()
|
|
49
|
+
score = float(score_of(message))
|
|
50
|
+
confidence = float(confidence_of(message)) if confidence_of and confidence_of(message) is not None else None
|
|
51
|
+
|
|
52
|
+
content = _get_field(message, "content")
|
|
53
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
54
|
+
|
|
55
|
+
if isinstance(content, dict):
|
|
56
|
+
metadata = dict(content)
|
|
57
|
+
elif isinstance(content, str):
|
|
58
|
+
metadata = {"raw": content}
|
|
59
|
+
elif content is not None:
|
|
60
|
+
metadata = {"raw": content}
|
|
61
|
+
|
|
62
|
+
finding = Finding(
|
|
63
|
+
agent_id=agent_id,
|
|
64
|
+
score=score,
|
|
65
|
+
confidence=confidence,
|
|
66
|
+
metadata=metadata,
|
|
67
|
+
)
|
|
68
|
+
findings.append(finding)
|
|
69
|
+
|
|
70
|
+
return findings
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def from_records(
|
|
74
|
+
records: Sequence[Any],
|
|
75
|
+
agent_id: Callable[[Any, int], str],
|
|
76
|
+
score_of: Callable[[Any, int], float],
|
|
77
|
+
confidence_of: Optional[Callable[[Any, int], Optional[float]]] = None,
|
|
78
|
+
) -> List[Finding]:
|
|
79
|
+
"""
|
|
80
|
+
Converts an arbitrary sequence of records into `Finding` objects using user-supplied mapping functions.
|
|
81
|
+
Preserves the original record in `metadata`.
|
|
82
|
+
|
|
83
|
+
:param records: Sequence of arbitrary records or dicts.
|
|
84
|
+
:param agent_id: Function returning the agent ID string for each record `(record, index) -> str`.
|
|
85
|
+
:param score_of: Function returning the score for each record `(record, index) -> float`.
|
|
86
|
+
:param confidence_of: Optional function returning confidence `(record, index) -> Optional[float]`.
|
|
87
|
+
:returns: List of `Finding` objects ready for `run_ablation`.
|
|
88
|
+
"""
|
|
89
|
+
findings: List[Finding] = []
|
|
90
|
+
|
|
91
|
+
for index, record in enumerate(records):
|
|
92
|
+
aid = agent_id(record, index)
|
|
93
|
+
score = float(score_of(record, index))
|
|
94
|
+
confidence = float(confidence_of(record, index)) if confidence_of and confidence_of(record, index) is not None else None
|
|
95
|
+
|
|
96
|
+
if isinstance(record, dict):
|
|
97
|
+
metadata: Dict[str, Any] = dict(record)
|
|
98
|
+
else:
|
|
99
|
+
metadata = {"raw": record}
|
|
100
|
+
|
|
101
|
+
finding = Finding(
|
|
102
|
+
agent_id=aid,
|
|
103
|
+
score=score,
|
|
104
|
+
confidence=confidence,
|
|
105
|
+
metadata=metadata,
|
|
106
|
+
)
|
|
107
|
+
findings.append(finding)
|
|
108
|
+
|
|
109
|
+
return findings
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapter for Vercel AI SDK step and tool execution traces.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
8
|
+
|
|
9
|
+
from agent_ablation.models import Finding
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_field(obj: Any, key: str, default: Any = None) -> Any:
|
|
13
|
+
if isinstance(obj, dict):
|
|
14
|
+
return obj.get(key, default)
|
|
15
|
+
return getattr(obj, key, default)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def from_ai_sdk_steps(
|
|
19
|
+
steps: Sequence[Any],
|
|
20
|
+
score_of: Callable[[Any], float],
|
|
21
|
+
agent_id_of: Optional[Callable[[Any], str]] = None,
|
|
22
|
+
confidence_of: Optional[Callable[[Any], Optional[float]]] = None,
|
|
23
|
+
cost_of: Optional[Callable[[Any], Optional[float]]] = None,
|
|
24
|
+
) -> List[Finding]:
|
|
25
|
+
"""
|
|
26
|
+
Converts Vercel AI SDK tool call steps or agent trace steps into `Finding` objects.
|
|
27
|
+
"""
|
|
28
|
+
findings: List[Finding] = []
|
|
29
|
+
|
|
30
|
+
for step in steps:
|
|
31
|
+
if step is None:
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
agent_id: Optional[str] = None
|
|
35
|
+
if agent_id_of is not None:
|
|
36
|
+
agent_id = agent_id_of(step)
|
|
37
|
+
else:
|
|
38
|
+
agent_id = (
|
|
39
|
+
_get_field(step, "toolName")
|
|
40
|
+
or _get_field(step, "tool_name")
|
|
41
|
+
or _get_field(step, "stepType")
|
|
42
|
+
or _get_field(step, "step_type")
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if not agent_id or not isinstance(agent_id, str) or not agent_id.strip():
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
agent_id = agent_id.strip()
|
|
49
|
+
score = float(score_of(step))
|
|
50
|
+
confidence = float(confidence_of(step)) if confidence_of and confidence_of(step) is not None else None
|
|
51
|
+
cost = float(cost_of(step)) if cost_of and cost_of(step) is not None else None
|
|
52
|
+
|
|
53
|
+
usage = _get_field(step, "usage")
|
|
54
|
+
tokens: Optional[int] = None
|
|
55
|
+
if isinstance(usage, dict):
|
|
56
|
+
if "totalTokens" in usage:
|
|
57
|
+
tokens = int(usage["totalTokens"])
|
|
58
|
+
elif "total_tokens" in usage:
|
|
59
|
+
tokens = int(usage["total_tokens"])
|
|
60
|
+
else:
|
|
61
|
+
prompt_t = usage.get("promptTokens", usage.get("prompt_tokens", 0))
|
|
62
|
+
comp_t = usage.get("completionTokens", usage.get("completion_tokens", 0))
|
|
63
|
+
total = (prompt_t or 0) + (comp_t or 0)
|
|
64
|
+
tokens = int(total) if total > 0 else None
|
|
65
|
+
elif usage is not None:
|
|
66
|
+
total_t = getattr(usage, "total_tokens", getattr(usage, "totalTokens", None))
|
|
67
|
+
if total_t is not None:
|
|
68
|
+
tokens = int(total_t)
|
|
69
|
+
|
|
70
|
+
latency_ms_val = _get_field(step, "latencyMs") or _get_field(step, "latency_ms")
|
|
71
|
+
latency_ms = float(latency_ms_val) if latency_ms_val is not None else None
|
|
72
|
+
|
|
73
|
+
result_val = _get_field(step, "result")
|
|
74
|
+
args_val = _get_field(step, "args")
|
|
75
|
+
|
|
76
|
+
metadata: Dict[str, Any] = {}
|
|
77
|
+
if isinstance(result_val, dict):
|
|
78
|
+
metadata.update(result_val)
|
|
79
|
+
elif result_val is not None:
|
|
80
|
+
metadata["result"] = result_val
|
|
81
|
+
|
|
82
|
+
if args_val is not None:
|
|
83
|
+
metadata["args"] = args_val
|
|
84
|
+
|
|
85
|
+
finding = Finding(
|
|
86
|
+
agent_id=agent_id,
|
|
87
|
+
score=score,
|
|
88
|
+
confidence=confidence,
|
|
89
|
+
tokens=tokens,
|
|
90
|
+
cost=cost,
|
|
91
|
+
latency_ms=latency_ms,
|
|
92
|
+
metadata=metadata if metadata else None,
|
|
93
|
+
)
|
|
94
|
+
findings.append(finding)
|
|
95
|
+
|
|
96
|
+
return findings
|