Files
iov_ana/cli.py
openclaw e8f8e2f1ba feat: 四层架构全面增强
安全与稳定性:
- 移除硬编码 API Key,改用 .env + 环境变量
- LLM 调用统一重试机制(指数退避,3 次重试,处理 429/5xx/超时)
- 中文字体检测增强(CJK 关键词兜底 + 无字体时英文 fallback)
- 缺失 API Key 给出友好提示而非崩溃

分析能力提升:
- 异常检测新增 z-score 检测(标准差>2 标记异常)
- 新增变异系数 CV 检测(数据波动性)
- 新增零值/缺失检测
- 上下文管理器升级为关键词语义匹配(替代简单取最近 2 条)

用户体验:
- 报告自动保存为 Markdown(reports/ 目录)
- 新增 export 命令导出查询结果为 CSV
- 新增 reports 命令查看已保存报告
- CLI 支持 readline 命令历史(方向键翻阅)
- CSV 导入工具重写:自动列名映射、容错处理、dry-run 模式
- 新增 .env.example 配置模板
2026-03-31 14:39:17 +08:00

195 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
交互式 CLI —— 四层架构自适应分析(增强版)
用法: python cli.py [数据库路径]
"""
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
from core.config import DB_PATH, LLM_CONFIG, MAX_EXPLORATION_ROUNDS, PLAYBOOK_DIR, PROJECT_ROOT
from agent import DataAnalysisAgent
def print_help():
print("""
可用命令:
<问题> 分析一个问题
rounds=<N> <问题> 设置探索轮数
report [主题] 整合所有分析,生成综合报告
export 导出最近一次分析结果为 CSV
reports 列出已保存的报告文件
schema 查看数据库 Schema
playbooks 查看已加载的预设剧本
regen 重新生成预设剧本
history 查看分析历史
audit 查看 SQL 审计日志
clear 清空分析历史
help 显示帮助
quit / q 退出
""")
def cmd_reports(agent):
"""列出已保存的报告"""
reports_dir = agent.reports_dir
if not os.path.isdir(reports_dir):
print("reports 目录不存在)")
return
files = sorted([f for f in os.listdir(reports_dir) if f.endswith(".md")])
if not files:
print("(尚无保存的报告)")
return
print(f"\n📁 已保存 {len(files)} 份报告:")
for f in files:
fpath = os.path.join(reports_dir, f)
size = os.path.getsize(fpath)
print(f" 📄 {f} ({size/1024:.1f} KB)")
def setup_readline():
"""启用命令历史Linux/macOS"""
try:
import readline
histfile = os.path.join(PROJECT_ROOT, ".cli_history")
try:
readline.read_history_file(histfile)
except FileNotFoundError:
pass
import atexit
atexit.register(readline.write_history_file, histfile)
readline.set_history_length(100)
except ImportError:
pass
def main():
db_path = sys.argv[1] if len(sys.argv) > 1 else DB_PATH
if not os.path.exists(db_path):
print(f"❌ 数据库不存在: {db_path}")
sys.exit(1)
if not LLM_CONFIG["api_key"]:
print("❌ LLM_API_KEY 未配置!")
print(" 请设置环境变量或创建 .env 文件:")
print(" LLM_API_KEY=your-key")
print(" LLM_BASE_URL=https://api.openai.com/v1")
print(" LLM_MODEL=gpt-4o-mini")
sys.exit(1)
setup_readline()
agent = DataAnalysisAgent(db_path)
print("=" * 60)
print(" 🤖 数据分析 Agent —— 四层架构")
print("=" * 60)
print(f"\n🔗 LLM: {LLM_CONFIG['model']} @ {LLM_CONFIG['base_url']}")
print(f"🔄 最大探索轮数: {MAX_EXPLORATION_ROUNDS}")
print(f"💾 数据库: {db_path}")
print(f"📋 预设剧本: {len(agent.playbook_mgr.playbooks)}")
print(f"\n💬 输入分析问题help 查看命令)\n")
last_steps = None # 记录最近一次分析的 steps用于 export
while True:
try:
user_input = input("📊 > ").strip()
except (EOFError, KeyboardInterrupt):
print("\n👋 再见!")
break
if not user_input:
continue
cmd = user_input.lower()
if cmd in ("quit", "exit", "q"):
print("👋 再见!")
break
elif cmd == "help":
print_help()
elif cmd == "schema":
print(agent.get_schema())
elif cmd == "history":
print(agent.get_history())
elif cmd == "audit":
print(agent.get_audit())
elif cmd == "clear":
agent.clear_history()
last_steps = None
print("✅ 历史已清空")
elif cmd == "reports":
cmd_reports(agent)
elif cmd == "export":
if last_steps:
fpath = agent.export_data(last_steps)
if fpath:
print(f"✅ 导出成功: {fpath}")
else:
print("⚠️ 无数据可导出")
else:
print("⚠️ 请先执行一次分析")
elif cmd.startswith("report"):
topic = user_input[6:].strip()
try:
report = agent.full_report(question=topic)
print("\n" + report)
print("\n" + "~" * 60)
except Exception as e:
print(f"\n❌ 报告整合出错: {e}")
import traceback
traceback.print_exc()
elif cmd == "playbooks":
if not agent.playbook_mgr.playbooks:
print("(无预设剧本,输入 regen 让 AI 自动生成)")
else:
for i, pb in enumerate(agent.playbook_mgr.playbooks, 1):
print(f" {i}. 📋 {pb.name}{pb.description} ({len(pb.preset_queries)} 条预设)")
elif cmd == "regen":
if os.path.isdir(PLAYBOOK_DIR):
for f in os.listdir(PLAYBOOK_DIR):
if f.startswith("auto_") and f.endswith(".json"):
os.remove(os.path.join(PLAYBOOK_DIR, f))
agent.playbook_mgr.playbooks.clear()
print("🤖 AI 正在重新生成预设剧本...")
generated = agent.playbook_mgr.auto_generate(agent.schema_text, save_dir=PLAYBOOK_DIR)
if generated:
print(f"✅ 生成 {len(generated)} 个剧本:")
for pb in generated:
print(f" 📋 {pb.name}{pb.description}")
else:
print("⚠️ 生成失败")
else:
# 解析 rounds=N
max_rounds = MAX_EXPLORATION_ROUNDS
question = user_input
if "rounds=" in question.lower():
parts = question.split("rounds=")
question = parts[0].strip()
try:
max_rounds = int(parts[1].strip().split()[0])
except (ValueError, IndexError):
pass
try:
report = agent.analyze(question, max_rounds=max_rounds)
# 保存 steps 用于 export
if agent.context.sessions:
last_steps = agent.context.sessions[-1].steps
print("\n" + report)
print("\n" + "~" * 60)
except Exception as e:
print(f"\n❌ 分析出错: {e}")
import traceback
traceback.print_exc()
print("\n📋 本次会话审计:")
print(agent.get_audit())
agent.close()
if __name__ == "__main__":
main()