flock-core 0.4.0b17__py3-none-any.whl → 0.4.0b19__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.

@@ -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 get_origin, get_type_hints
4
-
5
-
6
- # -----------------------------------------------------------
7
- # Dummy FlockAgent for demonstration:
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
- # Very naive parse of output string: "title: str | desc, budget: int | desc, ..."
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
- # Optional: a decorator that marks a class as "flockclass"
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
- # The main hydrator that can handle:
66
- # - basic types (do nothing)
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
- # 1) If None or basic, do nothing
76
- if obj is None or isinstance(obj, (str, int, float, bool)):
77
- return
78
-
79
- # 2) If list, check if it is empty => ask the LLM how many items we need
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
- result = asyncio.run(list_agent.evaluate({}))
92
- num_items = result.get("count", 0)
93
- # We'll assume the list should hold some type T.
94
- # But in Python, we rarely store that info in the runtime.
95
- # For demonstration, let's just store dummy strings or we can guess "object".
96
- for i in range(num_items):
97
- # For demonstration, create a simple string or dict
98
- # If you want a typed approach, you'll need additional metadata or pass in generics
99
- item = f"Generated item {i + 1}"
100
- obj.append(item)
101
-
102
- # Now recursively fill each item
103
- for i in range(len(obj)):
104
- hydrate_object(
105
- obj[i],
106
- model=model,
107
- class_name=f"{class_name or 'list'}[item={i}]",
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
- return
110
-
111
- # 3) If dict, check if it is empty => ask LLM for which keys to create
112
- if isinstance(obj, dict):
113
- if len(obj) == 0:
114
- # We'll do a single LLM call that returns a list of keys
115
- dict_agent = FlockAgent(
116
- name=f"{class_name or 'dict'}Generator",
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
- result = asyncio.run(dict_agent.evaluate({}))
123
- keys_str = result.get("keys", "")
124
- keys = [k.strip() for k in keys_str.split(",") if k.strip()]
125
-
126
- # For demonstration, let's assume the dict holds sub-objects that we can fill further
127
- # We'll create a plain dict or plain string for each key
128
- for k in keys:
129
- obj[k] = f"Placeholder for {k}"
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
- return
139
-
140
- # 4) If it's a user-defined class with annotations, fill missing fields
141
- cls = type(obj)
142
- if hasattr(cls, "__annotations__"):
143
- # If there's a model stored on the class, we can use that. Else fallback to the default
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
- output_fields_str = []
169
- for bf in missing_basic_fields:
170
- bf_type = type_hints[bf]
171
- bf_type_name = (
172
- bf_type.__name__
173
- if hasattr(bf_type, "__name__")
174
- else str(bf_type)
175
- )
176
- desc = f"property of a class named {cls.__name__}"
177
- output_fields_str.append(f"{bf}: {bf_type_name} | {desc}")
178
-
179
- agent = FlockAgent(
180
- name=cls.__name__,
181
- input=input_str,
182
- output=", ".join(output_fields_str),
183
- model=used_model,
184
- description=f"Agent for {cls.__name__}",
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
- result = asyncio.run(agent.evaluate(obj.__dict__))
187
- for bf in missing_basic_fields:
188
- if bf in result:
189
- setattr(obj, bf, result[bf])
190
-
191
- # For each "complex" field, instantiate if None + recurse
192
- for cf in complex_fields:
193
- cf_value = getattr(obj, cf, None)
194
- cf_type = type_hints[cf]
195
-
196
- if cf_value is None:
197
- # We need to create something of the appropriate type
198
- new_val = _instantiate_type(cf_type)
199
- setattr(obj, cf, new_val)
200
- hydrate_object(
201
- new_val, model=used_model, class_name=cf_type.__name__
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
- # Recurse into it
205
- hydrate_object(
206
- cf_value, model=used_model, class_name=cf_type.__name__
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
- else:
210
- # It's some Python object with no annotations -> do nothing
211
- pass
212
-
213
-
214
- # -----------------------------------------------------------
215
- # Helper: is a type "basic"?
216
- # -----------------------------------------------------------
217
- def _is_basic_type(t):
218
- if t in BASIC_TYPES:
219
- return True
220
- # You may want to check for Optionals or Unions
221
- # e.g., if get_origin(t) == Union, parse that, etc.
222
- return False
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
- # If it's a user-defined class
240
- if isinstance(t, type):
241
- try:
242
- # Attempt parameterless init
243
- return t()
244
- except:
245
- # Or try __new__
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
- return t.__new__(t)
248
- except:
249
- return None
250
- return None
251
-
252
-
253
- # -----------------------------------------------------------
254
- # Example classes
255
- # -----------------------------------------------------------
256
- @flockclass("gpt-4")
257
- class LongContent:
258
- title: str
259
- content: str
260
-
261
-
262
- @flockclass("gpt-4")
263
- class MyBlogPost:
264
- title: str
265
- headers: str
266
- # We'll have a dict of key->LongContent
267
- content: dict[str, LongContent]
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