Paul720810 commited on
Commit
4799a74
·
verified ·
1 Parent(s): 7371ddd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -90
app.py CHANGED
@@ -481,49 +481,43 @@ class TextToSQLSystem:
481
 
482
  return formatted.strip()
483
 
484
-
485
- # in class TextToSQLSystem:
486
-
487
  # in class TextToSQLSystem:
488
 
489
- def _validate_and_fix_sql(self, question: str, raw_response: str) -> Tuple[Optional[str], str]:
490
  """
491
- (V17 / 最終決策版)
492
  一個全面、多層次的 SQL 驗證與生成引擎。
493
- 本函數作為第一決策者,優先匹配用戶問題與專家知識庫。
494
- 如果匹配成功,則直接使用模板覆寫;若不成功,才解析並修正 AI 的輸出。
495
- 返回一個元組 (SQL字符串或None, 狀態消息)。
496
  """
497
- q_lower = question.lower()
 
 
498
 
 
 
 
 
 
499
  # ==============================================================================
500
  # 第一層:高價值意圖識別與模板覆寫 (Intent Recognition & Templating)
501
  # ==============================================================================
502
-
503
  # --- 預先檢測所有可能的意圖和實體 ---
504
  job_no_match = re.search(r"(?:工單|jobno)\s*'\"?([A-Z]{2,3}\d+)'\"?", question, re.IGNORECASE)
505
-
506
  entity_match_data = None
507
- ENTITY_TO_COLUMN_MAP = {
508
- '申請廠商': 'sd.ApplicantName', '申請方': 'sd.ApplicantName', 'applicant': 'sd.ApplicantName',
509
- '付款廠商': 'sd.InvoiceToName', '付款方': 'sd.InvoiceToName', 'invoiceto': 'sd.InvoiceToName',
510
- '代理商': 'sd.AgentName', 'agent': 'sd.AgentName',
511
- '買家': 'sd.BuyerName', 'buyer': 'sd.BuyerName', '客戶': 'sd.BuyerName', '品牌': 'tsr.BuyerName'
512
- }
513
  for keyword, column in ENTITY_TO_COLUMN_MAP.items():
514
  if keyword in q_lower:
515
  match = re.search(fr"{re.escape(keyword)}[\s:;\'\"-]*([a-zA-Z0-9&.\s-]+?)(?:\s*的|\s+|為|$)", question, re.IGNORECASE)
516
- if match:
517
- entity_match_data = {"type": keyword, "name": match.group(1).strip(), "column": column}
518
- break
519
-
520
  lab_group_match_data = None
521
  LAB_GROUP_MAP = {'A':'TA','B':'TB','C':'TC','D':'TD','E':'TE','Y':'TY','TA':'TA','TB':'TB','TC':'TC','TD':'TD','TE':'TE','TY':'TY','WC':'WC','EO':'EO','GCI':'GCI','GCO':'GCO','MI':'MI'}
522
  lab_group_match = re.findall(r"([A-Z]+)\s*組", question, re.IGNORECASE)
523
  if lab_group_match:
524
  codes = [LAB_GROUP_MAP.get(g.upper()) for g in lab_group_match if LAB_GROUP_MAP.get(g.upper())]
525
- if codes:
526
- lab_group_match_data = {"codes": codes, "identifiers": lab_group_match}
527
 
528
  is_tat_query = any(k in q_lower for k in ['平均', 'average']) and any(k in q_lower for k in ['時間', '時長', '多久', '天', 'tat', 'turnaround'])
529
 
@@ -532,39 +526,56 @@ class TextToSQLSystem:
532
  if job_no_match and any(kw in q_lower for kw in ['工作日', 'workday']):
533
  job_no = job_no_match.group(1).upper()
534
  self._log(f"🔄 檢測到計算【工單 {job_no}】工作日TAT的意圖,啟用模板。", "INFO")
535
- template_sql = f"WITH span AS (SELECT date(jt.JobCreation) AS d1, date(jt.ReportAuthorization) AS d2 FROM JobTimeline jt WHERE jt.JobNo = '{job_no}'), days AS (SELECT 1 FROM calendar_days, span WHERE date BETWEEN d1 AND d2 AND is_workday = 1) SELECT COUNT(*) FROM days;"
536
- return self._finalize_sql(template_sql, f"模板覆寫: {job_no} 的工作日天數")
537
 
538
- if job_no_match and any(kw in q_lower for kw in ['總處理時長', '時長', '多少天']):
539
  job_no = job_no_match.group(1).upper()
540
  self._log(f"🔄 檢測到計算【工單 {job_no}】日曆日TAT的意圖,啟用模板。", "INFO")
541
- template_sql = f"SELECT ROUND(julianday(ReportAuthorization) - julianday(JobCreation), 2) AS days FROM JobTimeline WHERE JobNo = '{job_no}';"
542
- return self._finalize_sql(template_sql, f"模板覆寫: {job_no} 的日曆日總時長")
543
 
544
- if job_no_match and any(kw in q_lower for kw in ['總金額', '金額', '業績', 'total amount']):
545
  job_no = job_no_match.group(1).upper()
546
  self._log(f"🔄 檢測到對【單一工作單 '{job_no}'】的【標準金額計算】意圖,啟用模板。", "INFO")
547
- template_sql = f"WITH JobTotalAmount AS (SELECT JobNo, SUM(LocalAmount) AS TotalAmount FROM (SELECT DISTINCT JobNo, InvoiceCreditNoteNo, LocalAmount FROM TSR53Invoice) GROUP BY JobNo) SELECT TotalAmount FROM JobTotalAmount WHERE JobNo = '{job_no}';"
548
- return self._finalize_sql(template_sql, f"模板覆寫: 工作單 {job_no} 的標準總金額 (CTE去重)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
 
550
- if entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
551
  entity_type, entity_name, column_name = entity_match_data["type"], entity_match_data["name"], entity_match_data["column"]
552
  year = (re.search(r'(\d{4})\s*年?', question) or ['', datetime.now().strftime('%Y')])[1]
553
  self._log(f"🔄 檢測到查詢【{entity_type} '{entity_name}' 在 {year} 年的總業績】意圖,啟用模板。", "INFO")
554
- template_sql = f"WITH JobTotalAmount AS (SELECT JobNo, SUM(LocalAmount) AS TotalAmount FROM (SELECT DISTINCT JobNo, InvoiceCreditNoteNo, LocalAmount FROM TSR53Invoice) GROUP BY JobNo) SELECT SUM(jta.TotalAmount) AS total_revenue FROM TSR53SampleDescription AS sd JOIN JobTotalAmount AS jta ON sd.JobNo = jta.JobNo WHERE {column_name} LIKE '%{entity_name}%' AND strftime('%Y', sd.FirstReportAuthorizedDate) = '{year}';"
555
- return self._finalize_sql(template_sql, f"模板覆寫: 查詢 {entity_type}='{entity_name}' ({year}年) 的總業績")
556
 
557
- if not entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
558
  year_match, month_match = re.search(r'(\d{4})\s*年?', question), re.search(r'(\d{1,2})\s*月', question)
559
  time_condition, time_log = "", "總"
560
  if year_match:
561
  year = year_match.group(1); time_condition = f"WHERE strftime('%Y', InvoiceCreditNoteDate) = '{year}'"; time_log = f"{year}年"
562
  if month_match: month = month_match.group(1).zfill(2); time_condition += f" AND strftime('%m', InvoiceCreditNoteDate) = '{month}'"; time_log += f"{month}月"
563
  self._log(f"🔄 檢測到查詢【{time_log}全局總業績】意圖,啟用模板。", "INFO")
564
- template_sql = f"SELECT SUM(LocalAmount) AS total_revenue FROM TSR53Invoice {time_condition};"
565
- return self._finalize_sql(template_sql, f"模板覆寫: {time_log}全局總業績查詢")
566
 
567
- if lab_group_match_data and any(kw in q_lower for kw in ['測試項目', 'test item']):
568
  lab_group_code = lab_group_match_data["codes"][0]
569
  target_table = f"JobTimeline_{lab_group_code}"
570
  year = (re.search(r'(\d{4})\s*年', question) or ['', datetime.now().strftime('%Y')])[1]
@@ -576,10 +587,10 @@ class TextToSQLSystem:
576
  time_condition += f" AND strftime('%m', end_time) = '{month}'"
577
  month_str = f"{month}月"
578
  self._log(f"🔄 檢測到查詢【{lab_group_code}組】完成的【測試項目數】意圖,啟用專屬模板。", "INFO")
579
- template_sql = f"SELECT COUNT(JobItemKey) AS test_item_count FROM {target_table} WHERE end_time IS NOT NULL AND {time_condition};"
580
- return self._finalize_sql(template_sql, f"模板覆寫: 查詢 {lab_group_code}組 在 {year}年{month_str} 完成的測試項目數")
581
 
582
- if '報告' in q_lower and any(kw in q_lower for kw in ['幾份', '多少', '數量', '總數']) and not entity_match_data and not lab_group_match_data:
583
  year_match = re.search(r'(\d{4})\s*年?', question)
584
  time_condition, time_log = "", "總"
585
  if year_match:
@@ -589,59 +600,68 @@ class TextToSQLSystem:
589
  else:
590
  time_condition = "WHERE ReportAuthorization IS NOT NULL"
591
  self._log(f"🔄 檢測到查詢【{time_log}全局報告總數】意圖,啟用模板。", "INFO")
592
- template_sql = f"SELECT COUNT(DISTINCT JobNo) AS report_count FROM JobTimeline {time_condition};"
593
- return self._finalize_sql(template_sql, f"模板覆寫: {time_log}全局報告總數查詢")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
 
595
  # ==============================================================================
596
- # 第二層:常規修正流程 (Fallback Corrections)
597
  # ==============================================================================
598
- self._log("未觸發任何模板,嘗試解析並修正 AI 輸出...", "INFO")
599
-
600
- parsed_sql = parse_sql_from_response(raw_response)
601
- if not parsed_sql:
602
- self._log(f"❌ 未能從模型回應中解析出任何 SQL。原始回應: {raw_response}", "ERROR")
603
- return None, f"無法解析SQL。原始回應:\n{raw_response}"
604
-
605
- self._log(f"📊 解析出的原始 SQL: {parsed_sql}", "DEBUG")
606
-
607
- fixed_sql = " " + parsed_sql.strip() + " "
608
- fixes_applied_fallback = []
 
 
609
 
610
- dialect_corrections = {
611
- r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)",
612
- r"(strftime\('%Y',\s*[^)]+\))\s*=\s*(\d{4})": r"\1 = '\2'",
613
- r"EXTRACT\s*\(\s*YEAR\s+FROM\s+([^)]+)\s*\)": r"strftime('%Y', \1)"
614
- }
615
- for pattern, replacement in dialect_corrections.items():
616
- if re.search(pattern, fixed_sql, re.IGNORECASE):
617
- fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
618
- fixes_applied_fallback.append(f"修正方言: {pattern}")
619
-
620
- schema_corrections = {
621
- 'TSR53ReportAuthorization': 'TSR53SampleDescription', 'TSR53TestResult': 'TSR53SampleDescription',
622
- 'JobInvoice': 'TSR53Invoice', 'JobInvoiceAuthorization': 'TSR53Invoice', 'JobInvoiceCreditNote': 'TSR53Invoice',
623
- 'Customer': 'TSR53SampleDescription', 'Customers': 'TSR53SampleDescription',
624
- 'Invoice': 'TSR53Invoice', 'Invoices': 'TSR53Invoice', 'Job': 'JobTimeline', 'Jobs': 'JobsInProgress',
625
- 'Tests': 'TSR53MarsItem', 'TestsLog': 'JobItemsInProgress',
626
- 'AuthorizationDate': 'ReportAuthorization', 'ReportAuthorizationDate': 'ReportAuthorization',
627
- 'LegalAuthorization': 'OverallRating', 'LegalAuthorizationDate': 'ReportAuthorization',
628
- 'TestResult': 'OverallRating', 'Rating': 'OverallRating', 'CustomerName': 'BuyerName', 'InvoiceTo': 'InvoiceToName',
629
- 'Applicant': 'ApplicantName', 'Agent': 'AgentName', 'JobNumber': 'JobNo', 'ReportNo': 'JobNo', 'TestName': 'ItemInvoiceDescriptionJob',
630
- 'CreationDate': 'JobCreation', 'CreateDate': 'JobCreation', 'CompletedDate': 'ReportAuthorization',
631
- 'Amount': 'LocalAmount', 'Price': 'LocalAmount', 'Lab': 'LabGroup'
632
- }
633
- for wrong, correct in schema_corrections.items():
634
- pattern = r'\b' + re.escape(wrong) + r'\b'
635
- if re.search(pattern, fixed_sql, re.IGNORECASE):
636
- fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
637
- fixes_applied_fallback.append(f"映射 Schema: '{wrong}' -> '{correct}'")
638
-
639
- if any(kw in q_lower for kw in ['幾份', '多少', 'how many', 'count', '數量']) and 'select ' in fixed_sql.lower() and 'count' not in fixed_sql.lower() and 'group by' not in fixed_sql.lower():
640
- fixed_sql = re.sub(r'SELECT\s+.*?FROM', 'SELECT COUNT(*) FROM', fixed_sql, count=1, flags=re.IGNORECASE)
641
- fixes_applied_fallback.append("修正邏輯: 補全 COUNT(*)")
642
-
643
- log_msg = "AI 生成並成功修正" if fixes_applied_fallback else "AI 生成且無需修正"
644
- return self._finalize_sql(fixed_sql, log_msg)
645
 
646
  def _finalize_sql(self, sql: str, log_message: str) -> Tuple[str, str]:
647
  """一個輔助函數,用於清理最終的SQL並記錄成功日誌。"""
 
481
 
482
  return formatted.strip()
483
 
 
 
 
484
  # in class TextToSQLSystem:
485
 
486
+ def _validate_and_fix_sql(self, sql: str, question: str) -> str:
487
  """
488
+ (V19 / Top N 工作單版)
489
  一個全面、多層次的 SQL 驗證與生成引擎。
490
+ 新增了對“查詢總金額最高的 N 個工作單”這一高頻查詢的專家模板支持。
 
 
491
  """
492
+ if not sql or not self.schema:
493
+ self._log("SQL 修正被跳過,因輸入為空或 schema 未載入。", "WARNING")
494
+ return sql
495
 
496
+ original_sql = sql
497
+ fixed_sql = " " + sql.strip() + " "
498
+ fixes_applied = []
499
+ q_lower = question.lower()
500
+
501
  # ==============================================================================
502
  # 第一層:高價值意圖識別與模板覆寫 (Intent Recognition & Templating)
503
  # ==============================================================================
504
+
505
  # --- 預先檢測所有可能的意圖和實體 ---
506
  job_no_match = re.search(r"(?:工單|jobno)\s*'\"?([A-Z]{2,3}\d+)'\"?", question, re.IGNORECASE)
507
+
508
  entity_match_data = None
509
+ ENTITY_TO_COLUMN_MAP = {'申請廠商':'sd.ApplicantName','申請方':'sd.ApplicantName','applicant':'sd.ApplicantName','付款廠商':'sd.InvoiceToName','付款方':'sd.InvoiceToName','invoiceto':'sd.InvoiceToName','代理商':'sd.AgentName','agent':'sd.AgentName','買家':'sd.BuyerName','buyer':'sd.BuyerName','客戶':'sd.BuyerName', '品牌': 'tsr.BuyerName'}
 
 
 
 
 
510
  for keyword, column in ENTITY_TO_COLUMN_MAP.items():
511
  if keyword in q_lower:
512
  match = re.search(fr"{re.escape(keyword)}[\s:;\'\"-]*([a-zA-Z0-9&.\s-]+?)(?:\s*的|\s+|為|$)", question, re.IGNORECASE)
513
+ if match: entity_match_data = {"type": keyword, "name": match.group(1).strip(), "column": column}; break
514
+
 
 
515
  lab_group_match_data = None
516
  LAB_GROUP_MAP = {'A':'TA','B':'TB','C':'TC','D':'TD','E':'TE','Y':'TY','TA':'TA','TB':'TB','TC':'TC','TD':'TD','TE':'TE','TY':'TY','WC':'WC','EO':'EO','GCI':'GCI','GCO':'GCO','MI':'MI'}
517
  lab_group_match = re.findall(r"([A-Z]+)\s*組", question, re.IGNORECASE)
518
  if lab_group_match:
519
  codes = [LAB_GROUP_MAP.get(g.upper()) for g in lab_group_match if LAB_GROUP_MAP.get(g.upper())]
520
+ if codes: lab_group_match_data = {"codes": codes, "identifiers": lab_group_match}
 
521
 
522
  is_tat_query = any(k in q_lower for k in ['平均', 'average']) and any(k in q_lower for k in ['時間', '時長', '多久', '天', 'tat', 'turnaround'])
523
 
 
526
  if job_no_match and any(kw in q_lower for kw in ['工作日', 'workday']):
527
  job_no = job_no_match.group(1).upper()
528
  self._log(f"🔄 檢測到計算【工單 {job_no}】工作日TAT的意圖,啟用模板。", "INFO")
529
+ fixed_sql = f"""WITH span AS (SELECT date(jt.JobCreation) AS d1, date(jt.ReportAuthorization) AS d2 FROM JobTimeline jt WHERE jt.JobNo = '{job_no}'), days AS (SELECT 1 FROM calendar_days, span WHERE date BETWEEN d1 AND d2 AND is_workday = 1) SELECT COUNT(*) FROM days;"""
530
+ fixes_applied.append(f"模板覆寫: {job_no} 的工作日天數")
531
 
532
+ elif job_no_match and any(kw in q_lower for kw in ['總處理���長', '時長', '多少天']):
533
  job_no = job_no_match.group(1).upper()
534
  self._log(f"🔄 檢測到計算【工單 {job_no}】日曆日TAT的意圖,啟用模板。", "INFO")
535
+ fixed_sql = f"""SELECT ROUND(julianday(ReportAuthorization) - julianday(JobCreation), 2) AS days FROM JobTimeline WHERE JobNo = '{job_no}';"""
536
+ fixes_applied.append(f"模板覆寫: {job_no} 的日曆日總時長")
537
 
538
+ elif job_no_match and any(kw in q_lower for kw in ['總金額', '金額', '業績', 'total amount']):
539
  job_no = job_no_match.group(1).upper()
540
  self._log(f"🔄 檢測到對【單一工作單 '{job_no}'】的【標準金額計算】意圖,啟用模板。", "INFO")
541
+ fixed_sql = f"""WITH JobTotalAmount AS (SELECT JobNo, SUM(LocalAmount) AS TotalAmount FROM (SELECT DISTINCT JobNo, InvoiceCreditNoteNo, LocalAmount FROM TSR53Invoice) GROUP BY JobNo) SELECT TotalAmount FROM JobTotalAmount WHERE JobNo = '{job_no}';"""
542
+ fixes_applied.append(f"模板覆寫: 工作單 {job_no} 的標準總金額 (CTE去重)")
543
+
544
+ elif '總金額最高' in q_lower and '工作單' in q_lower:
545
+ limit_match = re.search(r'(\d+)', question)
546
+ limit = limit_match.group(1) if limit_match else '10'
547
+ self._log(f"🔄 檢測到查詢【Top {limit} 工作單金額】意圖,啟用模板。", "INFO")
548
+ fixed_sql = f"""
549
+ WITH JobTotalAmount AS (
550
+ SELECT JobNo, SUM(LocalAmount) AS TotalAmount
551
+ FROM (SELECT DISTINCT JobNo, InvoiceCreditNoteNo, LocalAmount FROM TSR53Invoice)
552
+ GROUP BY JobNo
553
+ )
554
+ SELECT JobNo, TotalAmount
555
+ FROM JobTotalAmount
556
+ ORDER BY TotalAmount DESC
557
+ LIMIT {limit};
558
+ """
559
+ fixes_applied.append(f"模板覆寫: Top {limit} 工作單金額")
560
 
561
+ elif entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
562
  entity_type, entity_name, column_name = entity_match_data["type"], entity_match_data["name"], entity_match_data["column"]
563
  year = (re.search(r'(\d{4})\s*年?', question) or ['', datetime.now().strftime('%Y')])[1]
564
  self._log(f"🔄 檢測到查詢【{entity_type} '{entity_name}' 在 {year} 年的總業績】意圖,啟用模板。", "INFO")
565
+ fixed_sql = f"""WITH JobTotalAmount AS (SELECT JobNo, SUM(LocalAmount) AS TotalAmount FROM (SELECT DISTINCT JobNo, InvoiceCreditNoteNo, LocalAmount FROM TSR53Invoice) GROUP BY JobNo) SELECT SUM(jta.TotalAmount) AS total_revenue FROM TSR53SampleDescription AS sd JOIN JobTotalAmount AS jta ON sd.JobNo = jta.JobNo WHERE {column_name} LIKE '%{entity_name}%' AND strftime('%Y', sd.FirstReportAuthorizedDate) = '{year}';"""
566
+ fixes_applied.append(f"模板覆寫: 查詢 {entity_type}='{entity_name}' ({year}年) 的總業績")
567
 
568
+ elif not entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
569
  year_match, month_match = re.search(r'(\d{4})\s*年?', question), re.search(r'(\d{1,2})\s*月', question)
570
  time_condition, time_log = "", "總"
571
  if year_match:
572
  year = year_match.group(1); time_condition = f"WHERE strftime('%Y', InvoiceCreditNoteDate) = '{year}'"; time_log = f"{year}年"
573
  if month_match: month = month_match.group(1).zfill(2); time_condition += f" AND strftime('%m', InvoiceCreditNoteDate) = '{month}'"; time_log += f"{month}月"
574
  self._log(f"🔄 檢測到查詢【{time_log}全局總業績】意圖,啟用模板。", "INFO")
575
+ fixed_sql = f"""SELECT SUM(LocalAmount) AS total_revenue FROM TSR53Invoice {time_condition};"""
576
+ fixes_applied.append(f"模板覆寫: {time_log}全局總業績查詢")
577
 
578
+ elif lab_group_match_data and any(kw in q_lower for kw in ['測試項目', 'test item']):
579
  lab_group_code = lab_group_match_data["codes"][0]
580
  target_table = f"JobTimeline_{lab_group_code}"
581
  year = (re.search(r'(\d{4})\s*年', question) or ['', datetime.now().strftime('%Y')])[1]
 
587
  time_condition += f" AND strftime('%m', end_time) = '{month}'"
588
  month_str = f"{month}月"
589
  self._log(f"🔄 檢測到查詢【{lab_group_code}組】完成的【測試項目數】意圖,啟用專屬模板。", "INFO")
590
+ fixed_sql = f"""SELECT COUNT(JobItemKey) AS test_item_count FROM {target_table} WHERE end_time IS NOT NULL AND {time_condition};"""
591
+ fixes_applied.append(f"模板覆寫: 查詢 {lab_group_code}組 在 {year}年{month_str} 完成的測試項目數")
592
 
593
+ elif '報告' in q_lower and any(kw in q_lower for kw in ['幾份', '多少', '數量', '總數']) and not entity_match_data and not lab_group_match_data:
594
  year_match = re.search(r'(\d{4})\s*年?', question)
595
  time_condition, time_log = "", "總"
596
  if year_match:
 
600
  else:
601
  time_condition = "WHERE ReportAuthorization IS NOT NULL"
602
  self._log(f"🔄 檢測到查詢【{time_log}全局報告總數】意圖,啟用模板。", "INFO")
603
+ fixed_sql = f"SELECT COUNT(DISTINCT JobNo) AS report_count FROM JobTimeline {time_condition};"
604
+ fixes_applied.append(f"模板覆寫: {time_log}全局報告總數查詢")
605
+
606
+ if not fixes_applied:
607
+ self._log("未觸發任何模板,執行常規修正流程...", "DEBUG")
608
+
609
+ # ==============================================================================
610
+ # 第二層:常規修正流程 (Fallback Corrections)
611
+ # ==============================================================================
612
+
613
+ dialect_corrections = {
614
+ r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)",
615
+ r"(strftime\('%Y',\s*[^)]+\))\s*=\s*(\d{4})": r"\1 = '\2'",
616
+ r"EXTRACT\s*\(\s*YEAR\s+FROM\s+([^)]+)\s*\)": r"strftime('%Y', \1)"
617
+ }
618
+ for pattern, replacement in dialect_corrections.items():
619
+ if re.search(pattern, fixed_sql, re.IGNORECASE):
620
+ fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
621
+ fixes_applied.append(f"修正方言: {pattern}")
622
+
623
+ schema_corrections = {
624
+ 'TSR53ReportAuthorization': 'TSR53SampleDescription', 'TSR53TestResult': 'TSR53SampleDescription',
625
+ 'JobInvoice': 'TSR53Invoice', 'JobInvoiceAuthorization': 'TSR53Invoice', 'JobInvoiceCreditNote': 'TSR53Invoice',
626
+ 'Customer': 'TSR53SampleDescription', 'Customers': 'TSR53SampleDescription',
627
+ 'Invoice': 'TSR53Invoice', 'Invoices': 'TSR53Invoice', 'Job': 'JobTimeline', 'Jobs': 'JobsInProgress',
628
+ 'Tests': 'TSR53MarsItem', 'TestsLog': 'JobItemsInProgress',
629
+ 'AuthorizationDate': 'ReportAuthorization', 'ReportAuthorizationDate': 'ReportAuthorization',
630
+ 'LegalAuthorization': 'OverallRating', 'LegalAuthorizationDate': 'ReportAuthorization',
631
+ 'TestResult': 'OverallRating', 'Rating': 'OverallRating', 'CustomerName': 'BuyerName', 'InvoiceTo': 'InvoiceToName',
632
+ 'Applicant': 'ApplicantName', 'Agent': 'AgentName', 'JobNumber': 'JobNo', 'ReportNo': 'JobNo', 'TestName': 'ItemInvoiceDescriptionJob',
633
+ 'CreationDate': 'JobCreation', 'CreateDate': 'JobCreation', 'CompletedDate': 'ReportAuthorization',
634
+ 'InvoiceCreditNoteAmount': 'LocalAmount',
635
+ 'Amount': 'LocalAmount', 'Price': 'LocalAmount', 'Lab': 'LabGroup'
636
+ }
637
+ for wrong, correct in schema_corrections.items():
638
+ pattern = r'\b' + re.escape(wrong) + r'\b'
639
+ if re.search(pattern, fixed_sql, re.IGNORECASE):
640
+ fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
641
+ fixes_applied.append(f"映射 Schema: '{wrong}' -> '{correct}'")
642
+
643
+ if any(kw in q_lower for kw in ['幾份', '多少', 'how many', 'count', '數量']) and 'select ' in fixed_sql.lower() and 'count' not in fixed_sql.lower() and 'group by' not in fixed_sql.lower():
644
+ fixed_sql = re.sub(r'SELECT\s+.*?FROM', 'SELECT COUNT(*) FROM', fixed_sql, count=1, flags=re.IGNORECASE)
645
+ fixes_applied.append("修正邏輯: 補全 COUNT(*)")
646
 
647
  # ==============================================================================
648
+ # 第三層:清理與完成 (Finalization)
649
  # ==============================================================================
650
+ fixed_sql = fixed_sql.strip()
651
+ if not fixed_sql.endswith(';'):
652
+ fixed_sql += ';'
653
+ fixed_sql = re.sub(r'\s+', ' ', fixed_sql).strip()
654
+
655
+ if fixes_applied:
656
+ self._log("🔄 SQL 已被自動修正:", "INFO")
657
+ self._log(f" - 原始 SQL: {original_sql}", "DEBUG")
658
+ for fix in fixes_applied:
659
+ self._log(f" - 應用規則: {fix}", "DEBUG")
660
+ self._log(f" - 修正後 SQL: {fixed_sql}", "INFO")
661
+ else:
662
+ self._log("✅ SQL 驗證通過,無需常規修正。", "INFO")
663
 
664
+ return fixed_sql
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
 
666
  def _finalize_sql(self, sql: str, log_message: str) -> Tuple[str, str]:
667
  """一個輔助函數,用於清理最終的SQL並記錄成功日誌。"""