import asyncio import logging import os import sys import uuid from pathlib import Path import gradio as gr from dotenv import load_dotenv # 환경 변수 로드 (에이전트/트레이싱 import 이전에 실행) load_dotenv() # 프로젝트 루트를 경로에 추가 sys.path.insert(0, str(Path(__file__).parent.parent)) from src.agent.graph import agent from src.agent.state import AgentState # 로깅 설정 (WARNING 이상만 출력) logging.basicConfig( level=logging.WARNING, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) # 외부 라이브러리 로그는 WARNING만 logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) logging.getLogger("langsmith").setLevel(logging.WARNING) # CodeWeaver 모듈 로그도 WARNING만 (로그 비활성화) logging.getLogger("src.agent").setLevel(logging.WARNING) logging.getLogger("src.tools").setLevel(logging.WARNING) logging.getLogger("src.vector_db").setLevel(logging.WARNING) logger = logging.getLogger(__name__) async def chat( message: str, history: list, thread_id: str, ) -> str: """ 사용자 메시지를 처리하고 에이전트 응답을 반환합니다. Args: message: 사용자 입력 메시지 history: 대화 내역 (Gradio 자동 관리) thread_id: 세션별 고유 ID (MemorySaver가 대화 맥락 추적에 사용) Returns: 에이전트의 최종 답변 """ if not message or not message.strip(): return "질문을 입력해주세요." try: # 초기 상태 생성 (Pydantic BaseModel 사용) from langchain_core.messages import HumanMessage initial_state = AgentState( user_question=message.strip(), messages=[HumanMessage(content=message.strip())], conversation_history=history[-5:] if history else None, # 최근 5턴만 전달 ) # 세션별 thread_id를 config에 전달 (MemorySaver가 대화 맥락 유지) config = {"configurable": {"thread_id": thread_id}} # 에이전트 실행 result = await agent.ainvoke(initial_state, config=config) # 최종 답변 추출 final_answer = result.get("final_answer", "답변을 생성하지 못했습니다.") return final_answer except Exception as e: logger.error("에러 발생: %s", e, exc_info=True) return f"⚠️ 오류가 발생했습니다: {str(e)}\n다시 시도해주세요." def create_demo() -> gr.Blocks: """Gradio 인터페이스를 생성합니다.""" # CSS 스타일 (깔끔한 디자인) # - Gradio 기본 CSS가 .contain/.gradio-container 폭을 덮어쓰는 경우가 있어 # 둘 다 !important로 고정하여 "처음부터 넓은 폭"을 확실히 유지합니다. css = """ .gradio-container { max-width: 1280px !important; width: min(1280px, 100%) !important; margin: 0 auto !important; } .contain { max-width: 1280px !important; width: min(1280px, 100%) !important; margin: 0 auto !important; padding-top: 1.5rem; } .message { font-size: 1.1rem; line-height: 1.6; } """ with gr.Blocks( title="CodeWeaver - AI 개발 도우미", theme=gr.themes.Soft(), css=css ) as demo: gr.Markdown(""" # 🤖 CodeWeaver ### AI 기반 개발 질문 답변 시스템 초보 개발자를 위한 친절한 AI 도우미입니다. **주요 기능:** - ✅ 에러 해결 (디버깅) - ✅ 개념 학습 - ✅ 코드 리뷰 및 개선 제안 - ✅ **다중 질문 처리** (최대 2개까지 동시 처리) - ✅ **대화 맥락 이해** (이전 대화를 참고한 후속 질문 답변) - ✅ **스마트 캐싱** (유사 질문 즉시 답변) - ✅ **자동 검색 개선** (결과 부족 시 쿼리 자동 최적화) 💬 개발 관련 질문을 자유롭게 해보세요! - 단일 질문: "Spring Boot JPA N+1 문제 해결 방법은?" - 다중 질문: "JWT가 뭐야? CORS는?" (최대 2개) - 후속 질문: "좀 더 쉽게 설명해줘" (이전 답변 참고) """) # 세션별 고유 ID (브라우저 세션마다 독립적으로 생성) session_id = gr.State(value=lambda: str(uuid.uuid4())) # 채팅 인터페이스 chatbot_interface = gr.ChatInterface( fn=chat, examples=None, # examples는 아래 Accordion에서 수동 처리 chatbot=gr.Chatbot(height=500), textbox=gr.Textbox( placeholder="질문을 입력하세요...", container=False, scale=7 ), retry_btn=None, undo_btn=None, clear_btn="🗑️ 대화 초기화", additional_inputs=[session_id], # thread_id 전달 ) # Clear 버튼 클릭 시 새 세션 ID 생성 (새 대화 시작) def reset_session(): new_id = str(uuid.uuid4()) return new_id # Clear 버튼에 세션 리셋 핸들러 추가 if chatbot_interface.clear_btn: chatbot_interface.clear_btn.click( reset_session, None, session_id, queue=False ) # 빠른 질문 버튼들 (Accordion 밖으로 분리) gr.Markdown("### 💬 예시 질문") example_questions = [ "Spring Boot JPA N+1 문제 해결 방법은?", "ImportError: No module named 'requests' 해결 방법", "Docker Compose 설정 예제를 알려주세요", "이 코드를 개선해주세요: for i in range(len(arr)): print(arr[i])", "JWT가 뭐야? CORS는?", # 다중 질문 예시 ] with gr.Row(): for question in example_questions: btn = gr.Button( question, variant="secondary", size="sm", scale=1, ) # 버튼 클릭 시 입력창에 자동 입력 btn.click( fn=lambda q=question: q, outputs=[chatbot_interface.textbox], ) # 정보 섹션 with gr.Accordion("📊 시스템 정보", open=False): gr.Markdown(""" ### 사용된 기술 - **LLM**: Gemini 2.5 Flash Lite - **임베딩**: BAAI/bge-m3 (로컬) - **벡터 DB**: Qdrant Cloud - **검색 API**: Stack Overflow, GitHub, Tavily - **프레임워크**: LangGraph ### 주요 기능 - 🔍 **병렬 검색**: Stack Overflow, GitHub, 공식 문서 동시 검색 - 💾 **의미적 캐싱**: 유사 질문(임계값 0.85 이상) 즉시 답변 - 🎯 **의도 기반 라우팅**: debugging/learning/code_review 자동 분류 - 🔄 **자동 쿼리 개선**: 검색 결과 부족 시 최대 1회 자동 최적화 - 📝 **초보자 친화 답변**: 의도별 맞춤형 답변 구조 - 🔀 **다중 질문 처리**: 독립 질문 2개까지 병렬 처리 - 💬 **대화 맥락 이해**: clarification 질문은 히스토리 기반 답변 ### LangGraph로 구현한 핵심 기능 1. ✅ **Conditional Edges**: 질문 유형/캐시 여부/검색 결과에 따른 동적 라우팅 2. ✅ **Send API**: 3개 검색 소스 병렬 실행 (fan-out/fan-in) 3. ✅ **Subgraph**: 검색 결과 필터링 및 요약 파이프라인 4. ✅ **Map-Reduce**: 다중 질문 처리 시 각 질문별 독립 실행 후 결과 통합 5. ✅ **Checkpointing**: MemorySaver로 대화 상태 저장 및 재개 6. ✅ **Pydantic Typed State**: 타입 안전한 상태 관리 ### GitHub [프로젝트 소스코드](https://github.com/shin-heewon/codeweaver) """) # 사용 가이드 with gr.Accordion("💡 사용 팁", open=False): gr.Markdown(""" ### 1. 구체적으로 질문하기 - ❌ "파이썬 에러" - ✅ "ImportError: No module named 'requests' 해결 방법" ### 2. 질문 유형별 예시 - **디버깅**: "이 에러 메시지는 무엇을 의미하나요?" - **학습**: "JPA N+1 문제는 왜 발생하나요?" - **코드 리뷰**: "이 코드를 더 효율적으로 개선하려면?" ### 3. 다중 질문 사용법 - ✅ **2개까지 가능**: "JWT가 뭐야? CORS는?" - ❌ **3개 이상 불가**: "JWT? CORS? Docker?" → 안내 메시지 표시 - 💡 **팁**: 관련 질문은 하나로 통합하거나, 순차적으로 질문하세요 ### 4. 대화 맥락 활용 - **후속 질문**: "좀 더 쉽게 설명해줘", "예제 코드로 보여줘" - **새 개념 질문**: 대화 중에도 "Event Listener는 뭐야?" 같은 독립 질문 가능 - 💡 **팁**: 이전 대화를 참고한 답변이 필요하면 자연스럽게 질문하세요 ### 5. 응답 시간 - **첫 질문**: 10~15초 소요 (검색 + 답변 생성) - **유사 질문**: 즉시 답변 (캐시 활용, 임계값 0.85 이상) - **다중 질문**: 각 질문별 병렬 처리로 효율적 ### 6. 더 나은 답변을 위한 팁 - 에러 메시지를 포함해주세요 - 사용 중인 언어/프레임워크를 명시하세요 - 시도했던 해결 방법을 함께 알려주세요 - 검색 결과가 부족하면 자동으로 쿼리를 개선합니다 (최대 1회) """) return demo # 앱 생성 app = create_demo() if __name__ == "__main__": # 로컬 실행 app.launch( server_name="0.0.0.0", server_port=7860, share=False, # True로 하면 공개 URL 생성 show_api=False, # Gradio 4.44.x 버그 우회용 )