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:
Stan Girard 2023-11-06 12:09:18 +01:00 committed by GitHub
parent 8e179759ee
commit 192610d008
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 133 additions and 18 deletions

View File

@ -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)

View File

@ -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";

View File

@ -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>
);
};