mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-28 21:05:17 +02:00
@@ -1,5 +1,5 @@
|
|||||||
from sqlalchemy.engine.base import Connection
|
from sqlalchemy.engine.base import Connection
|
||||||
from typing import Any
|
from typing import Literal
|
||||||
import asyncio
|
import asyncio
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
import logging
|
import logging
|
||||||
@@ -8,6 +8,7 @@ from alembic import context
|
|||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
from sqlalchemy.sql.schema import SchemaItem
|
||||||
|
|
||||||
from shared_configs.configs import MULTI_TENANT
|
from shared_configs.configs import MULTI_TENANT
|
||||||
from danswer.db.engine import build_connection_string
|
from danswer.db.engine import build_connection_string
|
||||||
@@ -35,7 +36,18 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def include_object(
|
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:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Determines whether a database object should be included in migrations.
|
Determines whether a database object should be included in migrations.
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
from sqlalchemy.engine import Connection
|
from sqlalchemy.engine import Connection
|
||||||
@@ -37,8 +38,15 @@ EXCLUDE_TABLES = {"kombu_queue", "kombu_message"}
|
|||||||
|
|
||||||
def include_object(
|
def include_object(
|
||||||
object: SchemaItem,
|
object: SchemaItem,
|
||||||
name: str,
|
name: str | None,
|
||||||
type_: str,
|
type_: Literal[
|
||||||
|
"schema",
|
||||||
|
"table",
|
||||||
|
"column",
|
||||||
|
"index",
|
||||||
|
"unique_constraint",
|
||||||
|
"foreign_key_constraint",
|
||||||
|
],
|
||||||
reflected: bool,
|
reflected: bool,
|
||||||
compare_to: SchemaItem | None,
|
compare_to: SchemaItem | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
@@ -31,6 +31,7 @@ def llm_doc_from_inference_section(inference_section: InferenceSection) -> LlmDo
|
|||||||
if inference_section.center_chunk.source_links
|
if inference_section.center_chunk.source_links
|
||||||
else None,
|
else None,
|
||||||
source_links=inference_section.center_chunk.source_links,
|
source_links=inference_section.center_chunk.source_links,
|
||||||
|
match_highlights=inference_section.center_chunk.match_highlights,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -25,6 +25,7 @@ class LlmDoc(BaseModel):
|
|||||||
updated_at: datetime | None
|
updated_at: datetime | None
|
||||||
link: str | None
|
link: str | None
|
||||||
source_links: dict[int, str] | None
|
source_links: dict[int, str] | None
|
||||||
|
match_highlights: list[str] | None
|
||||||
|
|
||||||
|
|
||||||
# First chunk of info for streaming QA
|
# First chunk of info for streaming QA
|
||||||
|
@@ -77,6 +77,7 @@ def llm_doc_from_internet_search_result(result: InternetSearchResult) -> LlmDoc:
|
|||||||
updated_at=datetime.now(),
|
updated_at=datetime.now(),
|
||||||
link=result.link,
|
link=result.link,
|
||||||
source_links={0: result.link},
|
source_links={0: result.link},
|
||||||
|
match_highlights=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -44,6 +44,7 @@ def test_persona_category_management(reset: None) -> None:
|
|||||||
category=updated_persona_category,
|
category=updated_persona_category,
|
||||||
user_performing_action=regular_user,
|
user_performing_action=regular_user,
|
||||||
)
|
)
|
||||||
|
assert exc_info.value.response is not None
|
||||||
assert exc_info.value.response.status_code == 403
|
assert exc_info.value.response.status_code == 403
|
||||||
|
|
||||||
assert PersonaCategoryManager.verify(
|
assert PersonaCategoryManager.verify(
|
||||||
|
@@ -64,6 +64,7 @@ def mock_search_results() -> list[LlmDoc]:
|
|||||||
updated_at=datetime(2023, 1, 1),
|
updated_at=datetime(2023, 1, 1),
|
||||||
link="https://example.com/doc1",
|
link="https://example.com/doc1",
|
||||||
source_links={0: "https://example.com/doc1"},
|
source_links={0: "https://example.com/doc1"},
|
||||||
|
match_highlights=[],
|
||||||
),
|
),
|
||||||
LlmDoc(
|
LlmDoc(
|
||||||
content="Search result 2",
|
content="Search result 2",
|
||||||
@@ -75,6 +76,7 @@ def mock_search_results() -> list[LlmDoc]:
|
|||||||
updated_at=datetime(2023, 1, 2),
|
updated_at=datetime(2023, 1, 2),
|
||||||
link="https://example.com/doc2",
|
link="https://example.com/doc2",
|
||||||
source_links={0: "https://example.com/doc2"},
|
source_links={0: "https://example.com/doc2"},
|
||||||
|
match_highlights=[],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -46,6 +46,7 @@ mock_docs = [
|
|||||||
updated_at=datetime.now(),
|
updated_at=datetime.now(),
|
||||||
link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None,
|
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"},
|
source_links={0: "https://mintlify.com/docs/settings/broken-links"},
|
||||||
|
match_highlights=[],
|
||||||
)
|
)
|
||||||
for id in range(10)
|
for id in range(10)
|
||||||
]
|
]
|
||||||
|
@@ -20,6 +20,7 @@ mock_docs = [
|
|||||||
updated_at=datetime.now(),
|
updated_at=datetime.now(),
|
||||||
link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None,
|
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"},
|
source_links={0: "https://mintlify.com/docs/settings/broken-links"},
|
||||||
|
match_highlights=[],
|
||||||
)
|
)
|
||||||
for id in range(10)
|
for id in range(10)
|
||||||
]
|
]
|
||||||
|
@@ -18,7 +18,7 @@ import {
|
|||||||
SendIcon,
|
SendIcon,
|
||||||
StopGeneratingIcon,
|
StopGeneratingIcon,
|
||||||
} from "@/components/icons/icons";
|
} from "@/components/icons/icons";
|
||||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
import { DanswerDocument, SourceMetadata } from "@/lib/search/interfaces";
|
||||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -37,9 +37,41 @@ import { AssistantsTab } from "../modal/configuration/AssistantsTab";
|
|||||||
import { IconType } from "react-icons";
|
import { IconType } from "react-icons";
|
||||||
import { LlmTab } from "../modal/configuration/LlmTab";
|
import { LlmTab } from "../modal/configuration/LlmTab";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
|
import { FilterPills } from "./FilterPills";
|
||||||
|
import { Tag } from "@/lib/types";
|
||||||
|
import FiltersDisplay from "./FilterDisplay";
|
||||||
|
|
||||||
const MAX_INPUT_HEIGHT = 200;
|
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<HTMLTextAreaElement>;
|
||||||
|
chatSessionId?: string;
|
||||||
|
toggleFilters?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function ChatInputBar({
|
export function ChatInputBar({
|
||||||
removeFilters,
|
removeFilters,
|
||||||
removeDocs,
|
removeDocs,
|
||||||
@@ -68,32 +100,7 @@ export function ChatInputBar({
|
|||||||
chatSessionId,
|
chatSessionId,
|
||||||
inputPrompts,
|
inputPrompts,
|
||||||
toggleFilters,
|
toggleFilters,
|
||||||
}: {
|
}: ChatInputBarProps) {
|
||||||
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<HTMLTextAreaElement>;
|
|
||||||
chatSessionId?: string;
|
|
||||||
toggleFilters?: () => void;
|
|
||||||
}) {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const textarea = textAreaRef.current;
|
const textarea = textAreaRef.current;
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
@@ -340,23 +347,26 @@ export function ChatInputBar({
|
|||||||
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
|
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
|
||||||
>
|
>
|
||||||
<div className="rounded-lg py-1.5 bg-white border border-border-medium overflow-hidden shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
|
<div className="rounded-lg py-1.5 bg-white border border-border-medium overflow-hidden shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
|
||||||
{filteredPrompts.map((currentPrompt, index) => (
|
{filteredPrompts.map(
|
||||||
<button
|
(currentPrompt: InputPrompt, index: number) => (
|
||||||
key={index}
|
<button
|
||||||
className={`px-2 ${
|
key={index}
|
||||||
tabbingIconIndex == index && "bg-hover"
|
className={`px-2 ${
|
||||||
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-hover cursor-pointer`}
|
tabbingIconIndex == index && "bg-hover"
|
||||||
onClick={() => {
|
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-hover cursor-pointer`}
|
||||||
updateInputPrompt(currentPrompt);
|
onClick={() => {
|
||||||
}}
|
updateInputPrompt(currentPrompt);
|
||||||
>
|
}}
|
||||||
<p className="font-bold">{currentPrompt.prompt}:</p>
|
>
|
||||||
<p className="text-left flex-grow mr-auto line-clamp-1">
|
<p className="font-bold">{currentPrompt.prompt}:</p>
|
||||||
{currentPrompt.id == selectedAssistant.id && "(default) "}
|
<p className="text-left flex-grow mr-auto line-clamp-1">
|
||||||
{currentPrompt.content?.trim()}
|
{currentPrompt.id == selectedAssistant.id &&
|
||||||
</p>
|
"(default) "}
|
||||||
</button>
|
{currentPrompt.content?.trim()}
|
||||||
))}
|
</p>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
key={filteredPrompts.length}
|
key={filteredPrompts.length}
|
||||||
@@ -430,6 +440,7 @@ export function ChatInputBar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(selectedDocuments.length > 0 || files.length > 0) && (
|
{(selectedDocuments.length > 0 || files.length > 0) && (
|
||||||
<div className="flex gap-x-2 px-2 pt-2">
|
<div className="flex gap-x-2 px-2 pt-2">
|
||||||
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
|
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
|
||||||
@@ -564,6 +575,16 @@ export function ChatInputBar({
|
|||||||
onClick={toggleFilters}
|
onClick={toggleFilters}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{(filterManager.selectedSources.length > 0 ||
|
||||||
|
filterManager.selectedDocumentSets.length > 0 ||
|
||||||
|
filterManager.selectedTags.length > 0 ||
|
||||||
|
filterManager.timeRange) &&
|
||||||
|
toggleFilters && (
|
||||||
|
<FiltersDisplay
|
||||||
|
filterManager={filterManager}
|
||||||
|
toggleFilters={toggleFilters}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
|
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
|
||||||
|
109
web/src/app/chat/input/FilterDisplay.tsx
Normal file
109
web/src/app/chat/input/FilterDisplay.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex my-auto flex-wrap gap-2 px-2">
|
||||||
|
{(() => {
|
||||||
|
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 (
|
||||||
|
<FilterPills<SourceMetadata>
|
||||||
|
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 (
|
||||||
|
<FilterPills<string>
|
||||||
|
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 (
|
||||||
|
<FilterPills<Tag>
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center bg-background-150 rounded-full px-3 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{filter.from.toLocaleDateString()} -{" "}
|
||||||
|
{filter.to.toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
<XIcon
|
||||||
|
onClick={() => filterManager.setTimeRange(null)}
|
||||||
|
size={16}
|
||||||
|
className="ml-2 text-text-400 hover:text-text-600 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
{remainingFilters > 0 && (
|
||||||
|
<div className="flex items-center bg-background-150 rounded-full px-3 py-1 text-sm">
|
||||||
|
<span>+{remainingFilters} more</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
39
web/src/app/chat/input/FilterPills.tsx
Normal file
39
web/src/app/chat/input/FilterPills.tsx
Normal file
@@ -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<T extends FilterItem> {
|
||||||
|
item: T;
|
||||||
|
itemToString: (item: T) => string;
|
||||||
|
onRemove: (item: T) => void;
|
||||||
|
toggleFilters?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FilterPills<T extends FilterItem>({
|
||||||
|
item,
|
||||||
|
itemToString,
|
||||||
|
onRemove,
|
||||||
|
toggleFilters,
|
||||||
|
}: FilterPillsProps<T>) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleFilters}
|
||||||
|
className="cursor-pointer flex flex-wrap gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center bg-background-150 rounded-full px-3 py-1 text-sm">
|
||||||
|
<span>{itemToString(item)}</span>
|
||||||
|
<XIcon
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemove(item);
|
||||||
|
}}
|
||||||
|
size={16}
|
||||||
|
className="ml-2 text-text-400 hover:text-text-600 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
@@ -96,7 +96,7 @@ export function SourceSelector({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let allSourcesSelected = selectedSources.length > 0;
|
let allSourcesSelected = selectedSources.length == existingSources.length;
|
||||||
|
|
||||||
const toggleAllSources = () => {
|
const toggleAllSources = () => {
|
||||||
if (allSourcesSelected) {
|
if (allSourcesSelected) {
|
||||||
|
Reference in New Issue
Block a user