diff --git a/.gitignore b/.gitignore index 24aedfa..dcbbf4a 100644 --- a/.gitignore +++ b/.gitignore @@ -231,4 +231,8 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/windows -/data/ \ No newline at end of file +app/data/code +app/data/commit +app/data/pr +app/data/project +app/data/repo \ No newline at end of file diff --git a/app/api/v1/routes.py b/app/api/v1/routes.py index 5eecbdc..b79ca04 100644 --- a/app/api/v1/routes.py +++ b/app/api/v1/routes.py @@ -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 @@ -54,4 +57,36 @@ async def generate_resume(request: ResumeRequest): aboutMe=aboutme # aboutMe=aboutme_techstack.aboutMe ) - return resume_response \ No newline at end of file + 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.") \ No newline at end of file diff --git a/app/config/settings.py b/app/config/settings.py index 59aa8ea..2415884 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -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 @@ -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" diff --git a/app/dto/resume_modify_dto.py b/app/dto/resume_modify_dto.py new file mode 100644 index 0000000..ad72835 --- /dev/null +++ b/app/dto/resume_modify_dto.py @@ -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] + projects: List[ProjectResponse] + links: Optional[List[Link]] # null 가능 + educations: List[Education] + certificates: List[Certificate] + +class UpdateRequestDto(BaseModel): + selectedText: str + requirement: str + resumeInfo: ResumeResponseDto \ No newline at end of file diff --git a/app/prompts/resume_prompt.py b/app/prompts/resume_prompt.py index 4c387d9..26656ca 100644 --- a/app/prompts/resume_prompt.py +++ b/app/prompts/resume_prompt.py @@ -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." ) \ No newline at end of file diff --git a/app/services/gpt_service.py b/app/services/gpt_service.py index 849fbc1..bbf073a 100644 --- a/app/services/gpt_service.py +++ b/app/services/gpt_service.py @@ -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 @@ -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...") @@ -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 "" \ No newline at end of file + 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." + ) + }, + { + "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 # 오류 발생 시 기존 데이터 반환 diff --git a/app/services/json_service.py b/app/services/json_service.py new file mode 100644 index 0000000..33d3244 --- /dev/null +++ b/app/services/json_service.py @@ -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 # 새로운 값으로 업데이트 \ No newline at end of file