jarvis-ai-assistant 0.2.4__py3-none-any.whl → 0.2.6__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.
@@ -5,9 +5,10 @@
5
5
  """
6
6
 
7
7
  import os
8
-
8
+ import io
9
9
  from typing import Dict, List, Optional, Any
10
10
  from collections import OrderedDict
11
+ import plotext as plt
11
12
  from rich.console import Console
12
13
  from rich.table import Table
13
14
  from rich.panel import Panel
@@ -51,106 +52,50 @@ class StatsVisualizer:
51
52
  show_values: bool = True,
52
53
  ) -> str:
53
54
  """
54
- 绘制折线图
55
-
56
- Args:
57
- data: 数据字典,键为时间标签,值为数值
58
- title: 图表标题
59
- unit: 单位
60
- show_values: 是否显示数值
61
-
62
- Returns:
63
- 图表字符串
55
+ 使用 plotext 绘制折线图
64
56
  """
65
57
  if not data:
66
58
  return "无数据可显示"
67
59
 
68
- # 排序数据
69
60
  sorted_data = OrderedDict(sorted(data.items()))
70
-
71
- # 提取值和标签
72
61
  labels = list(sorted_data.keys())
73
62
  values = list(sorted_data.values())
74
63
 
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
- # 标题
64
+ plt.clf()
65
+ plt.plotsize(self.width, self.height)
66
+ plt.plot(values)
67
+ plt.xticks(range(len(labels)), labels)
127
68
  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)
69
+ plt.title(title)
70
+ if unit:
71
+ plt.ylabel(unit)
138
72
 
139
- # X轴标签
140
- x_labels = self._generate_x_labels(labels, chart_width)
141
- output.append(" " * 9 + x_labels)
73
+ chart = plt.build()
142
74
 
143
- # 单位
144
- if unit:
145
- output.append(f"\n单位: {unit}")
75
+ if show_values and values:
76
+ min_val = min(values)
77
+ max_val = max(values)
78
+ avg_val = sum(values) / len(values)
79
+ stats_info_text = (
80
+ f"最小值: {min_val:.2f}, 最大值: {max_val:.2f}, 平均值: {avg_val:.2f}"
81
+ )
146
82
 
147
- # 统计信息
148
- if show_values:
149
- output.append(
150
- f"\n最小值: {min_val:.2f}, 最大值: {max_val:.2f}, 平均值: {sum(values)/len(values):.2f}"
83
+ # 使用StringIO捕获Panel输出
84
+ string_io = io.StringIO()
85
+ temp_console = Console(file=string_io, width=self.width)
86
+ temp_console.print(
87
+ Panel(
88
+ stats_info_text,
89
+ title="[bold]数据统计[/bold]",
90
+ expand=False,
91
+ style="dim",
92
+ border_style="blue",
93
+ )
151
94
  )
95
+ stats_panel_str = string_io.getvalue()
152
96
 
153
- return "\n".join(output)
97
+ return chart + "\n" + stats_panel_str.strip()
98
+ return chart
154
99
 
155
100
  def plot_bar_chart(
156
101
  self,
@@ -160,75 +105,27 @@ class StatsVisualizer:
160
105
  horizontal: bool = False,
161
106
  ) -> str:
162
107
  """
163
- 绘制柱状图
164
-
165
- Args:
166
- data: 数据字典
167
- title: 图表标题
168
- unit: 单位
169
- horizontal: 是否横向
170
-
171
- Returns:
172
- 图表字符串
108
+ 使用 plotext 绘制柱状图
173
109
  """
174
110
  if not data:
175
111
  return "无数据可显示"
176
112
 
177
- output = []
178
-
179
- # 标题
180
- if title:
181
- output.append(f"\n{title.center(self.width)}")
182
- output.append("=" * self.width)
113
+ labels = list(data.keys())
114
+ values = list(data.values())
183
115
 
184
- # 找出最大值用于缩放
185
- max_value = max(data.values()) if data.values() else 1
116
+ plt.clf()
117
+ plt.plotsize(self.width, self.height)
186
118
 
187
119
  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}")
120
+ plt.bar(labels, values, orientation="horizontal")
196
121
  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
- # 单位
122
+ plt.bar(labels, values)
123
+ if title:
124
+ plt.title(title)
228
125
  if unit:
229
- output.append(f"\n单位: {unit}")
126
+ plt.ylabel(unit)
230
127
 
231
- return "\n".join(output)
128
+ return plt.build()
232
129
 
233
130
  def show_summary(
234
131
  self,
@@ -346,98 +243,40 @@ class StatsVisualizer:
346
243
  if unit:
347
244
  info_items.append(f"单位: {unit}")
348
245
  if start_time and end_time:
349
- info_items.append(f"时间范围: {start_time} ~ {end_time}")
246
+ info_items.append(f"时间范围: [cyan]{start_time}[/] ~ [cyan]{end_time}[/]")
350
247
  if tags_filter:
351
248
  filter_str = ", ".join([f"{k}={v}" for k, v in tags_filter.items()])
352
249
  info_items.append(f"过滤条件: {filter_str}")
353
250
 
354
251
  if info_items:
355
- self.console.print(Panel(" | ".join(info_items), style="dim"))
252
+ self.console.print(
253
+ Panel(
254
+ " | ".join(info_items),
255
+ title="[bold]查询详情[/bold]",
256
+ expand=False,
257
+ style="dim",
258
+ border_style="green",
259
+ )
260
+ )
356
261
 
357
262
  # 统计信息
358
263
  if len(records) > 0:
359
264
  values = [r["value"] for r in records]
360
- stats_info = (
265
+ stats_info_text = (
361
266
  f"总记录数: {len(records)} | "
362
267
  f"显示: {len(display_records)} | "
363
268
  f"最小值: {min(values):.2f} | "
364
269
  f"最大值: {max(values):.2f} | "
365
270
  f"平均值: {sum(values)/len(values):.2f}"
366
271
  )
367
- self.console.print(f"\n[dim]{stats_info}[/dim]")
272
+ self.console.print(
273
+ Panel(
274
+ stats_info_text,
275
+ title="[bold]数据统计[/bold]",
276
+ expand=False,
277
+ style="dim",
278
+ border_style="blue",
279
+ )
280
+ )
368
281
 
369
282
  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
@@ -48,8 +48,6 @@ def stat_tools(
48
48
  """显示工具调用统计信息"""
49
49
  from jarvis.jarvis_stats.stats import StatsManager
50
50
 
51
- stats_manager = StatsManager()
52
-
53
51
  if format == "table":
54
52
  registry = ToolRegistry()
55
53
  stats = registry._get_tool_stats()
@@ -78,13 +76,13 @@ def stat_tools(
78
76
  # 使用 stats 系统的高级功能
79
77
  PrettyOutput.section("工具组统计", OutputType.SYSTEM)
80
78
  # 显示所有标记为 tool 组的指标
81
- metrics = stats_manager.list_metrics()
79
+ metrics = StatsManager.list_metrics()
82
80
  tool_metrics = []
83
81
 
84
82
  for metric in metrics:
85
83
  # 检查是否是工具组的指标
86
84
  if last_days:
87
- stats_data = stats_manager.get_stats(
85
+ stats_data = StatsManager.get_stats(
88
86
  metric_name=metric,
89
87
  last_days=last_days,
90
88
  tags={"group": "tool"}
@@ -92,7 +90,7 @@ def stat_tools(
92
90
  else:
93
91
  # 获取所有历史数据
94
92
  from datetime import datetime
95
- stats_data = stats_manager.get_stats(
93
+ stats_data = StatsManager.get_stats(
96
94
  metric_name=metric,
97
95
  start_time=datetime(2000, 1, 1),
98
96
  end_time=datetime.now(),
@@ -105,10 +103,10 @@ def stat_tools(
105
103
  for metric in tool_metrics:
106
104
  if format == "chart":
107
105
  if last_days:
108
- stats_manager.plot(metric, last_days=last_days, tags={"group": "tool"})
106
+ StatsManager.plot(metric, last_days=last_days, tags={"group": "tool"})
109
107
  else:
110
108
  from datetime import datetime
111
- stats_manager.plot(
109
+ StatsManager.plot(
112
110
  metric,
113
111
  start_time=datetime(2000, 1, 1),
114
112
  end_time=datetime.now(),
@@ -116,10 +114,10 @@ def stat_tools(
116
114
  )
117
115
  elif format == "summary":
118
116
  if last_days:
119
- stats_manager.show(metric, last_days=last_days, format="summary", tags={"group": "tool"})
117
+ StatsManager.show(metric, last_days=last_days, format="summary", tags={"group": "tool"})
120
118
  else:
121
119
  from datetime import datetime
122
- stats_manager.show(
120
+ StatsManager.show(
123
121
  metric,
124
122
  start_time=datetime(2000, 1, 1),
125
123
  end_time=datetime.now(),
@@ -203,8 +203,6 @@ class ToolRegistry(OutputHandlerProtocol):
203
203
  from jarvis.jarvis_stats.stats import StatsManager
204
204
  from datetime import datetime, timedelta
205
205
 
206
- stats_manager = StatsManager()
207
-
208
206
  # 获取所有工具的统计数据
209
207
  tool_stats = {}
210
208
  tools = self.get_all_tools()
@@ -216,7 +214,7 @@ class ToolRegistry(OutputHandlerProtocol):
216
214
  for tool in tools:
217
215
  tool_name = tool["name"]
218
216
  # 获取该工具的统计数据
219
- stats_data = stats_manager.get_stats(
217
+ stats_data = StatsManager.get_stats(
220
218
  metric_name=tool_name,
221
219
  start_time=start_time,
222
220
  end_time=end_time,
@@ -236,8 +234,7 @@ class ToolRegistry(OutputHandlerProtocol):
236
234
  """更新工具调用统计"""
237
235
  from jarvis.jarvis_stats.stats import StatsManager
238
236
 
239
- stats_manager = StatsManager()
240
- stats_manager.increment(name, group="tool")
237
+ StatsManager.increment(name, group="tool")
241
238
 
242
239
  def use_tools(self, name: List[str]) -> None:
243
240
  """使用指定工具
@@ -0,0 +1,206 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ import random
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from jarvis.jarvis_utils.config import get_data_dir
8
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
9
+ from jarvis.jarvis_utils.globals import get_short_term_memories
10
+
11
+
12
+ class RetrieveMemoryTool:
13
+ """检索记忆工具,用于从长短期记忆系统中检索信息"""
14
+
15
+ name = "retrieve_memory"
16
+ description = """从长短期记忆系统中检索信息。
17
+
18
+ 支持的记忆类型:
19
+ - project_long_term: 项目长期记忆(与当前项目相关的信息)
20
+ - global_long_term: 全局长期记忆(通用的信息、用户喜好、知识、方法等)
21
+ - short_term: 短期记忆(当前任务相关的信息)
22
+ - all: 从所有类型中检索
23
+
24
+ 可以通过标签过滤检索结果,支持多个标签(满足任一标签即可)
25
+ """
26
+
27
+ parameters = {
28
+ "type": "object",
29
+ "properties": {
30
+ "memory_types": {
31
+ "type": "array",
32
+ "items": {
33
+ "type": "string",
34
+ "enum": [
35
+ "project_long_term",
36
+ "global_long_term",
37
+ "short_term",
38
+ "all",
39
+ ],
40
+ },
41
+ "description": "要检索的记忆类型列表,如果包含'all'则检索所有类型",
42
+ },
43
+ "tags": {
44
+ "type": "array",
45
+ "items": {"type": "string"},
46
+ "description": "用于过滤的标签列表(可选)",
47
+ },
48
+ "limit": {
49
+ "type": "integer",
50
+ "description": "返回结果的最大数量(可选,默认返回所有)",
51
+ "minimum": 1,
52
+ },
53
+ },
54
+ "required": ["memory_types"],
55
+ }
56
+
57
+ def __init__(self):
58
+ """初始化检索记忆工具"""
59
+ self.project_memory_dir = Path(".jarvis/memory")
60
+ self.global_memory_dir = Path(get_data_dir()) / "memory"
61
+
62
+ def _get_memory_dir(self, memory_type: str) -> Path:
63
+ """根据记忆类型获取存储目录"""
64
+ if memory_type == "project_long_term":
65
+ return self.project_memory_dir
66
+ elif memory_type in ["global_long_term", "short_term"]:
67
+ return self.global_memory_dir / memory_type
68
+ else:
69
+ raise ValueError(f"未知的记忆类型: {memory_type}")
70
+
71
+ def _retrieve_from_type(
72
+ self, memory_type: str, tags: Optional[List[str]] = None
73
+ ) -> List[Dict[str, Any]]:
74
+ """从指定类型中检索记忆"""
75
+ memories: List[Dict[str, Any]] = []
76
+
77
+ if memory_type == "short_term":
78
+ # 从全局变量获取短期记忆
79
+ memories = get_short_term_memories(tags)
80
+ else:
81
+ # 从文件系统获取长期记忆
82
+ memory_dir = self._get_memory_dir(memory_type)
83
+
84
+ if not memory_dir.exists():
85
+ return memories
86
+
87
+ # 遍历记忆文件
88
+ for memory_file in memory_dir.glob("*.json"):
89
+ try:
90
+ with open(memory_file, "r", encoding="utf-8") as f:
91
+ memory_data = json.load(f)
92
+
93
+ # 如果指定了标签,检查是否匹配
94
+ if tags:
95
+ memory_tags = memory_data.get("tags", [])
96
+ if not any(tag in memory_tags for tag in tags):
97
+ continue
98
+
99
+ memories.append(memory_data)
100
+ except Exception as e:
101
+ PrettyOutput.print(
102
+ f"读取记忆文件 {memory_file} 失败: {str(e)}", OutputType.WARNING
103
+ )
104
+
105
+ return memories
106
+
107
+ def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
108
+ """执行检索记忆操作"""
109
+ try:
110
+ memory_types = args.get("memory_types", [])
111
+ tags = args.get("tags", [])
112
+ limit = args.get("limit", None)
113
+
114
+ # 确定要检索的记忆类型
115
+ if "all" in memory_types:
116
+ types_to_search = [
117
+ "project_long_term",
118
+ "global_long_term",
119
+ "short_term",
120
+ ]
121
+ else:
122
+ types_to_search = memory_types
123
+
124
+ # 从各个类型中检索记忆
125
+ all_memories = []
126
+ for memory_type in types_to_search:
127
+ memories = self._retrieve_from_type(memory_type, tags)
128
+ all_memories.extend(memories)
129
+
130
+ # 按创建时间排序(最新的在前)
131
+ all_memories.sort(key=lambda x: x.get("created_at", ""), reverse=True)
132
+
133
+ # 限制最多返回50条记忆,随机选取
134
+ if len(all_memories) > 50:
135
+ all_memories = random.sample(all_memories, 50)
136
+ # 重新排序,保持时间顺序
137
+ all_memories.sort(key=lambda x: x.get("created_at", ""), reverse=True)
138
+
139
+ # 如果指定了限制,只返回前N个
140
+ if limit:
141
+ all_memories = all_memories[:limit]
142
+
143
+ # 打印结果摘要
144
+ PrettyOutput.print(f"检索到 {len(all_memories)} 条记忆", OutputType.INFO)
145
+
146
+ if tags:
147
+ PrettyOutput.print(f"使用标签过滤: {', '.join(tags)}", OutputType.INFO)
148
+
149
+ # 格式化为Markdown输出
150
+ markdown_output = f"# 记忆检索结果\n\n"
151
+ markdown_output += f"**检索到 {len(all_memories)} 条记忆**\n\n"
152
+
153
+ if tags:
154
+ markdown_output += f"**使用标签过滤**: {', '.join(tags)}\n\n"
155
+
156
+ markdown_output += f"**记忆类型**: {', '.join(types_to_search)}\n\n"
157
+
158
+ markdown_output += "---\n\n"
159
+
160
+ # 输出所有记忆
161
+ for i, memory in enumerate(all_memories):
162
+ markdown_output += f"## {i+1}. {memory.get('id', '未知ID')}\n\n"
163
+ markdown_output += f"**类型**: {memory.get('type', '未知类型')}\n\n"
164
+ markdown_output += f"**标签**: {', '.join(memory.get('tags', []))}\n\n"
165
+ markdown_output += f"**创建时间**: {memory.get('created_at', '未知时间')}\n\n"
166
+
167
+ # 内容部分
168
+ content = memory.get('content', '')
169
+ if content:
170
+ markdown_output += f"**内容**:\n\n{content}\n\n"
171
+
172
+ # 如果有额外的元数据
173
+ metadata = {k: v for k, v in memory.items()
174
+ if k not in ['id', 'type', 'tags', 'created_at', 'content']}
175
+ if metadata:
176
+ markdown_output += f"**其他信息**:\n"
177
+ for key, value in metadata.items():
178
+ markdown_output += f"- {key}: {value}\n"
179
+ markdown_output += "\n"
180
+
181
+ markdown_output += "---\n\n"
182
+
183
+ # 如果记忆较多,在终端显示摘要
184
+ if len(all_memories) > 5:
185
+ PrettyOutput.print(f"记忆较多,仅显示前5条摘要:", OutputType.INFO)
186
+ for i, memory in enumerate(all_memories[:5]):
187
+ content_preview = memory.get("content", "")[:100]
188
+ if len(memory.get("content", "")) > 100:
189
+ content_preview += "..."
190
+ PrettyOutput.print(
191
+ f"{i+1}. [{memory.get('type')}] {memory.get('id')}\n"
192
+ f" 标签: {', '.join(memory.get('tags', []))}\n"
193
+ f" 内容: {content_preview}",
194
+ OutputType.INFO,
195
+ )
196
+
197
+ return {
198
+ "success": True,
199
+ "stdout": markdown_output,
200
+ "stderr": "",
201
+ }
202
+
203
+ except Exception as e:
204
+ error_msg = f"检索记忆失败: {str(e)}"
205
+ PrettyOutput.print(error_msg, OutputType.ERROR)
206
+ return {"success": False, "stdout": "", "stderr": error_msg}