lionagi 0.18.1__py3-none-any.whl → 0.18.2__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.
@@ -16,7 +16,7 @@ from typing import Annotated, Any, ClassVar
16
16
  from typing_extensions import Self, override
17
17
 
18
18
  from .._errors import ValidationError
19
- from ..ln.types import Meta, Params
19
+ from ..ln.types import Meta, ModelConfig, Params, Spec
20
20
 
21
21
  # Cache of valid Pydantic Field parameters
22
22
  _PYDANTIC_FIELD_PARAMS: set[str] | None = None
@@ -77,8 +77,9 @@ class FieldModel(Params):
77
77
  """
78
78
 
79
79
  # Class configuration - let Params handle Unset population
80
- _prefill_unset: ClassVar[bool] = True
81
- _none_as_sentinel: ClassVar[bool] = True
80
+ _config: ClassVar[ModelConfig] = ModelConfig(
81
+ prefill_unset=True, none_as_sentinel=True
82
+ )
82
83
 
83
84
  # Public fields (all start as Unset when not provided)
84
85
  base_type: type[Any]
@@ -782,6 +783,59 @@ class FieldModel(Params):
782
783
  ]
783
784
  )
784
785
 
786
+ def to_spec(self) -> "Spec":
787
+ """Convert FieldModel to Spec.
788
+
789
+ Returns:
790
+ Spec object with equivalent configuration
791
+ """
792
+ from ..ln.types import Spec
793
+
794
+ # Build kwargs for Spec constructor
795
+ kwargs = {}
796
+
797
+ # Extract name from metadata
798
+ name = self.extract_metadata("name")
799
+ if name:
800
+ kwargs["name"] = name
801
+
802
+ # Add nullable/listable flags using properties
803
+ kwargs["nullable"] = self.is_nullable
804
+ kwargs["listable"] = self.is_listable
805
+
806
+ # Extract default/default_factory
807
+ default = self.extract_metadata("default")
808
+ if default is not None:
809
+ kwargs["default"] = default
810
+
811
+ default_factory = self.extract_metadata("default_factory")
812
+ if default_factory is not None:
813
+ kwargs["default_factory"] = default_factory
814
+
815
+ # Extract validator
816
+ validator = self.extract_metadata("validator")
817
+ if validator is not None:
818
+ kwargs["validator"] = validator
819
+
820
+ # Extract description
821
+ description = self.extract_metadata("description")
822
+ if description:
823
+ kwargs["description"] = description
824
+
825
+ # Extract other common metadata
826
+ for key in ["title", "alias", "frozen", "exclude"]:
827
+ val = self.extract_metadata(key)
828
+ if val is not None:
829
+ kwargs[key] = val
830
+
831
+ # Extract json_schema_extra
832
+ json_schema_extra = self.extract_metadata("json_schema_extra")
833
+ if json_schema_extra:
834
+ for k, v in json_schema_extra.items():
835
+ kwargs[k] = v
836
+
837
+ return Spec(self.base_type, **kwargs)
838
+
785
839
  def metadata_dict(
786
840
  self, exclude: list[str] | None = None
787
841
  ) -> dict[str, Any]:
@@ -21,7 +21,7 @@ from typing import Any, ClassVar
21
21
  from pydantic import BaseModel, create_model
22
22
  from pydantic.fields import FieldInfo
23
23
 
24
- from lionagi.ln.types import Params
24
+ from lionagi.ln.types import ModelConfig, Params
25
25
  from lionagi.utils import copy
26
26
 
27
27
  from .field_model import FieldModel
@@ -83,8 +83,9 @@ class ModelParams(Params):
83
83
  """
84
84
 
85
85
  # Class configuration - let Params handle Unset population
86
- _prefill_unset: ClassVar[bool] = True
87
- _none_as_sentinel: ClassVar[bool] = True
86
+ _config: ClassVar[ModelConfig] = ModelConfig(
87
+ prefill_unset=True, none_as_sentinel=True
88
+ )
88
89
 
89
90
  # Public fields (all start as Unset when not provided)
90
91
  name: str | None
@@ -2,13 +2,14 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import warnings
5
- from typing import TYPE_CHECKING, Literal
5
+ from typing import TYPE_CHECKING, Literal, Union
6
6
 
7
7
  from pydantic import BaseModel, JsonValue
8
8
 
9
9
  from lionagi.ln import AlcallParams
10
10
  from lionagi.ln.fuzzy import FuzzyMatchKeysParams
11
- from lionagi.models import FieldModel, ModelParams
11
+ from lionagi.ln.types import Spec
12
+ from lionagi.models import FieldModel
12
13
  from lionagi.protocols.generic import Progression
13
14
  from lionagi.protocols.messages import Instruction, SenderRecipient
14
15
 
@@ -32,7 +33,7 @@ def prepare_operate_kw(
32
33
  sender: SenderRecipient = None,
33
34
  recipient: SenderRecipient = None,
34
35
  progression: Progression = None,
35
- imodel: "iModel" = None, # deprecated, alias of chat_model
36
+ imodel: "iModel" = None, # deprecated
36
37
  chat_model: "iModel" = None,
37
38
  invoke_actions: bool = True,
38
39
  tool_schemas: list[dict] = None,
@@ -40,35 +41,32 @@ def prepare_operate_kw(
40
41
  image_detail: Literal["low", "high", "auto"] = None,
41
42
  parse_model: "iModel" = None,
42
43
  skip_validation: bool = False,
44
+ handle_validation: HandleValidation = "return_value",
43
45
  tools: "ToolRef" = None,
44
46
  operative: "Operative" = None,
45
- response_format: type[BaseModel] = None, # alias of operative.request_type
47
+ response_format: type[BaseModel] = None,
46
48
  actions: bool = False,
47
49
  reason: bool = False,
48
50
  call_params: AlcallParams = None,
49
51
  action_strategy: Literal["sequential", "concurrent"] = "concurrent",
50
52
  verbose_action: bool = False,
51
- field_models: list[FieldModel] = None,
52
- exclude_fields: list | dict | None = None,
53
- request_params: ModelParams = None,
54
- request_param_kwargs: dict = None,
55
- handle_validation: HandleValidation = "return_value",
56
- operative_model: type[BaseModel] = None,
57
- request_model: type[BaseModel] = None,
53
+ field_models: list[FieldModel | Spec] = None,
54
+ operative_model: type[BaseModel] = None, # deprecated
55
+ request_model: type[BaseModel] = None, # deprecated
58
56
  include_token_usage_to_model: bool = False,
59
57
  clear_messages: bool = False,
60
58
  **kwargs,
61
- ) -> list | BaseModel | None | dict | str:
59
+ ) -> dict:
62
60
  # Handle deprecated parameters
63
61
  if operative_model:
64
62
  warnings.warn(
65
- "Parameter 'operative_model' is deprecated. Use 'response_format' instead.",
63
+ "Parameter 'operative_model' is deprecated. Use 'response_format'.",
66
64
  DeprecationWarning,
67
65
  stacklevel=2,
68
66
  )
69
67
  if imodel:
70
68
  warnings.warn(
71
- "Parameter 'imodel' is deprecated. Use 'chat_model' instead.",
69
+ "Parameter 'imodel' is deprecated. Use 'chat_model'.",
72
70
  DeprecationWarning,
73
71
  stacklevel=2,
74
72
  )
@@ -79,26 +77,23 @@ def prepare_operate_kw(
79
77
  or (response_format and request_model)
80
78
  ):
81
79
  raise ValueError(
82
- "Cannot specify both `operative_model` and `response_format` (or `request_model`) "
83
- "as they are aliases of each other."
80
+ "Cannot specify multiple of: operative_model, response_format, request_model"
84
81
  )
85
82
 
86
83
  response_format = response_format or operative_model or request_model
87
84
  chat_model = chat_model or imodel or branch.chat_model
88
85
  parse_model = parse_model or chat_model
89
86
 
90
- # Convert dict-based instructions to Instruct if needed
87
+ # Convert dict-based instructions
91
88
  if isinstance(instruct, dict):
92
89
  instruct = Instruct(**instruct)
93
90
 
94
- # Or create a new Instruct if not provided
95
91
  instruct = instruct or Instruct(
96
92
  instruction=instruction,
97
93
  guidance=guidance,
98
94
  context=context,
99
95
  )
100
96
 
101
- # If reason or actions are requested, apply them to instruct
102
97
  if reason:
103
98
  instruct.reason = True
104
99
  if actions:
@@ -106,21 +101,40 @@ def prepare_operate_kw(
106
101
  if action_strategy:
107
102
  instruct.action_strategy = action_strategy
108
103
 
109
- # Build the Operative - always create it for backwards compatibility
110
- from .step import Step
111
-
112
- operative = Step.request_operative(
113
- request_params=request_params,
114
- reason=instruct.reason,
115
- actions=instruct.actions or actions,
116
- exclude_fields=exclude_fields,
117
- base_type=response_format,
118
- field_models=field_models,
119
- **(request_param_kwargs or {}),
104
+ # Convert field_models to Spec if needed
105
+ fields_dict = None
106
+ if field_models:
107
+ fields_dict = {}
108
+ for fm in field_models:
109
+ # Convert FieldModel to Spec
110
+ if isinstance(fm, FieldModel):
111
+ spec = fm.to_spec()
112
+ elif isinstance(fm, Spec):
113
+ spec = fm
114
+ else:
115
+ raise TypeError(f"Expected FieldModel or Spec, got {type(fm)}")
116
+
117
+ if spec.name:
118
+ fields_dict[spec.name] = spec
119
+
120
+ # Build Operative if needed
121
+ operative = None
122
+ if instruct.reason or instruct.actions or response_format or fields_dict:
123
+ from .step import Step
124
+
125
+ operative = Step.request_operative(
126
+ base_type=response_format,
127
+ reason=instruct.reason,
128
+ actions=instruct.actions or actions,
129
+ fields=fields_dict,
130
+ )
131
+
132
+ # Create response model
133
+ operative = Step.respond_operative(operative)
134
+
135
+ final_response_format = (
136
+ operative.response_type if operative else response_format
120
137
  )
121
- # Use the operative's request_type which is a proper Pydantic model
122
- # created from field_models if provided
123
- final_response_format = operative.request_type
124
138
 
125
139
  # Build contexts
126
140
  chat_param = ChatParam(
@@ -146,7 +160,7 @@ def prepare_operate_kw(
146
160
  parse_param = ParseParam(
147
161
  response_format=final_response_format,
148
162
  fuzzy_match_params=FuzzyMatchKeysParams(),
149
- handle_validation="return_value",
163
+ handle_validation=handle_validation,
150
164
  alcall_params=get_default_call(),
151
165
  imodel=parse_model,
152
166
  imodel_kw={},
@@ -175,25 +189,43 @@ def prepare_operate_kw(
175
189
  "invoke_actions": invoke_actions,
176
190
  "skip_validation": skip_validation,
177
191
  "clear_messages": clear_messages,
192
+ "operative": operative,
178
193
  }
179
194
 
180
195
 
181
196
  async def operate(
182
197
  branch: "Branch",
183
- instruction: JsonValue | Instruction,
198
+ instruction: Union[JsonValue, Instruction],
184
199
  chat_param: ChatParam,
185
- action_param: ActionParam | None = None,
186
- parse_param: ParseParam | None = None,
200
+ action_param: Union[ActionParam, None] = None,
201
+ parse_param: Union[ParseParam, None] = None,
187
202
  handle_validation: HandleValidation = "return_value",
188
203
  invoke_actions: bool = True,
189
204
  skip_validation: bool = False,
190
205
  clear_messages: bool = False,
191
206
  reason: bool = False,
192
- field_models: list[FieldModel] | None = None,
193
- ) -> BaseModel | dict | str | None:
194
-
195
- # 1. communicate chat context building to avoid changing parameters
196
- # Start with base chat param
207
+ field_models: Union[list[Union[FieldModel, Spec]], None] = None,
208
+ operative: Union["Operative", None] = None,
209
+ ) -> Union[BaseModel, dict, str, None]:
210
+ """Execute operation with optional action handling.
211
+
212
+ Args:
213
+ branch: Branch instance
214
+ instruction: Instruction or JSON value
215
+ chat_param: Chat parameters
216
+ action_param: Action parameters
217
+ parse_param: Parse parameters
218
+ handle_validation: Validation handling strategy
219
+ invoke_actions: Whether to invoke actions
220
+ skip_validation: Whether to skip validation
221
+ clear_messages: Whether to clear messages
222
+ reason: Whether to include reasoning
223
+ field_models: List of FieldModel or Spec objects
224
+ operative: Operative instance
225
+
226
+ Returns:
227
+ Result of operation
228
+ """
197
229
  _cctx = chat_param
198
230
  _pctx = (
199
231
  parse_param.with_updates(handle_validation="return_value")
@@ -205,12 +237,12 @@ async def operate(
205
237
  )
206
238
  )
207
239
 
208
- # Update tool schemas if needed
240
+ # Update tool schemas
209
241
  if tools := (action_param.tools or True) if action_param else None:
210
242
  tool_schemas = branch.acts.get_tool_schema(tools=tools)
211
243
  _cctx = _cctx.with_updates(tool_schemas=tool_schemas)
212
244
 
213
- # Extract model class from response_format (can be class, instance, or dict)
245
+ # Extract model class
214
246
  model_class = None
215
247
  if chat_param.response_format is not None:
216
248
  if isinstance(chat_param.response_format, type) and issubclass(
@@ -220,36 +252,38 @@ async def operate(
220
252
  elif isinstance(chat_param.response_format, BaseModel):
221
253
  model_class = type(chat_param.response_format)
222
254
 
223
- def normalize_field_model(fms):
224
- if not fms:
225
- return []
226
- if not isinstance(fms, list):
227
- return [fms]
228
- return fms
229
-
230
- fms = normalize_field_model(field_models)
231
- operative = None
232
-
233
- if model_class:
255
+ # Convert field_models to fields dict
256
+ fields_dict = None
257
+ if field_models:
258
+ fields_dict = {}
259
+ for fm in field_models:
260
+ if isinstance(fm, FieldModel):
261
+ spec = fm.to_spec()
262
+ elif isinstance(fm, Spec):
263
+ spec = fm
264
+ else:
265
+ raise TypeError(f"Expected FieldModel or Spec, got {type(fm)}")
266
+
267
+ if spec.name:
268
+ fields_dict[spec.name] = spec
269
+
270
+ # Create operative if needed
271
+ if not operative and (model_class or action_param or fields_dict):
234
272
  from .step import Step
235
273
 
236
274
  operative = Step.request_operative(
237
- reason=reason,
238
- actions=bool(action_param is not None),
239
275
  base_type=model_class,
240
- field_models=fms,
276
+ reason=reason,
277
+ actions=bool(action_param),
278
+ fields=fields_dict,
241
279
  )
242
- # Update contexts with new response format
243
- _cctx = _cctx.with_updates(response_format=operative.request_type)
244
- _pctx = _pctx.with_updates(response_format=operative.request_type)
245
- elif field_models:
246
- dict_ = {}
247
- for fm in fms:
248
- if fm.name:
249
- dict_[fm.name] = str(fm.annotated())
250
- # Update contexts with dict format
251
- _cctx = _cctx.with_updates(response_format=dict_)
252
- _pctx = _pctx.with_updates(response_format=dict_)
280
+ operative = Step.respond_operative(operative)
281
+
282
+ # Update contexts
283
+ response_fmt = operative.response_type or model_class
284
+ if response_fmt:
285
+ _cctx = _cctx.with_updates(response_format=response_fmt)
286
+ _pctx = _pctx.with_updates(response_format=response_fmt)
253
287
 
254
288
  from ..communicate.communicate import communicate
255
289
 
@@ -262,8 +296,10 @@ async def operate(
262
296
  skip_validation=skip_validation,
263
297
  request_fields=None,
264
298
  )
299
+
265
300
  if skip_validation:
266
301
  return result
302
+
267
303
  if model_class and not isinstance(result, model_class):
268
304
  match handle_validation:
269
305
  case "return_value":
@@ -271,12 +307,12 @@ async def operate(
271
307
  case "return_none":
272
308
  return None
273
309
  case "raise":
274
- raise ValueError(
275
- "Failed to parse the LLM response into the requested format."
276
- )
310
+ raise ValueError("Failed to parse LLM response.")
311
+
277
312
  if not invoke_actions:
278
313
  return result
279
314
 
315
+ # Handle actions
280
316
  requests = (
281
317
  getattr(result, "action_requests", None)
282
318
  if model_class
@@ -287,32 +323,28 @@ async def operate(
287
323
  if action_param and requests is not None:
288
324
  from ..act.act import act
289
325
 
290
- action_response_models = await act(
291
- branch,
292
- requests,
293
- action_param,
294
- )
326
+ action_response_models = await act(branch, requests, action_param)
295
327
 
296
328
  if not action_response_models:
297
329
  return result
298
330
 
299
- # Filter out None values from action responses
331
+ # Filter None values
300
332
  action_response_models = [
301
333
  r for r in action_response_models if r is not None
302
334
  ]
303
335
 
304
- if not action_response_models: # All were None
336
+ if not action_response_models:
305
337
  return result
306
338
 
307
339
  if not model_class: # Dict response
308
340
  result.update({"action_responses": action_response_models})
309
341
  return result
310
342
 
311
- from .step import Step
312
-
343
+ # If we have model_class, we must have operative (created at line 268)
344
+ # First set the response_model to the existing result
313
345
  operative.response_model = result
314
- operative = Step.respond_operative(
315
- operative=operative,
316
- additional_data={"action_responses": action_response_models},
346
+ # Then update it with action_responses
347
+ operative.update_response_model(
348
+ data={"action_responses": action_response_models}
317
349
  )
318
350
  return operative.response_model