flock-core 0.5.9__py3-none-any.whl → 0.5.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (54) hide show
  1. flock/agent.py +149 -62
  2. flock/api/themes.py +6 -2
  3. flock/api_models.py +285 -0
  4. flock/artifact_collector.py +6 -3
  5. flock/batch_accumulator.py +3 -1
  6. flock/cli.py +3 -1
  7. flock/components.py +45 -56
  8. flock/context_provider.py +531 -0
  9. flock/correlation_engine.py +8 -4
  10. flock/dashboard/collector.py +48 -29
  11. flock/dashboard/events.py +10 -4
  12. flock/dashboard/launcher.py +3 -1
  13. flock/dashboard/models/graph.py +9 -3
  14. flock/dashboard/service.py +187 -93
  15. flock/dashboard/websocket.py +17 -4
  16. flock/engines/dspy_engine.py +174 -98
  17. flock/engines/examples/simple_batch_engine.py +9 -3
  18. flock/examples.py +6 -2
  19. flock/frontend/src/services/indexeddb.test.ts +4 -4
  20. flock/frontend/src/services/indexeddb.ts +1 -1
  21. flock/helper/cli_helper.py +14 -1
  22. flock/logging/auto_trace.py +6 -1
  23. flock/logging/formatters/enum_builder.py +3 -1
  24. flock/logging/formatters/theme_builder.py +32 -17
  25. flock/logging/formatters/themed_formatter.py +38 -22
  26. flock/logging/logging.py +21 -7
  27. flock/logging/telemetry.py +9 -3
  28. flock/logging/telemetry_exporter/duckdb_exporter.py +27 -25
  29. flock/logging/trace_and_logged.py +14 -5
  30. flock/mcp/__init__.py +3 -6
  31. flock/mcp/client.py +49 -19
  32. flock/mcp/config.py +12 -6
  33. flock/mcp/manager.py +6 -2
  34. flock/mcp/servers/sse/flock_sse_server.py +9 -3
  35. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +6 -2
  36. flock/mcp/tool.py +18 -6
  37. flock/mcp/types/handlers.py +3 -1
  38. flock/mcp/types/types.py +9 -3
  39. flock/orchestrator.py +449 -58
  40. flock/orchestrator_component.py +15 -5
  41. flock/patches/dspy_streaming_patch.py +12 -4
  42. flock/registry.py +9 -3
  43. flock/runtime.py +69 -18
  44. flock/service.py +135 -64
  45. flock/store.py +29 -10
  46. flock/subscription.py +6 -4
  47. flock/system_artifacts.py +33 -0
  48. flock/utilities.py +41 -13
  49. flock/utility/output_utility_component.py +31 -11
  50. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/METADATA +150 -26
  51. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/RECORD +54 -51
  52. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/WHEEL +0 -0
  53. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/entry_points.txt +0 -0
  54. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/licenses/LICENSE +0 -0
flock/api_models.py ADDED
@@ -0,0 +1,285 @@
1
+ """Pydantic response models for Flock REST API.
2
+
3
+ Provides proper OpenAPI schemas for all public API endpoints.
4
+ This improves API documentation and enables SDK generation.
5
+
6
+ All models maintain 100% backwards compatibility with existing wire format.
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Any, Literal
11
+ from uuid import UUID
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ # ============================================================================
17
+ # Agent Models
18
+ # ============================================================================
19
+
20
+
21
+ class AgentSubscription(BaseModel):
22
+ """Subscription configuration for an agent."""
23
+
24
+ types: list[str] = Field(description="Artifact types this subscription consumes")
25
+ mode: str = Field(description="Subscription mode (e.g., 'all', 'any')")
26
+ delivery: str = Field(description="Delivery mode (e.g., 'immediate', 'batch')")
27
+
28
+
29
+ class Agent(BaseModel):
30
+ """Single agent representation."""
31
+
32
+ name: str = Field(description="Unique name of the agent")
33
+ description: str = Field(default="", description="Human-readable description")
34
+ subscriptions: list[AgentSubscription] = Field(
35
+ description="List of subscriptions this agent listens to"
36
+ )
37
+ outputs: list[str] = Field(description="Artifact types this agent can produce")
38
+
39
+
40
+ class AgentListResponse(BaseModel):
41
+ """Response for GET /api/v1/agents."""
42
+
43
+ agents: list[Agent] = Field(description="List of all registered agents")
44
+
45
+
46
+ # ============================================================================
47
+ # Artifact Models
48
+ # ============================================================================
49
+
50
+
51
+ class VisibilityInfo(BaseModel):
52
+ """Artifact visibility configuration."""
53
+
54
+ kind: str = Field(description="Visibility kind (e.g., 'Public', 'Private')")
55
+ # Additional visibility fields added dynamically
56
+
57
+
58
+ class ArtifactBase(BaseModel):
59
+ """Base artifact representation with common fields."""
60
+
61
+ id: str = Field(description="Unique artifact identifier (UUID)")
62
+ type: str = Field(description="Artifact type name")
63
+ payload: dict[str, Any] = Field(description="Artifact payload data")
64
+ produced_by: str = Field(description="Name of agent/source that produced this")
65
+ visibility: dict[str, Any] = Field(description="Visibility configuration")
66
+ visibility_kind: str = Field(description="Visibility kind (Public/Private/etc)")
67
+ created_at: str = Field(
68
+ description="Timestamp when artifact was created (ISO 8601)"
69
+ )
70
+ correlation_id: str | None = Field(
71
+ None, description="Optional correlation ID for workflow tracking"
72
+ )
73
+ partition_key: str | None = Field(None, description="Optional partition key")
74
+ tags: list[str] = Field(default_factory=list, description="List of tags")
75
+ version: int = Field(description="Artifact version number")
76
+
77
+
78
+ class ConsumptionRecord(BaseModel):
79
+ """Record of an artifact being consumed by an agent."""
80
+
81
+ artifact_id: str = Field(description="ID of the artifact that was consumed")
82
+ consumer: str = Field(description="Name of the agent that consumed it")
83
+ run_id: str = Field(description="Run ID of the consumption")
84
+ correlation_id: str = Field(description="Correlation ID of the consumption")
85
+ consumed_at: str = Field(description="Timestamp of consumption (ISO 8601)")
86
+
87
+
88
+ class ArtifactWithConsumptions(ArtifactBase):
89
+ """Artifact with consumption metadata included."""
90
+
91
+ consumptions: list[ConsumptionRecord] = Field(
92
+ default_factory=list, description="List of consumption records"
93
+ )
94
+ consumed_by: list[str] = Field(
95
+ default_factory=list,
96
+ description="List of unique agent names that consumed this artifact",
97
+ )
98
+
99
+
100
+ class PaginationInfo(BaseModel):
101
+ """Pagination metadata."""
102
+
103
+ limit: int = Field(description="Number of items per page")
104
+ offset: int = Field(description="Offset into the result set")
105
+ total: int = Field(description="Total number of items matching the query")
106
+
107
+
108
+ class ArtifactListResponse(BaseModel):
109
+ """Response for GET /api/v1/artifacts."""
110
+
111
+ items: list[ArtifactBase | ArtifactWithConsumptions] = Field(
112
+ description="List of artifacts (may include consumption data if embed_meta=true)"
113
+ )
114
+ pagination: PaginationInfo = Field(description="Pagination information")
115
+
116
+
117
+ class ArtifactPublishRequest(BaseModel):
118
+ """Request body for POST /api/v1/artifacts."""
119
+
120
+ type: str = Field(description="Artifact type name")
121
+ payload: dict[str, Any] = Field(
122
+ default_factory=dict, description="Artifact payload data"
123
+ )
124
+
125
+
126
+ class ArtifactPublishResponse(BaseModel):
127
+ """Response for POST /api/v1/artifacts."""
128
+
129
+ status: Literal["accepted"] = Field(description="Publication status")
130
+
131
+
132
+ class ArtifactSummary(BaseModel):
133
+ """Summary statistics for artifacts."""
134
+
135
+ # Define based on actual summary structure from store
136
+ # This is a placeholder - update based on actual implementation
137
+
138
+
139
+ class ArtifactSummaryResponse(BaseModel):
140
+ """Response for GET /api/v1/artifacts/summary."""
141
+
142
+ summary: dict[str, Any] = Field(description="Summary statistics")
143
+
144
+
145
+ # ============================================================================
146
+ # Agent Run Models
147
+ # ============================================================================
148
+
149
+
150
+ class AgentRunInput(BaseModel):
151
+ """Input artifact for agent run."""
152
+
153
+ type: str = Field(description="Artifact type name")
154
+ payload: dict[str, Any] = Field(
155
+ default_factory=dict, description="Artifact payload data"
156
+ )
157
+
158
+
159
+ class AgentRunRequest(BaseModel):
160
+ """Request body for POST /api/v1/agents/{name}/run."""
161
+
162
+ inputs: list[AgentRunInput] = Field(
163
+ default_factory=list, description="List of input artifacts"
164
+ )
165
+
166
+
167
+ class ProducedArtifact(BaseModel):
168
+ """Artifact produced by agent run."""
169
+
170
+ id: str = Field(description="Artifact ID (UUID)")
171
+ type: str = Field(description="Artifact type name")
172
+ payload: dict[str, Any] = Field(description="Artifact payload data")
173
+ produced_by: str = Field(description="Name of agent that produced this")
174
+
175
+
176
+ class AgentRunResponse(BaseModel):
177
+ """Response for POST /api/v1/agents/{name}/run."""
178
+
179
+ artifacts: list[ProducedArtifact] = Field(
180
+ description="Artifacts produced by the agent run"
181
+ )
182
+
183
+
184
+ # ============================================================================
185
+ # Schema Discovery Models
186
+ # ============================================================================
187
+
188
+
189
+ class ArtifactTypeSchema(BaseModel):
190
+ """Schema information for an artifact type."""
191
+
192
+ model_config = {"populate_by_name": True} # Allow using 'schema' as field name
193
+
194
+ name: str = Field(description="Type name")
195
+ schema_: dict[str, Any] = Field(
196
+ alias="schema", description="JSON Schema for this type"
197
+ )
198
+
199
+
200
+ class ArtifactTypesResponse(BaseModel):
201
+ """Response for GET /api/artifact-types."""
202
+
203
+ artifact_types: list[ArtifactTypeSchema] = Field(
204
+ description="List of all registered artifact types with their schemas"
205
+ )
206
+
207
+
208
+ # ============================================================================
209
+ # Agent History Models
210
+ # ============================================================================
211
+
212
+
213
+ class AgentHistorySummary(BaseModel):
214
+ """Summary of agent execution history."""
215
+
216
+ agent_id: str = Field(description="Agent identifier")
217
+ summary: dict[str, Any] = Field(description="History summary statistics")
218
+
219
+
220
+ # ============================================================================
221
+ # Correlation Status Models
222
+ # ============================================================================
223
+
224
+
225
+ class CorrelationStatusResponse(BaseModel):
226
+ """Response for GET /api/v1/correlations/{correlation_id}/status."""
227
+
228
+ correlation_id: str = Field(description="The correlation ID")
229
+ state: Literal["active", "completed", "failed", "not_found"] = Field(
230
+ description="Workflow state: active (work pending), completed (success), failed (only errors), not_found (no artifacts)"
231
+ )
232
+ has_pending_work: bool = Field(
233
+ description="Whether the orchestrator has pending work for this correlation"
234
+ )
235
+ artifact_count: int = Field(
236
+ description="Total number of artifacts with this correlation_id"
237
+ )
238
+ error_count: int = Field(description="Number of WorkflowError artifacts")
239
+ started_at: str | None = Field(
240
+ None, description="Timestamp of first artifact (ISO 8601)"
241
+ )
242
+ last_activity_at: str | None = Field(
243
+ None, description="Timestamp of most recent artifact (ISO 8601)"
244
+ )
245
+
246
+
247
+ # ============================================================================
248
+ # Health & Metrics Models
249
+ # ============================================================================
250
+
251
+
252
+ class HealthResponse(BaseModel):
253
+ """Response for GET /health."""
254
+
255
+ status: Literal["ok"] = Field(description="Health status")
256
+
257
+
258
+ __all__ = [
259
+ # Agent models
260
+ "Agent",
261
+ "AgentSubscription",
262
+ "AgentListResponse",
263
+ # Artifact models
264
+ "ArtifactBase",
265
+ "ArtifactWithConsumptions",
266
+ "ArtifactListResponse",
267
+ "ArtifactPublishRequest",
268
+ "ArtifactPublishResponse",
269
+ "ArtifactSummaryResponse",
270
+ "PaginationInfo",
271
+ "ConsumptionRecord",
272
+ # Agent run models
273
+ "AgentRunRequest",
274
+ "AgentRunResponse",
275
+ "ProducedArtifact",
276
+ # Schema discovery
277
+ "ArtifactTypesResponse",
278
+ "ArtifactTypeSchema",
279
+ # History
280
+ "AgentHistorySummary",
281
+ # Correlation status
282
+ "CorrelationStatusResponse",
283
+ # Health
284
+ "HealthResponse",
285
+ ]
@@ -43,8 +43,8 @@ class ArtifactCollector:
43
43
  # Structure: {(agent_name, subscription_index): {type_name: [artifact1, artifact2, ...]}}
44
44
  # Example: {("diagnostician", 0): {"XRay": [artifact1], "LabResult": [artifact2]}}
45
45
  # For count-based AND gates: {"TypeA": [artifact1, artifact2, artifact3]} (3 As collected)
46
- self._waiting_pools: dict[tuple[str, int], dict[str, list[Artifact]]] = defaultdict(
47
- lambda: defaultdict(list)
46
+ self._waiting_pools: dict[tuple[str, int], dict[str, list[Artifact]]] = (
47
+ defaultdict(lambda: defaultdict(list))
48
48
  )
49
49
 
50
50
  def add_artifact(
@@ -72,7 +72,10 @@ class ArtifactCollector:
72
72
  - After returning complete=True, the pool is automatically cleared
73
73
  """
74
74
  # Single-type subscription with count=1: No waiting needed (immediate trigger)
75
- if len(subscription.type_names) == 1 and subscription.type_counts[artifact.type] == 1:
75
+ if (
76
+ len(subscription.type_names) == 1
77
+ and subscription.type_counts[artifact.type] == 1
78
+ ):
76
79
  return (True, [artifact])
77
80
 
78
81
  # Multi-type or count-based subscription: Use waiting pool (AND gate logic)
@@ -194,7 +194,9 @@ class BatchEngine:
194
194
 
195
195
  return False # Not ready to flush yet
196
196
 
197
- def flush_batch(self, agent_name: str, subscription_index: int) -> list[Artifact] | None:
197
+ def flush_batch(
198
+ self, agent_name: str, subscription_index: int
199
+ ) -> list[Artifact] | None:
198
200
  """
199
201
  Flush a batch and return its artifacts.
200
202
 
flock/cli.py CHANGED
@@ -123,7 +123,9 @@ def sqlite_maintenance(
123
123
  try:
124
124
  before_dt = datetime.fromisoformat(delete_before)
125
125
  except ValueError as exc: # pragma: no cover - Typer handles but defensive
126
- raise typer.BadParameter(f"Invalid ISO timestamp: {delete_before}") from exc
126
+ raise typer.BadParameter(
127
+ f"Invalid ISO timestamp: {delete_before}"
128
+ ) from exc
127
129
  deleted = await store.delete_before(before_dt)
128
130
  if vacuum:
129
131
  await store.vacuum()
flock/components.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Self
5
+ from typing import TYPE_CHECKING, Self
6
6
 
7
7
  from pydantic import BaseModel, Field, create_model
8
8
  from pydantic._internal._model_construction import ModelMetaclass
@@ -12,8 +12,6 @@ from flock.logging.auto_trace import AutoTracedMeta
12
12
 
13
13
 
14
14
  if TYPE_CHECKING: # pragma: no cover - type checking only
15
- from uuid import UUID
16
-
17
15
  from flock.agent import Agent, OutputGroup
18
16
  from flock.artifacts import Artifact
19
17
  from flock.runtime import Context, EvalInputs, EvalResult
@@ -71,7 +69,9 @@ class AgentComponent(BaseModel, metaclass=TracedModelMeta):
71
69
  ) -> list[Artifact]:
72
70
  return inputs
73
71
 
74
- async def on_pre_evaluate(self, agent: Agent, ctx: Context, inputs: EvalInputs) -> EvalInputs:
72
+ async def on_pre_evaluate(
73
+ self, agent: Agent, ctx: Context, inputs: EvalInputs
74
+ ) -> EvalInputs:
75
75
  return inputs
76
76
 
77
77
  async def on_post_evaluate(
@@ -89,7 +89,9 @@ class AgentComponent(BaseModel, metaclass=TracedModelMeta):
89
89
  ) -> None: # pragma: no cover - default
90
90
  return None
91
91
 
92
- async def on_terminate(self, agent: Agent, ctx: Context) -> None: # pragma: no cover - default
92
+ async def on_terminate(
93
+ self, agent: Agent, ctx: Context
94
+ ) -> None: # pragma: no cover - default
93
95
  return None
94
96
 
95
97
 
@@ -153,67 +155,54 @@ class EngineComponent(AgentComponent):
153
155
  """
154
156
  raise NotImplementedError
155
157
 
156
- async def fetch_conversation_context(
158
+ def get_conversation_context(
157
159
  self,
158
160
  ctx: Context,
159
- correlation_id: UUID | None = None,
160
161
  max_artifacts: int | None = None,
161
- ) -> list[dict[str, Any]]:
162
- """Fetch all artifacts with the same correlation_id for conversation context."""
163
- if not self.enable_context or not ctx:
164
- return []
162
+ ) -> list[Artifact]:
163
+ """Get conversation context from Context (read-only helper).
164
+
165
+ Phase 8 Security Fix: This method now simply reads pre-filtered artifacts from
166
+ Context. The orchestrator evaluates context BEFORE creating Context, so engines
167
+ can no longer query arbitrary data.
168
+
169
+ REMOVED METHODS (Security Fix):
170
+ - fetch_conversation_context() - REMOVED (engines can't query anymore)
171
+ - get_latest_artifact_of_type() - REMOVED (engines can't query anymore)
172
+
173
+ Migration Guide:
174
+ Old (vulnerable): context = await self.fetch_conversation_context(ctx, agent, exclude_ids)
175
+ New (secure): context = ctx.artifacts # Pre-filtered by orchestrator!
165
176
 
166
- target_correlation_id = correlation_id or getattr(ctx, "correlation_id", None)
167
- if not target_correlation_id:
177
+ Args:
178
+ ctx: Execution context with pre-filtered artifacts
179
+ max_artifacts: Optional limit (applies to already-filtered list)
180
+
181
+ Returns:
182
+ List of Artifact objects (pre-filtered by orchestrator via context provider)
183
+ with full metadata (type, payload, produced_by, created_at, tags, etc.)
184
+ """
185
+ if not self.enable_context or not ctx:
168
186
  return []
169
187
 
170
- try:
171
- all_artifacts = await ctx.board.list()
188
+ context_items = list(ctx.artifacts) # Copy to avoid mutation
172
189
 
173
- context_artifacts = [
174
- a
175
- for a in all_artifacts
176
- if (
177
- a.correlation_id == target_correlation_id
178
- and a.type not in self.context_exclude_types
179
- )
190
+ # Apply engine-level filtering (type exclusions)
191
+ if self.context_exclude_types:
192
+ context_items = [
193
+ item
194
+ for item in context_items
195
+ if item.type not in self.context_exclude_types
180
196
  ]
181
197
 
182
- context_artifacts.sort(key=lambda a: a.created_at)
183
-
184
- max_limit = max_artifacts if max_artifacts is not None else self.context_max_artifacts
185
- if max_limit is not None and max_limit > 0:
186
- context_artifacts = context_artifacts[-max_limit:]
187
-
188
- context = []
189
- i = 0
190
- for artifact in context_artifacts:
191
- context.append(
192
- {
193
- "type": artifact.type,
194
- "payload": artifact.payload,
195
- "produced_by": artifact.produced_by,
196
- "event_number": i,
197
- # "created_at": artifact.created_at.isoformat(),
198
- }
199
- )
200
- i += 1
201
-
202
- return context
203
-
204
- except Exception:
205
- return []
198
+ # Apply max artifacts limit
199
+ max_limit = (
200
+ max_artifacts if max_artifacts is not None else self.context_max_artifacts
201
+ )
202
+ if max_limit is not None and max_limit > 0:
203
+ context_items = context_items[-max_limit:]
206
204
 
207
- async def get_latest_artifact_of_type(
208
- self,
209
- ctx: Context,
210
- artifact_type: str,
211
- correlation_id: UUID | None = None,
212
- ) -> dict[str, Any] | None:
213
- """Get the most recent artifact of a specific type in the conversation."""
214
- context = await self.fetch_conversation_context(ctx, correlation_id)
215
- matching = [a for a in context if a["type"].endswith(artifact_type)]
216
- return matching[-1] if matching else None
205
+ return context_items
217
206
 
218
207
  def should_use_context(self, inputs: EvalInputs) -> bool:
219
208
  """Determine if context should be included based on the current inputs."""