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
@@ -1,218 +0,0 @@
1
- # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- import asyncio
5
- from collections import deque
6
- from typing import Any
7
-
8
- from lionagi._errors import ItemNotFoundError
9
-
10
- from .._concepts import Manager, Observable
11
- from ..generic.element import ID, IDType
12
- from ..generic.pile import Pile, to_list_type
13
- from .exchange import Exchange
14
- from .mail import Mail, Package, PackageCategory
15
-
16
-
17
- class MailManager(Manager):
18
- """
19
- A manager for mail operations across various observable sources
20
- within LionAGI. Unlike `Exchange`, this class can manage the state
21
- of multiple sources in a more general or higher-level context,
22
- storing mail queues in a dictionary rather than individual buffers.
23
-
24
- Attributes
25
- ----------
26
- sources : Pile[Observable]
27
- A concurrency-safe collection of known sources.
28
- mails : dict[str, dict[str, deque]]
29
- A nested mapping of recipient -> sender -> queue of mail.
30
- execute_stop : bool
31
- Controls the asynchronous execution loop; set to True to exit.
32
- """
33
-
34
- def __init__(self, sources: ID.Item | ID.ItemSeq = None) -> None:
35
- """
36
- Initialize a MailManager instance.
37
-
38
- Parameters
39
- ----------
40
- sources : ID.Item | ID.ItemSeq, optional
41
- Initial source(s) to manage. Each source must be an Observable.
42
- """
43
- self.sources: Pile[Observable] = Pile()
44
- self.mails: dict[str, dict[str, deque]] = {}
45
- self.execute_stop: bool = False
46
-
47
- if sources:
48
- self.add_sources(sources)
49
-
50
- def add_sources(self, sources: ID.Item | ID.ItemSeq, /) -> None:
51
- """
52
- Register new sources in the MailManager.
53
-
54
- Parameters
55
- ----------
56
- sources : ID.Item | ID.ItemSeq
57
- A single source or multiple sources to be added.
58
-
59
- Raises
60
- ------
61
- ValueError
62
- If adding the sources fails for any reason.
63
- """
64
- try:
65
- sources = to_list_type(sources)
66
- self.sources.include(sources)
67
- for item in sources:
68
- self.mails[item.id] = {}
69
- except Exception as e:
70
- raise ValueError("Failed to add source.") from e
71
-
72
- @staticmethod
73
- def create_mail(
74
- sender: ID.Ref,
75
- recipient: ID.Ref,
76
- category: PackageCategory | str,
77
- package: Any,
78
- request_source: Any = None,
79
- ) -> Mail:
80
- """
81
- Factory method to generate a Mail object.
82
-
83
- Parameters
84
- ----------
85
- sender : ID.Ref
86
- Reference (ID or object) for the sender.
87
- recipient : ID.Ref
88
- Reference (ID or object) for the recipient.
89
- category : PackageCategory | str
90
- The category of this package.
91
- package : Any
92
- The payload or content in the mail.
93
- request_source : Any, optional
94
- Additional context about the request source.
95
-
96
- Returns
97
- -------
98
- Mail
99
- A new mail object with specified sender, recipient, and package.
100
- """
101
- pack = Package(
102
- category=category, package=package, request_source=request_source
103
- )
104
- return Mail(sender=sender, recipient=recipient, package=pack)
105
-
106
- def delete_source(self, source_id: IDType) -> None:
107
- """
108
- Remove a source from the manager, discarding any associated mail.
109
-
110
- Parameters
111
- ----------
112
- source_id : IDType
113
- The ID of the source to be removed.
114
-
115
- Raises
116
- ------
117
- ItemNotFoundError
118
- If the given source ID is not present.
119
- """
120
- if source_id not in self.sources:
121
- raise ItemNotFoundError(f"Source {source_id} does not exist.")
122
- self.sources.pop(source_id)
123
- self.mails.pop(source_id)
124
-
125
- def collect(self, sender: IDType) -> None:
126
- """
127
- Collect outbound mail from a single source.
128
-
129
- Parameters
130
- ----------
131
- sender : IDType
132
- The ID of the sender whose outbound mail is retrieved.
133
-
134
- Raises
135
- ------
136
- ItemNotFoundError
137
- If the sender is not recognized.
138
- """
139
- if sender not in self.sources:
140
- raise ItemNotFoundError(f"Sender source {sender} does not exist.")
141
- mailbox: Exchange = (
142
- self.sources[sender]
143
- if isinstance(self.sources[sender], Exchange)
144
- else self.sources[sender].mailbox
145
- )
146
- while mailbox.pending_outs.size() > 0:
147
- mail_id = mailbox.pending_outs.popleft()
148
- mail: Mail = mailbox.pile_.pop(mail_id)
149
- if mail.recipient not in self.sources:
150
- rec_ = mail.recipient
151
- raise ItemNotFoundError(
152
- f"Recipient source {rec_} does not exist"
153
- )
154
- if mail.sender not in self.mails[mail.recipient]:
155
- self.mails[mail.recipient].update({mail.sender: deque()})
156
- self.mails[mail.recipient][mail.sender].append(mail)
157
-
158
- def send(self, recipient: IDType) -> None:
159
- """
160
- Send any pending mail to a specified recipient.
161
-
162
- Parameters
163
- ----------
164
- recipient : IDType
165
- The ID of the recipient to which mail should be delivered.
166
-
167
- Raises
168
- ------
169
- ItemNotFoundError
170
- If the recipient ID is not recognized.
171
- """
172
- if recipient not in self.sources:
173
- raise ItemNotFoundError(
174
- f"Recipient source {recipient} does not exist."
175
- )
176
- if not self.mails[recipient]:
177
- return
178
- for key in list(self.mails[recipient].keys()):
179
- pending_mails = self.mails[recipient].pop(key)
180
- mailbox: Exchange = (
181
- self.sources[recipient]
182
- if isinstance(self.sources[recipient], Exchange)
183
- else self.sources[recipient].mailbox
184
- )
185
- while pending_mails:
186
- mail = pending_mails.popleft()
187
- mailbox.include(mail, direction="in")
188
-
189
- def collect_all(self) -> None:
190
- """
191
- Collect outbound mail from all known sources.
192
- """
193
- for source in self.sources:
194
- self.collect(sender=source.id)
195
-
196
- def send_all(self) -> None:
197
- """
198
- Send mail to all known recipients who have pending items.
199
- """
200
- for source in self.sources:
201
- self.send(recipient=source.id)
202
-
203
- async def execute(self, refresh_time: int = 1) -> None:
204
- """
205
- Continuously collect and send mail in an asynchronous loop.
206
-
207
- Parameters
208
- ----------
209
- refresh_time : int, optional
210
- Delay (in seconds) between each collect/send cycle.
211
- """
212
- while not self.execute_stop:
213
- self.collect_all()
214
- self.send_all()
215
- await asyncio.sleep(refresh_time)
216
-
217
-
218
- # File: lion_core/communication/manager.py
@@ -1,101 +0,0 @@
1
- # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- from enum import Enum
5
- from typing import Any
6
-
7
- from lionagi.ln import now_utc
8
- from lionagi.protocols.generic.element import ID, IDType
9
-
10
- from .._concepts import Communicatable, Observable
11
-
12
-
13
- class PackageCategory(str, Enum):
14
- """
15
- Enumeration of common package categories in LionAGI:
16
-
17
- - MESSAGE: General message content
18
- - TOOL: A tool or action to be invoked
19
- - IMODEL: Some internal model reference
20
- - NODE: A node in a graph
21
- - NODE_LIST: A list of nodes
22
- - NODE_ID: A node ID
23
- - START: A 'start' signal
24
- - END: An 'end' signal
25
- - CONDITION: A condition or gating logic
26
- - SIGNAL: A more generic signal or marker
27
- """
28
-
29
- MESSAGE = "message"
30
- TOOL = "tool"
31
- IMODEL = "imodel"
32
- NODE = "node"
33
- NODE_LIST = "node_list"
34
- NODE_ID = "node_id"
35
- START = "start"
36
- END = "end"
37
- CONDITION = "condition"
38
- SIGNAL = "signal"
39
-
40
-
41
- def validate_category(value: Any) -> PackageCategory:
42
- """
43
- Validate and convert the input to a valid PackageCategory.
44
-
45
- Parameters
46
- ----------
47
- value : Any
48
- The input to interpret as a `PackageCategory`.
49
-
50
- Returns
51
- -------
52
- PackageCategory
53
- The validated category.
54
-
55
- Raises
56
- ------
57
- ValueError
58
- If the value cannot be converted into a valid package category.
59
- """
60
- if isinstance(value, PackageCategory):
61
- return value
62
- try:
63
- return PackageCategory(str(value))
64
- except ValueError as e:
65
- raise ValueError("Invalid value for category.") from e
66
-
67
-
68
- class Package(Observable):
69
- """
70
- A self-contained package that can be attached to `Mail` items.
71
- Includes a unique ID, creation timestamp, category, payload item,
72
- and an optional request source for context.
73
-
74
- Attributes
75
- ----------
76
- category : PackageCategory
77
- The classification or type of package.
78
- item : Any
79
- The main payload or data of this package.
80
- request_source : ID[Communicatable] | None
81
- An optional reference indicating the origin or context
82
- for this package.
83
- """
84
-
85
- __slots__ = ("id", "created_at", "category", "item", "request_source")
86
-
87
- def __init__(
88
- self,
89
- category: PackageCategory,
90
- item: Any,
91
- request_source: ID[Communicatable] = None,
92
- ):
93
- super().__init__()
94
- self.id = IDType.create()
95
- self.created_at = now_utc().timestamp()
96
- self.category = validate_category(category)
97
- self.item = item
98
- self.request_source = request_source
99
-
100
-
101
- # File: lionagi/protocols/mail/package.py
@@ -1,2 +0,0 @@
1
- # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
2
- # SPDX-License-Identifier: Apache-2.0
@@ -1,362 +0,0 @@
1
- # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- from typing import Any
5
-
6
- from pydantic import BaseModel
7
- from pydantic.fields import FieldInfo
8
-
9
- from lionagi.ln import extract_json
10
- from lionagi.ln.fuzzy._fuzzy_match import fuzzy_match_keys
11
- from lionagi.models import FieldModel, ModelParams, OperableModel
12
- from lionagi.utils import UNDEFINED
13
-
14
-
15
- class Operative:
16
- """Class representing an operative that handles request and response models for operations.
17
-
18
- This implementation uses OperableModel internally for better performance while
19
- maintaining backward compatibility with the existing API.
20
- """
21
-
22
- def __init__(
23
- self,
24
- name: str | None = None,
25
- request_type: type[BaseModel] | None = None,
26
- response_type: type[BaseModel] | None = None,
27
- response_model: BaseModel | None = None,
28
- response_str_dict: dict | str | None = None,
29
- auto_retry_parse: bool = True,
30
- max_retries: int = 3,
31
- parse_kwargs: dict | None = None,
32
- request_params: (
33
- ModelParams | None
34
- ) = None, # Deprecated, for backward compatibility
35
- **_kwargs, # Ignored for backward compatibility
36
- ):
37
- """Initialize the Operative.
38
-
39
- Args:
40
- name: Name of the operative
41
- request_type: Pydantic model type for requests
42
- response_type: Pydantic model type for responses
43
- response_model: Current response model instance
44
- response_str_dict: Raw response string/dict
45
- auto_retry_parse: Whether to auto-retry parsing
46
- max_retries: Maximum parse retries
47
- parse_kwargs: Additional parse arguments
48
- request_params: Deprecated - use direct field addition
49
- response_params: Deprecated - use direct field addition
50
- """
51
- self.name = name
52
- self.request_type = request_type
53
- self.response_type = response_type
54
- self.response_model = response_model
55
- self.response_str_dict = response_str_dict
56
- self.auto_retry_parse = auto_retry_parse
57
- self.max_retries = max_retries
58
- self.parse_kwargs = parse_kwargs or {}
59
- self._should_retry = None
60
-
61
- # Internal OperableModel instances
62
- self._request_operable = OperableModel()
63
- self._response_operable = OperableModel()
64
-
65
- # Handle deprecated ModelParams for backward compatibility
66
- if request_params:
67
- self._init_from_model_params(request_params)
68
-
69
- # Set default name if not provided
70
- if not self.name:
71
- self.name = (
72
- self.request_type.__name__
73
- if self.request_type
74
- else "Operative"
75
- )
76
-
77
- def _init_from_model_params(self, params: ModelParams):
78
- """Initialize from ModelParams for backward compatibility."""
79
- # Add field models to the request operable
80
- if params.field_models:
81
- # Coerce to list if single FieldModel instance
82
- field_models_list = (
83
- [params.field_models]
84
- if isinstance(params.field_models, FieldModel)
85
- else params.field_models
86
- )
87
- for field_model in field_models_list:
88
- self._request_operable.add_field(
89
- field_model.name,
90
- field_model=field_model,
91
- annotation=field_model.base_type,
92
- )
93
-
94
- # Add parameter fields (skip if already added from field_models)
95
- if params.parameter_fields:
96
- for name, field_info in params.parameter_fields.items():
97
- if (
98
- name not in (params.exclude_fields or [])
99
- and name not in self._request_operable.all_fields
100
- ):
101
- self._request_operable.add_field(
102
- name, field_obj=field_info
103
- )
104
-
105
- # Generate request_type if not provided
106
- if not self.request_type:
107
- exclude_fields = params.exclude_fields or []
108
- use_fields = set(self._request_operable.all_fields.keys()) - set(
109
- exclude_fields
110
- )
111
-
112
- # Determine model name - prefer explicit name, then base_type name, then default
113
- model_name = "RequestModel"
114
- if not params._is_sentinel(params.name):
115
- model_name = params.name
116
- elif not params._is_sentinel(params.base_type):
117
- model_name = params.base_type.__name__
118
-
119
- self.request_type = self._request_operable.new_model(
120
- name=model_name,
121
- use_fields=use_fields,
122
- base_type=params.base_type,
123
- frozen=params.frozen,
124
- config_dict=params.config_dict,
125
- doc=params.doc,
126
- )
127
-
128
- # Update name if not set - prefer explicit name, then base_type name
129
- if not self.name:
130
- if not params._is_sentinel(params.name):
131
- self.name = params.name
132
- elif not params._is_sentinel(params.base_type):
133
- self.name = params.base_type.__name__
134
-
135
- def model_dump(self) -> dict[str, Any]:
136
- """Convert to dictionary for backward compatibility.
137
-
138
- Note: This returns a Python dict, not JSON-serializable data.
139
- For JSON serialization, convert types appropriately.
140
- """
141
- return {
142
- "name": self.name,
143
- "request_type": self.request_type, # Python class object
144
- "response_type": self.response_type, # Python class object
145
- "response_model": self.response_model,
146
- "response_str_dict": self.response_str_dict,
147
- "auto_retry_parse": self.auto_retry_parse,
148
- "max_retries": self.max_retries,
149
- "parse_kwargs": self.parse_kwargs,
150
- }
151
-
152
- def to_dict(self) -> dict[str, Any]:
153
- """Alias for model_dump() - more appropriate name for non-Pydantic class."""
154
- return self.model_dump()
155
-
156
- def raise_validate_pydantic(self, text: str) -> None:
157
- """Validates and updates the response model using strict matching.
158
-
159
- Args:
160
- text (str): The text to validate and parse into the response model.
161
-
162
- Raises:
163
- Exception: If the validation fails.
164
- """
165
- d_ = extract_json(text, fuzzy_parse=True)
166
- if isinstance(d_, list | tuple) and len(d_) == 1:
167
- d_ = d_[0]
168
- try:
169
- d_ = fuzzy_match_keys(
170
- d_, self.request_type.model_fields, handle_unmatched="raise"
171
- )
172
- d_ = {k: v for k, v in d_.items() if v != UNDEFINED}
173
- self.response_model = self.request_type.model_validate(d_)
174
- self._should_retry = False
175
- except Exception:
176
- self.response_str_dict = d_
177
- self._should_retry = True
178
-
179
- def force_validate_pydantic(self, text: str):
180
- """Forcibly validates and updates the response model, allowing unmatched fields.
181
-
182
- Args:
183
- text (str): The text to validate and parse into the response model.
184
- """
185
- d_ = text
186
- try:
187
- d_ = extract_json(text, fuzzy_parse=True)
188
- if isinstance(d_, list | tuple) and len(d_) == 1:
189
- d_ = d_[0]
190
- d_ = fuzzy_match_keys(
191
- d_, self.request_type.model_fields, handle_unmatched="force"
192
- )
193
- d_ = {k: v for k, v in d_.items() if v != UNDEFINED}
194
- self.response_model = self.request_type.model_validate(d_)
195
- self._should_retry = False
196
- except Exception:
197
- self.response_str_dict = d_
198
- self.response_model = None
199
- self._should_retry = True
200
-
201
- def update_response_model(
202
- self, text: str | None = None, data: dict | None = None
203
- ) -> BaseModel | dict | str | None:
204
- """Updates the response model based on the provided text or data.
205
-
206
- Args:
207
- text (str, optional): The text to parse and validate.
208
- data (dict, optional): The data to update the response model with.
209
-
210
- Returns:
211
- BaseModel | dict | str | None: The updated response model or raw data.
212
-
213
- Raises:
214
- ValueError: If neither text nor data is provided.
215
- """
216
- if text is None and data is None:
217
- raise ValueError("Either text or data must be provided.")
218
-
219
- if text:
220
- self.response_str_dict = text
221
- try:
222
- self.raise_validate_pydantic(text)
223
- except Exception:
224
- self.force_validate_pydantic(text)
225
-
226
- if data and self.response_type:
227
- d_ = self.response_model.model_dump()
228
- d_.update(data)
229
- self.response_model = self.response_type.model_validate(d_)
230
-
231
- if not self.response_model and isinstance(
232
- self.response_str_dict, list
233
- ):
234
- try:
235
- self.response_model = [
236
- self.request_type.model_validate(d_)
237
- for d_ in self.response_str_dict
238
- ]
239
- except Exception:
240
- pass
241
-
242
- return self.response_model or self.response_str_dict
243
-
244
- def create_response_type(
245
- self,
246
- response_params: ModelParams | None = None,
247
- field_models: list[FieldModel] | None = None,
248
- parameter_fields: dict[str, FieldInfo] | None = None,
249
- exclude_fields: list[str] | None = None,
250
- field_descriptions: dict[str, str] | None = None,
251
- inherit_base: bool = True,
252
- config_dict: dict | None = None,
253
- doc: str | None = None,
254
- frozen: bool = False,
255
- validators: dict | None = None,
256
- ) -> None:
257
- """Creates a new response type based on the provided parameters.
258
-
259
- Args:
260
- response_params (ModelParams, optional): Parameters for the new response model.
261
- field_models (list[FieldModel], optional): List of field models.
262
- parameter_fields (dict[str, FieldInfo], optional): Dictionary of parameter fields.
263
- exclude_fields (list, optional): List of fields to exclude.
264
- field_descriptions (dict, optional): Dictionary of field descriptions.
265
- inherit_base (bool, optional): Whether to inherit the base model.
266
- config_dict (dict | None, optional): Configuration dictionary.
267
- doc (str | None, optional): Documentation string.
268
- frozen (bool, optional): Whether the model is frozen.
269
- validators (dict, optional): Dictionary of validators.
270
- """
271
- # Process response_params if provided (for backward compatibility)
272
- if response_params:
273
- # Extract values from ModelParams
274
- field_models = field_models or response_params.field_models
275
- parameter_fields = (
276
- parameter_fields or response_params.parameter_fields
277
- )
278
- exclude_fields = exclude_fields or response_params.exclude_fields
279
- field_descriptions = (
280
- field_descriptions or response_params.field_descriptions
281
- )
282
- inherit_base = (
283
- response_params.inherit_base if inherit_base else False
284
- )
285
- config_dict = config_dict or response_params.config_dict
286
- doc = doc or response_params.doc
287
- frozen = frozen or response_params.frozen
288
-
289
- # Clear response operable and rebuild
290
- self._response_operable = OperableModel()
291
-
292
- # Copy fields from request operable if inherit_base
293
- if inherit_base and self._request_operable:
294
- for (
295
- field_name,
296
- field_model,
297
- ) in self._request_operable.extra_field_models.items():
298
- self._response_operable.add_field(
299
- field_name, field_model=field_model
300
- )
301
-
302
- # Add field models (skip if already exists from inheritance)
303
- if field_models:
304
- # Coerce to list if single FieldModel instance
305
- field_models_list = (
306
- [field_models]
307
- if isinstance(field_models, FieldModel)
308
- else field_models
309
- )
310
- for field_model in field_models_list:
311
- if field_model.name not in self._response_operable.all_fields:
312
- self._response_operable.add_field(
313
- field_model.name,
314
- field_model=field_model,
315
- annotation=field_model.base_type,
316
- )
317
-
318
- # Add parameter fields (skip if already added)
319
- if parameter_fields:
320
- for name, field_info in parameter_fields.items():
321
- if (
322
- name not in (exclude_fields or [])
323
- and name not in self._response_operable.all_fields
324
- ):
325
- self._response_operable.add_field(
326
- name, field_obj=field_info
327
- )
328
-
329
- # Add validators if provided
330
- if validators:
331
- for field_name, validator in validators.items():
332
- if field_name in self._response_operable.all_fields:
333
- field_model = (
334
- self._response_operable.extra_field_models.get(
335
- field_name
336
- )
337
- )
338
- if field_model:
339
- field_model.validator = validator
340
-
341
- # Generate response type
342
- exclude_fields = exclude_fields or []
343
- use_fields = set(self._response_operable.all_fields.keys()) - set(
344
- exclude_fields
345
- )
346
-
347
- # Determine base type - use request_type if inheriting and no specific base provided
348
- base_type = None
349
- if response_params and response_params.base_type:
350
- base_type = response_params.base_type
351
- elif inherit_base and self.request_type:
352
- base_type = self.request_type
353
-
354
- self.response_type = self._response_operable.new_model(
355
- name=(response_params.name if response_params else None)
356
- or "ResponseModel",
357
- use_fields=use_fields,
358
- base_type=base_type,
359
- frozen=frozen,
360
- config_dict=config_dict,
361
- doc=doc,
362
- )