flock-core 0.2.18__py3-none-any.whl → 0.3.2__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 +39 -29
- flock/cli/assets/release_notes.md +111 -0
- flock/cli/constants.py +1 -0
- flock/cli/load_release_notes.py +23 -0
- flock/core/__init__.py +12 -1
- flock/core/context/context.py +10 -5
- flock/core/flock.py +61 -21
- flock/core/flock_agent.py +112 -442
- flock/core/flock_evaluator.py +49 -0
- flock/core/flock_factory.py +73 -0
- flock/core/flock_module.py +77 -0
- flock/{interpreter → core/interpreter}/python_interpreter.py +9 -1
- flock/core/logging/formatters/themes.py +1 -1
- flock/core/logging/logging.py +119 -15
- flock/core/mixin/dspy_integration.py +11 -8
- flock/core/registry/agent_registry.py +4 -2
- flock/core/tools/basic_tools.py +1 -1
- flock/core/util/cli_helper.py +41 -3
- flock/evaluators/declarative/declarative_evaluator.py +52 -0
- flock/evaluators/natural_language/natural_language_evaluator.py +66 -0
- flock/evaluators/zep/zep_evaluator.py +55 -0
- flock/modules/callback/callback_module.py +86 -0
- flock/modules/memory/memory_module.py +235 -0
- flock/modules/memory/memory_parser.py +125 -0
- flock/modules/memory/memory_storage.py +736 -0
- flock/modules/output/output_module.py +194 -0
- flock/modules/performance/metrics_module.py +477 -0
- flock/modules/zep/zep_module.py +177 -0
- flock/themes/aardvark-blue.toml +1 -1
- {flock_core-0.2.18.dist-info → flock_core-0.3.2.dist-info}/METADATA +49 -2
- {flock_core-0.2.18.dist-info → flock_core-0.3.2.dist-info}/RECORD +34 -19
- {flock_core-0.2.18.dist-info → flock_core-0.3.2.dist-info}/WHEEL +0 -0
- {flock_core-0.2.18.dist-info → flock_core-0.3.2.dist-info}/entry_points.txt +0 -0
- {flock_core-0.2.18.dist-info → flock_core-0.3.2.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,55 @@
|
|
|
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
|
+
from flock.modules.zep.zep_module import ZepModule, ZepModuleConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ZepEvaluatorConfig(FlockEvaluatorConfig):
|
|
13
|
+
zep_url: str = "http://localhost:8000"
|
|
14
|
+
zep_api_key: str = "apikey"
|
|
15
|
+
min_fact_rating: float = Field(
|
|
16
|
+
default=0.7, description="Minimum rating for facts to be considered"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ZepEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMixin):
|
|
21
|
+
"""Evaluator that uses DSPy for generation."""
|
|
22
|
+
|
|
23
|
+
config: ZepEvaluatorConfig = Field(
|
|
24
|
+
default_factory=ZepEvaluatorConfig,
|
|
25
|
+
description="Evaluator configuration",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
async def evaluate(
|
|
29
|
+
self, agent: FlockAgent, inputs: dict[str, Any], tools: list[Any]
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""Simple evaluator that uses Zep.
|
|
32
|
+
|
|
33
|
+
if inputs contain "query", it searches memory for the query and returns the facts.
|
|
34
|
+
if inputs contain "data", it adds the data to memory
|
|
35
|
+
"""
|
|
36
|
+
result = {}
|
|
37
|
+
zep = ZepModule(
|
|
38
|
+
name=self.name,
|
|
39
|
+
config=ZepModuleConfig(
|
|
40
|
+
zep_api_key=self.config.zep_api_key,
|
|
41
|
+
zep_url=self.config.zep_url,
|
|
42
|
+
min_fact_rating=self.config.min_fact_rating,
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
client = zep.get_client()
|
|
46
|
+
if "query" in inputs:
|
|
47
|
+
query = inputs["query"]
|
|
48
|
+
facts = zep.search_memory(query, client)
|
|
49
|
+
result = {"facts": facts}
|
|
50
|
+
|
|
51
|
+
if "data" in inputs:
|
|
52
|
+
data = inputs["data"]
|
|
53
|
+
zep.add_to_memory(data, client)
|
|
54
|
+
result = {"message": "Data added to memory"}
|
|
55
|
+
return result
|
|
@@ -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
|