File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -39,11 +39,14 @@ def extract_question_span(text: str) -> str:
3939 return ""
4040 tail = text [open_idx + len ("<question>" ) :]
4141 tail = tail .split ("<meta>" , 1 )[0 ]
42- # 닫는/메타 태그가 스트림 청크 경계로 잘려 들어와도(예: "질문?</quest")
43- # 미완성 '<...' 꼬리를 잘라낸다 — 질문 본문에는 '<' 가 없다고 가정.
42+ # 닫는/메타 태그가 스트림 청크 경계로 잘려 들어온 경우(예: "질문?</quest")만 그
43+ # partial 을 제거한다. 질문 본문의 부등호·제네릭('a < b', 'List<T>')까지 잘라내지
44+ # 않도록, 마지막 '<' 꼬리가 태그 시작과 일치할 때만 자른다.
4445 lt = tail .rfind ("<" )
4546 if lt != - 1 and ">" not in tail [lt :]:
46- tail = tail [:lt ]
47+ partial = tail [lt :]
48+ if "</question>" .startswith (partial ) or "<meta>" .startswith (partial ):
49+ tail = tail [:lt ]
4750 return tail .strip ()
4851
4952
Original file line number Diff line number Diff line change @@ -29,3 +29,36 @@ def test_extract_question_only_helper():
2929 extract_question_span ("<intent>NORMAL</intent><question>안녕</question><meta>{}" )
3030 == "안녕"
3131 )
32+
33+
34+ def test_streaming_keeps_lt_in_body ():
35+ # 스트리밍 중(닫는 태그 전) 질문 본문의 부등호·제네릭 '<' 를 잘라내면 안 된다.
36+ from ai_server .chain .followup_generation_chain import extract_question_span
37+
38+ assert (
39+ extract_question_span ("<intent>NORMAL</intent><question>a < b 인지 확인하세요" )
40+ == "a < b 인지 확인하세요"
41+ )
42+ assert (
43+ extract_question_span ("<intent>NORMAL</intent><question>제네릭 List<T> 를 설명하세요" )
44+ == "제네릭 List<T> 를 설명하세요"
45+ )
46+
47+
48+ def test_streaming_strips_partial_closing_or_meta_tag ():
49+ # 청크 경계로 잘려 들어온 닫는/메타 태그 partial 만 제거한다.
50+ from ai_server .chain .followup_generation_chain import extract_question_span
51+
52+ assert extract_question_span ("<intent>NORMAL</intent><question>질문입니다?</quest" ) == "질문입니다?"
53+ assert extract_question_span ("<intent>NORMAL</intent><question>끝났나요?<met" ) == "끝났나요?"
54+
55+
56+ def test_parses_question_with_lt_when_closed ():
57+ # 닫는 태그가 있으면 '<' 포함 질문이 그대로 보존된다(최종 저장값 잘림 방지).
58+ text = (
59+ "<intent>NORMAL</intent>"
60+ "<question>리스트에서 a < b 를 만족하는 쌍을 어떻게 셀까요?</question>"
61+ "<meta>{}</meta>"
62+ )
63+ r = parse_followup_result (text )
64+ assert r .followup_question == "리스트에서 a < b 를 만족하는 쌍을 어떻게 셀까요?"
Original file line number Diff line number Diff line change @@ -48,6 +48,35 @@ async def test_stream_emits_question_tokens_and_final_result():
4848 assert result .followup_question == "그 설계에서 트레이드오프는?"
4949
5050
51+ @pytest .mark .asyncio
52+ async def test_stream_emits_full_question_with_lt_operator ():
53+ # 부등호 '<' 가 든 질문이 스트리밍 중 '<' 에서 잘리지 않고 끝까지 흘러야 한다.
54+ pieces = [
55+ "<intent>NORMAL</intent><question>리스트에서 a " ,
56+ "< b 를 " ,
57+ "만족하는 쌍을 세는 법은?" ,
58+ "</question><meta>{}</meta>" ,
59+ ]
60+ prompt = ChatPromptTemplate .from_messages ([("human" , "{answer_text}" )])
61+ gen = StreamingFollowupGenerator (prompt = prompt , llm = _FakeStreamLLM (pieces ))
62+
63+ seen = []
64+ result = await gen .stream (
65+ on_question_token = lambda piece : seen .append (piece ),
66+ job_category = "BACKEND" ,
67+ mode = "TECHNICAL" ,
68+ previous_question = "q" ,
69+ answer_text = "a" ,
70+ context = "(none)" ,
71+ parent_category = "X" ,
72+ expected_signal = "(none)" ,
73+ history = "(none)" ,
74+ )
75+ joined = "" .join (seen )
76+ assert joined == "리스트에서 a < b 를 만족하는 쌍을 세는 법은?"
77+ assert result .followup_question == "리스트에서 a < b 를 만족하는 쌍을 세는 법은?"
78+
79+
5180@pytest .mark .asyncio
5281async def test_stream_dont_know_emits_no_question_tokens ():
5382 pieces = ["<intent>DONT_KNOW</intent><question>모르면 이렇게</question><meta>{}</meta>" ]
You can’t perform that action at this time.
0 commit comments