Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -596,11 +596,12 @@ SELECT
|
|
| 596 |
if m:
|
| 597 |
y1, y2 = m.group(1), m.group(2)
|
| 598 |
return (
|
| 599 |
-
"SELECT strftime('%Y-%m',
|
| 600 |
-
f"
|
| 601 |
-
f"
|
| 602 |
-
"FROM
|
| 603 |
-
|
|
|
|
| 604 |
"GROUP BY month ORDER BY month;"
|
| 605 |
)
|
| 606 |
|
|
@@ -609,33 +610,46 @@ SELECT
|
|
| 609 |
if m:
|
| 610 |
year = m.group(1)
|
| 611 |
return (
|
| 612 |
-
"SELECT strftime('%Y-%m',
|
| 613 |
-
"FROM
|
| 614 |
-
|
|
|
|
| 615 |
"GROUP BY month ORDER BY month;"
|
| 616 |
)
|
| 617 |
|
| 618 |
# 評級分布(Pass/Fail)
|
| 619 |
if ("評級" in q) or ("pass" in q_lower) or ("fail" in q_lower):
|
| 620 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
# 金額最高 Top N(預設 10)
|
| 623 |
m = re.search(r"金額.*?(?:最高|前|top)\s*(\d+)?", q_lower)
|
| 624 |
if m:
|
| 625 |
n = m.group(1) or "10"
|
| 626 |
-
return f"SELECT * FROM
|
| 627 |
|
| 628 |
# 客戶工作單數量最多 Top N
|
| 629 |
m = re.search(r"客戶.*?(?:最多|top|前)\s*(\d+)?", q_lower)
|
| 630 |
if m:
|
| 631 |
n = m.group(1) or "10"
|
| 632 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
|
| 634 |
# 昨天完成多少
|
| 635 |
if "昨天" in q:
|
| 636 |
return (
|
| 637 |
-
"SELECT COUNT(
|
| 638 |
-
"WHERE
|
|
|
|
| 639 |
)
|
| 640 |
|
| 641 |
return None
|
|
@@ -651,6 +665,28 @@ SELECT
|
|
| 651 |
self._log(f"最終整理 SQL 失敗: {e}", "ERROR")
|
| 652 |
return (sql_text or ""), status
|
| 653 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
def _validate_and_fix_sql(self, question: str, raw_response: str) -> Tuple[Optional[str], str]:
|
| 655 |
"""
|
| 656 |
(V29 / 穩健正則 + 智能計數) 多層次 SQL 生成:
|
|
@@ -741,7 +777,8 @@ SELECT
|
|
| 741 |
entity_name, column_name = entity_match_data['name'], entity_match_data['column']
|
| 742 |
match_op = '=' if column_name.endswith('ID') else 'LIKE'
|
| 743 |
entity_val = f"'%{entity_name}%'" if match_op == 'LIKE' else f"'{entity_name}'"
|
| 744 |
-
|
|
|
|
| 745 |
sql['log_parts'].append(entity_match_data['type'] + ":" + entity_name)
|
| 746 |
if intents.get('action') == 'list':
|
| 747 |
sql['select'].append("sd.BuyerName")
|
|
@@ -794,6 +831,9 @@ SELECT
|
|
| 794 |
salvage_sql = 'SELECT ' + resp
|
| 795 |
parsed_sql = parse_sql_from_response(salvage_sql) or salvage_sql
|
| 796 |
|
|
|
|
|
|
|
|
|
|
| 797 |
if not parsed_sql:
|
| 798 |
self._log(f"❌ 未能從模型回應中解析出任何 SQL。原始回應: {raw_response}", "ERROR")
|
| 799 |
return None, f"無法解析SQL。原始回應:\n{raw_response}"
|
|
@@ -842,45 +882,45 @@ SELECT
|
|
| 842 |
return self._finalize_sql(fixed_sql, status)
|
| 843 |
|
| 844 |
def _generate_fallback_sql(self, prompt: str) -> str:
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
|
| 885 |
def process_question(self, question: str) -> Tuple[str, str]:
|
| 886 |
"""處理使用者問題"""
|
|
|
|
| 596 |
if m:
|
| 597 |
y1, y2 = m.group(1), m.group(2)
|
| 598 |
return (
|
| 599 |
+
"SELECT strftime('%Y-%m', jt.ReportAuthorization) AS month, "
|
| 600 |
+
f"COUNT(DISTINCT CASE WHEN strftime('%Y', jt.ReportAuthorization)='{y1}' THEN jt.JobNo END) AS count_{y1}, "
|
| 601 |
+
f"COUNT(DISTINCT CASE WHEN strftime('%Y', jt.ReportAuthorization)='{y2}' THEN jt.JobNo END) AS count_{y2} "
|
| 602 |
+
"FROM JobTimeline AS jt "
|
| 603 |
+
"WHERE jt.ReportAuthorization IS NOT NULL "
|
| 604 |
+
f"AND strftime('%Y', jt.ReportAuthorization) IN ('{y1}','{y2}') "
|
| 605 |
"GROUP BY month ORDER BY month;"
|
| 606 |
)
|
| 607 |
|
|
|
|
| 610 |
if m:
|
| 611 |
year = m.group(1)
|
| 612 |
return (
|
| 613 |
+
"SELECT strftime('%Y-%m', jt.ReportAuthorization) AS month, COUNT(DISTINCT jt.JobNo) AS count "
|
| 614 |
+
"FROM JobTimeline AS jt "
|
| 615 |
+
"WHERE jt.ReportAuthorization IS NOT NULL "
|
| 616 |
+
f"AND strftime('%Y', jt.ReportAuthorization)='{year}' "
|
| 617 |
"GROUP BY month ORDER BY month;"
|
| 618 |
)
|
| 619 |
|
| 620 |
# 評級分布(Pass/Fail)
|
| 621 |
if ("評級" in q) or ("pass" in q_lower) or ("fail" in q_lower):
|
| 622 |
+
return (
|
| 623 |
+
"SELECT sd.OverallRating AS rating, COUNT(*) AS count "
|
| 624 |
+
"FROM TSR53SampleDescription AS sd "
|
| 625 |
+
"GROUP BY sd.OverallRating;"
|
| 626 |
+
)
|
| 627 |
|
| 628 |
# 金額最高 Top N(預設 10)
|
| 629 |
m = re.search(r"金額.*?(?:最高|前|top)\s*(\d+)?", q_lower)
|
| 630 |
if m:
|
| 631 |
n = m.group(1) or "10"
|
| 632 |
+
return f"SELECT iv.* FROM TSR53Invoice AS iv ORDER BY iv.LocalAmount DESC LIMIT {n};"
|
| 633 |
|
| 634 |
# 客戶工作單數量最多 Top N
|
| 635 |
m = re.search(r"客戶.*?(?:最多|top|前)\s*(\d+)?", q_lower)
|
| 636 |
if m:
|
| 637 |
n = m.group(1) or "10"
|
| 638 |
+
return (
|
| 639 |
+
f"SELECT sd.ApplicantName AS applicant, COUNT(DISTINCT jt.JobNo) AS count "
|
| 640 |
+
"FROM JobTimeline AS jt "
|
| 641 |
+
"JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo "
|
| 642 |
+
"WHERE jt.ReportAuthorization IS NOT NULL "
|
| 643 |
+
"GROUP BY sd.ApplicantName ORDER BY count DESC "
|
| 644 |
+
f"LIMIT {n};"
|
| 645 |
+
)
|
| 646 |
|
| 647 |
# 昨天完成多少
|
| 648 |
if "昨天" in q:
|
| 649 |
return (
|
| 650 |
+
"SELECT COUNT(DISTINCT jt.JobNo) AS count FROM JobTimeline AS jt "
|
| 651 |
+
"WHERE jt.ReportAuthorization IS NOT NULL "
|
| 652 |
+
"AND date(jt.ReportAuthorization)=date('now','-1 day');"
|
| 653 |
)
|
| 654 |
|
| 655 |
return None
|
|
|
|
| 665 |
self._log(f"最終整理 SQL 失敗: {e}", "ERROR")
|
| 666 |
return (sql_text or ""), status
|
| 667 |
|
| 668 |
+
def _regenerate_sql_strict(self, question: str) -> Optional[str]:
|
| 669 |
+
"""當模型輸出非 SQL 或無法解析時,使用嚴格限制的提示詞重生一次。"""
|
| 670 |
+
try:
|
| 671 |
+
rel = self._identify_relevant_tables(question)
|
| 672 |
+
schema_str = self._format_relevant_schema(rel)
|
| 673 |
+
strict_prompt = (
|
| 674 |
+
"You are a SQLite SQL generator.\n"
|
| 675 |
+
+ "Given the schema below and the question, output ONE valid SQL query only.\n\n"
|
| 676 |
+
+ "SCHEMA:\n" + schema_str + "\n\n"
|
| 677 |
+
+ "QUESTION:\n" + (question or "").strip() + "\n\n"
|
| 678 |
+
+ "Return only the final SQL query in a fenced code block (```sql ... ```). "
|
| 679 |
+
+ "The SQL must start with SELECT and end with a semicolon. No explanation."
|
| 680 |
+
)
|
| 681 |
+
raw = self.huggingface_api_call(strict_prompt)
|
| 682 |
+
sql = parse_sql_from_response(raw)
|
| 683 |
+
if sql:
|
| 684 |
+
self._log("🔁 嚴格模式重生成功。")
|
| 685 |
+
return sql
|
| 686 |
+
except Exception as e:
|
| 687 |
+
self._log(f"嚴格模式重生失敗: {e}", "ERROR")
|
| 688 |
+
return None
|
| 689 |
+
|
| 690 |
def _validate_and_fix_sql(self, question: str, raw_response: str) -> Tuple[Optional[str], str]:
|
| 691 |
"""
|
| 692 |
(V29 / 穩健正則 + 智能計數) 多層次 SQL 生成:
|
|
|
|
| 777 |
entity_name, column_name = entity_match_data['name'], entity_match_data['column']
|
| 778 |
match_op = '=' if column_name.endswith('ID') else 'LIKE'
|
| 779 |
entity_val = f"'%{entity_name}%'" if match_op == 'LIKE' else f"'{entity_name}'"
|
| 780 |
+
collate = " COLLATE NOCASE" if match_op == 'LIKE' else ""
|
| 781 |
+
sql['where'].append(f"{column_name} {match_op} {entity_val}{collate}")
|
| 782 |
sql['log_parts'].append(entity_match_data['type'] + ":" + entity_name)
|
| 783 |
if intents.get('action') == 'list':
|
| 784 |
sql['select'].append("sd.BuyerName")
|
|
|
|
| 831 |
salvage_sql = 'SELECT ' + resp
|
| 832 |
parsed_sql = parse_sql_from_response(salvage_sql) or salvage_sql
|
| 833 |
|
| 834 |
+
if not parsed_sql:
|
| 835 |
+
self._log("模型輸出非 SQL,啟用嚴格模式重生一次…")
|
| 836 |
+
parsed_sql = self._regenerate_sql_strict(q)
|
| 837 |
if not parsed_sql:
|
| 838 |
self._log(f"❌ 未能從模型回應中解析出任何 SQL。原始回應: {raw_response}", "ERROR")
|
| 839 |
return None, f"無法解析SQL。原始回應:\n{raw_response}"
|
|
|
|
| 882 |
return self._finalize_sql(fixed_sql, status)
|
| 883 |
|
| 884 |
def _generate_fallback_sql(self, prompt: str) -> str:
|
| 885 |
+
"""當模型不可用時的備用 SQL 生成"""
|
| 886 |
+
prompt_lower = (prompt or "").lower()
|
| 887 |
+
# 統計類:優先使用 JobTimeline.ReportAuthorization,避免不存在的 completed_time 欄位
|
| 888 |
+
if ("統計" in prompt) or ("數量" in prompt) or ("多少" in prompt) or ("count" in prompt_lower):
|
| 889 |
+
if ("月" in prompt) or ("per month" in prompt_lower) or ("monthly" in prompt_lower):
|
| 890 |
+
return (
|
| 891 |
+
"SELECT strftime('%Y-%m', jt.ReportAuthorization) AS month, "
|
| 892 |
+
"COUNT(DISTINCT jt.JobNo) AS count "
|
| 893 |
+
"FROM JobTimeline AS jt "
|
| 894 |
+
"WHERE jt.ReportAuthorization IS NOT NULL "
|
| 895 |
+
"GROUP BY month ORDER BY month;"
|
| 896 |
+
)
|
| 897 |
+
elif ("客戶" in prompt) or ("buyer" in prompt_lower) or ("applicant" in prompt_lower):
|
| 898 |
+
return (
|
| 899 |
+
"SELECT sd.ApplicantName AS applicant, COUNT(DISTINCT jt.JobNo) AS count "
|
| 900 |
+
"FROM JobTimeline AS jt "
|
| 901 |
+
"JOIN TSR53SampleDescription AS sd ON jt.JobNo = sd.JobNo "
|
| 902 |
+
"WHERE jt.ReportAuthorization IS NOT NULL "
|
| 903 |
+
"GROUP BY sd.ApplicantName ORDER BY count DESC;"
|
| 904 |
+
)
|
| 905 |
+
else:
|
| 906 |
+
return (
|
| 907 |
+
"SELECT COUNT(DISTINCT jt.JobNo) AS total_count "
|
| 908 |
+
"FROM JobTimeline AS jt "
|
| 909 |
+
"WHERE jt.ReportAuthorization IS NOT NULL;"
|
| 910 |
+
)
|
| 911 |
+
# 金額彙總
|
| 912 |
+
if ("金額" in prompt) or ("總額" in prompt) or ("amount" in prompt_lower) or ("sum" in prompt_lower):
|
| 913 |
+
return "SELECT SUM(LocalAmount) AS total_amount FROM TSR53Invoice;"
|
| 914 |
+
# 評級分布
|
| 915 |
+
if ("評級" in prompt) or ("rating" in prompt_lower) or ("pass" in prompt_lower) or ("fail" in prompt_lower):
|
| 916 |
+
return "SELECT OverallRating AS rating, COUNT(*) AS count FROM TSR53SampleDescription GROUP BY OverallRating;"
|
| 917 |
+
# 通用後備:最近 10 筆報告
|
| 918 |
+
return (
|
| 919 |
+
"SELECT jt.JobNo, jt.ReportAuthorization "
|
| 920 |
+
"FROM JobTimeline AS jt "
|
| 921 |
+
"WHERE jt.ReportAuthorization IS NOT NULL "
|
| 922 |
+
"ORDER BY jt.ReportAuthorization DESC LIMIT 10;"
|
| 923 |
+
)
|
| 924 |
|
| 925 |
def process_question(self, question: str) -> Tuple[str, str]:
|
| 926 |
"""處理使用者問題"""
|