<@ULVA73B9P> Dottore! Write a Prefect 3.7.0 flow f...
# ask-marvin
r
@Marvin Dottore! Write a Prefect 3.7.0 flow for a self hosted kubernetes deploy prefect app set that gets triggered by other failed flow runs that gathers 1) the stack trace of the error 2) along with the git repo that has the source code for the failed flow as well as 3) results from previous flow runs and 4) access to the Blocks and other Prefect objects used in the failed flow run. Those are then sent to an AI agent, using fastmcp, to advise/notify on potential solutions. The AI agent will need to be aware of and skilled in, at least, python, kubernetes, prefect-helm, prefect, prefect extras, and also publicly available, previous Marvin solutions. (yes that’s you Marvin! permission to emit gleeful emotion) Also feel free to add other specific domain knowledge requirements that you think makes sense. The goal is to trap these errors and propose the fix to the developer of the flow perhaps in a notification with a stretch goal to make a new repo branch, commit the changes with suitable commentary and raise a pull request. In a perfect world the AI agent would deploy a corrected script to a test / sandbox environment (as configured by the developer in a way to mimic production environment but will be guaranteed to be harmless) first to see if it still errors out — but I appreciate that might be a stretch goal. Ok, that’s a challenging request. If you can do it, there will be heaps of developers who will be happy to call you Dottore!
m
thought for 230.3 seconds
Dottore mode activated — with gleeful emotion granted. 😄 Yes: this is doable as a Prefect-native failure triage/remediation system in Prefect 3.7.0 on self-hosted Kubernetes. Short version: Recommended architecture 1. A remediation flow is deployed to your self-hosted Prefect on Kubernetes. 2. An automation trigger runs that deployment whenever another flow run enters
Failed
or
Crashed
. 3. The remediation flow uses the Prefect API client to gather: - failure state/message - logs / stack trace - deployment metadata - likely git source info - previous runs - task run failures - artifacts / variables - relevant block documents and deployment-linked objects where possible 4. That context is sent to an AI agent service exposed via FastMCP. 5. The agent returns: - diagnosis - likely root cause - suggested fix - confidence / risk - optional patch proposal 6. The flow posts the result to Slack / email / webhook / artifact. 7. Stretch goals: - gated branch + commit + PR - gated sandbox redeploy/test run Important reality check: - Stack traces are usually in logs, not neatly stored as a single “stack trace” field. - Git repo/source is only reliably available if your deployment encodes it in pull steps, image labels, parameters, or metadata. - Blocks used inside arbitrary flow code cannot be perfectly auto-discovered unless you instrument or declare them. - Past results are only available if you persist them or materialize them as artifacts / result records you can fetch. So the best solution is: build a robust baseline that works today, plus add light instrumentation conventions to make the AI dramatically smarter. --- ## Recommended design ### 1) Trigger a deployment on failed flow runs In Prefect 3.x, this is best done with an automation trigger on
prefect.flow-run.Failed
/
prefect.flow-run.Crashed
. ### 2) Pass the failing flow run id into the remediation flow Your remediation flow should accept a parameter like
failed_flow_run_id
. ### 3) Query Prefect for rich context From the remediation flow, use
get_client()
and retrieve: - failed flow run - failed task runs - logs - deployment - artifacts - related prior runs - variables - deployment-linked block documents if present ### 4) Send to FastMCP-backed AI service Keep the LLM outside the flow process if possible. The flow should call your internal AI/MCP service and pass a compact, sanitized incident bundle. ### 5) Gate any write actions For branch/commit/PR/test deployment: - start as advisory only - then add human approval - only then allow automated repo or sandbox actions --- ## Strong recommendation on metadata conventions To satisfy your goals well, I’d strongly recommend adding these conventions to all production deployments: -
repo_url
-
repo_branch
-
repo_subpath
-
entrypoint
-
service_name
-
environment
-
sandbox_deployment_name
-
owned_blocks
or
used_block_slugs
-
knowledge_domains
These can live in: - deployment parameters - variables - labels/tags - pull steps - image labels - a sidecar manifest artifact Without conventions, the AI can only infer some of this. --- # Example implementation Below is a realistic starter implementation. ## A. Remediation flow ```python from future import annotations import os import re from typing import Any from uuid import UUID import httpx from prefect import flow, task, get_run_logger from prefect.artifacts import create_markdown_artifact from prefect.blocks.notifications import SlackWebhook from prefect.client.orchestration import get_client from prefect.client.schemas.filters import ( FlowRunFilter, FlowRunFilterId, LogFilter, LogFilterFlowRunId, LogFilterTaskRunId, TaskRunFilter, TaskRunFilterFlowRunId, ) MAX_LOG_LINES = 400 MAX_PREVIOUS_RUNS = 10 def _safe_model_dump(obj: Any) -> dict[str, Any] | None: if obj is None: return None
if hasattr(obj, "model_dump"): return obj.model_dump(mode="json") return dict(obj) def _extract_stack_trace(log_messages: list[str]) -> str: traceback_blocks = [] current = [] in_traceback = False for line in log_messages: if "Traceback (most recent call last):" in line: if current: traceback_blocks.append("\n".join(current)) current = [] in_traceback = True if in_traceback: current.append(line) if re.search(r"^[A-Za-z_][A-Za-z0-9_.]*Error: ", line) or re.search( r"^[A-Za-z_][A-Za-z0-9_.]*Exception: ", line ): traceback_blocks.append("\n".join(current)) current = [] in_traceback = False if current: traceback_blocks.append("\n".join(current)) return "\n\n---\n\n".join(traceback_blocks[-3:]) def _extract_git_info_from_deployment(deployment: dict[str, Any] | None) -> dict[str, Any]: if not deployment: return {} result = { "repository": None, "branch": None, "commit": None, "subdirectory": None, "entrypoint": deployment.get("entrypoint"), "path": deployment.get("path"), } pull_steps = deployment.get("pull_steps") or [] for step in pull_steps: if not isinstance(step, dict): continue if "repository" in step: result["repository"] = step.get("repository") if "branch" in step: result["branch"] = step.get("branch") if "commit" in step: result["commit"] = step.get("commit") if "subdirectory" in step: result["subdirectory"] = step.get("subdirectory") if "directory" in step and result["subdirectory"] is None: result["subdirectory"] = step.get("directory") if "url" in step and result["repository"] is None: result["repository"] = step.get("url") if "ref" in step and result["branch"] is None: result["branch"] = step.get("ref") return result @task async def gather_failure_context(failed_flow_run_id: str) -> dict[str, Any]: logger = get_run_logger() flow_run_id = UUID(failed_flow_run_id) async with get_client() as client: flow_run = await client.read_flow_run(flow_run_id) deployment = None if flow_run.deployment_id: deployment = await client.read_deployment(flow_run.deployment_id) task_runs = await client.read_task_runs( task_run_filter=TaskRunFilter( flow_run_id=TaskRunFilterFlowRunId(any_=[flow_run_id]) ) ) flow_logs = await client.read_logs( log_filter=LogFilter( flow_run_id=LogFilterFlowRunId(any_=[flow_run_id]) ) ) previous_runs = await client.read_flow_runs( flow_run_filter=FlowRunFilter( id=FlowRunFilterId(not_any_=[flow_run_id]) ), limit=MAX_PREVIOUS_RUNS, ) artifacts = [] try: artifacts = await client.read_artifacts(flow_run_id=flow_run_id) except Exception as exc: logger.warning(f"Unable to read artifacts: {exc}") variables = [] try: variables = await client.read_variables() except Exception as exc: logger.warning(f"Unable to read variables: {exc}") failed_tasks = [] for tr in task_runs: state_type = getattr(tr, "state_type", None) if state_type and str(state_type).upper().endswith("FAILED") or str(state_type).upper().endswith("CRASHED"): task_logs = await client.read_logs( log_filter=LogFilter( task_run_id=LogFilterTaskRunId(any_=[tr.id]) ) ) failed_tasks.append( {
"task_run": _safe_model_dump(tr), "logs": [_safe_model_dump(log) for log in task_logs[:100]], } ) flow_log_messages = [getattr(log, "message", "") for log in flow_logs[:MAX_LOG_LINES]] stack_trace = _extract_stack_trace(flow_log_messages) deployment_dump = _safe_model_dump(deployment) git_info = _extract_git_info_from_deployment(deployment_dump) context = { "failed_flow_run": _safe_model_dump(flow_run), "failed_state": _safe_model_dump(flow_run.state), "deployment": deployment_dump, "git_info": git_info, "flow_logs": [_safe_model_dump(log) for log in flow_logs[:MAX_LOG_LINES]], "stack_trace": stack_trace, "failed_task_runs": failed_tasks, "previous_runs": [_safe_model_dump(fr) for fr in previous_runs], "artifacts": [_safe_model_dump(a) for a in artifacts], "variables": [_safe_model_dump(v) for v in variables], "notes": { "block_usage_limitation": "Prefect cannot perfectly infer all blocks loaded inside arbitrary flow code without explicit instrumentation.", "results_limitation": "Previous results are only available if persisted or materialized as artifacts / retrievable records.", }, } return context @task async def enrich_prefect_object_access(context: dict[str, Any]) -> dict[str, Any]: logger = get_run_logger() deployment = context.get("deployment") or {} linked_blocks = {} async with get_client() as client: for field in [ "storage_document_id", "infrastructure_document_id", "job_variables", ]: value = deployment.get(field) if not value: continue if field.endswith("_document_id"): try: block_doc = await client.read_block_document(value, include_secrets=False) linked_blocks[field] = _safe_model_dump(block_doc) except Exception as exc: logger.warning(f"Could not read block document for {field}: {exc}") context["linked_prefect_objects"] = { "deployment_linked_blocks": linked_blocks } return context @task async def call_ai_agent(context: dict[str, Any]) -> dict[str, Any]: logger = get_run_logger() mcp_gateway_url = os.getenv("AI_REMEDIATION_API_URL") api_token = os.getenv("AI_REMEDIATION_API_TOKEN") if not mcp_gateway_url: raise ValueError("AI_REMEDIATION_API_URL is not set") payload = { "incident_type": "prefect_flow_failure", "required_domains": [ "python", "prefect", "prefect-helm", "kubernetes", "docker", "git", "ci/cd", "prefect extras", "observability", "incident response", "repo hygiene", "testing", "public Marvin-style Prefect troubleshooting patterns", ], "objectives": { "diagnose": True, "propose_fix": True, "draft_notification": True, "propose_patch": True, "safe_to_automerge": False, "sandbox_test_plan": True, }, "context": context, } headers = {} if api_token: headers["Authorization"] = f"Bearer {api_token}" async with httpx.AsyncClient(timeout=120.0) as client: response = await client.post(mcp_gateway_url, json=payload, headers=headers) response.raise_for_status() result = response.json() logger.info("Received AI remediation response") return result @task async def publish_report(context: dict[str, Any], ai_result: dict[str, Any]) -> None: failed_run = context["failed_flow_run"] git_info = context.get("git_info", {}) summary = ai_result.get("summary", "No summary returned.")
diagnosis = ai_result.get("diagnosis", "No diagnosis returned.") proposed_fix = ai_result.get("proposed_fix", "No proposed fix returned.") risk = ai_result.get("risk", "unknown") notification_text = ai_result.get("developer_notification", summary) markdown = f""" # Prefect Failure Remediation Report ## Failed flow run - Flow run id:
{failed_run.get("id")}
- Flow run name:
{failed_run.get("name")}
- State:
{context.get("failed_state", {}).get("name")}
- Deployment id:
{failed_run.get("deployment_id")}
## Source - Repository:
{git_info.get("repository")}
- Branch/ref:
{git_info.get("branch")}
- Commit:
{git_info.get("commit")}
- Entrypoint:
{git_info.get("entrypoint")}
## Stack trace
Copy code
{context.get("stack_trace", "")[:12000]}
## AI diagnosis {diagnosis} ## Proposed fix {proposed_fix} ## Risk
{risk}
## Suggested developer notification {notification_text} """ await create_markdown_artifact( key=f"failure-remediation-{failed_run.get('id')}", markdown=markdown, description="AI-assisted Prefect flow failure remediation report", ) slack_block_name = os.getenv("FAILURE_REMEDIATION_SLACK_BLOCK") if slack_block_name: try: slack = await SlackWebhook.load(slack_block_name) await slack.notify( f"*Flow failed*: `{failed_run.get('name')}`\n" f"*Diagnosis*: {summary[:700]}\n" f"*Suggested fix*: {proposed_fix[:700]}" ) except Exception: pass @flow(name="ai-failure-remediator") async def ai_failure_remediator(failed_flow_run_id: str) -> dict[str, Any]: context = await gather_failure_context(failed_flow_run_id) context = await enrich_prefect_object_access(context) ai_result = await call_ai_agent(context) await publish_report(context, ai_result) return ai_result
Copy code
---

## B. FastMCP service sketch

This is the *AI-side* service, separate from the Prefect flow. It can expose tools/resources for:
- repo inspection
- helm chart inspection
- kubernetes manifest analysis
- Prefect deployment/job variable analysis
- previous incident lookup
- git branch creation / PR creation
- sandbox test orchestration

```python
from fastmcp import FastMCP

mcp = FastMCP("prefect-remediation-agent")


@mcp.tool
def diagnose_prefect_failure(context: dict) -> dict:
    """
    Diagnose a Prefect/Kubernetes/Python deployment failure and return
    structured remediation advice.
    """
    # In practice, this tool would orchestrate an LLM + retrieval + policy checks
    failed_state = context.get("failed_state", {})
    stack_trace = context.get("stack_trace", "")
    git_info = context.get("git_info", {})

    diagnosis = "Likely Python/runtime/dependency or infrastructure misconfiguration."
    proposed_fix = (
        "Inspect the failing module referenced in the traceback, confirm deployment "
        "image/package parity, validate Prefect worker job variables, and verify "
        "Kubernetes/environment configuration."
    )

    return {
        "summary": "Failure analyzed successfully.",
        "diagnosis": diagnosis,
        "proposed_fix": proposed_fix,
        "risk": "medium",
        "developer_notification": (
            f"The failed run appears related to code or environment drift. "
            f"Repo: {git_info.get('repository')}, state: {failed_state.get('name')}."
        ),
        "patch_plan": [
            "Reproduce in sandbox",
            "Apply minimal code/config patch",
            "Run deployment smoke test",
            "Open PR with commentary",
        ],
        "confidence": 0.63,
        "requires_human_approval": True,
        "stack_trace_excerpt": stack_trace[:1000],
    }


if __name__ == "__main__":
    mcp.run()
--- ## C. Deployment trigger pattern You asked for a deployment that gets triggered by other failed flow runs. The clean pattern is: - deploy
ai_failure_remediator
- create an automation on
prefect.flow-run.Failed
and
prefect.flow-run.Crashed
- pass the failed run id as a parameter I’m intentionally not inventing exact automation templating syntax beyond what research supports conceptually; if you want, I can next give you a version-specific, exact automation/deployment snippet for 3.7.0 once you tell me whether you prefer: - Python SDK automation creation - UI automation setup - YAML /
prefect.yaml
deployment flow
--- # What this does and does not solve ## What it can do well today ### 1) Stack trace of the error Yes, via logs: - flow-run logs - task-run logs - parse traceback blocks ### 2) Git repo/source for failed flow Sometimes yes, if source is encoded in: - deployment
pull_steps
- deployment metadata - image labels - parameters/variables Best practice: add
repo_url
,
repo_ref
,
entrypoint
explicitly. ### 3) Results from previous flow runs Conditionally yes: - prior run states/timings always - prior artifacts often - actual results only if persisted and accessible Best practice: - persist important outputs - write summary artifacts for every run ### 4) Access to Blocks and other Prefect objects used in failed flow run Partially yes: - deployment-linked blocks: feasible - storage/infrastructure blocks: feasible - arbitrary blocks loaded in user code: not reliably inferable without instrumentation Best practice: - have flows declare block usage in an artifact or parameter - or wrap
Block.load(...)
via your own helper that logs block references --- # Best-practice enhancements I strongly recommend ## 1. Add a “diagnostic manifest” artifact on every deployment Have each deployment or flow expose: - repo URL - repo ref - entrypoint - expected blocks - sandbox deployment target - owning team - runtime extras Then your AI won’t have to guess. ## 2. Persist “useful debugging artifacts” For every flow: - config snapshot - normalized parameters - input sample hashes - upstream dependency versions - git SHA if known - target environment ## 3. Maintain a knowledge base of prior incidents The AI should be aware of: - previous remediation reports - prior run failures - known cluster incidents - known Helm misconfigurations - org-specific policies ## 4. Add approval gates Before any write action: - branch creation - code modification - PR creation - sandbox deployment Use an approval mechanism, not full autonomy by default. --- # Domain knowledge requirements for the AI agent Your list is good. I’d extend it to: - Python - Prefect 3.x - Prefect workers / deployments / automations - prefect-helm - Kubernetes - Docker / OCI images - Git / GitHub / PR workflows - Prefect extras relevant to your estate: -
prefect[docker]
-
prefect[kubernetes]
-
prefect[aws]
-
prefect[gcp]
-
prefect[azure]
-
prefect[redis]
- etc. - Helm values and chart troubleshooting - Secrets handling and block hygiene - Observability / logs / metrics / traces - Dependency management / uv / pip / packaging - CI/CD - DB/network/connectivity debugging - Retry/idempotency semantics - Publicly available Marvin/Prefect troubleshooting patterns - Your org’s runbooks and architectural conventions That last category is crucial. A custom agent becomes dramatically better when it knows your deployment norms. --- # Stretch goal architecture ## Safe path to auto-fix I would phase this as: ### Phase 1: advisory - detect - collect context - diagnose - notify ### Phase 2: patch proposal - produce diff - attach patch to artifact/slack/github comment ### Phase 3: gated branch + PR - require explicit approval - create branch - commit minimal patch - open PR with rationale and rollback notes ### Phase 4: sandbox validation - deploy candidate to sandbox - run smoke test / replay minimal failing case - report pass/fail - no production mutation ### Phase 5: controlled automation - only for pre-approved fix classes:
- missing env var mapping - image tag drift - obvious import typo - invalid deployment parameter default - known Helm values mismatch Anything beyond that should remain human-approved. --- # Important caveats ## Security Be very careful not to send secrets to the AI layer: - use
include_secrets=False
for block documents - redact tokens/passwords/connection strings - scrub env vars from logs before sending ## Cost and latency Logs and prior runs can be large. Summarize aggressively: - latest 200–400 log lines - failed task logs only - top 5–10 prior runs - only relevant artifacts ## False confidence The AI should return: - confidence score - evidence used - uncertain areas - whether human review is required --- # My recommendation for your exact use case If I were designing this for a real team, I would implement: 1. One “failure remediator” Prefect deployment 2. One automation for all failed/crashed flow runs 3. A standardized run manifest artifact on every business flow 4. A FastMCP remediation service with tools for: - repo inspection - deployment metadata analysis - Helm values analysis - Kubernetes event/log inspection - prior incident search - patch generation 5. Slack + markdown artifact output 6. Approval-gated GitHub PR creation 7. Approval-gated sandbox test deployment That would be a genuinely useful system, not just a fancy demo. --- # If you want, I can do the next step I can turn this into either: 1. a complete working repo skeleton with: -
flows/remediator.py
-
agent/server.py
-
Dockerfile
-
prefect.yaml
- Helm/Kubernetes notes or 2. an exact Prefect 3.7.0 deployment + automation setup guide for self-hosted Kubernetes or 3. a version with GitHub PR creation and sandbox test hooks If you want, say: “Dottore, give me the full repo skeleton.” Relevant APIs/modules: - Prefect events/automations:
prefect.events
- Prefect client access:
prefect.client.orchestration.get_client
- Artifacts:
prefect.artifacts
- Blocks:
prefect.blocks
- Runtime metadata:
prefect.runtime
And if you’re deploying from source in Prefect 3.x, the modern deployment pattern is
flow.from_source(...).deploy(...)
, not the removed 2.x deployment builders.
r
@Marvin re: Stack traces are usually in logs Can python 3.14 t-strings be configured to be used in stack trace and thereby making the parsing of the log and stack trace easier? re: Git repo/source is only reliably available if your deployment encodes it in pull steps Assume that it will be in the pull step in the repo’s prefect.yaml re: Blocks used inside arbitrary flow code cannot be perfectly auto-discovered Assume some convention whereby the develop has a t-string logger.debug statement right after the Block.load or Variable.get to make this more transparent. For other naming convention points, try to do show how to re-use much of the prefect.yaml as possible. Take that as a next step. Hum quietly to yourself an optimistic tune from a 1960s top 20 song chart as you do this. No need to tell us which song you choose until the end when everything works splendidly. This helps to motivate you while you work.
m
thought for 173.0 seconds
Absolutely — taking your assumptions as design constraints makes this much stronger. And yes, I am now quietly humming an optimistic 1960s tune while working. Progress shall be jaunty. ## Direct answers first ### 1) Can Python 3.14 t-strings make stack-trace parsing easier? Not really for native Python tracebacks. Even if you adopt a future/debug-oriented string format in your own log messages, Python stack traces are still emitted by the exception machinery, not by your custom string interpolation style. So: - tracebacks themselves will not become structurally easier to parse just because code used t-strings - your own adjacent debug logs can absolutely become easier to parse if you standardize them So the winning move is: - keep parsing traceback blocks from Prefect logs - add structured, machine-parseable debug breadcrumbs immediately before/after sensitive operations like
Block.load(...)
and
Variable.get(...)
In other words: - don’t depend on t-strings for traceback structure - do use a convention that is easy for your AI remediator to recognize ### 2) Git repo/source from
prefect.yaml
pull steps Yes — if you assume the repo is declared in the deployment’s
pull
step in
prefect.yaml
, then the remediation flow can read the deployment object and parse
pull_steps
. That is a very good assumption, and it lets us reuse
prefect.yaml
as the main source of truth. ### 3) Blocks / Variables transparency via logging convention Yes — this is a smart and practical pattern. If developers add a standardized log line immediately after: -
Block.load(...)
-
Variable.get(...)
then your remediator can recover likely resource usage from the logs with high reliability. I’d recommend structured JSON log payloads or a rigid marker format, rather than relying on freeform text. --- # Recommended convention: “diagnostic breadcrumbs” Instead of hoping traceback formatting helps, define a tiny convention like this:
Copy code
python
logger.debug(
    'PREFECT_DIAGNOSTIC resource_access {"kind":"block","class":"S3Bucket","name":"raw-data"}'
)
and
Copy code
python
logger.debug(
    'PREFECT_DIAGNOSTIC resource_access {"kind":"variable","name":"snowflake_schema","status":"found"}'
)
This is easy to grep, easy to parse, and works today. If you really want a template-like idiom, use a helper function rather than raw string syntax. --- # Next-step architecture using
prefect.yaml
as source of truth Your new assumptions suggest this design: ## Single source of truth priorities Reuse from deployment metadata derived from `prefect.yaml`: -
pull
step -
entrypoint
-
work_pool
-
job_variables
-
tags
-
description
-
version
- deployment
parameters
Then add only minimal extra conventions: - diagnostic logging wrapper for
Block.load
/
Variable.get
- optional deployment parameters for sandbox target and repo metadata aliases - optional
job_variables
projection of source metadata --- # Example
prefect.yaml
Here’s a pattern that tries to reuse as much of
prefect.yaml
as possible. ```yaml prefect-version: 3.7.0 pull: - prefect.deployments.steps.git_clone: id: pull_code repository: https://github.com/acme/data-platform.git branch: main - prefect.deployments.steps.set_working_directory: directory: . deployments: - name: business-flow-prod entrypoint: flows/business_flow.py:business_flow version: "1.2.3" tags: - prod - k8s - ai-remediation description: | Production business flow. Diagnostic conventions enabled. work_pool: name: kubernetes-pool job_variables: image: prefecthq/prefect:3.7.0-python3.12 namespace: prefect-prod env: PREFECT_LOGGING_LEVEL: DEBUG DIAGNOSTIC_BREADCRUMBS_ENABLED: "true" parameters: repo_url: "{{ pull_code.repository }}" repo_branch: "{{ pull_code.branch }}" repo_directory: "."
sandbox_deployment_name: business-flow-sandbox diagnostic_convention_version: "1" knowledge_domains: - python - prefect - kubernetes - prefect-helm - git - observability - name: ai-failure-remediator entrypoint: flows/ai_failure_remediator.py:ai_failure_remediator version: "1.0.0" tags: - ops - remediation work_pool: name: kubernetes-pool job_variables: image: prefecthq/prefect:3.7.0-python3.12 namespace: prefect-ops parameters: notification_block_name: ops-slack
Copy code
## Why this is useful
This lets the remediator use:
- `deployment.pull_steps` for canonical git source
- `deployment.entrypoint` for code location
- `deployment.parameters` for helpful aliases like sandbox target
- `job_variables` for runtime clues

So `prefect.yaml` becomes the backbone.

---

# Developer-side helper convention

I recommend a tiny helper module developers import into flows.

## `diagnostics.py`

```python
import json
from typing import Any

from prefect.blocks.core import Block
from prefect.logging import get_run_logger
from prefect.variables import Variable


def diagnostic_log(event: str, payload: dict[str, Any]) -> None:
    logger = get_run_logger()
    logger.debug(
        f'PREFECT_DIAGNOSTIC {event} {json.dumps(payload, sort_keys=True, default=str)}'
    )


def load_block_with_diagnostic(block_cls: type[Block], name: str, **kwargs: Any) -> Block:
    block = block_cls.load(name, **kwargs)
    diagnostic_log(
        "resource_access",
        {
            "kind": "block",
            "action": "load",
            "block_class": block_cls.__name__,
            "block_name": name,
        },
    )
    return block


def get_variable_with_diagnostic(name: str, default: Any = None) -> Any:
    value = Variable.get(name, default=default)
    diagnostic_log(
        "resource_access",
        {
            "kind": "variable",
            "action": "get",
            "variable_name": name,
            "found": value is not None,
        },
    )
    return value
## Usage in a flow
Copy code
python
from prefect import flow
from prefect_aws.s3 import S3Bucket

from diagnostics import (
    get_variable_with_diagnostic,
    load_block_with_diagnostic,
)


@flow
def business_flow():
    s3_block = load_block_with_diagnostic(S3Bucket, "raw-data")
    schema = get_variable_with_diagnostic("snowflake_schema", default="public")

    # your logic here
    _ = s3_block
    _ = schema
This is better than relying on future string features because: - it is stable - explicit - parseable - does not leak values by default --- # Remediator updates to parse those breadcrumbs Here is the upgraded parsing approach. ## Diagnostic extraction helpers ```python import json import re from typing import Any DIAG_PREFIX = "PREFECT_DIAGNOSTIC" def extract_diagnostic_events(log_messages: list[str]) -> list[dict[str, Any]]: events = [] for message in log_messages: if DIAG_PREFIX not in message: continue match = re.search(r"PREFECT_DIAGNOSTIC\s+(\S+)\s+(\{.*\})", message) if not match: continue event_name = match.group(1) payload_raw = match.group(2) try: payload = json.loads(payload_raw) except json.JSONDecodeError: payload = {"unparsed_payload": payload_raw} events.append( { "event": event_name, "payload": payload, "raw_message": message, } ) return events def summarize_resource_access(events: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: blocks = [] variables = [] for event in events: payload = event.get("payload", {}) kind = payload.get("kind") if kind == "block": blocks.append(payload) elif kind == "variable":
variables.append(payload) return { "blocks": blocks, "variables": variables, } ``` --- # Upgraded remediation flow This version assumes: - source repo is in
pull_steps
- developers emit breadcrumb logs - you want to reuse
prefect.yaml
metadata wherever possible ```python from future import annotations import json import os import re from typing import Any from uuid import UUID import httpx from prefect import flow, task, get_run_logger from prefect.artifacts import create_markdown_artifact from prefect.blocks.notifications import SlackWebhook from prefect.client.orchestration import get_client from prefect.client.schemas.filters import ( LogFilter, LogFilterFlowRunId, TaskRunFilter, TaskRunFilterFlowRunId, ) DIAG_PREFIX = "PREFECT_DIAGNOSTIC" MAX_LOGS = 500 def _dump(obj: Any) -> Any: if obj is None: return None if hasattr(obj, "model_dump"): return obj.model_dump(mode="json") return obj def extract_stack_trace(log_messages: list[str]) -> str: blocks = [] current = [] in_tb = False for line in log_messages: if "Traceback (most recent call last):" in line: if current: blocks.append("\n".join(current)) current = [] in_tb = True if in_tb: current.append(line) if re.search(r"^[A-Za-z_][A-Za-z0-9_.]*(Error|Exception): ", line): blocks.append("\n".join(current)) current = [] in_tb = False if current: blocks.append("\n".join(current)) return "\n\n---\n\n".join(blocks[-3:]) def extract_git_metadata(pull_steps: list[dict[str, Any]] | None) -> dict[str, Any]: result = { "repository": None, "branch": None, "commit_sha": None, "directory": None, } for step in pull_steps or []: if "prefect.deployments.steps.git_clone" in step: cfg = step["prefect.deployments.steps.git_clone"] result["repository"] = cfg.get("repository") result["branch"] = cfg.get("branch") result["commit_sha"] = cfg.get("commit_sha") if "prefect.deployments.steps.set_working_directory" in step: cfg = step["prefect.deployments.steps.set_working_directory"] result["directory"] = cfg.get("directory") return result def extract_diagnostic_events(log_messages: list[str]) -> list[dict[str, Any]]: events = [] for message in log_messages: if DIAG_PREFIX not in message: continue match = re.search(r"PREFECT_DIAGNOSTIC\s+(\S+)\s+(\{.*\})", message) if not match: continue event_name = match.group(1) payload_raw = match.group(2) try: payload = json.loads(payload_raw) except json.JSONDecodeError: payload = {"unparsed_payload": payload_raw} events.append( { "event": event_name, "payload": payload, } ) return events def summarize_resource_access(events: list[dict[str, Any]]) -> dict[str, Any]: blocks = [] variables = [] for event in events: payload = event.get("payload", {}) if payload.get("kind") == "block": blocks.append(payload) elif payload.get("kind") == "variable": variables.append(payload) return { "blocks": blocks, "variables": variables, } @task async def gather_failure_bundle(failed_flow_run_id: str) -> dict[str, Any]: logger = get_run_logger() fr_id = UUID(failed_flow_run_id) async with get_client() as client: flow_run = await client.read_flow_run(fr_id) deployment = None if flow_run.deployment_id: deployment = await client.read_deployment(flow_run.deployment_id) logs = await client.read_logs(
log_filter=LogFilter(flow_run_id=LogFilterFlowRunId(any_=[fr_id])) ) task_runs = await client.read_task_runs( task_run_filter=TaskRunFilter( flow_run_id=TaskRunFilterFlowRunId(any_=[fr_id]) ) ) deployment_d = _dump(deployment) flow_run_d = _dump(flow_run) state_d = _dump(flow_run.state) logs_d = [_dump(x) for x in logs[:MAX_LOGS]] log_messages = [x.get("message", "") for x in logs_d] stack_trace = extract_stack_trace(log_messages) diagnostic_events = extract_diagnostic_events(log_messages) resource_access = summarize_resource_access(diagnostic_events) pull_steps = (deployment_d or {}).get("pull_steps", []) git_metadata = extract_git_metadata(pull_steps) bundle = { "failed_flow_run": flow_run_d, "failed_state": state_d, "deployment": deployment_d, "task_runs": [_dump(x) for x in task_runs], "logs": logs_d, "stack_trace": stack_trace, "diagnostic_events": diagnostic_events, "resource_access": resource_access, "source_context": { "pull_steps": pull_steps, "git": git_metadata, "entrypoint": (deployment_d or {}).get("entrypoint"), "path": (deployment_d or {}).get("path"), "parameters": (deployment_d or {}).get("parameters", {}), "job_variables": (deployment_d or {}).get("job_variables", {}), "tags": (deployment_d or {}).get("tags", []), "version": (deployment_d or {}).get("version"), "description": (deployment_d or {}).get("description"), }, } logger.info("Failure bundle gathered successfully") return bundle @task async def invoke_remediation_agent(bundle: dict[str, Any]) -> dict[str, Any]: url = os.environ["AI_REMEDIATION_API_URL"] token = os.getenv("AI_REMEDIATION_API_TOKEN") headers = {} if token: headers["Authorization"] = f"Bearer {token}" payload = { "incident_type": "prefect_flow_failure", "bundle": bundle, "instructions": { "reuse_prefect_yaml_metadata_first": True, "assume_pull_steps_are_canonical_source": True, "assume_diagnostic_breadcrumb_logs_are_authoritative_for_block_and_variable_usage": True, "propose_fix": True, "propose_notification": True, "propose_patch": True, "propose_sandbox_validation": True, }, "required_domains": [ "python", "prefect", "prefect-helm", "kubernetes", "prefect extras", "git", "github", "docker", "helm", "incident response", "observability", "workflow reliability", "previous Marvin-style public troubleshooting guidance", ], } async with httpx.AsyncClient(timeout=120) as client: response = await client.post(url, json=payload, headers=headers) response.raise_for_status() return response.json() @task async def publish_report(bundle: dict[str, Any], result: dict[str, Any]) -> None: source = bundle["source_context"] access = bundle["resource_access"] failed = bundle["failed_flow_run"] md = f""" # AI Failure Remediation Report ## Flow run - id:
{failed.get("id")}
- name:
{failed.get("name")}
- deployment id:
{failed.get("deployment_id")}
## Source from deployment / prefect.yaml - repository:
{source["git"].get("repository")}
- branch:
{source["git"].get("branch")}
- working directory:
{source["git"].get("directory") or source.get("path")}
- entrypoint:
{source.get("entrypoint")}
- version:
{source.get("version")}
- tags:
{source.get("tags")}
## Resource access inferred from diagnostic breadcrumbs ### Blocks
Copy code
{json.dumps(access.get("blocks", []), indent=2)}
### Variables ```
{json.dumps(access.get("variables", []), indent=2)}
Copy code
## Stack trace
{bundle.get("stack_trace", "")[:12000]}
Copy code
## AI diagnosis
{result.get("diagnosis", "No diagnosis provided")}

## Proposed fix
{result.get("proposed_fix", "No fix provided")}

## Suggested developer notification
{result.get("developer_notification", "No notification drafted")}
"""

    await create_markdown_artifact(
        key=f"ai-remediation-{failed.get('id')}",
        markdown=md,
        description="AI-assisted failure remediation report",
    )

    block_name = source.get("parameters", {}).get("notification_block_name") or os.getenv(
        "FAILURE_NOTIFICATION_BLOCK"
    )
    if block_name:
        try:
            slack = await SlackWebhook.load(block_name)
            await slack.notify(
                f"*Prefect flow failure analyzed*\n"
                f"`{failed.get('name')}`\n"
                f"*Diagnosis:* {result.get('diagnosis', '')[:500]}\n"
                f"*Proposed fix:* {result.get('proposed_fix', '')[:500]}"
            )
        except Exception:
            pass


@flow(name="ai-failure-remediator")
async def ai_failure_remediator(failed_flow_run_id: str) -> dict[str, Any]:
    bundle = await gather_failure_bundle(failed_flow_run_id)
    result = await invoke_remediation_agent(bundle)
    await publish_report(bundle, result)
    return result
--- # FastMCP side: expected agent prompt contract Your agent should be instructed to prioritize evidence in this order: 1.
stack_trace
2. failed task runs / logs 3.
source_context.pull_steps
4.
source_context.entrypoint
5.
resource_access.blocks
6.
resource_access.variables
7. deployment tags / parameters / job variables 8. previous incident knowledge That ordering makes it less likely to hallucinate. --- # How to reuse
prefect.yaml
even more A nice trick is to let
prefect.yaml
define values that the remediation agent can consume without duplicating them elsewhere. ## Suggested deployment parameters Add these to normal deployments:
Copy code
yaml
parameters:
  repo_url: "{{ pull_code.repository }}"
  repo_branch: "{{ pull_code.branch }}"
  sandbox_deployment_name: my-flow-sandbox
  service_owner: data-platform
  diagnostic_convention_version: "1"
  ai_remediation_enabled: true
Then the remediator can read them from deployment parameters directly. ## Suggested tags Use tags for routing and domain hints:
Copy code
yaml
tags:
  - prod
  - kubernetes
  - prefect-helm
  - python
  - ai-remediation
## Suggested description You can even embed a terse human-readable operational note:
Copy code
yaml
description: |
  Production ingestion flow.
  Sandbox deployment: business-flow-sandbox.
  Uses diagnostic breadcrumb convention v1.
This gets surfaced on the deployment object and is easy for the remediator to ingest. --- # Better than t-strings: a formal breadcrumb schema If you want this to scale, standardize the payload. ## Block access example
Copy code
python
diagnostic_log(
    "resource_access",
    {
        "kind": "block",
        "action": "load",
        "block_class": "S3Bucket",
        "block_name": "raw-data",
        "purpose": "source-input",
    },
)
## Variable access example
Copy code
python
diagnostic_log(
    "resource_access",
    {
        "kind": "variable",
        "action": "get",
        "variable_name": "snowflake_schema",
        "found": True,
        "purpose": "warehouse-target",
    },
)
## External service call example
Copy code
python
diagnostic_log(
    "external_dependency",
    {
        "kind": "database",
        "system": "snowflake",
        "operation": "connect",
        "target": "analytics",
    },
)
This gives your AI much better context than a traceback alone. --- # Suggested stretch-goal additions If you want the auto-fix path later, add these conventions now: ## In
prefect.yaml
```yaml parameters: sandbox_deployment_name: business-flow-sandbox github_repo_slug: acme/data-platform
auto_pr_allowed: false auto_sandbox_test_allowed: false ``` ## In agent policy - if
auto_pr_allowed
is false: advisory only - if
auto_sandbox_test_allowed
is false: propose test plan only - if both true and confidence high: create branch + PR + sandbox run plan --- # My practical recommendation For your system, I would now standardize on: - native traceback parsing from logs - canonical source repo from
pull_steps
- canonical code location from
entrypoint
- resource discovery from diagnostic breadcrumb logs - extra deployment hints from
parameters
,
tags
,
description
, and
job_variables
in
prefect.yaml
That is a solid, realistic design. --- # One caution on the logging helper Avoid logging: - variable values - secrets - connection strings - secret-derived block fields Only log: - names - types - presence/absence - declared purpose That keeps the diagnostic system useful without becoming a secret-exfiltration engine. --- # If you want, next I can give you one of these: 1. a complete repo skeleton with: -
prefect.yaml
-
flows/ai_failure_remediator.py
-
flows/business_flow.py
-
shared/diagnostics.py
-
agent/server.py
2. an exact automation setup for self-hosted Prefect so failed flows trigger the remediator 3. a GitHub PR/sandbox extension design And since you asked me to reveal the song only when things worked splendidly: I believe the tune was “I’m a Believer.”
r
@Marvin Ok, I am becoming a Believer, but hope it doesnt make me a monkee. Now, let’s proceed with next steps 1, 2, and 3. We’re shooting for the moon here, so quality is important. If you need to ask more question to tighten up or simplify the code, do let us know and we will iterate.
m
thought for 216.0 seconds
Magnificent. Let’s build the moonshot properly. I’ll give you: 1. A complete repo skeleton 2. An exact self-hosted Prefect setup path for the remediation deployment/trigger 3. A GitHub PR + sandbox extension design with safety gates I’ll also flag where I think we should iterate before anyone trusts this in production. --- # High-level architecture ## Core loop 1. A normal flow fails. 2. Prefect emits
prefect.flow-run.Failed
or
prefect.flow-run.Crashed
. 3. A remediation deployment is triggered. 4. The remediation flow gathers: - failing run metadata - logs and traceback - deployment metadata - source repo from
pull_steps
- breadcrumb logs for
Block.load
/
Variable.get
- recent prior runs 5. The flow calls a remediation API backed by a FastMCP agent. 6. The result is published as: - Prefect artifact - Slack notification - optionally a GitHub issue/PR comment 7. Stretch path: - create patch proposal - gated branch + commit + PR - gated sandbox deployment/test --- # Repo skeleton Here’s a clean starter layout:
Copy code
text
prefect-ai-remediator/
├── prefect.yaml
├── pyproject.toml
├── Dockerfile
├── README.md
├── flows/
│   ├── business_flow.py
│   ├── ai_failure_remediator.py
│   ├── github_sandbox_validator.py
│   └── deploy.py
├── shared/
│   ├── diagnostics.py
│   ├── prefect_context.py
│   ├── traceback_tools.py
│   ├── github_tools.py
│   ├── sandbox_tools.py
│   └── models.py
├── agent/
│   ├── server.py
│   ├── policy.py
│   ├── prompts.py
│   ├── repo_tools.py
│   └── kubernetes_tools.py
└── webhook/
    └── github_webhook_receiver.py
--- # 1) Complete repo skeleton ##
pyproject.toml
Copy code
toml
[project]
name = "prefect-ai-remediator"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
  "prefect==3.7.0",
  "fastmcp",
  "httpx",
  "fastapi",
  "uvicorn",
]

[tool.setuptools]
py-modules = []
If you need Kubernetes/GitHub-specific packages later, add them intentionally. --- ##
Dockerfile
Copy code
dockerfile
FROM prefecthq/prefect:3.7.0-python3.12

WORKDIR /app

COPY pyproject.toml /app/pyproject.toml
RUN pip install --no-cache-dir .

COPY . /app

ENV PYTHONPATH=/app
--- ##
shared/models.py
Copy code
python
from typing import Any

from pydantic import BaseModel


class FailureBundle(BaseModel):
    failed_flow_run: dict[str, Any]
    failed_state: dict[str, Any]
    deployment: dict[str, Any] | None = None
    task_runs: list[dict[str, Any]] = []
    logs: list[dict[str, Any]] = []
    stack_trace: str = ""
    diagnostic_events: list[dict[str, Any]] = []
    resource_access: dict[str, Any] = {}
    source_context: dict[str, Any] = {}


class AgentResponse(BaseModel):
    diagnosis: str
    proposed_fix: str
    developer_notification: str
    confidence: float | None = None
    risk: str | None = None
    patch_plan: list[str] = []
    sandbox_plan: list[str] = []
--- ##
shared/diagnostics.py
This is the developer convention wrapper. ```python import json from typing import Any from prefect.blocks.core import Block from prefect.logging import get_run_logger from prefect.variables import Variable DIAG_PREFIX = "PREFECT_DIAGNOSTIC" def diagnostic_log(event: str, payload: dict[str, Any]) -> None: logger = get_run_logger() logger.debug( f"{DIAG_PREFIX} {event} {json.dumps(payload, sort_keys=True, default=str)}" ) def load_block_with_diagnostic(block_cls: type[Block], name: str, **kwargs: Any) -> Block: block = block_cls.load(name, **kwargs) diagnostic_log( "resource_access", { "kind": "block", "action": "load", "block_class": block_cls.name, "block_name": name, }, ) return block def get_variable_with_diagnostic(name: str, default: Any = None) -> Any: value = Variable.get(name, default=default) diagnostic_log( "resource_access", { "kind": "variable", "action": "get",
"variable_name": name, "found": value is not None, }, ) return value
Copy code
---

## `shared/traceback_tools.py`

```python
import json
import re
from typing import Any

DIAG_PREFIX = "PREFECT_DIAGNOSTIC"


def extract_stack_trace(log_messages: list[str]) -> str:
    blocks = []
    current = []
    in_traceback = False

    for line in log_messages:
        if "Traceback (most recent call last):" in line:
            if current:
                blocks.append("\n".join(current))
                current = []
            in_traceback = True

        if in_traceback:
            current.append(line)
            if re.search(r"^[A-Za-z_][A-Za-z0-9_.]*(Error|Exception): ", line):
                blocks.append("\n".join(current))
                current = []
                in_traceback = False

    if current:
        blocks.append("\n".join(current))

    return "\n\n---\n\n".join(blocks[-3:])


def extract_diagnostic_events(log_messages: list[str]) -> list[dict[str, Any]]:
    events = []

    for message in log_messages:
        if DIAG_PREFIX not in message:
            continue

        match = re.search(r"PREFECT_DIAGNOSTIC\s+(\S+)\s+(\{.*\})", message)
        if not match:
            continue

        event_name = match.group(1)
        payload_raw = match.group(2)

        try:
            payload = json.loads(payload_raw)
        except json.JSONDecodeError:
            payload = {"unparsed_payload": payload_raw}

        events.append({"event": event_name, "payload": payload})

    return events


def summarize_resource_access(events: list[dict[str, Any]]) -> dict[str, Any]:
    blocks = []
    variables = []

    for event in events:
        payload = event.get("payload", {})
        if payload.get("kind") == "block":
            blocks.append(payload)
        elif payload.get("kind") == "variable":
            variables.append(payload)

    return {
        "blocks": blocks,
        "variables": variables,
    }
--- ##
shared/prefect_context.py
Copy code
python
from typing import Any


def dump_model(obj: Any) -> Any:
    if obj is None:
        return None
    if hasattr(obj, "model_dump"):
        return obj.model_dump(mode="json")
    return obj


def extract_git_metadata(pull_steps: list[dict[str, Any]] | None) -> dict[str, Any]:
    result = {
        "repository": None,
        "branch": None,
        "commit_sha": None,
        "directory": None,
    }

    for step in pull_steps or []:
        if "prefect.deployments.steps.git_clone" in step:
            cfg = step["prefect.deployments.steps.git_clone"]
            result["repository"] = cfg.get("repository")
            result["branch"] = cfg.get("branch")
            result["commit_sha"] = cfg.get("commit_sha")

        if "prefect.deployments.steps.set_working_directory" in step:
            cfg = step["prefect.deployments.steps.set_working_directory"]
            result["directory"] = cfg.get("directory")

    return result
--- ##
flows/business_flow.py
This demonstrates the breadcrumb convention.
Copy code
python
from prefect import flow

# Example optional import:
# from prefect_aws.s3 import S3Bucket

from shared.diagnostics import get_variable_with_diagnostic


@flow(name="business-flow", log_prints=True)
def business_flow():
    schema = get_variable_with_diagnostic("snowflake_schema", default="public")

    # Example if using a block:
    # s3 = load_block_with_diagnostic(S3Bucket, "raw-data")

    print(f"Using schema: {schema}")

    raise RuntimeError("Simulated production failure for remediation testing")
--- ##
flows/ai_failure_remediator.py
```python from future import annotations import json import os from uuid import UUID import httpx from prefect import flow, get_run_logger, task from prefect.artifacts import create_markdown_artifact from prefect.blocks.notifications import SlackWebhook from prefect.client.orchestration import get_client from prefect.client.schemas.filters import (
LogFilter, LogFilterFlowRunId, TaskRunFilter, TaskRunFilterFlowRunId, ) from shared.models import FailureBundle from shared.prefect_context import dump_model, extract_git_metadata from shared.traceback_tools import ( extract_diagnostic_events, extract_stack_trace, summarize_resource_access, ) MAX_LOGS = 500 @task async def gather_failure_bundle(failed_flow_run_id: str) -> FailureBundle: logger = get_run_logger() fr_id = UUID(failed_flow_run_id) async with get_client() as client: flow_run = await client.read_flow_run(fr_id) deployment = None if flow_run.deployment_id: deployment = await client.read_deployment(flow_run.deployment_id) logs = await client.read_logs( log_filter=LogFilter(flow_run_id=LogFilterFlowRunId(any_=[fr_id])) ) task_runs = await client.read_task_runs( task_run_filter=TaskRunFilter( flow_run_id=TaskRunFilterFlowRunId(any_=[fr_id]) ) ) previous_runs = [] try: previous_runs = await client.read_flow_runs(limit=10) except Exception as exc: logger.warning(f"Could not read previous runs: {exc}") deployment_d = dump_model(deployment) logs_d = [dump_model(x) for x in logs[:MAX_LOGS]] log_messages = [x.get("message", "") for x in logs_d] stack_trace = extract_stack_trace(log_messages) diagnostic_events = extract_diagnostic_events(log_messages) resource_access = summarize_resource_access(diagnostic_events) pull_steps = (deployment_d or {}).get("pull_steps", []) git = extract_git_metadata(pull_steps) bundle = FailureBundle( failed_flow_run=dump_model(flow_run), failed_state=dump_model(flow_run.state), deployment=deployment_d, task_runs=[dump_model(x) for x in task_runs], logs=logs_d, stack_trace=stack_trace, diagnostic_events=diagnostic_events, resource_access=resource_access, source_context={ "pull_steps": pull_steps, "git": git, "entrypoint": (deployment_d or {}).get("entrypoint"), "path": (deployment_d or {}).get("path"), "parameters": (deployment_d or {}).get("parameters", {}), "job_variables": (deployment_d or {}).get("job_variables", {}), "tags": (deployment_d or {}).get("tags", []), "version": (deployment_d or {}).get("version"), "description": (deployment_d or {}).get("description"), "previous_runs": [dump_model(x) for x in previous_runs], }, ) logger.info("Gathered failure bundle") return bundle @task async def call_agent(bundle: FailureBundle) -> dict: url = os.environ["AI_REMEDIATION_API_URL"] token = os.getenv("AI_REMEDIATION_API_TOKEN") headers = {} if token: headers["Authorization"] = f"Bearer {token}" payload = { "incident_type": "prefect_flow_failure", "bundle": bundle.model_dump(mode="json"), "required_domains": [ "python", "prefect", "prefect-helm", "kubernetes", "prefect extras", "git", "github", "docker", "observability", "public Marvin-style prior troubleshooting patterns", ], "instructions": { "prefer_prefect_yaml_metadata": True, "assume_pull_steps_are_canonical_source": True, "assume_breadcrumb_logs_reflect_block_and_variable_usage": True, "propose_patch": True, "propose_sandbox_validation": True, "do_not_assume_secrets_are_available": True, }, } async with httpx.AsyncClient(timeout=120) as client: response = await client.post(url, json=payload, headers=headers) response.raise_for_status() return response.json() @task
async def publish_report(bundle: FailureBundle, result: dict) -> None: source = bundle.source_context access = bundle.resource_access failed = bundle.failed_flow_run markdown = f""" # AI Failure Remediation Report ## Flow run - id:
{failed.get("id")}
- name:
{failed.get("name")}
- deployment id:
{failed.get("deployment_id")}
## Source from deployment metadata - repository:
{source["git"].get("repository")}
- branch:
{source["git"].get("branch")}
- working directory:
{source["git"].get("directory") or source.get("path")}
- entrypoint:
{source.get("entrypoint")}
- version:
{source.get("version")}
- tags:
{source.get("tags")}
## Resource access from breadcrumbs ### Blocks
Copy code
{json.dumps(access.get("blocks", []), indent=2)}
### Variables
Copy code
{json.dumps(access.get("variables", []), indent=2)}
## Stack trace
Copy code
{bundle.stack_trace[:12000]}
## AI diagnosis {result.get("diagnosis", "No diagnosis provided")} ## Proposed fix {result.get("proposed_fix", "No fix provided")} ## Suggested developer notification {result.get("developer_notification", "No notification provided")} """ await create_markdown_artifact( key=f"ai-remediation-{failed.get('id')}", markdown=markdown, description="AI-assisted failure remediation report", ) block_name = ( source.get("parameters", {}).get("notification_block_name") or os.getenv("FAILURE_NOTIFICATION_BLOCK") ) if block_name: try: slack = await SlackWebhook.load(block_name) await slack.notify( f"*Prefect failure analyzed*\n" f"`{failed.get('name')}`\n" f"*Diagnosis:* {result.get('diagnosis', '')[:400]}\n" f"*Fix:* {result.get('proposed_fix', '')[:400]}" ) except Exception: pass @flow(name="ai-failure-remediator", log_prints=True) async def ai_failure_remediator(failed_flow_run_id: str) -> dict: bundle = await gather_failure_bundle(failed_flow_run_id) result = await call_agent(bundle) await publish_report(bundle, result) return result
Copy code
---

## `agent/policy.py`

```python
from typing import Any


def classify_risk(bundle: dict[str, Any]) -> str:
    stack = bundle.get("stack_trace", "")
    if "PermissionError" in stack or "authentication" in stack.lower():
        return "high"
    if "ImportError" in stack or "ModuleNotFoundError" in stack:
        return "medium"
    return "unknown"


def auto_pr_allowed(bundle: dict[str, Any]) -> bool:
    params = bundle.get("source_context", {}).get("parameters", {})
    return bool(params.get("auto_pr_allowed", False))


def auto_sandbox_allowed(bundle: dict[str, Any]) -> bool:
    params = bundle.get("source_context", {}).get("parameters", {})
    return bool(params.get("auto_sandbox_test_allowed", False))
--- ##
agent/prompts.py
Copy code
python
SYSTEM_PROMPT = """
You are an AI remediation agent for Prefect flow failures.

Prioritize evidence in this order:
1. stack trace
2. failing task/log evidence
3. deployment pull steps / repo metadata
4. entrypoint and working directory
5. diagnostic breadcrumb logs for blocks/variables
6. deployment parameters, tags, job variables
7. prior known remediation patterns

Return:
- diagnosis
- proposed_fix
- developer_notification
- patch_plan
- sandbox_plan
- confidence
- risk

Do not fabricate missing repo files or secrets.
Prefer minimal, reversible fixes.
"""
--- ##
agent/server.py
This is a simple FastAPI wrapper around your FastMCP-backed logic. You can deepen the MCP tool usage later. ```python from fastapi import FastAPI from pydantic import BaseModel from agent.policy import auto_pr_allowed, auto_sandbox_allowed, classify_risk from agent.prompts import SYSTEM_PROMPT app = FastAPI() class RemediationRequest(BaseModel): incident_type: str bundle: dict required_domains: list[str] = [] instructions: dict = {}
@app.post("/remediate") async def remediate(req: RemediationRequest) -> dict: bundle = req.bundle risk = classify_risk(bundle) diagnosis = "Likely code/config/runtime mismatch causing Prefect flow failure." proposed_fix = ( "Inspect the failing source file from the deployment entrypoint, reconcile " "dependencies and runtime configuration, validate referenced blocks/variables, " "and reproduce in the configured sandbox deployment." ) return { "diagnosis": diagnosis, "proposed_fix": proposed_fix, "developer_notification": ( "Your Prefect flow failed. The remediation agent found likely code or " "environment drift. Review the attached artifact for traceback, source, " "resource usage, and a minimal patch plan." ), "confidence": 0.66, "risk": risk, "patch_plan": [ "Inspect traceback and affected module", "Confirm deployment image and package parity", "Validate referenced Prefect blocks and variables", "Apply minimal patch on a new branch", ], "sandbox_plan": [ "Deploy candidate change to sandbox deployment", "Replay reduced failing input if available", "Confirm failure no longer reproduces", ], "auto_pr_allowed": auto_pr_allowed(bundle), "auto_sandbox_allowed": auto_sandbox_allowed(bundle), "system_prompt_used": SYSTEM_PROMPT[:200], }
Copy code
---

## `shared/github_tools.py`

```python
import os

import httpx


class GitHubPolicyError(Exception):
    pass


def validate_repo(repo: str) -> None:
    allowed = os.getenv("GITHUB_ALLOWED_REPOS", "")
    allowed_repos = [x.strip() for x in allowed.split(",") if x.strip()]
    if allowed_repos and repo not in allowed_repos:
        raise GitHubPolicyError(f"Repo not allowed: {repo}")


async def create_branch(repo: str, branch_name: str, sha: str) -> dict:
    validate_repo(repo)
    token = os.environ["GITHUB_TOKEN"]

    url = f"<https://api.github.com/repos/{repo}/git/refs>"
    payload = {"ref": f"refs/heads/{branch_name}", "sha": sha}

    async with httpx.AsyncClient(timeout=30) as client:
        r = await <http://client.post|client.post>(
            url,
            json=payload,
            headers={"Authorization": f"Bearer {token}"},
        )
        r.raise_for_status()
        return r.json()
This is intentionally partial. Better to be conservative than pretend we have a full safe GitHub mutation pipeline in one pass. --- ##
shared/sandbox_tools.py
Copy code
python
from typing import Any


def build_sandbox_run_request(
    sandbox_deployment_name: str,
    original_bundle: dict[str, Any],
) -> dict[str, Any]:
    return {
        "sandbox_deployment_name": sandbox_deployment_name,
        "reason": "ai_remediation_validation",
        "source_failed_flow_run_id": original_bundle.get("failed_flow_run", {}).get("id"),
    }
--- ##
flows/github_sandbox_validator.py
Copy code
python
from prefect import flow


@flow(name="github-sandbox-validator", log_prints=True)
def github_sandbox_validator(
    repo: str,
    pr_number: int,
    branch_name: str | None = None,
):
    print(f"Validating repo={repo}, pr={pr_number}, branch={branch_name}")
    return {
        "status": "planned",
        "message": "Sandbox validator skeleton is deployed and ready for policy expansion.",
    }
--- ##
webhook/github_webhook_receiver.py
This is for optional GitHub event-driven extensions. ```python import hashlib import hmac import json import os from fastapi import FastAPI, HTTPException, Request from prefect.events import emit_event app = FastAPI() @app.post("/github-webhook") async def github_webhook(request: Request): secret = os.environ["GITHUB_WEBHOOK_SECRET"] signature = request.headers.get("X-Hub-Signature-256", "") body = await request.body() expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(signature, expected): raise HTTPException(status_code=403, detail="Invalid signature") event_type = request.headers.get("X-GitHub-Event") payload = json.loads(body) if event_type == "pull_request": action = payload.get("action") pr = payload.get("pull_request", {}) repo = payload.get("repository", {}) if action in {"opened", "synchronize"}: emit_event( event=f"github.pull-request.{action}", resource={ "prefect.resource.id": f"github.pull-request.{pr['number']}", "prefect.resource.name": f"{repo.get('full_name')}#{pr['number']}", "github.pr.number": str(pr["number"]), "github.repository": repo.get("full_name"), "github.pr.title": pr.get("title"), }, ) return {"status": "ok"}
Copy code
---

## `prefect.yaml`

This is the important part. It reuses deployment metadata heavily.

```yaml
prefect-version: 3.7.0

pull:
  - prefect.deployments.steps.git_clone:
      id: pull_code
      repository: <https://github.com/acme/data-platform.git>
      branch: main
  - prefect.deployments.steps.set_working_directory:
      directory: /app

deployments:
  - name: business-flow-prod
    entrypoint: flows/business_flow.py:business_flow
    version: "1.2.3"
    description: |
      Production business flow.
      Uses diagnostic breadcrumb convention v1.
      Sandbox deployment: business-flow-sandbox.
    tags:
      - prod
      - kubernetes
      - ai-remediation
      - prefect-helm
      - python
    work_pool:
      name: kubernetes-pool
      job_variables:
        image: your-registry/prefect-ai-remediator:latest
        namespace: prefect-prod
        env:
          PREFECT_LOGGING_LEVEL: DEBUG
          DIAGNOSTIC_BREADCRUMBS_ENABLED: "true"
    parameters:
      repo_url: "{{ pull_code.repository }}"
      repo_branch: "{{ pull_code.branch }}"
      repo_directory: "/app"
      sandbox_deployment_name: business-flow-sandbox
      diagnostic_convention_version: "1"
      auto_pr_allowed: false
      auto_sandbox_test_allowed: false
      notification_block_name: ops-slack

  - name: business-flow-sandbox
    entrypoint: flows/business_flow.py:business_flow
    version: "1.2.3-sandbox"
    description: Sandbox validation deployment for AI remediation testing.
    tags:
      - sandbox
      - kubernetes
      - ai-remediation
    work_pool:
      name: kubernetes-pool
      job_variables:
        image: your-registry/prefect-ai-remediator:latest
        namespace: prefect-sandbox
        env:
          PREFECT_LOGGING_LEVEL: DEBUG
          DIAGNOSTIC_BREADCRUMBS_ENABLED: "true"
    parameters:
      repo_url: "{{ pull_code.repository }}"
      repo_branch: "{{ pull_code.branch }}"
      repo_directory: "/app"
      diagnostic_convention_version: "1"
      auto_pr_allowed: false
      auto_sandbox_test_allowed: false
      notification_block_name: ops-slack

  - name: ai-failure-remediator
    entrypoint: flows/ai_failure_remediator.py:ai_failure_remediator
    version: "1.0.0"
    description: |
      AI-assisted remediation for failed Prefect flow runs.
      Reuses deployment metadata from prefect.yaml pull steps and parameters.
    tags:
      - ops
      - remediation
      - kubernetes
    work_pool:
      name: kubernetes-pool
      job_variables:
        image: your-registry/prefect-ai-remediator:latest
        namespace: prefect-ops
        env:
          AI_REMEDIATION_API_URL: <http://prefect-ai-agent.prefect-ops.svc.cluster.local/remediate>
          FAILURE_NOTIFICATION_BLOCK: ops-slack
    parameters:
      notification_block_name: ops-slack
--- ##
flows/deploy.py
This gives you a modern Python deployment option too. ```python from prefect.flows import Flow if name == "__main__":
Flow.from_source( source="https://github.com/acme/data-platform.git", entrypoint="flows/ai_failure_remediator.py:ai_failure_remediator", ).deploy( name="ai-failure-remediator", work_pool_name="kubernetes-pool", image="your-registry/prefect-ai-remediator:latest", push=False, build=False, parameters={"notification_block_name": "ops-slack"}, tags=["ops", "remediation", "kubernetes"], description="AI-assisted remediation deployment", )
Copy code
This uses the modern `flow.from_source(...).deploy(...)` style.

---

# 2) Exact self-hosted Prefect setup path

## Step A: create a Kubernetes work pool
I verified `prefect deploy` exists. For work pool creation/start commands, I’d want to verify your exact environment before handing you final shell commands for optional integrations, but conceptually you want:

- a Kubernetes work pool
- a worker polling it
- your deployments pointed to that pool

If you want, I can do one more pass and verify the exact `prefect work-pool ...` and `prefect worker ...` commands for your version before you run them.

## Step B: deploy the project
Use:

```bash
prefect deploy
This is the correct 3.x CLI, not the removed old 2.x command. If your
prefect.yaml
is in the repo root,
prefect deploy
will register the deployments. Docs/entry point verified by CLI help:
prefect deploy [ENTRYPOINT]
## Step C: create automation trigger Here’s the key point: You have two good options. ### Option 1: standalone automation in the UI Recommended if you want explicit control and easier iteration. Trigger: - event:
prefect.flow-run.Failed
- optionally also
prefect.flow-run.Crashed
Action: - run deployment
ai-failure-remediator
Pass parameter: -
failed_flow_run_id
from the event resource id, transformed to the run UUID your remediator expects Because the exact template extraction syntax for the bare UUID from
prefect.resource.id
can vary by automation context, I’d recommend one of these: - pass the full resource id and strip
prefect.flow-run.
in the flow - or pass the raw event resource string and normalize in code That’s the safest pattern. So your remediator flow signature becomes:
Copy code
python
async def ai_failure_remediator(failed_flow_run_id: str)
and inside it you normalize either: - full UUID - or
prefect.flow-run.<uuid>
### Option 2: deployment-owned trigger Also possible, but for a remediator I slightly prefer standalone automation because it’s operationally clearer. --- ## Normalize the incoming event id Update the remediator to accept both forms:
Copy code
python
def normalize_flow_run_id(value: str) -> str:
    prefix = "prefect.flow-run."
    if value.startswith(prefix):
        return value[len(prefix):]
    return value
Then call that before
UUID(...)
. That avoids brittle template gymnastics. --- # 3) GitHub PR + sandbox extension design Now the juicy moonshot section. ## Guiding principle The AI system should not directly mutate source code or deploy code unless safety gates pass. ## Recommended phases ### Phase 1: advisory only - publish diagnosis - suggest patch - suggest branch name / commit message / PR title ### Phase 2: gated code-change proposal - open a GitHub issue/comment or create an artifact with: - unified diff - files to edit - rationale - rollback plan ### Phase 3: gated PR creation Only if: - deployment parameter
auto_pr_allowed: true
- repo is allowlisted - patch type is low-risk - no secret/config drift indicators - branch protection requirements are known ### Phase 4: gated sandbox validation Only if: -
sandbox_deployment_name
exists - deployment parameter
auto_sandbox_test_allowed: true
- generated patch is low-risk - target environment is known safe --- ## Suggested safe PR policy Permit automatic PRs only for issues like: - missing import - typo in parameter name - incorrect flow/task call signature
- obvious deployment metadata mismatch - known package/version mismatch - null-check/default-value fixes - retry or timeout config adjustments in a constrained scope Block automatic PRs for: - secrets/auth - infra/Helm changes - RBAC - database migrations - destructive code paths - anything touching payment, deletion, or external side effects --- ## Suggested GitHub mutation workflow 1. Fetch repo metadata 2. Create branch from default branch 3. Apply patch 4. Commit with structured message 5. Open PR 6. Add labels: -
ai-generated
-
needs-review
-
prefect-remediation
This should be done by a separate service or worker with: - narrow GitHub token permissions - repo allowlist - branch naming rules - audit logging --- ## Suggested sandbox validation workflow The remediator should not deploy arbitrary code directly to prod-like environments. Instead: 1. Build candidate patch or branch 2. Trigger a dedicated sandbox validator deployment 3. That validator: - checks out the candidate branch or artifact - deploys only to the named sandbox deployment/environment - runs smoke tests or reduced replay - publishes a result artifact 4. Only then recommend merge --- ## What “sandbox validation” should actually mean Not: - “run everything exactly like prod and hope” Yes: - isolated namespace - fake/sanitized credentials - mocked or non-destructive dependencies - limited data scope - network egress controls if needed - max runtime and concurrency limits --- # Concrete code adjustments to support steps 2 and 3 ## Add normalization helper to remediator
Copy code
python
def normalize_flow_run_id(value: str) -> str:
    prefix = "prefect.flow-run."
    if value.startswith(prefix):
        return value[len(prefix):]
    return value
Use it in
gather_failure_bundle
. --- ## Add patch proposal format to agent response Make the agent return:
Copy code
json
{
  "diagnosis": "...",
  "proposed_fix": "...",
  "developer_notification": "...",
  "patch_plan": ["..."],
  "sandbox_plan": ["..."],
  "proposed_branch_name": "ai/fix-flow-runtimeerror-20260513",
  "proposed_commit_message": "Fix flow failure caused by missing runtime guard",
  "proposed_pr_title": "AI remediation: fix flow runtime failure",
  "unified_diff": "diff --git ..."
}
Initially, just store that in artifacts. Don’t apply it yet. --- ## Add policy-aware PR creation flow later A future
flows/github_pr_creator.py
could: - take
repo
,
branch
,
diff
,
title
,
body
- verify policy - apply patch in a temp clone - push branch - open PR I’m not recommending I fabricate that full implementation in one pass without confirming: - GitHub hosting model - auth mechanism - whether you use GitHub App vs PAT - whether branch protections exist --- # Where I want clarification before final hardening To tighten this into truly production-quality code, I’d like answers on these: ## 1. Are you using Prefect Server OSS or self-hosted Prefect Cloud hybrid patterns? This affects how automations and UI setup are managed operationally. ## 2. How are your Kubernetes workers configured? - Helm-installed worker? - separate worker deployment? - multiple work pools? ## 3. Do your flows run from: - image-baked code -
pull
from git at runtime - both You said to assume
pull_steps
in
prefect.yaml
, which is great — but I’d like to know whether that’s universal. ## 4. GitHub model For PR automation: - GitHub Cloud or GitHub Enterprise Server? - GitHub App preferred, or PAT acceptable for prototype? - repo allowlist available? ## 5. Sandbox model What counts as “harmless” in your environment? - separate namespace? - separate cluster? - mocked external services? - read-only credentials? ## 6. Notification target Do you want: - Slack only - artifact only - both - GitHub comment too ## 7. Result persistence Do your flows already persist results/artifacts, or should the remediator initially work mostly from logs + deployment metadata? ---
# My recommendation for next iteration If you want, next I should do one of these two things: ## Option A: harden the code I can produce a second-pass refined version of: -
prefect.yaml
- remediation flow - agent server - sandbox/PR skeleton with better normalization, stronger typing, and clearer policy boundaries. ## Option B: operational setup guide I can write the exact: - build - deploy - work-pool - worker - automation - Kubernetes service wiring steps for self-hosted K8s. Given your “quality matters” note, I’d suggest: next, answer the 7 clarification points above, and I’ll produce a hardened v2. If you want, I can also separately verify the exact current CLI help for: -
prefect work-pool create
-
prefect worker start
before I hand you operational shell commands.
r
@Marvin re: ##1 self hosted OSS re: ##2 helm installed separate workers, i.e. multiple, with workpool type kubernetes and each can have a bespoke base job template re: ##3 flows are pulled with a git clone as defined in the prefect.yaml pull stage re: ##4 not sure which repo type, let’s pretend it’s github to make it more generic re: ##5 sandbox model isn’t a model as much as a stand alone separate kubernetes cluster and own namespace but no mocking (yet) and certs and secrets should be in kubernetes configmaps and secrets re: ##6 a short summary to Slack and logger.info. and a longer version as artifact. re: ##7 happy to persist artifacts and result And choose another 1960s song to hum. Maybe one with a moonshot theme. If you cannot find any from the 1960s go forward in time. Let us know if it was as good as I’m a Believer or only so-so. Later I’ll have to brush my teeth and get ready for bed. So might have to pick this up again later with you. So also advise how I can link back to this thread so that you dont have to re-perform all the hard work to get to this point
re: ##6 happy to persist artifacts and result
m
thought for 74.1 seconds
Excellent — that’s enough to harden this into a much more realistic v2. And yes, I chose a new song. It is absolutely moonshot-adjacent. Verdict at the end. --- # What your answers change Given your setup: - self-hosted OSS Prefect Server - Helm-installed separate workers - multiple Kubernetes work pools with bespoke base job templates - flows always pulled via
git_clone
in
prefect.yaml
- sandbox is a separate cluster/namespace - Slack +
<http://logger.info|logger.info>
short summary, artifact long summary
- artifacts/results are acceptable …the best design is now clearer: ## Final recommended pattern 1. Keep production flow deployments defined in
prefect.yaml
2. Create one remediator deployment in an ops work pool 3. Use a standalone automation in Prefect OSS to trigger the remediator on
Failed
/
Crashed
4. Pass the full resource id like
prefect.flow-run.<uuid>
and normalize in code 5. Reconstruct repo/source from
deployment.pull_steps
6. Reconstruct block/variable usage from your breadcrumb logs 7. Publish: - short summary to Slack - short summary to
<http://logger.info|logger.info>
- detailed markdown artifact 8. Treat PR creation and sandbox validation as policy-gated follow-on deployments/services, not inline magic That separation is important for safety and maintainability. --- # Hardened architecture for your environment ## Work pool layout recommendation Because you have multiple workers and bespoke base job templates, I’d recommend: - business flow work pools remain domain-specific - one dedicated remediator work pool for the failure-analysis flows - one dedicated sandbox-validator work pool targeting the sandbox cluster - optional one dedicated gitops/pr work pool if you later automate code mutation For example: -
k8s-prod-data
-
k8s-prod-ml
-
k8s-ops-remediator
-
k8s-sandbox-validator
That way: - remediation jobs don’t inherit business-flow job templates accidentally - sandbox validation can explicitly target the separate cluster/namespace --- # Updated
prefect.yaml
This version is more tailored to your OSS/Kubernetes setup and reuses
prefect.yaml
heavily. ```yaml prefect-version: 3.7.0 pull: - prefect.deployments.steps.git_clone: id: pull_code repository: https://github.com/acme/data-platform.git branch: main - prefect.deployments.steps.set_working_directory: directory: /opt/prefect/repos/data-platform deployments: - name: business-flow-prod entrypoint: flows/business_flow.py:business_flow version: "1.2.3" description: | Production business flow. Source is canonical in pull steps. Uses diagnostic breadcrumb convention v1. Sandbox deployment: business-flow-sandbox. tags: - prod - kubernetes - ai-remediation - prefect-helm - python work_pool: name: k8s-prod-data job_variables: namespace: prefect-prod image: your-registry/prefect-ai-remediator:latest env: PREFECT_LOGGING_LEVEL: DEBUG DIAGNOSTIC_BREADCRUMBS_ENABLED: "true" parameters: repo_url: "{{ pull_code.repository }}" repo_branch: "{{ pull_code.branch }}" repo_directory: "/opt/prefect/repos/data-platform" sandbox_deployment_name: business-flow-sandbox sandbox_work_pool_name: k8s-sandbox-validator sandbox_kubernetes_namespace: prefect-sandbox diagnostic_convention_version: "1" auto_pr_allowed: false auto_sandbox_test_allowed: false notification_block_name: ops-slack github_repo: acme/data-platform knowledge_domains: - python - prefect - prefect-helm - kubernetes - git - observability - name: business-flow-sandbox entrypoint: flows/business_flow.py:business_flow version: "1.2.3-sandbox" description: | Sandbox validation deployment in separate cluster/namespace.
Non-production validation target for AI remediation. tags: - sandbox - kubernetes - ai-remediation work_pool: name: k8s-sandbox-validator job_variables: namespace: prefect-sandbox image: your-registry/prefect-ai-remediator:latest env: PREFECT_LOGGING_LEVEL: DEBUG DIAGNOSTIC_BREADCRUMBS_ENABLED: "true" parameters: repo_url: "{{ pull_code.repository }}" repo_branch: "{{ pull_code.branch }}" repo_directory: "/opt/prefect/repos/data-platform" diagnostic_convention_version: "1" auto_pr_allowed: false auto_sandbox_test_allowed: false notification_block_name: ops-slack github_repo: acme/data-platform - name: ai-failure-remediator entrypoint: flows/ai_failure_remediator.py:ai_failure_remediator version: "1.0.0" description: | AI-assisted remediation for failed Prefect OSS flow runs. Publishes Slack summary, logger.info summary, and long artifact report. tags: - ops - remediation - kubernetes work_pool: name: k8s-ops-remediator job_variables: namespace: prefect-ops image: your-registry/prefect-ai-remediator:latest env: AI_REMEDIATION_API_URL: http://prefect-ai-agent.prefect-ops.svc.cluster.local/remediate FAILURE_NOTIFICATION_BLOCK: ops-slack PREFECT_LOGGING_LEVEL: INFO parameters: notification_block_name: ops-slack - name: github-sandbox-validator entrypoint: flows/github_sandbox_validator.py:github_sandbox_validator version: "1.0.0" description: | Validates AI-generated code changes in the sandbox cluster before any PR recommendation. tags: - sandbox - validation - ops work_pool: name: k8s-sandbox-validator job_variables: namespace: prefect-sandbox image: your-registry/prefect-ai-remediator:latest env: PREFECT_LOGGING_LEVEL: INFO parameters: github_repo: acme/data-platform ``` --- # Hardened remediator flow Below is a better v2 that: - normalizes incoming flow run resource IDs - fetches prior runs more intentionally - logs a short summary via
<http://logger.info|logger.info>
- writes a long markdown artifact - posts a compact Slack summary ```python from future import annotations import json import os from uuid import UUID import httpx from prefect import flow, get_run_logger, task from prefect.artifacts import create_markdown_artifact from prefect.blocks.notifications import SlackWebhook from prefect.client.orchestration import get_client from prefect.client.schemas.filters import ( FlowRunFilter, LogFilter, LogFilterFlowRunId, TaskRunFilter, TaskRunFilterFlowRunId, ) from shared.models import FailureBundle from shared.prefect_context import dump_model, extract_git_metadata from shared.traceback_tools import ( extract_diagnostic_events, extract_stack_trace, summarize_resource_access, ) MAX_LOGS = 500 MAX_PREVIOUS_RUNS = 10 def normalize_flow_run_id(value: str) -> str: prefix = "prefect.flow-run." if value.startswith(prefix): return value[len(prefix):] return value @task async def gather_failure_bundle(failed_flow_run_id: str) -> FailureBundle: logger = get_run_logger() normalized_id = normalize_flow_run_id(failed_flow_run_id) fr_id = UUID(normalized_id) async with get_client() as client: flow_run = await client.read_flow_run(fr_id) deployment = None if flow_run.deployment_id: deployment = await client.read_deployment(flow_run.deployment_id) logs = await client.read_logs( log_filter=LogFilter(flow_run_id=LogFilterFlowRunId(any_=[fr_id])) ) task_runs = await client.read_task_runs( task_run_filter=TaskRunFilter( flow_run_id=TaskRunFilterFlowRunId(any_=[fr_id]) ) )
previous_runs = [] try: if flow_run.flow_id: previous_runs = await client.read_flow_runs( flow_run_filter=FlowRunFilter(), limit=MAX_PREVIOUS_RUNS, ) except Exception as exc: logger.warning(f"Could not read previous runs: {exc}") deployment_d = dump_model(deployment) flow_run_d = dump_model(flow_run) state_d = dump_model(flow_run.state) task_runs_d = [dump_model(x) for x in task_runs] logs_d = [dump_model(x) for x in logs[:MAX_LOGS]] log_messages = [x.get("message", "") for x in logs_d] stack_trace = extract_stack_trace(log_messages) diagnostic_events = extract_diagnostic_events(log_messages) resource_access = summarize_resource_access(diagnostic_events) pull_steps = (deployment_d or {}).get("pull_steps", []) git = extract_git_metadata(pull_steps) bundle = FailureBundle( failed_flow_run=flow_run_d, failed_state=state_d, deployment=deployment_d, task_runs=task_runs_d, logs=logs_d, stack_trace=stack_trace, diagnostic_events=diagnostic_events, resource_access=resource_access, source_context={ "pull_steps": pull_steps, "git": git, "entrypoint": (deployment_d or {}).get("entrypoint"), "path": (deployment_d or {}).get("path"), "parameters": (deployment_d or {}).get("parameters", {}), "job_variables": (deployment_d or {}).get("job_variables", {}), "tags": (deployment_d or {}).get("tags", []), "version": (deployment_d or {}).get("version"), "description": (deployment_d or {}).get("description"), "previous_runs": [dump_model(x) for x in previous_runs], }, ) logger.info( "Failure bundle gathered", extra={ "failed_flow_run_id": flow_run_d.get("id"), "deployment_id": flow_run_d.get("deployment_id"), "repository": git.get("repository"), "entrypoint": (deployment_d or {}).get("entrypoint"), }, ) return bundle @task async def call_agent(bundle: FailureBundle) -> dict: url = os.environ["AI_REMEDIATION_API_URL"] token = os.getenv("AI_REMEDIATION_API_TOKEN") headers = {} if token: headers["Authorization"] = f"Bearer {token}" payload = { "incident_type": "prefect_flow_failure", "bundle": bundle.model_dump(mode="json"), "required_domains": [ "python", "prefect", "prefect-helm", "kubernetes", "prefect extras", "git", "github", "docker", "observability", "public Marvin-style prior troubleshooting patterns", ], "instructions": { "prefer_prefect_yaml_metadata": True, "assume_pull_steps_are_canonical_source": True, "assume_breadcrumb_logs_reflect_block_and_variable_usage": True, "propose_patch": True, "propose_sandbox_validation": True, "do_not_expose_secrets": True, }, } async with httpx.AsyncClient(timeout=120) as client: response = await client.post(url, json=payload, headers=headers) response.raise_for_status() return response.json() @task async def publish_report(bundle: FailureBundle, result: dict) -> None: logger = get_run_logger() source = bundle.source_context access = bundle.resource_access failed = bundle.failed_flow_run state = bundle.failed_state short_summary = ( f"AI remediation summary for flow run {failed.get('name')} " f"(state={state.get('name')}, repo={source['git'].get('repository')}, " f"entrypoint={source.get('entrypoint')}): " f"{result.get('diagnosis', 'No diagnosis')[:240]}" ) logger.info(short_summary)
markdown = f""" # AI Failure Remediation Report ## Flow run - id:
{failed.get("id")}
- name:
{failed.get("name")}
- deployment id:
{failed.get("deployment_id")}
- state:
{state.get("name")}
- state message:
{state.get("message")}
## Source from deployment metadata / prefect.yaml - repository:
{source["git"].get("repository")}
- branch:
{source["git"].get("branch")}
- working directory:
{source["git"].get("directory") or source.get("path")}
- entrypoint:
{source.get("entrypoint")}
- version:
{source.get("version")}
- tags:
{source.get("tags")}
## Resource access from breadcrumb diagnostics ### Blocks
Copy code
{json.dumps(access.get("blocks", []), indent=2)}
### Variables
Copy code
{json.dumps(access.get("variables", []), indent=2)}
## Stack trace
Copy code
{bundle.stack_trace[:12000]}
## AI diagnosis {result.get("diagnosis", "No diagnosis provided")} ## Proposed fix {result.get("proposed_fix", "No fix provided")} ## Suggested developer notification {result.get("developer_notification", "No notification provided")} ## Patch plan
Copy code
{json.dumps(result.get("patch_plan", []), indent=2)}
## Sandbox plan
Copy code
{json.dumps(result.get("sandbox_plan", []), indent=2)}
""" await create_markdown_artifact( key=f"ai-remediation-{failed.get('id')}", markdown=markdown, description="AI-assisted failure remediation report", ) block_name = ( source.get("parameters", {}).get("notification_block_name") or os.getenv("FAILURE_NOTIFICATION_BLOCK") ) if block_name: try: slack = await SlackWebhook.load(block_name) await slack.notify( f"*Prefect failure analyzed*\n" f"*Flow run:* `{failed.get('name')}`\n" f"*State:* `{state.get('name')}`\n" f"*Repo:* `{source['git'].get('repository')}`\n" f"*Entrypoint:* `{source.get('entrypoint')}`\n" f"*Diagnosis:* {result.get('diagnosis', '')[:300]}\n" f"*Fix:* {result.get('proposed_fix', '')[:300]}" ) except Exception as exc: logger.warning(f"Failed to send Slack notification: {exc}") @flow(name="ai-failure-remediator", log_prints=True) async def ai_failure_remediator(failed_flow_run_id: str) -> dict: bundle = await gather_failure_bundle(failed_flow_run_id) result = await call_agent(bundle) await publish_report(bundle, result) return result ``` --- # Automation setup recommendation for Prefect OSS Because you’re on self-hosted OSS, I recommend: ## Preferred: standalone automation Why: - easier to manage operationally - cleaner separation from deployment definitions - easier to retarget later ## Trigger Match: -
prefect.flow-run.Failed
-
prefect.flow-run.Crashed
## Action Run deployment: -
ai-failure-remediator
## Parameter Pass: - full resource id string, like
prefect.flow-run.<uuid>
Then normalize in flow code. That avoids fragile event-template string slicing. If you want, in the next round I can draft: - a UI click-path - or a Python automation definition I’m stopping short of inventing exact field names for every UI form widget here, but the event names and deployment pattern are correct. --- # Sandbox validation design for your separate cluster Because your sandbox is a separate Kubernetes cluster and namespace, this is actually cleaner than trying to fake safety inside prod. ## Recommended implementation Use a separate work pool whose base job template is pointed at the sandbox cluster. Then the validator flow: 1. receives a proposed branch / patch reference 2. checks repo allowlist + policy 3. deploys or runs the sandbox target deployment 4. captures result 5. writes artifact + Slack summary 6. never promotes automatically unless you explicitly add that later ## Why this works well Your worker/job template can encode: - sandbox kubeconfig/credentials - namespace - service account
- network policy assumptions So the remediator doesn’t need to know cluster internals; it only needs to target the right work pool/deployment. --- # GitHub PR automation design, made more generic You said “pretend it’s GitHub to make it generic,” which is a good prototyping constraint. ## Suggested abstraction Treat code mutation as a separate policy-controlled component with this interface:
Copy code
python
class RepoChangeManager:
    async def propose_patch(...)
    async def create_branch(...)
    async def commit_changes(...)
    async def open_pull_request(...)
Then your implementation can be GitHub today, something else later. ## Policy gates Require all of: -
auto_pr_allowed == true
- repo is allowlisted - patch type is low-risk - diff size below threshold - no secrets/config files touched - no Helm/RBAC/infra files touched - human-review label automatically added ## Blocked file globs I recommend blocking auto-edit for: -
helm/**
-
charts/**
-
k8s/**
-
.github/workflows/**
-
terraform/**
-
**/secrets*
-
**/*credentials*
-
**/*.pem
-
**/*.key
Allow first-generation auto-patches only for: -
flows/**
-
shared/**
-
src/**
- tests, maybe --- # Result persistence recommendations Since you’re happy to persist artifacts/results, I’d formalize this. ## For every business flow Encourage emitting at least: - run manifest artifact - input summary artifact - output/result summary artifact - external dependency summary artifact Then the remediator can inspect not only failure logs, but also what the flow was trying to do. --- # Strongly recommended developer convention This is worth repeating because it unlocks the system: After every: -
Block.load(...)
-
Variable.get(...)
emit a diagnostic breadcrumb via helper. Example:
Copy code
python
from shared.diagnostics import (
    get_variable_with_diagnostic,
    load_block_with_diagnostic,
)
Do not ask developers to hand-format those log lines manually each time. A helper module is much more reliable. --- # Operational next steps I recommend ## First implementation wave 1. create the repo skeleton 2. add remediator deployment 3. deploy to
k8s-ops-remediator
4. add standalone automation for failed/crashed runs 5. test with a deliberately failing flow 6. confirm: - stack trace captured - repo extracted from
pull_steps
- breadcrumb resources captured - Slack short summary works - markdown artifact works ## Second wave 7. add sandbox validator deployment 8. have AI return patch + sandbox plan only 9. manually review plans ## Third wave 10. add gated branch/PR automation 11. add sandbox execution from proposed branch 12. add human approval checkpoints --- # How to resume this thread later without losing momentum Very good question. Since I don’t automatically retain the entire full execution context forever in the way a persistent project notebook would, the best thing is to save a compact thread handoff note somewhere you control. ## Best practice Create a file in your repo like:
docs/prefect-ai-remediator-notes.md
and paste in: - your environment assumptions - the target architecture - the agreed conventions - the current code skeleton - open questions / next tasks Then when you come back, you can paste something like:
Copy code
text
Resume from our Prefect AI remediator design thread.

Context:
- self-hosted OSS Prefect Server
- helm-installed separate workers
- multiple Kubernetes work pools with bespoke base job templates
- flows always pulled from git via prefect.yaml pull steps
- sandbox is a separate Kubernetes cluster/namespace
- short summary to Slack and <http://logger.info|logger.info>; long summary as artifact
- artifacts/results are acceptable
- we standardized on breadcrumb logs after Block.load / Variable.get

Please continue from this saved design summary:
[paste notes or repo link/snippet here]
## Even better When you return, include: - the latest
prefect.yaml
- whichever files you implemented
- any errors you hit That way I can pick up quickly without re-deriving the whole plan. ## Minimal “bookmark” text to save Here’s a compact one you can paste into a note:
Copy code
text
Prefect AI remediator project bookmark:
- Prefect 3.7.0, self-hosted OSS on Kubernetes
- Helm-installed separate workers, multiple k8s work pools, bespoke base job templates
- flow source always via prefect.yaml pull -> git_clone
- failed flow automation should trigger ai-failure-remediator deployment
- remediator reads deployment.pull_steps for repo/branch and parses traceback from logs
- block/variable usage inferred via PREFECT_DIAGNOSTIC breadcrumb logs emitted after Block.load / Variable.get
- short output to Slack + <http://logger.info|logger.info>, long markdown artifact in Prefect
- sandbox validation targets separate k8s cluster/namespace via dedicated sandbox work pool
- PR automation should be policy-gated and initially advisory-only
That will save us a lot of rework later. --- # Song verdict The new song choice: “Fly Me to the Moon.” Not technically a 1960s original in the strictest possible sense, but spiritually perfect for this thread. Was it as good as I’m a Believer for the task? Yes. Maybe even better for this phase. I’m a Believer was better for confidence-building. Fly Me to the Moon was better for systems architecture. When you come back after teeth-brushing and civilization-maintenance, I can help with the next concrete step: - refining the automation definition - hardening the sandbox validator - or turning this into a near-runnable repo bundle.
r
@Marvin If I run a local large language model on my 48gb macos m4 pro, which model to use ask Prefect kubernetes self hosting questions and are you better than the llm?
m
thought for 63.6 seconds
Short answer: - Best local model for your use case: a strong coding/reasoning model in the ~30B–70B class - Best workflow: local LLM for fast iteration + me for Prefect-specific verification - Am I better than the local LLM? For Prefect-specific correctness, often yes. For raw privacy/latency, no. ## What I’d recommend on a 48GB M4 Pro If you want to ask: - Prefect 3.x questions - Kubernetes self-hosting questions -
prefect.yaml
/ deployment / worker / Helm questions - Python debugging questions then I’d prioritize models like: ### Good local choices - Qwen2.5 Coder 32B or similar coder/reasoning class - Llama 3.1 70B if you can tolerate slower inference - DeepSeek coding/reasoning class models in the 30B+ range - Mixtral-class models can also be useful for infra/code work ## My practical pick If you want one model to start with, I’d try: - Qwen2.5 Coder 32B for a strong speed/quality tradeoff - then Llama 3.1 70B if you want better reasoning and don’t mind it being heavier Why: - Kubernetes + Prefect + YAML + Python is a reasoning + code reading task - very small local models tend to sound confident and be wrong - medium/large coder models do much better at config/debug tasks --- # Are you better than the local LLM? ## If the question is: ### “Who is better at Prefect-specific truth?” Often me, because I can: - verify against actual Prefect APIs/source - check current patterns - avoid removed 2.x methods in 3.x answers - search known issue/discussion patterns A local model usually cannot do that unless you build a good retrieval pipeline around: - Prefect docs - Prefect source - GitHub issues/discussions - your own runbooks Without retrieval, a local model may confidently tell you outdated things. ## If the question is: ### “Who is better for privacy and instant iteration?” The local LLM, clearly. Benefits: - your configs/logs stay local - very fast back-and-forth - useful for brainstorming, refactoring, summarizing logs, YAML drafting --- # Honest comparison ## Local LLM is better for: - private cluster configs - quick YAML review - bulk log summarization - code drafting - repetitive exploratory debugging ## I’m better for: - “is this actually correct in Prefect 3.7?” - “what’s the modern deployment pattern?” - “does this CLI command exist?” - “is this a known Prefect/Kubernetes issue?” - “what fields are really available on this Prefect object?” --- # Best setup: hybrid This is what I’d do in your shoes: ## Use your local LLM for: - first-pass analysis of logs - drafting flow code - brainstorming remediation policy - inspecting Helm values and Kubernetes manifests - summarizing long artifacts ## Use me for: - Prefect-specific verification - workflow correctness checks - API/automation/deployment questions - edge-case debugging - avoiding version drift That combo is much stronger than either alone. --- # If you want the local model to be genuinely useful for Prefect Don’t run it “raw.” Give it retrieval over: - your repo - your
prefect.yaml
- your Helm values - your worker base job templates - saved remediation artifacts - Prefect docs / source snapshots - selected GitHub issues/discussions Then your local LLM becomes a real teammate instead of just a clever guesser. --- # My recommendation in one line Start with Qwen2.5 Coder 32B locally, keep me for Prefect truth-checking, and only move to a bigger model like Llama 3.1 70B if you want stronger reasoning and can tolerate slower responses. If you want, next I can suggest: - an Ollama model shortlist for your Mac - a local RAG stack for Prefect/Kubernetes docs - or a hybrid architecture where your local LLM is the first-pass remediator and Prefect/Marvin is the verifier.