Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 이력서 수정 #37

Merged
merged 8 commits into from
Dec 2, 2024
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,8 @@ $RECYCLE.BIN/

# End of https://www.toptal.com/developers/gitignore/api/windows

/data/
app/data/code
app/data/commit
app/data/pr
app/data/project
app/data/repo
41 changes: 38 additions & 3 deletions app/api/v1/routes.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from app.dto.resume_dto import ResumeRequest, ResumeResponse
from app.dto.resume_modify_dto import UpdateRequestDto,ResumeResponseDto
from app.services.api_service import process_repository
from concurrent.futures import ProcessPoolExecutor
import asyncio
import logging
import json
from app.services.stack_service import generate_techstack
from app.services.gpt_service import generate_aboutme
from app.services.gpt_service import generate_aboutme, resume_update
from app.services.github_service import get_github_profile_and_repos
from app.config.settings import settings



router = APIRouter()

# 이력서 생성 api
Expand Down Expand Up @@ -54,4 +57,36 @@ async def generate_resume(request: ResumeRequest):
aboutMe=aboutme
# aboutMe=aboutme_techstack.aboutMe
)
return resume_response
return resume_response




@router.put("/api/resumes", response_model=ResumeResponseDto)
async def update_resume(request: UpdateRequestDto):
try:
# 요청 데이터 확인
print("=== Received Request Data ===")
print(f"Selected Text: {request.selectedText}")
print(f"Requirement: {request.requirement}")
print(f"Resume Info: {request.resumeInfo.dict()}")

# `resume_update` 호출 준비
print("=== Calling `resume_update` ===")
updated_resume = resume_update(
openai_api_key=settings.openai_api_key,
requirements=request.requirement,
selected_text=request.selectedText,
context_data=request.resumeInfo.dict()
)

# 업데이트된 결과 확인
print("=== Updated Resume Data ===")
print(updated_resume)

# 업데이트된 결과 반환
return updated_resume

except Exception as e:
print(f"Error in update_resume: {e}")
raise HTTPException(status_code=500, detail="An error occurred while updating the resume.")
3 changes: 2 additions & 1 deletion app/config/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List
from pydantic_settings import BaseSettings
from app.config.constant import GPT_MODEL, MAX_TOTAL_TOKENS, PROMPT_TOKEN_RESERVE, MAX_OUTPUT_TOKENS, MAX_CONTENT_TOKENS, DEFAULT_DATA, CODE_DATA, PR_DATA, COMMIT_DATA, PROJECT_DATA, REPO_DIRECTORY, FILE_EXTENSIONS
from app.prompts.resume_prompt import CODE_SUMMARY_PROMPT, PR_SUMMARY_PROMPT, COMMIT_DIFF_SUMMARY_PROMPT, FINAL_SUMMARY_PROMPT, FINAL_PROJECT_PROMPT, SIMPLIFY_PROJECT_PROMPT, ABOUTME_TECHSTACK_PROMPT, ABOUTME_PROMPT
from app.prompts.resume_prompt import CODE_SUMMARY_PROMPT, PR_SUMMARY_PROMPT, COMMIT_DIFF_SUMMARY_PROMPT, FINAL_SUMMARY_PROMPT, FINAL_PROJECT_PROMPT, SIMPLIFY_PROJECT_PROMPT, ABOUTME_TECHSTACK_PROMPT, ABOUTME_PROMPT, RESUME_UPDATE_PROMPT
class Settings(BaseSettings):
# API 키
openai_api_key: str
Expand Down Expand Up @@ -38,6 +38,7 @@ class Settings(BaseSettings):
simplify_project_prompt: str = SIMPLIFY_PROJECT_PROMPT
aboutme_techstack_prompt: str = ABOUTME_TECHSTACK_PROMPT
aboutme_prompt: str = ABOUTME_PROMPT
resume_update_prompt: str = RESUME_UPDATE_PROMPT

class Config:
env_file = ".env"
Expand Down
57 changes: 57 additions & 0 deletions app/dto/resume_modify_dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from pydantic import BaseModel
from typing import List, Optional

class WorkExperience(BaseModel):
companyName: str
departmentName: str
role: str
workType: str # FULL_TIME, PART_TIME 등
employmentStatus: str # EMPLOYMENT, UNEMPLOYMENT 등
startedAt: str # YYYY-MM 형식
endedAt: Optional[str] # YYYY-MM 형식 또는 빈 값

class ProjectResponse(BaseModel):
projectName: str
projectStartedAt: str # YYYY.MM 형식
projectEndedAt: str # YYYY.MM 형식
skillSet: str
projectDescription: str
repoLink: str
class Link(BaseModel):
linkTitle: str
linkUrl: str

class Education(BaseModel):
schoolType: str # UNIVERSITY_BACHELOR 등
schoolName: str
major: str
graduationStatus: str # ATTENDING, GRADUATED 등
startedAt: str # YYYY-MM 형식
endedAt: Optional[str] # YYYY-MM 형식 또는 빈 값

class Certificate(BaseModel):
certificateName: str
certificateGrade: str
certificatedAt: str # YYYY-MM 형식
certificateOrganization: str

class ResumeResponseDto(BaseModel):
resumeId: str
memberId: int
memberName: str
avatarUrl: str
email: str
position: str
techStack: List[str]
aboutMe: str
tags: Optional[List[str]] # null 가능
workExperiences: List[WorkExperience]
Comment on lines +1 to +48
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dto도 멋진데요? ㅎㅎ 리턴하고 요청받는것만 약속한대로 유지하고, 나머지는 형이 편한대로 잘짜시면되요 보기에는 문제가 없어보입니다. :) 굿

projects: List[ProjectResponse]
links: Optional[List[Link]] # null 가능
educations: List[Education]
certificates: List[Certificate]

class UpdateRequestDto(BaseModel):
selectedText: str
requirement: str
resumeInfo: ResumeResponseDto
6 changes: 6 additions & 0 deletions app/prompts/resume_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,10 @@
"2. Ensure the tone remains professional, and the explanation is concise.\n"
"3. Use metaphorical or creative expressions to enhance engagement, avoiding direct repetition of the input text.\n"
"4. Ensure each point reflects the provided company values and GitHub data."
)

RESUME_UPDATE_PROMPT = (
"Update the provided `selected_text` based on the user's request. "
"Modify only the specified text, and ensure the updated text aligns with the tone and style of the surrounding context. "
"Return only the modified text without altering the structure or other parts of the resume."
)
75 changes: 73 additions & 2 deletions app/services/gpt_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from openai import OpenAI
from app.config.settings import settings
from app.dto.resume_dto import GptProject
from app.dto.resume_modify_dto import ResumeResponseDto
from app.services.github_service import get_github_profile_and_repos
# from app.services.json_service import find_key_by_value
import tiktoken
import json
import os
Expand Down Expand Up @@ -182,7 +184,7 @@ def simplify_project_summary_byJson(summary_text, openai_api_key, requirements,
return GptProject(projectName="", skillSet="", projectDescription="")

# 어바웃미 생성
def generate_aboutme(openai_api_key, prompt=settings.aboutme_prompt) -> str:
def generate_aboutme(openai_api_key, prompt=settings.aboutme_prompt):
try:
# 요약 요청
print("Generating about me...")
Expand Down Expand Up @@ -248,4 +250,73 @@ def generate_aboutme(openai_api_key, prompt=settings.aboutme_prompt) -> str:

except Exception as e:
print(f"Error generating About Me: {e}")
return ""
return ""

def resume_update(openai_api_key, requirements, selected_text, context_data, prompt=settings.resume_update_prompt) :
try:
# 선택된 텍스트가 없을 때 처리
if not selected_text or not selected_text.strip():
print("Error: Selected text is empty or missing.")
return context_data # 오류 발생시 기존 데이터 반환

# 수정 요구사항이 없을 때 처리
if not requirements or not requirements.strip():
print("Error: User request (requirements) is empty or missing.")
return context_data # 오류 발생시 기존 데이터 반환

# # 선택된 텍스트의 키 경로 탐색
# key_path = find_key_by_value(context_data, selected_text)

# if not key_path:
# print("Error: Selected text does not match any value in the JSON data.")
# return context_data # 오류 발생시 기존 데이터 반환

# 수정 요청
print("이력서 수정")

# OpenAI API 호출
client = OpenAI(api_key=openai_api_key)
response = client.beta.chat.completions.parse(
model=settings.gpt_model,
messages=[
{
"role": "system",
"content": (
"You are a friendly and professional resume modification expert."
"Your task is to update only the specified sections of the resume based on the user's request while leaving all other parts unchanged."
"Ensure the modifications are concise, professional, and aligned with the tone of the original resume."
)
Comment on lines +283 to +288
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프롬프트 작성한건 잘 작성하신거같아요
이 부분은 정확한 정답이 없다보니, 이것저것 일반적인 상황들 넣어서 테스트해보고, 엣지케이스들도 넣어서 테스트하면서
결과값들을 보면서 잘나오는지 확인하는게 방법인거같아요.
저 방식대로 점점 개선해나가면 될겁니다. :)

},
{
"role": "user",
"content": (
"Context Data:\n"
f"{json.dumps(context_data, indent=4)}\n\n"
"Selected Text:\n"
f"{selected_text}\n\n"
# "Key Path:\n"
# f"{key_path}\n\n"
"User Request:\n"
f"{requirements}\n\n"
"Please update the selected text based on the instructions provided."
)
},
{
"role": "assistant",
"content": f"Sample summary format: {prompt}."
}
],
max_tokens=settings.max_output_tokens,
response_format=ResumeResponseDto
)

# GPT 응답 파싱
response_text = response.choices[0].message.parsed

# ResumeResponseDto 객체로 반환
return response_text


except Exception as e:
print(f"Error modifying resume with GPT: {e}")
return context_data # 오류 발생 시 기존 데이터 반환
44 changes: 44 additions & 0 deletions app/services/json_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import json

def find_key_by_value(json_data, target_value):
"""
JSON 데이터에서 target_value를 가진 키를 재귀적으로 검색합니다.
"""
for key, value in json_data.items(): # JSON 데이터의 각 키-값 쌍을 순회
if isinstance(value, dict): # 값이 중첩된 딕셔너리인 경우
result = find_key_by_value(value, target_value) # 재귀적으로 해당 딕셔너리 내부 탐색
if result:
return f"{key}.{result}" # 부모 키와 결과 키를 합쳐 반환
elif isinstance(value, list): # 값이 리스트인 경우
for i, item in enumerate(value): # 리스트의 각 항목을 순회
if isinstance(item, dict): # 리스트 항목이 딕셔너리인 경우
result = find_key_by_value(item, target_value) # 딕셔너리를 재귀적으로 탐색
if result:
return f"{key}[{i}].{result}" # 부모 키와 인덱스를 포함한 경로 반환
elif item == target_value: # 리스트 항목이 target_value와 동일한 경우
return f"{key}[{i}]" # 리스트의 인덱스를 포함한 경로 반환
elif value == target_value: # 값이 target_value와 동일한 경우
return key # 키 반환
return None # target_value를 찾지 못한 경우 None 반환


def update_json_by_key(json_data, key_path, new_value):
"""
JSON 데이터에서 지정된 key_path에 있는 값을 new_value로 업데이트합니다.
"""
keys = key_path.split('.') # key_path를 '.' 기준으로 나눠 리스트로 변환
current = json_data # JSON 데이터를 탐색하기 위해 초기화

for key in keys[:-1]: # 마지막 키를 제외한 경로를 순회
if '[' in key and ']' in key: # 키가 리스트 인덱스를 포함하는 경우
list_key, index = key[:-1].split('[') # 리스트 키와 인덱스를 분리
current = current[list_key][int(index)] # 리스트 인덱스를 사용해 값 탐색
else: # 리스트가 아닌 일반 키인 경우
current = current[key] # 키를 사용해 값 탐색

final_key = keys[-1] # 경로의 마지막 키
if '[' in final_key and ']' in final_key: # 마지막 키가 리스트 인덱스를 포함하는 경우
list_key, index = final_key[:-1].split('[') # 리스트 키와 인덱스를 분리
current[list_key][int(index)] = new_value # 새로운 값으로 업데이트
else: # 마지막 키가 리스트 인덱스가 아닌 경우
current[final_key] = new_value # 새로운 값으로 업데이트