add files

This commit is contained in:
Tobias Weise 2024-04-30 01:08:06 +02:00
parent b7ced7d964
commit d35e9d7a21
15 changed files with 2296 additions and 0 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
APP_PREFIX=llm

11
backend/Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM python:3.12
#RUN curl https://ollama.ai/install.sh | sh
#RUN ollama run llama2
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
ENTRYPOINT ["python3", "app.py"]

Binary file not shown.

Binary file not shown.

160
backend/app.py Normal file
View File

@ -0,0 +1,160 @@
"""
OpenAPI access via http://localhost:5000/openapi/ on local docker-compose deployment
"""
#import warnings
#warnings.filterwarnings("ignore")
#std lib modules:
import os, sys, json
from typing import Any, Tuple, List, Dict, Any, Callable, Optional
from datetime import datetime, date
from collections import namedtuple
import hashlib, traceback, logging
#llm
from langchain.callbacks.manager import CallbackManager
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
from langchain_community.llms import Ollama
#import openai #even used?
import tiktoken
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
#from langchain.callbacks import get_openai_callback
#from langchain_community.callbacks import get_openai_callback
#from langchain_openai import ChatOpenAI, AzureChatOpenAI
#from langchain_openai import OpenAIEmbeddings, AzureOpenAIEmbeddings
from langchain_community.vectorstores.elasticsearch import ElasticsearchStore
#from langchain.document_loaders import PyPDFLoader, Docx2txtLoader
from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader
from langchain.callbacks.base import BaseCallbackHandler, BaseCallbackManager
from langchain.prompts import PromptTemplate
#ext libs
from elasticsearch import NotFoundError, Elasticsearch # for normal read/write without vectors
from elasticsearch_dsl import Search, A
from elasticsearch_dsl import Document, Date, Integer, Keyword, Float, Long, Text, connections
from pydantic import BaseModel, Field
#flask, openapi
from flask import Flask, send_from_directory, Response, request, jsonify
import sys, os
from flask_cors import CORS, cross_origin
from werkzeug.utils import secure_filename
from flask_openapi3 import Info, Tag, OpenAPI, Server, FileStorage
from flask_socketio import SocketIO, join_room, leave_room, rooms, send
#home grown
#from scraper import WebScraper
from funcs import group_by
#from elastictools import update_by_id, delete_by_id
#TODO: implement some kind of logging mechanism
#logging.basicConfig(filename='record.log', level=logging.DEBUG)
#logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(level=logging.WARN)
app = Flask(__name__)
from flask_cors import CORS #falls cross-orgin verwendet werden soll
CORS(app)
socket = SocketIO(app, cors_allowed_origins="*")
@socket.on('connect')
def sockcon(data):
"""
put every connection into it's own room
to avoid broadcasting messages
answer in callback only to room with sid
"""
room = request.sid
join_room(room)
socket.emit('backend response', {'msg': f'Connected to room {room} !', "room": room}) # looks like iOS needs an answer
class StreamingCallback(BaseCallbackHandler):
def __init__(self, key: str, sid: str):
pass
def on_llm_new_token(self, token: str, **kwargs):
pass
def on_llm_end(self, response, **kwargs):
pass
@socket.on('client message')
def handle_message(message):
room = message["room"]
stream_key = "chatbot_stream"
llm = Ollama(
model="llama3",
#callback_manager=CallbackManager([StreamingCallback(stream_key, room)]),
base_url="http://ollama:11434"
)
system_prompt = "Answer in the language of the question. Give a short answer. Answer all questions without reservations or advice."
query = system_prompt + " " + message["data"]
print(message["data"])
for chunks in llm.stream(query):
socket.emit('backend token', {'data': chunks, "done": False}, to=room)
socket.emit('backend token', {'done': True}, to=room)
#==============Routes===============
@app.route("/") #Index Verzeichnis
def index():
return send_from_directory('.', "index.html")
@app.route("/info") #spezielle Nutzer definierte Route
def info():
return sys.version+" "+os.getcwd()
@app.route('/<path:path>') #generische Route (auch Unterordner)
def catchAll(path):
return send_from_directory('.', path)
if __name__ == '__main__':
#Wenn HTTPS benötigt wird (Pfade für RHEL7/können je OS variieren)
#cert = "/etc/pki/tls/certs/cert-payment.pem" #cert
#key = "/etc/pki/tls/private/cert-payment-private.pem" #key
#context = (cert, key)
#app.run(debug=True, host='0.0.0.0', ssl_context=context)
app.run(debug=True, host='0.0.0.0')
#app.run(debug=True)
"""
llm = Ollama(
model="llama2",
callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]),
base_url="http://ollama:11434"
)
assume = "Answer the next question with either true or false and name an example."
question = "Can cats use guns?"
print(question)
s = llm.invoke(assume + " " + question)
"""

0
backend/backend.env Normal file
View File

45
backend/elastictools.py Normal file
View File

@ -0,0 +1,45 @@
"""
Some helper functions to make querying easier
"""
from typing import Any, Tuple, List, Dict, Any, Callable, Optional
import json
from elasticsearch import NotFoundError, Elasticsearch # for normal read/write without vectors
from elasticsearch_dsl import Search, A
from elasticsearch_dsl import Document, Date, Integer, Keyword, Float, Long, Text, connections
def update_by_id(client: Elasticsearch, index: str, id_field_name: str, id_value: str, values_to_set: Dict[str, Any]) -> None:
#create painless insert script
source = ""
for k, v in values_to_set.items():
source += f"ctx._source.{k} = {json.dumps(v)};"
body = {
"query": {
"term": {
id_field_name: id_value
}
},
"script": {
"source": source,
"lang": "painless"
}
}
client.update_by_query(index=index, body=body)
def delete_by_id(client: Elasticsearch, index: str, id_field_name: str, id_value: str):
s = Search(using=client, index=index).filter("term", **{id_field_name: id_value})
response = s.delete()
#if not response.success():
# raise Exception("Unable to delete id '%s' in index '%' !" % (index, id_value))
print(response, flush=True)
def get_datetime_interval(search: Search, start, end) -> Search:
return search.filter("range", timest={"gte": start}).filter("range", timest={"lte": end})

36
backend/funcs.py Normal file
View File

@ -0,0 +1,36 @@
"""
Function lib
"""
from typing import Any, Tuple, List, Dict, Any, Callable, Optional
import json
def group_by(fs: List[Callable[[Any], Any]], ls: List[Any]) -> Dict[Any, Any]:
"""
Recursivly divides a list into sublists according to a list of membership-defining functions
"""
match fs:
case [f]:
match ls:
case []: return {}
case _:
d = {}
for x in ls:
k = f(x)
if k in d:
d[k].append(x)
else:
d[k] = [x]
return d
case [f, *fs]:
d = group_by([f], ls)
return {k: group_by(fs, v) for k, v in d.items()}
def pretty(x):
"""
A convenience pretty printing function
"""
print( json.dumps(x, indent=4) )

77
backend/index.html Normal file
View File

@ -0,0 +1,77 @@
<!doctype html>
<html>
<head>
<title>Ollama Chatbot</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="w3.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script>
</head>
<body>
<table id="log" class="w3-table-all" style="width: 100%;">
<tr>
<th></th>
<th>Message</th>
</tr>
</table>
<textarea style="width: 100%; height: 50px;" id="user_input"></textarea>
<button id="submit_btn">Submit</button>
<script type="text/javascript" charset="utf-8">
window.onload = ()=>{
let tA = document.getElementById("user_input");
let log = document.getElementById("log");
let btn = document.getElementById("submit_btn");
function log_msg(nick, msg){
console.log(nick + ": " + msg);
log.innerHTML += "<tr><td><b>" + nick + "</b>:</td><td>" + msg + "</td></tr>";
}
log_msg("Bot", "Ask a question!");
const socket = io();
let room = null;
socket.on('backend response', function(data) {
console.log(data);
if(data.room) room = data.room;
});
let answer_count = 0;
let acc_text = "";
let first_token = true;
socket.on('backend token', function(obj) {
console.log(obj);
if(first_token){
let id = answer_count;
log.innerHTML += "<tr><td><b>Bot</b>:</td><td id='" + id + "'></td></tr>";
}
if(!obj.done){
acc_text += "" + obj.data;
first_token = false;
document.getElementById(answer_count).innerHTML += obj.data;
}
else{
//log_msg("Bot", acc_text);
acc_text = "";
first_token = true;
answer_count += 1;
}
});
btn.onclick = evt=>{
let s = tA.value;
if(s.trim() != '' && room){
tA.value = "";
log_msg('User', s);
socket.emit('client message', {data: s, room: room});
}
};
};
</script>
</body>
</html>

93
backend/models.py Normal file
View File

@ -0,0 +1,93 @@
import os
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Keyword, Float, Long, Text, connections, Object
# Define a default Elasticsearch client
connections.create_connection(hosts="http://localhost:9200")
class Article(Document):
title = Text(analyzer='snowball', fields={'raw': Keyword()})
body = Text(analyzer='snowball')
tags = Keyword()
published_from = Date()
lines = Integer()
class Index:
name = 'blog'
settings = {
"number_of_shards": 1,
}
def save(self, ** kwargs):
self.lines = len(self.body.split())
return super(Article, self).save(** kwargs)
#======= nextsearch_log ===========
class Sources(InnerDoc):
score = Float()
sourceFileId = Text()
sourceType = Text()
tags = Text()
class NextsearchLog(Document):
a = Text()
chatbotid = Keyword()
durasecs = Float()
inCt = Float()
inToks = Long()
llm = Text()
outCt = Float()
outToks = Long()
q = Text()
queryid = Keyword()
rating = Long()
reason = Text()
reasontags = Text()
session = Keyword()
sources = Object(Sources) #Text(analyzer='snowball')
temperature = Float()
totalCt = Float()
timest = Date() #timestamp
date = Date() #iso date
class Index:
#name = 'test_nextsearch_log'
name = 'nextsearch_log'
settings = {
"number_of_shards": 1,
}
def save(self, ** kwargs):
self.lines = len(self.body.split())
return super(NextsearchLog, self).save(** kwargs)
if __name__ == "__main__":
elastic_uri = os.getenv("ELASTIC_URI")
#elastic_uri = "http://localhost:9200"
assert elastic_uri
# Define a default Elasticsearch client
connections.create_connection(hosts=elastic_uri)
#connections.create_connection(hosts)
# create the mappings in elasticsearch
NextsearchLog.init()
# create the mappings in elasticsearch
#Article.init()
# create and save and article
#article = Article(meta={'id': 42}, title='Hello world!', tags=['test'])
#article.body = ''' looong text '''
##article.published_from = datetime.now()
#article.save()
#article = Article.get(id=42)
#print(article.is_published())
# Display cluster health
#print(connections.get_connection().cluster.health())

1322
backend/old_app.py Normal file

File diff suppressed because it is too large Load Diff

14
backend/requirements.txt Normal file
View File

@ -0,0 +1,14 @@
elasticsearch
elasticsearch-dsl
langchain
tiktoken
pydantic
Werkzeug
flask
Flask-Cors
Flask-SocketIO
flask-openapi3
minio

361
backend/w3.css Executable file
View File

@ -0,0 +1,361 @@
/* W3.CSS 2.99 Mar 2017 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
a{background-color:transparent;-webkit-text-decoration-skip:objects}
a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
dfn{font-style:italic}mark{background:#ff0;color:#000}
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}
img{border-style:none}svg:not(:root){overflow:hidden}
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}
hr{box-sizing:content-box;height:0;overflow:visible}
button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}
button,input{overflow:visible}button,select{text-transform:none}
button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}
button::-moz-focus-inner, [type=button]::-moz-focus-inner, [type=reset]::-moz-focus-inner, [type=submit]::-moz-focus-inner{border-style:none;padding:0}
button:-moz-focusring, [type=button]:-moz-focusring, [type=reset]:-moz-focusring, [type=submit]:-moz-focusring{outline:1px dotted ButtonText}
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
[type=checkbox],[type=radio]{padding:0}
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}
::-webkit-input-placeholder{color:inherit;opacity:0.54}
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
/* End extract */
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
h1,h2,h3,h4,h5,h6,.w3-slim,.w3-wide{font-family:"Segoe UI",Arial,sans-serif}
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
.w3-serif{font-family:"Times New Roman",Times,serif}
h1,h2,h3,h4,h5,h6{font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}
hr{border:0;border-top:1px solid #eee;margin:20px 0}
img{margin-bottom:-5px}a{color:inherit}
.w3-image{max-width:100%;height:auto}
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}
.w3-table-all{border:1px solid #ccc}
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}
.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
.w3-table-all tr:nth-child(odd){background-color:#fff}
.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}
.w3-centered tr th,.w3-centered tr td{text-align:center}
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-btn-block,.w3-button{border:none;display:inline-block;outline:0;padding:6px 16px;vertical-align:middle;overflow:hidden;text-decoration:none!important;color:#fff;background-color:#000;text-align:center;cursor:pointer;white-space:nowrap}
.w3-btn:hover,.w3-btn-block:hover,.w3-btn-floating:hover,.w3-btn-floating-large:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-button{color:#000;background-color:#f1f1f1;padding:8px 16px}.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-btn,.w3-btn-floating,.w3-btn-floating-large,.w3-closenav,.w3-opennav,.w3-btn-block,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-btn-floating,.w3-btn-floating-large{display:inline-block;text-align:center;color:#fff;background-color:#000;position:relative;overflow:hidden;z-index:1;padding:0;border-radius:50%;cursor:pointer;font-size:24px}
.w3-btn-floating{width:40px;height:40px;line-height:40px}.w3-btn-floating-large{width:56px;height:56px;line-height:56px}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled,.w3-btn-floating:disabled,.w3-btn-floating-large:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn-block.w3-disabled:hover,.w3-btn:disabled:hover,.w3-btn-floating.w3-disabled:hover,.w3-btn-floating:disabled:hover,
.w3-btn-floating-large.w3-disabled:hover,.w3-btn-floating-large:disabled:hover{box-shadow:none}
.w3-btn-group .w3-btn{float:left}.w3-btn-block{width:100%}
.w3-btn-bar .w3-btn{box-shadow:none;background-color:inherit;color:inherit;float:left}.w3-btn-bar .w3-btn:hover{background-color:#ccc}
.w3-badge,.w3-tag,.w3-sign{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}
.w3-badge{border-radius:50%}
ul.w3-ul{list-style-type:none;padding:0;margin:0}ul.w3-ul li{padding:6px 2px 6px 16px;border-bottom:1px solid #ddd}ul.w3-ul li:last-child{border-bottom:none}
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
.w3-navbar{list-style-type:none;margin:0;padding:0;overflow:hidden}
.w3-navbar li{float:left}.w3-navbar li a,.w3-navitem,.w3-navbar li .w3-btn,.w3-navbar li .w3-input{display:block;padding:8px 16px}.w3-navbar li .w3-btn,.w3-navbar li .w3-input{border:none;outline:none;width:100%}
.w3-navbar li a:hover{color:#000;background-color:#ccc}
.w3-navbar .w3-dropdown-hover,.w3-navbar .w3-dropdown-click{position:static}
.w3-navbar .w3-dropdown-hover:hover,.w3-navbar .w3-dropdown-hover:first-child,.w3-navbar .w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-navbar a,.w3-topnav a,.w3-sidenav a,.w3-dropdown-content a,.w3-accordion-content a,.w3-dropnav a,.w3-navblock a{text-decoration:none!important}
.w3-navbar .w3-opennav.w3-right{float:right!important}.w3-topnav{padding:8px 8px}
.w3-navblock .w3-dropdown-hover:hover,.w3-navblock .w3-dropdown-hover:first-child,.w3-navblock .w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-navblock .w3-dropdown-hover,.w3-navblock .w3-dropdown-click{width:100%}.w3-navblock .w3-dropdown-hover .w3-dropdown-content,.w3-navblock .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-topnav a{padding:0 8px;border-bottom:3px solid transparent;-webkit-transition:border-bottom .25s;transition:border-bottom .25s}
.w3-topnav a:hover{border-bottom:3px solid #fff}.w3-topnav .w3-dropdown-hover a{border-bottom:0}
.w3-opennav,.w3-closenav{color:inherit}.w3-opennav:hover,.w3-closenav:hover{cursor:pointer;opacity:0.8}
.w3-btn,.w3-btn-floating,.w3-dropnav a,.w3-btn-floating-large,.w3-btn-block, .w3-navbar a,.w3-navblock a,.w3-sidenav a,.w3-pagination li a,.w3-hoverable tbody tr,.w3-hoverable li,
.w3-accordion-content a,.w3-dropdown-content a,.w3-dropdown-click:hover,.w3-dropdown-hover:hover,.w3-opennav,.w3-closenav,.w3-closebtn,*[class*="w3-hover-"]
{-webkit-transition:background-color .25s,color .15s,box-shadow .25s,opacity 0.25s,filter 0.25s,border 0.15s;transition:background-color .25s,color .15s,box-shadow .15s,opacity .25s,filter .25s,border .15s}
.w3-ripple:active{opacity:0.5}.w3-ripple{-webkit-transition:opacity 0s;transition:opacity 0s}
.w3-sidenav,.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
.w3-sidenav a,.w3-navblock a{padding:4px 2px 4px 16px}.w3-sidenav a:hover,.w3-navblock a:hover{background-color:#ccc;color:#000}.w3-sidenav a,.w3-dropnav a,.w3-navblock a{display:block}
.w3-sidenav .w3-dropdown-hover:hover,.w3-sidenav .w3-dropdown-hover:first-child,.w3-sidenav .w3-dropdown-click:hover,.w3-dropnav a:hover{background-color:#ccc;color:#000}
.w3-sidenav .w3-dropdown-hover,.w3-sidenav .w3-dropdown-click,.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
.w3-sidenav .w3-dropdown-hover .w3-dropdown-content,.w3-sidenav .w3-dropdown-click .w3-dropdown-content,.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;background-color:inherit;color:inherit;padding:6px 2px 6px 16px}
.w3-main,#main{transition:margin-left .4s}
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}.w3-closebtn{text-decoration:none;float:right;font-size:24px;font-weight:bold;color:inherit}
.w3-closebtn:hover,.w3-closebtn:focus{color:#000;text-decoration:none;cursor:pointer}
.w3-pagination{display:inline-block;padding:0;margin:0}.w3-pagination li{display:inline}
.w3-pagination li a{text-decoration:none;color:#000;float:left;padding:8px 16px}
.w3-pagination li a:hover{background-color:#ccc}
.w3-input-group,.w3-group{margin-top:24px;margin-bottom:24px}
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #808080;width:100%}
.w3-label{color:#009688}.w3-input:not(:valid)~.w3-validate{color:#f44336}
.w3-select{padding:9px 0;width:100%;color:#000;border:1px solid transparent;border-bottom:1px solid #009688}
.w3-select select:focus{color:#000;border:1px solid #009688}.w3-select option[disabled]{color:#009688}
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
.w3-dropdown-hover:hover .w3-dropdown-content{display:block;z-index:1}
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0}
.w3-dropdown-content a{padding:6px 16px;display:block}
.w3-dropdown-content a:hover{background-color:#ccc}
.w3-accordion{width:100%;cursor:pointer}
.w3-accordion-content{cursor:auto;display:none;position:relative;width:100%;margin:0;padding:0}
.w3-accordion-content a{padding:6px 16px;display:block}.w3-accordion-content a:hover{background-color:#ccc}
.w3-progress-container{width:100%;height:1.5em;position:relative;background-color:#f1f1f1}
.w3-progressbar{background-color:#757575;height:100%;position:absolute;line-height:inherit}
input[type=checkbox].w3-check,input[type=radio].w3-radio{width:24px;height:24px;position:relative;top:6px}
input[type=checkbox].w3-check:checked+.w3-validate,input[type=radio].w3-radio:checked+.w3-validate{color:#009688}
input[type=checkbox].w3-check:disabled+.w3-validate,input[type=radio].w3-radio:disabled+.w3-validate{color:#aaa}
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
.w3-bar .w3-bar-item{padding:8px 16px;float:left;background-color:inherit;color:inherit;width:auto;border:none;outline:none;display:block}
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
.w3-bar .w3-button{background-color:inherit;color:inherit;white-space:normal}
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;background-color:inherit;color:inherit;border:none;outline:none;white-space:normal}
.w3-bar-block.w3-center .w3-bar-item{text-align:center}
.w3-block{display:block;width:100%}
.w3-responsive{overflow-x:auto}
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,.w3-cell-row:before,.w3-cell-row:after,
.w3-topnav:after,.w3-topnav:before,.w3-clear:after,.w3-clear:before,.w3-btn-group:before,.w3-btn-group:after,.w3-btn-bar:before,.w3-btn-bar:after,.w3-bar:before,.w3-bar:after
{content:"";display:table;clear:both}
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
.w3-col.s1{width:8.33333%}
.w3-col.s2{width:16.66666%}
.w3-col.s3{width:24.99999%}
.w3-col.s4{width:33.33333%}
.w3-col.s5{width:41.66666%}
.w3-col.s6{width:49.99999%}
.w3-col.s7{width:58.33333%}
.w3-col.s8{width:66.66666%}
.w3-col.s9{width:74.99999%}
.w3-col.s10{width:83.33333%}
.w3-col.s11{width:91.66666%}
.w3-col.s12,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{width:99.99999%}
@media (min-width:601px){
.w3-col.m1{width:8.33333%}
.w3-col.m2{width:16.66666%}
.w3-col.m3,.w3-quarter{width:24.99999%}
.w3-col.m4,.w3-third{width:33.33333%}
.w3-col.m5{width:41.66666%}
.w3-col.m6,.w3-half{width:49.99999%}
.w3-col.m7{width:58.33333%}
.w3-col.m8,.w3-twothird{width:66.66666%}
.w3-col.m9,.w3-threequarter{width:74.99999%}
.w3-col.m10{width:83.33333%}
.w3-col.m11{width:91.66666%}
.w3-col.m12{width:99.99999%}}
@media (min-width:993px){
.w3-col.l1{width:8.33333%}
.w3-col.l2{width:16.66666%}
.w3-col.l3,.w3-quarter{width:24.99999%}
.w3-col.l4,.w3-third{width:33.33333%}
.w3-col.l5{width:41.66666%}
.w3-col.l6,.w3-half{width:49.99999%}
.w3-col.l7{width:58.33333%}
.w3-col.l8,.w3-twothird{width:66.66666%}
.w3-col.l9,.w3-threequarter{width:74.99999%}
.w3-col.l10{width:83.33333%}
.w3-col.l11{width:91.66666%}
.w3-col.l12{width:99.99999%}}
.w3-content{max-width:980px;margin:auto}
.w3-rest{overflow:hidden}
.w3-layout-container,.w3-cell-row{display:table;width:100%}.w3-layout-row{display:table-row}.w3-layout-cell,.w3-layout-col,.w3-cell{display:table-cell}
.w3-layout-top,.w3-cell-top{vertical-align:top}.w3-layout-middle,.w3-cell-middle{vertical-align:middle}.w3-layout-bottom,.w3-cell-bottom{vertical-align:bottom}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-topnav a{display:block}.w3-navbar li:not(.w3-opennav){float:none;width:100%!important}.w3-navbar li.w3-right{float:none!important}
.w3-topnav .w3-dropdown-hover .w3-dropdown-content,.w3-navbar .w3-dropdown-click .w3-dropdown-content,.w3-navbar .w3-dropdown-hover .w3-dropdown-content,.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-topnav,.w3-navbar{text-align:center}.w3-hide-small{display:none!important}.w3-layout-col,.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidenav.w3-collapse,.w3-sidebar.w3-collapse{display:block!important}}
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
@media (max-width:992px){.w3-sidenav.w3-collapse,.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}}
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
.w3-left{float:left!important}.w3-right{float:right!important}
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}
.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}
.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
.w3-vertical{word-break:break-all;line-height:1;text-align:center;width:0.6em}
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}
.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
.w3-display-position{position:absolute}
.w3-circle{border-radius:50%!important}
.w3-round-small{border-radius:2px!important}.w3-round,.w3-round-medium{border-radius:4px!important}
.w3-round-large{border-radius:8px!important}.w3-round-xlarge{border-radius:16px!important}
.w3-round-xxlarge{border-radius:32px!important}.w3-round-jumbo{border-radius:64px!important}
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
.w3-margin{margin:16px!important}.w3-margin-0{margin:0!important}
.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
.w3-section{margin-top:16px!important;margin-bottom:16px!important}
.w3-padding-tiny{padding:2px 4px!important}.w3-padding-small{padding:4px 8px!important}
.w3-padding-medium,.w3-padding,.w3-form{padding:8px 16px!important}
.w3-padding-large{padding:12px 24px!important}.w3-padding-xlarge{padding:16px 32px!important}
.w3-padding-xxlarge{padding:24px 48px!important}.w3-padding-jumbo{padding:32px 64px!important}
.w3-padding-4{padding-top:4px!important;padding-bottom:4px!important}
.w3-padding-8{padding-top:8px!important;padding-bottom:8px!important}
.w3-padding-12{padding-top:12px!important;padding-bottom:12px!important}
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}
.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}
.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
.w3-padding-128{padding-top:128px!important;padding-bottom:128px!important}
.w3-padding-0{padding:0!important}
.w3-padding-top{padding-top:8px!important}.w3-padding-bottom{padding-bottom:8px!important}
.w3-padding-left{padding-left:16px!important}.w3-padding-right{padding-right:16px!important}
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-spin{animation:w3-spin 2s infinite linear;-webkit-animation:w3-spin 2s infinite linear}
@-webkit-keyframes w3-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}
@keyframes w3-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}
.w3-container{padding:0.01em 16px}
.w3-panel{padding:0.01em 16px;margin-top:16px!important;margin-bottom:16px!important}
.w3-example{background-color:#f1f1f1;padding:0.01em 16px}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{line-height:1.4;width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
.w3-example,.w3-code{margin:20px 0}.w3-card{border:1px solid #ccc}
.w3-card-2,.w3-example{box-shadow:0 2px 4px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)!important}
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)!important}
.w3-card-8{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)!important}
.w3-card-12{box-shadow:0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19)!important}
.w3-card-16{box-shadow:0 16px 24px 0 rgba(0,0,0,0.22),0 25px 55px 0 rgba(0,0,0,0.21)!important}
.w3-card-24{box-shadow:0 24px 24px 0 rgba(0,0,0,0.2),0 40px 77px 0 rgba(0,0,0,0.22)!important}
.w3-animate-fading{-webkit-animation:fading 10s infinite;animation:fading 10s infinite}
@-webkit-keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
.w3-animate-opacity{-webkit-animation:opac 0.8s;animation:opac 0.8s}
@-webkit-keyframes opac{from{opacity:0} to{opacity:1}}
@keyframes opac{from{opacity:0} to{opacity:1}}
.w3-animate-top{position:relative;-webkit-animation:animatetop 0.4s;animation:animatetop 0.4s}
@-webkit-keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
.w3-animate-left{position:relative;-webkit-animation:animateleft 0.4s;animation:animateleft 0.4s}
@-webkit-keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
.w3-animate-right{position:relative;-webkit-animation:animateright 0.4s;animation:animateright 0.4s}
@-webkit-keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
.w3-animate-bottom{position:relative;-webkit-animation:animatebottom 0.4s;animation:animatebottom 0.4s}
@-webkit-keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0px;opacity:1}}
@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
.w3-animate-zoom {-webkit-animation:animatezoom 0.6s;animation:animatezoom 0.6s}
@-webkit-keyframes animatezoom{from{-webkit-transform:scale(0)} to{-webkit-transform:scale(1)}}
@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
.w3-animate-input{-webkit-transition:width 0.4s ease-in-out;transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60;-webkit-backface-visibility:hidden}
.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1;-webkit-backface-visibility:hidden}
.w3-opacity-max{opacity:0.25;-webkit-backface-visibility:hidden}
.w3-opacity-min{opacity:0.75;-webkit-backface-visibility:hidden}
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{-webkit-filter:grayscale(100%);filter:grayscale(100%)}
.w3-greyscale,.w3-grayscale{-webkit-filter:grayscale(75%);filter:grayscale(75%)}
.w3-greyscale-min,.w3-grayscale-min{-webkit-filter:grayscale(50%);filter:grayscale(50%)}
.w3-sepia{-webkit-filter:sepia(75%);filter:sepia(75%)}
.w3-sepia-max,.w3-hover-sepia:hover{-webkit-filter:sepia(100%);filter:sepia(100%)}
.w3-sepia-min{-webkit-filter:sepia(50%);filter:sepia(50%)}
.w3-text-shadow{text-shadow:1px 1px 0 #444}.w3-text-shadow-white{text-shadow:1px 1px 0 #ddd}
.w3-transparent{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important;background-color:transparent!important}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}

116
docker-compose.yml Normal file
View File

@ -0,0 +1,116 @@
version: "3.9"
networks:
llm_network:
driver: bridge
services:
nginx:
container_name: ${APP_PREFIX}_reverseproxy
image: nginxinc/nginx-unprivileged #:1.25-alpine
restart: always
ports:
- "81:8080"
volumes:
- type: bind
source: ./nginx.conf
target: /etc/nginx/nginx.conf
networks:
- llm_network
#minio:
# container_name: ${APP_PREFIX}_minio
# #image: docker.io/bitnami/minio #:2022
# image: minio/minio
# ports:
# - '9000:9000'
# - '9001:9001'
# networks:
# - llm_network
# volumes:
# - 'minio_data:/data'
# environment:
# - MINIO_ROOT_USER=root
# - MINIO_ROOT_PASSWORD=rootrootroot
# - MINIO_DEFAULT_BUCKETS=defaultbucket
# command: server --console-address ":9001" /data
elasticsearch:
container_name: ${APP_PREFIX}_elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
restart: always
ports:
- "9200:9200"
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- logger.level=ERROR
volumes:
- esdata:/usr/share/elasticsearch/data
ulimits:
memlock:
soft: -1
hard: -1
networks:
- llm_network
ollama:
container_name: ${APP_PREFIX}_ollama
image: ollama/ollama:latest
restart: always
ports:
- "11434:11434"
volumes:
- .:/code
- ./ollama/ollama:/root/.ollama
networks:
- llm_network
#command: "ollama pull llama2"
ollama-webui:
image: ghcr.io/ollama-webui/ollama-webui:main
container_name: ollama-webui
volumes:
- ./ollama/ollama-webui:/app/backend/data
depends_on:
- ollama
ports:
- 8888:8080
environment:
- '/ollama/api=http://ollama:11434/api'
extra_hosts:
- host.docker.internal:host-gateway
restart: unless-stopped
networks:
- llm_network
#frontend:
# container_name: ${APP_PREFIX}_frontend
# image: ${APP_PREFIX}-vue-frontend
# restart: always
# ports:
# - "3000:3000"
# build: ./frontend
backend:
container_name: ${APP_PREFIX}_backend
image: ${APP_PREFIX}-python-backend
restart: always
ports:
- "5000:5000"
env_file:
- backend/backend.env
build: ./backend
networks:
- llm_network
depends_on:
- elasticsearch
- ollama
#- minio
volumes:
esdata:
driver: local
filedata:
minio_data:
driver: local

59
nginx.conf Normal file
View File

@ -0,0 +1,59 @@
worker_processes 1;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format main '$time_iso8601 :: $status :: $request';
access_log /dev/stdout main;
error_log /dev/stderr error;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
default_type application/octet-stream;
sendfile on;
send_timeout 600;
tcp_nopush on;
proxy_read_timeout 300;
client_max_body_size 100m;
server {
server_name localhost;
listen 8080;
resolver 127.0.0.11;
# Frontend
# location / {
# proxy_pass http://frontend:3000;
# }
#location /epdm/chat/frontend {
# rewrite ^/epdm/chat/frontend/assets/(.*) $1 break;
# proxy_pass http://frontend:3000;
#}
#location /epdm/chat/frontend/assets {
# rewrite ^/epdm/chat/frontend/assets/(.*) $1 break;
# proxy_pass http://frontend:3000/assets/$uri$is_args$args;
#}
# Node Backend API Server
#location /epdm/chat/backend {
# rewrite ^/epdm/chat/backend(.*) $1 break;
# proxy_pass http://backend:8000$uri$is_args$args;
#}
# Python Backend
location /backend {
rewrite ^/backend(.*) $1 break;
proxy_pass http://backend:5000$uri$is_args$args;
}
}
}