Added inline citations and updated sources display as per new data format

This commit is contained in:
Utkarsh-Patel-13 2025-07-25 15:11:19 -07:00
parent 2b647a9e7d
commit 1318feef66
7 changed files with 349 additions and 204 deletions

View file

@ -6,19 +6,20 @@ from typing import Any, Dict
from .prompts import get_qna_citation_system_prompt, get_qna_no_documents_system_prompt
from langchain_core.messages import HumanMessage, SystemMessage
from ..utils import (
optimize_documents_for_token_limit,
optimize_documents_for_token_limit,
calculate_token_count,
format_documents_section
)
format_documents_section,
)
async def rerank_documents(state: State, config: RunnableConfig) -> Dict[str, Any]:
"""
Rerank the documents based on relevance to the user's question.
This node takes the relevant documents provided in the configuration,
reranks them using the reranker service based on the user's query,
and updates the state with the reranked documents.
Returns:
Dict containing the reranked documents.
"""
@ -30,16 +31,14 @@ async def rerank_documents(state: State, config: RunnableConfig) -> Dict[str, An
# If no documents were provided, return empty list
if not documents or len(documents) == 0:
return {
"reranked_documents": []
}
return {"reranked_documents": []}
# Get reranker service from app config
reranker_service = RerankerService.get_reranker_instance()
# Use documents as is if no reranker service is available
reranked_docs = documents
if reranker_service:
try:
# Convert documents to format expected by reranker if needed
@ -51,58 +50,64 @@ async def rerank_documents(state: State, config: RunnableConfig) -> Dict[str, An
"document": {
"id": doc.get("document", {}).get("id", ""),
"title": doc.get("document", {}).get("title", ""),
"document_type": doc.get("document", {}).get("document_type", ""),
"metadata": doc.get("document", {}).get("metadata", {})
}
} for i, doc in enumerate(documents)
"document_type": doc.get("document", {}).get(
"document_type", ""
),
"metadata": doc.get("document", {}).get("metadata", {}),
},
}
for i, doc in enumerate(documents)
]
# Rerank documents using the user's query
reranked_docs = reranker_service.rerank_documents(user_query + "\n" + reformulated_query, reranker_input_docs)
reranked_docs = reranker_service.rerank_documents(
user_query + "\n" + reformulated_query, reranker_input_docs
)
# Sort by score in descending order
reranked_docs.sort(key=lambda x: x.get("score", 0), reverse=True)
print(f"Reranked {len(reranked_docs)} documents for Q&A query: {user_query}")
print(
f"Reranked {len(reranked_docs)} documents for Q&A query: {user_query}"
)
except Exception as e:
print(f"Error during reranking: {str(e)}")
# Use original docs if reranking fails
return {
"reranked_documents": reranked_docs
}
return {"reranked_documents": reranked_docs}
async def answer_question(state: State, config: RunnableConfig) -> Dict[str, Any]:
"""
Answer the user's question using the provided documents.
This node takes the relevant documents provided in the configuration and uses
an LLM to generate a comprehensive answer to the user's question with
proper citations. The citations follow IEEE format using source IDs from the
proper citations. The citations follow [citation:source_id] format using source IDs from the
documents. If no documents are provided, it will use chat history to generate
an answer.
Returns:
Dict containing the final answer in the "final_answer" key.
"""
from app.services.llm_service import get_user_fast_llm
# Get configuration and relevant documents from configuration
configuration = Configuration.from_runnable_config(config)
documents = state.reranked_documents
user_query = configuration.user_query
user_id = configuration.user_id
# Get user's fast LLM
llm = await get_user_fast_llm(state.db_session, user_id)
if not llm:
error_message = f"No fast LLM configured for user {user_id}"
print(error_message)
raise RuntimeError(error_message)
# Determine if we have documents and optimize for token limits
has_documents_initially = documents and len(documents) > 0
if has_documents_initially:
# Create base message template for token calculation (without documents)
base_human_message_template = f"""
@ -114,41 +119,48 @@ async def answer_question(state: State, config: RunnableConfig) -> Dict[str, Any
Please provide a detailed, comprehensive answer to the user's question using the information from their personal knowledge sources. Make sure to cite all information appropriately and engage in a conversational manner.
"""
# Use initial system prompt for token calculation
initial_system_prompt = get_qna_citation_system_prompt()
base_messages = state.chat_history + [
SystemMessage(content=initial_system_prompt),
HumanMessage(content=base_human_message_template)
HumanMessage(content=base_human_message_template),
]
# Optimize documents to fit within token limits
optimized_documents, has_optimized_documents = optimize_documents_for_token_limit(
documents, base_messages, llm.model
optimized_documents, has_optimized_documents = (
optimize_documents_for_token_limit(documents, base_messages, llm.model)
)
# Update state based on optimization result
documents = optimized_documents
has_documents = has_optimized_documents
else:
has_documents = False
# Choose system prompt based on final document availability
system_prompt = get_qna_citation_system_prompt() if has_documents else get_qna_no_documents_system_prompt()
system_prompt = (
get_qna_citation_system_prompt()
if has_documents
else get_qna_no_documents_system_prompt()
)
# Generate documents section
documents_text = format_documents_section(
documents,
"Source material from your personal knowledge base"
) if has_documents else ""
documents_text = (
format_documents_section(
documents, "Source material from your personal knowledge base"
)
if has_documents
else ""
)
# Create final human message content
instruction_text = (
"Please provide a detailed, comprehensive answer to the user's question using the information from their personal knowledge sources. Make sure to cite all information appropriately and engage in a conversational manner."
if has_documents else
"Please provide a helpful answer to the user's question based on our conversation history and your general knowledge. Engage in a conversational manner."
if has_documents
else "Please provide a helpful answer to the user's question based on our conversation history and your general knowledge. Engage in a conversational manner."
)
human_message_content = f"""
{documents_text}
@ -159,22 +171,19 @@ async def answer_question(state: State, config: RunnableConfig) -> Dict[str, Any
{instruction_text}
"""
# Create final messages for the LLM
messages_with_chat_history = state.chat_history + [
SystemMessage(content=system_prompt),
HumanMessage(content=human_message_content)
HumanMessage(content=human_message_content),
]
# Log final token count
total_tokens = calculate_token_count(messages_with_chat_history, llm.model)
print(f"Final token count: {total_tokens}")
# Call the LLM and get the response
response = await llm.ainvoke(messages_with_chat_history)
final_answer = response.content
return {
"final_answer": final_answer
}
return {"final_answer": final_answer}

View file

@ -24,21 +24,21 @@ You are SurfSense, an advanced AI research assistant that provides detailed, wel
1. Carefully analyze all provided documents in the <document> sections.
2. Extract relevant information that directly addresses the user's question.
3. Provide a comprehensive, detailed answer using information from the user's personal knowledge sources.
4. For EVERY piece of information you include from the documents, add an IEEE-style citation in square brackets [X] where X is the source_id from the document's metadata.
4. For EVERY piece of information you include from the documents, add a citation in the format [citation:knowledge_source_id] where knowledge_source_id is the source_id from the document's metadata.
5. Make sure ALL factual statements from the documents have proper citations.
6. If multiple documents support the same point, include all relevant citations [X], [Y].
6. If multiple documents support the same point, include all relevant citations [citation:source_id1], [citation:source_id2].
7. Structure your answer logically and conversationally, as if having a detailed discussion with the user.
8. Use your own words to synthesize and connect ideas, but cite ALL information from the documents.
9. If documents contain conflicting information, acknowledge this and present both perspectives with appropriate citations.
10. If the user's question cannot be fully answered with the provided documents, clearly state what information is missing.
11. Provide actionable insights and practical information when relevant to the user's question.
12. CRITICAL: You MUST use the exact source_id value from each document's metadata for citations. Do not create your own citation numbers.
13. CRITICAL: Every citation MUST be in the IEEE format [X] where X is the exact source_id value.
14. CRITICAL: Never renumber or reorder citations - always use the original source_id values.
13. CRITICAL: Every citation MUST be in the format [citation:knowledge_source_id] where knowledge_source_id is the exact source_id value.
14. CRITICAL: Never modify or change the source_id - always use the original values exactly as provided in the metadata.
15. CRITICAL: Do not return citations as clickable links.
16. CRITICAL: Never format citations as markdown links like "([1](https://example.com))". Always use plain square brackets only.
17. CRITICAL: Citations must ONLY appear as [X] or [X], [Y], [Z] format - never with parentheses, hyperlinks, or other formatting.
18. CRITICAL: Never make up citation numbers. Only use source_id values that are explicitly provided in the document metadata.
16. CRITICAL: Never format citations as markdown links like "([citation:5](https://example.com))". Always use plain square brackets only.
17. CRITICAL: Citations must ONLY appear as [citation:source_id] or [citation:source_id1], [citation:source_id2] format - never with parentheses, hyperlinks, or other formatting.
18. CRITICAL: Never make up source IDs. Only use source_id values that are explicitly provided in the document metadata.
19. CRITICAL: If you are unsure about a source_id, do not include a citation rather than guessing or making one up.
20. CRITICAL: Remember that all knowledge sources contain personal information - provide answers that reflect this personal context.
21. CRITICAL: Be conversational and engaging while maintaining accuracy and proper citations.
@ -48,13 +48,13 @@ You are SurfSense, an advanced AI research assistant that provides detailed, wel
- Write in a clear, conversational tone suitable for detailed Q&A discussions
- Provide comprehensive answers that thoroughly address the user's question
- Use appropriate paragraphs and structure for readability
- Every fact from the documents must have an IEEE-style citation in square brackets [X] where X is the EXACT source_id from the document's metadata
- Every fact from the documents must have a citation in the format [citation:knowledge_source_id] where knowledge_source_id is the EXACT source_id from the document's metadata
- Citations should appear at the end of the sentence containing the information they support
- Multiple citations should be separated by commas: [X], [Y], [Z]
- No need to return references section. Just citation numbers in answer.
- NEVER create your own citation numbering system - use the exact source_id values from the documents
- NEVER format citations as clickable links or as markdown links like "([1](https://example.com))". Always use plain square brackets only
- NEVER make up citation numbers if you are unsure about the source_id. It is better to omit the citation than to guess
- Multiple citations should be separated by commas: [citation:source_id1], [citation:source_id2], [citation:source_id3]
- No need to return references section. Just citations in answer.
- NEVER create your own citation format - use the exact source_id values from the documents in the [citation:source_id] format
- NEVER format citations as clickable links or as markdown links like "([citation:5](https://example.com))". Always use plain square brackets only
- NEVER make up source IDs if you are unsure about the source_id. It is better to omit the citation than to guess
- ALWAYS provide personalized answers that reflect the user's own knowledge and context
- Be thorough and detailed in your explanations while remaining focused on the user's specific question
- If asking follow-up questions would be helpful, suggest them at the end of your response
@ -87,26 +87,31 @@ User Question: "How does Python asyncio work and when should I use it?"
</input_example>
<output_example>
Based on your GitHub repositories and video content, Python's asyncio library provides tools for writing concurrent code using the async/await syntax [5]. It's particularly useful for I/O-bound and high-level structured network code [5].
Based on your GitHub repositories and video content, Python's asyncio library provides tools for writing concurrent code using the async/await syntax [citation:5]. It's particularly useful for I/O-bound and high-level structured network code [citation:5].
The key advantage of asyncio is that it can improve performance by allowing other code to run while waiting for I/O operations to complete [12]. This makes it excellent for scenarios like web scraping, API calls, database operations, or any situation where your program spends time waiting for external resources.
The key advantage of asyncio is that it can improve performance by allowing other code to run while waiting for I/O operations to complete [citation:12]. This makes it excellent for scenarios like web scraping, API calls, database operations, or any situation where your program spends time waiting for external resources.
However, from your video learning, it's important to note that asyncio is not suitable for CPU-bound tasks as it runs on a single thread [12]. For computationally intensive work, you'd want to use multiprocessing instead.
However, from your video learning, it's important to note that asyncio is not suitable for CPU-bound tasks as it runs on a single thread [citation:12]. For computationally intensive work, you'd want to use multiprocessing instead.
Would you like me to explain more about specific asyncio patterns or help you determine if asyncio is right for a particular project you're working on?
</output_example>
<incorrect_citation_formats>
DO NOT use any of these incorrect citation formats:
- Using parentheses and markdown links: ([1](https://github.com/MODSetter/SurfSense))
- Using parentheses around brackets: ([1])
- Using hyperlinked text: [link to source 1](https://example.com)
- Using parentheses and markdown links: ([citation:5](https://github.com/MODSetter/SurfSense))
- Using parentheses around brackets: ([citation:5])
- Using hyperlinked text: [link to source 5](https://example.com)
- Using footnote style: ... library¹
- Making up citation numbers when source_id is unknown
- Making up source IDs when source_id is unknown
- Using old IEEE format: [1], [2], [3]
- Using source types instead of IDs: [citation:GITHUB_CONNECTOR] instead of [citation:5]
ONLY use plain square brackets [1] or multiple citations [1], [2], [3]
</incorrect_citation_formats>
<correct_citation_formats>
ONLY use the format [citation:source_id] or multiple citations [citation:source_id1], [citation:source_id2], [citation:source_id3]
</correct_citation_formats>
<user_query_instructions>
When you see a user query, focus exclusively on providing a detailed, comprehensive answer using information from the provided documents, which contain the user's personal knowledge and data.

View file

@ -7,19 +7,20 @@ from .prompts import get_citation_system_prompt, get_no_documents_system_prompt
from langchain_core.messages import HumanMessage, SystemMessage
from .configuration import SubSectionType
from ..utils import (
optimize_documents_for_token_limit,
optimize_documents_for_token_limit,
calculate_token_count,
format_documents_section
format_documents_section,
)
async def rerank_documents(state: State, config: RunnableConfig) -> Dict[str, Any]:
"""
Rerank the documents based on relevance to the sub-section title.
This node takes the relevant documents provided in the configuration,
reranks them using the reranker service based on the sub-section title,
and updates the state with the reranked documents.
Returns:
Dict containing the reranked documents.
"""
@ -30,23 +31,23 @@ async def rerank_documents(state: State, config: RunnableConfig) -> Dict[str, An
# If no documents were provided, return empty list
if not documents or len(documents) == 0:
return {
"reranked_documents": []
}
return {"reranked_documents": []}
# Get reranker service from app config
reranker_service = RerankerService.get_reranker_instance()
# Use documents as is if no reranker service is available
reranked_docs = documents
if reranker_service:
try:
# Use the sub-section questions for reranking context
# rerank_query = "\n".join(sub_section_questions)
# rerank_query = configuration.user_query
rerank_query = configuration.user_query + "\n" + "\n".join(sub_section_questions)
rerank_query = (
configuration.user_query + "\n" + "\n".join(sub_section_questions)
)
# Convert documents to format expected by reranker if needed
reranker_input_docs = [
@ -57,54 +58,60 @@ async def rerank_documents(state: State, config: RunnableConfig) -> Dict[str, An
"document": {
"id": doc.get("document", {}).get("id", ""),
"title": doc.get("document", {}).get("title", ""),
"document_type": doc.get("document", {}).get("document_type", ""),
"metadata": doc.get("document", {}).get("metadata", {})
}
} for i, doc in enumerate(documents)
"document_type": doc.get("document", {}).get(
"document_type", ""
),
"metadata": doc.get("document", {}).get("metadata", {}),
},
}
for i, doc in enumerate(documents)
]
# Rerank documents using the section title
reranked_docs = reranker_service.rerank_documents(rerank_query, reranker_input_docs)
reranked_docs = reranker_service.rerank_documents(
rerank_query, reranker_input_docs
)
# Sort by score in descending order
reranked_docs.sort(key=lambda x: x.get("score", 0), reverse=True)
print(f"Reranked {len(reranked_docs)} documents for section: {configuration.sub_section_title}")
print(
f"Reranked {len(reranked_docs)} documents for section: {configuration.sub_section_title}"
)
except Exception as e:
print(f"Error during reranking: {str(e)}")
# Use original docs if reranking fails
return {
"reranked_documents": reranked_docs
}
return {"reranked_documents": reranked_docs}
async def write_sub_section(state: State, config: RunnableConfig) -> Dict[str, Any]:
"""
Write the sub-section using the provided documents.
This node takes the relevant documents provided in the configuration and uses
an LLM to generate a comprehensive answer to the sub-section title with
proper citations. The citations follow IEEE format using source IDs from the
proper citations. The citations follow [citation:source_id] format using source IDs from the
documents. If no documents are provided, it will use chat history to generate
content.
Returns:
Dict containing the final answer in the "final_answer" key.
"""
from app.services.llm_service import get_user_fast_llm
# Get configuration and relevant documents from configuration
configuration = Configuration.from_runnable_config(config)
documents = state.reranked_documents
user_id = configuration.user_id
# Get user's fast LLM
llm = await get_user_fast_llm(state.db_session, user_id)
if not llm:
error_message = f"No fast LLM configured for user {user_id}"
print(error_message)
raise RuntimeError(error_message)
# Extract configuration data
section_title = configuration.sub_section_title
sub_section_questions = configuration.sub_section_questions
@ -113,18 +120,18 @@ async def write_sub_section(state: State, config: RunnableConfig) -> Dict[str, A
# Format the questions as bullet points for clarity
questions_text = "\n".join([f"- {question}" for question in sub_section_questions])
# Provide context based on the subsection type
section_position_context_map = {
SubSectionType.START: "This is the INTRODUCTION section.",
SubSectionType.MIDDLE: "This is a MIDDLE section. Ensure this content flows naturally from previous sections and into subsequent ones. This could be any middle section in the document, so maintain coherence with the overall structure while addressing the specific topic of this section. Do not provide any conclusions in this section, as conclusions should only appear in the final section.",
SubSectionType.END: "This is the CONCLUSION section. Focus on summarizing key points, providing closure."
SubSectionType.END: "This is the CONCLUSION section. Focus on summarizing key points, providing closure.",
}
section_position_context = section_position_context_map.get(sub_section_type, "")
# Determine if we have documents and optimize for token limits
has_documents_initially = documents and len(documents) > 0
if has_documents_initially:
# Create base message template for token calculation (without documents)
base_human_message_template = f"""
@ -149,38 +156,44 @@ async def write_sub_section(state: State, config: RunnableConfig) -> Dict[str, A
Please write content for this sub-section using the provided source material and cite all information appropriately.
"""
# Use initial system prompt for token calculation
initial_system_prompt = get_citation_system_prompt()
base_messages = state.chat_history + [
SystemMessage(content=initial_system_prompt),
HumanMessage(content=base_human_message_template)
HumanMessage(content=base_human_message_template),
]
# Optimize documents to fit within token limits
optimized_documents, has_optimized_documents = optimize_documents_for_token_limit(
documents, base_messages, llm.model
optimized_documents, has_optimized_documents = (
optimize_documents_for_token_limit(documents, base_messages, llm.model)
)
# Update state based on optimization result
documents = optimized_documents
has_documents = has_optimized_documents
else:
has_documents = False
# Choose system prompt based on final document availability
system_prompt = get_citation_system_prompt() if has_documents else get_no_documents_system_prompt()
system_prompt = (
get_citation_system_prompt()
if has_documents
else get_no_documents_system_prompt()
)
# Generate documents section
documents_text = format_documents_section(documents, "Source material") if has_documents else ""
documents_text = (
format_documents_section(documents, "Source material") if has_documents else ""
)
# Create final human message content
instruction_text = (
"Please write content for this sub-section using the provided source material and cite all information appropriately."
if has_documents else
"Please write content for this sub-section based on our conversation history and your general knowledge."
if has_documents
else "Please write content for this sub-section based on our conversation history and your general knowledge."
)
human_message_content = f"""
{documents_text}
@ -204,22 +217,19 @@ async def write_sub_section(state: State, config: RunnableConfig) -> Dict[str, A
{instruction_text}
"""
# Create final messages for the LLM
messages_with_chat_history = state.chat_history + [
SystemMessage(content=system_prompt),
HumanMessage(content=human_message_content)
HumanMessage(content=human_message_content),
]
# Log final token count
total_tokens = calculate_token_count(messages_with_chat_history, llm.model)
print(f"Final token count: {total_tokens}")
# Call the LLM and get the response
response = await llm.ainvoke(messages_with_chat_history)
final_answer = response.content
return {
"final_answer": final_answer
}
return {"final_answer": final_answer}

View file

@ -23,20 +23,20 @@ You are SurfSense, an advanced AI research assistant that synthesizes informatio
1. Carefully analyze all provided documents in the <document> section's.
2. Extract relevant information that addresses the user's query.
3. Synthesize a comprehensive, personalized answer using information from the user's personal knowledge sources.
4. For EVERY piece of information you include from the documents, add an IEEE-style citation in square brackets [X] where X is the source_id from the document's metadata.
4. For EVERY piece of information you include from the documents, add a citation in the format [citation:knowledge_source_id] where knowledge_source_id is the source_id from the document's metadata.
5. Make sure ALL factual statements from the documents have proper citations.
6. If multiple documents support the same point, include all relevant citations [X], [Y].
6. If multiple documents support the same point, include all relevant citations [citation:source_id1], [citation:source_id2].
7. Present information in a logical, coherent flow that reflects the user's personal context.
8. Use your own words to connect ideas, but cite ALL information from the documents.
9. If documents contain conflicting information, acknowledge this and present both perspectives with appropriate citations.
10. Do not make up or include information not found in the provided documents.
11. CRITICAL: You MUST use the exact source_id value from each document's metadata for citations. Do not create your own citation numbers.
12. CRITICAL: Every citation MUST be in the IEEE format [X] where X is the exact source_id value.
13. CRITICAL: Never renumber or reorder citations - always use the original source_id values.
12. CRITICAL: Every citation MUST be in the format [citation:knowledge_source_id] where knowledge_source_id is the exact source_id value.
13. CRITICAL: Never modify or change the source_id - always use the original values exactly as provided in the metadata.
14. CRITICAL: Do not return citations as clickable links.
15. CRITICAL: Never format citations as markdown links like "([1](https://example.com))". Always use plain square brackets only.
16. CRITICAL: Citations must ONLY appear as [X] or [X], [Y], [Z] format - never with parentheses, hyperlinks, or other formatting.
17. CRITICAL: Never make up citation numbers. Only use source_id values that are explicitly provided in the document metadata.
15. CRITICAL: Never format citations as markdown links like "([citation:5](https://example.com))". Always use plain square brackets only.
16. CRITICAL: Citations must ONLY appear as [citation:source_id] or [citation:source_id1], [citation:source_id2] format - never with parentheses, hyperlinks, or other formatting.
17. CRITICAL: Never make up source IDs. Only use source_id values that are explicitly provided in the document metadata.
18. CRITICAL: If you are unsure about a source_id, do not include a citation rather than guessing or making one up.
19. CRITICAL: Focus only on answering the user's query. Any guiding questions provided are for your thinking process only and should not be mentioned in your response.
20. CRITICAL: Ensure your response aligns with the provided sub-section title and section position.
@ -47,13 +47,13 @@ You are SurfSense, an advanced AI research assistant that synthesizes informatio
- Write in clear, professional language suitable for academic or technical audiences
- Tailor your response to the user's personal context based on their knowledge sources
- Organize your response with appropriate paragraphs, headings, and structure
- Every fact from the documents must have an IEEE-style citation in square brackets [X] where X is the EXACT source_id from the document's metadata
- Every fact from the documents must have a citation in the format [citation:knowledge_source_id] where knowledge_source_id is the EXACT source_id from the document's metadata
- Citations should appear at the end of the sentence containing the information they support
- Multiple citations should be separated by commas: [X], [Y], [Z]
- No need to return references section. Just citation numbers in answer.
- NEVER create your own citation numbering system - use the exact source_id values from the documents.
- NEVER format citations as clickable links or as markdown links like "([1](https://example.com))". Always use plain square brackets only.
- NEVER make up citation numbers if you are unsure about the source_id. It is better to omit the citation than to guess.
- Multiple citations should be separated by commas: [citation:source_id1], [citation:source_id2], [citation:source_id3]
- No need to return references section. Just citations in answer.
- NEVER create your own citation format - use the exact source_id values from the documents in the [citation:source_id] format.
- NEVER format citations as clickable links or as markdown links like "([citation:5](https://example.com))". Always use plain square brackets only.
- NEVER make up source IDs if you are unsure about the source_id. It is better to omit the citation than to guess.
- NEVER include or mention the guiding questions in your response. They are only to help guide your thinking.
- ALWAYS focus on answering the user's query directly from the information in the documents.
- ALWAYS provide personalized answers that reflect the user's own knowledge and context.
@ -94,21 +94,23 @@ You are SurfSense, an advanced AI research assistant that synthesizes informatio
</input_example>
<output_example>
Based on your saved browser content and videos, the Great Barrier Reef is the world's largest coral reef system, stretching over 2,300 kilometers along the coast of Queensland, Australia [1]. From your browsing history, you've looked into its designation as a UNESCO World Heritage Site in 1981 due to its outstanding universal value and biological diversity [21]. The reef is home to over 1,500 species of fish and 400 types of coral [21]. According to a YouTube video you've watched, climate change poses a significant threat to coral reefs worldwide, with rising ocean temperatures leading to mass coral bleaching events in the Great Barrier Reef in 2016, 2017, and 2020 [13]. The reef system comprises over 2,900 individual reefs and 900 islands [1], making it an ecological treasure that requires protection from multiple threats [1], [13].
Based on your saved browser content and videos, the Great Barrier Reef is the world's largest coral reef system, stretching over 2,300 kilometers along the coast of Queensland, Australia [citation:1]. From your browsing history, you've looked into its designation as a UNESCO World Heritage Site in 1981 due to its outstanding universal value and biological diversity [citation:21]. The reef is home to over 1,500 species of fish and 400 types of coral [citation:21]. According to a YouTube video you've watched, climate change poses a significant threat to coral reefs worldwide, with rising ocean temperatures leading to mass coral bleaching events in the Great Barrier Reef in 2016, 2017, and 2020 [citation:13]. The reef system comprises over 2,900 individual reefs and 900 islands [citation:1], making it an ecological treasure that requires protection from multiple threats [citation:1], [citation:13].
</output_example>
<incorrect_citation_formats>
DO NOT use any of these incorrect citation formats:
- Using parentheses and markdown links: ([1](https://github.com/MODSetter/SurfSense))
- Using parentheses around brackets: ([1])
- Using parentheses and markdown links: ([citation:1](https://github.com/MODSetter/SurfSense))
- Using parentheses around brackets: ([citation:1])
- Using hyperlinked text: [link to source 1](https://example.com)
- Using footnote style: ... reef system¹
- Making up citation numbers when source_id is unknown
- Making up source IDs when source_id is unknown
- Using old IEEE format: [1], [2], [3]
- Using source types instead of IDs: [citation:EXTENSION] instead of [citation:1]
ONLY use plain square brackets [1] or multiple citations [1], [2], [3]
</incorrect_citation_formats>
Note that the citation numbers match exactly with the source_id values (1, 13, and 21) and are not renumbered sequentially. Citations follow IEEE style with square brackets and appear at the end of sentences.
ONLY use the format [citation:source_id] or multiple citations [citation:source_id1], [citation:source_id2], [citation:source_id3]
Note that the citations use the exact source_id values (1, 13, and 21) from the document metadata. Citations appear at the end of sentences and maintain the new citation format.
<user_query_instructions>
When you see a user query like:
@ -182,4 +184,4 @@ When writing content for a sub-section without access to personal documents:
5. Address the guiding questions through natural content flow without explicitly listing them
6. Suggest how adding relevant sources to SurfSense could enhance future content when appropriate
</user_query_instructions>
"""
"""

View file

@ -60,7 +60,23 @@ class StreamingService:
self.message_annotations[1]["content"] = sources
# Return only the delta annotation
annotation = {"type": "SOURCES", "data": sources}
nodes = []
for group in sources:
for source in group.get("sources", []):
node = {
"id": str(source.get("id", "")),
"text": source.get("description", ""),
"url": source.get("url", ""),
"metadata": {
"title": source.get("title", ""),
"source_type": group.get("type", ""),
"group_name": group.get("name", ""),
},
}
nodes.append(node)
annotation = {"type": "sources", "data": {"nodes": nodes}}
return f"8:[{json.dumps(annotation)}]\n"
def format_answer_delta(self, answer_chunk: str) -> str:

View file

@ -9,12 +9,15 @@ import {
useChatUI,
ChatMessage,
Message,
getAnnotationData,
} from "@llamaindex/chat-ui";
import { Document } from "@/hooks/use-documents";
import { CustomChatInput } from "@/components/chat_v2/ChatInputGroup";
import { ResearchMode } from "@/components/chat";
import TerminalDisplay from "@/components/chat_v2/ChatTerminal";
import ChatSourcesDisplay from "@/components/chat_v2/ChatSources";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ExternalLink } from "lucide-react";
interface ChatInterfaceProps {
handler: ChatHandler;
@ -28,6 +31,59 @@ interface ChatInterfaceProps {
onResearchModeChange?: (mode: ResearchMode) => void;
}
const CitationDisplay: React.FC<{index: number, node: any}> = ({index, node}) => {
const truncateText = (text: string, maxLength: number = 200) => {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
const handleUrlClick = (e: React.MouseEvent, url: string) => {
e.preventDefault();
e.stopPropagation();
window.open(url, '_blank', 'noopener,noreferrer');
};
return (
<Popover >
<PopoverTrigger asChild >
<span className="text-[10px] font-bold bg-slate-500 hover:bg-slate-600 text-white rounded-full w-4 h-4 inline-flex items-center justify-center align-super cursor-pointer transition-colors">
{index + 1}
</span>
</PopoverTrigger>
<PopoverContent className="w-80 p-4 space-y-3 relative" align="start" >
{/* External Link Button - Top Right */}
{node?.url && (
<button
onClick={(e) => handleUrlClick(e, node.url)}
className="absolute top-3 right-3 inline-flex items-center justify-center w-6 h-6 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors"
title="Open in new tab"
>
<ExternalLink size={14} />
</button>
)}
{/* Heading */}
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100 pr-8">
{node?.metadata?.group_name || 'Source'}
</div>
{/* Source */}
<div className="text-xs text-slate-600 dark:text-slate-400 font-medium">
{node?.metadata?.title || 'Untitled'}
</div>
{/* Body */}
<div className="text-xs text-slate-700 dark:text-slate-300 leading-relaxed">
{truncateText(node?.text || 'No content available')}
</div>
</PopoverContent>
</Popover>
);
}
function ChatMessageDisplay({
message,
isLast,
@ -46,7 +102,7 @@ function ChatMessageDisplay({
<TerminalDisplay message={message} />
<ChatSourcesDisplay message={message} />
<ChatMessage.Content className="flex-1">
<ChatMessage.Content.Markdown />
<ChatMessage.Content.Markdown citationComponent={CitationDisplay} />
</ChatMessage.Content>
<ChatMessage.Actions className="flex-1 flex flex-row justify-end" />
</div>

View file

@ -19,10 +19,11 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ExternalLink, FileText, Github, Globe } from "lucide-react";
import { ExternalLink, FileText, Globe } from "lucide-react";
import { IconBrandGithub } from "@tabler/icons-react";
interface Source {
id: number;
id: string;
title: string;
description: string;
url: string;
@ -35,14 +36,34 @@ interface SourceGroup {
sources: Source[];
}
// New interfaces for the updated data format
interface NodeMetadata {
title: string;
source_type: string;
group_name: string;
}
interface SourceNode {
id: string;
text: string;
url: string;
metadata: NodeMetadata;
}
interface NodesResponse {
nodes: SourceNode[];
}
function getSourceIcon(type: string) {
switch (type) {
case "USER_SELECTED_GITHUB_CONNECTOR":
case "GITHUB_CONNECTOR":
return <Github className="h-4 w-4" />;
return <IconBrandGithub className="h-4 w-4" />;
case "USER_SELECTED_NOTION_CONNECTOR":
case "NOTION_CONNECTOR":
return <FileText className="h-4 w-4" />;
case "FILE":
case "USER_SELECTED_FILE":
case "FILE":
return <FileText className="h-4 w-4" />;
default:
return <Globe className="h-4 w-4" />;
@ -55,24 +76,24 @@ function SourceCard({ source }: { source: Source }) {
return (
<Card className="mb-3">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium truncate">
<div className="flex items-start justify-between gap-2">
<CardTitle className="text-sm md:text-base font-medium leading-tight">
{source.title}
</CardTitle>
{hasUrl && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
className="h-6 w-6 md:h-8 md:w-8 p-0 flex-shrink-0"
onClick={() => window.open(source.url, "_blank")}
>
<ExternalLink className="h-3 w-3" />
<ExternalLink className="h-3 w-3 md:h-4 md:w-4" />
</Button>
)}
</div>
</CardHeader>
<CardContent className="pt-0">
<CardDescription className="text-xs line-clamp-3">
<CardDescription className="text-xs md:text-sm line-clamp-3 md:line-clamp-4 leading-relaxed">
{source.description}
</CardDescription>
</CardContent>
@ -82,22 +103,49 @@ function SourceCard({ source }: { source: Source }) {
export default function ChatSourcesDisplay({ message }: { message: Message }) {
const [open, setOpen] = useState(false);
const annotations = getAnnotationData(message, "SOURCES");
const annotations = getAnnotationData(message, "sources");
// Flatten the nested array structure and ensure we have source groups
const sourceGroups: SourceGroup[] =
Array.isArray(annotations) && annotations.length > 0
? annotations
.flat()
.filter(
(group): group is SourceGroup =>
group !== null &&
group !== undefined &&
typeof group === "object" &&
"sources" in group &&
Array.isArray(group.sources)
)
: [];
// Transform the new data format to the expected SourceGroup format
const sourceGroups: SourceGroup[] = [];
if (Array.isArray(annotations) && annotations.length > 0) {
// Extract all nodes from the response
const allNodes: SourceNode[] = [];
annotations.forEach((item) => {
if (item && typeof item === "object" && "nodes" in item && Array.isArray(item.nodes)) {
allNodes.push(...item.nodes);
}
});
// Group nodes by source_type
const groupedByType = allNodes.reduce((acc, node) => {
const sourceType = node.metadata.source_type;
if (!acc[sourceType]) {
acc[sourceType] = [];
}
acc[sourceType].push(node);
return acc;
}, {} as Record<string, SourceNode[]>);
// Convert grouped nodes to SourceGroup format
Object.entries(groupedByType).forEach(([sourceType, nodes], index) => {
if (nodes.length > 0) {
const firstNode = nodes[0];
sourceGroups.push({
id: index + 100, // Generate unique ID
name: firstNode.metadata.group_name,
type: sourceType,
sources: nodes.map(node => ({
id: node.id,
title: node.metadata.title,
description: node.text,
url: node.url || ""
}))
});
}
});
}
if (sourceGroups.length === 0) {
return null;
@ -116,7 +164,7 @@ export default function ChatSourcesDisplay({ message }: { message: Message }) {
View Sources ({totalSources})
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl h-[80vh] flex flex-col">
<DialogContent className="max-w-4xl md:h-[80vh] h-[90vh] w-[95vw] md:w-auto flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>Sources</DialogTitle>
</DialogHeader>
@ -124,29 +172,28 @@ export default function ChatSourcesDisplay({ message }: { message: Message }) {
defaultValue={sourceGroups[0]?.type}
className="flex-1 flex flex-col min-h-0"
>
<TabsList
className="grid w-full flex-shrink-0"
style={{
gridTemplateColumns: `repeat(${sourceGroups.length}, 1fr)`,
}}
>
{sourceGroups.map((group) => (
<TabsTrigger
key={group.type}
value={group.type}
className="flex items-center gap-2"
>
{getSourceIcon(group.type)}
<span className="truncate">{group.name}</span>
<Badge
variant="secondary"
className="ml-1 h-5 text-xs"
<div className="flex-shrink-0 w-full overflow-x-auto">
<TabsList className="flex w-max min-w-full">
{sourceGroups.map((group) => (
<TabsTrigger
key={group.type}
value={group.type}
className="flex items-center gap-2 whitespace-nowrap px-3 md:px-4"
>
{group.sources.length}
</Badge>
</TabsTrigger>
))}
</TabsList>
{getSourceIcon(group.type)}
<span className="truncate max-w-[100px] md:max-w-none">
{group.name}
</span>
<Badge
variant="secondary"
className="ml-1 h-5 text-xs flex-shrink-0"
>
{group.sources.length}
</Badge>
</TabsTrigger>
))}
</TabsList>
</div>
{sourceGroups.map((group) => (
<TabsContent
key={group.type}