flock-core 0.2.1__py3-none-any.whl → 0.2.3__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().isoformat(),
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.3
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,12 @@ 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-proto-grpc>=1.21.0
19
+ Requires-Dist: opentelemetry-exporter-jaeger>=1.21.0
20
+ Requires-Dist: opentelemetry-exporter-otlp>=1.30.0
21
+ Requires-Dist: opentelemetry-instrumentation-logging>=0.51b0
22
+ Requires-Dist: opentelemetry-sdk>=1.30.0
17
23
  Requires-Dist: pydantic>=2.10.5
18
24
  Requires-Dist: python-box>=7.3.2
19
25
  Requires-Dist: rich>=13.9.4
@@ -30,7 +36,7 @@ Description-Content-Type: text/markdown
30
36
 
31
37
  <p align="center">
32
38
  <img src="docs/img/flock.png" width="600"><br>
33
- <h1 align="center">Flock</h1><br>
39
+ <h1 align="center">Flock<br></h1><br>
34
40
 
35
41
 
36
42
  ## Overview
@@ -83,6 +89,10 @@ Flock is a framework for orchestrating LLM-powered agents. It leverages a **decl
83
89
  - **DSPy Integration:**
84
90
  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
91
 
92
+
93
+ <p align="center">
94
+ <img src="docs/img/flock_cli.png" width="200"><br>
95
+
86
96
  ## Quick Start
87
97
 
88
98
  Below is a simple example of how to create and run an agent with Flock:
@@ -238,6 +248,30 @@ pip install flock-core[all-tools]
238
248
  uv build && uv pip install -e .
239
249
  ```
240
250
 
251
+ Install Jaeger for telemetry
252
+ ```
253
+
254
+ docker run -d --name jaeger \
255
+ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
256
+ -p 5775:5775/udp \
257
+ -p 6831:6831/udp \
258
+ -p 6832:6832/udp \
259
+ -p 5778:5778 \
260
+ -p 16686:16686 \
261
+ -p 14268:14268 \
262
+ -p 14250:14250 \
263
+ -p 9411:9411 \
264
+ jaegertracing/all-in-one:1.41
265
+
266
+
267
+ ```
268
+
269
+ or zipkin
270
+
271
+ ```
272
+ docker run -d -p 9411:9411 openzipkin/zipkin
273
+
274
+ ```
241
275
 
242
276
  ## Contributing
243
277
 
@@ -1,31 +1,31 @@
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
- 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
1
+ flock/__init__.py,sha256=uJxXAxt0def69cccAAdLjBxQOFXVRO72RE82hJnODSw,108
2
+ flock/config.py,sha256=hul_Vmgl8w5a_4YNfg3Mvll3iVEkjiR5At9D0WsVesw,1006
3
+ flock/core/__init__.py,sha256=0Xq_txurlxxjKGXjRn6GNJusGTiBcd7zw2eF0L7JyuU,183
4
+ flock/core/flock.py,sha256=Iw7Frmz2aZheApxi2KSjsX7gA8ZcemzXhfKXeQdtY0w,9438
5
+ flock/core/flock_agent.py,sha256=59qQ7ohOy2lc1KjI6SV7IcrqYL86ofAhq32pZGgk6eA,27761
6
+ flock/core/context/context.py,sha256=jH06w4C_O5CEL-YxjX_x_dmgLe9Rcllnn1Ebs0dvwaE,6171
11
7
  flock/core/context/context_manager.py,sha256=qMySVny_dbTNLh21RHK_YT0mNKIOrqJDZpi9ZVdBsxU,1103
12
8
  flock/core/context/context_vars.py,sha256=0Hn6fM2iNc0_jIIU0B7KX-K2o8qXqtZ5EYtwujETQ7U,272
13
9
  flock/core/execution/local_executor.py,sha256=O_dgQ_HJPCp97ghdEoDSNDIiaYkogrUS0G2FfK04RRc,973
14
- flock/core/execution/temporal_executor.py,sha256=rHZPPzgqm6x3AYHOrleWUfEQcF1XpW-c0x4bAvMJCCQ,1727
10
+ flock/core/execution/temporal_executor.py,sha256=ai6ikr9rEiN2Kc-208OalxtfqL_FTt_UaH6a--oEkJM,2010
15
11
  flock/core/logging/__init__.py,sha256=Q8hp9-1ilPIUIV0jLgJ3_cP7COrea32cVwL7dicPnlM,82
16
- flock/core/logging/logging.py,sha256=ny90ocZatLn-wHtpNrmMK2y1lPLqUpiw6kzWUj7C-Ao,4005
12
+ flock/core/logging/logging.py,sha256=F-FDz9etBXmAIT-fjx3pUfvNXsckgR6ONCMaFZIc4Kw,4619
13
+ flock/core/logging/telemetry.py,sha256=T2CRSiqOWvOsXe-WRsObkkOkrrd6z-BwEYLaBUU2AAM,4017
14
+ flock/core/logging/trace_and_logged.py,sha256=h4YH8s0KjK4tiBdrEZdCLd4fDzMB5-NKwqzrtkWhQw4,1999
17
15
  flock/core/logging/formatters/base_formatter.py,sha256=CyG-X2NWq8sqEhFEO2aG7Mey5tVkIzoWiihW301_VIo,1023
18
16
  flock/core/logging/formatters/formatter_factory.py,sha256=hmH-NpCESHkioX0GBQ5CuQR4axyIXnSRWwAZCHylx6Q,1283
19
17
  flock/core/logging/formatters/pprint_formatter.py,sha256=2CZoMbfzJB9Xp9WaItxnXY3hfANMjDTXowk3ni6N-h4,565
20
18
  flock/core/logging/formatters/rich_formatters.py,sha256=h1FD0_cIdQBQ8P2x05XhgD1cmmP80IBNVT5jb3cAV9M,4776
21
19
  flock/core/logging/formatters/theme_builder.py,sha256=1RUEwPIDfCjwTapbK1liasA5SdukOn7YwbZ4H4j1WkI,17364
22
20
  flock/core/logging/formatters/themed_formatter.py,sha256=CbxmqUC7zkLzyIxngk-3dcpQ6vxPR6zaDNA2TAMitCI,16714
23
- flock/core/mixin/dspy_integration.py,sha256=f22SK4iF8I3h3HPyOTPa9iAb3RzsVFvCJC3vO5MHYko,6122
21
+ flock/core/logging/telemetry_exporter/file_span.py,sha256=e4hr4D7tC9j4KT7JZBuZU0YxQCdHKADpXNeUNEwggN4,1294
22
+ flock/core/logging/telemetry_exporter/sqllite_span.py,sha256=9bqxHt1mDQGyhKxA9dON5xDi_6FMOfBSdrU_zWV0xv4,2107
23
+ flock/core/mixin/dspy_integration.py,sha256=oT5YfXxPhHkMCuwhXoppBAYBGePUAKse7KebGSM-bq0,6880
24
24
  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
25
+ flock/core/registry/agent_registry.py,sha256=QHdr3Cb-32PEdz8jFCIZSH9OlfpRwAJMtSRpHCWJDq4,4889
26
+ flock/core/tools/basic_tools.py,sha256=nRc1bIz96z-WUTe_yYf9V6EfCPEncl_XnrpGdC7dEmo,8721
27
+ flock/core/tools/dev_tools/github.py,sha256=6ya2_eN-qITV3b_pYP24jQC3X4oZbRY5GKh1AF-9Zic,6836
28
+ flock/core/util/cli_helper.py,sha256=hXBXhTQizZILXiVeBFR6QdAsQm0Qj29ANHJRgiekwyw,859
29
29
  flock/core/util/input_resolver.py,sha256=OesGqX2Dld8myL9Qz04mmxLqoYqOSQC632pj1EMk9Yk,5456
30
30
  flock/core/util/serializable.py,sha256=SymJ0YrjBx48mOBItYSqoRpKuzIc4vKWRS6ScTzre7s,2573
31
31
  flock/themes/3024-day.toml,sha256=uOVHqEzSyHx0WlUk3D0lne4RBsNBAPCTy3C58yU7kEY,667
@@ -365,11 +365,11 @@ flock/themes/zenburned.toml,sha256=UEmquBbcAO3Zj652XKUwCsNoC2iQSlIh-q5c6DH-7Kc,1
365
365
  flock/themes/zenwritten-dark.toml,sha256=To5l6520_3UqAGiEumpzGWsHhXxqu9ThrMildXKgIO0,1669
366
366
  flock/themes/zenwritten-light.toml,sha256=G1iEheCPfBNsMTGaVpEVpDzYBHA_T-MV27rolUYolmE,1666
367
367
  flock/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
368
- flock/workflow/activities.py,sha256=IZM9wOJZWFUzT88nPdmtXkk288nc_FOZsyR2xhA2dWs,4302
368
+ flock/workflow/activities.py,sha256=YEg-Gr8kzVsxWsmsZguIVhX2XwMRvhZ2OlnsJoG5g_A,7646
369
369
  flock/workflow/agent_activities.py,sha256=NhBZscflEf2IMfSRa_pBM_TRP7uVEF_O0ROvWZ33eDc,963
370
370
  flock/workflow/temporal_setup.py,sha256=VWBgmBgfTBjwM5ruS_dVpA5AVxx6EZ7oFPGw4j3m0l0,1091
371
371
  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,,
372
+ flock_core-0.2.3.dist-info/METADATA,sha256=cgqdCwNfYyRzXuhttnph9L7L87YOolVUDh6PYRHCbJ4,11129
373
+ flock_core-0.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
374
+ flock_core-0.2.3.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
375
+ flock_core-0.2.3.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