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:
+256
@@ -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()
|
||||
Reference in New Issue
Block a user