75601988c2
- 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
257 lines
10 KiB
Python
257 lines
10 KiB
Python
"""
|
||
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()
|