Skip to content
Merged
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
9 changes: 6 additions & 3 deletions ai/src/ai_server/chain/followup_generation_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ def extract_question_span(text: str) -> str:
return ""
tail = text[open_idx + len("<question>") :]
tail = tail.split("<meta>", 1)[0]
# 닫는/메타 태그가 스트림 청크 경계로 잘려 들어와도(예: "질문?</quest")
# 미완성 '<...' 꼬리를 잘라낸다 — 질문 본문에는 '<' 가 없다고 가정.
# 닫는/메타 태그가 스트림 청크 경계로 잘려 들어온 경우(예: "질문?</quest")만 그
# partial 을 제거한다. 질문 본문의 부등호·제네릭('a < b', 'List<T>')까지 잘라내지
# 않도록, 마지막 '<' 꼬리가 태그 시작과 일치할 때만 자른다.
lt = tail.rfind("<")
if lt != -1 and ">" not in tail[lt:]:
tail = tail[:lt]
partial = tail[lt:]
if "</question>".startswith(partial) or "<meta>".startswith(partial):
tail = tail[:lt]
return tail.strip()


Expand Down
33 changes: 33 additions & 0 deletions ai/tests/test_followup_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,36 @@ def test_extract_question_only_helper():
extract_question_span("<intent>NORMAL</intent><question>안녕</question><meta>{}")
== "안녕"
)


def test_streaming_keeps_lt_in_body():
# 스트리밍 중(닫는 태그 전) 질문 본문의 부등호·제네릭 '<' 를 잘라내면 안 된다.
from ai_server.chain.followup_generation_chain import extract_question_span

assert (
extract_question_span("<intent>NORMAL</intent><question>a < b 인지 확인하세요")
== "a < b 인지 확인하세요"
)
assert (
extract_question_span("<intent>NORMAL</intent><question>제네릭 List<T> 를 설명하세요")
== "제네릭 List<T> 를 설명하세요"
)


def test_streaming_strips_partial_closing_or_meta_tag():
# 청크 경계로 잘려 들어온 닫는/메타 태그 partial 만 제거한다.
from ai_server.chain.followup_generation_chain import extract_question_span

assert extract_question_span("<intent>NORMAL</intent><question>질문입니다?</quest") == "질문입니다?"
assert extract_question_span("<intent>NORMAL</intent><question>끝났나요?<met") == "끝났나요?"


def test_parses_question_with_lt_when_closed():
# 닫는 태그가 있으면 '<' 포함 질문이 그대로 보존된다(최종 저장값 잘림 방지).
text = (
"<intent>NORMAL</intent>"
"<question>리스트에서 a < b 를 만족하는 쌍을 어떻게 셀까요?</question>"
"<meta>{}</meta>"
)
r = parse_followup_result(text)
assert r.followup_question == "리스트에서 a < b 를 만족하는 쌍을 어떻게 셀까요?"
29 changes: 29 additions & 0 deletions ai/tests/test_followup_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ async def test_stream_emits_question_tokens_and_final_result():
assert result.followup_question == "그 설계에서 트레이드오프는?"


@pytest.mark.asyncio
async def test_stream_emits_full_question_with_lt_operator():
# 부등호 '<' 가 든 질문이 스트리밍 중 '<' 에서 잘리지 않고 끝까지 흘러야 한다.
pieces = [
"<intent>NORMAL</intent><question>리스트에서 a ",
"< b 를 ",
"만족하는 쌍을 세는 법은?",
"</question><meta>{}</meta>",
]
prompt = ChatPromptTemplate.from_messages([("human", "{answer_text}")])
gen = StreamingFollowupGenerator(prompt=prompt, llm=_FakeStreamLLM(pieces))

seen = []
result = await gen.stream(
on_question_token=lambda piece: seen.append(piece),
job_category="BACKEND",
mode="TECHNICAL",
previous_question="q",
answer_text="a",
context="(none)",
parent_category="X",
expected_signal="(none)",
history="(none)",
)
joined = "".join(seen)
assert joined == "리스트에서 a < b 를 만족하는 쌍을 세는 법은?"
assert result.followup_question == "리스트에서 a < b 를 만족하는 쌍을 세는 법은?"


@pytest.mark.asyncio
async def test_stream_dont_know_emits_no_question_tokens():
pieces = ["<intent>DONT_KNOW</intent><question>모르면 이렇게</question><meta>{}</meta>"]
Expand Down
Loading