aient 1.1.91__py3-none-any.whl → 1.1.93__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.
@@ -1,10 +1,12 @@
1
+ import pickle
1
2
  import base64
2
3
  import asyncio
3
4
  import logging
5
+ import hashlib
4
6
  import mimetypes
5
7
  from dataclasses import dataclass
6
8
  from abc import ABC, abstractmethod
7
- from typing import List, Dict, Any, Optional
9
+ from typing import List, Dict, Any, Optional, Union, Callable
8
10
 
9
11
  # 1. 核心数据结构: ContentBlock
10
12
  @dataclass
@@ -19,49 +21,179 @@ class ContextProvider(ABC):
19
21
  def mark_stale(self): self._is_stale = True
20
22
  async def refresh(self):
21
23
  if self._is_stale:
22
- self._cached_content = await self._fetch_content()
24
+ self._cached_content = await self.render()
23
25
  self._is_stale = False
24
26
  @abstractmethod
25
- async def _fetch_content(self) -> Optional[str]: raise NotImplementedError
27
+ async def render(self) -> Optional[str]: raise NotImplementedError
28
+ @abstractmethod
29
+ def update(self, *args, **kwargs): raise NotImplementedError
26
30
  def get_content_block(self) -> Optional[ContentBlock]:
27
31
  if self._cached_content is not None: return ContentBlock(self.name, self._cached_content)
28
32
  return None
29
33
 
30
34
  class Texts(ContextProvider):
31
- def __init__(self, name: str, text: str): super().__init__(name); self._text = text
32
- async def _fetch_content(self) -> str: return self._text
35
+ def __init__(self, text: Union[str, Callable[[], str]], name: Optional[str] = None):
36
+ self._text = text
37
+ self._is_dynamic = callable(self._text)
38
+
39
+ if name is None:
40
+ if self._is_dynamic:
41
+ import uuid
42
+ _name = f"dynamic_text_{uuid.uuid4().hex[:8]}"
43
+ else:
44
+ h = hashlib.sha1(self._text.encode()).hexdigest()
45
+ _name = f"text_{h[:8]}"
46
+ else:
47
+ _name = name
48
+ super().__init__(_name)
49
+
50
+ async def refresh(self):
51
+ if self._is_dynamic:
52
+ self._is_stale = True
53
+ await super().refresh()
54
+
55
+ def update(self, text: Union[str, Callable[[], str]]):
56
+ self._text = text
57
+ self._is_dynamic = callable(self._text)
58
+ self.mark_stale()
59
+
60
+ async def render(self) -> str:
61
+ if self._is_dynamic:
62
+ return self._text()
63
+ return self._text
33
64
 
34
65
  class Tools(ContextProvider):
35
66
  def __init__(self, tools_json: List[Dict]): super().__init__("tools"); self._tools_json = tools_json
36
- async def _fetch_content(self) -> str: return f"<tools>{str(self._tools_json)}</tools>"
67
+ def update(self, tools_json: List[Dict]):
68
+ self._tools_json = tools_json
69
+ self.mark_stale()
70
+ async def render(self) -> str: return f"<tools>{str(self._tools_json)}</tools>"
37
71
 
38
72
  class Files(ContextProvider):
39
- def __init__(self): super().__init__("files"); self._files: Dict[str, str] = {}
40
- def update(self, path: str, content: str): self._files[path] = content; self.mark_stale()
41
- async def _fetch_content(self) -> str:
73
+ def __init__(self, *paths: Union[str, List[str]]):
74
+ super().__init__("files")
75
+ self._files: Dict[str, str] = {}
76
+
77
+ file_paths: List[str] = []
78
+ if paths:
79
+ # Handle the case where the first argument is a list of paths, e.g., Files(['a', 'b'])
80
+ if len(paths) == 1 and isinstance(paths[0], list):
81
+ file_paths.extend(paths[0])
82
+ # Handle the case where arguments are individual string paths, e.g., Files('a', 'b')
83
+ else:
84
+ file_paths.extend(paths)
85
+
86
+ if file_paths:
87
+ for path in file_paths:
88
+ try:
89
+ with open(path, 'r', encoding='utf-8') as f:
90
+ self._files[path] = f.read()
91
+ except FileNotFoundError:
92
+ logging.warning(f"File not found during initialization: {path}. Skipping.")
93
+ except Exception as e:
94
+ logging.error(f"Error reading file {path} during initialization: {e}")
95
+
96
+ async def refresh(self):
97
+ """
98
+ Overrides the default refresh behavior. It synchronizes the content of
99
+ all tracked files with the file system. If a file is not found, its
100
+ content is updated to reflect the error.
101
+ """
102
+ is_changed = False
103
+ for path in list(self._files.keys()):
104
+ try:
105
+ with open(path, 'r', encoding='utf-8') as f:
106
+ new_content = f.read()
107
+ if self._files.get(path) != new_content:
108
+ self._files[path] = new_content
109
+ is_changed = True
110
+ except FileNotFoundError:
111
+ error_msg = f"[Error: File not found at path '{path}']"
112
+ if self._files.get(path) != error_msg:
113
+ self._files[path] = error_msg
114
+ is_changed = True
115
+ except Exception as e:
116
+ error_msg = f"[Error: Could not read file at path '{path}': {e}]"
117
+ if self._files.get(path) != error_msg:
118
+ self._files[path] = error_msg
119
+ is_changed = True
120
+
121
+ if is_changed:
122
+ self.mark_stale()
123
+
124
+ await super().refresh()
125
+
126
+ def update(self, path: str, content: Optional[str] = None):
127
+ """
128
+ Updates a single file. If content is provided, it updates the file in
129
+ memory. If content is None, it reads the file from disk.
130
+ """
131
+ if content is not None:
132
+ self._files[path] = content
133
+ else:
134
+ try:
135
+ with open(path, 'r', encoding='utf-8') as f:
136
+ self._files[path] = f.read()
137
+ except FileNotFoundError:
138
+ logging.error(f"File not found for update: {path}.")
139
+ self._files[path] = f"[Error: File not found at path '{path}']"
140
+ except Exception as e:
141
+ logging.error(f"Error reading file for update {path}: {e}.")
142
+ self._files[path] = f"[Error: Could not read file at path '{path}': {e}]"
143
+ self.mark_stale()
144
+ async def render(self) -> str:
42
145
  if not self._files: return None
43
- return "<files>\n" + "\n".join([f"<file path='{p}'>{c}...</file>" for p, c in self._files.items()]) + "\n</files>"
146
+ return "<latest_file_content>" + "\n".join([f"<file><file_path>{p}</file_path><file_content>{c}</file_content></file>" for p, c in self._files.items()]) + "\n</latest_file_content>"
44
147
 
45
148
  class Images(ContextProvider):
46
- def __init__(self, image_path: str, name: Optional[str] = None):
47
- super().__init__(name or image_path)
48
- self.image_path = image_path
49
- async def _fetch_content(self) -> Optional[str]:
149
+ def __init__(self, url: str, name: Optional[str] = None):
150
+ super().__init__(name or url)
151
+ self.url = url
152
+ def update(self, url: str):
153
+ self.url = url
154
+ self.mark_stale()
155
+ async def render(self) -> Optional[str]:
156
+ if self.url.startswith("data:"):
157
+ return self.url
50
158
  try:
51
- with open(self.image_path, "rb") as image_file:
159
+ with open(self.url, "rb") as image_file:
52
160
  encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
53
- mime_type, _ = mimetypes.guess_type(self.image_path)
161
+ mime_type, _ = mimetypes.guess_type(self.url)
54
162
  if not mime_type: mime_type = "application/octet-stream" # Fallback
55
163
  return f"data:{mime_type};base64,{encoded_string}"
56
164
  except FileNotFoundError:
57
- logging.warning(f"Image file not found: {self.image_path}. Skipping.")
165
+ logging.warning(f"Image file not found: {self.url}. Skipping.")
58
166
  return None # Or handle error appropriately
59
167
 
60
168
  # 3. 消息类 (已合并 MessageContent)
61
169
  class Message(ABC):
62
- def __init__(self, role: str, *initial_items: ContextProvider):
170
+ def __init__(self, role: str, *initial_items: Union[ContextProvider, str, list]):
63
171
  self.role = role
64
- self._items: List[ContextProvider] = list(initial_items)
172
+ processed_items = []
173
+ for item in initial_items:
174
+ if isinstance(item, str):
175
+ processed_items.append(Texts(text=item))
176
+ elif isinstance(item, Message):
177
+ processed_items.extend(item.providers())
178
+ elif isinstance(item, ContextProvider):
179
+ processed_items.append(item)
180
+ elif isinstance(item, list):
181
+ for sub_item in item:
182
+ if not isinstance(sub_item, dict) or 'type' not in sub_item:
183
+ raise ValueError("List items must be dicts with a 'type' key.")
184
+
185
+ item_type = sub_item['type']
186
+ if item_type == 'text':
187
+ processed_items.append(Texts(text=sub_item.get('text', '')))
188
+ elif item_type == 'image_url':
189
+ image_url = sub_item.get('image_url', {}).get('url')
190
+ if image_url:
191
+ processed_items.append(Images(url=image_url))
192
+ else:
193
+ raise ValueError(f"Unsupported item type in list: {item_type}")
194
+ else:
195
+ raise TypeError(f"Unsupported item type: {type(item)}. Must be str, ContextProvider, or list.")
196
+ self._items: List[ContextProvider] = processed_items
65
197
  self._parent_messages: Optional['Messages'] = None
66
198
 
67
199
  def _render_content(self) -> str:
@@ -89,7 +221,52 @@ class Message(ABC):
89
221
  self._parent_messages._notify_provider_added(item, self)
90
222
 
91
223
  def providers(self) -> List[ContextProvider]: return self._items
224
+
225
+ def __add__(self, other):
226
+ if isinstance(other, str):
227
+ new_items = self._items + [Texts(text=other)]
228
+ return type(self)(*new_items)
229
+ if isinstance(other, Message):
230
+ new_items = self._items + other.providers()
231
+ return type(self)(*new_items)
232
+ return NotImplemented
233
+
234
+ def __radd__(self, other):
235
+ if isinstance(other, str):
236
+ new_items = [Texts(text=other)] + self._items
237
+ return type(self)(*new_items)
238
+ if isinstance(other, Message):
239
+ new_items = other.providers() + self._items
240
+ return type(self)(*new_items)
241
+ return NotImplemented
242
+
243
+ def __getitem__(self, key: str) -> Any:
244
+ """
245
+ 使得 Message 对象支持字典风格的访问 (e.g., message['content'])。
246
+ """
247
+ if key == 'role':
248
+ return self.role
249
+ elif key == 'content':
250
+ # 直接调用 to_dict 并提取 'content',确保逻辑一致
251
+ rendered_dict = self.to_dict()
252
+ return rendered_dict.get('content') if rendered_dict else None
253
+ # 对于 tool_calls 等特殊属性,也通过 to_dict 获取
254
+ elif hasattr(self, key):
255
+ rendered_dict = self.to_dict()
256
+ if rendered_dict and key in rendered_dict:
257
+ return rendered_dict[key]
258
+
259
+ # 如果在对象本身或其 to_dict() 中都找不到,则引发 KeyError
260
+ if hasattr(self, key):
261
+ return getattr(self, key)
262
+ raise KeyError(f"'{key}'")
263
+
92
264
  def __repr__(self): return f"Message(role='{self.role}', items={[i.name for i in self._items]})"
265
+ def __bool__(self) -> bool:
266
+ return bool(self._items)
267
+ def get(self, key: str, default: Any = None) -> Any:
268
+ """提供类似字典的 .get() 方法来访问属性。"""
269
+ return getattr(self, key, default)
93
270
  def to_dict(self) -> Optional[Dict[str, Any]]:
94
271
  is_multimodal = any(isinstance(p, Images) for p in self._items)
95
272
 
@@ -113,6 +290,64 @@ class SystemMessage(Message):
113
290
  def __init__(self, *items): super().__init__("system", *items)
114
291
  class UserMessage(Message):
115
292
  def __init__(self, *items): super().__init__("user", *items)
293
+ class AssistantMessage(Message):
294
+ def __init__(self, *items): super().__init__("assistant", *items)
295
+
296
+ class RoleMessage:
297
+ """A factory class that creates a specific message type based on the role."""
298
+ def __new__(cls, role: str, *items):
299
+ if role == 'system':
300
+ return SystemMessage(*items)
301
+ elif role == 'user':
302
+ return UserMessage(*items)
303
+ elif role == 'assistant':
304
+ return AssistantMessage(*items)
305
+ else:
306
+ raise ValueError(f"Invalid role: {role}. Must be 'system', 'user', or 'assistant'.")
307
+
308
+ class ToolCalls(Message):
309
+ """Represents an assistant message that requests tool calls."""
310
+ def __init__(self, tool_calls: List[Any]):
311
+ super().__init__("assistant")
312
+ self.tool_calls = tool_calls
313
+
314
+ def to_dict(self) -> Dict[str, Any]:
315
+ # Duck-typing serialization for OpenAI's tool_call objects
316
+ serialized_calls = []
317
+ for tc in self.tool_calls:
318
+ try:
319
+ # Attempt to serialize based on openai-python > 1.0 tool_call structure
320
+ func = tc.function
321
+ serialized_calls.append({
322
+ "id": tc.id,
323
+ "type": tc.type,
324
+ "function": { "name": func.name, "arguments": func.arguments }
325
+ })
326
+ except AttributeError:
327
+ if isinstance(tc, dict):
328
+ serialized_calls.append(tc) # Assume it's already a serializable dict
329
+ else:
330
+ raise TypeError(f"Unsupported tool_call type: {type(tc)}. It should be an OpenAI tool_call object or a dict.")
331
+
332
+ return {
333
+ "role": self.role,
334
+ "tool_calls": serialized_calls,
335
+ "content": None
336
+ }
337
+
338
+ class ToolResults(Message):
339
+ """Represents a tool message with the result of a single tool call."""
340
+ def __init__(self, tool_call_id: str, content: str):
341
+ super().__init__("tool")
342
+ self.tool_call_id = tool_call_id
343
+ self.content = content
344
+
345
+ def to_dict(self) -> Dict[str, Any]:
346
+ return {
347
+ "role": self.role,
348
+ "tool_call_id": self.tool_call_id,
349
+ "content": self.content
350
+ }
116
351
 
117
352
  # 4. 顶层容器: Messages
118
353
  class Messages:
@@ -136,12 +371,32 @@ class Messages:
136
371
  indexed = self._providers_index.get(name)
137
372
  return indexed[0] if indexed else None
138
373
 
139
- def pop(self, name: str) -> Optional[ContextProvider]:
140
- indexed = self._providers_index.get(name)
141
- if not indexed:
142
- return None
143
- _provider, parent_message = indexed
144
- return parent_message.pop(name)
374
+ def pop(self, key: Optional[Union[str, int]] = None) -> Union[Optional[ContextProvider], Optional[Message]]:
375
+ # If no key is provided, pop the last message.
376
+ if key is None:
377
+ key = len(self._messages) - 1
378
+
379
+ if isinstance(key, str):
380
+ indexed = self._providers_index.get(key)
381
+ if not indexed:
382
+ return None
383
+ _provider, parent_message = indexed
384
+ return parent_message.pop(key)
385
+ elif isinstance(key, int):
386
+ try:
387
+ if key < 0: # Handle negative indices like -1
388
+ key += len(self._messages)
389
+ if not (0 <= key < len(self._messages)):
390
+ return None
391
+ popped_message = self._messages.pop(key)
392
+ popped_message._parent_messages = None
393
+ for provider in popped_message.providers():
394
+ self._notify_provider_removed(provider)
395
+ return popped_message
396
+ except IndexError:
397
+ return None
398
+
399
+ return None
145
400
 
146
401
  async def refresh(self):
147
402
  tasks = [provider.refresh() for provider, _ in self._providers_index.values()]
@@ -166,72 +421,31 @@ class Messages:
166
421
  for p in message.providers():
167
422
  self._notify_provider_added(p, message)
168
423
 
424
+ def save(self, file_path: str):
425
+ """
426
+ Saves the entire Messages object to a file using pickle.
427
+ Warning: Deserializing data with pickle from an untrusted source is insecure.
428
+ """
429
+ with open(file_path, 'wb') as f:
430
+ pickle.dump(self, f)
431
+
432
+ @classmethod
433
+ def load(cls, file_path: str) -> Optional['Messages']:
434
+ """
435
+ Loads a Messages object from a file using pickle.
436
+ Returns the loaded object, or None if the file is not found or an error occurs.
437
+ Warning: Only load files from a trusted source.
438
+ """
439
+ try:
440
+ with open(file_path, 'rb') as f:
441
+ return pickle.load(f)
442
+ except FileNotFoundError:
443
+ logging.warning(f"File not found at {file_path}, returning empty Messages.")
444
+ return cls()
445
+ except (pickle.UnpicklingError, EOFError) as e:
446
+ logging.error(f"Could not deserialize file {file_path}: {e}")
447
+ return cls()
448
+
169
449
  def __getitem__(self, index: int) -> Message: return self._messages[index]
170
450
  def __len__(self) -> int: return len(self._messages)
171
451
  def __iter__(self): return iter(self._messages)
172
-
173
- # ==============================================================================
174
- # 6. 演示
175
- # ==============================================================================
176
- async def run_demo():
177
- # --- 1. 初始化提供者 ---
178
- system_prompt_provider = Texts("system_prompt", "你是一个AI助手。")
179
- tools_provider = Tools(tools_json=[{"name": "read_file"}])
180
- files_provider = Files()
181
-
182
- # --- 2. 演示新功能:优雅地构建 Messages ---
183
- print("\n>>> 场景 A: 使用新的、优雅的构造函数直接初始化 Messages")
184
- messages = Messages(
185
- SystemMessage(system_prompt_provider, tools_provider),
186
- UserMessage(files_provider, Texts("user_input", "这是我的初始问题。")),
187
- UserMessage(Texts("user_input2", "这是我的初始问题2。"))
188
- )
189
-
190
- print("\n--- 渲染后的初始 Messages (首次渲染,全部刷新) ---")
191
- for msg_dict in await messages.render_latest(): print(msg_dict)
192
- print("-" * 40)
193
-
194
- # --- 3. 演示穿透更新 ---
195
- print("\n>>> 场景 B: 穿透更新 File Provider,渲染时自动刷新")
196
- files_provider_instance = messages.provider("files")
197
- if isinstance(files_provider_instance, Files):
198
- files_provider_instance.update("file1.py", "这是新的文件内容!")
199
-
200
- print("\n--- 再次渲染 Messages (只有文件提供者会刷新) ---")
201
- for msg_dict in await messages.render_latest(): print(msg_dict)
202
- print("-" * 40)
203
-
204
- # --- 4. 演示全局 Pop 和通过索引 Insert ---
205
- print("\n>>> 场景 C: 全局 Pop 工具提供者,并 Insert 到 UserMessage 中")
206
- popped_tools_provider = messages.pop("tools")
207
- if popped_tools_provider:
208
- messages[1].insert(0, popped_tools_provider)
209
- print(f"\n已成功将 '{popped_tools_provider.name}' 提供者移动到用户消息。")
210
-
211
- print("\n--- Pop 和 Insert 后渲染的 Messages (验证移动效果) ---")
212
- for msg_dict in messages.render(): print(msg_dict)
213
- print("-" * 40)
214
-
215
- # --- 5. 演示多模态渲染 ---
216
- print("\n>>> 场景 D: 演示多模态 (文本+图片) 渲染")
217
- with open("dummy_image.png", "w") as f:
218
- f.write("This is a dummy image file.")
219
-
220
- multimodal_message = Messages(
221
- UserMessage(
222
- Texts("prompt", "What do you see in this image?"),
223
- Images("dummy_image.png")
224
- )
225
- )
226
- print("\n--- 渲染后的多模态 Message ---")
227
- for msg_dict in await multimodal_message.render_latest():
228
- if isinstance(msg_dict['content'], list):
229
- for item in msg_dict['content']:
230
- if item['type'] == 'image_url':
231
- item['image_url']['url'] = item['image_url']['url'][:80] + "..."
232
- print(msg_dict)
233
- print("-" * 40)
234
-
235
-
236
- if __name__ == "__main__":
237
- asyncio.run(run_demo())
@@ -0,0 +1,146 @@
1
+ import os
2
+ import json
3
+ import asyncio
4
+ from openai import AsyncOpenAI
5
+
6
+ # 从我们设计的 architext 库中导入消息类
7
+ from architext.core import (
8
+ Messages,
9
+ SystemMessage,
10
+ UserMessage,
11
+ AssistantMessage,
12
+ ToolCalls,
13
+ ToolResults,
14
+ Texts,
15
+ )
16
+
17
+ def _add_tool(a: int, b: int) -> int:
18
+ """(工具函数) 计算两个整数的和。"""
19
+ print(f"Executing tool: add(a={a}, b={b})")
20
+ return a + b
21
+
22
+ async def main():
23
+ """
24
+ 一个简化的、函数式的流程,用于处理单个包含工具调用的用户查询。
25
+ """
26
+ print("Starting simplified Tool Use demonstration...")
27
+
28
+ # --- 1. 初始化 ---
29
+ # 确保环境变量已设置
30
+ if not os.getenv("API_KEY"):
31
+ print("\nERROR: API_KEY environment variable not set.")
32
+ return
33
+
34
+ client = AsyncOpenAI(base_url=os.getenv("BASE_URL"), api_key=os.getenv("API_KEY"))
35
+ model = os.getenv("MODEL", "gpt-4o-mini")
36
+
37
+ # 定义工具
38
+ tool_executors = { "add": _add_tool }
39
+ tools_definition = [{
40
+ "type": "function", "function": {
41
+ "name": "add", "description": "Calculate the sum of two integers.",
42
+ "parameters": {
43
+ "type": "object",
44
+ "properties": {
45
+ "a": {"type": "integer", "description": "The first integer."},
46
+ "b": {"type": "integer", "description": "The second integer."},
47
+ }, "required": ["a", "b"],
48
+ },
49
+ },
50
+ }]
51
+
52
+ # --- 2. 处理查询 ---
53
+ # 初始消息
54
+ messages = Messages(
55
+ SystemMessage(Texts("system_prompt", "You are a helpful assistant. You must use the provided tools to answer questions.")),
56
+ UserMessage(Texts("user_question", "What is the sum of 5 and 10?"))
57
+ )
58
+
59
+ # 第一次 API 调用
60
+ print("\n--- [Step 1] Calling OpenAI with tools...")
61
+ response = await client.chat.completions.create(
62
+ model=model,
63
+ messages=await messages.render_latest(),
64
+ tools=tools_definition,
65
+ tool_choice="auto",
66
+ )
67
+ response_message = response.choices[0].message
68
+
69
+ # 检查是否需要工具调用
70
+ if not response_message.tool_calls:
71
+ final_content = response_message.content or ""
72
+ messages.append(AssistantMessage(Texts("assistant_response", final_content)))
73
+ else:
74
+ # 执行工具调用
75
+ print("--- [Step 2] Assistant requested tool calls. Executing them...")
76
+ messages.append(ToolCalls(response_message.tool_calls))
77
+
78
+ for tool_call in response_message.tool_calls:
79
+ if tool_call.function is None: continue
80
+
81
+ executor = tool_executors.get(tool_call.function.name)
82
+ if not executor: continue
83
+
84
+ try:
85
+ args = json.loads(tool_call.function.arguments)
86
+ result = executor(**args)
87
+ messages.append(ToolResults(tool_call_id=tool_call.id, content=str(result)))
88
+ print(f" - Executed '{tool_call.function.name}'. Result: {result}")
89
+ except (json.JSONDecodeError, TypeError) as e:
90
+ print(f" - Error processing tool call '{tool_call.function.name}': {e}")
91
+
92
+ # 第二次 API 调用
93
+ print("--- [Step 3] Calling OpenAI with tool results for final answer...")
94
+ final_response = await client.chat.completions.create(
95
+ model=model,
96
+ messages=await messages.render_latest(),
97
+ )
98
+ final_content = final_response.choices[0].message.content or ""
99
+ messages.append(AssistantMessage(Texts("final_response", final_content)))
100
+
101
+ # --- 3. 显示结果 ---
102
+ print("\n--- Final request body sent to OpenAI: ---")
103
+ print(json.dumps(await messages.render_latest(), indent=2, ensure_ascii=False))
104
+
105
+ print("\n--- Final Assistant Answer ---")
106
+ print(final_content)
107
+ print("\nDemonstration finished.")
108
+
109
+ if __name__ == "__main__":
110
+ asyncio.run(main())
111
+
112
+ """
113
+ [
114
+ {
115
+ "role": "system",
116
+ "content": "You are a helpful assistant. You must use the provided tools to answer questions."
117
+ },
118
+ {
119
+ "role": "user",
120
+ "content": "What is the sum of 5 and 10?"
121
+ },
122
+ {
123
+ "role": "assistant",
124
+ "tool_calls": [
125
+ {
126
+ "id": "call_rddWXkDikIxllRgbPrR6XjtMVSBPv",
127
+ "type": "function",
128
+ "function": {
129
+ "name": "add",
130
+ "arguments": "{\"b\": 10, \"a\": 5}"
131
+ }
132
+ }
133
+ ],
134
+ "content": null
135
+ },
136
+ {
137
+ "role": "tool",
138
+ "tool_call_id": "call_rddWXkDikIxllRgbPrR6XjtMVSBPv",
139
+ "content": "15"
140
+ },
141
+ {
142
+ "role": "assistant",
143
+ "content": "The sum of 5 and 10 is 15."
144
+ }
145
+ ]
146
+ """