feat: 自动添加上下文
自动添加历史命令记录以及当前文件夹下面的内容为上下文
This commit is contained in:
parent
c8b615eecc
commit
9870926054
14
README.md
14
README.md
@ -9,6 +9,8 @@ AutoTerminal 是一个基于大语言模型的智能终端工具,可以将自
|
||||
- ⚙️ 灵活的配置管理
|
||||
- 🌍 中文支持
|
||||
- 🔄 支持多种LLM模型
|
||||
- 📚 命令历史记录和上下文感知
|
||||
- 📁 当前目录内容上下文感知
|
||||
|
||||
## 安装
|
||||
|
||||
@ -50,6 +52,10 @@ sudo pip install .
|
||||
|
||||
配置信息会保存在 `config.json` 文件中。
|
||||
|
||||
### 配置选项
|
||||
|
||||
- `max_history`: 历史命令记录数量(默认:10)
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方法1:使用uv run
|
||||
@ -63,6 +69,11 @@ uv pip install -e .
|
||||
at "查看当前目录下的所有文件"
|
||||
```
|
||||
|
||||
### 使用历史命令上下文
|
||||
```bash
|
||||
at --history-count 5 "基于前面的命令,删除所有.txt文件"
|
||||
```
|
||||
|
||||
程序会生成终端命令并显示提示,用户按回车后程序会直接执行该命令。
|
||||
|
||||
## 示例
|
||||
@ -92,6 +103,9 @@ autoterminal/
|
||||
├── llm/ # LLM相关模块
|
||||
│ ├── __init__.py # 包初始化文件
|
||||
│ └── client.py # LLM客户端
|
||||
├── history/ # 历史命令管理模块
|
||||
│ ├── __init__.py # 包初始化文件
|
||||
│ └── history.py # 历史命令管理器
|
||||
├── utils/ # 工具函数
|
||||
│ ├── __init__.py # 包初始化文件
|
||||
│ └── helpers.py # 辅助函数
|
||||
|
@ -0,0 +1,5 @@
|
||||
from . import config, llm, utils
|
||||
from .history import HistoryManager
|
||||
from .main import main
|
||||
|
||||
__all__ = ['config', 'llm', 'utils', 'HistoryManager', 'main']
|
@ -1,6 +1,6 @@
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
class ConfigLoader:
|
||||
"""配置加载器,支持从文件加载配置"""
|
||||
|
@ -20,7 +20,8 @@ class ConfigManager:
|
||||
self.default_config = {
|
||||
'base_url': 'https://api.openai.com/v1',
|
||||
'model': 'gpt-4o',
|
||||
'default_prompt': '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!'
|
||||
'default_prompt': '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!',
|
||||
'max_history': 10
|
||||
}
|
||||
|
||||
def save_config(self, config: Dict[str, Any]) -> bool:
|
||||
@ -51,21 +52,40 @@ class ConfigManager:
|
||||
config = self.default_config.copy()
|
||||
|
||||
# 获取API密钥
|
||||
try:
|
||||
api_key = input("请输入您的API密钥: ").strip()
|
||||
if not api_key:
|
||||
print("错误: API密钥不能为空")
|
||||
return {}
|
||||
config['api_key'] = api_key
|
||||
except EOFError:
|
||||
print("\n配置向导已取消。")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"错误: 无法读取API密钥输入: {e}")
|
||||
return {}
|
||||
|
||||
# 获取Base URL
|
||||
try:
|
||||
base_url = input(f"请输入Base URL (默认: {self.default_config['base_url']}): ").strip()
|
||||
if base_url:
|
||||
config['base_url'] = base_url
|
||||
except EOFError:
|
||||
print("\n配置向导已取消。")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"警告: 无法读取Base URL输入: {e}")
|
||||
|
||||
# 获取模型名称
|
||||
try:
|
||||
model = input(f"请输入模型名称 (默认: {self.default_config['model']}): ").strip()
|
||||
if model:
|
||||
config['model'] = model
|
||||
except EOFError:
|
||||
print("\n配置向导已取消。")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"警告: 无法读取模型名称输入: {e}")
|
||||
|
||||
# 保存配置
|
||||
if self.save_config(config):
|
||||
|
73
autoterminal/history.py
Normal file
73
autoterminal/history.py
Normal file
@ -0,0 +1,73 @@
|
||||
import os
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
class HistoryManager:
|
||||
"""历史命令管理器,用于记录和检索命令历史"""
|
||||
|
||||
def __init__(self, history_file: str = None, max_history: int = 10):
|
||||
if history_file is None:
|
||||
# 将历史文件存储在用户主目录下的.autoterminal目录中
|
||||
home_dir = os.path.expanduser("~")
|
||||
config_dir = os.path.join(home_dir, ".autoterminal")
|
||||
self.history_file = os.path.join(config_dir, "history.json")
|
||||
else:
|
||||
self.history_file = history_file
|
||||
|
||||
self.max_history = max_history
|
||||
self.history = self.load_history()
|
||||
|
||||
def load_history(self) -> List[Dict[str, Any]]:
|
||||
"""从历史文件加载命令历史"""
|
||||
if os.path.exists(self.history_file):
|
||||
try:
|
||||
with open(self.history_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"警告: 无法读取历史文件 {self.history_file}: {e}")
|
||||
return []
|
||||
|
||||
def save_history(self) -> bool:
|
||||
"""保存命令历史到文件"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(self.history_file) if os.path.dirname(self.history_file) else '.', exist_ok=True)
|
||||
|
||||
with open(self.history_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.history, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"错误: 无法保存历史文件 {self.history_file}: {e}")
|
||||
return False
|
||||
|
||||
def add_command(self, user_input: str, generated_command: str, executed: bool = True) -> None:
|
||||
"""添加命令到历史记录"""
|
||||
entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"user_input": user_input,
|
||||
"generated_command": generated_command,
|
||||
"executed": executed
|
||||
}
|
||||
|
||||
self.history.append(entry)
|
||||
|
||||
# 保持历史记录在最大数量限制内
|
||||
if len(self.history) > self.max_history:
|
||||
self.history = self.history[-self.max_history:]
|
||||
|
||||
# 保存到文件
|
||||
self.save_history()
|
||||
|
||||
def get_recent_history(self, count: int = None) -> List[Dict[str, Any]]:
|
||||
"""获取最近的命令历史"""
|
||||
if count is None:
|
||||
count = self.max_history
|
||||
|
||||
return self.history[-count:] if self.history else []
|
||||
|
||||
def get_last_command(self) -> Dict[str, Any]:
|
||||
"""获取最后一条命令"""
|
||||
if self.history:
|
||||
return self.history[-1]
|
||||
return {}
|
4
autoterminal/history/__init__.py
Normal file
4
autoterminal/history/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# History module initialization
|
||||
from .history import HistoryManager
|
||||
|
||||
__all__ = ['HistoryManager']
|
80
autoterminal/history/history.py
Normal file
80
autoterminal/history/history.py
Normal file
@ -0,0 +1,80 @@
|
||||
import os
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
class HistoryManager:
|
||||
"""历史命令管理器,用于记录和检索命令历史"""
|
||||
|
||||
def __init__(self, history_file: str = None, max_history: int = 10):
|
||||
if history_file is None:
|
||||
# 将历史文件存储在用户主目录下的.autoterminal目录中
|
||||
home_dir = os.path.expanduser("~")
|
||||
config_dir = os.path.join(home_dir, ".autoterminal")
|
||||
self.history_file = os.path.join(config_dir, "history.json")
|
||||
else:
|
||||
self.history_file = history_file
|
||||
|
||||
self.max_history = max_history
|
||||
self.history = self.load_history()
|
||||
|
||||
def load_history(self) -> List[Dict[str, Any]]:
|
||||
"""从历史文件加载命令历史"""
|
||||
if os.path.exists(self.history_file):
|
||||
try:
|
||||
with open(self.history_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"警告: 无法读取历史文件 {self.history_file}: {e}")
|
||||
return []
|
||||
|
||||
def save_history(self) -> bool:
|
||||
"""保存命令历史到文件"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(self.history_file) if os.path.dirname(self.history_file) else '.', exist_ok=True)
|
||||
|
||||
with open(self.history_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.history, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"错误: 无法保存历史文件 {self.history_file}: {e}")
|
||||
return False
|
||||
|
||||
def add_command(self, user_input: str, generated_command: str, executed: bool = True) -> None:
|
||||
"""添加命令到历史记录"""
|
||||
entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"user_input": user_input,
|
||||
"generated_command": generated_command,
|
||||
"executed": executed
|
||||
}
|
||||
|
||||
self.history.append(entry)
|
||||
|
||||
# 保持历史记录在最大数量限制内
|
||||
if len(self.history) > self.max_history:
|
||||
self.history = self.history[-self.max_history:]
|
||||
|
||||
# 保存到文件
|
||||
self.save_history()
|
||||
|
||||
def get_last_executed_command(self) -> str:
|
||||
"""获取最后一条已执行的命令"""
|
||||
for entry in reversed(self.history):
|
||||
if entry.get("executed", False):
|
||||
return entry.get("generated_command", "")
|
||||
return ""
|
||||
|
||||
def get_recent_history(self, count: int = None) -> List[Dict[str, Any]]:
|
||||
"""获取最近的命令历史"""
|
||||
if count is None:
|
||||
count = self.max_history
|
||||
|
||||
return self.history[-count:] if self.history else []
|
||||
|
||||
def get_last_command(self) -> Dict[str, Any]:
|
||||
"""获取最后一条命令"""
|
||||
if self.history:
|
||||
return self.history[-1]
|
||||
return {}
|
@ -1,5 +1,6 @@
|
||||
from openai import OpenAI
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, List
|
||||
import os
|
||||
|
||||
class LLMClient:
|
||||
"""LLM客户端,封装OpenAI API调用"""
|
||||
@ -11,14 +12,43 @@ class LLMClient:
|
||||
base_url=config.get('base_url')
|
||||
)
|
||||
|
||||
def generate_command(self, user_input: str, prompt: Optional[str] = None) -> str:
|
||||
def generate_command(self, user_input: str, prompt: Optional[str] = None,
|
||||
history: Optional[List[Dict[str, Any]]] = None,
|
||||
current_dir_content: Optional[List[str]] = None,
|
||||
last_executed_command: str = "") -> str:
|
||||
"""根据用户输入生成命令"""
|
||||
# 根据用户输入是否为空选择不同的提示词
|
||||
if not user_input:
|
||||
if not prompt:
|
||||
prompt = self.config.get('default_prompt', '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!')
|
||||
prompt = self.config.get('recommendation_prompt', '你现在是一个终端助手,根据上下文自动推荐命令:当用户没有输入时,基于最近执行的命令历史和当前目录内容,智能推荐最可能需要的终端命令(仅当有明确上下文线索时);当用户输入命令需求时,生成对应命令。仅输出纯命令文本,不要任何解释或多余内容!')
|
||||
else:
|
||||
if not prompt:
|
||||
prompt = self.config.get('default_prompt', '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!')
|
||||
|
||||
# 构建系统提示,包含上下文信息
|
||||
system_prompt = prompt
|
||||
|
||||
# 添加历史命令上下文
|
||||
if history:
|
||||
history_context = "\n最近执行的命令历史:\n"
|
||||
for i, entry in enumerate(reversed(history), 1):
|
||||
history_context += f"{i}. 用户输入: {entry.get('user_input', '')} -> 生成命令: {entry.get('generated_command', '')}\n"
|
||||
system_prompt += history_context
|
||||
|
||||
# 添加当前目录内容上下文
|
||||
if current_dir_content:
|
||||
dir_context = "\n当前目录下的文件和文件夹:\n" + "\n".join(current_dir_content)
|
||||
system_prompt += dir_context
|
||||
|
||||
# 当用户输入为空时,使用特殊的提示来触发推荐模式
|
||||
if not user_input:
|
||||
user_content = f"根据提供的上下文信息,推荐一个最可能需要的终端命令(仅当有明确的上下文线索时)。如果上下文信息不足以确定一个有用的命令,则返回空。请直接返回一个可执行的终端命令,不要包含任何解释或其他文本。例如:ls -la 或 git status。特别注意:不要使用echo命令来列出文件,应该使用ls命令。推荐命令时请考虑最近执行的命令历史,避免重复推荐相同的命令。最后执行的命令是: {last_executed_command}。如果当前目录有pyproject.toml或setup.py文件,可以考虑使用pip list查看已安装的包。"
|
||||
else:
|
||||
user_content = user_input
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "user", "content": user_input}
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_content}
|
||||
]
|
||||
|
||||
try:
|
||||
|
@ -4,11 +4,13 @@
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import glob
|
||||
|
||||
from autoterminal.config.loader import ConfigLoader
|
||||
from autoterminal.config.manager import ConfigManager
|
||||
from autoterminal.llm.client import LLMClient
|
||||
from autoterminal.utils.helpers import clean_command
|
||||
from autoterminal.history import HistoryManager
|
||||
|
||||
def main():
|
||||
"""主程序入口"""
|
||||
@ -18,6 +20,7 @@ def main():
|
||||
parser.add_argument('--api-key', help='API密钥')
|
||||
parser.add_argument('--base-url', help='Base URL')
|
||||
parser.add_argument('--model', help='模型名称')
|
||||
parser.add_argument('--history-count', type=int, help='历史命令数量')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@ -36,6 +39,9 @@ def main():
|
||||
if args.model:
|
||||
config['model'] = args.model
|
||||
|
||||
# 获取历史命令数量配置
|
||||
history_count = args.history_count or config.get('max_history', 10)
|
||||
|
||||
# 如果配置不完整,使用配置管理器初始化
|
||||
config_manager = ConfigManager()
|
||||
if not all([config.get('api_key'), config.get('base_url'), config.get('model')]):
|
||||
@ -43,8 +49,22 @@ def main():
|
||||
if not config:
|
||||
print("错误: 缺少必要的配置参数,请通过命令行参数或配置文件提供API密钥、Base URL和模型名称。")
|
||||
return 1
|
||||
|
||||
# 如果有命令行参数输入,直接处理
|
||||
if user_input:
|
||||
# 初始化历史管理器
|
||||
history_manager = HistoryManager(max_history=history_count)
|
||||
|
||||
# 获取历史命令
|
||||
history = history_manager.get_recent_history(history_count)
|
||||
|
||||
# 获取当前目录内容
|
||||
try:
|
||||
current_dir_content = glob.glob("*")
|
||||
except Exception as e:
|
||||
print(f"警告: 无法获取当前目录内容: {e}")
|
||||
current_dir_content = []
|
||||
|
||||
# 初始化LLM客户端
|
||||
try:
|
||||
llm_client = LLMClient(config)
|
||||
@ -54,7 +74,11 @@ def main():
|
||||
|
||||
# 调用LLM生成命令
|
||||
try:
|
||||
generated_command = llm_client.generate_command(user_input)
|
||||
generated_command = llm_client.generate_command(
|
||||
user_input=user_input,
|
||||
history=history,
|
||||
current_dir_content=current_dir_content
|
||||
)
|
||||
cleaned_command = clean_command(generated_command)
|
||||
|
||||
# 优化输出格式
|
||||
@ -62,18 +86,72 @@ def main():
|
||||
print("\033[1;37mPress Enter to execute...\033[0m")
|
||||
|
||||
# 等待用户回车确认执行
|
||||
try:
|
||||
input()
|
||||
|
||||
# 在用户的环境中执行命令
|
||||
os.system(cleaned_command)
|
||||
|
||||
# 记录到历史
|
||||
history_manager.add_command(user_input, cleaned_command)
|
||||
except EOFError:
|
||||
print("\n输入已取消。")
|
||||
return 0
|
||||
except Exception as exec_e:
|
||||
print(f"命令执行失败: {exec_e}")
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"命令生成失败: {e}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
else:
|
||||
print("错误: 请提供要执行的命令,例如: python main.py \"查看当前目录\"")
|
||||
# 处理空输入情况 - 生成基于上下文的推荐命令
|
||||
history_manager = HistoryManager(max_history=history_count)
|
||||
history = history_manager.get_recent_history(history_count)
|
||||
|
||||
try:
|
||||
current_dir_content = glob.glob("*")
|
||||
except Exception as e:
|
||||
print(f"警告: 无法获取当前目录内容: {e}")
|
||||
current_dir_content = []
|
||||
|
||||
try:
|
||||
llm_client = LLMClient(config)
|
||||
except Exception as e:
|
||||
print(f"LLM客户端初始化失败: {e}")
|
||||
return 1
|
||||
|
||||
# 获取最后执行的命令以避免重复推荐
|
||||
last_executed_command = history_manager.get_last_executed_command()
|
||||
|
||||
try:
|
||||
recommendation = llm_client.generate_command(
|
||||
user_input="",
|
||||
history=history,
|
||||
current_dir_content=current_dir_content,
|
||||
last_executed_command=last_executed_command
|
||||
)
|
||||
cleaned_recommendation = clean_command(recommendation)
|
||||
|
||||
if cleaned_recommendation.strip():
|
||||
print(f"\033[1;34m💡 建议命令:\033[0m {cleaned_recommendation}")
|
||||
print("\033[1;37mPress Enter to execute, or Ctrl+C to cancel...\033[0m")
|
||||
try:
|
||||
input()
|
||||
os.system(cleaned_recommendation)
|
||||
history_manager.add_command("自动推荐", cleaned_recommendation)
|
||||
except EOFError:
|
||||
print("\n输入已取消。")
|
||||
return 0
|
||||
except Exception as exec_e:
|
||||
print(f"命令执行失败: {exec_e}")
|
||||
return 1
|
||||
else:
|
||||
print("没有找到相关的命令建议。")
|
||||
except Exception as e:
|
||||
print(f"命令推荐生成失败: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -32,9 +32,9 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/wds-dxh/autoterminal"
|
||||
Repository = "https://github.com/wds-dxh/autoterminal"
|
||||
Issues = "https://github.com/wds-dxh/autoterminal/issues"
|
||||
Homepage = "http://cloud-home.dxh-wds.top:20100/w/autoterminal"
|
||||
Repository = "http://cloud-home.dxh-wds.top:20100/w/autoterminal"
|
||||
Issues = "http://cloud-home.dxh-wds.top:20100/w/autoterminal/issues"
|
||||
|
||||
[project.scripts]
|
||||
at = "autoterminal.main:main"
|
||||
|
2
setup.py
2
setup.py
@ -8,7 +8,7 @@ setup(
|
||||
long_description_content_type="text/markdown",
|
||||
author="wds",
|
||||
author_email="wdsnpshy@163.com",
|
||||
url="https://github.com/wds-dxh/autoterminal",
|
||||
url="http://cloud-home.dxh-wds.top:20100/w/AutoTerminal",
|
||||
license="MIT",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
|
Loading…
x
Reference in New Issue
Block a user