auto-coder 0.1.298__py3-none-any.whl → 0.1.300__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.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- {auto_coder-0.1.298.dist-info → auto_coder-0.1.300.dist-info}/METADATA +2 -2
- {auto_coder-0.1.298.dist-info → auto_coder-0.1.300.dist-info}/RECORD +23 -21
- autocoder/agent/auto_learn_from_commit.py +125 -59
- autocoder/agent/auto_review_commit.py +106 -16
- autocoder/auto_coder.py +65 -66
- autocoder/auto_coder_runner.py +23 -40
- autocoder/command_parser.py +280 -0
- autocoder/commands/auto_command.py +112 -33
- autocoder/commands/tools.py +170 -10
- autocoder/common/__init__.py +5 -1
- autocoder/common/action_yml_file_manager.py +367 -0
- autocoder/common/auto_coder_lang.py +8 -2
- autocoder/common/auto_configure.py +6 -0
- autocoder/common/command_completer.py +8 -1
- autocoder/common/memory_manager.py +5 -1
- autocoder/index/entry.py +17 -0
- autocoder/rag/cache/local_duckdb_storage_cache.py +111 -17
- autocoder/utils/__init__.py +13 -9
- autocoder/version.py +1 -1
- {auto_coder-0.1.298.dist-info → auto_coder-0.1.300.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.298.dist-info → auto_coder-0.1.300.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.298.dist-info → auto_coder-0.1.300.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.298.dist-info → auto_coder-0.1.300.dist-info}/top_level.txt +0 -0
autocoder/auto_coder.py
CHANGED
|
@@ -5,6 +5,7 @@ from autocoder.dispacher import Dispacher
|
|
|
5
5
|
from autocoder.common import git_utils, code_auto_execute
|
|
6
6
|
from autocoder.utils.llm_client_interceptors import token_counter_interceptor
|
|
7
7
|
from autocoder.db.store import Store
|
|
8
|
+
from autocoder.common.action_yml_file_manager import ActionYmlFileManager
|
|
8
9
|
|
|
9
10
|
from autocoder.utils.llms import get_llm_names
|
|
10
11
|
from autocoder.utils.queue_communicate import (
|
|
@@ -196,59 +197,25 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
196
197
|
return
|
|
197
198
|
|
|
198
199
|
if raw_args.command == "next":
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
action_files = [
|
|
205
|
-
f for f in os.listdir(actions_dir) if f[:3].isdigit() and "_" in f and f.endswith(".yml")
|
|
206
|
-
]
|
|
207
|
-
|
|
208
|
-
def get_old_seq(name):
|
|
209
|
-
return name.split("_")[0]
|
|
210
|
-
|
|
211
|
-
if not action_files:
|
|
212
|
-
max_seq = 0
|
|
213
|
-
else:
|
|
214
|
-
seqs = [int(get_old_seq(f)) for f in action_files]
|
|
215
|
-
max_seq = max(seqs)
|
|
216
|
-
|
|
217
|
-
new_seq = str(max_seq + 1).zfill(12)
|
|
218
|
-
prev_files = [f for f in action_files if int(
|
|
219
|
-
get_old_seq(f)) < int(new_seq)]
|
|
220
|
-
|
|
200
|
+
# 使用 ActionYmlFileManager 创建下一个 action 文件
|
|
201
|
+
action_manager = ActionYmlFileManager(args.source_dir)
|
|
202
|
+
|
|
221
203
|
if raw_args.from_yaml:
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
new_file = os.path.join(
|
|
230
|
-
actions_dir, f"{new_seq}_{raw_args.name}.yml")
|
|
231
|
-
with open(new_file, "w",encoding="utf-8") as f:
|
|
232
|
-
f.write(content)
|
|
233
|
-
else:
|
|
234
|
-
print(
|
|
235
|
-
f"No YAML file found matching prefix: {raw_args.from_yaml}")
|
|
204
|
+
# 基于指定的 yaml 文件创建新文件
|
|
205
|
+
new_file = action_manager.create_next_action_file(
|
|
206
|
+
name=raw_args.name,
|
|
207
|
+
from_yaml=raw_args.from_yaml
|
|
208
|
+
)
|
|
209
|
+
if not new_file:
|
|
210
|
+
print(f"No YAML file found matching prefix: {raw_args.from_yaml}")
|
|
236
211
|
return
|
|
237
212
|
else:
|
|
238
|
-
#
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
else:
|
|
245
|
-
prev_file = sorted(prev_files)[-1] # 取序号最大的文件
|
|
246
|
-
with open(os.path.join(actions_dir, prev_file), "r",encoding="utf-8") as f:
|
|
247
|
-
content = f.read()
|
|
248
|
-
new_file = os.path.join(
|
|
249
|
-
actions_dir, f"{new_seq}_{raw_args.name}.yml")
|
|
250
|
-
with open(new_file, "w",encoding="utf-8") as f:
|
|
251
|
-
f.write(content)
|
|
213
|
+
# 创建新的 action 文件
|
|
214
|
+
new_file = action_manager.create_next_action_file(name=raw_args.name)
|
|
215
|
+
if not new_file:
|
|
216
|
+
print("Failed to create new action file")
|
|
217
|
+
return
|
|
218
|
+
|
|
252
219
|
# open_yaml_file_in_editor(new_file)
|
|
253
220
|
return
|
|
254
221
|
|
|
@@ -883,7 +850,6 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
883
850
|
# os.path.join(args.source_dir, "actions")
|
|
884
851
|
# )
|
|
885
852
|
# )
|
|
886
|
-
# )
|
|
887
853
|
return
|
|
888
854
|
elif raw_args.agent_command == "project_reader":
|
|
889
855
|
|
|
@@ -1062,6 +1028,15 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
1062
1028
|
|
|
1063
1029
|
elif raw_args.agent_command == "chat":
|
|
1064
1030
|
|
|
1031
|
+
# 统一格式
|
|
1032
|
+
# {"command1": {"args": ["arg1", "arg2"], "kwargs": {"key1": "value1", "key2": "value2"}}}
|
|
1033
|
+
if isinstance(args.action, dict):
|
|
1034
|
+
commands_info = args.action
|
|
1035
|
+
else:
|
|
1036
|
+
commands_info = {}
|
|
1037
|
+
for command in args.action:
|
|
1038
|
+
commands_info[command] = {}
|
|
1039
|
+
|
|
1065
1040
|
memory_dir = os.path.join(args.source_dir, ".auto-coder", "memory")
|
|
1066
1041
|
os.makedirs(memory_dir, exist_ok=True)
|
|
1067
1042
|
memory_file = os.path.join(memory_dir, "chat_history.json")
|
|
@@ -1167,7 +1142,7 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
1167
1142
|
file_path=source.module_name,
|
|
1168
1143
|
model_name=",".join(get_llm_names(chat_llm)))
|
|
1169
1144
|
|
|
1170
|
-
if "no_context" not in
|
|
1145
|
+
if "no_context" not in commands_info:
|
|
1171
1146
|
s = build_index_and_filter_files(
|
|
1172
1147
|
llm=llm, args=args, sources=filtered_sources).to_str()
|
|
1173
1148
|
|
|
@@ -1304,7 +1279,7 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
1304
1279
|
),
|
|
1305
1280
|
)
|
|
1306
1281
|
|
|
1307
|
-
if "save" in
|
|
1282
|
+
if "save" in commands_info:
|
|
1308
1283
|
save_to_memory_file(ask_conversation=chat_history["ask_conversation"],
|
|
1309
1284
|
query=args.query,
|
|
1310
1285
|
response=result)
|
|
@@ -1314,8 +1289,9 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
1314
1289
|
|
|
1315
1290
|
# 计算耗时
|
|
1316
1291
|
start_time = time.time()
|
|
1292
|
+
commit_file_name = None
|
|
1317
1293
|
|
|
1318
|
-
if "rag" in
|
|
1294
|
+
if "rag" in commands_info:
|
|
1319
1295
|
from autocoder.rag.rag_entry import RAGFactory
|
|
1320
1296
|
args.enable_rag_search = True
|
|
1321
1297
|
args.enable_rag_context = False
|
|
@@ -1324,25 +1300,35 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
1324
1300
|
conversations=loaded_conversations)[0]
|
|
1325
1301
|
v = (item for item in response)
|
|
1326
1302
|
|
|
1327
|
-
elif "mcp" in
|
|
1303
|
+
elif "mcp" in commands_info:
|
|
1328
1304
|
from autocoder.common.mcp_server import get_mcp_server, McpRequest, McpInstallRequest, McpRemoveRequest, McpListRequest, McpListRunningRequest, McpRefreshRequest
|
|
1329
1305
|
mcp_server = get_mcp_server()
|
|
1306
|
+
|
|
1307
|
+
pos_args = commands_info["mcp"].get("args", [])
|
|
1308
|
+
final_query = pos_args[0] if pos_args else args.query
|
|
1330
1309
|
response = mcp_server.send_request(
|
|
1331
1310
|
McpRequest(
|
|
1332
|
-
query=
|
|
1311
|
+
query=final_query,
|
|
1333
1312
|
model=args.inference_model or args.model,
|
|
1334
1313
|
product_mode=args.product_mode
|
|
1335
1314
|
)
|
|
1336
1315
|
)
|
|
1337
1316
|
v = [[response.result,None]]
|
|
1338
|
-
elif "
|
|
1339
|
-
from autocoder.agent.auto_review_commit import AutoReviewCommit
|
|
1317
|
+
elif "review" in commands_info:
|
|
1318
|
+
from autocoder.agent.auto_review_commit import AutoReviewCommit
|
|
1340
1319
|
reviewer = AutoReviewCommit(llm=chat_llm, args=args)
|
|
1341
|
-
|
|
1342
|
-
|
|
1320
|
+
pos_args = commands_info["review"].get("args", [])
|
|
1321
|
+
final_query = pos_args[0] if pos_args else args.query
|
|
1322
|
+
kwargs = commands_info["review"].get("kwargs", {})
|
|
1323
|
+
commit_id = kwargs.get("commit", None)
|
|
1324
|
+
v = reviewer.review_commit(query=final_query, conversations=loaded_conversations, commit_id=commit_id)
|
|
1325
|
+
elif "learn" in commands_info:
|
|
1343
1326
|
from autocoder.agent.auto_learn_from_commit import AutoLearnFromCommit
|
|
1344
1327
|
learner = AutoLearnFromCommit(llm=chat_llm, args=args)
|
|
1345
|
-
|
|
1328
|
+
pos_args = commands_info["learn"].get("args", [])
|
|
1329
|
+
final_query = pos_args[0] if pos_args else args.query
|
|
1330
|
+
v,tmp_file_name = learner.learn_from_commit(query=final_query,conversations=loaded_conversations)
|
|
1331
|
+
commit_file_name = tmp_file_name
|
|
1346
1332
|
else:
|
|
1347
1333
|
# 预估token数量
|
|
1348
1334
|
dumped_conversations = json.dumps(loaded_conversations, ensure_ascii=False)
|
|
@@ -1375,7 +1361,15 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
1375
1361
|
"input": {
|
|
1376
1362
|
"query": args.query
|
|
1377
1363
|
}
|
|
1378
|
-
})
|
|
1364
|
+
})
|
|
1365
|
+
|
|
1366
|
+
if "learn" in commands_info:
|
|
1367
|
+
if commit_file_name:
|
|
1368
|
+
# 使用 ActionYmlFileManager 更新 YAML 文件
|
|
1369
|
+
action_manager = ActionYmlFileManager(args.source_dir)
|
|
1370
|
+
if not action_manager.update_yaml_field(commit_file_name, 'how_to_reproduce', assistant_response):
|
|
1371
|
+
printer = Printer()
|
|
1372
|
+
printer.print_in_terminal("yaml_save_error", style="red", yaml_file=commit_file_name)
|
|
1379
1373
|
|
|
1380
1374
|
# 打印耗时和token统计
|
|
1381
1375
|
if last_meta:
|
|
@@ -1410,7 +1404,7 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
1410
1404
|
with open(memory_file, "w",encoding="utf-8") as f:
|
|
1411
1405
|
json.dump(chat_history, f, ensure_ascii=False)
|
|
1412
1406
|
|
|
1413
|
-
if "copy" in
|
|
1407
|
+
if "copy" in commands_info:
|
|
1414
1408
|
#copy assistant_response to clipboard
|
|
1415
1409
|
import pyperclip
|
|
1416
1410
|
try:
|
|
@@ -1418,12 +1412,17 @@ def main(input_args: Optional[List[str]] = None):
|
|
|
1418
1412
|
except:
|
|
1419
1413
|
print("pyperclip not installed or clipboard is not supported, instruction will not be copied to clipboard.")
|
|
1420
1414
|
|
|
1421
|
-
if "save" in
|
|
1422
|
-
save_to_memory_file(ask_conversation=chat_history["ask_conversation"],
|
|
1415
|
+
if "save" in commands_info:
|
|
1416
|
+
tmp_dir = save_to_memory_file(ask_conversation=chat_history["ask_conversation"],
|
|
1423
1417
|
query=args.query,
|
|
1424
1418
|
response=assistant_response)
|
|
1425
1419
|
printer = Printer()
|
|
1426
|
-
printer.print_in_terminal("memory_save_success")
|
|
1420
|
+
printer.print_in_terminal("memory_save_success", style="green", path=tmp_dir)
|
|
1421
|
+
|
|
1422
|
+
if len(commands_info["save"]["args"]) > 0:
|
|
1423
|
+
# 保存到指定文件
|
|
1424
|
+
with open(commands_info["save"]["args"][0], "w",encoding="utf-8") as f:
|
|
1425
|
+
f.write(assistant_response)
|
|
1427
1426
|
return
|
|
1428
1427
|
|
|
1429
1428
|
else:
|
autocoder/auto_coder_runner.py
CHANGED
|
@@ -51,6 +51,7 @@ from autocoder.common.printer import Printer
|
|
|
51
51
|
from autocoder.utils.thread_utils import run_in_raw_thread
|
|
52
52
|
from autocoder.common.command_completer import CommandCompleter,FileSystemModel as CCFileSystemModel,MemoryConfig as CCMemoryModel
|
|
53
53
|
from autocoder.common.conf_validator import ConfigValidator
|
|
54
|
+
from autocoder import command_parser as CommandParser
|
|
54
55
|
|
|
55
56
|
class SymbolItem(BaseModel):
|
|
56
57
|
symbol_name: str
|
|
@@ -1598,48 +1599,30 @@ def chat(query: str):
|
|
|
1598
1599
|
if "emb_model" in conf:
|
|
1599
1600
|
yaml_config["emb_model"] = conf["emb_model"]
|
|
1600
1601
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
yaml_config["action"].append("copy")
|
|
1617
|
-
query = query.replace("/copy", "", 1).strip()
|
|
1618
|
-
|
|
1619
|
-
if "/save" in query:
|
|
1620
|
-
yaml_config["action"].append("save")
|
|
1621
|
-
query = query.replace("/save", "", 1).strip()
|
|
1622
|
-
|
|
1623
|
-
if "/review" in query and "/commit" in query:
|
|
1624
|
-
yaml_config["action"].append("review_commit")
|
|
1625
|
-
query = query.replace("/review", "", 1).replace("/commit", "", 1).strip()
|
|
1626
|
-
elif "/learn" in query:
|
|
1627
|
-
yaml_config["action"].append("learn_from_commit")
|
|
1628
|
-
query = query.replace("/learn", "", 1).strip()
|
|
1629
|
-
else:
|
|
1630
|
-
is_review = query.strip().startswith("/review")
|
|
1631
|
-
if is_review:
|
|
1632
|
-
query = query.replace("/review", "", 1).strip()
|
|
1633
|
-
if "prompt_review" in conf:
|
|
1634
|
-
query = format_str_jinja2(conf["prompt_review"], query=query)
|
|
1635
|
-
else:
|
|
1636
|
-
query = code_review.prompt(query)
|
|
1602
|
+
# 解析命令
|
|
1603
|
+
commands_infos = CommandParser.parse_query(query)
|
|
1604
|
+
if len(commands_infos) > 0:
|
|
1605
|
+
if "query" in commands_infos:
|
|
1606
|
+
query = commands_infos["query"]["args"][-1]
|
|
1607
|
+
else:
|
|
1608
|
+
# 获取第一个command 的最后一个位置参数作为默认query
|
|
1609
|
+
temp_query = ""
|
|
1610
|
+
for (command,command_info) in commands_infos.items():
|
|
1611
|
+
if command_info["args"]:
|
|
1612
|
+
temp_query = command_info["args"][-1]
|
|
1613
|
+
break
|
|
1614
|
+
query = temp_query
|
|
1615
|
+
|
|
1616
|
+
is_new = "new" in commands_infos
|
|
1637
1617
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
query = query.replace("/no_context", "", 1).strip()
|
|
1641
|
-
yaml_config["action"].append("no_context")
|
|
1618
|
+
if "learn" in commands_infos:
|
|
1619
|
+
commands_infos["no_context"] = {}
|
|
1642
1620
|
|
|
1621
|
+
if "review" in commands_infos:
|
|
1622
|
+
commands_infos["no_context"] = {}
|
|
1623
|
+
|
|
1624
|
+
yaml_config["action"] = commands_infos
|
|
1625
|
+
|
|
1643
1626
|
for key, value in conf.items():
|
|
1644
1627
|
converted_value = convert_config_value(key, value)
|
|
1645
1628
|
if converted_value is not None:
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from typing import Dict, List, Tuple, Any, Optional
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CommandParser:
|
|
6
|
+
"""
|
|
7
|
+
命令解析器,用于解析命令行格式的查询字符串。
|
|
8
|
+
支持以下格式:
|
|
9
|
+
1. /command arg1 arg2
|
|
10
|
+
2. /command key1=value1 key2=value2
|
|
11
|
+
3. /command arg1 key1=value1
|
|
12
|
+
4. /command1 arg1 /command2 arg2
|
|
13
|
+
5. /command1 /command2 arg2
|
|
14
|
+
6. /command1 /command2 key=value
|
|
15
|
+
7. /command key="value with spaces"
|
|
16
|
+
8. /command key='value with spaces'
|
|
17
|
+
|
|
18
|
+
注意:路径参数(如/path/to/file)不会被识别为命令。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
# 匹配命令的正则表达式 - 必须是以/开头,后跟单词字符,且不能后跟/或.
|
|
23
|
+
# (?<!\S) 确保命令前是字符串开头或空白字符
|
|
24
|
+
self.command_pattern = r'(?<!\S)/(\w+)(?!/|\.)'
|
|
25
|
+
# 匹配键值对参数的正则表达式,支持带引号的值
|
|
26
|
+
self.key_value_pattern = r'(\w+)=(?:"([^"]*?)"|\'([^\']*?)\'|([^\s"\']+))(?:\s|$)'
|
|
27
|
+
# 匹配路径模式的正则表达式
|
|
28
|
+
self.path_pattern = r'/\w+(?:/[^/\s]+)+'
|
|
29
|
+
|
|
30
|
+
def parse(self, query: str) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
解析命令行格式的查询字符串,返回命令和参数的字典。
|
|
33
|
+
|
|
34
|
+
参数:
|
|
35
|
+
query: 命令行格式的查询字符串
|
|
36
|
+
|
|
37
|
+
返回:
|
|
38
|
+
Dict[str, Any]: 命令和参数的字典,格式为:
|
|
39
|
+
{
|
|
40
|
+
'command1': {
|
|
41
|
+
'args': ['arg1', 'arg2'],
|
|
42
|
+
'kwargs': {'key1': 'value1', 'key2': 'value with spaces'}
|
|
43
|
+
},
|
|
44
|
+
'command2': {
|
|
45
|
+
'args': [],
|
|
46
|
+
'kwargs': {'key': 'value'}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
"""
|
|
50
|
+
if not query or not query.strip():
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
# 预处理:标记路径参数,避免被识别为命令
|
|
54
|
+
processed_query = query
|
|
55
|
+
path_matches = re.finditer(self.path_pattern, query)
|
|
56
|
+
placeholders = {}
|
|
57
|
+
|
|
58
|
+
for i, match in enumerate(path_matches):
|
|
59
|
+
path = match.group(0)
|
|
60
|
+
placeholder = f"__PATH_PLACEHOLDER_{i}__"
|
|
61
|
+
placeholders[placeholder] = path
|
|
62
|
+
processed_query = processed_query.replace(path, placeholder, 1)
|
|
63
|
+
|
|
64
|
+
# 找出所有命令
|
|
65
|
+
commands = re.findall(self.command_pattern, processed_query)
|
|
66
|
+
if not commands:
|
|
67
|
+
return {}
|
|
68
|
+
|
|
69
|
+
# 将查询字符串按命令分割
|
|
70
|
+
parts = re.split(self.command_pattern, processed_query)
|
|
71
|
+
# 第一个元素是空字符串或之前的非命令内容,保留它
|
|
72
|
+
first_part = parts[0]
|
|
73
|
+
parts = parts[1:]
|
|
74
|
+
|
|
75
|
+
result = {}
|
|
76
|
+
|
|
77
|
+
# 处理每个命令和它的参数
|
|
78
|
+
for i in range(0, len(parts), 2):
|
|
79
|
+
command = parts[i]
|
|
80
|
+
|
|
81
|
+
# 获取此命令的参数部分
|
|
82
|
+
params_str = parts[i+1].strip() if i+1 < len(parts) else ""
|
|
83
|
+
|
|
84
|
+
# 恢复路径参数的原始值
|
|
85
|
+
for placeholder, path in placeholders.items():
|
|
86
|
+
params_str = params_str.replace(placeholder, path)
|
|
87
|
+
|
|
88
|
+
# 解析参数
|
|
89
|
+
args, kwargs = self._parse_params(params_str)
|
|
90
|
+
|
|
91
|
+
result[command] = {
|
|
92
|
+
'args': args,
|
|
93
|
+
'kwargs': kwargs
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
def _parse_params(self, params_str: str) -> Tuple[List[str], Dict[str, str]]:
|
|
99
|
+
"""
|
|
100
|
+
解析参数字符串,区分位置参数和键值对参数。
|
|
101
|
+
支持带引号(双引号或单引号)的值,引号内可以包含空格。
|
|
102
|
+
|
|
103
|
+
参数:
|
|
104
|
+
params_str: 参数字符串
|
|
105
|
+
|
|
106
|
+
返回:
|
|
107
|
+
Tuple[List[str], Dict[str, str]]: 位置参数列表和键值对参数字典
|
|
108
|
+
"""
|
|
109
|
+
args = []
|
|
110
|
+
kwargs = {}
|
|
111
|
+
|
|
112
|
+
if not params_str:
|
|
113
|
+
return args, kwargs
|
|
114
|
+
|
|
115
|
+
# 找出所有键值对
|
|
116
|
+
key_value_pairs = re.findall(self.key_value_pattern, params_str)
|
|
117
|
+
|
|
118
|
+
# 如果有键值对,处理它们
|
|
119
|
+
if key_value_pairs:
|
|
120
|
+
for match in key_value_pairs:
|
|
121
|
+
key = match[0]
|
|
122
|
+
# 值可能在三个捕获组中的一个,取非空的那个
|
|
123
|
+
value = match[1] or match[2] or match[3]
|
|
124
|
+
kwargs[key] = value.strip()
|
|
125
|
+
|
|
126
|
+
# 替换带引号的键值对
|
|
127
|
+
processed_params_str = params_str
|
|
128
|
+
for match in re.finditer(self.key_value_pattern, params_str):
|
|
129
|
+
full_match = match.group(0)
|
|
130
|
+
processed_params_str = processed_params_str.replace(full_match, "", 1).strip()
|
|
131
|
+
|
|
132
|
+
# 现在 processed_params_str 中应该只剩下位置参数
|
|
133
|
+
|
|
134
|
+
# 处理带引号的位置参数
|
|
135
|
+
quote_pattern = r'(?:"([^"]*?)"|\'([^\']*?)\')'
|
|
136
|
+
quoted_args = re.findall(quote_pattern, processed_params_str)
|
|
137
|
+
for quoted_arg in quoted_args:
|
|
138
|
+
# 取非空的那个捕获组
|
|
139
|
+
arg = quoted_arg[0] or quoted_arg[1]
|
|
140
|
+
args.append(arg)
|
|
141
|
+
# 从参数字符串中移除这个带引号的参数
|
|
142
|
+
quoted_pattern = f'"{arg}"' if quoted_arg[0] else f"'{arg}'"
|
|
143
|
+
processed_params_str = processed_params_str.replace(quoted_pattern, "", 1).strip()
|
|
144
|
+
|
|
145
|
+
# 分割剩余的位置参数(不带引号的)
|
|
146
|
+
remaining_args = [arg.strip() for arg in processed_params_str.split() if arg.strip()]
|
|
147
|
+
args.extend(remaining_args)
|
|
148
|
+
else:
|
|
149
|
+
# 如果没有键值对,处理所有参数作为位置参数
|
|
150
|
+
|
|
151
|
+
# 处理带引号的位置参数
|
|
152
|
+
quote_pattern = r'(?:"([^"]*?)"|\'([^\']*?)\')'
|
|
153
|
+
quoted_args = re.findall(quote_pattern, params_str)
|
|
154
|
+
processed_params_str = params_str
|
|
155
|
+
|
|
156
|
+
for quoted_arg in quoted_args:
|
|
157
|
+
# 取非空的那个捕获组
|
|
158
|
+
arg = quoted_arg[0] or quoted_arg[1]
|
|
159
|
+
args.append(arg)
|
|
160
|
+
# 从参数字符串中移除这个带引号的参数
|
|
161
|
+
quoted_pattern = f'"{arg}"' if quoted_arg[0] else f"'{arg}'"
|
|
162
|
+
processed_params_str = processed_params_str.replace(quoted_pattern, "", 1).strip()
|
|
163
|
+
|
|
164
|
+
# 分割剩余的位置参数(不带引号的)
|
|
165
|
+
remaining_args = [arg.strip() for arg in processed_params_str.split() if arg.strip()]
|
|
166
|
+
args.extend(remaining_args)
|
|
167
|
+
|
|
168
|
+
return args, kwargs
|
|
169
|
+
|
|
170
|
+
def parse_command(self, query: str, command: str) -> Optional[Dict[str, Any]]:
|
|
171
|
+
"""
|
|
172
|
+
解析特定命令的参数。
|
|
173
|
+
|
|
174
|
+
参数:
|
|
175
|
+
query: 命令行格式的查询字符串
|
|
176
|
+
command: 要解析的命令名
|
|
177
|
+
|
|
178
|
+
返回:
|
|
179
|
+
Optional[Dict[str, Any]]: 如果找到命令,返回其参数;否则返回None
|
|
180
|
+
"""
|
|
181
|
+
commands = self.parse(query)
|
|
182
|
+
return commands.get(command)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def parse_query(query: str) -> Dict[str, Any]:
|
|
186
|
+
"""
|
|
187
|
+
解析命令行格式的查询字符串的便捷函数。
|
|
188
|
+
|
|
189
|
+
参数:
|
|
190
|
+
query: 命令行格式的查询字符串
|
|
191
|
+
|
|
192
|
+
返回:
|
|
193
|
+
Dict[str, Any]: 命令和参数的字典
|
|
194
|
+
"""
|
|
195
|
+
parser = CommandParser()
|
|
196
|
+
return parser.parse(query)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def has_command(query: str, command: str) -> bool:
|
|
200
|
+
"""
|
|
201
|
+
检查查询字符串中是否包含特定命令。
|
|
202
|
+
|
|
203
|
+
参数:
|
|
204
|
+
query: 命令行格式的查询字符串
|
|
205
|
+
command: 要检查的命令名
|
|
206
|
+
|
|
207
|
+
返回:
|
|
208
|
+
bool: 如果包含命令返回True,否则返回False
|
|
209
|
+
"""
|
|
210
|
+
parser = CommandParser()
|
|
211
|
+
commands = parser.parse(query)
|
|
212
|
+
return command in commands
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_command_args(query: str, command: str) -> List[str]:
|
|
216
|
+
"""
|
|
217
|
+
获取特定命令的位置参数。
|
|
218
|
+
|
|
219
|
+
参数:
|
|
220
|
+
query: 命令行格式的查询字符串
|
|
221
|
+
command: 要获取参数的命令名
|
|
222
|
+
|
|
223
|
+
返回:
|
|
224
|
+
List[str]: 命令的位置参数列表,如果命令不存在返回空列表
|
|
225
|
+
"""
|
|
226
|
+
parser = CommandParser()
|
|
227
|
+
command_info = parser.parse_command(query, command)
|
|
228
|
+
if command_info:
|
|
229
|
+
return command_info['args']
|
|
230
|
+
return []
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_command_kwargs(query: str, command: str) -> Dict[str, str]:
|
|
234
|
+
"""
|
|
235
|
+
获取特定命令的键值对参数。
|
|
236
|
+
|
|
237
|
+
参数:
|
|
238
|
+
query: 命令行格式的查询字符串
|
|
239
|
+
command: 要获取参数的命令名
|
|
240
|
+
|
|
241
|
+
返回:
|
|
242
|
+
Dict[str, str]: 命令的键值对参数字典,如果命令不存在返回空字典
|
|
243
|
+
"""
|
|
244
|
+
parser = CommandParser()
|
|
245
|
+
command_info = parser.parse_command(query, command)
|
|
246
|
+
if command_info:
|
|
247
|
+
return command_info['kwargs']
|
|
248
|
+
return {}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# 示例用法
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
# 测试各种格式的查询
|
|
254
|
+
test_queries = [
|
|
255
|
+
"/learn hello world /commit 123456",
|
|
256
|
+
"/learn /commit 123456",
|
|
257
|
+
"/learn /commit commit_id=123456",
|
|
258
|
+
"/learn msg=hello /commit commit_id=123456",
|
|
259
|
+
"/learn hello key=value /commit",
|
|
260
|
+
# 带引号的值
|
|
261
|
+
'/learn msg="hello world" /commit message="Fix bug #123"',
|
|
262
|
+
"/learn 'quoted arg' key='value with spaces' /commit",
|
|
263
|
+
# 路径参数测试
|
|
264
|
+
"/learn /path/to/file.txt",
|
|
265
|
+
"/commit message='Added /path/to/file.txt'",
|
|
266
|
+
"Check /path/to/file.txt and also /another/path/file.md",
|
|
267
|
+
"/clone /path/to/repo /checkout branch",
|
|
268
|
+
"Use the file at /usr/local/bin/python with /learn"
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
for query in test_queries:
|
|
272
|
+
print(f"\nQuery: {query}")
|
|
273
|
+
result = parse_query(query)
|
|
274
|
+
print(f"Parsed: {result}")
|
|
275
|
+
|
|
276
|
+
if has_command(query, "commit"):
|
|
277
|
+
args = get_command_args(query, "commit")
|
|
278
|
+
kwargs = get_command_kwargs(query, "commit")
|
|
279
|
+
print(f"Commit args: {args}")
|
|
280
|
+
print(f"Commit kwargs: {kwargs}")
|