"""Ancrage pour du code : les modules et les noms importés existent-ils vraiment dans l'environnement ? L'hallucination typique d'un petit modèle de code n'est pas une fausse citation mais une API inventée (`from math import sqrtt`). La vérification se fait dans le bac à sable, jamais dans le processus du Space. """ from __future__ import annotations import ast import json from mouh.skills.sandbox import SandboxLimits, run_in_sandbox _PROBE = ( "import importlib, json, sys\n" "bad = []\n" "for mod, names in json.loads(sys.argv[1]):\n" " try:\n" " m = importlib.import_module(mod)\n" " except Exception:\n" " bad.append(mod)\n" " continue\n" " for n in names:\n" " if not hasattr(m, n):\n" " try:\n" " importlib.import_module(mod + '.' + n)\n" " except Exception:\n" " bad.append(mod + '.' + n)\n" "print(json.dumps(bad))\n" ) def imported_names(files: dict[str, str]) -> list[tuple[str, list[str]]]: """[(module, [noms])] des imports absolus, hors modules fournis par la proposition elle-même.""" local = {name[:-3].replace("/", ".") for name in files if name.endswith(".py")} wanted: list[tuple[str, list[str]]] = [] for code in files.values(): try: tree = ast.parse(code) except SyntaxError: return [] # la porte de compilation et pytest le signalent mieux for node in ast.walk(tree): if isinstance(node, ast.Import): wanted += [(a.name, []) for a in node.names] elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0: wanted.append((node.module, [a.name for a in node.names if a.name != "*"])) return [(m, n) for m, n in wanted if m.split(".")[0] not in local] class ApiCheck: def __init__(self, timeout_s: float = 10.0) -> None: self.timeout_s = timeout_s self._memo: dict[str, list[str]] = {} def check(self, files, notes=None) -> list[str]: wanted = imported_names(files) if not wanted: return [] key = json.dumps(wanted, sort_keys=True) if key not in self._memo: run = run_in_sandbox(["probe.py", key], {"probe.py": _PROBE}, timeout_s=self.timeout_s, limits=SandboxLimits(cpu_seconds=int(self.timeout_s))) try: bad = json.loads(run.stdout.strip().splitlines()[-1]) except (ValueError, IndexError): bad = [] self._memo[key] = [f"API introuvable : {b}" for b in bad] return list(self._memo[key])