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.
Files changed (87) hide show
  1. botrun_flow_lang/api/auth_api.py +39 -39
  2. botrun_flow_lang/api/auth_utils.py +183 -183
  3. botrun_flow_lang/api/botrun_back_api.py +65 -65
  4. botrun_flow_lang/api/flow_api.py +3 -3
  5. botrun_flow_lang/api/hatch_api.py +508 -508
  6. botrun_flow_lang/api/langgraph_api.py +811 -811
  7. botrun_flow_lang/api/line_bot_api.py +1484 -1484
  8. botrun_flow_lang/api/model_api.py +300 -300
  9. botrun_flow_lang/api/rate_limit_api.py +32 -32
  10. botrun_flow_lang/api/routes.py +79 -79
  11. botrun_flow_lang/api/search_api.py +53 -53
  12. botrun_flow_lang/api/storage_api.py +395 -395
  13. botrun_flow_lang/api/subsidy_api.py +290 -290
  14. botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
  15. botrun_flow_lang/api/user_setting_api.py +70 -70
  16. botrun_flow_lang/api/version_api.py +31 -31
  17. botrun_flow_lang/api/youtube_api.py +26 -26
  18. botrun_flow_lang/constants.py +13 -13
  19. botrun_flow_lang/langgraph_agents/agents/agent_runner.py +178 -178
  20. botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
  21. botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
  22. botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
  23. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gemini_subsidy_graph.py +460 -460
  24. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
  25. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
  26. botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +723 -723
  27. botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
  28. botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
  29. botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
  30. botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
  31. botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
  32. botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
  33. botrun_flow_lang/langgraph_agents/agents/util/local_files.py +419 -419
  34. botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
  35. botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
  36. botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +486 -486
  37. botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +250 -250
  38. botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +204 -204
  39. botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
  40. botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
  41. botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
  42. botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
  43. botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
  44. botrun_flow_lang/llm_agent/llm_agent.py +19 -19
  45. botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
  46. botrun_flow_lang/log/.gitignore +2 -2
  47. botrun_flow_lang/main.py +61 -61
  48. botrun_flow_lang/main_fast.py +51 -51
  49. botrun_flow_lang/mcp_server/__init__.py +10 -10
  50. botrun_flow_lang/mcp_server/default_mcp.py +744 -744
  51. botrun_flow_lang/models/nodes/utils.py +205 -205
  52. botrun_flow_lang/models/token_usage.py +34 -34
  53. botrun_flow_lang/requirements.txt +21 -21
  54. botrun_flow_lang/services/base/firestore_base.py +30 -30
  55. botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
  56. botrun_flow_lang/services/hatch/hatch_fs_store.py +419 -419
  57. botrun_flow_lang/services/storage/storage_cs_store.py +206 -206
  58. botrun_flow_lang/services/storage/storage_factory.py +12 -12
  59. botrun_flow_lang/services/storage/storage_store.py +65 -65
  60. botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
  61. botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
  62. botrun_flow_lang/static/docs/tools/index.html +926 -926
  63. botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
  64. botrun_flow_lang/tests/api_stress_test.py +357 -357
  65. botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
  66. botrun_flow_lang/tests/test_botrun_app.py +46 -46
  67. botrun_flow_lang/tests/test_html_util.py +31 -31
  68. botrun_flow_lang/tests/test_img_analyzer.py +190 -190
  69. botrun_flow_lang/tests/test_img_util.py +39 -39
  70. botrun_flow_lang/tests/test_local_files.py +114 -114
  71. botrun_flow_lang/tests/test_mermaid_util.py +103 -103
  72. botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
  73. botrun_flow_lang/tests/test_plotly_util.py +151 -151
  74. botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
  75. botrun_flow_lang/tools/generate_docs.py +133 -133
  76. botrun_flow_lang/tools/templates/tools.html +153 -153
  77. botrun_flow_lang/utils/__init__.py +7 -7
  78. botrun_flow_lang/utils/botrun_logger.py +344 -344
  79. botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
  80. botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
  81. botrun_flow_lang/utils/google_drive_utils.py +654 -654
  82. botrun_flow_lang/utils/langchain_utils.py +324 -324
  83. botrun_flow_lang/utils/yaml_utils.py +9 -9
  84. {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/METADATA +1 -1
  85. botrun_flow_lang-5.12.264.dist-info/RECORD +102 -0
  86. botrun_flow_lang-5.12.263.dist-info/RECORD +0 -102
  87. {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