first commit

This commit is contained in:
tcsenpai 2025-01-17 14:29:02 +01:00
commit 775bfa7c56
11 changed files with 1572 additions and 0 deletions

13
.env.example Normal file
View 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
View 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
View File

@ -0,0 +1 @@
3.11

66
README.md Normal file
View File

@ -0,0 +1,66 @@
# Suggestoor
Suggestoor is a tool that helps you find the right command-line tools for your needs.
![suggestoor](./screenshot.png)
## 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

1073
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff