dataface 0.4.1.dev53__py3-none-any.whl → 0.4.1.dev224__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/DATAFACE_SYNTAX.md +6 -2
- dataface/agent_api/_init_templates/meta.yaml +3 -3
- dataface/agent_api/board_artifact.py +15 -1
- dataface/agent_api/dashboards.py +21 -4
- dataface/agent_api/docs/yaml-reference.md +22 -12
- dataface/agent_api/pack.py +1 -1
- dataface/agent_api/project_session.py +4 -1
- dataface/agent_api/query.py +112 -12
- dataface/agent_api/serve.py +1 -3
- dataface/ai/llm.py +59 -13
- dataface/ai/skills/dashboard-build/SKILL.md +2 -0
- dataface/ai/tools/__init__.py +21 -7
- dataface/cli/commands/board_artifact.py +1 -1
- dataface/cli/commands/extension.py +1 -0
- dataface/cli/commands/query.py +1 -1
- dataface/cli/commands/serve.py +5 -14
- dataface/cli/commands/skills_init.py +1 -2
- dataface/cli/main.py +29 -13
- dataface/core/compile/compiler.py +51 -27
- dataface/core/compile/config.py +71 -5
- dataface/core/compile/data_table.py +8 -8
- dataface/core/compile/format.py +12 -2
- dataface/core/compile/inherit_graph.py +22 -1
- dataface/core/compile/inherit_resolver.py +4 -4
- dataface/core/compile/introspection.py +6 -0
- dataface/core/compile/label_data.py +3 -3
- dataface/core/compile/labels_env.py +14 -2
- dataface/core/compile/migrations/migrations.py +3 -1
- dataface/core/compile/migrations/versions/__init__.py +16 -0
- dataface/core/compile/models/chart/normalized/callout.py +7 -0
- dataface/core/compile/models/chart/resolved/_base.py +13 -0
- dataface/core/compile/models/chart/resolved/pie.py +10 -0
- dataface/core/compile/models/config.py +20 -19
- dataface/core/compile/models/face/authored.py +12 -8
- dataface/core/compile/models/face/normalized.py +15 -7
- dataface/core/compile/models/face/resolved.py +68 -2
- dataface/core/compile/models/markers.py +20 -0
- dataface/core/compile/models/primitives.py +9 -1
- dataface/core/compile/models/style/authored/table.py +22 -3
- dataface/core/compile/models/style/context.py +225 -0
- dataface/core/compile/models/style/resolved/__init__.py +19 -8
- dataface/core/compile/models/style/resolved/_base.py +131 -129
- dataface/core/compile/models/style/resolved/_cascade.py +433 -104
- dataface/core/compile/models/style/resolved/point_map.py +3 -0
- dataface/core/compile/models/style/resolved/table.py +1 -1
- dataface/core/compile/models/style/theme/__init__.py +5 -7
- dataface/core/compile/models/style/theme/board.py +16 -43
- dataface/core/compile/models/style/theme/charts.py +1 -1
- dataface/core/compile/models/style/theme/marks.py +20 -7
- dataface/core/compile/models/style/theme/style.py +2 -2
- dataface/core/compile/models/style/theme/table.py +7 -1
- dataface/core/compile/models/variable/authored.py +5 -1
- dataface/core/compile/normalize_layout.py +7 -1
- dataface/core/compile/normalize_queries.py +1 -3
- dataface/core/compile/normalize_variables.py +10 -3
- dataface/core/compile/normalizer.py +136 -33
- dataface/core/compile/parameterized.py +25 -12
- dataface/core/compile/resolve/__init__.py +6 -4
- dataface/core/compile/resolve/_axes.py +99 -43
- dataface/core/compile/resolve/_dispatch.py +18 -20
- dataface/core/compile/resolve/_domain.py +34 -1
- dataface/core/compile/resolve/_kwargs.py +90 -46
- dataface/core/compile/resolve/_layers.py +36 -12
- dataface/core/compile/resolve/_marks.py +21 -5
- dataface/core/compile/resolve/_palette.py +25 -18
- dataface/core/compile/resolve/_table.py +42 -25
- dataface/core/compile/resolve/adaptive_stroke.py +7 -3
- dataface/core/compile/resolve/area.py +60 -27
- dataface/core/compile/resolve/bar.py +236 -64
- dataface/core/compile/resolve/geo.py +40 -28
- dataface/core/compile/resolve/heatmap.py +33 -21
- dataface/core/compile/resolve/line.py +61 -28
- dataface/core/compile/resolve/pie.py +48 -24
- dataface/core/compile/resolve/pie_attachment.py +25 -2
- dataface/core/compile/resolve/scatter.py +54 -27
- dataface/core/compile/resolve/simple.py +23 -20
- dataface/core/compile/schema_renderers/json_schema.py +5 -1
- dataface/core/compile/schema_renderers/prompt.py +7 -1
- dataface/core/compile/schema_renderers/yaml_schema_catalog.py +4 -2
- dataface/core/compile/single_series_allocation.py +1 -1
- dataface/core/compile/sizing.py +16 -10
- dataface/core/compile/source_map.py +56 -76
- dataface/core/compile/sources.py +2 -4
- dataface/core/compile/style_cascade.py +166 -66
- dataface/core/compile/typography.py +27 -27
- dataface/core/compile/validate_formats.py +35 -3
- dataface/core/compile/vega_config.py +3 -3
- dataface/core/compile/vega_lite/mapping.py +23 -23
- dataface/core/compile/yaml_error_formatter.py +1 -2
- dataface/core/compile/yaml_patch.py +1 -1
- dataface/core/dashboard.py +6 -5
- dataface/core/defaults/default_config.yml +42 -42
- dataface/core/defaults/themes/_base.yaml +4 -1
- dataface/core/defaults/themes/solid.yaml +245 -0
- dataface/core/defaults/themes/stark.yaml +7 -5
- dataface/core/diagnostics/__init__.py +18 -0
- dataface/core/diagnostics/codes_compile.py +34 -0
- dataface/core/diagnostics/codes_render.py +303 -6
- dataface/core/dialects/athena.py +0 -2
- dataface/core/dialects/base.py +0 -40
- dataface/core/dialects/bigquery.py +0 -2
- dataface/core/dialects/databricks.py +0 -2
- dataface/core/dialects/duckdb.py +0 -2
- dataface/core/dialects/mysql.py +0 -2
- dataface/core/dialects/postgres.py +0 -2
- dataface/core/dialects/snowflake.py +0 -2
- dataface/core/dialects/sqlite.py +0 -2
- dataface/core/dialects/sqlserver.py +0 -2
- dataface/core/execute/_duckdb_cache_base.py +1 -3
- dataface/core/execute/adapters/adapter_registry.py +31 -0
- dataface/core/execute/adapters/sql_adapter.py +1 -3
- dataface/core/execute/cache_composition.py +1 -3
- dataface/core/execute/chart_resolution.py +3 -3
- dataface/core/execute/sql_literals.py +1 -2
- dataface/core/execute/trivial_local_cache.py +3 -1
- dataface/core/font_measure.py +155 -1
- dataface/core/fonts.py +77 -5
- dataface/core/inspect/resolver.py +3 -1
- dataface/core/numeric.py +15 -0
- dataface/core/registered_views/expander.py +95 -24
- dataface/core/registered_views/render_pipeline.py +36 -1
- dataface/core/render/board_links.py +1 -1
- dataface/core/render/chart/callout.py +44 -30
- dataface/core/render/chart/data_table_attachment.py +88 -60
- dataface/core/render/chart/emitters/_cartesian.py +125 -22
- dataface/core/render/chart/emitters/_channels.py +3 -3
- dataface/core/render/chart/emitters/_label_overlap.py +2 -2
- dataface/core/render/chart/emitters/_layers.py +90 -43
- dataface/core/render/chart/emitters/_measured_label_padding.py +26 -11
- dataface/core/render/chart/emitters/_overlay.py +42 -9
- dataface/core/render/chart/emitters/_tooltip.py +6 -6
- dataface/core/render/chart/emitters/area.py +47 -19
- dataface/core/render/chart/emitters/bar.py +92 -47
- dataface/core/render/chart/emitters/geo.py +2 -0
- dataface/core/render/chart/emitters/line.py +21 -19
- dataface/core/render/chart/emitters/pie.py +9 -8
- dataface/core/render/chart/emitters/scatter.py +21 -10
- dataface/core/render/chart/features/baseline.py +9 -9
- dataface/core/render/chart/features/endpoint_labels.py +39 -28
- dataface/core/render/chart/features/facet.py +12 -10
- dataface/core/render/chart/features/mirror_axis.py +59 -11
- dataface/core/render/chart/features/structured_tooltip.py +5 -3
- dataface/core/render/chart/kpi.py +51 -48
- dataface/core/render/chart/rendering.py +136 -36
- dataface/core/render/chart/series_label_truncation.py +81 -0
- dataface/core/render/chart/spark.py +8 -8
- dataface/core/render/chart/spark_bar.py +15 -10
- dataface/core/render/chart/spec.py +16 -4
- dataface/core/render/chart/spec_builders.py +1 -1
- dataface/core/render/chart/table.py +187 -90
- dataface/core/render/chart/table_support.py +11 -46
- dataface/core/render/chart/text_truncation.py +96 -0
- dataface/core/render/chart/time_unit_detect.py +134 -41
- dataface/core/render/chart/title_overflow.py +39 -62
- dataface/core/render/chart/translate.py +4 -10
- dataface/core/render/chart/type_inference.py +14 -0
- dataface/core/render/chart/vega_lite.py +77 -23
- dataface/core/render/chart/vl_field_maps.py +178 -37
- dataface/core/render/chart_diagnostics.py +27 -0
- dataface/core/render/chart_interactivity.py +4 -2
- dataface/core/render/chrome_css.py +2 -2
- dataface/core/render/column_packer.py +65 -29
- dataface/core/render/converters/chart.py +40 -25
- dataface/core/render/face_resolve.py +67 -33
- dataface/core/render/face_to_dict.py +8 -9
- dataface/core/render/faces.py +49 -23
- dataface/core/render/font_support.py +71 -83
- dataface/core/render/fonts/DFTSansTabular-Medium.ttf +0 -0
- dataface/core/render/fonts/DFTSansTabular-SemiBold.ttf +0 -0
- dataface/core/render/fonts/InterVariable-Medium.ttf +0 -0
- dataface/core/render/fonts/InterVariable-SemiBold.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-Medium.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-SemiBold.ttf +0 -0
- dataface/core/render/format_utils.py +44 -19
- dataface/core/render/layout_sizing.py +294 -183
- dataface/core/render/layouts.py +6 -6
- dataface/core/render/numeral_expr.py +27 -17
- dataface/core/render/prose.py +29 -25
- dataface/core/render/renderer.py +112 -69
- dataface/core/render/sizing.py +191 -153
- dataface/core/render/svg_utils.py +7 -10
- dataface/core/render/templates/scripts/chart_interactivity.js +1 -1
- dataface/core/render/terminal.py +8 -2
- dataface/core/render/terminal_charts.py +2 -1
- dataface/core/render/variables_layout.py +5 -3
- dataface/core/render/variables_resolve.py +4 -7
- dataface/core/render/variables_strip.py +9 -2
- dataface/core/render/warnings/axis_title_truncated.py +43 -0
- dataface/core/render/warnings/base.py +18 -0
- dataface/core/render/warnings/callout_text_truncated.py +41 -0
- dataface/core/render/warnings/chart_title_truncated.py +36 -0
- dataface/core/render/warnings/facet_panel_width_below_minimum.py +71 -0
- dataface/core/render/warnings/kpi_label_truncated.py +34 -0
- dataface/core/render/warnings/registry.py +16 -0
- dataface/core/render/warnings/series_label_truncated.py +64 -0
- dataface/core/render/warnings/spark_label_truncated.py +37 -0
- dataface/core/render/warnings/table_text_truncated.py +42 -0
- dataface/core/render/warnings/value_labels_crowd_width.py +1 -1
- dataface/core/render/yaml_format.py +15 -0
- dataface/core/serve/server.py +3 -5
- dataface/core/text/format_d3.py +241 -28
- dataface/core/text/numeral_scale.py +47 -7
- dataface/core/utils.py +84 -2
- dataface/core/validate.py +0 -1
- dataface/data/highlighting/face.json +1 -1
- dataface/data/schemas/board-resolved/board-resolved.schema.json +1644 -3554
- dataface/schema_release.py +4 -2
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev224.dist-info}/METADATA +3 -3
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev224.dist-info}/RECORD +215 -196
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev224.dist-info}/WHEEL +1 -1
- mdsvg/fonts.py +13 -6
- mdsvg/parser.py +17 -0
- mdsvg/renderer.py +175 -12
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev224.dist-info}/entry_points.txt +0 -0
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev224.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -307,9 +307,11 @@ style:
|
|
|
307
307
|
text:
|
|
308
308
|
align: left
|
|
309
309
|
column:
|
|
310
|
-
|
|
310
|
+
max_number: 2
|
|
311
311
|
gap: 24
|
|
312
|
-
rule:
|
|
312
|
+
rule:
|
|
313
|
+
width: 1
|
|
314
|
+
color: "#cbd5e1"
|
|
313
315
|
```
|
|
314
316
|
|
|
315
317
|
CSS-only keys like `border-radius`, `border-left`, `margin-top` are not board-style fields and are rejected.
|
|
@@ -926,6 +928,8 @@ variables:
|
|
|
926
928
|
|
|
927
929
|
Top-level option-source binding (alternative to `options:`): `column`, `query`, `dimension` (MetricFlow), `measure` (MetricFlow), `model` (dbt).
|
|
928
930
|
|
|
931
|
+
Top-level `column` is `table.column`, and the table may be schema-qualified when it is not in the connection's default schema — `column: gis.fact_sales.property_type`. `filter()` accepts the same qualified form.
|
|
932
|
+
|
|
929
933
|
Enabled/disabled forms (`enabled: false` means the control is inactive):
|
|
930
934
|
|
|
931
935
|
```yaml
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
# directory and its subdirectories; each face overrides what it sets.
|
|
7
7
|
#
|
|
8
8
|
# A meta.yaml deeper in the tree layers on top of the ones above it, and a face
|
|
9
|
-
# always wins over meta. Nested style keys merge (a face setting style.
|
|
10
|
-
# keeps a meta-supplied style.
|
|
9
|
+
# always wins over meta. Nested style keys merge (a face setting style.frame.margin
|
|
10
|
+
# keeps a meta-supplied style.frame.width).
|
|
11
11
|
#
|
|
12
12
|
# Uncomment and edit to set project-wide defaults:
|
|
13
13
|
#
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
# source: my_warehouse # default data source for faces that don't name one
|
|
16
16
|
#
|
|
17
17
|
# style:
|
|
18
|
-
#
|
|
18
|
+
# frame:
|
|
19
19
|
# width: 1000
|
|
@@ -22,6 +22,7 @@ from pydantic import BaseModel, ConfigDict
|
|
|
22
22
|
|
|
23
23
|
from dataface.agent_api._paths import resolve_face_or_error
|
|
24
24
|
from dataface.core.compile import compile_file
|
|
25
|
+
from dataface.core.compile.models.face.resolved import ChartResolveFailure
|
|
25
26
|
from dataface.core.compile.variables import normalize_multiselect_values
|
|
26
27
|
from dataface.core.diagnostics import ERR_FILE_NOT_FOUND, Diagnostic
|
|
27
28
|
from dataface.core.diagnostics.base import DatafaceError
|
|
@@ -133,14 +134,27 @@ def emit_board_artifact(
|
|
|
133
134
|
set_filter_variables_context(
|
|
134
135
|
frozenset(face.variable_registry) if face.variable_registry else frozenset()
|
|
135
136
|
)
|
|
137
|
+
resolve_errors: dict[str, ChartResolveFailure] = {}
|
|
136
138
|
try:
|
|
137
|
-
resolved, _render_cache = build_resolved_face(
|
|
139
|
+
resolved, _render_cache = build_resolved_face(
|
|
140
|
+
face, executor, merged_variables, resolve_errors=resolve_errors
|
|
141
|
+
)
|
|
138
142
|
except DatafaceError as exc:
|
|
139
143
|
return EmitBoardArtifactResult(success=False, errors=[exc.to_diagnostic()])
|
|
140
144
|
finally:
|
|
141
145
|
set_auto_link_context(False)
|
|
142
146
|
set_filter_variables_context(frozenset())
|
|
143
147
|
|
|
148
|
+
# A chart-level resolve failure no longer raises — it leaves a hole in the
|
|
149
|
+
# face instead. Publishing that hole silently would be worse than the fatal
|
|
150
|
+
# it replaced, since a live render at least draws an error tile where an
|
|
151
|
+
# artifact just has nothing. Artifacts stay all-or-nothing.
|
|
152
|
+
if resolve_errors:
|
|
153
|
+
return EmitBoardArtifactResult(
|
|
154
|
+
success=False,
|
|
155
|
+
errors=[failure.diagnostic for failure in resolve_errors.values()],
|
|
156
|
+
)
|
|
157
|
+
|
|
144
158
|
recording = record_board(resolved, executor, merged_variables)
|
|
145
159
|
|
|
146
160
|
artifact_path.parent.mkdir(parents=True, exist_ok=True)
|
dataface/agent_api/dashboards.py
CHANGED
|
@@ -7,13 +7,20 @@ values. No bare dict[str, Any] in any return position. Tool/CLI callers use
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import sys
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
from typing import
|
|
12
|
+
from typing import Literal
|
|
12
13
|
|
|
13
14
|
import yaml
|
|
14
|
-
from pydantic import BaseModel, ConfigDict, Field, field_serializer
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, field_serializer, model_validator
|
|
16
|
+
|
|
17
|
+
if sys.version_info >= (3, 11):
|
|
18
|
+
from typing import Self
|
|
19
|
+
else:
|
|
20
|
+
from typing_extensions import Self
|
|
15
21
|
|
|
16
22
|
from dataface.agent_api._paths import resolve_face_or_error
|
|
23
|
+
from dataface.agent_api.query import VariableBinding, variables_to_dict as _vars_to_dict
|
|
17
24
|
from dataface.core.compile import Face, compile_file
|
|
18
25
|
from dataface.core.dashboard import RenderedDashboard as RenderedDashboard
|
|
19
26
|
from dataface.core.diagnostics import Diagnostic
|
|
@@ -112,8 +119,12 @@ class RenderDashboardArgs(BaseModel):
|
|
|
112
119
|
"the list of available chart ids."
|
|
113
120
|
),
|
|
114
121
|
)
|
|
115
|
-
variables:
|
|
116
|
-
None,
|
|
122
|
+
variables: list[VariableBinding] | None = Field(
|
|
123
|
+
None,
|
|
124
|
+
description=(
|
|
125
|
+
"Variable values to apply to the dashboard, as an array of {name, value} pairs. "
|
|
126
|
+
'Example: [{"name": "region", "value": "US"}, {"name": "year", "value": 2024}]'
|
|
127
|
+
),
|
|
117
128
|
)
|
|
118
129
|
format: Literal["json", "text", "yaml", "svg", "terminal", "data"] | None = Field(
|
|
119
130
|
None,
|
|
@@ -148,6 +159,12 @@ class RenderDashboardArgs(BaseModel):
|
|
|
148
159
|
),
|
|
149
160
|
)
|
|
150
161
|
|
|
162
|
+
@model_validator(mode="after")
|
|
163
|
+
def _no_duplicate_variable_names(self) -> Self:
|
|
164
|
+
if self.variables is not None:
|
|
165
|
+
_vars_to_dict(self.variables)
|
|
166
|
+
return self
|
|
167
|
+
|
|
151
168
|
|
|
152
169
|
# ---------------------------------------------------------------------------
|
|
153
170
|
# Public verbs
|
|
@@ -11,7 +11,7 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
11
11
|
| `tags` | list[str] | ✓ | Tags for categorization and search. |
|
|
12
12
|
| `aliases` | list[str] | ✓ | Additional URLs that redirect to this face's canonical file-path URL. Each entry must be absolute (leading /). Requests to these URLs are redirected (302) to the face's real path, query string preserved. Valid on .yml, .yaml, .md, and folder index.* faces. |
|
|
13
13
|
| `text` | str | ✓ | Markdown text content for text-only sections. |
|
|
14
|
-
| `
|
|
14
|
+
| `html_policy` | enum: "none", "safe-subset", "trusted-raw" | ✓ | HTML rendering policy for the face's body text. One of: "none" (default) — HTML is escaped and rendered as plain markdown; "safe-subset" — reserved for a parser-checked allowlist (not yet enforced — currently renders as none); "trusted-raw" — raw HTML via foreignObject. TRUSTED-CONTENT ONLY: this is NOT a security sandbox. <script>/event-handlers are stripped as a best-effort guard, not a guarantee. Enable only on first-party faces you fully control. |
|
|
15
15
|
| `source` | str | ✓ | Default source name for all queries in this face. Inheritable via meta.yaml cascade. |
|
|
16
16
|
| `cache` | [Cache](#cache) | ✓ | Cache policy for every query in this dashboard, e.g. cache: 1h — queries inherit it and may refine it; cache: false opts the whole dashboard out. Inheritable via the meta.yaml cascade. |
|
|
17
17
|
| `variables` | dict[str, [Variable](#variable) \| VariableRef] | ✓ | Variable definitions for dynamic filtering and UI controls. |
|
|
@@ -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). On the root face there is no parent to place it into, so it instead sets the board's own width (equivalent to 'style.
|
|
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.frame.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. |
|
|
@@ -54,7 +54,7 @@ Variable definition from YAML.
|
|
|
54
54
|
| `required` | bool | ✓ | When True, a value must be provided before queries execute. |
|
|
55
55
|
| `visible` | bool | ✓ | When False, the variable is not rendered in the UI but can still be set via URL params. |
|
|
56
56
|
| `enabled` | bool \| str \| [SingleRowBoolProbe](#singlerowboolprobe) | ✓ | Enable this control. Accepts: static bool; a variable name or Jinja boolean expression string (no {{ }} required — bare names auto-wrap); or a {query, column} form that reads a single boolean cell from a named query. None = enabled. Absent variable in a string expression raises (use a default). |
|
|
57
|
-
| `column` | str | ✓ |
|
|
57
|
+
| `column` | str | ✓ | Table column to draw option values from, as 'table.column'. The table may be schema-qualified ('schema.table.column') when it is not in the connection's default schema. |
|
|
58
58
|
| `query` | str \| [SqlQuery](#sqlquery) \| [MetricflowQuery](#metricflowquery) \| [HttpQuery](#httpquery) \| [ValuesQuery](#valuesquery) \| [CompactValuesQuery](#compactvaluesquery) \| [SchemaQuery](#schemaquery) | ✓ | Query name or inline query definition for populating options. |
|
|
59
59
|
| `dimension` | str | ✓ | MetricFlow dimension name for populating options. |
|
|
60
60
|
| `measure` | str | ✓ | MetricFlow measure name for populating options. |
|
|
@@ -496,7 +496,7 @@ Authored overlay for Style — all fields optional. Adds CSS shorthand coercers.
|
|
|
496
496
|
|
|
497
497
|
| Field | Type | Optional | Description |
|
|
498
498
|
|-------|------|:--------:|-------------|
|
|
499
|
-
| `
|
|
499
|
+
| `frame` | [FrameStyle](#framestyle) | ✓ | Face-level structural frame dimensions. |
|
|
500
500
|
| `background` | str | ✓ | Working-surface background color (board and card fills). |
|
|
501
501
|
| `accent` | str | ✓ | Accent color token cascaded to sparklines, bars, and focus rings. |
|
|
502
502
|
| `muted` | str | ✓ | Muted secondary-text color token (KPI support rows, table and spark subtitles). |
|
|
@@ -1023,13 +1023,13 @@ Tab layout item.
|
|
|
1023
1023
|
| `grid` | [GridLayout](#gridlayout) | ✓ | CSS-grid layout for this tab's content. |
|
|
1024
1024
|
| `tabs` | [TabLayout](#tablayout) | ✓ | Nested tab layout (tabs within tabs). |
|
|
1025
1025
|
|
|
1026
|
-
<a id="
|
|
1027
|
-
##
|
|
1028
|
-
Authored overlay for
|
|
1026
|
+
<a id="framestyle"></a>
|
|
1027
|
+
## FrameStyle
|
|
1028
|
+
Authored overlay for FrameStyle. Face-level structural dimensions. Do NOT cascade to child faces.
|
|
1029
1029
|
|
|
1030
1030
|
| Field | Type | Optional | Description |
|
|
1031
1031
|
|-------|------|:--------:|-------------|
|
|
1032
|
-
| `width` | float | ✓ | Maximum
|
|
1032
|
+
| `width` | float | ✓ | Maximum board width in pixels. Caps the measured layout width, setting the board's proportions. Rendered as an on-screen pixel maximum everywhere except the dft HTML page (dft serve, dft render --format html), which scales the board to its container. |
|
|
1033
1033
|
| `min_height` | float | ✓ | Minimum board height in pixels. |
|
|
1034
1034
|
| `margin` | float | ✓ | Board outer margin in pixels. |
|
|
1035
1035
|
| `card_padding` | float | ✓ | Padding added to each card side in pixels. |
|
|
@@ -1689,7 +1689,7 @@ Authored overlay for TableTitleStyle.
|
|
|
1689
1689
|
|
|
1690
1690
|
| Field | Type | Optional | Description |
|
|
1691
1691
|
|-------|------|:--------:|-------------|
|
|
1692
|
-
| `height` | float | ✓ |
|
|
1692
|
+
| `height` | float | ✓ | Minimum title-block height in pixels. Applies as a floor only when a subtitle is present; a title-only block sizes to its natural content height instead. |
|
|
1693
1693
|
| `font` | [FontStyle](#fontstyle) | ✓ | Table title font style overrides. Unset fields fall back to [`style.charts.table.font`](#tablechartstyle). |
|
|
1694
1694
|
|
|
1695
1695
|
<a id="paginationconfig"></a>
|
|
@@ -1898,9 +1898,9 @@ Authored overlay for TextColumnStyle. Author overrides for the column layout of
|
|
|
1898
1898
|
|
|
1899
1899
|
| Field | Type | Optional | Description |
|
|
1900
1900
|
|-------|------|:--------:|-------------|
|
|
1901
|
-
| `
|
|
1901
|
+
| `max_number` | int | ✓ | Ceiling on the column count. The renderer may choose fewer when there is not enough text to fill them. None = no ceiling. |
|
|
1902
1902
|
| `gap` | float | ✓ | Gap between columns in pixels. None = 1.5 line boxes, so the gutter scales with the type it separates. |
|
|
1903
|
-
| `rule` |
|
|
1903
|
+
| `rule` | [ColumnRuleStyle](#columnrulestyle) | ✓ | Vertical rule drawn between columns. None = no rule. |
|
|
1904
1904
|
| `max_chars` | int | ✓ | Column width as a character count, overriding the shipped measure. The width is used exactly and the column count follows from it. |
|
|
1905
1905
|
|
|
1906
1906
|
<a id="textcodestyle"></a>
|
|
@@ -2620,6 +2620,16 @@ Authored overlay for GeoshapeMarkStyle. Geoshape (choropleth) mark fill and boun
|
|
|
2620
2620
|
| `fill` | str | ✓ | Neutral geoshape fill color. |
|
|
2621
2621
|
| `stroke` | [StrokeStyle](#strokestyle) | ✓ | Geoshape boundary stroke style. |
|
|
2622
2622
|
|
|
2623
|
+
<a id="columnrulestyle"></a>
|
|
2624
|
+
## ColumnRuleStyle
|
|
2625
|
+
Authored overlay for ColumnRuleStyle. Vertical rule drawn between prose columns, structured like BorderStyle.
|
|
2626
|
+
|
|
2627
|
+
| Field | Type | Optional | Description |
|
|
2628
|
+
|-------|------|:--------:|-------------|
|
|
2629
|
+
| `width` | float | ✓ | Rule line width in pixels. |
|
|
2630
|
+
| `color` | str | ✓ | Rule color as a CSS color string. |
|
|
2631
|
+
| `style` | enum: "solid", "dashed", "dotted" | ✓ | Rule line style. |
|
|
2632
|
+
|
|
2623
2633
|
<a id="circlemarkstyle"></a>
|
|
2624
2634
|
## CircleMarkStyle
|
|
2625
2635
|
Authored overlay for CircleMarkStyle. Circle mark opacity and stroke.
|
|
@@ -2847,7 +2857,7 @@ Authored overlay for SliceLabelsStyle. Pie labels: typography + positioning offs
|
|
|
2847
2857
|
|-------|------|:--------:|-------------|
|
|
2848
2858
|
| `offset` | float | ✓ | Radial offset of slice labels from the arc in pixels. |
|
|
2849
2859
|
| `line_height` | float | ✓ | Line height for slice labels in pixels. Reserved vertical space above the disk is ``line_height × <rendered lines>`` per row, so the same value handles 1-line, 2-line, and multi-line templates. |
|
|
2850
|
-
| `font` | [FontStyle](#fontstyle) | ✓ | Slice label font style overrides. Unset fields fall back to [`style.charts.font`](#chartsstyle). |
|
|
2860
|
+
| `font` | [FontStyle](#fontstyle) | ✓ | Slice label font style overrides. ``color`` only takes effect on single-series pies (no ``color:`` channel authored); multi-series pies always paint each label the dark companion of its own wedge color and ignore an authored ``color`` here. Unset fields fall back to [`style.charts.font`](#chartsstyle) (except `color`). |
|
|
2851
2861
|
| `default_template` | [LabelsDefaultTemplate](#labelsdefaulttemplate) | ✓ | Default Jinja templates for per-slice labels when template is not authored. |
|
|
2852
2862
|
| `template` | str | ✓ | Jinja2 label template. Overrides default_template when authored. |
|
|
2853
2863
|
| `where` | str | ✓ | Jinja2 boolean filter — labels only render on rows where this is truthy. |
|
dataface/agent_api/pack.py
CHANGED
|
@@ -274,7 +274,7 @@ def _build_face_dict(dashboard: ProposedDashboard) -> dict[str, Any]:
|
|
|
274
274
|
"title": dashboard.title,
|
|
275
275
|
"description": dashboard.purpose,
|
|
276
276
|
"text": (
|
|
277
|
-
f"Dashboard for {dashboard.primary_entity}.
|
|
277
|
+
f"Dashboard for {dashboard.primary_entity}. Add queries and charts here."
|
|
278
278
|
),
|
|
279
279
|
}
|
|
280
280
|
if dashboard.canonical_data_url:
|
|
@@ -365,8 +365,11 @@ class ProjectSession:
|
|
|
365
365
|
self,
|
|
366
366
|
name: str,
|
|
367
367
|
path: Path,
|
|
368
|
+
vars: dict[str, Any] | None = None,
|
|
368
369
|
) -> _query.FaceQueryLookupResult:
|
|
369
|
-
return _query.lookup_face_query_sql(
|
|
370
|
+
return _query.lookup_face_query_sql(
|
|
371
|
+
name=name, path=path, project=self.project, vars=vars
|
|
372
|
+
)
|
|
370
373
|
|
|
371
374
|
def query_face(
|
|
372
375
|
self,
|
dataface/agent_api/query.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"""Typed query verbs for the agent API."""
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from pathlib import Path, PurePosixPath
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 11):
|
|
10
|
+
from typing import Self
|
|
11
|
+
else:
|
|
12
|
+
from typing_extensions import Self
|
|
7
13
|
|
|
8
14
|
from dataface.agent_api._paths import resolve_face_path
|
|
9
15
|
from dataface.core.compile import compile_file
|
|
@@ -16,6 +22,34 @@ from dataface.core.validate import normalize_data_for_json
|
|
|
16
22
|
MAX_QUERY_LIMIT = 1000
|
|
17
23
|
|
|
18
24
|
|
|
25
|
+
class VariableBinding(BaseModel):
|
|
26
|
+
"""One variable binding in a tool call: a name–value pair.
|
|
27
|
+
|
|
28
|
+
Used in the ``variables`` / ``vars`` fields of ``render_dashboard``,
|
|
29
|
+
``execute_query``, and ``query_face`` instead of an open-keyed dict, so
|
|
30
|
+
that OpenAI strict mode can validate the full tool-call structure.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
34
|
+
|
|
35
|
+
name: str = Field(..., description="Variable name, e.g. 'region'")
|
|
36
|
+
value: str | int | float | bool = Field(
|
|
37
|
+
..., description="Scalar value for the variable"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def variables_to_dict(
|
|
42
|
+
bindings: list[VariableBinding],
|
|
43
|
+
) -> dict[str, str | int | float | bool]:
|
|
44
|
+
"""Convert a VariableBinding list to a dict, raising on duplicate names."""
|
|
45
|
+
result: dict[str, str | int | float | bool] = {}
|
|
46
|
+
for binding in bindings:
|
|
47
|
+
if binding.name in result:
|
|
48
|
+
raise ValueError(f"Duplicate variable name: {binding.name!r}")
|
|
49
|
+
result[binding.name] = binding.value
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
19
53
|
class FaceQueryLookupResult(BaseModel):
|
|
20
54
|
"""Result of extracting SQL from a named face query for offline use (validate/describe)."""
|
|
21
55
|
|
|
@@ -33,12 +67,17 @@ def lookup_face_query_sql(
|
|
|
33
67
|
path: Path,
|
|
34
68
|
*,
|
|
35
69
|
project: Project,
|
|
70
|
+
vars: dict[str, Any] | None = None,
|
|
36
71
|
) -> FaceQueryLookupResult:
|
|
37
|
-
"""Compile a face file
|
|
72
|
+
"""Compile a face file, render the SQL template, and return it for offline use.
|
|
38
73
|
|
|
39
|
-
|
|
40
|
-
|
|
74
|
+
Expands ``{{ queries.X }}`` and substitutes variables so callers receive
|
|
75
|
+
valid SQL rather than a raw Jinja template. Used by validate and describe
|
|
76
|
+
paths that need the rendered SQL text without executing it against a
|
|
77
|
+
warehouse.
|
|
41
78
|
"""
|
|
79
|
+
from dataface.core.compile.parameterized import render_parameterized_with_queries
|
|
80
|
+
|
|
42
81
|
try:
|
|
43
82
|
file_path = resolve_face_path(path, project)
|
|
44
83
|
except ValueError as e:
|
|
@@ -74,7 +113,18 @@ def lookup_face_query_sql(
|
|
|
74
113
|
],
|
|
75
114
|
)
|
|
76
115
|
|
|
77
|
-
|
|
116
|
+
merged_vars = {**face.variable_defaults, **(vars or {})}
|
|
117
|
+
try:
|
|
118
|
+
rendered = render_parameterized_with_queries(
|
|
119
|
+
query.sql,
|
|
120
|
+
merged_vars,
|
|
121
|
+
queries=face.queries,
|
|
122
|
+
strict=not query.lenient_variables,
|
|
123
|
+
)
|
|
124
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
125
|
+
return FaceQueryLookupResult(success=False, errors=[str(exc)])
|
|
126
|
+
|
|
127
|
+
return FaceQueryLookupResult(success=True, sql=rendered.sql, source=query.source)
|
|
78
128
|
|
|
79
129
|
|
|
80
130
|
class ExecuteQueryArgs(BaseModel):
|
|
@@ -84,8 +134,12 @@ class ExecuteQueryArgs(BaseModel):
|
|
|
84
134
|
...,
|
|
85
135
|
description="SQL query to execute. Use {{ variable_name }} for parameterized values — these work identically in dashboard YAML, so queries you test here will be cached when reused in dashboards.",
|
|
86
136
|
)
|
|
87
|
-
variables:
|
|
88
|
-
None,
|
|
137
|
+
variables: list[VariableBinding] | None = Field(
|
|
138
|
+
None,
|
|
139
|
+
description=(
|
|
140
|
+
"Variable values to substitute, as an array of {name, value} pairs. "
|
|
141
|
+
'Example: [{"name": "region", "value": "US"}, {"name": "year", "value": 2024}]'
|
|
142
|
+
),
|
|
89
143
|
)
|
|
90
144
|
source: str | None = Field(None, description="Data source name to execute against")
|
|
91
145
|
limit: int | None = Field(None, description="Maximum rows to return (default 50)")
|
|
@@ -93,6 +147,20 @@ class ExecuteQueryArgs(BaseModel):
|
|
|
93
147
|
False,
|
|
94
148
|
description="When True, undefined Jinja variables degrade gracefully (useful for iterative chart editing where not all variables are set).",
|
|
95
149
|
)
|
|
150
|
+
# Display only — never reaches execute_query(). Ad-hoc SQL is the one query
|
|
151
|
+
# surface with no author to describe it: a named face query carries the
|
|
152
|
+
# `description:` its YAML already declares, and this is the equivalent for
|
|
153
|
+
# a query that exists only for the length of one tool call.
|
|
154
|
+
description: str | None = Field(
|
|
155
|
+
None,
|
|
156
|
+
description="Short phrase naming what this query is for (e.g. 'Leads by industry'), shown to the user in place of the raw SQL.",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@model_validator(mode="after")
|
|
160
|
+
def _no_duplicate_variable_names(self) -> Self:
|
|
161
|
+
if self.variables is not None:
|
|
162
|
+
variables_to_dict(self.variables)
|
|
163
|
+
return self
|
|
96
164
|
|
|
97
165
|
|
|
98
166
|
class ExecuteQueryResult(BaseModel):
|
|
@@ -218,8 +286,12 @@ class QueryFaceArgs(BaseModel):
|
|
|
218
286
|
..., description="Name of the query inside the face's `queries:` block"
|
|
219
287
|
)
|
|
220
288
|
path: Path = Field(..., description="Path to the face YAML file")
|
|
221
|
-
vars:
|
|
222
|
-
None,
|
|
289
|
+
vars: list[VariableBinding] | None = Field(
|
|
290
|
+
None,
|
|
291
|
+
description=(
|
|
292
|
+
"Variable overrides as an array of {name, value} pairs. "
|
|
293
|
+
'Example: [{"name": "country", "value": "FR"}]'
|
|
294
|
+
),
|
|
223
295
|
)
|
|
224
296
|
limit: int = Field(
|
|
225
297
|
20, ge=1, le=1000, description="Max rows to return (default 20, max 1000)"
|
|
@@ -227,6 +299,12 @@ class QueryFaceArgs(BaseModel):
|
|
|
227
299
|
|
|
228
300
|
model_config = ConfigDict(extra="forbid")
|
|
229
301
|
|
|
302
|
+
@model_validator(mode="after")
|
|
303
|
+
def _no_duplicate_variable_names(self) -> Self:
|
|
304
|
+
if self.vars is not None:
|
|
305
|
+
variables_to_dict(self.vars)
|
|
306
|
+
return self
|
|
307
|
+
|
|
230
308
|
|
|
231
309
|
class QueryFaceResult(BaseModel):
|
|
232
310
|
model_config = ConfigDict(frozen=True)
|
|
@@ -236,6 +314,9 @@ class QueryFaceResult(BaseModel):
|
|
|
236
314
|
path: PurePosixPath
|
|
237
315
|
query_type: str | None = None
|
|
238
316
|
sql: str | None = None
|
|
317
|
+
#: The query's authored `description:`, when its YAML declares one. Lets a
|
|
318
|
+
#: caller label the run with what it is for rather than its identifier.
|
|
319
|
+
description: str | None = None
|
|
239
320
|
columns: list[str] = []
|
|
240
321
|
data: list[dict[str, Any]] = []
|
|
241
322
|
row_count: int = 0
|
|
@@ -249,11 +330,19 @@ def _fail(
|
|
|
249
330
|
path: PurePosixPath,
|
|
250
331
|
errors: list[str],
|
|
251
332
|
available: list[str] | None = None,
|
|
333
|
+
sql: str | None = None,
|
|
334
|
+
description: str | None = None,
|
|
252
335
|
) -> QueryFaceResult:
|
|
336
|
+
"""A failure reports the query it was running whenever it got far enough to
|
|
337
|
+
know it: an undefined-variable error is unreadable without the SQL holding
|
|
338
|
+
the `{{ }}` that went unbound. Failures before the query is looked up
|
|
339
|
+
(unresolvable path, compile error, unknown name) have neither to report."""
|
|
253
340
|
return QueryFaceResult(
|
|
254
341
|
success=False,
|
|
255
342
|
name=name,
|
|
256
343
|
path=path,
|
|
344
|
+
sql=sql,
|
|
345
|
+
description=description,
|
|
257
346
|
errors=errors,
|
|
258
347
|
available_queries=available or [],
|
|
259
348
|
)
|
|
@@ -298,6 +387,8 @@ def query_face(
|
|
|
298
387
|
|
|
299
388
|
merged_vars = {**face.variable_defaults, **(vars or {})}
|
|
300
389
|
query = face.queries[name].model_copy(update={"limit": limit + 1})
|
|
390
|
+
sql = query.sql if isinstance(query, SqlQuery) else None
|
|
391
|
+
described = query.description
|
|
301
392
|
|
|
302
393
|
# Detect file sources before dispatching — the adapter path has no materializer
|
|
303
394
|
# and would fall through to SqlAdapter with a cryptic dbt error. File-source
|
|
@@ -324,6 +415,8 @@ def query_face(
|
|
|
324
415
|
"(dft render / dft serve). "
|
|
325
416
|
"Use dft render to preview this dashboard."
|
|
326
417
|
],
|
|
418
|
+
sql=sql,
|
|
419
|
+
description=described,
|
|
327
420
|
)
|
|
328
421
|
|
|
329
422
|
try:
|
|
@@ -331,10 +424,16 @@ def query_face(
|
|
|
331
424
|
query, variables=merged_vars or None, face=face, query_name=name
|
|
332
425
|
)
|
|
333
426
|
except Exception as e: # noqa: BLE001
|
|
334
|
-
return _fail(name, resolved_display, [str(e)])
|
|
427
|
+
return _fail(name, resolved_display, [str(e)], sql=sql, description=described)
|
|
335
428
|
|
|
336
429
|
if exec_result.error:
|
|
337
|
-
return _fail(
|
|
430
|
+
return _fail(
|
|
431
|
+
name,
|
|
432
|
+
resolved_display,
|
|
433
|
+
[exec_result.error],
|
|
434
|
+
sql=sql,
|
|
435
|
+
description=described,
|
|
436
|
+
)
|
|
338
437
|
|
|
339
438
|
data = normalize_data_for_json(exec_result.data)
|
|
340
439
|
truncated = len(data) > limit
|
|
@@ -345,7 +444,8 @@ def query_face(
|
|
|
345
444
|
name=name,
|
|
346
445
|
path=resolved_display,
|
|
347
446
|
query_type=query.query_type,
|
|
348
|
-
sql=
|
|
447
|
+
sql=sql,
|
|
448
|
+
description=described,
|
|
349
449
|
columns=list(data[0].keys()) if data else [],
|
|
350
450
|
data=data,
|
|
351
451
|
row_count=len(data),
|
dataface/agent_api/serve.py
CHANGED
|
@@ -58,9 +58,7 @@ def prepare_serve(
|
|
|
58
58
|
from dataface.core.serve.port import resolve_port
|
|
59
59
|
from dataface.core.serve.server import create_server
|
|
60
60
|
|
|
61
|
-
env_theme = os.environ.get(
|
|
62
|
-
"DFT_DEFAULT_THEME"
|
|
63
|
-
) # noqa: TID251 — DFT_DEFAULT_THEME knob
|
|
61
|
+
env_theme = os.environ.get("DFT_DEFAULT_THEME") # noqa: TID251 — DFT_DEFAULT_THEME knob
|
|
64
62
|
# Empty string is treated as unset (matches get_default_theme_name, which
|
|
65
63
|
# falls back to the shipped default) — only a non-empty unknown name errors.
|
|
66
64
|
if env_theme:
|
dataface/ai/llm.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import copy
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
@@ -93,24 +94,69 @@ def _resolve_model(
|
|
|
93
94
|
return explicit_model or os.getenv(env_var) or default_model
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
def _to_strict_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
98
|
+
"""Transform a JSON Schema dict in-place to conform to OpenAI strict-mode rules.
|
|
99
|
+
|
|
100
|
+
Mirrors openai/lib/_pydantic.py::_ensure_strict_json_schema:
|
|
101
|
+
- Every object gets additionalProperties: false.
|
|
102
|
+
- required covers all property keys whenever properties is present.
|
|
103
|
+
- 'default: null' entries are stripped (non-null defaults are left intact).
|
|
104
|
+
Recurses into $defs, definitions, properties, items, anyOf, allOf, oneOf.
|
|
105
|
+
"""
|
|
106
|
+
if schema.get("type") == "object":
|
|
107
|
+
schema.setdefault("additionalProperties", False)
|
|
108
|
+
props = schema.get("properties", {})
|
|
109
|
+
if props:
|
|
110
|
+
schema["required"] = list(props.keys())
|
|
111
|
+
|
|
112
|
+
if schema.get("default") is None and "default" in schema:
|
|
113
|
+
del schema["default"]
|
|
114
|
+
|
|
115
|
+
for defs_key in ("$defs", "definitions"):
|
|
116
|
+
for sub in schema.get(defs_key, {}).values():
|
|
117
|
+
_to_strict_json_schema(sub)
|
|
118
|
+
|
|
119
|
+
for sub in schema.get("properties", {}).values():
|
|
120
|
+
_to_strict_json_schema(sub)
|
|
121
|
+
|
|
122
|
+
for key in ("anyOf", "allOf", "oneOf"):
|
|
123
|
+
for sub in schema.get(key, []):
|
|
124
|
+
_to_strict_json_schema(sub)
|
|
125
|
+
|
|
126
|
+
if "items" in schema:
|
|
127
|
+
_to_strict_json_schema(schema["items"])
|
|
128
|
+
|
|
129
|
+
return schema
|
|
130
|
+
|
|
131
|
+
|
|
96
132
|
def _normalize_openai_tools(
|
|
97
133
|
tools: list[dict[str, Any]] | None,
|
|
98
134
|
) -> list[dict[str, Any]]:
|
|
99
135
|
# None means "use default agent tools"; [] means "no tools, text-only completion".
|
|
100
136
|
source_tools = ALL_TOOLS if tools is None else tools
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
result: list[dict[str, Any]] = []
|
|
138
|
+
for tool in source_tools:
|
|
139
|
+
if "input_schema" in tool:
|
|
140
|
+
# First-party tool in MCP/input_schema format — strict-transform and
|
|
141
|
+
# convert to OpenAI Responses API format. Strict mode is a property of
|
|
142
|
+
# schemas we authored, not of the transport; MCP passthrough schemas
|
|
143
|
+
# (already in OpenAI function format) are appended below unchanged.
|
|
144
|
+
result.append(
|
|
145
|
+
{
|
|
146
|
+
"type": "function",
|
|
147
|
+
"strict": True,
|
|
148
|
+
"name": tool["name"],
|
|
149
|
+
"description": tool["description"],
|
|
150
|
+
"parameters": _to_strict_json_schema(
|
|
151
|
+
copy.deepcopy(tool["input_schema"])
|
|
152
|
+
),
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
# Already in OpenAI function format (e.g. MCP passthrough) — pass through
|
|
157
|
+
# untouched. Caller-supplied schemas must not be strict-marked or rewritten.
|
|
158
|
+
result.append(tool)
|
|
159
|
+
return result
|
|
114
160
|
|
|
115
161
|
|
|
116
162
|
class LLMClientError(RuntimeError):
|
|
@@ -260,12 +260,14 @@ Named presets (prefer these over raw D3 strings):
|
|
|
260
260
|
| Preset | Example output | Notes |
|
|
261
261
|
|--------|---------------|-------|
|
|
262
262
|
| `integer` | `1,234` | |
|
|
263
|
+
| `year` | `2025` — no grouping, no SI scaling | an identifier, not a quantity |
|
|
263
264
|
| `currency_whole` | `$1,234` | |
|
|
264
265
|
| `currency_compact` | `$1.2M` | |
|
|
265
266
|
| `percent` | `2.3%` — input is decimal fraction (0.023) | line/bar/area/scatter via `style.number_format` |
|
|
266
267
|
| `percent_number` | `2.3%` — input is whole-number percent (2.3) | **KPI only** (`style.value.format`) |
|
|
267
268
|
| `percent_delta` | `+2.3%` — input is decimal fraction | |
|
|
268
269
|
| `percent_number_delta` | `+2.3%` — input is whole-number (2.3) | **KPI only** |
|
|
270
|
+
| `percentage_points_delta` | `+3.2 pts` — the difference of two percentages | for a point delta, not a ratio delta |
|
|
269
271
|
|
|
270
272
|
**`percent_number` and `percent_number_delta` only work on `type: kpi`.** Using them on line/bar/area/scatter causes a render error. For those chart types use `percent` or `percent_delta` (decimal-fraction input) or omit formatting and accept axis defaults.
|
|
271
273
|
|