agently 4.0.6.7__py3-none-any.whl → 4.0.7__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 (30) hide show
  1. agently/base.py +3 -5
  2. agently/builtins/agent_extensions/ConfigurePromptExtension.py +40 -9
  3. agently/builtins/hookers/SystemMessageHooker.py +51 -7
  4. agently/builtins/plugins/ModelRequester/OpenAICompatible.py +32 -0
  5. agently/builtins/plugins/PromptGenerator/AgentlyPromptGenerator.py +64 -1
  6. agently/builtins/plugins/ResponseParser/AgentlyResponseParser.py +22 -14
  7. agently/core/Agent.py +67 -27
  8. agently/core/ModelRequest.py +216 -26
  9. agently/core/PluginManager.py +2 -0
  10. agently/core/Prompt.py +15 -45
  11. agently/core/TriggerFlow/BluePrint.py +2 -0
  12. agently/core/TriggerFlow/Chunk.py +5 -4
  13. agently/core/TriggerFlow/Execution.py +29 -12
  14. agently/core/TriggerFlow/TriggerFlow.py +24 -10
  15. agently/core/TriggerFlow/process/BaseProcess.py +63 -21
  16. agently/core/TriggerFlow/process/ForEachProcess.py +30 -24
  17. agently/core/TriggerFlow/process/MatchCaseProcess.py +6 -6
  18. agently/integrations/chromadb.py +15 -0
  19. agently/types/data/response.py +10 -1
  20. agently/types/plugins/PromptGenerator.py +5 -1
  21. agently/types/plugins/ResponseParser.py +26 -6
  22. agently/types/trigger_flow/trigger_flow.py +6 -6
  23. agently/utils/DataFormatter.py +77 -0
  24. agently/utils/PythonSandbox.py +101 -0
  25. agently/utils/Settings.py +19 -2
  26. agently/utils/__init__.py +1 -0
  27. {agently-4.0.6.7.dist-info → agently-4.0.7.dist-info}/METADATA +1 -1
  28. {agently-4.0.6.7.dist-info → agently-4.0.7.dist-info}/RECORD +30 -29
  29. {agently-4.0.6.7.dist-info → agently-4.0.7.dist-info}/WHEEL +0 -0
  30. {agently-4.0.6.7.dist-info → agently-4.0.7.dist-info}/licenses/LICENSE +0 -0
agently/core/Prompt.py CHANGED
@@ -13,9 +13,10 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import re
16
- from typing import Any, Literal, Mapping, Sequence, TYPE_CHECKING, cast, overload, TypeVar
16
+ from textwrap import dedent
17
+ from typing import Any, Literal, TYPE_CHECKING, cast, overload, TypeVar
17
18
 
18
- from agently.utils import RuntimeData, Settings
19
+ from agently.utils import RuntimeData, Settings, DataFormatter
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from agently.types.data.prompt import ChatMessage, PromptStandardSlot
@@ -79,8 +80,6 @@ class Prompt(RuntimeData):
79
80
  ):
80
81
  super().__init__(prompt_dict, parent=parent_prompt, name=name)
81
82
 
82
- self._placeholder_pattern = re.compile(r"\$\{\s*([^}]+?)\s*\}")
83
-
84
83
  self.settings = Settings(
85
84
  name="Prompt-Settings",
86
85
  parent=parent_settings,
@@ -98,42 +97,9 @@ class Prompt(RuntimeData):
98
97
  self.to_messages = self.prompt_generator.to_messages
99
98
  self.to_prompt_object = self.prompt_generator.to_prompt_object
100
99
  self.to_output_model = self.prompt_generator.to_output_model
101
-
102
- def _substitute_placeholder(self, obj: T, variable_mappings: dict[str, Any]) -> T | Any:
103
- if not isinstance(variable_mappings, dict):
104
- raise TypeError(f"Variable mappings require a dictionary but got: { variable_mappings }")
105
-
106
- if isinstance(obj, str):
107
- full_match = self._placeholder_pattern.fullmatch(obj)
108
- if full_match:
109
- key = full_match.group(1).strip()
110
- return variable_mappings.get(key, obj)
111
- else:
112
-
113
- def replacer(match):
114
- key = match.group(1).strip()
115
- return str(variable_mappings.get(key, match.group(0)))
116
-
117
- return self._placeholder_pattern.sub(replacer, obj)
118
-
119
- if isinstance(obj, Mapping):
120
- return {
121
- self._substitute_placeholder(key, variable_mappings): self._substitute_placeholder(
122
- value, variable_mappings
123
- )
124
- for key, value in obj.items()
125
- }
126
-
127
- if isinstance(obj, Sequence) and not isinstance(obj, (str, bytes, bytearray)):
128
- if isinstance(obj, tuple):
129
- return tuple(self._substitute_placeholder(value, variable_mappings) for value in obj)
130
- else:
131
- return [self._substitute_placeholder(value, variable_mappings) for value in obj]
132
-
133
- if isinstance(obj, set):
134
- return {self._substitute_placeholder(value, variable_mappings) for value in obj}
135
-
136
- return obj
100
+ self.to_serializable_prompt_data = self.prompt_generator.to_serializable_prompt_data
101
+ self.to_json_prompt = self.prompt_generator.to_json_prompt
102
+ self.to_yaml_prompt = self.prompt_generator.to_yaml_prompt
137
103
 
138
104
  @overload
139
105
  def set(
@@ -157,10 +123,12 @@ class Prompt(RuntimeData):
157
123
  value: Any,
158
124
  mappings: dict[str, Any] | None = None,
159
125
  ):
126
+ if isinstance(value, str):
127
+ value = dedent(value.strip())
160
128
  if mappings is not None:
161
129
  super().set(
162
- self._substitute_placeholder(key, mappings),
163
- self._substitute_placeholder(value, mappings),
130
+ DataFormatter.substitute_placeholder(key, mappings),
131
+ DataFormatter.substitute_placeholder(value, mappings),
164
132
  )
165
133
  else:
166
134
  super().set(key, value)
@@ -172,7 +140,7 @@ class Prompt(RuntimeData):
172
140
  ):
173
141
  if mappings is not None:
174
142
  super().update(
175
- self._substitute_placeholder(new, mappings),
143
+ DataFormatter.substitute_placeholder(new, mappings),
176
144
  )
177
145
  else:
178
146
  super().update(new)
@@ -183,10 +151,12 @@ class Prompt(RuntimeData):
183
151
  value: Any,
184
152
  mappings: dict[str, Any] | None = None,
185
153
  ):
154
+ if isinstance(value, str):
155
+ value = dedent(value.strip())
186
156
  if mappings is not None:
187
157
  super().append(
188
- self._substitute_placeholder(key, mappings),
189
- self._substitute_placeholder(value, mappings),
158
+ DataFormatter.substitute_placeholder(key, mappings),
159
+ DataFormatter.substitute_placeholder(value, mappings),
190
160
  )
191
161
  else:
192
162
  super().append(key, value)
@@ -132,6 +132,7 @@ class TriggerFlowBluePrint:
132
132
  *,
133
133
  execution_id: str | None = None,
134
134
  skip_exceptions: bool = False,
135
+ concurrency: int | None = None,
135
136
  ):
136
137
  handlers_snapshot: TriggerFlowAllHandlers = {
137
138
  "event": {k: v.copy() for k, v in self._handlers["event"].items()},
@@ -143,6 +144,7 @@ class TriggerFlowBluePrint:
143
144
  trigger_flow=trigger_flow,
144
145
  id=execution_id,
145
146
  skip_exceptions=skip_exceptions,
147
+ concurrency=concurrency,
146
148
  )
147
149
 
148
150
  def copy(self, *, name: str | None = None):
@@ -30,16 +30,17 @@ class TriggerFlowChunk:
30
30
  *,
31
31
  name: str | None = None,
32
32
  ):
33
- self.name = name if name is not None else uuid.uuid4().hex
33
+ self.id = uuid.uuid4().hex
34
+ self.name = name if name is not None else self.id
34
35
  self._handler = handler
35
- self.trigger = f"Chunk[{ handler.__name__ }]-{ self.name }"
36
+ self.trigger = f"Chunk[{ handler.__name__ }]-{ self.id }"
36
37
 
37
38
  async def async_call(self, data: "TriggerFlowEventData"):
38
39
  result = await FunctionShifter.asyncify(self._handler)(data)
39
- await data.async_emit(self.trigger, result, layer_marks=data.layer_marks.copy())
40
+ await data.async_emit(self.trigger, result, _layer_marks=data._layer_marks.copy())
40
41
  return result
41
42
 
42
43
  def call(self, data: "TriggerFlowEventData"):
43
44
  result = FunctionShifter.syncify(self._handler)(data)
44
- data.emit(self.trigger, result, layer_marks=data.layer_marks.copy())
45
+ data.emit(self.trigger, result, _layer_marks=data._layer_marks.copy())
45
46
  return result
@@ -16,6 +16,7 @@
16
16
  import uuid
17
17
  import asyncio
18
18
  import warnings
19
+ from contextvars import ContextVar
19
20
 
20
21
  from typing import Any, Literal, TYPE_CHECKING
21
22
 
@@ -37,6 +38,7 @@ class TriggerFlowExecution:
37
38
  trigger_flow: "TriggerFlow",
38
39
  id: str | None = None,
39
40
  skip_exceptions: bool = False,
41
+ concurrency: int | None = None,
40
42
  ):
41
43
  # Basic Attributions
42
44
  self.id = id if id is not None else uuid.uuid4().hex
@@ -45,6 +47,11 @@ class TriggerFlowExecution:
45
47
  self._runtime_data = RuntimeData()
46
48
  self._system_runtime_data = RuntimeData()
47
49
  self._skip_exceptions = skip_exceptions
50
+ self._concurrency_semaphore = asyncio.Semaphore(concurrency) if concurrency and concurrency > 0 else None
51
+ self._concurrency_depth = ContextVar(
52
+ f"trigger_flow_execution_concurrency_depth_{ self.id }",
53
+ default=0,
54
+ )
48
55
 
49
56
  # Settings
50
57
  self.settings = Settings(
@@ -97,7 +104,7 @@ class TriggerFlowExecution:
97
104
  self,
98
105
  trigger_event: str,
99
106
  value: Any = None,
100
- layer_marks: list[str] | None = None,
107
+ _layer_marks: list[str] | None = None,
101
108
  *,
102
109
  trigger_type: Literal["event", "runtime_data", "flow_data"] = "event",
103
110
  ):
@@ -126,19 +133,29 @@ class TriggerFlowExecution:
126
133
  },
127
134
  self.settings,
128
135
  )
129
- tasks.append(
130
- asyncio.ensure_future(
131
- FunctionShifter.asyncify(handler)(
132
- TriggerFlowEventData(
133
- trigger_event=trigger_event,
134
- trigger_type=trigger_type,
135
- value=value,
136
- execution=self,
137
- layer_marks=layer_marks,
138
- )
139
- )
136
+ async def run_handler(handler_func):
137
+ if self._concurrency_semaphore is None:
138
+ return await handler_func
139
+ depth = self._concurrency_depth.get()
140
+ token = self._concurrency_depth.set(depth + 1)
141
+ try:
142
+ if depth > 0:
143
+ return await handler_func
144
+ async with self._concurrency_semaphore:
145
+ return await handler_func
146
+ finally:
147
+ self._concurrency_depth.reset(token)
148
+
149
+ handler_task = FunctionShifter.asyncify(handler)(
150
+ TriggerFlowEventData(
151
+ trigger_event=trigger_event,
152
+ trigger_type=trigger_type,
153
+ value=value,
154
+ execution=self,
155
+ _layer_marks=_layer_marks,
140
156
  )
141
157
  )
158
+ tasks.append(asyncio.ensure_future(run_handler(handler_task)))
142
159
 
143
160
  if tasks:
144
161
  await asyncio.gather(*tasks, return_exceptions=self._skip_exceptions)
@@ -51,6 +51,7 @@ class TriggerFlow:
51
51
  self._skip_exceptions = skip_exceptions
52
52
  self._executions: dict[str, "TriggerFlowExecution"] = {}
53
53
  self._start_process = TriggerFlowProcess(
54
+ flow_chunk=self.chunk,
54
55
  trigger_event="START",
55
56
  blue_print=self._blue_print,
56
57
  block_data=TriggerFlowBlockData(
@@ -60,6 +61,8 @@ class TriggerFlow:
60
61
 
61
62
  self.chunks = self._blue_print.chunks
62
63
 
64
+ self.set_settings = self.settings.set_settings
65
+
63
66
  self.get_flow_data = self._flow_data.get
64
67
  self.set_flow_data = FunctionShifter.syncify(self.async_set_flow_data)
65
68
  self.append_flow_data = FunctionShifter.syncify(self.async_append_flow_data)
@@ -74,10 +77,6 @@ class TriggerFlow:
74
77
  self.start_execution = FunctionShifter.syncify(self.async_start_execution)
75
78
  self.start = FunctionShifter.syncify(self.async_start)
76
79
 
77
- def set_settings(self, key: str, value: "SerializableValue"):
78
- self.settings.set_settings(key, value)
79
- return self
80
-
81
80
  @overload
82
81
  def chunk(self, handler_or_name: "TriggerFlowHandler") -> TriggerFlowChunk: ...
83
82
 
@@ -100,13 +99,19 @@ class TriggerFlow:
100
99
  self._blue_print.chunks[handler_or_name.__name__] = chunk
101
100
  return chunk
102
101
 
103
- def create_execution(self, *, skip_exceptions: bool | None = None):
102
+ def create_execution(
103
+ self,
104
+ *,
105
+ skip_exceptions: bool | None = None,
106
+ concurrency: int | None = None,
107
+ ):
104
108
  execution_id = uuid.uuid4().hex
105
109
  skip_exceptions = skip_exceptions if skip_exceptions is not None else self._skip_exceptions
106
110
  execution = self._blue_print.create_execution(
107
111
  self,
108
112
  execution_id=execution_id,
109
113
  skip_exceptions=skip_exceptions,
114
+ concurrency=concurrency,
110
115
  )
111
116
  self._executions[execution_id] = execution
112
117
  return execution
@@ -119,8 +124,14 @@ class TriggerFlow:
119
124
  if execution.id in self._executions:
120
125
  del self._executions[execution.id]
121
126
 
122
- async def async_start_execution(self, initial_value: Any, *, wait_for_result: bool = False):
123
- execution = self.create_execution()
127
+ async def async_start_execution(
128
+ self,
129
+ initial_value: Any,
130
+ *,
131
+ wait_for_result: bool = False,
132
+ concurrency: int | None = None,
133
+ ):
134
+ execution = self.create_execution(concurrency=concurrency)
124
135
  await execution.async_start(initial_value, wait_for_result=wait_for_result)
125
136
  return execution
126
137
 
@@ -193,8 +204,9 @@ class TriggerFlow:
193
204
  *,
194
205
  wait_for_result: bool = True,
195
206
  timeout: int | None = 10,
207
+ concurrency: int | None = None,
196
208
  ):
197
- execution = await self.async_start_execution(initial_value)
209
+ execution = await self.async_start_execution(initial_value, concurrency=concurrency)
198
210
  if wait_for_result:
199
211
  return await execution.async_get_result(timeout=timeout)
200
212
 
@@ -203,8 +215,9 @@ class TriggerFlow:
203
215
  initial_value: Any = None,
204
216
  *,
205
217
  timeout: int | None = 10,
218
+ concurrency: int | None = None,
206
219
  ):
207
- execution = self.create_execution()
220
+ execution = self.create_execution(concurrency=concurrency)
208
221
  return execution.get_async_runtime_stream(
209
222
  initial_value,
210
223
  timeout=timeout,
@@ -215,8 +228,9 @@ class TriggerFlow:
215
228
  initial_value: Any = None,
216
229
  *,
217
230
  timeout: int | None = 10,
231
+ concurrency: int | None = None,
218
232
  ):
219
- execution = self.create_execution()
233
+ execution = self.create_execution(concurrency=concurrency)
220
234
  return execution.get_runtime_stream(
221
235
  initial_value,
222
236
  timeout=timeout,
@@ -14,10 +14,10 @@
14
14
 
15
15
 
16
16
  import uuid
17
- from asyncio import Event
17
+ from asyncio import Event, Semaphore
18
18
  from threading import Lock
19
19
 
20
- from typing import Any, Literal, TYPE_CHECKING, overload
20
+ from typing import Callable, Any, Literal, TYPE_CHECKING, overload, cast
21
21
  from typing_extensions import Self
22
22
 
23
23
 
@@ -31,15 +31,18 @@ from agently.types.trigger_flow import TriggerFlowBlockData
31
31
 
32
32
 
33
33
  class TriggerFlowBaseProcess:
34
+
34
35
  def __init__(
35
36
  self,
36
37
  *,
38
+ flow_chunk,
37
39
  trigger_event: str,
38
40
  blue_print: "TriggerFlowBluePrint",
39
41
  block_data: "TriggerFlowBlockData",
40
42
  trigger_type: Literal["event", "runtime_data", "flow_data"] = "event",
41
43
  **options,
42
44
  ):
45
+ self._flow_chunk = flow_chunk
43
46
  self.trigger_event = trigger_event
44
47
  self.trigger_type: Literal["event", "runtime_data", "flow_data"] = trigger_type
45
48
  self._blue_print = blue_print
@@ -55,6 +58,7 @@ class TriggerFlowBaseProcess:
55
58
  **options,
56
59
  ):
57
60
  return type(self)(
61
+ flow_chunk=self._flow_chunk,
58
62
  trigger_event=trigger_event,
59
63
  trigger_type=trigger_type,
60
64
  blue_print=blue_print,
@@ -112,8 +116,12 @@ class TriggerFlowBaseProcess:
112
116
  if isinstance(trigger_or_triggers, TriggerFlowChunk):
113
117
  trigger_or_triggers = trigger_or_triggers.trigger
114
118
  if isinstance(trigger_or_triggers, str):
119
+ if trigger_or_triggers in self._blue_print.chunks:
120
+ trigger = self._blue_print.chunks[trigger_or_triggers].trigger
121
+ else:
122
+ trigger = trigger_or_triggers
115
123
  return self._new(
116
- trigger_event=trigger_or_triggers,
124
+ trigger_event=trigger,
117
125
  trigger_type="event",
118
126
  blue_print=self._blue_print,
119
127
  block_data=TriggerFlowBlockData(
@@ -178,7 +186,7 @@ class TriggerFlowBaseProcess:
178
186
  if mode == "simple_or"
179
187
  else (data.trigger_type, data.trigger_event, data.value)
180
188
  ),
181
- layer_marks=data.layer_marks.copy(),
189
+ _layer_marks=data._layer_marks.copy(),
182
190
  )
183
191
  case "and":
184
192
  if data.trigger_type in values and data.trigger_event in values[trigger_type]: # type: ignore
@@ -191,7 +199,7 @@ class TriggerFlowBaseProcess:
191
199
  await data.async_emit(
192
200
  when_trigger,
193
201
  values,
194
- layer_marks=data.layer_marks.copy(),
202
+ _layer_marks=data._layer_marks.copy(),
195
203
  )
196
204
 
197
205
  for trigger_type, trigger_event_dict in values.items():
@@ -213,15 +221,22 @@ class TriggerFlowBaseProcess:
213
221
 
214
222
  def to(
215
223
  self,
216
- chunk: "TriggerFlowChunk | TriggerFlowHandler | str",
224
+ chunk: "TriggerFlowChunk | TriggerFlowHandler | str | tuple[str, TriggerFlowHandler]",
217
225
  side_branch: bool = False,
226
+ name: str | None = None,
218
227
  ):
219
228
  if isinstance(chunk, str):
220
229
  if chunk in self._blue_print.chunks:
221
230
  chunk = self._blue_print.chunks[chunk]
222
231
  else:
223
232
  raise NotImplementedError(f"Cannot find chunk named '{ chunk }'")
224
- chunk = TriggerFlowChunk(chunk) if callable(chunk) else chunk
233
+ elif isinstance(chunk, tuple):
234
+ chunk_name = chunk[0]
235
+ chunk_func = chunk[1]
236
+ chunk = self._flow_chunk(chunk_name)(chunk_func)
237
+ else:
238
+ chunk = self._flow_chunk(name or chunk.__name__)(chunk) if callable(chunk) else chunk
239
+ assert isinstance(chunk, TriggerFlowChunk)
225
240
  self._blue_print.add_handler(
226
241
  self.trigger_type,
227
242
  self.trigger_event,
@@ -235,38 +250,65 @@ class TriggerFlowBaseProcess:
235
250
  **self._options,
236
251
  )
237
252
 
238
- def side_branch(self, chunk: "TriggerFlowChunk | TriggerFlowHandler"):
239
- return self.to(chunk, side_branch=True)
253
+ def side_branch(
254
+ self,
255
+ chunk: "TriggerFlowChunk | TriggerFlowHandler",
256
+ *,
257
+ name: str | None = None,
258
+ ):
259
+ return self.to(
260
+ chunk,
261
+ side_branch=True,
262
+ name=name,
263
+ )
240
264
 
241
265
  def batch(
242
266
  self,
243
- *chunks: "TriggerFlowChunk | TriggerFlowHandler",
267
+ *chunks: "TriggerFlowChunk | TriggerFlowHandler | tuple[str, TriggerFlowHandler]",
244
268
  side_branch: bool = False,
269
+ concurrency: int | None = None,
245
270
  ):
246
271
  batch_trigger = f"Batch-{ uuid.uuid4().hex }"
247
272
  results = {}
248
- chunks_to_wait = {}
273
+ triggers_to_wait = {}
274
+ trigger_to_chunk_name = {}
275
+ semaphore = Semaphore(concurrency) if concurrency and concurrency > 0 else None
249
276
 
250
277
  async def wait_all_chunks(data: "TriggerFlowEventData"):
251
- if data.event in chunks_to_wait:
252
- results[data.event] = data.value
253
- chunks_to_wait[data.event] = True
254
- for done in chunks_to_wait.values():
278
+ if data.event in triggers_to_wait:
279
+ results[trigger_to_chunk_name[data.event]] = data.value
280
+ triggers_to_wait[data.event] = True
281
+ for done in triggers_to_wait.values():
255
282
  if done is False:
256
283
  return
257
284
  await data.async_emit(
258
285
  batch_trigger,
259
286
  results,
260
- layer_marks=data.layer_marks.copy(),
287
+ _layer_marks=data._layer_marks.copy(),
261
288
  )
262
289
 
263
290
  for chunk in chunks:
264
- chunk = TriggerFlowChunk(chunk) if callable(chunk) else chunk
265
- chunks_to_wait[chunk.name] = False
291
+ if isinstance(chunk, tuple):
292
+ chunk_name = chunk[0]
293
+ chunk_func = chunk[1]
294
+ chunk = self._flow_chunk(chunk_name)(chunk_func)
295
+ else:
296
+ chunk = self._flow_chunk(chunk.__name__)(chunk) if callable(chunk) else chunk
297
+ triggers_to_wait[chunk.trigger] = False
298
+ trigger_to_chunk_name[chunk.trigger] = chunk.name
299
+ results[chunk.name] = None
300
+
301
+ if semaphore is None:
302
+ handler = chunk.async_call
303
+ else:
304
+ async def handler(data: "TriggerFlowEventData", _chunk=chunk):
305
+ async with semaphore:
306
+ return await _chunk.async_call(data)
307
+
266
308
  self._blue_print.add_handler(
267
309
  self.trigger_type,
268
310
  self.trigger_event,
269
- chunk.async_call,
311
+ handler,
270
312
  )
271
313
  self._blue_print.add_event_handler(chunk.trigger, wait_all_chunks)
272
314
 
@@ -299,13 +341,13 @@ class TriggerFlowBaseProcess:
299
341
  await data.async_emit(
300
342
  collect_trigger,
301
343
  self._block_data.global_data.get(f"collections.{ collection_name}"),
302
- layer_marks=data.layer_marks.copy(),
344
+ _layer_marks=data._layer_marks.copy(),
303
345
  )
304
346
  elif mode == "filled_then_empty":
305
347
  await data.async_emit(
306
348
  collect_trigger,
307
349
  self._block_data.global_data.get(f"collections.{ collection_name}"),
308
- layer_marks=data.layer_marks.copy(),
350
+ _layer_marks=data._layer_marks.copy(),
309
351
  )
310
352
  del self._block_data.global_data[f"collections.{ collection_name}"]
311
353
 
@@ -22,7 +22,7 @@ from agently.utils import RuntimeDataNamespace
22
22
 
23
23
 
24
24
  class TriggerFlowForEachProcess(TriggerFlowBaseProcess):
25
- def for_each(self):
25
+ def for_each(self, *, concurrency: int | None = None):
26
26
  for_each_id = uuid.uuid4().hex
27
27
  for_each_block_data = TriggerFlowBlockData(
28
28
  outer_block=self._block_data,
@@ -31,6 +31,7 @@ class TriggerFlowForEachProcess(TriggerFlowBaseProcess):
31
31
  },
32
32
  )
33
33
  send_item_trigger = f"ForEach-{ for_each_id }-Send"
34
+ semaphore = asyncio.Semaphore(concurrency) if concurrency and concurrency > 0 else None
34
35
 
35
36
  async def send_items(data: "TriggerFlowEventData"):
36
37
  data.layer_in()
@@ -38,33 +39,38 @@ class TriggerFlowForEachProcess(TriggerFlowBaseProcess):
38
39
  assert for_each_instance_id is not None
39
40
 
40
41
  send_tasks = []
41
- if not isinstance(data.value, str) and isinstance(data.value, Sequence):
42
- items = list(data.value)
43
- for item in items:
44
- data.layer_in()
45
- item_id = data.layer_mark
46
- assert item_id is not None
47
- data._system_runtime_data.set(f"for_each_results.{ for_each_instance_id }.{ item_id }", EMPTY)
48
- send_tasks.append(
49
- data.async_emit(
50
- send_item_trigger,
51
- item,
52
- data.layer_marks.copy(),
53
- )
54
- )
55
- data.layer_out()
56
- await asyncio.gather(*send_tasks)
57
- else:
42
+ def prepare_item(item):
58
43
  data.layer_in()
59
44
  item_id = data.layer_mark
60
45
  assert item_id is not None
46
+ layer_marks = data._layer_marks.copy()
61
47
  data._system_runtime_data.set(f"for_each_results.{ for_each_instance_id }.{ item_id }", EMPTY)
62
- await data.async_emit(
63
- send_item_trigger,
64
- data.value,
65
- data.layer_marks.copy(),
66
- )
67
48
  data.layer_out()
49
+ return item_id, layer_marks, item
50
+
51
+ async def emit_item(item, layer_marks):
52
+ if semaphore is None:
53
+ await data.async_emit(
54
+ send_item_trigger,
55
+ item,
56
+ layer_marks,
57
+ )
58
+ else:
59
+ async with semaphore:
60
+ await data.async_emit(
61
+ send_item_trigger,
62
+ item,
63
+ layer_marks,
64
+ )
65
+ if not isinstance(data.value, str) and isinstance(data.value, Sequence):
66
+ items = list(data.value)
67
+ for item in items:
68
+ _, layer_marks, item_value = prepare_item(item)
69
+ send_tasks.append(emit_item(item_value, layer_marks))
70
+ await asyncio.gather(*send_tasks)
71
+ else:
72
+ _, layer_marks, item_value = prepare_item(data.value)
73
+ await emit_item(item_value, layer_marks)
68
74
 
69
75
  self.to(send_items)
70
76
 
@@ -103,7 +109,7 @@ class TriggerFlowForEachProcess(TriggerFlowBaseProcess):
103
109
  await data.async_emit(
104
110
  end_for_each_trigger,
105
111
  list(for_each_results[for_each_instance_id].values()),
106
- data.layer_marks.copy(),
112
+ data._layer_marks.copy(),
107
113
  )
108
114
  for_each_results.delete(for_each_instance_id)
109
115
 
@@ -58,7 +58,7 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
58
58
  await data.async_emit(
59
59
  f"Match-{ match_id }-Case-{ case_id }",
60
60
  data.value,
61
- layer_marks=data.layer_marks.copy(),
61
+ _layer_marks=data._layer_marks.copy(),
62
62
  )
63
63
  return
64
64
  elif mode == "hit_all":
@@ -71,7 +71,7 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
71
71
  data.async_emit(
72
72
  f"Match-{ match_id }-Case-{ case_id }",
73
73
  data.value,
74
- layer_marks=data.layer_marks.copy(),
74
+ _layer_marks=data._layer_marks.copy(),
75
75
  )
76
76
  )
77
77
  data.layer_out()
@@ -81,13 +81,13 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
81
81
  await data.async_emit(
82
82
  f"Match-{ match_id }-Else",
83
83
  data.value,
84
- layer_marks=data.layer_marks.copy(),
84
+ _layer_marks=data._layer_marks.copy(),
85
85
  )
86
86
  else:
87
87
  await data.async_emit(
88
88
  f"Match-{ match_id }-Result",
89
89
  data.value,
90
- layer_marks=data.layer_marks.copy(),
90
+ _layer_marks=data._layer_marks.copy(),
91
91
  )
92
92
 
93
93
  self.to(match_case)
@@ -164,7 +164,7 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
164
164
  await data.async_emit(
165
165
  f"Match-{ match_id }-Result",
166
166
  list(match_results.values()),
167
- layer_marks=data.layer_marks.copy(),
167
+ _layer_marks=data._layer_marks.copy(),
168
168
  )
169
169
  del data._system_runtime_data[f"match_results.{ data.upper_layer_mark }"]
170
170
  else:
@@ -172,7 +172,7 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
172
172
  await data.async_emit(
173
173
  f"Match-{ match_id }-Result",
174
174
  data.value,
175
- layer_marks=data.layer_marks.copy(),
175
+ _layer_marks=data._layer_marks.copy(),
176
176
  )
177
177
 
178
178
  for trigger in branch_ends:
@@ -1,3 +1,18 @@
1
+ # Copyright 2023-2025 AgentEra(Agently.Tech)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  from agently.utils import LazyImport
2
17
 
3
18
  LazyImport.import_package("chromadb")