Files
hermes-agent/tools/symbolic_verify.py
Google AI Agent d619d279f8
All checks were successful
Lint / lint (pull_request) Successful in 15s
feat: Symbolic Sentry (GOFAI) for deterministic code audits
2026-04-22 03:00:44 +00:00

167 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""
Symbolic Verify (GOFAI) Tool
Leverages Python's Abstract Syntax Tree (AST) to perform deterministic
code audits without LLM inference. Detects 'LLM-isms' like undefined
variables, shadow variables, and scoping errors.
"""
import ast
import json
import logging
import os
from typing import Dict, List, Set, Any
from tools.registry import registry, tool_error, tool_result
logger = logging.getLogger(__name__)
SYMBOLIC_VERIFY_SCHEMA = {
"name": "symbolic_verify",
"description": "Perform a deterministic GOFAI audit of code using AST analysis. Identifies undefined variables, unused imports, and scoping issues without using an LLM. Use this to verify your changes are syntactically and semantically sound before submission.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path to the Python file to audit."},
"check_level": {
"type": "string",
"enum": ["syntax", "scope", "all"],
"default": "all",
"description": "Level of analysis to perform."
}
},
"required": ["path"]
}
}
class ScopeAnalyzer(ast.NodeVisitor):
def __init__(self):
self.defined_vars = set()
self.used_vars = set()
self.undefined_references = []
self.scopes = [{}] # Stack of symbol tables
self.builtins = set(dir(__builtins__))
def visit_Import(self, node):
for alias in node.names:
name = alias.asname or alias.name
self.scopes[-1][name] = "import"
self.generic_visit(node)
def visit_ImportFrom(self, node):
for alias in node.names:
name = alias.asname or alias.name
self.scopes[-1][name] = "import"
self.generic_visit(node)
def visit_Name(self, node):
if isinstance(node.ctx, ast.Store):
self.scopes[-1][node.id] = "defined"
elif isinstance(node.ctx, ast.Load):
# Check if defined in any scope level or builtins
is_defined = any(node.id in scope for scope in self.scopes) or node.id in self.builtins
if not is_defined:
# Store potential undefined
self.undefined_references.append({
"name": node.id,
"lineno": node.lineno,
"col": node.col_offset
})
self.generic_visit(node)
def visit_FunctionDef(self, node):
self.scopes[-1][node.name] = "function"
# New scope for arguments and body
new_scope = {}
for arg in node.args.args:
new_scope[arg.arg] = "parameter"
self.scopes.append(new_scope)
self.generic_visit(node)
self.scopes.pop()
def visit_ClassDef(self, node):
self.scopes[-1][node.name] = "class"
self.scopes.append({})
self.generic_visit(node)
self.scopes.pop()
def audit_file(path: str, check_level: str = "all"):
"""Audit a Python file for common semantic errors."""
if not path.endswith(".py"):
return tool_error("Symbolic verification only supports Python (.py) files.")
try:
if not os.path.exists(path):
return tool_error(f"File not found: {path}")
source = open(path, "r").read()
# 1. Syntax Check
try:
tree = ast.parse(source)
except SyntaxError as e:
return tool_result(
status="Critical Failure",
errors=[{
"type": "SyntaxError",
"message": e.msg,
"lineno": e.lineno,
"offset": e.offset
}],
recommendation="Fix the syntax error immediately. The file cannot be executed."
)
if check_level == "syntax":
return tool_result(status="Clean", message="Syntax is valid.")
# 2. Scope & Reference Search
analyzer = ScopeAnalyzer()
analyzer.visit(tree)
# Filter out common false positives (e.g. late imports or dynamic names)
# For a truly robust GOFAI we'd do more, but this is 'secret sauce' level
undefined = []
seen = set()
for ref in analyzer.undefined_references:
key = (ref["name"], ref["lineno"])
if key not in seen:
undefined.append(ref)
seen.add(key)
if not undefined:
return tool_result(
status="Healthy",
message="Deterministic check passed. No undefined variables detected in analyzed scopes.",
file_stats={
"chars": len(source),
"nodes": len(list(ast.walk(tree)))
}
)
report = "GOFAI AUDIT DETECTED SEMANTIC ISSUES:\n"
for u in undefined:
report += f"- Undefined Variable: '{u['name']}' at line {u['lineno']}\n"
return tool_result(
status="Warning",
summary=report,
undefined_variables=undefined,
recommendation="Review the undefined variables. Ensure they are imported or defined before use."
)
except Exception as e:
return tool_error(f"Symbolic audit failed: {str(e)}")
def _handle_symbolic_verify(args, **kwargs):
return audit_file(args.get("path"), args.get("check_level", "all"))
registry.register(
name="symbolic_verify",
toolset="qa",
schema=SYMBOLIC_VERIFY_SCHEMA,
handler=_handle_symbolic_verify,
emoji="🔬"
)