flock-core 0.5.20__py3-none-any.whl → 0.5.22__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/api/models.py +3 -2
- flock/api/service.py +0 -1
- flock/core/agent.py +50 -11
- flock/core/orchestrator.py +1 -5
- flock/core/subscription.py +151 -8
- flock/dashboard/events.py +1 -1
- flock/engines/dspy/streaming_executor.py +483 -529
- flock/engines/streaming/__init__.py +3 -0
- flock/engines/streaming/sinks.py +489 -0
- flock/semantic/__init__.py +49 -0
- flock/semantic/context_provider.py +173 -0
- flock/semantic/embedding_service.py +235 -0
- flock_core-0.5.22.dist-info/METADATA +976 -0
- {flock_core-0.5.20.dist-info → flock_core-0.5.22.dist-info}/RECORD +17 -12
- flock_core-0.5.20.dist-info/METADATA +0 -1327
- {flock_core-0.5.20.dist-info → flock_core-0.5.22.dist-info}/WHEEL +0 -0
- {flock_core-0.5.20.dist-info → flock_core-0.5.22.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.20.dist-info → flock_core-0.5.22.dist-info}/licenses/LICENSE +0 -0
flock/api/models.py
CHANGED
|
@@ -20,8 +20,9 @@ class AgentSubscription(BaseModel):
|
|
|
20
20
|
"""Subscription configuration for an agent."""
|
|
21
21
|
|
|
22
22
|
types: list[str] = Field(description="Artifact types this subscription consumes")
|
|
23
|
-
mode: str = Field(
|
|
24
|
-
|
|
23
|
+
mode: str = Field(
|
|
24
|
+
description="Subscription mode (e.g., 'both', 'direct', 'events')"
|
|
25
|
+
)
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class Agent(BaseModel):
|
flock/api/service.py
CHANGED
flock/core/agent.py
CHANGED
|
@@ -19,7 +19,7 @@ from flock.agent.mcp_integration import MCPIntegration
|
|
|
19
19
|
# Phase 4: Import extracted modules
|
|
20
20
|
from flock.agent.output_processor import OutputProcessor
|
|
21
21
|
from flock.core.artifacts import Artifact, ArtifactSpec
|
|
22
|
-
from flock.core.subscription import BatchSpec, JoinSpec, Subscription
|
|
22
|
+
from flock.core.subscription import BatchSpec, JoinSpec, Subscription
|
|
23
23
|
from flock.core.visibility import AgentIdentity, Visibility, ensure_visibility
|
|
24
24
|
from flock.logging.auto_trace import AutoTracedMeta
|
|
25
25
|
from flock.logging.logging import get_logger
|
|
@@ -539,13 +539,16 @@ class AgentBuilder:
|
|
|
539
539
|
where: Callable[[BaseModel], bool]
|
|
540
540
|
| Sequence[Callable[[BaseModel], bool]]
|
|
541
541
|
| None = None,
|
|
542
|
-
|
|
543
|
-
|
|
542
|
+
semantic_match: str
|
|
543
|
+
| list[str]
|
|
544
|
+
| list[dict[str, Any]]
|
|
545
|
+
| dict[str, Any]
|
|
546
|
+
| None = None,
|
|
547
|
+
semantic_threshold: float = 0.0,
|
|
544
548
|
from_agents: Iterable[str] | None = None,
|
|
545
549
|
tags: Iterable[str] | None = None,
|
|
546
550
|
join: dict | JoinSpec | None = None,
|
|
547
551
|
batch: dict | BatchSpec | None = None,
|
|
548
|
-
delivery: str = "exclusive",
|
|
549
552
|
mode: str = "both",
|
|
550
553
|
priority: int = 0,
|
|
551
554
|
) -> AgentBuilder:
|
|
@@ -558,14 +561,21 @@ class AgentBuilder:
|
|
|
558
561
|
*types: Artifact types (Pydantic models) to consume
|
|
559
562
|
where: Optional filter predicate(s). Agent only executes if predicate returns True.
|
|
560
563
|
Can be a single callable or sequence of callables (all must pass).
|
|
561
|
-
|
|
562
|
-
|
|
564
|
+
semantic_match: Optional semantic similarity filter. Matches artifacts based on
|
|
565
|
+
meaning rather than keywords. Can be:
|
|
566
|
+
- str: Single query (e.g., "security vulnerability")
|
|
567
|
+
- list[str]: Multiple queries, all must match (AND logic)
|
|
568
|
+
- dict: Advanced config with "query", "threshold", "field"
|
|
569
|
+
- list[dict]: Multiple queries with individual thresholds
|
|
570
|
+
semantic_threshold: Minimum similarity threshold for semantic matching (0.0-1.0).
|
|
571
|
+
Applied to all queries when semantic_match is a string or list of strings.
|
|
572
|
+
Ignored if semantic_match is a dict/list of dicts with explicit "threshold".
|
|
573
|
+
Default: 0.0 (uses default 0.4 when not specified)
|
|
563
574
|
from_agents: Only consume artifacts from specific agents
|
|
564
575
|
tags: Only consume artifacts with matching tags
|
|
565
576
|
join: Join specification for coordinating multiple artifact types
|
|
566
577
|
batch: Batch specification for processing multiple artifacts together
|
|
567
|
-
|
|
568
|
-
mode: Processing mode - "both", "streaming", or "batch"
|
|
578
|
+
mode: Processing mode - "both", "direct", or "events"
|
|
569
579
|
priority: Execution priority (higher = executes first)
|
|
570
580
|
|
|
571
581
|
Returns:
|
|
@@ -587,6 +597,12 @@ class AgentBuilder:
|
|
|
587
597
|
... where=[lambda o: o.total > 100, lambda o: o.status == "pending"],
|
|
588
598
|
... )
|
|
589
599
|
|
|
600
|
+
>>> # Semantic matching
|
|
601
|
+
>>> agent.consumes(Ticket, semantic_match="security vulnerability")
|
|
602
|
+
|
|
603
|
+
>>> # Semantic matching with custom threshold
|
|
604
|
+
>>> agent.consumes(Ticket, semantic_match="urgent", semantic_threshold=0.6)
|
|
605
|
+
|
|
590
606
|
>>> # Consume from specific agents
|
|
591
607
|
>>> agent.consumes(Report, from_agents=["analyzer", "validator"])
|
|
592
608
|
|
|
@@ -607,17 +623,40 @@ class AgentBuilder:
|
|
|
607
623
|
# Phase 5B: Use BuilderValidator for normalization
|
|
608
624
|
join_spec = BuilderValidator.normalize_join(join)
|
|
609
625
|
batch_spec = BuilderValidator.normalize_batch(batch)
|
|
610
|
-
|
|
626
|
+
|
|
627
|
+
# Handle semantic_threshold parameter to control semantic matching threshold
|
|
628
|
+
# If semantic_threshold is provided and semantic_match is simple, convert to dict
|
|
629
|
+
semantic_param: (
|
|
630
|
+
str | list[str] | list[dict[str, Any]] | dict[str, Any] | None
|
|
631
|
+
) = semantic_match
|
|
632
|
+
if semantic_match is not None and semantic_threshold > 0.0:
|
|
633
|
+
if isinstance(semantic_match, str):
|
|
634
|
+
# Simple string: create dict with semantic_threshold as threshold
|
|
635
|
+
semantic_param = {
|
|
636
|
+
"query": semantic_match,
|
|
637
|
+
"threshold": semantic_threshold,
|
|
638
|
+
}
|
|
639
|
+
elif isinstance(semantic_match, list):
|
|
640
|
+
# List of strings: convert to list of dicts with semantic_threshold
|
|
641
|
+
semantic_param = [
|
|
642
|
+
{"query": q, "threshold": semantic_threshold}
|
|
643
|
+
for q in semantic_match
|
|
644
|
+
]
|
|
645
|
+
elif isinstance(semantic_match, dict) and "threshold" not in semantic_match:
|
|
646
|
+
# Dict without explicit threshold: add semantic_threshold
|
|
647
|
+
semantic_param = {**semantic_match, "threshold": semantic_threshold}
|
|
648
|
+
|
|
649
|
+
# Semantic matching: pass semantic_match parameter to Subscription
|
|
650
|
+
# which will parse it into TextPredicate objects
|
|
611
651
|
subscription = Subscription(
|
|
612
652
|
agent_name=self._agent.name,
|
|
613
653
|
types=types,
|
|
614
654
|
where=predicates,
|
|
615
|
-
|
|
655
|
+
semantic_match=semantic_param, # Let Subscription handle conversion
|
|
616
656
|
from_agents=from_agents,
|
|
617
657
|
tags=tags,
|
|
618
658
|
join=join_spec,
|
|
619
659
|
batch=batch_spec,
|
|
620
|
-
delivery=delivery,
|
|
621
660
|
mode=mode,
|
|
622
661
|
priority=priority,
|
|
623
662
|
)
|
flock/core/orchestrator.py
CHANGED
|
@@ -111,7 +111,7 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
111
111
|
# Patch litellm imports and setup logger
|
|
112
112
|
self._patch_litellm_proxy_imports()
|
|
113
113
|
self._logger = logging.getLogger(__name__)
|
|
114
|
-
self.model = model
|
|
114
|
+
self.model = model or os.getenv("DEFAULT_MODEL")
|
|
115
115
|
|
|
116
116
|
# Phase 3: Initialize all components using OrchestratorInitializer
|
|
117
117
|
components = OrchestratorInitializer.initialize_components(
|
|
@@ -168,10 +168,6 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
168
168
|
self._scheduler = AgentScheduler(self, self._component_runner)
|
|
169
169
|
self._artifact_manager = ArtifactManager(self, self.store, self._scheduler)
|
|
170
170
|
|
|
171
|
-
# Resolve model default
|
|
172
|
-
if not model:
|
|
173
|
-
self.model = os.getenv("DEFAULT_MODEL")
|
|
174
|
-
|
|
175
171
|
# Log initialization
|
|
176
172
|
self._logger.debug("Orchestrator initialized: components=[]")
|
|
177
173
|
|
flock/core/subscription.py
CHANGED
|
@@ -21,8 +21,17 @@ Predicate = Callable[[BaseModel], bool]
|
|
|
21
21
|
|
|
22
22
|
@dataclass
|
|
23
23
|
class TextPredicate:
|
|
24
|
-
text
|
|
25
|
-
|
|
24
|
+
"""Semantic text matching predicate.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
query: The semantic query text to match against
|
|
28
|
+
threshold: Minimum similarity score (0.0 to 1.0) to consider a match
|
|
29
|
+
field: Optional field name to extract from payload. If None, uses all text.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
query: str
|
|
33
|
+
threshold: float = 0.4 # Default threshold for semantic matching
|
|
34
|
+
field: str | None = None # Optional field to extract from payload
|
|
26
35
|
|
|
27
36
|
|
|
28
37
|
@dataclass
|
|
@@ -97,21 +106,21 @@ class Subscription:
|
|
|
97
106
|
def __init__(
|
|
98
107
|
self,
|
|
99
108
|
*,
|
|
100
|
-
agent_name: str,
|
|
109
|
+
agent_name: str | None = None,
|
|
101
110
|
types: Sequence[type[BaseModel]],
|
|
102
111
|
where: Sequence[Predicate] | None = None,
|
|
103
112
|
text_predicates: Sequence[TextPredicate] | None = None,
|
|
113
|
+
semantic_match: str | list[str | dict[str, Any]] | dict[str, Any] | None = None,
|
|
104
114
|
from_agents: Iterable[str] | None = None,
|
|
105
115
|
tags: Iterable[str] | None = None,
|
|
106
116
|
join: JoinSpec | None = None,
|
|
107
117
|
batch: BatchSpec | None = None,
|
|
108
|
-
delivery: str = "exclusive",
|
|
109
118
|
mode: str = "both",
|
|
110
119
|
priority: int = 0,
|
|
111
120
|
) -> None:
|
|
112
121
|
if not types:
|
|
113
122
|
raise ValueError("Subscription must declare at least one type.")
|
|
114
|
-
self.agent_name = agent_name
|
|
123
|
+
self.agent_name = agent_name or ""
|
|
115
124
|
self.type_models: list[type[BaseModel]] = list(types)
|
|
116
125
|
|
|
117
126
|
# Register all types and build counts (supports duplicates for count-based AND gates)
|
|
@@ -127,15 +136,62 @@ class Subscription:
|
|
|
127
136
|
self.type_counts[type_name] = self.type_counts.get(type_name, 0) + 1
|
|
128
137
|
|
|
129
138
|
self.where = list(where or [])
|
|
130
|
-
|
|
139
|
+
|
|
140
|
+
# Parse semantic_match parameter into TextPredicate objects
|
|
141
|
+
parsed_text_predicates = self._parse_semantic_match_parameter(semantic_match)
|
|
142
|
+
self.text_predicates = list(text_predicates or []) + parsed_text_predicates
|
|
143
|
+
|
|
131
144
|
self.from_agents = set(from_agents or [])
|
|
132
145
|
self.tags = set(tags or [])
|
|
133
146
|
self.join = join
|
|
134
147
|
self.batch = batch
|
|
135
|
-
self.delivery = delivery
|
|
136
148
|
self.mode = mode
|
|
137
149
|
self.priority = priority
|
|
138
150
|
|
|
151
|
+
def _parse_semantic_match_parameter(
|
|
152
|
+
self, semantic_match: str | list[str | dict[str, Any]] | dict[str, Any] | None
|
|
153
|
+
) -> list[TextPredicate]:
|
|
154
|
+
"""Parse the semantic_match parameter into TextPredicate objects.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
semantic_match: Can be:
|
|
158
|
+
- str: "query" → TextPredicate(query="query", threshold=0.4)
|
|
159
|
+
- list: ["q1", "q2"] → multiple TextPredicates (AND logic)
|
|
160
|
+
or [{"query": "q1", "threshold": 0.8}, ...] with explicit thresholds
|
|
161
|
+
- dict: {"query": "...", "threshold": 0.8, "field": "body"}
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of TextPredicate objects
|
|
165
|
+
"""
|
|
166
|
+
if semantic_match is None:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
if isinstance(semantic_match, str):
|
|
170
|
+
return [TextPredicate(query=semantic_match)]
|
|
171
|
+
|
|
172
|
+
if isinstance(semantic_match, list):
|
|
173
|
+
# Handle both list of strings and list of dicts
|
|
174
|
+
predicates = []
|
|
175
|
+
for item in semantic_match:
|
|
176
|
+
if isinstance(item, str):
|
|
177
|
+
predicates.append(TextPredicate(query=item))
|
|
178
|
+
elif isinstance(item, dict):
|
|
179
|
+
query = item.get("query", "")
|
|
180
|
+
threshold = item.get("threshold", 0.4)
|
|
181
|
+
field = item.get("field", None)
|
|
182
|
+
predicates.append(
|
|
183
|
+
TextPredicate(query=query, threshold=threshold, field=field)
|
|
184
|
+
)
|
|
185
|
+
return predicates
|
|
186
|
+
|
|
187
|
+
if isinstance(semantic_match, dict):
|
|
188
|
+
query = semantic_match.get("query", "")
|
|
189
|
+
threshold = semantic_match.get("threshold", 0.4) # Match dataclass default
|
|
190
|
+
field = semantic_match.get("field", None)
|
|
191
|
+
return [TextPredicate(query=query, threshold=threshold, field=field)]
|
|
192
|
+
|
|
193
|
+
return []
|
|
194
|
+
|
|
139
195
|
def accepts_direct(self) -> bool:
|
|
140
196
|
return self.mode in {"direct", "both"}
|
|
141
197
|
|
|
@@ -159,12 +215,99 @@ class Subscription:
|
|
|
159
215
|
return False
|
|
160
216
|
except Exception:
|
|
161
217
|
return False
|
|
218
|
+
|
|
219
|
+
# Evaluate text predicates using semantic matching
|
|
220
|
+
if self.text_predicates:
|
|
221
|
+
if not self._matches_text_predicates(artifact):
|
|
222
|
+
return False
|
|
223
|
+
|
|
162
224
|
return True
|
|
163
225
|
|
|
226
|
+
def _matches_text_predicates(self, artifact: Artifact) -> bool:
|
|
227
|
+
"""Check if artifact matches all text predicates (AND logic).
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
artifact: The artifact to check
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
bool: True if all text predicates match (or if semantic unavailable)
|
|
234
|
+
"""
|
|
235
|
+
# Check if semantic features available
|
|
236
|
+
try:
|
|
237
|
+
from flock.semantic import SEMANTIC_AVAILABLE, EmbeddingService
|
|
238
|
+
except ImportError:
|
|
239
|
+
# Graceful degradation - if semantic not available, skip text predicates
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
if not SEMANTIC_AVAILABLE:
|
|
243
|
+
# Graceful degradation
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
embedding_service = EmbeddingService.get_instance()
|
|
248
|
+
except Exception:
|
|
249
|
+
# If embedding service fails, degrade gracefully
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
# Extract text from artifact payload
|
|
253
|
+
artifact_text = self._extract_text_from_payload(artifact.payload)
|
|
254
|
+
if not artifact_text or not artifact_text.strip():
|
|
255
|
+
# No text to match against
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
# Check all predicates (AND logic)
|
|
259
|
+
for predicate in self.text_predicates:
|
|
260
|
+
try:
|
|
261
|
+
# Extract text based on field specification
|
|
262
|
+
if predicate.field:
|
|
263
|
+
# Use specific field
|
|
264
|
+
text_to_match = str(artifact.payload.get(predicate.field, ""))
|
|
265
|
+
else:
|
|
266
|
+
# Use all text from payload
|
|
267
|
+
text_to_match = artifact_text
|
|
268
|
+
|
|
269
|
+
if not text_to_match or not text_to_match.strip():
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
# Compute semantic similarity
|
|
273
|
+
similarity = embedding_service.similarity(
|
|
274
|
+
predicate.query, text_to_match
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Check threshold
|
|
278
|
+
if similarity < predicate.threshold:
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
except Exception:
|
|
282
|
+
# If any error occurs, fail the match
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
def _extract_text_from_payload(self, payload: dict[str, Any]) -> str:
|
|
288
|
+
"""Extract all text content from payload.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
payload: The artifact payload dict
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
str: Concatenated text from all string fields
|
|
295
|
+
"""
|
|
296
|
+
text_parts = []
|
|
297
|
+
for value in payload.values():
|
|
298
|
+
if isinstance(value, str):
|
|
299
|
+
text_parts.append(value)
|
|
300
|
+
elif isinstance(value, (list, tuple)):
|
|
301
|
+
for item in value:
|
|
302
|
+
if isinstance(item, str):
|
|
303
|
+
text_parts.append(item)
|
|
304
|
+
|
|
305
|
+
return " ".join(text_parts)
|
|
306
|
+
|
|
164
307
|
def __repr__(self) -> str: # pragma: no cover - debug helper
|
|
165
308
|
return (
|
|
166
309
|
f"Subscription(agent={self.agent_name!r}, types={list(self.type_names)!r}, "
|
|
167
|
-
f"
|
|
310
|
+
f"mode={self.mode!r})"
|
|
168
311
|
)
|
|
169
312
|
|
|
170
313
|
|
flock/dashboard/events.py
CHANGED
|
@@ -21,7 +21,7 @@ class SubscriptionInfo(BaseModel):
|
|
|
21
21
|
class VisibilitySpec(BaseModel):
|
|
22
22
|
"""Visibility specification for artifacts.
|
|
23
23
|
|
|
24
|
-
Matches visibility types from flock.visibility module.
|
|
24
|
+
Matches visibility types from flock.core.visibility module.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
kind: str # "Public" | "Private" | "Labelled" | "Tenant" | "After"
|