pybotchi 3.2.0__tar.gz → 3.4.1__tar.gz

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 (46) hide show
  1. {pybotchi-3.2.0 → pybotchi-3.4.1}/PKG-INFO +18 -17
  2. {pybotchi-3.2.0 → pybotchi-3.4.1}/README.md +17 -16
  3. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/action.py +115 -25
  4. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/action.pyi +7 -1
  5. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/common.py +4 -2
  6. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/context.py +25 -1
  7. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/context.pyi +4 -1
  8. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/cli.py +7 -6
  9. {pybotchi-3.2.0 → pybotchi-3.4.1}/pyproject.toml +8 -8
  10. {pybotchi-3.2.0 → pybotchi-3.4.1}/LICENSE +0 -0
  11. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/__init__.py +0 -0
  12. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/__init__.pyi +0 -0
  13. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/common.pyi +0 -0
  14. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/__init__.py +0 -0
  15. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/__init__.pyi +0 -0
  16. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/action.py +0 -0
  17. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/action.pyi +0 -0
  18. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/cli.pyi +0 -0
  19. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/common.py +0 -0
  20. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/common.pyi +0 -0
  21. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/context.py +0 -0
  22. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/context.pyi +0 -0
  23. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/exception.py +0 -0
  24. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/exception.pyi +0 -0
  25. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/handler.py +0 -0
  26. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/handler.pyi +0 -0
  27. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi.proto +0 -0
  28. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi_pb2.py +0 -0
  29. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi_pb2.pyi +0 -0
  30. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi_pb2_grpc.py +0 -0
  31. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi_pb2_grpc.pyi +0 -0
  32. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/utils.py +0 -0
  33. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/utils.pyi +0 -0
  34. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/llm.py +0 -0
  35. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/llm.pyi +0 -0
  36. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/__init__.py +0 -0
  37. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/__init__.pyi +0 -0
  38. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/action.py +0 -0
  39. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/action.pyi +0 -0
  40. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/common.py +0 -0
  41. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/common.pyi +0 -0
  42. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/context.py +0 -0
  43. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/context.pyi +0 -0
  44. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/py.typed +0 -0
  45. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/utils.py +0 -0
  46. {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/utils.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybotchi
3
- Version: 3.2.0
3
+ Version: 3.4.1
4
4
  Summary: A deterministic, intent-based AI agent builder.
5
5
  License-File: LICENSE
6
6
  Author: Alexie (Boyong) Madolid
@@ -129,11 +129,16 @@ LLM.add(base=ChatOpenAI(
129
129
  ```python
130
130
  from pybotchi import Action, ActionReturn
131
131
 
132
+ from pydantic import Field
133
+
132
134
  class Translation(Action):
133
- """Translate to specified language."""
135
+ """Translate to specific language."""
136
+
137
+ message: str = Field(description="The text content to be translated.")
138
+ language: str = Field(description="The ISO code or name of the target language.")
134
139
 
135
140
  async def pre(self, context):
136
- message = await context.llm.ainvoke(context.prompts)
141
+ message = await context.llm.ainvoke(f"Reply only with translation of `{self.message}` to {self.language}.")
137
142
  await context.add_response(self, message.text)
138
143
  return ActionReturn.GO
139
144
  ```
@@ -181,22 +186,20 @@ async def test():
181
186
  context = Context(
182
187
  prompts=[
183
188
  {"role": "system", "content": "You're an AI that can solve math problems and translate requests."},
184
- {"role": "user", "content": "4 x 4 and explain in Filipino"}
189
+ {"role": "user", "content": "4 x 4 then explain in Filipino"},
185
190
  ],
186
191
  )
187
192
  await context.start(MultiAgent)
188
- print(context.prompts[-1]["content"])
193
+ print(f"MathProblem: {context.prompts[-3]['content']}")
194
+ print(f"Translate: {context.prompts[-1]['content']}")
189
195
 
190
196
  asyncio.run(test())
191
197
  ```
192
198
 
193
199
  **Result:**
194
200
  ```
195
- Ang sagot sa 4 x 4 ay 16.
196
-
197
- Paliwanag: Kapag sinabi nating 4 x 4, ibig sabihin ay apat na grupo ng apat. Kung bibilangin natin ito, makakakuha tayo ng kabuuang labing-anim (16).
198
-
199
- Ibig sabihin, 4 + 4 + 4 + 4 = 16.
201
+ MathProblem: Four multiplied by four is sixteen. Imagine you have four groups, and each group has four candies. If you count all the candies together, you will have sixteen candies. That's what 4 x 4 means!
202
+ Translate: Ang apat na pinarami sa apat ay labing-anim. Ipagpalagay mong may apat na grupo, at bawat grupo ay may apat na kendi. Kung bibilangin mo lahat ng kendi, magkakaroon ka ng labing-anim na kendi. Iyan ang ibig sabihin ng 4 x 4!
200
203
  ```
201
204
 
202
205
  ### Visualize Your Graph
@@ -210,7 +213,7 @@ async def print_mermaid_graph():
210
213
  multi_agent_graph = await graph(MultiAgent)
211
214
  print(multi_agent_graph.flowchart())
212
215
 
213
- run(print_mermaid_graph())
216
+ asyncio.run(print_mermaid_graph())
214
217
  ```
215
218
  **Result:**
216
219
  ```
@@ -393,14 +396,12 @@ pybotchi-grpc server.py
393
396
  **Result**
394
397
  ```bash
395
398
  #-------------------------------------------------------#
396
- # Agent ID: agent_b6c9ada82c7444818356a6338e975c09
397
- # Agent Path: server.py
399
+ # Agent ID: agent_8b3c5685c84b4602966d1b3252916aa7
398
400
  # Agent Path: server.py
399
401
  # Starting None worker(s) on 0.0.0.0:50051
400
402
  #-------------------------------------------------------#
401
- # Agent Path: server.py
403
+ # Agent Process: Process-1 [173012]
402
404
  # Agent Handler: PyBotchiGRPC
403
- # gRPC server running on 0.0.0.0:50051
404
405
  #-------------------------------------------------------#
405
406
  ```
406
407
  gRPC client print graph:
@@ -409,9 +410,9 @@ python3 client.py
409
410
  ```
410
411
  ```bash
411
412
  flowchart TD
413
+ grpc.agent_8b3c5685c84b4602966d1b3252916aa7.MathProblem[MathProblem]
412
414
  __main__.MultiAgent[MultiAgent]
413
- grpc.agent_b6c9ada82c7444818356a6338e975c09.MathProblem[MathProblem]
414
- __main__.MultiAgent --**GRPC** : remote--> grpc.agent_b6c9ada82c7444818356a6338e975c09.MathProblem
415
+ __main__.MultiAgent --"`**GRPC** : remote`"--> grpc.agent_8b3c5685c84b4602966d1b3252916aa7.MathProblem
415
416
  style __main__.MultiAgent fill:#4CAF50,color:#000000
416
417
  ```
417
418
  ![gRPC MultiAgent Graph](docs/mermaid2.png)
@@ -103,11 +103,16 @@ LLM.add(base=ChatOpenAI(
103
103
  ```python
104
104
  from pybotchi import Action, ActionReturn
105
105
 
106
+ from pydantic import Field
107
+
106
108
  class Translation(Action):
107
- """Translate to specified language."""
109
+ """Translate to specific language."""
110
+
111
+ message: str = Field(description="The text content to be translated.")
112
+ language: str = Field(description="The ISO code or name of the target language.")
108
113
 
109
114
  async def pre(self, context):
110
- message = await context.llm.ainvoke(context.prompts)
115
+ message = await context.llm.ainvoke(f"Reply only with translation of `{self.message}` to {self.language}.")
111
116
  await context.add_response(self, message.text)
112
117
  return ActionReturn.GO
113
118
  ```
@@ -155,22 +160,20 @@ async def test():
155
160
  context = Context(
156
161
  prompts=[
157
162
  {"role": "system", "content": "You're an AI that can solve math problems and translate requests."},
158
- {"role": "user", "content": "4 x 4 and explain in Filipino"}
163
+ {"role": "user", "content": "4 x 4 then explain in Filipino"},
159
164
  ],
160
165
  )
161
166
  await context.start(MultiAgent)
162
- print(context.prompts[-1]["content"])
167
+ print(f"MathProblem: {context.prompts[-3]['content']}")
168
+ print(f"Translate: {context.prompts[-1]['content']}")
163
169
 
164
170
  asyncio.run(test())
165
171
  ```
166
172
 
167
173
  **Result:**
168
174
  ```
169
- Ang sagot sa 4 x 4 ay 16.
170
-
171
- Paliwanag: Kapag sinabi nating 4 x 4, ibig sabihin ay apat na grupo ng apat. Kung bibilangin natin ito, makakakuha tayo ng kabuuang labing-anim (16).
172
-
173
- Ibig sabihin, 4 + 4 + 4 + 4 = 16.
175
+ MathProblem: Four multiplied by four is sixteen. Imagine you have four groups, and each group has four candies. If you count all the candies together, you will have sixteen candies. That's what 4 x 4 means!
176
+ Translate: Ang apat na pinarami sa apat ay labing-anim. Ipagpalagay mong may apat na grupo, at bawat grupo ay may apat na kendi. Kung bibilangin mo lahat ng kendi, magkakaroon ka ng labing-anim na kendi. Iyan ang ibig sabihin ng 4 x 4!
174
177
  ```
175
178
 
176
179
  ### Visualize Your Graph
@@ -184,7 +187,7 @@ async def print_mermaid_graph():
184
187
  multi_agent_graph = await graph(MultiAgent)
185
188
  print(multi_agent_graph.flowchart())
186
189
 
187
- run(print_mermaid_graph())
190
+ asyncio.run(print_mermaid_graph())
188
191
  ```
189
192
  **Result:**
190
193
  ```
@@ -367,14 +370,12 @@ pybotchi-grpc server.py
367
370
  **Result**
368
371
  ```bash
369
372
  #-------------------------------------------------------#
370
- # Agent ID: agent_b6c9ada82c7444818356a6338e975c09
371
- # Agent Path: server.py
373
+ # Agent ID: agent_8b3c5685c84b4602966d1b3252916aa7
372
374
  # Agent Path: server.py
373
375
  # Starting None worker(s) on 0.0.0.0:50051
374
376
  #-------------------------------------------------------#
375
- # Agent Path: server.py
377
+ # Agent Process: Process-1 [173012]
376
378
  # Agent Handler: PyBotchiGRPC
377
- # gRPC server running on 0.0.0.0:50051
378
379
  #-------------------------------------------------------#
379
380
  ```
380
381
  gRPC client print graph:
@@ -383,9 +384,9 @@ python3 client.py
383
384
  ```
384
385
  ```bash
385
386
  flowchart TD
387
+ grpc.agent_8b3c5685c84b4602966d1b3252916aa7.MathProblem[MathProblem]
386
388
  __main__.MultiAgent[MultiAgent]
387
- grpc.agent_b6c9ada82c7444818356a6338e975c09.MathProblem[MathProblem]
388
- __main__.MultiAgent --**GRPC** : remote--> grpc.agent_b6c9ada82c7444818356a6338e975c09.MathProblem
389
+ __main__.MultiAgent --"`**GRPC** : remote`"--> grpc.agent_8b3c5685c84b4602966d1b3252916aa7.MathProblem
389
390
  style __main__.MultiAgent fill:#4CAF50,color:#000000
390
391
  ```
391
392
  ![gRPC MultiAgent Graph](docs/mermaid2.png)
@@ -6,7 +6,6 @@ from asyncio import TaskGroup
6
6
  from collections import deque
7
7
  from collections.abc import Generator
8
8
  from inspect import getmembers
9
- from itertools import islice
10
9
  from os import getenv
11
10
  from typing import Any, Generic, TYPE_CHECKING, TypeVar
12
11
 
@@ -19,7 +18,6 @@ from .common import (
19
18
  Graph,
20
19
  Groups,
21
20
  ToolCall,
22
- UNSPECIFIED,
23
21
  UsageData,
24
22
  )
25
23
  from .utils import apply_placeholders, unwrap_exceptions, uuid
@@ -49,6 +47,29 @@ ${system}
49
47
  ${addons}
50
48
  """.strip(),
51
49
  )
50
+ DEFAULT_MAX_ITERATION_PROMPT = getenv(
51
+ "DEFAULT_MAX_ITERATION_PROMPT",
52
+ """
53
+ You are an AI assistant responsible for delivering the final response to the user.
54
+ Your primary responsibility is to synthesize all prior tool calls and results from the conversation history into a clear, coherent, and complete response.
55
+
56
+ # Finalization Guidelines:
57
+ - Review the full conversation history, including all function calls made, their inputs, and their returned results.
58
+ - Synthesize all gathered information into a single, well-structured final response.
59
+ - Do NOT invoke any additional functions or tools — this is a finalization step only.
60
+ - If the task was fully completed through prior iterations, summarize and present the results clearly.
61
+ - If the task was only partially completed, clearly communicate:
62
+ - What was successfully accomplished.
63
+ - What could not be completed and why (e.g., iteration limit reached before all steps finished).
64
+ - Any actionable next steps the user can take.
65
+ - If conflicting or incomplete data exists across iterations, use your best judgment to reconcile it and flag any uncertainty to the user.
66
+ - Maintain the tone and context established in the original task.
67
+ - Never expose raw function names, parameters, or internal tool output directly — always translate them into natural, user-friendly language.
68
+
69
+ # Initial Task:
70
+ ${system}
71
+ """.strip(),
72
+ )
52
73
 
53
74
  TAction = TypeVar("TAction", bound="Action")
54
75
  TContext = TypeVar("TContext", bound="Context")
@@ -67,6 +88,7 @@ class Action(BaseModel, Generic[TContext]):
67
88
  __enabled__: bool = True
68
89
  __system_prompt__: str | None = None
69
90
  __tool_call_prompt__: str | None = None
91
+ __max_iteration_prompt__: str | None = None
70
92
  __temperature__: float | None = None
71
93
  __max_tool_prompts__: int | None = None
72
94
  __default_tool__ = DEFAULT_ACTION
@@ -75,6 +97,7 @@ class Action(BaseModel, Generic[TContext]):
75
97
 
76
98
  __has_pre__: bool
77
99
  __has_fallback__: bool
100
+ __has_on_child_init_error__: bool
78
101
  __has_on_error__: bool
79
102
  __has_post__: bool
80
103
  __has_as_tool__: bool
@@ -115,6 +138,7 @@ class Action(BaseModel, Generic[TContext]):
115
138
  cls.__display_name__ = src.get("__display_name__", cls.__name__)
116
139
  cls.__has_pre__ = cls.pre is not Action.pre
117
140
  cls.__has_fallback__ = cls.fallback is not Action.fallback
141
+ cls.__has_on_child_init_error__ = cls.on_child_init_error is not Action.on_child_init_error
118
142
  cls.__has_on_error__ = cls.on_error is not Action.on_error
119
143
  cls.__has_post__ = cls.post is not Action.post
120
144
  cls.__has_as_tool__ = cls._as_tool is not Action._as_tool
@@ -156,6 +180,16 @@ class Action(BaseModel, Generic[TContext]):
156
180
  """Execute fallback process."""
157
181
  return ActionReturn.GO
158
182
 
183
+ async def on_child_init_error(
184
+ self,
185
+ context: TContext,
186
+ next_actions: list["Action"],
187
+ child_cls: type[Action],
188
+ child_args: dict[str, Any],
189
+ exception: Exception,
190
+ ) -> str | None:
191
+ """Execute on child init error process."""
192
+
159
193
  async def on_error(
160
194
  self,
161
195
  context: TContext,
@@ -165,6 +199,49 @@ class Action(BaseModel, Generic[TContext]):
165
199
  """Execute on error process."""
166
200
  return ActionReturn.GO
167
201
 
202
+ async def on_max_iteration(self, context: TContext) -> ActionReturn:
203
+ """Execute on max iteration process."""
204
+ await context.notify(
205
+ {
206
+ "event": "tool",
207
+ "type": "finalize",
208
+ "status": "started",
209
+ "data": self.__display_name__,
210
+ }
211
+ )
212
+ llm = context.llm
213
+ if self.__temperature__ is not None:
214
+ llm = llm.with_config(configurable={"llm_temperature": self.__temperature__})
215
+
216
+ message = await llm.ainvoke(
217
+ [
218
+ {
219
+ "content": self.max_iteration_prompt(context),
220
+ "role": "system",
221
+ },
222
+ *context.shifted_prompts(self.__max_tool_prompts__),
223
+ ]
224
+ )
225
+
226
+ await context.add_usage(
227
+ self,
228
+ context.llm_model,
229
+ message.usage_metadata,
230
+ "$finalize",
231
+ )
232
+
233
+ await context.notify(
234
+ {
235
+ "event": "tool",
236
+ "type": "finalize",
237
+ "status": "completed",
238
+ "data": self.__display_name__,
239
+ }
240
+ )
241
+ await context.add_response(self, message.text)
242
+
243
+ return ActionReturn.GO
244
+
168
245
  async def post(self, context: TContext) -> ActionReturn:
169
246
  """Execute post process."""
170
247
  return ActionReturn.GO
@@ -183,6 +260,13 @@ class Action(BaseModel, Generic[TContext]):
183
260
  system=self.__system_prompt__ or context.prompts[0]["content"] or "Not defined",
184
261
  )
185
262
 
263
+ def max_iteration_prompt(self, context: TContext) -> str:
264
+ """Get max iteration prompt."""
265
+ return apply_placeholders(
266
+ self.__max_iteration_prompt__ or DEFAULT_MAX_ITERATION_PROMPT,
267
+ system=self.__system_prompt__ or context.prompts[0]["content"] or "Not defined",
268
+ )
269
+
186
270
  async def get_child_actions(self, context: TContext) -> ChildActions:
187
271
  """Retrieve child Actions."""
188
272
  return {
@@ -204,35 +288,46 @@ class Action(BaseModel, Generic[TContext]):
204
288
  llm = context.llm.bind_tools(
205
289
  [await child._as_tool(context) if child.__has_as_tool__ else child for child in child_actions.values()],
206
290
  tool_choice=tool_choice,
291
+ parallel_tool_calls=not self.__first_tool_only__,
207
292
  )
208
293
  if self.__temperature__ is not None:
209
294
  llm = llm.with_config(configurable={"llm_temperature": self.__temperature__})
210
295
 
211
- max = len(context.prompts)
212
- if self.__max_tool_prompts__:
213
- min = max - self.__max_tool_prompts__
214
- min = 1 if min < 1 else min
215
- else:
216
- min = 1
217
-
218
296
  message = await llm.ainvoke(
219
297
  [
220
298
  {
221
299
  "content": self.child_selection_prompt(context, tool_choice),
222
300
  "role": "system",
223
301
  },
224
- *islice(context.prompts, min, max),
302
+ *context.shifted_prompts(self.__max_tool_prompts__),
225
303
  ]
226
304
  )
227
305
  await context.add_usage(
228
306
  self,
229
- context.llm.model_name,
307
+ context.llm_model,
230
308
  message.usage_metadata,
231
309
  "$tool",
232
310
  )
233
311
 
234
- next_actions = [child_actions[call["name"]](**call["args"]) for call in message.tool_calls]
235
-
312
+ next_actions: list[Action] = []
313
+ for call in message.tool_calls:
314
+ child_action = child_actions[call["name"]]
315
+ try:
316
+ next_actions.append(child_action(**call["args"]))
317
+ except Exception as error:
318
+ if self.__has_on_child_init_error__:
319
+ if (
320
+ error_message := await self.on_child_init_error(
321
+ context,
322
+ next_actions,
323
+ child_action,
324
+ call["args"],
325
+ error,
326
+ )
327
+ ) is not None:
328
+ return [], error_message
329
+ else:
330
+ raise error
236
331
  return next_actions, message.text
237
332
 
238
333
  async def execute(self, context: TContext, parent: Action | None = None, append: bool = True) -> ActionReturn:
@@ -254,11 +349,14 @@ class Action(BaseModel, Generic[TContext]):
254
349
 
255
350
  if self.__max_child_iteration__:
256
351
  iteration = 0
257
- while iteration <= self.__max_child_iteration__:
352
+ while iteration < self.__max_child_iteration__:
258
353
  if (result := await self.execution(context)).is_break:
259
354
  break
260
355
  iteration += 1
261
- if result.is_end:
356
+ if result.is_end or (
357
+ iteration >= self.__max_child_iteration__
358
+ and (result := await self.on_max_iteration(context)).is_break
359
+ ):
262
360
  return result
263
361
  elif (result := await self.execution(context)).is_break:
264
362
  return result
@@ -355,15 +453,7 @@ class Action(BaseModel, Generic[TContext]):
355
453
 
356
454
  await context.add_usage(
357
455
  self,
358
- getattr(
359
- context.llm,
360
- "model_name",
361
- getattr(
362
- context.llm,
363
- "deployment_name",
364
- UNSPECIFIED,
365
- ),
366
- ),
456
+ context.llm_model,
367
457
  message.usage_metadata,
368
458
  "$fallback",
369
459
  )
@@ -501,7 +591,7 @@ async def graph(action: type[Action], allowed_actions: dict[str, bool] | None =
501
591
  """Retrieve Graph."""
502
592
  origin = f"{action.__module__}.{action.__qualname__}"
503
593
  await traverse(
504
- graph := Graph(origin=origin, nodes={origin}),
594
+ graph := Graph(origin=origin, nodes={origin}, edges=set()),
505
595
  action,
506
596
  allowed_actions,
507
597
  )
@@ -1,4 +1,4 @@
1
- from .common import ActionEntry as ActionEntry, ActionReturn as ActionReturn, ConcurrentBreakPoint as ConcurrentBreakPoint, Graph as Graph, Groups as Groups, ToolCall as ToolCall, UNSPECIFIED as UNSPECIFIED, UsageData as UsageData
1
+ from .common import ActionEntry as ActionEntry, ActionReturn as ActionReturn, ConcurrentBreakPoint as ConcurrentBreakPoint, Graph as Graph, Groups as Groups, ToolCall as ToolCall, UsageData as UsageData
2
2
  from .context import Context as Context
3
3
  from .utils import apply_placeholders as apply_placeholders, unwrap_exceptions as unwrap_exceptions, uuid as uuid
4
4
  from _typeshed import Incomplete
@@ -8,6 +8,7 @@ from typing import Any, Generic, TypeVar
8
8
 
9
9
  DEFAULT_ACTION: Incomplete
10
10
  DEFAULT_TOOL_CALL_PROMPT: Incomplete
11
+ DEFAULT_MAX_ITERATION_PROMPT: Incomplete
11
12
  TAction = TypeVar('TAction', bound='Action')
12
13
  TContext = TypeVar('TContext', bound='Context')
13
14
  T = TypeVar('T')
@@ -17,6 +18,7 @@ class Action(BaseModel, Generic[TContext]):
17
18
  __enabled__: bool
18
19
  __system_prompt__: str | None
19
20
  __tool_call_prompt__: str | None
21
+ __max_iteration_prompt__: str | None
20
22
  __temperature__: float | None
21
23
  __max_tool_prompts__: int | None
22
24
  __default_tool__ = DEFAULT_ACTION
@@ -24,6 +26,7 @@ class Action(BaseModel, Generic[TContext]):
24
26
  __concurrent__: bool
25
27
  __has_pre__: bool
26
28
  __has_fallback__: bool
29
+ __has_on_child_init_error__: bool
27
30
  __has_on_error__: bool
28
31
  __has_post__: bool
29
32
  __has_as_tool__: bool
@@ -41,10 +44,13 @@ class Action(BaseModel, Generic[TContext]):
41
44
  def __init_child_actions__(cls) -> None: ...
42
45
  async def pre(self, context: TContext) -> ActionReturn: ...
43
46
  async def fallback(self, context: TContext, content: str) -> ActionReturn: ...
47
+ async def on_child_init_error(self, context: TContext, next_actions: list['Action'], child_cls: type[Action], child_args: dict[str, Any], exception: Exception) -> str | None: ...
44
48
  async def on_error(self, context: TContext, exception: Exception, unwrapped_exceptions: Generator[Exception, None, None]) -> ActionReturn: ...
49
+ async def on_max_iteration(self, context: TContext) -> ActionReturn: ...
45
50
  async def post(self, context: TContext) -> ActionReturn: ...
46
51
  async def commit_context(self, parent: TContext, child: TContext) -> None: ...
47
52
  def child_selection_prompt(self, context: TContext, tool_choice: str) -> str: ...
53
+ def max_iteration_prompt(self, context: TContext) -> str: ...
48
54
  async def get_child_actions(self, context: TContext) -> ChildActions: ...
49
55
  async def child_selection(self, context: TContext, child_actions: ChildActions | None = None) -> tuple[list['Action'], str]: ...
50
56
  async def execute(self, context: TContext, parent: Action | None = None, append: bool = True) -> ActionReturn: ...
@@ -110,11 +110,13 @@ class Graph(BaseModel):
110
110
 
111
111
  if concurrent:
112
112
  connection = (
113
- f"ed{con}@--**{base}** : {alias}<br>*[concurrent]*-->" if alias else f"ed{con}@--*[concurrent]*-->"
113
+ f'ed{con}@--"`**{base}** : {alias}<br>*[concurrent]*`"-->'
114
+ if alias
115
+ else f'ed{con}@--"`*[concurrent]*`"-->'
114
116
  )
115
117
  con += 1
116
118
  else:
117
- connection = f"--**{base}** : {alias}-->" if alias else "-->"
119
+ connection = f'--"`**{base}** : {alias}`"-->' if alias else "-->"
118
120
  content += f"{source} {connection} {target}\n"
119
121
 
120
122
  constraints = (
@@ -1,7 +1,7 @@
1
1
  """Pybotchi Context."""
2
2
 
3
3
  from asyncio import Future, get_event_loop, new_event_loop
4
- from collections.abc import Callable, Coroutine, Iterable
4
+ from collections.abc import Callable, Coroutine, Iterable, Iterator
5
5
  from concurrent.futures import Executor
6
6
  from copy import deepcopy
7
7
  from functools import cached_property, partial
@@ -41,6 +41,30 @@ class Context(BaseModel, Generic[TLLM]):
41
41
  """Get base LLM."""
42
42
  return LLM.base()
43
43
 
44
+ @cached_property
45
+ def llm_model(self) -> str:
46
+ """Get base LLM Model."""
47
+ return getattr(
48
+ self.llm,
49
+ "model_name",
50
+ getattr(
51
+ self.llm,
52
+ "deployment_name",
53
+ UNSPECIFIED,
54
+ ),
55
+ )
56
+
57
+ def shifted_prompts(self, offset: int | None) -> Iterator[dict[str, Any]]:
58
+ """Get shifted prompts."""
59
+ max = len(self.prompts)
60
+ if offset:
61
+ min = max - offset
62
+ min = 1 if min < 1 else min
63
+ else:
64
+ min = 1
65
+
66
+ return islice(self.prompts, min, max)
67
+
44
68
  async def start(self, action: type[TAction], /, **kwargs: Any) -> tuple[TAction, ActionReturn]:
45
69
  """Start Action."""
46
70
  if not self.prompts or self.prompts[0]["role"] != ChatRole.SYSTEM:
@@ -2,7 +2,7 @@ from .action import Action as Action, ActionReturn as ActionReturn, T as T, TAct
2
2
  from .common import ChatRole as ChatRole, ToolCall as ToolCall, UNSPECIFIED as UNSPECIFIED, UsageMetadata as UsageMetadata
3
3
  from .llm import LLM as LLM
4
4
  from asyncio import Future
5
- from collections.abc import Callable as Callable, Coroutine
5
+ from collections.abc import Callable as Callable, Coroutine, Iterator
6
6
  from concurrent.futures import Executor
7
7
  from functools import cached_property as cached_property
8
8
  from langchain_core.language_models.chat_models import BaseChatModel
@@ -24,6 +24,9 @@ class Context(BaseModel, Generic[TLLM]):
24
24
  parent: Self | None
25
25
  @cached_property
26
26
  def llm(self) -> TLLM: ...
27
+ @cached_property
28
+ def llm_model(self) -> str: ...
29
+ def shifted_prompts(self, offset: int | None) -> Iterator[dict[str, Any]]: ...
27
30
  async def start(self, action: type[TAction], /, **kwargs: Any) -> tuple[TAction, ActionReturn]: ...
28
31
  def check_self_recursion(self, action: Action) -> bool: ...
29
32
  async def merge_to_usages(self, model: str, usage: UsageMetadata) -> None: ...
@@ -4,7 +4,7 @@ from asyncio import run
4
4
  from importlib.resources import files
5
5
  from importlib.util import module_from_spec, spec_from_file_location
6
6
  from inspect import getmembers, isclass
7
- from multiprocessing import Process, cpu_count
7
+ from multiprocessing import Process, cpu_count, current_process
8
8
  from os import getenv
9
9
  from pathlib import Path
10
10
  from signal import SIGINT, SIGTERM, signal
@@ -96,10 +96,12 @@ async def serve(
96
96
  server.add_insecure_port(address)
97
97
  await server.start()
98
98
 
99
- echo(f"# Agent Path: {path}")
100
- echo(f"# Agent Handler: {grpc_handler.__name__}")
101
- echo(f"# gRPC server running on {address}")
102
- echo("#-------------------------------------------------------#")
99
+ process = current_process()
100
+ echo(
101
+ f"# Agent Process: {process.name} [{process.pid}]\n"
102
+ f"# Agent Handler: {grpc_handler.__name__}\n"
103
+ "#-------------------------------------------------------#"
104
+ )
103
105
  await server.wait_for_termination()
104
106
 
105
107
 
@@ -191,7 +193,6 @@ def main(
191
193
  else:
192
194
  _certificate_chain = None
193
195
 
194
- echo(f"# Agent Path: {path}")
195
196
  echo(f"# Starting {workers} worker(s) on {host}:{port}")
196
197
  echo("#-------------------------------------------------------#")
197
198
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pybotchi"
3
- version = "3.2.0"
3
+ version = "3.4.1"
4
4
  description = "A deterministic, intent-based AI agent builder."
5
5
  authors = ["Alexie (Boyong) Madolid <madolid.alexie@gmail.com>"]
6
6
  readme = "README.md"
@@ -31,13 +31,13 @@ grpcio-tools = { version = ">=1.76.0", optional = true }
31
31
  aiofiles = { version = ">=25.1.0", optional = true }
32
32
 
33
33
  [tool.poetry.group.dev.dependencies]
34
- python-dotenv = "1.1.1"
35
- mypy = "1.19.1"
36
- ruff = "0.14.10"
37
- pre-commit = "4.5.1"
38
- types-protobuf = "6.32.1.20251105"
39
- types-aiofiles = "25.1.0.20251011"
40
- mypy-protobuf = "4.0.0"
34
+ python-dotenv = "1.2.2"
35
+ mypy = "2.1.0"
36
+ ruff = "0.15.13"
37
+ pre-commit = "4.6.0"
38
+ types-protobuf = "7.34.1.20260518"
39
+ types-aiofiles = "25.1.0.20260518"
40
+ mypy-protobuf = "5.1.0"
41
41
 
42
42
  # for examples
43
43
  langchain-openai = ">=0.3.15"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes