pycoze 0.1.307__py3-none-any.whl → 0.1.309__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.
- pycoze/bot/__init__.py +1 -1
- pycoze/bot/chat.py +98 -0
- pycoze/bot/chat_base.py +243 -0
- pycoze/bot/lib.py +235 -0
- pycoze/bot/{agent/chat.py → message.py} +0 -1
- pycoze/bot/tools.py +279 -0
- pycoze/reference/lib.py +4 -3
- {pycoze-0.1.307.dist-info → pycoze-0.1.309.dist-info}/METADATA +1 -1
- {pycoze-0.1.307.dist-info → pycoze-0.1.309.dist-info}/RECORD +12 -16
- pycoze/bot/agent/__init__.py +0 -5
- pycoze/bot/agent/agent.py +0 -95
- pycoze/bot/agent/agent_types/__init__.py +0 -4
- pycoze/bot/agent/agent_types/const.py +0 -1
- pycoze/bot/agent/agent_types/openai_func_call_agent.py +0 -181
- pycoze/bot/agent/assistant.py +0 -35
- pycoze/bot/agent_chat.py +0 -110
- pycoze/bot/bot.py +0 -23
- {pycoze-0.1.307.dist-info → pycoze-0.1.309.dist-info}/LICENSE +0 -0
- {pycoze-0.1.307.dist-info → pycoze-0.1.309.dist-info}/WHEEL +0 -0
- {pycoze-0.1.307.dist-info → pycoze-0.1.309.dist-info}/top_level.txt +0 -0
pycoze/bot/tools.py
ADDED
@@ -0,0 +1,279 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
import subprocess
|
4
|
+
import fnmatch
|
5
|
+
from typing import Dict
|
6
|
+
from .message import info
|
7
|
+
from .lib import get_formatted_filelist_str, read_local_file, resolve_relative_path
|
8
|
+
from pycoze import ai
|
9
|
+
from pycoze.api import window, web
|
10
|
+
import traceback
|
11
|
+
|
12
|
+
|
13
|
+
class InvalidToolError(Exception):
|
14
|
+
"""无效工具错误"""
|
15
|
+
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
class Tool:
|
20
|
+
"""工具基类"""
|
21
|
+
|
22
|
+
def __init__(self, params: Dict[str, str], cwd: str):
|
23
|
+
self.params = params
|
24
|
+
self.cwd = cwd
|
25
|
+
|
26
|
+
def validate(self) -> bool:
|
27
|
+
"""验证参数是否有效"""
|
28
|
+
raise NotImplementedError
|
29
|
+
|
30
|
+
def execute(self) -> str:
|
31
|
+
"""执行工具并返回结果"""
|
32
|
+
raise NotImplementedError
|
33
|
+
|
34
|
+
|
35
|
+
class ExecuteCommandTool(Tool):
|
36
|
+
"""执行命令工具"""
|
37
|
+
|
38
|
+
def validate(self) -> bool:
|
39
|
+
return "command" in self.params and "requires_approval" in self.params
|
40
|
+
|
41
|
+
def execute(self) -> str:
|
42
|
+
try:
|
43
|
+
if self.params["requires_approval"]:
|
44
|
+
approve = window.confirm(
|
45
|
+
"confirm",
|
46
|
+
f"Do you want to execute the command{self.params['command']}? ",
|
47
|
+
)
|
48
|
+
if approve.strip().lower().startswith("n"):
|
49
|
+
return "User does not wish to run the command."
|
50
|
+
result = subprocess.run(
|
51
|
+
self.params["command"],
|
52
|
+
shell=True,
|
53
|
+
capture_output=True,
|
54
|
+
text=True,
|
55
|
+
cwd=self.cwd
|
56
|
+
)
|
57
|
+
if result.returncode != 0:
|
58
|
+
raise subprocess.CalledProcessError(
|
59
|
+
result.returncode,
|
60
|
+
self.params["command"],
|
61
|
+
result.stdout,
|
62
|
+
result.stderr,
|
63
|
+
)
|
64
|
+
return result.stdout + result.stderr
|
65
|
+
except subprocess.CalledProcessError as e:
|
66
|
+
return f"命令执行失败: {e.stderr}"
|
67
|
+
except Exception as e:
|
68
|
+
return f"执行命令时发生错误: {str(e)}"
|
69
|
+
|
70
|
+
|
71
|
+
class ReadFileTool(Tool):
|
72
|
+
"""读取文件工具"""
|
73
|
+
|
74
|
+
def validate(self) -> bool:
|
75
|
+
return "path" in self.params and os.path.exists(resolve_relative_path(self.cwd, self.params["path"]))
|
76
|
+
|
77
|
+
def execute(self) -> str:
|
78
|
+
path = resolve_relative_path(self.cwd, self.params["path"])
|
79
|
+
content = read_local_file(path)
|
80
|
+
return f"Successfully read the file, the content of the file [[{path}]] is:\n{content}\n\n\n"
|
81
|
+
|
82
|
+
|
83
|
+
class ReadMultipleFilesTool(Tool):
|
84
|
+
"""读取多个文件工具"""
|
85
|
+
|
86
|
+
def validate(self) -> bool:
|
87
|
+
return "paths_list" in self.params
|
88
|
+
|
89
|
+
def execute(self) -> str:
|
90
|
+
return_str = ""
|
91
|
+
for path in self.params["paths_list"]:
|
92
|
+
if os.path.exists(path):
|
93
|
+
path = resolve_relative_path(self.cwd, path)
|
94
|
+
content = read_local_file(path)
|
95
|
+
return_str += f"The content of the file [[{path}]] is:\n{content}\n\n\n"
|
96
|
+
else:
|
97
|
+
return_str += f"File {path} does not exist.\n\n\n"
|
98
|
+
if return_str == "":
|
99
|
+
return_str = "No files were read"
|
100
|
+
return "Result of reading multiple files:" + return_str
|
101
|
+
|
102
|
+
|
103
|
+
class WriteFileTool(Tool):
|
104
|
+
"""写入或覆盖文件工具"""
|
105
|
+
|
106
|
+
def validate(self) -> bool:
|
107
|
+
return "path" in self.params and "content" in self.params
|
108
|
+
|
109
|
+
def execute(self) -> str:
|
110
|
+
path = resolve_relative_path(self.cwd, self.params["path"])
|
111
|
+
# 确保路径是绝对路径
|
112
|
+
if not os.path.isabs(path):
|
113
|
+
path = os.path.join(os.getcwd(), path)
|
114
|
+
# 创建目录并写入或覆盖文件
|
115
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
116
|
+
with open(path, "w", encoding="utf-8") as f:
|
117
|
+
f.write(self.params["content"])
|
118
|
+
return f"File [[{path}]] written successfully."
|
119
|
+
|
120
|
+
|
121
|
+
class ReplaceInFileTool(Tool):
|
122
|
+
"""替换文件部分内容工具"""
|
123
|
+
|
124
|
+
def validate(self) -> bool:
|
125
|
+
return "path" in self.params and "diff" in self.params
|
126
|
+
|
127
|
+
def execute(self) -> str:
|
128
|
+
path = resolve_relative_path(self.cwd, self.params["path"])
|
129
|
+
with open(path, "r", encoding="utf-8") as f:
|
130
|
+
content = f.read()
|
131
|
+
|
132
|
+
# 处理差异内容
|
133
|
+
diff_content = self.params["diff"]
|
134
|
+
for diff_block in diff_content.split("<<<<<<< SEARCH")[1:]:
|
135
|
+
search, replace = diff_block.split("=======")
|
136
|
+
search = search.strip()
|
137
|
+
replace = replace.split(">>>>>>> REPLACE")[0].strip()
|
138
|
+
content = content.replace(search, replace, 1)
|
139
|
+
|
140
|
+
with open(path, "w", encoding="utf-8") as f:
|
141
|
+
f.write(content)
|
142
|
+
return f"The content of the file {path} has been replaced successfully."
|
143
|
+
|
144
|
+
|
145
|
+
class SearchFilesTool(Tool):
|
146
|
+
"""搜索文件工具"""
|
147
|
+
|
148
|
+
def validate(self) -> bool:
|
149
|
+
return "path" in self.params and "regex" in self.params
|
150
|
+
|
151
|
+
def execute(self) -> str:
|
152
|
+
path = resolve_relative_path(self.cwd, self.params["path"])
|
153
|
+
results = []
|
154
|
+
for root, _, files in os.walk(path):
|
155
|
+
for file in files:
|
156
|
+
if "file_pattern" in self.params:
|
157
|
+
if not fnmatch.fnmatch(file, self.params["file_pattern"]):
|
158
|
+
continue
|
159
|
+
|
160
|
+
file_path = os.path.join(root, file)
|
161
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
162
|
+
content = f.read()
|
163
|
+
matches = re.finditer(self.params["regex"], content)
|
164
|
+
for match in matches:
|
165
|
+
results.append(f"{file_path}: {match.group()}")
|
166
|
+
|
167
|
+
return "\n".join(results) if results else "No matching content found."
|
168
|
+
|
169
|
+
|
170
|
+
class ListFilesTool(Tool):
|
171
|
+
"""列出文件工具"""
|
172
|
+
|
173
|
+
def validate(self) -> bool:
|
174
|
+
return "path" in self.params
|
175
|
+
|
176
|
+
def execute(self) -> str:
|
177
|
+
recursive = str(self.params.get("recursive", "false")).lower() == "true"
|
178
|
+
path = resolve_relative_path(self.cwd, self.params["path"])
|
179
|
+
return (
|
180
|
+
"Listing files completed successfully, result is:\n"
|
181
|
+
+ get_formatted_filelist_str(path, recursive, 200)
|
182
|
+
+ "\n\n\n"
|
183
|
+
)
|
184
|
+
|
185
|
+
|
186
|
+
class WebAccessTool(Tool):
|
187
|
+
"""访问网页工具"""
|
188
|
+
|
189
|
+
def validate(self) -> bool:
|
190
|
+
return "url" in self.params and "question" in self.params
|
191
|
+
|
192
|
+
def execute(self) -> str:
|
193
|
+
url = self.params["url"]
|
194
|
+
question = self.params["question"]
|
195
|
+
try:
|
196
|
+
content = web.get_simplified_webpage(url)[: 32 * 1024]
|
197
|
+
result = ai.chat(
|
198
|
+
[{
|
199
|
+
"role": "user",
|
200
|
+
"content": f"""Please answer user question based on web page content. The user's question is:
|
201
|
+
{question}
|
202
|
+
Web page content is:
|
203
|
+
{content}"""
|
204
|
+
}],
|
205
|
+
)
|
206
|
+
return f"Web page access completed, result is: {result}"
|
207
|
+
except Exception as e:
|
208
|
+
print("WebAccessTool Exception", e)
|
209
|
+
return f"Failed to access web page: {str(e)}"
|
210
|
+
|
211
|
+
|
212
|
+
class AskFollowUpQuestionTool(Tool):
|
213
|
+
"""询问后续问题工具"""
|
214
|
+
|
215
|
+
def validate(self) -> bool:
|
216
|
+
return 'question' in self.params
|
217
|
+
|
218
|
+
def execute(self) -> str:
|
219
|
+
info("assistant", self.params['question'])
|
220
|
+
return f"Asked user: {self.params['question']}, Waiting for user replied.\n\n"
|
221
|
+
|
222
|
+
|
223
|
+
class AttemptTaskCompletionTool(Tool):
|
224
|
+
"""完成所有任务工具"""
|
225
|
+
|
226
|
+
def validate(self) -> bool:
|
227
|
+
return 'result' in self.params
|
228
|
+
|
229
|
+
def execute(self) -> str:
|
230
|
+
result = self.params['result']
|
231
|
+
info("assistant", 'Completed:' + result + "\n")
|
232
|
+
if 'command' in self.params:
|
233
|
+
result = subprocess.run(self.params['command'], shell=True, capture_output=True, text=True, cwd=self.cwd)
|
234
|
+
return f"Task completed: {result}, executed command: {self.params['command']}, execution result: {result.stdout + result.stderr}"
|
235
|
+
else:
|
236
|
+
return f"Task completed: {result}"
|
237
|
+
|
238
|
+
|
239
|
+
class ToolExecutor:
|
240
|
+
"""工具执行器"""
|
241
|
+
|
242
|
+
TOOL_MAP = {
|
243
|
+
"execute_command": ExecuteCommandTool,
|
244
|
+
"read_file": ReadFileTool,
|
245
|
+
"read_multiple_files": ReadMultipleFilesTool,
|
246
|
+
"write_or_overwrite_file": WriteFileTool,
|
247
|
+
"replace_part_of_a_file": ReplaceInFileTool,
|
248
|
+
"search_files": SearchFilesTool,
|
249
|
+
"list_files": ListFilesTool,
|
250
|
+
"access_webpage": WebAccessTool,
|
251
|
+
'ask_follow_up_question': AskFollowUpQuestionTool,
|
252
|
+
'complete_all_tasks': AttemptTaskCompletionTool,
|
253
|
+
}
|
254
|
+
|
255
|
+
@classmethod
|
256
|
+
def execute_tool(cls, cwd:str, tool_request, abilities) -> str:
|
257
|
+
"""执行工具"""
|
258
|
+
try:
|
259
|
+
tool_name = list(tool_request.keys())[0]
|
260
|
+
params = tool_request[tool_name]
|
261
|
+
if not tool_name:
|
262
|
+
return "Error: Tool type is empty"
|
263
|
+
if tool_name in cls.TOOL_MAP:
|
264
|
+
tool_class = cls.TOOL_MAP[tool_name]
|
265
|
+
elif tool_name in [a.__name__ for a in abilities]:
|
266
|
+
func = [a for a in abilities if a.__name__ == tool_name][0]
|
267
|
+
try:
|
268
|
+
result = func(**params)
|
269
|
+
except Exception as e:
|
270
|
+
return traceback.format_exc()
|
271
|
+
return str(result)
|
272
|
+
else:
|
273
|
+
return f"Unknown tool: {tool_name}, the first key of output json ({tool_name}) will be recognized as a tool, so do not output other json except for executing tools."
|
274
|
+
tool = tool_class(params, cwd)
|
275
|
+
if not tool.validate():
|
276
|
+
return "Tool parameter validation failed."
|
277
|
+
return tool.execute()
|
278
|
+
except Exception as e:
|
279
|
+
return f"Tool execution failed: {str(e)}"
|
pycoze/reference/lib.py
CHANGED
@@ -2,6 +2,7 @@ import os
|
|
2
2
|
import sys
|
3
3
|
import importlib
|
4
4
|
import types
|
5
|
+
import functools
|
5
6
|
|
6
7
|
|
7
8
|
class ChangeDirectoryAndPath:
|
@@ -64,8 +65,8 @@ def wrapped_func(func, module_path):
|
|
64
65
|
except:
|
65
66
|
pass
|
66
67
|
return result
|
67
|
-
|
68
|
-
|
69
|
-
return
|
68
|
+
|
69
|
+
wrapped_function = functools.wraps(func)(_wrapped)
|
70
|
+
return wrapped_function
|
70
71
|
|
71
72
|
|
@@ -11,19 +11,15 @@ pycoze/api/lib/tab.py,sha256=lD2Uvylzi-zsHj-27YI1mS4ggbIGtCi3vypvGvY48gM,2619
|
|
11
11
|
pycoze/api/lib/view.py,sha256=_PIpTfeuTPPlMDKshMGsqFQYMq7ZiO4Hg5XwHwDoU60,7357
|
12
12
|
pycoze/api/lib/web.py,sha256=GWgtiTJOolKOX2drXcwuyqTcbo5FQVxa1NuBGcNyjyc,223
|
13
13
|
pycoze/api/lib/window.py,sha256=bTkQCzQZ7i3pYXB70bUSTBNJ9C4TW_X3yMae1VkquGk,1944
|
14
|
-
pycoze/bot/__init__.py,sha256=
|
15
|
-
pycoze/bot/
|
16
|
-
pycoze/bot/
|
17
|
-
pycoze/bot/
|
18
|
-
pycoze/bot/
|
19
|
-
pycoze/bot/
|
20
|
-
pycoze/bot/agent/chat.py,sha256=9wZ24CPdSbSnPCWmCQJle05U5VlDGgZhZ9z1mezLst0,816
|
21
|
-
pycoze/bot/agent/agent_types/__init__.py,sha256=zmU2Kmrv5mCdfg-QlPn2H6pWxbGeq8s7YTqLhpzJC6k,179
|
22
|
-
pycoze/bot/agent/agent_types/const.py,sha256=BfUKPrhAHREoMLHuFNG2bCIEkC1-f7K0LEqNg4RwiRE,70
|
23
|
-
pycoze/bot/agent/agent_types/openai_func_call_agent.py,sha256=3qOyrddujtJ50W9SbH5bapbVTwjgE_LC2TnYJWUH9yc,6649
|
14
|
+
pycoze/bot/__init__.py,sha256=rL3Q-ycczRpSFfKn84fg3QBl5k22WpyeIU5qOEjEby8,79
|
15
|
+
pycoze/bot/chat.py,sha256=cYMXFe0hgN0CTcI5BiUtte2eTZwgYLKq1vBDQ-eqB5c,3246
|
16
|
+
pycoze/bot/chat_base.py,sha256=6I_ZfHun9w95Aysz5zGa5E62Dd0YJIYMkd-_Bl4AjJA,9031
|
17
|
+
pycoze/bot/lib.py,sha256=2QyOOlPR8K0_KDJNT96X35SMdn5SF26vYiYzZMw5484,6855
|
18
|
+
pycoze/bot/message.py,sha256=Zq-_k8HztBMOUIs3hbOvWvwHBNopn4UJJBliCROIGcc,718
|
19
|
+
pycoze/bot/tools.py,sha256=vjRksWfE3IJv405IaDAhdqIsIaB156FUP8HBbcroQRc,9601
|
24
20
|
pycoze/reference/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
21
|
pycoze/reference/bot.py,sha256=pxHVYo0G3P3YZ--vBYbMEiEyBoxxPwaO5dMTf9WFMSc,2014
|
26
|
-
pycoze/reference/lib.py,sha256=
|
22
|
+
pycoze/reference/lib.py,sha256=6jvowP2LqjyV21ppRhmtxHxnQJHCbHmlpbj9btV2RQ4,2191
|
27
23
|
pycoze/reference/tool.py,sha256=h7G3KSoZYWq5IJu6E0-shIQ3XiJeJsgSM85GxEnhF98,1107
|
28
24
|
pycoze/reference/workflow.py,sha256=whQtw_FAxvlSbjow1oNFLdytPjjRs_pDBbQmNjaX6zc,1340
|
29
25
|
pycoze/ui/__init__.py,sha256=uaXet23wUk64TcZjpBX8qOx4aUhwA_ucrmcxy7Q4Qr4,929
|
@@ -36,8 +32,8 @@ pycoze/utils/arg.py,sha256=jop1tBfe5hYkHW1NSpCeaZBEznkgguBscj_7M2dWfrs,503
|
|
36
32
|
pycoze/utils/env.py,sha256=5pWlXfM1F5ZU9hhv1rHlDEanjEW5wf0nbyez9bNRqqA,559
|
37
33
|
pycoze/utils/socket.py,sha256=bZbFFRH4mfThzRqt55BAAGQ6eICx_ja4x8UGGrUdAm8,2428
|
38
34
|
pycoze/utils/text_or_file.py,sha256=gpxZVWt2DW6YiEg_MnMuwg36VNf3TX383QD_1oZNB0Y,551
|
39
|
-
pycoze-0.1.
|
40
|
-
pycoze-0.1.
|
41
|
-
pycoze-0.1.
|
42
|
-
pycoze-0.1.
|
43
|
-
pycoze-0.1.
|
35
|
+
pycoze-0.1.309.dist-info/LICENSE,sha256=QStd_Qsd0-kAam_-sOesCIp_uKrGWeoKwt9M49NVkNU,1090
|
36
|
+
pycoze-0.1.309.dist-info/METADATA,sha256=Q4lw_smH95fa-wbcASknh_-AkBOhHnvYn1MOxRzAu6w,854
|
37
|
+
pycoze-0.1.309.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
38
|
+
pycoze-0.1.309.dist-info/top_level.txt,sha256=76dPeDhKvOCleL3ZC5gl1-y4vdS1tT_U1hxWVAn7sFo,7
|
39
|
+
pycoze-0.1.309.dist-info/RECORD,,
|
pycoze/bot/agent/__init__.py
DELETED
pycoze/bot/agent/agent.py
DELETED
@@ -1,95 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
import json
|
3
|
-
from langchain_openai import ChatOpenAI
|
4
|
-
from .chat import info
|
5
|
-
from .assistant import Runnable
|
6
|
-
from langchain_core.messages import (
|
7
|
-
HumanMessage,
|
8
|
-
AIMessage,
|
9
|
-
AIMessageChunk,
|
10
|
-
SystemMessage,
|
11
|
-
)
|
12
|
-
from .agent_types.const import HumanToolString
|
13
|
-
|
14
|
-
|
15
|
-
async def run_agent(agent, inputs: list, tool_compatibility_mode: bool):
|
16
|
-
exist_ids = set()
|
17
|
-
content_list = []
|
18
|
-
async for event in agent.astream_events(inputs, version="v2"):
|
19
|
-
kind = event["event"]
|
20
|
-
if kind == "on_chain_end":
|
21
|
-
if "data" in event:
|
22
|
-
if (
|
23
|
-
"output" in event["data"]
|
24
|
-
and event["data"]["output"] == "end"
|
25
|
-
and "input" in event["data"]
|
26
|
-
and isinstance(event["data"]["input"], list)
|
27
|
-
):
|
28
|
-
input_list = event["data"]["input"]
|
29
|
-
for msg in input_list:
|
30
|
-
if isinstance(msg, HumanMessage) or isinstance(
|
31
|
-
msg, SystemMessage
|
32
|
-
):
|
33
|
-
if (
|
34
|
-
not tool_compatibility_mode
|
35
|
-
or not msg.content.startswith(HumanToolString)
|
36
|
-
):
|
37
|
-
content_list = [] # 防止内容重复
|
38
|
-
if isinstance(msg, AIMessage) and not isinstance(
|
39
|
-
msg, AIMessageChunk
|
40
|
-
):
|
41
|
-
content = msg.content
|
42
|
-
if content:
|
43
|
-
content_list.append(content)
|
44
|
-
elif kind == "on_chat_model_stream":
|
45
|
-
content = event["data"]["chunk"].content
|
46
|
-
if content:
|
47
|
-
info("assistant", content)
|
48
|
-
elif kind == "on_chain_start":
|
49
|
-
data = event["data"]
|
50
|
-
if "input" in data:
|
51
|
-
input_list = (
|
52
|
-
data["input"]
|
53
|
-
if isinstance(data["input"], list)
|
54
|
-
else [data["input"]]
|
55
|
-
)
|
56
|
-
if len(input_list) == 0:
|
57
|
-
continue
|
58
|
-
msg = input_list[-1]
|
59
|
-
if isinstance(msg, AIMessage) and not isinstance(msg, AIMessageChunk):
|
60
|
-
if "tool_calls" in msg.additional_kwargs:
|
61
|
-
tool_calls = msg.additional_kwargs["tool_calls"]
|
62
|
-
for t in tool_calls:
|
63
|
-
if t["id"] in exist_ids:
|
64
|
-
continue
|
65
|
-
exist_ids.add(t["id"])
|
66
|
-
tool = t["function"]["name"]
|
67
|
-
info("assistant", f"\n[调用工具:{tool}]\n\n")
|
68
|
-
|
69
|
-
return "\n".join(content_list)
|
70
|
-
|
71
|
-
|
72
|
-
if __name__ == "__main__":
|
73
|
-
from langchain_experimental.tools import PythonREPLTool
|
74
|
-
import threading
|
75
|
-
|
76
|
-
llm_file = r"C:\Users\aiqqq\AppData\Roaming\pycoze\JsonStorage\llm.json"
|
77
|
-
with open(llm_file, "r", encoding="utf-8") as f:
|
78
|
-
cfg = json.load(f)
|
79
|
-
chat = ChatOpenAI(
|
80
|
-
api_key=cfg["apiKey"],
|
81
|
-
base_url=cfg["baseURL"],
|
82
|
-
model=cfg["model"],
|
83
|
-
temperature=0,
|
84
|
-
)
|
85
|
-
python_tool = PythonREPLTool()
|
86
|
-
agent = Runnable(
|
87
|
-
agent_execution_mode="FuncCall",
|
88
|
-
tools=[python_tool],
|
89
|
-
llm=chat,
|
90
|
-
assistant_message="请以女友的口吻回答,输出不小于100字,可以随便说点其他的",
|
91
|
-
tool_compatibility_mode=False,
|
92
|
-
)
|
93
|
-
|
94
|
-
inputs = [HumanMessage(content="计算根号7+根号88")]
|
95
|
-
print(asyncio.run(run_agent(agent, inputs, True, threading.Event())))
|
@@ -1 +0,0 @@
|
|
1
|
-
HumanToolString = 'The tool call is done, the result is as follows:\n'
|
@@ -1,181 +0,0 @@
|
|
1
|
-
# reference:https://github.com/maxtheman/opengpts/blob/d3425b1ba80aec48953a327ecd9a61b80efb0e69/backend/app/agent_types/openai_agent.py
|
2
|
-
import json
|
3
|
-
from langchain.tools import BaseTool
|
4
|
-
from langchain_core.utils.function_calling import convert_to_openai_tool
|
5
|
-
from langchain_core.language_models.base import LanguageModelLike
|
6
|
-
from langchain_core.messages import SystemMessage, ToolMessage, HumanMessage
|
7
|
-
from langgraph.graph import END
|
8
|
-
from langgraph.graph.message import MessageGraph
|
9
|
-
from langgraph.prebuilt import ToolExecutor, ToolInvocation
|
10
|
-
import re
|
11
|
-
import json
|
12
|
-
import random
|
13
|
-
from .const import HumanToolString
|
14
|
-
|
15
|
-
|
16
|
-
def get_all_markdown_json(content):
|
17
|
-
# Find all markdown json blocks
|
18
|
-
markdown_json_blocks = re.findall(r"```json(.*?)```", content, re.DOTALL)
|
19
|
-
json_list = []
|
20
|
-
|
21
|
-
for block in markdown_json_blocks:
|
22
|
-
try:
|
23
|
-
# Remove any leading/trailing whitespace and parse the JSON
|
24
|
-
json_data = json.loads(block.strip())
|
25
|
-
json_list.append(json_data)
|
26
|
-
except json.JSONDecodeError:
|
27
|
-
# If the block is not valid JSON, skip it
|
28
|
-
continue
|
29
|
-
|
30
|
-
return json_list
|
31
|
-
|
32
|
-
|
33
|
-
def get_tools(last_message):
|
34
|
-
if "tool_calls" in last_message.additional_kwargs:
|
35
|
-
return last_message.additional_kwargs["tool_calls"]
|
36
|
-
else:
|
37
|
-
tool_calls = []
|
38
|
-
if '"name"' in last_message.content and '"parameters":' in last_message.content:
|
39
|
-
print("name 和 paremeters 模式")
|
40
|
-
all_json = get_all_markdown_json(last_message.content)
|
41
|
-
tool_calls = []
|
42
|
-
for tool_call in all_json:
|
43
|
-
if "name" not in tool_call or "parameters" not in tool_call:
|
44
|
-
return "end"
|
45
|
-
tool_call["arguments"] = json.dumps(tool_call["parameters"])
|
46
|
-
tool_call.pop("parameters")
|
47
|
-
tool_calls.append(
|
48
|
-
{
|
49
|
-
"function": tool_call,
|
50
|
-
"id": random.randint(0, 1000000),
|
51
|
-
}
|
52
|
-
)
|
53
|
-
if "<|tool▁sep|>" in last_message.content:
|
54
|
-
print("deepseek的bug: <|tool▁sep|> 模式")
|
55
|
-
name = (
|
56
|
-
last_message.content.split("<|tool▁sep|>")[1].split("```")[0].strip()
|
57
|
-
)
|
58
|
-
all_json = get_all_markdown_json(last_message.content)
|
59
|
-
tool_calls = []
|
60
|
-
for argument in all_json:
|
61
|
-
tool_calls.append(
|
62
|
-
{
|
63
|
-
"function": {
|
64
|
-
"name": name,
|
65
|
-
"arguments": json.dumps(argument),
|
66
|
-
},
|
67
|
-
"id": random.randint(0, 1000000),
|
68
|
-
}
|
69
|
-
)
|
70
|
-
|
71
|
-
return tool_calls
|
72
|
-
|
73
|
-
|
74
|
-
def create_openai_func_call_agent_executor(
|
75
|
-
tools: list[BaseTool],
|
76
|
-
llm: LanguageModelLike,
|
77
|
-
system_message: str,
|
78
|
-
tool_compatibility_mode: str,
|
79
|
-
**kwargs
|
80
|
-
):
|
81
|
-
|
82
|
-
async def _get_messages(messages):
|
83
|
-
msgs = []
|
84
|
-
for m in messages:
|
85
|
-
if isinstance(m, ToolMessage):
|
86
|
-
_dict = m.model_dump()
|
87
|
-
_dict["content"] = str(_dict["content"])
|
88
|
-
m_c = ToolMessage(**_dict)
|
89
|
-
msgs.append(m_c)
|
90
|
-
else:
|
91
|
-
msgs.append(m)
|
92
|
-
|
93
|
-
return [SystemMessage(content=system_message)] + msgs
|
94
|
-
|
95
|
-
if tools and not tool_compatibility_mode:
|
96
|
-
llm_with_tools = llm.bind(tools=[convert_to_openai_tool(t) for t in tools])
|
97
|
-
else:
|
98
|
-
llm_with_tools = llm
|
99
|
-
agent = _get_messages | llm_with_tools
|
100
|
-
tool_executor = ToolExecutor(tools)
|
101
|
-
|
102
|
-
# Define the function that determines whether to continue or not
|
103
|
-
def should_continue(messages):
|
104
|
-
# If there is no FuncCall, then we finish
|
105
|
-
last_message = messages[-1]
|
106
|
-
if last_message.content.strip().endswith("```"):
|
107
|
-
last_message.content = last_message.content + "\n\n" # 避免影响阅读
|
108
|
-
tools = get_tools(last_message)
|
109
|
-
if last_message.tool_calls or tools:
|
110
|
-
return "continue"
|
111
|
-
return "end"
|
112
|
-
|
113
|
-
# Define the function to execute tools
|
114
|
-
async def call_tool(messages):
|
115
|
-
actions: list[ToolInvocation] = []
|
116
|
-
# Based on the continue condition
|
117
|
-
# we know the last message involves a FuncCall
|
118
|
-
last_message = messages[-1]
|
119
|
-
for tool_call in get_tools(last_message):
|
120
|
-
function = tool_call["function"]
|
121
|
-
function_name = function["name"]
|
122
|
-
if function_name == "a_delay_function":
|
123
|
-
return [
|
124
|
-
ToolMessage(
|
125
|
-
tool_call_id=tool_call["id"],
|
126
|
-
content="a_delay_function只是一个占位符,请忽略重新调用工具",
|
127
|
-
additional_kwargs={"name": tool_call["function"]["name"]},
|
128
|
-
)
|
129
|
-
]
|
130
|
-
|
131
|
-
_tool_input = json.loads(function["arguments"] or "{}")
|
132
|
-
# We construct an ToolInvocation from the function_call
|
133
|
-
actions.append(
|
134
|
-
ToolInvocation(
|
135
|
-
tool=function_name,
|
136
|
-
tool_input=_tool_input,
|
137
|
-
)
|
138
|
-
)
|
139
|
-
# We call the tool_executor and get back a response
|
140
|
-
responses = await tool_executor.abatch(actions, **kwargs)
|
141
|
-
# We use the response to create a ToolMessage
|
142
|
-
tool_messages = []
|
143
|
-
for tool_call, response in zip(get_tools(last_message), responses):
|
144
|
-
if not isinstance(response, (str, int, float, bool, list, tuple)):
|
145
|
-
response = repr(
|
146
|
-
response
|
147
|
-
) # 不支持其他类型,包括dict也不支持,因此需要转换为字符串
|
148
|
-
|
149
|
-
message = ToolMessage(
|
150
|
-
tool_call_id=tool_call["id"],
|
151
|
-
content=response,
|
152
|
-
additional_kwargs={"name": tool_call["function"]["name"]},
|
153
|
-
)
|
154
|
-
tool_messages.append(message)
|
155
|
-
if tool_compatibility_mode:
|
156
|
-
# HumanMessage
|
157
|
-
tool_msgs_str = repr(tool_messages)
|
158
|
-
tool_messages = [HumanMessage(content=HumanToolString + tool_msgs_str)]
|
159
|
-
return tool_messages
|
160
|
-
|
161
|
-
workflow = MessageGraph()
|
162
|
-
|
163
|
-
# Define the two nodes we will cycle between
|
164
|
-
workflow.add_node("agent", agent)
|
165
|
-
workflow.add_node("call_tool", call_tool)
|
166
|
-
|
167
|
-
workflow.set_entry_point("agent")
|
168
|
-
|
169
|
-
workflow.add_conditional_edges(
|
170
|
-
"agent",
|
171
|
-
should_continue,
|
172
|
-
{
|
173
|
-
"continue": "call_tool",
|
174
|
-
"end": END,
|
175
|
-
},
|
176
|
-
)
|
177
|
-
|
178
|
-
# 调用完工具后,再次调用agent
|
179
|
-
workflow.add_edge("call_tool", "agent")
|
180
|
-
|
181
|
-
return workflow.compile()
|
pycoze/bot/agent/assistant.py
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
from typing import Sequence
|
2
|
-
from langchain.tools import BaseTool
|
3
|
-
from langchain_core.language_models.base import LanguageModelLike
|
4
|
-
from langchain_core.runnables import RunnableBinding
|
5
|
-
from .agent_types import create_openai_func_call_agent_executor
|
6
|
-
|
7
|
-
|
8
|
-
class Runnable(RunnableBinding):
|
9
|
-
agent_execution_mode: str
|
10
|
-
tools: Sequence[BaseTool]
|
11
|
-
llm: LanguageModelLike
|
12
|
-
assistant_message: str
|
13
|
-
|
14
|
-
def __init__(
|
15
|
-
self,
|
16
|
-
*,
|
17
|
-
agent_execution_mode: str,
|
18
|
-
tools: Sequence[BaseTool],
|
19
|
-
llm: LanguageModelLike,
|
20
|
-
assistant_message: str,
|
21
|
-
tool_compatibility_mode: bool
|
22
|
-
) -> None:
|
23
|
-
|
24
|
-
agent_executor = create_openai_func_call_agent_executor(
|
25
|
-
tools, llm, assistant_message, tool_compatibility_mode
|
26
|
-
)
|
27
|
-
agent_executor = agent_executor.with_config({"recursion_limit": 50})
|
28
|
-
super().__init__(
|
29
|
-
tools=tools if not tool_compatibility_mode else [],
|
30
|
-
llm=llm,
|
31
|
-
agent_execution_mode=agent_execution_mode,
|
32
|
-
assistant_message=assistant_message,
|
33
|
-
bound=agent_executor,
|
34
|
-
return_intermediate_steps=True,
|
35
|
-
)
|