diff --git a/ReadMe.md b/ReadMe.md index 8424e05..d483ec8 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,6 +1,6 @@ # Nous: Your Personal Knowledge Assistant 🧠💬 -Nous is an open-source personal knowledge assistant that allows you to interact with your personal documents and text files using natural language. It's designed with a focus on privacy, security, and local processing. +Nous is an open-source personal knowledge assistant that allows you to interact with your personal documents and text files using natural language. It's designed with a focus on privacy, security, and local processing. ## Demo 🎥 @@ -179,6 +179,124 @@ URLS = [ After adding your links, restart the Flask app to ingest the new documents into the vector database. +## 🔧 Configuration + +Nous uses two main configuration files: one for the Golang HTTP server and another for the Python Flask LLM service. + +### Golang HTTP Server Configuration + +Located at: `app/internal/config/config.go` + +The Golang server configuration manages various aspects of the web server, including paths, addresses, and connections. + +#### Configuration Structure + +```go +type Config struct { + StaticPath string + TemplatesPath string + ServerAddr string + DatabasePath string + LLMBaseURL string + RedisAddr string +} +``` + +#### Configuration Options + +- `StaticPath`: Path to static files (default: `"../static"` relative to the executable) +- `TemplatesPath`: Path to HTML templates (default: `"../templates/*"` relative to the executable) +- `ServerAddr`: Address and port for the HTTP server (default: `:8080`) +- `DatabasePath`: Path to the SQLite database file (default: `"./nous.db"`) +- `LLMBaseURL`: URL of the LLM service (default: `"http://localhost:5000"`) +- `RedisAddr`: Address of the Redis server (default: `"localhost:6379"`) + +#### Customizing the Configuration + +You can customize these settings using environment variables: + +1. `STATIC_PATH`: Set the path to static files +2. `TEMPLATES_PATH`: Set the path to HTML templates +3. `SERVER_ADDR`: Set the server address and port +4. `DATABASE_PATH`: Set the path to the SQLite database file +5. `LLM_BASE_URL`: Set the URL of the LLM service +6. `REDIS_ADDR`: Set the address of the Redis server + +Example: +```bash +export SERVER_ADDR=:8081 +``` + +### LLM Service Configuration + +Located at: `llm_api/config.py` + +The LLM service configuration controls the behavior of the Flask app that manages LLM interactions. + +#### Configuration Options + +1. **API Keys** + ```python + TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") + ``` + - Used for web search capabilities. Set this in your `.env` file. + +2. **Vectorstore Paths** + ```python + DATA_INDEX_PATH = os.getenv('DATA_INDEX_PATH', "data/faiss_index.bin") + DATA_VECTOR_PATH = os.getenv('DATA_VECTOR_PATH', "data/vectors.npy") + DATA_INDEX_TO_ID_PATH = os.getenv('DATA_INDEX_TO_ID_PATH', "data/index_to_id.npy") + ``` + - Determine where vectorstore data is saved and loaded from. + +3. **Ollama Configuration** + ```python + class OllamaConfig: + HOST = os.getenv('OLLAMA_HOST', 'http://localhost:11434') + EMBEDDINGS_MODEL = os.getenv('OLLAMA_EMBEDDINGS_MODEL', "nomic-embed-text") + MODEL_RAG = os.getenv('OLLAMA_MODEL_RAG', "phi3:mini") + MODEL_RETRIEVER_GRADER = os.getenv('OLLAMA_MODEL_RETRIEVER_GRADER', "thinyllama:mini") + ``` + - `HOST`: URL where Ollama is running + - `EMBEDDINGS_MODEL`: Model for generating embeddings + - `MODEL_RAG`: Model for Retrieval-Augmented Generation + - `MODEL_RETRIEVER_GRADER`: Model for grading retrieval results + +4. **Personal Knowledge Base Resources** + ```python + KNOWLEDGE_BASE_URLS = [ + "https://tip.golang.org/tour/concurrency.article", + "https://tip.golang.org/doc/effective_go", + "https://gosafir.com/mag/wp-content/uploads/2019/12/Tolkien-J.-The-lord-of-the-rings-HarperCollins-ebooks-2010.pdf", + "https://gist.github.com/silver-xu/1dcceaa14c4f0253d9637d4811948437", + ] + ``` + - List of URLs for documents to be ingested into the knowledge base. + +#### Customizing the Configuration + +To modify LLM service settings: + +1. Open `llm_api/config.py` in a text editor. +2. Adjust values as needed. +3. For environment variables, either: + - Set them in your system environment, or + - Create a `.env` file in the `llm_api` directory with the required key-value pairs. +4. Save the file and restart the LLM service. + +### Applying Configuration Changes + +After modifying any configuration: + +1. For the Golang server, rebuild and restart the server. +2. For the LLM service, restart the Flask app. +3. If using Docker, rebuild the containers: + ```bash + docker-compose up --build + ``` + +This ensures that all configuration changes are properly incorporated into the running services. + ## Docker Compose Configuration The `docker-compose.yml` file in the root directory contains the following services: diff --git a/app/internal/config/config.go b/app/internal/config/config.go index 698b859..32a215a 100644 --- a/app/internal/config/config.go +++ b/app/internal/config/config.go @@ -5,35 +5,47 @@ import ( "path/filepath" ) +// Config holds all configuration for the application type Config struct { - StaticPath string - TemplatesPath string - ServerAddr string - DatabasePath string - LLMBaseURL string - RedisAddr string + StaticPath string + TemplatesPath string + ServerAddr string + DatabasePath string + LLMBaseURL string + RedisAddr string } +// Load returns a new Config struct populated with values from environment variables or defaults func Load() (*Config, error) { - ex, err := os.Executable() - if err != nil { - return nil, err - } - exPath := filepath.Dir(ex) + exPath, err := getExecutablePath() + if err != nil { + return nil, err + } - return &Config{ - StaticPath: getEnv("STATIC_PATH", filepath.Join(exPath, "..", "static")), - TemplatesPath: getEnv("TEMPLATES_PATH", filepath.Join(exPath, "..", "templates", "*")), - ServerAddr: getEnv("SERVER_ADDR", ":8080"), - DatabasePath: getEnv("DATABASE_PATH", "./nous.db"), - LLMBaseURL: getEnv("LLM_BASE_URL", "http://localhost:5000"), - RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"), - }, nil + return &Config{ + StaticPath: getEnv("STATIC_PATH", filepath.Join(exPath, "..", "static")), + TemplatesPath: getEnv("TEMPLATES_PATH", filepath.Join(exPath, "..", "templates", "*")), + ServerAddr: getEnv("SERVER_ADDR", ":8080"), + DatabasePath: getEnv("DATABASE_PATH", "./nous.db"), + LLMBaseURL: getEnv("LLM_BASE_URL", "http://localhost:5000"), + RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"), + }, nil } +// getExecutablePath returns the directory of the current executable +func getExecutablePath() (string, error) { + ex, err := os.Executable() + if err != nil { + return "", err + } + return filepath.Dir(ex), nil +} + +// getEnv retrieves the value of the environment variable named by the key +// If the variable is not present, it returns the fallback value func getEnv(key, fallback string) string { - if value, exists := os.LookupEnv(key); exists { - return value - } - return fallback + if value, exists := os.LookupEnv(key); exists { + return value + } + return fallback } diff --git a/llm_api/config.py b/llm_api/config.py index 40e6152..74a5387 100644 --- a/llm_api/config.py +++ b/llm_api/config.py @@ -1,14 +1,37 @@ import os from dotenv import load_dotenv +# Load environment variables load_dotenv() +# API Keys TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") -URLS = [ +# Vectorstore paths +DATA_INDEX_PATH = os.getenv('DATA_INDEX_PATH', "data/faiss_index.bin") +DATA_VECTOR_PATH = os.getenv('DATA_VECTOR_PATH', "data/vectors.npy") +DATA_INDEX_TO_ID_PATH = os.getenv('DATA_INDEX_TO_ID_PATH', "data/index_to_id.npy") + +# Ollama configuration +class OllamaConfig: + HOST = os.getenv('OLLAMA_HOST', 'http://localhost:11434') + EMBEDDINGS_MODEL = os.getenv('OLLAMA_EMBEDDINGS_MODEL', "nomic-embed-text") + MODEL_RAG = os.getenv('OLLAMA_MODEL_RAG', "phi3:mini") + MODEL_RETRIEVER_GRADER = os.getenv('OLLAMA_MODEL_RETRIEVER_GRADER', "thinyllama:mini") + +# Personal Knowledge base resources +KNOWLEDGE_BASE_URLS = [ "https://tip.golang.org/tour/concurrency.article", "https://tip.golang.org/doc/effective_go", - # "https://socrates.acadiau.ca/courses/engl/rcunningham/resources/Shpe/Hamlet.pdf", - "https://gosafir.com/mag/wp-content/uploads/2019/12/Tolkien-J.-The-lord-of-the-rings-HarperCollins-ebooks-2010.pdf" + "https://gosafir.com/mag/wp-content/uploads/2019/12/Tolkien-J.-The-lord-of-the-rings-HarperCollins-ebooks-2010.pdf", "https://gist.github.com/silver-xu/1dcceaa14c4f0253d9637d4811948437", ] + +# Ensure all required environment variables are set +def validate_env_vars(): + required_vars = ["TAVILY_API_KEY"] + for var in required_vars: + if not os.getenv(var): + raise EnvironmentError(f"Missing required environment variable: {var}") + +validate_env_vars() diff --git a/llm_api/embeddings.py b/llm_api/embeddings.py index f12efb4..ec198ed 100644 --- a/llm_api/embeddings.py +++ b/llm_api/embeddings.py @@ -2,26 +2,26 @@ from langchain_community.embeddings import OllamaEmbeddings from langchain_community.docstore.in_memory import InMemoryDocstore import faiss +from llm_api.config import DATA_INDEX_PATH, DATA_INDEX_TO_ID_PATH, DATA_VECTOR_PATH, OllamaConfig import numpy as np import os from typing import TypedDict, List def create_vectorstore(documents: List[str], force_rebuild: bool = False) -> FAISS: - index_path = "data/faiss_index.bin" - vectors_path = "data/vectors.npy" - index_to_id_path = "data/index_to_id.npy" - + index_path = DATA_INDEX_PATH + vectors_path = DATA_VECTOR_PATH + index_to_id_path = DATA_INDEX_TO_ID_PATH + if not force_rebuild and os.path.exists(index_path) and os.path.exists(vectors_path) and os.path.exists(index_to_id_path): # Load existing index and vectors index = faiss.read_index(index_path) docstore_dict = np.load(vectors_path, allow_pickle=True).item() index_to_id = np.load(index_to_id_path, allow_pickle=True).item() - + # Create InMemoryDocstore from the loaded dictionary docstore = InMemoryDocstore(docstore_dict) - - ollama_host = os.getenv('OLLAMA_HOST', 'http://localhost:11434') - ollama_embeddings = OllamaEmbeddings(model="nomic-embed-text", base_url=ollama_host) + + ollama_embeddings = OllamaEmbeddings(model=OllamaConfig.EMBEDDINGS_MODEL, base_url=OllamaConfig.HOST) vectorstore = FAISS( embedding_function=ollama_embeddings, index=index, @@ -31,14 +31,13 @@ def create_vectorstore(documents: List[str], force_rebuild: bool = False) -> FAI print("Loaded existing vectorstore from disk.") else: # Create new vectorstore - ollama_host = os.getenv('OLLAMA_HOST', 'http://localhost:11434') - ollama_embeddings = OllamaEmbeddings(model="nomic-embed-text", base_url=ollama_host) + ollama_embeddings = OllamaEmbeddings(model=OllamaConfig.EMBEDDINGS_MODEL, base_url=OllamaConfig.HOST) vectorstore = FAISS.from_documents(documents, ollama_embeddings) - + # Save the index, vectors, and index_to_docstore_id separately faiss.write_index(vectorstore.index, index_path) np.save(vectors_path, vectorstore.docstore._dict) np.save(index_to_id_path, vectorstore.index_to_docstore_id) print("Created new vectorstore and saved to disk.") - - return vectorstore \ No newline at end of file + + return vectorstore diff --git a/llm_api/graph.py b/llm_api/graph.py index 2fe4b49..09dd41d 100644 --- a/llm_api/graph.py +++ b/llm_api/graph.py @@ -7,7 +7,7 @@ import logging import concurrent.futures -from config import TAVILY_API_KEY, URLS +from config import OllamaConfig, TAVILY_API_KEY, KNOWLEDGE_BASE_URLS from document_processor import load_and_split_documents from embeddings import create_vectorstore from retriever import create_retriever @@ -21,13 +21,13 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -doc_splits = load_and_split_documents(URLS) +doc_splits = load_and_split_documents(KNOWLEDGE_BASE_URLS) vectorstore = create_vectorstore(doc_splits, force_rebuild=False) retriever = create_retriever(vectorstore) -llm = create_llm(model="phi3:mini") +llm = create_llm(model=OllamaConfig.MODEL_RAG) rag_chain = rag_prompt | llm | StrOutputParser() -retrieval_grader = grading_prompt | create_llm(model="tinyllama", format="json") | JsonOutputParser() +retrieval_grader = grading_prompt | create_llm(model=OllamaConfig.MODEL_RETRIEVER_GRADER, format="json") | JsonOutputParser() web_search_tool = TavilySearchResults(api_key=TAVILY_API_KEY) diff --git a/llm_api/llm.py b/llm_api/llm.py index 79827db..c9b629a 100644 --- a/llm_api/llm.py +++ b/llm_api/llm.py @@ -2,12 +2,13 @@ from langchain_ollama import ChatOllama from functools import lru_cache +from llm_api.config import OLLAMA_HOST + @lru_cache(maxsize=2) def create_llm(model="llama3.1", temperature=0, format=''): - ollama_host = os.getenv('OLLAMA_HOST', 'http://localhost:11434') return ChatOllama( - model=model, - temperature=temperature, + model=model, + temperature=temperature, format=format, - base_url=ollama_host - ) \ No newline at end of file + base_url=OLLAMA_HOST + )