κΉ€λ―Όκ²½
Add full project source code
70d407c
"""LangGraph State β€” λ©€ν‹°ν„΄ λ³΄ν—˜ 챗봇 μƒνƒœ μŠ€ν‚€λ§ˆ.
ν•„μˆ˜ ν•„λ“œ:
messages β€” Human/AI/Tool λ©”μ‹œμ§€ λˆ„μ  (add_messages reducer)
trace β€” λ…Έλ“œλ³„ μ‹€ν–‰ 둜그 (append reducer)
"""
from __future__ import annotations
from typing import Annotated, Literal
from langchain_core.messages import HumanMessage
from langgraph.graph import MessagesState
from pydantic import Field
# ── Trace Reset Sentinel ───────────────────────────────────────────────────────
# ainvoke μ‹œμž‘ μ‹œ 이전 ν„΄ traceλ₯Ό μ΄ˆκΈ°ν™”ν•˜λŠ” 마컀 객체.
# dict νƒ€μž…μ΄μ§€λ§Œ 객체 동일성(is)으둜 λΉ„κ΅ν•˜λ―€λ‘œ μ‚¬μš©μž 데이터와 좩돌 μ—†μŒ.
_TRACE_RESET: dict = {"__reset__": True}
def _append_trace(current: list[dict], update: list[dict]) -> list[dict]:
"""Trace reducer β€” μƒˆ ainvoke μ‹œμž‘ μ‹œ μ΄ˆκΈ°ν™”, ν„΄ λ‚΄μ—μ„œλŠ” λˆ„μ .
update[0] is _TRACE_RESET 이면 이번 ν„΄λ§Œ μœ μ§€ (체크포인트 trace 폐기).
"""
if update and update[0] is _TRACE_RESET:
return list(update[1:])
return current + update
class AgentState(MessagesState):
"""κ·Έλž˜ν”„ 전체 μƒνƒœ. λͺ¨λ“  λ…Έλ“œκ°€ 이 νƒ€μž…μ˜ 뢀뢄집합을 μž…μΆœλ ₯."""
trace: Annotated[list[dict], _append_trace]
guardrail_action: Literal["pass", "block", "retry"] = Field(default="pass")
rewritten_query: str = Field(default="")
"""query_rewriterκ°€ 단문/후속 μ§ˆλ¬Έμ„ μž¬μž‘μ„±ν•œ κ²°κ³Ό. λΉ„μ–΄μžˆμœΌλ©΄ 원본 μ‚¬μš©."""
guardrail_retry_count: int = Field(default=0)
"""output_guardrail 차단 ν›„ μž¬μ‹œλ„ 횟수. λ¬΄ν•œ 루프 λ°©μ§€μš©."""
conversation_started: bool = Field(default=False)
"""output_guardrailλ₯Ό ν†΅κ³Όν•œ μ‹€μ œ AI 응닡이 μ΅œμ†Œ 1회 이상 μ „μ†‘λœ 적 μžˆλŠ”μ§€ μ—¬λΆ€.
guardrailμ—μ„œ 거절된 μ‘λ‹΅λ§Œ μžˆμ„ λ•Œ Falseλ₯Ό μœ μ§€ν•¨μœΌλ‘œμ¨
도메인 체크 우회(1ν„΄ 차단 β†’ 2ν„΄ followup νŒμ •) 취약점을 λ°©μ§€ν•œλ‹€.
build_graph_input()μ—μ„œ λ¦¬μ…‹ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ checkpointerλ₯Ό 톡해 λŒ€ν™” μ „λ°˜μ— 걸쳐 μœ μ§€λœλ‹€.
"""
def build_graph_input(query: str) -> dict:
"""κ·Έλž˜ν”„ 호좜용 μž…λ ₯ dictλ₯Ό κ΅¬μ„±ν•œλ‹€.
λͺ¨λ“  μ§„μž…μ (FastAPI, MCP)이 이 ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜μ—¬
AgentState μŠ€ν‚€λ§ˆμ— λ§žλŠ” μž…λ ₯을 μƒμ„±ν•œλ‹€.
trace에 _TRACE_RESET을 μ£Όμž…ν•˜μ—¬ 이전 ν„΄ traceκ°€ λˆ„μ λ˜μ§€ μ•Šλ„λ‘ ν•œλ‹€.
conversation_startedλŠ” μ˜λ„μ μœΌλ‘œ λ¦¬μ…‹ν•˜μ§€ μ•ŠλŠ”λ‹€ (λŒ€ν™” μ „λ°˜ μœ μ§€).
"""
return {
"messages": [HumanMessage(content=query)],
"trace": [_TRACE_RESET],
"guardrail_action": "pass",
"rewritten_query": "",
"guardrail_retry_count": 0,
}
def extract_last_human_query(messages: list) -> str:
"""λ©”μ‹œμ§€ νžˆμŠ€ν† λ¦¬μ—μ„œ κ°€μž₯ 졜근 HumanMessage의 contentλ₯Ό λ°˜ν™˜."""
for msg in reversed(messages):
if isinstance(msg, HumanMessage) and msg.content:
return msg.content
return ""
def extract_tools_used(messages: list) -> list[str]:
"""λ©”μ‹œμ§€ λ¦¬μŠ€νŠΈμ—μ„œ μ‚¬μš©λœ 도ꡬ 이름을 쀑볡 없이 μˆœμ„œ λ³΄μ‘΄ν•˜μ—¬ λ°˜ν™˜."""
return list(dict.fromkeys(
msg.name for msg in messages
if hasattr(msg, "name") and msg.name and getattr(msg, "type", "") == "tool"
))