| """Instance-aware synthetic few-shot generation (phase A3, CHASE-SQL 2410.01943). |
| |
| CHASE-SQL Table 4 credits instance-aware synthetic examples with the largest |
| single lever in the literature (+9.3 EA on Gemini): instead of retrieving |
| train-set Q→SQL pairs, one extra LLM call writes a few fresh pairs against |
| the *target* schema, mirroring the structural form of the target question. |
| The pairs replace the retrieved few-shots in the generate prompt. |
| |
| Default OFF — wired only when ``PipelineConfig.fewshot_selection == |
| "synthetic"`` and ``fewshot_synthesis_provider`` is set. The synthesis call |
| goes through the ordinary provider stack, so it is cached alongside |
| generation calls and replays on reruns. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from nl_sql.agent.nodes._text_utils import _safe_loads, _strip_code_fence |
| from nl_sql.agent.prompts import load_prompt |
| from nl_sql.llm.providers.base import GenerateRequest, LLMProvider |
| from nl_sql.schema_index.indexer import FewShotHit |
|
|
| MAX_SYNTHETIC_FEWSHOTS = 4 |
| _SYNTHESIS_MAX_TOKENS = 2048 |
|
|
|
|
| def parse_synthetic_pairs(text: str) -> list[tuple[str, str]]: |
| """Parse the model reply into ``(question, sql)`` pairs. |
| |
| Tolerates a fenced JSON block. Entries that are not objects with |
| non-empty string ``question``/``sql`` (where sql contains a SELECT) |
| are dropped; an unparseable reply yields ``[]`` so the caller can |
| fall back to the retrieved shots. |
| """ |
| data = _safe_loads(_strip_code_fence(text or "")) |
| if not isinstance(data, list): |
| return [] |
| pairs: list[tuple[str, str]] = [] |
| for item in data: |
| if not isinstance(item, dict): |
| continue |
| question = str(item.get("question") or "").strip() |
| sql = str(item.get("sql") or "").strip() |
| if not question or not sql or "select" not in sql.lower(): |
| continue |
| pairs.append((question, sql)) |
| if len(pairs) >= MAX_SYNTHETIC_FEWSHOTS: |
| break |
| return pairs |
|
|
|
|
| def synthesize_fewshots( |
| provider: LLMProvider, |
| *, |
| question: str, |
| db_id: str, |
| dialect: str, |
| schema_text: str, |
| num_examples: int = 3, |
| ) -> list[FewShotHit]: |
| """One LLM call → synthetic few-shot hits for the current question.""" |
| prompt = load_prompt( |
| "synthesize_fewshots", |
| dialect=dialect, |
| schema_block=schema_text, |
| question=question, |
| num_examples=num_examples, |
| ) |
| response = provider.generate( |
| GenerateRequest(prompt=prompt, max_tokens=_SYNTHESIS_MAX_TOKENS, temperature=0.0) |
| ) |
| return [ |
| FewShotHit( |
| example_id=f"synthetic-{idx}", |
| db_id=db_id, |
| question=q, |
| sql=sql, |
| distance=0.0, |
| metadata={"source": "synthetic"}, |
| ) |
| for idx, (q, sql) in enumerate(parse_synthetic_pairs(response.text), start=1) |
| ] |
|
|