flowllm 0.1.2__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 (84) hide show
  1. flowllm/__init__.py +8 -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 +73 -12
  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 -11
  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 -33
  19. flowllm/flow/tool_op_flow.py +97 -0
  20. flowllm/llm/base_llm.py +0 -2
  21. flowllm/llm/litellm_llm.py +2 -1
  22. flowllm/op/__init__.py +3 -3
  23. flowllm/op/akshare/get_ak_a_code_op.py +1 -1
  24. flowllm/op/akshare/get_ak_a_info_op.py +1 -1
  25. flowllm/op/base_llm_op.py +3 -2
  26. flowllm/op/base_op.py +258 -25
  27. flowllm/op/base_tool_op.py +47 -0
  28. flowllm/op/gallery/__init__.py +0 -1
  29. flowllm/op/gallery/mock_op.py +13 -7
  30. flowllm/op/llm/__init__.py +3 -0
  31. flowllm/op/llm/react_llm_op.py +105 -0
  32. flowllm/op/{agent/react_prompt.yaml → llm/react_llm_prompt.yaml} +17 -10
  33. flowllm/op/llm/simple_llm_op.py +48 -0
  34. flowllm/op/llm/stream_llm_op.py +61 -0
  35. flowllm/op/mcp/__init__.py +2 -0
  36. flowllm/op/mcp/ant_op.py +42 -0
  37. flowllm/op/mcp/base_sse_mcp_op.py +28 -0
  38. flowllm/op/parallel_op.py +5 -1
  39. flowllm/op/search/__init__.py +1 -2
  40. flowllm/op/search/dashscope_search_op.py +73 -121
  41. flowllm/op/search/tavily_search_op.py +69 -80
  42. flowllm/op/sequential_op.py +4 -0
  43. flowllm/schema/flow_stream_chunk.py +11 -0
  44. flowllm/schema/message.py +2 -0
  45. flowllm/schema/service_config.py +8 -3
  46. flowllm/schema/tool_call.py +53 -4
  47. flowllm/service/__init__.py +0 -1
  48. flowllm/service/base_service.py +31 -14
  49. flowllm/service/http_service.py +46 -37
  50. flowllm/service/mcp_service.py +17 -23
  51. flowllm/storage/vector_store/__init__.py +1 -0
  52. flowllm/storage/vector_store/base_vector_store.py +99 -12
  53. flowllm/storage/vector_store/chroma_vector_store.py +250 -8
  54. flowllm/storage/vector_store/es_vector_store.py +291 -35
  55. flowllm/storage/vector_store/local_vector_store.py +206 -9
  56. flowllm/storage/vector_store/memory_vector_store.py +509 -0
  57. flowllm/utils/common_utils.py +54 -0
  58. flowllm/utils/logger_utils.py +28 -0
  59. flowllm/utils/miner_u_pdf_processor.py +726 -0
  60. {flowllm-0.1.2.dist-info → flowllm-0.1.5.dist-info}/METADATA +7 -6
  61. flowllm-0.1.5.dist-info/RECORD +98 -0
  62. flowllm/config/default.yaml +0 -77
  63. flowllm/config/empty.yaml +0 -37
  64. flowllm/flow/gallery/cmd_flow.py +0 -11
  65. flowllm/flow/gallery/code_tool_flow.py +0 -30
  66. flowllm/flow/gallery/dashscope_search_tool_flow.py +0 -34
  67. flowllm/flow/gallery/deepsearch_tool_flow.py +0 -39
  68. flowllm/flow/gallery/expression_tool_flow.py +0 -18
  69. flowllm/flow/gallery/tavily_search_tool_flow.py +0 -30
  70. flowllm/flow/gallery/terminate_tool_flow.py +0 -30
  71. flowllm/flow/parser/__init__.py +0 -0
  72. flowllm/op/agent/__init__.py +0 -0
  73. flowllm/op/agent/react_op.py +0 -83
  74. flowllm/op/base_ray_op.py +0 -313
  75. flowllm/op/code/__init__.py +0 -1
  76. flowllm/op/code/execute_code_op.py +0 -42
  77. flowllm/op/gallery/terminate_op.py +0 -29
  78. flowllm/op/search/dashscope_deep_research_op.py +0 -260
  79. flowllm/service/cmd_service.py +0 -15
  80. flowllm-0.1.2.dist-info/RECORD +0 -99
  81. {flowllm-0.1.2.dist-info → flowllm-0.1.5.dist-info}/WHEEL +0 -0
  82. {flowllm-0.1.2.dist-info → flowllm-0.1.5.dist-info}/entry_points.txt +0 -0
  83. {flowllm-0.1.2.dist-info → flowllm-0.1.5.dist-info}/licenses/LICENSE +0 -0
  84. {flowllm-0.1.2.dist-info → flowllm-0.1.5.dist-info}/top_level.txt +0 -0
@@ -7,61 +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
- "arguments": {
29
- "input_data": "sample_data",
30
- "processing_mode": "advanced",
31
- "output_format": "json"
32
- },
33
30
  "input_schema": {
34
- "input_data": ParamAttrs(
35
- type="string",
31
+ "a": ParamAttrs(
32
+ type="str",
36
33
  description="The input data to be processed",
37
34
  required=True
38
35
  ),
39
- "processing_mode": ParamAttrs(
36
+ "b": ParamAttrs(
40
37
  type="string",
41
38
  description="Processing mode: basic, advanced, or expert",
42
39
  required=False
43
40
  ),
44
- "output_format": ParamAttrs(
45
- type="string",
46
- description="Output format: json, xml, or plain",
47
- required=False
48
- )
49
- },
50
- "output_schema": {
51
- "result": ParamAttrs(
52
- type="object",
53
- 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",
54
72
  required=True
55
73
  ),
56
- "status": ParamAttrs(
74
+ "b": ParamAttrs(
57
75
  type="string",
58
- description="Processing status: success, warning, or error",
59
- required=True
60
- ),
61
- "metadata": ParamAttrs(
62
- type="object",
63
- description="Additional metadata about the processing",
76
+ description="Processing mode: basic, advanced, or expert",
64
77
  required=False
65
- )
78
+ ),
66
79
  }
67
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
@@ -2,7 +2,6 @@ import asyncio
2
2
  import os
3
3
  from typing import List, Dict
4
4
 
5
- from litellm import completion, acompletion
6
5
  from loguru import logger
7
6
  from pydantic import Field, PrivateAttr, model_validator
8
7
 
@@ -98,6 +97,7 @@ class LiteLLMBaseLLM(BaseLLM):
98
97
  Yields:
99
98
  Tuple of (chunk_content, ChunkEnum) for each streaming piece
100
99
  """
100
+ from litellm import completion
101
101
  for i in range(self.max_retries):
102
102
  try:
103
103
  # Prepare parameters for LiteLLM
@@ -200,6 +200,7 @@ class LiteLLMBaseLLM(BaseLLM):
200
200
  Yields:
201
201
  Tuple of (chunk_content, ChunkEnum) for each streaming piece
202
202
  """
203
+ from litellm import acompletion
203
204
  for i in range(self.max_retries):
204
205
  try:
205
206
  # Prepare parameters for LiteLLM
flowllm/op/__init__.py CHANGED
@@ -1,11 +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
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_llm_op.py CHANGED
@@ -23,8 +23,9 @@ class BaseLLMOp(BaseOp, ABC):
23
23
  super().__init__(**kwargs)
24
24
 
25
25
  self.language: str = language or C.language
26
- self.prompt_path: Path = Path(prompt_path) if prompt_path else \
27
- Path(self.file_path).parent / self.name.replace("_op", "_prompt.yaml")
26
+ default_prompt_path = self.file_path.replace("op.py", "prompt.yaml")
27
+ self.prompt_path: Path = Path(prompt_path) if prompt_path else default_prompt_path
28
+
28
29
  self._llm: BaseLLM | str = llm
29
30
  self._embedding_model: BaseEmbeddingModel | str = embedding_model
30
31
  self._vector_store: BaseVectorStore | str = vector_store
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,54 +15,153 @@ 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,
28
+ enable_multithread: bool = True,
19
29
  **kwargs):
20
30
  super().__init__()
21
31
 
22
32
  self.name: str = name or camel_to_snake(self.__class__.__name__)
33
+ self.max_retries: int = max_retries
23
34
  self.raise_exception: bool = raise_exception
35
+ self.enable_multithread: bool = enable_multithread
24
36
  self.op_params: dict = kwargs
25
37
 
26
38
  self.task_list: List[Future] = []
27
- self.ray_task_list: List = [] # Ray ObjectRef list
39
+ self.async_task_list: List = []
40
+ self.ray_task_list: List = []
28
41
  self.timer = Timer(name=self.name)
29
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
+ ...
30
50
 
31
- @abstractmethod
32
51
  def execute(self):
33
52
  ...
34
53
 
54
+ def default_execute(self):
55
+ ...
56
+
57
+ async def async_execute(self):
58
+ ...
59
+
35
60
  def __call__(self, context: FlowContext = None):
36
61
  self.context = context
37
62
  with self.timer:
38
- if self.raise_exception:
63
+ if self.max_retries == 1 and self.raise_exception:
64
+ self.before_execute()
39
65
  self.execute()
66
+ self.after_execute()
40
67
 
41
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()
42
95
 
43
- try:
44
- self.execute()
45
- except Exception as e:
46
- logger.exception(f"op={self.name} execute failed, error={e.args}")
47
-
48
- return self.context.response if self.context else None
49
-
50
- def submit_task(self, fn, *args, **kwargs):
51
- task = C.thread_pool.submit(fn, *args, **kwargs)
52
- self.task_list.append(task)
53
- return self
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!")
54
122
 
55
- def join_task(self, task_desc: str = None) -> list:
123
+ async def join_async_task(self):
56
124
  result = []
57
- for task in tqdm(self.task_list, desc=task_desc or self.name):
58
- t_result = task.result()
125
+ for task in self.async_task_list:
126
+ t_result = await task
59
127
  if t_result:
60
128
  if isinstance(t_result, list):
61
129
  result.extend(t_result)
62
130
  else:
63
131
  result.append(t_result)
132
+
133
+ self.async_task_list.clear()
134
+ return result
135
+
136
+ def submit_task(self, fn, *args, **kwargs):
137
+ if self.enable_multithread:
138
+ task = C.thread_pool.submit(fn, *args, **kwargs)
139
+ self.task_list.append(task)
140
+
141
+ else:
142
+ result = fn(*args, **kwargs)
143
+ if result:
144
+ if isinstance(result, list):
145
+ result.extend(result)
146
+ else:
147
+ result.append(result)
148
+
149
+ return self
150
+
151
+ def join_task(self, task_desc: str = None) -> list:
152
+ result = []
153
+ if self.enable_multithread:
154
+ for task in tqdm(self.task_list, desc=task_desc or self.name):
155
+ t_result = task.result()
156
+ if t_result:
157
+ if isinstance(t_result, list):
158
+ result.extend(t_result)
159
+ else:
160
+ result.append(t_result)
161
+
162
+ else:
163
+ result.extend(self.task_list)
164
+
64
165
  self.task_list.clear()
65
166
  return result
66
167
 
@@ -75,6 +176,9 @@ class BaseOp(ABC):
75
176
  sequential_op.ops.append(op)
76
177
  return sequential_op
77
178
 
179
+ def __lshift__(self, op: "BaseOp"):
180
+ self.sub_op = op
181
+
78
182
  def __or__(self, op: "BaseOp"):
79
183
  from flowllm.op.parallel_op import ParallelOp
80
184
 
@@ -87,6 +191,9 @@ class BaseOp(ABC):
87
191
 
88
192
  return parallel_op
89
193
 
194
+ def copy(self) -> "BaseOp":
195
+ return self.__class__(*self._init_args, **self._init_kwargs)
196
+
90
197
 
91
198
  def run1():
92
199
  """Basic test"""
@@ -95,6 +202,9 @@ def run1():
95
202
  def execute(self):
96
203
  logger.info(f"op={self.name} execute")
97
204
 
205
+ async def async_execute(self):
206
+ logger.info(f"op={self.name} async_execute")
207
+
98
208
  mock_op = MockOp()
99
209
  mock_op()
100
210
 
@@ -112,6 +222,12 @@ def run2():
112
222
  logger.info(f"Executing {op_result}")
113
223
  return op_result
114
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
+
115
231
  # Create service_context for parallel execution
116
232
  C["thread_pool"] = ThreadPoolExecutor(max_workers=4)
117
233
 
@@ -131,18 +247,135 @@ def run2():
131
247
  result = parallel()
132
248
  logger.info(f"Parallel result: {result}")
133
249
 
134
- 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 ===")
135
304
  mixed = op1 >> (op2 | op3) >> op4
136
- result = mixed()
137
- logger.info(f"Mixed result: {result}")
305
+ result = await mixed.async_call()
306
+ logger.info(f"Async mixed result: {result}")
138
307
 
139
- logger.info("=== Testing complex mixed calls op1 >> (op1 | (op2 >> op3)) >> op4 ===")
308
+ logger.info("=== Testing async complex mixed calls op1 >> (op1 | (op2 >> op3)) >> op4 ===")
140
309
  complex_mixed = op1 >> (op1 | (op2 >> op3)) >> op4
141
- result = complex_mixed()
142
- 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}")
143
362
 
144
363
 
145
364
  if __name__ == "__main__":
146
365
  run1()
147
366
  print("\n" + "=" * 50 + "\n")
148
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())