Skip to content

Commit 1e6411f

Browse files
authored
v4.0.0b3 (#232)
New Features: 1. Support MCP as Tools 2. Auto Func Decorator 3. Use system message to manage in-frame events 4. Streaming debug logs (new Debug Console CUI still need to be optimized) Bug fix: 1. Bugs of Tool Extension in v4.0.0v2 2. Httpx connection close too soon when request need a long time
1 parent 948f4f3 commit 1e6411f

39 files changed

+1310
-354
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<img width="640" alt="image" src="https://github.com/user-attachments/assets/c645d031-c8b0-4dba-a515-9d7a4b0a6881" />
22

3-
# Agently 4 (v4.0.0.Beta2)
3+
# Agently 4 (v4.0.0.Beta3)
44

55
[English Introduction](https://github.com/AgentEra/Agently/blob/main/README.md) | [中文介绍](https://github.com/AgentEra/Agently/blob/main/README_CN.md)
66

@@ -42,7 +42,7 @@ Agently is a Python-based framework for building GenAI applications. You can ins
4242
Install via pip:
4343

4444
```shell
45-
pip install agently==4.0.0b2
45+
pip install agently==4.0.0b3
4646
```
4747

4848
⚠️: Version specifier is required during beta testing.

README_CN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<img width="640" alt="image" src="https://github.com/user-attachments/assets/c645d031-c8b0-4dba-a515-9d7a4b0a6881" />
22

3-
# Agently 4 (v4.0.0.Beta2)
3+
# Agently 4 (v4.0.0.Beta3)
44

55
[English Introduction](https://github.com/AgentEra/Agently/blob/main/README.md) | [中文介绍](https://github.com/AgentEra/Agently/blob/main/README_CN.md)
66

@@ -41,7 +41,7 @@ Agently GenAI应用开发框架目前提供在Python语言中可用的包,开
4141
使用pip安装:
4242

4343
```shell
44-
pip install agently==4.0.0b2
44+
pip install agently==4.0.0b3
4545
```
4646

4747
⚠️: 公测阶段必须携带版本号参数

agently/_default_init.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ def _load_default_settings(settings: "Settings"):
5252

5353

5454
def _hook_default_event_handlers(event_center: "EventCenter"):
55+
from agently.builtins.hookers.SystemMessageHooker import SystemMessageHooker
56+
57+
event_center.register_hooker_plugin(SystemMessageHooker)
58+
5559
from agently.builtins.hookers.PureLoggerHooker import PureLoggerHooker
5660

5761
event_center.register_hooker_plugin(PureLoggerHooker)

agently/_default_settings.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
# Copyright 2023-2025 AgentEra(Agently.Tech)
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
116
storage:
217
db_url: "sqlite+aiosqlite:///localstorage.db"
318
prompt:
@@ -13,6 +28,8 @@ response:
1328
runtime:
1429
raise_error: True
1530
raise_critical: True
31+
show_model_logs: False
32+
show_tool_logs: True
1633
plugins:
1734
ToolManager:
1835
activate: AgentlyToolManager

agently/_entrypoint.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,49 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import Any, TYPE_CHECKING, Literal
15+
from typing import Any, TYPE_CHECKING, Literal, overload
1616
from agently.base import settings, plugin_manager, tool, event_center, logger, print_, async_print
1717
from agently.core import Prompt, ModelRequest, BaseAgent
1818

1919
if TYPE_CHECKING:
2020
from agently.types.data import MessageLevel, SerializableValue
2121

22+
settings.update_mappings(
23+
{
24+
"key_value_mappings": {
25+
"debug": {
26+
True: {
27+
"runtime.show_model_logs": True,
28+
"runtime.show_tool_logs": False,
29+
},
30+
False: {
31+
"runtime.show_model_logs": False,
32+
"runtime.show_tool_logs": True,
33+
},
34+
"silent": {
35+
"runtime.show_model_logs": False,
36+
"runtime.show_tool_logs": False,
37+
},
38+
}
39+
}
40+
}
41+
)
42+
2243
# Extensions Installation
2344
# BaseAgent + Extensions = Agent
24-
from agently.builtins.agent_extensions import ToolExtension
45+
from agently.builtins.agent_extensions import (
46+
ToolExtension,
47+
KeyWaiterExtension,
48+
AutoFuncExtension,
49+
)
2550

2651

27-
class Agent(ToolExtension, BaseAgent): ...
52+
class Agent(
53+
ToolExtension,
54+
KeyWaiterExtension,
55+
AutoFuncExtension,
56+
BaseAgent,
57+
): ...
2858

2959

3060
class AgentlyMain:
@@ -51,6 +81,10 @@ def set_log_level(self, log_level: "MessageLevel"):
5181
self.logger.setLevel(log_level)
5282
return self
5383

84+
@overload
85+
def set_settings(self, key: Literal["debug"], value: Literal[True, False, "silent"]): ...
86+
@overload
87+
def set_settings(self, key: str, value: "SerializableValue"): ...
5488
def set_settings(self, key: str, value: "SerializableValue"):
5589
self.settings.set_settings(key, value)
5690
return self
@@ -66,7 +100,7 @@ def create_request(self, name: str | None = None) -> ModelRequest:
66100
return ModelRequest(
67101
self.plugin_manager,
68102
parent_settings=self.settings,
69-
request_name=name,
103+
agent_name=name,
70104
)
71105

72106
def create_agent(self, name: str | None = None) -> Agent:

agently/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
_load_default_plugins(plugin_manager)
3030
event_center = EventCenter()
3131
_hook_default_event_handlers(event_center)
32+
async_system_message = event_center.async_system_message
33+
system_message = event_center.system_message
3234
logger = create_logger()
3335
tool = Tool(plugin_manager, settings)
3436
_agently_messenger = event_center.create_messenger("Agently")
@@ -53,4 +55,4 @@ async def async_print(content: Any, *args):
5355
await _agently_messenger.async_message(content_text, event="log")
5456

5557

56-
__all__ = ["settings", "plugin_manager", "event_center", "logger"]
58+
__all__ = ["settings", "plugin_manager", "event_center", "async_system_message", "system_message", "logger"]
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright 2023-2025 AgentEra(Agently.Tech)
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import inspect
17+
18+
from typing import Callable
19+
20+
from agently.core import BaseAgent
21+
22+
23+
class AutoFuncExtension(BaseAgent):
24+
def auto_func(self, func: Callable):
25+
if inspect.iscoroutinefunction(func):
26+
27+
async def async_wrapper(*args, **kwargs):
28+
signature = inspect.signature(func)
29+
arguments = signature.bind(*args, **kwargs)
30+
arguments.apply_defaults()
31+
input_dict = {}
32+
for param in signature.parameters:
33+
input_dict.update({param: arguments.arguments[param]})
34+
# generate instruction
35+
instruction = inspect.getdoc(func)
36+
# generate output dict
37+
output_dict = signature.return_annotation
38+
return await self.input(input_dict).instruct(instruction).output(output_dict).async_start()
39+
40+
return async_wrapper
41+
elif (
42+
inspect.isfunction(func) and not inspect.isasyncgenfunction(func) and not inspect.isgeneratorfunction(func)
43+
):
44+
45+
def wrapper(*args, **kwargs):
46+
signature = inspect.signature(func)
47+
arguments = signature.bind(*args, **kwargs)
48+
arguments.apply_defaults()
49+
input_dict = {}
50+
for param in signature.parameters:
51+
input_dict.update({param: arguments.arguments[param]})
52+
# generate instruction
53+
instruction = inspect.getdoc(func)
54+
# generate output dict
55+
output_dict = signature.return_annotation
56+
return self.input(input_dict).instruct(instruction).output(output_dict).start()
57+
58+
return wrapper
59+
else:
60+
raise TypeError(f"Error: Cannot decorate generator as an automatic function.\nFunction: { func }")
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Copyright 2023-2025 AgentEra(Agently.Tech)
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import asyncio
17+
18+
from typing import Any, Callable, TYPE_CHECKING
19+
from agently.core import BaseAgent
20+
from agently.utils import FunctionShifter, GeneratorConsumer
21+
22+
if TYPE_CHECKING:
23+
from agently.core.ModelRequest import ModelResponse
24+
from agently.types.data import AgentlyModelResponseEvent
25+
26+
27+
class KeyWaiterExtension(BaseAgent):
28+
def __init__(self, *args, **kwargs):
29+
super().__init__(*args, **kwargs)
30+
self.__when_handlers = {}
31+
32+
self.get_key_result = FunctionShifter.syncify(self.async_get_key_result)
33+
34+
def __check_keys_in_output(
35+
self,
36+
keys: list[str],
37+
must_in_prompt: bool = False,
38+
):
39+
if "output" not in self.request.prompt or not self.request.prompt["output"]:
40+
raise NotImplementedError(
41+
f"Error: Cannot find keys without 'output' prompt definition.\nPrompt: { self.request.prompt }"
42+
)
43+
if must_in_prompt:
44+
no_found_keys = []
45+
for key in keys:
46+
if key not in self.request.prompt:
47+
no_found_keys.append(key)
48+
if no_found_keys:
49+
raise NotImplementedError(
50+
f"Error: Cannot wait key/keys { no_found_keys } because they were not in 'output' prompt\nPrompt: { self.request.prompt }"
51+
)
52+
53+
def __get_consumer(self):
54+
response = self.get_response()
55+
return GeneratorConsumer(response.get_async_generator(content="instant"))
56+
57+
async def async_get_key_result(
58+
self,
59+
key: str,
60+
*,
61+
must_in_prompt: bool = False,
62+
):
63+
self.__check_keys_in_output(
64+
[key],
65+
must_in_prompt=must_in_prompt,
66+
)
67+
consumer = self.__get_consumer()
68+
69+
async for data in consumer.get_async_generator():
70+
if key == data.path and data.is_complete:
71+
return data.value
72+
73+
async def async_wait_keys(
74+
self,
75+
keys: list[str],
76+
*,
77+
must_in_prompt: bool = False,
78+
):
79+
self.__check_keys_in_output(
80+
keys,
81+
must_in_prompt=must_in_prompt,
82+
)
83+
consumer = self.__get_consumer()
84+
85+
async for data in consumer.get_async_generator():
86+
if data.path in keys and data.is_complete:
87+
yield data.path, data.value
88+
89+
def wait_keys(
90+
self,
91+
keys: list[str],
92+
*,
93+
must_in_prompt: bool = False,
94+
):
95+
self.__check_keys_in_output(
96+
keys,
97+
must_in_prompt=must_in_prompt,
98+
)
99+
consumer = self.__get_consumer()
100+
101+
for data in consumer.get_generator():
102+
if data.path in keys and data.is_complete:
103+
yield data.path, data.value
104+
105+
def when_key(self, key: str, handler: Callable[[Any], Any]):
106+
if key not in self.__when_handlers:
107+
self.__when_handlers.update({key: []})
108+
self.__when_handlers[key].append(handler)
109+
return self
110+
111+
async def async_start_waiter(self, *, must_in_prompt: bool = False):
112+
if not self.__when_handlers:
113+
raise NotImplementedError(
114+
f"Use .when_key(<key>, <handler>) to provide at least one key handler before .start_waiter()."
115+
)
116+
handler_keys = list(self.__when_handlers.keys())
117+
self.__check_keys_in_output(
118+
handler_keys,
119+
must_in_prompt=must_in_prompt,
120+
)
121+
consumer = self.__get_consumer()
122+
tasks = []
123+
124+
async def handler_wrapper(path: str, value: Any, handler: Callable[[Any], Any]) -> Any:
125+
return path, value, await FunctionShifter.asyncify(handler)(value)
126+
127+
async for data in consumer.get_async_generator():
128+
if data.path in handler_keys and data.is_complete:
129+
for handler in self.__when_handlers[data.path]:
130+
tasks.append(asyncio.create_task(handler_wrapper(data.path, data.value, handler)))
131+
132+
self.request.prompt.clear()
133+
134+
return await asyncio.gather(*tasks)
135+
136+
def start_waiter(self, *, must_in_prompt: bool = False):
137+
if not self.__when_handlers:
138+
raise NotImplementedError(
139+
f"Use .when_key(<key>, <handler>) to provide at least one key handler before .start_waiter()."
140+
)
141+
handler_keys = list(self.__when_handlers.keys())
142+
self.__check_keys_in_output(
143+
handler_keys,
144+
must_in_prompt=must_in_prompt,
145+
)
146+
consumer = self.__get_consumer()
147+
results = []
148+
149+
for data in consumer.get_generator():
150+
if data.path in handler_keys and data.is_complete:
151+
for handler in self.__when_handlers[data.path]:
152+
results.append(
153+
(
154+
data.path,
155+
data.value,
156+
FunctionShifter.syncify(
157+
handler,
158+
)(data.value),
159+
)
160+
)
161+
162+
self.request.prompt.clear()
163+
164+
return results

0 commit comments

Comments
 (0)