mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-17 07:45:47 +00:00
Compare commits
23 Commits
v0.4.1
...
eval/split
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4293543a6a | ||
|
|
e95bfa0e0b | ||
|
|
4848b5f1de | ||
|
|
7ba5c434fa | ||
|
|
59bf5ba848 | ||
|
|
f66c33380c | ||
|
|
115650ce9f | ||
|
|
7aa3602fca | ||
|
|
864c552a17 | ||
|
|
07b2ed3d8f | ||
|
|
38290057f2 | ||
|
|
2344edf158 | ||
|
|
86d1804eb0 | ||
|
|
1ebae50d0c | ||
|
|
a9fbaa396c | ||
|
|
27d5f69427 | ||
|
|
5d98421ae8 | ||
|
|
6b561b8ca9 | ||
|
|
2dc7e64dd7 | ||
|
|
5230f7e22f | ||
|
|
a595d43ae3 | ||
|
|
ee561f42ff | ||
|
|
f00b3d76b3 |
@@ -1,8 +1,6 @@
|
||||
name: Build Backend Image on Merge Group
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name: Build Web Image on Merge Group
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""add search doc relevance details
|
||||
|
||||
Revision ID: 05c07bf07c00
|
||||
Revises: 3a7802814195
|
||||
Revises: b896bbd0d5a7
|
||||
Create Date: 2024-07-10 17:48:15.886653
|
||||
|
||||
"""
|
||||
|
||||
@@ -11,8 +11,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "b896bbd0d5a7"
|
||||
down_revision = "44f856ae2a4a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: None = None
|
||||
depends_on: None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -223,6 +223,11 @@ MAX_PRUNING_DOCUMENT_RETRIEVAL_PER_MINUTE = int(
|
||||
os.environ.get("MAX_PRUNING_DOCUMENT_RETRIEVAL_PER_MINUTE", 0)
|
||||
)
|
||||
|
||||
# comma delimited list of zendesk article labels to skip indexing for
|
||||
ZENDESK_CONNECTOR_SKIP_ARTICLE_LABELS = os.environ.get(
|
||||
"ZENDESK_CONNECTOR_SKIP_ARTICLE_LABELS", ""
|
||||
).split(",")
|
||||
|
||||
|
||||
#####
|
||||
# Indexing Configs
|
||||
|
||||
@@ -38,7 +38,7 @@ def make_confluence_call_handle_rate_limit(confluence_call: F) -> F:
|
||||
retry_after = None
|
||||
try:
|
||||
retry_after = int(e.response.headers.get("Retry-After"))
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if retry_after:
|
||||
|
||||
@@ -88,7 +88,7 @@ def _process_file(
|
||||
# add a prefix to avoid conflicts with other connectors
|
||||
doc_id = f"FILE_CONNECTOR__{file_name}"
|
||||
if metadata:
|
||||
doc_id = metadata.get("id") or doc_id
|
||||
doc_id = metadata.get("document_id") or doc_id
|
||||
|
||||
# If this is set, we will show this in the UI as the "name" of the file
|
||||
file_display_name = all_metadata.get("file_display_name") or os.path.basename(
|
||||
@@ -111,6 +111,7 @@ def _process_file(
|
||||
for k, v in all_metadata.items()
|
||||
if k
|
||||
not in [
|
||||
"document_id",
|
||||
"time_updated",
|
||||
"doc_updated_at",
|
||||
"link",
|
||||
|
||||
@@ -4,6 +4,7 @@ from zenpy import Zenpy # type: ignore
|
||||
from zenpy.lib.api_objects.help_centre_objects import Article # type: ignore
|
||||
|
||||
from danswer.configs.app_configs import INDEX_BATCH_SIZE
|
||||
from danswer.configs.app_configs import ZENDESK_CONNECTOR_SKIP_ARTICLE_LABELS
|
||||
from danswer.configs.constants import DocumentSource
|
||||
from danswer.connectors.cross_connector_utils.miscellaneous_utils import (
|
||||
time_str_to_utc,
|
||||
@@ -81,7 +82,14 @@ class ZendeskConnector(LoadConnector, PollConnector):
|
||||
)
|
||||
doc_batch = []
|
||||
for article in articles:
|
||||
if article.body is None or article.draft:
|
||||
if (
|
||||
article.body is None
|
||||
or article.draft
|
||||
or any(
|
||||
label in ZENDESK_CONNECTOR_SKIP_ARTICLE_LABELS
|
||||
for label in article.label_names
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
doc_batch.append(_article_to_document(article))
|
||||
|
||||
@@ -146,6 +146,12 @@ def delete_search_doc_message_relationship(
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def delete_tool_call_for_message_id(message_id: int, db_session: Session) -> None:
|
||||
stmt = delete(ToolCall).where(ToolCall.message_id == message_id)
|
||||
db_session.execute(stmt)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def delete_orphaned_search_docs(db_session: Session) -> None:
|
||||
orphaned_docs = (
|
||||
db_session.query(SearchDoc)
|
||||
@@ -169,6 +175,7 @@ def delete_messages_and_files_from_chat_session(
|
||||
).fetchall()
|
||||
|
||||
for id, files in messages_with_files:
|
||||
delete_tool_call_for_message_id(message_id=id, db_session=db_session)
|
||||
delete_search_doc_message_relationship(message_id=id, db_session=db_session)
|
||||
for file_info in files or {}:
|
||||
lobj_name = file_info.get("id")
|
||||
|
||||
@@ -153,43 +153,41 @@ schema DANSWER_CHUNK_NAME {
|
||||
query(query_embedding) tensor<float>(x[VARIABLE_DIM])
|
||||
}
|
||||
|
||||
# This must be separate function for normalize_linear to work
|
||||
function vector_score() {
|
||||
function title_vector_score() {
|
||||
expression {
|
||||
# If no title, the full vector score comes from the content embedding
|
||||
(query(title_content_ratio) * if(attribute(skip_title), closeness(field, embeddings), closeness(field, title_embedding))) +
|
||||
((1 - query(title_content_ratio)) * closeness(field, embeddings))
|
||||
}
|
||||
}
|
||||
|
||||
# This must be separate function for normalize_linear to work
|
||||
function keyword_score() {
|
||||
expression {
|
||||
(query(title_content_ratio) * bm25(title)) +
|
||||
((1 - query(title_content_ratio)) * bm25(content))
|
||||
#query(title_content_ratio) * if(attribute(skip_title), closeness(field, embeddings), closeness(field, title_embedding))
|
||||
if(attribute(skip_title), closeness(field, embeddings), closeness(field, title_embedding))
|
||||
}
|
||||
}
|
||||
|
||||
first-phase {
|
||||
expression: vector_score
|
||||
expression: closeness(field, embeddings)
|
||||
}
|
||||
|
||||
# Weighted average between Vector Search and BM-25
|
||||
# Each is a weighted average between the Title and Content fields
|
||||
# Finally each doc is boosted by it's user feedback based boost and recency
|
||||
# If any embedding or index field is missing, it just receives a score of 0
|
||||
# Assumptions:
|
||||
# - For a given query + corpus, the BM-25 scores will be relatively similar in distribution
|
||||
# therefore not normalizing before combining.
|
||||
# - For documents without title, it gets a score of 0 for that and this is ok as documents
|
||||
# without any title match should be penalized.
|
||||
global-phase {
|
||||
expression {
|
||||
(
|
||||
# Weighted Vector Similarity Score
|
||||
(query(alpha) * normalize_linear(vector_score)) +
|
||||
(
|
||||
query(alpha) * (
|
||||
(query(title_content_ratio) * normalize_linear(title_vector_score))
|
||||
+
|
||||
((1 - query(title_content_ratio)) * normalize_linear(closeness(field, embeddings)))
|
||||
)
|
||||
)
|
||||
|
||||
+
|
||||
|
||||
# Weighted Keyword Similarity Score
|
||||
((1 - query(alpha)) * normalize_linear(keyword_score))
|
||||
(
|
||||
(1 - query(alpha)) * (
|
||||
(query(title_content_ratio) * normalize_linear(bm25(title)))
|
||||
+
|
||||
((1 - query(title_content_ratio)) * normalize_linear(bm25(content)))
|
||||
)
|
||||
)
|
||||
)
|
||||
# Boost based on user feedback
|
||||
* document_boost
|
||||
@@ -204,8 +202,6 @@ schema DANSWER_CHUNK_NAME {
|
||||
bm25(content)
|
||||
closeness(field, title_embedding)
|
||||
closeness(field, embeddings)
|
||||
keyword_score
|
||||
vector_score
|
||||
document_boost
|
||||
recency_bias
|
||||
closest(embeddings)
|
||||
|
||||
@@ -232,32 +232,6 @@ class DefaultMultiLLM(LLM):
|
||||
|
||||
self._model_kwargs = model_kwargs
|
||||
|
||||
@staticmethod
|
||||
def _log_prompt(prompt: LanguageModelInput) -> None:
|
||||
if isinstance(prompt, list):
|
||||
for ind, msg in enumerate(prompt):
|
||||
if isinstance(msg, AIMessageChunk):
|
||||
if msg.content:
|
||||
log_msg = msg.content
|
||||
elif msg.tool_call_chunks:
|
||||
log_msg = "Tool Calls: " + str(
|
||||
[
|
||||
{
|
||||
key: value
|
||||
for key, value in tool_call.items()
|
||||
if key != "index"
|
||||
}
|
||||
for tool_call in msg.tool_call_chunks
|
||||
]
|
||||
)
|
||||
else:
|
||||
log_msg = ""
|
||||
logger.debug(f"Message {ind}:\n{log_msg}")
|
||||
else:
|
||||
logger.debug(f"Message {ind}:\n{msg.content}")
|
||||
if isinstance(prompt, str):
|
||||
logger.debug(f"Prompt:\n{prompt}")
|
||||
|
||||
def log_model_configs(self) -> None:
|
||||
logger.info(f"Config: {self.config}")
|
||||
|
||||
@@ -311,7 +285,7 @@ class DefaultMultiLLM(LLM):
|
||||
api_version=self._api_version,
|
||||
)
|
||||
|
||||
def invoke(
|
||||
def _invoke_implementation(
|
||||
self,
|
||||
prompt: LanguageModelInput,
|
||||
tools: list[dict] | None = None,
|
||||
@@ -319,7 +293,6 @@ class DefaultMultiLLM(LLM):
|
||||
) -> BaseMessage:
|
||||
if LOG_DANSWER_MODEL_INTERACTIONS:
|
||||
self.log_model_configs()
|
||||
self._log_prompt(prompt)
|
||||
|
||||
response = cast(
|
||||
litellm.ModelResponse, self._completion(prompt, tools, tool_choice, False)
|
||||
@@ -328,7 +301,7 @@ class DefaultMultiLLM(LLM):
|
||||
response.choices[0].message
|
||||
)
|
||||
|
||||
def stream(
|
||||
def _stream_implementation(
|
||||
self,
|
||||
prompt: LanguageModelInput,
|
||||
tools: list[dict] | None = None,
|
||||
@@ -336,7 +309,6 @@ class DefaultMultiLLM(LLM):
|
||||
) -> Iterator[BaseMessage]:
|
||||
if LOG_DANSWER_MODEL_INTERACTIONS:
|
||||
self.log_model_configs()
|
||||
self._log_prompt(prompt)
|
||||
|
||||
if DISABLE_LITELLM_STREAMING:
|
||||
yield self.invoke(prompt)
|
||||
|
||||
@@ -76,7 +76,7 @@ class CustomModelServer(LLM):
|
||||
def log_model_configs(self) -> None:
|
||||
logger.debug(f"Custom model at: {self._endpoint}")
|
||||
|
||||
def invoke(
|
||||
def _invoke_implementation(
|
||||
self,
|
||||
prompt: LanguageModelInput,
|
||||
tools: list[dict] | None = None,
|
||||
@@ -84,7 +84,7 @@ class CustomModelServer(LLM):
|
||||
) -> BaseMessage:
|
||||
return self._execute(prompt)
|
||||
|
||||
def stream(
|
||||
def _stream_implementation(
|
||||
self,
|
||||
prompt: LanguageModelInput,
|
||||
tools: list[dict] | None = None,
|
||||
|
||||
@@ -3,9 +3,12 @@ from collections.abc import Iterator
|
||||
from typing import Literal
|
||||
|
||||
from langchain.schema.language_model import LanguageModelInput
|
||||
from langchain_core.messages import AIMessageChunk
|
||||
from langchain_core.messages import BaseMessage
|
||||
from pydantic import BaseModel
|
||||
|
||||
from danswer.configs.app_configs import DISABLE_GENERATIVE_AI
|
||||
from danswer.configs.app_configs import LOG_DANSWER_MODEL_INTERACTIONS
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
|
||||
@@ -23,6 +26,32 @@ class LLMConfig(BaseModel):
|
||||
api_version: str | None
|
||||
|
||||
|
||||
def log_prompt(prompt: LanguageModelInput) -> None:
|
||||
if isinstance(prompt, list):
|
||||
for ind, msg in enumerate(prompt):
|
||||
if isinstance(msg, AIMessageChunk):
|
||||
if msg.content:
|
||||
log_msg = msg.content
|
||||
elif msg.tool_call_chunks:
|
||||
log_msg = "Tool Calls: " + str(
|
||||
[
|
||||
{
|
||||
key: value
|
||||
for key, value in tool_call.items()
|
||||
if key != "index"
|
||||
}
|
||||
for tool_call in msg.tool_call_chunks
|
||||
]
|
||||
)
|
||||
else:
|
||||
log_msg = ""
|
||||
logger.debug(f"Message {ind}:\n{log_msg}")
|
||||
else:
|
||||
logger.debug(f"Message {ind}:\n{msg.content}")
|
||||
if isinstance(prompt, str):
|
||||
logger.debug(f"Prompt:\n{prompt}")
|
||||
|
||||
|
||||
class LLM(abc.ABC):
|
||||
"""Mimics the LangChain LLM / BaseChatModel interfaces to make it easy
|
||||
to use these implementations to connect to a variety of LLM providers."""
|
||||
@@ -45,20 +74,48 @@ class LLM(abc.ABC):
|
||||
def log_model_configs(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def _precall(self, prompt: LanguageModelInput) -> None:
|
||||
if DISABLE_GENERATIVE_AI:
|
||||
raise Exception("Generative AI is disabled")
|
||||
if LOG_DANSWER_MODEL_INTERACTIONS:
|
||||
log_prompt(prompt)
|
||||
|
||||
def invoke(
|
||||
self,
|
||||
prompt: LanguageModelInput,
|
||||
tools: list[dict] | None = None,
|
||||
tool_choice: ToolChoiceOptions | None = None,
|
||||
) -> BaseMessage:
|
||||
self._precall(prompt)
|
||||
# TODO add a postcall to log model outputs independent of concrete class
|
||||
# implementation
|
||||
return self._invoke_implementation(prompt, tools, tool_choice)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _invoke_implementation(
|
||||
self,
|
||||
prompt: LanguageModelInput,
|
||||
tools: list[dict] | None = None,
|
||||
tool_choice: ToolChoiceOptions | None = None,
|
||||
) -> BaseMessage:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def stream(
|
||||
self,
|
||||
prompt: LanguageModelInput,
|
||||
tools: list[dict] | None = None,
|
||||
tool_choice: ToolChoiceOptions | None = None,
|
||||
) -> Iterator[BaseMessage]:
|
||||
self._precall(prompt)
|
||||
# TODO add a postcall to log model outputs independent of concrete class
|
||||
# implementation
|
||||
return self._stream_implementation(prompt, tools, tool_choice)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _stream_implementation(
|
||||
self,
|
||||
prompt: LanguageModelInput,
|
||||
tools: list[dict] | None = None,
|
||||
tool_choice: ToolChoiceOptions | None = None,
|
||||
) -> Iterator[BaseMessage]:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -26,6 +26,7 @@ OPENAI_PROVIDER_NAME = "openai"
|
||||
OPEN_AI_MODEL_NAMES = [
|
||||
"gpt-4",
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4-turbo",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-1106-preview",
|
||||
|
||||
@@ -189,7 +189,7 @@ def stream_answer_objects(
|
||||
chunks_below=query_req.chunks_below,
|
||||
full_doc=query_req.full_doc,
|
||||
bypass_acl=bypass_acl,
|
||||
evaluate_response=query_req.evaluate_response,
|
||||
llm_doc_eval=query_req.llm_doc_eval,
|
||||
)
|
||||
|
||||
answer_config = AnswerStyleConfig(
|
||||
|
||||
@@ -37,7 +37,7 @@ class DirectQARequest(ChunkContext):
|
||||
# This is to toggle agentic evaluation:
|
||||
# 1. Evaluates whether each response is relevant or not
|
||||
# 2. Provides a summary of the document's relevance in the resulsts
|
||||
evaluate_response: bool = False
|
||||
llm_doc_eval: bool = False
|
||||
# If True, skips generative an AI response to the search query
|
||||
skip_gen_ai_answer_generation: bool = False
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ SEARCH_RESPONSE_SUMMARY_ID = "search_response_summary"
|
||||
SEARCH_DOC_CONTENT_ID = "search_doc_content"
|
||||
SECTION_RELEVANCE_LIST_ID = "section_relevance_list"
|
||||
FINAL_CONTEXT_DOCUMENTS = "final_context_documents"
|
||||
SEARCH_EVALUATION_ID = "evaluate_response"
|
||||
SEARCH_EVALUATION_ID = "llm_doc_eval"
|
||||
|
||||
|
||||
class SearchResponseSummary(BaseModel):
|
||||
@@ -85,7 +85,7 @@ class SearchTool(Tool):
|
||||
chunks_below: int = 0,
|
||||
full_doc: bool = False,
|
||||
bypass_acl: bool = False,
|
||||
evaluate_response: bool = False,
|
||||
llm_doc_eval: bool = False,
|
||||
) -> None:
|
||||
self.user = user
|
||||
self.persona = persona
|
||||
@@ -102,7 +102,7 @@ class SearchTool(Tool):
|
||||
self.full_doc = full_doc
|
||||
self.bypass_acl = bypass_acl
|
||||
self.db_session = db_session
|
||||
self.evaluate_response = evaluate_response
|
||||
self.llm_doc_eval = llm_doc_eval
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -295,7 +295,7 @@ class SearchTool(Tool):
|
||||
|
||||
yield ToolResponse(id=FINAL_CONTEXT_DOCUMENTS, response=llm_docs)
|
||||
|
||||
if self.evaluate_response and not DISABLE_AGENTIC_SEARCH:
|
||||
if self.llm_doc_eval and not DISABLE_AGENTIC_SEARCH:
|
||||
yield ToolResponse(
|
||||
id=SEARCH_EVALUATION_ID, response=search_pipeline.relevance_summaries
|
||||
)
|
||||
|
||||
@@ -6,7 +6,13 @@ from danswer.llm.utils import get_default_llm_tokenizer
|
||||
from danswer.tools.tool import Tool
|
||||
|
||||
|
||||
OPEN_AI_TOOL_CALLING_MODELS = {"gpt-3.5-turbo", "gpt-4-turbo", "gpt-4"}
|
||||
OPEN_AI_TOOL_CALLING_MODELS = {
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4-turbo",
|
||||
"gpt-4",
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
}
|
||||
|
||||
|
||||
def explicit_tool_calling_supported(model_provider: str, model_name: str) -> bool:
|
||||
|
||||
@@ -33,6 +33,7 @@ def process_question(danswer_url: str, question: str, api_key: str | None) -> No
|
||||
"message": question,
|
||||
"chat_session_id": chat_session_id,
|
||||
"parent_message_id": None,
|
||||
"file_descriptors": [],
|
||||
# Default Question Answer prompt
|
||||
"prompt_id": 0,
|
||||
# Not specifying any specific docs to chat to, we want to run a search
|
||||
|
||||
@@ -2,6 +2,8 @@ from openai import OpenAI
|
||||
|
||||
|
||||
VALID_MODEL_LIST = [
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-vision-preview",
|
||||
"gpt-4",
|
||||
|
||||
@@ -70,7 +70,7 @@ Edit `search_test_config.yaml` to set:
|
||||
- model_server_port
|
||||
- This is the port of the remote model server
|
||||
- Only need to set this if use_cloud_gpu is true
|
||||
- existing_test_suffix
|
||||
- existing_test_suffix (THIS IS NOT A SUFFIX ANYMORE, TODO UPDATE THE DOCS HERE)
|
||||
- Use this if you would like to relaunch a previous test instance
|
||||
- Input the suffix of the test you'd like to re-launch
|
||||
- (E.g. to use the data from folder "test-1234-5678" put "-1234-5678")
|
||||
|
||||
@@ -97,7 +97,7 @@ def get_docker_container_env_vars(suffix: str) -> dict:
|
||||
|
||||
def manage_data_directories(suffix: str, base_path: str, use_cloud_gpu: bool) -> None:
|
||||
# Use the user's home directory as the base path
|
||||
target_path = os.path.join(os.path.expanduser(base_path), f"test{suffix}")
|
||||
target_path = os.path.join(os.path.expanduser(base_path), suffix)
|
||||
directories = {
|
||||
"DANSWER_POSTGRES_DATA_DIR": os.path.join(target_path, "postgres/"),
|
||||
"DANSWER_VESPA_DATA_DIR": os.path.join(target_path, "vespa/"),
|
||||
@@ -144,26 +144,30 @@ def _is_port_in_use(port: int) -> bool:
|
||||
|
||||
|
||||
def start_docker_compose(
|
||||
run_suffix: str, launch_web_ui: bool, use_cloud_gpu: bool
|
||||
run_suffix: str, launch_web_ui: bool, use_cloud_gpu: bool, only_state: bool = False
|
||||
) -> None:
|
||||
print("Starting Docker Compose...")
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
os.chdir("../../../../deployment/docker_compose/")
|
||||
command = f"docker compose -f docker-compose.search-testing.yml -p danswer-stack{run_suffix} up -d"
|
||||
command = f"docker compose -f docker-compose.search-testing.yml -p danswer-stack-{run_suffix} up -d"
|
||||
command += " --build"
|
||||
command += " --force-recreate"
|
||||
if use_cloud_gpu:
|
||||
command += " --scale indexing_model_server=0"
|
||||
command += " --scale inference_model_server=0"
|
||||
if launch_web_ui:
|
||||
web_ui_port = 3000
|
||||
while _is_port_in_use(web_ui_port):
|
||||
web_ui_port += 1
|
||||
print(f"UI will be launched at http://localhost:{web_ui_port}")
|
||||
os.environ["NGINX_PORT"] = str(web_ui_port)
|
||||
|
||||
if only_state:
|
||||
command += " index relational_db"
|
||||
else:
|
||||
command += " --scale web_server=0"
|
||||
command += " --scale nginx=0"
|
||||
if use_cloud_gpu:
|
||||
command += " --scale indexing_model_server=0"
|
||||
command += " --scale inference_model_server=0"
|
||||
if launch_web_ui:
|
||||
web_ui_port = 3000
|
||||
while _is_port_in_use(web_ui_port):
|
||||
web_ui_port += 1
|
||||
print(f"UI will be launched at http://localhost:{web_ui_port}")
|
||||
os.environ["NGINX_PORT"] = str(web_ui_port)
|
||||
else:
|
||||
command += " --scale web_server=0"
|
||||
command += " --scale nginx=0"
|
||||
|
||||
print("Docker Command:\n", command)
|
||||
|
||||
|
||||
@@ -39,9 +39,11 @@ def main() -> None:
|
||||
if config.commit_sha:
|
||||
switch_to_commit(config.commit_sha)
|
||||
|
||||
start_docker_compose(run_suffix, config.launch_web_ui, config.use_cloud_gpu)
|
||||
start_docker_compose(
|
||||
run_suffix, config.launch_web_ui, config.use_cloud_gpu, config.only_state
|
||||
)
|
||||
|
||||
if not config.existing_test_suffix:
|
||||
if not config.existing_test_suffix and not config.only_state:
|
||||
upload_test_files(config.zipped_documents_file, run_suffix)
|
||||
|
||||
run_qa_test_and_save_results(run_suffix)
|
||||
|
||||
@@ -46,19 +46,16 @@ def _get_test_output_folder(config: dict) -> str:
|
||||
base_output_folder = os.path.expanduser(config["output_folder"])
|
||||
if config["run_suffix"]:
|
||||
base_output_folder = os.path.join(
|
||||
base_output_folder, ("test" + config["run_suffix"]), "evaluations_output"
|
||||
base_output_folder, config["run_suffix"], "evaluations_output"
|
||||
)
|
||||
else:
|
||||
base_output_folder = os.path.join(base_output_folder, "no_defined_suffix")
|
||||
|
||||
counter = 1
|
||||
run_suffix = config["run_suffix"][1:]
|
||||
output_folder_path = os.path.join(base_output_folder, f"{run_suffix}_run_1")
|
||||
output_folder_path = os.path.join(base_output_folder, "run_1")
|
||||
while os.path.exists(output_folder_path):
|
||||
output_folder_path = os.path.join(
|
||||
output_folder_path.replace(
|
||||
f"{run_suffix}_run_{counter-1}", f"{run_suffix}_run_{counter}"
|
||||
),
|
||||
output_folder_path.replace(f"run_{counter-1}", f"run_{counter}"),
|
||||
)
|
||||
counter += 1
|
||||
|
||||
@@ -163,6 +160,9 @@ def _process_and_write_query_results(config: dict) -> None:
|
||||
if not result.get("answer"):
|
||||
invalid_answer_count += 1
|
||||
|
||||
if not result.get("context_data_list"):
|
||||
raise RuntimeError("Search failed, this is a critical failure!")
|
||||
|
||||
_update_metadata_file(test_output_folder, invalid_answer_count)
|
||||
|
||||
if invalid_answer_count:
|
||||
|
||||
@@ -19,6 +19,9 @@ clean_up_docker_containers: true
|
||||
# Whether to launch a web UI for the test
|
||||
launch_web_ui: false
|
||||
|
||||
# Whether to only run Vespa and Postgres
|
||||
only_state: false
|
||||
|
||||
# Whether to use a cloud GPU for processing
|
||||
use_cloud_gpu: false
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
environment:
|
||||
# Auth Settings
|
||||
- AUTH_TYPE=${AUTH_TYPE:-disabled}
|
||||
- SESSION_EXPIRE_TIME_SECONDS=${SESSION_EXPIRE_TIME_SECONDS:-86400}
|
||||
- SESSION_EXPIRE_TIME_SECONDS=${SESSION_EXPIRE_TIME_SECONDS:-}
|
||||
- ENCRYPTION_KEY_SECRET=${ENCRYPTION_KEY_SECRET:-}
|
||||
- VALID_EMAIL_DOMAINS=${VALID_EMAIL_DOMAINS:-}
|
||||
- GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-}
|
||||
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
environment:
|
||||
# Auth Settings
|
||||
- AUTH_TYPE=${AUTH_TYPE:-disabled}
|
||||
- SESSION_EXPIRE_TIME_SECONDS=${SESSION_EXPIRE_TIME_SECONDS:-86400}
|
||||
- SESSION_EXPIRE_TIME_SECONDS=${SESSION_EXPIRE_TIME_SECONDS:-}
|
||||
- ENCRYPTION_KEY_SECRET=${ENCRYPTION_KEY_SECRET:-}
|
||||
- VALID_EMAIL_DOMAINS=${VALID_EMAIL_DOMAINS:-}
|
||||
- GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-}
|
||||
|
||||
@@ -57,8 +57,8 @@ SECRET=
|
||||
#SAML_CONF_DIR=
|
||||
|
||||
|
||||
# How long before user needs to reauthenticate, default to 1 day. (cookie expiration time)
|
||||
SESSION_EXPIRE_TIME_SECONDS=86400
|
||||
# How long before user needs to reauthenticate, default to 7 days. (cookie expiration time)
|
||||
SESSION_EXPIRE_TIME_SECONDS=604800
|
||||
|
||||
|
||||
# Use the below to specify a list of allowed user domains, only checked if user Auth is turned on
|
||||
|
||||
3
examples/widget/.eslintrc.json
Normal file
3
examples/widget/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
36
examples/widget/.gitignore
vendored
Normal file
36
examples/widget/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
19
examples/widget/README.md
Normal file
19
examples/widget/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
This is a code example for how you can use Danswer's APIs to build a chat bot widget for a website! The main code to look at can be found in `src/app/widget/Widget.tsx`.
|
||||
|
||||
If you want to get fancier, then take a peek at the Chat implementation within Danswer itself [here](https://github.com/danswer-ai/danswer/blob/main/web/src/app/chat/ChatPage.tsx#L82).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the requirements:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
Then run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
4
examples/widget/next.config.mjs
Normal file
4
examples/widget/next.config.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
5933
examples/widget/package-lock.json
generated
Normal file
5933
examples/widget/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
examples/widget/package.json
Normal file
28
examples/widget/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "widget",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.2.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-markdown": "^8.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
8
examples/widget/postcss.config.mjs
Normal file
8
examples/widget/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
3
examples/widget/src/app/globals.css
Normal file
3
examples/widget/src/app/globals.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
23
examples/widget/src/app/layout.tsx
Normal file
23
examples/widget/src/app/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Example Danswer Widget",
|
||||
description: "Example Danswer Widget",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
9
examples/widget/src/app/page.tsx
Normal file
9
examples/widget/src/app/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ChatWidget } from "./widget/Widget";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<ChatWidget />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
344
examples/widget/src/app/widget/Widget.tsx
Normal file
344
examples/widget/src/app/widget/Widget.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
|
||||
const API_KEY = process.env.NEXT_PUBLIC_API_KEY || "";
|
||||
|
||||
type NonEmptyObject = { [k: string]: any };
|
||||
|
||||
const processSingleChunk = <T extends NonEmptyObject>(
|
||||
chunk: string,
|
||||
currPartialChunk: string | null,
|
||||
): [T | null, string | null] => {
|
||||
const completeChunk = (currPartialChunk || "") + chunk;
|
||||
try {
|
||||
// every complete chunk should be valid JSON
|
||||
const chunkJson = JSON.parse(completeChunk);
|
||||
return [chunkJson, null];
|
||||
} catch (err) {
|
||||
// if it's not valid JSON, then it's probably an incomplete chunk
|
||||
return [null, completeChunk];
|
||||
}
|
||||
};
|
||||
|
||||
const processRawChunkString = <T extends NonEmptyObject>(
|
||||
rawChunkString: string,
|
||||
previousPartialChunk: string | null,
|
||||
): [T[], string | null] => {
|
||||
/* This is required because, in practice, we see that nginx does not send over
|
||||
each chunk one at a time even with buffering turned off. Instead,
|
||||
chunks are sometimes in batches or are sometimes incomplete */
|
||||
if (!rawChunkString) {
|
||||
return [[], null];
|
||||
}
|
||||
const chunkSections = rawChunkString
|
||||
.split("\n")
|
||||
.filter((chunk) => chunk.length > 0);
|
||||
let parsedChunkSections: T[] = [];
|
||||
let currPartialChunk = previousPartialChunk;
|
||||
chunkSections.forEach((chunk) => {
|
||||
const [processedChunk, partialChunk] = processSingleChunk<T>(
|
||||
chunk,
|
||||
currPartialChunk,
|
||||
);
|
||||
if (processedChunk) {
|
||||
parsedChunkSections.push(processedChunk);
|
||||
currPartialChunk = null;
|
||||
} else {
|
||||
currPartialChunk = partialChunk;
|
||||
}
|
||||
});
|
||||
|
||||
return [parsedChunkSections, currPartialChunk];
|
||||
};
|
||||
|
||||
async function* handleStream<T extends NonEmptyObject>(
|
||||
streamingResponse: Response,
|
||||
): AsyncGenerator<T[], void, unknown> {
|
||||
const reader = streamingResponse.body?.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
let previousPartialChunk: string | null = null;
|
||||
while (true) {
|
||||
const rawChunk = await reader?.read();
|
||||
if (!rawChunk) {
|
||||
throw new Error("Unable to process chunk");
|
||||
}
|
||||
const { done, value } = rawChunk;
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const [completedChunks, partialChunk] = processRawChunkString<T>(
|
||||
decoder.decode(value, { stream: true }),
|
||||
previousPartialChunk,
|
||||
);
|
||||
if (!completedChunks.length && !partialChunk) {
|
||||
break;
|
||||
}
|
||||
previousPartialChunk = partialChunk as string | null;
|
||||
|
||||
yield await Promise.resolve(completedChunks);
|
||||
}
|
||||
}
|
||||
|
||||
async function* sendMessage({
|
||||
message,
|
||||
chatSessionId,
|
||||
parentMessageId,
|
||||
}: {
|
||||
message: string;
|
||||
chatSessionId?: number;
|
||||
parentMessageId?: number;
|
||||
}) {
|
||||
if (!chatSessionId || !parentMessageId) {
|
||||
// Create a new chat session if one doesn't exist
|
||||
const createSessionResponse = await fetch(
|
||||
`${API_URL}/chat/create-chat-session`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// or specify an assistant you have defined
|
||||
persona_id: 0,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!createSessionResponse.ok) {
|
||||
const errorJson = await createSessionResponse.json();
|
||||
const errorMsg = errorJson.message || errorJson.detail || "";
|
||||
throw Error(`Failed to create chat session - ${errorMsg}`);
|
||||
}
|
||||
|
||||
const sessionData = await createSessionResponse.json();
|
||||
chatSessionId = sessionData.chat_session_id;
|
||||
}
|
||||
|
||||
const sendMessageResponse = await fetch(`${API_URL}/chat/send-message`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_session_id: chatSessionId,
|
||||
parent_message_id: parentMessageId,
|
||||
message: message,
|
||||
prompt_id: null,
|
||||
search_doc_ids: null,
|
||||
file_descriptors: [],
|
||||
// checkout https://github.com/danswer-ai/danswer/blob/main/backend/danswer/search/models.py#L105 for
|
||||
// all available options
|
||||
retrieval_options: {
|
||||
run_search: "always",
|
||||
filters: null,
|
||||
},
|
||||
query_override: null,
|
||||
}),
|
||||
});
|
||||
if (!sendMessageResponse.ok) {
|
||||
const errorJson = await sendMessageResponse.json();
|
||||
const errorMsg = errorJson.message || errorJson.detail || "";
|
||||
throw Error(`Failed to send message - ${errorMsg}`);
|
||||
}
|
||||
|
||||
yield* handleStream<NonEmptyObject>(sendMessageResponse);
|
||||
}
|
||||
|
||||
export const ChatWidget = () => {
|
||||
const [messages, setMessages] = useState<{ text: string; isUser: boolean }[]>(
|
||||
[],
|
||||
);
|
||||
const [inputText, setInputText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (inputText.trim()) {
|
||||
const initialPrevMessages = messages;
|
||||
setMessages([...initialPrevMessages, { text: inputText, isUser: true }]);
|
||||
setInputText("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const messageGenerator = sendMessage({
|
||||
message: inputText,
|
||||
chatSessionId: undefined,
|
||||
parentMessageId: undefined,
|
||||
});
|
||||
let fullResponse = "";
|
||||
|
||||
for await (const chunks of messageGenerator) {
|
||||
for (const chunk of chunks) {
|
||||
if ("answer_piece" in chunk) {
|
||||
fullResponse += chunk.answer_piece;
|
||||
setMessages([
|
||||
...initialPrevMessages,
|
||||
{ text: inputText, isUser: true },
|
||||
{ text: fullResponse, isUser: false },
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending message:", error);
|
||||
setMessages((prevMessages) => [
|
||||
...prevMessages,
|
||||
{ text: "An error occurred. Please try again.", isUser: false },
|
||||
]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
fixed
|
||||
bottom-4
|
||||
right-4
|
||||
z-50
|
||||
bg-white
|
||||
rounded-lg
|
||||
shadow-xl
|
||||
w-96
|
||||
h-[32rem]
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
transition-all
|
||||
duration-300
|
||||
ease-in-out
|
||||
"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
bg-gradient-to-r
|
||||
from-blue-600
|
||||
to-blue-800
|
||||
text-white
|
||||
p-4
|
||||
font-bold
|
||||
flex
|
||||
justify-between
|
||||
items-center
|
||||
"
|
||||
>
|
||||
<span>Chat Support</span>
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
flex-grow
|
||||
overflow-y-auto
|
||||
p-4
|
||||
space-y-4
|
||||
bg-gray-50
|
||||
border-b
|
||||
border-gray-200
|
||||
"
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`
|
||||
flex
|
||||
${message.isUser ? "justify-end" : "justify-start"}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
max-w-[75%]
|
||||
p-3
|
||||
rounded-lg
|
||||
${
|
||||
message.isUser
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white text-black"
|
||||
}
|
||||
shadow
|
||||
`}
|
||||
>
|
||||
<ReactMarkdown>{message.text}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex justify-center">
|
||||
<div className="animate-pulse flex space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="
|
||||
p-4
|
||||
bg-white
|
||||
border-t
|
||||
border-gray-200
|
||||
"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
placeholder="Type a message..."
|
||||
className="
|
||||
w-full
|
||||
p-2
|
||||
pr-10
|
||||
border
|
||||
border-gray-300
|
||||
rounded-full
|
||||
focus:outline-none
|
||||
focus:ring-2
|
||||
focus:ring-blue-500
|
||||
focus:border-transparent
|
||||
"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="
|
||||
absolute
|
||||
right-2
|
||||
top-1/2
|
||||
transform
|
||||
-translate-y-1/2
|
||||
text-blue-500
|
||||
hover:text-blue-600
|
||||
focus:outline-none
|
||||
"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
20
examples/widget/tailwind.config.ts
Normal file
20
examples/widget/tailwind.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
26
examples/widget/tsconfig.json
Normal file
26
examples/widget/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { User } from "@/lib/types";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import Cookies from "js-cookie";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/contants";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { useSidebarVisibility } from "@/components/chat_search/hooks";
|
||||
import FunctionalHeader from "@/components/chat_search/Header";
|
||||
@@ -143,6 +143,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
<div className="mt-4 mx-auto">{content(contentProps)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<FixedLogo />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,9 +65,6 @@ export default async function GalleryPage({
|
||||
user={user}
|
||||
assistants={assistants}
|
||||
/>
|
||||
|
||||
{/* Temporary - fixed logo */}
|
||||
<FixedLogo />
|
||||
</ChatProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -67,9 +67,6 @@ export default async function GalleryPage({
|
||||
user={user}
|
||||
assistants={assistants}
|
||||
/>
|
||||
|
||||
{/* Temporary - fixed logo */}
|
||||
<FixedLogo />
|
||||
</ChatProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ import { ChatBanner } from "./ChatBanner";
|
||||
|
||||
import FunctionalHeader from "@/components/chat_search/Header";
|
||||
import { useSidebarVisibility } from "@/components/chat_search/hooks";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/contants";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
|
||||
import FixedLogo from "./shared_chat_search/FixedLogo";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
|
||||
@@ -57,6 +57,8 @@ import {
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { Tooltip } from "@/components/tooltip/Tooltip";
|
||||
import { useMouseTracking } from "./hooks";
|
||||
import { InternetSearchIcon } from "@/components/InternetSearchIcon";
|
||||
import { getTitleFromDocument } from "@/lib/sources";
|
||||
|
||||
const TOOLS_WITH_CUSTOM_HANDLING = [
|
||||
SEARCH_TOOL_NAME,
|
||||
@@ -443,17 +445,17 @@ export const AIMessage = ({
|
||||
>
|
||||
<Citation link={doc.link} index={ind + 1} />
|
||||
<p className="shrink truncate ellipsis break-all ">
|
||||
{
|
||||
doc.document_id.split("/")[
|
||||
doc.document_id.split("/").length - 1
|
||||
]
|
||||
}
|
||||
{getTitleFromDocument(doc)}
|
||||
</p>
|
||||
<div className="ml-auto flex-none">
|
||||
<SourceIcon
|
||||
sourceType={doc.source_type}
|
||||
iconSize={18}
|
||||
/>
|
||||
{doc.is_internet ? (
|
||||
<InternetSearchIcon url={doc.link} />
|
||||
) : (
|
||||
<SourceIcon
|
||||
sourceType={doc.source_type}
|
||||
iconSize={18}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
<div className="flex overscroll-x-scroll mt-.5">
|
||||
|
||||
@@ -54,11 +54,13 @@ export function PagesTab({
|
||||
}
|
||||
};
|
||||
|
||||
const isHistoryEmpty = !existingChats || existingChats.length === 0;
|
||||
|
||||
return (
|
||||
<div className="mb-1 ml-3 relative miniscroll overflow-y-auto h-full">
|
||||
{folders && folders.length > 0 && (
|
||||
<div className="py-2 border-b border-border">
|
||||
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-2 font-medium">
|
||||
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-2 font-bold">
|
||||
Folders
|
||||
</div>
|
||||
<FolderList
|
||||
@@ -80,43 +82,51 @@ export function PagesTab({
|
||||
} rounded-md`}
|
||||
>
|
||||
{(page == "chat" || page == "search") && (
|
||||
<p className="my-2 text-xs text-subtle flex font-semibold">
|
||||
<p className="my-2 text-xs text-subtle flex font-bold">
|
||||
{page == "chat" && "Chat "}
|
||||
{page == "search" && "Search "}
|
||||
History
|
||||
</p>
|
||||
)}
|
||||
{Object.entries(groupedChatSessions).map(
|
||||
([dateRange, chatSessions], ind) => {
|
||||
if (chatSessions.length > 0) {
|
||||
return (
|
||||
<div key={dateRange}>
|
||||
<div
|
||||
className={`text-xs text-subtle ${
|
||||
ind != 0 && "mt-5"
|
||||
} flex pb-0.5 mb-1.5 font-medium`}
|
||||
>
|
||||
{dateRange}
|
||||
{isHistoryEmpty ? (
|
||||
<p className="text-sm text-subtle mt-2 w-[250px]">
|
||||
{page === "search"
|
||||
? "Try running a search! Your search history will appear here."
|
||||
: "Try sending a message! Your chat history will appear here."}
|
||||
</p>
|
||||
) : (
|
||||
Object.entries(groupedChatSessions).map(
|
||||
([dateRange, chatSessions], ind) => {
|
||||
if (chatSessions.length > 0) {
|
||||
return (
|
||||
<div key={dateRange}>
|
||||
<div
|
||||
className={`text-xs text-subtle ${
|
||||
ind != 0 && "mt-5"
|
||||
} flex pb-0.5 mb-1.5 font-medium`}
|
||||
>
|
||||
{dateRange}
|
||||
</div>
|
||||
{chatSessions
|
||||
.filter((chat) => chat.folder_id === null)
|
||||
.map((chat) => {
|
||||
const isSelected = currentChatId === chat.id;
|
||||
return (
|
||||
<div key={`${chat.id}-${chat.name}`}>
|
||||
<ChatSessionDisplay
|
||||
search={page == "search"}
|
||||
chatSession={chat}
|
||||
isSelected={isSelected}
|
||||
skipGradient={isDragOver}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{chatSessions
|
||||
.filter((chat) => chat.folder_id === null)
|
||||
.map((chat) => {
|
||||
const isSelected = currentChatId === chat.id;
|
||||
return (
|
||||
<div key={`${chat.id}-${chat.name}`}>
|
||||
<ChatSessionDisplay
|
||||
search={page == "search"}
|
||||
chatSession={chat}
|
||||
isSelected={isSelected}
|
||||
skipGradient={isDragOver}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { HeaderTitle } from "@/components/header/Header";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from "@/lib/assistants/fetchAssistantsSS";
|
||||
import FunctionalWrapper from "../chat/shared_chat_search/FunctionalWrapper";
|
||||
import { ChatSession } from "../chat/interfaces";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/contants";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
|
||||
import ToggleSearch from "./WrappedSearch";
|
||||
import {
|
||||
AGENTIC_SEARCH_TYPE_COOKIE_NAME,
|
||||
|
||||
@@ -329,17 +329,10 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="pb-8 relative h-full overflow-y-auto w-full">
|
||||
<div className="fixed bg-background left-0 border-b gap-x-4 mb-8 px-4 py-2 w-full items-center flex justify-end">
|
||||
<a
|
||||
href="/chat"
|
||||
className="transition-all duration-150 cursor-pointer p-1 text-sm items-center flex gap-x-1 px-2 py-1 rounded-lg hover:shadow-sm hover:ring-1 hover:ring-ingio-900/40 hover:bg-opacity-90 text-neutral-100 bg-accent"
|
||||
>
|
||||
<BackIcon size={20} className="text-neutral" />
|
||||
Back to Danswer
|
||||
</a>
|
||||
<div className="fixed bg-background left-0 gap-x-4 mb-8 px-4 py-2 w-full items-center flex justify-end">
|
||||
<UserDropdown user={user} />
|
||||
</div>
|
||||
<div className="pt-20 flex overflow-y-auto h-full px-4 md:px-12">
|
||||
<div className="pt-12 flex overflow-y-auto h-full px-4 md:px-12">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Logo } from "@/components/Logo";
|
||||
import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constants";
|
||||
import { HeaderTitle } from "@/components/header/Header";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { BackIcon } from "@/components/icons/icons";
|
||||
|
||||
interface Item {
|
||||
name: string | JSX.Element;
|
||||
@@ -62,6 +63,12 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Link href={"/chat"}>
|
||||
<button className="text-sm block w-48 py-2.5 flex px-2 text-left bg-background-200 hover:bg-background-200/80 cursor-pointer rounded">
|
||||
<BackIcon size={20} className="text-neutral" />
|
||||
<p className="ml-1">Back to Danswer</p>
|
||||
</button>
|
||||
</Link>
|
||||
{collections.map((collection, collectionInd) => (
|
||||
<div key={collectionInd}>
|
||||
<h2 className="text-xs text-strong font-bold pb-2">
|
||||
@@ -70,7 +77,7 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
|
||||
{collection.items.map((item) => (
|
||||
<Link key={item.link} href={item.link}>
|
||||
<button className="text-sm block w-48 py-2.5 px-2 text-left hover:bg-hover rounded">
|
||||
<div className="">{item.name}</div>
|
||||
{item.name}
|
||||
</button>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Cookies from "js-cookie";
|
||||
import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "./contants";
|
||||
import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "./constants";
|
||||
|
||||
function applyMinAndMax(
|
||||
width: number,
|
||||
|
||||
@@ -191,6 +191,12 @@ export const FullSearchBar = ({
|
||||
|
||||
<div className="flex justify-end w-full items-center space-x-3 mr-12 px-4 pb-2">
|
||||
{searchState == "searching" && (
|
||||
<div key={"Reading"} className="mr-auto relative inline-block">
|
||||
<span className="loading-text">Searching...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "reading" && (
|
||||
<div key={"Reading"} className="mr-auto relative inline-block">
|
||||
<span className="loading-text">Reading Documents...</span>
|
||||
</div>
|
||||
|
||||
@@ -29,12 +29,17 @@ import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
|
||||
import { ChatSession, SearchSession } from "@/app/chat/interfaces";
|
||||
import FunctionalHeader from "../chat_search/Header";
|
||||
import { useSidebarVisibility } from "../chat_search/hooks";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "../resizable/contants";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "../resizable/constants";
|
||||
import { AGENTIC_SEARCH_TYPE_COOKIE_NAME } from "@/lib/constants";
|
||||
import Cookies from "js-cookie";
|
||||
import FixedLogo from "@/app/chat/shared_chat_search/FixedLogo";
|
||||
|
||||
export type searchState = "input" | "searching" | "analyzing" | "summarizing";
|
||||
export type searchState =
|
||||
| "input"
|
||||
| "searching"
|
||||
| "reading"
|
||||
| "analyzing"
|
||||
| "summarizing";
|
||||
|
||||
const SEARCH_DEFAULT_OVERRIDES_START: SearchDefaultOverrides = {
|
||||
forceDisplayQA: false,
|
||||
@@ -231,10 +236,23 @@ export const SearchSection = ({
|
||||
|
||||
const updateDocs = (documents: SearchDanswerDocument[]) => {
|
||||
setTimeout(() => {
|
||||
if (searchState != "input") {
|
||||
setSearchState("analyzing");
|
||||
}
|
||||
setSearchState((searchState) => {
|
||||
if (searchState != "input") {
|
||||
return "reading";
|
||||
}
|
||||
return "input";
|
||||
});
|
||||
}, 1500);
|
||||
|
||||
setTimeout(() => {
|
||||
setSearchState((searchState) => {
|
||||
if (searchState != "input") {
|
||||
return "analyzing";
|
||||
}
|
||||
return "input";
|
||||
});
|
||||
}, 4500);
|
||||
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
documents,
|
||||
|
||||
@@ -23,7 +23,7 @@ import { cookies } from "next/headers";
|
||||
import {
|
||||
SIDEBAR_TOGGLED_COOKIE_NAME,
|
||||
DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME,
|
||||
} from "@/components/resizable/contants";
|
||||
} from "@/components/resizable/constants";
|
||||
import { hasCompletedWelcomeFlowSS } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import { fetchAssistantsSS } from "../assistants/fetchAssistantsSS";
|
||||
|
||||
|
||||
@@ -36,10 +36,12 @@ export function getFinalLLM(
|
||||
|
||||
const MODELS_SUPPORTING_IMAGES = [
|
||||
["openai", "gpt-4o"],
|
||||
["openai", "gpt-4o-mini"],
|
||||
["openai", "gpt-4-vision-preview"],
|
||||
["openai", "gpt-4-turbo"],
|
||||
["openai", "gpt-4-1106-vision-preview"],
|
||||
["azure", "gpt-4o"],
|
||||
["azure", "gpt-4o-mini"],
|
||||
["azure", "gpt-4-vision-preview"],
|
||||
["azure", "gpt-4-turbo"],
|
||||
["azure", "gpt-4-1106-vision-preview"],
|
||||
|
||||
@@ -61,7 +61,7 @@ export const searchRequestStreamed = async ({
|
||||
filters: filters,
|
||||
enable_auto_detect_filters: false,
|
||||
},
|
||||
evaluate_response: true,
|
||||
llm_doc_eval: true,
|
||||
skip_gen_ai_answer_generation: true,
|
||||
}),
|
||||
headers: {
|
||||
|
||||
@@ -38,7 +38,11 @@ import {
|
||||
ColorSlackIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { ValidSources } from "./types";
|
||||
import { SourceCategory, SourceMetadata } from "./search/interfaces";
|
||||
import {
|
||||
DanswerDocument,
|
||||
SourceCategory,
|
||||
SourceMetadata,
|
||||
} from "./search/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { FaAccessibleIcon, FaSlack } from "react-icons/fa";
|
||||
|
||||
@@ -290,3 +294,16 @@ export function getSourcesForPersona(persona: Persona): ValidSources[] {
|
||||
});
|
||||
return personaSources;
|
||||
}
|
||||
|
||||
function stripTrailingSlash(str: string) {
|
||||
if (str.substr(-1) === "/") {
|
||||
return str.substr(0, str.length - 1);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export const getTitleFromDocument = (document: DanswerDocument) => {
|
||||
return stripTrailingSlash(document.document_id).split("/")[
|
||||
stripTrailingSlash(document.document_id).split("/").length - 1
|
||||
];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user