agentomatic 0.5.1__tar.gz → 0.6.0__tar.gz

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.
Files changed (109) hide show
  1. {agentomatic-0.5.1 → agentomatic-0.6.0}/PKG-INFO +1 -1
  2. {agentomatic-0.5.1 → agentomatic-0.6.0}/pyproject.toml +2 -1
  3. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/_version.py +1 -1
  4. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/config.py +32 -0
  5. agentomatic-0.6.0/src/agentomatic/optimize/context.py +276 -0
  6. agentomatic-0.6.0/src/agentomatic/optimize/dashboard.py +562 -0
  7. agentomatic-0.6.0/src/agentomatic/optimize/events.py +248 -0
  8. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/fitter.py +207 -7
  9. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/fitter_optimizers.py +195 -102
  10. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/loop.py +148 -25
  11. agentomatic-0.6.0/src/agentomatic/optimize/progress.py +642 -0
  12. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_fitter.py +3 -3
  13. agentomatic-0.6.0/tests/test_optimizer_dashboard.py +64 -0
  14. agentomatic-0.6.0/tests/test_optimizer_events.py +65 -0
  15. agentomatic-0.6.0/tests/test_optimizer_progress.py +59 -0
  16. {agentomatic-0.5.1 → agentomatic-0.6.0}/LICENSE +0 -0
  17. {agentomatic-0.5.1 → agentomatic-0.6.0}/README.md +0 -0
  18. {agentomatic-0.5.1 → agentomatic-0.6.0}/examples/full_agent/README.md +0 -0
  19. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/__init__.py +0 -0
  20. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/cli/__init__.py +0 -0
  21. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/cli/commands.py +0 -0
  22. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/cli/templates.py +0 -0
  23. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/config/__init__.py +0 -0
  24. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/config/defaults.py +0 -0
  25. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/config/settings.py +0 -0
  26. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/core/__init__.py +0 -0
  27. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/core/lifespan.py +0 -0
  28. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/core/manifest.py +0 -0
  29. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/core/memory_manager.py +0 -0
  30. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/core/platform.py +0 -0
  31. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/core/registry.py +0 -0
  32. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/core/router_factory.py +0 -0
  33. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/core/state.py +0 -0
  34. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/demo/__init__.py +0 -0
  35. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/demo/agent.py +0 -0
  36. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/demo/server.py +0 -0
  37. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/middleware/__init__.py +0 -0
  38. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/middleware/auth.py +0 -0
  39. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/middleware/feedback.py +0 -0
  40. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/middleware/logging.py +0 -0
  41. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/middleware/metrics.py +0 -0
  42. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/middleware/rate_limit.py +0 -0
  43. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/observability/__init__.py +0 -0
  44. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/observability/concurrency.py +0 -0
  45. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/observability/metrics.py +0 -0
  46. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/observability/telemetry.py +0 -0
  47. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/__init__.py +0 -0
  48. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/dataset.py +0 -0
  49. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/deployment.py +0 -0
  50. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/eval_contract.py +0 -0
  51. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/failure_analysis.py +0 -0
  52. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/judges.py +0 -0
  53. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/llm_caller.py +0 -0
  54. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/metrics.py +0 -0
  55. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/optimizer.py +0 -0
  56. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/report.py +0 -0
  57. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/runner.py +0 -0
  58. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/search_space.py +0 -0
  59. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/strategies.py +0 -0
  60. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/optimize/synthesizer.py +0 -0
  61. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/prompts/__init__.py +0 -0
  62. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/prompts/manager.py +0 -0
  63. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/protocols/__init__.py +0 -0
  64. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/protocols/decorators.py +0 -0
  65. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/providers/__init__.py +0 -0
  66. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/providers/embeddings.py +0 -0
  67. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/providers/llm.py +0 -0
  68. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/py.typed +0 -0
  69. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/storage/__init__.py +0 -0
  70. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/storage/base.py +0 -0
  71. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/storage/checkpointer.py +0 -0
  72. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/storage/memory.py +0 -0
  73. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/storage/models.py +0 -0
  74. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/storage/sqlalchemy.py +0 -0
  75. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/__init__.py +0 -0
  76. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/adapter.py +0 -0
  77. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/adapters/__init__.py +0 -0
  78. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/adapters/generic.py +0 -0
  79. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/adapters/langchain.py +0 -0
  80. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/adapters/langgraph.py +0 -0
  81. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/decorators.py +0 -0
  82. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/graph_inspector.py +0 -0
  83. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/models.py +0 -0
  84. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/router.py +0 -0
  85. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/run_tracker.py +0 -0
  86. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/serve.py +0 -0
  87. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/asset-manifest.json +0 -0
  88. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/imgs/graph_view.png +0 -0
  89. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/imgs/logo.png +0 -0
  90. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/imgs/main_screen.png +0 -0
  91. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/imgs/main_screen_ok.png +0 -0
  92. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/index.html +0 -0
  93. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/manifest.json +0 -0
  94. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/static/css/main.8b3c42dd.css +0 -0
  95. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/static/css/main.8b3c42dd.css.map +0 -0
  96. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/static/js/main.cbb2a8ee.js +0 -0
  97. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/static/js/main.cbb2a8ee.js.LICENSE.txt +0 -0
  98. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/studio/static/static/js/main.cbb2a8ee.js.map +0 -0
  99. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/ui/__init__.py +0 -0
  100. {agentomatic-0.5.1 → agentomatic-0.6.0}/src/agentomatic/ui/chat.py +0 -0
  101. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_agentomatic.py +0 -0
  102. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_bugfixes_041.py +0 -0
  103. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_cli.py +0 -0
  104. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_coverage_boost.py +0 -0
  105. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_deep_agent.py +0 -0
  106. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_integration.py +0 -0
  107. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_optimize.py +0 -0
  108. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_platform_features.py +0 -0
  109. {agentomatic-0.5.1 → agentomatic-0.6.0}/tests/test_studio.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentomatic
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Drop agents, not code — zero-code multi-agent API platform framework
5
5
  Project-URL: Homepage, https://github.com/UnicoLab/agentomatic
6
6
  Project-URL: Documentation, https://unicolab.github.io/agentomatic
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentomatic"
3
- version = "0.5.1"
3
+ version = "0.6.0"
4
4
  description = "Drop agents, not code — zero-code multi-agent API platform framework"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -85,6 +85,7 @@ Changelog = "https://github.com/UnicoLab/agentomatic/blob/main/CHANGELOG.md"
85
85
 
86
86
 
87
87
 
88
+
88
89
  [dependency-groups]
89
90
  dev = [
90
91
  "pytest>=8",
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.5.1"
5
+ __version__ = "0.6.0"
@@ -47,6 +47,28 @@ from typing import Any
47
47
 
48
48
  from loguru import logger
49
49
 
50
+
51
+ def escape_braces(text: str, preserve: list[str] | None = None) -> str:
52
+ """Escape literal braces while preserving specific template variables.
53
+
54
+ Replaces '{' with '{{' and '}' with '}}', except for variables listed in `preserve`
55
+ (e.g., 'query' for '{query}'). This prevents KeyError during subsequent .format() calls
56
+ on system prompts that contain literal JSON or markdown code blocks.
57
+ """
58
+ if not text:
59
+ return text
60
+
61
+ # First escape all
62
+ escaped = text.replace("{", "{{").replace("}", "}}")
63
+
64
+ # Then unescape the specific variables we want to preserve
65
+ if preserve:
66
+ for var in preserve:
67
+ escaped = escaped.replace(f"{{{{{var}}}}}", f"{{{var}}}")
68
+
69
+ return escaped
70
+
71
+
50
72
  # =====================================================================
51
73
  # Runtime configuration
52
74
  # =====================================================================
@@ -88,6 +110,16 @@ class PromptRuntimeConfig:
88
110
  fallback_model: str | None = None # automatic fallback model
89
111
  routing_config: dict[str, Any] = field(default_factory=dict) # A/B weights, local/remote
90
112
 
113
+ @property
114
+ def safe_system_prompt(self) -> str:
115
+ """Returns the system prompt with literal braces safely escaped.
116
+
117
+ Assumes the system prompt does not contain runtime format variables.
118
+ Useful when passing the system prompt into LangChain or other formatters
119
+ that might crash on literal JSON blocks.
120
+ """
121
+ return escape_braces(self.system_prompt)
122
+
91
123
  # -- serialization ---------------------------------------------------
92
124
 
93
125
  def to_dict(self) -> dict[str, Any]:
@@ -0,0 +1,276 @@
1
+ """Rich context objects for informed prompt rewriting.
2
+
3
+ Provides :class:`OptimizationContext` — a structured accumulator of
4
+ evaluation state, score history, pipeline metadata, and dataset
5
+ characteristics that optimisers receive to make deeply informed
6
+ rewrite decisions.
7
+
8
+ Example::
9
+
10
+ ctx = OptimizationContext(
11
+ baseline_score=0.72,
12
+ current_score=0.85,
13
+ score_history=[
14
+ RoundStats(round_idx=0, score=0.72, dims={"relevance": 0.68}),
15
+ RoundStats(round_idx=1, score=0.79, dims={"relevance": 0.75}),
16
+ ],
17
+ )
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass, field
23
+ from typing import Any
24
+
25
+ # =====================================================================
26
+ # Score history per round
27
+ # =====================================================================
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class RoundStats:
32
+ """Snapshot of scores and metadata from a single optimisation round.
33
+
34
+ Stored in :attr:`OptimizationContext.score_history` so the rewrite
35
+ model can see performance trends across iterations.
36
+ """
37
+
38
+ round_idx: int = 0
39
+ score: float = 0.0
40
+ dims: dict[str, float] = field(default_factory=dict)
41
+ best_candidate: str = ""
42
+ accepted: bool = False
43
+ n_candidates: int = 0
44
+ elapsed_seconds: float = 0.0
45
+
46
+
47
+ # =====================================================================
48
+ # Dataset summary
49
+ # =====================================================================
50
+
51
+
52
+ @dataclass(slots=True)
53
+ class DatasetSummary:
54
+ """Lightweight summary of dataset characteristics.
55
+
56
+ Included in :class:`OptimizationContext` so optimisers can adapt
57
+ their strategies based on dataset size and composition.
58
+ """
59
+
60
+ n_samples: int = 0
61
+ categories: list[str] = field(default_factory=list)
62
+ avg_query_length: int = 0
63
+ avg_expected_length: int = 0
64
+ has_context: bool = False
65
+
66
+
67
+ # =====================================================================
68
+ # Full optimisation context
69
+ # =====================================================================
70
+
71
+
72
+ @dataclass(slots=True)
73
+ class OptimizationContext:
74
+ """Rich context passed to optimisers for informed prompt rewriting.
75
+
76
+ Accumulates state across iterations so the rewriting model can
77
+ reason about *trends* and *root causes* rather than treating each
78
+ round as an independent rewrite.
79
+
80
+ Attributes:
81
+ baseline_score: Score of the original, unoptimised config.
82
+ baseline_dims: Per-dimension scores at baseline.
83
+ current_score: Score of the current-best config.
84
+ current_dims: Per-dimension scores for the current best.
85
+ score_history: List of :class:`RoundStats` for previous rounds.
86
+ failure_clusters: Failure-analysis clusters (label, count, fix).
87
+ eval_details: Full per-point evaluation results with pipeline
88
+ context (``retrieval_context``, ``tool_calls``, ``reasoning``,
89
+ ``citations``, ``metadata``, ``duration_ms``).
90
+ dataset_summary: Size and shape of the evaluation dataset.
91
+ metric_names: Names of the metrics being evaluated.
92
+ round_idx: Current round index.
93
+ total_rounds: Total planned rounds.
94
+ """
95
+
96
+ # ── Scores ───────────────────────────────────────────────────────
97
+ baseline_score: float = 0.0
98
+ baseline_dims: dict[str, float] = field(default_factory=dict)
99
+ current_score: float = 0.0
100
+ current_dims: dict[str, float] = field(default_factory=dict)
101
+
102
+ # ── History ──────────────────────────────────────────────────────
103
+ score_history: list[RoundStats] = field(default_factory=list)
104
+
105
+ # ── Failure analysis ─────────────────────────────────────────────
106
+ failure_clusters: list[dict[str, Any]] = field(default_factory=list)
107
+
108
+ # ── Evaluation detail (with pipeline context) ────────────────────
109
+ eval_details: list[dict[str, Any]] = field(default_factory=list)
110
+
111
+ # ── Dataset ──────────────────────────────────────────────────────
112
+ dataset_summary: DatasetSummary = field(default_factory=DatasetSummary)
113
+ metric_names: list[str] = field(default_factory=list)
114
+
115
+ # ── Progress ─────────────────────────────────────────────────────
116
+ round_idx: int = 0
117
+ total_rounds: int = 0
118
+
119
+ # ── Formatting helpers ───────────────────────────────────────────
120
+
121
+ def format_score_history(self, max_rounds: int = 5) -> str:
122
+ """Format recent score history as a human-readable string.
123
+
124
+ Args:
125
+ max_rounds: Maximum number of recent rounds to include.
126
+
127
+ Returns:
128
+ Multi-line string showing score evolution with trend
129
+ indicators.
130
+ """
131
+ if not self.score_history:
132
+ return "No previous rounds."
133
+
134
+ recent = self.score_history[-max_rounds:]
135
+ lines: list[str] = []
136
+ for i, rs in enumerate(recent):
137
+ trend = ""
138
+ if i > 0:
139
+ prev = recent[i - 1].score
140
+ delta = rs.score - prev
141
+ trend = f" (Δ {delta:+.4f})" if abs(delta) > 1e-6 else " (=)"
142
+ accepted = "✅" if rs.accepted else "❌"
143
+ lines.append(
144
+ f" Round {rs.round_idx + 1}: {rs.score:.4f}{trend} "
145
+ f"{accepted} ({rs.n_candidates} candidates, "
146
+ f"{rs.elapsed_seconds:.1f}s)"
147
+ )
148
+
149
+ return "\n".join(lines)
150
+
151
+ def format_score_sparkline(self) -> str:
152
+ """Format score evolution as an inline sparkline.
153
+
154
+ Returns:
155
+ String like ``📈 0.72 → 0.79 → 0.85``.
156
+ """
157
+ if not self.score_history:
158
+ return f"📊 baseline={self.baseline_score:.3f}"
159
+
160
+ scores = [self.baseline_score] + [rs.score for rs in self.score_history]
161
+ parts = [f"{s:.3f}" for s in scores]
162
+ trend = "📈" if scores[-1] > scores[0] else "📉"
163
+ return f"{trend} {' → '.join(parts)}"
164
+
165
+ def format_dimension_table(self) -> str:
166
+ """Format per-dimension comparison (baseline vs current).
167
+
168
+ Returns:
169
+ Multi-line table of dimension scores with deltas.
170
+ """
171
+ all_dims = sorted(set(self.baseline_dims) | set(self.current_dims))
172
+ if not all_dims:
173
+ return "No per-dimension scores available."
174
+
175
+ lines: list[str] = [
176
+ f" {'Dimension':<20} {'Baseline':>10} {'Current':>10} {'Delta':>10}",
177
+ f" {'─' * 20} {'─' * 10} {'─' * 10} {'─' * 10}",
178
+ ]
179
+ for dim in all_dims:
180
+ base = self.baseline_dims.get(dim, 0.0)
181
+ curr = self.current_dims.get(dim, 0.0)
182
+ delta = curr - base
183
+ lines.append(f" {dim:<20} {base:>10.4f} {curr:>10.4f} {delta:>+10.4f}")
184
+ return "\n".join(lines)
185
+
186
+ def format_failure_clusters(self) -> str:
187
+ """Format failure clusters for the rewrite prompt.
188
+
189
+ Returns:
190
+ Multi-line summary of failure categories.
191
+ """
192
+ if not self.failure_clusters:
193
+ return "No failure clusters identified."
194
+
195
+ lines: list[str] = []
196
+ for i, cluster in enumerate(self.failure_clusters, 1):
197
+ label = cluster.get("label", "unknown")
198
+ count = cluster.get("count", 0)
199
+ fix = cluster.get("suggested_fix", "N/A")
200
+ severity = cluster.get("severity", 0.0)
201
+ lines.append(f" {i}. [{severity:.2f}] {label} ({count} cases): {fix[:120]}")
202
+ return "\n".join(lines)
203
+
204
+ def format_eval_details_for_rewrite(
205
+ self,
206
+ max_failures: int = 5,
207
+ max_successes: int = 3,
208
+ ) -> str:
209
+ """Format evaluation details for inclusion in a rewrite prompt.
210
+
211
+ Includes full pipeline context (retrieval docs, tool calls,
212
+ reasoning) for the lowest-scoring and highest-scoring results.
213
+
214
+ Args:
215
+ max_failures: Number of worst results to include.
216
+ max_successes: Number of best results to include.
217
+
218
+ Returns:
219
+ Multi-section string with failures and successes.
220
+ """
221
+ if not self.eval_details:
222
+ return "No evaluation details available."
223
+
224
+ scored = sorted(
225
+ self.eval_details,
226
+ key=lambda r: r.get("score", r.get("avg_score", 0.0)),
227
+ )
228
+ failures = scored[:max_failures]
229
+ successes = scored[-max_successes:]
230
+
231
+ lines: list[str] = []
232
+
233
+ # ── Failures ─────────────────────────────────────────────────
234
+ if failures:
235
+ lines.append("### Failures (lowest-scoring)")
236
+ for idx, f in enumerate(failures, 1):
237
+ score = f.get("score", f.get("avg_score", 0.0))
238
+ lines.append(f"\n**Failure {idx}** (score: {score:.3f})")
239
+ lines.append(f"- Query: {f.get('query', 'N/A')[:300]}")
240
+ lines.append(f"- Expected: {str(f.get('expected', 'N/A'))[:300]}")
241
+ lines.append(f"- Response: {f.get('response', 'N/A')[:300]}")
242
+
243
+ feedback = f.get("feedback") or f.get("reason") or f.get("details", "")
244
+ if feedback:
245
+ lines.append(f"- Feedback: {str(feedback)[:250]}")
246
+
247
+ dims = f.get("dimensions", {})
248
+ if dims:
249
+ dim_str = ", ".join(f"{k}={v:.3f}" for k, v in dims.items())
250
+ lines.append(f"- Dimensions: {dim_str}")
251
+
252
+ # Pipeline context
253
+ ret_ctx = f.get("retrieval_context", [])
254
+ if ret_ctx:
255
+ docs = "; ".join(str(d)[:100] for d in ret_ctx[:3])
256
+ lines.append(f"- Retrieved docs: {docs}")
257
+
258
+ tool_calls = f.get("tool_calls", [])
259
+ if tool_calls:
260
+ tools = ", ".join(str(t.get("name", t)) for t in tool_calls[:3])
261
+ lines.append(f"- Tool calls: {tools}")
262
+
263
+ reasoning = f.get("reasoning", "")
264
+ if reasoning:
265
+ lines.append(f"- Reasoning: {str(reasoning)[:200]}")
266
+
267
+ # ── Successes ────────────────────────────────────────────────
268
+ if successes:
269
+ lines.append("\n### Successes (highest-scoring)")
270
+ for idx, s in enumerate(successes, 1):
271
+ score = s.get("score", s.get("avg_score", 0.0))
272
+ lines.append(f"\n**Success {idx}** (score: {score:.3f})")
273
+ lines.append(f"- Query: {s.get('query', 'N/A')[:200]}")
274
+ lines.append(f"- Response: {s.get('response', 'N/A')[:200]}")
275
+
276
+ return "\n".join(lines)