From a4f3b20447797f15ebe6c0a9f11b6d5d89562935 Mon Sep 17 00:00:00 2001 From: ehddnr301 Date: Fri, 21 Nov 2025 13:53:37 +0900 Subject: [PATCH] =?UTF-8?q?chatbot=20module=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20&=20=EA=B0=80=EC=9D=B4=EB=93=9C=EB=9D=BC=EC=9D=B8?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=EC=B1=97=EB=B4=87=20=EB=8F=99=EC=9E=91?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/llm/README.md | 7 +- utils/llm/chatbot.py | 214 -------------------------------- utils/llm/chatbot/README.md | 57 +++++++++ utils/llm/chatbot/__init__.py | 7 ++ utils/llm/chatbot/core.py | 186 +++++++++++++++++++++++++++ utils/llm/chatbot/guidelines.py | 67 ++++++++++ utils/llm/chatbot/matcher.py | 84 +++++++++++++ utils/llm/chatbot/types.py | 33 +++++ 8 files changed, 439 insertions(+), 216 deletions(-) delete mode 100644 utils/llm/chatbot.py create mode 100644 utils/llm/chatbot/README.md create mode 100644 utils/llm/chatbot/__init__.py create mode 100644 utils/llm/chatbot/core.py create mode 100644 utils/llm/chatbot/guidelines.py create mode 100644 utils/llm/chatbot/matcher.py create mode 100644 utils/llm/chatbot/types.py diff --git a/utils/llm/README.md b/utils/llm/README.md index 993ee98..22435dd 100644 --- a/utils/llm/README.md +++ b/utils/llm/README.md @@ -10,7 +10,10 @@ utils/llm/ ├── chains.py # LangChain 체인 생성 모듈 ├── retrieval.py # 테이블 메타 검색 및 재순위화 ├── llm_response_parser.py # LLM 응답에서 SQL 블록 추출 -├── chatbot.py # LangGraph ChatBot 구현 +├── chatbot/ # LangGraph ChatBot 패키지 +│ ├── __init__.py +│ ├── core.py # ChatBot 핵심 로직 +│ └── README.md # [상세 문서](./chatbot/README.md) ├── core/ # LLM/Embedding 팩토리 모듈 │ ├── __init__.py │ ├── factory.py # LLM 및 Embedding 모델 생성 팩토리 @@ -152,7 +155,7 @@ utils/llm/ **사용처:** - `utils/llm/vectordb/faiss_db.py`: 벡터DB 초기화 시 메타데이터 수집 - `utils/llm/vectordb/pgvector_db.py`: 벡터DB 초기화 시 메타데이터 수집 -- `utils/llm/chatbot.py`: ChatBot 도구로 사용 +- `utils/llm/chatbot/`: ChatBot 도구로 사용 **상세 문서**: [tools/README.md](./tools/README.md) diff --git a/utils/llm/chatbot.py b/utils/llm/chatbot.py deleted file mode 100644 index 51bcab0..0000000 --- a/utils/llm/chatbot.py +++ /dev/null @@ -1,214 +0,0 @@ -""" -LangGraph 기반 ChatBot 모델 -OpenAI의 ChatGPT 모델을 사용하여 대화 기록을 유지하는 챗봇 구현 -""" - -from typing import Annotated, Sequence, TypedDict - -from langchain_core.messages import BaseMessage, SystemMessage -from langchain_openai import ChatOpenAI -from langgraph.checkpoint.memory import MemorySaver -from langgraph.graph import START, StateGraph -from langgraph.graph.message import add_messages -from langgraph.prebuilt import ToolNode - -from utils.llm.tools import ( - search_database_tables, - get_glossary_terms, - get_query_examples, -) - - -class ChatBotState(TypedDict): - """ - 챗봇 상태 - 사용자 질문을 SQL로 변환 가능한 구체적인 질문으로 만들어가는 과정 추적 - """ - - # 기본 메시지 (MessagesState와 동일) - messages: Annotated[Sequence[BaseMessage], add_messages] - - # datahub 서버 정보 - gms_server: str - - -class ChatBot: - """ - LangGraph를 사용한 대화형 챗봇 클래스 - OpenAI API를 통해 다양한 GPT 모델을 사용할 수 있으며, - MemorySaver를 통해 대화 기록을 관리합니다. - """ - - def __init__( - self, - openai_api_key: str, - model_name: str = "gpt-4o-mini", - gms_server: str = "http://localhost:8080", - ): - """ - ChatBot 인스턴스 초기화 - - Args: - openai_api_key: OpenAI API 키 - model_name: 사용할 모델명 (기본값: gpt-4o-mini) - gms_server: DataHub GMS 서버 URL (기본값: http://localhost:8080) - """ - self.openai_api_key = openai_api_key - self.model_name = model_name - self.gms_server = gms_server - # SQL 생성을 위한 데이터베이스 메타데이터 조회 도구 - self.tools = [ - search_database_tables, # 데이터베이스 테이블 정보 검색 - get_glossary_terms, # 용어집 조회 도구 - get_query_examples, # 쿼리 예제 조회 도구 - ] - self.llm = self._setup_llm() # LLM 인스턴스 설정 - self.app = self._setup_workflow() # LangGraph 워크플로우 설정 - - def _setup_llm(self): - """ - OpenAI ChatGPT LLM 인스턴스 생성 - Tool을 바인딩하여 LLM이 필요시 tool을 호출할 수 있도록 설정합니다. - - Returns: - ChatOpenAI: Tool이 바인딩된 LLM 인스턴스 - """ - llm = ChatOpenAI( - temperature=0.0, # SQL 생성은 정확성이 중요하므로 0으로 설정 - openai_api_key=self.openai_api_key, - model_name=self.model_name, - ) - # Tool을 LLM에 바인딩하여 함수 호출 기능 활성화 - return llm.bind_tools(self.tools) - - def _setup_workflow(self): - """ - LangGraph 워크플로우 설정 - 대화 기록을 관리하고 LLM과 통신하는 그래프 구조를 생성합니다. - Tool 호출 기능을 포함하여 LLM이 필요시 도구를 사용할 수 있도록 합니다. - - Returns: - CompiledGraph: 컴파일된 LangGraph 워크플로우 - """ - # ChatBotState를 사용하는 StateGraph 생성 - workflow = StateGraph(state_schema=ChatBotState) - - def call_model(state: ChatBotState): - """ - LLM 모델을 호출하는 노드 함수 - LLM이 응답을 생성하거나 tool 호출을 결정합니다. - - Args: - state: 현재 메시지 상태 - - Returns: - dict: LLM 응답이 포함된 상태 업데이트 - """ - # 질문 구체화 전문 어시스턴트 시스템 메시지 - sys_msg = SystemMessage( - content="""# 역할 -당신은 사용자의 모호한 질문을 명확하고 구체적인 질문으로 만드는 전문 AI 어시스턴트입니다. - -# 주요 임무 -- 사용자의 자연어 질문을 이해하고 의도를 정확히 파악합니다 -- 대화를 통해 날짜, 지표, 필터 조건 등 구체적인 정보를 수집합니다 -- 단계별로 사용자와 대화하며 명확하고 구체적인 질문으로 다듬어갑니다 - -# 작업 프로세스 -1. 사용자의 최초 질문에서 의도 파악 -2. 질문을 명확히 하기 위해 필요한 정보 식별 (날짜, 지표, 대상, 조건 등) -3. **도구를 적극 활용하여 데이터베이스 스키마, 테이블 정보, 용어집 등을 확인** -4. 부족한 정보를 자연스럽게 질문하여 수집 -5. 수집된 정보를 바탕으로 질문을 점진적으로 구체화 -6. 충분히 구체화되면 최종 질문 확정 - -# 도구 사용 가이드 -- **search_database_tables**: 사용자와의 대화를 데이터와 연관짓기 위해 관련 테이블을 적극적으로 확인할 수 있는 도구 -- **get_glossary_terms**: 사용자가 사용한 용어의 정확한 의미를 확인할 때 사용가능한 도구 -- **get_query_examples**: 조직내 저장된 쿼리 예제를 조회하여 참고할 수 있는 도구 -- 답변하기 전에 최대한 많은 도구를 적극 활용하여 정보를 수집하세요 -- 불확실한 정보가 있다면 추측하지 말고 도구를 사용하여 확인하세요 - -# 예시 -- 모호한 질문: "KPI가 궁금해" -- 대화 후 구체화: "2025-01-02 날짜의 신규 유저가 발생시킨 매출이 궁금해" - -# 주의사항 -- 항상 친절하고 명확하게 대화합니다 -- 이전 대화 맥락을 고려하여 일관성 있게 응답합니다 -- 한 번에 너무 많은 것을 물어보지 않고 단계적으로 진행합니다 -- **중요: 사용자가 말한 내용이 충분히 구체화되지 않거나 의도가 명확히 파악되지 않을 경우, 추측하지 말고 모든 도구(get_glossary_terms, get_query_examples, search_database_tables)를 적극적으로 사용하여 맥락을 파악하세요** -- 도구를 통해 수집한 정보를 바탕으로 사용자에게 구체적인 방향성과 옵션을 제안하세요 -- 불확실한 정보가 있다면 추측하지 말고 도구를 사용하여 확인한 후 답변하세요 - ---- -다음은 사용자와의 대화입니다:""" - ) - # 시스템 메시지를 대화의 맨 앞에 추가 - messages = [sys_msg] + state["messages"] - response = self.llm.invoke(messages) - return {"messages": response} - - def route_model_output(state: ChatBotState): - """ - LLM 출력에 따라 다음 노드를 결정하는 라우팅 함수 - Tool 호출이 필요한 경우 'tools' 노드로, 아니면 대화를 종료합니다. - - Args: - state: 현재 메시지 상태 - - Returns: - str: 다음에 실행할 노드 이름 ('tools' 또는 '__end__') - """ - messages = state["messages"] - last_message = messages[-1] - # LLM이 tool을 호출하려고 하는 경우 (tool_calls가 있는 경우) - if hasattr(last_message, "tool_calls") and last_message.tool_calls: - return "tools" - # Tool 호출이 없으면 대화 종료 - return "__end__" - - # 워크플로우 구조 정의 - workflow.add_edge(START, "model") # 시작 -> model 노드 - workflow.add_node("model", call_model) # LLM 호출 노드 - workflow.add_node("tools", ToolNode(self.tools)) # Tool 실행 노드 - - # model 노드 이후 조건부 라우팅 - workflow.add_conditional_edges("model", route_model_output) - # Tool 실행 후 다시 model로 돌아가서 최종 응답 생성 - workflow.add_edge("tools", "model") - - # MemorySaver를 사용하여 대화 기록 저장 기능 추가 - return workflow.compile(checkpointer=MemorySaver()) - - def chat(self, message: str, thread_id: str): - """ - 사용자 메시지에 대한 응답 생성 - - Args: - message: 사용자 입력 메시지 - thread_id: 대화 세션을 구분하는 고유 ID - - Returns: - dict: LLM 응답을 포함한 결과 딕셔너리 - """ - config = {"configurable": {"thread_id": thread_id}} - - # 상태 준비 - input_state = { - "messages": [{"role": "user", "content": message}], - "gms_server": self.gms_server, # DataHub 서버 URL을 상태에 포함 - } - - return self.app.invoke(input_state, config) - - def update_model(self, model_name: str): - """ - 사용 중인 LLM 모델 변경 - 모델 변경 시 LLM 인스턴스와 워크플로우를 재설정합니다. - - Args: - model_name: 변경할 모델명 - """ - self.model_name = model_name - self.llm = self._setup_llm() # 새 모델로 LLM 재설정 - self.app = self._setup_workflow() # 워크플로우 재생성 diff --git a/utils/llm/chatbot/README.md b/utils/llm/chatbot/README.md new file mode 100644 index 0000000..8d52722 --- /dev/null +++ b/utils/llm/chatbot/README.md @@ -0,0 +1,57 @@ +# ChatBot Module + +LangGraph 기반의 대화형 챗봇 모듈입니다. 사용자의 자연어 질문을 이해하고, 적절한 가이드라인과 도구를 선택하여 답변을 생성합니다. + +## 구조 + +``` +utils/llm/chatbot/ +├── __init__.py # 패키지 초기화 및 ChatBot 클래스 export +├── core.py # ChatBot 클래스 및 LangGraph 워크플로우 정의 +├── guidelines.py # 가이드라인 및 툴 래퍼 함수 정의 +├── matcher.py # LLM 기반 가이드라인 매칭 로직 +└── types.py # 데이터 타입 및 구조 정의 +``` + +## 주요 컴포넌트 + +### `ChatBot` (`core.py`) +챗봇의 메인 클래스입니다. LangGraph를 사용하여 대화 흐름을 제어합니다. +- **초기화**: OpenAI API 키, 모델명, GMS 서버 URL 등을 설정합니다. +- **워크플로우**: `select_guidelines` -> `call_model` 순서로 실행됩니다. +- **chat 메서드**: 사용자 메시지를 입력받아 응답을 생성합니다. + +### `LLMGuidelineMatcher` (`matcher.py`) +사용자의 메시지를 분석하여 가장 적절한 가이드라인을 선택하는 클래스입니다. +- LLM을 사용하여 사용자 의도를 파악하고, 미리 정의된 가이드라인 중 하나 이상을 매칭합니다. +- JSON Schema를 사용하여 구조화된 출력을 보장합니다. + +### `Guideline` (`types.py`) +챗봇이 따를 규칙과 도구를 정의하는 데이터 클래스입니다. +- `id`: 가이드라인 식별자 +- `description`: 가이드라인 설명 +- `example_phrases`: 매칭에 사용될 예시 문구 +- `tools`: 해당 가이드라인에서 사용할 도구 함수 목록 +- `priority`: 매칭 우선순위 + +### `GUIDELINES` (`guidelines.py`) +기본적으로 제공되는 가이드라인 목록입니다. +- `db_search`: 데이터베이스 테이블 정보 검색 +- `glossary`: 용어집 조회 +- `query_examples`: 쿼리 예제 조회 + +## 사용 예시 + +```python +from utils.llm.chatbot import ChatBot + +# 챗봇 인스턴스 생성 +bot = ChatBot( + openai_api_key="sk-...", + gms_server="http://localhost:8080" +) + +# 대화하기 +response = bot.chat("매출 테이블 정보 알려줘", thread_id="session_1") +print(response["messages"][-1].content) +``` diff --git a/utils/llm/chatbot/__init__.py b/utils/llm/chatbot/__init__.py new file mode 100644 index 0000000..d816b62 --- /dev/null +++ b/utils/llm/chatbot/__init__.py @@ -0,0 +1,7 @@ +""" +ChatBot 패키지 초기화 모듈 +""" + +from utils.llm.chatbot.core import ChatBot + +__all__ = ["ChatBot"] diff --git a/utils/llm/chatbot/core.py b/utils/llm/chatbot/core.py new file mode 100644 index 0000000..829a2ad --- /dev/null +++ b/utils/llm/chatbot/core.py @@ -0,0 +1,186 @@ +""" +ChatBot 핵심 로직 및 LangGraph 워크플로우 정의 +""" + +from typing import Any, Dict, List, Optional + +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_openai import ChatOpenAI +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import END, START, StateGraph +from openai import OpenAI + +from utils.llm.chatbot.guidelines import GUIDELINES +from utils.llm.chatbot.matcher import LLMGuidelineMatcher +from utils.llm.chatbot.types import ChatBotState, Guideline + + +class ChatBot: + """ + LangGraph를 사용한 대화형 챗봇 클래스 (Guideline 기반) + """ + + def __init__( + self, + openai_api_key: str, + model_name: str = "gpt-4o-mini", + gms_server: str = "http://localhost:8080", + guidelines: Optional[List[Guideline]] = None, + ): + """ + ChatBot 인스턴스 초기화 + + Args: + openai_api_key: OpenAI API 키 + model_name: 사용할 모델명 (기본값: gpt-4o-mini) + gms_server: DataHub GMS 서버 URL (기본값: http://localhost:8080) + guidelines: 사용할 가이드라인 목록 (없으면 기본값 사용) + """ + self.openai_api_key = openai_api_key + self.model_name = model_name + self.gms_server = gms_server + self.guidelines = guidelines or GUIDELINES + self.guideline_map = {g.id: g for g in self.guidelines} + + self._client = OpenAI(api_key=openai_api_key) + self.matcher = LLMGuidelineMatcher( + self.guidelines, + model=self.model_name, + client_obj=self._client, + ) + self.llm = ChatOpenAI( + temperature=0.0, + model_name=self.model_name, + openai_api_key=openai_api_key, + ) + self.app = self._setup_workflow() + + def _setup_workflow(self): + """ + LangGraph 워크플로우 설정 + """ + workflow = StateGraph(state_schema=ChatBotState) + + def select_guidelines(state: ChatBotState): + user_text = "" + # 마지막 사용자 메시지 찾기 + for msg in reversed(state["messages"]): + if isinstance(msg, HumanMessage) or ( + hasattr(msg, "type") and msg.type == "human" + ): + user_text = msg.content + break + + # 만약 메시지 객체 구조가 달라서 못 찾았을 경우를 대비해 마지막 메시지 내용 사용 + if not user_text and state["messages"]: + user_text = state["messages"][-1].content + + matched = self.matcher.match(str(user_text)) + + # 컨텍스트 업데이트 (현재 사용자 메시지 추가) + ctx = state.get("context") or {} + ctx["last_user_message"] = user_text + ctx["gms_server"] = self.gms_server + # search_database_tables_tool을 위해 query 키도 설정 + ctx["query"] = user_text + + outs: List[str] = [] + for g in matched: + for tool in g.tools or []: + try: + # tool 실행 + result = tool(ctx) + outs.append(f"[{g.id}] {result}") + except Exception as exc: + outs.append(f"[tool_error] {tool.__name__}: {exc}") + + return { + "selected_ids": [g.id for g in matched], + "tool_outputs": outs, + "context": ctx, + } + + def call_model(state: ChatBotState): + selected_ids = state.get("selected_ids", []) + tool_outs = state.get("tool_outputs", []) + + guideline_lines = [ + f"- {gid}: {self.guideline_map[gid].description}" + for gid in selected_ids + if gid in self.guideline_map + ] or ["- 적용 가능한 가이드라인 없음 (일반 대화 진행)"] + + tool_lines = tool_outs or ["(툴 실행 결과 없음)"] + + sys_msg = SystemMessage( + content=( + "# 역할\n" + "당신은 사용자의 모호한 질문을 명확하고 구체적인 질문으로 만드는 전문 AI 어시스턴트입니다.\n" + "제공된 툴 실행 결과와 가이드라인을 바탕으로 사용자에게 답변하세요.\n\n" + "# 적용된 가이드라인\n" + + "\n".join(guideline_lines) + + "\n\n# 툴 실행 결과 (참고 정보)\n" + + "\n".join(f"- {line}" for line in tool_lines) + + "\n\n# 지침\n" + "- 툴 실행 결과에 유용한 정보가 있다면 적극적으로 인용하여 답변하세요.\n" + "- 정보가 부족하다면 추가 질문을 통해 구체화하세요.\n" + "- 항상 친절하고 명확하게 대화하세요." + ) + ) + + # 시스템 메시지를 대화의 맨 앞에 추가 (또는 매번 컨텍스트로 주입) + # LangGraph에서는 메시지 리스트가 계속 쌓이므로, + # 이번 턴의 시스템 메시지를 앞에 붙여서 invoke 하는 방식 사용 + messages = [sys_msg] + list(state["messages"]) + response = self.llm.invoke(messages) + return {"messages": response} + + workflow.add_node("select", select_guidelines) + workflow.add_node("respond", call_model) + + workflow.add_edge(START, "select") + workflow.add_edge("select", "respond") + workflow.add_edge("respond", END) + + return workflow.compile(checkpointer=MemorySaver()) + + def chat(self, message: str, thread_id: str): + """ + 사용자 메시지에 대한 응답 생성 + + Args: + message: 사용자 입력 메시지 + thread_id: 대화 세션을 구분하는 고유 ID + + Returns: + dict: LLM 응답을 포함한 결과 딕셔너리 + """ + config = {"configurable": {"thread_id": thread_id}} + + # 초기 상태 설정 + # add_messages 리듀서가 있으므로 messages에는 새 메시지만 넣으면 됨 + input_state = { + "messages": [HumanMessage(content=message)], + "context": {"gms_server": self.gms_server}, + "selected_ids": [], + "tool_outputs": [], + } + + return self.app.invoke(input_state, config) + + def update_model(self, model_name: str): + """ + 사용 중인 LLM 모델 변경 + """ + self.model_name = model_name + self._client = OpenAI(api_key=self.openai_api_key) + self.matcher = LLMGuidelineMatcher( + self.guidelines, + model=self.model_name, + client_obj=self._client, + ) + self.llm = ChatOpenAI( + temperature=0.0, + model_name=self.model_name, + openai_api_key=self.openai_api_key, + ) diff --git a/utils/llm/chatbot/guidelines.py b/utils/llm/chatbot/guidelines.py new file mode 100644 index 0000000..fa36e5f --- /dev/null +++ b/utils/llm/chatbot/guidelines.py @@ -0,0 +1,67 @@ +""" +ChatBot 가이드라인 및 툴 정의 +""" + +from typing import Any, Dict, List + +from utils.llm.tools import ( + search_database_tables, + get_glossary_terms, + get_query_examples, +) +from utils.llm.chatbot.types import Guideline + + +def search_database_tables_tool(ctx: Dict[str, Any]) -> str: + query = ctx.get("query") or ctx.get("last_user_message", "") + return str(search_database_tables.invoke({"query": query})) + + +def get_glossary_terms_tool(ctx: Dict[str, Any]) -> str: + gms_server = ctx.get("gms_server", "http://localhost:8080") + return str(get_glossary_terms.invoke({"gms_server": gms_server})) + + +def get_query_examples_tool(ctx: Dict[str, Any]) -> str: + gms_server = ctx.get("gms_server", "http://localhost:8080") + return str(get_query_examples.invoke({"gms_server": gms_server})) + + +GUIDELINES: List[Guideline] = [ + Guideline( + id="db_search", + description="데이터베이스 테이블 정보나 스키마 확인이 필요할 때 사용", + example_phrases=[ + "테이블 정보 알려줘", + "어떤 컬럼이 있어?", + "스키마 보여줘", + "데이터 구조가 궁금해", + ], + tools=[search_database_tables_tool], + priority=10, + ), + Guideline( + id="glossary", + description="용어의 정의나 비즈니스 의미 확인이 필요할 때 사용", + example_phrases=[ + "용어집 보여줘", + "이 단어 뜻이 뭐야?", + "비즈니스 용어 설명해줘", + "KPI 정의가 뭐야?", + ], + tools=[get_glossary_terms_tool], + priority=8, + ), + Guideline( + id="query_examples", + description="쿼리 예제나 SQL 작성 패턴 확인이 필요할 때 사용", + example_phrases=[ + "쿼리 예제 보여줘", + "비슷한 쿼리 있어?", + "SQL 어떻게 짜야해?", + "다른 사람들은 어떻게 쿼리했어?", + ], + tools=[get_query_examples_tool], + priority=9, + ), +] diff --git a/utils/llm/chatbot/matcher.py b/utils/llm/chatbot/matcher.py new file mode 100644 index 0000000..ab1ba92 --- /dev/null +++ b/utils/llm/chatbot/matcher.py @@ -0,0 +1,84 @@ +""" +LLM 기반 가이드라인 매칭 로직 +""" + +import json +from typing import Any, Dict, List, Optional + +from openai import OpenAI + +from utils.llm.chatbot.types import Guideline + + +class LLMGuidelineMatcher: + def __init__( + self, + guidelines: List[Guideline], + model: str, + client_obj: Optional[OpenAI] = None, + ): + self.guidelines = guidelines + self.model = model + self.client = client_obj or OpenAI() + self._id_set = {g.id for g in guidelines} + + def _build_messages(self, message: str) -> List[Dict[str, str]]: + sys = ( + "You are a strict GuidelineMatcher.\n" + "Return ONLY a JSON object that matches the provided JSON schema." + ) + lines = [ + "아래 사용자 메시지에 해당하는 모든 가이드라인 id를 선택하세요.", + f"[USER MESSAGE]\n{message}\n", + "[GUIDELINES]", + ] + for g in self.guidelines: + examples = ", ".join(g.example_phrases) if g.example_phrases else "-" + lines.append( + f"- id: {g.id}\n desc: {g.description}\n examples: {examples}" + ) + return [ + {"role": "system", "content": sys}, + {"role": "user", "content": "\n".join(lines)}, + ] + + def _json_schema_spec(self) -> Dict[str, Any]: + return { + "name": "guideline_matches", + "schema": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": {"type": "string", "enum": list(self._id_set)}, + } + }, + "required": ["matches"], + "additionalProperties": False, + }, + "strict": True, + } + + def match(self, message: str) -> List[Guideline]: + ids: List[str] = [] + try: + completion = self.client.chat.completions.create( + model=self.model, + temperature=0, + messages=self._build_messages(message), + response_format={ + "type": "json_schema", + "json_schema": self._json_schema_spec(), + }, + ) + raw = completion.choices[0].message.content + data = json.loads(raw) if isinstance(raw, str) else raw + ids = [i for i in (data.get("matches") or []) if i in self._id_set] + except Exception: + # LLM 호출 실패 시 빈 리스트 반환 (일반 대화로 처리) + ids = [] + + id_to_g = {g.id: g for g in self.guidelines} + selected = [id_to_g[i] for i in ids if i in id_to_g] + selected.sort(key=lambda g: g.priority, reverse=True) + return selected diff --git a/utils/llm/chatbot/types.py b/utils/llm/chatbot/types.py new file mode 100644 index 0000000..ab23240 --- /dev/null +++ b/utils/llm/chatbot/types.py @@ -0,0 +1,33 @@ +""" +ChatBot 관련 데이터 타입 및 구조 정의 +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional, Sequence, TypedDict, Annotated + +from langchain_core.messages import BaseMessage +from langgraph.graph.message import add_messages + +ToolFn = Callable[[Dict[str, Any]], Any] + + +@dataclass +class Guideline: + id: str + description: str + example_phrases: List[str] + tools: Optional[List[ToolFn]] = None + priority: int = 0 + + +class ChatBotState(TypedDict): + """ + 챗봇 상태 + """ + + messages: Annotated[Sequence[BaseMessage], add_messages] + context: Dict[str, Any] + selected_ids: List[str] + tool_outputs: List[str]