Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
635951b5
Unverified
Commit
635951b5
authored
May 06, 2024
by
Timothy Jaeryang Baek
Committed by
GitHub
May 06, 2024
Browse files
Merge branch 'dev' into feat/backend-web-search
parents
8b3e370a
bf604bc0
Changes
56
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1063 additions
and
877 deletions
+1063
-877
.github/workflows/integration-test.yml
.github/workflows/integration-test.yml
+10
-1
backend/apps/rag/main.py
backend/apps/rag/main.py
+37
-10
backend/apps/rag/utils.py
backend/apps/rag/utils.py
+27
-22
backend/config.py
backend/config.py
+20
-11
backend/main.py
backend/main.py
+36
-3
src/app.css
src/app.css
+9
-0
src/lib/apis/ollama/index.ts
src/lib/apis/ollama/index.ts
+5
-1
src/lib/apis/openai/index.ts
src/lib/apis/openai/index.ts
+3
-1
src/lib/apis/rag/index.ts
src/lib/apis/rag/index.ts
+3
-2
src/lib/apis/streaming/index.ts
src/lib/apis/streaming/index.ts
+11
-0
src/lib/components/chat/Messages/CitationsModal.svelte
src/lib/components/chat/Messages/CitationsModal.svelte
+77
-0
src/lib/components/chat/Messages/ResponseMessage.svelte
src/lib/components/chat/Messages/ResponseMessage.svelte
+473
-420
src/lib/components/chat/ModelSelector.svelte
src/lib/components/chat/ModelSelector.svelte
+1
-1
src/lib/components/chat/ModelSelector/Selector.svelte
src/lib/components/chat/ModelSelector/Selector.svelte
+2
-2
src/lib/components/chat/Settings/Account.svelte
src/lib/components/chat/Settings/Account.svelte
+2
-2
src/lib/components/chat/Settings/Connections.svelte
src/lib/components/chat/Settings/Connections.svelte
+1
-1
src/lib/components/chat/ShareChatModal.svelte
src/lib/components/chat/ShareChatModal.svelte
+10
-6
src/lib/components/common/ImagePreview.svelte
src/lib/components/common/ImagePreview.svelte
+1
-1
src/lib/components/documents/Settings/ChunkParams.svelte
src/lib/components/documents/Settings/ChunkParams.svelte
+126
-0
src/lib/components/documents/Settings/General.svelte
src/lib/components/documents/Settings/General.svelte
+209
-393
No files found.
.github/workflows/integration-test.yml
View file @
635951b5
...
@@ -20,7 +20,16 @@ jobs:
...
@@ -20,7 +20,16 @@ jobs:
-
name
:
Build and run Compose Stack
-
name
:
Build and run Compose Stack
run
:
|
run
:
|
docker compose up --detach --build
docker compose --file docker-compose.yaml --file docker-compose.api.yaml up --detach --build
-
name
:
Wait for Ollama to be up
timeout-minutes
:
5
run
:
|
until curl --output /dev/null --silent --fail http://localhost:11434; do
printf '.'
sleep 1
done
echo "Service is up!"
-
name
:
Preload Ollama model
-
name
:
Preload Ollama model
run
:
|
run
:
|
...
...
backend/apps/rag/main.py
View file @
635951b5
...
@@ -80,6 +80,7 @@ from config import (
...
@@ -80,6 +80,7 @@ from config import (
RAG_EMBEDDING_MODEL_AUTO_UPDATE
,
RAG_EMBEDDING_MODEL_AUTO_UPDATE
,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE
,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE
,
ENABLE_RAG_HYBRID_SEARCH
,
ENABLE_RAG_HYBRID_SEARCH
,
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
,
RAG_RERANKING_MODEL
,
RAG_RERANKING_MODEL
,
PDF_EXTRACT_IMAGES
,
PDF_EXTRACT_IMAGES
,
RAG_RERANKING_MODEL_AUTO_UPDATE
,
RAG_RERANKING_MODEL_AUTO_UPDATE
,
...
@@ -91,7 +92,7 @@ from config import (
...
@@ -91,7 +92,7 @@ from config import (
CHUNK_SIZE
,
CHUNK_SIZE
,
CHUNK_OVERLAP
,
CHUNK_OVERLAP
,
RAG_TEMPLATE
,
RAG_TEMPLATE
,
ENABLE_LOCAL_WEB_FETCH
,
ENABLE_
RAG_
LOCAL_WEB_FETCH
,
)
)
from
constants
import
ERROR_MESSAGES
from
constants
import
ERROR_MESSAGES
...
@@ -105,6 +106,9 @@ app.state.TOP_K = RAG_TOP_K
...
@@ -105,6 +106,9 @@ app.state.TOP_K = RAG_TOP_K
app
.
state
.
RELEVANCE_THRESHOLD
=
RAG_RELEVANCE_THRESHOLD
app
.
state
.
RELEVANCE_THRESHOLD
=
RAG_RELEVANCE_THRESHOLD
app
.
state
.
ENABLE_RAG_HYBRID_SEARCH
=
ENABLE_RAG_HYBRID_SEARCH
app
.
state
.
ENABLE_RAG_HYBRID_SEARCH
=
ENABLE_RAG_HYBRID_SEARCH
app
.
state
.
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
=
(
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app
.
state
.
CHUNK_SIZE
=
CHUNK_SIZE
app
.
state
.
CHUNK_SIZE
=
CHUNK_SIZE
app
.
state
.
CHUNK_OVERLAP
=
CHUNK_OVERLAP
app
.
state
.
CHUNK_OVERLAP
=
CHUNK_OVERLAP
...
@@ -114,6 +118,7 @@ app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
...
@@ -114,6 +118,7 @@ app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app
.
state
.
RAG_RERANKING_MODEL
=
RAG_RERANKING_MODEL
app
.
state
.
RAG_RERANKING_MODEL
=
RAG_RERANKING_MODEL
app
.
state
.
RAG_TEMPLATE
=
RAG_TEMPLATE
app
.
state
.
RAG_TEMPLATE
=
RAG_TEMPLATE
app
.
state
.
OPENAI_API_BASE_URL
=
RAG_OPENAI_API_BASE_URL
app
.
state
.
OPENAI_API_BASE_URL
=
RAG_OPENAI_API_BASE_URL
app
.
state
.
OPENAI_API_KEY
=
RAG_OPENAI_API_KEY
app
.
state
.
OPENAI_API_KEY
=
RAG_OPENAI_API_KEY
...
@@ -313,6 +318,7 @@ async def get_rag_config(user=Depends(get_admin_user)):
...
@@ -313,6 +318,7 @@ async def get_rag_config(user=Depends(get_admin_user)):
"chunk_size"
:
app
.
state
.
CHUNK_SIZE
,
"chunk_size"
:
app
.
state
.
CHUNK_SIZE
,
"chunk_overlap"
:
app
.
state
.
CHUNK_OVERLAP
,
"chunk_overlap"
:
app
.
state
.
CHUNK_OVERLAP
,
},
},
"web_loader_ssl_verification"
:
app
.
state
.
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
,
}
}
...
@@ -322,15 +328,34 @@ class ChunkParamUpdateForm(BaseModel):
...
@@ -322,15 +328,34 @@ class ChunkParamUpdateForm(BaseModel):
class
ConfigUpdateForm
(
BaseModel
):
class
ConfigUpdateForm
(
BaseModel
):
pdf_extract_images
:
bool
pdf_extract_images
:
Optional
[
bool
]
=
None
chunk
:
ChunkParamUpdateForm
chunk
:
Optional
[
ChunkParamUpdateForm
]
=
None
web_loader_ssl_verification
:
Optional
[
bool
]
=
None
@
app
.
post
(
"/config/update"
)
@
app
.
post
(
"/config/update"
)
async
def
update_rag_config
(
form_data
:
ConfigUpdateForm
,
user
=
Depends
(
get_admin_user
)):
async
def
update_rag_config
(
form_data
:
ConfigUpdateForm
,
user
=
Depends
(
get_admin_user
)):
app
.
state
.
PDF_EXTRACT_IMAGES
=
form_data
.
pdf_extract_images
app
.
state
.
PDF_EXTRACT_IMAGES
=
(
app
.
state
.
CHUNK_SIZE
=
form_data
.
chunk
.
chunk_size
form_data
.
pdf_extract_images
app
.
state
.
CHUNK_OVERLAP
=
form_data
.
chunk
.
chunk_overlap
if
form_data
.
pdf_extract_images
!=
None
else
app
.
state
.
PDF_EXTRACT_IMAGES
)
app
.
state
.
CHUNK_SIZE
=
(
form_data
.
chunk
.
chunk_size
if
form_data
.
chunk
!=
None
else
app
.
state
.
CHUNK_SIZE
)
app
.
state
.
CHUNK_OVERLAP
=
(
form_data
.
chunk
.
chunk_overlap
if
form_data
.
chunk
!=
None
else
app
.
state
.
CHUNK_OVERLAP
)
app
.
state
.
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
=
(
form_data
.
web_loader_ssl_verification
if
form_data
.
web_loader_ssl_verification
!=
None
else
app
.
state
.
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
return
{
return
{
"status"
:
True
,
"status"
:
True
,
...
@@ -339,6 +364,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
...
@@ -339,6 +364,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
"chunk_size"
:
app
.
state
.
CHUNK_SIZE
,
"chunk_size"
:
app
.
state
.
CHUNK_SIZE
,
"chunk_overlap"
:
app
.
state
.
CHUNK_OVERLAP
,
"chunk_overlap"
:
app
.
state
.
CHUNK_OVERLAP
,
},
},
"web_loader_ssl_verification"
:
app
.
state
.
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
,
}
}
...
@@ -490,7 +516,9 @@ def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
...
@@ -490,7 +516,9 @@ def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
def
store_web
(
form_data
:
UrlForm
,
user
=
Depends
(
get_current_user
)):
def
store_web
(
form_data
:
UrlForm
,
user
=
Depends
(
get_current_user
)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
try
:
try
:
loader
=
get_web_loader
(
form_data
.
url
)
loader
=
get_web_loader
(
form_data
.
url
,
verify_ssl
=
app
.
state
.
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
data
=
loader
.
load
()
data
=
loader
.
load
()
collection_name
=
form_data
.
collection_name
collection_name
=
form_data
.
collection_name
...
@@ -510,12 +538,11 @@ def store_web(form_data: UrlForm, user=Depends(get_current_user)):
...
@@ -510,12 +538,11 @@ def store_web(form_data: UrlForm, user=Depends(get_current_user)):
detail
=
ERROR_MESSAGES
.
DEFAULT
(
e
),
detail
=
ERROR_MESSAGES
.
DEFAULT
(
e
),
)
)
def
get_web_loader
(
url
:
Union
[
str
,
Sequence
[
str
]],
verify_ssl
:
bool
=
True
):
def
get_web_loader
(
url
:
Union
[
str
,
Sequence
[
str
]]):
# Check if the URL is valid
# Check if the URL is valid
if
not
validate_url
(
url
):
if
not
validate_url
(
url
):
raise
ValueError
(
ERROR_MESSAGES
.
INVALID_URL
)
raise
ValueError
(
ERROR_MESSAGES
.
INVALID_URL
)
return
WebBaseLoader
(
url
)
return
WebBaseLoader
(
url
,
verify_ssl
=
verify_ssl
)
def
validate_url
(
url
:
Union
[
str
,
Sequence
[
str
]]):
def
validate_url
(
url
:
Union
[
str
,
Sequence
[
str
]]):
...
...
backend/apps/rag/utils.py
View file @
635951b5
...
@@ -287,14 +287,14 @@ def rag_messages(
...
@@ -287,14 +287,14 @@ def rag_messages(
for
doc
in
docs
:
for
doc
in
docs
:
context
=
None
context
=
None
collection
=
doc
.
get
(
"collection_name"
)
collection
_names
=
(
if
collection
:
doc
[
"
collection
_names"
]
collection
=
[
collection
]
if
doc
[
"type"
]
=
=
"
collection
"
else
:
else
[
doc
[
"collection_name"
]]
collection
=
doc
.
get
(
"collection_names"
,
[]
)
)
collection
=
set
(
collection
).
difference
(
extracted_collections
)
collection
_names
=
set
(
collection
_names
).
difference
(
extracted_collections
)
if
not
collection
:
if
not
collection
_names
:
log
.
debug
(
f
"skipping
{
doc
}
as it has already been extracted"
)
log
.
debug
(
f
"skipping
{
doc
}
as it has already been extracted"
)
continue
continue
...
@@ -304,11 +304,7 @@ def rag_messages(
...
@@ -304,11 +304,7 @@ def rag_messages(
else
:
else
:
if
hybrid_search
:
if
hybrid_search
:
context
=
query_collection_with_hybrid_search
(
context
=
query_collection_with_hybrid_search
(
collection_names
=
(
collection_names
=
collection_names
,
doc
[
"collection_names"
]
if
doc
[
"type"
]
==
"collection"
else
[
doc
[
"collection_name"
]]
),
query
=
query
,
query
=
query
,
embedding_function
=
embedding_function
,
embedding_function
=
embedding_function
,
k
=
k
,
k
=
k
,
...
@@ -317,11 +313,7 @@ def rag_messages(
...
@@ -317,11 +313,7 @@ def rag_messages(
)
)
else
:
else
:
context
=
query_collection
(
context
=
query_collection
(
collection_names
=
(
collection_names
=
collection_names
,
doc
[
"collection_names"
]
if
doc
[
"type"
]
==
"collection"
else
[
doc
[
"collection_name"
]]
),
query
=
query
,
query
=
query
,
embedding_function
=
embedding_function
,
embedding_function
=
embedding_function
,
k
=
k
,
k
=
k
,
...
@@ -331,18 +323,31 @@ def rag_messages(
...
@@ -331,18 +323,31 @@ def rag_messages(
context
=
None
context
=
None
if
context
:
if
context
:
relevant_contexts
.
append
(
context
)
relevant_contexts
.
append
(
{
**
context
,
"source"
:
doc
}
)
extracted_collections
.
extend
(
collection
)
extracted_collections
.
extend
(
collection
_names
)
context_string
=
""
context_string
=
""
citations
=
[]
for
context
in
relevant_contexts
:
for
context
in
relevant_contexts
:
try
:
try
:
if
"documents"
in
context
:
if
"documents"
in
context
:
items
=
[
item
for
item
in
context
[
"documents"
][
0
]
if
item
is
not
None
]
context_string
+=
"
\n\n
"
.
join
(
context_string
+=
"
\n\n
"
.
join
(
items
)
[
text
for
text
in
context
[
"documents"
][
0
]
if
text
is
not
None
]
)
if
"metadatas"
in
context
:
citations
.
append
(
{
"source"
:
context
[
"source"
],
"document"
:
context
[
"documents"
][
0
],
"metadata"
:
context
[
"metadatas"
][
0
],
}
)
except
Exception
as
e
:
except
Exception
as
e
:
log
.
exception
(
e
)
log
.
exception
(
e
)
context_string
=
context_string
.
strip
()
context_string
=
context_string
.
strip
()
ra_content
=
rag_template
(
ra_content
=
rag_template
(
...
@@ -371,7 +376,7 @@ def rag_messages(
...
@@ -371,7 +376,7 @@ def rag_messages(
messages
[
last_user_message_idx
]
=
new_user_message
messages
[
last_user_message_idx
]
=
new_user_message
return
messages
return
messages
,
citations
def
get_model_path
(
model
:
str
,
update_model
:
bool
=
False
):
def
get_model_path
(
model
:
str
,
update_model
:
bool
=
False
):
...
...
backend/config.py
View file @
635951b5
...
@@ -18,6 +18,18 @@ from secrets import token_bytes
...
@@ -18,6 +18,18 @@ from secrets import token_bytes
from
constants
import
ERROR_MESSAGES
from
constants
import
ERROR_MESSAGES
####################################
# Load .env file
####################################
try
:
from
dotenv
import
load_dotenv
,
find_dotenv
load_dotenv
(
find_dotenv
(
"../.env"
))
except
ImportError
:
print
(
"dotenv not installed, skipping..."
)
####################################
####################################
# LOGGING
# LOGGING
####################################
####################################
...
@@ -59,16 +71,6 @@ for source in log_sources:
...
@@ -59,16 +71,6 @@ for source in log_sources:
log
.
setLevel
(
SRC_LOG_LEVELS
[
"CONFIG"
])
log
.
setLevel
(
SRC_LOG_LEVELS
[
"CONFIG"
])
####################################
# Load .env file
####################################
try
:
from
dotenv
import
load_dotenv
,
find_dotenv
load_dotenv
(
find_dotenv
(
"../.env"
))
except
ImportError
:
log
.
warning
(
"dotenv not installed, skipping..."
)
WEBUI_NAME
=
os
.
environ
.
get
(
"WEBUI_NAME"
,
"Open WebUI"
)
WEBUI_NAME
=
os
.
environ
.
get
(
"WEBUI_NAME"
,
"Open WebUI"
)
if
WEBUI_NAME
!=
"Open WebUI"
:
if
WEBUI_NAME
!=
"Open WebUI"
:
...
@@ -454,6 +456,11 @@ ENABLE_RAG_HYBRID_SEARCH = (
...
@@ -454,6 +456,11 @@ ENABLE_RAG_HYBRID_SEARCH = (
os
.
environ
.
get
(
"ENABLE_RAG_HYBRID_SEARCH"
,
""
).
lower
()
==
"true"
os
.
environ
.
get
(
"ENABLE_RAG_HYBRID_SEARCH"
,
""
).
lower
()
==
"true"
)
)
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
=
(
os
.
environ
.
get
(
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION"
,
"True"
).
lower
()
==
"true"
)
RAG_EMBEDDING_ENGINE
=
os
.
environ
.
get
(
"RAG_EMBEDDING_ENGINE"
,
""
)
RAG_EMBEDDING_ENGINE
=
os
.
environ
.
get
(
"RAG_EMBEDDING_ENGINE"
,
""
)
PDF_EXTRACT_IMAGES
=
os
.
environ
.
get
(
"PDF_EXTRACT_IMAGES"
,
"False"
).
lower
()
==
"true"
PDF_EXTRACT_IMAGES
=
os
.
environ
.
get
(
"PDF_EXTRACT_IMAGES"
,
"False"
).
lower
()
==
"true"
...
@@ -531,7 +538,9 @@ RAG_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE)
...
@@ -531,7 +538,9 @@ RAG_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE)
RAG_OPENAI_API_BASE_URL
=
os
.
getenv
(
"RAG_OPENAI_API_BASE_URL"
,
OPENAI_API_BASE_URL
)
RAG_OPENAI_API_BASE_URL
=
os
.
getenv
(
"RAG_OPENAI_API_BASE_URL"
,
OPENAI_API_BASE_URL
)
RAG_OPENAI_API_KEY
=
os
.
getenv
(
"RAG_OPENAI_API_KEY"
,
OPENAI_API_KEY
)
RAG_OPENAI_API_KEY
=
os
.
getenv
(
"RAG_OPENAI_API_KEY"
,
OPENAI_API_KEY
)
ENABLE_LOCAL_WEB_FETCH
=
os
.
getenv
(
"ENABLE_LOCAL_WEB_FETCH"
,
"False"
).
lower
()
==
"true"
ENABLE_RAG_LOCAL_WEB_FETCH
=
(
os
.
getenv
(
"ENABLE_RAG_LOCAL_WEB_FETCH"
,
"False"
).
lower
()
==
"true"
)
SEARXNG_QUERY_URL
=
os
.
getenv
(
"SEARXNG_QUERY_URL"
,
""
)
SEARXNG_QUERY_URL
=
os
.
getenv
(
"SEARXNG_QUERY_URL"
,
""
)
GOOGLE_PSE_API_KEY
=
os
.
getenv
(
"GOOGLE_PSE_API_KEY"
,
""
)
GOOGLE_PSE_API_KEY
=
os
.
getenv
(
"GOOGLE_PSE_API_KEY"
,
""
)
...
...
backend/main.py
View file @
635951b5
...
@@ -15,7 +15,7 @@ from fastapi.middleware.wsgi import WSGIMiddleware
...
@@ -15,7 +15,7 @@ from fastapi.middleware.wsgi import WSGIMiddleware
from
fastapi.middleware.cors
import
CORSMiddleware
from
fastapi.middleware.cors
import
CORSMiddleware
from
starlette.exceptions
import
HTTPException
as
StarletteHTTPException
from
starlette.exceptions
import
HTTPException
as
StarletteHTTPException
from
starlette.middleware.base
import
BaseHTTPMiddleware
from
starlette.middleware.base
import
BaseHTTPMiddleware
from
starlette.responses
import
StreamingResponse
from
apps.ollama.main
import
app
as
ollama_app
from
apps.ollama.main
import
app
as
ollama_app
from
apps.openai.main
import
app
as
openai_app
from
apps.openai.main
import
app
as
openai_app
...
@@ -102,6 +102,8 @@ origins = ["*"]
...
@@ -102,6 +102,8 @@ origins = ["*"]
class
RAGMiddleware
(
BaseHTTPMiddleware
):
class
RAGMiddleware
(
BaseHTTPMiddleware
):
async
def
dispatch
(
self
,
request
:
Request
,
call_next
):
async
def
dispatch
(
self
,
request
:
Request
,
call_next
):
return_citations
=
False
if
request
.
method
==
"POST"
and
(
if
request
.
method
==
"POST"
and
(
"/api/chat"
in
request
.
url
.
path
or
"/chat/completions"
in
request
.
url
.
path
"/api/chat"
in
request
.
url
.
path
or
"/chat/completions"
in
request
.
url
.
path
):
):
...
@@ -114,11 +116,15 @@ class RAGMiddleware(BaseHTTPMiddleware):
...
@@ -114,11 +116,15 @@ class RAGMiddleware(BaseHTTPMiddleware):
# Parse string to JSON
# Parse string to JSON
data
=
json
.
loads
(
body_str
)
if
body_str
else
{}
data
=
json
.
loads
(
body_str
)
if
body_str
else
{}
return_citations
=
data
.
get
(
"citations"
,
False
)
if
"citations"
in
data
:
del
data
[
"citations"
]
# Example: Add a new key-value pair or modify existing ones
# Example: Add a new key-value pair or modify existing ones
# data["modified"] = True # Example modification
# data["modified"] = True # Example modification
if
"docs"
in
data
:
if
"docs"
in
data
:
data
=
{
**
data
}
data
=
{
**
data
}
data
[
"messages"
]
=
rag_messages
(
data
[
"messages"
]
,
citations
=
rag_messages
(
docs
=
data
[
"docs"
],
docs
=
data
[
"docs"
],
messages
=
data
[
"messages"
],
messages
=
data
[
"messages"
],
template
=
rag_app
.
state
.
RAG_TEMPLATE
,
template
=
rag_app
.
state
.
RAG_TEMPLATE
,
...
@@ -130,7 +136,9 @@ class RAGMiddleware(BaseHTTPMiddleware):
...
@@ -130,7 +136,9 @@ class RAGMiddleware(BaseHTTPMiddleware):
)
)
del
data
[
"docs"
]
del
data
[
"docs"
]
log
.
debug
(
f
"data['messages']:
{
data
[
'messages'
]
}
"
)
log
.
debug
(
f
"data['messages']:
{
data
[
'messages'
]
}
, citations:
{
citations
}
"
)
modified_body_bytes
=
json
.
dumps
(
data
).
encode
(
"utf-8"
)
modified_body_bytes
=
json
.
dumps
(
data
).
encode
(
"utf-8"
)
...
@@ -148,11 +156,36 @@ class RAGMiddleware(BaseHTTPMiddleware):
...
@@ -148,11 +156,36 @@ class RAGMiddleware(BaseHTTPMiddleware):
]
]
response
=
await
call_next
(
request
)
response
=
await
call_next
(
request
)
if
return_citations
:
# Inject the citations into the response
if
isinstance
(
response
,
StreamingResponse
):
# If it's a streaming response, inject it as SSE event or NDJSON line
content_type
=
response
.
headers
.
get
(
"Content-Type"
)
if
"text/event-stream"
in
content_type
:
return
StreamingResponse
(
self
.
openai_stream_wrapper
(
response
.
body_iterator
,
citations
),
)
if
"application/x-ndjson"
in
content_type
:
return
StreamingResponse
(
self
.
ollama_stream_wrapper
(
response
.
body_iterator
,
citations
),
)
return
response
return
response
async
def
_receive
(
self
,
body
:
bytes
):
async
def
_receive
(
self
,
body
:
bytes
):
return
{
"type"
:
"http.request"
,
"body"
:
body
,
"more_body"
:
False
}
return
{
"type"
:
"http.request"
,
"body"
:
body
,
"more_body"
:
False
}
async
def
openai_stream_wrapper
(
self
,
original_generator
,
citations
):
yield
f
"data:
{
json
.
dumps
(
{
'citations'
:
citations
}
)
}
\n\n
"
async
for
data
in
original_generator
:
yield
data
async
def
ollama_stream_wrapper
(
self
,
original_generator
,
citations
):
yield
f
"
{
json
.
dumps
(
{
'citations'
:
citations
}
)
}
\n
"
async
for
data
in
original_generator
:
yield
data
app
.
add_middleware
(
RAGMiddleware
)
app
.
add_middleware
(
RAGMiddleware
)
...
...
src/app.css
View file @
635951b5
...
@@ -82,3 +82,12 @@ select {
...
@@ -82,3 +82,12 @@ select {
.katex-mathml
{
.katex-mathml
{
display
:
none
;
display
:
none
;
}
}
.scrollbar-none
:active::-webkit-scrollbar-thumb
,
.scrollbar-none
:focus::-webkit-scrollbar-thumb
,
.scrollbar-none
:hover::-webkit-scrollbar-thumb
{
visibility
:
visible
;
}
.scrollbar-none
::-webkit-scrollbar-thumb
{
visibility
:
hidden
;
}
src/lib/apis/ollama/index.ts
View file @
635951b5
...
@@ -159,7 +159,11 @@ export const generateTitle = async (
...
@@ -159,7 +159,11 @@ export const generateTitle = async (
body
:
JSON
.
stringify
({
body
:
JSON
.
stringify
({
model
:
model
,
model
:
model
,
prompt
:
template
,
prompt
:
template
,
stream
:
false
stream
:
false
,
options
:
{
// Restrict the number of tokens generated to 50
num_predict
:
50
}
})
})
})
})
.
then
(
async
(
res
)
=>
{
.
then
(
async
(
res
)
=>
{
...
...
src/lib/apis/openai/index.ts
View file @
635951b5
...
@@ -295,7 +295,9 @@ export const generateTitle = async (
...
@@ -295,7 +295,9 @@ export const generateTitle = async (
content
:
template
content
:
template
}
}
],
],
stream
:
false
stream
:
false
,
// Restricting the max tokens to 50 to avoid long titles
max_tokens
:
50
})
})
})
})
.
then
(
async
(
res
)
=>
{
.
then
(
async
(
res
)
=>
{
...
...
src/lib/apis/rag/index.ts
View file @
635951b5
...
@@ -33,8 +33,9 @@ type ChunkConfigForm = {
...
@@ -33,8 +33,9 @@ type ChunkConfigForm = {
};
};
type
RAGConfigForm
=
{
type
RAGConfigForm
=
{
pdf_extract_images
:
boolean
;
pdf_extract_images
?:
boolean
;
chunk
:
ChunkConfigForm
;
chunk
?:
ChunkConfigForm
;
web_loader_ssl_verification
?:
boolean
;
};
};
export
const
updateRAGConfig
=
async
(
token
:
string
,
payload
:
RAGConfigForm
)
=>
{
export
const
updateRAGConfig
=
async
(
token
:
string
,
payload
:
RAGConfigForm
)
=>
{
...
...
src/lib/apis/streaming/index.ts
View file @
635951b5
...
@@ -4,6 +4,8 @@ import type { ParsedEvent } from 'eventsource-parser';
...
@@ -4,6 +4,8 @@ import type { ParsedEvent } from 'eventsource-parser';
type
TextStreamUpdate
=
{
type
TextStreamUpdate
=
{
done
:
boolean
;
done
:
boolean
;
value
:
string
;
value
:
string
;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
citations
?:
any
;
};
};
// createOpenAITextStream takes a responseBody with a SSE response,
// createOpenAITextStream takes a responseBody with a SSE response,
...
@@ -45,6 +47,11 @@ async function* openAIStreamToIterator(
...
@@ -45,6 +47,11 @@ async function* openAIStreamToIterator(
const
parsedData
=
JSON
.
parse
(
data
);
const
parsedData
=
JSON
.
parse
(
data
);
console
.
log
(
parsedData
);
console
.
log
(
parsedData
);
if
(
parsedData
.
citations
)
{
yield
{
done
:
false
,
value
:
''
,
citations
:
parsedData
.
citations
};
continue
;
}
yield
{
done
:
false
,
value
:
parsedData
.
choices
?.[
0
]?.
delta
?.
content
??
''
};
yield
{
done
:
false
,
value
:
parsedData
.
choices
?.[
0
]?.
delta
?.
content
??
''
};
}
catch
(
e
)
{
}
catch
(
e
)
{
console
.
error
(
'
Error extracting delta from SSE event:
'
,
e
);
console
.
error
(
'
Error extracting delta from SSE event:
'
,
e
);
...
@@ -62,6 +69,10 @@ async function* streamLargeDeltasAsRandomChunks(
...
@@ -62,6 +69,10 @@ async function* streamLargeDeltasAsRandomChunks(
yield
textStreamUpdate
;
yield
textStreamUpdate
;
return
;
return
;
}
}
if
(
textStreamUpdate
.
citations
)
{
yield
textStreamUpdate
;
continue
;
}
let
content
=
textStreamUpdate
.
value
;
let
content
=
textStreamUpdate
.
value
;
if
(
content
.
length
<
5
)
{
if
(
content
.
length
<
5
)
{
yield
{
done
:
false
,
value
:
content
};
yield
{
done
:
false
,
value
:
content
};
...
...
src/lib/components/chat/Messages/CitationsModal.svelte
0 → 100644
View file @
635951b5
<script lang="ts">
import { getContext, onMount, tick } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false;
export let citation;
let mergedDocuments = [];
$: if (citation) {
mergedDocuments = citation.document?.map((c, i) => {
return {
source: citation.source,
document: c,
metadata: citation.metadata?.[i]
};
});
}
</script>
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center capitalize">
{$i18n.t('Citation')}
</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
<div
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-none"
>
{#each mergedDocuments as document, documentIdx}
<div class="flex flex-col w-full">
<div class="text-sm font-medium dark:text-gray-300">
{$i18n.t('Source')}
</div>
<div class="text-sm dark:text-gray-400">
{document.source?.name ?? $i18n.t('No source available')}
</div>
</div>
<div class="flex flex-col w-full">
<div class=" text-sm font-medium dark:text-gray-300">
{$i18n.t('Content')}
</div>
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">
{document.document}
</pre>
</div>
{#if documentIdx !== mergedDocuments.length - 1}
<hr class=" dark:border-gray-850 my-3" />
{/if}
{/each}
</div>
</div>
</div>
</Modal>
src/lib/components/chat/Messages/ResponseMessage.svelte
View file @
635951b5
...
@@ -32,6 +32,7 @@
...
@@ -32,6 +32,7 @@
import { WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import RateComment from './RateComment.svelte';
import RateComment from './RateComment.svelte';
import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
export let modelfiles = [];
export let modelfiles = [];
export let message;
export let message;
...
@@ -65,6 +66,9 @@
...
@@ -65,6 +66,9 @@
let showRateComment = false;
let showRateComment = false;
let showCitationModal = false;
let selectedCitation = null;
$: tokens = marked.lexer(sanitizeResponseContent(message.content));
$: tokens = marked.lexer(sanitizeResponseContent(message.content));
const renderer = new marked.Renderer();
const renderer = new marked.Renderer();
...
@@ -324,6 +328,8 @@
...
@@ -324,6 +328,8 @@
});
});
</script>
</script>
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
{#key message.id}
{#key message.id}
<div class=" flex w-full message-{message.id}" id="message-{message.id}">
<div class=" flex w-full message-{message.id}" id="message-{message.id}">
<ProfileImage
<ProfileImage
...
@@ -360,7 +366,6 @@
...
@@ -360,7 +366,6 @@
{/each}
{/each}
</div>
</div>
{/if}
{/if}
<div
<div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
>
>
...
@@ -441,6 +446,66 @@
...
@@ -441,6 +446,66 @@
{/each}
{/each}
<!-- {@html marked(message.content.replaceAll('\\', '\\\\'))} -->
<!-- {@html marked(message.content.replaceAll('\\', '\\\\'))} -->
{/if}
{/if}
</div>
{/if}
</div>
</div>
<!-- if (message.citations) {
citations = message.citations.forEach((citation) => {
citation.document.forEach((document, index) => {
const metadata = citation.metadata?.[index];
const source = citation?.source?.name ?? metadata?.source ?? 'N/A';
citations[source] = citations[source] || {
source: citation.source,
document: [],
metadata: []
};
citations[source].document.push(document);
citations[source].metadata.push(metadata);
});
});
} -->
{#if message.citations}
<hr class=" dark:border-gray-800" />
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
{#each message.citations.reduce((acc, citation) => {
citation.document.forEach((document, index) => {
const metadata = citation.metadata?.[index];
const id = metadata?.source ?? 'N/A';
const existingSource = acc.find((item) => item.id === id);
if (existingSource) {
existingSource.document.push(document);
existingSource.metadata.push(metadata);
} else {
acc.push( { id: id, source: citation?.source, document: [document], metadata: metadata ? [metadata] : [] } );
}
});
return acc;
}, []) as citation, idx}
<div class="flex gap-1 text-xs font-semibold">
<div>
[{idx + 1}]
</div>
<button
class="dark:text-gray-500 underline"
on:click={() => {
showCitationModal = true;
selectedCitation = citation;
}}
>
{citation.source.name}
</button>
</div>
{/each}
</div>
{/if}
{#if message.done}
{#if message.done}
<div
<div
...
@@ -553,8 +618,8 @@
...
@@ -553,8 +618,8 @@
<button
<button
class="{isLastMessage
class="{isLastMessage
? 'visible'
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
?.rating ===
?.rating ===
1
1
? 'bg-gray-100 dark:bg-gray-800'
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
on:click={() => {
...
@@ -562,9 +627,7 @@
...
@@ -562,9 +627,7 @@
showRateComment = true;
showRateComment = true;
window.setTimeout(() => {
window.setTimeout(() => {
document
document.getElementById(`message-feedback-${message.id}`)?.scrollIntoView();
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}, 0);
}}
}}
>
>
...
@@ -577,10 +640,11 @@
...
@@ -577,10 +640,11 @@
stroke-linejoin="round"
stroke-linejoin="round"
class="w-4 h-4"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
><path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
>
>
<path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/>
</svg>
</button>
</button>
</Tooltip>
</Tooltip>
...
@@ -588,17 +652,15 @@
...
@@ -588,17 +652,15 @@
<button
<button
class="{isLastMessage
class="{isLastMessage
? 'visible'
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
?.rating ===
?.rating ===
-1
-1
? 'bg-gray-100 dark:bg-gray-800'
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
on:click={() => {
rateMessage(message.id, -1);
rateMessage(message.id, -1);
showRateComment = true;
showRateComment = true;
window.setTimeout(() => {
window.setTimeout(() => {
document
document.getElementById(`message-feedback-${message.id}`)?.scrollIntoView();
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}, 0);
}}
}}
>
>
...
@@ -611,10 +673,11 @@
...
@@ -611,10 +673,11 @@
stroke-linejoin="round"
stroke-linejoin="round"
class="w-4 h-4"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
><path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
>
>
<path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/>
</svg>
</button>
</button>
</Tooltip>
</Tooltip>
{/if}
{/if}
...
@@ -637,35 +700,32 @@
...
@@ -637,35 +700,32 @@
fill="currentColor"
fill="currentColor"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
><style>
>
<style>
.spinner_S1WN {
.spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite;
animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s;
animation-delay: -0.8s;
}
}
.spinner_Km9P {
.spinner_Km9P {
animation-delay: -0.65s;
animation-delay: -0.65s;
}
}
.spinner_JApP {
.spinner_JApP {
animation-delay: -0.5s;
animation-delay: -0.5s;
}
}
@keyframes spinner_MGfb {
@keyframes spinner_MGfb {
93.75%,
93.75%,
100% {
100% {
opacity: 0.2;
opacity: 0.2;
}
}
}
}
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
</style>
class="spinner_S1WN spinner_Km9P"
<circle class="spinner_S1WN" cx="4" cy="12" r="3" />
cx="12"
<circle class="spinner_S1WN spinner_Km9P" cx="12" cy="12" r="3" />
cy="12"
<circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" />
r="3"
</svg>
/><circle
class="spinner_S1WN spinner_JApP"
cx="20"
cy="12"
r="3"
/></svg
>
{:else if speaking}
{:else if speaking}
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
...
@@ -718,35 +778,32 @@
...
@@ -718,35 +778,32 @@
fill="currentColor"
fill="currentColor"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
><style>
>
<style>
.spinner_S1WN {
.spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite;
animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s;
animation-delay: -0.8s;
}
}
.spinner_Km9P {
.spinner_Km9P {
animation-delay: -0.65s;
animation-delay: -0.65s;
}
}
.spinner_JApP {
.spinner_JApP {
animation-delay: -0.5s;
animation-delay: -0.5s;
}
}
@keyframes spinner_MGfb {
@keyframes spinner_MGfb {
93.75%,
93.75%,
100% {
100% {
opacity: 0.2;
opacity: 0.2;
}
}
}
}
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
</style>
class="spinner_S1WN spinner_Km9P"
<circle class="spinner_S1WN" cx="4" cy="12" r="3" />
cx="12"
<circle class="spinner_S1WN spinner_Km9P" cx="12" cy="12" r="3" />
cy="12"
<circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" />
r="3"
</svg>
/><circle
class="spinner_S1WN spinner_JApP"
cx="20"
cy="12"
r="3"
/></svg
>
{:else}
{:else}
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
...
@@ -867,10 +924,6 @@
...
@@ -867,10 +924,6 @@
}}
}}
/>
/>
{/if}
{/if}
</div>
{/if}
</div>
</div>
{/if}
{/if}
</div>
</div>
</div>
</div>
...
...
src/lib/components/chat/ModelSelector.svelte
View file @
635951b5
...
@@ -82,7 +82,7 @@
...
@@ -82,7 +82,7 @@
</div>
</div>
{:else}
{:else}
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
<Tooltip content=
"
Remove Model
"
>
<Tooltip content=
{$i18n.t('
Remove Model
')}
>
<button
<button
{disabled}
{disabled}
on:click={() => {
on:click={() => {
...
...
src/lib/components/chat/ModelSelector/Selector.svelte
View file @
635951b5
...
@@ -305,7 +305,7 @@
...
@@ -305,7 +305,7 @@
{:else}
{:else}
<div>
<div>
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
No results found
{$i18n.t('
No results found
')}
</div>
</div>
</div>
</div>
{/each}
{/each}
...
@@ -317,7 +317,7 @@
...
@@ -317,7 +317,7 @@
pullModelHandler();
pullModelHandler();
}}
}}
>
>
Pull "{searchValue}" from Ollama.com
{$i18n.t(`
Pull "{
{
searchValue}
}
" from Ollama.com
`, { searchValue: searchValue })}
</button>
</button>
{/if}
{/if}
...
...
src/lib/components/chat/Settings/Account.svelte
View file @
635951b5
...
@@ -447,7 +447,7 @@
...
@@ -447,7 +447,7 @@
{/if}
{/if}
</button>
</button>
<Tooltip content=
"
Create new key
"
>
<Tooltip content=
{$i18n.t('
Create new key
')}
>
<button
<button
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
on:click={() => {
on:click={() => {
...
@@ -479,7 +479,7 @@
...
@@ -479,7 +479,7 @@
>
>
<Plus strokeWidth="2" className=" size-3.5" />
<Plus strokeWidth="2" className=" size-3.5" />
Create new secret key</button
{$i18n.t('
Create new secret key
')}
</button
>
>
{/if}
{/if}
</div>
</div>
...
...
src/lib/components/chat/Settings/Connections.svelte
View file @
635951b5
...
@@ -164,7 +164,7 @@
...
@@ -164,7 +164,7 @@
<div class="flex gap-1.5">
<div class="flex gap-1.5">
<input
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder=
"
Enter URL (e.g. http://localhost:11434)
"
placeholder=
{$i18n.t('
Enter URL (e.g. http://localhost:11434)
')}
bind:value={url}
bind:value={url}
/>
/>
...
...
src/lib/components/chat/ShareChatModal.svelte
View file @
635951b5
...
@@ -97,9 +97,10 @@
...
@@ -97,9 +97,10 @@
<div class=" text-sm dark:text-gray-300 mb-1">
<div class=" text-sm dark:text-gray-300 mb-1">
{#if chat.share_id}
{#if chat.share_id}
<a href="/s/{chat.share_id}" target="_blank"
<a href="/s/{chat.share_id}" target="_blank"
>You have shared this chat <span class=" underline">before</span>.</a
>{$i18n.t('You have shared this chat')}
<span class=" underline">{$i18n.t('before')}</span>.</a
>
>
Click here to
{$i18n.t('
Click here to
')}
<button
<button
class="underline"
class="underline"
on:click={async () => {
on:click={async () => {
...
@@ -108,11 +109,14 @@
...
@@ -108,11 +109,14 @@
if (res) {
if (res) {
chat = await getChatById(localStorage.token, chatId);
chat = await getChatById(localStorage.token, chatId);
}
}
}}>delete this link</button
}}
> and create a new shared link.
>{$i18n.t('delete this link')}
</button>
{$i18n.t('and create a new shared link.')}
{:else}
{:else}
Messages you send after creating your link won't be shared. Users with the URL will be
{$i18n.t(
able to view the shared chat.
"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat."
)}
{/if}
{/if}
</div>
</div>
...
...
src/lib/components/common/ImagePreview.svelte
View file @
635951b5
...
@@ -51,7 +51,7 @@
...
@@ -51,7 +51,7 @@
<button
<button
class=" p-5"
class=" p-5"
on:click={() => {
on:click={() => {
downloadImage(src,
'Image.png'
);
downloadImage(src,
src.substring(src.lastIndexOf('/') + 1)
);
}}
}}
>
>
<svg
<svg
...
...
src/lib/components/documents/Settings/ChunkParams.svelte
0 → 100644
View file @
635951b5
<script lang="ts">
import { getDocs } from '$lib/apis/documents';
import {
getRAGConfig,
updateRAGConfig,
getQuerySettings,
scanDocs,
updateQuerySettings,
resetVectorDB,
getEmbeddingConfig,
updateEmbeddingConfig,
getRerankingConfig,
updateRerankingConfig
} from '$lib/apis/rag';
import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
let scanDirLoading = false;
let updateEmbeddingModelLoading = false;
let updateRerankingModelLoading = false;
let showResetConfirm = false;
let chunkSize = 0;
let chunkOverlap = 0;
let pdfExtractImages = true;
const submitHandler = async () => {
const res = await updateRAGConfig(localStorage.token, {
pdf_extract_images: pdfExtractImages,
chunk: {
chunk_overlap: chunkOverlap,
chunk_size: chunkSize
}
});
};
onMount(async () => {
const res = await getRAGConfig(localStorage.token);
if (res) {
pdfExtractImages = res.pdf_extract_images;
chunkSize = res.chunk.chunk_size;
chunkOverlap = res.chunk.chunk_overlap;
}
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
submitHandler();
saveHandler();
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-full max-h-[22rem]">
<div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
<div class=" flex">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Size')}</div>
<div class="self-center p-3">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number"
placeholder={$i18n.t('Enter Chunk Size')}
bind:value={chunkSize}
autocomplete="off"
min="0"
/>
</div>
</div>
<div class="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">
{$i18n.t('Chunk Overlap')}
</div>
<div class="self-center p-3">
<input
class="w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number"
placeholder={$i18n.t('Enter Chunk Overlap')}
bind:value={chunkOverlap}
autocomplete="off"
min="0"
/>
</div>
</div>
</div>
<div class="pr-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
pdfExtractImages = !pdfExtractImages;
}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
src/lib/components/documents/Settings/General.svelte
View file @
635951b5
<script lang="ts">
<script lang="ts">
import { getDocs } from '$lib/apis/documents';
import { getDocs } from '$lib/apis/documents';
import {
import {
getRAGConfig,
updateRAGConfig,
getQuerySettings,
getQuerySettings,
scanDocs,
scanDocs,
updateQuerySettings,
updateQuerySettings,
...
@@ -17,8 +15,6 @@
...
@@ -17,8 +15,6 @@
import { onMount, getContext } from 'svelte';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
export let saveHandler: Function;
export let saveHandler: Function;
...
@@ -36,10 +32,6 @@
...
@@ -36,10 +32,6 @@
let OpenAIKey = '';
let OpenAIKey = '';
let OpenAIUrl = '';
let OpenAIUrl = '';
let chunkSize = 0;
let chunkOverlap = 0;
let pdfExtractImages = true;
let querySettings = {
let querySettings = {
template: '',
template: '',
r: 0.0,
r: 0.0,
...
@@ -151,14 +143,11 @@
...
@@ -151,14 +143,11 @@
};
};
const submitHandler = async () => {
const submitHandler = async () => {
const res = await updateRAGConfig(localStorage.token, {
embeddingModelUpdateHandler();
pdf_extract_images: pdfExtractImages,
chunk: {
if (querySettings.hybrid) {
chunk_overlap: chunkOverlap,
rerankingModelUpdateHandler();
chunk_size: chunkSize
}
}
});
querySettings = await updateQuerySettings(localStorage.token, querySettings);
};
};
const setEmbeddingConfig = async () => {
const setEmbeddingConfig = async () => {
...
@@ -183,20 +172,10 @@
...
@@ -183,20 +172,10 @@
const toggleHybridSearch = async () => {
const toggleHybridSearch = async () => {
querySettings.hybrid = !querySettings.hybrid;
querySettings.hybrid = !querySettings.hybrid;
querySettings = await updateQuerySettings(localStorage.token, querySettings);
querySettings = await updateQuerySettings(localStorage.token, querySettings);
};
};
onMount(async () => {
onMount(async () => {
const res = await getRAGConfig(localStorage.token);
if (res) {
pdfExtractImages = res.pdf_extract_images;
chunkSize = res.chunk.chunk_size;
chunkOverlap = res.chunk.chunk_overlap;
}
await setEmbeddingConfig();
await setEmbeddingConfig();
await setRerankingConfig();
await setRerankingConfig();
...
@@ -211,24 +190,54 @@
...
@@ -211,24 +190,54 @@
saveHandler();
saveHandler();
}}
}}
>
>
<div class=" space-y-
3
pr-1.5 overflow-y-scroll max-h-[22rem]">
<div class=" space-y-
2.5
pr-1.5 overflow-y-scroll max-h-[22rem]">
<div>
<div
class="flex flex-col gap-0.5"
>
<div class=" mb-
2
text-sm font-medium">{$i18n.t('General Settings')}</div>
<div class=" mb-
0.5
text-sm font-medium">{$i18n.t('General Settings')}</div>
<div class=" flex w-full justify-between">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
</div>
<button
<button
class="p-1 px-3 text-xs flex rounded transition"
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
? ' cursor-not-allowed'
: ''}"
on:click={() => {
on:click={() => {
toggleHybridSearch();
scanHandler();
console.log('check');
}}
}}
type="button"
type="button"
disabled={scanDirLoading}
>
>
{#if querySettings.hybrid === true}
<div class="self-center font-medium">{$i18n.t('Scan')}</div>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
{#if scanDirLoading}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
<div class="ml-3 self-center">
<svg
class=" w-3 h-3"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
{/if}
</button>
</button>
</div>
</div>
...
@@ -256,7 +265,7 @@
...
@@ -256,7 +265,7 @@
</div>
</div>
{#if embeddingEngine === 'openai'}
{#if embeddingEngine === 'openai'}
<div class="m
t-1
flex gap-2">
<div class="m
y-0.5
flex gap-2">
<input
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
placeholder={$i18n.t('API Base URL')}
...
@@ -272,11 +281,31 @@
...
@@ -272,11 +281,31 @@
/>
/>
</div>
</div>
{/if}
{/if}
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleHybridSearch();
}}
type="button"
>
{#if querySettings.hybrid === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
</div>
<hr class=" dark:border-gray-700 my-1" />
<div class="space-y-2">
<div class="space-y-2"
/
>
<div>
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('
Update
Embedding Model')}</div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Embedding Model')}</div>
{#if embeddingEngine === 'ollama'}
{#if embeddingEngine === 'ollama'}
<div class="flex w-full">
<div class="flex w-full">
...
@@ -297,66 +326,20 @@
...
@@ -297,66 +326,20 @@
{/each}
{/each}
</select>
</select>
</div>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
embeddingModelUpdateHandler();
}}
disabled={updateEmbeddingModelLoading}
>
{#if updateEmbeddingModelLoading}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
</div>
{:else}
{:else}
<div class="flex w-full">
<div class="flex w-full">
<div class="flex-1 mr-2">
<div class="flex-1 mr-2">
<input
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('
Update
embedding model (e.g. {{model}})', {
placeholder={$i18n.t('
Set
embedding model (e.g. {{model}})', {
model: embeddingModel.slice(-40)
model: embeddingModel.slice(-40)
})}
})}
bind:value={embeddingModel}
bind:value={embeddingModel}
/>
/>
</div>
</div>
{#if embeddingEngine === ''}
<button
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
on:click={() => {
...
@@ -406,6 +389,7 @@
...
@@ -406,6 +389,7 @@
</svg>
</svg>
{/if}
{/if}
</button>
</button>
{/if}
</div>
</div>
{/if}
{/if}
...
@@ -415,18 +399,16 @@
...
@@ -415,18 +399,16 @@
)}
)}
</div>
</div>
<hr class=" dark:border-gray-700 my-3" />
{#if querySettings.hybrid === true}
{#if querySettings.hybrid === true}
<div class=" ">
<div class=" ">
<div class=" mb-2 text-sm font-medium">{$i18n.t('
Update
Reranking Model')}</div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Reranking Model')}</div>
<div class="flex w-full">
<div class="flex w-full">
<div class="flex-1 mr-2">
<div class="flex-1 mr-2">
<input
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('
Update
reranking model (e.g. {{model}})', {
placeholder={$i18n.t('
Set
reranking model (e.g. {{model}})', {
model:
rerankingModel.slice(-40)
model:
'BAAI/bge-reranker-v2-m3'
})}
})}
bind:value={rerankingModel}
bind:value={rerankingModel}
/>
/>
...
@@ -482,174 +464,10 @@
...
@@ -482,174 +464,10 @@
</button>
</button>
</div>
</div>
</div>
</div>
<hr class=" dark:border-gray-700 my-3" />
{/if}
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
</div>
<button
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
? ' cursor-not-allowed'
: ''}"
on:click={() => {
scanHandler();
console.log('check');
}}
type="button"
disabled={scanDirLoading}
>
<div class="self-center font-medium">{$i18n.t('Scan')}</div>
{#if scanDirLoading}
<div class="ml-3 self-center">
<svg
class=" w-3 h-3"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
{/if}
</button>
</div>
</div>
<hr class=" dark:border-gray-700 my-3" />
<hr class=" dark:border-gray-700" />
<div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
<div class=" flex">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Size')}</div>
<div class="self-center p-3">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number"
placeholder={$i18n.t('Enter Chunk Size')}
bind:value={chunkSize}
autocomplete="off"
min="0"
/>
</div>
</div>
<div class="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">
{$i18n.t('Chunk Overlap')}
</div>
<div class="self-center p-3">
<input
class="w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number"
placeholder={$i18n.t('Enter Chunk Overlap')}
bind:value={chunkOverlap}
autocomplete="off"
min="0"
/>
</div>
</div>
</div>
<div class="pr-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
pdfExtractImages = !pdfExtractImages;
}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div>
</div>
</div>
<hr class=" dark:border-gray-700 my-3" />
<div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
<div class=" flex">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div>
<div class="self-center p-3">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number"
placeholder={$i18n.t('Enter Top K')}
bind:value={querySettings.k}
autocomplete="off"
min="0"
/>
</div>
</div>
{#if querySettings.hybrid === true}
<div class="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">
{$i18n.t('Minimum Score')}
</div>
<div class="self-center p-3">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number"
step="0.01"
placeholder={$i18n.t('Enter Score')}
bind:value={querySettings.r}
autocomplete="off"
min="0.0"
title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')}
/>
</div>
</div>
{/if}
</div>
{#if querySettings.hybrid === true}
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(
'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.'
)}
</div>
<hr class=" dark:border-gray-700 my-3" />
{/if}
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
<textarea
bind:value={querySettings.template}
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="4"
/>
</div>
</div>
{#if showResetConfirm}
{#if showResetConfirm}
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
...
@@ -743,8 +561,6 @@
...
@@ -743,8 +561,6 @@
</button>
</button>
{/if}
{/if}
</div>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<div class="flex justify-end pt-3 text-sm font-medium">
<button
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
...
...
Prev
1
2
3
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment