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

Files changed (32) hide show
  1. flock/__init__.py +39 -29
  2. flock/cli/assets/release_notes.md +111 -0
  3. flock/cli/constants.py +1 -0
  4. flock/cli/load_release_notes.py +23 -0
  5. flock/core/__init__.py +12 -1
  6. flock/core/context/context.py +10 -5
  7. flock/core/flock.py +61 -21
  8. flock/core/flock_agent.py +112 -442
  9. flock/core/flock_evaluator.py +49 -0
  10. flock/core/flock_factory.py +73 -0
  11. flock/core/flock_module.py +77 -0
  12. flock/{interpreter → core/interpreter}/python_interpreter.py +9 -1
  13. flock/core/logging/formatters/themes.py +1 -1
  14. flock/core/logging/logging.py +119 -15
  15. flock/core/mixin/dspy_integration.py +11 -8
  16. flock/core/registry/agent_registry.py +4 -2
  17. flock/core/tools/basic_tools.py +1 -1
  18. flock/core/util/cli_helper.py +59 -2
  19. flock/evaluators/declarative/declarative_evaluator.py +52 -0
  20. flock/evaluators/natural_language/natural_language_evaluator.py +66 -0
  21. flock/modules/callback/callback_module.py +86 -0
  22. flock/modules/memory/memory_module.py +235 -0
  23. flock/modules/memory/memory_parser.py +125 -0
  24. flock/modules/memory/memory_storage.py +736 -0
  25. flock/modules/output/output_module.py +194 -0
  26. flock/modules/performance/metrics_module.py +477 -0
  27. flock/themes/aardvark-blue.toml +1 -1
  28. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/METADATA +112 -4
  29. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/RECORD +32 -19
  30. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/WHEEL +0 -0
  31. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/entry_points.txt +0 -0
  32. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,52 @@
1
+ from typing import Any
2
+
3
+ from pydantic import Field
4
+
5
+ from flock.core.flock_agent import FlockAgent
6
+ from flock.core.flock_evaluator import FlockEvaluator, FlockEvaluatorConfig
7
+ from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
8
+ from flock.core.mixin.prompt_parser import PromptParserMixin
9
+
10
+
11
+ class DeclarativeEvaluatorConfig(FlockEvaluatorConfig):
12
+ agent_type_override: str | None = None
13
+ model: str | None = "openai/gpt-4o"
14
+ use_cache: bool = True
15
+ temperature: float = 0.0
16
+ max_tokens: int = 4096
17
+
18
+
19
+ class DeclarativeEvaluator(
20
+ FlockEvaluator, DSPyIntegrationMixin, PromptParserMixin
21
+ ):
22
+ """Evaluator that uses DSPy for generation."""
23
+
24
+ config: DeclarativeEvaluatorConfig = Field(
25
+ default_factory=DeclarativeEvaluatorConfig,
26
+ description="Evaluator configuration",
27
+ )
28
+
29
+ async def evaluate(
30
+ self, agent: FlockAgent, inputs: dict[str, Any], tools: list[Any]
31
+ ) -> dict[str, Any]:
32
+ """Evaluate using DSPy."""
33
+ _dspy_signature = self.create_dspy_signature_class(
34
+ agent.name,
35
+ agent.description,
36
+ f"{agent.input} -> {agent.output}",
37
+ )
38
+ self._configure_language_model(
39
+ model=self.config.model,
40
+ use_cache=self.config.use_cache,
41
+ temperature=self.config.temperature,
42
+ max_tokens=self.config.max_tokens,
43
+ )
44
+ agent_task = self._select_task(
45
+ _dspy_signature,
46
+ agent_type_override=self.config.agent_type_override,
47
+ tools=tools,
48
+ )
49
+ # Execute the task.
50
+ result = agent_task(**inputs)
51
+ result = self._process_result(result, inputs)
52
+ return result
@@ -0,0 +1,66 @@
1
+ from typing import Any
2
+
3
+ from flock.core.flock_evaluator import FlockEvaluator
4
+
5
+
6
+ class NaturalLanguageEvaluator(FlockEvaluator):
7
+ """Evaluator that uses natural language prompting."""
8
+
9
+ name: str = "natural_language"
10
+ prompt_template: str = ""
11
+ client: Any = None # OpenAI client
12
+
13
+ async def setup(self, input_schema: str, output_schema: str) -> None:
14
+ """Set up prompt template and client."""
15
+ from openai import AsyncOpenAI
16
+
17
+ # Create prompt template
18
+ self.prompt_template = f"""
19
+ You are an AI assistant that processes inputs and generates outputs.
20
+
21
+ Input Format:
22
+ {input_schema}
23
+
24
+ Required Output Format:
25
+ {output_schema}
26
+
27
+ Please process the following input and provide output in the required format:
28
+ {{input}}
29
+ """
30
+
31
+ # Set up client
32
+ self.client = AsyncOpenAI()
33
+
34
+ async def evaluate(self, inputs: dict[str, Any]) -> dict[str, Any]:
35
+ """Evaluate using natural language."""
36
+ if not self.client:
37
+ raise RuntimeError("Evaluator not set up")
38
+
39
+ # Format input for prompt
40
+ input_str = "\n".join(f"{k}: {v}" for k, v in inputs.items())
41
+
42
+ # Get completion
43
+ response = await self.client.chat.completions.create(
44
+ model=self.config.model,
45
+ messages=[
46
+ {
47
+ "role": "user",
48
+ "content": self.prompt_template.format(input=input_str),
49
+ }
50
+ ],
51
+ temperature=self.config.temperature,
52
+ max_tokens=self.config.max_tokens,
53
+ )
54
+
55
+ # Parse response into dictionary
56
+ try:
57
+ import json
58
+
59
+ return json.loads(response.choices[0].message.content)
60
+ except json.JSONDecodeError:
61
+ return {"result": response.choices[0].message.content}
62
+
63
+ async def cleanup(self) -> None:
64
+ """Close client."""
65
+ if self.client:
66
+ await self.client.close()
@@ -0,0 +1,86 @@
1
+ """Callback module for handling agent lifecycle hooks."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import Any
5
+
6
+ from pydantic import Field
7
+
8
+ from flock.core import FlockModule, FlockModuleConfig
9
+
10
+
11
+ class CallbackModuleConfig(FlockModuleConfig):
12
+ """Configuration for callback module."""
13
+
14
+ initialize_callback: (
15
+ Callable[[Any, dict[str, Any]], Awaitable[None]] | None
16
+ ) = Field(
17
+ default=None,
18
+ description="Optional callback function for initialization",
19
+ )
20
+ evaluate_callback: (
21
+ Callable[[Any, dict[str, Any]], Awaitable[dict[str, Any]]] | None
22
+ ) = Field(
23
+ default=None, description="Optional callback function for evaluate"
24
+ )
25
+ terminate_callback: (
26
+ Callable[[Any, dict[str, Any], dict[str, Any]], Awaitable[None]] | None
27
+ ) = Field(
28
+ default=None, description="Optional callback function for termination"
29
+ )
30
+ on_error_callback: (
31
+ Callable[[Any, Exception, dict[str, Any]], Awaitable[None]] | None
32
+ ) = Field(
33
+ default=None,
34
+ description="Optional callback function for error handling",
35
+ )
36
+
37
+
38
+ class CallbackModule(FlockModule):
39
+ """Module that provides callback functionality for agent lifecycle events."""
40
+
41
+ name: str = "callbacks"
42
+ config: CallbackModuleConfig = Field(
43
+ default_factory=CallbackModuleConfig,
44
+ description="Callback module configuration",
45
+ )
46
+
47
+ async def pre_initialize(self, agent: Any, inputs: dict[str, Any]) -> None:
48
+ """Run initialize callback if configured."""
49
+ if self.config.initialize_callback:
50
+ await self.config.initialize_callback(agent, inputs)
51
+
52
+ async def pre_evaluate(
53
+ self, agent: Any, inputs: dict[str, Any]
54
+ ) -> dict[str, Any]:
55
+ """Run evaluate callback if configured."""
56
+ if self.config.evaluate_callback:
57
+ return await self.config.evaluate_callback(agent, inputs)
58
+ return inputs
59
+
60
+ async def pre_terminate(
61
+ self, agent: Any, inputs: dict[str, Any], result: dict[str, Any]
62
+ ) -> None:
63
+ """Run terminate callback if configured."""
64
+ if self.config.terminate_callback:
65
+ await self.config.terminate_callback(agent, inputs, result)
66
+
67
+ async def on_error(
68
+ self, agent: Any, error: Exception, inputs: dict[str, Any]
69
+ ) -> None:
70
+ """Run error callback if configured."""
71
+ if self.config.on_error_callback:
72
+ await self.config.on_error_callback(agent, error, inputs)
73
+
74
+ # Other hooks just pass through
75
+ async def post_initialize(self, agent: Any, inputs: dict[str, Any]) -> None:
76
+ pass
77
+
78
+ async def post_evaluate(
79
+ self, agent: Any, inputs: dict[str, Any], result: dict[str, Any]
80
+ ) -> dict[str, Any]:
81
+ return result
82
+
83
+ async def post_terminate(
84
+ self, agent: Any, inputs: dict[str, Any], result: dict[str, Any]
85
+ ) -> None:
86
+ pass
@@ -0,0 +1,235 @@
1
+ """Memory module implementation for Flock agents."""
2
+
3
+ import json
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ from pydantic import Field
9
+
10
+ from flock.core import FlockAgent, FlockModule, FlockModuleConfig
11
+ from flock.core.logging.logging import get_logger
12
+ from flock.modules.memory.memory_parser import MemoryMappingParser
13
+ from flock.modules.memory.memory_storage import FlockMemoryStore, MemoryEntry
14
+
15
+
16
+ class MemoryModuleConfig(FlockModuleConfig):
17
+ """Configuration for the memory module."""
18
+
19
+ file_path: str | None = Field(
20
+ default="agent_memory.json", description="Path to save memory file"
21
+ )
22
+ memory_mapping: str | None = Field(
23
+ default=None, description="Memory mapping configuration"
24
+ )
25
+ similarity_threshold: float = Field(
26
+ default=0.5, description="Threshold for semantic similarity"
27
+ )
28
+ context_window: int = Field(
29
+ default=3, description="Number of memory entries to return"
30
+ )
31
+ max_length: int = Field(
32
+ default=1000, description="Max length of memory entry before splitting"
33
+ )
34
+ save_after_update: bool = Field(
35
+ default=True, description="Whether to save memory after each update"
36
+ )
37
+
38
+
39
+ logger = get_logger("memory")
40
+
41
+
42
+ class MemoryModule(FlockModule):
43
+ """Module that adds memory capabilities to a Flock agent.
44
+
45
+ This module encapsulates all memory-related functionality that was previously
46
+ hardcoded into FlockAgent.
47
+ """
48
+
49
+ name: str = "memory"
50
+ config: MemoryModuleConfig = Field(
51
+ default_factory=MemoryModuleConfig,
52
+ description="Memory module configuration",
53
+ )
54
+ memory_store: FlockMemoryStore | None = None
55
+ memory_ops: list = []
56
+
57
+ async def pre_initialize(
58
+ self, agent: FlockAgent, inputs: dict[str, Any]
59
+ ) -> None:
60
+ """Initialize memory store if needed."""
61
+ if not self.memory_store:
62
+ self.memory_store = FlockMemoryStore.load_from_file(
63
+ self.config.file_path
64
+ )
65
+
66
+ if not self.config.memory_mapping:
67
+ self.memory_ops = []
68
+ self.memory_ops.append({"type": "semantic"})
69
+ else:
70
+ self.memory_ops = MemoryMappingParser().parse(
71
+ self.config.memory_mapping
72
+ )
73
+
74
+ logger.debug(f"Initialized memory module for agent {agent.name}")
75
+
76
+ async def post_initialize(self, agent: Any, inputs: dict[str, Any]) -> None:
77
+ """No post-initialization needed."""
78
+ pass
79
+
80
+ async def pre_evaluate(
81
+ self, agent: FlockAgent, inputs: dict[str, Any]
82
+ ) -> dict[str, Any]:
83
+ """Check memory before evaluation."""
84
+ if not self.memory_store:
85
+ return inputs
86
+
87
+ try:
88
+ # Convert input to embedding
89
+ input_text = json.dumps(inputs)
90
+ query_embedding = self.memory_store.compute_embedding(input_text)
91
+
92
+ # Extract concepts
93
+ concepts = await self._extract_concepts(agent, input_text)
94
+
95
+ memory_results = []
96
+ for op in self.memory_ops:
97
+ if op["type"] == "semantic":
98
+ semantic_results = self.memory_store.retrieve(
99
+ query_embedding,
100
+ concepts,
101
+ similarity_threshold=self.config.similarity_threshold,
102
+ )
103
+ memory_results.extend(semantic_results)
104
+
105
+ elif op["type"] == "exact":
106
+ exact_results = self.memory_store.exact_match(inputs)
107
+ memory_results.extend(exact_results)
108
+
109
+ if memory_results:
110
+ logger.debug(
111
+ f"Found {len(memory_results)} relevant memories",
112
+ agent=agent.name,
113
+ )
114
+ inputs["memory_results"] = memory_results
115
+
116
+ return inputs
117
+
118
+ except Exception as e:
119
+ logger.warning(f"Memory retrieval failed: {e!s}", agent=agent.name)
120
+ return inputs
121
+
122
+ async def post_evaluate(
123
+ self, agent: FlockAgent, inputs: dict[str, Any], result: dict[str, Any]
124
+ ) -> dict[str, Any]:
125
+ """Store results in memory after evaluation."""
126
+ if not self.memory_store:
127
+ return result
128
+
129
+ try:
130
+ # Extract information chunks
131
+ chunks = await self._extract_information(agent, inputs, result)
132
+ chunk_concepts = await self._extract_concepts(agent, chunks)
133
+
134
+ # Create memory entry
135
+ entry = MemoryEntry(
136
+ id=str(uuid.uuid4()),
137
+ content=chunks,
138
+ embedding=self.memory_store.compute_embedding(chunks).tolist(),
139
+ concepts=chunk_concepts,
140
+ timestamp=datetime.now(),
141
+ )
142
+
143
+ # Add to memory store
144
+ self.memory_store.add_entry(entry)
145
+
146
+ if self.config.save_after_update:
147
+ self.save_memory()
148
+
149
+ logger.debug(
150
+ "Stored interaction in memory",
151
+ agent=agent.name,
152
+ entry_id=entry.id,
153
+ concepts=chunk_concepts,
154
+ )
155
+
156
+ except Exception as e:
157
+ logger.warning(f"Memory storage failed: {e!s}", agent=agent.name)
158
+
159
+ return result
160
+
161
+ async def pre_terminate(
162
+ self, agent: Any, inputs: dict[str, Any], result: dict[str, Any]
163
+ ) -> None:
164
+ """No pre-termination needed."""
165
+ pass
166
+
167
+ async def post_terminate(
168
+ self, agent: Any, inputs: dict[str, Any], result: dict[str, Any]
169
+ ) -> None:
170
+ """Save memory store if configured."""
171
+ if self.config.save_after_update and self.memory_store:
172
+ self.save_memory()
173
+
174
+ async def _extract_concepts(self, agent: FlockAgent, text: str) -> set[str]:
175
+ """Extract concepts using agent's LLM capabilities."""
176
+ existing_concepts = None
177
+ if self.memory_store.concept_graph:
178
+ existing_concepts = set(
179
+ self.memory_store.concept_graph.graph.nodes()
180
+ )
181
+
182
+ input = "text: str | Text to analyze"
183
+ if existing_concepts:
184
+ input += ", existing_concepts: list[str] | Already known concepts that might apply"
185
+
186
+ # Create signature for concept extraction using agent's capabilities
187
+ concept_signature = agent.create_dspy_signature_class(
188
+ f"{agent.name}_concept_extractor",
189
+ "Extract key concepts from text",
190
+ f"{input} -> concepts: list[str] | Max five key concepts all lower case",
191
+ )
192
+
193
+ # Configure and run the predictor
194
+ agent._configure_language_model()
195
+ predictor = agent._select_task(concept_signature, "Completion")
196
+ result = predictor(
197
+ text=text,
198
+ existing_concepts=list(existing_concepts)
199
+ if existing_concepts
200
+ else None,
201
+ )
202
+
203
+ concept_list = result.concepts if hasattr(result, "concepts") else []
204
+ return set(concept_list)
205
+
206
+ async def _extract_information(
207
+ self, agent: FlockAgent, inputs: dict[str, Any], result: dict[str, Any]
208
+ ) -> str:
209
+ """Extract information chunks from interaction."""
210
+ # Create splitter signature using agent's capabilities
211
+ split_signature = agent.create_dspy_signature_class(
212
+ f"{agent.name}_splitter",
213
+ "Extract a list of potentially needed data and information for future reference",
214
+ """
215
+ content: str | The content to split
216
+ -> chunks: list[str] | list of data and information for future reference
217
+ """,
218
+ )
219
+
220
+ # Configure and run the predictor
221
+ agent._configure_language_model()
222
+ splitter = agent._select_task(split_signature, "Completion")
223
+
224
+ # Get the content to split
225
+ full_text = json.dumps(inputs) + json.dumps(result)
226
+ split_result = splitter(content=full_text)
227
+
228
+ return "\n".join(split_result.chunks)
229
+
230
+ def save_memory(self) -> None:
231
+ """Save memory store to file."""
232
+ if self.memory_store and self.config.file_path:
233
+ json_str = self.memory_store.model_dump_json()
234
+ with open(self.config.file_path, "w") as file:
235
+ file.write(json_str)
@@ -0,0 +1,125 @@
1
+ """Parser for memory mapping declarations into executable operations."""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from flock.core.memory.memory_storage import (
7
+ CombineOperation,
8
+ EnrichOperation,
9
+ ExactOperation,
10
+ FilterOperation,
11
+ MemoryOperation,
12
+ MemoryScope,
13
+ SemanticOperation,
14
+ SortOperation,
15
+ )
16
+
17
+
18
+ class MemoryMappingParser:
19
+ """Parses memory mapping declarations into executable operations."""
20
+
21
+ def parse(self, mapping: str) -> list[MemoryOperation]:
22
+ """Parse a memory mapping string into operations.
23
+
24
+ Example mappings:
25
+ "topic -> memory.semantic(threshold=0.9) | memory.exact -> output"
26
+ "query -> memory.semantic(scope='global') | memory.filter(recency='7d') | memory.sort(by='relevance')"
27
+ """
28
+ operations = []
29
+ stages = [s.strip() for s in mapping.split("|")]
30
+
31
+ for stage in stages:
32
+ if "->" not in stage:
33
+ continue
34
+
35
+ inputs, op_spec = stage.split("->")
36
+ inputs = [i.strip() for i in inputs.split(",")]
37
+
38
+ if "memory." in op_spec:
39
+ # Extract operation name and parameters
40
+ match = re.match(r"memory\.(\w+)(?:\((.*)\))?", op_spec.strip())
41
+ if not match:
42
+ continue
43
+
44
+ op_name, params_str = match.groups()
45
+ params = self._parse_params(params_str or "")
46
+
47
+ # Create appropriate operation object
48
+ if op_name == "semantic":
49
+ operation = SemanticOperation(
50
+ threshold=params.get("threshold", 0.8),
51
+ scope=params.get("scope", MemoryScope.BOTH),
52
+ max_results=params.get("max_results", 10),
53
+ )
54
+ elif op_name == "exact":
55
+ operation = ExactOperation(
56
+ keys=inputs, scope=params.get("scope", MemoryScope.BOTH)
57
+ )
58
+ elif op_name == "enrich":
59
+ operation = EnrichOperation(
60
+ tools=params.get("tools", []),
61
+ strategy=params.get("strategy", "comprehensive"),
62
+ scope=params.get("scope", MemoryScope.BOTH),
63
+ )
64
+ elif op_name == "filter":
65
+ operation = FilterOperation(
66
+ recency=params.get("recency"),
67
+ relevance=params.get("relevance"),
68
+ metadata=params.get("metadata", {}),
69
+ )
70
+ elif op_name == "sort":
71
+ operation = SortOperation(
72
+ by=params.get("by", "relevance"),
73
+ ascending=params.get("ascending", False),
74
+ )
75
+ elif op_name == "combine":
76
+ operation = CombineOperation(
77
+ weights=params.get(
78
+ "weights", {"semantic": 0.7, "exact": 0.3}
79
+ )
80
+ )
81
+
82
+ operations.append(operation)
83
+
84
+ return operations
85
+
86
+ def _parse_params(self, params_str: str) -> dict[str, Any]:
87
+ """Parse parameters string into a dictionary.
88
+
89
+ Handles:
90
+ - Quoted strings: threshold='high'
91
+ - Numbers: threshold=0.9
92
+ - Lists: tools=['web_search', 'extract_numbers']
93
+ - Dictionaries: weights={'semantic': 0.7, 'exact': 0.3}
94
+ """
95
+ if not params_str:
96
+ return {}
97
+
98
+ params = {}
99
+ # Split on commas not inside brackets or quotes
100
+ param_pairs = re.findall(
101
+ r"""
102
+ (?:[^,"]|"[^"]*"|'[^']*')+ # Match everything except comma, or quoted strings
103
+ """,
104
+ params_str,
105
+ re.VERBOSE,
106
+ )
107
+
108
+ for pair in param_pairs:
109
+ if "=" not in pair:
110
+ continue
111
+ key, value = pair.split("=", 1)
112
+ key = key.strip()
113
+ value = value.strip()
114
+
115
+ # Try to evaluate the value (for lists, dicts, numbers)
116
+ try:
117
+ # Safely evaluate the value
118
+ value = eval(value, {"__builtins__": {}}, {})
119
+ except:
120
+ # If evaluation fails, treat as string
121
+ value = value.strip("'\"")
122
+
123
+ params[key] = value
124
+
125
+ return params