Initial commit: LangChain evolution tutorial deck (132 slides)
- Cover, TOC, 5 dividers, 3 recap slides - 5 sections (chains, langgraph, deepagents, openswe, ecosystem) - design-system.js with theme tokens + 9 helper functions - research/: timeline + sources + per-tech notes - final-compile.js + merge.js for rebuild pipeline - output/: langchain-evolution.pptx (2.3 MB) + langchain-evolution.pdf (1.1 MB) + 7 sample previews
@@ -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
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# langchain-evolution-deck
|
|
||||||
|
|
||||||
Большой tutorial-дек по эволюции LangChain: от chains 2022 до Open SWE 2025/2026. 132 слайда в dark mode, Python 1.0+.
|
|
||||||
@@ -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<number>} [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,
|
||||||
|
};
|
||||||
@@ -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` |
|
||||||
@@ -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); });
|
||||||
@@ -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<num>-<name>/
|
||||||
|
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()
|
||||||
@@ -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(
|
||||||
|
/<Relationship\s+[^>]*Type="[^"]*slideLayout"[^>]*\/>/g,
|
||||||
|
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>'
|
||||||
|
);
|
||||||
|
// Drop other layout/master/image rels (we don't carry media)
|
||||||
|
relsXml = relsXml.replace(/<Relationship\s+[^>]*Type="[^"]*image"[^>]*\/>/g, '');
|
||||||
|
relsXml = relsXml.replace(/<Relationship\s+[^>]*Type="[^"]*notesSlide"[^>]*\/>/g, '');
|
||||||
|
} else {
|
||||||
|
relsXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/></Relationships>';
|
||||||
|
}
|
||||||
|
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 = '<p:sldId id="' + newSldId + '" r:id="rId' + newSldId + '"/>';
|
||||||
|
const updated = presXml.replace(/<\/p:sldIdLst>/, newSldEntry + '</p:sldIdLst>');
|
||||||
|
// 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 = '<Relationship Id="rId' + newSldId + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide' + newId + '.xml"/>';
|
||||||
|
const updatedRels = presRelsXml.replace(/<\/Relationships>/, newRelEntry + '</Relationships>');
|
||||||
|
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); });
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 обратная совместимость».
|
||||||
@@ -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-а** для кодинг-агента.
|
||||||
@@ -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) — отдельная тема.
|
||||||
@@ -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).
|
||||||
@@ -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 — обе даты зафиксированы.
|
||||||
@@ -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.
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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<number>} [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,
|
||||||
|
};
|
||||||
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 128 KiB |
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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',
|
||||||
|
'',
|
||||||
|
'# Формат: "<provider>:<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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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("<provider>:<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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 116 KiB |