Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions backend/app/rag/chat/chat_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@
from app.rag.llms.dspy import get_dspy_lm_by_llama_llm
from app.rag.retrievers.knowledge_graph.schema import KnowledgeGraphRetrievalResult
from app.rag.types import ChatEventType, ChatMessageSate
from app.rag.utils import parse_goal_response_format
from app.rag.utils import (
iter_without_hidden_reasoning,
parse_goal_response_format,
strip_hidden_reasoning,
)
from app.repositories import chat_repo
from app.site_settings import SiteSetting
from app.utils.tracing import LangfuseContextManager
Expand Down Expand Up @@ -364,6 +368,7 @@ def _refine_user_question(
question=user_question,
current_date=datetime.now().strftime("%Y-%m-%d"),
)
refined_question = strip_hidden_reasoning(refined_question)

if not annotation_silent:
yield ChatEvent(
Expand Down Expand Up @@ -504,12 +509,13 @@ def _generate_answer(
),
)
response_text = ""
for word in response.response_gen:
for word in iter_without_hidden_reasoning(response.response_gen):
response_text += word
yield ChatEvent(
event_type=ChatEventType.TEXT_PART,
payload=word,
)
response_text = strip_hidden_reasoning(response_text)

if not response_text:
raise Exception("Got empty response from LLM")
Expand Down Expand Up @@ -585,6 +591,8 @@ def _chat_finish(
source_documents: Optional[List[SourceDocument]] = [],
annotation_silent: bool = False,
):
response_text = strip_hidden_reasoning(response_text)

if not annotation_silent:
yield ChatEvent(
event_type=ChatEventType.MESSAGE_ANNOTATIONS_PART,
Expand Down
8 changes: 4 additions & 4 deletions backend/app/rag/chat/chat_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from app.repositories.embedding_model import embedding_model_repo
from app.repositories.llm import llm_repo
from app.site_settings import SiteSetting
from app.rag.utils import parse_question_lines
from llama_index.core.prompts.rich import RichPromptTemplate

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -290,10 +291,7 @@ def get_chat_message_recommend_questions(
prompt_template,
chat_message_content=chat_message.content,
)
recommend_question_list = recommend_questions.splitlines()
recommend_question_list = [
question.strip() for question in recommend_question_list if question.strip()
]
recommend_question_list = parse_question_lines(recommend_questions)

longest_question = 0
for question in recommend_question_list:
Expand All @@ -304,6 +302,7 @@ def get_chat_message_recommend_questions(
"##" in recommend_questions
or "**" in recommend_questions
or longest_question > 500
or len(recommend_question_list) < 3
):
regenerate_content = f"""
Please note that you are generating a question list. You previously generated it incorrectly; try again.
Expand All @@ -315,6 +314,7 @@ def get_chat_message_recommend_questions(
prompt_template,
chat_message_content=regenerate_content,
)
recommend_question_list = parse_question_lines(recommend_questions)

db_session.add(
RecommendQuestion(
Expand Down
97 changes: 96 additions & 1 deletion backend/app/rag/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,100 @@
import re
from typing import Tuple, Dict
from typing import Dict, Iterable, Iterator, Tuple

_THINK_BLOCK_RE = re.compile(r"<think\b[^>]*>.*?</think>", re.IGNORECASE | re.DOTALL)
_THINK_START_RE = re.compile(r"<think\b[^>]*>.*", re.IGNORECASE | re.DOTALL)
_GENERIC_ASSISTANT_QUESTION_RE = re.compile(
r"\b("
r"how (can|may) i (assist|help) you|"
r"what specific topic would you like|"
r"do you prefer the questions|"
r"how many follow[-‑ ]?up questions|"
r"should the questions be concise|"
r"is there a particular audience"
r")\b",
re.IGNORECASE,
)


def strip_hidden_reasoning(text: str | None) -> str:
if not text:
return ""

cleaned = _THINK_BLOCK_RE.sub("", text)
cleaned = _THINK_START_RE.sub("", cleaned)
return cleaned.strip()


def iter_without_hidden_reasoning(chunks: Iterable[str]) -> Iterator[str]:
"""Yield text chunks while suppressing <think>...</think> blocks."""
buffer = ""
in_think_block = False
start_tag_overlap = len("<think") - 1
end_tag_overlap = len("</think>") - 1

for chunk in chunks:
if not chunk:
continue

buffer += chunk

while buffer:
lower_buffer = buffer.lower()

if in_think_block:
end_index = lower_buffer.find("</think>")
if end_index < 0:
buffer = buffer[-end_tag_overlap:]
break

buffer = buffer[end_index + len("</think>") :]
in_think_block = False
continue

start_index = lower_buffer.find("<think")
if start_index < 0:
emit_length = max(0, len(buffer) - start_tag_overlap)
if emit_length == 0:
break

yield buffer[:emit_length]
buffer = buffer[emit_length:]
break

if start_index > 0:
yield buffer[:start_index]

tag_end_index = buffer.find(">", start_index)
if tag_end_index < 0:
buffer = buffer[start_index:]
break

buffer = buffer[tag_end_index + 1 :]
in_think_block = True

if buffer and not in_think_block:
yield strip_hidden_reasoning(buffer)


def parse_question_lines(text: str | None, limit: int = 5) -> list[str]:
cleaned = strip_hidden_reasoning(text)
questions: list[str] = []

for line in cleaned.splitlines():
line = line.strip()
line = re.sub(r"^[-*•]\s*", "", line)
line = re.sub(r"^\d+[\.)、]\s*", "", line)
if not line or line.startswith("<"):
continue
if not line.endswith(("?", "?")):
continue
if _GENERIC_ASSISTANT_QUESTION_RE.search(line):
continue
questions.append(line)
if len(questions) >= limit:
break

return questions


def _parse_response_format(response_format_str: str) -> Dict[str, str]:
Expand Down
44 changes: 44 additions & 0 deletions backend/tests/test_rag_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from app.rag.utils import (
iter_without_hidden_reasoning,
parse_question_lines,
strip_hidden_reasoning,
)


def test_strip_hidden_reasoning_removes_think_block():
output = strip_hidden_reasoning(
"<think>reasoning that should not be shown</think>\n\nFinal answer"
)

assert output == "Final answer"


def test_strip_hidden_reasoning_removes_unclosed_think_block():
output = strip_hidden_reasoning("Visible answer\n<think>unfinished reasoning")

assert output == "Visible answer"


def test_iter_without_hidden_reasoning_handles_split_tags():
chunks = ["Hello ", "<thi", "nk>hidden", " reasoning</th", "ink>", " world"]

assert "".join(iter_without_hidden_reasoning(chunks)) == "Hello world"


def test_parse_question_lines_filters_reasoning_and_metadata():
questions = parse_question_lines(
"""
<think>
Need to generate follow-up questions.
</think>
1. 如何定位 TiDB 写入热点?
- TiDB Dashboard 中哪些面板可以辅助排查热点?
How can I assist you today?
This is not a question
"""
)

assert questions == [
"如何定位 TiDB 写入热点?",
"TiDB Dashboard 中哪些面板可以辅助排查热点?",
]