Skip to content

Commit dbef526

Browse files
committed
python tools requirement
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
1 parent e68e1b2 commit dbef526

4 files changed

Lines changed: 1272 additions & 0 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# pytest: ollama, e2e, qualitative
2+
"""Granite 4.1 repairs the three canonical plotting failures with Python tool.
3+
4+
This example demonstrates:
5+
1. Creating a PythonToolRequirements bundle for plotting validation
6+
2. Using SOFAI sampling strategy with repair feedback loop
7+
3. Granite 4.1 repairing through: syntax → imports → headless backend → savefig
8+
9+
Canonical task: "Create a plot of sin(x) for x in 0..2π and save to /tmp/plot.png"
10+
11+
The model will encounter and repair:
12+
- Attempt 1: Missing matplotlib.use('Agg') (non-headless backend)
13+
- Attempt 2: Missing plt.savefig() call
14+
- Attempt 3: Success with both fixes applied
15+
16+
The requirements bundle provides actionable failure messages that guide the model
17+
through each repair iteration without explicit instruction.
18+
"""
19+
20+
import mellea
21+
from mellea.backends import ModelOption
22+
from mellea.backends.tools import MelleaTool
23+
from mellea.stdlib.components import Instruction
24+
from mellea.stdlib.context import ChatContext
25+
from mellea.stdlib.requirements import PythonToolRequirements
26+
from mellea.stdlib.sampling import SOFAISamplingStrategy
27+
from mellea.stdlib.tools import local_code_interpreter
28+
from mellea.stdlib.tools.interpreter import ExecutionResult
29+
30+
31+
def python(code: str) -> ExecutionResult:
32+
"""Execute Python code.
33+
34+
Args:
35+
code: Python code to execute
36+
37+
Returns:
38+
Execution result containing stdout, stderr, and success status
39+
"""
40+
return local_code_interpreter(code)
41+
42+
43+
async def main():
44+
"""Run the canonical plotting repair example."""
45+
import tempfile
46+
from pathlib import Path
47+
48+
with tempfile.TemporaryDirectory() as tmpdir:
49+
output_path = str(Path(tmpdir) / "plot.png")
50+
51+
# Initialize session with local backend
52+
m = mellea.start_session()
53+
54+
# Create requirements bundle for plotting validation
55+
# Allows matplotlib import (no output_path = skip file creation check)
56+
bundle = PythonToolRequirements(allowed_imports=["numpy", "matplotlib", "math"])
57+
58+
# Define SOFAI strategy for repair: S1 (fast) up to 3 times, then S2 (slow)
59+
sampling_strategy = SOFAISamplingStrategy(
60+
s1_solver_backend=m.backend,
61+
s2_solver_backend=m.backend,
62+
s2_solver_mode="fresh_start",
63+
loop_budget=3,
64+
feedback_strategy="first_error",
65+
)
66+
67+
# Create the plotting task instruction
68+
description = f"""Create a plot of sin(x) for x in 0..2π and save it to {output_path}.
69+
70+
Requirements:
71+
- Use the python tool to execute your code
72+
- Import numpy and matplotlib
73+
- Generate x values from 0 to 2π
74+
- Plot sin(x) against x
75+
- Save the plot to the specified file path
76+
77+
Use the python tool with your complete code."""
78+
instruction = Instruction(description=description)
79+
80+
# Create a chat context for multi-turn repair
81+
ctx = ChatContext()
82+
83+
print("=" * 70)
84+
print("Testing Granite 4.1's ability to repair plotting failures")
85+
print("=" * 70)
86+
print(f"Task: Create a plot of sin(x) and save to {output_path}\n")
87+
88+
try:
89+
# Run the sampling strategy with requirements
90+
result = await sampling_strategy.sample(
91+
action=instruction,
92+
context=ctx,
93+
backend=m.backend,
94+
requirements=bundle.requirements,
95+
tool_calls=True,
96+
model_options={ModelOption.TOOLS: [MelleaTool.from_callable(python)]},
97+
)
98+
99+
print(f"\nResult: {'SUCCESS' if result.success else 'FAILED'}\n")
100+
101+
if result.success:
102+
print("✓ Granite 4.1 successfully generated and executed plotting code")
103+
print("\nFinal generated code:")
104+
print("-" * 70)
105+
print(result.result.value)
106+
print("-" * 70)
107+
108+
# Verify output file exists
109+
from pathlib import Path
110+
111+
if Path(output_path).exists(): # noqa: ASYNC240
112+
file_size = Path(output_path).stat().st_size # noqa: ASYNC240
113+
print(f"\n✓ Output file created: {output_path}")
114+
print(f" File size: {file_size} bytes")
115+
else:
116+
print(f"\n✗ Output file not found: {output_path}")
117+
118+
# Print repair history
119+
print(f"\nRepair iterations: {len(result.sample_validations)}")
120+
for attempt_idx, validations in enumerate(result.sample_validations, 1):
121+
passed = sum(1 for _, val in validations if val.as_bool())
122+
total = len(validations)
123+
status = "✓" if passed == total else "✗"
124+
print(
125+
f" {status} Attempt {attempt_idx}: {passed}/{total} "
126+
f"requirements passed"
127+
)
128+
129+
# Show which requirements failed
130+
for req, val in validations:
131+
if not val.as_bool():
132+
print(f" - {req.description}")
133+
if val.reason:
134+
reason_preview = val.reason[:100].replace("\n", " ")
135+
print(f" Error: {reason_preview}...")
136+
137+
else:
138+
print("✗ Failed to generate working plotting code after all attempts\n")
139+
print("Last attempt output:")
140+
print("-" * 70)
141+
print(result.result.value)
142+
print("-" * 70)
143+
144+
# Print failure history
145+
print(f"\nFailure history ({len(result.sample_validations)} attempts):")
146+
for attempt_idx, validations in enumerate(result.sample_validations, 1):
147+
failed_count = sum(1 for _, val in validations if not val.as_bool())
148+
if failed_count > 0:
149+
print(f"\n Attempt {attempt_idx}:")
150+
for req, val in validations:
151+
if not val.as_bool():
152+
print(f" - {req.description}")
153+
if val.reason:
154+
reason_lines = val.reason.split("\n")[:2]
155+
for line in reason_lines:
156+
print(f" {line}")
157+
158+
except Exception as e:
159+
print(f"✗ Exception during sampling: {e}")
160+
import traceback
161+
162+
traceback.print_exc()
163+
164+
print("\n" + "=" * 70)
165+
print("Test completed")
166+
print("=" * 70)
167+
168+
169+
if __name__ == "__main__":
170+
import asyncio
171+
172+
asyncio.run(main())

mellea/stdlib/requirements/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ...core import Requirement, ValidationResult, default_output_to_bool
55
from .md import as_markdown_list, is_markdown_list, is_markdown_table
66
from .python_reqs import PythonExecutionReq
7+
from .python_tools import PythonToolRequirements
78
from .requirement import (
89
ALoraRequirement,
910
LLMaJRequirement,
@@ -19,6 +20,7 @@
1920
"ALoraRequirement",
2021
"LLMaJRequirement",
2122
"PythonExecutionReq",
23+
"PythonToolRequirements",
2224
"Requirement",
2325
"ValidationResult",
2426
"as_markdown_list",

0 commit comments

Comments
 (0)