Skip to content

Commit 48ecada

Browse files
feat: add universal agent tool wrapper, test suite and real-world e2e tool calling example
1 parent df51c17 commit 48ecada

5 files changed

Lines changed: 182 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,17 @@ python -m codeshield run script.py --llm # try Gemini self-healing if con
156156
python -m codeshield run script.py --no-llm # force local fallback
157157
```
158158

159+
## 🤖 Agent Tool Integration (LangChain, CrewAI, Gemini)
160+
161+
```python
162+
from codeshield import create_code_execution_tool
163+
164+
# Pasa la tool directamente a tu agente
165+
tools = [create_code_execution_tool()]
166+
```
167+
168+
`create_code_execution_tool()` returns a ready-to-register `execute_python_code(code: str) -> str` function. It runs the provided Python in a self-healing sandbox and returns either the stdout or a structured error report with `error_type` and `stderr`.
169+
159170
---
160171

161172
## Examples

‎examples/04_agent_tool_dropin.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Example 4: real-world agentic function calling with CodeShield + Gemini."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
7+
from dotenv import load_dotenv
8+
from google import genai
9+
from google.genai import types
10+
11+
from codeshield import create_code_execution_tool
12+
13+
14+
def main() -> None:
15+
load_dotenv()
16+
17+
api_key = os.environ.get("GEMINI_API_KEY")
18+
model = os.environ.get("GEMINI_MODEL", "gemini-3.7-flash")
19+
if not api_key:
20+
print("GEMINI_API_KEY not set. Set it in .env to run this example.")
21+
return
22+
23+
execute_python_code = create_code_execution_tool()
24+
25+
client = genai.Client(api_key=api_key)
26+
27+
prompt = (
28+
"Calculate the cumulative return and the annualized Sharpe Ratio "
29+
"(assuming a risk-free rate of 0.02) for this daily return series:\n"
30+
"[0.012, -0.005, 0.008, 0.015, -0.002, 0.021, -0.010, 0.018]\n\n"
31+
"Use only the Python standard library (math, statistics). "
32+
"Do not use numpy, pandas or any third-party package. "
33+
"Print the cumulative return and the annualized Sharpe ratio clearly."
34+
)
35+
36+
response = client.models.generate_content(
37+
model=model,
38+
contents=prompt,
39+
config=types.GenerateContentConfig(
40+
tools=[execute_python_code],
41+
temperature=0.2,
42+
automatic_function_calling=types.AutomaticFunctionCallingConfig(),
43+
),
44+
)
45+
46+
print("\n--- Final response ---")
47+
print(response.text)
48+
49+
50+
if __name__ == "__main__":
51+
main()

‎src/codeshield/__init__.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,11 @@
77

88
from codeshield.loop import SelfHealingEngine
99
from codeshield.schemas import CodeExecutionRequest, ExecutionResult
10+
from codeshield.tools import create_code_execution_tool
1011

11-
__all__ = ["SelfHealingEngine", "CodeExecutionRequest", "ExecutionResult"]
12+
__all__ = [
13+
"SelfHealingEngine",
14+
"CodeExecutionRequest",
15+
"ExecutionResult",
16+
"create_code_execution_tool",
17+
]

‎src/codeshield/tools.py‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Universal agent tool wrapper for CodeShield.
2+
3+
The function returned by ``create_code_execution_tool`` can be registered as a
4+
tool in any agent framework (LangChain, CrewAI, Google Gen AI, etc.). It runs
5+
the provided Python source inside a self-healing sandbox and returns either the
6+
stdout or a structured error report.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from collections.abc import Callable
12+
13+
from codeshield.environment import SandboxError
14+
from codeshield.loop import SelfHealingEngine, SelfHealingError
15+
from codeshield.runner import SubprocessRunnerError
16+
17+
18+
def create_code_execution_tool(
19+
engine: SelfHealingEngine | None = None,
20+
) -> Callable[[str], str]:
21+
"""Return a drop-in ``execute_python_code(code: str) -> str`` tool.
22+
23+
Args:
24+
engine: Optional ``SelfHealingEngine`` instance. When ``None``, a fresh
25+
engine is created for each tool call.
26+
27+
Returns:
28+
A callable ready to be registered as an agent tool.
29+
"""
30+
31+
def execute_python_code(code: str) -> str:
32+
"""Execute Python code in an isolated, self-healing sandbox.
33+
34+
Use this tool to run numerical, statistical or data-processing
35+
computations that cannot be done directly in the conversation.
36+
37+
Args:
38+
code: A valid Python script as a string.
39+
40+
Returns:
41+
The stdout of the script if execution succeeds, or a structured
42+
error report if it fails after all self-healing attempts.
43+
"""
44+
_engine = engine or SelfHealingEngine()
45+
with _engine:
46+
try:
47+
result, diagnosis = _engine.run(code)
48+
except SelfHealingError as exc:
49+
return f"error_type: SelfHealingError\nmessage: {exc}"
50+
except (SandboxError, SubprocessRunnerError) as exc:
51+
return f"error_type: {type(exc).__name__}\nmessage: {exc}"
52+
53+
if (
54+
result.exit_code == 0
55+
and not result.silent_failure_detected
56+
and not result.timed_out
57+
):
58+
return result.stdout.strip()
59+
60+
report: list[str] = ["The Python script did not execute successfully."]
61+
if diagnosis is not None:
62+
report.append(f"error_type: {diagnosis.error_type}")
63+
if diagnosis.root_cause_line is not None:
64+
report.append(f"root_cause_line: {diagnosis.root_cause_line}")
65+
if diagnosis.message:
66+
report.append(f"message: {diagnosis.message}")
67+
if result.stderr.strip():
68+
report.append(f"stderr: {result.stderr.strip()}")
69+
if result.timed_out:
70+
report.append("timed_out: true")
71+
if result.silent_failure_detected:
72+
report.append("silent_failure_detected: true")
73+
74+
return "\n".join(report)
75+
76+
return execute_python_code

‎tests/test_tools.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Unit tests for the CodeShield agent tool wrapper."""
2+
3+
from __future__ import annotations
4+
5+
from codeshield import create_code_execution_tool
6+
from codeshield.loop import SelfHealingEngine
7+
8+
9+
def test_tool_returns_stdout_on_success() -> None:
10+
"""A clean execution returns the stripped stdout."""
11+
engine = SelfHealingEngine(use_llm=False)
12+
tool = create_code_execution_tool(engine)
13+
14+
result = tool("print('hello from tool')")
15+
16+
assert result == "hello from tool"
17+
18+
19+
def test_tool_reports_controlled_failure() -> None:
20+
"""An unresolved import error surfaces a structured error report."""
21+
engine = SelfHealingEngine(use_llm=False)
22+
tool = create_code_execution_tool(engine)
23+
24+
result = tool("import not_a_real_module_xyz_123")
25+
26+
assert "error_type" in result
27+
assert "ModuleNotFoundError" in result
28+
assert "stderr" in result
29+
30+
31+
def test_tool_without_engine_uses_default() -> None:
32+
"""Calling the tool without an explicit engine still works."""
33+
tool = create_code_execution_tool()
34+
35+
result = tool("print(2 + 2)")
36+
37+
assert result == "4"

0 commit comments

Comments
 (0)