jarvis-ai-assistant 0.2.4__py3-none-any.whl → 0.2.5__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.
@@ -14,18 +14,37 @@ from jarvis.jarvis_stats.visualizer import StatsVisualizer
14
14
  class StatsManager:
15
15
  """统计管理器"""
16
16
 
17
+ # 类级别的存储和可视化器实例
18
+ _storage: Optional[StatsStorage] = None
19
+ _visualizer: Optional[StatsVisualizer] = None
20
+
21
+ @classmethod
22
+ def _get_storage(cls) -> StatsStorage:
23
+ """获取存储实例"""
24
+ if cls._storage is None:
25
+ cls._storage = StatsStorage()
26
+ return cls._storage
27
+
28
+ @classmethod
29
+ def _get_visualizer(cls) -> StatsVisualizer:
30
+ """获取可视化器实例"""
31
+ if cls._visualizer is None:
32
+ cls._visualizer = StatsVisualizer()
33
+ return cls._visualizer
34
+
17
35
  def __init__(self, storage_dir: Optional[str] = None):
18
36
  """
19
- 初始化统计管理器
37
+ 初始化统计管理器(保留以兼容旧代码)
20
38
 
21
39
  Args:
22
40
  storage_dir: 存储目录路径
23
41
  """
24
- self.storage = StatsStorage(storage_dir)
25
- self.visualizer = StatsVisualizer()
42
+ # 如果提供了特定的存储目录,则重新初始化存储
43
+ if storage_dir is not None:
44
+ StatsManager._storage = StatsStorage(storage_dir)
26
45
 
46
+ @staticmethod
27
47
  def increment(
28
- self,
29
48
  metric_name: str,
30
49
  amount: Union[int, float] = 1,
31
50
  tags: Optional[Dict[str, str]] = None,
@@ -43,11 +62,10 @@ class StatsManager:
43
62
  unit: 计量单位,默认为 "count"
44
63
 
45
64
  Examples:
46
- >>> stats = StatsManager()
47
- >>> stats.increment("page_views")
48
- >>> stats.increment("downloads", 5)
49
- >>> stats.increment("response_time", 0.123, unit="seconds")
50
- >>> stats.increment("execute_script", 1, group="tool")
65
+ >>> StatsManager.increment("page_views")
66
+ >>> StatsManager.increment("downloads", 5)
67
+ >>> StatsManager.increment("response_time", 0.123, unit="seconds")
68
+ >>> StatsManager.increment("execute_script", 1, group="tool")
51
69
  """
52
70
  # 如果指定了分组,自动添加到 tags 中
53
71
  if group:
@@ -55,7 +73,8 @@ class StatsManager:
55
73
  tags = {}
56
74
  tags["group"] = group
57
75
 
58
- self.storage.add_metric(
76
+ storage = StatsManager._get_storage()
77
+ storage.add_metric(
59
78
  metric_name=metric_name,
60
79
  value=float(amount),
61
80
  unit=unit,
@@ -63,17 +82,19 @@ class StatsManager:
63
82
  tags=tags,
64
83
  )
65
84
 
66
- def list_metrics(self) -> List[str]:
85
+ @staticmethod
86
+ def list_metrics() -> List[str]:
67
87
  """
68
88
  列出所有指标
69
89
 
70
90
  Returns:
71
91
  指标名称列表
72
92
  """
73
- return self.storage.list_metrics()
93
+ storage = StatsManager._get_storage()
94
+ return storage.list_metrics()
74
95
 
96
+ @staticmethod
75
97
  def show(
76
- self,
77
98
  metric_name: Optional[str] = None,
78
99
  last_hours: Optional[int] = None,
79
100
  last_days: Optional[int] = None,
@@ -97,10 +118,9 @@ class StatsManager:
97
118
  tags: 过滤标签
98
119
 
99
120
  Examples:
100
- >>> stats = StatsManager()
101
- >>> stats.show() # 显示所有指标摘要
102
- >>> stats.show("api_calls", last_hours=24) # 显示最近24小时
103
- >>> stats.show("response_time", last_days=7, format="chart") # 图表显示
121
+ >>> StatsManager.show() # 显示所有指标摘要
122
+ >>> StatsManager.show("api_calls", last_hours=24) # 显示最近24小时
123
+ >>> StatsManager.show("response_time", last_days=7, format="chart") # 图表显示
104
124
  """
105
125
  # 处理时间范围
106
126
  if end_time is None:
@@ -116,18 +136,22 @@ class StatsManager:
116
136
 
117
137
  if metric_name is None:
118
138
  # 显示所有指标摘要
119
- self._show_metrics_summary()
139
+ StatsManager._show_metrics_summary(start_time, end_time, tags)
120
140
  else:
121
141
  # 根据格式显示数据
122
142
  if format == "chart":
123
- self._show_chart(metric_name, start_time, end_time, aggregation, tags)
143
+ StatsManager._show_chart(
144
+ metric_name, start_time, end_time, aggregation, tags
145
+ )
124
146
  elif format == "summary":
125
- self._show_summary(metric_name, start_time, end_time, aggregation, tags)
147
+ StatsManager._show_summary(
148
+ metric_name, start_time, end_time, aggregation, tags
149
+ )
126
150
  else:
127
- self._show_table(metric_name, start_time, end_time, tags)
151
+ StatsManager._show_table(metric_name, start_time, end_time, tags)
128
152
 
153
+ @staticmethod
129
154
  def plot(
130
- self,
131
155
  metric_name: Optional[str] = None,
132
156
  last_hours: Optional[int] = None,
133
157
  last_days: Optional[int] = None,
@@ -153,9 +177,8 @@ class StatsManager:
153
177
  height: 图表高度
154
178
 
155
179
  Examples:
156
- >>> stats = StatsManager()
157
- >>> stats.plot("response_time", last_hours=24)
158
- >>> stats.plot(tags={"service": "api"}, last_days=7)
180
+ >>> StatsManager.plot("response_time", last_hours=24)
181
+ >>> StatsManager.plot(tags={"service": "api"}, last_days=7)
159
182
  """
160
183
  # 处理时间范围
161
184
  if end_time is None:
@@ -171,17 +194,17 @@ class StatsManager:
171
194
 
172
195
  # 如果指定了metric_name,显示单个图表
173
196
  if metric_name:
174
- self._show_chart(
197
+ StatsManager._show_chart(
175
198
  metric_name, start_time, end_time, aggregation, tags, width, height
176
199
  )
177
200
  else:
178
201
  # 如果没有指定metric_name,根据标签过滤获取所有匹配的指标
179
- self._show_multiple_charts(
202
+ StatsManager._show_multiple_charts(
180
203
  start_time, end_time, aggregation, tags, width, height
181
204
  )
182
205
 
206
+ @staticmethod
183
207
  def get_stats(
184
- self,
185
208
  metric_name: str,
186
209
  last_hours: Optional[int] = None,
187
210
  last_days: Optional[int] = None,
@@ -217,14 +240,15 @@ class StatsManager:
217
240
  else:
218
241
  start_time = end_time - timedelta(days=7)
219
242
 
243
+ storage = StatsManager._get_storage()
220
244
  if aggregation:
221
245
  # 返回聚合数据
222
- return self.storage.aggregate_metrics(
246
+ return storage.aggregate_metrics(
223
247
  metric_name, start_time, end_time, aggregation, tags
224
248
  )
225
249
  else:
226
250
  # 返回原始数据
227
- records = self.storage.get_metrics(metric_name, start_time, end_time, tags)
251
+ records = storage.get_metrics(metric_name, start_time, end_time, tags)
228
252
  return {
229
253
  "metric": metric_name,
230
254
  "records": records,
@@ -233,28 +257,56 @@ class StatsManager:
233
257
  "end_time": end_time.isoformat(),
234
258
  }
235
259
 
236
- def clean_old_data(self, days_to_keep: int = 30):
260
+ @staticmethod
261
+ def clean_old_data(days_to_keep: int = 30):
237
262
  """
238
263
  清理旧数据
239
264
 
240
265
  Args:
241
266
  days_to_keep: 保留最近N天的数据
242
267
  """
243
- self.storage.delete_old_data(days_to_keep)
268
+ storage = StatsManager._get_storage()
269
+ storage.delete_old_data(days_to_keep)
244
270
  print(f"已清理 {days_to_keep} 天前的数据")
245
271
 
246
- def _show_metrics_summary(self):
272
+ @staticmethod
273
+ def remove_metric(metric_name: str) -> bool:
274
+ """
275
+ 删除指定的指标及其所有数据
276
+
277
+ Args:
278
+ metric_name: 要删除的指标名称
279
+
280
+ Returns:
281
+ True 如果成功删除,False 如果指标不存在
282
+ """
283
+ storage = StatsManager._get_storage()
284
+ return storage.delete_metric(metric_name)
285
+
286
+ @staticmethod
287
+ def _show_metrics_summary(
288
+ start_time: Optional[datetime] = None,
289
+ end_time: Optional[datetime] = None,
290
+ tags: Optional[Dict[str, str]] = None,
291
+ ):
247
292
  """显示所有指标摘要"""
248
293
  from rich.console import Console
249
294
  from rich.table import Table
250
295
 
251
296
  console = Console()
252
- metrics = self.storage.list_metrics()
297
+ storage = StatsManager._get_storage()
298
+ metrics = storage.list_metrics()
253
299
 
254
300
  if not metrics:
255
301
  console.print("[yellow]没有找到任何统计指标[/yellow]")
256
302
  return
257
303
 
304
+ # 如果没有指定时间范围,使用默认值
305
+ if end_time is None:
306
+ end_time = datetime.now()
307
+ if start_time is None:
308
+ start_time = end_time - timedelta(days=7)
309
+
258
310
  # 创建表格
259
311
  table = Table(title="统计指标摘要")
260
312
  table.add_column("指标名称", style="cyan")
@@ -262,12 +314,20 @@ class StatsManager:
262
314
  table.add_column("最后更新", style="yellow")
263
315
  table.add_column("7天数据点", style="magenta")
264
316
 
265
- # 获取每个指标的信息
266
- end_time = datetime.now()
267
- start_time = end_time - timedelta(days=7)
268
-
317
+ # 过滤满足标签条件的指标
318
+ displayed_count = 0
269
319
  for metric in metrics:
270
- info = self.storage.get_metric_info(metric)
320
+ # 获取该指标的记录
321
+ records = storage.get_metrics(metric, start_time, end_time, tags)
322
+
323
+ # 如果指定了标签过滤,但没有匹配的记录,跳过该指标
324
+ if tags and len(records) == 0:
325
+ continue
326
+
327
+ info = storage.get_metric_info(metric)
328
+ unit = "-"
329
+ last_updated = "-"
330
+
271
331
  if info:
272
332
  unit = info.get("unit", "-")
273
333
  last_updated = info.get("last_updated", "-")
@@ -280,31 +340,38 @@ class StatsManager:
280
340
  except:
281
341
  pass
282
342
 
283
- # 获取数据点数
284
- records = self.storage.get_metrics(metric, start_time, end_time)
285
- count = len(records)
343
+ count = len(records)
344
+ table.add_row(metric, unit, last_updated, str(count))
345
+ displayed_count += 1
286
346
 
287
- table.add_row(metric, unit, last_updated, str(count))
288
-
289
- console.print(table)
290
- console.print(f"\n[green]总计: {len(metrics)} 个指标[/green]")
347
+ if displayed_count == 0:
348
+ console.print("[yellow]没有找到符合条件的指标[/yellow]")
349
+ if tags:
350
+ console.print(f"过滤条件: {tags}")
351
+ else:
352
+ console.print(table)
353
+ console.print(f"\n[green]总计: {displayed_count} 个指标[/green]")
354
+ if tags:
355
+ console.print(f"过滤条件: {tags}")
291
356
 
357
+ @staticmethod
292
358
  def _show_table(
293
- self,
294
359
  metric_name: str,
295
360
  start_time: datetime,
296
361
  end_time: datetime,
297
362
  tags: Optional[Dict[str, str]],
298
363
  ):
299
364
  """以表格形式显示数据"""
300
- records = self.storage.get_metrics(metric_name, start_time, end_time, tags)
365
+ storage = StatsManager._get_storage()
366
+ visualizer = StatsManager._get_visualizer()
367
+ records = storage.get_metrics(metric_name, start_time, end_time, tags)
301
368
 
302
369
  # 获取指标信息
303
- info = self.storage.get_metric_info(metric_name)
370
+ info = storage.get_metric_info(metric_name)
304
371
  unit = info.get("unit", "") if info else ""
305
372
 
306
373
  # 使用visualizer显示表格
307
- self.visualizer.show_table(
374
+ visualizer.show_table(
308
375
  records=records,
309
376
  metric_name=metric_name,
310
377
  unit=unit,
@@ -313,8 +380,8 @@ class StatsManager:
313
380
  tags_filter=tags,
314
381
  )
315
382
 
383
+ @staticmethod
316
384
  def _show_chart(
317
- self,
318
385
  metric_name: str,
319
386
  start_time: datetime,
320
387
  end_time: datetime,
@@ -324,8 +391,11 @@ class StatsManager:
324
391
  height: Optional[int] = None,
325
392
  ):
326
393
  """显示图表"""
394
+ storage = StatsManager._get_storage()
395
+ visualizer = StatsManager._get_visualizer()
396
+
327
397
  # 获取聚合数据
328
- aggregated = self.storage.aggregate_metrics(
398
+ aggregated = storage.aggregate_metrics(
329
399
  metric_name, start_time, end_time, aggregation, tags
330
400
  )
331
401
 
@@ -334,19 +404,30 @@ class StatsManager:
334
404
  return
335
405
 
336
406
  # 获取指标信息
337
- info = self.storage.get_metric_info(metric_name)
407
+ info = storage.get_metric_info(metric_name)
338
408
  unit = info.get("unit", "") if info else ""
339
409
 
340
410
  # 准备数据
341
- data = {k: v["avg"] for k, v in aggregated.items()}
411
+ first_item = next(iter(aggregated.values()), None)
412
+ is_simple_count = (
413
+ first_item
414
+ and first_item.get("min") == 1
415
+ and first_item.get("max") == 1
416
+ and first_item.get("avg") == 1
417
+ )
342
418
 
343
- # 设置可视化器尺寸
419
+ if unit == "count" or is_simple_count:
420
+ # 对于计数类指标,使用总和更有意义
421
+ data = {k: v["sum"] for k, v in aggregated.items()}
422
+ else:
423
+ # 对于其他指标(如耗时),使用平均值
424
+ data = {k: v["avg"] for k, v in aggregated.items()} # 设置可视化器尺寸
344
425
  if width or height:
345
- self.visualizer.width = width or self.visualizer.width
346
- self.visualizer.height = height or self.visualizer.height
426
+ visualizer.width = width or visualizer.width
427
+ visualizer.height = height or visualizer.height
347
428
 
348
429
  # 绘制图表
349
- chart = self.visualizer.plot_line_chart(
430
+ chart = visualizer.plot_line_chart(
350
431
  data=data,
351
432
  title=f"{metric_name} - {aggregation}聚合",
352
433
  unit=unit,
@@ -356,12 +437,22 @@ class StatsManager:
356
437
  print(chart)
357
438
 
358
439
  # 显示时间范围
359
- print(
360
- f"\n时间范围: {start_time.strftime('%Y-%m-%d %H:%M')} ~ {end_time.strftime('%Y-%m-%d %H:%M')}"
440
+ from rich.panel import Panel
441
+ from rich.console import Console
442
+
443
+ console = Console()
444
+ console.print(
445
+ Panel(
446
+ f"[cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/] ~ [cyan]{end_time.strftime('%Y-%m-%d %H:%M')}[/]",
447
+ title="[bold]时间范围[/bold]",
448
+ expand=False,
449
+ style="dim",
450
+ border_style="green",
451
+ )
361
452
  )
362
453
 
454
+ @staticmethod
363
455
  def _show_multiple_charts(
364
- self,
365
456
  start_time: datetime,
366
457
  end_time: datetime,
367
458
  aggregation: str,
@@ -373,15 +464,16 @@ class StatsManager:
373
464
  from rich.console import Console
374
465
 
375
466
  console = Console()
467
+ storage = StatsManager._get_storage()
376
468
 
377
469
  # 获取所有指标
378
- all_metrics = self.list_metrics()
470
+ all_metrics = StatsManager.list_metrics()
379
471
 
380
472
  # 根据标签过滤指标
381
473
  matched_metrics = []
382
474
  for metric in all_metrics:
383
475
  # 获取该指标在时间范围内的数据
384
- records = self.storage.get_metrics(metric, start_time, end_time, tags)
476
+ records = storage.get_metrics(metric, start_time, end_time, tags)
385
477
  if records: # 如果有匹配标签的数据
386
478
  matched_metrics.append(metric)
387
479
 
@@ -396,12 +488,12 @@ class StatsManager:
396
488
  if i > 0:
397
489
  console.print("\n" + "=" * 80 + "\n") # 分隔符
398
490
 
399
- self._show_chart(
491
+ StatsManager._show_chart(
400
492
  metric, start_time, end_time, aggregation, tags, width, height
401
493
  )
402
494
 
495
+ @staticmethod
403
496
  def _show_summary(
404
- self,
405
497
  metric_name: str,
406
498
  start_time: datetime,
407
499
  end_time: datetime,
@@ -409,8 +501,11 @@ class StatsManager:
409
501
  tags: Optional[Dict[str, str]],
410
502
  ):
411
503
  """显示汇总信息"""
504
+ storage = StatsManager._get_storage()
505
+ visualizer = StatsManager._get_visualizer()
506
+
412
507
  # 获取聚合数据
413
- aggregated = self.storage.aggregate_metrics(
508
+ aggregated = storage.aggregate_metrics(
414
509
  metric_name, start_time, end_time, aggregation, tags
415
510
  )
416
511
 
@@ -419,15 +514,25 @@ class StatsManager:
419
514
  return
420
515
 
421
516
  # 获取指标信息
422
- info = self.storage.get_metric_info(metric_name)
517
+ info = storage.get_metric_info(metric_name)
423
518
  unit = info.get("unit", "") if info else ""
424
519
 
425
520
  # 显示汇总
426
- summary = self.visualizer.show_summary(aggregated, metric_name, unit, tags)
521
+ summary = visualizer.show_summary(aggregated, metric_name, unit, tags)
427
522
  if summary: # 如果返回了内容才打印(兼容性)
428
523
  print(summary)
429
524
 
430
525
  # 显示时间范围
431
- print(
432
- f"\n时间范围: {start_time.strftime('%Y-%m-%d %H:%M')} ~ {end_time.strftime('%Y-%m-%d %H:%M')}"
526
+ from rich.panel import Panel
527
+ from rich.console import Console
528
+
529
+ console = Console()
530
+ console.print(
531
+ Panel(
532
+ f"[cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/] ~ [cyan]{end_time.strftime('%Y-%m-%d %H:%M')}[/]",
533
+ title="[bold]时间范围[/bold]",
534
+ expand=False,
535
+ style="dim",
536
+ border_style="green",
537
+ )
433
538
  )
@@ -240,8 +240,23 @@ class StatsStorage:
240
240
 
241
241
  def list_metrics(self) -> List[str]:
242
242
  """列出所有指标"""
243
+ # 从元数据文件获取指标
243
244
  meta = self._load_json(self.meta_file)
244
- return list(meta.get("metrics", {}).keys())
245
+ metrics_from_meta = set(meta.get("metrics", {}).keys())
246
+
247
+ # 扫描所有数据文件获取实际存在的指标
248
+ metrics_from_data = set()
249
+ for data_file in self.data_dir.glob("stats_*.json"):
250
+ try:
251
+ data = self._load_json(data_file)
252
+ metrics_from_data.update(data.keys())
253
+ except (json.JSONDecodeError, OSError):
254
+ # 忽略无法读取的文件
255
+ continue
256
+
257
+ # 合并两个来源的指标并返回排序后的列表
258
+ all_metrics = metrics_from_meta.union(metrics_from_data)
259
+ return sorted(list(all_metrics))
245
260
 
246
261
  def aggregate_metrics(
247
262
  self,
@@ -310,6 +325,43 @@ class StatsStorage:
310
325
 
311
326
  return result
312
327
 
328
+ def delete_metric(self, metric_name: str) -> bool:
329
+ """
330
+ 删除指定的指标及其所有数据
331
+
332
+ Args:
333
+ metric_name: 要删除的指标名称
334
+
335
+ Returns:
336
+ True 如果成功删除,False 如果指标不存在
337
+ """
338
+ # 检查指标是否存在
339
+ meta = self._load_json(self.meta_file)
340
+ if metric_name not in meta.get("metrics", {}):
341
+ return False
342
+
343
+ # 从元数据中删除指标
344
+ del meta["metrics"][metric_name]
345
+ self._save_json(self.meta_file, meta)
346
+
347
+ # 遍历所有数据文件,删除该指标的数据
348
+ for data_file in self.data_dir.glob("stats_*.json"):
349
+ try:
350
+ data = self._load_json(data_file)
351
+ if metric_name in data:
352
+ del data[metric_name]
353
+ # 如果文件中还有其他数据,保存更新后的文件
354
+ if data:
355
+ self._save_json(data_file, data)
356
+ # 如果文件变空了,删除文件
357
+ else:
358
+ data_file.unlink()
359
+ except Exception:
360
+ # 忽略单个文件的错误,继续处理其他文件
361
+ pass
362
+
363
+ return True
364
+
313
365
  def delete_old_data(self, days_to_keep: int = 30):
314
366
  """删除旧数据"""
315
367
  cutoff_date = (datetime.now() - timedelta(days=days_to_keep)).date()