flowllm 0.1.3__py3-none-any.whl → 0.1.5__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 (81) hide show
  1. flowllm/__init__.py +4 -3
  2. flowllm/app.py +1 -1
  3. flowllm/config/base.yaml +75 -0
  4. flowllm/config/fin_supply.yaml +39 -0
  5. flowllm/config/pydantic_config_parser.py +16 -1
  6. flowllm/context/__init__.py +2 -0
  7. flowllm/context/base_context.py +10 -20
  8. flowllm/context/flow_context.py +45 -2
  9. flowllm/context/service_context.py +69 -10
  10. flowllm/embedding_model/openai_compatible_embedding_model.py +1 -2
  11. flowllm/enumeration/chunk_enum.py +1 -0
  12. flowllm/flow/__init__.py +9 -0
  13. flowllm/flow/base_flow.py +44 -13
  14. flowllm/flow/expression/__init__.py +1 -0
  15. flowllm/flow/{parser → expression}/expression_parser.py +5 -2
  16. flowllm/flow/expression/expression_tool_flow.py +25 -0
  17. flowllm/flow/gallery/__init__.py +1 -8
  18. flowllm/flow/gallery/mock_tool_flow.py +46 -28
  19. flowllm/flow/tool_op_flow.py +97 -0
  20. flowllm/llm/base_llm.py +0 -2
  21. flowllm/op/__init__.py +3 -4
  22. flowllm/op/akshare/get_ak_a_code_op.py +1 -1
  23. flowllm/op/akshare/get_ak_a_info_op.py +1 -1
  24. flowllm/op/base_op.py +232 -16
  25. flowllm/op/base_tool_op.py +47 -0
  26. flowllm/op/gallery/__init__.py +0 -1
  27. flowllm/op/gallery/mock_op.py +13 -7
  28. flowllm/op/llm/__init__.py +3 -0
  29. flowllm/op/{agent/react_v2_op.py → llm/react_llm_op.py} +43 -24
  30. flowllm/op/llm/simple_llm_op.py +48 -0
  31. flowllm/op/llm/stream_llm_op.py +61 -0
  32. flowllm/op/mcp/__init__.py +2 -0
  33. flowllm/op/mcp/ant_op.py +42 -0
  34. flowllm/op/mcp/base_sse_mcp_op.py +28 -0
  35. flowllm/op/parallel_op.py +5 -1
  36. flowllm/op/search/__init__.py +1 -2
  37. flowllm/op/search/dashscope_search_op.py +73 -128
  38. flowllm/op/search/tavily_search_op.py +64 -82
  39. flowllm/op/sequential_op.py +4 -0
  40. flowllm/schema/flow_stream_chunk.py +11 -0
  41. flowllm/schema/service_config.py +8 -3
  42. flowllm/schema/tool_call.py +46 -1
  43. flowllm/service/__init__.py +0 -1
  44. flowllm/service/base_service.py +31 -14
  45. flowllm/service/http_service.py +45 -36
  46. flowllm/service/mcp_service.py +17 -23
  47. flowllm/storage/vector_store/__init__.py +1 -0
  48. flowllm/storage/vector_store/base_vector_store.py +99 -15
  49. flowllm/storage/vector_store/chroma_vector_store.py +250 -8
  50. flowllm/storage/vector_store/es_vector_store.py +288 -32
  51. flowllm/storage/vector_store/local_vector_store.py +206 -9
  52. flowllm/storage/vector_store/memory_vector_store.py +509 -0
  53. flowllm/utils/common_utils.py +54 -0
  54. flowllm/utils/miner_u_pdf_processor.py +726 -0
  55. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/METADATA +7 -6
  56. flowllm-0.1.5.dist-info/RECORD +98 -0
  57. flowllm/config/default.yaml +0 -77
  58. flowllm/config/empty.yaml +0 -37
  59. flowllm/flow/gallery/cmd_flow.py +0 -11
  60. flowllm/flow/gallery/code_tool_flow.py +0 -30
  61. flowllm/flow/gallery/dashscope_search_tool_flow.py +0 -34
  62. flowllm/flow/gallery/deepsearch_tool_flow.py +0 -39
  63. flowllm/flow/gallery/expression_tool_flow.py +0 -18
  64. flowllm/flow/gallery/tavily_search_tool_flow.py +0 -30
  65. flowllm/flow/gallery/terminate_tool_flow.py +0 -30
  66. flowllm/flow/parser/__init__.py +0 -0
  67. flowllm/op/agent/__init__.py +0 -1
  68. flowllm/op/agent/react_v1_op.py +0 -109
  69. flowllm/op/agent/react_v1_prompt.yaml +0 -54
  70. flowllm/op/base_ray_op.py +0 -313
  71. flowllm/op/code/__init__.py +0 -1
  72. flowllm/op/code/execute_code_op.py +0 -42
  73. flowllm/op/gallery/terminate_op.py +0 -29
  74. flowllm/op/search/dashscope_deep_research_op.py +0 -267
  75. flowllm/service/cmd_service.py +0 -15
  76. flowllm-0.1.3.dist-info/RECORD +0 -102
  77. /flowllm/op/{agent/react_v2_prompt.yaml → llm/react_llm_prompt.yaml} +0 -0
  78. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/WHEEL +0 -0
  79. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/entry_points.txt +0 -0
  80. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/licenses/LICENSE +0 -0
  81. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/top_level.txt +0 -0
@@ -7,56 +7,74 @@ from flowllm.schema.tool_call import ToolCall, ParamAttrs
7
7
  @C.register_tool_flow()
8
8
  class MockToolFlow(BaseToolFlow):
9
9
 
10
+ def __init__(self,
11
+ use_async: bool = False,
12
+ stream: bool = False,
13
+ service_type: str = "http+mcp",
14
+ **kwargs):
15
+ super().__init__(use_async=use_async, stream=stream, service_type=service_type, **kwargs)
16
+
10
17
  def build_flow(self):
11
18
  mock1_op = Mock1Op()
12
19
  mock2_op = Mock2Op()
13
20
  mock3_op = Mock3Op()
14
- mock4_op = Mock4Op()
15
- mock5_op = Mock5Op()
16
- mock6_op = Mock6Op()
17
21
 
18
- op = mock1_op >> ((mock4_op >> mock2_op) | mock5_op) >> (mock3_op | mock6_op)
22
+ op = mock1_op >> ((mock2_op >> mock3_op) | mock1_op) >> (mock2_op | mock3_op)
19
23
  return op
20
24
 
21
25
  def build_tool_call(self) -> ToolCall:
22
26
  return ToolCall(**{
23
- "index": 0,
24
- "id": "call_mock_tool_12345",
25
27
  "type": "function",
26
- "name": "mock_data_processor",
28
+ "name": "mock_data",
27
29
  "description": "A mock tool that processes data through multiple operations and returns structured results",
28
30
  "input_schema": {
29
- "input_data": ParamAttrs(
30
- type="string",
31
+ "a": ParamAttrs(
32
+ type="str",
31
33
  description="The input data to be processed",
32
34
  required=True
33
35
  ),
34
- "processing_mode": ParamAttrs(
36
+ "b": ParamAttrs(
35
37
  type="string",
36
38
  description="Processing mode: basic, advanced, or expert",
37
39
  required=False
38
40
  ),
39
- "output_format": ParamAttrs(
40
- type="string",
41
- description="Output format: json, xml, or plain",
42
- required=False
43
- )
44
- },
45
- "output_schema": {
46
- "result": ParamAttrs(
47
- type="object",
48
- description="Processed result data",
41
+ }
42
+ })
43
+
44
+
45
+ @C.register_tool_flow()
46
+ class MockAsyncToolFlow(BaseToolFlow):
47
+
48
+ def __init__(self,
49
+ use_async: bool = True,
50
+ stream: bool = False,
51
+ service_type: str = "http+mcp",
52
+ **kwargs):
53
+ super().__init__(use_async=use_async, stream=stream, service_type=service_type, **kwargs)
54
+
55
+ def build_flow(self):
56
+ mock4_op = Mock4Op()
57
+ mock5_op = Mock5Op()
58
+ mock6_op = Mock6Op()
59
+
60
+ op = mock4_op >> ((mock5_op >> mock6_op) | mock4_op) >> (mock5_op | mock6_op)
61
+ return op
62
+
63
+ def build_tool_call(self) -> ToolCall:
64
+ return ToolCall(**{
65
+ "type": "function",
66
+ "name": "mock_data",
67
+ "description": "A mock tool that processes data through multiple operations and returns structured results",
68
+ "input_schema": {
69
+ "a": ParamAttrs(
70
+ type="str",
71
+ description="The input data to be processed",
49
72
  required=True
50
73
  ),
51
- "status": ParamAttrs(
74
+ "b": ParamAttrs(
52
75
  type="string",
53
- description="Processing status: success, warning, or error",
54
- required=True
55
- ),
56
- "metadata": ParamAttrs(
57
- type="object",
58
- description="Additional metadata about the processing",
76
+ description="Processing mode: basic, advanced, or expert",
59
77
  required=False
60
- )
78
+ ),
61
79
  }
62
80
  })
@@ -0,0 +1,97 @@
1
+ from flowllm.context.service_context import C
2
+ from flowllm.flow.base_tool_flow import BaseToolFlow
3
+ from flowllm.op.llm import SimpleLLMOp, ReactLLMOp, StreamLLMOp
4
+ from flowllm.op.search import DashscopeSearchOp
5
+ from flowllm.op.search import TavilySearchOp
6
+ from flowllm.schema.tool_call import ToolCall
7
+
8
+
9
+ @C.register_tool_flow()
10
+ class TavilySearchToolFlow(BaseToolFlow):
11
+
12
+ def __init__(self,
13
+ use_async: bool = True,
14
+ stream: bool = False,
15
+ service_type: str = "http+mcp",
16
+ **kwargs):
17
+ super().__init__(use_async=use_async, stream=stream, service_type=service_type, **kwargs)
18
+
19
+ def build_flow(self):
20
+ return TavilySearchOp()
21
+
22
+ def build_tool_call(self) -> ToolCall:
23
+ return self.flow_op.tool_call
24
+
25
+ def after_flow(self, context):
26
+ context.response.answer = context.tavily_search_result
27
+
28
+
29
+ @C.register_tool_flow()
30
+ class DashscopeSearchToolFlow(BaseToolFlow):
31
+
32
+ def __init__(self,
33
+ use_async: bool = True,
34
+ stream: bool = False,
35
+ service_type: str = "http+mcp",
36
+ **kwargs):
37
+ super().__init__(use_async=use_async, stream=stream, service_type=service_type, **kwargs)
38
+
39
+ def build_flow(self):
40
+ return DashscopeSearchOp()
41
+
42
+ def build_tool_call(self) -> ToolCall:
43
+ return self.flow_op.tool_call
44
+
45
+ def after_flow(self, context):
46
+ context.response.answer = context.dashscope_search_result
47
+
48
+
49
+ @C.register_tool_flow(name="stream_llm_tool_flow")
50
+ class StreamLLMToolFlow(BaseToolFlow):
51
+
52
+ def __init__(self,
53
+ use_async: bool = True,
54
+ stream: bool = True,
55
+ service_type: str = "http",
56
+ **kwargs):
57
+ super().__init__(use_async=use_async, stream=stream, service_type=service_type, **kwargs)
58
+
59
+ def build_flow(self):
60
+ return StreamLLMOp()
61
+
62
+ def build_tool_call(self) -> ToolCall:
63
+ return self.flow_op.tool_call
64
+
65
+
66
+ @C.register_tool_flow(name="simple_llm_tool_flow")
67
+ class SimpleLLMToolFlow(BaseToolFlow):
68
+
69
+ def __init__(self,
70
+ use_async: bool = True,
71
+ stream: bool = False,
72
+ service_type: str = "http",
73
+ **kwargs):
74
+ super().__init__(use_async=use_async, stream=stream, service_type=service_type, **kwargs)
75
+
76
+ def build_flow(self):
77
+ return SimpleLLMOp()
78
+
79
+ def build_tool_call(self) -> ToolCall:
80
+ return self.flow_op.tool_call
81
+
82
+
83
+ @C.register_tool_flow(name="react_llm_tool_flow")
84
+ class ReactLLMToolFlow(BaseToolFlow):
85
+
86
+ def __init__(self,
87
+ use_async: bool = True,
88
+ stream: bool = False,
89
+ service_type: str = "http",
90
+ **kwargs):
91
+ super().__init__(use_async=use_async, stream=stream, service_type=service_type, **kwargs)
92
+
93
+ def build_flow(self):
94
+ return ReactLLMOp()
95
+
96
+ def build_tool_call(self) -> ToolCall:
97
+ return self.flow_op.tool_call
flowllm/llm/base_llm.py CHANGED
@@ -155,10 +155,8 @@ class BaseLLM(BaseModel, ABC):
155
155
  except Exception as e:
156
156
  logger.exception(f"chat with model={self.model_name} encounter error with e={e.args}")
157
157
 
158
- # Exponential backoff: wait longer after each failure
159
158
  time.sleep(1 + i)
160
159
 
161
- # Handle final retry failure
162
160
  if i == self.max_retries - 1:
163
161
  if self.raise_exception:
164
162
  raise e
flowllm/op/__init__.py CHANGED
@@ -1,12 +1,11 @@
1
1
  from .base_llm_op import BaseLLMOp
2
2
  from .base_op import BaseOp
3
- from .base_ray_op import BaseRayOp
3
+ from .base_tool_op import BaseToolOp
4
4
 
5
5
  """
6
- op folder
7
6
  """
8
7
  from . import akshare
9
- from . import code
10
8
  from . import gallery
9
+ from . import llm
11
10
  from . import search
12
- from . import agent
11
+ from . import mcp
@@ -101,7 +101,7 @@ class GetAkACodeOp(BaseLLMOp):
101
101
 
102
102
 
103
103
  if __name__ == "__main__":
104
- C.set_default_service_config().init_by_service_config()
104
+ C.set_service_config().init_by_service_config()
105
105
  context = FlowContext(query="茅台和五粮现在价格多少?")
106
106
 
107
107
  op = GetAkACodeOp()
@@ -124,7 +124,7 @@ class MergeAkAInfoOp(BaseOp):
124
124
 
125
125
 
126
126
  if __name__ == "__main__":
127
- C.set_default_service_config().init_by_service_config()
127
+ C.set_service_config().init_by_service_config()
128
128
 
129
129
  code_infos = {"000858": {}, "600519": {}}
130
130
  context = FlowContext(code_infos=code_infos, query="茅台和五粮现在价格多少?")
flowllm/op/base_op.py CHANGED
@@ -1,6 +1,8 @@
1
- from abc import abstractmethod, ABC
1
+ import asyncio
2
+ import copy
3
+ from abc import ABC
2
4
  from concurrent.futures import Future
3
- from typing import List
5
+ from typing import List, Any, Optional, Callable
4
6
 
5
7
  from loguru import logger
6
8
  from tqdm import tqdm
@@ -13,41 +15,123 @@ from flowllm.utils.timer import Timer
13
15
 
14
16
  class BaseOp(ABC):
15
17
 
18
+ def __new__(cls, *args, **kwargs):
19
+ instance = super().__new__(cls)
20
+ instance._init_args = copy.copy(args)
21
+ instance._init_kwargs = copy.copy(kwargs)
22
+ return instance
23
+
16
24
  def __init__(self,
17
25
  name: str = "",
26
+ max_retries: int = 1,
18
27
  raise_exception: bool = True,
19
28
  enable_multithread: bool = True,
20
29
  **kwargs):
21
30
  super().__init__()
22
31
 
23
32
  self.name: str = name or camel_to_snake(self.__class__.__name__)
33
+ self.max_retries: int = max_retries
24
34
  self.raise_exception: bool = raise_exception
25
35
  self.enable_multithread: bool = enable_multithread
26
36
  self.op_params: dict = kwargs
27
37
 
28
38
  self.task_list: List[Future] = []
29
- self.ray_task_list: List = [] # Ray ObjectRef list
39
+ self.async_task_list: List = []
40
+ self.ray_task_list: List = []
30
41
  self.timer = Timer(name=self.name)
31
42
  self.context: FlowContext | None = None
43
+ self.sub_op: Optional["BaseOp"] = None
44
+
45
+ def before_execute(self):
46
+ ...
47
+
48
+ def after_execute(self):
49
+ ...
32
50
 
33
- @abstractmethod
34
51
  def execute(self):
35
52
  ...
36
53
 
54
+ def default_execute(self):
55
+ ...
56
+
57
+ async def async_execute(self):
58
+ ...
59
+
37
60
  def __call__(self, context: FlowContext = None):
38
61
  self.context = context
39
62
  with self.timer:
40
- if self.raise_exception:
63
+ if self.max_retries == 1 and self.raise_exception:
64
+ self.before_execute()
41
65
  self.execute()
66
+ self.after_execute()
42
67
 
43
68
  else:
69
+ for i in range(self.max_retries):
70
+ try:
71
+ self.before_execute()
72
+ self.execute()
73
+ self.after_execute()
74
+
75
+ except Exception as e:
76
+ logger.exception(f"op={self.name} execute failed, error={e.args}")
77
+
78
+ if i == self.max_retries - 1:
79
+ if self.raise_exception:
80
+ raise e
81
+ else:
82
+ self.default_execute()
83
+
84
+ if self.context is not None and self.context.response is not None:
85
+ return self.context.response
86
+ return None
87
+
88
+ async def async_call(self, context: FlowContext = None) -> Any:
89
+ self.context = context
90
+ with self.timer:
91
+ if self.max_retries == 1 and self.raise_exception:
92
+ self.before_execute()
93
+ await self.async_execute()
94
+ self.after_execute()
44
95
 
45
- try:
46
- self.execute()
47
- except Exception as e:
48
- logger.exception(f"op={self.name} execute failed, error={e.args}")
96
+ else:
97
+ for i in range(self.max_retries):
98
+ try:
99
+ self.before_execute()
100
+ await self.async_execute()
101
+ self.after_execute()
102
+
103
+ except Exception as e:
104
+ logger.exception(f"op={self.name} async execute failed, error={e.args}")
105
+
106
+ if i == self.max_retries:
107
+ if self.raise_exception:
108
+ raise e
109
+ else:
110
+ self.default_execute()
111
+
112
+ if self.context is not None and self.context.response is not None:
113
+ return self.context.response
114
+ return None
115
+
116
+ def submit_async_task(self, fn: Callable, *args, **kwargs):
117
+ if asyncio.iscoroutinefunction(fn):
118
+ task = asyncio.create_task(fn(*args, **kwargs))
119
+ self.async_task_list.append(task)
120
+ else:
121
+ logger.warning("submit_async_task failed, fn is not a coroutine function!")
122
+
123
+ async def join_async_task(self):
124
+ result = []
125
+ for task in self.async_task_list:
126
+ t_result = await task
127
+ if t_result:
128
+ if isinstance(t_result, list):
129
+ result.extend(t_result)
130
+ else:
131
+ result.append(t_result)
49
132
 
50
- return self.context.response if self.context else None
133
+ self.async_task_list.clear()
134
+ return result
51
135
 
52
136
  def submit_task(self, fn, *args, **kwargs):
53
137
  if self.enable_multithread:
@@ -92,6 +176,9 @@ class BaseOp(ABC):
92
176
  sequential_op.ops.append(op)
93
177
  return sequential_op
94
178
 
179
+ def __lshift__(self, op: "BaseOp"):
180
+ self.sub_op = op
181
+
95
182
  def __or__(self, op: "BaseOp"):
96
183
  from flowllm.op.parallel_op import ParallelOp
97
184
 
@@ -104,6 +191,9 @@ class BaseOp(ABC):
104
191
 
105
192
  return parallel_op
106
193
 
194
+ def copy(self) -> "BaseOp":
195
+ return self.__class__(*self._init_args, **self._init_kwargs)
196
+
107
197
 
108
198
  def run1():
109
199
  """Basic test"""
@@ -112,6 +202,9 @@ def run1():
112
202
  def execute(self):
113
203
  logger.info(f"op={self.name} execute")
114
204
 
205
+ async def async_execute(self):
206
+ logger.info(f"op={self.name} async_execute")
207
+
115
208
  mock_op = MockOp()
116
209
  mock_op()
117
210
 
@@ -129,6 +222,12 @@ def run2():
129
222
  logger.info(f"Executing {op_result}")
130
223
  return op_result
131
224
 
225
+ async def async_execute(self):
226
+ await asyncio.sleep(0.1)
227
+ op_result = f"{self.name}"
228
+ logger.info(f"Async executing {op_result}")
229
+ return op_result
230
+
132
231
  # Create service_context for parallel execution
133
232
  C["thread_pool"] = ThreadPoolExecutor(max_workers=4)
134
233
 
@@ -148,18 +247,135 @@ def run2():
148
247
  result = parallel()
149
248
  logger.info(f"Parallel result: {result}")
150
249
 
151
- logger.info("=== Testing mixed calls op1 >> (op2 | op3) >> op4 ===")
250
+
251
+ async def async_run1():
252
+ """Basic async test"""
253
+
254
+ class MockOp(BaseOp):
255
+ def execute(self):
256
+ logger.info(f"op={self.name} execute")
257
+
258
+ async def async_execute(self):
259
+ logger.info(f"op={self.name} async_execute")
260
+
261
+ mock_op = MockOp()
262
+ await mock_op.async_call()
263
+
264
+
265
+ async def async_run2():
266
+ """Test async operator overloading functionality"""
267
+ from concurrent.futures import ThreadPoolExecutor
268
+ import time
269
+
270
+ class TestOp(BaseOp):
271
+
272
+ def execute(self):
273
+ time.sleep(0.1)
274
+ op_result = f"{self.name}"
275
+ logger.info(f"Executing {op_result}")
276
+ return op_result
277
+
278
+ async def async_execute(self):
279
+ await asyncio.sleep(0.1)
280
+ op_result = f"{self.name}"
281
+ logger.info(f"Async executing {op_result}")
282
+ return op_result
283
+
284
+ # Create service_context for parallel execution
285
+ C["thread_pool"] = ThreadPoolExecutor(max_workers=4)
286
+
287
+ # Create test operations
288
+ op1 = TestOp("op1")
289
+ op2 = TestOp("op2")
290
+ op3 = TestOp("op3")
291
+ op4 = TestOp("op4")
292
+
293
+ logger.info("=== Testing async sequential execution op1 >> op2 ===")
294
+ sequential = op1 >> op2
295
+ result = await sequential.async_call()
296
+ logger.info(f"Async sequential result: {result}")
297
+
298
+ logger.info("=== Testing async parallel execution op1 | op2 ===")
299
+ parallel = op1 | op2
300
+ result = await parallel.async_call()
301
+ logger.info(f"Async parallel result: {result}")
302
+
303
+ logger.info("=== Testing async mixed calls op1 >> (op2 | op3) >> op4 ===")
152
304
  mixed = op1 >> (op2 | op3) >> op4
153
- result = mixed()
154
- logger.info(f"Mixed result: {result}")
305
+ result = await mixed.async_call()
306
+ logger.info(f"Async mixed result: {result}")
155
307
 
156
- logger.info("=== Testing complex mixed calls op1 >> (op1 | (op2 >> op3)) >> op4 ===")
308
+ logger.info("=== Testing async complex mixed calls op1 >> (op1 | (op2 >> op3)) >> op4 ===")
157
309
  complex_mixed = op1 >> (op1 | (op2 >> op3)) >> op4
158
- result = complex_mixed()
159
- logger.info(f"Complex mixed result: {result}")
310
+ result = await complex_mixed.async_call()
311
+ logger.info(f"Async complex mixed result: {result}")
312
+
313
+
314
+ def test_copy():
315
+ class TestOp(BaseOp):
316
+ def __init__(self, name="", custom_param="default", **kwargs):
317
+ super().__init__(name=name, **kwargs)
318
+ self.custom_param = custom_param
319
+
320
+ def execute(self):
321
+ logger.info(f"TestOp {self.name} executing with custom_param={self.custom_param}")
322
+
323
+ class AdvancedOp(TestOp):
324
+ def __init__(self, name="", custom_param="default", advanced_param=42, **kwargs):
325
+ super().__init__(name=name, custom_param=custom_param, **kwargs)
326
+ self.advanced_param = advanced_param
327
+
328
+ def execute(self):
329
+ logger.info(
330
+ f"AdvancedOp {self.name} executing with custom_param={self.custom_param}, advanced_param={self.advanced_param}")
331
+
332
+ logger.info("=== Testing copy functionality ===")
333
+
334
+ original_op = TestOp(name="test_op", custom_param="custom_value", max_retries=3, enable_multithread=False)
335
+ copied_op = original_op.copy()
336
+
337
+ logger.info(
338
+ f"Original: name={original_op.name}, custom_param={original_op.custom_param}, max_retries={original_op.max_retries}")
339
+ logger.info(
340
+ f"Copied: name={copied_op.name}, custom_param={copied_op.custom_param}, max_retries={copied_op.max_retries}")
341
+ logger.info(f"Same object? {original_op is copied_op}")
342
+ logger.info(f"Same class? {type(original_op) == type(copied_op)}")
343
+
344
+ original_advanced = AdvancedOp(
345
+ name="advanced_op",
346
+ custom_param="advanced_custom",
347
+ advanced_param=100,
348
+ max_retries=5,
349
+ raise_exception=False
350
+ )
351
+ copied_advanced = original_advanced.copy()
352
+
353
+ logger.info(
354
+ f"Advanced Original: name={original_advanced.name}, custom_param={original_advanced.custom_param}, advanced_param={original_advanced.advanced_param}")
355
+ logger.info(
356
+ f"Advanced Copied: name={copied_advanced.name}, custom_param={copied_advanced.custom_param}, advanced_param={copied_advanced.advanced_param}")
357
+ logger.info(f"Advanced Same object? {original_advanced is copied_advanced}")
358
+ logger.info(f"Advanced Same class? {type(original_advanced) == type(copied_advanced)}")
359
+
360
+ copied_op.name = "modified_name"
361
+ logger.info(f"After modifying copy - Original name: {original_op.name}, Copied name: {copied_op.name}")
160
362
 
161
363
 
162
364
  if __name__ == "__main__":
163
365
  run1()
164
366
  print("\n" + "=" * 50 + "\n")
165
367
  run2()
368
+ print("\n" + "=" * 50 + "\n")
369
+ test_copy()
370
+
371
+ print("\n" + "=" * 50 + "\n")
372
+ print("Running async tests:")
373
+
374
+
375
+ async def main():
376
+ await async_run1()
377
+ print("\n" + "=" * 50 + "\n")
378
+ await async_run2()
379
+
380
+
381
+ asyncio.run(main())
@@ -0,0 +1,47 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from loguru import logger
4
+
5
+ from flowllm.op.base_llm_op import BaseLLMOp
6
+ from flowllm.schema.tool_call import ToolCall
7
+ from flowllm.storage.cache import DataCache
8
+
9
+
10
+ class BaseToolOp(BaseLLMOp, ABC):
11
+
12
+ def __init__(self,
13
+ enable_cache: bool = False,
14
+ cache_path: str = "cache",
15
+ cache_expire_hours: float = 0.1,
16
+ enable_print_output: bool = True,
17
+ **kwargs):
18
+ super().__init__(**kwargs)
19
+
20
+ self.enable_cache = enable_cache
21
+ self.cache_path: str = cache_path
22
+ self.cache_expire_hours: float = cache_expire_hours
23
+ self.enable_print_output: bool = enable_print_output
24
+ self._cache: DataCache | None = None
25
+
26
+ self.tool_call: ToolCall = self.build_tool_call()
27
+ self.input_dict: dict = {}
28
+ self.output_dict: dict = {}
29
+
30
+ @property
31
+ def cache(self):
32
+ if self.enable_cache and self._cache is None:
33
+ self._cache = DataCache(f"{self.cache_path}/{self.name}")
34
+ return self._cache
35
+
36
+ @abstractmethod
37
+ def build_tool_call(self) -> ToolCall:
38
+ ...
39
+
40
+ def before_execute(self):
41
+ for key in self.tool_call.input_schema.keys():
42
+ self.input_dict[key] = self.context.get(key)
43
+
44
+ def after_execute(self):
45
+ self.context.update(self.output_dict)
46
+ if self.enable_print_output:
47
+ logger.info(f"{self.name}.output_dict={self.output_dict}")
@@ -1,2 +1 @@
1
1
  from .mock_op import Mock1Op, Mock2Op, Mock3Op, Mock4Op, Mock5Op, Mock6Op
2
- from .terminate_op import TerminateOp
@@ -1,8 +1,9 @@
1
+ import asyncio
1
2
  import time
2
3
 
3
4
  from loguru import logger
4
5
 
5
- from flowllm.context.service_context import C
6
+ from flowllm.context import C
6
7
  from flowllm.op.base_llm_op import BaseLLMOp
7
8
 
8
9
 
@@ -10,8 +11,8 @@ from flowllm.op.base_llm_op import BaseLLMOp
10
11
  class Mock1Op(BaseLLMOp):
11
12
  def execute(self):
12
13
  time.sleep(1)
13
- a = self.context.a
14
- b = self.context.b
14
+ a = self.context.get("a", 1)
15
+ b = self.context.get("b", 2)
15
16
  logger.info(f"enter class={self.name}. a={a} b={b}")
16
17
 
17
18
  self.context.response.answer = f"{self.name} {a} {b} answer=47"
@@ -28,15 +29,20 @@ class Mock3Op(Mock1Op):
28
29
 
29
30
 
30
31
  @C.register_op()
31
- class Mock4Op(Mock1Op):
32
- ...
32
+ class Mock4Op(BaseLLMOp):
33
+ async def async_execute(self):
34
+ await asyncio.sleep(1)
35
+ a = self.context.get("a", 1)
36
+ b = self.context.get("b", 2)
37
+ logger.info(f"enter class={self.name}. a={a} b={b}")
38
+ self.context.response.answer = f"{self.name} {a} {b} answer=47"
33
39
 
34
40
 
35
41
  @C.register_op()
36
- class Mock5Op(Mock1Op):
42
+ class Mock5Op(Mock4Op):
37
43
  ...
38
44
 
39
45
 
40
46
  @C.register_op()
41
- class Mock6Op(Mock1Op):
47
+ class Mock6Op(Mock4Op):
42
48
  ...
@@ -0,0 +1,3 @@
1
+ from .react_llm_op import ReactLLMOp
2
+ from .simple_llm_op import SimpleLLMOp
3
+ from .stream_llm_op import StreamLLMOp