mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-16 23:35:46 +00:00
Compare commits
10 Commits
v0.24.0-cl
...
auto_promp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06bc8f1f92 | ||
|
|
8093ceeb45 | ||
|
|
3d0ace1e45 | ||
|
|
553aba79dc | ||
|
|
da038b317a | ||
|
|
6769dc373f | ||
|
|
b35e05315c | ||
|
|
7cfd3d2d44 | ||
|
|
b2aa1c864b | ||
|
|
d2f8177b8f |
@@ -41,6 +41,19 @@ personas:
|
||||
icon_color: "#6FB1FF"
|
||||
display_priority: 1
|
||||
is_visible: true
|
||||
starter_messages:
|
||||
- name: "General Information"
|
||||
description: "Ask about available information"
|
||||
message: "Hello! I'm interested in learning more about the information available here. Could you give me an overview of the types of data or documents that might be accessible?"
|
||||
- name: "Specific Topic Search"
|
||||
description: "Search for specific information"
|
||||
message: "Hi! I'd like to learn more about a specific topic. Could you help me find relevant documents and information?"
|
||||
- name: "Recent Updates"
|
||||
description: "Inquire about latest additions"
|
||||
message: "Hello! I'm curious about any recent updates or additions to the knowledge base. Can you tell me what new information has been added lately?"
|
||||
- name: "Cross-referencing Information"
|
||||
description: "Connect information from different sources"
|
||||
message: "Hi! I'm working on a project that requires connecting information from multiple sources. How can I effectively cross-reference data across different documents or categories?"
|
||||
|
||||
- id: 1
|
||||
name: "General"
|
||||
@@ -57,6 +70,19 @@ personas:
|
||||
icon_color: "#FF6F6F"
|
||||
display_priority: 0
|
||||
is_visible: true
|
||||
starter_messages:
|
||||
- name: "Open Discussion"
|
||||
description: "Start an open-ended conversation"
|
||||
message: "Hi! Can you help me write a professional email?"
|
||||
- name: "Problem Solving"
|
||||
description: "Get help with a challenge"
|
||||
message: "Hello! I need help managing my daily tasks better. Do you have any simple tips?"
|
||||
- name: "Learn Something New"
|
||||
description: "Explore a new topic"
|
||||
message: "Hi! Could you explain what project management is in simple terms?"
|
||||
- name: "Creative Brainstorming"
|
||||
description: "Generate creative ideas"
|
||||
message: "Hello! I need to brainstorm some team building activities. Do you have any fun suggestions?"
|
||||
|
||||
- id: 2
|
||||
name: "Paraphrase"
|
||||
@@ -73,7 +99,19 @@ personas:
|
||||
icon_color: "#6FFF8D"
|
||||
display_priority: 2
|
||||
is_visible: false
|
||||
|
||||
starter_messages:
|
||||
- name: "Document Search"
|
||||
description: "Find exact information"
|
||||
message: "Hi! Could you help me find information about our team structure and reporting lines from our internal documents?"
|
||||
- name: "Process Verification"
|
||||
description: "Find exact quotes"
|
||||
message: "Hello! I need to understand our project approval process. Could you find the exact steps from our documentation?"
|
||||
- name: "Technical Documentation"
|
||||
description: "Search technical details"
|
||||
message: "Hi there! I'm looking for information about our deployment procedures. Can you find the specific steps from our technical guides?"
|
||||
- name: "Policy Reference"
|
||||
description: "Check official policies"
|
||||
message: "Hello! Could you help me find our official guidelines about client communication? I need the exact wording from our documentation."
|
||||
|
||||
- id: 3
|
||||
name: "Art"
|
||||
@@ -86,8 +124,21 @@ personas:
|
||||
llm_filter_extraction: false
|
||||
recency_bias: "no_decay"
|
||||
document_sets: []
|
||||
icon_shape: 234124
|
||||
icon_shape: 234124
|
||||
icon_color: "#9B59B6"
|
||||
image_generation: true
|
||||
image_generation: true
|
||||
display_priority: 3
|
||||
is_visible: true
|
||||
starter_messages:
|
||||
- name: "Landscape"
|
||||
description: "Generate a landscape image"
|
||||
message: "Create an image of a serene mountain lake at sunset, with snow-capped peaks reflected in the calm water and a small wooden cabin on the shore."
|
||||
- name: "Character"
|
||||
description: "Generate a character image"
|
||||
message: "Generate an image of a futuristic robot with glowing blue eyes, sleek metallic body, and intricate circuitry visible through transparent panels on its chest and arms."
|
||||
- name: "Abstract"
|
||||
description: "Create an abstract image"
|
||||
message: "Create an abstract image representing the concept of time, using swirling clock hands, fragmented hourglasses, and streaks of light to convey the passage of moments and eras."
|
||||
- name: "Urban Scene"
|
||||
description: "Generate an urban landscape"
|
||||
message: "Generate an image of a bustling futuristic cityscape at night, with towering skyscrapers, flying vehicles, holographic advertisements, and a mix of neon and bioluminescent lighting."
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
from danswer.access.models import DocumentAccess
|
||||
from danswer.indexing.models import DocMetadataAwareIndexChunk
|
||||
from danswer.search.models import IndexFilters
|
||||
from danswer.search.models import InferenceChunk
|
||||
from danswer.search.models import InferenceChunkUncleaned
|
||||
from shared_configs.model_server_models import Embedding
|
||||
|
||||
@@ -363,6 +364,19 @@ class AdminCapable(abc.ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class RandomCapable(abc.ABC):
|
||||
"""Class must implement random document retrieval capability"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def random_retrieval(
|
||||
self,
|
||||
filters: IndexFilters,
|
||||
num_to_retrieve: int = 10,
|
||||
) -> list[InferenceChunk]:
|
||||
"""Retrieve random chunks matching the filters"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BaseIndex(
|
||||
Verifiable,
|
||||
Indexable,
|
||||
@@ -370,6 +384,7 @@ class BaseIndex(
|
||||
Deletable,
|
||||
AdminCapable,
|
||||
IdRetrievalCapable,
|
||||
RandomCapable,
|
||||
abc.ABC,
|
||||
):
|
||||
"""
|
||||
|
||||
@@ -69,6 +69,7 @@ from danswer.document_index.vespa_constants import YQL_BASE
|
||||
from danswer.indexing.models import DocMetadataAwareIndexChunk
|
||||
from danswer.key_value_store.factory import get_kv_store
|
||||
from danswer.search.models import IndexFilters
|
||||
from danswer.search.models import InferenceChunk
|
||||
from danswer.search.models import InferenceChunkUncleaned
|
||||
from danswer.utils.batching import batch_generator
|
||||
from danswer.utils.logger import setup_logger
|
||||
@@ -899,6 +900,41 @@ class VespaIndex(DocumentIndex):
|
||||
|
||||
logger.info("Batch deletion completed")
|
||||
|
||||
def random_retrieval(
|
||||
self,
|
||||
filters: IndexFilters,
|
||||
num_to_retrieve: int = 10,
|
||||
) -> list[InferenceChunk]:
|
||||
"""Retrieve random chunks matching the filters"""
|
||||
vespa_where_clauses = build_vespa_filters(filters)
|
||||
|
||||
# Remove trailing 'and' if it exists
|
||||
if vespa_where_clauses.strip().endswith("and"):
|
||||
vespa_where_clauses = vespa_where_clauses.strip()[:-3].strip()
|
||||
|
||||
# Add a true condition if we only have hidden filter
|
||||
if vespa_where_clauses == "!(hidden=true)":
|
||||
vespa_where_clauses += " and true"
|
||||
|
||||
yql = YQL_BASE.format(index_name=self.index_name) + vespa_where_clauses
|
||||
|
||||
import random
|
||||
|
||||
params: dict[str, str | int | float] = {
|
||||
"yql": yql,
|
||||
"hits": num_to_retrieve
|
||||
* 10, # Request more hits to get better randomization
|
||||
"timeout": VESPA_TIMEOUT,
|
||||
"ranking.softtimeout.enable": "false", # Ensure we get all results
|
||||
}
|
||||
|
||||
results = query_vespa(params)
|
||||
|
||||
# Randomly sample from the results
|
||||
if len(results) > num_to_retrieve:
|
||||
return random.sample(results, num_to_retrieve)
|
||||
return results
|
||||
|
||||
|
||||
class _VespaDeleteRequest:
|
||||
def __init__(self, document_id: str, index_name: str) -> None:
|
||||
|
||||
@@ -14,6 +14,7 @@ from danswer.auth.users import current_curator_or_admin_user
|
||||
from danswer.auth.users import current_user
|
||||
from danswer.configs.constants import FileOrigin
|
||||
from danswer.configs.constants import NotificationType
|
||||
from danswer.db.document_set import get_document_sets_by_ids
|
||||
from danswer.db.engine import get_session
|
||||
from danswer.db.models import User
|
||||
from danswer.db.notification import create_notification
|
||||
@@ -26,10 +27,16 @@ from danswer.db.persona import update_all_personas_display_priority
|
||||
from danswer.db.persona import update_persona_public_status
|
||||
from danswer.db.persona import update_persona_shared_users
|
||||
from danswer.db.persona import update_persona_visibility
|
||||
from danswer.document_index.document_index_utils import get_both_index_names
|
||||
from danswer.document_index.factory import get_default_document_index
|
||||
from danswer.file_store.file_store import get_default_file_store
|
||||
from danswer.file_store.models import ChatFileType
|
||||
from danswer.llm.answering.prompts.utils import build_dummy_prompt
|
||||
from danswer.llm.factory import get_default_llms
|
||||
from danswer.search.models import IndexFilters
|
||||
from danswer.search.models import InferenceChunk
|
||||
from danswer.server.features.persona.models import CreatePersonaRequest
|
||||
from danswer.server.features.persona.models import GeneratePersonaPromptRequest
|
||||
from danswer.server.features.persona.models import ImageGenerationToolStatus
|
||||
from danswer.server.features.persona.models import PersonaSharedNotificationData
|
||||
from danswer.server.features.persona.models import PersonaSnapshot
|
||||
@@ -299,3 +306,68 @@ def build_final_template_prompt(
|
||||
retrieval_disabled=retrieval_disabled,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class StarterMessage(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
message: str
|
||||
|
||||
|
||||
def get_random_chunks_from_doc_sets(
|
||||
doc_set_ids: list[str], num_chunks: int, db_session: Session
|
||||
) -> list[InferenceChunk]:
|
||||
# Get the document index
|
||||
curr_ind_name, sec_ind_name = get_both_index_names(db_session)
|
||||
document_index = get_default_document_index(curr_ind_name, sec_ind_name)
|
||||
|
||||
# Create filters to only get chunks from specified document sets
|
||||
filters = IndexFilters(document_set_ids=doc_set_ids, access_control_list=None)
|
||||
|
||||
# Get random chunks directly from Vespa
|
||||
return document_index.random_retrieval(filters=filters, num_to_retrieve=num_chunks)
|
||||
|
||||
|
||||
# Based on an assistant schema, generates 4 prompts
|
||||
@basic_router.post("/assistant-prompt-refresh")
|
||||
def build_assistant_prompts(
|
||||
generate_persona_prompt_request: GeneratePersonaPromptRequest,
|
||||
db_session: Session = Depends(get_session),
|
||||
_: User | None = Depends(current_user),
|
||||
) -> list[StarterMessage]:
|
||||
document_sets = get_document_sets_by_ids(
|
||||
document_set_ids=generate_persona_prompt_request.document_set_ids,
|
||||
db_session=db_session,
|
||||
)
|
||||
print(document_sets)
|
||||
llm, fast_llm = get_default_llms(temperature=1.0)
|
||||
chunks = get_random_chunks_from_doc_sets(
|
||||
doc_set_ids=generate_persona_prompt_request.document_set_ids,
|
||||
num_chunks=4,
|
||||
db_session=db_session,
|
||||
)
|
||||
random_content = "".join([chunk.content for chunk in chunks])
|
||||
prompt = (
|
||||
"Based on the following content sample, create a natural, engaging starter message for a chatbot. "
|
||||
"The message should demonstrate knowledge of the content domain while inviting user interaction. "
|
||||
"and 'message' (the actual conversation starter). Content sample: "
|
||||
+ random_content
|
||||
)
|
||||
prompts: StarterMessage = []
|
||||
for _ in range(4):
|
||||
import json
|
||||
|
||||
response = json.loads(
|
||||
fast_llm.invoke(prompt, structured_response_format=StarterMessage).content
|
||||
)
|
||||
# Convert the LLM response into a StarterMessage object
|
||||
starter_message = StarterMessage(
|
||||
name=response["name"],
|
||||
description=response["description"],
|
||||
message=response["message"],
|
||||
)
|
||||
prompts.append(starter_message)
|
||||
|
||||
return prompts
|
||||
|
||||
# return build_assistant_prompts(assistant_schema=assistant_schema)
|
||||
|
||||
@@ -16,6 +16,13 @@ from danswer.utils.logger import setup_logger
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
# More minimal request for generating a persona prompt
|
||||
class GeneratePersonaPromptRequest(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
document_set_ids: list[int]
|
||||
|
||||
|
||||
class CreatePersonaRequest(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
|
||||
@@ -57,6 +57,7 @@ import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
|
||||
import { buildImgUrl } from "@/app/chat/files/images/utils";
|
||||
import { LlmList } from "@/components/llm/LLMList";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
function findSearchTool(tools: ToolSnapshot[]) {
|
||||
return tools.find((tool) => tool.in_code_tool_id === "SearchTool");
|
||||
@@ -214,6 +215,43 @@ export function AssistantEditor({
|
||||
groups: existingPersona?.groups ?? [],
|
||||
};
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
interface AssistantPrompt {
|
||||
message: string;
|
||||
description: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Create debounced refresh function
|
||||
const debouncedRefreshPrompts = debounce(
|
||||
async (values: any, setFieldValue: any) => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
const response = await fetch("/api/persona/assistant-prompt-refresh", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
document_set_ids: values.document_set_ids || [],
|
||||
}),
|
||||
});
|
||||
|
||||
const data: AssistantPrompt = await response.json();
|
||||
if (response.ok) {
|
||||
setFieldValue("starter_messages", data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh prompts:", error);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
},
|
||||
1000
|
||||
);
|
||||
|
||||
const [isRequestSuccessful, setIsRequestSuccessful] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -407,6 +445,8 @@ export function AssistantEditor({
|
||||
isSubmitting,
|
||||
values,
|
||||
setFieldValue,
|
||||
errors,
|
||||
|
||||
...formikProps
|
||||
}: FormikProps<any>) => {
|
||||
function toggleToolInValues(toolId: number) {
|
||||
@@ -429,6 +469,22 @@ export function AssistantEditor({
|
||||
values.llm_model_version_override || defaultModelName || ""
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Only refresh if we have required fields and no errors
|
||||
if (
|
||||
values.name &&
|
||||
values.description &&
|
||||
Object.keys(errors).length === 0
|
||||
) {
|
||||
debouncedRefreshPrompts(values, setFieldValue);
|
||||
}
|
||||
}, [
|
||||
values.name,
|
||||
values.description,
|
||||
values.document_set_ids,
|
||||
errors,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Form className="w-full text-text-950">
|
||||
<div className="w-full flex gap-x-2 justify-center">
|
||||
@@ -627,7 +683,10 @@ export function AssistantEditor({
|
||||
otherwise specified below.
|
||||
{admin &&
|
||||
user?.preferences.default_model &&
|
||||
` Your current (user-specific) default model is ${getDisplayNameForModel(destructureValue(user?.preferences?.default_model!).modelName)}`}
|
||||
` Your current (user-specific) default model is ${getDisplayNameForModel(
|
||||
destructureValue(user?.preferences?.default_model!)
|
||||
.modelName
|
||||
)}`}
|
||||
</p>
|
||||
{admin ? (
|
||||
<div className="mb-2 flex items-starts">
|
||||
@@ -972,6 +1031,162 @@ export function AssistantEditor({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex flex-col">
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<div className="block font-medium text-base">
|
||||
Starter Messages
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
onClick={() => debouncedRefreshPrompts(values, setFieldValue)}
|
||||
disabled={isRefreshing || Object.keys(errors).length > 0}
|
||||
>
|
||||
{isRefreshing ? "Refreshing..." : "Refresh Messages"}
|
||||
</Button>
|
||||
|
||||
<SubLabel>
|
||||
Add pre-defined messages to help users get started. Only the
|
||||
first 4 will be displayed.
|
||||
</SubLabel>
|
||||
<FieldArray
|
||||
name="starter_messages"
|
||||
render={(arrayHelpers: ArrayHelpers<StarterMessage[]>) => (
|
||||
<div>
|
||||
{values.starter_messages &&
|
||||
values.starter_messages.length > 0 &&
|
||||
values.starter_messages.map(
|
||||
(starterMessage: StarterMessage, index: number) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={index === 0 ? "mt-2" : "mt-6"}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="w-full mr-6 border border-border p-3 rounded">
|
||||
<div>
|
||||
<Label small>Name</Label>
|
||||
<SubLabel>
|
||||
Shows up as the "title" for
|
||||
this Starter Message. For example,
|
||||
"Write an email".
|
||||
</SubLabel>
|
||||
<Field
|
||||
name={`starter_messages[${index}].name`}
|
||||
className={`
|
||||
border
|
||||
border-border
|
||||
bg-background
|
||||
rounded
|
||||
w-full
|
||||
py-2
|
||||
px-3
|
||||
mr-4
|
||||
`}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ErrorMessage
|
||||
name={`starter_messages[${index}].name`}
|
||||
component="div"
|
||||
className="text-error text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label small>Description</Label>
|
||||
<SubLabel>
|
||||
A description which tells the user what
|
||||
they might want to use this Starter
|
||||
Message for. For example "to a
|
||||
client about a new feature"
|
||||
</SubLabel>
|
||||
<Field
|
||||
name={`starter_messages.${index}.description`}
|
||||
className={`
|
||||
border
|
||||
border-border
|
||||
bg-background
|
||||
rounded
|
||||
w-full
|
||||
py-2
|
||||
px-3
|
||||
mr-4
|
||||
`}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ErrorMessage
|
||||
name={`starter_messages[${index}].description`}
|
||||
component="div"
|
||||
className="text-error text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label small>Message</Label>
|
||||
<SubLabel>
|
||||
The actual message to be sent as the
|
||||
initial user message if a user selects
|
||||
this starter prompt. For example,
|
||||
"Write me an email to a client
|
||||
about a new billing feature we just
|
||||
released."
|
||||
</SubLabel>
|
||||
<Field
|
||||
name={`starter_messages[${index}].message`}
|
||||
className={`
|
||||
border
|
||||
border-border
|
||||
bg-background
|
||||
rounded
|
||||
w-full
|
||||
py-2
|
||||
px-3
|
||||
mr-4
|
||||
`}
|
||||
as="textarea"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ErrorMessage
|
||||
name={`starter_messages[${index}].message`}
|
||||
component="div"
|
||||
className="text-error text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-auto">
|
||||
<FiX
|
||||
className="my-auto w-10 h-10 cursor-pointer hover:bg-hover rounded p-2"
|
||||
onClick={() => arrayHelpers.remove(index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
arrayHelpers.push({
|
||||
name: "",
|
||||
description: "",
|
||||
message: "",
|
||||
});
|
||||
}}
|
||||
className="text-white mt-3"
|
||||
size="xs"
|
||||
type="button"
|
||||
icon={FiPlus}
|
||||
color="green"
|
||||
>
|
||||
Add New
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
<AdvancedOptionsToggle
|
||||
showAdvancedOptions={showAdvancedOptions}
|
||||
@@ -996,157 +1211,6 @@ export function AssistantEditor({
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mb-6 flex flex-col">
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<div className="block font-medium text-base">
|
||||
Starter Messages (Optional){" "}
|
||||
</div>
|
||||
</div>
|
||||
<FieldArray
|
||||
name="starter_messages"
|
||||
render={(
|
||||
arrayHelpers: ArrayHelpers<StarterMessage[]>
|
||||
) => (
|
||||
<div>
|
||||
{values.starter_messages &&
|
||||
values.starter_messages.length > 0 &&
|
||||
values.starter_messages.map(
|
||||
(
|
||||
starterMessage: StarterMessage,
|
||||
index: number
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={index === 0 ? "mt-2" : "mt-6"}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="w-full mr-6 border border-border p-3 rounded">
|
||||
<div>
|
||||
<Label small>Name</Label>
|
||||
<SubLabel>
|
||||
Shows up as the "title"
|
||||
for this Starter Message. For
|
||||
example, "Write an email".
|
||||
</SubLabel>
|
||||
<Field
|
||||
name={`starter_messages[${index}].name`}
|
||||
className={`
|
||||
border
|
||||
border-border
|
||||
bg-background
|
||||
rounded
|
||||
w-full
|
||||
py-2
|
||||
px-3
|
||||
mr-4
|
||||
`}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ErrorMessage
|
||||
name={`starter_messages[${index}].name`}
|
||||
component="div"
|
||||
className="text-error text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label small>Description</Label>
|
||||
<SubLabel>
|
||||
A description which tells the user
|
||||
what they might want to use this
|
||||
Starter Message for. For example
|
||||
"to a client about a new
|
||||
feature"
|
||||
</SubLabel>
|
||||
<Field
|
||||
name={`starter_messages.${index}.description`}
|
||||
className={`
|
||||
border
|
||||
border-border
|
||||
bg-background
|
||||
rounded
|
||||
w-full
|
||||
py-2
|
||||
px-3
|
||||
mr-4
|
||||
`}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ErrorMessage
|
||||
name={`starter_messages[${index}].description`}
|
||||
component="div"
|
||||
className="text-error text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label small>Message</Label>
|
||||
<SubLabel>
|
||||
The actual message to be sent as the
|
||||
initial user message if a user
|
||||
selects this starter prompt. For
|
||||
example, "Write me an email to
|
||||
a client about a new billing feature
|
||||
we just released."
|
||||
</SubLabel>
|
||||
<Field
|
||||
name={`starter_messages[${index}].message`}
|
||||
className={`
|
||||
border
|
||||
border-border
|
||||
bg-background
|
||||
rounded
|
||||
w-full
|
||||
py-2
|
||||
px-3
|
||||
mr-4
|
||||
`}
|
||||
as="textarea"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ErrorMessage
|
||||
name={`starter_messages[${index}].message`}
|
||||
component="div"
|
||||
className="text-error text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-auto">
|
||||
<FiX
|
||||
className="my-auto w-10 h-10 cursor-pointer hover:bg-hover rounded p-2"
|
||||
onClick={() =>
|
||||
arrayHelpers.remove(index)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
arrayHelpers.push({
|
||||
name: "",
|
||||
description: "",
|
||||
message: "",
|
||||
});
|
||||
}}
|
||||
className="text-white mt-3"
|
||||
size="xs"
|
||||
type="button"
|
||||
icon={FiPlus}
|
||||
color="green"
|
||||
>
|
||||
Add New
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IsPublicGroupSelector
|
||||
formikProps={{
|
||||
values,
|
||||
|
||||
@@ -1,96 +1,44 @@
|
||||
import { getSourceMetadataForSources } from "@/lib/sources";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { Divider } from "@tremor/react";
|
||||
import { FiBookmark, FiInfo } from "react-icons/fi";
|
||||
import { HoverPopup } from "@/components/HoverPopup";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { useState } from "react";
|
||||
import { DisplayAssistantCard } from "@/components/assistants/AssistantCards";
|
||||
|
||||
export function ChatIntro({
|
||||
availableSources,
|
||||
selectedPersona,
|
||||
}: {
|
||||
availableSources: ValidSources[];
|
||||
selectedPersona: Persona;
|
||||
}) {
|
||||
const availableSourceMetadata = getSourceMetadataForSources(availableSources);
|
||||
export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) {
|
||||
const [hoveredAssistant, setHoveredAssistant] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<div className="mobile:w-[90%] mobile:px-4 w-message-xs 2xl:w-message-sm 3xl:w-message">
|
||||
<div className="flex">
|
||||
<div className="mx-auto">
|
||||
<div className="m-auto text-3xl font-strong font-bold text-strong w-fit">
|
||||
{selectedPersona?.name || "How can I help you today?"}
|
||||
<div className="mobile:w-[90%] mobile:px-4 w-message-xs 2xl:w-message-sm 3xl:w-message">
|
||||
<div className="relative flex w-fit mx-auto justify-center">
|
||||
<div className="absolute z-10 -left-20 top-1/2 -translate-y-1/2">
|
||||
<div className="relative">
|
||||
<div
|
||||
onMouseEnter={() => setHoveredAssistant(true)}
|
||||
onMouseLeave={() => setHoveredAssistant(false)}
|
||||
className="p-4 scale-[.8] cursor-pointer border-dashed rounded-full flex border border-border border-2 border-dashed"
|
||||
style={{
|
||||
borderStyle: "dashed",
|
||||
borderWidth: "1.5px",
|
||||
borderSpacing: "4px",
|
||||
}}
|
||||
>
|
||||
<AssistantIcon
|
||||
disableToolip
|
||||
size={"large"}
|
||||
assistant={selectedPersona}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute right-full mr-2 w-[300px] top-0">
|
||||
{hoveredAssistant && (
|
||||
<DisplayAssistantCard selectedPersona={selectedPersona} />
|
||||
)}
|
||||
</div>
|
||||
{selectedPersona && (
|
||||
<div className="mt-1">{selectedPersona.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedPersona && selectedPersona.num_chunks !== 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div>
|
||||
{selectedPersona.document_sets.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="font-bold mb-1 mt-4 text-emphasis">
|
||||
Knowledge Sets:{" "}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedPersona.document_sets.map((documentSet) => (
|
||||
<div key={documentSet.id} className="w-fit">
|
||||
<HoverPopup
|
||||
mainContent={
|
||||
<span className="flex w-fit p-1 rounded border border-border text-xs font-medium cursor-default">
|
||||
<div className="mr-1 my-auto">
|
||||
<FiBookmark />
|
||||
</div>
|
||||
{documentSet.name}
|
||||
</span>
|
||||
}
|
||||
popupContent={
|
||||
<div className="flex py-1 w-96">
|
||||
<FiInfo className="my-auto mr-2" />
|
||||
<div className="text-sm">
|
||||
{documentSet.description}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
direction="top"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableSources.length > 0 && (
|
||||
<div className="mt-1">
|
||||
<p className="font-bold mb-1 mt-4 text-emphasis">
|
||||
Connected Sources:{" "}
|
||||
</p>
|
||||
<div className={`flex flex-wrap gap-2`}>
|
||||
{availableSourceMetadata.map((sourceMetadata) => (
|
||||
<span
|
||||
key={sourceMetadata.internalName}
|
||||
className="flex w-fit p-1 rounded border border-border text-xs font-medium cursor-default"
|
||||
>
|
||||
<div className="mr-1 my-auto">
|
||||
{sourceMetadata.icon({})}
|
||||
</div>
|
||||
<div className="my-auto">
|
||||
{sourceMetadata.displayName}
|
||||
</div>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="text-3xl line-clamp-2 text-text-800 font-base font-semibold text-strong">
|
||||
{selectedPersona?.name || "How can I help you today?"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -103,6 +103,7 @@ import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
|
||||
import BlurBackground from "./shared_chat_search/BlurBackground";
|
||||
import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { Divider } from "@tremor/react";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@@ -741,12 +742,6 @@ export function ChatPage({
|
||||
}, [liveAssistant]);
|
||||
|
||||
const filterManager = useFilters();
|
||||
const [finalAvailableSources, finalAvailableDocumentSets] =
|
||||
computeAvailableFilters({
|
||||
selectedPersona: selectedAssistant,
|
||||
availableSources,
|
||||
availableDocumentSets,
|
||||
});
|
||||
|
||||
const [currentFeedback, setCurrentFeedback] = useState<
|
||||
[FeedbackType, number] | null
|
||||
@@ -2002,7 +1997,7 @@ export function ChatPage({
|
||||
{...getRootProps()}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-full flex flex-col overflow-y-auto include-scrollbar overflow-x-hidden relative`}
|
||||
className={`w-full h-full flex flex-col overflow-y-auto include-scrollbar overflow-x-hidden relative`}
|
||||
ref={scrollableDivRef}
|
||||
>
|
||||
{/* ChatBanner is a custom banner that displays a admin-specified message at
|
||||
@@ -2012,11 +2007,51 @@ export function ChatPage({
|
||||
!isFetchingChatMessages &&
|
||||
currentSessionChatState == "input" &&
|
||||
!loadingError && (
|
||||
<ChatIntro
|
||||
availableSources={finalAvailableSources}
|
||||
selectedPersona={liveAssistant}
|
||||
/>
|
||||
<div className="h-full flex flex-col justify-center items-center">
|
||||
<ChatIntro selectedPersona={liveAssistant} />
|
||||
|
||||
<div
|
||||
key={-4}
|
||||
className={`
|
||||
mx-auto
|
||||
px-4
|
||||
w-full
|
||||
max-w-[750px]
|
||||
flex
|
||||
flex-wrap
|
||||
justify-center
|
||||
mt-2
|
||||
h-40
|
||||
items-start
|
||||
mb-6`}
|
||||
>
|
||||
{currentPersona?.starter_messages &&
|
||||
currentPersona.starter_messages.length >
|
||||
0 && (
|
||||
<>
|
||||
<Divider className="mx-2" />
|
||||
|
||||
{currentPersona.starter_messages
|
||||
.slice(0, 4)
|
||||
.map((starterMessage, i) => (
|
||||
<div key={i} className="w-1/2">
|
||||
<StarterMessage
|
||||
starterMessage={starterMessage}
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
messageOverride:
|
||||
starterMessage.message,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={
|
||||
"-ml-4 w-full mx-auto " +
|
||||
@@ -2383,45 +2418,6 @@ export function ChatPage({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{currentPersona &&
|
||||
currentPersona.starter_messages &&
|
||||
currentPersona.starter_messages.length > 0 &&
|
||||
selectedAssistant &&
|
||||
messageHistory.length === 0 &&
|
||||
!isFetchingChatMessages && (
|
||||
<div
|
||||
key={-4}
|
||||
className={`
|
||||
mx-auto
|
||||
px-4
|
||||
w-searchbar-xs
|
||||
2xl:w-searchbar-sm
|
||||
3xl:w-searchbar
|
||||
grid
|
||||
gap-4
|
||||
grid-cols-1
|
||||
grid-rows-1
|
||||
mt-4
|
||||
md:grid-cols-2
|
||||
mb-6`}
|
||||
>
|
||||
{currentPersona.starter_messages.map(
|
||||
(starterMessage, i) => (
|
||||
<div key={i} className="w-full">
|
||||
<StarterMessage
|
||||
starterMessage={starterMessage}
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
messageOverride:
|
||||
starterMessage.message,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
|
||||
<div ref={endPaddingRef} className="h-[95px]" />
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import { StarterMessage } from "../admin/assistants/interfaces";
|
||||
import { StarterMessage as StarterMessageType } from "../admin/assistants/interfaces";
|
||||
|
||||
export function StarterMessage({
|
||||
starterMessage,
|
||||
onClick,
|
||||
}: {
|
||||
starterMessage: StarterMessage;
|
||||
starterMessage: StarterMessageType;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"py-2 px-3 rounded border border-border bg-white cursor-pointer hover:bg-hover-light h-full"
|
||||
}
|
||||
className="mb-4 mx-2 group relative overflow-hidden rounded-xl border border-border bg-gradient-to-br from-white to-background p-4 shadow-sm transition-all duration-300 hover:shadow-md hover:scale-[1.005] cursor-pointer"
|
||||
onClick={onClick}
|
||||
>
|
||||
<p className="font-medium text-emphasis">{starterMessage.name}</p>
|
||||
<p className="text-subtle text-sm">{starterMessage.description}</p>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-100 to-purple-100 opacity-0 group-hover:opacity-20 transition-opacity duration-300" />
|
||||
<h3
|
||||
className="text-base flex items-center font-medium text-text-800 group-hover:text-text-900 transition-colors duration-300
|
||||
line-clamp-2 gap-x-2 overflow-hidden"
|
||||
>
|
||||
{starterMessage.name}
|
||||
</h3>
|
||||
<div className={`overflow-hidden transition-all duration-300 max-h-20}`}>
|
||||
<p className="text-sm text-text-600 mt-2">
|
||||
{starterMessage.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,3 +115,63 @@ export function DraggableAssistantCard(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DisplayAssistantCard({
|
||||
selectedPersona,
|
||||
}: {
|
||||
selectedPersona: Persona;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-4 bg-white/90 backdrop-blur-sm rounded-lg shadow-md border border-border/50 max-w-md w-full mx-auto transition-all duration-300 ease-in-out hover:shadow-lg">
|
||||
<div className="flex items-center mb-3">
|
||||
<AssistantIcon
|
||||
disableToolip
|
||||
size="medium"
|
||||
assistant={selectedPersona}
|
||||
/>
|
||||
<h2 className="ml-3 text-xl font-semibold text-text-900">
|
||||
{selectedPersona.name}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-text-600 mb-3 leading-relaxed">
|
||||
{selectedPersona.description}
|
||||
</p>
|
||||
{selectedPersona.tools.length > 0 ||
|
||||
selectedPersona.llm_relevance_filter ||
|
||||
selectedPersona.llm_filter_extraction ? (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-medium text-text-900">Capabilities:</h3>
|
||||
<ul className="space-y-.5">
|
||||
{/* display all tools */}
|
||||
{selectedPersona.tools.map((tool, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="flex items-center text-sm text-text-700"
|
||||
>
|
||||
<span className="mr-2 text-text-500 opacity-70">•</span>{" "}
|
||||
{tool.display_name}
|
||||
</li>
|
||||
))}
|
||||
{/* Built in capabilities */}
|
||||
{selectedPersona.llm_relevance_filter && (
|
||||
<li className="flex items-center text-sm text-text-700">
|
||||
<span className="mr-2 text-text-500 opacity-70">•</span>{" "}
|
||||
Advanced Relevance Filtering
|
||||
</li>
|
||||
)}
|
||||
{selectedPersona.llm_filter_extraction && (
|
||||
<li className="flex items-center text-sm text-text-700">
|
||||
<span className="mr-2 text-text-500 opacity-70">•</span> Smart
|
||||
Information Extraction
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-600 italic">
|
||||
No specific capabilities listed for this assistant.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,22 +23,38 @@ export function AssistantIcon({
|
||||
assistant,
|
||||
size,
|
||||
border,
|
||||
disableToolip,
|
||||
}: {
|
||||
assistant: Persona;
|
||||
size?: "small" | "medium" | "large";
|
||||
size?: "small" | "medium" | "large" | "header";
|
||||
border?: boolean;
|
||||
disableToolip?: boolean;
|
||||
}) {
|
||||
const color = darkerGenerateColorFromId(assistant.id.toString());
|
||||
|
||||
return (
|
||||
<CustomTooltip showTick line wrap content={assistant.description}>
|
||||
<CustomTooltip
|
||||
disabled={disableToolip}
|
||||
showTick
|
||||
line
|
||||
wrap
|
||||
content={assistant.description}
|
||||
>
|
||||
{
|
||||
// Prioritization order: image, graph, defaults
|
||||
assistant.uploaded_image_id ? (
|
||||
<img
|
||||
alt={assistant.name}
|
||||
className={`object-cover object-center rounded-sm overflow-hidden transition-opacity duration-300 opacity-100
|
||||
${size === "large" ? "w-8 h-8" : "w-6 h-6"}`}
|
||||
${
|
||||
size === "large"
|
||||
? "w-10 h-10"
|
||||
: size === "header"
|
||||
? "w-14 h-14"
|
||||
: size === "medium"
|
||||
? "w-8 h-8"
|
||||
: "w-6 h-6"
|
||||
}`}
|
||||
src={buildImgUrl(assistant.uploaded_image_id)}
|
||||
loading="lazy"
|
||||
/>
|
||||
@@ -46,20 +62,36 @@ export function AssistantIcon({
|
||||
<div
|
||||
className={`flex-none
|
||||
${border && "ring ring-[1px] ring-border-strong "}
|
||||
${size === "large" ? "w-10 h-10" : "w-6 h-6"} `}
|
||||
${
|
||||
size === "large"
|
||||
? "w-10 h-10"
|
||||
: size === "header"
|
||||
? "w-14 h-14"
|
||||
: size === "medium"
|
||||
? "w-8 h-8"
|
||||
: "w-6 h-6"
|
||||
} `}
|
||||
>
|
||||
{createSVG(
|
||||
{ encodedGrid: assistant.icon_shape, filledSquares: 0 },
|
||||
assistant.icon_color,
|
||||
size == "large" ? 36 : 24
|
||||
size === "large"
|
||||
? 40
|
||||
: size === "header"
|
||||
? 56
|
||||
: size === "medium"
|
||||
? 32
|
||||
: 24
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`flex-none rounded-sm overflow-hidden
|
||||
${border && "border border-.5 border-border-strong "}
|
||||
${size === "large" && "w-12 h-12"}
|
||||
${(!size || size === "small") && "w-6 h-6"} `}
|
||||
${size === "large" ? "w-10 h-10" : ""}
|
||||
${size === "header" ? "w-14 h-14" : ""}
|
||||
${size === "medium" ? "w-8 h-8" : ""}
|
||||
${!size || size === "small" ? "w-6 h-6" : ""} `}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -124,9 +124,9 @@ export const CustomTooltip = ({
|
||||
!disabled &&
|
||||
createPortal(
|
||||
<div
|
||||
className={`min-w-8 fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
|
||||
large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"
|
||||
}
|
||||
className={`min-w-8 fixed z-[1000] ${
|
||||
citation ? "max-w-[350px]" : "w-40"
|
||||
} ${large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"}
|
||||
transform -translate-x-1/2 text-sm
|
||||
${
|
||||
light
|
||||
|
||||
Reference in New Issue
Block a user