brestok commited on
Commit
0632cd1
1 Parent(s): 1983cea

Upload 40 files

Browse files
.env ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FASTAPI_CONFIG=production
2
+ SECRET=zu=*nck@&r26rsa$2qv8a7g-p$slw79ym81mylj6s**w(da6&#
3
+ OPENAI_API_KEY=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
4
+
5
+ DATABASE_USER=hectool_ai_filter
6
+ DATABASE_PASSWORD=nixtz3orhepndjw4m1D4
7
+ DATABASE_HOST=localhost
8
+ DATABASE_PORT=5432
9
+ DATABASE_NAME=javea
10
+
11
+ CONFIDENCE_PARAMETER=50
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ faiss_javea.index filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ ENV PYTHONDONTWRITEBYCODE 1
4
+ ENV PYTHONBUFFERED 1
5
+ ENV TRANSFORMERS_CACHE=/istvanai/.cache/huggingface/transformers
6
+
7
+ RUN pip install --upgrade pip
8
+
9
+ WORKDIR /code
10
+
11
+ ADD . /code
12
+
13
+ RUN chmod -R 777 /code
14
+
15
+ RUN pip install -r requirements.txt
16
+
17
+ COPY . .
18
+
19
+ EXPOSE 7860
20
+
21
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
chunks_javea.csv ADDED
The diff for this file is too large to render. See raw diff
 
faiss_javea.index ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:983785b39b3e03719744c884620b11e0b64975ae4388157865abe0b205c02993
3
+ size 3947565
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,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+
5
+ from project.admin import AdminCustom, MessagePairAdmin
6
+ from project.bot.auth import authentication_backend_admin
7
+ from project.database import engine
8
+
9
+
10
+ def create_app() -> FastAPI:
11
+ app = FastAPI()
12
+
13
+ from project.bot import bot_router
14
+ app.include_router(bot_router, tags=['bot'])
15
+
16
+ from project.ws import ws_router
17
+ app.include_router(ws_router, tags=['ws'])
18
+
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["*"],
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ app.mount('/static', StaticFiles(directory="static"), name="static")
27
+
28
+ admin = AdminCustom(app, engine, authentication_backend=authentication_backend_admin)
29
+ admin.add_view(MessagePairAdmin)
30
+ return app
project/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (1.08 kB). View file
 
project/__pycache__/admin.cpython-310.pyc ADDED
Binary file (1.94 kB). View file
 
project/__pycache__/config.cpython-310.pyc ADDED
Binary file (3.46 kB). View file
 
project/__pycache__/database.cpython-310.pyc ADDED
Binary file (1.07 kB). View file
 
project/admin.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqladmin import ModelView, action
2
+ from sqladmin import Admin
3
+
4
+ from fastapi import Request, Response
5
+ from fastapi.responses import RedirectResponse
6
+ from fastapi.templating import Jinja2Templates
7
+
8
+ import logging
9
+
10
+ from project.bot.models import MessagePair
11
+
12
+ template = Jinja2Templates(directory='templates')
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class MessagePairAdmin(ModelView, model=MessagePair):
18
+ category = 'Message History'
19
+ column_list = [MessagePair.user_message, MessagePair.bot_response, MessagePair.country]
20
+ is_async = True
21
+ icon = "fa-solid fa-message"
22
+ name = "Message Pair"
23
+ name_plural = "Message pairs"
24
+ column_searchable_list = [MessagePair.user_message, MessagePair.bot_response, MessagePair.country]
25
+ column_export_list = ['user_message', 'bot_response', 'country']
26
+ can_edit = False
27
+ can_create = False
28
+ column_default_sort = ('id', True)
29
+ column_formatters = {
30
+ MessagePair.bot_response: lambda m, a: m.bot_response[:50] + '...' if len(m.bot_response) > 50
31
+ else m.bot_response}
32
+
33
+
34
+ class AdminCustom(Admin):
35
+
36
+ async def login(self, request: Request) -> Response:
37
+ assert self.authentication_backend is not None
38
+
39
+ context = {"request": request, "error": ""}
40
+
41
+ if request.method == "GET":
42
+ return template.TemplateResponse("login_admin.html", context)
43
+
44
+ ok = await self.authentication_backend.login(request)
45
+ if not ok:
46
+ context["error"] = "Invalid credentials."
47
+ return template.TemplateResponse(
48
+ "login_admin.html", context, status_code=400
49
+ )
50
+
51
+ return RedirectResponse(request.url_for("admin:index"), status_code=302)
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, models
project/bot/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (299 Bytes). View file
 
project/bot/__pycache__/auth.cpython-310.pyc ADDED
Binary file (1.26 kB). View file
 
project/bot/__pycache__/models.cpython-310.pyc ADDED
Binary file (814 Bytes). View file
 
project/bot/__pycache__/openai_backend.cpython-310.pyc ADDED
Binary file (4.79 kB). View file
 
project/bot/__pycache__/views.cpython-310.pyc ADDED
Binary file (567 Bytes). View file
 
project/bot/auth.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+
4
+ from fastapi import Request
5
+ from sqladmin.authentication import AuthenticationBackend as AuthBackendAdmin
6
+
7
+
8
+ class AdminAuth(AuthBackendAdmin):
9
+ async def login(self, request: Request) -> bool:
10
+ form = await request.form()
11
+ username, password = form["username"], form["password"]
12
+ if username == 'hectool24' and password == 'hectoolshopify2024@':
13
+ request.session.update({"session": str(uuid.uuid4())})
14
+ return True
15
+ return False
16
+
17
+ async def logout(self, request: Request) -> bool:
18
+ request.session.clear()
19
+ return True
20
+
21
+ async def authenticate(self, request: Request) -> bool:
22
+ token = request.session.get("session")
23
+ if not token:
24
+ return False
25
+ return True
26
+
27
+
28
+ authentication_backend_admin = AdminAuth(secret_key=os.getenv('SECRET'))
project/bot/documents.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import pandas as pd
3
+
4
+ with open('../../data.json', 'r') as f:
5
+ data = json.load(f)
6
+
7
+ chunks = []
8
+ for post in data:
9
+ post_text = post['text']
10
+ comments: list[dict] = post['comments']
11
+ comments_str = ''
12
+ for i, comment in enumerate(comments):
13
+ comment_text = list(comment.keys())[0]
14
+ replies = comment[comment_text]
15
+ reply_str = 'Replies:\n'
16
+ for j, reply in enumerate(replies):
17
+ if j + 1 == len(replies):
18
+ reply_str += f' • {reply}'
19
+ else:
20
+ reply_str += f' • {reply}\n'
21
+ comments_str += f'{i + 1}. {comment_text}\n'
22
+ if replies:
23
+ comments_str += f'{reply_str}\n'
24
+
25
+ chunk = f"Post: {post_text}\n"
26
+ if comments:
27
+ chunk += f'Comments:\n{comments_str}'
28
+ chunks.append(chunk)
29
+ #
30
+ df = pd.DataFrame({"chunks": chunks})
31
+ df.to_csv('chunks_javea.csv', index=False)
32
+
33
+ # for post in data:
34
+ # post_text = post['text']
35
+ # comments: list[dict] = post['comments']
36
+ # comments_str = ''
37
+ # for i, comment in enumerate(comments):
38
+ # comment_text = list(comment.keys())[0]
39
+ # replies = comment[comment_text]
40
+ # reply_str = '\n'
41
+ # for j, reply in enumerate(replies):
42
+ # if j + 1 == len(replies):
43
+ # reply_str += f'{reply}'
44
+ # else:
45
+ # reply_str += f'{reply}\n'
46
+ # comments_str += f'{comment_text}\n'
47
+ # if replies:
48
+ # comments_str += f'{reply_str}\n'
49
+ #
50
+ # chunk = f"{post_text}\n"
51
+ # if comments:
52
+ # chunk += f'\n{comments_str}'
53
+ # chunks.append(chunk)
54
+
55
+ # df = pd.DataFrame({"chunks": chunks})
56
+ # df.to_csv('chunks_javea_raw.csv', index=False)
project/bot/models.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, String, Integer
2
+
3
+ from project.database import Base
4
+
5
+
6
+ class MessagePair(Base):
7
+ __tablename__ = 'message_pair'
8
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
9
+ user_message = Column(String)
10
+ bot_response = Column(String)
11
+ country = Column(String, default='Undefined')
12
+
13
+ def __init__(self, user_message, bot_response, country):
14
+ self.user_message = user_message
15
+ self.bot_response = bot_response
16
+ self.country = country
project/bot/openai_backend.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from typing import List, Dict
3
+ import faiss
4
+ import numpy as np
5
+ import pandas as pd
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+ from starlette.websockets import WebSocket
8
+
9
+ from project.bot.models import MessagePair
10
+ from project.config import settings
11
+
12
+
13
+ class SearchBot:
14
+ chat_history = []
15
+ # is_unknown = False
16
+ # unknown_counter = 0
17
+
18
+ def __init__(self, memory=None):
19
+ if memory is None:
20
+ memory = []
21
+ self.chat_history = memory
22
+
23
+ async def _summarize_user_intent(self, user_query: str) -> str:
24
+ chat_history_str = ''
25
+ chat_history = self.chat_history[-self.unknown_counter * 2:]
26
+ for i in chat_history:
27
+ if i['role'] == 'user':
28
+ chat_history_str += f"{i['role']}: {i['content']}\n"
29
+ messages = [
30
+ {
31
+ 'role': 'system',
32
+ 'content': f"{settings.SUMMARIZE_PROMPT}\n"
33
+ f"Chat history: ```{chat_history_str}```\n"
34
+ f"User query: ```{user_query}```"
35
+ }
36
+ ]
37
+ response = await settings.OPENAI_CLIENT.chat.completions.create(
38
+ messages=messages,
39
+ temperature=0.1,
40
+ n=1,
41
+ model="gpt-3.5-turbo-0125"
42
+ )
43
+ user_intent = response.choices[0].message.content
44
+ return user_intent
45
+
46
+ @staticmethod
47
+ def _cls_pooling(model_output):
48
+ return model_output.last_hidden_state[:, 0]
49
+
50
+ async def _convert_to_embeddings(self, text_list):
51
+ encoded_input = settings.INFO_TOKENIZER(
52
+ text_list, padding=True, truncation=True, return_tensors="pt"
53
+ )
54
+ encoded_input = {k: v.to(settings.device) for k, v in encoded_input.items()}
55
+ model_output = settings.INFO_MODEL(**encoded_input)
56
+ return self._cls_pooling(model_output).cpu().detach().numpy().astype('float32')
57
+
58
+ @staticmethod
59
+ async def _get_context_data(user_query: list[float]) -> list[dict]:
60
+ radius = 30
61
+ _, distances, indices = settings.FAISS_INDEX.range_search(user_query, radius)
62
+ indices_distances_df = pd.DataFrame({'index': indices, 'distance': distances})
63
+ filtered_data_df = settings.products_dataset.iloc[indices].copy()
64
+ filtered_data_df.loc[:, 'distance'] = indices_distances_df['distance'].values
65
+ sorted_data_df: pd.DataFrame = filtered_data_df.sort_values(by='distance').reset_index(drop=True)
66
+ sorted_data_df = sorted_data_df.drop('distance', axis=1)
67
+ data = sorted_data_df.head(3).to_dict(orient='records')
68
+ return data
69
+
70
+ @staticmethod
71
+ async def create_context_str(context: List[Dict]) -> str:
72
+ context_str = ''
73
+ for i, chunk in enumerate(context):
74
+ context_str += f'{i + 1}) {chunk["chunks"]}'
75
+ return context_str
76
+
77
+ async def _rag(self, context: List[Dict], query: str, session: AsyncSession, country: str):
78
+ if context:
79
+ context_str = await self.create_context_str(context)
80
+ assistant_message = {"role": 'assistant', "content": context_str}
81
+ self.chat_history.append(assistant_message)
82
+ content = settings.PROMPT
83
+ else:
84
+ content = settings.EMPTY_PROMPT
85
+ user_message = {"role": 'user', "content": query}
86
+
87
+ self.chat_history.append(user_message)
88
+ messages = [
89
+ {
90
+ 'role': 'system',
91
+ 'content': content
92
+ },
93
+ ]
94
+ messages = messages + self.chat_history
95
+
96
+ stream = await settings.OPENAI_CLIENT.chat.completions.create(
97
+ messages=messages,
98
+ temperature=0.1,
99
+ n=1,
100
+ model="gpt-3.5-turbo",
101
+ stream=True
102
+ )
103
+ response = ''
104
+ async for chunk in stream:
105
+ if chunk.choices[0].delta.content is not None:
106
+ chunk_content = chunk.choices[0].delta.content
107
+ response += chunk_content
108
+ yield response
109
+ await asyncio.sleep(0.02)
110
+ assistant_message = {"role": 'assistant', "content": response}
111
+ self.chat_history.append(assistant_message)
112
+ try:
113
+ session.add(MessagePair(user_message=query, bot_response=response, country=country))
114
+ except Exception as e:
115
+ print(e)
116
+
117
+ async def ask_and_send(self, data: Dict, websocket: WebSocket, session: AsyncSession):
118
+ query = data['query']
119
+ country = data['country']
120
+ transformed_query = await self._convert_to_embeddings(query)
121
+ context = await self._get_context_data(transformed_query)
122
+ try:
123
+ async for chunk in self._rag(context, query, session, country):
124
+ await websocket.send_text(chunk)
125
+ # await websocket.send_text('finish')
126
+ except Exception:
127
+ await self.emergency_db_saving(session)
128
+
129
+ @staticmethod
130
+ async def emergency_db_saving(session: AsyncSession):
131
+ await session.commit()
132
+ await session.close()
project/bot/schemas.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # from pydantic import BaseModel
2
+ #
3
+ #
4
+ # class UserQuery(BaseModel):
5
+ # query: str
project/bot/templates/home.html ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Hector AI</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
+ <!-- Connect style.css -->
15
+ <link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
16
+ <!-- <link rel="stylesheet" href="../../../static/css/style.css">-->
17
+ </head>
18
+
19
+ <div class="container-fluid px-0">
20
+ <div class="row mx-0">
21
+ <div class="app px-0">
22
+ <div id="toggleButton" class="rounded-4">
23
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="60px" height="60px">
24
+ <path d="M 25 4.5 C 15.204 4.5 5.9439688 11.985969 3.9179688 21.542969 C 3.9119687 21.571969 3.9200156 21.599906 3.9160156 21.628906 C 1.5620156 23.233906 -0.04296875 26.383 -0.04296875 30 C -0.04296875 35.238 3.3210312 39.5 7.4570312 39.5 C 7.7850313 39.5 8.0913438 39.339313 8.2773438 39.070312 C 8.4643437 38.800312 8.5065781 38.456438 8.3925781 38.148438 C 8.3775781 38.110438 6.9550781 34.244 6.9550781 29.5 C 6.9550781 24.506 8.3091719 22.022187 8.3261719 21.992188 C 8.5011719 21.683187 8.4983125 21.305047 8.3203125 20.998047 C 8.1433125 20.689047 7.8130313 20.5 7.4570312 20.5 C 7.0350313 20.5 6.62275 20.554625 6.21875 20.640625 C 8.58675 12.613625 16.57 6.5 25 6.5 C 32.992 6.5 40.688641 12.044172 43.431641 19.576172 C 43.133641 19.530172 42.831438 19.5 42.523438 19.5 C 42.169438 19.5 41.841109 19.689094 41.662109 19.996094 C 41.482109 20.302094 41.481297 20.683187 41.654297 20.992188 C 41.668297 21.016188 43.023437 23.5 43.023438 28.5 C 43.023438 32.44 42.045078 35.767641 41.705078 36.806641 C 40.558078 37.740641 38.815344 39.034297 36.777344 40.154297 C 36.016344 39.305297 34.839391 38.873437 33.650391 39.148438 L 31.867188 39.558594 C 31.024188 39.751594 30.308609 40.262094 29.849609 40.996094 C 29.391609 41.728094 29.245453 42.5965 29.439453 43.4375 C 29.783453 44.9335 31.11975 45.949219 32.59375 45.949219 C 32.83275 45.949219 33.074359 45.923187 33.318359 45.867188 L 35.103516 45.455078 C 35.945516 45.262078 36.661141 44.752531 37.119141 44.019531 C 37.503141 43.406531 37.653984 42.698234 37.583984 41.990234 C 39.728984 40.828234 41.570453 39.481469 42.814453 38.480469 C 46.814453 38.285469 50.023438 34.114 50.023438 29 C 50.023438 25.237 48.284437 21.989172 45.773438 20.451172 C 45.769438 20.376172 45.777859 20.301563 45.755859 20.226562 C 43.152859 11.113563 34.423 4.5 25 4.5 z M 12 19 C 11.447 19 11 19.447 11 20 L 11 32 C 11 32.553 11.447 33 12 33 L 28.044922 33 C 27.540922 34.057 26.743578 35.482375 26.142578 36.484375 C 25.941578 36.819375 25.954828 37.2405 26.173828 37.5625 C 26.360828 37.8395 26.673 38 27 38 C 27.055 38 27.109063 37.995328 27.164062 37.986328 C 33.351062 36.955328 38.412 32.95125 38.625 32.78125 C 38.862 32.59125 39 32.304 39 32 L 39 20 C 39 19.447 38.553 19 38 19 L 12 19 z M 13 21 L 37 21 L 37 31.501953 C 35.952 32.266953 32.821953 34.393672 29.001953 35.513672 C 29.643953 34.334672 30.328469 32.955266 30.480469 32.197266 C 30.539469 31.903266 30.462438 31.598187 30.273438 31.367188 C 30.082438 31.135188 29.8 31 29.5 31 L 13 31 L 13 21 z M 44.121094 21.822266 C 46.378094 22.758266 48.023437 25.622 48.023438 29 C 48.023438 32.456 46.299891 35.373281 43.962891 36.238281 C 44.420891 34.565281 45.023438 31.747 45.023438 28.5 C 45.023438 25.445 44.556094 23.226266 44.121094 21.822266 z M 5.859375 22.822266 C 5.423375 24.225266 4.9570313 26.445 4.9570312 29.5 C 4.9570312 32.747 5.5595781 35.565281 6.0175781 37.238281 C 3.6805781 36.373281 1.9570312 33.456 1.9570312 30 C 1.9570312 26.622 3.602375 23.758266 5.859375 22.822266 z M 18.5 23 C 17.098 23 16 24.317 16 26 C 16 27.683 17.098 29 18.5 29 C 19.902 29 21 27.683 21 26 C 21 24.317 19.902 23 18.5 23 z M 31.5 23 C 30.098 23 29 24.317 29 26 C 29 27.683 30.098 29 31.5 29 C 32.902 29 34 27.683 34 26 C 34 24.317 32.902 23 31.5 23 z M 18.5 25 C 18.677 25 19 25.38 19 26 C 19 26.62 18.677 27 18.5 27 C 18.323 27 18 26.62 18 26 C 18 25.38 18.323 25 18.5 25 z M 31.5 25 C 31.677 25 32 25.38 32 26 C 32 26.62 31.677 27 31.5 27 C 31.323 27 31 26.62 31 26 C 31 25.38 31.323 25 31.5 25 z M 34.376953 41.064453 C 34.605953 41.064453 34.83225 41.128906 35.03125 41.253906 C 35.31025 41.428906 35.504125 41.702391 35.578125 42.025391 C 35.652125 42.348391 35.598828 42.678984 35.423828 42.958984 C 35.248828 43.237984 34.976297 43.433812 34.654297 43.507812 L 34.652344 43.507812 L 32.869141 43.917969 C 32.208141 44.071969 31.540672 43.654234 31.388672 42.990234 C 31.314672 42.668234 31.369922 42.337641 31.544922 42.056641 C 31.719922 41.777641 31.992453 41.581813 32.314453 41.507812 L 34.097656 41.097656 C 34.190656 41.076656 34.284953 41.064453 34.376953 41.064453 z"
25
+ fill="#008dff"/>
26
+ </svg>
27
+ </div>
28
+ <div class="chatbot-window" id="chatbotWindow" style="visibility: hidden;">
29
+ <div class="chat-header d-flex align-items-center justify-content-between py-2" id="chatHeader">
30
+ <div class="d-flex align-items-center">
31
+ <div style="color: #008dff; font-size: 30px; letter-spacing: 7px" class="fw-bold me-3 ms-3">
32
+ Hector AI
33
+ </div>
34
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"
35
+ class="">
36
+ <path d="M 25 4.5 C 15.204 4.5 5.9439688 11.985969 3.9179688 21.542969 C 3.9119687 21.571969 3.9200156 21.599906 3.9160156 21.628906 C 1.5620156 23.233906 -0.04296875 26.383 -0.04296875 30 C -0.04296875 35.238 3.3210312 39.5 7.4570312 39.5 C 7.7850313 39.5 8.0913438 39.339313 8.2773438 39.070312 C 8.4643437 38.800312 8.5065781 38.456438 8.3925781 38.148438 C 8.3775781 38.110438 6.9550781 34.244 6.9550781 29.5 C 6.9550781 24.506 8.3091719 22.022187 8.3261719 21.992188 C 8.5011719 21.683187 8.4983125 21.305047 8.3203125 20.998047 C 8.1433125 20.689047 7.8130313 20.5 7.4570312 20.5 C 7.0350313 20.5 6.62275 20.554625 6.21875 20.640625 C 8.58675 12.613625 16.57 6.5 25 6.5 C 32.992 6.5 40.688641 12.044172 43.431641 19.576172 C 43.133641 19.530172 42.831438 19.5 42.523438 19.5 C 42.169438 19.5 41.841109 19.689094 41.662109 19.996094 C 41.482109 20.302094 41.481297 20.683187 41.654297 20.992188 C 41.668297 21.016188 43.023437 23.5 43.023438 28.5 C 43.023438 32.44 42.045078 35.767641 41.705078 36.806641 C 40.558078 37.740641 38.815344 39.034297 36.777344 40.154297 C 36.016344 39.305297 34.839391 38.873437 33.650391 39.148438 L 31.867188 39.558594 C 31.024188 39.751594 30.308609 40.262094 29.849609 40.996094 C 29.391609 41.728094 29.245453 42.5965 29.439453 43.4375 C 29.783453 44.9335 31.11975 45.949219 32.59375 45.949219 C 32.83275 45.949219 33.074359 45.923187 33.318359 45.867188 L 35.103516 45.455078 C 35.945516 45.262078 36.661141 44.752531 37.119141 44.019531 C 37.503141 43.406531 37.653984 42.698234 37.583984 41.990234 C 39.728984 40.828234 41.570453 39.481469 42.814453 38.480469 C 46.814453 38.285469 50.023438 34.114 50.023438 29 C 50.023438 25.237 48.284437 21.989172 45.773438 20.451172 C 45.769438 20.376172 45.777859 20.301563 45.755859 20.226562 C 43.152859 11.113563 34.423 4.5 25 4.5 z M 12 19 C 11.447 19 11 19.447 11 20 L 11 32 C 11 32.553 11.447 33 12 33 L 28.044922 33 C 27.540922 34.057 26.743578 35.482375 26.142578 36.484375 C 25.941578 36.819375 25.954828 37.2405 26.173828 37.5625 C 26.360828 37.8395 26.673 38 27 38 C 27.055 38 27.109063 37.995328 27.164062 37.986328 C 33.351062 36.955328 38.412 32.95125 38.625 32.78125 C 38.862 32.59125 39 32.304 39 32 L 39 20 C 39 19.447 38.553 19 38 19 L 12 19 z M 13 21 L 37 21 L 37 31.501953 C 35.952 32.266953 32.821953 34.393672 29.001953 35.513672 C 29.643953 34.334672 30.328469 32.955266 30.480469 32.197266 C 30.539469 31.903266 30.462438 31.598187 30.273438 31.367188 C 30.082438 31.135188 29.8 31 29.5 31 L 13 31 L 13 21 z M 44.121094 21.822266 C 46.378094 22.758266 48.023437 25.622 48.023438 29 C 48.023438 32.456 46.299891 35.373281 43.962891 36.238281 C 44.420891 34.565281 45.023438 31.747 45.023438 28.5 C 45.023438 25.445 44.556094 23.226266 44.121094 21.822266 z M 5.859375 22.822266 C 5.423375 24.225266 4.9570313 26.445 4.9570312 29.5 C 4.9570312 32.747 5.5595781 35.565281 6.0175781 37.238281 C 3.6805781 36.373281 1.9570312 33.456 1.9570312 30 C 1.9570312 26.622 3.602375 23.758266 5.859375 22.822266 z M 18.5 23 C 17.098 23 16 24.317 16 26 C 16 27.683 17.098 29 18.5 29 C 19.902 29 21 27.683 21 26 C 21 24.317 19.902 23 18.5 23 z M 31.5 23 C 30.098 23 29 24.317 29 26 C 29 27.683 30.098 29 31.5 29 C 32.902 29 34 27.683 34 26 C 34 24.317 32.902 23 31.5 23 z M 18.5 25 C 18.677 25 19 25.38 19 26 C 19 26.62 18.677 27 18.5 27 C 18.323 27 18 26.62 18 26 C 18 25.38 18.323 25 18.5 25 z M 31.5 25 C 31.677 25 32 25.38 32 26 C 32 26.62 31.677 27 31.5 27 C 31.323 27 31 26.62 31 26 C 31 25.38 31.323 25 31.5 25 z M 34.376953 41.064453 C 34.605953 41.064453 34.83225 41.128906 35.03125 41.253906 C 35.31025 41.428906 35.504125 41.702391 35.578125 42.025391 C 35.652125 42.348391 35.598828 42.678984 35.423828 42.958984 C 35.248828 43.237984 34.976297 43.433812 34.654297 43.507812 L 34.652344 43.507812 L 32.869141 43.917969 C 32.208141 44.071969 31.540672 43.654234 31.388672 42.990234 C 31.314672 42.668234 31.369922 42.337641 31.544922 42.056641 C 31.719922 41.777641 31.992453 41.581813 32.314453 41.507812 L 34.097656 41.097656 C 34.190656 41.076656 34.284953 41.064453 34.376953 41.064453 z"
37
+ fill="#008dff"/>
38
+ </svg>
39
+ </div>
40
+ <div style="color: #008dff; cursor: pointer" onclick="closeChatBotWindow()">
41
+ <i class="fa-solid fa-xmark fs-1 me-3"></i>
42
+ </div>
43
+ </div>
44
+ <div class="chat-body" id="chatBody">
45
+ <div class="message">
46
+ <div class="bot_message mt-4 py-3 px-4 rounded-4 w-75 ms-3">
47
+ Hallo! Ik ben een virtuele assistent, een helper voor Nederlanders die naar Javea zijn
48
+ gemigreerd. Ik weet alles over deze regio en zal je helpen om je aan te passen in je nieuwe
49
+ thuis. Waarmee kan ik je helpen?
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <div class="chat-footer px-3 mb-2 bg-white pt-3" id="chatFooter">
54
+ <input type="text" class="form-control rounded-5" id="textInput"
55
+ placeholder="Stellen Sie eine Frage" style="height: 50px">
56
+ <div style="cursor: pointer; background-color: #105ead; height: 50px"
57
+ class="rounded-5 mt-3 d-flex text-white justify-content-center align-items-center fs-4"
58
+ id="sendButton">
59
+ <span>Send <img src="../../../static/images/send.png" alt="" height="35px"></span>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ </html>
68
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
69
+ integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
70
+ crossorigin="anonymous"></script>
71
+ <script src="https://code.jquery.com/jquery-3.6.3.min.js"
72
+ integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
73
+ <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
74
+ <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
75
+ <script src="https://kit.fontawesome.com/d4ffd37f75.js" crossorigin="anonymous"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
77
+ <!--<script type="text/javascript" src="../../../static/js/main.js"></script>-->
78
+ <!--<script type="text/javascript" src="../../../static/js/utils.js"></script>-->
79
+ <!--<script type="text/javascript" src="../../../static/js/ws.js"></script>-->
80
+ <script type="text/javascript" src="{{ url_for('static', path='/js/main.js') }}"></script>
81
+ <script type="text/javascript" src="{{ url_for('static', path='/js/utils.js') }}"></script>
82
+ <script type="text/javascript" src="{{ url_for('static', path='/js/ws.js') }}"></script>
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='main')
10
+ async def main(request: Request):
11
+ return template.TemplateResponse("home.html", {'request': request})
project/config.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import faiss
4
+ import pandas as pd
5
+ from openai import AsyncOpenAI
6
+ import pathlib
7
+ from functools import lru_cache
8
+ from environs import Env
9
+ from transformers import AutoModel, AutoTokenizer
10
+ import torch
11
+
12
+ env = Env()
13
+ env.read_env()
14
+
15
+
16
+ class BaseConfig:
17
+ BASE_DIR: pathlib.Path = pathlib.Path(__file__).parent.parent
18
+ DATA_DIR: pathlib.Path = BASE_DIR / 'project' / 'data'
19
+ MODEL_NAME = 'sentence-transformers/paraphrase-multilingual-mpnet-base-v2'
20
+ INFO_MODEL = AutoModel.from_pretrained(MODEL_NAME)
21
+ INFO_TOKENIZER = AutoTokenizer.from_pretrained(MODEL_NAME)
22
+ OPENAI_CLIENT = AsyncOpenAI(api_key=os.getenv('OPENAI_API_KEY'))
23
+ FAISS_INDEX = faiss.read_index(str(BASE_DIR / 'faiss_javea.index'))
24
+
25
+
26
+ class DevelopmentConfig(BaseConfig):
27
+ pass
28
+
29
+
30
+ class ProductionConfig(BaseConfig):
31
+ DATABASE_URL = f"postgresql+asyncpg://{env('DATABASE_USER')}:" \
32
+ f"{env('DATABASE_PASSWORD')}@" \
33
+ f"{env('DATABASE_HOST')}:" \
34
+ f"{env('DATABASE_PORT')}/" \
35
+ f"{env('DATABASE_NAME')}"
36
+ PROMPT = "Je bent een expert in de regio Javea in Italië, die alles weet om mensen te helpen die migreren van " \
37
+ "Nederland naar Spanje. Je taak is om mensen te helpen zich te vestigen in de nieuwe stad. Gebruik " \
38
+ "kennis uit je vorige antwoord (voornamelijk uit opmerkingen) om een informatief antwoord " \
39
+ "te geven op de gebruikersvraag. Vermeld nooit dat je kennis haalt uit posts of opmerkingen. Spreek " \
40
+ "vanuit jezelf."
41
+ EMPTY_PROMPT = "Je bent een expert in Javea aan de Costa Blanca in Spanje, met uitgebreide kennis om Nederlanders " \
42
+ "te helpen die naar deze regio verhuizen. Je taak is om mensen te helpen zich thuis te voelen in " \
43
+ "hun nieuwe stad. Gebruik je kennis over deze regio maar informatieve antwoorden te " \
44
+ "geven op de vragen van gebruikers."
45
+ SUMMARIZE_PROMPT = "Study the user's requests, paying special attention to the specific mentioned wishes when " \
46
+ "choosing a house. Combine these details into a single query that reflects all the user's " \
47
+ "needs. Formulate your answer as if you were a user, clearly and concisely stating the " \
48
+ "requirements. Make sure that all relevant user wishes are indicated in your response. "
49
+
50
+ def __init__(self):
51
+ if torch.cuda.is_available():
52
+ device = torch.device("cuda")
53
+
54
+ else:
55
+ device = torch.device("cpu")
56
+ self.device = device
57
+ self.INFO_MODEL.to(device)
58
+ self.products_dataset = pd.read_csv(self.BASE_DIR / 'chunks_javea.csv')
59
+
60
+
61
+ class TestConfig(BaseConfig):
62
+ pass
63
+
64
+
65
+ @lru_cache()
66
+ def get_settings() -> DevelopmentConfig | ProductionConfig | TestConfig:
67
+ config_cls_dict = {
68
+ 'development': DevelopmentConfig,
69
+ 'production': ProductionConfig,
70
+ 'testing': TestConfig
71
+ }
72
+ config_name = env('FASTAPI_CONFIG', default='development')
73
+ config_cls = config_cls_dict[config_name]
74
+ return config_cls()
75
+
76
+
77
+ settings = get_settings()
project/database.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import AsyncGenerator
2
+
3
+ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine, async_sessionmaker, AsyncSession
4
+ from sqlalchemy.orm import declarative_base
5
+
6
+ from project.config import settings
7
+
8
+ Base = declarative_base()
9
+
10
+
11
+ def get_async_engine(url: str) -> AsyncEngine:
12
+ return create_async_engine(url=url, echo=True, pool_pre_ping=True)
13
+
14
+
15
+ def get_async_sessionmaker(engine: AsyncEngine) -> async_sessionmaker:
16
+ return async_sessionmaker(bind=engine, class_=AsyncSession)
17
+
18
+
19
+ engine = get_async_engine(
20
+ settings.DATABASE_URL
21
+ )
22
+ async_session_maker = get_async_sessionmaker(engine)
23
+
24
+
25
+ async def get_async_session() -> AsyncSession:
26
+ async with async_session_maker() as session:
27
+ return session
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 (254 Bytes). View file
 
project/ws/__pycache__/views.cpython-310.pyc ADDED
Binary file (793 Bytes). View file
 
project/ws/views.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import WebSocket, WebSocketDisconnect
2
+
3
+ from . import ws_router
4
+ from ..bot.openai_backend import SearchBot
5
+ from ..database import get_async_session
6
+
7
+
8
+ @ws_router.websocket("/ws/{client_id}")
9
+ async def websocket_endpoint(websocket: WebSocket, client_id: str):
10
+ await websocket.accept()
11
+ chatbot = SearchBot()
12
+ session = await get_async_session()
13
+ try:
14
+ while True:
15
+ data = await websocket.receive_json()
16
+ await chatbot.ask_and_send(data, websocket, session)
17
+ except WebSocketDisconnect:
18
+ await session.commit()
19
+ await session.close()
requirements.txt ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.6.0
2
+ anyio==4.3.0
3
+ async-timeout==4.0.3
4
+ asyncpg==0.29.0
5
+ certifi==2024.2.2
6
+ charset-normalizer==3.3.2
7
+ click==8.1.7
8
+ distro==1.9.0
9
+ environs==11.0.0
10
+ exceptiongroup==1.2.0
11
+ faiss-gpu==1.7.2
12
+ fastapi==0.110.0
13
+ filelock==3.13.4
14
+ fsspec==2024.3.1
15
+ greenlet==3.0.3
16
+ h11==0.14.0
17
+ httpcore==1.0.5
18
+ httptools==0.6.1
19
+ httpx==0.27.0
20
+ huggingface-hub==0.22.2
21
+ idna==3.6
22
+ itsdangerous==2.1.2
23
+ Jinja2==3.1.3
24
+ MarkupSafe==2.1.5
25
+ marshmallow==3.21.1
26
+ mpmath==1.3.0
27
+ networkx==3.3
28
+ numpy==1.26.4
29
+ nvidia-cublas-cu12==12.1.3.1
30
+ nvidia-cuda-cupti-cu12==12.1.105
31
+ nvidia-cuda-nvrtc-cu12==12.1.105
32
+ nvidia-cuda-runtime-cu12==12.1.105
33
+ nvidia-cudnn-cu12==8.9.2.26
34
+ nvidia-cufft-cu12==11.0.2.54
35
+ nvidia-curand-cu12==10.3.2.106
36
+ nvidia-cusolver-cu12==11.4.5.107
37
+ nvidia-cusparse-cu12==12.1.0.106
38
+ nvidia-nccl-cu12==2.19.3
39
+ nvidia-nvjitlink-cu12==12.4.127
40
+ nvidia-nvtx-cu12==12.1.105
41
+ openai==1.17.1
42
+ packaging==24.0
43
+ pandas==2.2.2
44
+ pydantic==2.6.4
45
+ pydantic_core==2.16.3
46
+ python-dateutil==2.9.0.post0
47
+ python-dotenv==1.0.1
48
+ python-multipart==0.0.9
49
+ pytz==2024.1
50
+ PyYAML==6.0.1
51
+ regex==2023.12.25
52
+ requests==2.31.0
53
+ safetensors==0.4.2
54
+ six==1.16.0
55
+ sniffio==1.3.1
56
+ sqladmin==0.16.1
57
+ SQLAlchemy==2.0.29
58
+ starlette==0.36.3
59
+ sympy==1.12
60
+ tokenizers==0.15.2
61
+ torch==2.2.2
62
+ tqdm==4.66.2
63
+ transformers==4.39.3
64
+ triton==2.2.0
65
+ typing_extensions==4.10.0
66
+ tzdata==2024.1
67
+ urllib3==2.2.1
68
+ uvicorn==0.29.0
69
+ uvloop==0.19.0
70
+ watchfiles==0.21.0
71
+ websockets==12.0
72
+ WTForms==3.1.2
static/css/style.css ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --main-blue-color: #0e88df;
3
+ --main-blue-color-hover: #f51958;
4
+ --main-bg-color: #e8edf3;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Montserrat', sans-serif;
9
+ }
10
+
11
+ .chat-header {
12
+ background-color: #f1f1f1;
13
+ border-bottom: 1px solid #bdbdbd;
14
+ }
15
+
16
+ .chat-body {
17
+ background-image: url('../../static/images/bg.png');
18
+ background-size: cover;
19
+ padding-top: 1px;
20
+ border-top: 1px solid transparent;
21
+ display: flex;
22
+ flex-direction: column;
23
+ overflow-y: auto;
24
+ }
25
+
26
+ .chatbot-window {
27
+ border: 1px solid #bdbdbd;
28
+ }
29
+
30
+ /*::placeholder {*/
31
+ /* font-size: 30px;*/
32
+ /*}*/
33
+
34
+ .message {
35
+ max-width: 100%;
36
+ display: flex;
37
+ justify-content: flex-start;
38
+ margin-top: 5px;
39
+ font-size: 14px;
40
+ }
41
+
42
+ .user_message {
43
+ background-color: #f3f2f2;
44
+ margin-left: auto;
45
+ border: 1px solid #d7d7d7;
46
+ /*font-size: 20px;*/
47
+ }
48
+
49
+ .bot_message {
50
+ margin-right: auto;
51
+ background-color: #d1e8ff;
52
+ border: 1px solid #71beff;
53
+ }
54
+
55
+ .bot_message p {
56
+ margin-bottom: 0;
57
+ }
58
+
59
+ .bot_message ol {
60
+ margin-bottom: 0;
61
+ }
62
+
63
+ .bot_message ul {
64
+ margin-bottom: 0;
65
+ }
66
+
67
+ .bot_message h3 {
68
+ margin-top: 0.5rem;
69
+ font-size: 20px;
70
+ }
71
+
72
+ #toggleButton {
73
+ cursor: pointer;
74
+ position: fixed;
75
+ right: 15px;
76
+ bottom: 15px;
77
+ background-color: #f7f9fc;
78
+ border: 1px solid #ddd;
79
+ padding: 20px;
80
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
81
+ }
static/images/alcolm_logo.png ADDED
static/images/bg.png ADDED
static/images/icons8-ai-chatting-500.svg ADDED
static/images/send.png ADDED
static/js/main.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const chatBody = document.getElementById('chatBody')
2
+ const openButton = document.getElementById('toggleButton')
3
+ const chatbotWindow = document.getElementById('chatbotWindow');
4
+
5
+ let socket
6
+ let isFirstWord = false
7
+ let botMessage
8
+
9
+ window.addEventListener('load', adjustChatBodyHeight);
10
+ window.addEventListener('resize', adjustChatBodyHeight);
11
+
12
+ function getTotalHeight(element) {
13
+ const styles = window.getComputedStyle(element);
14
+ const margins = ['marginTop', 'marginBottom', 'borderTopWidth', 'borderBottomWidth']
15
+ .reduce((acc, style) => acc + parseFloat(styles[style]), 0);
16
+ return element.offsetHeight + margins;
17
+ }
18
+
19
+ function adjustChatBodyHeight() {
20
+ const chatFooter = document.getElementById('chatFooter')
21
+ const chatHeader = document.getElementById('chatHeader')
22
+ const chatFooterHeight = getTotalHeight(chatFooter);
23
+ const chatHeaderHeight = getTotalHeight(chatHeader);
24
+ const openButtonWindow = getTotalHeight(openButton)
25
+ const viewportHeight = window.innerHeight - chatHeaderHeight - chatFooterHeight;
26
+ chatBody.style.height = viewportHeight + 'px';
27
+ }
28
+
29
+ function openChatBotWindow() {
30
+ let lastScrollHeight = chatBody.scrollHeight;
31
+ const uuid = generateUUID()
32
+ socket = new WebSocket(`ws://127.0.0.1:8000/ws/${uuid}`);
33
+ socket.onclose = (event) => console.log('WebSocket disconnected', event);
34
+ socket.onerror = (error) => {
35
+ alert('Something was wrong. Try again later.')
36
+ window.location.reload()
37
+ };
38
+ socket.onmessage = (event) => {
39
+ if (chatBody.scrollHeight > lastScrollHeight) {
40
+ chatBody.scrollTop = chatBody.scrollHeight;
41
+ lastScrollHeight = chatBody.scrollHeight;
42
+ }
43
+ if (!isFirstWord) {
44
+ isFirstWord = true
45
+ createNewMessage(event.data, 'bot')
46
+ const botMessages = document.querySelectorAll('.bot_message');
47
+ botMessage = botMessages[botMessages.length - 1];
48
+ } else {
49
+ botMessage.innerHTML = marked.parse(event.data)
50
+ }
51
+ }
52
+ }
53
+
54
+ function closeChatBotWindow() {
55
+ socket.close()
56
+ chatbotWindow.style.visibility = 'hidden'
57
+ openButton.style.display = 'block'
58
+ }
59
+
60
+ openButton.addEventListener('click', function () {
61
+ chatbotWindow.style.visibility = "visible";
62
+ openButton.style.display = 'none'
63
+ openChatBotWindow();
64
+ });
static/js/utils.js ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const sendButton = document.getElementById('sendButton')
2
+ const textInput = document.getElementById('textInput')
3
+
4
+ function createNewMessage(text, type) {
5
+ const message = document.createElement('div')
6
+ message.className = 'message'
7
+ let content = document.createElement('div')
8
+ if (type === 'bot') {
9
+ content.className = 'bot_message mt-4 py-3 px-4 rounded-4 w-75 ms-3'
10
+ } else {
11
+ content.className = 'user_message mt-4 py-3 px-4 rounded-4 w-75 me-3'
12
+ }
13
+ content.innerText = text
14
+ message.appendChild(content)
15
+ chatBody.appendChild(message)
16
+ }
17
+
18
+ function sendMessageToServer() {
19
+ const userQuery = textInput.value;
20
+ if (userQuery.length === 0) {
21
+ return
22
+ }
23
+ createNewMessage(userQuery, 'user')
24
+ socket.send(JSON.stringify({'query': userQuery, 'country': "Undefined"}));
25
+ textInput.value = ''
26
+ isFirstWord = false
27
+ chatBody.scrollTop = chatBody.scrollHeight;
28
+ }
29
+
30
+ function generateUUID() {
31
+ const arr = new Uint8Array(16);
32
+ window.crypto.getRandomValues(arr);
33
+
34
+ arr[6] = (arr[6] & 0x0f) | 0x40;
35
+ arr[8] = (arr[8] & 0x3f) | 0x80;
36
+
37
+ return ([...arr].map((b, i) =>
38
+ (i === 4 || i === 6 || i === 8 || i === 10 ? "-" : "") + b.toString(16).padStart(2, "0")
39
+ ).join(""));
40
+ }
41
+
42
+ textInput.addEventListener("keydown", function (event) {
43
+ if (event.key === "Enter") {
44
+ sendMessageToServer();
45
+ }
46
+ });
47
+
48
+ sendButton.addEventListener("click", sendMessageToServer);
static/js/ws.js ADDED
File without changes