Spaces:
Running
Running
| """ | |
| 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 | |