prompt-copilot-cli 0.2.6__tar.gz → 0.2.8__tar.gz
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.
- {prompt_copilot_cli-0.2.6/prompt_copilot_cli.egg-info → prompt_copilot_cli-0.2.8}/PKG-INFO +1 -1
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/main.py +242 -15
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8/prompt_copilot_cli.egg-info}/PKG-INFO +1 -1
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/pyproject.toml +1 -1
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/tests/test_file_tools.py +28 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/LICENSE +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/MANIFEST.in +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/README.md +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/README.zh-CN.md +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/prompt_copilot_cli.egg-info/SOURCES.txt +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/prompt_copilot_cli.egg-info/dependency_links.txt +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/prompt_copilot_cli.egg-info/entry_points.txt +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/prompt_copilot_cli.egg-info/requires.txt +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/prompt_copilot_cli.egg-info/top_level.txt +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/requirements.txt +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/setup.cfg +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/tests/test_cli_args.py +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/tests/test_command_timeout.py +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/tests/test_image_tool.py +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/tests/test_mcp_transport.py +0 -0
- {prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/tests/test_multimodal_messages.py +0 -0
|
@@ -111,7 +111,9 @@ DEFAULT_SYSTEM_WORKING_PRINCIPLES_ZH = """工作原则:
|
|
|
111
111
|
12、使用 execute_python_script 时必须在脚本内自行管理进程生命周期:若是 ls、pytest、build 等短命令用 subprocess.run 显式设置 timeout,若是 npm run dev、flask run 等常驻服务用 subprocess.Popen 脱离终端启动并在脚本内轮询健康检查 URL,确认就绪后打印 PID 并立即 sys.exit() 退出以防触发 360 秒强制超时。
|
|
112
112
|
13、无法调用官方联网检索工具而通过脚本抓取网络内容时需区分数据类型处理,抓取普通网页文本需剔除 HTML 标签、样式代码、广告碎片、无效注释、多余空行等冗余内容,仅留存有效正文且文本超出 8000 字符则截断,文本输出上限为 8000 字符,抓取程序源代码则无需过滤,完整保留原始代码、自带注释、缩进换行与原有格式,代码输出无字符数量限制。
|
|
113
113
|
14、脱离终端启动常驻服务时务必兼顾跨平台兼容:Linux/Mac 环境设置 start_new_session=True,Windows 环境设置 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP。
|
|
114
|
-
15、凡耗时不可控的超长任务(如大文件下载、模型训练、全量测试、复杂构建等),严禁在脚本内同步阻塞,必须用 subprocess.Popen 跨平台脱离终端启动并将进度心跳写入本地日志文件,打印 PID 后立即 sys.exit(),工具 timeout_seconds 设为 15 秒仅验证启动;后续须通过检查日志监控进度,若发现任务完成或长时间无心跳更新,必须主动调用系统命令(如 kill -9 PID)清理后台进程以防孤儿进程耗尽资源;仅在任务自身支持超时参数或耗时可预估时,方可在脚本内同步执行并设合理 timeout_seconds。
|
|
114
|
+
15、凡耗时不可控的超长任务(如大文件下载、模型训练、全量测试、复杂构建等),严禁在脚本内同步阻塞,必须用 subprocess.Popen 跨平台脱离终端启动并将进度心跳写入本地日志文件,打印 PID 后立即 sys.exit(),工具 timeout_seconds 设为 15 秒仅验证启动;后续须通过检查日志监控进度,若发现任务完成或长时间无心跳更新,必须主动调用系统命令(如 kill -9 PID)清理后台进程以防孤儿进程耗尽资源;仅在任务自身支持超时参数或耗时可预估时,方可在脚本内同步执行并设合理 timeout_seconds。
|
|
115
|
+
16、如果需要搜索互联网信息时候优先使用中国国内搜索引擎优先,例如:bing、百度等。
|
|
116
|
+
"""
|
|
115
117
|
|
|
116
118
|
DEFAULT_SYSTEM_WORKING_PRINCIPLES_EN = """Working principles:
|
|
117
119
|
1. First inspect the working directory and understand the requirement.
|
|
@@ -128,7 +130,9 @@ DEFAULT_SYSTEM_WORKING_PRINCIPLES_EN = """Working principles:
|
|
|
128
130
|
12. When using the execute_python_script tool, manage the process lifecycle within the script: for short commands like ls, pytest, and build, use subprocess.run with an explicit timeout; for persistent commands like npm run dev and flask run, use subprocess.Popen to start them in the background and poll a health check URL to confirm they are ready before printing the PID and exiting immediately to avoid triggering the 360-second timeout.
|
|
129
131
|
13. When official network retrieval tools are unavailable and network content is crawled via scripts, differentiated processing shall be performed according to data types: for regular webpage texts, remove redundant contents including HTML tags, style codes, advertising fragments, invalid comments and redundant blank lines, retain only valid main texts and truncate the content if it exceeds 8000 characters with a strict upper limit of 8000 characters for text output; for program source codes, no filtering or cleaning is required, keep the original codes, built-in comments, indentations, line breaks and original formats completely with no character limit on code output.
|
|
130
132
|
14. When starting a persistent service in the background, ensure cross-platform compatibility: set start_new_session=True for Linux/Mac environments and creationflags=subprocess.CREATE_NEW_PROCESS_GROUP for Windows environments.
|
|
131
|
-
15. For long-running tasks with unpredictable durations (such as large file downloads, model training, full tests, complex builds, etc.), synchronous blocking execution within the script is strictly prohibited. Instead, use subprocess.Popen to start the task in the background across platforms and write progress heartbeats to a local log file. After printing the PID, immediately call sys.exit() and set the tool's timeout_seconds to 15 seconds for startup verification only. Subsequently, monitor progress by checking the log file. If the task completes or there is no heartbeat update for an extended period, proactively call system commands (like kill -9 PID) to clean up background processes and prevent orphan processes from consuming resources. Only when the task itself supports timeout parameters or has a predictable duration can synchronous execution be performed within the script with a reasonable timeout_seconds setting.
|
|
133
|
+
15. For long-running tasks with unpredictable durations (such as large file downloads, model training, full tests, complex builds, etc.), synchronous blocking execution within the script is strictly prohibited. Instead, use subprocess.Popen to start the task in the background across platforms and write progress heartbeats to a local log file. After printing the PID, immediately call sys.exit() and set the tool's timeout_seconds to 15 seconds for startup verification only. Subsequently, monitor progress by checking the log file. If the task completes or there is no heartbeat update for an extended period, proactively call system commands (like kill -9 PID) to clean up background processes and prevent orphan processes from consuming resources. Only when the task itself supports timeout parameters or has a predictable duration can synchronous execution be performed within the script with a reasonable timeout_seconds setting.
|
|
134
|
+
16. When searching the internet for information, prioritize domestic Chinese search engines such as Bing or Baidu.
|
|
135
|
+
"""
|
|
132
136
|
|
|
133
137
|
TASK_END_GUIDANCE_ZH = """你是提示词优化专家,负责将对话记录整理为更清晰、可执行的最终任务提示词。
|
|
134
138
|
|
|
@@ -215,6 +219,8 @@ TRANSLATIONS = {
|
|
|
215
219
|
"copy_file_desc": {"zh": "复制文件。", "en": "Copy a file."},
|
|
216
220
|
"read_image_as_base64_desc": {"zh": "读取图片文件并将其编码为 base64。", "en": "Read an image file and encode it as base64."},
|
|
217
221
|
"list_dir_desc": {"zh": "列出目录中的文件和子目录。", "en": "List files and subdirectories in the given directory."},
|
|
222
|
+
"search_code_desc": {"zh": "在目录中按文本模式搜索匹配内容。", "en": "Search for matching text within a directory."},
|
|
223
|
+
"edit_file_desc": {"zh": "在文件中替换指定文本。", "en": "Replace a specific string in a file."},
|
|
218
224
|
"execute_command_desc": {"zh": "在指定目录下执行 shell 命令。", "en": "Execute a shell command in the specified directory."},
|
|
219
225
|
"execute_python_script_desc": {"zh": "执行一个 Python 脚本或一段 Python 代码。", "en": "Execute a Python script or a block of Python code."},
|
|
220
226
|
"path_desc": {"zh": "要读取的文件路径。", "en": "Path of the file to read."},
|
|
@@ -322,7 +328,7 @@ def t(key: str, **kwargs: Any) -> str:
|
|
|
322
328
|
|
|
323
329
|
|
|
324
330
|
UI_SYSTEM_LANGUAGE = "en"
|
|
325
|
-
APPLICATION_VERSION = "0.2.
|
|
331
|
+
APPLICATION_VERSION = "0.2.8"
|
|
326
332
|
DEFAULT_SYSTEM_PROMPT = t("system_prompt")
|
|
327
333
|
WORKSPACE_DIR = ROOT / "workspace"
|
|
328
334
|
LOG_DIR = ROOT / "logs"
|
|
@@ -508,6 +514,39 @@ TOOL_DEFINITIONS = [
|
|
|
508
514
|
},
|
|
509
515
|
},
|
|
510
516
|
},
|
|
517
|
+
{
|
|
518
|
+
"type": "function",
|
|
519
|
+
"function": {
|
|
520
|
+
"name": "search_code",
|
|
521
|
+
"description": t("search_code_desc"),
|
|
522
|
+
"parameters": {
|
|
523
|
+
"type": "object",
|
|
524
|
+
"properties": {
|
|
525
|
+
"path": {"type": "string", "description": t("list_dir_path_desc")},
|
|
526
|
+
"pattern": {"type": "string", "description": "Text pattern to search for."},
|
|
527
|
+
"recursive": {"type": "boolean", "description": t("list_dir_recursive_desc")},
|
|
528
|
+
"max_results": {"type": "integer", "description": "Maximum number of matches to return."}
|
|
529
|
+
},
|
|
530
|
+
"required": ["path", "pattern"],
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
"type": "function",
|
|
536
|
+
"function": {
|
|
537
|
+
"name": "edit_file",
|
|
538
|
+
"description": t("edit_file_desc"),
|
|
539
|
+
"parameters": {
|
|
540
|
+
"type": "object",
|
|
541
|
+
"properties": {
|
|
542
|
+
"path": {"type": "string", "description": t("write_path_desc")},
|
|
543
|
+
"old_string": {"type": "string", "description": "The text to replace."},
|
|
544
|
+
"new_string": {"type": "string", "description": "The replacement text."}
|
|
545
|
+
},
|
|
546
|
+
"required": ["path", "old_string", "new_string"],
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
511
550
|
{
|
|
512
551
|
"type": "function",
|
|
513
552
|
"function": {
|
|
@@ -1622,6 +1661,94 @@ def execute_tool_call(tool_call: Any) -> dict[str, Any]:
|
|
|
1622
1661
|
logger.info("list_dir result: %s", result)
|
|
1623
1662
|
return result
|
|
1624
1663
|
|
|
1664
|
+
if name == "search_code":
|
|
1665
|
+
file_path = args.get("path") or args.get("file_path")
|
|
1666
|
+
pattern = args.get("pattern")
|
|
1667
|
+
if not file_path or not pattern:
|
|
1668
|
+
result = {"status": "error", "content": t("tool_missing_arg", name="search_code", arg="path/pattern")}
|
|
1669
|
+
logger.error("search_code missing arguments, raw args: %s", args)
|
|
1670
|
+
return result
|
|
1671
|
+
|
|
1672
|
+
path = Path(file_path).expanduser()
|
|
1673
|
+
if not path.is_absolute():
|
|
1674
|
+
path = Path.cwd() / path
|
|
1675
|
+
if not path.exists():
|
|
1676
|
+
result = {"status": "ok", "content": json.dumps([], ensure_ascii=False)}
|
|
1677
|
+
logger.info("search_code result: %s", result)
|
|
1678
|
+
return result
|
|
1679
|
+
|
|
1680
|
+
recursive = bool(args.get("recursive", True))
|
|
1681
|
+
max_results = args.get("max_results")
|
|
1682
|
+
try:
|
|
1683
|
+
max_results = int(max_results) if max_results is not None else 50
|
|
1684
|
+
except (TypeError, ValueError):
|
|
1685
|
+
max_results = 50
|
|
1686
|
+
|
|
1687
|
+
matches: list[dict[str, Any]] = []
|
|
1688
|
+
if path.is_file():
|
|
1689
|
+
candidates = [path]
|
|
1690
|
+
elif recursive:
|
|
1691
|
+
candidates = [item for item in sorted(path.rglob("*")) if item.is_file()]
|
|
1692
|
+
else:
|
|
1693
|
+
candidates = [item for item in sorted(path.iterdir()) if item.is_file()]
|
|
1694
|
+
|
|
1695
|
+
for candidate in candidates:
|
|
1696
|
+
try:
|
|
1697
|
+
content = candidate.read_text(encoding="utf-8")
|
|
1698
|
+
except (UnicodeDecodeError, OSError):
|
|
1699
|
+
continue
|
|
1700
|
+
for line_number, line in enumerate(content.splitlines(), start=1):
|
|
1701
|
+
if pattern in line:
|
|
1702
|
+
matches.append({
|
|
1703
|
+
"path": str(candidate.resolve()),
|
|
1704
|
+
"line": line_number,
|
|
1705
|
+
"content": line,
|
|
1706
|
+
})
|
|
1707
|
+
if len(matches) >= max_results:
|
|
1708
|
+
break
|
|
1709
|
+
if len(matches) >= max_results:
|
|
1710
|
+
break
|
|
1711
|
+
|
|
1712
|
+
result = {"status": "ok", "content": json.dumps(matches, ensure_ascii=False)}
|
|
1713
|
+
logger.info("search_code result: %s", result)
|
|
1714
|
+
return result
|
|
1715
|
+
|
|
1716
|
+
if name == "edit_file":
|
|
1717
|
+
file_path = args.get("path") or args.get("file_path")
|
|
1718
|
+
old_string = args.get("old_string")
|
|
1719
|
+
new_string = args.get("new_string")
|
|
1720
|
+
if not file_path or old_string is None or new_string is None:
|
|
1721
|
+
result = {"status": "error", "content": t("tool_missing_arg", name="edit_file", arg="path/old_string/new_string")}
|
|
1722
|
+
logger.error("edit_file missing arguments, raw args: %s", args)
|
|
1723
|
+
return result
|
|
1724
|
+
|
|
1725
|
+
path = Path(file_path).expanduser()
|
|
1726
|
+
if not path.is_absolute():
|
|
1727
|
+
path = Path.cwd() / path
|
|
1728
|
+
if not path.exists():
|
|
1729
|
+
result = {"status": "error", "content": f"File does not exist: {path}"}
|
|
1730
|
+
logger.info("edit_file result: %s", result)
|
|
1731
|
+
return result
|
|
1732
|
+
|
|
1733
|
+
try:
|
|
1734
|
+
original_text = path.read_text(encoding="utf-8")
|
|
1735
|
+
except Exception as exc:
|
|
1736
|
+
result = {"status": "error", "content": f"Failed to read file: {path}\n{exc}"}
|
|
1737
|
+
logger.exception("edit_file failed: %s", path)
|
|
1738
|
+
logger.info("edit_file result: %s", result)
|
|
1739
|
+
return result
|
|
1740
|
+
|
|
1741
|
+
if old_string not in original_text:
|
|
1742
|
+
result = {"status": "error", "content": f"Target text not found in file: {path}"}
|
|
1743
|
+
logger.info("edit_file result: %s", result)
|
|
1744
|
+
return result
|
|
1745
|
+
|
|
1746
|
+
updated_text = original_text.replace(old_string, new_string, 1)
|
|
1747
|
+
path.write_text(updated_text, encoding="utf-8")
|
|
1748
|
+
result = {"status": "ok", "content": f"Updated: {path}"}
|
|
1749
|
+
logger.info("edit_file result: %s", result)
|
|
1750
|
+
return result
|
|
1751
|
+
|
|
1625
1752
|
if name == "execute_python_script":
|
|
1626
1753
|
script = args.get("script")
|
|
1627
1754
|
if not script:
|
|
@@ -1715,10 +1842,12 @@ def show_tool_result(tool_call: Any, result: dict[str, Any]) -> None:
|
|
|
1715
1842
|
)
|
|
1716
1843
|
|
|
1717
1844
|
|
|
1718
|
-
def chat_once(client: OpenAI, model: str, messages: list[dict[str, Any]], temperature: float, debug_enabled: bool = False) -> Any:
|
|
1845
|
+
def chat_once(client: OpenAI, model: str, messages: list[dict[str, Any]], temperature: float, debug_enabled: bool = False, disable_tools:bool = False) -> Any:
|
|
1719
1846
|
ensure_not_interrupted()
|
|
1720
1847
|
wait_for_model_call_interval()
|
|
1721
1848
|
tool_definitions = TOOL_DEFINITIONS + ACTIVE_MCP_TOOL_DEFINITIONS
|
|
1849
|
+
if disable_tools:
|
|
1850
|
+
tool_definitions = []
|
|
1722
1851
|
request_payload = {
|
|
1723
1852
|
"model": model,
|
|
1724
1853
|
"messages": messages,
|
|
@@ -1915,16 +2044,38 @@ def plan_user_request(client: OpenAI, model: str, history: list[dict[str, Any]],
|
|
|
1915
2044
|
if is_special_command(user_text):
|
|
1916
2045
|
return user_text
|
|
1917
2046
|
|
|
1918
|
-
planning_prompt =
|
|
2047
|
+
planning_prompt = f"""
|
|
2048
|
+
你是一个编程行业需求分析专家,你能够通过历史上下文和用户的输入分析出用户的真实意图,能够拆分步骤,并且能够生成任务执行清单。
|
|
2049
|
+
|
|
2050
|
+
### 请严格按照以下格式输出任务清单:
|
|
2051
|
+
|
|
2052
|
+
用户原始指令:......
|
|
2053
|
+
|
|
2054
|
+
结合上下文得到用户的完整意图:.....
|
|
2055
|
+
|
|
2056
|
+
接下来按照这个步骤逐步执行完成任务:
|
|
2057
|
+
1、第一步:......
|
|
2058
|
+
2、第二步:......
|
|
2059
|
+
......
|
|
2060
|
+
|
|
2061
|
+
"""
|
|
2062
|
+
|
|
2063
|
+
user_prompt = f"""请结合历史上下文分析用户输入并生成任务清单。
|
|
2064
|
+
### 历史上下文:
|
|
2065
|
+
{json.dumps(history, ensure_ascii=False)}
|
|
2066
|
+
|
|
2067
|
+
### 用户输入:
|
|
2068
|
+
{user_text}
|
|
2069
|
+
"""
|
|
1919
2070
|
|
|
1920
2071
|
planning_messages = [
|
|
1921
2072
|
{"role": "system", "content": planning_prompt},
|
|
1922
|
-
{"role": "user", "content":
|
|
2073
|
+
{"role": "user", "content": user_prompt},
|
|
1923
2074
|
]
|
|
1924
|
-
if history:
|
|
1925
|
-
|
|
2075
|
+
# if history:
|
|
2076
|
+
# planning_messages.insert(1, {"role": "user", "content": "以下是历史上下文:\n" + json.dumps(history, ensure_ascii=False)})
|
|
1926
2077
|
|
|
1927
|
-
assistant_message = chat_once(client, model, planning_messages, temperature=0.2, debug_enabled=debug_enabled)
|
|
2078
|
+
assistant_message = chat_once(client, model, planning_messages, temperature=0.2, debug_enabled=debug_enabled, disable_tools=True)
|
|
1928
2079
|
return (assistant_message.content or "").strip() or user_text
|
|
1929
2080
|
|
|
1930
2081
|
|
|
@@ -2053,6 +2204,68 @@ def run_agent(client: OpenAI, model: str, system_prompt: str, session_store: Ses
|
|
|
2053
2204
|
# if recorder:
|
|
2054
2205
|
# recorder.record_error(traceback.format_exc())
|
|
2055
2206
|
|
|
2207
|
+
import json
|
|
2208
|
+
from typing import List, Union
|
|
2209
|
+
|
|
2210
|
+
def get_content_from_tool_calls(tool_calls: List) -> str:
|
|
2211
|
+
"""
|
|
2212
|
+
Extract the `content` field from a list of tool call objects
|
|
2213
|
+
(as returned by OpenAI's ChatCompletionMessage.tool_calls).
|
|
2214
|
+
|
|
2215
|
+
Args:
|
|
2216
|
+
tool_calls: List[ChatCompletionMessageFunctionToolCall] or similar.
|
|
2217
|
+
Each item should have a `function.arguments` attribute
|
|
2218
|
+
which is a JSON string containing a dict with a `content` key.
|
|
2219
|
+
|
|
2220
|
+
Returns:
|
|
2221
|
+
The extracted `content` string if found, otherwise an empty string ("").
|
|
2222
|
+
"""
|
|
2223
|
+
if not tool_calls:
|
|
2224
|
+
return ""
|
|
2225
|
+
|
|
2226
|
+
for tool_call in tool_calls:
|
|
2227
|
+
try:
|
|
2228
|
+
# 兼容 dict 形式(比如有些 mock / 测试数据)
|
|
2229
|
+
if isinstance(tool_call, dict):
|
|
2230
|
+
func = tool_call.get("function")
|
|
2231
|
+
if not func:
|
|
2232
|
+
continue
|
|
2233
|
+
# func 可能是 dict,也可能是有 arguments 属性的对象
|
|
2234
|
+
if isinstance(func, dict):
|
|
2235
|
+
arguments_str = func.get("arguments")
|
|
2236
|
+
else:
|
|
2237
|
+
arguments_str = getattr(func, "arguments", None)
|
|
2238
|
+
else:
|
|
2239
|
+
# OpenAI SDK 对象:ChatCompletionMessageFunctionToolCall
|
|
2240
|
+
func = getattr(tool_call, "function", None)
|
|
2241
|
+
if func is None:
|
|
2242
|
+
continue
|
|
2243
|
+
# Function.arguments 是一个 JSON 字符串
|
|
2244
|
+
arguments_str = getattr(func, "arguments", None)
|
|
2245
|
+
|
|
2246
|
+
if not arguments_str:
|
|
2247
|
+
continue
|
|
2248
|
+
|
|
2249
|
+
# arguments 本身就是 JSON 字符串,需要 parse
|
|
2250
|
+
if isinstance(arguments_str, str):
|
|
2251
|
+
arguments = json.loads(arguments_str)
|
|
2252
|
+
elif isinstance(arguments_str, dict):
|
|
2253
|
+
arguments = arguments_str
|
|
2254
|
+
else:
|
|
2255
|
+
# 其他类型直接放弃
|
|
2256
|
+
continue
|
|
2257
|
+
|
|
2258
|
+
# 真正取 content 字段
|
|
2259
|
+
if isinstance(arguments, dict) and "content" in arguments:
|
|
2260
|
+
return arguments["content"]
|
|
2261
|
+
|
|
2262
|
+
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
2263
|
+
# 解析失败或结构异常,忽略这条 tool_call
|
|
2264
|
+
continue
|
|
2265
|
+
|
|
2266
|
+
return ""
|
|
2267
|
+
|
|
2268
|
+
|
|
2056
2269
|
|
|
2057
2270
|
def handle_task_end_command(md_path: Path, client: OpenAI, model: str, system_prompt: str, workdir: Path, debug_enabled: bool = False) -> None:
|
|
2058
2271
|
"""Process recent_conversations.md to find the most recent /task-start and use the
|
|
@@ -2101,20 +2314,34 @@ def handle_task_end_command(md_path: Path, client: OpenAI, model: str, system_pr
|
|
|
2101
2314
|
compiled_text = "\n\n".join(["## Round " + r for r in selected])
|
|
2102
2315
|
|
|
2103
2316
|
# Build messages for the model
|
|
2104
|
-
sys_prompt =
|
|
2317
|
+
sys_prompt = f"""
|
|
2318
|
+
你是提示词优化专家,负责将对话记录整理为更清晰、可执行的最终任务提示词。
|
|
2105
2319
|
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2320
|
+
### 生成最终提示词时,请严格遵循以下约束:
|
|
2321
|
+
1. 只输出最终改进后的提示词文本,不要添加任何解释、元信息或注释。
|
|
2322
|
+
2. 保留用户原始目标、关键上下文、澄清或变更后的要求、待执行步骤与预期产物。
|
|
2323
|
+
3. 语言必须清晰、可执行、步骤化,适合直接给后续执行器使用。
|
|
2324
|
+
4. 如果上下文中存在不确定信息,应显式写成“待确认”或“需验证”的约束项。
|
|
2325
|
+
5. 输出长度控制在 18000 字符以内。
|
|
2326
|
+
|
|
2327
|
+
"""
|
|
2328
|
+
|
|
2329
|
+
user_prompt = f"""分析对话记录帮我生成最终提示词,把最终提示词内容输出给我。
|
|
2330
|
+
### 对话记录
|
|
2331
|
+
{compiled_text}
|
|
2332
|
+
"""
|
|
2109
2333
|
|
|
2110
2334
|
try:
|
|
2111
|
-
assistant_message = chat_once(client, model, [{"role": "system", "content": sys_prompt}, {"role": "user", "content":
|
|
2335
|
+
assistant_message = chat_once(client, model, [{"role": "system", "content": sys_prompt}, {"role": "user", "content": user_prompt}], temperature=0.2, debug_enabled=debug_enabled, disable_tools=True)
|
|
2112
2336
|
except Exception:
|
|
2113
2337
|
logger.exception(t("task_end_generation_failed_log"))
|
|
2114
2338
|
console.print(Panel.fit(traceback.format_exc(), title=t("task_end_generate_failed")))
|
|
2115
2339
|
return
|
|
2116
|
-
|
|
2340
|
+
prompt_call_tools = get_content_from_tool_calls(assistant_message.tool_calls)
|
|
2341
|
+
# console.print(f"{prompt_call_tools}")
|
|
2117
2342
|
final_prompt = (assistant_message.content or "").strip()
|
|
2343
|
+
if not final_prompt:
|
|
2344
|
+
final_prompt = prompt_call_tools
|
|
2118
2345
|
if not final_prompt:
|
|
2119
2346
|
console.print(Panel.fit(t("task_end_no_prompt"), title=t("task_end_result")))
|
|
2120
2347
|
return
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "prompt-copilot-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
8
8
|
description = "A lightweight terminal-based coding agent with file tools, command execution, multimodal image support, and MCP integration."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -87,6 +87,34 @@ class FileToolTests(unittest.TestCase):
|
|
|
87
87
|
self.assertTrue(dest.exists())
|
|
88
88
|
self.assertEqual(dest.read_text(encoding="utf-8"), "hello")
|
|
89
89
|
|
|
90
|
+
def test_search_code_tool_finds_pattern_in_file(self) -> None:
|
|
91
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
92
|
+
root = Path(tmpdir)
|
|
93
|
+
target = root / "demo.py"
|
|
94
|
+
target.write_text("def greet():\n return 'hello'\n", encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
result = main.execute_tool_call(DummyToolCall("search_code", {"path": str(root), "pattern": "return 'hello'"}))
|
|
97
|
+
|
|
98
|
+
self.assertEqual(result["status"], "ok")
|
|
99
|
+
matches = json.loads(result["content"])
|
|
100
|
+
self.assertTrue(matches)
|
|
101
|
+
self.assertEqual(matches[0]["path"], str(target))
|
|
102
|
+
|
|
103
|
+
def test_edit_file_tool_replaces_text(self) -> None:
|
|
104
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
105
|
+
target = Path(tmpdir) / "demo.py"
|
|
106
|
+
target.write_text("print('before')\n", encoding="utf-8")
|
|
107
|
+
|
|
108
|
+
result = main.execute_tool_call(
|
|
109
|
+
DummyToolCall(
|
|
110
|
+
"edit_file",
|
|
111
|
+
{"path": str(target), "old_string": "before", "new_string": "after"},
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self.assertEqual(result["status"], "ok")
|
|
116
|
+
self.assertEqual(target.read_text(encoding="utf-8"), "print('after')\n")
|
|
117
|
+
|
|
90
118
|
|
|
91
119
|
if __name__ == "__main__":
|
|
92
120
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/prompt_copilot_cli.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/prompt_copilot_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/prompt_copilot_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{prompt_copilot_cli-0.2.6 → prompt_copilot_cli-0.2.8}/prompt_copilot_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|