Add brand selection support for Barclays vs Barclaycard guidelines

- Add brand field to AnalyzeProofOptions interface and WebSocket message
- Pass campaign's brandGuidelines to analyzeProof in App.tsx (upload & retry)
- Extract brand from WebSocket message in handlers.py and pass to analysis
- Update AnalysisService.analyze_proof to accept brand parameter
- Refactor BrandAgent to dynamically select brand spec based on brand param
- Add get_barclays_brand_spec() method to ReferenceDocsService (placeholder)

The brand agent now uses the appropriate specification (Barclaycard spec or
Barclays spec when available) based on the campaign's brandGuidelines setting.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael 2026-01-24 11:31:59 -06:00
parent d3e7f99be0
commit 2cd3b2b9ae
6 changed files with 73 additions and 16 deletions

View file

@ -20,42 +20,61 @@ class BrandAgent(BaseAgent):
reference_docs: Service for loading reference documents
"""
self.gemini = gemini_service
# Use the Barclaycard-specific brand spec (more concise and structured for analysis)
self.brand_context = reference_docs.get_barclaycard_brand_spec()
self.reference_docs = reference_docs
async def analyze(self, images: List[Tuple[bytes, str]]) -> SubReview:
def _get_brand_context(self, brand: str) -> str:
"""
Get the appropriate brand specification based on the brand selection.
Args:
brand: Brand name ('Barclays' or 'Barclaycard')
Returns:
Brand specification content
"""
if brand == "Barclays":
return self.reference_docs.get_barclays_brand_spec()
else:
# Default to Barclaycard
return self.reference_docs.get_barclaycard_brand_spec()
async def analyze(self, images: List[Tuple[bytes, str]], brand: str = "Barclaycard") -> SubReview:
"""
Analyze the proof for brand guideline adherence.
Args:
images: List of (file_data, mime_type) tuples representing the proof
brand: Brand to analyze against ('Barclays' or 'Barclaycard')
Returns:
SubReview with brand compliance assessment
"""
prompt = f"""You are a brand expert for Barclaycard. Your role is to analyze marketing proofs for adherence to Barclaycard brand guidelines.
# Get the appropriate brand specification
brand_context = self._get_brand_context(brand)
Here is the Barclaycard brand specification to use for your analysis:
prompt = f"""You are a brand expert for {brand}. Your role is to analyze marketing proofs for adherence to {brand} brand guidelines.
{self.brand_context}
Here is the {brand} brand specification to use for your analysis:
{brand_context}
---
Analyze the uploaded proof against the Barclaycard brand specification above, checking for:
Analyze the uploaded proof against the {brand} brand specification above, checking for:
1. **Logo Usage**: Is the Barclaycard logo (Open World symbol + wordmark) used correctly? Check minimum size (25px), clear space, placement, and that it hasn't been altered. Logo should be cyan or white only.
1. **Logo Usage**: Is the {brand} logo used correctly? Check minimum size, clear space, placement, and that it hasn't been altered. Verify correct logo colors per guidelines.
2. **Card Portal**: If the Card Portal is present, verify it follows sizing rules (stroke weight and corner radius based on shortest side), cyan border color (#00AEEF), rotation within ±1.5°, and that the logo is not placed outside it.
2. **Card Portal** (if applicable for {brand}): If the Card Portal asset is present, verify it follows sizing rules (stroke weight and corner radius based on shortest side), proper border color, rotation limits, and that the logo is not placed outside it.
3. **Color Palette**: Are only approved Barclaycard masterbrand colors used? Check for proper WCAG-compliant color pairings, cyan presence as sacred asset, and Active Blue (#006DE3) only used for interactive elements.
3. **Color Palette**: Are only approved {brand} masterbrand colors used? Check for proper WCAG-compliant color pairings and that sacred/primary colors are used appropriately.
4. **Typography**: Is Barclays Effra (or Arial fallback) used correctly? Check font weights (Medium/Bold for headings, Regular/Medium for body), sizes per type scale, and line spacing (1.1x or 1.2x).
4. **Typography**: Is Barclays Effra (or Arial fallback) used correctly? Check font weights (Medium/Bold for headings, Regular/Medium for body), sizes per type scale, and line spacing.
5. **Design Principles**: Does the overall design reflect Bold, Purposeful, and Expressive principles?
5. **Design Principles**: Does the overall design reflect the brand principles defined in the specification?
6. **Sacred Assets**: Verify the three sacred assets (Cyan, Logo, Card Portal) are present and not altered or misused.
6. **Sacred Assets**: Verify that sacred/primary brand assets are present and not altered or misused.
7. **Accessibility**: Check for legible font sizes, proper contrast, and appropriate use of ticker tape (max once per communication, ±1.5° rotation only).
7. **Accessibility**: Check for legible font sizes, proper contrast, and appropriate use of special typography elements per guidelines.
Provide your analysis as a JSON object. Be specific about any issues found and reference the relevant guideline sections.

View file

@ -58,6 +58,7 @@ class AnalysisService:
file_type: str,
on_agent_update: AgentCallback | None = None,
is_wip: bool = False,
brand: str = "Barclaycard",
) -> Tuple[AgentReview, Optional[List[Tuple[bytes, int, int]]]]:
"""
Analyze a proof using all agents sequentially.
@ -69,6 +70,7 @@ class AnalysisService:
Called with (agent_name, None) when agent starts,
and (agent_name, review) when agent completes.
is_wip: Whether this is a work-in-progress analysis
brand: Brand to use for brand guidelines analysis ('Barclays' or 'Barclaycard')
Returns:
Tuple of:
@ -76,7 +78,7 @@ class AnalysisService:
- List of rasterized PDF pages if input was PDF, else None
Each page is (png_bytes, width, height)
"""
logger.info(f"[ANALYSIS] Starting proof analysis - file_type: {file_type}, file_size: {len(file_data)} bytes, is_wip: {is_wip}")
logger.info(f"[ANALYSIS] Starting proof analysis - file_type: {file_type}, file_size: {len(file_data)} bytes, is_wip: {is_wip}, brand: {brand}")
reviews: dict[str, SubReview] = {}
# Prepare images for analysis
@ -122,7 +124,11 @@ class AnalysisService:
await on_agent_update(agent_name, None)
# Run the agent with images list
review = await agent.analyze(images)
# Pass brand to Brand Agent for selecting appropriate guidelines
if agent_name == "Brand Agent":
review = await agent.analyze(images, brand=brand)
else:
review = await agent.analyze(images)
reviews[agent_name] = review
logger.info(f"[ANALYSIS] Agent completed: {agent_name} - ragStatus: {review.ragStatus}")

View file

@ -48,6 +48,26 @@ class ReferenceDocsService:
self._barclaycard_brand_spec = self.get_brand_context()
return self._barclaycard_brand_spec
def get_barclays_brand_spec(self) -> str:
"""Load and return the Barclays brand specification from prompts directory."""
# Check cache first
if not hasattr(self, '_barclays_brand_spec'):
self._barclays_brand_spec = None
if self._barclays_brand_spec is None:
spec_path = self.prompts_path / "brand_barclays.md"
try:
if spec_path.exists():
self._barclays_brand_spec = spec_path.read_text(encoding="utf-8")
else:
print(f"Warning: Barclays brand spec not found at {spec_path}, using raw brand context")
# Fall back to raw brand context from reference_docs/brand/
self._barclays_brand_spec = self.get_brand_context()
except Exception as e:
print(f"Warning: Could not read Barclays brand spec: {e}")
self._barclays_brand_spec = self.get_brand_context()
return self._barclays_brand_spec
def get_channel_context(self) -> str:
"""Load and return all channel guideline documents as a single context string."""
if self._channel_context is None:

View file

@ -84,6 +84,10 @@ async def handle_analyze_message(
}
})
# Extract brand selection for brand agent
brand = data.get("brand", "Barclaycard") # Default to Barclaycard if not specified
logger.info(f"[WEBSOCKET] Brand selection: {brand}")
# Run the analysis
logger.info("[WEBSOCKET] Starting analysis...")
result, pdf_pages = await analysis_service.analyze_proof(
@ -91,6 +95,7 @@ async def handle_analyze_message(
file_type=file_type,
on_agent_update=on_agent_update,
is_wip=is_wip,
brand=brand,
)
# Build the result dict

View file

@ -299,6 +299,7 @@ const App: React.FC = () => {
channel,
subChannel,
proofType,
brand: campaign.brandGuidelines,
});
const feedback = result.review;
@ -409,6 +410,7 @@ const App: React.FC = () => {
channel,
subChannel,
proofType,
brand: campaign.brandGuidelines,
});
// Refresh proofs from API to get the persisted data

View file

@ -15,6 +15,8 @@ export interface AnalyzeProofOptions {
channel?: string;
subChannel?: string;
proofType?: string;
/** Brand to use for brand guidelines analysis: 'Barclays' or 'Barclaycard' */
brand?: string;
}
/**
@ -78,6 +80,9 @@ export const analyzeProof = async (
if (options?.proofType) {
message.proof_type = options.proofType;
}
if (options?.brand) {
message.brand = options.brand;
}
ws.send(JSON.stringify(message));
};