brestok commited on
Commit
98dceee
·
verified ·
1 Parent(s): 6721c07

Upload 27 files

Browse files
.env ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ FASTAPI_CONFIG=production
2
+ SECRET='zu=*nck@&r26rsa$2qv8a7g-p$slw79ym81mylj6s**w(da6&#'
3
+ OPENAI_API_KEY=sk-proj-GYY7hx5SM02oDz5lhrK0T3BlbkFJegeuu9bXdNONd3Ds1rqY
4
+
5
+ #DATABASE_USER=
6
+ #DATABASE_PASSWORD=
7
+ #DATABASE_HOST=
8
+ #DATABASE_PORT=
9
+ #DATABASE_NAME=
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ FASTAPI_CONFIG=production
2
+ SECRET='zu=*nck@&r26rsa$2qv8a7g-p$slw79ym81mylj6s**w(da6&#'
3
+ OPENAI_API_KEY=
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .env
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+
7
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
+
9
+ COPY . .
10
+
11
+ RUN chmod -R 777 /code
12
+
13
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
main.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from project import create_app
2
+
3
+ app = create_app()
project/__init__.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+
5
+ from project.config import settings
6
+
7
+
8
+ def create_app() -> FastAPI:
9
+ app = FastAPI()
10
+
11
+ from project.bot import bot_router
12
+ app.include_router(bot_router, tags=['bot'])
13
+
14
+ from project.ws import ws_router
15
+ app.include_router(ws_router, tags=['ws'])
16
+
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=settings.ORIGINS,
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ app.mount('/static', StaticFiles(directory="static"), name="static")
26
+
27
+ return app
project/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (879 Bytes). View file
 
project/__pycache__/config.cpython-310.pyc ADDED
Binary file (4 kB). View file
 
project/asgi.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from project import create_app
2
+
3
+ app = create_app()
4
+
project/bot/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ bot_router = APIRouter(
4
+ prefix=''
5
+ )
6
+
7
+ from project.bot import views
project/bot/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (278 Bytes). View file
 
project/bot/__pycache__/chatbot.cpython-310.pyc ADDED
Binary file (4.58 kB). View file
 
project/bot/__pycache__/views.cpython-310.pyc ADDED
Binary file (565 Bytes). View file
 
project/bot/chatbot.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import base64
3
+ import os
4
+ import tempfile
5
+
6
+ import numpy as np
7
+
8
+ from project.config import settings
9
+ import pandas as pd
10
+
11
+
12
+ class ChatBot:
13
+ chat_history = []
14
+
15
+ def __init__(self, memory=None):
16
+ self.chat_history.append({
17
+ "role": 'assistant',
18
+ 'content': "Hi! What would you like to order from the food?"
19
+ })
20
+
21
+ @staticmethod
22
+ def _transform_bytes_to_file(data_bytes) -> str:
23
+ audio_bytes = base64.b64decode(data_bytes)
24
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp3')
25
+ try:
26
+ temp_file.write(audio_bytes)
27
+ filepath = temp_file.name
28
+ finally:
29
+ temp_file.close()
30
+ return filepath
31
+
32
+ @staticmethod
33
+ async def _transcript_audio(temp_filepath: str) -> str:
34
+ with open(temp_filepath, 'rb') as file:
35
+ transcript = await settings.OPENAI_CLIENT.audio.transcriptions.create(
36
+ model='whisper-1',
37
+ file=file,
38
+ # language='nl'
39
+ )
40
+ text = transcript.text
41
+ return text
42
+
43
+ @staticmethod
44
+ async def _convert_to_embeddings(query: str):
45
+ response = await settings.OPENAI_CLIENT.embeddings.create(
46
+ input=query,
47
+ model='text-embedding-3-large'
48
+ )
49
+ embeddings = response.data[0].embedding
50
+ return embeddings
51
+
52
+ @staticmethod
53
+ async def _convert_response_to_voice(ai_response: str) -> str:
54
+ audio = await settings.OPENAI_CLIENT.audio.speech.create(
55
+ model="tts-1",
56
+ voice="alloy",
57
+ input=ai_response
58
+ )
59
+ encoded_audio = base64.b64encode(audio.content).decode('utf-8')
60
+ return encoded_audio
61
+
62
+ @staticmethod
63
+ async def _get_context_data(query: list[float]) -> str:
64
+ query = np.array([query]).astype('float32')
65
+ _, distances, indices = settings.FAISS_INDEX.range_search(query.astype('float32'), settings.SEARCH_RADIUS)
66
+ indices_distances_df = pd.DataFrame({'index': indices, 'distance': distances})
67
+ filtered_data_df = settings.products_dataset.iloc[indices]
68
+ filtered_data_df['distance'] = indices_distances_df['distance'].values
69
+ sorted_data_df: pd.DataFrame = filtered_data_df.sort_values(by='distance').reset_index(drop=True)
70
+ sorted_data_df = sorted_data_df.drop('distance', axis=1)
71
+ data = sorted_data_df.head(1).to_dict(orient='records')
72
+ context_str = ''
73
+ for row in data:
74
+ context_str += f'{row["Search"]}\n\n'
75
+ return context_str
76
+
77
+ async def _rag(self, query: str, query_type: str, context: str = None):
78
+ if context:
79
+ self.chat_history.append({'role': 'assistant', 'content': context})
80
+ prompt = settings.PRODUCT_PROMPT
81
+ else:
82
+ if 'search' in query_type.lower():
83
+ prompt = settings.EMPTY_PRODUCT_PROMPT
84
+ elif 'purchase' in query_type.lower():
85
+ prompt = settings.ADD_TO_CART_PROMPT
86
+ elif 'product_list' in query_type.lower():
87
+ prompt = settings.PRODUCT_LIST_PROMPT
88
+ else:
89
+ prompt = settings.EMPTY_PRODUCT_PROMPT
90
+ self.chat_history.append({
91
+ 'role': 'user',
92
+ 'content': query
93
+ })
94
+ messages = [
95
+ {
96
+ 'role': 'system',
97
+ 'content': f"{prompt}"
98
+ },
99
+ ]
100
+ messages += self.chat_history
101
+ completion = await settings.OPENAI_CLIENT.chat.completions.create(
102
+ messages=messages,
103
+ temperature=0,
104
+ n=1,
105
+ model="gpt-3.5-turbo",
106
+ )
107
+ response = completion.choices[0].message.content
108
+ self.chat_history.append({'role': 'assistant', 'content': response})
109
+ return response
110
+
111
+ async def _get_query_type(self, query: str) -> str:
112
+ assistant_message = self.chat_history[-1]['content']
113
+ messages = [
114
+ {
115
+ "role": 'system',
116
+ 'content': settings.ANALYZER_PROMPT
117
+ },
118
+ {
119
+ "role": 'user',
120
+ "content": f"Assistant message: {assistant_message}\n"
121
+ f"User response: {query}"
122
+ }
123
+ ]
124
+ completion = await settings.OPENAI_CLIENT.chat.completions.create(
125
+ messages=messages,
126
+ temperature=0,
127
+ n=1,
128
+ model="gpt-3.5-turbo",
129
+ )
130
+ response = completion.choices[0].message.content
131
+ return response
132
+
133
+ async def ask(self, data: dict):
134
+ audio = data['audio']
135
+ temp_filepath = self._transform_bytes_to_file(audio)
136
+ transcript = await self._transcript_audio(temp_filepath)
137
+ query_type = await self._get_query_type(transcript)
138
+
139
+ context = None
140
+ if query_type == 'search':
141
+ transformed_query = await self._convert_to_embeddings(transcript)
142
+ context = await self._get_context_data(transformed_query)
143
+ ai_response = await self._rag(transcript, query_type, context)
144
+ voice_ai_response = await self._convert_response_to_voice(ai_response)
145
+
146
+ data = {
147
+ 'user_query': transcript,
148
+ 'ai_response': ai_response,
149
+ 'voice_response': voice_ai_response
150
+ }
151
+
152
+ try:
153
+ os.remove(temp_filepath)
154
+ except FileNotFoundError:
155
+ pass
156
+ return data
project/bot/search_tools/cleaned_products.csv ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Search
2
+ Naam van de producten: Maak je eigen Poke . Productomschrijving: nan. Prijs: € 14.95
3
+ "Naam van de producten: So Salmon Bowl. Productomschrijving: Gemarineerde zalm, komkommer, avocado, wortel, edamame, sesam zaadjes, gebakken ui en Japanse mayo. Prijs: € 14.95"
4
+ "Naam van de producten: Rock 'n chicken. Productomschrijving: Krokante kip, mais, komkommer, edamame bonen, rode ui, gebakken ui, chilisaus dressing, sesamzaadjes.. Prijs: € 14.95"
5
+ "Naam van de producten: So Salmon Bowl. Productomschrijving: Gemarineerde zalm, komkommer, avocado, wortel, edamame, sesam zaadjes, gebakken ui en Japanse mayo. Prijs: € 14.95"
6
+ "Naam van de producten: Truly Tuna. Productomschrijving: Raw Tuna, avacado, wortel, komkommer, mais, gebakken ui, nori flakes, wasabi malunaise. Prijs: € 14.95"
7
+ "Naam van de producten: Rock 'n chicken. Productomschrijving: Krokante kip, mais, komkommer, edamame bonen, rode ui, gebakken ui, chilisaus dressing, sesamzaadjes.. Prijs: € 14.95"
8
+ "Naam van de producten: I'm Veggie Baby. Productomschrijving: Knapperige tofu, Komkommer, ananas, lente ui, mango, sriracha saus, kokos vlokken en sesamzaadjes.. Prijs: € 14.5"
9
+ "Naam van de producten: Teriyaki Chicks. Productomschrijving: Krokante kip, komkommer, mais, edamame, wortel, sesamzaadjes, gebakken ui, teriyaki dressing. Prijs: € 14.95"
10
+ "Naam van de producten: Spicy Tuna. Productomschrijving: Spicy gemarineerde tonijn, Edamame, mango, wakame, mais, masago en gebakken ui.. Prijs: € 14.95"
11
+ "Naam van de producten: Tempura Trouble. Productomschrijving: Ebi tempura garnalen, Avocado, edamame, wortel, komkommer, furikake en gebakken uitjes. Sriracha malunaise.. Prijs: € 14.95"
12
+ Naam van de producten: Maak je eigen Poke . Productomschrijving: nan. Prijs: € 14.95
13
+ Naam van de producten: Edamame. Productomschrijving: Japanse sojaboontjes. Deze zijn op smaak gebracht met een beetje zeezout. Prijs: € 5.95
14
+ "Naam van de producten: Chuka Wakame. Productomschrijving: Zeewier salade op smaak met sesamolie, sojasaus en sesamzaadjes. Prijs: € 5.95"
15
+ Naam van de producten: Ebi Fry - 4 Stuks. Productomschrijving: Gefrituurde garnaal.. Prijs: € 5.95
16
+ Naam van de producten: Karaage - 5 Stuks. Productomschrijving: Gefrituurde kipstukjes in Japanese style. Prijs: € 2.98
17
+ "Naam van de producten: Malumpia. Productomschrijving: Malu´s treasure, 8 mini loempia's.. Prijs: € 5.95"
18
+ Naam van de producten: Gyoza. Productomschrijving: Japanse Dumpling. 5 stuks.. Prijs: € 5.95
19
+ Naam van de producten: Never Enough. Productomschrijving: Malu's zeldzame kipstukken geserveerd met een overheerlijke chilisaus. 5 stuks.. Prijs: € 5.95
20
+ "Naam van de producten: Pink Lady. Productomschrijving: Aardbei, banaan, soja-vanillemelk. Prijs: € 4.5"
21
+ "Naam van de producten: Sweet Baby. Productomschrijving: Framboos, soja-vanillemelk, banaan. Prijs: € 4.5"
22
+ "Naam van de producten: Power Shake. Productomschrijving: Framboos, aarbei, soja-vanillemelk. Prijs: € 4.5"
23
+ "Naam van de producten: Aloha Shake. Productomschrijving: Avocado, banaan, soja-vanillemelk. Prijs: € 4.5"
24
+ "Naam van de producten: Choco Loco. Productomschrijving: Banaan, avocado, soja-chocolademelk. Prijs: € 4.5"
25
+ "Naam van de producten: Coca Cola original taste 330ml. Productomschrijving: Coca-Cola Original Taste, een heerlijke en verfrissende bruisende frisdrank, voor het eerst ontwikkeld in 1886, met een geweldige smaak. Made for sharing. Prijs: € 2.5"
26
+ Naam van de producten: Coca-Cola Light 330ml. Productomschrijving: Coca-Cola Light is een heerlijke calorie- en suikervrije bruisende frisdrank met een geweldige smaak en slechts 1 kcal per 330ml. Iconische en verfrissende cola. Prijs: € 2.5
27
+ "Naam van de producten: Fanta Orange 330ml. Productomschrijving: Fanta Orange: de iconische, heerlijke bruisende frisdrank gemaakt met vruchtensap. Perfect voor snacken en ontwikkeld om kleur aan je leven toe te voegen. Prijs: € 2.5"
28
+ Naam van de producten: Hawaii Tropical Drink. Productomschrijving: nan. Prijs: € 2.5
29
+ "Naam van de producten: Sprite zero sugar 330ml. Productomschrijving: Laat je ware ik sprankelen met Sprite: een zuivere, fris smakende, bruisende limoen- en citroenfrisdrank. 100% natuurlijke aroma's, zonder suiker, zonder calorieën.. Prijs: € 2.5"
30
+ "Naam van de producten: Fuze Tea black tea sparkling lemon 330ml. Productomschrijving: FUZETEA Black Tea Sparkling Lemon is een unieke, caloriearme* bruisende theedrank gemaakt met een fusie van zwarte thee en zonnig citroen.. Prijs: € 2.5"
31
+ "Naam van de producten: Fuze Tea green tea 330ml. Productomschrijving: FUZETEA Green Tea Citrus is een unieke, verfrissende caloriearme* theedrank gemaakt met een fusie van groene thee en citrus.. Prijs: € 2.5"
32
+ Naam van de producten: Red Dragon. Productomschrijving: Framboos - Banaan en Appelsap. Prijs: € 4.5
33
+ Naam van de producten: Tropical Island. Productomschrijving: Passievrucht - Ananas - Appelsap. Prijs: € 4.5
34
+ Naam van de producten: Spicy Dutch. Productomschrijving: Wortel - Appelsap - Gember. Prijs: € 4.5
project/bot/search_tools/products_faiss.index ADDED
Binary file (406 kB). View file
 
project/bot/templates/voice.html ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Real Estate</title>
7
+ <!-- Fonts -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500&display=swap" rel="stylesheet">
11
+ <!-- Bootstrap css -->
12
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
13
+ integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
14
+ <link rel="stylesheet" href="../../../static/css/style.css">
15
+ </head>
16
+
17
+ <div id="message"></div>
18
+
19
+ <div class="overlay" id="loadingModal">
20
+ <div class="report-loader">
21
+ <div class="report-inner report-one"></div>
22
+ <div class="report-inner report-two"></div>
23
+ <div class="report-inner report-three"></div>
24
+ </div>
25
+ </div>
26
+ <div class="container-fluid px-0" style="height: 100vh">
27
+ <div class="row mx-0 d-flex align-items-center" style="height: 100vh">
28
+ <div class="col-4 ms-2">
29
+ <div class="rounded-5 border shadow" style="height: 95vh ;background-color: #f1f1f1">
30
+ <div id="chatHistory" class="my-4" style="height: 90vh; overflow-y: auto" >
31
+
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <div class="col-6 align-items-center justify-content-around d-flex">
36
+ <button class="btn btn-lg btn-success" onclick="startCall()">Start call</button>
37
+ <button class="btn btn-lg btn-primary" onclick="stopAndSend()">Send a record</button>
38
+ <button class="btn btn-lg btn-danger" onclick="endCall()">End call</button>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </html>
43
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
44
+ integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
45
+ crossorigin="anonymous"></script>
46
+ <script src="https://code.jquery.com/jquery-3.6.3.min.js"
47
+ integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
48
+ <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
49
+ <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
50
+ <script src="https://kit.fontawesome.com/d4ffd37f75.js" crossorigin="anonymous"></script>
51
+ <script type="text/javascript" src="../../../static/js/voice.js"></script>
project/bot/utils.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import datetime
3
+ import os
4
+ import tempfile
5
+ from io import BytesIO
6
+
7
+ from PIL import Image
8
+ import aiofiles
9
+ from fastapi import UploadFile
10
+ from fastapi.params import File
11
+
12
+ from project.bot.models import Report
13
+ from project.config import settings
14
+
15
+
16
+ async def generate_ai_report(history: list[str], language: str) -> Report:
17
+ history = list(map(lambda x: {'role': 'user', 'content': x}, history))
18
+ messages = [
19
+ {
20
+ 'role': 'system',
21
+ 'content': f"""Summarize the key points from the user's messages, organizing the summary into a structured
22
+ format. Conclude with a brief report that encapsulates the essence of the
23
+ discussion. Make your answer in {language}"""
24
+ },
25
+ *history
26
+ ]
27
+ chat_completion = await settings.OPENAI_CLIENT.chat.completions.create(
28
+ model="gpt-4-0125-preview",
29
+ temperature=0.4,
30
+ n=1,
31
+ messages=messages
32
+ )
33
+ response = chat_completion.choices[0].message.content
34
+ report = Report()
35
+ report.content = response
36
+ return report
37
+
38
+
39
+ async def encode_file_to_base64(filepath):
40
+ async with aiofiles.open(filepath, 'rb') as file:
41
+ content = await file.read()
42
+ return base64.b64encode(content).decode('utf-8')
43
+
44
+
45
+ async def transcript_audio_from_base64(data: str):
46
+ current_time = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
47
+ file_name = f"voice_record_{current_time}.mp3"
48
+ file_path = str(settings.BASE_DIR / 'project' / 'records' / file_name)
49
+ data_bytes = base64.b64decode(data)
50
+ async with aiofiles.open(file_path, 'wb') as f:
51
+ await f.write(data_bytes)
52
+ with open(file_path, 'rb') as f:
53
+ transcript = await settings.OPENAI_CLIENT.audio.transcriptions.create(
54
+ model='whisper-1',
55
+ file=f,
56
+ )
57
+ text = transcript.text
58
+ return text, file_path
59
+
60
+
61
+ async def generate_image_description(image: str, file_format: str) -> str:
62
+ messages = [
63
+ {
64
+ 'role': 'system',
65
+ 'content': settings.REPORT_PROMPT
66
+ },
67
+ {
68
+ 'role': 'user',
69
+ 'content': [
70
+ {
71
+ 'type': 'text',
72
+ 'text': settings.IMAGE_PROMPT
73
+ },
74
+ {
75
+ 'type': 'image_url',
76
+ 'image_url': {
77
+ 'url': f"data:image/{file_format};base64,{image}",
78
+ 'detail': 'low'
79
+ }
80
+ }
81
+ ]
82
+ }
83
+ ]
84
+ chat_completion = await settings.OPENAI_CLIENT.chat.completions.create(
85
+ model="gpt-4-vision-preview",
86
+ temperature=0.5,
87
+ n=1,
88
+ messages=messages
89
+ )
90
+ response = chat_completion.choices[0].message.content
91
+ return response
92
+
93
+
94
+ def compress_and_save_image(image_content: bytes, width=768) -> str:
95
+ img = Image.open(BytesIO(image_content))
96
+ orig_width, orig_height = img.size
97
+ new_height = int((orig_height * width) / orig_width)
98
+ resized_img = img.resize((width, new_height), Image.LANCZOS)
99
+ current_time = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
100
+ file_format = img.format.lower()
101
+ file_path = str(settings.BASE_DIR / 'project' / 'images' / f'image_{current_time}.{file_format}')
102
+ if file_format in ['jpeg', 'jpg']:
103
+ resized_img.save(file_path, 'JPEG', optimize=True, quality=70)
104
+ elif file_format == 'png':
105
+ resized_img.save(file_path, 'PNG', optimize=True, compress_level=7)
106
+ else:
107
+ raise ValueError(f"{file_format.upper()} format is not supported.")
108
+ return file_path
project/bot/views.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.templating import Jinja2Templates
2
+ from fastapi.requests import Request
3
+
4
+ from project.bot import bot_router
5
+
6
+ template = Jinja2Templates(directory='project/bot/templates')
7
+
8
+
9
+ @bot_router.get('/', name='voice')
10
+ async def voice(request: Request):
11
+ return template.TemplateResponse("voice.html", {'request': request})
project/config.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pathlib
2
+ from functools import lru_cache
3
+
4
+ import pandas as pd
5
+ from openai import AsyncOpenAI
6
+ from environs import Env
7
+ import faiss
8
+
9
+ env = Env()
10
+ env.read_env()
11
+
12
+
13
+ class BaseConfig:
14
+ BASE_DIR: pathlib.Path = pathlib.Path(__file__).parent.parent
15
+ OPENAI_CLIENT = AsyncOpenAI(api_key=env('OPENAI_API_KEY'))
16
+ FAISS_INDEX = faiss.read_index(str(BASE_DIR / 'project' / 'bot' / 'search_tools' / 'products_faiss.index'))
17
+ SEARCH_RADIUS = 1
18
+ PRODUCT_PROMPT = "Je bent de virtuele assistent van de Malu Haarlemse winkel. Uw taak is om het product " \
19
+ "zeer kort en bondig aan de gebruiker te presenteren. De gebruiker zal vragen om voedsel " \
20
+ "te bestellen in de winkel, en u, met behulp van uw vorige antwoord als kennis van " \
21
+ "productinformatie, moet kort vertellen over het product, de prijs en aanbieden om het toe "
22
+ EMPTY_PRODUCT_PROMPT = "Je bent de virtuele assistent van de Malu Haarlemse winkel. Uw taak is om de gebruiker heel kort en bondig te vragen om informatie over het voedsel in meer detail te verstrekken."
23
+ ANALYZER_PROMPT = """
24
+ Je bent een model voor classificatie. Analyseer de boodschap van de assistent en het antwoord van de gebruiker. Je taak is om hun dialoog te classificeren naar een van de antwoordcategorieën "search", "product_list", "purchase".
25
+ search - wanneer de gebruiker voedselkenmerken, naam, prijs opsomt. De gebruiker zal vragen om een specifiek product te vinden
26
+ product_list - wanneer de gebruiker vraagt om alle producten die aan de winkelwagen zijn toegevoegd, weer te geven. Of alle producten die hij van plan is te bestellen.
27
+ purchase - wanneer de assistent voorstelt om een product aan de winkelwagen toe te voegen en de gebruiker akkoord gaat.
28
+ Dus je antwoord moet slechts één woord bevatten - de categorie waartoe de dialoog van de assistent en de gebruiker behoort (search, product_list of purchase).
29
+ """
30
+ ADD_TO_CART_PROMPT = "Je bent de virtuele assistent van de Malu Haarlemse winkel. Uw taak is om heel kort en bondig te reageren op het verzoek van de gebruiker om voedsel aan het winkelwagentje toe te voegen. Stel bijvoorbeeld dat het gevraagde product aan de winkelwagen is toegevoegd en bied aan om iets anders te kopen"
31
+ PRODUCT_LIST_PROMPT = "Je bent de virtuele assistent van de Malu Haarlemse winkel. Uw taak, op verzoek van de gebruiker, is om een lijst van alle producten toegevoegd aan de winkelwagen en vertellen de totale kosten. Analyseer de geschiedenis van de dialoog om te begrijpen welke producten de gebruiker aan de winkelwagen heeft toegevoegd"
32
+
33
+
34
+ class DevelopmentConfig(BaseConfig):
35
+ pass
36
+
37
+
38
+ class ProductionConfig(BaseConfig):
39
+ ORIGINS = ["http://127.0.0.1:8000",
40
+ "http://localhost:8000",
41
+ 'https://alcolm.com/',
42
+ 'https://help.alcolm.com/'
43
+ ]
44
+
45
+ def __init__(self):
46
+ self.products_dataset = pd.read_csv(
47
+ str(self.BASE_DIR / 'project' / 'bot' / 'search_tools' / 'cleaned_products.csv'))
48
+
49
+
50
+ class TestConfig(BaseConfig):
51
+ pass
52
+
53
+
54
+ @lru_cache()
55
+ def get_settings() -> DevelopmentConfig | ProductionConfig | TestConfig:
56
+ config_cls_dict = {
57
+ 'development': DevelopmentConfig,
58
+ 'production': ProductionConfig,
59
+ 'testing': TestConfig
60
+ }
61
+ config_name = env('FASTAPI_CONFIG', default='production')
62
+ config_cls = config_cls_dict[config_name]
63
+ return config_cls()
64
+
65
+
66
+ settings = get_settings()
project/ws/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ ws_router = APIRouter()
4
+
5
+ from . import views
project/ws/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (250 Bytes). View file
 
project/ws/__pycache__/views.cpython-310.pyc ADDED
Binary file (681 Bytes). View file
 
project/ws/views.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import WebSocket, WebSocketDisconnect
2
+ from . import ws_router
3
+ from ..bot.chatbot import ChatBot
4
+
5
+
6
+ @ws_router.websocket("/ws/{client_id}")
7
+ async def websocket_endpoint(websocket: WebSocket, client_id: str):
8
+ await websocket.accept()
9
+ chatbot = ChatBot()
10
+ try:
11
+ while True:
12
+ data = await websocket.receive_json()
13
+ answer = await chatbot.ask(data)
14
+ await websocket.send_json(answer)
15
+ except WebSocketDisconnect:
16
+ pass
requirements.txt ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.6.0
2
+ anyio==4.3.0
3
+ certifi==2024.2.2
4
+ click==8.1.7
5
+ distro==1.9.0
6
+ environs==11.0.0
7
+ exceptiongroup==1.2.1
8
+ faiss-gpu==1.7.2
9
+ fastapi==0.110.3
10
+ h11==0.14.0
11
+ httpcore==1.0.5
12
+ httptools==0.6.1
13
+ httpx==0.27.0
14
+ idna==3.7
15
+ Jinja2==3.1.3
16
+ MarkupSafe==2.1.5
17
+ marshmallow==3.21.2
18
+ numpy==1.26.4
19
+ openai==1.25.1
20
+ packaging==24.0
21
+ pandas==2.2.2
22
+ pydantic==2.7.1
23
+ pydantic_core==2.18.2
24
+ python-dateutil==2.9.0.post0
25
+ python-dotenv==1.0.1
26
+ pytz==2024.1
27
+ PyYAML==6.0.1
28
+ six==1.16.0
29
+ sniffio==1.3.1
30
+ starlette==0.37.2
31
+ tqdm==4.66.4
32
+ typing_extensions==4.11.0
33
+ tzdata==2024.1
34
+ uvicorn==0.29.0
35
+ uvloop==0.19.0
36
+ watchfiles==0.21.0
37
+ websockets==12.0
static/css/style.css ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ code {
2
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
3
+ monospace;
4
+ }
5
+
6
+ ::-webkit-scrollbar {
7
+ width: 10px;
8
+ }
9
+
10
+ ::-webkit-scrollbar-track {
11
+ border-radius: 10px;
12
+ background: transparent;
13
+ }
14
+
15
+ ::-webkit-scrollbar-thumb {
16
+ background: #14274E;
17
+ border-radius: 10px
18
+ }
19
+
20
+ ::-webkit-scrollbar-thumb:hover {
21
+ background: #1d3970;
22
+ }
23
+
24
+
25
+ :root {
26
+ margin: 0px;
27
+ }
28
+
29
+ .App {
30
+ display: none;
31
+ position: fixed;
32
+ bottom: 20px;
33
+ left: 20px;
34
+ z-index: 81;
35
+
36
+ width: 540px;
37
+ box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
38
+ border-radius: 10px;
39
+ }
40
+
41
+ .App .head-container {
42
+ background-color: #14274E;
43
+ padding: 19px 50px 19px 50px;
44
+ display: flex;
45
+ align-items: center;
46
+ border-radius: 10px 10px 0px 0px;
47
+ }
48
+
49
+ .App .head-container .head-log-wrapper {
50
+ display: flex;
51
+ }
52
+
53
+ .App .head-container .head-log-wrapper .head-icon {
54
+ margin-right: 15.72px;
55
+ }
56
+
57
+ .App .head-container .head-log-wrapper .head-content {
58
+ position: relative;
59
+ margin-top: 5px;
60
+ }
61
+
62
+ .App .head-container .head-log-wrapper .head-content p {
63
+ margin: 0px;
64
+ position: absolute;
65
+ bottom: -1px;
66
+ color: white;
67
+ font-weight: 500;
68
+ letter-spacing: 0.755px;
69
+ font-size: 16px;
70
+ margin-left: 18px;
71
+ }
72
+
73
+ .App .head-container .head-close-wrapper {
74
+ margin-left: auto;
75
+ display: flex;
76
+ cursor: pointer;
77
+ }
78
+
79
+ .App .head-container .head-close-wrapper p {
80
+ margin: 0px;
81
+ color: #EF9E00;
82
+ font-size: 18px;
83
+ font-style: normal;
84
+ font-weight: 500;
85
+ }
86
+
87
+ .App .head-container .head-close-wrapper span {
88
+ display: flex;
89
+ align-items: center;
90
+ }
91
+
92
+ .App .chatui-container {
93
+ background-image: url('/static/bgs/bg.png');
94
+ background-size: cover;
95
+ background-repeat: no-repeat;
96
+
97
+ padding-top: 20px;
98
+ padding-left: 35px;
99
+ padding-right: 35px;
100
+ overflow-y: scroll;
101
+ /* Define the animation for scrolling down */
102
+ scroll-behavior: smooth;
103
+ }
104
+
105
+ .App .chatui-container .user-msg {
106
+ max-width: 360px;
107
+ margin-left: auto;
108
+ border: 2px solid #EF9E00;
109
+ padding: 10px 10px 10px 10px;
110
+ border-radius: 15px 0px 15px 15px;
111
+ background: #FCF4E5;
112
+ word-wrap: break-word;
113
+ box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
114
+ margin-bottom: 40px;
115
+ }
116
+
117
+ .App .chatui-container .bot-msg {
118
+ display: flex;
119
+ }
120
+
121
+ .App .chatui-container .bot-avatar-box span {
122
+ background-color: #14274E;
123
+ width: 55px;
124
+ height: 55px;
125
+ border-radius: 55px;
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ margin-bottom: 10px;
130
+ box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
131
+ }
132
+
133
+ .App .chatui-container .bot-msg .bot-content-box {
134
+ max-width: 360px;
135
+ /* width: 100%; */
136
+ margin-left: 20px;
137
+ padding: 10px 15px 10px 15px;
138
+ border-radius: 0px 15px 15px 15px;
139
+ border: 2px solid var(--surface-surface-invert, #14274E);
140
+ background: #E7E8EC;
141
+ box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
142
+ margin-bottom: 40px;
143
+ }
144
+
145
+ .App .chatui-container .bot-msg .bot-content-box p {
146
+ margin: 0 0 0em;
147
+ <!-- /* min-height: 48px; */
148
+ -->
149
+ }
150
+
151
+ .App .search-container {
152
+ padding: 15px 15px 15px 15px;
153
+ display: flex;
154
+ background-color: #ffffff;
155
+ align-items: center;
156
+ border-radius: 0px 0px 10px 10px;
157
+ border-bottom: 1px solid rgba(57, 72, 103, 0.1);
158
+ border-left: 1px solid rgba(57, 72, 103, 0.1);
159
+ border-right: 1px solid rgba(57, 72, 103, 0.1);
160
+ }
161
+
162
+ .App .search-container .search-input {
163
+ width: 100%;
164
+ padding: 15px 20px 15px 20px;
165
+ font-size: 16px;
166
+ border: 2px solid #000308;
167
+ border-radius: 10px 0px 0px 10px;
168
+ border-right: none;
169
+ outline: none;
170
+ }
171
+
172
+ .App .search-container .submit-btn {
173
+ cursor: pointer;
174
+ }
175
+
176
+ .App .search-container .submit-btn span {
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ background-color: #14274E;
181
+ padding: 11px;
182
+ border-radius: 0px 10px 10px 0px;
183
+ }
184
+
185
+
186
+ .sticky-chat-icon {
187
+ position: fixed;
188
+ bottom: 20px;
189
+ left: 20px;
190
+ width: 80px;
191
+ height: 80px;
192
+ text-align: center;
193
+ cursor: pointer;
194
+ z-index: 80;
195
+ }
196
+
197
+
198
+ .chat-wrapper {
199
+ height: 80px;
200
+ width: 80px;
201
+ background-color: #14274e;
202
+ border-radius: 50% 50% 50% 0;
203
+ border: 2px solid #ef9e00;
204
+ display: flex; /* Включить flex-контейнер */
205
+ justify-content: center; /* Выравнивание по горизонтали по центру */
206
+ align-items: center;
207
+ }
208
+
209
+ .bot-avatar {
210
+ fill: white;
211
+ width: 40px;
212
+ height: 40px;
213
+ }
214
+
215
+ .sticky-chat-icon:hover .bot-avatar {
216
+ fill: #ef9e00; /* Цвет при наведении (оранжевый) */
217
+ width: 45px; /* Размер при наведении (45x45 пикселей) */
218
+ height: 45px;
219
+ }
220
+
221
+
222
+ .ball {
223
+ background-color: #394867;
224
+ width: 8px;
225
+ height: 8px;
226
+ margin: 2px;
227
+ border-radius: 100%;
228
+ display: inline-block;
229
+ animation: bounce 0.5s ease-in-out infinite;
230
+ }
231
+
232
+ .ball1 {
233
+ animation-delay: 0.1s;
234
+ animation-direction: alternate; /* Для движения вверх и вниз */
235
+ }
236
+
237
+ .ball2 {
238
+ animation-delay: 0.2s;
239
+ animation-direction: alternate;
240
+ }
241
+
242
+ .ball3 {
243
+ animation-delay: 0.3s;
244
+ animation-direction: alternate;
245
+ }
246
+
247
+ @keyframes bounce {
248
+ 0%, 100% {
249
+ transform: translateY(0);
250
+ }
251
+ 50% {
252
+ transform: translateY(-7px); /* Для дерганного движения */
253
+ }
254
+ }
255
+
256
+ /* new loading state */
257
+ .loading-text {
258
+ min-height: 48px;
259
+ }
260
+
261
+ .loading-text p {
262
+ opacity: 0;
263
+ }
264
+
265
+ @keyframes fade-in-out {
266
+ 0% {
267
+ opacity: 0;
268
+ }
269
+ 50% {
270
+ opacity: 1;
271
+ }
272
+ 100% {
273
+ opacity: 0;
274
+ }
275
+ }
276
+
277
+ @keyframes fade-in-out-2 {
278
+ 0% {
279
+ opacity: 0;
280
+ }
281
+ 55% {
282
+ opacity: 1;
283
+ }
284
+ 100% {
285
+ opacity: 0;
286
+ }
287
+ }
288
+
289
+ /* end of new loading state */
290
+
291
+
292
+ .gpt-send-button {
293
+ background-color: #14274E;
294
+ width: 100%;
295
+ display: flex;
296
+ justify-content: center;
297
+ border-radius: 10px;
298
+ border: 2px solid #14274E;
299
+ align-items: center;
300
+ cursor: pointer;
301
+ }
302
+
303
+ .gpt-send-button span {
304
+ color: #FCFDFF;
305
+ text-align: center;
306
+ font-size: 20px;
307
+ padding: 10px 8px;
308
+ font-style: normal;
309
+ font-weight: 700;
310
+ line-height: normal;
311
+ letter-spacing: -0.4px;
312
+ }
313
+
314
+ .send-button-blocked {
315
+ background-color: #14274ed8;
316
+ border: 2px solid #14274ed8 !important;
317
+ pointer-events: none;
318
+ cursor: default;
319
+ }
320
+
321
+ .gpt-form {
322
+ /* max-width: var(--reading-width, 48em); */
323
+ margin-right: auto;
324
+ margin-left: auto;
325
+ margin-bottom: 40px;
326
+ }
327
+
328
+ .gpt-input {
329
+ height: 46px;
330
+ width: 100%;
331
+ margin: 10px 0;
332
+ display: flex;
333
+ box-sizing: border-box;
334
+ padding: 2px 20px;
335
+ flex-direction: column;
336
+ justify-content: center;
337
+ align-items: flex-start;
338
+ flex: 1 0 0;
339
+ border-radius: 10px;
340
+ border: 2px solid #14274E;
341
+ background-color: #ffffff;
342
+ color: rgb(42, 43, 42);
343
+ font-size: 14px;
344
+ line-height: 1.2em;
345
+ }
346
+
347
+ .gpt-input::placeholder {
348
+ color: rgba(42, 43, 42, 0.808);
349
+ }
350
+
351
+ .gpt-input.invalid {
352
+ border: 2px solid rgb(219, 0, 0);
353
+ }
354
+
355
+
356
+ .error-message {
357
+ color: rgb(219, 0, 0);
358
+ font-size: 14px;
359
+ margin-top: 5px;
360
+ display: none;
361
+ }
362
+
363
+ .gpt-textarea {
364
+ height: auto;
365
+ resize: vertical;
366
+ padding-top: 20px;
367
+ font-size: 14px;
368
+ }
369
+
370
+ .gpt-textarea::placeholder {
371
+ color: rgba(42, 43, 42, 0.808);
372
+ }
373
+
374
+ .custom_bot_initial_question {
375
+ width: 100%;
376
+ display: flex;
377
+ flex-direction: column;
378
+ row-gap: 10px;
379
+ max-width: 335px;
380
+ margin-left: 20px;
381
+ margin-top: -2.7rem;
382
+ margin-bottom: 40px;
383
+ }
384
+
385
+ .custom_bot_initial_question p {
386
+ padding: 5px 12px;
387
+ font-size: 12px;
388
+ background: #fff;
389
+ border: 2px solid #14274E;
390
+ border-radius: 50px;
391
+ margin: 0;
392
+ transition: 0.3s ease;
393
+ cursor: pointer;
394
+ font-style: italic;
395
+ }
396
+
397
+ .custom_bot_initial_question p:hover {
398
+ background: #14274E;
399
+ color: #fff;
400
+ }
401
+
402
+ @media (max-width: 767px) {
403
+ .App {
404
+ width: 100%;
405
+ height: 100vh;
406
+ left: 0;
407
+ bottom: 0;
408
+ display: none;
409
+ position: fixed;
410
+ z-index: 81;
411
+ box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
412
+ border-radius: 10px;
413
+ /* Дополнительные стили для .app на маленьких экранах */
414
+ }
415
+
416
+ .chat-section {
417
+ height: 100vh;
418
+ }
419
+
420
+ .App .chatui-container {
421
+ padding-left: 15px;
422
+ padding-right: 15px;
423
+ }
424
+
425
+ .App .chatui-container .user-msg {
426
+ padding: 15px 20px;
427
+ margin-bottom: 20px;
428
+ }
429
+
430
+ .App .chatui-container .bot-msg .bot-content-box {
431
+ padding: 5px 20px 5px 20px;
432
+ margin-bottom: 20px;
433
+ }
434
+
435
+ .custom_bot_initial_question {
436
+ margin-top: 0;
437
+ }
438
+
439
+ #chatui-container {
440
+ height: calc(100vh - 95px - 93px);
441
+ }
442
+ }
443
+
444
+
445
+ .overlay {
446
+ position: fixed;
447
+ top: 0;
448
+ left: 0;
449
+ width: 100%;
450
+ height: 100%;
451
+ background: rgba(255, 255, 255, 0.7);
452
+ display: flex;
453
+ align-items: center;
454
+ justify-content: center;
455
+ visibility: hidden;
456
+ z-index: 999;
457
+ }
458
+
459
+ .overlay h1 {
460
+ margin: 0;
461
+ font-size: 2em;
462
+ }
463
+
464
+
465
+ .report-loader {
466
+ position: absolute;
467
+ top: calc(50% - 45px);
468
+ left: calc(50% - 45px);
469
+ width: 90px;
470
+ height: 90px;
471
+ border-radius: 50%;
472
+ perspective: 800px;
473
+ }
474
+
475
+ .report-inner {
476
+ position: absolute;
477
+ box-sizing: border-box;
478
+ width: 100%;
479
+ height: 100%;
480
+ border-radius: 50%;
481
+ }
482
+
483
+ .report-inner.report-one {
484
+ left: 0;
485
+ top: 0;
486
+ animation: rotate-one 1s linear infinite;
487
+ border-bottom: 3px solid #d52121;
488
+ }
489
+
490
+ .report-inner.report-two {
491
+ right: 0%;
492
+ top: 0%;
493
+ animation: rotate-two 1s linear infinite;
494
+ border-right: 3px solid #d52121;
495
+ }
496
+
497
+ .report-inner.report-three {
498
+ right: 0%;
499
+ bottom: 0%;
500
+ animation: rotate-three 1s linear infinite;
501
+ border-top: 3px solid #d52121;
502
+ }
503
+
504
+ @keyframes rotate-one {
505
+ 0% {
506
+ transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
507
+ }
508
+ 100% {
509
+ transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
510
+ }
511
+ }
512
+
513
+ @keyframes rotate-two {
514
+ 0% {
515
+ transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
516
+ }
517
+ 100% {
518
+ transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
519
+ }
520
+ }
521
+
522
+ @keyframes rotate-three {
523
+ 0% {
524
+ transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
525
+ }
526
+ 100% {
527
+ transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
528
+ }
529
+ }
530
+
531
+ #message {
532
+ display: none;
533
+ position: absolute;
534
+ top: 0;
535
+ right: 0;
536
+ background-color: #b3ff00;
537
+ padding: 10px;
538
+ z-index: 100;
539
+ }
540
+
541
+ .message h5{
542
+ margin-bottom: 0;
543
+ }
static/js/voice.js ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const initialMessage = ''
2
+
3
+ const loadingModal = document.getElementById('loadingModal');
4
+ const messageHistory = document.getElementById('chatHistory')
5
+ let mediaRecorder;
6
+ let audioChunks = [];
7
+ let socket
8
+
9
+ function makeLoading() {
10
+ loadingModal.style.visibility = 'visible';
11
+ }
12
+
13
+ function stopLoading() {
14
+ loadingModal.style.visibility = 'hidden';
15
+ }
16
+
17
+ function startCall() {
18
+ const uuid = generateUUID()
19
+ messageHistory.innerHTML = ``
20
+ socket = new WebSocket(`ws://127.0.0.1:8000/ws/${uuid}`);
21
+ socket.onopen = () => {
22
+ startRecording()
23
+ };
24
+
25
+ socket.onclose = (event) => console.log('WebSocket disconnected', event);
26
+ socket.onerror = (error) => {
27
+ alert('Something was wrong. Try again later.')
28
+ window.location.reload()
29
+ };
30
+ socket.onmessage = (event) => playResponse(event.data);
31
+ }
32
+
33
+ const startRecording = () => {
34
+ showMessage('You can speak!')
35
+ navigator.mediaDevices.getUserMedia({audio: true})
36
+ .then(stream => {
37
+ mediaRecorder = new MediaRecorder(stream);
38
+ try {
39
+ mediaRecorder.ondataavailable = (event) => {
40
+ audioChunks.push(event.data)
41
+
42
+ };
43
+ } catch (e) {
44
+ alert('It is not possible to send an empty message')
45
+ }
46
+ mediaRecorder.start();
47
+ })
48
+ .catch(error => {
49
+ console.error('Error accessing the microphone', error);
50
+ });
51
+ };
52
+
53
+ const stopAndSend = () => {
54
+ makeLoading()
55
+ mediaRecorder.ondataavailable = (event) => {
56
+ audioChunks.push(event.data)
57
+ console.log(audioChunks)
58
+
59
+ const audioBlob = new Blob(audioChunks, {type: 'audio/wav'});
60
+ const reader = new FileReader();
61
+ reader.readAsDataURL(audioBlob);
62
+ reader.onloadend = () => {
63
+ let base64String = reader.result;
64
+ base64String = base64String.split(',')[1];
65
+ const dataWS = {
66
+ 'audio': base64String,
67
+ }
68
+ console.log(dataWS)
69
+ socket.send(JSON.stringify(dataWS));
70
+ audioChunks = [];
71
+ }
72
+ }
73
+ try {
74
+ mediaRecorder.stop();
75
+ } catch (e) {
76
+ return
77
+ }
78
+ }
79
+
80
+
81
+ const playResponse = (data, initMessage = false) => {
82
+ data = JSON.parse(data)
83
+ if (!initMessage) {
84
+ const aiResponse = data['ai_response']
85
+ if (aiResponse.startsWith('https://blue-estate-agency.com/') || aiResponse.startsWith('http://blue-estate-agency.com/')) {
86
+ window.open(aiResponse, '_blank');
87
+ }
88
+ }
89
+ const audioSrc = `data:audio/mp3;base64,${data['voice_response']}`;
90
+ const audio = new Audio(audioSrc);
91
+ stopLoading()
92
+ if (!initMessage) {
93
+ createMessage('User', data['user_query'])
94
+ }
95
+ audio.play();
96
+
97
+ audio.onended = () => {
98
+ startRecording()
99
+ if (!initMessage) {
100
+ createMessage('Assistant', data['ai_response'])
101
+ }
102
+ };
103
+ }
104
+
105
+ function endCall() {
106
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
107
+ mediaRecorder.stop();
108
+ audioChunks = []
109
+ }
110
+ if (socket && socket.readyState === WebSocket.OPEN) {
111
+ socket.close();
112
+ }
113
+ showMessage('The call is ended')
114
+ }
115
+
116
+ function generateUUID() {
117
+ const arr = new Uint8Array(16);
118
+ window.crypto.getRandomValues(arr);
119
+
120
+ arr[6] = (arr[6] & 0x0f) | 0x40;
121
+ arr[8] = (arr[8] & 0x3f) | 0x80;
122
+
123
+ return ([...arr].map((b, i) =>
124
+ (i === 4 || i === 6 || i === 8 || i === 10 ? "-" : "") + b.toString(16).padStart(2, "0")
125
+ ).join(""));
126
+ }
127
+
128
+ function showMessage(message_text) {
129
+ const message = document.getElementById('message');
130
+ message.innerText = message_text
131
+ message.style.display = 'block';
132
+
133
+ setTimeout(function () {
134
+ message.style.display = 'none';
135
+ }, 2000);
136
+ }
137
+
138
+ function createMessage(type, message) {
139
+ const newMessage = document.createElement('div')
140
+ newMessage.className = 'message rounded-4 bg-white mb-4 mx-4 py-2 px-3 border'
141
+ newMessage.innerHTML = `
142
+ <h5>${type}</h5>
143
+ ${message}
144
+ `
145
+ messageHistory.appendChild(newMessage)
146
+ }