mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-23 03:42:59 -05:00
170 lines
5.6 KiB
Python
170 lines
5.6 KiB
Python
import re
|
|
from collections.abc import Sequence
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from danswer.chat.models import CitationInfo
|
|
from danswer.chat.models import LlmDoc
|
|
from danswer.db.chat import get_chat_messages_by_session
|
|
from danswer.db.models import ChatMessage
|
|
from danswer.search.models import InferenceChunk
|
|
from danswer.search.models import InferenceSection
|
|
from danswer.utils.logger import setup_logger
|
|
|
|
logger = setup_logger()
|
|
|
|
|
|
def llm_doc_from_inference_section(inf_chunk: InferenceSection) -> LlmDoc:
|
|
return LlmDoc(
|
|
document_id=inf_chunk.document_id,
|
|
# This one is using the combined content of all the chunks of the section
|
|
# In default settings, this is the same as just the content of base chunk
|
|
content=inf_chunk.combined_content,
|
|
blurb=inf_chunk.blurb,
|
|
semantic_identifier=inf_chunk.semantic_identifier,
|
|
source_type=inf_chunk.source_type,
|
|
metadata=inf_chunk.metadata,
|
|
updated_at=inf_chunk.updated_at,
|
|
link=inf_chunk.source_links[0] if inf_chunk.source_links else None,
|
|
source_links=inf_chunk.source_links,
|
|
)
|
|
|
|
|
|
def map_document_id_order(
|
|
chunks: Sequence[InferenceChunk | LlmDoc], one_indexed: bool = True
|
|
) -> dict[str, int]:
|
|
order_mapping = {}
|
|
current = 1 if one_indexed else 0
|
|
for chunk in chunks:
|
|
if chunk.document_id not in order_mapping:
|
|
order_mapping[chunk.document_id] = current
|
|
current += 1
|
|
|
|
return order_mapping
|
|
|
|
|
|
def create_chat_chain(
|
|
chat_session_id: int,
|
|
db_session: Session,
|
|
) -> tuple[ChatMessage, list[ChatMessage]]:
|
|
"""Build the linear chain of messages without including the root message"""
|
|
mainline_messages: list[ChatMessage] = []
|
|
all_chat_messages = get_chat_messages_by_session(
|
|
chat_session_id=chat_session_id,
|
|
user_id=None,
|
|
db_session=db_session,
|
|
skip_permission_check=True,
|
|
)
|
|
id_to_msg = {msg.id: msg for msg in all_chat_messages}
|
|
|
|
if not all_chat_messages:
|
|
raise RuntimeError("No messages in Chat Session")
|
|
|
|
root_message = all_chat_messages[0]
|
|
if root_message.parent_message is not None:
|
|
raise RuntimeError(
|
|
"Invalid root message, unable to fetch valid chat message sequence"
|
|
)
|
|
|
|
current_message: ChatMessage | None = root_message
|
|
while current_message is not None:
|
|
child_msg = current_message.latest_child_message
|
|
if not child_msg:
|
|
break
|
|
current_message = id_to_msg.get(child_msg)
|
|
|
|
if current_message is None:
|
|
raise RuntimeError(
|
|
"Invalid message chain,"
|
|
"could not find next message in the same session"
|
|
)
|
|
|
|
mainline_messages.append(current_message)
|
|
|
|
if not mainline_messages:
|
|
raise RuntimeError("Could not trace chat message history")
|
|
|
|
return mainline_messages[-1], mainline_messages[:-1]
|
|
|
|
|
|
def combine_message_chain(
|
|
messages: list[ChatMessage],
|
|
token_limit: int,
|
|
msg_limit: int | None = None,
|
|
) -> str:
|
|
"""Used for secondary LLM flows that require the chat history,"""
|
|
message_strs: list[str] = []
|
|
total_token_count = 0
|
|
|
|
if msg_limit is not None:
|
|
messages = messages[-msg_limit:]
|
|
|
|
for message in reversed(messages):
|
|
message_token_count = message.token_count
|
|
|
|
if total_token_count + message_token_count > token_limit:
|
|
break
|
|
|
|
role = message.message_type.value.upper()
|
|
message_strs.insert(0, f"{role}:\n{message.message}")
|
|
total_token_count += message_token_count
|
|
|
|
return "\n\n".join(message_strs)
|
|
|
|
|
|
def reorganize_citations(
|
|
answer: str, citations: list[CitationInfo]
|
|
) -> tuple[str, list[CitationInfo]]:
|
|
"""For a complete, citation-aware response, we want to reorganize the citations so that
|
|
they are in the order of the documents that were used in the response. This just looks nicer / avoids
|
|
confusion ("Why is there [7] when only 2 documents are cited?")."""
|
|
|
|
# Regular expression to find all instances of [[x]](LINK)
|
|
pattern = r"\[\[(.*?)\]\]\((.*?)\)"
|
|
|
|
all_citation_matches = re.findall(pattern, answer)
|
|
|
|
new_citation_info: dict[int, CitationInfo] = {}
|
|
for citation_match in all_citation_matches:
|
|
try:
|
|
citation_num = int(citation_match[0])
|
|
if citation_num in new_citation_info:
|
|
continue
|
|
|
|
matching_citation = next(
|
|
iter([c for c in citations if c.citation_num == int(citation_num)]),
|
|
None,
|
|
)
|
|
if matching_citation is None:
|
|
continue
|
|
|
|
new_citation_info[citation_num] = CitationInfo(
|
|
citation_num=len(new_citation_info) + 1,
|
|
document_id=matching_citation.document_id,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
# Function to replace citations with their new number
|
|
def slack_link_format(match: re.Match) -> str:
|
|
link_text = match.group(1)
|
|
try:
|
|
citation_num = int(link_text)
|
|
if citation_num in new_citation_info:
|
|
link_text = new_citation_info[citation_num].citation_num
|
|
except Exception:
|
|
pass
|
|
|
|
link_url = match.group(2)
|
|
return f"[[{link_text}]]({link_url})"
|
|
|
|
# Substitute all matches in the input text
|
|
new_answer = re.sub(pattern, slack_link_format, answer)
|
|
|
|
# if any citations weren't parsable, just add them back to be safe
|
|
for citation in citations:
|
|
if citation.citation_num not in new_citation_info:
|
|
new_citation_info[citation.citation_num] = citation
|
|
|
|
return new_answer, list(new_citation_info.values())
|