flock-core 0.2.13__py3-none-any.whl → 0.2.15__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.

flock/__init__.py CHANGED
@@ -1 +1,57 @@
1
1
  """Flock package initialization."""
2
+
3
+ from flock.cli.constants import CLI_THEME_BUILDER
4
+ from flock.core.logging.formatters.theme_builder import theme_builder
5
+
6
+
7
+ def main():
8
+ """Main function."""
9
+ import questionary
10
+ from rich.console import Console
11
+
12
+ from flock.cli.constants import (
13
+ CLI_CREATE_AGENT,
14
+ CLI_CREATE_FLOCK,
15
+ CLI_LOAD_AGENT,
16
+ CLI_LOAD_EXAMPLE,
17
+ CLI_LOAD_FLOCK,
18
+ CLI_SETTINGS,
19
+ CLI_START_ADVANCED_MODE,
20
+ CLI_START_WEB_SERVER,
21
+ )
22
+ from flock.cli.load_flock import load_flock
23
+ from flock.core.util.cli_helper import display_banner
24
+
25
+ console = Console()
26
+
27
+ display_banner()
28
+
29
+ console.print("Flock Management Console\n", style="bold green")
30
+
31
+ result = questionary.select(
32
+ "What do you want to do?",
33
+ choices=[
34
+ questionary.Separator(line=" "),
35
+ # CLI_CREATE_AGENT,
36
+ # CLI_CREATE_FLOCK,
37
+ # CLI_LOAD_AGENT,
38
+ CLI_LOAD_FLOCK,
39
+ # CLI_LOAD_EXAMPLE,
40
+ questionary.Separator(),
41
+ CLI_THEME_BUILDER,
42
+ CLI_SETTINGS,
43
+ questionary.Separator(),
44
+ CLI_START_ADVANCED_MODE,
45
+ CLI_START_WEB_SERVER,
46
+ "Exit",
47
+ ],
48
+ ).ask()
49
+
50
+ if result == CLI_LOAD_FLOCK:
51
+ load_flock()
52
+ if result == CLI_THEME_BUILDER:
53
+ theme_builder()
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
flock/cli/constants.py ADDED
@@ -0,0 +1,23 @@
1
+ """Constants for the CLI module."""
2
+
3
+ CLI_CREATE_AGENT = "Create an agent"
4
+ CLI_CREATE_FLOCK = "Create a flock"
5
+ CLI_LOAD_AGENT = "Load an agent"
6
+ CLI_LOAD_FLOCK = "Load a *.flock file"
7
+ CLI_THEME_BUILDER = "Theme builder"
8
+ CLI_LOAD_EXAMPLE = "Load a example"
9
+ CLI_SETTINGS = "Settings"
10
+ CLI_START_ADVANCED_MODE = "Start advanced mode (coming soon)"
11
+ CLI_START_WEB_SERVER = "Start web server (coming soon)"
12
+ CLI_EXIT = "Exit"
13
+ CLI_CHOICES = [
14
+ CLI_CREATE_AGENT,
15
+ CLI_CREATE_FLOCK,
16
+ CLI_LOAD_AGENT,
17
+ CLI_LOAD_FLOCK,
18
+ CLI_LOAD_EXAMPLE,
19
+ CLI_SETTINGS,
20
+ CLI_START_ADVANCED_MODE,
21
+ CLI_START_WEB_SERVER,
22
+ CLI_EXIT,
23
+ ]
@@ -0,0 +1 @@
1
+ # TODO
@@ -0,0 +1 @@
1
+ # TODO
@@ -0,0 +1 @@
1
+ # TODO
@@ -0,0 +1 @@
1
+ # TODO
@@ -0,0 +1,38 @@
1
+ """Load a Flock from a file."""
2
+
3
+ from pathlib import Path
4
+
5
+ import questionary
6
+ from rich.console import Console
7
+ from rich.markdown import Markdown
8
+
9
+ from flock.core.flock import Flock
10
+
11
+
12
+ def filter(file_path) -> bool:
13
+ """Filter function for file selection."""
14
+ path = Path(file_path)
15
+ if path.is_dir():
16
+ return True
17
+ return path.is_file() and path.suffix == ".flock"
18
+
19
+
20
+ def load_flock():
21
+ """Load a Flock from a file."""
22
+ console = Console()
23
+
24
+ console.print("\nPlease select a *.flock file\n", style="bold green")
25
+
26
+ result = questionary.path("", file_filter=filter).ask()
27
+
28
+ selected_file = Path(result)
29
+ if selected_file.is_file():
30
+ console.print(f"Selected file: {selected_file}", style="bold green")
31
+
32
+ flock = Flock.load_from_file(result)
33
+
34
+ console.line()
35
+ console.print(Markdown("# Flock loaded...."), style="bold orange")
36
+ console.line()
37
+
38
+ flock.run()
flock/cli/settings.py ADDED
@@ -0,0 +1 @@
1
+ # TODO
flock/config.py CHANGED
@@ -44,4 +44,3 @@ TELEMETRY = TelemetryConfig(
44
44
  OTEL_ENABLE_FILE,
45
45
  OTEL_ENABLE_SQL,
46
46
  )
47
- TELEMETRY.setup_tracing()
@@ -1,12 +1,16 @@
1
1
  # src/your_package/core/execution/local_executor.py
2
2
  from flock.core.context.context import FlockContext
3
3
  from flock.core.logging.logging import get_logger
4
- from flock.workflow.activities import run_agent # This should be the local activity function
4
+ from flock.workflow.activities import (
5
+ run_agent, # This should be the local activity function
6
+ )
5
7
 
6
8
  logger = get_logger("flock")
7
9
 
8
10
 
9
- async def run_local_workflow(context: FlockContext, output_formatter, box_result: bool = True) -> dict:
11
+ async def run_local_workflow(
12
+ context: FlockContext, box_result: bool = True
13
+ ) -> dict:
10
14
  """Execute the agent workflow locally (for debugging).
11
15
 
12
16
  Args:
@@ -18,7 +22,7 @@ async def run_local_workflow(context: FlockContext, output_formatter, box_result
18
22
  A dictionary containing the workflow result.
19
23
  """
20
24
  logger.info("Running local debug workflow")
21
- result = await run_agent(context, output_formatter)
25
+ result = await run_agent(context)
22
26
  if box_result:
23
27
  from box import Box
24
28
 
@@ -1,9 +1,7 @@
1
1
  # src/your_package/core/execution/temporal_executor.py
2
- from devtools import pprint
3
2
 
4
3
  from flock.core.context.context import FlockContext
5
4
  from flock.core.context.context_vars import FLOCK_RUN_ID
6
- from flock.core.logging.formatters.formatter_factory import FormatterFactory
7
5
  from flock.core.logging.logging import get_logger
8
6
  from flock.workflow.activities import (
9
7
  run_agent, # Activity function used in Temporal
@@ -16,7 +14,6 @@ logger = get_logger("flock")
16
14
 
17
15
  async def run_temporal_workflow(
18
16
  context: FlockContext,
19
- output_formatter,
20
17
  box_result: bool = True,
21
18
  ) -> dict:
22
19
  """Execute the agent workflow via Temporal for robust, distributed processing.
@@ -43,11 +40,7 @@ async def run_temporal_workflow(
43
40
 
44
41
  agent_name = context.get_variable("FLOCK_CURRENT_AGENT")
45
42
  logger.debug("Formatting Temporal result", agent=agent_name)
46
- if output_formatter:
47
- formatter = FormatterFactory.create_formatter(output_formatter)
48
- formatter.display(result, agent_name, output_formatter.wait_for_input)
49
- else:
50
- pprint(result)
43
+
51
44
  if box_result:
52
45
  from box import Box
53
46
 
flock/core/flock.py CHANGED
@@ -1,19 +1,21 @@
1
1
  """High-level orchestrator for creating and executing agents."""
2
2
 
3
+ import asyncio
4
+ import json
3
5
  import os
4
6
  import uuid
5
- from typing import TypeVar
7
+ from typing import Any, TypeVar
6
8
 
9
+ import cloudpickle
7
10
  from opentelemetry import trace
8
11
  from opentelemetry.baggage import get_baggage, set_baggage
9
12
 
13
+ from flock.config import TELEMETRY
10
14
  from flock.core.context.context import FlockContext
11
15
  from flock.core.context.context_manager import initialize_context
12
16
  from flock.core.execution.local_executor import run_local_workflow
13
17
  from flock.core.execution.temporal_executor import run_temporal_workflow
14
18
  from flock.core.flock_agent import FlockAgent
15
- from flock.core.logging.formatters.base_formatter import FormatterOptions
16
- from flock.core.logging.formatters.pprint_formatter import PrettyPrintFormatter
17
19
  from flock.core.logging.logging import get_logger
18
20
  from flock.core.registry.agent_registry import Registry
19
21
  from flock.core.util.cli_helper import display_banner
@@ -21,7 +23,7 @@ from flock.core.util.input_resolver import top_level_to_keys
21
23
 
22
24
  T = TypeVar("T", bound=FlockAgent)
23
25
  logger = get_logger("flock")
24
-
26
+ TELEMETRY.setup_tracing()
25
27
  tracer = trace.get_tracer(__name__)
26
28
 
27
29
 
@@ -37,9 +39,6 @@ class Flock:
37
39
  model: str = "openai/gpt-4o",
38
40
  local_debug: bool = False,
39
41
  enable_logging: bool = False,
40
- output_formatter: FormatterOptions = FormatterOptions(
41
- PrettyPrintFormatter
42
- ),
43
42
  show_cli_banner: bool = True,
44
43
  ):
45
44
  """Initialize the Flock orchestrator.
@@ -54,9 +53,6 @@ class Flock:
54
53
  span.set_attribute("model", model)
55
54
  span.set_attribute("local_debug", local_debug)
56
55
  span.set_attribute("enable_logging", enable_logging)
57
- span.set_attribute(
58
- "output_formatter", output_formatter.formatter.__name__
59
- )
60
56
  logger.info(
61
57
  "Initializing Flock",
62
58
  model=model,
@@ -77,7 +73,8 @@ class Flock:
77
73
  self.context = FlockContext()
78
74
  self.model = model
79
75
  self.local_debug = local_debug
80
- self.output_formatter = output_formatter
76
+ self.start_agent: FlockAgent | str | None = None
77
+ self.input: dict = {}
81
78
 
82
79
  if local_debug:
83
80
  os.environ["LOCAL_DEBUG"] = "1"
@@ -142,9 +139,173 @@ class Flock:
142
139
  self.registry.register_tool(tool_name, tool)
143
140
  logger.debug("Tool registered successfully")
144
141
 
142
+ def run(
143
+ self,
144
+ start_agent: FlockAgent | str | None = None,
145
+ input: dict = {},
146
+ context: FlockContext = None,
147
+ run_id: str = "",
148
+ box_result: bool = True,
149
+ ) -> dict:
150
+ """Entry point for running an agent system synchronously."""
151
+ return asyncio.run(
152
+ self.run_async(start_agent, input, context, run_id, box_result)
153
+ )
154
+
155
+ def save_to_file(
156
+ self,
157
+ file_path: str,
158
+ start_agent: str | None = None,
159
+ input: dict | None = None,
160
+ ) -> None:
161
+ """Save the Flock instance to a file.
162
+
163
+ This method serializes the Flock instance to a dictionary using the `to_dict()` method and saves it to a file.
164
+ The saved file can be reloaded later using the `from_file()` method.
165
+
166
+ Args:
167
+ file_path (str): The path to the file where the Flock instance should be saved.
168
+ """
169
+ hex_str = cloudpickle.dumps(self).hex()
170
+
171
+ result = {
172
+ "start_agent": start_agent,
173
+ "input": input,
174
+ "flock": hex_str,
175
+ }
176
+
177
+ path = os.path.dirname(file_path)
178
+ if path:
179
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
180
+
181
+ with open(file_path, "w") as file:
182
+ file.write(json.dumps(result))
183
+
184
+ @staticmethod
185
+ def load_from_file(file_path: str) -> "Flock":
186
+ """Load a Flock instance from a file.
187
+
188
+ This class method deserializes a Flock instance from a file that was previously saved using the `save_to_file()`
189
+ method. It reads the file, converts the hexadecimal string back into a Flock instance, and returns it.
190
+
191
+ Args:
192
+ file_path (str): The path to the file containing the serialized Flock instance.
193
+
194
+ Returns:
195
+ Flock: A new Flock instance reconstructed from the saved file.
196
+ """
197
+ with open(file_path) as file:
198
+ json_flock = json.load(file)
199
+ hex_str = json_flock["flock"]
200
+ flock = cloudpickle.loads(bytes.fromhex(hex_str))
201
+ if json_flock["start_agent"]:
202
+ agent = flock.registry.get_agent(json_flock["start_agent"])
203
+ flock.start_agent = agent
204
+ if json_flock["input"]:
205
+ flock.input = json_flock["input"]
206
+ return flock
207
+
208
+ def to_dict(self) -> dict[str, Any]:
209
+ """Serialize the FlockAgent instance to a dictionary.
210
+
211
+ This method converts the entire agent instance—including its configuration, state, and lifecycle hooks—
212
+ into a dictionary format. It uses cloudpickle to serialize any callable objects (such as functions or
213
+ methods), converting them into hexadecimal string representations. This ensures that the agent can be
214
+ easily persisted, transmitted, or logged as JSON.
215
+
216
+ The serialization process is recursive:
217
+ - If a field is a callable (and not a class), it is serialized using cloudpickle.
218
+ - Lists and dictionaries are processed recursively to ensure that all nested callables are properly handled.
219
+
220
+ **Returns:**
221
+ dict[str, Any]: A dictionary representing the FlockAgent, which includes all of its configuration data.
222
+ This dictionary is suitable for storage, debugging, or transmission over the network.
223
+
224
+ **Example:**
225
+ For an agent defined as:
226
+ name = "idea_agent",
227
+ model = "openai/gpt-4o",
228
+ input = "query: str | The search query, context: dict | The full conversation context",
229
+ output = "idea: str | The generated idea"
230
+ Calling `agent.to_dict()` might produce:
231
+ {
232
+ "name": "idea_agent",
233
+ "model": "openai/gpt-4o",
234
+ "input": "query: str | The search query, context: dict | The full conversation context",
235
+ "output": "idea: str | The generated idea",
236
+ "tools": ["<serialized tool representation>"],
237
+ "use_cache": False,
238
+ "hand_off": None,
239
+ "termination": None,
240
+ ...
241
+ }
242
+ """
243
+
244
+ def convert_callable(obj: Any) -> Any:
245
+ if callable(obj) and not isinstance(obj, type):
246
+ return cloudpickle.dumps(obj).hex()
247
+ if isinstance(obj, list):
248
+ return [convert_callable(item) for item in obj]
249
+ if isinstance(obj, dict):
250
+ return {k: convert_callable(v) for k, v in obj.items()}
251
+ return obj
252
+
253
+ data = self.model_dump()
254
+ return convert_callable(data)
255
+
256
+ @classmethod
257
+ def from_dict(cls: type[T], data: dict[str, Any]) -> T:
258
+ """Deserialize a FlockAgent instance from a dictionary.
259
+
260
+ This class method reconstructs a FlockAgent from its serialized dictionary representation, as produced
261
+ by the `to_dict()` method. It recursively processes the dictionary to convert any serialized callables
262
+ (stored as hexadecimal strings via cloudpickle) back into executable callable objects.
263
+
264
+ **Arguments:**
265
+ data (dict[str, Any]): A dictionary representation of a FlockAgent, typically produced by `to_dict()`.
266
+ The dictionary should contain all configuration fields and state information necessary to fully
267
+ reconstruct the agent.
268
+
269
+ **Returns:**
270
+ FlockAgent: An instance of FlockAgent reconstructed from the provided dictionary. The deserialized agent
271
+ will have the same configuration, state, and behavior as the original instance.
272
+
273
+ **Example:**
274
+ Suppose you have the following dictionary:
275
+ {
276
+ "name": "idea_agent",
277
+ "model": "openai/gpt-4o",
278
+ "input": "query: str | The search query, context: dict | The full conversation context",
279
+ "output": "idea: str | The generated idea",
280
+ "tools": ["<serialized tool representation>"],
281
+ "use_cache": False,
282
+ "hand_off": None,
283
+ "termination": None,
284
+ ...
285
+ }
286
+ Then, calling:
287
+ agent = FlockAgent.from_dict(data)
288
+ will return a FlockAgent instance with the same properties and behavior as when it was originally serialized.
289
+ """
290
+
291
+ def convert_callable(obj: Any) -> Any:
292
+ if isinstance(obj, str) and len(obj) > 2:
293
+ try:
294
+ return cloudpickle.loads(bytes.fromhex(obj))
295
+ except Exception:
296
+ return obj
297
+ if isinstance(obj, list):
298
+ return [convert_callable(item) for item in obj]
299
+ if isinstance(obj, dict):
300
+ return {k: convert_callable(v) for k, v in obj.items()}
301
+ return obj
302
+
303
+ converted = convert_callable(data)
304
+ return cls(**converted)
305
+
145
306
  async def run_async(
146
307
  self,
147
- start_agent: FlockAgent | str,
308
+ start_agent: FlockAgent | str | None = None,
148
309
  input: dict = {},
149
310
  context: FlockContext = None,
150
311
  run_id: str = "",
@@ -180,68 +341,70 @@ class Flock:
180
341
  if hasattr(start_agent, "name")
181
342
  else start_agent,
182
343
  )
344
+ if start_agent:
345
+ self.start_agent = start_agent
346
+ if input:
347
+ self.input = input
183
348
 
184
- span.set_attribute("input", str(input))
349
+ span.set_attribute("input", str(self.input))
185
350
  span.set_attribute("context", str(context))
186
351
  span.set_attribute("run_id", run_id)
187
352
  span.set_attribute("box_result", box_result)
188
353
 
189
354
  try:
190
- if isinstance(start_agent, str):
355
+ if isinstance(self.start_agent, str):
191
356
  logger.debug(
192
- "Looking up agent by name", agent_name=start_agent
357
+ "Looking up agent by name", agent_name=self.start_agent
193
358
  )
194
- start_agent = self.registry.get_agent(start_agent)
195
- if not start_agent:
196
- logger.error("Agent not found", agent_name=start_agent)
359
+ self.start_agent = self.registry.get_agent(self.start_agent)
360
+ if not self.start_agent:
361
+ logger.error(
362
+ "Agent not found", agent_name=self.start_agent
363
+ )
197
364
  raise ValueError(
198
- f"Agent '{start_agent}' not found in registry"
365
+ f"Agent '{self.start_agent}' not found in registry"
199
366
  )
200
- start_agent.resolve_callables(context=self.context)
367
+ self.start_agent.resolve_callables(context=self.context)
201
368
  if context:
202
369
  logger.debug("Using provided context")
203
370
  self.context = context
204
371
  if not run_id:
205
- run_id = f"{start_agent.name}_{uuid.uuid4().hex[:4]}"
372
+ run_id = f"{self.start_agent.name}_{uuid.uuid4().hex[:4]}"
206
373
  logger.debug("Generated run ID", run_id=run_id)
207
374
 
208
375
  set_baggage("run_id", run_id)
209
376
 
210
377
  # TODO - Add a check for required input keys
211
- input_keys = top_level_to_keys(start_agent.input)
378
+ input_keys = top_level_to_keys(self.start_agent.input)
212
379
  for key in input_keys:
213
380
  if key.startswith("flock."):
214
381
  key = key[6:] # Remove the "flock." prefix
215
- if key not in input:
382
+ if key not in self.input:
216
383
  from rich.prompt import Prompt
217
384
 
218
- input[key] = Prompt.ask(
219
- f"Please enter {key} for {start_agent.name}"
385
+ self.input[key] = Prompt.ask(
386
+ f"Please enter {key} for {self.start_agent.name}"
220
387
  )
221
388
 
222
389
  # Initialize the context with standardized variables
223
390
  initialize_context(
224
391
  self.context,
225
- start_agent.name,
226
- input,
392
+ self.start_agent.name,
393
+ self.input,
227
394
  run_id,
228
395
  self.local_debug,
229
396
  )
230
397
 
231
398
  logger.info(
232
399
  "Starting agent execution",
233
- agent=start_agent.name,
400
+ agent=self.start_agent.name,
234
401
  local_debug=self.local_debug,
235
402
  )
236
403
 
237
404
  if self.local_debug:
238
- return await run_local_workflow(
239
- self.context, self.output_formatter, box_result
240
- )
405
+ return await run_local_workflow(self.context, box_result)
241
406
  else:
242
- return await run_temporal_workflow(
243
- self.context, self.output_formatter, box_result
244
- )
407
+ return await run_temporal_workflow(self.context, box_result)
245
408
  except Exception as e:
246
409
  logger.exception("Execution failed", error=str(e))
247
410
  raise