diff --git a/antigravity/sdk.go b/antigravity/sdk.go index a25eab3..bbfcc51 100644 --- a/antigravity/sdk.go +++ b/antigravity/sdk.go @@ -3,7 +3,6 @@ package antigravity import ( "bytes" "context" - "encoding/json" "fmt" "os/exec" "strings" @@ -19,15 +18,16 @@ type StudyElement struct { // GeneratedQuestion matches the structure to be saved in QuizQuestions DB. type GeneratedQuestion struct { - QuestionText string `json:"questionText"` - Choice1 string `json:"choice1"` - Choice2 string `json:"choice2"` - Choice3 string `json:"choice3"` - Choice4 string `json:"choice4"` - CorrectAnswer int `json:"correctAnswer"` - Explanation string `json:"explanation"` - Difficulty string `json:"difficulty"` - Subject string `json:"subject"` + QuestionText string `json:"questionText"` + Choice1 string `json:"choice1"` + Choice2 string `json:"choice2"` + Choice3 string `json:"choice3"` + Choice4 string `json:"choice4"` + CorrectAnswer int `json:"correctAnswer"` + CorrectAnswerStr string `json:"correctAnswerStr"` + Explanation string `json:"explanation"` + Difficulty string `json:"difficulty"` + Subject string `json:"subject"` } // CallAgy runs the agy CLI with the given prompt. @@ -58,54 +58,26 @@ func ExtractStudyElement(ctx context.Context, questionText, choice1, choice2, ch "정답: %s\n"+ "해설: %s\n\n"+ "[작성 지침]\n"+ - "1. 단순 정답 해설을 넘어, 문제에 포함된 핵심 개념(예: 특정 암호화 알고리즘, 시스템 보안 기술, 네트워크 프로토콜 등)의 정의, 특징, 작동 방식, 장단점을 일목요연하게 정리해 주세요.\n"+ - "2. 가독성을 위해 마크다운 포맷을 사용해 주세요. (예: 표, 리스트)\n"+ - "3. 수식이나 암호학적 기호가 필요한 경우 KaTeX 문법(예: $E_k(M) = C$)을 적극적으로 사용해 주세요.\n"+ - "4. 아키텍처나 흐름을 시각화할 수 있도록 Mermaid 다이어그램(예: ```mermaid ... ```) 또는 간단한 아스키 차트(ASCII Chart/Plot)를 본문에 반드시 하나 이상 포함해 주세요.\n"+ - "5. 다른 잡다한 설명(예: \"네, 작성해 드리겠습니다\" 등)은 모두 제외하고, 오직 마크다운으로 구성된 학습요소 내용만 반환해 주세요.", + "1. **매우 중요**: 문서의 가장 첫 번째 줄은 반드시 핵심 학습개념/키워드를 설명하는 제목으로 시작해 주세요. (예: `# AES 암호화 알고리즘` 또는 `# OSI 7계층과 캡슐화`)\n"+ + " - 첫 줄 외에 다른 서두(예: \"네, 아래는 개념 정리입니다\" 등)는 일절 포함하지 마십시오.\n"+ + "2. 단순 정답 해설을 넘어, 문제에 포함된 핵심 개념의 정의, 특징, 작동 방식, 장단점을 일목요연하게 정리해 주세요. (표, 리스트 적극 활용)\n"+ + "3. **KaTeX 문법 규칙**: 수식 작성 시 파싱 오류를 철저히 방지하세요. 괄호 매칭을 완벽히 지키고, 수식 내부에 한글을 포함해야 하는 경우 반드시 `\\text{한글}` 형식을 사용하세요. 인라인 수식은 `$수식$`, 디스플레이 수식은 `$$수식$$`로만 작성하세요.\n"+ + "4. **Mermaid 문법 규칙**: Mermaid 버전 10.9.6 문법을 엄격히 지켜 `syntax error in text mermaid` 에러를 예방하세요.\n"+ + " - `subgraph` 이름(타이틀)에는 공백이나 한글, 괄호가 들어가므로 **반드시** 쌍따옴표로 감싸서 선언하세요. (예: `subgraph \"정상 스택 (Normal)\"`)\n"+ + " - 화살표 연결선의 텍스트 라벨 `|설명|` 안에는 괄호 `()`, 슬래시 `/`, 백슬래시 `\\` 등의 특수문자를 절대 쓰지 마세요. (예: `-->|SMTP 프로토콜 포트 25|` 로 표현)\n"+ + " - 노드 내부에 한글, 공백, 기호, 괄호가 포함되는 경우 **반드시** 쌍따옴표로 감싸서 선언하세요 (예: `A[\"AES 암호화\"] --> B[\"키 생성\"]`).\n"+ + " - 연결 화살표 기호는 오직 `-->` 또는 `-.->` 만 사용해야 하며, `->` 나 `- >` 처럼 깨진 형식을 쓰지 마세요.\n"+ + " - 서브그래프(subgraph)를 열었다면 반드시 `end`로 닫아 마감하세요.\n"+ + " - 백슬래시(`\\`)는 이스케이프 오류 방지를 위해 두 번(`\\\\`) 겹쳐 적으세요.\n"+ + "5. 다른 불필요한 설명은 제외하고, 오직 첫 줄 개념명(# ...)으로 시작하는 마크다운 본문만 반환해 주세요.", questionText, choice1, choice2, choice3, choice4, correctAnswer, explanation) - return CallAgy(ctx, prompt) + return prompt, nil } // GenerateDescriptiveQuestion generates a new descriptive/practical exam question based on given reference questions. func GenerateDescriptiveQuestion(ctx context.Context, referenceExams []string) (*GeneratedQuestion, error) { - refs := strings.Join(referenceExams, "\n---\n") - prompt := fmt.Sprintf("기존 정보보호기사 실기 문제들을 분석하여, 이와 유사한 출제 패턴과 난이도를 가진 \"새로운 정보보호기사 실기 문제\"를 만들어 주세요.\n\n"+ - "[기존 실기 문제 예시]\n"+ - "%s\n\n"+ - "[출제 지침]\n"+ - "1. 문제 유형은 정보보호기사 실기 시험의 단답형 또는 서술형 형태여야 합니다. (지문을 제시하고 빈칸 채우기, 또는 특정 시나리오를 주고 대처 방안 서술하기 등)\n"+ - "2. 문제의 설명, 예시, 지문이 풍부하고 실무적이어야 합니다.\n"+ - "3. 생성된 문제는 반드시 아래의 JSON 포맷으로만 출력되어야 합니다. 마크다운 코드 블록(```json ... ```)을 사용하지 말고, 원시 JSON 텍스트만 출력하세요.\n\n"+ - "JSON 구조:\n"+ - "{\n"+ - " \"questionText\": \"문제 지문 및 지시 사항 (예: 다음 지문을 읽고 빈칸 A, B에 들어갈 단어를 기술하시오...)\",\n"+ - " \"explanation\": \"이 문제에 대한 모범 답안 및 상세한 해설\",\n"+ - " \"difficulty\": \"실기(Go)\",\n"+ - " \"subject\": \"만든 자료\"\n"+ - "}\n\n"+ - "오직 JSON만 출력해야 합니다. 다른 텍스트는 절대 포함하지 마십시오.", refs) - - resp, err := CallAgy(ctx, prompt) - if err != nil { - return nil, err - } - - // Clean JSON response (sometimes LLM might return markdown code blocks despite instructions) - cleaned := resp - if idx := strings.Index(cleaned, "{"); idx != -1 { - cleaned = cleaned[idx:] - } - if idx := strings.LastIndex(cleaned, "}"); idx != -1 { - cleaned = cleaned[:idx+1] - } - - var gq GeneratedQuestion - err = json.Unmarshal([]byte(cleaned), &gq) - if err != nil { - return nil, fmt.Errorf("failed to parse JSON from AI: %v, raw output: %s", err, resp) - } - - return &gq, nil + // We will create the prompt text in quiz_service.go using callAIEngine. + // This function is kept for backwards compatibility but we will format the prompt inside quiz_service to leverage both API and agy engines. + return nil, fmt.Errorf("use the prompt constructor and callAIEngine helper instead") } diff --git a/frontend/bindings/changeme/models.ts b/frontend/bindings/changeme/models.ts index eff6642..5c9b5cc 100644 --- a/frontend/bindings/changeme/models.ts +++ b/frontend/bindings/changeme/models.ts @@ -185,6 +185,7 @@ export class StudyElementDB { "source": string; "questionText": string; "content": string; + "keyword": string; "category": string; /** Creates a new StudyElementDB instance. */ @@ -201,6 +202,9 @@ export class StudyElementDB { if (!("content" in $$source)) { this["content"] = ""; } + if (!("keyword" in $$source)) { + this["keyword"] = ""; + } if (!("category" in $$source)) { this["category"] = ""; } diff --git a/frontend/bindings/changeme/quizservice.ts b/frontend/bindings/changeme/quizservice.ts index f308cb1..dc617af 100644 --- a/frontend/bindings/changeme/quizservice.ts +++ b/frontend/bindings/changeme/quizservice.ts @@ -63,15 +63,15 @@ export function DeleteUser(username: string): $CancellablePromise { * ExtractStudyElementsForCategory extracts study elements for all questions in the category. * It skips questions that already have an extracted study element. */ -export function ExtractStudyElementsForCategory(category: string): $CancellablePromise { - return $Call.ByID(3340623770, category); +export function ExtractStudyElementsForCategory(category: string, useGeminiAPI: boolean, delaySeconds: number): $CancellablePromise { + return $Call.ByID(3340623770, category, useGeminiAPI, delaySeconds); } /** * GenerateGoPracticalExams creates new exam questions based on existing ones. */ -export function GenerateGoPracticalExams(count: number): $CancellablePromise { - return $Call.ByID(4059537052, count); +export function GenerateGoPracticalExams(count: number, useGeminiAPI: boolean, delaySeconds: number, examType: string): $CancellablePromise { + return $Call.ByID(4059537052, count, useGeminiAPI, delaySeconds, examType); } /** diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 9437fb1..fc0a953 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,5 +1,5 @@