commit - bbb301914b711755897a6000f8f68aea62bb8a2e
commit + 0762a220875890223346c1e2d17c867524ec8680
blob - 5d850b59c2c32db1e9c1152b34ca49ae97d17a48
blob + 0f2d9245df28f973652682991fb7e648086a2e72
--- .gitignore
+++ .gitignore
.idea
venv
backup
-chroma
-static/files/*
\ No newline at end of file
+chroma/*
+static/files/*
blob - /dev/null
blob + 2a78adbcc6f879f78cc1ecdc6121d01d77a45982 (mode 644)
--- /dev/null
+++ CHANGELOG.md
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+### Added
+- Environment variable support for configuration
+- Structured citation system in responses
+- Message-based prompt template system
+- New Ollama-related configuration variables
+- Enhanced CSS styling for better user experience
+
+### Changed
+- Updated context formatting to include document metadata
+- Simplified query response structure
+- Improved file path handling in citations
+- Enhanced UI responsiveness
+- Streamlined response handling in interface
+
+### Removed
+- Separate references field from response structure
+- Reset method from RagBackend class
+
+### Fixed
+- Path specifications in .gitignore
+- CSS formatting and styling issues
\ No newline at end of file
blob - 16a934cd6277ab0d51f850ea3e82d6dc805a8120
blob + 27a1fb414f93866c7a12fe643e52e7efe1a0d287
--- configuration.py
+++ configuration.py
+import os
+
from langchain_ollama import OllamaEmbeddings
# default paths
-DB_PATH: str = "chroma"
-FILE_PATH: str = "static/files"
+DB_PATH: str = os.getenv("RAG_DB_PATH", "chroma")
+FILE_PATH: str = os.getenv("RAG_STATIC_FILES", "static/files")
+# Ollama address
+OLLAMA_URL = os.getenv('RAG_OLLAMA_SERVER_URL', "http://localhost:11434")
+OLLAMA_MODEL = os.getenv('RAG_OLLAMA_MODEL', "llama3")
+OLLAMA_EMBEDDINGS_MODEL = os.getenv('RAG_OLLAMA_EMBEDDINGS_MODEL', "nomic-embed-text")
-# prompt template
-PROMPT_TEMPLATE: str = """
-Do not include any introductory or closing remarks.
-Use Markdown for all formatting (e.g., bold, italics, code blocks, lists, links).
-If there is not definitive answer or the the question is unclear, ask questions to narrow down on the answer.
-Answer the question (section QUESTION) based only on the following context (section CONTEXT) and the history of our conversation
-(section HISTORY:
+# prompt templates
+SYSTEM_PROMPT: str = r"""
+* Do not include introductory or closing remarks.
+* You are acting as a sparing partner for a roleplaying game master.
+* Answer the question using only the contents of the CONTEXT and HISTORY sections. Do not mention the sections.
+* Use Markdown for all formatting (e.g., bold, italics, code blocks, lists, links).
+* If the question is unclear, ask clarifying questions.
+IMPORTANT:
+Each document in the CONTEXT section starts with `DOCUMENT: ` followed by the document ID.
+It is followed by an empty line and the document text.
+An example of a document is:
+```
+---
+DOCUMENT: some-file.pdf:1
+
+Some text of the document
+```
+
+List the document IDs from the CONTEXT section that were used in the answer at the end.
+Never include the same document ID twice.
+An example of the sources section is:
+```
+### Sources:
+* some-file.pdf:1
+```
+
+"""
+HUMAN_TEMPLATE: str = r"""
# CONTEXT
{context}
{history}
# QUESTION
-You are acting as a sparing partner for a roleplaying game master.
-Answer the question based on the above context and history: {question}
+Answer the question based on the above context and history:
+{question}
"""
def embeddings():
- embeddings = OllamaEmbeddings(model="nomic-embed-text")
+ embeddings = OllamaEmbeddings(
+ base_url=OLLAMA_URL,
+ model=OLLAMA_EMBEDDINGS_MODEL,
+ # temperature=0.0,
+ )
return embeddings
blob - e6c9ee3b06423ff23974eeb7819fe41f4a89d705
blob + 41c204b908758b6918f49614f651c5a3c53f5e78
--- rag_backend.py
+++ rag_backend.py
from langchain.prompts import ChatPromptTemplate
from langchain_community.document_loaders import TextLoader, PyPDFDirectoryLoader
from langchain_core.documents import Document
+from langchain_core.messages import SystemMessage
+from langchain_core.prompts import HumanMessagePromptTemplate
from langchain_ollama import OllamaLLM
from langchain_text_splitters import RecursiveCharacterTextSplitter
-from configuration import embeddings, DB_PATH, PROMPT_TEMPLATE
+from configuration import embeddings, DB_PATH, HUMAN_TEMPLATE, OLLAMA_MODEL, OLLAMA_URL, SYSTEM_PROMPT
class RagBackend:
- def __init__(self, db_path: str = None):
- self.db_path = db_path if db_path else DB_PATH
+ def __init__(self, db_path: str = DB_PATH, ollama_url: str = OLLAMA_URL, model=OLLAMA_MODEL):
self.db = Chroma(
- persist_directory=self.db_path,
+ persist_directory=db_path,
embedding_function=embeddings(),
client_settings=Settings(anonymized_telemetry=False)
)
- self.model = OllamaLLM(model="llama3")
+ self.model = OllamaLLM(base_url=ollama_url, model=model)
def query(self, query_text: str, history: str) -> (str, list[str]):
- # look up possible context from the index
context_docs = self.db.similarity_search_with_score(query_text, k=5)
- context_text = "\n\n---\n\n".join([doc.page_content for doc, _score in context_docs])
- prompt_template = ChatPromptTemplate.from_template(PROMPT_TEMPLATE)
+ # Format context from filtered documents
+ context_text = "\n\n---\n".join([
+ f"DOCUMENT: {':'.join(doc.metadata.get('id').split(':')[:2])}\n\n{doc.page_content}" for
+ doc, _score in context_docs])
+
+ prompt_template = ChatPromptTemplate.from_messages([
+ SystemMessage(content=SYSTEM_PROMPT),
+ HumanMessagePromptTemplate.from_template(HUMAN_TEMPLATE)])
prompt = prompt_template.format(context=context_text, history=history, question=query_text)
+ # print("================")
+ # print(prompt)
+ # print("================")
response_text = self.model.invoke(prompt)
+ # print("----------------")
+ # print(response_text)
+ # print("----------------")
+ return response_text
- sources = [doc.metadata.get("id", None) for doc, _score in context_docs]
- return [response_text, sources]
-
@staticmethod
def load_pdf_documents(path: str) -> list[Document]:
return PyPDFDirectoryLoader(Path(path)).load()
current_chunk_index = 0
for chunk in chunks:
- source = chunk.metadata.get("source")
+ source = Path(chunk.metadata.get("source")).name
page = chunk.metadata.get("page")
current_page_id = f"{source}:{page}"
return chunks
- def reset(self) -> None:
- self.db.reset_collection()
-
if __name__ == "__main__":
logging.basicConfig(
level=logging.INFO,
blob - fc8369a5e6dd63413cfa96ed393d27f6ba8dda21
blob + 1df8c9a7c238359be87915a3bece49b191f1f4a7
--- rag_indexer.py
+++ rag_indexer.py
import argparse
import logging
+import shutil
import sys
from configuration import DB_PATH
parser.print_help()
sys.exit(1)
- indexer = RagBackend(args.db)
if args.reset:
- logging.info("deleting RAG indexer collection")
- indexer.reset()
- logging.info("indexer collection deleted")
+ try:
+ shutil.rmtree(DB_PATH)
+ logging.info("database deleted")
+ except FileNotFoundError:
+ pass
+ indexer = RagBackend(args.db)
for source in args.sources:
logging.info(f"searching {source}")
pdf_docs = indexer.load_pdf_documents(source)
+ pdf_docs = indexer.split_documents(pdf_docs)
indexer.add_to_index(pdf_docs)
text_docs = indexer.load_text_documents(source)
+ text_docs = indexer.split_documents(text_docs)
indexer.add_to_index(text_docs)
logging.info(f"added {len(text_docs)} text documents to index")
blob - 084a7fa2fb50bb73d3c1cc670e39ce79417e01c4
blob + 6488fa41e5750ac622af34ae8b0c9c30d553a31d
--- rag_interface.py
+++ rag_interface.py
params = data.get('params', {})
# Dispatch to the correct tool
if method == "llm_chat":
- result, refs = rag.query(params.get('query', ''), params.get('history', ''))
+ result = rag.query(params.get('query', ''), params.get('history', ''))
return jsonify({
"jsonrpc": "2.0",
"result": {
- 'text': result,
- 'ref': refs
+ 'text': result
},
"id": data.get("id")
})
blob - c51160a44bcbdf3037f850b08e4a5a7d08efbc4b
blob + a668a2e62eb42faa91582421e5e2abae6bed247b
--- requirements.txt
+++ requirements.txt
pypdf
+langchain
langchain-community
langchain-ollama
langchain-chroma
flask
chromadb
-pytest
-mcp[cli]
\ No newline at end of file
+pytest
\ No newline at end of file
blob - 8a95ebf26866fb6afc2dc76c1f1a36e26976fe26
blob + 88329261c0ecff8190c540617f5e4ab7ed601a0c
--- templates/page.html
+++ templates/page.html
background: #f4f7fa;
box-sizing: border-box;
}
+
#container {
height: 100vh;
width: 100vw;
flex-direction: column;
background: #fff;
}
+
#chat-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
}
+
#header {
background: #4a90e2;
color: #fff;
font-size: 1.3em;
letter-spacing: 1px;
}
+
#chat {
flex: 1 1 auto;
padding: 32px 24px;
flex-direction: column;
gap: 10px;
}
+
.msg {
display: flex;
margin-bottom: 8px;
}
+
.msg.user {
justify-content: flex-end;
}
+
.msg.bot {
justify-content: flex-start;
}
+
.bubble {
padding: 12px 16px;
border-radius: 18px;
max-width: 70%;
word-wrap: break-word;
}
+
.bubble.user {
background: #e1f5fe;
color: #222;
border-bottom-right-radius: 4px;
}
+
.bubble.bot {
background: #f1f0f0;
color: #222;
border-bottom-left-radius: 4px;
}
+
#input-area {
display: flex;
border-top: 1px solid #eee;
padding: 16px 24px;
background: #fafbfc;
}
+
#input {
flex: 1;
padding: 10px;
line-height: 1.2;
box-sizing: border-box;
}
+
#send {
margin-left: 10px;
padding: 10px 20px;
font-size: 1em;
cursor: pointer;
}
+
#send:disabled {
background: #a0c8f0;
cursor: not-allowed;
}
+
#spinner {
display: none;
margin-left: 10px;
align-self: center;
}
+
.lds-ring {
display: inline-block;
position: relative;
width: 24px;
height: 24px;
}
+
.lds-ring div {
box-sizing: border-box;
display: block;
animation: lds-ring 1.2s linear infinite;
border-color: #4a90e2 transparent transparent transparent;
}
- .lds-ring div:nth-child(1) { animation-delay: -0.45s; }
- .lds-ring div:nth-child(2) { animation-delay: -0.3s; }
- .lds-ring div:nth-child(3) { animation-delay: -0.15s; }
+
+ .lds-ring div:nth-child(1) {
+ animation-delay: -0.45s;
+ }
+
+ .lds-ring div:nth-child(2) {
+ animation-delay: -0.3s;
+ }
+
+ .lds-ring div:nth-child(3) {
+ animation-delay: -0.15s;
+ }
+
@keyframes lds-ring {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
}
+
@media (max-width: 700px) {
#container, #chat-container {
height: 100vh;
width: 100vw;
}
+
#chat {
padding: 16px 8px;
}
+
#input-area {
padding: 12px 8px;
}
const send = document.getElementById('send');
const spinner = document.getElementById('spinner');
- function appendMessage(sender, text) {
+ function appendMessage(sender, text, references = []) {
const msgDiv = document.createElement('div');
msgDiv.className = 'msg ' + sender;
const bubble = document.createElement('div');
const requestPayload = {
jsonrpc: "2.0",
method: "llm_chat",
- params: { query: query, history: historyText },
+ params: {query: query, history: historyText},
id: requestId
};
const response = await fetch('/mcp', {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ headers: {'Content-Type': 'application/json'},
body: JSON.stringify(requestPayload)
});
return data.result;
}
- send.onclick = async function() {
+ send.onclick = async function () {
const text = input.value.trim();
if (!text) return;
appendMessage('user', text);
try {
const result = await sendMCPMessage(text, context_prompt);
appendMessage('bot', result.text);
- history.push({ question: text, answer: result.text });
+ history.push({question: text, answer: result.text});
} catch (error) {
appendMessage('bot', `Error: ${error.message}`);
} finally {
// input.addEventListener("keyup", function(event) {
// if (event.key === "Enter" && !send.disabled) send.click();
// });
- input.addEventListener("keydown", function(event) {
+ input.addEventListener("keydown", function (event) {
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
- if(send.disabled) return;
+ if (send.disabled) return;
event.preventDefault();
send.click();
}
});
- input.addEventListener('input', function() {
+ input.addEventListener('input', function () {
this.style.height = 'auto'; // Reset height
this.style.height = (this.scrollHeight) + 'px'; // Set to scroll height
});
</script>
</body>
-</html>
+</html>
\ No newline at end of file