nmpAgents/demo/scripts/demo.py

184 lines
7.1 KiB
Python

"""CLI de la demo. validate/package son locales; invoke llama a AWS explícitamente."""
import argparse
import ast
import json
import os
from pathlib import Path
import re
import sys
import uuid
import zipfile
ROOT = Path(__file__).resolve().parents[1]
SCENARIOS = {
"onboarding": "scenarios/01-onboarding/scenario.json",
"infraestructura": "scenarios/02-infraestructura/scenario.json",
}
LAMBDAS = {
"agentcore-proxy": "src/lambdas/agentcore_proxy/agentcore-proxy.py",
"aws-infrastructure": "src/lambdas/aws_infrastructure/aws_infrastructure.py",
}
IGNORED = {"node_modules", ".venv", "__pycache__", ".cache", ".cli", "cdk.out", "artifacts", ".git"}
def read_json(path):
return json.loads(path.read_text(encoding="utf-8-sig"))
def require_file(path):
if not path.is_file():
raise ValueError(f"Falta archivo: {path}")
def validate():
counts = {".py": 0, ".json": 0}
for folder, dirs, files in os.walk(ROOT):
dirs[:] = [name for name in dirs if name not in IGNORED]
for name in files:
path = Path(folder) / name
if path.suffix == ".py":
ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
counts[".py"] += 1
elif path.suffix == ".json":
read_json(path)
counts[".json"] += 1
spec = read_json(ROOT / "agentcore/agentcore.json")
runtime_names = {runtime["name"] for runtime in spec["runtimes"]}
for runtime in spec["runtimes"]:
location = ROOT / runtime["codeLocation"]
require_file(location / runtime["entrypoint"])
require_file(location / "pyproject.toml")
require_file(location / "uv.lock")
for gateway in spec["agentCoreGateways"]:
for target in gateway["targets"]:
schema = ROOT / target["lambdaFunctionArn"]["toolSchemaFile"]
require_file(schema)
tools = read_json(schema)
if not isinstance(tools, list) or not tools:
raise ValueError(f"Contrato de herramientas vacío o inválido: {schema}")
for tool in tools:
if not isinstance(tool.get("name"), str) or not isinstance(tool.get("inputSchema"), dict):
raise ValueError(f"Herramienta inválida: {schema}")
for harness in spec["harnesses"]:
path = ROOT / harness["path"] / "harness.json"
require_file(path)
if read_json(path)["name"] != harness["name"]:
raise ValueError(f"Nombre de harness inconsistente: {path}")
for key, relative in SCENARIOS.items():
scenario = read_json(ROOT / relative)
if scenario["id"] != key or scenario["runtimeName"] not in runtime_names:
raise ValueError(f"Escenario inconsistente: {relative}")
if not isinstance(scenario.get("prompt"), str) or not scenario["prompt"].strip():
raise ValueError(f"Prompt inválido: {relative}")
for relative in LAMBDAS.values():
require_file(ROOT / relative)
print(f"OK: {counts['.py']} Python, {counts['.json']} JSON y referencias locales.")
print("Validación estática; ejecutar agentcore validate para el esquema oficial.")
def package():
validate()
output = ROOT / "artifacts"
output.mkdir(exist_ok=True)
for name, relative in LAMBDAS.items():
source = ROOT / relative
target = output / f"{name}.zip"
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as archive:
archive.write(source, arcname=source.name)
print(f"Creado: {target}")
def extract_text(raw):
"""Extrae deltas Strands SSE; no transforma eventos de error en éxito."""
parts = []
for line in raw.splitlines():
if not line.startswith("data:"):
continue
try:
event = json.loads(line[5:].strip())
except json.JSONDecodeError:
continue
if not isinstance(event, dict):
continue
if event.get("error") or event.get("errorMessage"):
raise ValueError(f"Error del runtime: {event}")
inner = event.get("event", {})
if not isinstance(inner, dict):
continue
for key, value in inner.items():
if key.lower().endswith("exception"):
raise ValueError(f"Error del stream: {key}: {value}")
text = inner.get("contentBlockDelta", {}).get("delta", {}).get("text")
if isinstance(text, str):
parts.append(text)
return "".join(parts)
def invoke(args):
match = re.fullmatch(r"arn:aws(?:-[a-z]+)*:bedrock-agentcore:([a-z0-9-]+):\d{12}:runtime/[A-Za-z0-9_-]+", args.runtime_arn)
if not match:
raise ValueError("Indica un ARN de runtime Bedrock AgentCore válido.")
scenario = read_json(ROOT / SCENARIOS[args.scenario])
prompt = args.prompt if args.prompt is not None else scenario["prompt"]
if not prompt.strip():
raise ValueError("El prompt no puede estar vacío.")
session_id = args.session_id or str(uuid.uuid4())
if not 33 <= len(session_id) <= 256:
raise ValueError("El ID de sesión debe tener entre 33 y 256 caracteres.")
try:
import boto3
from botocore.config import Config
except ImportError as exc:
raise ValueError("Instala requirements-demo.txt para invocar AWS.") from exc
session = boto3.Session(profile_name=args.profile, region_name=match.group(1))
client = session.client("bedrock-agentcore", config=Config(read_timeout=180))
print(f"Escenario: {args.scenario}; runtime esperado: {scenario['runtimeName']}")
print(f"Destino: {args.runtime_arn}\nSesión: {session_id}", flush=True)
result = client.invoke_agent_runtime(
agentRuntimeArn=args.runtime_arn,
runtimeSessionId=session_id,
payload=json.dumps({"prompt": prompt}).encode("utf-8"),
)
stream = result["response"]
try:
raw = stream.read().decode("utf-8")
finally:
stream.close()
text = extract_text(raw)
if not text:
print(raw)
raise ValueError("No se recibieron deltas de texto SSE; revisar respuesta del runtime.")
print(text)
print("\nCriterios de revisión manual:")
for item in scenario["review"]:
print(f"- {item}")
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("validate", help="Valida archivos y referencias sin AWS")
commands.add_parser("package", help="Genera los dos ZIPs de Lambda sin desplegar")
online = commands.add_parser("invoke", help="Invoca un runtime existente en AWS")
online.add_argument("--scenario", choices=SCENARIOS, required=True)
online.add_argument("--runtime-arn", required=True)
online.add_argument("--profile")
online.add_argument("--session-id")
online.add_argument("--prompt")
args = parser.parse_args()
try:
if args.command == "validate":
validate()
elif args.command == "package":
package()
else:
invoke(args)
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())