mirror of
https://github.com/StanGirard/quivr.git
synced 2024-09-22 10:28:16 +03:00
feat: 🎸 sources (#1591)
added sources in front and backend <img width="1536" alt="image" src="https://github.com/StanGirard/quivr/assets/19614572/eb997288-282d-4f6e-b489-08ab3db400c6">
This commit is contained in:
parent
8e179759ee
commit
192610d008
@ -208,6 +208,7 @@ class QABaseBrainPicking(BaseModel):
|
||||
),
|
||||
verbose=False,
|
||||
rephrase_question=False,
|
||||
return_source_documents=True,
|
||||
)
|
||||
|
||||
prompt_content = (
|
||||
@ -283,6 +284,7 @@ class QABaseBrainPicking(BaseModel):
|
||||
),
|
||||
verbose=False,
|
||||
rephrase_question=False,
|
||||
return_source_documents=True,
|
||||
)
|
||||
|
||||
transformed_history = format_chat_history(history)
|
||||
@ -291,9 +293,10 @@ class QABaseBrainPicking(BaseModel):
|
||||
|
||||
async def wrap_done(fn: Awaitable, event: asyncio.Event):
|
||||
try:
|
||||
await fn
|
||||
return await fn
|
||||
except Exception as e:
|
||||
logger.error(f"Caught exception: {e}")
|
||||
return None # Or some sentinel value that indicates failure
|
||||
finally:
|
||||
event.set()
|
||||
|
||||
@ -342,17 +345,42 @@ class QABaseBrainPicking(BaseModel):
|
||||
}
|
||||
)
|
||||
|
||||
async for token in callback.aiter():
|
||||
logger.info("Token: %s", token)
|
||||
response_tokens.append(token)
|
||||
streamed_chat_history.assistant = token
|
||||
yield f"data: {json.dumps(streamed_chat_history.dict())}"
|
||||
try:
|
||||
async for token in callback.aiter():
|
||||
logger.debug("Token: %s", token)
|
||||
response_tokens.append(token)
|
||||
streamed_chat_history.assistant = token
|
||||
yield f"data: {json.dumps(streamed_chat_history.dict())}"
|
||||
except Exception as e:
|
||||
logger.error("Error during streaming tokens: %s", e)
|
||||
sources_string = ""
|
||||
try:
|
||||
result = await run
|
||||
source_documents = result.get("source_documents", [])
|
||||
if source_documents:
|
||||
# Formatting the source documents using Markdown without new lines for each source
|
||||
sources_string = "\n\n**Sources:** " + ", ".join(
|
||||
f"{doc.metadata.get('file_name', 'Unnamed Document')}"
|
||||
for doc in source_documents
|
||||
)
|
||||
streamed_chat_history.assistant += sources_string
|
||||
yield f"data: {json.dumps(streamed_chat_history.dict())}"
|
||||
else:
|
||||
logger.info(
|
||||
"No source documents found or source_documents is not a list."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error processing source documents: %s", e)
|
||||
|
||||
await run
|
||||
# Combine all response tokens to form the final assistant message
|
||||
assistant = "".join(response_tokens)
|
||||
assistant += sources_string
|
||||
|
||||
update_message_by_id(
|
||||
message_id=str(streamed_chat_history.message_id),
|
||||
user_message=question.question,
|
||||
assistant=assistant,
|
||||
)
|
||||
try:
|
||||
update_message_by_id(
|
||||
message_id=str(streamed_chat_history.message_id),
|
||||
user_message=question.question,
|
||||
assistant=assistant,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error updating message by ID: %s", e)
|
||||
|
@ -4,8 +4,10 @@ import { CopyButton } from "./components/CopyButton";
|
||||
import { MessageContent } from "./components/MessageContent";
|
||||
import { QuestionBrain } from "./components/QuestionBrain";
|
||||
import { QuestionPrompt } from "./components/QuestionPrompt";
|
||||
import { SourcesButton } from './components/SourcesButton'; // Import the new component
|
||||
import { useMessageRow } from "./hooks/useMessageRow";
|
||||
|
||||
|
||||
type MessageRowProps = {
|
||||
speaker: "user" | "assistant";
|
||||
text?: string;
|
||||
@ -31,21 +33,39 @@ export const MessageRow = React.forwardRef(
|
||||
text,
|
||||
});
|
||||
|
||||
let messageContent = text ?? "";
|
||||
let sourcesContent = "";
|
||||
|
||||
const sourcesIndex = messageContent.lastIndexOf("**Sources:**");
|
||||
const hasSources = sourcesIndex !== -1;
|
||||
|
||||
if (hasSources) {
|
||||
sourcesContent = messageContent.substring(sourcesIndex + "**Sources:**".length).trim();
|
||||
messageContent = messageContent.substring(0, sourcesIndex).trim();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={containerWrapperClasses}>
|
||||
<div ref={ref} className={containerClasses}>
|
||||
<div className="w-full gap-1 flex justify-between">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
{/* Left section for the question and prompt */}
|
||||
<div className="flex">
|
||||
<QuestionBrain brainName={brainName} />
|
||||
<QuestionPrompt promptName={promptName} />
|
||||
</div>
|
||||
{!isUserSpeaker && text !== undefined && (
|
||||
<CopyButton handleCopy={handleCopy} isCopied={isCopied} />
|
||||
)}
|
||||
{/* Right section for buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{!isUserSpeaker && (
|
||||
<>
|
||||
{hasSources && <SourcesButton sources={sourcesContent} />}
|
||||
<CopyButton handleCopy={handleCopy} isCopied={isCopied} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{children ?? (
|
||||
<MessageContent
|
||||
text={text ?? ""}
|
||||
text={messageContent}
|
||||
markdownClasses={markdownClasses}
|
||||
/>
|
||||
)}
|
||||
@ -54,5 +74,5 @@ export const MessageRow = React.forwardRef(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
MessageRow.displayName = "MessageRow";
|
||||
|
@ -0,0 +1,67 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { FaQuestionCircle } from 'react-icons/fa';
|
||||
|
||||
type SourcesButtonProps = {
|
||||
sources: string;
|
||||
};
|
||||
|
||||
export const SourcesButton = ({ sources }: SourcesButtonProps): JSX.Element => {
|
||||
const [showSources, setShowSources] = useState(false);
|
||||
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 });
|
||||
// Specify the type of element the ref will be attached to
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const updatePopupPosition = () => {
|
||||
// Use the 'current' property of the ref with the correct type
|
||||
if (buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setPopupPosition({
|
||||
top: rect.bottom + window.scrollY,
|
||||
left: rect.left + window.scrollX,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', updatePopupPosition);
|
||||
|
||||
// Remove the event listener when the component is unmounted
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updatePopupPosition);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sourcesList = (
|
||||
<ul className="list-disc list-inside">
|
||||
{sources.split(', ').map((source, index) => (
|
||||
<li key={index} className="truncate">{source.trim()}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block">
|
||||
<button
|
||||
ref={buttonRef} // Attach the ref to the button
|
||||
onMouseEnter={() => {
|
||||
setShowSources(true);
|
||||
updatePopupPosition();
|
||||
}}
|
||||
onMouseLeave={() => setShowSources(false)}
|
||||
className="text-gray-500 hover:text-gray-700 transition p-1"
|
||||
title="View sources"
|
||||
>
|
||||
<FaQuestionCircle />
|
||||
</button>
|
||||
{showSources && ReactDOM.createPortal(
|
||||
<div className="absolute z-50 min-w-max p-2 bg-white shadow-lg rounded-md border border-gray-200"
|
||||
style={{ top: `${popupPosition.top}px`, left: `${popupPosition.left}px` }}>
|
||||
{/* Display the sources list here */}
|
||||
{sourcesList}
|
||||
</div>,
|
||||
document.body // Render the popup to the body element
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user