"""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()