Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ea4a21f
feat(agents): validator DATA profile for data-generating snippets
MarkusNeusinger Oct 14, 2026
2fe5793
feat(agents): harness data mode with an audit-hook guard
MarkusNeusinger Oct 14, 2026
b31f789
feat(renderer): POST /run-data runs a data snippet in the render slot
MarkusNeusinger Oct 14, 2026
2cc810d
feat(agents): run_data_code tool runs a data snippet in the sandbox
MarkusNeusinger Oct 14, 2026
4e29817
feat(agents): dataset route refuses code with 422 code before parsing
MarkusNeusinger Oct 14, 2026
b77ddde
feat(agents): root and scope rubric for data code, with eval cases
MarkusNeusinger Oct 14, 2026
3e9fd19
feat(app): offer pasted code to the chat and show the code behind the…
MarkusNeusinger Oct 14, 2026
51cc691
docs(agents): run_data_code, POST /run-data and the data-code threats
MarkusNeusinger Oct 14, 2026
b86bc3c
docs(agents): name rate_start as the one rate helper for sandbox runs
MarkusNeusinger Oct 14, 2026
f1fc39a
fix(agents): close the frame and literal bypasses of run_data_code
MarkusNeusinger Oct 14, 2026
2cab798
Merge origin/main into feat/run-data-code
MarkusNeusinger Oct 14, 2026
f6b15ae
test(agents): the DATA profile refuses frame introspection (#12169)
MarkusNeusinger Oct 14, 2026
d7fe269
feat(agents): the computation decides between generate_data and run_d…
MarkusNeusinger Oct 14, 2026
6dea341
Merge origin/main into feat/run-data-code
MarkusNeusinger Oct 14, 2026
1a70e3e
chore: changelog PR reference
MarkusNeusinger Oct 14, 2026
10903bd
fix(agents): linear-time code classifier; data judge tokens count aga…
MarkusNeusinger Oct 14, 2026
eaac925
fix(agents): mypy on agents (parent lookup and decorator lines)
MarkusNeusinger Oct 14, 2026
b09ba0d
Merge origin/main into feat/run-data-code
MarkusNeusinger Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions agents/README.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion agents/anyplot/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ async def session_context(context: ReadonlyContext) -> str:
dataset = services.datasets.get(view.dataset_id, session_id) if view.dataset_id else None
if dataset is None:
lines.append(
"- Dataset: none yet; the user pastes it in the data panel, or generate_data computes it from a formula"
"- Dataset: none yet; the user pastes it in the data panel, or generate_data computes it from a formula, "
"or run_data_code from the user's code"
)
else:
profile = dataset.profile
Expand Down
64 changes: 64 additions & 0 deletions agents/anyplot/code/data_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""The data snippet before it runs: the `__main__` guard stripped, then the DATA profile of the validator.

`run_data_code` (`tools/session.py`) runs a snippet the root translated to Python from
what the user pasted or typed. Scripts often end in `if __name__ == "__main__":`,
which the dunder rule refuses; the guard is boilerplate, not intent, so
`strip_main_guard` replaces every top-level guard without an `else` by its dedented
body before validation, deterministically, instead of asking the root to remember it.
A guard with an `else`, or one that is not top-level, stays and fails the dunder rule.
No ADK import.
"""

import ast
import textwrap

from .validate import Finding, validate_data_code


def _is_main_test(test: ast.expr) -> bool:
"""`__name__ == "__main__"`, either way round."""
if not (isinstance(test, ast.Compare) and len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)):
return False
sides = [test.left, test.comparators[0]]
names = [side for side in sides if isinstance(side, ast.Name) and side.id == "__name__"]
texts = [side for side in sides if isinstance(side, ast.Constant) and side.value == "__main__"]
return len(names) == 1 and len(texts) == 1


def _first_line(statement: ast.stmt) -> int:
"""The first line of a statement, its decorators included."""
decorators: list[ast.expr] = getattr(statement, "decorator_list", [])
return min([statement.lineno, *(decorator.lineno for decorator in decorators)])


def strip_main_guard(code: str) -> str:
"""`code` with every top-level `if __name__ == "__main__":` (no `else`) replaced by its dedented body.

Code that does not parse is returned unchanged: the validator reports its syntax error.
"""
try:
tree = ast.parse(code)
except (SyntaxError, ValueError, RecursionError):
return code
guards = [node for node in tree.body if isinstance(node, ast.If) and not node.orelse and _is_main_test(node.test)]
if not guards:
return code
lines = code.splitlines(keepends=True)
for guard in reversed(guards): # from the bottom, so the line numbers above stay valid
end = guard.end_lineno or guard.lineno
first = _first_line(guard.body[0])
if first == guard.lineno: # `if __name__ == "__main__": main()` on one line
segments = [ast.get_source_segment(code, statement) or "" for statement in guard.body]
body = "".join(segment + "\n" for segment in segments)
else:
body = textwrap.dedent("".join(lines[first - 1 : end]))
if not body.endswith("\n"):
body += "\n"
lines[guard.lineno - 1 : end] = [body]
return "".join(lines)


def check_data_code(code: str) -> tuple[str, list[Finding]]:
"""The snippet as it will run (the guard stripped) and its DATA-profile findings; empty findings mean it may run."""
prepared = strip_main_guard(code)
return prepared, validate_data_code(prepared)
Loading
Loading