CoFix AI는 BE가 protobuf/gRPC로 전달한 학습 데이터를 Gemini에 전달하고, 세 가지 기능의 결과를 다시 protobuf로 반환한다.
- 코드 분석: 현재 코드의 오류와 patch로 해결된 분류를 반환
- 문제 생성: 최근 이슈를 우선순위화해 4지선다 문제를 반환
- 학습 챗봇: 누적 약점과 최근 이슈를 참고한 Markdown 답변을 반환
git clone --recurse-submodules https://github.com/Yanus306/CoFix-AI.git
cd CoFix-AI
.\.venv\Scripts\python.exe scripts\generate_protos.py
.\.venv\Scripts\python.exe -m unittest discoverproto는 Yanus306/Cofix-proto 서브모듈이며 .proto 파일만 둔다. 생성된 Python binding은 generated_proto에 둔다. gRPC 서버의 기본 주소는 127.0.0.1:50051이다.
구현 파일: ai_feedback_pipeline.py
AnalyzeCodeRequest는 다음 세 필드를 받는다. reserved 1은 과거에 사용했던 필드 번호를 재사용하지 않기 위한 예약 번호다.
message RecentIssue {
string dataset = 1;
string title = 2;
repeated string learning_directions = 3;
string code = 4;
string guide = 5;
}
message LearningContext {
repeated RecentIssue recent_issues = 1;
}
message AnalyzeCodeRequest {
reserved 1;
string code = 2;
string patch = 3;
LearningContext learning_context = 4;
}code: 분석할 최종 전체 코드patch: 작성 중 변경된 표준 unified diff 원문. 오류 판정의 기준은code이고 patch는 수정·개선 여부를 확인하는 보조 자료다.learning_context.recent_issues: 최근 분석 이슈. 현재 코드에 실제로 존재하지 않는 과거 오류를 새 이슈로 만들지는 않는다.
code: "def calculate_average(values):\n total = 0\n for i in range(len(values)):\n total += values[i]\n return total / len(values)"
patch: "===================================================================\n--- a/calculate.py\n+++ b/calculate.py\n@@ -3 +3 @@\n- for i in range(len(values) + 1):\n+ for i in range(len(values)):"
learning_context {
recent_issues {
dataset: "edge_case"
title: "빈 리스트 평균 계산 오류"
learning_directions: "빈 입력 검사"
learning_directions: "경계값 처리"
code: "return total / len(values)"
guide: "🚨 ## 문제\n빈 리스트에서 0으로 나눌 수 있음\n💡 ## 해결\n빈 입력을 먼저 검사\n✨ ## 핵심 원리\n연산 전에 경계값을 확인해야 함"
}
recent_issues {
dataset: "loop_control"
title: "반복 범위가 리스트 길이를 초과함"
learning_directions: "반복문 범위"
learning_directions: "오프바이원 오류"
code: "for i in range(len(values) + 1):\n total += values[i]"
guide: "🚨 ## 문제\n유효 인덱스를 초과함\n💡 ## 해결\n올바른 반복 범위 사용\n✨ ## 핵심 원리\n유효 인덱스는 0부터 길이-1까지임"
}
}message CodeIssue {
string code = 1;
string label = 2;
string title = 3;
string description = 4;
repeated string learning_directions = 5;
string dataset = 6;
string guide = 7;
}
message AnalyzeCodeResponse {
repeated CodeIssue issues = 1;
reserved 2;
repeated string improvements = 3;
}issues의 각 항목은 오류 코드 범위, 분류 표시명, 제목, 설명, 학습 방향, dataset key, 가이드의 7개 필드를 가진다. guide는 항상 다음 섹션 헤더로 정규화된다.
🚨 ## 문제
문제 내용
💡 ## 해결
해결 내용
✨ ## 핵심 원리
핵심 원리
improvements는 patch로 해결되었다고 판단한 분류의 dataset key만 중복 없이 담는다. 따라서 CodeImprovement 객체나 코드·제목을 넣지 않는다.
위 입력을 현재 코드로 protobuf에 매핑한 전체 출력은 다음과 같다.
issues {
code: "def calculate_average(values):\n total = 0\n for i in range(len(values)):\n total += values[i]\n return total / len(values)"
label: "경계값"
title: "빈 리스트에서 평균을 계산할 수 없음"
description: "values가 비어 있으면 len(values)가 0이어서 ZeroDivisionError가 발생합니다."
learning_directions: "빈 입력 검사"
learning_directions: "경계값 처리"
dataset: "edge_case"
guide: "🚨 ## 문제\n빈 리스트에서 0으로 나눔\n💡 ## 해결\n계산 전에 빈 입력을 처리함\n✨ ## 핵심 원리\n연산 전 경계값을 검사해야 함"
}
improvements: "loop_control"최종 코드가 다음처럼 두 오류를 모두 수정된 상태로 실제 Gemini 분석에 전달된 경우 정규화 결과는 다음과 같다.
{
"issues": [],
"improvements": [
"loop_control",
"edge_case"
]
}처리 순서는 입력 검증 → 분류표 로딩 → 코드 줄 번호화(프롬프트 내부) → patch와 학습 문맥을 포함한 Gemini 요청 → JSON 파싱 → dataset/label/7개 필드 검증 → guide 정규화 → protobuf 매핑이다.
구현 파일: issue_quiz.py
message RecentIssue {
string dataset = 1;
string title = 2;
repeated string learning_directions = 3;
string code = 4;
string guide = 5;
}
message GenerateIssueQuizRequest {
string selected_level = 1;
int32 problem_count = 2;
repeated RecentIssue issues = 3;
}selected_level:easy,medium,hard중 하나problem_count: 양의 문제 수issues: 코드 분석에서 정리된 개별 이슈 목록. 현재 기본값은 3개지만 요청값을 검증해 처리한다.
selected_level: "medium"
problem_count: 3
issues {
dataset: "loop_control"
title: "반복 범위가 리스트 길이를 초과함"
learning_directions: "반복문 범위"
learning_directions: "오프바이원 오류"
code: "for i in range(len(values) + 1):\n total += values[i]"
guide: "🚨 ## 문제\n유효 인덱스를 초과함\n💡 ## 해결\n올바른 반복 범위 사용\n✨ ## 핵심 원리\n유효 인덱스는 0부터 길이-1까지임"
}
issues {
dataset: "edge_case"
title: "빈 리스트 평균 계산 오류"
learning_directions: "빈 입력 검사"
learning_directions: "경계값 처리"
code: "return total / len(values)"
guide: "🚨 ## 문제\n빈 리스트에서 0으로 나눌 수 있음\n💡 ## 해결\n빈 입력을 먼저 검사\n✨ ## 핵심 원리\n연산 전에 경계값을 확인해야 함"
}message QuizChoice {
string id = 1;
string text = 2;
}
message QuizCodeBlock {
string language = 1;
string content = 2;
}
message QuizProblem {
string question = 1;
optional QuizCodeBlock code_block = 2;
repeated QuizChoice choices = 3;
string answer = 4;
string explanation = 5;
}
message GenerateIssueQuizResponse {
repeated QuizProblem problems = 1;
}실제 protobuf 응답은 다음처럼 각 문제에 질문, 선택적 코드 빈칸, A~D 선택지, 정답, 해설을 담는다.
problems {
question: "다음 반복문이 IndexError를 발생시키지 않도록 빈칸에 들어갈 표현은?"
code_block { language: "python" content: "for i in range(/* 빈칸 */):\n total += values[i]" }
choices { id: "A" text: "len(values)" }
choices { id: "B" text: "len(values) + 1" }
choices { id: "C" text: "len(values) - 1" }
choices { id: "D" text: "values" }
answer: "A"
explanation: "range(len(values))는 0부터 len(values)-1까지 순회합니다."
}
problems {
question: "빈 리스트의 평균을 계산할 때 가장 먼저 확인해야 하는 조건은?"
choices { id: "A" text: "합계가 음수인지" }
choices { id: "B" text: "리스트가 비었는지" }
choices { id: "C" text: "값이 정렬됐는지" }
choices { id: "D" text: "중복 값이 있는지" }
answer: "B"
explanation: "빈 리스트는 길이가 0이므로 나눗셈 전에 빈 입력을 처리해야 합니다."
}
problems {
question: "리스트 인덱스의 올바른 유효 범위는?"
choices { id: "A" text: "1부터 길이까지" }
choices { id: "B" text: "0부터 길이까지" }
choices { id: "C" text: "0부터 길이-1까지" }
choices { id: "D" text: "1부터 길이-1까지" }
answer: "C"
explanation: "길이가 n인 리스트의 유효 인덱스는 0부터 n-1까지입니다."
}처리 순서는 난이도·문제 수·이슈 필드 검증 → dataset 반복 횟수와 보안/런타임 중요도를 반영한 우선순위화 → 선택한 난이도와 사용자 약점에 맞춘 주제 계획 → Gemini 요청 → 문제 수, A~D, 정답, 선택적 코드 블록 검증 → protobuf 매핑이다.
구현 파일: learning_chatbot.py
message CategoryCount {
string dataset = 1;
int32 count = 2;
}
message RecentIssue {
string dataset = 1;
string title = 2;
repeated string learning_directions = 3;
string code = 4;
string guide = 5;
}
message ChatRequest {
reserved 1;
string title = 2;
string message = 3;
repeated CategoryCount category_counts = 4;
repeated RecentIssue recent_issues = 5;
string summary = 6;
}title은 현재 채팅방 제목(비어 있으면 모델이 새 제목을 제안), message는 사용자 질문, category_counts는 분류별 누적 횟수, recent_issues는 최근 이슈, summary는 BE가 보관한 이전 대화 요약 문자열이다. 0회 분류는 모델 프롬프트에 넣기 전에 제거하고, 최근 이슈와 요약은 각각 최대 5개 범위로 압축한다.
title: "반복문 범위 오류"
message: "최근 반복문 실수를 참고해서 이 코드를 어떻게 고칠지 알려줘."
category_counts { dataset: "syntax_structure" count: 0 }
category_counts { dataset: "variable_type" count: 2 }
category_counts { dataset: "scope_lifetime" count: 0 }
category_counts { dataset: "operator_logic" count: 1 }
category_counts { dataset: "assignment_mutability" count: 0 }
category_counts { dataset: "type_annotation" count: 0 }
category_counts { dataset: "string_handling" count: 0 }
category_counts { dataset: "array_collection" count: 0 }
category_counts { dataset: "data_format_parsing" count: 0 }
category_counts { dataset: "null_missing_value" count: 0 }
category_counts { dataset: "data_validation" count: 1 }
category_counts { dataset: "function_usage" count: 0 }
category_counts { dataset: "api_misuse" count: 0 }
category_counts { dataset: "side_effect" count: 0 }
category_counts { dataset: "conditional" count: 0 }
category_counts { dataset: "loop_control" count: 7 }
category_counts { dataset: "edge_case" count: 4 }
category_counts { dataset: "state_management" count: 0 }
category_counts { dataset: "exception_handling" count: 0 }
category_counts { dataset: "error_propagation" count: 0 }
category_counts { dataset: "logging_diagnostics" count: 0 }
category_counts { dataset: "time_complexity" count: 0 }
category_counts { dataset: "space_complexity" count: 0 }
category_counts { dataset: "algo_selection" count: 0 }
category_counts { dataset: "data_structure_choice" count: 0 }
category_counts { dataset: "recursion" count: 0 }
category_counts { dataset: "memory_management" count: 0 }
category_counts { dataset: "resource_management" count: 0 }
category_counts { dataset: "buffer_boundary" count: 0 }
category_counts { dataset: "ui_dom_rendering" count: 0 }
category_counts { dataset: "tensor_matrix_shape" count: 0 }
category_counts { dataset: "concurrency" count: 0 }
category_counts { dataset: "async_handling" count: 0 }
category_counts { dataset: "transaction_atomicity" count: 0 }
category_counts { dataset: "security_input" count: 0 }
category_counts { dataset: "auth_access_control" count: 0 }
category_counts { dataset: "secret_handling" count: 0 }
category_counts { dataset: "crypto_randomness" count: 0 }
category_counts { dataset: "dependency_config" count: 0 }
category_counts { dataset: "io_network" count: 0 }
category_counts { dataset: "database_query" count: 0 }
category_counts { dataset: "test_coverage" count: 0 }
category_counts { dataset: "readability" count: 0 }
category_counts { dataset: "clean_code" count: 0 }
category_counts { dataset: "maintainability_design" count: 0 }
category_counts { dataset: "performance_runtime" count: 0 }
recent_issues {
dataset: "loop_control"
title: "반복 범위가 리스트 길이를 초과함"
learning_directions: "반복문 범위"
learning_directions: "오프바이원 오류"
code: "for i in range(len(values) + 1):\n total += values[i]"
guide: "🚨 ## 문제\n유효 인덱스를 초과함\n💡 ## 해결\n올바른 반복 범위 사용\n✨ ## 핵심 원리\n유효 인덱스는 0부터 길이-1까지임"
}
summary: "사용자는 이전 대화에서 반복문 범위 오류의 원인과 오프바이원 개념을 질문했다."Gemini 원문은 첫 줄의 한 줄 JSON과 두 번째 줄부터의 Markdown으로 구성된다. 서버는 이를 한 번 파싱해 protobuf의 세 필드로 나눈다. Markdown을 JSON 문자열로 변환했다가 다시 복원하는 방식은 사용하지 않는다.
message ChatResponse {
optional string title = 1;
optional string conversation_summary = 2;
string markdown_answer = 3;
}실제 출력 protobuf 예시는 다음과 같다.
title: "반복문 범위 오류"
conversation_summary: "사용자는 반복 범위 초과 오류의 수정 방향을 질문했고 유효 인덱스와 반복 범위에 관한 힌트를 안내받았다."
markdown_answer: "## 수정 방향\n\n반복 범위를 리스트의 유효 인덱스에 맞춰 확인해 보세요."처리 순서는 필드와 문자열 검증 → 분류표 key 검증 → 0회 분류 제거 및 오류 횟수 내림차순 정렬 → 최근 이슈/대화 요약의 최대 5개 압축 → 챗봇 프롬프트 생성 → Gemini 원문 응답의 첫 줄 JSON 파싱 → title·conversation_summary·Markdown 분리 → protobuf 응답이다. 코딩과 무관한 질문은 두 메타데이터를 null로 하고 Markdown으로 코딩 질문만 지원한다고 안내한다.
구현 파일: cofix_ai_server.py
서버는 protobuf 요청을 Python 매핑으로 변환하고 해당 기능을 호출한 뒤 결과를 출력 protobuf로 변환한다.
| RPC | Python 처리기 | 결과 |
|---|---|---|
CodeAnalysisService.AnalyzeCode |
ai_feedback_pipeline |
issues, improvements |
IssueQuizService.GenerateIssueQuiz |
issue_quiz |
problems |
LearningChatbotService.Chat |
learning_chatbot |
title, conversation_summary, markdown_answer |
입력 오류는 INVALID_ARGUMENT, Gemini 연결 실패는 UNAVAILABLE, 시간 초과는 DEADLINE_EXCEEDED, 예상하지 못한 오류는 INTERNAL gRPC 상태로 변환한다.
완전한 wire 예시는 examples/proto에 있으며, schema 변경 후에는 다음 명령으로 binding을 재생성한다.
.\.venv\Scripts\python.exe scripts\generate_protos.py