Paul720810 commited on
Commit
1021a18
·
verified ·
1 Parent(s): f452661

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -66
app.py CHANGED
@@ -11,6 +11,7 @@ from llama_cpp import Llama
11
  from typing import List, Dict, Tuple, Optional
12
  import faiss
13
  from functools import lru_cache
 
14
 
15
  # 使用 transformers 替代 sentence-transformers
16
  from transformers import AutoModel, AutoTokenizer
@@ -44,48 +45,56 @@ def format_log(message: str, level: str = "INFO") -> str:
44
  return f"[{get_current_time()}] [{level.upper()}] {message}"
45
 
46
  def parse_sql_from_response(response_text: str) -> Optional[str]:
47
- """從模型輸出提取 SQL,增強版"""
48
  if not response_text:
49
  return None
50
 
51
- # 清理回應文本
52
- response_text = response_text.strip()
53
-
54
- # 1. 先找 ```sql ... ```
55
- match = re.search(r"```sql\s*\n(.*?)\n```", response_text, re.DOTALL | re.IGNORECASE)
56
- if match:
57
- return match.group(1).strip()
58
-
59
- # 2. 找任何 ``` 包圍的內容
60
- match = re.search(r"```\s*\n?(.*?)\n?```", response_text, re.DOTALL)
61
- if match:
62
- sql_candidate = match.group(1).strip()
63
- if sql_candidate.upper().startswith('SELECT'):
64
- return sql_candidate
65
-
66
- # 3. 找 SQL 語句(更寬鬆的匹配)
67
- match = re.search(r"(SELECT\s+.*?;)", response_text, re.DOTALL | re.IGNORECASE)
68
- if match:
69
- return match.group(1).strip()
70
-
71
- # 4. 找沒有分號的 SQL
72
- match = re.search(r"(SELECT\s+.*?)(?=\n\n|\n```|$|\n[^,\s])", response_text, re.DOTALL | re.IGNORECASE)
73
- if match:
74
- sql = match.group(1).strip()
75
- if not sql.endswith(';'):
76
- sql += ';'
77
- return sql
78
-
79
- # 5. 如果包含 SELECT,嘗試提取整行
80
- if 'SELECT' in response_text.upper():
81
- lines = response_text.split('\n')
82
- for line in lines:
83
- line = line.strip()
84
- if line.upper().startswith('SELECT'):
85
- if not line.endswith(';'):
86
- line += ';'
87
- return line
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  return None
90
 
91
  # ==================== Text-to-SQL 核心類 ====================
@@ -228,14 +237,14 @@ class TextToSQLSystem:
228
  return self._generate_fallback_sql(prompt)
229
 
230
  try:
 
231
  output = self.llm(
232
  prompt,
233
  max_tokens=350,
234
  temperature=0.05,
235
  top_p=0.9,
236
  echo=False,
237
- # --- 將 stop 參數加回來 ---
238
- stop=["```", ";", "\n\n", "</s>"],
239
  )
240
 
241
  self._log(f"🧠 模型原始輸出 (Raw Output): {output}", "DEBUG")
@@ -258,7 +267,7 @@ class TextToSQLSystem:
258
  # --- 清理邏輯結束 ---
259
  else:
260
  self._log("❌ 模型的原始輸出格式不正確或為空。", "ERROR")
261
- return ""
262
 
263
  except Exception as e:
264
  self._log(f"❌ 模型生成過程中發生嚴重錯誤: {e}", "CRITICAL")
@@ -505,7 +514,7 @@ class TextToSQLSystem:
505
  - 根據問題是關於「報告」還是「測試項目」來智能地決定計數目標。
506
  """
507
  q_lower = question.lower()
508
-
509
  # ==============================================================================
510
  # 第零層:統一實體識別引擎 (Unified Entity Recognition Engine)
511
  # ==============================================================================
@@ -517,40 +526,40 @@ class TextToSQLSystem:
517
  {'pattern': r"(申請方|申请方|申請廠商|申请厂商|applicant)\s*(?:id|代號|代碼|代号|代码)\s*'\"?\b([A-Z]\d{4}[A-Z])\b'\"?", 'column': 'sd.ApplicantID', 'type': '申請方ID'},
518
  {'pattern': r"(付款方|付款厂商|invoiceto)\s*(?:id|代號|代碼|代号|代码)\s*'\"?\b([A-Z]\d{4}[A-Z])\b'\"?", 'column': 'sd.InvoiceToID', 'type': '付款方ID'},
519
  {'pattern': r"(代理商|agent)\s*(?:id|代號|代碼|代号|代码)\s*'\"?\b([A-Z]\d{4}[A-Z])\b'\"?", 'column': 'sd.AgentID', 'type': '代理商ID'},
520
-
521
  # 模式2: 匹配 "類型 + 名稱" - (簡化了模式,使其更穩健)
522
  {'pattern': r"(買家|买家|buyer|客戶)\s+([a-zA-Z0-9&.-]+)", 'column': 'sd.BuyerName', 'type': '買家'},
523
  {'pattern': r"(申請方|申请方|申請廠商|申请厂商|applicant)\s+([a-zA-Z0-9&.-]+)", 'column': 'sd.ApplicantName', 'type': '申請方'},
524
  {'pattern': r"(付款方|付款厂商|invoiceto)\s+([a-zA-Z0-9&.-]+)", 'column': 'sd.InvoiceToName', 'type': '付款方'},
525
  {'pattern': r"(代理商|agent)\s+([a-zA-Z0-9&.-]+)", 'column': 'sd.AgentName', 'type': '代理商'},
526
-
527
  # 模式3: 单独匹配一个 ID - (保持不變)
528
  {'pattern': r"\b([A-Z]\d{4}[A-Z])\b", 'column': 'sd.ApplicantID', 'type': 'ID'}
529
  ]
530
-
531
  for p in entity_patterns:
532
  match = re.search(p['pattern'], question, re.IGNORECASE)
533
  if match:
534
  entity_value = match.group(2) if len(match.groups()) > 1 else match.group(1)
535
  entity_match_data = {
536
- "type": p['type'],
537
  "name": entity_value.strip().upper(),
538
  "column": p['column']
539
  }
540
  break
541
-
542
  # ==============================================================================
543
  # 第一層:模組化意圖偵測與動態SQL組合
544
  # ==============================================================================
545
-
546
  intents = {}
547
  sql_components = {
548
  'select': [], 'from': "", 'joins': [], 'where': [],
549
  'group_by': [], 'order_by': [], 'log_parts': []
550
  }
551
-
552
  # --- 運行一系列獨立的意圖偵測器 ---
553
-
554
  # 偵測器 2.1: 核心動作意圖
555
  if any(kw in q_lower for kw in ['幾份', '多少', '數量', '總數', 'how many', 'count']):
556
  intents['action'] = 'count'
@@ -566,7 +575,7 @@ class TextToSQLSystem:
566
  sql_components['select'].append("jt.JobNo, jt.ReportAuthorization")
567
  sql_components['order_by'].append("jt.ReportAuthorization DESC")
568
  sql_components['log_parts'].append("報告列表")
569
-
570
  # 偵測器 2.2: 時間意圖
571
  year_match = re.search(r'(\d{4})\s*年?', question)
572
  month_match = re.search(r'(\d{1,2})\s*月', question)
@@ -578,7 +587,7 @@ class TextToSQLSystem:
578
  month = month_match.group(1).zfill(2)
579
  sql_components['where'].append(f"strftime('%m', jt.ReportAuthorization) = '{month}'")
580
  sql_components['log_parts'].append(f"{month}月")
581
-
582
  # 偵測器 2.3: 實體意圖
583
  if entity_match_data:
584
  if "TSR53SampleDescription" not in " ".join(sql_components['joins']):
@@ -590,7 +599,7 @@ class TextToSQLSystem:
590
  sql_components['log_parts'].append(entity_match_data["type"] + ":" + entity_name)
591
  if intents.get('action') == 'list':
592
  sql_components['select'].append("sd.BuyerName")
593
-
594
  # 偵測器 2.4: 評級意圖
595
  if 'fail' in q_lower or '失敗' in q_lower:
596
  if "TSR53SampleDescription" not in " ".join(sql_components['joins']):
@@ -602,7 +611,7 @@ class TextToSQLSystem:
602
  sql_components['joins'].append("JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo")
603
  sql_components['where'].append("sd.OverallRating = 'Pass'")
604
  sql_components['log_parts'].append("Pass")
605
-
606
  # 偵測器 2.5: 實驗組 (LabGroup) 意圖 (帶有別名映射)
607
  lab_group_mapping = {'A': 'TA', 'B': 'TB', 'C': 'TC', 'D': 'TD', 'E': 'TE', 'Y': 'TY'}
608
  lab_group_match = re.search(r'([A-Z]{1,2})組', question, re.IGNORECASE)
@@ -612,54 +621,54 @@ class TextToSQLSystem:
612
  sql_components['joins'].append("JOIN JobItemsInProgress AS jip ON jt.JobNo = jip.JobNo")
613
  sql_components['where'].append(f"jip.LabGroup = '{db_lab_group}'")
614
  sql_components['log_parts'].append(f"{user_input_group}組(->{db_lab_group})")
615
-
616
  # --- 3. 判斷是否觸發了模板,並動態組合 SQL ---
617
  if 'action' in intents:
618
  sql_components['from'] = "FROM JobTimeline AS jt"
619
  # 只要有任何篩選條件,就加上報告已授權的基礎限制
620
  if sql_components['where']:
621
  sql_components['where'].insert(0, "jt.ReportAuthorization IS NOT NULL")
622
-
623
  select_clause = "SELECT " + ", ".join(sorted(list(set(sql_components['select']))))
624
  from_clause = sql_components['from']
625
  joins_clause = " ".join(sql_components['joins'])
626
  where_clause = "WHERE " + " AND ".join(sql_components['where']) if sql_components['where'] else ""
627
  orderby_clause = "ORDER BY " + ", ".join(sql_components['order_by']) if sql_components['order_by'] else ""
628
-
629
  template_sql = f"{select_clause} {from_clause} {joins_clause} {where_clause} {orderby_clause};"
630
-
631
  query_log = " ".join(sql_components['log_parts'])
632
  self._log(f"🔄 偵測到組合意圖【{query_log}】,啟用動態模板。", "INFO")
633
  return self._finalize_sql(template_sql, f"模板覆寫: {query_log} 查詢")
634
-
635
  # ==============================================================================
636
  # 第二层:AI 生成修正流程 (Fallback)
637
  # ==============================================================================
638
  self._log("未觸發任何模板,嘗試解析並修正 AI 輸出...", "INFO")
639
-
640
  parsed_sql = parse_sql_from_response(raw_response)
641
  if not parsed_sql:
642
  self._log(f"❌ 未能從模型回應中解析出任何 SQL。原始回應: {raw_response}", "ERROR")
643
  return None, f"無法解析SQL。原始回應:\n{raw_response}"
644
-
645
  self._log(f"📊 解析出的原始 SQL: {parsed_sql}", "DEBUG")
646
-
647
  fixed_sql = " " + parsed_sql.strip() + " "
648
  fixes_applied_fallback = []
649
-
650
  dialect_corrections = {r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)"}
651
  for pattern, replacement in dialect_corrections.items():
652
  if re.search(pattern, fixed_sql, re.IGNORECASE):
653
  fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
654
  fixes_applied_fallback.append(f"修正方言: {pattern}")
655
-
656
  schema_corrections = {'TSR53Report':'TSR53SampleDescription', 'TSR53InvoiceReportNo':'JobNo', 'TSR53ReportNo':'JobNo', 'TSR53InvoiceNo':'JobNo', 'TSR53InvoiceCreditNoteNo':'InvoiceCreditNoteNo', 'TSR53InvoiceLocalAmount':'LocalAmount', 'Status':'OverallRating', 'ReportStatus':'OverallRating'}
657
  for wrong, correct in schema_corrections.items():
658
  pattern = r'\b' + re.escape(wrong) + r'\b'
659
  if re.search(pattern, fixed_sql, re.IGNORECASE):
660
  fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
661
  fixes_applied_fallback.append(f"映射 Schema: '{wrong}' -> '{correct}'")
662
-
663
  log_msg = "AI 生成並成功修正" if fixes_applied_fallback else "AI 生成且無需修正"
664
  return self._finalize_sql(fixed_sql, log_msg)
665
 
@@ -830,7 +839,7 @@ Your single SQLite query response:
830
  # 2. 建立提示詞
831
  self._log("📝 建立 Prompt...")
832
  prompt = self._build_prompt(question, examples)
833
-
834
  # --- 新增:如果是第二次嘗試,加入修正指令 ---
835
  if attempt > 0:
836
  correction_prompt = "\nYour previous attempt failed because you did not provide a valid SQL query. REMEMBER: ONLY output the SQL code inside a ```sql block. DO NOT write comments or explanations.\nSQL:\n```sql\n"
 
11
  from typing import List, Dict, Tuple, Optional
12
  import faiss
13
  from functools import lru_cache
14
+ import re
15
 
16
  # 使用 transformers 替代 sentence-transformers
17
  from transformers import AutoModel, AutoTokenizer
 
45
  return f"[{get_current_time()}] [{level.upper()}] {message}"
46
 
47
  def parse_sql_from_response(response_text: str) -> Optional[str]:
48
+ """更健壯的 SQL 擷取 (multi-line 安全版)"""
49
  if not response_text:
50
  return None
51
 
52
+ text = response_text.strip()
53
+
54
+ # 1) 取得所有 ```sql / ``` 區塊,優先使用
55
+ code_blocks = re.findall(r"```(?:sql)?\s*\n([\s\S]*?)```", text, flags=re.IGNORECASE)
56
+ candidates = []
57
+ for block in code_blocks:
58
+ b = block.strip()
59
+ if 'select' in b.lower():
60
+ candidates.append(b)
61
+
62
+ # 2) 若無 code block,直接以正則抓第一個 SELECT...; 或到結尾
63
+ if not candidates:
64
+ m = re.search(r"SELECT\b[\s\S]*?(?:;|$)", text, flags=re.IGNORECASE)
65
+ if m:
66
+ candidates.append(m.group(0).strip())
67
+
68
+ if not candidates:
69
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ def clean(sql_raw: str) -> str:
72
+ # 去除註解行與多餘空白
73
+ lines = []
74
+ for line in sql_raw.split('\n'):
75
+ l = line.strip()
76
+ if not l:
77
+ continue
78
+ if l.startswith('--') or l.startswith('#'):
79
+ continue
80
+ lines.append(l)
81
+ sql_clean = ' '.join(lines)
82
+ # 移除多個反引號殘留
83
+ sql_clean = sql_clean.replace('```', '').strip()
84
+ # 若有多個分號只保留第一個前面內容後加單一分號
85
+ if ';' in sql_clean:
86
+ first_part = sql_clean.split(';')[0].strip()
87
+ sql_clean = first_part
88
+ if not sql_clean.lower().startswith('select'):
89
+ return ''
90
+ if not sql_clean.endswith(';'):
91
+ sql_clean += ';'
92
+ return sql_clean
93
+
94
+ for cand in candidates:
95
+ cleaned = clean(cand)
96
+ if cleaned:
97
+ return cleaned
98
  return None
99
 
100
  # ==================== Text-to-SQL 核心類 ====================
 
237
  return self._generate_fallback_sql(prompt)
238
 
239
  try:
240
+ # 重要: 移除 ";" 讓模型可輸出完整查詢(包含結尾分號前所有內容)
241
  output = self.llm(
242
  prompt,
243
  max_tokens=350,
244
  temperature=0.05,
245
  top_p=0.9,
246
  echo=False,
247
+ stop=["```", "\n\n", "</s>"]
 
248
  )
249
 
250
  self._log(f"🧠 模型原始輸出 (Raw Output): {output}", "DEBUG")
 
267
  # --- 清理邏輯結束 ---
268
  else:
269
  self._log("❌ 模型的原始輸出格式不正確或為空。", "ERROR")
270
+ return ""
271
 
272
  except Exception as e:
273
  self._log(f"❌ 模型生成過程中發生嚴重錯誤: {e}", "CRITICAL")
 
514
  - 根據問題是關於「報告」還是「測試項目」來智能地決定計數目標。
515
  """
516
  q_lower = question.lower()
517
+
518
  # ==============================================================================
519
  # 第零層:統一實體識別引擎 (Unified Entity Recognition Engine)
520
  # ==============================================================================
 
526
  {'pattern': r"(申請方|申请方|申請廠商|申请厂商|applicant)\s*(?:id|代號|代碼|代号|代码)\s*'\"?\b([A-Z]\d{4}[A-Z])\b'\"?", 'column': 'sd.ApplicantID', 'type': '申請方ID'},
527
  {'pattern': r"(付款方|付款厂商|invoiceto)\s*(?:id|代號|代碼|代号|代码)\s*'\"?\b([A-Z]\d{4}[A-Z])\b'\"?", 'column': 'sd.InvoiceToID', 'type': '付款方ID'},
528
  {'pattern': r"(代理商|agent)\s*(?:id|代號|代碼|代号|代码)\s*'\"?\b([A-Z]\d{4}[A-Z])\b'\"?", 'column': 'sd.AgentID', 'type': '代理商ID'},
529
+
530
  # 模式2: 匹配 "類型 + 名稱" - (簡化了模式,使其更穩健)
531
  {'pattern': r"(買家|买家|buyer|客戶)\s+([a-zA-Z0-9&.-]+)", 'column': 'sd.BuyerName', 'type': '買家'},
532
  {'pattern': r"(申請方|申请方|申請廠商|申请厂商|applicant)\s+([a-zA-Z0-9&.-]+)", 'column': 'sd.ApplicantName', 'type': '申請方'},
533
  {'pattern': r"(付款方|付款厂商|invoiceto)\s+([a-zA-Z0-9&.-]+)", 'column': 'sd.InvoiceToName', 'type': '付款方'},
534
  {'pattern': r"(代理商|agent)\s+([a-zA-Z0-9&.-]+)", 'column': 'sd.AgentName', 'type': '代理商'},
535
+
536
  # 模式3: 单独匹配一个 ID - (保持不變)
537
  {'pattern': r"\b([A-Z]\d{4}[A-Z])\b", 'column': 'sd.ApplicantID', 'type': 'ID'}
538
  ]
539
+
540
  for p in entity_patterns:
541
  match = re.search(p['pattern'], question, re.IGNORECASE)
542
  if match:
543
  entity_value = match.group(2) if len(match.groups()) > 1 else match.group(1)
544
  entity_match_data = {
545
+ "type": p['type'],
546
  "name": entity_value.strip().upper(),
547
  "column": p['column']
548
  }
549
  break
550
+
551
  # ==============================================================================
552
  # 第一層:模組化意圖偵測與動態SQL組合
553
  # ==============================================================================
554
+
555
  intents = {}
556
  sql_components = {
557
  'select': [], 'from': "", 'joins': [], 'where': [],
558
  'group_by': [], 'order_by': [], 'log_parts': []
559
  }
560
+
561
  # --- 運行一系列獨立的意圖偵測器 ---
562
+
563
  # 偵測器 2.1: 核心動作意圖
564
  if any(kw in q_lower for kw in ['幾份', '多少', '數量', '總數', 'how many', 'count']):
565
  intents['action'] = 'count'
 
575
  sql_components['select'].append("jt.JobNo, jt.ReportAuthorization")
576
  sql_components['order_by'].append("jt.ReportAuthorization DESC")
577
  sql_components['log_parts'].append("報告列表")
578
+
579
  # 偵測器 2.2: 時間意圖
580
  year_match = re.search(r'(\d{4})\s*年?', question)
581
  month_match = re.search(r'(\d{1,2})\s*月', question)
 
587
  month = month_match.group(1).zfill(2)
588
  sql_components['where'].append(f"strftime('%m', jt.ReportAuthorization) = '{month}'")
589
  sql_components['log_parts'].append(f"{month}月")
590
+
591
  # 偵測器 2.3: 實體意圖
592
  if entity_match_data:
593
  if "TSR53SampleDescription" not in " ".join(sql_components['joins']):
 
599
  sql_components['log_parts'].append(entity_match_data["type"] + ":" + entity_name)
600
  if intents.get('action') == 'list':
601
  sql_components['select'].append("sd.BuyerName")
602
+
603
  # 偵測器 2.4: 評級意圖
604
  if 'fail' in q_lower or '失敗' in q_lower:
605
  if "TSR53SampleDescription" not in " ".join(sql_components['joins']):
 
611
  sql_components['joins'].append("JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo")
612
  sql_components['where'].append("sd.OverallRating = 'Pass'")
613
  sql_components['log_parts'].append("Pass")
614
+
615
  # 偵測器 2.5: 實驗組 (LabGroup) 意圖 (帶有別名映射)
616
  lab_group_mapping = {'A': 'TA', 'B': 'TB', 'C': 'TC', 'D': 'TD', 'E': 'TE', 'Y': 'TY'}
617
  lab_group_match = re.search(r'([A-Z]{1,2})組', question, re.IGNORECASE)
 
621
  sql_components['joins'].append("JOIN JobItemsInProgress AS jip ON jt.JobNo = jip.JobNo")
622
  sql_components['where'].append(f"jip.LabGroup = '{db_lab_group}'")
623
  sql_components['log_parts'].append(f"{user_input_group}組(->{db_lab_group})")
624
+
625
  # --- 3. 判斷是否觸發了模板,並動態組合 SQL ---
626
  if 'action' in intents:
627
  sql_components['from'] = "FROM JobTimeline AS jt"
628
  # 只要有任何篩選條件,就加上報告已授權的基礎限制
629
  if sql_components['where']:
630
  sql_components['where'].insert(0, "jt.ReportAuthorization IS NOT NULL")
631
+
632
  select_clause = "SELECT " + ", ".join(sorted(list(set(sql_components['select']))))
633
  from_clause = sql_components['from']
634
  joins_clause = " ".join(sql_components['joins'])
635
  where_clause = "WHERE " + " AND ".join(sql_components['where']) if sql_components['where'] else ""
636
  orderby_clause = "ORDER BY " + ", ".join(sql_components['order_by']) if sql_components['order_by'] else ""
637
+
638
  template_sql = f"{select_clause} {from_clause} {joins_clause} {where_clause} {orderby_clause};"
639
+
640
  query_log = " ".join(sql_components['log_parts'])
641
  self._log(f"🔄 偵測到組合意圖【{query_log}】,啟用動態模板。", "INFO")
642
  return self._finalize_sql(template_sql, f"模板覆寫: {query_log} 查詢")
643
+
644
  # ==============================================================================
645
  # 第二层:AI 生成修正流程 (Fallback)
646
  # ==============================================================================
647
  self._log("未觸發任何模板,嘗試解析並修正 AI 輸出...", "INFO")
648
+
649
  parsed_sql = parse_sql_from_response(raw_response)
650
  if not parsed_sql:
651
  self._log(f"❌ 未能從模型回應中解析出任何 SQL。原始回應: {raw_response}", "ERROR")
652
  return None, f"無法解析SQL。原始回應:\n{raw_response}"
653
+
654
  self._log(f"📊 解析出的原始 SQL: {parsed_sql}", "DEBUG")
655
+
656
  fixed_sql = " " + parsed_sql.strip() + " "
657
  fixes_applied_fallback = []
658
+
659
  dialect_corrections = {r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)"}
660
  for pattern, replacement in dialect_corrections.items():
661
  if re.search(pattern, fixed_sql, re.IGNORECASE):
662
  fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
663
  fixes_applied_fallback.append(f"修正方言: {pattern}")
664
+
665
  schema_corrections = {'TSR53Report':'TSR53SampleDescription', 'TSR53InvoiceReportNo':'JobNo', 'TSR53ReportNo':'JobNo', 'TSR53InvoiceNo':'JobNo', 'TSR53InvoiceCreditNoteNo':'InvoiceCreditNoteNo', 'TSR53InvoiceLocalAmount':'LocalAmount', 'Status':'OverallRating', 'ReportStatus':'OverallRating'}
666
  for wrong, correct in schema_corrections.items():
667
  pattern = r'\b' + re.escape(wrong) + r'\b'
668
  if re.search(pattern, fixed_sql, re.IGNORECASE):
669
  fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
670
  fixes_applied_fallback.append(f"映射 Schema: '{wrong}' -> '{correct}'")
671
+
672
  log_msg = "AI 生成並成功修正" if fixes_applied_fallback else "AI 生成且無需修正"
673
  return self._finalize_sql(fixed_sql, log_msg)
674
 
 
839
  # 2. 建立提示詞
840
  self._log("📝 建立 Prompt...")
841
  prompt = self._build_prompt(question, examples)
842
+
843
  # --- 新增:如果是第二次嘗試,加入修正指令 ---
844
  if attempt > 0:
845
  correction_prompt = "\nYour previous attempt failed because you did not provide a valid SQL query. REMEMBER: ONLY output the SQL code inside a ```sql block. DO NOT write comments or explanations.\nSQL:\n```sql\n"