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/core/flock_agent.py CHANGED
@@ -1,14 +1,21 @@
1
1
  """FlockAgent is the core, declarative base class for all agents in the Flock framework."""
2
2
 
3
+ import asyncio
4
+ import json
5
+ import os
3
6
  from abc import ABC
4
7
  from collections.abc import Awaitable, Callable
5
8
  from dataclasses import dataclass, field
6
- from typing import Any, Literal, Union
9
+ from typing import Any, TypeVar, Union
7
10
 
8
11
  import cloudpickle
9
12
  from pydantic import BaseModel, Field
10
13
 
11
14
  from flock.core.context.context import FlockContext
15
+ from flock.core.logging.formatters.themed_formatter import (
16
+ ThemedAgentResultFormatter,
17
+ )
18
+ from flock.core.logging.formatters.themes import OutputTheme
12
19
  from flock.core.logging.logging import get_logger
13
20
  from flock.core.mixin.dspy_integration import AgentType, DSPyIntegrationMixin
14
21
  from flock.core.mixin.prompt_parser import PromptParserMixin
@@ -21,6 +28,9 @@ from opentelemetry import trace
21
28
  tracer = trace.get_tracer(__name__)
22
29
 
23
30
 
31
+ T = TypeVar("T", bound="FlockAgent")
32
+
33
+
24
34
  @dataclass
25
35
  class FlockAgentConfig:
26
36
  """Configuration options for a FlockAgent."""
@@ -34,13 +44,31 @@ class FlockAgentConfig:
34
44
  disable_output: bool = field(
35
45
  default=False, metadata={"description": "Disables the agent's output."}
36
46
  )
37
- save_to_file: bool = field(
38
- default=False,
39
- metadata={
40
- "description": "Saves the serialized agent to a file every time it gets serialized."
41
- },
47
+ temperature: float = field(
48
+ default=0.0, metadata={"description": "Temperature for the LLM"}
49
+ )
50
+ max_tokens: int = field(
51
+ default=2000, metadata={"description": "Max tokens for the LLM"}
52
+ )
53
+
54
+
55
+ @dataclass
56
+ class FlockAgentOutputConfig:
57
+ """Configuration options for a FlockAgent."""
58
+
59
+ render_table: bool = field(
60
+ default=False, metadata={"description": "Renders a table."}
61
+ )
62
+ theme: OutputTheme = field( # type: ignore
63
+ default=OutputTheme.afterglow,
64
+ metadata={"description": "Disables the agent's output."},
65
+ )
66
+ max_length: int = field(
67
+ default=1000, metadata={"description": "Disables the agent's output."}
68
+ )
69
+ wait_for_input: bool = field(
70
+ default=False, metadata={"description": "Wait for input."}
42
71
  )
43
- data_type: Literal["json", "cloudpickle", "msgpack"] = "cloudpickle"
44
72
 
45
73
 
46
74
  @dataclass
@@ -181,6 +209,11 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
181
209
  description="Configuration options for the agent, such as serialization settings.",
182
210
  )
183
211
 
212
+ output_config: FlockAgentOutputConfig = Field(
213
+ default_factory=FlockAgentOutputConfig,
214
+ description="Configuration options for the agent's output.",
215
+ )
216
+
184
217
  # Lifecycle callback fields: if provided, these callbacks are used instead of overriding the methods.
185
218
  initialize_callback: Callable[[dict[str, Any]], Awaitable[None]] | None = (
186
219
  Field(
@@ -188,6 +221,12 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
188
221
  description="Optional callback function for initialization. If provided, this async function is called with the inputs.",
189
222
  )
190
223
  )
224
+ evaluate_callback: (
225
+ Callable[[dict[str, Any]], Awaitable[dict[str, Any]]] | None
226
+ ) = Field(
227
+ default=None,
228
+ description="Optional callback function for evaluate. If provided, this async function is called with the inputs instead of the internal evaluate",
229
+ )
191
230
  terminate_callback: (
192
231
  Callable[[dict[str, Any], dict[str, Any]], Awaitable[None]] | None
193
232
  ) = Field(
@@ -296,6 +335,8 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
296
335
  with tracer.start_as_current_span("agent.evaluate") as span:
297
336
  span.set_attribute("agent.name", self.name)
298
337
  span.set_attribute("inputs", str(inputs))
338
+ if self.evaluate_callback is not None:
339
+ return await self.evaluate_callback(self, inputs)
299
340
  try:
300
341
  # Create and configure the signature and language model.
301
342
  self.__dspy_signature = self.create_dspy_signature_class(
@@ -323,7 +364,33 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
323
364
  span.record_exception(eval_error)
324
365
  raise
325
366
 
326
- async def run(self, inputs: dict[str, Any]) -> dict[str, Any]:
367
+ def save_to_file(self, file_path: str | None = None) -> None:
368
+ """Save the serialized agent to a file."""
369
+ if file_path is None:
370
+ file_path = f"{self.name}.json"
371
+ dict_data = self.to_dict()
372
+
373
+ # create all needed directories
374
+ path = os.path.dirname(file_path)
375
+ if path:
376
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
377
+
378
+ with open(file_path, "w") as file:
379
+ file.write(json.dumps(dict_data))
380
+
381
+ @classmethod
382
+ def load_from_file(cls: type[T], file_path: str) -> T:
383
+ """Load a serialized agent from a file."""
384
+ with open(file_path) as file:
385
+ data = json.load(file)
386
+ # Fallback: use the current class.
387
+ return cls.from_dict(data)
388
+
389
+ def run(self, inputs: dict[str, Any]) -> dict[str, Any]:
390
+ """Run the agent with the given inputs and return its generated output."""
391
+ return asyncio.run(self.run_async(inputs))
392
+
393
+ async def run_async(self, inputs: dict[str, Any]) -> dict[str, Any]:
327
394
  """Run the agent with the given inputs and return its generated output.
328
395
 
329
396
  This method represents the primary execution flow for a FlockAgent and performs the following
@@ -375,6 +442,7 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
375
442
  try:
376
443
  await self.initialize(inputs)
377
444
  result = await self.evaluate(inputs)
445
+ self.display_output(result)
378
446
  await self.terminate(inputs, result)
379
447
  span.set_attribute("result", str(result))
380
448
  logger.info("Agent run completed", agent=self.name)
@@ -387,6 +455,18 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
387
455
  span.record_exception(run_error)
388
456
  raise
389
457
 
458
+ def display_info(self) -> None:
459
+ pass
460
+
461
+ def display_output(self, result: dict[str, Any]) -> None:
462
+ """Display the agent's output using the configured output formatter."""
463
+ ThemedAgentResultFormatter(
464
+ self.output_config.theme,
465
+ self.output_config.max_length,
466
+ self.output_config.render_table,
467
+ self.output_config.wait_for_input,
468
+ ).display_result(result, self.name)
469
+
390
470
  async def run_temporal(self, inputs: dict[str, Any]) -> dict[str, Any]:
391
471
  """Execute this agent via a Temporal workflow for enhanced fault tolerance and asynchronous processing.
392
472
 
@@ -526,7 +606,7 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
526
606
  return convert_callable(data)
527
607
 
528
608
  @classmethod
529
- def from_dict(cls, data: dict[str, Any]) -> "FlockAgent":
609
+ def from_dict(cls: type[T], data: dict[str, Any]) -> T:
530
610
  """Deserialize a FlockAgent instance from a dictionary.
531
611
 
532
612
  This class method reconstructs a FlockAgent from its serialized dictionary representation, as produced
@@ -0,0 +1,38 @@
1
+ """Enum Builder."""
2
+
3
+ import os
4
+ import pathlib
5
+ import re
6
+
7
+ theme_folder = pathlib.Path(__file__).parent.parent.parent.parent / "themes"
8
+
9
+ if not theme_folder.exists():
10
+ raise FileNotFoundError(f"Theme folder not found: {theme_folder}")
11
+
12
+ theme_files = [
13
+ pathlib.Path(f.path).stem for f in os.scandir(theme_folder) if f.is_file()
14
+ ]
15
+
16
+ theme_enum_entries = {}
17
+ for theme in theme_files:
18
+ safe_name = (
19
+ theme.replace("-", "_")
20
+ .replace(" ", "_")
21
+ .replace("(", "_")
22
+ .replace(")", "_")
23
+ .replace("+", "_")
24
+ .replace(".", "_")
25
+ )
26
+
27
+ if re.match(r"^\d", safe_name):
28
+ safe_name = f"_{safe_name}"
29
+
30
+ theme_enum_entries[safe_name] = theme
31
+
32
+ with open("theme_enum.py", "w") as f:
33
+ f.write("from enum import Enum\n\n")
34
+ f.write("class OutputOptionsTheme(Enum):\n")
35
+ for safe_name, original_name in theme_enum_entries.items():
36
+ f.write(f' {safe_name} = "{original_name}"\n')
37
+
38
+ print("Generated theme_enum.py ✅")
@@ -371,7 +371,7 @@ def save_theme(theme: dict, filename: pathlib.Path) -> None:
371
371
  # --- Main Interactive Loop --- #
372
372
 
373
373
 
374
- def main():
374
+ def theme_builder():
375
375
  console = Console(force_terminal=True, color_system="truecolor")
376
376
  themes_dir = pathlib.Path(__file__).parent.parent.parent.parent / "themes"
377
377
  theme_files = load_theme_files(themes_dir)
@@ -458,7 +458,7 @@ def main():
458
458
  )
459
459
  if sel2.lower() == "r":
460
460
  console.print("Regenerating samples...")
461
- main() # restart the builder
461
+ theme_builder() # restart the builder
462
462
  return
463
463
  try:
464
464
  sel2 = int(sel2)
@@ -474,7 +474,3 @@ def main():
474
474
  save_path = themes_dir / filename
475
475
  save_theme(chosen_sample_theme, save_path)
476
476
  console.print(f"\n[green]Theme saved as {save_path}.[/green]")
477
-
478
-
479
- if __name__ == "__main__":
480
- main()
@@ -5,16 +5,19 @@ import random
5
5
  import re
6
6
  from typing import Any
7
7
 
8
- from devtools import pprint
9
8
  from temporalio import workflow
10
9
 
11
- from flock.core.logging.formatters.base_formatter import BaseFormatter
10
+ from flock.core.logging.formatters.themes import OutputTheme
12
11
 
13
12
  with workflow.unsafe.imports_passed_through():
13
+ from pygments.style import Style
14
+ from pygments.token import Token
14
15
  from rich import box
15
16
  from rich.console import Console, Group
16
17
  from rich.panel import Panel
18
+ from rich.syntax import PygmentsSyntaxTheme, Syntax
17
19
  from rich.table import Table
20
+ from rich.theme import Theme
18
21
 
19
22
  import toml # install with: pip install toml
20
23
 
@@ -335,18 +338,101 @@ def create_rich_renderable(
335
338
  return s
336
339
 
337
340
 
338
- class ThemedAgentResultFormatter(BaseFormatter):
341
+ def load_syntax_theme_from_file(filepath: str) -> dict:
342
+ """Load a syntax highlighting theme from a TOML file and map it to Rich styles."""
343
+ with open(filepath) as f:
344
+ theme = toml.load(f)
345
+
346
+ if "colors" not in theme:
347
+ raise ValueError(
348
+ f"Theme file {filepath} does not contain a 'colors' section."
349
+ )
350
+
351
+ # Map theme colors to syntax categories
352
+ syntax_theme = {
353
+ "background": theme["colors"]["primary"].get("background", "#161719"),
354
+ "text": theme["colors"]["primary"].get("foreground", "#c5c8c6"),
355
+ "comment": theme["colors"]["normal"].get("black", "#666666"),
356
+ "keyword": theme["colors"]["bright"].get("magenta", "#ff79c6"),
357
+ "builtin": theme["colors"]["bright"].get("cyan", "#8be9fd"),
358
+ "string": theme["colors"]["bright"].get("green", "#50fa7b"),
359
+ "name": theme["colors"]["bright"].get("blue", "#6272a4"),
360
+ "number": theme["colors"]["bright"].get("yellow", "#f1fa8c"),
361
+ "operator": theme["colors"]["bright"].get("red", "#ff5555"),
362
+ "punctuation": theme["colors"]["normal"].get("white", "#bbbbbb"),
363
+ "error": theme["colors"]["bright"].get("red", "#ff5555"),
364
+ }
365
+
366
+ return syntax_theme
367
+
368
+
369
+ def create_rich_syntax_theme(syntax_theme: dict) -> Theme:
370
+ """Convert a syntax theme dict to a Rich-compatible Theme."""
371
+ return Theme(
372
+ {
373
+ "background": f"on {syntax_theme['background']}",
374
+ "text": syntax_theme["text"],
375
+ "keyword": f"bold {syntax_theme['keyword']}",
376
+ "builtin": f"bold {syntax_theme['builtin']}",
377
+ "string": syntax_theme["string"],
378
+ "name": syntax_theme["name"],
379
+ "number": syntax_theme["number"],
380
+ "operator": syntax_theme["operator"],
381
+ "punctuation": syntax_theme["punctuation"],
382
+ "error": f"bold {syntax_theme['error']}",
383
+ }
384
+ )
385
+
386
+
387
+ def create_pygments_syntax_theme(syntax_theme: dict) -> PygmentsSyntaxTheme:
388
+ """Convert a syntax theme dict to a Pygments-compatible Rich syntax theme."""
389
+
390
+ class CustomSyntaxStyle(Style):
391
+ """Dynamically generated Pygments style based on the loaded theme."""
392
+
393
+ background_color = syntax_theme["background"]
394
+ styles = {
395
+ Token.Text: syntax_theme["text"],
396
+ Token.Comment: f"italic {syntax_theme['comment']}",
397
+ Token.Keyword: f"bold {syntax_theme['keyword']}",
398
+ Token.Name.Builtin: f"bold {syntax_theme['builtin']}",
399
+ Token.String: syntax_theme["string"],
400
+ Token.Name: syntax_theme["name"],
401
+ Token.Number: syntax_theme["number"],
402
+ Token.Operator: syntax_theme["operator"],
403
+ Token.Punctuation: syntax_theme["punctuation"],
404
+ Token.Error: f"bold {syntax_theme['error']}",
405
+ }
406
+
407
+ return PygmentsSyntaxTheme(CustomSyntaxStyle)
408
+
409
+
410
+ class ThemedAgentResultFormatter:
339
411
  """Formats agent results in a Rich table with nested subtables and theme support."""
340
412
 
341
- def __init__(self, theme: str = "atom", max_length: int = -1):
413
+ def __init__(
414
+ self,
415
+ theme: OutputTheme = OutputTheme.afterglow,
416
+ max_length: int = -1,
417
+ render_table: bool = True,
418
+ wait_for_input: bool = False,
419
+ ):
342
420
  """Initialize the formatter with a theme and optional max length."""
343
421
  self.theme = theme
344
422
  self.styles = None
345
423
  self.max_length = max_length
424
+ self.render_table = render_table
425
+ self.wait_for_input = wait_for_input
346
426
 
347
427
  def format_result(
348
- self, result: dict[str, Any], agent_name: str, theme, styles
428
+ self,
429
+ result: dict[str, Any],
430
+ agent_name: str,
431
+ theme,
432
+ styles,
349
433
  ) -> Panel:
434
+ from devtools import pformat
435
+
350
436
  """Format an agent's result as a Rich Panel containing a table."""
351
437
  box_style = (
352
438
  getattr(box, styles["table_box"])
@@ -394,14 +480,32 @@ class ThemedAgentResultFormatter(BaseFormatter):
394
480
  )
395
481
  table.add_row(key, rich_renderable)
396
482
 
397
- return Panel(
398
- table,
399
- title="🐤🐧🐓🦆",
400
- title_align=styles["panel_title_align"],
401
- border_style=styles["panel_border_style"],
402
- padding=styles["panel_padding"],
403
- style=styles["panel_style"],
404
- )
483
+ s = pformat(result, highlight=False)
484
+
485
+ if self.render_table:
486
+ return Panel(
487
+ table,
488
+ title="🐤🐧🐓🦆",
489
+ title_align=styles["panel_title_align"],
490
+ border_style=styles["panel_border_style"],
491
+ padding=styles["panel_padding"],
492
+ style=styles["panel_style"],
493
+ )
494
+ else:
495
+ syntax = Syntax(
496
+ s, # The formatted string
497
+ "python", # Highlight as Python (change this for other formats)
498
+ theme=self.syntax_style, # Choose a Rich theme (matches your color setup)
499
+ line_numbers=False,
500
+ )
501
+ return Panel(
502
+ syntax,
503
+ title=agent_name,
504
+ title_align=styles["panel_title_align"],
505
+ border_style=styles["panel_border_style"],
506
+ padding=styles["panel_padding"],
507
+ style=styles["panel_style"],
508
+ )
405
509
 
406
510
  def display_result(self, result: dict[str, Any], agent_name: str) -> None:
407
511
  """Print an agent's result using Rich formatting."""
@@ -410,7 +514,11 @@ class ThemedAgentResultFormatter(BaseFormatter):
410
514
  pathlib.Path(__file__).parent.parent.parent.parent / "themes"
411
515
  )
412
516
  all_themes = list(themes_dir.glob("*.toml"))
413
- theme = theme + ".toml" if not theme.endswith(".toml") else theme
517
+ theme = (
518
+ theme.value + ".toml"
519
+ if not theme.value.endswith(".toml")
520
+ else theme.value
521
+ )
414
522
  theme = (
415
523
  pathlib.Path(__file__).parent.parent.parent.parent
416
524
  / "themes"
@@ -426,6 +534,9 @@ class ThemedAgentResultFormatter(BaseFormatter):
426
534
 
427
535
  styles = get_default_styles(theme_dict)
428
536
  self.styles = styles
537
+ self.syntax_style = create_pygments_syntax_theme(
538
+ load_syntax_theme_from_file(theme)
539
+ )
429
540
 
430
541
  console = Console()
431
542
  panel = self.format_result(
@@ -435,8 +546,5 @@ class ThemedAgentResultFormatter(BaseFormatter):
435
546
  styles=styles,
436
547
  )
437
548
  console.print(panel)
438
-
439
- @staticmethod
440
- def display_data(data: dict[str, Any]) -> None:
441
- """Print agent data using Rich formatting."""
442
- pprint(data)
549
+ if self.wait_for_input:
550
+ console.input(prompt="Press Enter to continue...")