agentscope-runtime 0.1.1__py3-none-any.whl → 0.1.2__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 (21) hide show
  1. agentscope_runtime/engine/services/context_manager.py +28 -1
  2. agentscope_runtime/engine/services/rag_service.py +101 -0
  3. agentscope_runtime/sandbox/box/training_box/env_service.py +1 -1
  4. agentscope_runtime/sandbox/box/training_box/environments/bfcl/bfcl_dataprocess.py +216 -0
  5. agentscope_runtime/sandbox/box/training_box/environments/bfcl/bfcl_env.py +380 -0
  6. agentscope_runtime/sandbox/box/training_box/environments/bfcl/env_handler.py +934 -0
  7. agentscope_runtime/sandbox/box/training_box/training_box.py +139 -9
  8. agentscope_runtime/sandbox/enums.py +2 -0
  9. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +19 -9
  10. agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +61 -6
  11. agentscope_runtime/sandbox/manager/sandbox_manager.py +95 -35
  12. agentscope_runtime/sandbox/manager/server/app.py +41 -4
  13. agentscope_runtime/sandbox/model/__init__.py +1 -5
  14. agentscope_runtime/sandbox/model/manager_config.py +2 -13
  15. agentscope_runtime/version.py +1 -1
  16. {agentscope_runtime-0.1.1.dist-info → agentscope_runtime-0.1.2.dist-info}/METADATA +6 -1
  17. {agentscope_runtime-0.1.1.dist-info → agentscope_runtime-0.1.2.dist-info}/RECORD +21 -17
  18. {agentscope_runtime-0.1.1.dist-info → agentscope_runtime-0.1.2.dist-info}/WHEEL +0 -0
  19. {agentscope_runtime-0.1.1.dist-info → agentscope_runtime-0.1.2.dist-info}/entry_points.txt +0 -0
  20. {agentscope_runtime-0.1.1.dist-info → agentscope_runtime-0.1.2.dist-info}/licenses/LICENSE +0 -0
  21. {agentscope_runtime-0.1.1.dist-info → agentscope_runtime-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,380 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This file is part of https://github.com/ShishirPatil/gorilla
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ # environments/bfcl_env.py
18
+ from __future__ import annotations
19
+ import json
20
+ import os
21
+ from pathlib import Path
22
+ from typing import Any, Dict
23
+ import re
24
+
25
+ from training_box.base import BaseEnv
26
+ from training_box.registry import Registry
27
+ from training_box.src.trajectory import StateMessage
28
+
29
+
30
+ from training_box.environments.bfcl.env_handler import EnvHandler
31
+
32
+ os.environ.setdefault(
33
+ "BFCL_DATA_PATH",
34
+ "./bfcl/multiturn_dataset/multiturn_data.jsonl",
35
+ )
36
+ os.environ.setdefault("BFCL_ANSWER_PATH", "./bfcl/data/possible_answer")
37
+
38
+ __all__ = ["BfclEnv"]
39
+
40
+
41
+ def parse_assistant_content_to_tool_calls(
42
+ msg: Dict[str, Any],
43
+ ) -> Dict[str, Any]:
44
+ content = msg.get("content", "") or ""
45
+ if not isinstance(content, str):
46
+ content = str(content)
47
+
48
+ tool_calls = []
49
+ call_id_counter = 1
50
+
51
+ pattern = r"<tool_call>\s*\n?({.*?})\s*\n?\</tool_call>"
52
+ matches = list(re.finditer(pattern, content, re.DOTALL))
53
+
54
+ if not matches:
55
+ return {
56
+ "role": "assistant",
57
+ "content": content.strip(),
58
+ "tool_calls": [],
59
+ }
60
+
61
+ for match in matches:
62
+ json_str = match.group(1).strip()
63
+ try:
64
+ data = json.loads(json_str)
65
+ if not isinstance(data, dict):
66
+ continue
67
+ if "name" not in data or "arguments" not in data:
68
+ continue
69
+
70
+ func_name = data["name"]
71
+ tool_call = {
72
+ "id": f"{func_name}_{call_id_counter}",
73
+ "type": "function",
74
+ "function": {
75
+ "name": data["name"],
76
+ "arguments": data["arguments"],
77
+ },
78
+ }
79
+ tool_calls.append(tool_call)
80
+ call_id_counter += 1
81
+ except json.JSONDecodeError as e:
82
+ print(f"JSON 解析失败: {json_str[:50]}... -> {e}")
83
+ continue
84
+
85
+ cleaned_content = re.sub(pattern, "", content, flags=re.DOTALL).strip()
86
+ cleaned_content = re.sub(r"\n\s*\n", "\n\n", cleaned_content).strip()
87
+
88
+ result = {
89
+ "role": "assistant",
90
+ "content": cleaned_content,
91
+ "tool_calls": tool_calls,
92
+ }
93
+
94
+ return result
95
+
96
+
97
+ def tools_schema_to_qwen_prompt(tools_schema):
98
+ if not tools_schema:
99
+ return ""
100
+
101
+ lines = []
102
+ lines.append("\n\n# Tools\n")
103
+ lines.append(
104
+ "You may call one or more functions to assist with the user query.\n",
105
+ )
106
+ lines.append(
107
+ "You are provided with function signatures within <tools></tools> \
108
+ XML tags:",
109
+ )
110
+ lines.append("<tools>")
111
+
112
+ for tool in tools_schema:
113
+ tool_json = json.dumps(
114
+ tool,
115
+ ensure_ascii=False,
116
+ separators=(",", ":"),
117
+ )
118
+ lines.append(tool_json)
119
+ lines.append("</tools>\n")
120
+ lines.append(
121
+ "Important: Always use only the latest tool list provided, \
122
+ ignoring any functions mentioned in previous messages.",
123
+ )
124
+ lines.append(
125
+ "For each function call, return a json object with function name \
126
+ and arguments within <tool_call> and <tool_call> XML tags:",
127
+ )
128
+ lines.append("<tool_call>")
129
+ lines.append('{"name": <function-name>, "arguments": <args-json-object>}')
130
+ lines.append("</tool_call>")
131
+
132
+ return "\n".join(lines)
133
+
134
+
135
+ def tool_message_to_qwen_text(tool_messages):
136
+ if isinstance(tool_messages, dict):
137
+ tool_messages = [tool_messages]
138
+
139
+ if not tool_messages:
140
+ return ""
141
+
142
+ tool_entries = []
143
+ for msg in tool_messages:
144
+ if msg.get("role") != "tool":
145
+ raise ValueError("All messages must have role 'tool'")
146
+
147
+ content = msg.get("content", "")
148
+ tool_call_id = msg.get("tool_call_id", "")
149
+
150
+ name = msg.get("name", tool_call_id)
151
+
152
+ if not name:
153
+ raise ValueError("Missing 'name' in tool message.")
154
+
155
+ try:
156
+ if isinstance(content, str):
157
+ parsed_content = (
158
+ json.loads(content)
159
+ if content.strip().startswith(("{", "["))
160
+ else content
161
+ )
162
+ else:
163
+ parsed_content = content
164
+ except Exception:
165
+ parsed_content = content
166
+
167
+ entry = {
168
+ "name": name,
169
+ "content": parsed_content,
170
+ }
171
+ tool_entries.append(
172
+ f"<tool_call>\n{json.dumps(entry, ensure_ascii=False)}"
173
+ f"\n</tool_call>",
174
+ )
175
+
176
+ inner_text = "\n".join(tool_entries) + "\n"
177
+
178
+ return inner_text
179
+
180
+
181
+ @Registry.register("bfcl")
182
+ class BfclEnv(BaseEnv):
183
+ def __init__(
184
+ self,
185
+ task_id: str | None = None,
186
+ instance_id: str | None = None,
187
+ params: Dict[str, Any] | None = None,
188
+ ):
189
+ self.task_id, self.instance_id = task_id, instance_id
190
+ self.params: Dict[str, Any] = params or {}
191
+
192
+ self.data_path = self.params.get(
193
+ "data_path",
194
+ os.getenv("BFCL_DATA_PATH"),
195
+ )
196
+ self.answer_path = self.params.get(
197
+ "answer_path",
198
+ os.getenv("BFCL_ANSWER_PATH"),
199
+ )
200
+ self.model_name = self.params.get("model_name", "env_handler")
201
+
202
+ self.test_entry: Dict[str, Any] | None = None
203
+ self.original_test_entry: Dict[str, Any] | None = None
204
+ self.env_handler: EnvHandler | None = None
205
+ self.conversation_history: list[Dict[str, Any]] = []
206
+ self.current_turn = 0
207
+ self.total_input_tokens = 0
208
+ self.total_output_tokens = 0
209
+ self.tools_info = ""
210
+
211
+ def get_init_state(
212
+ self,
213
+ _params: Dict[str, Any] | None = None,
214
+ ) -> Dict[str, Any]:
215
+ self.test_entry = self._load_test_case(self.data_path, self.task_id)
216
+ self.original_test_entry = self.test_entry
217
+
218
+ self.env_handler = EnvHandler(
219
+ model_name=self.model_name,
220
+ answer_path=Path(self.answer_path),
221
+ )
222
+
223
+ self.conversation_history = self.test_entry.get("question", [[]])[
224
+ 0
225
+ ].copy()
226
+ self.current_turn = 0
227
+
228
+ tools = self.test_entry.get("function", [])
229
+ self.tools_info = "Available tools:\n" + "\n".join(
230
+ f"- {t.get('function', {}).get('name', 'unknown')}" for t in tools
231
+ )
232
+
233
+ first_query = (
234
+ self.conversation_history[0]["content"]
235
+ if self.conversation_history
236
+ else ""
237
+ )
238
+
239
+ tool_prompt = tools_schema_to_qwen_prompt(tools)
240
+ return {
241
+ "state": [
242
+ {"role": "system", "content": tool_prompt},
243
+ {"role": "user", "content": first_query},
244
+ ],
245
+ "info": {
246
+ "instance_id": self.instance_id,
247
+ "task_id": self.task_id,
248
+ "test_id": self.test_entry.get("id", "unknown"),
249
+ "tools_count": len(tools),
250
+ "questions_count": len(
251
+ self.original_test_entry.get("question", []),
252
+ ),
253
+ },
254
+ }
255
+
256
+ def step(
257
+ self,
258
+ action: Dict[str, Any],
259
+ params: Dict[str, Any] | None = None,
260
+ ) -> Dict[str, Any]:
261
+ state_msg = self.transition(
262
+ action,
263
+ params or {},
264
+ )
265
+ terminated = self._is_terminated(
266
+ state_msg.simple_dict["content"],
267
+ )
268
+ reward = self.evaluate(params={"sparse": True}) if terminated else 0.0
269
+ return {
270
+ "state": [state_msg.simple_dict],
271
+ "reward": reward,
272
+ "is_terminated": terminated,
273
+ "info": {},
274
+ }
275
+
276
+ def transition(
277
+ self,
278
+ assistant_entry: Dict[str, Any],
279
+ _params: Dict[str, Any],
280
+ ) -> StateMessage:
281
+ assistant_entry = parse_assistant_content_to_tool_calls(
282
+ assistant_entry,
283
+ )
284
+
285
+ self.conversation_history.append(
286
+ assistant_entry,
287
+ )
288
+
289
+ if self.env_handler is None or self.original_test_entry is None:
290
+ raise RuntimeError(
291
+ "EnvHandler not initialised – call get_init_state() first.",
292
+ )
293
+ env_resp = self.env_handler.interact(
294
+ self.conversation_history,
295
+ self.original_test_entry,
296
+ )
297
+ next_msg_content = ""
298
+
299
+ for _idx, msg in enumerate(env_resp.get("messages", [])):
300
+ self.conversation_history.append(msg)
301
+ if msg["role"] == "tool":
302
+ next_msg_content += tool_message_to_qwen_text(msg)
303
+ elif msg["role"] == "user":
304
+ next_msg_content = msg.get("content", "")
305
+ self.current_turn += 1
306
+ elif msg["role"] == "env":
307
+ next_msg_content = msg.get("content", "")
308
+
309
+ return StateMessage(role="user", content=next_msg_content)
310
+
311
+ def evaluate(
312
+ self,
313
+ _messages: Dict[str, Any] | None = None,
314
+ params: Dict[str, Any] | None = None,
315
+ ):
316
+ if self.env_handler is None:
317
+ raise RuntimeError("EnvHandler not initialised – cannot evaluate.")
318
+
319
+ conv_result = {
320
+ "test_id": self.test_entry.get("id", "unknown"),
321
+ "messages": self.conversation_history,
322
+ "turn_count": self.current_turn,
323
+ "total_input_tokens": self.total_input_tokens,
324
+ "total_output_tokens": self.total_output_tokens,
325
+ "completed": self._is_terminated(
326
+ self.conversation_history[-1]["content"],
327
+ ),
328
+ "original_test_entry": self.original_test_entry,
329
+ }
330
+ sparse = (params or {}).get("sparse", False)
331
+ result = self.env_handler.evaluate(conv_result)
332
+ return result.get("accuracy", 0.0) if sparse else result
333
+
334
+ def get_info(
335
+ self,
336
+ _messages: Dict[str, Any] | None = None,
337
+ _params: Dict[str, Any] | None = None,
338
+ ) -> str:
339
+ return self.tools_info
340
+
341
+ def close(self):
342
+ self.conversation_history.clear()
343
+
344
+ def _is_terminated(self, env_content) -> bool:
345
+ return env_content == "[CONVERSATION_COMPLETED]"
346
+
347
+ @staticmethod
348
+ def _load_test_case(data_path: str, test_id: str | None) -> Dict[str, Any]:
349
+ if not Path(data_path).exists():
350
+ raise FileNotFoundError(f"BFCL data file '{data_path}' not found")
351
+
352
+ if test_id is None:
353
+ raise ValueError("task_id is required")
354
+
355
+ with open(data_path, "r", encoding="utf-8") as f:
356
+ if str(test_id).isdigit():
357
+ idx = int(test_id)
358
+ for line_no, line in enumerate(f):
359
+ if line_no == idx:
360
+ return json.loads(line)
361
+ raise ValueError(
362
+ f"Test case index {idx} not found in {data_path}",
363
+ )
364
+ for line in f:
365
+ data = json.loads(line)
366
+ if data.get("id") == test_id:
367
+ return data
368
+ raise ValueError(
369
+ f"Test case id '{test_id}' not found in {data_path}",
370
+ )
371
+
372
+ @staticmethod
373
+ def get_query_list(
374
+ split: str = "train",
375
+ ):
376
+ path = os.getenv("BFCL_SPLID_ID_PATH")
377
+ if path is None:
378
+ raise ValueError("path must be provided")
379
+ with open(path, "r", encoding="utf-8") as f:
380
+ return json.load(f)[split]