Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,15 @@ cherry-evals/
│ └── ARCHITECTURE.md # Technical architecture and ADRs
├── api/ # FastAPI REST API layer
│ ├── main.py # App factory and router registration
│ ├── main.py # App factory, CORS, router registration
│ ├── deps.py # Auth, rate limits, quota enforcement
│ ├── routes/ # Endpoint definitions
│ │ └── analytics.py # Analytics endpoints
│ ├── models/ # Pydantic request/response schemas
│ └── deps.py # Dependency injection
│ │ ├── account.py # User profile + usage stats
│ │ ├── analytics.py # Analytics endpoints
│ │ ├── api_keys.py # API key CRUD
│ │ ├── billing.py # Polar.sh webhook
│ │ └── ... # collections, search, export, agents, etc.
│ └── models/ # Pydantic request/response schemas
├── cherry_evals/ # Core application package
│ ├── config.py # Pydantic settings
Expand Down Expand Up @@ -424,6 +428,21 @@ LANGFUSE_PUBLIC_KEY=your-public-key
LANGFUSE_SECRET_KEY=your-secret-key
LANGFUSE_BASE_URL=https://cloud.langfuse.com

# Auth (Supabase)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_JWT_SECRET=your-jwt-secret

# Billing (Polar.sh)
POLAR_WEBHOOK_SECRET=your-polar-webhook-secret
POLAR_PRO_PRODUCT_ID=your-polar-pro-product-id
POLAR_ULTRA_PRODUCT_ID=your-polar-ultra-product-id

# Auth toggle (default True — set False for local dev)
AUTH_ENABLED=True

# CORS origins (comma-separated)
CORS_ORIGINS=https://app.cherryevals.com,http://localhost:5173

# Optional
CHERRY_DATA_DIR=./data
CHERRY_LOG_LEVEL=INFO
Expand Down
2 changes: 2 additions & 0 deletions agents/export_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def _compile_convert_function(code: str) -> callable | None:
"""
import csv
import io
import re

safe_builtins = {
"True": True,
Expand Down Expand Up @@ -156,6 +157,7 @@ def _compile_convert_function(code: str) -> callable | None:
"json": json,
"csv": csv,
"io": io,
"re": re,
}
namespace: dict = {}
exec(code, safe_globals, namespace) # noqa: S102
Expand Down
4 changes: 3 additions & 1 deletion agents/ingestion_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,10 @@ def _compile_parse_function(code: str) -> callable | None:
}

try:
import re

namespace: dict = {}
exec(code, {"__builtins__": safe_builtins}, namespace) # noqa: S102
exec(code, {"__builtins__": safe_builtins, "re": re}, namespace) # noqa: S102
fn = namespace.get("parse_row")
if fn is None or not callable(fn):
logger.warning("Generated code does not define a callable 'parse_row'")
Expand Down
89 changes: 89 additions & 0 deletions alembic/versions/004_add_users_and_api_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""add users and api_keys tables

Revision ID: 004_add_users_and_api_keys
Revises: 003_add_curation_events
Create Date: 2026-03-10 00:00:00.000000

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "004_add_users_and_api_keys"
down_revision: str | Sequence[str] | None = "003_add_curation_events"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
op.create_table(
"users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("supabase_id", sa.String(length=255), nullable=False),
sa.Column("email", sa.String(length=255), nullable=False),
sa.Column("tier", sa.String(length=50), nullable=False, server_default="free"),
sa.Column("polar_customer_id", sa.String(length=255), nullable=True),
sa.Column("polar_subscription_id", sa.String(length=255), nullable=True),
sa.Column("subscription_status", sa.String(length=50), nullable=True),
sa.Column("llm_calls_today", sa.Integer(), nullable=False, server_default="0"),
sa.Column("semantic_searches_today", sa.Integer(), nullable=False, server_default="0"),
sa.Column(
"quota_reset_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False)
op.create_index(op.f("ix_users_supabase_id"), "users", ["supabase_id"], unique=True)

op.create_table(
"api_keys",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("key_prefix", sa.String(length=20), nullable=False),
sa.Column("key_hash", sa.String(length=64), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False, server_default="Default"),
sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_api_keys_id"), "api_keys", ["id"], unique=False)
op.create_index(op.f("ix_api_keys_user_id"), "api_keys", ["user_id"], unique=False)
op.create_index(op.f("ix_api_keys_key_hash"), "api_keys", ["key_hash"], unique=True)


def downgrade() -> None:
"""Downgrade schema."""
op.drop_index(op.f("ix_api_keys_key_hash"), table_name="api_keys")
op.drop_index(op.f("ix_api_keys_user_id"), table_name="api_keys")
op.drop_index(op.f("ix_api_keys_id"), table_name="api_keys")
op.drop_table("api_keys")
op.drop_index(op.f("ix_users_supabase_id"), table_name="users")
op.drop_index(op.f("ix_users_id"), table_name="users")
op.drop_table("users")
29 changes: 29 additions & 0 deletions alembic/versions/005_add_trial_ends_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add trial_ends_at to users

Revision ID: 005_add_trial_ends_at
Revises: 004_add_users_and_api_keys
Create Date: 2026-03-10 00:00:00.000000

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "005_add_trial_ends_at"
down_revision: str | Sequence[str] | None = "004_add_users_and_api_keys"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Add trial_ends_at column to users table."""
op.add_column("users", sa.Column("trial_ends_at", sa.DateTime(timezone=True), nullable=True))


def downgrade() -> None:
"""Remove trial_ends_at column from users table."""
op.drop_column("users", "trial_ends_at")
27 changes: 27 additions & 0 deletions alembic/versions/006_unique_user_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""add unique constraint on users.email

Revision ID: 006_unique_user_email
Revises: 005_add_trial_ends_at
Create Date: 2026-03-10 00:00:00.000000

"""

from collections.abc import Sequence

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "006_unique_user_email"
down_revision: str | Sequence[str] | None = "005_add_trial_ends_at"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Add UNIQUE constraint on users.email to prevent duplicate accounts."""
op.create_unique_constraint("uq_users_email", "users", ["email"])


def downgrade() -> None:
"""Remove UNIQUE constraint on users.email."""
op.drop_constraint("uq_users_email", "users", type_="unique")
Loading