Spaces:
Running
Running
File size: 19,671 Bytes
50efdc6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | """
src/tools/text_to_sql.py
Text-to-SQL: dd03l μ€ν€λ§ 컨ν
μ€νΈ κΈ°λ° SQLite κ²μ¦ 쿼리 μμ±
νλ¦:
1. LLM (worker_a_sql) β SQL μμ±
2. hardcoded fallback β LLM μ€ν¨ μ
"""
import logging
import os
import re
import sqlite3
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from src.config import get_config
logger = logging.getLogger(__name__)
from pathlib import Path as _Path
DB_PATH = str(_Path(__file__).resolve().parent.parent.parent / "data" / "sap_erp.db")
# ---------------------------------------------------------------------------
# SAP ABAP λμ
λ리 κΈ°λ° μ€ν€λ§ 컨ν
μ€νΈ
# ---------------------------------------------------------------------------
SCHEMA_CONTEXT = """\
Table: VBAK (Sales Order Header)
VBELN: Sales order number (PK, TEXT 10-digit zero-padded), KUNNR: Customer code, AUDAT: Order date
Table: VBAP (Sales Order Item)
VBELN: Sales order number (FK), POSNR: Item number (INTEGER), MATNR: Material number,
KWMENG: Requested quantity (REAL), NETPR: Net price (REAL), ARKTX: Short text for sales order item
Table: VBEP (Delivery Schedule)
VBELN: Sales order number, POSNR: Item number, EDATU: Delivery date (TEXT YYYYMMDD), WMENG: Scheduled delivery quantity
Table: VBUP (Item Status)
VBELN: Sales order number, POSNR: Item number,
WBSTA: Goods movement status (A=Not yet processed, B=Partially processed, C=Fully processed)
Table: MARD (Plant Stock)
MATNR: Material number, WERKS: Plant, LGORT: Storage location, LABST: Unrestricted-use stock (REAL)
NOTE: a material has many MARD rows (one per plant/storage location). "available stock"
for an order line means the stock at THAT line's plant (VBAP.WERKS), summed over its
storage locations β NOT the material's stock across every plant.
Table: MAKT (Material Descriptions)
MATNR: Material number, SPRAS: Language key, MAKTX: Material description
Notes:
- Database engine is SQLite.
- VBELN is stored inconsistently: some rows are 10-digit zero-padded ('0000015353'),
others are raw numbers ('6105'). NEVER match VBELN with plain '=' equality.
Always normalize both sides numerically:
CAST(vbap.VBELN AS INTEGER) = CAST('<order_id>' AS INTEGER)
- POSNR is an INTEGER (e.g. 10, 20). Match with CAST(vbap.POSNR AS INTEGER) = CAST('<item_no>' AS INTEGER).
- VBAP and VBUP hold exactly ONE row per (VBELN, POSNR) β the order line is unique.
- EDATU is already stored as TEXT in ISO 'YYYY-MM-DD' format. Output it as-is (do NOT reformat).
"""
# ---------------------------------------------------------------------------
# Prompt
# ---------------------------------------------------------------------------
_SYSTEM_PROMPT = """\
You are a Text-to-SQL expert for SAP ERP data stored in SQLite.
Given the schema below and a request, generate a single valid SQLite SELECT query.
βββββββββββββββββ HARD RULES (violating ANY of these = wrong answer) βββββββββββββββββ
1. Output ONLY the raw SQL β no markdown, no explanation, no code fences.
2. The result must be exactly ONE row. It MUST ALWAYS include AT LEAST these five
aliases (use these EXACT alias names) β they are required on every query:
material_name, quantity, delivery_status, delivery_date, available_stock
These five are the MINIMUM, not the maximum: when the request needs extra context
to be validated, you MAY add further read-only columns (e.g. confirmed/open
quantity, plant WERKS, schedule line, requested date) with clear aliases. Never
drop, rename, or reorder the five required aliases. Use LEFT JOINs so missing data
yields NULL/0, never zero rows.
3. CRITICAL β MARD, MAKT, VBEP, VBUP have MULTIPLE rows per material/item (one row per
storage location / language / schedule line / status record). Direct JOIN + LIMIT 1
picks an ARBITRARY (often NULL or conflicting) row. You MUST aggregate them in subqueries:
available_stock β SUM stock at the ORDER LINE'S OWN PLANT (VBAP.WERKS), then
COALESCE to 0. Group MARD by (MATNR, WERKS) and join on BOTH MATNR and WERKS,
so you sum only the storage locations of that plant β never every plant:
LEFT JOIN (SELECT MATNR, WERKS, SUM(LABST) AS total_stock
FROM MARD GROUP BY MATNR, WERKS) stock
ON stock.MATNR = vbap.MATNR AND stock.WERKS = vbap.WERKS
SELECT COALESCE(stock.total_stock, 0) AS available_stock
material_name β prefer English (SPRAS='E'), else any language:
LEFT JOIN (SELECT MATNR,
COALESCE(MAX(CASE WHEN SPRAS='E' THEN MAKTX END), MIN(MAKTX)) AS material_name
FROM MAKT GROUP BY MATNR) makt ON makt.MATNR = vbap.MATNR
delivery_date β earliest schedule line, MIN(EDATU); EDATU is already ISO
'YYYY-MM-DD', so output it unchanged:
LEFT JOIN (SELECT VBELN, POSNR, MIN(EDATU) AS delivery_date
FROM VBEP WHERE EDATU IS NOT NULL GROUP BY VBELN, POSNR) vbep
ON CAST(vbep.VBELN AS INTEGER) = CAST(vbap.VBELN AS INTEGER)
AND CAST(vbep.POSNR AS INTEGER) = CAST(vbap.POSNR AS INTEGER)
delivery_status β one item can have several VBUP rows, sometimes with CONFLICTING
WBSTA. Pick the most advanced status deterministically with MAX(WBSTA) (A < B < C β
conservative: prefer "shipped" so a partially/fully processed item isn't wrongly editable):
LEFT JOIN (SELECT CAST(VBELN AS INTEGER) AS v, CAST(POSNR AS INTEGER) AS p,
MAX(WBSTA) AS delivery_status
FROM VBUP GROUP BY v, p) vbup
ON vbup.v = CAST(vbap.VBELN AS INTEGER) AND vbup.p = CAST(vbap.POSNR AS INTEGER)
4. β οΈ MOST COMMON FAILURE β read carefully β οΈ
VBELN is stored INCONSISTENTLY in the DB: some rows are 10-digit zero-padded
('0000015353'), others are raw ('6105'). Plain string equality WILL fail and
return 0 rows for most inputs. You MUST normalize numerically on BOTH sides
of every VBELN comparison:
WHERE CAST(vbap.VBELN AS INTEGER) = CAST('<order_id>' AS INTEGER)
AND CAST(vbap.POSNR AS INTEGER) = CAST('<item_no>' AS INTEGER)
This applies to ALL VBELN/POSNR comparisons β WHERE clauses AND every
JOIN condition that uses VBELN or POSNR. Do NOT zero-pad the input yourself;
CAST handles it.
5. End with LIMIT 1 (with proper aggregation above, this only guards against
duplicate VBAP rows).
6. Use exact column and table names as shown in the schema.
βββββββββββββββββ EXAMPLES βββββββββββββββββ
β WRONG β this is what a careless LLM produces. It returns 0 rows because
VBELN is stored padded ('0000000040') but the request says '40', and MARD/MAKT/VBEP
are joined directly, picking arbitrary rows:
SELECT MAKT.MAKTX AS material_name, VBAP.KWMENG AS quantity,
VBUP.WBSTA AS delivery_status, VBEP.EDATU AS delivery_date,
COALESCE(MARD.LABST, 0) AS available_stock
FROM VBAP
LEFT JOIN MAKT ON VBAP.MATNR = MAKT.MATNR
LEFT JOIN VBUP ON VBAP.VBELN = VBUP.VBELN AND VBAP.POSNR = VBUP.POSNR
LEFT JOIN VBEP ON VBAP.VBELN = VBEP.VBELN AND VBAP.POSNR = VBEP.POSNR
LEFT JOIN MARD ON VBAP.MATNR = MARD.MATNR
WHERE VBAP.VBELN = '40' AND VBAP.POSNR = 10
LIMIT 1;
-- violations: rule 3 (no aggregation), rule 4 (no CAST on VBELN/POSNR)
β
CORRECT β aggregates MARD/MAKT/VBEP/VBUP in subqueries, scopes stock to the order's
plant (VBAP.WERKS), CASTs every VBELN/POSNR comparison to INTEGER, COALESCEs stock to 0:
SELECT
makt.material_name AS material_name,
vbap.KWMENG AS quantity,
vbup.delivery_status AS delivery_status,
vbep.delivery_date AS delivery_date,
COALESCE(stock.total_stock, 0) AS available_stock
FROM VBAP vbap
LEFT JOIN (
SELECT MATNR,
COALESCE(MAX(CASE WHEN SPRAS='E' THEN MAKTX END), MIN(MAKTX)) AS material_name
FROM MAKT GROUP BY MATNR
) makt ON makt.MATNR = vbap.MATNR
LEFT JOIN (
SELECT CAST(VBELN AS INTEGER) AS v, CAST(POSNR AS INTEGER) AS p,
MAX(WBSTA) AS delivery_status
FROM VBUP GROUP BY v, p
) vbup ON vbup.v = CAST(vbap.VBELN AS INTEGER)
AND vbup.p = CAST(vbap.POSNR AS INTEGER)
LEFT JOIN (
SELECT VBELN, POSNR, MIN(EDATU) AS delivery_date
FROM VBEP WHERE EDATU IS NOT NULL GROUP BY VBELN, POSNR
) vbep ON CAST(vbep.VBELN AS INTEGER) = CAST(vbap.VBELN AS INTEGER)
AND CAST(vbep.POSNR AS INTEGER) = CAST(vbap.POSNR AS INTEGER)
LEFT JOIN (
SELECT MATNR, WERKS, SUM(LABST) AS total_stock FROM MARD GROUP BY MATNR, WERKS
) stock ON stock.MATNR = vbap.MATNR AND stock.WERKS = vbap.WERKS
WHERE CAST(vbap.VBELN AS INTEGER) = CAST('40' AS INTEGER)
AND CAST(vbap.POSNR AS INTEGER) = CAST('10' AS INTEGER)
LIMIT 1;
Before answering, mentally check: did I CAST every VBELN/POSNR? Did I aggregate
MARD (by MATNR, WERKS), MAKT, VBEP? Did I join stock on BOTH MATNR and WERKS?
Did I COALESCE stock to 0? If any answer is "no", rewrite.
βββββββββββββββββ SCHEMA βββββββββββββββββ
{schema}
"""
_HUMAN_TEMPLATE = """\
Generate a SQLite SELECT query to validate the current state of a sales order item.
Parameters:
- VBELN (order_id): {order_id}
- POSNR (item_no) : {item_no}
Requested change to validate (decide whether any EXTRA columns are needed to check it;
the five required columns below must always be present regardless):
{request_context}
These columns are ALWAYS required in the result (use these exact aliases) β they are
the minimum, and you may add more read-only columns if the request needs extra context:
- material_name (MAKT.MAKTX β English preferred, else any language; see rule 3)
- quantity (from VBAP.KWMENG)
- delivery_status (from VBUP.WBSTA)
- delivery_date (earliest VBEP.EDATU, already ISO 'YYYY-MM-DD'; see rule 3)
- available_stock (SUM of MARD.LABST at the order's plant VBAP.WERKS, COALESCE to 0; see rule 3)
"""
_PROMPT = ChatPromptTemplate.from_messages([
("system", _SYSTEM_PROMPT),
("human", _HUMAN_TEMPLATE),
])
# ---------------------------------------------------------------------------
# LLM factory (OpenRouter)
# ---------------------------------------------------------------------------
def _build_openrouter_llm(model_name: str, temperature: float) -> ChatOpenAI:
api_key = os.getenv("OPENROUTER_API_KEY", "")
cfg = get_config()
return ChatOpenAI(
model=model_name,
temperature=temperature,
openai_api_key=api_key,
openai_api_base=cfg.openrouter.base_url,
default_headers={
"HTTP-Referer": "https://github.com/daisysooyeon/SAP-ERP-AI-Agent",
"X-Title": "SAP ERP AI Agent",
},
)
def _build_chain():
"""LLM 체μΈμ lazyνκ² μμ±"""
cfg = get_config()
sql_cfg = cfg.models.worker_a_sql
return _PROMPT | _build_openrouter_llm(sql_cfg.name, sql_cfg.temperature)
_chain = None
def _get_chain():
global _chain
if _chain is None:
_chain = _build_chain()
return _chain
# ---------------------------------------------------------------------------
# SQL μ μ ν¬νΌ
# ---------------------------------------------------------------------------
def _clean_sql(raw: str) -> str:
"""LLM μλ΅μμ λ§ν¬λ€μ΄ μ½λ λΈλ‘ μ κ±° ν SQLλ§ μΆμΆ"""
# ```sql ... ``` λλ ``` ... ``` μ κ±°
raw = re.sub(r"```(?:sql)?", "", raw, flags=re.IGNORECASE).strip()
# 첫 λ²μ§Έ SELECT ~ λ¬Έμ₯λ§ μΆμΆ
match = re.search(r"(SELECT\b.*)", raw, flags=re.IGNORECASE | re.DOTALL)
return match.group(1).strip() if match else raw.strip()
# ---------------------------------------------------------------------------
# Hardcoded fallback query
# ---------------------------------------------------------------------------
def _hardcoded_query(order_id: str, item_no: str) -> str:
# μ΄ μΏΌλ¦¬λ νκ°μ "μ λ΅(golden)"μ λ§λλ λ νΌλ°μ€μ΄κΈ°λ νλ€
# (generate_text2sql_dataset.pyκ° λμΌ ν¨μλ₯Ό νΈμΆ). λ°λΌμ ν둬ννΈκ° LLMμκ²
# μ§μνλ μλ―Έμ 1:1λ‘ μΌμΉν΄μΌ νλ€. κ²°μ μ (deterministic) μ μ:
# material_name : μμ΄(SPRAS='E') μ°μ , μμΌλ©΄ MIN(MAKTX)
# delivery_date : κ°μ₯ μ΄λ₯Έ μΌμ λΌμΈ MIN(EDATU) β EDATUλ μ΄λ―Έ ISO 'YYYY-MM-DD'
# available_stock : μ£Όλ¬Έ λΌμΈμ νλνΈ(VBAP.WERKS) νμ SUM(LABST), μμΌλ©΄ 0
# delivery_status : VBUPκ° (VBELN,POSNR)λΉ μ¬λ¬ ν(λλ‘ WBSTA μΆ©λ)μΌ μ μμ΄ MAX(WBSTA)λ‘
# μ§κ³ β μΆ©λ μ κ°μ₯ μ§νλ μν(A<B<C)λ₯Ό κ²°μ λ‘ μ ·보μμ μΌλ‘ μ ν.
# 보쑰 ν
μ΄λΈμ λͺ¨λ μλΈμΏΌλ¦¬λ‘ λΌμΈλΉ 1νμΌλ‘ μ μΌλ―λ‘ LIMIT 1μ μμ κ°λμΌ λΏμ΄λ€.
return f"""
SELECT
makt.material_name AS material_name,
vbap.KWMENG AS quantity,
vbup.delivery_status AS delivery_status,
vbep.delivery_date AS delivery_date,
COALESCE(stock.total_stock, 0) AS available_stock
FROM VBAP vbap
LEFT JOIN (
SELECT MATNR,
COALESCE(MAX(CASE WHEN SPRAS = 'E' THEN MAKTX END), MIN(MAKTX)) AS material_name
FROM MAKT GROUP BY MATNR
) makt ON makt.MATNR = vbap.MATNR
LEFT JOIN (
SELECT CAST(VBELN AS INTEGER) AS v, CAST(POSNR AS INTEGER) AS p,
MAX(WBSTA) AS delivery_status
FROM VBUP GROUP BY v, p
) vbup ON vbup.v = CAST(vbap.VBELN AS INTEGER)
AND vbup.p = CAST(vbap.POSNR AS INTEGER)
LEFT JOIN (
SELECT VBELN, POSNR, MIN(EDATU) AS delivery_date
FROM VBEP WHERE EDATU IS NOT NULL GROUP BY VBELN, POSNR
) vbep
ON CAST(vbep.VBELN AS INTEGER) = CAST(vbap.VBELN AS INTEGER)
AND CAST(vbep.POSNR AS INTEGER) = CAST(vbap.POSNR AS INTEGER)
LEFT JOIN (
SELECT MATNR, WERKS, SUM(LABST) AS total_stock FROM MARD GROUP BY MATNR, WERKS
) stock ON stock.MATNR = vbap.MATNR AND stock.WERKS = vbap.WERKS
WHERE CAST(vbap.VBELN AS INTEGER) = CAST('{order_id}' AS INTEGER)
AND CAST(vbap.POSNR AS INTEGER) = CAST('{item_no}' AS INTEGER)
LIMIT 1
"""
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def build_validation_query(
order_id: str,
item_no: str,
request_context: str | None = None,
) -> tuple[str, str]:
"""
LLM(primary β hardcoded) μμλ‘ SQLite κ²μ¦ 쿼리λ₯Ό μμ±νκ³ λ°ν.
Args:
request_context: μ΄λ€ μμ²μ κ²μ¦νλ €λμ§μ λν μ€λͺ
(μ‘μ
μλ). LLMμ΄ νμν
μΆκ° 컬λΌμ νλ¨νλ λ° μ°μΈλ€. Noneμ΄λ©΄ μΌλ° μν μ‘°νλ‘ μ²λ¦¬. νμ 5κ°
컬λΌμ 컨ν
μ€νΈμ 무κ΄νκ² νμ ν¬ν¨λλ€. hardcoded fallbackμ μ΄λ₯Ό 무μνλ€.
Returns:
(sql, strategy) β strategyλ λ€μ μ€ νλ:
"primary" : LLMμ΄ μ μμ μΌλ‘ SQLμ μμ±
"hardcoded_llm_fail" : LLM νΈμΆ/νμ± λ¨κ³μμ μμΈ β hardcoded fallback μ¬μ©
(run_validation_queryλ μ¬κΈ°μ λν΄ "hardcoded_zero_rows"λ₯Ό λ³λλ‘ μ¬μ©νλ€.)
"""
invoke_input = {
"schema": SCHEMA_CONTEXT,
"order_id": order_id,
"item_no": item_no,
"request_context": request_context or "(none β general status validation)",
}
chain = _get_chain()
# ββ 1. Primary LLM βββββββββββββββββββββββββββββββββββββββββββββββββββββ
try:
response = chain.invoke(invoke_input)
sql = _clean_sql(response.content)
logger.info("[text_to_sql] strategy=primary | LLM 쿼리 μμ± μ±κ³΅ (order=%s item=%s)",
order_id, item_no)
logger.debug("[text_to_sql] SQL:\n%s", sql)
return sql, "primary"
except Exception as e:
# LLM μ€ν¨λ μ΄μμ μ£Όλͺ©ν΄μΌ ν¨ β ERROR λ 벨 + λͺ
μμ λ§μ»€
logger.error(
"[text_to_sql] FALLBACK(llm_fail) | LLM νΈμΆ μ€ν¨ β hardcoded 쿼리 μ¬μ© "
"(order=%s item=%s) | reason=%s: %s",
order_id, item_no, type(e).__name__, e,
)
# ββ 2. Hardcoded fallback βββββββββββββββββββββββββββββββββββββββββββββββ
return _hardcoded_query(order_id, item_no), "hardcoded_llm_fail"
def _execute_query(sql: str) -> dict | None:
"""λ¨μΌ SQLμ μ€ννκ³ μ²« νμ dictλ‘ λ°ν (μκ±°λ μ€λ₯ μ None)."""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
try:
row = conn.execute(sql).fetchone()
return dict(row) if row else None
except sqlite3.Error as e:
logger.error("[text_to_sql] SQLite μ€λ₯: %s\nSQL:\n%s", e, sql)
return None
finally:
conn.close()
def run_validation_query(
order_id: str,
item_no: str,
request_context: str | None = None,
) -> dict | None:
"""
build_validation_query()λ‘ μμ±λ 쿼리λ₯Ό SQLiteμμ μ€ν, 첫 λ²μ§Έ νμ dictλ‘ λ°ν.
Primary(LLM) μΏΌλ¦¬κ° 0건μ λ°ννλ©΄ β LLMμ΄ VBELNμ μ격 λ§€μΉ(padded vs raw λΆμΌμΉ)
νλ κ²½μ°κ° μμΌλ―λ‘ β robust hardcoded μΏΌλ¦¬λ‘ ν λ² λ μ¬μλνλ€. λ λ€ μ€ν¨ μ None.
request_contextλ LLM SQL μμ±μλ§ μ λ¬λλ€(μΆκ° μ»¬λΌ νλ¨μ©). 0건 μ¬μλ μ μ°λ
hardcoded 쿼리λ κ²°μ λ‘ μ κ³ μ 쿼리μ΄λ―λ‘ μ»¨ν
μ€νΈμ μν₯μ λ°μ§ μλλ€.
fallbackμ΄ μΌμ΄λ¬λμ§ μ΄μμ μΆμ κ°λ₯νλλ‘, κ° κ²½λ‘μμ λͺ
μμ WARNING λ‘κ·Έλ₯Ό λ¨κΈ΄λ€.
"""
sql, strategy = build_validation_query(order_id, item_no, request_context=request_context)
result = _execute_query(sql)
if result is None and strategy == "primary":
# LLMμ μ μ νΈμΆλλλ° μ€ν κ²°κ³Όκ° 0건 β JOIN/WHERE κ·μΉ λλ½μ΄ μμ¬λ¨
logger.warning(
"[text_to_sql] FALLBACK(primary_zero_rows) | LLM SQLμ΄ 0건 λ°ν β "
"hardcoded μΏΌλ¦¬λ‘ μ¬μλ (order=%s item=%s)",
order_id, item_no,
)
logger.debug("[text_to_sql] 0-row primary SQL:\n%s", sql)
result = _execute_query(_hardcoded_query(order_id, item_no))
if result is not None:
logger.warning(
"[text_to_sql] FALLBACK(primary_zero_rows) RECOVERED | hardcoded μΏΌλ¦¬κ° "
"rowλ₯Ό λ°ν β LLM μΏΌλ¦¬κ° μλͺ» λ§λ€μ΄μ‘λ€λ κ°ν μ νΈ (order=%s item=%s)",
order_id, item_no,
)
else:
logger.warning(
"[text_to_sql] FALLBACK(primary_zero_rows) STILL_EMPTY | hardcoded 쿼리λ "
"0건 β μ€μ λ‘ DBμ λ°μ΄ν°κ° μμ κ°λ₯μ± (order=%s item=%s)",
order_id, item_no,
)
logger.info("[text_to_sql] 쿼리 μ€ν μλ£: strategy=%s result=%s", strategy, result)
return result
|