Files
Alex 574f96341e refactor: rename the application package to docsgpt
The backend import package is now docsgpt, the name it will carry on PyPI;
application was far too generic to install into anyone's site-packages.
git mv plus a mechanical rewrite of every import, dotted string and path
reference: 734 Python files, the compose files, Dockerfile, workflows, docs,
setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage
config, .gitignore. Behaviour is unchanged.

Kept for one release:
- A top-level application package whose meta-path finder resolves
  application.x.y to the already-imported docsgpt.x.y object, so old imports
  and entry points (celery -A application.app.celery,
  uvicorn application.asgi:asgi_app) keep working with a FutureWarning.
- Celery registers every application.* task name as an alias of its
  docsgpt.* task on start-up, so messages queued by the previous release still
  run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries
  the previous release wrote are left unread instead of firing twice.

The backend image builds from the repository root (docker build -f
docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore
allow-lists docsgpt/ and application/ and keeps caches, local data, .env
files, the sample index files and the Dockerfile out. Compose and the image
workflows point at the new context.
2026-09-07 10:20:43 +01:00

148 lines
5.8 KiB
Python

"""0010 scheduler — schedules + schedule_runs tables.
Revision ID: 0010_schedules
Revises: 0009_tool_preferences
"""
from typing import Sequence, Union
from alembic import op
revision: str = "0010_schedules"
down_revision: Union[str, None] = "0009_tool_preferences"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"""
CREATE TABLE schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
trigger_type TEXT NOT NULL,
name TEXT,
instruction TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
cron TEXT,
run_at TIMESTAMPTZ,
timezone TEXT NOT NULL DEFAULT 'UTC',
next_run_at TIMESTAMPTZ,
last_run_at TIMESTAMPTZ,
end_at TIMESTAMPTZ,
tool_allowlist JSONB NOT NULL DEFAULT '[]'::jsonb,
model_id TEXT,
token_budget INTEGER,
origin_conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL,
created_via TEXT NOT NULL DEFAULT 'ui',
consecutive_failure_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT schedules_trigger_type_chk
CHECK (trigger_type IN ('once', 'recurring')),
CONSTRAINT schedules_status_chk
CHECK (status IN ('active', 'paused', 'completed', 'cancelled')),
CONSTRAINT schedules_created_via_chk
CHECK (created_via IN ('chat', 'ui')),
CONSTRAINT schedules_recurring_cron_chk
CHECK (trigger_type <> 'recurring' OR cron IS NOT NULL),
CONSTRAINT schedules_once_run_at_chk
CHECK (trigger_type <> 'once' OR run_at IS NOT NULL)
);
"""
)
op.execute(
"CREATE INDEX schedules_user_idx ON schedules (user_id);"
)
op.execute(
"CREATE INDEX schedules_agent_idx ON schedules (agent_id);"
)
# Dispatcher hot path: status='active' AND next_run_at <= now().
op.execute(
"CREATE INDEX schedules_due_idx "
"ON schedules (status, next_run_at) "
"WHERE status = 'active';"
)
op.execute(
"CREATE TRIGGER schedules_set_updated_at "
"BEFORE UPDATE ON schedules "
"FOR EACH ROW EXECUTE FUNCTION set_updated_at();"
)
op.execute(
"""
CREATE TABLE schedule_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
schedule_id UUID NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,
user_id TEXT NOT NULL,
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending',
scheduled_for TIMESTAMPTZ NOT NULL,
trigger_source TEXT NOT NULL DEFAULT 'cron',
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ,
output TEXT,
output_truncated BOOLEAN NOT NULL DEFAULT false,
error TEXT,
error_type TEXT,
prompt_tokens INTEGER NOT NULL DEFAULT 0,
generated_tokens INTEGER NOT NULL DEFAULT 0,
conversation_id UUID,
message_id UUID,
celery_task_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT schedule_runs_status_chk
CHECK (status IN (
'pending', 'running', 'success', 'failed', 'skipped', 'timeout'
)),
CONSTRAINT schedule_runs_trigger_source_chk
CHECK (trigger_source IN ('cron', 'manual')),
CONSTRAINT schedule_runs_error_type_chk
CHECK (error_type IS NULL OR error_type IN (
'auth_expired', 'tool_not_allowed', 'budget_exceeded',
'timeout', 'agent_error', 'internal', 'missed', 'overlap'
))
);
"""
)
# Dedup primitive: racing dispatchers hit ON CONFLICT on this index.
op.execute(
"CREATE UNIQUE INDEX schedule_runs_dedup_uidx "
"ON schedule_runs (schedule_id, scheduled_for);"
)
op.execute(
"CREATE INDEX schedule_runs_schedule_recent_idx "
"ON schedule_runs (schedule_id, scheduled_for DESC);"
)
op.execute(
"CREATE INDEX schedule_runs_user_idx ON schedule_runs (user_id);"
)
op.execute(
"CREATE INDEX schedule_runs_running_idx "
"ON schedule_runs (status, started_at) "
"WHERE status = 'running';"
)
op.execute(
"CREATE TRIGGER schedule_runs_set_updated_at "
"BEFORE UPDATE ON schedule_runs "
"FOR EACH ROW EXECUTE FUNCTION set_updated_at();"
)
def downgrade() -> None:
# Drop triggers explicitly (grep-able) before CASCADE-dropping the tables.
op.execute(
"DROP TRIGGER IF EXISTS schedule_runs_set_updated_at ON schedule_runs;"
)
op.execute("DROP TABLE IF EXISTS schedule_runs CASCADE;")
op.execute(
"DROP TRIGGER IF EXISTS schedules_set_updated_at ON schedules;"
)
op.execute("DROP TABLE IF EXISTS schedules CASCADE;")