lionagi 0.17.10__py3-none-any.whl → 0.18.0__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 (164) hide show
  1. lionagi/__init__.py +1 -2
  2. lionagi/_class_registry.py +1 -2
  3. lionagi/_errors.py +1 -2
  4. lionagi/adapters/async_postgres_adapter.py +2 -10
  5. lionagi/config.py +1 -2
  6. lionagi/fields/action.py +1 -2
  7. lionagi/fields/base.py +3 -0
  8. lionagi/fields/code.py +3 -0
  9. lionagi/fields/file.py +3 -0
  10. lionagi/fields/instruct.py +1 -2
  11. lionagi/fields/reason.py +1 -2
  12. lionagi/fields/research.py +3 -0
  13. lionagi/libs/__init__.py +1 -2
  14. lionagi/libs/file/__init__.py +1 -2
  15. lionagi/libs/file/chunk.py +1 -2
  16. lionagi/libs/file/process.py +1 -2
  17. lionagi/libs/schema/__init__.py +1 -2
  18. lionagi/libs/schema/as_readable.py +1 -2
  19. lionagi/libs/schema/extract_code_block.py +1 -2
  20. lionagi/libs/schema/extract_docstring.py +1 -2
  21. lionagi/libs/schema/function_to_schema.py +1 -2
  22. lionagi/libs/schema/load_pydantic_model_from_schema.py +1 -2
  23. lionagi/libs/schema/minimal_yaml.py +98 -0
  24. lionagi/libs/validate/__init__.py +1 -2
  25. lionagi/libs/validate/common_field_validators.py +1 -2
  26. lionagi/libs/validate/validate_boolean.py +1 -2
  27. lionagi/ln/fuzzy/_string_similarity.py +1 -2
  28. lionagi/ln/types.py +32 -5
  29. lionagi/models/__init__.py +1 -2
  30. lionagi/models/field_model.py +9 -1
  31. lionagi/models/hashable_model.py +4 -2
  32. lionagi/models/model_params.py +1 -2
  33. lionagi/models/operable_model.py +1 -2
  34. lionagi/models/schema_model.py +1 -2
  35. lionagi/operations/ReAct/ReAct.py +475 -239
  36. lionagi/operations/ReAct/__init__.py +1 -2
  37. lionagi/operations/ReAct/utils.py +4 -2
  38. lionagi/operations/__init__.py +1 -2
  39. lionagi/operations/act/__init__.py +2 -0
  40. lionagi/operations/act/act.py +206 -0
  41. lionagi/operations/brainstorm/__init__.py +1 -2
  42. lionagi/operations/brainstorm/brainstorm.py +1 -2
  43. lionagi/operations/brainstorm/prompt.py +1 -2
  44. lionagi/operations/builder.py +1 -2
  45. lionagi/operations/chat/__init__.py +1 -2
  46. lionagi/operations/chat/chat.py +131 -116
  47. lionagi/operations/communicate/communicate.py +102 -44
  48. lionagi/operations/flow.py +5 -6
  49. lionagi/operations/instruct/__init__.py +1 -2
  50. lionagi/operations/instruct/instruct.py +1 -2
  51. lionagi/operations/interpret/__init__.py +1 -2
  52. lionagi/operations/interpret/interpret.py +66 -22
  53. lionagi/operations/operate/__init__.py +1 -2
  54. lionagi/operations/operate/operate.py +213 -108
  55. lionagi/operations/parse/__init__.py +1 -2
  56. lionagi/operations/parse/parse.py +171 -144
  57. lionagi/operations/plan/__init__.py +1 -2
  58. lionagi/operations/plan/plan.py +1 -2
  59. lionagi/operations/plan/prompt.py +1 -2
  60. lionagi/operations/select/__init__.py +1 -2
  61. lionagi/operations/select/select.py +79 -19
  62. lionagi/operations/select/utils.py +2 -3
  63. lionagi/operations/types.py +120 -25
  64. lionagi/operations/utils.py +1 -2
  65. lionagi/protocols/__init__.py +1 -2
  66. lionagi/protocols/_concepts.py +1 -2
  67. lionagi/protocols/action/__init__.py +1 -2
  68. lionagi/protocols/action/function_calling.py +3 -20
  69. lionagi/protocols/action/manager.py +34 -4
  70. lionagi/protocols/action/tool.py +1 -2
  71. lionagi/protocols/contracts.py +1 -2
  72. lionagi/protocols/forms/__init__.py +1 -2
  73. lionagi/protocols/forms/base.py +1 -2
  74. lionagi/protocols/forms/flow.py +1 -2
  75. lionagi/protocols/forms/form.py +1 -2
  76. lionagi/protocols/forms/report.py +1 -2
  77. lionagi/protocols/generic/__init__.py +1 -2
  78. lionagi/protocols/generic/element.py +17 -65
  79. lionagi/protocols/generic/event.py +1 -2
  80. lionagi/protocols/generic/log.py +17 -14
  81. lionagi/protocols/generic/pile.py +3 -4
  82. lionagi/protocols/generic/processor.py +1 -2
  83. lionagi/protocols/generic/progression.py +1 -2
  84. lionagi/protocols/graph/__init__.py +1 -2
  85. lionagi/protocols/graph/edge.py +1 -2
  86. lionagi/protocols/graph/graph.py +1 -2
  87. lionagi/protocols/graph/node.py +1 -2
  88. lionagi/protocols/ids.py +1 -2
  89. lionagi/protocols/mail/__init__.py +1 -2
  90. lionagi/protocols/mail/exchange.py +1 -2
  91. lionagi/protocols/mail/mail.py +1 -2
  92. lionagi/protocols/mail/mailbox.py +1 -2
  93. lionagi/protocols/mail/manager.py +1 -2
  94. lionagi/protocols/mail/package.py +1 -2
  95. lionagi/protocols/messages/__init__.py +28 -2
  96. lionagi/protocols/messages/action_request.py +87 -186
  97. lionagi/protocols/messages/action_response.py +74 -133
  98. lionagi/protocols/messages/assistant_response.py +131 -161
  99. lionagi/protocols/messages/base.py +27 -20
  100. lionagi/protocols/messages/instruction.py +281 -626
  101. lionagi/protocols/messages/manager.py +113 -64
  102. lionagi/protocols/messages/message.py +88 -199
  103. lionagi/protocols/messages/system.py +53 -125
  104. lionagi/protocols/operatives/__init__.py +1 -2
  105. lionagi/protocols/operatives/operative.py +1 -2
  106. lionagi/protocols/operatives/step.py +1 -2
  107. lionagi/protocols/types.py +1 -4
  108. lionagi/service/connections/__init__.py +1 -2
  109. lionagi/service/connections/api_calling.py +1 -2
  110. lionagi/service/connections/endpoint.py +1 -10
  111. lionagi/service/connections/endpoint_config.py +1 -2
  112. lionagi/service/connections/header_factory.py +1 -2
  113. lionagi/service/connections/match_endpoint.py +1 -2
  114. lionagi/service/connections/mcp/__init__.py +1 -2
  115. lionagi/service/connections/mcp/wrapper.py +1 -2
  116. lionagi/service/connections/providers/__init__.py +1 -2
  117. lionagi/service/connections/providers/anthropic_.py +1 -2
  118. lionagi/service/connections/providers/claude_code_cli.py +1 -2
  119. lionagi/service/connections/providers/exa_.py +1 -2
  120. lionagi/service/connections/providers/nvidia_nim_.py +2 -27
  121. lionagi/service/connections/providers/oai_.py +30 -96
  122. lionagi/service/connections/providers/ollama_.py +4 -4
  123. lionagi/service/connections/providers/perplexity_.py +1 -2
  124. lionagi/service/hooks/__init__.py +1 -1
  125. lionagi/service/hooks/_types.py +1 -1
  126. lionagi/service/hooks/_utils.py +1 -1
  127. lionagi/service/hooks/hook_event.py +1 -1
  128. lionagi/service/hooks/hook_registry.py +1 -1
  129. lionagi/service/hooks/hooked_event.py +3 -4
  130. lionagi/service/imodel.py +1 -2
  131. lionagi/service/manager.py +1 -2
  132. lionagi/service/rate_limited_processor.py +1 -2
  133. lionagi/service/resilience.py +1 -2
  134. lionagi/service/third_party/anthropic_models.py +1 -2
  135. lionagi/service/third_party/claude_code.py +4 -4
  136. lionagi/service/third_party/openai_models.py +433 -0
  137. lionagi/service/token_calculator.py +1 -2
  138. lionagi/session/__init__.py +1 -2
  139. lionagi/session/branch.py +171 -180
  140. lionagi/session/session.py +4 -11
  141. lionagi/tools/__init__.py +1 -2
  142. lionagi/tools/base.py +1 -2
  143. lionagi/tools/file/__init__.py +1 -2
  144. lionagi/tools/file/reader.py +3 -4
  145. lionagi/tools/types.py +1 -2
  146. lionagi/utils.py +1 -2
  147. lionagi/version.py +1 -1
  148. {lionagi-0.17.10.dist-info → lionagi-0.18.0.dist-info}/METADATA +1 -2
  149. lionagi-0.18.0.dist-info/RECORD +191 -0
  150. lionagi/operations/_act/__init__.py +0 -3
  151. lionagi/operations/_act/act.py +0 -87
  152. lionagi/protocols/messages/templates/README.md +0 -28
  153. lionagi/protocols/messages/templates/action_request.jinja2 +0 -5
  154. lionagi/protocols/messages/templates/action_response.jinja2 +0 -9
  155. lionagi/protocols/messages/templates/assistant_response.jinja2 +0 -6
  156. lionagi/protocols/messages/templates/instruction_message.jinja2 +0 -61
  157. lionagi/protocols/messages/templates/system_message.jinja2 +0 -11
  158. lionagi/protocols/messages/templates/tool_schemas.jinja2 +0 -7
  159. lionagi/service/connections/providers/types.py +0 -28
  160. lionagi/service/third_party/openai_model_names.py +0 -198
  161. lionagi/service/types.py +0 -59
  162. lionagi-0.17.10.dist-info/RECORD +0 -199
  163. {lionagi-0.17.10.dist-info → lionagi-0.18.0.dist-info}/WHEEL +0 -0
  164. {lionagi-0.17.10.dist-info → lionagi-0.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,27 +1,38 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
1
+ # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
3
2
  # SPDX-License-Identifier: Apache-2.0
4
3
 
5
4
  import logging
6
5
  from collections.abc import AsyncGenerator
7
- from typing import TYPE_CHECKING, Any, Literal
6
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar
8
7
 
9
8
  from pydantic import BaseModel
10
9
 
10
+ logger = logging.getLogger(__name__)
11
+
11
12
  from lionagi.fields.instruct import Instruct
12
13
  from lionagi.libs.schema.as_readable import as_readable
13
14
  from lionagi.libs.validate.common_field_validators import (
14
15
  validate_model_to_type,
15
16
  )
16
- from lionagi.models import FieldModel, ModelParams
17
+ from lionagi.ln.fuzzy import FuzzyMatchKeysParams
18
+ from lionagi.models.field_model import FieldModel
17
19
  from lionagi.service.imodel import iModel
18
- from lionagi.utils import copy
19
20
 
21
+ from ..types import (
22
+ ActionParam,
23
+ ChatParam,
24
+ HandleValidation,
25
+ InterpretParam,
26
+ ParseParam,
27
+ )
20
28
  from .utils import Analysis, ReActAnalysis
21
29
 
22
30
  if TYPE_CHECKING:
23
31
  from lionagi.session.branch import Branch
24
32
 
33
+ B = TypeVar("B", bound=type[BaseModel])
34
+ logger = logging.getLogger(__name__)
35
+
25
36
 
26
37
  async def ReAct(
27
38
  branch: "Branch",
@@ -30,7 +41,7 @@ async def ReAct(
30
41
  interpret_domain: str | None = None,
31
42
  interpret_style: str | None = None,
32
43
  interpret_sample: str | None = None,
33
- interpret_model: str | None = None,
44
+ interpret_model: iModel | None = None,
34
45
  interpret_kwargs: dict | None = None,
35
46
  tools: Any = None,
36
47
  tool_schemas: Any = None,
@@ -50,263 +61,469 @@ async def ReAct(
50
61
  continue_after_failed_response: bool = False,
51
62
  **kwargs,
52
63
  ):
64
+ """ReAct reasoning loop with legacy API - wrapper around ReAct_v1."""
65
+
66
+ # Handle legacy verbose parameter
67
+ if "verbose" in kwargs:
68
+ verbose_analysis = kwargs.pop("verbose")
69
+
70
+ # Convert Instruct to dict if needed
71
+ instruct_dict = (
72
+ instruct.to_dict()
73
+ if isinstance(instruct, Instruct)
74
+ else dict(instruct)
75
+ )
76
+
77
+ # Build InterpretParam if interpretation requested
78
+ intp_param = None
79
+ if interpret:
80
+ intp_param = InterpretParam(
81
+ domain=interpret_domain or "general",
82
+ style=interpret_style or "concise",
83
+ sample_writing=interpret_sample or "",
84
+ imodel=interpret_model or analysis_model or branch.chat_model,
85
+ imodel_kw=interpret_kwargs or {},
86
+ )
87
+
88
+ # Build ChatParam
89
+ chat_param = ChatParam(
90
+ guidance=instruct_dict.get("guidance"),
91
+ context=instruct_dict.get("context"),
92
+ sender=branch.user or "user",
93
+ recipient=branch.id,
94
+ response_format=None, # Will be set in operate calls
95
+ progression=None,
96
+ tool_schemas=tool_schemas or [],
97
+ images=[],
98
+ image_detail="auto",
99
+ plain_content="",
100
+ include_token_usage_to_model=include_token_usage_to_model,
101
+ imodel=analysis_model or branch.chat_model,
102
+ imodel_kw=kwargs,
103
+ )
104
+
105
+ # Build ActionParam
106
+ action_param = None
107
+ if tools is not None or tool_schemas is not None:
108
+ from ..act.act import _get_default_call_params
109
+
110
+ action_param = ActionParam(
111
+ action_call_params=_get_default_call_params(),
112
+ tools=tools or True,
113
+ strategy="concurrent",
114
+ suppress_errors=True,
115
+ verbose_action=False,
116
+ )
117
+
118
+ # Build ParseParam
119
+ from ..parse.parse import get_default_call
120
+
121
+ parse_param = ParseParam(
122
+ response_format=ReActAnalysis, # Initial format
123
+ fuzzy_match_params=FuzzyMatchKeysParams(),
124
+ handle_validation="return_value",
125
+ alcall_params=get_default_call(),
126
+ imodel=analysis_model or branch.chat_model,
127
+ imodel_kw={},
128
+ )
129
+
130
+ # Response context for final answer
131
+ resp_ctx = response_kwargs or {}
132
+ if response_format:
133
+ resp_ctx["response_format"] = response_format
134
+
135
+ return await ReAct_v1(
136
+ branch,
137
+ instruction=instruct_dict.get("instruction", str(instruct)),
138
+ chat_param=chat_param,
139
+ action_param=action_param,
140
+ parse_param=parse_param,
141
+ intp_param=intp_param,
142
+ resp_ctx=resp_ctx,
143
+ reasoning_effort=reasoning_effort,
144
+ reason=True, # ReAct always uses reasoning
145
+ field_models=None,
146
+ handle_validation="return_value",
147
+ invoke_actions=True, # ReAct always invokes actions
148
+ clear_messages=False,
149
+ intermediate_response_options=intermediate_response_options,
150
+ intermediate_listable=intermediate_listable,
151
+ intermediate_nullable=False,
152
+ max_extensions=max_extensions,
153
+ extension_allowed=extension_allowed,
154
+ verbose_analysis=verbose_analysis,
155
+ display_as=display_as,
156
+ verbose_length=verbose_length,
157
+ continue_after_failed_response=continue_after_failed_response,
158
+ return_analysis=return_analysis,
159
+ )
160
+
161
+
162
+ async def ReAct_v1(
163
+ branch: "Branch",
164
+ instruction: str,
165
+ chat_param: ChatParam,
166
+ action_param: ActionParam | None = None,
167
+ parse_param: ParseParam | None = None,
168
+ intp_param: InterpretParam | None = None,
169
+ resp_ctx: dict | None = None,
170
+ reasoning_effort: Literal["low", "medium", "high"] | None = None,
171
+ reason: bool = False,
172
+ field_models: list[FieldModel] | None = None,
173
+ handle_validation: HandleValidation = "raise",
174
+ invoke_actions: bool = True,
175
+ clear_messages=False,
176
+ intermediate_response_options: B | list[B] = None,
177
+ intermediate_listable: bool = False,
178
+ intermediate_nullable: bool = False,
179
+ max_extensions: int | None = 0,
180
+ extension_allowed: bool = True,
181
+ verbose_analysis: bool = False,
182
+ display_as: Literal["yaml", "json"] = "yaml",
183
+ verbose_length: int = None,
184
+ continue_after_failed_response: bool = False,
185
+ return_analysis: bool = False,
186
+ ):
187
+ """
188
+ Context-based ReAct implementation - collects all outputs from ReActStream.
189
+
190
+ Args:
191
+ return_analysis: If True, returns list of all intermediate analyses.
192
+ If False, returns only the final result.
193
+ """
53
194
  outs = []
195
+
54
196
  if verbose_analysis:
55
197
  async for i in ReActStream(
56
198
  branch=branch,
57
- instruct=instruct,
58
- interpret=interpret,
59
- interpret_domain=interpret_domain,
60
- interpret_style=interpret_style,
61
- interpret_sample=interpret_sample,
62
- interpret_model=interpret_model,
63
- interpret_kwargs=interpret_kwargs,
64
- tools=tools,
65
- tool_schemas=tool_schemas,
66
- response_format=response_format,
199
+ instruction=instruction,
200
+ chat_param=chat_param,
201
+ action_param=action_param,
202
+ parse_param=parse_param,
203
+ intp_param=intp_param,
204
+ resp_ctx=resp_ctx,
205
+ reasoning_effort=reasoning_effort,
206
+ reason=reason,
207
+ field_models=field_models,
208
+ handle_validation=handle_validation,
209
+ invoke_actions=invoke_actions,
210
+ clear_messages=clear_messages,
67
211
  intermediate_response_options=intermediate_response_options,
68
212
  intermediate_listable=intermediate_listable,
69
- reasoning_effort=reasoning_effort,
70
- extension_allowed=extension_allowed,
213
+ intermediate_nullable=intermediate_nullable,
71
214
  max_extensions=max_extensions,
72
- response_kwargs=response_kwargs,
73
- analysis_model=analysis_model,
215
+ extension_allowed=extension_allowed,
74
216
  verbose_analysis=verbose_analysis,
75
217
  display_as=display_as,
76
218
  verbose_length=verbose_length,
77
- include_token_usage_to_model=include_token_usage_to_model,
78
219
  continue_after_failed_response=continue_after_failed_response,
79
- **kwargs,
80
220
  ):
81
221
  analysis, str_ = i
82
- str_ += "\n---------\n"
83
- as_readable(str_, md=True, display_str=True)
222
+ # str_ is already formatted markdown - just print it
223
+ as_readable(
224
+ str_,
225
+ md=True,
226
+ display_str=True,
227
+ )
84
228
  outs.append(analysis)
85
229
  else:
86
230
  async for i in ReActStream(
87
231
  branch=branch,
88
- instruct=instruct,
89
- interpret=interpret,
90
- interpret_domain=interpret_domain,
91
- interpret_style=interpret_style,
92
- interpret_sample=interpret_sample,
93
- interpret_model=interpret_model,
94
- interpret_kwargs=interpret_kwargs,
95
- tools=tools,
96
- tool_schemas=tool_schemas,
97
- response_format=response_format,
232
+ instruction=instruction,
233
+ chat_param=chat_param,
234
+ action_param=action_param,
235
+ parse_param=parse_param,
236
+ intp_param=intp_param,
237
+ resp_ctx=resp_ctx,
238
+ reasoning_effort=reasoning_effort,
239
+ reason=reason,
240
+ field_models=field_models,
241
+ handle_validation=handle_validation,
242
+ invoke_actions=invoke_actions,
243
+ clear_messages=clear_messages,
98
244
  intermediate_response_options=intermediate_response_options,
99
245
  intermediate_listable=intermediate_listable,
100
- reasoning_effort=reasoning_effort,
101
- extension_allowed=extension_allowed,
246
+ intermediate_nullable=intermediate_nullable,
102
247
  max_extensions=max_extensions,
103
- response_kwargs=response_kwargs,
104
- analysis_model=analysis_model,
248
+ extension_allowed=extension_allowed,
105
249
  display_as=display_as,
106
250
  verbose_length=verbose_length,
107
- include_token_usage_to_model=include_token_usage_to_model,
108
251
  continue_after_failed_response=continue_after_failed_response,
109
- **kwargs,
110
252
  ):
111
253
  outs.append(i)
254
+
112
255
  if return_analysis:
113
256
  return outs
114
- return outs[-1]
257
+
258
+ # Extract answer from the final Analysis object
259
+ final_result = outs[-1]
260
+ if hasattr(final_result, "answer"):
261
+ return final_result.answer
262
+ return final_result
263
+
264
+
265
+ async def handle_instruction_interpretation(
266
+ branch: "Branch",
267
+ instruction: str,
268
+ chat_param: ChatParam,
269
+ intp_param: InterpretParam | None,
270
+ ):
271
+ """Handle instruction interpretation if requested."""
272
+ if not intp_param:
273
+ return instruction
274
+
275
+ from ..interpret.interpret import interpret
276
+
277
+ return await interpret(branch, instruction, intp_param)
278
+
279
+
280
+ def handle_field_models(
281
+ field_models: list[FieldModel] | None,
282
+ intermediate_response_options: B | list[B] = None,
283
+ intermediate_listable: bool = False,
284
+ intermediate_nullable: bool = False,
285
+ ):
286
+ """Build field models including intermediate response options."""
287
+ fms = [] if not field_models else field_models
288
+
289
+ if intermediate_response_options:
290
+
291
+ def create_intermediate_response_field_model():
292
+ from lionagi.models import OperableModel
293
+
294
+ _iro = intermediate_response_options
295
+ iro = [_iro] if not isinstance(_iro, list) else _iro
296
+ opm = OperableModel()
297
+
298
+ for i in iro:
299
+ type_ = validate_model_to_type(None, i)
300
+ opm.add_field(
301
+ str(type_.__name__).lower(),
302
+ annotation=type_ | None,
303
+ # Remove lambda validator to avoid Pydantic serialization errors
304
+ )
305
+
306
+ m_ = opm.new_model(name="IntermediateResponseOptions")
307
+ irfm = FieldModel(
308
+ name="intermediate_response_options",
309
+ base_type=m_,
310
+ description="Intermediate deliverable outputs. fill as needed ",
311
+ # Remove lambda validator to avoid Pydantic serialization errors
312
+ )
313
+
314
+ if intermediate_listable:
315
+ irfm = irfm.as_listable()
316
+
317
+ if intermediate_nullable:
318
+ irfm = irfm.as_nullable()
319
+
320
+ return irfm
321
+
322
+ fms = [fms] if not isinstance(fms, list) else fms
323
+ fms += [create_intermediate_response_field_model()]
324
+
325
+ return fms
115
326
 
116
327
 
117
328
  async def ReActStream(
118
329
  branch: "Branch",
119
- instruct: Instruct | dict[str, Any],
120
- interpret: bool = False,
121
- interpret_domain: str | None = None,
122
- interpret_style: str | None = None,
123
- interpret_sample: str | None = None,
124
- interpret_model: str | None = None,
125
- interpret_kwargs: dict | None = None,
126
- tools: Any = None,
127
- tool_schemas: Any = None,
128
- response_format: type[BaseModel] | BaseModel = None,
129
- intermediate_response_options: list[BaseModel] | BaseModel = None,
330
+ instruction: str,
331
+ chat_param: ChatParam,
332
+ action_param: ActionParam | None = None,
333
+ parse_param: ParseParam | None = None,
334
+ intp_param: InterpretParam | None = None,
335
+ resp_ctx: dict | None = None,
336
+ reasoning_effort: Literal["low", "medium", "high"] | None = None,
337
+ reason: bool = False,
338
+ field_models: list[FieldModel] | None = None,
339
+ handle_validation: HandleValidation = "raise",
340
+ invoke_actions: bool = True,
341
+ clear_messages=False,
342
+ intermediate_response_options: B | list[B] = None,
130
343
  intermediate_listable: bool = False,
131
- reasoning_effort: Literal["low", "medium", "high"] = None,
344
+ intermediate_nullable: bool = False,
345
+ max_extensions: int | None = 0,
132
346
  extension_allowed: bool = True,
133
- max_extensions: int | None = 3,
134
- response_kwargs: dict | None = None,
135
- analysis_model: iModel | None = None,
136
347
  verbose_analysis: bool = False,
137
- display_as: Literal["json", "yaml"] = "yaml",
348
+ display_as: Literal["yaml", "json"] = "yaml",
138
349
  verbose_length: int = None,
139
- include_token_usage_to_model: bool = True,
140
350
  continue_after_failed_response: bool = False,
141
- **kwargs,
142
351
  ) -> AsyncGenerator:
143
- irfm: FieldModel | None = None
352
+ """Core ReAct streaming implementation with context-based architecture."""
144
353
 
145
- if intermediate_response_options is not None:
146
- iro = (
147
- [intermediate_response_options]
148
- if not isinstance(intermediate_response_options, list)
149
- else intermediate_response_options
150
- )
151
- field_models = []
152
- for i in iro:
153
- type_ = validate_model_to_type(None, i)
154
- fm = FieldModel(
155
- name=str(type_.__name__).lower(),
156
- annotation=type_ | None,
157
- validator=lambda cls, x: None if x == {} else x,
158
- )
159
- field_models.append(fm)
160
-
161
- m_ = ModelParams(
162
- name="IntermediateResponseOptions", field_models=field_models
163
- ).create_new_model()
164
-
165
- irfm = FieldModel(
166
- name="intermediate_response_options",
167
- annotation=(
168
- m_ | None if not intermediate_listable else list[m_] | None
169
- ),
170
- description="Optional intermediate deliverable outputs. fill as needed ",
171
- validator=lambda cls, x: None if not x else x,
354
+ # Validate and clamp max_extensions
355
+ if max_extensions and max_extensions > 100:
356
+ logger.warning(
357
+ "max_extensions should not exceed 100; defaulting to 100."
172
358
  )
359
+ max_extensions = 100
173
360
 
174
- # If no tools or tool schemas are provided, default to "all tools"
175
- if not tools and not tool_schemas:
176
- tools = True
177
-
178
- # Possibly interpret the instruction to refine it
179
- instruction_str = None
180
- if interpret:
181
- instruction_str = await branch.interpret(
182
- str(
183
- instruct.to_dict()
184
- if isinstance(instruct, Instruct)
185
- else instruct
186
- ),
187
- domain=interpret_domain,
188
- style=interpret_style,
189
- sample_writing=interpret_sample,
190
- interpret_model=interpret_model,
191
- **(interpret_kwargs or {}),
192
- )
361
+ def verbose_yield(title, s_):
193
362
  if verbose_analysis:
194
- str_ = "\n### Interpreted instruction:\n"
363
+ str_ = title + "\n"
195
364
  str_ += as_readable(
196
- instruction_str,
365
+ s_,
197
366
  md=True,
198
367
  format_curly=True if display_as == "yaml" else False,
199
368
  max_chars=verbose_length,
200
369
  )
201
- yield instruction_str, str_
370
+ return s_, str_
202
371
  else:
203
- yield instruction_str
204
-
205
- # Convert Instruct to dict if necessary
206
- instruct_dict = (
207
- instruct.to_dict()
208
- if isinstance(instruct, Instruct)
209
- else dict(instruct)
372
+ return s_
373
+
374
+ # Step 1: Interpret instruction if requested
375
+ ins_str = await handle_instruction_interpretation(
376
+ branch,
377
+ instruction=instruction,
378
+ chat_param=chat_param,
379
+ intp_param=intp_param,
210
380
  )
211
-
212
- # Overwrite "instruction" with the interpreted prompt (if any) plus a note about expansions
213
- max_ext_info = f"\nIf needed, you can do up to {max_extensions or 0 if extension_allowed else 0} expansions."
214
- instruct_dict["instruction"] = (
215
- instruction_str
216
- or (instruct_dict.get("instruction") or "") # in case it's missing
217
- ) + max_ext_info
218
-
219
- # Prepare a copy of user-provided kwargs for the first operate call
220
- kwargs_for_operate = copy(kwargs)
221
- kwargs_for_operate["actions"] = True
222
- kwargs_for_operate["reason"] = True
223
- kwargs_for_operate["include_token_usage_to_model"] = (
224
- include_token_usage_to_model
225
- )
226
-
227
- # Step 1: Generate initial ReAct analysis
228
- analysis: ReActAnalysis = await branch.operate(
229
- instruct=instruct_dict,
230
- response_format=ReActAnalysis,
231
- tools=tools,
232
- tool_schemas=tool_schemas,
233
- chat_model=analysis_model or branch.chat_model,
234
- **kwargs_for_operate,
235
- )
236
- # If verbose, show round #1 analysis
237
- if verbose_analysis:
238
- str_ = "\n### ReAct Round No.1 Analysis:\n"
381
+ # Print interpreted instruction if verbose (don't yield it - not an analysis object)
382
+ if verbose_analysis and intp_param:
383
+ str_ = "\n### Interpreted instruction:\n"
239
384
  str_ += as_readable(
240
- analysis,
385
+ ins_str,
241
386
  md=True,
242
387
  format_curly=True if display_as == "yaml" else False,
243
388
  max_chars=verbose_length,
244
389
  )
245
- yield analysis, str_
246
- else:
247
- yield analysis
248
390
 
249
- # Validate and clamp max_extensions if needed
250
- if max_extensions and max_extensions > 100:
251
- logging.warning(
252
- "max_extensions should not exceed 100; defaulting to 100."
391
+ # Step 2: Handle field models
392
+ fms = handle_field_models(
393
+ field_models,
394
+ intermediate_response_options,
395
+ intermediate_listable,
396
+ intermediate_nullable,
397
+ )
398
+
399
+ # Step 3: Initial ReAct analysis
400
+ from ..operate.operate import operate
401
+
402
+ # Build context for initial analysis
403
+ initial_chat_param = chat_param.with_updates(response_format=ReActAnalysis)
404
+
405
+ initial_parse_param = (
406
+ parse_param.with_updates(response_format=ReActAnalysis)
407
+ if parse_param
408
+ else None
409
+ )
410
+
411
+ # Add proper extension prompt for initial analysis
412
+ initial_instruction = ins_str
413
+ if extension_allowed and max_extensions:
414
+ initial_instruction += "\n\n" + ReActAnalysis.FIRST_EXT_PROMPT.format(
415
+ extensions=max_extensions
253
416
  )
254
- max_extensions = 100
255
417
 
256
- # Step 2: Possibly loop through expansions if extension_needed
257
- extensions = max_extensions
418
+ analysis = await operate(
419
+ branch,
420
+ instruction=initial_instruction,
421
+ chat_param=initial_chat_param,
422
+ action_param=action_param,
423
+ parse_param=initial_parse_param,
424
+ handle_validation=handle_validation,
425
+ invoke_actions=invoke_actions,
426
+ skip_validation=False,
427
+ clear_messages=clear_messages,
428
+ reason=reason,
429
+ field_models=fms,
430
+ )
431
+
432
+ out = verbose_yield("\n### ReAct Round No.1 Analysis:\n", analysis)
433
+ yield out
434
+
435
+ # Step 4: Extension loop
436
+ extensions = max_extensions or 0
258
437
  round_count = 1
259
438
 
260
- while (
261
- extension_allowed and analysis.extension_needed
262
- if hasattr(analysis, "extension_needed")
263
- else (
264
- analysis.get("extension_needed", None)
265
- if isinstance(analysis, dict)
266
- else False
267
- )
268
- and (extensions - 1 if max_extensions else 0) > 0
269
- ):
439
+ def _need_extension(analysis):
440
+ if hasattr(analysis, "extension_needed"):
441
+ return analysis.extension_needed
442
+ if isinstance(analysis, dict):
443
+ return analysis.get("extension_needed", False)
444
+ return False
445
+
446
+ def _extension_allowed(exts):
447
+ return extension_allowed and exts > 0
448
+
449
+ def prepare_analysis_kwargs(exts):
270
450
  new_instruction = None
271
- if extensions == max_extensions:
451
+ if exts == max_extensions:
272
452
  new_instruction = ReActAnalysis.FIRST_EXT_PROMPT.format(
273
- extensions=extensions
453
+ extensions=exts
274
454
  )
275
455
  else:
276
456
  new_instruction = ReActAnalysis.CONTINUE_EXT_PROMPT.format(
277
- extensions=extensions
457
+ extensions=exts
278
458
  )
279
459
 
280
- operate_kwargs = copy(kwargs)
281
- operate_kwargs["actions"] = True
282
- operate_kwargs["reason"] = True
283
- operate_kwargs["response_format"] = ReActAnalysis
284
- operate_kwargs["action_strategy"] = analysis.action_strategy
285
- operate_kwargs["include_token_usage_to_model"] = (
286
- include_token_usage_to_model
287
- )
288
- if irfm:
289
- operate_kwargs["field_models"] = operate_kwargs.get(
290
- "field_models", []
291
- ) + [irfm]
460
+ # Use with_updates to create new context instances
461
+ updates = {"response_format": ReActAnalysis}
462
+
292
463
  if reasoning_effort:
293
- guide = None
294
- if reasoning_effort == "low":
295
- guide = "Quick concise reasoning.\n"
296
- if reasoning_effort == "medium":
297
- guide = "Reasonably balanced reasoning.\n"
298
- if reasoning_effort == "high":
299
- guide = "Thorough, try as hard as you can in reasoning.\n"
300
- operate_kwargs["guidance"] = guide + operate_kwargs.get(
301
- "guidance", ""
464
+ guide = {
465
+ "low": "Quick concise reasoning.\n",
466
+ "medium": "Reasonably balanced reasoning.\n",
467
+ "high": "Thorough, try as hard as you can in reasoning.\n",
468
+ }.get(reasoning_effort, "")
469
+
470
+ updates["guidance"] = (guide or "") + (chat_param.guidance or "")
471
+ updates["imodel_kw"] = {
472
+ **(chat_param.imodel_kw or {}),
473
+ "reasoning_effort": reasoning_effort,
474
+ }
475
+
476
+ _cctx = chat_param.with_updates(**updates)
477
+
478
+ # Import default call params if needed
479
+ from ..act.act import _get_default_call_params
480
+
481
+ _actx = (
482
+ action_param.with_updates(
483
+ strategy=getattr(analysis, "action_strategy", "concurrent")
484
+ )
485
+ if action_param
486
+ else ActionParam(
487
+ action_call_params=_get_default_call_params(),
488
+ tools=True,
489
+ strategy=getattr(analysis, "action_strategy", "concurrent"),
490
+ suppress_errors=True,
491
+ verbose_action=False,
302
492
  )
303
- operate_kwargs["reasoning_effort"] = reasoning_effort
493
+ )
304
494
 
305
- analysis = await branch.operate(
306
- instruction=new_instruction,
307
- tools=tools,
308
- tool_schemas=tool_schemas,
309
- **operate_kwargs,
495
+ return {
496
+ "instruction": new_instruction,
497
+ "chat_param": _cctx,
498
+ "action_param": _actx,
499
+ "reason": reason,
500
+ "field_models": fms,
501
+ }
502
+
503
+ while _extension_allowed(extensions) and _need_extension(analysis):
504
+ kwargs = prepare_analysis_kwargs(extensions)
505
+
506
+ # Build parse context for extension
507
+ ext_parse_param = (
508
+ parse_param.with_updates(
509
+ response_format=kwargs["chat_param"].response_format
510
+ )
511
+ if parse_param
512
+ else None
513
+ )
514
+
515
+ analysis = await operate(
516
+ branch,
517
+ instruction=kwargs["instruction"],
518
+ chat_param=kwargs["chat_param"],
519
+ action_param=kwargs.get("action_param"),
520
+ parse_param=ext_parse_param,
521
+ handle_validation=handle_validation,
522
+ invoke_actions=invoke_actions,
523
+ skip_validation=False,
524
+ clear_messages=False, # Keep messages to maintain context
525
+ reason=kwargs.get("reason", True),
526
+ field_models=kwargs.get("field_models"),
310
527
  )
311
528
  round_count += 1
312
529
 
@@ -320,40 +537,73 @@ async def ReActStream(
320
537
  "Set `continue_after_failed_response=True` to ignore this error."
321
538
  )
322
539
 
323
- # If verbose, show round analysis
324
- if verbose_analysis:
325
- str_ = f"\n### ReAct Round No.{round_count} Analysis:\n"
326
-
327
- str_ += as_readable(
328
- analysis,
329
- md=True,
330
- format_curly=True if display_as == "yaml" else False,
331
- max_chars=verbose_length,
332
- )
333
-
334
- yield analysis, str_
335
- else:
336
- yield analysis
540
+ out = verbose_yield(
541
+ f"\n### ReAct Round No.{round_count} Analysis:\n", analysis
542
+ )
543
+ yield out
337
544
 
338
545
  if extensions:
339
546
  extensions -= 1
340
547
 
341
- # Step 3: Produce final answer by calling branch.instruct with an answer prompt
342
- answer_prompt = ReActAnalysis.ANSWER_PROMPT.format(
343
- instruction=instruct_dict["instruction"]
548
+ # Step 5: Final answer
549
+ answer_prompt = ReActAnalysis.ANSWER_PROMPT.format(instruction=ins_str)
550
+
551
+ final_response_format = (
552
+ resp_ctx.get("response_format") if resp_ctx else None
553
+ )
554
+ if not final_response_format:
555
+ final_response_format = Analysis
556
+
557
+ # Build contexts for final answer
558
+ resp_ctx_updates = {"response_format": final_response_format}
559
+ if resp_ctx:
560
+ # Merge resp_ctx into updates (filter allowed keys)
561
+ for k, v in resp_ctx.items():
562
+ if k in chat_param.allowed() and k != "response_format":
563
+ resp_ctx_updates[k] = v
564
+
565
+ final_chat_param = chat_param.with_updates(**resp_ctx_updates)
566
+
567
+ final_parse_param = (
568
+ parse_param.with_updates(response_format=final_response_format)
569
+ if parse_param
570
+ else None
344
571
  )
345
- if not response_format:
346
- response_format = Analysis
572
+
573
+ # Build operate kwargs, honoring response_kwargs
574
+ operate_kwargs = {
575
+ "branch": branch,
576
+ "instruction": answer_prompt,
577
+ "chat_param": final_chat_param,
578
+ "action_param": None, # No actions in final answer
579
+ "parse_param": final_parse_param,
580
+ "invoke_actions": False,
581
+ "clear_messages": False,
582
+ "reason": False, # No reasoning wrapper in final answer
583
+ "field_models": None,
584
+ # Defaults that can be overridden by resp_ctx
585
+ "handle_validation": handle_validation,
586
+ "skip_validation": False,
587
+ }
588
+
589
+ # Honor response_kwargs for final answer generation
590
+ if resp_ctx:
591
+ # Extract operate specific parameters from resp_ctx
592
+ operate_params = {
593
+ "skip_validation",
594
+ "handle_validation",
595
+ "clear_messages",
596
+ "reason",
597
+ "field_models",
598
+ }
599
+ for param in operate_params:
600
+ if param in resp_ctx:
601
+ operate_kwargs[param] = resp_ctx[param]
347
602
 
348
603
  try:
349
- out = await branch.operate(
350
- instruction=answer_prompt,
351
- response_format=response_format,
352
- **(response_kwargs or {}),
353
- )
354
- if isinstance(analysis, dict) and all(
355
- i is None for i in analysis.values()
356
- ):
604
+ out = await operate(**operate_kwargs)
605
+
606
+ if isinstance(out, dict) and all(i is None for i in out.values()):
357
607
  if not continue_after_failed_response:
358
608
  raise ValueError(
359
609
  "All values in the response are None. "
@@ -363,20 +613,6 @@ async def ReActStream(
363
613
  except Exception:
364
614
  out = branch.msgs.last_response.response
365
615
 
366
- if isinstance(out, Analysis):
367
- out = out.answer
368
-
369
- if verbose_analysis:
370
- str_ = "\n### ReAct Final Answer:\n"
371
- str_ += as_readable(
372
- out,
373
- md=True,
374
- format_curly=True if display_as == "yaml" else False,
375
- max_chars=verbose_length,
376
- )
377
- yield out, str_
378
- else:
379
- yield out
380
-
381
-
382
- # TODO: Do partial intermeditate output for longer analysis with form and report
616
+ # Don't extract .answer - return the full Analysis object
617
+ _o = verbose_yield("\n### ReAct Final Answer:\n", out)
618
+ yield _o