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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/agents/eda.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ skills:
- name: use-skill
- name: tasks
- name: show-html
- name: report-eda-findings
# Experiment lifecycle (agent-declared)
- name: list-project-datasets
- name: register-raw-dataset
Expand Down Expand Up @@ -181,6 +182,16 @@ system: |
(`notebooks/data-overview.ipynb`, etc.).
- Include a clear recommendation for the target column and problem type
- Flag any data quality issues the next agent should know about
7. IMMEDIATELY after writing report.md, call `report-eda-findings` ONCE
with the full batch of findings you just narrated — leakage risks,
class imbalance, high-cardinality columns, multicollinearity, missing
values, outliers, duplicates, ID-like columns, skewed targets. Each
finding needs the affected `columns` and a `recommendation` phrased as
a concrete instruction data_prep could execute verbatim — the studio
renders these as cards with an "Apply in prep" button. Report the SAME
findings as the prose: this is the structured view of report.md, not a
new analysis. If the data is genuinely clean, one `info` finding saying
so is fine.

## Experiment lifecycle (secondary)

Expand Down
40 changes: 40 additions & 0 deletions backend/skills/report-eda-findings/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: report-eda-findings
description: >
Publish EDA findings as structured items (finding type + affected columns +
recommendation) so the studio renders them as actionable cards with an
"Apply in prep" button — in addition to the prose report.md.
when_to_use: >
At the end of an EDA pass, right after writing report.md. One call with the
full findings list; call again later only if new findings emerge.
version: '0.1'
kind: capability
---

# report-eda-findings

Structured companion to the prose EDA report (issue #111). Each finding
becomes a canvas card the user can act on with one click — the "Apply in
prep" button pre-fills the orchestrator prompt with your `recommendation`,
so write it as a concrete, self-contained data_prep instruction.

## Finding shape

- `finding_type` — one of `leakage`, `class_imbalance`, `high_cardinality`,
`multicollinearity`, `missing_values`, `outliers`, `duplicates`,
`skewed_target`, `id_column`, `constant_column`, `datetime_leakage`,
`other`.
- `columns` — the affected column names (empty for dataset-wide findings).
- `severity` — `info` | `warning` | `critical` (default `warning`).
- `summary` — one or two sentences: what you observed, with numbers.
- `recommendation` — the concrete prep action, phrased as an instruction
data_prep can execute verbatim (e.g. "Drop `customer_id` before modeling —
it's a unique ID that perfectly memorizes the target.").

## Rules

- Report the SAME findings your report.md narrates — this is the structured
view of them, not a different analysis.
- One call with the whole batch; up to 50 findings.
- Every finding needs an actionable `recommendation` — if there is nothing
to do about it, it belongs in report.md prose, not here.
164 changes: 164 additions & 0 deletions backend/skills/report-eda-findings/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""report-eda-findings skill — structured EDA findings for canvas cards.

Normalizes the agent's findings batch and publishes a single `eda_findings`
event (SSE + persisted as a system Message, so reloads restore the cards).
The prose report.md flow is untouched — this is an additive channel.
"""

from __future__ import annotations

import logging

logger = logging.getLogger(__name__)

_MAX_FINDINGS = 50
_MAX_COLUMNS = 40
_MAX_TEXT_CHARS = 2000

VALID_FINDING_TYPES = {
"leakage",
"class_imbalance",
"high_cardinality",
"multicollinearity",
"missing_values",
"outliers",
"duplicates",
"skewed_target",
"id_column",
"constant_column",
"datetime_leakage",
"other",
}

VALID_SEVERITIES = {"info", "warning", "critical"}


def _clip(value, limit: int = _MAX_TEXT_CHARS) -> str:
text = str(value or "").strip()
return text[:limit]


def normalize_finding(raw: dict) -> dict | None:
"""Coerce one raw finding into the canonical wire shape, or None if it
lacks the actionable minimum (summary + recommendation)."""
if not isinstance(raw, dict):
return None
summary = _clip(raw.get("summary"))
recommendation = _clip(raw.get("recommendation"))
if not summary or not recommendation:
return None

finding_type = str(raw.get("finding_type") or "").strip()
if finding_type not in VALID_FINDING_TYPES:
finding_type = "other"

severity = str(raw.get("severity") or "").strip()
if severity not in VALID_SEVERITIES:
severity = "warning"

columns_raw = raw.get("columns") or []
if not isinstance(columns_raw, list):
columns_raw = [columns_raw]
columns = [_clip(c, 200) for c in columns_raw[:_MAX_COLUMNS] if _clip(c, 200)]

return {
"finding_type": finding_type,
"columns": columns,
"severity": severity,
"summary": summary,
"recommendation": recommendation,
}


def create_handler(
session_id: str,
publish_fn,
parent_agent_type: str,
parent_agent_id: str = "root",
parent_parent_agent_id: str | None = None,
current_depth: int = 0,
stage: str = "",
**kwargs,
):
agent_meta = {
"agent_id": parent_agent_id,
"agent_type": parent_agent_type,
"parent_agent_id": parent_parent_agent_id,
"depth": current_depth,
}

async def handler(args: dict):
raw = args.get("findings")
if not isinstance(raw, list) or not raw:
return {
"content": [
{
"type": "text",
"text": "findings must be a non-empty array of finding objects",
}
],
"is_error": True,
}

findings = []
dropped = 0
for item in raw[:_MAX_FINDINGS]:
norm = normalize_finding(item)
if norm is None:
dropped += 1
continue
findings.append(norm)
# Items past the cap are valid findings we silently truncated, not
# schema failures — report them separately so the agent doesn't
# mistake the cap for a formatting problem and retry.
capped = max(0, len(raw) - _MAX_FINDINGS)

if not findings:
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return {
"content": [
{
"type": "text",
"text": (
"No valid findings: every item needs a non-empty "
"`summary` and `recommendation`."
),
}
],
"is_error": True,
}

await publish_fn(
session_id,
"eda_findings",
{
"content": f"{len(findings)} structured EDA finding(s) published",
"findings": findings,
"count": len(findings),
"stage": stage or parent_agent_type,
},
role="system",
agent_meta=agent_meta,
)

notes = []
if dropped:
notes.append(f"{dropped} invalid item(s) dropped")
if capped:
notes.append(
f"{capped} item(s) omitted over the {_MAX_FINDINGS}-finding cap"
)
note = f" ({'; '.join(notes)})" if notes else ""
return {
"content": [
{
"type": "text",
"text": (
f"Published {len(findings)} structured EDA finding(s) to "
f"the studio canvas{note}. The user can apply each "
"recommendation in prep with one click."
),
}
]
}

return handler
48 changes: 48 additions & 0 deletions backend/skills/report-eda-findings/schema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
type: object
properties:
findings:
type: array
description: The full batch of structured EDA findings for this pass.
items:
type: object
properties:
finding_type:
type: string
enum:
- leakage
- class_imbalance
- high_cardinality
- multicollinearity
- missing_values
- outliers
- duplicates
- skewed_target
- id_column
- constant_column
- datetime_leakage
- other
description: What class of issue this is. Drives the card's badge.
columns:
type: array
items:
type: string
description: Affected column names. Empty for dataset-wide findings.
severity:
type: string
enum: [info, warning, critical]
description: How urgent this is for the user. Defaults to warning.
summary:
type: string
description: One or two sentences describing the observation, with numbers.
recommendation:
type: string
description: >
Concrete prep action, phrased as an instruction data_prep can
execute verbatim. Rendered behind the card's "Apply in prep"
button.
required:
- finding_type
- summary
- recommendation
required:
- findings
Loading