Paul720810 commited on
Commit
72a95e8
·
verified ·
1 Parent(s): 587a01f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -133
app.py CHANGED
@@ -19,7 +19,8 @@ import torch.nn.functional as F
19
  # ==================== 配置區 ====================
20
  DATASET_REPO_ID = "Paul720810/Text-to-SQL-Softline"
21
  GGUF_REPO_ID = "Paul720810/gguf-models"
22
- GGUF_FILENAME = "qwen2.5-coder-1.5b-sql-finetuned.q4_k_m.gguf"
 
23
 
24
  # 添加這一行:你的原始微調模型路徑
25
  FINETUNED_MODEL_PATH = "Paul720810/qwen2.5-coder-1.5b-sql-finetuned" # ← 新增這行
@@ -485,11 +486,10 @@ 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
 
@@ -497,7 +497,7 @@ class TextToSQLSystem:
497
  # 第一層:高價值意圖識別與模板覆寫 (Intent Recognition & Templating)
498
  # ==============================================================================
499
 
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'}
@@ -506,120 +506,70 @@ class TextToSQLSystem:
506
  match = re.search(fr"{re.escape(keyword)}[\s:;\'\"-]*([a-zA-Z0-9&.\s-]+?)(?:\s*的|\s+|為|$)", question, re.IGNORECASE)
507
  if match: entity_match_data = {"type": keyword, "name": match.group(1).strip(), "column": column}; break
508
 
509
- lab_group_match_data = None
510
- 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'}
511
- lab_group_match = re.findall(r"([A-Z]+)\s*組", question, re.IGNORECASE)
512
- if lab_group_match:
513
- codes = [LAB_GROUP_MAP.get(g.upper()) for g in lab_group_match if LAB_GROUP_MAP.get(g.upper())]
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)
546
- GROUP BY JobNo
547
- )
548
- SELECT JobNo, TotalAmount
549
- FROM JobTotalAmount
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'")
567
- time_log += " Fail"
568
  elif 'pass' in q_lower or '通過' in q_lower:
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:
580
- year = year_match.group(1)
581
- time_condition = f"WHERE ReportAuthorization IS NOT NULL AND strftime('%Y', ReportAuthorization) = '{year}'"
582
- time_log = f"{year}年"
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]
610
- month_match = re.search(r'(\d{1,2})\s*月', question)
611
- time_condition = f"strftime('%Y', end_time) = '{year}'"
612
- month_str = ""
613
- if month_match:
614
- month = month_match.group(1).zfill(2)
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
 
@@ -633,39 +583,18 @@ LIMIT {limit};
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)
 
19
  # ==================== 配置區 ====================
20
  DATASET_REPO_ID = "Paul720810/Text-to-SQL-Softline"
21
  GGUF_REPO_ID = "Paul720810/gguf-models"
22
+ #GGUF_FILENAME = "qwen2.5-coder-1.5b-sql-finetuned.q4_k_m.gguf"
23
+ GGUF_FILENAME = "qwen2.5-coder-1.5b-sql-finetuned.q8_0.gguf"
24
 
25
  # 添加這一行:你的原始微調模型路徑
26
  FINETUNED_MODEL_PATH = "Paul720810/qwen2.5-coder-1.5b-sql-finetuned" # ← 新增這行
 
486
 
487
  def _validate_and_fix_sql(self, question: str, raw_response: str) -> Tuple[Optional[str], str]:
488
  """
489
+ (V21 / 终极智能列表版)
490
+ 一个全面、多层次的 SQL 验证与生成引擎。
491
+ 本函数作为第一决策者,优先匹配用户问题与专家知识库。
492
+ “报告列表”模板已被极致强化,能够动态处理时间、状态、实体等多重条件的任意组合。
 
493
  """
494
  q_lower = question.lower()
495
 
 
497
  # 第一層:高價值意圖識別與模板覆寫 (Intent Recognition & Templating)
498
  # ==============================================================================
499
 
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'}
 
506
  match = re.search(fr"{re.escape(keyword)}[\s:;\'\"-]*([a-zA-Z0-9&.\s-]+?)(?:\s*的|\s+|為|$)", question, re.IGNORECASE)
507
  if match: entity_match_data = {"type": keyword, "name": match.group(1).strip(), "column": column}; break
508
 
509
+ # --- 判断逻辑: 依优先级进入对应的模板 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
 
511
+ # 意图 0: 单一工单查询 (最高优先级)
512
+ if job_no_match:
513
  job_no = job_no_match.group(1).upper()
514
+ if any(kw in q_lower for kw in ['工作日', 'workday']):
515
+ self._log(f"🔄 檢測到計算【工單 {job_no}】工作日TAT的意圖,啟用模板。", "INFO")
516
+ 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;"
517
+ return self._finalize_sql(template_sql, f"模板覆寫: {job_no} 的工作日天數")
518
+ if any(kw in q_lower for kw in ['總處理時長', '時長', '多少天']):
519
+ self._log(f"🔄 檢測到計算【工單 {job_no}】日曆日TAT的意圖,啟用模板。", "INFO")
520
+ template_sql = f"SELECT ROUND(julianday(ReportAuthorization) - julianday(JobCreation), 2) AS days FROM JobTimeline WHERE JobNo = '{job_no}';"
521
+ return self._finalize_sql(template_sql, f"模板覆寫: {job_no} 的日曆日總時長")
522
+ if any(kw in q_lower for kw in ['總金額', '金額', '業績', 'total amount']):
523
+ self._log(f"🔄 檢測到對【單一工作單 '{job_no}'】的【標準金額計算】意圖,啟用模板。", "INFO")
524
+ 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}';"
525
+ return self._finalize_sql(template_sql, f"模板覆寫: 工作單 {job_no} 的標準總金額 (CTE去重)")
526
+
527
+ # 意图 1: 报告列表查询 (高优先级)
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  if any(kw in q_lower for kw in ['報告號碼', '報告清單', '列出報告', 'report number', 'list of reports']):
529
  year_match = re.search(r'(\d{4})\s*年?', question)
530
  month_match = re.search(r'(\d{1,2})\s*月', question)
531
  from_clause = "FROM JobTimeline AS jt"
532
+ select_clause = "SELECT jt.JobNo, jt.ReportAuthorization"
533
  where_conditions = ["jt.ReportAuthorization IS NOT NULL"]
534
+ log_parts = []
535
+
536
  if year_match:
537
+ year = year_match.group(1); where_conditions.append(f"strftime('%Y', jt.ReportAuthorization) = '{year}'"); log_parts.append(f"{year}年")
538
+ if month_match:
539
+ month = month_match.group(1).zfill(2); where_conditions.append(f"strftime('%m', jt.ReportAuthorization) = '{month}'"); log_parts.append(f"{month}月")
540
+
541
  if 'fail' in q_lower or '失敗' in q_lower:
542
  if "JOIN TSR53SampleDescription" not in from_clause: from_clause = "FROM JobTimeline AS jt JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo"
543
+ where_conditions.append("sd.OverallRating = 'Fail'"); log_parts.append("Fail")
 
544
  elif 'pass' in q_lower or '通過' in q_lower:
545
  if "JOIN TSR53SampleDescription" not in from_clause: from_clause = "FROM JobTimeline AS jt JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo"
546
+ where_conditions.append("sd.OverallRating = 'Pass'"); log_parts.append("Pass")
547
+
548
+ if entity_match_data:
549
+ entity_name, column_name = entity_match_data["name"], entity_match_data["column"]
550
+ if "JOIN TSR53SampleDescription" not in from_clause: from_clause = "FROM JobTimeline AS jt JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo"
551
+ where_conditions.append(f"{column_name} LIKE '%{entity_name}%'"); log_parts.append(entity_name)
552
+ select_clause = "SELECT jt.JobNo, sd.BuyerName, jt.ReportAuthorization"
553
+
554
  final_where_clause = "WHERE " + " AND ".join(where_conditions)
555
+ time_log = " ".join(log_parts)
556
+ self._log(f"🔄 檢測到查詢【{time_log} 報告列表】意圖,啟用智能模板。", "INFO")
557
+ template_sql = f"{select_clause} {from_clause} {final_where_clause} ORDER BY jt.ReportAuthorization DESC;"
558
  return self._finalize_sql(template_sql, f"模板覆寫: {time_log} 報告列表查詢")
559
 
560
+ # 意图 2: 纯计数查询 (中等优先级)
561
+ if '報告' in q_lower and any(kw in q_lower for kw in ['幾份', '多少', '數量', '總數']) and not entity_match_data:
562
  year_match = re.search(r'(\d{4})\s*年?', question)
563
+ time_condition, time_log = "WHERE ReportAuthorization IS NOT NULL", ""
564
+ if year_match: year = year_match.group(1); time_condition += f" AND strftime('%Y', ReportAuthorization) = '{year}'"; time_log = f"{year}年"
 
 
 
 
 
565
  self._log(f"🔄 檢測到查詢【{time_log}全局報告總數】意圖,啟用模板。", "INFO")
566
  template_sql = f"SELECT COUNT(DISTINCT JobNo) AS report_count FROM JobTimeline {time_condition};"
567
  return self._finalize_sql(template_sql, f"模板覆寫: {time_log}全局報告總數查詢")
568
+
569
+ # ... (此处可以继续添加 V17 版本中的其他 if/elif 模板)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
 
571
  # ==============================================================================
572
+ # 第二层:常规修正流程 (Fallback Corrections)
573
  # ==============================================================================
574
  self._log("未觸發任何模板,嘗試解析並修正 AI 輸出...", "INFO")
575
 
 
583
  fixed_sql = " " + parsed_sql.strip() + " "
584
  fixes_applied_fallback = []
585
 
586
+ # 完整的修正字典
587
+ dialect_corrections = {r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)",r"(strftime\('%Y',\s*[^)]+\))\s*=\s*(\d{4})": r"\1 = '\2'",r"EXTRACT\s*\(\s*YEAR\s+FROM\s+([^)]+)\s*\)": r"strftime('%Y', \1)"}
588
+ for p, r in dialect_corrections.items():
589
+ if re.search(p, fixed_sql, re.IGNORECASE): fixed_sql = re.sub(p, r, fixed_sql, flags=re.IGNORECASE); fixes_applied_fallback.append(f"修正方言: {p}")
590
+
591
+ schema_corrections = {'TSR53ReportAuthorization':'TSR53SampleDescription','TSR53TestResult':'TSR53SampleDescription','JobInvoice':'TSR53Invoice','JobInvoiceAuthorization':'TSR53Invoice','JobInvoiceCreditNote':'TSR53Invoice','Customer':'TSR53SampleDescription','Customers':'TSR53SampleDescription','Invoice':'TSR53Invoice','Invoices':'TSR53Invoice','Job':'JobTimeline','Jobs':'JobsInProgress','Tests':'TSR53MarsItem','TestsLog':'JobItemsInProgress','AuthorizationDate':'ReportAuthorization','ReportAuthorizationDate':'ReportAuthorization','LegalAuthorization':'OverallRating','LegalAuthorizationDate':'ReportAuthorization','TestResult':'OverallRating','Rating':'OverallRating','CustomerName':'BuyerName','InvoiceTo':'InvoiceToName','Applicant':'ApplicantName','Agent':'AgentName','JobNumber':'JobNo','ReportNo':'JobNo','TestName':'ItemInvoiceDescriptionJob','CreationDate':'JobCreation','CreateDate':'JobCreation','CompletedDate':'ReportAuthorization','InvoiceCreditNoteAmount':'LocalAmount','Amount':'LocalAmount','Price':'LocalAmount','Lab':'LabGroup'}
592
+ for w, c in schema_corrections.items():
593
+ p = r'\b' + re.escape(w) + r'\b'
594
+ if re.search(p, fixed_sql, re.IGNORECASE): fixed_sql = re.sub(p, c, fixed_sql, flags=re.IGNORECASE); fixes_applied_fallback.append(f"映射 Schema: '{w}' -> '{c}'")
595
+
596
+ 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():
597
+ fixed_sql = re.sub(r'SELECT\s+.*?FROM', 'SELECT COUNT(*) FROM', fixed_sql, count=1, flags=re.IGNORECASE); fixes_applied_fallback.append("修正邏輯: 補全 COUNT(*)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
 
599
  log_msg = "AI 生成並成功修正" if fixes_applied_fallback else "AI 生成且無需修正"
600
  return self._finalize_sql(fixed_sql, log_msg)