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.
@@ -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