/** * compile.js * ---------------------------------------------------------------------------- * Section 2: LangGraph 1.0 -- state, nodes, persistence, HITL. * 26 slides, 16:9, dark theme, code-heavy. * * Output: section2.pptx (same dir) * PDF + PNG preview: produced by build.sh (soffice + pdftoppm). * * Run: node compile.js */ 'use strict'; const path = require('path'); const ds = require(path.join(__dirname, '..', '..', 'design-system.js')); const { theme, helpers, layouts } = ds; const { slideBase, addHeader, addCodeBlock, addCallout, addProsCons, addPageNumber, addSectionDivider, addSourceLine, withFallback } = helpers; const pptxgen = require('pptxgenjs'); const pres = new pptxgen(); pres.layout = 'LAYOUT_16x9'; pres.title = 'LangChain Evolution: Section 2 -- LangGraph 1.0'; pres.subject = 'LangGraph 1.0 state, nodes, persistence, HITL'; // Override code font size for this section: more lines fit per card. // Original sizes.code = 12 (from design-system). 10 fits ~35% more. const CODE_FONT_SIZE = 10; theme.sizes.code = CODE_FONT_SIZE; // Section-wide constants const SECTION_NUMBER = 2; const SECTION_LABEL = 'SECTION 2'; // Slide counter for page numbers (1-based) let n = 0; const next = () => ++n; // --------------------------------------------------------------------------- // Slide 1 -- Section divider // --------------------------------------------------------------------------- { const s = pres.addSlide(); addSectionDivider(s, pres, theme, { number: SECTION_NUMBER, eyebrow: SECTION_LABEL + ': LANGGRAPH 1.0', title: 'LangGraph 1.0: stateful runtime', intro: 'State, nodes, persistence, HITL. ' + 'Production-ready durable execution: ' + 'агенты, которые переживают падение сервера и одобряются человеком.', }); // No page number on divider -- standard practice. } // --------------------------------------------------------------------------- // Slide 2 -- Why LangGraph // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'WHY LANGGRAPH', title: 'Зачем LangGraph: что не может обычный LangChain', }); addCallout(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 3.5, kind: 'info', title: 'LCEL хорош для', text: '- простых pipeline (prompt | model | parser)\n' + '- одного прохода без ветвлений\n' + '- read-only чатов без state между вызовами', }); addCallout(s, pres, theme, { x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 3.5, kind: 'warning', title: 'LCEL не даёт', text: '- циклов и произвольной топологии графа\n' + '- first-class persistence и time-travel\n' + '- настоящей паузы на human approval\n' + '- multi-agent и параллельных веток (Send)\n' + '- восстановления после падения посреди long-run', }); addSourceLine(s, pres, theme, { source: 'blog.langchain.com/langchain-langgraph-1dot0', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 3 -- Install // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'INSTALL', title: 'Установка: одна команда', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 6.0, h: 1.3, code: 'pip install -U langgraph', }); addCodeBlock(s, pres, theme, { x: 0.5, y: 2.85, w: 6.0, h: 2.0, code: [ '# extras: postgres / sqlite checkpointers -- отдельные пакеты', 'pip install -U langgraph langgraph-checkpoint-postgres', 'pip install -U langgraph langgraph-checkpoint-sqlite', '', '# JS / TypeScript', 'npm install @langchain/langgraph @langchain/langgraph-checkpoint', ].join('\n'), }); addCallout(s, pres, theme, { x: 6.8, y: layouts.CONTENT_TOP, w: 2.7, h: 3.5, kind: 'success', title: 'Что в коробке', text: 'state, persistence, HITL, streaming, ToolNode, subgraph API. ' + 'Никаких внешних сервисов кроме самого рантайма.', }); addSourceLine(s, pres, theme, { source: 'pypi.org/project/langgraph (langgraph 1.2.6, 2026-06-18)', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 4 -- State: TypedDict basic // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'STATE / 1', title: 'State как TypedDict -- первая итерация', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5, code: [ 'from typing_extensions import TypedDict', 'from langgraph.graph import StateGraph, START, END', '', 'class State(TypedDict):', ' question: str', ' answer: str', ' steps: int', '', 'def answer(state: State) -> dict:', ' return {"answer": f"echo: {state[\'question\']}", "steps": 1}', '', 'builder = StateGraph(State)', 'builder.add_node("answer", answer)', 'builder.add_edge(START, "answer")', 'builder.add_edge("answer", END)', 'graph = builder.compile()', '', 'print(graph.invoke({"question": "hi", "steps": 0}))', '# -> {\'question\': \'hi\', \'answer\': \'echo: hi\', \'steps\': 1}', ].join('\n'), highlightLines: [4, 5, 6, 17, 18, 19, 21], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 5 -- State: Annotated + add_messages reducer // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'STATE / 2', title: 'Annotated + add_messages -- каналы с reducer', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from typing import Annotated', 'from typing_extensions import TypedDict', 'from langgraph.graph import StateGraph, START, END', 'from langgraph.graph.message import add_messages', '', 'class State(TypedDict):', ' # reducer add_messages склеивает списки сообщений по правилам', ' messages: Annotated[list, add_messages]', '', 'def echo(state: State):', ' last = state["messages"][-1]', ' return {"messages": [{"role": "assistant", "content": f"echo: {last.content}"}]}', '', 'g = StateGraph(State)', 'g.add_node("echo", echo)', 'g.add_edge(START, "echo")', 'g.add_edge("echo", END)', 'app = g.compile()', '', 'print(app.invoke({"messages": [{"role": "user", "content": "hi"}]}))', ].join('\n'), highlightLines: [8, 12, 13], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/#reducers', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 6 -- State: operator.add // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'STATE / 3', title: 'operator.add -- аккумуляция для list-канала', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'import operator', 'from typing import Annotated', 'from typing_extensions import TypedDict', 'from langgraph.graph import StateGraph, START, END', '', 'class State(TypedDict):', ' # каждый узел возвращает кусок списка, operator.add склеивает', ' log: Annotated[list[str], operator.add]', ' tokens: Annotated[int, operator.add]', '', 'def step(state: State):', ' return {"log": ["step-1"], "tokens": 12}', '', 'def another(state: State):', ' return {"log": ["step-2"], "tokens": 7}', '', 'g = StateGraph(State)', 'g.add_node("a", step)', 'g.add_node("b", another)', 'g.add_edge(START, "a")', 'g.add_edge("a", "b")', 'g.add_edge("b", END)', 'app = g.compile()', 'print(app.invoke({"log": [], "tokens": 0}))', '# -> {\'log\': [\'step-1\', \'step-2\'], \'tokens\': 19}', ].join('\n'), highlightLines: [9, 10], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/#reducers', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 7 -- State: dataclass + LastValue // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'STATE / 4', title: 'dataclass + LastValue -- когда хочется типизации', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from dataclasses import dataclass, field', 'from typing import Annotated', 'from langgraph.graph import StateGraph, START, END', 'from langgraph.graph.message import add_messages', '', '@dataclass', 'class State:', ' question: str = ""', ' # dataclass + Annotated -- каналы работают точно так же', ' messages: Annotated[list, add_messages] = field(default_factory=list)', ' approved: bool = False', '', 'def greet(state: State):', ' return {"messages": [{"role": "assistant", "content": f"hi, {state.question}"}]}', '', 'g = StateGraph(State)', 'g.add_node("greet", greet)', 'g.add_edge(START, "greet")', 'g.add_edge("greet", END)', 'app = g.compile()', 'print(app.invoke(State(question="alex")))', ].join('\n'), highlightLines: [9, 10], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/#dataclass', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 8 -- StateGraph: builder.compile() минимальный граф // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'STATEGRAPH / 1', title: 'StateGraph: builder.compile() -- базовый граф', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from typing_extensions import TypedDict', 'from langgraph.graph import StateGraph, START, END', '', 'class State(TypedDict):', ' n: int', '', 'def inc(state: State):', ' return {"n": state["n"] + 1}', '', 'builder = StateGraph(State)', 'builder.add_node("inc", inc) # регистрируем узел', 'builder.add_edge(START, "inc") # поток входа', 'builder.add_edge("inc", END) # поток выхода', '', 'graph = builder.compile() # компиляция -- граф готов к invoke', '', 'print(graph.invoke({"n": 0})) # -> {\'n\': 1}', ].join('\n'), highlightLines: [12, 13, 14, 17, 19], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/#stategraph', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 9 -- START, END and data flow // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'STATEGRAPH / 2', title: 'START, END и поток данных через рёбра', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ '# START и END -- это специальные sentinel-узлы, не callable', 'from langgraph.graph import StateGraph, START, END', '', 'class State(TypedDict):', ' text: str', '', 'def upper(state: State):', ' return {"text": state["text"].upper()}', '', 'def exclaim(state: State):', ' return {"text": state["text"] + "!"}', '', 'g = StateGraph(State)', 'g.add_node("upper", upper)', 'g.add_node("exclaim", exclaim)', 'g.add_edge(START, "upper") # вход в граф', 'g.add_edge("upper", "exclaim") # внутреннее ребро', 'g.add_edge("exclaim", END) # выход из графа', '', 'app = g.compile()', 'print(app.invoke({"text": "hi"})) # -> {\'text\': \'HI!\'}', ].join('\n'), highlightLines: [3, 14, 16, 17], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/#why-langgraph', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 10 -- Nodes: sync/async // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'NODES / 1', title: 'Узлы: синхронные и асинхронные функции', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'import asyncio', 'from typing_extensions import TypedDict', 'from langgraph.graph import StateGraph, START, END', '', 'class State(TypedDict):', ' out: str', '', 'def sync_node(state: State): # обычная функция', ' return {"out": "sync-ok"}', '', 'async def async_node(state: State): # async тоже работает', ' await asyncio.sleep(0)', ' return {"out": "async-ok"}', '', 'g = StateGraph(State)', 'g.add_node("s", sync_node)', 'g.add_node("a", async_node)', 'g.add_edge(START, "s")', 'g.add_edge("s", "a")', 'g.add_edge("a", END)', 'app = g.compile()', '', 'print(app.invoke({"out": ""}))', 'print(asyncio.run(app.ainvoke({"out": ""})))', ].join('\n'), highlightLines: [11, 13], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/#nodes', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 11 -- Command: explicit goto // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'NODES / 2', title: 'Command -- узел сам решает, куда идти дальше', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from langgraph.graph import StateGraph, START, END', 'from langgraph.types import Command', 'from typing_extensions import TypedDict', 'from typing import Literal', 'class State(TypedDict):', ' n: int', 'def decide(state: State) -> Command[Literal["inc", "halt"]]:', ' # Command(update, goto) -- обновляет state и сам выбирает узел', ' if state["n"] < 3:', ' return Command(update={"n": state["n"] + 1}, goto="inc")', ' return Command(update={}, goto="halt")', 'g = StateGraph(State)', 'g.add_node("decide", decide)', 'g.add_node("inc", inc)', 'g.add_node("halt", lambda s: s)', 'g.add_edge(START, "decide")', 'g.add_edge("inc", "decide")', 'g.add_edge("halt", END)', 'app = g.compile()', 'print(app.invoke({"n": 0}))', ].join('\n'), highlightLines: [7, 8, 10], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/#command', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 12 -- Conditional edges // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'ROUTING / 1', title: 'Conditional edges: routing по содержимому', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from typing_extensions import TypedDict', 'from langgraph.graph import StateGraph, START, END', '', 'class State(TypedDict):', ' needs_tool: bool', ' answer: str', '', 'def agent(state: State) -> dict:', ' return {"answer": "model-output"}', '', 'def tool_node(state: State) -> dict:', ' return {"answer": "tool-output"}', '', 'def route(state: State) -> str:', ' # возвращаем ключ, который есть в path_map ниже', ' return "tool_node" if state["needs_tool"] else END', '', 'g = StateGraph(State)', 'g.add_node("agent", agent)', 'g.add_node("tool_node", tool_node)', 'g.add_edge(START, "agent")', 'g.add_conditional_edges("agent", route, {', ' "tool_node": "tool_node",', ' END: END,', '})', 'g.add_edge("tool_node", END)', 'app = g.compile()', 'print(app.invoke({"needs_tool": True, "answer": ""}))', ].join('\n'), highlightLines: [19, 20, 21, 22, 23], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/#conditional-edges', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 13 -- Cycles // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'ROUTING / 2', title: 'Циклы: agentic loop без рекурсии в коде', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ '# агентный цикл: agent <-> tools, выход когда done=true', 'from typing_extensions import TypedDict', 'from langgraph.graph import StateGraph, START, END', '', 'class State(TypedDict):', ' done: bool', ' iter: int', '', 'def agent(state: State) -> dict:', ' return {"iter": state["iter"] + 1, "done": state["iter"] >= 3}', '', 'def maybe_continue(state: State) -> str:', ' return "agent" if not state["done"] else END', '', 'g = StateGraph(State)', 'g.add_node("agent", agent)', 'g.add_edge(START, "agent")', 'g.add_conditional_edges("agent", maybe_continue, {"agent": "agent", END: END})', 'app = g.compile()', 'print(app.invoke({"done": False, "iter": 0}))', '# agent крутится 3 раза, потом уходит в END', ].join('\n'), highlightLines: [15, 16], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/low_level/#cycles', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 14 -- Persistence: InMemorySaver + thread_id // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'PERSISTENCE / 1', title: 'InMemorySaver + thread_id: stateful сессии', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from typing import Annotated', 'from typing_extensions import TypedDict', 'from langgraph.graph import StateGraph, START, END', 'from langgraph.graph.message import add_messages', 'from langgraph.checkpoint.memory import InMemorySaver', '', 'class State(TypedDict):', ' messages: Annotated[list, add_messages]', '', 'def echo(state: State):', ' last = state["messages"][-1]', ' return {"messages": [{"role": "assistant", "content": f"echo: {last.content}"}]}', '', 'g = StateGraph(State)', 'g.add_node("echo", echo)', 'g.add_edge(START, "echo")', 'g.add_edge("echo", END)', '', '# checkpointer -- обязателен для thread persistence', 'checkpointer = InMemorySaver()', 'app = g.compile(checkpointer=checkpointer)', '', 'cfg = {"configurable": {"thread_id": "user-1"}}', 'app.invoke({"messages": [{"role": "user", "content": "hi"}]}, cfg)', 'app.invoke({"messages": [{"role": "user", "content": "again"}]}, cfg)', '# второй вызов видит все сообщения первого: thread persistence работает', ].join('\n'), highlightLines: [22, 23, 25, 26], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/persistence/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 15 -- SqliteSaver / PostgresSaver // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'PERSISTENCE / 2', title: 'SqliteSaver и PostgresSaver для production', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 3.65, code: [ '# SQLite -- файл на диске, идеален для dev / small-prod', 'from langgraph.checkpoint.sqlite import SqliteSaver', '', 'with SqliteSaver.from_conn_string("./checkpoints.db") as cp:', ' app = g.compile(checkpointer=cp)', ' cfg = {"configurable": {"thread_id": "u1"}}', ' app.invoke({"messages": []}, cfg)', '', '# данные переживают рестарт процесса.', '# Для asyncio-варианта -- aiosqlite.', ].join('\n'), highlightLines: [1, 3, 4, 5], }); addCodeBlock(s, pres, theme, { x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 3.65, code: [ '# Postgres -- production-grade, multi-instance', 'from langgraph.checkpoint.postgres import PostgresSaver', '', 'DB = "postgresql://user:pass@host:5432/lg"', 'with PostgresSaver.from_conn_string(DB) as cp:', ' # первый запуск создаст schema', ' cp.setup()', ' app = g.compile(checkpointer=cp)', ' cfg = {"configurable": {"thread_id": "u1"}}', ' app.invoke({"messages": []}, cfg)', ].join('\n'), highlightLines: [1, 3, 6], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/persistence/#checkpointer-implementations', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 16 -- StateSnapshot // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'PERSISTENCE / 3', title: 'StateSnapshot -- что лежит в checkpoint', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ '# После invoke можно достать текущий snapshot:', 'snapshot = app.get_state(cfg)', '', '# snapshot -- это StateSnapshot с полями:', '# .config -- thread_id + checkpoint_id', '# .metadata -- step, source, writes', '# .values -- текущие значения всех каналов state', '# .next -- tuple узлов, которые будут выполняться следующими', '# .tasks -- PregelTask с pending/result/error', '', 'print(snapshot.next) # () -- граф завершён', 'print(snapshot.values["messages"][-1].content)', ].join('\n'), highlightLines: [2, 5, 6, 7, 8, 9], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/persistence/#state-snapshot', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 17 -- Time travel: get_state_history // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'TIME TRAVEL / 1', title: 'get_state_history: вся история шагов', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ '# Каждый super-step -- отдельный checkpoint в thread', 'history = list(app.get_state_history(cfg))', '', '# history[0] -- последний шаг (самый свежий)', '# history[-1] -- самый первый (начало сессии)', '', 'for i, snap in enumerate(history):', ' print(i, snap.metadata.get("step"), snap.values.get("iter"))', '', '# replays = форк от любого прошлого snapshot:', 'old = history[2].config', 'app.invoke(None, old) # переигрывает только следующие шаги', ].join('\n'), highlightLines: [2, 3, 4, 7, 10, 11], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/persistence/#time-travel', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 18 -- update_state: fork and replay // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'TIME TRAVEL / 2', title: 'update_state: форкнуть состояние и пойти другой веткой', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ '# patch значения канала прямо в snapshot -- создаётся новый checkpoint', 'app.update_state(', ' cfg,', ' values={"messages": [{"role": "user", "content": "rewind"}]},', ' as_node="user_input", # от имени какого узла пишем', ')', '', '# Дальше invoke(None, cfg) переигрывает граф с нового состояния', 'app.invoke(None, cfg)', '', '# Типичный приём: "что если пользователь сказал не X, а Y?" --', '# ответвляемся, смотрим альтернативный прогон без потери истории.', ].join('\n'), highlightLines: [2, 3, 4, 5, 10, 11], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/persistence/#update-state', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 19 -- HITL: interrupt + Command(resume) // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'HITL / 1', title: 'interrupt + Command(resume=): пауза на человеке', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from langgraph.types import interrupt, Command', 'from langgraph.graph import StateGraph, START, END', 'from langgraph.checkpoint.memory import InMemorySaver', 'from typing_extensions import TypedDict', 'class State(TypedDict):', ' question: str', ' approved: bool', 'def ask(state: State):', ' # interrupt() -- пауза. Возвращает значение из Command(resume=...)', ' answer = interrupt({"question": "Approve sending this message?"})', ' return {"approved": answer == "yes"}', 'g = StateGraph(State)', 'g.add_node("ask", ask)', 'g.add_edge(START, "ask")', 'g.add_edge("ask", END)', 'app = g.compile(checkpointer=InMemorySaver())', 'cfg = {"configurable": {"thread_id": "approval-1"}}', 'app.invoke({"question": "send email", "approved": False}, cfg) # пауза', 'result = app.invoke(Command(resume="yes"), cfg) # resume', 'print(result["approved"]) # -> True', ].join('\n'), highlightLines: [10, 17, 18, 19], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/human_in_the_loop/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 20 -- HITL: graph.invoke with config + pause // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'HITL / 2', title: 'Как выглядит HITL-цикл снаружи', }); addCallout(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 3.5, kind: 'info', title: 'Серверная сторона', text: '1. graph.invoke(input, cfg)\n' + '2. Внутри узла вызван interrupt(payload)\n' + '3. Граф замораживается, состояние в checkpointer\n' + '4. Возвращается GraphInterrupt с payload\n' + '5. Сервер ждёт -- пользователь думает часы/дни', }); addCallout(s, pres, theme, { x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 3.5, kind: 'success', title: 'Клиентская сторона', text: '1. UI получает payload, рисует форму\n' + '2. Пользователь жмёт Approve / Reject\n' + '3. UI делает POST /threads/{id}/resume\n' + '4. Сервер вызывает graph.invoke(Command(resume=answer), cfg)\n' + '5. Граф оживает с того же узла', }); addSourceLine(s, pres, theme, { source: 'blog.langchain.com/langchain-langgraph-1dot0 (HITL section)', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 21 -- HITL: multi-turn approval cycle (review-node only) // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'HITL / 3', title: 'Multi-turn approval: узел review', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ '# Узел review -- маршрутизация по ответу человека', 'from langgraph.types import interrupt, Command', 'from typing_extensions import TypedDict', 'class State(TypedDict):', ' plan: str', ' executed: bool', 'def plan(state: State):', ' return {"plan": "step-A; step-B; step-C"}', 'def review(state: State) -> Command:', ' # interrupt() возвращает ответ из Command(resume=...)', ' decision = interrupt({"plan": state["plan"]})', ' if decision == "approve":', ' return Command(goto="execute")', ' if decision == "abort":', ' return Command(goto=END)', ' return Command(goto="plan") # перепланировать', 'def execute(state: State):', ' return {"executed": True}', ].join('\n'), highlightLines: [9, 10, 11, 12, 13, 14, 15], }); addCallout(s, pres, theme, { x: 0.5, y: 4.0, w: 9.0, h: 0.65, kind: 'info', text: 'Полный пример со сборкой графа -- на следующем слайде.', }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/cycle/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 21b -- HITL: full graph assembly for multi-turn approval // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'HITL / 4', title: 'Multi-turn approval: сборка графа', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from langgraph.graph import StateGraph, START, END', 'from langgraph.checkpoint.memory import InMemorySaver', '# plan, review, execute -- из предыдущего слайда', 'g = StateGraph(State)', 'g.add_node("plan", plan)', 'g.add_node("review", review)', 'g.add_node("execute", execute)', 'g.add_edge(START, "plan")', 'g.add_edge("plan", "review")', 'g.add_edge("execute", END)', 'app = g.compile(checkpointer=InMemorySaver())', '', '# Цикл: каждый review -- это пауза; Command(goto=...) -- ответвление', '# plan -> review -> (approve: execute | abort: END | else: plan)', 'cfg = {"configurable": {"thread_id": "u1"}}', 'app.invoke({"plan": "", "executed": False}, cfg) # пауза 1', 'app.invoke(Command(resume="approve"), cfg) # resume -> execute', ].join('\n'), highlightLines: [5, 6, 8, 14, 15], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/cycle/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 22 -- Subgraphs // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'SUBGRAPHS', title: 'Subgraphs: композитность и изоляция state', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from typing_extensions import TypedDict', 'from langgraph.graph import StateGraph, START, END', '', 'class SubState(TypedDict):', ' internal: str', '', 'def inner(state: SubState):', ' return {"internal": "sub-output"}', '', '# Собираем subgraph -- у него свой state', 'sub = StateGraph(SubState)', 'sub.add_node("inner", inner)', 'sub.add_edge(START, "inner")', 'sub.add_edge("inner", END)', 'sub_compiled = sub.compile()', '', '# Вставляем как обычный узел в родительский граф', 'class ParentState(TypedDict):', ' out: str', '', 'parent = StateGraph(ParentState)', 'parent.add_node("sub_block", sub_compiled) # <- subgraph целиком', 'parent.add_edge(START, "sub_block")', 'parent.add_edge("sub_block", END)', 'app = parent.compile()', 'print(app.invoke({"out": ""}))', ].join('\n'), highlightLines: [11, 12, 13, 14, 21, 22], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/subgraphs/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 23 -- Streaming: 5 modes // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'STREAMING / 1', title: 'Пять режимов stream_mode', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'cfg = {"configurable": {"thread_id": "u1"}}', '# values -- весь state после каждого super-step', 'for snap in app.stream({"messages": []}, cfg, stream_mode="values"):', ' print(snap["messages"][-1].content)', '# updates -- delta: только что вернул каждый узел', 'for upd in app.stream({"messages": []}, cfg, stream_mode="updates"):', ' print(upd)', '# events -- низкоуровневые события (start, end, error, interrupt)', 'for ev in app.stream({"messages": []}, cfg, stream_mode="events"):', ' print(ev["event"], ev["name"])', '# messages -- токены LLM по мере генерации', 'for tok, meta in app.stream({"messages": []}, cfg, stream_mode="messages"):', ' print(tok.content, end="|")', '# custom -- только то, что узлы пишут через get_stream_writer()', 'for chunk in app.stream({"messages": []}, cfg, stream_mode="custom"):', ' print(chunk)', ].join('\n'), highlightLines: [3, 6, 9, 12, 15], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/streaming/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 24 -- Custom streaming with get_stream_writer // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'STREAMING / 2', title: 'Custom stream: пишем из узла как хотим', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from langgraph.graph import StateGraph, START, END', 'from langgraph.config import get_stream_writer', 'from typing_extensions import TypedDict', '', 'class State(TypedDict):', ' total: int', '', 'def progress(state: State):', ' writer = get_stream_writer() # доступен только во время выполнения узла', ' for i in range(3):', ' writer({"progress": i, "phase": "thinking"})', ' return {"total": 3}', '', 'g = StateGraph(State)', 'g.add_node("progress", progress)', 'g.add_edge(START, "progress")', 'g.add_edge("progress", END)', 'app = g.compile()', '', '# в UI -- только custom-чанки, без промежуточного state', 'for chunk in app.stream({"total": 0}, stream_mode="custom"):', ' print(chunk)', ].join('\n'), highlightLines: [10, 11, 12, 19, 20], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/streaming/#custom', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 25 -- Tool calling: tools + LLM binding // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'TOOL CALLING / 1', title: 'Tool calling: tools + LLM binding', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from langchain_openai import ChatOpenAI', 'from langchain.tools import tool', 'from langgraph.graph.message import add_messages', 'from typing import Annotated', 'from typing_extensions import TypedDict', '@tool', 'def add(a: int, b: int) -> int:', ' "Add two numbers."', ' return a + b', 'tools = [add]', 'llm = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools)', 'class State(TypedDict):', ' messages: Annotated[list, add_messages]', 'def agent(state: State):', ' return {"messages": [llm.invoke(state["messages"])]}', 'def route(state: State) -> str:', ' last = state["messages"][-1]', ' return "tools" if getattr(last, "tool_calls", None) else END', ].join('\n'), highlightLines: [11, 17], }); addCallout(s, pres, theme, { x: 0.5, y: 4.0, w: 9.0, h: 0.65, kind: 'info', text: 'Сборка графа и запуск -- на следующем слайде.', }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/how-tos/tool-calling/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 25b -- Tool calling: graph assembly + run // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'TOOL CALLING / 2', title: 'Tool calling: сборка графа и запуск', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'from langgraph.graph import StateGraph, START, END', 'from langgraph.prebuilt import ToolNode', '# tools, llm, agent, route -- из предыдущего слайда', 'g = StateGraph(State)', 'g.add_node("agent", agent)', 'g.add_node("tools", ToolNode(tools))', 'g.add_edge(START, "agent")', 'g.add_conditional_edges("agent", route, {"tools": "tools", END: END})', 'g.add_edge("tools", "agent")', 'app = g.compile()', '', '# Цикл: agent решает -> tools исполняет -> agent снова читает результат', 'result = app.invoke({', ' "messages": [{"role": "user", "content": "What is 2 + 3?"}]', '})', 'print(result["messages"][-1].content)', ].join('\n'), highlightLines: [6, 10, 13, 14], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/how-tos/tool-calling/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 26 -- LangGraph Studio // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'STUDIO', title: 'LangGraph Studio: визуальный debugger', }); addCallout(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 3.0, kind: 'info', title: 'Что это', text: 'Desktop IDE и web-UI для отладки графов. ' + 'Показывает граф, каждый super-step, ' + 'state на каждом шаге, время на узел.', }); addCallout(s, pres, theme, { x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 3.0, kind: 'success', title: 'Что умеет', text: '- запускать граф интерактивно\n' + '- ставить breakpoints на узлах\n' + '- модифицировать state вручную\n' + '- редактировать узлы и перезапускать\n' + '- экспорт trace в LangSmith', }); addCodeBlock(s, pres, theme, { x: 0.5, y: 4.2, w: 9.0, h: 0.8, code: [ '# запуск: langgraph dev -- поднимает Studio на http://localhost:8123', ].join('\n'), }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/langgraph_studio/', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 27 -- Deploy через LangGraph Platform // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'PLATFORM', title: 'Deploy через LangGraph Platform', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 1.3, code: [ '# langgraph.json -- declarative config', '{', ' "graphs": {"agent": "./agent.py:graph"},', ' "env": "./.env",', ' "python_version": "3.11"', '}', ].join('\n'), highlightLines: [2], }); addCodeBlock(s, pres, theme, { x: 0.5, y: 2.8, w: 9.0, h: 2.0, code: [ '# CLI: локальный dev-сервер и deploy', 'langgraph dev # локальный API + Studio', 'langgraph up # Docker-compose stack (Redis + API + Studio)', 'langgraph deploy # пуш в LangGraph Platform (managed)', ].join('\n'), highlightLines: [3], }); addSourceLine(s, pres, theme, { source: 'langchain-ai.github.io/langgraph/concepts/langgraph_platform/ (managed: scaling, queue, persistent threads, observability)', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 28 -- What's new in 1.0 // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'NEW IN 1.0', title: 'Что нового в LangGraph 1.0: четыре фичи', }); addCallout(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 1.55, kind: 'success', title: 'Durable execution', text: 'Состояние персистится автоматически. Падение сервера посреди long-run -- ' + 'граф восстанавливается ровно с точки остановки.', }); addCallout(s, pres, theme, { x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 1.55, kind: 'success', title: 'HITL first-class', text: 'interrupt() -- стабильный API, multi-day approval workflows без костылей.', }); addCallout(s, pres, theme, { x: 0.5, y: 3.1, w: 4.5, h: 1.5, kind: 'info', title: 'Built-in persistence', text: 'InMemorySaver / SqliteSaver / PostgresSaver -- first-class контракты, ' + 'а не отдельный набор фич.', }); addCallout(s, pres, theme, { x: 5.2, y: 3.1, w: 4.3, h: 1.5, kind: 'warning', title: 'Breaking changes', text: '- langgraph.prebuilt.create_react_agent -> langchain.agents.create_agent\n' + '- MemorySaver -> InMemorySaver (новый alias)\n' + '- semver: стабильно до 2.0', }); addSourceLine(s, pres, theme, { source: 'changelog.langchain.com/announcements/langgraph-1-0-is-now-generally-available (bonus 1.2: fault tolerance middleware -- retries / timeouts / error handlers)', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 29 -- TypeScript analogue // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'TYPESCRIPT', title: '@langchain/langgraph -- JS/TS-аналог', }); addCodeBlock(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.65, code: [ 'import { StateGraph, START, END, Annotation } from "@langchain/langgraph";', 'import { MemorySaver } from "@langchain/langgraph-checkpoint";', '', 'const State = Annotation.Root({', ' messages: Annotation({', ' reducer: (a, b) => a.concat(b),', ' default: () => [],', ' }),', '});', '', 'const g = new StateGraph(State)', ' .addNode("echo", (s) => ({', ' messages: [{ role: "assistant", content: "echo: " + s.messages.at(-1).content }],', ' }))', ' .addEdge(START, "echo")', ' .addEdge("echo", END);', '', 'const app = g.compile({ checkpointer: new MemorySaver() });', 'const cfg = { configurable: { thread_id: "t1" } };', 'const result = await app.invoke({ messages: [{ role: "user", content: "hi" }] }, cfg);', ].join('\n'), highlightLines: [1, 2, 19, 21], }); addSourceLine(s, pres, theme, { source: 'github.com/langchain-ai/langgraphjs', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 30 -- pros/cons: LangGraph vs LCEL // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'WHEN TO USE', title: 'Когда LangGraph, когда остаться на LCEL', }); addProsCons(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5, pros: [ 'Агент крутится в цикле (tool calls + retry)', 'Нужна пауза на human approval / review', 'Long-running workflow переживает рестарт', 'Сложная топология: ветвления, merge, map-reduce', 'Multi-agent: subgraphs + Send', 'Time-travel и replay для отладки', ], cons: [ 'Простой pipeline prompt | model | parser', 'Один проход без state между вызовами', 'Read-only чат без persistence', 'Быстрый прототип без долгоживущего state', 'Команда не готова к concepts: channels/reducers/Send', ], }); addSourceLine(s, pres, theme, { source: 'blog.langchain.com/langchain-langgraph-1dot0', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Slide 31 -- Bridge to Deep Agents // --------------------------------------------------------------------------- { const s = pres.addSlide(); slideBase(s, pres, theme); addHeader(s, pres, theme, { section: SECTION_LABEL, sectionNumber: SECTION_NUMBER, eyebrow: 'BRIDGE TO SECTION 3', title: 'Мостик к Deep Agents', }); addCallout(s, pres, theme, { x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 1.8, kind: 'info', title: 'Что мы только что разобрали', text: '- State, nodes, edges, persistence\n' + '- HITL через interrupt + Command(resume)\n' + '- Subgraphs, streaming, ToolNode\n' + '- Deploy через LangGraph Platform', }); addCallout(s, pres, theme, { x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 1.8, kind: 'success', title: 'Что дальше (Section 3)', text: 'Deep Agents -- это готовая архитектура поверх LangGraph: ' + 'planning tool, filesystem, subagents, middleware. ' + 'create_deep_agent -- одна функция вместо сотни строк boilerplate.', }); addCallout(s, pres, theme, { x: 0.5, y: 3.4, w: 9.0, h: 1.3, kind: 'warning', title: 'Связь', text: 'create_deep_agent из deepagents==0.6.11 -- это обёртка, которая собирает граф LangGraph ' + 'с planning tool, файловой системой и subagents. Внутри всё, что мы видели: ' + 'StateGraph, Command, interrupt, subgraphs.', }); addSourceLine(s, pres, theme, { source: 'github.com/langchain-ai/deepagents (README)', }); addPageNumber(s, pres, theme, next()); } // --------------------------------------------------------------------------- // Build // --------------------------------------------------------------------------- const outFile = path.join(__dirname, 'section2.pptx'); pres.writeFile({ fileName: outFile }).then(function (file) { console.log('Wrote: ' + file); }).catch(function (err) { console.error('ERROR:', err); process.exit(1); });