flock-core 0.4.0b18__py3-none-any.whl → 0.4.0b20__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/cli/utils.py +135 -0
- flock/core/flock.py +2 -3
- flock/core/flock_factory.py +2 -0
- flock/core/logging/logging.py +2 -0
- flock/core/mixin/dspy_integration.py +16 -7
- flock/core/serialization/serialization_utils.py +70 -1
- flock/core/util/cli_helper.py +4 -2
- flock/core/util/hydrator.py +305 -285
- flock/evaluators/declarative/declarative_evaluator.py +40 -6
- flock_core-0.4.0b20.dist-info/METADATA +292 -0
- {flock_core-0.4.0b18.dist-info → flock_core-0.4.0b20.dist-info}/RECORD +14 -13
- flock_core-0.4.0b18.dist-info/METADATA +0 -572
- {flock_core-0.4.0b18.dist-info → flock_core-0.4.0b20.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b18.dist-info → flock_core-0.4.0b20.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.0b18.dist-info → flock_core-0.4.0b20.dist-info}/licenses/LICENSE +0 -0
flock/core/util/hydrator.py
CHANGED
|
@@ -1,306 +1,326 @@
|
|
|
1
|
+
# src/flock/core/util/hydrator.py (Revised - Simpler)
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import json
|
|
3
|
-
from typing import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# -----------------------------------------------------------
|
|
9
|
-
class FlockAgent:
|
|
10
|
-
def __init__(self, name, input, output, model, description):
|
|
11
|
-
self.name = name
|
|
12
|
-
self.input = input
|
|
13
|
-
self.output = output
|
|
14
|
-
self.model = model
|
|
15
|
-
self.description = description
|
|
16
|
-
|
|
17
|
-
async def evaluate(self, data: dict) -> dict:
|
|
18
|
-
"""Pretend LLM call.
|
|
19
|
-
We'll parse self.output to see which keys we want,
|
|
20
|
-
then generate some placeholders for those keys.
|
|
21
|
-
"""
|
|
22
|
-
print(
|
|
23
|
-
f"[FlockAgent] Evaluate called for agent {self.name} with data: {data}"
|
|
24
|
-
)
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
TypeVar,
|
|
8
|
+
get_type_hints,
|
|
9
|
+
)
|
|
25
10
|
|
|
26
|
-
|
|
27
|
-
fields = []
|
|
28
|
-
for out_part in self.output.split(","):
|
|
29
|
-
out_part = out_part.strip()
|
|
30
|
-
# out_part might look like: "title: str | property of MyBlogPost"
|
|
31
|
-
if not out_part:
|
|
32
|
-
continue
|
|
33
|
-
field_name = out_part.split(":")[0].strip()
|
|
34
|
-
fields.append(field_name)
|
|
35
|
-
|
|
36
|
-
# We'll pretend the LLM returns either an integer for int fields or a string for others:
|
|
37
|
-
response = {}
|
|
38
|
-
for f in fields:
|
|
39
|
-
if " int" in self.output: # naive
|
|
40
|
-
response[f] = 42
|
|
41
|
-
else:
|
|
42
|
-
response[f] = f"Generated data for {f}"
|
|
43
|
-
return response
|
|
11
|
+
from pydantic import BaseModel
|
|
44
12
|
|
|
13
|
+
# Import necessary Flock components
|
|
14
|
+
from flock.core import Flock, FlockFactory
|
|
15
|
+
from flock.core.logging.logging import get_logger
|
|
45
16
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
# -----------------------------------------------------------
|
|
49
|
-
def flockclass(model: str):
|
|
50
|
-
def decorator(cls):
|
|
51
|
-
cls.__is_flockclass__ = True
|
|
52
|
-
cls.__flock_model__ = model
|
|
53
|
-
return cls
|
|
54
|
-
|
|
55
|
-
return decorator
|
|
17
|
+
# Import helper to format type hints back to strings
|
|
18
|
+
from flock.core.serialization.serialization_utils import _format_type_to_string
|
|
56
19
|
|
|
20
|
+
logger = get_logger("hydrator")
|
|
21
|
+
T = TypeVar("T", bound=BaseModel)
|
|
57
22
|
|
|
58
|
-
# -----------------------------------------------------------
|
|
59
|
-
# Utility sets
|
|
60
|
-
# -----------------------------------------------------------
|
|
61
|
-
BASIC_TYPES = {str, int, float, bool}
|
|
62
23
|
|
|
24
|
+
def flockclass(
|
|
25
|
+
model: str = "openai/gpt-4o", agent_description: str | None = None
|
|
26
|
+
):
|
|
27
|
+
"""Decorator to add a .hydrate() method to a Pydantic class.
|
|
28
|
+
Leverages a dynamic Flock agent to fill missing (None) fields.
|
|
63
29
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# - user-defined classes (auto-fill missing fields + recurse)
|
|
68
|
-
# - lists (ask LLM how many items to create + fill them)
|
|
69
|
-
# - dicts (ask LLM how many key->value pairs to create + fill them)
|
|
70
|
-
# -----------------------------------------------------------
|
|
71
|
-
def hydrate_object(obj, model="gpt-4", class_name=None):
|
|
72
|
-
"""Recursively hydrates the object in-place,
|
|
73
|
-
calling an LLM for missing fields or structure.
|
|
30
|
+
Args:
|
|
31
|
+
model: The default LLM model identifier to use for hydration.
|
|
32
|
+
agent_description: An optional description for the dynamically created agent.
|
|
74
33
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if isinstance(obj, list):
|
|
81
|
-
if len(obj) == 0:
|
|
82
|
-
# We'll do a single LLM call to decide how many items to put in:
|
|
83
|
-
# In real usage, you'd put a more robust prompt.
|
|
84
|
-
list_agent = FlockAgent(
|
|
85
|
-
name=f"{class_name or 'list'}Generator",
|
|
86
|
-
input="Generate number of items for this list",
|
|
87
|
-
output="count: int | number of items to create",
|
|
88
|
-
model=model,
|
|
89
|
-
description="Agent that decides how many items to create in a list.",
|
|
34
|
+
|
|
35
|
+
def decorator(cls: type[T]) -> type[T]:
|
|
36
|
+
if not issubclass(cls, BaseModel):
|
|
37
|
+
raise TypeError(
|
|
38
|
+
"@flockclass can only decorate Pydantic BaseModel subclasses."
|
|
90
39
|
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
40
|
+
|
|
41
|
+
# Store metadata on the class
|
|
42
|
+
setattr(cls, "__flock_model__", model)
|
|
43
|
+
setattr(cls, "__flock_agent_description__", agent_description)
|
|
44
|
+
|
|
45
|
+
# --- Attach the async hydrate method directly ---
|
|
46
|
+
async def hydrate_async(self) -> T:
|
|
47
|
+
"""Hydrates the object by filling None fields using a dynamic Flock agent.
|
|
48
|
+
Uses existing non-None fields as input context.
|
|
49
|
+
Returns the hydrated object (self).
|
|
50
|
+
"""
|
|
51
|
+
class_name = self.__class__.__name__
|
|
52
|
+
logger.info(f"Starting hydration for instance of {class_name}")
|
|
53
|
+
|
|
54
|
+
# Get field information
|
|
55
|
+
all_fields, type_hints = _get_model_fields(self, class_name)
|
|
56
|
+
if all_fields is None or type_hints is None:
|
|
57
|
+
return self # Return early if field introspection failed
|
|
58
|
+
|
|
59
|
+
# Identify existing and missing fields
|
|
60
|
+
existing_data, missing_fields = _identify_fields(self, all_fields)
|
|
61
|
+
|
|
62
|
+
if not missing_fields:
|
|
63
|
+
logger.info(f"No fields to hydrate for {class_name} instance.")
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
logger.debug(f"{class_name}: Fields to hydrate: {missing_fields}")
|
|
67
|
+
logger.debug(
|
|
68
|
+
f"{class_name}: Existing data for context: {json.dumps(existing_data, default=str)}"
|
|
108
69
|
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
input="Generate keys for this dict",
|
|
118
|
-
output="keys: str | comma-separated list of keys to create",
|
|
119
|
-
model=model,
|
|
120
|
-
description="Agent that decides which keys to create in a dict.",
|
|
70
|
+
|
|
71
|
+
# Create agent signatures
|
|
72
|
+
input_str, output_str, input_parts = _build_agent_signatures(
|
|
73
|
+
existing_data,
|
|
74
|
+
missing_fields,
|
|
75
|
+
type_hints,
|
|
76
|
+
all_fields,
|
|
77
|
+
class_name,
|
|
121
78
|
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# Now recursively fill each value
|
|
132
|
-
for key, val in obj.items():
|
|
133
|
-
hydrate_object(
|
|
134
|
-
val,
|
|
135
|
-
model=model,
|
|
136
|
-
class_name=f"{class_name or 'dict'}[key={key}]",
|
|
79
|
+
|
|
80
|
+
# Create and run agent
|
|
81
|
+
result = await _run_hydration_agent(
|
|
82
|
+
self,
|
|
83
|
+
input_str,
|
|
84
|
+
output_str,
|
|
85
|
+
input_parts,
|
|
86
|
+
existing_data,
|
|
87
|
+
class_name,
|
|
137
88
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
used_model = getattr(cls, "__flock_model__", model)
|
|
145
|
-
|
|
146
|
-
# Figure out which fields are missing or None
|
|
147
|
-
type_hints = get_type_hints(cls)
|
|
148
|
-
missing_basic_fields = []
|
|
149
|
-
complex_fields = []
|
|
150
|
-
for field_name, field_type in type_hints.items():
|
|
151
|
-
value = getattr(obj, field_name, None)
|
|
152
|
-
if value is None:
|
|
153
|
-
# It's missing. See if it's a basic type or complex
|
|
154
|
-
if _is_basic_type(field_type):
|
|
155
|
-
missing_basic_fields.append(field_name)
|
|
156
|
-
else:
|
|
157
|
-
complex_fields.append(field_name)
|
|
158
|
-
else:
|
|
159
|
-
# Already has some value, but if it's a complex type, we should recurse
|
|
160
|
-
if not _is_basic_type(field_type):
|
|
161
|
-
complex_fields.append(field_name)
|
|
162
|
-
|
|
163
|
-
# If we have missing basic fields, do a single LLM call to fill them
|
|
164
|
-
if missing_basic_fields:
|
|
165
|
-
input_str = (
|
|
166
|
-
f"Existing data: {json.dumps(obj.__dict__, default=str)}"
|
|
89
|
+
if result is None:
|
|
90
|
+
return self # Return early if agent run failed
|
|
91
|
+
|
|
92
|
+
# Update object fields with results
|
|
93
|
+
_update_fields_with_results(
|
|
94
|
+
self, result, missing_fields, class_name
|
|
167
95
|
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
96
|
+
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
# --- Attach the sync hydrate method directly ---
|
|
100
|
+
def hydrate(self) -> T:
|
|
101
|
+
"""Synchronous wrapper for the async hydrate method."""
|
|
102
|
+
try:
|
|
103
|
+
# Try to get the current running loop
|
|
104
|
+
loop = asyncio.get_running_loop()
|
|
105
|
+
|
|
106
|
+
# If we reach here, there is a running loop
|
|
107
|
+
if loop.is_running():
|
|
108
|
+
# This runs the coroutine in the existing loop from a different thread
|
|
109
|
+
import concurrent.futures
|
|
110
|
+
|
|
111
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
112
|
+
future = executor.submit(
|
|
113
|
+
asyncio.run, hydrate_async(self)
|
|
114
|
+
)
|
|
115
|
+
return future.result()
|
|
116
|
+
else:
|
|
117
|
+
# There's a loop but it's not running
|
|
118
|
+
return loop.run_until_complete(hydrate_async(self))
|
|
119
|
+
|
|
120
|
+
except RuntimeError: # No running loop
|
|
121
|
+
# If no loop is running, create a new one and run our coroutine
|
|
122
|
+
return asyncio.run(hydrate_async(self))
|
|
123
|
+
|
|
124
|
+
# Attach the methods to the class
|
|
125
|
+
setattr(cls, "hydrate_async", hydrate_async)
|
|
126
|
+
setattr(cls, "hydrate", hydrate)
|
|
127
|
+
setattr(
|
|
128
|
+
cls, "hydrate_sync", hydrate
|
|
129
|
+
) # Alias for backward compatibility
|
|
130
|
+
|
|
131
|
+
logger.debug(f"Attached hydrate methods to class {cls.__name__}")
|
|
132
|
+
return cls
|
|
133
|
+
|
|
134
|
+
return decorator
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _get_model_fields(
|
|
138
|
+
obj: BaseModel, class_name: str
|
|
139
|
+
) -> tuple[dict | None, dict | None]:
|
|
140
|
+
"""Extracts field information from a Pydantic model, handling v1/v2 compatibility."""
|
|
141
|
+
try:
|
|
142
|
+
if hasattr(obj, "model_fields"): # Pydantic v2
|
|
143
|
+
all_fields = obj.model_fields
|
|
144
|
+
type_hints = {
|
|
145
|
+
name: field.annotation for name, field in all_fields.items()
|
|
146
|
+
}
|
|
147
|
+
else: # Pydantic v1 fallback
|
|
148
|
+
type_hints = get_type_hints(obj.__class__)
|
|
149
|
+
all_fields = getattr(
|
|
150
|
+
obj, "__fields__", {name: None for name in type_hints}
|
|
185
151
|
)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
152
|
+
return all_fields, type_hints
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(
|
|
155
|
+
f"Could not get fields/type hints for {class_name}: {e}",
|
|
156
|
+
exc_info=True,
|
|
157
|
+
)
|
|
158
|
+
return None, None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _identify_fields(
|
|
162
|
+
obj: BaseModel, all_fields: dict
|
|
163
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
164
|
+
"""Identifies existing (non-None) and missing fields in the object."""
|
|
165
|
+
existing_data: dict[str, Any] = {}
|
|
166
|
+
missing_fields: list[str] = []
|
|
167
|
+
|
|
168
|
+
for field_name in all_fields:
|
|
169
|
+
if hasattr(obj, field_name): # Check if attribute exists
|
|
170
|
+
value = getattr(obj, field_name)
|
|
171
|
+
if value is not None:
|
|
172
|
+
existing_data[field_name] = value
|
|
203
173
|
else:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
174
|
+
missing_fields.append(field_name)
|
|
175
|
+
|
|
176
|
+
return existing_data, missing_fields
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _build_agent_signatures(
|
|
180
|
+
existing_data: dict[str, Any],
|
|
181
|
+
missing_fields: list[str],
|
|
182
|
+
type_hints: dict,
|
|
183
|
+
all_fields: dict,
|
|
184
|
+
class_name: str,
|
|
185
|
+
) -> tuple[str, str, list]:
|
|
186
|
+
"""Builds input and output signatures for the dynamic agent."""
|
|
187
|
+
# Input signature based on existing data
|
|
188
|
+
input_parts = []
|
|
189
|
+
for name in existing_data:
|
|
190
|
+
field_type = type_hints.get(name, Any)
|
|
191
|
+
type_str = _format_type_to_string(field_type)
|
|
192
|
+
field_info = all_fields.get(name)
|
|
193
|
+
field_desc = getattr(field_info, "description", "")
|
|
194
|
+
if field_desc:
|
|
195
|
+
input_parts.append(f"{name}: {type_str} | {field_desc}")
|
|
196
|
+
else:
|
|
197
|
+
input_parts.append(f"{name}: {type_str}")
|
|
198
|
+
|
|
199
|
+
input_str = (
|
|
200
|
+
", ".join(input_parts)
|
|
201
|
+
if input_parts
|
|
202
|
+
else "context_info: dict | Optional context if no fields have values"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Output signature based on missing fields
|
|
206
|
+
output_parts = []
|
|
207
|
+
for name in missing_fields:
|
|
208
|
+
field_type = type_hints.get(name, Any)
|
|
209
|
+
type_str = _format_type_to_string(field_type)
|
|
210
|
+
field_info = all_fields.get(name)
|
|
211
|
+
field_desc = getattr(field_info, "description", "")
|
|
212
|
+
if field_desc:
|
|
213
|
+
output_parts.append(f"{name}: {type_str} | {field_desc}")
|
|
214
|
+
else:
|
|
215
|
+
output_parts.append(f"{name}: {type_str}")
|
|
216
|
+
|
|
217
|
+
output_str = ", ".join(output_parts)
|
|
218
|
+
|
|
219
|
+
return input_str, output_str, input_parts
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def _run_hydration_agent(
|
|
223
|
+
obj: BaseModel,
|
|
224
|
+
input_str: str,
|
|
225
|
+
output_str: str,
|
|
226
|
+
input_parts: list,
|
|
227
|
+
existing_data: dict[str, Any],
|
|
228
|
+
class_name: str,
|
|
229
|
+
) -> dict[str, Any] | None:
|
|
230
|
+
"""Creates and runs a dynamic Flock agent to hydrate the object."""
|
|
231
|
+
# Agent configuration
|
|
232
|
+
agent_name = f"hydrator_{class_name}_{id(obj)}"
|
|
233
|
+
description = (
|
|
234
|
+
getattr(obj, "__flock_agent_description__", None)
|
|
235
|
+
or f"Agent that completes missing data for a {class_name} object."
|
|
236
|
+
)
|
|
237
|
+
hydration_model = getattr(obj, "__flock_model__", "openai/gpt-4o")
|
|
238
|
+
|
|
239
|
+
logger.debug(f"Creating dynamic agent '{agent_name}' for {class_name}")
|
|
240
|
+
logger.debug(f" Input Schema: {input_str}")
|
|
241
|
+
logger.debug(f" Output Schema: {output_str}")
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
# Create agent
|
|
245
|
+
dynamic_agent = FlockFactory.create_default_agent(
|
|
246
|
+
name=agent_name,
|
|
247
|
+
description=description,
|
|
248
|
+
input=input_str,
|
|
249
|
+
output=output_str,
|
|
250
|
+
model=hydration_model,
|
|
251
|
+
no_output=True,
|
|
252
|
+
use_cache=False,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Create temporary Flock
|
|
256
|
+
temp_flock = Flock(
|
|
257
|
+
name=f"temp_hydrator_flock_{agent_name}",
|
|
258
|
+
model=hydration_model,
|
|
259
|
+
enable_logging=False,
|
|
260
|
+
show_flock_banner=False,
|
|
261
|
+
)
|
|
262
|
+
temp_flock.add_agent(dynamic_agent)
|
|
263
|
+
|
|
264
|
+
# Prepare input data
|
|
265
|
+
agent_input_data = (
|
|
266
|
+
existing_data
|
|
267
|
+
if input_parts
|
|
268
|
+
else {"context_info": {"object_type": class_name}}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
logger.info(
|
|
272
|
+
f"Running hydration agent '{agent_name}' for {class_name}..."
|
|
273
|
+
)
|
|
208
274
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
# Helper: instantiate a type (list, dict, or user-defined)
|
|
227
|
-
# -----------------------------------------------------------
|
|
228
|
-
def _instantiate_type(t):
|
|
229
|
-
origin = get_origin(t)
|
|
230
|
-
if origin is list:
|
|
231
|
-
return []
|
|
232
|
-
if origin is dict:
|
|
233
|
-
return {}
|
|
234
|
-
|
|
235
|
-
# If it's a built-in basic type, return None (we fill it from LLM).
|
|
236
|
-
if t in BASIC_TYPES:
|
|
275
|
+
# Run agent
|
|
276
|
+
result = await temp_flock.run_async(
|
|
277
|
+
start_agent=agent_name,
|
|
278
|
+
input=agent_input_data,
|
|
279
|
+
box_result=False,
|
|
280
|
+
)
|
|
281
|
+
logger.info(
|
|
282
|
+
f"Hydration agent returned for {class_name}: {list(result.keys())}"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return result
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.error(
|
|
289
|
+
f"Hydration agent creation or run failed for {class_name}: {e}",
|
|
290
|
+
exc_info=True,
|
|
291
|
+
)
|
|
237
292
|
return None
|
|
238
293
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
294
|
+
|
|
295
|
+
def _update_fields_with_results(
|
|
296
|
+
obj: BaseModel,
|
|
297
|
+
result: dict[str, Any],
|
|
298
|
+
missing_fields: list[str],
|
|
299
|
+
class_name: str,
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Updates object fields with results from the hydration agent."""
|
|
302
|
+
updated_count = 0
|
|
303
|
+
for field_name in missing_fields:
|
|
304
|
+
if field_name in result:
|
|
246
305
|
try:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
@flockclass("gpt-4")
|
|
271
|
-
class MyProjectPlan:
|
|
272
|
-
project_idea: str
|
|
273
|
-
budget: int
|
|
274
|
-
title: str
|
|
275
|
-
content: MyBlogPost
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
# -----------------------------------------------------------
|
|
279
|
-
# Demo
|
|
280
|
-
# -----------------------------------------------------------
|
|
281
|
-
if __name__ == "__main__":
|
|
282
|
-
plan = MyProjectPlan()
|
|
283
|
-
plan.project_idea = "a declarative agent framework"
|
|
284
|
-
plan.budget = 100000
|
|
285
|
-
|
|
286
|
-
# content is None by default, so the hydrator will create MyBlogPost
|
|
287
|
-
# and fill it in. MyBlogPost.content is a dict[str, LongContent],
|
|
288
|
-
# also None -> becomes an empty dict -> we let the LLM decide the keys.
|
|
289
|
-
|
|
290
|
-
hydrate_object(plan, model="gpt-4", class_name="MyProjectPlan")
|
|
291
|
-
|
|
292
|
-
print("\n--- MyProjectPlan hydrated ---")
|
|
293
|
-
for k, v in plan.__dict__.items():
|
|
294
|
-
print(f"{k} = {v}")
|
|
295
|
-
if plan.content:
|
|
296
|
-
print("\n--- MyBlogPost hydrated ---")
|
|
297
|
-
for k, v in plan.content.__dict__.items():
|
|
298
|
-
print(f" {k} = {v}")
|
|
299
|
-
if k == "content" and isinstance(v, dict):
|
|
300
|
-
print(" (keys) =", list(v.keys()))
|
|
301
|
-
for sub_k, sub_val in v.items():
|
|
302
|
-
print(f" {sub_k} -> {sub_val}")
|
|
303
|
-
if isinstance(sub_val, LongContent):
|
|
304
|
-
print(
|
|
305
|
-
f" -> LongContent fields: {sub_val.__dict__}"
|
|
306
|
-
)
|
|
306
|
+
setattr(obj, field_name, result[field_name])
|
|
307
|
+
logger.debug(
|
|
308
|
+
f"Hydrated field '{field_name}' in {class_name} with value: {getattr(obj, field_name)}"
|
|
309
|
+
)
|
|
310
|
+
updated_count += 1
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.warning(
|
|
313
|
+
f"Failed to set hydrated value for '{field_name}' in {class_name}: {e}. Value received: {result[field_name]}"
|
|
314
|
+
)
|
|
315
|
+
else:
|
|
316
|
+
logger.warning(
|
|
317
|
+
f"Hydration result missing expected field for {class_name}: '{field_name}'"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
logger.info(
|
|
321
|
+
f"Hydration complete for {class_name}. Updated {updated_count}/{len(missing_fields)} fields."
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# Ensure helper functions are available
|
|
326
|
+
# from flock.core.serialization.serialization_utils import _format_type_to_string
|