Fase 4: puerta 1 (eval-loss offline por bucket) y contenedor/scripts de puertas 2-4
- scripts/30_eval_suite.py --gate 1: eval-loss sobre el checkpoint mergeado, agrupado por meta.bucket (aislando replay), comparado contra eval_loss=0.275 de Fase 3. - docker-compose.eval.yml: servicio vllm-eval propio (puerto 8001), sirviendo el checkpoint mergeado en BF16, con tool-call-parser=qwen3_coder y reasoning-parser=qwen3. No se pudo leer el compose real de produccion (/data/compose/43/docker-compose.yml no existe en spark, probablemente vive en el host del servidor Portainer) -- flags basados en la arquitectura conocida del modelo. - scripts/31_build_holdout_prompts.py: genera data/holdout_prompts.jsonl (200 prompts, 40 por MCP, sin overlap verificado contra train.jsonl/eval.jsonl). - scripts/32_gate2_toolcalls.py: valida tool-calls devueltas por vllm-eval (parser real de vLLM, nunca una regex propia) contra los 200 prompts held-out. - scripts/33_gate3_adherencia.py: checklists de adherencia por skill + no-activacion, con baseline opcional contra vllm-qwen36 si esta corriendo. - scripts/34_gate4_e2e.py: arma el plan de llamadas E2E contra los 5 MCPs y 5 skills via el checkpoint mergeado, para que el agente orquestador las ejecute con sus MCPs reales.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""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" / "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()
|
||||
Reference in New Issue
Block a user