lionagi 0.18.0__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.
Files changed (93) hide show
  1. lionagi/__init__.py +102 -59
  2. lionagi/_errors.py +0 -5
  3. lionagi/adapters/spec_adapters/__init__.py +9 -0
  4. lionagi/adapters/spec_adapters/_protocol.py +236 -0
  5. lionagi/adapters/spec_adapters/pydantic_field.py +158 -0
  6. lionagi/fields.py +83 -0
  7. lionagi/ln/__init__.py +3 -1
  8. lionagi/ln/_async_call.py +2 -2
  9. lionagi/ln/concurrency/primitives.py +4 -4
  10. lionagi/ln/concurrency/task.py +1 -0
  11. lionagi/ln/fuzzy/_fuzzy_match.py +2 -2
  12. lionagi/ln/types/__init__.py +51 -0
  13. lionagi/ln/types/_sentinel.py +154 -0
  14. lionagi/ln/{types.py → types/base.py} +108 -168
  15. lionagi/ln/types/operable.py +221 -0
  16. lionagi/ln/types/spec.py +441 -0
  17. lionagi/models/field_model.py +69 -7
  18. lionagi/models/hashable_model.py +2 -3
  19. lionagi/models/model_params.py +4 -3
  20. lionagi/operations/ReAct/ReAct.py +1 -1
  21. lionagi/operations/act/act.py +3 -3
  22. lionagi/operations/builder.py +5 -7
  23. lionagi/operations/fields.py +380 -0
  24. lionagi/operations/flow.py +4 -6
  25. lionagi/operations/node.py +4 -4
  26. lionagi/operations/operate/operate.py +123 -89
  27. lionagi/operations/operate/operative.py +198 -0
  28. lionagi/operations/operate/step.py +203 -0
  29. lionagi/operations/select/select.py +1 -1
  30. lionagi/operations/select/utils.py +7 -1
  31. lionagi/operations/types.py +7 -7
  32. lionagi/protocols/action/manager.py +5 -6
  33. lionagi/protocols/contracts.py +2 -2
  34. lionagi/protocols/generic/__init__.py +22 -0
  35. lionagi/protocols/generic/element.py +36 -127
  36. lionagi/protocols/generic/pile.py +9 -10
  37. lionagi/protocols/generic/progression.py +23 -22
  38. lionagi/protocols/graph/edge.py +6 -5
  39. lionagi/protocols/ids.py +6 -49
  40. lionagi/protocols/messages/__init__.py +3 -1
  41. lionagi/protocols/messages/base.py +7 -6
  42. lionagi/protocols/messages/instruction.py +0 -1
  43. lionagi/protocols/messages/message.py +2 -2
  44. lionagi/protocols/types.py +1 -11
  45. lionagi/service/connections/__init__.py +3 -0
  46. lionagi/service/connections/providers/claude_code_cli.py +3 -2
  47. lionagi/service/hooks/_types.py +1 -1
  48. lionagi/service/hooks/_utils.py +1 -1
  49. lionagi/service/hooks/hook_event.py +3 -8
  50. lionagi/service/hooks/hook_registry.py +5 -5
  51. lionagi/service/hooks/hooked_event.py +61 -1
  52. lionagi/service/imodel.py +24 -20
  53. lionagi/service/third_party/claude_code.py +1 -2
  54. lionagi/service/third_party/openai_models.py +24 -22
  55. lionagi/service/token_calculator.py +1 -94
  56. lionagi/session/branch.py +26 -228
  57. lionagi/session/session.py +5 -90
  58. lionagi/version.py +1 -1
  59. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/METADATA +6 -5
  60. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/RECORD +62 -82
  61. lionagi/fields/__init__.py +0 -47
  62. lionagi/fields/action.py +0 -188
  63. lionagi/fields/base.py +0 -153
  64. lionagi/fields/code.py +0 -239
  65. lionagi/fields/file.py +0 -234
  66. lionagi/fields/instruct.py +0 -135
  67. lionagi/fields/reason.py +0 -55
  68. lionagi/fields/research.py +0 -52
  69. lionagi/operations/brainstorm/__init__.py +0 -2
  70. lionagi/operations/brainstorm/brainstorm.py +0 -498
  71. lionagi/operations/brainstorm/prompt.py +0 -11
  72. lionagi/operations/instruct/__init__.py +0 -2
  73. lionagi/operations/instruct/instruct.py +0 -28
  74. lionagi/operations/plan/__init__.py +0 -6
  75. lionagi/operations/plan/plan.py +0 -386
  76. lionagi/operations/plan/prompt.py +0 -25
  77. lionagi/operations/utils.py +0 -45
  78. lionagi/protocols/forms/__init__.py +0 -2
  79. lionagi/protocols/forms/base.py +0 -85
  80. lionagi/protocols/forms/flow.py +0 -79
  81. lionagi/protocols/forms/form.py +0 -86
  82. lionagi/protocols/forms/report.py +0 -48
  83. lionagi/protocols/mail/__init__.py +0 -2
  84. lionagi/protocols/mail/exchange.py +0 -220
  85. lionagi/protocols/mail/mail.py +0 -51
  86. lionagi/protocols/mail/mailbox.py +0 -103
  87. lionagi/protocols/mail/manager.py +0 -218
  88. lionagi/protocols/mail/package.py +0 -101
  89. lionagi/protocols/operatives/__init__.py +0 -2
  90. lionagi/protocols/operatives/operative.py +0 -362
  91. lionagi/protocols/operatives/step.py +0 -227
  92. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/WHEEL +0 -0
  93. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/licenses/LICENSE +0 -0
@@ -2,23 +2,26 @@
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
- from lionagi.fields.instruct import Instruct
9
+ from lionagi.ln import AlcallParams
10
10
  from lionagi.ln.fuzzy import FuzzyMatchKeysParams
11
- from lionagi.models import FieldModel, ModelParams
12
- from lionagi.protocols.types import Instruction, Progression, SenderRecipient
13
- from lionagi.session.branch import AlcallParams
11
+ from lionagi.ln.types import Spec
12
+ from lionagi.models import FieldModel
13
+ from lionagi.protocols.generic import Progression
14
+ from lionagi.protocols.messages import Instruction, SenderRecipient
14
15
 
16
+ from ..fields import Instruct
15
17
  from ..types import ActionParam, ChatParam, HandleValidation, ParseParam
16
18
 
17
19
  if TYPE_CHECKING:
18
- from lionagi.protocols.operatives.step import Operative
19
20
  from lionagi.service.imodel import iModel
20
21
  from lionagi.session.branch import Branch, ToolRef
21
22
 
23
+ from .operative import Operative
24
+
22
25
 
23
26
  def prepare_operate_kw(
24
27
  branch: "Branch",
@@ -30,7 +33,7 @@ def prepare_operate_kw(
30
33
  sender: SenderRecipient = None,
31
34
  recipient: SenderRecipient = None,
32
35
  progression: Progression = None,
33
- imodel: "iModel" = None, # deprecated, alias of chat_model
36
+ imodel: "iModel" = None, # deprecated
34
37
  chat_model: "iModel" = None,
35
38
  invoke_actions: bool = True,
36
39
  tool_schemas: list[dict] = None,
@@ -38,35 +41,32 @@ def prepare_operate_kw(
38
41
  image_detail: Literal["low", "high", "auto"] = None,
39
42
  parse_model: "iModel" = None,
40
43
  skip_validation: bool = False,
44
+ handle_validation: HandleValidation = "return_value",
41
45
  tools: "ToolRef" = None,
42
46
  operative: "Operative" = None,
43
- response_format: type[BaseModel] = None, # alias of operative.request_type
47
+ response_format: type[BaseModel] = None,
44
48
  actions: bool = False,
45
49
  reason: bool = False,
46
50
  call_params: AlcallParams = None,
47
51
  action_strategy: Literal["sequential", "concurrent"] = "concurrent",
48
52
  verbose_action: bool = False,
49
- field_models: list[FieldModel] = None,
50
- exclude_fields: list | dict | None = None,
51
- request_params: ModelParams = None,
52
- request_param_kwargs: dict = None,
53
- handle_validation: HandleValidation = "return_value",
54
- operative_model: type[BaseModel] = None,
55
- 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
56
56
  include_token_usage_to_model: bool = False,
57
57
  clear_messages: bool = False,
58
58
  **kwargs,
59
- ) -> list | BaseModel | None | dict | str:
59
+ ) -> dict:
60
60
  # Handle deprecated parameters
61
61
  if operative_model:
62
62
  warnings.warn(
63
- "Parameter 'operative_model' is deprecated. Use 'response_format' instead.",
63
+ "Parameter 'operative_model' is deprecated. Use 'response_format'.",
64
64
  DeprecationWarning,
65
65
  stacklevel=2,
66
66
  )
67
67
  if imodel:
68
68
  warnings.warn(
69
- "Parameter 'imodel' is deprecated. Use 'chat_model' instead.",
69
+ "Parameter 'imodel' is deprecated. Use 'chat_model'.",
70
70
  DeprecationWarning,
71
71
  stacklevel=2,
72
72
  )
@@ -77,26 +77,23 @@ def prepare_operate_kw(
77
77
  or (response_format and request_model)
78
78
  ):
79
79
  raise ValueError(
80
- "Cannot specify both `operative_model` and `response_format` (or `request_model`) "
81
- "as they are aliases of each other."
80
+ "Cannot specify multiple of: operative_model, response_format, request_model"
82
81
  )
83
82
 
84
83
  response_format = response_format or operative_model or request_model
85
84
  chat_model = chat_model or imodel or branch.chat_model
86
85
  parse_model = parse_model or chat_model
87
86
 
88
- # Convert dict-based instructions to Instruct if needed
87
+ # Convert dict-based instructions
89
88
  if isinstance(instruct, dict):
90
89
  instruct = Instruct(**instruct)
91
90
 
92
- # Or create a new Instruct if not provided
93
91
  instruct = instruct or Instruct(
94
92
  instruction=instruction,
95
93
  guidance=guidance,
96
94
  context=context,
97
95
  )
98
96
 
99
- # If reason or actions are requested, apply them to instruct
100
97
  if reason:
101
98
  instruct.reason = True
102
99
  if actions:
@@ -104,21 +101,40 @@ def prepare_operate_kw(
104
101
  if action_strategy:
105
102
  instruct.action_strategy = action_strategy
106
103
 
107
- # Build the Operative - always create it for backwards compatibility
108
- from lionagi.protocols.operatives.step import Step
109
-
110
- operative = Step.request_operative(
111
- request_params=request_params,
112
- reason=instruct.reason,
113
- actions=instruct.actions or actions,
114
- exclude_fields=exclude_fields,
115
- base_type=response_format,
116
- field_models=field_models,
117
- **(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
118
137
  )
119
- # Use the operative's request_type which is a proper Pydantic model
120
- # created from field_models if provided
121
- final_response_format = operative.request_type
122
138
 
123
139
  # Build contexts
124
140
  chat_param = ChatParam(
@@ -144,7 +160,7 @@ def prepare_operate_kw(
144
160
  parse_param = ParseParam(
145
161
  response_format=final_response_format,
146
162
  fuzzy_match_params=FuzzyMatchKeysParams(),
147
- handle_validation="return_value",
163
+ handle_validation=handle_validation,
148
164
  alcall_params=get_default_call(),
149
165
  imodel=parse_model,
150
166
  imodel_kw={},
@@ -173,25 +189,43 @@ def prepare_operate_kw(
173
189
  "invoke_actions": invoke_actions,
174
190
  "skip_validation": skip_validation,
175
191
  "clear_messages": clear_messages,
192
+ "operative": operative,
176
193
  }
177
194
 
178
195
 
179
196
  async def operate(
180
197
  branch: "Branch",
181
- instruction: JsonValue | Instruction,
198
+ instruction: Union[JsonValue, Instruction],
182
199
  chat_param: ChatParam,
183
- action_param: ActionParam | None = None,
184
- parse_param: ParseParam | None = None,
200
+ action_param: Union[ActionParam, None] = None,
201
+ parse_param: Union[ParseParam, None] = None,
185
202
  handle_validation: HandleValidation = "return_value",
186
203
  invoke_actions: bool = True,
187
204
  skip_validation: bool = False,
188
205
  clear_messages: bool = False,
189
206
  reason: bool = False,
190
- field_models: list[FieldModel] | None = None,
191
- ) -> BaseModel | dict | str | None:
192
-
193
- # 1. communicate chat context building to avoid changing parameters
194
- # 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
+ """
195
229
  _cctx = chat_param
196
230
  _pctx = (
197
231
  parse_param.with_updates(handle_validation="return_value")
@@ -203,12 +237,12 @@ async def operate(
203
237
  )
204
238
  )
205
239
 
206
- # Update tool schemas if needed
240
+ # Update tool schemas
207
241
  if tools := (action_param.tools or True) if action_param else None:
208
242
  tool_schemas = branch.acts.get_tool_schema(tools=tools)
209
243
  _cctx = _cctx.with_updates(tool_schemas=tool_schemas)
210
244
 
211
- # Extract model class from response_format (can be class, instance, or dict)
245
+ # Extract model class
212
246
  model_class = None
213
247
  if chat_param.response_format is not None:
214
248
  if isinstance(chat_param.response_format, type) and issubclass(
@@ -218,36 +252,38 @@ async def operate(
218
252
  elif isinstance(chat_param.response_format, BaseModel):
219
253
  model_class = type(chat_param.response_format)
220
254
 
221
- def normalize_field_model(fms):
222
- if not fms:
223
- return []
224
- if not isinstance(fms, list):
225
- return [fms]
226
- return fms
227
-
228
- fms = normalize_field_model(field_models)
229
- operative = None
230
-
231
- if model_class:
232
- from lionagi.protocols.operatives.step import Step
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):
272
+ from .step import Step
233
273
 
234
274
  operative = Step.request_operative(
235
- reason=reason,
236
- actions=bool(action_param is not None),
237
275
  base_type=model_class,
238
- field_models=fms,
276
+ reason=reason,
277
+ actions=bool(action_param),
278
+ fields=fields_dict,
239
279
  )
240
- # Update contexts with new response format
241
- _cctx = _cctx.with_updates(response_format=operative.request_type)
242
- _pctx = _pctx.with_updates(response_format=operative.request_type)
243
- elif field_models:
244
- dict_ = {}
245
- for fm in fms:
246
- if fm.name:
247
- dict_[fm.name] = str(fm.annotated())
248
- # Update contexts with dict format
249
- _cctx = _cctx.with_updates(response_format=dict_)
250
- _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)
251
287
 
252
288
  from ..communicate.communicate import communicate
253
289
 
@@ -260,8 +296,10 @@ async def operate(
260
296
  skip_validation=skip_validation,
261
297
  request_fields=None,
262
298
  )
299
+
263
300
  if skip_validation:
264
301
  return result
302
+
265
303
  if model_class and not isinstance(result, model_class):
266
304
  match handle_validation:
267
305
  case "return_value":
@@ -269,12 +307,12 @@ async def operate(
269
307
  case "return_none":
270
308
  return None
271
309
  case "raise":
272
- raise ValueError(
273
- "Failed to parse the LLM response into the requested format."
274
- )
310
+ raise ValueError("Failed to parse LLM response.")
311
+
275
312
  if not invoke_actions:
276
313
  return result
277
314
 
315
+ # Handle actions
278
316
  requests = (
279
317
  getattr(result, "action_requests", None)
280
318
  if model_class
@@ -285,32 +323,28 @@ async def operate(
285
323
  if action_param and requests is not None:
286
324
  from ..act.act import act
287
325
 
288
- action_response_models = await act(
289
- branch,
290
- requests,
291
- action_param,
292
- )
326
+ action_response_models = await act(branch, requests, action_param)
293
327
 
294
328
  if not action_response_models:
295
329
  return result
296
330
 
297
- # Filter out None values from action responses
331
+ # Filter None values
298
332
  action_response_models = [
299
333
  r for r in action_response_models if r is not None
300
334
  ]
301
335
 
302
- if not action_response_models: # All were None
336
+ if not action_response_models:
303
337
  return result
304
338
 
305
339
  if not model_class: # Dict response
306
340
  result.update({"action_responses": action_response_models})
307
341
  return result
308
342
 
309
- from lionagi.protocols.operatives.step import Step
310
-
343
+ # If we have model_class, we must have operative (created at line 268)
344
+ # First set the response_model to the existing result
311
345
  operative.response_model = result
312
- operative = Step.respond_operative(
313
- operative=operative,
314
- 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}
315
349
  )
316
350
  return operative.response_model
@@ -0,0 +1,198 @@
1
+ # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from typing import TYPE_CHECKING, Any, Literal
5
+
6
+ from lionagi.ln.types import Operable
7
+
8
+ if TYPE_CHECKING:
9
+ from pydantic import BaseModel
10
+
11
+
12
+ class Operative:
13
+ """Framework-agnostic operation handler using Spec/Operable system.
14
+
15
+ Manages request/response field specifications, delegating framework-specific
16
+ operations to adapters. Single source of truth pattern with one Operable
17
+ containing all fields.
18
+
19
+ Architecture:
20
+ Spec Definition → Operable Collection → Adapter → Framework Model
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ name: str | None = None,
26
+ adapter: Literal["pydantic"] = "pydantic",
27
+ strict: bool = False,
28
+ auto_retry_parse: bool = True,
29
+ max_retries: int = 3,
30
+ base_type: type["BaseModel"] | None = None,
31
+ operable: Operable | None = None,
32
+ request_exclude: set[str] | None = None,
33
+ ):
34
+ """Initialize Operative with a single immutable Operable.
35
+
36
+ Args:
37
+ name: Operation name
38
+ adapter: Validation framework ("pydantic" only for now)
39
+ strict: If True, raise on validation errors
40
+ auto_retry_parse: Auto-retry validation with fuzzy matching
41
+ max_retries: Maximum validation retry attempts
42
+ base_type: Base Pydantic model to extend
43
+ operable: Single Operable with all fields
44
+ request_exclude: Fields to exclude from request (e.g., {"action_responses"})
45
+ """
46
+ self.name = name or (base_type.__name__ if base_type else "Operative")
47
+ self.adapter = adapter
48
+ self.strict = strict
49
+ self.auto_retry_parse = auto_retry_parse
50
+ self.max_retries = max_retries
51
+ self.base_type = base_type
52
+
53
+ # Single source of truth
54
+ self.operable = operable or Operable((), name=self.name)
55
+ self.request_exclude = request_exclude or set()
56
+
57
+ # Materialized models (cached)
58
+ self._request_model_cls = None
59
+ self._response_model_cls = None
60
+
61
+ # Response state
62
+ self.response_model = None
63
+ self.response_str_dict = None
64
+ self._should_retry = None
65
+
66
+ def _get_adapter(self):
67
+ """Get adapter class for current adapter type."""
68
+ if self.adapter == "pydantic":
69
+ from lionagi.adapters.spec_adapters import PydanticSpecAdapter
70
+
71
+ return PydanticSpecAdapter
72
+ else:
73
+ raise ValueError(f"Unsupported adapter: {self.adapter}")
74
+
75
+ def create_request_model(self) -> type:
76
+ """Materialize request specs into model (excluding certain fields)."""
77
+ if self._request_model_cls:
78
+ return self._request_model_cls
79
+
80
+ self._request_model_cls = self.operable.create_model(
81
+ adapter=self.adapter,
82
+ model_name=f"{self.name}Request",
83
+ base_type=self.base_type,
84
+ exclude=self.request_exclude,
85
+ )
86
+ return self._request_model_cls
87
+
88
+ def create_response_model(self) -> type:
89
+ """Materialize all specs into response model."""
90
+ if self._response_model_cls:
91
+ return self._response_model_cls
92
+
93
+ # Ensure request model exists first
94
+ if not self._request_model_cls:
95
+ self.create_request_model()
96
+
97
+ # Response model uses ALL fields and inherits from request
98
+ self._response_model_cls = self.operable.create_model(
99
+ adapter=self.adapter,
100
+ model_name=f"{self.name}Response",
101
+ base_type=self._request_model_cls,
102
+ )
103
+
104
+ return self._response_model_cls
105
+
106
+ def validate_response(self, text: str, strict: bool | None = None) -> Any:
107
+ """Validate response text using adapter.
108
+
109
+ Args:
110
+ text: Raw response text
111
+ strict: If True, raise on validation errors
112
+
113
+ Returns:
114
+ Validated model instance or None
115
+ """
116
+ strict = self.strict if strict is None else strict
117
+
118
+ if not self._response_model_cls:
119
+ self.create_response_model()
120
+
121
+ adapter_cls = self._get_adapter()
122
+
123
+ try:
124
+ self.response_model = adapter_cls.validate_response(
125
+ text,
126
+ self._response_model_cls,
127
+ strict=strict,
128
+ fuzzy_parse=True,
129
+ )
130
+ self._should_retry = False
131
+ return self.response_model
132
+
133
+ except Exception as e:
134
+ self.response_str_dict = text
135
+ self._should_retry = strict
136
+
137
+ if strict:
138
+ raise e
139
+
140
+ # Try fuzzy validation if auto-retry enabled
141
+ if self.auto_retry_parse and not strict:
142
+ try:
143
+ self.response_model = adapter_cls.validate_response(
144
+ text,
145
+ self._response_model_cls,
146
+ strict=False,
147
+ fuzzy_parse=True,
148
+ )
149
+ self._should_retry = False
150
+ return self.response_model
151
+ except Exception:
152
+ pass
153
+
154
+ return None
155
+
156
+ def update_response_model(
157
+ self, text: str | None = None, data: dict | None = None
158
+ ) -> Any:
159
+ """Update response model from text or dict.
160
+
161
+ Args:
162
+ text: Raw response text to validate
163
+ data: Dictionary updates to merge
164
+
165
+ Returns:
166
+ Updated model instance or raw data
167
+ """
168
+ if text is None and data is None:
169
+ raise ValueError("Either text or data must be provided")
170
+
171
+ if text:
172
+ self.response_str_dict = text
173
+ self.validate_response(text, strict=False)
174
+
175
+ if data and self._response_model_cls and self.response_model:
176
+ adapter_cls = self._get_adapter()
177
+ self.response_model = adapter_cls.update_model(
178
+ self.response_model, data, self._response_model_cls
179
+ )
180
+
181
+ return self.response_model or self.response_str_dict
182
+
183
+ @property
184
+ def request_type(self) -> type | None:
185
+ """Get request model type."""
186
+ if not self._request_model_cls:
187
+ self.create_request_model()
188
+ return self._request_model_cls
189
+
190
+ @property
191
+ def response_type(self) -> type | None:
192
+ """Get response model type."""
193
+ if not self._response_model_cls:
194
+ self.create_response_model()
195
+ return self._response_model_cls
196
+
197
+
198
+ __all__ = ("Operative",)