Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file added app/llm/__init__.py
Empty file.
63 changes: 63 additions & 0 deletions app/llm/agent_router.py
Original file line number Diff line number Diff line change
@@ -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 "알 수 없는 목표나 요청이에요."
90 changes: 90 additions & 0 deletions app/llm/goal_agents.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions app/llm/llm_provider.py
Original file line number Diff line number Diff line change
@@ -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
)

158 changes: 158 additions & 0 deletions app/llm/progress_analysis.py
Original file line number Diff line number Diff line change
@@ -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")
59 changes: 59 additions & 0 deletions app/llm/tools.py
Original file line number Diff line number Diff line change
@@ -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()
Loading