snapcontext 2.0.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {snapcontext-2.0.0/snapcontext.egg-info → snapcontext-2.2.0}/PKG-INFO +21 -4
- {snapcontext-2.0.0 → snapcontext-2.2.0}/README.md +20 -3
- snapcontext-2.2.0/agentes.py +423 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/orquestador.py +11 -33
- {snapcontext-2.0.0 → snapcontext-2.2.0}/pyproject.toml +89 -89
- {snapcontext-2.0.0 → snapcontext-2.2.0/snapcontext.egg-info}/PKG-INFO +21 -4
- {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/SOURCES.txt +2 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.py +416 -38
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_auto_017.py +1 -1
- snapcontext-2.2.0/tests/test_diff_editor_210.py +103 -0
- snapcontext-2.2.0/tests/test_editor_ast_220.py +181 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_editor_propio_200.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_exe_150.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_jetbrains_170.py +2 -2
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_mcp_014.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_mcp_140.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_memoria_015.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_permisos_013.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_plan_012.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_v010.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_validacion_130.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_vscode_016.py +2 -2
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_web_160.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.2.0}/web/app.py +392 -392
- snapcontext-2.0.0/agentes.py +0 -209
- {snapcontext-2.0.0 → snapcontext-2.2.0}/LICENSE +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/setup.cfg +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/dependency_links.txt +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/entry_points.txt +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/requires.txt +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/top_level.txt +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_agentes.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_deteccion.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_embeddings_110.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_orquestador.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_web_120.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/web/__init__.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.2.0}/web/static/index.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snapcontext
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: SnapContext: Asistente de IA con contexto automático para desarrollo
|
|
5
5
|
Author-email: SnapContext Contributors <tu@email.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
# SnapContext
|
|
47
47
|
|
|
48
|
-

|
|
49
49
|
[](https://pypi.org/project/snapcontext/)
|
|
50
50
|
[](https://github.com/NicolasBruña24/snapcontext/actions)
|
|
51
51
|

|
|
@@ -104,14 +104,31 @@ snapcontext interactive # o snapcontext --web
|
|
|
104
104
|
Auto-detección de proyecto (Flutter, Node/React, Python, Go, Rust…) que ajusta
|
|
105
105
|
carpetas, extensiones y comandos de test por defecto.
|
|
106
106
|
|
|
107
|
-
## 🛠️ Editor Integrado Propio (v2.
|
|
107
|
+
## 🛠️ Editor Integrado Propio (v2.2.0)
|
|
108
108
|
|
|
109
|
-
SnapContext
|
|
109
|
+
SnapContext incluye su propio editor integrado con soporte de parches unificados
|
|
110
|
+
y edición basada en AST (Árbol Sintáctico) para refactorizaciones precisas:
|
|
110
111
|
- **`--editor propio`**: Aplica cambios directamente y crea copias de seguridad automáticas en `~/.snapcontext/backups/`.
|
|
112
|
+
- **`--modo-edicion {auto,parche,sobrescribir,ast}`**:
|
|
113
|
+
- `auto` (por defecto): Genera un parche unificado (`git apply` / `patch`), pero
|
|
114
|
+
para refactorizaciones estructurales intenta primero la edición **AST** y cae a
|
|
115
|
+
sobrescritura si nada es aplicable.
|
|
116
|
+
- `ast`: Edita comprendiendo la estructura sintáctica del archivo (Python con
|
|
117
|
+
`ast`, otros lenguajes con `tree-sitter` si está instalado). Soporta renombrar
|
|
118
|
+
símbolos, insertar imports y recibir el código completo resultante.
|
|
119
|
+
- `parche`: Fuerza la aplicación de parches unificados para ediciones precisas.
|
|
120
|
+
- `sobrescribir`: Sobrescribe el archivo completo.
|
|
111
121
|
- **`--editor aider`** (por defecto): Mantiene el flujo de edición con Aider para máxima compatibilidad.
|
|
112
122
|
|
|
113
123
|
```bash
|
|
124
|
+
# Edición precisa mediante diffs unificados
|
|
114
125
|
snapcontext "añadir endpoint de métricas" --editor propio
|
|
126
|
+
|
|
127
|
+
# Forzar modo parche
|
|
128
|
+
snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
|
|
129
|
+
|
|
130
|
+
# Refactorización estructural con AST
|
|
131
|
+
snapcontext "renombrar la variable 'usuario' a 'cuenta'" --editor propio --modo-edicion ast
|
|
115
132
|
```
|
|
116
133
|
|
|
117
134
|
## 🧭 Modos y alias
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SnapContext
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
[](https://pypi.org/project/snapcontext/)
|
|
5
5
|
[](https://github.com/NicolasBruña24/snapcontext/actions)
|
|
6
6
|

|
|
@@ -59,14 +59,31 @@ snapcontext interactive # o snapcontext --web
|
|
|
59
59
|
Auto-detección de proyecto (Flutter, Node/React, Python, Go, Rust…) que ajusta
|
|
60
60
|
carpetas, extensiones y comandos de test por defecto.
|
|
61
61
|
|
|
62
|
-
## 🛠️ Editor Integrado Propio (v2.
|
|
62
|
+
## 🛠️ Editor Integrado Propio (v2.2.0)
|
|
63
63
|
|
|
64
|
-
SnapContext
|
|
64
|
+
SnapContext incluye su propio editor integrado con soporte de parches unificados
|
|
65
|
+
y edición basada en AST (Árbol Sintáctico) para refactorizaciones precisas:
|
|
65
66
|
- **`--editor propio`**: Aplica cambios directamente y crea copias de seguridad automáticas en `~/.snapcontext/backups/`.
|
|
67
|
+
- **`--modo-edicion {auto,parche,sobrescribir,ast}`**:
|
|
68
|
+
- `auto` (por defecto): Genera un parche unificado (`git apply` / `patch`), pero
|
|
69
|
+
para refactorizaciones estructurales intenta primero la edición **AST** y cae a
|
|
70
|
+
sobrescritura si nada es aplicable.
|
|
71
|
+
- `ast`: Edita comprendiendo la estructura sintáctica del archivo (Python con
|
|
72
|
+
`ast`, otros lenguajes con `tree-sitter` si está instalado). Soporta renombrar
|
|
73
|
+
símbolos, insertar imports y recibir el código completo resultante.
|
|
74
|
+
- `parche`: Fuerza la aplicación de parches unificados para ediciones precisas.
|
|
75
|
+
- `sobrescribir`: Sobrescribe el archivo completo.
|
|
66
76
|
- **`--editor aider`** (por defecto): Mantiene el flujo de edición con Aider para máxima compatibilidad.
|
|
67
77
|
|
|
68
78
|
```bash
|
|
79
|
+
# Edición precisa mediante diffs unificados
|
|
69
80
|
snapcontext "añadir endpoint de métricas" --editor propio
|
|
81
|
+
|
|
82
|
+
# Forzar modo parche
|
|
83
|
+
snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
|
|
84
|
+
|
|
85
|
+
# Refactorización estructural con AST
|
|
86
|
+
snapcontext "renombrar la variable 'usuario' a 'cuenta'" --editor propio --modo-edicion ast
|
|
70
87
|
```
|
|
71
88
|
|
|
72
89
|
## 🧭 Modos y alias
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Módulo de Agentes de SnapContext.
|
|
5
|
+
|
|
6
|
+
Arquitectura de agentes (cada agente se centra en una responsabilidad del
|
|
7
|
+
pipeline y se coordina desde el Orquestador):
|
|
8
|
+
|
|
9
|
+
- ``AgenteContexto``: analiza el repositorio y selecciona los archivos
|
|
10
|
+
relevantes para la tarea. Es el paso "Contexto" (escaneo + selección IA).
|
|
11
|
+
- ``AgenteEditor`` : recibe órdenes EXACTAS de qué cambiar y ejecuta Aider
|
|
12
|
+
sobre los archivos seleccionados. No decide qué cambiar; solo aplica órdenes.
|
|
13
|
+
- ``AgenteTester`` : ejecuta las pruebas y analiza el error devuelto para
|
|
14
|
+
que el Editor corrija justo lo que falló.
|
|
15
|
+
|
|
16
|
+
Cada método importa de forma diferida las funciones ya probadas de
|
|
17
|
+
``snapcontext``, para conservar exactamente el comportamiento actual, y añade
|
|
18
|
+
logs de depuración visibles con ``--depurar`` (marcas ``[Agente…]``).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import subprocess
|
|
22
|
+
from typing import List, Optional, Union
|
|
23
|
+
|
|
24
|
+
def _tarea_estructura(tarea: str) -> bool:
|
|
25
|
+
"""Heurística simple para `auto`: ¿la tarea parece una refactorización estructural?
|
|
26
|
+
|
|
27
|
+
Si es así, merece la pena intentar primero la edición basada en AST.
|
|
28
|
+
Se normalizan acentos para tolerar variaciones ("renombra"/"renombrar", etc.).
|
|
29
|
+
"""
|
|
30
|
+
if not tarea:
|
|
31
|
+
return False
|
|
32
|
+
import unicodedata as _u
|
|
33
|
+
|
|
34
|
+
t = _u.normalize("NFD", tarea.lower())
|
|
35
|
+
t = "".join(c for c in t if not _u.combining(c))
|
|
36
|
+
claves = ("refactor", "renombra", "renombrar", "extra", "extraer",
|
|
37
|
+
"mueve", "mover", "inserta", "insertar", "nueva funcion",
|
|
38
|
+
"crear funcion", "funcion", "extraccion")
|
|
39
|
+
return any(k in t for k in claves)
|
|
40
|
+
|
|
41
|
+
class AgenteContexto:
|
|
42
|
+
"""Agente de Contexto: selecciona los archivos relevantes para la consulta.
|
|
43
|
+
|
|
44
|
+
No modifica código: solo elige qué archivos merece la pena abrir/editar.
|
|
45
|
+
Compone el escaneo local (heurística) y la selección final con el proveedor
|
|
46
|
+
de IA configurado.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def escanear_candidatos(
|
|
50
|
+
self,
|
|
51
|
+
consulta: str,
|
|
52
|
+
directorio: str = ".",
|
|
53
|
+
carpetas: Optional[List[str]] = None,
|
|
54
|
+
extensiones: Optional[List[str]] = None,
|
|
55
|
+
max_candidatos: int = 80,
|
|
56
|
+
) -> List[str]:
|
|
57
|
+
"""Escanea ``directorio`` y devuelve los ``max_candidatos`` archivos más
|
|
58
|
+
relevantes para ``consulta`` usando heurística local (sin llamar a la IA).
|
|
59
|
+
|
|
60
|
+
``extensiones`` (opcional) restringe el escaneo a determinadas
|
|
61
|
+
extensiones (p. ej. ``[\".dart\"]``).
|
|
62
|
+
|
|
63
|
+
Este es el sub-paso 1 del pipeline: reduce el repositorio a candidatos.
|
|
64
|
+
"""
|
|
65
|
+
import snapcontext as sc
|
|
66
|
+
|
|
67
|
+
sc.depurar(
|
|
68
|
+
f"[AgenteContexto] Escaneando '{directorio}' "
|
|
69
|
+
f"(carpetas={carpetas}, extensiones={extensiones}, "
|
|
70
|
+
f"max_candidatos={max_candidatos})..."
|
|
71
|
+
)
|
|
72
|
+
resultado = sc.escanear_repositorio(
|
|
73
|
+
consulta,
|
|
74
|
+
directorio=directorio,
|
|
75
|
+
carpetas=carpetas,
|
|
76
|
+
extensiones=extensiones,
|
|
77
|
+
max_candidatos=max_candidatos,
|
|
78
|
+
)
|
|
79
|
+
sc.depurar(
|
|
80
|
+
f"[AgenteContexto] {len(resultado)} candidato(s) relevante(s) localmente."
|
|
81
|
+
)
|
|
82
|
+
return resultado
|
|
83
|
+
|
|
84
|
+
def seleccionar_archivos(
|
|
85
|
+
self,
|
|
86
|
+
consulta: str,
|
|
87
|
+
directorio: str,
|
|
88
|
+
carpetas: Optional[List[str]],
|
|
89
|
+
max_archivos: int,
|
|
90
|
+
provider: str,
|
|
91
|
+
modelo: str,
|
|
92
|
+
extensiones: Optional[List[str]] = None,
|
|
93
|
+
) -> List[str]:
|
|
94
|
+
"""Pipeline completo del agente de contexto: escanea el repositorio y
|
|
95
|
+
pide al proveedor IA ``provider`` que se quede con las ``max_archivos``
|
|
96
|
+
rutas más relevantes para ``consulta``.
|
|
97
|
+
|
|
98
|
+
Devuelve la lista final (vacía si no hay candidatos).
|
|
99
|
+
"""
|
|
100
|
+
import snapcontext as sc
|
|
101
|
+
|
|
102
|
+
candidatos = self.escanear_candidatos(
|
|
103
|
+
consulta,
|
|
104
|
+
directorio=directorio,
|
|
105
|
+
carpetas=carpetas,
|
|
106
|
+
extensiones=extensiones,
|
|
107
|
+
max_candidatos=max(max_archivos * 3, 1),
|
|
108
|
+
)
|
|
109
|
+
if not candidatos:
|
|
110
|
+
sc.depurar("[AgenteContexto] Sin candidatos; no hay qué seleccionar.")
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
sc.depurar(
|
|
114
|
+
f"[AgenteContexto] Seleccionando {max_archivos} rutas "
|
|
115
|
+
f"(proveedor='{provider}', modelo='{modelo}')..."
|
|
116
|
+
)
|
|
117
|
+
return sc.seleccionar_archivos(
|
|
118
|
+
consulta,
|
|
119
|
+
candidatos,
|
|
120
|
+
proveedor=provider,
|
|
121
|
+
modelo=modelo,
|
|
122
|
+
max_archivos=max_archivos,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# __PARTE2__
|
|
126
|
+
class AgenteEditor:
|
|
127
|
+
"""Agente Editor: recibe órdenes exactas y ejecuta Aider.
|
|
128
|
+
|
|
129
|
+
No decide de forma autónoma qué cambiar: aplica el ``mensaje`` (normalmente
|
|
130
|
+
la tarea original y, si viene del ``AgenteTester``, el error exacto) sobre
|
|
131
|
+
los ``archivos`` indicados por el orquestador.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def ejecutar_aider(
|
|
135
|
+
self,
|
|
136
|
+
archivos: List[str],
|
|
137
|
+
mensaje: str,
|
|
138
|
+
directorio: str,
|
|
139
|
+
opciones: str = "",
|
|
140
|
+
) -> bool:
|
|
141
|
+
"""Ejecuta Aider en ``directorio`` sobre ``archivos`` con ``mensaje``.
|
|
142
|
+
|
|
143
|
+
Devuelve ``True`` si Aider terminó con código 0; ``False`` en otro caso.
|
|
144
|
+
"""
|
|
145
|
+
import snapcontext as sc
|
|
146
|
+
|
|
147
|
+
sc.depurar(
|
|
148
|
+
f"[AgenteEditor] Aider sobre {len(archivos)} archivo(s) en '{directorio}'"
|
|
149
|
+
)
|
|
150
|
+
return sc.ejecutar_aider(archivos, mensaje, directorio, opciones)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class AgenteEditorPropio:
|
|
154
|
+
"""Agente Editor Propio: editor integrado de SnapContext.
|
|
155
|
+
|
|
156
|
+
Aplica cambios directamente sobre el sistema de archivos sin depender de
|
|
157
|
+
herramientas externas obligatorias.
|
|
158
|
+
- Fase 1: Sobrescritura segura con copia de seguridad previa en ``~/.snapcontext/backups/``.
|
|
159
|
+
- Fase 2: Generación y aplicación de parches unificados (unified diffs) con ``git apply`` / ``patch``
|
|
160
|
+
y fallback automático a sobrescritura.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def sobrescribir(
|
|
164
|
+
self,
|
|
165
|
+
archivo: str,
|
|
166
|
+
contenido: str,
|
|
167
|
+
directorio: str = ".",
|
|
168
|
+
) -> bool:
|
|
169
|
+
"""Sobrescribe ``archivo`` con ``contenido`` en ``directorio``.
|
|
170
|
+
|
|
171
|
+
Devuelve ``True`` si la escritura fue exitosa, ``False`` si falló.
|
|
172
|
+
"""
|
|
173
|
+
import snapcontext as sc
|
|
174
|
+
|
|
175
|
+
sc.depurar(
|
|
176
|
+
f"[AgenteEditorPropio] Sobrescribiendo '{archivo}' en '{directorio}'"
|
|
177
|
+
)
|
|
178
|
+
return sc._editor_sobrescribir(archivo, contenido, directorio=directorio)
|
|
179
|
+
|
|
180
|
+
def aplicar_parche(
|
|
181
|
+
self,
|
|
182
|
+
parche: str,
|
|
183
|
+
directorio: str = ".",
|
|
184
|
+
) -> bool:
|
|
185
|
+
"""Aplica un parche unificado en ``directorio``.
|
|
186
|
+
|
|
187
|
+
Devuelve ``True`` si se aplicó con éxito, ``False`` si falló.
|
|
188
|
+
"""
|
|
189
|
+
import snapcontext as sc
|
|
190
|
+
|
|
191
|
+
sc.depurar(f"[AgenteEditorPropio] Aplicando parche en '{directorio}'")
|
|
192
|
+
return sc._aplicar_parche(parche, directorio=directorio)
|
|
193
|
+
|
|
194
|
+
def _cadena_modos(self, archivo: str, mensaje: str,
|
|
195
|
+
modo_edicion: str) -> List[str]:
|
|
196
|
+
"""Devuelve la cadena de estrategias de edición a intentar para un archivo.
|
|
197
|
+
|
|
198
|
+
- 'sobrescribir' → solo sobrescritura.
|
|
199
|
+
- 'parche' → solo parche unificado (sin fallback).
|
|
200
|
+
- 'ast' → AST y, si falla, sobrescritura.
|
|
201
|
+
- 'auto' → AST (si el lenguaje lo permite y la tarea es estructural),
|
|
202
|
+
luego parche y, por último, sobrescritura.
|
|
203
|
+
"""
|
|
204
|
+
import snapcontext as sc
|
|
205
|
+
|
|
206
|
+
if modo_edicion == "sobrescribir":
|
|
207
|
+
return ["sobrescribir"]
|
|
208
|
+
if modo_edicion == "parche":
|
|
209
|
+
return ["parche"]
|
|
210
|
+
if modo_edicion == "ast":
|
|
211
|
+
return ["ast", "sobrescribir"]
|
|
212
|
+
# auto: heurística simple según lenguaje y tarea
|
|
213
|
+
if sc._ast_disponible(archivo) and _tarea_estructura(mensaje):
|
|
214
|
+
return ["ast", "parche", "sobrescribir"]
|
|
215
|
+
return ["parche", "sobrescribir"]
|
|
216
|
+
|
|
217
|
+
def editar_ast(self, archivo: str, tarea: str,
|
|
218
|
+
directorio: str = ".", modelo: Optional[str] = None) -> bool:
|
|
219
|
+
"""Edita ``archivo`` con base en su AST usando el proveedor de IA."""
|
|
220
|
+
import snapcontext as sc
|
|
221
|
+
|
|
222
|
+
pref = sc.cargar_configuracion()
|
|
223
|
+
proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
|
|
224
|
+
sc.depurar(f"[AgenteEditorPropio] Editando por AST '{archivo}' en '{directorio}'")
|
|
225
|
+
return sc._editor_ast(archivo, tarea, directorio=directorio,
|
|
226
|
+
proveedor=proveedor, modelo=modelo)
|
|
227
|
+
|
|
228
|
+
def _aplicar_modo_parche(self, archivo: str, mensaje: str,
|
|
229
|
+
contenido_actual: str, modelo: Optional[str],
|
|
230
|
+
directorio: str) -> bool:
|
|
231
|
+
"""Intenta editar `archivo` pidiendo un parche unificado al proveedor."""
|
|
232
|
+
import snapcontext as sc
|
|
233
|
+
|
|
234
|
+
pref = sc.cargar_configuracion()
|
|
235
|
+
proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
|
|
236
|
+
prompt = (
|
|
237
|
+
f"Genera un parche unificado (unified diff) que modifique el archivo para cumplir con la tarea. "
|
|
238
|
+
f"El parche debe ser aplicable con 'patch -p1' o 'git apply' (encabezados --- a/{archivo} y +++ b/{archivo}).\n\n"
|
|
239
|
+
f"Tarea: {mensaje}\n"
|
|
240
|
+
f"Archivo: {archivo}\n\n"
|
|
241
|
+
f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
|
|
242
|
+
f"Devuelve ÚNICAMENTE el diff unificado, sin explicaciones ni rodeos."
|
|
243
|
+
)
|
|
244
|
+
respuesta = sc._enviar_al_proveedor(
|
|
245
|
+
proveedor, modelo, [{"role": "user", "content": prompt}])
|
|
246
|
+
diff_limpio = respuesta
|
|
247
|
+
if "--- " in diff_limpio and "+++ " in diff_limpio:
|
|
248
|
+
idx_inicio = diff_limpio.find("--- ")
|
|
249
|
+
diff_limpio = diff_limpio[idx_inicio:]
|
|
250
|
+
if "```" in diff_limpio:
|
|
251
|
+
diff_limpio = diff_limpio[:diff_limpio.find("```")]
|
|
252
|
+
if "--- " in diff_limpio and "+++ " in diff_limpio and "@@" in diff_limpio:
|
|
253
|
+
return self.aplicar_parche(diff_limpio, directorio)
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def _aplicar_modo_sobrescribir(self, archivo: str, mensaje: str,
|
|
257
|
+
contenido_actual: str, modelo: Optional[str],
|
|
258
|
+
directorio: str) -> bool:
|
|
259
|
+
"""Sobrescribe `archivo` con el código completo que devuelve el proveedor."""
|
|
260
|
+
import snapcontext as sc
|
|
261
|
+
|
|
262
|
+
pref = sc.cargar_configuracion()
|
|
263
|
+
proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
|
|
264
|
+
prompt = (
|
|
265
|
+
f"Modifica el siguiente archivo para cumplir con la tarea.\n\n"
|
|
266
|
+
f"Tarea: {mensaje}\n"
|
|
267
|
+
f"Archivo: {archivo}\n\n"
|
|
268
|
+
f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
|
|
269
|
+
f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
|
|
270
|
+
)
|
|
271
|
+
nuevo_contenido = sc._enviar_al_proveedor(
|
|
272
|
+
proveedor, modelo, [{"role": "user", "content": prompt}])
|
|
273
|
+
if nuevo_contenido.startswith("```"):
|
|
274
|
+
lineas = nuevo_contenido.splitlines()
|
|
275
|
+
if len(lineas) >= 2 and lineas[-1].startswith("```"):
|
|
276
|
+
nuevo_contenido = "\n".join(lineas[1:-1])
|
|
277
|
+
return self.sobrescribir(archivo, nuevo_contenido, directorio)
|
|
278
|
+
|
|
279
|
+
def ejecutar(
|
|
280
|
+
self,
|
|
281
|
+
archivos: List[str],
|
|
282
|
+
mensaje: str,
|
|
283
|
+
directorio: str = ".",
|
|
284
|
+
modo_edicion: str = "auto",
|
|
285
|
+
modelo: Optional[str] = None,
|
|
286
|
+
) -> bool:
|
|
287
|
+
"""Aplica las modificaciones para `archivos` con el `mensaje` especificado.
|
|
288
|
+
|
|
289
|
+
- modo_edicion == 'parche': intenta aplicar parche unificado (falla si no puede).
|
|
290
|
+
- modo_edicion == 'sobrescribir': aplica directamente sobrescritura de archivo.
|
|
291
|
+
- modo_edicion == 'ast': edita con base en el AST y cae a sobrescritura si falla.
|
|
292
|
+
- modo_edicion == 'auto': decide por heurística (AST para refactorizaciones,
|
|
293
|
+
parche, y fallback a sobrescritura).
|
|
294
|
+
"""
|
|
295
|
+
import snapcontext as sc
|
|
296
|
+
from pathlib import Path
|
|
297
|
+
|
|
298
|
+
raiz = Path(directorio).resolve()
|
|
299
|
+
todo_ok = True
|
|
300
|
+
|
|
301
|
+
for arch in archivos:
|
|
302
|
+
camino = (raiz / arch).resolve()
|
|
303
|
+
contenido_actual = ""
|
|
304
|
+
if camino.is_file():
|
|
305
|
+
try:
|
|
306
|
+
contenido_actual = camino.read_text(encoding="utf-8",
|
|
307
|
+
errors="replace")
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
cadena = self._cadena_modos(arch, mensaje, modo_edicion)
|
|
312
|
+
conseguido = False
|
|
313
|
+
for estrategia in cadena:
|
|
314
|
+
try:
|
|
315
|
+
if estrategia == "ast":
|
|
316
|
+
conseguido = self.editar_ast(arch, mensaje,
|
|
317
|
+
str(raiz), modelo)
|
|
318
|
+
elif estrategia == "parche":
|
|
319
|
+
conseguido = self._aplicar_modo_parche(
|
|
320
|
+
arch, mensaje, contenido_actual, modelo, str(raiz))
|
|
321
|
+
elif estrategia == "sobrescribir":
|
|
322
|
+
conseguido = self._aplicar_modo_sobrescribir(
|
|
323
|
+
arch, mensaje, contenido_actual, modelo, str(raiz))
|
|
324
|
+
else:
|
|
325
|
+
conseguido = False
|
|
326
|
+
except Exception as exc:
|
|
327
|
+
sc.depurar(
|
|
328
|
+
f"[AgenteEditorPropio] {estrategia} falló para '{arch}': {exc}")
|
|
329
|
+
conseguido = False
|
|
330
|
+
if conseguido:
|
|
331
|
+
break
|
|
332
|
+
if not conseguido:
|
|
333
|
+
sc.error(f"[AgenteEditorPropio] No se pudo editar '{arch}'.")
|
|
334
|
+
todo_ok = False
|
|
335
|
+
|
|
336
|
+
return todo_ok
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class AgenteEditorAST:
|
|
340
|
+
"""Agente Editor AST (Fase 3): ediciones precisas basadas en árbol sintáctico.
|
|
341
|
+
|
|
342
|
+
Trabaja directamente con el AST del archivo (Python con ``ast``; otros
|
|
343
|
+
lenguajes con ``tree-sitter`` si está instalado) pidiendo al proveedor
|
|
344
|
+
operaciones de modificación del árbol o el código completo resultante.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
def editar(
|
|
348
|
+
self,
|
|
349
|
+
archivos: List[str],
|
|
350
|
+
mensaje: str,
|
|
351
|
+
directorio: str = ".",
|
|
352
|
+
modelo: Optional[str] = None,
|
|
353
|
+
) -> bool:
|
|
354
|
+
"""Aplica a cada archivo una edición guiada por AST. Devuelve bool."""
|
|
355
|
+
todo = True
|
|
356
|
+
for archivo in archivos:
|
|
357
|
+
if not self.editar_archivo(archivo, mensaje,
|
|
358
|
+
directorio=directorio, modelo=modelo):
|
|
359
|
+
todo = False
|
|
360
|
+
return todo
|
|
361
|
+
|
|
362
|
+
def editar_archivo(
|
|
363
|
+
self,
|
|
364
|
+
archivo: str,
|
|
365
|
+
tarea: str,
|
|
366
|
+
directorio: str = ".",
|
|
367
|
+
modelo: Optional[str] = None,
|
|
368
|
+
) -> bool:
|
|
369
|
+
"""Edita un único archivo con AST. Devuelve ``True`` si tuvo éxito."""
|
|
370
|
+
import snapcontext as sc
|
|
371
|
+
|
|
372
|
+
pref = sc.cargar_configuracion()
|
|
373
|
+
proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
|
|
374
|
+
sc.depurar(f"[AgenteEditorAST] AST sobre '{archivo}' en '{directorio}'")
|
|
375
|
+
return sc._editor_ast(archivo, tarea, directorio=directorio,
|
|
376
|
+
proveedor=proveedor, modelo=modelo)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class AgenteTester:
|
|
380
|
+
"""Agente Tester: ejecuta las pruebas y analiza los errores devueltos.
|
|
381
|
+
|
|
382
|
+
Su salida alimenta al ``AgenteEditor`` para que corrija justo lo que falló,
|
|
383
|
+
sin que Aider pierda de vista la tarea original.
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
def ejecutar_pruebas(
|
|
387
|
+
self, comando: List[str], directorio: str
|
|
388
|
+
) -> "subprocess.CompletedProcess":
|
|
389
|
+
"""Ejecuta ``comando`` (lista de argv) en ``directorio`` capturando la salida.
|
|
390
|
+
|
|
391
|
+
Devuelve el ``subprocess.CompletedProcess`` para poder analizarlo después.
|
|
392
|
+
"""
|
|
393
|
+
import snapcontext as sc
|
|
394
|
+
|
|
395
|
+
sc.depurar(f"[AgenteTester] Ejecutando pruebas: {' '.join(comando)}")
|
|
396
|
+
return subprocess.run(
|
|
397
|
+
comando, cwd=directorio, capture_output=True, text=True
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def analizar_error(
|
|
401
|
+
self, salida: Union["subprocess.CompletedProcess", str]
|
|
402
|
+
) -> str:
|
|
403
|
+
"""Normaliza la salida de unas pruebas que fallaron para entregársela a Aider.
|
|
404
|
+
|
|
405
|
+
Acepta el ``CompletedProcess`` devuelto por :meth:`ejecutar_pruebas` o un
|
|
406
|
+
``str`` con la salida cruda. Limpia códigos ANSI y recorta demasiado
|
|
407
|
+
texto para no llenar el contexto de Aider.
|
|
408
|
+
"""
|
|
409
|
+
import snapcontext as sc
|
|
410
|
+
|
|
411
|
+
if hasattr(salida, "stdout"):
|
|
412
|
+
# Objeto CompletedProcess → reutilizamos el normalizador de snapcontext.
|
|
413
|
+
return sc._extraer_error(salida)
|
|
414
|
+
|
|
415
|
+
limpieza: str = salida or ""
|
|
416
|
+
import re as _re
|
|
417
|
+
|
|
418
|
+
limpieza = _re.sub(r"\x1b\[[0-9;]*m", "", limpieza)
|
|
419
|
+
limpieza = limpieza.strip() or "(el comando de prueba no devolvió salida)"
|
|
420
|
+
limite = getattr(sc, "MAX_ERROR_SALIDA", 4000)
|
|
421
|
+
if len(limpieza) > limite:
|
|
422
|
+
limpieza = "\n--- (salida recortada) ---\n" + limpieza[-limite:]
|
|
423
|
+
return limpieza
|
|
@@ -20,7 +20,8 @@ import shlex
|
|
|
20
20
|
import sys
|
|
21
21
|
from typing import List, Optional, Tuple
|
|
22
22
|
|
|
23
|
-
from agentes import AgenteContexto, AgenteEditor,
|
|
23
|
+
from agentes import (AgenteContexto, AgenteEditor, AgenteEditorAST,
|
|
24
|
+
AgenteEditorPropio, AgenteTester)
|
|
24
25
|
|
|
25
26
|
# Centinela diferenciado del None de "aborto": _planificar lo devuelve cuando
|
|
26
27
|
# el pipeline termina de forma exitosa y temprana (p. ej. --vista-previa).
|
|
@@ -40,6 +41,7 @@ class Orquestador:
|
|
|
40
41
|
self.agente_contexto = AgenteContexto()
|
|
41
42
|
self.agente_editor = AgenteEditor()
|
|
42
43
|
self.agente_editor_propio = AgenteEditorPropio()
|
|
44
|
+
self.agente_editor_ast = AgenteEditorAST()
|
|
43
45
|
self.agente_tester = AgenteTester()
|
|
44
46
|
self.evento_callback = evento_callback
|
|
45
47
|
|
|
@@ -180,38 +182,14 @@ class Orquestador:
|
|
|
180
182
|
if editor_tipo == "propio":
|
|
181
183
|
sc.depurar("[Orquestador] Modo edición directa (AgenteEditorPropio).")
|
|
182
184
|
self._emitir_tipo("editor", accion="iniciar", archivos=seleccion)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
contenido_actual = camino.read_text(encoding="utf-8", errors="replace")
|
|
192
|
-
except Exception:
|
|
193
|
-
pass
|
|
194
|
-
prompt_editor = (
|
|
195
|
-
f"Modifica el siguiente archivo para cumplir con la tarea.\n\n"
|
|
196
|
-
f"Tarea: {consulta}\n"
|
|
197
|
-
f"Archivo: {arch}\n\n"
|
|
198
|
-
f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
|
|
199
|
-
f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
|
|
200
|
-
)
|
|
201
|
-
try:
|
|
202
|
-
nuevo_contenido = sc._enviar_al_proveedor(
|
|
203
|
-
proveedor, getattr(args, "modelo", None),
|
|
204
|
-
[{"role": "user", "content": prompt_editor}]
|
|
205
|
-
)
|
|
206
|
-
if nuevo_contenido.startswith("```"):
|
|
207
|
-
lineas = nuevo_contenido.splitlines()
|
|
208
|
-
if len(lineas) >= 2 and lineas[-1].startswith("```"):
|
|
209
|
-
nuevo_contenido = "\n".join(lineas[1:-1])
|
|
210
|
-
if not self.agente_editor_propio.sobrescribir(arch, nuevo_contenido, str(raiz)):
|
|
211
|
-
ok = False
|
|
212
|
-
except Exception as exc:
|
|
213
|
-
sc.error(f"Error generando cambios para {arch}: {exc}")
|
|
214
|
-
ok = False
|
|
185
|
+
modo_ed = getattr(args, "modo_edicion", "auto") or "auto"
|
|
186
|
+
ok = self.agente_editor_propio.ejecutar(
|
|
187
|
+
seleccion,
|
|
188
|
+
consulta,
|
|
189
|
+
directorio=str(raiz),
|
|
190
|
+
modo_edicion=modo_ed,
|
|
191
|
+
modelo=getattr(args, "modelo", None),
|
|
192
|
+
)
|
|
215
193
|
self._emitir_tipo("editor", accion="fin", ok=ok)
|
|
216
194
|
else:
|
|
217
195
|
sc.depurar("[Orquestador] Modo edición directa (AgenteEditor).")
|