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.
147 lines
6.0 KiB
Python
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()
|