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.
- {pybotchi-3.2.0 → pybotchi-3.4.1}/PKG-INFO +18 -17
- {pybotchi-3.2.0 → pybotchi-3.4.1}/README.md +17 -16
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/action.py +115 -25
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/action.pyi +7 -1
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/common.py +4 -2
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/context.py +25 -1
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/context.pyi +4 -1
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/cli.py +7 -6
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pyproject.toml +8 -8
- {pybotchi-3.2.0 → pybotchi-3.4.1}/LICENSE +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/__init__.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/__init__.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/common.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/__init__.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/__init__.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/action.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/action.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/cli.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/common.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/common.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/context.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/context.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/exception.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/exception.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/handler.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/handler.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi.proto +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi_pb2.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi_pb2.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi_pb2_grpc.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/pybotchi_pb2_grpc.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/utils.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/grpc/utils.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/llm.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/llm.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/__init__.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/__init__.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/action.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/action.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/common.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/common.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/context.py +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/mcp/context.pyi +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/py.typed +0 -0
- {pybotchi-3.2.0 → pybotchi-3.4.1}/pybotchi/utils.py +0 -0
- {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.
|
|
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
|
|
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(
|
|
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
|
|
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[-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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
|

|
|
@@ -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
|
|
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(
|
|
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
|
|
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[-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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
|

|
|
@@ -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
|
-
*
|
|
302
|
+
*context.shifted_prompts(self.__max_tool_prompts__),
|
|
225
303
|
]
|
|
226
304
|
)
|
|
227
305
|
await context.add_usage(
|
|
228
306
|
self,
|
|
229
|
-
context.
|
|
307
|
+
context.llm_model,
|
|
230
308
|
message.usage_metadata,
|
|
231
309
|
"$tool",
|
|
232
310
|
)
|
|
233
311
|
|
|
234
|
-
next_actions
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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"
|
|
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
|
-
|
|
100
|
-
echo(
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
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.
|
|
35
|
-
mypy = "1.
|
|
36
|
-
ruff = "0.
|
|
37
|
-
pre-commit = "4.
|
|
38
|
-
types-protobuf = "
|
|
39
|
-
types-aiofiles = "25.1.0.
|
|
40
|
-
mypy-protobuf = "
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|