feat(browser): auto-commit con 44 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:49:54 +02:00
parent e2c073b8b7
commit 5b10b419a2
44 changed files with 2543 additions and 28 deletions
@@ -0,0 +1,73 @@
---
name: render_ax_outline
kind: function
lang: py
domain: core
version: "1.0.0"
purity: pure
signature: "def render_ax_outline(nodes: list[dict], max_chars: int = 0) -> str"
description: "Convierte nodos AX tree CDP en un outline indentado jerárquico y legible. Nodos accionables (button, link, textbox, etc.) llevan #ref=nodeId para que el LLM pueda referenciarlos en acciones. Poda nodos ignored y roles sin valor semántico."
tags: [browser, cdp, ax-tree, perception, navegator, pure, llm]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: nodes
desc: "Lista de AXNode en formato CDP (campos: nodeId, role, name, childIds, parentId, ignored). Devuelto por cdp_get_ax_tree. Pasar trim_ax_tree(nodes) antes para reducir ruido."
- name: max_chars
desc: "Si > 0, trunca la salida a ese número de caracteres y añade '…[outline truncado]'. 0 = sin límite (default)."
output: "String multi-línea con el outline indentado. Nodos accionables llevan ' #ref=nodeId' alineado a columna 60. Vacío si nodes está vacío o todos los nodos son ignorados."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/core/render_ax_outline.py"
---
## Ejemplo
```python
from core.render_ax_outline import render_ax_outline
# Nodos de muestra (formato real CDP simplificado)
nodes = [
{"nodeId": "1", "role": {"value": "RootWebArea"}, "name": {"value": "Gmail"},
"childIds": ["2", "3"], "ignored": False},
{"nodeId": "2", "role": {"value": "navigation"}, "name": {"value": ""},
"childIds": ["4", "5"], "ignored": False},
{"nodeId": "3", "role": {"value": "main"}, "name": {"value": ""},
"childIds": ["6"], "ignored": False},
{"nodeId": "4", "role": {"value": "button"}, "name": {"value": "Redactar"},
"childIds": [], "ignored": False},
{"nodeId": "5", "role": {"value": "link"}, "name": {"value": "Recibidos (3)"},
"childIds": [], "ignored": False},
{"nodeId": "6", "role": {"value": "textbox"}, "name": {"value": "Buscar correo"},
"childIds": [], "ignored": False},
]
outline = render_ax_outline(nodes)
print(outline)
# RootWebArea "Gmail"
# navigation
# button "Redactar" #ref=4
# link "Recibidos (3)" #ref=5
# main
# textbox "Buscar correo" #ref=6
# Con límite de caracteres para contexto comprimido:
outline_short = render_ax_outline(nodes, max_chars=100)
```
## Cuando usarla
Después de obtener el AX tree con `cdp_get_ax_tree` (y opcionalmente podarlo con `trim_ax_tree`), cuando necesitas dar al LLM una vista compacta de la página para que decida qué elemento accionar. El outline con `#ref` permite al LLM responder "haz clic en #ref=4" sin ambigüedad. Úsala directamente o como parte del pipeline `cdp_perceive_outline_py_pipelines`.
## Gotchas
- Esta función es pura: no llama a Chrome ni tiene I/O. Solo transforma la lista de nodos → string.
- Pasar los nodos crudos de `cdp_get_ax_tree` funciona, pero el outline será más verboso. Usar `trim_ax_tree` antes reduce el ruido considerablemente.
- Nodos con `ignored: true` se saltan silenciosamente (no aparecen en el outline).
- Roles sin valor semántico (`none`, `presentation`) también se saltan; sus hijos se renderizan un nivel arriba.
- Si `max_chars` corta a mitad de un nodo accionable importante, el LLM no verá su `#ref`. Para páginas grandes usar `cdp_perceive_outline` con `max_chars=20000` o chunking via `chunk_ax_tree`.
+139
View File
@@ -0,0 +1,139 @@
"""Convierte una lista de AXNode CDP en un outline indentado legible por LLMs."""
# Roles que se consideran accionables (el LLM puede referirlos con #ref=nodeId).
_ACTIONABLE_ROLES = frozenset({
"button",
"link",
"textbox",
"searchbox",
"checkbox",
"radio",
"combobox",
"listbox",
"menuitem",
"menuitemcheckbox",
"menuitemradio",
"tab",
"option",
"switch",
"slider",
"spinbutton",
"treeitem",
"gridcell",
})
# Roles sin valor semántico para el outline: se omiten si tampoco tienen hijos.
_SKIP_ROLES = frozenset({"none", "presentation", "ignored"})
def _role_val(node: dict) -> str:
"""Extrae el valor de role del nodo CDP."""
r = node.get("role", {})
if isinstance(r, dict):
return r.get("value", "")
return str(r) if r else ""
def _name_val(node: dict) -> str:
"""Extrae el valor de name del nodo CDP."""
n = node.get("name", {})
if isinstance(n, dict):
return n.get("value", "")
return str(n) if n else ""
def render_ax_outline(nodes: list[dict], max_chars: int = 0) -> str:
"""Convierte nodos AX tree CDP en un outline indentado legible y accionable.
Reconstruye la jerarquía padre→hijo usando childIds/parentId y genera
una representación de texto con indentación de 2 espacios por nivel.
Los nodos accionables llevan un marcador #ref=nodeId para que el LLM
pueda referenciarlos en acciones posteriores.
Args:
nodes: Lista de AXNode en formato CDP (campos: nodeId, role, name,
childIds, parentId, ignored). Formato devuelto por
cdp_get_ax_tree. Se puede pasar trim_ax_tree(nodes) antes
para reducir ruido.
max_chars: Si > 0, trunca la salida a ese número de caracteres y
añade una línea final '…[outline truncado]'. 0 = sin límite.
Returns:
String multi-línea con el outline indentado. Vacío si nodes es vacío
o todos los nodos son ignorados/sin role útil.
"""
if not nodes:
return ""
# Construir lookup por nodeId
by_id: dict[str, dict] = {}
for node in nodes:
nid = node.get("nodeId")
if nid:
by_id[nid] = node
# Detectar nodos raíz: nodeId no aparece como childId de nadie visible
all_child_ids: set[str] = set()
for node in nodes:
for cid in node.get("childIds", []):
all_child_ids.add(cid)
roots = [n for n in nodes if n.get("nodeId") not in all_child_ids]
if not roots:
# Fallback: usar el primer nodo
roots = [nodes[0]]
lines: list[str] = []
def _render_node(node: dict, depth: int) -> None:
"""Renderiza un nodo y sus hijos recursivamente."""
if node.get("ignored", False):
return
role = _role_val(node)
if not role or role in _SKIP_ROLES:
# Nodos sin role útil: intentar renderizar hijos directamente
for cid in node.get("childIds", []):
child = by_id.get(cid)
if child:
_render_node(child, depth)
return
name = _name_val(node)
node_id = node.get("nodeId", "")
indent = " " * depth
# Construir línea base: role ["name"]
if name:
base = f'{indent}{role} "{name}"'
else:
base = f"{indent}{role}"
# Añadir #ref si es accionable
if role in _ACTIONABLE_ROLES and node_id:
# Alinear #ref a columna 60 para legibilidad, o adjunto si la línea es larga
ref_tag = f"#ref={node_id}"
if len(base) < 58:
base = base.ljust(60) + ref_tag
else:
base = base + " " + ref_tag
lines.append(base)
# Renderizar hijos en orden
for cid in node.get("childIds", []):
child = by_id.get(cid)
if child:
_render_node(child, depth + 1)
for root in roots:
_render_node(root, 0)
result = "\n".join(lines)
if max_chars > 0 and len(result) > max_chars:
result = result[:max_chars].rstrip()
result += "\n…[outline truncado]"
return result