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 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(description="Subscription mode (e.g., 'all', 'any')")
24
- delivery: str = Field(description="Delivery mode (e.g., 'immediate', 'batch')")
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
@@ -261,7 +261,6 @@ class BlackboardHTTPService:
261
261
  AgentSubscription(
262
262
  types=list(subscription.type_names),
263
263
  mode=subscription.mode,
264
- delivery=subscription.delivery,
265
264
  )
266
265
  for subscription in agent.subscriptions
267
266
  ],
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, TextPredicate
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
- text: str | None = None,
543
- min_p: float = 0.0,
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
- text: Optional semantic text filter using embedding similarity
562
- min_p: Minimum probability threshold for text similarity (0.0-1.0)
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
- delivery: Delivery mode - "exclusive" (one agent) or "broadcast" (all matching)
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
- text_predicates = [TextPredicate(text=text, min_p=min_p)] if text else []
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
- text_predicates=text_predicates,
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
  )
@@ -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
 
@@ -21,8 +21,17 @@ Predicate = Callable[[BaseModel], bool]
21
21
 
22
22
  @dataclass
23
23
  class TextPredicate:
24
- text: str
25
- min_p: float = 0.0
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
- self.text_predicates = list(text_predicates or [])
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"delivery={self.delivery!r}, mode={self.mode!r})"
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"