Paul720810 commited on
Commit
8244c41
·
verified ·
1 Parent(s): 4f1b47e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +87 -104
app.py CHANGED
@@ -483,20 +483,14 @@ class TextToSQLSystem:
483
 
484
  # in class TextToSQLSystem:
485
 
486
- def _validate_and_fix_sql(self, sql: str, question: str) -> str:
487
  """
488
- (V20 / 智能列表版)
489
  一個全面、多層次的 SQL 驗證與生成引擎。
490
- 強化了“報告列表”意圖模板,使其能夠動態識別並附加額外的篩選條件,
491
- 如報告狀態 (Fail/Pass),從而生成更精準的列表查詢。
 
492
  """
493
- if not sql or not self.schema:
494
- self._log("SQL 修正被跳過,因輸入為空或 schema 未載入。", "WARNING")
495
- return sql
496
-
497
- original_sql = sql
498
- fixed_sql = " " + sql.strip() + " "
499
- fixes_applied = []
500
  q_lower = question.lower()
501
 
502
  # ==============================================================================
@@ -506,7 +500,7 @@ class TextToSQLSystem:
506
  # --- 預先檢測所有可能的意圖和實體 ---
507
  job_no_match = re.search(r"(?:工單|jobno)\s*'\"?([A-Z]{2,3}\d+)'\"?", question, re.IGNORECASE)
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)
@@ -520,32 +514,32 @@ class TextToSQLSystem:
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
-
524
  # --- 判斷邏輯: 依優先級進入對應的模板 ---
525
-
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)
@@ -556,25 +550,17 @@ FROM JobTotalAmount
556
  ORDER BY TotalAmount DESC
557
  LIMIT {limit};
558
  """
559
- fixes_applied.append(f"模板覆寫: Top {limit} 工作單金額")
560
 
561
- elif any(kw in q_lower for kw in ['報告號碼', '報告清單', '列出報告', 'report number', 'list of reports']):
562
  year_match = re.search(r'(\d{4})\s*年?', question)
563
  month_match = re.search(r'(\d{1,2})\s*月', question)
564
-
565
  from_clause = "FROM JobTimeline AS jt"
566
  where_conditions = ["jt.ReportAuthorization IS NOT NULL"]
567
  time_log = ""
568
-
569
  if year_match:
570
- year = year_match.group(1)
571
- where_conditions.append(f"strftime('%Y', jt.ReportAuthorization) = '{year}'")
572
- time_log = f"{year}年"
573
- if month_match:
574
- month = month_match.group(1).zfill(2)
575
- where_conditions.append(f"strftime('%m', jt.ReportAuthorization) = '{month}'")
576
- time_log += f"{month}月"
577
-
578
  if 'fail' in q_lower or '失敗' in q_lower:
579
  if "JOIN TSR53SampleDescription" not in from_clause: from_clause = "FROM JobTimeline AS jt JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo"
580
  where_conditions.append("sd.OverallRating = 'Fail'")
@@ -583,13 +569,11 @@ LIMIT {limit};
583
  if "JOIN TSR53SampleDescription" not in from_clause: from_clause = "FROM JobTimeline AS jt JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo"
584
  where_conditions.append("sd.OverallRating = 'Pass'")
585
  time_log += " Pass"
586
-
587
  final_where_clause = "WHERE " + " AND ".join(where_conditions)
588
- self._log(f"🔄 檢測到查詢【{time_log} 報告列表】意圖,啟用智能模板。", "INFO")
589
- fixed_sql = f"SELECT jt.JobNo, jt.ReportAuthorization {from_clause} {final_where_clause} ORDER BY jt.ReportAuthorization DESC;"
590
- fixes_applied.append(f"模板覆寫: {time_log} 報告列表查詢")
591
 
592
- elif '報告' in q_lower and any(kw in q_lower for kw in ['幾份', '多少', '數量', '總數']) and not entity_match_data and not lab_group_match_data:
593
  year_match = re.search(r'(\d{4})\s*年?', question)
594
  time_condition, time_log = "", "總"
595
  if year_match:
@@ -599,27 +583,27 @@ LIMIT {limit};
599
  else:
600
  time_condition = "WHERE ReportAuthorization IS NOT NULL"
601
  self._log(f"🔄 檢測到查詢【{time_log}全局報告總數】意圖,啟用模板。", "INFO")
602
- fixed_sql = f"SELECT COUNT(DISTINCT JobNo) AS report_count FROM JobTimeline {time_condition};"
603
- fixes_applied.append(f"模板覆寫: {time_log}全局報告總數查詢")
604
 
605
- elif entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
606
  entity_type, entity_name, column_name = entity_match_data["type"], entity_match_data["name"], entity_match_data["column"]
607
  year = (re.search(r'(\d{4})\s*年?', question) or ['', datetime.now().strftime('%Y')])[1]
608
  self._log(f"🔄 檢測到查詢【{entity_type} '{entity_name}' 在 {year} 年的總業績】意圖,啟用模板。", "INFO")
609
- 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}';"""
610
- fixes_applied.append(f"模板覆寫: 查詢 {entity_type}='{entity_name}' ({year}年) 的總業績")
611
 
612
- elif not entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
613
  year_match, month_match = re.search(r'(\d{4})\s*年?', question), re.search(r'(\d{1,2})\s*月', question)
614
  time_condition, time_log = "", "總"
615
  if year_match:
616
  year = year_match.group(1); time_condition = f"WHERE strftime('%Y', InvoiceCreditNoteDate) = '{year}'"; time_log = f"{year}年"
617
  if month_match: month = month_match.group(1).zfill(2); time_condition += f" AND strftime('%m', InvoiceCreditNoteDate) = '{month}'"; time_log += f"{month}月"
618
  self._log(f"🔄 檢測到查詢【{time_log}全局總業績】意圖,啟用模板。", "INFO")
619
- fixed_sql = f"""SELECT SUM(LocalAmount) AS total_revenue FROM TSR53Invoice {time_condition};"""
620
- fixes_applied.append(f"模板覆寫: {time_log}全局總業績查詢")
621
 
622
- elif lab_group_match_data and any(kw in q_lower for kw in ['測試項目', 'test item']):
623
  lab_group_code = lab_group_match_data["codes"][0]
624
  target_table = f"JobTimeline_{lab_group_code}"
625
  year = (re.search(r'(\d{4})\s*年', question) or ['', datetime.now().strftime('%Y')])[1]
@@ -631,61 +615,60 @@ LIMIT {limit};
631
  time_condition += f" AND strftime('%m', end_time) = '{month}'"
632
  month_str = f"{month}月"
633
  self._log(f"🔄 檢測到查詢【{lab_group_code}組】完成的【測試項目數】意圖,啟用專屬模板。", "INFO")
634
- fixed_sql = f"""SELECT COUNT(JobItemKey) AS test_item_count FROM {target_table} WHERE end_time IS NOT NULL AND {time_condition};"""
635
- fixes_applied.append(f"模板覆寫: 查詢 {lab_group_code}組 在 {year}年{month_str} 完成的測試項目數")
636
 
637
- if not fixes_applied:
638
- self._log("未觸發任何模板,執行常規修正流程...", "DEBUG")
639
-
640
- dialect_corrections = {
641
- r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)",
642
- r"(strftime\('%Y',\s*[^)]+\))\s*=\s*(\d{4})": r"\1 = '\2'",
643
- r"EXTRACT\s*\(\s*YEAR\s+FROM\s+([^)]+)\s*\)": r"strftime('%Y', \1)"
644
- }
645
- for pattern, replacement in dialect_corrections.items():
646
- if re.search(pattern, fixed_sql, re.IGNORECASE):
647
- fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
648
- fixes_applied.append(f"修正方言: {pattern}")
649
-
650
- schema_corrections = {
651
- 'TSR53ReportAuthorization': 'TSR53SampleDescription', 'TSR53TestResult': 'TSR53SampleDescription',
652
- 'JobInvoice': 'TSR53Invoice', 'JobInvoiceAuthorization': 'TSR53Invoice', 'JobInvoiceCreditNote': 'TSR53Invoice',
653
- 'Customer': 'TSR53SampleDescription', 'Customers': 'TSR53SampleDescription',
654
- 'Invoice': 'TSR53Invoice', 'Invoices': 'TSR53Invoice', 'Job': 'JobTimeline', 'Jobs': 'JobsInProgress',
655
- 'Tests': 'TSR53MarsItem', 'TestsLog': 'JobItemsInProgress',
656
- 'AuthorizationDate': 'ReportAuthorization', 'ReportAuthorizationDate': 'ReportAuthorization',
657
- 'LegalAuthorization': 'OverallRating', 'LegalAuthorizationDate': 'ReportAuthorization',
658
- 'TestResult': 'OverallRating', 'Rating': 'OverallRating', 'CustomerName': 'BuyerName', 'InvoiceTo': 'InvoiceToName',
659
- 'Applicant': 'ApplicantName', 'Agent': 'AgentName', 'JobNumber': 'JobNo', 'ReportNo': 'JobNo', 'TestName': 'ItemInvoiceDescriptionJob',
660
- 'CreationDate': 'JobCreation', 'CreateDate': 'JobCreation', 'CompletedDate': 'ReportAuthorization',
661
- 'InvoiceCreditNoteAmount': 'LocalAmount',
662
- 'Amount': 'LocalAmount', 'Price': 'LocalAmount', 'Lab': 'LabGroup'
663
- }
664
- for wrong, correct in schema_corrections.items():
665
- pattern = r'\b' + re.escape(wrong) + r'\b'
666
- if re.search(pattern, fixed_sql, re.IGNORECASE):
667
- fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
668
- fixes_applied.append(f"映射 Schema: '{wrong}' -> '{correct}'")
669
-
670
- 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():
671
- fixed_sql = re.sub(r'SELECT\s+.*?FROM', 'SELECT COUNT(*) FROM', fixed_sql, count=1, flags=re.IGNORECASE)
672
- fixes_applied.append("修正邏輯: 補全 COUNT(*)")
673
-
674
- fixed_sql = fixed_sql.strip()
675
- if not fixed_sql.endswith(';'):
676
- fixed_sql += ';'
677
- fixed_sql = re.sub(r'\s+', ' ', fixed_sql).strip()
678
-
679
- if fixes_applied:
680
- self._log("🔄 SQL 已被自動修正:", "INFO")
681
- self._log(f" - 原始 SQL: {original_sql}", "DEBUG")
682
- for fix in fixes_applied:
683
- self._log(f" - 應用規則: {fix}", "DEBUG")
684
- self._log(f" - 修正後 SQL: {fixed_sql}", "INFO")
685
- else:
686
- self._log("✅ SQL 驗證通過,無需常規修正。", "INFO")
687
 
688
- return fixed_sql
 
689
 
690
  def _finalize_sql(self, sql: str, log_message: str) -> Tuple[str, str]:
691
  """一個輔助函數,用於清理最終的SQL並記錄成功日誌。"""
@@ -695,7 +678,7 @@ LIMIT {limit};
695
  final_sql = re.sub(r'\s+', ' ', final_sql).strip()
696
  self._log(f"✅ SQL 已生成 ({log_message})", "INFO")
697
  self._log(f" - 最終 SQL: {final_sql}", "DEBUG")
698
- return final_sql, "生成成功"
699
 
700
  def find_most_similar(self, question: str, top_k: int) -> List[Dict]:
701
  """使用 FAISS 快速檢索相似問題"""
 
483
 
484
  # in class TextToSQLSystem:
485
 
486
+ def _validate_and_fix_sql(self, question: str, raw_response: str) -> Tuple[Optional[str], str]:
487
  """
488
+ (V17 / 最終決策版)
489
  一個全面、多層次的 SQL 驗證與生成引擎。
490
+ 本函數作為第一決策者,優先匹配用戶問題與專家知識庫。
491
+ 如果匹配成功,則直接使用模板覆寫;若不成功,才解析並修正 AI 的輸出。
492
+ 返回一個元組 (SQL字符串或None, 狀態消息)。
493
  """
 
 
 
 
 
 
 
494
  q_lower = question.lower()
495
 
496
  # ==============================================================================
 
500
  # --- 預先檢測所有可能的意圖和實體 ---
501
  job_no_match = re.search(r"(?:工單|jobno)\s*'\"?([A-Z]{2,3}\d+)'\"?", question, re.IGNORECASE)
502
  entity_match_data = None
503
+ 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'}
504
  for keyword, column in ENTITY_TO_COLUMN_MAP.items():
505
  if keyword in q_lower:
506
  match = re.search(fr"{re.escape(keyword)}[\s:;\'\"-]*([a-zA-Z0-9&.\s-]+?)(?:\s*的|\s+|為|$)", question, re.IGNORECASE)
 
514
  if codes: lab_group_match_data = {"codes": codes, "identifiers": lab_group_match}
515
 
516
  is_tat_query = any(k in q_lower for k in ['平均', 'average']) and any(k in q_lower for k in ['時間', '時長', '多久', '天', 'tat', 'turnaround'])
517
+
518
  # --- 判斷邏輯: 依優先級進入對應的模板 ---
519
+
520
  if job_no_match and any(kw in q_lower for kw in ['工作日', 'workday']):
521
  job_no = job_no_match.group(1).upper()
522
  self._log(f"🔄 檢測到計算【工單 {job_no}】工作日TAT的意圖,啟用模板。", "INFO")
523
+ 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;"
524
+ return self._finalize_sql(template_sql, f"模板覆寫: {job_no} 的工作日天數")
525
 
526
+ if job_no_match and any(kw in q_lower for kw in ['總處理時長', '時長', '多少天']):
527
  job_no = job_no_match.group(1).upper()
528
  self._log(f"🔄 檢測到計算【工單 {job_no}】日曆日TAT的意圖,啟用模板。", "INFO")
529
+ template_sql = f"SELECT ROUND(julianday(ReportAuthorization) - julianday(JobCreation), 2) AS days FROM JobTimeline WHERE JobNo = '{job_no}';"
530
+ return self._finalize_sql(template_sql, f"模板覆寫: {job_no} 的日曆日總時長")
531
 
532
+ if job_no_match and any(kw in q_lower for kw in ['總金額', '金額', '業績', 'total amount']):
533
  job_no = job_no_match.group(1).upper()
534
  self._log(f"🔄 檢測到對【單一工作單 '{job_no}'】的【標準金額計算】意圖,啟用模板。", "INFO")
535
+ 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}';"
536
+ return self._finalize_sql(template_sql, f"模板覆寫: 工作單 {job_no} 的標準總金額 (CTE去重)")
537
 
538
+ if '總金額最高' in q_lower and '工作單' in q_lower:
539
  limit_match = re.search(r'(\d+)', question)
540
  limit = limit_match.group(1) if limit_match else '10'
541
  self._log(f"🔄 檢測到查詢【Top {limit} 工作單金額】意圖,啟用模板。", "INFO")
542
+ template_sql = f"""
543
  WITH JobTotalAmount AS (
544
  SELECT JobNo, SUM(LocalAmount) AS TotalAmount
545
  FROM (SELECT DISTINCT JobNo, InvoiceCreditNoteNo, LocalAmount FROM TSR53Invoice)
 
550
  ORDER BY TotalAmount DESC
551
  LIMIT {limit};
552
  """
553
+ return self._finalize_sql(template_sql, f"模板覆寫: Top {limit} 工作單金額")
554
 
555
+ if any(kw in q_lower for kw in ['報告號碼', '報告清單', '列出報告', 'report number', 'list of reports']):
556
  year_match = re.search(r'(\d{4})\s*年?', question)
557
  month_match = re.search(r'(\d{1,2})\s*月', question)
 
558
  from_clause = "FROM JobTimeline AS jt"
559
  where_conditions = ["jt.ReportAuthorization IS NOT NULL"]
560
  time_log = ""
 
561
  if year_match:
562
+ year = year_match.group(1); where_conditions.append(f"strftime('%Y', jt.ReportAuthorization) = '{year}'"); time_log = f"{year}年"
563
+ if month_match: month = month_match.group(1).zfill(2); where_conditions.append(f"strftime('%m', jt.ReportAuthorization) = '{month}'"); time_log += f"{month}月"
 
 
 
 
 
 
564
  if 'fail' in q_lower or '失敗' in q_lower:
565
  if "JOIN TSR53SampleDescription" not in from_clause: from_clause = "FROM JobTimeline AS jt JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo"
566
  where_conditions.append("sd.OverallRating = 'Fail'")
 
569
  if "JOIN TSR53SampleDescription" not in from_clause: from_clause = "FROM JobTimeline AS jt JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo"
570
  where_conditions.append("sd.OverallRating = 'Pass'")
571
  time_log += " Pass"
 
572
  final_where_clause = "WHERE " + " AND ".join(where_conditions)
573
+ template_sql = f"SELECT jt.JobNo, jt.ReportAuthorization {from_clause} {final_where_clause} ORDER BY jt.ReportAuthorization DESC;"
574
+ return self._finalize_sql(template_sql, f"模板覆寫: {time_log} 報告列表查詢")
 
575
 
576
+ if '報告' in q_lower and any(kw in q_lower for kw in ['幾份', '多少', '數量', '總數']) and not entity_match_data and not lab_group_match_data:
577
  year_match = re.search(r'(\d{4})\s*年?', question)
578
  time_condition, time_log = "", "總"
579
  if year_match:
 
583
  else:
584
  time_condition = "WHERE ReportAuthorization IS NOT NULL"
585
  self._log(f"🔄 檢測到查詢【{time_log}全局報告總數】意圖,啟用模板。", "INFO")
586
+ template_sql = f"SELECT COUNT(DISTINCT JobNo) AS report_count FROM JobTimeline {time_condition};"
587
+ return self._finalize_sql(template_sql, f"模板覆寫: {time_log}全局報告總數查詢")
588
 
589
+ if entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
590
  entity_type, entity_name, column_name = entity_match_data["type"], entity_match_data["name"], entity_match_data["column"]
591
  year = (re.search(r'(\d{4})\s*年?', question) or ['', datetime.now().strftime('%Y')])[1]
592
  self._log(f"🔄 檢測到查詢【{entity_type} '{entity_name}' 在 {year} 年的總業績】意圖,啟用模板。", "INFO")
593
+ 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}';"
594
+ return self._finalize_sql(template_sql, f"模板覆寫: 查詢 {entity_type}='{entity_name}' ({year}年) 的總業績")
595
 
596
+ if not entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
597
  year_match, month_match = re.search(r'(\d{4})\s*年?', question), re.search(r'(\d{1,2})\s*月', question)
598
  time_condition, time_log = "", "總"
599
  if year_match:
600
  year = year_match.group(1); time_condition = f"WHERE strftime('%Y', InvoiceCreditNoteDate) = '{year}'"; time_log = f"{year}年"
601
  if month_match: month = month_match.group(1).zfill(2); time_condition += f" AND strftime('%m', InvoiceCreditNoteDate) = '{month}'"; time_log += f"{month}月"
602
  self._log(f"🔄 檢測到查詢【{time_log}全局總業績】意圖,啟用模板。", "INFO")
603
+ template_sql = f"SELECT SUM(LocalAmount) AS total_revenue FROM TSR53Invoice {time_condition};"
604
+ return self._finalize_sql(template_sql, f"模板覆寫: {time_log}全局總業績查詢")
605
 
606
+ if lab_group_match_data and any(kw in q_lower for kw in ['測試項目', 'test item']):
607
  lab_group_code = lab_group_match_data["codes"][0]
608
  target_table = f"JobTimeline_{lab_group_code}"
609
  year = (re.search(r'(\d{4})\s*年', question) or ['', datetime.now().strftime('%Y')])[1]
 
615
  time_condition += f" AND strftime('%m', end_time) = '{month}'"
616
  month_str = f"{month}月"
617
  self._log(f"🔄 檢測到查詢【{lab_group_code}組】完成的【測試項目數】意圖,啟用專屬模板。", "INFO")
618
+ template_sql = f"SELECT COUNT(JobItemKey) AS test_item_count FROM {target_table} WHERE end_time IS NOT NULL AND {time_condition};"
619
+ return self._finalize_sql(template_sql, f"模板覆寫: 查詢 {lab_group_code}組 在 {year}年{month_str} 完成的測試項目數")
620
 
621
+ # ==============================================================================
622
+ # 第二層:常規修正流程 (Fallback Corrections)
623
+ # ==============================================================================
624
+ self._log("未觸發任何模板,嘗試解析並修正 AI 輸出...", "INFO")
625
+
626
+ parsed_sql = parse_sql_from_response(raw_response)
627
+ if not parsed_sql:
628
+ self._log(f"❌ 未能從模型回應中解析出任何 SQL。原始回應: {raw_response}", "ERROR")
629
+ return None, f"無法解析SQL。原始回應:\n{raw_response}"
630
+
631
+ self._log(f"📊 解析出的原始 SQL: {parsed_sql}", "DEBUG")
632
+
633
+ fixed_sql = " " + parsed_sql.strip() + " "
634
+ fixes_applied_fallback = []
635
+
636
+ dialect_corrections = {
637
+ r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)",
638
+ r"(strftime\('%Y',\s*[^)]+\))\s*=\s*(\d{4})": r"\1 = '\2'",
639
+ r"EXTRACT\s*\(\s*YEAR\s+FROM\s+([^)]+)\s*\)": r"strftime('%Y', \1)"
640
+ }
641
+ for pattern, replacement in dialect_corrections.items():
642
+ if re.search(pattern, fixed_sql, re.IGNORECASE):
643
+ fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
644
+ fixes_applied_fallback.append(f"修正方言: {pattern}")
645
+
646
+ schema_corrections = {
647
+ 'TSR53ReportAuthorization': 'TSR53SampleDescription', 'TSR53TestResult': 'TSR53SampleDescription',
648
+ 'JobInvoice': 'TSR53Invoice', 'JobInvoiceAuthorization': 'TSR53Invoice', 'JobInvoiceCreditNote': 'TSR53Invoice',
649
+ 'Customer': 'TSR53SampleDescription', 'Customers': 'TSR53SampleDescription',
650
+ 'Invoice': 'TSR53Invoice', 'Invoices': 'TSR53Invoice', 'Job': 'JobTimeline', 'Jobs': 'JobsInProgress',
651
+ 'Tests': 'TSR53MarsItem', 'TestsLog': 'JobItemsInProgress',
652
+ 'AuthorizationDate': 'ReportAuthorization', 'ReportAuthorizationDate': 'ReportAuthorization',
653
+ 'LegalAuthorization': 'OverallRating', 'LegalAuthorizationDate': 'ReportAuthorization',
654
+ 'TestResult': 'OverallRating', 'Rating': 'OverallRating', 'CustomerName': 'BuyerName', 'InvoiceTo': 'InvoiceToName',
655
+ 'Applicant': 'ApplicantName', 'Agent': 'AgentName', 'JobNumber': 'JobNo', 'ReportNo': 'JobNo', 'TestName': 'ItemInvoiceDescriptionJob',
656
+ 'CreationDate': 'JobCreation', 'CreateDate': 'JobCreation', 'CompletedDate': 'ReportAuthorization',
657
+ 'InvoiceCreditNoteAmount': 'LocalAmount',
658
+ 'Amount': 'LocalAmount', 'Price': 'LocalAmount', 'Lab': 'LabGroup'
659
+ }
660
+ for wrong, correct in schema_corrections.items():
661
+ pattern = r'\b' + re.escape(wrong) + r'\b'
662
+ if re.search(pattern, fixed_sql, re.IGNORECASE):
663
+ fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
664
+ fixes_applied_fallback.append(f"映射 Schema: '{wrong}' -> '{correct}'")
665
+
666
+ 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():
667
+ fixed_sql = re.sub(r'SELECT\s+.*?FROM', 'SELECT COUNT(*) FROM', fixed_sql, count=1, flags=re.IGNORECASE)
668
+ fixes_applied_fallback.append("修正邏輯: 補全 COUNT(*)")
 
 
669
 
670
+ log_msg = "AI 生成並成功修正" if fixes_applied_fallback else "AI 生成且無需修正"
671
+ return self._finalize_sql(fixed_sql, log_msg)
672
 
673
  def _finalize_sql(self, sql: str, log_message: str) -> Tuple[str, str]:
674
  """一個輔助函數,用於清理最終的SQL並記錄成功日誌。"""
 
678
  final_sql = re.sub(r'\s+', ' ', final_sql).strip()
679
  self._log(f"✅ SQL 已生成 ({log_message})", "INFO")
680
  self._log(f" - 最終 SQL: {final_sql}", "DEBUG")
681
+ return final_sql, "生成成功"```
682
 
683
  def find_most_similar(self, question: str, top_k: int) -> List[Dict]:
684
  """使用 FAISS 快速檢索相似問題"""