Skip to content

Commit d55192b

Browse files
authored
Merge pull request #35 from 9git9git/introduce-langchain
Introduce langchain
2 parents 5e98e32 + ed98a2c commit d55192b

7 files changed

Lines changed: 419 additions & 0 deletions

File tree

app/llm/__init__.py

Whitespace-only changes.

app/llm/agent_router.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from app.llm.tools import detect_intent, detect_goal_function
2+
from app.llm.goal_agents import (
3+
get_english_response, get_coding_response, get_fitness_response,
4+
get_english_question, get_coding_question,
5+
get_english_info, get_coding_info, get_fitness_info,
6+
get_english_mentalcare, get_coding_mentalcare, get_fitness_mentalcare,
7+
)
8+
9+
VALID_GOALS = ["영어", "코딩", "운동"]
10+
11+
# goal 별 응답 함수 매핑
12+
RESPONSE_FUNCS = {
13+
"영어": get_english_response,
14+
"코딩": get_coding_response,
15+
"운동": get_fitness_response,
16+
}
17+
18+
QUESTION_FUNCS = {
19+
"영어": get_english_question,
20+
"코딩": get_coding_question,
21+
}
22+
23+
INFO_FUNCS = {
24+
"영어": get_english_info,
25+
"코딩": get_coding_info,
26+
"운동": get_fitness_info,
27+
}
28+
29+
MENTAL_FUNCS = {
30+
"영어": get_english_mentalcare,
31+
"코딩": get_coding_mentalcare,
32+
"운동": get_fitness_mentalcare,
33+
}
34+
35+
36+
# 에이전트 선택을 제어하는 핵심 로직
37+
def route_to_agent(user_input: str, context: dict) -> str:
38+
goal = context.get("goal")
39+
if goal not in VALID_GOALS:
40+
return "목표는 영어, 코딩, 운동 중 하나여야 합니다."
41+
42+
# intent와 goal_function_type 분석
43+
intent = detect_intent(user_input)
44+
goal_func_type = detect_goal_function(user_input, goal)
45+
46+
# 멘탈케어는 intent 기준으로 분기
47+
if intent == "멘탈케어" and goal in MENTAL_FUNCS:
48+
return MENTAL_FUNCS[goal](user_input)
49+
50+
# 특수 요청: 문제 생성
51+
if goal_func_type == "문제 생성" and goal in QUESTION_FUNCS:
52+
return QUESTION_FUNCS[goal](user_input)
53+
54+
# 특수 요청: 정보 제공
55+
if goal_func_type == "정보 제공" and goal in INFO_FUNCS:
56+
return INFO_FUNCS[goal](user_input)
57+
58+
# 일반 응답 (피드백, 로드맵)
59+
response_func = RESPONSE_FUNCS.get(goal)
60+
if response_func:
61+
return response_func(user_input, intent)
62+
63+
return "알 수 없는 목표나 요청이에요."

app/llm/goal_agents.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from langchain.prompts import PromptTemplate
2+
from app.llm.llm_provider import get_agent
3+
4+
llm = get_agent()
5+
6+
7+
# 통합 프롬프트 템플릿
8+
9+
10+
general_prompt = PromptTemplate.from_template("""
11+
당신은 {goal} 분야의 따뜻하고 실용적인 전문가입니다.
12+
13+
[사용자 요청]
14+
"{user_input}"
15+
16+
[사용자의 현재 상태]
17+
요청 유형: {intent}
18+
19+
{extra_instruction}
20+
""")
21+
22+
23+
24+
# 일반 응답 (피드백, 로드맵)
25+
def get_response(goal: str, user_input: str, intent: str) -> str:
26+
instruction = "사용자의 질문에 전문가로서 조언이나 피드백을 제공해주세요." if intent == "피드백" else \
27+
"사용자의 목표에 맞는 단계별 학습 로드맵이나 실천 계획을 제시해주세요." if intent == "로드맵" else \
28+
"질문에 대해 상황을 이해하고 적절한 조언을 제공해주세요."
29+
30+
return llm.invoke(general_prompt.format(
31+
goal=goal,
32+
user_input=user_input,
33+
intent=intent,
34+
extra_instruction=instruction
35+
)).content.strip()
36+
37+
get_english_response = lambda u, i: get_response("영어", u, i)
38+
get_coding_response = lambda u, i: get_response("코딩", u, i)
39+
get_fitness_response = lambda u, i: get_response("운동", u, i)
40+
41+
42+
43+
# 문제 생성
44+
def get_question(goal: str, user_input: str) -> str:
45+
instruction = f"{goal} 분야의 간단한 퀴즈나 연습 문제를 생성해주세요. 상황이나 감정도 반영해주세요."
46+
return llm.invoke(general_prompt.format(
47+
goal=goal,
48+
user_input=user_input,
49+
intent="문제 생성",
50+
extra_instruction=instruction
51+
)).content.strip()
52+
53+
get_english_question = lambda u: get_question("영어", u)
54+
get_coding_question = lambda u: get_question("코딩", u)
55+
56+
57+
58+
# 정보 제공
59+
def get_info(goal: str, user_input: str) -> str:
60+
instruction = f"{goal}과 관련된 시험, 자격증, 학습 팁, 활동 정보 등을 전문가 입장에서 자세히 제공해주세요."
61+
return llm.invoke(general_prompt.format(
62+
goal=goal,
63+
user_input=user_input,
64+
intent="정보 제공",
65+
extra_instruction=instruction
66+
)).content.strip()
67+
68+
get_english_info = lambda u: get_info("영어", u)
69+
get_coding_info = lambda u: get_info("코딩", u)
70+
get_fitness_info = lambda u: get_info("운동", u)
71+
72+
73+
74+
# 멘탈케어 응답
75+
def get_mentalcare(goal: str, user_input: str) -> str:
76+
instruction = f"""
77+
당신은 {goal} 분야의 학습이나 실천 과정에서 지친 사람에게
78+
공감과 위로, 회복을 돕는 따뜻한 메시지를 전달하는 전문가입니다.
79+
현재 감정과 상황을 고려하여 부담스럽지 않게 응원해주세요.
80+
"""
81+
return llm.invoke(general_prompt.format(
82+
goal=goal,
83+
user_input=user_input,
84+
intent="멘탈케어",
85+
extra_instruction=instruction
86+
)).content.strip()
87+
88+
get_english_mentalcare = lambda u: get_mentalcare("영어", u)
89+
get_coding_mentalcare = lambda u: get_mentalcare("코딩", u)
90+
get_fitness_mentalcare = lambda u: get_mentalcare("운동", u)

app/llm/llm_provider.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
from functools import lru_cache
3+
from langchain_community.chat_models import AzureChatOpenAI
4+
from app.core.config import settings
5+
6+
@lru_cache()
7+
def get_agent():
8+
return AzureChatOpenAI(
9+
azure_deployment=settings.AZURE_OPENAI_DEPLOYMENT_NAME,
10+
azure_endpoint=settings.AZURE_OPENAI_ENDPOINT,
11+
openai_api_key=settings.AZURE_OPENAI_API_KEY,
12+
openai_api_version=settings.AZURE_OPENAI_API_VERSION,
13+
temperature=0.5
14+
)
15+

app/llm/progress_analysis.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from app.llm.llm_provider import get_agent
2+
from langchain_community.utilities import SQLDatabase
3+
from langchain.prompts import PromptTemplate
4+
from app.core.config import settings
5+
import json
6+
7+
# LLM & DB 연결
8+
llm = get_agent()
9+
10+
db = SQLDatabase.from_uri(
11+
settings.SYNC_DATABASE_URL,
12+
include_tables= ["progresses", "todos", "categories"],
13+
sample_rows_in_table_info=2 # 테이블 구조 예시를 GPT가 이해하도록 제공
14+
)
15+
16+
# 목표 한글 → DB ENUM 값 매핑
17+
GOAL_TO_ENUM = {
18+
"영어": "ENGLISH",
19+
"코딩": "CODING",
20+
"운동": "EXERCISE",
21+
}
22+
23+
24+
# GPT 응답 JSON 파싱 유틸 함수
25+
def extract_json(text: str, label: str):
26+
try:
27+
start = text.index('{')
28+
end = text.rindex('}') + 1
29+
return json.loads(text[start:end])
30+
except Exception as e:
31+
return {"error": f"{label} JSON parse 실패", "raw": text}
32+
33+
34+
35+
# 전체 목표 평균 달성률 요약
36+
async def get_user_summary(user_id: str):
37+
query = f"""
38+
SELECT AVG(progress_rate) AS average_rate
39+
FROM progresses
40+
WHERE user_id = '{user_id}';
41+
"""
42+
43+
result = db.run(query)
44+
45+
prompt = PromptTemplate.from_template("""
46+
당신은 목표 달성률을 기반으로 희망적인 복사정의 기준문장을 생성하는 AI입니다.
47+
48+
- 평균 달성률 수치를 기반으로 하되, 사용자가 성취에 대한 자신감을 가질 수 있도록 유도해야함.
49+
- '사용자'는 표현은 제외하고 계속해서 자연스러운 문장으로 작성되어야함.
50+
- 달성률이 낮아도 격려하거나 앞으로의 가능성을 강조해 줘야함.
51+
- 문장은 따뜻하고 응원하는 톤으로, 반드시 존댓말로 작성할 것. 문장은 한 문장 이내, 70자 이내로 작성할 것.
52+
- 길이는 2줄 이내여야 하며, 아래 JSON 형식으로 응답해야 함.
53+
- 모든 응답은 반드시 **자연스러운 한국어**로 작성할 것. 영어나 외국어는 사용 금지.
54+
55+
평균 달성률: {progress_data}
56+
57+
아래 JSON 형식으로 출력하세요:
58+
```json
59+
{{
60+
"summary": "..."
61+
}}
62+
""")
63+
formatted = prompt.format(progress_data=result)
64+
response = llm.invoke(formatted)
65+
return extract_json(response.content, "summary")
66+
67+
68+
69+
# 목표별 강점 & 개선점 분석
70+
async def get_strength_weakness(user_id: str):
71+
query = f"""
72+
WITH ranked_goals AS (
73+
SELECT c.category_name AS goal, AVG(p.progress_rate) AS avg_rate
74+
FROM progresses p
75+
JOIN categories c ON p.category_id = c.id
76+
WHERE p.user_id = '{user_id}'
77+
GROUP BY c.category_name
78+
)
79+
SELECT * FROM ranked_goals ORDER BY avg_rate DESC;
80+
"""
81+
82+
results = db.run(query)
83+
84+
prompt = PromptTemplate.from_template("""
85+
당신은 목표별 평균 달성률을 분석하고, 가장 높은 항목은 강점으로, 가장 낮은 항목은 감정점으로 표현하는 AI입니다.
86+
87+
요약 문장은 반드시 짧고 간결하게 작성할 것:
88+
- 각 문장은 1줄, 70자 이내로 작성되어야 함.
89+
- '사용자'는 표현은 제외하고 자연스러운 일상 문장처럼 작성해야 함.
90+
- 강점은 격려 문장, 개선점은 실천을 유도하는 조언 문장으로 모두 존댓말로 작성할 것.
91+
- 모든 응답은 반드시 **자연스러운 한국어**로 작성되어야 함. 영어나 외국어는 사용하면 안됨.
92+
93+
아래는 사용자의 목표별 평균 달성률임:
94+
{ranked_goals}
95+
96+
아래 JSON 형식으로 출력하세요:
97+
```json
98+
{{
99+
"strength": "...",
100+
"weakness": "..."
101+
}}
102+
""")
103+
formatted = prompt.format(ranked_goals=results)
104+
response = llm.invoke(formatted)
105+
return extract_json(response.content, "strength_weakness")
106+
107+
108+
109+
110+
# 목표별 도전과제 추천
111+
async def get_goal_challenges(user_id: str, goal: str):
112+
# 한글 목표명을 ENUM 값으로 매핑
113+
category_enum = GOAL_TO_ENUM.get(goal, None)
114+
if category_enum is None:
115+
return {"error": f"❌ goal '{goal}'은 지원되지 않음"}
116+
117+
query = f"""
118+
SELECT t.content, t.is_completed, t.start_date, t.end_date
119+
FROM todos t
120+
JOIN categories c ON t.category_id = c.id
121+
WHERE t.user_id = '{user_id}' AND c.category_name = '{category_enum}'
122+
AND (t.start_date >= CURRENT_DATE - INTERVAL '6 months' OR t.end_date >= CURRENT_DATE - INTERVAL '6 months')
123+
ORDER BY t.start_date DESC;
124+
"""
125+
126+
results = db.run(query)
127+
128+
prompt = PromptTemplate.from_template("""
129+
당신은 도전과제를 기획하는 전문가입니다.
130+
131+
선택된 목표: '{goal}'
132+
최근 기록된 활동 내역(최대 6개월 기준, 실제 기록이 적을 수 있음):
133+
{todo_info}
134+
135+
활동 내용을 기반으로 새로운 도전과제를 하나 추천할 것.
136+
단, 기록이 부족한 경우에는 격려 문구를 포함한 실현 가능한 추천을 생성할 것.
137+
도전과제는 다음 기준을 충족해야 함:
138+
139+
- 활동과 연관된 자격증, 시험, 수료증, 경진대회 등 실질적인 성취로 연결될 것
140+
- 사용자가 모르고 있을 수 있는 정보도 포함 가능
141+
- 예: 파이썬 → 정보처리기사, 크롤링 → 데이터 분석 전문가, 영어 회화 → OPIC, 운동 → 요가 지도자 수료증 등
142+
- 도전과제는 너무 거창하지 않으면서도 실현 가능하고, 자격증·공식 시험·현실성 있는 실습 챌린지 중심이어야 함.
143+
- 모든 응답은 반드시 **자연스러운 한국어**로 작성할것. 영어나 외국어는 절대 사용하면 안됨.
144+
- 아래 형식의 각 항목은 반드시 **간결하게**, 1~2문장 이내로 작성하며, 모든 문장은 존댓말로 작성할 것 (특히 reason과 motivation은 요점 중심으로 작성)
145+
146+
아래 JSON 형식으로 출력하세요:
147+
```json
148+
{{
149+
"title": "...",
150+
"reason": "...", ← 배경 설명 없이 추천 이유만 간단히
151+
"motivation": "...", ← 실천 유도 문장 위주, 핵심만 작성
152+
"duration": "...",
153+
"difficulty": "하/중/상"
154+
}}
155+
""")
156+
formatted = prompt.format(goal=goal, todo_info=results)
157+
response = llm.invoke(formatted)
158+
return extract_json(response.content, f"{goal}_challenge")

app/llm/tools.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from langchain.schema import HumanMessage
2+
from app.llm.llm_provider import get_agent
3+
4+
5+
# 프롬프트 템플릿
6+
# 사용자 입력을 4가지 유형 중 하나로 분류하기 위한 가이드
7+
INTENT_GUIDANCE = """
8+
다음은 사용자의 요청 유형을 분류하는 기준이야야. 분류는 반드시 다음 4개 중 하나로만 해줘줘:
9+
10+
1. 피드백: 사용자의 활동이나 학습 결과에 대한 평가, 코칭, 잘하고 있는지 질문하는 경우
11+
2. 로드맵: 앞으로 무엇을 해야 할지, 계획/단계 추천을 요청하는 경우
12+
3. 멘탈케어: 감정적 표현이 포함된 경우 (예: 지쳤다, 힘들다, 위로받고 싶다)
13+
4. 확장 요청: 문제를 만들어달라, 예시 보여달라, 시험/정보를 요청하는 등의 경우
14+
15+
사용자의 입력을 기반으로 위 4가지 중 가장 적절한 하나를 골라. 단어 그대로 "피드백", "로드맵", "멘탈케어", "확장 요청" 중 하나만 답변하도록 해.
16+
"""
17+
18+
# 목표(goal)에 따라 입력 요청을 분류하기 위한 템플릿 (문제 생성 / 정보 제공 / 일반 요청)
19+
GOAL_FUNCTION_GUIDANCE_TEMPLATE = """
20+
너는 '{goal}' 분야의 전문 AI 코치야.
21+
사용자의 요청이 아래 유형 중 하나인지 판단해줘:
22+
23+
- 문제 생성: 퀴즈, 예시 문제, 테스트 요청
24+
- 정보 제공: 자격증, 시험, 실습, 동작 등에 대한 구체적인 정보 요청
25+
- 일반 요청: 일상 대화, 피드백, 코칭 등 자유로운 조언
26+
27+
반드시 다음 중 하나로만 응답해: 문제 생성 / 정보 제공 / 일반 요청
28+
"""
29+
30+
31+
32+
# Intent 분석 함수 (대화 목적 분류)
33+
"""
34+
사용자의 입력 문장을 기반으로 '의도(intent)'를 분류함.
35+
'피드백', '로드맵', '멘탈케어', '확장 요청' 중 하나로 분류됨
36+
"""
37+
def detect_intent(user_input: str) -> str:
38+
llm = get_agent()
39+
response = llm.invoke([
40+
HumanMessage(content=INTENT_GUIDANCE),
41+
HumanMessage(content=user_input)
42+
])
43+
return response.content.strip()
44+
45+
46+
47+
# Goal Function 분석 함수 (특수 요청 분류)
48+
"""
49+
사용자의 입력 문장을 기반으로 목표(goal)에 맞는 요청의 유형을 분류
50+
'문제 생성', '정보 제공', '일반 요청' 중 하나로 응답
51+
"""
52+
def detect_goal_function(user_input: str, goal: str) -> str:
53+
llm = get_agent()
54+
guidance = GOAL_FUNCTION_GUIDANCE_TEMPLATE.format(goal=goal)
55+
response = llm.invoke([
56+
HumanMessage(content=guidance),
57+
HumanMessage(content=user_input)
58+
])
59+
return response.content.strip()

0 commit comments

Comments
 (0)