Skip to content

Commit 7cd5d8b

Browse files
committed
Add NumGuardTool: verify a number before an agent asserts it
1 parent b14d36b commit 7cd5d8b

4 files changed

Lines changed: 120 additions & 0 deletions

File tree

lib/crewai-tools/src/crewai_tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
from crewai_tools.tools.multion_tool.multion_tool import MultiOnTool
119119
from crewai_tools.tools.mysql_search_tool.mysql_search_tool import MySQLSearchTool
120120
from crewai_tools.tools.nl2sql.nl2sql_tool import NL2SQLTool
121+
from crewai_tools.tools.numguard_tool.numguard_tool import NumGuardTool
121122
from crewai_tools.tools.ocr_tool.ocr_tool import OCRTool
122123
from crewai_tools.tools.oxylabs_amazon_product_scraper_tool.oxylabs_amazon_product_scraper_tool import (
123124
OxylabsAmazonProductScraperTool,
@@ -281,6 +282,7 @@
281282
"MultiOnTool",
282283
"MySQLSearchTool",
283284
"NL2SQLTool",
285+
"NumGuardTool",
284286
"OCRTool",
285287
"OxylabsAmazonProductScraperTool",
286288
"OxylabsAmazonSearchScraperTool",

lib/crewai-tools/src/crewai_tools/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
from crewai_tools.tools.multion_tool.multion_tool import MultiOnTool
109109
from crewai_tools.tools.mysql_search_tool.mysql_search_tool import MySQLSearchTool
110110
from crewai_tools.tools.nl2sql.nl2sql_tool import NL2SQLTool
111+
from crewai_tools.tools.numguard_tool.numguard_tool import NumGuardTool
111112
from crewai_tools.tools.ocr_tool.ocr_tool import OCRTool
112113
from crewai_tools.tools.oxylabs_amazon_product_scraper_tool.oxylabs_amazon_product_scraper_tool import (
113114
OxylabsAmazonProductScraperTool,
@@ -265,6 +266,7 @@
265266
"MultiOnTool",
266267
"MySQLSearchTool",
267268
"NL2SQLTool",
269+
"NumGuardTool",
268270
"OCRTool",
269271
"OxylabsAmazonProductScraperTool",
270272
"OxylabsAmazonSearchScraperTool",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# NumGuardTool
2+
3+
Verify a number before an agent asserts it. `NumGuardTool` routes a numeric claim to the statistical check that
4+
most directly tests it and returns whether it survives — so a crew can gate a backtest Sharpe, an A/B accuracy
5+
gap, a cherry-picked subset win, or an LLM-judge preference before reporting it as real.
6+
7+
Backed by the open-source [`numguard`](https://github.com/ipezygj/numguard) library (a Deflated Sharpe Ratio for
8+
backtests plus eval-integrity statistics).
9+
10+
## Installation
11+
12+
```shell
13+
pip install numguard
14+
```
15+
16+
## Supported claims
17+
18+
| `kind` | Checks | Example `params` |
19+
|---|---|---|
20+
| `backtest` | Deflated Sharpe Ratio (multiple-testing + finite-sample) | `{"sr": 0.12, "T": 250, "n_trials": 100}` |
21+
| `model_gap` | Is an accuracy gap above the detectable effect? | `{"n": 2000, "p1": 0.85, "p2": 0.80}` |
22+
| `subset_win` | Does a subset win survive multiple-testing correction? | `{"p": 0.03, "n_tests": 20}` |
23+
| `judge_bias` | Is an LLM-judge preference real or noise/position bias? | `{"wins": 68, "n": 100}` |
24+
25+
## Example
26+
27+
```python
28+
from crewai_tools import NumGuardTool
29+
30+
tool = NumGuardTool()
31+
32+
# an overfit backtest — the best of 100 configs
33+
print(tool.run(kind="backtest", params={"sr": 0.12, "T": 250, "n_trials": 100}))
34+
# {"survives": false, "verdict": "... does NOT survive deflation ...", "detail": {...}}
35+
36+
# a genuine edge — one hypothesis, long sample
37+
print(tool.run(kind="backtest", params={"sr": 0.15, "T": 1000, "n_trials": 1}))
38+
# {"survives": true, "verdict": "... SURVIVES deflation ...", "detail": {...}}
39+
```
40+
41+
Give the tool to an agent so it can check a number instead of asserting it:
42+
43+
```python
44+
from crewai import Agent
45+
from crewai_tools import NumGuardTool
46+
47+
analyst = Agent(
48+
role="Quant Analyst",
49+
goal="Only report backtest results that survive verification",
50+
tools=[NumGuardTool()],
51+
)
52+
```
53+
54+
The tool returns a JSON string with `survives` (bool), a human-readable `verdict`, and the full `detail`.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import json
2+
import logging
3+
from typing import List, Type
4+
5+
from crewai.tools import BaseTool, EnvVar
6+
from pydantic import BaseModel, Field
7+
8+
logger = logging.getLogger(__file__)
9+
10+
_EXAMPLES = (
11+
"backtest -> {'sr': 0.12, 'T': 250, 'n_trials': 100}; "
12+
"model_gap -> {'n': 2000, 'p1': 0.85, 'p2': 0.80}; "
13+
"subset_win -> {'p': 0.03, 'n_tests': 20}; "
14+
"judge_bias -> {'wins': 68, 'n': 100}"
15+
)
16+
17+
18+
class NumGuardToolInput(BaseModel):
19+
kind: str = Field(
20+
...,
21+
description="The claim to check: 'backtest' (Deflated Sharpe Ratio), 'model_gap' (accuracy gap "
22+
"power), 'subset_win' (multiple-testing correction), or 'judge_bias' (LLM-judge preference).",
23+
)
24+
params: dict = Field(
25+
...,
26+
description=f"Inputs for the check. Examples: {_EXAMPLES}.",
27+
)
28+
29+
30+
class NumGuardTool(BaseTool):
31+
"""Verify a number before an agent asserts it.
32+
33+
Routes a numeric claim to the statistical check that most directly tests it and returns whether it survives
34+
plus an honest verdict — so an agent can gate a backtest Sharpe, an A/B accuracy gap, a cherry-picked subset
35+
win, or an LLM-judge preference before reporting it as real. Backed by the open-source ``numguard`` library
36+
(Deflated Sharpe Ratio + eval-integrity statistics).
37+
"""
38+
39+
name: str = "NumGuard Number Verifier"
40+
description: str = (
41+
"Verify a statistical claim before asserting it. Give it a 'kind' (backtest / model_gap / subset_win / "
42+
"judge_bias) and 'params'; it returns whether the number survives its check and the honest verdict to "
43+
"report instead. Use it before you state a backtest Sharpe, an A/B result, a subset win, or a judge "
44+
"preference as a fact."
45+
)
46+
args_schema: Type[BaseModel] = NumGuardToolInput
47+
package_dependencies: List[str] = ["numguard"]
48+
env_vars: List[EnvVar] = []
49+
50+
def _run(self, kind: str, params: dict) -> str:
51+
try:
52+
from numguard.guard import check
53+
except ImportError:
54+
return "numguard is not installed. Run: pip install numguard"
55+
try:
56+
verdict = check(kind, **(params or {}))
57+
except Exception as e:
58+
logger.error(f"NumGuardTool error: {e}")
59+
return f"Could not verify claim '{kind}': {e}"
60+
survives = bool(verdict.get("survives"))
61+
line = verdict.get("verdict") or ("survives" if survives else "flagged")
62+
return json.dumps({"survives": survives, "verdict": line, "detail": verdict}, default=str)

0 commit comments

Comments
 (0)