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,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Re-render section5 PDF -> PNG previews. Wipes stale PNGs via mavis-trash."""
|
||||
from pdf2image import convert_from_path
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
src = Path('/Users/alexandr/.mavis/plans/plan_85053139/workspace/lc-evo-deck/slides/section5-ecosystem/section5.pdf')
|
||||
out = Path('/Users/alexandr/.mavis/plans/plan_85053139/workspace/lc-evo-deck/slides/section5-ecosystem/previews')
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Move stale PNGs to trash via mavis-trash
|
||||
stale = sorted(out.glob('slide-*.png'))
|
||||
if stale:
|
||||
subprocess.run(['mavis-trash', *[str(p) for p in stale]], check=False)
|
||||
|
||||
imgs = convert_from_path(str(src), dpi=110)
|
||||
for i, im in enumerate(imgs, 1):
|
||||
p = out / f'slide-{i:02d}.png'
|
||||
im.save(str(p), 'PNG')
|
||||
print(f'wrote {len(imgs)} previews to {out}')
|
||||
@@ -0,0 +1,41 @@
|
||||
// Compile all section-5 slides into a single PPTX
|
||||
// Output: section5.pptx (12 slides, 16:9, dark theme, code-heavy)
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const pptxgen = require('pptxgenjs');
|
||||
|
||||
const ds = require('./design-system');
|
||||
const { theme } = ds;
|
||||
|
||||
const pres = new pptxgen();
|
||||
pres.layout = 'LAYOUT_16x9';
|
||||
pres.title = 'Ecosystem: LangSmith, Studio, deployment';
|
||||
pres.author = 'lc-evo-deck';
|
||||
pres.subject = 'Section 5 of the LangChain Evolution deck';
|
||||
|
||||
const SLIDE_COUNT = 12;
|
||||
|
||||
for (let i = 1; i <= SLIDE_COUNT; i++) {
|
||||
const num = String(i).padStart(2, '0');
|
||||
const file = path.join(__dirname, `slide-${num}.js`);
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error('Missing slide module: ' + file);
|
||||
}
|
||||
const mod = require(file);
|
||||
if (typeof mod.createSlide !== 'function') {
|
||||
throw new Error('Module does not export createSlide: ' + file);
|
||||
}
|
||||
mod.createSlide(pres, theme);
|
||||
}
|
||||
|
||||
const outFile = path.join(__dirname, 'section5.pptx');
|
||||
pres.writeFile({ fileName: outFile })
|
||||
.then((fileName) => {
|
||||
console.log('OK ->', fileName);
|
||||
console.log('Slides:', SLIDE_COUNT);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('ERR:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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: 91 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 109 KiB |
@@ -0,0 +1,152 @@
|
||||
// Slide 01: Section cover -- Stage 5 / Ecosystem (LangSmith + Studio + Platform)
|
||||
// Asymmetric layout: big section number + title block on left,
|
||||
// "ecosystem wheel" mock on right with the 3 satellite products.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
// Left vertical accent stripe -- gold for the ecosystem meta-section
|
||||
slide.addShape(pres.ShapeType.rect, {
|
||||
x: 0, y: 0, w: 0.25, h: 5.625,
|
||||
fill: { color: theme.palette.accent.secondary },
|
||||
line: { type: 'none' },
|
||||
});
|
||||
|
||||
// Top tag pill: section meta
|
||||
slide.addShape(pres.ShapeType.roundRect, {
|
||||
x: 0.7, y: 0.55, w: 2.8, h: 0.36,
|
||||
fill: { color: theme.palette.accent.secondary },
|
||||
line: { type: 'none' },
|
||||
rectRadius: 0.18,
|
||||
});
|
||||
slide.addText('STAGE 5 | ECOSYSTEM', {
|
||||
x: 0.7, y: 0.55, w: 2.8, h: 0.36,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10, bold: true,
|
||||
color: theme.palette.bg.primary,
|
||||
align: 'center', valign: 'middle',
|
||||
charSpacing: 4, margin: 0,
|
||||
});
|
||||
|
||||
// Main title
|
||||
slide.addText('Экосистема', {
|
||||
x: 0.7, y: 1.15, w: 5.9, h: 1.0,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 56, bold: true,
|
||||
color: theme.palette.text.primary,
|
||||
align: 'left', valign: 'middle',
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
// Subtitle
|
||||
slide.addText('LangSmith, Studio, deployment', {
|
||||
x: 0.7, y: 2.15, w: 9, h: 0.55,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 26,
|
||||
color: theme.palette.accent.tertiary,
|
||||
align: 'left', valign: 'middle',
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
// Description block
|
||||
slide.addText(
|
||||
'Сервисы вокруг 4 основных ступеней: observability и eval ' +
|
||||
'(LangSmith SDK), визуальный дебаг графов (LangGraph Studio), ' +
|
||||
'production deployment (LangGraph Platform), self-hosted и TypeScript-клиенты. ' +
|
||||
'Все, что превращает прототип в production-grade систему.',
|
||||
{
|
||||
x: 0.7, y: 2.85, w: 5.7, h: 1.6,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
align: 'left', valign: 'top',
|
||||
margin: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// Right-side ecosystem mock: LangSmith SDK in center, 3 satellite products
|
||||
const codeX = 6.7;
|
||||
const codeY = 1.55;
|
||||
const codeW = 2.85;
|
||||
const codeH = 3.05;
|
||||
|
||||
slide.addShape(pres.ShapeType.roundRect, {
|
||||
x: codeX, y: codeY, w: codeW, h: codeH,
|
||||
fill: { color: theme.palette.bg.elevated },
|
||||
line: { color: theme.palette.border.subtle, width: 1 },
|
||||
rectRadius: 0.1,
|
||||
});
|
||||
|
||||
// Center node: LangSmith SDK
|
||||
slide.addShape(pres.ShapeType.roundRect, {
|
||||
x: codeX + 0.7, y: codeY + 1.15, w: codeW - 1.4, h: 0.75,
|
||||
fill: { color: theme.palette.accent.primary },
|
||||
line: { type: 'none' },
|
||||
rectRadius: 0.08,
|
||||
});
|
||||
slide.addText('langsmith', {
|
||||
x: codeX + 0.7, y: codeY + 1.15, w: codeW - 1.4, h: 0.45,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.code),
|
||||
fontSize: 16, bold: true,
|
||||
color: theme.palette.bg.primary,
|
||||
align: 'center', valign: 'middle',
|
||||
margin: 0,
|
||||
});
|
||||
slide.addText('Python SDK 0.8.9', {
|
||||
x: codeX + 0.7, y: codeY + 1.5, w: codeW - 1.4, h: 0.35,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10, italic: true,
|
||||
color: theme.palette.bg.primary,
|
||||
align: 'center', valign: 'middle',
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
// Three satellite nodes
|
||||
const satellites = [
|
||||
{ label: '@traceable', color: theme.palette.accent.tertiary, y: codeY + 0.18 },
|
||||
{ label: 'Studio', color: theme.palette.accent.tertiary, y: codeY + 2.05 },
|
||||
{ label: 'Platform', color: theme.palette.accent.tertiary, y: codeY + 2.55 },
|
||||
];
|
||||
satellites.forEach((s) => {
|
||||
slide.addShape(pres.ShapeType.roundRect, {
|
||||
x: codeX + 0.4, y: s.y, w: codeW - 0.8, h: 0.4,
|
||||
fill: { color: theme.palette.bg.code },
|
||||
line: { color: s.color, width: 1.25 },
|
||||
rectRadius: 0.06,
|
||||
});
|
||||
slide.addText(s.label, {
|
||||
x: codeX + 0.4, y: s.y, w: codeW - 0.8, h: 0.4,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.code),
|
||||
fontSize: 12, bold: true,
|
||||
color: s.color,
|
||||
align: 'center', valign: 'middle',
|
||||
margin: 0,
|
||||
});
|
||||
});
|
||||
|
||||
// Caption under the mock
|
||||
slide.addText('observability | visual debug | deploy', {
|
||||
x: codeX, y: codeY + codeH + 0.05, w: codeW, h: 0.3,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 11, italic: true,
|
||||
color: theme.palette.text.muted,
|
||||
align: 'center', valign: 'middle', margin: 0,
|
||||
});
|
||||
|
||||
// Bottom meta strip
|
||||
slide.addText('12 СЛАЙДОВ | PYTHON >= 3.9 | LANGSMITH 0.8.x', {
|
||||
x: 0.7, y: 5.05, w: 9, h: 0.3,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10,
|
||||
color: theme.palette.text.muted,
|
||||
align: 'left', valign: 'middle',
|
||||
charSpacing: 3, margin: 0,
|
||||
});
|
||||
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 1);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,116 @@
|
||||
// Slide 02: Why LangSmith -- three pillars (observability, eval, datasets)
|
||||
// Content slide with 3 pillar cards.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Зачем LangSmith',
|
||||
title: 'Три столпа поверх LangChain/LangGraph',
|
||||
sectionNumber: 5,
|
||||
});
|
||||
|
||||
// Lead paragraph
|
||||
slide.addText(
|
||||
'LangSmith -- SaaS-платформа (с self-hosted вариантом) для production-наблюдения ' +
|
||||
'и оценки LLM-приложений. Три ключевые возможности покрывают весь цикл: ' +
|
||||
'отладка в dev, метрики в prod, оценка качества на датасетах.',
|
||||
{
|
||||
x: 0.5, y: 1.5, w: 9.0, h: 0.7,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
align: 'left', valign: 'top',
|
||||
margin: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// Three pillar cards
|
||||
const pillars = [
|
||||
{
|
||||
title: 'Tracing',
|
||||
sub: 'observability',
|
||||
body: 'Каждый вызов Runnable, графа, tool логируется как Run с input/output, ' +
|
||||
'latency, token-cost. Дерево вызовов -- в UI, и через SDK.',
|
||||
},
|
||||
{
|
||||
title: 'Datasets',
|
||||
sub: 'eval sets',
|
||||
body: 'Версионируемые наборы примеров (input + expected output). ' +
|
||||
'Используются для off-line eval, regression-тестов, few-shot examples.',
|
||||
},
|
||||
{
|
||||
title: 'Evaluators',
|
||||
sub: 'quality scoring',
|
||||
body: 'LLM-as-judge, heuristic, human -- на выбор. Прогоняются на датасете, ' +
|
||||
'результаты привязываются к эксперименту (commit, prompt version).',
|
||||
},
|
||||
];
|
||||
|
||||
const cardW = 3.0;
|
||||
const cardH = 2.2;
|
||||
const gap = 0.15;
|
||||
const startX = 0.5;
|
||||
const cardY = 2.4;
|
||||
|
||||
pillars.forEach((p, i) => {
|
||||
const x = startX + i * (cardW + gap);
|
||||
slide.addShape(pres.ShapeType.roundRect, {
|
||||
x: x, y: cardY, w: cardW, h: cardH,
|
||||
fill: { color: theme.palette.bg.elevated },
|
||||
line: { color: theme.palette.border.subtle, width: 1 },
|
||||
rectRadius: 0.1,
|
||||
});
|
||||
// Left accent bar
|
||||
slide.addShape(pres.ShapeType.rect, {
|
||||
x: x, y: cardY, w: 0.08, h: cardH,
|
||||
fill: { color: theme.palette.accent.secondary },
|
||||
line: { type: 'none' },
|
||||
});
|
||||
slide.addText(p.title, {
|
||||
x: x + 0.25, y: cardY + 0.2, w: cardW - 0.4, h: 0.45,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.code),
|
||||
fontSize: 22, bold: true,
|
||||
color: theme.palette.accent.secondary,
|
||||
valign: 'middle', margin: 0,
|
||||
});
|
||||
slide.addText(p.sub, {
|
||||
x: x + 0.25, y: cardY + 0.65, w: cardW - 0.4, h: 0.3,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10, italic: true,
|
||||
color: theme.palette.text.muted,
|
||||
valign: 'middle', margin: 0,
|
||||
});
|
||||
slide.addText(p.body, {
|
||||
x: x + 0.25, y: cardY + 1.05, w: cardW - 0.4, h: cardH - 1.2,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
valign: 'top', margin: 0,
|
||||
});
|
||||
});
|
||||
|
||||
// Bottom meta strip
|
||||
slide.addText(
|
||||
'Cloud: smith.langchain.com | SDK: langsmith==0.8.9 | Self-hosted v0.9 (changelog 21.01.2025)',
|
||||
{
|
||||
x: 0.5, y: 4.8, w: 9.0, h: 0.3,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10, italic: true,
|
||||
color: theme.palette.text.muted,
|
||||
align: 'left', valign: 'middle',
|
||||
margin: 0,
|
||||
}
|
||||
);
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'docs.smith.langchain.com + reference.langchain.com/python/langsmith',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 2);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,71 @@
|
||||
// Slide 03: Installation + environment setup
|
||||
// Code slide with two-column layout: pip install + env vars.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Установка',
|
||||
title: 'pip install и 3 переменные окружения',
|
||||
sectionNumber: 5,
|
||||
titleSize: 22,
|
||||
});
|
||||
|
||||
// Left code block -- pip install
|
||||
ds.helpers.addCodeBlock(slide, pres, theme, {
|
||||
x: 0.5, y: 1.45, w: 4.5, h: 2.5,
|
||||
language: 'bash',
|
||||
filePath: 'shell/install_langsmith.sh',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'# Standalone SDK (если не используете langchain)',
|
||||
'pip install langsmith',
|
||||
'',
|
||||
'# С langchain/LangGraph auto-tracing работает',
|
||||
'# автоматически при env-переменных -- отдельно',
|
||||
'# ставить SDK необязательно.',
|
||||
'pip install langchain langgraph',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
// Right code block -- env vars
|
||||
ds.helpers.addCodeBlock(slide, pres, theme, {
|
||||
x: 5.2, y: 1.45, w: 4.3, h: 2.5,
|
||||
language: 'bash',
|
||||
filePath: 'shell/env.sh',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'# 1. Tracing on/off (default: true если есть API key)',
|
||||
'export LANGSMITH_TRACING=true',
|
||||
'',
|
||||
'# 2. API key: https://smith.langchain.com/settings',
|
||||
'export LANGSMITH_API_KEY="lsv2_pt_..."',
|
||||
'',
|
||||
'# 3. Project name (default: "default")',
|
||||
'export LANGSMITH_PROJECT="my-agent-dev"',
|
||||
'',
|
||||
'# Опционально: self-hosted endpoint',
|
||||
'# export LANGSMITH_ENDPOINT=...',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
// Bottom callout -- safe positioning
|
||||
ds.helpers.addCallout(slide, pres, theme, {
|
||||
x: 0.5, y: 4.15, w: 9.0, h: 0.8,
|
||||
kind: 'info',
|
||||
title: 'Zero-config для LangChain/LangGraph',
|
||||
text: 'Если LANGSMITH_API_KEY задан, chain.invoke/graph.invoke/agent.stream ' +
|
||||
'пишут trace автоматически -- без оберток и monkey-patch.',
|
||||
});
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'docs.smith.langchain.com Observability > Set up tracing',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 3);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,55 @@
|
||||
// Slide 04: @traceable decorator -- the simplest way to instrument any function
|
||||
// Code slide: nested decorator pattern.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Tracing',
|
||||
title: '@traceable -- декоратор для любой функции',
|
||||
sectionNumber: 5,
|
||||
titleSize: 24,
|
||||
});
|
||||
|
||||
ds.helpers.addCodeBlock(slide, pres, theme, {
|
||||
x: 0.5, y: 1.45, w: 9.0, h: 3.1,
|
||||
language: 'python',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'from langsmith import traceable',
|
||||
'',
|
||||
'# Декоратор превращает функцию в traced Run.',
|
||||
'# Все аргументы и return попадают в trace автоматически.',
|
||||
'@traceable(name="retrieve_docs", run_type="retriever")',
|
||||
'def retrieve(query: str, k: int = 4) -> list[str]:',
|
||||
' return vector_store.similarity_search(query, k=k)',
|
||||
'',
|
||||
'@traceable(name="generate_answer", run_type="chain")',
|
||||
'def generate_answer(query: str) -> str:',
|
||||
' docs = retrieve(query) # nested Run',
|
||||
' context = "\\n".join(docs)',
|
||||
' return llm.invoke(f"Q: {query}\\nCtx: {context}")',
|
||||
'',
|
||||
'# Дерево: generate_answer -> retrieve_docs -> ChatModel',
|
||||
'print(generate_answer("What is LCEL?"))',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
// Bottom callout
|
||||
ds.helpers.addCallout(slide, pres, theme, {
|
||||
x: 0.5, y: 4.65, w: 9.0, h: 0.4,
|
||||
kind: 'success',
|
||||
text: 'Вложенные вызовы автоматически становятся дочерними Run-ами в дереве трассировки.',
|
||||
});
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'docs.smith.langchain.com Observability > Log traces > @traceable',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 4);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,57 @@
|
||||
// Slide 05: RunTree -- manual tracing when @traceable is not enough
|
||||
// Code slide: explicit parent/child control with RunTree.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Tracing',
|
||||
title: 'RunTree -- ручной контроль над деревом',
|
||||
sectionNumber: 5,
|
||||
titleSize: 24,
|
||||
});
|
||||
|
||||
ds.helpers.addCodeBlock(slide, pres, theme, {
|
||||
x: 0.5, y: 1.45, w: 9.0, h: 3.1,
|
||||
language: 'python',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'from langsmith.run_trees import RunTree',
|
||||
'',
|
||||
'# RunTree -- explicit handle: создается руками, передается',
|
||||
'# в код, потом post() отправляет в LangSmith. Нужно когда',
|
||||
'# @traceable не подходит (динамич. дерево, не-Python, metadata).',
|
||||
'parent = RunTree(',
|
||||
' name="agent_turn",',
|
||||
' run_type="chain",',
|
||||
' inputs={"q": "Who invented Python?"},',
|
||||
' project_name="my-agent-dev",',
|
||||
')',
|
||||
'try:',
|
||||
' answer = my_agent(question, run_tree=parent)',
|
||||
' parent.end(outputs={"answer": answer})',
|
||||
'except Exception as e:',
|
||||
' parent.end(error=str(e)) # ошибка логируется',
|
||||
' raise',
|
||||
'finally:',
|
||||
' parent.post() # flush в LangSmith',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
ds.helpers.addCallout(slide, pres, theme, {
|
||||
x: 0.5, y: 4.65, w: 9.0, h: 0.4,
|
||||
kind: 'warning',
|
||||
text: 'Не забудьте parent.post() -- иначе trace не отправится. Дочерние через parent.create_child(...).',
|
||||
});
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'docs.smith.langchain.com Observability > Log traces > RunTree',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 5);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,59 @@
|
||||
// Slide 06: Auto-tracing with LangChain + how to attach metadata/tags
|
||||
// Code slide with highlight on the metadata/tags block.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Tracing',
|
||||
title: 'Auto-tracing LangChain + метаданные',
|
||||
sectionNumber: 5,
|
||||
titleSize: 24,
|
||||
});
|
||||
|
||||
ds.helpers.addCodeBlockWithHighlight(slide, pres, theme, {
|
||||
x: 0.5, y: 1.45, w: 9.0, h: 3.1,
|
||||
language: 'python',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'from langchain_openai import ChatOpenAI',
|
||||
'from langchain_core.prompts import ChatPromptTemplate',
|
||||
'',
|
||||
'prompt = ChatPromptTemplate.from_template("Tell a joke about {topic}")',
|
||||
'model = ChatOpenAI(model="gpt-4o-mini")',
|
||||
'chain = prompt | model',
|
||||
'',
|
||||
'# auto-tracing: invoke/batch/stream автоматически создают Run',
|
||||
'# с input, output, token usage, latency.',
|
||||
'result = chain.invoke(',
|
||||
' {"topic": "databases"},',
|
||||
' config={',
|
||||
' "run_name": "joke_chain", # имя в UI',
|
||||
' "tags": ["prod", "experiment-v3"], # фильтр в UI',
|
||||
' "metadata": { # любые поля',
|
||||
' "user_id": "u_123",',
|
||||
' "request_id": "req_abc",',
|
||||
' },',
|
||||
' },',
|
||||
')',
|
||||
].join('\n'),
|
||||
lines: [12, 13, 14, 15, 16, 17, 18],
|
||||
});
|
||||
|
||||
ds.helpers.addCallout(slide, pres, theme, {
|
||||
x: 0.5, y: 4.65, w: 9.0, h: 0.4,
|
||||
kind: 'success',
|
||||
text: 'tags и metadata -- способ группировать и фильтровать trace-ы в UI по user/session/feature.',
|
||||
});
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'docs.smith.langchain.com Observability > Add metadata and tags',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 6);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,58 @@
|
||||
// Slide 07: Datasets -- create_dataset client API
|
||||
// Code slide: programmatic dataset creation + adding examples.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Datasets',
|
||||
title: 'create_dataset -- набор примеров под eval',
|
||||
sectionNumber: 5,
|
||||
titleSize: 24,
|
||||
});
|
||||
|
||||
ds.helpers.addCodeBlock(slide, pres, theme, {
|
||||
x: 0.5, y: 1.45, w: 9.0, h: 2.85,
|
||||
language: 'python',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'from langsmith import Client',
|
||||
'',
|
||||
'client = Client()',
|
||||
'',
|
||||
'# 1. Создаем датасет (или достаем существующий по имени)',
|
||||
'dataset = client.create_dataset(',
|
||||
' dataset_name="rag-qa-eval-v1",',
|
||||
' description="RAG golden set",',
|
||||
')',
|
||||
'',
|
||||
'# 2. Добавляем примеры пачкой: inputs + reference outputs',
|
||||
'client.create_examples(',
|
||||
' dataset_id=dataset.id,',
|
||||
' inputs=[{"q": "Что такое LCEL?"}, {"q": "Что такое HITL?"}],',
|
||||
' outputs=[{"a": "LangChain Expression Language"},',
|
||||
' {"a": "Human-in-the-loop через interrupt"}],',
|
||||
')',
|
||||
'',
|
||||
'# 3. Версионирование: новые данные -> новый датасет',
|
||||
'client.clone_dataset(dataset.id, dataset_name="rag-qa-eval-v2")',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
ds.helpers.addCallout(slide, pres, theme, {
|
||||
x: 0.5, y: 4.5, w: 9.0, h: 0.55,
|
||||
kind: 'info',
|
||||
text: 'В UI датасеты можно создавать из CSV/JSONL через web -- API нужен для CI и автообновления.',
|
||||
});
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'docs.smith.langchain.com Evaluation > Datasets',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 7);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,58 @@
|
||||
// Slide 08: Evaluators + run_on_dataset -- off-line evaluation pipeline
|
||||
// Code slide: custom evaluator + run_on_dataset.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Evaluators',
|
||||
title: 'run_on_dataset + evaluator-ы',
|
||||
sectionNumber: 5,
|
||||
titleSize: 24,
|
||||
});
|
||||
|
||||
ds.helpers.addCodeBlock(slide, pres, theme, {
|
||||
x: 0.5, y: 1.45, w: 9.0, h: 2.85,
|
||||
language: 'python',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'from langsmith import Client',
|
||||
'from langsmith.schemas import Example, Run',
|
||||
'',
|
||||
'client = Client()',
|
||||
'',
|
||||
'# 1. Target -- что прогоняем (chain, graph, agent, callable)',
|
||||
'def target(inputs: dict) -> dict:',
|
||||
' return {"answer": my_chain.invoke(inputs["question"])}',
|
||||
'',
|
||||
'# 2. Evaluator -- scoring function',
|
||||
'def answer_match(run: Run, example: Example) -> dict:',
|
||||
' score = 1.0 if example.outputs["answer"] in run.outputs["answer"] else 0.0',
|
||||
' return {"key": "answer_match", "score": score}',
|
||||
'',
|
||||
'# 3. Прогон: target на датасете + scoring',
|
||||
'client.run_on_dataset(',
|
||||
' dataset_name="rag-qa-eval-v1",',
|
||||
' llm_or_chain_factory=target,',
|
||||
' evaluators=[answer_match],',
|
||||
')',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
ds.helpers.addCallout(slide, pres, theme, {
|
||||
x: 0.5, y: 4.5, w: 9.0, h: 0.55,
|
||||
kind: 'success',
|
||||
text: 'Готовые evaluator-ы: llm_as_judge, exact_match, embedding_distance, cot_qa.',
|
||||
});
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'docs.smith.langchain.com Evaluation > Evaluator types',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 8);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,128 @@
|
||||
// Slide 09: LangSmith Studio -- visual debugger for LangGraph
|
||||
// Content + visual mock slide: graph canvas, run timeline, state inspector.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'LangGraph Studio',
|
||||
title: 'Визуальный дебаг графа',
|
||||
sectionNumber: 5,
|
||||
});
|
||||
|
||||
// Lead paragraph
|
||||
slide.addText(
|
||||
'Studio -- это desktop/web IDE для LangGraph. Показывает граф как граф, ' +
|
||||
'а не как свалку логов. Запускается локально через `langgraph dev`, ' +
|
||||
'либо хостится на LangGraph Platform.',
|
||||
{
|
||||
x: 0.5, y: 1.5, w: 9.0, h: 0.65,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
align: 'left', valign: 'top',
|
||||
margin: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// Left card: graph canvas mock
|
||||
const gx = 0.5;
|
||||
const gy = 2.25;
|
||||
const gw = 4.5;
|
||||
const gh = 2.6;
|
||||
|
||||
slide.addShape(pres.ShapeType.roundRect, {
|
||||
x: gx, y: gy, w: gw, h: gh,
|
||||
fill: { color: theme.palette.bg.code },
|
||||
line: { color: theme.palette.border.subtle, width: 1 },
|
||||
rectRadius: 0.1,
|
||||
});
|
||||
slide.addText('GRAPH CANVAS', {
|
||||
x: gx + 0.15, y: gy + 0.1, w: gw - 0.3, h: 0.25,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10, bold: true,
|
||||
color: theme.palette.accent.tertiary,
|
||||
charSpacing: 4, margin: 0,
|
||||
});
|
||||
|
||||
// Mock nodes in the graph
|
||||
const nodes = [
|
||||
{ label: '__start__', x: gx + 0.2, y: gy + 0.5, color: theme.palette.accent.tertiary },
|
||||
{ label: 'router', x: gx + 0.2, y: gy + 1.1, color: theme.palette.accent.primary },
|
||||
{ label: 'agent', x: gx + 1.6, y: gy + 1.7, color: theme.palette.accent.primary },
|
||||
{ label: 'tools', x: gx + 3.0, y: gy + 1.7, color: theme.palette.accent.primary },
|
||||
{ label: 'END', x: gx + 3.0, y: gy + 0.5, color: theme.palette.accent.tertiary },
|
||||
];
|
||||
nodes.forEach((n) => {
|
||||
slide.addShape(pres.ShapeType.roundRect, {
|
||||
x: n.x, y: n.y, w: 1.2, h: 0.45,
|
||||
fill: { color: theme.palette.bg.elevated },
|
||||
line: { color: n.color, width: 1.5 },
|
||||
rectRadius: 0.06,
|
||||
});
|
||||
slide.addText(n.label, {
|
||||
x: n.x, y: n.y, w: 1.2, h: 0.45,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.code),
|
||||
fontSize: 10, bold: true,
|
||||
color: n.color,
|
||||
align: 'center', valign: 'middle', margin: 0,
|
||||
});
|
||||
});
|
||||
|
||||
// Right card: run timeline mock
|
||||
const tx = 5.2;
|
||||
const ty = 2.25;
|
||||
const tw = 4.3;
|
||||
const th = 2.6;
|
||||
|
||||
slide.addShape(pres.ShapeType.roundRect, {
|
||||
x: tx, y: ty, w: tw, h: th,
|
||||
fill: { color: theme.palette.bg.code },
|
||||
line: { color: theme.palette.border.subtle, width: 1 },
|
||||
rectRadius: 0.1,
|
||||
});
|
||||
slide.addText('RUN INSPECTOR', {
|
||||
x: tx + 0.15, y: ty + 0.1, w: tw - 0.3, h: 0.25,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10, bold: true,
|
||||
color: theme.palette.accent.tertiary,
|
||||
charSpacing: 4, margin: 0,
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{ t: '0ms', label: '__start__', color: theme.palette.accent.tertiary },
|
||||
{ t: '+12ms', label: 'router', color: theme.palette.accent.primary },
|
||||
{ t: '+840ms', label: 'agent (LLM call)', color: theme.palette.accent.secondary },
|
||||
{ t: '+1.2s', label: 'tools[search]', color: theme.palette.accent.primary },
|
||||
{ t: '+1.4s', label: 'agent (LLM call)', color: theme.palette.accent.secondary },
|
||||
{ t: '+2.1s', label: 'END', color: theme.palette.accent.tertiary },
|
||||
];
|
||||
steps.forEach((s, i) => {
|
||||
const y = ty + 0.45 + i * 0.32;
|
||||
slide.addText(s.t, {
|
||||
x: tx + 0.15, y: y, w: 0.7, h: 0.28,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.code),
|
||||
fontSize: 9,
|
||||
color: theme.palette.text.muted,
|
||||
valign: 'middle', margin: 0,
|
||||
});
|
||||
slide.addText(s.label, {
|
||||
x: tx + 0.9, y: y, w: tw - 1.05, h: 0.28,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.code),
|
||||
fontSize: 11, bold: true,
|
||||
color: s.color,
|
||||
valign: 'middle', margin: 0,
|
||||
});
|
||||
});
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'langchain-ai.github.io/langgraph/concepts/langgraph_studio/',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 9);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,72 @@
|
||||
// Slide 10: LangGraph Platform deployment + langgraph.json + SDK invoke
|
||||
// Code slide with platform overview + deploy + remote invoke.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Deployment',
|
||||
title: 'LangGraph Platform: deploy + invoke',
|
||||
sectionNumber: 5,
|
||||
titleSize: 24,
|
||||
});
|
||||
|
||||
// Left: langgraph.json + deploy
|
||||
ds.helpers.addCodeBlock(slide, pres, theme, {
|
||||
x: 0.5, y: 1.45, w: 4.5, h: 2.55,
|
||||
language: 'bash',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'# 1. Конфиг: langgraph.json в корне репо',
|
||||
'cat > langgraph.json <<\'JSON\'',
|
||||
'{',
|
||||
' "graphs": {"agent": "./agent.py:graph"},',
|
||||
' "env": "./.env"',
|
||||
'}',
|
||||
'JSON',
|
||||
'',
|
||||
'# 2. Deploy one-shot',
|
||||
'langgraph deploy --name my-agent-prod',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
// Right: SDK invoke
|
||||
ds.helpers.addCodeBlock(slide, pres, theme, {
|
||||
x: 5.2, y: 1.45, w: 4.3, h: 2.55,
|
||||
language: 'python',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'from langgraph_sdk import get_client',
|
||||
'',
|
||||
'client = get_client(url=',
|
||||
' "https://my-agent-prod.us.langgraph.app"',
|
||||
')',
|
||||
'',
|
||||
'# async API: thread + run',
|
||||
'thread = await client.threads.create()',
|
||||
'run = await client.runs.create(',
|
||||
' thread["thread_id"], "agent", input={"q": "hi"}',
|
||||
')',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
// Bottom callout
|
||||
ds.helpers.addCallout(slide, pres, theme, {
|
||||
x: 0.5, y: 4.15, w: 9.0, h: 0.8,
|
||||
kind: 'info',
|
||||
title: 'LangGraph Platform',
|
||||
text: 'managed-хостинг для графа: build из langgraph.json, deploy через CLI, ' +
|
||||
'получаете HTTPS endpoint + Studio UI + threads + cron + webhooks.',
|
||||
});
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'langchain-ai.github.io/langgraph/concepts/langgraph_platform/',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 10);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,174 @@
|
||||
// Slide 11: Self-hosted vs Cloud -- pros/cons decision panel
|
||||
// Content slide: two cards with pros/cons.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Deployment',
|
||||
title: 'Self-hosted vs Cloud: что выбрать',
|
||||
sectionNumber: 5,
|
||||
});
|
||||
|
||||
// Lead paragraph
|
||||
slide.addText(
|
||||
'LangSmith и LangGraph Platform существуют в двух режимах: ' +
|
||||
'managed Cloud (быстрый старт, оплата по usage) и Self-Hosted (ваш K8s/VM, ' +
|
||||
'ваши данные остаются внутри периметра). Актуальная self-hosted версия -- v0.9.',
|
||||
{
|
||||
x: 0.5, y: 1.5, w: 9.0, h: 0.7,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
align: 'left', valign: 'top',
|
||||
margin: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// Two cards side by side
|
||||
const cards = [
|
||||
{
|
||||
x: 0.5,
|
||||
title: 'Cloud',
|
||||
sub: 'smith.langchain.com + managed platform',
|
||||
pros: [
|
||||
'Zero-setup: API key и заводите trace-ы',
|
||||
'Managed Postgres, Redis, scaling',
|
||||
'Studio UI включен в подписку',
|
||||
'Latest features сразу',
|
||||
],
|
||||
cons: [
|
||||
'Данные уходят в чужой VPC (US-region)',
|
||||
'Pay-per-trace: cost растет с нагрузкой',
|
||||
'Vendor lock на retention policy',
|
||||
],
|
||||
accentColor: 'primary',
|
||||
},
|
||||
{
|
||||
x: 5.1,
|
||||
title: 'Self-Hosted',
|
||||
sub: 'Docker/Helm chart, v0.9',
|
||||
pros: [
|
||||
'Данные внутри своего VPC/compliance',
|
||||
'Flat cost: предсказуемо для prod',
|
||||
'Полный контроль над retention',
|
||||
'Air-gapped окружения поддерживаются',
|
||||
],
|
||||
cons: [
|
||||
'Ops: K8s, Postgres, Redis, ClickHouse',
|
||||
'Обновления руками (breaking changes)',
|
||||
'Studio UI = отдельный пакет',
|
||||
'Не все beta-фичи доступны сразу',
|
||||
],
|
||||
accentColor: 'secondary',
|
||||
},
|
||||
];
|
||||
|
||||
const cardY = 2.35;
|
||||
const cardW = 4.4;
|
||||
const cardH = 2.6;
|
||||
cards.forEach((c) => {
|
||||
const accent = c.accentColor === 'primary'
|
||||
? theme.palette.accent.primary
|
||||
: theme.palette.accent.secondary;
|
||||
|
||||
slide.addShape(pres.ShapeType.roundRect, {
|
||||
x: c.x, y: cardY, w: cardW, h: cardH,
|
||||
fill: { color: theme.palette.bg.elevated },
|
||||
line: { color: theme.palette.border.subtle, width: 1 },
|
||||
rectRadius: 0.1,
|
||||
});
|
||||
// Left accent bar
|
||||
slide.addShape(pres.ShapeType.rect, {
|
||||
x: c.x, y: cardY, w: 0.08, h: cardH,
|
||||
fill: { color: accent },
|
||||
line: { type: 'none' },
|
||||
});
|
||||
|
||||
slide.addText(c.title, {
|
||||
x: c.x + 0.25, y: cardY + 0.15, w: cardW - 0.4, h: 0.4,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 22, bold: true,
|
||||
color: accent,
|
||||
valign: 'middle', margin: 0,
|
||||
});
|
||||
slide.addText(c.sub, {
|
||||
x: c.x + 0.25, y: cardY + 0.55, w: cardW - 0.4, h: 0.25,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10, italic: true,
|
||||
color: theme.palette.text.muted,
|
||||
valign: 'middle', margin: 0,
|
||||
});
|
||||
|
||||
// Pros
|
||||
slide.addText('+', {
|
||||
x: c.x + 0.25, y: cardY + 0.85, w: 0.25, h: 0.3,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.code),
|
||||
fontSize: 14, bold: true,
|
||||
color: theme.palette.state.success,
|
||||
valign: 'top', margin: 0,
|
||||
});
|
||||
const prosBody = c.pros.map((p) => ({
|
||||
text: p + '\n',
|
||||
options: {
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}));
|
||||
slide.addText(prosBody, {
|
||||
x: c.x + 0.5, y: cardY + 0.85, w: cardW - 0.65, h: 1.0,
|
||||
valign: 'top',
|
||||
paraSpaceAfter: 2,
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
// Cons
|
||||
slide.addText('-', {
|
||||
x: c.x + 0.25, y: cardY + 1.9, w: 0.25, h: 0.3,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.code),
|
||||
fontSize: 14, bold: true,
|
||||
color: theme.palette.state.danger,
|
||||
valign: 'top', margin: 0,
|
||||
});
|
||||
const consBody = c.cons.map((p) => ({
|
||||
text: p + '\n',
|
||||
options: {
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}));
|
||||
slide.addText(consBody, {
|
||||
x: c.x + 0.5, y: cardY + 1.9, w: cardW - 0.65, h: 0.7,
|
||||
valign: 'top',
|
||||
paraSpaceAfter: 2,
|
||||
margin: 0,
|
||||
});
|
||||
});
|
||||
|
||||
// Bottom meta strip
|
||||
slide.addText(
|
||||
'Рекомендация: Cloud для prototype и команд до 5 инженеров; ' +
|
||||
'self-hosted при compliance-требованиях и > 100M trace-ов в месяц.',
|
||||
{
|
||||
x: 0.5, y: 5.0, w: 9.0, h: 0.3,
|
||||
fontFace: ds.helpers.withFallback(theme.fonts.ui),
|
||||
fontSize: 10, italic: true,
|
||||
color: theme.palette.text.muted,
|
||||
align: 'left', valign: 'middle',
|
||||
margin: 0,
|
||||
}
|
||||
);
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'changelog.langchain.com/announcements/langsmith-self-hosted-v0-9',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 11);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||
@@ -0,0 +1,66 @@
|
||||
// Slide 12: TypeScript SDK + final ecosystem pros/cons
|
||||
// Two-column: code on left, pros/cons on right.
|
||||
|
||||
const ds = require('./design-system');
|
||||
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
ds.helpers.slideBase(slide, pres, theme);
|
||||
|
||||
ds.helpers.addHeader(slide, pres, theme, {
|
||||
eyebrow: 'STAGE 5: ECOSYSTEM',
|
||||
section: 'Итоги',
|
||||
title: 'TypeScript SDK и плюсы/минусы',
|
||||
sectionNumber: 5,
|
||||
titleSize: 24,
|
||||
});
|
||||
|
||||
// Left code block -- TypeScript SDK
|
||||
ds.helpers.addCodeBlock(slide, pres, theme, {
|
||||
x: 0.5, y: 1.45, w: 5.5, h: 3.1,
|
||||
language: 'typescript',
|
||||
fontSize: 10,
|
||||
code: [
|
||||
'import { Client, traceable } from "langsmith";',
|
||||
'',
|
||||
'const client = new Client();',
|
||||
'',
|
||||
'// @traceable decorator -- точно как в Python',
|
||||
'const retrieve = traceable(',
|
||||
' async function retrieve(query: string) {',
|
||||
' return vectorStore.similaritySearch(query, 4);',
|
||||
' },',
|
||||
' { name: "retrieve_docs", runType: "retriever" }',
|
||||
');',
|
||||
'',
|
||||
'// run_on_dataset для eval -- тоже есть',
|
||||
'await client.runOnDataset(',
|
||||
' "rag-qa-eval-v1", target,',
|
||||
' { evaluators: [answerMatch] }',
|
||||
');',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
// Right pros/cons panel
|
||||
ds.helpers.addProsCons(slide, pres, theme, {
|
||||
x: 6.2, y: 1.45, w: 3.3, h: 3.1,
|
||||
pros: [
|
||||
'Auto-tracing из коробки',
|
||||
'Eval = CI/CD для промптов',
|
||||
'Platform = deploy за минуты',
|
||||
'Self-hosted опция',
|
||||
],
|
||||
cons: [
|
||||
'LangSmith SDK 0.x',
|
||||
'Cloud растет в цене',
|
||||
'Self-hosted требует K8s',
|
||||
],
|
||||
});
|
||||
|
||||
ds.helpers.addSourceLine(slide, pres, theme, {
|
||||
source: 'github.com/langchain-ai/langsmith-sdk + langgraph-sdk README',
|
||||
});
|
||||
ds.helpers.addPageNumber(slide, pres, theme, 12);
|
||||
}
|
||||
|
||||
module.exports = { createSlide };
|
||||