Paul720810 commited on
Commit
eb941d7
·
verified ·
1 Parent(s): 504394d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -84
app.py CHANGED
@@ -459,14 +459,20 @@ class TextToSQLSystem:
459
 
460
  # in class TextToSQLSystem:
461
 
462
- def _validate_and_fix_sql(self, question: str, raw_response: str) -> Tuple[Optional[str], str]:
463
  """
464
- (V17 / 最終決策版)
465
  一個全面、多層次的 SQL 驗證與生成引擎。
466
- 本函數作為第一決策者,優先匹配用戶問題與專家知識庫。
467
- 如果匹配成功,則直接使用模板覆寫;若不成功,才解析並修正 AI 的輸出。
468
- 返回一個元組 (SQL字符串或None, 狀態消息)。
469
  """
 
 
 
 
 
 
 
470
  q_lower = question.lower()
471
 
472
  # ==============================================================================
@@ -499,96 +505,132 @@ class TextToSQLSystem:
499
  lab_group_match_data = {"codes": codes, "identifiers": lab_group_match}
500
 
501
  # --- 判斷邏輯: 依優先級進入對應的模板 ---
502
- # (如果任何模板被觸發,它將生成 SQL 並直接返回)
503
 
 
504
  if job_no_match and any(kw in q_lower for kw in ['工作日', 'workday']):
505
  job_no = job_no_match.group(1).upper()
506
  self._log(f"🔄 檢測到計算【工單 {job_no}】工作日TAT的意圖,啟用模板。", "INFO")
507
- 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;"
508
- return self._finalize_sql(template_sql, f"模板覆寫: {job_no} 的工作日天數")
509
-
510
- if job_no_match and any(kw in q_lower for kw in ['總處理時長', '時長', '多少天']):
511
- job_no = job_no_match.group(1).upper()
512
- self._log(f"🔄 檢測到計算【工單 {job_no}】日曆日TAT的意圖,啟用模板。", "INFO")
513
- template_sql = f"SELECT ROUND(julianday(ReportAuthorization) - julianday(JobCreation), 2) AS days FROM JobTimeline WHERE JobNo = '{job_no}';"
514
- return self._finalize_sql(template_sql, f"模板覆寫: {job_no} 的日曆日總時長")
515
-
516
- if job_no_match and any(kw in q_lower for kw in ['總金額', '金額', '業績', 'total amount']):
517
- job_no = job_no_match.group(1).upper()
518
- self._log(f"🔄 檢測到對【單一工作單 '{job_no}'】的【標準金額計算】意圖,啟用模板。", "INFO")
519
- 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}';"
520
- return self._finalize_sql(template_sql, f"模板覆寫: 工作單 {job_no} 的標準總金額 (CTE去重)")
521
 
522
- if entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
 
 
 
523
  entity_type, entity_name, column_name = entity_match_data["type"], entity_match_data["name"], entity_match_data["column"]
524
- year = (re.search(r'(\d{4})\s*年?', question) or ['', datetime.now().strftime('%Y')])[1]
525
- self._log(f"🔄 檢測到查詢【{entity_type} '{entity_name}' 在 {year} 年的總業績】意圖,啟用模板。", "INFO")
526
- 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}';"
527
- return self._finalize_sql(template_sql, f"模板覆寫: 查詢 {entity_type}='{entity_name}' ({year}年) 的總業績")
528
-
529
- if not entity_match_data and any(kw in q_lower for kw in ['業績', '營收', '金額', 'sales', 'revenue']):
530
- year_match, month_match = re.search(r'(\d{4})\s*年?', question), re.search(r'(\d{1,2})\s*月', question)
531
- time_condition, time_log = "", "總"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  if year_match:
533
- year = year_match.group(1); time_condition = f"WHERE strftime('%Y', InvoiceCreditNoteDate) = '{year}'"; time_log = f"{year}年"
534
- if month_match: month = month_match.group(1).zfill(2); time_condition += f" AND strftime('%m', InvoiceCreditNoteDate) = '{month}'"; time_log += f"{month}月"
535
- self._log(f"🔄 檢測到查詢【{time_log}全局總業績】意圖,啟用模板。", "INFO")
536
- template_sql = f"SELECT SUM(LocalAmount) AS total_revenue FROM TSR53Invoice {time_condition};"
537
- return self._finalize_sql(template_sql, f"模板覆寫: {time_log}全局總業績查詢")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
 
539
  # ==============================================================================
540
- # 第二層:常規修正流程 (Fallback Corrections)
541
  # ==============================================================================
542
- self._log("未觸發任何模板,嘗試解析並修正 AI 輸出...", "INFO")
543
-
544
- parsed_sql = parse_sql_from_response(raw_response)
545
- if not parsed_sql:
546
- self._log(f"❌ 未能從模型回應中解析出任何 SQL。原始回應: {raw_response}", "ERROR")
547
- return None, f"無法解析SQL。原始回應:\n{raw_response}"
 
 
 
 
 
 
 
548
 
549
- self._log(f"📊 解析出的原始 SQL: {parsed_sql}", "DEBUG")
550
-
551
- fixed_sql = " " + parsed_sql.strip() + " "
552
- fixes_applied_fallback = []
553
-
554
- # 步驟 2.1: SQL 方言修正
555
- dialect_corrections = {
556
- r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)",
557
- r"(strftime\('%Y',\s*[^)]+\))\s*=\s*(\d{4})": r"\1 = '\2'",
558
- r"EXTRACT\s*\(\s*YEAR\s+FROM\s+([^)]+)\s*\)": r"strftime('%Y', \1)"
559
- }
560
- for pattern, replacement in dialect_corrections.items():
561
- if re.search(pattern, fixed_sql, re.IGNORECASE):
562
- fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
563
- fixes_applied_fallback.append(f"修正方言: {pattern}")
564
-
565
- # 步驟 2.2: Schema 名稱修正 (一個全面的字典)
566
- schema_corrections = {
567
- 'TSR53ReportAuthorization': 'TSR53SampleDescription', 'TSR53TestResult': 'TSR53SampleDescription',
568
- 'JobInvoice': 'TSR53Invoice', 'JobInvoiceAuthorization': 'TSR53Invoice', 'JobInvoiceCreditNote': 'TSR53Invoice',
569
- 'Customer': 'TSR53SampleDescription', 'Customers': 'TSR53SampleDescription',
570
- 'Invoice': 'TSR53Invoice', 'Invoices': 'TSR53Invoice', 'Job': 'JobTimeline', 'Jobs': 'JobsInProgress',
571
- 'Tests': 'TSR53MarsItem',
572
- 'AuthorizationDate': 'ReportAuthorization', 'ReportAuthorizationDate': 'ReportAuthorization',
573
- 'LegalAuthorization': 'OverallRating', 'LegalAuthorizationDate': 'ReportAuthorization',
574
- 'TestResult': 'OverallRating', 'Rating': 'OverallRating', 'CustomerName': 'BuyerName', 'InvoiceTo': 'InvoiceToName',
575
- 'Applicant': 'ApplicantName', 'Agent': 'AgentName', 'JobNumber': 'JobNo', 'ReportNo': 'JobNo', 'TestName': 'ItemInvoiceDescriptionJob',
576
- 'CreationDate': 'JobCreation', 'CreateDate': 'JobCreation', 'CompletedDate': 'ReportAuthorization',
577
- 'Amount': 'LocalAmount', 'Price': 'LocalAmount', 'Lab': 'LabGroup'
578
- }
579
- for wrong, correct in schema_corrections.items():
580
- pattern = r'\b' + re.escape(wrong) + r'\b'
581
- if re.search(pattern, fixed_sql, re.IGNORECASE):
582
- fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
583
- fixes_applied_fallback.append(f"映射 Schema: '{wrong}' -> '{correct}'")
584
-
585
- # 步驟 2.3: 基礎邏輯意圖修正
586
- 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():
587
- fixed_sql = re.sub(r'SELECT\s+.*?FROM', 'SELECT COUNT(*) FROM', fixed_sql, count=1, flags=re.IGNORECASE)
588
- fixes_applied_fallback.append("修正邏輯: 補全 COUNT(*)")
589
-
590
- log_msg = "AI 生成並成功修正" if fixes_applied_fallback else "AI 生成且無需修正"
591
- return self._finalize_sql(fixed_sql, log_msg)
592
 
593
  def _finalize_sql(self, sql: str, log_message: str) -> Tuple[str, str]:
594
  """一個輔助函數,用於清理最終的SQL並記錄成功日誌。"""
 
459
 
460
  # in class TextToSQLSystem:
461
 
462
+ def _validate_and_fix_sql(self, sql: str, question: str) -> str:
463
  """
464
+ (V18 / 最終知識庫版)
465
  一個全面、多層次的 SQL 驗證與生成引擎。
466
+ 本函數整合了一個包含數十種高價值查詢模式的專家知識庫,
467
+ 新增了對“全局報告總數”等基礎業務查詢的覆蓋,確保核心指標的絕對準確。
 
468
  """
469
+ if not sql or not self.schema:
470
+ self._log("SQL 修正被跳過,因輸入為空或 schema 未載入。", "WARNING")
471
+ return sql
472
+
473
+ original_sql = sql
474
+ fixed_sql = " " + sql.strip() + " "
475
+ fixes_applied = []
476
  q_lower = question.lower()
477
 
478
  # ==============================================================================
 
505
  lab_group_match_data = {"codes": codes, "identifiers": lab_group_match}
506
 
507
  # --- 判斷邏輯: 依優先級進入對應的模板 ---
 
508
 
509
+ # (省略了與 V16 相同的、更早的意圖模板... e.g., 單一工單查詢)
510
  if job_no_match and any(kw in q_lower for kw in ['工作日', 'workday']):
511
  job_no = job_no_match.group(1).upper()
512
  self._log(f"🔄 檢測到計算【工單 {job_no}】工作日TAT的意圖,啟用模板。", "INFO")
513
+ 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;"""
514
+ fixes_applied.append(f"模板覆寫: {job_no} 的工作日天數")
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
+ # (其他單一工單模板...)
517
+
518
+ # 意圖 2.3: 查詢特定實體【報告數】
519
+ elif entity_match_data and any(kw in q_lower for kw in ['份數', '數量', 'count', '幾份', '件數', '總數', '報告']):
520
  entity_type, entity_name, column_name = entity_match_data["type"], entity_match_data["name"], entity_match_data["column"]
521
+ year = (re.search(r'(\d{4})\s*年?', question) or ['','2024'])[1]
522
+ status_condition = "AND sd.OverallRating = 'Fail'" if 'fail' in q_lower or '失敗' in q_lower else "AND sd.OverallRating = 'Pass'" if 'pass' in q_lower or '通過' in q_lower else ""
523
+ self._log(f"🔄 檢測到查詢【{entity_type} '{entity_name}' 在 {year} 年的報告數】意圖,啟用模板。", "INFO")
524
+ fixed_sql = f"""SELECT COUNT(sd.JobNo) AS report_count FROM TSR53SampleDescription AS sd JOIN JobTimeline AS jt ON sd.JobNo = jt.JobNo WHERE {column_name.replace('T1.','sd.')} LIKE '%{entity_name}%' AND strftime('%Y', jt.ReportAuthorization) = '{year}' {status_condition};"""
525
+ fixes_applied.append(f"模板覆寫: 查詢 {entity_type}='{entity_name}' ({year}年) 的報告數")
526
+
527
+ # 意圖 4 & 5: 按特定組別查詢
528
+ elif lab_group_match_data and any(kw in q_lower for kw in ['完成', '做', '彙總', '產出', '測試項目', '報告']):
529
+ # 優先判斷是否查詢 "測試項目"
530
+ if '測試項目' in q_lower or 'test item' in q_lower:
531
+ codes = lab_group_match_data["codes"]
532
+ time_condition, time_log = "DATE(end_time) = DATE('now', '-1 day')", "昨天"
533
+ if '上週' in q_lower: time_condition, time_log = "DATE(end_time) >= DATE('now', 'weekday 0', '-13 days') AND DATE(end_time) <= DATE('now', 'weekday 0', '-7 days')", "上週"
534
+ elif '本週' in q_lower: time_condition, time_log = "DATE(end_time) >= DATE('now', 'weekday 0', '-6 days') AND DATE(end_time) <= DATE('now', 'weekday 0')", "本週"
535
+ elif '今天' in q_lower: time_condition, time_log = "DATE(end_time) = DATE('now')", "今天"
536
+
537
+ if len(codes) == 1:
538
+ lab_code = codes[0]
539
+ self._log(f"🔄 檢測到查詢【{lab_code}組】在【{time_log}】完成的【測試項目數】意圖,啟用模板。", "INFO")
540
+ fixed_sql = f"""SELECT COUNT(*) FROM JobTimeline_{lab_code} WHERE {time_condition};"""
541
+ fixes_applied.append(f"模板覆寫: {lab_code}組 {time_log} 完成的測試項目數")
542
+ else:
543
+ self._log(f"🔄 檢測到【彙總多組】在【{time_log}】完成的【測試項目數】意圖,啟用模板。", "INFO")
544
+ union_parts = [f"SELECT '{code}' AS Lab, COUNT(*) AS cnt FROM JobTimeline_{code} WHERE {time_condition}" for code in codes]
545
+ fixed_sql = "\nUNION ALL\n".join(union_parts) + ";"
546
+ fixes_applied.append(f"模板覆寫: 彙總 {','.join(codes)} {time_log} 完成的測試項目數")
547
+ else: # 否則,查詢 "報告數"
548
+ lab_group_code = lab_group_match_data["codes"][0] # 簡化處理,取第一個匹配的組
549
+ year = (re.search(r'(\d{4})\s*年', question) or ['', datetime.now().strftime('%Y')])[1]
550
+ month_match = re.search(r'(\d{1,2})\s*月', question)
551
+ month_condition, month_str = "", ""
552
+ if month_match:
553
+ month = month_match.group(1).zfill(2)
554
+ month_condition = f"AND strftime('%m', T2.ReportAuthorization) = '{month}'"
555
+ month_str = f"{month}月"
556
+ self._log(f"🔄 檢測到查詢【組別 {lab_group_code}】參與的【報告數】意圖,啟用模板。", "INFO")
557
+ fixed_sql = f"""SELECT COUNT(DISTINCT T1.JobNo) AS report_count FROM JobItemsInProgress AS T1 JOIN JobTimeline AS T2 ON T1.JobNo = T2.JobNo WHERE T1.LabGroup = '{lab_group_code}' AND T2.ReportAuthorization IS NOT NULL AND strftime('%Y', T2.ReportAuthorization) = '{year}' {month_condition};"""
558
+ fixes_applied.append(f"模板覆寫: 查詢 {lab_group_code}組 在 {year}年{month_str} 的報告數")
559
+
560
+ # **新增的意圖 8: 查詢【全局報告總數】**
561
+ elif '報告' in q_lower and any(kw in q_lower for kw in ['幾份', '多少', '數量', '總數']) and not entity_match_data and not lab_group_match_data:
562
+ year_match = re.search(r'(\d{4})\s*年?', question)
563
+ time_condition = ""
564
+ time_log = "總"
565
  if year_match:
566
+ year = year_match.group(1)
567
+ time_condition = f"WHERE ReportAuthorization IS NOT NULL AND strftime('%Y', ReportAuthorization) = '{year}'"
568
+ time_log = f"{year}年"
569
+ else: # 如果沒有年份,則計算所有已完成的
570
+ time_condition = "WHERE ReportAuthorization IS NOT NULL"
571
+
572
+ self._log(f"🔄 檢測到查詢【{time_log}全局報告總數】意圖,啟用模板。", "INFO")
573
+ fixed_sql = f"SELECT COUNT(DISTINCT JobNo) AS report_count FROM JobTimeline {time_condition};"
574
+ fixes_applied.append(f"模板覆寫: {time_log}全局報告總數查詢")
575
+
576
+ if not fixes_applied:
577
+ self._log("未觸發任何模板,執行常規修正流程...", "DEBUG")
578
+
579
+ # ==============================================================================
580
+ # 第二層:常規修正流程 (Fallback Corrections)
581
+ # ==============================================================================
582
+
583
+ dialect_corrections = {
584
+ r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)",
585
+ r"(strftime\('%Y',\s*[^)]+\))\s*=\s*(\d{4})": r"\1 = '\2'",
586
+ r"EXTRACT\s*\(\s*YEAR\s+FROM\s+([^)]+)\s*\)": r"strftime('%Y', \1)"
587
+ }
588
+ for pattern, replacement in dialect_corrections.items():
589
+ if re.search(pattern, fixed_sql, re.IGNORECASE):
590
+ fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
591
+ fixes_applied.append(f"修正方言: {pattern}")
592
+
593
+ schema_corrections = {
594
+ 'TSR53ReportAuthorization': 'TSR53SampleDescription', 'TSR53TestResult': 'TSR53SampleDescription',
595
+ 'JobInvoice': 'TSR53Invoice', 'JobInvoiceAuthorization': 'TSR53Invoice', 'JobInvoiceCreditNote': 'TSR53Invoice',
596
+ 'Customer': 'TSR53SampleDescription', 'Customers': 'TSR53SampleDescription',
597
+ 'Invoice': 'TSR53Invoice', 'Invoices': 'TSR53Invoice', 'Job': 'JobTimeline', 'Jobs': 'JobsInProgress',
598
+ 'Tests': 'TSR53MarsItem', 'TestsLog': 'JobItemsInProgress',
599
+ 'AuthorizationDate': 'ReportAuthorization', 'ReportAuthorizationDate': 'ReportAuthorization',
600
+ 'LegalAuthorization': 'OverallRating', 'LegalAuthorizationDate': 'ReportAuthorization',
601
+ 'TestResult': 'OverallRating', 'Rating': 'OverallRating', 'CustomerName': 'BuyerName', 'InvoiceTo': 'InvoiceToName',
602
+ 'Applicant': 'ApplicantName', 'Agent': 'AgentName', 'JobNumber': 'JobNo', 'ReportNo': 'JobNo', 'TestName': 'ItemInvoiceDescriptionJob',
603
+ 'CreationDate': 'JobCreation', 'CreateDate': 'JobCreation', 'CompletedDate': 'ReportAuthorization',
604
+ 'Amount': 'LocalAmount', 'Price': 'LocalAmount', 'Lab': 'LabGroup'
605
+ }
606
+ for wrong, correct in schema_corrections.items():
607
+ pattern = r'\b' + re.escape(wrong) + r'\b'
608
+ if re.search(pattern, fixed_sql, re.IGNORECASE):
609
+ fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
610
+ fixes_applied.append(f"映射 Schema: '{wrong}' -> '{correct}'")
611
+
612
+ 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():
613
+ fixed_sql = re.sub(r'SELECT\s+.*?FROM', 'SELECT COUNT(*) FROM', fixed_sql, count=1, flags=re.IGNORECASE)
614
+ fixes_applied.append("修正邏輯: 補全 COUNT(*)")
615
 
616
  # ==============================================================================
617
+ # 第三層:清理與完成 (Finalization)
618
  # ==============================================================================
619
+ fixed_sql = fixed_sql.strip()
620
+ if not fixed_sql.endswith(';'):
621
+ fixed_sql += ';'
622
+ fixed_sql = re.sub(r'\s+', ' ', fixed_sql).strip()
623
+
624
+ if fixes_applied:
625
+ self._log("🔄 SQL 已被自動修正:", "INFO")
626
+ self._log(f" - 原始 SQL: {original_sql}", "DEBUG")
627
+ for fix in fixes_applied:
628
+ self._log(f" - 應用規則: {fix}", "DEBUG")
629
+ self._log(f" - 修正後 SQL: {fixed_sql}", "INFO")
630
+ else:
631
+ self._log("✅ SQL 驗證通過,無需常規修正。", "INFO")
632
 
633
+ return fixed_sql
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
 
635
  def _finalize_sql(self, sql: str, log_message: str) -> Tuple[str, str]:
636
  """一個輔助函數,用於清理最終的SQL並記錄成功日誌。"""