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,645 @@
|
|
|
1
|
+
"""Terminal chat CLI (`dft chat`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import signal
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from contextlib import AbstractContextManager, nullcontext
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from dataface.ai.external_mcp import ExternalMCPManager
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.syntax import Syntax
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
|
|
27
|
+
from dataface.agent_api import chat as _api
|
|
28
|
+
from dataface.agent_api._state import dft_home
|
|
29
|
+
from dataface.agent_api.file_refs import expand_file_refs
|
|
30
|
+
from dataface.ai.context import DatafaceAIContext
|
|
31
|
+
from dataface.ai.events import (
|
|
32
|
+
AgentDone,
|
|
33
|
+
AgentError,
|
|
34
|
+
ContentDelta,
|
|
35
|
+
ThinkingStatus,
|
|
36
|
+
ToolCallEvent,
|
|
37
|
+
ToolResultEvent,
|
|
38
|
+
)
|
|
39
|
+
from dataface.ai.llm import create_client, infer_provider
|
|
40
|
+
from dataface.cli.commands._agent_input import (
|
|
41
|
+
PromptToolkitInput,
|
|
42
|
+
select_input_layer,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Max JSON lines before we truncate and write the full result to a tmpfile.
|
|
46
|
+
_JSON_TRUNCATE_LINES = 80
|
|
47
|
+
|
|
48
|
+
# Completed stream status — used by _run_agent_session callers.
|
|
49
|
+
_SessionStatus = Literal["done", "interrupted", "errored"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Interrupt sentinel — BaseException so it bypasses 'except Exception' in tools
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _AgentInterrupt(BaseException):
|
|
58
|
+
"""Raised by the SIGINT handler to cancel an in-flight LLM/tool call."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Session dataclass (CLI REPL state — holds console, not persisted)
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Session:
|
|
68
|
+
"""Mutable REPL state for one dft chat session."""
|
|
69
|
+
|
|
70
|
+
client: Any
|
|
71
|
+
context: DatafaceAIContext
|
|
72
|
+
console: Console
|
|
73
|
+
messages: list[dict[str, Any]] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Slash-command helpers
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _cmd_exit(session: Session, args: list[str]) -> None:
|
|
82
|
+
session.console.print("[dim]Session ended.[/dim]")
|
|
83
|
+
raise SystemExit(0)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _cmd_clear(session: Session, args: list[str]) -> None:
|
|
87
|
+
session.messages.clear()
|
|
88
|
+
# Rebuild the client to reset any provider-side conversation state
|
|
89
|
+
# (e.g. OpenAIClient._previous_response_id / _sent_message_count).
|
|
90
|
+
# Without this, OpenAI's incremental mode would compute an empty input
|
|
91
|
+
# slice on the next prompt and silently return nothing.
|
|
92
|
+
session.client = create_client(model=session.client.model)
|
|
93
|
+
session.console.print("[dim]Conversation history cleared.[/dim]")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cmd_model(session: Session, args: list[str]) -> None:
|
|
97
|
+
if not args:
|
|
98
|
+
session.console.print(
|
|
99
|
+
f"[dim]Current model: {session.client.model} "
|
|
100
|
+
f"(provider: {session.client.provider})[/dim]"
|
|
101
|
+
)
|
|
102
|
+
return
|
|
103
|
+
requested_model = args[0]
|
|
104
|
+
requested_provider = infer_provider(requested_model)
|
|
105
|
+
if requested_provider != session.client.provider:
|
|
106
|
+
session.console.print(
|
|
107
|
+
f"[red]/model: cannot switch providers mid-session "
|
|
108
|
+
f"(current: {session.client.provider}, requested: {requested_provider}). "
|
|
109
|
+
f"Restart dft chat with --model {requested_model}.[/red]"
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
new_client = create_client(model=requested_model)
|
|
113
|
+
session.client = new_client
|
|
114
|
+
# Clear messages: the new client has no previous_response_id chain,
|
|
115
|
+
# so prior tool-call history would cause LLMClientError on OpenAI.
|
|
116
|
+
session.messages.clear()
|
|
117
|
+
session.console.print(
|
|
118
|
+
f"[dim]Switched to model: {new_client.model} (history cleared)[/dim]"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _cmd_help(session: Session, args: list[str]) -> None:
|
|
123
|
+
lines = [
|
|
124
|
+
"[bold]/help[/bold] — show this help",
|
|
125
|
+
"[bold]/clear[/bold] — drop conversation history (keep session alive)",
|
|
126
|
+
"[bold]/model[/bold] [name] — show current model or switch to another (same provider only)",
|
|
127
|
+
"[bold]/exit[/bold] — end the session",
|
|
128
|
+
"[bold]/quit[/bold] — alias for /exit",
|
|
129
|
+
"",
|
|
130
|
+
"Ctrl+C — cancel in-flight request; second Ctrl+C within 1s exits",
|
|
131
|
+
"Ctrl+D — exit session",
|
|
132
|
+
]
|
|
133
|
+
session.console.print("\n".join(lines))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
SLASH_COMMANDS: dict[str, Callable[[Session, list[str]], None]] = {
|
|
137
|
+
"exit": _cmd_exit,
|
|
138
|
+
"quit": _cmd_exit,
|
|
139
|
+
"clear": _cmd_clear,
|
|
140
|
+
"model": _cmd_model,
|
|
141
|
+
"help": _cmd_help,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _dispatch_slash(session: Session, user_input: str) -> bool:
|
|
146
|
+
"""Try to dispatch a slash command.
|
|
147
|
+
|
|
148
|
+
Returns True if the input was consumed as a slash command, False if the
|
|
149
|
+
input should be sent to the model.
|
|
150
|
+
|
|
151
|
+
Unknown slash commands pass through to the model verbatim (per spec: "no
|
|
152
|
+
magic").
|
|
153
|
+
"""
|
|
154
|
+
stripped = user_input.strip()
|
|
155
|
+
if not stripped.startswith("/"):
|
|
156
|
+
return False
|
|
157
|
+
parts = stripped[1:].split()
|
|
158
|
+
if not parts:
|
|
159
|
+
return False
|
|
160
|
+
cmd_name = parts[0].lower()
|
|
161
|
+
cmd_args = parts[1:]
|
|
162
|
+
handler = SLASH_COMMANDS.get(cmd_name)
|
|
163
|
+
if handler is None:
|
|
164
|
+
return False
|
|
165
|
+
handler(session, cmd_args)
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# Ctrl+C double-tap-exit helpers
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _check_double_tap_exit(last_sigint_ts: list[float]) -> bool:
|
|
175
|
+
"""Record an interrupt timestamp and return True if it's a double-tap.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
last_sigint_ts: A mutable list used as a single-slot timestamp store.
|
|
179
|
+
Pass an empty list on first call; the function appends/updates it.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if a second interrupt arrived within 1s of the previous one.
|
|
183
|
+
"""
|
|
184
|
+
now = time.monotonic()
|
|
185
|
+
if last_sigint_ts and (now - last_sigint_ts[0]) < 1.0:
|
|
186
|
+
return True
|
|
187
|
+
if last_sigint_ts:
|
|
188
|
+
last_sigint_ts[0] = now
|
|
189
|
+
else:
|
|
190
|
+
last_sigint_ts.append(now)
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Tool-result rendering
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _render_tool_result(name: str, result: Any, console: Console, tmpdir: Path) -> None:
|
|
200
|
+
"""Render a tool result to the console.
|
|
201
|
+
|
|
202
|
+
Dispatch:
|
|
203
|
+
- Tabular execute_query result → rich.Table
|
|
204
|
+
- Error dict → red Panel
|
|
205
|
+
- Oversized JSON → truncated Panel with tmpfile reference
|
|
206
|
+
- Default → JSON Syntax Panel
|
|
207
|
+
|
|
208
|
+
tmpdir is a session-scoped directory for oversized-result files; the
|
|
209
|
+
caller cleans it up at session end.
|
|
210
|
+
"""
|
|
211
|
+
# Tabular: execute_query returns {success, data: list[dict], columns, ...}
|
|
212
|
+
if (
|
|
213
|
+
isinstance(result, dict)
|
|
214
|
+
and isinstance(result.get("data"), list)
|
|
215
|
+
and isinstance(result.get("columns"), list)
|
|
216
|
+
and result.get("columns")
|
|
217
|
+
and not result.get("error")
|
|
218
|
+
):
|
|
219
|
+
_render_tabular(name, result, console)
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# Error dict
|
|
223
|
+
if isinstance(result, dict) and result.get("error"):
|
|
224
|
+
console.print(
|
|
225
|
+
Panel(
|
|
226
|
+
f"[red]{result['error']}[/red]",
|
|
227
|
+
title=f"Tool Error: {name}",
|
|
228
|
+
border_style="red",
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
# Default: JSON, possibly truncated
|
|
234
|
+
body = json.dumps(result, indent=2, default=str, sort_keys=True)
|
|
235
|
+
lines = body.splitlines()
|
|
236
|
+
if len(lines) > _JSON_TRUNCATE_LINES:
|
|
237
|
+
truncated_body = "\n".join(lines[:_JSON_TRUNCATE_LINES])
|
|
238
|
+
extra = len(lines) - _JSON_TRUNCATE_LINES
|
|
239
|
+
# Write the full result under the session tmpdir so it's cleaned up on exit.
|
|
240
|
+
# Sanitize name to avoid path separators from LLM-provided tool names.
|
|
241
|
+
safe_name = re.sub(r"[^\w-]", "_", name)
|
|
242
|
+
tmp_file = tmpdir / f"dft_{safe_name}_{int(time.monotonic() * 1000)}.json"
|
|
243
|
+
tmp_file.write_text(body)
|
|
244
|
+
summary = f"[dim]... ({extra} more lines, full result in {tmp_file})[/dim]"
|
|
245
|
+
console.print(
|
|
246
|
+
Panel(
|
|
247
|
+
Syntax(truncated_body, "json", word_wrap=True),
|
|
248
|
+
title=f"Tool Result: {name}",
|
|
249
|
+
border_style="blue",
|
|
250
|
+
subtitle=summary,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
console.print(
|
|
255
|
+
Panel(
|
|
256
|
+
Syntax(body, "json", word_wrap=True),
|
|
257
|
+
title=f"Tool Result: {name}",
|
|
258
|
+
border_style="blue",
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _render_tabular(name: str, result: dict[str, Any], console: Console) -> None:
|
|
264
|
+
columns: list[str] = result["columns"]
|
|
265
|
+
data: list[dict[str, Any]] = result["data"]
|
|
266
|
+
|
|
267
|
+
table = Table(title=f"Tool Result: {name}", border_style="blue", show_lines=False)
|
|
268
|
+
for col in columns:
|
|
269
|
+
table.add_column(col, overflow="fold")
|
|
270
|
+
for row in data:
|
|
271
|
+
table.add_row(*[str(row.get(col, "")) for col in columns])
|
|
272
|
+
|
|
273
|
+
console.print(table)
|
|
274
|
+
if result.get("truncated"):
|
|
275
|
+
console.print(f"[dim](results truncated to {len(data)} rows)[/dim]")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Agent stream loop — extracted so tests can call it directly
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _run_agent_session(
|
|
284
|
+
chat_session: _api.ChatSession,
|
|
285
|
+
prompt: str,
|
|
286
|
+
console: Console,
|
|
287
|
+
tmpdir: Path,
|
|
288
|
+
tools: list[dict[str, Any]] | None = None,
|
|
289
|
+
external_manager: ExternalMCPManager | None = None,
|
|
290
|
+
) -> _SessionStatus:
|
|
291
|
+
"""Stream one prompt's events through ``agent_api.chat.send_message``.
|
|
292
|
+
|
|
293
|
+
Installs a SIGINT handler for the duration that raises _AgentInterrupt.
|
|
294
|
+
Persistence is handled inside ``send_message`` (its ``try/finally`` writes
|
|
295
|
+
the turn to disk even when the generator is closed mid-stream).
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
tools: Merged tool list (built-in + external). Defaults to ALL_TOOLS.
|
|
299
|
+
external_manager: ExternalMCPManager for routing 'server__tool' calls.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
"done" — completed normally
|
|
303
|
+
"interrupted" — cancelled by Ctrl+C
|
|
304
|
+
"errored" — AgentError received (message already printed in red)
|
|
305
|
+
"""
|
|
306
|
+
started_content = False
|
|
307
|
+
status: _SessionStatus = "done"
|
|
308
|
+
|
|
309
|
+
def _sigint_handler(_signum: int, _frame: Any) -> None:
|
|
310
|
+
raise _AgentInterrupt()
|
|
311
|
+
|
|
312
|
+
prev_handler = signal.signal(signal.SIGINT, _sigint_handler)
|
|
313
|
+
|
|
314
|
+
gen = _api.send_message(
|
|
315
|
+
chat_session,
|
|
316
|
+
prompt,
|
|
317
|
+
tools=tools,
|
|
318
|
+
external_manager=external_manager,
|
|
319
|
+
)
|
|
320
|
+
try:
|
|
321
|
+
for event in gen:
|
|
322
|
+
match event:
|
|
323
|
+
case ThinkingStatus(status=thk_status):
|
|
324
|
+
if started_content:
|
|
325
|
+
console.print()
|
|
326
|
+
started_content = False
|
|
327
|
+
console.print(f"[dim]{thk_status}[/dim]")
|
|
328
|
+
case ToolCallEvent(name=name, arguments=args):
|
|
329
|
+
if started_content:
|
|
330
|
+
console.print()
|
|
331
|
+
started_content = False
|
|
332
|
+
console.print(
|
|
333
|
+
f"[bold cyan]Tool[/bold cyan] {name} "
|
|
334
|
+
f"[dim]{json.dumps(args, default=str)}[/dim]"
|
|
335
|
+
)
|
|
336
|
+
case ToolResultEvent(name=name, result=result):
|
|
337
|
+
if started_content:
|
|
338
|
+
console.print()
|
|
339
|
+
started_content = False
|
|
340
|
+
_render_tool_result(name, result, console, tmpdir)
|
|
341
|
+
case ContentDelta(delta=delta):
|
|
342
|
+
if not started_content:
|
|
343
|
+
console.print("[bold green]Assistant[/bold green]", end=" ")
|
|
344
|
+
started_content = True
|
|
345
|
+
console.out(delta, end="")
|
|
346
|
+
case AgentError(message=msg):
|
|
347
|
+
if started_content:
|
|
348
|
+
console.print()
|
|
349
|
+
started_content = False
|
|
350
|
+
console.print(f"[red]{msg}[/red]")
|
|
351
|
+
status = "errored"
|
|
352
|
+
return status
|
|
353
|
+
case AgentDone():
|
|
354
|
+
pass
|
|
355
|
+
except _AgentInterrupt:
|
|
356
|
+
status = "interrupted"
|
|
357
|
+
gen.close() # closes HTTP connection via generator protocol
|
|
358
|
+
finally:
|
|
359
|
+
signal.signal(signal.SIGINT, prev_handler)
|
|
360
|
+
if started_content:
|
|
361
|
+
console.print()
|
|
362
|
+
|
|
363
|
+
if status == "interrupted":
|
|
364
|
+
console.print("[dim]interrupted[/dim]")
|
|
365
|
+
|
|
366
|
+
return status
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ---------------------------------------------------------------------------
|
|
370
|
+
# Resume helpers
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _pick_session_interactive(console: Console) -> str | None:
|
|
375
|
+
"""Show a rich.Table of recent sessions and prompt for a selection.
|
|
376
|
+
|
|
377
|
+
Returns the selected session_id, or None if the user aborts.
|
|
378
|
+
"""
|
|
379
|
+
summaries = _api.list_sessions()
|
|
380
|
+
recent = summaries[:10]
|
|
381
|
+
|
|
382
|
+
if not recent:
|
|
383
|
+
console.print("[dim]No saved sessions found.[/dim]")
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
table = Table(title="Recent sessions", border_style="dim", show_lines=False)
|
|
387
|
+
table.add_column("#", style="dim", width=3)
|
|
388
|
+
table.add_column("Started", style="cyan")
|
|
389
|
+
table.add_column("CWD", style="green")
|
|
390
|
+
table.add_column("ID", style="dim")
|
|
391
|
+
for i, entry in enumerate(recent, 1):
|
|
392
|
+
table.add_row(
|
|
393
|
+
str(i),
|
|
394
|
+
entry.started_at[:19],
|
|
395
|
+
str(entry.cwd),
|
|
396
|
+
entry.session_id[:8],
|
|
397
|
+
)
|
|
398
|
+
console.print(table)
|
|
399
|
+
|
|
400
|
+
raw = typer.prompt("Select session number (Enter to cancel)", default="")
|
|
401
|
+
if not raw.strip():
|
|
402
|
+
return None
|
|
403
|
+
try:
|
|
404
|
+
idx_choice = int(raw.strip()) - 1
|
|
405
|
+
if not (0 <= idx_choice < len(recent)):
|
|
406
|
+
raise ValueError
|
|
407
|
+
return recent[idx_choice].session_id
|
|
408
|
+
except ValueError:
|
|
409
|
+
console.print("[red]Invalid selection.[/red]")
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _resolve_resume_id(
|
|
414
|
+
continue_session: bool,
|
|
415
|
+
resume: str | None,
|
|
416
|
+
pick: bool,
|
|
417
|
+
cwd: Path,
|
|
418
|
+
console: Console,
|
|
419
|
+
) -> str | None:
|
|
420
|
+
"""Return the session_id to resume, or None for a fresh session."""
|
|
421
|
+
if resume is not None:
|
|
422
|
+
return resume.strip()
|
|
423
|
+
if pick:
|
|
424
|
+
return _pick_session_interactive(console)
|
|
425
|
+
if continue_session:
|
|
426
|
+
summaries = _api.list_sessions(owner=cwd)
|
|
427
|
+
if not summaries:
|
|
428
|
+
console.print(
|
|
429
|
+
"[dim]No prior session found for this directory. "
|
|
430
|
+
"Starting fresh.[/dim]"
|
|
431
|
+
)
|
|
432
|
+
return None
|
|
433
|
+
return summaries[0].session_id
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
# Public command
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def chat_command(
|
|
443
|
+
prompt: Annotated[
|
|
444
|
+
str | None,
|
|
445
|
+
typer.Argument(help="Optional one-shot prompt"),
|
|
446
|
+
] = None,
|
|
447
|
+
model: Annotated[
|
|
448
|
+
str | None,
|
|
449
|
+
typer.Option("--model", help="Model name, optionally provider-prefixed"),
|
|
450
|
+
] = None,
|
|
451
|
+
continue_session: Annotated[
|
|
452
|
+
bool,
|
|
453
|
+
typer.Option(
|
|
454
|
+
"-c",
|
|
455
|
+
"--continue",
|
|
456
|
+
help="Resume the most recent session for the current directory",
|
|
457
|
+
),
|
|
458
|
+
] = False,
|
|
459
|
+
resume: Annotated[
|
|
460
|
+
str | None,
|
|
461
|
+
typer.Option(
|
|
462
|
+
"-r",
|
|
463
|
+
"--resume",
|
|
464
|
+
help="Resume a specific session by id (see --pick for the interactive list)",
|
|
465
|
+
),
|
|
466
|
+
] = None,
|
|
467
|
+
pick: Annotated[
|
|
468
|
+
bool,
|
|
469
|
+
typer.Option(
|
|
470
|
+
"-p",
|
|
471
|
+
"--pick",
|
|
472
|
+
help="Open an interactive picker to choose a session to resume",
|
|
473
|
+
),
|
|
474
|
+
] = False,
|
|
475
|
+
) -> None:
|
|
476
|
+
"""Chat with a terminal AI agent.
|
|
477
|
+
|
|
478
|
+
Sessions are auto-saved to ~/.dft/sessions/ and indexed by working directory.
|
|
479
|
+
|
|
480
|
+
Examples:
|
|
481
|
+
dft chat -c Resume last session for this directory
|
|
482
|
+
dft chat -r <session-id> Resume a specific session by id
|
|
483
|
+
dft chat -p Pick from a list of recent sessions
|
|
484
|
+
|
|
485
|
+
External MCP servers are auto-loaded from the same config files that
|
|
486
|
+
`dft init mcp` writes (.cursor/mcp.json, .mcp.json, etc.). Dataface's own
|
|
487
|
+
MCP server entry is skipped — in-process tools are used instead.
|
|
488
|
+
External tools are namespaced as `server__tool` (double underscore, matching
|
|
489
|
+
Claude Code's MCP convention and compatible with LLM provider tool-name rules).
|
|
490
|
+
"""
|
|
491
|
+
from dataface.cli._extras import require_extras
|
|
492
|
+
|
|
493
|
+
require_extras("chat")
|
|
494
|
+
|
|
495
|
+
import os
|
|
496
|
+
|
|
497
|
+
from dataface.ai.external_mcp import ExternalMCPManager, discover_servers
|
|
498
|
+
from dataface.ai.tool_schemas import ALL_TOOLS
|
|
499
|
+
from dataface.cli.commands._agent_server import (
|
|
500
|
+
start_http_server,
|
|
501
|
+
stop_http_server,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
console = Console()
|
|
505
|
+
|
|
506
|
+
# Start the embedded preview HTTP server before building context, so
|
|
507
|
+
# render_dashboard URLs point at the agent's own server.
|
|
508
|
+
server_port_hint = int(os.getenv("DFT_CHAT_PORT", "8765"))
|
|
509
|
+
http_handle = start_http_server(port_hint=server_port_hint)
|
|
510
|
+
console.print(f"[dim]Preview server: http://localhost:{http_handle.port}/[/dim]")
|
|
511
|
+
|
|
512
|
+
cwd = Path.cwd().resolve()
|
|
513
|
+
session_id_to_resume = _resolve_resume_id(
|
|
514
|
+
continue_session, resume, pick, cwd, console
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
chat_session: _api.ChatSession | None = None
|
|
518
|
+
external_manager: ExternalMCPManager | None = None
|
|
519
|
+
try:
|
|
520
|
+
if session_id_to_resume:
|
|
521
|
+
try:
|
|
522
|
+
chat_session = _api.resume_session(
|
|
523
|
+
session_id_to_resume,
|
|
524
|
+
project_dir=Path.cwd(),
|
|
525
|
+
server_port=http_handle.port,
|
|
526
|
+
model=model,
|
|
527
|
+
)
|
|
528
|
+
console.print(
|
|
529
|
+
f"[dim]Resumed session {session_id_to_resume[:8]} "
|
|
530
|
+
f"({len(chat_session.messages)} messages)[/dim]"
|
|
531
|
+
)
|
|
532
|
+
except ValueError as exc:
|
|
533
|
+
console.print(f"[red]{exc}[/red]")
|
|
534
|
+
raise typer.Exit(1) from exc
|
|
535
|
+
else:
|
|
536
|
+
chat_session = _api.start_session(
|
|
537
|
+
model=model,
|
|
538
|
+
project_dir=Path.cwd(),
|
|
539
|
+
server_port=http_handle.port,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
external_configs = discover_servers()
|
|
543
|
+
external_manager = ExternalMCPManager(external_configs)
|
|
544
|
+
external_manager.start(timeout=10.0)
|
|
545
|
+
merged_tools = ALL_TOOLS + external_manager.all_tools()
|
|
546
|
+
|
|
547
|
+
session = Session(
|
|
548
|
+
client=chat_session.client,
|
|
549
|
+
context=chat_session.context,
|
|
550
|
+
console=console,
|
|
551
|
+
)
|
|
552
|
+
session.messages = chat_session.messages
|
|
553
|
+
|
|
554
|
+
if prompt:
|
|
555
|
+
tmpdir = Path(tempfile.mkdtemp(prefix="dft_chat_"))
|
|
556
|
+
atexit.register(shutil.rmtree, str(tmpdir), True)
|
|
557
|
+
result_status = _run_agent_session(
|
|
558
|
+
chat_session,
|
|
559
|
+
prompt,
|
|
560
|
+
console=console,
|
|
561
|
+
tmpdir=tmpdir,
|
|
562
|
+
tools=merged_tools,
|
|
563
|
+
external_manager=external_manager,
|
|
564
|
+
)
|
|
565
|
+
if result_status == "interrupted":
|
|
566
|
+
raise typer.Exit(130)
|
|
567
|
+
if result_status == "errored":
|
|
568
|
+
raise typer.Exit(1)
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
_run_interactive(
|
|
572
|
+
session=session,
|
|
573
|
+
chat_session=chat_session,
|
|
574
|
+
console=console,
|
|
575
|
+
merged_tools=merged_tools,
|
|
576
|
+
external_manager=external_manager,
|
|
577
|
+
)
|
|
578
|
+
finally:
|
|
579
|
+
if external_manager is not None:
|
|
580
|
+
external_manager.close()
|
|
581
|
+
stop_http_server(http_handle)
|
|
582
|
+
if chat_session is not None:
|
|
583
|
+
chat_session.writer.close()
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _run_interactive(
|
|
587
|
+
session: Session,
|
|
588
|
+
chat_session: _api.ChatSession,
|
|
589
|
+
console: Console,
|
|
590
|
+
merged_tools: list[dict[str, Any]],
|
|
591
|
+
external_manager: ExternalMCPManager,
|
|
592
|
+
) -> None:
|
|
593
|
+
"""Run the interactive REPL loop."""
|
|
594
|
+
tmpdir = Path(tempfile.mkdtemp(prefix="dft_chat_"))
|
|
595
|
+
atexit.register(shutil.rmtree, str(tmpdir), True)
|
|
596
|
+
|
|
597
|
+
hist_path = dft_home() / "chat_history"
|
|
598
|
+
input_layer = select_input_layer(hist_path, slash_commands=set(SLASH_COMMANDS))
|
|
599
|
+
stream_ctx: Callable[..., AbstractContextManager[None]]
|
|
600
|
+
if isinstance(input_layer, PromptToolkitInput):
|
|
601
|
+
from prompt_toolkit.patch_stdout import (
|
|
602
|
+
patch_stdout as _patch_stdout,
|
|
603
|
+
) # noqa: PLC0415
|
|
604
|
+
|
|
605
|
+
stream_ctx = _patch_stdout
|
|
606
|
+
else:
|
|
607
|
+
stream_ctx = nullcontext
|
|
608
|
+
|
|
609
|
+
last_sigint_ts: list[float] = []
|
|
610
|
+
|
|
611
|
+
def _at_prompt_sigint(_signum: int, _frame: Any) -> None:
|
|
612
|
+
if _check_double_tap_exit(last_sigint_ts):
|
|
613
|
+
console.print("\n[dim]Session ended.[/dim]")
|
|
614
|
+
raise SystemExit(0)
|
|
615
|
+
console.print("\n[dim](Press Ctrl+C again within 1s to exit, or Ctrl+D)[/dim]")
|
|
616
|
+
|
|
617
|
+
signal.signal(signal.SIGINT, _at_prompt_sigint)
|
|
618
|
+
|
|
619
|
+
while True:
|
|
620
|
+
try:
|
|
621
|
+
user_input = input_layer.read("You: ")
|
|
622
|
+
except EOFError:
|
|
623
|
+
console.print("\n[dim]Session ended.[/dim]")
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
if not user_input.strip():
|
|
627
|
+
continue
|
|
628
|
+
if _dispatch_slash(session, user_input):
|
|
629
|
+
continue
|
|
630
|
+
|
|
631
|
+
user_input = expand_file_refs(user_input, project_dir=Path.cwd()).text
|
|
632
|
+
|
|
633
|
+
# Slash commands like /clear and /model rebuild session.client; mirror
|
|
634
|
+
# that onto chat_session so send_message picks up the new client.
|
|
635
|
+
chat_session.client = session.client
|
|
636
|
+
|
|
637
|
+
with stream_ctx():
|
|
638
|
+
_run_agent_session(
|
|
639
|
+
chat_session,
|
|
640
|
+
user_input,
|
|
641
|
+
console=session.console,
|
|
642
|
+
tmpdir=tmpdir,
|
|
643
|
+
tools=merged_tools,
|
|
644
|
+
external_manager=external_manager,
|
|
645
|
+
)
|