Create a comprehensive 25-slide PowerPoint presentation showcasing all Mod Comms features, including multi-agent AI system, campaign management, real-time analysis, feedback reports, knowledge base, analytics, auditing, user roles, and technical architecture. Includes a Python generator script for reproducible builds and a companion features markdown document. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1470 lines
57 KiB
Python
1470 lines
57 KiB
Python
"""
|
|
Mod Comms — PowerPoint Presentation Generator
|
|
Generates a professional 16:9 widescreen presentation showcasing all features.
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from pptx import Presentation
|
|
from pptx.util import Inches, Pt, Emu
|
|
from pptx.dml.color import RGBColor
|
|
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
|
from pptx.enum.shapes import MSO_SHAPE
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Paths
|
|
# ---------------------------------------------------------------------------
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
LOGO_PATH = PROJECT_ROOT / "UI_guidance" / "Barclays-Modcomms.png"
|
|
OUTPUT_PPTX = SCRIPT_DIR / "ModComms_Presentation.pptx"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Design tokens
|
|
# ---------------------------------------------------------------------------
|
|
DARK_NAVY = RGBColor(0x1A, 0x21, 0x42)
|
|
ACTIVE_BLUE = RGBColor(0x00, 0x6D, 0xE3)
|
|
ELECTRIC_VIOLET = RGBColor(0x7A, 0x0F, 0xF9)
|
|
LIME = RGBColor(0xC3, 0xFB, 0x5A)
|
|
TEAL = RGBColor(0x01, 0xA1, 0xA2)
|
|
CYAN_BRAND = RGBColor(0x00, 0xAE, 0xEF)
|
|
|
|
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
|
BLACK_TITLE = RGBColor(0x27, 0x27, 0x27)
|
|
GREY_700 = RGBColor(0x8E, 0x8E, 0x8E)
|
|
GREY_300 = RGBColor(0xE2, 0xE2, 0xE2)
|
|
GREY_100 = RGBColor(0xF6, 0xF6, 0xF6)
|
|
|
|
RAG_GREEN = RGBColor(0x09, 0x82, 0x1F)
|
|
RAG_AMBER = RGBColor(0xFF, 0xBA, 0x00)
|
|
RAG_RED = RGBColor(0xE3, 0x00, 0x0F)
|
|
|
|
FONT_NAME = "Arial"
|
|
|
|
# Slide dimensions (16:9)
|
|
SLIDE_WIDTH = Inches(13.333)
|
|
SLIDE_HEIGHT = Inches(7.5)
|
|
|
|
# Margins
|
|
MARGIN_LEFT = Inches(0.8)
|
|
MARGIN_RIGHT = Inches(0.8)
|
|
MARGIN_TOP = Inches(0.6)
|
|
CONTENT_WIDTH = SLIDE_WIDTH - MARGIN_LEFT - MARGIN_RIGHT
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def set_slide_bg(slide, color):
|
|
"""Set a solid background colour for a slide."""
|
|
bg = slide.background
|
|
fill = bg.fill
|
|
fill.solid()
|
|
fill.fore_color.rgb = color
|
|
|
|
|
|
def add_textbox(slide, left, top, width, height, text, font_size=18,
|
|
color=BLACK_TITLE, bold=False, alignment=PP_ALIGN.LEFT,
|
|
font_name=FONT_NAME, line_spacing=1.2):
|
|
"""Add a simple single-run text box and return the shape."""
|
|
txBox = slide.shapes.add_textbox(left, top, width, height)
|
|
tf = txBox.text_frame
|
|
tf.word_wrap = True
|
|
p = tf.paragraphs[0]
|
|
p.text = text
|
|
p.font.size = Pt(font_size)
|
|
p.font.color.rgb = color
|
|
p.font.bold = bold
|
|
p.font.name = font_name
|
|
p.alignment = alignment
|
|
p.space_after = Pt(0)
|
|
p.space_before = Pt(0)
|
|
if line_spacing != 1.0:
|
|
p.line_spacing = Pt(font_size * line_spacing)
|
|
return txBox
|
|
|
|
|
|
def add_bullet_list(slide, left, top, width, height, items, font_size=14,
|
|
color=BLACK_TITLE, bullet_color=ACTIVE_BLUE, line_spacing=1.5):
|
|
"""Add a bulleted list text box. Each item is a separate paragraph."""
|
|
txBox = slide.shapes.add_textbox(left, top, width, height)
|
|
tf = txBox.text_frame
|
|
tf.word_wrap = True
|
|
|
|
for i, item in enumerate(items):
|
|
if i == 0:
|
|
p = tf.paragraphs[0]
|
|
else:
|
|
p = tf.add_paragraph()
|
|
p.text = item
|
|
p.font.size = Pt(font_size)
|
|
p.font.color.rgb = color
|
|
p.font.name = FONT_NAME
|
|
p.space_after = Pt(4)
|
|
p.space_before = Pt(2)
|
|
p.line_spacing = Pt(font_size * line_spacing)
|
|
# Bullet
|
|
pPr = p._pPr
|
|
if pPr is None:
|
|
from pptx.oxml.ns import qn
|
|
pPr = p._p.get_or_add_pPr()
|
|
from pptx.oxml.ns import qn
|
|
buNone = pPr.find(qn("a:buNone"))
|
|
if buNone is not None:
|
|
pPr.remove(buNone)
|
|
buChar = pPr.makeelement(qn("a:buChar"), {"char": "\u2022"})
|
|
pPr.append(buChar)
|
|
buClr = pPr.makeelement(qn("a:buClr"), {})
|
|
srgbClr = buClr.makeelement(qn("a:srgbClr"), {"val": f"{bullet_color}"})
|
|
buClr.append(srgbClr)
|
|
pPr.append(buClr)
|
|
buSzPct = pPr.makeelement(qn("a:buSzPct"), {"val": "120000"})
|
|
pPr.append(buSzPct)
|
|
# Indent
|
|
pPr.set("marL", str(Emu(Inches(0.3))))
|
|
pPr.set("indent", str(Emu(Inches(-0.25))))
|
|
|
|
return txBox
|
|
|
|
|
|
def add_rounded_rect(slide, left, top, width, height, fill_color, text="",
|
|
font_size=12, font_color=WHITE, bold=False, line_color=None):
|
|
"""Add a rounded rectangle shape with optional text."""
|
|
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height)
|
|
shape.fill.solid()
|
|
shape.fill.fore_color.rgb = fill_color
|
|
if line_color:
|
|
shape.line.color.rgb = line_color
|
|
shape.line.width = Pt(1.5)
|
|
else:
|
|
shape.line.fill.background()
|
|
# Smaller corner radius
|
|
shape.adjustments[0] = 0.1
|
|
if text:
|
|
tf = shape.text_frame
|
|
tf.word_wrap = True
|
|
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
|
|
p = tf.paragraphs[0]
|
|
p.text = text
|
|
p.font.size = Pt(font_size)
|
|
p.font.color.rgb = font_color
|
|
p.font.bold = bold
|
|
p.font.name = FONT_NAME
|
|
tf.margin_left = Pt(6)
|
|
tf.margin_right = Pt(6)
|
|
tf.margin_top = Pt(4)
|
|
tf.margin_bottom = Pt(4)
|
|
return shape
|
|
|
|
|
|
def add_circle(slide, left, top, diameter, fill_color, text="", font_size=11,
|
|
font_color=WHITE, bold=True):
|
|
"""Add a circle shape with centered text."""
|
|
shape = slide.shapes.add_shape(MSO_SHAPE.OVAL, left, top, diameter, diameter)
|
|
shape.fill.solid()
|
|
shape.fill.fore_color.rgb = fill_color
|
|
shape.line.fill.background()
|
|
if text:
|
|
tf = shape.text_frame
|
|
tf.word_wrap = True
|
|
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
|
|
p = tf.paragraphs[0]
|
|
p.text = text
|
|
p.font.size = Pt(font_size)
|
|
p.font.color.rgb = font_color
|
|
p.font.bold = bold
|
|
p.font.name = FONT_NAME
|
|
tf.margin_left = Pt(2)
|
|
tf.margin_right = Pt(2)
|
|
tf.margin_top = Pt(2)
|
|
tf.margin_bottom = Pt(2)
|
|
return shape
|
|
|
|
|
|
def add_connector_line(slide, start_x, start_y, end_x, end_y, color=GREY_300, width=1.5):
|
|
"""Add a straight connector line."""
|
|
connector = slide.shapes.add_connector(
|
|
1, # MSO_CONNECTOR.STRAIGHT
|
|
start_x, start_y, end_x, end_y
|
|
)
|
|
connector.line.color.rgb = color
|
|
connector.line.width = Pt(width)
|
|
return connector
|
|
|
|
|
|
def add_rich_textbox(slide, left, top, width, height):
|
|
"""Add a text box and return the text frame for manual paragraph building."""
|
|
txBox = slide.shapes.add_textbox(left, top, width, height)
|
|
tf = txBox.text_frame
|
|
tf.word_wrap = True
|
|
return tf
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slide builders
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def make_title_slide(prs):
|
|
"""Slide 1: Title slide with logo."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
|
|
set_slide_bg(slide, DARK_NAVY)
|
|
|
|
# Logo
|
|
if LOGO_PATH.exists():
|
|
slide.shapes.add_picture(
|
|
str(LOGO_PATH),
|
|
Inches(0.8), Inches(1.0),
|
|
height=Inches(1.8)
|
|
)
|
|
|
|
# Title
|
|
add_textbox(slide, Inches(0.8), Inches(3.2), Inches(10), Inches(1.0),
|
|
"AI-Powered Proof Review", font_size=40, color=WHITE, bold=True)
|
|
|
|
# Subtitle
|
|
tf = add_rich_textbox(slide, Inches(0.8), Inches(4.2), Inches(10), Inches(0.8))
|
|
p = tf.paragraphs[0]
|
|
run1 = p.add_run()
|
|
run1.text = "Automated compliance, brand, and channel analysis for "
|
|
run1.font.size = Pt(20)
|
|
run1.font.color.rgb = GREY_300
|
|
run1.font.name = FONT_NAME
|
|
run2 = p.add_run()
|
|
run2.text = "Barclays"
|
|
run2.font.size = Pt(20)
|
|
run2.font.color.rgb = LIME
|
|
run2.font.bold = True
|
|
run2.font.name = FONT_NAME
|
|
run3 = p.add_run()
|
|
run3.text = " marketing materials"
|
|
run3.font.size = Pt(20)
|
|
run3.font.color.rgb = GREY_300
|
|
run3.font.name = FONT_NAME
|
|
|
|
# Built by line
|
|
add_textbox(slide, Inches(0.8), Inches(5.4), Inches(6), Inches(0.5),
|
|
"Built by OLIVER Agency", font_size=14, color=GREY_700)
|
|
|
|
# Decorative accent bar
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.05),
|
|
Inches(2.0), Pt(4))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = LIME
|
|
bar.line.fill.background()
|
|
|
|
|
|
def make_section_divider(prs, section_number, title, subtitle=""):
|
|
"""Section divider slide — dark navy with large number."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, DARK_NAVY)
|
|
|
|
# Large section number
|
|
add_textbox(slide, Inches(0.8), Inches(1.0), Inches(3), Inches(2.5),
|
|
f"{section_number:02d}", font_size=96, color=LIME, bold=True)
|
|
|
|
# Accent bar
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.6),
|
|
Inches(2.0), Pt(4))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = LIME
|
|
bar.line.fill.background()
|
|
|
|
# Title
|
|
add_textbox(slide, Inches(0.8), Inches(3.9), Inches(10), Inches(1.2),
|
|
title, font_size=36, color=WHITE, bold=True)
|
|
|
|
if subtitle:
|
|
add_textbox(slide, Inches(0.8), Inches(5.1), Inches(10), Inches(0.8),
|
|
subtitle, font_size=18, color=GREY_300)
|
|
|
|
|
|
def make_content_slide(prs, title, bullets, subtitle=""):
|
|
"""Standard content slide with heading and bullet list."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
# Title
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
title, font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
# Accent underline
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
y_start = Inches(1.3)
|
|
if subtitle:
|
|
add_textbox(slide, MARGIN_LEFT, y_start, CONTENT_WIDTH, Inches(0.5),
|
|
subtitle, font_size=15, color=GREY_700)
|
|
y_start = Inches(1.8)
|
|
|
|
add_bullet_list(slide, MARGIN_LEFT, y_start, CONTENT_WIDTH, Inches(5.5),
|
|
bullets, font_size=15, color=BLACK_TITLE)
|
|
|
|
return slide
|
|
|
|
|
|
def make_two_column_slide(prs, title, left_title, left_items, right_title, right_items):
|
|
"""Two-column content slide."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
# Title
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
title, font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
col_width = Inches(5.5)
|
|
gap = Inches(0.8)
|
|
|
|
# Left column
|
|
add_textbox(slide, MARGIN_LEFT, Inches(1.4), col_width, Inches(0.4),
|
|
left_title, font_size=18, color=TEAL, bold=True)
|
|
add_bullet_list(slide, MARGIN_LEFT, Inches(1.85), col_width, Inches(5.0),
|
|
left_items, font_size=14)
|
|
|
|
# Right column
|
|
right_left = MARGIN_LEFT + col_width + gap
|
|
add_textbox(slide, right_left, Inches(1.4), col_width, Inches(0.4),
|
|
right_title, font_size=18, color=TEAL, bold=True)
|
|
add_bullet_list(slide, right_left, Inches(1.85), col_width, Inches(5.0),
|
|
right_items, font_size=14)
|
|
|
|
return slide
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Specific slides
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def make_agenda_slide(prs):
|
|
"""Slide 2: Agenda."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
"Agenda", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
agenda_items = [
|
|
("01", "The Challenge", "Why manual proof review doesn't scale"),
|
|
("02", "Introducing Mod Comms", "AI-powered proof review at a glance"),
|
|
("03", "Multi-Agent AI System", "Four specialist agents + Lead Agent"),
|
|
("04", "Campaign Management", "Organising and tracking marketing proofs"),
|
|
("05", "Real-Time Analysis", "Live WebSocket-powered review"),
|
|
("06", "Feedback & Reporting", "Structured results and PDF export"),
|
|
("07", "Knowledge Base & Admin", "Managing guidelines, analytics, and access"),
|
|
("08", "Technical Architecture", "How it all fits together"),
|
|
]
|
|
|
|
y = Inches(1.4)
|
|
for num, title, desc in agenda_items:
|
|
# Number
|
|
add_textbox(slide, MARGIN_LEFT, y, Inches(0.6), Inches(0.45),
|
|
num, font_size=20, color=ACTIVE_BLUE, bold=True)
|
|
# Title
|
|
add_textbox(slide, Inches(1.5), y, Inches(4), Inches(0.3),
|
|
title, font_size=16, color=DARK_NAVY, bold=True)
|
|
# Description
|
|
add_textbox(slide, Inches(5.6), y + Inches(0.02), Inches(6), Inches(0.3),
|
|
desc, font_size=14, color=GREY_700)
|
|
# Divider line
|
|
if num != "08":
|
|
line = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
|
|
MARGIN_LEFT, y + Inches(0.48),
|
|
CONTENT_WIDTH, Pt(0.75))
|
|
line.fill.solid()
|
|
line.fill.fore_color.rgb = GREY_300
|
|
line.line.fill.background()
|
|
y += Inches(0.58)
|
|
|
|
|
|
def make_problem_slide(prs):
|
|
"""Slide 4: Problem statement."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
"The Manual Review Bottleneck", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
# Problem cards — 2x2 grid
|
|
cards = [
|
|
("Slow Turnaround", "Manual review of each proof takes hours.\nCampaign launches are delayed\nwaiting for feedback."),
|
|
("Inconsistent Quality", "Different reviewers apply guidelines\ndifferently. Critical compliance\nissues are sometimes missed."),
|
|
("Scaling Challenges", "Hundreds of proofs across Social,\nDisplay, Email, and Print channels\noverwhelm review teams."),
|
|
("Knowledge Silos", "Brand guidelines, legal requirements,\nand channel specs live in separate\ndocuments — hard to cross-reference."),
|
|
]
|
|
|
|
card_w = Inches(5.5)
|
|
card_h = Inches(2.3)
|
|
x_positions = [MARGIN_LEFT, MARGIN_LEFT + card_w + Inches(0.6)]
|
|
y_positions = [Inches(1.5), Inches(4.1)]
|
|
|
|
for idx, (card_title, card_desc) in enumerate(cards):
|
|
x = x_positions[idx % 2]
|
|
y = y_positions[idx // 2]
|
|
|
|
# Card background
|
|
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, card_w, card_h)
|
|
card.fill.solid()
|
|
card.fill.fore_color.rgb = GREY_100
|
|
card.line.color.rgb = GREY_300
|
|
card.line.width = Pt(1)
|
|
card.adjustments[0] = 0.05
|
|
|
|
# Red accent bar at top of card
|
|
accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x + Inches(0.2),
|
|
y + Inches(0.2), Inches(0.3), Pt(4))
|
|
accent.fill.solid()
|
|
accent.fill.fore_color.rgb = RAG_RED
|
|
accent.line.fill.background()
|
|
|
|
# Title
|
|
add_textbox(slide, x + Inches(0.7), y + Inches(0.1), card_w - Inches(1), Inches(0.4),
|
|
card_title, font_size=16, color=DARK_NAVY, bold=True)
|
|
|
|
# Description
|
|
add_textbox(slide, x + Inches(0.3), y + Inches(0.6), card_w - Inches(0.6), Inches(1.5),
|
|
card_desc, font_size=13, color=GREY_700, line_spacing=1.4)
|
|
|
|
|
|
def make_solution_slide(prs):
|
|
"""Slide 6: Solution overview with value proposition cards."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
"How Mod Comms Solves This", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
cards = [
|
|
(ACTIVE_BLUE, "Instant Analysis",
|
|
"Four AI agents review every\nproof in parallel — results in\nseconds, not days."),
|
|
(ELECTRIC_VIOLET, "Consistent Standards",
|
|
"Every proof is checked against\nthe same guidelines. No more\nhuman inconsistency."),
|
|
(TEAL, "Full Coverage",
|
|
"Legal, Brand, Channel Best\nPractices, and Tech Specs —\nall checked simultaneously."),
|
|
(RAG_GREEN, "Actionable Feedback",
|
|
"Clear RAG status with specific,\nconstructive recommendations\nfor every issue found."),
|
|
]
|
|
|
|
card_w = Inches(2.7)
|
|
card_h = Inches(4.0)
|
|
total_w = card_w * 4 + Inches(0.4) * 3
|
|
start_x = (SLIDE_WIDTH - total_w) / 2
|
|
|
|
for idx, (color, title, desc) in enumerate(cards):
|
|
x = start_x + idx * (card_w + Inches(0.4))
|
|
y = Inches(1.8)
|
|
|
|
# Card
|
|
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, card_w, card_h)
|
|
card.fill.solid()
|
|
card.fill.fore_color.rgb = DARK_NAVY
|
|
card.line.fill.background()
|
|
card.adjustments[0] = 0.06
|
|
|
|
# Top accent bar
|
|
accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
|
|
x + Inches(0.3), y + Inches(0.3),
|
|
Inches(0.8), Pt(4))
|
|
accent.fill.solid()
|
|
accent.fill.fore_color.rgb = color
|
|
accent.line.fill.background()
|
|
|
|
# Title
|
|
add_textbox(slide, x + Inches(0.3), y + Inches(0.6), card_w - Inches(0.6), Inches(0.5),
|
|
title, font_size=16, color=LIME, bold=True)
|
|
|
|
# Description
|
|
add_textbox(slide, x + Inches(0.3), y + Inches(1.2), card_w - Inches(0.6), Inches(2.5),
|
|
desc, font_size=13, color=GREY_300, line_spacing=1.5)
|
|
|
|
|
|
def make_agent_architecture_slide(prs):
|
|
"""Slide 8: Agent architecture diagram."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.3), CONTENT_WIDTH, Inches(0.6),
|
|
"Multi-Agent Architecture", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(0.88),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
# --- Proof Upload box ---
|
|
upload_x = Inches(0.5)
|
|
upload_y = Inches(2.8)
|
|
upload_w = Inches(1.8)
|
|
upload_h = Inches(1.2)
|
|
add_rounded_rect(slide, upload_x, upload_y, upload_w, upload_h,
|
|
GREY_100, "Proof\nUpload", font_size=14, font_color=DARK_NAVY,
|
|
bold=True, line_color=GREY_300)
|
|
|
|
# --- Arrow from upload to agents area ---
|
|
add_connector_line(slide, upload_x + upload_w, upload_y + upload_h / 2,
|
|
Inches(2.8), upload_y + upload_h / 2, ACTIVE_BLUE, 2)
|
|
|
|
# --- Four specialist agents ---
|
|
agent_x = Inches(2.8)
|
|
agents = [
|
|
(ACTIVE_BLUE, "Legal\nAgent"),
|
|
(ELECTRIC_VIOLET, "Brand\nAgent"),
|
|
(TEAL, "Channel Best\nPractices Agent"),
|
|
(CYAN_BRAND, "Channel Tech\nSpecs Agent"),
|
|
]
|
|
agent_w = Inches(2.0)
|
|
agent_h = Inches(0.9)
|
|
agent_gap = Inches(0.2)
|
|
agents_total_h = len(agents) * agent_h + (len(agents) - 1) * agent_gap
|
|
agent_start_y = Inches(1.2)
|
|
|
|
for idx, (color, name) in enumerate(agents):
|
|
y = agent_start_y + idx * (agent_h + agent_gap)
|
|
add_rounded_rect(slide, agent_x, y, agent_w, agent_h,
|
|
color, name, font_size=12, font_color=WHITE, bold=True)
|
|
# Arrow to lead agent
|
|
add_connector_line(slide, agent_x + agent_w, y + agent_h / 2,
|
|
Inches(5.6), y + agent_h / 2, GREY_300, 1.5)
|
|
|
|
# --- Gemini API box (below agents) ---
|
|
gemini_x = Inches(2.8)
|
|
gemini_y = agent_start_y + agents_total_h + Inches(0.4)
|
|
add_rounded_rect(slide, gemini_x, gemini_y, agent_w, Inches(0.7),
|
|
DARK_NAVY, "Google Gemini 2.5 Flash",
|
|
font_size=11, font_color=LIME, bold=True)
|
|
|
|
# Connector from Gemini to agents (vertical line on the left side)
|
|
gem_center_x = gemini_x + agent_w / 2
|
|
add_connector_line(slide, gem_center_x, agent_start_y + agents_total_h,
|
|
gem_center_x, gemini_y, GREY_300, 1.5)
|
|
|
|
# --- Lead Agent (central) ---
|
|
lead_x = Inches(5.6)
|
|
lead_y = Inches(2.4)
|
|
lead_w = Inches(2.2)
|
|
lead_h = Inches(1.8)
|
|
lead = add_rounded_rect(slide, lead_x, lead_y, lead_w, lead_h,
|
|
DARK_NAVY, "", line_color=LIME)
|
|
|
|
# Lead agent text
|
|
add_textbox(slide, lead_x + Inches(0.15), lead_y + Inches(0.2),
|
|
lead_w - Inches(0.3), Inches(0.4),
|
|
"Lead Agent", font_size=16, color=LIME, bold=True,
|
|
alignment=PP_ALIGN.CENTER)
|
|
add_textbox(slide, lead_x + Inches(0.15), lead_y + Inches(0.65),
|
|
lead_w - Inches(0.3), Inches(0.9),
|
|
"Synthesises all\nreviews into final\nstatus & summary",
|
|
font_size=11, color=WHITE, alignment=PP_ALIGN.CENTER)
|
|
|
|
# --- Arrow from lead to output ---
|
|
add_connector_line(slide, lead_x + lead_w, lead_y + lead_h / 2,
|
|
Inches(8.5), lead_y + lead_h / 2, ACTIVE_BLUE, 2)
|
|
|
|
# --- Knowledge Base box (below lead) ---
|
|
kb_x = Inches(5.6)
|
|
kb_y = gemini_y
|
|
add_rounded_rect(slide, kb_x, kb_y, lead_w, Inches(0.7),
|
|
DARK_NAVY, "Knowledge Base",
|
|
font_size=11, font_color=LIME, bold=True)
|
|
add_connector_line(slide, kb_x + lead_w / 2, lead_y + lead_h,
|
|
kb_x + lead_w / 2, kb_y, GREY_300, 1.5)
|
|
|
|
# --- Output: RAG Status ---
|
|
output_x = Inches(8.5)
|
|
output_y = Inches(1.5)
|
|
output_w = Inches(4.2)
|
|
output_h = Inches(4.8)
|
|
|
|
# Output container
|
|
out_card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
|
|
output_x, output_y, output_w, output_h)
|
|
out_card.fill.solid()
|
|
out_card.fill.fore_color.rgb = GREY_100
|
|
out_card.line.color.rgb = GREY_300
|
|
out_card.line.width = Pt(1)
|
|
out_card.adjustments[0] = 0.04
|
|
|
|
add_textbox(slide, output_x + Inches(0.2), output_y + Inches(0.15),
|
|
output_w - Inches(0.4), Inches(0.35),
|
|
"Analysis Output", font_size=14, color=DARK_NAVY, bold=True,
|
|
alignment=PP_ALIGN.CENTER)
|
|
|
|
# RAG circles
|
|
rag_items = [
|
|
(RAG_GREEN, "Green", "Passed — compliant"),
|
|
(RAG_AMBER, "Amber", "Minor issues to address"),
|
|
(RAG_RED, "Red", "Failed — must resolve"),
|
|
]
|
|
circle_d = Inches(0.4)
|
|
rag_y = output_y + Inches(0.7)
|
|
for idx, (color, label, desc) in enumerate(rag_items):
|
|
cy = rag_y + idx * Inches(0.6)
|
|
add_circle(slide, output_x + Inches(0.3), cy, circle_d, color,
|
|
"", font_size=9, bold=True)
|
|
add_textbox(slide, output_x + Inches(0.85), cy + Inches(0.02),
|
|
Inches(1), Inches(0.35),
|
|
label, font_size=12, color=DARK_NAVY, bold=True)
|
|
add_textbox(slide, output_x + Inches(1.8), cy + Inches(0.02),
|
|
Inches(2.2), Inches(0.35),
|
|
desc, font_size=11, color=GREY_700)
|
|
|
|
# Overall status labels
|
|
status_y = rag_y + Inches(2.0)
|
|
statuses = [
|
|
(RAG_GREEN, "Passed"),
|
|
(RAG_AMBER, "Requires Manual Legal Review"),
|
|
(RAG_RED, "Failed"),
|
|
(GREY_700, "Analysis Error"),
|
|
]
|
|
add_textbox(slide, output_x + Inches(0.2), status_y - Inches(0.3),
|
|
output_w - Inches(0.4), Inches(0.3),
|
|
"Overall Status:", font_size=12, color=DARK_NAVY, bold=True)
|
|
for idx, (color, label) in enumerate(statuses):
|
|
sy = status_y + idx * Inches(0.35)
|
|
dot = slide.shapes.add_shape(MSO_SHAPE.OVAL,
|
|
output_x + Inches(0.3), sy + Inches(0.05),
|
|
Inches(0.15), Inches(0.15))
|
|
dot.fill.solid()
|
|
dot.fill.fore_color.rgb = color
|
|
dot.line.fill.background()
|
|
add_textbox(slide, output_x + Inches(0.6), sy,
|
|
Inches(3.2), Inches(0.3),
|
|
label, font_size=11, color=DARK_NAVY)
|
|
|
|
|
|
def make_legal_agent_slide(prs):
|
|
"""Slide 9: Legal Agent deep dive."""
|
|
make_content_slide(prs, "Legal Agent",
|
|
[
|
|
"Detects financial promotions — interest rates, APR, credit products, savings rates",
|
|
"Checks advertising standards compliance against ASA/CAP code",
|
|
"Verifies required disclaimers are present, legible, and properly placed",
|
|
"Assesses FCA regulatory compliance for financial services marketing",
|
|
"Reviews terms and conditions — referenced where necessary, qualifying text clear",
|
|
"Checks third-party content — permissions, attributions, influencer disclosures",
|
|
"Financial promotion detected → overall status becomes 'Requires Manual Legal Review'",
|
|
"Uses British English and constructive language throughout all feedback",
|
|
],
|
|
subtitle="Compliance specialist ensuring all marketing materials meet legal and regulatory requirements")
|
|
|
|
|
|
def make_brand_agent_slide(prs):
|
|
"""Slide 10: Brand Agent deep dive."""
|
|
make_two_column_slide(prs,
|
|
"Brand Agent",
|
|
"Barclays Brand Checks",
|
|
[
|
|
"Logo usage — correct version, minimum size, clear space, placement",
|
|
"Colour palette — approved masterbrand colours, WCAG-compliant pairings",
|
|
"Typography — Barclays Effra (Arial fallback), correct weights and scale",
|
|
"Design principles — overall design reflects brand expression",
|
|
"Sacred assets — present and unaltered",
|
|
"Accessibility — legible font sizes, proper contrast ratios",
|
|
],
|
|
"Barclaycard Specifics",
|
|
[
|
|
"Card Portal — stroke weight, corner radius, border colour, rotation limits",
|
|
"Barclaycard-specific core principles and guidelines",
|
|
"Experiential and email-specific guidelines applied",
|
|
"Social media guidelines for Barclaycard-branded content",
|
|
"Brand selection is per-campaign — agents load the correct spec dynamically",
|
|
"15+ brand guideline documents in the Knowledge Base",
|
|
])
|
|
|
|
|
|
def make_channel_agents_slide(prs):
|
|
"""Slide 11: Channel agents deep dive."""
|
|
make_two_column_slide(prs,
|
|
"Channel Agents",
|
|
"Best Practices Agent",
|
|
[
|
|
"Content strategy — messaging clarity, CTA effectiveness",
|
|
"Creative best practices — visual hierarchy, engagement patterns",
|
|
"Platform optimisation — algorithm tips, safe zones, text-to-image ratios",
|
|
"Engagement — hashtags, mentions, tone suitability",
|
|
"Mobile-first design — legibility, touch targets, thumb-zone navigation",
|
|
],
|
|
"Tech Specs Agent",
|
|
[
|
|
"Dimensions & resolution — platform-specific sizes, DPI/PPI, aspect ratios",
|
|
"File format — type, size limits, compression quality",
|
|
"Typography specs — minimum font sizes, character counts",
|
|
"Digital grid system — 12-col desktop, 6-col mobile, 8px baseline",
|
|
"WCAG accessibility — colour contrast, documented pairings",
|
|
"Platform-specific specs — safe zones, video formats, frame rates",
|
|
])
|
|
|
|
|
|
def make_rag_status_slide(prs):
|
|
"""Slide 12: RAG Status & Decision Logic."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
"RAG Status & Decision Logic", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
# Agent RAG section
|
|
add_textbox(slide, MARGIN_LEFT, Inches(1.3), Inches(5), Inches(0.4),
|
|
"Per-Agent RAG Status", font_size=18, color=TEAL, bold=True)
|
|
|
|
rag_items = [
|
|
(RAG_GREEN, "Green", "Fully compliant — no issues found"),
|
|
(RAG_AMBER, "Amber", "Minor issues that should be addressed"),
|
|
(RAG_RED, "Red", "Significant issues that must be resolved"),
|
|
(GREY_700, "Error", "Agent could not analyse with confidence"),
|
|
]
|
|
|
|
for idx, (color, label, desc) in enumerate(rag_items):
|
|
y = Inches(1.85) + idx * Inches(0.55)
|
|
add_circle(slide, MARGIN_LEFT + Inches(0.2), y, Inches(0.35), color)
|
|
add_textbox(slide, MARGIN_LEFT + Inches(0.75), y + Inches(0.02),
|
|
Inches(1), Inches(0.3), label, font_size=14, color=DARK_NAVY, bold=True)
|
|
add_textbox(slide, MARGIN_LEFT + Inches(1.8), y + Inches(0.02),
|
|
Inches(4), Inches(0.3), desc, font_size=13, color=GREY_700)
|
|
|
|
# Decision logic section
|
|
add_textbox(slide, Inches(7.0), Inches(1.3), Inches(5.5), Inches(0.4),
|
|
"Lead Agent Decision Logic", font_size=18, color=TEAL, bold=True)
|
|
|
|
# Decision flow as cards
|
|
decisions = [
|
|
("1", RAG_AMBER, "Financial promotion detected?",
|
|
"→ Requires Manual Legal Review"),
|
|
("2", RAG_RED, "Any agent returned Error?",
|
|
"→ Analysis Error"),
|
|
("3", RAG_RED, "Any agent returned Red?",
|
|
"→ Failed"),
|
|
("4", RAG_GREEN, "Otherwise",
|
|
"→ Passed"),
|
|
]
|
|
|
|
for idx, (num, color, question, result) in enumerate(decisions):
|
|
y = Inches(1.85) + idx * Inches(1.15)
|
|
x = Inches(7.0)
|
|
|
|
# Decision card
|
|
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
|
|
x, y, Inches(5.5), Inches(0.95))
|
|
card.fill.solid()
|
|
card.fill.fore_color.rgb = GREY_100
|
|
card.line.color.rgb = GREY_300
|
|
card.line.width = Pt(1)
|
|
card.adjustments[0] = 0.08
|
|
|
|
# Step number
|
|
add_circle(slide, x + Inches(0.15), y + Inches(0.2), Inches(0.45),
|
|
color, num, font_size=14, font_color=WHITE)
|
|
|
|
add_textbox(slide, x + Inches(0.75), y + Inches(0.1),
|
|
Inches(4.5), Inches(0.35),
|
|
question, font_size=13, color=DARK_NAVY, bold=True)
|
|
add_textbox(slide, x + Inches(0.75), y + Inches(0.5),
|
|
Inches(4.5), Inches(0.35),
|
|
result, font_size=13, color=ACTIVE_BLUE, bold=True)
|
|
|
|
|
|
def make_campaign_lifecycle_slide(prs):
|
|
"""Slide 14: Campaign & proof lifecycle."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
"Campaign & Proof Lifecycle", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
# Flow steps
|
|
steps = [
|
|
("Create\nCampaign", "Set brand, agency,\nclient lead"),
|
|
("Upload\nProof", "Name, channel,\nsub-channel, type"),
|
|
("AI\nAnalysis", "4 agents analyse\nin parallel"),
|
|
("Review\nFeedback", "RAG status per\nagent + summary"),
|
|
("Resolve or\nRevise", "Mark issues resolved\nor upload new version"),
|
|
("Export\nReport", "PDF with full\nfeedback details"),
|
|
]
|
|
|
|
step_w = Inches(1.7)
|
|
step_h = Inches(1.3)
|
|
desc_h = Inches(0.9)
|
|
gap = Inches(0.25)
|
|
total_w = len(steps) * step_w + (len(steps) - 1) * gap
|
|
start_x = (SLIDE_WIDTH - total_w) / 2
|
|
step_y = Inches(1.8)
|
|
|
|
colors = [ACTIVE_BLUE, ELECTRIC_VIOLET, TEAL, ACTIVE_BLUE, ELECTRIC_VIOLET, TEAL]
|
|
|
|
for idx, (title, desc) in enumerate(steps):
|
|
x = start_x + idx * (step_w + gap)
|
|
|
|
# Step box
|
|
add_rounded_rect(slide, x, step_y, step_w, step_h,
|
|
colors[idx], title, font_size=13, font_color=WHITE, bold=True)
|
|
|
|
# Description below
|
|
add_textbox(slide, x, step_y + step_h + Inches(0.15), step_w, desc_h,
|
|
desc, font_size=11, color=GREY_700, alignment=PP_ALIGN.CENTER,
|
|
line_spacing=1.4)
|
|
|
|
# Arrow
|
|
if idx < len(steps) - 1:
|
|
arrow_x = x + step_w
|
|
arrow_y = step_y + step_h / 2
|
|
add_connector_line(slide, arrow_x, arrow_y,
|
|
arrow_x + gap, arrow_y, GREY_300, 2)
|
|
|
|
# Bottom section: Campaign table details
|
|
add_textbox(slide, MARGIN_LEFT, Inches(4.4), CONTENT_WIDTH, Inches(0.4),
|
|
"Campaign Table Features", font_size=16, color=TEAL, bold=True)
|
|
|
|
add_bullet_list(slide, MARGIN_LEFT, Inches(4.9), Inches(5.5), Inches(2.5),
|
|
[
|
|
"Sortable, filterable columns — name, status, proof count, agency",
|
|
"\"My Campaigns Only\" toggle for personal workspace",
|
|
"Show/Hide Completed toggle",
|
|
"Quick-create modal with brand guideline selection",
|
|
], font_size=13)
|
|
|
|
add_textbox(slide, Inches(7.0), Inches(4.4), Inches(5.5), Inches(0.4),
|
|
"Proof Management", font_size=16, color=TEAL, bold=True)
|
|
|
|
add_bullet_list(slide, Inches(7.0), Inches(4.9), Inches(5.5), Inches(2.5),
|
|
[
|
|
"Dependent dropdowns: Channel → Sub-Channel → Proof Type",
|
|
"Version history with download and comparison",
|
|
"Duplicate file detection via MD5 hash",
|
|
"Supported: Social, Display, Copy channels (22+ formats)",
|
|
], font_size=13)
|
|
|
|
|
|
def make_websocket_slide(prs):
|
|
"""Slide 16: WebSocket live analysis."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
"Real-Time WebSocket Analysis", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
# Message flow diagram
|
|
messages = [
|
|
("Client", ACTIVE_BLUE, "analyze", "File (base64) + metadata sent via WebSocket"),
|
|
("Server", TEAL, "agent_started", "\"Legal Agent is analysing...\""),
|
|
("Server", TEAL, "agent_completed", "Legal Agent review returned (RAG + feedback)"),
|
|
("Server", TEAL, "agent_started", "\"Brand Agent is analysing...\""),
|
|
("Server", TEAL, "agent_completed", "Brand Agent review returned"),
|
|
("Server", ELECTRIC_VIOLET, "summary", "Lead Agent summary generated"),
|
|
("Server", RAG_GREEN, "complete", "Full AgentReview + proof ID + version ID"),
|
|
]
|
|
|
|
y = Inches(1.5)
|
|
for sender, color, msg_type, desc in messages:
|
|
# Direction indicator
|
|
if sender == "Client":
|
|
arrow_text = "→"
|
|
label_color = ACTIVE_BLUE
|
|
else:
|
|
arrow_text = "←"
|
|
label_color = TEAL
|
|
|
|
# Message type badge
|
|
add_rounded_rect(slide, MARGIN_LEFT, y, Inches(0.8), Inches(0.4),
|
|
GREY_100, sender, font_size=10, font_color=GREY_700,
|
|
line_color=GREY_300)
|
|
|
|
add_textbox(slide, Inches(1.75), y + Inches(0.03), Inches(0.3), Inches(0.35),
|
|
arrow_text, font_size=16, color=label_color, bold=True)
|
|
|
|
add_rounded_rect(slide, Inches(2.2), y, Inches(2.0), Inches(0.4),
|
|
color, msg_type, font_size=11, font_color=WHITE, bold=True)
|
|
|
|
add_textbox(slide, Inches(4.4), y + Inches(0.05), Inches(5), Inches(0.35),
|
|
desc, font_size=12, color=GREY_700)
|
|
|
|
y += Inches(0.55)
|
|
|
|
# Right side: Key features
|
|
add_textbox(slide, Inches(9.8), Inches(1.3), Inches(3), Inches(0.4),
|
|
"Key Capabilities", font_size=16, color=TEAL, bold=True)
|
|
|
|
add_bullet_list(slide, Inches(9.8), Inches(1.8), Inches(3.2), Inches(4.5),
|
|
[
|
|
"Parallel agent execution via asyncio.gather()",
|
|
"Real-time progress — agents report as they finish",
|
|
"PDF rasterisation (up to 10 pages)",
|
|
"Revision-aware analysis with previous review context",
|
|
"Authenticated via MSAL bearer token",
|
|
"Automatic proof persistence to database",
|
|
], font_size=12)
|
|
|
|
|
|
def make_feedback_reports_slide(prs):
|
|
"""Slide 18: Feedback reports & PDF export."""
|
|
make_two_column_slide(prs,
|
|
"Feedback Reports & PDF Export",
|
|
"Asset Detail View",
|
|
[
|
|
"Two-column layout: proof preview (left) + agent feedback (right)",
|
|
"RAG status badge per agent with colour-coded indicators",
|
|
"Detailed text feedback with constructive recommendations",
|
|
"Actionable issues listed with 'Mark as Resolved' capability",
|
|
"Resolution notes recorded for audit trail",
|
|
"Flag incorrect feedback — sent to Auditing dashboard",
|
|
"Version history with one-click navigation between versions",
|
|
],
|
|
"PDF Export",
|
|
[
|
|
"Single Proof Report — detailed feedback for one proof",
|
|
"Campaign Report — consolidated report for all proofs",
|
|
"Cover page with Barclays branding, campaign name, date",
|
|
"Proof preview with metadata (name, version, channel)",
|
|
"Lead Agent summary with overall status",
|
|
"Per-agent sections: RAG status, full feedback, issues list",
|
|
"Professional formatting ready for stakeholder review",
|
|
])
|
|
|
|
|
|
def make_knowledge_base_slide(prs):
|
|
"""Slide 20: Knowledge Base management."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
"Knowledge Base Management", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
# Pipeline steps
|
|
pipe_steps = [
|
|
("Upload\nDocuments", "PDF or Markdown\nbrand/legal/channel\nguidelines"),
|
|
("Parse &\nConvert", "AI converts uploads\nto structured\nmarkdown"),
|
|
("Generate\nSpec", "AI synthesises all\ndocuments into\nunified specification"),
|
|
("Version\nControl", "New spec version\ncreated with diff\ncomparison"),
|
|
("Activate", "Admin selects\nwhich version\nagents use"),
|
|
]
|
|
|
|
step_w = Inches(2.0)
|
|
step_h = Inches(1.2)
|
|
gap = Inches(0.35)
|
|
total_w = len(pipe_steps) * step_w + (len(pipe_steps) - 1) * gap
|
|
start_x = (SLIDE_WIDTH - total_w) / 2
|
|
step_y = Inches(1.5)
|
|
|
|
pipe_colors = [ACTIVE_BLUE, ELECTRIC_VIOLET, TEAL, ACTIVE_BLUE, RAG_GREEN]
|
|
|
|
for idx, (title, desc) in enumerate(pipe_steps):
|
|
x = start_x + idx * (step_w + gap)
|
|
add_rounded_rect(slide, x, step_y, step_w, step_h,
|
|
pipe_colors[idx], title, font_size=13, font_color=WHITE, bold=True)
|
|
add_textbox(slide, x, step_y + step_h + Inches(0.1), step_w, Inches(0.9),
|
|
desc, font_size=11, color=GREY_700, alignment=PP_ALIGN.CENTER,
|
|
line_spacing=1.4)
|
|
if idx < len(pipe_steps) - 1:
|
|
ax = x + step_w
|
|
ay = step_y + step_h / 2
|
|
add_connector_line(slide, ax, ay, ax + gap, ay, GREY_300, 2)
|
|
|
|
# Knowledge bases list
|
|
add_textbox(slide, MARGIN_LEFT, Inches(4.3), CONTENT_WIDTH, Inches(0.4),
|
|
"Five Knowledge Bases", font_size=16, color=TEAL, bold=True)
|
|
|
|
kbs = [
|
|
("Legal", "FCA regulations, ASA/CAP code, financial promotion rules"),
|
|
("Brand — Barclays", "15+ guideline documents covering logo, colour, typography, design"),
|
|
("Brand — Barclaycard", "Core principles, digital guidelines, email and social specs"),
|
|
("Channel Best Practices", "LinkedIn, Reddit, platform optimisation, creative inspiration"),
|
|
("Channel Tech Specs", "Social templates, dimension specs, platform-specific requirements"),
|
|
]
|
|
|
|
kb_w = Inches(2.2)
|
|
kb_h = Inches(1.8)
|
|
kb_gap = Inches(0.25)
|
|
total_kb_w = len(kbs) * kb_w + (len(kbs) - 1) * kb_gap
|
|
kb_start_x = (SLIDE_WIDTH - total_kb_w) / 2
|
|
kb_y = Inches(4.8)
|
|
|
|
kb_colors = [ACTIVE_BLUE, ELECTRIC_VIOLET, ELECTRIC_VIOLET, TEAL, CYAN_BRAND]
|
|
|
|
for idx, (name, desc) in enumerate(kbs):
|
|
x = kb_start_x + idx * (kb_w + kb_gap)
|
|
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
|
|
x, kb_y, kb_w, kb_h)
|
|
card.fill.solid()
|
|
card.fill.fore_color.rgb = DARK_NAVY
|
|
card.line.fill.background()
|
|
card.adjustments[0] = 0.06
|
|
|
|
# Accent
|
|
accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
|
|
x + Inches(0.2), kb_y + Inches(0.15),
|
|
Inches(0.6), Pt(3))
|
|
accent.fill.solid()
|
|
accent.fill.fore_color.rgb = kb_colors[idx]
|
|
accent.line.fill.background()
|
|
|
|
add_textbox(slide, x + Inches(0.15), kb_y + Inches(0.35),
|
|
kb_w - Inches(0.3), Inches(0.35),
|
|
name, font_size=12, color=LIME, bold=True,
|
|
alignment=PP_ALIGN.CENTER)
|
|
add_textbox(slide, x + Inches(0.15), kb_y + Inches(0.75),
|
|
kb_w - Inches(0.3), Inches(0.9),
|
|
desc, font_size=10, color=GREY_300,
|
|
alignment=PP_ALIGN.CENTER, line_spacing=1.4)
|
|
|
|
|
|
def make_admin_three_col_slide(prs):
|
|
"""Slide 21: Analytics, Auditing & Settings (3-column)."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
"Analytics, Auditing & Settings", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
columns = [
|
|
("Analytics", ACTIVE_BLUE, [
|
|
"Proofs Uploaded — total count",
|
|
"Pass Rate — % of proofs that passed",
|
|
"Issues Found — total across agents",
|
|
"Time Saved — hours estimated",
|
|
"AI Performance Summary — weekly trends",
|
|
"Agent Performance Table — per-agent stats",
|
|
]),
|
|
("Auditing", ELECTRIC_VIOLET, [
|
|
"Flags Tab — user-reported incorrect feedback",
|
|
"Resolutions Tab — user-resolved issues",
|
|
"Errors Tab — analysis failures",
|
|
"Links to specific proof and version",
|
|
"Full audit trail with timestamps",
|
|
"Agency-filterable for oversight admins",
|
|
]),
|
|
("Settings", TEAL, [
|
|
"Manage Channels — add/remove options",
|
|
"Sub-Channels — dependent on parent",
|
|
"Proof Types — dependent on sub-channel",
|
|
"Changes propagate immediately",
|
|
"Admin-only access for editing",
|
|
"Oversight admins get read-only view",
|
|
]),
|
|
]
|
|
|
|
col_w = Inches(3.7)
|
|
col_gap = Inches(0.3)
|
|
total_col_w = len(columns) * col_w + (len(columns) - 1) * col_gap
|
|
start_x = (SLIDE_WIDTH - total_col_w) / 2
|
|
|
|
for idx, (title, color, items) in enumerate(columns):
|
|
x = start_x + idx * (col_w + col_gap)
|
|
y = Inches(1.4)
|
|
|
|
# Column header card
|
|
add_rounded_rect(slide, x, y, col_w, Inches(0.5),
|
|
color, title, font_size=15, font_color=WHITE, bold=True)
|
|
|
|
# Bullet list
|
|
add_bullet_list(slide, x + Inches(0.1), y + Inches(0.7),
|
|
col_w - Inches(0.2), Inches(5.0),
|
|
items, font_size=13, line_spacing=1.4)
|
|
|
|
|
|
def make_user_roles_slide(prs):
|
|
"""Slide 22: User Roles & Access Control (table-style)."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
|
|
"User Roles & Access Control", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
# Table data
|
|
headers = ["Role", "Write", "Analytics", "Auditing", "Knowledge\nBase", "Settings", "User\nMgmt", "Agency\nFilter"]
|
|
rows_data = [
|
|
["Super Admin", "\u2713", "\u2713", "\u2713", "\u2713", "Full", "\u2713", "\u2713"],
|
|
["Oversight Admin", "\u2717", "\u2713", "\u2713", "\u2717", "Read", "\u2717", "\u2713"],
|
|
["Agency Admin", "\u2713", "\u2713", "\u2717", "\u2717", "Full", "\u2717", "\u2717"],
|
|
["Basic User", "\u2713", "\u2717", "\u2717", "\u2717", "\u2717", "\u2717", "\u2717"],
|
|
]
|
|
|
|
# Dimensions
|
|
col_widths = [Inches(1.8), Inches(0.9), Inches(1.2), Inches(1.2), Inches(1.2),
|
|
Inches(1.0), Inches(1.0), Inches(1.1)]
|
|
row_h = Inches(0.6)
|
|
header_h = Inches(0.7)
|
|
table_start_x = (SLIDE_WIDTH - sum(w for w in col_widths)) / 2
|
|
table_start_y = Inches(1.5)
|
|
|
|
# Draw header row
|
|
x = table_start_x
|
|
for i, header in enumerate(headers):
|
|
cell = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, table_start_y,
|
|
col_widths[i], header_h)
|
|
cell.fill.solid()
|
|
cell.fill.fore_color.rgb = DARK_NAVY
|
|
cell.line.color.rgb = DARK_NAVY
|
|
cell.line.width = Pt(0.5)
|
|
|
|
tf = cell.text_frame
|
|
tf.word_wrap = True
|
|
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
|
|
p = tf.paragraphs[0]
|
|
p.text = header
|
|
p.font.size = Pt(11)
|
|
p.font.color.rgb = LIME
|
|
p.font.bold = True
|
|
p.font.name = FONT_NAME
|
|
tf.margin_top = Pt(6)
|
|
tf.margin_bottom = Pt(6)
|
|
x += col_widths[i]
|
|
|
|
# Draw data rows
|
|
for row_idx, row in enumerate(rows_data):
|
|
x = table_start_x
|
|
y = table_start_y + header_h + row_idx * row_h
|
|
bg = WHITE if row_idx % 2 == 0 else GREY_100
|
|
|
|
for col_idx, val in enumerate(row):
|
|
cell = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y,
|
|
col_widths[col_idx], row_h)
|
|
cell.fill.solid()
|
|
cell.fill.fore_color.rgb = bg
|
|
cell.line.color.rgb = GREY_300
|
|
cell.line.width = Pt(0.5)
|
|
|
|
tf = cell.text_frame
|
|
tf.word_wrap = True
|
|
p = tf.paragraphs[0]
|
|
p.text = val
|
|
p.font.size = Pt(12)
|
|
p.font.name = FONT_NAME
|
|
tf.margin_top = Pt(4)
|
|
tf.margin_bottom = Pt(4)
|
|
|
|
if col_idx == 0:
|
|
p.font.color.rgb = DARK_NAVY
|
|
p.font.bold = True
|
|
p.alignment = PP_ALIGN.LEFT
|
|
tf.margin_left = Pt(8)
|
|
else:
|
|
p.alignment = PP_ALIGN.CENTER
|
|
if val == "\u2713":
|
|
p.font.color.rgb = RAG_GREEN
|
|
p.font.bold = True
|
|
elif val == "\u2717":
|
|
p.font.color.rgb = RAG_RED
|
|
elif val in ("Full", "Read"):
|
|
p.font.color.rgb = ACTIVE_BLUE
|
|
p.font.bold = True
|
|
else:
|
|
p.font.color.rgb = DARK_NAVY
|
|
|
|
x += col_widths[col_idx]
|
|
|
|
# Auth info below table
|
|
auth_y = table_start_y + header_h + len(rows_data) * row_h + Inches(0.6)
|
|
add_textbox(slide, MARGIN_LEFT, auth_y, CONTENT_WIDTH, Inches(0.4),
|
|
"Authentication & Security", font_size=16, color=TEAL, bold=True)
|
|
|
|
add_bullet_list(slide, MARGIN_LEFT, auth_y + Inches(0.4), CONTENT_WIDTH, Inches(2.5),
|
|
[
|
|
"Azure AD / O365 SSO integration via MSAL — shared with CopyGenAI application",
|
|
"Bearer token authentication on all API requests and WebSocket connections",
|
|
"Agency-scoped data access — basic users only see their own agency's campaigns",
|
|
"User Management screen (Super Admin only) — assign roles, agencies, view change history",
|
|
], font_size=13)
|
|
|
|
|
|
def make_tech_architecture_slide(prs):
|
|
"""Slide 23: Technical Architecture (layered diagram)."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, WHITE)
|
|
|
|
add_textbox(slide, MARGIN_LEFT, Inches(0.3), CONTENT_WIDTH, Inches(0.6),
|
|
"Technical Architecture", font_size=28, color=DARK_NAVY, bold=True)
|
|
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(0.85),
|
|
Inches(1.5), Pt(3))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = ACTIVE_BLUE
|
|
bar.line.fill.background()
|
|
|
|
# Layer definitions
|
|
layers = [
|
|
("Frontend", ACTIVE_BLUE, [
|
|
("React + TypeScript", Inches(2.5)),
|
|
("Tailwind CSS", Inches(1.8)),
|
|
("MSAL Auth", Inches(1.5)),
|
|
("WebSocket Client", Inches(2.0)),
|
|
("REST Client", Inches(1.6)),
|
|
]),
|
|
("Backend API", ELECTRIC_VIOLET, [
|
|
("FastAPI (Python)", Inches(2.5)),
|
|
("WebSocket /ws/analyze", Inches(2.3)),
|
|
("REST /api/*", Inches(1.8)),
|
|
("Analysis Service", Inches(2.0)),
|
|
]),
|
|
("AI Layer", TEAL, [
|
|
("Legal Agent", Inches(1.8)),
|
|
("Brand Agent", Inches(1.8)),
|
|
("Best Practices Agent", Inches(2.3)),
|
|
("Tech Specs Agent", Inches(2.0)),
|
|
("Lead Agent", Inches(1.6)),
|
|
]),
|
|
("Data Layer", DARK_NAVY, [
|
|
("PostgreSQL", Inches(2.0)),
|
|
("SQLAlchemy Async", Inches(2.2)),
|
|
("Alembic Migrations", Inches(2.2)),
|
|
("File Storage", Inches(1.8)),
|
|
]),
|
|
]
|
|
|
|
layer_h = Inches(1.1)
|
|
layer_gap = Inches(0.25)
|
|
start_y = Inches(1.15)
|
|
label_w = Inches(1.8)
|
|
content_start_x = MARGIN_LEFT + label_w + Inches(0.2)
|
|
# Reserve 2.6" on right for external service boxes
|
|
ext_col_w = Inches(2.3)
|
|
ext_gap = Inches(0.3)
|
|
content_end_x = SLIDE_WIDTH - MARGIN_RIGHT - ext_col_w - ext_gap
|
|
|
|
for layer_idx, (layer_name, color, components) in enumerate(layers):
|
|
y = start_y + layer_idx * (layer_h + layer_gap)
|
|
|
|
# Layer label
|
|
add_rounded_rect(slide, MARGIN_LEFT, y, label_w, layer_h,
|
|
color, layer_name, font_size=13, font_color=WHITE, bold=True)
|
|
|
|
# Components
|
|
comp_gap = Inches(0.15)
|
|
total_comp_w = sum(w for _, w in components) + (len(components) - 1) * comp_gap
|
|
available_w = content_end_x - content_start_x
|
|
scale = min(1.0, available_w / total_comp_w)
|
|
|
|
cx = content_start_x
|
|
for comp_name, comp_w in components:
|
|
scaled_w = comp_w * scale
|
|
add_rounded_rect(slide, cx, y + Inches(0.15), scaled_w, layer_h - Inches(0.3),
|
|
GREY_100, comp_name, font_size=11, font_color=DARK_NAVY,
|
|
bold=False, line_color=color)
|
|
cx += scaled_w + comp_gap * scale
|
|
|
|
# External services on the right (in reserved column)
|
|
ext_x = SLIDE_WIDTH - MARGIN_RIGHT - ext_col_w
|
|
# Position Azure AD aligned with Backend API row
|
|
ext_y_azure = start_y + 1 * (layer_h + layer_gap) + Inches(0.15)
|
|
add_rounded_rect(slide, ext_x, ext_y_azure, ext_col_w, layer_h - Inches(0.3),
|
|
DARK_NAVY, "Azure AD", font_size=12, font_color=LIME, bold=True)
|
|
|
|
# Position Gemini aligned with AI Layer row
|
|
ext_y_gemini = start_y + 2 * (layer_h + layer_gap) + Inches(0.15)
|
|
add_rounded_rect(slide, ext_x, ext_y_gemini, ext_col_w, layer_h - Inches(0.3),
|
|
DARK_NAVY, "Google Gemini\n2.5 Flash", font_size=12,
|
|
font_color=LIME, bold=True)
|
|
|
|
# Bottom note
|
|
note_y = start_y + len(layers) * (layer_h + layer_gap) + Inches(0.1)
|
|
add_textbox(slide, MARGIN_LEFT, note_y, CONTENT_WIDTH, Inches(0.8),
|
|
"All communication secured via MSAL bearer tokens. "
|
|
"Agents run in parallel via asyncio. "
|
|
"Database uses async SQLAlchemy + asyncpg for non-blocking I/O.",
|
|
font_size=12, color=GREY_700)
|
|
|
|
|
|
def make_closing_slide(prs):
|
|
"""Slide 24: Closing slide with logo."""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
set_slide_bg(slide, DARK_NAVY)
|
|
|
|
# Logo
|
|
if LOGO_PATH.exists():
|
|
slide.shapes.add_picture(
|
|
str(LOGO_PATH),
|
|
Inches(0.8), Inches(1.2),
|
|
height=Inches(1.8)
|
|
)
|
|
|
|
# Accent bar
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.4),
|
|
Inches(2.0), Pt(4))
|
|
bar.fill.solid()
|
|
bar.fill.fore_color.rgb = LIME
|
|
bar.line.fill.background()
|
|
|
|
tf = add_rich_textbox(slide, Inches(0.8), Inches(3.7), Inches(10), Inches(1.0))
|
|
p = tf.paragraphs[0]
|
|
run1 = p.add_run()
|
|
run1.text = "Intelligent Review. "
|
|
run1.font.size = Pt(32)
|
|
run1.font.color.rgb = ELECTRIC_VIOLET
|
|
run1.font.bold = True
|
|
run1.font.name = FONT_NAME
|
|
run2 = p.add_run()
|
|
run2.text = "Confident Delivery."
|
|
run2.font.size = Pt(32)
|
|
run2.font.color.rgb = WHITE
|
|
run2.font.bold = True
|
|
run2.font.name = FONT_NAME
|
|
|
|
add_textbox(slide, Inches(0.8), Inches(4.8), Inches(8), Inches(0.5),
|
|
"AI-powered proof review for Barclays marketing materials",
|
|
font_size=16, color=GREY_300)
|
|
|
|
add_textbox(slide, Inches(0.8), Inches(5.6), Inches(6), Inches(0.5),
|
|
"Built by OLIVER Agency", font_size=14, color=GREY_700)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main():
|
|
prs = Presentation()
|
|
prs.slide_width = SLIDE_WIDTH
|
|
prs.slide_height = SLIDE_HEIGHT
|
|
|
|
# Slide 1: Title
|
|
make_title_slide(prs)
|
|
|
|
# Slide 2: Agenda
|
|
make_agenda_slide(prs)
|
|
|
|
# Slide 3: Section — The Challenge
|
|
make_section_divider(prs, 1, "The Challenge",
|
|
"Why manual proof review doesn't scale")
|
|
|
|
# Slide 4: Problem statement
|
|
make_problem_slide(prs)
|
|
|
|
# Slide 5: Section — Introducing Mod Comms
|
|
make_section_divider(prs, 2, "Introducing Mod Comms",
|
|
"AI-powered proof review at a glance")
|
|
|
|
# Slide 6: Solution overview
|
|
make_solution_slide(prs)
|
|
|
|
# Slide 7: Section — Multi-Agent AI System
|
|
make_section_divider(prs, 3, "Multi-Agent AI System",
|
|
"Four specialist agents working in parallel")
|
|
|
|
# Slide 8: Agent architecture diagram
|
|
make_agent_architecture_slide(prs)
|
|
|
|
# Slide 9: Legal Agent
|
|
make_legal_agent_slide(prs)
|
|
|
|
# Slide 10: Brand Agent
|
|
make_brand_agent_slide(prs)
|
|
|
|
# Slide 11: Channel Agents
|
|
make_channel_agents_slide(prs)
|
|
|
|
# Slide 12: RAG Status
|
|
make_rag_status_slide(prs)
|
|
|
|
# Slide 13: Section — Campaign Management
|
|
make_section_divider(prs, 4, "Campaign Management",
|
|
"Organising and tracking marketing proofs")
|
|
|
|
# Slide 14: Campaign lifecycle
|
|
make_campaign_lifecycle_slide(prs)
|
|
|
|
# Slide 15: Section — Real-Time Analysis
|
|
make_section_divider(prs, 5, "Real-Time Analysis",
|
|
"Live WebSocket-powered proof review")
|
|
|
|
# Slide 16: WebSocket analysis
|
|
make_websocket_slide(prs)
|
|
|
|
# Slide 17: Section — Feedback & Reporting
|
|
make_section_divider(prs, 6, "Feedback & Reporting",
|
|
"Structured results and PDF export")
|
|
|
|
# Slide 18: Feedback reports
|
|
make_feedback_reports_slide(prs)
|
|
|
|
# Slide 19: Section — Knowledge Base & Admin
|
|
make_section_divider(prs, 7, "Knowledge Base & Admin",
|
|
"Managing guidelines, analytics, and access control")
|
|
|
|
# Slide 20: Knowledge Base
|
|
make_knowledge_base_slide(prs)
|
|
|
|
# Slide 21: Analytics, Auditing, Settings
|
|
make_admin_three_col_slide(prs)
|
|
|
|
# Slide 22: User Roles
|
|
make_user_roles_slide(prs)
|
|
|
|
# Slide 23: Section — Technical Architecture
|
|
make_section_divider(prs, 8, "Technical Architecture",
|
|
"How it all fits together")
|
|
|
|
# Slide 24: Tech Architecture
|
|
make_tech_architecture_slide(prs)
|
|
|
|
# Slide 25: Closing
|
|
make_closing_slide(prs)
|
|
|
|
# Save
|
|
prs.save(str(OUTPUT_PPTX))
|
|
print(f"Presentation saved to: {OUTPUT_PPTX}")
|
|
print(f"Total slides: {len(prs.slides)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|