google-cloud-db-context-engineering 0.5.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.
Files changed (38) hide show
  1. google/cloud/db_context_enrichment/__init__.py +1 -0
  2. google/cloud/db_context_enrichment/bootstrap/__init__.py +1 -0
  3. google/cloud/db_context_enrichment/bootstrap/bootstrap_generator.py +74 -0
  4. google/cloud/db_context_enrichment/common/__init__.py +0 -0
  5. google/cloud/db_context_enrichment/common/config.py +10 -0
  6. google/cloud/db_context_enrichment/common/context_mutator.py +132 -0
  7. google/cloud/db_context_enrichment/common/parameterizer.py +192 -0
  8. google/cloud/db_context_enrichment/dataset/__init__.py +0 -0
  9. google/cloud/db_context_enrichment/dataset/dataset_generator.py +44 -0
  10. google/cloud/db_context_enrichment/evaluate/__init__.py +3 -0
  11. google/cloud/db_context_enrichment/evaluate/db_generators/__init__.py +0 -0
  12. google/cloud/db_context_enrichment/evaluate/db_generators/alloydb.py +72 -0
  13. google/cloud/db_context_enrichment/evaluate/db_generators/base.py +76 -0
  14. google/cloud/db_context_enrichment/evaluate/db_generators/mysql.py +69 -0
  15. google/cloud/db_context_enrichment/evaluate/db_generators/postgres.py +69 -0
  16. google/cloud/db_context_enrichment/evaluate/db_generators/spanner.py +62 -0
  17. google/cloud/db_context_enrichment/evaluate/evaluate_generator.py +239 -0
  18. google/cloud/db_context_enrichment/evaluate/result_reader.py +181 -0
  19. google/cloud/db_context_enrichment/facet/__init__.py +0 -0
  20. google/cloud/db_context_enrichment/facet/facet_generator.py +68 -0
  21. google/cloud/db_context_enrichment/main.py +450 -0
  22. google/cloud/db_context_enrichment/model/__init__.py +0 -0
  23. google/cloud/db_context_enrichment/model/context.py +86 -0
  24. google/cloud/db_context_enrichment/prompts/__init__.py +9 -0
  25. google/cloud/db_context_enrichment/prompts/targeted_facets.py +56 -0
  26. google/cloud/db_context_enrichment/prompts/targeted_templates.py +57 -0
  27. google/cloud/db_context_enrichment/prompts/targeted_value_search.py +93 -0
  28. google/cloud/db_context_enrichment/template/__init__.py +0 -0
  29. google/cloud/db_context_enrichment/template/template_generator.py +67 -0
  30. google/cloud/db_context_enrichment/value_search/__init__.py +0 -0
  31. google/cloud/db_context_enrichment/value_search/generator.py +91 -0
  32. google/cloud/db_context_enrichment/value_search/match_templates.py +267 -0
  33. google_cloud_db_context_engineering-0.5.1.dist-info/METADATA +160 -0
  34. google_cloud_db_context_engineering-0.5.1.dist-info/RECORD +38 -0
  35. google_cloud_db_context_engineering-0.5.1.dist-info/WHEEL +5 -0
  36. google_cloud_db_context_engineering-0.5.1.dist-info/entry_points.txt +2 -0
  37. google_cloud_db_context_engineering-0.5.1.dist-info/licenses/LICENSE +202 -0
  38. google_cloud_db_context_engineering-0.5.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,69 @@
1
+ from typing import Any
2
+
3
+ import google.cloud.geminidataanalytics_v1beta as gda
4
+ import yaml
5
+
6
+ from .base import BaseDBConfigGenerator
7
+
8
+
9
+ class MySQLConfigGenerator(BaseDBConfigGenerator):
10
+ """
11
+ Dedicated generator mapping properties to explicit Cloud SQL MySQL configuration
12
+ topologies utilized by both EvalBench binaries and GDA Context objects.
13
+ """
14
+
15
+ SOURCE_TYPE = "cloud-sql-mysql"
16
+ DIALECT = "mysql"
17
+ REQUIRED_FIELDS = BaseDBConfigGenerator.REQUIRED_FIELDS | {
18
+ "project",
19
+ "region",
20
+ "instance",
21
+ "database",
22
+ }
23
+
24
+ def __init__(self, params: dict[str, Any]):
25
+ super().__init__(params)
26
+ self.project = params.get("project")
27
+ self.region = params.get("region")
28
+ self.instance = params.get("instance")
29
+ self.database = params.get("database")
30
+ self.user = params.get("user")
31
+ self.password = params.get("password")
32
+
33
+ def generate_db_config(self) -> str:
34
+ db_type = "mysql"
35
+ db_path = f"{self.project}:{self.region}:{self.instance}"
36
+
37
+ db_config = {
38
+ "db_type": db_type,
39
+ "dialect": self.DIALECT,
40
+ "database_name": self.database,
41
+ "database_path": db_path,
42
+ "max_executions_per_minute": 180,
43
+ }
44
+ if self.user:
45
+ db_config["user_name"] = self.user
46
+ if self.password:
47
+ db_config["password"] = self.password
48
+ return yaml.safe_dump(
49
+ db_config, sort_keys=False, default_flow_style=False
50
+ ).strip()
51
+
52
+ def build_datasource_reference(
53
+ self, context_set_id: str
54
+ ) -> gda.DatasourceReferences:
55
+ datasource_ref = gda.DatasourceReferences()
56
+
57
+ datasource_ref.cloud_sql_reference = gda.CloudSqlReference(
58
+ database_reference=gda.CloudSqlDatabaseReference(
59
+ engine=gda.CloudSqlDatabaseReference.Engine.MYSQL,
60
+ project_id=self.project,
61
+ region=self.region,
62
+ instance_id=self.instance,
63
+ database_id=self.database,
64
+ ),
65
+ agent_context_reference=gda.AgentContextReference(
66
+ context_set_id=context_set_id
67
+ ),
68
+ )
69
+ return datasource_ref
@@ -0,0 +1,69 @@
1
+ from typing import Any
2
+
3
+ import google.cloud.geminidataanalytics_v1beta as gda
4
+ import yaml
5
+
6
+ from .base import BaseDBConfigGenerator
7
+
8
+
9
+ class PostgresConfigGenerator(BaseDBConfigGenerator):
10
+ """
11
+ Dedicated generator mapping properties to explicit Cloud SQL Postgres configuration
12
+ topologies utilized by both EvalBench binaries and GDA Context objects.
13
+ """
14
+
15
+ SOURCE_TYPE = "cloud-sql-postgres"
16
+ DIALECT = "postgres"
17
+ REQUIRED_FIELDS = BaseDBConfigGenerator.REQUIRED_FIELDS | {
18
+ "project",
19
+ "region",
20
+ "instance",
21
+ "database",
22
+ }
23
+
24
+ def __init__(self, params: dict[str, Any]):
25
+ super().__init__(params)
26
+ self.project = params.get("project")
27
+ self.region = params.get("region")
28
+ self.instance = params.get("instance")
29
+ self.database = params.get("database")
30
+ self.user = params.get("user")
31
+ self.password = params.get("password")
32
+
33
+ def generate_db_config(self) -> str:
34
+ db_type = "postgres"
35
+ db_path = f"{self.project}:{self.region}:{self.instance}"
36
+
37
+ db_config = {
38
+ "db_type": db_type,
39
+ "dialect": self.DIALECT,
40
+ "database_name": self.database,
41
+ "database_path": db_path,
42
+ "max_executions_per_minute": 180,
43
+ }
44
+ if self.user:
45
+ db_config["user_name"] = self.user
46
+ if self.password:
47
+ db_config["password"] = self.password
48
+ return yaml.safe_dump(
49
+ db_config, sort_keys=False, default_flow_style=False
50
+ ).strip()
51
+
52
+ def build_datasource_reference(
53
+ self, context_set_id: str
54
+ ) -> gda.DatasourceReferences:
55
+ datasource_ref = gda.DatasourceReferences()
56
+
57
+ datasource_ref.cloud_sql_reference = gda.CloudSqlReference(
58
+ database_reference=gda.CloudSqlDatabaseReference(
59
+ engine=gda.CloudSqlDatabaseReference.Engine.POSTGRESQL,
60
+ project_id=self.project,
61
+ region=self.region,
62
+ instance_id=self.instance,
63
+ database_id=self.database,
64
+ ),
65
+ agent_context_reference=gda.AgentContextReference(
66
+ context_set_id=context_set_id
67
+ ),
68
+ )
69
+ return datasource_ref
@@ -0,0 +1,62 @@
1
+ from typing import Any
2
+
3
+ import google.cloud.geminidataanalytics_v1beta as gda
4
+ import yaml
5
+
6
+ from .base import BaseDBConfigGenerator
7
+
8
+
9
+ class SpannerConfigGenerator(BaseDBConfigGenerator):
10
+ """
11
+ Dedicated generator mapping properties to explicit Spanner configuration
12
+ topologies utilized by both EvalBench binaries and GDA Context objects.
13
+ """
14
+
15
+ SOURCE_TYPE = "spanner"
16
+ DIALECT = "spanner_gsql"
17
+ REQUIRED_FIELDS = BaseDBConfigGenerator.REQUIRED_FIELDS | {
18
+ "project",
19
+ "instance",
20
+ "database",
21
+ }
22
+
23
+ def __init__(self, params: dict[str, Any]):
24
+ super().__init__(params)
25
+ self.project = params.get("project")
26
+ self.instance = params.get("instance")
27
+ self.database = params.get("database")
28
+
29
+ def generate_db_config(self) -> str:
30
+ db_type = "spanner"
31
+ db_path = f"projects/{self.project}/instances/{self.instance}/databases/{self.database}"
32
+
33
+ db_config = {
34
+ "db_type": db_type,
35
+ "dialect": self.DIALECT,
36
+ "database_name": self.database,
37
+ "database_path": db_path,
38
+ "instance_id": self.instance,
39
+ "gcp_project_id": self.project,
40
+ "max_executions_per_minute": 100,
41
+ }
42
+ return yaml.safe_dump(
43
+ db_config, sort_keys=False, default_flow_style=False
44
+ ).strip()
45
+
46
+ def build_datasource_reference(
47
+ self, context_set_id: str
48
+ ) -> gda.DatasourceReferences:
49
+ datasource_ref = gda.DatasourceReferences()
50
+
51
+ datasource_ref.spanner_reference = gda.SpannerReference(
52
+ database_reference=gda.SpannerDatabaseReference(
53
+ engine=gda.SpannerDatabaseReference.Engine.GOOGLE_SQL,
54
+ project_id=self.project,
55
+ instance_id=self.instance,
56
+ database_id=self.database,
57
+ ),
58
+ agent_context_reference=gda.AgentContextReference(
59
+ context_set_id=context_set_id
60
+ ),
61
+ )
62
+ return datasource_ref
@@ -0,0 +1,239 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import textwrap
5
+ from typing import Any
6
+
7
+ import yaml
8
+
9
+ from google.cloud.db_context_enrichment.common import config
10
+
11
+ from .db_generators.alloydb import AlloyDBConfigGenerator
12
+ from .db_generators.base import BaseDBConfigGenerator
13
+ from .db_generators.mysql import MySQLConfigGenerator
14
+ from .db_generators.postgres import PostgresConfigGenerator
15
+ from .db_generators.spanner import SpannerConfigGenerator
16
+
17
+ # Constants for EvalBench configuration filenames
18
+ DB_CONFIG_NAME = "db_config.yaml"
19
+ MODEL_CONFIG_NAME = "model_config.yaml"
20
+ RUN_CONFIG_NAME = "run_config.yaml"
21
+ LLMRATER_CONFIG_NAME = "llmrater_config.yaml"
22
+ GOLDEN_QUERIES_NAME = "golden_queries.json"
23
+
24
+
25
+ def generate_evalbench_configs(
26
+ experiment_name: str,
27
+ dataset_path: str,
28
+ context_set_id: str,
29
+ toolbox_config_path: str,
30
+ toolbox_source_name: str,
31
+ ) -> None:
32
+ """
33
+ Main entrypoint: Generates Evalbench-compatible YAML configurations natively using
34
+ private DB format converters and the google-cloud-geminidataanalytics API validations.
35
+ """
36
+ params = _extract_toolbox_params(toolbox_config_path, toolbox_source_name)
37
+ generator = _get_db_generator(params)
38
+
39
+ db_config_yaml = generator.generate_db_config()
40
+ model_config_yaml = generator.generate_model_config(context_set_id)
41
+ llmrater_config_yaml = _generate_llmrater_config(params.get("project"))
42
+ run_config_yaml = _generate_run_config(experiment_name, generator.DIALECT)
43
+
44
+ # Convert simplified dataset to EvalBench standard format
45
+ golden_queries_json = _convert_dataset(dataset_path, generator.DIALECT)
46
+
47
+ # Write all files directly
48
+ eval_configs_dir = f"autoctx/experiments/{experiment_name}/eval_configs"
49
+ os.makedirs(eval_configs_dir, exist_ok=True)
50
+
51
+ with open(os.path.join(eval_configs_dir, DB_CONFIG_NAME), "w") as f:
52
+ f.write(db_config_yaml)
53
+
54
+ with open(os.path.join(eval_configs_dir, MODEL_CONFIG_NAME), "w") as f:
55
+ f.write(model_config_yaml)
56
+
57
+ with open(os.path.join(eval_configs_dir, RUN_CONFIG_NAME), "w") as f:
58
+ f.write(run_config_yaml)
59
+
60
+ with open(os.path.join(eval_configs_dir, LLMRATER_CONFIG_NAME), "w") as f:
61
+ f.write(llmrater_config_yaml)
62
+
63
+ with open(os.path.join(eval_configs_dir, GOLDEN_QUERIES_NAME), "w") as f:
64
+ f.write(golden_queries_json)
65
+
66
+
67
+ def _extract_toolbox_params(
68
+ toolbox_config_path: str, toolbox_source_name: str
69
+ ) -> dict[str, Any]:
70
+ """Deterministically extracts connection parameters for a specific database source from tools.yaml."""
71
+ try:
72
+ with open(toolbox_config_path) as f:
73
+ content = f.read()
74
+ interpolated = _interpolate_env_vars(content)
75
+ docs = yaml.safe_load_all(interpolated)
76
+ for doc in docs:
77
+ if not doc:
78
+ continue
79
+ if (
80
+ doc.get("kind") == "source"
81
+ and doc.get("name") == toolbox_source_name
82
+ ):
83
+ if not doc.get("type"):
84
+ raise ValueError(
85
+ f"Selected source '{toolbox_source_name}' is missing the 'type' field."
86
+ )
87
+ return doc
88
+
89
+ raise ValueError(
90
+ f"Could not find a 'kind: source' named '{toolbox_source_name}' in {toolbox_config_path}"
91
+ )
92
+
93
+ except FileNotFoundError:
94
+ raise ValueError(f"Config file not found: {toolbox_config_path}")
95
+ except PermissionError:
96
+ raise ValueError(
97
+ f"Permission denied reading config file: {toolbox_config_path}"
98
+ )
99
+ except yaml.YAMLError as e:
100
+ raise ValueError(f"Failed to parse {toolbox_config_path} as YAML: {e}")
101
+
102
+
103
+ def _interpolate_env_vars(raw_yaml: str) -> str:
104
+ """Replaces ${ENV_NAME} or ${ENV_NAME:default_value} with environment variables."""
105
+ # Matches ${VAR_NAME} or ${VAR_NAME:fallback}
106
+ pattern = re.compile(r"\$\{(\w+)(?::([^}]*))?\}")
107
+
108
+ def replacer(match):
109
+ var_name = match.group(1)
110
+ fallback = match.group(2)
111
+
112
+ if var_name in os.environ:
113
+ return os.environ[var_name]
114
+ if fallback is not None:
115
+ return fallback
116
+ raise ValueError(
117
+ f"Environment variable '{var_name}' not found and no default provided."
118
+ )
119
+
120
+ return pattern.sub(replacer, raw_yaml)
121
+
122
+
123
+ def _get_db_generator(params: dict[str, Any]) -> BaseDBConfigGenerator:
124
+ """Factory function to build the correct Evaluation Generator."""
125
+ source_type = params.get("type", "").lower()
126
+
127
+ generators = {
128
+ AlloyDBConfigGenerator.SOURCE_TYPE: AlloyDBConfigGenerator,
129
+ PostgresConfigGenerator.SOURCE_TYPE: PostgresConfigGenerator,
130
+ MySQLConfigGenerator.SOURCE_TYPE: MySQLConfigGenerator,
131
+ SpannerConfigGenerator.SOURCE_TYPE: SpannerConfigGenerator,
132
+ }
133
+
134
+ if source_type not in generators:
135
+ supported = ", ".join(generators.keys())
136
+ raise ValueError(
137
+ f"Unsupported evaluating toolbox source type: '{source_type}'. Must be one of: {supported}"
138
+ )
139
+
140
+ return generators[source_type](params)
141
+
142
+
143
+ def _generate_run_config(experiment_name: str, dialect: str) -> str:
144
+ """Generates the main EvalBench Run Experiment scaffolding."""
145
+ configs_dir = f"autoctx/experiments/{experiment_name}/eval_configs"
146
+ reports_dir = f"autoctx/experiments/{experiment_name}/eval_reports"
147
+
148
+ return textwrap.dedent(f"""\
149
+ ############################################################
150
+ ### Dataset / Eval Items
151
+ ############################################################
152
+ dataset_config: {configs_dir}/{GOLDEN_QUERIES_NAME}
153
+ dataset_format: evalbench-standard-format
154
+ database_configs:
155
+ - {configs_dir}/{DB_CONFIG_NAME}
156
+ dialect: {dialect} # DB connection mapping
157
+ query_types:
158
+ - dql
159
+
160
+ ############################################################
161
+ ### Prompt and Generation Modules
162
+ ############################################################
163
+ model_config: {configs_dir}/{MODEL_CONFIG_NAME}
164
+ prompt_generator: 'NOOPGenerator'
165
+
166
+ ############################################################
167
+ ### Evaluator Execution / Parallelism Tuning
168
+ ############################################################
169
+ runners:
170
+ eval_runners: 4
171
+ sqlgen_runners: 20
172
+
173
+ ############################################################
174
+ ### Scorer Related Configs
175
+ ############################################################
176
+ scorers:
177
+ llmrater:
178
+ model_config: {configs_dir}/{LLMRATER_CONFIG_NAME}
179
+
180
+ ############################################################
181
+ ### Reporting Related Configs
182
+ ############################################################
183
+ reporting:
184
+ csv:
185
+ output_directory: '{reports_dir}/'
186
+ """).strip()
187
+
188
+
189
+ def _generate_llmrater_config(project_id: str) -> str:
190
+ """Generates a dedicated LLM rater model configuration mimicking standard text models."""
191
+ return textwrap.dedent(f"""\
192
+ generator: gcp_vertex_gemini
193
+ vertex_model: {config.get_model_name()}
194
+ gcp_project_id: {project_id}
195
+ gcp_region: global
196
+ base_prompt: ""
197
+ execs_per_minute: 20
198
+ """).strip()
199
+
200
+
201
+ def _convert_dataset(dataset_path: str, dialect: str) -> str:
202
+ """Reads simplified dataset and converts to EvalBench standard format."""
203
+ try:
204
+ with open(dataset_path) as f:
205
+ data = json.load(f)
206
+
207
+ if not isinstance(data, list):
208
+ raise ValueError("Dataset must be a JSON list.")
209
+
210
+ required_keys = {"id", "nlq", "database", "golden_sql"}
211
+ for i, entry in enumerate(data):
212
+ if not isinstance(entry, dict):
213
+ raise ValueError(f"Dataset entry at index {i} is not a dictionary.")
214
+ missing = required_keys - set(entry.keys())
215
+ if missing:
216
+ raise ValueError(
217
+ f"Dataset entry at index {i} is missing required keys: {missing}"
218
+ )
219
+
220
+ converted = []
221
+ for entry in data:
222
+ converted_entry = {
223
+ "id": entry.get("id"),
224
+ "nl_prompt": entry.get("nlq"),
225
+ "query_type": "DQL",
226
+ "database": entry.get("database"),
227
+ "dialects": [dialect],
228
+ "golden_sql": {dialect: [entry.get("golden_sql")]},
229
+ "eval_query": {},
230
+ "setup_sql": {},
231
+ "cleanup_sql": {},
232
+ "other": {},
233
+ "tags": [],
234
+ }
235
+ converted.append(converted_entry)
236
+
237
+ return json.dumps(converted, indent=2)
238
+ except Exception as e:
239
+ raise ValueError(f"Failed to convert dataset at {dataset_path}: {e}")
@@ -0,0 +1,181 @@
1
+ import csv
2
+ import os
3
+ import re
4
+ import textwrap
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class ScoreRecord:
10
+ id: str
11
+ score: float
12
+ comparison_logs: str = "N/A"
13
+
14
+
15
+ @dataclass
16
+ class EvalRecord:
17
+ id: str
18
+ nl_prompt: str = "N/A"
19
+ golden_sql: str = "N/A"
20
+ generated_sql: str = "N/A"
21
+ sql_generator_error: str = "N/A"
22
+ generated_error: str = "N/A"
23
+ other: str = "N/A"
24
+
25
+
26
+ def read_eval_results(
27
+ run_folder_path: str, offset: int = 0, batch_size: int = 10
28
+ ) -> str:
29
+ """
30
+ Reads evaluation results from a folder and produces a markdown summary.
31
+
32
+ Args:
33
+ run_folder_path: The absolute path to the evaluation run result folder, which ends with the eval run job id.
34
+ offset: Offset to start reading failure cases from (default: 0).
35
+ batch_size: Number of failure cases to show in the report (default: 10).
36
+
37
+ Returns:
38
+ A string in markdown format containing the summary and failure cases.
39
+ """
40
+ scores_path = os.path.join(run_folder_path, "scores.csv")
41
+ evals_path = os.path.join(run_folder_path, "evals.csv")
42
+
43
+ # Read Summary
44
+ summary_md = _read_summary(run_folder_path)
45
+
46
+ # Read Scores to find failures
47
+ with open(scores_path, encoding="utf-8") as f:
48
+ failures = sorted(
49
+ [
50
+ ScoreRecord(
51
+ id=row["id"],
52
+ score=score,
53
+ comparison_logs=row.get("comparison_logs") or "N/A",
54
+ )
55
+ for row in csv.DictReader(f)
56
+ if (score := _get_score(row)) is not None and score < 100
57
+ ],
58
+ key=lambda x: _natural_sort_key(x.id),
59
+ )
60
+
61
+ if not failures:
62
+ return summary_md + "No failure cases found (all passed)."
63
+
64
+ # Apply batching
65
+ batched_failures = failures[offset : offset + batch_size]
66
+
67
+ # Add failures info to summary
68
+ summary_md += textwrap.dedent(
69
+ f"""\
70
+ ## All Failures
71
+ {", ".join([str(f.id) for f in failures])}
72
+
73
+ **Showing failures**: {offset + 1} to {min(offset + batch_size, len(failures))} of {len(failures)}
74
+
75
+ """
76
+ )
77
+
78
+ # Read Evals to get prompts and golden SQL
79
+ with open(evals_path, encoding="utf-8") as f:
80
+ evals_data = {
81
+ row["id"]: EvalRecord(
82
+ id=row["id"],
83
+ nl_prompt=row.get("nl_prompt") or "N/A",
84
+ golden_sql=row.get("golden_sql") or "N/A",
85
+ generated_sql=row.get("generated_sql") or "N/A",
86
+ sql_generator_error=row.get("sql_generator_error") or "N/A",
87
+ generated_error=row.get("generated_error") or "N/A",
88
+ other=row.get("other") or "N/A",
89
+ )
90
+ for row in csv.DictReader(f)
91
+ }
92
+
93
+ # Format Failures
94
+ failures_md = _format_failures(batched_failures, evals_data)
95
+
96
+ return summary_md + failures_md
97
+
98
+
99
+ def _format_failures(
100
+ failures: list[ScoreRecord], evals_data: dict[str, EvalRecord]
101
+ ) -> str:
102
+ failures_md = "# Failure Cases\n\n"
103
+ for fail in failures:
104
+ fail_id = fail.id
105
+ eval_info = evals_data.get(fail_id, EvalRecord(id=fail_id))
106
+
107
+ failures_md += textwrap.dedent(
108
+ f"""\
109
+ ## Case ID: {fail_id} (Score: {fail.score})
110
+
111
+ **Prompt**:
112
+ {eval_info.nl_prompt}
113
+
114
+ **Golden SQL**:
115
+ ```sql
116
+ {eval_info.golden_sql}
117
+ ```
118
+
119
+ **Generated SQL**:
120
+ ```sql
121
+ {eval_info.generated_sql}
122
+ ```
123
+
124
+ **SQL Generator Error** (Errors during SQL generation):
125
+ ```
126
+ {eval_info.sql_generator_error}
127
+ ```
128
+
129
+ **Execution Error** (Errors when executing the generated SQL):
130
+ ```
131
+ {eval_info.generated_error}
132
+ ```
133
+
134
+ **Additional Output**:
135
+ ```
136
+ {eval_info.other}
137
+ ```
138
+
139
+ **Evaluation Details**:
140
+ {fail.comparison_logs}
141
+
142
+ ---
143
+
144
+ """
145
+ )
146
+ return failures_md
147
+
148
+
149
+ def _read_summary(run_folder_path: str) -> str:
150
+ summary_path = os.path.join(run_folder_path, "summary.csv")
151
+ summary_md = "# Evaluation Summary\n\n"
152
+ with open(summary_path, encoding="utf-8") as f:
153
+ reader = csv.DictReader(f)
154
+ for row in reader:
155
+ summary_md += textwrap.dedent(
156
+ f"""\
157
+ - **Metric**: {row.get("metric_name", "N/A")}
158
+ - **Correct / Total**: {row.get("correct_results_count", "N/A")}/{row.get("total_results_count", "N/A")}
159
+ - **Run Time**: {row.get("run_time", "N/A")}
160
+
161
+ """
162
+ )
163
+ return summary_md
164
+
165
+
166
+ def _natural_sort_key(val):
167
+ """
168
+ Splits a string into a list of string and integer chunks.
169
+ Example: "user10" -> ["user", 10, ""]
170
+ """
171
+ return [
172
+ int(text) if text.isdigit() else text.lower()
173
+ for text in re.split(r"(\d+)", str(val))
174
+ ]
175
+
176
+
177
+ def _get_score(row: dict[str, str]) -> float | None:
178
+ try:
179
+ return float(row.get("score", ""))
180
+ except ValueError:
181
+ return None
File without changes
@@ -0,0 +1,68 @@
1
+ import json
2
+
3
+ from google.cloud.db_context_enrichment.common import parameterizer
4
+ from google.cloud.db_context_enrichment.model import context
5
+
6
+
7
+ async def generate_facets(
8
+ facet_inputs_json: str, sql_dialect: str = "postgresql"
9
+ ) -> str:
10
+ """
11
+ Generates the final, detailed facets based on user-approved items.
12
+ """
13
+ try:
14
+ # Convert the string to the Enum member
15
+ db_dialect = parameterizer.SQLDialect(sql_dialect)
16
+ except ValueError:
17
+ return f'{{"error": "Invalid database dialect specified: {sql_dialect}"}}'
18
+
19
+ try:
20
+ # The input is expected to be a direct list of items
21
+ item_list = json.loads(facet_inputs_json)
22
+ if not isinstance(item_list, list):
23
+ raise json.JSONDecodeError("Input is not a list.", facet_inputs_json, 0)
24
+ except json.JSONDecodeError:
25
+ return '{"error": "Invalid JSON format for approved items. Expected a JSON array."}'
26
+
27
+ final_facets = []
28
+
29
+ for item in item_list:
30
+ sql_snippet = item.get("sql_snippet")
31
+ if not sql_snippet:
32
+ return '{"error": "Each item must have a \'sql_snippet\' key."}'
33
+
34
+ intent = item.get("intent")
35
+ if not intent:
36
+ return '{"error": "Each item must have an \'intent\' key."}'
37
+
38
+ # 1. Extract value phrases from the intent (used as nl_query)
39
+ phrases = await parameterizer.extract_value_phrases(nl_query=intent)
40
+
41
+ # 2. Generate the manifest
42
+ manifest = intent
43
+ # Sort keys by length descending to replace longer phrases first
44
+ sorted_phrases = sorted(phrases.keys(), key=len, reverse=True)
45
+ for phrase in sorted_phrases:
46
+ # Use the first identified type for the manifest
47
+ phrase_type = phrases[phrase][0] if phrases[phrase] else "value"
48
+ manifest = manifest.replace(phrase, f"a given {phrase_type}")
49
+
50
+ # 3. Parameterize the SQL and Intent
51
+ parameterized_result = parameterizer.parameterize_sql_and_intent(
52
+ phrases, sql_snippet, intent, db_dialect=db_dialect
53
+ )
54
+
55
+ # 4. Assemble the final facet object
56
+ facet = context.Facet(
57
+ sql_snippet=sql_snippet,
58
+ intent=intent,
59
+ manifest=manifest,
60
+ parameterized=context.ParameterizedFacet(
61
+ parameterized_sql_snippet=parameterized_result["sql"],
62
+ parameterized_intent=parameterized_result["intent"],
63
+ ),
64
+ )
65
+ final_facets.append(facet)
66
+
67
+ context_set = context.ContextSet(templates=None, facets=final_facets)
68
+ return context_set.model_dump_json(indent=2, exclude_none=True)