Files
langchain-evolution-deck/final-merge.py
T
petya 75601988c2 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
2026-06-22 11:29:03 +03:00

257 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()