lionagi 0.5.3__py3-none-any.whl → 0.5.4__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.
@@ -128,9 +128,9 @@ class Session(Component):
128
128
  branch: The branch to set as default or its identifier.
129
129
  """
130
130
  branch = self.branches[branch]
131
- if branch and len(branch) == 1:
132
- self.default_branch = branch
133
- raise ValueError("Session can only have one default branch.")
131
+ if not isinstance(branch, Branch):
132
+ raise ValueError("Input value for branch is not a valid branch.")
133
+ self.default_branch = branch
134
134
 
135
135
  def to_df(self, branches: ID.RefSeq = None) -> pd.DataFrame:
136
136
  out = self.concat_messages(branches=branches)
@@ -148,6 +148,13 @@ async def alcall(
148
148
  ucall(func, i, **kwargs), retry_timeout
149
149
  )
150
150
  return index, result
151
+
152
+ except InterruptedError:
153
+ return index, None
154
+
155
+ except asyncio.CancelledError:
156
+ return index, None
157
+
151
158
  except TimeoutError as e:
152
159
  raise TimeoutError(
153
160
  f"{error_msg or ''} Timeout {retry_timeout} seconds "
@@ -2,6 +2,8 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ import logging
6
+ from typing import Literal
5
7
 
6
8
  from lionagi.core.session.branch import Branch
7
9
  from lionagi.core.session.session import Session
@@ -17,13 +19,41 @@ from lionagi.protocols.operatives.instruct import (
17
19
  from ..utils import prepare_instruct, prepare_session
18
20
  from .prompt import PROMPT
19
21
 
22
+ # ---------------------------------------------------------------------
23
+ # Data Models & Utilities
24
+ # ---------------------------------------------------------------------
25
+
20
26
 
21
27
  class BrainstormOperation(BaseModel):
28
+ """
29
+ Container for the outcomes of a brainstorming session:
30
+ 1. initial: the initial result of the 'brainstorm' prompt
31
+ 2. brainstorm: the results of auto-run instructions (if auto_run = True)
32
+ 3. explore: the results of exploring those instructions (if auto_explore = True)
33
+ """
34
+
22
35
  initial: Any
23
36
  brainstorm: list[Instruct] | None = None
24
37
  explore: list[InstructResponse] | None = None
25
38
 
26
39
 
40
+ def chunked(iterable, n):
41
+ """
42
+ Yield successive n-sized chunks from an iterable.
43
+
44
+ Example:
45
+ >>> list(chunked([1,2,3,4,5], 2))
46
+ [[1,2],[3,4],[5]]
47
+ """
48
+ for i in range(0, len(iterable), n):
49
+ yield iterable[i : i + n]
50
+
51
+
52
+ # ---------------------------------------------------------------------
53
+ # Core Instruction Execution
54
+ # ---------------------------------------------------------------------
55
+
56
+
27
57
  async def run_instruct(
28
58
  ins: Instruct,
29
59
  session: Session,
@@ -32,53 +62,50 @@ async def run_instruct(
32
62
  verbose: bool = True,
33
63
  **kwargs: Any,
34
64
  ) -> Any:
35
- """Execute an instruction within a brainstorming session.
36
-
37
- Args:
38
- ins: The instruction model to run.
39
- session: The current session.
40
- branch: The branch to operate on.
41
- auto_run: Whether to automatically run nested instructions.
42
- verbose: Whether to enable verbose output.
43
- **kwargs: Additional keyword arguments.
44
-
45
- Returns:
46
- The result of the instruction execution.
65
+ """
66
+ Execute a single instruction within a brainstorming session.
67
+ Optionally auto-run any child instructions that result.
47
68
  """
48
69
 
49
- async def run(ins_):
70
+ async def _run_child_instruction(child_ins: Instruct):
71
+ """
72
+ Helper for recursively running child instructions.
73
+ """
50
74
  if verbose:
51
- msg_ = (
52
- ins_.guidance[:100] + "..."
53
- if len(ins_.guidance) > 100
54
- else ins_.guidance
75
+ snippet = (
76
+ child_ins.guidance[:100] + "..."
77
+ if len(child_ins.guidance) > 100
78
+ else child_ins.guidance
55
79
  )
56
- print(f"\n-----Running instruction-----\n{msg_}")
57
- b_ = session.split(branch)
80
+ print(f"\n-----Running instruction-----\n{snippet}")
81
+ child_branch = session.split(branch)
58
82
  return await run_instruct(
59
- ins_, session, b_, False, verbose=verbose, **kwargs
83
+ child_ins, session, child_branch, False, verbose=verbose, **kwargs
60
84
  )
61
85
 
86
+ # Prepare config for the branch operation
62
87
  config = {**ins.model_dump(), **kwargs}
63
- res = await branch.operate(**config)
88
+ result = await branch.operate(**config)
64
89
  branch.msgs.logger.dump()
65
- instructs = []
66
-
67
- if hasattr(res, "instruct_models"):
68
- instructs = res.instruct_models
69
90
 
70
- if auto_run is True and instructs:
71
- ress = await alcall(instructs, run)
72
- response_ = []
73
- for res in ress:
74
- if isinstance(res, list):
75
- response_.extend(res)
91
+ # Extract any newly generated instructions
92
+ instructs = []
93
+ if hasattr(result, "instruct_models"):
94
+ instructs = result.instruct_models
95
+
96
+ # If we're allowed to auto-run child instructions, handle them
97
+ if auto_run and instructs:
98
+ child_results = await alcall(instructs, _run_child_instruction)
99
+ combined = []
100
+ for c in child_results:
101
+ if isinstance(c, list):
102
+ combined.extend(c)
76
103
  else:
77
- response_.append(res)
78
- response_.insert(0, res)
79
- return response_
104
+ combined.append(c)
105
+ combined.insert(0, result)
106
+ return combined
80
107
 
81
- return res
108
+ return result
82
109
 
83
110
 
84
111
  async def brainstorm(
@@ -89,116 +116,314 @@ async def brainstorm(
89
116
  auto_run: bool = True,
90
117
  auto_explore: bool = False,
91
118
  explore_kwargs: dict[str, Any] | None = None,
119
+ explore_strategy: Literal[
120
+ "concurrent",
121
+ "sequential",
122
+ "sequential_concurrent_chunk",
123
+ "concurrent_sequential_chunk",
124
+ ] = "concurrent",
92
125
  branch_kwargs: dict[str, Any] | None = None,
93
126
  return_session: bool = False,
94
127
  verbose: bool = False,
128
+ branch_as_default: bool = True,
95
129
  **kwargs: Any,
96
130
  ) -> Any:
97
- """Perform a brainstorming session.
98
-
99
- Args:
100
- instruct: Instruction model or dictionary.
101
- num_instruct: Number of instructions to generate.
102
- session: Existing session or None to create a new one.
103
- branch: Existing branch or reference.
104
- auto_run: If True, automatically run generated instructions.
105
- branch_kwargs: Additional arguments for branch creation.
106
- return_session: If True, return the session with results.
107
- verbose: Whether to enable verbose output.
108
- **kwargs: Additional keyword arguments.
109
-
110
- Returns:
111
- The results of the brainstorming session, optionally with the session.
112
131
  """
132
+ High-level function to perform a brainstorming session.
113
133
 
134
+ Steps:
135
+ 1. Run the initial 'instruct' prompt to generate suggestions.
136
+ 2. Optionally auto-run those suggestions (auto_run=True).
137
+ 3. Optionally explore the resulting instructions (auto_explore=True)
138
+ using the chosen strategy (concurrent, sequential, etc.).
139
+ """
140
+
141
+ # -----------------------------------------------------------------
142
+ # Basic Validations and Setup
143
+ # -----------------------------------------------------------------
114
144
  if auto_explore and not auto_run:
115
145
  raise ValueError("auto_explore requires auto_run to be True.")
116
146
 
117
147
  if verbose:
118
- print(f"Starting brainstorming...")
148
+ print("Starting brainstorming...")
119
149
 
150
+ # Make sure the correct field model is present
120
151
  field_models: list = kwargs.get("field_models", [])
121
152
  if INSTRUCT_FIELD_MODEL not in field_models:
122
153
  field_models.append(INSTRUCT_FIELD_MODEL)
123
-
124
154
  kwargs["field_models"] = field_models
155
+
156
+ # Prepare session, branch, and the instruction
125
157
  session, branch = prepare_session(session, branch, branch_kwargs)
126
- instruct = prepare_instruct(
127
- instruct, PROMPT.format(num_instruct=num_instruct)
128
- )
158
+ prompt_str = PROMPT.format(num_instruct=num_instruct)
159
+ instruct = prepare_instruct(instruct, prompt_str)
160
+
161
+ # -----------------------------------------------------------------
162
+ # 1. Initial Brainstorm
163
+ # -----------------------------------------------------------------
129
164
  res1 = await branch.operate(**instruct, **kwargs)
130
165
  out = BrainstormOperation(initial=res1)
131
166
 
132
167
  if verbose:
133
168
  print("Initial brainstorming complete.")
134
169
 
135
- instructs = None
136
-
137
- async def run(ins_):
170
+ # Helper to run single instructions from the 'brainstorm'
171
+ async def run_brainstorm_instruction(ins_):
138
172
  if verbose:
139
- msg_ = (
173
+ snippet = (
140
174
  ins_.guidance[:100] + "..."
141
175
  if len(ins_.guidance) > 100
142
176
  else ins_.guidance
143
177
  )
144
- print(f"\n-----Running instruction-----\n{msg_}")
145
- b_ = session.split(branch)
178
+ print(f"\n-----Running instruction-----\n{snippet}")
179
+ new_branch = session.split(branch)
146
180
  return await run_instruct(
147
- ins_, session, b_, auto_run, verbose=verbose, **kwargs
181
+ ins_, session, new_branch, auto_run, verbose=verbose, **kwargs
148
182
  )
149
183
 
184
+ # -----------------------------------------------------------------
185
+ # 2. Auto-run child instructions if requested
186
+ # -----------------------------------------------------------------
150
187
  if not auto_run:
151
188
  if return_session:
152
189
  return out, session
153
190
  return out
154
191
 
192
+ # We run inside the context manager for branching
155
193
  async with session.branches:
156
194
  response_ = []
195
+
196
+ # If the initial result has instructions, run them
157
197
  if hasattr(res1, "instruct_models"):
158
198
  instructs: list[Instruct] = res1.instruct_models
159
- ress = await alcall(instructs, run)
160
- ress = to_flat_list(ress, dropna=True)
199
+ brainstorm_results = await alcall(
200
+ instructs, run_brainstorm_instruction
201
+ )
202
+ brainstorm_results = to_flat_list(brainstorm_results, dropna=True)
161
203
 
162
- response_ = [
163
- res if not isinstance(res, str | dict) else None
164
- for res in ress
204
+ # Filter out plain str/dict responses, keep model-based
205
+ filtered = [
206
+ r if not isinstance(r, (str, dict)) else None
207
+ for r in brainstorm_results
165
208
  ]
166
- response_ = to_flat_list(response_, unique=True, dropna=True)
209
+ filtered = to_flat_list(filtered, unique=True, dropna=True)
210
+
167
211
  out.brainstorm = (
168
- response_ if isinstance(response_, list) else [response_]
212
+ filtered if isinstance(filtered, list) else [filtered]
169
213
  )
170
- response_.insert(0, res1)
214
+ # Insert the initial result at index 0 for reference
215
+ filtered.insert(0, res1)
216
+ response_ = filtered
171
217
 
218
+ # -----------------------------------------------------------------
219
+ # 3. Explore the results (if auto_explore = True)
220
+ # -----------------------------------------------------------------
172
221
  if response_ and auto_explore:
173
-
174
- async def explore(ins_: Instruct):
175
- if verbose:
176
- msg_ = (
177
- ins_.guidance[:100] + "..."
178
- if len(ins_.guidance) > 100
179
- else ins_.guidance
180
- )
181
- print(f"\n-----Exploring Idea-----\n{msg_}")
182
- b_ = session.split(branch)
183
- res = await b_.instruct(ins_, **(explore_kwargs or {}))
184
- return InstructResponse(
185
- instruct=ins_,
186
- response=res,
187
- )
188
-
189
- response_ = to_flat_list(
222
+ # Gather all newly generated instructions
223
+ all_explore_instructs = to_flat_list(
190
224
  [
191
- i.instruct_models
192
- for i in response_
193
- if hasattr(i, "instruct_models")
225
+ r.instruct_models
226
+ for r in response_
227
+ if hasattr(r, "instruct_models")
194
228
  ],
195
229
  dropna=True,
196
230
  unique=True,
197
231
  )
198
- res_explore = await alcall(response_, explore)
199
- out.explore = res_explore
200
232
 
233
+ # Decide how to explore based on the strategy
234
+ match explore_strategy:
235
+ # ---------------------------------------------------------
236
+ # Strategy A: CONCURRENT
237
+ # ---------------------------------------------------------
238
+ case "concurrent":
239
+
240
+ async def explore_concurrently(ins_: Instruct):
241
+ if verbose:
242
+ snippet = (
243
+ ins_.guidance[:100] + "..."
244
+ if len(ins_.guidance) > 100
245
+ else ins_.guidance
246
+ )
247
+ print(f"\n-----Exploring Idea-----\n{snippet}")
248
+ new_branch = session.split(branch)
249
+ resp = await new_branch.instruct(
250
+ ins_, **(explore_kwargs or {})
251
+ )
252
+ return InstructResponse(instruct=ins_, response=resp)
253
+
254
+ res_explore = await alcall(
255
+ all_explore_instructs, explore_concurrently
256
+ )
257
+ out.explore = res_explore
258
+
259
+ # Add messages for logging / auditing
260
+ branch.msgs.add_message(
261
+ instruction="\n".join(
262
+ i.model_dump_json() for i in all_explore_instructs
263
+ )
264
+ )
265
+ branch.msgs.add_message(
266
+ assistant_response="\n".join(
267
+ i.model_dump_json() for i in res_explore
268
+ )
269
+ )
270
+
271
+ # ---------------------------------------------------------
272
+ # Strategy B: SEQUENTIAL
273
+ # ---------------------------------------------------------
274
+ case "sequential":
275
+ explore_results = []
276
+
277
+ # Warn/log if a large number of instructions
278
+ if len(all_explore_instructs) > 30:
279
+ all_explore_instructs = all_explore_instructs[:30]
280
+ logging.warning(
281
+ "Maximum number of instructions for sequential exploration is 50. defaulting to 50."
282
+ )
283
+ if len(all_explore_instructs) > 10:
284
+ logging.warning(
285
+ "Large number of instructions for sequential exploration. This may take a while."
286
+ )
287
+
288
+ for i in all_explore_instructs:
289
+ if verbose:
290
+ snippet = (
291
+ i.guidance[:100] + "..."
292
+ if len(i.guidance) > 100
293
+ else i.guidance
294
+ )
295
+ print(f"\n-----Exploring Idea-----\n{snippet}")
296
+ seq_res = await branch.instruct(
297
+ i, **(explore_kwargs or {})
298
+ )
299
+ explore_results.append(
300
+ InstructResponse(instruct=i, response=seq_res)
301
+ )
302
+
303
+ out.explore = explore_results
304
+
305
+ # ---------------------------------------------------------
306
+ # Strategy C: SEQUENTIAL_CONCURRENT_CHUNK
307
+ # (chunks processed sequentially, each chunk in parallel)
308
+ # ---------------------------------------------------------
309
+ case "sequential_concurrent_chunk":
310
+ chunk_size = (explore_kwargs or {}).get("chunk_size", 5)
311
+ all_responses = []
312
+
313
+ async def explore_concurrent_chunk(
314
+ sub_instructs: list[Instruct], base_branch: Branch
315
+ ):
316
+ """
317
+ Explore instructions in a single chunk concurrently.
318
+ """
319
+ if verbose:
320
+ print(
321
+ f"\n--- Exploring a chunk of size {len(sub_instructs)} ---\n"
322
+ )
323
+
324
+ async def _explore(ins_: Instruct):
325
+ child_branch = session.split(base_branch)
326
+ child_resp = await child_branch.instruct(
327
+ ins_, **(explore_kwargs or {})
328
+ )
329
+ return InstructResponse(
330
+ instruct=ins_, response=child_resp
331
+ )
332
+
333
+ # Run all instructions in the chunk concurrently
334
+ res_chunk = await alcall(sub_instructs, _explore)
335
+
336
+ # Log messages for debugging / auditing
337
+ next_branch = session.split(base_branch)
338
+ next_branch.msgs.add_message(
339
+ instruction="\n".join(
340
+ i.model_dump_json() for i in sub_instructs
341
+ )
342
+ )
343
+ next_branch.msgs.add_message(
344
+ assistant_response="\n".join(
345
+ i.model_dump_json() for i in res_chunk
346
+ )
347
+ )
348
+ return res_chunk, next_branch
349
+
350
+ # Process each chunk sequentially
351
+ for chunk in chunked(all_explore_instructs, chunk_size):
352
+ chunk_result, branch = await explore_concurrent_chunk(
353
+ chunk, branch
354
+ )
355
+ all_responses.extend(chunk_result)
356
+
357
+ out.explore = all_responses
358
+
359
+ # ---------------------------------------------------------
360
+ # Strategy D: CONCURRENT_SEQUENTIAL_CHUNK
361
+ # (all chunks processed concurrently, each chunk sequentially)
362
+ # ---------------------------------------------------------
363
+ case "concurrent_sequential_chunk":
364
+ chunk_size = (explore_kwargs or {}).get("chunk_size", 5)
365
+ all_chunks = list(
366
+ chunked(all_explore_instructs, chunk_size)
367
+ )
368
+
369
+ async def explore_chunk_sequentially(
370
+ sub_instructs: list[Instruct],
371
+ ):
372
+ """
373
+ Explore instructions in a single chunk, one at a time.
374
+ """
375
+ chunk_results = []
376
+ local_branch = session.split(branch)
377
+
378
+ for ins_ in sub_instructs:
379
+ if verbose:
380
+ snippet = (
381
+ ins_.guidance[:100] + "..."
382
+ if len(ins_.guidance) > 100
383
+ else ins_.guidance
384
+ )
385
+ print(
386
+ f"\n-----Exploring Idea (sequential in chunk)-----\n{snippet}"
387
+ )
388
+
389
+ seq_resp = await local_branch.instruct(
390
+ ins_, **(explore_kwargs or {})
391
+ )
392
+ chunk_results.append(
393
+ InstructResponse(
394
+ instruct=ins_, response=seq_resp
395
+ )
396
+ )
397
+
398
+ return chunk_results
399
+
400
+ # Run all chunks in parallel
401
+ all_responses = await alcall(
402
+ all_chunks,
403
+ explore_chunk_sequentially,
404
+ flatten=True,
405
+ dropna=True,
406
+ )
407
+ out.explore = all_responses
408
+
409
+ # Log final messages
410
+ branch.msgs.add_message(
411
+ instruction="\n".join(
412
+ i.model_dump_json() for i in all_explore_instructs
413
+ )
414
+ )
415
+ branch.msgs.add_message(
416
+ assistant_response="\n".join(
417
+ i.model_dump_json() for i in all_responses
418
+ )
419
+ )
420
+
421
+ if branch_as_default:
422
+ session.change_default_branch(branch)
423
+
424
+ # -----------------------------------------------------------------
425
+ # 4. Return Results
426
+ # -----------------------------------------------------------------
201
427
  if return_session:
202
428
  return out, session
203
-
204
429
  return out
@@ -2,10 +2,11 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
-
6
5
  from lionagi.core.session.branch import Branch
7
6
  from lionagi.core.session.session import Session
8
7
  from lionagi.core.typing import ID, Any, BaseModel, Literal
8
+ from lionagi.libs.func.types import alcall
9
+ from lionagi.libs.parse import to_flat_list
9
10
  from lionagi.protocols.operatives.instruct import (
10
11
  INSTRUCT_FIELD_MODEL,
11
12
  Instruct,
@@ -15,13 +16,45 @@ from lionagi.protocols.operatives.instruct import (
15
16
  from ..utils import prepare_instruct, prepare_session
16
17
  from .prompt import EXPANSION_PROMPT, PLAN_PROMPT
17
18
 
19
+ # ---------------------------------------------------------------------
20
+ # Data Model
21
+ # ---------------------------------------------------------------------
22
+
18
23
 
19
24
  class PlanOperation(BaseModel):
25
+ """
26
+ Stores all relevant outcomes for a multi-step Plan:
27
+ * initial: The result of the initial plan prompt
28
+ * plan: A list of plan steps (Instruct objects) generated from the initial planning
29
+ * execute: Any responses from executing those plan steps
30
+ """
31
+
20
32
  initial: Any
21
33
  plan: list[Instruct] | None = None
22
34
  execute: list[InstructResponse] | None = None
23
35
 
24
36
 
37
+ # ---------------------------------------------------------------------
38
+ # Utilities
39
+ # ---------------------------------------------------------------------
40
+
41
+
42
+ def chunked(iterable, n):
43
+ """
44
+ Yield successive n-sized chunks from an iterable.
45
+ Example:
46
+ >>> list(chunked([1,2,3,4,5], 2))
47
+ [[1,2],[3,4],[5]]
48
+ """
49
+ for i in range(0, len(iterable), n):
50
+ yield iterable[i : i + n]
51
+
52
+
53
+ # ---------------------------------------------------------------------
54
+ # Single-Step Runner
55
+ # ---------------------------------------------------------------------
56
+
57
+
25
58
  async def run_step(
26
59
  ins: Instruct,
27
60
  session: Session,
@@ -29,33 +62,41 @@ async def run_step(
29
62
  verbose: bool = True,
30
63
  **kwargs: Any,
31
64
  ) -> Any:
32
- """Execute a single step of the plan.
65
+ """
66
+ Execute a single step of the plan with an 'expansion' or guidance prompt.
33
67
 
34
68
  Args:
35
69
  ins: The instruction model for the step.
36
- session: The current session.
37
- branch: The branch to operate on.
70
+ session: The current session context.
71
+ branch: The branch to operate on for this step.
38
72
  verbose: Whether to enable verbose output.
39
- **kwargs: Additional keyword arguments.
73
+ **kwargs: Additional keyword arguments passed to the branch operation.
40
74
 
41
75
  Returns:
42
- The result of the branch operation.
76
+ The result of the branch operation (which may contain more instructions).
43
77
  """
44
78
  if verbose:
45
- instruction = (
79
+ snippet = (
46
80
  ins.instruction[:100] + "..."
47
81
  if len(ins.instruction) > 100
48
82
  else ins.instruction
49
83
  )
50
- print(f"Further planning: {instruction}")
84
+ print(f"Further planning: {snippet}")
51
85
 
86
+ # Incorporate the EXPANSION_PROMPT into guidance
52
87
  config = {**ins.model_dump(), **kwargs}
53
- guide = config.pop("guidance", "")
54
- config["guidance"] = EXPANSION_PROMPT + "\n" + str(guide)
88
+ guidance_text = config.pop("guidance", "")
89
+ config["guidance"] = f"{EXPANSION_PROMPT}\n{guidance_text}"
90
+
91
+ # Run the step
92
+ result = await branch.operate(**config)
93
+ branch.msgs.logger.dump() # Dump logs if needed
94
+ return result
55
95
 
56
- res = await branch.operate(**config)
57
- branch.msgs.logger.dump()
58
- return res
96
+
97
+ # ---------------------------------------------------------------------
98
+ # Main Plan Function (with Multiple Execution Strategies)
99
+ # ---------------------------------------------------------------------
59
100
 
60
101
 
61
102
  async def plan(
@@ -65,108 +106,280 @@ async def plan(
65
106
  branch: Branch | ID.Ref | None = None,
66
107
  auto_run: bool = True,
67
108
  auto_execute: bool = False,
68
- execution_strategy: Literal["sequential"] = "sequential",
109
+ execution_strategy: Literal[
110
+ "sequential",
111
+ "concurrent",
112
+ "sequential_concurrent_chunk",
113
+ "concurrent_sequential_chunk",
114
+ ] = "sequential",
69
115
  execution_kwargs: dict[str, Any] | None = None,
70
116
  branch_kwargs: dict[str, Any] | None = None,
71
117
  return_session: bool = False,
72
118
  verbose: bool = True,
73
119
  **kwargs: Any,
74
- ) -> PlanOperation | tuple[list[InstructResponse], Session]:
75
- """Create and execute a multi-step plan.
120
+ ) -> PlanOperation | tuple[PlanOperation, Session]:
121
+ """
122
+ Create and optionally execute a multi-step plan with up to `num_steps`.
123
+
124
+ Steps:
125
+ 1. Generate an initial plan with up to `num_steps`.
126
+ 2. Optionally (auto_run=True) expand on each planned step
127
+ to refine or further clarify them.
128
+ 3. Optionally (auto_execute=True) execute those refined steps
129
+ according to `execution_strategy`.
76
130
 
77
131
  Args:
78
- instruct: Instruction model or dictionary.
79
- num_steps: Number of steps in the plan.
80
- session: Existing session or None to create a new one.
81
- branch: Existing branch or reference.
82
- auto_run: If True, automatically run the steps.
83
- branch_kwargs: Additional keyword arguments for branch creation.
84
- return_session: If True, return the session along with results.
85
- verbose: Whether to enable verbose output.
86
- **kwargs: Additional keyword arguments.
132
+ instruct: Initial instruction or a dict describing it.
133
+ num_steps: Maximum number of plan steps (must be <= 5).
134
+ session: An existing Session, or None to create a new one.
135
+ branch: An existing Branch, or None to create a new one.
136
+ auto_run: If True, automatically run the intermediate plan steps.
137
+ auto_execute: If True, automatically execute the fully refined steps.
138
+ execution_strategy:
139
+ - "sequential" (default) runs steps one by one
140
+ - "concurrent" runs all steps in parallel
141
+ - "sequential_concurrent_chunk" processes chunks sequentially, each chunk in parallel
142
+ - "concurrent_sequential_chunk" processes all chunks in parallel, each chunk sequentially
143
+ execution_kwargs: Extra kwargs used during execution calls.
144
+ branch_kwargs: Extra kwargs for branch/session creation.
145
+ return_session: Whether to return (PlanOperation, Session) instead of just PlanOperation.
146
+ verbose: If True, prints verbose logs.
147
+ **kwargs: Additional arguments for the initial plan operation.
87
148
 
88
149
  Returns:
89
- Results of the plan execution, optionally with the session.
150
+ A PlanOperation object containing:
151
+ - initial plan
152
+ - (optional) plan expansions
153
+ - (optional) execution responses
154
+ Optionally returns the session as well, if `return_session=True`.
90
155
  """
156
+
157
+ # -----------------------------------------------------------------
158
+ # 0. Basic Validation & Setup
159
+ # -----------------------------------------------------------------
91
160
  if num_steps > 5:
92
161
  raise ValueError("Number of steps must be 5 or less")
93
162
 
94
163
  if verbose:
95
164
  print(f"Planning execution with {num_steps} steps...")
96
165
 
166
+ # Ensure the correct field model
97
167
  field_models: list = kwargs.get("field_models", [])
98
168
  if INSTRUCT_FIELD_MODEL not in field_models:
99
169
  field_models.append(INSTRUCT_FIELD_MODEL)
100
170
  kwargs["field_models"] = field_models
171
+
172
+ # Prepare session/branch
101
173
  session, branch = prepare_session(session, branch, branch_kwargs)
102
- execute_branch: Branch = session.split(branch)
103
- instruct = prepare_instruct(
104
- instruct, PLAN_PROMPT.format(num_steps=num_steps)
105
- )
174
+ execute_branch: Branch = session.split(
175
+ branch
176
+ ) # a separate branch for execution
106
177
 
107
- res1 = await branch.operate(**instruct, **kwargs)
108
- out = PlanOperation(initial=res1)
178
+ # -----------------------------------------------------------------
179
+ # 1. Run the Initial Plan Prompt
180
+ # -----------------------------------------------------------------
181
+ plan_prompt = PLAN_PROMPT.format(num_steps=num_steps)
182
+ instruct = prepare_instruct(instruct, plan_prompt)
183
+ initial_res = await branch.operate(**instruct, **kwargs)
184
+
185
+ # Wrap initial result in the PlanOperation
186
+ out = PlanOperation(initial=initial_res)
109
187
 
110
188
  if verbose:
111
189
  print("Initial planning complete. Starting step planning...")
112
190
 
191
+ # If we aren't auto-running the steps, just return the initial plan
113
192
  if not auto_run:
114
- if return_session:
115
- return res1, session
116
- return res1
193
+ return (out, session) if return_session else out
117
194
 
195
+ # -----------------------------------------------------------------
196
+ # 2. Expand Each Step (auto_run=True)
197
+ # -----------------------------------------------------------------
118
198
  results = []
119
- if hasattr(res1, "instruct_models"):
120
- instructs: list[Instruct] = res1.instruct_models
121
- for i, ins in enumerate(instructs, 1):
199
+ if hasattr(initial_res, "instruct_models"):
200
+ instructs: list[Instruct] = initial_res.instruct_models
201
+ for i, step_ins in enumerate(instructs, start=1):
122
202
  if verbose:
123
203
  print(f"\n----- Planning step {i}/{len(instructs)} -----")
124
- res = await run_step(
125
- ins, session, branch, verbose=verbose, **kwargs
204
+ expanded_res = await run_step(
205
+ step_ins, session, branch, verbose=verbose, **kwargs
126
206
  )
127
- results.append(res)
207
+ results.append(expanded_res)
128
208
 
129
209
  if verbose:
130
- print("\nAll planning completed successfully!")
210
+ print("\nAll planning steps expanded/refined successfully!")
211
+
212
+ # Gather all newly created plan instructions
213
+ refined_plans = []
214
+ for step_result in results:
215
+ if hasattr(step_result, "instruct_models"):
216
+ for model in step_result.instruct_models:
217
+ if model and model not in refined_plans:
218
+ refined_plans.append(model)
131
219
 
132
- all_plans = []
133
- for res in results:
134
- if hasattr(res, "instruct_models"):
135
- for i in res.instruct_models:
136
- if i and i not in all_plans:
137
- all_plans.append(i)
138
- out.plan = all_plans
220
+ out.plan = refined_plans
139
221
 
222
+ # -----------------------------------------------------------------
223
+ # 3. Execute the Plan Steps (auto_execute=True)
224
+ # -----------------------------------------------------------------
140
225
  if auto_execute:
141
226
  if verbose:
142
- print("\nStarting execution of all steps...")
143
- results = []
227
+ print("\nStarting execution of all plan steps...")
228
+
229
+ # We now handle multiple strategies:
144
230
  match execution_strategy:
231
+
232
+ # ---------------------------------------------------------
233
+ # Strategy A: SEQUENTIAL
234
+ # ---------------------------------------------------------
145
235
  case "sequential":
146
- for i, ins in enumerate(all_plans, 1):
236
+ seq_results = []
237
+ for i, plan_step in enumerate(refined_plans, start=1):
238
+ if verbose:
239
+ snippet = (
240
+ plan_step.instruction[:100] + "..."
241
+ if len(plan_step.instruction) > 100
242
+ else plan_step.instruction
243
+ )
244
+ print(
245
+ f"\n------ Executing step {i}/{len(refined_plans)} ------"
246
+ )
247
+ print(f"Instruction: {snippet}")
248
+
249
+ step_response = await execute_branch.instruct(
250
+ plan_step, **(execution_kwargs or {})
251
+ )
252
+ seq_results.append(
253
+ InstructResponse(
254
+ instruct=plan_step, response=step_response
255
+ )
256
+ )
257
+
258
+ out.execute = seq_results
259
+ if verbose:
260
+ print("\nAll steps executed successfully (sequential)!")
261
+
262
+ # ---------------------------------------------------------
263
+ # Strategy B: CONCURRENT
264
+ # ---------------------------------------------------------
265
+ case "concurrent":
266
+
267
+ async def execute_step_concurrently(plan_step: Instruct):
268
+ if verbose:
269
+ snippet = (
270
+ plan_step.instruction[:100] + "..."
271
+ if len(plan_step.instruction) > 100
272
+ else plan_step.instruction
273
+ )
274
+ print(f"\n------ Executing step (concurrently) ------")
275
+ print(f"Instruction: {snippet}")
276
+ local_branch = session.split(execute_branch)
277
+ resp = await local_branch.instruct(
278
+ plan_step, **(execution_kwargs or {})
279
+ )
280
+ return InstructResponse(instruct=plan_step, response=resp)
281
+
282
+ # Launch all steps in parallel
283
+ concurrent_res = await alcall(
284
+ refined_plans, execute_step_concurrently
285
+ )
286
+ out.execute = concurrent_res
287
+ if verbose:
288
+ print("\nAll steps executed successfully (concurrent)!")
289
+
290
+ # ---------------------------------------------------------
291
+ # Strategy C: SEQUENTIAL_CONCURRENT_CHUNK
292
+ # - process plan steps in chunks (one chunk after another),
293
+ # - each chunk’s steps run in parallel.
294
+ # ---------------------------------------------------------
295
+ case "sequential_concurrent_chunk":
296
+ chunk_size = (execution_kwargs or {}).get("chunk_size", 5)
297
+ all_exec_responses = []
298
+
299
+ async def execute_chunk_concurrently(
300
+ sub_steps: list[Instruct],
301
+ ):
147
302
  if verbose:
148
303
  print(
149
- f"\n------ Executing step {i}/{len(all_plans)} ------"
304
+ f"\n--- Executing a chunk of size {len(sub_steps)} concurrently ---"
150
305
  )
151
- msg = (
152
- ins.instruction[:100] + "..."
153
- if len(ins.instruction) > 100
154
- else ins.instruction
306
+
307
+ async def _execute(plan_step: Instruct):
308
+ local_branch = session.split(execute_branch)
309
+ resp = await local_branch.instruct(
310
+ plan_step, **(execution_kwargs or {})
311
+ )
312
+ return InstructResponse(
313
+ instruct=plan_step, response=resp
155
314
  )
156
- print(f"Instruction: {msg}")
157
- res = await execute_branch.instruct(
158
- ins, **(execution_kwargs or {})
315
+
316
+ # run each chunk in parallel
317
+ return await alcall(sub_steps, _execute)
318
+
319
+ # process each chunk sequentially
320
+ for chunk in chunked(refined_plans, chunk_size):
321
+ chunk_responses = await execute_chunk_concurrently(chunk)
322
+ all_exec_responses.extend(chunk_responses)
323
+
324
+ out.execute = all_exec_responses
325
+ if verbose:
326
+ print(
327
+ "\nAll steps executed successfully (sequential concurrent chunk)!"
159
328
  )
160
- res_ = InstructResponse(instruct=ins, response=res)
161
- results.append(res_)
162
- out.execute = results
329
+
330
+ # ---------------------------------------------------------
331
+ # Strategy D: CONCURRENT_SEQUENTIAL_CHUNK
332
+ # - split plan steps into chunks,
333
+ # - run all chunks in parallel,
334
+ # - but each chunk’s steps run sequentially.
335
+ # ---------------------------------------------------------
336
+ case "concurrent_sequential_chunk":
337
+ chunk_size = (execution_kwargs or {}).get("chunk_size", 5)
338
+ all_chunks = list(chunked(refined_plans, chunk_size))
339
+
340
+ async def execute_chunk_sequentially(
341
+ sub_steps: list[Instruct],
342
+ ):
343
+ chunk_result = []
344
+ local_branch = session.split(execute_branch)
345
+ for plan_step in sub_steps:
346
+ if verbose:
347
+ snippet = (
348
+ plan_step.instruction[:100] + "..."
349
+ if len(plan_step.instruction) > 100
350
+ else plan_step.instruction
351
+ )
352
+ print(
353
+ f"\n--- Executing step (sequential in chunk) ---\nInstruction: {snippet}"
354
+ )
355
+ resp = await local_branch.instruct(
356
+ plan_step, **(execution_kwargs or {})
357
+ )
358
+ chunk_result.append(
359
+ InstructResponse(instruct=plan_step, response=resp)
360
+ )
361
+ return chunk_result
362
+
363
+ # run all chunks in parallel, each chunk sequentially
364
+ parallel_chunk_results = await alcall(
365
+ all_chunks,
366
+ execute_chunk_sequentially,
367
+ flatten=True,
368
+ dropna=True,
369
+ )
370
+
371
+ out.execute = parallel_chunk_results
163
372
  if verbose:
164
- print("\nAll steps executed successfully!")
373
+ print(
374
+ "\nAll steps executed successfully (concurrent sequential chunk)!"
375
+ )
376
+
165
377
  case _:
166
378
  raise ValueError(
167
379
  f"Invalid execution strategy: {execution_strategy}"
168
380
  )
169
381
 
170
- if return_session:
171
- return out, session
172
- return out
382
+ # -----------------------------------------------------------------
383
+ # 4. Final Return
384
+ # -----------------------------------------------------------------
385
+ return (out, session) if return_session else out
lionagi/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.5.3"
1
+ __version__ = "0.5.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lionagi
3
- Version: 0.5.3
3
+ Version: 0.5.4
4
4
  Summary: An AGentic Intelligence Operating System.
5
5
  Author-email: HaiyangLi <quantocean.li@gmail.com>
6
6
  License: Apache License
@@ -1,6 +1,6 @@
1
1
  lionagi/__init__.py,sha256=oybfu2VsZc4ElN7ZeaW3KQrz8T8EcSDHPA8lUE-8G2I,537
2
2
  lionagi/settings.py,sha256=BOjxRV4N9zQJervvajPhbaHmgZ-nhbCy7AaQJi3Avug,2726
3
- lionagi/version.py,sha256=tgzuqHKcEdKBaP57F5oXxq4XlW2n9J4Fj8ZGu7nGOZg,22
3
+ lionagi/version.py,sha256=DITpct-LrdIsTgwx2NgH5Ghx5y8Xgz1YMimy1ZV5RTY,22
4
4
  lionagi/core/__init__.py,sha256=v8vNyJVIVj8_Oz9RJdVe6ZKUQMYTgDh1VQpnr1KdLaw,112
5
5
  lionagi/core/_class_registry.py,sha256=srSWefqCS9EZrMvyA8zCrZ9KFvzAhTIj8g6mJG5KlIc,1982
6
6
  lionagi/core/action/__init__.py,sha256=v8vNyJVIVj8_Oz9RJdVe6ZKUQMYTgDh1VQpnr1KdLaw,112
@@ -56,7 +56,7 @@ lionagi/core/models/types.py,sha256=elcUuz_9dx4AhZddnICF-Cs62VJWIBqMET7MiRe4c1I,
56
56
  lionagi/core/session/__init__.py,sha256=v8vNyJVIVj8_Oz9RJdVe6ZKUQMYTgDh1VQpnr1KdLaw,112
57
57
  lionagi/core/session/branch.py,sha256=r6yNXwTm0oYA-9YOereuvLJtDnhj9IJWJ5fjTyKN88U,4406
58
58
  lionagi/core/session/branch_mixins.py,sha256=C9lGHmD1AmXAePw0kGIeuZjZ7SzOrFcIwV6rVaGhoGg,19623
59
- lionagi/core/session/session.py,sha256=Uup50oMqs00ZH6zgEiqtk0pG_ANByHW_Ael07dfbT1M,5122
59
+ lionagi/core/session/session.py,sha256=cutece_iTs5K_m5soRfU9oTfHmw1icDEvx77E1RelIM,5129
60
60
  lionagi/core/session/types.py,sha256=MUGTSa2HWK79p7z-CG22RFP07N5AKnPVNXZwZt_wIvU,202
61
61
  lionagi/core/typing/__init__.py,sha256=Y9BK1OUXzjQgIo3epCVwWqUYhFwQQ_ayhRwI1UOmINg,228
62
62
  lionagi/core/typing/_concepts.py,sha256=uIzqfwtPBsIREhvT7NDAowc4i-u-69n_DRzLHzvHZO4,3730
@@ -257,7 +257,7 @@ lionagi/libs/func/throttle.py,sha256=iOOmS6i81NGRCClf4WaIhMMXwPt_rZ6WXYBqMJ_weEE
257
257
  lionagi/libs/func/types.py,sha256=wdjGNmY82pVACQBuDMUt3XJO0G9_wKHR7VesOeMxo_A,831
258
258
  lionagi/libs/func/utils.py,sha256=-LMdUEjksXP6JjjcUBh0XEQiXF30Zmv3xpKbmXjfya8,2674
259
259
  lionagi/libs/func/async_calls/__init__.py,sha256=qDHIkH7B-Ka5NJqeeyu_YL3TY36xL8kBwTjtCR4H8AU,495
260
- lionagi/libs/func/async_calls/alcall.py,sha256=U8-lWwqNwhe50Q07kzyW4YVcbx_LkUN5X1K1zvFg7i0,7283
260
+ lionagi/libs/func/async_calls/alcall.py,sha256=GhPXId0YLvfxpDSYOABwr_3D-nRu8aLWshaYboJQzmQ,7436
261
261
  lionagi/libs/func/async_calls/bcall.py,sha256=gwfKpRZkExjVn-1YGZfaboFES8RqUgyaBrdpNtz1IdY,4436
262
262
  lionagi/libs/func/async_calls/mcall.py,sha256=9O5gWbBT4iIqXfcdZjgAdqsOSLWrNS4_tt6ou231ozA,4607
263
263
  lionagi/libs/func/async_calls/pcall.py,sha256=6u3RJPV-3yOkWxC9iSoVFl1veFfZFJpR0QRyGtfBODI,5831
@@ -317,10 +317,10 @@ lionagi/libs/string_similarity/utils.py,sha256=NdD0qF5tuytWBsm0WMrH0gRBBSxw2p4-m
317
317
  lionagi/operations/__init__.py,sha256=Dt7o6DFP7zVR-uxZ4xsGHQcse3XVlF6S8Y9NhaUTn_4,68
318
318
  lionagi/operations/utils.py,sha256=kn5SkZRczl1aQ-vJBeVPlMdeyUUD0s5iyuAW4P6KOvQ,1164
319
319
  lionagi/operations/brainstorm/__init__.py,sha256=amsoH65wepsx58DfgH-TRTN1wDH5TC24qYI_f02zkVg,61
320
- lionagi/operations/brainstorm/brainstorm.py,sha256=H2D0ZBCbRA2Ig60IHuXhyH6Hu02jxKhbj028UtS4eik,6279
320
+ lionagi/operations/brainstorm/brainstorm.py,sha256=1Uuc11OH34jEYfDdombX5ui9b-bJTn4bVSLt0jjQUIc,16747
321
321
  lionagi/operations/brainstorm/prompt.py,sha256=3a7LsmtiqGAK5mtWoX-2qhsjETBzBx8FxM3cFCBEoOo,497
322
322
  lionagi/operations/plan/__init__.py,sha256=SVqoVmlSGz9lsLztm487H2qOLwgyFxSi2yZ8ubn-bgE,43
323
- lionagi/operations/plan/plan.py,sha256=Ui8fFdRNLiD3RJmnKCwfY48yj0Fxxw2LBye0egYttWE,5611
323
+ lionagi/operations/plan/plan.py,sha256=AX4h_TeyhcXag572ywN9rmvo3fEiuGER7skAKf25_LY,15329
324
324
  lionagi/operations/plan/prompt.py,sha256=sKHa_jDahzCJ60oILj1XNYCIlbS-H8ybKRXpf9zd5x0,880
325
325
  lionagi/operations/select/__init__.py,sha256=dUd-KS1l404_ueYlIQsVNhS9jAqjn5pJbtUEbbh6KlI,49
326
326
  lionagi/operations/select/prompt.py,sha256=wbmuDC96fcQ4LFKjqmeErOQwVRpGWRqcwUeLTWnbeNs,186
@@ -368,7 +368,7 @@ lionagi/strategies/sequential_chunk.py,sha256=jG_WZXG-Ra3yd30CmX4b3XeCNAUrZGA2-i
368
368
  lionagi/strategies/sequential_concurrent_chunk.py,sha256=H7GShaqYlD5XxNJMG2GdOR4Vl8JHDhZb5jxNq8zY0hI,3365
369
369
  lionagi/strategies/types.py,sha256=fEvE4d1H4SeCcXcd2dz3q4k8jFIBtxYzjxDN7eJRLtI,769
370
370
  lionagi/strategies/utils.py,sha256=DX1dvxia8cNRqEJJbssJ3mgRzo7kgWCTA4y5DYLCCZE,1281
371
- lionagi-0.5.3.dist-info/METADATA,sha256=EDYWV82gJQWvmwXKbRLUaJjEGQ20B2DL-3emvuhdchg,22736
372
- lionagi-0.5.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
373
- lionagi-0.5.3.dist-info/licenses/LICENSE,sha256=VXFWsdoN5AAknBCgFqQNgPWYx7OPp-PFEP961zGdOjc,11288
374
- lionagi-0.5.3.dist-info/RECORD,,
371
+ lionagi-0.5.4.dist-info/METADATA,sha256=or-K2I6vye-JTu559FMXpy14cZlI5_3xLAgLCURMu1w,22736
372
+ lionagi-0.5.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
373
+ lionagi-0.5.4.dist-info/licenses/LICENSE,sha256=VXFWsdoN5AAknBCgFqQNgPWYx7OPp-PFEP961zGdOjc,11288
374
+ lionagi-0.5.4.dist-info/RECORD,,