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
This commit is contained in:
2026-06-22 11:29:03 +03:00
parent ed5b5c91bf
commit 75601988c2
220 changed files with 13069 additions and 3 deletions
+256
View File
@@ -0,0 +1,256 @@
"""
final-merge.py
Склеивает 5 секционных PPTX в один + cover/TOC/итоги.
Использует python-pptx и прямые XML-манипуляции.
"""
import copy
import os
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.shapes import MSO_SHAPE
from pptx.dml.color import RGBColor
WORKSPACE = "/Users/alexandr/.mavis/plans/plan_85053139/workspace/lc-evo-deck"
SECTIONS = [
("section1-chains", "LangChain 1.0: chains, LCEL, agents, retrievers", 27),
("section2-langgraph", "LangGraph 1.0: state, nodes, persistence, HITL", 33),
("section3-deepagents", "Deep Agents: harness, todos, virtual FS, subagents", 26),
("section4-openswe", "Open SWE: async coding agent, triggers, dashboard", 24),
("section5-ecosystem", "Ecosystem: LangSmith, Studio, deployment", 12),
]
# Theme colors (from design-system.js)
BG_PRIMARY = RGBColor(0x0A, 0x1A, 0x2A)
BG_ELEVATED = RGBColor(0x14, 0x2B, 0x3F)
TEXT_PRIMARY = RGBColor(0xE6, 0xF0, 0xF7)
TEXT_SECONDARY = RGBColor(0xB5, 0xC4, 0xD1)
TEXT_MUTED = RGBColor(0x8A, 0x9A, 0xAB)
ACCENT_TEAL = RGBColor(0x21, 0x9E, 0xBC)
ACCENT_GOLD = RGBColor(0xFF, 0xB7, 0x03)
ACCENT_BLUE = RGBColor(0x8E, 0xCA, 0xE6)
BORDER_SUBTLE = RGBColor(0x23, 0x3A, 0x4F)
def add_dark_background(slide):
"""Fill slide background with dark navy."""
bg = slide.background
fill = bg.fill
fill.solid()
fill.fore_color.rgb = BG_PRIMARY
def add_text(slide, x, y, w, h, text, *, size=18, bold=False, color=TEXT_PRIMARY, align=None, font="Inter"):
tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
tf = tb.text_frame
tf.word_wrap = True
tf.margin_left = Inches(0.05)
tf.margin_right = Inches(0.05)
tf.margin_top = Inches(0.02)
tf.margin_bottom = Inches(0.02)
p = tf.paragraphs[0]
if align == "center":
from pptx.enum.text import PP_ALIGN
p.alignment = PP_ALIGN.CENTER
elif align == "right":
from pptx.enum.text import PP_ALIGN
p.alignment = PP_ALIGN.RIGHT
run = p.add_run()
run.text = text
run.font.size = Pt(size)
run.font.bold = bold
run.font.name = font
run.font.color.rgb = color
return tb
def add_rect(slide, x, y, w, h, fill, line=None, line_width=0.75):
shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h))
shape.fill.solid()
shape.fill.fore_color.rgb = fill
if line is None:
shape.line.fill.background()
else:
shape.line.color.rgb = line
shape.line.width = Pt(line_width)
return shape
def add_rounded_rect(slide, x, y, w, h, fill, line=None, line_width=0.75):
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h))
shape.fill.solid()
shape.fill.fore_color.rgb = fill
if line is None:
shape.line.fill.background()
else:
shape.line.color.rgb = line
shape.line.width = Pt(line_width)
return shape
def make_cover_slide(prs):
"""Cover slide."""
slide = prs.slides.add_slide(blank_layout_global)
add_dark_background(slide)
# Top accent bar
add_rect(slide, 0, 0, 10, 0.15, ACCENT_TEAL)
# Eyebrow
add_text(slide, 0.5, 0.6, 9, 0.4, "DEEP DIVE / TUTORIAL", size=14, bold=True, color=ACCENT_TEAL)
# Main title
add_text(slide, 0.5, 1.3, 9, 1.2, "Эволюция LangChain", size=44, bold=True, color=TEXT_PRIMARY)
# Subtitle
add_text(slide, 0.5, 2.5, 9, 0.7, "от chains до Deep Agents и Open SWE", size=28, color=TEXT_SECONDARY)
# Decorative line
add_rect(slide, 0.5, 3.4, 1.5, 0.06, ACCENT_GOLD)
# Description
add_text(slide, 0.5, 3.7, 9, 1.3,
"Большой tutorial по экосистеме LangChain: chains, LCEL, "
"LangGraph, Deep Agents, Open SWE. Python ≥ 1.0, реальные API, "
"плотный код.",
size=18, color=TEXT_SECONDARY)
# Bottom metadata
add_text(slide, 0.5, 5.05, 9, 0.4, "100+ слайдов | 2022 -- 2026 | Python 1.0+", size=12, color=TEXT_MUTED, align="center")
def make_toc_slide(prs, sections):
"""Table of contents slide."""
slide = prs.slides.add_slide(blank_layout_global)
add_dark_background(slide)
add_text(slide, 0.5, 0.4, 9, 0.5, "Содержание", size=36, bold=True, color=TEXT_PRIMARY)
add_rect(slide, 0.5, 1.0, 1.2, 0.05, ACCENT_TEAL)
y = 1.4
total = 0
for i, (sid, title, count) in enumerate(sections, 1):
# Numbered card
add_rounded_rect(slide, 0.5, y, 9, 0.7, BG_ELEVATED, line=BORDER_SUBTLE, line_width=0.5)
# Number badge
add_rounded_rect(slide, 0.7, y + 0.1, 0.5, 0.5, ACCENT_TEAL)
add_text(slide, 0.7, y + 0.13, 0.5, 0.4, str(i), size=22, bold=True, color=BG_PRIMARY, align="center")
# Title
add_text(slide, 1.4, y + 0.05, 6, 0.35, title, size=18, bold=True, color=TEXT_PRIMARY)
# Subtitle / count
add_text(slide, 1.4, y + 0.38, 6, 0.3, f"{count} слайдов", size=12, color=TEXT_MUTED)
# Page range placeholder (will be filled after merge)
add_text(slide, 8.3, y + 0.18, 1.1, 0.4, f"~{count} сл.", size=14, color=ACCENT_BLUE, align="right")
total += count
y += 0.85
# Total
add_text(slide, 0.5, y + 0.1, 9, 0.4, f"Всего: {total} слайдов + cover, TOC, итоги = {total + 3}", size=14, color=ACCENT_GOLD)
def make_summary_slide(prs, total, section_counts):
"""Final summary / takeaways slide."""
slide = prs.slides.add_slide(blank_layout_global)
add_dark_background(slide)
add_text(slide, 0.5, 0.4, 9, 0.5, "Итоги", size=36, bold=True, color=TEXT_PRIMARY)
add_rect(slide, 0.5, 1.0, 1.2, 0.05, ACCENT_GOLD)
add_text(slide, 0.5, 1.3, 9, 0.6,
f"Всего {total} слайдов: от chains 2022 до Open SWE 2025/2026.",
size=18, color=TEXT_SECONDARY)
# Takeaways list
y = 2.1
add_text(slide, 0.5, y, 9, 0.4, "Что мы разобрали:", size=20, bold=True, color=ACCENT_TEAL)
y += 0.6
items = [
"LangChain 1.0: chains, LCEL, agents, retrievers -- ядро фреймворка",
"LangGraph 1.0: stateful графы, persistence, HITL, streaming",
"Deep Agents: harness, todos, virtual FS, subagents -- 'out of the box' reasoning",
"Open SWE: async coding agent с триггерами и дашбордом",
"LangSmith + LangGraph Studio: observability и локальная разработка",
]
for item in items:
add_text(slide, 0.7, y, 9, 0.4, "- " + item, size=14, color=TEXT_PRIMARY)
y += 0.45
# Final call-out
y += 0.2
add_rounded_rect(slide, 0.5, y, 9, 0.7, BG_ELEVATED, line=ACCENT_GOLD, line_width=1.5)
add_text(slide, 0.7, y + 0.1, 8.6, 0.5,
"Главный тренд: от chain-of-prompts к stateful агентам с harness и human-in-the-loop.",
size=15, bold=True, color=ACCENT_GOLD)
# Footer
add_text(slide, 0.5, 5.25, 9, 0.3,
"Mavis / 2026 / Python 1.0+ / pre-compile lint passed (no em-dash / smart quotes)",
size=10, color=TEXT_MUTED, align="center")
def copy_slide_from_to(src_prs, src_idx, dst_prs):
"""Copy slide at src_idx from src_prs to dst_prs, preserving shapes/formatting via XML."""
src_slide = src_prs.slides[src_idx]
# Use blank layout of dest
dst_slide = dst_prs.slides.add_slide(blank_layout_global)
# Copy background if present
if src_slide.background and src_slide.background.fill.type is not None:
try:
dst_slide.background.fill.solid()
# Don't override -- just let shapes drive background
except Exception:
pass
# Copy all shapes via deep XML clone
for shape in src_slide.shapes:
el = shape.element
new_el = copy.deepcopy(el)
dst_slide.shapes._spTree.insert_element_before(new_el, "p:extLst")
# Copy slide notes if any
if src_slide.has_notes_slide:
try:
notes_text = src_slide.notes_slide.notes_text_frame.text
if notes_text.strip():
dst_slide.notes_slide.notes_text_frame.text = notes_text
except Exception:
pass
def main():
out_path = os.path.join(WORKSPACE, "output", "langchain-evolution.pptx")
os.makedirs(os.path.dirname(out_path), exist_ok=True)
# Start from scratch with 16:9
from pptx.util import Emu
prs = Presentation()
prs.slide_width = Inches(10)
prs.slide_height = Inches(5.625)
blank_layout = prs.slide_layouts[6] if len(prs.slide_layouts) > 6 else prs.slide_layouts[-1]
print(f"[merge] using layout index {prs.slide_layouts.index(blank_layout)} (total: {len(prs.slide_layouts)})")
global blank_layout_global
blank_layout_global = blank_layout
# Add cover
print("[merge] adding cover...")
make_cover_slide(prs)
# Add TOC
print("[merge] adding TOC...")
make_toc_slide(prs, SECTIONS)
# Copy slides from each section
total = 0
for sid, title, expected in SECTIONS:
sec_path = os.path.join(WORKSPACE, "slides", sid, f"{sid.replace('section', 'section')}.pptx")
# Actually file is sectionN.pptx inside section<num>-<name>/
pptx_name = f"{sid.split('-')[0]}.pptx" # e.g. "section1.pptx"
sec_path = os.path.join(WORKSPACE, "slides", sid, pptx_name)
if not os.path.exists(sec_path):
print(f"[merge] MISSING: {sec_path}")
continue
print(f"[merge] merging {sid} from {sec_path}")
sec_prs = Presentation(sec_path)
actual = len(sec_prs.slides)
for i in range(actual):
copy_slide_from_to(sec_prs, i, prs)
total += actual
print(f"[merge] copied {actual} slides (expected {expected})")
# Add summary slide
print(f"[merge] adding summary (total so far: {total + 3})")
make_summary_slide(prs, total + 2, [s[2] for s in SECTIONS])
prs.save(out_path)
print(f"[merge] saved {out_path}")
print(f"[merge] final slide count: {len(prs.slides)}")
if __name__ == "__main__":
main()