mirror of
https://github.com/tcsenpai/suggestoor.git
synced 2025-06-03 01:40:16 +00:00
first commit
This commit is contained in:
commit
775bfa7c56
13
.env.example
Normal file
13
.env.example
Normal file
@ -0,0 +1,13 @@
|
||||
FOLDERS=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
|
||||
|
||||
# OLLAMA settings
|
||||
OLLAMA_HOST=http://localhost:11434
|
||||
# NOTE On the model: you should use this model as it's tested
|
||||
# or models with higher params than 8b anyway.
|
||||
# You can download this model with:
|
||||
# ollama pull llama3.1:8b-instruct-q4_K_M
|
||||
OLLAMA_MODEL=llama3.1:8b-instruct-q4_K_M
|
||||
EMBEDDING_MODEL=nomic-embed-text
|
||||
|
||||
# Vector search parameters
|
||||
SIMILARITY_K=8 # Number of similar chunks to retrieve
|
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Faiss
|
||||
vector_store/
|
||||
mans/
|
||||
embedding_progress.json
|
||||
|
||||
# env
|
||||
.env
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.11
|
66
README.md
Normal file
66
README.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Suggestoor
|
||||
|
||||
Suggestoor is a tool that helps you find the right command-line tools for your needs.
|
||||
|
||||

|
||||
|
||||
## Important
|
||||
|
||||
At the first run, it will take a while to embed all the man pages.
|
||||
It can take up to 1-2 hours.
|
||||
This is a one-time operation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- ollama server running
|
||||
- a GPU for embedding (using a CPU is unfeasible)
|
||||
|
||||
## Usage
|
||||
|
||||
### Using uv
|
||||
|
||||
```bash
|
||||
uv run python main.py
|
||||
```
|
||||
|
||||
### Using system python
|
||||
|
||||
```bash
|
||||
python suggestoor.py
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Using uv
|
||||
|
||||
Just run the script and it will install the dependencies for you.
|
||||
|
||||
### Using pip
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
You can configure the tool by editing the `.env` file.
|
||||
|
||||
### OLLAMA_HOST
|
||||
|
||||
This is the host of the ollama server.
|
||||
|
||||
### OLLAMA_MODEL
|
||||
|
||||
This is the model that will be used to generate the response.
|
||||
|
||||
### EMBEDDING_MODEL
|
||||
|
||||
This is the model that will be used to embed the man pages.
|
||||
|
||||
### SIMILARITY_K
|
||||
|
||||
This is the number of similar chunks to retrieve. Increase this number if you want more context. For a 8gb ram machine, 8 to 12 is a good number.
|
||||
|
||||
### FOLDERS
|
||||
|
||||
This is the folders of binaries to search for man pages and embed them.
|
225
libs/get_all_mans.py
Normal file
225
libs/get_all_mans.py
Normal file
@ -0,0 +1,225 @@
|
||||
# This script will get all the man pages from the binaries and save them in a file
|
||||
import os
|
||||
import subprocess
|
||||
import dotenv
|
||||
import json
|
||||
from langchain_ollama import OllamaEmbeddings
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from colorama import Fore, Style
|
||||
from tqdm import tqdm
|
||||
import sys
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
def get_binaries():
|
||||
binaries = []
|
||||
folders = os.getenv("FOLDERS").split(":")
|
||||
for folder in folders:
|
||||
for file in os.listdir(folder):
|
||||
binaries.append(file)
|
||||
return binaries
|
||||
|
||||
|
||||
def load_progress():
|
||||
"""Load the progress of already processed binaries"""
|
||||
try:
|
||||
if os.path.exists("embedding_progress.json"):
|
||||
with open("embedding_progress.json", "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"{Fore.YELLOW}Could not load progress, starting fresh: {e}{Style.RESET_ALL}"
|
||||
)
|
||||
return {"processed_binaries": [], "vector_store_exists": False}
|
||||
|
||||
|
||||
def save_progress(processed_binaries, vector_store_exists=False):
|
||||
"""Save the progress of processed binaries"""
|
||||
try:
|
||||
with open("embedding_progress.json", "w") as f:
|
||||
json.dump(
|
||||
{
|
||||
"processed_binaries": processed_binaries,
|
||||
"vector_store_exists": vector_store_exists,
|
||||
},
|
||||
f,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"{Fore.YELLOW}Could not save progress: {e}{Style.RESET_ALL}")
|
||||
|
||||
|
||||
def create_embeddings_db(texts, metadatas, existing_db=None):
|
||||
vector_store_path = os.path.abspath("vector_store")
|
||||
print(
|
||||
f"{Fore.CYAN}Will create/update vector store at: {vector_store_path}{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
print(f"{Fore.CYAN}Creating embeddings...{Style.RESET_ALL}")
|
||||
embeddings = OllamaEmbeddings(
|
||||
model=os.getenv("EMBEDDING_MODEL"),
|
||||
base_url=os.getenv("OLLAMA_HOST"),
|
||||
)
|
||||
|
||||
batch_size = 64
|
||||
total_batches = (len(texts) + batch_size - 1) // batch_size
|
||||
|
||||
all_embeddings = []
|
||||
with tqdm(
|
||||
total=len(texts),
|
||||
desc="Creating vectors",
|
||||
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
|
||||
) as pbar:
|
||||
for i in range(0, len(texts), batch_size):
|
||||
batch = texts[i : i + batch_size]
|
||||
try:
|
||||
batch_embeddings = embeddings.embed_documents(batch)
|
||||
all_embeddings.extend(batch_embeddings)
|
||||
pbar.update(len(batch))
|
||||
except Exception as e:
|
||||
print(
|
||||
f"{Fore.RED}Error creating embeddings for batch {i//batch_size + 1}/{total_batches}: {e}{Style.RESET_ALL}"
|
||||
)
|
||||
raise
|
||||
|
||||
print(f"{Fore.CYAN}Creating/updating FAISS index...{Style.RESET_ALL}")
|
||||
try:
|
||||
if existing_db is None:
|
||||
db = FAISS.from_embeddings(
|
||||
text_embeddings=list(zip(texts, all_embeddings)),
|
||||
embedding=embeddings,
|
||||
metadatas=metadatas,
|
||||
)
|
||||
else:
|
||||
db = existing_db
|
||||
db.add_embeddings(
|
||||
text_embeddings=list(zip(texts, all_embeddings)), metadatas=metadatas
|
||||
)
|
||||
|
||||
print(
|
||||
f"{Fore.CYAN}Saving vector store to: {vector_store_path}{Style.RESET_ALL}"
|
||||
)
|
||||
db.save_local(vector_store_path)
|
||||
if os.path.exists(vector_store_path):
|
||||
print(
|
||||
f"{Fore.GREEN}Vector store successfully saved at: {vector_store_path}{Style.RESET_ALL}"
|
||||
)
|
||||
print(
|
||||
f"{Fore.CYAN}Vector store size: {sum(os.path.getsize(os.path.join(vector_store_path, f)) for f in os.listdir(vector_store_path)) / (1024*1024):.2f} MB{Style.RESET_ALL}"
|
||||
)
|
||||
return db
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}Error with FAISS index: {e}{Style.RESET_ALL}")
|
||||
raise
|
||||
|
||||
|
||||
def get_all_mans():
|
||||
# Load previous progress
|
||||
progress = load_progress()
|
||||
processed_binaries = progress["processed_binaries"]
|
||||
|
||||
binaries = get_binaries()
|
||||
remaining_binaries = [b for b in binaries if b not in processed_binaries]
|
||||
|
||||
if not remaining_binaries:
|
||||
print(f"{Fore.GREEN}All binaries already processed!{Style.RESET_ALL}")
|
||||
if progress["vector_store_exists"]:
|
||||
embeddings = OllamaEmbeddings(
|
||||
model=os.getenv("EMBEDDING_MODEL"),
|
||||
base_url=os.getenv("OLLAMA_HOST"),
|
||||
)
|
||||
return FAISS.load_local("vector_store", embeddings)
|
||||
|
||||
texts = []
|
||||
metadatas = []
|
||||
|
||||
# Load existing vector store if it exists
|
||||
existing_db = None
|
||||
if progress["vector_store_exists"]:
|
||||
try:
|
||||
embeddings = OllamaEmbeddings(
|
||||
model=os.getenv("EMBEDDING_MODEL"),
|
||||
base_url=os.getenv("OLLAMA_HOST"),
|
||||
)
|
||||
existing_db = FAISS.load_local("vector_store", embeddings)
|
||||
print(f"{Fore.GREEN}Loaded existing vector store{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(
|
||||
f"{Fore.YELLOW}Could not load existing vector store: {e}{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
# Text splitter for chunking
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", " ", ""]
|
||||
)
|
||||
|
||||
print(f"{Fore.CYAN}Processing remaining man pages...{Style.RESET_ALL}")
|
||||
try:
|
||||
for binary in tqdm(
|
||||
remaining_binaries,
|
||||
desc="Reading man pages",
|
||||
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
|
||||
):
|
||||
man_content = ""
|
||||
if os.path.exists(f"mans/{binary}.man"):
|
||||
with open(f"mans/{binary}.man", "r") as file:
|
||||
man_content = file.read()
|
||||
else:
|
||||
man_page = subprocess.run(
|
||||
["man", binary], capture_output=True, text=True
|
||||
)
|
||||
man_content = man_page.stdout
|
||||
with open(f"mans/{binary}.man", "w") as file:
|
||||
file.write(man_content)
|
||||
|
||||
if man_content.strip():
|
||||
chunks = text_splitter.split_text(man_content)
|
||||
texts.extend(chunks)
|
||||
metadatas.extend(
|
||||
[
|
||||
{"binary": binary, "chunk": i, "total_chunks": len(chunks)}
|
||||
for i in range(len(chunks))
|
||||
]
|
||||
)
|
||||
|
||||
# Save progress after each binary
|
||||
processed_binaries.append(binary)
|
||||
save_progress(processed_binaries, progress["vector_store_exists"])
|
||||
|
||||
# Create embeddings in smaller batches
|
||||
if len(texts) >= 100: # Process every 100 documents
|
||||
db = create_embeddings_db(texts, metadatas, existing_db)
|
||||
existing_db = db
|
||||
texts = []
|
||||
metadatas = []
|
||||
progress["vector_store_exists"] = True
|
||||
save_progress(processed_binaries, True)
|
||||
|
||||
# Process any remaining texts
|
||||
if texts:
|
||||
db = create_embeddings_db(texts, metadatas, existing_db)
|
||||
elif existing_db:
|
||||
db = existing_db
|
||||
|
||||
save_progress(processed_binaries, True)
|
||||
return db
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(
|
||||
f"\n{Fore.YELLOW}Process interrupted! Progress has been saved.{Style.RESET_ALL}"
|
||||
)
|
||||
if texts:
|
||||
print(f"{Fore.YELLOW}Saving current batch...{Style.RESET_ALL}")
|
||||
try:
|
||||
db = create_embeddings_db(texts, metadatas, existing_db)
|
||||
save_progress(processed_binaries, True)
|
||||
print(f"{Fore.GREEN}Current batch saved successfully!{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}Could not save current batch: {e}{Style.RESET_ALL}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
db = get_all_mans()
|
||||
print(f"{Fore.GREEN}✓ Created embeddings database{Style.RESET_ALL}")
|
69
libs/ollama_client.py
Normal file
69
libs/ollama_client.py
Normal file
@ -0,0 +1,69 @@
|
||||
import os
|
||||
import dotenv
|
||||
from ollama import Client
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_ollama import OllamaEmbeddings
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
def load_vector_store():
|
||||
embeddings = OllamaEmbeddings(
|
||||
model=os.getenv("EMBEDDING_MODEL"),
|
||||
base_url=os.getenv("OLLAMA_HOST"),
|
||||
)
|
||||
return FAISS.load_local(
|
||||
"vector_store", embeddings, allow_dangerous_deserialization=True
|
||||
)
|
||||
|
||||
|
||||
def ollama_client():
|
||||
return Client(
|
||||
host=os.getenv("OLLAMA_HOST"),
|
||||
)
|
||||
|
||||
|
||||
def ollama_chat(client: Client, prompt: str):
|
||||
db = load_vector_store()
|
||||
|
||||
# Get configurable parameters from environment
|
||||
k_similar = int(
|
||||
os.getenv("SIMILARITY_K", "5")
|
||||
) # Number of similar chunks to retrieve
|
||||
|
||||
# Search for relevant context
|
||||
relevant_chunks = db.similarity_search(prompt, k=k_similar)
|
||||
|
||||
context = "\n\n".join([doc.page_content for doc in relevant_chunks])
|
||||
|
||||
return client.chat(
|
||||
model=os.getenv("OLLAMA_MODEL"),
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": """You are a command-line tool expert. Your role is to suggest the most appropriate command-line tools based on the user's needs.
|
||||
|
||||
Rules:
|
||||
1. ONLY suggest tools that are explicitly mentioned in the provided context
|
||||
2. Start with the most fundamental/common tool for the task
|
||||
3. Format your response as:
|
||||
Primary tool: [tool name] - [brief description]
|
||||
Usage: [basic usage example]
|
||||
|
||||
Related tools:
|
||||
- [alternative tool] - [why/when to use it]
|
||||
|
||||
4. If no suitable tool is found in the context, say "I don't see a suitable tool in my current knowledge base for that specific task"
|
||||
5. Be concise and direct
|
||||
6. Include specific, relevant details from the man pages
|
||||
7. Don't make assumptions about tools that aren't in the context
|
||||
|
||||
Here is the relevant documentation context:
|
||||
{}
|
||||
""".format(
|
||||
context
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
)
|
83
main.py
Normal file
83
main.py
Normal file
@ -0,0 +1,83 @@
|
||||
from libs.ollama_client import ollama_chat, ollama_client
|
||||
from libs.get_all_mans import get_all_mans
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
import sys
|
||||
from colorama import init, Fore, Style
|
||||
|
||||
# Initialize colorama for cross-platform colored output
|
||||
init()
|
||||
|
||||
|
||||
def check_vector_store():
|
||||
"""Check if vector store exists and is up to date"""
|
||||
if not os.path.exists("vector_store"):
|
||||
print(f"{Fore.YELLOW}Vector store not found{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
# Check if any man page is newer than vector store
|
||||
vector_store_time = datetime.fromtimestamp(
|
||||
os.path.getmtime("vector_store"), timezone.utc
|
||||
)
|
||||
mans_dir = "mans"
|
||||
|
||||
if not os.path.exists(mans_dir):
|
||||
print(f"{Fore.YELLOW}Man pages directory not found{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
for man_file in os.listdir(mans_dir):
|
||||
if man_file.endswith(".man"):
|
||||
man_time = datetime.fromtimestamp(
|
||||
os.path.getmtime(os.path.join(mans_dir, man_file)), timezone.utc
|
||||
)
|
||||
if man_time > vector_store_time:
|
||||
print(
|
||||
f"{Fore.YELLOW}Vector store outdated (newer man pages found){Style.RESET_ALL}"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def initialize_knowledge_base():
|
||||
"""Initialize or update the knowledge base with progress indicators"""
|
||||
if check_vector_store():
|
||||
print(f"{Fore.GREEN}✓ Vector store is up to date{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
print(f"{Fore.CYAN}Initializing knowledge base...{Style.RESET_ALL}")
|
||||
try:
|
||||
db = get_all_mans()
|
||||
print(f"{Fore.GREEN}✓ Successfully created vector store{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}Error creating vector store: {e}{Style.RESET_ALL}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
print(f"{Fore.CYAN}Welcome to suggestoor!{Style.RESET_ALL}")
|
||||
|
||||
# Initialize knowledge base
|
||||
initialize_knowledge_base()
|
||||
|
||||
print(f"\n{Fore.GREEN}Ready to help! Type your questions below.{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}(Press Ctrl+C to exit){Style.RESET_ALL}\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
prompt = input(f"{Fore.CYAN}Which tool do you need? {Style.RESET_ALL}")
|
||||
client = ollama_client()
|
||||
print(f"{Fore.YELLOW}Thinking...{Style.RESET_ALL}")
|
||||
response = ollama_chat(client, prompt)
|
||||
print(f"\n{Fore.GREEN}Answer:{Style.RESET_ALL}")
|
||||
print(response.message.content)
|
||||
print() # Empty line for readability
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Fore.YELLOW}Goodbye!{Style.RESET_ALL}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}Error: {e}{Style.RESET_ALL}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "suggestoor"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"colorama>=0.4.6",
|
||||
"langchain-ollama>=0.2.2",
|
||||
"langchain>=0.3.14",
|
||||
"langchain-community>=0.3.14",
|
||||
"ollama>=0.4.6",
|
||||
"python-dotenv>=1.0.1",
|
||||
"tqdm>=4.67.1",
|
||||
"faiss-gpu-cu12>=1.9.0.post1",
|
||||
]
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
colorama>=0.4.6
|
||||
langchain-ollama>=0.2.2
|
||||
langchain>=0.3.14
|
||||
langchain-community>=0.3.14
|
||||
ollama>=0.4.6
|
||||
python-dotenv>=1.0.1
|
||||
tqdm>=4.67.1
|
||||
faiss-gpu-cu12>=1.9.0.post1
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 528 KiB |
Loading…
x
Reference in New Issue
Block a user