flock-core 0.5.0b27__py3-none-any.whl → 0.5.0b28__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,11 +1,18 @@
1
1
  # src/flock/components/utility/__init__.py
2
2
  """Utility components for the Flock framework."""
3
3
 
4
+ from .example_utility_component import ExampleUtilityComponent, ExampleUtilityConfig, ExampleRecord
5
+ from .feedback_utility_component import FeedbackUtilityComponent, FeedbackUtilityConfig
4
6
  from .memory_utility_component import MemoryUtilityComponent, MemoryUtilityConfig
5
7
  from .metrics_utility_component import MetricsUtilityComponent, MetricsUtilityConfig
6
8
  from .output_utility_component import OutputUtilityComponent, OutputUtilityConfig
7
9
 
8
10
  __all__ = [
11
+ "ExampleUtilityComponent",
12
+ "ExampleUtilityConfig",
13
+ "ExampleRecord",
14
+ "FeedbackUtilityComponent",
15
+ "FeedbackUtilityConfig",
9
16
  "MemoryUtilityComponent",
10
17
  "MemoryUtilityConfig",
11
18
  "MetricsUtilityComponent",
@@ -0,0 +1,250 @@
1
+ """Example utility component for n-shot learning."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import Field
7
+
8
+ from flock.core.component.agent_component_base import AgentComponentConfig
9
+ from flock.core.component.utility_component import UtilityComponent
10
+ from flock.core.context.context import FlockContext
11
+ from flock.core.logging.logging import get_logger
12
+ from flock.core.registry import flock_component
13
+
14
+ if TYPE_CHECKING:
15
+ from flock.core.flock_agent import FlockAgent
16
+ from flock.webapp.app.services.sharing_store import SharedLinkStoreInterface
17
+
18
+ logger = get_logger("components.utility.example")
19
+
20
+
21
+ class ExampleUtilityConfig(AgentComponentConfig):
22
+ """Configuration for the ExampleUtilityComponent."""
23
+
24
+ # Storage configuration
25
+ storage_type: Literal["sqlite", "azure"] = Field(
26
+ default="sqlite",
27
+ description="Type of storage backend for example data"
28
+ )
29
+
30
+ # SQLite configuration
31
+ sqlite_db_path: str = Field(
32
+ default="./flock_examples.db",
33
+ description="Path to SQLite database file"
34
+ )
35
+
36
+ # Azure Table Storage configuration
37
+ azure_connection_string: str | None = Field(
38
+ default=None,
39
+ description="Azure Table Storage connection string"
40
+ )
41
+ azure_table_name: str = Field(
42
+ default="flockexamples",
43
+ description="Azure Table Storage table name"
44
+ )
45
+
46
+ # Example selection criteria
47
+ max_examples: int = Field(
48
+ default=5,
49
+ description="Maximum number of examples to include"
50
+ )
51
+ example_timeframe_days: int = Field(
52
+ default=30,
53
+ description="Only include examples from the last N days"
54
+ )
55
+
56
+ # Example injection settings
57
+ example_input_key: str = Field(
58
+ default="examples_context",
59
+ description="Input key to use for injected examples"
60
+ )
61
+
62
+ # Example filtering
63
+ example_filter_keywords: list[str] = Field(
64
+ default_factory=list,
65
+ description="Keywords to filter examples (only include examples containing these)"
66
+ )
67
+ example_exclude_keywords: list[str] = Field(
68
+ default_factory=list,
69
+ description="Keywords to exclude examples containing these"
70
+ )
71
+
72
+
73
+ @flock_component(config_class=ExampleUtilityConfig)
74
+ class ExampleUtilityComponent(UtilityComponent):
75
+ """Utility component that injects relevant examples into agent inputs for n-shot learning."""
76
+
77
+ config: ExampleUtilityConfig = Field(
78
+ default_factory=ExampleUtilityConfig,
79
+ description="Example component configuration"
80
+ )
81
+
82
+ def __init__(self, name: str = "examples", config: ExampleUtilityConfig | None = None, **data):
83
+ super().__init__(name=name, config=config or ExampleUtilityConfig(), **data)
84
+ self._store: SharedLinkStoreInterface | None = None
85
+
86
+ async def _get_store(self) -> SharedLinkStoreInterface:
87
+ """Get the appropriate example store based on configuration."""
88
+ if self._store is None:
89
+ if self.config.storage_type == "sqlite":
90
+ from flock.webapp.app.services.sharing_store import SQLiteSharedLinkStore
91
+ self._store = SQLiteSharedLinkStore(self.config.sqlite_db_path)
92
+ elif self.config.storage_type == "azure":
93
+ if not self.config.azure_connection_string:
94
+ raise ValueError("Azure connection string is required for Azure storage")
95
+ from flock.webapp.app.services.sharing_store import AzureTableSharedLinkStore
96
+ self._store = AzureTableSharedLinkStore(
97
+ connection_string=self.config.azure_connection_string,
98
+ table_name=self.config.azure_table_name
99
+ )
100
+ else:
101
+ raise ValueError(f"Unsupported storage type: {self.config.storage_type}")
102
+
103
+ await self._store.initialize()
104
+
105
+ return self._store
106
+
107
+ @staticmethod
108
+ def seed_examples(examples: list["ExampleRecord"]) -> None:
109
+ """Seed examples into the storage system.
110
+
111
+ Args:
112
+ examples: List of ExampleRecord objects to seed
113
+ """
114
+ import asyncio
115
+
116
+ async def _seed_examples():
117
+ # Create a default component for seeding
118
+ component = ExampleUtilityComponent()
119
+ store = await component._get_store()
120
+
121
+ for example in examples:
122
+ await store.save_example(example)
123
+
124
+ logger.info(f"Seeded {len(examples)} examples into storage")
125
+
126
+ # Run the async function
127
+ asyncio.run(_seed_examples())
128
+
129
+ async def _get_relevant_examples(
130
+ self,
131
+ agent_name: str,
132
+ inputs: dict[str, Any]
133
+ ) -> list["ExampleRecord"]:
134
+ """Get relevant examples for the given agent and inputs."""
135
+ store = await self._get_store()
136
+
137
+ # Get all examples for this agent
138
+ all_examples = await store.get_all_examples_for_agent(agent_name)
139
+
140
+ # Filter by timeframe
141
+ cutoff_date = datetime.utcnow() - timedelta(days=self.config.example_timeframe_days)
142
+ filtered_examples = [
143
+ ex for ex in all_examples
144
+ if ex.created_at >= cutoff_date
145
+ ]
146
+
147
+ # Filter by keywords if specified
148
+ if self.config.example_filter_keywords:
149
+ filtered_examples = [
150
+ ex for ex in filtered_examples
151
+ if any(keyword.lower() in ex.content.lower() for keyword in self.config.example_filter_keywords)
152
+ ]
153
+
154
+ # Exclude by keywords if specified
155
+ if self.config.example_exclude_keywords:
156
+ filtered_examples = [
157
+ ex for ex in filtered_examples
158
+ if not any(keyword.lower() in ex.content.lower() for keyword in self.config.example_exclude_keywords)
159
+ ]
160
+
161
+ # Sort by recency and limit
162
+ filtered_examples.sort(key=lambda ex: ex.created_at, reverse=True)
163
+ return filtered_examples[:self.config.max_examples]
164
+
165
+ def _format_examples_for_injection(
166
+ self,
167
+ example_records: list["ExampleRecord"]
168
+ ) -> str:
169
+ """Format example records for injection into agent input."""
170
+ if not example_records:
171
+ return "No relevant examples available."
172
+
173
+ formatted_parts = []
174
+ formatted_parts.append(f"Here are {len(example_records)} examples to guide your response:")
175
+
176
+ for i, ex in enumerate(example_records, 1):
177
+ ex_text = f"\nExample {i} (ID: {ex.example_id}):"
178
+ ex_text += f"\n{ex.content}"
179
+ ex_text += f"\nDate: {ex.created_at.strftime('%Y-%m-%d')}"
180
+ formatted_parts.append(ex_text)
181
+
182
+ return "\n".join(formatted_parts)
183
+
184
+ async def on_pre_evaluate(
185
+ self,
186
+ agent: "FlockAgent",
187
+ inputs: dict[str, Any],
188
+ context: FlockContext | None = None,
189
+ ) -> dict[str, Any]:
190
+ """Inject relevant examples into agent inputs before evaluation."""
191
+ logger.debug(f"Injecting examples for agent '{agent.name}'")
192
+
193
+ try:
194
+ # Get relevant examples for this agent
195
+ example_records = await self._get_relevant_examples(agent.name, inputs)
196
+
197
+ # Format examples for injection
198
+ formatted_examples = self._format_examples_for_injection(example_records)
199
+
200
+ # Create a copy of inputs to avoid modifying the original
201
+ enhanced_inputs = inputs.copy()
202
+
203
+ # Inject examples using the configured key
204
+ enhanced_inputs[self.config.example_input_key] = formatted_examples
205
+
206
+ logger.debug(f"Injected {len(example_records)} examples into '{self.config.example_input_key}'")
207
+
208
+ return enhanced_inputs
209
+
210
+ except Exception as e:
211
+ logger.error(f"Error injecting examples: {e}")
212
+ # Return original inputs if example injection fails
213
+ return inputs
214
+
215
+
216
+ # Example record model
217
+ class ExampleRecord:
218
+ """Record for storing example data."""
219
+
220
+ def __init__(
221
+ self,
222
+ agent_name: str,
223
+ example_id: str,
224
+ content: str,
225
+ created_at: datetime | None = None
226
+ ):
227
+ self.agent_name = agent_name
228
+ self.example_id = example_id
229
+ self.content = content
230
+ self.created_at = created_at or datetime.utcnow()
231
+
232
+ def to_dict(self) -> dict[str, Any]:
233
+ """Convert to dictionary for storage."""
234
+ return {
235
+ "agent_name": self.agent_name,
236
+ "example_id": self.example_id,
237
+ "content": self.content,
238
+ "created_at": self.created_at.isoformat(),
239
+ "context_type": "example"
240
+ }
241
+
242
+ @classmethod
243
+ def from_dict(cls, data: dict[str, Any]) -> "ExampleRecord":
244
+ """Create from dictionary from storage."""
245
+ return cls(
246
+ agent_name=data["agent_name"],
247
+ example_id=data["example_id"],
248
+ content=data["content"],
249
+ created_at=datetime.fromisoformat(data["created_at"])
250
+ )
@@ -0,0 +1,206 @@
1
+ """Feedback utility component for learning from user feedback."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import TYPE_CHECKING, Any, Literal
5
+
6
+ from pydantic import Field
7
+
8
+ from flock.core.component.agent_component_base import AgentComponentConfig
9
+ from flock.core.component.utility_component import UtilityComponent
10
+ from flock.core.context.context import FlockContext
11
+ from flock.core.logging.logging import get_logger
12
+ from flock.core.registry import flock_component
13
+
14
+ if TYPE_CHECKING:
15
+ from flock.core.flock_agent import FlockAgent
16
+ from flock.webapp.app.services.sharing_models import FeedbackRecord
17
+ from flock.webapp.app.services.sharing_store import SharedLinkStoreInterface
18
+
19
+ logger = get_logger("components.utility.feedback")
20
+
21
+
22
+ class FeedbackUtilityConfig(AgentComponentConfig):
23
+ """Configuration for the FeedbackUtilityComponent."""
24
+
25
+ # Storage configuration
26
+ storage_type: Literal["sqlite", "azure"] = Field(
27
+ default="sqlite",
28
+ description="Type of storage backend for feedback data"
29
+ )
30
+
31
+ # SQLite configuration
32
+ sqlite_db_path: str = Field(
33
+ default="./flock_feedback.db",
34
+ description="Path to SQLite database file"
35
+ )
36
+
37
+ # Azure Table Storage configuration
38
+ azure_connection_string: str | None = Field(
39
+ default=None,
40
+ description="Azure Table Storage connection string"
41
+ )
42
+ azure_table_name: str = Field(
43
+ default="flockfeedback",
44
+ description="Azure Table Storage table name"
45
+ )
46
+
47
+ # Feedback selection criteria
48
+ max_feedback_items: int = Field(
49
+ default=5,
50
+ description="Maximum number of feedback items to include"
51
+ )
52
+ feedback_timeframe_days: int = Field(
53
+ default=30,
54
+ description="Only include feedback from the last N days"
55
+ )
56
+
57
+ # Feedback injection settings
58
+ feedback_input_key: str = Field(
59
+ default="feedback_context",
60
+ description="Input key to use for injected feedback"
61
+ )
62
+ include_expected_responses: bool = Field(
63
+ default=True,
64
+ description="Whether to include expected responses from feedback"
65
+ )
66
+ include_actual_responses: bool = Field(
67
+ default=False,
68
+ description="Whether to include actual responses from feedback"
69
+ )
70
+
71
+ # Feedback filtering
72
+ feedback_filter_keywords: list[str] = Field(
73
+ default_factory=list,
74
+ description="Keywords to filter feedback (only include feedback containing these)"
75
+ )
76
+ feedback_exclude_keywords: list[str] = Field(
77
+ default_factory=list,
78
+ description="Keywords to exclude feedback containing these"
79
+ )
80
+
81
+
82
+ @flock_component(config_class=FeedbackUtilityConfig)
83
+ class FeedbackUtilityComponent(UtilityComponent):
84
+ """Utility component that injects relevant feedback into agent inputs."""
85
+
86
+ config: FeedbackUtilityConfig = Field(
87
+ default_factory=FeedbackUtilityConfig,
88
+ description="Feedback component configuration"
89
+ )
90
+
91
+ def __init__(self, name: str = "feedback", config: FeedbackUtilityConfig | None = None, **data):
92
+ super().__init__(name=name, config=config or FeedbackUtilityConfig(), **data)
93
+ self._store: SharedLinkStoreInterface | None = None
94
+
95
+ async def _get_store(self) -> SharedLinkStoreInterface:
96
+ """Get the appropriate feedback store based on configuration."""
97
+ if self._store is None:
98
+ if self.config.storage_type == "sqlite":
99
+ from flock.webapp.app.services.sharing_store import SQLiteSharedLinkStore
100
+ self._store = SQLiteSharedLinkStore(self.config.sqlite_db_path)
101
+ elif self.config.storage_type == "azure":
102
+ if not self.config.azure_connection_string:
103
+ raise ValueError("Azure connection string is required for Azure storage")
104
+ from flock.webapp.app.services.sharing_store import AzureTableSharedLinkStore
105
+ self._store = AzureTableSharedLinkStore(
106
+ connection_string=self.config.azure_connection_string,
107
+ table_name=self.config.azure_table_name
108
+ )
109
+ else:
110
+ raise ValueError(f"Unsupported storage type: {self.config.storage_type}")
111
+
112
+ await self._store.initialize()
113
+
114
+ return self._store
115
+
116
+ async def _get_relevant_feedback(
117
+ self,
118
+ agent_name: str,
119
+ inputs: dict[str, Any]
120
+ ) -> list["FeedbackRecord"]:
121
+ """Get relevant feedback for the given agent and inputs."""
122
+ store = await self._get_store()
123
+
124
+ # Get all feedback for this agent
125
+ all_feedback = await store.get_all_feedback_records_for_agent(agent_name)
126
+
127
+ # Filter by timeframe
128
+ cutoff_date = datetime.utcnow() - timedelta(days=self.config.feedback_timeframe_days)
129
+ filtered_feedback = [
130
+ fb for fb in all_feedback
131
+ if fb.created_at >= cutoff_date
132
+ ]
133
+
134
+ # Filter by keywords if specified
135
+ if self.config.feedback_filter_keywords:
136
+ filtered_feedback = [
137
+ fb for fb in filtered_feedback
138
+ if any(keyword.lower() in fb.reason.lower() for keyword in self.config.feedback_filter_keywords)
139
+ ]
140
+
141
+ # Exclude by keywords if specified
142
+ if self.config.feedback_exclude_keywords:
143
+ filtered_feedback = [
144
+ fb for fb in filtered_feedback
145
+ if not any(keyword.lower() in fb.reason.lower() for keyword in self.config.feedback_exclude_keywords)
146
+ ]
147
+
148
+ # Sort by recency and limit
149
+ filtered_feedback.sort(key=lambda fb: fb.created_at, reverse=True)
150
+ return filtered_feedback[:self.config.max_feedback_items]
151
+
152
+ def _format_feedback_for_injection(
153
+ self,
154
+ feedback_records: list["FeedbackRecord"]
155
+ ) -> str:
156
+ """Format feedback records for injection into agent input."""
157
+ if not feedback_records:
158
+ return "No relevant feedback available."
159
+
160
+ formatted_parts = []
161
+ formatted_parts.append(f"Here are {len(feedback_records)} pieces of relevant feedback from previous interactions:")
162
+
163
+ for i, fb in enumerate(feedback_records, 1):
164
+ fb_text = f"\n{i}. Feedback: {fb.reason}"
165
+
166
+ if self.config.include_expected_responses and fb.expected_response:
167
+ fb_text += f"\n Expected response: {fb.expected_response}"
168
+
169
+ if self.config.include_actual_responses and fb.actual_response:
170
+ fb_text += f"\n Actual response: {fb.actual_response}"
171
+
172
+ fb_text += f"\n Date: {fb.created_at.strftime('%Y-%m-%d')}"
173
+ formatted_parts.append(fb_text)
174
+
175
+ return "\n".join(formatted_parts)
176
+
177
+ async def on_pre_evaluate(
178
+ self,
179
+ agent: "FlockAgent",
180
+ inputs: dict[str, Any],
181
+ context: FlockContext | None = None,
182
+ ) -> dict[str, Any]:
183
+ """Inject relevant feedback into agent inputs before evaluation."""
184
+ logger.debug(f"Injecting feedback for agent '{agent.name}'")
185
+
186
+ try:
187
+ # Get relevant feedback for this agent
188
+ feedback_records = await self._get_relevant_feedback(agent.name, inputs)
189
+
190
+ # Format feedback for injection
191
+ formatted_feedback = self._format_feedback_for_injection(feedback_records)
192
+
193
+ # Create a copy of inputs to avoid modifying the original
194
+ enhanced_inputs = inputs.copy()
195
+
196
+ # Inject feedback using the configured key
197
+ enhanced_inputs[self.config.feedback_input_key] = formatted_feedback
198
+
199
+ logger.debug(f"Injected {len(feedback_records)} feedback items into '{self.config.feedback_input_key}'")
200
+
201
+ return enhanced_inputs
202
+
203
+ except Exception as e:
204
+ logger.error(f"Error injecting feedback: {e}")
205
+ # Return original inputs if feedback injection fails
206
+ return inputs
@@ -10,6 +10,14 @@ from __future__ import annotations
10
10
  from collections.abc import Callable
11
11
  from typing import Any, Literal
12
12
 
13
+ from flock.components.utility.example_utility_component import (
14
+ ExampleUtilityComponent,
15
+ ExampleUtilityConfig,
16
+ )
17
+ from flock.components.utility.feedback_utility_component import (
18
+ FeedbackUtilityComponent,
19
+ FeedbackUtilityConfig,
20
+ )
13
21
  from flock.components.utility.metrics_utility_component import (
14
22
  MetricsUtilityComponent,
15
23
  MetricsUtilityConfig,
@@ -28,6 +36,7 @@ class DefaultAgent(FlockAgent):
28
36
  - DeclarativeEvaluationComponent (LLM evaluation)
29
37
  - OutputUtilityComponent (formatting/printing)
30
38
  - MetricsUtilityComponent (latency tracking)
39
+ - FeedbackUtilityComponent (feedback learning) - optional
31
40
  """
32
41
 
33
42
  def __init__(
@@ -62,6 +71,12 @@ class DefaultAgent(FlockAgent):
62
71
  wait_for_input: bool = False,
63
72
  # Metrics utility
64
73
  alert_latency_threshold_ms: int = 30_000,
74
+ # Feedback utility
75
+ enable_feedback: bool = False,
76
+ feedback_config: FeedbackUtilityConfig | None = None,
77
+ # Example utility
78
+ enable_examples: bool = False,
79
+ example_config: ExampleUtilityConfig | None = None,
65
80
  # Workflow
66
81
  next_agent: DynamicStr | None = None,
67
82
  temporal_activity_config: TemporalActivityConfig | None = None,
@@ -99,6 +114,10 @@ class DefaultAgent(FlockAgent):
99
114
  write_to_file: Save outputs to file
100
115
  wait_for_input: Wait for user input after execution
101
116
  alert_latency_threshold_ms: Threshold for latency alerts
117
+ enable_feedback: Whether to enable feedback learning component
118
+ feedback_config: Configuration for feedback component
119
+ enable_examples: Whether to enable example learning component
120
+ example_config: Configuration for example component
102
121
  next_agent: Next agent in workflow chain
103
122
  temporal_activity_config: Configuration for Temporal workflow execution
104
123
  """
@@ -161,6 +180,23 @@ class DefaultAgent(FlockAgent):
161
180
  name="metrics_tracker", config=metrics_config
162
181
  )
163
182
 
183
+ # Feedback utility component (optional)
184
+ components = [evaluator, output_component, metrics_component]
185
+ if enable_feedback:
186
+ feedback_component = FeedbackUtilityComponent(
187
+ name="feedback",
188
+ config=feedback_config or FeedbackUtilityConfig()
189
+ )
190
+ components.append(feedback_component)
191
+
192
+ # Example utility component (optional)
193
+ if enable_examples:
194
+ example_component = ExampleUtilityComponent(
195
+ name="examples",
196
+ config=example_config or ExampleUtilityConfig()
197
+ )
198
+ components.append(example_component)
199
+
164
200
  super().__init__(
165
201
  name=name,
166
202
  model=model,
@@ -170,7 +206,7 @@ class DefaultAgent(FlockAgent):
170
206
  tools=tools,
171
207
  servers=servers,
172
208
  tool_whitelist=tool_whitelist,
173
- components=[evaluator, output_component, metrics_component],
209
+ components=components,
174
210
  config=FlockAgentConfig(
175
211
  write_to_file=write_to_file,
176
212
  wait_for_input=wait_for_input,
@@ -443,6 +443,30 @@ class FlockFactory:
443
443
  include_reasoning: bool = False,
444
444
  next_agent: DynamicStr | None = None,
445
445
  temporal_activity_config: TemporalActivityConfig | None = None,
446
+ # Feedback parameters
447
+ enable_feedback: bool = False,
448
+ feedback_storage_type: Literal["sqlite", "azure"] = "sqlite",
449
+ feedback_max_items: int = 5,
450
+ feedback_timeframe_days: int = 30,
451
+ feedback_input_key: str = "feedback_context",
452
+ feedback_include_expected_responses: bool = True,
453
+ feedback_include_actual_responses: bool = False,
454
+ feedback_filter_keywords: list[str] | None = None,
455
+ feedback_exclude_keywords: list[str] | None = None,
456
+ feedback_sqlite_db_path: str = "./flock_feedback.db",
457
+ feedback_azure_connection_string: str | None = None,
458
+ feedback_azure_table_name: str = "flockfeedback",
459
+ # Example parameters
460
+ enable_examples: bool = False,
461
+ example_storage_type: Literal["sqlite", "azure"] = "sqlite",
462
+ example_max_examples: int = 5,
463
+ example_timeframe_days: int = 30,
464
+ example_input_key: str = "examples_context",
465
+ example_filter_keywords: list[str] | None = None,
466
+ example_exclude_keywords: list[str] | None = None,
467
+ example_sqlite_db_path: str = "./flock_examples.db",
468
+ example_azure_connection_string: str | None = None,
469
+ example_azure_table_name: str = "flockexamples",
446
470
  ) -> FlockAgent:
447
471
  """Create a default FlockAgent.
448
472
 
@@ -452,6 +476,40 @@ class FlockFactory:
452
476
  """
453
477
  _maybe_warn_factory_deprecation()
454
478
 
479
+ # Configure feedback if enabled
480
+ feedback_config = None
481
+ if enable_feedback:
482
+ from flock.components.utility.feedback_utility_component import FeedbackUtilityConfig
483
+ feedback_config = FeedbackUtilityConfig(
484
+ storage_type=feedback_storage_type,
485
+ max_feedback_items=feedback_max_items,
486
+ feedback_timeframe_days=feedback_timeframe_days,
487
+ feedback_input_key=feedback_input_key,
488
+ include_expected_responses=feedback_include_expected_responses,
489
+ include_actual_responses=feedback_include_actual_responses,
490
+ feedback_filter_keywords=feedback_filter_keywords or [],
491
+ feedback_exclude_keywords=feedback_exclude_keywords or [],
492
+ sqlite_db_path=feedback_sqlite_db_path,
493
+ azure_connection_string=feedback_azure_connection_string,
494
+ azure_table_name=feedback_azure_table_name,
495
+ )
496
+
497
+ # Configure examples if enabled
498
+ example_config = None
499
+ if enable_examples:
500
+ from flock.components.utility.example_utility_component import ExampleUtilityConfig
501
+ example_config = ExampleUtilityConfig(
502
+ storage_type=example_storage_type,
503
+ max_examples=example_max_examples,
504
+ example_timeframe_days=example_timeframe_days,
505
+ example_input_key=example_input_key,
506
+ example_filter_keywords=example_filter_keywords or [],
507
+ example_exclude_keywords=example_exclude_keywords or [],
508
+ sqlite_db_path=example_sqlite_db_path,
509
+ azure_connection_string=example_azure_connection_string,
510
+ azure_table_name=example_azure_table_name,
511
+ )
512
+
455
513
  return DefaultAgent(
456
514
  name=name,
457
515
  description=description,
@@ -477,6 +535,10 @@ class FlockFactory:
477
535
  alert_latency_threshold_ms=alert_latency_threshold_ms,
478
536
  next_agent=next_agent,
479
537
  temporal_activity_config=temporal_activity_config,
538
+ enable_feedback=enable_feedback,
539
+ feedback_config=feedback_config,
540
+ enable_examples=enable_examples,
541
+ example_config=example_config,
480
542
  )
481
543
 
482
544
  @staticmethod
@@ -5,6 +5,7 @@ import sqlite3
5
5
  from abc import ABC, abstractmethod
6
6
  from pathlib import Path
7
7
  from typing import Any
8
+ from datetime import datetime
8
9
 
9
10
  import aiosqlite
10
11
 
@@ -65,6 +66,22 @@ class SharedLinkStoreInterface(ABC):
65
66
  async def get_all_feedback_records_for_agent(self, agent_name: str) -> list[FeedbackRecord]:
66
67
  """Get all feedback records for a given agent."""
67
68
  pass
69
+
70
+ # Examples
71
+ @abstractmethod
72
+ async def save_example(self, example: Any):
73
+ """Persist an example record."""
74
+ pass
75
+
76
+ @abstractmethod
77
+ async def get_example(self, id: str) -> Any | None:
78
+ """Get a single example record."""
79
+ pass
80
+
81
+ @abstractmethod
82
+ async def get_all_examples_for_agent(self, agent_name: str) -> list[Any]:
83
+ """Get all example records for a given agent."""
84
+ pass
68
85
 
69
86
  class SQLiteSharedLinkStore(SharedLinkStoreInterface):
70
87
  """SQLite implementation for storing and retrieving shared link configurations."""
@@ -99,18 +116,13 @@ class SQLiteSharedLinkStore(SharedLinkStoreInterface):
99
116
  ("chat_history_key", "TEXT"),
100
117
  ("chat_response_key", "TEXT")
101
118
  ]
102
-
103
- for column_name, column_type in new_columns:
119
+ for col_name, col_def in new_columns:
104
120
  try:
105
- await db.execute(f"ALTER TABLE shared_links ADD COLUMN {column_name} {column_type}")
106
- logger.info(f"Added column '{column_name}' to shared_links table.")
107
- except sqlite3.OperationalError as e:
108
- if "duplicate column name" in str(e).lower():
109
- logger.debug(f"Column '{column_name}' already exists in shared_links table.")
110
- else:
111
- raise # Re-raise if it's a different operational error
112
-
113
- # Feedback table
121
+ await db.execute(f"ALTER TABLE shared_links ADD COLUMN {col_name} {col_def}")
122
+ except sqlite3.OperationalError:
123
+ pass # Column already exists
124
+
125
+ # Create feedback table if it doesn't exist
114
126
  await db.execute(
115
127
  """
116
128
  CREATE TABLE IF NOT EXISTS feedback (
@@ -123,16 +135,27 @@ class SQLiteSharedLinkStore(SharedLinkStoreInterface):
123
135
  flock_name TEXT,
124
136
  agent_name TEXT,
125
137
  flock_definition TEXT,
138
+ created_at TEXT NOT NULL
139
+ )
140
+ """
141
+ )
142
+
143
+ # Create examples table if it doesn't exist
144
+ await db.execute(
145
+ """
146
+ CREATE TABLE IF NOT EXISTS examples (
147
+ example_id TEXT PRIMARY KEY,
148
+ agent_name TEXT NOT NULL,
149
+ content TEXT NOT NULL,
126
150
  created_at TEXT NOT NULL,
127
- FOREIGN KEY(share_id) REFERENCES shared_links(share_id)
151
+ context_type TEXT NOT NULL DEFAULT 'example'
128
152
  )
129
153
  """
130
154
  )
131
155
 
132
156
  await db.commit()
133
- logger.info(f"Database initialized and shared_links table schema ensured at {self.db_path}")
134
- except sqlite3.Error as e:
135
- logger.error(f"SQLite error during initialization: {e}", exc_info=True)
157
+ except Exception as e:
158
+ logger.error(f"Error initializing database: {e}", exc_info=True)
136
159
  raise
137
160
 
138
161
  async def save_config(self, config: SharedLinkConfig) -> SharedLinkConfig:
@@ -304,6 +327,76 @@ class SQLiteSharedLinkStore(SharedLinkStoreInterface):
304
327
  logger.error(f"SQLite error saving feedback {record.feedback_id}: {e}", exc_info=True)
305
328
  raise
306
329
 
330
+ async def save_example(self, example: Any) -> Any:
331
+ """Persist an example record to SQLite."""
332
+ try:
333
+ example_dict = example.to_dict()
334
+ async with aiosqlite.connect(self.db_path) as db:
335
+ await db.execute(
336
+ """INSERT INTO examples (
337
+ example_id, agent_name, content, created_at, context_type
338
+ ) VALUES (?, ?, ?, ?, ?)""",
339
+ (
340
+ example_dict["example_id"],
341
+ example_dict["agent_name"],
342
+ example_dict["content"],
343
+ example_dict["created_at"],
344
+ example_dict["context_type"],
345
+ ),
346
+ )
347
+ await db.commit()
348
+ logger.info(f"Saved example {example_dict['example_id']} for agent {example_dict['agent_name']}")
349
+ return example
350
+ except sqlite3.Error as e:
351
+ logger.error(f"SQLite error saving example {example.example_id}: {e}", exc_info=True)
352
+ raise
353
+
354
+ async def get_example(self, id: str) -> Any | None:
355
+ """Get a single example record."""
356
+ try:
357
+ async with aiosqlite.connect(self.db_path) as db, db.execute(
358
+ """SELECT example_id, agent_name, content, created_at FROM examples WHERE example_id = ?""",
359
+ (id,)
360
+ ) as cursor:
361
+ row = await cursor.fetchone()
362
+ if row:
363
+ from flock.components.utility.example_utility_component import ExampleRecord
364
+ return ExampleRecord(
365
+ example_id=row[0],
366
+ agent_name=row[1],
367
+ content=row[2],
368
+ created_at=datetime.fromisoformat(row[3])
369
+ )
370
+ return None
371
+ except sqlite3.Error as e:
372
+ logger.error(f"SQLite error retrieving example {id}: {e}", exc_info=True)
373
+ return None
374
+
375
+ async def get_all_examples_for_agent(self, agent_name: str) -> list[Any]:
376
+ """Retrieve all example records from SQLite."""
377
+ try:
378
+ async with aiosqlite.connect(self.db_path) as db, db.execute(
379
+ """SELECT example_id, agent_name, content, created_at FROM examples WHERE agent_name = ? ORDER BY created_at DESC""",
380
+ (agent_name,)
381
+ ) as cursor:
382
+ rows = await cursor.fetchall()
383
+
384
+ examples = []
385
+ for row in rows:
386
+ from flock.components.utility.example_utility_component import ExampleRecord
387
+ examples.append(ExampleRecord(
388
+ example_id=row[0],
389
+ agent_name=row[1],
390
+ content=row[2],
391
+ created_at=datetime.fromisoformat(row[3])
392
+ ))
393
+
394
+ logger.debug(f"Retrieved {len(examples)} example records")
395
+ return examples
396
+ except sqlite3.Error as e:
397
+ logger.error(f"SQLite error retrieving all example records: {e}", exc_info=True)
398
+ return [] # Return empty list on error
399
+
307
400
 
308
401
  # ---------------------------------------------------------------------------
309
402
  # Azure Table + Blob implementation
@@ -568,6 +661,77 @@ class AzureTableSharedLinkStore(SharedLinkStoreInterface):
568
661
  )
569
662
  return records
570
663
 
664
+ # -------------------------------------------------------- save_example --
665
+ async def save_example(self, example: Any) -> Any:
666
+ """Persist an example record to Azure Table Storage."""
667
+ tbl_client = self.table_svc.get_table_client("flockexamples")
668
+
669
+ example_dict = example.to_dict()
670
+
671
+ entity = {
672
+ "PartitionKey": "examples",
673
+ "RowKey": example_dict["example_id"],
674
+ "agent_name": example_dict["agent_name"],
675
+ "content": example_dict["content"],
676
+ "created_at": example_dict["created_at"],
677
+ "context_type": example_dict["context_type"]
678
+ }
679
+
680
+ # ------------------------------------------------------------------ Table upsert
681
+ await tbl_client.upsert_entity(entity)
682
+ logger.info(f"Saved example {example_dict['example_id']} for agent {example_dict['agent_name']}")
683
+ return example
684
+
685
+ # -------------------------------------------------------- get_example --
686
+ async def get_example(self, id: str) -> Any | None:
687
+ """Retrieve a single example record from Azure Table Storage."""
688
+ tbl_client = self.table_svc.get_table_client("flockexamples")
689
+ try:
690
+ entity = await tbl_client.get_entity("examples", id)
691
+ except ResourceNotFoundError:
692
+ logger.debug("No example record found for ID: %s", id)
693
+ return None
694
+
695
+ from flock.components.utility.example_utility_component import ExampleRecord
696
+ return ExampleRecord(
697
+ example_id=id,
698
+ agent_name=entity["agent_name"],
699
+ content=entity["content"],
700
+ created_at=entity["created_at"]
701
+ )
702
+
703
+ # ------------------------------------------- get_all_examples_for_agent --
704
+ async def get_all_examples_for_agent(self, agent_name: str) -> list[Any]:
705
+ """Retrieve all example records from Azure Table Storage for a specific agent."""
706
+ tbl_client = self.table_svc.get_table_client("flockexamples")
707
+
708
+ # Use Azure Table Storage filtering to only get records for the specified agent
709
+ escaped_agent_name = agent_name.replace("'", "''")
710
+ filter_query = f"agent_name eq '{escaped_agent_name}'"
711
+
712
+ logger.debug(f"Querying example records with filter: {filter_query}")
713
+
714
+ examples = []
715
+ try:
716
+ async for entity in tbl_client.query_entities(filter_query):
717
+ from flock.components.utility.example_utility_component import ExampleRecord
718
+ examples.append(ExampleRecord(
719
+ example_id=entity["RowKey"],
720
+ agent_name=entity["agent_name"],
721
+ content=entity["content"],
722
+ created_at=entity["created_at"]
723
+ ))
724
+
725
+ logger.debug("Retrieved %d example records for agent %s", len(examples), agent_name)
726
+ return examples
727
+
728
+ except Exception as e:
729
+ # Log the error.
730
+ logger.error(
731
+ f"Unable to query example entries for agent {agent_name}. Exception: {e}"
732
+ )
733
+ return examples
734
+
571
735
 
572
736
  # ----------------------- Factory Function -----------------------
573
737
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flock-core
3
- Version: 0.5.0b27
3
+ Version: 0.5.0b28
4
4
  Summary: Declarative LLM Orchestration at Scale
5
5
  Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
6
6
  License-File: LICENSE
@@ -32,18 +32,20 @@ flock/components/routing/__init__.py,sha256=BH_pFm9T6bUuf8HH4byDJ0dO0fzEVHv9m-gh
32
32
  flock/components/routing/conditional_routing_component.py,sha256=WqZLMz-0Dhfb97xvttNrJCIVe6FNMLEQ2m4KQTDpIbI,21374
33
33
  flock/components/routing/default_routing_component.py,sha256=ZHt2Kjf-GHB5n7evU5NSGeQJ1Wuims5soeMswqaUb1E,3370
34
34
  flock/components/routing/llm_routing_component.py,sha256=SAaOFjlnhnenM6QEBn3WIpjjNXO-tFpP44TS73zvqzQ,7502
35
- flock/components/utility/__init__.py,sha256=JRj932upddjzZMWs1avOupEFr_GZNu21ac66Rhw_XgY,532
35
+ flock/components/utility/__init__.py,sha256=FRgYC06ko6mlNiGDCpYCez_61LQ4s7YJKI2Rys8Ba6w,861
36
+ flock/components/utility/example_utility_component.py,sha256=PUbObpFaBG8dtM3llZemjT9RS_R_wsolZ2bqqrHB6-8,9206
37
+ flock/components/utility/feedback_utility_component.py,sha256=XsE-em-KGvrkwQeDvNrzliSlvIGb9EoikKSn7kgiK4c,8144
36
38
  flock/components/utility/memory_utility_component.py,sha256=26Io61bbCGjD8UQ4BltMA5RLkMXp8tQoQmddXbQSrzA,20183
37
39
  flock/components/utility/metrics_utility_component.py,sha256=Mck_sFCkfXvNpoSgW2N_WOLnjxazzx8jh79tIx5zJhw,24635
38
40
  flock/components/utility/output_utility_component.py,sha256=TdHhY5qJJDUk-_LK54zAFMSG_Zafe-UiEkwiJwPjfh0,8063
39
41
  flock/core/__init__.py,sha256=OkjsVjRkAB-I6ibeTKVikZ3MxLIcTIzWKphHTbzbr7s,3231
40
42
  flock/core/flock.py,sha256=wRycQlGeaq-Vd75mFpPe02qyWTOEyXthT873iBhA3TI,23388
41
43
  flock/core/flock_agent.py,sha256=4Vdhyk-rdsPEuN3xYBsLBBsfpklad6bNj_it9r6XIDc,12868
42
- flock/core/flock_factory.py,sha256=Z6GJpYXN9_DXuOqvBH9ir0SMoUw78DkWhrhkm90luAQ,20910
44
+ flock/core/flock_factory.py,sha256=1EI4lj9SDlNAR1RSg1-39zevDjXe9RRn37CsQbk2MoA,24145
43
45
  flock/core/flock_scheduler.py,sha256=ng_s7gyijmc-AmYvBn5rtg61CSUZiIkXPRSlA1xO6VQ,8766
44
46
  flock/core/flock_server_manager.py,sha256=tM_nOs37vAbEvxmhwy_DL2JPvgFViWroNxrRSu5MfUQ,4523
45
47
  flock/core/agent/__init__.py,sha256=l32KFMJnC_gidMXpAXK8-OX228bWOhNc8OY_NzXm59Q,515
46
- flock/core/agent/default_agent.py,sha256=faeWCgMbMmU_QyTxeBxxc_nsD3h-wHHVTzUaFXgvcM4,7653
48
+ flock/core/agent/default_agent.py,sha256=Y_Kd-ziScW3o1M_q9z2NfAIaHtMuvYQjwF-Hn7ySbpY,9126
47
49
  flock/core/agent/flock_agent_components.py,sha256=LamOgpRC7wDKuU3d6enDG0UFlNxyKPErLpH7SQ_Pi74,4539
48
50
  flock/core/agent/flock_agent_execution.py,sha256=pdOddBGv8y1P89Ix8XFWa1eW9i3bWjOYiQQxeY2K0yo,4217
49
51
  flock/core/agent/flock_agent_integration.py,sha256=fnxzEA8-gIopHwD1de8QKt2A7Ilb1iH5Koxk1uiASas,10737
@@ -497,7 +499,7 @@ flock/webapp/app/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
497
499
  flock/webapp/app/services/feedback_file_service.py,sha256=6WYJjml8lt_ULH5vq7JSWrPQPvUSLvp91qMBt-_tg5Q,9376
498
500
  flock/webapp/app/services/flock_service.py,sha256=a2gcmx7_8uxSnu_Y3IlEl6e-JbeWO5_ejDyE5yOKVeA,14929
499
501
  flock/webapp/app/services/sharing_models.py,sha256=XeJk1akILV_1l-cIUaG8k_eYhjV3EWBCWZ2kpwbdImA,3609
500
- flock/webapp/app/services/sharing_store.py,sha256=Ee7D-2g44oM_PixxEuN1oqe1PmBqONHvfo3LY7SMlzY,27164
502
+ flock/webapp/app/services/sharing_store.py,sha256=eFcNzfLrMmmjXJkvbJQt3SeKoNyVKqRR-QMXl_AVlos,33929
501
503
  flock/webapp/app/templates/theme_mapper.html,sha256=z8ZY7nmk6PiUGzD_-px7wSXcEnuBM121rMq6u-2oaCo,14249
502
504
  flock/webapp/static/css/chat.css,sha256=Njc9gXfQzbXMrqtFJH2Yda-IQlwNPd2z4apXxzfA0sY,8169
503
505
  flock/webapp/static/css/components.css,sha256=WnicEHy3ptPzggKmyG9_oZp3X30EMJBUW3KEXaiUCUE,6018
@@ -552,8 +554,8 @@ flock/workflow/agent_execution_activity.py,sha256=0exwmeWKYXXxdUqDf4YaUVpn0zl06S
552
554
  flock/workflow/flock_workflow.py,sha256=sKFsRIL_bDGonXSNhK1zwu6UechghC_PihJJMidI-VI,9139
553
555
  flock/workflow/temporal_config.py,sha256=3_8O7SDEjMsSMXsWJBfnb6XTp0TFaz39uyzSlMTSF_I,3988
554
556
  flock/workflow/temporal_setup.py,sha256=KR6MlWOrpMtv8NyhaIPAsfl4tjobt81OBByQvg8Kw-Y,1948
555
- flock_core-0.5.0b27.dist-info/METADATA,sha256=CZTMnb9cOHSI6w5UQHSR9H9Ps3RKIakJndLB-OX66Vw,9997
556
- flock_core-0.5.0b27.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
557
- flock_core-0.5.0b27.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
558
- flock_core-0.5.0b27.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
559
- flock_core-0.5.0b27.dist-info/RECORD,,
557
+ flock_core-0.5.0b28.dist-info/METADATA,sha256=X_aEWYlTDI-nVJnkxCKu_4OKBMSS604qO5UHVeFn56U,9997
558
+ flock_core-0.5.0b28.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
559
+ flock_core-0.5.0b28.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
560
+ flock_core-0.5.0b28.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
561
+ flock_core-0.5.0b28.dist-info/RECORD,,