agentscope-runtime 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentscope_runtime/__init__.py +4 -0
- agentscope_runtime/engine/__init__.py +9 -0
- agentscope_runtime/engine/agents/__init__.py +2 -0
- agentscope_runtime/engine/agents/agentscope_agent/__init__.py +6 -0
- agentscope_runtime/engine/agents/agentscope_agent/agent.py +342 -0
- agentscope_runtime/engine/agents/agentscope_agent/hooks.py +156 -0
- agentscope_runtime/engine/agents/agno_agent.py +220 -0
- agentscope_runtime/engine/agents/base_agent.py +29 -0
- agentscope_runtime/engine/agents/langgraph_agent.py +59 -0
- agentscope_runtime/engine/agents/llm_agent.py +51 -0
- agentscope_runtime/engine/deployers/__init__.py +3 -0
- agentscope_runtime/engine/deployers/adapter/__init__.py +0 -0
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +2 -0
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_adapter_utils.py +425 -0
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_agent_adapter.py +69 -0
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +60 -0
- agentscope_runtime/engine/deployers/adapter/protocol_adapter.py +24 -0
- agentscope_runtime/engine/deployers/base.py +17 -0
- agentscope_runtime/engine/deployers/local_deployer.py +586 -0
- agentscope_runtime/engine/helpers/helper.py +127 -0
- agentscope_runtime/engine/llms/__init__.py +3 -0
- agentscope_runtime/engine/llms/base_llm.py +60 -0
- agentscope_runtime/engine/llms/qwen_llm.py +47 -0
- agentscope_runtime/engine/misc/__init__.py +0 -0
- agentscope_runtime/engine/runner.py +186 -0
- agentscope_runtime/engine/schemas/__init__.py +0 -0
- agentscope_runtime/engine/schemas/agent_schemas.py +551 -0
- agentscope_runtime/engine/schemas/context.py +54 -0
- agentscope_runtime/engine/services/__init__.py +9 -0
- agentscope_runtime/engine/services/base.py +77 -0
- agentscope_runtime/engine/services/context_manager.py +129 -0
- agentscope_runtime/engine/services/environment_manager.py +50 -0
- agentscope_runtime/engine/services/manager.py +174 -0
- agentscope_runtime/engine/services/memory_service.py +270 -0
- agentscope_runtime/engine/services/sandbox_service.py +198 -0
- agentscope_runtime/engine/services/session_history_service.py +256 -0
- agentscope_runtime/engine/tracing/__init__.py +40 -0
- agentscope_runtime/engine/tracing/base.py +309 -0
- agentscope_runtime/engine/tracing/local_logging_handler.py +356 -0
- agentscope_runtime/engine/tracing/tracing_metric.py +69 -0
- agentscope_runtime/engine/tracing/wrapper.py +321 -0
- agentscope_runtime/sandbox/__init__.py +14 -0
- agentscope_runtime/sandbox/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/base/__init__.py +0 -0
- agentscope_runtime/sandbox/box/base/base_sandbox.py +37 -0
- agentscope_runtime/sandbox/box/base/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/browser/__init__.py +0 -0
- agentscope_runtime/sandbox/box/browser/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +176 -0
- agentscope_runtime/sandbox/box/dummy/__init__.py +0 -0
- agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +26 -0
- agentscope_runtime/sandbox/box/filesystem/__init__.py +0 -0
- agentscope_runtime/sandbox/box/filesystem/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +87 -0
- agentscope_runtime/sandbox/box/sandbox.py +115 -0
- agentscope_runtime/sandbox/box/shared/__init__.py +0 -0
- agentscope_runtime/sandbox/box/shared/app.py +44 -0
- agentscope_runtime/sandbox/box/shared/dependencies/__init__.py +5 -0
- agentscope_runtime/sandbox/box/shared/dependencies/deps.py +22 -0
- agentscope_runtime/sandbox/box/shared/routers/__init__.py +12 -0
- agentscope_runtime/sandbox/box/shared/routers/generic.py +173 -0
- agentscope_runtime/sandbox/box/shared/routers/mcp.py +207 -0
- agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +153 -0
- agentscope_runtime/sandbox/box/shared/routers/runtime_watcher.py +187 -0
- agentscope_runtime/sandbox/box/shared/routers/workspace.py +325 -0
- agentscope_runtime/sandbox/box/training_box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/training_box/base.py +120 -0
- agentscope_runtime/sandbox/box/training_box/env_service.py +752 -0
- agentscope_runtime/sandbox/box/training_box/environments/__init__.py +0 -0
- agentscope_runtime/sandbox/box/training_box/environments/appworld/appworld_env.py +987 -0
- agentscope_runtime/sandbox/box/training_box/registry.py +54 -0
- agentscope_runtime/sandbox/box/training_box/src/trajectory.py +278 -0
- agentscope_runtime/sandbox/box/training_box/training_box.py +219 -0
- agentscope_runtime/sandbox/build.py +213 -0
- agentscope_runtime/sandbox/client/__init__.py +5 -0
- agentscope_runtime/sandbox/client/http_client.py +527 -0
- agentscope_runtime/sandbox/client/training_client.py +265 -0
- agentscope_runtime/sandbox/constant.py +5 -0
- agentscope_runtime/sandbox/custom/__init__.py +16 -0
- agentscope_runtime/sandbox/custom/custom_sandbox.py +40 -0
- agentscope_runtime/sandbox/custom/example.py +37 -0
- agentscope_runtime/sandbox/enums.py +68 -0
- agentscope_runtime/sandbox/manager/__init__.py +4 -0
- agentscope_runtime/sandbox/manager/collections/__init__.py +22 -0
- agentscope_runtime/sandbox/manager/collections/base_mapping.py +20 -0
- agentscope_runtime/sandbox/manager/collections/base_queue.py +25 -0
- agentscope_runtime/sandbox/manager/collections/base_set.py +25 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +22 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_queue.py +28 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_set.py +27 -0
- agentscope_runtime/sandbox/manager/collections/redis_mapping.py +26 -0
- agentscope_runtime/sandbox/manager/collections/redis_queue.py +27 -0
- agentscope_runtime/sandbox/manager/collections/redis_set.py +23 -0
- agentscope_runtime/sandbox/manager/container_clients/__init__.py +8 -0
- agentscope_runtime/sandbox/manager/container_clients/base_client.py +39 -0
- agentscope_runtime/sandbox/manager/container_clients/docker_client.py +170 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +694 -0
- agentscope_runtime/sandbox/manager/server/__init__.py +0 -0
- agentscope_runtime/sandbox/manager/server/app.py +194 -0
- agentscope_runtime/sandbox/manager/server/config.py +68 -0
- agentscope_runtime/sandbox/manager/server/models.py +17 -0
- agentscope_runtime/sandbox/manager/storage/__init__.py +10 -0
- agentscope_runtime/sandbox/manager/storage/data_storage.py +16 -0
- agentscope_runtime/sandbox/manager/storage/local_storage.py +44 -0
- agentscope_runtime/sandbox/manager/storage/oss_storage.py +89 -0
- agentscope_runtime/sandbox/manager/utils.py +78 -0
- agentscope_runtime/sandbox/mcp_server.py +192 -0
- agentscope_runtime/sandbox/model/__init__.py +12 -0
- agentscope_runtime/sandbox/model/api.py +16 -0
- agentscope_runtime/sandbox/model/container.py +72 -0
- agentscope_runtime/sandbox/model/manager_config.py +158 -0
- agentscope_runtime/sandbox/registry.py +129 -0
- agentscope_runtime/sandbox/tools/__init__.py +12 -0
- agentscope_runtime/sandbox/tools/base/__init__.py +8 -0
- agentscope_runtime/sandbox/tools/base/tool.py +52 -0
- agentscope_runtime/sandbox/tools/browser/__init__.py +57 -0
- agentscope_runtime/sandbox/tools/browser/tool.py +597 -0
- agentscope_runtime/sandbox/tools/filesystem/__init__.py +32 -0
- agentscope_runtime/sandbox/tools/filesystem/tool.py +319 -0
- agentscope_runtime/sandbox/tools/function_tool.py +321 -0
- agentscope_runtime/sandbox/tools/mcp_tool.py +191 -0
- agentscope_runtime/sandbox/tools/sandbox_tool.py +104 -0
- agentscope_runtime/sandbox/tools/tool.py +123 -0
- agentscope_runtime/sandbox/tools/utils.py +68 -0
- agentscope_runtime/version.py +2 -0
- agentscope_runtime-0.1.0.dist-info/METADATA +327 -0
- agentscope_runtime-0.1.0.dist-info/RECORD +131 -0
- agentscope_runtime-0.1.0.dist-info/WHEEL +5 -0
- agentscope_runtime-0.1.0.dist-info/entry_points.txt +4 -0
- agentscope_runtime-0.1.0.dist-info/licenses/LICENSE +202 -0
- agentscope_runtime-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# pylint:disable=too-many-nested-blocks, too-many-branches, too-many-statements
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
import uuid
|
|
6
|
+
from functools import partial
|
|
7
|
+
from typing import Optional, Type
|
|
8
|
+
|
|
9
|
+
from agentscope.agent import ReActAgent
|
|
10
|
+
from agentscope.formatter import (
|
|
11
|
+
FormatterBase,
|
|
12
|
+
DashScopeChatFormatter,
|
|
13
|
+
OpenAIChatFormatter,
|
|
14
|
+
AnthropicChatFormatter,
|
|
15
|
+
OllamaChatFormatter,
|
|
16
|
+
GeminiChatFormatter,
|
|
17
|
+
)
|
|
18
|
+
from agentscope.model import (
|
|
19
|
+
ChatModelBase,
|
|
20
|
+
DashScopeChatModel,
|
|
21
|
+
OpenAIChatModel,
|
|
22
|
+
AnthropicChatModel,
|
|
23
|
+
OllamaChatModel,
|
|
24
|
+
GeminiChatModel,
|
|
25
|
+
)
|
|
26
|
+
from agentscope.memory import InMemoryMemory
|
|
27
|
+
from agentscope.message import Msg
|
|
28
|
+
from agentscope import setup_logger
|
|
29
|
+
from agentscope.tool import (
|
|
30
|
+
Toolkit,
|
|
31
|
+
ToolResponse,
|
|
32
|
+
)
|
|
33
|
+
from agentscope.tool._toolkit import RegisteredToolFunction
|
|
34
|
+
|
|
35
|
+
from .hooks import (
|
|
36
|
+
pre_speak_msg_buffer_hook,
|
|
37
|
+
get_msg_instances,
|
|
38
|
+
clear_msg_instances,
|
|
39
|
+
run_async_in_thread,
|
|
40
|
+
)
|
|
41
|
+
from ...agents import Agent
|
|
42
|
+
from ...schemas.agent_schemas import (
|
|
43
|
+
Message,
|
|
44
|
+
TextContent,
|
|
45
|
+
DataContent,
|
|
46
|
+
FunctionCall,
|
|
47
|
+
FunctionCallOutput,
|
|
48
|
+
MessageType,
|
|
49
|
+
RunStatus,
|
|
50
|
+
)
|
|
51
|
+
from ...schemas.context import Context
|
|
52
|
+
|
|
53
|
+
# Disable logging from agentscope
|
|
54
|
+
setup_logger(level="CRITICAL")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AgentScopeContextAdapter:
|
|
58
|
+
def __init__(self, context: Context, attr: dict):
|
|
59
|
+
self.context = context
|
|
60
|
+
self.attr = attr
|
|
61
|
+
|
|
62
|
+
# Adapted attribute
|
|
63
|
+
self.toolkit = None
|
|
64
|
+
self.model = None
|
|
65
|
+
self.memory = None
|
|
66
|
+
self.new_message = None
|
|
67
|
+
|
|
68
|
+
async def initialize(self):
|
|
69
|
+
self.model, self.formatter = await self.adapt_model()
|
|
70
|
+
self.memory = await self.adapt_memory()
|
|
71
|
+
self.new_message = await self.adapt_new_message()
|
|
72
|
+
self.toolkit = await self.adapt_tools()
|
|
73
|
+
|
|
74
|
+
async def adapt_memory(self):
|
|
75
|
+
memory = self.attr["agent_config"].get("memory", InMemoryMemory())
|
|
76
|
+
messages = []
|
|
77
|
+
|
|
78
|
+
# Build context
|
|
79
|
+
for msg in self.context.session.messages[:-1]: # Exclude the last one
|
|
80
|
+
messages.append(AgentScopeContextAdapter.converter(msg))
|
|
81
|
+
|
|
82
|
+
memory.load_state_dict({"content": messages})
|
|
83
|
+
|
|
84
|
+
return memory
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def converter(message: Message):
|
|
88
|
+
if message.role not in ["user", "system", "assistant"]:
|
|
89
|
+
role_label = "user"
|
|
90
|
+
else:
|
|
91
|
+
role_label = message.role
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"name": message.role,
|
|
95
|
+
"role": role_label,
|
|
96
|
+
"content": message.content[0].text if message.content else "",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async def adapt_new_message(self):
|
|
100
|
+
last_message = self.context.session.messages[-1]
|
|
101
|
+
return Msg(**AgentScopeContextAdapter.converter(last_message))
|
|
102
|
+
|
|
103
|
+
async def adapt_model(self):
|
|
104
|
+
model = self.attr["model"]
|
|
105
|
+
formatter = self.attr["agent_config"].get("formatter")
|
|
106
|
+
if formatter and isinstance(formatter, FormatterBase):
|
|
107
|
+
return model, formatter
|
|
108
|
+
|
|
109
|
+
if isinstance(model, OpenAIChatModel):
|
|
110
|
+
formatter = OpenAIChatFormatter()
|
|
111
|
+
elif isinstance(model, DashScopeChatModel):
|
|
112
|
+
formatter = DashScopeChatFormatter()
|
|
113
|
+
elif isinstance(model, AnthropicChatModel):
|
|
114
|
+
formatter = AnthropicChatFormatter()
|
|
115
|
+
elif isinstance(model, OllamaChatModel):
|
|
116
|
+
formatter = OllamaChatFormatter()
|
|
117
|
+
elif isinstance(model, GeminiChatModel):
|
|
118
|
+
formatter = GeminiChatFormatter()
|
|
119
|
+
|
|
120
|
+
return model, formatter
|
|
121
|
+
|
|
122
|
+
async def adapt_tools(self):
|
|
123
|
+
def func_wrapper(func, **kwargs):
|
|
124
|
+
func_res = func(**kwargs)
|
|
125
|
+
return ToolResponse(
|
|
126
|
+
content=func_res["content"],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
toolkit = self.attr["agent_config"].get("toolkit", Toolkit())
|
|
130
|
+
tools = self.attr["tools"]
|
|
131
|
+
|
|
132
|
+
# in case, tools is None and tools == []
|
|
133
|
+
if not tools:
|
|
134
|
+
return toolkit
|
|
135
|
+
|
|
136
|
+
if self.context.activate_tools:
|
|
137
|
+
# Only add activated tool
|
|
138
|
+
activated_tools = self.context.activate_tools
|
|
139
|
+
else:
|
|
140
|
+
# Lazy import
|
|
141
|
+
from ....sandbox.tools.utils import setup_tools
|
|
142
|
+
|
|
143
|
+
activated_tools = setup_tools(
|
|
144
|
+
tools=self.attr["tools"],
|
|
145
|
+
environment_manager=self.context.environment_manager,
|
|
146
|
+
session_id=self.context.session.id,
|
|
147
|
+
user_id=self.context.session.user_id,
|
|
148
|
+
include_schemas=False,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for tool in activated_tools:
|
|
152
|
+
function = RegisteredToolFunction(
|
|
153
|
+
name=tool.name,
|
|
154
|
+
source="mcp_server",
|
|
155
|
+
mcp_name=tool.tool_type,
|
|
156
|
+
original_func=partial(
|
|
157
|
+
func_wrapper,
|
|
158
|
+
tool,
|
|
159
|
+
),
|
|
160
|
+
json_schema=tool.schema,
|
|
161
|
+
group="basic",
|
|
162
|
+
)
|
|
163
|
+
toolkit.tools[tool.name] = function
|
|
164
|
+
|
|
165
|
+
return toolkit
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class AgentScopeAgent(Agent):
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
name: str,
|
|
172
|
+
model: ChatModelBase,
|
|
173
|
+
tools=None,
|
|
174
|
+
agent_config=None,
|
|
175
|
+
agent_builder: Optional[Type[ReActAgent]] = ReActAgent,
|
|
176
|
+
):
|
|
177
|
+
super().__init__(name=name, agent_config=agent_config)
|
|
178
|
+
assert isinstance(
|
|
179
|
+
model,
|
|
180
|
+
ChatModelBase,
|
|
181
|
+
), "model must be a subclass of ChatModelBase in AgentScope"
|
|
182
|
+
|
|
183
|
+
# Set default agent_builder
|
|
184
|
+
if agent_builder is None:
|
|
185
|
+
agent_builder = ReActAgent
|
|
186
|
+
|
|
187
|
+
assert issubclass(
|
|
188
|
+
agent_builder,
|
|
189
|
+
ReActAgent,
|
|
190
|
+
), "agent_builder must be a subclass of AgentBase in AgentScope"
|
|
191
|
+
|
|
192
|
+
# Replace name if not exists
|
|
193
|
+
self.agent_config["name"] = self.agent_config.get("name") or name
|
|
194
|
+
|
|
195
|
+
self._attr = {
|
|
196
|
+
"model": model,
|
|
197
|
+
"tools": tools,
|
|
198
|
+
"agent_config": self.agent_config,
|
|
199
|
+
"agent_builder": agent_builder,
|
|
200
|
+
}
|
|
201
|
+
self._agent = None
|
|
202
|
+
self.tools = tools
|
|
203
|
+
|
|
204
|
+
def copy(self) -> "AgentScopeAgent":
|
|
205
|
+
return AgentScopeAgent(**self._attr)
|
|
206
|
+
|
|
207
|
+
def build(self, as_context):
|
|
208
|
+
self._agent = self._attr["agent_builder"](
|
|
209
|
+
**self._attr["agent_config"],
|
|
210
|
+
model=as_context.model,
|
|
211
|
+
formatter=as_context.formatter,
|
|
212
|
+
memory=as_context.memory,
|
|
213
|
+
toolkit=as_context.toolkit,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
self._agent.register_instance_hook(
|
|
217
|
+
"pre_print",
|
|
218
|
+
"pre_speak_msg_buffer_hook",
|
|
219
|
+
pre_speak_msg_buffer_hook,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return self._agent
|
|
223
|
+
|
|
224
|
+
async def run(self, context):
|
|
225
|
+
as_context = AgentScopeContextAdapter(context=context, attr=self._attr)
|
|
226
|
+
await as_context.initialize()
|
|
227
|
+
|
|
228
|
+
# We should always build a new agent since the state is manage outside
|
|
229
|
+
# the agent
|
|
230
|
+
self._agent = self.build(as_context)
|
|
231
|
+
|
|
232
|
+
# Make the output a generator
|
|
233
|
+
thread_id = "pipeline" + str(uuid.uuid4())
|
|
234
|
+
try:
|
|
235
|
+
# Run the main function in a separate thread
|
|
236
|
+
thread = threading.Thread(
|
|
237
|
+
target=run_async_in_thread,
|
|
238
|
+
args=(self._agent.reply(msg=as_context.new_message),),
|
|
239
|
+
name=thread_id,
|
|
240
|
+
)
|
|
241
|
+
clear_msg_instances(thread_id=thread_id)
|
|
242
|
+
thread.start()
|
|
243
|
+
|
|
244
|
+
# Yield new Msg instances as they are logged
|
|
245
|
+
last_content = ""
|
|
246
|
+
|
|
247
|
+
for msg, msg_len in get_msg_instances(thread_id=thread_id):
|
|
248
|
+
if msg:
|
|
249
|
+
content = msg.content
|
|
250
|
+
|
|
251
|
+
if isinstance(content, str):
|
|
252
|
+
last_content = content
|
|
253
|
+
else:
|
|
254
|
+
for element in content:
|
|
255
|
+
if isinstance(element, str):
|
|
256
|
+
content = TextContent(text=element)
|
|
257
|
+
message = Message(
|
|
258
|
+
type=MessageType.MESSAGE,
|
|
259
|
+
role="assistant",
|
|
260
|
+
content=[content],
|
|
261
|
+
status=RunStatus.Completed,
|
|
262
|
+
)
|
|
263
|
+
yield message
|
|
264
|
+
elif isinstance(element, dict):
|
|
265
|
+
if element.get("type") == "text":
|
|
266
|
+
content = TextContent(
|
|
267
|
+
text=element.get("text"),
|
|
268
|
+
)
|
|
269
|
+
message = Message(
|
|
270
|
+
type=MessageType.MESSAGE,
|
|
271
|
+
role="assistant",
|
|
272
|
+
status=RunStatus.Completed,
|
|
273
|
+
content=[content],
|
|
274
|
+
)
|
|
275
|
+
yield message
|
|
276
|
+
elif element.get("type") == "tool_use":
|
|
277
|
+
json_str = json.dumps(element.get("input"))
|
|
278
|
+
data = DataContent(
|
|
279
|
+
data=FunctionCall(
|
|
280
|
+
call_id=element.get("id"),
|
|
281
|
+
name=element.get("name"),
|
|
282
|
+
arguments=json_str,
|
|
283
|
+
).model_dump(),
|
|
284
|
+
)
|
|
285
|
+
message = Message(
|
|
286
|
+
type=MessageType.PLUGIN_CALL,
|
|
287
|
+
role="assistant",
|
|
288
|
+
status=RunStatus.Completed,
|
|
289
|
+
content=[data],
|
|
290
|
+
)
|
|
291
|
+
yield message
|
|
292
|
+
elif element.get("type") == "tool_result":
|
|
293
|
+
data = DataContent(
|
|
294
|
+
data=FunctionCallOutput(
|
|
295
|
+
call_id=element.get("id"),
|
|
296
|
+
output=str(element.get("output")),
|
|
297
|
+
).model_dump(),
|
|
298
|
+
)
|
|
299
|
+
message = Message(
|
|
300
|
+
type=MessageType.PLUGIN_CALL_OUTPUT,
|
|
301
|
+
role="assistant",
|
|
302
|
+
status=RunStatus.Completed,
|
|
303
|
+
content=[data],
|
|
304
|
+
)
|
|
305
|
+
yield message
|
|
306
|
+
else:
|
|
307
|
+
message = Message(
|
|
308
|
+
type=MessageType.MESSAGE,
|
|
309
|
+
role="assistant",
|
|
310
|
+
status=RunStatus.Completed,
|
|
311
|
+
content=[
|
|
312
|
+
TextContent(text=f"{element}"),
|
|
313
|
+
],
|
|
314
|
+
)
|
|
315
|
+
yield message
|
|
316
|
+
|
|
317
|
+
# Break if the thread is dead and no more messages are expected
|
|
318
|
+
if not thread.is_alive() and msg_len == 0:
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
if last_content:
|
|
322
|
+
content = TextContent(text=last_content)
|
|
323
|
+
message = Message(
|
|
324
|
+
type=MessageType.MESSAGE,
|
|
325
|
+
role="assistant",
|
|
326
|
+
content=[content],
|
|
327
|
+
status=RunStatus.Completed,
|
|
328
|
+
)
|
|
329
|
+
yield message
|
|
330
|
+
|
|
331
|
+
# Wait for the function to finish
|
|
332
|
+
thread.join()
|
|
333
|
+
finally:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
async def run_async(
|
|
337
|
+
self,
|
|
338
|
+
context,
|
|
339
|
+
**kwargs,
|
|
340
|
+
):
|
|
341
|
+
async for event in self.run(context):
|
|
342
|
+
yield event
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
""" Hooks for stream output """
|
|
3
|
+
# pylint: disable=unused-argument,too-many-nested-blocks
|
|
4
|
+
import asyncio
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from typing import Union, Optional, Generator, Any
|
|
11
|
+
|
|
12
|
+
from agentscope.agent import AgentBase
|
|
13
|
+
from agentscope.message import Msg
|
|
14
|
+
|
|
15
|
+
_MSG_INSTANCE = defaultdict(list)
|
|
16
|
+
_LOCKS = defaultdict(threading.Lock)
|
|
17
|
+
TIMEOUT = 30
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_async_in_thread(coro):
|
|
21
|
+
"""
|
|
22
|
+
Run an async coroutine in a thread-safe manner.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
return asyncio.run(coro)
|
|
26
|
+
except RuntimeError as e:
|
|
27
|
+
if "cannot be called from a running event loop" in str(e):
|
|
28
|
+
# If we're somehow in a context with an existing event loop,
|
|
29
|
+
# fall back to the manual approach with better error handling.
|
|
30
|
+
return _run_with_manual_loop(coro)
|
|
31
|
+
else:
|
|
32
|
+
logging.error(f"Runtime error in async thread: {e}")
|
|
33
|
+
return None
|
|
34
|
+
except Exception as e:
|
|
35
|
+
logging.error(f"Error in async thread: {e}")
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_with_manual_loop(coro):
|
|
40
|
+
"""Fallback method using manual event loop management.
|
|
41
|
+
|
|
42
|
+
This is only used when asyncio.run cannot be used due to
|
|
43
|
+
an existing event loop in the current context.
|
|
44
|
+
"""
|
|
45
|
+
loop = None
|
|
46
|
+
try:
|
|
47
|
+
loop = asyncio.new_event_loop()
|
|
48
|
+
return loop.run_until_complete(coro)
|
|
49
|
+
finally:
|
|
50
|
+
if loop:
|
|
51
|
+
try:
|
|
52
|
+
# Cancel all remaining tasks with better error handling
|
|
53
|
+
pending_tasks = asyncio.all_tasks(loop)
|
|
54
|
+
if pending_tasks:
|
|
55
|
+
for task in pending_tasks:
|
|
56
|
+
if not task.done():
|
|
57
|
+
task.cancel()
|
|
58
|
+
# Wait for cancellation to complete
|
|
59
|
+
try:
|
|
60
|
+
loop.run_until_complete(
|
|
61
|
+
asyncio.gather(
|
|
62
|
+
*pending_tasks,
|
|
63
|
+
return_exceptions=True,
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
except Exception:
|
|
67
|
+
pass # Ignore exceptions during cleanup
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logging.error(f"Error during task cleanup: {e}")
|
|
70
|
+
finally:
|
|
71
|
+
try:
|
|
72
|
+
loop.close()
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logging.error(f"Error closing event loop: {e}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def pre_speak_msg_buffer_hook(
|
|
78
|
+
self: AgentBase,
|
|
79
|
+
kwargs: dict[str, Any],
|
|
80
|
+
) -> Union[Msg, None]:
|
|
81
|
+
"""Hook for pre speak msg buffer"""
|
|
82
|
+
msg = kwargs["msg"]
|
|
83
|
+
thread_id = threading.current_thread().name
|
|
84
|
+
if thread_id.startswith("pipeline"):
|
|
85
|
+
with _LOCKS[thread_id]:
|
|
86
|
+
_MSG_INSTANCE[thread_id].append(msg)
|
|
87
|
+
return kwargs
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def clear_msg_instances(thread_id: Optional[str] = None) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Clears all message instances for a specific thread ID.
|
|
93
|
+
This function removes all message instances associated with a given
|
|
94
|
+
thread ID (`thread_id`). It ensures thread safety through the use of a
|
|
95
|
+
threading lock when accessing the shared message instance list. This
|
|
96
|
+
prevents race conditions in concurrent environments.
|
|
97
|
+
Args:
|
|
98
|
+
thread_id (optional): The thread ID for which to clear message
|
|
99
|
+
instances. If `None`, the function will do nothing.
|
|
100
|
+
Notes:
|
|
101
|
+
- It assumes the existence of a global `_LOCKS` for synchronization and
|
|
102
|
+
a dictionary `_MSG_INSTANCE` where each thread ID maps to a list of
|
|
103
|
+
message instances.
|
|
104
|
+
"""
|
|
105
|
+
if not thread_id:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
with _LOCKS[thread_id]:
|
|
109
|
+
_MSG_INSTANCE[thread_id].clear()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_msg_instances(thread_id: Optional[str] = None) -> Generator:
|
|
113
|
+
"""
|
|
114
|
+
A generator function that yields message instances for a specific thread ID
|
|
115
|
+
This function is designed to continuously monitor and yield new message
|
|
116
|
+
instances associated with a given thread ID (`thread_id`). It ensures
|
|
117
|
+
thread safety through the use of a threading lock when accessing the shared
|
|
118
|
+
message instance list. This prevents race conditions in concurrent
|
|
119
|
+
environments.
|
|
120
|
+
Args:
|
|
121
|
+
thread_id (optional): The thread ID for which to monitor and yield
|
|
122
|
+
message instances. If `None`, the function will yield `None` and
|
|
123
|
+
terminate.
|
|
124
|
+
Yields:
|
|
125
|
+
The next available message instance for the specified thread ID. If no
|
|
126
|
+
message is available, it will wait and check periodically.
|
|
127
|
+
Notes:
|
|
128
|
+
- The function uses a small delay (`time.sleep(0.1)`) to prevent busy
|
|
129
|
+
waiting. This ensures efficient CPU usage while waiting for new
|
|
130
|
+
messages.
|
|
131
|
+
- It assumes the existence of a global `_LOCK` for synchronization and
|
|
132
|
+
a dictionary `_MSG_INSTANCE` where each thread ID maps to a list of
|
|
133
|
+
message instances.
|
|
134
|
+
Example:
|
|
135
|
+
for msg in get_msg_instances(thread_id=123):
|
|
136
|
+
process_message(msg)
|
|
137
|
+
"""
|
|
138
|
+
cnt = 0
|
|
139
|
+
|
|
140
|
+
if not thread_id:
|
|
141
|
+
yield
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
while True:
|
|
145
|
+
with _LOCKS[thread_id]:
|
|
146
|
+
if _MSG_INSTANCE[thread_id]:
|
|
147
|
+
cnt = 0
|
|
148
|
+
yield _MSG_INSTANCE[thread_id].pop(0), len(
|
|
149
|
+
_MSG_INSTANCE[thread_id],
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
cnt += 0.1
|
|
153
|
+
if cnt > TIMEOUT:
|
|
154
|
+
raise TimeoutError
|
|
155
|
+
yield None, None
|
|
156
|
+
time.sleep(0.1) # Avoid busy waiting
|