meta-optimizer/setup_baserow.py
José Manuel Gómez 8fb0b69896 Add Baserow persistence, Slack interactive reports, Streamlit dashboard, and full analysis pipeline
- Migrate from Airtable to Baserow: BaserowClient with snapshots, actions, creatives, logs
- Claude agents (Haiku for decisions/units, Sonnet for creatives) with cost_cap_eur vs CPL comparison
- Slack bot with colored action emojis, effect text before approval buttons, 500-char justifications
- Streamlit dashboard with date-range navigation, campaign drill-down (adsets/ads), Histórico tab
- Approval server (FastAPI + ngrok) for Slack button callbacks
- backfill.py for historical snapshot regeneration with Claude re-analysis
- Margin fix: 0-lead campaigns contribute -spend (not 0) to margin
- CTR fix: Meta returns CTR as percentage already, removed *100
- Parameter fix: pass decision parameter to Slack action for correct budget effect display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:30:13 +02:00

164 lines
6.4 KiB
Python

"""
One-time script: creates the meta_optimizer database in Baserow with all tables and fields.
Run once, then paste the printed IDs into your .env file.
Usage:
python setup_baserow.py
"""
import os
import sys
import requests
from dotenv import load_dotenv
load_dotenv()
BASE_URL = os.environ.get("BASEROW_URL", "").rstrip("/")
EMAIL = os.environ.get("BASEROW_EMAIL", "")
PASSWORD = os.environ.get("BASEROW_PASSWORD", "")
if not BASE_URL or not EMAIL or not PASSWORD:
print("Error: BASEROW_URL, BASEROW_EMAIL and BASEROW_PASSWORD must be set in your .env file.")
sys.exit(1)
# Authenticate to get a JWT token
_auth = requests.post(f"{BASE_URL}/api/user/token-auth/",
json={"email": EMAIL, "password": PASSWORD}, timeout=10)
if not _auth.ok:
print(f"Auth error: {_auth.text}")
sys.exit(1)
JWT = _auth.json()["access_token"]
HEADERS = {
"Authorization": f"JWT {JWT}",
"Content-Type": "application/json",
}
def api(method: str, path: str, **kwargs) -> dict:
url = f"{BASE_URL}/api{path}"
resp = requests.request(method, url, headers=HEADERS, **kwargs)
if not resp.ok:
print(f" API error {resp.status_code} {method} {path}: {resp.text[:300]}")
resp.raise_for_status()
return resp.json()
def setup_table(db_id: int, table_name: str, fields: list) -> int:
"""Create table, rename the auto-created primary field, then add remaining fields."""
t = api("POST", f"/database/tables/database/{db_id}/", json={"name": table_name})
table_id = t["id"]
print(f"\n Table: {table_name} (id={table_id})")
existing = api("GET", f"/database/fields/table/{table_id}/")
primary_id = existing[0]["id"]
first = fields[0]
api("PATCH", f"/database/fields/{primary_id}/", json={"name": first["name"], "type": first["type"]})
print(f" ~ primary field renamed to: {first['name']}")
for field in fields[1:]:
api("POST", f"/database/fields/table/{table_id}/", json=field)
print(f" + {field['name']}")
return table_id
# ── 1. Workspace ──────────────────────────────────────────────────────────────
workspaces = api("GET", "/workspaces/")
if not workspaces:
print("No workspaces found. Create one in Baserow first.")
sys.exit(1)
workspace_id = workspaces[0]["id"]
print(f"Workspace: {workspaces[0]['name']} (id={workspace_id})")
# ── 2. Database ───────────────────────────────────────────────────────────────
db = api("POST", f"/applications/workspace/{workspace_id}/", json={
"type": "database",
"name": "meta_optimizer",
})
db_id = db["id"]
print(f"Database: meta_optimizer (id={db_id})")
# ── 3. Tables ─────────────────────────────────────────────────────────────────
tid_campaigns = setup_table(db_id, "campaigns", [
{"name": "campaign_id", "type": "text"},
{"name": "name", "type": "text"},
{"name": "max_cpl", "type": "number", "number_decimal_places": 2},
{"name": "daily_budget", "type": "number", "number_decimal_places": 2},
{"name": "is_active", "type": "boolean"},
{"name": "notes", "type": "long_text"},
])
tid_actions = setup_table(db_id, "proposed_actions", [
{"name": "campaign_id", "type": "text"},
{"name": "campaign_name", "type": "text"},
{
"name": "action_type",
"type": "single_select",
"select_options": [
{"value": "PAUSE", "color": "red"},
{"value": "REDUCE_BUDGET", "color": "orange"},
{"value": "INCREASE_BUDGET", "color": "green"},
{"value": "MAINTAIN", "color": "blue"},
{"value": "REVIEW_CREATIVES", "color": "purple"},
],
},
{"name": "parameter", "type": "number", "number_decimal_places": 2},
{"name": "justification", "type": "long_text"},
{"name": "advice", "type": "long_text"},
{"name": "alert", "type": "long_text"},
{"name": "confidence", "type": "number", "number_decimal_places": 2},
{
"name": "status",
"type": "single_select",
"select_options": [
{"value": "pending", "color": "yellow"},
{"value": "approved", "color": "green"},
{"value": "rejected", "color": "red"},
{"value": "executed", "color": "blue"},
],
},
{"name": "proposed_at", "type": "date", "date_format": "ISO", "date_include_time": True},
{"name": "executed_at", "type": "date", "date_format": "ISO", "date_include_time": True},
{"name": "slack_message_ts", "type": "text"},
])
tid_creatives = setup_table(db_id, "creative_analyses", [
{"name": "ad_id", "type": "text"},
{"name": "ad_name", "type": "text"},
{"name": "campaign_id", "type": "text"},
{"name": "image_url", "type": "url"},
{"name": "analysis", "type": "long_text"},
{"name": "score", "type": "number", "number_decimal_places": 1},
{"name": "recommendations", "type": "long_text"},
{"name": "created_at", "type": "date", "date_format": "ISO", "date_include_time": True},
])
tid_logs = setup_table(db_id, "execution_logs", [
{"name": "executed_at", "type": "date", "date_format": "ISO", "date_include_time": True},
{"name": "mode", "type": "text"},
{"name": "campaigns_analyzed", "type": "number"},
{"name": "actions_proposed", "type": "number"},
{"name": "actions_executed", "type": "number"},
{"name": "errors", "type": "long_text"},
{"name": "summary", "type": "long_text"},
{"name": "duration_seconds", "type": "number", "number_decimal_places": 1},
])
# ── 4. Output env vars ────────────────────────────────────────────────────────
print(f"""
{'='*55}
Add these to your .env file:
{'='*55}
BASEROW_DB_ID={db_id}
BASEROW_TABLE_CAMPAIGNS={tid_campaigns}
BASEROW_TABLE_ACTIONS={tid_actions}
BASEROW_TABLE_CREATIVES={tid_creatives}
BASEROW_TABLE_LOGS={tid_logs}
{'='*55}
""")