Галоўная / Артыкулы / Практычныя прытамулкі: Яшчэ адні проект у LangGraph – стварэнне службы падтрымкі кляўэнтаў

Практычныя прытамулкі: Яшчэ адні проект у LangGraph – стварэнне службы падтрымкі кляўэнтаў

Практычныя прыказкі: Яшчэ адні проекты з LangGraph – стварэнне службы падтрымкі клёячых: контракты, перакантровкі та месцы для коду для команд, якія викорыстоўваюць гэты патэрн.

5139 слоў

Наступныя прыміткі паказваюць практычны шлях адпрацоўкі проекту «Ваш першы справжній LangGraph-проект: стварэнне агента падтрымкі кляўэнтаў». Акцэнс ставіцца на контракты, перакананняя і месцы для коду, а не на мотывацыйныя аспекты. Калі працуеце над стадзіяй агледжэння, спачатку запісайце контракт: неабходныя даны, сігнал успеху і тое, што вядзецца ў разы частковай нявыполнення. Такі список контроля дапамагае заліцваты змяны в кодзе пасля таго. Храніце настройкі за межамі коду прыемленае. Файлы сераўіса, хранальнікі секрэтных данных і флагі функцыйяў должны знаходзіцца ў аднам месцы, куда аператары можуць адбавіць перагляд без неабходнасці чытання всего графа.

Перш чым напісаць хоць адну лінію: разумейце план

Этап «Перш чым пісаць» працюе найэфектывней, калі яго розглядаць як вимерную паверхню. Зафіксавце адны ідеальны прыклад, адну справу з бягамі та прыметку па анулюванні змян перш чым расширваць масштаб. Дакументавайце як шлях успеху, так і шлях вярнення да нормальнага стану. Перапрыбуткі, людзкія контраліны та обработка некоректных паведамленняў ёсць частью продукту, а не пасляднім дапрацоўкам. Храніце стан графа ў простым і типаванам формате. Вкладзеныя блокі маскуюць інфармацыю пра тое, канфігурацыйны вузел запісаў канкрэтнае поле, і спакоююць продовжэнне роботы пасля перарываў.

Настройка

Процес настройкі працюе найэфектывней, калі яго розглядаць як вимерную паверхню. Зафіксавце адны ідеальны прыклад, адну справу з бягамі та прыметку па анулюванні змян перш чым расширваць масштаб. Вядзецца прыямо да маленькіх, тэставаных елементаў у працоўным процесе, а не да вялікіх скрыптав. Калі якісь крок не выйшаў, прычына бягу должна вказываць на адну конкрэтную адпаведальнасць, а не на заплутаны ланцюг задач. Храніце стан графа ў простым і типаванам формате. Вкладзеныя блокі маскуюць інфармацыю пра тое, канфігурацыйны вузел запісаў канкрэтнае поле, і спакоююць продовжэнне роботы пасля перарываў.

pip install langgraph langchain langchain-openai langgraph-checkpoint-sqlite python-dotenv
OPENAI_API_KEY=your-key-here

Модуль 1: Імпорты і налаштаванні

Модуль 1: Імпорты і налаштаванні працюе найэфективней, калі яго рассматрываюць як мераваемую структуру. Перш чым расширваць сферу яго дзеяння, зафіксавайце адны ідеальны прыклад работы, адны прыклад неудачы і прыметку па вярнэнню да пачатковага стану. Рассматрывайце гэты этап як кантракт межа вхіднымі даннымі і пераканаленымі выходнымі рэзультатамі. Даўце назвы всім элементам, задаць критэрыя успеху і не падзеўляйцеся частым, непূরным выкананнем задач. Храніце стан графа ў простам і типаваным формате. Вярнутыя структуры данных маскуюць інфармацыю пра тое, який вузел запісаў кожны поле, і спакойваюць працэс пасля перарываў. Модуль 1: Імпорты і налаштаванні працюе найэфективней, калі яго рассматрываюць як мераваемую структуру. Перш чым расширваць сферу яго дзеяння, зафіксавайце адны ідеальны прыклад работы, адны прыклад неудачы і прыметку па вярнэнню да пачатковага стану. Храніце налаштаванні паза кодам прыкладнага програму. Файлы сераўіса, базы секрэтных данных і флагі функцыйяў должны знаходзіцца ў аднам месцы, куда аператары можу аудытаваць іх без неабходнасці чытаць весь граф.

# ============================================================
# MODULE 1: IMPORTS & CONFIGURATION
# ============================================================
import os
import sqlite3
from typing import Annotated, Literal
from datetime import datetime
from dotenv import load_dotenv
# LangChain - the AI layer
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
    HumanMessage,
    AIMessage,
    SystemMessage,
    BaseMessage,
    RemoveMessage,
)
from langchain_core.tools import tool
# LangGraph - the graph layer
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
load_dotenv()
# ── The LLM ─────────────────────────────────────────────────
# temperature=0 means deterministic - the agent behaves
# consistently, which is what you want for a support bot.
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

Модуль 2: Стан

Для стадіі „Стан“ модуля 2 неабяжна ўзначыць вхідныя даны, адпаведальнага за крок і крэтыяры завершэння пры зміне коду. Аперацыйныя працавнікі должны магчымае запускаць крок з вядомай точкі контролю, не падозрываючы прыхованы стан. Неабяжна задокументаваць як шлях успеху, так і шлях вярнення. Перапрыбуткі, людзкіе перакрыцця і обробка некоректных паведамленняў ёсць часткай продукту, а не пасляднім дапрацоўкам. Неабяжна атрыбуаваць людзкую затверджэнняе тым крокам, якія витрачаюць грошы або зменяюць даны праўдзівай роботы. Працэс кампіляцыі не ўзначае повнайшага адпрацоўкі бізнес-логіки.

# ============================================================
# MODULE 2: STATE
# ============================================================

class SupportState(MessagesState):
    # MessagesState already gives us:
    #   messages: Annotated[list[BaseMessage], add_messages]

    # We add three more fields for our specific needs:
    # The running summary of the conversation (Part 2 pattern).
    # Starts empty. Gets written by summarize_node when conversation gets long.
    summary: str

    # The name of the customer, extracted early in the conversation.
    # Used to personalise every response. Starts empty.
    customer_name: str

    # Tracks the current ticket category, set by the agent.
    # Helps the human reviewer understand context during escalation.
    # Values: "order_inquiry" | "refund_request" | "complaint" | "general"
    ticket_category: str

Чаму саме этыя тры поля?

Кабы з’ясаваць, чаму трэба выкарыстоўваць гэтыя тры поля, паказваецца неабходнасць задаць вхідныя даны, адпаведальнага за крок і критэрыя завершэння пры зміне коду. Аперацыйныя працавнікі должны магчыма ўвайсці крок з вядомай точкі контролю, не прабуючы спадарацца пра схованы стан. Лепш выкарыстоўваць маленькія, тэставаныя елементы замест большых скрыптаў. Калі крок не выйшоў, прычына неудачы должна вказываць на адну конкрэтную адпаведальнасць, а не на заплутаны процес. Неабходна людская апраўда ў тых моментах, калі выконваюцца витраты чы зміняюцыся даны для працы. Компіляцыйныя налаштаванні не ўзроўнаўцяюцься з повнай адпаведальнасцю ў бізнесе.

Модуль 3: Інструменты

Для стадіі «Адыявы модуля 3: Інструменты» неабходна прадзеявленне вхідных дадзей, адпаведнага адпаведальніка за крок і крэтарыяў выходу пры перадзеявленні коду. Аператары должны магчымаць паўтарнае адрыхленне крока з вядомага пункта контролю, не спадзяючыся на заштынены стан. Спрыятлівае ставленне да гэтай стадіі як да кантракту межы вхідных дадзей і перакананых выходных рэзультатаў. Назваць артыфакты, прадзеявліць крэтарыяў успеху і адмовіцца ад беззвучнага частковага завершэння. Аутентыфікацыя практыкуецца на в’язку, а пера-автарызацыя — на роўні дадзэй. Толькі токен-носіцель не ёстся межай арэнды. Для стадіі «Адыявы модуля 3: Інструменты» неабходна прадзеявленне вхідных дадзей, адпаведнага адпаведальніка за крок і крэтарыяў выходу пры перадзеявленні коду. Аператары должны магчымаць паўтарнае адрыхленне крока з вядомага пункта контролю, не спадзяючыся на заштынены стан. Конфігурацыю трэба зберагчы за межамі коду прыемленае. Файлы сяродавішча, хранільнікі секрэтных дадзей і флагі функцияў должны знаходзіцца ў аднам месцы, якое аператары можаць пераглядаць без неабходнасці чытання всей структуры.

# ============================================================
# MODULE 3: TOOLS
# ============================================================
# ── Fake Database ────────────────────────────────────────────
# In a real project, these would be database queries or API calls.
# For learning purposes, we use a simple Python dictionary.
ORDERS_DB = {
    "ORD-001": {
        "customer": "Alex",
        "product": "Wireless Headphones",
        "status": "Delivered",
        "amount": 89.99,
        "delivery_date": "2025-06-10",
    },
    "ORD-002": {
        "customer": "Sam",
        "product": "Phone Case",
        "status": "In Transit",
        "amount": 14.99,
        "delivery_date": "Expected 2025-06-18",
    },
    "ORD-003": {
        "customer": "Jordan",
        "product": "Laptop Stand",
        "status": "Processing",
        "amount": 45.00,
        "delivery_date": "Expected 2025-06-20",
    },
}

@tool
def lookup_order(order_id: str) -> str:
    """Look up the details of a customer's order by order ID.
    Use this when the customer provides an order number and wants
    to know the status, product name, or delivery date of their order.
    Args:
        order_id: The order ID string, e.g. 'ORD-001'
    Returns:
        A formatted string with full order details, or an error message
        if the order is not found.
    """
    order = ORDERS_DB.get(order_id.upper())
    if not order:
        return f"No order found with ID '{order_id}'. Please double-check the order number."
    return (
        f"Order {order_id.upper()}: {order['product']} | "
        f"Status: {order['status']} | "
        f"Amount: ${order['amount']:.2f} | "
        f"Delivery: {order['delivery_date']}"
    )

@tool
def check_refund_eligibility(order_id: str) -> str:
    """Check whether an order is eligible for a refund.
    Use this BEFORE processing any refund request. An order is eligible
    for a refund only if its status is 'Delivered'. Orders Fthat are
    'In Transit' or 'Processing' cannot be refunded yet.
    Args:
        order_id: The order ID string, e.g. 'ORD-001'
    Returns:
        A string stating whether the order is eligible and why.
    """
    order = ORDERS_DB.get(order_id.upper())
    if not order:
        return f"Cannot check refund: order '{order_id}' not found."
    if order["status"] == "Delivered":
        return (
            f"Order {order_id.upper()} IS eligible for a refund. "
            f"Product: {order['product']}, Amount: ${order['amount']:.2f}. "
            f"Proceed to refund processing."
        )
    else:
        return (
            f"Order {order_id.upper()} is NOT eligible for a refund yet. "
            f"Current status: {order['status']}. Refunds are only available "
            f"for delivered orders."
        )

@tool
def process_refund(order_id: str, reason: str) -> str:
    """Process a refund for a delivered order.
    IMPORTANT: This tool actually issues the refund. It should only be
    called AFTER human approval has been obtained. Never call this tool
    without prior confirmation.
    Args:
        order_id: The order ID to refund
        reason: The customer's stated reason for the refund
    Returns:
        A confirmation string with the refund reference number.
    """
    order = ORDERS_DB.get(order_id.upper())
    if not order:
        return f"Refund failed: order '{order_id}' not found."
    # In a real system, this would hit your payments API.
    refund_ref = f"REF-{order_id.upper()}-{datetime.now().strftime('%H%M%S')}"
    return (
        f"Refund APPROVED and PROCESSED. Reference: {refund_ref}. "
        f"${order['amount']:.2f} will be returned to the original payment method "
        f"within 3–5 business days. Reason logged: '{reason}'."
    )

# ── Collect tools and bind to LLM ───────────────────────────
# All three tools in one list.
tools = [lookup_order, check_refund_eligibility, process_refund]
# llm_with_tools = the LLM that KNOWS about the tools and can decide to call them.
# This is what we use inside agent_node.
llm_with_tools = llm.bind_tools(tools)
# tool_node = the pre-built node that EXECUTES whatever tool the LLM chose.
# This is what we register in Module 6.
tool_node = ToolNode(tools)

Правілла докстрынгу — ўсё зноў

Калі працуеце над стадзіяй «Правілла докстрынгу», спачатку запісайце умовы викорыстання: неабходныя даны, сігнал успеху і тое, што вядзець да частковага невыпання. Такі список дапамагае заліцвачыць змяны ў кодзе. Документавайце як шлях успеху, так і шлях вярнення да нормы. Перапрыбуткі, людзкі контроль і обработка некоректных паведамленняў є частью продукту, а не дадатковым элементам для доўнешняй наладкі. Ставьце контрольныя пункты пасля дорогіх крокаў. Система вярнення не должна занова ставіць плату за той самы вызыв LLM, калі аператар перапрыбуе выконання наступнага вузла.

Модуль 4: Вузлы

Калі працюеце над стадзіяй «Вузлы» Модуля 4, спачатку запісайце контракт: неабяжлівыя вхідныя даны, сігнал успеху і тое, што выходзіць у разе частковага абякання. Такій чэк-ліст дапамагае заставаць змяны коду чыстымі. Валіце маленькія, тэставаныя елементы замест вялікіх скрыптав. Калі якісь крок абякае, прычына абякання павінна вказваць на адзіну абяспекавую функцыю, а не на заплутаны ланцужок задач. Зробіце перапактаванне пасля дорогіх крокаў. Програма не павінна зноў выклікаць той самы календар LLM, калі аператар прабуе зноў запрацаваць з пазнейшым вузлам.

# ============================================================
# MODULE 4: NODES
# ============================================================

# ── The System Prompt ────────────────────────────────────────
# Written once, used in every call to the LLM from agent_node.
# This is the personality and rulebook of your agent.

SYSTEM_PROMPT = """You are ShopBot, a friendly and professional customer support \
agent for an e-commerce store.
Your capabilities:
- Look up order details using the lookup_order tool
- Check if an order qualifies for a refund using check_refund_eligibility
- Process approved refunds using the process_refund tool
Your rules:
- Always greet the customer by name once you know it
- Always check refund eligibility BEFORE attempting to process a refund
- For refund requests, set ticket_category to "refund_request" in your reasoning
- Be empathetic, clear, and concise
- If you cannot help, offer to escalate to a human agent
Important: The process_refund tool requires prior human approval. Do not call it \
unless the conversation shows that a human has already approved the refund."""

# ── Node 1: agent_node ──────────────────────────────────────
def agent_node(state: SupportState) -> dict:
    """The brain of the operation. Reads state, calls the LLM, and decides
    whether to use a tool, give a final answer, or do something else.
    This node handles two cases:
    1. Normal conversation - just call the LLM and respond
    2. Long conversation - if a summary exists, prepend it so the LLM
       has context without seeing all the raw messages
    """

    # Part 2 pattern: check for an existing summary
    summary = state.get("summary", "")
    if summary:
        # Build context: system prompt + compressed history + recent messages
        system_with_summary = SystemMessage(
            content=f"{SYSTEM_PROMPT}\n\nSummary of conversation so far:\n{summary}"
        )
        messages_to_send = [system_with_summary] + state["messages"]
    else:
        # No summary yet - full history is short enough to send as-is
        system_msg = SystemMessage(content=SYSTEM_PROMPT)
        messages_to_send = [system_msg] + state["messages"]
    # Call the LLM. It sees tools and can choose to call one.
    response = llm_with_tools.invoke(messages_to_send)

    # Detect ticket category from the response for routing purposes.
    # A smarter version would have the LLM explicitly set this -
    # for now, we scan for keywords.
    content_lower = response.content.lower() if response.content else ""
    updates: dict = {"messages": [response]}
    if "refund" in content_lower or (
        hasattr(response, "tool_calls")
        and any("refund" in str(tc).lower() for tc in (response.tool_calls or []))
    ):
        updates["ticket_category"] = "refund_request"
    return updates

# ── Node 2: review_refund ───────────────────────────────────
def review_refund(state: SupportState) -> dict:
    """The human approval gate. Pauses execution, shows the pending refund
    details to a human agent, and waits for their decision.
    This implements the Part 3 interrupt() pattern. Execution stops here
    until someone calls graph.invoke(Command(resume=...), config).
    Three outcomes the human can choose:
    - "approve"  → let the refund tool call proceed unchanged
    - "reject"   → cancel the refund, send a message to the customer
    - "escalate" → hand the entire ticket to a human support agent
    """

    last_message = state["messages"][-1]
    # Find the refund-related tool call in the last AI message.
    # We look for process_refund specifically - the "real action" tool.
    refund_tool_call = None
    if hasattr(last_message, "tool_calls"):
        for tc in last_message.tool_calls:
            if "refund" in tc["name"].lower():
                refund_tool_call = tc
                break

    # Surface the context to the human reviewer via interrupt().
    # Everything in this dict is what the human sees before deciding.
    human_decision = interrupt({
        "message": "⚠️ Refund approval required",
        "customer_name": state.get("customer_name", "Unknown"),
        "tool_being_called": refund_tool_call["name"] if refund_tool_call else "refund tool",
        "arguments": refund_tool_call["args"] if refund_tool_call else {},
        "conversation_summary": state.get("summary", "No summary yet"),
        "options": ["approve", "reject", "escalate"],
    })

    # ── Handle the human's decision ─────────────────────────
    if human_decision == "approve":
        # Do nothing to state - let tool_node execute the tool call as-is
        return {}
    elif human_decision == "reject":
        # Cancel the tool call. The LLM will see a ToolMessage explaining why,
        # and generate a polite response to the customer.
        from langchain_core.messages import ToolMessage
        return {
            "messages": [
                ToolMessage(
                    content=(
                        "Refund request was reviewed and declined by our support team. "
                        "Please inform the customer politely and offer alternatives."
                    ),
                    tool_call_id=refund_tool_call["id"] if refund_tool_call else "unknown",
                )
            ]
        }
    elif human_decision == "escalate":
        # Signal escalation - in a real system you'd open a ticket,
        # ping Slack, or transfer to a live agent queue.
        from langchain_core.messages import ToolMessage
        return {
            "messages": [
                ToolMessage(
                    content=(
                        "This ticket has been escalated to a senior support agent. "
                        "Inform the customer that a human agent will contact them "
                        "within 2 business hours."
                    ),
                    tool_call_id=refund_tool_call["id"] if refund_tool_call else "unknown",
                )
            ]
        }
    # Fallback - treat as approve
    return {}

# ── Node 3: summarize_node ──────────────────────────────────
def summarize_node(state: SupportState) -> dict:
    """Triggered when the conversation exceeds 6 messages. Compresses the
    full message history into a short summary, then deletes old raw messages.
    This is the rolling summary pattern from Part 2. The summary grows
    richer turn by turn. Token costs stay nearly flat no matter how long
    the conversation runs.
    """

    existing_summary = state.get("summary", "")
    if existing_summary:
        # Extend the existing summary with new messages
        summary_instruction = (
            f"Current summary:\n{existing_summary}\n\n"
            "Extend this summary with the new messages above. "
            "Keep it under 5 sentences. Focus on: the customer's name, "
            "their issue, any orders mentioned, and what actions were taken."
        )
    else:
        # First time summarising
        summary_instruction = (
            "Summarise this customer support conversation in under 5 sentences. "
            "Include: the customer's name (if mentioned), their issue, "
            "any order numbers discussed, and what actions were taken so far."
        )

    messages = state["messages"] + [HumanMessage(content=summary_instruction)]
    response = llm.invoke(messages)  # Plain llm, no tools needed here
    # Delete all but the 2 most recent messages.
    # The summary now holds everything that was in the deleted messages.
    messages_to_delete = [
        RemoveMessage(id=m.id) for m in state["messages"][:-2]
    ]
    return {
        "summary": response.content,
        "messages": messages_to_delete,
    }

Модуль 5: Канцы і маршрутызацыя

Калі працуеце над стадзіяй «Маршрутаванне на краях» Модуля 5, спачатку запісайце контракт: неабяжлівыя вхідныя даны, сигнал успеху і тое, што выходзіць у разе частковага невыпання. Такі список пераконвае ў тым, што пазнейшыя змены коду будуць чыстымі. Спрыймайце гэтую стадзію як контракт межа вхіднымі данымі і перакананымі выходнымі рэзультатамі. Дайце назвы артыфактам, задаце правіла пераканання успеху і не падзволяйце частковаму завершэнню без паведамлення. Зробіце перапытку пасля дорогіх крокаў. Система вярнення не павінна знову ставіць плату за той самы вызов LLM, калі аператар прабуе зноў запрацаваць з пазнейшым вузлом. Калі працуеце над стадзіяй «Маршрутаванне на краях» Модуля 5, спачатку запісайце контракт: неабяжлівыя вхідныя даны, сигнал успеху і тое, што выходзіць у разе частковага невыпання. Такі список пераконвае ў тым, што пазнейшыя змены коду будуць чыстымі. Зберагаеце настройкі паза кодам прыемліка. Файлы сераўнавання, хранільнікі секрэтных данных і флагі функцыйяў павінны знаходзіцца ў аднам месцы, якое аператары можаць пераглядаць без неабяжлівага чытання всей структуры.

# ============================================================
# MODULE 5: EDGES & ROUTING
# ============================================================

def route_after_agent(state: SupportState) -> Literal[
    "review_refund", "tools", "summarize_node", "__end__"
]:
    """Called after agent_node runs. Decides what happens next.
    Four possible routes:
    1. The LLM wants to call process_refund → must go through human review first
    2. The LLM wants to call any other tool → go directly to tool_node
    3. The LLM gave a plain text answer AND the conversation is long → summarise
    4. The LLM gave a plain text answer and conversation is short → we're done
    """
    last_message = state["messages"][-1]
    has_tool_calls = hasattr(last_message, "tool_calls") and bool(last_message.tool_calls)
    if has_tool_calls:
        # Check if ANY of the tool calls is the sensitive process_refund tool
        tool_names = [tc["name"] for tc in last_message.tool_calls]
        if "process_refund" in tool_names:
            return "review_refund"   # → Pause for human approval first
        return "tools"               # → Safe tool, run it directly
    # No tool call - the LLM gave a plain response.
    # Check if the conversation is long enough to need summarisation.
    if len(state["messages"]) > 6:
        return "summarize_node"
    return "__end__"                 # → Conversation turn is complete

def route_after_review(state: SupportState) -> Literal["tools", "agent_node"]:
    """Called after review_refund runs (i.e., after the human has decided).
    Two routes:
    1. Human approved or escalated → run the tool (tool_node handles the call)
    2. Human rejected → the review node already added a ToolMessage cancelling
       the tool call, so skip tool_node and go back to agent_node to respond
    """
    last_message = state["messages"][-1]
    # If the last message is a ToolMessage, the review node cancelled the call.
    # Go back to agent_node so it can generate a customer-facing response.
    from langchain_core.messages import ToolMessage
    if isinstance(last_message, ToolMessage):
        return "agent_node"
    # Otherwise, the review node returned {} (approved) - proceed to tools.
    return "tools"

Модуль 6: Складанне графа

Модуль 6: Робота з асамбляжамі графа ўскладнюецца, калі яго спрыяваць як вимерную паверхню. Зафіксавайце адны ідеальны прыклад роботы, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перш чым расширваць сферу дзеяння. Документавайце як шлях успеху, так і шлях вярнэння да нормальнага стану разам. Перапрыбуткі, людзкі контроль і обработка некоректных паведамленняў є частью продукту, а не етапамі далейшай наладкі. Храніце стан графа у простам і типаваным формате. Вкладаныя структуры маскуюць інфармацію пра тое, який вузел запісаў якое поле, і спакошуюць працю пасля перерываў.

# ============================================================
# MODULE 6: GRAPH ASSEMBLY
# ============================================================

# ── Step 1: Initialize ──────────────────────────────────────
graph_builder = StateGraph(SupportState)
# ── Step 2: Register All Nodes ──────────────────────────────
# Format: add_node("string_name", function)
# The string name is what you use in every edge definition below.
graph_builder.add_node("agent_node", agent_node)
graph_builder.add_node("tools", tool_node)          # Pre-built from Module 3
graph_builder.add_node("review_refund", review_refund)
graph_builder.add_node("summarize_node", summarize_node)
# ── Step 3: Set Entry Point ─────────────────────────────────
# The first node that runs when a user sends a message.
graph_builder.add_edge(START, "agent_node")
# ── Step 4: Wire the Edges ──────────────────────────────────
# After agent_node: conditional - depends on what the LLM decided
graph_builder.add_conditional_edges(
    "agent_node",           # Source
    route_after_agent,      # Router function from Module 5
    {
        "review_refund": "review_refund",   # Refund tool → human review first
        "tools": "tools",                   # Other tools → run directly
        "summarize_node": "summarize_node", # Long conversation → summarise
        "__end__": END,                     # Plain answer → done
    }
)
# After review_refund: conditional - depends on human's decision
graph_builder.add_conditional_edges(
    "review_refund",
    route_after_review,
    {
        "tools": "tools",           # Approved → execute the tool
        "agent_node": "agent_node", # Rejected → back to agent to respond
    }
)
# After tools run: always go back to agent_node
# (the ReAct loop - agent sees tool result, decides what to do next)
graph_builder.add_edge("tools", "agent_node")
# After summarization: conversation turn is done
graph_builder.add_edge("summarize_node", END)
# ── Step 5: Compile ─────────────────────────────────────────
# Using MemorySaver for development.
# For production, swap this one line to SqliteSaver or PostgresSaver.
memory = MemorySaver()
shopbot = graph_builder.compile(checkpointer=memory)

Візуалізацыя графа (необавязкова, але рэкамендуемая)

Этап візуалізацыі графа, як апшароннай функцыі, працюе найэфектывнейша, калі яго розглядаць як вимерную паверхню. Запісаце адна «золатая» транскрыпцыя, адзін прыклад неудачы і прыметку па адвярненні роботы перш чым расширваць масштаб. Вольбяйце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок не выйшае, прычына неудачы павінна вказваць на адную адпаведальнасць, а не на заплутаны ланцюг задач. Рэзультаты роботы графа трэба зберагаць у простай форме з чыстым типам. Вкладаныя структуры маскуюць інфармацыю пра тое, який вузел запісаў канкрэтны поле, і спакоююць продовжэнне роботы пасля перерываў.

from IPython.display import display, Image
from langchain_core.runnables.graph import MermaidDrawMethod

display(Image(
    shopbot.get_graph().draw_mermaid_png(
        draw_method=MermaidDrawMethod.API
    )
))

Модуль 7: Точка входу

Модуль 7: Entrypoint працюе найкраща, калі яго розглядаць як вимерную паверхню. Зафіксавце адзін ідеальны прымер роботы, адзін кейс неудачы і прыметкі па поверненню да пачатковага стану пры расшырэнні масштаба. Разглядзайце гэты этап як кантракт межа вхіднымі даннымі і пераканаленымі выходнымі рэзультатамі. Даўце назвы артыфактам, задаце критэрыя успеху і адмовіцеся ад тыхоўскага частковага завершэння. Храніце стан графа ў простам і типаваным формате. Вкладзеныя блокі маскуюць, який вузел запісаў канкрэтны поле, і спакойваюць працу пасля перарываў. Модуль 7: Entrypoint працюе найкраща, калі яго розглядаць як вимерную паверхню. Зафіксавце адзін ідеальны прымер роботы, адзін кейс неудачы і прыметкі па поверненню да пачатковага стану пры расшырэнні масштаба. Храніце настройкі за межамі коду прыемліка. Файлы сяродавішча, хранальнікі секрэтных данных і флагі функцыйяў должны знаходзіцца ў аднам месцы, якое аператары можаць пераглядаць без неабяжнага чытання всего графа.

# ============================================================
# MODULE 7: ENTRYPOINT
# ============================================================

def run_shopbot():
    """
    Interactive command-line session with ShopBot.
    Demonstrates: multi-turn conversation, tool use, and human-in-the-loop.
    """
    print("=" * 55)
    print("  ShopBot - Customer Support Agent")
    print("  Powered by LangGraph")
    print("=" * 55)
    print("Type your message below. Type 'exit' to quit.")
    print("Type 'state' to inspect what ShopBot currently remembers.\n")
    # One config per session.
    # thread_id is the session key - same ID = same memory thread.
    # Change the ID to start a completely fresh conversation.

    config = {"configurable": {"thread_id": "customer-session-001"}}
    while True:
        user_input = input("You: ").strip()
        if not user_input:
            continue
        if user_input.lower() == "exit":
            print("ShopBot: Thank you for contacting support. Have a great day!")
            break

        # ── Debug: inspect current state ────────────────────
        if user_input.lower() == "state":
            snapshot = shopbot.get_state(config)
            print("\n[DEBUG] Current State:")
            print(f"  Messages in state : {len(snapshot.values.get('messages', []))}")
            print(f"  Customer name     : {snapshot.values.get('customer_name', '(not set)')}")
            print(f"  Ticket category   : {snapshot.values.get('ticket_category', '(not set)')}")
            print(f"  Summary           : {snapshot.values.get('summary', '(none yet)')}")
            print(f"  Next node(s)      : {snapshot.next}\n")
            continue

        # ── Normal message: invoke the graph ─────────────────
        result = shopbot.invoke(
            {"messages": [HumanMessage(content=user_input)]},
            config=config,
        )

        # ── Check if graph paused for human approval ─────────
        # This is how you detect that interrupt() was called inside review_refund.
        while "__interrupt__" in result:
            interrupt_data = result["__interrupt__"][0].value
            print("\n" + "=" * 55)
            print("  HUMAN APPROVAL REQUIRED")
            print("=" * 55)
            print(f"  Customer    : {interrupt_data.get('customer_name', 'Unknown')}")
            print(f"  Action      : {interrupt_data.get('tool_being_called', 'refund')}")
            print(f"  Arguments   : {interrupt_data.get('arguments', {})}")
            print(f"  Context     : {interrupt_data.get('conversation_summary', 'N/A')}")
            print("=" * 55)
            print("Options: [a] Approve   [r] Reject   [e] Escalate")
            human_choice = input("Your decision: ").strip().lower()
            if human_choice == "a":
                resume_value = "approve"
            elif human_choice == "r":
                resume_value = "reject"
            elif human_choice == "e":
                resume_value = "escalate"
            else:
                print("Invalid choice. Defaulting to reject.")
                resume_value = "reject"
            # Resume the graph with the human's decision.
            # Command(resume=...) answers the pending interrupt() call.
            result = shopbot.invoke(
                Command(resume=resume_value),
                config=config,
            )

        # ── Print the agent's final response ─────────────────
        last_message = result["messages"][-1]
        print(f"\nShopBot: {last_message.content}\n")

if __name__ == "__main__":
    run_shopbot()

Експлуатацыя: як выглядае рэальная кансарабатація

Для запуску — календарная стадія, пакуйце вхідныя даны, адміністратара крока і критэрыя завершэння пры перадзеіснаванні коду. Аператары должны магчымаць перапрацаваць крок з вядомага пункта контролю, не спрабоўваючы здогадвацца пра схованы стан. Документавайце як шлях успеху, так і шлях вярнення. Перапрацавкі, людзкіе перакрыцця і обробка некоректных паведамленняў ёсць часткай продукту, а не пасляднім дапрацоўкам. Заставьце людзкую затверджэнняе на тых кроках, якія витрачаюць грошы або зменяюць даны прадукцыі. Працэс складання коду не ўзроўнаважваецца цэлыснасцю бізнес-процэса.

=======================================================
  ShopBot — Customer Support Agent
  Powered by LangGraph
=======================================================

Type your message below. Type 'exit' to quit.
Type 'state' to inspect what ShopBot currently remembers.

You: Hi, my name is Alex and I need help with order ORD-001
ShopBot: Hi Alex! I'd be happy to help you with order ORD-001.
I can see your order:
- Product: Wireless Headphones
- Status: Delivered on 2025-06-10
- Amount: $89.99
What can I help you with regarding this order?
You: I want to return it and get a refund
ShopBot: I've checked your order ORD-001 and it's eligible for a refund
since it's been delivered. Let me process that for you right away.


=======================================================
  HUMAN APPROVAL REQUIRED
=======================================================

  Customer    : Alex
  Action      : process_refund
  Arguments   : {'order_id': 'ORD-001', 'reason': 'Customer requested return'}
  Context     : No summary yet
=======================================================

Options: [a] Approve   [r] Reject   [e] Escalate
Your decision: a
ShopBot: Great news, Alex! Your refund has been successfully processed.
Refund Reference: REF-ORD-001-143022
Amount: $89.99 will be returned to your original payment method within
3–5 business days.

Is there anything else I can help you with?
You: state
[DEBUG] Current State:
  Messages in state : 6
  Customer name     : (not set)
  Ticket category   : refund_request
  Summary           : (none yet)
  Next node(s)      : ()
You: exit
ShopBot: Thank you for contacting support. Have a great day!

Што вы ўжо створылі і чаму гэта важна

Для стадіі «Што вы ўжо створылі» неабходна прадзефінаваць вхідныя даны, адпаведальную особу за крок і критэрыя завершэння пры перадзмене коду. Аперацыйныя працавнікі должны магчыма ўвайсці крок з вядомай точкі контролю, не падозрываючы прыхованы стан. Валідзіце маленькія, тэставаныя елементы замест большых скрыптов. Калі крок не выйшоў, прычына неудачы павінна вказываць на адзіну адпаведальнасць, а не на заплутаны процес. Заставіце людзкую апраўду на тых этапах, дзе відбываецца выдатак грошэй або зміняюцыся даны для працы системы. Компіляцыйныя налашчэння не ўзроўнаўцуюцься з повнай адпрацоўкай бізнес-процэсаў.

Расшырэнне гэтага проекту (ваашы наступныя крокі)

Для стадіі расшырэння гэтага проекту неабяжна практычная дзеянь: перад змінайом код неабяжна адзначыць вхідныя данні, адпаведальнага за крок і критэрыя завершэння. Аперацыйныя працавнікі должны магчымасць перапрыявленне кроку з вядомага пункта контролю без неабяжнай спрабы з’ясавання схованага стану. Спрыятлівайце гэтую стадію як даговор межа вхіднымі данніма і перакананымі выходнымі рэзультатамі. Даеце назвы артыфактам, адзначайце критэрыя успеху і не практыкуйце тыхія частковыя завершэння. Заставляйце людзкія апраўленні на тых этапах, дзе выкорыстоўваюцца грошы або зміняюцыся данні для працы. Компіляцыйныя налашчэнні не ўзроўнаўцяюцься з пачатковай цэласнасцю бізнес-процэсаў. Для стадіі расшырэння гэтага проекту неабяжна практычная дзеянь: перад змінайом код неабяжна адзначыць вхідныя данні, адпаведальнага за крок і критэрыя завершэння. Аперацыйныя працавнікі должны магчымасць перапрыявленне кроку з вядомага пункта контролю без неабяжнай спрабы з’ясавання схованага стану. Зберагайце налашчэнні за межамі коду прыкладнага праграмы. Файлы сяродавішча, хранільнікі секрэтных дадзеных і флагі функцыйяў должны знаходзіцца ў аднам месцы, якое працавнікі можуць аудытаваць без неабяжнай чытання цэлага коду.

Граф.

Аптавка ключоўых слоў для гэтага проекту

Калі працуеце над аптавкай ключоўых слоў, спачатку запісуйце умовы контракту: неабходныя даны, сігнал успеху і тое, што выканаецца у разы частковага невяснення. Такі список контроля дапамагае заліцвачыць змяны ў кодзе. Документавайце як шлях успеху, так і шлях вяснення проблемы. Перапрыбуткі, людзкія контрольныя пункты і обработка некоректных паведамленняў є частью продукту, а не дадатковым доўнесенням пазней. Ставіце контрольныя пункты пасля дорогіх крокаў. Система вярнення не павінна знову ставіць плату за той самы вызов LLM, калі аператар перапрыбуе пазнейшы вузел.

Вывад: Карта — гэта не тэрыторія

Калі працюеце над стадзіяй «Заключэнне: Карта», спачатку запісайце умовы кантракту: неабяжлівыя даны, сігнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі список перакладзець пазнейшыя змены коду ў правільным напрамку. Валіце маленькія, тэставаныя елементы замест вялікіх скрыптав. Калі якісь крок не выйшае, прычына нявыпання павінна вказваць на адну конкрэтную адпаведальнасць, а не на заплутаны ланцюг задач. Зробіце перапактаванне пасля дорогіх крокаў. Програма не павінна зноў стягваць плата за той самы вызов LLM, калі аператар прабуе зноў выконаць пазнейшы элемент.

ДЛЯ ДЗЕЁЖ НАЎЖНЕЙ ЧАСТИ ГЭТАГО ПРАКТУ: Удосконаленне нашага агента LangGraph для рэальнага е-камерцыі

Калі працюеце над стадзіяй «ДЗЕЙНЫ ЧАСТЬ», спачатку запісайце контракт: неабяжлівыя даны, сігнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі список пераконтроўкаў дапамагае заліцваліваць пазнейшыя змены ў кодзе. Спрыймайце гэтую стадзію як контракт межа данымі і перакананымі выходамі. Дайце назву рэзультатам, задаць критэрыя успеху і не падтрымайце тыхі частковыя завершэння. Зробіце пераконтроўку пасля дорогіх крокаў. Програма не должна зноў выкарыстоўваць той самы вызов LLM, калі аператар прабуе зноў запрацаваць пазнейшы вузел.

Чэрніцкі список для эксплуатацыі

Для стадзіяй «Чэрніцкі список для эксплуатацыі» задаць даны, адпаведальнага за крок і критэрыя завершэння прычымоўкі кодзу. Аператары должны магчыма было зноў запрацаваць крок на адной вядомай пераконтроўцы, не здагадваючыся пра схованы стан.

Запісвайце часы выконання аперацый і косць токенаў чы роезыкаў праза функцыйнае рэзультат. Відразліва візуалізацыя косцаў запобегае неспакойным рахункам, калі парадок пераходзіць з дэмовай среды ў спяльнаныя среды.

Заставьце людзкія апраўды на лініях, якія витрачаюць грошы чы зменяюць данні ў працоўный сэрвыс. Падключэння ў час компіляцыі не адпавядае пачатковай цэлесообразнасі для бізнесу.

Напісце кароткі посоўнік: як ротаваць клучы, як спрачыслаць чергу, як анулюваць пярэдні процес імпорту.

Зберагаўце настройкі праза код аплікацыі. Файлы среды, хранільнікі секрэтных дадзеных і флагі функцыйяў должны знаходзіцца ў аднам месцы, куды аператары можаць адбавіць аудыт без неабходнасці чытання всей структуры.

Заставьце людзкія апраўды на лініях, якія витрачаюць грошы чы зменяюць данні ў працоўный сэрвыс. Падключэння ў час компіляцыі не адпавядае пачатковай цэлесообразнасі для бізнесу.

Перш чым запускать даныя стэка, заморозьце версіі, зафіксавце «золаты» транскрыпты для критычных шляхоў і паказваце спосабы атрыбуціі. У спадзяльных сэрвісах неабходны ліміты частоты запытоў, перакананні ў прыналежнасці тэрміналаў і чысткі выбранага власніка для змены секрэтных даных. Валіце надзейнасць працы над красавімі, але еднаковымі дэманстрацыйнымі прыкладамі.

Прымечанне для пакета ac5eb00f923a: не кладзіце ключы прадаўцаў у репазітарый, задаце верхнюю межу токенаў на кожную сесію і зберагачыце транскрыпты празаўсёды ля інструментаў ацэнкі, каб пазнейшыя замены моделяў заставаліся пораўнанымі.

Знак перапісву 1 для ac5eb00f923a: перафразавайце суседзячыя тверджэння мовай аперацый, не прабачайце змянія слоў у [[CODE_n]] і утримайце ад павторэння рэчэй з выхіднага тексту.

Знак перапісву 2 для ac5eb00f923a: перафразавайце суседзячыя тверджэння мовай аперацый, не прабачайце змянія слоў у [[CODE_n]] і утримайце ад павторэння рэчэй з выхіднага тексту.

Перапісаць маркер 3 для ac5eb00f923a: перафразаваць суседзяючыя тверджэння на языке аперарабоў, застаўляць слоты [[CODE_n]] недзейнымі, і утримвацца ад павторэння рэчэй з выкладку.

Перапісаць маркер 4 для ac5eb00f923a: перафразаваць суседзяючыя тверджэння на языке аперарабоў, застаўляць слоты [[CODE_n]] недзейнымі, і утримвацца ад павторэння рэчэй з выкладку.

Перапісаць маркер 5 для ac5eb00f923a: перафразаваць суседзяючыя тверджэння на языке аперарабоў, застаўляць слоты [[CODE_n]] недзейнымі, і утримвацца ад павторэння рэчэй з выкладку.

Перапісаць маркер 6 для ac5eb00f923a: перафразаваць суседзяючыя тверджэння на языке аперарабоў, застаўляць слоты [[CODE_n]] недзейнымі, і утримвацца ад павторэння рэчэй з выкладку.

Перапісаць маркер 7 для ac5eb00f923a: перафразаваць суседзяючыя тверджэння на языке аперарабоў, застаўляць слоты [[CODE_n]] недзейнымі, і утримвацца ад павторэння рэчэй з выкладку.

Перапісацыя маркера 8 для ac5eb00f923a: перафразавайце суседзяючыя тверджэння на языке аперацый, застаўляйце слоты [[CODE_n]] недзейнасцю, і утримвайцеся ад павторэння рэчы з вучорашняго тексту.

Перапісацыя маркера 9 для ac5eb00f923a: перафразавайце суседзяючыя тверджэння на языке аперацый, застаўляйце слоты [[CODE_n]] недзейнасцю, і утримвайцеся ад павторэння рэчы з вучорашняго тексту.

Перапісацыя маркера 10 для ac5eb00f923a: перафразавайце суседзяючыя тверджэння на языке аперацый, застаўляйце слоты [[CODE_n]] недзейнасцю, і утримвайцеся ад павторэння рэчы з вучорашняго тексту.