jarvis-ai-assistant 0.1.130__py3-none-any.whl → 0.1.132__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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +71 -38
- jarvis/jarvis_agent/builtin_input_handler.py +73 -0
- jarvis/{jarvis_code_agent → jarvis_agent}/file_input_handler.py +1 -1
- jarvis/jarvis_agent/main.py +1 -1
- jarvis/{jarvis_code_agent → jarvis_agent}/patch.py +77 -55
- jarvis/{jarvis_code_agent → jarvis_agent}/shell_input_handler.py +1 -2
- jarvis/jarvis_code_agent/code_agent.py +93 -88
- jarvis/jarvis_dev/main.py +335 -626
- jarvis/jarvis_git_squash/main.py +11 -32
- jarvis/jarvis_lsp/base.py +2 -26
- jarvis/jarvis_lsp/cpp.py +2 -14
- jarvis/jarvis_lsp/go.py +0 -13
- jarvis/jarvis_lsp/python.py +1 -30
- jarvis/jarvis_lsp/registry.py +10 -14
- jarvis/jarvis_lsp/rust.py +0 -12
- jarvis/jarvis_multi_agent/__init__.py +20 -29
- jarvis/jarvis_platform/ai8.py +7 -32
- jarvis/jarvis_platform/base.py +2 -7
- jarvis/jarvis_platform/kimi.py +3 -144
- jarvis/jarvis_platform/ollama.py +54 -68
- jarvis/jarvis_platform/openai.py +0 -4
- jarvis/jarvis_platform/oyi.py +0 -75
- jarvis/jarvis_platform/registry.py +1 -1
- jarvis/jarvis_platform/yuanbao.py +264 -0
- jarvis/jarvis_platform_manager/main.py +3 -3
- jarvis/jarvis_rag/file_processors.py +138 -0
- jarvis/jarvis_rag/main.py +1305 -425
- jarvis/jarvis_tools/ask_codebase.py +227 -41
- jarvis/jarvis_tools/code_review.py +229 -166
- jarvis/jarvis_tools/create_code_agent.py +76 -72
- jarvis/jarvis_tools/create_sub_agent.py +32 -15
- jarvis/jarvis_tools/execute_python_script.py +58 -0
- jarvis/jarvis_tools/execute_shell.py +15 -28
- jarvis/jarvis_tools/execute_shell_script.py +2 -2
- jarvis/jarvis_tools/file_analyzer.py +271 -0
- jarvis/jarvis_tools/file_operation.py +3 -3
- jarvis/jarvis_tools/find_caller.py +213 -0
- jarvis/jarvis_tools/find_symbol.py +211 -0
- jarvis/jarvis_tools/function_analyzer.py +248 -0
- jarvis/jarvis_tools/git_commiter.py +89 -70
- jarvis/jarvis_tools/lsp_find_definition.py +83 -67
- jarvis/jarvis_tools/lsp_find_references.py +62 -46
- jarvis/jarvis_tools/lsp_get_diagnostics.py +90 -74
- jarvis/jarvis_tools/methodology.py +89 -48
- jarvis/jarvis_tools/project_analyzer.py +220 -0
- jarvis/jarvis_tools/read_code.py +24 -3
- jarvis/jarvis_tools/read_webpage.py +195 -81
- jarvis/jarvis_tools/registry.py +132 -11
- jarvis/jarvis_tools/search_web.py +73 -30
- jarvis/jarvis_tools/tool_generator.py +7 -9
- jarvis/jarvis_utils/__init__.py +1 -0
- jarvis/jarvis_utils/config.py +67 -3
- jarvis/jarvis_utils/embedding.py +344 -45
- jarvis/jarvis_utils/git_utils.py +18 -2
- jarvis/jarvis_utils/input.py +7 -4
- jarvis/jarvis_utils/methodology.py +379 -7
- jarvis/jarvis_utils/output.py +5 -3
- jarvis/jarvis_utils/utils.py +62 -10
- {jarvis_ai_assistant-0.1.130.dist-info → jarvis_ai_assistant-0.1.132.dist-info}/METADATA +3 -4
- jarvis_ai_assistant-0.1.132.dist-info/RECORD +82 -0
- {jarvis_ai_assistant-0.1.130.dist-info → jarvis_ai_assistant-0.1.132.dist-info}/entry_points.txt +2 -0
- jarvis/jarvis_c2rust/c2rust.yaml +0 -734
- jarvis/jarvis_code_agent/builtin_input_handler.py +0 -43
- jarvis/jarvis_codebase/__init__.py +0 -0
- jarvis/jarvis_codebase/main.py +0 -1011
- jarvis/jarvis_tools/lsp_get_document_symbols.py +0 -87
- jarvis/jarvis_tools/lsp_prepare_rename.py +0 -130
- jarvis_ai_assistant-0.1.130.dist-info/RECORD +0 -79
- {jarvis_ai_assistant-0.1.130.dist-info → jarvis_ai_assistant-0.1.132.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.130.dist-info → jarvis_ai_assistant-0.1.132.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.130.dist-info → jarvis_ai_assistant-0.1.132.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from typing import Dict, Any
|
|
2
|
-
import
|
|
3
|
-
from bs4 import BeautifulSoup
|
|
4
|
-
from
|
|
2
|
+
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
|
|
3
|
+
from bs4 import BeautifulSoup, Tag
|
|
4
|
+
from urllib.parse import urlparse, urljoin
|
|
5
|
+
import re
|
|
5
6
|
|
|
6
7
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
7
8
|
|
|
@@ -20,93 +21,206 @@ class WebpageTool:
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
def execute(self, args: Dict) -> Dict[str, Any]:
|
|
23
|
-
"""Read webpage content"""
|
|
24
|
+
"""Read webpage content using Playwright to handle JavaScript-rendered pages"""
|
|
24
25
|
try:
|
|
25
|
-
url = args["url"].strip()
|
|
26
|
+
url = args["url"].strip()
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
with yaspin(text="正在读取网页...", color="cyan") as spinner:
|
|
34
|
-
response = requests.get(url, headers=headers, timeout=10)
|
|
35
|
-
response.raise_for_status()
|
|
36
|
-
spinner.text = "网页读取完成"
|
|
37
|
-
spinner.ok("✅")
|
|
28
|
+
with sync_playwright() as p:
|
|
29
|
+
# Launch browser
|
|
30
|
+
browser = p.chromium.launch(
|
|
31
|
+
headless=True,
|
|
32
|
+
args=['--disable-gpu', '--no-sandbox', '--disable-dev-shm-usage']
|
|
33
|
+
)
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# Parse HTML
|
|
45
|
-
with yaspin(text="正在解析网页...", color="cyan") as spinner:
|
|
46
|
-
soup = BeautifulSoup(response.text, 'html.parser')
|
|
47
|
-
spinner.text = "网页解析完成"
|
|
48
|
-
spinner.ok("✅")
|
|
49
|
-
|
|
50
|
-
# Remove script and style tags
|
|
51
|
-
with yaspin(text="正在移除脚本和样式...", color="cyan") as spinner:
|
|
52
|
-
for script in soup(["script", "style"]):
|
|
53
|
-
script.decompose()
|
|
54
|
-
spinner.text = "脚本和样式移除完成"
|
|
55
|
-
spinner.ok("✅")
|
|
56
|
-
|
|
57
|
-
# Extract title
|
|
58
|
-
with yaspin(text="正在提取标题...", color="cyan") as spinner:
|
|
59
|
-
title = soup.title.string if soup.title else ""
|
|
60
|
-
title = title.strip() if title else "No title"
|
|
61
|
-
spinner.text = "标题提取完成"
|
|
62
|
-
spinner.ok("✅")
|
|
63
|
-
|
|
64
|
-
with yaspin(text="正在提取文本和链接...", color="cyan") as spinner:
|
|
65
|
-
# Extract text and links
|
|
66
|
-
text_parts = []
|
|
67
|
-
links = []
|
|
35
|
+
# Create a new page with appropriate settings
|
|
36
|
+
page = browser.new_page(
|
|
37
|
+
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
|
38
|
+
viewport={'width': 1920, 'height': 1080}
|
|
39
|
+
)
|
|
68
40
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
41
|
+
# Set timeout to avoid long waits
|
|
42
|
+
page.set_default_timeout(30000) # 30 seconds
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Navigate to URL and wait for page to load
|
|
46
|
+
response = page.goto(url, wait_until="domcontentloaded")
|
|
47
|
+
|
|
48
|
+
# Additional wait for network to be idle (with a timeout)
|
|
49
|
+
try:
|
|
50
|
+
page.wait_for_load_state("networkidle", timeout=10000)
|
|
51
|
+
except PlaywrightTimeoutError:
|
|
52
|
+
# Continue even if network doesn't become completely idle
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
# Make sure we got a valid response
|
|
56
|
+
if not response or response.status >= 400:
|
|
57
|
+
raise Exception(f"Failed to load page: HTTP {response.status if response else 'No response'}")
|
|
58
|
+
|
|
59
|
+
# Get page title safely
|
|
60
|
+
title = "No title"
|
|
61
|
+
try:
|
|
62
|
+
title = page.title()
|
|
63
|
+
except Exception:
|
|
64
|
+
# Try to extract title from content if direct method fails
|
|
65
|
+
try:
|
|
66
|
+
title_element = page.query_selector("title")
|
|
67
|
+
if title_element:
|
|
68
|
+
title = title_element.text_content() or "No title"
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# Get the HTML content after JavaScript execution
|
|
73
|
+
html_content = page.content()
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
raise Exception(f"Error navigating to page: {str(e)}")
|
|
77
|
+
finally:
|
|
78
|
+
# Always close browser
|
|
79
|
+
browser.close()
|
|
80
|
+
|
|
81
|
+
# Parse with BeautifulSoup and convert to markdown
|
|
82
|
+
markdown_content = self._html_to_markdown(html_content, url)
|
|
83
|
+
|
|
84
|
+
# Build output in markdown format
|
|
83
85
|
output = [
|
|
84
|
-
f"
|
|
85
|
-
"",
|
|
86
|
-
|
|
87
|
-
"\n".join(text_parts),
|
|
88
|
-
"",
|
|
89
|
-
"Links found:",
|
|
90
|
-
"\n".join(links) if links else "No links found"
|
|
86
|
+
f"# {title}",
|
|
87
|
+
f"Url: {url}",
|
|
88
|
+
markdown_content
|
|
91
89
|
]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"stderr": ""
|
|
99
|
-
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"success": True,
|
|
93
|
+
"stdout": "\n".join(output),
|
|
94
|
+
"stderr": ""
|
|
95
|
+
}
|
|
100
96
|
|
|
101
|
-
except requests.RequestException as e:
|
|
102
|
-
return {
|
|
103
|
-
"success": False,
|
|
104
|
-
"stdout": "",
|
|
105
|
-
"stderr": f"Webpage request failed: {str(e)}"
|
|
106
|
-
}
|
|
107
97
|
except Exception as e:
|
|
98
|
+
PrettyOutput.print(f"读取网页失败: {str(e)}", OutputType.ERROR)
|
|
108
99
|
return {
|
|
109
100
|
"success": False,
|
|
110
101
|
"stdout": "",
|
|
111
102
|
"stderr": f"Failed to parse webpage: {str(e)}"
|
|
112
|
-
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def _create_soup_element(self, content):
|
|
106
|
+
"""Safely create a BeautifulSoup element, ensuring it's treated as markup"""
|
|
107
|
+
if isinstance(content, str):
|
|
108
|
+
# Create a wrapper tag to ensure proper parsing
|
|
109
|
+
soup_div = BeautifulSoup(f"<div>{content}</div>", 'html.parser').div
|
|
110
|
+
if soup_div is not None:
|
|
111
|
+
return soup_div.contents
|
|
112
|
+
# Return an empty list if the div is None
|
|
113
|
+
return []
|
|
114
|
+
return content
|
|
115
|
+
|
|
116
|
+
def _html_to_markdown(self, html_content: str, base_url: str) -> str:
|
|
117
|
+
"""Convert HTML to Markdown format preserving the content structure"""
|
|
118
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
119
|
+
|
|
120
|
+
# Remove unwanted elements
|
|
121
|
+
for element in soup(['script', 'style', 'meta', 'noscript', 'head']):
|
|
122
|
+
element.decompose()
|
|
123
|
+
|
|
124
|
+
# Process headings
|
|
125
|
+
for level in range(1, 7):
|
|
126
|
+
for heading in soup.find_all(f'h{level}'):
|
|
127
|
+
text = heading.get_text().strip()
|
|
128
|
+
heading_md = "\n\n" + "#" * level + " " + text + "\n\n"
|
|
129
|
+
new_element = self._create_soup_element(heading_md)
|
|
130
|
+
heading.replace_with(*new_element)
|
|
131
|
+
|
|
132
|
+
# Process paragraphs
|
|
133
|
+
for p in soup.find_all('p'):
|
|
134
|
+
text = p.get_text().strip()
|
|
135
|
+
if text:
|
|
136
|
+
new_element = self._create_soup_element("\n\n" + text + "\n\n")
|
|
137
|
+
p.replace_with(*new_element)
|
|
138
|
+
|
|
139
|
+
# Process unordered lists
|
|
140
|
+
for ul in soup.find_all('ul'):
|
|
141
|
+
items = []
|
|
142
|
+
for li in ul.find_all('li', recursive=False):
|
|
143
|
+
items.append("* " + li.get_text().strip())
|
|
144
|
+
new_element = self._create_soup_element("\n\n" + "\n".join(items) + "\n\n")
|
|
145
|
+
ul.replace_with(*new_element)
|
|
146
|
+
|
|
147
|
+
# Process ordered lists
|
|
148
|
+
for ol in soup.find_all('ol'):
|
|
149
|
+
items = []
|
|
150
|
+
for i, li in enumerate(ol.find_all('li', recursive=False), 1):
|
|
151
|
+
items.append(str(i) + ". " + li.get_text().strip())
|
|
152
|
+
new_element = self._create_soup_element("\n\n" + "\n".join(items) + "\n\n")
|
|
153
|
+
ol.replace_with(*new_element)
|
|
154
|
+
|
|
155
|
+
# Process links (first pass)
|
|
156
|
+
for a in soup.find_all('a', href=True):
|
|
157
|
+
try:
|
|
158
|
+
href = a['href']
|
|
159
|
+
text = a.get_text().strip()
|
|
160
|
+
if text and href:
|
|
161
|
+
# Convert relative URLs to absolute
|
|
162
|
+
if href.startswith('/') and not href.startswith('//'):
|
|
163
|
+
href = urljoin(base_url, href)
|
|
164
|
+
link_md = "[" + text + "](" + href + ")"
|
|
165
|
+
new_element = self._create_soup_element(link_md)
|
|
166
|
+
a.replace_with(*new_element)
|
|
167
|
+
except (KeyError, AttributeError):
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# Process images
|
|
171
|
+
for img in soup.find_all('img', src=True):
|
|
172
|
+
try:
|
|
173
|
+
src = img['src']
|
|
174
|
+
alt = img.get('alt', 'Image').strip()
|
|
175
|
+
# Convert relative URLs to absolute
|
|
176
|
+
if src.startswith('/') and not src.startswith('//'):
|
|
177
|
+
src = urljoin(base_url, src)
|
|
178
|
+
img_md = ""
|
|
179
|
+
new_element = self._create_soup_element(img_md)
|
|
180
|
+
img.replace_with(*new_element)
|
|
181
|
+
except (KeyError, AttributeError, UnboundLocalError):
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
# Process code blocks
|
|
185
|
+
for pre in soup.find_all('pre'):
|
|
186
|
+
code = pre.get_text().strip()
|
|
187
|
+
pre_md = "\n\n```\n" + code + "\n```\n\n"
|
|
188
|
+
new_element = self._create_soup_element(pre_md)
|
|
189
|
+
pre.replace_with(*new_element)
|
|
190
|
+
|
|
191
|
+
# Process inline code
|
|
192
|
+
for code in soup.find_all('code'):
|
|
193
|
+
text = code.get_text().strip()
|
|
194
|
+
code_md = "`" + text + "`"
|
|
195
|
+
new_element = self._create_soup_element(code_md)
|
|
196
|
+
code.replace_with(*new_element)
|
|
197
|
+
|
|
198
|
+
# Process line breaks
|
|
199
|
+
for br in soup.find_all('br'):
|
|
200
|
+
new_element = self._create_soup_element('\n')
|
|
201
|
+
br.replace_with(*new_element)
|
|
202
|
+
|
|
203
|
+
# Get the full text
|
|
204
|
+
markdown_text = soup.get_text()
|
|
205
|
+
|
|
206
|
+
# Clean up extra whitespace and line breaks
|
|
207
|
+
markdown_text = re.sub(r'\n{3,}', '\n\n', markdown_text)
|
|
208
|
+
markdown_text = re.sub(r'\s{2,}', ' ', markdown_text)
|
|
209
|
+
|
|
210
|
+
# Process links again (for any that might have been missed)
|
|
211
|
+
link_pattern = r'\[([^\]]+)\]\(([^)]+)\)'
|
|
212
|
+
all_links = re.findall(link_pattern, markdown_text)
|
|
213
|
+
|
|
214
|
+
# Add a section with all links at the end
|
|
215
|
+
if all_links:
|
|
216
|
+
link_section = ["", "## Links", ""]
|
|
217
|
+
seen_links = set()
|
|
218
|
+
for text, href in all_links:
|
|
219
|
+
link_entry = "[" + text + "](" + href + ")"
|
|
220
|
+
if link_entry not in seen_links:
|
|
221
|
+
link_section.append(link_entry)
|
|
222
|
+
seen_links.add(link_entry)
|
|
223
|
+
|
|
224
|
+
markdown_text += "\n\n" + "\n".join(link_section)
|
|
225
|
+
|
|
226
|
+
return markdown_text.strip()
|
jarvis/jarvis_tools/registry.py
CHANGED
|
@@ -13,20 +13,21 @@ from jarvis.jarvis_tools.base import Tool
|
|
|
13
13
|
from jarvis.jarvis_utils.config import get_max_token_count
|
|
14
14
|
from jarvis.jarvis_utils.embedding import get_context_token_count
|
|
15
15
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
16
|
+
from jarvis.jarvis_utils.utils import ct, ot, init_env
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
tool_call_help = """
|
|
20
|
+
tool_call_help = f"""
|
|
20
21
|
# 🛠️ 工具使用系统
|
|
21
22
|
您正在使用一个需要精确格式和严格规则的工具执行系统。
|
|
22
23
|
|
|
23
24
|
# 📋 工具调用格式
|
|
24
|
-
|
|
25
|
+
{ot("TOOL_CALL")}
|
|
25
26
|
name: 工具名称
|
|
26
27
|
arguments:
|
|
27
28
|
param1: 值1
|
|
28
29
|
param2: 值2
|
|
29
|
-
|
|
30
|
+
{ct("TOOL_CALL")}
|
|
30
31
|
|
|
31
32
|
# ❗ 关键规则
|
|
32
33
|
1. 每次只使用一个工具
|
|
@@ -53,12 +54,12 @@ arguments:
|
|
|
53
54
|
# 📝 字符串参数格式
|
|
54
55
|
始终使用 | 语法表示字符串参数:
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
{ot("TOOL_CALL")}
|
|
57
58
|
name: execute_shell
|
|
58
59
|
arguments:
|
|
59
60
|
command: |
|
|
60
61
|
git status --porcelain
|
|
61
|
-
|
|
62
|
+
{ct("TOOL_CALL")}
|
|
62
63
|
|
|
63
64
|
# 💡 最佳实践
|
|
64
65
|
- 准备好后立即开始执行
|
|
@@ -233,7 +234,7 @@ class ToolRegistry(OutputHandler):
|
|
|
233
234
|
Exception: 如果工具调用缺少必要字段
|
|
234
235
|
"""
|
|
235
236
|
# 将内容拆分为行
|
|
236
|
-
data = re.findall(r'
|
|
237
|
+
data = re.findall(ot("TOOL_CALL")+r'(.*?)'+ct("TOOL_CALL"), content, re.DOTALL)
|
|
237
238
|
ret = []
|
|
238
239
|
for item in data:
|
|
239
240
|
try:
|
|
@@ -270,18 +271,18 @@ class ToolRegistry(OutputHandler):
|
|
|
270
271
|
name = tool_call["name"]
|
|
271
272
|
args = tool_call["arguments"]
|
|
272
273
|
|
|
273
|
-
tool_call_help = """
|
|
274
|
+
tool_call_help = f"""
|
|
274
275
|
# 🛠️ 工具使用系统
|
|
275
276
|
您正在使用一个需要精确格式和严格规则的工具执行系统。
|
|
276
277
|
|
|
277
278
|
# 📋 工具调用格式
|
|
278
279
|
|
|
279
|
-
|
|
280
|
+
{ot("TOOL_CALL")}
|
|
280
281
|
name: 工具名称
|
|
281
282
|
arguments:
|
|
282
283
|
param1: 值1
|
|
283
284
|
param2: 值2
|
|
284
|
-
|
|
285
|
+
{ct("TOOL_CALL")}
|
|
285
286
|
|
|
286
287
|
# ❗ 关键规则
|
|
287
288
|
1. 每次只使用一个工具
|
|
@@ -308,12 +309,12 @@ arguments:
|
|
|
308
309
|
# 📝 字符串参数格式
|
|
309
310
|
始终使用 | 语法表示字符串参数:
|
|
310
311
|
|
|
311
|
-
|
|
312
|
+
{ot("TOOL_CALL")}
|
|
312
313
|
name: execute_shell
|
|
313
314
|
arguments:
|
|
314
315
|
command: |
|
|
315
316
|
git status --porcelain
|
|
316
|
-
|
|
317
|
+
{ct("TOOL_CALL")}
|
|
317
318
|
|
|
318
319
|
# 💡 最佳实践
|
|
319
320
|
- 准备好后立即开始执行
|
|
@@ -394,3 +395,123 @@ arguments:
|
|
|
394
395
|
except Exception as e:
|
|
395
396
|
PrettyOutput.print(f"工具执行失败:{str(e)}", OutputType.ERROR)
|
|
396
397
|
return f"工具调用失败: {str(e)}"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def main():
|
|
401
|
+
"""命令行工具入口,提供工具列表查看和工具调用功能"""
|
|
402
|
+
import argparse
|
|
403
|
+
import json
|
|
404
|
+
|
|
405
|
+
init_env()
|
|
406
|
+
|
|
407
|
+
parser = argparse.ArgumentParser(description='Jarvis 工具系统命令行界面')
|
|
408
|
+
subparsers = parser.add_subparsers(dest='command', help='命令')
|
|
409
|
+
|
|
410
|
+
# 列出工具子命令
|
|
411
|
+
list_parser = subparsers.add_parser('list', help='列出所有可用工具')
|
|
412
|
+
list_parser.add_argument('--json', action='store_true', help='以JSON格式输出')
|
|
413
|
+
list_parser.add_argument('--detailed', action='store_true', help='显示详细信息')
|
|
414
|
+
|
|
415
|
+
# 调用工具子命令
|
|
416
|
+
call_parser = subparsers.add_parser('call', help='调用指定工具')
|
|
417
|
+
call_parser.add_argument('tool_name', help='要调用的工具名称')
|
|
418
|
+
call_parser.add_argument('--args', type=str, help='工具参数 (JSON格式)')
|
|
419
|
+
call_parser.add_argument('--args-file', type=str, help='从文件加载工具参数 (JSON格式)')
|
|
420
|
+
|
|
421
|
+
args = parser.parse_args()
|
|
422
|
+
|
|
423
|
+
# 初始化工具注册表
|
|
424
|
+
registry = ToolRegistry()
|
|
425
|
+
|
|
426
|
+
if args.command == 'list':
|
|
427
|
+
tools = registry.get_all_tools()
|
|
428
|
+
|
|
429
|
+
if args.json:
|
|
430
|
+
if args.detailed:
|
|
431
|
+
print(json.dumps(tools, indent=2, ensure_ascii=False))
|
|
432
|
+
else:
|
|
433
|
+
simple_tools = [{"name": t["name"], "description": t["description"]} for t in tools]
|
|
434
|
+
print(json.dumps(simple_tools, indent=2, ensure_ascii=False))
|
|
435
|
+
else:
|
|
436
|
+
PrettyOutput.section("可用工具列表", OutputType.SYSTEM)
|
|
437
|
+
for tool in tools:
|
|
438
|
+
print(f"\n✅ {tool['name']}")
|
|
439
|
+
print(f" 描述: {tool['description']}")
|
|
440
|
+
if args.detailed:
|
|
441
|
+
print(f" 参数:")
|
|
442
|
+
params = tool['parameters'].get('properties', {})
|
|
443
|
+
required = tool['parameters'].get('required', [])
|
|
444
|
+
for param_name, param_info in params.items():
|
|
445
|
+
req_mark = "*" if param_name in required else ""
|
|
446
|
+
desc = param_info.get('description', '无描述')
|
|
447
|
+
print(f" - {param_name}{req_mark}: {desc}")
|
|
448
|
+
|
|
449
|
+
elif args.command == 'call':
|
|
450
|
+
tool_name = args.tool_name
|
|
451
|
+
tool = registry.get_tool(tool_name)
|
|
452
|
+
|
|
453
|
+
if not tool:
|
|
454
|
+
PrettyOutput.print(f"错误: 工具 '{tool_name}' 不存在", OutputType.ERROR)
|
|
455
|
+
available_tools = ", ".join([t["name"] for t in registry.get_all_tools()])
|
|
456
|
+
print(f"可用工具: {available_tools}")
|
|
457
|
+
return 1
|
|
458
|
+
|
|
459
|
+
# 获取参数
|
|
460
|
+
tool_args = {}
|
|
461
|
+
if args.args:
|
|
462
|
+
try:
|
|
463
|
+
tool_args = json.loads(args.args)
|
|
464
|
+
except json.JSONDecodeError:
|
|
465
|
+
PrettyOutput.print("错误: 参数必须是有效的JSON格式", OutputType.ERROR)
|
|
466
|
+
return 1
|
|
467
|
+
|
|
468
|
+
elif args.args_file:
|
|
469
|
+
try:
|
|
470
|
+
with open(args.args_file, 'r', encoding='utf-8') as f:
|
|
471
|
+
tool_args = json.load(f)
|
|
472
|
+
except (json.JSONDecodeError, FileNotFoundError) as e:
|
|
473
|
+
PrettyOutput.print(f"错误: 无法从文件加载参数: {str(e)}", OutputType.ERROR)
|
|
474
|
+
return 1
|
|
475
|
+
|
|
476
|
+
# 检查必需参数
|
|
477
|
+
required_params = tool.parameters.get('required', [])
|
|
478
|
+
missing_params = [p for p in required_params if p not in tool_args]
|
|
479
|
+
|
|
480
|
+
if missing_params:
|
|
481
|
+
PrettyOutput.print(f"错误: 缺少必需参数: {', '.join(missing_params)}", OutputType.ERROR)
|
|
482
|
+
print("\n参数说明:")
|
|
483
|
+
params = tool.parameters.get('properties', {})
|
|
484
|
+
for param_name in required_params:
|
|
485
|
+
param_info = params.get(param_name, {})
|
|
486
|
+
desc = param_info.get('description', '无描述')
|
|
487
|
+
print(f" - {param_name}: {desc}")
|
|
488
|
+
return 1
|
|
489
|
+
|
|
490
|
+
# 执行工具
|
|
491
|
+
with yaspin(text=f"正在执行工具 {tool_name}...").dots12:
|
|
492
|
+
result = registry.execute_tool(tool_name, tool_args)
|
|
493
|
+
|
|
494
|
+
# 显示结果
|
|
495
|
+
if result["success"]:
|
|
496
|
+
PrettyOutput.section(f"工具 {tool_name} 执行成功", OutputType.SUCCESS)
|
|
497
|
+
else:
|
|
498
|
+
PrettyOutput.section(f"工具 {tool_name} 执行失败", OutputType.ERROR)
|
|
499
|
+
|
|
500
|
+
if result.get("stdout"):
|
|
501
|
+
print("\n输出:")
|
|
502
|
+
print(result["stdout"])
|
|
503
|
+
|
|
504
|
+
if result.get("stderr"):
|
|
505
|
+
PrettyOutput.print("\n错误:", OutputType.ERROR)
|
|
506
|
+
print(result["stderr"])
|
|
507
|
+
|
|
508
|
+
return 0 if result["success"] else 1
|
|
509
|
+
|
|
510
|
+
else:
|
|
511
|
+
parser.print_help()
|
|
512
|
+
|
|
513
|
+
return 0
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
if __name__ == "__main__":
|
|
517
|
+
sys.exit(main())
|