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,21 +1,35 @@
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
 
4
+ import contextlib
5
+ import warnings
5
6
  from typing import TYPE_CHECKING, Any, Literal
6
7
 
7
8
  from pydantic import BaseModel
8
9
 
10
+ from lionagi.ln import (
11
+ extract_json,
12
+ fuzzy_validate_mapping,
13
+ get_cancelled_exc_class,
14
+ to_list,
15
+ )
16
+ from lionagi.ln.fuzzy import FuzzyMatchKeysParams
17
+ from lionagi.protocols.types import AssistantResponse
18
+ from lionagi.session.branch import AlcallParams
19
+
20
+ from ..types import HandleValidation, ParseParam
21
+
9
22
  if TYPE_CHECKING:
10
23
  from lionagi.session.branch import Branch
11
24
 
12
25
 
13
- async def parse(
26
+ _CALL = None # type: ignore
27
+
28
+
29
+ def prepare_parse_kws(
14
30
  branch: "Branch",
15
31
  text: str,
16
- handle_validation: Literal[
17
- "raise", "return_value", "return_none"
18
- ] = "return_value",
32
+ handle_validation: HandleValidation = "return_value",
19
33
  max_retries: int = 3,
20
34
  request_type: type[BaseModel] = None,
21
35
  operative=None,
@@ -30,159 +44,172 @@ async def parse(
30
44
  strict: bool = False,
31
45
  suppress_conversion_errors: bool = False,
32
46
  response_format=None,
47
+ request_fields=None,
48
+ return_res_message: bool = False,
49
+ **kw,
33
50
  ):
34
- from lionagi.libs.schema.breakdown_pydantic_annotation import (
35
- breakdown_pydantic_annotation,
36
- )
37
- from lionagi.ln.fuzzy._fuzzy_validate import fuzzy_validate_mapping
38
-
39
- if operative is not None:
40
- max_retries = operative.max_retries
41
- response_format = operative.request_type or response_format
42
- request_type = request_type or operative.request_type
43
51
 
44
- if not request_type and not response_format:
45
- raise ValueError(
46
- "Either request_type or response_format must be provided"
52
+ if suppress_conversion_errors:
53
+ warnings.warn(
54
+ "Parameter 'suppress_conversion_errors' is deprecated and no longer used. "
55
+ "It will be removed in a future version.",
56
+ DeprecationWarning,
57
+ stacklevel=2,
47
58
  )
48
59
 
49
- request_type = request_type or response_format
60
+ response_format = (
61
+ operative.request_type
62
+ if operative
63
+ else response_format or request_type
64
+ )
65
+ _alcall_params = get_default_call()
66
+ max_retries = operative.max_retries if operative else max_retries or 3
67
+
68
+ fuzzy_params = FuzzyMatchKeysParams(
69
+ similarity_algo=similarity_algo,
70
+ similarity_threshold=similarity_threshold,
71
+ handle_unmatched=handle_unmatched,
72
+ fill_value=fill_value,
73
+ fill_mapping=fill_mapping,
74
+ strict=strict,
75
+ fuzzy_match=fuzzy_match,
76
+ )
50
77
 
51
- # First attempt: try to parse the text directly
52
- import logging
78
+ return {
79
+ "text": text,
80
+ "parse_param": ParseParam(
81
+ response_format=response_format or request_fields,
82
+ fuzzy_match_params=fuzzy_params,
83
+ handle_validation=handle_validation,
84
+ alcall_params=_alcall_params.with_updates(
85
+ retry_attempts=max_retries
86
+ ),
87
+ imodel=branch.parse_model,
88
+ imodel_kw=kw,
89
+ ),
90
+ "return_res_message": return_res_message,
91
+ }
53
92
 
54
- initial_error = None
55
- parsed_data = None # Initialize to avoid scoping issues
56
93
 
57
- try:
58
- # Try fuzzy validation first
59
- parsed_data = fuzzy_validate_mapping(
60
- text,
61
- breakdown_pydantic_annotation(request_type),
62
- similarity_algo=similarity_algo,
63
- similarity_threshold=similarity_threshold,
64
- fuzzy_match=fuzzy_match,
65
- handle_unmatched=handle_unmatched,
66
- fill_value=fill_value,
67
- fill_mapping=fill_mapping,
68
- strict=strict,
69
- suppress_conversion_errors=False, # Don't suppress on first attempt
94
+ async def parse(
95
+ branch: "Branch",
96
+ text: str,
97
+ parse_param: ParseParam,
98
+ return_res_message: bool = False,
99
+ ) -> Any | tuple[Any, AssistantResponse | None]:
100
+
101
+ # Try direct validation first
102
+ with contextlib.suppress(Exception):
103
+ result = _validate_dict_or_model(
104
+ text, parse_param.response_format, parse_param.fuzzy_match_params
105
+ )
106
+ return result if not return_res_message else (result, None)
107
+
108
+ async def _inner_parse(i):
109
+ _, res = await branch.chat(
110
+ instruction="reformat text into specified model or structure",
111
+ guidance="follow the required response format, using the model schema as a guide",
112
+ context=[{"text_to_format": text}],
113
+ request_fields=(
114
+ parse_param.response_format
115
+ if isinstance(parse_param.response_format, dict)
116
+ else None
117
+ ),
118
+ response_format=(
119
+ parse_param.response_format
120
+ if isinstance(parse_param.response_format, BaseModel)
121
+ else None
122
+ ),
123
+ imodel=parse_param.imodel or branch.parse_model,
124
+ sender=branch.user,
125
+ recipient=branch.id,
126
+ return_ins_res_message=True,
70
127
  )
71
128
 
72
- logging.debug(f"Parsed data from fuzzy validation: {parsed_data}")
129
+ res.metadata["is_parsed"] = True
130
+ res.metadata["original_text"] = text
73
131
 
74
- # Validate with pydantic
75
- if operative is not None:
76
- response_model = operative.update_response_model(parsed_data)
77
- else:
78
- response_model = request_type.model_validate(parsed_data)
132
+ return (
133
+ _validate_dict_or_model(
134
+ res.response,
135
+ parse_param.response_format,
136
+ parse_param.fuzzy_match_params,
137
+ ),
138
+ res,
139
+ )
79
140
 
80
- # If successful, return immediately
81
- if isinstance(response_model, BaseModel):
82
- return response_model
141
+ _call = parse_param.alcall_params or get_default_call()
142
+ if isinstance(parse_param.alcall_params, dict):
143
+ _call = AlcallParams(**parse_param.alcall_params)
83
144
 
145
+ try:
146
+ result = await _call([0], _inner_parse)
147
+ except get_cancelled_exc_class():
148
+ raise
84
149
  except Exception as e:
85
- initial_error = e
86
- # Log the initial parsing error for debugging
87
- logging.debug(
88
- f"Initial parsing failed for text '{text[:100]}...': {e}"
89
- )
90
- logging.debug(
91
- f"Parsed data was: {locals().get('parsed_data', 'not set')}"
92
- )
93
-
94
- # Only continue if we have retries left
95
- if max_retries <= 0:
96
- if handle_validation == "raise":
150
+ match parse_param.handle_validation:
151
+ case "raise":
97
152
  raise ValueError(f"Failed to parse response: {e}") from e
98
- elif handle_validation == "return_none":
99
- return None
100
- else: # return_value
101
- return text
102
-
103
- # If direct parsing failed, try using the parse model
104
- num_try = 0
105
- last_error = initial_error
106
-
107
- # Check if the parsed_data exists but just failed validation
108
- # This might mean we have the right structure but wrong values
109
- if parsed_data is not None and isinstance(parsed_data, dict):
110
- logging.debug(
111
- f"Have parsed_data dict, checking if it's close to valid..."
112
- )
113
- # If we got a dict with the right keys, maybe we just need to clean it up
114
- expected_fields = set(request_type.model_fields.keys())
115
- parsed_fields = set(parsed_data.keys())
116
- if expected_fields == parsed_fields and all(
117
- parsed_data.get(k) is not None for k in expected_fields
118
- ):
119
- # We have the right structure with non-None values, don't retry with parse model
120
- logging.debug(
121
- "Structure matches with valid values, returning original error"
153
+ case "return_none":
154
+ return (None, None) if return_res_message else None
155
+ case "return_value":
156
+ return (text, None) if return_res_message else text
157
+ return (*result[0],) if return_res_message else result[0][0]
158
+
159
+
160
+ def _validate_dict_or_model(
161
+ text: str,
162
+ response_format: type[BaseModel] | dict,
163
+ fuzzy_match_params: FuzzyMatchKeysParams | dict = None,
164
+ ):
165
+ try:
166
+ if isinstance(fuzzy_match_params, dict):
167
+ fuzzy_match_params = FuzzyMatchKeysParams(**fuzzy_match_params)
168
+
169
+ d_ = extract_json(text, fuzzy_parse=True, return_one_if_single=False)
170
+ dict_, keys_ = None, None
171
+ if d_:
172
+ dict_ = to_list(d_, flatten=True)[0]
173
+ if isinstance(fuzzy_match_params, FuzzyMatchKeysParams):
174
+ keys_ = (
175
+ response_format.model_fields
176
+ if isinstance(response_format, type)
177
+ else response_format
122
178
  )
123
- if handle_validation == "raise":
124
- raise ValueError(
125
- f"Failed to parse response: {initial_error}"
126
- ) from initial_error
127
- elif handle_validation == "return_none":
128
- return None
129
- else:
130
- return text
131
-
132
- while num_try < max_retries:
133
- num_try += 1
134
-
135
- try:
136
- logging.debug(f"Retry {num_try}: Using parse model to reformat")
137
- _, res = await branch.chat(
138
- instruction="reformat text into specified model",
139
- guidance="follow the required response format, using the model schema as a guide",
140
- context=[{"text_to_format": text}],
141
- response_format=request_type,
142
- sender=branch.user,
143
- recipient=branch.id,
144
- imodel=branch.parse_model,
145
- return_ins_res_message=True,
179
+ dict_ = fuzzy_validate_mapping(
180
+ dict_, keys_, **fuzzy_match_params.to_dict()
146
181
  )
147
-
148
- # Try to parse the reformatted response
149
- parsed_data = fuzzy_validate_mapping(
150
- res.response,
151
- breakdown_pydantic_annotation(request_type),
152
- similarity_algo=similarity_algo,
153
- similarity_threshold=similarity_threshold,
154
- fuzzy_match=fuzzy_match,
155
- handle_unmatched=handle_unmatched,
156
- fill_value=fill_value,
157
- fill_mapping=fill_mapping,
158
- strict=strict,
159
- suppress_conversion_errors=suppress_conversion_errors,
182
+ elif fuzzy_match_params:
183
+ keys_ = (
184
+ response_format.model_fields
185
+ if isinstance(response_format, type)
186
+ else response_format
160
187
  )
188
+ dict_ = fuzzy_validate_mapping(
189
+ dict_,
190
+ keys_,
191
+ handle_unmatched="force",
192
+ fill_value=None,
193
+ strict=False,
194
+ )
195
+ if isinstance(response_format, type) and issubclass(
196
+ response_format, BaseModel
197
+ ):
198
+ return response_format.model_validate(dict_)
199
+ return dict_
161
200
 
162
- if operative is not None:
163
- response_model = operative.update_response_model(parsed_data)
164
- else:
165
- response_model = request_type.model_validate(parsed_data)
166
-
167
- # If successful, return
168
- if isinstance(response_model, BaseModel):
169
- return response_model
170
-
171
- except InterruptedError as e:
172
- raise e
173
- except Exception as e:
174
- last_error = e
175
- # Continue to next retry
176
- continue
177
-
178
- # All retries exhausted
179
- match handle_validation:
180
- case "return_value":
181
- return text
182
- case "return_none":
183
- return None
184
- case "raise":
185
- error_msg = "Failed to parse response into request format"
186
- if last_error:
187
- error_msg += f": {last_error}"
188
- raise ValueError(error_msg) from last_error
201
+ except Exception as e:
202
+ raise ValueError(f"Failed to parse text: {e}") from e
203
+
204
+
205
+ def get_default_call() -> AlcallParams:
206
+ global _CALL
207
+ if _CALL is None:
208
+ _CALL = AlcallParams(
209
+ retry_initial_delay=1,
210
+ retry_backoff=1.85,
211
+ retry_attempts=3,
212
+ max_concurrent=1,
213
+ throttle_period=1,
214
+ )
215
+ return _CALL
@@ -1,5 +1,4 @@
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
  from .plan import plan
@@ -1,5 +1,4 @@
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
  from typing import Any, Literal
@@ -1,5 +1,4 @@
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
  PLAN_PROMPT = """
@@ -1,3 +1,2 @@
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
@@ -1,5 +1,4 @@
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
  from enum import Enum
@@ -9,7 +8,7 @@ from pydantic import BaseModel
9
8
 
10
9
  from lionagi.fields.instruct import Instruct
11
10
 
12
- from .utils import SelectionModel
11
+ from .utils import SelectionModel, parse_selection, parse_to_representation
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from lionagi.session.branch import Branch
@@ -25,42 +24,104 @@ async def select(
25
24
  verbose: bool = False,
26
25
  **kwargs: Any,
27
26
  ) -> SelectionModel | tuple[SelectionModel, "Branch"]:
27
+ """
28
+ Select from choices using LLM - legacy wrapper with backwards compatibility.
29
+
30
+ Args:
31
+ branch: Branch instance to use
32
+ instruct: Instruction for selection
33
+ choices: Available choices (list, dict, or Enum)
34
+ max_num_selections: Max number of selections
35
+ branch_kwargs: Kwargs for branch creation (deprecated)
36
+ return_branch: Return (result, branch) tuple
37
+ verbose: Print progress
38
+ **kwargs: Additional operate kwargs
39
+
40
+ Returns:
41
+ SelectionModel or (SelectionModel, Branch) tuple
42
+ """
28
43
  if verbose:
29
44
  print(f"Starting selection with up to {max_num_selections} choices.")
30
45
 
31
- from .utils import SelectionModel, parse_selection, parse_to_representation
46
+ # Handle branch creation for backwards compatibility
47
+ if branch is None and branch_kwargs:
48
+ from lionagi.session.branch import Branch
32
49
 
33
- branch = branch or Branch(**(branch_kwargs or {}))
50
+ branch = Branch(**branch_kwargs)
51
+
52
+ result = await select_v1(
53
+ branch=branch,
54
+ instruct=instruct,
55
+ choices=choices,
56
+ max_num_selections=max_num_selections,
57
+ verbose=verbose,
58
+ **kwargs,
59
+ )
60
+
61
+ if return_branch:
62
+ return result, branch
63
+ return result
64
+
65
+
66
+ async def select_v1(
67
+ branch: "Branch",
68
+ instruct: Instruct | dict[str, Any],
69
+ choices: list[str] | type[Enum] | dict[str, Any],
70
+ max_num_selections: int = 1,
71
+ verbose: bool = False,
72
+ **operate_kwargs: Any,
73
+ ) -> SelectionModel:
74
+ """
75
+ Context-based selection implementation.
76
+
77
+ Args:
78
+ branch: Branch instance
79
+ instruct: Selection instruction
80
+ choices: Available choices
81
+ max_num_selections: Maximum selections allowed
82
+ verbose: Print progress
83
+ **operate_kwargs: Additional operate parameters
84
+
85
+ Returns:
86
+ SelectionModel with corrected selections
87
+ """
88
+ # Parse choices into keys and representations
34
89
  selections, contents = parse_to_representation(choices)
35
90
  prompt = SelectionModel.PROMPT.format(
36
91
  max_num_selections=max_num_selections, choices=selections
37
92
  )
38
93
 
94
+ # Build instruction dictionary
39
95
  if isinstance(instruct, Instruct):
40
- instruct = instruct.to_dict()
41
-
42
- instruct = instruct or {}
96
+ instruct_dict = instruct.to_dict()
97
+ else:
98
+ instruct_dict = instruct or {}
43
99
 
44
- if instruct.get("instruction", None) is not None:
45
- instruct["instruction"] = (
46
- f"{instruct['instruction']}\n\n{prompt} \n\n "
100
+ # Append selection prompt to instruction
101
+ if instruct_dict.get("instruction", None) is not None:
102
+ instruct_dict["instruction"] = (
103
+ f"{instruct_dict['instruction']}\n\n{prompt} \n\n "
47
104
  )
48
105
  else:
49
- instruct["instruction"] = prompt
106
+ instruct_dict["instruction"] = prompt
50
107
 
51
- context = instruct.get("context", None) or []
108
+ # Add choice representations to context
109
+ context = instruct_dict.get("context", None) or []
52
110
  context = [context] if not isinstance(context, list) else context
53
111
  context.extend([{k: v} for k, v in zip(selections, contents)])
54
- instruct["context"] = context
112
+ instruct_dict["context"] = context
55
113
 
114
+ # Call branch.operate with SelectionModel as response format
56
115
  response_model: SelectionModel = await branch.operate(
57
116
  response_format=SelectionModel,
58
- **kwargs,
59
- **instruct,
117
+ **operate_kwargs,
118
+ **instruct_dict,
60
119
  )
120
+
61
121
  if verbose:
62
122
  print(f"Received selection: {response_model.selected}")
63
123
 
124
+ # Extract and normalize selected values
64
125
  selected = response_model
65
126
  if isinstance(response_model, BaseModel) and hasattr(
66
127
  response_model, "selected"
@@ -68,14 +129,13 @@ async def select(
68
129
  selected = response_model.selected
69
130
  selected = [selected] if not isinstance(selected, list) else selected
70
131
 
132
+ # Parse selections back to original choice values
71
133
  corrected_selections = [parse_selection(i, choices) for i in selected]
72
134
 
135
+ # Update response model with corrected selections
73
136
  if isinstance(response_model, BaseModel):
74
137
  response_model.selected = corrected_selections
75
-
76
138
  elif isinstance(response_model, dict):
77
139
  response_model["selected"] = corrected_selections
78
140
 
79
- if return_branch:
80
- return response_model, branch
81
141
  return response_model
@@ -1,5 +1,4 @@
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 inspect
@@ -8,7 +7,7 @@ from typing import Any, ClassVar
8
7
 
9
8
  from pydantic import BaseModel, Field, JsonValue
10
9
 
11
- from lionagi.libs.validate.string_similarity import string_similarity
10
+ from lionagi.ln.fuzzy import string_similarity
12
11
  from lionagi.utils import is_same_dtype
13
12
 
14
13