From 554e09a423a82a3d253e45618911907f24fd513a Mon Sep 17 00:00:00 2001 From: Manush Maheshwari Date: Sun, 26 Jul 2026 19:22:44 -0700 Subject: [PATCH 1/6] refactor(deep-research): single-source the chart contract via an on-demand visualization skill Move the chart-spec contract out of the always-on writer prompt into the visualization skill (sandbox PNG mode and non-sandbox inline-spec mode), and wire the skill into every default deep-research config so inline charts are preserved everywhere with the writer prompt shrunk to an on-demand pointer. Signed-off-by: Manush Maheshwari --- configs/config_cli_default.yml | 6 + configs/config_domain_routing_and_skills.yml | 2 +- configs/config_frontier_models.yml | 6 + configs/config_mcp.yml | 6 + configs/config_openshell.yml | 2 +- configs/config_web_azure_ai_search.yml | 6 + configs/config_web_default_guardrails.yml | 6 + configs/config_web_default_llamaindex.yml | 6 + configs/config_web_frag.yml | 6 + configs/config_web_frag_mcp_auth.yml | 6 + configs/config_web_opensearch.yml | 6 + .../agents/deep_researcher/prompts/writer.j2 | 48 ++---- .../chart-generation/SKILL.md | 131 +++++++++++---- .../test_chart_artifact_contract.py | 3 +- .../test_deepagents_runtime.py | 24 ++- .../agents/test_result_chart_prompt.py | 153 ++++++++++++------ 16 files changed, 298 insertions(+), 119 deletions(-) rename src/aiq_agent/agents/deep_researcher/skills/{research => visualization}/chart-generation/SKILL.md (58%) diff --git a/configs/config_cli_default.yml b/configs/config_cli_default.yml index 85d78c731..22726b8f0 100644 --- a/configs/config_cli_default.yml +++ b/configs/config_cli_default.yml @@ -114,6 +114,11 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -124,6 +129,7 @@ functions: writer_llm: nemotron_super_llm exclude_tools: - web_search_tool + skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_domain_routing_and_skills.yml b/configs/config_domain_routing_and_skills.yml index 8a94c20ba..41aac1ce5 100644 --- a/configs/config_domain_routing_and_skills.yml +++ b/configs/config_domain_routing_and_skills.yml @@ -166,7 +166,7 @@ functions: _type: deep_research_skills agents: researcher-agent: [research] - writer-agent: [synthesis] + writer-agent: [synthesis, visualization] require_sandbox: - research diff --git a/configs/config_frontier_models.yml b/configs/config_frontier_models.yml index 1d952ef76..b72654a94 100644 --- a/configs/config_frontier_models.yml +++ b/configs/config_frontier_models.yml @@ -144,6 +144,11 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -157,6 +162,7 @@ functions: # - paper_search_tool # Uncomment if SERPER_API_KEY is set - advanced_web_search_tool - knowledge_search + skills: deep_research_skills workflow: diff --git a/configs/config_mcp.yml b/configs/config_mcp.yml index 43a31d641..62e0fe8f2 100644 --- a/configs/config_mcp.yml +++ b/configs/config_mcp.yml @@ -89,6 +89,11 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -99,6 +104,7 @@ functions: writer_llm: nemotron_super_llm exclude_tools: - web_search_tool + skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_openshell.yml b/configs/config_openshell.yml index c2796a292..73b94ff2c 100644 --- a/configs/config_openshell.yml +++ b/configs/config_openshell.yml @@ -120,7 +120,7 @@ functions: _type: deep_research_skills agents: researcher-agent: [research] - writer-agent: [synthesis, research] + writer-agent: [synthesis, research, visualization] require_sandbox: - research diff --git a/configs/config_web_azure_ai_search.yml b/configs/config_web_azure_ai_search.yml index 32cb949f6..33edc7797 100644 --- a/configs/config_web_azure_ai_search.yml +++ b/configs/config_web_azure_ai_search.yml @@ -189,6 +189,11 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -201,6 +206,7 @@ functions: exclude_tools: # Remove basic variant; deep uses advanced_web_search_tool - web_search_tool verbose: true + skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_web_default_guardrails.yml b/configs/config_web_default_guardrails.yml index d786774f6..0339d105f 100644 --- a/configs/config_web_default_guardrails.yml +++ b/configs/config_web_default_guardrails.yml @@ -219,6 +219,11 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent orchestrator_llm: gpt_oss_llm @@ -229,6 +234,7 @@ functions: exclude_tools: - web_search_tool verbose: true + skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_web_default_llamaindex.yml b/configs/config_web_default_llamaindex.yml index b5ca6bfe1..ca4f4dbc0 100644 --- a/configs/config_web_default_llamaindex.yml +++ b/configs/config_web_default_llamaindex.yml @@ -188,6 +188,11 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -200,6 +205,7 @@ functions: exclude_tools: # Remove basic variant; deep uses advanced_web_search_tool - web_search_tool verbose: true + skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_web_frag.yml b/configs/config_web_frag.yml index 195f0e665..0a4cccee3 100644 --- a/configs/config_web_frag.yml +++ b/configs/config_web_frag.yml @@ -156,6 +156,11 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -167,6 +172,7 @@ functions: # tools: omitted -> inherits all from data_source_registry exclude_tools: - web_search_tool + skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_web_frag_mcp_auth.yml b/configs/config_web_frag_mcp_auth.yml index 0b48c34da..5ea322327 100644 --- a/configs/config_web_frag_mcp_auth.yml +++ b/configs/config_web_frag_mcp_auth.yml @@ -205,6 +205,11 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -216,6 +221,7 @@ functions: # tools: omitted -> inherits all from data_source_registry exclude_tools: - web_search_tool + skills: deep_research_skills # ========================================================================= # Protected per-user MCP source wiring (example: Google Drive) — OPTIONAL diff --git a/configs/config_web_opensearch.yml b/configs/config_web_opensearch.yml index 39f1735e2..b083fa09e 100644 --- a/configs/config_web_opensearch.yml +++ b/configs/config_web_opensearch.yml @@ -163,6 +163,11 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -174,6 +179,7 @@ functions: # tools: omitted -> inherits all from data_source_registry exclude_tools: - web_search_tool + skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/src/aiq_agent/agents/deep_researcher/prompts/writer.j2 b/src/aiq_agent/agents/deep_researcher/prompts/writer.j2 index 82b70047e..d17e66023 100644 --- a/src/aiq_agent/agents/deep_researcher/prompts/writer.j2 +++ b/src/aiq_agent/agents/deep_researcher/prompts/writer.j2 @@ -75,15 +75,19 @@ When synthesizing: - Do not expose internal `evidence_judgment` scores or rationale in the final answer unless the user explicitly asks for methodology. - Err on the side of more useful information rather than less, while staying focused on the requested answer shape. -{% if execution_enabled %}## Figures +{% if skills_enabled %}## Figures and Charts -When `answer_strategy.required_components` calls for a chart, GENERATE it yourself with the chart skill and embed it once, where it is discussed, with `![](artifact://)`. Read the relevant chart `SKILL.md` first, then write the plotting script to `{{ sandbox_workdir }}/make_chart.py`. The script must accept its artifact directory as its first argument; run it exactly as `python3 {{ sandbox_workdir }}/make_chart.py {{ sandbox_artifact_dir }}` so it writes the image plus `manifest.json` under the only directory the runtime harvests. Never put a literal `` or `` placeholder into code or a command. After execution, use the exact filename confirmed by the artifact checkpoint in the report reference. - -Each `execute` runs in a fresh shell, so `cd` does NOT persist between calls and a standalone `cd` accomplishes nothing — never navigate with `cd`; put absolute paths in every command (or chain in one line as `cd && `). +When `answer_strategy.required_components` calls for a chart (a ranking or top-N, a distribution across categories, a trend over an ordered axis, gains vs losses, or a single headline value), consult the `chart-generation` skill and follow it as the controlling procedure. Read its `SKILL.md` first. The skill defines the full chart contract and both delivery modes, so this base prompt stays free of chart syntax. +{% if execution_enabled %} +A sandbox is available, so the skill renders a PNG artifact. Write the plotting script to `{{ sandbox_workdir }}/make_chart.py` and run it exactly as `python3 {{ sandbox_workdir }}/make_chart.py {{ sandbox_artifact_dir }}` so the image plus its `manifest.json` land in the only directory the runtime harvests, then embed it once where it is discussed with `![](artifact://)`. Never put a literal `` or `` placeholder into code or a command, and never paste plotting code, a sandbox path, or base64 image data into the report. +{% else %} +There is no sandbox, so the skill emits an inline `chart` (or `chart-carousel`) JSON spec that the web app renders, followed by a compact portable table of the same values. +{% endif %} +A figure is earned: chart only source-anchored, reasonably complete data. If a series is mostly undisclosed or mixes metric definitions, present the table (which shows the gaps) and state the limitation in one sentence instead of a misleading chart. -NEVER paste the plotting code, a sandbox file path (e.g. `{{ sandbox_artifact_dir }}/chart.png`), or base64 image data into the report as prose - the `artifact://` token is the ONLY way a figure renders. Do not call `read_file` on a generated PNG just to verify it; that injects the image's base64 bytes into model context. Verify the manifest and use the artifact-checkpoint response instead. A "plotting code" or "chart plan" section is not a substitute for the actual figure. Precede each embed with a one-sentence description of what it shows. +{% else %}## Figures and Charts -A figure is earned: generate or embed one only when its data is source-anchored and reasonably complete. If a quantitative series is mostly undisclosed or mixes metric definitions, present the table (which shows the gaps) and state the limitation in one sentence instead of a misleading chart. +When a required component calls for a chart, present the values as a compact Markdown table and state the takeaway in one sentence. {% endif %}## Citations - Citations are mandatory for any sourced final answer. Every material source-derived factual claim, number, date, quote, source-specific caveat, or source claim must have an inline citation like `[1]`. @@ -102,37 +106,7 @@ A figure is earned: generate or embed one only when its data is source-anchored - Do not place bare URLs in the report body; URLs belong only in the `## Sources` section. - Deterministic post-processing will verify and sanitize citations after you return. Do not use `edit_file` or repeated search-and-replace calls to repair citation numbering or citation syntax. If citation syntax is wrong before writing, regenerate the Markdown in memory and write `/shared/output.md` once. -{% if not execution_enabled %}## Presenting Data (Charts) -When the report presents numbers that compare multiple things (a ranking or top-N across entities, a distribution or counts across categories, a trend over an ordered/time axis, or gains vs losses), present them as a chart, not just prose or a table. Lead with a one-sentence verdict, then the chart. - -- Emit the chart as a fenced code block tagged `chart` holding a SINGLE line of valid JSON (no comments, no trailing commas), right after the sentence that introduces it. -- Chart to reveal the pattern and state the verdict in prose. Inline `chart` blocks render only in the web app, so ALSO place a compact markdown table of the same values immediately after each chart, keeping PDF, Markdown, API, and CLI exports readable. -- Use the ACTUAL numbers from the evidence (top ~10 rows); never invent, pad, or round away data. If you show only the top rows of a larger set, say so. -- At most 3 charts per section, and put each chart before any table. -- A single value or a one-entity yes/no result is NOT a chart: emit a KPI-only block, a fenced `chart` block whose JSON has just `title` and `kpis`. - -Chart types: `bar` (category magnitudes), `hbar` (rankings with long text labels), `line`/`area` (a trend across an ordered axis), `grouped-bar` (2-4 series per category), `delta` (gains vs losses around zero). - -Spec fields: `type`; `title` (short) and optional `subtitle`; `x` = `{ "key": "", "label": "optional" }`; optional `y` = `{ "label": "optional unit", "format": "number | compact | percent | currency" }`; `series` = `[ { "key": "", "label": "optional", "color": "green | blue | amber | red" } ]`; `data` = rows as objects with raw numbers (fractions 0-1 for `percent`); optional `kpis` = `[ { "label": "...", "value": "preformatted", "tone": "accent | warn | alarm" } ]`. - -Example (ranking): -```chart -{"type":"hbar","title":"Top suppliers by late shipments","x":{"key":"supplier"},"y":{"format":"number"},"series":[{"key":"late","color":"amber"}],"data":[{"supplier":"Acme","late":42},{"supplier":"Globex","late":31},{"supplier":"Initech","late":19}]} -``` - -Example (single value, KPI-only): -```chart -{"title":"On-time delivery rate","kpis":[{"label":"On-time","value":"92.4%","tone":"accent"}]} -``` - -For several related trends over time, emit one fenced `chart-carousel` block holding a SINGLE line of JSON with at least two line-chart specs: `{ "title": "...", "charts": [ , ... ] }`. - -Example (related trends, carousel): -```chart-carousel -{"title":"Quarterly delivery trends","charts":[{"type":"line","title":"On-time delivery rate","x":{"key":"quarter"},"y":{"format":"percent"},"series":[{"key":"rate","color":"green"}],"data":[{"quarter":"Q1","rate":0.88},{"quarter":"Q2","rate":0.90},{"quarter":"Q3","rate":0.93}]},{"type":"line","title":"Late shipments","x":{"key":"quarter"},"y":{"format":"number"},"series":[{"key":"late","color":"amber"}],"data":[{"quarter":"Q1","late":52},{"quarter":"Q2","late":41},{"quarter":"Q3","late":28}]}]} -``` - -{% endif %}## Final Output +## Final Output Write the final Markdown to `/shared/output.md`. Then return only this short completion marker as your final response: `Wrote /shared/output.md`. diff --git a/src/aiq_agent/agents/deep_researcher/skills/research/chart-generation/SKILL.md b/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md similarity index 58% rename from src/aiq_agent/agents/deep_researcher/skills/research/chart-generation/SKILL.md rename to src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md index 76847076f..c372582ba 100644 --- a/src/aiq_agent/agents/deep_researcher/skills/research/chart-generation/SKILL.md +++ b/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md @@ -2,18 +2,59 @@ name: chart-generation description: > Use this skill to turn researched or computed numeric data into source-grounded - charts (PNG) plus the underlying CSV, by writing Python/matplotlib code and running - it in the job-scoped sandbox. The chart is harvested as a durable artifact and - embedded in the final report. + charts. It has two delivery modes and picks one from the tools available to you. + When an `execute` tool (sandbox) is available, render a PNG chart plus its CSV with + Python/matplotlib and embed it as a durable `artifact://` reference. When there is + no sandbox, emit the chart as an inline fenced `chart` (or `chart-carousel`) JSON + spec that the web UI renders deterministically, followed by a portable Markdown table. Triggers: "chart", "plot", "graph", "bar chart", "line chart", "visualize", - "trend over time", "compare visually", "figure". - Outputs: a PNG chart artifact, a CSV of the plotted data, and a manifest describing them. + "trend over time", "compare visually", "figure", "ranking", "top-N", "distribution". + Outputs: either a PNG chart artifact (plus CSV and manifest) or an inline chart spec. --- # Chart Generation Skill -Produce accurate, source-grounded charts using Python/matplotlib, save them as durable -artifacts, and embed them in the report by reference (never by pasting image data). +Produce accurate, source-grounded charts from researched or computed data. This skill +has two delivery modes; choose the one that matches the tools you were given, then +follow the matching section below. + +## Choose your mode + +1. **Sandbox mode (an `execute` tool is available):** render a PNG with Python/matplotlib, + save it as a durable artifact, and embed it by reference. Follow **Sandbox Mode (PNG + artifact)** below. +2. **Inline mode (no `execute` tool / no sandbox):** emit the chart as an inline fenced + `chart` JSON spec that the web app renders, plus a portable Markdown table. Follow + **Inline Mode (chart spec)** below. Do NOT attempt to run code or produce a PNG. + +Both modes share the same discipline: a chart confers authority, so it must be earned. + +## Data sufficiency (earn the chart, both modes) + +A polished chart of wrong or sparse numbers misleads more than it informs. + +1. **Source-anchored points only:** every plotted value must trace to a specific source + (the as-reported figure and its URL). Never plot a fabricated, guessed, or inferred + number as if it were reported; mark genuine estimates as estimates. +2. **Suppress misleading charts:** if a series is mostly missing (a majority of periods + undisclosed) or mixes metric definitions (e.g. "cash capex" vs "capex including finance + leases"), do NOT produce a trend chart. Present the table (which shows the gaps) and + state the limitation in one sentence instead. +3. **Show gaps honestly:** never interpolate or connect across missing periods. Plot only + the periods a series actually reports, and render estimates distinctly so they do not + read as reported values. +4. **Prefer gap-tolerant forms:** grouped bars show missing periods as absent bars; favor + them over a connected line when series are uneven, since a line drawn across gaps + implies a trend the data does not support. +5. **Use the ACTUAL numbers** from the evidence (top ~10 rows); never invent, pad, or round + away data. If you show only the top rows of a larger set, say so. + +--- + +# Sandbox Mode (PNG artifact) + +Render with Python/matplotlib, save the chart as a durable artifact, and embed it in the +report by reference (never by pasting image data). ## Required Execution Standard @@ -31,25 +72,6 @@ artifacts, and embed them in the report by reference (never by pasting image dat `![caption](artifact://.png)`. The runtime resolves this to the durable artifact; never paste base64 image data into the report. -## Data sufficiency (earn the chart) - -A chart confers authority, so it must be earned - never give unreliable data a cleaner -outfit. A polished chart of wrong or sparse numbers misleads more than it informs. - -1. **Source-anchored points only:** every plotted value must trace to a specific source - (the as-reported figure and its URL). Never plot a fabricated, guessed, or inferred - number as if it were reported; mark genuine estimates as estimates. -2. **Suppress misleading charts:** if a series is mostly missing (a majority of periods - undisclosed) or mixes metric definitions (e.g. "cash capex" vs "capex including finance - leases"), do NOT produce a trend chart. Present the table (which shows the gaps) and - state the limitation in one sentence instead. -3. **Show gaps honestly:** never interpolate or connect across missing periods. Plot only - the periods a series actually reports, and render estimates distinctly (e.g. hollow or - dashed markers) so they do not read as reported values. -4. **Prefer gap-tolerant forms:** grouped bars show missing periods as absent bars; favor - them over a connected line when series are uneven, since a line drawn across gaps - implies a trend that the data does not support. - ## Execution Flow 1. Assemble the normalized rows (prefer explicit records embedded in the script). If the @@ -62,7 +84,9 @@ outfit. A polished chart of wrong or sparse numbers misleads more than it inform `python3 /sandbox/JOB/make_chart.py /sandbox/JOB/aiq-artifacts`. Never execute a literal `` or `` token. `sandbox_workdir` is already per-job, so scripts there cannot collide with another job's - leftovers. Only ever execute a script you wrote this session. The script must: + leftovers. Only ever execute a script you wrote this session. Each `execute` runs in a + fresh shell, so `cd` does NOT persist between calls; put absolute paths in every command + (or chain in one line as `cd && `). The script must: - import pandas and matplotlib (use the non-interactive `Agg` backend), - build the DataFrame, compute any derived metrics, - set a single `ARTIFACT_DIR` to your `sandbox_artifact_dir` and write the chart @@ -84,7 +108,8 @@ Each figure must appear where it is discussed, not buried in a file list: 3. **Reference by filename, never a raw path:** the way to show a figure is the `![caption](artifact://.png)` token. Do NOT instead write the sandbox path (e.g. `/.png`) as prose and expect it to render - a bare path - is not an image. + is not an image. Never paste the plotting code or base64 image data into the report; do + not `read_file` a generated PNG just to verify it (that injects base64 bytes into context). 4. **One embed per artifact:** list supporting files (CSVs, manifests) by name in an appendix if useful, but the chart itself must be embedded inline as above. @@ -156,7 +181,7 @@ must be the real absolute artifact directory, not an angle-bracket placeholder. artifact-checkpoint response after `execute` as authoritative: reference the exact confirmed filename in the report and do not invent or rename it later. -## Notes and Limitations +## Sandbox notes and limitations - Use the `Agg` backend; the sandbox has no display. - Keep charts legible: labeled axes, a title, and a legend when multiple series are shown. @@ -167,3 +192,51 @@ filename in the report and do not invent or rename it later. than fabricating a chart. - Reference charts only by `artifact://`; the runtime assigns the durable id and rewrites the reference for the UI, PDF export, and the packaged skill CLI. + +--- + +# Inline Mode (chart spec) + +When there is no `execute` tool, present numbers that compare multiple things (a ranking or +top-N across entities, a distribution or counts across categories, a trend over an +ordered/time axis, or gains vs losses) as an inline chart, not just prose or a table. Lead +with a one-sentence verdict, then the chart. + +- Emit the chart as a fenced code block tagged `chart` holding a SINGLE line of valid JSON + (no comments, no trailing commas), right after the sentence that introduces it. +- Chart to reveal the pattern and state the verdict in prose. Inline `chart` blocks render + only in the web app, so ALSO place a compact markdown table of the same values immediately + after each chart, keeping PDF, Markdown, API, and CLI exports readable. +- At most 3 charts per section, and put each chart before any table. +- A single value or a one-entity yes/no result is NOT a chart: emit a KPI-only block, a + fenced `chart` block whose JSON has just `title` and `kpis`. + +Chart types: `bar` (category magnitudes), `hbar` (rankings with long text labels), +`line`/`area` (a trend across an ordered axis), `grouped-bar` (2-4 series per category), +`delta` (gains vs losses around zero). + +Spec fields: `type`; `title` (short) and optional `subtitle`; `x` = `{ "key": "", "label": "optional" }`; optional `y` = `{ "label": "optional unit", "format": "number | +compact | percent | currency" }`; `series` = `[ { "key": "", "label": "optional", +"color": "green | blue | amber | red" } ]`; `data` = rows as objects with raw numbers (fractions +0-1 for `percent`); optional `kpis` = `[ { "label": "...", "value": "preformatted", "tone": "accent +| warn | alarm" } ]`. A `delta` chart encodes exactly one series. + +Example (ranking): +```chart +{"type":"hbar","title":"Top suppliers by late shipments","x":{"key":"supplier"},"y":{"format":"number"},"series":[{"key":"late","color":"amber"}],"data":[{"supplier":"Acme","late":42},{"supplier":"Globex","late":31},{"supplier":"Initech","late":19}]} +``` + +Example (single value, KPI-only): +```chart +{"title":"On-time delivery rate","kpis":[{"label":"On-time","value":"92.4%","tone":"accent"}]} +``` + +For several related trends over time, emit one fenced `chart-carousel` block holding a SINGLE +line of JSON with at least two line-chart specs: `{ "title": "...", "charts": [ , ... ] }`. + +Example (related trends, carousel): +```chart-carousel +{"title":"Quarterly delivery trends","charts":[{"type":"line","title":"On-time delivery rate","x":{"key":"quarter"},"y":{"format":"percent"},"series":[{"key":"rate","color":"green"}],"data":[{"quarter":"Q1","rate":0.88},{"quarter":"Q2","rate":0.90},{"quarter":"Q3","rate":0.93}]},{"type":"line","title":"Late shipments","x":{"key":"quarter"},"y":{"format":"number"},"series":[{"key":"late","color":"amber"}],"data":[{"quarter":"Q1","late":52},{"quarter":"Q2","late":41},{"quarter":"Q3","late":28}]}]} +``` diff --git a/tests/aiq_agent/agents/deep_researcher/test_chart_artifact_contract.py b/tests/aiq_agent/agents/deep_researcher/test_chart_artifact_contract.py index e0015a4e5..a81f982bf 100644 --- a/tests/aiq_agent/agents/deep_researcher/test_chart_artifact_contract.py +++ b/tests/aiq_agent/agents/deep_researcher/test_chart_artifact_contract.py @@ -13,7 +13,7 @@ def test_chart_skill_uses_runtime_argument_instead_of_executable_placeholder() -> None: - skill = (_AGENT_ROOT / "skills" / "research" / "chart-generation" / "SKILL.md").read_text(encoding="utf-8") + skill = (_AGENT_ROOT / "skills" / "visualization" / "chart-generation" / "SKILL.md").read_text(encoding="utf-8") assert 'ARTIFACT_DIR = ""' not in skill assert '"path": "' not in skill @@ -26,6 +26,7 @@ def test_writer_runs_chart_script_with_rendered_per_job_paths() -> None: rendered = render_prompt_template( prompt, current_datetime="2026-07-09", + skills_enabled=True, execution_enabled=True, parent_report_context_available=False, sandbox_workdir="/sandbox/job-123", diff --git a/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py b/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py index 170411117..23238ac63 100644 --- a/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py +++ b/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py @@ -46,7 +46,14 @@ def test_openshell_workflow_only_diverges_for_sandbox_wiring() -> None: - """Keep the OpenShell workflow aligned with the standard web config.""" + """Keep the OpenShell workflow aligned with the standard web config. + + Both configs now wire the on-demand visualization skill, so the skill + wiring is dropped from each before the structural comparison. OpenShell's + skills block additionally carries the sandbox-gated research and synthesis + collections, leaving the sandbox function and its agent ref as the only + remaining divergence. + """ def load(path: str) -> dict[str, Any]: text = Path(path).read_text(encoding="utf-8") @@ -55,16 +62,24 @@ def load(path: str) -> dict[str, Any]: standard = load("configs/config_web_default_llamaindex.yml") openshell = load("configs/config_openshell.yml") + + standard_functions = standard["functions"].copy() openshell_functions = openshell["functions"].copy() + + assert standard_functions.pop("deep_research_skills") == { + "_type": "deep_research_skills", + "agents": {"writer-agent": ["visualization"]}, + } openshell_functions.pop("deep_research_skills") openshell_functions.pop("deep_research_sandbox") - openshell_functions["deep_research_agent"] = openshell_functions["deep_research_agent"].copy() - openshell_functions["deep_research_agent"].pop("skills") + for functions in (standard_functions, openshell_functions): + functions["deep_research_agent"] = functions["deep_research_agent"].copy() + functions["deep_research_agent"].pop("skills") openshell_functions["deep_research_agent"].pop("sandbox") assert openshell["general"] == standard["general"] assert openshell["llms"] == standard["llms"] - assert openshell_functions == standard["functions"] + assert openshell_functions == standard_functions assert openshell["workflow"] == standard["workflow"] @@ -100,6 +115,7 @@ def test_builtin_skill_collections_are_discovered(self) -> None: assert collections["research"] == "/skills/research/" assert collections["synthesis"] == "/skills/synthesis/" + assert collections["visualization"] == "/skills/visualization/" def test_nested_skill_collections_are_discovered(self, tmp_path) -> None: skill_dir = tmp_path / "finance" / "earnings" / "quarterly-summary" diff --git a/tests/aiq_agent/agents/test_result_chart_prompt.py b/tests/aiq_agent/agents/test_result_chart_prompt.py index f1a9db2ac..0898e9456 100644 --- a/tests/aiq_agent/agents/test_result_chart_prompt.py +++ b/tests/aiq_agent/agents/test_result_chart_prompt.py @@ -12,12 +12,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Guards the result-chart contract embedded in the researcher/writer prompts. +"""Guards the result-chart contract wherever the agents document it. -The shallow researcher and deep-research writer both instruct the agent to emit -declarative ``chart`` specs that the UI's ResultChart renderer parses. These -tests ensure the worked examples in those prompts stay valid JSON that matches -the renderer's schema, so the documented contract cannot silently drift. +The inline ``chart`` spec used to be embedded in the deep-research writer prompt. +It now lives in the on-demand ``chart-generation`` skill, so the always-on writer +prompt stays short and both delivery modes are driven by the skill. The shallow +researcher keeps its own inline copy because the shallow path has no skill runtime. + +These tests ensure the worked examples in every source that still documents the +contract (the shallow prompt and the chart skill) stay valid JSON that matches the +UI ResultChart zod schema, and that the writer prompt now delegates to the skill +instead of embedding the contract. """ import json @@ -29,52 +34,87 @@ from aiq_agent.common import render_prompt_template _AGENTS = Path(__file__).resolve().parents[3] / "src" / "aiq_agent" / "agents" -_PROMPTS = { +_WRITER = _AGENTS / "deep_researcher" / "prompts" / "writer.j2" +_CHART_SKILL = _AGENTS / "deep_researcher" / "skills" / "visualization" / "chart-generation" / "SKILL.md" + +# The inline ``chart`` contract now lives in exactly these two places: the shallow +# researcher prompt (no skill runtime) and the chart-generation skill (both the +# deep researcher and writer read it on demand). +_CONTRACT_SOURCES = { "shallow": _AGENTS / "shallow_researcher" / "prompts" / "researcher.j2", - "writer": _AGENTS / "deep_researcher" / "prompts" / "writer.j2", + "skill": _CHART_SKILL, } -# Kept in lockstep with CHART_TYPES in the UI's ResultChart/types.ts. +# Kept in lockstep with the UI ResultChart schema in types.ts. _CHART_TYPES = {"bar", "hbar", "line", "area", "grouped-bar", "delta"} +_CHART_COLORS = {"green", "blue", "amber", "red", "neutral"} +_VALUE_FORMATS = {"number", "compact", "percent", "currency"} +_KPI_TONES = {"default", "accent", "warn", "alarm"} + _CHART_BLOCK = re.compile(r"```chart\n(.*?)\n```", re.DOTALL) _CAROUSEL_BLOCK = re.compile(r"```chart-carousel\n(.*?)\n```", re.DOTALL) -def _chart_examples(prompt_key: str) -> list[dict]: - text = _PROMPTS[prompt_key].read_text() +def _chart_examples(source_key: str) -> list[dict]: + text = _CONTRACT_SOURCES[source_key].read_text() return [json.loads(block) for block in _CHART_BLOCK.findall(text)] -@pytest.mark.parametrize("prompt_key", list(_PROMPTS)) -def test_prompt_defines_the_chart_contract(prompt_key: str) -> None: - text = _PROMPTS[prompt_key].read_text() - assert "## Presenting Data (Charts)" in text +def _assert_kpis_match_schema(kpis: list) -> None: + assert 1 <= len(kpis) <= 4 + for kpi in kpis: + assert kpi["label"] and kpi["value"] + if "tone" in kpi: + assert kpi["tone"] in _KPI_TONES + + +def _assert_full_chart_matches_schema(spec: dict) -> None: + """Mirror the ResultChart zod ChartSpecSchema shape.""" + assert spec["type"] in _CHART_TYPES + assert spec["title"] + assert spec["x"]["key"] + if "y" in spec and spec["y"].get("format") is not None: + assert spec["y"]["format"] in _VALUE_FORMATS + assert 1 <= len(spec["series"]) <= 6 + for series in spec["series"]: + assert series["key"] + if series.get("color") is not None: + assert series["color"] in _CHART_COLORS + # A delta chart colors bars by sign with no legend, so it encodes one series. + if spec["type"] == "delta": + assert len(spec["series"]) == 1 + assert 1 <= len(spec["data"]) <= 60 + assert all(isinstance(row, dict) for row in spec["data"]) + if "kpis" in spec: + _assert_kpis_match_schema(spec["kpis"]) + + +@pytest.mark.parametrize("source_key", list(_CONTRACT_SOURCES)) +def test_contract_source_defines_the_chart_contract(source_key: str) -> None: + text = _CONTRACT_SOURCES[source_key].read_text() assert "chart-carousel" in text for chart_type in _CHART_TYPES: - assert chart_type in text, f"{prompt_key} prompt omits chart type {chart_type!r}" + assert chart_type in text, f"{source_key} omits chart type {chart_type!r}" -@pytest.mark.parametrize("prompt_key", list(_PROMPTS)) -def test_prompt_carousel_examples_match_the_schema(prompt_key: str) -> None: - text = _PROMPTS[prompt_key].read_text() +@pytest.mark.parametrize("source_key", list(_CONTRACT_SOURCES)) +def test_contract_source_carousel_examples_match_the_schema(source_key: str) -> None: + text = _CONTRACT_SOURCES[source_key].read_text() blocks = _CAROUSEL_BLOCK.findall(text) - assert blocks, f"{prompt_key} prompt has no ```chart-carousel example" + assert blocks, f"{source_key} has no ```chart-carousel example" for block in blocks: carousel = json.loads(block) assert carousel["title"] - assert len(carousel["charts"]) >= 2 + assert 2 <= len(carousel["charts"]) <= 12 for chart in carousel["charts"]: assert chart["type"] == "line" - assert chart["title"], f"{prompt_key} carousel child is missing a non-empty title" - assert chart["x"]["key"] - assert chart["series"] and all(s["key"] for s in chart["series"]) - assert chart["data"] and all(isinstance(row, dict) for row in chart["data"]) + _assert_full_chart_matches_schema(chart) -@pytest.mark.parametrize("prompt_key", list(_PROMPTS)) -def test_prompt_chart_examples_match_the_schema(prompt_key: str) -> None: - examples = _chart_examples(prompt_key) - assert examples, f"{prompt_key} prompt has no ```chart example" +@pytest.mark.parametrize("source_key", list(_CONTRACT_SOURCES)) +def test_contract_source_chart_examples_match_the_schema(source_key: str) -> None: + examples = _chart_examples(source_key) + assert examples, f"{source_key} has no ```chart example" saw_full_chart = False saw_kpi_only = False @@ -83,16 +123,26 @@ def test_prompt_chart_examples_match_the_schema(prompt_key: str) -> None: assert spec["title"] if "type" in spec: saw_full_chart = True - assert spec["type"] in _CHART_TYPES - assert spec["x"]["key"] - assert spec["series"] and all(s["key"] for s in spec["series"]) - assert spec["data"] and all(isinstance(row, dict) for row in spec["data"]) + _assert_full_chart_matches_schema(spec) else: saw_kpi_only = True - assert spec["kpis"] and all(k["label"] and k["value"] for k in spec["kpis"]) + _assert_kpis_match_schema(spec["kpis"]) + + assert saw_full_chart, f"{source_key} should show a full chart example" + assert saw_kpi_only, f"{source_key} should show a KPI-only example" - assert saw_full_chart, f"{prompt_key} prompt should show a full chart example" - assert saw_kpi_only, f"{prompt_key} prompt should show a KPI-only example" + +def test_chart_skill_covers_both_delivery_modes() -> None: + """The single skill drives sandbox (PNG artifact) and non-sandbox (inline spec) charts.""" + skill = _CHART_SKILL.read_text() + assert "## Choose your mode" in skill + # Sandbox mode markers. + assert "artifact://" in skill + assert "make_chart.py" in skill + assert "matplotlib" in skill + # Inline mode markers. + assert "```chart" in skill + assert "chart-carousel" in skill _WRITER_RENDER_CONTEXT = { @@ -104,21 +154,32 @@ def test_prompt_chart_examples_match_the_schema(prompt_key: str) -> None: } -def _render_writer(*, execution_enabled: bool) -> str: +def _render_writer(*, skills_enabled: bool, execution_enabled: bool) -> str: return render_prompt_template( - _PROMPTS["writer"].read_text(), + _WRITER.read_text(), + skills_enabled=skills_enabled, execution_enabled=execution_enabled, **_WRITER_RENDER_CONTEXT, ) -def test_writer_gates_inline_charts_to_the_non_sandbox_path() -> None: - with_sandbox = _render_writer(execution_enabled=True) - assert "## Figures" in with_sandbox - assert "## Presenting Data" not in with_sandbox - assert "```chart" not in with_sandbox +def test_writer_prompt_no_longer_embeds_the_inline_chart_contract() -> None: + text = _WRITER.read_text() + assert "## Presenting Data (Charts)" not in text + assert "```chart" not in text + + +@pytest.mark.parametrize("execution_enabled", [True, False]) +def test_writer_delegates_charts_to_the_skill_when_skills_enabled(execution_enabled: bool) -> None: + rendered = _render_writer(skills_enabled=True, execution_enabled=execution_enabled) + assert "## Figures and Charts" in rendered + assert "chart-generation" in rendered + # The contract is delivered on demand by the skill, never inlined here. + assert "```chart" not in rendered + - without_sandbox = _render_writer(execution_enabled=False) - assert "## Presenting Data (Charts)" in without_sandbox - assert "```chart" in without_sandbox - assert "## Figures" not in without_sandbox +def test_writer_degrades_to_a_table_without_skills() -> None: + rendered = _render_writer(skills_enabled=False, execution_enabled=False) + assert "## Figures and Charts" in rendered + assert "chart-generation" not in rendered + assert "```chart" not in rendered From 7cb2a101febb6be4d4ef32825c5493c218795370 Mon Sep 17 00:00:00 2001 From: Manush Maheshwari Date: Tue, 28 Jul 2026 15:26:14 -0700 Subject: [PATCH 2/6] fix(deep-research): address #389 review (writer-specific chart gating, prompt/skill ownership, migration docs, lint) Gate the writer chart section on a writer-specific capability instead of the global skills flag, and tighten the prompt/skill ownership boundary so the chart procedure lives only in the visualization skill. - deepagents_runtime: add CHART_SKILL_NAME and DeepAgentsRuntime.agent_has_chart_skill, which maps an agent's resolved skill sources back to collection dirs and checks for a chart-generation SKILL.md on disk (false when the agent has no sources). - factory: render the writer prompt with chart_skill_enabled derived from agent_has_chart_skill(WRITER_AGENT); leave the global skills_enabled injection for the other prompts unchanged. - writer.j2: gate the chart section on chart_skill_enabled with the markdown-table fallback preserved; trim it to the earned-figure rule, a pointer to the skill as the controlling procedure, and runtime context only (the real sandbox_workdir and sandbox_artifact_dir paths). The detailed sandbox procedure now lives solely in the skill. - chart-generation SKILL.md: make the sandbox-vs-inline mode precedence explicit (mutually exclusive, sandbox always wins when execute is available) and add the MD031 blank lines before the three chart example fences. - docs/CHANGELOG: document the chart-generation move from the research collection to visualization (charts are no longer sandbox-gated; external writer configs that relied on research for charts must now assign visualization). - tests: harden the OpenShell normalization assertion to require writer-agent visualization before discarding, add an agent_has_chart_skill unit test, and update the writer prompt gating tests to the chart_skill_enabled flag. Signed-off-by: Manush Maheshwari --- CHANGELOG.md | 1 + .../customization/configuration-reference.md | 9 +++++++++ .../agents/deep_researcher/deepagents_runtime.py | 13 +++++++++++++ src/aiq_agent/agents/deep_researcher/factory.py | 5 ++++- .../agents/deep_researcher/prompts/writer.j2 | 13 ++++++++----- .../skills/visualization/chart-generation/SKILL.md | 8 ++++++++ .../test_chart_artifact_contract.py | 10 ++++++---- .../deep_researcher/test_deepagents_runtime.py | 14 +++++++++++++- tests/aiq_agent/agents/test_result_chart_prompt.py | 13 +++++++------ 9 files changed, 69 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 636bc2666..6fd274ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ release; the candidate will be stabilized before the final `v2.2.0` release. - Routed deep research now uses explicit source-router, structured planner, concurrent researcher, and writer roles, with bounded source-tool batching and no research-plan approval step - Report follow-up supports answers over a completed report, child-job cosmetic rewrites, and delta research that carries the parent report forward as context - Clarification is more targeted: it can search for context before asking the user to narrow scope or choose an output shape +- The writer produces figures through an on-demand `chart-generation` skill in the new `visualization` skill collection, rendering a sandbox PNG artifact when a sandbox is available and an inline chart spec otherwise; the skill moved out of the always-on writer prompt and out of the `research` collection, so writer configs that relied on `research` for charts must now also assign `visualization` **Sources and integrations** diff --git a/docs/source/customization/configuration-reference.md b/docs/source/customization/configuration-reference.md index aec309689..26ae6469c 100644 --- a/docs/source/customization/configuration-reference.md +++ b/docs/source/customization/configuration-reference.md @@ -496,6 +496,15 @@ ordered `preferred_tools` and `fallback_tools` guidance on each `ResearchQuery`; request-filtered callable set. Refer to [Tools and Sources](./tools-and-sources.md#automatic-source-routing) and the [`config_domain_routing_and_skills.yml`](../../../configs/config_domain_routing_and_skills.yml) reference profile. +```{note} +**Migration: `chart-generation` moved to the `visualization` collection.** The built-in +`chart-generation` skill previously lived in the `research` collection; it now lives in its own +`visualization` collection, and charts are no longer sandbox-gated. A `writer-agent` that +previously listed only `research` to obtain charts must now also list `visualization` in its +`deep_research_skills` assignment. The shipped configs already include `visualization` for the +writer; update any external config that relied on `research` for charts. +``` + --- ## `workflow` Section diff --git a/src/aiq_agent/agents/deep_researcher/deepagents_runtime.py b/src/aiq_agent/agents/deep_researcher/deepagents_runtime.py index 03d412f1e..6e89310cb 100644 --- a/src/aiq_agent/agents/deep_researcher/deepagents_runtime.py +++ b/src/aiq_agent/agents/deep_researcher/deepagents_runtime.py @@ -46,6 +46,7 @@ BUILTIN_SKILL_SOURCE = "/skills/" SHARED_ROUTE = "/shared/" SKILL_AGENT_NAMES = frozenset({"researcher-agent", "writer-agent"}) +CHART_SKILL_NAME = "chart-generation" DEFAULT_WORKDIR = "/workspace" @@ -267,6 +268,18 @@ def skill_sources_for(self, agent_name: str) -> list[str] | None: sources = self._skill_sources_by_agent.get(agent_name) return list(sources) if sources else None + def agent_has_chart_skill(self, agent_name: str) -> bool: + """Return true when the agent's assigned collections provide the chart skill on disk.""" + sources = self._skill_sources_by_agent.get(agent_name) + if not sources: + return False + collection_for_source = {source: name for name, source in discover_skill_collections().items()} + return any( + (BUILTIN_SKILLS_DIR / collection_for_source[source] / CHART_SKILL_NAME / "SKILL.md").exists() + for source in sources + if source in collection_for_source + ) + @property def workdir(self) -> str: """Job-scoped sandbox working directory (or the default when no sandbox).""" diff --git a/src/aiq_agent/agents/deep_researcher/factory.py b/src/aiq_agent/agents/deep_researcher/factory.py index a1b4d4431..ccb29e814 100644 --- a/src/aiq_agent/agents/deep_researcher/factory.py +++ b/src/aiq_agent/agents/deep_researcher/factory.py @@ -485,7 +485,10 @@ def build_deep_research_subagents(context: DeepResearchGraphContext) -> list[dic TodoSuppressionMiddleware(), RequiredOutputFileMiddleware(tracker=context.final_report_tracker), ], - prompt_values={"parent_report_context_available": context.parent_report_context_available}, + prompt_values={ + "parent_report_context_available": context.parent_report_context_available, + "chart_skill_enabled": context.runtime.agent_has_chart_skill(WRITER_AGENT), + }, skills=context.skill_sources(WRITER_AGENT), ), ) diff --git a/src/aiq_agent/agents/deep_researcher/prompts/writer.j2 b/src/aiq_agent/agents/deep_researcher/prompts/writer.j2 index d17e66023..51e160e1d 100644 --- a/src/aiq_agent/agents/deep_researcher/prompts/writer.j2 +++ b/src/aiq_agent/agents/deep_researcher/prompts/writer.j2 @@ -75,15 +75,18 @@ When synthesizing: - Do not expose internal `evidence_judgment` scores or rationale in the final answer unless the user explicitly asks for methodology. - Err on the side of more useful information rather than less, while staying focused on the requested answer shape. -{% if skills_enabled %}## Figures and Charts +{% if chart_skill_enabled %}## Figures and Charts -When `answer_strategy.required_components` calls for a chart (a ranking or top-N, a distribution across categories, a trend over an ordered axis, gains vs losses, or a single headline value), consult the `chart-generation` skill and follow it as the controlling procedure. Read its `SKILL.md` first. The skill defines the full chart contract and both delivery modes, so this base prompt stays free of chart syntax. +Charts are available for this answer. When `answer_strategy.required_components` calls for a chart (a ranking or top-N, a distribution across categories, a trend over an ordered axis, gains vs losses, or a single headline value), consult the `chart-generation` skill and read its `SKILL.md` first. The skill is the controlling procedure: it owns the full chart contract, both delivery modes, and the mode decision, so this base prompt carries no chart syntax or steps. + +A figure is earned: chart only source-anchored, reasonably complete data. If a series is mostly undisclosed or mixes metric definitions, present the table (which shows the gaps) and state the limitation in one sentence instead of a misleading chart. + +Runtime context for the skill: {% if execution_enabled %} -A sandbox is available, so the skill renders a PNG artifact. Write the plotting script to `{{ sandbox_workdir }}/make_chart.py` and run it exactly as `python3 {{ sandbox_workdir }}/make_chart.py {{ sandbox_artifact_dir }}` so the image plus its `manifest.json` land in the only directory the runtime harvests, then embed it once where it is discussed with `![](artifact://)`. Never put a literal `` or `` placeholder into code or a command, and never paste plotting code, a sandbox path, or base64 image data into the report. +- A sandbox is available, so the skill renders a PNG artifact. Your per-job working directory is `{{ sandbox_workdir }}` and the harvested artifact directory is `{{ sandbox_artifact_dir }}`; give the skill these exact paths where it asks for `sandbox_workdir` and `sandbox_artifact_dir`. {% else %} -There is no sandbox, so the skill emits an inline `chart` (or `chart-carousel`) JSON spec that the web app renders, followed by a compact portable table of the same values. +- There is no sandbox, so the skill delivers the chart inline in the report. {% endif %} -A figure is earned: chart only source-anchored, reasonably complete data. If a series is mostly undisclosed or mixes metric definitions, present the table (which shows the gaps) and state the limitation in one sentence instead of a misleading chart. {% else %}## Figures and Charts diff --git a/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md b/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md index c372582ba..bdfd2bb1d 100644 --- a/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md +++ b/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md @@ -27,6 +27,11 @@ follow the matching section below. `chart` JSON spec that the web app renders, plus a portable Markdown table. Follow **Inline Mode (chart spec)** below. Do NOT attempt to run code or produce a PNG. +The two modes are mutually exclusive and are selected only by tool availability: pick exactly +one and emit only that output path. When an `execute` tool (sandbox) is available you must use +Sandbox mode and must not emit an inline `chart` spec; only when no `execute` tool exists do you +use Inline mode. Never produce both a PNG artifact and an inline spec for the same figure. + Both modes share the same discipline: a chart confers authority, so it must be earned. ## Data sufficiency (earn the chart, both modes) @@ -223,11 +228,13 @@ compact | percent | currency" }`; `series` = `[ { "key": "", "lab | warn | alarm" } ]`. A `delta` chart encodes exactly one series. Example (ranking): + ```chart {"type":"hbar","title":"Top suppliers by late shipments","x":{"key":"supplier"},"y":{"format":"number"},"series":[{"key":"late","color":"amber"}],"data":[{"supplier":"Acme","late":42},{"supplier":"Globex","late":31},{"supplier":"Initech","late":19}]} ``` Example (single value, KPI-only): + ```chart {"title":"On-time delivery rate","kpis":[{"label":"On-time","value":"92.4%","tone":"accent"}]} ``` @@ -237,6 +244,7 @@ line of JSON with at least two line-chart specs: `{ "title": "...", "charts": [ spec>, ... ] }`. Example (related trends, carousel): + ```chart-carousel {"title":"Quarterly delivery trends","charts":[{"type":"line","title":"On-time delivery rate","x":{"key":"quarter"},"y":{"format":"percent"},"series":[{"key":"rate","color":"green"}],"data":[{"quarter":"Q1","rate":0.88},{"quarter":"Q2","rate":0.90},{"quarter":"Q3","rate":0.93}]},{"type":"line","title":"Late shipments","x":{"key":"quarter"},"y":{"format":"number"},"series":[{"key":"late","color":"amber"}],"data":[{"quarter":"Q1","late":52},{"quarter":"Q2","late":41},{"quarter":"Q3","late":28}]}]} ``` diff --git a/tests/aiq_agent/agents/deep_researcher/test_chart_artifact_contract.py b/tests/aiq_agent/agents/deep_researcher/test_chart_artifact_contract.py index a81f982bf..cfd40d5d8 100644 --- a/tests/aiq_agent/agents/deep_researcher/test_chart_artifact_contract.py +++ b/tests/aiq_agent/agents/deep_researcher/test_chart_artifact_contract.py @@ -21,12 +21,12 @@ def test_chart_skill_uses_runtime_argument_instead_of_executable_placeholder() - assert 'ARTIFACT_DIR / "manifest.json"' in skill -def test_writer_runs_chart_script_with_rendered_per_job_paths() -> None: +def test_writer_provides_rendered_per_job_paths_as_chart_skill_context() -> None: prompt = (_AGENT_ROOT / "prompts" / "writer.j2").read_text(encoding="utf-8") rendered = render_prompt_template( prompt, current_datetime="2026-07-09", - skills_enabled=True, + chart_skill_enabled=True, execution_enabled=True, parent_report_context_available=False, sandbox_workdir="/sandbox/job-123", @@ -34,5 +34,7 @@ def test_writer_runs_chart_script_with_rendered_per_job_paths() -> None: user_info=None, ) - assert "python3 /sandbox/job-123/make_chart.py /sandbox/job-123/aiq-artifacts" in rendered - assert "Never put a literal `` or ``" in rendered + assert "/sandbox/job-123" in rendered + assert "/sandbox/job-123/aiq-artifacts" in rendered + assert "" not in rendered + assert "" not in rendered diff --git a/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py b/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py index 23238ac63..d413a2766 100644 --- a/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py +++ b/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py @@ -70,7 +70,8 @@ def load(path: str) -> dict[str, Any]: "_type": "deep_research_skills", "agents": {"writer-agent": ["visualization"]}, } - openshell_functions.pop("deep_research_skills") + openshell_skills = openshell_functions.pop("deep_research_skills") + assert "visualization" in openshell_skills["agents"]["writer-agent"] openshell_functions.pop("deep_research_sandbox") for functions in (standard_functions, openshell_functions): functions["deep_research_agent"] = functions["deep_research_agent"].copy() @@ -158,6 +159,17 @@ def test_skills_only_adds_skills_route(self) -> None: assert isinstance(backend.routes[BUILTIN_SKILL_SOURCE], FilesystemBackend) assert runtime.skill_sources_for("writer-agent") == [SYNTHESIS_SKILL_SOURCE] + def test_agent_has_chart_skill_tracks_visualization_collection(self) -> None: + runtime = DeepAgentsRuntime( + skills=DeepResearchSkillsConfig( + agents={"writer-agent": ("visualization",), "researcher-agent": ("synthesis",)}, + ), + ) + + assert runtime.agent_has_chart_skill("writer-agent") is True + assert runtime.agent_has_chart_skill("researcher-agent") is False + assert runtime.agent_has_chart_skill("planner-agent") is False + def test_sandbox_only_adds_shared_route(self) -> None: fake_sandbox = MagicMock() with patch( diff --git a/tests/aiq_agent/agents/test_result_chart_prompt.py b/tests/aiq_agent/agents/test_result_chart_prompt.py index 0898e9456..c5d032d9d 100644 --- a/tests/aiq_agent/agents/test_result_chart_prompt.py +++ b/tests/aiq_agent/agents/test_result_chart_prompt.py @@ -154,10 +154,10 @@ def test_chart_skill_covers_both_delivery_modes() -> None: } -def _render_writer(*, skills_enabled: bool, execution_enabled: bool) -> str: +def _render_writer(*, chart_skill_enabled: bool, execution_enabled: bool) -> str: return render_prompt_template( _WRITER.read_text(), - skills_enabled=skills_enabled, + chart_skill_enabled=chart_skill_enabled, execution_enabled=execution_enabled, **_WRITER_RENDER_CONTEXT, ) @@ -170,16 +170,17 @@ def test_writer_prompt_no_longer_embeds_the_inline_chart_contract() -> None: @pytest.mark.parametrize("execution_enabled", [True, False]) -def test_writer_delegates_charts_to_the_skill_when_skills_enabled(execution_enabled: bool) -> None: - rendered = _render_writer(skills_enabled=True, execution_enabled=execution_enabled) +def test_writer_delegates_charts_to_the_skill_when_chart_skill_enabled(execution_enabled: bool) -> None: + rendered = _render_writer(chart_skill_enabled=True, execution_enabled=execution_enabled) assert "## Figures and Charts" in rendered assert "chart-generation" in rendered # The contract is delivered on demand by the skill, never inlined here. assert "```chart" not in rendered -def test_writer_degrades_to_a_table_without_skills() -> None: - rendered = _render_writer(skills_enabled=False, execution_enabled=False) +def test_writer_degrades_to_a_table_without_the_chart_skill() -> None: + rendered = _render_writer(chart_skill_enabled=False, execution_enabled=False) assert "## Figures and Charts" in rendered assert "chart-generation" not in rendered + assert "compact Markdown table" in rendered assert "```chart" not in rendered From de09dbbd0e0cf606f62748f2679e22e89f23acc3 Mon Sep 17 00:00:00 2001 From: Manush Maheshwari Date: Tue, 28 Jul 2026 19:55:03 -0700 Subject: [PATCH 3/6] fix(deep-research): wire benchmark configs and correct chart-skill docs (#389 review) Wire the visualization skill into the four deep-research benchmark configs so inline charts are preserved there too, and correct the docs that still placed chart-generation in the research collection. Signed-off-by: Manush Maheshwari --- docs/source/architecture/agents/deep-researcher.md | 9 +++++---- docs/source/examples/skills-sandbox/index.md | 6 ++++-- .../configs/config_deep_research_bench.yml | 6 ++++++ .../configs/config_deep_research_bench_profiling.yml | 6 ++++++ .../deepsearch_qa/configs/config_deepsearch_qa.yml | 6 ++++++ .../benchmarks/freshqa/configs/config_full_workflow.yml | 6 ++++++ 6 files changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/source/architecture/agents/deep-researcher.md b/docs/source/architecture/agents/deep-researcher.md index f7ecfdc20..bb4cace3e 100644 --- a/docs/source/architecture/agents/deep-researcher.md +++ b/docs/source/architecture/agents/deep-researcher.md @@ -137,10 +137,11 @@ runtime dependencies, not additional agents: | Inference and source tools | LLM calls, source-tool calls, credentials, orchestration state, and `/shared/` remain in the AI-Q process. Only generated code and job-workspace files cross the sandbox boundary. | The shipped `config_domain_routing_and_skills.yml` profile assigns the -`research` collection to researcher workers and the `synthesis` collection to -the writer. The research collection currently includes chart generation, -table analysis, forecast analysis, and lightweight calculations. The synthesis -collection includes long-form and prediction report writers. A skill provides +`research` collection to researcher workers and the `synthesis` and +`visualization` collections to the writer. The research collection currently +includes table analysis, forecast analysis, and lightweight calculations. The +synthesis collection includes long-form and prediction report writers, and the +visualization collection provides chart generation. A skill provides instructions; only skills that invoke `execute` require the optional sandbox. Modal and OpenShell implement the same provider-neutral job-scoped contract. diff --git a/docs/source/examples/skills-sandbox/index.md b/docs/source/examples/skills-sandbox/index.md index d1c63f658..dc9d134cf 100644 --- a/docs/source/examples/skills-sandbox/index.md +++ b/docs/source/examples/skills-sandbox/index.md @@ -33,8 +33,9 @@ The built-in collections currently expose these role-oriented skills: | Collection | Default assignment | Skills | | ---------- | ------------------ | ------ | -| `research` | `researcher-agent` | `chart-generation`, `data-table-analysis`, `forecast-analysis`, `lightweight-calculation` | +| `research` | `researcher-agent` | `data-table-analysis`, `forecast-analysis`, `lightweight-calculation` | | `synthesis` | `writer-agent` | `long-form-report-writer`, `prediction-report-writer` | +| `visualization` | `writer-agent` | `chart-generation` | The assignment is configurable. Skill definitions stay host-side and read-only; only workflows that invoke `execute` require a sandbox. @@ -78,6 +79,7 @@ functions: - research writer-agent: - synthesis + - visualization require_sandbox: - research @@ -104,7 +106,7 @@ functions: sandbox: deep_research_sandbox ``` -AI-Q validates the public skill collection names (`research`, `synthesis`) and resolves them to DeepAgents source paths internally. When skills are configured, AI-Q mounts the configured built-in skill collections into the DeepAgents virtual filesystem. When the sandbox ref is present, DeepAgents `execute` calls run in the configured provider. Modal creates a fresh sandbox named for the job. +AI-Q validates the public skill collection names (`research`, `synthesis`, `visualization`) and resolves them to DeepAgents source paths internally. When skills are configured, AI-Q mounts the configured built-in skill collections into the DeepAgents virtual filesystem. When the sandbox ref is present, DeepAgents `execute` calls run in the configured provider. Modal creates a fresh sandbox named for the job. In the reference async API flow, artifact capture uses the job database configured by `general.front_end.db_url` (`NAT_JOB_STORE_DB_URL`) for metadata. Without that job-scoped database URL, as in a direct diff --git a/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench.yml b/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench.yml index e897396f7..730604d1e 100644 --- a/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench.yml +++ b/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench.yml @@ -50,6 +50,11 @@ functions: max_results: 2 advanced_search: true + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent orchestrator_llm: gpt_oss_llm @@ -61,6 +66,7 @@ functions: tools: - paper_search_tool - advanced_web_search_tool + skills: deep_research_skills workflow: _type: deep_research_workflow diff --git a/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench_profiling.yml b/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench_profiling.yml index 45139aeb2..3fe33d00e 100644 --- a/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench_profiling.yml +++ b/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench_profiling.yml @@ -55,6 +55,11 @@ functions: max_results: 2 advanced_search: true + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent orchestrator_llm: openai_gpt_5_2 @@ -66,6 +71,7 @@ functions: tools: - paper_search_tool - advanced_web_search_tool + skills: deep_research_skills workflow: _type: deep_research_workflow diff --git a/frontends/benchmarks/deepsearch_qa/configs/config_deepsearch_qa.yml b/frontends/benchmarks/deepsearch_qa/configs/config_deepsearch_qa.yml index 2a19043b3..cef003bff 100644 --- a/frontends/benchmarks/deepsearch_qa/configs/config_deepsearch_qa.yml +++ b/frontends/benchmarks/deepsearch_qa/configs/config_deepsearch_qa.yml @@ -48,6 +48,11 @@ functions: _type: tavily_web_search max_results: 5 + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent orchestrator_llm: gpt_oss_llm @@ -58,6 +63,7 @@ functions: max_loops: 2 tools: - web_search_tool + skills: deep_research_skills workflow: _type: deep_research_workflow diff --git a/frontends/benchmarks/freshqa/configs/config_full_workflow.yml b/frontends/benchmarks/freshqa/configs/config_full_workflow.yml index ddd12579b..abd04bda1 100644 --- a/frontends/benchmarks/freshqa/configs/config_full_workflow.yml +++ b/frontends/benchmarks/freshqa/configs/config_full_workflow.yml @@ -66,6 +66,11 @@ functions: max_tool_iterations: 5 verbose: true + deep_research_skills: + _type: deep_research_skills + agents: + writer-agent: [visualization] + deep_research_agent: _type: deep_research_agent orchestrator_llm: gpt_oss_llm @@ -76,6 +81,7 @@ functions: max_loops: 2 tools: - web_search_tool + skills: deep_research_skills workflow: _type: chat_deepresearcher_agent From dcae9098c2f7a4a05198a723ea098eeab37f09c6 Mon Sep 17 00:00:00 2001 From: Manush Maheshwari Date: Tue, 28 Jul 2026 22:11:10 -0700 Subject: [PATCH 4/6] fix(mcp): expect deep_research_skills in public MCP config function set Wiring the visualization skill into config_mcp.yml adds a deep_research_skills function; update the public-config contract test to expect it. Signed-off-by: Manush Maheshwari --- mcp/tests/test_config_and_packaging.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mcp/tests/test_config_and_packaging.py b/mcp/tests/test_config_and_packaging.py index d47ae83dc..cb98fa8cc 100644 --- a/mcp/tests/test_config_and_packaging.py +++ b/mcp/tests/test_config_and_packaging.py @@ -67,6 +67,7 @@ def test_public_mcp_config_preserves_reference_orchestration_choices() -> None: "clarifier_agent", "data_sources", "deep_research_agent", + "deep_research_skills", "intent_classifier", "shallow_research_agent", "web_search_tool", From 1d1ad9c4a16a562a254a2782d22fd53cb4235ea3 Mon Sep 17 00:00:00 2001 From: Manush Maheshwari Date: Wed, 29 Jul 2026 20:32:03 -0700 Subject: [PATCH 5/6] refactor(deep-research): scope the visualization chart skill to the skills and sandbox configs (#389 review) Per Chantal's review, wire the on-demand visualization chart skill into only the two skills and sandbox example configs (config_domain_routing_and_skills and config_openshell) and remove the deep_research_skills block plus its skills ref from the nine default configs and the four benchmark configs. The writer.j2 chart_skill_enabled gating is unchanged, so the now-unwired configs fall back to the Markdown-table branch. Update the two affected tests and the CHANGELOG and configuration-reference migration note to match the new scope. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Manush Maheshwari --- CHANGELOG.md | 2 +- configs/config_cli_default.yml | 6 ---- configs/config_frontier_models.yml | 6 ---- configs/config_mcp.yml | 6 ---- configs/config_web_azure_ai_search.yml | 6 ---- configs/config_web_default_guardrails.yml | 6 ---- configs/config_web_default_llamaindex.yml | 6 ---- configs/config_web_frag.yml | 6 ---- configs/config_web_frag_mcp_auth.yml | 6 ---- configs/config_web_opensearch.yml | 6 ---- .../customization/configuration-reference.md | 9 ++--- .../configs/config_deep_research_bench.yml | 6 ---- .../config_deep_research_bench_profiling.yml | 6 ---- .../configs/config_deepsearch_qa.yml | 6 ---- .../freshqa/configs/config_full_workflow.yml | 6 ---- mcp/tests/test_config_and_packaging.py | 1 - .../test_deepagents_runtime.py | 34 +++++++++++-------- 17 files changed, 26 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fd274ce8..067baa9f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ release; the candidate will be stabilized before the final `v2.2.0` release. - Routed deep research now uses explicit source-router, structured planner, concurrent researcher, and writer roles, with bounded source-tool batching and no research-plan approval step - Report follow-up supports answers over a completed report, child-job cosmetic rewrites, and delta research that carries the parent report forward as context - Clarification is more targeted: it can search for context before asking the user to narrow scope or choose an output shape -- The writer produces figures through an on-demand `chart-generation` skill in the new `visualization` skill collection, rendering a sandbox PNG artifact when a sandbox is available and an inline chart spec otherwise; the skill moved out of the always-on writer prompt and out of the `research` collection, so writer configs that relied on `research` for charts must now also assign `visualization` +- The writer produces figures through an on-demand `chart-generation` skill in the new `visualization` skill collection, rendering a sandbox PNG artifact when a sandbox is available and an inline chart spec otherwise; the skill ships enabled only in the skills and sandbox example configs (`config_domain_routing_and_skills`, `config_openshell`), while every other config presents chart-worthy data as a Markdown table, and a writer that wants inline charts must be assigned the `visualization` collection (charts are not sandbox-gated) **Sources and integrations** diff --git a/configs/config_cli_default.yml b/configs/config_cli_default.yml index 0e39c13d0..18c6a7d62 100644 --- a/configs/config_cli_default.yml +++ b/configs/config_cli_default.yml @@ -125,11 +125,6 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -140,7 +135,6 @@ functions: writer_llm: nemotron_super_llm exclude_tools: - web_search_tool - skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_frontier_models.yml b/configs/config_frontier_models.yml index e6015fb2c..b3af9f0f1 100644 --- a/configs/config_frontier_models.yml +++ b/configs/config_frontier_models.yml @@ -155,11 +155,6 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -173,7 +168,6 @@ functions: # - paper_search_tool # Uncomment if SERPER_API_KEY is set - advanced_web_search_tool - knowledge_search - skills: deep_research_skills workflow: diff --git a/configs/config_mcp.yml b/configs/config_mcp.yml index fe603fc7c..6ffde33e0 100644 --- a/configs/config_mcp.yml +++ b/configs/config_mcp.yml @@ -100,11 +100,6 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -115,7 +110,6 @@ functions: writer_llm: nemotron_super_llm exclude_tools: - web_search_tool - skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_web_azure_ai_search.yml b/configs/config_web_azure_ai_search.yml index f1b4d7b94..bafbe6e8d 100644 --- a/configs/config_web_azure_ai_search.yml +++ b/configs/config_web_azure_ai_search.yml @@ -200,11 +200,6 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -217,7 +212,6 @@ functions: exclude_tools: # Remove basic variant; deep uses advanced_web_search_tool - web_search_tool verbose: true - skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_web_default_guardrails.yml b/configs/config_web_default_guardrails.yml index d579c132b..d419b2945 100644 --- a/configs/config_web_default_guardrails.yml +++ b/configs/config_web_default_guardrails.yml @@ -231,11 +231,6 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent orchestrator_llm: gpt_oss_llm @@ -246,7 +241,6 @@ functions: exclude_tools: - web_search_tool verbose: true - skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_web_default_llamaindex.yml b/configs/config_web_default_llamaindex.yml index 661574cfd..69b4901b2 100644 --- a/configs/config_web_default_llamaindex.yml +++ b/configs/config_web_default_llamaindex.yml @@ -199,11 +199,6 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -216,7 +211,6 @@ functions: exclude_tools: # Remove basic variant; deep uses advanced_web_search_tool - web_search_tool verbose: true - skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_web_frag.yml b/configs/config_web_frag.yml index 288fec1a4..ee3c21933 100644 --- a/configs/config_web_frag.yml +++ b/configs/config_web_frag.yml @@ -167,11 +167,6 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -183,7 +178,6 @@ functions: # tools: omitted -> inherits all from data_source_registry exclude_tools: - web_search_tool - skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/configs/config_web_frag_mcp_auth.yml b/configs/config_web_frag_mcp_auth.yml index 2fe283a10..33562dc90 100644 --- a/configs/config_web_frag_mcp_auth.yml +++ b/configs/config_web_frag_mcp_auth.yml @@ -216,11 +216,6 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -232,7 +227,6 @@ functions: # tools: omitted -> inherits all from data_source_registry exclude_tools: - web_search_tool - skills: deep_research_skills # ========================================================================= # Protected per-user MCP source wiring (example: Google Drive) — OPTIONAL diff --git a/configs/config_web_opensearch.yml b/configs/config_web_opensearch.yml index 103d3e109..063443ca1 100644 --- a/configs/config_web_opensearch.yml +++ b/configs/config_web_opensearch.yml @@ -174,11 +174,6 @@ functions: max_llm_turns: 10 max_tool_iterations: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent enable_citation_verification: true @@ -190,7 +185,6 @@ functions: # tools: omitted -> inherits all from data_source_registry exclude_tools: - web_search_tool - skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/docs/source/customization/configuration-reference.md b/docs/source/customization/configuration-reference.md index 4e5c63cc9..c44e66c93 100644 --- a/docs/source/customization/configuration-reference.md +++ b/docs/source/customization/configuration-reference.md @@ -499,10 +499,11 @@ request-filtered callable set. Refer to [Tools and Sources](./tools-and-sources. ```{note} **Migration: `chart-generation` moved to the `visualization` collection.** The built-in `chart-generation` skill previously lived in the `research` collection; it now lives in its own -`visualization` collection, and charts are no longer sandbox-gated. A `writer-agent` that -previously listed only `research` to obtain charts must now also list `visualization` in its -`deep_research_skills` assignment. The shipped configs already include `visualization` for the -writer; update any external config that relied on `research` for charts. +`visualization` collection, and charts are no longer sandbox-gated. The `visualization` skill ships +enabled only in the skills and sandbox example configs (`config_domain_routing_and_skills.yml` and +`config_openshell.yml`); every other shipped config presents chart-worthy data as a Markdown table. +A writer that wants inline charts must be assigned the `visualization` collection in its +`deep_research_skills` assignment. ``` --- diff --git a/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench.yml b/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench.yml index d0b8bbdcd..0ccdeb8c2 100644 --- a/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench.yml +++ b/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench.yml @@ -61,11 +61,6 @@ functions: max_results: 2 advanced_search: true - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent orchestrator_llm: gpt_oss_llm @@ -77,7 +72,6 @@ functions: tools: - paper_search_tool - advanced_web_search_tool - skills: deep_research_skills workflow: _type: deep_research_workflow diff --git a/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench_profiling.yml b/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench_profiling.yml index 0fc756942..80aca7bd6 100644 --- a/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench_profiling.yml +++ b/frontends/benchmarks/deepresearch_bench/configs/config_deep_research_bench_profiling.yml @@ -66,11 +66,6 @@ functions: max_results: 2 advanced_search: true - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent orchestrator_llm: openai_gpt_5_2 @@ -82,7 +77,6 @@ functions: tools: - paper_search_tool - advanced_web_search_tool - skills: deep_research_skills workflow: _type: deep_research_workflow diff --git a/frontends/benchmarks/deepsearch_qa/configs/config_deepsearch_qa.yml b/frontends/benchmarks/deepsearch_qa/configs/config_deepsearch_qa.yml index 3defcb7d1..93a44293c 100644 --- a/frontends/benchmarks/deepsearch_qa/configs/config_deepsearch_qa.yml +++ b/frontends/benchmarks/deepsearch_qa/configs/config_deepsearch_qa.yml @@ -59,11 +59,6 @@ functions: _type: tavily_web_search max_results: 5 - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent orchestrator_llm: gpt_oss_llm @@ -74,7 +69,6 @@ functions: max_loops: 2 tools: - web_search_tool - skills: deep_research_skills workflow: _type: deep_research_workflow diff --git a/frontends/benchmarks/freshqa/configs/config_full_workflow.yml b/frontends/benchmarks/freshqa/configs/config_full_workflow.yml index 97faacb73..168859fca 100644 --- a/frontends/benchmarks/freshqa/configs/config_full_workflow.yml +++ b/frontends/benchmarks/freshqa/configs/config_full_workflow.yml @@ -77,11 +77,6 @@ functions: max_tool_iterations: 5 verbose: true - deep_research_skills: - _type: deep_research_skills - agents: - writer-agent: [visualization] - deep_research_agent: _type: deep_research_agent orchestrator_llm: gpt_oss_llm @@ -92,7 +87,6 @@ functions: max_loops: 2 tools: - web_search_tool - skills: deep_research_skills workflow: _type: chat_deepresearcher_agent diff --git a/mcp/tests/test_config_and_packaging.py b/mcp/tests/test_config_and_packaging.py index cb98fa8cc..d47ae83dc 100644 --- a/mcp/tests/test_config_and_packaging.py +++ b/mcp/tests/test_config_and_packaging.py @@ -67,7 +67,6 @@ def test_public_mcp_config_preserves_reference_orchestration_choices() -> None: "clarifier_agent", "data_sources", "deep_research_agent", - "deep_research_skills", "intent_classifier", "shallow_research_agent", "web_search_tool", diff --git a/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py b/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py index d413a2766..bce540fac 100644 --- a/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py +++ b/tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py @@ -45,14 +45,16 @@ SYNTHESIS_SKILL_SOURCE = f"{BUILTIN_SKILL_SOURCE}synthesis/" -def test_openshell_workflow_only_diverges_for_sandbox_wiring() -> None: +def test_openshell_workflow_only_diverges_for_skills_and_sandbox_wiring() -> None: """Keep the OpenShell workflow aligned with the standard web config. - Both configs now wire the on-demand visualization skill, so the skill - wiring is dropped from each before the structural comparison. OpenShell's - skills block additionally carries the sandbox-gated research and synthesis - collections, leaving the sandbox function and its agent ref as the only - remaining divergence. + The visualization chart skill now ships only in the skills and sandbox + example configs, so the standard web config wires no deep_research_skills + at all and renders chart-worthy data as Markdown tables. OpenShell layers + the sandbox-gated research and synthesis collections plus the on-demand + visualization skill on top of a sandbox, so the skills function, the + sandbox function, and their two agent refs are the only divergence from + the standard config. """ def load(path: str) -> dict[str, Any]: @@ -66,17 +68,21 @@ def load(path: str) -> dict[str, Any]: standard_functions = standard["functions"].copy() openshell_functions = openshell["functions"].copy() - assert standard_functions.pop("deep_research_skills") == { - "_type": "deep_research_skills", - "agents": {"writer-agent": ["visualization"]}, - } + assert "deep_research_skills" not in standard_functions + assert "deep_research_sandbox" not in standard_functions + assert "skills" not in standard_functions["deep_research_agent"] + assert "sandbox" not in standard_functions["deep_research_agent"] + openshell_skills = openshell_functions.pop("deep_research_skills") + assert openshell_skills["_type"] == "deep_research_skills" + assert openshell_skills["agents"]["researcher-agent"] == ["research"] assert "visualization" in openshell_skills["agents"]["writer-agent"] + assert "research" in openshell_skills["require_sandbox"] openshell_functions.pop("deep_research_sandbox") - for functions in (standard_functions, openshell_functions): - functions["deep_research_agent"] = functions["deep_research_agent"].copy() - functions["deep_research_agent"].pop("skills") - openshell_functions["deep_research_agent"].pop("sandbox") + + openshell_agent = openshell_functions["deep_research_agent"] = openshell_functions["deep_research_agent"].copy() + assert openshell_agent.pop("skills") == "deep_research_skills" + assert openshell_agent.pop("sandbox") == "deep_research_sandbox" assert openshell["general"] == standard["general"] assert openshell["llms"] == standard["llms"] From aa6efecfbf82df617721dfabe386d161fef24909 Mon Sep 17 00:00:00 2001 From: Manush Maheshwari Date: Fri, 31 Jul 2026 09:45:55 -0700 Subject: [PATCH 6/6] docs(chart-skill): clarify manifest is preferred with terminal-scan recovery as an accepted path (#389 review) Kyle observed a live sandbox chart captured only by the terminal directory scan with default metadata. The backend treats the manifest as optional (scan is the documented fallback), so the skill's 'harvested reliably' wording overstated the requirement. Reframe the manifest as preferred (metadata plus the mid-run execute checkpoint) and note that terminal-scan recovery is an accepted successful path with default metadata. Signed-off-by: Manush Maheshwari --- .../visualization/chart-generation/SKILL.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md b/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md index bdfd2bb1d..397c1a511 100644 --- a/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md +++ b/src/aiq_agent/agents/deep_researcher/skills/visualization/chart-generation/SKILL.md @@ -72,7 +72,9 @@ report by reference (never by pasting image data). `sandbox_artifact_dir` given in your instructions (a per-job path such as `/sandbox//aiq-artifacts`). Use that value verbatim - do NOT write to a bare `/sandbox/aiq-artifacts`; the runtime only harvests files under `sandbox_artifact_dir`. -5. **Write a manifest** so the chart is harvested reliably (see below). +5. **Write a manifest** to carry the chart's title, caption, and inline flag and to checkpoint + it mid-run (see below). It is preferred, not strictly required: a chart left in + `sandbox_artifact_dir` is still captured by the terminal directory scan without one. 6. **Reference, do not embed bytes:** in the report, link the chart with `![caption](artifact://.png)`. The runtime resolves this to the durable artifact; never paste base64 image data into the report. @@ -121,10 +123,16 @@ Each figure must appear where it is discussed, not buried in a file list: ## Manifest Write a `manifest.json` in your `sandbox_artifact_dir` so the runtime captures the chart -with metadata. Manifest `path` values must be absolute and inside your `sandbox_artifact_dir` -(the per-job path from your instructions). Construct every manifest path from the runtime -argument as shown below; do not hand-copy an angle-bracket placeholder into JSON. Set -`inline: true` only for a raster image intended to appear in the report. +with its metadata. The manifest is the preferred path, not a hard requirement: a successful +`execute` checkpoints the manifest-declared artifacts immediately, and the manifest carries the +`title`, `caption`, and `inline` flag that let the chart render inline with a caption. If no +valid manifest is written, the terminal directory scan still captures any file left in +`sandbox_artifact_dir` as a successful fallback, but with default metadata (no title or caption, +and not auto-inlined), so the manifest is how you get an inline, captioned chart. Manifest +`path` values must be absolute and inside your `sandbox_artifact_dir` (the per-job path from +your instructions). Construct every manifest path from the runtime argument as shown below; do +not hand-copy an angle-bracket placeholder into JSON. Set `inline: true` only for a raster image +intended to appear in the report. ## Example Script