flowllm 0.1.1__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 (87) hide show
  1. flowllm/__init__.py +15 -6
  2. flowllm/app.py +4 -14
  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_config.yaml → default.yaml} +3 -8
  10. flowllm/config/empty.yaml +37 -0
  11. flowllm/config/pydantic_config_parser.py +17 -17
  12. flowllm/context/base_context.py +27 -7
  13. flowllm/context/flow_context.py +6 -18
  14. flowllm/context/registry.py +5 -1
  15. flowllm/context/service_context.py +81 -37
  16. flowllm/embedding_model/__init__.py +1 -1
  17. flowllm/embedding_model/base_embedding_model.py +91 -0
  18. flowllm/embedding_model/openai_compatible_embedding_model.py +63 -5
  19. flowllm/flow/__init__.py +1 -0
  20. flowllm/flow/base_flow.py +72 -0
  21. flowllm/flow/base_tool_flow.py +15 -0
  22. flowllm/flow/gallery/__init__.py +8 -0
  23. flowllm/flow/gallery/cmd_flow.py +11 -0
  24. flowllm/flow/gallery/code_tool_flow.py +30 -0
  25. flowllm/flow/gallery/dashscope_search_tool_flow.py +34 -0
  26. flowllm/flow/gallery/deepsearch_tool_flow.py +39 -0
  27. flowllm/flow/gallery/expression_tool_flow.py +18 -0
  28. flowllm/flow/gallery/mock_tool_flow.py +67 -0
  29. flowllm/flow/gallery/tavily_search_tool_flow.py +30 -0
  30. flowllm/flow/gallery/terminate_tool_flow.py +30 -0
  31. flowllm/flow/parser/__init__.py +0 -0
  32. flowllm/{flow_engine/simple_flow_engine.py → flow/parser/expression_parser.py} +25 -67
  33. flowllm/llm/__init__.py +2 -1
  34. flowllm/llm/base_llm.py +94 -4
  35. flowllm/llm/litellm_llm.py +455 -0
  36. flowllm/llm/openai_compatible_llm.py +205 -5
  37. flowllm/op/__init__.py +11 -3
  38. flowllm/op/agent/__init__.py +0 -0
  39. flowllm/op/agent/react_op.py +83 -0
  40. flowllm/op/agent/react_prompt.yaml +28 -0
  41. flowllm/op/akshare/__init__.py +3 -0
  42. flowllm/op/akshare/get_ak_a_code_op.py +14 -22
  43. flowllm/op/akshare/get_ak_a_info_op.py +17 -20
  44. flowllm/op/{llm_base_op.py → base_llm_op.py} +6 -5
  45. flowllm/op/base_op.py +14 -35
  46. flowllm/op/base_ray_op.py +313 -0
  47. flowllm/op/code/__init__.py +1 -0
  48. flowllm/op/code/execute_code_op.py +42 -0
  49. flowllm/op/gallery/__init__.py +2 -0
  50. flowllm/op/{mock_op.py → gallery/mock_op.py} +4 -4
  51. flowllm/op/gallery/terminate_op.py +29 -0
  52. flowllm/op/parallel_op.py +2 -9
  53. flowllm/op/search/__init__.py +3 -0
  54. flowllm/op/search/dashscope_deep_research_op.py +260 -0
  55. flowllm/op/search/dashscope_search_op.py +179 -0
  56. flowllm/op/search/dashscope_search_prompt.yaml +13 -0
  57. flowllm/op/search/tavily_search_op.py +102 -0
  58. flowllm/op/sequential_op.py +1 -9
  59. flowllm/schema/flow_request.py +12 -0
  60. flowllm/schema/service_config.py +12 -16
  61. flowllm/schema/tool_call.py +13 -5
  62. flowllm/schema/vector_node.py +1 -0
  63. flowllm/service/__init__.py +3 -2
  64. flowllm/service/base_service.py +50 -41
  65. flowllm/service/cmd_service.py +15 -0
  66. flowllm/service/http_service.py +34 -42
  67. flowllm/service/mcp_service.py +13 -11
  68. flowllm/storage/cache/__init__.py +1 -0
  69. flowllm/storage/cache/cache_data_handler.py +104 -0
  70. flowllm/{utils/dataframe_cache.py → storage/cache/data_cache.py} +136 -92
  71. flowllm/storage/vector_store/__init__.py +3 -3
  72. flowllm/storage/vector_store/es_vector_store.py +1 -2
  73. flowllm/storage/vector_store/local_vector_store.py +0 -1
  74. flowllm/utils/common_utils.py +9 -21
  75. flowllm/utils/fetch_url.py +16 -12
  76. flowllm/utils/llm_utils.py +28 -0
  77. flowllm/utils/ridge_v2.py +54 -0
  78. {flowllm-0.1.1.dist-info → flowllm-0.1.2.dist-info}/METADATA +43 -390
  79. flowllm-0.1.2.dist-info/RECORD +99 -0
  80. flowllm-0.1.2.dist-info/entry_points.txt +2 -0
  81. flowllm/flow_engine/__init__.py +0 -1
  82. flowllm/flow_engine/base_flow_engine.py +0 -34
  83. flowllm-0.1.1.dist-info/RECORD +0 -62
  84. flowllm-0.1.1.dist-info/entry_points.txt +0 -4
  85. {flowllm-0.1.1.dist-info → flowllm-0.1.2.dist-info}/WHEEL +0 -0
  86. {flowllm-0.1.1.dist-info → flowllm-0.1.2.dist-info}/licenses/LICENSE +0 -0
  87. {flowllm-0.1.1.dist-info → flowllm-0.1.2.dist-info}/top_level.txt +0 -0
@@ -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
+ 直接从上下文中提取与用户问题相关的原始内容,保持准确性,避免过度处理。
@@ -0,0 +1,102 @@
1
+ import json
2
+ import os
3
+ import time
4
+ from typing import Literal
5
+
6
+ from loguru import logger
7
+ from tavily import TavilyClient
8
+
9
+ from flowllm.context.flow_context import FlowContext
10
+ from flowllm.context.service_context import C
11
+ from flowllm.op.base_op import BaseOp
12
+ from flowllm.storage.cache.data_cache import DataCache
13
+
14
+
15
+ @C.register_op()
16
+ class TavilySearchOp(BaseOp):
17
+ def __init__(self,
18
+ enable_print: bool = True,
19
+ enable_cache: bool = False,
20
+ cache_path: str = "./web_search_cache",
21
+ cache_expire_hours: float = 0.1,
22
+ topic: Literal["general", "news", "finance"] = "general",
23
+ max_retries: int = 3,
24
+ return_only_content: bool = True,
25
+ **kwargs):
26
+ super().__init__(**kwargs)
27
+
28
+ self.enable_print = enable_print
29
+ self.enable_cache = enable_cache
30
+ self.cache_expire_hours = cache_expire_hours
31
+ self.topic = topic
32
+ self.max_retries = max_retries
33
+ self.return_only_content = return_only_content
34
+
35
+ # Initialize DataCache if caching is enabled
36
+ self.cache = DataCache(cache_path) if self.enable_cache else None
37
+ self._client = TavilyClient(api_key=os.getenv("FLOW_TAVILY_API_KEY", ""))
38
+
39
+ def post_process(self, response):
40
+ if self.enable_print:
41
+ logger.info("response=\n" + json.dumps(response, indent=2, ensure_ascii=False))
42
+
43
+ return response
44
+
45
+ def execute(self):
46
+ # Get query from context
47
+ query: str = self.context.query
48
+
49
+ # Check cache first
50
+ if self.enable_cache and self.cache:
51
+ cached_result = self.cache.load(query)
52
+ if cached_result:
53
+ final_result = self.post_process(cached_result)
54
+ if self.return_only_content:
55
+ self.context.tavily_search_result = json.dumps(final_result, ensure_ascii=False, indent=2)
56
+ else:
57
+ self.context.tavily_search_result = final_result
58
+ return
59
+
60
+ for i in range(self.max_retries):
61
+ try:
62
+ response = self._client.search(query=query, topic=self.topic)
63
+ url_info_dict = {item["url"]: item for item in response["results"]}
64
+ response_extract = self._client.extract(urls=[item["url"] for item in response["results"]],
65
+ format="text")
66
+
67
+ final_result = {}
68
+ for item in response_extract["results"]:
69
+ url = item["url"]
70
+ final_result[url] = url_info_dict[url]
71
+ final_result[url]["raw_content"] = item["raw_content"]
72
+
73
+ # Cache the result if enabled
74
+ if self.enable_cache and self.cache:
75
+ self.cache.save(query, final_result, expire_hours=self.cache_expire_hours)
76
+
77
+ final_result = self.post_process(final_result)
78
+
79
+ if self.return_only_content:
80
+ self.context.tavily_search_result = json.dumps(final_result, ensure_ascii=False, indent=2)
81
+ else:
82
+ self.context.tavily_search_result = final_result
83
+ return
84
+
85
+ except Exception as e:
86
+ logger.exception(f"tavily search with query={query} encounter error with e={e.args}")
87
+ time.sleep(i + 1)
88
+
89
+ self.context.tavily_search_result = "tavily search failed!"
90
+
91
+
92
+ if __name__ == "__main__":
93
+ from flowllm.utils.common_utils import load_env
94
+
95
+ load_env()
96
+
97
+ C.set_default_service_config().init_by_service_config()
98
+
99
+ op = TavilySearchOp(enable_cache=True)
100
+ context = FlowContext(query="A股医药为什么一直涨")
101
+ op(context=context)
102
+ print(context.tavily_search_result)
@@ -4,22 +4,14 @@ from flowllm.op.base_op import BaseOp
4
4
 
5
5
 
6
6
  class SequentialOp(BaseOp):
7
- """Container class for sequential operation execution
8
-
9
- Executes multiple operations in sequence, where the output of the previous operation
10
- becomes the input of the next operation.
11
- Supports chaining: op1 >> op2 >> op3
12
- """
13
7
 
14
8
  def __init__(self, ops: List[BaseOp], **kwargs):
15
9
  super().__init__(**kwargs)
16
10
  self.ops = ops
17
11
 
18
12
  def execute(self):
19
- result = None
20
13
  for op in self.ops:
21
- result = op.execute()
22
- return result
14
+ op.__call__(self.context)
23
15
 
24
16
  def __rshift__(self, op: BaseOp):
25
17
  if isinstance(op, SequentialOp):
@@ -0,0 +1,12 @@
1
+ from typing import List
2
+
3
+ from pydantic import Field, BaseModel
4
+
5
+ from flowllm.schema.message import Message
6
+
7
+
8
+ class FlowRequest(BaseModel, extra="allow"):
9
+ query: str = Field(default="")
10
+ messages: List[Message] = Field(default_factory=list)
11
+ workspace_id: str = Field(default="")
12
+ metadata: dict = Field(default_factory=dict)
@@ -14,31 +14,27 @@ class MCPConfig(BaseModel):
14
14
  class HttpConfig(BaseModel):
15
15
  host: str = Field(default="0.0.0.0")
16
16
  port: int = Field(default=8001)
17
- timeout_keep_alive: int = Field(default=600)
17
+ timeout_keep_alive: int = Field(default=3600)
18
18
  limit_concurrency: int = Field(default=64)
19
19
 
20
20
 
21
- class FlowConfig(ToolCall):
22
- flow_content: str = Field(default="")
23
-
24
- def set_name(self, name: str):
25
- self.name = name
26
- return self
27
-
28
-
29
- class FlowEngineConfig(BaseModel):
30
- backend: str = Field(default="")
21
+ class CmdConfig(BaseModel):
22
+ flow: str = Field(default="")
31
23
  params: dict = Field(default_factory=dict)
32
24
 
33
25
 
26
+ class FlowConfig(ToolCall):
27
+ flow_content: str = Field(default="")
28
+ service_type: str = Field(default="all", description="all/http/mcp/cmd")
29
+
34
30
  class OpConfig(BaseModel):
35
31
  backend: str = Field(default="")
36
32
  language: str = Field(default="")
37
33
  raise_exception: bool = Field(default=True)
38
34
  prompt_path: str = Field(default="")
39
- llm: str = Field(default="default")
40
- embedding_model: str = Field(default="default")
41
- vector_store: str = Field(default="default")
35
+ llm: str = Field(default="")
36
+ embedding_model: str = Field(default="")
37
+ vector_store: str = Field(default="")
42
38
  params: dict = Field(default_factory=dict)
43
39
 
44
40
 
@@ -64,11 +60,11 @@ class ServiceConfig(BaseModel):
64
60
  backend: str = Field(default="")
65
61
  language: str = Field(default="")
66
62
  thread_pool_max_workers: int = Field(default=16)
67
- ray_max_workers: int = Field(default=8)
63
+ ray_max_workers: int = Field(default=1)
68
64
 
65
+ cmd: CmdConfig = Field(default_factory=CmdConfig)
69
66
  mcp: MCPConfig = Field(default_factory=MCPConfig)
70
67
  http: HttpConfig = Field(default_factory=HttpConfig)
71
- flow_engine: FlowEngineConfig = Field(default_factory=FlowEngineConfig)
72
68
  flow: Dict[str, FlowConfig] = Field(default_factory=dict)
73
69
  op: Dict[str, OpConfig] = Field(default_factory=dict)
74
70
  llm: Dict[str, LLMConfig] = Field(default_factory=dict)
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Dict
2
+ from typing import Dict, List
3
3
 
4
4
  from pydantic import BaseModel, Field
5
5
 
@@ -8,7 +8,18 @@ class ParamAttrs(BaseModel):
8
8
  type: str = Field(default="str", description="tool parameter type")
9
9
  description: str = Field(default="", description="tool parameter description")
10
10
  required: bool = Field(default=True, description="tool parameter required")
11
+ enum: List[str] | None = Field(default=None, description="tool parameter enum")
11
12
 
13
+ def simple_dump(self) -> dict:
14
+ result: dict = {
15
+ "type": self.type,
16
+ "description": self.description,
17
+ }
18
+
19
+ if self.enum:
20
+ result["enum"] = self.enum
21
+
22
+ return result
12
23
 
13
24
  class ToolCall(BaseModel):
14
25
  """
@@ -56,10 +67,7 @@ class ToolCall(BaseModel):
56
67
  def simple_input_dump(self, version: str = "v1") -> dict:
57
68
  if version == "v1":
58
69
  required_list = [name for name, tool_param in self.input_schema.items() if tool_param.required]
59
- properties = {name: {
60
- "type": tool_param.type,
61
- "description": tool_param.description
62
- } for name, tool_param in self.input_schema.items()}
70
+ properties = {name: tool_param.simple_dump() for name, tool_param in self.input_schema.items()}
63
71
 
64
72
  return {
65
73
  "type": self.type,