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,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 };
|
||||