testbot_v3 / app.py
soyleyicicem's picture
Update app.py
5d4faf8 verified
import uuid
from embedding_loader import *
from initialize_db import QdrantClientInitializer
from pdf_loader import PDFLoader
from IPython.display import display, Markdown
import gradio as gr
from langchain_core.messages import HumanMessage, AIMessage
from langchain.memory import ConversationBufferMemory
from langchain_core.chat_history import InMemoryChatMessageHistory
from qdrant_client import QdrantClient, models
from db_operations import DatabaseOperations
from openai import AzureOpenAI
import json
from qdrant_client.http import models as rest
import time
from fastembed.sparse.bm25 import Bm25
from fastembed.late_interaction import LateInteractionTextEmbedding
from pydantic import BaseModel, Field
from langchain_core.load import dumpd, dumps, load, loads
from langchain.schema import Document
dense_embedding_model = import_embedding()
late_interaction_embedding_model = LateInteractionTextEmbedding("colbert-ir/colbertv2.0")
bm25_embedding_model = Bm25("Qdrant/bm25", language="turkish")
AZURE_OPENAI_KEY = os.getenv('azure_api')
os.environ['AZURE_OPENAI_KEY'] = AZURE_OPENAI_KEY
openai.api_version = "2024-08-01-preview" # change it with your own version
openai.azure_endpoint = os.getenv('azure_endpoint')
model = "gpt-4o-mini" # deployment name on Azure OPENAI Studio
client = AzureOpenAI(azure_endpoint = openai.azure_endpoint,
api_key=AZURE_OPENAI_KEY,
api_version=openai.api_version)
obj_qdrant = QdrantClientInitializer()
qclient = obj_qdrant.initialize_db()
obj_loader = PDFLoader()
# -----
def retriever_db(client, query, collection_name, CAR_ID):
dense_query_vector = list(dense_embedding_model.embed_documents([query]))[0]
sparse_query_vector = list(bm25_embedding_model.query_embed(query))[0]
late_query_vector = list(late_interaction_embedding_model.query_embed(query))[0].tolist()
prefetch = [
models.Prefetch(
query=dense_query_vector,
using="sfr-mistral",
limit=30,
),
models.Prefetch(
query=models.SparseVector(**sparse_query_vector.as_object()),
using="bm25",
limit=30,
),
models.Prefetch(
query=late_query_vector,
using="colbertv2.0",
limit=30,
),
]
results = client.query_points(
collection_name,
prefetch=prefetch,
query=models.FusionQuery(
fusion=models.Fusion.RRF,
),
with_payload=True,
query_filter=models.Filter(
must=[
models.FieldCondition(key="car_id", match=models.MatchValue(value=CAR_ID))
]),
limit=10,
)
# retrieved_chunks = [doc.payload for doc in results.points]
retrieved_chunks = [Document(metadata=doc.payload["metadata"], page_content=doc.payload["page_content"]) for doc in results.points]
return retrieved_chunks
## new version
def chat_gpt(prompt=None, history=[], model=model, client=client, tools=[None]):
if prompt is None:
messages = history
else:
history.append({"role": "user", "content": f"{prompt}"})
messages = history
completion = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
tool_choice="required",
temperature=0.0
)
return completion
retrieval_functions = [
{
"type": "function",
"function":{
"name": "get_section_titles",
"description": """Use this function to get the section, subsection and subsubsection titles from a user manual table of content.""",
"parameters": {
"type": "object",
"properties": {
"section_title": {
"type": "string",
"description": "Title of the section in the table of content",
},
"sub_section_title": {
"type": "string",
"description": "Title of the subsection in the table of content",
},
"sub_sub_section_title": {
"type": "string",
"description": "Title of the subsubsection in the table of content",
}
},
"required": ["section_title", "sub_section_title", "sub_sub_section_title"],
}
}
}
]
def get_section_content(section_title, sub_section_title, sub_sub_section_title, content_path):
with open(content_path, "r") as file:
doc_section_content = json.loads(file.read())
response = None
try:
content = doc_section_content["TableOfContents"][section_title][sub_section_title][sub_sub_section_title]["content"]
pages = doc_section_content["TableOfContents"][section_title][sub_section_title][sub_sub_section_title]["pages"]
response = Document(metadata={"page": pages,
"section_title": section_title,
"sub_section_title": sub_section_title,
"sub_sub_section_title": sub_sub_section_title},
page_content = content)
except:
pass
return response
def get_lead_result(question):
hizmet_listesi = {"Bakım": """Check-Up, Periyodik Bakım, Aks Değişimi, Amortisör Değişimi, Amortisör Takozu Değişimi, Baskı Balata Değişimi, Benzin Filtresi Değişimi,
Debriyaj Balatası Değişimi, Direksiyon Kutusu Değişimi, Dizel Araç Bakımı, Egzoz Muayenesi, Fren Kaliperi Değişimi, El Freni Teli Değişimi,
Fren Balatası Değişimi, Fren Disk Değişimi, Hava Filtresi Değişimi, Helezon Yay Değişimi, Kampana Fren Balatası Değişimi,
Kızdırma Bujisi Değişimi, Rot Başı Değişimi, Rot Kolu Değişimi, Rotil Değişimi, Silecek Değişimi, Süspansiyon, Triger Kayışı Değişimi,
Triger Zinciri Değişimi, V Kayışı Değişimi, Yağ Filtresi Değişimi, Yakıt Filtresi Değişimi, Havayastığı Değişimi""",
"Yağ ve Sıvılar": """Şanzıman Yağı Değişimi, Dizel Araçlarda Yağ Değişimi, Yağ Değişimi, Fren Hidrolik Değişimi, Antifriz Değişimi,""",
"Akü": """Akü Şarj Etme, Akü Değişimi""",
"Klima": """Oto Klima Kompresörü Tamiri, Oto Klima Tamiri, Araç Klima Temizliği, Araç Klima Bakteri Temizliği, Klima Gazı Dolumu, Klima Dezenfeksiyonu, Polen Filtresi Değişimi""",
"Elektrik": """Servis Uyarı Lambası Sıfırlama,Buji Kablosu Değişimi, Arıza Tespit, Göstergelerin Kontrolü, Far Ayarı ve Ampul Değişimi, Buji Değişimi, Sigorta Değişimi""",
"Lastik/ Jant": """Lastik Jant Satış, Lastik Değişimi, Balans Ayarı, Rot Ayarı, Rotasyon, Lastik Tamiri, Hava Kontrolü, Nitrojen Dolumu, Supap Değişimi, Lastik Saklama (Lastik Oteli), Jant Sökme Takma,""",
"Diğer": """Cam Tamiri""",
"Hibrit Araçlar": "Hibrit Araç Aküsü"}
lead_functions = [
{
"type": "function",
"function": {
"name": "grade_service_relevance",
"description": "Grade the relevance of services to a user question",
"parameters": {
"type": "object",
"properties": {
"binary_score": {
"type": "string",
"description": "Services are relevant to the question, 'yes' or 'no'",
"enum": ["yes", "no"]
}
},
"required": ["binary_score"]
}
}
}
]
# System message
system_message = """Soruyu cevaplarken:
1- Önce soruyu düşün.
2- Kullanıcının sorduğu soru, hizmet listesinde sunulan hizmetlerle alakalı mı?
Alakalı ise "yes", değilse "no" olarak cevap ver."""
def service_grader_relevance(hizmet_listesi: str, question: str) -> dict:
completion = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_message},
{"role": "user", "content": f"Provided services: \n\n {hizmet_listesi} \n\n User question: {question}"}
],
tools=lead_functions,
tool_choice={"type": "function", "function": {"name": "grade_service_relevance"}}
)
tool_call = completion.choices[0].message.tool_calls[0]
return json.loads(tool_call.function.arguments)
result = service_grader_relevance(hizmet_listesi, question)
return result['binary_score']
def chat_gpt_nofn(prompt=None, history=[], model=model, client=client):
if prompt is None:
messages = history
else:
history.append({"role": "user", "content": f"{prompt}"})
messages = history
completion = client.chat.completions.create(
model=model,
messages=messages,
stream=True)
return completion
def format_chat_prompt(chat_history):
prompt = []
print(chat_history)
for turn in chat_history:
user_message, ai_message = turn
prompt.append({"role": "user", "content": user_message})
prompt.append({"role": "assistant", "content": ai_message})
return prompt
class GradeDocuments(BaseModel):
"""Binary score for relevance check on retrieved documents."""
binary_score: str = Field(description="Documents are relevant to the question, 'yes' or 'no'")
def grade_document_with_openai(document: str, question: str) -> GradeDocuments:
system_message = """
You are a grader assessing relevance of a retrieved document to a user question.
Consider the following when making your assessment:
- Does the document directly or indiretly address the user's question?
- Does it provide information or context that is pertinent to the question?
- Does it discuss relevant risks, benefits, recommendations, or considerations related to the question?
If the document contains keyword(s) or semantic meaning related or partially related to the question, grade it as relevant.
Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.
"""
# """
# You are a grader assessing relevance of a retrieved document to a user question.
# Consider the following when making your assessment:
# - Does the documents encapsulate the car related feature mentioned the question?
# - Does the documents directly or indirectly address the user's question?
# - Does it provide information or context that is pertinent to the question?
# - Does it discuss relevant risks, benefits, recommendations, or considerations related to the question?
# If the documents contains keyword(s) or semantic meaning related or partially related to the question, grade it as relevant
# Give a binary score 'yes' or 'no' to indicate whether the documents is relevant to the question.
# """
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_message},
{"role": "user", "content": f'Retrieved document: \n\n {document} \n\n User question: {question}'}
]
)
score = response.choices[0].message.content
return GradeDocuments(binary_score=score.strip())
liked_state = gr.State(None)
last_interaction = gr.State(None)
def chat(question, manual, history, liked, graded_doc_history):
history = history or []
graded_doc_history = graded_doc_history or []
conv = format_chat_prompt(history)
# print("History: ", history)
# print("CONV: ", conv)
# print("Graded_doc_history: ", graded_doc_history)
manual_list = {"Toyota_Corolla_2024_TR": -8580416610875007536,
"Renault_Clio_2024_TR":-5514489544983735006,
"Fiat_Egea_2024_TR":-2026113796962100812}
collection_list = {"Toyota_Corolla_2024_TR": "HYBRID_TOYOTA_MANUAL_COLLECTION_EMBED3",
"Renault_Clio_2024_TR": "HYBRID_RENAULT_MANUAL_COLLECTION_EMBED3",
"Fiat_Egea_2024_TR": "HYBRID_FIAT_MANUAL_COLLECTION_EMBED3"}
collection_name = collection_list[manual]
toc_name = "ToC_" + manual + ".txt"
start_time = time.time()
with open("ToCs/" + toc_name, "r") as file:
content = json.loads(file.read())
print("ToCs:--- %s seconds ---" % (time.time() - start_time))
# start_time = time.time()
# db = obj_loader.load_from_database(embeddings=embeddings, collection_name=collection_name)
# print("DB Load:--- %s seconds ---" % (time.time() - start_time))
start_time = time.time()
for i in range(3):
first_hop = f"""Soruyu cevaplarken:
1- Önce soruyu düşün.
2- Kullanıcının sorduğu sorunun konu başlıkları neler olabilir?
3- Sorulan soru bir arızaya işaret ediyor olabilir mi?
4- Bu konu başlıkları kullanım kılavuzu içindekiler tablosu başlıkları ile alakalı mı?
5- Alakalı olabilecek tüm başlıkları türet.
Buna göre, aşağıda vereceğim kullanım kılavuzu içindekiler tablosu (başlıklar) bilgisini kullanarak bu içeriğe erişmek için uygun fonksiyonları üret.
Eğer herhangi bir içeriğe ulaşamazsan, bunu belir ve sorunun cevabı hakkında yorum yapma.
Kullanım Kılavuzu İçindekiler Tablosu:
{content}
"""
# conv = [{"role": "system", "content": f"{first_hop}"}]
# conv.append({"role": "system", "content": f"{first_hop}"})
# first_hop_response = chat_gpt(prompt=f"Soru: {question}", history=conv, tools=retrieval_functions)
# conv.append(first_hop_response.choices[-1].message)
first_hop_response = chat_gpt(prompt=f"Soru: {question}",
history=[{"role": "system", "content": f"{first_hop}"}],
tools=retrieval_functions)
tool_calls = first_hop_response.choices[-1].message.tool_calls
arg_list = []
if tool_calls:
for tool_call in tool_calls:
function_name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
arg_list.append(args)
print(f"Will call {function_name} with arguments {args}")
break
else:
print("No tool calls")
print("First_hop:--- %s seconds ---" % (time.time() - start_time))
path = "Contents/" + manual + ".json"
start_time = time.time()
documents = []
for i in arg_list:
documents.append(get_section_content(str(i['section_title']), str(i['sub_section_title']), str(i['sub_sub_section_title']), content_path=path))
# break
# print("Documents:", documents)
CAR_ID = manual_list[manual]
documents_db = retriever_db(qclient, question, collection_name, CAR_ID)
for i in documents_db:
documents.append(i)
print("Retriever:--- %s seconds ---" % (time.time() - start_time))
# print("Documents: ", documents)
start_time = time.time()
graded_doc = []
for r in documents:
grade = grade_document_with_openai(r, question)
if grade.binary_score == "yes":
graded_doc.append(r)
# graded_doc_history.append(r)
print(r.metadata["page"])
print("GRADED DOC:", graded_doc)
print("GRADED DOC HISTORY", graded_doc_history)
# if not graded_doc:
# documents = []
# CAR_ID = manual_list[manual]
# documents_db = retriever_db(qclient, question, collection_name, CAR_ID)
# for i in documents_db:
# documents.append(i)
# for r in documents:
# grade = grade_document_with_openai(r, question)
# if grade.binary_score == "yes":
# graded_doc.append(r)
# graded_doc_history.append(r)
print("Onaylanan Doküman Sayısı:", len(graded_doc))
# print(graded_doc_history)
print("Grade Doc:--- %s seconds ---" % (time.time() - start_time))
start_time = time.time()
if get_lead_result(question).lower() == 'yes':
lead = """ Her zaman tamirci yerine geçen "servis", "yetkili servis", ""... servisi", ""... yetkili servisi", "bakım" gibi ifadeler yerine yalnızca "OtoBakım Servisi" yazmalısın.
Cevabın sonunda, "Nasıl devam edeceğinizden emin değilseniz." ifadesiyle başlayarak, kullanıcılara hızlı ve uygun fiyatlı çözümler için OtoBakım mağazalarını ziyaret etmelerini teklif etmelisin."""
print('Lead: yes')
else:
lead = 'Her zaman tamirci yerine geçen "servis", "yetkili servis", ""... servisi", ""... yetkili servisi", "bakım" gibi ifadeler yerine yalnızca "OtoBakım Servisi" yazmalısın. Sorunun tipine göre OtoBakım servislerini ziyaret edebileceklerini söylemelisin.'
print('Lead: no')
print("Lead Check:--- %s seconds ---" % (time.time() - start_time))
start_time = time.time()
marka = manual.split("_")[0]
model = manual.split("_")[1]
yıl = manual.split("_")[2]
prompt = f"""
Sen, yalnızca {yıl} {marka} {model} marka araba ile ilgili sorunlara ve araç bilgilerine odaklanan, ARVI adında uzman bir asistansın.
Amacın, araba sorunları, bakım, onarımlar, teknik özellikler ve diğer araçla ilgili konularla ilgili sorulara eğer dokümanlarda (yani kullanım kılavuzunda) yeterli bilgi varsa doğru, yardımcı, net ve yorum yapmadan cevaplar vermektir.
Temel nezaket etkileşimlerine uygun ve kibar bir şekilde yanıt vermek için tasarlandın.
Soruları yanıtlarken aşağıdaki adımları izle: \n
- Dokümanlar soruyla ilgiliyse, soruyu yanıtlamak için dokümanlardan yararlan.
- Sorulara cevap verirken sana sağlanan bilgilerdeki uyarılara, tehlikelere vurgu yap ve öne çıkar.
- Soruları yanıtlarken yorum yapma, kişisel görüşlerini belirtme ve önceki bilgilerini kullanma.
- Dokümandakiler dışında terim ve bilgileri kullanma.
- Eğer dokümanlarda bir işlemin nasıl yapıldığı adım adım anlatılıyorsa, bu adımları direkt şekilde ekle.
- Kullanıcıya doğrudan cevap ver.
- Cevaplar kısa ama anlamlı ve yeterli olsun.
- Cevabın sonunda kullanılan kaynakları listelerken şu kurallara uy:
1. Başlık yapısı tamamen aynı olan kaynakları (section_title -> sub_section_title -> sub_sub_section_title) tek bir referans altında birleştir.
2. Başlık boş olsa bile mutlaka referansta göster
3. Boş başlıklar için -> işaretinden sonra boşluk bırak
3. Her başlık yapısı sadece bir kez yazılmalı
4. Aynı başlığa ait tüm sayfa numaralarını tek bir "Sayfa:" satırında göster
5. Sayfa numaralarını şu kurallara göre formatla:
- Ardışık sayfalar için tire kullan (örn: **-**)
- Ardışık olmayan sayfalar için virgül kullan (örn: **, **, **)
- Tekrar eden sayfa numaralarını eleme
6. Format şu şekilde olmalı:
Kaynaklar:
a. [section_title -> sub_section_title -> sub_sub_section_title] // dolu başlık örneği
Sayfa: X-Z ya da A, B, ...
b. Sayfa: K, L, ... // boş başlık örneği
.
.
.
Ek yönerge: {lead}
Son Kontrol:
- Cevabın doğruluğunu ve tamlığını kontrol et.
- Gereksiz bilgi veya yorum olup olmadığını kontrol et.
- Referansların doğru eklendiğinden emin ol.
Sorulan soru bir önceki soru ile bağlantılı olabilir. Bu durumda önceki soruyla yeni soruyu beraber değerlendir.
Kullanıcı detaylı bilgi, daha çok, başka vb. ifadeler kullanmışsa, daha detaylı bilgi ver.
Eğer Dokümanlar boş ise ve Geçmiş Dokümanlar soru ile alakasızsa: sorulan soruyla uyumlu bir şekilde, kullanım kılavuzunda bu bilginin olmadığını, soruyu neden yanıtlayamadığını belirt ve soru hakkında yorum yapma.
Soru çok genel ise, spesifik bilgi iste.
Geçmiş Dokümanlar: {graded_doc_history}
Dokümanlar: {graded_doc}
"""
for r in graded_doc:
graded_doc_history.append(r)
conv.append({"role": "system", "content": f"{prompt}"})
final_response = chat_gpt_nofn(prompt=f"Soru: {question}", history=conv)
# final_response = chat_gpt_nofn(prompt=prompt, history=conv)
partial_response = ""
print("Answer:--- %s seconds ---" % (time.time() - start_time))
for chunk in final_response:
try:
if chunk.choices[0].delta.content is not None:
partial_response += chunk.choices[0].delta.content
yield partial_response, history + [(question, partial_response)], graded_doc_history
except:
pass
response = partial_response
# conv.append({"role": "user", "content": prompt})
conv.append({"role": "assistant", "content": response})
history.append((question, response))
print("Answer:--- %s seconds ---" % (time.time() - start_time))
# Store the last interaction without saving to the database yet
last_interaction.value = {
"question": question,
"response": response,
"manual": manual,
"point_id": uuid.uuid4().hex
}
yield response, history, graded_doc_history
def save_last_interaction(feedback):
if last_interaction.value:
DatabaseOperations.save_user_history_demo(
qclient,
"USER_COLLECTION_EMBED3_v3",
last_interaction.value["question"],
last_interaction.value["response"],
dense_embedding_model,
last_interaction.value["point_id"],
last_interaction.value["manual"],
feedback
)
last_interaction.value = None
manual_list = ["Toyota_Corolla_2024_TR", "Renault_Clio_2024_TR", "Fiat_Egea_2024_TR"]
with gr.Blocks() as demo:
chatbot = gr.Chatbot(height=600)
manual = gr.Dropdown(label="Kullanım Kılavuzları", value="Toyota_Corolla_2024_TR", choices=manual_list)
textbox = gr.Textbox()
graded_doc_history = gr.State([])
clear = gr.ClearButton(components=[textbox, chatbot, graded_doc_history], value='Clear console')
def handle_like(data: gr.LikeData):
liked_state.value = data.liked
if liked_state.value is not None:
feedback = "LIKE" if liked_state.value else "DISLIKE"
save_last_interaction(feedback)
# def gradio_chat(question, manual, history, graded_doc_history):
# save_last_interaction("N/A") # Save previous interaction before starting a new one
# chat_generator = chat(question, manual, history, liked_state.value, graded_doc_history)
# final_response = ""
# final_history = history
# for partial_response, updated_history, doc_history in chat_generator:
# final_response += partial_response
# final_history = updated_history
# yield "", final_history
# return "", final_history
def gradio_chat(question, manual, history, graded_doc_history):
save_last_interaction("N/A")
chat_generator = chat(question, manual, history, liked_state.value, graded_doc_history)
final_history = history
final_doc_history = graded_doc_history or []
for partial_response, updated_history, doc_history in chat_generator:
final_history = updated_history
final_doc_history = doc_history
yield "", final_history, final_doc_history
return "", final_history, final_doc_history
textbox.submit(gradio_chat,
[textbox, manual, chatbot, graded_doc_history],
[textbox, chatbot, graded_doc_history])
chatbot.like(handle_like, None, None)
demo.queue()
demo.launch()