diff --git a/app/database/chroma_db.py b/app/database/chroma_db.py index 0d7e3d1..a54f229 100644 --- a/app/database/chroma_db.py +++ b/app/database/chroma_db.py @@ -10,7 +10,7 @@ import os import datetime from dotenv import load_dotenv -from app.dto.db_dto import AddScheduleDTO, RecommendationMainRequestDTO +from app.dto.db_dto import AddScheduleDTO, RecommendationMainRequestDTO, ReportTagsRequestDTO load_dotenv() CHROMA_DB_IP_ADDRESS = os.getenv("CHROMA_DB_IP_ADDRESS") @@ -48,20 +48,26 @@ async def search_db_query(query): # 스프링 백엔드로부터 chroma DB에 저장할 데이터를 받아 DB에 추가한다. async def add_db_data(schedule_data: AddScheduleDTO): schedule_date = schedule_data.schedule_datetime_start.split("T")[0] + year = int(schedule_date.split("-")[0]) + month = int(schedule_date.split("-")[1]) + date = int(schedule_date.split("-")[2]) schedules.add( documents=[schedule_data.data], ids=[str(schedule_data.schedule_id)], - metadatas=[{"date": schedule_date, "datetime_start": schedule_data.schedule_datetime_start, "datetime_end": schedule_data.schedule_datetime_end, "member": schedule_data.member_id, "category": schedule_data.category, "location": schedule_data.location, "person": schedule_data.person}] + metadatas=[{"year": year, "month": month, "date": date, "datetime_start": schedule_data.schedule_datetime_start, "datetime_end": schedule_data.schedule_datetime_end, "member": schedule_data.member_id, "category": schedule_data.category, "location": schedule_data.location, "person": schedule_data.person}] ) return True # 메인페이지 한 줄 추천 기능에 사용하는 함수 # 유저의 id, 해당 날짜로 필터링 -async def db_recommendation_main(user_data: RecommendationMainRequestDTO): +async def db_daily_schedule(user_data: RecommendationMainRequestDTO): member = user_data.member_id schedule_datetime_start = user_data.schedule_datetime_start schedule_datetime_end = user_data.schedule_datetime_end schedule_date = schedule_datetime_start.split("T")[0] + year = int(schedule_date.split("-")[0]) + month = int(schedule_date.split("-")[1]) + date = int(schedule_date.split("-")[2]) persona = user_data.user_persona or "hard working" results = schedules.query( query_texts=[persona], @@ -69,7 +75,39 @@ async def db_recommendation_main(user_data: RecommendationMainRequestDTO): where={"$and": [ {"member": {"$eq": int(member)}}, - {"date": {"$eq": schedule_date}} + {"year": {"$eq": year}}, + {"month": {"$eq": month}}, + {"date": {"$eq": date}} + ] + } + # where_document={"$contains":"search_string"} # optional filter + ) + return results['documents'] + +# 태그 생성용 스케쥴 반환 - 카테고리에 따라 +async def db_monthly_tag_schedule(user_data: ReportTagsRequestDTO): + member = user_data.member_id + schedule_datetime_start = user_data.schedule_datetime_start + schedule_datetime_end = user_data.schedule_datetime_end + schedule_date = schedule_datetime_start.split("T")[0] + year = int(schedule_date.split("-")[0]) + month = int(schedule_date.split("-")[1]) + date = int(schedule_date.split("-")[2]) + persona = user_data.user_persona or "hard working" + results = schedules.query( + query_texts=[persona], + n_results=15, + where={"$and": + [ + {"member": {"$eq": int(member)}}, + {"year": {"$eq": year}}, + {"month": {"$eq": month - 1}} + # {"$or": + # [{"$and": + # [{"month": {"$eq": month-1}}, {"date": {"$gte": 10}}]}, + # {"$and": + # [{"month": {"$eq": month}}, {"date": {"$lt": 10}}]} + # ]} ] } # where_document={"$contains":"search_string"} # optional filter diff --git a/app/dto/db_dto.py b/app/dto/db_dto.py index 8228fd5..f6fef84 100644 --- a/app/dto/db_dto.py +++ b/app/dto/db_dto.py @@ -7,7 +7,7 @@ class AddScheduleDTO(BaseModel): schedule_datetime_end: str schedule_id: int member_id: int - category: str + category: int location: str person: str @@ -17,3 +17,15 @@ class RecommendationMainRequestDTO(BaseModel): schedule_datetime_start: str schedule_datetime_end: str +class ReportMemoryEmojiRequestDTO(BaseModel): + member_id: int + user_persona: str + schedule_datetime_start: str + schedule_datetime_end: str + +class ReportTagsRequestDTO(BaseModel): + member_id: int + user_persona: str + schedule_datetime_start: str + schedule_datetime_end: str + diff --git a/app/dto/openai_dto.py b/app/dto/openai_dto.py index cfa374f..824c603 100644 --- a/app/dto/openai_dto.py +++ b/app/dto/openai_dto.py @@ -1,5 +1,5 @@ from pydantic import BaseModel - +from typing import List class PromptRequest(BaseModel): prompt: str @@ -9,4 +9,11 @@ class ChatResponse(BaseModel): class ChatCaseResponse(BaseModel): ness: str - case: int \ No newline at end of file + case: int + +class TagDescription(BaseModel): + tag: str + desc: str + +class TagsResponse(BaseModel): + tagList: List[TagDescription] \ No newline at end of file diff --git a/app/main.py b/app/main.py index 523db77..2504363 100644 --- a/app/main.py +++ b/app/main.py @@ -32,6 +32,9 @@ from app.routers import recommendation app.include_router(recommendation.router) +from app.routers import report +app.include_router(report.router) + # description: prevent CORS error origins = [ "*", diff --git a/app/prompt/openai_config.ini b/app/prompt/openai_config.ini index 8a62d43..70ee694 100644 --- a/app/prompt/openai_config.ini +++ b/app/prompt/openai_config.ini @@ -1,7 +1,7 @@ [NESS_NORMAL] -TEMPERATURE = 0 +TEMPERATURE = 0.5 MAX_TOKENS = 2048 -MODEL_NAME = gpt-3.5-turbo-1106 +MODEL_NAME = gpt-4 [NESS_CASE] TEMPERATURE = 0 @@ -9,6 +9,11 @@ MAX_TOKENS = 2048 MODEL_NAME = gpt-4 [NESS_RECOMMENDATION] -TEMPERATURE = 0 +TEMPERATURE = 1 +MAX_TOKENS = 2048 +MODEL_NAME = gpt-4 + +[NESS_TAGS] +TEMPERATURE = 0.5 MAX_TOKENS = 2048 -MODEL_NAME = gpt-3.5-turbo-1106 \ No newline at end of file +MODEL_NAME = gpt-4 \ No newline at end of file diff --git a/app/prompt/openai_prompt.py b/app/prompt/openai_prompt.py index e17f97d..b37762e 100644 --- a/app/prompt/openai_prompt.py +++ b/app/prompt/openai_prompt.py @@ -2,12 +2,14 @@ class Template: recommendation_template = """ You are an AI assistant designed to recommend daily activities based on a user's schedule. You will receive a day's worth of the user's schedule information. Your task is to understand that schedule and, based on it, recommend an activity for the user to perform that day. There are a few rules you must follow in your recommendations: 1. YOU MUST USE {output_language} TO RESPOND TO THE USER INPUT. - 2. Ensure your recommendation is encouraging, so the user doesn't feel compelled. + 2. Ensure your recommendation is encouraging and delicate, so the user doesn't feel compelled. 3. The recommendation must be concise, limited to one sentence without any additional commentary. + 4. The recommendation must not exceed 20 characters in the {output_language}. + 5. The final punctuation of the recommendation must be exclusively an exclamation point or a question mark. Example: User schedule: [Practice guitar, Calculate accuracy, Study backend development, Run AI models in the lab, Study NEST.JS] - AI Recommendation: "Your day is filled with learning and research. how about taking a short walk in between studies?" + AI Recommendation: "공부 사이에 짧게 산책 어때요?" User schedule: {schedule} AI Recommendation: @@ -48,36 +50,39 @@ class Template: Answer: """ case1_template = """ - You are a friendly assistant who helps users manage their schedules. Respond kindly to the user's input. YOU MUST USE {output_language} TO RESPOND TO THE USER INPUT. - User input: {question} - """ + You are a friendly assistant, NESS. NESS helps users manage their schedules. Respond kindly to the user's input. YOU MUST USE {output_language} TO RESPOND TO THE USER INPUT. + User input: {question} + """ case2_template = """ - You are a friendly assistant who helps users manage their schedules. The user's input contains information about a new event they want to add to their schedule. You have two tasks to perform: + You are a friendly assistant who helps users manage their schedules. The user's input contains information about a new event they want to add to their schedule. You have two tasks to perform: - 1. Respond kindly to the user's input. YOU MUST USE {output_language} TO RESPOND TO THE USER INPUT. - 2. Organize the event the user wants to add into a json format for saving in a database. The returned json will have keys for info, location, person, and date. - - info: Summarizes what the user wants to do. This value must always be present. - - location: If the user's event information includes a place, save that place as the value. - - person: If the user's event mentions a person they want to include, save that person as the value. - - date: If the user's event information includes a specific date and time, save that date and time in datetime format. - Separate the outputs for tasks 1 and 2 with a special token . + 1. Respond kindly to the user's input. YOU MUST USE {output_language} TO RESPOND TO THE USER INPUT. + 2. Organize the event the user wants to add into a json format for saving in a database. The returned json will have keys for info, location, person, and date. + - info: Summarizes what the user wants to do. This value must always be present. + - location: If the user's event information includes a place, save that place as the value. + - person: If the user's event mentions a person they want to include, save that person as the value. + - date: If the user's event information includes a specific date and time, save that date and time in datetime format. Dates should be organized based on the current time at the user's location. Current time is {current_time}. + Separate the outputs for tasks 1 and 2 with a special token . - Example for one-shot learning: + Example for one-shot learning: - User input: "I have a meeting with Dr. Smith at her office on March 3rd at 10am." + User input: "I have a meeting with Dr. Smith at her office on March 3rd at 10am." - Response to user: - "I've added your meeting with Dr. Smith at her office on March 3rd at 10am to your schedule. Is there anything else you'd like to add or modify?" - - {{ - "info": "meeting with Dr. Smith", - "location": "Dr. Smith's office", - "person": "Dr. Smith", - "date": "2023-03-03T10:00:00" - }} + Response to user: + "I've added your meeting with Dr. Smith at her office on March 3rd at 10am to your schedule. Is there anything else you'd like to add or modify?" + + {{ + "info": "meeting with Dr. Smith", + "location": "Dr. Smith's office", + "person": "Dr. Smith", + "date": "2023-03-03T10:00:00" + }} + + User input: {question} + + Response to user: + """ - User input: {question} - """ case3_template = """ You are an advanced, friendly assistant dedicated to helping users efficiently manage their schedules and navigate their day-to-day tasks with ease. Your primary role is to interact with users in a supportive and courteous manner, ensuring they feel valued and assisted at every step. diff --git a/app/prompt/report_prompt.py b/app/prompt/report_prompt.py new file mode 100644 index 0000000..a739cc1 --- /dev/null +++ b/app/prompt/report_prompt.py @@ -0,0 +1,36 @@ +class Template: + memory_emoji_template = """ + You are an AI assistant designed to return an emoji that represents a user's day based on their schedule. You will receive information about the user's daily schedule. Your task is to analyze this schedule and select a single emoji that encapsulates the day's activities. There are a few guidelines you must adhere to in your selection: + + YOU MUST RESPOND WITH A SINGLE EMOJI without any additional commentary. + The emoji must reflect the overall theme or mood of the day's activities. + Your recommendation should be intuitive, making it easy for the user to see the connection between the schedule and the chosen emoji. + Example: + User schedule: [Practice guitar, Calculate accuracy, Study backend development, Run AI models in the lab, Study NEST.JS] + AI Recommendation: 🎓 + + User schedule: [Morning jog, Office work, Lunch with friends, Evening yoga] + AI Recommendation: ☀️ + + User schedule: {schedule} + AI Recommendation: + """ + + report_tags_template = """ + You are an AI assistant tasked with analyzing a user's schedule over the span of a month. From this detailed schedule, you will distill three keywords that best encapsulate the user's activities, interests, or achievements throughout the month. Additionally, you will provide a brief explanation for each keyword to illustrate why it was chosen, making the output more informative and engaging. Here are the rules for your analysis: + + YOU MUST USE {output_language} TO RESPOND TO THE INPUT. + YOU MUST PROVIDE THREE KEYWORDS in your response, each accompanied by a concise explanation. + The keywords must capture the essence of the user's monthly activities, highlighting aspects that are both rewarding and enjoyable. + Your selections should be creative and personalized, aiming to reflect the user's unique experiences over the month. + Each explanation of the keywords must not exceed 50 characters in the {output_language}. + Example: + User's monthly schedule: [Attended a programming bootcamp, Completed a marathon, Read three novels, Volunteered at the local food bank, Started a blog about sustainability] + AI Recommendation: "공부 매니아: 이번 달엔 공부를 정말 많이 하셨군요!, 환경 지킴이: 지속 가능한 생활에 대한 열정이 느껴져요., 자기 계발 홀릭: 성장을 위해 노력하는 모습이 멋져요." + + User's monthly schedule: [Took photography classes, Explored three new hiking trails, Organized a neighborhood clean-up, Experimented with vegan recipes] + AI Recommendation: "모험가: 새로운 활동에 많이 도전하셨네요!, 미식가: 레시피 실험을 했다니 멋져요., 도파민 중독자: 즐거운 일을 많이 만드시네요!" + + User's monthly schedule: {schedule} + AI Recommendation: + """ diff --git a/app/routers/chat.py b/app/routers/chat.py index 400ded8..2c9c437 100644 --- a/app/routers/chat.py +++ b/app/routers/chat.py @@ -6,6 +6,7 @@ from fastapi import APIRouter, HTTPException, status from langchain_community.chat_models import ChatOpenAI from langchain_core.prompts import PromptTemplate +from datetime import datetime from app.dto.openai_dto import PromptRequest, ChatResponse, ChatCaseResponse from app.prompt import openai_prompt @@ -30,7 +31,7 @@ async def get_langchain_case(data: PromptRequest) -> ChatCaseResponse: # description: use langchain - config_chat = config['NESS_CHAT'] + config_chat = config['NESS_CASE'] chat_model = ChatOpenAI(temperature=config_chat['TEMPERATURE'], # 창의성 (0.0 ~ 2.0) max_tokens=config_chat['MAX_TOKENS'], # 최대 토큰수 @@ -57,9 +58,11 @@ async def get_langchain_case(data: PromptRequest) -> ChatCaseResponse: response = await get_langchain_rag(data) else: - print("wrong case classification") - # 적절한 HTTP 상태 코드와 함께 오류 메시지를 반환하거나, 다른 처리를 할 수 있습니다. - raise HTTPException(status_code=400, detail="Wrong case classification") + # print("wrong case classification") + # # 적절한 HTTP 상태 코드와 함께 오류 메시지를 반환하거나, 다른 처리를 할 수 있습니다. + # raise HTTPException(status_code=400, detail="Wrong case classification") + response = "좀 더 명확한 요구가 필요해요. 다시 한 번 얘기해주실 수 있나요?" + case = "Exception" return ChatCaseResponse(ness=response, case=case) @@ -104,7 +107,8 @@ async def get_langchain_schedule(data: PromptRequest): case2_template = openai_prompt.Template.case2_template prompt = PromptTemplate.from_template(case2_template) - response = chat_model.predict(prompt.format(output_language="Korean", question=question)) + current_time = datetime.now() + response = chat_model.predict(prompt.format(output_language="Korean", question=question, current_time=current_time)) print(response) return response diff --git a/app/routers/recommendation.py b/app/routers/recommendation.py index 2ca270f..612e658 100644 --- a/app/routers/recommendation.py +++ b/app/routers/recommendation.py @@ -38,7 +38,7 @@ async def get_recommendation(user_data: RecommendationMainRequestDTO) -> ChatRes ) # vectordb에서 유저의 정보를 가져온다. - schedule = await vectordb.db_recommendation_main(user_data) + schedule = await vectordb.db_daily_schedule(user_data) print(schedule) diff --git a/app/routers/report.py b/app/routers/report.py new file mode 100644 index 0000000..2b92bdc --- /dev/null +++ b/app/routers/report.py @@ -0,0 +1,92 @@ +import configparser +import os + +from dotenv import load_dotenv +from fastapi import APIRouter, Depends, status, HTTPException +from langchain_community.chat_models import ChatOpenAI +from langchain_core.prompts import PromptTemplate + +from app.dto.db_dto import ReportMemoryEmojiRequestDTO, ReportTagsRequestDTO +from app.dto.openai_dto import ChatResponse, TagsResponse, TagDescription +from app.prompt import report_prompt +import app.database.chroma_db as vectordb + +router = APIRouter( + prefix="/report", + tags=["report"] +) + +# description: load env variables from .env file +load_dotenv() +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + +# description: load config variables from openai_config.ini file +CONFIG_FILE_PATH = "app/prompt/openai_config.ini" +config = configparser.ConfigParser() +config.read(CONFIG_FILE_PATH) + +@router.post("/memory_emoji", status_code=status.HTTP_200_OK) +async def get_memory_emoji(user_data: ReportMemoryEmojiRequestDTO) -> ChatResponse: + try: + # 모델 + config_report_emoji = config['NESS_RECOMMENDATION'] + + chat_model = ChatOpenAI(temperature=config_report_emoji['TEMPERATURE'], # 창의성 (0.0 ~ 2.0) + max_tokens=config_report_emoji['MAX_TOKENS'], # 최대 토큰수 + model_name=config_report_emoji['MODEL_NAME'], # 모델명 + openai_api_key=OPENAI_API_KEY # API 키 + ) + + # vectordb에서 유저의 정보를 가져온다. + schedule = await vectordb.db_daily_schedule(user_data) + + print(schedule) + + # 템플릿 + memory_emoji_template = report_prompt.Template.memory_emoji_template + + prompt = PromptTemplate.from_template(memory_emoji_template) + result = chat_model.predict(prompt.format(output_language="Korean", schedule=schedule)) + print(result) + return ChatResponse(ness=result) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/tags", status_code=status.HTTP_200_OK) +async def get_tags(user_data: ReportTagsRequestDTO) -> TagsResponse: + try: + # 모델 + config_tags = config['NESS_TAGS'] + + chat_model = ChatOpenAI(temperature=config_tags['TEMPERATURE'], # 창의성 (0.0 ~ 2.0) + max_tokens=config_tags['MAX_TOKENS'], # 최대 토큰수 + model_name=config_tags['MODEL_NAME'], # 모델명 + openai_api_key=OPENAI_API_KEY # API 키 + ) + + # vectordb에서 유저의 정보를 가져온다. + schedule = await vectordb.db_monthly_tag_schedule(user_data) + + print(schedule) + + # 템플릿 + report_tags_template = report_prompt.Template.report_tags_template + + prompt = PromptTemplate.from_template(report_tags_template) + result = chat_model.predict(prompt.format(output_language="Korean", schedule=schedule)) + print(result) + + # 문자열 파싱해서 dto로 매핑 + tag_entries = result.split("\"")[1].split(", ") + tags = [] + + for entry in tag_entries: + # ':'를 기준으로 태그와 설명을 분리 + tag, desc = entry.split(": ") + tags.append(TagDescription(tag=tag.strip(), desc=desc.strip())) + + return TagsResponse(tagList=tags) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file