botrun-flow-lang 5.12.263__py3-none-any.whl → 5.12.264__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- botrun_flow_lang/api/auth_api.py +39 -39
- botrun_flow_lang/api/auth_utils.py +183 -183
- botrun_flow_lang/api/botrun_back_api.py +65 -65
- botrun_flow_lang/api/flow_api.py +3 -3
- botrun_flow_lang/api/hatch_api.py +508 -508
- botrun_flow_lang/api/langgraph_api.py +811 -811
- botrun_flow_lang/api/line_bot_api.py +1484 -1484
- botrun_flow_lang/api/model_api.py +300 -300
- botrun_flow_lang/api/rate_limit_api.py +32 -32
- botrun_flow_lang/api/routes.py +79 -79
- botrun_flow_lang/api/search_api.py +53 -53
- botrun_flow_lang/api/storage_api.py +395 -395
- botrun_flow_lang/api/subsidy_api.py +290 -290
- botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
- botrun_flow_lang/api/user_setting_api.py +70 -70
- botrun_flow_lang/api/version_api.py +31 -31
- botrun_flow_lang/api/youtube_api.py +26 -26
- botrun_flow_lang/constants.py +13 -13
- botrun_flow_lang/langgraph_agents/agents/agent_runner.py +178 -178
- botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
- botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gemini_subsidy_graph.py +460 -460
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
- botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +723 -723
- botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
- botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
- botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
- botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
- botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
- botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
- botrun_flow_lang/langgraph_agents/agents/util/local_files.py +419 -419
- botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
- botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
- botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +486 -486
- botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +250 -250
- botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +204 -204
- botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
- botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
- botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
- botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
- botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
- botrun_flow_lang/llm_agent/llm_agent.py +19 -19
- botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
- botrun_flow_lang/log/.gitignore +2 -2
- botrun_flow_lang/main.py +61 -61
- botrun_flow_lang/main_fast.py +51 -51
- botrun_flow_lang/mcp_server/__init__.py +10 -10
- botrun_flow_lang/mcp_server/default_mcp.py +744 -744
- botrun_flow_lang/models/nodes/utils.py +205 -205
- botrun_flow_lang/models/token_usage.py +34 -34
- botrun_flow_lang/requirements.txt +21 -21
- botrun_flow_lang/services/base/firestore_base.py +30 -30
- botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
- botrun_flow_lang/services/hatch/hatch_fs_store.py +419 -419
- botrun_flow_lang/services/storage/storage_cs_store.py +206 -206
- botrun_flow_lang/services/storage/storage_factory.py +12 -12
- botrun_flow_lang/services/storage/storage_store.py +65 -65
- botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
- botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
- botrun_flow_lang/static/docs/tools/index.html +926 -926
- botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
- botrun_flow_lang/tests/api_stress_test.py +357 -357
- botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
- botrun_flow_lang/tests/test_botrun_app.py +46 -46
- botrun_flow_lang/tests/test_html_util.py +31 -31
- botrun_flow_lang/tests/test_img_analyzer.py +190 -190
- botrun_flow_lang/tests/test_img_util.py +39 -39
- botrun_flow_lang/tests/test_local_files.py +114 -114
- botrun_flow_lang/tests/test_mermaid_util.py +103 -103
- botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
- botrun_flow_lang/tests/test_plotly_util.py +151 -151
- botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
- botrun_flow_lang/tools/generate_docs.py +133 -133
- botrun_flow_lang/tools/templates/tools.html +153 -153
- botrun_flow_lang/utils/__init__.py +7 -7
- botrun_flow_lang/utils/botrun_logger.py +344 -344
- botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
- botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
- botrun_flow_lang/utils/google_drive_utils.py +654 -654
- botrun_flow_lang/utils/langchain_utils.py +324 -324
- botrun_flow_lang/utils/yaml_utils.py +9 -9
- {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/METADATA +1 -1
- botrun_flow_lang-5.12.264.dist-info/RECORD +102 -0
- botrun_flow_lang-5.12.263.dist-info/RECORD +0 -102
- {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/WHEEL +0 -0
|
@@ -1,822 +1,822 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import time
|
|
3
|
-
import asyncio
|
|
4
|
-
import logging
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from typing import List, Dict, Any, Optional, Annotated
|
|
7
|
-
from typing_extensions import TypedDict
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
9
|
-
|
|
10
|
-
from langchain_core.messages import SystemMessage, AIMessage, HumanMessage, ToolMessage
|
|
11
|
-
from langchain_core.runnables.config import RunnableConfig
|
|
12
|
-
|
|
13
|
-
from langgraph.graph import StateGraph, START, END
|
|
14
|
-
from langgraph.graph import MessagesState
|
|
15
|
-
from langgraph.checkpoint.memory import MemorySaver
|
|
16
|
-
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
17
|
-
|
|
18
|
-
from botrun_flow_lang.langgraph_agents.agents.util.perplexity_search import (
|
|
19
|
-
respond_with_perplexity_search,
|
|
20
|
-
)
|
|
21
|
-
from botrun_flow_lang.langgraph_agents.agents.util.tavily_search import (
|
|
22
|
-
respond_with_tavily_search,
|
|
23
|
-
)
|
|
24
|
-
from botrun_flow_lang.langgraph_agents.agents.util.model_utils import (
|
|
25
|
-
get_model_instance,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
from dotenv import load_dotenv
|
|
29
|
-
|
|
30
|
-
load_dotenv()
|
|
31
|
-
|
|
32
|
-
logging.basicConfig(
|
|
33
|
-
level=logging.INFO,
|
|
34
|
-
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
35
|
-
datefmt="%Y-%m-%d %H:%M:%S",
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
# 節點名稱常數
|
|
39
|
-
TOPIC_DECOMPOSITION_NODE = "topic_decomposition_node"
|
|
40
|
-
PARALLEL_SEARCH_NODE = "parallel_search_node"
|
|
41
|
-
REASONING_ANALYSIS_NODE = "reasoning_analysis_node"
|
|
42
|
-
COMPUTATION_VERIFICATION_NODE = "computation_verification_node"
|
|
43
|
-
HALLUCINATION_VERIFICATION_NODE = "hallucination_verification_node"
|
|
44
|
-
SUMMARY_RESPONSE_NODE = "summary_response_node"
|
|
45
|
-
|
|
46
|
-
# 預設 General Guide
|
|
47
|
-
DEFAULT_GENERAL_GUIDE = """
|
|
48
|
-
<General Guide>
|
|
49
|
-
妳回應時會採用臺灣繁體中文,並且避免中國大陸用語
|
|
50
|
-
妳絕對不會使用 markdown 語法回應
|
|
51
|
-
但是你絕對不會使用 ** 或者 ### ,各種類型的 markdown 語法都禁止使用
|
|
52
|
-
如果要比較美觀排版的話,妳可以搭配使用 emoji or 純文字 or 斷行 or 空白 來展示你想講的
|
|
53
|
-
每一個 step 的前面增添適當斷行
|
|
54
|
-
每個分段的標題「前面」要增添適當 emoji (這個 emoji 挑選必須跟動態情境吻合)
|
|
55
|
-
</General Guide>
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
# 題目拆解提示詞模板
|
|
59
|
-
TOPIC_DECOMPOSITION_PROMPT = """
|
|
60
|
-
{general_guide}
|
|
61
|
-
|
|
62
|
-
你是一位專業的政府研究分析師,負責將使用者的政府相關問題進行智能分析和拆解。
|
|
63
|
-
|
|
64
|
-
你的任務:
|
|
65
|
-
1. 分析用戶提問的複雜度
|
|
66
|
-
2. 如果是單純問題:轉化為更細緻的單一子題目
|
|
67
|
-
3. 如果是複雜問題:拆解為多個子題目以確保回答的準確性
|
|
68
|
-
|
|
69
|
-
重要考量因素:
|
|
70
|
-
- 考量使用者的身份、年齡、性別、居住地等個人條件
|
|
71
|
-
- 考量時間性:政策、法規的生效日期、申請期限、變更時程
|
|
72
|
-
- 考量地域性:中央 vs 地方政府、縣市差異、區域特殊規定
|
|
73
|
-
- 考量適用性:不同身份別、不同條件下的差異化規定
|
|
74
|
-
|
|
75
|
-
請將用戶問題拆解為 1-5 個具體的子題目,每個子題目都應該:
|
|
76
|
-
- 明確且具體,但涵蓋全面性考量
|
|
77
|
-
- 可以透過搜尋找到答案
|
|
78
|
-
- 與政府政策、法規、程序相關
|
|
79
|
-
- 盡量包含多個思考面向:時效性、地域性、身份差異、適用條件等
|
|
80
|
-
- 每個子題目都要設計得廣泛且深入,以獲取豐富的搜尋資訊
|
|
81
|
-
- 使用繁體中文表達
|
|
82
|
-
|
|
83
|
-
**重要指導原則:**
|
|
84
|
-
雖然子題目數量限制在 1-5 個,但每個子題目都要做全面性的考量,
|
|
85
|
-
盡量把思考面設定廣泛,這樣搜尋時才能獲得更多角度的資訊,
|
|
86
|
-
最終總結時就會有比較豐富的資料可以使用。
|
|
87
|
-
|
|
88
|
-
用戶問題:{user_question}
|
|
89
|
-
|
|
90
|
-
請輸出結構化的子題目列表。
|
|
91
|
-
"""
|
|
92
|
-
|
|
93
|
-
# 推理分析提示詞模板
|
|
94
|
-
REASONING_ANALYSIS_PROMPT = """
|
|
95
|
-
{general_guide}
|
|
96
|
-
|
|
97
|
-
你是一位專業的政府研究分析師,負責基於搜尋結果和內建知識進行縝密推理。
|
|
98
|
-
|
|
99
|
-
你的任務:
|
|
100
|
-
1. 基於搜尋結果和內建知識進行推理
|
|
101
|
-
2. 分析常見錯誤後進行縝密推理
|
|
102
|
-
3. 逐一回答所有子題目
|
|
103
|
-
4. 保持客觀與準確
|
|
104
|
-
|
|
105
|
-
搜尋結果:
|
|
106
|
-
{search_results}
|
|
107
|
-
|
|
108
|
-
子題目:
|
|
109
|
-
{subtopics}
|
|
110
|
-
|
|
111
|
-
請針對每個子題目提供詳細的推理分析,確保:
|
|
112
|
-
- 基於事實與證據
|
|
113
|
-
- 引用具體的搜尋來源
|
|
114
|
-
- 避免推測與臆斷
|
|
115
|
-
- 提供清晰的結論
|
|
116
|
-
"""
|
|
117
|
-
|
|
118
|
-
# 幻覺驗證提示詞模板
|
|
119
|
-
HALLUCINATION_VERIFICATION_PROMPT = """
|
|
120
|
-
{general_guide}
|
|
121
|
-
|
|
122
|
-
你是一位獨立的審查專家,負責以懷疑的角度檢視前述所有分析結果。
|
|
123
|
-
|
|
124
|
-
你的使命:
|
|
125
|
-
1. 假設前面結果有高機率的錯誤
|
|
126
|
-
2. 識別可能的AI幻覺位置
|
|
127
|
-
3. 透過延伸搜尋證明或反駁前面的結論
|
|
128
|
-
4. 提供客觀的驗證報告
|
|
129
|
-
|
|
130
|
-
前面的分析結果:
|
|
131
|
-
{previous_results}
|
|
132
|
-
|
|
133
|
-
請進行幻覺驗證,特別注意:
|
|
134
|
-
- 事實性錯誤
|
|
135
|
-
- 過度推理
|
|
136
|
-
- 來源不可靠
|
|
137
|
-
- 時效性問題
|
|
138
|
-
- 法規變更
|
|
139
|
-
|
|
140
|
-
如發現問題,請提供修正建議。
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
# 匯總回答提示詞模板
|
|
144
|
-
SUMMARY_RESPONSE_PROMPT = """
|
|
145
|
-
{general_guide}
|
|
146
|
-
|
|
147
|
-
你是一位專業的政府資訊服務專員,負責提供最終的完整回答。
|
|
148
|
-
|
|
149
|
-
**重要要求:你的回應必須完全基於以下提供的資訊,絕對不能使用你自己的知識或進行額外推測**
|
|
150
|
-
|
|
151
|
-
你的任務:
|
|
152
|
-
1. 提供「精準回答」:簡潔的結論
|
|
153
|
-
2. 提供「詳實回答」:完整的推理過程和引證
|
|
154
|
-
3. 使用適當的 emoji 輔助閱讀
|
|
155
|
-
4. 根據目標受眾調整語氣和格式
|
|
156
|
-
5. **所有回答內容必須嚴格基於「推理分析」和「計算驗證」的結果**
|
|
157
|
-
|
|
158
|
-
所有處理結果:
|
|
159
|
-
- 原始問題:{original_question}
|
|
160
|
-
- 子題目:{subtopics}
|
|
161
|
-
- 搜尋結果:{search_results}
|
|
162
|
-
- 推理分析:{reasoning_results}
|
|
163
|
-
- 計算驗證:{computation_results}
|
|
164
|
-
|
|
165
|
-
**回答原則:**
|
|
166
|
-
- 只使用「推理分析」和「計算驗證」中明確提到的資訊
|
|
167
|
-
- 如果某個問題在這些資訊中沒有充分說明,請明確指出資訊不足
|
|
168
|
-
- 不要添加任何未在上述資訊中出現的內容
|
|
169
|
-
- 確保所有結論都有明確的來源依據
|
|
170
|
-
|
|
171
|
-
請提供結構化的最終回答,包含:
|
|
172
|
-
📋 精準回答(簡潔版)
|
|
173
|
-
📖 詳實回答(完整版)
|
|
174
|
-
🔗 參考資料來源
|
|
175
|
-
"""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
class SubTopic(BaseModel):
|
|
179
|
-
"""子題目結構"""
|
|
180
|
-
|
|
181
|
-
topic: str
|
|
182
|
-
description: str
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
class SubTopicList(BaseModel):
|
|
186
|
-
"""子題目列表"""
|
|
187
|
-
|
|
188
|
-
subtopics: List[SubTopic]
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
class SearchResult(BaseModel):
|
|
192
|
-
"""搜尋結果結構"""
|
|
193
|
-
|
|
194
|
-
subtopic: str
|
|
195
|
-
content: str
|
|
196
|
-
sources: List[str]
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
class ReasoningResult(BaseModel):
|
|
200
|
-
"""推理結果結構"""
|
|
201
|
-
|
|
202
|
-
subtopic: str
|
|
203
|
-
analysis: str
|
|
204
|
-
conclusion: str
|
|
205
|
-
confidence: float
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
class VerificationResult(BaseModel):
|
|
209
|
-
"""驗證結果結構"""
|
|
210
|
-
|
|
211
|
-
issues_found: List[str]
|
|
212
|
-
corrections: List[str]
|
|
213
|
-
confidence_adjustments: Dict[str, float]
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
# LangGraph Assistant 配置 Schema
|
|
217
|
-
class GovResearcherConfigSchema(BaseModel):
|
|
218
|
-
"""政府研究員助手配置 Schema - 可在 LangGraph UI 中設定"""
|
|
219
|
-
|
|
220
|
-
# 模型選擇
|
|
221
|
-
decomposition_model: str = Field(default="gemini-2.5-pro") # 題目拆解模型
|
|
222
|
-
reasoning_model: str # 推理分析模型
|
|
223
|
-
computation_model: str # 計算驗證模型
|
|
224
|
-
verification_model: str # 幻覺驗證模型
|
|
225
|
-
summary_model: str # 匯總回答模型
|
|
226
|
-
|
|
227
|
-
# 搜尋引擎設定
|
|
228
|
-
search_vendor: str # "perplexity" | "tavily"
|
|
229
|
-
search_model: str # 搜尋模型名稱
|
|
230
|
-
max_parallel_searches: int # 最大並行搜尋數量
|
|
231
|
-
|
|
232
|
-
# 提示詞模板(可動態設定)
|
|
233
|
-
general_guide: Optional[str] # 通用指導原則
|
|
234
|
-
topic_decomposition_prompt: Optional[str] # 題目拆解提示詞
|
|
235
|
-
reasoning_analysis_prompt: Optional[str] # 推理分析提示詞
|
|
236
|
-
hallucination_verification_prompt: Optional[str] # 幻覺驗證提示詞
|
|
237
|
-
summary_response_prompt: Optional[str] # 匯總回答提示詞
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
class GovResearcherState(MessagesState):
|
|
241
|
-
"""政府研究員 LangGraph 狀態"""
|
|
242
|
-
|
|
243
|
-
original_question: str = ""
|
|
244
|
-
decomposed_topics: List[SubTopic] = []
|
|
245
|
-
search_tasks: List[SubTopic] = []
|
|
246
|
-
search_results: Annotated[List[SearchResult], lambda x, y: x + y] = (
|
|
247
|
-
[]
|
|
248
|
-
) # 支援 fan-in 合併
|
|
249
|
-
reasoning_results: List[ReasoningResult] = []
|
|
250
|
-
computation_results: Optional[str] = None
|
|
251
|
-
needs_computation: bool = False
|
|
252
|
-
hallucination_check: Optional[VerificationResult] = None
|
|
253
|
-
final_answer: str = ""
|
|
254
|
-
general_guide: str = DEFAULT_GENERAL_GUIDE
|
|
255
|
-
search_completed: bool = False
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def format_dates(dt):
|
|
259
|
-
"""將日期時間格式化為西元和民國格式"""
|
|
260
|
-
western_date = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
261
|
-
taiwan_year = dt.year - 1911
|
|
262
|
-
taiwan_date = f"{taiwan_year}-{dt.strftime('%m-%d %H:%M:%S')}"
|
|
263
|
-
return {"western_date": western_date, "taiwan_date": taiwan_date}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def get_config_value(config: RunnableConfig, key: str, default_value: Any) -> Any:
|
|
267
|
-
"""統一獲取配置值的輔助函數"""
|
|
268
|
-
# 如果 config.get("configurable", {}).get(key, default_value) 是 None,則返回 default_value
|
|
269
|
-
return config.get("configurable", {}).get(key, default_value) or default_value
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def get_decomposition_model(config: RunnableConfig):
|
|
273
|
-
"""獲取題目拆解用的模型"""
|
|
274
|
-
model_name = get_config_value(config, "decomposition_model", "gemini-2.5-pro")
|
|
275
|
-
return get_model_instance(model_name, temperature=0)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def get_reasoning_model(config: RunnableConfig):
|
|
279
|
-
"""獲取推理分析用的模型"""
|
|
280
|
-
model_name = get_config_value(config, "reasoning_model", "gemini-2.5-flash")
|
|
281
|
-
return get_model_instance(model_name, temperature=0)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def get_computation_model(config: RunnableConfig):
|
|
285
|
-
"""獲取計算驗證用的模型"""
|
|
286
|
-
model_name = get_config_value(config, "computation_model", "gemini-2.5-flash")
|
|
287
|
-
return get_model_instance(model_name, temperature=0, enable_code_execution=True)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def get_verification_model(config: RunnableConfig):
|
|
291
|
-
"""獲取幻覺驗證用的模型"""
|
|
292
|
-
model_name = get_config_value(config, "verification_model", "gemini-2.5-flash")
|
|
293
|
-
return get_model_instance(model_name, temperature=0)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def get_summary_model(config: RunnableConfig):
|
|
297
|
-
"""獲取匯總回答用的模型"""
|
|
298
|
-
model_name = get_config_value(config, "summary_model", "gemini-2.5-flash")
|
|
299
|
-
return get_model_instance(model_name, temperature=0)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def get_prompt_template(
|
|
303
|
-
config: RunnableConfig, prompt_key: str, default_prompt: str
|
|
304
|
-
) -> str:
|
|
305
|
-
"""獲取可配置的提示詞模板"""
|
|
306
|
-
return get_config_value(config, prompt_key, default_prompt)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def topic_decomposition_node(
|
|
310
|
-
state: GovResearcherState, config: RunnableConfig
|
|
311
|
-
) -> Dict[str, Any]:
|
|
312
|
-
"""Step-001: 題目拆解節點"""
|
|
313
|
-
logging.info("[GovResearcherGraph:topic_decomposition_node] 開始題目拆解")
|
|
314
|
-
|
|
315
|
-
# 獲取用戶最新問題
|
|
316
|
-
user_question = ""
|
|
317
|
-
for msg in reversed(state["messages"]):
|
|
318
|
-
if isinstance(msg, HumanMessage):
|
|
319
|
-
user_question = msg.content
|
|
320
|
-
break
|
|
321
|
-
|
|
322
|
-
if not user_question:
|
|
323
|
-
logging.warning("未找到用戶問題")
|
|
324
|
-
return {"decomposed_topics": []}
|
|
325
|
-
|
|
326
|
-
# 獲取可配置的提示詞模板
|
|
327
|
-
prompt_template = get_prompt_template(
|
|
328
|
-
config, "topic_decomposition_prompt", TOPIC_DECOMPOSITION_PROMPT
|
|
329
|
-
)
|
|
330
|
-
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
331
|
-
|
|
332
|
-
# 準備提示詞
|
|
333
|
-
prompt = prompt_template.format(
|
|
334
|
-
general_guide=general_guide,
|
|
335
|
-
user_question=user_question,
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
# 調用模型
|
|
339
|
-
model = get_decomposition_model(config)
|
|
340
|
-
from trustcall import create_extractor
|
|
341
|
-
|
|
342
|
-
extractor = create_extractor(
|
|
343
|
-
model, tools=[SubTopicList], tool_choice="SubTopicList"
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
response = extractor.invoke([HumanMessage(content=prompt)])
|
|
347
|
-
|
|
348
|
-
# 解析結果 - 統一處理 trustcall 的回應格式
|
|
349
|
-
subtopics = []
|
|
350
|
-
|
|
351
|
-
try:
|
|
352
|
-
# 直接是 SubTopicList 實例
|
|
353
|
-
if isinstance(response, SubTopicList):
|
|
354
|
-
subtopics = response.subtopics
|
|
355
|
-
# 有 subtopics 屬性
|
|
356
|
-
elif hasattr(response, "subtopics"):
|
|
357
|
-
subtopics = response.subtopics
|
|
358
|
-
# trustcall 字典格式(主要情況)
|
|
359
|
-
elif isinstance(response, dict):
|
|
360
|
-
if "responses" in response and response["responses"]:
|
|
361
|
-
first_response = response["responses"][0]
|
|
362
|
-
if hasattr(first_response, "subtopics"):
|
|
363
|
-
subtopics = first_response.subtopics
|
|
364
|
-
elif "subtopics" in response:
|
|
365
|
-
subtopics_data = response["subtopics"]
|
|
366
|
-
subtopics = [
|
|
367
|
-
SubTopic(**item) if isinstance(item, dict) else item
|
|
368
|
-
for item in subtopics_data
|
|
369
|
-
]
|
|
370
|
-
|
|
371
|
-
logging.info(f"成功解析 {len(subtopics)} 個子題目")
|
|
372
|
-
|
|
373
|
-
except Exception as e:
|
|
374
|
-
logging.error(f"解析 trustcall 回應失敗: {e}")
|
|
375
|
-
subtopics = []
|
|
376
|
-
|
|
377
|
-
# 備選方案:使用原始問題
|
|
378
|
-
if not subtopics:
|
|
379
|
-
logging.warning("未能解析出子題目,使用原始問題")
|
|
380
|
-
subtopics = [SubTopic(topic=user_question, description="原始問題")]
|
|
381
|
-
|
|
382
|
-
logging.info(f"題目拆解完成,共 {len(subtopics)} 個子題目")
|
|
383
|
-
|
|
384
|
-
# 額外的調試資訊
|
|
385
|
-
for i, subtopic in enumerate(subtopics):
|
|
386
|
-
logging.info(f"子題目 {i+1}: {subtopic.topic[:50]}...") # 只顯示前50字元
|
|
387
|
-
|
|
388
|
-
return {"original_question": user_question, "decomposed_topics": subtopics}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
def search_preparation_node(
|
|
392
|
-
state: GovResearcherState, config: RunnableConfig
|
|
393
|
-
) -> Dict[str, Any]:
|
|
394
|
-
"""搜尋準備節點:準備並分發搜尋任務"""
|
|
395
|
-
logging.info("[GovResearcherGraph:search_preparation_node] 準備搜尋任務")
|
|
396
|
-
|
|
397
|
-
subtopics = state.get("decomposed_topics", [])
|
|
398
|
-
if not subtopics:
|
|
399
|
-
logging.warning("無子題目可搜尋")
|
|
400
|
-
return {"search_tasks": []}
|
|
401
|
-
|
|
402
|
-
# 限制並行搜尋數量
|
|
403
|
-
max_parallel_searches = get_config_value(config, "max_parallel_searches", 5)
|
|
404
|
-
limited_subtopics = subtopics[:max_parallel_searches]
|
|
405
|
-
|
|
406
|
-
logging.info(f"準備平行搜尋 {len(limited_subtopics)} 個子題目")
|
|
407
|
-
|
|
408
|
-
return {"search_tasks": limited_subtopics, "search_completed": False}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
async def search_subtopic_node(
|
|
412
|
-
state: GovResearcherState, config: RunnableConfig
|
|
413
|
-
) -> Dict[str, Any]:
|
|
414
|
-
"""搜尋所有子題目(支援多搜尋引擎,不使用LLM)"""
|
|
415
|
-
logging.info("[GovResearcherGraph:search_subtopic_node] 開始搜尋所有子題目")
|
|
416
|
-
|
|
417
|
-
search_tasks = state.get("search_tasks", [])
|
|
418
|
-
if not search_tasks:
|
|
419
|
-
logging.warning("無搜尋任務")
|
|
420
|
-
return {"search_results": []}
|
|
421
|
-
|
|
422
|
-
# 獲取搜尋引擎配置
|
|
423
|
-
search_vendor = get_config_value(config, "search_vendor", "tavily")
|
|
424
|
-
search_model = get_config_value(config, "search_model", "sonar")
|
|
425
|
-
domain_filter = get_config_value(config, "domain_filter", [])
|
|
426
|
-
|
|
427
|
-
logging.info(f"使用搜尋服務商: {search_vendor}, 模型: {search_model}")
|
|
428
|
-
|
|
429
|
-
# 使用 asyncio.gather 進行真正的平行搜尋(PRD要求:不使用LLM,僅搜尋API)
|
|
430
|
-
async def search_single_topic(subtopic: SubTopic) -> SearchResult:
|
|
431
|
-
try:
|
|
432
|
-
content = ""
|
|
433
|
-
sources = []
|
|
434
|
-
search_query = subtopic.topic
|
|
435
|
-
|
|
436
|
-
# 根據搜尋服務商選擇不同的搜尋服務
|
|
437
|
-
if search_vendor == "tavily":
|
|
438
|
-
async for event in respond_with_tavily_search(
|
|
439
|
-
search_query,
|
|
440
|
-
"", # 無前綴
|
|
441
|
-
[{"role": "user", "content": search_query}], # 最直接的查詢
|
|
442
|
-
domain_filter,
|
|
443
|
-
False, # 不stream
|
|
444
|
-
search_model,
|
|
445
|
-
):
|
|
446
|
-
content += event.chunk
|
|
447
|
-
if event.raw_json and "sources" in event.raw_json:
|
|
448
|
-
sources = event.raw_json["sources"]
|
|
449
|
-
else:
|
|
450
|
-
sources = ["Tavily Search"]
|
|
451
|
-
|
|
452
|
-
else: # 預設使用 perplexity
|
|
453
|
-
async for event in respond_with_perplexity_search(
|
|
454
|
-
search_query,
|
|
455
|
-
"", # 無前綴
|
|
456
|
-
[{"role": "user", "content": search_query}], # 最直接的查詢
|
|
457
|
-
domain_filter,
|
|
458
|
-
False, # 不stream
|
|
459
|
-
search_model,
|
|
460
|
-
):
|
|
461
|
-
content += event.chunk
|
|
462
|
-
sources = ["Perplexity Search"]
|
|
463
|
-
|
|
464
|
-
return SearchResult(
|
|
465
|
-
subtopic=subtopic.topic, content=content, sources=sources
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
except Exception as e:
|
|
469
|
-
logging.error(f"搜尋 '{subtopic.topic}' 失敗: {e}")
|
|
470
|
-
return SearchResult(
|
|
471
|
-
subtopic=subtopic.topic, content=f"搜尋失敗: {str(e)}", sources=[]
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
# 平行執行所有搜尋
|
|
475
|
-
search_results = await asyncio.gather(
|
|
476
|
-
*[search_single_topic(subtopic) for subtopic in search_tasks]
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
logging.info(f"搜尋完成,共 {len(search_results)} 個結果")
|
|
480
|
-
|
|
481
|
-
return {"search_results": search_results, "search_completed": True}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
def reasoning_analysis_node(
|
|
485
|
-
state: GovResearcherState, config: RunnableConfig
|
|
486
|
-
) -> Dict[str, Any]:
|
|
487
|
-
"""Step-003: 推理分析節點"""
|
|
488
|
-
logging.info("[GovResearcherGraph:reasoning_analysis_node] 開始推理分析")
|
|
489
|
-
|
|
490
|
-
search_results = state.get("search_results", [])
|
|
491
|
-
subtopics = state.get("decomposed_topics", [])
|
|
492
|
-
|
|
493
|
-
if not search_results or not subtopics:
|
|
494
|
-
logging.warning("缺少搜尋結果或子題目")
|
|
495
|
-
return {"reasoning_results": []}
|
|
496
|
-
|
|
497
|
-
# 準備搜尋結果文本
|
|
498
|
-
search_text = "\n\n".join(
|
|
499
|
-
[
|
|
500
|
-
f"子題目: {result.subtopic}\n內容: {result.content}\n來源: {', '.join(result.sources)}"
|
|
501
|
-
for result in search_results
|
|
502
|
-
]
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
subtopics_text = "\n".join(
|
|
506
|
-
[
|
|
507
|
-
f"{i+1}. {topic.topic} - {topic.description}"
|
|
508
|
-
for i, topic in enumerate(subtopics)
|
|
509
|
-
]
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
# 獲取可配置的提示詞模板
|
|
513
|
-
prompt_template = get_prompt_template(
|
|
514
|
-
config, "reasoning_analysis_prompt", REASONING_ANALYSIS_PROMPT
|
|
515
|
-
)
|
|
516
|
-
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
517
|
-
|
|
518
|
-
# 準備提示詞
|
|
519
|
-
prompt = prompt_template.format(
|
|
520
|
-
general_guide=general_guide,
|
|
521
|
-
search_results=search_text,
|
|
522
|
-
subtopics=subtopics_text,
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
# 調用模型
|
|
526
|
-
model = get_reasoning_model(config)
|
|
527
|
-
response = model.invoke([HumanMessage(content=prompt)])
|
|
528
|
-
|
|
529
|
-
# 簡化版結果解析
|
|
530
|
-
reasoning_results = []
|
|
531
|
-
for i, subtopic in enumerate(subtopics):
|
|
532
|
-
reasoning_results.append(
|
|
533
|
-
ReasoningResult(
|
|
534
|
-
subtopic=subtopic.topic,
|
|
535
|
-
analysis=response.content, # 實際應該分段解析
|
|
536
|
-
conclusion=f"針對 '{subtopic.topic}' 的分析結論",
|
|
537
|
-
confidence=0.8,
|
|
538
|
-
)
|
|
539
|
-
)
|
|
540
|
-
|
|
541
|
-
# 檢查是否需要計算驗證
|
|
542
|
-
needs_computation = (
|
|
543
|
-
"計算" in response.content
|
|
544
|
-
or "金額" in response.content
|
|
545
|
-
or "數量" in response.content
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
logging.info(f"推理分析完成,需要計算驗證: {needs_computation}")
|
|
549
|
-
|
|
550
|
-
return {
|
|
551
|
-
"reasoning_results": reasoning_results,
|
|
552
|
-
"needs_computation": needs_computation,
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
def computation_verification_node(
|
|
557
|
-
state: GovResearcherState, config: RunnableConfig
|
|
558
|
-
) -> Dict[str, Any]:
|
|
559
|
-
"""Step-004: 計算驗證節點(條件性)"""
|
|
560
|
-
logging.info("[GovResearcherGraph:computation_verification_node] 開始計算驗證")
|
|
561
|
-
|
|
562
|
-
if not state.get("needs_computation", False):
|
|
563
|
-
logging.info("無需計算驗證,跳過")
|
|
564
|
-
return {"computation_results": None}
|
|
565
|
-
|
|
566
|
-
reasoning_results = state.get("reasoning_results", [])
|
|
567
|
-
|
|
568
|
-
# 準備計算驗證提示詞
|
|
569
|
-
reasoning_text = "\n\n".join(
|
|
570
|
-
[
|
|
571
|
-
f"子題目: {result.subtopic}\n分析: {result.analysis}\n結論: {result.conclusion}"
|
|
572
|
-
for result in reasoning_results
|
|
573
|
-
]
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
# 獲取可配置的通用指導原則
|
|
577
|
-
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
578
|
-
|
|
579
|
-
prompt = f"""
|
|
580
|
-
{general_guide}
|
|
581
|
-
|
|
582
|
-
你是專業的計算驗證專家,請針對以下推理結果中的計算部分進行獨立驗算:
|
|
583
|
-
|
|
584
|
-
{reasoning_text}
|
|
585
|
-
|
|
586
|
-
請使用程式碼執行功能驗證任何涉及數字計算的部分,並提供驗證結果。
|
|
587
|
-
"""
|
|
588
|
-
|
|
589
|
-
# 使用支援代碼執行的模型
|
|
590
|
-
model = get_computation_model(config)
|
|
591
|
-
response = model.invoke([HumanMessage(content=prompt)])
|
|
592
|
-
|
|
593
|
-
logging.info("計算驗證完成")
|
|
594
|
-
|
|
595
|
-
return {"computation_results": response.content}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
async def hallucination_verification_node(
|
|
599
|
-
state: GovResearcherState, config: RunnableConfig
|
|
600
|
-
) -> Dict[str, Any]:
|
|
601
|
-
"""Step-005: 幻覺驗證節點(支援搜尋引擎選擇)"""
|
|
602
|
-
logging.info("[GovResearcherGraph:hallucination_verification_node] 開始幻覺驗證")
|
|
603
|
-
|
|
604
|
-
# 收集前面所有結果
|
|
605
|
-
previous_results = {
|
|
606
|
-
"原始問題": state.get("original_question", ""),
|
|
607
|
-
"子題目": [topic.topic for topic in state.get("decomposed_topics", [])],
|
|
608
|
-
"搜尋結果": [result.content for result in state.get("search_results", [])],
|
|
609
|
-
"推理結果": [result.analysis for result in state.get("reasoning_results", [])],
|
|
610
|
-
"計算結果": state.get("computation_results", "無"),
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
results_text = "\n\n".join(
|
|
614
|
-
[f"{key}: {value}" for key, value in previous_results.items()]
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
# 獲取可配置的提示詞模板
|
|
618
|
-
prompt_template = get_prompt_template(
|
|
619
|
-
config, "hallucination_verification_prompt", HALLUCINATION_VERIFICATION_PROMPT
|
|
620
|
-
)
|
|
621
|
-
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
622
|
-
|
|
623
|
-
# 準備驗證提示詞
|
|
624
|
-
prompt = prompt_template.format(
|
|
625
|
-
general_guide=general_guide,
|
|
626
|
-
previous_results=results_text,
|
|
627
|
-
)
|
|
628
|
-
|
|
629
|
-
# 調用模型
|
|
630
|
-
model = get_verification_model(config)
|
|
631
|
-
response = model.invoke([HumanMessage(content=prompt)])
|
|
632
|
-
|
|
633
|
-
# 如果發現問題,進行額外搜尋驗證(PRD要求:透過延伸搜尋證明或反駁前面的結論)
|
|
634
|
-
if "問題" in response.content or "錯誤" in response.content:
|
|
635
|
-
logging.info("發現潛在問題,進行額外搜尋驗證")
|
|
636
|
-
|
|
637
|
-
# 獲取搜尋引擎配置(PRD要求:搜尋引擎選擇)
|
|
638
|
-
search_vendor = get_config_value(
|
|
639
|
-
config,
|
|
640
|
-
"verification_search_vendor",
|
|
641
|
-
get_config_value(config, "search_vendor", "perplexity"),
|
|
642
|
-
)
|
|
643
|
-
search_model = get_config_value(config, "search_model", "sonar")
|
|
644
|
-
domain_filter = get_config_value(config, "domain_filter", [])
|
|
645
|
-
|
|
646
|
-
# 提取需要驗證的關鍵問題
|
|
647
|
-
verification_query = (
|
|
648
|
-
f"驗證以下政府資訊的準確性:{state.get('original_question', '')}"
|
|
649
|
-
)
|
|
650
|
-
|
|
651
|
-
try:
|
|
652
|
-
verification_content = ""
|
|
653
|
-
|
|
654
|
-
# 根據搜尋服務商進行驗證搜尋
|
|
655
|
-
if search_vendor == "tavily":
|
|
656
|
-
async for event in respond_with_tavily_search(
|
|
657
|
-
verification_query,
|
|
658
|
-
"",
|
|
659
|
-
[{"role": "user", "content": verification_query}],
|
|
660
|
-
domain_filter,
|
|
661
|
-
False,
|
|
662
|
-
search_model,
|
|
663
|
-
):
|
|
664
|
-
verification_content += event.chunk
|
|
665
|
-
else: # perplexity
|
|
666
|
-
async for event in respond_with_perplexity_search(
|
|
667
|
-
verification_query,
|
|
668
|
-
"",
|
|
669
|
-
[{"role": "user", "content": verification_query}],
|
|
670
|
-
domain_filter,
|
|
671
|
-
False,
|
|
672
|
-
search_model,
|
|
673
|
-
):
|
|
674
|
-
verification_content += event.chunk
|
|
675
|
-
|
|
676
|
-
logging.info(f"完成額外搜尋驗證,使用服務商: {search_vendor}")
|
|
677
|
-
|
|
678
|
-
except Exception as e:
|
|
679
|
-
logging.error(f"額外搜尋驗證失敗: {e}")
|
|
680
|
-
verification_content = f"驗證搜尋失敗: {str(e)}"
|
|
681
|
-
else:
|
|
682
|
-
verification_content = "未發現明顯問題,無需額外搜尋"
|
|
683
|
-
|
|
684
|
-
# 簡化版驗證結果
|
|
685
|
-
verification_result = VerificationResult(
|
|
686
|
-
issues_found=["待實作:問題識別"],
|
|
687
|
-
corrections=["待實作:修正建議"],
|
|
688
|
-
confidence_adjustments={},
|
|
689
|
-
)
|
|
690
|
-
|
|
691
|
-
logging.info("幻覺驗證完成")
|
|
692
|
-
|
|
693
|
-
return {
|
|
694
|
-
"hallucination_check": verification_result,
|
|
695
|
-
"verification_search_results": verification_content,
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
def summary_response_node(
|
|
700
|
-
state: GovResearcherState, config: RunnableConfig
|
|
701
|
-
) -> Dict[str, Any]:
|
|
702
|
-
"""Step-006: 匯總回答節點"""
|
|
703
|
-
logging.info("[GovResearcherGraph:summary_response_node] 開始匯總回答")
|
|
704
|
-
|
|
705
|
-
# 收集所有處理結果
|
|
706
|
-
summary_data = {
|
|
707
|
-
"original_question": state.get("original_question", ""),
|
|
708
|
-
"subtopics": [topic.topic for topic in state.get("decomposed_topics", [])],
|
|
709
|
-
"search_results": "\n".join(
|
|
710
|
-
[result.content for result in state.get("search_results", [])]
|
|
711
|
-
),
|
|
712
|
-
"reasoning_results": "\n".join(
|
|
713
|
-
[result.analysis for result in state.get("reasoning_results", [])]
|
|
714
|
-
),
|
|
715
|
-
"computation_results": state.get("computation_results", "無計算需求"),
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
# 獲取可配置的提示詞模板
|
|
719
|
-
prompt_template = get_prompt_template(
|
|
720
|
-
config, "summary_response_prompt", SUMMARY_RESPONSE_PROMPT
|
|
721
|
-
)
|
|
722
|
-
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
723
|
-
|
|
724
|
-
# 準備匯總提示詞
|
|
725
|
-
prompt = prompt_template.format(general_guide=general_guide, **summary_data)
|
|
726
|
-
|
|
727
|
-
# 調用模型
|
|
728
|
-
model = get_summary_model(config)
|
|
729
|
-
response = model.invoke([HumanMessage(content=prompt)])
|
|
730
|
-
|
|
731
|
-
final_answer = response.content
|
|
732
|
-
|
|
733
|
-
logging.info("匯總回答完成")
|
|
734
|
-
|
|
735
|
-
return {"final_answer": final_answer, "messages": [AIMessage(content=final_answer)]}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
def should_compute(state: GovResearcherState) -> str:
|
|
739
|
-
"""條件分支:決定是否需要計算驗證"""
|
|
740
|
-
if state.get("needs_computation", False):
|
|
741
|
-
return COMPUTATION_VERIFICATION_NODE
|
|
742
|
-
else:
|
|
743
|
-
return SUMMARY_RESPONSE_NODE
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
# 預設配置(根據PRD規格修正)
|
|
747
|
-
DEFAULT_GOV_RESEARCHER_CONFIG = {
|
|
748
|
-
"decomposition_model": "gemini-2.5-pro", # PRD 預設
|
|
749
|
-
"reasoning_model": "gemini-2.5-flash",
|
|
750
|
-
"computation_model": "gemini-2.5-flash",
|
|
751
|
-
"verification_model": "gemini-2.5-flash",
|
|
752
|
-
"summary_model": "gemini-2.5-flash", # PRD 預設
|
|
753
|
-
"search_vendor": "perplexity", # PRD 預設
|
|
754
|
-
"max_parallel_searches": 5,
|
|
755
|
-
"domain_filter": [],
|
|
756
|
-
"search_model": "sonar", # PRD 預設,非 sonar-reasoning-pro
|
|
757
|
-
"general_guide": DEFAULT_GENERAL_GUIDE,
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
def get_content_for_gov_researcher(state: Dict[str, Any]) -> str:
|
|
762
|
-
"""從狀態中取得內容"""
|
|
763
|
-
return state.get("final_answer", "")
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
class GovResearcherGraph:
|
|
767
|
-
"""政府研究員 LangGraph Agent"""
|
|
768
|
-
|
|
769
|
-
def __init__(self, memory: BaseCheckpointSaver = None):
|
|
770
|
-
self.memory = memory if memory is not None else MemorySaver()
|
|
771
|
-
self._initialize_graph()
|
|
772
|
-
|
|
773
|
-
def _initialize_graph(self):
|
|
774
|
-
"""初始化 LangGraph 工作流"""
|
|
775
|
-
workflow = StateGraph(
|
|
776
|
-
GovResearcherState, context_schema=GovResearcherConfigSchema
|
|
777
|
-
)
|
|
778
|
-
|
|
779
|
-
# 添加節點
|
|
780
|
-
workflow.add_node(TOPIC_DECOMPOSITION_NODE, topic_decomposition_node)
|
|
781
|
-
workflow.add_node("search_preparation", search_preparation_node)
|
|
782
|
-
workflow.add_node("search_subtopic", search_subtopic_node)
|
|
783
|
-
workflow.add_node(REASONING_ANALYSIS_NODE, reasoning_analysis_node)
|
|
784
|
-
workflow.add_node(COMPUTATION_VERIFICATION_NODE, computation_verification_node)
|
|
785
|
-
workflow.add_node(
|
|
786
|
-
HALLUCINATION_VERIFICATION_NODE, hallucination_verification_node
|
|
787
|
-
)
|
|
788
|
-
workflow.add_node(SUMMARY_RESPONSE_NODE, summary_response_node)
|
|
789
|
-
|
|
790
|
-
# 定義邊(工作流程)
|
|
791
|
-
workflow.add_edge(START, TOPIC_DECOMPOSITION_NODE)
|
|
792
|
-
workflow.add_edge(TOPIC_DECOMPOSITION_NODE, "search_preparation")
|
|
793
|
-
workflow.add_edge("search_preparation", "search_subtopic")
|
|
794
|
-
workflow.add_edge("search_subtopic", REASONING_ANALYSIS_NODE)
|
|
795
|
-
|
|
796
|
-
# 條件分支:是否需要計算驗證
|
|
797
|
-
workflow.add_conditional_edges(
|
|
798
|
-
REASONING_ANALYSIS_NODE,
|
|
799
|
-
should_compute,
|
|
800
|
-
[COMPUTATION_VERIFICATION_NODE, SUMMARY_RESPONSE_NODE],
|
|
801
|
-
)
|
|
802
|
-
|
|
803
|
-
workflow.add_edge(COMPUTATION_VERIFICATION_NODE, SUMMARY_RESPONSE_NODE)
|
|
804
|
-
workflow.add_edge(SUMMARY_RESPONSE_NODE, END)
|
|
805
|
-
|
|
806
|
-
# 編譯圖
|
|
807
|
-
self._graph = workflow.compile(checkpointer=self.memory)
|
|
808
|
-
self._graph_no_memory = workflow.compile()
|
|
809
|
-
|
|
810
|
-
@property
|
|
811
|
-
def graph(self):
|
|
812
|
-
"""帶記憶的圖"""
|
|
813
|
-
return self._graph
|
|
814
|
-
|
|
815
|
-
@property
|
|
816
|
-
def graph_no_memory(self):
|
|
817
|
-
"""不帶記憶的圖"""
|
|
818
|
-
return self._graph_no_memory
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
# 導出實例
|
|
822
|
-
gov_researcher_graph = GovResearcherGraph().graph_no_memory
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import List, Dict, Any, Optional, Annotated
|
|
7
|
+
from typing_extensions import TypedDict
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from langchain_core.messages import SystemMessage, AIMessage, HumanMessage, ToolMessage
|
|
11
|
+
from langchain_core.runnables.config import RunnableConfig
|
|
12
|
+
|
|
13
|
+
from langgraph.graph import StateGraph, START, END
|
|
14
|
+
from langgraph.graph import MessagesState
|
|
15
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
16
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
17
|
+
|
|
18
|
+
from botrun_flow_lang.langgraph_agents.agents.util.perplexity_search import (
|
|
19
|
+
respond_with_perplexity_search,
|
|
20
|
+
)
|
|
21
|
+
from botrun_flow_lang.langgraph_agents.agents.util.tavily_search import (
|
|
22
|
+
respond_with_tavily_search,
|
|
23
|
+
)
|
|
24
|
+
from botrun_flow_lang.langgraph_agents.agents.util.model_utils import (
|
|
25
|
+
get_model_instance,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from dotenv import load_dotenv
|
|
29
|
+
|
|
30
|
+
load_dotenv()
|
|
31
|
+
|
|
32
|
+
logging.basicConfig(
|
|
33
|
+
level=logging.INFO,
|
|
34
|
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
35
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# 節點名稱常數
|
|
39
|
+
TOPIC_DECOMPOSITION_NODE = "topic_decomposition_node"
|
|
40
|
+
PARALLEL_SEARCH_NODE = "parallel_search_node"
|
|
41
|
+
REASONING_ANALYSIS_NODE = "reasoning_analysis_node"
|
|
42
|
+
COMPUTATION_VERIFICATION_NODE = "computation_verification_node"
|
|
43
|
+
HALLUCINATION_VERIFICATION_NODE = "hallucination_verification_node"
|
|
44
|
+
SUMMARY_RESPONSE_NODE = "summary_response_node"
|
|
45
|
+
|
|
46
|
+
# 預設 General Guide
|
|
47
|
+
DEFAULT_GENERAL_GUIDE = """
|
|
48
|
+
<General Guide>
|
|
49
|
+
妳回應時會採用臺灣繁體中文,並且避免中國大陸用語
|
|
50
|
+
妳絕對不會使用 markdown 語法回應
|
|
51
|
+
但是你絕對不會使用 ** 或者 ### ,各種類型的 markdown 語法都禁止使用
|
|
52
|
+
如果要比較美觀排版的話,妳可以搭配使用 emoji or 純文字 or 斷行 or 空白 來展示你想講的
|
|
53
|
+
每一個 step 的前面增添適當斷行
|
|
54
|
+
每個分段的標題「前面」要增添適當 emoji (這個 emoji 挑選必須跟動態情境吻合)
|
|
55
|
+
</General Guide>
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# 題目拆解提示詞模板
|
|
59
|
+
TOPIC_DECOMPOSITION_PROMPT = """
|
|
60
|
+
{general_guide}
|
|
61
|
+
|
|
62
|
+
你是一位專業的政府研究分析師,負責將使用者的政府相關問題進行智能分析和拆解。
|
|
63
|
+
|
|
64
|
+
你的任務:
|
|
65
|
+
1. 分析用戶提問的複雜度
|
|
66
|
+
2. 如果是單純問題:轉化為更細緻的單一子題目
|
|
67
|
+
3. 如果是複雜問題:拆解為多個子題目以確保回答的準確性
|
|
68
|
+
|
|
69
|
+
重要考量因素:
|
|
70
|
+
- 考量使用者的身份、年齡、性別、居住地等個人條件
|
|
71
|
+
- 考量時間性:政策、法規的生效日期、申請期限、變更時程
|
|
72
|
+
- 考量地域性:中央 vs 地方政府、縣市差異、區域特殊規定
|
|
73
|
+
- 考量適用性:不同身份別、不同條件下的差異化規定
|
|
74
|
+
|
|
75
|
+
請將用戶問題拆解為 1-5 個具體的子題目,每個子題目都應該:
|
|
76
|
+
- 明確且具體,但涵蓋全面性考量
|
|
77
|
+
- 可以透過搜尋找到答案
|
|
78
|
+
- 與政府政策、法規、程序相關
|
|
79
|
+
- 盡量包含多個思考面向:時效性、地域性、身份差異、適用條件等
|
|
80
|
+
- 每個子題目都要設計得廣泛且深入,以獲取豐富的搜尋資訊
|
|
81
|
+
- 使用繁體中文表達
|
|
82
|
+
|
|
83
|
+
**重要指導原則:**
|
|
84
|
+
雖然子題目數量限制在 1-5 個,但每個子題目都要做全面性的考量,
|
|
85
|
+
盡量把思考面設定廣泛,這樣搜尋時才能獲得更多角度的資訊,
|
|
86
|
+
最終總結時就會有比較豐富的資料可以使用。
|
|
87
|
+
|
|
88
|
+
用戶問題:{user_question}
|
|
89
|
+
|
|
90
|
+
請輸出結構化的子題目列表。
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
# 推理分析提示詞模板
|
|
94
|
+
REASONING_ANALYSIS_PROMPT = """
|
|
95
|
+
{general_guide}
|
|
96
|
+
|
|
97
|
+
你是一位專業的政府研究分析師,負責基於搜尋結果和內建知識進行縝密推理。
|
|
98
|
+
|
|
99
|
+
你的任務:
|
|
100
|
+
1. 基於搜尋結果和內建知識進行推理
|
|
101
|
+
2. 分析常見錯誤後進行縝密推理
|
|
102
|
+
3. 逐一回答所有子題目
|
|
103
|
+
4. 保持客觀與準確
|
|
104
|
+
|
|
105
|
+
搜尋結果:
|
|
106
|
+
{search_results}
|
|
107
|
+
|
|
108
|
+
子題目:
|
|
109
|
+
{subtopics}
|
|
110
|
+
|
|
111
|
+
請針對每個子題目提供詳細的推理分析,確保:
|
|
112
|
+
- 基於事實與證據
|
|
113
|
+
- 引用具體的搜尋來源
|
|
114
|
+
- 避免推測與臆斷
|
|
115
|
+
- 提供清晰的結論
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
# 幻覺驗證提示詞模板
|
|
119
|
+
HALLUCINATION_VERIFICATION_PROMPT = """
|
|
120
|
+
{general_guide}
|
|
121
|
+
|
|
122
|
+
你是一位獨立的審查專家,負責以懷疑的角度檢視前述所有分析結果。
|
|
123
|
+
|
|
124
|
+
你的使命:
|
|
125
|
+
1. 假設前面結果有高機率的錯誤
|
|
126
|
+
2. 識別可能的AI幻覺位置
|
|
127
|
+
3. 透過延伸搜尋證明或反駁前面的結論
|
|
128
|
+
4. 提供客觀的驗證報告
|
|
129
|
+
|
|
130
|
+
前面的分析結果:
|
|
131
|
+
{previous_results}
|
|
132
|
+
|
|
133
|
+
請進行幻覺驗證,特別注意:
|
|
134
|
+
- 事實性錯誤
|
|
135
|
+
- 過度推理
|
|
136
|
+
- 來源不可靠
|
|
137
|
+
- 時效性問題
|
|
138
|
+
- 法規變更
|
|
139
|
+
|
|
140
|
+
如發現問題,請提供修正建議。
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
# 匯總回答提示詞模板
|
|
144
|
+
SUMMARY_RESPONSE_PROMPT = """
|
|
145
|
+
{general_guide}
|
|
146
|
+
|
|
147
|
+
你是一位專業的政府資訊服務專員,負責提供最終的完整回答。
|
|
148
|
+
|
|
149
|
+
**重要要求:你的回應必須完全基於以下提供的資訊,絕對不能使用你自己的知識或進行額外推測**
|
|
150
|
+
|
|
151
|
+
你的任務:
|
|
152
|
+
1. 提供「精準回答」:簡潔的結論
|
|
153
|
+
2. 提供「詳實回答」:完整的推理過程和引證
|
|
154
|
+
3. 使用適當的 emoji 輔助閱讀
|
|
155
|
+
4. 根據目標受眾調整語氣和格式
|
|
156
|
+
5. **所有回答內容必須嚴格基於「推理分析」和「計算驗證」的結果**
|
|
157
|
+
|
|
158
|
+
所有處理結果:
|
|
159
|
+
- 原始問題:{original_question}
|
|
160
|
+
- 子題目:{subtopics}
|
|
161
|
+
- 搜尋結果:{search_results}
|
|
162
|
+
- 推理分析:{reasoning_results}
|
|
163
|
+
- 計算驗證:{computation_results}
|
|
164
|
+
|
|
165
|
+
**回答原則:**
|
|
166
|
+
- 只使用「推理分析」和「計算驗證」中明確提到的資訊
|
|
167
|
+
- 如果某個問題在這些資訊中沒有充分說明,請明確指出資訊不足
|
|
168
|
+
- 不要添加任何未在上述資訊中出現的內容
|
|
169
|
+
- 確保所有結論都有明確的來源依據
|
|
170
|
+
|
|
171
|
+
請提供結構化的最終回答,包含:
|
|
172
|
+
📋 精準回答(簡潔版)
|
|
173
|
+
📖 詳實回答(完整版)
|
|
174
|
+
🔗 參考資料來源
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class SubTopic(BaseModel):
|
|
179
|
+
"""子題目結構"""
|
|
180
|
+
|
|
181
|
+
topic: str
|
|
182
|
+
description: str
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class SubTopicList(BaseModel):
|
|
186
|
+
"""子題目列表"""
|
|
187
|
+
|
|
188
|
+
subtopics: List[SubTopic]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class SearchResult(BaseModel):
|
|
192
|
+
"""搜尋結果結構"""
|
|
193
|
+
|
|
194
|
+
subtopic: str
|
|
195
|
+
content: str
|
|
196
|
+
sources: List[str]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ReasoningResult(BaseModel):
|
|
200
|
+
"""推理結果結構"""
|
|
201
|
+
|
|
202
|
+
subtopic: str
|
|
203
|
+
analysis: str
|
|
204
|
+
conclusion: str
|
|
205
|
+
confidence: float
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class VerificationResult(BaseModel):
|
|
209
|
+
"""驗證結果結構"""
|
|
210
|
+
|
|
211
|
+
issues_found: List[str]
|
|
212
|
+
corrections: List[str]
|
|
213
|
+
confidence_adjustments: Dict[str, float]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# LangGraph Assistant 配置 Schema
|
|
217
|
+
class GovResearcherConfigSchema(BaseModel):
|
|
218
|
+
"""政府研究員助手配置 Schema - 可在 LangGraph UI 中設定"""
|
|
219
|
+
|
|
220
|
+
# 模型選擇
|
|
221
|
+
decomposition_model: str = Field(default="gemini-2.5-pro") # 題目拆解模型
|
|
222
|
+
reasoning_model: str # 推理分析模型
|
|
223
|
+
computation_model: str # 計算驗證模型
|
|
224
|
+
verification_model: str # 幻覺驗證模型
|
|
225
|
+
summary_model: str # 匯總回答模型
|
|
226
|
+
|
|
227
|
+
# 搜尋引擎設定
|
|
228
|
+
search_vendor: str # "perplexity" | "tavily"
|
|
229
|
+
search_model: str # 搜尋模型名稱
|
|
230
|
+
max_parallel_searches: int # 最大並行搜尋數量
|
|
231
|
+
|
|
232
|
+
# 提示詞模板(可動態設定)
|
|
233
|
+
general_guide: Optional[str] # 通用指導原則
|
|
234
|
+
topic_decomposition_prompt: Optional[str] # 題目拆解提示詞
|
|
235
|
+
reasoning_analysis_prompt: Optional[str] # 推理分析提示詞
|
|
236
|
+
hallucination_verification_prompt: Optional[str] # 幻覺驗證提示詞
|
|
237
|
+
summary_response_prompt: Optional[str] # 匯總回答提示詞
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class GovResearcherState(MessagesState):
|
|
241
|
+
"""政府研究員 LangGraph 狀態"""
|
|
242
|
+
|
|
243
|
+
original_question: str = ""
|
|
244
|
+
decomposed_topics: List[SubTopic] = []
|
|
245
|
+
search_tasks: List[SubTopic] = []
|
|
246
|
+
search_results: Annotated[List[SearchResult], lambda x, y: x + y] = (
|
|
247
|
+
[]
|
|
248
|
+
) # 支援 fan-in 合併
|
|
249
|
+
reasoning_results: List[ReasoningResult] = []
|
|
250
|
+
computation_results: Optional[str] = None
|
|
251
|
+
needs_computation: bool = False
|
|
252
|
+
hallucination_check: Optional[VerificationResult] = None
|
|
253
|
+
final_answer: str = ""
|
|
254
|
+
general_guide: str = DEFAULT_GENERAL_GUIDE
|
|
255
|
+
search_completed: bool = False
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def format_dates(dt):
|
|
259
|
+
"""將日期時間格式化為西元和民國格式"""
|
|
260
|
+
western_date = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
261
|
+
taiwan_year = dt.year - 1911
|
|
262
|
+
taiwan_date = f"{taiwan_year}-{dt.strftime('%m-%d %H:%M:%S')}"
|
|
263
|
+
return {"western_date": western_date, "taiwan_date": taiwan_date}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def get_config_value(config: RunnableConfig, key: str, default_value: Any) -> Any:
|
|
267
|
+
"""統一獲取配置值的輔助函數"""
|
|
268
|
+
# 如果 config.get("configurable", {}).get(key, default_value) 是 None,則返回 default_value
|
|
269
|
+
return config.get("configurable", {}).get(key, default_value) or default_value
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_decomposition_model(config: RunnableConfig):
|
|
273
|
+
"""獲取題目拆解用的模型"""
|
|
274
|
+
model_name = get_config_value(config, "decomposition_model", "gemini-2.5-pro")
|
|
275
|
+
return get_model_instance(model_name, temperature=0)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_reasoning_model(config: RunnableConfig):
|
|
279
|
+
"""獲取推理分析用的模型"""
|
|
280
|
+
model_name = get_config_value(config, "reasoning_model", "gemini-2.5-flash")
|
|
281
|
+
return get_model_instance(model_name, temperature=0)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_computation_model(config: RunnableConfig):
|
|
285
|
+
"""獲取計算驗證用的模型"""
|
|
286
|
+
model_name = get_config_value(config, "computation_model", "gemini-2.5-flash")
|
|
287
|
+
return get_model_instance(model_name, temperature=0, enable_code_execution=True)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def get_verification_model(config: RunnableConfig):
|
|
291
|
+
"""獲取幻覺驗證用的模型"""
|
|
292
|
+
model_name = get_config_value(config, "verification_model", "gemini-2.5-flash")
|
|
293
|
+
return get_model_instance(model_name, temperature=0)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_summary_model(config: RunnableConfig):
|
|
297
|
+
"""獲取匯總回答用的模型"""
|
|
298
|
+
model_name = get_config_value(config, "summary_model", "gemini-2.5-flash")
|
|
299
|
+
return get_model_instance(model_name, temperature=0)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def get_prompt_template(
|
|
303
|
+
config: RunnableConfig, prompt_key: str, default_prompt: str
|
|
304
|
+
) -> str:
|
|
305
|
+
"""獲取可配置的提示詞模板"""
|
|
306
|
+
return get_config_value(config, prompt_key, default_prompt)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def topic_decomposition_node(
|
|
310
|
+
state: GovResearcherState, config: RunnableConfig
|
|
311
|
+
) -> Dict[str, Any]:
|
|
312
|
+
"""Step-001: 題目拆解節點"""
|
|
313
|
+
logging.info("[GovResearcherGraph:topic_decomposition_node] 開始題目拆解")
|
|
314
|
+
|
|
315
|
+
# 獲取用戶最新問題
|
|
316
|
+
user_question = ""
|
|
317
|
+
for msg in reversed(state["messages"]):
|
|
318
|
+
if isinstance(msg, HumanMessage):
|
|
319
|
+
user_question = msg.content
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
if not user_question:
|
|
323
|
+
logging.warning("未找到用戶問題")
|
|
324
|
+
return {"decomposed_topics": []}
|
|
325
|
+
|
|
326
|
+
# 獲取可配置的提示詞模板
|
|
327
|
+
prompt_template = get_prompt_template(
|
|
328
|
+
config, "topic_decomposition_prompt", TOPIC_DECOMPOSITION_PROMPT
|
|
329
|
+
)
|
|
330
|
+
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
331
|
+
|
|
332
|
+
# 準備提示詞
|
|
333
|
+
prompt = prompt_template.format(
|
|
334
|
+
general_guide=general_guide,
|
|
335
|
+
user_question=user_question,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# 調用模型
|
|
339
|
+
model = get_decomposition_model(config)
|
|
340
|
+
from trustcall import create_extractor
|
|
341
|
+
|
|
342
|
+
extractor = create_extractor(
|
|
343
|
+
model, tools=[SubTopicList], tool_choice="SubTopicList"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
response = extractor.invoke([HumanMessage(content=prompt)])
|
|
347
|
+
|
|
348
|
+
# 解析結果 - 統一處理 trustcall 的回應格式
|
|
349
|
+
subtopics = []
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
# 直接是 SubTopicList 實例
|
|
353
|
+
if isinstance(response, SubTopicList):
|
|
354
|
+
subtopics = response.subtopics
|
|
355
|
+
# 有 subtopics 屬性
|
|
356
|
+
elif hasattr(response, "subtopics"):
|
|
357
|
+
subtopics = response.subtopics
|
|
358
|
+
# trustcall 字典格式(主要情況)
|
|
359
|
+
elif isinstance(response, dict):
|
|
360
|
+
if "responses" in response and response["responses"]:
|
|
361
|
+
first_response = response["responses"][0]
|
|
362
|
+
if hasattr(first_response, "subtopics"):
|
|
363
|
+
subtopics = first_response.subtopics
|
|
364
|
+
elif "subtopics" in response:
|
|
365
|
+
subtopics_data = response["subtopics"]
|
|
366
|
+
subtopics = [
|
|
367
|
+
SubTopic(**item) if isinstance(item, dict) else item
|
|
368
|
+
for item in subtopics_data
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
logging.info(f"成功解析 {len(subtopics)} 個子題目")
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logging.error(f"解析 trustcall 回應失敗: {e}")
|
|
375
|
+
subtopics = []
|
|
376
|
+
|
|
377
|
+
# 備選方案:使用原始問題
|
|
378
|
+
if not subtopics:
|
|
379
|
+
logging.warning("未能解析出子題目,使用原始問題")
|
|
380
|
+
subtopics = [SubTopic(topic=user_question, description="原始問題")]
|
|
381
|
+
|
|
382
|
+
logging.info(f"題目拆解完成,共 {len(subtopics)} 個子題目")
|
|
383
|
+
|
|
384
|
+
# 額外的調試資訊
|
|
385
|
+
for i, subtopic in enumerate(subtopics):
|
|
386
|
+
logging.info(f"子題目 {i+1}: {subtopic.topic[:50]}...") # 只顯示前50字元
|
|
387
|
+
|
|
388
|
+
return {"original_question": user_question, "decomposed_topics": subtopics}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def search_preparation_node(
|
|
392
|
+
state: GovResearcherState, config: RunnableConfig
|
|
393
|
+
) -> Dict[str, Any]:
|
|
394
|
+
"""搜尋準備節點:準備並分發搜尋任務"""
|
|
395
|
+
logging.info("[GovResearcherGraph:search_preparation_node] 準備搜尋任務")
|
|
396
|
+
|
|
397
|
+
subtopics = state.get("decomposed_topics", [])
|
|
398
|
+
if not subtopics:
|
|
399
|
+
logging.warning("無子題目可搜尋")
|
|
400
|
+
return {"search_tasks": []}
|
|
401
|
+
|
|
402
|
+
# 限制並行搜尋數量
|
|
403
|
+
max_parallel_searches = get_config_value(config, "max_parallel_searches", 5)
|
|
404
|
+
limited_subtopics = subtopics[:max_parallel_searches]
|
|
405
|
+
|
|
406
|
+
logging.info(f"準備平行搜尋 {len(limited_subtopics)} 個子題目")
|
|
407
|
+
|
|
408
|
+
return {"search_tasks": limited_subtopics, "search_completed": False}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
async def search_subtopic_node(
|
|
412
|
+
state: GovResearcherState, config: RunnableConfig
|
|
413
|
+
) -> Dict[str, Any]:
|
|
414
|
+
"""搜尋所有子題目(支援多搜尋引擎,不使用LLM)"""
|
|
415
|
+
logging.info("[GovResearcherGraph:search_subtopic_node] 開始搜尋所有子題目")
|
|
416
|
+
|
|
417
|
+
search_tasks = state.get("search_tasks", [])
|
|
418
|
+
if not search_tasks:
|
|
419
|
+
logging.warning("無搜尋任務")
|
|
420
|
+
return {"search_results": []}
|
|
421
|
+
|
|
422
|
+
# 獲取搜尋引擎配置
|
|
423
|
+
search_vendor = get_config_value(config, "search_vendor", "tavily")
|
|
424
|
+
search_model = get_config_value(config, "search_model", "sonar")
|
|
425
|
+
domain_filter = get_config_value(config, "domain_filter", [])
|
|
426
|
+
|
|
427
|
+
logging.info(f"使用搜尋服務商: {search_vendor}, 模型: {search_model}")
|
|
428
|
+
|
|
429
|
+
# 使用 asyncio.gather 進行真正的平行搜尋(PRD要求:不使用LLM,僅搜尋API)
|
|
430
|
+
async def search_single_topic(subtopic: SubTopic) -> SearchResult:
|
|
431
|
+
try:
|
|
432
|
+
content = ""
|
|
433
|
+
sources = []
|
|
434
|
+
search_query = subtopic.topic
|
|
435
|
+
|
|
436
|
+
# 根據搜尋服務商選擇不同的搜尋服務
|
|
437
|
+
if search_vendor == "tavily":
|
|
438
|
+
async for event in respond_with_tavily_search(
|
|
439
|
+
search_query,
|
|
440
|
+
"", # 無前綴
|
|
441
|
+
[{"role": "user", "content": search_query}], # 最直接的查詢
|
|
442
|
+
domain_filter,
|
|
443
|
+
False, # 不stream
|
|
444
|
+
search_model,
|
|
445
|
+
):
|
|
446
|
+
content += event.chunk
|
|
447
|
+
if event.raw_json and "sources" in event.raw_json:
|
|
448
|
+
sources = event.raw_json["sources"]
|
|
449
|
+
else:
|
|
450
|
+
sources = ["Tavily Search"]
|
|
451
|
+
|
|
452
|
+
else: # 預設使用 perplexity
|
|
453
|
+
async for event in respond_with_perplexity_search(
|
|
454
|
+
search_query,
|
|
455
|
+
"", # 無前綴
|
|
456
|
+
[{"role": "user", "content": search_query}], # 最直接的查詢
|
|
457
|
+
domain_filter,
|
|
458
|
+
False, # 不stream
|
|
459
|
+
search_model,
|
|
460
|
+
):
|
|
461
|
+
content += event.chunk
|
|
462
|
+
sources = ["Perplexity Search"]
|
|
463
|
+
|
|
464
|
+
return SearchResult(
|
|
465
|
+
subtopic=subtopic.topic, content=content, sources=sources
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logging.error(f"搜尋 '{subtopic.topic}' 失敗: {e}")
|
|
470
|
+
return SearchResult(
|
|
471
|
+
subtopic=subtopic.topic, content=f"搜尋失敗: {str(e)}", sources=[]
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# 平行執行所有搜尋
|
|
475
|
+
search_results = await asyncio.gather(
|
|
476
|
+
*[search_single_topic(subtopic) for subtopic in search_tasks]
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
logging.info(f"搜尋完成,共 {len(search_results)} 個結果")
|
|
480
|
+
|
|
481
|
+
return {"search_results": search_results, "search_completed": True}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def reasoning_analysis_node(
|
|
485
|
+
state: GovResearcherState, config: RunnableConfig
|
|
486
|
+
) -> Dict[str, Any]:
|
|
487
|
+
"""Step-003: 推理分析節點"""
|
|
488
|
+
logging.info("[GovResearcherGraph:reasoning_analysis_node] 開始推理分析")
|
|
489
|
+
|
|
490
|
+
search_results = state.get("search_results", [])
|
|
491
|
+
subtopics = state.get("decomposed_topics", [])
|
|
492
|
+
|
|
493
|
+
if not search_results or not subtopics:
|
|
494
|
+
logging.warning("缺少搜尋結果或子題目")
|
|
495
|
+
return {"reasoning_results": []}
|
|
496
|
+
|
|
497
|
+
# 準備搜尋結果文本
|
|
498
|
+
search_text = "\n\n".join(
|
|
499
|
+
[
|
|
500
|
+
f"子題目: {result.subtopic}\n內容: {result.content}\n來源: {', '.join(result.sources)}"
|
|
501
|
+
for result in search_results
|
|
502
|
+
]
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
subtopics_text = "\n".join(
|
|
506
|
+
[
|
|
507
|
+
f"{i+1}. {topic.topic} - {topic.description}"
|
|
508
|
+
for i, topic in enumerate(subtopics)
|
|
509
|
+
]
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# 獲取可配置的提示詞模板
|
|
513
|
+
prompt_template = get_prompt_template(
|
|
514
|
+
config, "reasoning_analysis_prompt", REASONING_ANALYSIS_PROMPT
|
|
515
|
+
)
|
|
516
|
+
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
517
|
+
|
|
518
|
+
# 準備提示詞
|
|
519
|
+
prompt = prompt_template.format(
|
|
520
|
+
general_guide=general_guide,
|
|
521
|
+
search_results=search_text,
|
|
522
|
+
subtopics=subtopics_text,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# 調用模型
|
|
526
|
+
model = get_reasoning_model(config)
|
|
527
|
+
response = model.invoke([HumanMessage(content=prompt)])
|
|
528
|
+
|
|
529
|
+
# 簡化版結果解析
|
|
530
|
+
reasoning_results = []
|
|
531
|
+
for i, subtopic in enumerate(subtopics):
|
|
532
|
+
reasoning_results.append(
|
|
533
|
+
ReasoningResult(
|
|
534
|
+
subtopic=subtopic.topic,
|
|
535
|
+
analysis=response.content, # 實際應該分段解析
|
|
536
|
+
conclusion=f"針對 '{subtopic.topic}' 的分析結論",
|
|
537
|
+
confidence=0.8,
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# 檢查是否需要計算驗證
|
|
542
|
+
needs_computation = (
|
|
543
|
+
"計算" in response.content
|
|
544
|
+
or "金額" in response.content
|
|
545
|
+
or "數量" in response.content
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
logging.info(f"推理分析完成,需要計算驗證: {needs_computation}")
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
"reasoning_results": reasoning_results,
|
|
552
|
+
"needs_computation": needs_computation,
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def computation_verification_node(
|
|
557
|
+
state: GovResearcherState, config: RunnableConfig
|
|
558
|
+
) -> Dict[str, Any]:
|
|
559
|
+
"""Step-004: 計算驗證節點(條件性)"""
|
|
560
|
+
logging.info("[GovResearcherGraph:computation_verification_node] 開始計算驗證")
|
|
561
|
+
|
|
562
|
+
if not state.get("needs_computation", False):
|
|
563
|
+
logging.info("無需計算驗證,跳過")
|
|
564
|
+
return {"computation_results": None}
|
|
565
|
+
|
|
566
|
+
reasoning_results = state.get("reasoning_results", [])
|
|
567
|
+
|
|
568
|
+
# 準備計算驗證提示詞
|
|
569
|
+
reasoning_text = "\n\n".join(
|
|
570
|
+
[
|
|
571
|
+
f"子題目: {result.subtopic}\n分析: {result.analysis}\n結論: {result.conclusion}"
|
|
572
|
+
for result in reasoning_results
|
|
573
|
+
]
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# 獲取可配置的通用指導原則
|
|
577
|
+
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
578
|
+
|
|
579
|
+
prompt = f"""
|
|
580
|
+
{general_guide}
|
|
581
|
+
|
|
582
|
+
你是專業的計算驗證專家,請針對以下推理結果中的計算部分進行獨立驗算:
|
|
583
|
+
|
|
584
|
+
{reasoning_text}
|
|
585
|
+
|
|
586
|
+
請使用程式碼執行功能驗證任何涉及數字計算的部分,並提供驗證結果。
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
# 使用支援代碼執行的模型
|
|
590
|
+
model = get_computation_model(config)
|
|
591
|
+
response = model.invoke([HumanMessage(content=prompt)])
|
|
592
|
+
|
|
593
|
+
logging.info("計算驗證完成")
|
|
594
|
+
|
|
595
|
+
return {"computation_results": response.content}
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
async def hallucination_verification_node(
|
|
599
|
+
state: GovResearcherState, config: RunnableConfig
|
|
600
|
+
) -> Dict[str, Any]:
|
|
601
|
+
"""Step-005: 幻覺驗證節點(支援搜尋引擎選擇)"""
|
|
602
|
+
logging.info("[GovResearcherGraph:hallucination_verification_node] 開始幻覺驗證")
|
|
603
|
+
|
|
604
|
+
# 收集前面所有結果
|
|
605
|
+
previous_results = {
|
|
606
|
+
"原始問題": state.get("original_question", ""),
|
|
607
|
+
"子題目": [topic.topic for topic in state.get("decomposed_topics", [])],
|
|
608
|
+
"搜尋結果": [result.content for result in state.get("search_results", [])],
|
|
609
|
+
"推理結果": [result.analysis for result in state.get("reasoning_results", [])],
|
|
610
|
+
"計算結果": state.get("computation_results", "無"),
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
results_text = "\n\n".join(
|
|
614
|
+
[f"{key}: {value}" for key, value in previous_results.items()]
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# 獲取可配置的提示詞模板
|
|
618
|
+
prompt_template = get_prompt_template(
|
|
619
|
+
config, "hallucination_verification_prompt", HALLUCINATION_VERIFICATION_PROMPT
|
|
620
|
+
)
|
|
621
|
+
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
622
|
+
|
|
623
|
+
# 準備驗證提示詞
|
|
624
|
+
prompt = prompt_template.format(
|
|
625
|
+
general_guide=general_guide,
|
|
626
|
+
previous_results=results_text,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# 調用模型
|
|
630
|
+
model = get_verification_model(config)
|
|
631
|
+
response = model.invoke([HumanMessage(content=prompt)])
|
|
632
|
+
|
|
633
|
+
# 如果發現問題,進行額外搜尋驗證(PRD要求:透過延伸搜尋證明或反駁前面的結論)
|
|
634
|
+
if "問題" in response.content or "錯誤" in response.content:
|
|
635
|
+
logging.info("發現潛在問題,進行額外搜尋驗證")
|
|
636
|
+
|
|
637
|
+
# 獲取搜尋引擎配置(PRD要求:搜尋引擎選擇)
|
|
638
|
+
search_vendor = get_config_value(
|
|
639
|
+
config,
|
|
640
|
+
"verification_search_vendor",
|
|
641
|
+
get_config_value(config, "search_vendor", "perplexity"),
|
|
642
|
+
)
|
|
643
|
+
search_model = get_config_value(config, "search_model", "sonar")
|
|
644
|
+
domain_filter = get_config_value(config, "domain_filter", [])
|
|
645
|
+
|
|
646
|
+
# 提取需要驗證的關鍵問題
|
|
647
|
+
verification_query = (
|
|
648
|
+
f"驗證以下政府資訊的準確性:{state.get('original_question', '')}"
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
verification_content = ""
|
|
653
|
+
|
|
654
|
+
# 根據搜尋服務商進行驗證搜尋
|
|
655
|
+
if search_vendor == "tavily":
|
|
656
|
+
async for event in respond_with_tavily_search(
|
|
657
|
+
verification_query,
|
|
658
|
+
"",
|
|
659
|
+
[{"role": "user", "content": verification_query}],
|
|
660
|
+
domain_filter,
|
|
661
|
+
False,
|
|
662
|
+
search_model,
|
|
663
|
+
):
|
|
664
|
+
verification_content += event.chunk
|
|
665
|
+
else: # perplexity
|
|
666
|
+
async for event in respond_with_perplexity_search(
|
|
667
|
+
verification_query,
|
|
668
|
+
"",
|
|
669
|
+
[{"role": "user", "content": verification_query}],
|
|
670
|
+
domain_filter,
|
|
671
|
+
False,
|
|
672
|
+
search_model,
|
|
673
|
+
):
|
|
674
|
+
verification_content += event.chunk
|
|
675
|
+
|
|
676
|
+
logging.info(f"完成額外搜尋驗證,使用服務商: {search_vendor}")
|
|
677
|
+
|
|
678
|
+
except Exception as e:
|
|
679
|
+
logging.error(f"額外搜尋驗證失敗: {e}")
|
|
680
|
+
verification_content = f"驗證搜尋失敗: {str(e)}"
|
|
681
|
+
else:
|
|
682
|
+
verification_content = "未發現明顯問題,無需額外搜尋"
|
|
683
|
+
|
|
684
|
+
# 簡化版驗證結果
|
|
685
|
+
verification_result = VerificationResult(
|
|
686
|
+
issues_found=["待實作:問題識別"],
|
|
687
|
+
corrections=["待實作:修正建議"],
|
|
688
|
+
confidence_adjustments={},
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
logging.info("幻覺驗證完成")
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
"hallucination_check": verification_result,
|
|
695
|
+
"verification_search_results": verification_content,
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def summary_response_node(
|
|
700
|
+
state: GovResearcherState, config: RunnableConfig
|
|
701
|
+
) -> Dict[str, Any]:
|
|
702
|
+
"""Step-006: 匯總回答節點"""
|
|
703
|
+
logging.info("[GovResearcherGraph:summary_response_node] 開始匯總回答")
|
|
704
|
+
|
|
705
|
+
# 收集所有處理結果
|
|
706
|
+
summary_data = {
|
|
707
|
+
"original_question": state.get("original_question", ""),
|
|
708
|
+
"subtopics": [topic.topic for topic in state.get("decomposed_topics", [])],
|
|
709
|
+
"search_results": "\n".join(
|
|
710
|
+
[result.content for result in state.get("search_results", [])]
|
|
711
|
+
),
|
|
712
|
+
"reasoning_results": "\n".join(
|
|
713
|
+
[result.analysis for result in state.get("reasoning_results", [])]
|
|
714
|
+
),
|
|
715
|
+
"computation_results": state.get("computation_results", "無計算需求"),
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
# 獲取可配置的提示詞模板
|
|
719
|
+
prompt_template = get_prompt_template(
|
|
720
|
+
config, "summary_response_prompt", SUMMARY_RESPONSE_PROMPT
|
|
721
|
+
)
|
|
722
|
+
general_guide = get_config_value(config, "general_guide", DEFAULT_GENERAL_GUIDE)
|
|
723
|
+
|
|
724
|
+
# 準備匯總提示詞
|
|
725
|
+
prompt = prompt_template.format(general_guide=general_guide, **summary_data)
|
|
726
|
+
|
|
727
|
+
# 調用模型
|
|
728
|
+
model = get_summary_model(config)
|
|
729
|
+
response = model.invoke([HumanMessage(content=prompt)])
|
|
730
|
+
|
|
731
|
+
final_answer = response.content
|
|
732
|
+
|
|
733
|
+
logging.info("匯總回答完成")
|
|
734
|
+
|
|
735
|
+
return {"final_answer": final_answer, "messages": [AIMessage(content=final_answer)]}
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def should_compute(state: GovResearcherState) -> str:
|
|
739
|
+
"""條件分支:決定是否需要計算驗證"""
|
|
740
|
+
if state.get("needs_computation", False):
|
|
741
|
+
return COMPUTATION_VERIFICATION_NODE
|
|
742
|
+
else:
|
|
743
|
+
return SUMMARY_RESPONSE_NODE
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# 預設配置(根據PRD規格修正)
|
|
747
|
+
DEFAULT_GOV_RESEARCHER_CONFIG = {
|
|
748
|
+
"decomposition_model": "gemini-2.5-pro", # PRD 預設
|
|
749
|
+
"reasoning_model": "gemini-2.5-flash",
|
|
750
|
+
"computation_model": "gemini-2.5-flash",
|
|
751
|
+
"verification_model": "gemini-2.5-flash",
|
|
752
|
+
"summary_model": "gemini-2.5-flash", # PRD 預設
|
|
753
|
+
"search_vendor": "perplexity", # PRD 預設
|
|
754
|
+
"max_parallel_searches": 5,
|
|
755
|
+
"domain_filter": [],
|
|
756
|
+
"search_model": "sonar", # PRD 預設,非 sonar-reasoning-pro
|
|
757
|
+
"general_guide": DEFAULT_GENERAL_GUIDE,
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def get_content_for_gov_researcher(state: Dict[str, Any]) -> str:
|
|
762
|
+
"""從狀態中取得內容"""
|
|
763
|
+
return state.get("final_answer", "")
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
class GovResearcherGraph:
|
|
767
|
+
"""政府研究員 LangGraph Agent"""
|
|
768
|
+
|
|
769
|
+
def __init__(self, memory: BaseCheckpointSaver = None):
|
|
770
|
+
self.memory = memory if memory is not None else MemorySaver()
|
|
771
|
+
self._initialize_graph()
|
|
772
|
+
|
|
773
|
+
def _initialize_graph(self):
|
|
774
|
+
"""初始化 LangGraph 工作流"""
|
|
775
|
+
workflow = StateGraph(
|
|
776
|
+
GovResearcherState, context_schema=GovResearcherConfigSchema
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# 添加節點
|
|
780
|
+
workflow.add_node(TOPIC_DECOMPOSITION_NODE, topic_decomposition_node)
|
|
781
|
+
workflow.add_node("search_preparation", search_preparation_node)
|
|
782
|
+
workflow.add_node("search_subtopic", search_subtopic_node)
|
|
783
|
+
workflow.add_node(REASONING_ANALYSIS_NODE, reasoning_analysis_node)
|
|
784
|
+
workflow.add_node(COMPUTATION_VERIFICATION_NODE, computation_verification_node)
|
|
785
|
+
workflow.add_node(
|
|
786
|
+
HALLUCINATION_VERIFICATION_NODE, hallucination_verification_node
|
|
787
|
+
)
|
|
788
|
+
workflow.add_node(SUMMARY_RESPONSE_NODE, summary_response_node)
|
|
789
|
+
|
|
790
|
+
# 定義邊(工作流程)
|
|
791
|
+
workflow.add_edge(START, TOPIC_DECOMPOSITION_NODE)
|
|
792
|
+
workflow.add_edge(TOPIC_DECOMPOSITION_NODE, "search_preparation")
|
|
793
|
+
workflow.add_edge("search_preparation", "search_subtopic")
|
|
794
|
+
workflow.add_edge("search_subtopic", REASONING_ANALYSIS_NODE)
|
|
795
|
+
|
|
796
|
+
# 條件分支:是否需要計算驗證
|
|
797
|
+
workflow.add_conditional_edges(
|
|
798
|
+
REASONING_ANALYSIS_NODE,
|
|
799
|
+
should_compute,
|
|
800
|
+
[COMPUTATION_VERIFICATION_NODE, SUMMARY_RESPONSE_NODE],
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
workflow.add_edge(COMPUTATION_VERIFICATION_NODE, SUMMARY_RESPONSE_NODE)
|
|
804
|
+
workflow.add_edge(SUMMARY_RESPONSE_NODE, END)
|
|
805
|
+
|
|
806
|
+
# 編譯圖
|
|
807
|
+
self._graph = workflow.compile(checkpointer=self.memory)
|
|
808
|
+
self._graph_no_memory = workflow.compile()
|
|
809
|
+
|
|
810
|
+
@property
|
|
811
|
+
def graph(self):
|
|
812
|
+
"""帶記憶的圖"""
|
|
813
|
+
return self._graph
|
|
814
|
+
|
|
815
|
+
@property
|
|
816
|
+
def graph_no_memory(self):
|
|
817
|
+
"""不帶記憶的圖"""
|
|
818
|
+
return self._graph_no_memory
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
# 導出實例
|
|
822
|
+
gov_researcher_graph = GovResearcherGraph().graph_no_memory
|