From d43a3465b67af543227735b28b2d016bee54d1ad Mon Sep 17 00:00:00 2001 From: Oh-JunTaek <143782929+Oh-JunTaek@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:41:27 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=82=A4=EA=B0=92=20=EC=B0=BE=EC=9D=84=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이력서 수정 고도화 예비책으로, 만약 드래그 된 부분에 해당하는 키값 찾는 로직 --- app/services/json_service.py | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/services/json_service.py 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 From 0749b746b605310ceddd5d3f8e19e1742c6f874e Mon Sep 17 00:00:00 2001 From: Oh-JunTaek <143782929+Oh-JunTaek@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:42:39 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20gpt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/gpt_service.py | 75 ++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/app/services/gpt_service.py b/app/services/gpt_service.py index 849fbc1..1145712 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 # 오류 발생 시 기존 데이터 반환 From fa825f87c3b92d1f0a39b5a65331f85291fe4f6a Mon Sep 17 00:00:00 2001 From: Oh-JunTaek <143782929+Oh-JunTaek@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:43:07 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/prompts/resume_prompt.py | 6 ++++++ 1 file changed, 6 insertions(+) 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 From c3927d3325d0f12bdc1628c7177ee85e1473027f Mon Sep 17 00:00:00 2001 From: Oh-JunTaek <143782929+Oh-JunTaek@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:43:28 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/dto/resume_modify_dto.py | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 app/dto/resume_modify_dto.py 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 From a8eea4ab102216752b8f5ee23b3f4614378215ab Mon Sep 17 00:00:00 2001 From: Oh-JunTaek <143782929+Oh-JunTaek@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:44:14 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20str?= =?UTF-8?q?=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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" From 61d30f2aebeef11bf4ea4a9edc3f744b46b2712a Mon Sep 17 00:00:00 2001 From: Oh-JunTaek <143782929+Oh-JunTaek@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:48:01 +0900 Subject: [PATCH 6/8] =?UTF-8?q?perf:=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80?= =?UTF-8?q?=20=EC=95=8A=EB=8A=90=20=EA=B8=B0=EB=8A=A5=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/gpt_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/gpt_service.py b/app/services/gpt_service.py index 1145712..bbf073a 100644 --- a/app/services/gpt_service.py +++ b/app/services/gpt_service.py @@ -3,7 +3,7 @@ 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 +# from app.services.json_service import find_key_by_value import tiktoken import json import os From 434c65c03ba55fc8c54cbf45c953ed9994959d9c Mon Sep 17 00:00:00 2001 From: Oh-JunTaek <143782929+Oh-JunTaek@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:48:21 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/routes.py | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) 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 From 462f04948f182f1123dc28b7f853ec75a7109ff1 Mon Sep 17 00:00:00 2001 From: Oh-JunTaek <143782929+Oh-JunTaek@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:49:56 +0900 Subject: [PATCH 8/8] =?UTF-8?q?docs:=20=EA=B9=83=20=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=85=B8=EC=96=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B2=B0=EA=B3=BC=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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