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.
- google/cloud/db_context_enrichment/__init__.py +1 -0
- google/cloud/db_context_enrichment/bootstrap/__init__.py +1 -0
- google/cloud/db_context_enrichment/bootstrap/bootstrap_generator.py +74 -0
- google/cloud/db_context_enrichment/common/__init__.py +0 -0
- google/cloud/db_context_enrichment/common/config.py +10 -0
- google/cloud/db_context_enrichment/common/context_mutator.py +132 -0
- google/cloud/db_context_enrichment/common/parameterizer.py +192 -0
- google/cloud/db_context_enrichment/dataset/__init__.py +0 -0
- google/cloud/db_context_enrichment/dataset/dataset_generator.py +44 -0
- google/cloud/db_context_enrichment/evaluate/__init__.py +3 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/__init__.py +0 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/alloydb.py +72 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/base.py +76 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/mysql.py +69 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/postgres.py +69 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/spanner.py +62 -0
- google/cloud/db_context_enrichment/evaluate/evaluate_generator.py +239 -0
- google/cloud/db_context_enrichment/evaluate/result_reader.py +181 -0
- google/cloud/db_context_enrichment/facet/__init__.py +0 -0
- google/cloud/db_context_enrichment/facet/facet_generator.py +68 -0
- google/cloud/db_context_enrichment/main.py +450 -0
- google/cloud/db_context_enrichment/model/__init__.py +0 -0
- google/cloud/db_context_enrichment/model/context.py +86 -0
- google/cloud/db_context_enrichment/prompts/__init__.py +9 -0
- google/cloud/db_context_enrichment/prompts/targeted_facets.py +56 -0
- google/cloud/db_context_enrichment/prompts/targeted_templates.py +57 -0
- google/cloud/db_context_enrichment/prompts/targeted_value_search.py +93 -0
- google/cloud/db_context_enrichment/template/__init__.py +0 -0
- google/cloud/db_context_enrichment/template/template_generator.py +67 -0
- google/cloud/db_context_enrichment/value_search/__init__.py +0 -0
- google/cloud/db_context_enrichment/value_search/generator.py +91 -0
- google/cloud/db_context_enrichment/value_search/match_templates.py +267 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/METADATA +160 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/RECORD +38 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/WHEEL +5 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/entry_points.txt +2 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/licenses/LICENSE +202 -0
- 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)
|