flock-core 0.2.1__py3-none-any.whl → 0.2.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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

@@ -1,12 +1,13 @@
1
- """Defines Temporal activities for running a chain of agents."""
1
+ """Defines Temporal activities for running a chain of agents with logging and tracing."""
2
2
 
3
3
  from datetime import datetime
4
4
 
5
+ from opentelemetry import trace
5
6
  from temporalio import activity
6
7
 
7
8
  from flock.core.context.context import FlockContext
8
9
  from flock.core.context.context_vars import FLOCK_CURRENT_AGENT
9
- from flock.core.flock_agent import FlockAgent, HandoffBase
10
+ from flock.core.flock_agent import FlockAgent, HandOff
10
11
  from flock.core.logging.formatters.base_formatter import FormatterOptions
11
12
  from flock.core.logging.formatters.formatter_factory import FormatterFactory
12
13
  from flock.core.logging.logging import get_logger
@@ -14,6 +15,7 @@ from flock.core.registry.agent_registry import Registry
14
15
  from flock.core.util.input_resolver import resolve_inputs
15
16
 
16
17
  logger = get_logger("activities")
18
+ tracer = trace.get_tracer(__name__)
17
19
 
18
20
 
19
21
  @activity.defn
@@ -22,96 +24,152 @@ async def run_agent(
22
24
  ) -> dict:
23
25
  """Runs a chain of agents using the provided context.
24
26
 
25
- The context contains:
26
- - A state (e.g., "init_input", "current_agent", etc.),
27
- - A history of agent runs.
28
-
29
- Each agent uses the current value of the variable specified in its `input` attribute.
30
- After each run, its output is merged into the context state.
31
- The default handoff behavior is to fetch the next agent's input automatically from the context.
27
+ The context contains state, history, and agent definitions.
28
+ After each agent run, its output is merged into the context.
32
29
  """
33
- registry = Registry()
34
- previous_agent_name = ""
35
- current_agent_name = context.get_variable(FLOCK_CURRENT_AGENT)
36
- handoff_data: HandoffBase = None
37
-
38
- logger.info("Starting agent chain", initial_agent=current_agent_name)
39
-
40
- agent = registry.get_agent(current_agent_name)
41
- if not agent:
42
- logger.error("Agent not found", agent=current_agent_name)
43
- return {"error": f"Agent '{current_agent_name}' not found."}
44
-
45
- while agent:
46
- # Get the inputs for the agent
47
-
48
- agent_inputs = resolve_inputs(agent.input, context, previous_agent_name)
49
-
50
- # Execute the agent
51
- logger.info("Executing agent")
52
- result = await agent.run(agent_inputs)
53
- logger.debug("Agent execution completed")
54
-
55
- if output_formatter:
56
- formatter = FormatterFactory.create_formatter(output_formatter)
57
- formatter.display(
58
- result, agent.name, output_formatter.wait_for_input
30
+ # Start a top-level span for the entire run_agent activity.
31
+ with tracer.start_as_current_span("run_agent") as span:
32
+ registry = Registry()
33
+ previous_agent_name = ""
34
+ if isinstance(context, dict):
35
+ context = FlockContext.from_dict(context)
36
+ current_agent_name = context.get_variable(FLOCK_CURRENT_AGENT)
37
+ span.set_attribute("initial.agent", current_agent_name)
38
+ logger.info("Starting agent chain", initial_agent=current_agent_name)
39
+
40
+ agent = registry.get_agent(current_agent_name)
41
+ agent.resolve_callables(context=context)
42
+ if not agent:
43
+ logger.error("Agent not found", agent=current_agent_name)
44
+ span.record_exception(
45
+ Exception(f"Agent '{current_agent_name}' not found")
59
46
  )
47
+ return {"error": f"Agent '{current_agent_name}' not found."}
60
48
 
61
- # If no handoff is defined, return the result
62
- if not agent.hand_off:
63
- context.record(
64
- agent.name,
65
- result,
66
- timestamp=datetime.now(),
67
- hand_off=None,
68
- called_from=previous_agent_name,
69
- )
70
- logger.info("No handoff defined, completing chain")
71
- return result
72
-
73
- # Determine the next agent
74
- handoff_data = HandoffBase()
75
- if callable(agent.hand_off):
76
- logger.debug("Executing handoff function")
77
- handoff_data = agent.hand_off(context, result)
78
- if isinstance(handoff_data.next_agent, FlockAgent):
79
- handoff_data.next_agent = handoff_data.next_agent.name
80
-
81
- elif isinstance(agent.hand_off, str | FlockAgent):
82
- handoff_data.next_agent = (
83
- agent.hand_off
84
- if isinstance(agent.hand_off, str)
85
- else agent.hand_off.name
86
- )
87
- else:
88
- logger.error("Unsupported hand_off type")
89
- return {"error": "Unsupported hand_off type."}
90
-
91
- context.record(
92
- agent.name,
93
- result,
94
- timestamp=datetime.now().isoformat(),
95
- hand_off=handoff_data,
96
- called_from=previous_agent_name,
97
- )
98
- previous_agent_name = agent.name
99
-
100
- # Update the current agent and prepare the next input automatically
101
- try:
102
- agent = registry.get_agent(handoff_data.next_agent)
103
- if not agent:
104
- logger.error(
105
- "Next agent not found", agent=handoff_data.next_agent
106
- )
107
- return {
108
- "error": f"Next agent '{handoff_data.next_agent}' not found."
109
- }
49
+ # Loop over agents in the chain.
50
+ while agent:
51
+ # Create a nested span for this iteration.
52
+ with tracer.start_as_current_span("agent_iteration") as iter_span:
53
+ iter_span.set_attribute("agent.name", agent.name)
110
54
 
111
- context.set_variable(FLOCK_CURRENT_AGENT, agent.name)
112
- logger.info("Handing off to next agent", next=agent.name)
113
- except Exception as e:
114
- logger.error("Error during handoff", error=e)
115
- return {"error": f"Error during handoff: {e}"}
55
+ # Resolve inputs for the agent.
56
+ agent_inputs = resolve_inputs(
57
+ agent.input, context, previous_agent_name
58
+ )
59
+ iter_span.add_event(
60
+ "resolved inputs", attributes={"inputs": str(agent_inputs)}
61
+ )
116
62
 
117
- return context.get_variable("init_input")
63
+ # Execute the agent with its own span.
64
+ with tracer.start_as_current_span("execute_agent") as exec_span:
65
+ logger.info("Executing agent", agent=agent.name)
66
+ try:
67
+ result = await agent.run(agent_inputs)
68
+ exec_span.set_attribute("result", str(result))
69
+ logger.debug(
70
+ "Agent execution completed", agent=agent.name
71
+ )
72
+ except Exception as e:
73
+ logger.error(
74
+ "Agent execution failed",
75
+ agent=agent.name,
76
+ error=str(e),
77
+ )
78
+ exec_span.record_exception(e)
79
+ raise
80
+
81
+ # Optionally display formatted output.
82
+ display_output = True
83
+ if agent.config:
84
+ display_output = not agent.config.disable_output
85
+
86
+ if output_formatter and display_output:
87
+ formatter = FormatterFactory.create_formatter(
88
+ output_formatter
89
+ )
90
+ formatter.display(
91
+ result, agent.name, output_formatter.wait_for_input
92
+ )
93
+
94
+ # If there is no handoff, record the result and finish.
95
+ if not agent.hand_off:
96
+ context.record(
97
+ agent.name,
98
+ result,
99
+ timestamp=datetime.now(),
100
+ hand_off=None,
101
+ called_from=previous_agent_name,
102
+ )
103
+ logger.info(
104
+ "No handoff defined, completing chain", agent=agent.name
105
+ )
106
+ iter_span.add_event("chain completed")
107
+ return result
108
+
109
+ # Determine the next agent.
110
+ handoff_data = HandOff()
111
+ if callable(agent.hand_off):
112
+ logger.debug("Executing handoff function", agent=agent.name)
113
+ try:
114
+ handoff_data = agent.hand_off(context, result)
115
+ if isinstance(handoff_data.next_agent, FlockAgent):
116
+ handoff_data.next_agent = (
117
+ handoff_data.next_agent.name
118
+ )
119
+ except Exception as e:
120
+ logger.error(
121
+ "Handoff function error",
122
+ agent=agent.name,
123
+ error=str(e),
124
+ )
125
+ iter_span.record_exception(e)
126
+ return {"error": f"Handoff function error: {e}"}
127
+ elif isinstance(agent.hand_off, str | FlockAgent):
128
+ handoff_data.next_agent = (
129
+ agent.hand_off
130
+ if isinstance(agent.hand_off, str)
131
+ else agent.hand_off.name
132
+ )
133
+ else:
134
+ logger.error("Unsupported hand_off type", agent=agent.name)
135
+ iter_span.add_event("unsupported hand_off type")
136
+ return {"error": "Unsupported hand_off type."}
137
+
138
+ # Record the agent run in the context.
139
+ context.record(
140
+ agent.name,
141
+ result,
142
+ timestamp=datetime.now().isoformat(),
143
+ hand_off=handoff_data,
144
+ called_from=previous_agent_name,
145
+ )
146
+ previous_agent_name = agent.name
147
+
148
+ # Prepare the next agent.
149
+ try:
150
+ agent = registry.get_agent(handoff_data.next_agent)
151
+ agent.resolve_callables(context=context)
152
+ if not agent:
153
+ logger.error(
154
+ "Next agent not found",
155
+ agent=handoff_data.next_agent,
156
+ )
157
+ iter_span.record_exception(
158
+ Exception(
159
+ f"Next agent '{handoff_data.next_agent}' not found"
160
+ )
161
+ )
162
+ return {
163
+ "error": f"Next agent '{handoff_data.next_agent}' not found."
164
+ }
165
+
166
+ context.set_variable(FLOCK_CURRENT_AGENT, agent.name)
167
+ logger.info("Handing off to next agent", next=agent.name)
168
+ iter_span.set_attribute("next.agent", agent.name)
169
+ except Exception as e:
170
+ logger.error("Error during handoff", error=str(e))
171
+ iter_span.record_exception(e)
172
+ return {"error": f"Error during handoff: {e}"}
173
+
174
+ # If the loop exits unexpectedly, return the initial input.
175
+ return context.get_variable("init_input")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flock-core
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Declarative LLM Orchestration at Scale
5
5
  Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
6
6
  License-File: LICENSE
@@ -14,6 +14,11 @@ Requires-Dist: dspy==2.5.42
14
14
  Requires-Dist: httpx>=0.28.1
15
15
  Requires-Dist: loguru>=0.7.3
16
16
  Requires-Dist: msgpack>=1.1.0
17
+ Requires-Dist: opentelemetry-api>=1.30.0
18
+ Requires-Dist: opentelemetry-exporter-jaeger>=1.21.0
19
+ Requires-Dist: opentelemetry-exporter-otlp>=1.30.0
20
+ Requires-Dist: opentelemetry-instrumentation-logging>=0.51b0
21
+ Requires-Dist: opentelemetry-sdk>=1.30.0
17
22
  Requires-Dist: pydantic>=2.10.5
18
23
  Requires-Dist: python-box>=7.3.2
19
24
  Requires-Dist: rich>=13.9.4
@@ -30,7 +35,7 @@ Description-Content-Type: text/markdown
30
35
 
31
36
  <p align="center">
32
37
  <img src="docs/img/flock.png" width="600"><br>
33
- <h1 align="center">Flock</h1><br>
38
+ <h1 align="center">Flock<br></h1><br>
34
39
 
35
40
 
36
41
  ## Overview
@@ -83,6 +88,10 @@ Flock is a framework for orchestrating LLM-powered agents. It leverages a **decl
83
88
  - **DSPy Integration:**
84
89
  Flock leverages DSPy for managing LLM interactions. The framework constructs clean signature strings and updates field metadata so that DSPy can include detailed instructions and context for each agent call.
85
90
 
91
+
92
+ <p align="center">
93
+ <img src="docs/img/flock_cli.png" width="200"><br>
94
+
86
95
  ## Quick Start
87
96
 
88
97
  Below is a simple example of how to create and run an agent with Flock:
@@ -238,6 +247,30 @@ pip install flock-core[all-tools]
238
247
  uv build && uv pip install -e .
239
248
  ```
240
249
 
250
+ Install Jaeger for telemetry
251
+ ```
252
+
253
+ docker run -d --name jaeger \
254
+ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
255
+ -p 5775:5775/udp \
256
+ -p 6831:6831/udp \
257
+ -p 6832:6832/udp \
258
+ -p 5778:5778 \
259
+ -p 16686:16686 \
260
+ -p 14268:14268 \
261
+ -p 14250:14250 \
262
+ -p 9411:9411 \
263
+ jaegertracing/all-in-one:1.41
264
+
265
+
266
+ ```
267
+
268
+ or zipkin
269
+
270
+ ```
271
+ docker run -d -p 9411:9411 openzipkin/zipkin
272
+
273
+ ```
241
274
 
242
275
  ## Contributing
243
276
 
@@ -1,31 +1,28 @@
1
1
  flock/__init__.py,sha256=JeRpPR29wyqXe8El-Ug4v_Ibhf5GQm_i8yWMi0cUYz0,36
2
- flock/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- flock/agents/batch_agent.py,sha256=TgSHSX1xv2CdaxP-XsaqWoZvaW1lDj4Xi3Cklr2H3wE,4946
4
- flock/agents/loop_agent.py,sha256=qoUun9Nl4ODClPXLY9jI3tdjC1t9DdBLouXJyyV_-5g,4264
5
- flock/agents/trigger_agent.py,sha256=ID82EobfqVh6b5oUOE2GimNoraHV_8vU9bh-52h0NI4,4222
6
- flock/agents/user_agent.py,sha256=2OJSUIxNDWRxoTqiVarxGtmaFdazm8tiQwI5biLbkBc,4704
7
2
  flock/core/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
- flock/core/flock.py,sha256=F7hZWycq6IXPw1VNqhT1w6Wil3O4vQIkDZTDBmmILaw,8027
9
- flock/core/flock_agent.py,sha256=P6GCE5-d8DvGARwN5u2HFDsDOtTQB6aEASozJc09w3g,23662
10
- flock/core/context/context.py,sha256=yNsYHanXbo8EijBp4f3PiY0YCFcN9dSbmnLKhx3Q8Nw,7340
3
+ flock/core/flock.py,sha256=BKuPrOVzudoqauokWyf59UXKlKZ3Qy9iKOm3ZaU_18E,9517
4
+ flock/core/flock_agent.py,sha256=59qQ7ohOy2lc1KjI6SV7IcrqYL86ofAhq32pZGgk6eA,27761
5
+ flock/core/context/context.py,sha256=jH06w4C_O5CEL-YxjX_x_dmgLe9Rcllnn1Ebs0dvwaE,6171
11
6
  flock/core/context/context_manager.py,sha256=qMySVny_dbTNLh21RHK_YT0mNKIOrqJDZpi9ZVdBsxU,1103
12
7
  flock/core/context/context_vars.py,sha256=0Hn6fM2iNc0_jIIU0B7KX-K2o8qXqtZ5EYtwujETQ7U,272
13
8
  flock/core/execution/local_executor.py,sha256=O_dgQ_HJPCp97ghdEoDSNDIiaYkogrUS0G2FfK04RRc,973
14
- flock/core/execution/temporal_executor.py,sha256=rHZPPzgqm6x3AYHOrleWUfEQcF1XpW-c0x4bAvMJCCQ,1727
9
+ flock/core/execution/temporal_executor.py,sha256=ai6ikr9rEiN2Kc-208OalxtfqL_FTt_UaH6a--oEkJM,2010
15
10
  flock/core/logging/__init__.py,sha256=Q8hp9-1ilPIUIV0jLgJ3_cP7COrea32cVwL7dicPnlM,82
16
- flock/core/logging/logging.py,sha256=ny90ocZatLn-wHtpNrmMK2y1lPLqUpiw6kzWUj7C-Ao,4005
11
+ flock/core/logging/logging.py,sha256=F-FDz9etBXmAIT-fjx3pUfvNXsckgR6ONCMaFZIc4Kw,4619
12
+ flock/core/logging/telemetry.py,sha256=K7uhMQHf1aGvfeNnuAM-wd65tpal9ch5piYF6RxU4Js,786
13
+ flock/core/logging/trace_and_logged.py,sha256=h4YH8s0KjK4tiBdrEZdCLd4fDzMB5-NKwqzrtkWhQw4,1999
17
14
  flock/core/logging/formatters/base_formatter.py,sha256=CyG-X2NWq8sqEhFEO2aG7Mey5tVkIzoWiihW301_VIo,1023
18
15
  flock/core/logging/formatters/formatter_factory.py,sha256=hmH-NpCESHkioX0GBQ5CuQR4axyIXnSRWwAZCHylx6Q,1283
19
16
  flock/core/logging/formatters/pprint_formatter.py,sha256=2CZoMbfzJB9Xp9WaItxnXY3hfANMjDTXowk3ni6N-h4,565
20
17
  flock/core/logging/formatters/rich_formatters.py,sha256=h1FD0_cIdQBQ8P2x05XhgD1cmmP80IBNVT5jb3cAV9M,4776
21
18
  flock/core/logging/formatters/theme_builder.py,sha256=1RUEwPIDfCjwTapbK1liasA5SdukOn7YwbZ4H4j1WkI,17364
22
19
  flock/core/logging/formatters/themed_formatter.py,sha256=CbxmqUC7zkLzyIxngk-3dcpQ6vxPR6zaDNA2TAMitCI,16714
23
- flock/core/mixin/dspy_integration.py,sha256=f22SK4iF8I3h3HPyOTPa9iAb3RzsVFvCJC3vO5MHYko,6122
20
+ flock/core/mixin/dspy_integration.py,sha256=oT5YfXxPhHkMCuwhXoppBAYBGePUAKse7KebGSM-bq0,6880
24
21
  flock/core/mixin/prompt_parser.py,sha256=eOqI-FK3y17gVqpc_y5GF-WmK1Jv8mFlkZxTcgweoxI,5121
25
- flock/core/registry/agent_registry.py,sha256=VuuztTZXDwa2JIzIgY1EL3h-9T8_Vnk-eXI0CdjP5xM,2884
26
- flock/core/tools/basic_tools.py,sha256=0PnPBc4fvzTpGaAEaI2GI_oKiwlHXCXJnpRDHQEH-b0,8336
27
- flock/core/tools/dev_tools/github.py,sha256=9nXt0KxR5oVK1AH2TN1KL2QK8xGxFhkn-o8bB-S1GcE,6296
28
- flock/core/util/cli_helper.py,sha256=1PvqhnxlLy4C5BohqSzrR84BrDGyk0NGGCTXgrCU4FM,567
22
+ flock/core/registry/agent_registry.py,sha256=QHdr3Cb-32PEdz8jFCIZSH9OlfpRwAJMtSRpHCWJDq4,4889
23
+ flock/core/tools/basic_tools.py,sha256=nRc1bIz96z-WUTe_yYf9V6EfCPEncl_XnrpGdC7dEmo,8721
24
+ flock/core/tools/dev_tools/github.py,sha256=6ya2_eN-qITV3b_pYP24jQC3X4oZbRY5GKh1AF-9Zic,6836
25
+ flock/core/util/cli_helper.py,sha256=hXBXhTQizZILXiVeBFR6QdAsQm0Qj29ANHJRgiekwyw,859
29
26
  flock/core/util/input_resolver.py,sha256=OesGqX2Dld8myL9Qz04mmxLqoYqOSQC632pj1EMk9Yk,5456
30
27
  flock/core/util/serializable.py,sha256=SymJ0YrjBx48mOBItYSqoRpKuzIc4vKWRS6ScTzre7s,2573
31
28
  flock/themes/3024-day.toml,sha256=uOVHqEzSyHx0WlUk3D0lne4RBsNBAPCTy3C58yU7kEY,667
@@ -365,11 +362,11 @@ flock/themes/zenburned.toml,sha256=UEmquBbcAO3Zj652XKUwCsNoC2iQSlIh-q5c6DH-7Kc,1
365
362
  flock/themes/zenwritten-dark.toml,sha256=To5l6520_3UqAGiEumpzGWsHhXxqu9ThrMildXKgIO0,1669
366
363
  flock/themes/zenwritten-light.toml,sha256=G1iEheCPfBNsMTGaVpEVpDzYBHA_T-MV27rolUYolmE,1666
367
364
  flock/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
368
- flock/workflow/activities.py,sha256=IZM9wOJZWFUzT88nPdmtXkk288nc_FOZsyR2xhA2dWs,4302
365
+ flock/workflow/activities.py,sha256=G3LcW0Bs0-Iu_ry0Ch8DbP8yJ1eftwaZZ71PVWJaoVs,7634
369
366
  flock/workflow/agent_activities.py,sha256=NhBZscflEf2IMfSRa_pBM_TRP7uVEF_O0ROvWZ33eDc,963
370
367
  flock/workflow/temporal_setup.py,sha256=VWBgmBgfTBjwM5ruS_dVpA5AVxx6EZ7oFPGw4j3m0l0,1091
371
368
  flock/workflow/workflow.py,sha256=I9MryXW_bqYVTHx-nl2epbTqeRy27CAWHHA7ZZA0nAk,1696
372
- flock_core-0.2.1.dist-info/METADATA,sha256=ywzUx2LF6i23eC5SBOnfu0v8El_9Z2K0biPmqkjvWzg,10381
373
- flock_core-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
374
- flock_core-0.2.1.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
375
- flock_core-0.2.1.dist-info/RECORD,,
369
+ flock_core-0.2.2.dist-info/METADATA,sha256=P_6coaQ37bbmA6pbqptH8N5EvDHqroC_shFqAChW1_A,11065
370
+ flock_core-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
371
+ flock_core-0.2.2.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
372
+ flock_core-0.2.2.dist-info/RECORD,,
flock/agents/__init__.py DELETED
File without changes
@@ -1,140 +0,0 @@
1
- import asyncio
2
- import uuid
3
- from typing import Any
4
-
5
- from pydantic import Field
6
-
7
- from flock.core.context.context import FlockContext
8
- from flock.core.flock_agent import FlockAgent
9
-
10
-
11
- class BatchAgent(FlockAgent):
12
- """A DeclarativeAgent that processes an iterable input in batches.
13
-
14
- Additional Attributes:
15
- iter_input: The key in the FlockContext that holds the iterable (a list).
16
- batch_site: The number of items per batch.
17
-
18
- For each batch, the agent's input dictionary is built from the FlockContext with the
19
- value for the iter_input key overridden by the current batch. The outputs across batches
20
- are then aggregated.
21
- """
22
-
23
- iter_input: str = Field(
24
- default="",
25
- description="Key of the iterable input (must be a list in the FlockContext)",
26
- )
27
- batch_size: int = Field(
28
- default=1, description="Batch size (number of items per batch)"
29
- )
30
-
31
- async def run(self, context: FlockContext) -> dict:
32
- """Run the BatchAgent locally by partitioning the iterable and aggregating the results."""
33
- try:
34
- iterable = context.get_variable(self.iter_input)
35
- if not isinstance(iterable, list):
36
- error_msg = (
37
- f"Expected a list for key '{self.iter_input}' in context."
38
- )
39
- return {"error": error_msg}
40
-
41
- # Partition the iterable into batches
42
- batches: list[list[Any]] = [
43
- iterable[i : i + self.batch_size]
44
- for i in range(0, len(iterable), self.batch_size)
45
- ]
46
-
47
- # Process batches
48
- tasks = []
49
- for batch in batches:
50
- tasks.append(
51
- self.evaluate(
52
- context, input_overrides={self.iter_input: batch}
53
- )
54
- )
55
-
56
- batch_results = await asyncio.gather(*tasks)
57
-
58
- # Aggregate the outputs
59
- output_keys = self._parse_keys(self.output)
60
- aggregated = {key: [] for key in output_keys}
61
- for res in batch_results:
62
- for key in output_keys:
63
- aggregated[key].append(res.get(key))
64
- aggregated["batch_results"] = batch_results
65
- return aggregated
66
-
67
- except Exception:
68
- raise
69
-
70
- async def run_temporal(self, context: FlockContext) -> dict:
71
- """Run the BatchAgent via Temporal.
72
-
73
- For each batch, the agent's evaluation is performed as a separate Temporal activity.
74
- The results are then aggregated.
75
- """
76
- try:
77
- from temporalio.client import Client
78
-
79
- from flock.workflow.agent_activities import (
80
- run_declarative_agent_activity,
81
- )
82
- from flock.workflow.temporal_setup import run_activity
83
-
84
- # Connect to Temporal
85
- client = await Client.connect("localhost:7233", namespace="default")
86
-
87
- # Validate and prepare input
88
- iterable = context.get_variable(self.iter_input)
89
- if not isinstance(iterable, list):
90
- error_msg = (
91
- f"Expected a list for key '{self.iter_input}' in context."
92
- )
93
- return {"error": error_msg}
94
-
95
- # Partition into batches
96
- batches: list[list[Any]] = [
97
- iterable[i : i + self.batch_size]
98
- for i in range(0, len(iterable), self.batch_size)
99
- ]
100
-
101
- # Process batches
102
- tasks = []
103
- for batch in batches:
104
- # Prepare context for this batch
105
- new_state = context.state.copy()
106
- new_state[self.iter_input] = batch
107
- context_data = {
108
- "state": new_state,
109
- "history": [], # you might choose to pass along history if needed
110
- "agent_definitions": [],
111
- }
112
- agent_data = self.dict()
113
- task_id = f"{self.name}_{uuid.uuid4().hex[:4]}"
114
-
115
- # Create temporal activity task
116
- tasks.append(
117
- run_activity(
118
- client,
119
- task_id,
120
- run_declarative_agent_activity,
121
- {
122
- "agent_data": agent_data,
123
- "context_data": context_data,
124
- },
125
- )
126
- )
127
-
128
- batch_results = await asyncio.gather(*tasks)
129
-
130
- # Aggregate the outputs
131
- output_keys = self._parse_keys(self.output)
132
- aggregated = {key: [] for key in output_keys}
133
- for res in batch_results:
134
- for key in output_keys:
135
- aggregated[key].append(res.get(key))
136
- aggregated["batch_results"] = batch_results
137
- return aggregated
138
-
139
- except Exception:
140
- raise
@@ -1,117 +0,0 @@
1
- from collections.abc import Callable
2
- from typing import Any
3
-
4
- from pydantic import Field
5
-
6
- from flock.core.context.context import FlockContext
7
- from flock.core.flock_agent import FlockAgent
8
-
9
-
10
- class LoopAgent(FlockAgent):
11
- """An agent that executes its logic in a loop until a termination condition is met.
12
-
13
- Attributes:
14
- input: Input domain for the agent
15
- output: Output types for the agent
16
- tools: Tools the agent is allowed to use
17
- max_iterations: Maximum number of iterations before forced termination
18
- termination_condition: Optional callable that determines when to stop the loop
19
- """
20
-
21
- input: str = Field(default="", description="Input domain for the agent")
22
- output: str = Field(default="", description="Output types for the agent")
23
- tools: list[Callable] | None = Field(default=None, description="Tools the agent is allowed to use")
24
- max_iterations: int = Field(default=10, description="Maximum number of iterations")
25
- termination_condition: Callable[[dict[str, Any]], bool] | None = Field(
26
- default=None, description="Optional function to determine loop termination"
27
- )
28
-
29
- async def _process_iteration(self, context: FlockContext, iteration: int) -> dict[str, Any]:
30
- """Process a single iteration of the loop."""
31
- try:
32
- # Here you would implement the actual iteration logic
33
- # For now, we'll just return a simple result
34
- return {"iteration": iteration, "status": "completed"}
35
- except Exception:
36
- raise
37
-
38
- def _should_continue(self, result: dict[str, Any], iteration: int) -> bool:
39
- """Determine if the loop should continue."""
40
- if iteration >= self.max_iterations:
41
- return False
42
-
43
- if self.termination_condition:
44
- should_terminate = self.termination_condition(result)
45
- return not should_terminate
46
-
47
- return True
48
-
49
- async def run(self, context: FlockContext) -> dict[str, Any]:
50
- """Run the agent in a loop until the termination condition is met or max iterations reached."""
51
- try:
52
- results = []
53
- iteration = 0
54
-
55
- while True:
56
- result = await self._process_iteration(context, iteration)
57
- results.append(result)
58
-
59
- # Check termination conditions
60
- if not self._should_continue(result, iteration):
61
- break
62
-
63
- iteration += 1
64
-
65
- return {
66
- "iterations": iteration + 1,
67
- "results": results,
68
- "final_result": results[-1] if results else None,
69
- }
70
-
71
- except Exception:
72
- raise
73
-
74
- async def run_temporal(self, context: FlockContext) -> dict[str, Any]:
75
- """Run the loop agent via Temporal."""
76
- try:
77
- from temporalio.client import Client
78
-
79
- from flock.workflow.agent_activities import run_agent_activity
80
- from flock.workflow.temporal_setup import run_activity
81
-
82
- client = await Client.connect("localhost:7233", namespace="default")
83
-
84
- results = []
85
- iteration = 0
86
-
87
- while True:
88
- # Process iteration as a temporal activity
89
- context_data = {
90
- "state": context.state,
91
- "history": [record.__dict__ for record in context.history],
92
- "agent_definitions": [definition.__dict__ for definition in context.agent_definitions],
93
- }
94
- agent_data = self.dict()
95
-
96
- result = await run_activity(
97
- client,
98
- f"{self.name}_iteration_{iteration}",
99
- run_agent_activity,
100
- {"agent_data": agent_data, "context_data": context_data},
101
- )
102
- results.append(result)
103
-
104
- # Check termination conditions
105
- if not self._should_continue(result, iteration):
106
- break
107
-
108
- iteration += 1
109
-
110
- return {
111
- "iterations": iteration + 1,
112
- "results": results,
113
- "final_result": results[-1] if results else None,
114
- }
115
-
116
- except Exception:
117
- raise