-
Notifications
You must be signed in to change notification settings - Fork 10
feat(Prompt Iteration): Move processing to Celery #1023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
534acfc
5e96bc4
0f96ca1
5a71814
e8e441b
3e6118f
de1d463
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| """add PROMPT_IMPROVEMENT jobtype enum value | ||
|
|
||
| Revision ID: 074 | ||
| Revises: 073 | ||
| Create Date: 2026-07-09 00:00:00.000000 | ||
|
|
||
| Prompt improvement moved from a synchronous request to a Celery job, so its runs | ||
| are tracked in the `job` table under this new jobtype. ALTER TYPE ... ADD VALUE | ||
| must run outside a transaction, hence the autocommit block. | ||
| """ | ||
|
|
||
| from alembic import op | ||
|
|
||
| revision = "074" | ||
| down_revision = "073" | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| with op.get_context().autocommit_block(): | ||
| op.execute("ALTER TYPE jobtype ADD VALUE IF NOT EXISTS 'PROMPT_IMPROVEMENT'") | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| # Postgres cannot drop a value from an enum type; leaving it is harmless. | ||
| pass |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,59 @@ | ||
| Generate a new, AI-improved prompt iteration for the configuration that was evaluated by this run. | ||
| Enqueue an AI prompt-improvement job for the configuration that was evaluated by this run. | ||
|
|
||
| The run must have `status = completed` and a `score_trace_url` pointing to the stored trace file. No request body is required — the service reads the trace file directly from S3, uploads it to the Anthropic Files API, and asks Claude to identify low-performing answers (by scores and divergence from ground truth) and rewrite the system prompt to address them. Only `completion.params.instructions` is changed — model, knowledge base, and all other settings are preserved. | ||
| The heavy LLM round-trip runs on a background worker, so this endpoint validates preconditions and returns immediately with `202 Accepted` and a `job_id`. The run must have `status = completed` and a `score_trace_url` pointing to the stored trace file. The worker reads the trace file, asks Claude to identify low-performing answers (by scores and divergence from ground truth) and rewrite the system prompt. Only `completion.params.instructions` is changed; model, knowledge base, and all other settings are preserved. | ||
|
|
||
| The new prompt is persisted as the next `config_version` for the evaluated configuration. AI provenance and the source evaluation run id are recorded in the version's `commit_message` field (prefixed with `[AI Generated]` for auditability). | ||
| On success the new prompt is persisted as the next `config_version` for the evaluated configuration, with AI provenance and the source evaluation run id recorded in the version's `commit_message` (prefixed with `[AI Generated]`). | ||
|
|
||
| **Error codes:** | ||
| **Request body (required):** | ||
|
|
||
| ```json | ||
| { "callback_url": "https://your-service.example.com/webhooks/prompt-improvement" } | ||
| ``` | ||
|
|
||
| - `callback_url` — HTTPS webhook that receives the result. Must be `https://` and must not resolve to a private, loopback, or cloud-metadata address. | ||
|
|
||
| **Callback delivery:** | ||
|
|
||
| Once the worker finishes (success or failure), it POSTs a single JSON body to `callback_url`. Delivery is **best-effort, single-attempt** — there is no retry. The `config_version` is persisted regardless of callback outcome and remains recoverable from the configuration's version list, so a failed callback loses the notification, not the work. | ||
|
|
||
| When a webhook signing secret is configured for the project, the request carries `X-Webhook-Signature` (HMAC-SHA256 of the body) and `X-Webhook-Timestamp` headers; verify them before trusting the payload. | ||
|
|
||
| The body is a standard `APIResponse` envelope whose `data` is a `PromptImprovementJobPublic`: | ||
|
|
||
| ```json | ||
| { | ||
| "success": true, | ||
| "data": { | ||
| "job_id": "…", | ||
| "status": "SUCCESS", | ||
| "config_version": { "…": "…" }, | ||
| "error_message": null | ||
| }, | ||
| "error": null, | ||
| "metadata": null | ||
| } | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "success": false, | ||
| "data": { | ||
| "job_id": "…", | ||
| "status": "FAILED", | ||
| "config_version": null, | ||
| "error_message": "…" | ||
| }, | ||
| "error": "…", | ||
| "metadata": null | ||
| } | ||
| ``` | ||
|
|
||
| LLM and trace-download failures surface only via the failure callback, not on the `202` response. | ||
|
|
||
| **Validation errors (returned synchronously before the job is created):** | ||
| - `404 evaluation_not_found` — no run with this id in the caller's project. | ||
| - `409 evaluation_not_completed` — run is not yet completed. | ||
| - `409 source_config_unavailable` — the run's config or config_version has been deleted. | ||
| - `422 traces_not_available` — the run has no `score_trace_url`; trace file is required. | ||
| - `502 trace_download_failed` — the trace file could not be retrieved from storage. | ||
| - `502 prompt_generation_failed` — the platform Anthropic key is not configured, or the LLM call failed. | ||
| - `422 invalid_callback_url` — `callback_url` is missing, not `https://`, or resolves to a blocked (private/loopback/metadata) address. | ||
| - `500 prompt_improvement_enqueue_failed` — the job could not be queued. | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -246,6 +246,25 @@ def run_evaluation_batch_submission( | |||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Priority 6 (fast-eval tier): user-blocking interactive prompt iteration in the | ||||||||||||||||||||||||||
| # evaluation domain, above default batch work but below core LLM call/chain jobs. | ||||||||||||||||||||||||||
| @celery_app.task(bind=True, queue="default", priority=6) | ||||||||||||||||||||||||||
| @gevent_timeout(settings.CELERY_TASK_SOFT_TIME_LIMIT, "run_prompt_improvement") | ||||||||||||||||||||||||||
| def run_prompt_improvement(self, project_id: int, job_id: str, trace_id: str, **kwargs): | ||||||||||||||||||||||||||
|
Comment on lines
+249
to
+253
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add missing type hints and extract magic values. As per coding guidelines, every function parameter and return value must have a type hint, and magic values must be extracted to constants.
💡 Proposed fix (example) # Priority 6 (fast-eval tier): user-blocking interactive prompt iteration in the
# evaluation domain, above default batch work but below core LLM call/chain jobs.
-@celery_app.task(bind=True, queue="default", priority=6)
+FAST_EVAL_PRIORITY = 6
+
+@celery_app.task(bind=True, queue="default", priority=FAST_EVAL_PRIORITY)
`@gevent_timeout`(settings.CELERY_TASK_SOFT_TIME_LIMIT, "run_prompt_improvement")
-def run_prompt_improvement(self, project_id: int, job_id: str, trace_id: str, **kwargs):
+def run_prompt_improvement(self: Any, project_id: int, job_id: str, trace_id: str, **kwargs: Any) -> dict:
from app.services.evaluations.prompt_improvement import execute_prompt_improvement📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||
| from app.services.evaluations.prompt_improvement import execute_prompt_improvement | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| _set_trace(trace_id) | ||||||||||||||||||||||||||
| return _run_with_otel_parent( | ||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||
| lambda: execute_prompt_improvement( | ||||||||||||||||||||||||||
| project_id=project_id, | ||||||||||||||||||||||||||
| job_id=job_id, | ||||||||||||||||||||||||||
| task_id=current_task.request.id, | ||||||||||||||||||||||||||
| **kwargs, | ||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @celery_app.task(bind=True, queue="default", priority=2) | ||||||||||||||||||||||||||
| @gevent_timeout(settings.CELERY_TASK_SOFT_TIME_LIMIT, "run_stt_batch_submission") | ||||||||||||||||||||||||||
| def run_stt_batch_submission( | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -180,6 +180,24 @@ def start_evaluation_batch_submission( | |||||||||||||||||||||
| return task_id | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def start_prompt_improvement( | ||||||||||||||||||||||
| project_id: int, job_id: str, trace_id: str = "N/A", **kwargs | ||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||
| from app.celery.tasks.job_execution import run_prompt_improvement | ||||||||||||||||||||||
|
Comment on lines
+183
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add type hints for As per coding guidelines, every function parameter must have a type hint, and magic values should be extracted into constants.
💡 Proposed fix (example)+DEFAULT_TRACE_ID = "N/A"
+
def start_prompt_improvement(
- project_id: int, job_id: str, trace_id: str = "N/A", **kwargs
+ project_id: int, job_id: str, trace_id: str = DEFAULT_TRACE_ID, **kwargs: Any
) -> str:📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| task_id = _enqueue_with_trace_context( | ||||||||||||||||||||||
| run_prompt_improvement, | ||||||||||||||||||||||
| project_id=project_id, | ||||||||||||||||||||||
| job_id=job_id, | ||||||||||||||||||||||
| trace_id=trace_id, | ||||||||||||||||||||||
| **kwargs, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||
| f"[start_prompt_improvement] Started job {job_id} with Celery task {task_id}" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| return task_id | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def start_stt_batch_submission( | ||||||||||||||||||||||
| project_id: int, job_id: str, trace_id: str = "N/A", **kwargs | ||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Align both documents with the worker’s at-least-once callback behavior.
Celery redelivery can cause the same successful job callback to be sent again, so clients must tolerate duplicates and deduplicate by
job_id.backend/app/api/docs/evaluation/improve_prompt.md#L17-L17: replace “single-attempt”/“no retry” with explicit at-least-once semantics and duplicate-callback guidance.docs/wiki/modules/evaluations.md#L31-L31: replace “single best-effort callback” with the same at-least-once contract.📍 Affects 2 files
backend/app/api/docs/evaluation/improve_prompt.md#L17-L17(this comment)docs/wiki/modules/evaluations.md#L31-L31🤖 Prompt for AI Agents