Replaces the earlier
creative_concept_clusters Bob tool that returned anonymous integer cluster IDs. Materialized weekly so cluster names stay stable across calls in the same week.How it works
Naming pass
For each cluster the task asksgpt-4o-mini (cheap, fast):
name="Cluster N" and summary="". The page still renders it — you just lose the readable label until the next refresh.
What’s in a cluster
| Field | Type | Meaning |
|---|---|---|
concept_id | UUID | Primary key. |
client_id | string | Tenant. |
name | string | Human-readable name (≤ 160 chars). |
summary | text | 1–2 sentences. |
member_creative_ids | JSON list | All creative_ids in the cluster. |
member_count | int | Length of member_creative_ids. |
centroid | vector(1536) | k-means centroid (used for “find this concept’s nearest neighbors” if needed). |
spend_30d | float | Sum of member creatives’ last-30d spend. |
roas_30d | float | Average member ROAS, last 30 days. |
model | string | Naming-pass model (gpt-4o-mini). |
generated_at | datetime | When this row was written. |
status | string | ready or stale (set when last refresh found too few embeddings). |
Endpoints
Read-only list of materialized concepts for the tenant.Query params:
client_id— required (or implicit for single-tenant users).
{ "client_id": "...", "concepts": [...] } ordered by (spend_30d desc, roas_30d desc).POST /api/marketing_resources/creatives/concept-clusters/refresh/
Force a recluster outside the weekly schedule. Useful after a sweep of new creatives lands.Body:
{ "client_id": "..." }. Returns 202 Accepted with a Celery task id. The page polls for completion and reloads.shared.auth.get_accessible_client_ids.
Bob tool
Bubble chart UI
The Ad Creative Analytics page renders concepts as a Highcharts bubble chart:| Axis / encoding | Maps to |
|---|---|
| X axis | spend_30d (USD) |
| Y axis | roas_30d |
| Bubble size | member_count |
| Bubble color | Static brand purple (#a855f7) |
| Tooltip | name, summary, spend_30d, roas_30d, member_count |
Worked example
After a tenant’s Monday refresh, the page might show:| Cluster | Members | Spend (30d) | ROAS (30d) |
|---|---|---|---|
| Bright UGC product reveal — vertical | 12 | $4,820 | 2.6 |
| Founder talking heads — slow-burn pacing | 7 | $2,100 | 3.1 |
| Studio hero shot with bold typography | 9 | $3,200 | 1.4 |
| Before / after split-screen | 5 | $1,450 | 0.9 |
Cost & cadence
- Embedding the corpus is a one-time cost (~$0.0002 per creative; shared with similarity / search).
- k-means is local CPU compute (sklearn).
- Naming pass is the only ongoing cost: ~0.07 per tenant per month.
Tunable knobs
Constant (in refresh_creative_concepts.py) | Default | Purpose |
|---|---|---|
CONCEPT_NAMING_MODEL_DEFAULT | gpt-4o-mini | Name + summary model. Override via Django settings: CONCEPT_NAMING_MODEL. |
CLUSTER_SIZE_FLOOR | 3 | Drop clusters with fewer members. |
since_days (lookback) | 90 | Embeddings older than this are excluded. |
| k auto-pick | max(2, min(8, n_rows / 40 + 2)) | Force a fixed k via the --k arg on the management command. |
Where the code lives
marketing_resources/tasks/refresh_creative_concepts.py— the cron task + naming pass.marketing_resources/models/creative_concept.py— the row.marketing_resources/views/creative_concepts.py— the two endpoints.ai_chat/tools_creative.py:creative_concept_clusters— the Bob tool.
.png?fit=max&auto=format&n=Frm2GFbmok4D-yJA&q=85&s=93c3ebd47542af65d1cd06d8563a7f6e)