dataface 0.2.1.dev323__py3-none-any.whl → 0.2.1.dev377__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataface/agent_api/_link_scan.py +125 -0
- dataface/agent_api/cache.py +6 -2
- dataface/agent_api/dashboards.py +10 -0
- dataface/agent_api/docs/yaml-reference.md +35 -18
- dataface/agent_api/files.py +128 -0
- dataface/agent_api/project_session.py +2 -0
- dataface/agent_api/surface_aliases.yaml +4 -0
- dataface/ai/agent.py +31 -2
- dataface/ai/skills/analyst-runbook/SKILL.md +3 -0
- dataface/ai/skills/dashboard-build/SKILL.md +4 -3
- dataface/ai/skills/dashboard-design/SKILL.md +17 -8
- dataface/ai/skills/dashboard-replicate/SKILL.md +175 -0
- dataface/ai/skills/dashboard-structural-review/SKILL.md +2 -1
- dataface/ai/tool_schemas.py +39 -1
- dataface/ai/tools/__init__.py +43 -1
- dataface/core/compile/enrich.py +38 -0
- dataface/core/compile/inherit_graph.py +0 -31
- dataface/core/compile/inherit_registry.yaml +0 -12
- dataface/core/compile/models/chart/authored/_base.py +45 -14
- dataface/core/compile/models/chart/authored/point_map.py +17 -0
- dataface/core/compile/models/face/authored.py +4 -1
- dataface/core/compile/models/source.py +16 -28
- dataface/core/compile/models/style/resolved/__init__.py +2 -0
- dataface/core/compile/models/style/resolved/_base.py +15 -21
- dataface/core/compile/models/style/resolved/_cascade.py +31 -62
- dataface/core/compile/models/style/theme/axis.py +23 -0
- dataface/core/compile/models/style/theme/board.py +17 -0
- dataface/core/compile/models/style/theme/charts.py +20 -10
- dataface/core/compile/models/style/theme/marks.py +7 -8
- dataface/core/compile/normalize_charts.py +11 -16
- dataface/core/compile/normalize_queries.py +42 -33
- dataface/core/compile/normalizer.py +71 -9
- dataface/core/compile/resolve/_resolve.py +136 -170
- dataface/core/compile/schema_renderers/completion_catalog.py +42 -0
- dataface/core/compile/style_cascade.py +143 -31
- dataface/core/dashboard.py +22 -5
- dataface/core/defaults/palettes/scaffold/dft-grays.yml +6 -1
- dataface/core/defaults/themes/_base.yaml +11 -7
- dataface/core/defaults/themes/cream.yaml +1 -1
- dataface/core/defaults/themes/editorial.yaml +15 -1
- dataface/core/defaults/themes/neon.yaml +12 -0
- dataface/core/defaults/themes/plain.yaml +25 -2
- dataface/core/defaults/themes/stark.yaml +22 -10
- dataface/core/defaults/themes/vivid.yaml +15 -3
- dataface/core/project.py +27 -0
- dataface/core/render/chart/data_table_attachment.py +240 -88
- dataface/core/render/chart/emitters/_cartesian.py +61 -23
- dataface/core/render/chart/emitters/_layers.py +30 -12
- dataface/core/render/chart/emitters/_overlay.py +22 -9
- dataface/core/render/chart/emitters/_tooltip.py +210 -0
- dataface/core/render/chart/emitters/area.py +33 -12
- dataface/core/render/chart/emitters/bar.py +16 -7
- dataface/core/render/chart/emitters/line.py +26 -10
- dataface/core/render/chart/features/__init__.py +5 -0
- dataface/core/render/chart/features/structured_tooltip.py +419 -0
- dataface/core/render/chart/spec.py +13 -0
- dataface/core/render/chart/table.py +16 -1
- dataface/core/render/chart/table_overflow.py +78 -0
- dataface/core/render/chart/time_unit_detect.py +51 -1
- dataface/core/render/chart/translate.py +81 -6
- dataface/core/render/chart/type_inference.py +27 -3
- dataface/core/render/chart_interactivity.py +2 -0
- dataface/core/render/face_to_dict.py +48 -5
- dataface/core/render/faces.py +14 -6
- dataface/core/render/json_format.py +5 -3
- dataface/core/render/renderer.py +91 -33
- dataface/core/render/sizing.py +5 -7
- dataface/core/render/templates/scripts/chart_interactivity.js +450 -25
- dataface/core/render/text_format.py +16 -5
- dataface/core/render/warnings/base.py +9 -0
- dataface/core/render/warnings/registry.py +2 -0
- dataface/core/render/warnings/table_columns_overflow.py +50 -0
- dataface/core/render/yaml_format.py +27 -7
- {dataface-0.2.1.dev323.dist-info → dataface-0.2.1.dev377.dist-info}/METADATA +1 -1
- {dataface-0.2.1.dev323.dist-info → dataface-0.2.1.dev377.dist-info}/RECORD +78 -73
- dataface/agent_api/render_face.py +0 -135
- dataface/core/compile/schema_renderers/settings_form.py +0 -171
- {dataface-0.2.1.dev323.dist-info → dataface-0.2.1.dev377.dist-info}/WHEEL +0 -0
- {dataface-0.2.1.dev323.dist-info → dataface-0.2.1.dev377.dist-info}/entry_points.txt +0 -0
- {dataface-0.2.1.dev323.dist-info → dataface-0.2.1.dev377.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Host-agnostic inbound-link scanning for move_file.
|
|
2
|
+
|
|
3
|
+
Scans the faces tree for references to a dashboard slug -- ``link:`` chart
|
|
4
|
+
fields and markdown link targets -- so a move can rewrite what it can prove
|
|
5
|
+
is a reference and report the rest. A first-class internal-link graph is out
|
|
6
|
+
of scope here (a likely-separate dft-core initiative); this is a narrow,
|
|
7
|
+
best-effort heuristic: exact syntactic matches are rewritten, everything else
|
|
8
|
+
(the slug's basename appearing in prose, without recognized link syntax) is
|
|
9
|
+
reported as a fuzzy hit and never touched.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
17
|
+
|
|
18
|
+
from dataface.core.project import FACES_SUBDIR, Project
|
|
19
|
+
|
|
20
|
+
_FACES_GLOB = f"{FACES_SUBDIR}/**/*"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def face_slug(relpath: str) -> str | None:
|
|
24
|
+
"""Return the dashboard slug for a ``faces/`` path (no prefix, no suffix).
|
|
25
|
+
|
|
26
|
+
``faces/finance/rev.yml`` -> ``finance/rev``. Returns None for a path
|
|
27
|
+
outside ``faces/`` -- link scanning only has meaning for dashboard slugs.
|
|
28
|
+
"""
|
|
29
|
+
prefix = f"{FACES_SUBDIR}/"
|
|
30
|
+
if not relpath.startswith(prefix):
|
|
31
|
+
return None
|
|
32
|
+
tail = relpath[len(prefix) :]
|
|
33
|
+
slug, dot, _suffix = tail.rpartition(".")
|
|
34
|
+
return slug if dot else tail
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _link_field_pattern(slug: str) -> re.Pattern[str]:
|
|
38
|
+
escaped = re.escape(slug)
|
|
39
|
+
return re.compile(rf'(link:\s*["\']?/?){escaped}(?=[\s"\'/?]|$)')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _markdown_link_pattern(slug: str) -> re.Pattern[str]:
|
|
43
|
+
escaped = re.escape(slug)
|
|
44
|
+
return re.compile(rf"(\]\(/?){escaped}(?=[?)])")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LinkHit(BaseModel):
|
|
48
|
+
model_config = ConfigDict(frozen=True)
|
|
49
|
+
|
|
50
|
+
path: str
|
|
51
|
+
line_number: int
|
|
52
|
+
line: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LinkScanResult(BaseModel):
|
|
56
|
+
model_config = ConfigDict(frozen=True)
|
|
57
|
+
|
|
58
|
+
exact: list[LinkHit] = Field(default_factory=list)
|
|
59
|
+
fuzzy: list[LinkHit] = Field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def scan_links(project: Project, slug: str) -> LinkScanResult:
|
|
63
|
+
"""Scan the faces tree for references to *slug* (e.g. ``finance/rev``).
|
|
64
|
+
|
|
65
|
+
Greps for the slug's basename (the last path segment), so a same-folder
|
|
66
|
+
relative reference (``link: rev`` inside another face already under
|
|
67
|
+
``finance/``) is picked up by the grep too -- but the exact-match regexes
|
|
68
|
+
below only recognize the *full* slug, so a bare relative reference is
|
|
69
|
+
always classified FUZZY (reported, never rewritten), never EXACT. Full
|
|
70
|
+
relative-link resolution is out of scope here (see the module docstring);
|
|
71
|
+
only a full-slug ``link:`` chart field or markdown link target classifies
|
|
72
|
+
as EXACT.
|
|
73
|
+
"""
|
|
74
|
+
basename = slug.rsplit("/", 1)[-1]
|
|
75
|
+
link_re = _link_field_pattern(slug)
|
|
76
|
+
md_re = _markdown_link_pattern(slug)
|
|
77
|
+
exact: list[LinkHit] = []
|
|
78
|
+
fuzzy: list[LinkHit] = []
|
|
79
|
+
for hit in project.files.grep(basename, glob=_FACES_GLOB):
|
|
80
|
+
bucket = (
|
|
81
|
+
exact if (link_re.search(hit.line) or md_re.search(hit.line)) else fuzzy
|
|
82
|
+
)
|
|
83
|
+
bucket.append(
|
|
84
|
+
LinkHit(path=hit.relpath, line_number=hit.line_number, line=hit.line)
|
|
85
|
+
)
|
|
86
|
+
return LinkScanResult(exact=exact, fuzzy=fuzzy)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def plan_link_rewrites(
|
|
90
|
+
project: Project, old_slug: str, new_slug: str
|
|
91
|
+
) -> tuple[LinkScanResult, dict[str, str]]:
|
|
92
|
+
"""Compute exact-match link rewrites *old_slug* -> *new_slug* without writing.
|
|
93
|
+
|
|
94
|
+
Returns the scan (so the caller can report ``exact`` vs ``fuzzy``)
|
|
95
|
+
alongside a ``{path: rewritten_content}`` mapping for every file with an
|
|
96
|
+
exact hit. The caller commits it however it needs to: one file at a time
|
|
97
|
+
(``rewrite_links``, below) or batched into a single commit (Cloud's
|
|
98
|
+
governed dashboard move).
|
|
99
|
+
"""
|
|
100
|
+
result = scan_links(project, old_slug)
|
|
101
|
+
if not result.exact:
|
|
102
|
+
return result, {}
|
|
103
|
+
link_re = _link_field_pattern(old_slug)
|
|
104
|
+
md_re = _markdown_link_pattern(old_slug)
|
|
105
|
+
replacement = new_slug.replace("\\", "\\\\")
|
|
106
|
+
rewrites: dict[str, str] = {}
|
|
107
|
+
for path in dict.fromkeys(hit.path for hit in result.exact):
|
|
108
|
+
content = project.read_text(path)
|
|
109
|
+
content = link_re.sub(rf"\g<1>{replacement}", content)
|
|
110
|
+
content = md_re.sub(rf"\g<1>{replacement}", content)
|
|
111
|
+
rewrites[path] = content
|
|
112
|
+
return result, rewrites
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def rewrite_links(project: Project, old_slug: str, new_slug: str) -> LinkScanResult:
|
|
116
|
+
"""Rewrite exact ``link:``/markdown references to *old_slug* -> *new_slug*
|
|
117
|
+
in place; fuzzy prose hits are left untouched.
|
|
118
|
+
|
|
119
|
+
Returns the scan computed before rewriting, so the caller can report
|
|
120
|
+
exactly what changed (``exact``) vs what needs a human look (``fuzzy``).
|
|
121
|
+
"""
|
|
122
|
+
result, rewrites = plan_link_rewrites(project, old_slug, new_slug)
|
|
123
|
+
for path, content in rewrites.items():
|
|
124
|
+
project.write_text(path, content)
|
|
125
|
+
return result
|
dataface/agent_api/cache.py
CHANGED
|
@@ -4,9 +4,13 @@ from collections.abc import Generator
|
|
|
4
4
|
from contextlib import contextmanager
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Re-exported: TrivialDuckDBCache is the declared return type of open_cache,
|
|
8
|
+
# so embedders annotating a held cache reach it here without a core import.
|
|
9
|
+
from dataface.core.execute.trivial_local_cache import (
|
|
10
|
+
TrivialDuckDBCache as TrivialDuckDBCache,
|
|
11
|
+
)
|
|
8
12
|
|
|
9
|
-
__all__ = ["open_cache", "cache_ctx"]
|
|
13
|
+
__all__ = ["TrivialDuckDBCache", "open_cache", "cache_ctx"]
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
def open_cache(path: Path | None) -> TrivialDuckDBCache:
|
dataface/agent_api/dashboards.py
CHANGED
|
@@ -93,6 +93,16 @@ class RenderDashboardArgs(BaseModel):
|
|
|
93
93
|
yaml_content: str | None = Field(
|
|
94
94
|
None, description="YAML content to render directly (use this OR path)"
|
|
95
95
|
)
|
|
96
|
+
chart: str | None = Field(
|
|
97
|
+
None,
|
|
98
|
+
description=(
|
|
99
|
+
"Render only this chart (by chart id) from the dashboard, with its "
|
|
100
|
+
"dependent variables. Use to show one existing chart that answers "
|
|
101
|
+
"a question instead of re-deriving its query — the rendered chart "
|
|
102
|
+
"keeps the dashboard's canonical definition. Unknown ids fail with "
|
|
103
|
+
"the list of available chart ids."
|
|
104
|
+
),
|
|
105
|
+
)
|
|
96
106
|
variables: dict[str, Any] | None = Field(
|
|
97
107
|
None, description="Variable values to apply to the dashboard"
|
|
98
108
|
)
|
|
@@ -26,7 +26,7 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
26
26
|
| `details` | [FaceDetails](#facedetails) | ✓ | Collapsible section metadata. String shorthand: details: 'text' → FaceDetails(summary='text'). Block form: details: {summary: ..., expanded_title: ..., expanded: false}. |
|
|
27
27
|
| `id` | str | ✓ | Explicit ID for this face. Auto-generated from filename if omitted. |
|
|
28
28
|
| `style` | [Style](#style) | ✓ | Style patch (background, padding, border, etc.). Background and semantic color tokens (accent, muted) cascade to nested child faces. |
|
|
29
|
-
| `width` | str \| int | ✓ | Width when nested (e.g., '50%', '400px', or an integer in pixels). |
|
|
29
|
+
| `width` | str \| int | ✓ | Width when nested (e.g., '50%', '400px', or an integer in pixels). On the root face there is no parent to place it into, so it instead sets the board's own width (equivalent to 'style.board.width') — percentages are rejected there since there's nothing to size relative to. |
|
|
30
30
|
| `height` | str \| int | ✓ | Height when nested (e.g., '300px' or an integer in pixels). |
|
|
31
31
|
| `visible` | bool \| str \| [SingleRowBoolProbe](#singlerowboolprobe) | ✓ | Controls whether this layout item is rendered. Accepts a bool, variable name, Jinja expression, or {query, column} probe. |
|
|
32
32
|
| `extends` | str \| list[str] | ✓ | Face name(s) or relative path(s) this face inherits from, low to high priority. |
|
|
@@ -188,8 +188,8 @@ Authored patch for bar and histogram charts.
|
|
|
188
188
|
| `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: column to sort by and direction (asc/desc). |
|
|
189
189
|
| `multiples` | [MultiplesConfig](#multiplesconfig) | ✓ | Partition this chart into small multiples by a `rows` column (vertical stack), a `columns` column (side by side), or both (grid). Panels share one measure scale by default. |
|
|
190
190
|
| `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
|
|
191
|
-
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade.
|
|
192
|
-
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container.
|
|
191
|
+
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
192
|
+
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
193
193
|
| `style` | [BarChartStyle](#barchartstyle) | ✓ | Chart-local style overrides. |
|
|
194
194
|
| `layers` | list[[BarLayer](#barlayer) \| [LineLayer](#linelayer) \| [AreaLayer](#arealayer) \| [ScatterLayer](#scatterlayer)] | ✓ | Typed overlay layers on this chart. |
|
|
195
195
|
|
|
@@ -217,8 +217,8 @@ Authored patch for line charts.
|
|
|
217
217
|
| `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: column to sort by and direction (asc/desc). |
|
|
218
218
|
| `multiples` | [MultiplesConfig](#multiplesconfig) | ✓ | Partition this chart into small multiples by a `rows` column (vertical stack), a `columns` column (side by side), or both (grid). Panels share one measure scale by default. |
|
|
219
219
|
| `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
|
|
220
|
-
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade.
|
|
221
|
-
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container.
|
|
220
|
+
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
221
|
+
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
222
222
|
| `style` | [LineChartStyle](#linechartstyle) | ✓ | Chart-local style overrides. |
|
|
223
223
|
| `layers` | list[[BarLayer](#barlayer) \| [LineLayer](#linelayer) \| [AreaLayer](#arealayer) \| [ScatterLayer](#scatterlayer)] | ✓ | Typed overlay layers on this chart. |
|
|
224
224
|
|
|
@@ -246,8 +246,8 @@ Authored patch for area charts.
|
|
|
246
246
|
| `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: column to sort by and direction (asc/desc). |
|
|
247
247
|
| `multiples` | [MultiplesConfig](#multiplesconfig) | ✓ | Partition this chart into small multiples by a `rows` column (vertical stack), a `columns` column (side by side), or both (grid). Panels share one measure scale by default. |
|
|
248
248
|
| `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
|
|
249
|
-
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade.
|
|
250
|
-
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container.
|
|
249
|
+
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
250
|
+
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
251
251
|
| `style` | [AreaChartStyle](#areachartstyle) | ✓ | Chart-local style overrides. |
|
|
252
252
|
| `layers` | list[[BarLayer](#barlayer) \| [LineLayer](#linelayer) \| [AreaLayer](#arealayer) \| [ScatterLayer](#scatterlayer)] | ✓ | Typed overlay layers on this chart. |
|
|
253
253
|
|
|
@@ -275,8 +275,8 @@ Authored patch for scatter charts.
|
|
|
275
275
|
| `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: column to sort by and direction (asc/desc). |
|
|
276
276
|
| `multiples` | [MultiplesConfig](#multiplesconfig) | ✓ | Partition this chart into small multiples by a `rows` column (vertical stack), a `columns` column (side by side), or both (grid). Panels share one measure scale by default. |
|
|
277
277
|
| `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
|
|
278
|
-
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade.
|
|
279
|
-
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container.
|
|
278
|
+
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
279
|
+
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
280
280
|
| `size` | str | ✓ | Column used to size-encode data points (quantitative). |
|
|
281
281
|
| `shape` | str | ✓ | Column used to shape-encode data points (categorical). |
|
|
282
282
|
| `style` | [ScatterChartStyle](#scatterchartstyle) | ✓ | Chart-local style overrides. |
|
|
@@ -305,8 +305,8 @@ Authored patch for heatmap charts.
|
|
|
305
305
|
| `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: column to sort by and direction (asc/desc). |
|
|
306
306
|
| `multiples` | [MultiplesConfig](#multiplesconfig) | ✓ | Partition this chart into small multiples by a `rows` column (vertical stack), a `columns` column (side by side), or both (grid). Panels share one measure scale by default. |
|
|
307
307
|
| `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
|
|
308
|
-
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade.
|
|
309
|
-
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container.
|
|
308
|
+
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
309
|
+
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container. Valid on cartesian chart families (area, bar, heatmap, histogram, line, scatter), pie/donut, and geo families (geoshape, map, point_map, bubble_map). Other chart families use renderer-owned or layout-owned sizing contracts. |
|
|
310
310
|
| `style` | [HeatmapChartStyle](#heatmapchartstyle) | ✓ | Chart-local style overrides. |
|
|
311
311
|
|
|
312
312
|
<a id="piechart"></a>
|
|
@@ -328,6 +328,8 @@ Authored patch for pie and donut charts.
|
|
|
328
328
|
| `subtitle` | str | ✓ | Chart subtitle displayed below the title. |
|
|
329
329
|
| `color` | str | ✓ | Color data channel: bare column name only. |
|
|
330
330
|
| `total` | [ChartTotal](#charttotal) | ✓ | Donut center total. |
|
|
331
|
+
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. |
|
|
332
|
+
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container. |
|
|
331
333
|
| `style` | [PieChartStyle](#piechartstyle) | ✓ | Chart-local style overrides. |
|
|
332
334
|
|
|
333
335
|
<a id="kpichart"></a>
|
|
@@ -394,6 +396,8 @@ Authored patch for point_map and bubble_map charts.
|
|
|
394
396
|
| `geo_source` | str | ✓ | Named geographic data source for loading GeoJSON boundaries. |
|
|
395
397
|
| `lookup` | str | ✓ | Data column to join against geographic data (map join key). |
|
|
396
398
|
| `value` | str | ✓ | Data column mapped to the fill color. |
|
|
399
|
+
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. |
|
|
400
|
+
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container. |
|
|
397
401
|
| `latitude` | str | ✓ | Column containing latitude values for point/bubble maps. |
|
|
398
402
|
| `longitude` | str | ✓ | Column containing longitude values for point/bubble maps. |
|
|
399
403
|
| `size` | str | ✓ | Data column used to scale bubble radius (quantitative). Only meaningful on bubble_map. |
|
|
@@ -422,8 +426,8 @@ Authored patch for map and geoshape charts.
|
|
|
422
426
|
| `geo_source` | str | ✓ | Named geographic data source for loading GeoJSON boundaries. |
|
|
423
427
|
| `lookup` | str | ✓ | Data column to join against geographic data (map join key). |
|
|
424
428
|
| `value` | str | ✓ | Data column mapped to the fill color. |
|
|
425
|
-
| `
|
|
426
|
-
| `
|
|
429
|
+
| `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. |
|
|
430
|
+
| `width` | int \| float | ✓ | Preferred chart width in pixels. Positive number only. Contributes to the dashboard's intrinsic width without overriding the final layout slot — the chart still fills its allocated container. |
|
|
427
431
|
| `style` | [GeoshapeChartStyle](#geoshapechartstyle) | ✓ | Chart-local style overrides. |
|
|
428
432
|
|
|
429
433
|
<a id="calloutchart"></a>
|
|
@@ -1172,9 +1176,9 @@ Authored overlay for ChartsStyle. Registry of all chart-type styles plus shared
|
|
|
1172
1176
|
| `default_table_height` | float | ✓ | Placeholder table height in pixels; replaced by data-aware row-count sizing at render time. |
|
|
1173
1177
|
| `label_usable_ratio` | float | ✓ | Fraction of chart width usable for axis labels (0–1); labels are tilted when full labels exceed this width. |
|
|
1174
1178
|
| `axis` | [AxisStyle](#axisstyle) | ✓ | Shared axis style applied to all axes before per-axis overrides. |
|
|
1175
|
-
| `axis_x` | [AxisStyle](#axisstyle) | ✓ | X-axis style overrides applied after the shared axis. Unset fields fall back to
|
|
1176
|
-
| `axis_y` | [AxisStyle](#axisstyle) | ✓ | Y-axis style overrides applied after the shared axis. Unset fields fall back to
|
|
1177
|
-
| `axis_quantitative` | [AxisStyle](#axisstyle) | ✓ | Quantitative axis style overrides applied after axis_x/axis_y. Unset fields fall back to
|
|
1179
|
+
| `axis_x` | [AxisStyle](#axisstyle) | ✓ | X-axis style overrides applied after the shared axis. Unset fields fall back to style.charts.axis. |
|
|
1180
|
+
| `axis_y` | [AxisStyle](#axisstyle) | ✓ | Y-axis style overrides applied after the shared axis. Unset fields fall back to style.charts.axis. |
|
|
1181
|
+
| `axis_quantitative` | [AxisStyle](#axisstyle) | ✓ | Quantitative axis style overrides applied after axis_x/axis_y. Unset fields fall back to style.charts.axis. |
|
|
1178
1182
|
| `view` | [ViewStyle](#viewstyle) | ✓ | Vega-Lite view dimensions and border. |
|
|
1179
1183
|
| `autosize` | [AutosizeStyle](#autosizestyle) | ✓ | Vega-Lite autosize configuration. |
|
|
1180
1184
|
| `marks` | [GlobalMarksStyle](#globalmarksstyle) | ✓ | Global mark defaults (tier-1 of the marks cascade). |
|
|
@@ -1391,6 +1395,8 @@ Authored overlay for TooltipStyle. Tooltip box style — all cascade keys for th
|
|
|
1391
1395
|
| `value` | [TooltipSlotStyle](#tooltipslotstyle) | ✓ | Value-column font overrides (color, weight). |
|
|
1392
1396
|
| `border` | [TooltipBorderStyle](#tooltipborderstyle) | ✓ | Tooltip border style; theme always provides this. |
|
|
1393
1397
|
| `shadow` | [TooltipShadowStyle](#tooltipshadowstyle) | ✓ | Tooltip drop-shadow config; theme always provides this. |
|
|
1398
|
+
| `swatch` | [TooltipSwatchStyle](#tooltipswatchstyle) | ✓ | Series colour swatch size/shape; theme always provides this. |
|
|
1399
|
+
| `active_marker` | enum: "fill", "triangle" | ✓ | How the hovered row is marked in a multi-row (x-unified) tooltip: 'fill' tints the row background (default); 'triangle' draws an edge-flush wedge in the box's left padding instead. Theme always provides this. |
|
|
1394
1400
|
|
|
1395
1401
|
<a id="stylecolorconfig"></a>
|
|
1396
1402
|
## StyleColorConfig
|
|
@@ -1917,6 +1923,8 @@ Authored overlay for TextCodeStyle. Inline and fenced code spans in markdown pro
|
|
|
1917
1923
|
| `font` | [FontStyle](#fontstyle) | ✓ | Box text font overrides — full FontStyle (family, color, size, weight, style, decoration, case). |
|
|
1918
1924
|
| `background` | str | ✓ | Box background fill. |
|
|
1919
1925
|
| `border` | [BorderStyle](#borderstyle) | ✓ | Box border (width, color, radius). |
|
|
1926
|
+
| `highlight` | bool | ✓ | Syntax-highlight fenced markdown code blocks. |
|
|
1927
|
+
| `theme` | str | ✓ | Pygments style name for fenced markdown code tokens. |
|
|
1920
1928
|
|
|
1921
1929
|
<a id="textblockquotestyle"></a>
|
|
1922
1930
|
## TextBlockquoteStyle
|
|
@@ -2159,6 +2167,15 @@ Authored overlay for TooltipShadowStyle. Tooltip drop-shadow toggle. JS applies
|
|
|
2159
2167
|
|-------|------|:--------:|-------------|
|
|
2160
2168
|
| `visible` | bool | ✓ | Show a drop-shadow on the tooltip box; theme always provides this. |
|
|
2161
2169
|
|
|
2170
|
+
<a id="tooltipswatchstyle"></a>
|
|
2171
|
+
## TooltipSwatchStyle
|
|
2172
|
+
Authored overlay for TooltipSwatchStyle. Series colour swatch in the tooltip — the mark-coloured chip next to each
|
|
2173
|
+
|
|
2174
|
+
| Field | Type | Optional | Description |
|
|
2175
|
+
|-------|------|:--------:|-------------|
|
|
2176
|
+
| `size` | float | ✓ | Swatch edge length in pixels (square). |
|
|
2177
|
+
| `radius` | float | ✓ | Swatch corner radius in pixels (0 = square, ~half of size = circle). |
|
|
2178
|
+
|
|
2162
2179
|
<a id="scaletargetconfig"></a>
|
|
2163
2180
|
## ScaleTargetConfig
|
|
2164
2181
|
Scale configuration for a single style target (background or color).
|
|
@@ -2644,7 +2661,7 @@ Authored overlay for BarLabelsStyle. Bar mark value-label config. Extends MarkLa
|
|
|
2644
2661
|
|-------|------|:--------:|-------------|
|
|
2645
2662
|
| `visible` | bool | ✓ | Show numeric value labels on each mark; False by default. |
|
|
2646
2663
|
| `field` | str | ✓ | Column to source label text from; None uses the chart's y-field. |
|
|
2647
|
-
| `format` | str | ✓ | Number format string for value labels.
|
|
2664
|
+
| `format` | str | ✓ | Number format string for value labels. |
|
|
2648
2665
|
| `dx` | int | ✓ | Horizontal pixel offset for value labels; overrides the position default. |
|
|
2649
2666
|
| `dy` | int | ✓ | Vertical pixel offset for value labels; overrides the position default. |
|
|
2650
2667
|
| `font` | [FontStyle](#fontstyle) | ✓ | Value label font style (color, size, family, etc.). |
|
|
@@ -2670,7 +2687,7 @@ Authored overlay for PointLabelsStyle. Point/line mark value-label config. Used
|
|
|
2670
2687
|
|-------|------|:--------:|-------------|
|
|
2671
2688
|
| `visible` | bool | ✓ | Show numeric value labels on each mark; False by default. |
|
|
2672
2689
|
| `field` | str | ✓ | Column to source label text from; None uses the chart's y-field. |
|
|
2673
|
-
| `format` | str | ✓ | Number format string for value labels.
|
|
2690
|
+
| `format` | str | ✓ | Number format string for value labels. |
|
|
2674
2691
|
| `dx` | int | ✓ | Horizontal pixel offset for value labels; overrides the position default. |
|
|
2675
2692
|
| `dy` | int | ✓ | Vertical pixel offset for value labels; overrides the position default. |
|
|
2676
2693
|
| `font` | [FontStyle](#fontstyle) | ✓ | Value label font style (color, size, family, etc.). |
|
dataface/agent_api/files.py
CHANGED
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
from pydantic import BaseModel, ConfigDict, Field
|
|
16
16
|
|
|
17
|
+
from dataface.agent_api._link_scan import LinkHit, face_slug, rewrite_links
|
|
17
18
|
from dataface.core.errors.execution import ExecutionError
|
|
18
19
|
from dataface.core.project import FACES_SUBDIR, Project
|
|
19
20
|
|
|
@@ -63,6 +64,32 @@ class EditFileArgs(BaseModel):
|
|
|
63
64
|
new_string: str = Field(description="Replacement text.")
|
|
64
65
|
|
|
65
66
|
|
|
67
|
+
class MoveFileArgs(BaseModel):
|
|
68
|
+
"""Move (rename) a project file. Fails if the destination already exists
|
|
69
|
+
or the source does not. Both paths are relative to the project root and
|
|
70
|
+
may not escape it.
|
|
71
|
+
|
|
72
|
+
When both paths are dashboard faces (under faces/), exact-match ``link:``
|
|
73
|
+
chart fields and markdown links pointing at the old slug are rewritten to
|
|
74
|
+
the new one in the same operation; any other reference to the old slug's
|
|
75
|
+
basename is reported as a fuzzy hit, never rewritten.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
source_path: str = Field(
|
|
79
|
+
description="Current file path relative to the project root."
|
|
80
|
+
)
|
|
81
|
+
destination_path: str = Field(
|
|
82
|
+
description="New file path relative to the project root."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DeleteFileArgs(BaseModel):
|
|
87
|
+
"""Delete a project file. Fails if it does not exist. Path is relative to
|
|
88
|
+
the project root and may not escape it."""
|
|
89
|
+
|
|
90
|
+
path: str = Field(description="File path relative to the project root.")
|
|
91
|
+
|
|
92
|
+
|
|
66
93
|
class GlobFilesArgs(BaseModel):
|
|
67
94
|
"""List files matching a glob pattern (e.g. 'faces/*.yml').
|
|
68
95
|
|
|
@@ -126,6 +153,25 @@ class EditFileResult(BaseModel):
|
|
|
126
153
|
error: str | None = None
|
|
127
154
|
|
|
128
155
|
|
|
156
|
+
class MoveFileResult(BaseModel):
|
|
157
|
+
model_config = ConfigDict(frozen=True)
|
|
158
|
+
|
|
159
|
+
success: bool
|
|
160
|
+
source_path: str
|
|
161
|
+
destination_path: str
|
|
162
|
+
links_rewritten: int = 0
|
|
163
|
+
fuzzy_links: list[LinkHit] = Field(default_factory=list)
|
|
164
|
+
error: str | None = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class DeleteFileResult(BaseModel):
|
|
168
|
+
model_config = ConfigDict(frozen=True)
|
|
169
|
+
|
|
170
|
+
success: bool
|
|
171
|
+
path: str
|
|
172
|
+
error: str | None = None
|
|
173
|
+
|
|
174
|
+
|
|
129
175
|
class GlobResult(BaseModel):
|
|
130
176
|
model_config = ConfigDict(frozen=True)
|
|
131
177
|
|
|
@@ -199,6 +245,88 @@ def edit_file(
|
|
|
199
245
|
return EditFileResult(success=True, path=path, replacements=1)
|
|
200
246
|
|
|
201
247
|
|
|
248
|
+
def move_file(
|
|
249
|
+
source_path: str, destination_path: str, project: Project
|
|
250
|
+
) -> MoveFileResult:
|
|
251
|
+
"""Move *source_path* to *destination_path*, rewriting exact inbound
|
|
252
|
+
dashboard links along the way (see ``MoveFileArgs``). LOCAL semantics: no
|
|
253
|
+
exceptions escape, everything is reported in the typed result."""
|
|
254
|
+
try:
|
|
255
|
+
content = project.read_text(source_path)
|
|
256
|
+
except FileNotFoundError:
|
|
257
|
+
return MoveFileResult(
|
|
258
|
+
success=False,
|
|
259
|
+
source_path=source_path,
|
|
260
|
+
destination_path=destination_path,
|
|
261
|
+
error=f"file not found: {source_path}",
|
|
262
|
+
)
|
|
263
|
+
except (ValueError, IsADirectoryError, *_UNREADABLE_FILE) as exc:
|
|
264
|
+
return MoveFileResult(
|
|
265
|
+
success=False,
|
|
266
|
+
source_path=source_path,
|
|
267
|
+
destination_path=destination_path,
|
|
268
|
+
error=str(exc),
|
|
269
|
+
)
|
|
270
|
+
try:
|
|
271
|
+
destination_exists = project.exists(destination_path)
|
|
272
|
+
except ValueError as exc:
|
|
273
|
+
return MoveFileResult(
|
|
274
|
+
success=False,
|
|
275
|
+
source_path=source_path,
|
|
276
|
+
destination_path=destination_path,
|
|
277
|
+
error=str(exc),
|
|
278
|
+
)
|
|
279
|
+
if destination_exists:
|
|
280
|
+
return MoveFileResult(
|
|
281
|
+
success=False,
|
|
282
|
+
source_path=source_path,
|
|
283
|
+
destination_path=destination_path,
|
|
284
|
+
error=f"destination already exists: {destination_path}",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
old_slug, new_slug = face_slug(source_path), face_slug(destination_path)
|
|
288
|
+
scan = rewrite_links(project, old_slug, new_slug) if old_slug and new_slug else None
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
project.write_text(destination_path, content)
|
|
292
|
+
except (ValueError, OSError) as exc:
|
|
293
|
+
return MoveFileResult(
|
|
294
|
+
success=False,
|
|
295
|
+
source_path=source_path,
|
|
296
|
+
destination_path=destination_path,
|
|
297
|
+
error=str(exc),
|
|
298
|
+
)
|
|
299
|
+
try:
|
|
300
|
+
project.delete_text(source_path)
|
|
301
|
+
except (ValueError, OSError, FileNotFoundError) as exc:
|
|
302
|
+
return MoveFileResult(
|
|
303
|
+
success=False,
|
|
304
|
+
source_path=source_path,
|
|
305
|
+
destination_path=destination_path,
|
|
306
|
+
error=f"wrote {destination_path!r} but failed to delete {source_path!r}: {exc}",
|
|
307
|
+
)
|
|
308
|
+
return MoveFileResult(
|
|
309
|
+
success=True,
|
|
310
|
+
source_path=source_path,
|
|
311
|
+
destination_path=destination_path,
|
|
312
|
+
links_rewritten=len(scan.exact) if scan else 0,
|
|
313
|
+
fuzzy_links=scan.fuzzy if scan else [],
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def delete_file(path: str, project: Project) -> DeleteFileResult:
|
|
318
|
+
"""Delete *path*. LOCAL semantics: no exceptions escape."""
|
|
319
|
+
try:
|
|
320
|
+
project.delete_text(path)
|
|
321
|
+
except FileNotFoundError:
|
|
322
|
+
return DeleteFileResult(
|
|
323
|
+
success=False, path=path, error=f"file not found: {path}"
|
|
324
|
+
)
|
|
325
|
+
except (ValueError, OSError) as exc:
|
|
326
|
+
return DeleteFileResult(success=False, path=path, error=str(exc))
|
|
327
|
+
return DeleteFileResult(success=True, path=path)
|
|
328
|
+
|
|
329
|
+
|
|
202
330
|
def glob_files(pattern: str | None, project: Project) -> GlobResult:
|
|
203
331
|
"""List files matching *pattern*; ``None`` (the documented tool default)
|
|
204
332
|
lists the faces subtree."""
|
|
@@ -432,6 +432,7 @@ class ProjectSession:
|
|
|
432
432
|
path: Path | None = None,
|
|
433
433
|
*,
|
|
434
434
|
yaml_content: str | None = None,
|
|
435
|
+
chart: str | None = None,
|
|
435
436
|
variables: dict[str, Any] | None = None,
|
|
436
437
|
format: RenderFormat = "json",
|
|
437
438
|
use_cache: bool = True,
|
|
@@ -453,6 +454,7 @@ class ProjectSession:
|
|
|
453
454
|
return _core_dashboard.render_dashboard(
|
|
454
455
|
path=path,
|
|
455
456
|
yaml_content=yaml_content,
|
|
457
|
+
chart=chart,
|
|
456
458
|
variables=variables,
|
|
457
459
|
adapter_registry=self.adapter_registry,
|
|
458
460
|
project=self.project,
|
|
@@ -63,6 +63,10 @@ skill_design_report:
|
|
|
63
63
|
tool: "get_skill('report-design')"
|
|
64
64
|
dft: "dft skills report-design"
|
|
65
65
|
|
|
66
|
+
skill_replicate_dashboard:
|
|
67
|
+
tool: "get_skill('dashboard-replicate')"
|
|
68
|
+
dft: "dft skills dashboard-replicate"
|
|
69
|
+
|
|
66
70
|
skill_review:
|
|
67
71
|
tool: "get_skill('dashboard-review')"
|
|
68
72
|
dft: "dft skills dashboard-review"
|
dataface/ai/agent.py
CHANGED
|
@@ -25,7 +25,7 @@ from dataface.ai.llm import LLMClient, LLMClientError
|
|
|
25
25
|
from dataface.ai.memories import format_memories_prompt, load_memories
|
|
26
26
|
from dataface.ai.prompts import load_shared_prompt
|
|
27
27
|
from dataface.ai.tool_schemas import CHAT_TOOLS
|
|
28
|
-
from dataface.ai.tools import dispatch_tool_call
|
|
28
|
+
from dataface.ai.tools import ToolHandler, dispatch_tool_call
|
|
29
29
|
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
31
|
from dataface.core.execute.adapters import AdapterRegistry
|
|
@@ -167,6 +167,12 @@ class AgentProfile:
|
|
|
167
167
|
name: str
|
|
168
168
|
tools: list[dict[str, Any]]
|
|
169
169
|
build_system_prompt: Callable[[DatafaceAIContext], str]
|
|
170
|
+
# Per-profile override for tool names the shared registry already handles
|
|
171
|
+
# (e.g. Cloud routing move_file/delete_file to a governed service instead
|
|
172
|
+
# of the generic local file-tool handler) — consulted before TOOL_HANDLERS
|
|
173
|
+
# in dispatch_tool_call — one tool name and contract across hosts, two
|
|
174
|
+
# backends. None for profiles with no host-specific behavior.
|
|
175
|
+
tool_overrides: dict[str, ToolHandler] | None = None
|
|
170
176
|
|
|
171
177
|
|
|
172
178
|
# The default profile: the dashboard analyst that `dft chat` and the MCP/cloud
|
|
@@ -178,6 +184,24 @@ DASHBOARD_PROFILE = AgentProfile(
|
|
|
178
184
|
)
|
|
179
185
|
|
|
180
186
|
|
|
187
|
+
def _model_facing_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
188
|
+
"""Return the copy of a tool result the model reads.
|
|
189
|
+
|
|
190
|
+
SVG output is a display artifact for the host (Cloud embeds it; the CLI
|
|
191
|
+
never requests it) — feeding ~100KB of markup to the model wastes context
|
|
192
|
+
and invites it to echo the blob as prose. The model keeps the verdict
|
|
193
|
+
(success, warnings, errors); the host-facing ToolResultEvent keeps the
|
|
194
|
+
full payload.
|
|
195
|
+
"""
|
|
196
|
+
data = result.get("data")
|
|
197
|
+
if isinstance(data, str) and data.lstrip().startswith("<svg"):
|
|
198
|
+
return {
|
|
199
|
+
**result,
|
|
200
|
+
"data": f"[svg omitted ({len(data)} chars) — rendered for the user]",
|
|
201
|
+
}
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
181
205
|
def run_agent(
|
|
182
206
|
prompt: str | list[dict[str, Any]],
|
|
183
207
|
*,
|
|
@@ -188,6 +212,7 @@ def run_agent(
|
|
|
188
212
|
profile: AgentProfile | None = None,
|
|
189
213
|
tools: list[dict[str, Any]] | None = None,
|
|
190
214
|
external_manager: ExternalMCPManager | None = None,
|
|
215
|
+
extra_handlers: dict[str, ToolHandler] | None = None,
|
|
191
216
|
) -> Generator[AgentEvent, None, None]:
|
|
192
217
|
"""Run the agent loop and stream typed events.
|
|
193
218
|
|
|
@@ -199,6 +224,8 @@ def run_agent(
|
|
|
199
224
|
tools. Used by chat sessions; defaults to the profile's tools.
|
|
200
225
|
external_manager: ExternalMCPManager instance for routing 'server__tool' calls.
|
|
201
226
|
Must be provided when tools contains external (namespaced) entries.
|
|
227
|
+
extra_handlers: Per-call handlers for tools with no meaning to dft-core
|
|
228
|
+
(e.g. Cloud's placement tool) — threaded straight to dispatch_tool_call.
|
|
202
229
|
"""
|
|
203
230
|
active = profile if profile is not None else DASHBOARD_PROFILE
|
|
204
231
|
effective_tools = tools if tools is not None else active.tools
|
|
@@ -246,13 +273,15 @@ def run_agent(
|
|
|
246
273
|
tc.arguments,
|
|
247
274
|
context=context,
|
|
248
275
|
external_manager=external_manager,
|
|
276
|
+
extra_handlers=extra_handlers,
|
|
277
|
+
tool_overrides=active.tool_overrides,
|
|
249
278
|
)
|
|
250
279
|
conversation.append(
|
|
251
280
|
{
|
|
252
281
|
"role": "tool",
|
|
253
282
|
"tool_call_id": tc.id,
|
|
254
283
|
"name": tc.name,
|
|
255
|
-
"content": json.dumps(result, default=str),
|
|
284
|
+
"content": json.dumps(_model_facing_result(result), default=str),
|
|
256
285
|
}
|
|
257
286
|
)
|
|
258
287
|
yield ToolResultEvent(id=tc.id, name=tc.name, result=result)
|
|
@@ -31,6 +31,9 @@ whenever one fits — and say which you reused.{{/if_tool}}
|
|
|
31
31
|
- **one chart** — a single trend, ranking, or breakdown
|
|
32
32
|
- **a dashboard** — several related charts that explore a topic together
|
|
33
33
|
- **a report** — narrative plus charts that walk through an analysis
|
|
34
|
+
- **a replication** — the user supplied an existing dashboard (screenshot,
|
|
35
|
+
image, export) to reproduce → `{{ s_skill_replicate_dashboard }}`;
|
|
36
|
+
fidelity to the source, not a redesign
|
|
34
37
|
2. **Clarify only what blocks you.** If the metric, grain, time window, or filters
|
|
35
38
|
are ambiguous *and* the choice changes the answer, ask one focused question.
|
|
36
39
|
Otherwise state the assumption and proceed.
|
|
@@ -40,6 +40,7 @@ Build dashboards and reports **incrementally** — one chart at a time, validati
|
|
|
40
40
|
## Companion Skills
|
|
41
41
|
|
|
42
42
|
- **`{{ s_skill_design_dashboard }}`** — chart-type, layout, and color decisions
|
|
43
|
+
- **`{{ s_skill_replicate_dashboard }}`** — reproducing an existing dashboard from a screenshot or export
|
|
43
44
|
- **`{{ s_skill_design_report }}`** — narrative reports (text + charts)
|
|
44
45
|
- **`{{ s_skill_review }}`** — self-review checklist run at delivery
|
|
45
46
|
- **`{{ s_skill_troubleshooting }}`** — when something breaks
|
|
@@ -268,7 +269,7 @@ Named presets (prefer these over raw D3 strings):
|
|
|
268
269
|
| Writing SQL without checking column names | Check INFORMATION_SCHEMA via `{{ s_execute_query }}` first |
|
|
269
270
|
| Hardcoding values during exploration | Use `{{ variables }}` from the start so the query cache survives |
|
|
270
271
|
| Skipping validation between changes | `{{ s_validate_dashboard }}` after every YAML edit |
|
|
271
|
-
| Too many charts (>8) | Split into multiple dashboards |
|
|
272
|
+
| Too many charts (>8) the user didn't ask for | Split into multiple dashboards |
|
|
272
273
|
| Using chart-type names as keys in layout | `table:` or `bar:` as keys cause parse errors — use descriptive names like `revenue_table` |
|
|
273
274
|
| Referencing query names in layout | Layout references charts, not queries — create a chart that wraps the query |
|
|
274
275
|
| KPI query returns multiple rows | Use `SUM()`/`COUNT()`/`AVG()` to aggregate to 1 row |
|
|
@@ -289,13 +290,13 @@ Named presets (prefer these over raw D3 strings):
|
|
|
289
290
|
| "I know what the columns are called" | You don't. Check INFORMATION_SCHEMA with `{{ s_execute_query }}`. |
|
|
290
291
|
| "Validation is slow, I'll do it at the end" | Validation is instant. Skipping it costs more time debugging later. |
|
|
291
292
|
| "It's just a quick chart, no need for variables" | Variables cost nothing and enable caching. Use them. |
|
|
292
|
-
| "
|
|
293
|
+
| "15 charts is too many, I'll trim their list" | An explicit user ask wins. Suggest a split once, then build all 15. The 8-max default applies when the design is yours to choose. |
|
|
293
294
|
|
|
294
295
|
## Red Flags — STOP
|
|
295
296
|
|
|
296
297
|
- About to write SQL without having explored the actual schema first
|
|
297
298
|
- Building more than one new chart before validating the previous one
|
|
298
|
-
- Dashboard has more than 8 visualizations
|
|
299
|
+
- Dashboard has more than 8 visualizations the user didn't ask for
|
|
299
300
|
- A chart exists without a clear question it answers
|
|
300
301
|
- Using hardcoded values that should be variables
|
|
301
302
|
- Delivering without running the self-review checklist
|