flock-core 0.3.5__py3-none-any.whl → 0.3.8__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,4 +1,3 @@
1
-
2
1
  # Flock v0.3 - Hummingbird
3
2
 
4
3
  We're excited to announce Flock v0.3, codenamed **"Hummingbird"**! This release brings a fundamental redesign of Flock's core architecture, introducing **unprecedented modularity and flexibility** to AI agent development.
@@ -13,17 +12,17 @@ Like a hummingbird, modules are small and nimble code packages. Put enough of th
13
12
 
14
13
  ### Other notable additions:
15
14
  - **CLI Interface** – Flock now has a command-line interface
15
+ - **REST API Server** – Expose your agents via HTTP endpoints
16
16
  - **Color-coded logging** – Better debugging experience
17
17
  - **New examples**
18
18
  - ...and much more!
19
19
 
20
20
  ---
21
21
 
22
- ## Core Changes
22
+ ## Core Changes
23
23
 
24
- ### New Module System
25
- - **Complete redesign** of the module architecture
26
- - Simple yet powerful lifecycle hooks: `initialize`, `pre_evaluate`, `post_evaluate`, `terminate`
24
+ ### New Module System
25
+ - **Pluggable modules system á la FastAPI**
27
26
  - **Easy-to-implement** module interface
28
27
  - **Configuration system** for clean parameter management
29
28
 
@@ -32,15 +31,44 @@ Like a hummingbird, modules are small and nimble code packages. Put enough of th
32
31
  - Built-in support for multiple evaluation strategies:
33
32
  - **Declarative Evaluator** – The default way Flock is designed
34
33
  - **Natural Language Evaluator** – Use "classic" prompting
34
+ - **Zep Evaluator** – Add or query data
35
35
  - **Easily extendable** with custom evaluation approaches
36
36
 
37
- ### FlockFactory
37
+ ### New Router System
38
+ - **Pluggable router system** for dynamic agent chaining
39
+ - Built-in support for multiple routing strategies:
40
+ - **Default Router** – Uses the agent's hand_off property
41
+ - **LLM Router** – Uses an LLM to determine the next agent
42
+ - **Agent Router** – Uses a dedicated agent to make routing decisions
43
+ - **Easily extendable** with custom routing approaches
44
+
45
+ ### REST API Server
46
+ - **FastAPI-based** HTTP server for exposing agents
47
+ - **Synchronous and asynchronous** execution modes
48
+ - **Run status tracking** with unique run IDs
49
+ - **Agent discovery** endpoint to list available agents
50
+ - **Simple integration** with existing Flock instances
51
+
52
+ ### Auto-Handoff Feature
53
+ - **Dynamic agent chaining** without explicit handoff definitions
54
+ - **LLM-powered routing** to determine the best next agent
55
+ - **Emergent behavior** in multi-agent systems
56
+ - **Simple to use** with the "auto_handoff" string value
57
+
58
+ ### New high end examples like the Repository Analyzer
59
+ - **Automatic documentation generation** for any codebase
60
+ - **Rule-based version** using custom evaluators
61
+ - **LLM-based version** for more flexible and powerful analysis
62
+ - **Comprehensive documentation** including overview, architecture, components, and more
63
+
64
+ ### FlockFactory
38
65
  - Provides **pre-configured agents**, so you don't have to manage modules and evaluators manually!
39
66
 
40
67
  ### Built-in Modules
41
68
  - **Memory Module** – Persistent agent memory
42
69
  - **Output Module** – Advanced output formatting and storage
43
70
  - **Metrics Module** – Detailed performance tracking
71
+ - **Zep Module** – Uses Zep for Knowledge Graphs
44
72
 
45
73
  ---
46
74
 
@@ -99,7 +127,7 @@ See? **Basically nothing changed!** Just more modular and flexible.
99
127
 
100
128
  ---
101
129
 
102
- ## 🛠 Installation
130
+ ## Installation
103
131
 
104
132
  ```bash
105
133
  pip install flock-core>=0.3.0
@@ -107,5 +135,5 @@ pip install flock-core>=0.3.0
107
135
 
108
136
  ---
109
137
 
110
- **Full documentation**: [docs.flock.ai](https://docs.flock.ai)
111
- **GitHub**: [github.com/flock-ai](https://github.com/flock-ai)
138
+ **Full documentation**: [https://whiteducksoftware.github.io/flock](https://whiteducksoftware.github.io/flock)
139
+ **GitHub**: [https://github.com/whiteducksoftware/flock](https://github.com/whiteducksoftware/flock)
@@ -7,7 +7,7 @@ from opentelemetry import trace
7
7
 
8
8
  from flock.core.context.context_vars import FLOCK_LAST_AGENT, FLOCK_LAST_RESULT
9
9
  from flock.core.logging.logging import get_logger
10
- from flock.core.util.serializable import Serializable
10
+ from flock.core.serialization.serializable import Serializable
11
11
 
12
12
  logger = get_logger("context")
13
13
  tracer = trace.get_tracer(__name__)
flock/core/flock_agent.py CHANGED
@@ -5,15 +5,15 @@ import json
5
5
  import os
6
6
  from abc import ABC
7
7
  from collections.abc import Callable
8
- from typing import Any, TypeVar, Union
8
+ from typing import Any, TypeVar
9
9
 
10
10
  import cloudpickle
11
11
  from opentelemetry import trace
12
12
  from pydantic import BaseModel, Field
13
13
 
14
- from flock.core.context.context import FlockContext
15
14
  from flock.core.flock_evaluator import FlockEvaluator
16
15
  from flock.core.flock_module import FlockModule
16
+ from flock.core.flock_router import FlockRouter
17
17
  from flock.core.logging.logging import get_logger
18
18
 
19
19
  logger = get_logger("agent")
@@ -23,21 +23,6 @@ tracer = trace.get_tracer(__name__)
23
23
  T = TypeVar("T", bound="FlockAgent")
24
24
 
25
25
 
26
- class HandOff(BaseModel):
27
- """Base class for handoff returns."""
28
-
29
- next_agent: Union[str, "FlockAgent"] = Field(
30
- default="", description="Next agent to invoke"
31
- )
32
- input: dict[str, Any] = Field(
33
- default_factory=dict,
34
- description="Input data for the next agent",
35
- )
36
- context: FlockContext = Field(
37
- default=None, descrio="Override context parameters"
38
- )
39
-
40
-
41
26
  class FlockAgent(BaseModel, ABC):
42
27
  name: str = Field(..., description="Unique identifier for the agent.")
43
28
  model: str | None = Field(
@@ -72,12 +57,9 @@ class FlockAgent(BaseModel, ABC):
72
57
  description="Set to True to enable caching of the agent's results.",
73
58
  )
74
59
 
75
- hand_off: str | HandOff | Callable[..., HandOff] | None = Field(
76
- None,
77
- description=(
78
- "Specifies the next agent in the workflow or a callable that determines the handoff. "
79
- "This allows chaining of agents."
80
- ),
60
+ handoff_router: FlockRouter | None = Field(
61
+ default=None,
62
+ description="Router to use for determining the next agent in the workflow.",
81
63
  )
82
64
 
83
65
  evaluator: FlockEvaluator = Field(
@@ -3,14 +3,17 @@
3
3
  from collections.abc import Callable
4
4
  from typing import Any
5
5
 
6
- from flock.core.flock_agent import FlockAgent, HandOff
6
+ from flock.core.flock_agent import FlockAgent
7
7
  from flock.core.logging.formatters.themes import OutputTheme
8
8
  from flock.evaluators.declarative.declarative_evaluator import (
9
9
  DeclarativeEvaluator,
10
10
  DeclarativeEvaluatorConfig,
11
11
  )
12
- from flock.modules.performance.metrics_module import MetricsModule, MetricsModuleConfig
13
12
  from flock.modules.output.output_module import OutputModule, OutputModuleConfig
13
+ from flock.modules.performance.metrics_module import (
14
+ MetricsModule,
15
+ MetricsModuleConfig,
16
+ )
14
17
 
15
18
 
16
19
  class FlockFactory:
@@ -24,7 +27,6 @@ class FlockFactory:
24
27
  input: str | Callable[..., str] | None = None,
25
28
  output: str | Callable[..., str] | None = None,
26
29
  tools: list[Callable[..., Any] | Any] | None = None,
27
- hand_off: str | HandOff | Callable[..., HandOff] | None = None,
28
30
  use_cache: bool = True,
29
31
  enable_rich_tables: bool = False,
30
32
  output_theme: OutputTheme = OutputTheme.abernathy,
@@ -53,7 +55,6 @@ class FlockFactory:
53
55
  input=input,
54
56
  output=output,
55
57
  tools=tools,
56
- hand_off=hand_off,
57
58
  model=model,
58
59
  description=description,
59
60
  evaluator=evaluator,
@@ -65,7 +66,9 @@ class FlockFactory:
65
66
  )
66
67
  output_module = OutputModule("output", config=output_config)
67
68
 
68
- metrics_config = MetricsModuleConfig(latency_threshold_ms=alert_latency_threshold_ms)
69
+ metrics_config = MetricsModuleConfig(
70
+ latency_threshold_ms=alert_latency_threshold_ms
71
+ )
69
72
  metrics_module = MetricsModule("metrics", config=metrics_config)
70
73
 
71
74
  agent.add_module(output_module)
@@ -43,7 +43,9 @@ class FlockModule(BaseModel, ABC):
43
43
  2. Using FlockModuleConfig.with_fields() to create a config class
44
44
  """
45
45
 
46
- name: str = Field(..., description="Unique identifier for the module")
46
+ name: str = Field(
47
+ default="", description="Unique identifier for the module"
48
+ )
47
49
  config: FlockModuleConfig = Field(
48
50
  default_factory=FlockModuleConfig, description="Module configuration"
49
51
  )
@@ -0,0 +1,70 @@
1
+ """Base router class for the Flock framework."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from flock.core.context.context import FlockContext
9
+
10
+
11
+ class HandOffRequest(BaseModel):
12
+ """Base class for handoff returns."""
13
+
14
+ next_agent: str = Field(default="", description="Next agent to invoke")
15
+ # match = use the output fields of the current agent that also exists as input field of the next agent
16
+ # add = add the output of the current agent to the input of the next agent
17
+ hand_off_mode: Literal["match", "add"] = Field(default="match")
18
+ override_next_agent: Any | None = Field(
19
+ default=None,
20
+ description="Override the next agent to hand off to",
21
+ )
22
+ override_context: FlockContext | None = Field(
23
+ default=None, descrio="Override context parameters"
24
+ )
25
+
26
+
27
+ class FlockRouterConfig(BaseModel):
28
+ """Configuration for a router.
29
+
30
+ This class defines the configuration parameters for a router.
31
+ Subclasses can extend this to add additional parameters.
32
+ """
33
+
34
+ enabled: bool = Field(
35
+ default=True, description="Whether the router is enabled"
36
+ )
37
+ agents: list[str] | None = Field(
38
+ default=None,
39
+ description="List of agents to choose from",
40
+ )
41
+
42
+
43
+ class FlockRouter(BaseModel, ABC):
44
+ """Base class for all routers.
45
+
46
+ A router is responsible for determining the next agent in a workflow
47
+ based on the current agent's output.
48
+ """
49
+
50
+ name: str = Field(..., description="Name of the router")
51
+ config: FlockRouterConfig = Field(default_factory=FlockRouterConfig)
52
+
53
+ @abstractmethod
54
+ async def route(
55
+ self,
56
+ current_agent: Any,
57
+ result: dict[str, Any],
58
+ context: FlockContext,
59
+ ) -> HandOffRequest:
60
+ """Determine the next agent to hand off to based on the current agent's output.
61
+
62
+ Args:
63
+ current_agent: The agent that just completed execution
64
+ result: The output from the current agent
65
+ context: The global execution context
66
+
67
+ Returns:
68
+ A HandOff object containing the next agent and input data
69
+ """
70
+ pass
@@ -0,0 +1,175 @@
1
+ import cloudpickle
2
+
3
+
4
+ class SecureSerializer:
5
+ """Security-focused serialization system with capability controls for Flock objects."""
6
+
7
+ # Define capability levels for different modules
8
+ MODULE_CAPABILITIES = {
9
+ # Core Python - unrestricted
10
+ "builtins": "unrestricted",
11
+ "datetime": "unrestricted",
12
+ "re": "unrestricted",
13
+ "math": "unrestricted",
14
+ "json": "unrestricted",
15
+ # Framework modules - unrestricted
16
+ "flock": "unrestricted",
17
+ # System modules - restricted but allowed
18
+ "os": "restricted",
19
+ "io": "restricted",
20
+ "sys": "restricted",
21
+ "subprocess": "high_risk",
22
+ # Network modules - high risk
23
+ "socket": "high_risk",
24
+ "requests": "high_risk",
25
+ }
26
+
27
+ # Functions that should never be serialized
28
+ BLOCKED_FUNCTIONS = {
29
+ "os.system",
30
+ "os.popen",
31
+ "os.spawn",
32
+ "os.exec",
33
+ "subprocess.call",
34
+ "subprocess.run",
35
+ "subprocess.Popen",
36
+ "eval",
37
+ "exec",
38
+ "__import__",
39
+ }
40
+
41
+ @staticmethod
42
+ def _get_module_capability(module_name):
43
+ """Get the capability level for a module."""
44
+ for prefix, level in SecureSerializer.MODULE_CAPABILITIES.items():
45
+ if module_name == prefix or module_name.startswith(f"{prefix}."):
46
+ return level
47
+ return "unknown" # Default to unknown for unlisted modules
48
+
49
+ @staticmethod
50
+ def _is_safe_callable(obj):
51
+ """Check if a callable is safe to serialize."""
52
+ if not callable(obj) or isinstance(obj, type):
53
+ return True, "Not a callable function"
54
+
55
+ module = obj.__module__
56
+ func_name = (
57
+ f"{module}.{obj.__name__}"
58
+ if hasattr(obj, "__name__")
59
+ else "unknown"
60
+ )
61
+
62
+ # Check against blocked functions
63
+ if func_name in SecureSerializer.BLOCKED_FUNCTIONS:
64
+ return False, f"Function {func_name} is explicitly blocked"
65
+
66
+ # Check module capability level
67
+ capability = SecureSerializer._get_module_capability(module)
68
+ if capability == "unknown":
69
+ return False, f"Module {module} has unknown security capability"
70
+
71
+ return True, capability
72
+
73
+ @staticmethod
74
+ def serialize(obj, allow_restricted=True, allow_high_risk=False):
75
+ """Serialize an object with capability checks."""
76
+ if callable(obj) and not isinstance(obj, type):
77
+ is_safe, capability = SecureSerializer._is_safe_callable(obj)
78
+
79
+ if not is_safe:
80
+ raise ValueError(
81
+ f"Cannot serialize unsafe callable: {capability}"
82
+ )
83
+
84
+ if capability == "high_risk" and not allow_high_risk:
85
+ raise ValueError(
86
+ f"High risk callable {obj.__module__}.{obj.__name__} requires explicit permission"
87
+ )
88
+
89
+ if capability == "restricted" and not allow_restricted:
90
+ raise ValueError(
91
+ f"Restricted callable {obj.__module__}.{obj.__name__} requires explicit permission"
92
+ )
93
+
94
+ # Store metadata about the callable for verification during deserialization
95
+ metadata = {
96
+ "module": obj.__module__,
97
+ "name": getattr(obj, "__name__", "unknown"),
98
+ "capability": capability,
99
+ }
100
+
101
+ return {
102
+ "__serialized_callable__": True,
103
+ "data": cloudpickle.dumps(obj).hex(),
104
+ "metadata": metadata,
105
+ }
106
+
107
+ if isinstance(obj, list):
108
+ return [
109
+ SecureSerializer.serialize(
110
+ item, allow_restricted, allow_high_risk
111
+ )
112
+ for item in obj
113
+ ]
114
+
115
+ if isinstance(obj, dict):
116
+ return {
117
+ k: SecureSerializer.serialize(
118
+ v, allow_restricted, allow_high_risk
119
+ )
120
+ for k, v in obj.items()
121
+ }
122
+
123
+ return obj
124
+
125
+ @staticmethod
126
+ def deserialize(obj, allow_restricted=True, allow_high_risk=False):
127
+ """Deserialize an object with capability enforcement."""
128
+ if isinstance(obj, dict) and obj.get("__serialized_callable__") is True:
129
+ # Validate the capability level during deserialization
130
+ metadata = obj.get("metadata", {})
131
+ capability = metadata.get("capability", "unknown")
132
+
133
+ if capability == "high_risk" and not allow_high_risk:
134
+ raise ValueError(
135
+ f"Cannot deserialize high risk callable {metadata.get('module')}.{metadata.get('name')}"
136
+ )
137
+
138
+ if capability == "restricted" and not allow_restricted:
139
+ raise ValueError(
140
+ f"Cannot deserialize restricted callable {metadata.get('module')}.{metadata.get('name')}"
141
+ )
142
+
143
+ try:
144
+ callable_obj = cloudpickle.loads(bytes.fromhex(obj["data"]))
145
+
146
+ # Additional verification that the deserialized object matches its metadata
147
+ if callable_obj.__module__ != metadata.get("module") or (
148
+ hasattr(callable_obj, "__name__")
149
+ and callable_obj.__name__ != metadata.get("name")
150
+ ):
151
+ raise ValueError(
152
+ "Callable metadata mismatch - possible tampering detected"
153
+ )
154
+
155
+ return callable_obj
156
+ except Exception as e:
157
+ raise ValueError(f"Failed to deserialize callable: {e!s}")
158
+
159
+ if isinstance(obj, list):
160
+ return [
161
+ SecureSerializer.deserialize(
162
+ item, allow_restricted, allow_high_risk
163
+ )
164
+ for item in obj
165
+ ]
166
+
167
+ if isinstance(obj, dict) and "__serialized_callable__" not in obj:
168
+ return {
169
+ k: SecureSerializer.deserialize(
170
+ v, allow_restricted, allow_high_risk
171
+ )
172
+ for k, v in obj.items()
173
+ }
174
+
175
+ return obj
@@ -40,6 +40,8 @@ class ZepEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMixin):
40
40
  zep_api_key=self.config.zep_api_key,
41
41
  zep_url=self.config.zep_url,
42
42
  min_fact_rating=self.config.min_fact_rating,
43
+ enable_read=True,
44
+ enable_write=True,
43
45
  ),
44
46
  )
45
47
  client = zep.get_client()
@@ -0,0 +1 @@
1
+ """Routers for the Flock framework."""
@@ -0,0 +1 @@
1
+ """Agent-based router implementation for the Flock framework."""