qagent-pytest 0.1.0__py3-none-any.whl

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.
Files changed (130) hide show
  1. qagent/__init__.py +14 -0
  2. qagent/__main__.py +6 -0
  3. qagent/agents/__init__.py +13 -0
  4. qagent/agents/generator.py +190 -0
  5. qagent/agents/planner.py +647 -0
  6. qagent/agents/reviewer.py +692 -0
  7. qagent/almacenamiento/__init__.py +25 -0
  8. qagent/almacenamiento/disco.py +76 -0
  9. qagent/almacenamiento/historial.py +80 -0
  10. qagent/almacenamiento/proyecto.py +699 -0
  11. qagent/almacenamiento/recientes.py +247 -0
  12. qagent/analysis/__init__.py +20 -0
  13. qagent/analysis/__main__.py +7 -0
  14. qagent/analysis/analyzer.py +313 -0
  15. qagent/analysis/laundering.py +596 -0
  16. qagent/analysis/rutas.py +698 -0
  17. qagent/app/__init__.py +0 -0
  18. qagent/app/bitacora.py +145 -0
  19. qagent/app/desktop.py +824 -0
  20. qagent/app/monitor_eventos.py +123 -0
  21. qagent/config/__init__.py +45 -0
  22. qagent/config/auditoria.py +127 -0
  23. qagent/config/carga.py +620 -0
  24. qagent/config/config.local.ejemplo.toml +35 -0
  25. qagent/config/config.toml +51 -0
  26. qagent/config/entorno.py +174 -0
  27. qagent/config/roles.py +561 -0
  28. qagent/contracts/__init__.py +91 -0
  29. qagent/contracts/ejemplos/generated_test.json +7 -0
  30. qagent/contracts/ejemplos/planner_contract.endpoint.json +72 -0
  31. qagent/contracts/ejemplos/planner_contract.v2.json +26 -0
  32. qagent/contracts/ejemplos/review_result.json +12 -0
  33. qagent/contracts/ejemplos/run_log.log.json +110 -0
  34. qagent/contracts/ejemplos/run_log.results.json +105 -0
  35. qagent/contracts/generated_test.schema.json +31 -0
  36. qagent/contracts/planner_contract.v2.schema.json +166 -0
  37. qagent/contracts/review_result.schema.json +89 -0
  38. qagent/contracts/run_log.schema.json +250 -0
  39. qagent/frontend/README.md +134 -0
  40. qagent/frontend/assets/fuentes/LEEME.md +13 -0
  41. qagent/frontend/assets/fuentes/LICENSE-Inter-OFL.txt +93 -0
  42. qagent/frontend/assets/fuentes/LICENSE-JetBrainsMono-OFL.txt +93 -0
  43. qagent/frontend/assets/fuentes/inter-latin-400-normal.woff2 +0 -0
  44. qagent/frontend/assets/fuentes/inter-latin-500-normal.woff2 +0 -0
  45. qagent/frontend/assets/fuentes/inter-latin-600-normal.woff2 +0 -0
  46. qagent/frontend/assets/fuentes/inter-latin-700-normal.woff2 +0 -0
  47. qagent/frontend/assets/fuentes/jetbrains-mono-latin-400-normal.woff2 +0 -0
  48. qagent/frontend/assets/fuentes/jetbrains-mono-latin-600-normal.woff2 +0 -0
  49. qagent/frontend/assets/logo/qagent-icono.svg +46 -0
  50. qagent/frontend/assets/logo/qagent-logo.svg +43 -0
  51. qagent/frontend/componentes/barra-corrida/barra-corrida.css +23 -0
  52. qagent/frontend/componentes/barra-corrida/barra-corrida.js +26 -0
  53. qagent/frontend/componentes/config-pruebas/config-pruebas.css +40 -0
  54. qagent/frontend/componentes/config-pruebas/config-pruebas.js +137 -0
  55. qagent/frontend/componentes/configuracion/configuracion.css +20 -0
  56. qagent/frontend/componentes/configuracion/configuracion.js +127 -0
  57. qagent/frontend/componentes/configuracion/modal-api.js +126 -0
  58. qagent/frontend/componentes/ejecucion/ejecucion.css +93 -0
  59. qagent/frontend/componentes/ejecucion/ejecucion.js +232 -0
  60. qagent/frontend/componentes/historial/historial.js +125 -0
  61. qagent/frontend/componentes/informe/informe.css +73 -0
  62. qagent/frontend/componentes/informe/informe.js +201 -0
  63. qagent/frontend/componentes/informe/modal-exportar.css +6 -0
  64. qagent/frontend/componentes/informe/modal-exportar.js +1072 -0
  65. qagent/frontend/componentes/inicio/inicio.css +21 -0
  66. qagent/frontend/componentes/inicio/inicio.js +90 -0
  67. qagent/frontend/componentes/inicio/modal-abrir.js +101 -0
  68. qagent/frontend/componentes/inicio/modal-clonar.js +89 -0
  69. qagent/frontend/componentes/monitor/monitor.css +33 -0
  70. qagent/frontend/componentes/monitor/monitor.js +223 -0
  71. qagent/frontend/componentes/sidebar/sidebar.css +25 -0
  72. qagent/frontend/componentes/sidebar/sidebar.js +130 -0
  73. qagent/frontend/componentes/vista-previa/vista-previa.css +26 -0
  74. qagent/frontend/componentes/vista-previa/vista-previa.js +373 -0
  75. qagent/frontend/css/componentes.css +57 -0
  76. qagent/frontend/css/fuentes.css +8 -0
  77. qagent/frontend/css/layout.css +33 -0
  78. qagent/frontend/css/tokens.css +93 -0
  79. qagent/frontend/index.html +90 -0
  80. qagent/frontend/js/cargador.js +12 -0
  81. qagent/frontend/js/datos-corrida.js +257 -0
  82. qagent/frontend/js/estado.js +81 -0
  83. qagent/frontend/js/iconos.js +21 -0
  84. qagent/frontend/js/main.js +12 -0
  85. qagent/frontend/js/navegacion.js +22 -0
  86. qagent/frontend/js/puente-python.js +16 -0
  87. qagent/frontend/js/utils.js +7 -0
  88. qagent/llm/__init__.py +36 -0
  89. qagent/llm/cliente.py +407 -0
  90. qagent/llm/fabrica.py +91 -0
  91. qagent/llm/fake.py +311 -0
  92. qagent/llm/ia_simulada.json +59 -0
  93. qagent/llm/modelos.py +35 -0
  94. qagent/llm/precios.py +92 -0
  95. qagent/llm/proveedores.py +380 -0
  96. qagent/llm/proveedores.toml +140 -0
  97. qagent/llm/tracker.py +164 -0
  98. qagent/llm/validar.py +126 -0
  99. qagent/orchestrator/__init__.py +42 -0
  100. qagent/orchestrator/corrida.py +460 -0
  101. qagent/orchestrator/estados.py +49 -0
  102. qagent/orchestrator/eventos.py +69 -0
  103. qagent/orchestrator/orquestador.py +480 -0
  104. qagent/orchestrator/puertos.py +39 -0
  105. qagent/orchestrator/regresion.py +311 -0
  106. qagent/proyectos/__init__.py +7 -0
  107. qagent/proyectos/clonador.py +283 -0
  108. qagent/proyectos/proyecto_local.py +111 -0
  109. qagent/proyectos/vista_previa.py +153 -0
  110. qagent/sandbox/Dockerfile +24 -0
  111. qagent/sandbox/__init__.py +46 -0
  112. qagent/sandbox/cliente.py +26 -0
  113. qagent/sandbox/ejecucion.py +306 -0
  114. qagent/sandbox/estado.py +71 -0
  115. qagent/sandbox/imagen.py +151 -0
  116. qagent/sandbox/modelos.py +93 -0
  117. qagent/sandbox/normalizacion.py +65 -0
  118. qagent/storage/__init__.py +23 -0
  119. qagent/storage/corrida_demo.py +672 -0
  120. qagent/storage/exportar_informe.py +1959 -0
  121. qagent/storage/registro.py +380 -0
  122. qagent/storage/vista_ejecucion.py +297 -0
  123. qagent/storage/vista_reporte.py +239 -0
  124. qagent/usuario.py +20 -0
  125. qagent_pytest-0.1.0.dist-info/METADATA +107 -0
  126. qagent_pytest-0.1.0.dist-info/RECORD +130 -0
  127. qagent_pytest-0.1.0.dist-info/WHEEL +5 -0
  128. qagent_pytest-0.1.0.dist-info/entry_points.txt +2 -0
  129. qagent_pytest-0.1.0.dist-info/licenses/LICENSE +22 -0
  130. qagent_pytest-0.1.0.dist-info/top_level.txt +1 -0
qagent/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """QAgent: sistema multiagente que genera y valida pruebas pytest para código Python."""
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version as _version
5
+
6
+ #: Nombre del paquete en PyPI (`pip install qagent-pytest`). El producto y el `import`
7
+ #: siguen siendo QAgent / `qagent`; «qagent» en PyPI es de otro proyecto (TA-06).
8
+ DISTRIBUCION = "qagent-pytest"
9
+
10
+ try:
11
+ #: Versión instalada; la única fuente es `version` en pyproject.toml.
12
+ __version__ = _version(DISTRIBUCION)
13
+ except PackageNotFoundError: # ejecutado desde src/ sin instalar el paquete
14
+ __version__ = "0.0.0+desarrollo"
qagent/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Permite abrir la aplicación de escritorio con ``python -m qagent``."""
2
+
3
+ from qagent.app.desktop import ejecutar
4
+
5
+ if __name__ == "__main__":
6
+ ejecutar()
@@ -0,0 +1,13 @@
1
+ """Agentes reales coordinados por el orquestador de QAgent."""
2
+
3
+ from qagent.agents.generator import ErrorRespuestaGenerator, GeneratorAgent
4
+ from qagent.agents.planner import ErrorRespuestaPlanner, PlannerAgent
5
+ from qagent.agents.reviewer import ReviewerAgent
6
+
7
+ __all__ = [
8
+ "ErrorRespuestaGenerator",
9
+ "ErrorRespuestaPlanner",
10
+ "GeneratorAgent",
11
+ "PlannerAgent",
12
+ "ReviewerAgent",
13
+ ]
@@ -0,0 +1,190 @@
1
+ """Agente Generator: convierte un contrato del Planner en código pytest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import json
7
+ from pathlib import PurePosixPath
8
+ from typing import Any
9
+
10
+ from qagent import contracts
11
+ from qagent.llm import ClienteLLM, TokenTracker
12
+ from qagent.llm.fake import MARCA_CONTRATO
13
+
14
+ #: Caracteres de feedback (salida de pytest) que se reenvían al modelo.
15
+ LARGO_MAXIMO_FEEDBACK = 2000
16
+
17
+
18
+ class ErrorRespuestaGenerator(ValueError):
19
+ """El Generator devolvió una respuesta que no puede utilizarse."""
20
+
21
+ def __init__(self, mensaje: str, *, codigo: str = "") -> None:
22
+ super().__init__(mensaje)
23
+ self.codigo = codigo
24
+
25
+
26
+ class GeneratorAgent:
27
+ """Genera el test de un único objetivo a partir de su contrato."""
28
+
29
+ def __init__(
30
+ self,
31
+ cliente: ClienteLLM,
32
+ tracker: TokenTracker,
33
+ *,
34
+ es_simulado: bool = False,
35
+ ) -> None:
36
+ """Inicializa el Generator con su cliente y registro de tokens."""
37
+ self.cliente = cliente
38
+ self.tracker = tracker
39
+ self.es_simulado = es_simulado
40
+
41
+ def generar(
42
+ self,
43
+ contrato: dict[str, Any],
44
+ intento: int,
45
+ feedback: str | None,
46
+ ) -> dict[str, Any]:
47
+ """Genera o corrige un test pytest."""
48
+ prompt = self._crear_prompt(contrato, intento, feedback)
49
+ respuesta = self.cliente.generar(prompt, rol="generator")
50
+
51
+ self.tracker.registrar(
52
+ agente="generator",
53
+ modelo=respuesta.modelo,
54
+ tokens_entrada=respuesta.tokens_entrada,
55
+ tokens_salida=respuesta.tokens_salida,
56
+ costo_usd=respuesta.costo_usd,
57
+ es_simulado=self.es_simulado,
58
+ )
59
+
60
+ codigo = _quitar_cerca_python(respuesta.contenido)
61
+ if not codigo.strip():
62
+ raise ErrorRespuestaGenerator("El Generator devolvió código vacío.")
63
+ try:
64
+ ast.parse(codigo)
65
+ except (SyntaxError, ValueError) as error:
66
+ raise ErrorRespuestaGenerator(
67
+ f"El Generator devolvió código Python inválido: {type(error).__name__}: {error}",
68
+ codigo=codigo,
69
+ ) from error
70
+
71
+ generado = {
72
+ "version": "1",
73
+ "objetivo": contrato["objetivo"],
74
+ "intento": intento,
75
+ "codigo": codigo,
76
+ "casos_cubiertos": [caso["id"] for caso in contrato["casos"]],
77
+ }
78
+ contracts.validar(contracts.GENERATED_TEST, generado)
79
+ return generado
80
+
81
+ @staticmethod
82
+ def _crear_prompt(
83
+ contrato: dict[str, Any],
84
+ intento: int,
85
+ feedback: str | None,
86
+ ) -> str:
87
+ # Una sola línea: más corto (menos tokens) y la IA simulada lo puede leer.
88
+ contrato_json = json.dumps(contrato, ensure_ascii=False)
89
+ correccion = (
90
+ _recortar(feedback)
91
+ if feedback
92
+ else "No existe feedback porque este es el primer intento."
93
+ )
94
+ if contrato.get("tipo") == "endpoint":
95
+ reglas_objetivo = _reglas_endpoint(contrato)
96
+ else:
97
+ importacion = (
98
+ f"from {modulo_importable(contrato['modulo'])} import "
99
+ f"{contrato['objetivo']}"
100
+ )
101
+ reglas_objetivo = f"""
102
+ - Importa el objetivo exactamente así: {importacion}
103
+ - Llama directamente a la función con la entrada de cada caso.
104
+ - Para un caso con "excepcion" usa: with pytest.raises(<Excepcion>).
105
+ """.strip()
106
+ return f"""
107
+ Escribe un archivo pytest completo para el contrato indicado.
108
+
109
+ REGLAS OBLIGATORIAS:
110
+ {reglas_objetivo}
111
+ - Escribe una función test_<id> por cada caso del contrato.
112
+ - Usa exactamente los valores esperados del contrato (pytest.approx para float).
113
+ - No ejecutes el código para descubrir resultados.
114
+ - No elimines, cambies ni debilites las aserciones; sin try/except, skip ni xfail.
115
+ - Simula con mocks las funciones incluidas en llama_a.
116
+ - Devuelve únicamente código Python, sin explicaciones.
117
+ - Este es el intento {intento}.
118
+
119
+ FEEDBACK DEL INTENTO ANTERIOR:
120
+ {correccion}
121
+
122
+ {MARCA_CONTRATO} {contrato_json}
123
+ """.strip()
124
+
125
+
126
+ def _reglas_endpoint(contrato: dict[str, Any]) -> str:
127
+ """Reglas de generación para un endpoint FastAPI descrito por el contrato."""
128
+ modulo = modulo_importable(contrato["modulo"])
129
+ aplicacion = contrato["aplicacion"]
130
+ dependencias = contrato.get("dependencias", [])
131
+ simbolos = [aplicacion]
132
+ for dependencia in dependencias:
133
+ simbolos.extend([dependencia["funcion"], dependencia["reemplazo"]])
134
+ importacion = f"from {modulo} import {', '.join(dict.fromkeys(simbolos))}"
135
+ detalle_overrides = (
136
+ json.dumps(dependencias, ensure_ascii=False)
137
+ if dependencias
138
+ else "[] (este endpoint no declara Depends)"
139
+ )
140
+ return f"""
141
+ - Usa fastapi.testclient.TestClient para realizar una petición HTTP real en memoria.
142
+ - Importa la aplicación y los símbolos de override exactamente así: {importacion}
143
+ - Construye TestClient({aplicacion}, raise_server_exceptions=False).
144
+ - Usa client.request con método, ruta, query, json y headers del contrato.
145
+ - Comprueba siempre response.status_code.
146
+ - Si valor_esperado trae "json", compara response.json() completo.
147
+ - Si trae "json_contiene", comprueba cada clave y valor indicados.
148
+ - Dependencias a reemplazar: {detalle_overrides}
149
+ - Para cada dependencia asigna {aplicacion}.dependency_overrides[funcion] = reemplazo.
150
+ - Si una entrada trae "preparacion", instancia el reemplazo una vez en la fixture,
151
+ ejecuta allí cada método/modelo/argumentos declarado y usa un lambda que devuelva
152
+ esa misma instancia durante el test.
153
+ - Usa una fixture con try/finally: guarda una copia de los overrides anteriores y,
154
+ al terminar cada test, limpia el diccionario y restaura exactamente esa copia.
155
+ - Nunca reemplaces el manejador de la ruta ni modelos Pydantic.
156
+ """.strip()
157
+
158
+
159
+ def modulo_importable(modulo: str) -> str:
160
+ """Ruta del módulo relativa al proyecto -> nombre importable.
161
+
162
+ En el sandbox `PYTHONPATH` es la raíz del proyecto, así que
163
+ `bench/banco_mvp.py` se importa como `bench.banco_mvp`.
164
+ """
165
+ return (
166
+ PurePosixPath(modulo.replace("\\", "/"))
167
+ .with_suffix("")
168
+ .as_posix()
169
+ .replace("/", ".")
170
+ )
171
+
172
+
173
+ def _recortar(texto: str, maximo: int = LARGO_MAXIMO_FEEDBACK) -> str:
174
+ """Deja solo el final del feedback (donde pytest pone el error) para ahorrar tokens."""
175
+ if len(texto) <= maximo:
176
+ return texto
177
+ return "[…recortado…]\n" + texto[-maximo:]
178
+
179
+
180
+ def _quitar_cerca_python(contenido: str) -> str:
181
+ """Elimina una cerca Markdown ```python si el modelo la agregó."""
182
+ texto = contenido.strip()
183
+ if texto.startswith("```"):
184
+ lineas = texto.splitlines()
185
+ if lineas and lineas[0].strip().startswith("```"):
186
+ lineas.pop(0)
187
+ if lineas and lineas[-1].strip() == "```":
188
+ lineas.pop()
189
+ texto = "\n".join(lineas).strip()
190
+ return texto + "\n" if texto else ""