jarvis-ai-assistant 0.2.3__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.
Files changed (33) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +13 -7
  3. jarvis/jarvis_agent/edit_file_handler.py +4 -0
  4. jarvis/jarvis_agent/jarvis.py +22 -25
  5. jarvis/jarvis_agent/main.py +6 -6
  6. jarvis/jarvis_code_agent/code_agent.py +273 -11
  7. jarvis/jarvis_code_analysis/code_review.py +21 -19
  8. jarvis/jarvis_data/config_schema.json +25 -29
  9. jarvis/jarvis_git_squash/main.py +3 -3
  10. jarvis/jarvis_git_utils/git_commiter.py +32 -11
  11. jarvis/jarvis_mcp/sse_mcp_client.py +4 -6
  12. jarvis/jarvis_mcp/streamable_mcp_client.py +5 -9
  13. jarvis/jarvis_rag/retriever.py +1 -1
  14. jarvis/jarvis_smart_shell/main.py +2 -2
  15. jarvis/jarvis_stats/__init__.py +13 -0
  16. jarvis/jarvis_stats/cli.py +404 -0
  17. jarvis/jarvis_stats/stats.py +538 -0
  18. jarvis/jarvis_stats/storage.py +381 -0
  19. jarvis/jarvis_stats/visualizer.py +282 -0
  20. jarvis/jarvis_tools/cli/main.py +82 -15
  21. jarvis/jarvis_tools/registry.py +32 -16
  22. jarvis/jarvis_tools/search_web.py +3 -3
  23. jarvis/jarvis_tools/virtual_tty.py +315 -26
  24. jarvis/jarvis_utils/config.py +12 -8
  25. jarvis/jarvis_utils/git_utils.py +8 -16
  26. jarvis/jarvis_utils/methodology.py +74 -67
  27. jarvis/jarvis_utils/utils.py +468 -72
  28. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/METADATA +29 -3
  29. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/RECORD +33 -28
  30. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/entry_points.txt +2 -0
  31. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/WHEEL +0 -0
  32. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/licenses/LICENSE +0 -0
  33. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,404 @@
1
+ """
2
+ 统计模块命令行接口
3
+
4
+ 使用 typer 提供友好的命令行交互
5
+ """
6
+
7
+ import builtins
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional, List
10
+ import typer
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+ from rich import print as rprint
14
+ from pathlib import Path
15
+
16
+ from .stats import StatsManager
17
+ from jarvis.jarvis_utils.utils import init_env
18
+ from jarvis.jarvis_utils.config import get_data_dir
19
+
20
+ app = typer.Typer(help="Jarvis 统计模块命令行工具")
21
+ console = Console()
22
+
23
+ # 全局变量,存储是否已初始化
24
+ _initialized = False
25
+ _stats_dir = None
26
+
27
+
28
+ def _get_stats_dir():
29
+ """获取统计数据目录"""
30
+ global _initialized, _stats_dir
31
+ if not _initialized:
32
+ _stats_dir = Path(get_data_dir()) / "stats"
33
+ _initialized = True
34
+ return str(_stats_dir)
35
+
36
+
37
+ @app.command()
38
+ def add(
39
+ metric: str = typer.Argument(..., help="指标名称"),
40
+ value: float = typer.Argument(..., help="指标值"),
41
+ unit: Optional[str] = typer.Option(None, "--unit", "-u", help="单位"),
42
+ tags: Optional[List[str]] = typer.Option(
43
+ None, "--tag", "-t", help="标签,格式: key=value"
44
+ ),
45
+ ):
46
+ """添加统计数据"""
47
+ stats = StatsManager(_get_stats_dir())
48
+
49
+ # 解析标签
50
+ tag_dict = {}
51
+ if tags:
52
+ for tag in tags:
53
+ if "=" in tag:
54
+ key, val = tag.split("=", 1)
55
+ tag_dict[key] = val
56
+
57
+ stats.increment(
58
+ metric,
59
+ amount=value,
60
+ unit=unit if unit else "count",
61
+ tags=tag_dict if tag_dict else None,
62
+ )
63
+
64
+ rprint(
65
+ f"[green]✓[/green] 已添加数据: {metric}={value}" + (f" {unit}" if unit else "")
66
+ )
67
+ if tag_dict:
68
+ rprint(f" 标签: {tag_dict}")
69
+
70
+
71
+ @app.command()
72
+ def inc(
73
+ metric: str = typer.Argument(..., help="指标名称"),
74
+ amount: int = typer.Option(1, "--amount", "-a", help="增加的数量"),
75
+ tags: Optional[List[str]] = typer.Option(
76
+ None, "--tag", "-t", help="标签,格式: key=value"
77
+ ),
78
+ ):
79
+ """增加计数型指标"""
80
+ stats = StatsManager(_get_stats_dir())
81
+
82
+ # 解析标签
83
+ tag_dict = {}
84
+ if tags:
85
+ for tag in tags:
86
+ if "=" in tag:
87
+ key, val = tag.split("=", 1)
88
+ tag_dict[key] = val
89
+
90
+ stats.increment(metric, amount=amount, tags=tag_dict if tag_dict else None)
91
+
92
+ rprint(f"[green]✓[/green] 已增加计数: {metric} +{amount}")
93
+ if tag_dict:
94
+ rprint(f" 标签: {tag_dict}")
95
+
96
+
97
+ @app.command()
98
+ def show(
99
+ metric: Optional[str] = typer.Argument(None, help="指标名称,不指定则显示所有"),
100
+ last_hours: Optional[int] = typer.Option(None, "--hours", "-h", help="最近N小时"),
101
+ last_days: Optional[int] = typer.Option(None, "--days", "-d", help="最近N天"),
102
+ format: str = typer.Option(
103
+ "table", "--format", "-f", help="显示格式: table/chart/summary"
104
+ ),
105
+ aggregation: str = typer.Option(
106
+ "hourly", "--agg", "-a", help="聚合方式: hourly/daily"
107
+ ),
108
+ tags: Optional[List[str]] = typer.Option(
109
+ None, "--tag", "-t", help="标签过滤,格式: key=value"
110
+ ),
111
+ ):
112
+ """显示统计数据"""
113
+ stats = StatsManager(_get_stats_dir())
114
+
115
+ # 解析标签
116
+ tag_dict = {}
117
+ if tags:
118
+ for tag in tags:
119
+ if "=" in tag:
120
+ key, val = tag.split("=", 1)
121
+ tag_dict[key] = val
122
+
123
+ stats.show(
124
+ metric_name=metric,
125
+ last_hours=last_hours,
126
+ last_days=last_days,
127
+ format=format,
128
+ aggregation=aggregation,
129
+ tags=tag_dict if tag_dict else None,
130
+ )
131
+
132
+
133
+ @app.command()
134
+ def plot(
135
+ metric: Optional[str] = typer.Argument(
136
+ None, help="指标名称(可选,不指定则根据标签过滤所有匹配的指标)"
137
+ ),
138
+ last_hours: Optional[int] = typer.Option(None, "--hours", "-h", help="最近N小时"),
139
+ last_days: Optional[int] = typer.Option(None, "--days", "-d", help="最近N天"),
140
+ aggregation: str = typer.Option(
141
+ "hourly", "--agg", "-a", help="聚合方式: hourly/daily"
142
+ ),
143
+ width: Optional[int] = typer.Option(None, "--width", "-w", help="图表宽度"),
144
+ height: Optional[int] = typer.Option(None, "--height", "-H", help="图表高度"),
145
+ tags: Optional[List[str]] = typer.Option(
146
+ None, "--tag", "-t", help="标签过滤,格式: key=value"
147
+ ),
148
+ ):
149
+ """绘制指标折线图,支持根据标签过滤显示多个指标"""
150
+ stats = StatsManager(_get_stats_dir())
151
+
152
+ # 解析标签
153
+ tag_dict = {}
154
+ if tags:
155
+ for tag in tags:
156
+ if "=" in tag:
157
+ key, val = tag.split("=", 1)
158
+ tag_dict[key] = val
159
+
160
+ stats.plot(
161
+ metric_name=metric,
162
+ last_hours=last_hours,
163
+ last_days=last_days,
164
+ aggregation=aggregation,
165
+ width=width,
166
+ height=height,
167
+ tags=tag_dict if tag_dict else None,
168
+ )
169
+
170
+
171
+ @app.command()
172
+ def list():
173
+ """列出所有指标"""
174
+ stats = StatsManager(_get_stats_dir())
175
+ metrics = stats.list_metrics()
176
+
177
+ if not metrics:
178
+ rprint("[yellow]没有找到任何指标[/yellow]")
179
+ return
180
+
181
+ # 创建表格
182
+ table = Table(title="统计指标列表")
183
+ table.add_column("指标名称", style="cyan")
184
+ table.add_column("单位", style="green")
185
+ table.add_column("最后更新", style="yellow")
186
+ table.add_column("7天数据点", style="magenta")
187
+ table.add_column("标签", style="blue")
188
+
189
+ # 获取每个指标的信息
190
+ end_time = datetime.now()
191
+ start_time = end_time - timedelta(days=7)
192
+
193
+ for metric in metrics:
194
+ info = stats._get_storage().get_metric_info(metric)
195
+ if info:
196
+ unit = info.get("unit", "-")
197
+ last_updated = info.get("last_updated", "-")
198
+
199
+ # 格式化时间
200
+ if last_updated != "-":
201
+ try:
202
+ dt = datetime.fromisoformat(last_updated)
203
+ last_updated = dt.strftime("%Y-%m-%d %H:%M")
204
+ except:
205
+ pass
206
+
207
+ # 获取数据点数和标签
208
+ records = stats._get_storage().get_metrics(metric, start_time, end_time)
209
+ count = len(records)
210
+
211
+ # 收集所有唯一的标签
212
+ all_tags = {}
213
+ for record in records:
214
+ tags = record.get("tags", {})
215
+ for k, v in tags.items():
216
+ if k not in all_tags:
217
+ all_tags[k] = set()
218
+ all_tags[k].add(v)
219
+
220
+ # 格式化标签显示
221
+ tag_str = ""
222
+ if all_tags:
223
+ tag_parts = []
224
+ for k, values in sorted(all_tags.items()):
225
+ # 使用内置的list函数
226
+ values_list = sorted(builtins.list(values))
227
+ if len(values_list) == 1:
228
+ tag_parts.append(f"{k}={values_list[0]}")
229
+ else:
230
+ # 转义方括号以避免Rich markup错误
231
+ tag_parts.append(f"{k}=\\[{', '.join(values_list)}\\]")
232
+ tag_str = ", ".join(tag_parts)
233
+ else:
234
+ tag_str = "-"
235
+
236
+ table.add_row(metric, unit, last_updated, str(count), tag_str)
237
+
238
+ console.print(table)
239
+ rprint(f"\n[green]总计: {len(metrics)} 个指标[/green]")
240
+
241
+
242
+ @app.command()
243
+ def clean(
244
+ days: int = typer.Option(30, "--days", "-d", help="保留最近N天的数据"),
245
+ yes: bool = typer.Option(False, "--yes", "-y", help="跳过确认"),
246
+ ):
247
+ """清理旧数据"""
248
+ if not yes:
249
+ confirm = typer.confirm(f"确定要删除 {days} 天前的数据吗?")
250
+ if not confirm:
251
+ rprint("[yellow]已取消操作[/yellow]")
252
+ return
253
+
254
+ stats = StatsManager(_get_stats_dir())
255
+ stats.clean_old_data(days_to_keep=days)
256
+ rprint(f"[green]✓[/green] 已清理 {days} 天前的数据")
257
+
258
+
259
+ @app.command()
260
+ def export(
261
+ metric: str = typer.Argument(..., help="指标名称"),
262
+ output: str = typer.Option("csv", "--format", "-f", help="输出格式: csv/json"),
263
+ last_hours: Optional[int] = typer.Option(None, "--hours", "-h", help="最近N小时"),
264
+ last_days: Optional[int] = typer.Option(None, "--days", "-d", help="最近N天"),
265
+ tags: Optional[List[str]] = typer.Option(
266
+ None, "--tag", "-t", help="标签过滤,格式: key=value"
267
+ ),
268
+ ):
269
+ """导出统计数据"""
270
+ import json
271
+ import csv
272
+ import sys
273
+
274
+ stats = StatsManager(_get_stats_dir())
275
+
276
+ # 解析标签
277
+ tag_dict = {}
278
+ if tags:
279
+ for tag in tags:
280
+ if "=" in tag:
281
+ key, val = tag.split("=", 1)
282
+ tag_dict[key] = val
283
+
284
+ # 获取数据
285
+ data = stats.get_stats(
286
+ metric_name=metric,
287
+ last_hours=last_hours,
288
+ last_days=last_days,
289
+ tags=tag_dict if tag_dict else None,
290
+ )
291
+
292
+ if output == "json":
293
+ # JSON格式输出
294
+ print(json.dumps(data, indent=2, ensure_ascii=False))
295
+ else:
296
+ # CSV格式输出
297
+ records = data.get("records", [])
298
+ if records:
299
+ writer = csv.writer(sys.stdout)
300
+ writer.writerow(["timestamp", "value", "tags"])
301
+ for record in records:
302
+ tags_str = json.dumps(record.get("tags", {}))
303
+ writer.writerow([record["timestamp"], record["value"], tags_str])
304
+ else:
305
+ rprint("[yellow]没有找到数据[/yellow]", file=sys.stderr)
306
+
307
+
308
+ @app.command()
309
+ def remove(
310
+ metric: str = typer.Argument(..., help="要删除的指标名称"),
311
+ yes: bool = typer.Option(False, "--yes", "-y", help="跳过确认"),
312
+ ):
313
+ """删除指定的指标及其所有数据"""
314
+ if not yes:
315
+ # 显示指标信息供用户确认
316
+ stats = StatsManager(_get_stats_dir())
317
+ metrics = stats.list_metrics()
318
+
319
+ if metric not in metrics:
320
+ rprint(f"[red]错误:指标 '{metric}' 不存在[/red]")
321
+ return
322
+
323
+ # 获取指标的基本信息
324
+ info = stats._get_storage().get_metric_info(metric)
325
+ if info:
326
+ unit = info.get("unit", "-")
327
+ last_updated = info.get("last_updated", "-")
328
+
329
+ rprint(f"\n[yellow]准备删除指标:[/yellow]")
330
+ rprint(f" 名称: {metric}")
331
+ rprint(f" 单位: {unit}")
332
+ rprint(f" 最后更新: {last_updated}")
333
+
334
+ confirm = typer.confirm(f"\n确定要删除指标 '{metric}' 及其所有数据吗?")
335
+ if not confirm:
336
+ rprint("[yellow]已取消操作[/yellow]")
337
+ return
338
+
339
+ stats = StatsManager(_get_stats_dir())
340
+ success = stats.remove_metric(metric)
341
+
342
+ if success:
343
+ rprint(f"[green]✓[/green] 已成功删除指标: {metric}")
344
+ else:
345
+ rprint(f"[red]✗[/red] 删除失败:指标 '{metric}' 不存在")
346
+
347
+
348
+ @app.command()
349
+ def demo():
350
+ """运行演示,展示统计模块的功能"""
351
+ import random
352
+ import time
353
+
354
+ console.print("[bold cyan]Jarvis 统计模块演示[/bold cyan]\n")
355
+
356
+ stats = StatsManager(_get_stats_dir())
357
+
358
+ # 添加演示数据
359
+ with console.status("[bold green]正在生成演示数据...") as status:
360
+ # API响应时间
361
+ for i in range(20):
362
+ response_time = random.uniform(0.1, 2.0)
363
+ status_code = random.choice(["200", "404", "500"])
364
+ stats.increment(
365
+ "demo_response_time",
366
+ amount=response_time,
367
+ unit="seconds",
368
+ tags={"status": status_code},
369
+ )
370
+ time.sleep(0.05)
371
+
372
+ # 访问计数
373
+ for i in range(30):
374
+ endpoint = random.choice(["/api/users", "/api/posts", "/api/admin"])
375
+ stats.increment("demo_api_calls", tags={"endpoint": endpoint})
376
+ time.sleep(0.05)
377
+
378
+ rprint("[green]✓[/green] 演示数据生成完成\n")
379
+
380
+ # 显示数据
381
+ console.rule("[bold blue]指标列表")
382
+ stats.show()
383
+
384
+ console.rule("[bold blue]响应时间详情")
385
+ stats.show("demo_response_time", last_hours=1)
386
+
387
+ console.rule("[bold blue]API调用折线图")
388
+ stats.plot("demo_api_calls", last_hours=1, height=10)
389
+
390
+ console.rule("[bold blue]响应时间汇总")
391
+ stats.show("demo_response_time", last_hours=1, format="summary")
392
+
393
+ rprint("\n[green]✓[/green] 演示完成!")
394
+
395
+
396
+ def main():
397
+ """主入口函数"""
398
+ # 初始化环境,防止设置初始化太迟
399
+ init_env("欢迎使用 Jarvis-Stats,您的统计分析工具已准备就绪!", None)
400
+ app()
401
+
402
+
403
+ if __name__ == "__main__":
404
+ main()