Open curriculum · verified editionContribute on GitHub
Lab20 min19 words

Lab 02 — First state graph with LangGraph 1.2.x, fully offline

Localized walkthrough. Run the canonical public lab locally and preserve the result as evidence.

SourceImprove this page

Localized walkthrough. Run the canonical public lab locally and preserve the result as evidence.

Run it#

python modulo-04-agentes/labs/02_langgraph_basico.py

Localized source walkthrough#

"""Lab 02 — First state graph with LangGraph 1.2.x, fully offline.

Build a classification → response → audit workflow. Nodes return partial updates
and edges, not the prompt, guarantee the order.

Execution:
    python modulo-04-agentes/labs/02_langgraph_basico.py
"""

from __future__ import annotations

import argparse
import operator
from typing import Annotated, Literal, TypedDict

from langgraph.graph import END, START, StateGraph
from rich.console import Console
from rich.table import Table

console = Console()


class SupportState(TypedDict):
    question: str
    category: Literal["security", "billing", "technical", "general"]
    answer: str
    audit: Annotated[list[str], operator.add]


def classify(state: SupportState) -> dict:
    text = state["question"].casefold()
    if any(term in text for term in ("clave", "mfa", "seguridad", "token")):
        category = "security"
    elif any(term in text for term in ("factura", "cobro", "pago")):
        category = "billing"
    elif any(term in text for term in ("error", "api", "latencia")):
        category = "technical"
    else:
        category = "general"
    return {"category": category, "audit": [f"classified:{category}"]}


def answer(state: SupportState) -> dict:
    policies = {
        "security": "Revoca la credencial afectada y escala al equipo de seguridad.",
        "billing": "Verifica factura, moneda y estado antes de modificar un cobro.",
        "technical": "Recoge request ID, intervalo temporal y región antes del diagnóstico.",
        "general": "Aclara el objetivo y el resultado esperado antes de actuar.",
    }
    return {
        "answer": policies[state["category"]],
        "audit": [f"answered:{state['category']}"],
    }


def audit(state: SupportState) -> dict:
    checks = [
        "answer_non_empty" if state["answer"].strip() else "answer_empty",
        "category_known" if state["category"] in {"security", "billing", "technical", "general"} else "category_unknown",
    ]
    return {"audit": checks}


def build_graph():
    builder = StateGraph(SupportState)
    builder.add_node("classify", classify)
    builder.add_node("answer", answer)
    builder.add_node("audit", audit)
    builder.add_edge(START, "classify")
    builder.add_edge("classify", "answer")
    builder.add_edge("answer", "audit")
    builder.add_edge("audit", END)
    return builder.compile()


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "question",
        nargs="?",
        default="He publicado por error una clave API, ¿qué hago?",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    result = build_graph().invoke(
        {"question": args.question, "category": "general", "answer": "", "audit": []}
    )
    table = Table(title="Estado final")
    table.add_column("campo")
    table.add_column("valor")
    for key in ("question", "category", "answer", "audit"):
        table.add_row(key, str(result[key]))
    console.print(table)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())