Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ff6e437
fix: 포트번호 고정
hyeong8465 Aug 16, 2025
da56f3d
refator: sql_agent 코드 스플릿
hyeong8465 Aug 17, 2025
52e90fb
refactor: 엔드 포인트 및 라우터
hyeong8465 Aug 17, 2025
ba4a953
refactor: init 파일 추가
hyeong8465 Aug 17, 2025
73082ae
feat: api 요청을 위한 클라이언트
hyeong8465 Aug 17, 2025
8b2c622
refactor: llm 연결 수정
hyeong8465 Aug 17, 2025
da39b4f
refactor: db_manager 삭제
hyeong8465 Aug 17, 2025
ed0c73b
refactor: 헬스체크 수정
hyeong8465 Aug 17, 2025
85cc1d1
refactor: 데이터 모델
hyeong8465 Aug 17, 2025
08c533a
refactor: 어노테이션 서비스
hyeong8465 Aug 17, 2025
c435bbd
refactor: 챗봇 서비스
hyeong8465 Aug 17, 2025
94be712
refactor: database 서비스
hyeong8465 Aug 17, 2025
74ca365
refactor: 고정포트 및 로그 추가
hyeong8465 Aug 17, 2025
70a2dd1
refactor: 쿼리 실행 요청 및 응답 모델 수정
hyeong8465 Aug 17, 2025
fcb8fe6
refactor: API 키 호출 로직 수정 및 예외 처리 개선
hyeong8465 Aug 17, 2025
d112991
test: 테스트 코드 추가
hyeong8465 Aug 17, 2025
065a39a
feat: 그래프 시각화 저장 기능 추가
hyeong8465 Aug 17, 2025
a63cf2a
refactor: API 키 및 데이터베이스 호출 로직 개선, 폴백 처리 추가
hyeong8465 Aug 17, 2025
e8f4d97
feat: 의도 분류 기능 개선 및 채팅 내역 활용 추가
hyeong8465 Aug 17, 2025
843e86a
chore: 기존 헬스 체크 모듈 삭제
hyeong8465 Aug 17, 2025
5d04e63
refactor: 에러 처리 방식 개선 - 예외를 라우터에서 처리하도록 변경
hyeong8465 Aug 18, 2025
92d1da9
refactor: 그래프 실행 중 에러 처리 방식을 개선하여 예외를 상위 레벨로 전달
hyeong8465 Aug 18, 2025
fc6877b
feat: 쿼리 실행 API 연결
hyeong8465 Aug 18, 2025
954373b
refactor: SQL 쿼리 실행 중 에러 처리 개선 및 폴백 로직 제거
hyeong8465 Aug 18, 2025
cd5c4d2
refactor: 채팅 메시지 역할 수정
hyeong8465 Aug 18, 2025
f760f50
feat: api key 전달 API 연결
hyeong8465 Aug 18, 2025
7b11ce1
refactor: 어노테이션 모델 클래스 구조 변경
hyeong8465 Aug 18, 2025
cf290e0
test: 테스트 파일 수정
hyeong8465 Aug 18, 2025
2eb6d53
fix: API 키 조회 경로 수정 및 복호화된 데이터 처리 로직 개선
hyeong8465 Aug 18, 2025
c61c13b
feat: End-to-End 채팅 및 어노테이션 기능 테스트 추가, 에러 시나리오 테스트 구현
hyeong8465 Aug 18, 2025
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
Binary file added sql_agent_workflow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
에이전트 루트 패키지
"""



27 changes: 27 additions & 0 deletions src/agents/sql_agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# src/agents/sql_agent/__init__.py

from .state import SqlAgentState
from .nodes import SqlAgentNodes
from .edges import SqlAgentEdges
from .graph import SqlAgentGraph
from .exceptions import (
SqlAgentException,
ValidationException,
ExecutionException,
DatabaseConnectionException,
LLMProviderException,
MaxRetryExceededException
)

__all__ = [
'SqlAgentState',
'SqlAgentNodes',
'SqlAgentEdges',
'SqlAgentGraph',
'SqlAgentException',
'ValidationException',
'ExecutionException',
'DatabaseConnectionException',
'LLMProviderException',
'MaxRetryExceededException'
]
51 changes: 51 additions & 0 deletions src/agents/sql_agent/edges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# src/agents/sql_agent/edges.py

from .state import SqlAgentState

# 상수 정의
MAX_ERROR_COUNT = 3

class SqlAgentEdges:
"""SQL Agent의 모든 엣지 로직을 담당하는 클래스"""

@staticmethod
def route_after_intent_classification(state: SqlAgentState) -> str:
"""의도 분류 결과에 따라 라우팅을 결정합니다."""
if state['intent'] == "SQL":
print("--- 의도: SQL 관련 질문 ---")
return "db_classifier"
print("--- 의도: SQL과 관련 없는 질문 ---")
return "unsupported_question"

@staticmethod
def should_execute_sql(state: SqlAgentState) -> str:
"""SQL 검증 결과에 따라 다음 단계를 결정합니다."""
validation_error_count = state.get("validation_error_count", 0)

if validation_error_count >= MAX_ERROR_COUNT:
print(f"--- 검증 실패 {MAX_ERROR_COUNT}회 초과: 답변 생성으로 이동 ---")
return "synthesize_failure"

if state.get("validation_error"):
print(f"--- 검증 실패 {validation_error_count}회: SQL 재생성 ---")
return "regenerate"

print("--- 검증 성공: SQL 실행 ---")
return "execute"

@staticmethod
def should_retry_or_respond(state: SqlAgentState) -> str:
"""SQL 실행 결과에 따라 다음 단계를 결정합니다."""
execution_error_count = state.get("execution_error_count", 0)
execution_result = state.get("execution_result", "")

if execution_error_count >= MAX_ERROR_COUNT:
print(f"--- 실행 실패 {MAX_ERROR_COUNT}회 초과: 답변 생성으로 이동 ---")
return "synthesize_failure"

if "오류" in execution_result:
print(f"--- 실행 실패 {execution_error_count}회: SQL 재생성 ---")
return "regenerate"

print("--- 실행 성공: 최종 답변 생성 ---")
return "synthesize_success"
31 changes: 31 additions & 0 deletions src/agents/sql_agent/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# src/agents/sql_agent/exceptions.py

class SqlAgentException(Exception):
"""SQL Agent 관련 기본 예외 클래스"""
pass

class ValidationException(SqlAgentException):
"""SQL 검증 실패 예외"""
def __init__(self, message: str, error_count: int = 0):
super().__init__(message)
self.error_count = error_count

class ExecutionException(SqlAgentException):
"""SQL 실행 실패 예외"""
def __init__(self, message: str, error_count: int = 0):
super().__init__(message)
self.error_count = error_count

class DatabaseConnectionException(SqlAgentException):
"""데이터베이스 연결 실패 예외"""
pass

class LLMProviderException(SqlAgentException):
"""LLM 제공자 관련 예외"""
pass

class MaxRetryExceededException(SqlAgentException):
"""최대 재시도 횟수 초과 예외"""
def __init__(self, message: str, max_retries: int):
super().__init__(f"{message} (최대 재시도 {max_retries}회 초과)")
self.max_retries = max_retries
129 changes: 129 additions & 0 deletions src/agents/sql_agent/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# src/agents/sql_agent/graph.py

from langgraph.graph import StateGraph, END
from core.providers.llm_provider import LLMProvider
from services.database.database_service import DatabaseService
from .state import SqlAgentState
from .nodes import SqlAgentNodes
from .edges import SqlAgentEdges

class SqlAgentGraph:
"""SQL Agent 그래프를 구성하고 관리하는 클래스"""

def __init__(self, llm_provider: LLMProvider, database_service: DatabaseService):
self.llm_provider = llm_provider
self.database_service = database_service
self.nodes = SqlAgentNodes(llm_provider, database_service)
self.edges = SqlAgentEdges()
self._graph = None

def create_graph(self) -> StateGraph:
"""SQL Agent 그래프를 생성하고 구성합니다."""
if self._graph is not None:
return self._graph

graph = StateGraph(SqlAgentState)

# 노드 추가
self._add_nodes(graph)

# 엣지 추가
self._add_edges(graph)

# 진입점 설정
graph.set_entry_point("intent_classifier")

# 그래프 컴파일
self._graph = graph.compile()
return self._graph

def _add_nodes(self, graph: StateGraph):
"""그래프에 모든 노드를 추가합니다."""
graph.add_node("intent_classifier", self.nodes.intent_classifier_node)
graph.add_node("db_classifier", self.nodes.db_classifier_node)
graph.add_node("unsupported_question", self.nodes.unsupported_question_node)
graph.add_node("sql_generator", self.nodes.sql_generator_node)
graph.add_node("sql_validator", self.nodes.sql_validator_node)
graph.add_node("sql_executor", self.nodes.sql_executor_node)
graph.add_node("response_synthesizer", self.nodes.response_synthesizer_node)

def _add_edges(self, graph: StateGraph):
"""그래프에 모든 엣지를 추가합니다."""
# 의도 분류 후 조건부 라우팅
graph.add_conditional_edges(
"intent_classifier",
self.edges.route_after_intent_classification,
{
"db_classifier": "db_classifier",
"unsupported_question": "unsupported_question"
}
)

# 지원되지 않는 질문 처리 후 종료
graph.add_edge("unsupported_question", END)

# DB 분류 후 SQL 생성으로 이동
graph.add_edge("db_classifier", "sql_generator")

# SQL 생성 후 검증으로 이동
graph.add_edge("sql_generator", "sql_validator")

# SQL 검증 후 조건부 라우팅
graph.add_conditional_edges(
"sql_validator",
self.edges.should_execute_sql,
{
"regenerate": "sql_generator",
"execute": "sql_executor",
"synthesize_failure": "response_synthesizer"
}
)

# SQL 실행 후 조건부 라우팅
graph.add_conditional_edges(
"sql_executor",
self.edges.should_retry_or_respond,
{
"regenerate": "sql_generator",
"synthesize_success": "response_synthesizer",
"synthesize_failure": "response_synthesizer"
}
)

# 응답 생성 후 종료
graph.add_edge("response_synthesizer", END)

async def run(self, initial_state: dict) -> dict:
"""그래프를 실행하고 결과를 반환합니다."""
try:
if self._graph is None:
self.create_graph()

result = await self._graph.ainvoke(initial_state)
return result

except Exception as e:
print(f"그래프 실행 중 오류 발생: {e}")
# 에러 발생 시 예외를 다시 발생시켜 상위 레벨에서 HTTP 에러로 처리되도록 함
raise e

def save_graph_visualization(self, file_path: str = "sql_agent_graph.png") -> bool:
"""그래프 시각화를 파일로 저장합니다."""
try:
if self._graph is None:
self.create_graph()

# PNG 이미지 생성
png_data = self._graph.get_graph(xray=True).draw_mermaid_png()

# 파일로 저장
with open(file_path, "wb") as f:
f.write(png_data)

print(f"그래프 시각화가 {file_path}에 저장되었습니다.")
return True

except Exception as e:
print(f"그래프 시각화 저장 실패: {e}")
return False

Loading