diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 019ea94b8362..6f9ecdbfced8 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -1,5 +1,5 @@ from sqlalchemy.engine.base import Connection -from typing import Any +from typing import Literal import asyncio from logging.config import fileConfig import logging @@ -8,6 +8,7 @@ from alembic import context from sqlalchemy import pool from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.sql import text +from sqlalchemy.sql.schema import SchemaItem from shared_configs.configs import MULTI_TENANT from danswer.db.engine import build_connection_string @@ -35,7 +36,18 @@ logger = logging.getLogger(__name__) def include_object( - object: Any, name: str, type_: str, reflected: bool, compare_to: Any + object: SchemaItem, + name: str | None, + type_: Literal[ + "schema", + "table", + "column", + "index", + "unique_constraint", + "foreign_key_constraint", + ], + reflected: bool, + compare_to: SchemaItem | None, ) -> bool: """ Determines whether a database object should be included in migrations. diff --git a/backend/alembic_tenants/env.py b/backend/alembic_tenants/env.py index f0f1178ce09e..506dbda03131 100644 --- a/backend/alembic_tenants/env.py +++ b/backend/alembic_tenants/env.py @@ -1,5 +1,6 @@ import asyncio from logging.config import fileConfig +from typing import Literal from sqlalchemy import pool from sqlalchemy.engine import Connection @@ -37,8 +38,15 @@ EXCLUDE_TABLES = {"kombu_queue", "kombu_message"} def include_object( object: SchemaItem, - name: str, - type_: str, + name: str | None, + type_: Literal[ + "schema", + "table", + "column", + "index", + "unique_constraint", + "foreign_key_constraint", + ], reflected: bool, compare_to: SchemaItem | None, ) -> bool: diff --git a/backend/danswer/chat/chat_utils.py b/backend/danswer/chat/chat_utils.py index 5e42ae23f5a9..9f4764381236 100644 --- a/backend/danswer/chat/chat_utils.py +++ b/backend/danswer/chat/chat_utils.py @@ -31,6 +31,7 @@ def llm_doc_from_inference_section(inference_section: InferenceSection) -> LlmDo if inference_section.center_chunk.source_links else None, source_links=inference_section.center_chunk.source_links, + match_highlights=inference_section.center_chunk.match_highlights, ) diff --git a/backend/danswer/chat/models.py b/backend/danswer/chat/models.py index 3852029c47b4..f939784d71af 100644 --- a/backend/danswer/chat/models.py +++ b/backend/danswer/chat/models.py @@ -25,6 +25,7 @@ class LlmDoc(BaseModel): updated_at: datetime | None link: str | None source_links: dict[int, str] | None + match_highlights: list[str] | None # First chunk of info for streaming QA diff --git a/backend/danswer/tools/tool_implementations/internet_search/internet_search_tool.py b/backend/danswer/tools/tool_implementations/internet_search/internet_search_tool.py index fd59b08abe17..85d93d833833 100644 --- a/backend/danswer/tools/tool_implementations/internet_search/internet_search_tool.py +++ b/backend/danswer/tools/tool_implementations/internet_search/internet_search_tool.py @@ -77,6 +77,7 @@ def llm_doc_from_internet_search_result(result: InternetSearchResult) -> LlmDoc: updated_at=datetime.now(), link=result.link, source_links={0: result.link}, + match_highlights=[], ) diff --git a/backend/tests/integration/tests/personas/test_persona_categories.py b/backend/tests/integration/tests/personas/test_persona_categories.py index fdd0e6458147..1ac2d3b3000a 100644 --- a/backend/tests/integration/tests/personas/test_persona_categories.py +++ b/backend/tests/integration/tests/personas/test_persona_categories.py @@ -44,6 +44,7 @@ def test_persona_category_management(reset: None) -> None: category=updated_persona_category, user_performing_action=regular_user, ) + assert exc_info.value.response is not None assert exc_info.value.response.status_code == 403 assert PersonaCategoryManager.verify( diff --git a/backend/tests/unit/danswer/llm/answering/conftest.py b/backend/tests/unit/danswer/llm/answering/conftest.py index a0077b53917a..46dfc3523714 100644 --- a/backend/tests/unit/danswer/llm/answering/conftest.py +++ b/backend/tests/unit/danswer/llm/answering/conftest.py @@ -64,6 +64,7 @@ def mock_search_results() -> list[LlmDoc]: updated_at=datetime(2023, 1, 1), link="https://example.com/doc1", source_links={0: "https://example.com/doc1"}, + match_highlights=[], ), LlmDoc( content="Search result 2", @@ -75,6 +76,7 @@ def mock_search_results() -> list[LlmDoc]: updated_at=datetime(2023, 1, 2), link="https://example.com/doc2", source_links={0: "https://example.com/doc2"}, + match_highlights=[], ), ] diff --git a/backend/tests/unit/danswer/llm/answering/stream_processing/test_citation_processing.py b/backend/tests/unit/danswer/llm/answering/stream_processing/test_citation_processing.py index 386f1d25e1e3..335c2e7b0842 100644 --- a/backend/tests/unit/danswer/llm/answering/stream_processing/test_citation_processing.py +++ b/backend/tests/unit/danswer/llm/answering/stream_processing/test_citation_processing.py @@ -46,6 +46,7 @@ mock_docs = [ updated_at=datetime.now(), link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None, source_links={0: "https://mintlify.com/docs/settings/broken-links"}, + match_highlights=[], ) for id in range(10) ] diff --git a/backend/tests/unit/danswer/llm/answering/stream_processing/test_quote_processing.py b/backend/tests/unit/danswer/llm/answering/stream_processing/test_quote_processing.py index 390d838043a4..163dc5b5e71c 100644 --- a/backend/tests/unit/danswer/llm/answering/stream_processing/test_quote_processing.py +++ b/backend/tests/unit/danswer/llm/answering/stream_processing/test_quote_processing.py @@ -20,6 +20,7 @@ mock_docs = [ updated_at=datetime.now(), link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None, source_links={0: "https://mintlify.com/docs/settings/broken-links"}, + match_highlights=[], ) for id in range(10) ] diff --git a/web/src/app/chat/input/ChatInputBar.tsx b/web/src/app/chat/input/ChatInputBar.tsx index 5d786a1784a2..3e41f4f32033 100644 --- a/web/src/app/chat/input/ChatInputBar.tsx +++ b/web/src/app/chat/input/ChatInputBar.tsx @@ -18,7 +18,7 @@ import { SendIcon, StopGeneratingIcon, } from "@/components/icons/icons"; -import { DanswerDocument } from "@/lib/search/interfaces"; +import { DanswerDocument, SourceMetadata } from "@/lib/search/interfaces"; import { AssistantIcon } from "@/components/assistants/AssistantIcon"; import { Tooltip, @@ -37,9 +37,41 @@ import { AssistantsTab } from "../modal/configuration/AssistantsTab"; import { IconType } from "react-icons"; import { LlmTab } from "../modal/configuration/LlmTab"; import { XIcon } from "lucide-react"; +import { FilterPills } from "./FilterPills"; +import { Tag } from "@/lib/types"; +import FiltersDisplay from "./FilterDisplay"; const MAX_INPUT_HEIGHT = 200; +interface ChatInputBarProps { + removeFilters: () => void; + removeDocs: () => void; + openModelSettings: () => void; + showDocs: () => void; + showConfigureAPIKey: () => void; + selectedDocuments: DanswerDocument[]; + message: string; + setMessage: (message: string) => void; + stopGenerating: () => void; + onSubmit: () => void; + filterManager: FilterManager; + llmOverrideManager: LlmOverrideManager; + chatState: ChatState; + alternativeAssistant: Persona | null; + inputPrompts: InputPrompt[]; + // assistants + selectedAssistant: Persona; + setSelectedAssistant: (assistant: Persona) => void; + setAlternativeAssistant: (alternativeAssistant: Persona | null) => void; + + files: FileDescriptor[]; + setFiles: (files: FileDescriptor[]) => void; + handleFileUpload: (files: File[]) => void; + textAreaRef: React.RefObject; + chatSessionId?: string; + toggleFilters?: () => void; +} + export function ChatInputBar({ removeFilters, removeDocs, @@ -68,32 +100,7 @@ export function ChatInputBar({ chatSessionId, inputPrompts, toggleFilters, -}: { - removeFilters: () => void; - removeDocs: () => void; - showConfigureAPIKey: () => void; - openModelSettings: () => void; - chatState: ChatState; - stopGenerating: () => void; - showDocs: () => void; - selectedDocuments: DanswerDocument[]; - setAlternativeAssistant: (alternativeAssistant: Persona | null) => void; - setSelectedAssistant: (assistant: Persona) => void; - inputPrompts: InputPrompt[]; - message: string; - setMessage: (message: string) => void; - onSubmit: () => void; - filterManager: FilterManager; - llmOverrideManager: LlmOverrideManager; - selectedAssistant: Persona; - alternativeAssistant: Persona | null; - files: FileDescriptor[]; - setFiles: (files: FileDescriptor[]) => void; - handleFileUpload: (files: File[]) => void; - textAreaRef: React.RefObject; - chatSessionId?: string; - toggleFilters?: () => void; -}) { +}: ChatInputBarProps) { useEffect(() => { const textarea = textAreaRef.current; if (textarea) { @@ -340,23 +347,26 @@ export function ChatInputBar({ className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full" >
- {filteredPrompts.map((currentPrompt, index) => ( - - ))} + {filteredPrompts.map( + (currentPrompt: InputPrompt, index: number) => ( + + ) + )}
)} + {(selectedDocuments.length > 0 || files.length > 0) && (
@@ -564,6 +575,16 @@ export function ChatInputBar({ onClick={toggleFilters} /> )} + {(filterManager.selectedSources.length > 0 || + filterManager.selectedDocumentSets.length > 0 || + filterManager.selectedTags.length > 0 || + filterManager.timeRange) && + toggleFilters && ( + + )}
diff --git a/web/src/app/chat/input/FilterDisplay.tsx b/web/src/app/chat/input/FilterDisplay.tsx new file mode 100644 index 000000000000..cc20266f9bce --- /dev/null +++ b/web/src/app/chat/input/FilterDisplay.tsx @@ -0,0 +1,109 @@ +import React from "react"; +import { XIcon } from "lucide-react"; + +import { FilterPills } from "./FilterPills"; +import { SourceMetadata } from "@/lib/search/interfaces"; +import { FilterManager } from "@/lib/hooks"; +import { Tag } from "@/lib/types"; + +interface FiltersDisplayProps { + filterManager: FilterManager; + toggleFilters: () => void; +} +export default function FiltersDisplay({ + filterManager, + toggleFilters, +}: FiltersDisplayProps) { + return ( +
+ {(() => { + const allFilters = [ + ...filterManager.selectedSources, + ...filterManager.selectedDocumentSets, + ...filterManager.selectedTags, + ...(filterManager.timeRange ? [filterManager.timeRange] : []), + ]; + const filtersToShow = allFilters.slice(0, 2); + const remainingFilters = allFilters.length - 2; + + return ( + <> + {filtersToShow.map((filter, index) => { + if (typeof filter === "object" && "displayName" in filter) { + return ( + + key={index} + item={filter} + itemToString={(source) => source.displayName} + onRemove={(source) => + filterManager.setSelectedSources((prev) => + prev.filter( + (s) => s.internalName !== source.internalName + ) + ) + } + toggleFilters={toggleFilters} + /> + ); + } else if (typeof filter === "string") { + return ( + + key={index} + item={filter} + itemToString={(set) => set} + onRemove={(set) => + filterManager.setSelectedDocumentSets((prev) => + prev.filter((s) => s !== set) + ) + } + toggleFilters={toggleFilters} + /> + ); + } else if ("tag_key" in filter) { + return ( + + key={index} + item={filter} + itemToString={(tag) => `${tag.tag_key}:${tag.tag_value}`} + onRemove={(tag) => + filterManager.setSelectedTags((prev) => + prev.filter( + (t) => + t.tag_key !== tag.tag_key || + t.tag_value !== tag.tag_value + ) + ) + } + toggleFilters={toggleFilters} + /> + ); + } else if ("from" in filter && "to" in filter) { + return ( +
+ + {filter.from.toLocaleDateString()} -{" "} + {filter.to.toLocaleDateString()} + + filterManager.setTimeRange(null)} + size={16} + className="ml-2 text-text-400 hover:text-text-600 cursor-pointer" + /> +
+ ); + } + })} + {remainingFilters > 0 && ( +
+ +{remainingFilters} more +
+ )} + + ); + })()} +
+ ); +} diff --git a/web/src/app/chat/input/FilterPills.tsx b/web/src/app/chat/input/FilterPills.tsx new file mode 100644 index 000000000000..4212eefaa80d --- /dev/null +++ b/web/src/app/chat/input/FilterPills.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { XIcon } from "lucide-react"; +import { SourceMetadata } from "@/lib/search/interfaces"; +import { Tag } from "@/lib/types"; + +type FilterItem = SourceMetadata | string | Tag; + +interface FilterPillsProps { + item: T; + itemToString: (item: T) => string; + onRemove: (item: T) => void; + toggleFilters?: () => void; +} + +export function FilterPills({ + item, + itemToString, + onRemove, + toggleFilters, +}: FilterPillsProps) { + return ( + + ); +} diff --git a/web/src/app/chat/shared_chat_search/SearchFilters.tsx b/web/src/app/chat/shared_chat_search/SearchFilters.tsx index 46ceda9a71ca..3f3e25c0e2b7 100644 --- a/web/src/app/chat/shared_chat_search/SearchFilters.tsx +++ b/web/src/app/chat/shared_chat_search/SearchFilters.tsx @@ -96,7 +96,7 @@ export function SourceSelector({ }); }; - let allSourcesSelected = selectedSources.length > 0; + let allSourcesSelected = selectedSources.length == existingSources.length; const toggleAllSources = () => { if (allSourcesSelected) {