243 lines
8.3 KiB
Python
243 lines
8.3 KiB
Python
import os
|
|
|
|
import gc
|
|
import re
|
|
import glob
|
|
import uuid
|
|
import textwrap
|
|
import subprocess
|
|
import nest_asyncio
|
|
from dotenv import load_dotenv
|
|
|
|
import streamlit as st
|
|
|
|
from llama_index.core import Settings
|
|
from llama_index.core import PromptTemplate
|
|
from llama_index.core import SimpleDirectoryReader
|
|
from llama_index.core import VectorStoreIndex
|
|
from llama_index.core.storage.storage_context import StorageContext
|
|
from llama_index.core.node_parser import CodeSplitter, MarkdownNodeParser
|
|
from llama_index.llms.openai import OpenAI
|
|
from llama_index.llms.anthropic import Anthropic
|
|
from llama_index.core.indices.vector_store.base import VectorStoreIndex
|
|
from llama_index.vector_stores.qdrant import QdrantVectorStore
|
|
from llama_index.embeddings.fastembed import FastEmbedEmbedding
|
|
import qdrant_client
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
# setting up the llm
|
|
@st.cache_resource
|
|
def load_llm(model_name, provider="openai"):
|
|
if provider == "anthropic":
|
|
return Anthropic(model=model_name)
|
|
elif provider == "openai":
|
|
return OpenAI(model=model_name)
|
|
else:
|
|
raise ValueError(f"Unsupported provider: {provider}")
|
|
|
|
|
|
# utility functions
|
|
def parse_github_url(url):
|
|
pattern = r"https://github\.com/([^/]+)/([^/]+)"
|
|
match = re.match(pattern, url)
|
|
return match.groups() if match else (None, None)
|
|
|
|
def clone_repo(repo_url):
|
|
return subprocess.run(["git", "clone", repo_url], check=True, text=True, capture_output=True)
|
|
|
|
|
|
def validate_owner_repo(owner, repo):
|
|
return bool(owner) and bool(repo)
|
|
|
|
def parse_docs_by_file_types(ext, language, input_dir_path):
|
|
"""Parse documents based on file extension"""
|
|
files = glob.glob(f"{input_dir_path}/**/*{ext}", recursive=True)
|
|
|
|
if len(files) > 0:
|
|
print(f"Found {len(files)} files with extension {ext}")
|
|
loader = SimpleDirectoryReader(
|
|
input_dir=input_dir_path, required_exts=[ext], recursive=True
|
|
)
|
|
docs = loader.load_data()
|
|
parser = (
|
|
MarkdownNodeParser()
|
|
if ext == ".md"
|
|
else CodeSplitter.from_defaults(language=language)
|
|
)
|
|
nodes = parser.get_nodes_from_documents(docs)
|
|
print(f"Processed {len(nodes)} nodes from {ext} files")
|
|
return nodes
|
|
return []
|
|
|
|
|
|
# create an qdrant collection and return an index
|
|
def create_index(nodes, client):
|
|
unique_collection_id = uuid.uuid4()
|
|
collection_name = f"chat_with_docs_{unique_collection_id}"
|
|
vector_store = QdrantVectorStore(client=client, collection_name=collection_name)
|
|
storage_context = StorageContext.from_defaults(vector_store=vector_store)
|
|
index = VectorStoreIndex(
|
|
nodes,
|
|
storage_context=storage_context,
|
|
)
|
|
return index
|
|
|
|
if "id" not in st.session_state:
|
|
st.session_state.id = uuid.uuid4()
|
|
st.session_state.file_cache = {}
|
|
|
|
session_id = st.session_state.id
|
|
client = None
|
|
|
|
|
|
def reset_chat():
|
|
st.session_state.messages = []
|
|
st.session_state.context = None
|
|
gc.collect()
|
|
|
|
|
|
with st.sidebar:
|
|
|
|
# Model selection
|
|
model_options = {
|
|
"OpenAI o3-mini": {"provider": "openai", "model": "o3-mini"},
|
|
"Claude 3.7 Sonnet": {"provider": "anthropic", "model": "claude-3-7-sonnet-20250219"}
|
|
}
|
|
selected_model = st.selectbox(
|
|
"Select Model",
|
|
options=list(model_options.keys()),
|
|
index=0
|
|
)
|
|
|
|
# Input for GitHub URL
|
|
github_url = st.text_input("GitHub Repository URL")
|
|
|
|
# Button to load and process the GitHub repository
|
|
process_button = st.button("Load")
|
|
|
|
message_container = st.empty() # Placeholder for dynamic messages
|
|
|
|
if process_button and github_url:
|
|
owner, repo = parse_github_url(github_url)
|
|
if validate_owner_repo(owner, repo):
|
|
with st.spinner(f"Loading {repo} repository by {owner}..."):
|
|
try:
|
|
input_dir_path = f"./{repo}"
|
|
|
|
if not os.path.exists(input_dir_path):
|
|
subprocess.run(["git", "clone", github_url], check=True, text=True, capture_output=True)
|
|
|
|
if os.path.exists(input_dir_path):
|
|
file_types = {
|
|
".md": "markdown",
|
|
".py": "python",
|
|
".ipynb": "python",
|
|
".js": "javascript",
|
|
".ts": "typescript"
|
|
}
|
|
|
|
nodes = []
|
|
for ext, language in file_types.items():
|
|
nodes += parse_docs_by_file_types(ext, language, input_dir_path)
|
|
else:
|
|
st.error('Error occurred while cloning the repository, carefully check the url')
|
|
st.stop()
|
|
|
|
# setting up the embedding model
|
|
Settings.embed_model = FastEmbedEmbedding(model_name="BAAI/bge-base-en-v1.5")
|
|
try:
|
|
index = create_index(nodes)
|
|
except:
|
|
index = VectorStoreIndex(nodes=nodes)
|
|
|
|
# ====== Setup a query engine ======
|
|
model_info = model_options[selected_model]
|
|
Settings.llm = load_llm(model_name=model_info["model"], provider=model_info["provider"])
|
|
query_engine = index.as_query_engine(streaming=True, similarity_top_k=4)
|
|
|
|
# ====== Customise prompt template ======
|
|
qa_prompt_tmpl_str = (
|
|
"Context information is below.\n"
|
|
"---------------------\n"
|
|
"{context_str}\n"
|
|
"---------------------\n"
|
|
"Given the context information and your knowledge, I want you to think step by step to answer the query in a crisp manner, incase case you don't know the answer say 'I don't know!'.\n"
|
|
"Query: {query_str}\n"
|
|
"Answer: "
|
|
)
|
|
qa_prompt_tmpl = PromptTemplate(qa_prompt_tmpl_str)
|
|
|
|
query_engine.update_prompts(
|
|
{"response_synthesizer:text_qa_template": qa_prompt_tmpl}
|
|
)
|
|
|
|
if nodes:
|
|
message_container.success("Data loaded successfully!!")
|
|
else:
|
|
message_container.write(
|
|
"No data found, check if the repository is not empty!"
|
|
)
|
|
st.session_state.query_engine = query_engine
|
|
|
|
except Exception as e:
|
|
st.error(f"An error occurred: {e}")
|
|
st.stop()
|
|
|
|
st.success("Ready to Chat!")
|
|
else:
|
|
st.error('Invalid owner or repository')
|
|
st.stop()
|
|
|
|
col1, col2 = st.columns([6, 1])
|
|
|
|
with col1:
|
|
st.header(f"Claude 3.7 Sonnet vs OpenAI o3! </>")
|
|
|
|
with col2:
|
|
st.button("Clear ↺", on_click=reset_chat)
|
|
|
|
|
|
# Initialize chat history
|
|
if "messages" not in st.session_state:
|
|
reset_chat()
|
|
|
|
|
|
# Display chat messages from history on app rerun
|
|
for message in st.session_state.messages:
|
|
with st.chat_message(message["role"]):
|
|
st.markdown(message["content"])
|
|
|
|
|
|
# Accept user input
|
|
if prompt := st.chat_input("What's up?"):
|
|
# Add user message to chat history
|
|
st.session_state.messages.append({"role": "user", "content": prompt})
|
|
# Display user message in chat message container
|
|
with st.chat_message("user"):
|
|
st.markdown(prompt)
|
|
|
|
# Display assistant response in chat message container
|
|
with st.chat_message("assistant"):
|
|
message_placeholder = st.empty()
|
|
full_response = ""
|
|
|
|
# context = st.session_state.context
|
|
query_engine = st.session_state.query_engine
|
|
|
|
# Simulate stream of response with milliseconds delay
|
|
streaming_response = query_engine.query(prompt)
|
|
|
|
for chunk in streaming_response.response_gen:
|
|
full_response += chunk
|
|
message_placeholder.markdown(full_response + "▌")
|
|
|
|
# full_response = query_engine.query(prompt)
|
|
|
|
message_placeholder.markdown(full_response)
|
|
# st.session_state.context = ctx
|
|
|
|
# Add assistant response to chat history
|
|
st.session_state.messages.append({"role": "assistant", "content": full_response}) |