lionagi 0.5.3__py3-none-any.whl → 0.5.4__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,,