flock-core 0.5.11__py3-none-any.whl → 0.5.20__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 (91) hide show
  1. flock/__init__.py +1 -1
  2. flock/agent/__init__.py +30 -0
  3. flock/agent/builder_helpers.py +192 -0
  4. flock/agent/builder_validator.py +169 -0
  5. flock/agent/component_lifecycle.py +325 -0
  6. flock/agent/context_resolver.py +141 -0
  7. flock/agent/mcp_integration.py +212 -0
  8. flock/agent/output_processor.py +304 -0
  9. flock/api/__init__.py +20 -0
  10. flock/{api_models.py → api/models.py} +0 -2
  11. flock/{service.py → api/service.py} +3 -3
  12. flock/cli.py +2 -2
  13. flock/components/__init__.py +41 -0
  14. flock/components/agent/__init__.py +22 -0
  15. flock/{components.py → components/agent/base.py} +4 -3
  16. flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
  17. flock/components/orchestrator/__init__.py +22 -0
  18. flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
  19. flock/components/orchestrator/circuit_breaker.py +95 -0
  20. flock/components/orchestrator/collection.py +143 -0
  21. flock/components/orchestrator/deduplication.py +78 -0
  22. flock/core/__init__.py +30 -0
  23. flock/core/agent.py +953 -0
  24. flock/{artifacts.py → core/artifacts.py} +1 -1
  25. flock/{context_provider.py → core/context_provider.py} +3 -3
  26. flock/core/orchestrator.py +1102 -0
  27. flock/{store.py → core/store.py} +99 -454
  28. flock/{subscription.py → core/subscription.py} +1 -1
  29. flock/dashboard/collector.py +5 -5
  30. flock/dashboard/graph_builder.py +7 -7
  31. flock/dashboard/routes/__init__.py +21 -0
  32. flock/dashboard/routes/control.py +327 -0
  33. flock/dashboard/routes/helpers.py +340 -0
  34. flock/dashboard/routes/themes.py +76 -0
  35. flock/dashboard/routes/traces.py +521 -0
  36. flock/dashboard/routes/websocket.py +108 -0
  37. flock/dashboard/service.py +43 -1316
  38. flock/engines/dspy/__init__.py +20 -0
  39. flock/engines/dspy/artifact_materializer.py +216 -0
  40. flock/engines/dspy/signature_builder.py +474 -0
  41. flock/engines/dspy/streaming_executor.py +858 -0
  42. flock/engines/dspy_engine.py +45 -1330
  43. flock/engines/examples/simple_batch_engine.py +2 -2
  44. flock/examples.py +7 -7
  45. flock/logging/logging.py +1 -16
  46. flock/models/__init__.py +10 -0
  47. flock/orchestrator/__init__.py +45 -0
  48. flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
  49. flock/orchestrator/artifact_manager.py +168 -0
  50. flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
  51. flock/orchestrator/component_runner.py +389 -0
  52. flock/orchestrator/context_builder.py +167 -0
  53. flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
  54. flock/orchestrator/event_emitter.py +167 -0
  55. flock/orchestrator/initialization.py +184 -0
  56. flock/orchestrator/lifecycle_manager.py +226 -0
  57. flock/orchestrator/mcp_manager.py +202 -0
  58. flock/orchestrator/scheduler.py +189 -0
  59. flock/orchestrator/server_manager.py +234 -0
  60. flock/orchestrator/tracing.py +147 -0
  61. flock/storage/__init__.py +10 -0
  62. flock/storage/artifact_aggregator.py +158 -0
  63. flock/storage/in_memory/__init__.py +6 -0
  64. flock/storage/in_memory/artifact_filter.py +114 -0
  65. flock/storage/in_memory/history_aggregator.py +115 -0
  66. flock/storage/sqlite/__init__.py +10 -0
  67. flock/storage/sqlite/agent_history_queries.py +154 -0
  68. flock/storage/sqlite/consumption_loader.py +100 -0
  69. flock/storage/sqlite/query_builder.py +112 -0
  70. flock/storage/sqlite/query_params_builder.py +91 -0
  71. flock/storage/sqlite/schema_manager.py +168 -0
  72. flock/storage/sqlite/summary_queries.py +194 -0
  73. flock/utils/__init__.py +14 -0
  74. flock/utils/async_utils.py +67 -0
  75. flock/{runtime.py → utils/runtime.py} +3 -3
  76. flock/utils/time_utils.py +53 -0
  77. flock/utils/type_resolution.py +38 -0
  78. flock/{utilities.py → utils/utilities.py} +2 -2
  79. flock/utils/validation.py +57 -0
  80. flock/utils/visibility.py +79 -0
  81. flock/utils/visibility_utils.py +134 -0
  82. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/METADATA +18 -4
  83. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/RECORD +89 -33
  84. flock/agent.py +0 -1578
  85. flock/orchestrator.py +0 -1983
  86. /flock/{visibility.py → core/visibility.py} +0 -0
  87. /flock/{system_artifacts.py → models/system_artifacts.py} +0 -0
  88. /flock/{helper → utils}/cli_helper.py +0 -0
  89. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/WHEEL +0 -0
  90. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/entry_points.txt +0 -0
  91. {flock_core-0.5.11.dist-info → flock_core-0.5.20.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,474 @@
1
+ """DSPy signature building from output specifications.
2
+
3
+ Phase 6: Extracted from dspy_engine.py to reduce file size and improve modularity.
4
+
5
+ This module handles all signature-related logic for DSPy program execution,
6
+ including semantic field naming, pluralization, batching, and fan-out support.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from flock.core.artifacts import Artifact
14
+ from flock.logging.logging import get_logger
15
+ from flock.registry import type_registry
16
+ from flock.utils.runtime import EvalInputs
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Mapping
21
+
22
+ from pydantic import BaseModel
23
+
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class DSPySignatureBuilder:
29
+ """Builds DSPy signatures from output group specifications.
30
+
31
+ Responsibilities:
32
+ - Convert Pydantic models to snake_case field names
33
+ - Pluralize field names for batching and fan-out
34
+ - Generate DSPy signatures with semantic naming
35
+ - Build execution payloads matching signatures
36
+ - Extract outputs from predictions
37
+ """
38
+
39
+ def _type_to_field_name(self, type_class: type) -> str:
40
+ """Convert Pydantic model class name to snake_case field name.
41
+
42
+ Examples:
43
+ Movie → "movie"
44
+ ResearchQuestion → "research_question"
45
+ APIResponse → "api_response"
46
+ UserAuthToken → "user_auth_token"
47
+
48
+ Args:
49
+ type_class: The Pydantic model class
50
+
51
+ Returns:
52
+ snake_case field name
53
+ """
54
+ import re
55
+
56
+ name = type_class.__name__
57
+ # Convert CamelCase to snake_case
58
+ snake_case = re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
59
+ return snake_case
60
+
61
+ def _pluralize(self, field_name: str) -> str:
62
+ """Convert singular field name to plural for lists.
63
+
64
+ Examples:
65
+ "idea" → "ideas"
66
+ "movie" → "movies"
67
+ "story" → "stories" (y → ies)
68
+ "analysis" → "analyses" (is → es)
69
+ "research_question" → "research_questions"
70
+
71
+ Args:
72
+ field_name: Singular field name in snake_case
73
+
74
+ Returns:
75
+ Pluralized field name
76
+ """
77
+ # Simple English pluralization rules
78
+ if (
79
+ field_name.endswith("y")
80
+ and len(field_name) > 1
81
+ and field_name[-2] not in "aeiou"
82
+ ):
83
+ # story → stories (consonant + y)
84
+ return field_name[:-1] + "ies"
85
+ if field_name.endswith(("s", "x", "z", "ch", "sh")):
86
+ # analysis → analyses, box → boxes
87
+ return field_name + "es"
88
+ # idea → ideas, movie → movies
89
+ return field_name + "s"
90
+
91
+ def _needs_multioutput_signature(self, output_group) -> bool:
92
+ """Determine if OutputGroup requires multi-output signature generation.
93
+
94
+ Args:
95
+ output_group: OutputGroup to analyze
96
+
97
+ Returns:
98
+ True if multi-output signature needed, False for single output
99
+ """
100
+ if (
101
+ not output_group
102
+ or not hasattr(output_group, "outputs")
103
+ or not output_group.outputs
104
+ ):
105
+ return False
106
+
107
+ # Multiple different types → multi-output
108
+ if len(output_group.outputs) > 1:
109
+ return True
110
+
111
+ # Fan-out (single type, count > 1) → multi-output
112
+ if output_group.outputs[0].count > 1:
113
+ return True
114
+
115
+ return False
116
+
117
+ def _prepare_signature_with_context(
118
+ self,
119
+ dspy_mod,
120
+ *,
121
+ description: str | None,
122
+ input_schema: type[BaseModel] | None,
123
+ output_schema: type[BaseModel] | None,
124
+ has_context: bool = False,
125
+ batched: bool = False,
126
+ ) -> Any:
127
+ """Prepare DSPy signature, optionally including context field."""
128
+ fields = {
129
+ "description": (str, dspy_mod.InputField()),
130
+ }
131
+
132
+ # Add context field if we have conversation history
133
+ if has_context:
134
+ fields["context"] = (
135
+ list,
136
+ dspy_mod.InputField(
137
+ desc="Previous conversation artifacts providing context for this request"
138
+ ),
139
+ )
140
+
141
+ if batched:
142
+ if input_schema is not None:
143
+ input_type = list[input_schema]
144
+ else:
145
+ input_type = list[dict[str, Any]]
146
+ else:
147
+ input_type = input_schema or dict
148
+
149
+ fields["input"] = (input_type, dspy_mod.InputField())
150
+ fields["output"] = (output_schema or dict, dspy_mod.OutputField())
151
+
152
+ signature = dspy_mod.Signature(fields)
153
+
154
+ instruction = (
155
+ description or "Produce a valid output that matches the 'output' schema."
156
+ )
157
+ if has_context:
158
+ instruction += (
159
+ " Consider the conversation context provided to inform your response."
160
+ )
161
+ if batched:
162
+ instruction += (
163
+ " The 'input' field will contain a list of items representing the batch; "
164
+ "process the entire collection coherently."
165
+ )
166
+
167
+ return signature.with_instructions(instruction)
168
+
169
+ def prepare_signature_for_output_group(
170
+ self,
171
+ dspy_mod,
172
+ *,
173
+ agent,
174
+ inputs: EvalInputs,
175
+ output_group,
176
+ has_context: bool = False,
177
+ batched: bool = False,
178
+ engine_instructions: str | None = None,
179
+ ) -> Any:
180
+ """Prepare DSPy signature dynamically based on OutputGroup with semantic field names.
181
+
182
+ This method generates signatures using semantic field naming:
183
+ - Type names → snake_case field names (Task → "task", ResearchQuestion → "research_question")
184
+ - Pluralization for fan-out (Idea → "ideas" for lists)
185
+ - Pluralization for batching (Task → "tasks" for list[Task])
186
+ - Multi-input support for joins (multiple input artifacts with semantic names)
187
+ - Collision handling (same input/output type → prefix with "input_" or "output_")
188
+
189
+ Examples:
190
+ Single output: .consumes(Task).publishes(Report)
191
+ → {"task": (Task, InputField()), "report": (Report, OutputField())}
192
+
193
+ Multiple inputs (joins): .consumes(Document, Guidelines).publishes(Report)
194
+ → {"document": (Document, InputField()), "guidelines": (Guidelines, InputField()),
195
+ "report": (Report, OutputField())}
196
+
197
+ Multiple outputs: .consumes(Task).publishes(Summary, Analysis)
198
+ → {"task": (Task, InputField()), "summary": (Summary, OutputField()),
199
+ "analysis": (Analysis, OutputField())}
200
+
201
+ Fan-out: .publishes(Idea, fan_out=5)
202
+ → {"topic": (Topic, InputField()), "ideas": (list[Idea], OutputField(...))}
203
+
204
+ Batching: evaluate_batch([task1, task2, task3])
205
+ → {"tasks": (list[Task], InputField()), "reports": (list[Report], OutputField())}
206
+
207
+ Args:
208
+ dspy_mod: DSPy module
209
+ agent: Agent instance
210
+ inputs: EvalInputs with input artifacts
211
+ output_group: OutputGroup defining what to generate
212
+ has_context: Whether conversation context should be included
213
+ batched: Whether this is a batch evaluation (pluralizes input fields)
214
+ engine_instructions: Optional override for engine instructions
215
+
216
+ Returns:
217
+ DSPy Signature with semantic field names
218
+ """
219
+ fields = {
220
+ "description": (str, dspy_mod.InputField()),
221
+ }
222
+
223
+ # Add context field if we have conversation history
224
+ if has_context:
225
+ fields["context"] = (
226
+ list,
227
+ dspy_mod.InputField(
228
+ desc="Previous conversation artifacts providing context for this request"
229
+ ),
230
+ )
231
+
232
+ # Track used field names for collision detection
233
+ used_field_names: set[str] = {"description", "context"}
234
+
235
+ # 1. Generate INPUT fields with semantic names
236
+ # Multi-input support: handle all input artifacts for joins
237
+ # Batching support: pluralize field names and use list[Type] when batched=True
238
+ if inputs.artifacts:
239
+ # Collect unique input types (avoid duplicates if multiple artifacts of same type)
240
+ input_types_seen: dict[type, list[Artifact]] = {}
241
+ for artifact in inputs.artifacts:
242
+ input_model = self._resolve_input_model(artifact)
243
+ if input_model is not None:
244
+ if input_model not in input_types_seen:
245
+ input_types_seen[input_model] = []
246
+ input_types_seen[input_model].append(artifact)
247
+
248
+ # Generate fields for each unique input type
249
+ for input_model, artifacts_of_type in input_types_seen.items():
250
+ field_name = self._type_to_field_name(input_model)
251
+
252
+ # Handle batching: pluralize field name and use list[Type]
253
+ if batched:
254
+ field_name = self._pluralize(field_name)
255
+ input_type = list[input_model]
256
+ desc = f"Batch of {input_model.__name__} instances to process"
257
+ fields[field_name] = (input_type, dspy_mod.InputField(desc=desc))
258
+ else:
259
+ # Single input: use singular field name
260
+ input_type = input_model
261
+ fields[field_name] = (input_type, dspy_mod.InputField())
262
+
263
+ used_field_names.add(field_name)
264
+
265
+ # Fallback: if we couldn't resolve any types, use generic "input"
266
+ if not input_types_seen:
267
+ fields["input"] = (dict, dspy_mod.InputField())
268
+ used_field_names.add("input")
269
+
270
+ # 2. Generate OUTPUT fields with semantic names
271
+ for output_decl in output_group.outputs:
272
+ output_schema = output_decl.spec.model
273
+ type_name = output_decl.spec.type_name
274
+
275
+ # Generate semantic field name
276
+ field_name = self._type_to_field_name(output_schema)
277
+
278
+ # Handle fan-out: pluralize field name and use list[Type]
279
+ if output_decl.count > 1:
280
+ field_name = self._pluralize(field_name)
281
+ output_type = list[output_schema]
282
+
283
+ # Create description with count hint
284
+ desc = f"Generate exactly {output_decl.count} {type_name} instances"
285
+ if output_decl.group_description:
286
+ desc = f"{desc}. {output_decl.group_description}"
287
+
288
+ fields[field_name] = (output_type, dspy_mod.OutputField(desc=desc))
289
+ else:
290
+ # Single output
291
+ output_type = output_schema
292
+
293
+ # Handle collision: if field name already used, prefix with "output_"
294
+ if field_name in used_field_names:
295
+ field_name = f"output_{field_name}"
296
+
297
+ desc = f"{type_name} output"
298
+ if output_decl.group_description:
299
+ desc = output_decl.group_description
300
+
301
+ fields[field_name] = (output_type, dspy_mod.OutputField(desc=desc))
302
+
303
+ used_field_names.add(field_name)
304
+
305
+ # 3. Create signature
306
+ signature = dspy_mod.Signature(fields)
307
+
308
+ # 4. Build instruction
309
+ description = engine_instructions or agent.description
310
+ instruction = (
311
+ description
312
+ or f"Process input and generate {len(output_group.outputs)} outputs."
313
+ )
314
+
315
+ if has_context:
316
+ instruction += (
317
+ " Consider the conversation context provided to inform your response."
318
+ )
319
+
320
+ # Add batching hint
321
+ if batched:
322
+ instruction += " Process the batch of inputs coherently, generating outputs for each item."
323
+
324
+ # Add semantic field names to instruction for clarity
325
+ output_field_names = [
326
+ name for name in fields.keys() if name not in {"description", "context"}
327
+ ]
328
+ if len(output_field_names) > 2: # Multiple outputs
329
+ instruction += f" Generate ALL output fields as specified: {', '.join(output_field_names[1:])}."
330
+
331
+ return signature.with_instructions(instruction)
332
+
333
+ def prepare_execution_payload_for_output_group(
334
+ self,
335
+ inputs: EvalInputs,
336
+ output_group,
337
+ *,
338
+ batched: bool,
339
+ has_context: bool,
340
+ context_history: list | None,
341
+ sys_desc: str,
342
+ ) -> dict[str, Any]:
343
+ """Prepare execution payload with semantic field names matching signature.
344
+
345
+ This method builds a payload dict with semantic field names that match the signature
346
+ generated by `prepare_signature_for_output_group()`.
347
+
348
+ Args:
349
+ inputs: EvalInputs with input artifacts
350
+ output_group: OutputGroup (not used here but kept for symmetry)
351
+ batched: Whether this is a batch evaluation
352
+ has_context: Whether conversation context should be included
353
+ context_history: Optional conversation history
354
+ sys_desc: System description for the "description" field
355
+
356
+ Returns:
357
+ Dict with semantic field names ready for DSPy program execution
358
+
359
+ Examples:
360
+ Single input: {"description": desc, "task": {...}}
361
+ Multi-input: {"description": desc, "task": {...}, "topic": {...}}
362
+ Batched: {"description": desc, "tasks": [{...}, {...}, {...}]}
363
+ """
364
+ payload = {"description": sys_desc}
365
+
366
+ # Add context if present
367
+ if has_context and context_history:
368
+ payload["context"] = context_history
369
+
370
+ # Build semantic input fields
371
+ if inputs.artifacts:
372
+ # Collect unique input types (same logic as signature generation)
373
+ input_types_seen: dict[type, list[Artifact]] = {}
374
+ for artifact in inputs.artifacts:
375
+ input_model = self._resolve_input_model(artifact)
376
+ if input_model is not None:
377
+ if input_model not in input_types_seen:
378
+ input_types_seen[input_model] = []
379
+ input_types_seen[input_model].append(artifact)
380
+
381
+ # Generate payload fields for each unique input type
382
+ for input_model, artifacts_of_type in input_types_seen.items():
383
+ field_name = self._type_to_field_name(input_model)
384
+
385
+ # Validate and prepare payloads
386
+ validated_payloads = [
387
+ self._validate_input_payload(input_model, art.payload)
388
+ for art in artifacts_of_type
389
+ ]
390
+
391
+ if batched:
392
+ # Batch mode: pluralize field name and use list
393
+ field_name = self._pluralize(field_name)
394
+ payload[field_name] = validated_payloads
395
+ else:
396
+ # Single mode: use first (or only) artifact
397
+ # For multi-input joins, we have one artifact per type
398
+ payload[field_name] = (
399
+ validated_payloads[0] if validated_payloads else {}
400
+ )
401
+
402
+ return payload
403
+
404
+ def extract_multi_output_payload(self, prediction, output_group) -> dict[str, Any]:
405
+ """Extract semantic fields from DSPy Prediction for multi-output scenarios.
406
+
407
+ Maps semantic field names (e.g., "movie", "ideas") back to type names (e.g., "Movie", "Idea")
408
+ for artifact materialization compatibility.
409
+
410
+ Args:
411
+ prediction: DSPy Prediction object with semantic field names
412
+ output_group: OutputGroup defining expected outputs
413
+
414
+ Returns:
415
+ Dict mapping type names to extracted values
416
+
417
+ Examples:
418
+ Prediction(movie={...}, summary={...})
419
+ → {"Movie": {...}, "Summary": {...}}
420
+
421
+ Prediction(ideas=[{...}, {...}, {...}])
422
+ → {"Idea": [{...}, {...}, {...}]}
423
+ """
424
+ payload = {}
425
+
426
+ for output_decl in output_group.outputs:
427
+ output_schema = output_decl.spec.model
428
+ type_name = output_decl.spec.type_name
429
+
430
+ # Generate the same semantic field name used in signature
431
+ field_name = self._type_to_field_name(output_schema)
432
+
433
+ # Handle fan-out: field name is pluralized
434
+ if output_decl.count > 1:
435
+ field_name = self._pluralize(field_name)
436
+
437
+ # Extract value from Prediction
438
+ if hasattr(prediction, field_name):
439
+ value = getattr(prediction, field_name)
440
+
441
+ # Store using type_name as key (for _select_output_payload compatibility)
442
+ payload[type_name] = value
443
+ else:
444
+ # Fallback: try with "output_" prefix (collision handling)
445
+ prefixed_name = f"output_{field_name}"
446
+ if hasattr(prediction, prefixed_name):
447
+ value = getattr(prediction, prefixed_name)
448
+ payload[type_name] = value
449
+
450
+ return payload
451
+
452
+ def _resolve_input_model(self, artifact: Artifact) -> type[BaseModel] | None:
453
+ """Resolve artifact type to Pydantic model."""
454
+ try:
455
+ return type_registry.resolve(artifact.type)
456
+ except KeyError:
457
+ return None
458
+
459
+ def _validate_input_payload(
460
+ self,
461
+ schema: type[BaseModel] | None,
462
+ payload: Mapping[str, Any] | None,
463
+ ) -> dict[str, Any]:
464
+ """Validate and normalize input payload against schema."""
465
+ data = dict(payload or {})
466
+ if schema is None:
467
+ return data
468
+ try:
469
+ return schema(**data).model_dump()
470
+ except Exception:
471
+ return data
472
+
473
+
474
+ __all__ = ["DSPySignatureBuilder"]