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.
Files changed (131) hide show
  1. agentscope_runtime/__init__.py +4 -0
  2. agentscope_runtime/engine/__init__.py +9 -0
  3. agentscope_runtime/engine/agents/__init__.py +2 -0
  4. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +6 -0
  5. agentscope_runtime/engine/agents/agentscope_agent/agent.py +342 -0
  6. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +156 -0
  7. agentscope_runtime/engine/agents/agno_agent.py +220 -0
  8. agentscope_runtime/engine/agents/base_agent.py +29 -0
  9. agentscope_runtime/engine/agents/langgraph_agent.py +59 -0
  10. agentscope_runtime/engine/agents/llm_agent.py +51 -0
  11. agentscope_runtime/engine/deployers/__init__.py +3 -0
  12. agentscope_runtime/engine/deployers/adapter/__init__.py +0 -0
  13. agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +2 -0
  14. agentscope_runtime/engine/deployers/adapter/a2a/a2a_adapter_utils.py +425 -0
  15. agentscope_runtime/engine/deployers/adapter/a2a/a2a_agent_adapter.py +69 -0
  16. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +60 -0
  17. agentscope_runtime/engine/deployers/adapter/protocol_adapter.py +24 -0
  18. agentscope_runtime/engine/deployers/base.py +17 -0
  19. agentscope_runtime/engine/deployers/local_deployer.py +586 -0
  20. agentscope_runtime/engine/helpers/helper.py +127 -0
  21. agentscope_runtime/engine/llms/__init__.py +3 -0
  22. agentscope_runtime/engine/llms/base_llm.py +60 -0
  23. agentscope_runtime/engine/llms/qwen_llm.py +47 -0
  24. agentscope_runtime/engine/misc/__init__.py +0 -0
  25. agentscope_runtime/engine/runner.py +186 -0
  26. agentscope_runtime/engine/schemas/__init__.py +0 -0
  27. agentscope_runtime/engine/schemas/agent_schemas.py +551 -0
  28. agentscope_runtime/engine/schemas/context.py +54 -0
  29. agentscope_runtime/engine/services/__init__.py +9 -0
  30. agentscope_runtime/engine/services/base.py +77 -0
  31. agentscope_runtime/engine/services/context_manager.py +129 -0
  32. agentscope_runtime/engine/services/environment_manager.py +50 -0
  33. agentscope_runtime/engine/services/manager.py +174 -0
  34. agentscope_runtime/engine/services/memory_service.py +270 -0
  35. agentscope_runtime/engine/services/sandbox_service.py +198 -0
  36. agentscope_runtime/engine/services/session_history_service.py +256 -0
  37. agentscope_runtime/engine/tracing/__init__.py +40 -0
  38. agentscope_runtime/engine/tracing/base.py +309 -0
  39. agentscope_runtime/engine/tracing/local_logging_handler.py +356 -0
  40. agentscope_runtime/engine/tracing/tracing_metric.py +69 -0
  41. agentscope_runtime/engine/tracing/wrapper.py +321 -0
  42. agentscope_runtime/sandbox/__init__.py +14 -0
  43. agentscope_runtime/sandbox/box/__init__.py +0 -0
  44. agentscope_runtime/sandbox/box/base/__init__.py +0 -0
  45. agentscope_runtime/sandbox/box/base/base_sandbox.py +37 -0
  46. agentscope_runtime/sandbox/box/base/box/__init__.py +0 -0
  47. agentscope_runtime/sandbox/box/browser/__init__.py +0 -0
  48. agentscope_runtime/sandbox/box/browser/box/__init__.py +0 -0
  49. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +176 -0
  50. agentscope_runtime/sandbox/box/dummy/__init__.py +0 -0
  51. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +26 -0
  52. agentscope_runtime/sandbox/box/filesystem/__init__.py +0 -0
  53. agentscope_runtime/sandbox/box/filesystem/box/__init__.py +0 -0
  54. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +87 -0
  55. agentscope_runtime/sandbox/box/sandbox.py +115 -0
  56. agentscope_runtime/sandbox/box/shared/__init__.py +0 -0
  57. agentscope_runtime/sandbox/box/shared/app.py +44 -0
  58. agentscope_runtime/sandbox/box/shared/dependencies/__init__.py +5 -0
  59. agentscope_runtime/sandbox/box/shared/dependencies/deps.py +22 -0
  60. agentscope_runtime/sandbox/box/shared/routers/__init__.py +12 -0
  61. agentscope_runtime/sandbox/box/shared/routers/generic.py +173 -0
  62. agentscope_runtime/sandbox/box/shared/routers/mcp.py +207 -0
  63. agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +153 -0
  64. agentscope_runtime/sandbox/box/shared/routers/runtime_watcher.py +187 -0
  65. agentscope_runtime/sandbox/box/shared/routers/workspace.py +325 -0
  66. agentscope_runtime/sandbox/box/training_box/__init__.py +0 -0
  67. agentscope_runtime/sandbox/box/training_box/base.py +120 -0
  68. agentscope_runtime/sandbox/box/training_box/env_service.py +752 -0
  69. agentscope_runtime/sandbox/box/training_box/environments/__init__.py +0 -0
  70. agentscope_runtime/sandbox/box/training_box/environments/appworld/appworld_env.py +987 -0
  71. agentscope_runtime/sandbox/box/training_box/registry.py +54 -0
  72. agentscope_runtime/sandbox/box/training_box/src/trajectory.py +278 -0
  73. agentscope_runtime/sandbox/box/training_box/training_box.py +219 -0
  74. agentscope_runtime/sandbox/build.py +213 -0
  75. agentscope_runtime/sandbox/client/__init__.py +5 -0
  76. agentscope_runtime/sandbox/client/http_client.py +527 -0
  77. agentscope_runtime/sandbox/client/training_client.py +265 -0
  78. agentscope_runtime/sandbox/constant.py +5 -0
  79. agentscope_runtime/sandbox/custom/__init__.py +16 -0
  80. agentscope_runtime/sandbox/custom/custom_sandbox.py +40 -0
  81. agentscope_runtime/sandbox/custom/example.py +37 -0
  82. agentscope_runtime/sandbox/enums.py +68 -0
  83. agentscope_runtime/sandbox/manager/__init__.py +4 -0
  84. agentscope_runtime/sandbox/manager/collections/__init__.py +22 -0
  85. agentscope_runtime/sandbox/manager/collections/base_mapping.py +20 -0
  86. agentscope_runtime/sandbox/manager/collections/base_queue.py +25 -0
  87. agentscope_runtime/sandbox/manager/collections/base_set.py +25 -0
  88. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +22 -0
  89. agentscope_runtime/sandbox/manager/collections/in_memory_queue.py +28 -0
  90. agentscope_runtime/sandbox/manager/collections/in_memory_set.py +27 -0
  91. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +26 -0
  92. agentscope_runtime/sandbox/manager/collections/redis_queue.py +27 -0
  93. agentscope_runtime/sandbox/manager/collections/redis_set.py +23 -0
  94. agentscope_runtime/sandbox/manager/container_clients/__init__.py +8 -0
  95. agentscope_runtime/sandbox/manager/container_clients/base_client.py +39 -0
  96. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +170 -0
  97. agentscope_runtime/sandbox/manager/sandbox_manager.py +694 -0
  98. agentscope_runtime/sandbox/manager/server/__init__.py +0 -0
  99. agentscope_runtime/sandbox/manager/server/app.py +194 -0
  100. agentscope_runtime/sandbox/manager/server/config.py +68 -0
  101. agentscope_runtime/sandbox/manager/server/models.py +17 -0
  102. agentscope_runtime/sandbox/manager/storage/__init__.py +10 -0
  103. agentscope_runtime/sandbox/manager/storage/data_storage.py +16 -0
  104. agentscope_runtime/sandbox/manager/storage/local_storage.py +44 -0
  105. agentscope_runtime/sandbox/manager/storage/oss_storage.py +89 -0
  106. agentscope_runtime/sandbox/manager/utils.py +78 -0
  107. agentscope_runtime/sandbox/mcp_server.py +192 -0
  108. agentscope_runtime/sandbox/model/__init__.py +12 -0
  109. agentscope_runtime/sandbox/model/api.py +16 -0
  110. agentscope_runtime/sandbox/model/container.py +72 -0
  111. agentscope_runtime/sandbox/model/manager_config.py +158 -0
  112. agentscope_runtime/sandbox/registry.py +129 -0
  113. agentscope_runtime/sandbox/tools/__init__.py +12 -0
  114. agentscope_runtime/sandbox/tools/base/__init__.py +8 -0
  115. agentscope_runtime/sandbox/tools/base/tool.py +52 -0
  116. agentscope_runtime/sandbox/tools/browser/__init__.py +57 -0
  117. agentscope_runtime/sandbox/tools/browser/tool.py +597 -0
  118. agentscope_runtime/sandbox/tools/filesystem/__init__.py +32 -0
  119. agentscope_runtime/sandbox/tools/filesystem/tool.py +319 -0
  120. agentscope_runtime/sandbox/tools/function_tool.py +321 -0
  121. agentscope_runtime/sandbox/tools/mcp_tool.py +191 -0
  122. agentscope_runtime/sandbox/tools/sandbox_tool.py +104 -0
  123. agentscope_runtime/sandbox/tools/tool.py +123 -0
  124. agentscope_runtime/sandbox/tools/utils.py +68 -0
  125. agentscope_runtime/version.py +2 -0
  126. agentscope_runtime-0.1.0.dist-info/METADATA +327 -0
  127. agentscope_runtime-0.1.0.dist-info/RECORD +131 -0
  128. agentscope_runtime-0.1.0.dist-info/WHEEL +5 -0
  129. agentscope_runtime-0.1.0.dist-info/entry_points.txt +4 -0
  130. agentscope_runtime-0.1.0.dist-info/licenses/LICENSE +202 -0
  131. agentscope_runtime-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .version import __version__
3
+
4
+ __all__ = ["__version__"]
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .services import (
3
+ Service,
4
+ SandboxService,
5
+ MemoryService,
6
+ SessionHistoryService,
7
+ )
8
+ from .deployers import DeployManager, LocalDeployManager
9
+ from .runner import Runner
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .base_agent import Agent
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .agent import AgentScopeAgent
3
+
4
+ __all__ = [
5
+ "AgentScopeAgent",
6
+ ]
@@ -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