flock-core 0.5.11__py3-none-any.whl → 0.5.21__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/__init__.py +1 -1
- flock/agent/__init__.py +30 -0
- flock/agent/builder_helpers.py +192 -0
- flock/agent/builder_validator.py +169 -0
- flock/agent/component_lifecycle.py +325 -0
- flock/agent/context_resolver.py +141 -0
- flock/agent/mcp_integration.py +212 -0
- flock/agent/output_processor.py +304 -0
- flock/api/__init__.py +20 -0
- flock/{api_models.py → api/models.py} +0 -2
- flock/{service.py → api/service.py} +3 -3
- flock/cli.py +2 -2
- flock/components/__init__.py +41 -0
- flock/components/agent/__init__.py +22 -0
- flock/{components.py → components/agent/base.py} +4 -3
- flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
- flock/components/orchestrator/__init__.py +22 -0
- flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
- flock/components/orchestrator/circuit_breaker.py +95 -0
- flock/components/orchestrator/collection.py +143 -0
- flock/components/orchestrator/deduplication.py +78 -0
- flock/core/__init__.py +30 -0
- flock/core/agent.py +953 -0
- flock/{artifacts.py → core/artifacts.py} +1 -1
- flock/{context_provider.py → core/context_provider.py} +3 -3
- flock/core/orchestrator.py +1102 -0
- flock/{store.py → core/store.py} +99 -454
- flock/{subscription.py → core/subscription.py} +1 -1
- flock/dashboard/collector.py +5 -5
- flock/dashboard/events.py +1 -1
- flock/dashboard/graph_builder.py +7 -7
- flock/dashboard/routes/__init__.py +21 -0
- flock/dashboard/routes/control.py +327 -0
- flock/dashboard/routes/helpers.py +340 -0
- flock/dashboard/routes/themes.py +76 -0
- flock/dashboard/routes/traces.py +521 -0
- flock/dashboard/routes/websocket.py +108 -0
- flock/dashboard/service.py +43 -1316
- flock/engines/dspy/__init__.py +20 -0
- flock/engines/dspy/artifact_materializer.py +216 -0
- flock/engines/dspy/signature_builder.py +474 -0
- flock/engines/dspy/streaming_executor.py +812 -0
- flock/engines/dspy_engine.py +45 -1330
- flock/engines/examples/simple_batch_engine.py +2 -2
- flock/engines/streaming/__init__.py +3 -0
- flock/engines/streaming/sinks.py +489 -0
- flock/examples.py +7 -7
- flock/logging/logging.py +1 -16
- flock/models/__init__.py +10 -0
- flock/orchestrator/__init__.py +45 -0
- flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
- flock/orchestrator/artifact_manager.py +168 -0
- flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
- flock/orchestrator/component_runner.py +389 -0
- flock/orchestrator/context_builder.py +167 -0
- flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
- flock/orchestrator/event_emitter.py +167 -0
- flock/orchestrator/initialization.py +184 -0
- flock/orchestrator/lifecycle_manager.py +226 -0
- flock/orchestrator/mcp_manager.py +202 -0
- flock/orchestrator/scheduler.py +189 -0
- flock/orchestrator/server_manager.py +234 -0
- flock/orchestrator/tracing.py +147 -0
- flock/storage/__init__.py +10 -0
- flock/storage/artifact_aggregator.py +158 -0
- flock/storage/in_memory/__init__.py +6 -0
- flock/storage/in_memory/artifact_filter.py +114 -0
- flock/storage/in_memory/history_aggregator.py +115 -0
- flock/storage/sqlite/__init__.py +10 -0
- flock/storage/sqlite/agent_history_queries.py +154 -0
- flock/storage/sqlite/consumption_loader.py +100 -0
- flock/storage/sqlite/query_builder.py +112 -0
- flock/storage/sqlite/query_params_builder.py +91 -0
- flock/storage/sqlite/schema_manager.py +168 -0
- flock/storage/sqlite/summary_queries.py +194 -0
- flock/utils/__init__.py +14 -0
- flock/utils/async_utils.py +67 -0
- flock/{runtime.py → utils/runtime.py} +3 -3
- flock/utils/time_utils.py +53 -0
- flock/utils/type_resolution.py +38 -0
- flock/{utilities.py → utils/utilities.py} +2 -2
- flock/utils/validation.py +57 -0
- flock/utils/visibility.py +79 -0
- flock/utils/visibility_utils.py +134 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/METADATA +19 -5
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/RECORD +92 -34
- flock/agent.py +0 -1578
- flock/orchestrator.py +0 -1983
- /flock/{visibility.py → core/visibility.py} +0 -0
- /flock/{system_artifacts.py → models/system_artifacts.py} +0 -0
- /flock/{helper → utils}/cli_helper.py +0 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/WHEEL +0 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.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"]
|