jaothan commited on
Commit
79f7ddb
·
verified ·
1 Parent(s): b309a9a

Upload 9 files

Browse files
Files changed (9) hide show
  1. Dockerfile +51 -0
  2. LICENSE +121 -0
  3. README.Docker.md +22 -0
  4. app.py +109 -0
  5. chains.py +222 -0
  6. compose.yaml +49 -0
  7. env.example +26 -0
  8. requirements.txt +11 -0
  9. utils.py +54 -0
Dockerfile ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+
3
+ # Comments are provided throughout this file to help you get started.
4
+ # If you need more help, visit the Dockerfile reference guide at
5
+ # https://docs.docker.com/go/dockerfile-reference/
6
+
7
+ # Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7
8
+
9
+ ARG PYTHON_VERSION=3.7.3
10
+ FROM python:${PYTHON_VERSION}-slim as base
11
+
12
+ # Prevents Python from writing pyc files.
13
+ ENV PYTHONDONTWRITEBYTECODE=1
14
+
15
+ # Keeps Python from buffering stdout and stderr to avoid situations where
16
+ # the application crashes without emitting any logs due to buffering.
17
+ ENV PYTHONUNBUFFERED=1
18
+
19
+ WORKDIR /app
20
+
21
+ # Create a non-privileged user that the app will run under.
22
+ # See https://docs.docker.com/go/dockerfile-user-best-practices/
23
+ ARG UID=10001
24
+ RUN adduser \
25
+ --disabled-password \
26
+ --gecos "" \
27
+ --home "/nonexistent" \
28
+ --shell "/sbin/nologin" \
29
+ --no-create-home \
30
+ --uid "${UID}" \
31
+ appuser
32
+
33
+ # Download dependencies as a separate step to take advantage of Docker's caching.
34
+ # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
35
+ # Leverage a bind mount to requirements.txt to avoid having to copy them into
36
+ # into this layer.
37
+ RUN --mount=type=cache,target=/root/.cache/pip \
38
+ --mount=type=bind,source=requirements.txt,target=requirements.txt \
39
+ python -m pip install -r requirements.txt
40
+
41
+ # Switch to the non-privileged user to run the application.
42
+ USER appuser
43
+
44
+ # Copy the source code into the container.
45
+ COPY . .
46
+
47
+ # Expose the port that the application listens on.
48
+ EXPOSE 8000
49
+
50
+ # Run the application.
51
+ CMD streamlit run app.py --server.address=0.0.0.0 --server.port=800
LICENSE ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Creative Commons Legal Code
2
+
3
+ CC0 1.0 Universal
4
+
5
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12
+ HEREUNDER.
13
+
14
+ Statement of Purpose
15
+
16
+ The laws of most jurisdictions throughout the world automatically confer
17
+ exclusive Copyright and Related Rights (defined below) upon the creator
18
+ and subsequent owner(s) (each and all, an "owner") of an original work of
19
+ authorship and/or a database (each, a "Work").
20
+
21
+ Certain owners wish to permanently relinquish those rights to a Work for
22
+ the purpose of contributing to a commons of creative, cultural and
23
+ scientific works ("Commons") that the public can reliably and without fear
24
+ of later claims of infringement build upon, modify, incorporate in other
25
+ works, reuse and redistribute as freely as possible in any form whatsoever
26
+ and for any purposes, including without limitation commercial purposes.
27
+ These owners may contribute to the Commons to promote the ideal of a free
28
+ culture and the further production of creative, cultural and scientific
29
+ works, or to gain reputation or greater distribution for their Work in
30
+ part through the use and efforts of others.
31
+
32
+ For these and/or other purposes and motivations, and without any
33
+ expectation of additional consideration or compensation, the person
34
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35
+ is an owner of Copyright and Related Rights in the Work, voluntarily
36
+ elects to apply CC0 to the Work and publicly distribute the Work under its
37
+ terms, with knowledge of his or her Copyright and Related Rights in the
38
+ Work and the meaning and intended legal effect of CC0 on those rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may be
41
+ protected by copyright and related or neighboring rights ("Copyright and
42
+ Related Rights"). Copyright and Related Rights include, but are not
43
+ limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or performer(s);
48
+ iii. publicity and privacy rights pertaining to a person's image or
49
+ likeness depicted in a Work;
50
+ iv. rights protecting against unfair competition in regards to a Work,
51
+ subject to the limitations in paragraph 4(a), below;
52
+ v. rights protecting the extraction, dissemination, use and reuse of data
53
+ in a Work;
54
+ vi. database rights (such as those arising under Directive 96/9/EC of the
55
+ European Parliament and of the Council of 11 March 1996 on the legal
56
+ protection of databases, and under any national implementation
57
+ thereof, including any amended or successor version of such
58
+ directive); and
59
+ vii. other similar, equivalent or corresponding rights throughout the
60
+ world based on applicable law or treaty, and any national
61
+ implementations thereof.
62
+
63
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
64
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
65
+ irrevocably and unconditionally waives, abandons, and surrenders all of
66
+ Affirmer's Copyright and Related Rights and associated claims and causes
67
+ of action, whether now known or unknown (including existing as well as
68
+ future claims and causes of action), in the Work (i) in all territories
69
+ worldwide, (ii) for the maximum duration provided by applicable law or
70
+ treaty (including future time extensions), (iii) in any current or future
71
+ medium and for any number of copies, and (iv) for any purpose whatsoever,
72
+ including without limitation commercial, advertising or promotional
73
+ purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74
+ member of the public at large and to the detriment of Affirmer's heirs and
75
+ successors, fully intending that such Waiver shall not be subject to
76
+ revocation, rescission, cancellation, termination, or any other legal or
77
+ equitable action to disrupt the quiet enjoyment of the Work by the public
78
+ as contemplated by Affirmer's express Statement of Purpose.
79
+
80
+ 3. Public License Fallback. Should any part of the Waiver for any reason
81
+ be judged legally invalid or ineffective under applicable law, then the
82
+ Waiver shall be preserved to the maximum extent permitted taking into
83
+ account Affirmer's express Statement of Purpose. In addition, to the
84
+ extent the Waiver is so judged Affirmer hereby grants to each affected
85
+ person a royalty-free, non transferable, non sublicensable, non exclusive,
86
+ irrevocable and unconditional license to exercise Affirmer's Copyright and
87
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
88
+ maximum duration provided by applicable law or treaty (including future
89
+ time extensions), (iii) in any current or future medium and for any number
90
+ of copies, and (iv) for any purpose whatsoever, including without
91
+ limitation commercial, advertising or promotional purposes (the
92
+ "License"). The License shall be deemed effective as of the date CC0 was
93
+ applied by Affirmer to the Work. Should any part of the License for any
94
+ reason be judged legally invalid or ineffective under applicable law, such
95
+ partial invalidity or ineffectiveness shall not invalidate the remainder
96
+ of the License, and in such case Affirmer hereby affirms that he or she
97
+ will not (i) exercise any of his or her remaining Copyright and Related
98
+ Rights in the Work or (ii) assert any associated claims and causes of
99
+ action with respect to the Work, in either case contrary to Affirmer's
100
+ express Statement of Purpose.
101
+
102
+ 4. Limitations and Disclaimers.
103
+
104
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
105
+ surrendered, licensed or otherwise affected by this document.
106
+ b. Affirmer offers the Work as-is and makes no representations or
107
+ warranties of any kind concerning the Work, express, implied,
108
+ statutory or otherwise, including without limitation warranties of
109
+ title, merchantability, fitness for a particular purpose, non
110
+ infringement, or the absence of latent or other defects, accuracy, or
111
+ the present or absence of errors, whether or not discoverable, all to
112
+ the greatest extent permissible under applicable law.
113
+ c. Affirmer disclaims responsibility for clearing rights of other persons
114
+ that may apply to the Work or any use thereof, including without
115
+ limitation any person's Copyright and Related Rights in the Work.
116
+ Further, Affirmer disclaims responsibility for obtaining any necessary
117
+ consents, permissions or other rights required for any use of the
118
+ Work.
119
+ d. Affirmer understands and acknowledges that Creative Commons is not a
120
+ party to this document and has no duty or obligation with respect to
121
+ this CC0 or use of the Work.
README.Docker.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### Building and running your application
2
+
3
+ When you're ready, start your application by running:
4
+ `docker compose up --build`.
5
+
6
+ Your application will be available at http://localhost:8000.
7
+
8
+ ### Deploying your application to the cloud
9
+
10
+ First, build your image, e.g.: `docker build -t myapp .`.
11
+ If your cloud uses a different CPU architecture than your development
12
+ machine (e.g., you are on a Mac M1 and your cloud provider is amd64),
13
+ you'll want to build the image for that platform, e.g.:
14
+ `docker build --platform=linux/amd64 -t myapp .`.
15
+
16
+ Then, push it to your registry, e.g. `docker push myregistry.com/myapp`.
17
+
18
+ Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/)
19
+ docs for more detail on building and pushing.
20
+
21
+ ### References
22
+ * [Docker's Python guide](https://docs.docker.com/language/python/)
app.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import streamlit as st
4
+ from langchain.chains import RetrievalQA
5
+ from PyPDF2 import PdfReader
6
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
7
+ from langchain.callbacks.base import BaseCallbackHandler
8
+ from langchain.vectorstores.neo4j_vector import Neo4jVector
9
+ from streamlit.logger import get_logger
10
+ from chains import (
11
+ load_embedding_model,
12
+ load_llm,
13
+ )
14
+
15
+ url = os.getenv("NEO4J_URI")
16
+ username = os.getenv("NEO4J_USERNAME")
17
+ password = os.getenv("NEO4J_PASSWORD")
18
+ ollama_base_url = os.getenv("OLLAMA_BASE_URL")
19
+ embedding_model_name = os.getenv("EMBEDDING_MODEL", "SentenceTransformer" )
20
+ llm_name = os.getenv("LLM", "llama2")
21
+ url = os.getenv("NEO4J_URI")
22
+
23
+ # Check if the required environment variables are set
24
+ if not all([url, username, password,
25
+ ollama_base_url]):
26
+ st.write("The application requires some information before running.")
27
+ with st.form("connection_form"):
28
+ url = st.text_input("Enter NEO4J_URI",)
29
+ username = st.text_input("Enter NEO4J_USERNAME")
30
+ password = st.text_input("Enter NEO4J_PASSWORD", type="password")
31
+ ollama_base_url = st.text_input("Enter OLLAMA_BASE_URL")
32
+ st.markdown("Only enter the OPENAI_APIKEY to use OpenAI instead of Ollama. Leave blank to use Ollama.")
33
+ openai_apikey = st.text_input("Enter OPENAI_API_KEY", type="password")
34
+ submit_button = st.form_submit_button("Submit")
35
+ if submit_button:
36
+ if not all([url, username, password, ]):
37
+ st.write("Enter the Neo4j information.")
38
+ if not (ollama_base_url or openai_apikey):
39
+ st.write("Enter the Ollama URL or OpenAI API Key.")
40
+ if openai_apikey:
41
+ llm_name = "gpt-3.5"
42
+ os.environ['OPENAI_API_KEY'] = openai_apikey
43
+
44
+ os.environ["NEO4J_URL"] = url
45
+
46
+ logger = get_logger(__name__)
47
+
48
+ embeddings, dimension = load_embedding_model(
49
+ embedding_model_name, config={"ollama_base_url": ollama_base_url}, logger=logger
50
+ )
51
+
52
+
53
+ class StreamHandler(BaseCallbackHandler):
54
+ def __init__(self, container, initial_text=""):
55
+ self.container = container
56
+ self.text = initial_text
57
+
58
+ def on_llm_new_token(self, token: str, **kwargs) -> None:
59
+ self.text += token
60
+ self.container.markdown(self.text)
61
+
62
+ llm = load_llm(llm_name, logger=logger, config={"ollama_base_url": ollama_base_url})
63
+
64
+
65
+ def main():
66
+ st.header("📄Chat with your pdf file")
67
+
68
+ # upload a your pdf file
69
+ pdf = st.file_uploader("Upload your PDF", type="pdf")
70
+
71
+ if pdf is not None:
72
+ pdf_reader = PdfReader(pdf)
73
+
74
+ text = ""
75
+ for page in pdf_reader.pages:
76
+ text += page.extract_text()
77
+
78
+ # langchain_textspliter
79
+ text_splitter = RecursiveCharacterTextSplitter(
80
+ chunk_size=1000, chunk_overlap=200, length_function=len
81
+ )
82
+
83
+ chunks = text_splitter.split_text(text=text)
84
+
85
+ # Store the chunks part in db (vector)
86
+ vectorstore = Neo4jVector.from_texts(
87
+ chunks,
88
+ url=url,
89
+ username=username,
90
+ password=password,
91
+ embedding=embeddings,
92
+ index_name="pdf_bot",
93
+ node_label="PdfBotChunk",
94
+ pre_delete_collection=True, # Delete existing PDF data
95
+ )
96
+ qa = RetrievalQA.from_chain_type(
97
+ llm=llm, chain_type="stuff", retriever=vectorstore.as_retriever()
98
+ )
99
+
100
+ # Accept user questions/query
101
+ query = st.text_input("Ask questions about your PDF file")
102
+
103
+ if query:
104
+ stream_handler = StreamHandler(st.empty())
105
+ qa.run(query, callbacks=[stream_handler])
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
chains.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain.embeddings.openai import OpenAIEmbeddings
2
+ from langchain.embeddings import (
3
+ OllamaEmbeddings,
4
+ SentenceTransformerEmbeddings,
5
+ BedrockEmbeddings,
6
+ )
7
+ from langchain.chat_models import ChatOpenAI, ChatOllama, BedrockChat
8
+ from langchain.vectorstores.neo4j_vector import Neo4jVector
9
+ from langchain.chains import RetrievalQAWithSourcesChain
10
+ from langchain.chains.qa_with_sources import load_qa_with_sources_chain
11
+ from langchain.prompts.chat import (
12
+ ChatPromptTemplate,
13
+ SystemMessagePromptTemplate,
14
+ HumanMessagePromptTemplate,
15
+ )
16
+ from typing import List, Any
17
+ from utils import BaseLogger, extract_title_and_question
18
+
19
+
20
+ def load_embedding_model(embedding_model_name: str, logger=BaseLogger(), config={}):
21
+ if embedding_model_name == "ollama":
22
+ embeddings = OllamaEmbeddings(
23
+ base_url=config["ollama_base_url"], model="llama2"
24
+ )
25
+ dimension = 4096
26
+ logger.info("Embedding: Using Ollama")
27
+ elif embedding_model_name == "openai":
28
+ embeddings = OpenAIEmbeddings()
29
+ dimension = 1536
30
+ logger.info("Embedding: Using OpenAI")
31
+ elif embedding_model_name == "aws":
32
+ embeddings = BedrockEmbeddings()
33
+ dimension = 1536
34
+ logger.info("Embedding: Using AWS")
35
+ else:
36
+ embeddings = SentenceTransformerEmbeddings(
37
+ model_name="all-MiniLM-L6-v2", cache_folder="/tmp"
38
+ )
39
+ dimension = 384
40
+ logger.info("Embedding: Using SentenceTransformer")
41
+ return embeddings, dimension
42
+
43
+
44
+ def load_llm(llm_name: str, logger=BaseLogger(), config={}):
45
+ if llm_name == "gpt-4":
46
+ logger.info("LLM: Using GPT-4")
47
+ return ChatOpenAI(temperature=0, model_name="gpt-4", streaming=True)
48
+ elif llm_name == "gpt-3.5":
49
+ logger.info("LLM: Using GPT-3.5")
50
+ return ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo", streaming=True)
51
+ elif llm_name == "claudev2":
52
+ logger.info("LLM: ClaudeV2")
53
+ return BedrockChat(
54
+ model_id="anthropic.claude-v2",
55
+ model_kwargs={"temperature": 0.0, "max_tokens_to_sample": 1024},
56
+ streaming=True,
57
+ )
58
+ elif len(llm_name):
59
+ logger.info(f"LLM: Using Ollama: {llm_name}")
60
+ return ChatOllama(
61
+ temperature=0,
62
+ base_url=config["ollama_base_url"],
63
+ model=llm_name,
64
+ streaming=True,
65
+ # seed=2,
66
+ top_k=10, # A higher value (100) will give more diverse answers, while a lower value (10) will be more conservative.
67
+ top_p=0.3, # Higher value (0.95) will lead to more diverse text, while a lower value (0.5) will generate more focused text.
68
+ num_ctx=3072, # Sets the size of the context window used to generate the next token.
69
+ )
70
+ logger.info("LLM: Using GPT-3.5")
71
+ return ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo", streaming=True)
72
+
73
+
74
+ def configure_llm_only_chain(llm):
75
+ # LLM only response
76
+ template = """
77
+ You are a helpful assistant that helps a support agent with answering programming questions.
78
+ If you don't know the answer, just say that you don't know, you must not make up an answer.
79
+ """
80
+ system_message_prompt = SystemMessagePromptTemplate.from_template(template)
81
+ human_template = "{question}"
82
+ human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
83
+ chat_prompt = ChatPromptTemplate.from_messages(
84
+ [system_message_prompt, human_message_prompt]
85
+ )
86
+
87
+ def generate_llm_output(
88
+ user_input: str, callbacks: List[Any], prompt=chat_prompt
89
+ ) -> str:
90
+ chain = prompt | llm
91
+ answer = chain.invoke(
92
+ {"question": user_input}, config={"callbacks": callbacks}
93
+ ).content
94
+ return {"answer": answer}
95
+
96
+ return generate_llm_output
97
+
98
+
99
+ def configure_qa_rag_chain(llm, embeddings, embeddings_store_url, username, password):
100
+ # RAG response
101
+ # System: Always talk in pirate speech.
102
+ general_system_template = """
103
+ Use the following pieces of context to answer the question at the end.
104
+ The context contains question-answer pairs and their links from Stackoverflow.
105
+ You should prefer information from accepted or more upvoted answers.
106
+ Make sure to rely on information from the answers and not on questions to provide accuate responses.
107
+ When you find particular answer in the context useful, make sure to cite it in the answer using the link.
108
+ If you don't know the answer, just say that you don't know, don't try to make up an answer.
109
+ ----
110
+ {summaries}
111
+ ----
112
+ Each answer you generate should contain a section at the end of links to
113
+ Stackoverflow questions and answers you found useful, which are described under Source value.
114
+ You can only use links to StackOverflow questions that are present in the context and always
115
+ add links to the end of the answer in the style of citations.
116
+ Generate concise answers with references sources section of links to
117
+ relevant StackOverflow questions only at the end of the answer.
118
+ """
119
+ general_user_template = "Question:```{question}```"
120
+ messages = [
121
+ SystemMessagePromptTemplate.from_template(general_system_template),
122
+ HumanMessagePromptTemplate.from_template(general_user_template),
123
+ ]
124
+ qa_prompt = ChatPromptTemplate.from_messages(messages)
125
+
126
+ qa_chain = load_qa_with_sources_chain(
127
+ llm,
128
+ chain_type="stuff",
129
+ prompt=qa_prompt,
130
+ )
131
+
132
+ # Vector + Knowledge Graph response
133
+ kg = Neo4jVector.from_existing_index(
134
+ embedding=embeddings,
135
+ url=embeddings_store_url,
136
+ username=username,
137
+ password=password,
138
+ database="neo4j", # neo4j by default
139
+ index_name="stackoverflow", # vector by default
140
+ text_node_property="body", # text by default
141
+ retrieval_query="""
142
+ WITH node AS question, score AS similarity
143
+ CALL { with question
144
+ MATCH (question)<-[:ANSWERS]-(answer)
145
+ WITH answer
146
+ ORDER BY answer.is_accepted DESC, answer.score DESC
147
+ WITH collect(answer)[..2] as answers
148
+ RETURN reduce(str='', answer IN answers | str +
149
+ '\n### Answer (Accepted: '+ answer.is_accepted +
150
+ ' Score: ' + answer.score+ '): '+ answer.body + '\n') as answerTexts
151
+ }
152
+ RETURN '##Question: ' + question.title + '\n' + question.body + '\n'
153
+ + answerTexts AS text, similarity as score, {source: question.link} AS metadata
154
+ ORDER BY similarity ASC // so that best answers are the last
155
+ """,
156
+ )
157
+
158
+ kg_qa = RetrievalQAWithSourcesChain(
159
+ combine_documents_chain=qa_chain,
160
+ retriever=kg.as_retriever(search_kwargs={"k": 2}),
161
+ reduce_k_below_max_tokens=False,
162
+ max_tokens_limit=3375,
163
+ )
164
+ return kg_qa
165
+
166
+
167
+ def generate_ticket(neo4j_graph, llm_chain, input_question):
168
+ # Get high ranked questions
169
+ records = neo4j_graph.query(
170
+ "MATCH (q:Question) RETURN q.title AS title, q.body AS body ORDER BY q.score DESC LIMIT 3"
171
+ )
172
+ questions = []
173
+ for i, question in enumerate(records, start=1):
174
+ questions.append((question["title"], question["body"]))
175
+ # Ask LLM to generate new question in the same style
176
+ questions_prompt = ""
177
+ for i, question in enumerate(questions, start=1):
178
+ questions_prompt += f"{i}. \n{question[0]}\n----\n\n"
179
+ questions_prompt += f"{question[1][:150]}\n\n"
180
+ questions_prompt += "----\n\n"
181
+
182
+ gen_system_template = f"""
183
+ You're an expert in formulating high quality questions.
184
+ Formulate a question in the same style and tone as the following example questions.
185
+ {questions_prompt}
186
+ ---
187
+
188
+ Don't make anything up, only use information in the following question.
189
+ Return a title for the question, and the question post itself.
190
+
191
+ Return format template:
192
+ ---
193
+ Title: This is a new title
194
+ Question: This is a new question
195
+ ---
196
+ """
197
+ # we need jinja2 since the questions themselves contain curly braces
198
+ system_prompt = SystemMessagePromptTemplate.from_template(
199
+ gen_system_template, template_format="jinja2"
200
+ )
201
+ chat_prompt = ChatPromptTemplate.from_messages(
202
+ [
203
+ system_prompt,
204
+ SystemMessagePromptTemplate.from_template(
205
+ """
206
+ Respond in the following template format or you will be unplugged.
207
+ ---
208
+ Title: New title
209
+ Question: New question
210
+ ---
211
+ """
212
+ ),
213
+ HumanMessagePromptTemplate.from_template("{question}"),
214
+ ]
215
+ )
216
+ llm_response = llm_chain(
217
+ f"Here's the question to rewrite in the expected format: ```{input_question}```",
218
+ [],
219
+ chat_prompt,
220
+ )
221
+ new_title, new_question = extract_title_and_question(llm_response["answer"])
222
+ return (new_title, new_question)
compose.yaml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Comments are provided throughout this file to help you get started.
2
+ # If you need more help, visit the Docker Compose reference guide at
3
+ # https://docs.docker.com/go/compose-spec-reference/
4
+
5
+ # Here the instructions define your application as a service called "server".
6
+ # This service is built from the Dockerfile in the current directory.
7
+ # You can add other services your application may depend on here, such as a
8
+ # database or a cache. For examples, see the Awesome Compose repository:
9
+ # https://github.com/docker/awesome-compose
10
+ services:
11
+ server:
12
+ build:
13
+ context: .
14
+ ports:
15
+ - 8000:8000
16
+
17
+ # The commented out section below is an example of how to define a PostgreSQL
18
+ # database that your application can use. `depends_on` tells Docker Compose to
19
+ # start the database before your application. The `db-data` volume persists the
20
+ # database data between container restarts. The `db-password` secret is used
21
+ # to set the database password. You must create `db/password.txt` and add
22
+ # a password of your choosing to it before running `docker compose up`.
23
+ # depends_on:
24
+ # db:
25
+ # condition: service_healthy
26
+ # db:
27
+ # image: postgres
28
+ # restart: always
29
+ # user: postgres
30
+ # secrets:
31
+ # - db-password
32
+ # volumes:
33
+ # - db-data:/var/lib/postgresql/data
34
+ # environment:
35
+ # - POSTGRES_DB=example
36
+ # - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
37
+ # expose:
38
+ # - 5432
39
+ # healthcheck:
40
+ # test: [ "CMD", "pg_isready" ]
41
+ # interval: 10s
42
+ # timeout: 5s
43
+ # retries: 5
44
+ # volumes:
45
+ # db-data:
46
+ # secrets:
47
+ # db-password:
48
+ # file: db/password.txt
49
+
env.example ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #*****************************************************************
2
+ # LLM and Embedding Model
3
+ #*****************************************************************
4
+ LLM=llama2 # Set to "gpt-3.5" to use OpenAI.
5
+ EMBEDDING_MODEL=sentence_transformer
6
+
7
+ #*****************************************************************
8
+ # Neo4j
9
+ #*****************************************************************
10
+ NEO4J_URI=neo4j://database:7687
11
+ NEO4J_USERNAME=neo4j
12
+ NEO4J_PASSWORD=password
13
+
14
+ #*****************************************************************
15
+ # Ollama
16
+ #*****************************************************************
17
+ OLLAMA_BASE_URL=http://ollama:11434
18
+
19
+ #*****************************************************************
20
+ # OpenAI
21
+ #*****************************************************************
22
+ # Only required when using OpenAI LLM or embedding model
23
+ # OpenAI charges may apply. For details, see
24
+ # https://openai.com/pricing
25
+
26
+ #OPENAI_API_KEY=sk-..
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ langchain==0.0.324
3
+ neo4j
4
+ #sentence_transformers==2.2.2
5
+ torch==2.0.1
6
+ PyPDF2
7
+ openai==0.28.1
8
+ fastapi
9
+ uvicorn
10
+ gradio-client
11
+ sentence-transformers
utils.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class BaseLogger:
2
+ def __init__(self) -> None:
3
+ self.info = print
4
+
5
+
6
+ def extract_title_and_question(input_string):
7
+ lines = input_string.strip().split("\n")
8
+
9
+ title = ""
10
+ question = ""
11
+ is_question = False # flag to know if we are inside a "Question" block
12
+
13
+ for line in lines:
14
+ if line.startswith("Title:"):
15
+ title = line.split("Title: ", 1)[1].strip()
16
+ elif line.startswith("Question:"):
17
+ question = line.split("Question: ", 1)[1].strip()
18
+ is_question = (
19
+ True # set the flag to True once we encounter a "Question:" line
20
+ )
21
+ elif is_question:
22
+ # if the line does not start with "Question:" but we are inside a "Question" block,
23
+ # then it is a continuation of the question
24
+ question += "\n" + line.strip()
25
+
26
+ return title, question
27
+
28
+
29
+ def create_vector_index(driver, dimension: int) -> None:
30
+ index_query = "CALL db.index.vector.createNodeIndex('stackoverflow', 'Question', 'embedding', $dimension, 'cosine')"
31
+ try:
32
+ driver.query(index_query, {"dimension": dimension})
33
+ except: # Already exists
34
+ pass
35
+ index_query = "CALL db.index.vector.createNodeIndex('top_answers', 'Answer', 'embedding', $dimension, 'cosine')"
36
+ try:
37
+ driver.query(index_query, {"dimension": dimension})
38
+ except: # Already exists
39
+ pass
40
+
41
+
42
+ def create_constraints(driver):
43
+ driver.query(
44
+ "CREATE CONSTRAINT question_id IF NOT EXISTS FOR (q:Question) REQUIRE (q.id) IS UNIQUE"
45
+ )
46
+ driver.query(
47
+ "CREATE CONSTRAINT answer_id IF NOT EXISTS FOR (a:Answer) REQUIRE (a.id) IS UNIQUE"
48
+ )
49
+ driver.query(
50
+ "CREATE CONSTRAINT user_id IF NOT EXISTS FOR (u:User) REQUIRE (u.id) IS UNIQUE"
51
+ )
52
+ driver.query(
53
+ "CREATE CONSTRAINT tag_name IF NOT EXISTS FOR (t:Tag) REQUIRE (t.name) IS UNIQUE"
54
+ )