Download tools.py from DHDRL/AgentSquares: direct link, hf CLI and curl.
- Browser
- Download file 7.69 kB
-
https://huggingface.co/DHDRL/AgentSquares/resolve/main/tools.py
- Command line
-
hf download hf://DHDRL/AgentSquares/tools.py
-
curl -L -o tools.py https://huggingface.co/DHDRL/AgentSquares/resolve/main/tools.py
7.69 kB
| """Tool interfaces: search, fetch, extract. | |
| - MockTools: scenario-driven, with failure injection (eval harness; no network). | |
| - NetTools: optional real fetch of public pages via an network client; search is a fixed seed-URL stub | |
| (no scraping of search engines; wire official source APIs later if needed). | |
| - extract_claims: LLM-free regex extractor with plausibility guards (default). | |
| - Optional LLM extraction is wired through interfaces.LLMExtractor + providers.json (SQ_LLM_* env). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import math | |
| import os | |
| import re | |
| from bounds import area_lower_bound, trivial_upper_bound | |
| class ToolError(Exception): | |
| pass | |
| class MockTools: | |
| """tools spec (from scenario JSON): | |
| {"search": [ {url, date, source_type, source_key?}, ... ] | "error", | |
| "search_fail_times": int, # transient failures before success (-1 = always) | |
| "pages": {url: {"text": str, "fail_times": int, "error": str}}}""" | |
| def __init__(self, spec): | |
| self.spec = spec | |
| self.calls = {} | |
| def _maybe_fail(self, key, fail_times, err): | |
| k = self.calls.get(key, 0) | |
| self.calls[key] = k + 1 | |
| if fail_times == -1 or k < fail_times: | |
| raise ToolError(f"{key}: {err}") | |
| def search(self, query): | |
| if self.spec.get("search") == "error": | |
| raise ToolError("search: status 503") | |
| self._maybe_fail("search", self.spec.get("search_fail_times", 0), "rate limited (429)") | |
| return list(self.spec.get("search", [])) | |
| def fetch(self, url): | |
| page = self.spec.get("pages", {}).get(url) | |
| if page is None: | |
| raise ToolError(f"fetch {url}: 404") | |
| self._maybe_fail(url, page.get("fail_times", 0), page.get("error", "timeout")) | |
| if "text_file" in page: # frozen real capture | |
| with open(page["text_file"], encoding="utf-8") as f: | |
| return f.read() | |
| return page.get("text", "") | |
| class NetTools: | |
| """Real-network tools for manual runs. Search returns configured seed URLs only.""" | |
| def __init__(self, seeds, timeout=20.0): | |
| self.seeds = seeds # list of {url, date, source_type} | |
| self.timeout = timeout | |
| def search(self, query): | |
| return list(self.seeds) | |
| def fetch(self, url): | |
| import httpx # optional dependency | |
| try: | |
| r = httpx.get(url, timeout=self.timeout, follow_redirects=True, | |
| headers={"User-Agent": "squares-progress-agent/0.1"}) | |
| r.raise_for_status() | |
| except Exception as e: # network errors become ToolError for uniform handling | |
| raise ToolError(f"fetch {url}: {e}") from e | |
| text = re.sub(r"<script.*?</script>|<style.*?</style>", " ", r.text, flags=re.S | re.I) | |
| return re.sub(r"<[^>]+>", " ", text) | |
| # ----------------------------------------------------------------- extraction | |
| NUM = r"([0-9]+(?:[.,][0-9]+)?)" | |
| REL = r"\s*(?:=|≈|~|:|is|≤|<=)\s*" | |
| PATTERNS = [ | |
| re.compile(r"\bs\s*\(\s*(\d{1,4})\s*\)" + REL + NUM), | |
| re.compile(r"\bn\s*=\s*(\d{1,4})\b[^\n]{0,60}?\b(?:s|L|side(?: length)?|container side)" + REL + NUM), | |
| re.compile(r"\b(\d{1,4})\s+(?:unit\s+)?squares?\b[^\n]{0,60}?\b(?:side|container)[^\n0-9]{0,30}?" + NUM), | |
| ] | |
| TABLE_HEADER = re.compile(r"^\s*\|\s*n\s*\|\s*(?:s|side|L|best s)\s*\|", re.I) | |
| TABLE_ROW = re.compile(r"^\s*\|\s*(\d{1,4})\s*\|\s*" + NUM + r"\s*\|") | |
| SKIP_LINE = re.compile(r"\b(retract\w*|withdrawn|was wrong|was a bug|target|goal|hope|if we|would be)\b", re.I) | |
| SCORE = re.compile(r"score\s*[=:]?\s*(-?[0-9]+\.[0-9]+)", re.I) | |
| BLOCK = re.compile(r"```packing\s*\n(.*?)(?:```|\Z)", re.S) | |
| def _num(v): | |
| return float(v.replace(",", ".")) | |
| TRUNC_AFTER = re.compile(r"^\s*(?:…|\.\.\.)") | |
| APPROX_BEFORE = re.compile(r"(?:≈|~|approx\w*\.?)\s*$", re.I) | |
| def display_uncertainty(line, start, end, raw): | |
| """Absolute uncertainty (container-side units) implied by how a number is printed. | |
| '2.707...' / '2.707…' (truncated) or '≈ 2.707' (rounded) -> 10**-decimals; otherwise 0.""" | |
| dec = len(raw.replace(",", ".").split(".")[1]) if re.search(r"[.,]", raw) else 0 | |
| if TRUNC_AFTER.match(line[end:]) or APPROX_BEFORE.search(line[max(0, start - 16):start]): | |
| return 10.0 ** -dec | |
| return 0.0 | |
| def extract_claims(text, meta): | |
| """Return (claims, notes). Each claim: n, s, coordinates_available, packing, score, | |
| source_url, source_type, source_key, date, extracted_text.""" | |
| claims, notes = [], [] | |
| base = {"source_url": meta["url"], "source_type": meta.get("source_type", "unknown"), | |
| "source_key": meta.get("source_key", meta["url"]), "date": meta.get("date", "")} | |
| def add(n, s, quote, packing=None, score=None, unc=0.0): | |
| if not 1 <= n <= 1000: | |
| notes.append(f"ignored n={n} (out of range)"); return | |
| if s is not None: | |
| if s < area_lower_bound(n) - 1e-9: | |
| notes.append(f"rejected n={n} s={s}: below area bound sqrt(n)={area_lower_bound(n):.6f}") | |
| claims.append({**base, "n": n, "s": s, "rejected": "impossible (area bound)", | |
| "coordinates_available": packing is not None, "packing": packing, | |
| "score": score, "extracted_text": quote[:160]}) | |
| return | |
| if s > trivial_upper_bound(n) + 1e-9: | |
| notes.append(f"ignored n={n} s={s}: worse than trivial grid (likely noise)"); return | |
| claims.append({**base, "n": n, "s": s, "coordinates_available": packing is not None, | |
| "packing": packing, "score": score, "s_uncertainty": unc, "extracted_text": quote[:160]}) | |
| # 1) structured packing blocks (coordinates) | |
| for m in BLOCK.finditer(text): | |
| raw = m.group(1) | |
| try: | |
| d = json.loads(raw) | |
| n, side = int(d["n"]), d["side"] | |
| s = float(eval_side(side)) | |
| add(n, s, raw[:160], packing=d) | |
| except Exception as e: # truncated/malformed block -> partial observation | |
| notes.append(f"unparseable packing block ({type(e).__name__})") | |
| text_wo_blocks = BLOCK.sub(" ", text) | |
| # 2) line-level claims | |
| in_table = False | |
| seen = set() | |
| for line in text_wo_blocks.splitlines(): | |
| if TABLE_HEADER.match(line): | |
| in_table = True; continue | |
| if in_table: | |
| m = TABLE_ROW.match(line) | |
| if m: | |
| key = (int(m.group(1)), _num(m.group(2))) | |
| if key not in seen: | |
| seen.add(key); add(key[0], key[1], line.strip()) | |
| continue | |
| if not line.strip().startswith("|"): | |
| in_table = False | |
| if SKIP_LINE.search(line): | |
| if any(p.search(line) for p in PATTERNS): | |
| notes.append(f"skipped hedged/retracted line: {line.strip()[:80]}") | |
| continue | |
| sc = SCORE.search(line) | |
| for p in PATTERNS: | |
| for m in p.finditer(line): | |
| key = (int(m.group(1)), _num(m.group(2))) | |
| if key in seen: | |
| continue | |
| seen.add(key) | |
| add(key[0], key[1], line.strip(), score=float(sc.group(1)) if sc else None, | |
| unc=display_uncertainty(line, m.start(2), m.end(2), m.group(2))) | |
| return claims, notes | |
| def eval_side(side): | |
| """side may be a number or an exact string like '7/5'. Returns Fraction or float.""" | |
| from fractions import Fraction | |
| if isinstance(side, str) and re.fullmatch(r"\s*-?\d+(?:/\d+)?\s*", side): | |
| return Fraction(side.strip()) | |
| return float(side) | |
| # LLM extraction lives in interfaces.LLMExtractor (same interface as the rule extractor). | |