jarvis-ai-assistant 0.2.2__py3-none-any.whl → 0.2.4__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/edit_file_handler.py +5 -0
- jarvis/jarvis_agent/jarvis.py +22 -25
- jarvis/jarvis_agent/main.py +6 -6
- jarvis/jarvis_agent/prompts.py +26 -4
- jarvis/jarvis_code_agent/code_agent.py +279 -11
- jarvis/jarvis_code_analysis/code_review.py +21 -19
- jarvis/jarvis_data/config_schema.json +86 -18
- jarvis/jarvis_git_squash/main.py +3 -3
- jarvis/jarvis_git_utils/git_commiter.py +32 -11
- jarvis/jarvis_mcp/sse_mcp_client.py +4 -6
- jarvis/jarvis_mcp/streamable_mcp_client.py +5 -9
- jarvis/jarvis_platform/tongyi.py +9 -9
- jarvis/jarvis_rag/cli.py +79 -23
- jarvis/jarvis_rag/query_rewriter.py +61 -12
- jarvis/jarvis_rag/rag_pipeline.py +143 -34
- jarvis/jarvis_rag/retriever.py +6 -6
- jarvis/jarvis_smart_shell/main.py +2 -2
- jarvis/jarvis_stats/__init__.py +13 -0
- jarvis/jarvis_stats/cli.py +337 -0
- jarvis/jarvis_stats/stats.py +433 -0
- jarvis/jarvis_stats/storage.py +329 -0
- jarvis/jarvis_stats/visualizer.py +443 -0
- jarvis/jarvis_tools/cli/main.py +84 -15
- jarvis/jarvis_tools/generate_new_tool.py +22 -1
- jarvis/jarvis_tools/registry.py +35 -16
- jarvis/jarvis_tools/search_web.py +3 -3
- jarvis/jarvis_tools/virtual_tty.py +315 -26
- jarvis/jarvis_utils/config.py +98 -11
- jarvis/jarvis_utils/git_utils.py +8 -16
- jarvis/jarvis_utils/globals.py +29 -8
- jarvis/jarvis_utils/input.py +114 -121
- jarvis/jarvis_utils/utils.py +213 -37
- {jarvis_ai_assistant-0.2.2.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/METADATA +99 -9
- {jarvis_ai_assistant-0.2.2.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/RECORD +39 -34
- {jarvis_ai_assistant-0.2.2.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/entry_points.txt +2 -0
- {jarvis_ai_assistant-0.2.2.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.2.2.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.2.2.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,443 @@
|
|
1
|
+
"""
|
2
|
+
统计数据可视化模块
|
3
|
+
|
4
|
+
提供终端图形化展示功能
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
|
9
|
+
from typing import Dict, List, Optional, Any
|
10
|
+
from collections import OrderedDict
|
11
|
+
from rich.console import Console
|
12
|
+
from rich.table import Table
|
13
|
+
from rich.panel import Panel
|
14
|
+
from rich import box
|
15
|
+
|
16
|
+
|
17
|
+
class StatsVisualizer:
|
18
|
+
"""统计数据可视化类"""
|
19
|
+
|
20
|
+
def __init__(self, width: Optional[int] = None, height: Optional[int] = None):
|
21
|
+
"""
|
22
|
+
初始化可视化器
|
23
|
+
|
24
|
+
Args:
|
25
|
+
width: 图表宽度,默认为终端宽度-10
|
26
|
+
height: 图表高度,默认为20
|
27
|
+
"""
|
28
|
+
self.width = width or self._get_terminal_width() - 10
|
29
|
+
self.height = height or 20
|
30
|
+
|
31
|
+
# 确保最小尺寸
|
32
|
+
self.width = max(self.width, 40)
|
33
|
+
self.height = max(self.height, 10)
|
34
|
+
|
35
|
+
# 初始化Rich Console
|
36
|
+
self.console = Console()
|
37
|
+
|
38
|
+
def _get_terminal_width(self) -> int:
|
39
|
+
"""获取终端宽度"""
|
40
|
+
try:
|
41
|
+
columns = os.get_terminal_size().columns
|
42
|
+
return columns
|
43
|
+
except:
|
44
|
+
return 80
|
45
|
+
|
46
|
+
def plot_line_chart(
|
47
|
+
self,
|
48
|
+
data: Dict[str, float],
|
49
|
+
title: str = "",
|
50
|
+
unit: Optional[str] = None,
|
51
|
+
show_values: bool = True,
|
52
|
+
) -> str:
|
53
|
+
"""
|
54
|
+
绘制折线图
|
55
|
+
|
56
|
+
Args:
|
57
|
+
data: 数据字典,键为时间标签,值为数值
|
58
|
+
title: 图表标题
|
59
|
+
unit: 单位
|
60
|
+
show_values: 是否显示数值
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
图表字符串
|
64
|
+
"""
|
65
|
+
if not data:
|
66
|
+
return "无数据可显示"
|
67
|
+
|
68
|
+
# 排序数据
|
69
|
+
sorted_data = OrderedDict(sorted(data.items()))
|
70
|
+
|
71
|
+
# 提取值和标签
|
72
|
+
labels = list(sorted_data.keys())
|
73
|
+
values = list(sorted_data.values())
|
74
|
+
|
75
|
+
# 计算数值范围
|
76
|
+
min_val = min(values)
|
77
|
+
max_val = max(values)
|
78
|
+
|
79
|
+
# 如果所有值相同,调整范围
|
80
|
+
if min_val == max_val:
|
81
|
+
if min_val == 0:
|
82
|
+
min_val = -1
|
83
|
+
max_val = 1
|
84
|
+
else:
|
85
|
+
min_val = min_val * 0.9
|
86
|
+
max_val = max_val * 1.1
|
87
|
+
|
88
|
+
# 创建图表画布
|
89
|
+
chart_width = self.width - 10 # 留出y轴标签空间
|
90
|
+
chart_height = self.height - 5 # 留出x轴标签空间
|
91
|
+
|
92
|
+
# 初始化画布
|
93
|
+
canvas = [[" " for _ in range(chart_width)] for _ in range(chart_height)]
|
94
|
+
|
95
|
+
# 计算缩放因子
|
96
|
+
x_scale = (chart_width - 1) / max(len(values) - 1, 1)
|
97
|
+
y_scale = (chart_height - 1) / (max_val - min_val)
|
98
|
+
|
99
|
+
# 绘制坐标轴
|
100
|
+
for i in range(chart_height):
|
101
|
+
canvas[i][0] = "│"
|
102
|
+
for j in range(chart_width):
|
103
|
+
canvas[chart_height - 1][j] = "─"
|
104
|
+
canvas[chart_height - 1][0] = "└"
|
105
|
+
|
106
|
+
# 绘制数据点和连线
|
107
|
+
points = []
|
108
|
+
for i, value in enumerate(values):
|
109
|
+
x = int(i * x_scale) + 1
|
110
|
+
y = chart_height - 1 - int((value - min_val) * y_scale)
|
111
|
+
y = max(0, min(chart_height - 1, y)) # 确保在范围内
|
112
|
+
|
113
|
+
if 0 <= x < chart_width and 0 <= y < chart_height:
|
114
|
+
points.append((x, y, value))
|
115
|
+
canvas[y][x] = "●"
|
116
|
+
|
117
|
+
# 连接数据点
|
118
|
+
for i in range(len(points) - 1):
|
119
|
+
x1, y1, _ = points[i]
|
120
|
+
x2, y2, _ = points[i + 1]
|
121
|
+
self._draw_line(canvas, x1, y1, x2, y2)
|
122
|
+
|
123
|
+
# 构建输出
|
124
|
+
output = []
|
125
|
+
|
126
|
+
# 标题
|
127
|
+
if title:
|
128
|
+
output.append(f"\n{title.center(self.width)}")
|
129
|
+
output.append("=" * self.width)
|
130
|
+
|
131
|
+
# Y轴标签和图表
|
132
|
+
y_labels = self._generate_y_labels(min_val, max_val, chart_height)
|
133
|
+
|
134
|
+
for i, row in enumerate(canvas):
|
135
|
+
label = y_labels.get(i, "")
|
136
|
+
line = f"{label:>8} " + "".join(row)
|
137
|
+
output.append(line)
|
138
|
+
|
139
|
+
# X轴标签
|
140
|
+
x_labels = self._generate_x_labels(labels, chart_width)
|
141
|
+
output.append(" " * 9 + x_labels)
|
142
|
+
|
143
|
+
# 单位
|
144
|
+
if unit:
|
145
|
+
output.append(f"\n单位: {unit}")
|
146
|
+
|
147
|
+
# 统计信息
|
148
|
+
if show_values:
|
149
|
+
output.append(
|
150
|
+
f"\n最小值: {min_val:.2f}, 最大值: {max_val:.2f}, 平均值: {sum(values)/len(values):.2f}"
|
151
|
+
)
|
152
|
+
|
153
|
+
return "\n".join(output)
|
154
|
+
|
155
|
+
def plot_bar_chart(
|
156
|
+
self,
|
157
|
+
data: Dict[str, float],
|
158
|
+
title: str = "",
|
159
|
+
unit: Optional[str] = None,
|
160
|
+
horizontal: bool = False,
|
161
|
+
) -> str:
|
162
|
+
"""
|
163
|
+
绘制柱状图
|
164
|
+
|
165
|
+
Args:
|
166
|
+
data: 数据字典
|
167
|
+
title: 图表标题
|
168
|
+
unit: 单位
|
169
|
+
horizontal: 是否横向
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
图表字符串
|
173
|
+
"""
|
174
|
+
if not data:
|
175
|
+
return "无数据可显示"
|
176
|
+
|
177
|
+
output = []
|
178
|
+
|
179
|
+
# 标题
|
180
|
+
if title:
|
181
|
+
output.append(f"\n{title.center(self.width)}")
|
182
|
+
output.append("=" * self.width)
|
183
|
+
|
184
|
+
# 找出最大值用于缩放
|
185
|
+
max_value = max(data.values()) if data.values() else 1
|
186
|
+
|
187
|
+
if horizontal:
|
188
|
+
# 横向柱状图
|
189
|
+
max_label_len = max(len(str(k)) for k in data.keys())
|
190
|
+
bar_width = self.width - max_label_len - 15
|
191
|
+
|
192
|
+
for label, value in data.items():
|
193
|
+
bar_len = int((value / max_value) * bar_width)
|
194
|
+
bar = "█" * bar_len
|
195
|
+
output.append(f"{str(label):>{max_label_len}} │{bar} {value:.2f}")
|
196
|
+
else:
|
197
|
+
# 纵向柱状图
|
198
|
+
bar_height = self.height - 5
|
199
|
+
labels = list(data.keys())
|
200
|
+
values = list(data.values())
|
201
|
+
|
202
|
+
# 创建画布
|
203
|
+
canvas = [[" " for _ in range(len(labels) * 4)] for _ in range(bar_height)]
|
204
|
+
|
205
|
+
# 绘制柱子
|
206
|
+
for i, (label, value) in enumerate(zip(labels, values)):
|
207
|
+
height = int((value / max_value) * bar_height)
|
208
|
+
x = i * 4 + 1
|
209
|
+
|
210
|
+
for y in range(bar_height - height, bar_height):
|
211
|
+
if y >= 0 and x < len(canvas[0]):
|
212
|
+
canvas[y][x] = "█"
|
213
|
+
if x + 1 < len(canvas[0]):
|
214
|
+
canvas[y][x + 1] = "█"
|
215
|
+
|
216
|
+
# 输出画布
|
217
|
+
for row in canvas:
|
218
|
+
output.append("".join(row))
|
219
|
+
|
220
|
+
# X轴标签
|
221
|
+
label_line = ""
|
222
|
+
for i, label in enumerate(labels):
|
223
|
+
x = i * 4
|
224
|
+
label_line += f"{label[:3]:>4}"
|
225
|
+
output.append(label_line)
|
226
|
+
|
227
|
+
# 单位
|
228
|
+
if unit:
|
229
|
+
output.append(f"\n单位: {unit}")
|
230
|
+
|
231
|
+
return "\n".join(output)
|
232
|
+
|
233
|
+
def show_summary(
|
234
|
+
self,
|
235
|
+
aggregated_data: Dict[str, Dict[str, Any]],
|
236
|
+
metric_name: str,
|
237
|
+
unit: Optional[str] = None,
|
238
|
+
tags_filter: Optional[Dict[str, str]] = None,
|
239
|
+
) -> str:
|
240
|
+
"""
|
241
|
+
显示数据摘要
|
242
|
+
|
243
|
+
Args:
|
244
|
+
aggregated_data: 聚合后的数据
|
245
|
+
metric_name: 指标名称
|
246
|
+
unit: 单位
|
247
|
+
tags_filter: 标签过滤条件
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
摘要字符串(用于兼容性,实际会直接打印)
|
251
|
+
"""
|
252
|
+
if not aggregated_data:
|
253
|
+
self.console.print("[yellow]无数据可显示[/yellow]")
|
254
|
+
return "无数据可显示"
|
255
|
+
|
256
|
+
# 创建表格
|
257
|
+
table = Table(title=f"{metric_name} 统计摘要", box=box.ROUNDED)
|
258
|
+
|
259
|
+
# 添加列
|
260
|
+
table.add_column("时间", justify="center", style="cyan")
|
261
|
+
table.add_column("计数", justify="right", style="green")
|
262
|
+
table.add_column("总和", justify="right", style="yellow")
|
263
|
+
table.add_column("平均", justify="right", style="yellow")
|
264
|
+
table.add_column("最小", justify="right", style="blue")
|
265
|
+
table.add_column("最大", justify="right", style="red")
|
266
|
+
|
267
|
+
# 添加数据行
|
268
|
+
for time_key, stats in sorted(aggregated_data.items()):
|
269
|
+
table.add_row(
|
270
|
+
time_key,
|
271
|
+
str(stats["count"]),
|
272
|
+
f"{stats['sum']:.2f}",
|
273
|
+
f"{stats['avg']:.2f}",
|
274
|
+
f"{stats['min']:.2f}",
|
275
|
+
f"{stats['max']:.2f}",
|
276
|
+
)
|
277
|
+
|
278
|
+
# 显示表格
|
279
|
+
self.console.print(table)
|
280
|
+
|
281
|
+
# 显示单位信息
|
282
|
+
if unit:
|
283
|
+
self.console.print(f"\n[dim]单位: {unit}[/dim]")
|
284
|
+
|
285
|
+
# 显示过滤条件
|
286
|
+
if tags_filter:
|
287
|
+
filter_str = ", ".join([f"{k}={v}" for k, v in tags_filter.items()])
|
288
|
+
self.console.print(f"[dim]过滤条件: {filter_str}[/dim]")
|
289
|
+
|
290
|
+
return "" # 返回空字符串,实际输出已经通过console打印
|
291
|
+
|
292
|
+
def show_table(
|
293
|
+
self,
|
294
|
+
records: List[Dict[str, Any]],
|
295
|
+
metric_name: str,
|
296
|
+
unit: Optional[str] = None,
|
297
|
+
start_time: Optional[str] = None,
|
298
|
+
end_time: Optional[str] = None,
|
299
|
+
tags_filter: Optional[Dict[str, str]] = None,
|
300
|
+
) -> str:
|
301
|
+
"""
|
302
|
+
使用Rich Table显示数据记录
|
303
|
+
|
304
|
+
Args:
|
305
|
+
records: 数据记录列表
|
306
|
+
metric_name: 指标名称
|
307
|
+
unit: 单位
|
308
|
+
start_time: 开始时间
|
309
|
+
end_time: 结束时间
|
310
|
+
tags_filter: 标签过滤条件
|
311
|
+
|
312
|
+
Returns:
|
313
|
+
空字符串(实际通过console打印)
|
314
|
+
"""
|
315
|
+
if not records:
|
316
|
+
self.console.print(f"[yellow]没有找到指标 '{metric_name}' 的数据[/yellow]")
|
317
|
+
return ""
|
318
|
+
|
319
|
+
# 创建表格
|
320
|
+
table = Table(title=f"指标: {metric_name}", box=box.ROUNDED)
|
321
|
+
|
322
|
+
# 添加列
|
323
|
+
table.add_column("时间", style="cyan", no_wrap=True)
|
324
|
+
table.add_column("值", justify="right", style="yellow")
|
325
|
+
table.add_column("标签", style="dim")
|
326
|
+
|
327
|
+
# 只显示最近的20条记录
|
328
|
+
display_records = records[-20:] if len(records) > 20 else records
|
329
|
+
|
330
|
+
# 添加数据行
|
331
|
+
from datetime import datetime
|
332
|
+
|
333
|
+
for record in display_records:
|
334
|
+
timestamp = datetime.fromisoformat(record["timestamp"])
|
335
|
+
time_str = timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
336
|
+
value = f"{record['value']:.2f}"
|
337
|
+
tags_str = ", ".join(f"{k}={v}" for k, v in record.get("tags", {}).items())
|
338
|
+
|
339
|
+
table.add_row(time_str, value, tags_str)
|
340
|
+
|
341
|
+
# 显示表格
|
342
|
+
self.console.print(table)
|
343
|
+
|
344
|
+
# 显示元信息
|
345
|
+
info_items = []
|
346
|
+
if unit:
|
347
|
+
info_items.append(f"单位: {unit}")
|
348
|
+
if start_time and end_time:
|
349
|
+
info_items.append(f"时间范围: {start_time} ~ {end_time}")
|
350
|
+
if tags_filter:
|
351
|
+
filter_str = ", ".join([f"{k}={v}" for k, v in tags_filter.items()])
|
352
|
+
info_items.append(f"过滤条件: {filter_str}")
|
353
|
+
|
354
|
+
if info_items:
|
355
|
+
self.console.print(Panel(" | ".join(info_items), style="dim"))
|
356
|
+
|
357
|
+
# 统计信息
|
358
|
+
if len(records) > 0:
|
359
|
+
values = [r["value"] for r in records]
|
360
|
+
stats_info = (
|
361
|
+
f"总记录数: {len(records)} | "
|
362
|
+
f"显示: {len(display_records)} | "
|
363
|
+
f"最小值: {min(values):.2f} | "
|
364
|
+
f"最大值: {max(values):.2f} | "
|
365
|
+
f"平均值: {sum(values)/len(values):.2f}"
|
366
|
+
)
|
367
|
+
self.console.print(f"\n[dim]{stats_info}[/dim]")
|
368
|
+
|
369
|
+
return ""
|
370
|
+
|
371
|
+
def _draw_line(self, canvas: List[List[str]], x1: int, y1: int, x2: int, y2: int):
|
372
|
+
"""在画布上绘制线条"""
|
373
|
+
# 使用Bresenham算法绘制线条
|
374
|
+
dx = abs(x2 - x1)
|
375
|
+
dy = abs(y2 - y1)
|
376
|
+
sx = 1 if x1 < x2 else -1
|
377
|
+
sy = 1 if y1 < y2 else -1
|
378
|
+
err = dx - dy
|
379
|
+
|
380
|
+
x, y = x1, y1
|
381
|
+
|
382
|
+
while True:
|
383
|
+
if 0 <= x < len(canvas[0]) and 0 <= y < len(canvas):
|
384
|
+
if canvas[y][x] == " ":
|
385
|
+
if dx > dy:
|
386
|
+
canvas[y][x] = "─"
|
387
|
+
else:
|
388
|
+
canvas[y][x] = "│"
|
389
|
+
|
390
|
+
if x == x2 and y == y2:
|
391
|
+
break
|
392
|
+
|
393
|
+
e2 = 2 * err
|
394
|
+
if e2 > -dy:
|
395
|
+
err -= dy
|
396
|
+
x += sx
|
397
|
+
if e2 < dx:
|
398
|
+
err += dx
|
399
|
+
y += sy
|
400
|
+
|
401
|
+
def _generate_y_labels(
|
402
|
+
self, min_val: float, max_val: float, height: int
|
403
|
+
) -> Dict[int, str]:
|
404
|
+
"""生成Y轴标签"""
|
405
|
+
labels = {}
|
406
|
+
|
407
|
+
# 在顶部、中部和底部放置标签
|
408
|
+
positions = [0, height // 2, height - 1]
|
409
|
+
values = [max_val, (max_val + min_val) / 2, min_val]
|
410
|
+
|
411
|
+
for pos, val in zip(positions, values):
|
412
|
+
labels[pos] = f"{val:.1f}"
|
413
|
+
|
414
|
+
return labels
|
415
|
+
|
416
|
+
def _generate_x_labels(self, labels: List[str], width: int) -> str:
|
417
|
+
"""生成X轴标签"""
|
418
|
+
if not labels:
|
419
|
+
return ""
|
420
|
+
|
421
|
+
# 简化标签(如果是时间格式)
|
422
|
+
simplified_labels = []
|
423
|
+
for label in labels:
|
424
|
+
if len(label) > 10:
|
425
|
+
# 尝试提取时间部分
|
426
|
+
parts = label.split()
|
427
|
+
if len(parts) >= 2:
|
428
|
+
simplified_labels.append(parts[1][:5]) # 只取时间
|
429
|
+
else:
|
430
|
+
simplified_labels.append(label[:8])
|
431
|
+
else:
|
432
|
+
simplified_labels.append(label)
|
433
|
+
|
434
|
+
# 根据空间决定显示多少标签
|
435
|
+
max_labels = width // 10
|
436
|
+
step = max(1, len(simplified_labels) // max_labels)
|
437
|
+
|
438
|
+
label_line = ""
|
439
|
+
for i in range(0, len(simplified_labels), step):
|
440
|
+
if i < len(simplified_labels):
|
441
|
+
label_line += f"{simplified_labels[i]:>10}"
|
442
|
+
|
443
|
+
return label_line
|
jarvis/jarvis_tools/cli/main.py
CHANGED
@@ -40,25 +40,94 @@ def list_tools(
|
|
40
40
|
|
41
41
|
|
42
42
|
@app.command("stat")
|
43
|
-
def stat_tools(
|
43
|
+
def stat_tools(
|
44
|
+
as_json: bool = typer.Option(False, "--json", help="以JSON格式输出"),
|
45
|
+
last_days: Optional[int] = typer.Option(None, "--days", help="显示最近N天的统计(默认显示所有历史数据)"),
|
46
|
+
format: str = typer.Option("table", "--format", help="显示格式: table, chart, summary")
|
47
|
+
):
|
44
48
|
"""显示工具调用统计信息"""
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
49
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
50
|
+
|
51
|
+
stats_manager = StatsManager()
|
52
|
+
|
53
|
+
if format == "table":
|
54
|
+
registry = ToolRegistry()
|
55
|
+
stats = registry._get_tool_stats()
|
56
|
+
tools = registry.get_all_tools()
|
57
|
+
|
58
|
+
table_data = []
|
59
|
+
for tool in tools:
|
60
|
+
name = tool["name"]
|
61
|
+
count = stats.get(name, 0)
|
62
|
+
if count > 0: # 只显示有调用记录的工具
|
63
|
+
table_data.append([name, count])
|
54
64
|
|
55
|
-
|
65
|
+
table_data.sort(key=lambda x: x[1], reverse=True)
|
56
66
|
|
57
|
-
|
58
|
-
|
67
|
+
if as_json:
|
68
|
+
print(json.dumps(dict(table_data), indent=2))
|
69
|
+
else:
|
70
|
+
time_desc = f"最近{last_days}天" if last_days else "所有历史"
|
71
|
+
PrettyOutput.section(f"工具调用统计 ({time_desc})", OutputType.SYSTEM)
|
72
|
+
if table_data:
|
73
|
+
print(tabulate(table_data, headers=["工具名称", "调用次数"], tablefmt="grid"))
|
74
|
+
print(f"\n总计: {len(table_data)} 个工具被使用,共 {sum(x[1] for x in table_data)} 次调用")
|
75
|
+
else:
|
76
|
+
print("暂无工具调用记录")
|
59
77
|
else:
|
60
|
-
|
61
|
-
|
78
|
+
# 使用 stats 系统的高级功能
|
79
|
+
PrettyOutput.section("工具组统计", OutputType.SYSTEM)
|
80
|
+
# 显示所有标记为 tool 组的指标
|
81
|
+
metrics = stats_manager.list_metrics()
|
82
|
+
tool_metrics = []
|
83
|
+
|
84
|
+
for metric in metrics:
|
85
|
+
# 检查是否是工具组的指标
|
86
|
+
if last_days:
|
87
|
+
stats_data = stats_manager.get_stats(
|
88
|
+
metric_name=metric,
|
89
|
+
last_days=last_days,
|
90
|
+
tags={"group": "tool"}
|
91
|
+
)
|
92
|
+
else:
|
93
|
+
# 获取所有历史数据
|
94
|
+
from datetime import datetime
|
95
|
+
stats_data = stats_manager.get_stats(
|
96
|
+
metric_name=metric,
|
97
|
+
start_time=datetime(2000, 1, 1),
|
98
|
+
end_time=datetime.now(),
|
99
|
+
tags={"group": "tool"}
|
100
|
+
)
|
101
|
+
if stats_data and stats_data.get("records"):
|
102
|
+
tool_metrics.append(metric)
|
103
|
+
|
104
|
+
if tool_metrics:
|
105
|
+
for metric in tool_metrics:
|
106
|
+
if format == "chart":
|
107
|
+
if last_days:
|
108
|
+
stats_manager.plot(metric, last_days=last_days, tags={"group": "tool"})
|
109
|
+
else:
|
110
|
+
from datetime import datetime
|
111
|
+
stats_manager.plot(
|
112
|
+
metric,
|
113
|
+
start_time=datetime(2000, 1, 1),
|
114
|
+
end_time=datetime.now(),
|
115
|
+
tags={"group": "tool"}
|
116
|
+
)
|
117
|
+
elif format == "summary":
|
118
|
+
if last_days:
|
119
|
+
stats_manager.show(metric, last_days=last_days, format="summary", tags={"group": "tool"})
|
120
|
+
else:
|
121
|
+
from datetime import datetime
|
122
|
+
stats_manager.show(
|
123
|
+
metric,
|
124
|
+
start_time=datetime(2000, 1, 1),
|
125
|
+
end_time=datetime.now(),
|
126
|
+
format="summary",
|
127
|
+
tags={"group": "tool"}
|
128
|
+
)
|
129
|
+
else:
|
130
|
+
print("暂无工具调用记录")
|
62
131
|
|
63
132
|
|
64
133
|
@app.command("call")
|
@@ -12,7 +12,9 @@ class generate_new_tool:
|
|
12
12
|
生成并注册新的Jarvis工具。该工具会在用户数据目录下创建新的工具文件,
|
13
13
|
并自动注册到当前的工具注册表中。适用场景:1. 需要创建新的自定义工具;
|
14
14
|
2. 扩展Jarvis功能;3. 自动化重复性操作;4. 封装特定领域的功能。
|
15
|
-
|
15
|
+
重要提示:
|
16
|
+
1. `tool_name` 参数必须与 `tool_code` 中定义的 `name` 属性完全一致。
|
17
|
+
2. 在编写工具代码时,应尽量将工具执行的过程和结果打印出来,方便追踪工具的执行状态。
|
16
18
|
"""
|
17
19
|
|
18
20
|
parameters = {
|
@@ -75,6 +77,25 @@ class generate_new_tool:
|
|
75
77
|
"stderr": f"工具名称 '{tool_name}' 不是有效的Python标识符",
|
76
78
|
}
|
77
79
|
|
80
|
+
# 验证工具代码中的名称是否与tool_name一致
|
81
|
+
import re
|
82
|
+
|
83
|
+
match = re.search(r"^\s*name\s*=\s*[\"'](.+?)[\"']", tool_code, re.MULTILINE)
|
84
|
+
if not match:
|
85
|
+
return {
|
86
|
+
"success": False,
|
87
|
+
"stdout": "",
|
88
|
+
"stderr": "无法在工具代码中找到 'name' 属性。请确保工具类中包含 'name = \"your_tool_name\"'。",
|
89
|
+
}
|
90
|
+
|
91
|
+
code_name = match.group(1)
|
92
|
+
if tool_name != code_name:
|
93
|
+
return {
|
94
|
+
"success": False,
|
95
|
+
"stdout": "",
|
96
|
+
"stderr": f"工具名称不一致:参数 'tool_name' ('{tool_name}') 与代码中的 'name' 属性 ('{code_name}') 必须相同。",
|
97
|
+
}
|
98
|
+
|
78
99
|
# 准备工具目录
|
79
100
|
tools_dir = Path(get_data_dir()) / "tools"
|
80
101
|
tools_dir.mkdir(parents=True, exist_ok=True)
|
jarvis/jarvis_tools/registry.py
CHANGED
@@ -200,25 +200,44 @@ class ToolRegistry(OutputHandlerProtocol):
|
|
200
200
|
|
201
201
|
def _get_tool_stats(self) -> Dict[str, int]:
|
202
202
|
"""从数据目录获取工具调用统计"""
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
203
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
204
|
+
from datetime import datetime, timedelta
|
205
|
+
|
206
|
+
stats_manager = StatsManager()
|
207
|
+
|
208
|
+
# 获取所有工具的统计数据
|
209
|
+
tool_stats = {}
|
210
|
+
tools = self.get_all_tools()
|
211
|
+
|
212
|
+
# 获取所有历史数据(从很早的时间开始)
|
213
|
+
end_time = datetime.now()
|
214
|
+
start_time = datetime(2000, 1, 1) # 使用一个足够早的时间
|
215
|
+
|
216
|
+
for tool in tools:
|
217
|
+
tool_name = tool["name"]
|
218
|
+
# 获取该工具的统计数据
|
219
|
+
stats_data = stats_manager.get_stats(
|
220
|
+
metric_name=tool_name,
|
221
|
+
start_time=start_time,
|
222
|
+
end_time=end_time,
|
223
|
+
tags={"group": "tool"}
|
224
|
+
)
|
225
|
+
|
226
|
+
# 计算总调用次数
|
227
|
+
if stats_data and "records" in stats_data:
|
228
|
+
total_count = sum(record["value"] for record in stats_data["records"])
|
229
|
+
tool_stats[tool_name] = int(total_count)
|
230
|
+
else:
|
231
|
+
tool_stats[tool_name] = 0
|
232
|
+
|
233
|
+
return tool_stats
|
211
234
|
|
212
235
|
def _update_tool_stats(self, name: str) -> None:
|
213
236
|
"""更新工具调用统计"""
|
214
|
-
stats
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
with open(stats_file, "w", encoding="utf-8") as f:
|
219
|
-
yaml.safe_dump(stats, f, allow_unicode=True)
|
220
|
-
except Exception as e:
|
221
|
-
PrettyOutput.print(f"保存工具调用统计失败: {str(e)}", OutputType.WARNING)
|
237
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
238
|
+
|
239
|
+
stats_manager = StatsManager()
|
240
|
+
stats_manager.increment(name, group="tool")
|
222
241
|
|
223
242
|
def use_tools(self, name: List[str]) -> None:
|
224
243
|
"""使用指定工具
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
2
|
+
"""网络搜索工具。"""
|
3
3
|
from typing import Any, Dict
|
4
4
|
|
5
5
|
import requests
|
@@ -17,7 +17,7 @@ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
17
17
|
|
18
18
|
|
19
19
|
class SearchWebTool:
|
20
|
-
"""
|
20
|
+
"""处理网络搜索的类。"""
|
21
21
|
|
22
22
|
name = "search_web"
|
23
23
|
description = "搜索互联网上的信息"
|
@@ -28,7 +28,7 @@ class SearchWebTool:
|
|
28
28
|
|
29
29
|
def _search_with_ddgs(self, query: str, agent: Agent) -> Dict[str, Any]:
|
30
30
|
# pylint: disable=too-many-locals, broad-except
|
31
|
-
"""
|
31
|
+
"""执行网络搜索、抓取内容并总结结果。"""
|
32
32
|
try:
|
33
33
|
PrettyOutput.print("▶️ 使用 DuckDuckGo 开始网页搜索...", OutputType.INFO)
|
34
34
|
results = list(DDGS().text(query, max_results=50))
|