dataface-playground 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataface_playground/__init__.py +1 -0
- dataface_playground/ai_service.py +535 -0
- dataface_playground/ai_service_streaming.py +341 -0
- dataface_playground/ai_tools.py +45 -0
- dataface_playground/app.py +182 -0
- dataface_playground/cli.py +184 -0
- dataface_playground/error_display.py +41 -0
- dataface_playground/examples.py +288 -0
- dataface_playground/examples_data/examples/_shared_queries.yml +40 -0
- dataface_playground/examples_data/examples/_sources.yaml +55 -0
- dataface_playground/examples_data/examples/assets/data/area-stack-modes/three_series.csv +61 -0
- dataface_playground/examples_data/examples/assets/data/chart-lab-import-review/lab_area_multi_2.csv +41 -0
- dataface_playground/examples_data/examples/assets/data/chart-lab-import-review/lab_bar_category.csv +9 -0
- dataface_playground/examples_data/examples/assets/data/chart-lab-import-review/lab_column_line_combo.csv +10 -0
- dataface_playground/examples_data/examples/assets/data/chart-lab-import-review/lab_column_stacked.csv +49 -0
- dataface_playground/examples_data/examples/assets/data/chart-lab-import-review/lab_donut_2.csv +3 -0
- dataface_playground/examples_data/examples/assets/data/chart-lab-import-review/lab_donut_6.csv +7 -0
- dataface_playground/examples_data/examples/assets/data/chart-lab-import-review/lab_line_multi_2.csv +157 -0
- dataface_playground/examples_data/examples/assets/data/chart-lab-import-review/lab_line_multi_6.csv +469 -0
- dataface_playground/examples_data/examples/assets/data/chart-lab-import-review/lab_vertical_bar.csv +10 -0
- dataface_playground/examples_data/examples/assets/data/ecommerce_orders.csv +153 -0
- dataface_playground/examples_data/examples/assets/data/employees.csv +11 -0
- dataface_playground/examples_data/examples/assets/data/inventory.csv +9 -0
- dataface_playground/examples_data/examples/assets/data/kpi-specimens/quarterly_kpis.csv +2 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/company_revenue.csv +9 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/company_revenue_full_summary.csv +10 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/company_revenue_with_avg.csv +10 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/company_revenue_with_total.csv +10 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/country_gdp.csv +8 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/country_gdp_with_avg_col.csv +8 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/events_dates.csv +9 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/events_dates_full.csv +9 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/growth_rates.csv +8 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/growth_rates_pct.csv +8 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/museum_visitors.csv +11 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/sales_pipeline.csv +11 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/state_demographics.csv +31 -0
- dataface_playground/examples_data/examples/assets/data/table-specimens/us_states_top8.csv +9 -0
- dataface_playground/examples_data/examples/assets/data/us_states.csv +51 -0
- dataface_playground/examples_data/examples/assets/data/world_countries.csv +55 -0
- dataface_playground/examples_data/examples/assets/images/dbt-logo.svg +19 -0
- dataface_playground/examples_data/examples/assets/images/fivetran-logo.svg +22 -0
- dataface_playground/examples_data/examples/dataface.yml +35 -0
- dataface_playground/examples_data/examples/examples.duckdb +0 -0
- dataface_playground/examples_data/examples/playground/faces/charts/_settings.json +4 -0
- dataface_playground/examples_data/examples/playground/faces/charts/all-chart-types.yml +145 -0
- dataface_playground/examples_data/examples/playground/faces/charts/anatomy-of-a-signature.yml +585 -0
- dataface_playground/examples_data/examples/playground/faces/charts/axis-label-stress-test.yml +228 -0
- dataface_playground/examples_data/examples/playground/faces/charts/callout-tones.yml +70 -0
- dataface_playground/examples_data/examples/playground/faces/charts/nyc-neighborhoods-map.yml +56 -0
- dataface_playground/examples_data/examples/playground/faces/charts/sf-neighborhoods-map.yml +69 -0
- dataface_playground/examples_data/examples/playground/faces/charts/store-locations-map.yml +48 -0
- dataface_playground/examples_data/examples/playground/faces/charts/us-states-map.yml +27 -0
- dataface_playground/examples_data/examples/playground/faces/charts/width-compaction.yml +172 -0
- dataface_playground/examples_data/examples/playground/faces/charts/world-map.yml +41 -0
- dataface_playground/examples_data/examples/playground/faces/general/_settings.json +4 -0
- dataface_playground/examples_data/examples/playground/faces/general/company-overview-tight.yml +467 -0
- dataface_playground/examples_data/examples/playground/faces/general/company-overview.yml +401 -0
- dataface_playground/examples_data/examples/playground/faces/general/dundersign-commercial-finance-default.yml +517 -0
- dataface_playground/examples_data/examples/playground/faces/general/dundersign-commercial-finance.yml +530 -0
- dataface_playground/examples_data/examples/playground/faces/general/dundersign-support-operations.yml +544 -0
- dataface_playground/examples_data/examples/playground/faces/general/products-overview.yml +57 -0
- dataface_playground/examples_data/examples/playground/faces/general/sales-overview.yml +58 -0
- dataface_playground/examples_data/examples/playground/faces/general/simple-example.yml +24 -0
- dataface_playground/examples_data/examples/playground/faces/general/users-overview.yml +41 -0
- dataface_playground/examples_data/examples/playground/faces/reference/_settings.json +4 -0
- dataface_playground/examples_data/examples/playground/faces/reference/area-stack-modes.yml +115 -0
- dataface_playground/examples_data/examples/playground/faces/reference/chart-design-lab.yml +151 -0
- dataface_playground/examples_data/examples/playground/faces/reference/chart-lab-import-review.yml +289 -0
- dataface_playground/examples_data/examples/playground/faces/reference/color-palettes.yml +1187 -0
- dataface_playground/examples_data/examples/playground/faces/reference/content-test.yml +34 -0
- dataface_playground/examples_data/examples/playground/faces/reference/dark-theme-table-example.yml +77 -0
- dataface_playground/examples_data/examples/playground/faces/reference/dataface-guide.yml +217 -0
- dataface_playground/examples_data/examples/playground/faces/reference/date-format-explorer.yml +456 -0
- dataface_playground/examples_data/examples/playground/faces/reference/editorial-stress-test.yml +253 -0
- dataface_playground/examples_data/examples/playground/faces/reference/hello_world.yml +35 -0
- dataface_playground/examples_data/examples/playground/faces/reference/link-examples.yml +77 -0
- dataface_playground/examples_data/examples/playground/faces/reference/markdown-guide.yml +401 -0
- dataface_playground/examples_data/examples/playground/faces/reference/nested-layouts.yml +244 -0
- dataface_playground/examples_data/examples/playground/faces/reference/overlap-tilt-explorer.yml +254 -0
- dataface_playground/examples_data/examples/playground/faces/reference/palette-reference.yml +295 -0
- dataface_playground/examples_data/examples/playground/faces/reference/quick-guide.yml +604 -0
- dataface_playground/examples_data/examples/playground/faces/reference/series-label-placement.yml +179 -0
- dataface_playground/examples_data/examples/playground/faces/reference/source_example.yml +61 -0
- dataface_playground/examples_data/examples/playground/faces/reference/spark-charts-demo.yml +46 -0
- dataface_playground/examples_data/examples/playground/faces/reference/stacked-bar-stress-test.yml +71 -0
- dataface_playground/examples_data/examples/playground/faces/reference/styling.yml +390 -0
- dataface_playground/examples_data/examples/playground/faces/reference/time-unit-explorer.yml +414 -0
- dataface_playground/examples_data/examples/playground/faces/tables-and-kpis/_settings.json +4 -0
- dataface_playground/examples_data/examples/playground/faces/tables-and-kpis/data-table-attached.yml +221 -0
- dataface_playground/examples_data/examples/playground/faces/tables-and-kpis/kpi-specimen-foundations.yml +254 -0
- dataface_playground/examples_data/examples/playground/faces/tables-and-kpis/pagination-autoshrink-repro.yml +65 -0
- dataface_playground/examples_data/examples/playground/faces/tables-and-kpis/paginator-lab.yml +132 -0
- dataface_playground/examples_data/examples/playground/faces/tables-and-kpis/table-emphasis.yml +484 -0
- dataface_playground/examples_data/examples/playground/faces/tables-and-kpis/table-foundations.yml +775 -0
- dataface_playground/examples_data/examples/playground/faces/tables-and-kpis/table-spark-charts.yml +87 -0
- dataface_playground/examples_data/examples/playground/faces/tables-and-kpis/table-style-variants.yml +141 -0
- dataface_playground/examples_data/examples/playground/faces/variables/_settings.json +4 -0
- dataface_playground/examples_data/examples/playground/faces/variables/game-of-life.yml +635 -0
- dataface_playground/examples_data/examples/playground/faces/variables/imported-board-nested.yml +135 -0
- dataface_playground/examples_data/examples/playground/faces/variables/interactive-nested.yml +118 -0
- dataface_playground/examples_data/examples/playground/faces/variables/interactive-test.yml +52 -0
- dataface_playground/examples_data/examples/playground/faces/variables/interactive-variables.yml +137 -0
- dataface_playground/examples_data/examples/playground/faces/variables/interactive.yml +126 -0
- dataface_playground/examples_data/examples/playground/faces/variables/tic-tac-toe.yml +386 -0
- dataface_playground/prompts/dashboard_design.md +76 -0
- dataface_playground/prompts/database_exploration.md +86 -0
- dataface_playground/prompts/report_generation.md +74 -0
- dataface_playground/prompts/yaml_generation.md +559 -0
- dataface_playground/routes.py +1145 -0
- dataface_playground/share_cache.py +16 -0
- dataface_playground/source_resolver_selector.py +33 -0
- dataface_playground/static/ai-sidebar.js +702 -0
- dataface_playground/static/bootstrap.js +122 -0
- dataface_playground/static/celebration.js +173 -0
- dataface_playground/static/code-preview.js +28 -0
- dataface_playground/static/dataface-mode.js +166 -0
- dataface_playground/static/dom.js +17 -0
- dataface_playground/static/download.js +123 -0
- dataface_playground/static/editor.js +66 -0
- dataface_playground/static/examples-gallery.js +564 -0
- dataface_playground/static/favicon.svg +4 -0
- dataface_playground/static/index.html +207 -0
- dataface_playground/static/pane-resize.js +252 -0
- dataface_playground/static/playground.css +1970 -0
- dataface_playground/static/preview.js +573 -0
- dataface_playground/static/search.js +152 -0
- dataface_playground/static/share-link.js +124 -0
- dataface_playground/static/state.js +84 -0
- dataface_playground/static/websocket.js +39 -0
- dataface_playground/thumbnails.py +10 -0
- dataface_playground/websocket.py +63 -0
- dataface_playground-0.0.1.dist-info/METADATA +78 -0
- dataface_playground-0.0.1.dist-info/RECORD +137 -0
- dataface_playground-0.0.1.dist-info/WHEEL +4 -0
- dataface_playground-0.0.1.dist-info/entry_points.txt +2 -0
- dataface_playground-0.0.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Dataface interactive playground — OSS dashboard editor package."""
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"""AI service for playground chat functionality.
|
|
2
|
+
|
|
3
|
+
This module provides OpenAI integration for generating dashboards, reports,
|
|
4
|
+
and providing database exploration assistance. Tool calls are dispatched
|
|
5
|
+
through the canonical Dataface AI tool surface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from dataface.agent_api.docs import read_full_text as _read_dataface_syntax
|
|
14
|
+
from dataface.ai.context import DatafaceAIContext
|
|
15
|
+
from dataface.ai.generate_sql import get_sql_generation_guidance
|
|
16
|
+
from dataface.ai.prompts import build_context_section, load_prompt, load_shared_prompt
|
|
17
|
+
from dataface.ai.tool_schemas import to_openai_tool
|
|
18
|
+
from dataface.ai.tools import dispatch_tool_call
|
|
19
|
+
from dataface.ai.yaml_utils import extract_yaml
|
|
20
|
+
from dataface_playground.ai_tools import PLAYGROUND_TOOLS, _handle_render_dashboard
|
|
21
|
+
from dataface_playground.error_display import error_summary as format_playground_error
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AIService:
|
|
25
|
+
"""Service for interacting with OpenAI API."""
|
|
26
|
+
|
|
27
|
+
_TOOL_INSTRUCTIONS = """
|
|
28
|
+
|
|
29
|
+
## IMPORTANT: Rendering and Testing
|
|
30
|
+
|
|
31
|
+
You have access to tools for working with dashboards:
|
|
32
|
+
|
|
33
|
+
1. **ALWAYS render** your generated YAML using `render_dashboard` before returning it — this checks structure, executes queries, and verifies render output in one step
|
|
34
|
+
2. **Use `execute_query`** to run SQL directly for data exploration (e.g. discovering tables, checking column names)
|
|
35
|
+
3. **Use `schema`** to browse the data hierarchy: sources, schemas, tables, and columns
|
|
36
|
+
4. **Fix any errors** reported by `render_dashboard` and render again until it succeeds
|
|
37
|
+
5. **Only return YAML** that renders successfully
|
|
38
|
+
6. If `schema(table=...)` says a table has **no cached profile**, that means the table may still exist live. Use the returned columns and/or `execute_query` to confirm instead of claiming the table is missing.
|
|
39
|
+
|
|
40
|
+
If rendering fails, analyze the errors (including tips), fix them, and render again. Each render attempt is visible to the user in the playground editor, so keep iterating with complete YAML.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
api_key: str | None = None,
|
|
46
|
+
ai_context: DatafaceAIContext | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
|
|
49
|
+
self._client: Any = None
|
|
50
|
+
self.prompts_dir = Path(__file__).parent / "prompts"
|
|
51
|
+
self._ai_context = ai_context
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def client(self) -> Any:
|
|
55
|
+
"""Lazy-load OpenAI client."""
|
|
56
|
+
if self._client is None and self.api_key:
|
|
57
|
+
from openai import OpenAI
|
|
58
|
+
|
|
59
|
+
self._client = OpenAI(api_key=self.api_key)
|
|
60
|
+
return self._client
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_available(self) -> bool:
|
|
64
|
+
"""Lazily initializes the client if API key is now available in environment."""
|
|
65
|
+
if self._client is None:
|
|
66
|
+
api_key = self.api_key or os.getenv("OPENAI_API_KEY")
|
|
67
|
+
if api_key:
|
|
68
|
+
self.api_key = api_key
|
|
69
|
+
self._client = None # Reset so client property re-creates
|
|
70
|
+
return self.client is not None
|
|
71
|
+
|
|
72
|
+
def _load_prompt(self, prompt_name: str) -> str:
|
|
73
|
+
"""Load a prompt template from markdown file.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
prompt_name: Name of prompt file (without .md extension)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Prompt content as string
|
|
80
|
+
"""
|
|
81
|
+
# Use core utility for prompt loading
|
|
82
|
+
return load_prompt(prompt_name, self.prompts_dir)
|
|
83
|
+
|
|
84
|
+
def _build_system_prompt(
|
|
85
|
+
self,
|
|
86
|
+
prompt_type: str,
|
|
87
|
+
database_context: str | None = None,
|
|
88
|
+
yaml_context: str | None = None,
|
|
89
|
+
chart_context: str | None = None,
|
|
90
|
+
) -> str:
|
|
91
|
+
"""Build system prompt with context.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
prompt_type: Type of prompt (database_exploration, yaml_generation, etc.)
|
|
95
|
+
database_context: Optional database schema information
|
|
96
|
+
yaml_context: Optional selected YAML code
|
|
97
|
+
chart_context: Optional chart information
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Complete system prompt
|
|
101
|
+
"""
|
|
102
|
+
base_prompt = self._load_prompt(prompt_type)
|
|
103
|
+
|
|
104
|
+
# Prepend shared design guide for dashboard/report prompts.
|
|
105
|
+
# Shared guides under dataface.ai.skills carry universal design
|
|
106
|
+
# principles; the app-specific prompts add tool instructions.
|
|
107
|
+
shared_guide_map = {
|
|
108
|
+
"dashboard_design": "dashboard-design",
|
|
109
|
+
"report_generation": "report-design",
|
|
110
|
+
}
|
|
111
|
+
if prompt_type in shared_guide_map:
|
|
112
|
+
shared = load_shared_prompt(shared_guide_map[prompt_type])
|
|
113
|
+
if shared:
|
|
114
|
+
base_prompt = shared + "\n\n---\n\n" + base_prompt
|
|
115
|
+
|
|
116
|
+
if prompt_type in ("yaml_generation", "dashboard_design", "report_generation"):
|
|
117
|
+
base_prompt += "\n\n" + get_sql_generation_guidance()
|
|
118
|
+
|
|
119
|
+
schema_section = ""
|
|
120
|
+
if prompt_type in ("yaml_generation", "dashboard_design", "report_generation"):
|
|
121
|
+
schema_section = f"\n\n{_read_dataface_syntax()}\n\n"
|
|
122
|
+
|
|
123
|
+
# Use core utility for building context section
|
|
124
|
+
context_section = build_context_section(
|
|
125
|
+
database_context=database_context,
|
|
126
|
+
yaml_context=yaml_context,
|
|
127
|
+
chart_context=chart_context,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Build final prompt: context first, then base prompt, then schema
|
|
131
|
+
if context_section:
|
|
132
|
+
return context_section + "\n\n" + base_prompt + schema_section
|
|
133
|
+
|
|
134
|
+
return base_prompt + schema_section
|
|
135
|
+
|
|
136
|
+
def _get_tools(self) -> list[dict[str, Any]]:
|
|
137
|
+
"""Get playground tool schemas in OpenAI function-calling format.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List of tool definitions for OpenAI function calling
|
|
141
|
+
"""
|
|
142
|
+
return [to_openai_tool(tool) for tool in PLAYGROUND_TOOLS]
|
|
143
|
+
|
|
144
|
+
def _dispatch_tool(
|
|
145
|
+
self, function_name: str, function_args: dict[str, Any]
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
"""Dispatch a tool call via the shared canonical tool surface.
|
|
148
|
+
|
|
149
|
+
When the AI omits a ``source`` on query/schema calls, injects the
|
|
150
|
+
context's ``default_source`` so the tool hits the same database the
|
|
151
|
+
schema preview discovered and the rendered YAML will use.
|
|
152
|
+
"""
|
|
153
|
+
ctx = self._ai_context
|
|
154
|
+
if ctx is None:
|
|
155
|
+
from dataface_playground.routes import get_playground_ai_context
|
|
156
|
+
|
|
157
|
+
ctx = get_playground_ai_context()
|
|
158
|
+
|
|
159
|
+
if ctx.default_source and function_name in (
|
|
160
|
+
"execute_query",
|
|
161
|
+
"schema",
|
|
162
|
+
):
|
|
163
|
+
function_args.setdefault("source", ctx.default_source)
|
|
164
|
+
|
|
165
|
+
if function_name == "render_dashboard":
|
|
166
|
+
return _handle_render_dashboard(function_args, ctx)
|
|
167
|
+
|
|
168
|
+
return dispatch_tool_call(function_name, function_args, context=ctx)
|
|
169
|
+
|
|
170
|
+
def _tool_instructions(self) -> str:
|
|
171
|
+
"""Return the shared tool instructions for playground prompts."""
|
|
172
|
+
return self._TOOL_INSTRUCTIONS
|
|
173
|
+
|
|
174
|
+
def chat(
|
|
175
|
+
self,
|
|
176
|
+
user_message: str,
|
|
177
|
+
prompt_type: str = "yaml_generation",
|
|
178
|
+
database_context: str | None = None,
|
|
179
|
+
yaml_context: str | None = None,
|
|
180
|
+
chart_context: str | None = None,
|
|
181
|
+
conversation_history: list[dict[str, Any]] | None = None,
|
|
182
|
+
enable_validation: bool = True,
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
"""Send a chat message to OpenAI with optional render tools.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
user_message: User's message
|
|
188
|
+
prompt_type: Type of prompt to use (database_exploration, yaml_generation, report_generation, dashboard_design)
|
|
189
|
+
database_context: Optional database schema information
|
|
190
|
+
yaml_context: Optional selected YAML code
|
|
191
|
+
chart_context: Optional chart information
|
|
192
|
+
conversation_history: Optional list of previous messages [{role: str, content: str}]
|
|
193
|
+
enable_validation: If True, enable function calling for render checks (default: True)
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dict with 'response' (str) and 'yaml' (optional str if YAML was generated)
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
ValueError: If AI service is not available
|
|
200
|
+
"""
|
|
201
|
+
if not self.is_available:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
"OpenAI API key not configured. Set OPENAI_API_KEY environment variable."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Build system prompt
|
|
207
|
+
system_prompt = self._build_system_prompt(
|
|
208
|
+
prompt_type=prompt_type,
|
|
209
|
+
database_context=database_context,
|
|
210
|
+
yaml_context=yaml_context,
|
|
211
|
+
chart_context=chart_context,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Add instructions about playground tools
|
|
215
|
+
if enable_validation and prompt_type in (
|
|
216
|
+
"yaml_generation",
|
|
217
|
+
"dashboard_design",
|
|
218
|
+
"report_generation",
|
|
219
|
+
):
|
|
220
|
+
system_prompt += self._tool_instructions()
|
|
221
|
+
|
|
222
|
+
# Build messages
|
|
223
|
+
messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt}]
|
|
224
|
+
|
|
225
|
+
# Add conversation history
|
|
226
|
+
if conversation_history:
|
|
227
|
+
messages.extend(conversation_history)
|
|
228
|
+
|
|
229
|
+
# Add current user message
|
|
230
|
+
messages.append({"role": "user", "content": user_message})
|
|
231
|
+
|
|
232
|
+
# Call OpenAI API with function calling if render checks are enabled
|
|
233
|
+
max_iterations = 10 # Increased to allow more iteration for complex dashboards
|
|
234
|
+
allow_return_with_errors_after = (
|
|
235
|
+
7 # After this many iterations, allow returning with errors
|
|
236
|
+
)
|
|
237
|
+
iteration = 0
|
|
238
|
+
last_yaml_content = None # Track last generated YAML
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
while iteration < max_iterations:
|
|
242
|
+
iteration += 1
|
|
243
|
+
|
|
244
|
+
# After threshold iterations, tell agent it can return with explanation if stuck
|
|
245
|
+
if iteration == allow_return_with_errors_after:
|
|
246
|
+
messages.append(
|
|
247
|
+
{
|
|
248
|
+
"role": "user",
|
|
249
|
+
"content": "You've tried several times to fix the errors. If you're hitting a real issue that you cannot fix (like a configuration problem, missing database connection, or something outside your control), you can return the YAML you've generated along with an explanation of what the issue is. The user will be able to see what you've created and understand the problem. However, if you think you can fix it with one more attempt, please try again.",
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Prepare API call
|
|
254
|
+
# Try GPT-5 Thinking (gpt-5.1) first, fallback to gpt-4o if not available
|
|
255
|
+
# GPT-5 supports advanced reasoning and function calling
|
|
256
|
+
model = "gpt-5.1"
|
|
257
|
+
|
|
258
|
+
api_kwargs: dict[str, Any] = {
|
|
259
|
+
"model": model,
|
|
260
|
+
"messages": messages,
|
|
261
|
+
"temperature": 0.7,
|
|
262
|
+
"max_tokens": 4000,
|
|
263
|
+
"stream": False, # Set to True for streaming (handled separately)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Add reasoning parameter if supported (GPT-5 models)
|
|
267
|
+
# This may not be supported in all SDK versions, so we'll handle errors gracefully
|
|
268
|
+
api_kwargs["reasoning"] = {
|
|
269
|
+
"summary": "auto", # Enable reasoning summaries for status indicators
|
|
270
|
+
"effort": "medium", # Balance between quality and latency
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Add tools if render checks are enabled
|
|
274
|
+
if enable_validation and prompt_type in (
|
|
275
|
+
"yaml_generation",
|
|
276
|
+
"dashboard_design",
|
|
277
|
+
"report_generation",
|
|
278
|
+
):
|
|
279
|
+
api_kwargs["tools"] = self._get_tools()
|
|
280
|
+
api_kwargs["tool_choice"] = "auto"
|
|
281
|
+
|
|
282
|
+
# Try API call with reasoning, fallback without if not supported
|
|
283
|
+
try:
|
|
284
|
+
response = self.client.chat.completions.create(**api_kwargs)
|
|
285
|
+
except (TypeError, ValueError) as e:
|
|
286
|
+
# If reasoning parameter is not supported, retry without it
|
|
287
|
+
error_str = str(e).lower()
|
|
288
|
+
if "reasoning" in error_str or "unexpected keyword" in error_str:
|
|
289
|
+
api_kwargs.pop("reasoning", None)
|
|
290
|
+
# Also try fallback model if gpt-5.1 doesn't exist
|
|
291
|
+
if model == "gpt-5.1":
|
|
292
|
+
api_kwargs["model"] = "gpt-4o"
|
|
293
|
+
response = self.client.chat.completions.create(**api_kwargs)
|
|
294
|
+
else:
|
|
295
|
+
raise
|
|
296
|
+
|
|
297
|
+
assistant_message = response.choices[0].message
|
|
298
|
+
|
|
299
|
+
# Check if function calling was used
|
|
300
|
+
if assistant_message.tool_calls:
|
|
301
|
+
# Add assistant message to conversation
|
|
302
|
+
# Convert tool_calls to dict format
|
|
303
|
+
tool_calls_dict = [
|
|
304
|
+
{
|
|
305
|
+
"id": tc.id,
|
|
306
|
+
"type": tc.type,
|
|
307
|
+
"function": {
|
|
308
|
+
"name": tc.function.name,
|
|
309
|
+
"arguments": tc.function.arguments,
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
for tc in assistant_message.tool_calls
|
|
313
|
+
]
|
|
314
|
+
messages.append(
|
|
315
|
+
{
|
|
316
|
+
"role": "assistant",
|
|
317
|
+
"content": assistant_message.content,
|
|
318
|
+
"tool_calls": tool_calls_dict,
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Execute tool calls
|
|
323
|
+
for tool_call in assistant_message.tool_calls:
|
|
324
|
+
# Handle both object and dict access
|
|
325
|
+
if hasattr(tool_call, "function"):
|
|
326
|
+
function_name = tool_call.function.name
|
|
327
|
+
function_args_str = tool_call.function.arguments
|
|
328
|
+
tool_call_id = tool_call.id
|
|
329
|
+
else:
|
|
330
|
+
function_name = tool_call["function"]["name"]
|
|
331
|
+
function_args_str = tool_call["function"]["arguments"]
|
|
332
|
+
tool_call_id = tool_call["id"]
|
|
333
|
+
|
|
334
|
+
function_args = (
|
|
335
|
+
json.loads(function_args_str) if function_args_str else {}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
result = self._dispatch_tool(function_name, function_args)
|
|
339
|
+
|
|
340
|
+
# Truncate large data fields for the AI context window
|
|
341
|
+
if "data" in result and isinstance(result["data"], list):
|
|
342
|
+
result = {
|
|
343
|
+
**result,
|
|
344
|
+
"data": result["data"][:20],
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
messages.append(
|
|
348
|
+
{
|
|
349
|
+
"role": "tool",
|
|
350
|
+
"tool_call_id": tool_call_id,
|
|
351
|
+
"name": function_name,
|
|
352
|
+
"content": json.dumps(result, default=str),
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Continue loop to get AI's response after function calls
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
# No function calls - we have the final response
|
|
360
|
+
assistant_content = assistant_message.content or ""
|
|
361
|
+
|
|
362
|
+
# Try to extract YAML from response
|
|
363
|
+
yaml_content = self._extract_yaml(assistant_content)
|
|
364
|
+
|
|
365
|
+
# Track the last YAML content we've seen
|
|
366
|
+
if yaml_content:
|
|
367
|
+
last_yaml_content = yaml_content
|
|
368
|
+
|
|
369
|
+
# If we have YAML but render checks were enabled, do final checks
|
|
370
|
+
if (
|
|
371
|
+
yaml_content
|
|
372
|
+
and enable_validation
|
|
373
|
+
and prompt_type
|
|
374
|
+
in (
|
|
375
|
+
"yaml_generation",
|
|
376
|
+
"dashboard_design",
|
|
377
|
+
"report_generation",
|
|
378
|
+
)
|
|
379
|
+
):
|
|
380
|
+
render_result = self._dispatch_tool(
|
|
381
|
+
"render_dashboard",
|
|
382
|
+
{"yaml_content": yaml_content},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if not render_result.get("success", False):
|
|
386
|
+
# After threshold, allow agent to return with explanation if it wants
|
|
387
|
+
if iteration >= allow_return_with_errors_after:
|
|
388
|
+
if any(
|
|
389
|
+
phrase in assistant_content.lower()
|
|
390
|
+
for phrase in [
|
|
391
|
+
"cannot fix",
|
|
392
|
+
"unable to",
|
|
393
|
+
"configuration",
|
|
394
|
+
"missing",
|
|
395
|
+
"outside my control",
|
|
396
|
+
"requires",
|
|
397
|
+
"need to",
|
|
398
|
+
"issue is",
|
|
399
|
+
]
|
|
400
|
+
):
|
|
401
|
+
return {
|
|
402
|
+
"response": assistant_content,
|
|
403
|
+
"yaml": yaml_content,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# Continue iterating — feed errors back to AI
|
|
407
|
+
render_error_summary = render_result.get("error_summary", "")
|
|
408
|
+
errors = render_result.get("errors", [])
|
|
409
|
+
tips = render_result.get("tips", [])
|
|
410
|
+
|
|
411
|
+
error_feedback = (
|
|
412
|
+
"Dashboard rendering failed. Errors:\n"
|
|
413
|
+
f"{render_error_summary}"
|
|
414
|
+
)
|
|
415
|
+
if errors:
|
|
416
|
+
error_feedback += "\n\nDetailed errors:\n" + "\n".join(
|
|
417
|
+
format_playground_error(e) for e in errors[:5]
|
|
418
|
+
)
|
|
419
|
+
if tips:
|
|
420
|
+
error_feedback += "\n\nTips:\n" + "\n".join(tips)
|
|
421
|
+
error_feedback += "\n\nPlease fix these errors and regenerate the complete YAML. Use `execute_query` to discover table names if needed. Do not ask questions - fix the errors and return complete, valid YAML."
|
|
422
|
+
|
|
423
|
+
messages.append({"role": "user", "content": error_feedback})
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
# Render checks passed (or were disabled) - return the response
|
|
427
|
+
return {
|
|
428
|
+
"response": assistant_content,
|
|
429
|
+
"yaml": yaml_content,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# If we've done too many iterations, return the last YAML we saw with explanation
|
|
433
|
+
# Ask the agent to explain what happened
|
|
434
|
+
if last_yaml_content:
|
|
435
|
+
# One final attempt to get an explanation from the agent
|
|
436
|
+
messages.append(
|
|
437
|
+
{
|
|
438
|
+
"role": "user",
|
|
439
|
+
"content": "You've reached the maximum number of render attempts. Please return the YAML you've generated along with an explanation of what issues remain and why they couldn't be fixed. The user needs to see what you've created.",
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
# Get final response with explanation
|
|
445
|
+
api_kwargs = {
|
|
446
|
+
"model": "gpt-5.1",
|
|
447
|
+
"messages": messages,
|
|
448
|
+
"temperature": 0.7,
|
|
449
|
+
"max_tokens": 4000,
|
|
450
|
+
}
|
|
451
|
+
# Try adding reasoning if supported
|
|
452
|
+
api_kwargs["reasoning"] = {
|
|
453
|
+
"summary": "auto",
|
|
454
|
+
"effort": "medium",
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
response = self.client.chat.completions.create(**api_kwargs)
|
|
459
|
+
except (TypeError, ValueError) as e:
|
|
460
|
+
# If reasoning parameter is not supported, retry without it
|
|
461
|
+
error_str = str(e).lower()
|
|
462
|
+
if (
|
|
463
|
+
"reasoning" in error_str
|
|
464
|
+
or "unexpected keyword" in error_str
|
|
465
|
+
):
|
|
466
|
+
api_kwargs.pop("reasoning", None)
|
|
467
|
+
# Also try fallback model if gpt-5.1 doesn't exist
|
|
468
|
+
if api_kwargs.get("model") == "gpt-5.1":
|
|
469
|
+
api_kwargs["model"] = "gpt-4o"
|
|
470
|
+
response = self.client.chat.completions.create(**api_kwargs)
|
|
471
|
+
else:
|
|
472
|
+
raise
|
|
473
|
+
assistant_content = response.choices[0].message.content or ""
|
|
474
|
+
|
|
475
|
+
# Extract YAML if present, otherwise use last known YAML
|
|
476
|
+
final_yaml = (
|
|
477
|
+
self._extract_yaml(assistant_content) or last_yaml_content
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
"response": assistant_content
|
|
482
|
+
or "Reached maximum render iterations. Here's the YAML that was generated. Please review it manually.",
|
|
483
|
+
"yaml": final_yaml,
|
|
484
|
+
}
|
|
485
|
+
except Exception: # noqa: BLE001
|
|
486
|
+
# If final call fails, still return what we have
|
|
487
|
+
return {
|
|
488
|
+
"response": "Reached maximum render iterations. Here's the YAML that was generated. Please review it manually.",
|
|
489
|
+
"yaml": last_yaml_content,
|
|
490
|
+
}
|
|
491
|
+
else:
|
|
492
|
+
# No YAML was ever generated
|
|
493
|
+
return {
|
|
494
|
+
"response": "Reached maximum render iterations but no YAML was generated. Please try again with a different request.",
|
|
495
|
+
"yaml": None,
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
except Exception as e: # noqa: BLE001
|
|
499
|
+
return {
|
|
500
|
+
"response": f"Error: {str(e)}",
|
|
501
|
+
"yaml": None,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
def _extract_yaml(self, text: str) -> str | None:
|
|
505
|
+
"""Extract YAML code block from text.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
text: Text that may contain YAML code blocks
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Extracted YAML content or None
|
|
512
|
+
"""
|
|
513
|
+
# Use core utility for YAML extraction
|
|
514
|
+
return extract_yaml(text)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
# Global instance
|
|
518
|
+
_ai_service: AIService | None = None
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def get_ai_service() -> AIService:
|
|
522
|
+
"""Get global AI service instance."""
|
|
523
|
+
global _ai_service
|
|
524
|
+
if _ai_service is None:
|
|
525
|
+
_ai_service = AIService()
|
|
526
|
+
return _ai_service
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def reset_ai_service() -> None:
|
|
530
|
+
"""Reset the global AI service instance.
|
|
531
|
+
|
|
532
|
+
This is useful when environment variables change (e.g., after loading .env file).
|
|
533
|
+
"""
|
|
534
|
+
global _ai_service
|
|
535
|
+
_ai_service = None
|