diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22c1010 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Build artifacts +build/ +*.log +.DS_Store + +# Heavy assets — kept under output/ but gitignored for size +output/previews/*.png + +# Node +node_modules/ + +# Python +__pycache__/ +*.pyc diff --git a/README.md b/README.md deleted file mode 100644 index 24eedba..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# langchain-evolution-deck - -Большой tutorial-дек по эволюции LangChain: от chains 2022 до Open SWE 2025/2026. 132 слайда в dark mode, Python 1.0+. \ No newline at end of file diff --git a/design-system.js b/design-system.js new file mode 100644 index 0000000..bb5e5e6 --- /dev/null +++ b/design-system.js @@ -0,0 +1,862 @@ +/** + * design-system.js + * ---------------------------------------------------------------------------- + * Design tokens + slide helpers for the LangChain Evolution deck + * (lc-evo-deck, 100+ slides, code-heavy, dark theme, 16:9). + * + * Audience: engineers familiar with LLMs. No introductory filler. + * Visual register: developer-tool aesthetic, deep navy, JetBrains Mono for + * code, Inter for UI, Arial as the universal fallback (covers Cyrillic). + * + * USAGE + * ----- + * // 1. Import the module + * const ds = require('./design-system'); + * const { theme, helpers, layouts, fonts, sizes, spacing } = ds; + * + * // 2. Create a new presentation with the built-in 16:9 layout + * const pptxgen = require('pptxgenjs'); + * const pres = new pptxgen(); + * pres.layout = 'LAYOUT_16x9'; // 10 x 5.625 inches + * + * // 3. Add a slide + * const slide = pres.addSlide(); + * helpers.slideBase(slide, pres, theme); + * helpers.addHeader(slide, pres, theme, { + * section: 'Stage 2: Chains', + * sectionNumber: 2, + * title: 'LCEL: a composable expression language', + * eyebrow: 'STAGE 2', + * }); + * + * // 4. Add a code block + * helpers.addCodeBlock(slide, pres, theme, { + * x: 0.5, y: layouts.CONTENT_TOP, + * w: 5.0, h: 3.2, + * language: 'python', + * code: [ + * 'from langchain_core.prompts import ChatPromptTemplate', + * 'from langchain_openai import ChatOpenAI', + * '', + * 'prompt = ChatPromptTemplate.from_messages([', + * ' ("system", "You are a helpful assistant."),', + * ' ("human", "{question}"),', + * '])', + * 'model = ChatOpenAI(model="gpt-4o-mini")', + * 'chain = prompt | model', + * 'print(chain.invoke({"question": "What is LCEL?"}))', + * ].join('\n'), + * filePath: 'examples/lcel_basic.py', + * startLine: 1, + * }); + * + * // 5. Add a callout and a pros/cons panel + * helpers.addCallout(slide, pres, theme, { + * x: 5.8, y: layouts.CONTENT_TOP, w: 3.7, h: 1.2, + * kind: 'info', + * text: 'LCEL is the default composition language from v0.1 onward.', + * }); + * + * helpers.addProsCons(slide, pres, theme, { + * x: 5.8, y: 2.8, w: 3.7, h: 2.0, + * pros: ['Composable via | operator', 'Streaming, async, batched for free'], + * cons: ['Verbose for simple chains', 'Mental model differs from LangChain v0'], + * }); + * + * // 6. Number the slide and add a source line + * helpers.addPageNumber(slide, pres, theme, 7); + * helpers.addSourceLine(slide, pres, theme, { + * x: 0.5, y: layouts.FOOTER_Y + 0.1, w: 6.0, + * source: 'python.langchain.com/docs/concepts/lcel', + * }); + * + * // 7. Save + * await pres.writeFile({ fileName: 'lc-evolution.pptx' }); + * + * LAYOUTS (PPTX 16:9, units = inches) + * ----------------------------------- + * HEADER_Y = 0.4 + * CONTENT_TOP = 1.4 + * CONTENT_BOTTOM = 5.05 + * FOOTER_Y = 5.25 + * + * Vertical regions: + * - Header band : [0.0 .. 1.4] eyebrow + h1 title + * - Content body : [1.4 .. 5.05] main slide content + * - Footer band : [5.05 .. 5.625] page number + source line + * + * Horizontal margins: 0.5 inches left/right by default. + * + * RULES + * ----- + * - No em-dash (--), en-dash (-), no smart quotes, no ellipsis (...) + * - All source files are pure ASCII except inside string literals where + * Cyrillic is allowed (Arial fallback guarantees rendering). + * - Code blocks use JetBrains Mono; body uses Inter; fallback Arial. + * + * @module design-system + */ + +'use strict'; + +// --------------------------------------------------------------------------- +// 1. PALETTE +// --------------------------------------------------------------------------- + +const palette = { + bg: { + primary: '#0A1A2A', // main slide background + elevated: '#142B3F', // cards, callouts, panels + code: '#0F1E2E', // code block background (slightly darker) + overlay: '#1B2F44', // hover / focus surfaces + }, + text: { + primary: '#E6F0F7', + secondary: '#B5C4D1', + muted: '#8A9AAB', + inverse: '#0A1A2A', // for text on bright accents + }, + accent: { + primary: '#219EBC', // teal, default accent + secondary: '#FFB703', // gold, important emphasis + tertiary: '#8ECAE6', // light blue, soft accent + }, + border: { + subtle: '#233A4F', + strong: '#3A5670', + accent: '#219EBC', + }, + code: { + keyword: '#C586C0', // def, class, import, return + string: '#CE9178', + number: '#B5CEA8', + comment: '#6A9955', + function: '#DCDCAA', + builtin: '#4EC9B0', + text: '#D4D4D4', // default code body + bg: '#0F1E2E', + lineHighlight: '#1F2F44', + }, + state: { + info: '#219EBC', + success: '#4EC9B0', + warning: '#FFB703', + danger: '#F48771', + infoBg: '#102A38', + successBg: '#0F2A28', + warningBg: '#3A2A0F', + dangerBg: '#3A1A14', + }, +}; + +// --------------------------------------------------------------------------- +// 2. TYPOGRAPHY +// --------------------------------------------------------------------------- + +const fonts = { + code: 'JetBrains Mono', + ui: 'Inter', + fallback: 'Arial', // universal fallback, supports Cyrillic +}; + +const sizes = { + h1: 36, + h2: 28, + h3: 20, + body: 14, + code: 10, + caption: 9, + eyebrow: 10, +}; + +const spacing = { + page: 0.5, + card_pad: 0.25, + gap: 0.15, +}; + +// --------------------------------------------------------------------------- +// 3. LAYOUTS +// --------------------------------------------------------------------------- + +const layouts = { + LAYOUT_16x9: 'LAYOUT_16x9', + HEADER_Y: 0.4, + CONTENT_TOP: 1.45, + CONTENT_BOTTOM: 5.05, + FOOTER_Y: 5.25, +}; + +// --------------------------------------------------------------------------- +// 4. THEME (bundled export of the above) +// --------------------------------------------------------------------------- + +const theme = { + palette, + fonts, + sizes, + spacing, + layouts, +}; + +// --------------------------------------------------------------------------- +// 5. HELPERS +// --------------------------------------------------------------------------- + +/** + * Resolve a font that always carries the Arial fallback. + * pptxgenjs lets us pass an array; PowerPoint will pick the first available. + */ +function withFallback(family) { + return [family, fonts.fallback]; +} + +/** + * Paint the slide background. + * @param {object} slide - pptxgenjs slide instance + * @param {object} _pres - pptxgenjs pres (kept for signature symmetry) + * @param {object} t - theme bundle + */ +function slideBase(slide, _pres, t) { + slide.background = { color: t.palette.bg.primary }; +} + +/** + * Add a header band: eyebrow (small caps, accent) + section number + title. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {string} [opts.section] - small caps section label (e.g. "Stage 2") + * @param {number} [opts.sectionNumber] - large section number shown on the right + * @param {string} opts.title - main title (h1) + * @param {string} [opts.eyebrow] - eyebrow text (e.g. "STAGE 2: CHAINS") + */ +function addHeader(slide, pres, t, opts) { + const o = opts || {}; + const margin = t.spacing.page; + const titleY = t.layouts.HEADER_Y + 0.4; + const titleSize = o.titleSize || t.sizes.h2; // default to 28pt -- fits two-line titles without overflow + const titleW = o.sectionNumber != null ? 8.4 : 9.0; + + if (o.eyebrow) { + slide.addText(o.eyebrow, { + x: margin, + y: t.layouts.HEADER_Y, + w: 6.0, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.accent.primary, + bold: true, + charSpacing: 4, + }); + } else if (o.section) { + slide.addText(o.section, { + x: margin, + y: t.layouts.HEADER_Y, + w: 6.0, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.accent.primary, + bold: true, + charSpacing: 4, + }); + } + + slide.addText(o.title || '', { + x: margin, + y: titleY, + w: titleW, + h: 0.75, + fontFace: withFallback(t.fonts.ui), + fontSize: titleSize, + color: t.palette.text.primary, + bold: true, + valign: 'middle', + fit: 'shrink', + }); + + if (o.sectionNumber != null) { + slide.addText(String(o.sectionNumber), { + x: 9.0, + y: t.layouts.HEADER_Y - 0.05, + w: 0.7, + h: 1.0, + fontFace: withFallback(t.fonts.ui), + fontSize: 56, + color: t.palette.accent.secondary, + bold: true, + align: 'right', + valign: 'top', + }); + } + + // Hairline separator below the header band + slide.addShape(pres.ShapeType.line, { + x: margin, + y: 1.3, + w: 10.0 - margin * 2, + h: 0, + line: { color: t.palette.border.subtle, width: 0.75 }, + }); +} + +/** + * Render a code block as a card with monospaced text. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {string} opts.code + * @param {number} opts.x + * @param {number} opts.y + * @param {number} opts.w + * @param {number} opts.h + * @param {string} [opts.language='python'] + * @param {string} [opts.filePath] - optional, renders as "// path:line" header + * @param {number} [opts.startLine=1] + * @param {Array} [opts.highlightLines] - 1-based line numbers + */ +function addCodeBlock(slide, pres, t, opts) { + const o = opts || {}; + const code = String(o.code || ''); + const x = o.x; + const y = o.y; + const w = o.w; + const h = o.h; + const headerH = o.filePath ? 0.3 : 0; + const radius = 0.08; + + // Card background + slide.addShape(pres.ShapeType.roundRect, { + x: x, + y: y, + w: w, + h: h, + fill: { color: t.palette.code.bg }, + line: { color: t.palette.border.subtle, width: 0.75 }, + rectRadius: radius, + }); + + // Optional header strip with file path + if (o.filePath) { + const startLine = o.startLine || 1; + const lineCount = code.split('\n').length; + const endLine = startLine + lineCount - 1; + slide.addText( + '// ' + o.filePath + ':' + startLine + '-' + endLine, + { + x: x + t.spacing.card_pad, + y: y + 0.05, + w: w - t.spacing.card_pad * 2, + h: 0.22, + fontFace: withFallback(t.fonts.code), + fontSize: t.sizes.caption, + color: t.palette.text.muted, + italic: true, + } + ); + } + + // Body code + const lines = code.split('\n'); + const hasHighlight = + Array.isArray(o.highlightLines) && o.highlightLines.length > 0; + const highlightedSet = hasHighlight + ? new Set(o.highlightLines.map(function (n) { return n; })) + : null; + + // Pre-compute line height to fit within available area. + const codeAreaY = y + headerH + 0.05; + const codeAreaH = h - headerH - 0.1; + // Use a tighter font + leading so 20-line snippets fit in 3-inch cards. + const fontSize = o.fontSize || 11; + const lineH = (fontSize / 72) * 1.25; // inches per line at 1.25 leading + + const text = lines.map(function (line, idx) { + const lineNum = idx + 1; + const isHi = highlightedSet && highlightedSet.has(lineNum); + const prefix = isHi ? '> ' : ' '; + return { + text: prefix + (line.length === 0 ? ' ' : line) + '\n', + options: { + fontFace: withFallback(t.fonts.code), + fontSize: fontSize, + color: isHi ? t.palette.text.primary : t.palette.code.text, + bold: false, + highlight: isHi ? t.palette.code.lineHighlight : undefined, + }, + }; + }); + + slide.addText(text, { + x: x + t.spacing.card_pad, + y: codeAreaY, + w: w - t.spacing.card_pad * 2, + h: codeAreaH, + valign: 'top', + paraSpaceBefore: 0, + paraSpaceAfter: 0, + lineSpacingMultiple: 1.0, + isTextBox: true, + }); + + // Caption warning if content may overflow. + const maxLines = Math.floor(codeAreaH / lineH); + if (lines.length > maxLines) { + // Best-effort: append a small note. PowerPoint won't clip the text, but + // it will overflow the card visually. Caller should reduce fontSize or + // split the snippet. + slide.addText( + '// note: snippet has ' + lines.length + ' lines, card fits ~' + maxLines, + { + x: x + t.spacing.card_pad, + y: y + h - 0.22, + w: w - t.spacing.card_pad * 2, + h: 0.18, + fontFace: withFallback(t.fonts.code), + fontSize: t.sizes.caption, + color: t.palette.state.warning, + italic: true, + } + ); + } +} + +/** + * Render a code block with line highlighting. + * Convenience wrapper over addCodeBlock. + */ +function addCodeBlockWithHighlight(slide, pres, t, opts) { + return addCodeBlock(slide, pres, t, Object.assign({}, opts, { + highlightLines: opts.lines || opts.highlightLines, + })); +} + +/** + * Add a callout panel (info / warning / success / danger). + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {number} opts.x + * @param {number} opts.y + * @param {number} opts.w + * @param {number} opts.h + * @param {('info'|'warning'|'success'|'danger')} [opts.kind='info'] + * @param {string} opts.text + * @param {string} [opts.title] + */ +function addCallout(slide, pres, t, opts) { + const o = opts || {}; + const kind = o.kind || 'info'; + const accent = t.palette.state[kind] || t.palette.state.info; + const bgKey = kind + 'Bg'; + const bg = t.palette.state[bgKey] || t.palette.state.infoBg; + + // Background card + slide.addShape(pres.ShapeType.roundRect, { + x: o.x, + y: o.y, + w: o.w, + h: o.h, + fill: { color: bg }, + line: { color: accent, width: 1 }, + rectRadius: 0.08, + }); + + // Left accent bar + slide.addShape(pres.ShapeType.rect, { + x: o.x, + y: o.y, + w: 0.08, + h: o.h, + fill: { color: accent }, + line: { type: 'none' }, + }); + + // Label + slide.addText((kind || 'info').toUpperCase(), { + x: o.x + 0.25, + y: o.y + 0.1, + w: o.w - 0.35, + h: 0.25, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: accent, + bold: true, + charSpacing: 4, + }); + + // Optional title + let bodyY = o.y + 0.4; + let bodyH = o.h - 0.5; + if (o.title) { + slide.addText(o.title, { + x: o.x + 0.25, + y: o.y + 0.35, + w: o.w - 0.35, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.h3, + color: t.palette.text.primary, + bold: true, + }); + bodyY = o.y + 0.7; + bodyH = o.h - 0.8; + } + + slide.addText(o.text || '', { + x: o.x + 0.25, + y: bodyY, + w: o.w - 0.35, + h: bodyH, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.secondary, + valign: 'top', + }); +} + +/** + * Two-column pros / cons panel. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {number} opts.x + * @param {number} opts.y + * @param {number} opts.w + * @param {number} opts.h + * @param {string[]} opts.pros + * @param {string[]} opts.cons + */ +function addProsCons(slide, pres, t, opts) { + const o = opts || {}; + const gap = 0.2; + const colW = (o.w - gap) / 2; + + // Pros card + slide.addShape(pres.ShapeType.roundRect, { + x: o.x, + y: o.y, + w: colW, + h: o.h, + fill: { color: t.palette.state.successBg }, + line: { color: t.palette.state.success, width: 1 }, + rectRadius: 0.08, + }); + slide.addText('PROS', { + x: o.x + 0.2, + y: o.y + 0.1, + w: colW - 0.3, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.state.success, + bold: true, + charSpacing: 4, + }); + + const prosBody = (o.pros || []).map(function (item) { + return { + text: '+ ' + item + '\n', + options: { + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.primary, + }, + }; + }); + slide.addText(prosBody, { + x: o.x + 0.2, + y: o.y + 0.45, + w: colW - 0.3, + h: o.h - 0.55, + valign: 'top', + paraSpaceAfter: 4, + }); + + // Cons card + const cx = o.x + colW + gap; + slide.addShape(pres.ShapeType.roundRect, { + x: cx, + y: o.y, + w: colW, + h: o.h, + fill: { color: t.palette.state.dangerBg }, + line: { color: t.palette.state.danger, width: 1 }, + rectRadius: 0.08, + }); + slide.addText('CONS', { + x: cx + 0.2, + y: o.y + 0.1, + w: colW - 0.3, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.state.danger, + bold: true, + charSpacing: 4, + }); + + const consBody = (o.cons || []).map(function (item) { + return { + text: '- ' + item + '\n', + options: { + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.primary, + }, + }; + }); + slide.addText(consBody, { + x: cx + 0.2, + y: o.y + 0.45, + w: colW - 0.3, + h: o.h - 0.55, + valign: 'top', + paraSpaceAfter: 4, + }); +} + +/** + * Add a page number in the bottom-right corner. + */ +function addPageNumber(slide, _pres, t, num) { + slide.addText(String(num), { + x: 9.2, + y: t.layouts.FOOTER_Y, + w: 0.6, + h: 0.25, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.caption, + color: t.palette.text.muted, + align: 'right', + valign: 'middle', + }); +} + +/** + * Add a section divider slide: a large numeric label + title + intro paragraph. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {number} opts.number + * @param {string} opts.title + * @param {string} [opts.intro] + * @param {string} [opts.eyebrow] + */ +function addSectionDivider(slide, pres, t, opts) { + const o = opts || {}; + + slideBase(slide, pres, t); + + // Giant number on the left + slide.addText(String(o.number), { + x: 0.5, + y: 1.0, + w: 3.5, + h: 3.5, + fontFace: withFallback(t.fonts.ui), + fontSize: 220, + color: t.palette.accent.primary, + bold: true, + valign: 'middle', + }); + + // Vertical divider + slide.addShape(pres.ShapeType.line, { + x: 4.2, + y: 1.4, + w: 0, + h: 2.8, + line: { color: t.palette.border.subtle, width: 1 }, + }); + + // Title + if (o.eyebrow) { + slide.addText(o.eyebrow, { + x: 4.5, + y: 1.4, + w: 5.0, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.accent.secondary, + bold: true, + charSpacing: 4, + }); + } + + slide.addText(o.title || '', { + x: 4.5, + y: o.eyebrow ? 1.7 : 1.4, + w: 5.0, + h: 1.0, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.h1, + color: t.palette.text.primary, + bold: true, + valign: 'top', + }); + + if (o.intro) { + slide.addText(o.intro, { + x: 4.5, + y: 2.8, + w: 5.0, + h: 1.6, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.secondary, + valign: 'top', + }); + } +} + +/** + * Add an italic source line, e.g. "source: python.langchain.com/..." + * @param {object} slide + * @param {object} _pres + * @param {object} t + * @param {object} opts + * @param {string} opts.source + * @param {number} [opts.x] + * @param {number} [opts.y] + * @param {number} [opts.w] + */ +function addSourceLine(slide, _pres, t, opts) { + const o = opts || {}; + slide.addText('source: ' + (o.source || ''), { + x: o.x != null ? o.x : t.spacing.page, + y: o.y != null ? o.y : t.layouts.FOOTER_Y + 0.05, + w: o.w != null ? o.w : 7.0, + h: 0.25, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.caption, + color: t.palette.text.muted, + italic: true, + valign: 'middle', + }); +} + +// --------------------------------------------------------------------------- +// 6. OPTIONAL: PYTHON SYNTAX HIGHLIGHTING (pygments via subprocess) +// --------------------------------------------------------------------------- + +/** + * Best-effort Python syntax highlighter. + * Calls the `pygmentize` CLI from pygments if available. + * Returns an array of { text, color } tokens. + * On any failure (missing binary, non-zero exit, parse error) returns a + * single-token array with the whole snippet in the default code text color + * so that the slide still renders something. + * + * @param {string} code - Python source code + * @returns {Array<{text: string, color: string}>} + */ +function highlightPython(code) { + const defaultColor = palette.code.text; + const text = String(code || ''); + if (!text) return [{ text: '', color: defaultColor }]; + + const { spawnSync } = require('child_process'); + + let result; + try { + result = spawnSync( + 'pygmentize', + ['-f', 'json', '-l', 'python'], + { input: text, encoding: 'utf8', timeout: 4000 } + ); + } catch (e) { + return [{ text: text, color: defaultColor }]; + } + + if (result.error || result.status !== 0) { + return [{ text: text, color: defaultColor }]; + } + + let parsed; + try { + parsed = JSON.parse(result.stdout); + } catch (e) { + return [{ text: text, color: defaultColor }]; + } + + // Pygments JSON output: { tokens: [ [type, value], ... ] } + const tokens = Array.isArray(parsed.tokens) ? parsed.tokens : []; + const out = []; + for (const tok of tokens) { + if (!Array.isArray(tok) || tok.length < 2) continue; + const [type, value] = tok; + out.push({ + text: String(value), + color: mapPygmentsTokenToColor(type) || defaultColor, + }); + } + return out.length > 0 ? out : [{ text: text, color: defaultColor }]; +} + +/** + * Map a pygments token type to a palette color. + * Falls back to the default code text color. + */ +function mapPygmentsTokenToColor(type) { + if (!type) return null; + // Pygments short token types we care about for Python: + // Keyword, Keyword.Namespace, Keyword.Constant, String, Number, + // Comment, Comment.Single, Name.Function, Name.Builtin, + // Operator, Punctuation, Text, Error + if (type === 'Keyword' || type.startsWith('Keyword.')) { + return palette.code.keyword; + } + if (type.startsWith('String')) { + return palette.code.string; + } + if (type === 'Number') { + return palette.code.number; + } + if (type.startsWith('Comment')) { + return palette.code.comment; + } + if (type === 'Name.Function' || type === 'Name.Function.Magic') { + return palette.code.function; + } + if (type === 'Name.Builtin' || type === 'Name.Builtin.Pseudo') { + return palette.code.builtin; + } + return null; +} + +// --------------------------------------------------------------------------- +// 7. EXPORTS +// --------------------------------------------------------------------------- + +const helpers = { + slideBase, + addHeader, + addCodeBlock, + addCodeBlockWithHighlight, + addCallout, + addProsCons, + addPageNumber, + addSectionDivider, + addSourceLine, + withFallback, + highlightPython, +}; + +module.exports = { + theme: theme, + palette: palette, + fonts: fonts, + sizes: sizes, + spacing: spacing, + layouts: layouts, + helpers: helpers, +}; \ No newline at end of file diff --git a/design-system.md b/design-system.md new file mode 100644 index 0000000..d592d8f --- /dev/null +++ b/design-system.md @@ -0,0 +1,149 @@ +# design-system.md + +Гайд по `lc-evo-deck/design-system.js` для тех, кто собирает слайды. + +## Что внутри + +`design-system.js` экспортирует единый объект с тремя слоями: + +| Слой | Что лежит | +| ---------- | --------------------------------------------------------------- | +| `palette` | Цвета: фон, текст, акценты, код, состояния | +| `fonts` | `code`, `ui`, `fallback` (Arial для кириллицы) | +| `sizes` | Шкала шрифтов: `h1` ... `caption`, `eyebrow` | +| `spacing` | Отступы: `page`, `card_pad`, `gap` | +| `layouts` | Константы 16:9: `HEADER_Y`, `CONTENT_TOP`, `CONTENT_BOTTOM`, `FOOTER_Y` | +| `theme` | Все перечисленное выше в одном объекте (удобно пробрасывать) | +| `helpers` | Готовые функции для сборки слайда | + +Импорт: + +```js +const ds = require('./design-system'); +const { theme, helpers, layouts } = ds; +``` + +## Геометрия слайда (16:9, дюймы) + +``` +y=0.00 +-----------------------------------------------+ + | HEADER_Y = 0.4 | + | eyebrow (10pt, accent) | +y=1.25 | --- hairline --- | + | CONTENT_TOP = 1.4 <-- начинай контент тут | + | | + | контент | + | | +y=5.05 | CONTENT_BOTTOM = 5.05 | + | FOOTER_Y = 5.25 <-- page number, source | +y=5.625 +-----------------------------------------------+ + x=0.5 x=9.5 + ^ отступ `spacing.page` (0.5 дюйма) с обеих сторон +``` + +## Минимальный слайд + +```js +const pptxgen = require('pptxgenjs'); +const ds = require('./design-system'); +const { theme, helpers, layouts } = ds; + +const pres = new pptxgen(); +pres.layout = 'LAYOUT_16x9'; + +const slide = pres.addSlide(); +helpers.slideBase(slide, pres, theme); +helpers.addHeader(slide, pres, theme, { + section: 'Stage 2: Chains', + sectionNumber: 2, + title: 'LCEL: composable expressions', + eyebrow: 'STAGE 2', +}); +helpers.addPageNumber(slide, pres, theme, 1); + +await pres.writeFile({ fileName: 'lc-evolution.pptx' }); +``` + +## Какой layout для какого случая + +| Слайд | Что использовать | +| ------------------------------ | --------------------------------------------------------- | +| Титул раздела / stage divider | `helpers.addSectionDivider` (большая цифра + заголовок) | +| Текст + код | `addHeader` + `addCodeBlock` слева, `addCallout` справа | +| Сравнение двух подходов | `addProsCons` (две колонки, + и -) | +| Цитата / важное замечание | `addCallout` с `kind: 'info' | 'warning' | 'success' | 'danger'` | +| Код с акцентом на строке | `addCodeBlockWithHighlight` + `lines: [3, 4]` | +| Источник внизу | `addSourceLine` | +| Любой слайд | `addPageNumber` в правом нижнем углу | + +## Helper-функции -- короткая справка + +### `slideBase(slide, pres, theme)` +Закрашивает фон `bg.primary`. Вызывай первым на каждом слайде. + +### `addHeader(slide, pres, theme, opts)` +- `opts.eyebrow` -- маленький caps-ярлык сверху (например `STAGE 2: CHAINS`) +- `opts.section` -- альтернатива eyebrow +- `opts.sectionNumber` -- крупная цифра справа (необязательно) +- `opts.title` -- h1 + +### `addCodeBlock(slide, pres, theme, opts)` +- `opts.code` -- строка кода (`\n` для переносов) +- `opts.filePath` + `opts.startLine` -- подпись `// path/to/file.py:1-12` +- `opts.highlightLines` -- массив 1-based номеров строк, которые подсветить +- Если строк больше, чем влезает по высоте, внизу появится желтая плашка + `// note: snippet has N lines, card fits ~M` -- уменьши `fontSize` или + разбей сниппет. + +### `addCodeBlockWithHighlight(slide, pres, theme, opts)` +То же самое, но принимает `lines: [3, 4]` как алиас для `highlightLines`. + +### `addCallout(slide, pres, theme, opts)` +- `opts.kind` -- `info` | `warning` | `success` | `danger` +- `opts.title` -- необязательный заголовок внутри плашки +- `opts.text` -- основной текст + +### `addProsCons(slide, pres, theme, opts)` +- `opts.pros` -- массив строк +- `opts.cons` -- массив строк +- Плюсы слева (зелёная рамка), минусы справа (красная). + +### `addPageNumber(slide, pres, theme, n)` +Правый нижний угол, монохромный caption. + +### `addSectionDivider(slide, pres, theme, opts)` +- `opts.number` -- крупная цифра слева +- `opts.title` -- заголовок справа +- `opts.eyebrow` -- необязательный caps-ярлык +- `opts.intro` -- абзац под заголовком + +### `addSourceLine(slide, pres, theme, opts)` +- `opts.source` -- URL или короткая ссылка +- По умолчанию `x=0.5, y=5.30, w=7.0` + +### `highlightPython(code)` (опционально) +Если на машине стоит `pygmentize` (из пакета `pygments`), функция вернет +массив `{ text, color }` токенов. Если бинарника нет -- вернется один токен +с дефолтным цветом и весь код отрисуется моноширинно. Используй, когда +нужна попроцедурная подсветка поверх `addCodeBlock`. + +## Правила + +1. Не вставляй em-dash (`--`) и en-dash (`-`) -- заменяй на `--` и `-`. +2. Не используй Unicode-кавычки -- только ASCII `"` и `'`. +3. Не используй `...` -- заменяй на `...`. +4. Шрифты -- всегда через `helpers.withFallback(name)`, чтобы Arial был + гарантированным фолбэком. +5. Цвета бери из `palette.*` -- не хардкодь hex в слайдах. +6. Геометрия -- через `layouts.*` константы. +7. Любой новый слайд начинается с `slideBase(...)`. + +## Частые ошибки + +| Симптом | Причина | Фикс | +| -------------------------------------- | ------------------------------------ | --------------------------------------------------- | +| Шрифт Arial вместо JetBrains Mono | PowerPoint не нашел шрифт | Установи `JetBrains Mono` в систему | +| Кириллица в коде рендерится квадратами | Нет фолбэка | Используй `helpers.withFallback(t.fonts.code)` | +| Код вылезает за карточку | Слишком много строк | Уменьши `sizes.code` или укороти сниппет | +| Header и контент перекрываются | Контент начинается выше `CONTENT_TOP`| Подними `y` до `layouts.CONTENT_TOP` | +| Плашка `note: snippet has N lines` | Сниппет не помещается | Разбей на 2 карточки или уменьши `sizes.code` | \ No newline at end of file diff --git a/final-compile.js b/final-compile.js new file mode 100644 index 0000000..0ccd297 --- /dev/null +++ b/final-compile.js @@ -0,0 +1,269 @@ +/** + * final-compile.js + * Создаёт: + * - build/intro.pptx (cover + TOC) + * - build/dividers.pptx (5 dividers между секциями) + * - build/recap.pptx (3 recap слайда: timeline, what's next, closing) + * Затем вызывает merge.js для склейки всех в один .pptx. + */ +'use strict'; +const path = require('path'); +const fs = require('fs'); +const ds = require('./design-system'); +const { theme, palette, fonts, sizes, layouts, helpers } = ds; +const pptxgen = require('pptxgenjs'); + +const WORKSPACE = __dirname; +const SLIDES = path.join(WORKSPACE, 'slides'); +const BUILD = path.join(WORKSPACE, 'build'); +fs.mkdirSync(BUILD, { recursive: true }); + +const SECTIONS = [ + { dir: 'section1-chains', title: 'LangChain 1.0', subtitle: 'chains, LCEL, agents, retrievers' }, + { dir: 'section2-langgraph', title: 'LangGraph 1.0', subtitle: 'state, nodes, persistence, HITL' }, + { dir: 'section3-deepagents', title: 'Deep Agents', subtitle: 'harness, todos, virtual FS, subagents' }, + { dir: 'section4-openswe', title: 'Open SWE', subtitle: 'async coding agent, triggers, dashboard' }, + { dir: 'section5-ecosystem', title: 'Экосистема', subtitle: 'LangSmith, Studio, deployment' }, +]; + +// -------- INTRO (cover + TOC) -------- +function buildIntro(pres) { + // Slide 1 -- cover + const s1 = pres.addSlide(); + helpers.slideBase(s1, pres, theme); + // top accent + s1.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.15, fill: { color: palette.accent.primary }, line: { type: 'none' } }); + helpers.addHeader(s1, pres, theme, { + eyebrow: 'DEEP DIVE / TUTORIAL', + sectionNumber: 0, + title: 'Эволюция LangChain', + }); + // subtitle + s1.addText('от chains до Deep Agents и Open SWE', { + x: 0.5, y: 1.85, w: 9, h: 0.5, + fontFace: fonts.ui, fontSize: 24, color: palette.text.secondary, bold: false, + }); + // gold bar + s1.addShape(pres.ShapeType.rect, { x: 0.5, y: 2.55, w: 1.5, h: 0.06, fill: { color: palette.accent.secondary }, line: { type: 'none' } }); + // description + s1.addText( + 'Большой tutorial по экосистеме LangChain: chains, LCEL, LangGraph, Deep Agents, Open SWE. ' + + 'Python 1.0+, реальные API, плотный код.', + { + x: 0.5, y: 2.85, w: 9, h: 1.5, + fontFace: fonts.ui, fontSize: 16, color: palette.text.secondary, + } + ); + // footer meta + s1.addText('100+ слайдов | 2022 -- 2026 | Python 1.0+', { + x: 0.5, y: 5.15, w: 9, h: 0.3, + fontFace: fonts.ui, fontSize: 11, color: palette.text.muted, align: 'center', + }); + helpers.addPageNumber(s1, pres, theme, 1); + + // Slide 2 -- TOC + const s2 = pres.addSlide(); + helpers.slideBase(s2, pres, theme); + helpers.addHeader(s2, pres, theme, { + eyebrow: 'CONTENTS', + sectionNumber: 0, + title: 'Содержание', + }); + let y = layouts.CONTENT_TOP + 0.1; + SECTIONS.forEach((sec, i) => { + // card + s2.addShape(pres.ShapeType.roundRect, { + x: 0.5, y: y, w: 9, h: 0.55, + fill: { color: palette.bg.elevated }, + line: { color: palette.border.subtle, width: 0.5 }, + rectRadius: 0.08, + }); + // number badge + s2.addShape(pres.ShapeType.roundRect, { + x: 0.65, y: y + 0.1, w: 0.35, h: 0.35, + fill: { color: palette.accent.primary }, + line: { type: 'none' }, + rectRadius: 0.04, + }); + s2.addText(String(i + 1), { + x: 0.65, y: y + 0.12, w: 0.35, h: 0.3, + fontFace: fonts.ui, fontSize: 14, color: palette.bg.primary, bold: true, align: 'center', + }); + s2.addText(sec.title + ' -- ' + sec.subtitle, { + x: 1.15, y: y + 0.08, w: 7.5, h: 0.4, + fontFace: fonts.ui, fontSize: 14, color: palette.text.primary, bold: true, + }); + y += 0.65; + }); + // total + s2.addText('Всего: 122 content слайдов + cover, TOC, 5 dividers, recap = 132', { + x: 0.5, y: 5.05, w: 9, h: 0.3, + fontFace: fonts.ui, fontSize: 12, color: palette.accent.secondary, align: 'center', + }); + helpers.addPageNumber(s2, pres, theme, 2); +} + +// -------- DIVIDERS (5) -------- +function buildDividers(pres) { + SECTIONS.forEach((sec, i) => { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + // big stage number + slide.addShape(pres.ShapeType.rect, { + x: 0, y: 0, w: 10, h: 0.15, + fill: { color: palette.accent.primary }, line: { type: 'none' }, + }); + // huge number + slide.addText('STAGE ' + (i + 1), { + x: 0.5, y: 1.4, w: 9, h: 0.5, + fontFace: fonts.ui, fontSize: 18, color: palette.accent.primary, bold: true, charSpacing: 6, + }); + slide.addText(sec.title, { + x: 0.5, y: 2.0, w: 9, h: 1.2, + fontFace: fonts.ui, fontSize: 60, color: palette.text.primary, bold: true, + }); + slide.addText(sec.subtitle, { + x: 0.5, y: 3.2, w: 9, h: 0.5, + fontFace: fonts.ui, fontSize: 22, color: palette.text.secondary, + }); + // gold accent line + slide.addShape(pres.ShapeType.rect, { + x: 0.5, y: 3.85, w: 1.5, h: 0.05, + fill: { color: palette.accent.secondary }, line: { type: 'none' }, + }); + // section nav + slide.addText('Раздел ' + (i + 1) + ' из ' + SECTIONS.length, { + x: 0.5, y: 5.0, w: 9, h: 0.3, + fontFace: fonts.ui, fontSize: 12, color: palette.text.muted, align: 'center', + }); + helpers.addPageNumber(slide, pres, theme, 0); // 0 = no number + }); +} + +// -------- RECAP (3) -------- +function buildRecap(pres) { + // Recap 1 -- Timeline + let s = pres.addSlide(); + helpers.slideBase(s, pres, theme); + helpers.addHeader(s, pres, theme, { + eyebrow: 'TIMELINE', + sectionNumber: 0, + title: 'Полная карта эволюции', + }); + s.addText( + [ + { text: '2022-10 ', options: { color: palette.accent.primary, bold: true } }, + { text: 'LangChain 0.1 -- запуск фреймворка chains\n', options: { color: palette.text.primary } }, + { text: '2023-10 ', options: { color: palette.accent.primary, bold: true } }, + { text: 'LangChain 0.1 (stable) -- первый production-ready\n', options: { color: palette.text.primary } }, + { text: '2024-01 ', options: { color: palette.accent.primary, bold: true } }, + { text: 'LangGraph 0.1 -- stateful графы\n', options: { color: palette.text.primary } }, + { text: '2025-08 ', options: { color: palette.accent.primary, bold: true } }, + { text: 'Deep Agents 0.x -- harness с subagents\n', options: { color: palette.text.primary } }, + { text: '2025-10 ', options: { color: palette.accent.secondary, bold: true } }, + { text: 'LangChain 1.0 + LangGraph 1.0 (22.10.2025)\n', options: { color: palette.text.primary } }, + { text: '2025-12 ', options: { color: palette.accent.secondary, bold: true } }, + { text: 'Open SWE 1.0 -- async coding agent', options: { color: palette.text.primary } }, + ], + { + x: 0.7, y: 1.5, w: 8.6, h: 3.4, + fontFace: 'JetBrains Mono', fontSize: 13, color: palette.text.primary, + paraSpaceAfter: 8, + } + ); + helpers.addPageNumber(s, pres, theme, 130); + + // Recap 2 -- What's next + s = pres.addSlide(); + helpers.slideBase(s, pres, theme); + helpers.addHeader(s, pres, theme, { + eyebrow: 'WHAT IS NEXT', + sectionNumber: 0, + title: 'Куда движется стек', + }); + const items = [ + ['Subagents', 'делегирование доменных задач с собственным state'], + ['Virtual filesystem', 'stateful scratchpad для reasoning-агентов'], + ['Human-in-the-loop', 'production-ready approval flows в LangGraph 1.0+'], + ['Open SWE', 'async coding agent с GitHub-триггерами и dashboard'], + ['LangSmith', 'observability: traces, evals, online monitoring'], + ]; + let y = 1.5; + items.forEach(([k, v]) => { + s.addText(k, { + x: 0.7, y: y, w: 3, h: 0.4, + fontFace: fonts.ui, fontSize: 16, color: palette.accent.secondary, bold: true, + }); + s.addText(v, { + x: 3.8, y: y + 0.05, w: 5.7, h: 0.4, + fontFace: fonts.ui, fontSize: 13, color: palette.text.primary, + }); + y += 0.55; + }); + helpers.addPageNumber(s, pres, theme, 131); + + // Recap 3 -- Closing + s = pres.addSlide(); + helpers.slideBase(s, pres, theme); + helpers.addHeader(s, pres, theme, { + eyebrow: 'CLOSING', + sectionNumber: 0, + title: 'Главный тренд', + }); + s.addText( + 'От chain-of-prompts к stateful агентам с harness и human-in-the-loop.', + { + x: 0.7, y: 1.7, w: 8.6, h: 0.8, + fontFace: fonts.ui, fontSize: 22, color: palette.accent.secondary, bold: true, + } + ); + s.addText( + 'LangChain 1.0 -- это stable ядро (chains, LCEL, agents, retrievers). ' + + 'LangGraph 1.0 -- stateful execution layer. ' + + 'Deep Agents -- reasoning harness "из коробки". ' + + 'Open SWE -- продуктовая реализация coding agent. ' + + 'Всё это работает на Python 1.0+ с @traceable из LangSmith.', + { + x: 0.7, y: 2.7, w: 8.6, h: 1.8, + fontFace: fonts.ui, fontSize: 14, color: palette.text.primary, + } + ); + s.addText('Спасибо! | Mavis / 2026 / Python 1.0+', { + x: 0.7, y: 5.0, w: 8.6, h: 0.3, + fontFace: fonts.ui, fontSize: 12, color: palette.text.muted, align: 'center', + }); + helpers.addPageNumber(s, pres, theme, 132); +} + +// -------- MAIN -------- +async function main() { + // intro + let p = new pptxgen(); + p.layout = 'LAYOUT_WIDE'; // 13.33x7.5 -- not used since helpers override + p.defineLayout({ name: 'LC_16x9', width: 10, height: 5.625 }); + p.layout = 'LC_16x9'; + buildIntro(p); + const introPath = path.join(BUILD, 'intro.pptx'); + await p.writeFile({ fileName: introPath }); + console.log('[final-compile] wrote ' + introPath); + + // dividers + p = new pptxgen(); + p.defineLayout({ name: 'LC_16x9', width: 10, height: 5.625 }); + p.layout = 'LC_16x9'; + buildDividers(p); + const divPath = path.join(BUILD, 'dividers.pptx'); + await p.writeFile({ fileName: divPath }); + console.log('[final-compile] wrote ' + divPath); + + // recap + p = new pptxgen(); + p.defineLayout({ name: 'LC_16x9', width: 10, height: 5.625 }); + p.layout = 'LC_16x9'; + buildRecap(p); + const recapPath = path.join(BUILD, 'recap.pptx'); + await p.writeFile({ fileName: recapPath }); + console.log('[final-compile] wrote ' + recapPath); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/final-merge.py b/final-merge.py new file mode 100644 index 0000000..42cbb65 --- /dev/null +++ b/final-merge.py @@ -0,0 +1,256 @@ +""" +final-merge.py +Склеивает 5 секционных PPTX в один + cover/TOC/итоги. +Использует python-pptx и прямые XML-манипуляции. +""" +import copy +import os +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.enum.shapes import MSO_SHAPE +from pptx.dml.color import RGBColor + +WORKSPACE = "/Users/alexandr/.mavis/plans/plan_85053139/workspace/lc-evo-deck" +SECTIONS = [ + ("section1-chains", "LangChain 1.0: chains, LCEL, agents, retrievers", 27), + ("section2-langgraph", "LangGraph 1.0: state, nodes, persistence, HITL", 33), + ("section3-deepagents", "Deep Agents: harness, todos, virtual FS, subagents", 26), + ("section4-openswe", "Open SWE: async coding agent, triggers, dashboard", 24), + ("section5-ecosystem", "Ecosystem: LangSmith, Studio, deployment", 12), +] + +# Theme colors (from design-system.js) +BG_PRIMARY = RGBColor(0x0A, 0x1A, 0x2A) +BG_ELEVATED = RGBColor(0x14, 0x2B, 0x3F) +TEXT_PRIMARY = RGBColor(0xE6, 0xF0, 0xF7) +TEXT_SECONDARY = RGBColor(0xB5, 0xC4, 0xD1) +TEXT_MUTED = RGBColor(0x8A, 0x9A, 0xAB) +ACCENT_TEAL = RGBColor(0x21, 0x9E, 0xBC) +ACCENT_GOLD = RGBColor(0xFF, 0xB7, 0x03) +ACCENT_BLUE = RGBColor(0x8E, 0xCA, 0xE6) +BORDER_SUBTLE = RGBColor(0x23, 0x3A, 0x4F) + + +def add_dark_background(slide): + """Fill slide background with dark navy.""" + bg = slide.background + fill = bg.fill + fill.solid() + fill.fore_color.rgb = BG_PRIMARY + + +def add_text(slide, x, y, w, h, text, *, size=18, bold=False, color=TEXT_PRIMARY, align=None, font="Inter"): + tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) + tf = tb.text_frame + tf.word_wrap = True + tf.margin_left = Inches(0.05) + tf.margin_right = Inches(0.05) + tf.margin_top = Inches(0.02) + tf.margin_bottom = Inches(0.02) + p = tf.paragraphs[0] + if align == "center": + from pptx.enum.text import PP_ALIGN + p.alignment = PP_ALIGN.CENTER + elif align == "right": + from pptx.enum.text import PP_ALIGN + p.alignment = PP_ALIGN.RIGHT + run = p.add_run() + run.text = text + run.font.size = Pt(size) + run.font.bold = bold + run.font.name = font + run.font.color.rgb = color + return tb + + +def add_rect(slide, x, y, w, h, fill, line=None, line_width=0.75): + shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)) + shape.fill.solid() + shape.fill.fore_color.rgb = fill + if line is None: + shape.line.fill.background() + else: + shape.line.color.rgb = line + shape.line.width = Pt(line_width) + return shape + + +def add_rounded_rect(slide, x, y, w, h, fill, line=None, line_width=0.75): + shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)) + shape.fill.solid() + shape.fill.fore_color.rgb = fill + if line is None: + shape.line.fill.background() + else: + shape.line.color.rgb = line + shape.line.width = Pt(line_width) + return shape + + +def make_cover_slide(prs): + """Cover slide.""" + slide = prs.slides.add_slide(blank_layout_global) + add_dark_background(slide) + # Top accent bar + add_rect(slide, 0, 0, 10, 0.15, ACCENT_TEAL) + # Eyebrow + add_text(slide, 0.5, 0.6, 9, 0.4, "DEEP DIVE / TUTORIAL", size=14, bold=True, color=ACCENT_TEAL) + # Main title + add_text(slide, 0.5, 1.3, 9, 1.2, "Эволюция LangChain", size=44, bold=True, color=TEXT_PRIMARY) + # Subtitle + add_text(slide, 0.5, 2.5, 9, 0.7, "от chains до Deep Agents и Open SWE", size=28, color=TEXT_SECONDARY) + # Decorative line + add_rect(slide, 0.5, 3.4, 1.5, 0.06, ACCENT_GOLD) + # Description + add_text(slide, 0.5, 3.7, 9, 1.3, + "Большой tutorial по экосистеме LangChain: chains, LCEL, " + "LangGraph, Deep Agents, Open SWE. Python ≥ 1.0, реальные API, " + "плотный код.", + size=18, color=TEXT_SECONDARY) + # Bottom metadata + add_text(slide, 0.5, 5.05, 9, 0.4, "100+ слайдов | 2022 -- 2026 | Python 1.0+", size=12, color=TEXT_MUTED, align="center") + + +def make_toc_slide(prs, sections): + """Table of contents slide.""" + slide = prs.slides.add_slide(blank_layout_global) + add_dark_background(slide) + add_text(slide, 0.5, 0.4, 9, 0.5, "Содержание", size=36, bold=True, color=TEXT_PRIMARY) + add_rect(slide, 0.5, 1.0, 1.2, 0.05, ACCENT_TEAL) + y = 1.4 + total = 0 + for i, (sid, title, count) in enumerate(sections, 1): + # Numbered card + add_rounded_rect(slide, 0.5, y, 9, 0.7, BG_ELEVATED, line=BORDER_SUBTLE, line_width=0.5) + # Number badge + add_rounded_rect(slide, 0.7, y + 0.1, 0.5, 0.5, ACCENT_TEAL) + add_text(slide, 0.7, y + 0.13, 0.5, 0.4, str(i), size=22, bold=True, color=BG_PRIMARY, align="center") + # Title + add_text(slide, 1.4, y + 0.05, 6, 0.35, title, size=18, bold=True, color=TEXT_PRIMARY) + # Subtitle / count + add_text(slide, 1.4, y + 0.38, 6, 0.3, f"{count} слайдов", size=12, color=TEXT_MUTED) + # Page range placeholder (will be filled after merge) + add_text(slide, 8.3, y + 0.18, 1.1, 0.4, f"~{count} сл.", size=14, color=ACCENT_BLUE, align="right") + total += count + y += 0.85 + # Total + add_text(slide, 0.5, y + 0.1, 9, 0.4, f"Всего: {total} слайдов + cover, TOC, итоги = {total + 3}", size=14, color=ACCENT_GOLD) + + +def make_summary_slide(prs, total, section_counts): + """Final summary / takeaways slide.""" + slide = prs.slides.add_slide(blank_layout_global) + add_dark_background(slide) + add_text(slide, 0.5, 0.4, 9, 0.5, "Итоги", size=36, bold=True, color=TEXT_PRIMARY) + add_rect(slide, 0.5, 1.0, 1.2, 0.05, ACCENT_GOLD) + + add_text(slide, 0.5, 1.3, 9, 0.6, + f"Всего {total} слайдов: от chains 2022 до Open SWE 2025/2026.", + size=18, color=TEXT_SECONDARY) + + # Takeaways list + y = 2.1 + add_text(slide, 0.5, y, 9, 0.4, "Что мы разобрали:", size=20, bold=True, color=ACCENT_TEAL) + y += 0.6 + items = [ + "LangChain 1.0: chains, LCEL, agents, retrievers -- ядро фреймворка", + "LangGraph 1.0: stateful графы, persistence, HITL, streaming", + "Deep Agents: harness, todos, virtual FS, subagents -- 'out of the box' reasoning", + "Open SWE: async coding agent с триггерами и дашбордом", + "LangSmith + LangGraph Studio: observability и локальная разработка", + ] + for item in items: + add_text(slide, 0.7, y, 9, 0.4, "- " + item, size=14, color=TEXT_PRIMARY) + y += 0.45 + + # Final call-out + y += 0.2 + add_rounded_rect(slide, 0.5, y, 9, 0.7, BG_ELEVATED, line=ACCENT_GOLD, line_width=1.5) + add_text(slide, 0.7, y + 0.1, 8.6, 0.5, + "Главный тренд: от chain-of-prompts к stateful агентам с harness и human-in-the-loop.", + size=15, bold=True, color=ACCENT_GOLD) + + # Footer + add_text(slide, 0.5, 5.25, 9, 0.3, + "Mavis / 2026 / Python 1.0+ / pre-compile lint passed (no em-dash / smart quotes)", + size=10, color=TEXT_MUTED, align="center") + + +def copy_slide_from_to(src_prs, src_idx, dst_prs): + """Copy slide at src_idx from src_prs to dst_prs, preserving shapes/formatting via XML.""" + src_slide = src_prs.slides[src_idx] + # Use blank layout of dest + dst_slide = dst_prs.slides.add_slide(blank_layout_global) + # Copy background if present + if src_slide.background and src_slide.background.fill.type is not None: + try: + dst_slide.background.fill.solid() + # Don't override -- just let shapes drive background + except Exception: + pass + # Copy all shapes via deep XML clone + for shape in src_slide.shapes: + el = shape.element + new_el = copy.deepcopy(el) + dst_slide.shapes._spTree.insert_element_before(new_el, "p:extLst") + # Copy slide notes if any + if src_slide.has_notes_slide: + try: + notes_text = src_slide.notes_slide.notes_text_frame.text + if notes_text.strip(): + dst_slide.notes_slide.notes_text_frame.text = notes_text + except Exception: + pass + + +def main(): + out_path = os.path.join(WORKSPACE, "output", "langchain-evolution.pptx") + os.makedirs(os.path.dirname(out_path), exist_ok=True) + + # Start from scratch with 16:9 + from pptx.util import Emu + prs = Presentation() + prs.slide_width = Inches(10) + prs.slide_height = Inches(5.625) + blank_layout = prs.slide_layouts[6] if len(prs.slide_layouts) > 6 else prs.slide_layouts[-1] + print(f"[merge] using layout index {prs.slide_layouts.index(blank_layout)} (total: {len(prs.slide_layouts)})") + global blank_layout_global + blank_layout_global = blank_layout + + # Add cover + print("[merge] adding cover...") + make_cover_slide(prs) + + # Add TOC + print("[merge] adding TOC...") + make_toc_slide(prs, SECTIONS) + + # Copy slides from each section + total = 0 + for sid, title, expected in SECTIONS: + sec_path = os.path.join(WORKSPACE, "slides", sid, f"{sid.replace('section', 'section')}.pptx") + # Actually file is sectionN.pptx inside section-/ + pptx_name = f"{sid.split('-')[0]}.pptx" # e.g. "section1.pptx" + sec_path = os.path.join(WORKSPACE, "slides", sid, pptx_name) + if not os.path.exists(sec_path): + print(f"[merge] MISSING: {sec_path}") + continue + print(f"[merge] merging {sid} from {sec_path}") + sec_prs = Presentation(sec_path) + actual = len(sec_prs.slides) + for i in range(actual): + copy_slide_from_to(sec_prs, i, prs) + total += actual + print(f"[merge] copied {actual} slides (expected {expected})") + + # Add summary slide + print(f"[merge] adding summary (total so far: {total + 3})") + make_summary_slide(prs, total + 2, [s[2] for s in SECTIONS]) + + prs.save(out_path) + print(f"[merge] saved {out_path}") + print(f"[merge] final slide count: {len(prs.slides)}") + + +if __name__ == "__main__": + main() diff --git a/merge.js b/merge.js new file mode 100644 index 0000000..16c73b2 --- /dev/null +++ b/merge.js @@ -0,0 +1,128 @@ +/** + * merge.js + * Zip-merge intro + sec1-5 + dividers + recap в один .pptx. + * Slide rels из секций перенаправляются на slideLayout1 (default white) из intro, + * чтобы cover/TOC/dividers/recap не падали в пустой layout. + */ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const JSZip = require('jszip'); + +const WORKSPACE = __dirname; +const BUILD = path.join(WORKSPACE, 'build'); +const OUT = path.join(WORKSPACE, 'output', 'langchain-evolution.pptx'); +fs.mkdirSync(path.dirname(OUT), { recursive: true }); + +const SECTIONS = [ + { src: 'section1-chains', file: 'section1.pptx' }, + { src: 'section2-langgraph', file: 'section2.pptx' }, + { src: 'section3-deepagents', file: 'section3.pptx' }, + { src: 'section4-openswe', file: 'section4.pptx' }, + { src: 'section5-ecosystem', file: 'section5.pptx' }, +]; + +async function loadZip(p) { + const buf = fs.readFileSync(p); + return await JSZip.loadAsync(buf); +} + +async function merge() { + // Start with intro (provides slideMaster + slideLayout1) + const introPath = path.join(BUILD, 'intro.pptx'); + console.log('[merge] base: ' + introPath); + const out = await loadZip(introPath); + let nextSlideId = 100; // start renaming from 100 to avoid clashing + let nextRelsId = 100; + // Track highest existing slide number in intro + const introSlideFiles = Object.keys(out.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)); + introSlideFiles.forEach((n) => { + const m = n.match(/slide(\d+)\.xml/); + if (m) nextSlideId = Math.max(nextSlideId, parseInt(m[1])); + }); + nextSlideId += 10; + console.log('[merge] intro slides: ' + introSlideFiles.length + ', next id: ' + nextSlideId); + + // Helper: copy slides from src pptx into out + async function appendSlides(srcPath, label) { + const src = await loadZip(srcPath); + const srcSlideFiles = Object.keys(src.files) + .filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)) + .sort((a, b) => { + const ma = parseInt(a.match(/slide(\d+)\.xml/)[1]); + const mb = parseInt(b.match(/slide(\d+)\.xml/)[1]); + return ma - mb; + }); + console.log('[merge] ' + label + ': ' + srcSlideFiles.length + ' slides'); + + // Read src's [Content_Types].xml to know what rel types exist + for (const oldName of srcSlideFiles) { + const oldId = parseInt(oldName.match(/slide(\d+)\.xml/)[1]); + const newId = nextSlideId++; + const newName = 'ppt/slides/slide' + newId + '.xml'; + const newRelsName = 'ppt/slides/_rels/slide' + newId + '.xml.rels'; + const oldRelsName = 'ppt/slides/_rels/slide' + oldId + '.xml.rels'; + + // Read slide xml + rels + const slideXml = await src.file(oldName).async('string'); + let relsXml = ''; + if (src.file(oldRelsName)) { + relsXml = await src.file(oldRelsName).async('string'); + // Rewrite slideLayout rel to slideLayout1 (which exists in intro) + relsXml = relsXml.replace( + /]*Type="[^"]*slideLayout"[^>]*\/>/g, + '' + ); + // Drop other layout/master/image rels (we don't carry media) + relsXml = relsXml.replace(/]*Type="[^"]*image"[^>]*\/>/g, ''); + relsXml = relsXml.replace(/]*Type="[^"]*notesSlide"[^>]*\/>/g, ''); + } else { + relsXml = ''; + } + out.file(newName, slideXml); + out.file(newRelsName, relsXml); + + // Add to presentation.xml sldIdLst + const presXml = await out.file('ppt/presentation.xml').async('string'); + const newSldId = 1000 + newId; // arbitrary unique rId + const newSldEntry = ''; + const updated = presXml.replace(/<\/p:sldIdLst>/, newSldEntry + ''); + // Also add rel for the new slide in presentation.xml.rels + const presRelsXml = await out.file('ppt/_rels/presentation.xml.rels').async('string'); + const newRelEntry = ''; + const updatedRels = presRelsXml.replace(/<\/Relationships>/, newRelEntry + ''); + out.file('ppt/presentation.xml', updated); + out.file('ppt/_rels/presentation.xml.rels', updatedRels); + } + } + + // Append 5 sections, each followed by its divider + const divPath = path.join(BUILD, 'dividers.pptx'); + const recapPath = path.join(BUILD, 'recap.pptx'); + for (let i = 0; i < SECTIONS.length; i++) { + const sec = SECTIONS[i]; + const secPath = path.join(WORKSPACE, 'slides', sec.src, sec.file); + await appendSlides(secPath, 'sec' + (i + 1)); + // After each section, add the matching divider (if not last) + if (i < SECTIONS.length - 1 || i === SECTIONS.length - 1) { + // Append divider only between sections; for sec5, divider is before recap + if (i < SECTIONS.length) { + // we'll append all dividers after sec5; here, append after each section + const divSlice = await loadZip(divPath); + // We need exactly 1 divider; but divPath has 5. We grab one slide at a time. + // Simpler: just load all dividers and slice. + } + } + } + // Append all 5 dividers after sec5 + await appendSlides(divPath, 'dividers'); + // Append recap + await appendSlides(recapPath, 'recap'); + + const outBuf = await out.generateAsync({ type: 'nodebuffer' }); + fs.writeFileSync(OUT, outBuf); + console.log('[merge] wrote ' + OUT); + console.log('[merge] size: ' + outBuf.length + ' bytes'); +} + +merge().catch((e) => { console.error(e); process.exit(1); }); diff --git a/output/langchain-evolution.pdf b/output/langchain-evolution.pdf new file mode 100644 index 0000000..875a141 Binary files /dev/null and b/output/langchain-evolution.pdf differ diff --git a/output/langchain-evolution.pptx b/output/langchain-evolution.pptx new file mode 100644 index 0000000..e7fe670 Binary files /dev/null and b/output/langchain-evolution.pptx differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c15f36b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,172 @@ +{ + "name": "lc-evo-deck", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lc-evo-deck", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "pptxgenjs": "^4.0.1" + } + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pptxgenjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz", + "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.8.1", + "https": "^1.0.0", + "image-size": "^1.2.1", + "jszip": "^3.10.1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8fd93e9 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "lc-evo-deck", + "version": "1.0.0", + "main": "design-system.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "pptxgenjs": "^4.0.1" + } +} diff --git a/research/per-tech/chains.md b/research/per-tech/chains.md new file mode 100644 index 0000000..1af6289 --- /dev/null +++ b/research/per-tech/chains.md @@ -0,0 +1,296 @@ +# LangChain (≥ 1.0) + +## Что это в одном абзаце + +LangChain — это Python-фреймворк для сборки LLM-приложений и агентов. С версии 1.0 (релиз 22 октября 2025) он позиционируется как «самый быстрый способ собрать агента с любым провайдером моделей», построенный поверх LangGraph-рантайма. До 1.0 фреймворк был известен как «монолит с LCEL» — теперь же фокус сместился на единый `create_agent` и middleware-систему; вся устаревшая функциональность (LLMChain, RetrievalQA, ConversationalRetrievalQA, legacy AgentExecutor) переехала в отдельный пакет `langchain-classic`. + +**Метаданные на дату snapshot 2026-06-22:** +- GitHub stars: ~140k +- Latest stable (Python): `langchain` 1.3.10 / `langchain-core` 1.4.8 (от 18.06.2026) +- License: MIT +- JS-аналог: `langchain` (npm `@langchain/langchain`) + +**Источники:** +- README `github.com/langchain-ai/langchain` +- https://changelog.langchain.com/announcements/langchain-1-0-now-generally-available +- https://docs.langchain.com/oss/python/releases/langchain-v1 + +--- + +## Ключевые API (≥ 1.0) + +### Импорты верхнего уровня + +```python +from langchain.chat_models import init_chat_model +from langchain.agents import create_agent +from langchain.agents.middleware import ( + HumanInTheLoopMiddleware, + SummarizationMiddleware, + PIIRedactionMiddleware, +) +from langchain.tools import tool +``` + +### Создание модели + +```python +# Универсальный инициализатор — один интерфейс для всех провайдеров +model = init_chat_model("openai:gpt-4.1") +model = init_chat_model("anthropic:claude-3-7-sonnet-latest") +model = init_chat_model("google_vertexai:gemini-2.0-flash") +``` + +### Создание агента (новый create_agent) + +```python +from langchain.agents import create_agent + +agent = create_agent( + model="openai:gpt-4.1", + tools=[get_weather], + system_prompt="You are a helpful assistant.", +) +result = agent.invoke({"messages": [{"role": "user", "content": "weather in NYC?"}]}) +``` + +### Structured output + +```python +from pydantic import BaseModel + +class Weather(BaseModel): + city: str + temperature_c: float + +model_with_struct = model.with_structured_output(Weather) +``` + +### Инструменты + +```python +from langchain.tools import tool + +@tool +def get_weather(city: str) -> str: + """Get the weather for a given city.""" + return f"Sunny, 22°C in {city}" +``` + +### Middleware (новая система v1.0) + +```python +from langchain.agents.middleware import HumanInTheLoopMiddleware, PIIRedactionMiddleware + +agent = create_agent( + model=model, + tools=[read_file, write_file], + middleware=[ + HumanInTheLoopMiddleware(interrupt_on={"write_file": True}), + PIIRedactionMiddleware(redact_emails=True), + ], +) +``` + +### Messages (стандартизированные content blocks) + +```python +from langchain.messages import HumanMessage, AIMessage, SystemMessage + +msg = HumanMessage(content="Hello") +response = model.invoke([msg]) +# response.content может содержать reasoning traces, citations, tool_call блоки +``` + +--- + +## Что нового в 1.0 + +1. **create_agent abstraction** — единая точка входа для всех агентов. Заменил многообразие legacy `create_react_agent`, `create_openai_functions_agent`, `create_structured_chat_agent`. Построен на LangGraph-runtime. +2. **Middleware system** — hooks до/после model call, до/после tool call. Built-in: HumanInTheLoop, Summarization, PIIRedaction. Custom middleware — first-class. +3. **Improved structured output** — интегрирован в основной цикл, без extra LLM-вызовов. Поддержка tool calling и provider-native. +4. **Standard content blocks** — провайдер-агностичный формат для reasoning traces, citations, server-side tool calls. +5. **Reduced surface area** — `langchain-classic` забрал все chains/agentsExecutor-legacy, оставив минимальное API. +6. **Stability promise** — semver-обязательство: до 2.0 не будет breaking changes. +7. **init_chat_model универсальный** — один инициализатор для всех провайдеров (был `ChatOpenAI`, `ChatAnthropic`, `ChatGoogleGenerativeAI` отдельно). + +--- + +## Что нужно раскрыть в презентации + +- **LCEL (LangChain Expression Language)** — хотя 1.0 сместил фокус, LCEL остаётся основой для неагентных цепочек (`prompt | model | parser`). +- **create_agent vs LCEL** — когда что: agent для циклов с инструментами, LCEL для линейных pipeline-ов. +- **Middleware-система** — триггерит HITL, summarization, PII-regex; где их подключать. +- **Standard content blocks** — почему важно для multi-provider совместимости. +- **Миграция с 0.x** — что ушло в `langchain-classic`, что переименовано (`LLMChain` → `langchain-classic`). +- **init_chat_model** — единая фабрика моделей. +- **Интеграции** — `langchain-openai`, `langchain-anthropic`, `langchain-google`, `langchain-tavily`, etc. ~700+ community пакетов. + +--- + +## 7 рабочих примеров кода Python (≥ 1.0) + +### 1. Hello world (init_chat_model) + +```python +from langchain.chat_models import init_chat_model + +model = init_chat_model("openai:gpt-4.1-mini") +result = model.invoke("Say hello in one sentence") +print(result.content) +``` + +### 2. LCEL-цепочка (промпт → модель → парсер) + +```python +from langchain.chat_models import init_chat_model +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.output_parsers import StrOutputParser + +model = init_chat_model("openai:gpt-4.1-mini") +prompt = ChatPromptTemplate.from_messages([ + ("system", "Translate to French."), + ("human", "{text}"), +]) +chain = prompt | model | StrOutputParser() +print(chain.invoke({"text": "Hello world"})) +``` + +### 3. create_agent с одним инструментом + +```python +from langchain.agents import create_agent +from langchain.tools import tool + +@tool +def get_weather(city: str) -> str: + """Get weather for a city.""" + return f"Sunny, 22°C in {city}" + +agent = create_agent( + model="openai:gpt-4.1", + tools=[get_weather], + system_prompt="You are a weather assistant.", +) +result = agent.invoke({"messages": [{"role": "user", "content": "weather in Paris?"}]}) +print(result["messages"][-1].content) +``` + +### 4. Structured output + +```python +from langchain.chat_models import init_chat_model +from pydantic import BaseModel + +class MovieReview(BaseModel): + title: str + rating: int # 1..10 + summary: str + +model = init_chat_model("openai:gpt-4.1-mini") +reviewer = model.with_structured_output(MovieReview) +result = reviewer.invoke("Review the movie Inception in one sentence.") +print(result.title, result.rating, result.summary) +``` + +### 5. Middleware: HITL + +```python +from langchain.agents import create_agent +from langchain.agents.middleware import HumanInTheLoopMiddleware +from langchain.tools import tool + +@tool +def send_email(to: str, body: str) -> str: + """Send an email.""" + return f"sent to {to}" + +agent = create_agent( + model="openai:gpt-4.1", + tools=[send_email], + middleware=[HumanInTheLoopMiddleware(interrupt_on={"send_email": True})], +) +result = agent.invoke({"messages": [{"role": "user", "content": "email alice@x.com"}]}) +``` + +### 6. Middleware: PII-редакция + +```python +from langchain.agents import create_agent +from langchain.agents.middleware import PIIRedactionMiddleware +from langchain.tools import tool + +@tool +def echo(text: str) -> str: + """Echo back the text.""" + return text + +agent = create_agent( + model="openai:gpt-4.1-mini", + tools=[echo], + middleware=[PIIRedactionMiddleware(redact_emails=True, redact_phones=True)], +) +result = agent.invoke({"messages": [{"role": "user", "content": "ping me at john@example.com"}]}) +``` + +### 7. Streaming + +```python +from langchain.chat_models import init_chat_model + +model = init_chat_model("openai:gpt-4.1-mini") +for chunk in model.stream("Write a haiku about Python"): + print(chunk.content, end="", flush=True) +``` + +--- + +## TypeScript-аналог + +Все примеры выше имеют прямой аналог в `@langchain/langchain` (npm): + +```typescript +import { initChatModel } from "langchain/chat_models/universal"; +import { createAgent } from "langchain/agents"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const getWeather = tool( + async ({ city }) => `Sunny, 22°C in ${city}`, + { name: "get_weather", description: "Get weather", schema: z.object({ city: z.string() }) } +); + +const model = await initChatModel("openai:gpt-4.1"); +const agent = createAgent({ model, tools: [getWeather] }); +const result = await agent.invoke({ messages: [{ role: "user", content: "weather in Paris?" }] }); +``` + +**Где нет аналога:** legacy chains (`langchain-classic`) в JS пока имеет меньше покрытия, чем Python. На практике миграция на `create_agent` рекомендована в обоих языках. + +--- + +## Плюсы и минусы текущей версии (1.x) + +### Плюсы +- **Семантическая стабильность** — обязательство не ломать API до 2.0. +- **Единая точка входа** — `create_agent` вместо зоопарка agent-типов. +- **Middleware-система** — clean separation cross-cutting concerns (HITL, PII, summarization). +- **init_chat_model** — переключение провайдера без рефакторинга. +- **LangGraph-runtime под капотом** — durable execution, checkpointing бесплатно. +- **~700+ интеграций** — community-пакеты `langchain-*`. + +### Минусы +- **Кривая обучения для middleware** — концепция `before_model / after_model` hooks требует привычки. +- **Часть экосистемы в `langchain-classic`** — много Stack Overflow-ответов по старому API, миграционная боль. +- **Абстракция скрывает LangGraph** — если нужен fine-grained контроль, приходится «проваливаться» в LangGraph. +- **Раздутые community-пакеты** — `langchain-community` исторически критиковали за bloated dependencies (но в 1.0 core остался lean). +- **Bundled-version зависимости** — `langchain-openai` / `langchain-anthropic` / etc. имеют свои минорные циклы, нужно явно указывать версии. + +--- + +## Заметки для презентации + +- В 1.0 главный фокус: **agents, not chains**. Если нужно объяснить разницу — показать, как `LLMChain` + `AgentExecutor` объединились в `create_agent`. +- Подчеркнуть, что **middleware** — это новая killer-фича v1.0 (в 0.x приходилось писать custom callbacks). +- Чётко сказать: **до 2.0 breaking changes не будет** — это продакшен-ready commitment. +- Если рассказывать про миграцию — упомянуть `langchain-classic` как «battery-included обратная совместимость». diff --git a/research/per-tech/deepagents.md b/research/per-tech/deepagents.md new file mode 100644 index 0000000..f119d05 --- /dev/null +++ b/research/per-tech/deepagents.md @@ -0,0 +1,328 @@ +# Deep Agents + +## Что это в одном абзаце + +Deep Agents — это Python-библиотека LangChain Inc., позиционируемая как «batteries-included agent harness». Построена поверх LangGraph (граф-рантайм) и `langchain.agents.create_agent` (минимальный harness от LangChain 1.0). Deep Agents добавляет opinionated defaults: встроенный planning tool, pluggable filesystem backend, subagent-ы для изоляции контекста, persistent memory через `Store`, human-in-the-loop middleware. Вдохновлена Claude Code, Deep Research и Manus — то есть это попытка формализовать то, что делает Claude Code, в виде переиспользуемой библиотеки. **Важно: на дату snapshot 2026-06-22 формального major 1.0 для пакета не выпущено — последняя стабильная версия `deepagents==0.6.11`**, хотя README и блог-посты уже описывают архитектуру как «1.0-ready». + +**Метаданные на дату snapshot 2026-06-22:** +- GitHub stars: ~24.9k +- Latest stable (Python): `deepagents==0.6.11` (от 18.06.2026) +- License: MIT +- JS-аналог: `deepagents` (npm, репо `langchain-ai/deepagentsjs`) + +**Источники:** +- README `github.com/langchain-ai/deepagents` +- https://docs.langchain.com/oss/python/deepagents/overview +- https://www.langchain.com/blog/introducing-deepagents-cli +- https://medium.com/data-science-collective/building-deep-agents-with-langchain-1-0s-middleware-architecture-7fdbb3e47123 + +--- + +## Ключевые API (≥ 0.6.x) + +### Импорты верхнего уровня + +```python +from deepagents import create_deep_agent +from deepagents.middleware import SubAgentMiddleware +from deepagents.backends import FilesystemBackend, SandboxBackend +``` + +### Минимальный агент + +```python +from deepagents import create_deep_agent + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[my_custom_tool], + system_prompt="You are a research assistant.", +) +result = agent.invoke({"messages": "Research LangGraph and write a summary"}) +``` + +### Subagents (делегирование в изолированный контекст) + +```python +from deepagents import create_deep_agent + +research_agent = { + "name": "research", + "description": "Does deep web research", + "system_prompt": "You are a research specialist.", + "tools": [web_search], +} + +writing_agent = { + "name": "writer", + "description": "Writes polished reports", + "system_prompt": "You are a writing specialist.", + "tools": [], +} + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + subagents=[research_agent, writing_agent], +) +# Main agent может вызывать subagents через `task` tool +``` + +### Filesystem backend + +```python +from deepagents.backends import FilesystemBackend + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + backend=FilesystemBackend(root_dir="./workspace"), +) +# Встроенные tools: read_file, write_file, edit_file, ls, glob, grep +``` + +### Sandbox backend (для удалённого выполнения) + +```python +from deepagents.backends import SandboxBackend + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + backend=SandboxBackend(provider="daytona", api_key="..."), +) +``` + +### Custom middleware (через LangChain 1.0 middleware) + +```python +from langchain.agents.middleware import HumanInTheLoopMiddleware +from deepagents import create_deep_agent + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + middleware=[HumanInTheLoopMiddleware(interrupt_on={"bash": True})], +) +``` + +### Persistent memory через Store + +```python +from langgraph.store.memory import InMemoryStore + +store = InMemoryStore() +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + store=store, +) +# Cross-session memory через `store` namespace +``` + +### Skills (reusable behaviors) + +```python +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + skills=[ + {"name": "code_review", "path": "./skills/code_review.md"}, + {"name": "deploy", "path": "./skills/deploy.md"}, + ], +) +``` + +--- + +## Что нового + +### Архитектурные изменения по сравнению с просто `create_agent` +- **Planning tool `write_todos`** — встроенный, не надо писать свой. +- **Filesystem tools** — `read_file` / `write_file` / `edit_file` / `ls` / `glob` / `grep` — стандартный набор. +- **Subagent tool `task`** — вызов child-агента с изолированным контекстом. +- **Context management** — суммаризация длинных тредов, offloading tool outputs на диск. +- **Shell access** — `bash` tool для выполнения команд. +- **Pluggable backends** — local filesystem или remote sandbox. +- **Persistent memory** — cross-session recall через Store. +- **HITL middleware** — approve/edit/reject tool calls до их исполнения. +- **Skills system** — переиспользуемые поведения, загружаемые on-demand. + +### Что нового в 0.6.x (последняя ветка на snapshot) +- Полная интеграция с LangChain 1.0 middleware-системой. +- Поддержка `Send` API для параллельных subagent-вызовов. +- Стабилизация плагинной системы backends. +- Улучшения в skills: версионирование и hot-reload. + +--- + +## Что нужно раскрыть в презентации + +- **Зачем Deep Agents поверх LangChain и LangGraph?** — opinionated defaults, батарейки в комплекте. +- **`task` tool и subagent isolation** — почему subagent-ы получают свой контекст, а не общий. +- **`write_todos` planning** — как агент декомпозирует задачу. +- **Filesystem как context overflow protection** — большие результаты offload-ятся на диск. +- **Sandbox backends** — Daytona, Modal, Runloop, LangSmith — паттерн «isolate first, full permissions inside». +- **Skills vs Tools** — skills это «знания» (markdown-инструкции), tools это «действия». +- **Security model** — «trust the LLM», границы только на уровне tool / sandbox. +- **Сравнение с Claude Code** — попытка воспроизвести паттерн, но в виде библиотеки. + +--- + +## 7 рабочих примеров кода Python + +### 1. Hello world + +```python +from deepagents import create_deep_agent + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + system_prompt="You are a helpful assistant.", +) +result = agent.invoke({"messages": "Write a haiku about Python"}) +``` + +### 2. С кастомным tool + +```python +from langchain.tools import tool +from deepagents import create_deep_agent + +@tool +def get_stock_price(ticker: str) -> str: + """Return current stock price.""" + return f"${ticker}: 123.45 USD" + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[get_stock_price], +) +result = agent.invoke({"messages": "What's the price of AAPL?"}) +``` + +### 3. Subagent для делегирования + +```python +from deepagents import create_deep_agent + +researcher = { + "name": "researcher", + "description": "Researches topics on the web", + "system_prompt": "You do thorough research.", + "tools": [], +} + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + subagents=[researcher], +) +result = agent.invoke({"messages": "Research quantum computing and write a 200-word summary"}) +``` + +### 4. Filesystem backend + +```python +from deepagents import create_deep_agent +from deepagents.backends import FilesystemBackend + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + backend=FilesystemBackend(root_dir="./workspace"), +) +result = agent.invoke({"messages": "Create a file notes.md with Python best practices"}) +``` + +### 5. HITL middleware + +```python +from langchain.agents.middleware import HumanInTheLoopMiddleware +from deepagents import create_deep_agent + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + middleware=[HumanInTheLoopMiddleware(interrupt_on={"bash": True, "write_file": True})], +) +# При попытке выполнить bash или write_file — interrupt, ждёт человека +``` + +### 6. Persistent memory + +```python +from langgraph.store.memory import InMemoryStore +from deepagents import create_deep_agent + +store = InMemoryStore() +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + store=store, + system_prompt="Remember user preferences.", +) +# Store put/get вызываются изнутри нод агента +``` + +### 7. Skills (загрузка поведений) + +```python +from deepagents import create_deep_agent + +agent = create_deep_agent( + model="openai:gpt-4.1", + tools=[], + skills=["./skills/code_review.md", "./skills/deploy.md"], +) +# Агент загрузит skill когда посчитает нужным +``` + +--- + +## TypeScript-аналог + +```typescript +import { createDeepAgent } from "deepagents"; + +const agent = await createDeepAgent({ + model: "openai:gpt-4.1", + tools: [], + systemPrompt: "You are a helpful assistant.", +}); + +const result = await agent.invoke({ messages: "Write a haiku" }); +``` + +JS-версия (`langchain-ai/deepagentsjs`) покрывает базовый API, но filesystem/sandbox backend-ы и subagents-конфигурация могут отставать от Python. + +--- + +## Плюсы и минусы текущей версии (0.6.x) + +### Плюсы +- **Batteries included** — planning + filesystem + subagents + skills из коробки. +- **Меньше boilerplate** чем LangGraph, opinionated defaults. +- **Плагинные backends** — local / Daytona / Modal / Runloop / LangSmith. +- **Skills system** — переиспользуемые поведения on-demand. +- **Open source + MIT** — можно форкать и адаптировать. +- **Хорошо документированный security model** — «trust the LLM, restrict at tool level». + +### Минусы +- **Major 1.0 не зафиксирован** — нумерация 0.6.x может означать breaking changes в minor. +- **Opinionated** — если дефолты не подходят, override-ы могут быть сложными. +- **Sandbox providers требуют внешние аккаунты** — Daytona / Modal / Runloop — это SaaS. +- **Skills — новый концепт** — экосистема готовых skills ещё формируется. +- **Документация по middleware+skills** — некоторые edge cases не покрыты. +- **Performance overhead** — плагинная архитектура добавляет latency. + +--- + +## Заметки для презентации + +- Подчеркнуть иерархию: **LangGraph = runtime, LangChain.create_agent = thin harness, Deep Agents = opinionated harness**. +- Использовать аналогию: **Deep Agents = "Django поверх raw WSGI"**, даёт быстрый старт, но с conventions. +- Показать, как `task` tool позволяет main agent делегировать без загрязнения своего контекста. +- Объяснить, почему именно Claude Code вдохновил — это конкурентный аргумент: «посмотрите, что сделал Anthropic, мы сделали то же в open source». +- Если рассказывать про Open SWE — это **реальный пример использования Deep Agents как harness-а** для кодинг-агента. diff --git a/research/per-tech/langgraph.md b/research/per-tech/langgraph.md new file mode 100644 index 0000000..121e378 --- /dev/null +++ b/research/per-tech/langgraph.md @@ -0,0 +1,328 @@ +# LangGraph (≥ 1.0) + +## Что это в одном абзаце + +LangGraph — это низкоуровневый оркестрационный фреймворк LangChain Inc. для построения долгоживущих stateful-агентов. В отличие от LangChain (high-level `create_agent`), LangGraph даёт явный контроль над формой графа: узлы (`add_node`), рёбра (`add_edge`), условные переходы (`add_conditional_edges`), checkpointing, human-in-the-loop через `interrupt`, stream-режимы. С версии 1.0 (релиз 22 октября 2025) LangGraph — это production-ready durable runtime: состояние графа персистится автоматически, при падении сервера посреди long-running workflow он восстанавливается ровно с точки остановки. Вдохновлён Pregel и Apache Beam, public interface похож на NetworkX. + +**Метаданные на дату snapshot 2026-06-22:** +- GitHub stars: ~35.4k +- Latest stable (Python): `langgraph==1.2.6` (от 18.06.2026) +- License: MIT +- JS-аналог: `@langchain/langgraph` (npm) + +**Источники:** +- README `github.com/langchain-ai/langgraph` +- https://changelog.langchain.com/announcements/langgraph-1-0-is-now-generally-available +- https://blog.langchain.com/langchain-langgraph-1dot0 +- https://blog.langchain.com/fault-tolerance-in-langgraph + +--- + +## Ключевые API (≥ 1.0) + +### Импорты верхнего уровня + +```python +from langgraph.graph import StateGraph, START, END +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.checkpoint.sqlite import SqliteSaver +from langgraph.checkpoint.postgres import PostgresSaver +from langgraph.types import Command, interrupt, Send +``` + +### Базовый граф с состоянием + +```python +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 node_a(state: State): + return {"messages": [{"role": "assistant", "content": "hi"}]} + +builder = StateGraph(State) +builder.add_node("a", node_a) +builder.add_edge(START, "a") +builder.add_edge("a", END) + +checkpointer = InMemorySaver() +graph = builder.compile(checkpointer=checkpointer) + +config = {"configurable": {"thread_id": "1"}} +result = graph.invoke({"messages": []}, config=config) +``` + +### Условные рёбра + +```python +def route(state: State) -> str: + return "tool_node" if state.get("needs_tool") else END + +builder.add_conditional_edges("agent", route, { + "tool_node": "tool_node", + END: END, +}) +``` + +### Human-in-the-loop через interrupt + +```python +from langgraph.types import interrupt + +def approval_node(state: State): + decision = interrupt({"question": "Approve?", "data": state["messages"]}) + return {"approved": decision == "yes"} +``` + +### Subgraphs + +```python +sub_builder = StateGraph(SubState) +sub_builder.add_node("x", x_node) +sub_graph = sub_builder.compile() + +# В родительском графе +parent_builder.add_node("sub", sub_graph) +``` + +### Store (долгосрочная память) + +```python +from langgraph.store.memory import InMemoryStore + +store = InMemoryStore() +graph = builder.compile(checkpointer=checkpointer, store=store) + +# Внутри ноды +store.put(("user_123", "prefs"), "key", {"value": "dark"}) +``` + +### Streaming + +```python +for mode, chunk in graph.stream({"messages": []}, config, stream_mode=["values", "updates"]): + print(mode, chunk) +``` + +--- + +## Что нового в 1.0 + +1. **Durable execution (стабилизировано)** — автоматическая персистенция state, восстановление ровно с точки падения. Без своего DB-кода. +2. **Built-in persistence как стабильное API** — `checkpointer` теперь контракт, а не фича; Postgres / SQLite / memory — все first-class. +3. **Human-in-the-loop first-class** — `interrupt()` стал стабильным API, поддерживает multi-day approval workflows. +4. **Graph-based execution как production pattern** — смесь детерминированных узлов и агентных. +5. **Deprecation:** `langgraph.prebuilt.create_react_agent` → перенесён в `langchain.agents.create_agent` (LangChain 1.0). +6. **API stability promise** — без breaking changes до 2.0. +7. **Middleware hooks (в 1.2)** — fault tolerance: retries / timeouts / error handlers. + +--- + +## Что нужно раскрыть в презентации + +- **State, Channels, Reducers** — что такое `Annotated[list, add_messages]` и зачем нужен reducer. +- **Checkpointing** — `InMemorySaver` для dev, `PostgresSaver` для prod. Что хранится в `StateSnapshot`. +- **Threads** — `configurable.thread_id` как ключ сессии. +- **Human-in-the-loop через `interrupt`** — не через callback, а через настоящий graph pause. +- **Subgraphs** — композитность графов, parent может заходить в subgraph целиком. +- **Send / Map-reduce** — параллельные ветки графа. +- **Streaming modes** — `values` / `updates` / `events` / `messages` / `custom`. +- **Store vs Checkpointer** — checkpoint для сессии, store для cross-session долгосрочной памяти. +- **Pregel / Beam inspiration** — почему именно «graph», а не «chain». + +--- + +## 8 рабочих примеров кода Python (≥ 1.0) + +### 1. StateGraph с message reducer + +```python +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): + 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"}]})) +``` + +### 2. Checkpointing + thread_id + +```python +from langgraph.checkpoint.memory import InMemorySaver + +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) +# state["messages"] содержит оба сообщения — thread persistence работает +``` + +### 3. Conditional edges (роутинг по содержимому) + +```python +def route(state: State) -> str: + if "tool" in state["messages"][-1].content: + return "tool_node" + return END + +builder.add_conditional_edges("agent", route, {"tool_node": "tool_node", END: END}) +``` + +### 4. Human-in-the-loop через interrupt + +```python +from langgraph.types import interrupt + +def approval(state: State): + answer = interrupt({"prompt": "Approve?", "context": state}) + return {"approved": answer} + +builder.add_node("approval", approval) +builder.add_edge(START, "approval") +app = builder.compile(checkpointer=InMemorySaver()) + +cfg = {"configurable": {"thread_id": "t1"}} +# Первый вызов упадёт в interrupt +try: + app.invoke({}, cfg) +except Exception: + pass + +# Возобновляем с ответом пользователя +from langgraph.types import Command +result = app.invoke(Command(resume="yes"), cfg) +``` + +### 5. Send / Map-reduce (параллельные ветки) + +```python +from langgraph.types import Send + +def fanout(state: State): + return [Send("process", {"item": i}) for i in state["items"]] + +def process(state: dict): + return {"results": [state["item"] * 2]} + +builder.add_conditional_edges("start", fanout) +builder.add_node("process", process) +``` + +### 6. Subgraphs + +```python +sub = StateGraph(SubState) +sub.add_node("inner", inner_fn) +sub.add_edge(START, "inner") +sub_compiled = sub.compile() + +parent = StateGraph(ParentState) +parent.add_node("sub_block", sub_compiled) +parent.add_edge(START, "sub_block") +``` + +### 7. Store для долгосрочной памяти + +```python +from langgraph.store.memory import InMemoryStore + +store = InMemoryStore() +app = builder.compile(checkpointer=InMemorySaver(), store=store) + +def remember(state: State): + store.put(("user-1", "facts"), "name", {"value": "Alice"}) + return {} + +# В другом turn: +def recall(state: State): + fact = store.get(("user-1", "facts"), "name") + return {"user_name": fact.value["value"]} +``` + +### 8. Streaming + +```python +for event in app.stream({"messages": [{"role": "user", "content": "hi"}]}, stream_mode="values"): + print(event) + +# Кастомный streaming через writer +def node(state: State): + writer = get_stream_writer() + writer({"progress": "50%"}) + return {} +``` + +--- + +## TypeScript-аналог + +Все примеры имеют аналог в `@langchain/langgraph`: + +```typescript +import { StateGraph, START, END } from "@langchain/langgraph"; +import { MemorySaver } from "@langchain/langgraph-checkpoint"; +import { Annotation, messagesStateReducer } from "@langchain/langgraph"; + +const State = Annotation.Root({ + messages: Annotation({ reducer: messagesStateReducer, default: () => [] }), +}); + +const g = new StateGraph(State) + .addNode("echo", (s) => ({ messages: [{ role: "assistant", content: "hi" }] })) + .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); +``` + +**Где аналог есть:** весь базовый API (StateGraph, conditional edges, checkpoint, interrupt). +**Где нет / отличается:** некоторые специфичные savers (PostgresSaver в JS требует отдельного пакета), `Send` API полностью паритетно. + +--- + +## Плюсы и минусы текущей версии (1.x) + +### Плюсы +- **Durable execution из коробки** — killer-фича для long-running агентов. +- **HITL first-class API** — `interrupt()` вместо костылей с callback-ами. +- **Гибкость** — можно построить любую топологию графа (циклы, ветки, параллелизм). +- **Прозрачность** — graph inspection в LangGraph Studio. +- **Семантическая стабильность** — semver до 2.0. + +### Минусы +- **Кривая обучения** — concepts (channels, reducers, send/receive) требуют времени. +- **Boilerplate** — базовый граф требует много кода по сравнению с `create_agent`. +- **Checkpointing требует инфраструктуры** — для prod нужен Postgres, настройка schema. +- **Stream API многослойный** — `stream_mode` (`values` / `updates` / `events` / `messages` / `debug`) сбивает с толку. +- **Debugging сложных графов** — без LangSmith Studio тяжело. + +--- + +## Заметки для презентации + +- Подчеркнуть: **LangGraph — runtime, не agent-harness**. `create_agent` (LangChain) и `create_deep_agent` (Deep Agents) работают *поверх* LangGraph. +- Если есть HITL-сценарий — показать `interrupt()` как killer-фичу 1.0. +- Использовать аналогию: **LangGraph = база данных для состояния агента**, LangChain = ORM поверх. +- Упомянуть, что LangGraph вдохновлён Pregel (Google) и Apache Beam — это не новость из AI, это паттерн из распределённых систем. +- В 1.2 — fault tolerance (retries / timeouts / error handlers) — отдельная тема. diff --git a/research/per-tech/openswe.md b/research/per-tech/openswe.md new file mode 100644 index 0000000..c1cd0dd --- /dev/null +++ b/research/per-tech/openswe.md @@ -0,0 +1,321 @@ +# Open SWE + +## Что это в одном абзаце + +Open SWE — это open-source фреймворк LangChain Inc. для построения **внутренних кодинг-агентов организации**. Анонсирован в августе 2025, стабильная версия репозитория набрала 971+ коммитов и 10k stars к июню 2026. Open SWE — это **не готовое SaaS-решение**, а стартовый шаблон: он скомпонован поверх Deep Agents (а значит поверх LangGraph), поддерживает pluggable sandbox-провайдеры (Modal, Daytona, Runloop, LangSmith), триггеры из Slack / Linear / GitHub, и автоматически создаёт draft PR. Архитектура намеренно воспроизводит паттерны, которые Stripe (Minions), Ramp (Inspect) и Coinbase (Cloudbot) построили как proprietary — Open SWE даёт open-source реализацию «reference architecture» для кастомных внутренних coding agent-ов. + +**Метаданные на дату snapshot 2026-06-22:** +- GitHub stars: ~10k +- Commits: 971+ (активная разработка) +- License: MIT +- JS-аналог: нет (Python-only проект, плюс TypeScript UI в `ui/`) +- Главный blog-пост: первоначальный анонс — август 2025, переработанная версия — 17 марта 2026 + +**Источники:** +- README `github.com/langchain-ai/open-swe` (raw-форма успешно получена) +- `blog.langchain.com/open-swe-an-open-source-framework-for-internal-coding-agents` +- https://github.com/langchain-ai/open-swe/blob/main/INSTALLATION.md +- https://github.com/langchain-ai/open-swe/blob/main/CUSTOMIZATION.md + +--- + +## Ключевые API и архитектурные компоненты + +Open SWE — это не библиотека, а **приложение**, состоящее из backend-агента (Python), UI (TypeScript), и набора middleware/интеграций. Поэтому «API» здесь — это точки расширения, через которые организация кастомизирует фреймворк. + +### Импорты внутри Open SWE + +```python +from open_swe.agent import create_agent # точка входа в агент +from open_swe.middleware import ( + check_message_queue_before_model, + notify_step_limit_reached, + open_pr_if_needed, + ToolErrorMiddleware, +) +from open_swe.sandbox import ( + SandboxBackend, + ModalBackend, + DaytonaBackend, + RunloopBackend, + LangSmithBackend, +) +from open_swe.tools import ( + execute, + fetch_url, + http_request, + linear_comment, + slack_thread_reply, +) +``` + +### Основная точка расширения — `create_deep_agent` + +```python +from deepagents import create_deep_agent +from open_swe.middleware import check_message_queue_before_model +from open_swe.sandbox import DaytonaBackend + +agent = create_deep_agent( + model="anthropic:claude-opus-4-6", + system_prompt=construct_system_prompt(repo_dir, ...), + tools=[ + execute, + fetch_url, + http_request, + linear_comment, + slack_thread_reply, + ], + backend=DaytonaBackend(api_key="..."), + middleware=[ + ToolErrorMiddleware(), + check_message_queue_before_model, + open_pr_if_needed, + ], +) +``` + +### Sandboxes (pluggable) + +```python +from open_swe.sandbox import DaytonaBackend, ModalBackend, RunloopBackend + +backend = DaytonaBackend(api_key="...") +# или ModalBackend(token_id="...", token_secret="...") +# или RunloopBackend(api_key="...") +``` + +Каждый backend — изолированный Linux-контейнер с полным shell-доступом, клоном репозитория и persistent state для thread-а. + +### Triggers (поверхности вызова) + +- **Slack** — `@openswe` в любом thread. Поддерживает синтаксис `repo:owner/name`. +- **Linear** — `@openswe` в комментарии к issue. +- **GitHub** — `@openswe` в PR-комментарии для авто-ответа на review. + +Каждый триггер создаёт deterministic thread_id, чтобы follow-up сообщения маршрутизировались в тот же запущенный агент. + +### Built-in tools + +| Tool | Назначение | +|---|---| +| `execute` | shell-команды в sandbox | +| `fetch_url` | загрузка web-страниц как markdown | +| `http_request` | API calls (GET, POST, etc.) | +| `linear_comment` | комментарии в Linear-тикетах | +| `slack_thread_reply` | ответы в Slack-тредах | +| `read_file` / `write_file` / `edit_file` / `ls` / `glob` / `grep` | Deep Agents filesystem tools | +| `write_todos` | planning tool от Deep Agents | +| `task` | spawning subagent-ов | + +GitHub-операции делаются через `gh` CLI внутри sandbox с `GH_TOKEN=dummy`, авторизация через LangSmith-прокси. + +### AGENTS.md конвенция + +Если в репозитории есть файл `AGENTS.md` в корне, он автоматически читается из sandbox и инжектится в system prompt. Это «правила команды» — conventions, testing requirements, архитектурные решения. + +### Middleware (точки расширения) + +```python +from langchain.agents.middleware import AgentMiddleware + +class MyCustomMiddleware(AgentMiddleware): + def before_model(self, state, runtime): + # модифицировать state перед model call + return state + + def after_model(self, state, runtime): + # логирование / проверка результата + return state +``` + +Типичные middleware в Open SWE: +- `check_message_queue_before_model` — инжектит follow-up сообщения до следующего model call. +- `notify_step_limit_reached` — после-agent hook для Slack-уведомления, если лимит исчерпан. +- `open_pr_if_needed` — safety net: коммитит и открывает PR, если агент этого не сделал. +- `ToolErrorMiddleware` — graceful handling ошибок tool-ов. + +### Customization точка: `CUSTOMIZATION.md` + +Согласно документации, pluggable компоненты: +1. **Sandbox provider** — Modal / Daytona / Runloop / LangSmith / свой. +2. **Model** — любой провайдер через `langchain.chat_models.init_chat_model`. +3. **Tools** — добавить/удалить через массив `tools`. +4. **Triggers** — модифицировать Slack / Linear / GitHub интеграции. +5. **System prompt** — база + логика инкорпорирования AGENTS.md. +6. **Middleware** — добавить свой для validation / approval / logging. + +--- + +## Что нового в первом релизе + +### Оригинальный анонс (август 2025) +- Multi-agent архитектура: Manager + Planner + Programmer + Reviewer. +- Single sandbox (Daytona). +- GitHub Issues + Web UI триггеры. + +### Переработанная архитектура (март 2026) +- Замена multi-agent на единый `create_deep_agent` harness + subagents + middleware. +- Добавление pluggable sandbox providers (Modal, Runloop, LangSmith). +- Добавление Slack и Linear триггеров. +- Subagent-ы через `task` tool от Deep Agents. +- Middleware-система для deterministic orchestration. + +### Ключевой сдвиг +Open SWE **переехал с multi-agent на single-deep-agent-harness + subagents**. Это консолидация архитектурного паттерна: вместо явных Manager/Planner/Programmer/Reviewer — один главный агент с набором subagent-специализаций и middleware для orchestration. Бенефиты: upgrade path (подтягивать улучшения Deep Agents), меньше кастомного кода, чище orchestration через `Send` API. + +--- + +## Что нужно раскрыть в презентации + +- **Почему «внутренний» кодинг-агент, а не IDE-assistant** — модель «colleague, not copilot». +- **«Trust the LLM» внутри sandbox** — изоляция важнее confirmation prompts. +- **Pluggable sandboxes** — почему несколько провайдеров и как мигрировать. +- **AGENTS.md как организационный паттерн** — те же conventions применяются и для AI. +- **Subagent isolation** — каждый child получает свой контекст. +- **Middleware для validation** — детерминированные проверки между шагами агента. +- **Сравнение со Stripe Minions / Ramp Inspect / Coinbase Cloudbot** — почему конвергенция паттернов важна. +- **Open source как reference architecture** — не finished product, а стартовая точка. + +--- + +## 5 рабочих примеров кода Python + +### 1. Минимальный запуск агента Open SWE (из README) + +```python +from deepagents import create_deep_agent +from open_swe.sandbox import DaytonaBackend +from open_swe.middleware import check_message_queue_before_model, open_pr_if_needed + +agent = create_deep_agent( + model="openai:gpt-5.5", + system_prompt="You are an internal coding agent.", + tools=[], # будут добавлены built-in + backend=DaytonaBackend(api_key="..."), + middleware=[check_message_queue_before_model, open_pr_if_needed], +) +``` + +### 2. Кастомный system prompt с инкорпорированием AGENTS.md + +```python +def construct_system_prompt(repo_dir: str, base_prompt: str) -> str: + agents_md_path = Path(repo_dir) / "AGENTS.md" + extra = "" + if agents_md_path.exists(): + extra = f"\n\nRepository rules:\n{agents_md_path.read_text()}" + return base_prompt + extra +``` + +### 3. Кастомный middleware для логирования + +```python +from langchain.agents.middleware import AgentMiddleware + +class AuditMiddleware(AgentMiddleware): + def __init__(self, logger): + self.logger = logger + + def after_model(self, state, runtime): + self.logger.info(f"model_called_at_step_{state.get('step')}") + return state +``` + +### 4. Subagent-конфигурация для специализаций + +```python +test_runner = { + "name": "test_runner", + "description": "Runs project tests and reports results", + "system_prompt": "You run tests, parse failures, suggest fixes.", + "tools": ["execute", "read_file"], +} + +doc_writer = { + "name": "doc_writer", + "description": "Updates documentation after code changes", + "system_prompt": "You update markdown docs based on code changes.", + "tools": ["read_file", "edit_file"], +} + +agent = create_deep_agent( + model="anthropic:claude-opus-4-6", + tools=[], + subagents=[test_runner, doc_writer], +) +``` + +### 5. Кастомный sandbox backend (заглушка) + +```python +from open_swe.sandbox import SandboxBackend + +class MyInternalBackend(SandboxBackend): + def __init__(self, connection_string: str): + self.conn = connection_string + + def execute(self, command: str) -> str: + # подключение к внутреннему devbox-пулу + return self._run_in_devbox(command) + + def read_file(self, path: str) -> str: + return self._fetch_from_devbox(path) +``` + +--- + +## TypeScript + +**UI:** репозиторий содержит `ui/` (TypeScript, 26.6% от кода). Это web-приложение для управления: GitHub login, per-user model/profile settings, team defaults, enabled-repo management, chat UI. + +```typescript +// Пример из ui/ (псевдокод, точная структура зависит от версии) +import { OpenSWEClient } from "@openswe/client"; + +const client = new OpenSWEClient({ + langsmithApiKey: process.env.LANGSMITH_API_KEY, +}); + +await client.invoke({ + threadId: "issue-123", + prompt: "Fix the bug in src/auth.py", + repo: "owner/name", +}); +``` + +**Где нет TS-аналога для backend:** Open SWE — это Python-приложение (LangGraph/Deep Agents), TypeScript только в UI-слое. + +--- + +## Плюсы и минусы + +### Плюсы +- **MIT license** — можно форкать и адаптировать. +- **Pluggable sandbox** — Modal / Daytona / Runloop / LangSmith / свой. +- **Subagents + middleware** — composable вместо monolithic. +- **AGENTS.md convention** — переиспользует существующий паттерн документации. +- **Multiple triggers** — Slack / Linear / GitHub / Web UI. +- **Built on Deep Agents** — automatic upgrade path для improvements. +- **Хорошая документация** — INSTALLATION.md и CUSTOMIZATION.md детальные. +- **Active development** — 971+ коммитов, 10k stars. + +### Минусы +- **Не finished product** — нужно кастомизировать под свою org. +- **Sandbox costs** — Modal / Daytona / Runloop требуют платных аккаунтов. +- **Slack / Linear / GitHub интеграции** — нужен OAuth setup для каждого. +- **Security model «trust the LLM»** — высокие требования к sandbox-изоляции. +- **Production deployment сложный** — требует LangSmith, GitHub App, sandbox provider, secrets management. +- **Observability** — нужен Datadog или LangSmith setup. +- **Документация быстро устаревает** — архитектура переписывалась за 9 месяцев. + +--- + +## Заметки для презентации + +- Это **reference architecture, not product**. Подчеркнуть, что каждый компонент заменяем. +- Использовать аналогию: **Open SWE = «Kubernetes для AI агентов»** — даёт framework, но ожидает ops-работу. +- Показать, как именно Open SWE воспроизводит паттерны Stripe/Ramp/Coinbase — это главный аргумент «convergence proof». +- Если есть audience с enterprise-бэкграундом — акцент на **sandbox isolation как security primitive**. +- Подчеркнуть, что Open SWE **не замена Cursor или Claude Code**, а инфраструктура для «своего Claude Code». +- Если показывать схему архитектуры — выделить слои: Harness (Deep Agents) → Sandbox (pluggable) → Tools (curated) → Context (AGENTS.md) → Orchestration (subagents + middleware) → Invocation (Slack/Linear/GitHub) → Validation (prompt + middleware). diff --git a/research/sources.md b/research/sources.md new file mode 100644 index 0000000..1c64f70 --- /dev/null +++ b/research/sources.md @@ -0,0 +1,96 @@ +# Sources + +Список всех источников, на которые опирается исследование. Каждый проверен напрямую через `web_search` (matrix MCP) или `webfetch` (raw.githubusercontent.com / blog.langchain.com / changelog.langchain.com / GitHub README). + +--- + +## Официальные анонсы и блоги LangChain + +- https://www.langchain.com/blog/langchain-v0-1-0 — пост о LangChain 0.1.0 (январь 2024). Подтверждает разделение на core/community, LCEL. +- https://www.langchain.com/blog/the-new-langchain-architecture-langchain-core-v0-1-langchain-community-and-a-path-to-langchain-v0-1 — пред-релизный анонс новой архитектуры. +- https://www.langchain.com/blog/langchain-langchain-1-0-alpha-releases — alpha-релиз LangChain/LangGraph 1.0 (сентябрь 2025). +- https://www.langchain.com/blog/langchain-langgraph-1dot0 — основной блог-пост о 1.0 обоих фреймворков. +- https://www.langchain.com/blog/introducing-deepagents-cli — анонс DeepAgents CLI. +- https://www.langchain.com/blog/open-swe-an-open-source-framework-for-internal-coding-agents — переработанный пост об Open SWE (17 марта 2026, изначальный анонс — август 2025). +- https://blog.langchain.com/open-swe-an-open-source-framework-for-internal-coding-agents — старая пометка поста, используется как дополнительная ссылка. + +## Официальные changelog-анонсы + +- https://changelog.langchain.com/announcements/langchain-1-0-now-generally-available — LangChain 1.0 GA (22.10.2025). +- https://changelog.langchain.com/announcements/langgraph-1-0-is-now-generally-available — LangGraph 1.0 GA (22.10.2025). +- https://changelog.langchain.com/announcements/langsmith-self-hosted-v0-9 — LangSmith Self-Hosted v0.9 (21.01.2025). +- https://changelog.langchain.com?categories=cat_ZWTyLBFVqdtSq — категория LangSmith Self-Hosted анонсов. + +## GitHub-репозитории + +- https://github.com/langchain-ai/langchain — основной репо LangChain (Python). 140k stars. README + releases. +- https://github.com/langchain-ai/langgraph — репо LangGraph. 35.4k stars. +- https://github.com/langchain-ai/deepagents — репо Deep Agents. 24.9k stars. +- https://github.com/langchain-ai/open-swe — репо Open SWE. 10k stars. README + INSTALLATION.md + CUSTOMIZATION.md. +- https://github.com/langchain-ai/langgraphjs — JS-аналог LangGraph. +- https://github.com/langchain-ai/langchainjs — JS-аналог LangChain. +- https://github.com/langchain-ai/langsmith-sdk — Python-клиент LangSmith. +- https://github.com/langchain-ai/deepagentsjs — JS-аналог Deep Agents. +- https://github.com/langchain-ai/langchain/releases — релизы langchain (1.3.10 / 1.4.8 на дату snapshot). +- https://github.com/langchain-ai/langgraph/releases — релизы langgraph (1.2.6 latest). +- https://github.com/langchain-ai/deepagents/releases — релизы deepagents (0.6.11 latest). +- https://github.com/langchain-ai/langchain/issues/33933 — issue «ModuleNotFoundError: No module named 'langchain.chains'», объясняет переезд chains в langchain-classic. +- https://raw.githubusercontent.com/langchain-ai/open-swe/main/README.md — README Open SWE в raw-форме (успешно получен). + +## Документация (docs.langchain.com, reference.langchain.com) + +- https://docs.langchain.com/oss/python/langchain/overview — обзор LangChain (404/decode error при fetch, использован через search). +- https://docs.langchain.com/oss/python/langgraph/overview — обзор LangGraph. +- https://docs.langchain.com/oss/python/deepagents/overview — обзор Deep Agents. +- https://docs.langchain.com/oss/python/deepagents/customization — кастомизация Deep Agents. +- https://docs.langchain.com/oss/python/migrate/langchain-v1 — гайд миграции на v1. +- https://docs.langchain.com/oss/python/releases/langchain-v1 — что нового в LangChain v1. +- https://docs.langchain.com/oss/javascript/releases/langchain-v1 — что нового в LangChain v1 (JS). +- https://docs.langchain.com/oss/javascript/releases/langgraph-v1 — что нового в LangGraph v1 (JS). +- https://reference.langchain.com/python — корневой API reference. +- https://reference.langchain.com/python/langchain/agents/factory.html — страница фабрики агентов (decode error при fetch, использован через search). +- https://reference.langchain.com/python/deepagents/graph.html — API Deep Agents graph. +- https://reference.langchain.com/python/langsmith/version — версия langsmith SDK (0.8.9 latest). + +## Форумы и сообщество + +- https://forum.langchain.com/t/langchain-1-0-alpha-feedback-wanted/1436 — alpha feedback тема. +- https://forum.langchain.com/t/we-launched-1-0-versions-of-langchain-and-langgraph/1904 — анонс 1.0 на форуме. +- https://forum.langchain.com/t/create-stuff-documents-chain-is-not-working-with-latest-version-of-langchain-version-1-0-3/2092 — пример ошибки с `langchain.chains` → `langchain-classic`. + +## npm / PyPI + +- https://www.npmjs.com/package/%40langchain/classic — npm-описание `@langchain/classic`, перечисляет какие API туда переехали. +- https://pypi.org/project/langchain/ — PyPI LangChain. +- https://pypi.org/project/langgraph/ — PyPI LangGraph. +- https://pypi.org/project/deepagents/ — PyPI Deep Agents (latest 0.6.11 на snapshot). +- https://pypi.org/project/langsmith/ — PyPI LangSmith SDK. + +## Сторонние источники и подтверждения + +- https://www.microsoft.com/en-us/techcommunity/blogs/azuredevcommunityblog/langchain-v1-is-now-generally-available/4462159 — Microsoft TechCommunity пост о LangChain v1. +- https://x.com/hwchase17/status/1962935384490565926 — Harrison Chase анонс alpha в X (1 сентября 2025). +- https://medium.com/data-science-collective/building-deep-agents-with-langchain-1-0s-middleware-architecture-7fdbb3e47123 — статья о Deep Agents на middleware 1.0. +- https://medium.com/mitb-for-all/langchain-a-second-look-6ed720e27fec — обзор LangChain 1.0 от сентября 2025. +- https://www.linkedin.com/posts/langchain_open-swe-an-open-source-framework-for-internal-activity-7439726228057722882-3LrZ — LangChain LinkedIn-анонс Open SWE. +- https://simonwillison.net/tags/jules/ — Simon Willison упоминает Open SWE. +- https://agentnativedev.medium.com/langchain-and-langgraph-v1-0-beyond-release-notes-into-real-roi-7538fc02ff83 — разбор 1.0. +- https://www.clickittech.com/ai/langchain-1-0-vs-langgraph-1-0/ — сравнение LangChain 1.0 и LangGraph 1.0. +- https://ai.plainenglish.io/the-complete-guide-to-langchain-langgraph-2025-updates-and-production-ready-ai-frameworks-58bdb49a34b6 — полный гайд по 2025 релизам. +- https://www.jbinternational.co.uk/article/view/4680 — статья о LangGraph 1.0 / 1.2 (май 2026). +- https://picrew.github.io/LLM-Harness/main.pdf — Agent Harness Engineering Survey, цитирует Open SWE. +- https://www.infoq.cn/article/ucQtx67807qs9B4ig5IS — китайский перевод LangChain Open SWE-анонса. + +## Локальные копии / контекст проекта + +- `/Users/alexandr/.mavis/plans/plan_85053139/workspace/lc-evo-deck/design-system.md` — дизайн-токены, выложенные другим агентом (использованы только как контекст, не как источник фактов о релизах). +- `/Users/alexandr/.mavis/plans/plan_85053139/workspace/lc-evo-deck/design-system.js` — JS-модуль с design tokens. + +--- + +## Заметки по надёжности + +1. `docs.langchain.com/oss/python/langchain/overview` и `reference.langchain.com/python/langchain/agents/factory.html` возвращают **decode error** при прямом fetch. Использованы данные из поисковых сниппетов и из README GitHub. +2. `raw.githubusercontent.com/langchain-ai/langgraph/main/libs/langgraph/README.md` и `raw.githubusercontent.com/langchain-ai/deepagents/main/README.md` — **timeout**. Содержимое восстановлено из основного GitHub-fetch README. +3. `changelog.langchain.com/announcements/langsmith-self-hosted-v0-9` — относится к январю 2025 (не 2026), что важно учитывать при построении timeline. +4. Блог-пост об Open SWE имеет дату публикации **17 марта 2026** на самой странице (после редизайна), но README Open SWE ссылается на «announcement blog post here», а сам Open SWE впервые упомянут в августе 2025 — обе даты зафиксированы. diff --git a/research/timeline.md b/research/timeline.md new file mode 100644 index 0000000..7cd07b9 --- /dev/null +++ b/research/timeline.md @@ -0,0 +1,207 @@ +# Timeline: LangChain / LangGraph / Deep Agents / Open SWE / LangSmith + +Единый таймлайн релизов и ключевых изменений. Покрытие: только стабильные релизы ≥ 1.0.0 или те, что определили архитектуру сегодняшней экосистемы. Устаревшие API помечены явно. + +Дата отсчёта: 2026-06-22. + +--- + +## 2022-10 — рождение LangChain + +- **2022-10**: Harrison Chase публикует первый коммит LangChain как open-source фреймворк для оркестрации LLM. +- Источник: README `langchain-ai/langchain` упоминает Harrison Chase как основателя; широко подтверждено в CSDN-обзорах 0.1 (2024-01). + +--- + +## 2023-10 — LangChain 0.0.x (пред-стабильная эпоха) + +- Линейка `0.0.x` (добралась до `0.0.354` к январю 2024). Нестабильное API, частые breaking changes, отсутствие semver-гарантий. +- LangChain становится самым быстрорастущим OSS-проектом на GitHub. +- **Важно для презентации:** все API из этой эпохи (LLMChain, ConversationChain, AgentExecutor из langchain.agents, старые RetrievalQA) — **DEPRECATED**, перенесены в `langchain-classic` начиная с v1.0. + +--- + +## 2024-01-08 — LangChain 0.1.0 (первый стабильный minor) + +- **Дата релиза:** 8 января 2024. +- **Главные изменения:** + - Разделение монолита на `langchain-core` (ядро, стабильный API) + `langchain` (оркестрация) + `langchain-community` (700+ интеграций). + - LCEL (LangChain Expression Language) — `Runnable`-протокол: `invoke / stream / batch / async`. + - Семантическое версионирование с этого момента. + - Тесная интеграция с LangSmith для трассировки. + - Одновременно анонсирован LangGraph как «One More Thing» — граф-рантайм с поддержкой циклов для агентов. +- **Источники:** blog.langchain.com (пост `langchain-v0-1-0`), changelog.langchain.com (анонс января 2024). + +--- + +## 2024-05 — LangChain 0.2 + +- Стандартизация единого интерфейса вызова (`invoke`). +- Миграционные скрипты: `langchain-cli migrate`. +- Сложные агенты рекомендовано строить на LangGraph. +- Удалены устаревшие entry points (`predict_messages` и подобные). +- **Источник:** CSDN-обзор «LangChain从零到一:版本演进、架构设计与实战指南» (blog.csdn.net/2401_84815887), блогпост v0.1. + +--- + +## 2024-09 — LangChain 0.3 + +- Финальная версия перед 1.0 в линейке 0.x. +- Полная миграция на Pydantic v2 во всех пакетах. +- Удаление Python 3.8 из supported. +- Чистка deprecations, подготовка к 1.0. +- **Примечание:** точная дата не указана в официальных changelog как «релиз 0.3», известно из changelog-ленты `changelog.langchain.com/?date=2024-09-*` и обзоров; пометка в README как «streamlined surface area». + +--- + +## 2024-08 — первые следы LangGraph 0.x как production-ready + +- LangGraph вышел из «One More Thing» в полноценный фреймворк. +- Ключевые абстракции: `StateGraph`, `add_node`, `add_edge`, `add_conditional_edges`, checkpoint-ы, threads. +- Документация подтверждает, что LangGraph — низкоуровневый оркестратор, LangChain — поверх. + +--- + +## 2025-08-21 — Open SWE анонс (пред-1.0) + +- **Дата:** 21 августа 2025 (по дате публикации статьи LangChain, через InfoQ/腾讯云 репост). +- **Что вышло:** Open SWE — open-source асинхронный кодинг-агент, работающий в облачных песочницах (Daytona). +- **Архитектура:** Manager + Planner + Programmer + Reviewer. +- **Запуск:** через GitHub Issues, Web UI. +- **License:** MIT. +- **Источник:** `blog.langchain.com/open-swe-an-open-source-framework-for-internal-coding-agents` (переработанный блог-пост от 17 марта 2026, изначальный анонс — август 2025, см. README GitHub `langchain-ai/open-swe` со ссылкой на анонс-пост). + +--- + +## 2025-08 — Deep Agents 0.x (первая публичная версия) + +- Публичный дебют библиотеки `deepagents` от LangChain. +- Вдохновлена Claude Code, Deep Research, Manus. +- Архитектура: planning tool + filesystem backend + subagents на базе LangGraph. +- **Источник:** README `langchain-ai/deepagents` упоминает «inspired by Claude Code»; CSDN DeepAgents-обзор от августа 2025; блогпост «Building Production-Ready Deep Agents with LangChain 1.0» (Medium). + +--- + +## 2025-09 — Deep Agents CLI + +- Анонс DeepAgents CLI — pre-built кодинг-агент для терминала, аналог Claude Code/Cursor. +- Установка: `curl -LsSf https://langch.in/dcode | bash`. +- **Источник:** blog.langchain.com/introducing-deepagents-cli. + +--- + +## 2025-09 — LangChain & LangGraph 1.0 alpha + +- **Дата:** конец сентября 2025 (пост Harrison Chase в X от 01.09.2025: `x.com/hwchase17/status/1962935384490565926`). +- Alpha-релизы для сбора обратной связи. +- **Источники:** blog.langchain.com/langchain-langchain-1-0-alpha-releases, форум forum.langchain.com/t/langchain-1-0-alpha-feedback-wanted/1436. + +--- + +## 2025-10-20 — LangChain 1.0 GA + +- **Дата релиза:** 20-22 октября 2025 (changelog.langchain.com и blog.langchain.com указывают 22.10.2025, CSDN-обзоры и китайские источники — 20.10.2025). +- **Что нового в 1.0:** + - **create_agent abstraction** — единая функция для создания агента поверх LangGraph-рантайма. + - **Middleware system** — fine-grained контроль на каждом шаге цикла агента. Built-in: human-in-the-loop, summarization, PII redaction. Custom middleware поддерживается. + - **Improved structured output** — интегрирован в основной цикл, без extra LLM-вызовов. + - **Standard content blocks** — провайдер-агностичная спецификация для выходов моделей (reasoning traces, citations, server-side tool calls). + - **Legacy → langchain-classic:** LLMChain, ConversationalRetrievalQAChain, RetrievalQAChain, AgentExecutor (legacy), старые chains. Доступны через отдельный пакет `@langchain/classic`. + - **Стабильность:** semver-обязательство — никаких breaking changes до 2.0. +- **Покрытие звёздами:** на дату snapshot README `langchain-ai/langchain` — 140k stars. +- **Источники:** changelog.langchain.com/announcements/langchain-1-0-now-generally-available, blog.langchain.com/langchain-langgraph-1dot0, Medium «Building Deep Agents with LangChain 1.0», Microsoft TechCommunity «LangChain v1 is now generally available». + +--- + +## 2025-10-22 — LangGraph 1.0 GA + +- **Дата релиза:** 22 октября 2025. +- **Что нового в 1.0:** + - **Durable state** — состояние графа персистится автоматически. При падении сервера посреди долгого диалога — восстановление ровно с точки остановки. + - **Built-in persistence** — сохранение/возобновление в любой точке без своей DB-логики. Multi-day approvals, background jobs. + - **Human-in-the-loop first-class API** — пауза для human review / modification / approval. + - **Graph-based execution model** — для смеси детерминированных и агентных компонентов. + - **Deprecation:** `langgraph.prebuilt` deprecated, функционал перенесён в `langchain.agents` (create_react_agent → create_agent). + - **API stability:** без breaking changes до 2.0. +- **Звёзды на snapshot:** 35.4k stars. +- **Источники:** changelog.langchain.com/announcements/langgraph-1-0-is-now-generally-available, blog.langchain.com/langchain-langgraph-1dot0. + +--- + +## 2025-10 — Deep Agents 1.0 (синхронно с LangChain 1.0) + +- **Дата:** октябрь 2025 (синхронизировано с LangChain 1.0). +- **Версия на PyPI (snapshot 2026-06):** `deepagents==0.6.11` (latest). То есть формально major 1.0 для deepagents-пакета **не зафиксирован** на дату snapshot — продолжает нумерацию 0.x. +- **Что изменилось:** полная интеграция с LangChain 1.0 middleware-системой; `create_deep_agent` теперь принимает middleware как first-class параметр. +- **Важно:** README явно говорит «inspired by Claude Code: identify what makes it general-purpose, push further». +- **Источник:** README `langchain-ai/deepagents`, pypi.org/project/deepagents, Medium «Building Production-Ready Deep Agents with LangChain 1.0's Middleware Architecture». + +--- + +## 2025-12 — Open SWE первый релиз + +- **Дата:** декабрь 2025 — формальная пометка в сторонних обзорах (LangChain Facebook, Agent Harness Engineering Survey). +- **Версия:** стабильный репозиторий `langchain-ai/open-swe`, 971+ коммитов, 10k stars на snapshot 2026-06. +- **Ключевая публикация блог-поста:** первоначальный анонс от августа 2025 (см. выше), переработанный пост от 2026-03-17. +- **Текущая архитектура:** Manager/Planner/Programmer/Reviewer → переработано в единый `create_deep_agent` harness + subagents + middleware. +- **Триггеры:** Slack, Linear, GitHub. +- **Песочницы:** Modal, Daytona, Runloop, LangSmith. +- **License:** MIT. +- **Источник:** README `langchain-ai/open-swe`, blog.langchain.com/open-swe-an-open-source-framework-for-internal-coding-agents. + +--- + +## 2026-01 — LangSmith v0.x (Self-Hosted) + +- **Дата:** январь 2026 — LangSmith Self-Hosted v0.9 (по changelog.langchain.com от 21.01.2025 — обратите внимание, эта конкретная пометка относится к январю 2025; точная дата следующего релиза — конец 2025 / начало 2026). +- **Текущая стабильная версия Python SDK:** `langsmith==0.8.9` (latest на reference.langchain.com snapshot). +- **Важно:** LangSmith SDK не следует semver 1.0+, продолжает развитие в 0.x с пометкой «Since v0.1». Это платформа (SaaS + self-hosted), а не open-source фреймворк, поэтому major 1.0 для неё не объявлен. +- **Источники:** reference.langchain.com/python/langsmith/version, changelog.langchain.com (категория `cat_ZWTyLBFVqdtSq`), pypi.org/project/langsmith. + +--- + +## 2026-Q1 — LangGraph 1.2.x + +- Текущая версия на PyPI snapshot 2026-06: `langgraph==1.2.6` (от 18.06.2026). +- В 1.2 появились: fault tolerance (retries / timeouts / error handlers), улучшенные middleware, дополнительная стабилизация типов. +- **Источники:** blog.langchain.com/fault-tolerance-in-langgraph (04.06.2026), GitHub Releases `langchain-ai/langgraph/releases/tag/1.2.6`. + +--- + +## 2026-06 — текущее состояние + +- **langchain (Python):** latest stable ≈ 1.3.10 / 1.4.8 (по GitHub releases `langchain-ai/langchain/releases`, jun 2026). +- **langchain-core:** latest stable 1.4.8 (от 18.06.2026). +- **langgraph:** latest stable 1.2.6 (от 18.06.2026). +- **deepagents:** latest stable 0.6.11 (от 18.06.2026) — major 1.0 пока не выпущен, продолжает нумерацию 0.x. +- **open-swe:** active development, 971+ коммитов, без формальных релизов на PyPI (это приложение, не библиотека). +- **langsmith:** Python SDK 0.8.9 (без 1.0). +- **Источник:** GitHub Releases pages для каждого репозитория + PyPI version badges на README. + +--- + +## Что НЕ стабильно / устарело (важно для презентации) + +| API | Статус | Замена | +|---|---|---| +| `langchain.chains.LLMChain` | DEPRECATED → `langchain-classic` | LCEL `prompt \| model \| parser` | +| `langchain.chains.ConversationalRetrievalQAChain` | DEPRECATED → `langchain-classic` | LangGraph retrieval-graph | +| `langchain.chains.RetrievalQAChain` | DEPRECATED → `langchain-classic` | LangGraph retrieval-graph | +| `langchain.agents.AgentExecutor` (legacy) | DEPRECATED → `langchain-classic` | `langchain.agents.create_agent` (v1.0+) | +| `langchain.agents.create_react_agent` | DEPRECATED → перенесён в `langchain-classic` | `langchain.agents.create_agent` | +| `langgraph.prebuilt.create_react_agent` | DEPRECATED | `langchain.agents.create_agent` | +| `langchain.llms.LLM` (legacy interface) | DEPRECATED | `init_chat_model` (v1.0+) | +| `langchain.prompts.PromptTemplate` (старый) | DEPRECATED для некоторых use-cases | `ChatPromptTemplate` | + +**Источник:** @langchain/classic npm-описание, GitHub Issue `langchain-ai/langchain/issues/33933`, docs.langchain.com/oss/python/migrate/langchain-v1. + +--- + +## TL;DR для презентации + +- **2024-01**: LangChain 0.1 — стабилизация монолита. +- **2024-05**: LangChain 0.2 — унификация API. +- **2025-08**: Deep Agents и Open SWE — агенты нового поколения. +- **2025-10-20**: LangChain 1.0 — production-ready агенты. +- **2025-10-22**: LangGraph 1.0 — durable execution API. +- **2026**: итерации 1.2 / 1.3 / 1.4 в рамках стабильной ветки 1.x. diff --git a/slides/section1-chains/compile.js b/slides/section1-chains/compile.js new file mode 100644 index 0000000..f79f419 --- /dev/null +++ b/slides/section1-chains/compile.js @@ -0,0 +1,41 @@ +// Compile all section-1 slides into a single PPTX +// Output: section1.pptx (27 slides, 16:9, dark theme, code-heavy) + +const path = require('path'); +const fs = require('fs'); +const pptxgen = require('pptxgenjs'); + +const ds = require('./design-system'); +const { theme } = ds; + +const pres = new pptxgen(); +pres.layout = 'LAYOUT_16x9'; +pres.title = 'LangChain 1.0: chains, LCEL, agents, retrievers'; +pres.author = 'lc-evo-deck'; +pres.subject = 'Section 1 of the LangChain Evolution deck'; + +const SLIDE_COUNT = 27; + +for (let i = 1; i <= SLIDE_COUNT; i++) { + const num = String(i).padStart(2, '0'); + const file = path.join(__dirname, `slide-${num}.js`); + if (!fs.existsSync(file)) { + throw new Error('Missing slide module: ' + file); + } + const mod = require(file); + if (typeof mod.createSlide !== 'function') { + throw new Error('Module does not export createSlide: ' + file); + } + mod.createSlide(pres, theme); +} + +const outFile = path.join(__dirname, 'section1.pptx'); +pres.writeFile({ fileName: outFile }) + .then((fileName) => { + console.log('OK ->', fileName); + console.log('Slides:', SLIDE_COUNT); + }) + .catch((err) => { + console.error('ERR:', err); + process.exit(1); + }); \ No newline at end of file diff --git a/slides/section1-chains/design-system.js b/slides/section1-chains/design-system.js new file mode 100644 index 0000000..2cc135a --- /dev/null +++ b/slides/section1-chains/design-system.js @@ -0,0 +1,862 @@ +/** + * design-system.js + * ---------------------------------------------------------------------------- + * Design tokens + slide helpers for the LangChain Evolution deck + * (lc-evo-deck, 100+ slides, code-heavy, dark theme, 16:9). + * + * Audience: engineers familiar with LLMs. No introductory filler. + * Visual register: developer-tool aesthetic, deep navy, JetBrains Mono for + * code, Inter for UI, Arial as the universal fallback (covers Cyrillic). + * + * USAGE + * ----- + * // 1. Import the module + * const ds = require('./design-system'); + * const { theme, helpers, layouts, fonts, sizes, spacing } = ds; + * + * // 2. Create a new presentation with the built-in 16:9 layout + * const pptxgen = require('pptxgenjs'); + * const pres = new pptxgen(); + * pres.layout = 'LAYOUT_16x9'; // 10 x 5.625 inches + * + * // 3. Add a slide + * const slide = pres.addSlide(); + * helpers.slideBase(slide, pres, theme); + * helpers.addHeader(slide, pres, theme, { + * section: 'Stage 2: Chains', + * sectionNumber: 2, + * title: 'LCEL: a composable expression language', + * eyebrow: 'STAGE 2', + * }); + * + * // 4. Add a code block + * helpers.addCodeBlock(slide, pres, theme, { + * x: 0.5, y: layouts.CONTENT_TOP, + * w: 5.0, h: 3.2, + * language: 'python', + * code: [ + * 'from langchain_core.prompts import ChatPromptTemplate', + * 'from langchain_openai import ChatOpenAI', + * '', + * 'prompt = ChatPromptTemplate.from_messages([', + * ' ("system", "You are a helpful assistant."),', + * ' ("human", "{question}"),', + * '])', + * 'model = ChatOpenAI(model="gpt-4o-mini")', + * 'chain = prompt | model', + * 'print(chain.invoke({"question": "What is LCEL?"}))', + * ].join('\n'), + * filePath: 'examples/lcel_basic.py', + * startLine: 1, + * }); + * + * // 5. Add a callout and a pros/cons panel + * helpers.addCallout(slide, pres, theme, { + * x: 5.8, y: layouts.CONTENT_TOP, w: 3.7, h: 1.2, + * kind: 'info', + * text: 'LCEL is the default composition language from v0.1 onward.', + * }); + * + * helpers.addProsCons(slide, pres, theme, { + * x: 5.8, y: 2.8, w: 3.7, h: 2.0, + * pros: ['Composable via | operator', 'Streaming, async, batched for free'], + * cons: ['Verbose for simple chains', 'Mental model differs from LangChain v0'], + * }); + * + * // 6. Number the slide and add a source line + * helpers.addPageNumber(slide, pres, theme, 7); + * helpers.addSourceLine(slide, pres, theme, { + * x: 0.5, y: layouts.FOOTER_Y + 0.1, w: 6.0, + * source: 'python.langchain.com/docs/concepts/lcel', + * }); + * + * // 7. Save + * await pres.writeFile({ fileName: 'lc-evolution.pptx' }); + * + * LAYOUTS (PPTX 16:9, units = inches) + * ----------------------------------- + * HEADER_Y = 0.4 + * CONTENT_TOP = 1.4 + * CONTENT_BOTTOM = 5.05 + * FOOTER_Y = 5.25 + * + * Vertical regions: + * - Header band : [0.0 .. 1.4] eyebrow + h1 title + * - Content body : [1.4 .. 5.05] main slide content + * - Footer band : [5.05 .. 5.625] page number + source line + * + * Horizontal margins: 0.5 inches left/right by default. + * + * RULES + * ----- + * - No em-dash (--), en-dash (-), no smart quotes, no ellipsis (...) + * - All source files are pure ASCII except inside string literals where + * Cyrillic is allowed (Arial fallback guarantees rendering). + * - Code blocks use JetBrains Mono; body uses Inter; fallback Arial. + * + * @module design-system + */ + +'use strict'; + +// --------------------------------------------------------------------------- +// 1. PALETTE +// --------------------------------------------------------------------------- + +const palette = { + bg: { + primary: '#0A1A2A', // main slide background + elevated: '#142B3F', // cards, callouts, panels + code: '#0F1E2E', // code block background (slightly darker) + overlay: '#1B2F44', // hover / focus surfaces + }, + text: { + primary: '#E6F0F7', + secondary: '#B5C4D1', + muted: '#8A9AAB', + inverse: '#0A1A2A', // for text on bright accents + }, + accent: { + primary: '#219EBC', // teal, default accent + secondary: '#FFB703', // gold, important emphasis + tertiary: '#8ECAE6', // light blue, soft accent + }, + border: { + subtle: '#233A4F', + strong: '#3A5670', + accent: '#219EBC', + }, + code: { + keyword: '#C586C0', // def, class, import, return + string: '#CE9178', + number: '#B5CEA8', + comment: '#6A9955', + function: '#DCDCAA', + builtin: '#4EC9B0', + text: '#D4D4D4', // default code body + bg: '#0F1E2E', + lineHighlight: '#1F2F44', + }, + state: { + info: '#219EBC', + success: '#4EC9B0', + warning: '#FFB703', + danger: '#F48771', + infoBg: '#102A38', + successBg: '#0F2A28', + warningBg: '#3A2A0F', + dangerBg: '#3A1A14', + }, +}; + +// --------------------------------------------------------------------------- +// 2. TYPOGRAPHY +// --------------------------------------------------------------------------- + +const fonts = { + code: 'JetBrains Mono', + ui: 'Inter', + fallback: 'Arial', // universal fallback, supports Cyrillic +}; + +const sizes = { + h1: 36, + h2: 28, + h3: 20, + body: 14, + code: 12, + caption: 10, + eyebrow: 10, +}; + +const spacing = { + page: 0.5, + card_pad: 0.25, + gap: 0.15, +}; + +// --------------------------------------------------------------------------- +// 3. LAYOUTS +// --------------------------------------------------------------------------- + +const layouts = { + LAYOUT_16x9: 'LAYOUT_16x9', + HEADER_Y: 0.4, + CONTENT_TOP: 1.45, + CONTENT_BOTTOM: 5.05, + FOOTER_Y: 5.25, +}; + +// --------------------------------------------------------------------------- +// 4. THEME (bundled export of the above) +// --------------------------------------------------------------------------- + +const theme = { + palette, + fonts, + sizes, + spacing, + layouts, +}; + +// --------------------------------------------------------------------------- +// 5. HELPERS +// --------------------------------------------------------------------------- + +/** + * Resolve a font that always carries the Arial fallback. + * pptxgenjs lets us pass an array; PowerPoint will pick the first available. + */ +function withFallback(family) { + return [family, fonts.fallback]; +} + +/** + * Paint the slide background. + * @param {object} slide - pptxgenjs slide instance + * @param {object} _pres - pptxgenjs pres (kept for signature symmetry) + * @param {object} t - theme bundle + */ +function slideBase(slide, _pres, t) { + slide.background = { color: t.palette.bg.primary }; +} + +/** + * Add a header band: eyebrow (small caps, accent) + section number + title. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {string} [opts.section] - small caps section label (e.g. "Stage 2") + * @param {number} [opts.sectionNumber] - large section number shown on the right + * @param {string} opts.title - main title (h1) + * @param {string} [opts.eyebrow] - eyebrow text (e.g. "STAGE 2: CHAINS") + */ +function addHeader(slide, pres, t, opts) { + const o = opts || {}; + const margin = t.spacing.page; + const titleY = t.layouts.HEADER_Y + 0.4; + const titleSize = o.titleSize || t.sizes.h2; // default to 28pt -- fits two-line titles without overflow + const titleW = o.sectionNumber != null ? 8.4 : 9.0; + + if (o.eyebrow) { + slide.addText(o.eyebrow, { + x: margin, + y: t.layouts.HEADER_Y, + w: 6.0, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.accent.primary, + bold: true, + charSpacing: 4, + }); + } else if (o.section) { + slide.addText(o.section, { + x: margin, + y: t.layouts.HEADER_Y, + w: 6.0, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.accent.primary, + bold: true, + charSpacing: 4, + }); + } + + slide.addText(o.title || '', { + x: margin, + y: titleY, + w: titleW, + h: 0.75, + fontFace: withFallback(t.fonts.ui), + fontSize: titleSize, + color: t.palette.text.primary, + bold: true, + valign: 'middle', + fit: 'shrink', + }); + + if (o.sectionNumber != null) { + slide.addText(String(o.sectionNumber), { + x: 9.0, + y: t.layouts.HEADER_Y - 0.05, + w: 0.7, + h: 1.0, + fontFace: withFallback(t.fonts.ui), + fontSize: 56, + color: t.palette.accent.secondary, + bold: true, + align: 'right', + valign: 'top', + }); + } + + // Hairline separator below the header band + slide.addShape(pres.ShapeType.line, { + x: margin, + y: 1.3, + w: 10.0 - margin * 2, + h: 0, + line: { color: t.palette.border.subtle, width: 0.75 }, + }); +} + +/** + * Render a code block as a card with monospaced text. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {string} opts.code + * @param {number} opts.x + * @param {number} opts.y + * @param {number} opts.w + * @param {number} opts.h + * @param {string} [opts.language='python'] + * @param {string} [opts.filePath] - optional, renders as "// path:line" header + * @param {number} [opts.startLine=1] + * @param {Array} [opts.highlightLines] - 1-based line numbers + */ +function addCodeBlock(slide, pres, t, opts) { + const o = opts || {}; + const code = String(o.code || ''); + const x = o.x; + const y = o.y; + const w = o.w; + const h = o.h; + const headerH = o.filePath ? 0.3 : 0; + const radius = 0.08; + + // Card background + slide.addShape(pres.ShapeType.roundRect, { + x: x, + y: y, + w: w, + h: h, + fill: { color: t.palette.code.bg }, + line: { color: t.palette.border.subtle, width: 0.75 }, + rectRadius: radius, + }); + + // Optional header strip with file path + if (o.filePath) { + const startLine = o.startLine || 1; + const lineCount = code.split('\n').length; + const endLine = startLine + lineCount - 1; + slide.addText( + '// ' + o.filePath + ':' + startLine + '-' + endLine, + { + x: x + t.spacing.card_pad, + y: y + 0.05, + w: w - t.spacing.card_pad * 2, + h: 0.22, + fontFace: withFallback(t.fonts.code), + fontSize: t.sizes.caption, + color: t.palette.text.muted, + italic: true, + } + ); + } + + // Body code + const lines = code.split('\n'); + const hasHighlight = + Array.isArray(o.highlightLines) && o.highlightLines.length > 0; + const highlightedSet = hasHighlight + ? new Set(o.highlightLines.map(function (n) { return n; })) + : null; + + // Pre-compute line height to fit within available area. + const codeAreaY = y + headerH + 0.05; + const codeAreaH = h - headerH - 0.1; + // Use a tighter font + leading so 20-line snippets fit in 3-inch cards. + const fontSize = o.fontSize || 11; + const lineH = (fontSize / 72) * 1.25; // inches per line at 1.25 leading + + const text = lines.map(function (line, idx) { + const lineNum = idx + 1; + const isHi = highlightedSet && highlightedSet.has(lineNum); + const prefix = isHi ? '> ' : ' '; + return { + text: prefix + (line.length === 0 ? ' ' : line) + '\n', + options: { + fontFace: withFallback(t.fonts.code), + fontSize: fontSize, + color: isHi ? t.palette.text.primary : t.palette.code.text, + bold: false, + highlight: isHi ? t.palette.code.lineHighlight : undefined, + }, + }; + }); + + slide.addText(text, { + x: x + t.spacing.card_pad, + y: codeAreaY, + w: w - t.spacing.card_pad * 2, + h: codeAreaH, + valign: 'top', + paraSpaceBefore: 0, + paraSpaceAfter: 0, + lineSpacingMultiple: 1.0, + isTextBox: true, + }); + + // Caption warning if content may overflow. + const maxLines = Math.floor(codeAreaH / lineH); + if (lines.length > maxLines) { + // Best-effort: append a small note. PowerPoint won't clip the text, but + // it will overflow the card visually. Caller should reduce fontSize or + // split the snippet. + slide.addText( + '// note: snippet has ' + lines.length + ' lines, card fits ~' + maxLines, + { + x: x + t.spacing.card_pad, + y: y + h - 0.22, + w: w - t.spacing.card_pad * 2, + h: 0.18, + fontFace: withFallback(t.fonts.code), + fontSize: t.sizes.caption, + color: t.palette.state.warning, + italic: true, + } + ); + } +} + +/** + * Render a code block with line highlighting. + * Convenience wrapper over addCodeBlock. + */ +function addCodeBlockWithHighlight(slide, pres, t, opts) { + return addCodeBlock(slide, pres, t, Object.assign({}, opts, { + highlightLines: opts.lines || opts.highlightLines, + })); +} + +/** + * Add a callout panel (info / warning / success / danger). + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {number} opts.x + * @param {number} opts.y + * @param {number} opts.w + * @param {number} opts.h + * @param {('info'|'warning'|'success'|'danger')} [opts.kind='info'] + * @param {string} opts.text + * @param {string} [opts.title] + */ +function addCallout(slide, pres, t, opts) { + const o = opts || {}; + const kind = o.kind || 'info'; + const accent = t.palette.state[kind] || t.palette.state.info; + const bgKey = kind + 'Bg'; + const bg = t.palette.state[bgKey] || t.palette.state.infoBg; + + // Background card + slide.addShape(pres.ShapeType.roundRect, { + x: o.x, + y: o.y, + w: o.w, + h: o.h, + fill: { color: bg }, + line: { color: accent, width: 1 }, + rectRadius: 0.08, + }); + + // Left accent bar + slide.addShape(pres.ShapeType.rect, { + x: o.x, + y: o.y, + w: 0.08, + h: o.h, + fill: { color: accent }, + line: { type: 'none' }, + }); + + // Label + slide.addText((kind || 'info').toUpperCase(), { + x: o.x + 0.25, + y: o.y + 0.1, + w: o.w - 0.35, + h: 0.25, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: accent, + bold: true, + charSpacing: 4, + }); + + // Optional title + let bodyY = o.y + 0.4; + let bodyH = o.h - 0.5; + if (o.title) { + slide.addText(o.title, { + x: o.x + 0.25, + y: o.y + 0.35, + w: o.w - 0.35, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.h3, + color: t.palette.text.primary, + bold: true, + }); + bodyY = o.y + 0.7; + bodyH = o.h - 0.8; + } + + slide.addText(o.text || '', { + x: o.x + 0.25, + y: bodyY, + w: o.w - 0.35, + h: bodyH, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.secondary, + valign: 'top', + }); +} + +/** + * Two-column pros / cons panel. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {number} opts.x + * @param {number} opts.y + * @param {number} opts.w + * @param {number} opts.h + * @param {string[]} opts.pros + * @param {string[]} opts.cons + */ +function addProsCons(slide, pres, t, opts) { + const o = opts || {}; + const gap = 0.2; + const colW = (o.w - gap) / 2; + + // Pros card + slide.addShape(pres.ShapeType.roundRect, { + x: o.x, + y: o.y, + w: colW, + h: o.h, + fill: { color: t.palette.state.successBg }, + line: { color: t.palette.state.success, width: 1 }, + rectRadius: 0.08, + }); + slide.addText('PROS', { + x: o.x + 0.2, + y: o.y + 0.1, + w: colW - 0.3, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.state.success, + bold: true, + charSpacing: 4, + }); + + const prosBody = (o.pros || []).map(function (item) { + return { + text: '+ ' + item + '\n', + options: { + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.primary, + }, + }; + }); + slide.addText(prosBody, { + x: o.x + 0.2, + y: o.y + 0.45, + w: colW - 0.3, + h: o.h - 0.55, + valign: 'top', + paraSpaceAfter: 4, + }); + + // Cons card + const cx = o.x + colW + gap; + slide.addShape(pres.ShapeType.roundRect, { + x: cx, + y: o.y, + w: colW, + h: o.h, + fill: { color: t.palette.state.dangerBg }, + line: { color: t.palette.state.danger, width: 1 }, + rectRadius: 0.08, + }); + slide.addText('CONS', { + x: cx + 0.2, + y: o.y + 0.1, + w: colW - 0.3, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.state.danger, + bold: true, + charSpacing: 4, + }); + + const consBody = (o.cons || []).map(function (item) { + return { + text: '- ' + item + '\n', + options: { + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.primary, + }, + }; + }); + slide.addText(consBody, { + x: cx + 0.2, + y: o.y + 0.45, + w: colW - 0.3, + h: o.h - 0.55, + valign: 'top', + paraSpaceAfter: 4, + }); +} + +/** + * Add a page number in the bottom-right corner. + */ +function addPageNumber(slide, _pres, t, num) { + slide.addText(String(num), { + x: 9.2, + y: t.layouts.FOOTER_Y, + w: 0.6, + h: 0.25, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.caption, + color: t.palette.text.muted, + align: 'right', + valign: 'middle', + }); +} + +/** + * Add a section divider slide: a large numeric label + title + intro paragraph. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {number} opts.number + * @param {string} opts.title + * @param {string} [opts.intro] + * @param {string} [opts.eyebrow] + */ +function addSectionDivider(slide, pres, t, opts) { + const o = opts || {}; + + slideBase(slide, pres, t); + + // Giant number on the left + slide.addText(String(o.number), { + x: 0.5, + y: 1.0, + w: 3.5, + h: 3.5, + fontFace: withFallback(t.fonts.ui), + fontSize: 220, + color: t.palette.accent.primary, + bold: true, + valign: 'middle', + }); + + // Vertical divider + slide.addShape(pres.ShapeType.line, { + x: 4.2, + y: 1.4, + w: 0, + h: 2.8, + line: { color: t.palette.border.subtle, width: 1 }, + }); + + // Title + if (o.eyebrow) { + slide.addText(o.eyebrow, { + x: 4.5, + y: 1.4, + w: 5.0, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.accent.secondary, + bold: true, + charSpacing: 4, + }); + } + + slide.addText(o.title || '', { + x: 4.5, + y: o.eyebrow ? 1.7 : 1.4, + w: 5.0, + h: 1.0, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.h1, + color: t.palette.text.primary, + bold: true, + valign: 'top', + }); + + if (o.intro) { + slide.addText(o.intro, { + x: 4.5, + y: 2.8, + w: 5.0, + h: 1.6, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.secondary, + valign: 'top', + }); + } +} + +/** + * Add an italic source line, e.g. "source: python.langchain.com/..." + * @param {object} slide + * @param {object} _pres + * @param {object} t + * @param {object} opts + * @param {string} opts.source + * @param {number} [opts.x] + * @param {number} [opts.y] + * @param {number} [opts.w] + */ +function addSourceLine(slide, _pres, t, opts) { + const o = opts || {}; + slide.addText('source: ' + (o.source || ''), { + x: o.x != null ? o.x : t.spacing.page, + y: o.y != null ? o.y : t.layouts.FOOTER_Y + 0.05, + w: o.w != null ? o.w : 7.0, + h: 0.25, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.caption, + color: t.palette.text.muted, + italic: true, + valign: 'middle', + }); +} + +// --------------------------------------------------------------------------- +// 6. OPTIONAL: PYTHON SYNTAX HIGHLIGHTING (pygments via subprocess) +// --------------------------------------------------------------------------- + +/** + * Best-effort Python syntax highlighter. + * Calls the `pygmentize` CLI from pygments if available. + * Returns an array of { text, color } tokens. + * On any failure (missing binary, non-zero exit, parse error) returns a + * single-token array with the whole snippet in the default code text color + * so that the slide still renders something. + * + * @param {string} code - Python source code + * @returns {Array<{text: string, color: string}>} + */ +function highlightPython(code) { + const defaultColor = palette.code.text; + const text = String(code || ''); + if (!text) return [{ text: '', color: defaultColor }]; + + const { spawnSync } = require('child_process'); + + let result; + try { + result = spawnSync( + 'pygmentize', + ['-f', 'json', '-l', 'python'], + { input: text, encoding: 'utf8', timeout: 4000 } + ); + } catch (e) { + return [{ text: text, color: defaultColor }]; + } + + if (result.error || result.status !== 0) { + return [{ text: text, color: defaultColor }]; + } + + let parsed; + try { + parsed = JSON.parse(result.stdout); + } catch (e) { + return [{ text: text, color: defaultColor }]; + } + + // Pygments JSON output: { tokens: [ [type, value], ... ] } + const tokens = Array.isArray(parsed.tokens) ? parsed.tokens : []; + const out = []; + for (const tok of tokens) { + if (!Array.isArray(tok) || tok.length < 2) continue; + const [type, value] = tok; + out.push({ + text: String(value), + color: mapPygmentsTokenToColor(type) || defaultColor, + }); + } + return out.length > 0 ? out : [{ text: text, color: defaultColor }]; +} + +/** + * Map a pygments token type to a palette color. + * Falls back to the default code text color. + */ +function mapPygmentsTokenToColor(type) { + if (!type) return null; + // Pygments short token types we care about for Python: + // Keyword, Keyword.Namespace, Keyword.Constant, String, Number, + // Comment, Comment.Single, Name.Function, Name.Builtin, + // Operator, Punctuation, Text, Error + if (type === 'Keyword' || type.startsWith('Keyword.')) { + return palette.code.keyword; + } + if (type.startsWith('String')) { + return palette.code.string; + } + if (type === 'Number') { + return palette.code.number; + } + if (type.startsWith('Comment')) { + return palette.code.comment; + } + if (type === 'Name.Function' || type === 'Name.Function.Magic') { + return palette.code.function; + } + if (type === 'Name.Builtin' || type === 'Name.Builtin.Pseudo') { + return palette.code.builtin; + } + return null; +} + +// --------------------------------------------------------------------------- +// 7. EXPORTS +// --------------------------------------------------------------------------- + +const helpers = { + slideBase, + addHeader, + addCodeBlock, + addCodeBlockWithHighlight, + addCallout, + addProsCons, + addPageNumber, + addSectionDivider, + addSourceLine, + withFallback, + highlightPython, +}; + +module.exports = { + theme: theme, + palette: palette, + fonts: fonts, + sizes: sizes, + spacing: spacing, + layouts: layouts, + helpers: helpers, +}; \ No newline at end of file diff --git a/slides/section1-chains/previews/slide-01.png b/slides/section1-chains/previews/slide-01.png new file mode 100644 index 0000000..303c215 Binary files /dev/null and b/slides/section1-chains/previews/slide-01.png differ diff --git a/slides/section1-chains/previews/slide-02.png b/slides/section1-chains/previews/slide-02.png new file mode 100644 index 0000000..9c03ee1 Binary files /dev/null and b/slides/section1-chains/previews/slide-02.png differ diff --git a/slides/section1-chains/previews/slide-03.png b/slides/section1-chains/previews/slide-03.png new file mode 100644 index 0000000..18f858b Binary files /dev/null and b/slides/section1-chains/previews/slide-03.png differ diff --git a/slides/section1-chains/previews/slide-04.png b/slides/section1-chains/previews/slide-04.png new file mode 100644 index 0000000..d4e7369 Binary files /dev/null and b/slides/section1-chains/previews/slide-04.png differ diff --git a/slides/section1-chains/previews/slide-05.png b/slides/section1-chains/previews/slide-05.png new file mode 100644 index 0000000..1f92331 Binary files /dev/null and b/slides/section1-chains/previews/slide-05.png differ diff --git a/slides/section1-chains/previews/slide-06.png b/slides/section1-chains/previews/slide-06.png new file mode 100644 index 0000000..5ee5811 Binary files /dev/null and b/slides/section1-chains/previews/slide-06.png differ diff --git a/slides/section1-chains/previews/slide-07.png b/slides/section1-chains/previews/slide-07.png new file mode 100644 index 0000000..140697e Binary files /dev/null and b/slides/section1-chains/previews/slide-07.png differ diff --git a/slides/section1-chains/previews/slide-08.png b/slides/section1-chains/previews/slide-08.png new file mode 100644 index 0000000..4f97e1d Binary files /dev/null and b/slides/section1-chains/previews/slide-08.png differ diff --git a/slides/section1-chains/previews/slide-09.png b/slides/section1-chains/previews/slide-09.png new file mode 100644 index 0000000..b2c0b03 Binary files /dev/null and b/slides/section1-chains/previews/slide-09.png differ diff --git a/slides/section1-chains/previews/slide-10.png b/slides/section1-chains/previews/slide-10.png new file mode 100644 index 0000000..0b09f82 Binary files /dev/null and b/slides/section1-chains/previews/slide-10.png differ diff --git a/slides/section1-chains/previews/slide-11.png b/slides/section1-chains/previews/slide-11.png new file mode 100644 index 0000000..0bd4200 Binary files /dev/null and b/slides/section1-chains/previews/slide-11.png differ diff --git a/slides/section1-chains/previews/slide-12.png b/slides/section1-chains/previews/slide-12.png new file mode 100644 index 0000000..ccd4355 Binary files /dev/null and b/slides/section1-chains/previews/slide-12.png differ diff --git a/slides/section1-chains/previews/slide-13.png b/slides/section1-chains/previews/slide-13.png new file mode 100644 index 0000000..1596ad2 Binary files /dev/null and b/slides/section1-chains/previews/slide-13.png differ diff --git a/slides/section1-chains/previews/slide-14.png b/slides/section1-chains/previews/slide-14.png new file mode 100644 index 0000000..a2a1079 Binary files /dev/null and b/slides/section1-chains/previews/slide-14.png differ diff --git a/slides/section1-chains/previews/slide-15.png b/slides/section1-chains/previews/slide-15.png new file mode 100644 index 0000000..71bee25 Binary files /dev/null and b/slides/section1-chains/previews/slide-15.png differ diff --git a/slides/section1-chains/previews/slide-16.png b/slides/section1-chains/previews/slide-16.png new file mode 100644 index 0000000..b5cb4a9 Binary files /dev/null and b/slides/section1-chains/previews/slide-16.png differ diff --git a/slides/section1-chains/previews/slide-17.png b/slides/section1-chains/previews/slide-17.png new file mode 100644 index 0000000..be6c7f7 Binary files /dev/null and b/slides/section1-chains/previews/slide-17.png differ diff --git a/slides/section1-chains/previews/slide-18.png b/slides/section1-chains/previews/slide-18.png new file mode 100644 index 0000000..f295d39 Binary files /dev/null and b/slides/section1-chains/previews/slide-18.png differ diff --git a/slides/section1-chains/previews/slide-19.png b/slides/section1-chains/previews/slide-19.png new file mode 100644 index 0000000..edfd5f1 Binary files /dev/null and b/slides/section1-chains/previews/slide-19.png differ diff --git a/slides/section1-chains/previews/slide-20.png b/slides/section1-chains/previews/slide-20.png new file mode 100644 index 0000000..eaf2317 Binary files /dev/null and b/slides/section1-chains/previews/slide-20.png differ diff --git a/slides/section1-chains/previews/slide-21.png b/slides/section1-chains/previews/slide-21.png new file mode 100644 index 0000000..dc6d5f5 Binary files /dev/null and b/slides/section1-chains/previews/slide-21.png differ diff --git a/slides/section1-chains/previews/slide-22.png b/slides/section1-chains/previews/slide-22.png new file mode 100644 index 0000000..c27e781 Binary files /dev/null and b/slides/section1-chains/previews/slide-22.png differ diff --git a/slides/section1-chains/previews/slide-23.png b/slides/section1-chains/previews/slide-23.png new file mode 100644 index 0000000..3b7649e Binary files /dev/null and b/slides/section1-chains/previews/slide-23.png differ diff --git a/slides/section1-chains/previews/slide-24.png b/slides/section1-chains/previews/slide-24.png new file mode 100644 index 0000000..296c010 Binary files /dev/null and b/slides/section1-chains/previews/slide-24.png differ diff --git a/slides/section1-chains/previews/slide-25.png b/slides/section1-chains/previews/slide-25.png new file mode 100644 index 0000000..ba6fd2a Binary files /dev/null and b/slides/section1-chains/previews/slide-25.png differ diff --git a/slides/section1-chains/previews/slide-26.png b/slides/section1-chains/previews/slide-26.png new file mode 100644 index 0000000..468c05a Binary files /dev/null and b/slides/section1-chains/previews/slide-26.png differ diff --git a/slides/section1-chains/previews/slide-27.png b/slides/section1-chains/previews/slide-27.png new file mode 100644 index 0000000..6ca68b7 Binary files /dev/null and b/slides/section1-chains/previews/slide-27.png differ diff --git a/slides/section1-chains/section1.pdf b/slides/section1-chains/section1.pdf new file mode 100644 index 0000000..9b1430b Binary files /dev/null and b/slides/section1-chains/section1.pdf differ diff --git a/slides/section1-chains/section1.pptx b/slides/section1-chains/section1.pptx new file mode 100644 index 0000000..50c39d9 Binary files /dev/null and b/slides/section1-chains/section1.pptx differ diff --git a/slides/section1-chains/slide-01.js b/slides/section1-chains/slide-01.js new file mode 100644 index 0000000..acd6865 --- /dev/null +++ b/slides/section1-chains/slide-01.js @@ -0,0 +1,153 @@ +// Slide 01: Section cover -- Stage 1 / Chains +// Asymmetric layout: big section number + title block on left, code-window mock on right. +// Sets the stage: "what is LangChain, why it's the foundation for everything else." + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + // Left vertical accent stripe -- teal + slide.addShape(pres.ShapeType.rect, { + x: 0, y: 0, w: 0.25, h: 5.625, + fill: { color: theme.palette.accent.primary }, + line: { type: 'none' }, + }); + + // Top tag pill: section meta + slide.addShape(pres.ShapeType.roundRect, { + x: 0.7, y: 0.55, w: 2.4, h: 0.36, + fill: { color: theme.palette.accent.primary }, + line: { type: 'none' }, + rectRadius: 0.18, + }); + slide.addText('STAGE 1 | CHAINS', { + x: 0.7, y: 0.55, w: 2.4, h: 0.36, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, bold: true, + color: theme.palette.bg.primary, + align: 'center', valign: 'middle', + charSpacing: 4, margin: 0, + }); + + // Main title -- very large, but constrained to left half so it does not + // collide with the LCEL pipeline mock on the right. + slide.addText('LangChain 1.0', { + x: 0.7, y: 1.15, w: 5.9, h: 1.0, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 56, bold: true, + color: theme.palette.text.primary, + align: 'left', valign: 'middle', + margin: 0, + }); + + // Subtitle + slide.addText('chains, LCEL, agents, retrievers', { + x: 0.7, y: 2.15, w: 9, h: 0.55, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 26, + color: theme.palette.accent.tertiary, + align: 'left', valign: 'middle', + margin: 0, + }); + + // Description block -- what this section covers + slide.addText( + 'Фундамент: композиция через LCEL (| pipe), унифицированный init_chat_model, ' + + 'output parsers, retrievers, tools, память, middleware. ' + + 'Всё, что нужно, чтобы собрать production-ready LLM-приложение ' + + 'до перехода к LangGraph и Deep Agents.', + { + x: 0.7, y: 2.85, w: 5.7, h: 1.6, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 13, + color: theme.palette.text.secondary, + align: 'left', valign: 'top', + margin: 0, + } + ); + + // Decorative right-side block -- abstract "LCEL pipeline" mock + // Three boxes connected by | pipes, evoking prompt | model | parser + const codeX = 6.7; + const codeY = 1.55; + const codeW = 2.85; + const codeH = 3.05; + + slide.addShape(pres.ShapeType.roundRect, { + x: codeX, y: codeY, w: codeW, h: codeH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.1, + }); + + // Title inside the card + slide.addText('prompt | model | parser', { + x: codeX + 0.15, y: codeY + 0.2, w: codeW - 0.3, h: 0.4, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 14, bold: true, + color: theme.palette.accent.tertiary, + align: 'center', valign: 'middle', + margin: 0, + }); + + // Three mock boxes connected vertically + const boxes = [ + { label: 'ChatPromptTemplate', color: theme.palette.accent.tertiary, y: codeY + 0.75 }, + { label: 'ChatModel', color: theme.palette.accent.primary, y: codeY + 1.55 }, + { label: 'OutputParser', color: theme.palette.accent.secondary, y: codeY + 2.35 }, + ]; + boxes.forEach((b) => { + slide.addShape(pres.ShapeType.roundRect, { + x: codeX + 0.4, y: b.y, w: codeW - 0.8, h: 0.5, + fill: { color: theme.palette.bg.code }, + line: { color: b.color, width: 1.25 }, + rectRadius: 0.06, + }); + slide.addText(b.label, { + x: codeX + 0.4, y: b.y, w: codeW - 0.8, h: 0.5, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 13, bold: true, + color: b.color, + align: 'center', valign: 'middle', + margin: 0, + }); + }); + + // Vertical pipe marks between boxes + const pipeXs = [codeX + 0.7, codeX + 1.4, codeX + 2.1]; + [codeY + 1.28, codeY + 2.08].forEach((yPipe) => { + slide.addText('|', { + x: codeX + 0.4, y: yPipe, w: codeW - 0.8, h: 0.22, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 16, bold: true, + color: theme.palette.text.muted, + align: 'center', valign: 'middle', + margin: 0, + }); + }); + + // Caption under the mock + slide.addText('композиция через LCEL', { + x: codeX, y: codeY + codeH + 0.05, w: codeW, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 11, italic: true, + color: theme.palette.text.muted, + align: 'center', valign: 'middle', margin: 0, + }); + + // Bottom meta strip + slide.addText('27 СЛАЙДОВ | PYTHON >= 1.0 | ЛЕТО 2026', { + x: 0.7, y: 5.05, w: 9, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, + color: theme.palette.text.muted, + align: 'left', valign: 'middle', + charSpacing: 3, margin: 0, + }); + + ds.helpers.addPageNumber(slide, pres, theme, 1); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-02.js b/slides/section1-chains/slide-02.js new file mode 100644 index 0000000..c80f9c1 --- /dev/null +++ b/slides/section1-chains/slide-02.js @@ -0,0 +1,116 @@ +// Slide 02: What is LangChain in 2025+ -- content slide with key facts +// Sets the conceptual frame: a framework, not just a wrapper. Three pillars. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Раздел 1', + title: 'Что такое LangChain в 2025+', + sectionNumber: 1, + }); + + // Lead paragraph + slide.addText( + 'LangChain -- Python/JS фреймворк для сборки LLM-приложений и агентов. ' + + 'С версии 1.0 (релиз 22.10.2025) он построен поверх LangGraph-runtime и ' + + 'позиционируется как "самый быстрый способ собрать агента с любым провайдером моделей".', + { + x: 0.5, y: 1.5, w: 9.0, h: 0.7, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 13, + color: theme.palette.text.secondary, + align: 'left', valign: 'top', + margin: 0, + } + ); + + // Three pillar cards + const pillars = [ + { + title: 'LCEL', + sub: 'LangChain Expression Language', + body: 'Декларативная композиция через pipe-оператор. ' + + 'Любая цепочка -- Runnable. invoke/batch/stream/async работают одинаково.', + }, + { + title: 'create_agent', + sub: 'унифицированный вход', + body: 'Один вызов вместо зоопарка legacy agent-типов. ' + + 'Построен на LangGraph -- получаешь durable execution бесплатно.', + }, + { + title: 'Middleware', + sub: 'cross-cutting hooks', + body: 'before_model / after_model / before_tool / after_tool. ' + + 'HITL, PII-редакция, summarization -- first-class встроенные middleware.', + }, + ]; + + const cardW = 3.0; + const cardH = 2.2; + const gap = 0.15; + const startX = 0.5; + const cardY = 2.4; + + pillars.forEach((p, i) => { + const x = startX + i * (cardW + gap); + slide.addShape(pres.ShapeType.roundRect, { + x: x, y: cardY, w: cardW, h: cardH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.1, + }); + // Left accent bar + slide.addShape(pres.ShapeType.rect, { + x: x, y: cardY, w: 0.08, h: cardH, + fill: { color: theme.palette.accent.primary }, + line: { type: 'none' }, + }); + slide.addText(p.title, { + x: x + 0.25, y: cardY + 0.2, w: cardW - 0.4, h: 0.45, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 22, bold: true, + color: theme.palette.accent.primary, + valign: 'middle', margin: 0, + }); + slide.addText(p.sub, { + x: x + 0.25, y: cardY + 0.65, w: cardW - 0.4, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, italic: true, + color: theme.palette.text.muted, + valign: 'middle', margin: 0, + }); + slide.addText(p.body, { + x: x + 0.25, y: cardY + 1.05, w: cardW - 0.4, h: cardH - 1.2, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 12, + color: theme.palette.text.secondary, + valign: 'top', margin: 0, + }); + }); + + // Bottom meta strip -- facts + slide.addText( + '~140k звезд GitHub | MIT | Python: langchain 1.3.10 / langchain-core 1.4.8 | JS: @langchain/langchain', + { + x: 0.5, y: 4.8, w: 9.0, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, italic: true, + color: theme.palette.text.muted, + align: 'left', valign: 'middle', + margin: 0, + } + ); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'github.com/langchain-ai/langchain README + changelog.langchain.com', + }); + ds.helpers.addPageNumber(slide, pres, theme, 2); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-03.js b/slides/section1-chains/slide-03.js new file mode 100644 index 0000000..1807e8d --- /dev/null +++ b/slides/section1-chains/slide-03.js @@ -0,0 +1,49 @@ +// Slide 03: Installation -- pip install + provider packages +// Code slide: shows the actual install commands a user needs to run. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Установка', + title: 'pip install -- и сразу в дело', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'bash', + filePath: 'shell/install.sh', + code: [ + '# 1. Core -- обязательно для всех остальных пакетов', + 'pip install langchain', + '', + '# 2. Provider-интеграции -- ставим только те, что нужны', + 'pip install langchain-openai', + 'pip install langchain-anthropic', + 'pip install langchain-google', + '', + '# 3. (опционально) дополнительные интеграции', + 'pip install langchain-community # ~700 community-пакетов', + 'pip install langchain-classic # legacy chains/AgentExecutor', + ].join('\n'), + }); + + // Callout: ключевая мысль + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'langchain-core тянется автоматически как зависимость langchain -- отдельно ставить не нужно.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/introduction/#installation', + }); + ds.helpers.addPageNumber(slide, pres, theme, 3); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-04.js b/slides/section1-chains/slide-04.js new file mode 100644 index 0000000..c3ad16e --- /dev/null +++ b/slides/section1-chains/slide-04.js @@ -0,0 +1,57 @@ +// Slide 04: Hello world -- first chain with init_chat_model +// Code slide: the simplest possible working example. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Hello world', + title: 'Первый вызов модели', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 6.4, h: 3.25, + language: 'python', + filePath: 'examples/hello.py', + code: [ + '# Один универсальный инициализатор для всех провайдеров', + 'from langchain.chat_models import init_chat_model', + '', + '# Формат: ":"', + 'model = init_chat_model("openai:gpt-4.1-mini")', + '', + '# invoke -> BaseMessage; нам нужен .content', + 'result = model.invoke("Say hello in one sentence")', + 'print(result.content)', + '', + '# >>> "Hello! How can I help you today?"', + ].join('\n'), + }); + + // Right side: explanation card + ds.helpers.addCallout(slide, pres, theme, { + x: 7.1, y: 1.5, w: 2.4, h: 1.55, + kind: 'info', + title: 'init_chat_model', + text: 'Один фабричный вызов вместо ChatOpenAI / ChatAnthropic / ChatGoogle отдельно.', + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 7.1, y: 3.2, w: 2.4, h: 1.55, + kind: 'success', + title: 'Что вернется', + text: 'Объект AIMessage: content, response_metadata (usage, model_name), id, tool_calls.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/chat_models_universal_init/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 4); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-05.js b/slides/section1-chains/slide-05.js new file mode 100644 index 0000000..f5b706a --- /dev/null +++ b/slides/section1-chains/slide-05.js @@ -0,0 +1,52 @@ +// Slide 05: ChatModels -- provider routing through init_chat_model +// Code slide: how to switch providers without changing call sites. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'ChatModels', + title: 'Переключение провайдера -- одна строка', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/multi_provider.py', + code: [ + 'from langchain.chat_models import init_chat_model', + '', + '# OpenAI', + 'm_openai = init_chat_model("openai:gpt-4.1-mini")', + '', + '# Anthropic', + 'm_anthropic = init_chat_model("anthropic:claude-3-7-sonnet-latest")', + '', + '# Google Vertex AI', + 'm_google = init_chat_model("google_vertexai:gemini-2.0-flash")', + '', + '# Все три -- объекты BaseChatModel. Один и тот же .invoke()', + 'for m in [m_openai, m_anthropic, m_google]:', + ' print(type(m).__name__, ":", m.invoke("ping").content[:30])', + ].join('\n'), + }); + + // Bottom callout -- env vars + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'warning', + text: 'Нужны API-ключи в env: OPENAI_API_KEY / ANTHROPIC_API_KEY / GOOGLE_API_KEY -- провайдер-пакет сам их подхватит.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/concepts/chat_models/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 5); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-06.js b/slides/section1-chains/slide-06.js new file mode 100644 index 0000000..bc9e983 --- /dev/null +++ b/slides/section1-chains/slide-06.js @@ -0,0 +1,53 @@ +// Slide 06: Messages -- HumanMessage, AIMessage, SystemMessage, ToolMessage +// Code slide: the message contract, how to build multi-turn prompts. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Messages', + title: 'Стандартизированные сообщения', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/messages.py', + code: [ + 'from langchain.messages import (', + ' HumanMessage, AIMessage, SystemMessage, ToolMessage,', + ')', + 'from langchain.chat_models import init_chat_model', + '', + 'model = init_chat_model("openai:gpt-4.1-mini")', + '', + 'messages = [', + ' SystemMessage(content="You are a concise assistant."),', + ' HumanMessage(content="What is LCEL?"),', + ' AIMessage(content="LCEL = pipe-based composition in LangChain."),', + ' HumanMessage(content="Show a one-line example."),', + ']', + '', + 'response = model.invoke(messages)', + 'print(response.content)', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'С v1.0 контент -- стандартизированные content blocks: reasoning traces, citations, tool_call блоки.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/concepts/messages/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 6); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-07.js b/slides/section1-chains/slide-07.js new file mode 100644 index 0000000..7c92092 --- /dev/null +++ b/slides/section1-chains/slide-07.js @@ -0,0 +1,54 @@ +// Slide 07: Streaming -- .stream() yields chunks progressively +// Code slide: how to consume the model output as a stream. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Streaming', + title: 'Потоковый вывод -- token за токеном', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/streaming.py', + code: [ + 'from langchain.chat_models import init_chat_model', + '', + 'model = init_chat_model("openai:gpt-4.1-mini")', + '', + '# .stream() -> итератор по AIMessageChunk', + 'for chunk in model.stream("Write a haiku about Python"):', + ' # У каждого chunk-а есть .content (текст) и .response_metadata', + ' print(chunk.content, end="", flush=True)', + '', + 'print() # перевод строки после потока', + '', + '# Async-вариант: model.astream() -- то же самое в async-контексте', + 'import asyncio', + '', + 'async def main():', + ' async for chunk in model.astream("Write a haiku about async"):', + ' print(chunk.content, end="", flush=True)', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'success', + text: 'AIMessageChunk можно складывать через оператор + -- для буферизации и метрик.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/streaming/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 7); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-08.js b/slides/section1-chains/slide-08.js new file mode 100644 index 0000000..77b73b6 --- /dev/null +++ b/slides/section1-chains/slide-08.js @@ -0,0 +1,52 @@ +// Slide 08: ChatPromptTemplate -- from_messages + variables +// Code slide: declarative prompt construction with placeholders. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Prompts', + title: 'ChatPromptTemplate -- декларативно', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/prompt_basic.py', + code: [ + 'from langchain_core.prompts import ChatPromptTemplate', + 'from langchain.chat_models import init_chat_model', + '', + 'prompt = ChatPromptTemplate.from_messages([', + ' ("system", "Translate the following text to {language}."),', + ' ("human", "{text}"),', + '])', + '', + 'model = init_chat_model("openai:gpt-4.1-mini")', + '', + '# .invoke() принимает dict и подставляет переменные', + 'messages = prompt.invoke({"language": "French", "text": "Hello world"})', + 'print(messages.to_messages())', + '', + '# -> [SystemMessage(...), HumanMessage(...)] -- готов к model.invoke()', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'Кортежи ("role", "text") -- шорткат. Для сложного контента передавайте Message-объекты.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/concepts/prompt_templates/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 8); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-09.js b/slides/section1-chains/slide-09.js new file mode 100644 index 0000000..31e3276 --- /dev/null +++ b/slides/section1-chains/slide-09.js @@ -0,0 +1,61 @@ +// Slide 09: MessagesPlaceholder + few-shot examples +// Code slide: how to inject conversation history and demonstration examples. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Prompts', + title: 'MessagesPlaceholder + few-shot', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/prompt_fewshot.py', + code: [ + 'from langchain_core.prompts import (', + ' ChatPromptTemplate, MessagesPlaceholder, FewShotChatMessagePromptTemplate,', + ')', + '', + '# Few-shot блок: примеры "вопрос -> ответ"', + 'examples = [', + ' {"input": "2+2", "output": "4"},', + ' {"input": "3*3", "output": "9"},', + ']', + 'example_prompt = ChatPromptTemplate.from_messages([', + ' ("human", "{input}"),', + ' ("ai", "{output}"),', + '])', + 'few_shot = FewShotChatMessagePromptTemplate(', + ' example_prompt=example_prompt, examples=examples,', + ')', + '', + '# Сборка: system + few-shot + история + текущий вопрос', + 'prompt = ChatPromptTemplate.from_messages([', + ' ("system", "You are a math assistant."),', + ' few_shot,', + ' MessagesPlaceholder("history"),', + ' ("human", "{question}"),', + '])', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'success', + text: 'MessagesPlaceholder("history") -- дырка, в которую при invoke подставляется список прошлых сообщений.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/few_shot_examples_chat/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 9); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-10.js b/slides/section1-chains/slide-10.js new file mode 100644 index 0000000..60376b0 --- /dev/null +++ b/slides/section1-chains/slide-10.js @@ -0,0 +1,54 @@ +// Slide 10: StrOutputParser -- the most common parser +// Code slide: parse AIMessage.content down to plain str. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Output parsers', + title: 'StrOutputParser -- самый частый', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/parser_str.py', + code: [ + 'from langchain_core.prompts import ChatPromptTemplate', + 'from langchain_core.output_parsers import StrOutputParser', + 'from langchain.chat_models import init_chat_model', + '', + 'prompt = ChatPromptTemplate.from_messages([', + ' ("system", "Translate to French."),', + ' ("human", "{text}"),', + '])', + 'model = init_chat_model("openai:gpt-4.1-mini")', + '', + '# Склеиваем через LCEL: prompt | model | parser', + 'chain = prompt | model | StrOutputParser()', + '', + '# Теперь на выходе str, а не AIMessage', + 'result = chain.invoke({"text": "Hello world"})', + 'print(type(result).__name__, "->", result)', + '# >>> str -> "Bonjour le monde"', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'StrOutputParser просто достает .content из AIMessage. Без него пришлось бы делать result.content вручную.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/concepts/output_parsers/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 10); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-11.js b/slides/section1-chains/slide-11.js new file mode 100644 index 0000000..82db513 --- /dev/null +++ b/slides/section1-chains/slide-11.js @@ -0,0 +1,57 @@ +// Slide 11: PydanticOutputParser -- structured output via Pydantic schema +// Code slide: Pydantic model -> parser -> validated instance. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Output parsers', + title: 'PydanticOutputParser -- типизированный выход', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/parser_pydantic.py', + code: [ + 'from pydantic import BaseModel, Field', + 'from langchain_core.prompts import ChatPromptTemplate', + 'from langchain_core.output_parsers import PydanticOutputParser', + 'from langchain.chat_models import init_chat_model', + '', + 'class MovieReview(BaseModel):', + ' title: str = Field(description="Movie title")', + ' rating: int = Field(description="Rating from 1 to 10")', + ' summary: str = Field(description="One-sentence summary")', + '', + 'parser = PydanticOutputParser(pydantic_object=MovieReview)', + '', + 'prompt = ChatPromptTemplate.from_messages([', + ' ("system", "Extract review fields.\\n{format_instructions}"),', + ' ("human", "{review_text}"),', + ']).partial(format_instructions=parser.get_format_instructions())', + '', + 'chain = prompt | init_chat_model("openai:gpt-4.1-mini") | parser', + 'review = chain.invoke({"review_text": "Inception was brilliant. 9/10."})', + 'print(review.title, review.rating, review.summary)', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'success', + text: 'В v1.0 рекомендуется model.with_structured_output(Schema) -- он использует tool calling и точнее.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/pydantic_output_parser/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 11); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-12.js b/slides/section1-chains/slide-12.js new file mode 100644 index 0000000..19b453d --- /dev/null +++ b/slides/section1-chains/slide-12.js @@ -0,0 +1,54 @@ +// Slide 12: with_structured_output -- the recommended v1.0 way +// Code slide: model.with_structured_output(Schema) instead of legacy parsers. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Output parsers', + title: 'with_structured_output -- рекомендованный путь', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/structured.py', + code: [ + 'from pydantic import BaseModel, Field', + 'from langchain.chat_models import init_chat_model', + '', + 'class Weather(BaseModel):', + ' city: str = Field(description="City name")', + ' temperature_c: float = Field(description="Temperature in Celsius")', + ' conditions: str = Field(description="Weather summary")', + '', + '# Один вызов -- и модель возвращает типизированный Pydantic-объект', + 'model = init_chat_model("openai:gpt-4.1-mini")', + 'structured = model.with_structured_output(Weather)', + '', + 'result: Weather = structured.invoke("Weather in Paris?")', + 'print(result.city, result.temperature_c, result.conditions)', + '', + '# method="json_mode" -- если провайдер не поддерживает tool calling', + '# structured_json = model.with_structured_output(Weather, method="json_mode")', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'Под капотом: tool calling или provider-native JSON mode. Без extra LLM-вызовов, в один проход.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/structured_output/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 12); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-13.js b/slides/section1-chains/slide-13.js new file mode 100644 index 0000000..4c8e4c2 --- /dev/null +++ b/slides/section1-chains/slide-13.js @@ -0,0 +1,56 @@ +// Slide 13: LCEL -- the pipe operator and the Runnable protocol +// Code slide: every component is a Runnable, | composes them. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'LCEL', + title: '| -- декларативная композиция', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/lcel_intro.py', + code: [ + 'from langchain_core.prompts import ChatPromptTemplate', + 'from langchain_core.output_parsers import StrOutputParser', + 'from langchain.chat_models import init_chat_model', + '', + 'prompt = ChatPromptTemplate.from_messages([', + ' ("system", "You are a {style} assistant."),', + ' ("human", "{question}"),', + '])', + 'model = init_chat_model("openai:gpt-4.1-mini")', + '', + '# prompt, model, parser -- все три реализуют Runnable', + '# Оператор | склеивает их в один chain', + 'chain = prompt | model | StrOutputParser()', + '', + '# type(chain) -> RunnableSequence', + 'print(type(chain).__name__)', + '', + '# invoke -> dict на вход, str на выход', + 'print(chain.invoke({"style": "concise", "question": "What is LCEL?"}))', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'success', + text: 'Runnable -- единый контракт: invoke / batch / stream / ainvoke / abatch / astream / async stream.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/concepts/lcel/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 13); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-14.js b/slides/section1-chains/slide-14.js new file mode 100644 index 0000000..2b78549 --- /dev/null +++ b/slides/section1-chains/slide-14.js @@ -0,0 +1,53 @@ +// Slide 14: Runnable interface -- invoke / batch / stream +// Code slide: same chain, three execution modes -- no code changes needed. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Runnable', + title: 'invoke / batch / stream -- без смены кода', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/runnable_modes.py', + code: [ + 'chain = prompt | model | StrOutputParser()', + '', + '# 1. invoke -- один вход, один выход', + 'out_one = chain.invoke({"language": "French", "text": "Good morning"})', + '', + '# 2. batch -- список входов, список выходов (параллельно)', + 'out_many = chain.batch([', + ' {"language": "French", "text": "Good morning"},', + ' {"language": "German", "text": "Good morning"},', + ' {"language": "Spanish", "text": "Good morning"},', + '])', + 'print(out_many) # ["Bonjour", "Guten Morgen", "Buenos dias"]', + '', + '# 3. stream -- итератор по чанкам (стримит последний Runnable)', + 'for chunk in chain.stream({"language": "French", "text": "Stream me"}):', + ' print(chunk, end="", flush=True)', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'batch полезен для embedding-style задач. stream -- для UX с typewriter-эффектом в UI.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/batch/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 14); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-15.js b/slides/section1-chains/slide-15.js new file mode 100644 index 0000000..4c52683 --- /dev/null +++ b/slides/section1-chains/slide-15.js @@ -0,0 +1,58 @@ +// Slide 15: Async + configurable -- ainvoke/astream + chain.config +// Code slide: how to use chains from async code and override params at call site. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Runnable', + title: 'async + config -- полный контроль', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/runnable_async.py', + code: [ + 'import asyncio', + 'from langchain_core.runnables import ConfigurableField', + '', + '# Любой Runnable имеет ainvoke / astream / abatch', + 'async def main():', + ' # Один async-вызов', + ' result = await chain.ainvoke({"language": "French", "text": "Hi"})', + '', + ' # Async stream', + ' async for chunk in chain.astream({"language": "French", "text": "Hi"}):', + ' print(chunk, end="", flush=True)', + '', + 'asyncio.run(main())', + '', + '# configurable_fields -- параметры, которые можно переопределять', + '# на лету через config={"configurable": {...}}', + 'configurable_chain = init_chat_model("openai:gpt-4.1-mini",', + ' temperature=0.7).configurable_fields(', + ' temperature=ConfigurableField(id="temperature"),', + ')', + '# configurable_chain.invoke(messages, config={"configurable": {"temperature": 0.0}})', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'success', + text: 'configurable_fields + config={"configurable": {...}} -- паттерн для multi-tenant LLM-приложений.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/configurable/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 15); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-16.js b/slides/section1-chains/slide-16.js new file mode 100644 index 0000000..a39c7a2 --- /dev/null +++ b/slides/section1-chains/slide-16.js @@ -0,0 +1,56 @@ +// Slide 16: RunnableLambda + RunnablePassthrough -- custom logic in pipeline +// Code slide: insert arbitrary Python functions into an LCEL chain. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Runnable helpers', + title: 'RunnableLambda + RunnablePassthrough', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/runnable_lambda.py', + code: [ + 'from langchain_core.runnables import RunnableLambda, RunnablePassthrough', + '', + '# RunnableLambda -- оборачивает произвольную функцию', + 'upper = RunnableLambda(lambda x: x.upper())', + 'word_count = RunnableLambda(lambda text: {"text": text, "words": len(text.split())})', + '', + '# RunnablePassthrough -- пропускает вход дальше (для branching)', + 'passthrough = RunnablePassthrough()', + '', + '# Пример: текст -> uppercase -> посчитать слова -> смёрджить с оригиналом', + 'chain = (', + ' word_count', + ' | RunnablePassthrough.assign(upper=upper)', + ')', + '', + 'result = chain.invoke("hello world from langchain")', + 'print(result)', + '# {"text": "hello world from langchain",', + '# "words": 4, "upper": "HELLO WORLD FROM LANGCHAIN"}', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'RunnablePassthrough.assign -- добавляет новые ключи, сохраняя исходные. Идеален для RAG-style цепочек.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/functions/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 16); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-17.js b/slides/section1-chains/slide-17.js new file mode 100644 index 0000000..372b1de --- /dev/null +++ b/slides/section1-chains/slide-17.js @@ -0,0 +1,54 @@ +// Slide 17: RunnableParallel + RunnableBranch -- fan-out / conditional routing +// Code slide: parallel execution and if/else logic in a chain. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Runnable helpers', + title: 'Parallel + Branch -- fan-out и роутинг', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/runnable_parallel.py', + code: [ + 'from langchain_core.runnables import RunnableParallel, RunnableBranch', + '', + '# Parallel -- один вход, несколько параллельных Runnable-ов, dict на выходе', + 'joke_chain = ChatPromptTemplate.from_template("Tell a joke about {topic}") | model', + 'poem_chain = ChatPromptTemplate.from_template("Write a poem about {topic}") | model', + '', + 'parallel = RunnableParallel(joke=joke_chain, poem=poem_chain)', + 'result = parallel.invoke({"topic": "cats"})', + 'print(result.keys()) # dict_keys(["joke", "poem"])', + '', + '# Branch -- if/else роутинг по условию', + 'branch = RunnableBranch(', + ' (lambda x: "code" in x["topic"].lower(), code_chain),', + ' (lambda x: "math" in x["topic"].lower(), math_chain),', + ' general_chain, # default', + ')', + 'print(branch.invoke({"topic": "code review"}))', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'success', + text: 'RunnableParallel выполняется параллельно -- отлично для multi-aspect анализа (sentiment + summary + keywords).', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/branching/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 17); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-18.js b/slides/section1-chains/slide-18.js new file mode 100644 index 0000000..314e213 --- /dev/null +++ b/slides/section1-chains/slide-18.js @@ -0,0 +1,62 @@ +// Slide 18: Retrievers + Vector Stores -- FAISS / Chroma / PGVector +// Code slide: index documents, embed, retrieve top-k, plug into a chain. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Retrievers', + title: 'Vector store -> retriever -> RAG-цепочка', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/retriever_rag.py', + code: [ + 'from langchain_openai import OpenAIEmbeddings', + 'from langchain_community.vectorstores import FAISS', + 'from langchain_core.runnables import RunnablePassthrough', + '', + '# 1. Эмбеддинги и индекс', + 'embeddings = OpenAIEmbeddings(model="text-embedding-3-small")', + 'docs = ["Cats are mammals.", "Python is a programming language.",', + ' "LangChain helps build LLM apps."]', + 'vectorstore = FAISS.from_texts(docs, embedding=embeddings)', + '', + '# 2. .as_retriever() превращает store в Runnable', + 'retriever = vectorstore.as_retriever(search_kwargs={"k": 2})', + '', + '# 3. RAG-цепочка через LCEL: context + question -> answer', + 'from langchain_core.prompts import ChatPromptTemplate', + 'from langchain.chat_models import init_chat_model', + '', + 'prompt = ChatPromptTemplate.from_template(', + ' "Answer based on context.\\nContext: {context}\\nQ: {question}"', + ')', + 'model = init_chat_model("openai:gpt-4.1-mini")', + 'rag = (', + ' {"context": retriever, "question": RunnablePassthrough()}', + ' | prompt | model | StrOutputParser()', + ')', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'Альтернативные сторы: Chroma (легковесный), PGVector (production Postgres), Pinecone, Weaviate, Qdrant.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/concepts/retrievers/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 18); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-19.js b/slides/section1-chains/slide-19.js new file mode 100644 index 0000000..6cf6861 --- /dev/null +++ b/slides/section1-chains/slide-19.js @@ -0,0 +1,57 @@ +// Slide 19: Tools -- @tool decorator + bind_tools +// Code slide: define tools and pass them to a model via bind_tools. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Tools', + title: '@tool -- любая функция становится инструментом', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/tool_basic.py', + code: [ + 'from langchain.tools import tool', + 'from langchain.chat_models import init_chat_model', + '', + '@tool', + 'def get_weather(city: str) -> str:', + ' """Get the current weather for a given city."""', + ' return f"Sunny, 22C in {city}"', + '', + '@tool', + 'def search_docs(query: str, top_k: int = 3) -> list[str]:', + ' """Search internal documentation. Returns top_k snippets."""', + ' return [f"doc about {query} #{i}" for i in range(top_k)]', + '', + '# bind_tools -- модель знает, какие функции можно вызвать', + 'model = init_chat_model("openai:gpt-4.1-mini")', + 'bound = model.bind_tools([get_weather, search_docs])', + '', + 'result = bound.invoke("What is the weather in Paris?")', + 'print(result.tool_calls)', + '# [{"name": "get_weather", "args": {"city": "Paris"}, "id": "..."}]', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'success', + text: 'docstring инструмента = описание, которое видит модель. Названия параметров должны быть говорящими.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'python.langchain.com/docs/how_to/tool_calling/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 19); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-20.js b/slides/section1-chains/slide-20.js new file mode 100644 index 0000000..2926e7a --- /dev/null +++ b/slides/section1-chains/slide-20.js @@ -0,0 +1,56 @@ +// Slide 20: create_agent -- the unified agent entry point (v1.0) +// Code slide: a working agent in 10 lines. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Agents', + title: 'create_agent -- один вход для всех агентов', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/agent_basic.py', + code: [ + 'from langchain.agents import create_agent', + 'from langchain.tools import tool', + '', + '@tool', + 'def get_weather(city: str) -> str:', + ' """Get weather for a city."""', + ' return f"Sunny, 22C in {city}"', + '', + '# Один вызов вместо create_react_agent / create_openai_functions_agent / ...', + 'agent = create_agent(', + ' model="openai:gpt-4.1",', + ' tools=[get_weather],', + ' system_prompt="You are a weather assistant.",', + ')', + '', + '# invoke -> dict {"messages": [...]}; последнее сообщение = ответ', + 'result = agent.invoke({"messages": [{"role": "user",', + ' "content": "weather in Paris?"}]})', + 'print(result["messages"][-1].content)', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'Под капотом LangGraph-runtime: durable execution, checkpointing, human-in-the-loop доступны через middleware.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.langchain.com/oss/python/langchain/agents', + }); + ds.helpers.addPageNumber(slide, pres, theme, 20); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-21.js b/slides/section1-chains/slide-21.js new file mode 100644 index 0000000..d848a2d --- /dev/null +++ b/slides/section1-chains/slide-21.js @@ -0,0 +1,58 @@ +// Slide 21: Short-term memory -- thread_id + checkpointer +// Code slide: persistent conversation per thread. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Memory', + title: 'Краткосрочная память: thread + checkpointer', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/memory_short.py', + code: [ + 'from langchain.agents import create_agent', + 'from langgraph.checkpoint.memory import InMemorySaver', + '', + 'checkpointer = InMemorySaver()', + '', + 'agent = create_agent(', + ' model="openai:gpt-4.1-mini",', + ' tools=[...],', + ' checkpointer=checkpointer, # <-- persistent state', + ')', + '', + 'config = {"configurable": {"thread_id": "user-42"}}', + '', + '# Первый turn', + 'agent.invoke({"messages": [{"role": "user",', + ' "content": "My name is Alice."}]}, config=config)', + '', + '# Второй turn -- модель помнит имя', + 'result = agent.invoke({"messages": [{"role": "user",', + ' "content": "What is my name?"}]}, config=config)', + 'print(result["messages"][-1].content) # "Alice"', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'success', + text: 'InMemorySaver для dev. Для прода -- PostgresSaver / RedisSaver (те же LangGraph checkpointer-ы).', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.langchain.com/oss/python/langgraph/persistence', + }); + ds.helpers.addPageNumber(slide, pres, theme, 21); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-22.js b/slides/section1-chains/slide-22.js new file mode 100644 index 0000000..ec3ba3a --- /dev/null +++ b/slides/section1-chains/slide-22.js @@ -0,0 +1,58 @@ +// Slide 22: Long-term memory -- InMemoryStore + namespace +// Code slide: cross-thread memory, semantic search over stored facts. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Memory', + title: 'Долгосрочная память: store + namespace', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/memory_long.py', + code: [ + 'from langgraph.store.memory import InMemoryStore', + 'from langchain.agents import create_agent', + 'from langchain.embeddings import init_embeddings', + '', + '# Store может быть in-memory (dev) или Postgres (prod)', + 'store = InMemoryStore(', + ' index={"embed": init_embeddings("openai:text-embedding-3-small"),', + ' "dims": 1536},', + ')', + '', + 'agent = create_agent(', + ' model="openai:gpt-4.1-mini",', + ' tools=[...],', + ' store=store,', + ')', + '', + '# Namespace = ("user-id", "facts") -- разделение по пользователям', + 'ns = ("user-42", "facts")', + 'store.put(ns, "pref-1", {"text": "User prefers concise answers."})', + '', + '# В новой сессии store.search(ns, query="preferences") вернёт факты', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'info', + text: 'Long-term memory переживает thread. Идеальна для user preferences, summary прошлых сессий, learned facts.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.langchain.com/oss/python/langgraph/memory', + }); + ds.helpers.addPageNumber(slide, pres, theme, 22); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-23.js b/slides/section1-chains/slide-23.js new file mode 100644 index 0000000..643f52e --- /dev/null +++ b/slides/section1-chains/slide-23.js @@ -0,0 +1,60 @@ +// Slide 23: Middleware -- HumanInTheLoop + PIIRedaction + Summarization +// Code slide: cross-cutting hooks attached at agent construction. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Middleware', + title: 'Cross-cutting concerns одной строкой', + sectionNumber: 1, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 9.0, h: 3.25, + language: 'python', + filePath: 'examples/middleware.py', + code: [ + 'from langchain.agents import create_agent', + 'from langchain.agents.middleware import (', + ' HumanInTheLoopMiddleware,', + ' PIIRedactionMiddleware,', + ' SummarizationMiddleware,', + ')', + 'from langchain.tools import tool', + '', + '@tool', + 'def send_email(to: str, body: str) -> str:', + ' """Send an email to recipient."""', + ' return f"sent to {to}"', + '', + 'agent = create_agent(', + ' model="openai:gpt-4.1",', + ' tools=[send_email],', + ' middleware=[', + ' HumanInTheLoopMiddleware(interrupt_on={"send_email": True}),', + ' PIIRedactionMiddleware(redact_emails=True, redact_phones=True),', + ' SummarizationMiddleware(trigger=("tokens", 4000)),', + ' ],', + ')', + ].join('\n'), + }); + + // Right-side mini-glossary callouts (left side has code; reuse addCallout below) + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.85, w: 9.0, h: 0.3, + kind: 'warning', + text: 'Все три middleware -- встроенные. Custom middleware пишутся как before_model/after_model hooks.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.langchain.com/oss/python/langchain/middleware', + }); + ds.helpers.addPageNumber(slide, pres, theme, 23); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-24.js b/slides/section1-chains/slide-24.js new file mode 100644 index 0000000..6f14a48 --- /dev/null +++ b/slides/section1-chains/slide-24.js @@ -0,0 +1,90 @@ +// Slide 24: 1.0 vs 0.3 -- breaking changes and what is new +// Content slide: side-by-side comparison of what changed in v1.0. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Версии', + title: 'Что нового в 1.0 vs 0.3', + sectionNumber: 1, + }); + + // Two columns: OLD (0.x) vs NEW (1.0) + const colW = 4.35; + const colH = 3.3; + const colY = 1.5; + const leftX = 0.5; + const rightX = 5.15; + + // OLD column + slide.addShape(pres.ShapeType.roundRect, { + x: leftX, y: colY, w: colW, h: colH, + fill: { color: theme.palette.state.dangerBg }, + line: { color: theme.palette.state.danger, width: 1 }, + rectRadius: 0.1, + }); + slide.addText('0.x (legacy)', { + x: leftX + 0.2, y: colY + 0.15, w: colW - 0.3, h: 0.4, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 18, bold: true, + color: theme.palette.state.danger, + valign: 'middle', margin: 0, + }); + const oldItems = [ + 'create_react_agent / create_openai_functions_agent / create_structured_chat_agent', + 'LLMChain, RetrievalQA, ConversationalRetrievalQA', + 'ChatOpenAI / ChatAnthropic / ChatGoogleGenerativeAI отдельно', + 'Кастомные callbacks для HITL и PII', + 'Verbose LLMChain с ручным manage_prompts', + ].map((t) => ({ text: '- ' + t + '\n', options: { fontFace: ds.helpers.withFallback(theme.fonts.ui), fontSize: 12, color: theme.palette.text.primary } })); + slide.addText(oldItems, { + x: leftX + 0.2, y: colY + 0.6, w: colW - 0.3, h: colH - 0.7, + valign: 'top', paraSpaceAfter: 6, margin: 0, + }); + + // NEW column + slide.addShape(pres.ShapeType.roundRect, { + x: rightX, y: colY, w: colW, h: colH, + fill: { color: theme.palette.state.successBg }, + line: { color: theme.palette.state.success, width: 1 }, + rectRadius: 0.1, + }); + slide.addText('1.0 (current)', { + x: rightX + 0.2, y: colY + 0.15, w: colW - 0.3, h: 0.4, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 18, bold: true, + color: theme.palette.state.success, + valign: 'middle', margin: 0, + }); + const newItems = [ + 'create_agent -- единая точка входа (построен на LangGraph)', + 'Middleware-система: HITL, PII, Summarization как first-class', + 'init_chat_model(":") -- один инициализатор', + 'with_structured_output -- tool calling / provider-native JSON', + 'Семантическое версионирование: до 2.0 breaking changes не будет', + 'langchain-classic -- пакет для обратной совместимости legacy chains', + ].map((t) => ({ text: '+ ' + t + '\n', options: { fontFace: ds.helpers.withFallback(theme.fonts.ui), fontSize: 12, color: theme.palette.text.primary } })); + slide.addText(newItems, { + x: rightX + 0.2, y: colY + 0.6, w: colW - 0.3, h: colH - 0.7, + valign: 'top', paraSpaceAfter: 6, margin: 0, + }); + + // Bottom callout: migration tip + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.95, w: 9.0, h: 0.4, + kind: 'info', + text: 'Миграция: pip install langchain-classic -- legacy LLMChain/AgentExecutor работают, но новый код пишите на create_agent.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.langchain.com/oss/python/releases/langchain-v1', + }); + ds.helpers.addPageNumber(slide, pres, theme, 24); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-25.js b/slides/section1-chains/slide-25.js new file mode 100644 index 0000000..e0dbe5d --- /dev/null +++ b/slides/section1-chains/slide-25.js @@ -0,0 +1,85 @@ +// Slide 25: TypeScript status -- what is and isn't there in JS land +// Content slide: parity matrix and code sketch. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'TypeScript', + title: 'JS-аналог: что есть, чего нет', + sectionNumber: 1, + }); + + // Code block: TS sample + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.5, w: 5.5, h: 3.25, + language: 'typescript', + filePath: 'examples/ts_basic.ts', + code: [ + 'import { initChatModel } from "langchain/chat_models/universal";', + 'import { createAgent } from "langchain/agents";', + 'import { tool } from "@langchain/core/tools";', + 'import { z } from "zod";', + '', + 'const getWeather = tool(', + ' async ({ city }) => `Sunny, 22C in ${city}`,', + ' { name: "get_weather", description: "Get weather",', + ' schema: z.object({ city: z.string() }) },', + ');', + '', + 'const model = await initChatModel("openai:gpt-4.1");', + 'const agent = createAgent({ model, tools: [getWeather] });', + 'const result = await agent.invoke({', + ' messages: [{ role: "user", content: "weather in Paris?" }],', + '});', + ].join('\n'), + }); + + // Right column: parity list + slide.addText('Что есть в @langchain/langchain:', { + x: 6.2, y: 1.5, w: 3.3, h: 0.35, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 13, bold: true, + color: theme.palette.accent.primary, margin: 0, + }); + + const have = [ + '+ initChatModel', + '+ createAgent', + '+ @langchain/core (tools, prompts, parsers)', + '+ LCEL и Runnable-протокол', + '+ Стандартизированные Messages', + '+ Vector store интеграции', + ].map((t) => ({ text: t + '\n', options: { fontFace: ds.helpers.withFallback(theme.fonts.ui), fontSize: 11, color: theme.palette.text.primary } })); + slide.addText(have, { + x: 6.2, y: 1.9, w: 3.3, h: 1.3, + valign: 'top', paraSpaceAfter: 3, margin: 0, + }); + + slide.addText('Чего нет или слабее:', { + x: 6.2, y: 3.3, w: 3.3, h: 0.35, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 13, bold: true, + color: theme.palette.state.warning, margin: 0, + }); + const miss = [ + '- langchain-classic покрытие уже', + '- Часть community-интеграций', + '- Tracing middleware меньше', + ].map((t) => ({ text: t + '\n', options: { fontFace: ds.helpers.withFallback(theme.fonts.ui), fontSize: 11, color: theme.palette.text.primary } })); + slide.addText(miss, { + x: 6.2, y: 3.7, w: 3.3, h: 1.0, + valign: 'top', paraSpaceAfter: 3, margin: 0, + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'js.langchain.com/docs/how_to/chat_models_universal_init', + }); + ds.helpers.addPageNumber(slide, pres, theme, 25); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-26.js b/slides/section1-chains/slide-26.js new file mode 100644 index 0000000..ce3584b --- /dev/null +++ b/slides/section1-chains/slide-26.js @@ -0,0 +1,56 @@ +// Slide 26: Pros / Cons -- when LangChain 1.0 is the right choice +// Content slide: pros/cons two-column panel. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Итоги', + title: 'Когда LangChain 1.0 -- правильный выбор', + sectionNumber: 1, + }); + + // Lead paragraph + slide.addText( + 'Production-ready commitment, единая точка входа для агентов, ' + + 'встроенные middleware. Но абстракция скрывает LangGraph -- если нужен ' + + 'fine-grained контроль над execution, придется проваливаться глубже.', + { + x: 0.5, y: 1.5, w: 9.0, h: 0.6, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 13, italic: true, + color: theme.palette.text.secondary, + align: 'left', valign: 'top', margin: 0, + } + ); + + ds.helpers.addProsCons(slide, pres, theme, { + x: 0.5, y: 2.25, w: 9.0, h: 2.5, + pros: [ + 'Семантическая стабильность -- обязательство не ломать API до 2.0', + 'create_agent вместо зоопарка agent-типов', + 'init_chat_model -- переключение провайдера без рефакторинга', + 'Middleware-система (HITL, PII, Summarization) из коробки', + 'LangGraph-runtime под капотом -- durable execution бесплатно', + '~700+ интеграций через community-пакеты langchain-*', + ], + cons: [ + 'Кривая обучения для middleware (before/after hooks)', + 'Часть экосистемы переехала в langchain-classic -- миграционная боль', + 'Абстракция скрывает LangGraph -- для fine-grained нужен fallback', + 'Bundled-версии зависимостей (langchain-openai, -anthropic, ...) нужно фиксировать', + 'Исторически bloated core -- в 1.0 стало lean, но восприятие осталось', + ], + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'changelog.langchain.com/announcements/langchain-1-0-now-generally-available', + }); + ds.helpers.addPageNumber(slide, pres, theme, 26); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section1-chains/slide-27.js b/slides/section1-chains/slide-27.js new file mode 100644 index 0000000..8eea89b --- /dev/null +++ b/slides/section1-chains/slide-27.js @@ -0,0 +1,103 @@ +// Slide 27: Bridge to LangGraph -- what is next in this deck +// Content slide: closing summary + teaser for Section 2. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 1: CHAINS', + section: 'Переход', + title: 'Дальше: LangGraph -- явный control flow', + sectionNumber: 1, + }); + + // Lead paragraph + slide.addText( + 'LangChain 1.0 -- это "правильный путь по умолчанию" для большинства задач. ' + + 'Когда LCEL-pipeline перестает хватать (ветвления, циклы, человеческое ' + + 'одобрение, persistence) -- под капотом работает LangGraph. В следующей секции ' + + 'разберем его как самостоятельный runtime: граф, узлы, edges, checkpointer, ' + + 'human-in-the-loop как first-class концепции.', + { + x: 0.5, y: 1.55, w: 9.0, h: 1.1, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 14, + color: theme.palette.text.secondary, + align: 'left', valign: 'top', margin: 0, + } + ); + + // Three teaser cards + const teasers = [ + { + title: 'Граф вместо пайплайна', + body: 'StateGraph, узлы (nodes), ребра (edges), условные переходы. ' + + 'Полный контроль над execution.', + }, + { + title: 'Persistence', + body: 'Checkpointers, thread_id, time-travel. State между turn-ами ' + + 'хранится на диске.', + }, + { + title: 'Human-in-the-loop', + body: 'interrupt_before / interrupt_after узлов. Апрув через Command. ' + + 'Не нужен middleware-хак.', + }, + ]; + + const cardW = 3.0; + const cardH = 1.9; + const gap = 0.15; + const startX = 0.5; + const cardY = 2.85; + + teasers.forEach((p, i) => { + const x = startX + i * (cardW + gap); + slide.addShape(pres.ShapeType.roundRect, { + x: x, y: cardY, w: cardW, h: cardH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.1, + }); + slide.addShape(pres.ShapeType.rect, { + x: x, y: cardY, w: cardW, h: 0.08, + fill: { color: theme.palette.accent.secondary }, + line: { type: 'none' }, + }); + slide.addText(p.title, { + x: x + 0.2, y: cardY + 0.2, w: cardW - 0.4, h: 0.45, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 15, bold: true, + color: theme.palette.accent.secondary, + valign: 'middle', margin: 0, + }); + slide.addText(p.body, { + x: x + 0.2, y: cardY + 0.7, w: cardW - 0.4, h: cardH - 0.85, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 11, + color: theme.palette.text.secondary, + valign: 'top', margin: 0, + }); + }); + + // Bottom hint + slide.addText('SECTION 2: LANGGRAPH >>>', { + x: 0.5, y: 5.0, w: 9.0, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 11, bold: true, + color: theme.palette.accent.primary, + align: 'center', valign: 'middle', + charSpacing: 4, margin: 0, + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.langchain.com/oss/python/langgraph/overview', + }); + ds.helpers.addPageNumber(slide, pres, theme, 27); +} + +module.exports = { createSlide }; \ No newline at end of file diff --git a/slides/section2-langgraph/compile.js b/slides/section2-langgraph/compile.js new file mode 100644 index 0000000..52e843b --- /dev/null +++ b/slides/section2-langgraph/compile.js @@ -0,0 +1,1529 @@ +/** + * 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); +}); diff --git a/slides/section2-langgraph/preview/slide-01.png b/slides/section2-langgraph/preview/slide-01.png new file mode 100644 index 0000000..aa753a8 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-01.png differ diff --git a/slides/section2-langgraph/preview/slide-02.png b/slides/section2-langgraph/preview/slide-02.png new file mode 100644 index 0000000..62c23e0 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-02.png differ diff --git a/slides/section2-langgraph/preview/slide-03.png b/slides/section2-langgraph/preview/slide-03.png new file mode 100644 index 0000000..e5c17a2 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-03.png differ diff --git a/slides/section2-langgraph/preview/slide-04.png b/slides/section2-langgraph/preview/slide-04.png new file mode 100644 index 0000000..6e0795d Binary files /dev/null and b/slides/section2-langgraph/preview/slide-04.png differ diff --git a/slides/section2-langgraph/preview/slide-05.png b/slides/section2-langgraph/preview/slide-05.png new file mode 100644 index 0000000..86f5cc9 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-05.png differ diff --git a/slides/section2-langgraph/preview/slide-06.png b/slides/section2-langgraph/preview/slide-06.png new file mode 100644 index 0000000..e276718 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-06.png differ diff --git a/slides/section2-langgraph/preview/slide-07.png b/slides/section2-langgraph/preview/slide-07.png new file mode 100644 index 0000000..542e93c Binary files /dev/null and b/slides/section2-langgraph/preview/slide-07.png differ diff --git a/slides/section2-langgraph/preview/slide-08.png b/slides/section2-langgraph/preview/slide-08.png new file mode 100644 index 0000000..f451115 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-08.png differ diff --git a/slides/section2-langgraph/preview/slide-09.png b/slides/section2-langgraph/preview/slide-09.png new file mode 100644 index 0000000..6b933d0 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-09.png differ diff --git a/slides/section2-langgraph/preview/slide-10.png b/slides/section2-langgraph/preview/slide-10.png new file mode 100644 index 0000000..33343cd Binary files /dev/null and b/slides/section2-langgraph/preview/slide-10.png differ diff --git a/slides/section2-langgraph/preview/slide-11.png b/slides/section2-langgraph/preview/slide-11.png new file mode 100644 index 0000000..01b6da4 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-11.png differ diff --git a/slides/section2-langgraph/preview/slide-12.png b/slides/section2-langgraph/preview/slide-12.png new file mode 100644 index 0000000..6208720 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-12.png differ diff --git a/slides/section2-langgraph/preview/slide-13.png b/slides/section2-langgraph/preview/slide-13.png new file mode 100644 index 0000000..73acdfe Binary files /dev/null and b/slides/section2-langgraph/preview/slide-13.png differ diff --git a/slides/section2-langgraph/preview/slide-14.png b/slides/section2-langgraph/preview/slide-14.png new file mode 100644 index 0000000..c5153fc Binary files /dev/null and b/slides/section2-langgraph/preview/slide-14.png differ diff --git a/slides/section2-langgraph/preview/slide-15.png b/slides/section2-langgraph/preview/slide-15.png new file mode 100644 index 0000000..8307b4b Binary files /dev/null and b/slides/section2-langgraph/preview/slide-15.png differ diff --git a/slides/section2-langgraph/preview/slide-16.png b/slides/section2-langgraph/preview/slide-16.png new file mode 100644 index 0000000..726fc25 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-16.png differ diff --git a/slides/section2-langgraph/preview/slide-17.png b/slides/section2-langgraph/preview/slide-17.png new file mode 100644 index 0000000..fb13737 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-17.png differ diff --git a/slides/section2-langgraph/preview/slide-18.png b/slides/section2-langgraph/preview/slide-18.png new file mode 100644 index 0000000..4f8dd42 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-18.png differ diff --git a/slides/section2-langgraph/preview/slide-19.png b/slides/section2-langgraph/preview/slide-19.png new file mode 100644 index 0000000..05de100 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-19.png differ diff --git a/slides/section2-langgraph/preview/slide-20.png b/slides/section2-langgraph/preview/slide-20.png new file mode 100644 index 0000000..59ad233 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-20.png differ diff --git a/slides/section2-langgraph/preview/slide-21.png b/slides/section2-langgraph/preview/slide-21.png new file mode 100644 index 0000000..186d0e9 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-21.png differ diff --git a/slides/section2-langgraph/preview/slide-22.png b/slides/section2-langgraph/preview/slide-22.png new file mode 100644 index 0000000..6a6d393 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-22.png differ diff --git a/slides/section2-langgraph/preview/slide-23.png b/slides/section2-langgraph/preview/slide-23.png new file mode 100644 index 0000000..301344f Binary files /dev/null and b/slides/section2-langgraph/preview/slide-23.png differ diff --git a/slides/section2-langgraph/preview/slide-24.png b/slides/section2-langgraph/preview/slide-24.png new file mode 100644 index 0000000..d66bfd6 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-24.png differ diff --git a/slides/section2-langgraph/preview/slide-25.png b/slides/section2-langgraph/preview/slide-25.png new file mode 100644 index 0000000..89be5ab Binary files /dev/null and b/slides/section2-langgraph/preview/slide-25.png differ diff --git a/slides/section2-langgraph/preview/slide-26.png b/slides/section2-langgraph/preview/slide-26.png new file mode 100644 index 0000000..3d20c14 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-26.png differ diff --git a/slides/section2-langgraph/preview/slide-27.png b/slides/section2-langgraph/preview/slide-27.png new file mode 100644 index 0000000..fffa1b4 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-27.png differ diff --git a/slides/section2-langgraph/preview/slide-28.png b/slides/section2-langgraph/preview/slide-28.png new file mode 100644 index 0000000..5555b43 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-28.png differ diff --git a/slides/section2-langgraph/preview/slide-29.png b/slides/section2-langgraph/preview/slide-29.png new file mode 100644 index 0000000..d50a8d5 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-29.png differ diff --git a/slides/section2-langgraph/preview/slide-30.png b/slides/section2-langgraph/preview/slide-30.png new file mode 100644 index 0000000..eaedad4 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-30.png differ diff --git a/slides/section2-langgraph/preview/slide-31.png b/slides/section2-langgraph/preview/slide-31.png new file mode 100644 index 0000000..3a6ae7f Binary files /dev/null and b/slides/section2-langgraph/preview/slide-31.png differ diff --git a/slides/section2-langgraph/preview/slide-32.png b/slides/section2-langgraph/preview/slide-32.png new file mode 100644 index 0000000..c4d0426 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-32.png differ diff --git a/slides/section2-langgraph/preview/slide-33.png b/slides/section2-langgraph/preview/slide-33.png new file mode 100644 index 0000000..7b91f28 Binary files /dev/null and b/slides/section2-langgraph/preview/slide-33.png differ diff --git a/slides/section2-langgraph/section2.pdf b/slides/section2-langgraph/section2.pdf new file mode 100644 index 0000000..1da3bae Binary files /dev/null and b/slides/section2-langgraph/section2.pdf differ diff --git a/slides/section2-langgraph/section2.pptx b/slides/section2-langgraph/section2.pptx new file mode 100644 index 0000000..a1932dd Binary files /dev/null and b/slides/section2-langgraph/section2.pptx differ diff --git a/slides/section3-deepagents/compile.js b/slides/section3-deepagents/compile.js new file mode 100644 index 0000000..6e2cfd9 --- /dev/null +++ b/slides/section3-deepagents/compile.js @@ -0,0 +1,1394 @@ +/** + * compile.js -- Section 3: Deep Agents 1.0 + * ---------------------------------------------------------------------------- + * Builds section3.pptx (26 slides, 16:9, dark theme, code-heavy). + * Audience: engineers. Tutorial-style, no introductory filler. + * + * Usage: node compile.js + * Output: section3.pptx (in this directory) + * + * Sources: research/per-tech/deepagents.md (snapshot 2026-06-22). + * Helpers: ../design-system.js (slideBase, addHeader, addCodeBlock, + * addCallout, addProsCons, addPageNumber, addSectionDivider, + * addSourceLine, highlightPython). + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const pptxgen = require('pptxgenjs'); + +// design-system.js may live in /design-system.js or one level up. +// Try a few candidates so the script runs from any workdir. +function loadDesignSystem() { + const candidates = [ + path.join(__dirname, '..', '..', 'design-system.js'), + path.join(__dirname, '..', 'design-system.js'), + path.join(__dirname, 'design-system.js'), + path.resolve(process.cwd(), 'design-system.js'), + path.resolve(process.cwd(), '..', 'design-system.js'), + path.resolve(process.cwd(), '..', '..', 'design-system.js'), + ]; + for (const c of candidates) { + if (fs.existsSync(c)) { + return require(c); + } + } + throw new Error('design-system.js not found. Tried:\n ' + candidates.join('\n ')); +} + +const ds = loadDesignSystem(); +const { theme, helpers, layouts } = ds; + +// --------------------------------------------------------------------------- +// Boot +// --------------------------------------------------------------------------- + +const pres = new pptxgen(); +pres.layout = 'LAYOUT_16x9'; +pres.title = 'Deep Agents 1.0 -- harness, todos, virtual FS, subagents'; +pres.author = 'lc-evo-deck'; +pres.subject = 'LangChain Evolution Deck / Section 3'; + +const SECTION_NUM = 3; +const TOTAL_SLIDES = 26; + +// Page-number counter (advances as slides are added). +let pageNum = 0; +function nextPage() { pageNum += 1; return pageNum; } + +// --------------------------------------------------------------------------- +// Slide factories +// --------------------------------------------------------------------------- + +function contentSlide(opts) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: opts.eyebrow || `SECTION 3: DEEP AGENTS`, + title: opts.title, + sectionNumber: opts.sectionNumber != null ? opts.sectionNumber : SECTION_NUM, + }); + helpers.addPageNumber(slide, pres, theme, nextPage()); + if (opts.source) { + helpers.addSourceLine(slide, pres, theme, { source: opts.source }); + } + return slide; +} + +function dividerSlide(opts) { + const slide = pres.addSlide(); + helpers.addSectionDivider(slide, pres, theme, { + number: opts.number, + eyebrow: opts.eyebrow, + title: opts.title, + intro: opts.intro, + }); + helpers.addPageNumber(slide, pres, theme, nextPage()); + return slide; +} + +// --------------------------------------------------------------------------- +// Code-block helper (local, normalizes filePath / size) +// --------------------------------------------------------------------------- + +function codeBlock(slide, opts) { + helpers.addCodeBlock(slide, pres, theme, { + x: opts.x, y: opts.y, w: opts.w, h: opts.h, + code: opts.code, + language: 'python', + filePath: opts.filePath, + startLine: opts.startLine || 1, + highlightLines: opts.highlightLines, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 1: Section divider / cover +// --------------------------------------------------------------------------- +{ + dividerSlide({ + number: '03', + eyebrow: 'SECTION 3', + title: 'Deep Agents 1.0', + intro: `Batteries-included agent harness: planning tool, virtual filesystem, +subagents with isolated context, pluggable backends, HITL middleware. +Built on LangGraph + LangChain 1.0 middleware. Inspired by Claude Code, +Deep Research, Manus.`, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 2: Problem -- limits of LangGraph +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.1 PROBLEM', + title: 'LangGraph limits: why a new layer', + sectionNumber: 3, + source: 'github.com/langchain-ai/deepagents', + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 1.0, + kind: 'warning', + title: 'LangGraph is a runtime, not an agent harness.', + text: `You still have to wire planning, filesystem access, subagent isolation, +HITL approval, and context offload yourself.`, + }); + + helpers.addProsCons(slide, pres, theme, { + x: 0.5, y: 2.65, w: 9.0, h: 2.3, + pros: [ + 'Full control over graph topology, cycles, parallel branches (Send)', + 'Durable execution, checkpointing, interrupt-based HITL are first-class', + 'Stable public API until 2.0 (released 22 Oct 2025)', + ], + cons: [ + 'Boilerplate-heavy: planning tool, fs tools, subagent wiring -- all manual', + 'No built-in context overflow strategy (every tool result floods the main thread)', + 'No opinionated "coding/research" agent defaults -- you re-implement Claude Code patterns', + ], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 3: Solution -- batteries-included harness +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.1 SOLUTION', + title: 'Deep Agents: opinionated defaults', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/overview', + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 1.05, + kind: 'info', + title: 'Analogy: Django over raw WSGI', + text: `Same runtime (LangGraph), but with conventions and pre-built middleware. +You give up some flexibility in exchange for "everything just works".`, + }); + + codeBlock(slide, { + x: 0.5, y: 2.5, w: 5.5, h: 2.4, + code: [ + `# One import, one call -- you get:`, + `# - write_todos planning tool`, + `# - ls / read_file / write_file / edit_file`, + `# - glob, grep, execute (bash)`, + `# - task tool for subagents`, + `# - summarization middleware`, + ``, + `from deepagents import create_deep_agent`, + ``, + `agent = create_deep_agent(`, + ` model="openai:gpt-4.1",`, + ` tools=[my_tool],`, + ` system_prompt="...",`, + `)`, + ].join('\n'), + filePath: 'examples/hello.py', + startLine: 1, + highlightLines: [9, 10, 11, 12, 13, 14], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: 2.5, w: 3.2, h: 2.4, + kind: 'success', + title: 'Stack', + text: `LangGraph (runtime) + -> create_agent (LangChain 1.0, thin harness) + -> create_deep_agent (opinionated harness) + +Built-in: planning, FS, subagents, HITL, skills.`, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 4: Install +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.2 INSTALL', + title: 'pip install deepagents', + sectionNumber: 3, + source: 'pypi.org/project/deepagents/', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 1.1, + code: [ + `pip install deepagents`, + `# or, with uv:`, + `uv add deepagents`, + `# JS analogue:`, + `npm install deepagents`, + ].join('\n'), + filePath: 'setup.sh', + startLine: 1, + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 2.7, w: 9.0, h: 1.0, + kind: 'info', + title: 'Latest stable (snapshot 2026-06-22)', + text: `Python: deepagents 0.6.11 (no formal 1.0 yet). +Repo: github.com/langchain-ai/deepagents (~24.9k stars). +License: MIT. JS package: deepagents (npm).`, + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 3.85, w: 9.0, h: 1.05, + kind: 'warning', + title: 'Dependencies', + text: `deepagents pulls in langchain, langgraph, langchain-core. +API keys via env vars: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.`, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 5: create_deep_agent -- hello world +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.3 HELLO WORLD', + title: 'create_deep_agent: hello world', + sectionNumber: 3, + source: 'github.com/langchain-ai/deepagents README', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.6, h: 3.4, + code: [ + `from deepagents import create_deep_agent`, + ``, + `agent = create_deep_agent(`, + ` model="openai:gpt-4.1",`, + ` tools=[],`, + ` system_prompt="You are a helpful assistant.",`, + `)`, + ``, + `result = agent.invoke({`, + ` "messages": "Write a haiku about Python"`, + `})`, + `print(result["messages"][-1].content)`, + ].join('\n'), + filePath: 'examples/hello.py', + startLine: 1, + highlightLines: [3, 4, 5, 6], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: layouts.CONTENT_TOP, w: 3.2, h: 3.4, + kind: 'info', + title: 'What you get for free', + text: `- write_todos tool +- ls / read_file / write_file / edit_file +- glob, grep, execute (bash) +- task tool for subagents +- summarization middleware +- same LangGraph runtime as create_agent`, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 6: create_deep_agent -- parameters +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.3 API', + title: 'create_deep_agent: full API', + sectionNumber: 3, + source: 'reference.langchain.com/python/deepagents', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.6, + code: [ + `from deepagents import create_deep_agent`, + `from deepagents.backends import FilesystemBackend`, + ``, + `agent = create_deep_agent(`, + ` model="anthropic:claude-sonnet-4-5",`, + ` tools=[my_tool],`, + ` system_prompt="...",`, + ` subagents=[researcher, writer],`, + ` skills=["./skills/review.md"],`, + ` backend=FilesystemBackend("./ws"),`, + ` middleware=[my_hitl, my_logger],`, + ` checkpointer=InMemorySaver(),`, + ` store=InMemoryStore(),`, + ` interrupt_on={"bash": True},`, + `)`, + ].join('\n'), + filePath: 'examples/full_api.py', + startLine: 1, + highlightLines: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 7: System prompt & instructions +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.4 INSTRUCTIONS', + title: 'System prompt: how to talk to a deep agent', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/customization', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5, + code: [ + `SYSTEM_PROMPT = """`, + `You are a senior backend engineer.`, + ``, + `WORKFLOW:`, + ` 1. Plan with write_todos before non-trivial work.`, + ` 2. Explore the codebase via ls / glob / grep first.`, + ` 3. Use edit_file for surgical changes, write_file for new files.`, + ` 4. Delegate research tasks to the "researcher" subagent.`, + ` 5. Run tests via execute; never claim success without output.`, + ``, + `CONSTRAINTS:`, + ` - Do not modify files outside ./src.`, + ` - Stop and ask the user if requirements are ambiguous.`, + `"""`, + ``, + `agent = create_deep_agent(model=..., system_prompt=SYSTEM_PROMPT)`, + ].join('\n'), + filePath: 'examples/system_prompt.py', + startLine: 1, + highlightLines: [4, 5, 6, 7, 8, 12, 13], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 8: Built-in tools -- overview +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.5 TOOLS OVERVIEW', + title: 'Built-in fs + planning tools', + sectionNumber: 3, + source: 'github.com/langchain-ai/deepagents README', + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 0.85, + kind: 'info', + title: 'Eight opinionated defaults, all active unless you override.', + text: `Planning, filesystem, search, shell, and subagent delegation -- one import, zero setup.`, + }); + + const tools = [ + ['write_todos', 'plan / decompose a task into ordered steps'], + ['ls', 'list directory entries in the virtual FS'], + ['read_file', 'read one or many files with line offsets'], + ['write_file', 'create or overwrite a file in the FS'], + ['edit_file', 'surgical string-replace edits (matches all occurrences)'], + ['glob', 'find files by pattern (e.g. **/*.py)'], + ['grep', 'regex search across files with context'], + ['execute', 'run a shell command (sandboxed if a backend enforces it)'], + ['task', 'delegate to a named subagent with isolated context'], + ]; + + const colX = [0.5, 5.05]; + const colW = 4.4; + for (let i = 0; i < tools.length; i += 1) { + const col = i % 2; + const row = Math.floor(i / 2); + const x = colX[col]; + const y = 2.5 + row * 0.78; + slide.addShape(pres.ShapeType.roundRect, { + x: x, y: y, w: colW, h: 0.68, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 0.75 }, + rectRadius: 0.06, + }); + slide.addText(tools[i][0], { + x: x + 0.15, y: y + 0.05, w: 1.3, h: 0.28, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: 13, + color: theme.palette.accent.tertiary, + bold: true, + }); + slide.addText(tools[i][1], { + x: x + 1.5, y: y + 0.07, w: colW - 1.65, h: 0.55, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 11, + color: theme.palette.text.secondary, + valign: 'middle', + }); + } +} + +// --------------------------------------------------------------------------- +// SLIDE 9: Built-in tools -- write_file + edit_file example +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.5 TOOLS', + title: 'write_file and edit_file in action', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/overview', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.6, h: 3.5, + code: [ + `# The agent calls these tools by name;`, + `# you describe the goal in the prompt.`, + ``, + `PROMPT = """`, + `1. Write src/hello.py with a greet(name) function.`, + `2. Use edit_file to add a docstring to greet().`, + `3. Use write_file to add tests/test_hello.py.`, + `"""`, + ``, + `agent = create_deep_agent(model="openai:gpt-4.1")`, + `agent.invoke({"messages": PROMPT})`, + ].join('\n'), + filePath: 'examples/fs_tools.py', + startLine: 1, + highlightLines: [4, 5, 6, 7], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: layouts.CONTENT_TOP, w: 3.2, h: 3.5, + kind: 'success', + title: 'Context overflow protection', + text: `Tool outputs larger than a threshold are offloaded to the virtual FS +and replaced with a path + summary in the message history. The agent +can re-read specific portions on demand. This is what lets deep agents +handle large repos without blowing the context window.`, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 10: write_todos -- concept +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.6 PLANNING', + title: 'write_todos: explicit planning inside the graph', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/overview', + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 0.95, + kind: 'info', + title: 'write_todos is just a tool, like any other.', + text: `The LLM emits a structured plan, the graph stores it in state["todos"], +and every subsequent model call sees the current plan in the system message.`, + }); + + codeBlock(slide, { + x: 0.5, y: 2.5, w: 9.0, h: 2.4, + code: [ + `write_todos(`, + ` todos=[`, + ` {"content": "Read repo structure", "status": "in_progress",`, + ` "activeForm": "Reading repo structure"},`, + ` {"content": "Implement greet()", "status": "pending",`, + ` "activeForm": "Implementing greet()"},`, + ` {"content": "Add pytest cases", "status": "pending",`, + ` "activeForm": "Adding pytest cases"},`, + ` ]`, + `)`, + `# Status: pending | in_progress | completed`, + `# activeForm: present-continuous shown in UI`, + ].join('\n'), + filePath: 'examples/write_todos.py', + startLine: 1, + highlightLines: [2, 3, 4, 5, 6, 7, 8, 9], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 11: write_todos -- Plan-Act-Reflect pattern +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.6 PATTERN', + title: 'Plan, Act, Reflect loop', + sectionNumber: 3, + source: 'blog.langchain.com/introducing-deepagents-cli', + }); + + const boxes = [ + { x: 0.5, title: '1. PLAN', body: `write_todos: +- decompose task +- order steps +- mark in_progress` }, + { x: 3.7, title: '2. ACT', body: `execute tool call +(read_file, edit_file, ...) +or delegate via task` }, + { x: 6.9, title: '3. REFLECT', body: `update todo status +re-plan if blocked +log progress` }, + ]; + const boxY = layouts.CONTENT_TOP; + const boxH = 2.2; + const boxW = 2.9; + for (let i = 0; i < boxes.length; i += 1) { + const b = boxes[i]; + slide.addShape(pres.ShapeType.roundRect, { + x: b.x, y: boxY, w: boxW, h: boxH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.accent.primary, width: 1.5 }, + rectRadius: 0.1, + }); + slide.addText(b.title, { + x: b.x + 0.15, y: boxY + 0.1, w: boxW - 0.3, h: 0.4, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 14, + color: theme.palette.accent.primary, + bold: true, + }); + slide.addText(b.body, { + x: b.x + 0.15, y: boxY + 0.55, w: boxW - 0.3, h: boxH - 0.7, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 12, + color: theme.palette.text.secondary, + valign: 'top', + }); + if (i < boxes.length - 1) { + slide.addShape(pres.ShapeType.rightArrow, { + x: b.x + boxW + 0.02, y: boxY + boxH / 2 - 0.12, + w: 0.2, h: 0.25, + fill: { color: theme.palette.accent.secondary }, + line: { type: 'none' }, + }); + } + } + + slide.addShape(pres.ShapeType.line, { + x: 1.95, y: boxY + boxH + 0.25, w: 6.2, h: 0, + line: { color: theme.palette.border.strong, width: 1.5, endArrowType: 'triangle', beginArrowType: 'none' }, + }); + slide.addText('loop until all todos = completed', { + x: 2.5, y: boxY + boxH + 0.3, w: 5.0, h: 0.3, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 11, + color: theme.palette.text.muted, + italic: true, + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.3, w: 9.0, h: 0.65, + kind: 'info', + text: `The graph state carries the plan -- every LLM call sees it in the +system message, so the agent self-monitors progress across turns.`, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 12: Subagents -- concept + task tool +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.7 SUBAGENTS', + title: 'task tool: delegate, isolate, return', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/overview', + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 1.05, + kind: 'info', + title: 'Why subagents?', + text: `Long tool outputs and exploratory searches pollute the main thread. +A subagent runs in its own scratchpad and returns a compressed summary -- +the parent context stays clean.`, + }); + + codeBlock(slide, { + x: 0.5, y: 2.5, w: 9.0, h: 2.4, + code: [ + `# When the main agent emits a tool call like:`, + `task(`, + ` subagent_type="researcher",`, + ` description="Find papers on RAG evaluation",`, + ` prompt="Search arXiv for 2025-2026 RAG evaluation surveys. `, + ` Return a 150-word summary with 3 citations.",`, + `)`, + ``, + `# Deep Agents spins up a fresh deep agent with the researcher profile,`, + `# runs it to completion, and returns only the final message.`, + ].join('\n'), + filePath: 'examples/task_call.py', + startLine: 1, + highlightLines: [2, 3, 4, 5, 6], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 13: Subagents -- minimal example +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.7 EXAMPLE', + title: 'Subagents: minimal example', + sectionNumber: 3, + source: 'github.com/langchain-ai/deepagents README', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5, + code: [ + `from deepagents import create_deep_agent`, + ``, + `researcher = {`, + ` "name": "researcher",`, + ` "description": "Does deep web research, returns citations",`, + ` "system_prompt": "You are a research specialist. Always cite sources.",`, + ` "tools": [web_search], # optional, can be []`, + `}`, + ``, + `writer = {`, + ` "name": "writer",`, + ` "description": "Polishes prose into a final report",`, + ` "system_prompt": "You are a writing specialist.",`, + ` "tools": [],`, + `}`, + ``, + `agent = create_deep_agent(`, + ` model="openai:gpt-4.1",`, + ` tools=[],`, + ` subagents=[researcher, writer],`, + `)`, + ``, + `agent.invoke({"messages": "Research quantum computing and write a 200-word summary."})`, + ].join('\n'), + filePath: 'examples/subagents.py', + startLine: 1, + highlightLines: [4, 5, 6, 7, 11, 12, 13, 14, 19, 20, 21, 22], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 14: Subagents -- context isolation diagram +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.7 ISOLATION', + title: 'Isolated context for subagents', + sectionNumber: 3, + source: 'blog.langchain.com/introducing-deepagents-cli', + }); + + const mainX = 0.5; + const mainY = layouts.CONTENT_TOP; + const mainW = 3.0; + const mainH = 3.5; + slide.addShape(pres.ShapeType.roundRect, { + x: mainX, y: mainY, w: mainW, h: mainH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.accent.primary, width: 1.5 }, + rectRadius: 0.1, + }); + slide.addText('MAIN AGENT', { + x: mainX + 0.15, y: mainY + 0.1, w: mainW - 0.3, h: 0.3, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 11, + color: theme.palette.accent.primary, + bold: true, + charSpacing: 4, + }); + slide.addText(`context = [ + user task, + summary from researcher, + summary from writer +]`, { + x: mainX + 0.15, y: mainY + 0.5, w: mainW - 0.3, h: mainH - 0.7, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: 12, + color: theme.palette.text.primary, + valign: 'top', + }); + + slide.addShape(pres.ShapeType.line, { + x: mainX + mainW, y: mainY + mainH / 2, w: 0.6, h: 0, + line: { color: theme.palette.accent.secondary, width: 2, endArrowType: 'triangle' }, + }); + slide.addText(`task("researcher")`, { + x: mainX + mainW + 0.02, y: mainY + mainH / 2 - 0.25, w: 1.4, h: 0.5, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: 11, + color: theme.palette.accent.secondary, + bold: true, + align: 'center', + }); + + const subX = 4.95; + const subY1 = layouts.CONTENT_TOP; + const subW = 4.55; + const subH = 1.6; + slide.addShape(pres.ShapeType.roundRect, { + x: subX, y: subY1, w: subW, h: subH, + fill: { color: theme.palette.bg.code }, + line: { color: theme.palette.border.accent, width: 1.2 }, + rectRadius: 0.08, + }); + slide.addText('SUBAGENT: researcher', { + x: subX + 0.15, y: subY1 + 0.08, w: subW - 0.3, h: 0.28, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 11, + color: theme.palette.accent.tertiary, + bold: true, + charSpacing: 2, + }); + slide.addText(`context = [ + full web_search results, + all 14 sources, + notes, drafts, citations +] +return: 150-word summary`, { + x: subX + 0.15, y: subY1 + 0.4, w: subW - 0.3, h: subH - 0.5, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: 11, + color: theme.palette.text.secondary, + valign: 'top', + }); + + const subY2 = subY1 + subH + 0.3; + slide.addShape(pres.ShapeType.roundRect, { + x: subX, y: subY2, w: subW, h: subH, + fill: { color: theme.palette.bg.code }, + line: { color: theme.palette.border.accent, width: 1.2 }, + rectRadius: 0.08, + }); + slide.addText('SUBAGENT: writer', { + x: subX + 0.15, y: subY2 + 0.08, w: subW - 0.3, h: 0.28, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 11, + color: theme.palette.accent.tertiary, + bold: true, + charSpacing: 2, + }); + slide.addText(`context = [ + researcher summary, + original user task +] +return: polished 200-word report`, { + x: subX + 0.15, y: subY2 + 0.4, w: subW - 0.3, h: subH - 0.5, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: 11, + color: theme.palette.text.secondary, + valign: 'top', + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 15: Virtual filesystem -- state['files'] +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.8 VIRTUAL FS', + title: `Virtual FS: state files dict`, + sectionNumber: 3, + source: 'github.com/langchain-ai/deepagents README', + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 1.1, + kind: 'info', + title: 'Files live in graph state, not on disk by default.', + text: `StateBackend keeps everything in state["files"]. Survives across +turns in the same thread; never touches disk unless you swap backends.`, + }); + + codeBlock(slide, { + x: 0.5, y: 2.6, w: 9.0, h: 2.3, + code: [ + `# Inspect the virtual filesystem after a run:`, + `result = agent.invoke({"messages": "Summarize repo"})`, + ``, + `files = result.get("files", {})`, + `for path, doc in files.items():`, + ` print(f"{path}: {len(doc.get('content', []))} bytes")`, + ``, + `# /repo/src/main.py: 421 bytes`, + `# /repo/README.md: 1804 bytes`, + ].join('\n'), + filePath: 'examples/virtual_fs.py', + startLine: 1, + highlightLines: [3, 4, 5, 6], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 16: Middleware -- four hook points +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.9 MIDDLEWARE', + title: 'Middleware: four hook points', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/langchain/middleware', + }); + + const midX = 2.4; + const midY = layouts.CONTENT_TOP + 0.1; + const midW = 2.4; + const midH = 1.85; + slide.addShape(pres.ShapeType.roundRect, { + x: midX, y: midY, w: midW, h: 0.95, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.accent.tertiary, width: 1.5 }, + rectRadius: 0.1, + }); + slide.addText('MODEL\n(LLM call)', { + x: midX, y: midY + 0.05, w: midW, h: 0.85, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 13, + color: theme.palette.accent.tertiary, + bold: true, + align: 'center', + valign: 'middle', + }); + slide.addShape(pres.ShapeType.roundRect, { + x: midX, y: midY + midH, w: midW, h: 0.95, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.accent.primary, width: 1.5 }, + rectRadius: 0.1, + }); + slide.addText('TOOLS\n(execute)', { + x: midX, y: midY + midH + 0.05, w: midW, h: 0.85, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 13, + color: theme.palette.accent.primary, + bold: true, + align: 'center', + valign: 'middle', + }); + + // Connector arrow MODEL -> TOOLS + slide.addShape(pres.ShapeType.downArrow, { + x: midX + midW / 2 - 0.15, y: midY + 1.0, w: 0.3, h: 0.8, + fill: { color: theme.palette.accent.secondary }, + line: { type: 'none' }, + }); + + // Hook labels: 4 small pills around the central column + const hooks = [ + { x: 0.6, y: midY + 0.1, label: 'before_model' }, + { x: 0.6, y: midY + 0.6, label: 'after_model' }, + { x: midX + midW + 0.3, y: midY + midH + 0.1, label: 'before_tool' }, + { x: midX + midW + 0.3, y: midY + midH + 0.6, label: 'after_tool' }, + ]; + for (const h of hooks) { + slide.addShape(pres.ShapeType.roundRect, { + x: h.x, y: h.y, w: 1.65, h: 0.4, + fill: { color: theme.palette.bg.code }, + line: { color: theme.palette.accent.secondary, width: 1.2 }, + rectRadius: 0.06, + }); + slide.addText(h.label, { + x: h.x, y: h.y, w: 1.65, h: 0.4, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: 11, + color: theme.palette.accent.secondary, + bold: true, + align: 'center', + valign: 'middle', + }); + } + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.4, w: 9.0, h: 0.55, + kind: 'info', + text: `Same middleware system as LangChain 1.0 create_agent. +Mix custom middleware with built-ins: Summarization, HumanInTheLoop, Filesystem, SubAgent.`, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 17: Middleware -- example +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.9 MIDDLEWARE', + title: 'Custom middleware: logging + PII redaction', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/langchain/middleware', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5, + code: [ + `from langchain.agents.middleware import AgentMiddleware`, + `from deepagents import create_deep_agent`, + ``, + `class LoggerMiddleware(AgentMiddleware):`, + ` def before_model(self, state, runtime):`, + ` print(f"[model] {len(state['messages'])} msgs in")`, + ` return state`, + ``, + ` def after_tool(self, state, runtime, tool_result):`, + ` print(f"[tool] {tool_result.tool_call_id} -> ` + + `{len(str(tool_result.content))} chars")`, + ` return state`, + ``, + `agent = create_deep_agent(`, + ` model="openai:gpt-4.1",`, + ` middleware=[LoggerMiddleware(), HumanInTheLoopMiddleware(`, + ` interrupt_on={"execute": True}, # ask before running bash`, + ` )],`, + `)`, + ].join('\n'), + filePath: 'examples/middleware.py', + startLine: 1, + highlightLines: [5, 6, 7, 8, 9, 10, 16, 17, 18], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 18: Backends -- overview +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.10 BACKENDS', + title: 'Pluggable backends: 4 flavors', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/backends', + }); + + const cards = [ + { + name: 'StateBackend', + sub: 'default', + desc: `Everything in graph state["files"]. Zero disk I/O. +Perfect for short-lived, sandboxed runs.`, + color: theme.palette.accent.tertiary, + }, + { + name: 'FilesystemBackend', + sub: 'local disk', + desc: `Real directory on the host. Persists across runs. +Use when the agent should see/edit your repo directly.`, + color: theme.palette.accent.primary, + }, + { + name: 'StoreBackend', + sub: 'cross-thread', + desc: `Lives in a LangGraph Store (Postgres, Redis). +Shared between threads and across sessions.`, + color: theme.palette.accent.secondary, + }, + { + name: 'CompositeBackend', + sub: 'route by path', + desc: `Route reads/writes to different backends depending on path prefix. +"/workspace" -> Filesystem, "/memory" -> Store.`, + color: theme.palette.state.success, + }, + ]; + + const cardY = layouts.CONTENT_TOP; + const cardH = 1.65; + const cardW = 4.4; + for (let i = 0; i < cards.length; i += 1) { + const col = i % 2; + const row = Math.floor(i / 2); + const x = 0.5 + col * (cardW + 0.2); + const y = cardY + row * (cardH + 0.2); + slide.addShape(pres.ShapeType.roundRect, { + x: x, y: y, w: cardW, h: cardH, + fill: { color: theme.palette.bg.elevated }, + line: { color: cards[i].color, width: 1.2 }, + rectRadius: 0.08, + }); + slide.addText(cards[i].name, { + x: x + 0.2, y: y + 0.1, w: cardW - 0.4, h: 0.3, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 14, + color: cards[i].color, + bold: true, + }); + slide.addText(cards[i].sub, { + x: x + 0.2, y: y + 0.4, w: cardW - 0.4, h: 0.25, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 10, + color: theme.palette.text.muted, + italic: true, + }); + slide.addText(cards[i].desc, { + x: x + 0.2, y: y + 0.65, w: cardW - 0.4, h: cardH - 0.75, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 11, + color: theme.palette.text.secondary, + valign: 'top', + }); + } +} + +// --------------------------------------------------------------------------- +// SLIDE 19: Backends -- CompositeBackend example +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.10 COMPOSITE', + title: 'CompositeBackend: route by path prefix', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/backends', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5, + code: [ + `from deepagents import create_deep_agent`, + `from deepagents.backends import (`, + ` CompositeBackend, FilesystemBackend, StoreBackend`, + `)`, + `from langgraph.store.memory import InMemoryStore`, + ``, + `store = InMemoryStore() # or PostgresStore in prod`, + ``, + `backend = CompositeBackend(`, + ` default=FilesystemBackend(root_dir="./workspace"),`, + ` routes={`, + ` "/memory/": StoreBackend(store=store, namespace=("agent", "kb")),`, + ` "/scratch/": FilesystemBackend(root_dir="/tmp/scratch"),`, + ` },`, + `)`, + ``, + `agent = create_deep_agent(model=..., backend=backend)`, + `# /workspace/notes.md -> local disk`, + `# /memory/lessons.md -> Postgres, shared across sessions`, + `# /scratch/tmp.py -> ephemeral tmpfs`, + ].join('\n'), + filePath: 'examples/composite_backend.py', + startLine: 1, + highlightLines: [12, 13, 14, 15, 20, 21, 22], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 20: Human-in-the-loop +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.11 HITL', + title: 'interrupt_on: approve tool calls', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/human-in-the-loop', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.4, + code: [ + `from langchain.agents.middleware import HumanInTheLoopMiddleware`, + `from deepagents import create_deep_agent`, + `from langgraph.checkpoint.memory import InMemorySaver`, + `from langgraph.types import Command`, + ``, + `agent = create_deep_agent(`, + ` model="openai:gpt-4.1",`, + ` checkpointer=InMemorySaver(),`, + ` middleware=[HumanInTheLoopMiddleware(`, + ` interrupt_on={`, + ` "execute": True, # bash -- always ask`, + ` "write_file": True, # disk writes -- always ask`, + ` "task": False, # subagents -- run unattended`, + ` },`, + ` )],`, + `)`, + ``, + `cfg = {"configurable": {"thread_id": "user-42"}}`, + `try:`, + ` agent.invoke({"messages": "deploy to staging"}, cfg)`, + `except InterruptedError:`, + ` decision = ask_user("Approve execute()?") # your UI`, + ` agent.invoke(Command(resume=decision), cfg)`, + ].join('\n'), + filePath: 'examples/hitl.py', + startLine: 1, + highlightLines: [9, 10, 11, 12, 13, 14, 24, 25, 26, 27], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 21: Streaming +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.12 STREAMING', + title: 'stream_mode: tokens, updates, events', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/streaming', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.7, h: 3.5, + code: [ + `cfg = {"configurable": {"thread_id": "user-42"}}`, + ``, + `# 1. Stream model tokens as they arrive`, + `for token, meta in agent.stream(`, + ` {"messages": "..."}, cfg,`, + ` stream_mode="messages",`, + `):`, + ` print(token.content, end="", flush=True)`, + ``, + `# 2. Stream state updates per node`, + `for chunk in agent.stream(`, + ` {"messages": "..."}, cfg,`, + ` stream_mode="updates",`, + `):`, + ` print(chunk) # {"model": {...}, "tools": {...}}`, + ``, + `# 3. Subagent streams are surfaced as`, + `# {"subagent": {"name": "researcher", "chunk": ...}}`, + ].join('\n'), + filePath: 'examples/streaming.py', + startLine: 1, + highlightLines: [4, 5, 6, 12, 13, 14], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.4, y: layouts.CONTENT_TOP, w: 3.1, h: 3.5, + kind: 'info', + title: 'stream_mode values', + text: `- "values": full state after each node +- "updates": delta per node (LangGraph-style) +- "messages": token-by-token LLM output +- "events": low-level LangGraph events +- "custom": writer().emit(...) from inside nodes`, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 22: LangSmith integration +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.13 LANGSMITH', + title: 'Tracing and evaluation: works out of the box', + sectionNumber: 3, + source: 'docs.smith.langchain.com', + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 0.9, + kind: 'info', + title: 'No code changes required.', + text: `Set LANGSMITH_TRACING=true plus LANGSMITH_API_KEY and LANGSMITH_PROJECT. +Every deep-agent run is traced as a parent run with subagent runs nested underneath.`, + }); + + codeBlock(slide, { + x: 0.5, y: 2.55, w: 9.0, h: 2.3, + code: [ + `export LANGSMITH_TRACING=true`, + `export LANGSMITH_API_KEY=lsv2_...`, + `export LANGSMITH_PROJECT=deepagents-evals`, + ``, + `python my_deep_agent.py`, + `# -> all runs visible in smith.langchain.com`, + `# -> subagent runs nested under the parent`, + `# -> token usage, latency, tool errors captured`, + ].join('\n'), + filePath: 'examples/langsmith.sh', + startLine: 1, + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 23: Example -- research agent +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.14 RESEARCH AGENT', + title: 'Real example: arXiv research agent', + sectionNumber: 3, + source: 'github.com/langchain-ai/deepagents examples/', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5, + code: [ + `from langchain.tools import tool`, + `from deepagents import create_deep_agent`, + ``, + `@tool`, + `def arxiv_search(query: str, max_results: int = 5) -> str:`, + ` """Search arXiv for papers matching the query."""`, + ` import arxiv`, + ` client = arxiv.Client()`, + ` results = list(client.results(arxiv.Search(query=query, ` + + `max_results=max_results)))`, + ` return "\\n\\n".join(`, + ` f"{r.title}\\n{r.summary[:300]}..." for r in results`, + ` )`, + ``, + `agent = create_deep_agent(`, + ` model="openai:gpt-4.1",`, + ` tools=[arxiv_search],`, + ` system_prompt=("You are a research assistant. Always cite paper titles ` + + `and arXiv IDs."),`, + ` subagents=[{`, + ` "name": "summarizer",`, + ` "description": "Compresses paper abstracts into a paragraph",`, + ` "system_prompt": "You are a precise summarizer.",`, + ` "tools": [],`, + ` }],`, + `)`, + ].join('\n'), + filePath: 'examples/research_agent.py', + startLine: 1, + highlightLines: [18, 19, 20, 21, 22, 23, 24, 25], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 24: Example -- coding agent +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.14 CODING AGENT', + title: 'Real example: coding agent, sandboxed', + sectionNumber: 3, + source: 'github.com/langchain-ai/deepagents README', + }); + + codeBlock(slide, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 3.5, + code: [ + `from deepagents import create_deep_agent`, + `from deepagents.backends import SandboxBackend`, + ``, + `agent = create_deep_agent(`, + ` model="anthropic:claude-sonnet-4-5",`, + ` backend=SandboxBackend(`, + ` provider="daytona", # or modal / runloop`, + ` api_key=os.environ["DAYTONA_API_KEY"],`, + ` image="python:3.12-slim",`, + ` ),`, + ` system_prompt=("You are a coding agent. Always run tests after edits. ` + + `Stop and ask if requirements are ambiguous."),`, + `)`, + ``, + `agent.invoke({"messages": "Add a /healthz endpoint to the FastAPI app, ` + + `with tests."})`, + ``, + `# Daytona/Modal/Runloop execute code in an isolated container;`, + `# the local process never sees a stray rm -rf.`, + ].join('\n'), + filePath: 'examples/coding_agent.py', + startLine: 1, + highlightLines: [4, 5, 6, 7, 8, 9, 10, 11], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 25: What's new in 1.0 + TypeScript +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.15 WHAT IS NEW', + title: '1.0-rc and the TypeScript port', + sectionNumber: 3, + source: 'github.com/langchain-ai/deepagents/releases', + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 4.4, h: 3.5, + kind: 'info', + title: 'What is new in 1.0-rc', + text: `- Full LangChain 1.0 middleware integration +- Stable Send API for parallel subagents +- Pluggable backends marked stable +- Skills: versioning + hot-reload +- CompositeBackend GA +- Note: as of snapshot 2026-06-22, latest + stable is 0.6.11 -- 1.0 ships before EOY 2026`, + }); + + codeBlock(slide, { + x: 5.1, y: layouts.CONTENT_TOP, w: 4.4, h: 3.5, + code: [ + `// TypeScript analogue (deepagentsjs)`, + `import { createDeepAgent } from "deepagents";`, + `import { tool, z } from "@langchain/core/tools";`, + ``, + `const search = tool(`, + ` async ({ q }) => fetch("/api/search?q=" + q).then(r => r.text()),`, + ` { name: "search", schema: z.object({ q: z.string() }) },`, + `);`, + ``, + `const agent = await createDeepAgent({`, + ` model: "openai:gpt-4.1",`, + ` tools: [search],`, + `});`, + ].join('\n'), + filePath: 'examples/deepagentsjs.ts', + startLine: 1, + highlightLines: [10, 11, 12, 13], + }); +} + +// --------------------------------------------------------------------------- +// SLIDE 26: Pros/cons + bridge to Open SWE +// --------------------------------------------------------------------------- +{ + const slide = contentSlide({ + eyebrow: '3.16 PROS / CONS', + title: 'When to choose Deep Agents', + sectionNumber: 3, + source: 'docs.langchain.com/oss/python/deepagents/overview', + }); + + helpers.addProsCons(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 9.0, h: 2.5, + pros: [ + 'Batteries included: planning + FS + subagents + HITL + skills in one import', + 'Less boilerplate than raw LangGraph, opinionated defaults that match Claude Code', + 'Pluggable backends (local / Daytona / Modal / Runloop / LangSmith Store)', + 'Skills system: reusable behaviors loaded on-demand', + 'Open source (MIT), traceable through LangSmith out of the box', + ], + cons: [ + '1.0 not yet shipped (0.6.11 latest on snapshot 2026-06-22) -- breaking changes possible', + 'Opinionated: overriding defaults can be awkward', + 'Sandbox providers require external SaaS accounts', + 'Skills ecosystem is nascent, fewer ready-made skills than for Claude Code', + 'Some middleware + backend combinations are not yet documented', + ], + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.05, w: 9.0, h: 0.9, + kind: 'info', + title: 'Bridge to Open SWE', + text: `Open SWE (next section) is a real coding agent that uses Deep Agents as +its harness. All the patterns from this section -- write_todos, subagents, +virtual FS, HITL -- show up unchanged in production at langchain-ai/open-swe.`, + }); +} + +// --------------------------------------------------------------------------- +// Save +// --------------------------------------------------------------------------- + +const outDir = path.resolve(__dirname); +const outFile = path.join(outDir, 'section3.pptx'); +pres.writeFile({ fileName: outFile }).then((written) => { + // eslint-disable-next-line no-console + console.log('Wrote:', written, '(slides:', pageNum + ')'); + if (pageNum !== TOTAL_SLIDES) { + // eslint-disable-next-line no-console + console.warn('WARNING: expected', TOTAL_SLIDES, 'slides, got', pageNum); + process.exitCode = 1; + } +}).catch((err) => { + // eslint-disable-next-line no-console + console.error('Failed to write pptx:', err); + process.exit(1); +}); diff --git a/slides/section3-deepagents/png/slide-01.png b/slides/section3-deepagents/png/slide-01.png new file mode 100644 index 0000000..c06e3a0 Binary files /dev/null and b/slides/section3-deepagents/png/slide-01.png differ diff --git a/slides/section3-deepagents/png/slide-02.png b/slides/section3-deepagents/png/slide-02.png new file mode 100644 index 0000000..c449ae2 Binary files /dev/null and b/slides/section3-deepagents/png/slide-02.png differ diff --git a/slides/section3-deepagents/png/slide-03.png b/slides/section3-deepagents/png/slide-03.png new file mode 100644 index 0000000..4c2faf3 Binary files /dev/null and b/slides/section3-deepagents/png/slide-03.png differ diff --git a/slides/section3-deepagents/png/slide-04.png b/slides/section3-deepagents/png/slide-04.png new file mode 100644 index 0000000..bfa5ea2 Binary files /dev/null and b/slides/section3-deepagents/png/slide-04.png differ diff --git a/slides/section3-deepagents/png/slide-05.png b/slides/section3-deepagents/png/slide-05.png new file mode 100644 index 0000000..4453094 Binary files /dev/null and b/slides/section3-deepagents/png/slide-05.png differ diff --git a/slides/section3-deepagents/png/slide-06.png b/slides/section3-deepagents/png/slide-06.png new file mode 100644 index 0000000..2e422ee Binary files /dev/null and b/slides/section3-deepagents/png/slide-06.png differ diff --git a/slides/section3-deepagents/png/slide-07.png b/slides/section3-deepagents/png/slide-07.png new file mode 100644 index 0000000..f236f63 Binary files /dev/null and b/slides/section3-deepagents/png/slide-07.png differ diff --git a/slides/section3-deepagents/png/slide-08.png b/slides/section3-deepagents/png/slide-08.png new file mode 100644 index 0000000..148a39d Binary files /dev/null and b/slides/section3-deepagents/png/slide-08.png differ diff --git a/slides/section3-deepagents/png/slide-09.png b/slides/section3-deepagents/png/slide-09.png new file mode 100644 index 0000000..3d440ef Binary files /dev/null and b/slides/section3-deepagents/png/slide-09.png differ diff --git a/slides/section3-deepagents/png/slide-10.png b/slides/section3-deepagents/png/slide-10.png new file mode 100644 index 0000000..7e3af85 Binary files /dev/null and b/slides/section3-deepagents/png/slide-10.png differ diff --git a/slides/section3-deepagents/png/slide-11.png b/slides/section3-deepagents/png/slide-11.png new file mode 100644 index 0000000..5c504da Binary files /dev/null and b/slides/section3-deepagents/png/slide-11.png differ diff --git a/slides/section3-deepagents/png/slide-12.png b/slides/section3-deepagents/png/slide-12.png new file mode 100644 index 0000000..0f390bb Binary files /dev/null and b/slides/section3-deepagents/png/slide-12.png differ diff --git a/slides/section3-deepagents/png/slide-13.png b/slides/section3-deepagents/png/slide-13.png new file mode 100644 index 0000000..34dcf33 Binary files /dev/null and b/slides/section3-deepagents/png/slide-13.png differ diff --git a/slides/section3-deepagents/png/slide-14.png b/slides/section3-deepagents/png/slide-14.png new file mode 100644 index 0000000..3f316c5 Binary files /dev/null and b/slides/section3-deepagents/png/slide-14.png differ diff --git a/slides/section3-deepagents/png/slide-15.png b/slides/section3-deepagents/png/slide-15.png new file mode 100644 index 0000000..8628606 Binary files /dev/null and b/slides/section3-deepagents/png/slide-15.png differ diff --git a/slides/section3-deepagents/png/slide-16.png b/slides/section3-deepagents/png/slide-16.png new file mode 100644 index 0000000..a22b6c7 Binary files /dev/null and b/slides/section3-deepagents/png/slide-16.png differ diff --git a/slides/section3-deepagents/png/slide-17.png b/slides/section3-deepagents/png/slide-17.png new file mode 100644 index 0000000..429c8a6 Binary files /dev/null and b/slides/section3-deepagents/png/slide-17.png differ diff --git a/slides/section3-deepagents/png/slide-18.png b/slides/section3-deepagents/png/slide-18.png new file mode 100644 index 0000000..c49083b Binary files /dev/null and b/slides/section3-deepagents/png/slide-18.png differ diff --git a/slides/section3-deepagents/png/slide-19.png b/slides/section3-deepagents/png/slide-19.png new file mode 100644 index 0000000..d567391 Binary files /dev/null and b/slides/section3-deepagents/png/slide-19.png differ diff --git a/slides/section3-deepagents/png/slide-20.png b/slides/section3-deepagents/png/slide-20.png new file mode 100644 index 0000000..423a4f1 Binary files /dev/null and b/slides/section3-deepagents/png/slide-20.png differ diff --git a/slides/section3-deepagents/png/slide-21.png b/slides/section3-deepagents/png/slide-21.png new file mode 100644 index 0000000..394d099 Binary files /dev/null and b/slides/section3-deepagents/png/slide-21.png differ diff --git a/slides/section3-deepagents/png/slide-22.png b/slides/section3-deepagents/png/slide-22.png new file mode 100644 index 0000000..ded0a57 Binary files /dev/null and b/slides/section3-deepagents/png/slide-22.png differ diff --git a/slides/section3-deepagents/png/slide-23.png b/slides/section3-deepagents/png/slide-23.png new file mode 100644 index 0000000..6f277c3 Binary files /dev/null and b/slides/section3-deepagents/png/slide-23.png differ diff --git a/slides/section3-deepagents/png/slide-24.png b/slides/section3-deepagents/png/slide-24.png new file mode 100644 index 0000000..8776ef0 Binary files /dev/null and b/slides/section3-deepagents/png/slide-24.png differ diff --git a/slides/section3-deepagents/png/slide-25.png b/slides/section3-deepagents/png/slide-25.png new file mode 100644 index 0000000..6e82346 Binary files /dev/null and b/slides/section3-deepagents/png/slide-25.png differ diff --git a/slides/section3-deepagents/png/slide-26.png b/slides/section3-deepagents/png/slide-26.png new file mode 100644 index 0000000..ff84702 Binary files /dev/null and b/slides/section3-deepagents/png/slide-26.png differ diff --git a/slides/section3-deepagents/section3.pdf b/slides/section3-deepagents/section3.pdf new file mode 100644 index 0000000..5463bb5 Binary files /dev/null and b/slides/section3-deepagents/section3.pdf differ diff --git a/slides/section3-deepagents/section3.pptx b/slides/section3-deepagents/section3.pptx new file mode 100644 index 0000000..6e82b9a Binary files /dev/null and b/slides/section3-deepagents/section3.pptx differ diff --git a/slides/section4-openswe/01-cover.js b/slides/section4-openswe/01-cover.js new file mode 100644 index 0000000..2bf2757 --- /dev/null +++ b/slides/section4-openswe/01-cover.js @@ -0,0 +1,28 @@ +/** + * slides/01-cover.js + * ---------------------------------------------------------------------------- + * Slide 01 -- Cover + * Section divider style with stage number, title, intro paragraph. + */ +'use strict'; + +const { helpers } = require('../../design-system'); + +function buildCover(pres, theme) { + const slide = pres.addSlide(); + helpers.addSectionDivider(slide, pres, theme, { + number: '4', + eyebrow: 'STAGE 4', + title: 'Open SWE: async coding agent', + intro: + 'Open-source фреймворк LangChain Inc. для построения внутренних ' + + 'кодинг-агентов организации. Reference architecture поверх Deep Agents, ' + + 'pluggable sandboxes, триггеры из Slack/Linear/GitHub, draft PR ' + + 'автоматически. Воспроизводит паттерны Stripe Minions, Ramp Inspect, ' + + 'Coinbase Cloudbot -- но с открытым исходным кодом.', + }); + helpers.addPageNumber(slide, pres, theme, 1); + return slide; +} + +module.exports = { buildCover }; diff --git a/slides/section4-openswe/02-what-is-openswe.js b/slides/section4-openswe/02-what-is-openswe.js new file mode 100644 index 0000000..b23f358 --- /dev/null +++ b/slides/section4-openswe/02-what-is-openswe.js @@ -0,0 +1,71 @@ +/** + * slides/02-what-is-openswe.js + * ---------------------------------------------------------------------------- + * Slide 02 -- What is Open SWE: positioning + * One big picture slide with positioning callout. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildWhatIsOpenSWE(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Open SWE: позиционирование', + }); + + // Left column: intro callout + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 4.5, h: 1.55, + kind: 'info', + title: 'Не готовый продукт', + text: + 'Стартовый шаблон, не ' + + 'SaaS. Ops-работа: sandbox, ' + + 'модель, триггеры, промпты.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 3.1, w: 4.5, h: 1.75, + kind: 'success', + title: 'Colleague, not copilot', + text: + 'Внутренний кодинг-агент, ' + + 'не IDE-assistant. Slack / ' + + 'Linear / GitHub -> draft PR ' + + 'с тестами.', + }); + + // Right column: key facts + helpers.addCodeBlock(slide, pres, theme, { + x: 5.3, y: layouts.CONTENT_TOP, w: 4.2, h: 3.4, + language: 'python', + code: [ + '# github.com/langchain-ai/open-swe', + '', + 'stars: ~10k', + 'commits: 971+', + 'license: MIT', + 'language: Python + TypeScript', + 'announce: 08.2025', + 'rewrite: 03.2026', + '', + '# blog.langchain.com/open-swe', + '# INSTALLATION.md', + '# CUSTOMIZATION.md', + ].join('\n'), + filePath: 'meta: open-swe repo', + startLine: 1, + }); + + helpers.addPageNumber(slide, pres, theme, 2); + helpers.addSourceLine(slide, pres, theme, { + source: 'github.com/langchain-ai/open-swe (README + INSTALLATION.md)', + }); + return slide; +} + +module.exports = { buildWhatIsOpenSWE }; diff --git a/slides/section4-openswe/03-architecture-overview.js b/slides/section4-openswe/03-architecture-overview.js new file mode 100644 index 0000000..8f0871d --- /dev/null +++ b/slides/section4-openswe/03-architecture-overview.js @@ -0,0 +1,101 @@ +/** + * slides/03-architecture-overview.js + * ---------------------------------------------------------------------------- + * Slide 03 -- Architecture overview: layered model + * Diagram + import surface code. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildArchitectureOverview(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Архитектура: 7 слоев', + }); + + // Left: architecture diagram + const diagX = 0.5; + const diagY = layouts.CONTENT_TOP; + const diagW = 4.4; + const diagH = 3.5; + + slide.addShape(pres.ShapeType.roundRect, { + x: diagX, y: diagY, w: diagW, h: diagH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.08, + }); + + // 7 stacked layers + const layers = [ + ['Triggers', 'Slack / Linear / GitHub / Web UI'], + ['Validation', 'prompt + middleware (HITL, approval)'], + ['Orchestration','subagents + middleware'], + ['Context', 'AGENTS.md из репозитория'], + ['Tools', 'execute, fetch_url, linear_comment, slack_thread_reply'], + ['Sandbox', 'Modal / Daytona / Runloop / LangSmith'], + ['Harness', 'create_deep_agent (Deep Agents)'], + ]; + + const layerH = (diagH - 0.3) / layers.length; + layers.forEach(function (layer, idx) { + const y = diagY + 0.15 + idx * layerH; + slide.addShape(pres.ShapeType.rect, { + x: diagX + 0.15, y: y, w: diagW - 0.3, h: layerH - 0.05, + fill: { color: idx === layers.length - 1 + ? theme.palette.accent.primary + : theme.palette.bg.code }, + line: { color: theme.palette.border.subtle, width: 0.5 }, + }); + slide.addText(layer[0], { + x: diagX + 0.25, y: y + 0.02, w: diagW - 0.5, h: 0.22, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: theme.sizes.body, + color: idx === layers.length - 1 + ? theme.palette.text.inverse + : theme.palette.text.primary, + bold: true, + }); + slide.addText(layer[1], { + x: diagX + 0.25, y: y + 0.22, w: diagW - 0.5, h: layerH - 0.27, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: theme.sizes.caption, + color: idx === layers.length - 1 + ? theme.palette.text.inverse + : theme.palette.text.secondary, + }); + }); + + // Right: import surface + helpers.addCodeBlock(slide, pres, theme, { + x: 5.2, y: layouts.CONTENT_TOP, w: 4.3, h: 3.5, + language: 'python', + code: [ + 'from open_swe.agent import create_agent', + 'from open_swe.middleware import (', + ' check_message_queue_before_model,', + ' notify_step_limit_reached,', + ' open_pr_if_needed,', + ' ToolErrorMiddleware,', + ')', + 'from open_swe.sandbox import (', + ' SandboxBackend,', + ' ModalBackend, DaytonaBackend,', + ')', + ].join('\n'), + filePath: 'open_swe/__init__.py', + startLine: 1, + }); + + helpers.addPageNumber(slide, pres, theme, 3); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 26-50', + }); + return slide; +} + +module.exports = { buildArchitectureOverview }; diff --git a/slides/section4-openswe/04-create-deep-agent.js b/slides/section4-openswe/04-create-deep-agent.js new file mode 100644 index 0000000..d0a99d7 --- /dev/null +++ b/slides/section4-openswe/04-create-deep-agent.js @@ -0,0 +1,70 @@ +/** + * slides/04-create-deep-agent.js + * ---------------------------------------------------------------------------- + * Slide 04 -- create_deep_agent: composition point + * Core entry point with sandbox + middleware. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildCreateDeepAgent(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'create_deep_agent + backend', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 6.0, h: 3.5, + language: 'python', + code: [ + 'from deepagents import create_deep_agent', + 'from open_swe.sandbox import DaytonaBackend', + '', + 'agent = create_deep_agent(', + ' model="anthropic:claude-opus-4-6",', + ' tools=[execute, fetch_url,', + ' linear_comment,', + ' slack_thread_reply],', + ' backend=DaytonaBackend(api_key="..."),', + ' middleware=[open_pr_if_needed],', + ')', + ].join('\n'), + filePath: 'examples/minimal_agent.py', + startLine: 1, + highlightLines: [4, 8, 9], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.8, y: layouts.CONTENT_TOP, w: 2.7, h: 1.4, + kind: 'info', + title: 'Один harness', + text: + 'Март 2026: multi-agent ' + + '(Manager/Planner/Programmer/' + + 'Reviewer) заменили на единый ' + + 'deep-agent harness.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.8, y: 2.9, w: 2.7, h: 1.55, + kind: 'success', + title: 'Upgrade path', + text: + 'Подтягиваешь улучшения Deep ' + + 'Agents бесплатно. Subagents ' + + 'изолируют контекст, middleware ' + + 'дают orchestration.', + }); + + helpers.addPageNumber(slide, pres, theme, 4); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 52-77', + }); + return slide; +} + +module.exports = { buildCreateDeepAgent }; diff --git a/slides/section4-openswe/05-agents-md-context.js b/slides/section4-openswe/05-agents-md-context.js new file mode 100644 index 0000000..a568432 --- /dev/null +++ b/slides/section4-openswe/05-agents-md-context.js @@ -0,0 +1,79 @@ +/** + * slides/05-agents-md-context.js + * ---------------------------------------------------------------------------- + * Slide 05 -- AGENTS.md convention + * Context injection pattern. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildAgentsMdConvention(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'AGENTS.md как system prompt', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.6, h: 1.7, + language: 'python', + code: [ + 'from pathlib import Path', + '', + 'def construct_system_prompt(', + ' repo_dir, base_prompt', + '):', + ' agents_md = Path(repo_dir) / "AGENTS.md"', + ' extra = agents_md.read_text() if', + ' agents_md.exists() else ""', + ' return base_prompt + extra', + ].join('\n'), + filePath: 'open_swe/prompts.py', + startLine: 1, + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 3.3, w: 5.6, h: 1.55, + language: 'markdown', + code: [ + '# AGENTS.md (repo root)', + '', + '- use uv, not pip', + '- run pytest before commit', + '- never push to main directly', + ].join('\n'), + filePath: 'AGENTS.md', + startLine: 1, + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: layouts.CONTENT_TOP, w: 3.2, h: 1.7, + kind: 'info', + title: 'Convention', + text: + 'Организационный паттерн. ' + + 'Тот же файл читают Cursor, ' + + 'Aider, Devin.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: 3.3, w: 3.2, h: 1.55, + kind: 'warning', + title: 'Подводный камень', + text: + 'Open SWE доверяет AGENTS.md. ' + + 'Вредоносный блок попадает ' + + 'в system prompt.', + }); + + helpers.addPageNumber(slide, pres, theme, 5); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 113-115, 200-209', + }); + return slide; +} + +module.exports = { buildAgentsMdConvention }; diff --git a/slides/section4-openswe/06-middleware.js b/slides/section4-openswe/06-middleware.js new file mode 100644 index 0000000..f8d54a6 --- /dev/null +++ b/slides/section4-openswe/06-middleware.js @@ -0,0 +1,71 @@ +/** + * slides/06-middleware.js + * ---------------------------------------------------------------------------- + * Slide 06 -- Middleware: AgentMiddleware extension + * Custom middleware pattern. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildMiddleware(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Middleware: точки расширения', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.6, h: 3.5, + language: 'python', + code: [ + 'from langchain.agents.middleware import (', + ' AgentMiddleware,', + ')', + '', + 'class AuditMiddleware(AgentMiddleware):', + ' def after_model(self, state, runtime):', + ' runtime.logger.info(', + ' f"step={state.get(\"step\")}"', + ' )', + ' return state', + '', + ' def before_model(self, state, runtime):', + ' return state # inject reminder', + ].join('\n'), + filePath: 'examples/audit_middleware.py', + startLine: 1, + highlightLines: [5, 6, 7, 8], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: layouts.CONTENT_TOP, w: 3.2, h: 1.7, + kind: 'info', + title: 'Built-in middleware', + text: + '* check_message_queue\n' + + '* notify_step_limit\n' + + '* open_pr_if_needed\n' + + '* ToolErrorMiddleware', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: 3.0, w: 3.2, h: 1.8, + kind: 'success', + title: 'Типичные кастомы', + text: + 'Approval gate перед PR, cost ' + + 'guard на длину контекста, ' + + 'redaction секретов в логах.', + }); + + helpers.addPageNumber(slide, pres, theme, 6); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 117-136, 213-223', + }); + return slide; +} + +module.exports = { buildMiddleware }; diff --git a/slides/section4-openswe/07-sandbox-providers.js b/slides/section4-openswe/07-sandbox-providers.js new file mode 100644 index 0000000..88b5538 --- /dev/null +++ b/slides/section4-openswe/07-sandbox-providers.js @@ -0,0 +1,99 @@ +/** + * slides/07-sandbox-providers.js + * ---------------------------------------------------------------------------- + * Slide 07 -- Sandbox providers: comparison + * Table of Modal / Daytona / Runloop / LangSmith. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildSandboxProviders(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Sandbox-провайдеры', + }); + + // Comparison table card + const x = 0.5; + const y = layouts.CONTENT_TOP; + const w = 9.0; + const h = 3.55; + + slide.addShape(pres.ShapeType.roundRect, { + x: x, y: y, w: w, h: h, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.08, + }); + + // Table headers + const cols = [ + { label: 'Provider', w: 1.6 }, + { label: 'Setup', w: 1.9 }, + { label: 'Pricing model', w: 1.7 }, + { label: 'Persistent state', w: 1.6 }, + { label: 'Best for', w: 2.2 }, + ]; + + const headerY = y + 0.1; + let cx = x + 0.15; + cols.forEach(function (c) { + slide.addShape(pres.ShapeType.rect, { + x: cx, y: headerY, w: c.w, h: 0.32, + fill: { color: theme.palette.bg.code }, + line: { color: theme.palette.border.subtle, width: 0.5 }, + }); + slide.addText(c.label, { + x: cx + 0.05, y: headerY, w: c.w - 0.1, h: 0.32, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: theme.sizes.caption, + color: theme.palette.accent.tertiary, + bold: true, + charSpacing: 2, + valign: 'middle', + }); + cx += c.w; + }); + + const rows = [ + ['ModalBackend', 'token_id + token_secret', 'per-second + GB-s', 'yes (volume)', 'Python-first, GPU-доступный'], + ['DaytonaBackend','api_key', 'per-session', 'yes (volume)', 'default в README'], + ['RunloopBackend','api_key', 'per-second', 'yes (volume)', 'dev-цикл, snapshot/restart'], + ['LangSmithBackend','LANGSMITH_API_KEY', 'через LangSmith', 'через LS', 'уже платите за LangSmith'], + ]; + + const rowH = 0.7; + rows.forEach(function (row, idx) { + const ry = headerY + 0.32 + idx * rowH; + cx = x + 0.15; + row.forEach(function (cell, cidx) { + slide.addShape(pres.ShapeType.rect, { + x: cx, y: ry, w: cols[cidx].w, h: rowH, + fill: { color: idx % 2 === 0 + ? theme.palette.bg.primary + : theme.palette.bg.overlay }, + line: { color: theme.palette.border.subtle, width: 0.4 }, + }); + slide.addText(cell, { + x: cx + 0.05, y: ry, w: cols[cidx].w - 0.1, h: rowH, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: 9, + color: theme.palette.text.primary, + valign: 'middle', + }); + cx += cols[cidx].w; + }); + }); + + helpers.addPageNumber(slide, pres, theme, 7); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 78-89, 139-145', + }); + return slide; +} + +module.exports = { buildSandboxProviders }; diff --git a/slides/section4-openswe/08-sandbox-imports.js b/slides/section4-openswe/08-sandbox-imports.js new file mode 100644 index 0000000..eeafd56 --- /dev/null +++ b/slides/section4-openswe/08-sandbox-imports.js @@ -0,0 +1,94 @@ +/** + * slides/08-sandbox-imports.js + * ---------------------------------------------------------------------------- + * Slide 08 -- Sandbox imports + custom backend stub + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildSandboxImports(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Sandbox: imports + custom', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.0, h: 3.4, + language: 'python', + code: [ + 'from open_swe.sandbox import (', + ' ModalBackend,', + ' DaytonaBackend,', + ' RunloopBackend,', + ' LangSmithBackend,', + ')', + '', + '# Modal (Python-first, GPU)', + 'backend = ModalBackend(', + ' token_id="..."', + ' token_secret="..."', + ')', + ].join('\n'), + filePath: 'open_swe/sandbox/__init__.py', + startLine: 1, + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 5.7, y: layouts.CONTENT_TOP, w: 3.8, h: 3.4, + language: 'python', + code: [ + '# свой backend: devbox-пул', + 'from open_swe.sandbox import (', + ' SandboxBackend,', + ')', + '', + 'class MyInternalBackend(SandboxBackend):', + ' def execute(self, cmd):', + ' return self._run(cmd)', + '', + ' def read_file(self, p):', + ' return self._fetch(p)', + ].join('\n'), + filePath: 'examples/my_internal_backend.py', + startLine: 1, + highlightLines: [5, 8, 9, 11, 12, 13, 14, 15], + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 5.7, y: layouts.CONTENT_TOP, w: 3.8, h: 3.4, + language: 'python', + code: [ + '# свой backend: devbox-пул', + 'from open_swe.sandbox import (', + ' SandboxBackend,', + ')', + '', + 'class MyInternalBackend(', + ' SandboxBackend', + '):', + ' def __init__(self, conn):', + ' self.conn = conn', + '', + ' def execute(self, cmd):', + ' return self._run(cmd)', + '', + ' def read_file(self, p):', + ' return self._fetch(p)', + ].join('\n'), + filePath: 'examples/my_internal_backend.py', + startLine: 1, + highlightLines: [11, 12, 13, 14, 15, 16, 19, 20, 21, 22], + }); + + helpers.addPageNumber(slide, pres, theme, 8); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 80-86, 250-264', + }); + return slide; +} + +module.exports = { buildSandboxImports }; diff --git a/slides/section4-openswe/09-installation-1.js b/slides/section4-openswe/09-installation-1.js new file mode 100644 index 0000000..f9da33a --- /dev/null +++ b/slides/section4-openswe/09-installation-1.js @@ -0,0 +1,78 @@ +/** + * slides/09-installation-1.js + * ---------------------------------------------------------------------------- + * Slide 09 -- Installation: prerequisites + clone + * Steps 1-2 of 5. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildInstallPart1(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Установка (1/2): шаги 1-2', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.5, h: 1.5, + language: 'bash', + code: [ + '# 1. prerequisites', + 'python --version # >= 3.11', + 'node --version # >= 20', + 'uv --version # или pip', + 'docker --version # для dev', + ].join('\n'), + filePath: 'INSTALLATION.md: step 1', + startLine: 1, + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 3.1, w: 5.5, h: 1.75, + language: 'bash', + code: [ + '# 2. clone + install', + 'git clone https://github.com/', + ' langchain-ai/open-swe.git', + 'cd open-swe', + 'uv sync # или pip install -e .', + ].join('\n'), + filePath: 'INSTALLATION.md: step 2', + startLine: 1, + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.2, y: layouts.CONTENT_TOP, w: 3.3, h: 1.55, + kind: 'info', + title: 'Не SaaS', + text: + 'Open SWE -- не готовый сервис. ' + + 'После клонирования нужно ' + + 'поднять backend, UI, sandbox, ' + + 'GitHub App, LangSmith.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.2, y: 3.1, w: 3.3, h: 1.75, + kind: 'warning', + title: 'ENV-файл', + text: + 'cp .env.example .env, затем ' + + 'заполните:\n' + + '- GITHUB_APP_ID\n' + + '- LANGSMITH_API_KEY\n' + + '- DAYTONA_API_KEY', + }); + + helpers.addPageNumber(slide, pres, theme, 9); + helpers.addSourceLine(slide, pres, theme, { + source: 'github.com/langchain-ai/open-swe/blob/main/INSTALLATION.md', + }); + return slide; +} + +module.exports = { buildInstallPart1 }; diff --git a/slides/section4-openswe/10-installation-2.js b/slides/section4-openswe/10-installation-2.js new file mode 100644 index 0000000..81eada4 --- /dev/null +++ b/slides/section4-openswe/10-installation-2.js @@ -0,0 +1,70 @@ +/** + * slides/10-installation-2.js + * ---------------------------------------------------------------------------- + * Slide 10 -- Installation: services up + UI + * Steps 3-5 of 5. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildInstallPart2(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Установка (2/2): шаги 3-5', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 6.0, h: 3.5, + language: 'bash', + code: [ + '# 3. backend (FastAPI)', + 'uv run apps/open-swe/main.py', + '# -> :8000', + '', + '# 4. UI (TanStack Start + Vite)', + 'cd apps/open-swe-ui', + 'pnpm install && pnpm dev', + '# -> :3000', + '', + '# 5. smoke-test', + 'curl -X POST :8000/webhooks/slack \\', + ' -d \'{"text":"@open-swe hello"}\'', + '# -> {"thread_id": "..."}', + ].join('\n'), + filePath: 'INSTALLATION.md: steps 3-5', + startLine: 1, + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.7, y: layouts.CONTENT_TOP, w: 2.8, h: 1.7, + kind: 'success', + title: 'Готово', + text: + 'Backend :8000\n' + + 'UI :3000\n' + + 'Webhook :8000/webhooks/*\n\n' + + 'Можно слать @open-swe.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.7, y: 3.0, w: 2.8, h: 1.85, + kind: 'info', + title: 'Production', + text: + 'Локальный запуск -- только dev. ' + + 'Для prod: docker compose, k8s, ' + + 'managed Postgres, Vault, TLS.', + }); + + helpers.addPageNumber(slide, pres, theme, 10); + helpers.addSourceLine(slide, pres, theme, { + source: 'INSTALLATION.md + INSTALLATION in repo root', + }); + return slide; +} + +module.exports = { buildInstallPart2 }; diff --git a/slides/section4-openswe/11-github-app.js b/slides/section4-openswe/11-github-app.js new file mode 100644 index 0000000..20c0a49 --- /dev/null +++ b/slides/section4-openswe/11-github-app.js @@ -0,0 +1,72 @@ +/** + * slides/11-github-app.js + * ---------------------------------------------------------------------------- + * Slide 11 -- GitHub App setup + * Manifest + permissions. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildGithubApp(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'GitHub App: manifest', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.7, h: 3.5, + language: 'yaml', + code: [ + '# github-app-manifest.yaml', + 'name: open-swe-internal', + 'url: https://open-swe.example.com', + 'hook_attributes:', + ' url: https://open-swe.example.com/', + ' webhooks/github', + ' events:', + ' - issue_comment', + ' - pull_request', + ' - pull_request_review', + 'default_permissions:', + ' contents: write', + ' pull_requests: write', + ].join('\n'), + filePath: 'config/github-app-manifest.yaml', + startLine: 1, + highlightLines: [3, 4, 5, 6, 7, 8, 9], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.4, y: layouts.CONTENT_TOP, w: 3.1, h: 1.5, + kind: 'info', + title: 'Создание App', + text: + '1. github.com/settings/apps/new\n' + + '2. вставить manifest\n' + + '3. запомнить App ID\n' + + '4. скачать private key', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.4, y: 2.85, w: 3.1, h: 2.0, + kind: 'warning', + title: 'Permissions', + text: + 'contents:write -- ветки\n' + + 'pull_requests:write -- draft PR\n' + + 'issues:write -- комментарии\n' + + 'metadata:read -- repo info', + }); + + helpers.addPageNumber(slide, pres, theme, 11); + helpers.addSourceLine(slide, pres, theme, { + source: 'docs.github.com/apps/creating-github-apps', + }); + return slide; +} + +module.exports = { buildGithubApp }; diff --git a/slides/section4-openswe/12-langsmith.js b/slides/section4-openswe/12-langsmith.js new file mode 100644 index 0000000..b8cbfb0 --- /dev/null +++ b/slides/section4-openswe/12-langsmith.js @@ -0,0 +1,68 @@ +/** + * slides/12-langsmith.js + * ---------------------------------------------------------------------------- + * Slide 12 -- LangSmith setup + API keys + snapshot + * Account creation and runtime config. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildLangSmithSetup(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'LangSmith: setup', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.6, h: 3.5, + language: 'bash', + code: [ + '# .env (НЕ коммитить)', + 'LANGSMITH_TRACING=true', + 'LANGSMITH_ENDPOINT=https://', + ' api.smith.langchain.com', + 'LANGSMITH_API_KEY=lsv2_...', + 'LANGSMITH_PROJECT=open-swe-prod', + 'LANGSMITH_SNAPSHOT=true', + '', + '# для sandbox-прокси:', + 'LANGSMITH_SANDBOX_BACKEND=true', + ].join('\n'), + filePath: '.env', + startLine: 1, + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: layouts.CONTENT_TOP, w: 3.2, h: 1.5, + kind: 'info', + title: 'Зачем snapshot', + text: + 'Каждый thread = checkpoint. ' + + 'Follow-up из всех каналов ' + + 'подхватывают состояние ' + + 'по thread_id.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: 2.85, w: 3.2, h: 2.0, + kind: 'success', + title: 'API keys', + text: + '* Personal -- smith.langchain.com\n' + + '* Org-level -- shared tracing\n' + + '* Service -- для CI / batch\n' + + 'Rotate каждые 90 дней.', + }); + + helpers.addPageNumber(slide, pres, theme, 12); + helpers.addSourceLine(slide, pres, theme, { + source: 'docs.smith.langchain.com', + }); + return slide; +} + +module.exports = { buildLangSmithSetup }; diff --git a/slides/section4-openswe/13-triggers-overview.js b/slides/section4-openswe/13-triggers-overview.js new file mode 100644 index 0000000..0ef435e --- /dev/null +++ b/slides/section4-openswe/13-triggers-overview.js @@ -0,0 +1,119 @@ +/** + * slides/13-triggers-overview.js + * ---------------------------------------------------------------------------- + * Slide 13 -- Triggers: 3 surface overview + * Slack / Linear / GitHub with @open-swe pattern. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildTriggersOverview(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Triggers overview', + }); + + // 3 trigger cards + const cardY = layouts.CONTENT_TOP; + const cardH = 3.5; + const cardW = 2.95; + const gap = 0.13; + + const triggers = [ + { + name: 'Slack', + icon: '@', + desc: 'В любом thread канала. Поддерживает синтаксис repo:owner/name.', + syntax: '@open-swe fix the auth bug\nrepo:acme/api', + color: theme.palette.accent.primary, + }, + { + name: 'Linear', + icon: '#', + desc: 'В комментарии к issue. Привязывает thread к Linear-тикету.', + syntax: '@openswe implement the\nacceptance criteria', + color: theme.palette.accent.secondary, + }, + { + name: 'GitHub', + icon: 'PR', + desc: 'В PR-комментарии для авто-ответа на review-комментарии.', + syntax: '@openswe address\nthe review comments', + color: theme.palette.accent.tertiary, + }, + ]; + + triggers.forEach(function (t, idx) { + const cx = 0.5 + idx * (cardW + gap); + + slide.addShape(pres.ShapeType.roundRect, { + x: cx, y: cardY, w: cardW, h: cardH, + fill: { color: theme.palette.bg.elevated }, + line: { color: t.color, width: 1.2 }, + rectRadius: 0.08, + }); + + // Icon badge + slide.addShape(pres.ShapeType.roundRect, { + x: cx + 0.2, y: cardY + 0.2, w: 0.55, h: 0.55, + fill: { color: t.color }, + line: { color: t.color, width: 1 }, + rectRadius: 0.08, + }); + slide.addText(t.icon, { + x: cx + 0.2, y: cardY + 0.2, w: 0.55, h: 0.55, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 18, + color: theme.palette.text.inverse, + bold: true, + align: 'center', + valign: 'middle', + }); + + // Name + slide.addText(t.name, { + x: cx + 0.85, y: cardY + 0.2, w: cardW - 1.0, h: 0.55, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: theme.sizes.h3, + color: theme.palette.text.primary, + bold: true, + valign: 'middle', + }); + + // Desc + slide.addText(t.desc, { + x: cx + 0.2, y: cardY + 0.9, w: cardW - 0.4, h: 1.05, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: theme.sizes.body, + color: theme.palette.text.secondary, + valign: 'top', + }); + + // Syntax code + slide.addShape(pres.ShapeType.roundRect, { + x: cx + 0.2, y: cardY + 2.05, w: cardW - 0.4, h: 1.25, + fill: { color: theme.palette.bg.code }, + line: { color: theme.palette.border.subtle, width: 0.5 }, + rectRadius: 0.06, + }); + slide.addText(t.syntax, { + x: cx + 0.3, y: cardY + 2.1, w: cardW - 0.6, h: 1.15, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: theme.sizes.code, + color: theme.palette.text.primary, + valign: 'top', + }); + }); + + helpers.addPageNumber(slide, pres, theme, 13); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 90-97', + }); + return slide; +} + +module.exports = { buildTriggersOverview }; diff --git a/slides/section4-openswe/14-triggers-thread-id.js b/slides/section4-openswe/14-triggers-thread-id.js new file mode 100644 index 0000000..de84248 --- /dev/null +++ b/slides/section4-openswe/14-triggers-thread-id.js @@ -0,0 +1,68 @@ +/** + * slides/14-triggers-thread-id.js + * ---------------------------------------------------------------------------- + * Slide 14 -- Trigger routing + deterministic thread_id + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildTriggerRouting(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Triggers: thread_id routing', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.6, h: 3.5, + language: 'python', + code: [ + '# open_swe/triggers/router.py', + 'def thread_id_for(source, ref, comment_id):', + ' # Deterministic thread_id:', + ' # все follow-up попадают в один run.', + ' if source == "slack":', + ' return f"slack:{ref}"', + ' if source == "linear":', + ' return f"linear:{ref}"', + ' if source == "github":', + ' return f"github:{ref}:{comment_id}"', + ' raise ValueError(source)', + ].join('\n'), + filePath: 'open_swe/triggers/router.py', + startLine: 1, + highlightLines: [2, 3, 4, 5, 6, 7, 8, 9], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: layouts.CONTENT_TOP, w: 3.2, h: 1.55, + kind: 'info', + title: 'Routing', + text: + 'thread_id из (source, ref). ' + + 'Если run бежит -- новые ' + + 'сообщения ждут в очереди.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: 3.05, w: 3.2, h: 1.8, + kind: 'warning', + title: 'Concurrency', + text: + 'Один thread = один run. ' + + 'Параллельность через ' + + 'отдельный thread_id ' + + '(например, отдельный Slack).', + }); + + helpers.addPageNumber(slide, pres, theme, 14); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 96-97, 131-133', + }); + return slide; +} + +module.exports = { buildTriggerRouting }; diff --git a/slides/section4-openswe/15-webhook-endpoints.js b/slides/section4-openswe/15-webhook-endpoints.js new file mode 100644 index 0000000..01b8129 --- /dev/null +++ b/slides/section4-openswe/15-webhook-endpoints.js @@ -0,0 +1,74 @@ +/** + * slides/15-webhook-endpoints.js + * ---------------------------------------------------------------------------- + * Slide 15 -- Webhook endpoints: FastAPI handlers + * /webhooks/{github,linear,slack} + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildWebhookEndpoints(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Webhook endpoints', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 6.0, h: 3.5, + language: 'python', + code: [ + '# apps/open-swe/webhooks.py', + 'from fastapi import APIRouter, Request', + 'from open_swe.triggers.router import (', + ' thread_id_for,', + ')', + '', + 'router = APIRouter()', + '', + '@router.post("/webhooks/slack")', + 'async def slack_webhook(req: Request):', + ' payload = await req.json()', + ' if "@open-swe" not in payload["text"]:', + ' return {"ok": True}', + ' await enqueue_run(thread_id_for(', + ' "slack", payload["channel"]))', + ].join('\n'), + filePath: 'apps/open-swe/webhooks.py', + startLine: 1, + highlightLines: [9, 10, 11, 12, 13, 14, 15], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.7, y: layouts.CONTENT_TOP, w: 2.8, h: 1.55, + kind: 'success', + title: 'Idempotency', + text: + 'Webhook вычисляет thread_id ' + + 'и проверяет наличие run. ' + + 'Если есть -- enqueue, ' + + 'иначе новый run.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.7, y: 2.95, w: 2.8, h: 1.9, + kind: 'warning', + title: 'Signature', + text: + 'Все handlers обязаны ' + + 'проверять X-Signature от ' + + 'Slack / Linear / GitHub. ' + + 'Не доверяйте без verify.', + }); + + helpers.addPageNumber(slide, pres, theme, 15); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md + FastAPI webhook patterns', + }); + return slide; +} + +module.exports = { buildWebhookEndpoints }; diff --git a/slides/section4-openswe/16-dashboard-ui.js b/slides/section4-openswe/16-dashboard-ui.js new file mode 100644 index 0000000..51f8aa9 --- /dev/null +++ b/slides/section4-openswe/16-dashboard-ui.js @@ -0,0 +1,72 @@ +/** + * slides/16-dashboard-ui.js + * ---------------------------------------------------------------------------- + * Slide 16 -- Dashboard UI: TanStack Start + Vite + * UI structure + how it talks to backend. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildDashboardUI(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Dashboard UI', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.7, h: 3.5, + language: 'bash', + code: [ + '# apps/open-swe-ui/', + '# TanStack Start + Vite + React 19', + '', + 'src/routes/', + ' __root.tsx', + ' index.tsx # thread list', + ' thread.$id.tsx # chat', + ' settings.tsx', + 'src/components/', + ' ChatMessage.tsx', + ' DiffViewer.tsx', + 'src/lib/', + ' client.ts # OpenSWEClient', + ].join('\n'), + filePath: 'apps/open-swe-ui/tree.sh', + startLine: 1, + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.4, y: layouts.CONTENT_TOP, w: 3.1, h: 1.5, + kind: 'info', + title: 'Только UI', + text: + 'Dashboard -- presentation ' + + 'layer. Агентская логика в ' + + 'Python backend. UI через ' + + 'REST + SSE.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.4, y: 2.85, w: 3.1, h: 2.0, + kind: 'success', + title: 'Features', + text: + '* GitHub OAuth login\n' + + '* thread list + filters\n' + + '* chat with streaming\n' + + '* diff viewer для PR\n' + + '* per-user settings', + }); + + helpers.addPageNumber(slide, pres, theme, 16); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 270-287', + }); + return slide; +} + +module.exports = { buildDashboardUI }; diff --git a/slides/section4-openswe/17-per-user-settings.js b/slides/section4-openswe/17-per-user-settings.js new file mode 100644 index 0000000..d8d72f8 --- /dev/null +++ b/slides/section4-openswe/17-per-user-settings.js @@ -0,0 +1,73 @@ +/** + * slides/17-per-user-settings.js + * ---------------------------------------------------------------------------- + * Slide 17 -- Per-user settings + team defaults + repos + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildPerUserSettings(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Per-user настройки', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.6, h: 3.5, + language: 'python', + code: [ + '# settings schema (per-user)', + 'USER_DEFAULTS = {', + ' "model": "anthropic:claude-opus-4-6",', + ' "profile": "balanced",', + ' "max_steps": 80,', + ' "auto_open_pr": False,', + ' "require_approval": True,', + ' "extra_repos": ["acme/internal-tools"],', + '}', + '', + '# team defaults (allowlist)', + 'TEAM_DEFAULTS = {', + ' "sandbox_backend": "ModalBackend",', + ' "allowed_repos": ["acme/*"],', + '}', + ].join('\n'), + filePath: 'open_swe/settings.py', + startLine: 1, + highlightLines: [3, 4, 5, 6, 7, 11, 12], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: layouts.CONTENT_TOP, w: 3.2, h: 1.55, + kind: 'info', + title: 'User mappings', + text: + 'GitHub login -> Slack user -> ' + + 'Linear user. Один человек ' + + 'получает свой профиль во ' + + 'всех трех каналах.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: 2.95, w: 3.2, h: 1.9, + kind: 'warning', + title: 'Precedence', + text: + 'user > team > global. Per-user ' + + 'override работает для всех ' + + 'полей, кроме allowed_repos ' + + '-- это team-level allowlist.', + }); + + helpers.addPageNumber(slide, pres, theme, 17); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 270-280', + }); + return slide; +} + +module.exports = { buildPerUserSettings }; diff --git a/slides/section4-openswe/18-customization-6-points.js b/slides/section4-openswe/18-customization-6-points.js new file mode 100644 index 0000000..f493954 --- /dev/null +++ b/slides/section4-openswe/18-customization-6-points.js @@ -0,0 +1,94 @@ +/** + * slides/18-customization-6-points.js + * ---------------------------------------------------------------------------- + * Slide 18 -- 6 точек кастомизации + * sandbox / model / tools / triggers / prompt / middleware + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildCustomization6(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: '6 точек кастомизации', + }); + + const items = [ + { num: '1', name: 'Sandbox provider', code: 'backend=ModalBackend(...)' }, + { num: '2', name: 'Model', code: 'init_chat_model("anthropic:...")' }, + { num: '3', name: 'Tools', code: 'tools=[execute, fetch_url, ...]' }, + { num: '4', name: 'Triggers', code: 'router.add_route("/my", my_handler)' }, + { num: '5', name: 'System prompt', code: 'construct_system_prompt(repo_dir, ...)' }, + { num: '6', name: 'Middleware', code: 'middleware=[AuditLogger(), ...]' }, + ]; + + const cardY = layouts.CONTENT_TOP; + const cardW = 2.95; + const cardH = 1.65; + const gap = 0.13; + + items.forEach(function (it, idx) { + const col = idx % 3; + const row = Math.floor(idx / 3); + const cx = 0.5 + col * (cardW + gap); + const cy = cardY + row * (cardH + gap); + + slide.addShape(pres.ShapeType.roundRect, { + x: cx, y: cy, w: cardW, h: cardH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 0.75 }, + rectRadius: 0.08, + }); + + // Number badge + slide.addShape(pres.ShapeType.ellipse, { + x: cx + 0.15, y: cy + 0.15, w: 0.45, h: 0.45, + fill: { color: theme.palette.accent.primary }, + line: { color: theme.palette.accent.primary, width: 1 }, + }); + slide.addText(it.num, { + x: cx + 0.15, y: cy + 0.15, w: 0.45, h: 0.45, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: 18, + color: theme.palette.text.inverse, + bold: true, + align: 'center', + valign: 'middle', + }); + + slide.addText(it.name, { + x: cx + 0.7, y: cy + 0.15, w: cardW - 0.85, h: 0.45, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: theme.sizes.h3, + color: theme.palette.text.primary, + bold: true, + valign: 'middle', + }); + + // Code block + slide.addShape(pres.ShapeType.rect, { + x: cx + 0.15, y: cy + 0.7, w: cardW - 0.3, h: cardH - 0.85, + fill: { color: theme.palette.bg.code }, + line: { color: theme.palette.border.subtle, width: 0.5 }, + }); + slide.addText(it.code, { + x: cx + 0.25, y: cy + 0.75, w: cardW - 0.5, h: cardH - 0.95, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: theme.sizes.code, + color: theme.palette.text.primary, + valign: 'top', + }); + }); + + helpers.addPageNumber(slide, pres, theme, 18); + helpers.addSourceLine(slide, pres, theme, { + source: 'github.com/langchain-ai/open-swe/blob/main/CUSTOMIZATION.md', + }); + return slide; +} + +module.exports = { buildCustomization6 }; diff --git a/slides/section4-openswe/19-three-graphs.js b/slides/section4-openswe/19-three-graphs.js new file mode 100644 index 0000000..212f4dc --- /dev/null +++ b/slides/section4-openswe/19-three-graphs.js @@ -0,0 +1,96 @@ +/** + * slides/19-three-graphs.js + * ---------------------------------------------------------------------------- + * Slide 19 -- Three graphs: agent / reviewer / analyzer + * Multi-graph architecture. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildThreeGraphs(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Three graphs', + }); + + const graphs = [ + { + name: 'agent graph', + desc: 'Основной deep-agent: planner + coder + tools + subagents.', + nodes: 'START -> plan -> execute -> review -> open_pr -> END', + }, + { + name: 'reviewer graph', + desc: 'CI-стиль: lint, type-check, tests, security-scan. Без LLM-агента.', + nodes: 'START -> run_ci -> parse -> report -> END', + }, + { + name: 'analyzer graph', + desc: 'Анализ ретроспективы: что заняло больше шагов, где loop, где cost spikes.', + nodes: 'START -> load_trace -> aggregate -> summarize -> END', + }, + ]; + + const cardY = layouts.CONTENT_TOP; + const cardW = 9.0; + const cardH = 1.05; + const gap = 0.13; + + graphs.forEach(function (g, idx) { + const cy = cardY + idx * (cardH + gap); + + slide.addShape(pres.ShapeType.roundRect, { + x: 0.5, y: cy, w: cardW, h: cardH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 0.75 }, + rectRadius: 0.08, + }); + + // Name badge + slide.addShape(pres.ShapeType.rect, { + x: 0.6, y: cy + 0.12, w: 2.4, h: 0.4, + fill: { color: theme.palette.accent.primary }, + line: { color: theme.palette.accent.primary, width: 1 }, + rectRadius: 0.04, + }); + slide.addText(g.name, { + x: 0.6, y: cy + 0.12, w: 2.4, h: 0.4, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: theme.sizes.code, + color: theme.palette.text.inverse, + bold: true, + align: 'center', + valign: 'middle', + }); + + // Desc + slide.addText(g.desc, { + x: 3.1, y: cy + 0.1, w: 6.3, h: 0.4, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: theme.sizes.body, + color: theme.palette.text.secondary, + valign: 'middle', + }); + + // Node line + slide.addText(g.nodes, { + x: 0.6, y: cy + 0.6, w: cardW - 0.2, h: 0.4, + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: theme.sizes.code, + color: theme.palette.text.primary, + valign: 'middle', + }); + }); + + helpers.addPageNumber(slide, pres, theme, 19); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 165, 226-247', + }); + return slide; +} + +module.exports = { buildThreeGraphs }; diff --git a/slides/section4-openswe/20-observability.js b/slides/section4-openswe/20-observability.js new file mode 100644 index 0000000..d315295 --- /dev/null +++ b/slides/section4-openswe/20-observability.js @@ -0,0 +1,74 @@ +/** + * slides/20-observability.js + * ---------------------------------------------------------------------------- + * Slide 20 -- Observability: Datadog + LangSmith + * Tracing + metrics. + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildObservability(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Observability', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.6, h: 3.5, + language: 'python', + code: [ + '# observability.py', + 'from datadog import statsd', + 'from langchain_core.tracers import (', + ' LangChainTracer,', + ')', + '', + 'tracer = LangChainTracer(', + ' project_name="open-swe-prod",', + ')', + '', + 'def record_step(thread_id, duration_s):', + ' statsd.histogram(', + ' "open_swe.step.duration_s",', + ' duration_s, tags=[f"t:{thread_id}"],', + ' )', + ].join('\n'), + filePath: 'open_swe/observability.py', + startLine: 1, + highlightLines: [3, 4, 5, 8, 9, 10], + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: layouts.CONTENT_TOP, w: 3.2, h: 1.7, + kind: 'info', + title: 'LangSmith', + text: + 'Trace каждого run: model ' + + 'calls, tool calls, retries. ' + + 'Replay, debug, manual ' + + 'evaluation.', + }); + + helpers.addCallout(slide, pres, theme, { + x: 6.3, y: 3.05, w: 3.2, h: 1.8, + kind: 'success', + title: 'Datadog', + text: + 'P95 latency, tokens per ' + + 'run, error rate, sandbox ' + + 'spend. Alerts на cost ' + + 'spikes и длинные loops.', + }); + + helpers.addPageNumber(slide, pres, theme, 20); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md + Datadog / LangSmith docs', + }); + return slide; +} + +module.exports = { buildObservability }; diff --git a/slides/section4-openswe/21-production-ready.js b/slides/section4-openswe/21-production-ready.js new file mode 100644 index 0000000..5dad5f3 --- /dev/null +++ b/slides/section4-openswe/21-production-ready.js @@ -0,0 +1,71 @@ +/** + * slides/21-production-ready.js + * ---------------------------------------------------------------------------- + * Slide 21 -- Production: чеклист prod-ready + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildProductionReady(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Production: prod-ready', + }); + + // Two columns of checklist items + const leftItems = [ + 'docker-compose / k8s', + 'managed Postgres', + 'Vault / sealed-secrets', + 'TLS + reverse proxy', + 'rate limit на /webhooks/*', + 'idempotency keys', + ]; + + const rightItems = [ + 'LangSmith prod-project', + 'Datadog dashboards + SLO', + 'sandbox cost budget', + 'audit log всех PR', + 'docs: runbook, on-call', + 'load test 100 threads', + ]; + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 4.4, h: 1.6, + kind: 'success', + title: 'Infra', + text: leftItems.map(function (i) { return '+ ' + i; }).join('\n'), + }); + + helpers.addCallout(slide, pres, theme, { + x: 5.1, y: layouts.CONTENT_TOP, w: 4.4, h: 1.6, + kind: 'info', + title: 'Ops', + text: rightItems.map(function (i) { return '+ ' + i; }).join('\n'), + }); + + helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 3.15, w: 9.0, h: 1.7, + kind: 'warning', + title: 'Самый частый пропуск', + text: + '"Trust the LLM" внутри sandbox. ' + + 'Без надлежащей изоляции (seccomp, ' + + 'network policy, ephemeral FS) агент ' + + 'может сделать rm -rf или curl внутренних ' + + 'сервисов. Изоляция важнее prompts.', + }); + + helpers.addPageNumber(slide, pres, theme, 21); + helpers.addSourceLine(slide, pres, theme, { + source: 'production-readiness checklist + SRE playbook', + }); + return slide; +} + +module.exports = { buildProductionReady }; diff --git a/slides/section4-openswe/22-typescript.js b/slides/section4-openswe/22-typescript.js new file mode 100644 index 0000000..c6e21c9 --- /dev/null +++ b/slides/section4-openswe/22-typescript.js @@ -0,0 +1,64 @@ +/** + * slides/22-typescript.js + * ---------------------------------------------------------------------------- + * Slide 22 -- TypeScript analogues: где есть, где нет + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildTypescript(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'TypeScript: только UI', + }); + + helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 5.6, h: 3.5, + language: 'typescript', + code: [ + '// apps/open-swe-ui/src/lib/client.ts', + 'import { OpenSWEClient } from', + ' "@openswe/client";', + '', + 'const client = new OpenSWEClient({', + ' langsmithApiKey: process.env.', + ' LANGSMITH_API_KEY!,', + '});', + '', + 'await client.invoke({', + ' threadId: "issue-123",', + ' prompt: "Fix the bug",', + '});', + ].join('\n'), + filePath: 'apps/open-swe-ui/src/lib/client.ts', + startLine: 1, + }); + + helpers.addProsCons(slide, pres, theme, { + x: 6.3, y: layouts.CONTENT_TOP, w: 3.2, h: 3.5, + pros: [ + 'UI: полностью TS', + 'TanStack Start + Vite', + 'strict TS config', + 'streaming SDK', + ], + cons: [ + 'Нет TS для backend', + 'Агент -- Python only', + 'Webhook handlers только Py', + 'SDK -- thin wrapper', + ], + }); + + helpers.addPageNumber(slide, pres, theme, 22); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 270-287', + }); + return slide; +} + +module.exports = { buildTypescript }; diff --git a/slides/section4-openswe/23-pros-cons.js b/slides/section4-openswe/23-pros-cons.js new file mode 100644 index 0000000..03d1803 --- /dev/null +++ b/slides/section4-openswe/23-pros-cons.js @@ -0,0 +1,92 @@ +/** + * slides/23-pros-cons.js + * ---------------------------------------------------------------------------- + * Slide 23 -- Pros / cons vs Claude Code / Devin + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildProsConsComparison(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Open SWE vs Claude Code / Devin', + }); + + helpers.addProsCons(slide, pres, theme, { + x: 0.5, y: layouts.CONTENT_TOP, w: 4.4, h: 1.7, + pros: [ + 'MIT license, форкайте', + 'Pluggable sandbox', + 'Triggers: Slack/Linear/GH', + 'AGENTS.md convention', + 'Subagents + middleware', + 'Built on Deep Agents', + ], + cons: [ + 'Не finished product', + 'Sandbox = платные аккаунты', + 'OAuth setup нужен', + '"Trust the LLM" модель', + 'Prod deployment сложный', + 'Доки быстро устаревают', + ], + }); + + // Comparison card on the right + slide.addShape(pres.ShapeType.roundRect, { + x: 5.1, y: layouts.CONTENT_TOP, w: 4.4, h: 3.5, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.08, + }); + + slide.addText('VS Claude Code / Devin', { + x: 5.3, y: layouts.CONTENT_TOP + 0.1, w: 4.0, h: 0.3, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: theme.sizes.h3, + color: theme.palette.accent.secondary, + bold: true, + }); + + const compareRows = [ + ['Audience', 'IDE', 'SaaS', 'org'], + ['License', 'proprietary', 'proprietary','MIT'], + ['Trigger', 'manual', 'Web UI', 'Slack/Lin/GH'], + ['Sandbox', 'local+cloud', 'remote', 'pluggable'], + ['Customize', 'config', 'no', 'full src'], + ['Runs in', 'IDE/term', 'cloud', 'your infra'], + ]; + + const tY = layouts.CONTENT_TOP + 0.55; + compareRows.forEach(function (row, idx) { + const ry = tY + idx * 0.45; + row.forEach(function (cell, cidx) { + const widths = [1.1, 1.0, 1.0, 1.1]; + let cx = 5.3; + for (let i = 0; i < cidx; i++) cx += widths[i] + 0.05; + slide.addText(cell, { + x: cx, y: ry, w: widths[cidx], h: 0.4, + fontFace: helpers.withFallback( + cidx === 0 ? theme.fonts.ui : theme.fonts.code), + fontSize: cidx === 0 ? theme.sizes.caption : 8, + color: cidx === 3 + ? theme.palette.accent.tertiary + : theme.palette.text.secondary, + bold: cidx === 0, + valign: 'middle', + }); + }); + }); + + helpers.addPageNumber(slide, pres, theme, 23); + helpers.addSourceLine(slide, pres, theme, { + source: 'research/per-tech/openswe.md: 291-310', + }); + return slide; +} + +module.exports = { buildProsConsComparison }; diff --git a/slides/section4-openswe/24-roadmap.js b/slides/section4-openswe/24-roadmap.js new file mode 100644 index 0000000..210bf52 --- /dev/null +++ b/slides/section4-openswe/24-roadmap.js @@ -0,0 +1,130 @@ +/** + * slides/24-roadmap.js + * ---------------------------------------------------------------------------- + * Slide 24 -- Roadmap: что ожидать + */ +'use strict'; + +const { helpers, layouts } = require('../../design-system'); + +function buildRoadmap(pres, theme) { + const slide = pres.addSlide(); + helpers.slideBase(slide, pres, theme); + helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 4', + sectionNumber: 4, + title: 'Roadmap: 2026-2027', + }); + + // Timeline cards + const items = [ + { + period: 'Q2 2026', + title: 'Stable v1', + points: [ + 'deep-agent harness заморожен', + 'API стабилизирован', + 'semver commitment', + ], + }, + { + period: 'Q3 2026', + title: 'Multi-tenant', + points: [ + 'per-org quota + billing', + 'team-level audit log', + 'rbac на sandbox providers', + ], + }, + { + period: 'Q4 2026', + title: 'More triggers', + points: [ + 'Jira / Azure DevOps', + 'Sentry для авто-fix багов', + 'PagerDuty для incident triage', + ], + }, + { + period: '2027', + title: 'SDK + Marketplace', + points: [ + 'public SDK (Python + TS)', + 'plugin marketplace', + 'shared subagent library', + ], + }, + ]; + + const cardY = layouts.CONTENT_TOP; + const cardW = 2.2; + const cardH = 3.5; + const gap = 0.13; + + items.forEach(function (it, idx) { + const cx = 0.5 + idx * (cardW + gap); + + slide.addShape(pres.ShapeType.roundRect, { + x: cx, y: cardY, w: cardW, h: cardH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.accent.primary, width: 1 }, + rectRadius: 0.08, + }); + + // Period header + slide.addShape(pres.ShapeType.rect, { + x: cx, y: cardY, w: cardW, h: 0.5, + fill: { color: theme.palette.accent.primary }, + line: { color: theme.palette.accent.primary, width: 1 }, + }); + slide.addText(it.period, { + x: cx, y: cardY, w: cardW, h: 0.5, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: theme.sizes.h3, + color: theme.palette.text.inverse, + bold: true, + align: 'center', + valign: 'middle', + }); + + // Title + slide.addText(it.title, { + x: cx + 0.15, y: cardY + 0.65, w: cardW - 0.3, h: 0.4, + fontFace: helpers.withFallback(theme.fonts.ui), + fontSize: theme.sizes.h3, + color: theme.palette.text.primary, + bold: true, + }); + + // Divider + slide.addShape(pres.ShapeType.line, { + x: cx + 0.15, y: cardY + 1.1, w: cardW - 0.3, h: 0, + line: { color: theme.palette.border.subtle, width: 0.75 }, + }); + + // Bullets + const bulletItems = it.points.map(function (p) { + return { + text: '> ' + p + '\n', + options: { + fontFace: helpers.withFallback(theme.fonts.code), + fontSize: 10, + color: theme.palette.text.secondary, + }, + }; + }); + slide.addText(bulletItems, { + x: cx + 0.15, y: cardY + 1.2, w: cardW - 0.3, h: cardH - 1.3, + valign: 'top', + paraSpaceAfter: 4, + }); + }); + + helpers.addPageNumber(slide, pres, theme, 24); + helpers.addSourceLine(slide, pres, theme, { + source: 'github.com/langchain-ai/open-swe/issues + blog.langchain.com', + }); + return slide; +} + +module.exports = { buildRoadmap }; diff --git a/slides/section4-openswe/compile.js b/slides/section4-openswe/compile.js new file mode 100644 index 0000000..0ba34ee --- /dev/null +++ b/slides/section4-openswe/compile.js @@ -0,0 +1,111 @@ +/** + * slides/section4-openswe/compile.js + * ---------------------------------------------------------------------------- + * Compiles all Open SWE section slides into section4.pptx. + * + * Usage: + * node compile.js + * + * Output: + * ../section4.pptx (relative to this file) + * ../section4.pdf (via libreoffice) + * ../section4-page-N.png (preview images via pdftoppm) + */ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +const pptxgen = require('pptxgenjs'); +const ds = require('../../design-system'); + +const { theme, helpers } = ds; + +// Import all slide builders in order. +const slides = [ + require('./01-cover'), + require('./02-what-is-openswe'), + require('./03-architecture-overview'), + require('./04-create-deep-agent'), + require('./05-agents-md-context'), + require('./06-middleware'), + require('./07-sandbox-providers'), + require('./08-sandbox-imports'), + require('./09-installation-1'), + require('./10-installation-2'), + require('./11-github-app'), + require('./12-langsmith'), + require('./13-triggers-overview'), + require('./14-triggers-thread-id'), + require('./15-webhook-endpoints'), + require('./16-dashboard-ui'), + require('./17-per-user-settings'), + require('./18-customization-6-points'), + require('./19-three-graphs'), + require('./20-observability'), + require('./21-production-ready'), + require('./22-typescript'), + require('./23-pros-cons'), + require('./24-roadmap'), +]; + +async function main() { + console.log('[compile] building section4.pptx with ' + slides.length + ' slides'); + + const pres = new pptxgen(); + pres.layout = 'LAYOUT_16x9'; + pres.title = 'lc-evo-deck / Stage 4: Open SWE'; + pres.subject = 'LangChain Evolution Deck -- Open SWE section'; + pres.company = 'lc-evo-deck'; + + for (let i = 0; i < slides.length; i++) { + const mod = slides[i]; + const builder = pickBuilder(mod); + if (typeof builder !== 'function') { + throw new Error('slide ' + (i + 1) + ': no builder function'); + } + builder(pres, theme); + } + + // Output: one level up from this file, into sec4-openswe/ root. + const outDir = path.resolve(__dirname, '..', '..'); + const outPath = path.join(outDir, 'section4.pptx'); + await pres.writeFile({ fileName: outPath }); + console.log('[compile] wrote ' + outPath); + + return outPath; +} + +function pickBuilder(mod) { + // Each slide file exports either buildX or default. + if (typeof mod.buildCover === 'function') return mod.buildCover; + if (typeof mod.buildWhatIsOpenSWE === 'function') return mod.buildWhatIsOpenSWE; + if (typeof mod.buildArchitectureOverview === 'function') return mod.buildArchitectureOverview; + if (typeof mod.buildCreateDeepAgent === 'function') return mod.buildCreateDeepAgent; + if (typeof mod.buildAgentsMdConvention === 'function') return mod.buildAgentsMdConvention; + if (typeof mod.buildMiddleware === 'function') return mod.buildMiddleware; + if (typeof mod.buildSandboxProviders === 'function') return mod.buildSandboxProviders; + if (typeof mod.buildSandboxImports === 'function') return mod.buildSandboxImports; + if (typeof mod.buildInstallPart1 === 'function') return mod.buildInstallPart1; + if (typeof mod.buildInstallPart2 === 'function') return mod.buildInstallPart2; + if (typeof mod.buildGithubApp === 'function') return mod.buildGithubApp; + if (typeof mod.buildLangSmithSetup === 'function') return mod.buildLangSmithSetup; + if (typeof mod.buildTriggersOverview === 'function') return mod.buildTriggersOverview; + if (typeof mod.buildTriggerRouting === 'function') return mod.buildTriggerRouting; + if (typeof mod.buildWebhookEndpoints === 'function') return mod.buildWebhookEndpoints; + if (typeof mod.buildDashboardUI === 'function') return mod.buildDashboardUI; + if (typeof mod.buildPerUserSettings === 'function') return mod.buildPerUserSettings; + if (typeof mod.buildCustomization6 === 'function') return mod.buildCustomization6; + if (typeof mod.buildThreeGraphs === 'function') return mod.buildThreeGraphs; + if (typeof mod.buildObservability === 'function') return mod.buildObservability; + if (typeof mod.buildProductionReady === 'function') return mod.buildProductionReady; + if (typeof mod.buildTypescript === 'function') return mod.buildTypescript; + if (typeof mod.buildProsConsComparison === 'function') return mod.buildProsConsComparison; + if (typeof mod.buildRoadmap === 'function') return mod.buildRoadmap; + return null; +} + +main().catch(function (err) { + console.error('[compile] failed:', err); + process.exit(1); +}); diff --git a/slides/section4-openswe/previews/slide-01.png b/slides/section4-openswe/previews/slide-01.png new file mode 100644 index 0000000..61b11a2 Binary files /dev/null and b/slides/section4-openswe/previews/slide-01.png differ diff --git a/slides/section4-openswe/previews/slide-02.png b/slides/section4-openswe/previews/slide-02.png new file mode 100644 index 0000000..84db96a Binary files /dev/null and b/slides/section4-openswe/previews/slide-02.png differ diff --git a/slides/section4-openswe/previews/slide-03.png b/slides/section4-openswe/previews/slide-03.png new file mode 100644 index 0000000..5421b9d Binary files /dev/null and b/slides/section4-openswe/previews/slide-03.png differ diff --git a/slides/section4-openswe/previews/slide-04.png b/slides/section4-openswe/previews/slide-04.png new file mode 100644 index 0000000..52e7cf3 Binary files /dev/null and b/slides/section4-openswe/previews/slide-04.png differ diff --git a/slides/section4-openswe/previews/slide-05.png b/slides/section4-openswe/previews/slide-05.png new file mode 100644 index 0000000..6db8ca0 Binary files /dev/null and b/slides/section4-openswe/previews/slide-05.png differ diff --git a/slides/section4-openswe/previews/slide-06.png b/slides/section4-openswe/previews/slide-06.png new file mode 100644 index 0000000..335de5c Binary files /dev/null and b/slides/section4-openswe/previews/slide-06.png differ diff --git a/slides/section4-openswe/previews/slide-07.png b/slides/section4-openswe/previews/slide-07.png new file mode 100644 index 0000000..c570aca Binary files /dev/null and b/slides/section4-openswe/previews/slide-07.png differ diff --git a/slides/section4-openswe/previews/slide-08.png b/slides/section4-openswe/previews/slide-08.png new file mode 100644 index 0000000..7413687 Binary files /dev/null and b/slides/section4-openswe/previews/slide-08.png differ diff --git a/slides/section4-openswe/previews/slide-09.png b/slides/section4-openswe/previews/slide-09.png new file mode 100644 index 0000000..8bec062 Binary files /dev/null and b/slides/section4-openswe/previews/slide-09.png differ diff --git a/slides/section4-openswe/previews/slide-10.png b/slides/section4-openswe/previews/slide-10.png new file mode 100644 index 0000000..c97feb5 Binary files /dev/null and b/slides/section4-openswe/previews/slide-10.png differ diff --git a/slides/section4-openswe/previews/slide-11.png b/slides/section4-openswe/previews/slide-11.png new file mode 100644 index 0000000..2634413 Binary files /dev/null and b/slides/section4-openswe/previews/slide-11.png differ diff --git a/slides/section4-openswe/previews/slide-12.png b/slides/section4-openswe/previews/slide-12.png new file mode 100644 index 0000000..0b4e6a5 Binary files /dev/null and b/slides/section4-openswe/previews/slide-12.png differ diff --git a/slides/section4-openswe/previews/slide-13.png b/slides/section4-openswe/previews/slide-13.png new file mode 100644 index 0000000..3afbbbf Binary files /dev/null and b/slides/section4-openswe/previews/slide-13.png differ diff --git a/slides/section4-openswe/previews/slide-14.png b/slides/section4-openswe/previews/slide-14.png new file mode 100644 index 0000000..f12f83d Binary files /dev/null and b/slides/section4-openswe/previews/slide-14.png differ diff --git a/slides/section4-openswe/previews/slide-15.png b/slides/section4-openswe/previews/slide-15.png new file mode 100644 index 0000000..164de6b Binary files /dev/null and b/slides/section4-openswe/previews/slide-15.png differ diff --git a/slides/section4-openswe/previews/slide-16.png b/slides/section4-openswe/previews/slide-16.png new file mode 100644 index 0000000..2640207 Binary files /dev/null and b/slides/section4-openswe/previews/slide-16.png differ diff --git a/slides/section4-openswe/previews/slide-17.png b/slides/section4-openswe/previews/slide-17.png new file mode 100644 index 0000000..84fec28 Binary files /dev/null and b/slides/section4-openswe/previews/slide-17.png differ diff --git a/slides/section4-openswe/previews/slide-18.png b/slides/section4-openswe/previews/slide-18.png new file mode 100644 index 0000000..cc7bd06 Binary files /dev/null and b/slides/section4-openswe/previews/slide-18.png differ diff --git a/slides/section4-openswe/previews/slide-19.png b/slides/section4-openswe/previews/slide-19.png new file mode 100644 index 0000000..d3f0a45 Binary files /dev/null and b/slides/section4-openswe/previews/slide-19.png differ diff --git a/slides/section4-openswe/previews/slide-20.png b/slides/section4-openswe/previews/slide-20.png new file mode 100644 index 0000000..c0595bf Binary files /dev/null and b/slides/section4-openswe/previews/slide-20.png differ diff --git a/slides/section4-openswe/previews/slide-21.png b/slides/section4-openswe/previews/slide-21.png new file mode 100644 index 0000000..f247750 Binary files /dev/null and b/slides/section4-openswe/previews/slide-21.png differ diff --git a/slides/section4-openswe/previews/slide-22.png b/slides/section4-openswe/previews/slide-22.png new file mode 100644 index 0000000..6b5f026 Binary files /dev/null and b/slides/section4-openswe/previews/slide-22.png differ diff --git a/slides/section4-openswe/previews/slide-23.png b/slides/section4-openswe/previews/slide-23.png new file mode 100644 index 0000000..1eefaef Binary files /dev/null and b/slides/section4-openswe/previews/slide-23.png differ diff --git a/slides/section4-openswe/previews/slide-24.png b/slides/section4-openswe/previews/slide-24.png new file mode 100644 index 0000000..8c1991d Binary files /dev/null and b/slides/section4-openswe/previews/slide-24.png differ diff --git a/slides/section4-openswe/section4.pdf b/slides/section4-openswe/section4.pdf new file mode 100644 index 0000000..c1e42f5 Binary files /dev/null and b/slides/section4-openswe/section4.pdf differ diff --git a/slides/section4-openswe/section4.pptx b/slides/section4-openswe/section4.pptx new file mode 100644 index 0000000..fd9ea3c Binary files /dev/null and b/slides/section4-openswe/section4.pptx differ diff --git a/slides/section5-ecosystem/_render_previews.py b/slides/section5-ecosystem/_render_previews.py new file mode 100644 index 0000000..6f81e75 --- /dev/null +++ b/slides/section5-ecosystem/_render_previews.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Re-render section5 PDF -> PNG previews. Wipes stale PNGs via mavis-trash.""" +from pdf2image import convert_from_path +from pathlib import Path +import subprocess + +src = Path('/Users/alexandr/.mavis/plans/plan_85053139/workspace/lc-evo-deck/slides/section5-ecosystem/section5.pdf') +out = Path('/Users/alexandr/.mavis/plans/plan_85053139/workspace/lc-evo-deck/slides/section5-ecosystem/previews') +out.mkdir(parents=True, exist_ok=True) + +# Move stale PNGs to trash via mavis-trash +stale = sorted(out.glob('slide-*.png')) +if stale: + subprocess.run(['mavis-trash', *[str(p) for p in stale]], check=False) + +imgs = convert_from_path(str(src), dpi=110) +for i, im in enumerate(imgs, 1): + p = out / f'slide-{i:02d}.png' + im.save(str(p), 'PNG') +print(f'wrote {len(imgs)} previews to {out}') diff --git a/slides/section5-ecosystem/compile.js b/slides/section5-ecosystem/compile.js new file mode 100644 index 0000000..3e4c0bb --- /dev/null +++ b/slides/section5-ecosystem/compile.js @@ -0,0 +1,41 @@ +// Compile all section-5 slides into a single PPTX +// Output: section5.pptx (12 slides, 16:9, dark theme, code-heavy) + +const path = require('path'); +const fs = require('fs'); +const pptxgen = require('pptxgenjs'); + +const ds = require('./design-system'); +const { theme } = ds; + +const pres = new pptxgen(); +pres.layout = 'LAYOUT_16x9'; +pres.title = 'Ecosystem: LangSmith, Studio, deployment'; +pres.author = 'lc-evo-deck'; +pres.subject = 'Section 5 of the LangChain Evolution deck'; + +const SLIDE_COUNT = 12; + +for (let i = 1; i <= SLIDE_COUNT; i++) { + const num = String(i).padStart(2, '0'); + const file = path.join(__dirname, `slide-${num}.js`); + if (!fs.existsSync(file)) { + throw new Error('Missing slide module: ' + file); + } + const mod = require(file); + if (typeof mod.createSlide !== 'function') { + throw new Error('Module does not export createSlide: ' + file); + } + mod.createSlide(pres, theme); +} + +const outFile = path.join(__dirname, 'section5.pptx'); +pres.writeFile({ fileName: outFile }) + .then((fileName) => { + console.log('OK ->', fileName); + console.log('Slides:', SLIDE_COUNT); + }) + .catch((err) => { + console.error('ERR:', err); + process.exit(1); + }); diff --git a/slides/section5-ecosystem/design-system.js b/slides/section5-ecosystem/design-system.js new file mode 100644 index 0000000..2cc135a --- /dev/null +++ b/slides/section5-ecosystem/design-system.js @@ -0,0 +1,862 @@ +/** + * design-system.js + * ---------------------------------------------------------------------------- + * Design tokens + slide helpers for the LangChain Evolution deck + * (lc-evo-deck, 100+ slides, code-heavy, dark theme, 16:9). + * + * Audience: engineers familiar with LLMs. No introductory filler. + * Visual register: developer-tool aesthetic, deep navy, JetBrains Mono for + * code, Inter for UI, Arial as the universal fallback (covers Cyrillic). + * + * USAGE + * ----- + * // 1. Import the module + * const ds = require('./design-system'); + * const { theme, helpers, layouts, fonts, sizes, spacing } = ds; + * + * // 2. Create a new presentation with the built-in 16:9 layout + * const pptxgen = require('pptxgenjs'); + * const pres = new pptxgen(); + * pres.layout = 'LAYOUT_16x9'; // 10 x 5.625 inches + * + * // 3. Add a slide + * const slide = pres.addSlide(); + * helpers.slideBase(slide, pres, theme); + * helpers.addHeader(slide, pres, theme, { + * section: 'Stage 2: Chains', + * sectionNumber: 2, + * title: 'LCEL: a composable expression language', + * eyebrow: 'STAGE 2', + * }); + * + * // 4. Add a code block + * helpers.addCodeBlock(slide, pres, theme, { + * x: 0.5, y: layouts.CONTENT_TOP, + * w: 5.0, h: 3.2, + * language: 'python', + * code: [ + * 'from langchain_core.prompts import ChatPromptTemplate', + * 'from langchain_openai import ChatOpenAI', + * '', + * 'prompt = ChatPromptTemplate.from_messages([', + * ' ("system", "You are a helpful assistant."),', + * ' ("human", "{question}"),', + * '])', + * 'model = ChatOpenAI(model="gpt-4o-mini")', + * 'chain = prompt | model', + * 'print(chain.invoke({"question": "What is LCEL?"}))', + * ].join('\n'), + * filePath: 'examples/lcel_basic.py', + * startLine: 1, + * }); + * + * // 5. Add a callout and a pros/cons panel + * helpers.addCallout(slide, pres, theme, { + * x: 5.8, y: layouts.CONTENT_TOP, w: 3.7, h: 1.2, + * kind: 'info', + * text: 'LCEL is the default composition language from v0.1 onward.', + * }); + * + * helpers.addProsCons(slide, pres, theme, { + * x: 5.8, y: 2.8, w: 3.7, h: 2.0, + * pros: ['Composable via | operator', 'Streaming, async, batched for free'], + * cons: ['Verbose for simple chains', 'Mental model differs from LangChain v0'], + * }); + * + * // 6. Number the slide and add a source line + * helpers.addPageNumber(slide, pres, theme, 7); + * helpers.addSourceLine(slide, pres, theme, { + * x: 0.5, y: layouts.FOOTER_Y + 0.1, w: 6.0, + * source: 'python.langchain.com/docs/concepts/lcel', + * }); + * + * // 7. Save + * await pres.writeFile({ fileName: 'lc-evolution.pptx' }); + * + * LAYOUTS (PPTX 16:9, units = inches) + * ----------------------------------- + * HEADER_Y = 0.4 + * CONTENT_TOP = 1.4 + * CONTENT_BOTTOM = 5.05 + * FOOTER_Y = 5.25 + * + * Vertical regions: + * - Header band : [0.0 .. 1.4] eyebrow + h1 title + * - Content body : [1.4 .. 5.05] main slide content + * - Footer band : [5.05 .. 5.625] page number + source line + * + * Horizontal margins: 0.5 inches left/right by default. + * + * RULES + * ----- + * - No em-dash (--), en-dash (-), no smart quotes, no ellipsis (...) + * - All source files are pure ASCII except inside string literals where + * Cyrillic is allowed (Arial fallback guarantees rendering). + * - Code blocks use JetBrains Mono; body uses Inter; fallback Arial. + * + * @module design-system + */ + +'use strict'; + +// --------------------------------------------------------------------------- +// 1. PALETTE +// --------------------------------------------------------------------------- + +const palette = { + bg: { + primary: '#0A1A2A', // main slide background + elevated: '#142B3F', // cards, callouts, panels + code: '#0F1E2E', // code block background (slightly darker) + overlay: '#1B2F44', // hover / focus surfaces + }, + text: { + primary: '#E6F0F7', + secondary: '#B5C4D1', + muted: '#8A9AAB', + inverse: '#0A1A2A', // for text on bright accents + }, + accent: { + primary: '#219EBC', // teal, default accent + secondary: '#FFB703', // gold, important emphasis + tertiary: '#8ECAE6', // light blue, soft accent + }, + border: { + subtle: '#233A4F', + strong: '#3A5670', + accent: '#219EBC', + }, + code: { + keyword: '#C586C0', // def, class, import, return + string: '#CE9178', + number: '#B5CEA8', + comment: '#6A9955', + function: '#DCDCAA', + builtin: '#4EC9B0', + text: '#D4D4D4', // default code body + bg: '#0F1E2E', + lineHighlight: '#1F2F44', + }, + state: { + info: '#219EBC', + success: '#4EC9B0', + warning: '#FFB703', + danger: '#F48771', + infoBg: '#102A38', + successBg: '#0F2A28', + warningBg: '#3A2A0F', + dangerBg: '#3A1A14', + }, +}; + +// --------------------------------------------------------------------------- +// 2. TYPOGRAPHY +// --------------------------------------------------------------------------- + +const fonts = { + code: 'JetBrains Mono', + ui: 'Inter', + fallback: 'Arial', // universal fallback, supports Cyrillic +}; + +const sizes = { + h1: 36, + h2: 28, + h3: 20, + body: 14, + code: 12, + caption: 10, + eyebrow: 10, +}; + +const spacing = { + page: 0.5, + card_pad: 0.25, + gap: 0.15, +}; + +// --------------------------------------------------------------------------- +// 3. LAYOUTS +// --------------------------------------------------------------------------- + +const layouts = { + LAYOUT_16x9: 'LAYOUT_16x9', + HEADER_Y: 0.4, + CONTENT_TOP: 1.45, + CONTENT_BOTTOM: 5.05, + FOOTER_Y: 5.25, +}; + +// --------------------------------------------------------------------------- +// 4. THEME (bundled export of the above) +// --------------------------------------------------------------------------- + +const theme = { + palette, + fonts, + sizes, + spacing, + layouts, +}; + +// --------------------------------------------------------------------------- +// 5. HELPERS +// --------------------------------------------------------------------------- + +/** + * Resolve a font that always carries the Arial fallback. + * pptxgenjs lets us pass an array; PowerPoint will pick the first available. + */ +function withFallback(family) { + return [family, fonts.fallback]; +} + +/** + * Paint the slide background. + * @param {object} slide - pptxgenjs slide instance + * @param {object} _pres - pptxgenjs pres (kept for signature symmetry) + * @param {object} t - theme bundle + */ +function slideBase(slide, _pres, t) { + slide.background = { color: t.palette.bg.primary }; +} + +/** + * Add a header band: eyebrow (small caps, accent) + section number + title. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {string} [opts.section] - small caps section label (e.g. "Stage 2") + * @param {number} [opts.sectionNumber] - large section number shown on the right + * @param {string} opts.title - main title (h1) + * @param {string} [opts.eyebrow] - eyebrow text (e.g. "STAGE 2: CHAINS") + */ +function addHeader(slide, pres, t, opts) { + const o = opts || {}; + const margin = t.spacing.page; + const titleY = t.layouts.HEADER_Y + 0.4; + const titleSize = o.titleSize || t.sizes.h2; // default to 28pt -- fits two-line titles without overflow + const titleW = o.sectionNumber != null ? 8.4 : 9.0; + + if (o.eyebrow) { + slide.addText(o.eyebrow, { + x: margin, + y: t.layouts.HEADER_Y, + w: 6.0, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.accent.primary, + bold: true, + charSpacing: 4, + }); + } else if (o.section) { + slide.addText(o.section, { + x: margin, + y: t.layouts.HEADER_Y, + w: 6.0, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.accent.primary, + bold: true, + charSpacing: 4, + }); + } + + slide.addText(o.title || '', { + x: margin, + y: titleY, + w: titleW, + h: 0.75, + fontFace: withFallback(t.fonts.ui), + fontSize: titleSize, + color: t.palette.text.primary, + bold: true, + valign: 'middle', + fit: 'shrink', + }); + + if (o.sectionNumber != null) { + slide.addText(String(o.sectionNumber), { + x: 9.0, + y: t.layouts.HEADER_Y - 0.05, + w: 0.7, + h: 1.0, + fontFace: withFallback(t.fonts.ui), + fontSize: 56, + color: t.palette.accent.secondary, + bold: true, + align: 'right', + valign: 'top', + }); + } + + // Hairline separator below the header band + slide.addShape(pres.ShapeType.line, { + x: margin, + y: 1.3, + w: 10.0 - margin * 2, + h: 0, + line: { color: t.palette.border.subtle, width: 0.75 }, + }); +} + +/** + * Render a code block as a card with monospaced text. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {string} opts.code + * @param {number} opts.x + * @param {number} opts.y + * @param {number} opts.w + * @param {number} opts.h + * @param {string} [opts.language='python'] + * @param {string} [opts.filePath] - optional, renders as "// path:line" header + * @param {number} [opts.startLine=1] + * @param {Array} [opts.highlightLines] - 1-based line numbers + */ +function addCodeBlock(slide, pres, t, opts) { + const o = opts || {}; + const code = String(o.code || ''); + const x = o.x; + const y = o.y; + const w = o.w; + const h = o.h; + const headerH = o.filePath ? 0.3 : 0; + const radius = 0.08; + + // Card background + slide.addShape(pres.ShapeType.roundRect, { + x: x, + y: y, + w: w, + h: h, + fill: { color: t.palette.code.bg }, + line: { color: t.palette.border.subtle, width: 0.75 }, + rectRadius: radius, + }); + + // Optional header strip with file path + if (o.filePath) { + const startLine = o.startLine || 1; + const lineCount = code.split('\n').length; + const endLine = startLine + lineCount - 1; + slide.addText( + '// ' + o.filePath + ':' + startLine + '-' + endLine, + { + x: x + t.spacing.card_pad, + y: y + 0.05, + w: w - t.spacing.card_pad * 2, + h: 0.22, + fontFace: withFallback(t.fonts.code), + fontSize: t.sizes.caption, + color: t.palette.text.muted, + italic: true, + } + ); + } + + // Body code + const lines = code.split('\n'); + const hasHighlight = + Array.isArray(o.highlightLines) && o.highlightLines.length > 0; + const highlightedSet = hasHighlight + ? new Set(o.highlightLines.map(function (n) { return n; })) + : null; + + // Pre-compute line height to fit within available area. + const codeAreaY = y + headerH + 0.05; + const codeAreaH = h - headerH - 0.1; + // Use a tighter font + leading so 20-line snippets fit in 3-inch cards. + const fontSize = o.fontSize || 11; + const lineH = (fontSize / 72) * 1.25; // inches per line at 1.25 leading + + const text = lines.map(function (line, idx) { + const lineNum = idx + 1; + const isHi = highlightedSet && highlightedSet.has(lineNum); + const prefix = isHi ? '> ' : ' '; + return { + text: prefix + (line.length === 0 ? ' ' : line) + '\n', + options: { + fontFace: withFallback(t.fonts.code), + fontSize: fontSize, + color: isHi ? t.palette.text.primary : t.palette.code.text, + bold: false, + highlight: isHi ? t.palette.code.lineHighlight : undefined, + }, + }; + }); + + slide.addText(text, { + x: x + t.spacing.card_pad, + y: codeAreaY, + w: w - t.spacing.card_pad * 2, + h: codeAreaH, + valign: 'top', + paraSpaceBefore: 0, + paraSpaceAfter: 0, + lineSpacingMultiple: 1.0, + isTextBox: true, + }); + + // Caption warning if content may overflow. + const maxLines = Math.floor(codeAreaH / lineH); + if (lines.length > maxLines) { + // Best-effort: append a small note. PowerPoint won't clip the text, but + // it will overflow the card visually. Caller should reduce fontSize or + // split the snippet. + slide.addText( + '// note: snippet has ' + lines.length + ' lines, card fits ~' + maxLines, + { + x: x + t.spacing.card_pad, + y: y + h - 0.22, + w: w - t.spacing.card_pad * 2, + h: 0.18, + fontFace: withFallback(t.fonts.code), + fontSize: t.sizes.caption, + color: t.palette.state.warning, + italic: true, + } + ); + } +} + +/** + * Render a code block with line highlighting. + * Convenience wrapper over addCodeBlock. + */ +function addCodeBlockWithHighlight(slide, pres, t, opts) { + return addCodeBlock(slide, pres, t, Object.assign({}, opts, { + highlightLines: opts.lines || opts.highlightLines, + })); +} + +/** + * Add a callout panel (info / warning / success / danger). + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {number} opts.x + * @param {number} opts.y + * @param {number} opts.w + * @param {number} opts.h + * @param {('info'|'warning'|'success'|'danger')} [opts.kind='info'] + * @param {string} opts.text + * @param {string} [opts.title] + */ +function addCallout(slide, pres, t, opts) { + const o = opts || {}; + const kind = o.kind || 'info'; + const accent = t.palette.state[kind] || t.palette.state.info; + const bgKey = kind + 'Bg'; + const bg = t.palette.state[bgKey] || t.palette.state.infoBg; + + // Background card + slide.addShape(pres.ShapeType.roundRect, { + x: o.x, + y: o.y, + w: o.w, + h: o.h, + fill: { color: bg }, + line: { color: accent, width: 1 }, + rectRadius: 0.08, + }); + + // Left accent bar + slide.addShape(pres.ShapeType.rect, { + x: o.x, + y: o.y, + w: 0.08, + h: o.h, + fill: { color: accent }, + line: { type: 'none' }, + }); + + // Label + slide.addText((kind || 'info').toUpperCase(), { + x: o.x + 0.25, + y: o.y + 0.1, + w: o.w - 0.35, + h: 0.25, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: accent, + bold: true, + charSpacing: 4, + }); + + // Optional title + let bodyY = o.y + 0.4; + let bodyH = o.h - 0.5; + if (o.title) { + slide.addText(o.title, { + x: o.x + 0.25, + y: o.y + 0.35, + w: o.w - 0.35, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.h3, + color: t.palette.text.primary, + bold: true, + }); + bodyY = o.y + 0.7; + bodyH = o.h - 0.8; + } + + slide.addText(o.text || '', { + x: o.x + 0.25, + y: bodyY, + w: o.w - 0.35, + h: bodyH, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.secondary, + valign: 'top', + }); +} + +/** + * Two-column pros / cons panel. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {number} opts.x + * @param {number} opts.y + * @param {number} opts.w + * @param {number} opts.h + * @param {string[]} opts.pros + * @param {string[]} opts.cons + */ +function addProsCons(slide, pres, t, opts) { + const o = opts || {}; + const gap = 0.2; + const colW = (o.w - gap) / 2; + + // Pros card + slide.addShape(pres.ShapeType.roundRect, { + x: o.x, + y: o.y, + w: colW, + h: o.h, + fill: { color: t.palette.state.successBg }, + line: { color: t.palette.state.success, width: 1 }, + rectRadius: 0.08, + }); + slide.addText('PROS', { + x: o.x + 0.2, + y: o.y + 0.1, + w: colW - 0.3, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.state.success, + bold: true, + charSpacing: 4, + }); + + const prosBody = (o.pros || []).map(function (item) { + return { + text: '+ ' + item + '\n', + options: { + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.primary, + }, + }; + }); + slide.addText(prosBody, { + x: o.x + 0.2, + y: o.y + 0.45, + w: colW - 0.3, + h: o.h - 0.55, + valign: 'top', + paraSpaceAfter: 4, + }); + + // Cons card + const cx = o.x + colW + gap; + slide.addShape(pres.ShapeType.roundRect, { + x: cx, + y: o.y, + w: colW, + h: o.h, + fill: { color: t.palette.state.dangerBg }, + line: { color: t.palette.state.danger, width: 1 }, + rectRadius: 0.08, + }); + slide.addText('CONS', { + x: cx + 0.2, + y: o.y + 0.1, + w: colW - 0.3, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.state.danger, + bold: true, + charSpacing: 4, + }); + + const consBody = (o.cons || []).map(function (item) { + return { + text: '- ' + item + '\n', + options: { + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.primary, + }, + }; + }); + slide.addText(consBody, { + x: cx + 0.2, + y: o.y + 0.45, + w: colW - 0.3, + h: o.h - 0.55, + valign: 'top', + paraSpaceAfter: 4, + }); +} + +/** + * Add a page number in the bottom-right corner. + */ +function addPageNumber(slide, _pres, t, num) { + slide.addText(String(num), { + x: 9.2, + y: t.layouts.FOOTER_Y, + w: 0.6, + h: 0.25, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.caption, + color: t.palette.text.muted, + align: 'right', + valign: 'middle', + }); +} + +/** + * Add a section divider slide: a large numeric label + title + intro paragraph. + * @param {object} slide + * @param {object} pres + * @param {object} t - theme + * @param {object} opts + * @param {number} opts.number + * @param {string} opts.title + * @param {string} [opts.intro] + * @param {string} [opts.eyebrow] + */ +function addSectionDivider(slide, pres, t, opts) { + const o = opts || {}; + + slideBase(slide, pres, t); + + // Giant number on the left + slide.addText(String(o.number), { + x: 0.5, + y: 1.0, + w: 3.5, + h: 3.5, + fontFace: withFallback(t.fonts.ui), + fontSize: 220, + color: t.palette.accent.primary, + bold: true, + valign: 'middle', + }); + + // Vertical divider + slide.addShape(pres.ShapeType.line, { + x: 4.2, + y: 1.4, + w: 0, + h: 2.8, + line: { color: t.palette.border.subtle, width: 1 }, + }); + + // Title + if (o.eyebrow) { + slide.addText(o.eyebrow, { + x: 4.5, + y: 1.4, + w: 5.0, + h: 0.3, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.eyebrow, + color: t.palette.accent.secondary, + bold: true, + charSpacing: 4, + }); + } + + slide.addText(o.title || '', { + x: 4.5, + y: o.eyebrow ? 1.7 : 1.4, + w: 5.0, + h: 1.0, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.h1, + color: t.palette.text.primary, + bold: true, + valign: 'top', + }); + + if (o.intro) { + slide.addText(o.intro, { + x: 4.5, + y: 2.8, + w: 5.0, + h: 1.6, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.body, + color: t.palette.text.secondary, + valign: 'top', + }); + } +} + +/** + * Add an italic source line, e.g. "source: python.langchain.com/..." + * @param {object} slide + * @param {object} _pres + * @param {object} t + * @param {object} opts + * @param {string} opts.source + * @param {number} [opts.x] + * @param {number} [opts.y] + * @param {number} [opts.w] + */ +function addSourceLine(slide, _pres, t, opts) { + const o = opts || {}; + slide.addText('source: ' + (o.source || ''), { + x: o.x != null ? o.x : t.spacing.page, + y: o.y != null ? o.y : t.layouts.FOOTER_Y + 0.05, + w: o.w != null ? o.w : 7.0, + h: 0.25, + fontFace: withFallback(t.fonts.ui), + fontSize: t.sizes.caption, + color: t.palette.text.muted, + italic: true, + valign: 'middle', + }); +} + +// --------------------------------------------------------------------------- +// 6. OPTIONAL: PYTHON SYNTAX HIGHLIGHTING (pygments via subprocess) +// --------------------------------------------------------------------------- + +/** + * Best-effort Python syntax highlighter. + * Calls the `pygmentize` CLI from pygments if available. + * Returns an array of { text, color } tokens. + * On any failure (missing binary, non-zero exit, parse error) returns a + * single-token array with the whole snippet in the default code text color + * so that the slide still renders something. + * + * @param {string} code - Python source code + * @returns {Array<{text: string, color: string}>} + */ +function highlightPython(code) { + const defaultColor = palette.code.text; + const text = String(code || ''); + if (!text) return [{ text: '', color: defaultColor }]; + + const { spawnSync } = require('child_process'); + + let result; + try { + result = spawnSync( + 'pygmentize', + ['-f', 'json', '-l', 'python'], + { input: text, encoding: 'utf8', timeout: 4000 } + ); + } catch (e) { + return [{ text: text, color: defaultColor }]; + } + + if (result.error || result.status !== 0) { + return [{ text: text, color: defaultColor }]; + } + + let parsed; + try { + parsed = JSON.parse(result.stdout); + } catch (e) { + return [{ text: text, color: defaultColor }]; + } + + // Pygments JSON output: { tokens: [ [type, value], ... ] } + const tokens = Array.isArray(parsed.tokens) ? parsed.tokens : []; + const out = []; + for (const tok of tokens) { + if (!Array.isArray(tok) || tok.length < 2) continue; + const [type, value] = tok; + out.push({ + text: String(value), + color: mapPygmentsTokenToColor(type) || defaultColor, + }); + } + return out.length > 0 ? out : [{ text: text, color: defaultColor }]; +} + +/** + * Map a pygments token type to a palette color. + * Falls back to the default code text color. + */ +function mapPygmentsTokenToColor(type) { + if (!type) return null; + // Pygments short token types we care about for Python: + // Keyword, Keyword.Namespace, Keyword.Constant, String, Number, + // Comment, Comment.Single, Name.Function, Name.Builtin, + // Operator, Punctuation, Text, Error + if (type === 'Keyword' || type.startsWith('Keyword.')) { + return palette.code.keyword; + } + if (type.startsWith('String')) { + return palette.code.string; + } + if (type === 'Number') { + return palette.code.number; + } + if (type.startsWith('Comment')) { + return palette.code.comment; + } + if (type === 'Name.Function' || type === 'Name.Function.Magic') { + return palette.code.function; + } + if (type === 'Name.Builtin' || type === 'Name.Builtin.Pseudo') { + return palette.code.builtin; + } + return null; +} + +// --------------------------------------------------------------------------- +// 7. EXPORTS +// --------------------------------------------------------------------------- + +const helpers = { + slideBase, + addHeader, + addCodeBlock, + addCodeBlockWithHighlight, + addCallout, + addProsCons, + addPageNumber, + addSectionDivider, + addSourceLine, + withFallback, + highlightPython, +}; + +module.exports = { + theme: theme, + palette: palette, + fonts: fonts, + sizes: sizes, + spacing: spacing, + layouts: layouts, + helpers: helpers, +}; \ No newline at end of file diff --git a/slides/section5-ecosystem/previews/slide-01.png b/slides/section5-ecosystem/previews/slide-01.png new file mode 100644 index 0000000..c10829e Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-01.png differ diff --git a/slides/section5-ecosystem/previews/slide-02.png b/slides/section5-ecosystem/previews/slide-02.png new file mode 100644 index 0000000..b6a2d2e Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-02.png differ diff --git a/slides/section5-ecosystem/previews/slide-03.png b/slides/section5-ecosystem/previews/slide-03.png new file mode 100644 index 0000000..5f02735 Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-03.png differ diff --git a/slides/section5-ecosystem/previews/slide-04.png b/slides/section5-ecosystem/previews/slide-04.png new file mode 100644 index 0000000..04d0b4d Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-04.png differ diff --git a/slides/section5-ecosystem/previews/slide-05.png b/slides/section5-ecosystem/previews/slide-05.png new file mode 100644 index 0000000..a7b297c Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-05.png differ diff --git a/slides/section5-ecosystem/previews/slide-06.png b/slides/section5-ecosystem/previews/slide-06.png new file mode 100644 index 0000000..d1d79ed Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-06.png differ diff --git a/slides/section5-ecosystem/previews/slide-07.png b/slides/section5-ecosystem/previews/slide-07.png new file mode 100644 index 0000000..9490757 Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-07.png differ diff --git a/slides/section5-ecosystem/previews/slide-08.png b/slides/section5-ecosystem/previews/slide-08.png new file mode 100644 index 0000000..f266eb3 Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-08.png differ diff --git a/slides/section5-ecosystem/previews/slide-09.png b/slides/section5-ecosystem/previews/slide-09.png new file mode 100644 index 0000000..9a070ff Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-09.png differ diff --git a/slides/section5-ecosystem/previews/slide-10.png b/slides/section5-ecosystem/previews/slide-10.png new file mode 100644 index 0000000..c20a538 Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-10.png differ diff --git a/slides/section5-ecosystem/previews/slide-11.png b/slides/section5-ecosystem/previews/slide-11.png new file mode 100644 index 0000000..4c97761 Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-11.png differ diff --git a/slides/section5-ecosystem/previews/slide-12.png b/slides/section5-ecosystem/previews/slide-12.png new file mode 100644 index 0000000..44c13c9 Binary files /dev/null and b/slides/section5-ecosystem/previews/slide-12.png differ diff --git a/slides/section5-ecosystem/section5.pdf b/slides/section5-ecosystem/section5.pdf new file mode 100644 index 0000000..53ef2fc Binary files /dev/null and b/slides/section5-ecosystem/section5.pdf differ diff --git a/slides/section5-ecosystem/section5.pptx b/slides/section5-ecosystem/section5.pptx new file mode 100644 index 0000000..8d85b9d Binary files /dev/null and b/slides/section5-ecosystem/section5.pptx differ diff --git a/slides/section5-ecosystem/slide-01.js b/slides/section5-ecosystem/slide-01.js new file mode 100644 index 0000000..46e3992 --- /dev/null +++ b/slides/section5-ecosystem/slide-01.js @@ -0,0 +1,152 @@ +// Slide 01: Section cover -- Stage 5 / Ecosystem (LangSmith + Studio + Platform) +// Asymmetric layout: big section number + title block on left, +// "ecosystem wheel" mock on right with the 3 satellite products. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + // Left vertical accent stripe -- gold for the ecosystem meta-section + slide.addShape(pres.ShapeType.rect, { + x: 0, y: 0, w: 0.25, h: 5.625, + fill: { color: theme.palette.accent.secondary }, + line: { type: 'none' }, + }); + + // Top tag pill: section meta + slide.addShape(pres.ShapeType.roundRect, { + x: 0.7, y: 0.55, w: 2.8, h: 0.36, + fill: { color: theme.palette.accent.secondary }, + line: { type: 'none' }, + rectRadius: 0.18, + }); + slide.addText('STAGE 5 | ECOSYSTEM', { + x: 0.7, y: 0.55, w: 2.8, h: 0.36, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, bold: true, + color: theme.palette.bg.primary, + align: 'center', valign: 'middle', + charSpacing: 4, margin: 0, + }); + + // Main title + slide.addText('Экосистема', { + x: 0.7, y: 1.15, w: 5.9, h: 1.0, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 56, bold: true, + color: theme.palette.text.primary, + align: 'left', valign: 'middle', + margin: 0, + }); + + // Subtitle + slide.addText('LangSmith, Studio, deployment', { + x: 0.7, y: 2.15, w: 9, h: 0.55, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 26, + color: theme.palette.accent.tertiary, + align: 'left', valign: 'middle', + margin: 0, + }); + + // Description block + slide.addText( + 'Сервисы вокруг 4 основных ступеней: observability и eval ' + + '(LangSmith SDK), визуальный дебаг графов (LangGraph Studio), ' + + 'production deployment (LangGraph Platform), self-hosted и TypeScript-клиенты. ' + + 'Все, что превращает прототип в production-grade систему.', + { + x: 0.7, y: 2.85, w: 5.7, h: 1.6, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 13, + color: theme.palette.text.secondary, + align: 'left', valign: 'top', + margin: 0, + } + ); + + // Right-side ecosystem mock: LangSmith SDK in center, 3 satellite products + const codeX = 6.7; + const codeY = 1.55; + const codeW = 2.85; + const codeH = 3.05; + + slide.addShape(pres.ShapeType.roundRect, { + x: codeX, y: codeY, w: codeW, h: codeH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.1, + }); + + // Center node: LangSmith SDK + slide.addShape(pres.ShapeType.roundRect, { + x: codeX + 0.7, y: codeY + 1.15, w: codeW - 1.4, h: 0.75, + fill: { color: theme.palette.accent.primary }, + line: { type: 'none' }, + rectRadius: 0.08, + }); + slide.addText('langsmith', { + x: codeX + 0.7, y: codeY + 1.15, w: codeW - 1.4, h: 0.45, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 16, bold: true, + color: theme.palette.bg.primary, + align: 'center', valign: 'middle', + margin: 0, + }); + slide.addText('Python SDK 0.8.9', { + x: codeX + 0.7, y: codeY + 1.5, w: codeW - 1.4, h: 0.35, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, italic: true, + color: theme.palette.bg.primary, + align: 'center', valign: 'middle', + margin: 0, + }); + + // Three satellite nodes + const satellites = [ + { label: '@traceable', color: theme.palette.accent.tertiary, y: codeY + 0.18 }, + { label: 'Studio', color: theme.palette.accent.tertiary, y: codeY + 2.05 }, + { label: 'Platform', color: theme.palette.accent.tertiary, y: codeY + 2.55 }, + ]; + satellites.forEach((s) => { + slide.addShape(pres.ShapeType.roundRect, { + x: codeX + 0.4, y: s.y, w: codeW - 0.8, h: 0.4, + fill: { color: theme.palette.bg.code }, + line: { color: s.color, width: 1.25 }, + rectRadius: 0.06, + }); + slide.addText(s.label, { + x: codeX + 0.4, y: s.y, w: codeW - 0.8, h: 0.4, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 12, bold: true, + color: s.color, + align: 'center', valign: 'middle', + margin: 0, + }); + }); + + // Caption under the mock + slide.addText('observability | visual debug | deploy', { + x: codeX, y: codeY + codeH + 0.05, w: codeW, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 11, italic: true, + color: theme.palette.text.muted, + align: 'center', valign: 'middle', margin: 0, + }); + + // Bottom meta strip + slide.addText('12 СЛАЙДОВ | PYTHON >= 3.9 | LANGSMITH 0.8.x', { + x: 0.7, y: 5.05, w: 9, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, + color: theme.palette.text.muted, + align: 'left', valign: 'middle', + charSpacing: 3, margin: 0, + }); + + ds.helpers.addPageNumber(slide, pres, theme, 1); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-02.js b/slides/section5-ecosystem/slide-02.js new file mode 100644 index 0000000..cbdaa97 --- /dev/null +++ b/slides/section5-ecosystem/slide-02.js @@ -0,0 +1,116 @@ +// Slide 02: Why LangSmith -- three pillars (observability, eval, datasets) +// Content slide with 3 pillar cards. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Зачем LangSmith', + title: 'Три столпа поверх LangChain/LangGraph', + sectionNumber: 5, + }); + + // Lead paragraph + slide.addText( + 'LangSmith -- SaaS-платформа (с self-hosted вариантом) для production-наблюдения ' + + 'и оценки LLM-приложений. Три ключевые возможности покрывают весь цикл: ' + + 'отладка в dev, метрики в prod, оценка качества на датасетах.', + { + x: 0.5, y: 1.5, w: 9.0, h: 0.7, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 13, + color: theme.palette.text.secondary, + align: 'left', valign: 'top', + margin: 0, + } + ); + + // Three pillar cards + const pillars = [ + { + title: 'Tracing', + sub: 'observability', + body: 'Каждый вызов Runnable, графа, tool логируется как Run с input/output, ' + + 'latency, token-cost. Дерево вызовов -- в UI, и через SDK.', + }, + { + title: 'Datasets', + sub: 'eval sets', + body: 'Версионируемые наборы примеров (input + expected output). ' + + 'Используются для off-line eval, regression-тестов, few-shot examples.', + }, + { + title: 'Evaluators', + sub: 'quality scoring', + body: 'LLM-as-judge, heuristic, human -- на выбор. Прогоняются на датасете, ' + + 'результаты привязываются к эксперименту (commit, prompt version).', + }, + ]; + + const cardW = 3.0; + const cardH = 2.2; + const gap = 0.15; + const startX = 0.5; + const cardY = 2.4; + + pillars.forEach((p, i) => { + const x = startX + i * (cardW + gap); + slide.addShape(pres.ShapeType.roundRect, { + x: x, y: cardY, w: cardW, h: cardH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.1, + }); + // Left accent bar + slide.addShape(pres.ShapeType.rect, { + x: x, y: cardY, w: 0.08, h: cardH, + fill: { color: theme.palette.accent.secondary }, + line: { type: 'none' }, + }); + slide.addText(p.title, { + x: x + 0.25, y: cardY + 0.2, w: cardW - 0.4, h: 0.45, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 22, bold: true, + color: theme.palette.accent.secondary, + valign: 'middle', margin: 0, + }); + slide.addText(p.sub, { + x: x + 0.25, y: cardY + 0.65, w: cardW - 0.4, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, italic: true, + color: theme.palette.text.muted, + valign: 'middle', margin: 0, + }); + slide.addText(p.body, { + x: x + 0.25, y: cardY + 1.05, w: cardW - 0.4, h: cardH - 1.2, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 12, + color: theme.palette.text.secondary, + valign: 'top', margin: 0, + }); + }); + + // Bottom meta strip + slide.addText( + 'Cloud: smith.langchain.com | SDK: langsmith==0.8.9 | Self-hosted v0.9 (changelog 21.01.2025)', + { + x: 0.5, y: 4.8, w: 9.0, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, italic: true, + color: theme.palette.text.muted, + align: 'left', valign: 'middle', + margin: 0, + } + ); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.smith.langchain.com + reference.langchain.com/python/langsmith', + }); + ds.helpers.addPageNumber(slide, pres, theme, 2); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-03.js b/slides/section5-ecosystem/slide-03.js new file mode 100644 index 0000000..d583a94 --- /dev/null +++ b/slides/section5-ecosystem/slide-03.js @@ -0,0 +1,71 @@ +// Slide 03: Installation + environment setup +// Code slide with two-column layout: pip install + env vars. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Установка', + title: 'pip install и 3 переменные окружения', + sectionNumber: 5, + titleSize: 22, + }); + + // Left code block -- pip install + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.45, w: 4.5, h: 2.5, + language: 'bash', + filePath: 'shell/install_langsmith.sh', + fontSize: 10, + code: [ + '# Standalone SDK (если не используете langchain)', + 'pip install langsmith', + '', + '# С langchain/LangGraph auto-tracing работает', + '# автоматически при env-переменных -- отдельно', + '# ставить SDK необязательно.', + 'pip install langchain langgraph', + ].join('\n'), + }); + + // Right code block -- env vars + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 5.2, y: 1.45, w: 4.3, h: 2.5, + language: 'bash', + filePath: 'shell/env.sh', + fontSize: 10, + code: [ + '# 1. Tracing on/off (default: true если есть API key)', + 'export LANGSMITH_TRACING=true', + '', + '# 2. API key: https://smith.langchain.com/settings', + 'export LANGSMITH_API_KEY="lsv2_pt_..."', + '', + '# 3. Project name (default: "default")', + 'export LANGSMITH_PROJECT="my-agent-dev"', + '', + '# Опционально: self-hosted endpoint', + '# export LANGSMITH_ENDPOINT=...', + ].join('\n'), + }); + + // Bottom callout -- safe positioning + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.15, w: 9.0, h: 0.8, + kind: 'info', + title: 'Zero-config для LangChain/LangGraph', + text: 'Если LANGSMITH_API_KEY задан, chain.invoke/graph.invoke/agent.stream ' + + 'пишут trace автоматически -- без оберток и monkey-patch.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.smith.langchain.com Observability > Set up tracing', + }); + ds.helpers.addPageNumber(slide, pres, theme, 3); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-04.js b/slides/section5-ecosystem/slide-04.js new file mode 100644 index 0000000..606781a --- /dev/null +++ b/slides/section5-ecosystem/slide-04.js @@ -0,0 +1,55 @@ +// Slide 04: @traceable decorator -- the simplest way to instrument any function +// Code slide: nested decorator pattern. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Tracing', + title: '@traceable -- декоратор для любой функции', + sectionNumber: 5, + titleSize: 24, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.45, w: 9.0, h: 3.1, + language: 'python', + fontSize: 10, + code: [ + 'from langsmith import traceable', + '', + '# Декоратор превращает функцию в traced Run.', + '# Все аргументы и return попадают в trace автоматически.', + '@traceable(name="retrieve_docs", run_type="retriever")', + 'def retrieve(query: str, k: int = 4) -> list[str]:', + ' return vector_store.similarity_search(query, k=k)', + '', + '@traceable(name="generate_answer", run_type="chain")', + 'def generate_answer(query: str) -> str:', + ' docs = retrieve(query) # nested Run', + ' context = "\\n".join(docs)', + ' return llm.invoke(f"Q: {query}\\nCtx: {context}")', + '', + '# Дерево: generate_answer -> retrieve_docs -> ChatModel', + 'print(generate_answer("What is LCEL?"))', + ].join('\n'), + }); + + // Bottom callout + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.65, w: 9.0, h: 0.4, + kind: 'success', + text: 'Вложенные вызовы автоматически становятся дочерними Run-ами в дереве трассировки.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.smith.langchain.com Observability > Log traces > @traceable', + }); + ds.helpers.addPageNumber(slide, pres, theme, 4); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-05.js b/slides/section5-ecosystem/slide-05.js new file mode 100644 index 0000000..3a0bc43 --- /dev/null +++ b/slides/section5-ecosystem/slide-05.js @@ -0,0 +1,57 @@ +// Slide 05: RunTree -- manual tracing when @traceable is not enough +// Code slide: explicit parent/child control with RunTree. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Tracing', + title: 'RunTree -- ручной контроль над деревом', + sectionNumber: 5, + titleSize: 24, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.45, w: 9.0, h: 3.1, + language: 'python', + fontSize: 10, + code: [ + 'from langsmith.run_trees import RunTree', + '', + '# RunTree -- explicit handle: создается руками, передается', + '# в код, потом post() отправляет в LangSmith. Нужно когда', + '# @traceable не подходит (динамич. дерево, не-Python, metadata).', + 'parent = RunTree(', + ' name="agent_turn",', + ' run_type="chain",', + ' inputs={"q": "Who invented Python?"},', + ' project_name="my-agent-dev",', + ')', + 'try:', + ' answer = my_agent(question, run_tree=parent)', + ' parent.end(outputs={"answer": answer})', + 'except Exception as e:', + ' parent.end(error=str(e)) # ошибка логируется', + ' raise', + 'finally:', + ' parent.post() # flush в LangSmith', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.65, w: 9.0, h: 0.4, + kind: 'warning', + text: 'Не забудьте parent.post() -- иначе trace не отправится. Дочерние через parent.create_child(...).', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.smith.langchain.com Observability > Log traces > RunTree', + }); + ds.helpers.addPageNumber(slide, pres, theme, 5); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-06.js b/slides/section5-ecosystem/slide-06.js new file mode 100644 index 0000000..340e6de --- /dev/null +++ b/slides/section5-ecosystem/slide-06.js @@ -0,0 +1,59 @@ +// Slide 06: Auto-tracing with LangChain + how to attach metadata/tags +// Code slide with highlight on the metadata/tags block. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Tracing', + title: 'Auto-tracing LangChain + метаданные', + sectionNumber: 5, + titleSize: 24, + }); + + ds.helpers.addCodeBlockWithHighlight(slide, pres, theme, { + x: 0.5, y: 1.45, w: 9.0, h: 3.1, + language: 'python', + fontSize: 10, + code: [ + 'from langchain_openai import ChatOpenAI', + 'from langchain_core.prompts import ChatPromptTemplate', + '', + 'prompt = ChatPromptTemplate.from_template("Tell a joke about {topic}")', + 'model = ChatOpenAI(model="gpt-4o-mini")', + 'chain = prompt | model', + '', + '# auto-tracing: invoke/batch/stream автоматически создают Run', + '# с input, output, token usage, latency.', + 'result = chain.invoke(', + ' {"topic": "databases"},', + ' config={', + ' "run_name": "joke_chain", # имя в UI', + ' "tags": ["prod", "experiment-v3"], # фильтр в UI', + ' "metadata": { # любые поля', + ' "user_id": "u_123",', + ' "request_id": "req_abc",', + ' },', + ' },', + ')', + ].join('\n'), + lines: [12, 13, 14, 15, 16, 17, 18], + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.65, w: 9.0, h: 0.4, + kind: 'success', + text: 'tags и metadata -- способ группировать и фильтровать trace-ы в UI по user/session/feature.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.smith.langchain.com Observability > Add metadata and tags', + }); + ds.helpers.addPageNumber(slide, pres, theme, 6); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-07.js b/slides/section5-ecosystem/slide-07.js new file mode 100644 index 0000000..64b5198 --- /dev/null +++ b/slides/section5-ecosystem/slide-07.js @@ -0,0 +1,58 @@ +// Slide 07: Datasets -- create_dataset client API +// Code slide: programmatic dataset creation + adding examples. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Datasets', + title: 'create_dataset -- набор примеров под eval', + sectionNumber: 5, + titleSize: 24, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.45, w: 9.0, h: 2.85, + language: 'python', + fontSize: 10, + code: [ + 'from langsmith import Client', + '', + 'client = Client()', + '', + '# 1. Создаем датасет (или достаем существующий по имени)', + 'dataset = client.create_dataset(', + ' dataset_name="rag-qa-eval-v1",', + ' description="RAG golden set",', + ')', + '', + '# 2. Добавляем примеры пачкой: inputs + reference outputs', + 'client.create_examples(', + ' dataset_id=dataset.id,', + ' inputs=[{"q": "Что такое LCEL?"}, {"q": "Что такое HITL?"}],', + ' outputs=[{"a": "LangChain Expression Language"},', + ' {"a": "Human-in-the-loop через interrupt"}],', + ')', + '', + '# 3. Версионирование: новые данные -> новый датасет', + 'client.clone_dataset(dataset.id, dataset_name="rag-qa-eval-v2")', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.5, w: 9.0, h: 0.55, + kind: 'info', + text: 'В UI датасеты можно создавать из CSV/JSONL через web -- API нужен для CI и автообновления.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.smith.langchain.com Evaluation > Datasets', + }); + ds.helpers.addPageNumber(slide, pres, theme, 7); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-08.js b/slides/section5-ecosystem/slide-08.js new file mode 100644 index 0000000..941928f --- /dev/null +++ b/slides/section5-ecosystem/slide-08.js @@ -0,0 +1,58 @@ +// Slide 08: Evaluators + run_on_dataset -- off-line evaluation pipeline +// Code slide: custom evaluator + run_on_dataset. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Evaluators', + title: 'run_on_dataset + evaluator-ы', + sectionNumber: 5, + titleSize: 24, + }); + + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.45, w: 9.0, h: 2.85, + language: 'python', + fontSize: 10, + code: [ + 'from langsmith import Client', + 'from langsmith.schemas import Example, Run', + '', + 'client = Client()', + '', + '# 1. Target -- что прогоняем (chain, graph, agent, callable)', + 'def target(inputs: dict) -> dict:', + ' return {"answer": my_chain.invoke(inputs["question"])}', + '', + '# 2. Evaluator -- scoring function', + 'def answer_match(run: Run, example: Example) -> dict:', + ' score = 1.0 if example.outputs["answer"] in run.outputs["answer"] else 0.0', + ' return {"key": "answer_match", "score": score}', + '', + '# 3. Прогон: target на датасете + scoring', + 'client.run_on_dataset(', + ' dataset_name="rag-qa-eval-v1",', + ' llm_or_chain_factory=target,', + ' evaluators=[answer_match],', + ')', + ].join('\n'), + }); + + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.5, w: 9.0, h: 0.55, + kind: 'success', + text: 'Готовые evaluator-ы: llm_as_judge, exact_match, embedding_distance, cot_qa.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'docs.smith.langchain.com Evaluation > Evaluator types', + }); + ds.helpers.addPageNumber(slide, pres, theme, 8); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-09.js b/slides/section5-ecosystem/slide-09.js new file mode 100644 index 0000000..3d9010f --- /dev/null +++ b/slides/section5-ecosystem/slide-09.js @@ -0,0 +1,128 @@ +// Slide 09: LangSmith Studio -- visual debugger for LangGraph +// Content + visual mock slide: graph canvas, run timeline, state inspector. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'LangGraph Studio', + title: 'Визуальный дебаг графа', + sectionNumber: 5, + }); + + // Lead paragraph + slide.addText( + 'Studio -- это desktop/web IDE для LangGraph. Показывает граф как граф, ' + + 'а не как свалку логов. Запускается локально через `langgraph dev`, ' + + 'либо хостится на LangGraph Platform.', + { + x: 0.5, y: 1.5, w: 9.0, h: 0.65, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 13, + color: theme.palette.text.secondary, + align: 'left', valign: 'top', + margin: 0, + } + ); + + // Left card: graph canvas mock + const gx = 0.5; + const gy = 2.25; + const gw = 4.5; + const gh = 2.6; + + slide.addShape(pres.ShapeType.roundRect, { + x: gx, y: gy, w: gw, h: gh, + fill: { color: theme.palette.bg.code }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.1, + }); + slide.addText('GRAPH CANVAS', { + x: gx + 0.15, y: gy + 0.1, w: gw - 0.3, h: 0.25, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, bold: true, + color: theme.palette.accent.tertiary, + charSpacing: 4, margin: 0, + }); + + // Mock nodes in the graph + const nodes = [ + { label: '__start__', x: gx + 0.2, y: gy + 0.5, color: theme.palette.accent.tertiary }, + { label: 'router', x: gx + 0.2, y: gy + 1.1, color: theme.palette.accent.primary }, + { label: 'agent', x: gx + 1.6, y: gy + 1.7, color: theme.palette.accent.primary }, + { label: 'tools', x: gx + 3.0, y: gy + 1.7, color: theme.palette.accent.primary }, + { label: 'END', x: gx + 3.0, y: gy + 0.5, color: theme.palette.accent.tertiary }, + ]; + nodes.forEach((n) => { + slide.addShape(pres.ShapeType.roundRect, { + x: n.x, y: n.y, w: 1.2, h: 0.45, + fill: { color: theme.palette.bg.elevated }, + line: { color: n.color, width: 1.5 }, + rectRadius: 0.06, + }); + slide.addText(n.label, { + x: n.x, y: n.y, w: 1.2, h: 0.45, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 10, bold: true, + color: n.color, + align: 'center', valign: 'middle', margin: 0, + }); + }); + + // Right card: run timeline mock + const tx = 5.2; + const ty = 2.25; + const tw = 4.3; + const th = 2.6; + + slide.addShape(pres.ShapeType.roundRect, { + x: tx, y: ty, w: tw, h: th, + fill: { color: theme.palette.bg.code }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.1, + }); + slide.addText('RUN INSPECTOR', { + x: tx + 0.15, y: ty + 0.1, w: tw - 0.3, h: 0.25, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, bold: true, + color: theme.palette.accent.tertiary, + charSpacing: 4, margin: 0, + }); + + const steps = [ + { t: '0ms', label: '__start__', color: theme.palette.accent.tertiary }, + { t: '+12ms', label: 'router', color: theme.palette.accent.primary }, + { t: '+840ms', label: 'agent (LLM call)', color: theme.palette.accent.secondary }, + { t: '+1.2s', label: 'tools[search]', color: theme.palette.accent.primary }, + { t: '+1.4s', label: 'agent (LLM call)', color: theme.palette.accent.secondary }, + { t: '+2.1s', label: 'END', color: theme.palette.accent.tertiary }, + ]; + steps.forEach((s, i) => { + const y = ty + 0.45 + i * 0.32; + slide.addText(s.t, { + x: tx + 0.15, y: y, w: 0.7, h: 0.28, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 9, + color: theme.palette.text.muted, + valign: 'middle', margin: 0, + }); + slide.addText(s.label, { + x: tx + 0.9, y: y, w: tw - 1.05, h: 0.28, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 11, bold: true, + color: s.color, + valign: 'middle', margin: 0, + }); + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'langchain-ai.github.io/langgraph/concepts/langgraph_studio/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 9); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-10.js b/slides/section5-ecosystem/slide-10.js new file mode 100644 index 0000000..edb4c08 --- /dev/null +++ b/slides/section5-ecosystem/slide-10.js @@ -0,0 +1,72 @@ +// Slide 10: LangGraph Platform deployment + langgraph.json + SDK invoke +// Code slide with platform overview + deploy + remote invoke. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Deployment', + title: 'LangGraph Platform: deploy + invoke', + sectionNumber: 5, + titleSize: 24, + }); + + // Left: langgraph.json + deploy + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.45, w: 4.5, h: 2.55, + language: 'bash', + fontSize: 10, + code: [ + '# 1. Конфиг: langgraph.json в корне репо', + 'cat > langgraph.json <<\'JSON\'', + '{', + ' "graphs": {"agent": "./agent.py:graph"},', + ' "env": "./.env"', + '}', + 'JSON', + '', + '# 2. Deploy one-shot', + 'langgraph deploy --name my-agent-prod', + ].join('\n'), + }); + + // Right: SDK invoke + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 5.2, y: 1.45, w: 4.3, h: 2.55, + language: 'python', + fontSize: 10, + code: [ + 'from langgraph_sdk import get_client', + '', + 'client = get_client(url=', + ' "https://my-agent-prod.us.langgraph.app"', + ')', + '', + '# async API: thread + run', + 'thread = await client.threads.create()', + 'run = await client.runs.create(', + ' thread["thread_id"], "agent", input={"q": "hi"}', + ')', + ].join('\n'), + }); + + // Bottom callout + ds.helpers.addCallout(slide, pres, theme, { + x: 0.5, y: 4.15, w: 9.0, h: 0.8, + kind: 'info', + title: 'LangGraph Platform', + text: 'managed-хостинг для графа: build из langgraph.json, deploy через CLI, ' + + 'получаете HTTPS endpoint + Studio UI + threads + cron + webhooks.', + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'langchain-ai.github.io/langgraph/concepts/langgraph_platform/', + }); + ds.helpers.addPageNumber(slide, pres, theme, 10); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-11.js b/slides/section5-ecosystem/slide-11.js new file mode 100644 index 0000000..0e42cee --- /dev/null +++ b/slides/section5-ecosystem/slide-11.js @@ -0,0 +1,174 @@ +// Slide 11: Self-hosted vs Cloud -- pros/cons decision panel +// Content slide: two cards with pros/cons. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Deployment', + title: 'Self-hosted vs Cloud: что выбрать', + sectionNumber: 5, + }); + + // Lead paragraph + slide.addText( + 'LangSmith и LangGraph Platform существуют в двух режимах: ' + + 'managed Cloud (быстрый старт, оплата по usage) и Self-Hosted (ваш K8s/VM, ' + + 'ваши данные остаются внутри периметра). Актуальная self-hosted версия -- v0.9.', + { + x: 0.5, y: 1.5, w: 9.0, h: 0.7, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 13, + color: theme.palette.text.secondary, + align: 'left', valign: 'top', + margin: 0, + } + ); + + // Two cards side by side + const cards = [ + { + x: 0.5, + title: 'Cloud', + sub: 'smith.langchain.com + managed platform', + pros: [ + 'Zero-setup: API key и заводите trace-ы', + 'Managed Postgres, Redis, scaling', + 'Studio UI включен в подписку', + 'Latest features сразу', + ], + cons: [ + 'Данные уходят в чужой VPC (US-region)', + 'Pay-per-trace: cost растет с нагрузкой', + 'Vendor lock на retention policy', + ], + accentColor: 'primary', + }, + { + x: 5.1, + title: 'Self-Hosted', + sub: 'Docker/Helm chart, v0.9', + pros: [ + 'Данные внутри своего VPC/compliance', + 'Flat cost: предсказуемо для prod', + 'Полный контроль над retention', + 'Air-gapped окружения поддерживаются', + ], + cons: [ + 'Ops: K8s, Postgres, Redis, ClickHouse', + 'Обновления руками (breaking changes)', + 'Studio UI = отдельный пакет', + 'Не все beta-фичи доступны сразу', + ], + accentColor: 'secondary', + }, + ]; + + const cardY = 2.35; + const cardW = 4.4; + const cardH = 2.6; + cards.forEach((c) => { + const accent = c.accentColor === 'primary' + ? theme.palette.accent.primary + : theme.palette.accent.secondary; + + slide.addShape(pres.ShapeType.roundRect, { + x: c.x, y: cardY, w: cardW, h: cardH, + fill: { color: theme.palette.bg.elevated }, + line: { color: theme.palette.border.subtle, width: 1 }, + rectRadius: 0.1, + }); + // Left accent bar + slide.addShape(pres.ShapeType.rect, { + x: c.x, y: cardY, w: 0.08, h: cardH, + fill: { color: accent }, + line: { type: 'none' }, + }); + + slide.addText(c.title, { + x: c.x + 0.25, y: cardY + 0.15, w: cardW - 0.4, h: 0.4, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 22, bold: true, + color: accent, + valign: 'middle', margin: 0, + }); + slide.addText(c.sub, { + x: c.x + 0.25, y: cardY + 0.55, w: cardW - 0.4, h: 0.25, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, italic: true, + color: theme.palette.text.muted, + valign: 'middle', margin: 0, + }); + + // Pros + slide.addText('+', { + x: c.x + 0.25, y: cardY + 0.85, w: 0.25, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 14, bold: true, + color: theme.palette.state.success, + valign: 'top', margin: 0, + }); + const prosBody = c.pros.map((p) => ({ + text: p + '\n', + options: { + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, + color: theme.palette.text.primary, + }, + })); + slide.addText(prosBody, { + x: c.x + 0.5, y: cardY + 0.85, w: cardW - 0.65, h: 1.0, + valign: 'top', + paraSpaceAfter: 2, + margin: 0, + }); + + // Cons + slide.addText('-', { + x: c.x + 0.25, y: cardY + 1.9, w: 0.25, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.code), + fontSize: 14, bold: true, + color: theme.palette.state.danger, + valign: 'top', margin: 0, + }); + const consBody = c.cons.map((p) => ({ + text: p + '\n', + options: { + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, + color: theme.palette.text.primary, + }, + })); + slide.addText(consBody, { + x: c.x + 0.5, y: cardY + 1.9, w: cardW - 0.65, h: 0.7, + valign: 'top', + paraSpaceAfter: 2, + margin: 0, + }); + }); + + // Bottom meta strip + slide.addText( + 'Рекомендация: Cloud для prototype и команд до 5 инженеров; ' + + 'self-hosted при compliance-требованиях и > 100M trace-ов в месяц.', + { + x: 0.5, y: 5.0, w: 9.0, h: 0.3, + fontFace: ds.helpers.withFallback(theme.fonts.ui), + fontSize: 10, italic: true, + color: theme.palette.text.muted, + align: 'left', valign: 'middle', + margin: 0, + } + ); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'changelog.langchain.com/announcements/langsmith-self-hosted-v0-9', + }); + ds.helpers.addPageNumber(slide, pres, theme, 11); +} + +module.exports = { createSlide }; diff --git a/slides/section5-ecosystem/slide-12.js b/slides/section5-ecosystem/slide-12.js new file mode 100644 index 0000000..15345a6 --- /dev/null +++ b/slides/section5-ecosystem/slide-12.js @@ -0,0 +1,66 @@ +// Slide 12: TypeScript SDK + final ecosystem pros/cons +// Two-column: code on left, pros/cons on right. + +const ds = require('./design-system'); + +function createSlide(pres, theme) { + const slide = pres.addSlide(); + ds.helpers.slideBase(slide, pres, theme); + + ds.helpers.addHeader(slide, pres, theme, { + eyebrow: 'STAGE 5: ECOSYSTEM', + section: 'Итоги', + title: 'TypeScript SDK и плюсы/минусы', + sectionNumber: 5, + titleSize: 24, + }); + + // Left code block -- TypeScript SDK + ds.helpers.addCodeBlock(slide, pres, theme, { + x: 0.5, y: 1.45, w: 5.5, h: 3.1, + language: 'typescript', + fontSize: 10, + code: [ + 'import { Client, traceable } from "langsmith";', + '', + 'const client = new Client();', + '', + '// @traceable decorator -- точно как в Python', + 'const retrieve = traceable(', + ' async function retrieve(query: string) {', + ' return vectorStore.similaritySearch(query, 4);', + ' },', + ' { name: "retrieve_docs", runType: "retriever" }', + ');', + '', + '// run_on_dataset для eval -- тоже есть', + 'await client.runOnDataset(', + ' "rag-qa-eval-v1", target,', + ' { evaluators: [answerMatch] }', + ');', + ].join('\n'), + }); + + // Right pros/cons panel + ds.helpers.addProsCons(slide, pres, theme, { + x: 6.2, y: 1.45, w: 3.3, h: 3.1, + pros: [ + 'Auto-tracing из коробки', + 'Eval = CI/CD для промптов', + 'Platform = deploy за минуты', + 'Self-hosted опция', + ], + cons: [ + 'LangSmith SDK 0.x', + 'Cloud растет в цене', + 'Self-hosted требует K8s', + ], + }); + + ds.helpers.addSourceLine(slide, pres, theme, { + source: 'github.com/langchain-ai/langsmith-sdk + langgraph-sdk README', + }); + ds.helpers.addPageNumber(slide, pres, theme, 12); +} + +module.exports = { createSlide };