prompt-copilot-cli 0.1.9__tar.gz → 0.2.2__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.1.9/prompt_copilot_cli.egg-info → prompt_copilot_cli-0.2.2}/PKG-INFO +1 -1
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/main.py +138 -44
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2/prompt_copilot_cli.egg-info}/PKG-INFO +1 -1
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/pyproject.toml +1 -1
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/tests/test_command_timeout.py +13 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/LICENSE +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/MANIFEST.in +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/README.md +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/README.zh-CN.md +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/prompt_copilot_cli.egg-info/SOURCES.txt +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/prompt_copilot_cli.egg-info/dependency_links.txt +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/prompt_copilot_cli.egg-info/entry_points.txt +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/prompt_copilot_cli.egg-info/requires.txt +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/prompt_copilot_cli.egg-info/top_level.txt +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/requirements.txt +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/setup.cfg +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/tests/test_cli_args.py +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/tests/test_file_tools.py +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/tests/test_image_tool.py +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/tests/test_mcp_transport.py +0 -0
- {prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/tests/test_multimodal_messages.py +0 -0
|
@@ -110,10 +110,55 @@ ZH_SYSTEM_PROMPT = """
|
|
|
110
110
|
10、对于图片类任务,优先使用图片工具而不是尝试直接读取二进制原始内容。
|
|
111
111
|
11、当用户输入一条任务指令时,如果是详细的任务清单,你就理解用户要求逐步完成工作,同时每个步骤执行完毕要反馈状态和进度。
|
|
112
112
|
12、在执行命令前,先判断该命令是短时命令还是持久命令:短时命令如 ls、pytest、python -m compileall 等应同步执行并等待结果;持久命令如 npm run dev、python app.py、uvicorn、flask run、serve、http.server 等会持续运行,应该优先以后台方式启动,并在必要时用健康检查确认服务已就绪。
|
|
113
|
+
13、无法调用官方联网检索工具而通过脚本抓取网络内容时需区分数据类型处理,抓取普通网页文本需剔除 HTML 标签、样式代码、广告碎片、无效注释、多余空行等冗余内容,仅留存有效正文且文本超出 8000 字符则截断,文本输出上限为 8000 字符,抓取程序源代码则无需过滤,完整保留原始代码、自带注释、缩进换行与原有格式,代码输出无字符数量限制。
|
|
113
114
|
|
|
114
115
|
"""
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
|
|
117
|
+
EN_SYSTEM_PROMPT = """
|
|
118
|
+
You are a CLI-based coding agent named Jason Li, focused on completing development tasks by using files, commands, Python scripts, and other tools.
|
|
119
|
+
|
|
120
|
+
Working principles:
|
|
121
|
+
1. First inspect the working directory and understand the requirement.
|
|
122
|
+
2. Prefer using network search tools for real-time information, such as weather, news, or other current topics.
|
|
123
|
+
3. All code file creation, editing, and deletion operations are performed in the working directory. If the user does not specify a directory, the project root is used by default.
|
|
124
|
+
4. If needed, provide concise explanations and next-step suggestions.
|
|
125
|
+
5. If the task involves deleting system files or executing dangerous commands, you must ask for confirmation first.
|
|
126
|
+
6. When the user enters /task-start, respond with: Please enter your first initial prompt.
|
|
127
|
+
7. When information is missing, do not speculate; prefer using search tools to fill the gap and keep the answer precise and evidence-based.
|
|
128
|
+
8. If the user wants to read or recognize other binary files, you must clearly state that you cannot directly read or understand generic binary file contents.
|
|
129
|
+
9. If the user wants to inspect image content, you should first use the image-reading tool to convert the image into base64 or a visual-message format and then send it to a multimodal-capable model.
|
|
130
|
+
10. For image-related tasks, prefer the image tool rather than trying to read the raw binary contents directly.
|
|
131
|
+
11. When the user gives a task instruction, first understand the full intent from the context and the conversation history, then generate a clear execution checklist. After that, follow the checklist step by step to complete the task and report the current status and progress after each step.
|
|
132
|
+
12. Before executing a command, first determine whether it is a short-lived command or a persistent one: short-lived commands such as ls, pytest, and python -m compileall should be executed synchronously and awaited; persistent commands such as npm run dev, python app.py, uvicorn, flask run, serve, and http.server will keep running, so they should be started in the background and, when needed, checked for readiness with a health check.
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
PLANNING_PROMPT = (
|
|
139
|
+
"你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,专注于使用文件、命令、Python 脚本等工具完成开发任务。\n\n"
|
|
140
|
+
"在生成任务执行计划时,请遵循以下工作原则:\n"
|
|
141
|
+
"1、先检查工作目录并理解需求。\n"
|
|
142
|
+
"2、优先使用网络搜索类工具搜索网络实时信息,例如:天气、新闻、资讯等等。\n"
|
|
143
|
+
"3、所有代码文件生成、编辑、删除等操作均在工作目录中执行。如果用户没有指定具体目录,默认工作目录为工程的根目录。\n"
|
|
144
|
+
"4、如果需要,输出简洁的说明与下一步建议。\n"
|
|
145
|
+
"5、若涉及删除系统文件或执行危险命令,必须先向用户确认后方可执行。\n"
|
|
146
|
+
"6、当用户输入指令:/task-start 的时候,直接回复:请输入你的第一条初始提示。\n"
|
|
147
|
+
"7、遇到信息盲区时,严禁主观臆测,请优先使用搜索工具补齐信息缺口,确保回答精准有据。\n"
|
|
148
|
+
"8、如果用户想“读取/识别其他二进制文件”,你必须明确告知:我无法直接读取或理解通用二进制文件内容。\n"
|
|
149
|
+
"9、如果用户想“查看图片内容”,应优先调用图片读取工具将图片转成 base64 或视觉消息格式,再将其发送给支持多模态的模型。\n"
|
|
150
|
+
"10、对于图片类任务,优先使用图片工具而不是尝试直接读取二进制原始内容。\n"
|
|
151
|
+
"11、基于上下文和历史对话,先梳理用户的完整意图,再生成一份清晰、可执行的步骤清单;随后按步骤逐步完成任务,并在每个步骤完成后反馈执行状态和进度。\n"
|
|
152
|
+
"12、在任何需要执行命令的步骤中,先判断该命令属于短时命令还是持久命令,并在任务清单中明确写出你的判断说明:短时命令如 ls、pytest、python -m compileall 等应同步执行并等待结果;持久命令如 npm run dev、python app.py、uvicorn、flask run、serve、http.server 等应优先以后台方式启动,并在必要时用健康检查确认服务已就绪。\n\n"
|
|
153
|
+
"13、无法调用官方联网检索工具而通过脚本抓取网络内容时需区分数据类型处理,抓取普通网页文本需剔除 HTML 标签、样式代码、广告碎片、无效注释、多余空行等冗余内容,仅留存有效正文且文本超出 8000 字符则截断,文本输出上限为 8000 字符,抓取程序源代码则无需过滤,完整保留原始代码、自带注释、缩进换行与原有格式,代码输出无字符数量限制。\n\n"
|
|
154
|
+
"请严格按照以下格式输出结果:\n\n"
|
|
155
|
+
"用户原始指令:......\n\n"
|
|
156
|
+
"结合上下文得到用户的完整意图:.....\n\n"
|
|
157
|
+
"接下来按照这个步骤逐步执行完成任务:\n\n"
|
|
158
|
+
"1、第一步:......\n"
|
|
159
|
+
"2、第二步:......\n"
|
|
160
|
+
"......"
|
|
161
|
+
)
|
|
117
162
|
|
|
118
163
|
|
|
119
164
|
TRANSLATIONS = {
|
|
@@ -228,7 +273,7 @@ TRANSLATIONS = {
|
|
|
228
273
|
"context_size_label": {"zh": "提交上下文大小", "en": "Submitted context size"},
|
|
229
274
|
"response_length_label": {"zh": "回复内容长度", "en": "Response content length"},
|
|
230
275
|
"recent_conversations_header": {"zh": "# 最近对话记录", "en": "# Recent conversations"},
|
|
231
|
-
"task_end_system_prompt": {"zh": "你是提示词优化专家(精通将对话记录转化为清晰、可执行的任务提示)。\n输入:首先是用户在输入 /task-start 后会给出第一条初始提示,随后是从该标记以来的对话记录(包括模型回复、工具调用与结果)。\n任务:阅读这些记录,理解用户起始提示与后续的澄清或更改,并结合这些信息生成一个改进后的、清晰且可直接用于执行的最终提示词。\n输出要求:只输出最终改进后的提示词文本,不要添加任何解释、元信息或注释。长度控制在
|
|
276
|
+
"task_end_system_prompt": {"zh": "你是提示词优化专家(精通将对话记录转化为清晰、可执行的任务提示)。\n输入:首先是用户在输入 /task-start 后会给出第一条初始提示,随后是从该标记以来的对话记录(包括模型回复、工具调用与结果)。\n任务:阅读这些记录,理解用户起始提示与后续的澄清或更改,并结合这些信息生成一个改进后的、清晰且可直接用于执行的最终提示词。\n输出要求:只输出最终改进后的提示词文本,不要添加任何解释、元信息或注释。长度控制在 18000 字符以内。", "en": "You are a prompt-optimization expert skilled at turning conversation history into a clear and actionable task prompt.\nInput: first the user provides an initial prompt after /task-start, then the conversation history from that point onward including model replies, tool calls, and results.\nTask: read these records, understand the initial prompt and any follow-up clarifications or changes, and generate an improved final prompt that is clear, executable, and ready to use.\nOutput requirements: return only the final improved prompt text, with no explanation, metadata, or comments. Keep it within 18000 characters."},
|
|
232
277
|
"task_end_user_message": {"zh": "对话记录(从 /task-start 开始,按时间从旧到新):\n\n{compiled_text}", "en": "Conversation history (starting from /task-start, from oldest to newest):\n\n{compiled_text}"},
|
|
233
278
|
"task_end_generation_failed_log": {"zh": "调用模型生成最终提示失败", "en": "Failed to generate the final prompt with the model"},
|
|
234
279
|
"final_prompt_header": {"zh": "# 最终提示词", "en": "# Final prompt"},
|
|
@@ -244,7 +289,7 @@ def t(key: str, **kwargs: Any) -> str:
|
|
|
244
289
|
|
|
245
290
|
|
|
246
291
|
UI_SYSTEM_LANGUAGE = "en"
|
|
247
|
-
APPLICATION_VERSION = "0.
|
|
292
|
+
APPLICATION_VERSION = "0.2.2"
|
|
248
293
|
DEFAULT_SYSTEM_PROMPT = t("system_prompt")
|
|
249
294
|
WORKSPACE_DIR = ROOT / "workspace"
|
|
250
295
|
LOG_DIR = ROOT / "logs"
|
|
@@ -252,8 +297,9 @@ LOG_FILE = LOG_DIR / "agent_runtime.log"
|
|
|
252
297
|
DEFAULT_MAX_CHAT_COUNT = 7
|
|
253
298
|
CHAT_MESSAGE_MAX_COUNT = 8
|
|
254
299
|
CONFIG_SAVE_FILE_PATH = ROOT / "config.json"
|
|
255
|
-
RE_ACTION_DELAY =
|
|
300
|
+
RE_ACTION_DELAY = 1 # unit: seconds
|
|
256
301
|
TOOL_SUBPROCESS_TIMEOUT = 6 * 60 # 1 hour in seconds
|
|
302
|
+
MODEL_REQUEST_TIMEOUT_SECONDS = 600 # generous timeout for slower model generations
|
|
257
303
|
TASK_DESCRIPTION_TARGET = "[This is the task list after understanding the user's needs]"
|
|
258
304
|
DEFAULT_MODEL_CONFIG: dict[str, Any] = {
|
|
259
305
|
"model": "",
|
|
@@ -733,6 +779,7 @@ def build_client(model_cfg: dict[str, Any]) -> OpenAI:
|
|
|
733
779
|
kwargs: dict[str, Any] = {"api_key": api_key}
|
|
734
780
|
if base_url:
|
|
735
781
|
kwargs["base_url"] = base_url
|
|
782
|
+
kwargs["timeout"] = MODEL_REQUEST_TIMEOUT_SECONDS
|
|
736
783
|
return OpenAI(**kwargs)
|
|
737
784
|
|
|
738
785
|
|
|
@@ -754,6 +801,23 @@ def normalize_mcp_tool_definition(tool_obj: Any) -> dict[str, Any]:
|
|
|
754
801
|
}
|
|
755
802
|
|
|
756
803
|
|
|
804
|
+
def get_tool_description(tool_call: Any) -> str:
|
|
805
|
+
description = getattr(getattr(tool_call, "function", None), "description", None)
|
|
806
|
+
if isinstance(description, str) and description.strip():
|
|
807
|
+
return description.strip()
|
|
808
|
+
|
|
809
|
+
tool_name = getattr(getattr(tool_call, "function", None), "name", None)
|
|
810
|
+
if not isinstance(tool_name, str) or not tool_name:
|
|
811
|
+
return ""
|
|
812
|
+
|
|
813
|
+
for definition in TOOL_DEFINITIONS + ACTIVE_MCP_TOOL_DEFINITIONS:
|
|
814
|
+
function = definition.get("function") or {}
|
|
815
|
+
if function.get("name") == tool_name:
|
|
816
|
+
return str(function.get("description", "")).strip()
|
|
817
|
+
|
|
818
|
+
return ""
|
|
819
|
+
|
|
820
|
+
|
|
757
821
|
async def _run_mcp_session(server_config: dict[str, Any], handler: Any) -> Any:
|
|
758
822
|
transport = str(server_config.get("transport") or server_config.get("type") or "stdio").lower()
|
|
759
823
|
if transport in {"http", "streamable_http", "streamable-http"}:
|
|
@@ -987,6 +1051,19 @@ def looks_like_background_service_command(command: Any) -> bool:
|
|
|
987
1051
|
return False
|
|
988
1052
|
|
|
989
1053
|
|
|
1054
|
+
def _read_text_file_tail(path: str | os.PathLike[str], max_chars: int = 4000) -> str:
|
|
1055
|
+
try:
|
|
1056
|
+
p = Path(path)
|
|
1057
|
+
if not p.exists():
|
|
1058
|
+
return ""
|
|
1059
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
1060
|
+
if len(text) > max_chars:
|
|
1061
|
+
return text[-max_chars:]
|
|
1062
|
+
return text
|
|
1063
|
+
except Exception:
|
|
1064
|
+
return ""
|
|
1065
|
+
|
|
1066
|
+
|
|
990
1067
|
def stream_background_process_output(process: subprocess.Popen[Any], log_path: str | os.PathLike[str]) -> None:
|
|
991
1068
|
log_file = Path(log_path)
|
|
992
1069
|
|
|
@@ -1030,14 +1107,14 @@ def start_background_process(command: Any, cwd: str, timeout_seconds: int | None
|
|
|
1030
1107
|
log_path = Path(output_log_path).expanduser() if output_log_path else ROOT / "logs" / "background" / f"cmd_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{os.getpid()}.log"
|
|
1031
1108
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1032
1109
|
|
|
1033
|
-
log_handle = log_path.open("a", encoding="utf-8")
|
|
1034
1110
|
startup_kwargs: dict[str, Any] = {
|
|
1035
1111
|
"cwd": safe_cwd,
|
|
1036
|
-
"stdout":
|
|
1112
|
+
"stdout": subprocess.PIPE,
|
|
1037
1113
|
"stderr": subprocess.STDOUT,
|
|
1038
1114
|
"text": True,
|
|
1039
1115
|
"encoding": "utf-8",
|
|
1040
1116
|
"errors": "replace",
|
|
1117
|
+
"bufsize": 1,
|
|
1041
1118
|
}
|
|
1042
1119
|
|
|
1043
1120
|
if os.name == "nt":
|
|
@@ -1050,17 +1127,22 @@ def start_background_process(command: Any, cwd: str, timeout_seconds: int | None
|
|
|
1050
1127
|
process = subprocess.Popen(str(command), shell=True, start_new_session=True, **startup_kwargs)
|
|
1051
1128
|
except (FileNotFoundError, NotADirectoryError, OSError) as exc:
|
|
1052
1129
|
logger.warning(t("tool_subprocess_failed"), cwd, safe_cwd, exc)
|
|
1130
|
+
return {"status": "error", "content": str(exc)}
|
|
1131
|
+
|
|
1132
|
+
def _drain_output() -> None:
|
|
1053
1133
|
try:
|
|
1054
|
-
|
|
1134
|
+
with log_path.open("a", encoding="utf-8") as handle:
|
|
1135
|
+
while True:
|
|
1136
|
+
chunk = process.stdout.readline() if process.stdout is not None else ""
|
|
1137
|
+
if chunk == "":
|
|
1138
|
+
break
|
|
1139
|
+
handle.write(chunk)
|
|
1140
|
+
handle.flush()
|
|
1055
1141
|
except Exception:
|
|
1056
1142
|
pass
|
|
1057
|
-
return {"status": "error", "content": str(exc)}
|
|
1058
1143
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
log_handle.close()
|
|
1062
|
-
except Exception:
|
|
1063
|
-
pass
|
|
1144
|
+
output_thread = threading.Thread(target=_drain_output, daemon=True)
|
|
1145
|
+
output_thread.start()
|
|
1064
1146
|
|
|
1065
1147
|
if timeout_seconds is not None and timeout_seconds > 0:
|
|
1066
1148
|
def _watch_timeout() -> None:
|
|
@@ -1075,6 +1157,7 @@ def start_background_process(command: Any, cwd: str, timeout_seconds: int | None
|
|
|
1075
1157
|
process.wait(timeout=5)
|
|
1076
1158
|
with log_path.open("a", encoding="utf-8") as handle:
|
|
1077
1159
|
handle.write(f"\n[timeout] process terminated after {timeout_seconds}s\n")
|
|
1160
|
+
handle.flush()
|
|
1078
1161
|
except Exception:
|
|
1079
1162
|
pass
|
|
1080
1163
|
|
|
@@ -1089,7 +1172,28 @@ def start_background_process(command: Any, cwd: str, timeout_seconds: int | None
|
|
|
1089
1172
|
"pid": process.pid,
|
|
1090
1173
|
"cwd": safe_cwd,
|
|
1091
1174
|
"log_path": str(log_path),
|
|
1175
|
+
"state": "running",
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def get_background_process_status(process_id: int, log_path: str | os.PathLike[str] | None = None, timeout_seconds: int | None = None) -> dict[str, Any]:
|
|
1180
|
+
try:
|
|
1181
|
+
process = subprocess.Popen(["ps", "-p", str(process_id), "-o", "pid="], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
1182
|
+
stdout, _ = process.communicate(timeout=5)
|
|
1183
|
+
running = bool(stdout and str(stdout).strip())
|
|
1184
|
+
except Exception:
|
|
1185
|
+
running = False
|
|
1186
|
+
|
|
1187
|
+
output_text = _read_text_file_tail(log_path) if log_path else ""
|
|
1188
|
+
payload = {
|
|
1189
|
+
"status": "running" if running else "completed",
|
|
1190
|
+
"pid": process_id,
|
|
1191
|
+
"log_path": str(log_path) if log_path else None,
|
|
1192
|
+
"output_tail": output_text,
|
|
1092
1193
|
}
|
|
1194
|
+
if timeout_seconds is not None and timeout_seconds > 0 and not running:
|
|
1195
|
+
payload["timeout_seconds"] = timeout_seconds
|
|
1196
|
+
return payload
|
|
1093
1197
|
|
|
1094
1198
|
|
|
1095
1199
|
def wait_for_health_check(url: str, timeout_seconds: int = 20) -> bool:
|
|
@@ -1383,6 +1487,7 @@ def execute_tool_call(tool_call: Any) -> dict[str, Any]:
|
|
|
1383
1487
|
except Exception:
|
|
1384
1488
|
result["health_check_ready"] = False
|
|
1385
1489
|
result["content"] = f"Started background process (pid={result['pid']})"
|
|
1490
|
+
result["output_tail"] = _read_text_file_tail(output_log_path, max_chars=4000)
|
|
1386
1491
|
logger.info("execute_command background result: %s", result)
|
|
1387
1492
|
return result
|
|
1388
1493
|
|
|
@@ -1461,7 +1566,14 @@ def show_stage(title: str, content: str) -> None:
|
|
|
1461
1566
|
|
|
1462
1567
|
def show_tool_result(tool_call: Any, result: dict[str, Any]) -> None:
|
|
1463
1568
|
display_result = sanitize_tool_result_for_display(result)
|
|
1464
|
-
|
|
1569
|
+
description = get_tool_description(tool_call)
|
|
1570
|
+
description_text = f"{description}\n\n" if description else ""
|
|
1571
|
+
console.print(
|
|
1572
|
+
Panel.fit(
|
|
1573
|
+
f"[bold cyan]{tool_call.function.name}[/bold cyan]\n{description_text}{json.dumps(display_result, ensure_ascii=False, indent=2)}",
|
|
1574
|
+
title=t("tool_result_title"),
|
|
1575
|
+
)
|
|
1576
|
+
)
|
|
1465
1577
|
|
|
1466
1578
|
|
|
1467
1579
|
def chat_once(client: OpenAI, model: str, messages: list[dict[str, Any]], temperature: float, debug_enabled: bool = False) -> Any:
|
|
@@ -1475,6 +1587,9 @@ def chat_once(client: OpenAI, model: str, messages: list[dict[str, Any]], temper
|
|
|
1475
1587
|
"tools": tool_definitions,
|
|
1476
1588
|
"tool_choice": "auto",
|
|
1477
1589
|
}
|
|
1590
|
+
request_timeout = getattr(client, "timeout", None)
|
|
1591
|
+
if request_timeout is None:
|
|
1592
|
+
request_timeout = MODEL_REQUEST_TIMEOUT_SECONDS
|
|
1478
1593
|
|
|
1479
1594
|
context_payload = json.dumps(request_payload, ensure_ascii=False)
|
|
1480
1595
|
context_size_chars = len(context_payload)
|
|
@@ -1484,7 +1599,7 @@ def chat_once(client: OpenAI, model: str, messages: list[dict[str, Any]], temper
|
|
|
1484
1599
|
show_stage(t("model_request_params"), json.dumps(request_payload, ensure_ascii=False, indent=2))
|
|
1485
1600
|
|
|
1486
1601
|
try:
|
|
1487
|
-
response = client.chat.completions.create(**request_payload)
|
|
1602
|
+
response = client.chat.completions.create(**request_payload, timeout=request_timeout)
|
|
1488
1603
|
assistant_message = response.choices[0].message
|
|
1489
1604
|
usage = getattr(response, "usage", None)
|
|
1490
1605
|
if usage is not None:
|
|
@@ -1610,6 +1725,7 @@ def to_tool_call_objects(tool_calls: list[Any]) -> list[Any]:
|
|
|
1610
1725
|
function=SimpleNamespace(
|
|
1611
1726
|
name=getattr(function, "name", ""),
|
|
1612
1727
|
arguments=getattr(function, "arguments", {}),
|
|
1728
|
+
description=getattr(function, "description", ""),
|
|
1613
1729
|
),
|
|
1614
1730
|
)
|
|
1615
1731
|
)
|
|
@@ -1660,31 +1776,8 @@ def plan_user_request(client: OpenAI, model: str, history: list[dict[str, Any]],
|
|
|
1660
1776
|
if is_special_command(user_text):
|
|
1661
1777
|
return user_text
|
|
1662
1778
|
|
|
1663
|
-
planning_prompt = (
|
|
1664
|
-
"你是一个基于 CLI 的编程 Agent,你的名字是 Jason Li,专注于使用文件、命令、Python 脚本等工具完成开发任务。\n\n"
|
|
1665
|
-
"在生成任务执行计划时,请遵循以下工作原则:\n"
|
|
1666
|
-
"1、先检查工作目录并理解需求。\n"
|
|
1667
|
-
"2、优先使用网络搜索类工具搜索网络实时信息,例如:天气、新闻、资讯等等。\n"
|
|
1668
|
-
"3、所有代码文件生成、编辑、删除等操作均在工作目录中执行。如果用户没有指定具体目录,默认工作目录为工程的根目录。\n"
|
|
1669
|
-
"4、如果需要,输出简洁的说明与下一步建议。\n"
|
|
1670
|
-
"5、若涉及删除系统文件或执行危险命令,必须先向用户确认后方可执行。\n"
|
|
1671
|
-
"6、当用户输入指令:/task-start 的时候,直接回复:请输入你的第一条初始提示。\n"
|
|
1672
|
-
"7、遇到信息盲区时,严禁主观臆测,请优先使用搜索工具补齐信息缺口,确保回答精准有据。\n"
|
|
1673
|
-
"8、如果用户想“读取/识别其他二进制文件”,你必须明确告知:我无法直接读取或理解通用二进制文件内容。\n"
|
|
1674
|
-
"9、如果用户想“查看图片内容”,应优先调用图片读取工具将图片转成 base64 或视觉消息格式,再将其发送给支持多模态的模型。\n"
|
|
1675
|
-
"10、对于图片类任务,优先使用图片工具而不是尝试直接读取二进制原始内容。\n"
|
|
1676
|
-
"11、基于上下文和历史对话,先梳理用户的完整意图,再生成一份清晰、可执行的步骤清单;随后按步骤逐步完成任务,并在每个步骤完成后反馈执行状态和进度。\n"
|
|
1677
|
-
"12、在任何需要执行命令的步骤中,先判断该命令属于短时命令还是持久命令,并在任务清单中明确写出你的判断说明:短时命令如 ls、pytest、python -m compileall 等应同步执行并等待结果;持久命令如 npm run dev、python app.py、uvicorn、flask run、serve、http.server 等应优先以后台方式启动,并在必要时用健康检查确认服务已就绪。\n\n"
|
|
1678
|
-
"请严格按照以下格式输出结果:\n\n"
|
|
1679
|
-
"用户原始指令:......\n\n"
|
|
1680
|
-
"结合上下文得到用户的完整意图:.....\n\n"
|
|
1681
|
-
"接下来按照这个步骤逐步执行完成任务:\n\n"
|
|
1682
|
-
"1、第一步:......\n"
|
|
1683
|
-
"2、第二步:......\n"
|
|
1684
|
-
"......"
|
|
1685
|
-
)
|
|
1686
1779
|
planning_messages = [
|
|
1687
|
-
{"role": "system", "content":
|
|
1780
|
+
{"role": "system", "content": PLANNING_PROMPT},
|
|
1688
1781
|
{"role": "user", "content": user_text},
|
|
1689
1782
|
]
|
|
1690
1783
|
if history:
|
|
@@ -1779,15 +1872,16 @@ def run_agent(client: OpenAI, model: str, system_prompt: str, session_store: Ses
|
|
|
1779
1872
|
|
|
1780
1873
|
for tc in tool_calls:
|
|
1781
1874
|
try:
|
|
1782
|
-
|
|
1875
|
+
tool_desc = get_tool_description(tc)
|
|
1876
|
+
start_calling_tips = f"tool={tc.function.name}\ndescription={tool_desc}\narguments={tc.function.arguments}"
|
|
1783
1877
|
show_stage(t("starting_tool_call"), start_calling_tips[:80])
|
|
1784
1878
|
# if recorder:
|
|
1785
1879
|
# recorder.record_tool_start(tc.function.name, tc.function.arguments)
|
|
1786
1880
|
result = execute_tool_call(tc)
|
|
1787
|
-
end_calling_tips = f"tool={tc.function.name}\nresult={json.dumps(result, ensure_ascii=False)}"
|
|
1788
|
-
show_stage(t("tool_call_finished"), end_calling_tips[:80])
|
|
1789
|
-
# show_tool_result(tc, result)
|
|
1790
1881
|
messages.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False)})
|
|
1882
|
+
show_tool_result(tc, result)
|
|
1883
|
+
# end_calling_tips = f"tool={tc.function.name}\nresult={json.dumps(result, ensure_ascii=False)}"
|
|
1884
|
+
# show_stage(t("tool_call_finished"), end_calling_tips[:80])
|
|
1791
1885
|
|
|
1792
1886
|
if tc.function.name == "read_image_as_base64":
|
|
1793
1887
|
try:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "prompt-copilot-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.2"
|
|
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"
|
|
@@ -19,6 +19,19 @@ class CommandTimeoutTests(unittest.TestCase):
|
|
|
19
19
|
self.assertEqual(stdout, "")
|
|
20
20
|
self.assertIn("timed out", stderr.lower())
|
|
21
21
|
|
|
22
|
+
def test_start_background_process_returns_running_state(self) -> None:
|
|
23
|
+
result = main.start_background_process(
|
|
24
|
+
[sys.executable, "-c", "import time; time.sleep(0.2)"],
|
|
25
|
+
cwd=".",
|
|
26
|
+
timeout_seconds=3,
|
|
27
|
+
output_log_path=str(Path("logs") / "test_background.log"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.assertEqual(result.get("status"), "ok")
|
|
31
|
+
self.assertEqual(result.get("state"), "running")
|
|
32
|
+
self.assertIsNotNone(result.get("pid"))
|
|
33
|
+
self.assertIn("log_path", result)
|
|
34
|
+
|
|
22
35
|
|
|
23
36
|
if __name__ == "__main__":
|
|
24
37
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/prompt_copilot_cli.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/prompt_copilot_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/prompt_copilot_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{prompt_copilot_cli-0.1.9 → prompt_copilot_cli-0.2.2}/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
|