govon-runtime / src /inference /query_builder.py
GovOn Deploy
sync: PR#584 RAG removal + ReAct architecture
1635ec4
"""Context-aware query builder for search-oriented tools.
Issue #159: follow-up ์š”์ฒญ์—์„œ ์›๋ฌธ ์งˆ๋ฌธ, ๊ธฐ์กด ์ดˆ์•ˆ, ์ตœ๊ทผ tool ์š”์•ฝ์„ ๋ฐ˜์˜ํ•ด
RAG/API ์กฐํšŒ์šฉ query variant๋ฅผ ๋งŒ๋“ ๋‹ค.
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Any, Dict, Sequence
if TYPE_CHECKING:
from .session_context import SessionContext
_FOLLOW_UP_PATTERNS = tuple(
re.compile(pattern)
for pattern in (
r"๊ทผ๊ฑฐ",
r"์ถœ์ฒ˜",
r"๋งํฌ",
r"์ด์œ ",
r"๋ณด๊ฐ•",
r"์ถ”๊ฐ€",
r"๋‹ค์‹œ",
r"์ˆ˜์ •",
r"์ •์ค‘",
r"๊ณต์†",
r"์ด ๋‹ต๋ณ€",
r"์œ„ ๋‹ต๋ณ€",
r"๊ธฐ์กด ๋‹ต๋ณ€",
)
)
_MAX_QUERY_LEN = 480
_MAX_USER_LEN = 180
_MAX_ASSISTANT_LEN = 220
_MAX_TOOL_SUMMARY_LEN = 120
# ์ง€์‹œ๋Œ€๋ช…์‚ฌ/์ฐธ์กฐ ํ‘œํ˜„: ์ด์ „ turn์„ ๊ฐ€๋ฆฌํ‚ค๋Š” ํ‘œํ˜„์ด ์žˆ์œผ๋ฉด follow-up์œผ๋กœ ๊ฐ„์ฃผ
_ANAPHORA_PATTERNS = tuple(
re.compile(pattern)
for pattern in (
r"๊ทธ๊ฑฐ",
r"์ด๊ฑฐ",
r"์ €๊ฑฐ",
r"์ด๊ฒƒ",
r"๊ทธ๊ฒƒ",
r"์ €๊ฒƒ",
r"์œ„\s*(๋‹ต๋ณ€|๋‚ด์šฉ|๊ธ€|์„ค๋ช…|ํ•ญ๋ชฉ)",
r"์•„๋ž˜\s*(๋‹ต๋ณ€|๋‚ด์šฉ|๊ธ€|์„ค๋ช…|ํ•ญ๋ชฉ)",
r"์ด\s*(๋‹ต๋ณ€|๋‚ด์šฉ|๊ธ€|์„ค๋ช…)",
r"๊ทธ\s*(๋‹ต๋ณ€|๋‚ด์šฉ|๊ธ€|์„ค๋ช…)",
r"๊ธฐ์กด\s*(๋‹ต๋ณ€|๋‚ด์šฉ|๊ธ€|์ดˆ์•ˆ)",
r"๋ฐฉ๊ธˆ",
r"์•ž์„œ",
r"์œ„์—์„œ",
)
)
# ์ž๊ธฐ์™„๊ฒฐ ํŒ์ •: ์ฟผ๋ฆฌ์— ๋…๋ฆฝ ๋ช…์‚ฌ๊ฐ€ ์ด ์ˆ˜ ์ด์ƒ์ด๋ฉด ์ž๊ธฐ์™„๊ฒฐ์ ์œผ๋กœ ๊ฐ„์ฃผ
_SELF_CONTAINED_NOUN_MIN = 2
def build_runtime_query_context(session: "SessionContext", current_query: str) -> Dict[str, Any]:
"""์„ธ์…˜์—์„œ query builder ์ž…๋ ฅ์šฉ ๊ตฌ์กฐํ™” ์ปจํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•œ๋‹ค."""
previous_user, previous_assistant = extract_previous_turns(session, current_query)
return {
"session_id": session.session_id,
"query": normalize_text(current_query),
"session_context": session.build_context_summary(),
"previous_user_query": clip_text(previous_user, _MAX_USER_LEN),
"previous_assistant_response": clip_text(previous_assistant, _MAX_ASSISTANT_LEN),
"recent_tool_summary": clip_text(build_recent_tool_summary(session), _MAX_TOOL_SUMMARY_LEN),
}
def extract_previous_turns(
session: "SessionContext",
current_query: str,
) -> tuple[str, str]:
"""ํ˜„์žฌ ์š”์ฒญ ์ง์ „์˜ user / assistant turn์„ ์ถ”์ถœํ•œ๋‹ค."""
turns = list(session.recent_history)
normalized_current = normalize_text(current_query)
if (
turns
and turns[-1].role == "user"
and normalize_text(turns[-1].content) == normalized_current
):
turns = turns[:-1]
previous_user = next(
(normalize_text(turn.content) for turn in reversed(turns) if turn.role == "user"),
"",
)
previous_assistant = next(
(normalize_text(turn.content) for turn in reversed(turns) if turn.role == "assistant"),
"",
)
return previous_user, previous_assistant
def build_recent_tool_summary(session: "SessionContext") -> str:
"""์ตœ๊ทผ tool ์‹คํ–‰์„ ์งง์€ ์š”์•ฝ์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค."""
parts: list[str] = []
for record in session.recent_tool_runs[-3:]:
tool_parts = [record.tool]
if isinstance(record.metadata, dict):
query = normalize_text(record.metadata.get("query", ""))
text_preview = normalize_text(record.metadata.get("text_preview", ""))
if query:
tool_parts.append(clip_text(query, 60))
elif text_preview:
tool_parts.append(clip_text(text_preview, 60))
count = record.metadata.get("count")
if count is not None:
tool_parts.append(f"count {count}")
parts.append(" ".join(part for part in tool_parts if part))
return " | ".join(parts)
def is_self_contained_query(query: str) -> bool:
"""์ฟผ๋ฆฌ๊ฐ€ ์ด์ „ ๋งฅ๋ฝ ์—†์ด ๋…๋ฆฝ์ ์œผ๋กœ ์ดํ•ด ๊ฐ€๋Šฅํ•œ์ง€ ํŒ์ •ํ•œ๋‹ค.
์ง€์‹œ๋Œ€๋ช…์‚ฌ๋‚˜ ์ฐธ์กฐ ํ‘œํ˜„์ด ์—†๊ณ , ๋…๋ฆฝ ๋ช…์‚ฌ(๊ณต๋ฐฑ ๊ตฌ๋ถ„ ํ† ํฐ)๊ฐ€ ์ถฉ๋ถ„ํžˆ
ํฌํ•จ๋œ ์ฟผ๋ฆฌ๋Š” ์ž๊ธฐ์™„๊ฒฐ์ ์œผ๋กœ ๊ฐ„์ฃผํ•œ๋‹ค.
"""
if any(pattern.search(query) for pattern in _ANAPHORA_PATTERNS):
return False
tokens = [t for t in query.split() if len(t) >= 2]
return len(tokens) >= _SELF_CONTAINED_NOUN_MIN
def should_use_follow_up_context(
query: str,
*,
tool_names: Sequence[str],
previous_user: str,
previous_assistant: str,
) -> bool:
"""์ด์ „ user/assistant turn์„ query์— ์„ž์–ด์•ผ ํ•˜๋Š” follow-up์ธ์ง€ ํŒ๋‹จํ•œ๋‹ค.
์ฟผ๋ฆฌ๊ฐ€ ์ž๊ธฐ์™„๊ฒฐ์ ์ด๋ฉด ์ด์ „ ๋งฅ๋ฝ์„ ์ฃผ์ž…ํ•˜์ง€ ์•Š๋Š”๋‹ค.
์‹ค์ œ ํ›„์† ์งˆ๋ฌธ(์ง€์‹œ๋Œ€๋ช…์‚ฌยท์ฐธ์กฐ ํ‘œํ˜„ ํฌํ•จ ๋˜๋Š”
_FOLLOW_UP_PATTERNS ๋งค์นญ)์ธ ๊ฒฝ์šฐ์—๋งŒ True๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
"""
if not (previous_user or previous_assistant):
return False
# ์ž๊ธฐ์™„๊ฒฐ์  ์ฟผ๋ฆฌ๋ฉด ์ด์ „ ๋งฅ๋ฝ ์ฃผ์ž… ์•ˆ ํ•จ
if is_self_contained_query(query):
return False
return any(pattern.search(query) for pattern in _FOLLOW_UP_PATTERNS)
def clip_text(value: Any, limit: int) -> str:
"""๊ธด ๋ฌธ์žฅ์„ ๊ณ ์ • ๊ธธ์ด๋กœ ์ž˜๋ผ๋‚ธ๋‹ค."""
text = normalize_text(value)
if len(text) <= limit:
return text
return text[: max(limit - 3, 0)].rstrip() + "..."
def normalize_text(value: Any) -> str:
"""๊ณต๋ฐฑ์„ ์ •๊ทœํ™”ํ•œ ๋‹จ์ผ ๋ผ์ธ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค."""
return re.sub(r"\s+", " ", str(value or "")).strip()