flowllm 0.1.0__py3-none-any.whl → 0.1.2__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 (141) hide show
  1. flowllm/__init__.py +21 -0
  2. flowllm/app.py +15 -0
  3. flowllm/client/__init__.py +25 -0
  4. flowllm/client/async_http_client.py +81 -0
  5. flowllm/client/http_client.py +81 -0
  6. flowllm/client/mcp_client.py +133 -0
  7. flowllm/client/sync_mcp_client.py +116 -0
  8. flowllm/config/__init__.py +1 -0
  9. flowllm/config/default.yaml +77 -0
  10. flowllm/config/empty.yaml +37 -0
  11. flowllm/config/pydantic_config_parser.py +242 -0
  12. flowllm/context/base_context.py +79 -0
  13. flowllm/context/flow_context.py +16 -0
  14. llmflow/op/prompt_mixin.py → flowllm/context/prompt_handler.py +25 -14
  15. flowllm/context/registry.py +30 -0
  16. flowllm/context/service_context.py +147 -0
  17. flowllm/embedding_model/__init__.py +1 -0
  18. {llmflow → flowllm}/embedding_model/base_embedding_model.py +93 -2
  19. {llmflow → flowllm}/embedding_model/openai_compatible_embedding_model.py +71 -13
  20. flowllm/flow/__init__.py +1 -0
  21. flowllm/flow/base_flow.py +72 -0
  22. flowllm/flow/base_tool_flow.py +15 -0
  23. flowllm/flow/gallery/__init__.py +8 -0
  24. flowllm/flow/gallery/cmd_flow.py +11 -0
  25. flowllm/flow/gallery/code_tool_flow.py +30 -0
  26. flowllm/flow/gallery/dashscope_search_tool_flow.py +34 -0
  27. flowllm/flow/gallery/deepsearch_tool_flow.py +39 -0
  28. flowllm/flow/gallery/expression_tool_flow.py +18 -0
  29. flowllm/flow/gallery/mock_tool_flow.py +67 -0
  30. flowllm/flow/gallery/tavily_search_tool_flow.py +30 -0
  31. flowllm/flow/gallery/terminate_tool_flow.py +30 -0
  32. flowllm/flow/parser/expression_parser.py +171 -0
  33. flowllm/llm/__init__.py +2 -0
  34. {llmflow → flowllm}/llm/base_llm.py +100 -18
  35. flowllm/llm/litellm_llm.py +455 -0
  36. flowllm/llm/openai_compatible_llm.py +439 -0
  37. flowllm/op/__init__.py +11 -0
  38. llmflow/op/react/react_v1_op.py → flowllm/op/agent/react_op.py +17 -22
  39. flowllm/op/akshare/__init__.py +3 -0
  40. flowllm/op/akshare/get_ak_a_code_op.py +108 -0
  41. flowllm/op/akshare/get_ak_a_code_prompt.yaml +21 -0
  42. flowllm/op/akshare/get_ak_a_info_op.py +140 -0
  43. flowllm/op/base_llm_op.py +64 -0
  44. flowllm/op/base_op.py +148 -0
  45. flowllm/op/base_ray_op.py +313 -0
  46. flowllm/op/code/__init__.py +1 -0
  47. flowllm/op/code/execute_code_op.py +42 -0
  48. flowllm/op/gallery/__init__.py +2 -0
  49. flowllm/op/gallery/mock_op.py +42 -0
  50. flowllm/op/gallery/terminate_op.py +29 -0
  51. flowllm/op/parallel_op.py +23 -0
  52. flowllm/op/search/__init__.py +3 -0
  53. flowllm/op/search/dashscope_deep_research_op.py +260 -0
  54. flowllm/op/search/dashscope_search_op.py +179 -0
  55. flowllm/op/search/dashscope_search_prompt.yaml +13 -0
  56. flowllm/op/search/tavily_search_op.py +102 -0
  57. flowllm/op/sequential_op.py +21 -0
  58. flowllm/schema/flow_request.py +12 -0
  59. flowllm/schema/flow_response.py +12 -0
  60. flowllm/schema/message.py +35 -0
  61. flowllm/schema/service_config.py +72 -0
  62. flowllm/schema/tool_call.py +118 -0
  63. {llmflow → flowllm}/schema/vector_node.py +1 -0
  64. flowllm/service/__init__.py +3 -0
  65. flowllm/service/base_service.py +68 -0
  66. flowllm/service/cmd_service.py +15 -0
  67. flowllm/service/http_service.py +79 -0
  68. flowllm/service/mcp_service.py +47 -0
  69. flowllm/storage/__init__.py +1 -0
  70. flowllm/storage/cache/__init__.py +1 -0
  71. flowllm/storage/cache/cache_data_handler.py +104 -0
  72. flowllm/storage/cache/data_cache.py +375 -0
  73. flowllm/storage/vector_store/__init__.py +3 -0
  74. flowllm/storage/vector_store/base_vector_store.py +44 -0
  75. {llmflow → flowllm/storage}/vector_store/chroma_vector_store.py +11 -10
  76. {llmflow → flowllm/storage}/vector_store/es_vector_store.py +11 -11
  77. llmflow/vector_store/file_vector_store.py → flowllm/storage/vector_store/local_vector_store.py +110 -11
  78. flowllm/utils/common_utils.py +52 -0
  79. flowllm/utils/fetch_url.py +117 -0
  80. flowllm/utils/llm_utils.py +28 -0
  81. flowllm/utils/ridge_v2.py +54 -0
  82. {llmflow → flowllm}/utils/timer.py +5 -4
  83. {flowllm-0.1.0.dist-info → flowllm-0.1.2.dist-info}/METADATA +45 -388
  84. flowllm-0.1.2.dist-info/RECORD +99 -0
  85. flowllm-0.1.2.dist-info/entry_points.txt +2 -0
  86. {flowllm-0.1.0.dist-info → flowllm-0.1.2.dist-info}/licenses/LICENSE +1 -1
  87. flowllm-0.1.2.dist-info/top_level.txt +1 -0
  88. flowllm-0.1.0.dist-info/RECORD +0 -66
  89. flowllm-0.1.0.dist-info/entry_points.txt +0 -3
  90. flowllm-0.1.0.dist-info/top_level.txt +0 -1
  91. llmflow/app.py +0 -53
  92. llmflow/config/config_parser.py +0 -80
  93. llmflow/config/mock_config.yaml +0 -58
  94. llmflow/embedding_model/__init__.py +0 -5
  95. llmflow/enumeration/agent_state.py +0 -8
  96. llmflow/llm/__init__.py +0 -5
  97. llmflow/llm/openai_compatible_llm.py +0 -283
  98. llmflow/mcp_server.py +0 -110
  99. llmflow/op/__init__.py +0 -10
  100. llmflow/op/base_op.py +0 -125
  101. llmflow/op/mock_op.py +0 -40
  102. llmflow/op/vector_store/__init__.py +0 -13
  103. llmflow/op/vector_store/recall_vector_store_op.py +0 -48
  104. llmflow/op/vector_store/update_vector_store_op.py +0 -28
  105. llmflow/op/vector_store/vector_store_action_op.py +0 -46
  106. llmflow/pipeline/pipeline.py +0 -94
  107. llmflow/pipeline/pipeline_context.py +0 -37
  108. llmflow/schema/app_config.py +0 -69
  109. llmflow/schema/experience.py +0 -144
  110. llmflow/schema/message.py +0 -68
  111. llmflow/schema/request.py +0 -32
  112. llmflow/schema/response.py +0 -29
  113. llmflow/service/__init__.py +0 -0
  114. llmflow/service/llmflow_service.py +0 -96
  115. llmflow/tool/__init__.py +0 -9
  116. llmflow/tool/base_tool.py +0 -80
  117. llmflow/tool/code_tool.py +0 -43
  118. llmflow/tool/dashscope_search_tool.py +0 -162
  119. llmflow/tool/mcp_tool.py +0 -77
  120. llmflow/tool/tavily_search_tool.py +0 -109
  121. llmflow/tool/terminate_tool.py +0 -23
  122. llmflow/utils/__init__.py +0 -0
  123. llmflow/utils/common_utils.py +0 -17
  124. llmflow/utils/file_handler.py +0 -25
  125. llmflow/utils/http_client.py +0 -156
  126. llmflow/utils/op_utils.py +0 -102
  127. llmflow/utils/registry.py +0 -33
  128. llmflow/vector_store/__init__.py +0 -7
  129. llmflow/vector_store/base_vector_store.py +0 -136
  130. {llmflow → flowllm/context}/__init__.py +0 -0
  131. {llmflow/config → flowllm/enumeration}/__init__.py +0 -0
  132. {llmflow → flowllm}/enumeration/chunk_enum.py +0 -0
  133. {llmflow → flowllm}/enumeration/http_enum.py +0 -0
  134. {llmflow → flowllm}/enumeration/role.py +0 -0
  135. {llmflow/enumeration → flowllm/flow/parser}/__init__.py +0 -0
  136. {llmflow/op/react → flowllm/op/agent}/__init__.py +0 -0
  137. /llmflow/op/react/react_v1_prompt.yaml → /flowllm/op/agent/react_prompt.yaml +0 -0
  138. {llmflow/pipeline → flowllm/schema}/__init__.py +0 -0
  139. {llmflow/schema → flowllm/utils}/__init__.py +0 -0
  140. {llmflow → flowllm}/utils/singleton.py +0 -0
  141. {flowllm-0.1.0.dist-info → flowllm-0.1.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,42 @@
1
+ import sys
2
+ from io import StringIO
3
+
4
+ from loguru import logger
5
+
6
+ from flowllm.context.flow_context import FlowContext
7
+ from flowllm.context.service_context import C
8
+ from flowllm.op.base_op import BaseOp
9
+
10
+
11
+ @C.register_op()
12
+ class ExecuteCodeOp(BaseOp):
13
+
14
+ def execute(self):
15
+ old_stdout = sys.stdout
16
+ redirected_output = sys.stdout = StringIO()
17
+
18
+ try:
19
+ code_key: str = self.op_params.get("code_key", "code")
20
+ code_str: str = self.context[code_key]
21
+ exec(code_str)
22
+ code_result = redirected_output.getvalue()
23
+
24
+ except Exception as e:
25
+ logger.info(f"{self.name} encounter exception! error={e.args}")
26
+ code_result = str(e)
27
+
28
+ sys.stdout = old_stdout
29
+ self.context.code_result = code_result
30
+
31
+
32
+ if __name__ == "__main__":
33
+ C.set_default_service_config().init_by_service_config()
34
+ op = ExecuteCodeOp()
35
+
36
+ context = FlowContext(code="print('Hello World')")
37
+ op(context=context)
38
+ print(context.code_result)
39
+
40
+ context.code = "print('Hello World!'"
41
+ op(context=context)
42
+ print(context.code_result)
@@ -0,0 +1,2 @@
1
+ from .mock_op import Mock1Op, Mock2Op, Mock3Op, Mock4Op, Mock5Op, Mock6Op
2
+ from .terminate_op import TerminateOp
@@ -0,0 +1,42 @@
1
+ import time
2
+
3
+ from loguru import logger
4
+
5
+ from flowllm.context.service_context import C
6
+ from flowllm.op.base_llm_op import BaseLLMOp
7
+
8
+
9
+ @C.register_op()
10
+ class Mock1Op(BaseLLMOp):
11
+ def execute(self):
12
+ time.sleep(1)
13
+ a = self.context.a
14
+ b = self.context.b
15
+ logger.info(f"enter class={self.name}. a={a} b={b}")
16
+
17
+ self.context.response.answer = f"{self.name} {a} {b} answer=47"
18
+
19
+
20
+ @C.register_op()
21
+ class Mock2Op(Mock1Op):
22
+ ...
23
+
24
+
25
+ @C.register_op()
26
+ class Mock3Op(Mock1Op):
27
+ ...
28
+
29
+
30
+ @C.register_op()
31
+ class Mock4Op(Mock1Op):
32
+ ...
33
+
34
+
35
+ @C.register_op()
36
+ class Mock5Op(Mock1Op):
37
+ ...
38
+
39
+
40
+ @C.register_op()
41
+ class Mock6Op(Mock1Op):
42
+ ...
@@ -0,0 +1,29 @@
1
+ from flowllm.context.service_context import C
2
+ from flowllm.op.base_op import BaseOp
3
+
4
+
5
+ @C.register_op()
6
+ class TerminateOp(BaseOp):
7
+
8
+ def execute(self):
9
+ # Get status from context
10
+ status = self.context.status
11
+ assert status in ["success", "failure"], f"Invalid status: {status}"
12
+ self.context.terminate_answer = f"The interaction has been completed with status: {status}"
13
+
14
+
15
+ if __name__ == "__main__":
16
+ from flowllm.context.flow_context import FlowContext
17
+
18
+ C.set_default_service_config().init_by_service_config()
19
+
20
+ # Test success termination
21
+ op = TerminateOp()
22
+ context = FlowContext(status="success")
23
+ result = op(context=context)
24
+ print(f"Result: {context.terminate_answer}")
25
+
26
+ # Test failure termination
27
+ context.status = "failure"
28
+ op(context=context)
29
+ print(f"Result: {context.terminate_answer}")
@@ -0,0 +1,23 @@
1
+ from typing import List
2
+
3
+ from flowllm.op.base_op import BaseOp
4
+
5
+
6
+ class ParallelOp(BaseOp):
7
+
8
+ def __init__(self, ops: List[BaseOp], **kwargs):
9
+ super().__init__(**kwargs)
10
+ self.ops = ops
11
+
12
+ def execute(self):
13
+ for op in self.ops:
14
+ self.submit_task(op.__call__, context=self.context)
15
+
16
+ self.join_task(task_desc="parallel execution")
17
+
18
+ def __or__(self, op: BaseOp):
19
+ if isinstance(op, ParallelOp):
20
+ self.ops.extend(op.ops)
21
+ else:
22
+ self.ops.append(op)
23
+ return self
@@ -0,0 +1,3 @@
1
+ from .dashscope_deep_research_op import DashscopeDeepResearchOp
2
+ from .dashscope_search_op import DashscopeSearchOp
3
+ from .tavily_search_op import TavilySearchOp
@@ -0,0 +1,260 @@
1
+ import os
2
+
3
+ import dashscope
4
+ from loguru import logger
5
+
6
+ from flowllm.context.flow_context import FlowContext
7
+ from flowllm.context.service_context import C
8
+ from flowllm.op.base_llm_op import BaseLLMOp
9
+ from flowllm.storage.cache.data_cache import DataCache
10
+
11
+
12
+ @C.register_op()
13
+ class DashscopeDeepResearchOp(BaseLLMOp):
14
+ file_path: str = __file__
15
+
16
+ """
17
+ Dashscope deep research operation using Alibaba's Qwen-deep-research model.
18
+
19
+ This operation performs deep research using Dashscope's Generation API with the
20
+ qwen-deep-research model. It handles the multi-phase research process including
21
+ model questioning, web research, and result generation.
22
+ """
23
+
24
+ def __init__(self,
25
+ model: str = "qwen-deep-research",
26
+ enable_print: bool = True,
27
+ enable_cache: bool = True,
28
+ cache_path: str = "./dashscope_deep_research_cache",
29
+ cache_expire_hours: float = 24,
30
+ max_retries: int = 3,
31
+ return_only_content: bool = True,
32
+ **kwargs):
33
+ super().__init__(**kwargs)
34
+
35
+ self.model = model
36
+ self.enable_print = enable_print
37
+ self.enable_cache = enable_cache
38
+ self.cache_expire_hours = cache_expire_hours
39
+ self.max_retries = max_retries
40
+ self.return_only_content = return_only_content
41
+
42
+ # Ensure API key is available
43
+ self.api_key = os.getenv("FLOW_DASHSCOPE_API_KEY")
44
+ self.cache = DataCache(cache_path) if self.enable_cache else None
45
+
46
+ def process_responses(self, responses, step_name):
47
+ """Process streaming responses from the deep research model"""
48
+ current_phase = None
49
+ phase_content = ""
50
+ research_goal = ""
51
+ web_sites = []
52
+ keepalive_shown = False # 标记是否已经显示过KeepAlive提示
53
+
54
+ for response in responses:
55
+ # 检查响应状态码
56
+ if hasattr(response, 'status_code') and response.status_code != 200:
57
+ logger.warning(f"HTTP返回码:{response.status_code}")
58
+ if hasattr(response, 'code'):
59
+ logger.warning(f"错误码:{response.code}")
60
+ if hasattr(response, 'message'):
61
+ logger.warning(f"错误信息:{response.message}")
62
+ continue
63
+
64
+ if hasattr(response, 'output') and response.output:
65
+ message = response.output.get('message', {})
66
+ phase = message.get('phase')
67
+ content = message.get('content', '')
68
+ status = message.get('status')
69
+ extra = message.get('extra', {})
70
+
71
+ # 阶段变化检测
72
+ if phase != current_phase:
73
+ if current_phase and phase_content:
74
+ # 根据阶段名称和步骤名称来显示不同的完成描述
75
+ if step_name == "第一步:模型反问确认" and current_phase == "answer":
76
+ logger.info("模型反问阶段完成")
77
+ else:
78
+ logger.info(f"{current_phase} 阶段完成")
79
+ current_phase = phase
80
+ phase_content = ""
81
+ keepalive_shown = False # 重置KeepAlive提示标记
82
+
83
+ # 根据阶段名称和步骤名称来显示不同的描述
84
+ if step_name == "第一步:模型反问确认" and phase == "answer":
85
+ logger.info("进入模型反问阶段")
86
+ else:
87
+ logger.info(f"进入 {phase} 阶段")
88
+
89
+ # 处理WebResearch阶段的特殊信息
90
+ if phase == "WebResearch":
91
+ if extra.get('deep_research', {}).get('research'):
92
+ research_info = extra['deep_research']['research']
93
+
94
+ # 处理streamingQueries状态
95
+ if status == "streamingQueries":
96
+ if 'researchGoal' in research_info:
97
+ goal = research_info['researchGoal']
98
+ if goal:
99
+ research_goal += goal
100
+ if self.enable_print:
101
+ print(f" 研究目标: {goal}", end='', flush=True)
102
+
103
+ # 处理streamingWebResult状态
104
+ elif status == "streamingWebResult":
105
+ if 'webSites' in research_info:
106
+ sites = research_info['webSites']
107
+ if sites and sites != web_sites: # 避免重复显示
108
+ web_sites = sites
109
+ if self.enable_print:
110
+ print(f" 找到 {len(sites)} 个相关网站:")
111
+ for i, site in enumerate(sites, 1):
112
+ print(f" {i}. {site.get('title', '无标题')}")
113
+ print(f" 描述: {site.get('description', '无描述')[:100]}...")
114
+ print(f" URL: {site.get('url', '无链接')}")
115
+ if site.get('favicon'):
116
+ print(f" 图标: {site['favicon']}")
117
+ print()
118
+
119
+ # 处理WebResultFinished状态
120
+ elif status == "WebResultFinished":
121
+ if self.enable_print:
122
+ print(f" 网络搜索完成,共找到 {len(web_sites)} 个参考信息源")
123
+ if research_goal:
124
+ print(f" 研究目标: {research_goal}")
125
+
126
+ # 累积内容并显示
127
+ if content:
128
+ phase_content += content
129
+ # 实时显示内容
130
+ if self.enable_print:
131
+ print(content, end='', flush=True)
132
+
133
+ # 显示阶段状态变化
134
+ if status and status != "typing":
135
+ if self.enable_print:
136
+ print(f" 状态: {status}")
137
+
138
+ # 显示状态说明
139
+ if status == "streamingQueries":
140
+ if self.enable_print:
141
+ print(" → 正在生成研究目标和搜索查询(WebResearch阶段)")
142
+ elif status == "streamingWebResult":
143
+ if self.enable_print:
144
+ print(" → 正在执行搜索、网页阅读和代码执行(WebResearch阶段)")
145
+ elif status == "WebResultFinished":
146
+ if self.enable_print:
147
+ print(" → 网络搜索阶段完成(WebResearch阶段)")
148
+
149
+ # 当状态为finished时,显示token消耗情况
150
+ if status == "finished":
151
+ if hasattr(response, 'usage') and response.usage:
152
+ usage = response.usage
153
+ if self.enable_print:
154
+ print(f" Token消耗统计:")
155
+ print(f" 输入tokens: {usage.get('input_tokens', 0)}")
156
+ print(f" 输出tokens: {usage.get('output_tokens', 0)}")
157
+ print(f" 请求ID: {response.get('request_id', '未知')}")
158
+
159
+ if phase == "KeepAlive":
160
+ # 只在第一次进入KeepAlive阶段时显示提示
161
+ if not keepalive_shown:
162
+ if self.enable_print:
163
+ print("当前步骤已经完成,准备开始下一步骤工作")
164
+ keepalive_shown = True
165
+ continue
166
+
167
+ if current_phase and phase_content:
168
+ if step_name == "第一步:模型反问确认" and current_phase == "answer":
169
+ logger.info("模型反问阶段完成")
170
+ else:
171
+ logger.info(f"{current_phase} 阶段完成")
172
+
173
+ return phase_content
174
+
175
+ def call_deep_research_model(self, messages, step_name):
176
+ """Call the deep research model with the given messages"""
177
+ if self.enable_print:
178
+ print(f"\n=== {step_name} ===")
179
+
180
+ try:
181
+ responses = dashscope.Generation.call(
182
+ api_key=self.api_key,
183
+ model=self.model,
184
+ messages=messages,
185
+ # qwen-deep-research模型目前仅支持流式输出
186
+ stream=True
187
+ # incremental_output=True 使用增量输出请添加此参数
188
+ )
189
+
190
+ return self.process_responses(responses, step_name)
191
+
192
+ except Exception as e:
193
+ logger.error(f"调用API时发生错误: {e}")
194
+ return ""
195
+
196
+ def execute(self):
197
+ """Execute the Dashscope deep research operation"""
198
+ # Get query from context
199
+ query = self.context.query
200
+
201
+ # Check cache first
202
+ if self.enable_cache and self.cache:
203
+ cached_result = self.cache.load(query)
204
+ if cached_result:
205
+ if self.return_only_content:
206
+ self.context.dashscope_deep_research_result = cached_result.get("content", "")
207
+ else:
208
+ self.context.dashscope_deep_research_result = cached_result
209
+ return
210
+
211
+ # 第一步:模型反问确认
212
+ # 模型会分析用户问题,提出细化问题来明确研究方向
213
+ messages = [{'role': 'user', 'content': query}]
214
+ step1_content = self.call_deep_research_model(messages, "第一步:模型反问确认")
215
+
216
+ # 第二步:深入研究
217
+ # 基于第一步的反问内容,模型会执行完整的研究流程
218
+ messages = [
219
+ {'role': 'user', 'content': query},
220
+ {'role': 'assistant', 'content': step1_content}, # 包含模型的反问内容
221
+ {'role': 'user', 'content': '帮我生成完整且逻辑性的报告'}
222
+ ]
223
+
224
+ result_content = self.call_deep_research_model(messages, "第二步:深入研究")
225
+
226
+ if self.enable_print:
227
+ print(result_content)
228
+ print("\n 研究完成!")
229
+
230
+ # Prepare final result
231
+ final_result = {
232
+ "query": query,
233
+ "step1_content": step1_content,
234
+ "final_result": result_content,
235
+ "model": self.model
236
+ }
237
+
238
+ # Cache the result if enabled
239
+ if self.enable_cache and self.cache:
240
+ self.cache.save(query, final_result, expire_hours=self.cache_expire_hours)
241
+
242
+ # Set context
243
+ if self.return_only_content:
244
+ self.context.dashscope_deep_research_result = result_content
245
+ else:
246
+ self.context.dashscope_deep_research_result = final_result
247
+
248
+
249
+ def main():
250
+ C.set_default_service_config().init_by_service_config()
251
+
252
+ op = DashscopeDeepResearchOp(enable_print=True, enable_cache=True)
253
+
254
+ context = FlowContext(query="中国电解铝行业值得投资吗,有哪些值得投资的标的,各个标的之间需要对比优劣势")
255
+ op(context=context)
256
+ print(context.dashscope_deep_research_result)
257
+
258
+
259
+ if __name__ == "__main__":
260
+ main()
@@ -0,0 +1,179 @@
1
+ import os
2
+ import time
3
+ from typing import Dict, Any, List
4
+
5
+ import dashscope
6
+ from loguru import logger
7
+
8
+ from flowllm.context.flow_context import FlowContext
9
+ from flowllm.context.service_context import C
10
+ from flowllm.op.base_llm_op import BaseLLMOp
11
+ from flowllm.storage.cache.data_cache import DataCache
12
+
13
+
14
+ @C.register_op()
15
+ class DashscopeSearchOp(BaseLLMOp):
16
+ file_path: str = __file__
17
+
18
+ """
19
+ Dashscope search operation using Alibaba's Qwen model with search capabilities.
20
+
21
+ This operation performs web search using Dashscope's Generation API with search enabled.
22
+ It extracts search results and provides formatted responses with citations.
23
+ """
24
+
25
+ def __init__(self,
26
+ model: str = "qwen-plus",
27
+ enable_print: bool = True,
28
+ enable_cache: bool = False,
29
+ cache_path: str = "./dashscope_search_cache",
30
+ cache_expire_hours: float = 0.1,
31
+ max_retries: int = 3,
32
+ search_strategy: str = "max",
33
+ return_only_content: bool = True,
34
+ enable_role_prompt: bool = True,
35
+ **kwargs):
36
+ super().__init__(**kwargs)
37
+
38
+ self.model = model
39
+ self.enable_print = enable_print
40
+ self.enable_cache = enable_cache
41
+ self.cache_expire_hours = cache_expire_hours
42
+ self.max_retries = max_retries
43
+ self.search_strategy = search_strategy
44
+ self.return_only_content = return_only_content
45
+ self.enable_role_prompt = enable_role_prompt
46
+
47
+ # Ensure API key is available
48
+ self.api_key = os.getenv("FLOW_DASHSCOPE_API_KEY")
49
+ self.cache = DataCache(cache_path) if self.enable_cache else None
50
+
51
+ @staticmethod
52
+ def format_search_results(search_results: List[Dict[str, Any]]) -> str:
53
+ """Format search results for display"""
54
+ formatted_results = ["=" * 20 + " Search Results " + "=" * 20]
55
+
56
+ for web in search_results:
57
+ formatted_results.append(f"[{web['index']}]: [{web['title']}]({web['url']})")
58
+
59
+ return "\n".join(formatted_results)
60
+
61
+ def post_process(self, response_data: dict) -> dict:
62
+ """Post-process the response and optionally print results"""
63
+ if self.enable_print:
64
+ # Print search information
65
+ if "search_results" in response_data:
66
+ search_info = self.format_search_results(response_data["search_results"])
67
+ logger.info(f"Search Information:\n{search_info}")
68
+
69
+ # Print response content
70
+ if "response_content" in response_data:
71
+ logger.info("=" * 20 + " Response Content " + "=" * 20)
72
+ logger.info(response_data["response_content"])
73
+
74
+ return response_data
75
+
76
+ def execute(self):
77
+ """Execute the Dashscope search operation"""
78
+ # Get query from context - support multiple parameter names
79
+ query = self.context.query
80
+
81
+ # Check cache first
82
+ if self.enable_cache and self.cache:
83
+ cached_result = self.cache.load(query)
84
+ if cached_result:
85
+ result = self.post_process(cached_result)
86
+ if self.return_only_content:
87
+ self.context.dashscope_search_result = result["response_content"]
88
+ else:
89
+ self.context.dashscope_search_result = result
90
+
91
+ return
92
+
93
+ if self.enable_role_prompt:
94
+ user_query = self.prompt_format(prompt_name="role_prompt", query=query)
95
+ else:
96
+ user_query = query
97
+ messages: list = [{"role": "user", "content": user_query}]
98
+
99
+ # Retry logic for API calls
100
+ for attempt in range(self.max_retries):
101
+ try:
102
+ # Call Dashscope Generation API with search enabled
103
+ response = dashscope.Generation.call(
104
+ api_key=self.api_key,
105
+ model=self.model,
106
+ messages=messages,
107
+ enable_search=True, # Enable web search
108
+ search_options={
109
+ "forced_search": True, # Force web search
110
+ "enable_source": True, # Include search source information
111
+ "enable_citation": False, # Enable citation markers
112
+ "search_strategy": self.search_strategy, # Search strategy
113
+ },
114
+ result_format="message",
115
+ )
116
+
117
+ # Extract search results and response content
118
+ search_results = []
119
+ response_content = ""
120
+
121
+ if hasattr(response, 'output') and response.output:
122
+ # Extract search information
123
+ if hasattr(response.output, 'search_info') and response.output.search_info:
124
+ search_results = response.output.search_info.get("search_results", [])
125
+
126
+ # Extract response content
127
+ if (hasattr(response.output, 'choices') and
128
+ response.output.choices and
129
+ len(response.output.choices) > 0):
130
+ response_content = response.output.choices[0].message.content
131
+
132
+ # Prepare final result
133
+ final_result = {
134
+ "query": query,
135
+ "search_results": search_results,
136
+ "response_content": response_content,
137
+ "model": self.model,
138
+ "search_strategy": self.search_strategy
139
+ }
140
+
141
+ # Cache the result if enabled
142
+ if self.enable_cache and self.cache:
143
+ self.cache.save(query, final_result, expire_hours=self.cache_expire_hours)
144
+
145
+ # Post-process and set context
146
+ result = self.post_process(final_result)
147
+ if self.return_only_content:
148
+ self.context.dashscope_search_result = result["response_content"]
149
+ else:
150
+ self.context.dashscope_search_result = result
151
+
152
+ return
153
+
154
+ except Exception as e:
155
+ logger.warning(f"Dashscope search attempt {attempt + 1} failed for query='{query}': {e}")
156
+ if attempt < self.max_retries - 1:
157
+ time.sleep(attempt + 1) # Exponential backoff
158
+ else:
159
+ logger.error(f"All {self.max_retries} attempts failed for Dashscope search")
160
+
161
+ self.context.dashscope_search_result = "dashscope_search failed"
162
+
163
+
164
+ def main():
165
+ from flowllm.utils.common_utils import load_env
166
+
167
+ load_env()
168
+
169
+ C.set_default_service_config().init_by_service_config()
170
+
171
+ op = DashscopeSearchOp(enable_print=True, enable_cache=False)
172
+
173
+ context = FlowContext(query="杭州明天天气")
174
+ op(context=context)
175
+ print(context.dashscope_search_result)
176
+
177
+
178
+ if __name__ == "__main__":
179
+ main()
@@ -0,0 +1,13 @@
1
+ role_prompt: |
2
+ # user's question
3
+ {query}
4
+
5
+ # task
6
+ Extract the original content related to the user's query directly from the context, maintain accuracy, and avoid excessive processing.
7
+
8
+ role_prompt_zh: |
9
+ # 用户问题
10
+ {query}
11
+
12
+ # task
13
+ 直接从上下文中提取与用户问题相关的原始内容,保持准确性,避免过度处理。