dataface 0.1.2__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.
- d3_format/__init__.py +14 -0
- d3_format/errors.py +19 -0
- d3_format/format.py +551 -0
- d3_format/spec.py +159 -0
- dataface/DATAFACE_SYNTAX.md +1135 -0
- dataface/__init__.py +93 -0
- dataface/_docs_site.py +20 -0
- dataface/_install_hint.py +26 -0
- dataface/agent_api/__init__.py +79 -0
- dataface/agent_api/_init_templates/__init__.py +0 -0
- dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
- dataface/agent_api/_init_templates/dataface.yml +15 -0
- dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
- dataface/agent_api/_init_templates/index.md +24 -0
- dataface/agent_api/_paths.py +118 -0
- dataface/agent_api/_project_agents_md.py +43 -0
- dataface/agent_api/_session_store.py +486 -0
- dataface/agent_api/_state.py +28 -0
- dataface/agent_api/chat.py +221 -0
- dataface/agent_api/dashboards.py +257 -0
- dataface/agent_api/describe.py +366 -0
- dataface/agent_api/describe_query.py +120 -0
- dataface/agent_api/docs/__init__.py +25 -0
- dataface/agent_api/docs/_loader.py +292 -0
- dataface/agent_api/docs/yaml-reference.md +2757 -0
- dataface/agent_api/file_refs.py +118 -0
- dataface/agent_api/init.py +126 -0
- dataface/agent_api/inspect.py +128 -0
- dataface/agent_api/mcp_install.py +170 -0
- dataface/agent_api/query.py +274 -0
- dataface/agent_api/schema.py +658 -0
- dataface/agent_api/schema_search.py +284 -0
- dataface/agent_api/search.py +270 -0
- dataface/agent_api/skill_install.py +141 -0
- dataface/agent_api/skill_render.py +90 -0
- dataface/agent_api/skills.py +293 -0
- dataface/agent_api/surface_aliases.yaml +128 -0
- dataface/agent_api/validate.py +175 -0
- dataface/agent_api/validate_query.py +84 -0
- dataface/ai/__init__.py +39 -0
- dataface/ai/agent.py +139 -0
- dataface/ai/context.py +45 -0
- dataface/ai/events.py +62 -0
- dataface/ai/external_mcp.py +610 -0
- dataface/ai/generate_sql.py +96 -0
- dataface/ai/llm.py +403 -0
- dataface/ai/mcp/__init__.py +51 -0
- dataface/ai/mcp/server.py +289 -0
- dataface/ai/memories.py +85 -0
- dataface/ai/prompts.py +177 -0
- dataface/ai/schema_context.py +138 -0
- dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
- dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
- dataface/ai/skills/dashboard-build/SKILL.md +212 -0
- dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
- dataface/ai/skills/dashboard-design/SKILL.md +182 -0
- dataface/ai/skills/dashboard-review/SKILL.md +113 -0
- dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
- dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
- dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
- dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
- dataface/ai/skills/drill-down-link/SKILL.md +112 -0
- dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
- dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
- dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
- dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
- dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
- dataface/ai/skills/kpi-row/SKILL.md +101 -0
- dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
- dataface/ai/skills/report-design/SKILL.md +184 -0
- dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
- dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
- dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
- dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
- dataface/ai/skills/time-series-trend/SKILL.md +93 -0
- dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
- dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
- dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
- dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
- dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
- dataface/ai/tool_schemas.py +132 -0
- dataface/ai/tools/__init__.py +312 -0
- dataface/ai/yaml_utils.py +57 -0
- dataface/cli/__init__.py +3 -0
- dataface/cli/_console.py +48 -0
- dataface/cli/_error_format.py +83 -0
- dataface/cli/_extras.py +190 -0
- dataface/cli/_json_output.py +8 -0
- dataface/cli/_parsing.py +17 -0
- dataface/cli/_version_info.py +56 -0
- dataface/cli/commands/__init__.py +3 -0
- dataface/cli/commands/_agent_input.py +205 -0
- dataface/cli/commands/_agent_server.py +115 -0
- dataface/cli/commands/chat.py +645 -0
- dataface/cli/commands/describe.py +107 -0
- dataface/cli/commands/docs.py +131 -0
- dataface/cli/commands/extension.py +179 -0
- dataface/cli/commands/init.py +240 -0
- dataface/cli/commands/inspect.py +94 -0
- dataface/cli/commands/mcp_init.py +167 -0
- dataface/cli/commands/query.py +386 -0
- dataface/cli/commands/render.py +291 -0
- dataface/cli/commands/schema.py +411 -0
- dataface/cli/commands/search.py +49 -0
- dataface/cli/commands/serve.py +114 -0
- dataface/cli/commands/skills.py +133 -0
- dataface/cli/commands/skills_init.py +161 -0
- dataface/cli/commands/validate.py +63 -0
- dataface/cli/main.py +1501 -0
- dataface/core/__init__.py +75 -0
- dataface/core/compile/__init__.py +244 -0
- dataface/core/compile/_jinja_helpers.py +78 -0
- dataface/core/compile/channel.py +222 -0
- dataface/core/compile/chart_focus.py +101 -0
- dataface/core/compile/chart_resolved.py +169 -0
- dataface/core/compile/chart_type_detection.py +489 -0
- dataface/core/compile/chart_update.py +261 -0
- dataface/core/compile/colors.py +64 -0
- dataface/core/compile/compiler.py +904 -0
- dataface/core/compile/config.py +823 -0
- dataface/core/compile/custom_chart_types.py +208 -0
- dataface/core/compile/data_table_attachment.py +1287 -0
- dataface/core/compile/detect.py +110 -0
- dataface/core/compile/errors.py +302 -0
- dataface/core/compile/filter_injection.py +319 -0
- dataface/core/compile/introspection.py +527 -0
- dataface/core/compile/jinja.py +511 -0
- dataface/core/compile/labels_env.py +52 -0
- dataface/core/compile/markdown.py +154 -0
- dataface/core/compile/meta.py +388 -0
- dataface/core/compile/models/__init__.py +0 -0
- dataface/core/compile/models/chart/__init__.py +0 -0
- dataface/core/compile/models/chart/authored.py +2137 -0
- dataface/core/compile/models/chart/compiled.py +398 -0
- dataface/core/compile/models/config.py +347 -0
- dataface/core/compile/models/face/__init__.py +0 -0
- dataface/core/compile/models/face/authored.py +659 -0
- dataface/core/compile/models/face/compiled.py +522 -0
- dataface/core/compile/models/factories.py +201 -0
- dataface/core/compile/models/markers.py +40 -0
- dataface/core/compile/models/palette.py +36 -0
- dataface/core/compile/models/primitives.py +415 -0
- dataface/core/compile/models/query/__init__.py +0 -0
- dataface/core/compile/models/query/authored.py +246 -0
- dataface/core/compile/models/query/compiled.py +710 -0
- dataface/core/compile/models/refs.py +137 -0
- dataface/core/compile/models/source.py +611 -0
- dataface/core/compile/models/style/__init__.py +0 -0
- dataface/core/compile/models/style/authored.py +481 -0
- dataface/core/compile/models/style/compiled.py +3399 -0
- dataface/core/compile/models/style/merged.py +1682 -0
- dataface/core/compile/models/theme.py +362 -0
- dataface/core/compile/models/variable/__init__.py +0 -0
- dataface/core/compile/models/variable/authored.py +254 -0
- dataface/core/compile/models/vega_lite/__init__.py +0 -0
- dataface/core/compile/models/vega_lite/config.py +510 -0
- dataface/core/compile/models/vega_lite/contracts.py +171 -0
- dataface/core/compile/normalize_charts.py +494 -0
- dataface/core/compile/normalize_layout.py +1000 -0
- dataface/core/compile/normalize_queries.py +297 -0
- dataface/core/compile/normalize_variables.py +489 -0
- dataface/core/compile/normalizer.py +543 -0
- dataface/core/compile/palette.py +1100 -0
- dataface/core/compile/parameterized.py +658 -0
- dataface/core/compile/parser.py +228 -0
- dataface/core/compile/schema.py +20 -0
- dataface/core/compile/schema_renderers/__init__.py +0 -0
- dataface/core/compile/schema_renderers/json_schema.py +163 -0
- dataface/core/compile/schema_renderers/prompt.py +152 -0
- dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
- dataface/core/compile/sizing.py +2126 -0
- dataface/core/compile/sources.py +518 -0
- dataface/core/compile/sql_authoring_lint.py +56 -0
- dataface/core/compile/style_cascade.py +471 -0
- dataface/core/compile/typography.py +299 -0
- dataface/core/compile/validator.py +301 -0
- dataface/core/compile/variables.py +53 -0
- dataface/core/compile/vega_config.py +98 -0
- dataface/core/compile/vega_lite/__init__.py +6 -0
- dataface/core/compile/vega_lite/validation.py +95 -0
- dataface/core/compile/yaml_error_formatter.py +838 -0
- dataface/core/connections.py +38 -0
- dataface/core/dashboard.py +358 -0
- dataface/core/defaults/default_config.yml +101 -0
- dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
- dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
- dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
- dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
- dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
- dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
- dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
- dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
- dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
- dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
- dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
- dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
- dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
- dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
- dataface/core/defaults/palettes/sequential/amber.yml +22 -0
- dataface/core/defaults/palettes/sequential/blue.yml +22 -0
- dataface/core/defaults/palettes/sequential/brown.yml +22 -0
- dataface/core/defaults/palettes/sequential/gray.yml +22 -0
- dataface/core/defaults/palettes/sequential/green.yml +22 -0
- dataface/core/defaults/palettes/sequential/purple.yml +22 -0
- dataface/core/defaults/palettes/sequential/rust.yml +22 -0
- dataface/core/defaults/palettes/sequential/teal.yml +22 -0
- dataface/core/defaults/palettes/tone/negative.yml +32 -0
- dataface/core/defaults/palettes/tone/positive.yml +22 -0
- dataface/core/defaults/palettes/tone/warning.yml +22 -0
- dataface/core/defaults/themes/_base.yaml +786 -0
- dataface/core/defaults/themes/bi.yaml +16 -0
- dataface/core/defaults/themes/carbong100.yaml +41 -0
- dataface/core/defaults/themes/cream.yaml +122 -0
- dataface/core/defaults/themes/dark.yaml +40 -0
- dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
- dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
- dataface/core/defaults/themes/editorial.yaml +147 -0
- dataface/core/defaults/themes/light.yaml +30 -0
- dataface/core/defaults/themes/looker.yaml +17 -0
- dataface/core/defaults/themes/stark.yaml +134 -0
- dataface/core/errors/__init__.py +67 -0
- dataface/core/errors/codes_compile.py +56 -0
- dataface/core/errors/codes_execute.py +177 -0
- dataface/core/errors/codes_render.py +106 -0
- dataface/core/errors/codes_unknown.py +15 -0
- dataface/core/errors/hints.py +74 -0
- dataface/core/errors/registry.py +42 -0
- dataface/core/errors/structured.py +92 -0
- dataface/core/execute/__init__.py +91 -0
- dataface/core/execute/adapters/__init__.py +49 -0
- dataface/core/execute/adapters/adapter_registry.py +400 -0
- dataface/core/execute/adapters/base.py +245 -0
- dataface/core/execute/adapters/csv_adapter.py +239 -0
- dataface/core/execute/adapters/dbt_adapter.py +283 -0
- dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
- dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
- dataface/core/execute/adapters/dbt_utils.py +150 -0
- dataface/core/execute/adapters/http_adapter.py +224 -0
- dataface/core/execute/adapters/metricflow_adapter.py +94 -0
- dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
- dataface/core/execute/adapters/sql_adapter.py +710 -0
- dataface/core/execute/adapters/values_adapter.py +58 -0
- dataface/core/execute/batch.py +744 -0
- dataface/core/execute/cache_backend.py +135 -0
- dataface/core/execute/cache_keys.py +66 -0
- dataface/core/execute/dbt_jinja.py +21 -0
- dataface/core/execute/dialects/__init__.py +121 -0
- dataface/core/execute/dialects/athena.py +75 -0
- dataface/core/execute/dialects/base.py +302 -0
- dataface/core/execute/dialects/bigquery.py +38 -0
- dataface/core/execute/dialects/databricks.py +68 -0
- dataface/core/execute/dialects/duckdb.py +35 -0
- dataface/core/execute/dialects/mysql.py +68 -0
- dataface/core/execute/dialects/postgres.py +39 -0
- dataface/core/execute/dialects/redshift.py +12 -0
- dataface/core/execute/dialects/snowflake.py +51 -0
- dataface/core/execute/dialects/sqlserver.py +92 -0
- dataface/core/execute/duckdb_cache.py +712 -0
- dataface/core/execute/duckdb_config.py +26 -0
- dataface/core/execute/errors.py +213 -0
- dataface/core/execute/executor.py +1249 -0
- dataface/core/execute/parallel.py +162 -0
- dataface/core/execute/setup_sql.py +58 -0
- dataface/core/execute/source_registry.py +72 -0
- dataface/core/execute/source_resolver.py +255 -0
- dataface/core/execute/sql_guard.py +387 -0
- dataface/core/execute/sql_literals.py +199 -0
- dataface/core/fonts.py +52 -0
- dataface/core/inspect/__init__.py +32 -0
- dataface/core/inspect/cache_factory.py +98 -0
- dataface/core/inspect/db_types.py +162 -0
- dataface/core/inspect/dbt_schema.py +96 -0
- dataface/core/inspect/defaults.yml +37 -0
- dataface/core/inspect/fanout_risk.py +109 -0
- dataface/core/inspect/manifest_utils.py +77 -0
- dataface/core/inspect/partials/categorical.yml +40 -0
- dataface/core/inspect/partials/date.yml +40 -0
- dataface/core/inspect/partials/numeric.yml +55 -0
- dataface/core/inspect/partition_types.py +38 -0
- dataface/core/inspect/query_validator.py +975 -0
- dataface/core/inspect/renderer.py +354 -0
- dataface/core/inspect/resolver.py +808 -0
- dataface/core/inspect/search.py +461 -0
- dataface/core/inspect/sources/__init__.py +32 -0
- dataface/core/inspect/sources/dbt.py +738 -0
- dataface/core/inspect/sources/duckdb_utils.py +66 -0
- dataface/core/inspect/templates/__init__.py +1 -0
- dataface/core/inspect/templates/categorical_column.yml +196 -0
- dataface/core/inspect/templates/charts.yml +109 -0
- dataface/core/inspect/templates/date_column.yml +248 -0
- dataface/core/inspect/templates/model.yml +138 -0
- dataface/core/inspect/templates/numeric_column.yml +261 -0
- dataface/core/inspect/templates/quality.yml +80 -0
- dataface/core/inspect/templates/string_column.yml +263 -0
- dataface/core/project_roots.py +165 -0
- dataface/core/render/__init__.py +87 -0
- dataface/core/render/board_links.py +176 -0
- dataface/core/render/chart/__init__.py +27 -0
- dataface/core/render/chart/arc_attached_table.py +251 -0
- dataface/core/render/chart/artifacts.py +16 -0
- dataface/core/render/chart/callout.py +225 -0
- dataface/core/render/chart/decisions.py +358 -0
- dataface/core/render/chart/geo.py +700 -0
- dataface/core/render/chart/kpi.py +916 -0
- dataface/core/render/chart/labels.py +76 -0
- dataface/core/render/chart/pipeline.py +818 -0
- dataface/core/render/chart/presentation.py +36 -0
- dataface/core/render/chart/profile.py +3438 -0
- dataface/core/render/chart/render_single.py +347 -0
- dataface/core/render/chart/renderers.py +193 -0
- dataface/core/render/chart/rendering.py +565 -0
- dataface/core/render/chart/serialization.py +90 -0
- dataface/core/render/chart/spark.py +496 -0
- dataface/core/render/chart/spark_bar.py +370 -0
- dataface/core/render/chart/spec_builders.py +154 -0
- dataface/core/render/chart/standard_renderer.py +2645 -0
- dataface/core/render/chart/table.py +2957 -0
- dataface/core/render/chart/table_support.py +1452 -0
- dataface/core/render/chart/tick_values.py +66 -0
- dataface/core/render/chart/time_unit_detect.py +809 -0
- dataface/core/render/chart/title_overflow.py +157 -0
- dataface/core/render/chart/type_inference.py +122 -0
- dataface/core/render/chart/validation.py +99 -0
- dataface/core/render/chart/vega_lite.py +125 -0
- dataface/core/render/chart/vega_lite_types.py +268 -0
- dataface/core/render/chart/vl_field_maps.py +346 -0
- dataface/core/render/chart_interactivity.py +24 -0
- dataface/core/render/control_registry.py +287 -0
- dataface/core/render/converters/__init__.py +24 -0
- dataface/core/render/converters/chart.py +276 -0
- dataface/core/render/converters/html.py +98 -0
- dataface/core/render/converters/pdf.py +40 -0
- dataface/core/render/converters/png.py +41 -0
- dataface/core/render/errors.py +144 -0
- dataface/core/render/face_api.py +160 -0
- dataface/core/render/faces.py +1194 -0
- dataface/core/render/font_measurement.py +48 -0
- dataface/core/render/font_support.py +197 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/InterVariable.ttf +0 -0
- dataface/core/render/fonts/InterVariable.woff2 +0 -0
- dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
- dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
- dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
- dataface/core/render/fonts/_emoji_font_face.css +43 -0
- dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
- dataface/core/render/format_utils.py +329 -0
- dataface/core/render/geo_defaults.yml +28 -0
- dataface/core/render/json_format.py +146 -0
- dataface/core/render/layout_sizing.py +865 -0
- dataface/core/render/layouts.py +541 -0
- dataface/core/render/markdown_defaults.yml +16 -0
- dataface/core/render/missing_vars_prompt.py +79 -0
- dataface/core/render/placeholder.py +389 -0
- dataface/core/render/render_result.py +14 -0
- dataface/core/render/renderer.py +467 -0
- dataface/core/render/script_embedding.py +16 -0
- dataface/core/render/svg_utils.py +212 -0
- dataface/core/render/template_loader.py +69 -0
- dataface/core/render/templates/controls/_styles.css +606 -0
- dataface/core/render/templates/controls/checkbox.html +16 -0
- dataface/core/render/templates/controls/date.html +16 -0
- dataface/core/render/templates/controls/number.html +19 -0
- dataface/core/render/templates/controls/readonly.html +9 -0
- dataface/core/render/templates/controls/select.html +21 -0
- dataface/core/render/templates/controls/slider.html +22 -0
- dataface/core/render/templates/controls/text.html +16 -0
- dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
- dataface/core/render/templates/scripts/variables.js +976 -0
- dataface/core/render/templates/svg/grid_pattern.svg +3 -0
- dataface/core/render/templates/svg/styles.css +51 -0
- dataface/core/render/terminal.py +311 -0
- dataface/core/render/terminal_charts.py +563 -0
- dataface/core/render/terminal_defaults.yml +2 -0
- dataface/core/render/terminal_layouts.py +299 -0
- dataface/core/render/terminal_text.py +31 -0
- dataface/core/render/text/__init__.py +1 -0
- dataface/core/render/text/case.py +113 -0
- dataface/core/render/text_format.py +129 -0
- dataface/core/render/utils.py +106 -0
- dataface/core/render/variable_controls.py +946 -0
- dataface/core/render/variable_input_refinement.py +140 -0
- dataface/core/render/warnings/__init__.py +15 -0
- dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
- dataface/core/render/warnings/base.py +44 -0
- dataface/core/render/warnings/fanout_risk.py +15 -0
- dataface/core/render/warnings/from_query_diagnostic.py +56 -0
- dataface/core/render/warnings/missing_join_predicate.py +13 -0
- dataface/core/render/warnings/query_parse_error.py +14 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
- dataface/core/render/warnings/reaggregation.py +14 -0
- dataface/core/render/warnings/registry.py +45 -0
- dataface/core/render/warnings/suppression.py +46 -0
- dataface/core/render/warnings/temporal_single_point.py +63 -0
- dataface/core/render/warnings/unreferenced_chart.py +15 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
- dataface/core/render/yaml_format.py +167 -0
- dataface/core/resolve_face.py +195 -0
- dataface/core/schema/__init__.py +0 -0
- dataface/core/schema/guidance.py +151 -0
- dataface/core/scoped_paths.py +59 -0
- dataface/core/serve/__init__.py +14 -0
- dataface/core/serve/bootstrap.py +39 -0
- dataface/core/serve/embedded.py +57 -0
- dataface/core/serve/port.py +129 -0
- dataface/core/serve/server.py +938 -0
- dataface/core/serve/templates/__init__.py +0 -0
- dataface/core/serve/templates/directory.yml +6 -0
- dataface/core/serve/templates/error.html.j2 +217 -0
- dataface/core/utils.py +121 -0
- dataface/core/validate.py +64 -0
- dataface/integrations/__init__.py +0 -0
- dataface/integrations/highlighting.py +351 -0
- dataface/integrations/markdown.py +537 -0
- dataface/py.typed +0 -0
- dataface-0.1.2.dist-info/METADATA +375 -0
- dataface-0.1.2.dist-info/RECORD +455 -0
- dataface-0.1.2.dist-info/WHEEL +4 -0
- dataface-0.1.2.dist-info/entry_points.txt +2 -0
- dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
- mdsvg/__init__.py +168 -0
- mdsvg/fonts.py +656 -0
- mdsvg/images.py +299 -0
- mdsvg/parser.py +629 -0
- mdsvg/playground.py +284 -0
- mdsvg/py.typed +2 -0
- mdsvg/renderer.py +1623 -0
- mdsvg/style.py +355 -0
- mdsvg/types.py +200 -0
- mdsvg/utils.py +86 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Session auto-save and resume for dft chat.
|
|
2
|
+
|
|
3
|
+
Storage layout (mirrors Codex CLI's date-partitioned shape):
|
|
4
|
+
|
|
5
|
+
~/.dft/sessions/
|
|
6
|
+
2026/04/28/
|
|
7
|
+
rollout-20260428142133-7e3b9f.jsonl
|
|
8
|
+
index.json # cwd → list of SessionMeta; rebuild-able from JSONL files
|
|
9
|
+
|
|
10
|
+
JSONL schema (one JSON object per line):
|
|
11
|
+
|
|
12
|
+
{"type":"meta","session_id":"...","cwd":"...","provider":"...","model":"...","started_at":"..."}
|
|
13
|
+
|
|
14
|
+
After each user prompt:
|
|
15
|
+
{"type":"turn","messages":[
|
|
16
|
+
{"role":"user","content":"..."},
|
|
17
|
+
{"role":"assistant","content":"...","tool_calls":[...]},
|
|
18
|
+
{"role":"tool","tool_call_id":"...","name":"...","content":"..."},
|
|
19
|
+
...
|
|
20
|
+
]}
|
|
21
|
+
|
|
22
|
+
The ``turn`` event holds the exact message dicts that run_agent appended to the
|
|
23
|
+
conversation for one prompt/response cycle. Replay is trivial: concatenate all
|
|
24
|
+
turn["messages"] arrays in order.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
import uuid
|
|
32
|
+
from collections.abc import Iterator
|
|
33
|
+
from datetime import datetime, timezone
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from dataface.agent_api._state import dft_home, dft_state_path
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _now_iso() -> str:
|
|
43
|
+
return datetime.now(tz=timezone.utc).astimezone().isoformat()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _session_filename(dt: datetime, short_id: str) -> str:
|
|
47
|
+
"""Build the filename from a parsed datetime + short id."""
|
|
48
|
+
return f"rollout-{dt.strftime('%Y%m%d%H%M%S')}-{short_id}.jsonl"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SessionWriter:
|
|
52
|
+
"""Append-only JSONL session writer.
|
|
53
|
+
|
|
54
|
+
Writes a ``meta`` line first, then one ``turn`` JSON line per prompt/response
|
|
55
|
+
cycle. Every write flushes so a kill -9 loses at most the current in-flight
|
|
56
|
+
turn, not the whole transcript.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
session_id: str,
|
|
62
|
+
cwd: Path,
|
|
63
|
+
*,
|
|
64
|
+
provider: str,
|
|
65
|
+
model: str = "",
|
|
66
|
+
started_at: str | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
self.session_id = session_id
|
|
69
|
+
self.cwd = cwd
|
|
70
|
+
self.provider = provider
|
|
71
|
+
self.model = model
|
|
72
|
+
self.started_at = started_at or _now_iso()
|
|
73
|
+
|
|
74
|
+
# Derive storage path from the started_at timestamp
|
|
75
|
+
dt = datetime.fromisoformat(self.started_at)
|
|
76
|
+
yyyy = f"{dt.year:04d}"
|
|
77
|
+
mm = f"{dt.month:02d}"
|
|
78
|
+
dd = f"{dt.day:02d}"
|
|
79
|
+
fname = _session_filename(dt, session_id[:6])
|
|
80
|
+
self.path: Path = dft_state_path("sessions", yyyy, mm, dd, fname)
|
|
81
|
+
|
|
82
|
+
# Open for append; write meta line immediately.
|
|
83
|
+
self._fh = self.path.open("a")
|
|
84
|
+
meta: dict[str, Any] = {
|
|
85
|
+
"type": "meta",
|
|
86
|
+
"session_id": session_id,
|
|
87
|
+
"cwd": str(cwd),
|
|
88
|
+
"provider": provider,
|
|
89
|
+
"model": model,
|
|
90
|
+
"started_at": self.started_at,
|
|
91
|
+
}
|
|
92
|
+
self._fh.write(json.dumps(meta) + "\n")
|
|
93
|
+
self._fh.flush()
|
|
94
|
+
|
|
95
|
+
def write_turn(self, new_messages: list[dict[str, Any]]) -> None:
|
|
96
|
+
"""Persist the messages appended during one prompt/response cycle."""
|
|
97
|
+
self._fh.write(
|
|
98
|
+
json.dumps({"type": "turn", "messages": new_messages}, default=str) + "\n"
|
|
99
|
+
)
|
|
100
|
+
self._fh.flush()
|
|
101
|
+
|
|
102
|
+
def close(self) -> None:
|
|
103
|
+
self._fh.close()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class SessionReader:
|
|
107
|
+
"""Iterate events from a JSONL session file."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, path: Path) -> None:
|
|
110
|
+
self.path = path
|
|
111
|
+
|
|
112
|
+
def __iter__(self) -> Iterator[dict[str, Any]]:
|
|
113
|
+
with self.path.open() as f:
|
|
114
|
+
for line in f:
|
|
115
|
+
line = line.strip()
|
|
116
|
+
if not line:
|
|
117
|
+
continue
|
|
118
|
+
try:
|
|
119
|
+
yield json.loads(line)
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
logger.warning("Skipping malformed JSONL line in %s", self.path)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Index
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
SessionMeta = dict[str, Any]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class SessionIndex:
|
|
132
|
+
"""Persistent index mapping cwd → list of SessionMeta.
|
|
133
|
+
|
|
134
|
+
The index is stored as ~/.dft/sessions/index.json.
|
|
135
|
+
If the file is missing or malformed the index is rebuilt by scanning
|
|
136
|
+
all JSONL files under ~/.dft/sessions/ — it is a cache, not a source of truth.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self) -> None:
|
|
140
|
+
self._path = dft_home() / "sessions" / "index.json"
|
|
141
|
+
self._data: dict[str, list[SessionMeta]] = self._load()
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
# Public API
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def record(
|
|
148
|
+
self,
|
|
149
|
+
session_id: str,
|
|
150
|
+
cwd: Path,
|
|
151
|
+
started_at: str,
|
|
152
|
+
path: Path,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Add or update a session entry and persist the index."""
|
|
155
|
+
cwd_str = str(cwd)
|
|
156
|
+
entry: SessionMeta = {
|
|
157
|
+
"session_id": session_id,
|
|
158
|
+
"cwd": cwd_str,
|
|
159
|
+
"started_at": started_at,
|
|
160
|
+
"path": str(path),
|
|
161
|
+
}
|
|
162
|
+
bucket = self._data.setdefault(cwd_str, [])
|
|
163
|
+
# Replace if already present (same id), else append.
|
|
164
|
+
for i, existing in enumerate(bucket):
|
|
165
|
+
if existing["session_id"] == session_id:
|
|
166
|
+
bucket[i] = entry
|
|
167
|
+
self._save()
|
|
168
|
+
return
|
|
169
|
+
bucket.append(entry)
|
|
170
|
+
self._save()
|
|
171
|
+
|
|
172
|
+
def list_for_cwd(self, cwd: Path) -> list[SessionMeta]:
|
|
173
|
+
"""Return all session metadata entries for *cwd*, newest first."""
|
|
174
|
+
entries = list(self._data.get(str(cwd), []))
|
|
175
|
+
entries.sort(key=lambda e: e.get("started_at", ""), reverse=True)
|
|
176
|
+
return entries
|
|
177
|
+
|
|
178
|
+
def list_all(self) -> list[SessionMeta]:
|
|
179
|
+
"""Return all session metadata entries across all cwds, newest first."""
|
|
180
|
+
all_entries: list[SessionMeta] = []
|
|
181
|
+
for entries in self._data.values():
|
|
182
|
+
all_entries.extend(entries)
|
|
183
|
+
all_entries.sort(key=lambda e: e.get("started_at", ""), reverse=True)
|
|
184
|
+
return all_entries
|
|
185
|
+
|
|
186
|
+
def find(self, session_id: str) -> SessionMeta | None:
|
|
187
|
+
"""Find a session by id, searching across all cwds."""
|
|
188
|
+
for entries in self._data.values():
|
|
189
|
+
for entry in entries:
|
|
190
|
+
if entry["session_id"] == session_id:
|
|
191
|
+
return entry
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
# ------------------------------------------------------------------
|
|
195
|
+
# Persistence helpers
|
|
196
|
+
# ------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def _load(self) -> dict[str, list[SessionMeta]]:
|
|
199
|
+
if self._path.exists():
|
|
200
|
+
try:
|
|
201
|
+
raw = json.loads(self._path.read_text())
|
|
202
|
+
if isinstance(raw, dict):
|
|
203
|
+
return raw
|
|
204
|
+
logger.warning("sessions/index.json has unexpected shape; rebuilding")
|
|
205
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
206
|
+
logger.warning("sessions/index.json unreadable (%s); rebuilding", exc)
|
|
207
|
+
return self._rebuild()
|
|
208
|
+
|
|
209
|
+
def _save(self) -> None:
|
|
210
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
self._path.write_text(json.dumps(self._data, indent=2))
|
|
212
|
+
|
|
213
|
+
def _rebuild(self) -> dict[str, list[SessionMeta]]:
|
|
214
|
+
"""Scan all JSONL files under ~/.dft/sessions/ and reconstruct the index."""
|
|
215
|
+
sessions_root = dft_home() / "sessions"
|
|
216
|
+
data: dict[str, list[SessionMeta]] = {}
|
|
217
|
+
if not sessions_root.exists():
|
|
218
|
+
return data
|
|
219
|
+
for jsonl_file in sessions_root.rglob("*.jsonl"):
|
|
220
|
+
meta = _read_meta_line(jsonl_file)
|
|
221
|
+
if meta is None:
|
|
222
|
+
continue
|
|
223
|
+
cwd = meta.get("cwd", "")
|
|
224
|
+
session_id = meta.get("session_id", "")
|
|
225
|
+
started_at = meta.get("started_at", "")
|
|
226
|
+
if not (cwd and session_id):
|
|
227
|
+
continue
|
|
228
|
+
entry: SessionMeta = {
|
|
229
|
+
"session_id": session_id,
|
|
230
|
+
"cwd": cwd,
|
|
231
|
+
"started_at": started_at,
|
|
232
|
+
"path": str(jsonl_file.relative_to(dft_home())),
|
|
233
|
+
}
|
|
234
|
+
data.setdefault(cwd, []).append(entry)
|
|
235
|
+
# Persist the rebuilt index
|
|
236
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
self._path.write_text(json.dumps(data, indent=2))
|
|
238
|
+
return data
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _read_meta_line(path: Path) -> dict[str, Any] | None:
|
|
242
|
+
"""Read and return the first line of a JSONL file as a dict, or None."""
|
|
243
|
+
try:
|
|
244
|
+
with path.open() as f:
|
|
245
|
+
first = f.readline().strip()
|
|
246
|
+
if not first:
|
|
247
|
+
return None
|
|
248
|
+
obj = json.loads(first)
|
|
249
|
+
return obj if isinstance(obj, dict) and obj.get("type") == "meta" else None
|
|
250
|
+
except (OSError, json.JSONDecodeError):
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
# Replay
|
|
256
|
+
# ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _estimate_tokens(messages: list[dict[str, Any]]) -> int:
|
|
260
|
+
"""Rough token estimate via JSON-bytes / 4. No tokenizer dep.
|
|
261
|
+
|
|
262
|
+
The 4-bytes-per-token heuristic is pessimistic enough for English-heavy
|
|
263
|
+
LLM messages; the safety margin baked into trim callers covers the noise.
|
|
264
|
+
"""
|
|
265
|
+
if not messages:
|
|
266
|
+
return 0
|
|
267
|
+
return sum(len(json.dumps(m, default=str)) for m in messages) // 4
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _split_into_units(messages: list[dict[str, Any]]) -> list[list[dict[str, Any]]]:
|
|
271
|
+
"""Group messages into atomic units that must drop or stay together.
|
|
272
|
+
|
|
273
|
+
A unit is one of:
|
|
274
|
+
- A user turn (one message).
|
|
275
|
+
- An assistant turn without tool_calls (one message).
|
|
276
|
+
- An assistant turn WITH tool_calls + all following tool responses (1 + N).
|
|
277
|
+
|
|
278
|
+
Tool responses (`role == "tool"`) are never standalone — they belong to
|
|
279
|
+
the immediately preceding assistant turn that issued the tool_call.
|
|
280
|
+
Splitting on assistant boundaries preserves tool_call/tool_result pairs.
|
|
281
|
+
"""
|
|
282
|
+
units: list[list[dict[str, Any]]] = []
|
|
283
|
+
current: list[dict[str, Any]] = []
|
|
284
|
+
for msg in messages:
|
|
285
|
+
role = msg.get("role")
|
|
286
|
+
if role == "tool":
|
|
287
|
+
# A tool response must follow the assistant turn that issued
|
|
288
|
+
# the tool_call — otherwise the JSONL session is corrupt.
|
|
289
|
+
if not current:
|
|
290
|
+
raise ValueError(
|
|
291
|
+
"Session corrupt: tool response without preceding "
|
|
292
|
+
"assistant turn. The session JSONL file is malformed."
|
|
293
|
+
)
|
|
294
|
+
current.append(msg)
|
|
295
|
+
continue
|
|
296
|
+
# New user / assistant message starts a new unit.
|
|
297
|
+
if current:
|
|
298
|
+
units.append(current)
|
|
299
|
+
current = [msg]
|
|
300
|
+
if current:
|
|
301
|
+
units.append(current)
|
|
302
|
+
return units
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def trim_messages_to_context(
|
|
306
|
+
messages: list[dict[str, Any]],
|
|
307
|
+
*,
|
|
308
|
+
max_tokens: int,
|
|
309
|
+
) -> tuple[list[dict[str, Any]], int]:
|
|
310
|
+
"""Drop oldest message-units until the estimated token count fits.
|
|
311
|
+
|
|
312
|
+
Tool_call/tool_result pairs drop together so the LLM never sees an
|
|
313
|
+
orphan tool response (which would error or hallucinate).
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
messages: The replayed message list.
|
|
317
|
+
max_tokens: Target token ceiling (model context minus safety margin).
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
``(trimmed_messages, dropped_message_count)``.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
ValueError: If the most-recent unit alone exceeds *max_tokens*.
|
|
324
|
+
Resuming would corrupt the conversation; the caller should ask
|
|
325
|
+
the user to start a fresh session or shrink the prompt.
|
|
326
|
+
"""
|
|
327
|
+
if _estimate_tokens(messages) <= max_tokens and (
|
|
328
|
+
not messages or messages[0].get("role") == "user"
|
|
329
|
+
):
|
|
330
|
+
return messages, 0
|
|
331
|
+
|
|
332
|
+
units = _split_into_units(messages)
|
|
333
|
+
if not units:
|
|
334
|
+
return messages, 0
|
|
335
|
+
|
|
336
|
+
def _flat(us: list[list[dict[str, Any]]]) -> list[dict[str, Any]]:
|
|
337
|
+
return [m for unit in us for m in unit]
|
|
338
|
+
|
|
339
|
+
# Drop oldest units until we fit AND the resulting first message is a
|
|
340
|
+
# user turn — LLM APIs reject conversations that start with assistant
|
|
341
|
+
# or tool. If the most-recent unit alone can't satisfy both invariants,
|
|
342
|
+
# raise rather than corrupt the conversation.
|
|
343
|
+
dropped = 0
|
|
344
|
+
while units and (
|
|
345
|
+
_estimate_tokens(_flat(units)) > max_tokens or units[0][0].get("role") != "user"
|
|
346
|
+
):
|
|
347
|
+
if len(units) == 1:
|
|
348
|
+
raise ValueError(
|
|
349
|
+
f"Session too large to resume: the most-recent turn alone "
|
|
350
|
+
f"exceeds the {max_tokens}-token budget, or doesn't start "
|
|
351
|
+
f"with a user message. Start a fresh session or trim the inputs."
|
|
352
|
+
)
|
|
353
|
+
dropped += len(units[0])
|
|
354
|
+
units.pop(0)
|
|
355
|
+
|
|
356
|
+
return _flat(units), dropped
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def replay_into_messages(
|
|
360
|
+
reader: SessionReader,
|
|
361
|
+
*,
|
|
362
|
+
expected_provider: str,
|
|
363
|
+
) -> list[dict[str, Any]]:
|
|
364
|
+
"""Convert a JSONL session stream back into the LLM messages list.
|
|
365
|
+
|
|
366
|
+
Each ``turn`` event contains the exact message dicts that run_agent appended
|
|
367
|
+
during one prompt/response cycle. Replay concatenates them in order.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
reader: SessionReader over a session JSONL file.
|
|
371
|
+
expected_provider: The provider the current client uses. Raises
|
|
372
|
+
ValueError if it doesn't match the session's recorded provider.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
List of ``{"role": ..., "content": ...}`` dicts suitable for
|
|
376
|
+
passing to ``run_agent(..., messages=...)``.
|
|
377
|
+
"""
|
|
378
|
+
messages: list[dict[str, Any]] = []
|
|
379
|
+
meta_seen = False
|
|
380
|
+
|
|
381
|
+
for event in reader:
|
|
382
|
+
event_type = event.get("type", "")
|
|
383
|
+
|
|
384
|
+
if event_type == "meta":
|
|
385
|
+
if not meta_seen:
|
|
386
|
+
meta_seen = True
|
|
387
|
+
recorded_provider = event.get("provider", "")
|
|
388
|
+
if recorded_provider and recorded_provider != expected_provider:
|
|
389
|
+
raise ValueError(
|
|
390
|
+
f"Cannot resume session {event.get('session_id', '?')}: "
|
|
391
|
+
f"it was started with {recorded_provider}; "
|
|
392
|
+
f"current default is {expected_provider}. "
|
|
393
|
+
f"Run with --model {recorded_provider}:<model> to resume."
|
|
394
|
+
)
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
elif event_type == "turn":
|
|
398
|
+
turn_messages = event.get("messages", [])
|
|
399
|
+
if isinstance(turn_messages, list):
|
|
400
|
+
messages.extend(turn_messages)
|
|
401
|
+
else:
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"Unknown JSONL event type {event_type!r} in session file. "
|
|
404
|
+
"The session file may be from a newer version of dft."
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
return messages
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# Load helpers used by the CLI
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def new_session(
|
|
416
|
+
cwd: Path,
|
|
417
|
+
*,
|
|
418
|
+
provider: str,
|
|
419
|
+
model: str = "",
|
|
420
|
+
) -> tuple[SessionWriter, SessionIndex]:
|
|
421
|
+
"""Create a new session writer and register it in the index.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
(writer, index)
|
|
425
|
+
"""
|
|
426
|
+
session_id = uuid.uuid4().hex
|
|
427
|
+
started_at = _now_iso()
|
|
428
|
+
writer = SessionWriter(
|
|
429
|
+
session_id=session_id,
|
|
430
|
+
cwd=cwd,
|
|
431
|
+
provider=provider,
|
|
432
|
+
model=model,
|
|
433
|
+
started_at=started_at,
|
|
434
|
+
)
|
|
435
|
+
index = SessionIndex()
|
|
436
|
+
index.record(
|
|
437
|
+
session_id=session_id,
|
|
438
|
+
cwd=cwd,
|
|
439
|
+
started_at=started_at,
|
|
440
|
+
path=writer.path.relative_to(dft_home()),
|
|
441
|
+
)
|
|
442
|
+
return writer, index
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def load_session_for_resume(
|
|
446
|
+
session_id: str,
|
|
447
|
+
*,
|
|
448
|
+
expected_provider: str,
|
|
449
|
+
max_tokens: int | None = None,
|
|
450
|
+
) -> tuple[list[dict[str, Any]], SessionMeta, int]:
|
|
451
|
+
"""Load and replay a session by id, optionally trimming to fit context.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
session_id: The session id to load.
|
|
455
|
+
expected_provider: Provider to validate against.
|
|
456
|
+
max_tokens: When set, oldest tool_call/tool_result-preserving units
|
|
457
|
+
are dropped until the message count fits. ``None`` skips trim.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
``(messages, meta, dropped_count)`` — replayed messages, the session
|
|
461
|
+
meta dict, and the number of messages dropped to fit ``max_tokens``
|
|
462
|
+
(0 when no trim was needed or requested).
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
ValueError: If session not found, cross-provider mismatch, or the
|
|
466
|
+
most-recent unit alone exceeds ``max_tokens``.
|
|
467
|
+
"""
|
|
468
|
+
index = SessionIndex()
|
|
469
|
+
meta = index.find(session_id)
|
|
470
|
+
if meta is None:
|
|
471
|
+
raise ValueError(f"No session found with id {session_id}")
|
|
472
|
+
|
|
473
|
+
session_path_str = meta.get("path", "")
|
|
474
|
+
if not session_path_str:
|
|
475
|
+
raise ValueError(f"No session found with id {session_id}")
|
|
476
|
+
|
|
477
|
+
session_path = dft_home() / session_path_str
|
|
478
|
+
if not session_path.exists():
|
|
479
|
+
raise ValueError(f"No session found with id {session_id}")
|
|
480
|
+
|
|
481
|
+
reader = SessionReader(session_path)
|
|
482
|
+
messages = replay_into_messages(reader, expected_provider=expected_provider)
|
|
483
|
+
dropped = 0
|
|
484
|
+
if max_tokens is not None:
|
|
485
|
+
messages, dropped = trim_messages_to_context(messages, max_tokens=max_tokens)
|
|
486
|
+
return messages, meta, dropped
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""On-disk state directory for dft.
|
|
2
|
+
|
|
3
|
+
Default: ~/.dft/
|
|
4
|
+
Override via DFT_HOME (unconditional) or XDG_CONFIG_HOME (maps to <XDG>/dft).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def dft_home() -> Path:
|
|
14
|
+
if dft := os.environ.get("DFT_HOME"):
|
|
15
|
+
return Path(dft)
|
|
16
|
+
if xdg := os.environ.get("XDG_CONFIG_HOME"):
|
|
17
|
+
return Path(xdg) / "dft"
|
|
18
|
+
return Path.home() / ".dft"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def dft_state_path(*parts: str) -> Path:
|
|
22
|
+
if not parts:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
"dft_state_path requires at least one path component; call dft_home() for the bare home dir"
|
|
25
|
+
)
|
|
26
|
+
path = dft_home().joinpath(*parts)
|
|
27
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
return path
|