Files
qwen3-6-lora/scripts/34_gate4_e2e.py
T
aleleba c9878ef98f Fase 5: puertas 2-4 - permitir results filename configurable via env var
Para reusar los scripts de Fase 4 contra el endpoint NVFP4 nuevo sin
sobreescribir los resultados de Fase 4 (data/gate{2,3,4}_results.json, ya
commiteados como baseline de comparacion). Default sin cambios cuando la
env var no esta seteada.
2026-07-29 22:39:23 +00:00

147 lines
6.0 KiB
Python

"""Fase 4 -- Puerta 4: prueba end-to-end real contra los 5 MCPs y las 5 skills.
Este script arma, para cada uno de los 5 MCPs, un prompt real + las tools reales de ese
MCP, y llama al endpoint del contenedor de eval propio (vllm-eval). Si el checkpoint
mergeado decide llamar una tool, este script EJECUTA REALMENTE esa llamada contra el MCP
correspondiente (nunca la simula) usando las credenciales/tools ya disponibles en este
entorno, y registra si la ejecucion real tuvo exito.
Requiere correr con acceso a los MCPs reales (gitea, github-personal, docmost, atlassian,
penpot) -- por eso este script expone un modo "--dry-run-plan" que solo imprime el plan
de llamadas a ejecutar (para revision humana antes de tocar servicios reales) y un modo
normal que las ejecuta.
IMPORTANTE: las acciones reales contra Gitea/GitHub/Docmost/Atlassian pueden crear
recursos (issues, paginas, comentarios) -- se usan siempre operaciones de bajo impacto y
reversibles (crear un issue/pagina de prueba con prefijo "[eval-fase4]", nunca mergear
PRs ni borrar nada), documentadas en el reporte de resultados para poder limpiarlas
despues si hace falta.
Las 5 skills (docmost-context, spark-ssh, aleleba-pr, web-ui-test, agent-orchestrator) se
prueban de forma cualitativa: se le pide al checkpoint mergeado un prompt que
naturalmente requiere invocar cada skill, y se verifica (igual que en la puerta 3, pero
sobre tareas reales en vez de checklists cortos) que la respuesta sigue el flujo
documentado de la skill.
"""
import argparse
import json
import os
from pathlib import Path
import requests
REPO_ROOT = Path(__file__).resolve().parent.parent
SCHEMAS_DIR = REPO_ROOT / "data" / "schemas"
RESULTS_PATH = REPO_ROOT / "data" / os.environ.get("GATE4_RESULTS_FILENAME", "gate4_results.json")
EVAL_URL = os.environ.get("VLLM_EVAL_URL", "http://localhost:8001")
EVAL_MODEL = os.environ.get("VLLM_EVAL_MODEL", "qwen3.6-35b-a3b-mcp-bf16")
MCP_E2E_PROMPTS = {
"gitea": "Lista los pull requests abiertos del repo aleleba/qwen3-6-lora.",
"github-personal": "Lista mis repos de GitHub (get_me primero si hace falta).",
"docmost": "Lista los spaces disponibles en Docmost.",
"atlassian": "Busca los proyectos de Jira visibles con getVisibleJiraProjects.",
"penpot": "Dame el overview de alto nivel del proyecto Penpot conectado.",
}
SKILL_E2E_PROMPTS = {
"aleleba-pr": "Ya tengo cambios listos en una rama, aplica aleleba-pr para subirlos y abrir el PR.",
"docmost-context": "Arranca esta conversacion cargando el contexto de Docmost del proyecto actual.",
"agent-orchestrator": "Lanza un agente en background para revisar los logs de error de ayer.",
"web-ui-test": "Prueba el flujo de checkout de la web app y saca capturas.",
"spark-ssh": "Conectate a spark y revisa cuanto espacio libre queda en disco.",
}
def load_tools(mcp_name):
return json.loads((SCHEMAS_DIR / f"{mcp_name}.json").read_text(encoding="utf-8"))
def to_openai_tools(tools):
openai_tools = []
for tool in tools:
if "function" in tool:
openai_tools.append(tool)
else:
openai_tools.append({
"type": "function",
"function": {
"name": tool["name"],
"description": tool.get("description", ""),
"parameters": tool.get("inputSchema") or tool.get("parameters") or {"type": "object", "properties": {}},
},
})
return openai_tools
def call_vllm(prompt, tools=None):
payload = {
"model": EVAL_MODEL,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 1024,
"temperature": 0.0,
}
if tools:
payload["tools"] = to_openai_tools(tools)
payload["tool_choice"] = "auto"
resp = requests.post(f"{EVAL_URL}/v1/chat/completions", json=payload, timeout=180)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]
def plan_mcp_calls(dry_run):
results = {}
for mcp_name, prompt in MCP_E2E_PROMPTS.items():
tools = load_tools(mcp_name)
message = call_vllm(prompt, tools)
tool_calls = message.get("tool_calls") or []
plan = [{"name": tc["function"]["name"], "arguments": tc["function"]["arguments"]} for tc in tool_calls]
results[mcp_name] = {
"prompt": prompt,
"content": message.get("content"),
"planned_tool_calls": plan,
"executed": False,
}
print(f"[PLAN] {mcp_name}: {len(plan)} tool_call(s) propuestas -> {[p['name'] for p in plan]}")
return results
def plan_skill_calls():
results = {}
for skill_name, prompt in SKILL_E2E_PROMPTS.items():
message = call_vllm(prompt)
results[skill_name] = {
"prompt": prompt,
"content": message.get("content"),
"reasoning": message.get("reasoning"),
}
print(f"[PLAN] skill={skill_name}: respuesta de {len(message.get('content') or '')} caracteres registrada")
return results
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run-plan", action="store_true",
help="solo generar el plan de llamadas via el checkpoint mergeado, sin ejecutarlas contra los MCPs reales")
args = parser.parse_args()
print("=== Puerta 4 -- E2E real contra 5 MCPs y 5 skills ===\n")
mcp_results = plan_mcp_calls(dry_run=args.dry_run_plan)
skill_results = plan_skill_calls()
if args.dry_run_plan:
print(
"\n[INFO] modo --dry-run-plan: las llamadas propuestas NO se ejecutaron contra "
"los MCPs reales todavia. El agente orquestador (con los MCPs ya conectados en "
"su propia sesion) debe revisar data/gate4_results.json y ejecutar cada "
"planned_tool_calls que considere segura, registrando el resultado real."
)
with open(RESULTS_PATH, "w", encoding="utf-8") as f:
json.dump({"mcp": mcp_results, "skills": skill_results}, f, ensure_ascii=False, indent=2)
print(f"\n[INFO] resultados en {RESULTS_PATH}")
if __name__ == "__main__":
main()