|
| 1 | +from pptx import Presentation |
| 2 | +from pptx.util import Pt, Emu |
| 3 | +from pptx.enum.text import PP_ALIGN, MSO_ANCHOR |
| 4 | +from pptx.dml.color import RGBColor |
| 5 | +from pptx.enum.shapes import MSO_SHAPE |
| 6 | +from pptx.oxml.ns import qn |
| 7 | + |
| 8 | +prs = Presentation('C:/source/slack-plus-teams/docs/slack-plus-teams-architecture.pptx') |
| 9 | + |
| 10 | +# Theme colors from slides 1 and 2 — no platform-specific colors |
| 11 | +INDIGO = RGBColor(0x4B, 0x4A, 0x78) |
| 12 | +WHITE = RGBColor(0xFF, 0xFF, 0xFF) |
| 13 | +GRAY_TEXT = RGBColor(0x6D, 0x6D, 0x6D) |
| 14 | +LIGHT_GRAY = RGBColor(0x7A, 0x7A, 0x7A) |
| 15 | +GREEN = RGBColor(0x2E, 0x7D, 0x32) |
| 16 | +LIGHT_INDIGO_BG = RGBColor(0xE8, 0xE8, 0xF0) # light indigo for adapter bg |
| 17 | +LIGHT_GREEN_BG = RGBColor(0xE8, 0xF5, 0xE9) # light green for shared layer bg |
| 18 | +ARROW_COLOR = INDIGO |
| 19 | + |
| 20 | +slide_layout = prs.slide_layouts[0] |
| 21 | +slide = prs.slides.add_slide(slide_layout) |
| 22 | + |
| 23 | + |
| 24 | +def add_rect(left, top, width, height, fill_color=None): |
| 25 | + shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height) |
| 26 | + shape.line.fill.background() |
| 27 | + if fill_color: |
| 28 | + shape.fill.solid() |
| 29 | + shape.fill.fore_color.rgb = fill_color |
| 30 | + else: |
| 31 | + shape.fill.background() |
| 32 | + tf = shape.text_frame |
| 33 | + tf.word_wrap = True |
| 34 | + tf.auto_size = None |
| 35 | + tf.margin_left = Emu(91440) |
| 36 | + tf.margin_right = Emu(91440) |
| 37 | + tf.margin_top = Emu(45720) |
| 38 | + tf.margin_bottom = Emu(45720) |
| 39 | + return shape, tf |
| 40 | + |
| 41 | + |
| 42 | +def add_rounded(left, top, width, height, fill_color=None, radius=5000, |
| 43 | + border_color=None, border_width=None): |
| 44 | + shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height) |
| 45 | + if border_color: |
| 46 | + shape.line.color.rgb = border_color |
| 47 | + shape.line.width = border_width or Pt(1.5) |
| 48 | + else: |
| 49 | + shape.line.fill.background() |
| 50 | + spPr = shape._element.spPr |
| 51 | + prstGeom = spPr.find(qn('a:prstGeom')) |
| 52 | + if prstGeom is not None: |
| 53 | + avLst = prstGeom.find(qn('a:avLst')) |
| 54 | + if avLst is None: |
| 55 | + from lxml import etree |
| 56 | + avLst = etree.SubElement(prstGeom, qn('a:avLst')) |
| 57 | + else: |
| 58 | + for child in list(avLst): |
| 59 | + avLst.remove(child) |
| 60 | + from lxml import etree |
| 61 | + gd = etree.SubElement(avLst, qn('a:gd')) |
| 62 | + gd.set('name', 'adj') |
| 63 | + gd.set('fmla', f'val {radius}') |
| 64 | + if fill_color: |
| 65 | + shape.fill.solid() |
| 66 | + shape.fill.fore_color.rgb = fill_color |
| 67 | + else: |
| 68 | + shape.fill.background() |
| 69 | + tf = shape.text_frame |
| 70 | + tf.word_wrap = True |
| 71 | + tf.auto_size = None |
| 72 | + tf.margin_left = Emu(91440) |
| 73 | + tf.margin_right = Emu(91440) |
| 74 | + tf.margin_top = Emu(45720) |
| 75 | + tf.margin_bottom = Emu(45720) |
| 76 | + return shape, tf |
| 77 | + |
| 78 | + |
| 79 | +def set_text(tf, text, size, color, bold=False, align=PP_ALIGN.LEFT): |
| 80 | + p = tf.paragraphs[0] |
| 81 | + p.text = text |
| 82 | + p.alignment = align |
| 83 | + p.runs[0].font.size = size |
| 84 | + p.runs[0].font.color.rgb = color |
| 85 | + p.runs[0].font.bold = bold |
| 86 | + |
| 87 | + |
| 88 | +def add_box(left, top, width, height, title, subtitle_lines, |
| 89 | + fill_color, text_color, border_color=None): |
| 90 | + """Add a labeled box with title and subtitle lines.""" |
| 91 | + shape, tf = add_rounded(left, top, width, height, fill_color=fill_color, |
| 92 | + radius=4000, border_color=border_color, |
| 93 | + border_width=Pt(1.5)) |
| 94 | + tf.margin_top = Emu(36000) |
| 95 | + tf.margin_bottom = Emu(36000) |
| 96 | + p = tf.paragraphs[0] |
| 97 | + p.alignment = PP_ALIGN.CENTER |
| 98 | + run = p.add_run() |
| 99 | + run.text = title |
| 100 | + run.font.size = Pt(13) |
| 101 | + run.font.bold = True |
| 102 | + run.font.color.rgb = text_color |
| 103 | + for line in subtitle_lines: |
| 104 | + p2 = tf.add_paragraph() |
| 105 | + p2.alignment = PP_ALIGN.CENTER |
| 106 | + r2 = p2.add_run() |
| 107 | + r2.text = line |
| 108 | + r2.font.size = Pt(9.5) |
| 109 | + r2.font.color.rgb = text_color |
| 110 | + r2.font.bold = False |
| 111 | + return shape |
| 112 | + |
| 113 | + |
| 114 | +def add_arrow(x1, y1, x2, y2, color=ARROW_COLOR, width=Pt(2), head=True, tail=False): |
| 115 | + """Add an arrow. head=arrowhead at end, tail=arrowhead at start.""" |
| 116 | + connector = slide.shapes.add_connector( |
| 117 | + 1, Emu(x1), Emu(y1), Emu(x2), Emu(y2) |
| 118 | + ) |
| 119 | + connector.line.color.rgb = color |
| 120 | + connector.line.width = width |
| 121 | + connector.line.fill.solid() |
| 122 | + connector.line.color.rgb = color |
| 123 | + ln = connector._element.find(qn('a:ln')) |
| 124 | + if ln is None: |
| 125 | + spPr = connector._element.spPr |
| 126 | + ln = spPr.find(qn('a:ln')) |
| 127 | + if ln is not None: |
| 128 | + from lxml import etree |
| 129 | + if head: |
| 130 | + t = etree.SubElement(ln, qn('a:tailEnd')) |
| 131 | + t.set('type', 'triangle') |
| 132 | + t.set('w', 'med') |
| 133 | + t.set('len', 'med') |
| 134 | + if tail: |
| 135 | + h = etree.SubElement(ln, qn('a:headEnd')) |
| 136 | + h.set('type', 'triangle') |
| 137 | + h.set('w', 'med') |
| 138 | + h.set('len', 'med') |
| 139 | + return connector |
| 140 | + |
| 141 | + |
| 142 | +# --- Title banner (squared corners, matching slide 2) --- |
| 143 | +shape, tf = add_rect(Emu(109728), Emu(411480), Emu(4500000), Emu(713232), fill_color=INDIGO) |
| 144 | +p = tf.paragraphs[0] |
| 145 | +run = p.add_run() |
| 146 | +run.text = "Adapter Pattern Architecture" |
| 147 | +run.font.size = Pt(22) |
| 148 | +run.font.bold = True |
| 149 | +run.font.color.rgb = WHITE |
| 150 | + |
| 151 | +# Subtitle |
| 152 | +shape, tf = add_rect(Emu(4800000), Emu(493776), Emu(7200000), Emu(219456)) |
| 153 | +set_text(tf, "Inbound calls from each platform converge on a shared service layer", |
| 154 | + Pt(13.5), GRAY_TEXT) |
| 155 | + |
| 156 | + |
| 157 | +# === Layout === |
| 158 | +# |
| 159 | +# [Slack] --> [Slack Adapter] --> [ Shared Service ] <-- [Teams Adapter] <-- [Teams] |
| 160 | +# [ Layer ] |
| 161 | +# |
| 162 | +# Both platforms send calls INWARD to the central shared layer. |
| 163 | +# The shared layer is the hub; adapters translate platform-specific protocols. |
| 164 | + |
| 165 | +slide_w = 12192000 |
| 166 | +center_x = slide_w // 2 |
| 167 | +center_y = 3700000 # slightly below center to account for title |
| 168 | + |
| 169 | +# Shared Service Layer — center, prominent |
| 170 | +shared_w = 2600000 |
| 171 | +shared_h = 2000000 |
| 172 | +shared_left = center_x - shared_w // 2 |
| 173 | +shared_top = center_y - shared_h // 2 |
| 174 | + |
| 175 | +# Adapter boxes |
| 176 | +adapter_w = 1800000 |
| 177 | +adapter_h = 1400000 |
| 178 | +adapter_gap = 400000 # gap between adapter and shared layer |
| 179 | + |
| 180 | +slack_adapter_left = shared_left - adapter_gap - adapter_w |
| 181 | +slack_adapter_top = center_y - adapter_h // 2 |
| 182 | + |
| 183 | +teams_adapter_left = shared_left + shared_w + adapter_gap |
| 184 | +teams_adapter_top = center_y - adapter_h // 2 |
| 185 | + |
| 186 | +# Platform boxes (outer) |
| 187 | +platform_w = 1500000 |
| 188 | +platform_h = 1000000 |
| 189 | +platform_gap = 350000 |
| 190 | + |
| 191 | +slack_left = slack_adapter_left - platform_gap - platform_w |
| 192 | +slack_top = center_y - platform_h // 2 |
| 193 | + |
| 194 | +teams_left = teams_adapter_left + adapter_w + platform_gap |
| 195 | +teams_top = center_y - platform_h // 2 |
| 196 | + |
| 197 | + |
| 198 | +# --- Draw boxes --- |
| 199 | + |
| 200 | +# Shared Service Layer (center, green theme) |
| 201 | +add_box(shared_left, shared_top, shared_w, shared_h, |
| 202 | + "Shared Service Layer", |
| 203 | + ["Business logic", "AI / LLM calls", "Data access", "Common types"], |
| 204 | + fill_color=LIGHT_GREEN_BG, text_color=GREEN, |
| 205 | + border_color=GREEN) |
| 206 | + |
| 207 | +# Slack Adapter (left of center) |
| 208 | +add_box(slack_adapter_left, slack_adapter_top, adapter_w, adapter_h, |
| 209 | + "Slack Adapter", |
| 210 | + ["@slack/bolt", "Event handlers", "Block Kit"], |
| 211 | + fill_color=LIGHT_INDIGO_BG, text_color=INDIGO, |
| 212 | + border_color=INDIGO) |
| 213 | + |
| 214 | +# Teams Adapter (right of center) |
| 215 | +add_box(teams_adapter_left, teams_adapter_top, adapter_w, adapter_h, |
| 216 | + "Teams Adapter", |
| 217 | + ["Agents SDK", "Activity handlers", "Adaptive Cards"], |
| 218 | + fill_color=LIGHT_INDIGO_BG, text_color=INDIGO, |
| 219 | + border_color=INDIGO) |
| 220 | + |
| 221 | +# Slack Platform (far left) |
| 222 | +add_box(slack_left, slack_top, platform_w, platform_h, |
| 223 | + "Slack", |
| 224 | + ["WebSocket", "Socket Mode"], |
| 225 | + fill_color=INDIGO, text_color=WHITE) |
| 226 | + |
| 227 | +# Teams Platform (far right) |
| 228 | +add_box(teams_left, teams_top, platform_w, platform_h, |
| 229 | + "Microsoft Teams", |
| 230 | + ["HTTPS", "Bot Framework"], |
| 231 | + fill_color=INDIGO, text_color=WHITE) |
| 232 | + |
| 233 | + |
| 234 | +# --- Arrows: both sides point INWARD to the shared layer --- |
| 235 | +arrow_y_top = center_y - 150000 # upper arrow (inbound) |
| 236 | +arrow_y_bot = center_y + 150000 # lower arrow (outbound/response) |
| 237 | + |
| 238 | +# LEFT SIDE: Slack → Slack Adapter → Shared Layer |
| 239 | + |
| 240 | +# Slack → Slack Adapter (inbound) |
| 241 | +add_arrow(slack_left + platform_w, arrow_y_top, |
| 242 | + slack_adapter_left, arrow_y_top) |
| 243 | + |
| 244 | +# Slack Adapter → Shared Layer (inbound) |
| 245 | +add_arrow(slack_adapter_left + adapter_w, arrow_y_top, |
| 246 | + shared_left, arrow_y_top) |
| 247 | + |
| 248 | +# Shared Layer → Slack Adapter (response) |
| 249 | +add_arrow(shared_left, arrow_y_bot, |
| 250 | + slack_adapter_left + adapter_w, arrow_y_bot) |
| 251 | + |
| 252 | +# Slack Adapter → Slack (response) |
| 253 | +add_arrow(slack_adapter_left, arrow_y_bot, |
| 254 | + slack_left + platform_w, arrow_y_bot) |
| 255 | + |
| 256 | + |
| 257 | +# RIGHT SIDE: Teams → Teams Adapter → Shared Layer |
| 258 | + |
| 259 | +# Teams → Teams Adapter (inbound) |
| 260 | +add_arrow(teams_left, arrow_y_top, |
| 261 | + teams_adapter_left + adapter_w, arrow_y_top) |
| 262 | + |
| 263 | +# Teams Adapter → Shared Layer (inbound) |
| 264 | +add_arrow(teams_adapter_left, arrow_y_top, |
| 265 | + shared_left + shared_w, arrow_y_top) |
| 266 | + |
| 267 | +# Shared Layer → Teams Adapter (response) |
| 268 | +add_arrow(shared_left + shared_w, arrow_y_bot, |
| 269 | + teams_adapter_left, arrow_y_bot) |
| 270 | + |
| 271 | +# Teams Adapter → Teams (response) |
| 272 | +add_arrow(teams_adapter_left + adapter_w, arrow_y_bot, |
| 273 | + teams_left, arrow_y_bot) |
| 274 | + |
| 275 | + |
| 276 | +# --- Arrow labels --- |
| 277 | +def add_label(cx, cy, text): |
| 278 | + w = 1400000 |
| 279 | + h = 220000 |
| 280 | + shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, |
| 281 | + Emu(cx - w // 2), Emu(cy), |
| 282 | + Emu(w), Emu(h)) |
| 283 | + shape.fill.background() |
| 284 | + shape.line.fill.background() |
| 285 | + tf = shape.text_frame |
| 286 | + tf.word_wrap = True |
| 287 | + p = tf.paragraphs[0] |
| 288 | + p.alignment = PP_ALIGN.CENTER |
| 289 | + p.text = text |
| 290 | + p.runs[0].font.size = Pt(8.5) |
| 291 | + p.runs[0].font.color.rgb = LIGHT_GRAY |
| 292 | + p.runs[0].font.italic = True |
| 293 | + |
| 294 | + |
| 295 | +# Labels above the inbound arrows |
| 296 | +label_y = center_y - shared_h // 2 - 280000 |
| 297 | + |
| 298 | +# Left side labels |
| 299 | +slack_mid = (slack_left + platform_w + slack_adapter_left) // 2 |
| 300 | +add_label(slack_mid, label_y, "Events / Messages") |
| 301 | + |
| 302 | +slack_adapter_mid = (slack_adapter_left + adapter_w + shared_left) // 2 |
| 303 | +add_label(slack_adapter_mid, label_y, "Normalized Calls") |
| 304 | + |
| 305 | +# Right side labels |
| 306 | +teams_mid = (teams_left + teams_adapter_left + adapter_w) // 2 |
| 307 | +add_label(teams_mid, label_y, "Activities / Messages") |
| 308 | + |
| 309 | +teams_adapter_mid = (teams_adapter_left + shared_left + shared_w) // 2 |
| 310 | +add_label(teams_adapter_mid, label_y, "Normalized Calls") |
| 311 | + |
| 312 | + |
| 313 | +# --- Footer --- |
| 314 | +shape, tf = add_rect(Emu(219456), Emu(6455664), Emu(6035040), Emu(128016)) |
| 315 | +set_text(tf, "Slack to Teams migration guide \u2022 architecture diagram 4 of 7", |
| 316 | + Pt(9.5), LIGHT_GRAY) |
| 317 | + |
| 318 | +prs.save('C:/source/slack-plus-teams/docs/slack-plus-teams-architecture-v3.pptx') |
| 319 | +print("Slide 4 (Adapter Pattern Architecture) added successfully!") |
0 commit comments