From ed98a2c188fb500ad8b5354053f6217d95c356a7 Mon Sep 17 00:00:00 2001 From: YiHeeJu <“lehejo0330@cu.ac.kr”> Date: Wed, 16 Apr 2025 10:46:35 +0900 Subject: [PATCH] =?UTF-8?q?langchain=20=EB=AA=A8=EB=8D=B8=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/llm/__init__.py | 0 app/llm/agent_router.py | 63 ++++++++++++++ app/llm/goal_agents.py | 90 ++++++++++++++++++++ app/llm/llm_provider.py | 15 ++++ app/llm/progress_analysis.py | 158 +++++++++++++++++++++++++++++++++++ app/llm/tools.py | 59 +++++++++++++ test_user_summary.py | 34 ++++++++ 7 files changed, 419 insertions(+) create mode 100644 app/llm/__init__.py create mode 100644 app/llm/agent_router.py create mode 100644 app/llm/goal_agents.py create mode 100644 app/llm/llm_provider.py create mode 100644 app/llm/progress_analysis.py create mode 100644 app/llm/tools.py create mode 100644 test_user_summary.py diff --git a/app/llm/__init__.py b/app/llm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/llm/agent_router.py b/app/llm/agent_router.py new file mode 100644 index 0000000..4fd0561 --- /dev/null +++ b/app/llm/agent_router.py @@ -0,0 +1,63 @@ +from app.llm.tools import detect_intent, detect_goal_function +from app.llm.goal_agents import ( + get_english_response, get_coding_response, get_fitness_response, + get_english_question, get_coding_question, + get_english_info, get_coding_info, get_fitness_info, + get_english_mentalcare, get_coding_mentalcare, get_fitness_mentalcare, +) + +VALID_GOALS = ["영어", "코딩", "운동"] + +# goal 별 응답 함수 매핑 +RESPONSE_FUNCS = { + "영어": get_english_response, + "코딩": get_coding_response, + "운동": get_fitness_response, +} + +QUESTION_FUNCS = { + "영어": get_english_question, + "코딩": get_coding_question, +} + +INFO_FUNCS = { + "영어": get_english_info, + "코딩": get_coding_info, + "운동": get_fitness_info, +} + +MENTAL_FUNCS = { + "영어": get_english_mentalcare, + "코딩": get_coding_mentalcare, + "운동": get_fitness_mentalcare, +} + + +# 에이전트 선택을 제어하는 핵심 로직 +def route_to_agent(user_input: str, context: dict) -> str: + goal = context.get("goal") + if goal not in VALID_GOALS: + return "목표는 영어, 코딩, 운동 중 하나여야 합니다." + + # intent와 goal_function_type 분석 + intent = detect_intent(user_input) + goal_func_type = detect_goal_function(user_input, goal) + + # 멘탈케어는 intent 기준으로 분기 + if intent == "멘탈케어" and goal in MENTAL_FUNCS: + return MENTAL_FUNCS[goal](user_input) + + # 특수 요청: 문제 생성 + if goal_func_type == "문제 생성" and goal in QUESTION_FUNCS: + return QUESTION_FUNCS[goal](user_input) + + # 특수 요청: 정보 제공 + if goal_func_type == "정보 제공" and goal in INFO_FUNCS: + return INFO_FUNCS[goal](user_input) + + # 일반 응답 (피드백, 로드맵) + response_func = RESPONSE_FUNCS.get(goal) + if response_func: + return response_func(user_input, intent) + + return "알 수 없는 목표나 요청이에요." diff --git a/app/llm/goal_agents.py b/app/llm/goal_agents.py new file mode 100644 index 0000000..ba073fc --- /dev/null +++ b/app/llm/goal_agents.py @@ -0,0 +1,90 @@ +from langchain.prompts import PromptTemplate +from app.llm.llm_provider import get_agent + +llm = get_agent() + + +# 통합 프롬프트 템플릿 + + +general_prompt = PromptTemplate.from_template(""" +당신은 {goal} 분야의 따뜻하고 실용적인 전문가입니다. + +[사용자 요청] +"{user_input}" + +[사용자의 현재 상태] +요청 유형: {intent} + +{extra_instruction} +""") + + + +# 일반 응답 (피드백, 로드맵) +def get_response(goal: str, user_input: str, intent: str) -> str: + instruction = "사용자의 질문에 전문가로서 조언이나 피드백을 제공해주세요." if intent == "피드백" else \ + "사용자의 목표에 맞는 단계별 학습 로드맵이나 실천 계획을 제시해주세요." if intent == "로드맵" else \ + "질문에 대해 상황을 이해하고 적절한 조언을 제공해주세요." + + return llm.invoke(general_prompt.format( + goal=goal, + user_input=user_input, + intent=intent, + extra_instruction=instruction + )).content.strip() + +get_english_response = lambda u, i: get_response("영어", u, i) +get_coding_response = lambda u, i: get_response("코딩", u, i) +get_fitness_response = lambda u, i: get_response("운동", u, i) + + + +# 문제 생성 +def get_question(goal: str, user_input: str) -> str: + instruction = f"{goal} 분야의 간단한 퀴즈나 연습 문제를 생성해주세요. 상황이나 감정도 반영해주세요." + return llm.invoke(general_prompt.format( + goal=goal, + user_input=user_input, + intent="문제 생성", + extra_instruction=instruction + )).content.strip() + +get_english_question = lambda u: get_question("영어", u) +get_coding_question = lambda u: get_question("코딩", u) + + + +# 정보 제공 +def get_info(goal: str, user_input: str) -> str: + instruction = f"{goal}과 관련된 시험, 자격증, 학습 팁, 활동 정보 등을 전문가 입장에서 자세히 제공해주세요." + return llm.invoke(general_prompt.format( + goal=goal, + user_input=user_input, + intent="정보 제공", + extra_instruction=instruction + )).content.strip() + +get_english_info = lambda u: get_info("영어", u) +get_coding_info = lambda u: get_info("코딩", u) +get_fitness_info = lambda u: get_info("운동", u) + + + +# 멘탈케어 응답 +def get_mentalcare(goal: str, user_input: str) -> str: + instruction = f""" +당신은 {goal} 분야의 학습이나 실천 과정에서 지친 사람에게 +공감과 위로, 회복을 돕는 따뜻한 메시지를 전달하는 전문가입니다. +현재 감정과 상황을 고려하여 부담스럽지 않게 응원해주세요. +""" + return llm.invoke(general_prompt.format( + goal=goal, + user_input=user_input, + intent="멘탈케어", + extra_instruction=instruction + )).content.strip() + +get_english_mentalcare = lambda u: get_mentalcare("영어", u) +get_coding_mentalcare = lambda u: get_mentalcare("코딩", u) +get_fitness_mentalcare = lambda u: get_mentalcare("운동", u) diff --git a/app/llm/llm_provider.py b/app/llm/llm_provider.py new file mode 100644 index 0000000..0501d54 --- /dev/null +++ b/app/llm/llm_provider.py @@ -0,0 +1,15 @@ + +from functools import lru_cache +from langchain_community.chat_models import AzureChatOpenAI +from app.core.config import settings + +@lru_cache() +def get_agent(): + return AzureChatOpenAI( + azure_deployment=settings.AZURE_OPENAI_DEPLOYMENT_NAME, + azure_endpoint=settings.AZURE_OPENAI_ENDPOINT, + openai_api_key=settings.AZURE_OPENAI_API_KEY, + openai_api_version=settings.AZURE_OPENAI_API_VERSION, + temperature=0.5 + ) + diff --git a/app/llm/progress_analysis.py b/app/llm/progress_analysis.py new file mode 100644 index 0000000..41a9b14 --- /dev/null +++ b/app/llm/progress_analysis.py @@ -0,0 +1,158 @@ +from app.llm.llm_provider import get_agent +from langchain_community.utilities import SQLDatabase +from langchain.prompts import PromptTemplate +from app.core.config import settings +import json + +# LLM & DB 연결 +llm = get_agent() + +db = SQLDatabase.from_uri( + settings.SYNC_DATABASE_URL, + include_tables= ["progresses", "todos", "categories"], + sample_rows_in_table_info=2 # 테이블 구조 예시를 GPT가 이해하도록 제공 +) + +# 목표 한글 → DB ENUM 값 매핑 +GOAL_TO_ENUM = { + "영어": "ENGLISH", + "코딩": "CODING", + "운동": "EXERCISE", +} + + +# GPT 응답 JSON 파싱 유틸 함수 +def extract_json(text: str, label: str): + try: + start = text.index('{') + end = text.rindex('}') + 1 + return json.loads(text[start:end]) + except Exception as e: + return {"error": f"{label} JSON parse 실패", "raw": text} + + + +# 전체 목표 평균 달성률 요약 +async def get_user_summary(user_id: str): + query = f""" + SELECT AVG(progress_rate) AS average_rate + FROM progresses + WHERE user_id = '{user_id}'; + """ + + result = db.run(query) + + prompt = PromptTemplate.from_template(""" + 당신은 목표 달성률을 기반으로 희망적인 복사정의 기준문장을 생성하는 AI입니다. + + - 평균 달성률 수치를 기반으로 하되, 사용자가 성취에 대한 자신감을 가질 수 있도록 유도해야함. + - '사용자'는 표현은 제외하고 계속해서 자연스러운 문장으로 작성되어야함. + - 달성률이 낮아도 격려하거나 앞으로의 가능성을 강조해 줘야함. + - 문장은 따뜻하고 응원하는 톤으로, 반드시 존댓말로 작성할 것. 문장은 한 문장 이내, 70자 이내로 작성할 것. + - 길이는 2줄 이내여야 하며, 아래 JSON 형식으로 응답해야 함. + - 모든 응답은 반드시 **자연스러운 한국어**로 작성할 것. 영어나 외국어는 사용 금지. + + 평균 달성률: {progress_data} + + 아래 JSON 형식으로 출력하세요: + ```json + {{ + "summary": "..." + }} + """) + formatted = prompt.format(progress_data=result) + response = llm.invoke(formatted) + return extract_json(response.content, "summary") + + + +# 목표별 강점 & 개선점 분석 +async def get_strength_weakness(user_id: str): + query = f""" + WITH ranked_goals AS ( + SELECT c.category_name AS goal, AVG(p.progress_rate) AS avg_rate + FROM progresses p + JOIN categories c ON p.category_id = c.id + WHERE p.user_id = '{user_id}' + GROUP BY c.category_name + ) + SELECT * FROM ranked_goals ORDER BY avg_rate DESC; + """ + + results = db.run(query) + + prompt = PromptTemplate.from_template(""" + 당신은 목표별 평균 달성률을 분석하고, 가장 높은 항목은 강점으로, 가장 낮은 항목은 감정점으로 표현하는 AI입니다. + + 요약 문장은 반드시 짧고 간결하게 작성할 것: + - 각 문장은 1줄, 70자 이내로 작성되어야 함. + - '사용자'는 표현은 제외하고 자연스러운 일상 문장처럼 작성해야 함. + - 강점은 격려 문장, 개선점은 실천을 유도하는 조언 문장으로 모두 존댓말로 작성할 것. + - 모든 응답은 반드시 **자연스러운 한국어**로 작성되어야 함. 영어나 외국어는 사용하면 안됨. + + 아래는 사용자의 목표별 평균 달성률임: + {ranked_goals} + + 아래 JSON 형식으로 출력하세요: + ```json + {{ + "strength": "...", + "weakness": "..." + }} + """) + formatted = prompt.format(ranked_goals=results) + response = llm.invoke(formatted) + return extract_json(response.content, "strength_weakness") + + + + +# 목표별 도전과제 추천 +async def get_goal_challenges(user_id: str, goal: str): + # 한글 목표명을 ENUM 값으로 매핑 + category_enum = GOAL_TO_ENUM.get(goal, None) + if category_enum is None: + return {"error": f"❌ goal '{goal}'은 지원되지 않음"} + + query = f""" + SELECT t.content, t.is_completed, t.start_date, t.end_date + FROM todos t + JOIN categories c ON t.category_id = c.id + WHERE t.user_id = '{user_id}' AND c.category_name = '{category_enum}' + AND (t.start_date >= CURRENT_DATE - INTERVAL '6 months' OR t.end_date >= CURRENT_DATE - INTERVAL '6 months') + ORDER BY t.start_date DESC; + """ + + results = db.run(query) + + prompt = PromptTemplate.from_template(""" + 당신은 도전과제를 기획하는 전문가입니다. + + 선택된 목표: '{goal}' + 최근 기록된 활동 내역(최대 6개월 기준, 실제 기록이 적을 수 있음): + {todo_info} + + 활동 내용을 기반으로 새로운 도전과제를 하나 추천할 것. + 단, 기록이 부족한 경우에는 격려 문구를 포함한 실현 가능한 추천을 생성할 것. + 도전과제는 다음 기준을 충족해야 함: + + - 활동과 연관된 자격증, 시험, 수료증, 경진대회 등 실질적인 성취로 연결될 것 + - 사용자가 모르고 있을 수 있는 정보도 포함 가능 + - 예: 파이썬 → 정보처리기사, 크롤링 → 데이터 분석 전문가, 영어 회화 → OPIC, 운동 → 요가 지도자 수료증 등 + - 도전과제는 너무 거창하지 않으면서도 실현 가능하고, 자격증·공식 시험·현실성 있는 실습 챌린지 중심이어야 함. + - 모든 응답은 반드시 **자연스러운 한국어**로 작성할것. 영어나 외국어는 절대 사용하면 안됨. + - 아래 형식의 각 항목은 반드시 **간결하게**, 1~2문장 이내로 작성하며, 모든 문장은 존댓말로 작성할 것 (특히 reason과 motivation은 요점 중심으로 작성) + + 아래 JSON 형식으로 출력하세요: + ```json + {{ + "title": "...", + "reason": "...", ← 배경 설명 없이 추천 이유만 간단히 + "motivation": "...", ← 실천 유도 문장 위주, 핵심만 작성 + "duration": "...", + "difficulty": "하/중/상" + }} + """) + formatted = prompt.format(goal=goal, todo_info=results) + response = llm.invoke(formatted) + return extract_json(response.content, f"{goal}_challenge") \ No newline at end of file diff --git a/app/llm/tools.py b/app/llm/tools.py new file mode 100644 index 0000000..41ba15c --- /dev/null +++ b/app/llm/tools.py @@ -0,0 +1,59 @@ +from langchain.schema import HumanMessage +from app.llm.llm_provider import get_agent + + +# 프롬프트 템플릿 +# 사용자 입력을 4가지 유형 중 하나로 분류하기 위한 가이드 +INTENT_GUIDANCE = """ +다음은 사용자의 요청 유형을 분류하는 기준이야야. 분류는 반드시 다음 4개 중 하나로만 해줘줘: + +1. 피드백: 사용자의 활동이나 학습 결과에 대한 평가, 코칭, 잘하고 있는지 질문하는 경우 +2. 로드맵: 앞으로 무엇을 해야 할지, 계획/단계 추천을 요청하는 경우 +3. 멘탈케어: 감정적 표현이 포함된 경우 (예: 지쳤다, 힘들다, 위로받고 싶다) +4. 확장 요청: 문제를 만들어달라, 예시 보여달라, 시험/정보를 요청하는 등의 경우 + +사용자의 입력을 기반으로 위 4가지 중 가장 적절한 하나를 골라. 단어 그대로 "피드백", "로드맵", "멘탈케어", "확장 요청" 중 하나만 답변하도록 해. +""" + +# 목표(goal)에 따라 입력 요청을 분류하기 위한 템플릿 (문제 생성 / 정보 제공 / 일반 요청) +GOAL_FUNCTION_GUIDANCE_TEMPLATE = """ +너는 '{goal}' 분야의 전문 AI 코치야. +사용자의 요청이 아래 유형 중 하나인지 판단해줘: + +- 문제 생성: 퀴즈, 예시 문제, 테스트 요청 +- 정보 제공: 자격증, 시험, 실습, 동작 등에 대한 구체적인 정보 요청 +- 일반 요청: 일상 대화, 피드백, 코칭 등 자유로운 조언 + +반드시 다음 중 하나로만 응답해: 문제 생성 / 정보 제공 / 일반 요청 +""" + + + +# Intent 분석 함수 (대화 목적 분류) +""" +사용자의 입력 문장을 기반으로 '의도(intent)'를 분류함. +'피드백', '로드맵', '멘탈케어', '확장 요청' 중 하나로 분류됨 +""" +def detect_intent(user_input: str) -> str: + llm = get_agent() + response = llm.invoke([ + HumanMessage(content=INTENT_GUIDANCE), + HumanMessage(content=user_input) + ]) + return response.content.strip() + + + +# Goal Function 분석 함수 (특수 요청 분류) +""" +사용자의 입력 문장을 기반으로 목표(goal)에 맞는 요청의 유형을 분류 +'문제 생성', '정보 제공', '일반 요청' 중 하나로 응답 +""" +def detect_goal_function(user_input: str, goal: str) -> str: + llm = get_agent() + guidance = GOAL_FUNCTION_GUIDANCE_TEMPLATE.format(goal=goal) + response = llm.invoke([ + HumanMessage(content=guidance), + HumanMessage(content=user_input) + ]) + return response.content.strip() diff --git a/test_user_summary.py b/test_user_summary.py new file mode 100644 index 0000000..b7ee710 --- /dev/null +++ b/test_user_summary.py @@ -0,0 +1,34 @@ +# 분석 AI TEST CODE + +import asyncio +import nest_asyncio +from app.llm.progress_analysis import get_user_summary, get_strength_weakness, get_goal_challenges + + +# 테스트할 user_id +user_id = "user_id" + +async def test_summary(): + result = await get_user_summary(user_id) + print("🎯 GPT 요약 결과:") + print(result) + +async def test_strength(): + result = await get_strength_weakness(user_id) + print("✅ 강점 & 개선점 분석 결과:") + print(result) + +async def test_goal_challenge(): + for goal in ["영어", "코딩", "운동"]: + print(f"\n🎯 {goal} 도전과제 추천:") + result = await get_goal_challenges(user_id, goal) + print(result) + + +if __name__ == "__main__": + async def run_all(): + await test_summary() + await test_strength() + await test_goal_challenge() + + asyncio.run(run_all()) \ No newline at end of file