pycoze 0.1.307__py3-none-any.whl → 0.1.334__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 CHANGED
@@ -1,3 +1,3 @@
1
- from .bot import chat, get_chat_response
1
+ from .chat import chat, get_chat_response
2
2
 
3
3
  __all__ = [chat, get_chat_response]
pycoze/bot/chat.py ADDED
@@ -0,0 +1,99 @@
1
+ import json
2
+ from .chat_base import handle_user_inputs
3
+ from .lib import get_abilities, get_system_prompt
4
+ from .message import INPUT_MESSAGE, output, CHAT_DATA, clear_chat_data
5
+ import os
6
+ import asyncio
7
+ from pycoze import utils
8
+ import tempfile
9
+
10
+
11
+ async def check_interrupt_file(interval, interrupt_file, chat_task):
12
+ while True:
13
+ await asyncio.sleep(interval)
14
+ if os.path.exists(interrupt_file):
15
+ os.remove(interrupt_file)
16
+ chat_task.cancel()
17
+ break
18
+
19
+
20
+ async def run_with_interrupt_check(conversation_history, user_input, cwd: str, abilities, bot_setting, has_any_tool, interrupt_file):
21
+ clear_chat_data()
22
+ try:
23
+ chat_task = asyncio.create_task(
24
+ handle_user_inputs(conversation_history, user_input, cwd, abilities, bot_setting, has_any_tool)
25
+ )
26
+ check_task = asyncio.create_task(
27
+ check_interrupt_file(0.5, interrupt_file, chat_task)
28
+ )
29
+ result = await chat_task
30
+ return result
31
+ except asyncio.CancelledError:
32
+ return CHAT_DATA["info"]
33
+ except Exception as e:
34
+ import traceback
35
+
36
+ print(traceback.format_exc())
37
+ return None # 返回 None 或者处理异常后的结果
38
+ finally:
39
+ if not chat_task.done():
40
+ chat_task.cancel()
41
+ # 确保即使发生异常也会取消检查任务
42
+ if not check_task.done():
43
+ check_task.cancel()
44
+ try:
45
+ await check_task
46
+ except asyncio.CancelledError:
47
+ pass # 忽略取消错误
48
+
49
+
50
+ def chat(bot_setting_file: str):
51
+ with open(bot_setting_file, encoding="utf-8") as f:
52
+ bot_setting = json.load(f)
53
+ abilities = get_abilities(bot_setting)
54
+ cwd = tempfile.mkdtemp()
55
+ system_prompt, has_any_tool = get_system_prompt(abilities, bot_setting)
56
+ conversation_history = [
57
+ {
58
+ "role": "system",
59
+ "content": system_prompt,
60
+ }
61
+ ]
62
+ while True:
63
+ clear_chat_data()
64
+ input_text = input()
65
+ if not input_text.startswith(INPUT_MESSAGE):
66
+ raise ValueError("Invalid message")
67
+ message = json.loads(input_text[len(INPUT_MESSAGE) :])
68
+ user_input = message["content"]
69
+ params = utils.params
70
+ if "interruptFile" in params:
71
+ asyncio.run(
72
+ run_with_interrupt_check(
73
+ conversation_history, user_input, cwd, abilities, bot_setting, has_any_tool, params["interruptFile"]
74
+ )
75
+ )
76
+ else:
77
+ asyncio.run(
78
+ handle_user_inputs(conversation_history, user_input, cwd, abilities, bot_setting, has_any_tool)
79
+ )
80
+
81
+ output("assistant", CHAT_DATA["info"])
82
+
83
+
84
+ def get_chat_response(bot_setting_file: str, user_input: str):
85
+ with open(bot_setting_file, encoding="utf-8") as f:
86
+ bot_setting = json.load(f)
87
+ abilities = get_abilities(bot_setting)
88
+ cwd = tempfile.mkdtemp()
89
+ conversation_history = [
90
+ {
91
+ "role": "system",
92
+ "content": get_system_prompt(abilities, bot_setting),
93
+ }
94
+ ]
95
+ asyncio.run(
96
+ handle_user_inputs(conversation_history, user_input, cwd, abilities, bot_setting)
97
+ )
98
+
99
+ return CHAT_DATA["info"]
@@ -0,0 +1,247 @@
1
+ import json
2
+ import os
3
+ from .lib import get_formatted_filelist_str, read_local_file, resolve_relative_path
4
+ from .message import info
5
+ from pycoze.ai import chat_stream_async, extract
6
+ from .tools import ToolExecutor
7
+ from typing import List
8
+
9
+
10
+ def guess_files_in_message(cwd:str, user_message: str) -> List[str]:
11
+
12
+ value = extract({"includedFiles": ['relative path format', 'relative path format', '...']},
13
+ 'Please find the files mentioned in the text. If none, return {"includedFiles": []}:\n' + user_message)
14
+ return [resolve_relative_path(cwd, p) for p in value["includedFiles"]]
15
+
16
+
17
+ def user_task_prompt(conversation_history, cwd, user_input: str, programmer_mode: bool):
18
+ if programmer_mode:
19
+ potential_paths = guess_files_in_message(cwd, user_input)
20
+
21
+ exist_files = get_formatted_filelist_str(cwd, True, 200)
22
+ content = []
23
+ for file_path in potential_paths:
24
+ file_path = resolve_relative_path(cwd, file_path)
25
+ if os.path.isfile(file_path):
26
+ file_marker = f"[[{file_path}]]'"
27
+ file_content = read_local_file(file_path)
28
+ if not any(file_marker in msg["content"] for msg in conversation_history):
29
+ content.append(f"{file_marker}\n{file_content}")
30
+ content_str = "Partial contents of files are as follows:" + "\n".join(content) if content else ""
31
+ return f"""
32
+ <task>
33
+ {user_input}
34
+ </task>
35
+
36
+ <environment_details>
37
+ Current working directory: {cwd}
38
+
39
+ List of files under path:
40
+ {exist_files}
41
+
42
+ {content_str}
43
+
44
+ </environment_details>
45
+ """
46
+ else:
47
+ return f"""
48
+ <task>
49
+ {user_input}
50
+ </task>
51
+ """
52
+
53
+
54
+ def dumps_markdown_json(data):
55
+ json_str = json.dumps(data, indent=4, ensure_ascii=False)
56
+ return f"\n```json\n{json_str}\n```\n"
57
+
58
+
59
+ def process_code_block(language: str, content: str):
60
+ """
61
+ 处理并清理代码块内容,返回格式化后的字符串
62
+ """
63
+ if language and content:
64
+ # 清理代码块内容
65
+ cleaned_content = "\n".join(
66
+ line.strip() for line in content.split("\n") if line.strip()
67
+ )
68
+ return (language, cleaned_content)
69
+ return ""
70
+
71
+
72
+ async def stream_openai_response(conversation_history, start_new_stream):
73
+ """
74
+ 异步流式传输 OpenAI 聊天完成响应并处理结构化输出
75
+ """
76
+ stream = None
77
+ buffer = ""
78
+ in_code_block = False
79
+ code_block_language = ""
80
+ code_block_content = ""
81
+ text_content = ""
82
+
83
+ while True:
84
+ # 检查是否需要重新创建流
85
+ if stream is None or start_new_stream["value"]:
86
+ if stream is not None:
87
+ await stream.aclose() # 关闭之前的流
88
+ stream = chat_stream_async(conversation_history) # 获取新的异步生成器
89
+ start_new_stream["value"] = False # 重置标志
90
+ buffer = ""
91
+ in_code_block = False
92
+ code_block_language = ""
93
+ code_block_content = ""
94
+ text_content = ""
95
+
96
+ # 使用 async for 迭代异步生成器
97
+ try:
98
+ async for chunk in stream:
99
+ info("assistant", chunk)
100
+ buffer += chunk
101
+
102
+ # 检查是否需要重新创建流
103
+ if start_new_stream["value"]:
104
+ break # 退出当前的 async for 循环,进入下一次 while 循环
105
+
106
+ # 处理 buffer 中的每一行
107
+ while "\n" in buffer:
108
+ line, buffer = buffer.split("\n", 1)
109
+ if not in_code_block:
110
+ if line.strip().startswith("```"):
111
+ if text_content:
112
+ yield ("text", text_content.strip())
113
+ text_content = ""
114
+ in_code_block = True
115
+ code_block_language = line.strip()[3:].strip()
116
+ else:
117
+ text_content += line + "\n"
118
+ else:
119
+ if line.strip().startswith("```"):
120
+ in_code_block = False
121
+ yield process_code_block(
122
+ code_block_language, code_block_content
123
+ )
124
+ code_block_content = ""
125
+ else:
126
+ code_block_content += line + "\n"
127
+
128
+ # 如果流正常结束,退出 while 循环
129
+ break
130
+
131
+ except Exception as e:
132
+ # 捕获其他异常(如网络错误)
133
+ print(f"Error: {e}", style="bold red")
134
+ break
135
+
136
+ # 处理 buffer 中剩余的内容
137
+ if buffer:
138
+ if in_code_block:
139
+ buffer = buffer.split("```")[0]
140
+ code_block_content += buffer + "\n"
141
+ yield process_code_block(code_block_language, code_block_content)
142
+ else:
143
+ text_content += buffer
144
+ if text_content:
145
+ yield ("text", text_content.strip())
146
+
147
+
148
+ async def handle_user_inputs(conversation_history, user_input, cwd, abilities, bot_setting, has_any_tool):
149
+ no_exit_if_incomplete = bot_setting["systemAbility"]['no_exit_if_incomplete']
150
+ programmer_mode = bot_setting["systemAbility"]['programmer_mode']
151
+
152
+ start_new_stream = {
153
+ "value": False
154
+ } # 当遇到AI准备执行JSON,即需要新信息的时候,用于强制停止当前stream,减少后续无效的tokens
155
+
156
+ print("Processing user command", user_input)
157
+ if user_input.lower() in ["exit", "quit"]:
158
+ exit(0)
159
+ # 将用户消息添加到对话历史
160
+ conversation_history.append(
161
+ {
162
+ "role": "user",
163
+ "content": user_task_prompt(conversation_history, cwd, user_input, programmer_mode),
164
+ }
165
+ )
166
+ need_break = False
167
+
168
+ if no_exit_if_incomplete:
169
+ okay_str = 'Okay, please continue. If the tasks within <task>...task content...</task> have been completed, execute the tool "complete_all_tasks". If you have a question, use "ask_follow_up_question".'
170
+ else:
171
+ okay_str = "Okay"
172
+ while True:
173
+ async for response in stream_openai_response(
174
+ conversation_history, start_new_stream
175
+ ):
176
+ if len(response) == 2:
177
+ if response[0] == "text" and response[1].strip() != "" or (response[0] == "json" and not has_any_tool):
178
+ conversation_history.append(
179
+ {"role": "assistant", "content": response[1]}
180
+ )
181
+ conversation_history.append(
182
+ {
183
+ "role": "user",
184
+ "content": okay_str,
185
+ }
186
+ )
187
+ continue
188
+ elif response[0] == "json":
189
+ info("assistant", "\n")
190
+ cleaned_content = response[1]
191
+ try:
192
+ tool_request = json.loads(cleaned_content)
193
+ tool_name = list(tool_request.keys())[0]
194
+ except json.JSONDecodeError as e:
195
+ conversation_history.append(
196
+ {
197
+ "role": "assistant",
198
+ "content": f"\n```json\n{cleaned_content}\n```\n",
199
+ }
200
+ )
201
+ conversation_history.append(
202
+ {
203
+ "role": "user",
204
+ "content": "Invalid JSON content:" + str(e),
205
+ }
206
+ )
207
+ continue
208
+
209
+ ok, result = ToolExecutor.execute_tool(cwd, tool_request, abilities)
210
+ if ok:
211
+ info("assistant", "✅\n")
212
+ else:
213
+ info("assistant", "❌\n")
214
+ assistant_content = (
215
+ "Executing tool: "
216
+ + dumps_markdown_json(tool_request)
217
+ + "\n\nResult: "
218
+ + result
219
+ )
220
+ info("assistant", "```text\n" + result + "\n```\n\n")
221
+ conversation_history.append(
222
+ {"role": "assistant", "content": assistant_content}
223
+ )
224
+ if tool_name in ["complete_all_tasks", 'ask_follow_up_question']:
225
+ need_break = True
226
+ break
227
+ else:
228
+ conversation_history.append(
229
+ {
230
+ "role": "user",
231
+ "content": okay_str,
232
+ }
233
+ )
234
+ start_new_stream["value"] = True
235
+
236
+ if need_break:
237
+ break
238
+ if not no_exit_if_incomplete and not start_new_stream["value"]:
239
+ break
240
+
241
+
242
+ # 示例调用
243
+ # user_input_list = [
244
+ # "访问https://api-docs.deepseek.com/zh-cn/guides/chat_prefix_completion,并结合它编写一段代码,并保存"
245
+ # ]
246
+
247
+ # asyncio.run(handle_user_inputs(user_input_list))
pycoze/bot/lib.py ADDED
@@ -0,0 +1,245 @@
1
+ import os
2
+ from collections import defaultdict
3
+ import platform
4
+ from jinja2 import Template
5
+ from pathlib import Path
6
+ from pycoze.reference.bot import ref_bot
7
+ from pycoze.reference.tool import ref_tools
8
+ from pycoze.reference.workflow import ref_workflow
9
+ import inspect
10
+
11
+
12
+ def function_to_string(func):
13
+ # 获取函数签名
14
+ signature = inspect.signature(func)
15
+
16
+ # 获取函数名
17
+ func_name = func.__name__
18
+
19
+ # 获取函数的文档字符串
20
+ docstring = func.__doc__
21
+
22
+ # 格式化函数签名
23
+ func_signature = f"def {func_name}{signature}:"
24
+
25
+ # 构建最终的字符串
26
+ result = f"#### {func_name.capitalize().replace('_', ' ')}\n"
27
+ result += "**Function Signature:**\n"
28
+ result += f"```python\n{func_signature}\n \"\"\"{docstring}\n \"\"\"\n pass\n```\n\n"
29
+ result += "**Usage:**\n"
30
+ result += "```json\n"
31
+ result += f"{{\n \"{func_name}\": {{\n"
32
+
33
+ # 添加参数
34
+ for name, param in signature.parameters.items():
35
+ result += f" \"{name}\": \"{param.annotation.__name__ if param.annotation != inspect.Parameter.empty else 'value'}\"\n"
36
+
37
+ result += " }\n"
38
+ result += "}\n"
39
+ result += "```"
40
+
41
+ return "\n\n" + result + "\n\n"
42
+
43
+ def get_abilities(bot_setting):
44
+ abilities = []
45
+ for bot_id in bot_setting["bots"]:
46
+ bot = ref_bot(bot_id)
47
+ if bot:
48
+ abilities.append(bot)
49
+ for tool_id in bot_setting["tools"]:
50
+ abilities.extend(ref_tools(tool_id))
51
+ for workflow_id in bot_setting["workflows"]:
52
+ workflow = ref_workflow(workflow_id)
53
+ if workflow:
54
+ abilities.append(workflow)
55
+ return abilities
56
+
57
+
58
+ def get_system_prompt(abilities, bot_setting):
59
+ md_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "prompt.md")
60
+ with open(md_file, encoding="utf-8") as f:
61
+ template = Template(f.read())
62
+
63
+ if platform.system() == "Windows":
64
+ cd_prompt = "When executing outside the working directory, include the CD command, such as cd /path/to/directory ; ls."
65
+ else:
66
+ cd_prompt = "When executing outside the working directory, include the CD command, such as cd /path/to/directory && ls."
67
+ system = platform.system()
68
+
69
+
70
+ abilities_str = "\n".join([function_to_string(a) for a in abilities])
71
+
72
+ context = {
73
+ "prompt": bot_setting["prompt"],
74
+ "system": system,
75
+ "cd_prompt": cd_prompt,
76
+ "abilities_str": abilities_str,
77
+ "programmer_mode": False,
78
+ "no_exit_if_incomplete": False,
79
+ "allow_read_file": False,
80
+ "allow_read_multiple_files": False,
81
+ "allow_execute_command": False,
82
+ "allow_write_or_overwrite_file": False,
83
+ "allow_replace_part_of_a_file": False,
84
+ "allow_search_files": False,
85
+ "allow_list_files": False,
86
+ "allow_access_webpage": False,
87
+ }
88
+
89
+ context.update(bot_setting["systemAbility"])
90
+
91
+ has_any_tool = False
92
+ if len(abilities) > 0:
93
+ has_any_tool = True
94
+ for key in bot_setting["systemAbility"]:
95
+ if key != "programmer_mode" and bot_setting["systemAbility"][key] == True:
96
+ has_any_tool = True
97
+ break
98
+
99
+ context["has_any_tool"] = has_any_tool
100
+ system_prompt = template.render(context)
101
+
102
+ return system_prompt, has_any_tool
103
+
104
+
105
+ def resolve_relative_path(cwd:str, path_str: str) -> str:
106
+ """返回基于CWD的规范化绝对路径"""
107
+ path = Path(path_str)
108
+ if path.is_absolute():
109
+ return str(path.resolve())
110
+ else:
111
+ return str((Path(cwd) / path_str).resolve())
112
+
113
+
114
+ def read_local_file(file_path: str) -> str:
115
+ """读取本地文件内容"""
116
+ try:
117
+ with open(file_path, "r", encoding="utf-8") as f:
118
+ return f.read()
119
+ except:
120
+ import chardet
121
+
122
+ with open(file_path, "rb") as f:
123
+ rawdata = f.read()
124
+ encoding = chardet.detect(rawdata)["encoding"]
125
+ return rawdata.decode(encoding)
126
+
127
+
128
+ # 全局忽略列表
129
+ IGNORE_LIST = [
130
+ "node_modules",
131
+ ".git",
132
+ ".vscode",
133
+ ".idea",
134
+ "gitServer",
135
+ ".DS_Store",
136
+ "$RECYCLE.BIN",
137
+ ".Trash-1000",
138
+ ".Spotlight-V100",
139
+ ".Trashes",
140
+ ".TemporaryItems",
141
+ ".fseventsd",
142
+ "System Volume Information",
143
+ "pycache",
144
+ "env",
145
+ "venv",
146
+ "target/dependency",
147
+ "build/dependencies",
148
+ "dist",
149
+ "out",
150
+ "bundle",
151
+ "vendor",
152
+ "tmp",
153
+ "temp",
154
+ "deps",
155
+ "pkg",
156
+ "Pods",
157
+ ]
158
+
159
+
160
+ def should_ignore(path):
161
+ """检查路径是否在忽略列表中"""
162
+ parts = path.split(os.sep)
163
+ return any(part in IGNORE_LIST for part in parts)
164
+
165
+
166
+ def get_files_and_folders(root, recursive: bool):
167
+ """递归获取所有文件和文件夹,并记录属性"""
168
+ items = []
169
+
170
+ # 使用 os.walk 遍历目录
171
+ for dirpath, dirnames, filenames in os.walk(root):
172
+ # 排除忽略列表中的路径
173
+ if should_ignore(dirpath):
174
+ continue
175
+
176
+ # 记录文件夹
177
+ for dirname in dirnames:
178
+ full_path = os.path.join(dirpath, dirname)
179
+ if not should_ignore(full_path):
180
+ relative_path = os.path.relpath(full_path, root)
181
+ is_empty = not os.listdir(full_path)
182
+ depth = relative_path.count(os.sep)
183
+ items.append(
184
+ (relative_path, "empty_folder" if is_empty else "folder", depth)
185
+ )
186
+
187
+ # 记录文件
188
+ for filename in filenames:
189
+ full_path = os.path.join(dirpath, filename)
190
+ if not should_ignore(full_path):
191
+ relative_path = os.path.relpath(full_path, root)
192
+ depth = relative_path.count(os.sep)
193
+ items.append((relative_path, "file", depth))
194
+
195
+ # 如果 recursive 为 False,则只遍历当前目录,不进入子目录
196
+ if not recursive:
197
+ break
198
+
199
+ return items
200
+
201
+
202
+ def format_filelist_str(items, limit):
203
+ """根据limit格式化输出"""
204
+ depth_groups = defaultdict(list)
205
+ for item in items:
206
+ depth_groups[item[2]].append(item)
207
+
208
+ max_depth = max(depth_groups.keys(), default=0)
209
+ show_list = []
210
+ last_depth = 0
211
+
212
+ # 浅层
213
+ current_items = sorted(depth_groups[0], key=lambda x: x[0])
214
+ overflow = len(current_items) > limit
215
+ for item in current_items[:limit]:
216
+ show_list.append(item)
217
+
218
+ for depth in range(1, max_depth + 1):
219
+ current_items = depth_groups[depth]
220
+ if len(show_list) + len(current_items) <= limit:
221
+ last_depth = depth
222
+ for item in current_items:
223
+ show_list.append(item)
224
+ else:
225
+ break
226
+
227
+ result_str_list = []
228
+ show_list.sort(key=lambda x: x[0])
229
+ for item in show_list:
230
+ if item[1] == "file":
231
+ result_str_list.append(f"{item[0]}")
232
+ elif item[1] == "folder" and item[2] == last_depth:
233
+ result_str_list.append(f"{item[0]}/...more...")
234
+ else:
235
+ result_str_list.append(f"{item[0]}/")
236
+ if overflow:
237
+ result_str_list.append("...more...")
238
+
239
+ return "\n".join(result_str_list)
240
+
241
+
242
+ def get_formatted_filelist_str(root: str, recursive: bool, limit=200):
243
+ items = get_files_and_folders(root, recursive)
244
+ return format_filelist_str(items, limit=limit)
245
+
@@ -16,7 +16,6 @@ def log(content, *args, end="\n", **kwargs):
16
16
 
17
17
 
18
18
  def clear_chat_data():
19
- # 注意不能写为CHAT_DATA = {"output":"", "info": ""},这样不会清空原本的内容
20
19
  CHAT_DATA["output"] = ""
21
20
  CHAT_DATA["info"] = ""
22
21