본문 바로가기
파이썬 프로그래밍/파이썬 개발

5. DSPy 생기부 AI 에이전트 - 구조화1

by Majestyblue 2025. 5. 20.

4단계의 생기부 작성 AI 에이전트를 모두 완료하였다. 

이제 남은 일은 각 역할을 분배하고, 모듈화하여 GUI로 만들기 편하게 해야 한다. 

 

모듈화할 역할은 아래와 같다.

1. 공급자 선택하기

2. 프롬프트들 

3. Single 모드(작성자)

4. Dual 모드(작성자-평가자)

5. Triple 모드(분배자-작성자-평가자)

6. Quad 모드(분배자-요약자-작성자-평가자)

 

모듈화를 이미지로 표현해 보자

 

 

1. 공급자 모듈 구성하기(Provider.py)

gemini, openai, ollama 셋 중 하나를 선택하도록 구성하자.

 

Set_providers 클래스는 dspy 사용할 때, 코드 실행 전 언어 모델 제공자(Ollama, Gemini, OpenAI) 선택 및 필요한 설정(API 키, URL, 모델 이름 등) 적용 과정을 단순화한다.

  • 객체 생성 시 사용할 제공자 이름 및 해당 정보 전달 (__init__).
  • 객체 호출 시 (__call__), 내부적으로 configure_provider 실행되어 지정 제공자로 dspy 환경 설정
  • 각 제공자별 다른 설정 매개변수 코드 내부 분기 처리하고 오류 시 관련 정보 출력
  • 성공 설정 시 dspy.configure(lm=lm) 통해 전역 DSPy 설정 업데이트되어, 이후 DSPy 모듈들이 이 설정된 언어 모델 사용
import dspy
import random

class Set_providers():
    def __init__(self, provider_name: str, gemini_api_key: str, 
                 openai_api_key: str, ollama_base_url: str='http://localhost11434',
                 cache: str='False'):
        self.provider_name = provider_name
        self.gemini_api_key = gemini_api_key
        self.open_api_key = openai_api_key
        self.ollama_base_url = ollama_base_url
        self.cache = cache

    def configure_provider(self, provider_name: str, gemini_api_key: str, 
                           openai_api_key: str, ollama_base_url: str='http://localhost11434',
                           cache: str='False'):
        lm = None
        print(f"공급자를 사용하여 DSPy를 구성하려고 시도 중: {provider_name}")

        if provider_name == "ollama":
            # Ollama는 dspy.Ollama 클래스를 사용합니다. API 키는 필요하지 않습니다.
            try:
                lm = dspy.LM(model="ollama_chat/gemma3:12b-it-q4_K_M", api_base=ollama_base_url, api_key="", 
                             temperature=0.95 + 0.0001 * random.uniform(-1, 1), cache=cache)
                print(f"Ollama LM 생성됨: model='gemma3:12b-it-q4_K_M', base='{ollama_base_url}'")
            except Exception as e:
                print(f"생성 오류 Ollama LM: {e}")
                print("올라마가 작동 중이고 모델이 pull 되었는지 확인해 주세요.")

        elif provider_name == "gemini":
            # Gemini는 dspy.Google 클래스를 사용합니다.
            if not gemini_api_key or gemini_api_key == "YOUR_GEMINI_API_KEY":
                 print("경고: 제미니 API 키가 설정되지 않았습니다. 제미니 제공자가 작동하지 않을 수 있습니다.")
            try:
                lm = dspy.LM(model="gemini/gemini-1.5-flash-latest", api_key=gemini_api_key, 
                             temperature=0.95 + 0.0001 * random.uniform(-1, 1), cache=cache)
                print(f"Gemini LM 생성됨: model='gemini-1.5-flash-latest'")
            except Exception as e:
                print(f"생성 오류 Gemini LM: {e}")
                print("Gemini API key를 확인해 주세요.")


        elif provider_name == "openai":
            # OpenAI는 dspy.OpenAI 클래스를 사용합니다.
            if not openai_api_key or openai_api_key == "YOUR_OPENAI_API_KEY":
                 print("경고: OpenAI API 키가 설정되지 않았습니다. OpenAI 제공자가 작동하지 않을 수 있습니다.")
            try:
                lm = dspy.LM(model="openai/gpt-3.5-turbo", api_key=openai_api_key, 
                             temperature=0.95 + 0.0001 * random.uniform(-1, 1), cache=cache)
                print(f"OpenAI LM 생성됨: model='gpt-3.5-turbo'")
            except Exception as e:
                print(f"생성 오류 OpenAI LM: {e}")
                print("OpenAI API key를 확인해 주세요.")

        else:
            print(f"에러: 알 수 없는 공급자 이름입니다: {provider_name}. 'ollama', 'gemini', 'openai' 중에서 선택하세요.")
            raise ValueError(f"알 수 없는 공급자 이름: {provider_name}")
        
        # LM 인스턴스가 성공적으로 생성되었으면 dspy 설정을 업데이트합니다.
        if lm:
            dspy.configure(lm=lm)
            print(f"DSPy가 {provider_name}로 성공적으로 구성되었습니다.")
            return lm
        else:
            print(f"{provider_name}에 대한 DSPy 구성에 실패했습니다. LM 인스턴스는 없습니다.")

    def __call__ (self):
        return self.configure_provider(provider_name=self.provider_name, gemini_api_key=self.gemini_api_key,
                                       openai_api_key=self.open_api_key, ollama_base_url=self.ollama_base_url,
                                       cache = self.cache)

 

  1. 임포트 (Imports)
    • import dspy: DSPy 라이브러리( 언어 모델 파이프라인 구축 프레임워크 )를 가져온다. 
    • import random: 난수 생성. 여기서는 temperature 매개변수에 약간의 무작위성 추가 용도로 사용됨.
  2. Set_providers 클래스
    • 이 코어 클래스는 언어 모델 제공자 설정 로직 캡슐화한다.
  3. __init__ 메서드 (생성자)
    • def __init__(self, provider_name: str, gemini_api_key: str, openai_api_key: str, ollama_base_url: str='http://localhost11434', cache: str='False'):
    • Set_providers 클래스 객체 생성 시 호출
    • 객체 초기화하며, 필요한 설정 매개변수들을 인자로 받는다.
      • provider_name: 사용할 제공자 이름 ("ollama", "gemini", "openai" 중 하나).
      • gemini_api_key: Gemini API 키.
      • openai_api_key: OpenAI API 키.
      • ollama_base_url: Ollama 서버 기본 URL (기본값 http://localhost11434).
      • cache: 캐시 사용 여부 (문자열로 받으나 실제 DSPy 캐시는 불리언 값을 받는다).
    • 이 매개변수들을 객체 속성(instance attributes)으로 저장함 (self.provider_name 등).
  4. configure_provider 메서드
    • def configure_provider(self, ...):
    • 실제로 DSPy를 특정 제공자로 구성하는 핵심 로직
    • __init__와 동일 매개변수를 받음.
    • lm = None: 언어 모델 인스턴스 저장 변수 초기화
    • print(...): 현재 설정하려는 제공자 콘솔 출력.
    • 조건문 (if/elif/else): provider_name 값에 따라 다른 설정 적용
      • if provider_name == "ollama":
        • Ollama 제공자 설정함.
        • dspy.LM(...) 사용하여 dspy.Ollama 인스턴스 생성함. 모델 이름 ollama_chat/gemma3:12b-it-q4_K_M, api_base에 ollama_base_url 사용함. API 키는 필요하지 않음.
        • try...except 블록으로 오류 처리하고 메시지 출력함.
      • elif provider_name == "gemini":
        • Gemini 제공자 설정함.
        • gemini_api_key 설정되지 않았다면 경고 메시지 출력함.
        • dspy.LM(...) 사용하여 dspy.Google 인스턴스 생성함. 모델 이름 gemini/gemini-1.5-flash-latest, api_key 사용함.
        • try...except 블록으로 오류 처리하고 API 키 확인 메시지 출력함.
      • elif provider_name == "openai":
        • OpenAI 제공자 설정함.
        • openai_api_key 설정되지 않았다면 경고 메시지 출력함.
        • dspy.LM(...) 사용하여 dspy.OpenAI 인스턴스 생성함. 모델 이름 openai/gpt-3.5-turbo, api_key 사용함.
        • try...except 블록으로 오류 처리하고 API 키 확인 메시지 출력함.
      • else:
        • 지원하지 않는 provider_name인 경우 에러 메시지 출력하고 ValueError 예외 발생시킴.
    • 공통 설정: 각 제공자 설정 시 temperature, cache 값 설정됨. dspy.LM은 내부적으로 올바른 제공자 클래스 찾아 인스턴스화한다.
    • DSPy 구성 업데이트: if lm:으로 인스턴스 생성 확인 후, dspy.configure(lm=lm) 호출하여 DSPy가 이 lm 인스턴스 사용하도록 설정한다.
    • 성공/실패 메시지 출력하고, 성공 시 생성된 lm 인스턴스 반환한다.
  5. __call__ 메서드
    • def __call__(self):
    • 이 메서드 정의 시 Set_providers 클래스 객체 함수처럼 호출
    • 객체 생성 시 저장된 속성 사용하여 self.configure_provider 메서드 호출함.
    • 객체 생성 즉시 설정을 실행하는 편리한 방법을 제공함.

 

2. 싱글 모드(작성자) 코드(SingleWriter.py)

이 코드는 Writer라는 Signature를 통해 생활기록부 작성 작업의 입력과 출력 구조를 정의한다. 그리고 Single_Writer라는 Module을 통해 이 작업을 수행하는 구체적인 파이프라인을 구현한다. Single_Writer는 필요한 프롬프트 텍스트들을 미리 로드하고, Writer 시그니처를 사용하는 ChainOfThought 예측 모델을 내부적으로 사용하여, forward 메서드로 전달된 input_data와 로드된 프롬프트들을 조합하여 언어 모델에게 생활기록부 작성을 요청하고 그 결과를 반환하는 역할을 한다. 이것은 DSPy를 사용하여 특정 형식의 문서 생성을 자동화하는 기본적인 패턴을 보여준다.

 

 

import dspy
from .Prompts import single_instruction_prompt, activity_prompt, achieve_prompt, oneshot_prompt

class Writer(dspy.Signature):
    instruction = dspy.InputField(desc="생활기록부 작성, 지침과 작업에 대한 설명입니다.")
    input_data = dspy.InputField(desc="생활기록부 작성이 필요한 실제 데이터입니다.")
    activity = dspy.InputField(desc="1년동안 물리학I 활동 내용에 대한 소개로 학생 활동, 평가 등에 대한 정보입니다.")
    achievement  = dspy.InputField(desc="물리학I에서 목표로하는 성취기준입니다")
    one_shot = dspy.InputField(desc="생활기록부 예시(one shot) 입니다. 반드시! 예시의 형식을 준수하여 작성하세요!")
    student_record = dspy.OutputField(desc="생활기록부입니다. 반드시!! 한글로 출력하세요.")

class Single_Writer(dspy.Module):
    def __init__(self):
        self.instruction = single_instruction_prompt.write_prompt
        self.activity = activity_prompt.activity_context
        self.achievement = achieve_prompt.achievement_context
        self.one_shot = oneshot_prompt.one_shot_context

        self.writer = dspy.ChainOfThought(Writer)
        print("작성자(Writer) 단독 모델 선택되었습니다.")

    def forward(self, input_data):
        print("------생활기록부 작성중-----")
        output = self.writer(instruction=self.instruction, input_data=input_data,
                             activity=self.activity, achievement=self.achievement,
                             one_shot=self.one_shot).student_record
        print("------생활기록부 작성 완료-----")
        return output

 

1. 임포트 (Imports)

  • from .Prompts import single_instruction_prompt, activity_prompt, archieve_prompt, oneshot_prompt: 현재 패키지 내부의 Prompts 디렉토리(또는 모듈)에서 여러 프롬프트 내용을 담고 있는 객체들을 가져온다. 이 객체들은 언어 모델에게 전달할 구체적인 지침, 맥락, 예시 등을 포함하고 있다.

2. Writer 클래스

  • class Writer(dspy.Signature):: dspy.Signature 클래스를 상속받는다. Signature는 언어 모델에게 특정 작업을 요청할 때 사용될 입력 필드와 예상되는 출력 필드를 정의하는 역할을 한다. 이는 언어 모델과의 상호작용을 위한 일종의 계약 또는 인터페이스이다.
  • 클래스 내부에 정의된 변수들은 언어 모델이 이 작업을 수행하기 위해 필요로 하는 정보와 결과로 생성할 정보의 종류를 나타낸다. 모든 필드는 dspy.InputField 또는 dspy.OutputField로 선언되며, desc 매개변수를 통해 각 필드의 목적을 설명한다.
    • instruction = dspy.InputField(...): 언어 모델에게 생활기록부 작성을 위한 구체적인 지침이나 작업 설명을 제공하는 입력 필드이다.
    • input_data = dspy.InputField(...): 실제 생활기록부 작성에 필요한 원본 데이터를 담는 입력 필드이다. 학생 정보나 기타 필요한 원시 데이터이다.
    • activity = dspy.InputField(...): 학생의 특정 활동 내용에 대한 상세 정보를 담는 입력 필드이다. 여기서는 물리학I 활동 내용이다.
    • achievement = dspy.InputField(...): 해당 과목(물리학I)에서 목표로 하는 성취 기준에 대한 정보를 담는 입력 필드이다.
    • one_shot = dspy.InputField(...): 언어 모델에게 원하는 출력 형식이나 스타일을 보여주기 위한 하나의 작성 예시(one-shot example)를 제공하는 입력 필드이다. 예시 형식을 따르도록 강력히 지시한다.
    • student_record = dspy.OutputField(...): 언어 모델이 최종적으로 생성해야 할 결과물, 즉 생활기록부 내용을 담는 출력 필드이다. 반드시 한글로 출력되도록 요구한다.

3. Single_Writer 클래스

  • class Single_Writer(dspy.Module):: dspy.Module 클래스를 상속받는다. Module은 DSPy에서 특정 작업을 수행하는 논리적인 단위 또는 파이프라인을 나타낸다. 하나 이상의 예측 모델(Predictor)이나 다른 Module들을 조합하여 더 복잡한 작업을 수행할 수 있다.
  • __init__ 메서드:
    • 모듈이 생성될 때 초기화 작업을 수행한다.
    • 임포트한 프롬프트 객체들에서 실제 프롬프트 텍스트 내용을 가져와 클래스 멤버 변수(self.instruction, self.activity, self.achievement, self.one_shot)에 할당한다. 이 변수들은 forward 메서드에서 언어 모델에게 전달할 입력값으로 사용된다.
    • self.writer = dspy.ChainOfThought(Writer): dspy.ChainOfThought는 DSPy에서 제공하는 예측 모델 중 하나이다. 이것은 언어 모델에게 바로 최종 답을 내놓도록 하는 대신, 중간 추론 과정(Chain of Thought)을 생성하도록 유도하여 더 정확하고 신뢰성 있는 결과를 얻도록 한다. Writer 시그니처를 인자로 전달하여, 이 ChainOfThought 모델이 Writer 시그니처에 정의된 입력을 받고 출력을 생성하도록 설정한다.
    • "작성자(Writer) 단독 모델 선택되었습니다."라는 초기화 완료 메시지를 출력한다. 이 모듈이 Writer 시그니처를 사용하는 ChainOfThought 모델 하나로 구성되었음을 나타낸다.
  • forward 메서드:
    • def forward(self, input_data):: 이 Module이 실행될 때 호출되는 핵심 메서드이다. 이 메서드 내부에 실제 작업 수행 로직이 담겨 있다.
    • input_data를 인자로 받는다. 이 값은 아마도 Writer 시그니처의 input_data 필드에 해당하는 내용일 것이다.
    • "------생활기록부 작성중-----" 메시지를 출력하여 작업 시작을 알린다.
    • output = self.writer(...): __init__에서 설정한 self.writer (ChainOfThought 모델)를 호출한다. 이때 Writer 시그니처에 정의된 모든 입력 필드에 해당하는 값들을 키워드 인자로 전달한다. self.instruction, self.activity, self.achievement, self.one_shot은 초기화 시 로드된 프롬프트 내용이고, input_data는 forward 메서드의 인자로 전달받은 값이다.
    • .student_record: self.writer(...) 호출 결과는 Writer 시그니처의 출력 필드를 속성으로 가지는 객체이다. .student_record 속성을 통해 언어 모델이 생성한 최종 생활기록부 내용을 추출한다.
    • "------생활기록부 작성 완료-----" 메시지를 출력하여 작업 완료를 알린다.
    • 추출한 output (생활기록부 내용)을 반환한다.

 

각 프롬프트는 아래와 같다.

# achievement_prompt.py
achievement_context = r"""
1. 여러 가지 물체의 운동 사례를 찾아 속력의 변화와 운동 방향의 변화에 따라 분류할 수 있다.
2. 뉴턴 운동 법칙을 이용하여 직선 상에서 물체의 운동을 정량적으로 예측할 수 있다.
3. 뉴턴의 제3법칙의 적용 사례를 찾아 힘이 상호 작용임을 설명할 수 있다.
4. 물체의 1차원 충돌에서 충돌 전후의 운동량 보존을 이용하여 속력의 변화를 정량적으로 예측할 수 있다.
5. 충격량과 운동량의 관계를 이해하고, 일상생활에서 충격을 감소시키는 예를 찾아 설명할 수 있다.
6. 직선 상에서 운동하는 물체의 역학적 에너지가 보존되는 경우와 열에너지가 발생하여 역학적 에너지가 보존되지 않는 경우를 구별하여 설명할 수 있다.
7. 열기관이 외부와 열과 일을 주고받아 열기관의 내부 에너지가 변화됨을 사례를 들어 설명할 수 있다.
8. 열이 모두 일로 전환되지 않는다는 것을 사례를 들어 설명할 수 있다.
9. 모든 관성계에서 빛의 속도가 동일함을 알고 시간 지연, 길이 수축, 동시성과 관련된 현상을 설명할 수 있다.
10. 질량이 에너지로 변환됨을 사례를 들어 설명할 수 있다.
11. 전자가 원자에 속박되어 있음을 전기력을 이용하여 정성적으로 설명할 수 있다.
12. 원자 내의 전자는 불연속적 에너지 준위를 가지고 있음을 스펙트럼 관찰을 통하여 설명할 수 있다.
13. 고체의 에너지띠 이론으로 도체, 반도체, 절연체 등의 차이를 구분하고, 여러 가지 고체의 전기 전도성을 비교하는 탐구를 수행할 수 있다.
14. 종류가 다른 원소를 이용하여 반도체 소자를 만들 수 있음을 다이오드를 이용하여 설명할 수 있다.
15. 전류에 의한 자기 작용이 일상생활에서 적용되는 다양한 예를 찾아 그 원리를 설명할 수 있다.
16. 자성체의 종류를 알고 자성체가 활용되는 예를 찾을 수 있다.
17. 일상생활에서 전자기 유도 현상이 적용되는 다양한 예를 찾아 그 원리를 설명할 수 있다.
18. 파동의 진동수, 파장, 속력 사이의 관계를 알고 매질에 따라 파동의 속력이 다른 것을 활용한 예를 설명할 수 있다.
19. 파동의 전반사 원리를 이용한 광통신 과정을 설명할 수 있다.
20. 다양한 전자기파를 스펙트럼의 종류에 따라 구분하고, 그 사용 예를 찾아 설명할 수 있다.
21. 파동의 간섭이 활용되는 예를 찾아 설명할 수 있다.
22. 빛의 이중성을 알고, 영상정보가 기록되는 원리를 설명할 수 있다.
23. 물질의 이중성을 알고, 전자 현미경의 원리를 설명할 수 있다.
"""

# activity_prompt.py
activity_context = r"""1년동안 물리학I 활동 내용
[물리 점수]
첫 번째 시험 내용: 변위, 속도, 가속도, 뉴턴 법칙, 운동량과 충격량, 일과 운동에너지, 역학적 에너지, 열역학 1법칙, 열역학 2법칙
두 번째 시험 내용: 특수상대성이론, 핵융합, 핵분열, 전기력, 원자핵, 스펙트럼, 에너지 준위, 보어원자모형
세 번째 시험 내용: 에너지띠, 다이오드, 전기전도성, 전류에 의한 자기장, 물질의 자성, 전자기유도
네 번째 시험 내용: 파동의 정의, 파동의 굴절, 파동의 중첩, 전자기파, 광전 효과, 물질의 이중성
[물리의 재구성]
1.문제 선정 이유, 
2.문제에서 제시된 물리량과 확인해야 하는 물리량, 
3.확인해야 하는 물리량 구하는 법, 
4.재구성한 물리 현상 방법, 
5.재구성한 물리 현상과 기존 물리현상의 공통점과 차이점, 
6.재구성한 물리 현상으로 확인해야 하는 물리량 구하기, 
7.탐구를 통해 무엇을 배웠는지 구체적으로 설명하기, 
8.추가 계획 1가지 이상 제시하기
[물리 심화 탐구]
1.주제 선정에 대해 작성하세요, 
2.자료 수집에 대해 작성하세요, 
3.자료 분석에 대해 작성하세요, 
4.결론 도출에 대해 작성하세요, 
5.탐구 고찰에 대해 작성하세요
[교과선생님 관찰내용]
그 밖에 교과선생님이 적는 경우가 있습니다."""

# oneshot_prompt.py
one_shot_context = r"""<생활기록부 예시>
창의력과 집중력, 직관과 통찰이 우수한 학생임. 연필심의 길이에 따른 전기전도도 측정 실험에서 연필심, 전지, 전압계가 병렬로 연결되어 있으므로 전압이 연필심의 길이에 무관해야 함에도 불구하고 변한 이유를 연필심의 온도 변화에 의한 것으로 해석하고, 모둠 구성원에게 스위치 개폐시간에 대한 유의사항을 안내함. 직선 도선 전류 주위의 자기장 측정 실험에서 나침반의 회전 각도를 레이저 포인터를 이용하여 보다 정확하게 측정하는 방법을 고안하는 방식으로 실험 설계에서 창의력을 발휘하였으며, 전류에 의한 자기장의 이용의 사례로 자기 공명 영상 장치와 자기부상 열차의 구조에 대해 분석하고 작동 원리에 대해 탐구함. 네오디뮴 자석과 건전지, 철심을 이용해 간이 전동기를 만들어보며 전동기의 원리를 학습함. 물과 글리세린으로 각각 채워진 반원통에 입사각에 따라 달라지는 굴절각을 측정한 후 스넬의 법칙에 적용하여 굴절률을 구하는 실험 활동을 수행함. 실험 중 팀원들과의 소통 과정에서 뛰어난 협업 능력과 리더십을 보임."""

#single_instruction_prompt.py
write_prompt = r"""당신은 [물리학I] 교사이며, 학생의 학교생활기록부 항목을 작성하는 전문가입니다. 당신에게는 학생의 학교생활 기록에 대한 정보와 함께 [물리학I] 과목의 성취기준 목록이 입력됩니다.

**입력 데이터:**
1.  **"학생 활동 및 교사 평가 자료" 원문 (input_data):** 학생의 [물리학I] 과목 관련 활동에 대한 상세한 설명, 교사의 관찰 내용 및 평가 등이 담긴 원본 자료입니다. 이 자료의 내용 수준과 구체성은 학생의 활동 참여도에 따라 매우 다양할 수 있습니다.
2.  **"1년동안 물리학I 활동 내용 소개 (activity)":** input_data에서 특정 활동 내용만을 별도로 추출하거나 요약한 정보일 수 있습니다. (input_data와 함께 종합적으로 판단하여 활용합니다.) 활동 내용이 거의 없는 경우 "없음" 등으로 표시될 수 있습니다.
3.  **"물리학I 성취기준 (archievement)":** [물리학I] 과목에서 학생이 학기/학년 동안 달성해야 하는 학습 목표 목록입니다. 이 정보는 특히 원문 데이터가 부족할 때 내용을 구성하는 기반으로 활용됩니다.
4.  **생활기록부 예시 (one_shot):** 생활기록부의 일반적인 형식과 문체를 보여주는 예시입니다. **반드시 이 예시의 전체적인 구조, 흐름, 문체, 종결형식 등을 면밀히 참고하여 동일한 형식으로 작성**하세요.

**작성 목표:**
입력된 "학생 활동 및 교사 평가 자료" 원문(`input_data`) 및 `activity`의 **내용 수준과 구체성을 스스로 파악**하여 데이터의 전반적인 품질 수준(good, normal, bad, nothing)을 판단합니다. 판단된 품질에 따라 학생의 [물리학I] 과목에서의 학습 활동, 성취, 태도 및 성장을 객관적이고 설득력 있게 기술하는 생활기록부 항목을 작성합니다. `archievement` 정보는 원문 데이터가 부족한 'bad' 및 'nothing' 품질 수준에서 내용을 구성하는 주요 기반으로 활용됩니다.

**작성 지침:**

1.  **데이터 품질 파악:** 입력된 `input_data` 및 `activity`의 내용, 길이, 구체성, 긍정적인 정보 포함 여부 등을 종합적으로 분석하여 데이터의 전반적인 품질 수준(good, normal, bad, nothing 중 가장 가까운 것)을 스스로 판단합니다.
2.  **내용 구성:** 파악된 데이터 품질 수준에 맞는 작성 목표에 따라 내용을 구성합니다.
    *   **'good' 품질:** `input_data`와 `activity`에서 학생의 **뛰어난 역량, 심도 깊은 학업 성취, 구체적인 성과** 등을 보여주는 내용을 중심으로 풍성하게 기술합니다. `archievement`는 학생의 성취가 해당 성취기준을 **크게 상회하거나 심화했음**을 간접적으로 보여주는 배경 정보로 활용할 수 있습니다.
    *   **'normal' 품질:** `input_data`와 `activity`에서 학생의 **활동 참여 사실, 노력한 부분, 기본적인 이해, 수업 태도** 등을 중심으로 기술합니다. `archievement`는 학생이 **이러한 성취기준과 관련된 학습 활동에 참여했음**을 서술하는 정도로 활용할 수 있습니다. 깊이 있는 내용이나 뛰어난 성과를 과장하지 않습니다.
    *   **'bad' 품질:** `input_data`와 `activity`에서 파악되는 **극히 일부 정보**에, **`archievement` 목록을 주요 내용 구성의 기반**으로 삼아 내용을 확장합니다. 학생이 **이러한 성취기준과 관련된 학습 내용에 대해 배우거나 탐색하는 과정을 거쳤음**을 교사의 관점에서 관찰 가능한 '학습 과정 참여' 또는 '노력의 흔적' 위주로 기술합니다. 학생의 실제 성취 수준에 대한 과장 없이 사실에 기반하여 작성합니다.
    *   **'nothing' 품질:** 활동 내용(`activity`가 "없음" 등)에 대한 구체적인 언급 없이, **`archievement` 목록을 참고하여 [물리학I] 과목의 전반적인 수업 참여나 학습 태도**를 교사의 일반적인 관찰 관점에서 간결하게 기술합니다. 성취기준과 관련된 일반적인 학습 과정을 언급하는 방식으로 내용을 구성합니다.
3.  **문체 및 형식:**
    *   **반드시 ‘~~임’, ‘~~함.’과 같은 명사형 종결을 사용합니다.**
    *   **교사의 객관적이고 사실적인 관찰 및 평가 관점에서 작성합니다.** 학생의 주관적인 감정이나 자기 평가를 직접 서술하지 않고, **교사가 관찰하거나 확인할 수 있는 구체적인 행동, 성취, 변화된 태도, 드러난 역량** 등을 기술합니다.
    *   학교생활기록부 형식에 맞는 전문적이고 간결한 문체를 사용하되, 'good' 품질인 경우 학생의 우수성을 더욱 부각하는 긍정적인 톤을 사용합니다. 제공된 `one_shot` 예시의 형식을 엄격히 준수합니다.
4.  **길이:** 파악된 데이터 품질 수준에 따라 다음 글자 수 내외로 완성합니다.
    *   'good', 'normal', 'bad' 품질: **700글자 내외**
    *   'nothing' 품질: **300글자 내외**
5.  **언어:** 반드시 한글로 출력합니다.
6.  **결과물:** 완성된 생활기록부 항목은 하나의 연속된 문단 형태로 제시합니다.

**★★ 중요 주의사항 ★★**

*   **다음과 같은 '학생 스스로를 평가하는 듯한' 문구는 사용하지 마십시오.** 학교생활기록부는 교사의 객관적인 관찰 기록입니다.
    *   **예시:** ~을 알아봄, 이해함, 알게됨, 익힘, 계기가 됨, 자신의 능력에 대한 믿음이 생김, 큰 도움이 됨, 흥미를 느낌, 중요성을 깨달음 등 학생의 주관적인 감정, 자기 평가, 내면의 변화를 직접 서술하는 표현
*   **대신,** 교사의 관점에서 학생의 **구체적인 행동, 노력, 성과, 변화된 태도, 드러난 역량** 등을 객관적으로 묘사하는 방식으로 표현합니다.
*   'bad' 및 'nothing' 품질의 데이터로 작성 시, **`archievement` 내용을 학생의 '달성 성과'로 직접 서술하는 것을 엄격히 금지합니다.** 반드시 교사가 관찰한 '학습 과정 참여', '노력의 흔적', '관련 내용 학습 시도', 또는 일반적인 '수업 참여/태도' 등 객관적인 관찰 가능한 사실에 기반하여 기술합니다. **`archievement`는 내용을 채우는 기반일 뿐, 학생의 구체적인 성취 수준을 과장하는 도구가 아닙니다.**

**입력 데이터(`input_data`, `activity`)의 내용을 면밀히 분석하여 데이터 품질을 스스로 판단하고, 판단된 품질에 맞는 내용 구성 및 길이 지침을 따르며, `archievement`를 필요에 따라 올바른 방식(특히 bad/nothing에서 학습 과정 참여 위주)으로 활용하고, 교사의 객관적인 관점과 명사형 종결, 그리고 중요 주의사항(특히 금지 문구)을 엄격히 준수하여 제공된 one_shot 예시의 형식에 맞게 생활기록부 문체로 작성해주세요.**
"""

 

3. 메인 프로그램 코드(myApp.py)

이 코드는 DSPy 라이브러리를 사용하여 언어 모델(LLM)을 선택하고 구성하며, 선택된 LLM과 다양한 에이전트 모드를 활용하여 생활기록부를 작성하는 작업을 수행하는 스크립트이다.

import dspy
import os
import pandas as pd
from dspy import Example
from dotenv import load_dotenv
from openpyxl import Workbook, load_workbook
from tqdm import tqdm
from SR_Agent import Provider
from SR_Agent import SingleWriter, DualWriter, TripleWriter, QuadWriter

# API 키 불러오기
load_dotenv()
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL")

def Select_provider():
    """
    사용자 입력을 받아 DSPy에 사용할 LLM 공급자를 설정합니다.
    설정된 LLM 인스턴스를 생성할 수 있는 Provider 객체를 반환합니다.
    (주의: 캐싱 문제 회피를 위해 실제 LLM 인스턴스 생성 및 dspy.settings 설정은
     SR_main_loop 내에서 각 호출마다 수행됩니다.)
    """
    while True:
        provider_name = input("공급자를 openai, gemini, ollama 중 하나로 입력해 주세요: ").lower()
        if provider_name in ['openai', 'gemini', 'ollama']:
            break
        else:
            print("잘못된 입력입니다. 'openai', 'gemini', 'ollama' 중 하나를 입력해주세요.")

    if provider_name == 'ollama':
        # Ollama는 API 키 대신 base_url을 사용합니다.
        my_provider = Provider.Set_providers(
            provider_name=provider_name,
            gemini_api_key=None,
            openai_api_key=None,
            ollama_base_url=OLLAMA_BASE_URL,
            cache='False' # 캐시 비활성화 설정
        )
    elif provider_name == 'gemini':
        my_provider = Provider.Set_providers(
            provider_name=provider_name,
            gemini_api_key=GEMINI_API_KEY,
            openai_api_key=None,
            ollama_base_url=None,
            cache='False' # 캐시 비활성화 설정
        )
    elif provider_name == 'openai':
        my_provider = Provider.Set_providers(
            provider_name=provider_name,
            gemini_api_key=None,
            openai_api_key=OPENAI_API_KEY,
            ollama_base_url=None,
            cache='False' # 캐시 비활성화 설정
        )
    else:
        # 이 부분은 위의 while True 루프 때문에 실행될 가능성이 낮지만, 방어적으로 추가합니다.
        raise ValueError(f"지원하지 않는 공급자 이름: {provider_name}")

    print(f"DSPy LLM 공급자 설정 객체가 '{provider_name}'으로 준비되었습니다.")
    # 초기 dspy.settings 설정 (Testing 함수를 위해 필요)
    # 실제 작업 시에는 SR_main_loop 내에서 매번 재설정됩니다.
    dspy.settings.configure(lm=my_provider())
    return my_provider # Provider 객체 자체를 반환

def Testing():
    """
    설정된 LLM이 정상적으로 작동하는지 간단한 질문으로 테스트합니다.
    (dspy.settings에 설정된 현재 LLM 인스턴스를 사용합니다.)
    """
    print(f'\n--- LLM 연결 테스트 ---')
    test_question = "하늘은 무슨 색깔인가요?"
    print(f'테스트 질문: {test_question}')
    try:
        # dspy.settings에 설정된 기본 LM을 사용합니다.
        test_answer_predictor = dspy.Predict('question -> answer')
        test_result = test_answer_predictor(question=test_question)
        print(f'테스트 답변: {test_result.answer}')
        print(f'--- 테스트 완료 ---\n')
    except Exception as e:
        print(f"LLM 테스트 중 오류 발생: {e}")
        print("LLM 설정 또는 API 키를 확인해주세요.")


def Select_mode():
    """
    사용자 입력을 받아 생활기록부 작성에 사용할 에이전트 모드를 선택합니다.
    선택된 에이전트 인스턴스를 반환합니다.
    """
    while True:
        mode = input("생활기록부 작성 모델을 single, dual, triple, quad 중 하나로 입력해 주세요: ").lower()
        if mode == 'single':
            my_agent = SingleWriter.Single_Writer()
            break
        elif mode == 'dual':
            my_agent = DualWriter.Dual_Writer()
            break
        elif mode == 'triple':
            my_agent = TripleWriter.Triple_Writer()
            break
        elif mode == 'quad':
            my_agent = QuadWriter.Quad_Writer()
            break
        else:
            print("잘못된 입력입니다. 'single', 'dual', 'triple', 'quad' 중 하나를 입력해주세요.")
    print(f"생활기록부 작성 모드가 '{mode}'으로 설정되었습니다.")
    return my_agent

def SR_main_loop(provider_factory, agent):
    """
    사용자 입력 모드(엑셀 또는 단일 입력)에 따라 생활기록부 처리를 진행합니다.
    엑셀 모드: 지정된 엑셀 파일에서 데이터를 읽어와 처리하고 결과를 새 엑셀 파일에 저장합니다.
    단일 모드: 사용자로부터 직접 입력을 받아 처리하고 결과를 출력합니다.
    각 LLM 호출 전에 새로운 LLM 인스턴스를 생성하여 캐싱 문제를 회피합니다.
    """
    while True:
        excel_status = input("엑셀로 한번에 입력하려면 excel_mode, 한 개씩 입력하려면 one_mode라고 입력해 주세요: ").lower()
        if excel_status in ['excel_mode', 'one_mode']:
            break
        else:
            print("잘못된 입력입니다. 'excel_mode' 또는 'one_mode'를 입력해주세요.")

    if excel_status == 'excel_mode':
        data_path = 'student_record.xlsx'
        try:
            df = pd.read_excel(data_path, engine='openpyxl')
            print(f"'{data_path}' 파일 로드 성공. 총 {len(df)}개의 데이터를 처리합니다.")
        except FileNotFoundError:
            print(f"오류: '{data_path}' 파일을 찾을 수 없습니다. 파일을 확인하고 다시 실행해주세요.")
            return # 파일이 없으면 함수 종료
        except Exception as e:
            print(f"오류: 엑셀 파일을 읽는 중 오류 발생 - {e}")
            return # 읽기 오류 발생 시 함수 종료

        file_name = 'result.xlsx'

        # 결과 엑셀 파일 준비
        if not os.path.exists(file_name):
            workbook = Workbook()
            sheet = workbook.active
            sheet.title = '생기부 생성 결과'
            sheet.append(['순서', '학번', '이름', '결과'])
            workbook.save(file_name)
            workbook.close()
            print(f"결과를 기록하는 엑셀파일이 없어 '{file_name}' 파일을 생성합니다.")

        # 엑셀 파일에 결과를 추가하기 위해 다시 로드
        try:
            workbook = load_workbook(file_name)
            sheet = workbook.active
        except Exception as e:
            print(f"오류: 결과 엑셀 파일을 로드하는 중 오류 발생 - {e}")
            return # 로드 오류 발생 시 함수 종료

        results_to_save = [] # 결과를 모아 한 번에 저장하기 위한 리스트

        # 데이터프레임의 각 행을 순회하며 처리
        # tqdm을 사용하여 진행 상태 표시
        for index, row in tqdm(df.iterrows(), total=len(df), desc="생기부 처리 중"):
            try:
                detail = row['학생보고서']
                순서 = row['순서']
                학번 = row['학번']
                이름 = row['이름']

                # 사용자 피드백 반영: 각 LLM 호출 전에 새로운 인스턴스 생성 및 설정
                # 이렇게 하면 동일 입력에 대한 캐싱 문제를 회피할 수 있습니다.
                configured_lm = provider_factory()
                dspy.settings.configure(lm=configured_lm)

                # DSPy Example 객체 생성 및 에이전트 실행
                student_data = Example(input_data=detail).with_inputs("input_data")
                result = agent(**student_data.inputs())

                # 처리 결과를 리스트에 추가
                results_to_save.append([순서, 학번, 이름, result])

            except Exception as e:
                print(f"\n오류: 생기부 처리 중 오류 발생 (순서: {순서}, 이름: {이름}) - {e}")
                # 오류 발생 시에도 결과 파일에 기록할 수 있도록 오류 메시지를 추가
                results_to_save.append([순서, 학번, 이름, f"처리 오류: {e}"])

        # 모아둔 결과를 엑셀 파일에 일괄 추가
        for row_data in results_to_save:
            sheet.append(row_data)

        # 엑셀 파일 저장
        try:
            workbook.save(file_name)
            workbook.close()
            print(f"'{file_name}' 파일에 결과 저장 완료.")
        except Exception as e:
            print(f"\n오류: 결과 엑셀 파일을 저장하는 중 오류 발생 - {e}")

        print("엑셀 생기부 처리 모드 종료.")

    elif excel_status == 'one_mode':
        # 단일 입력 모드는 무한 루프로 사용자 입력을 받습니다.
        print("\n단일 생기부 처리 모드입니다. 종료하려면 'quit'을 입력하세요.")
        while True:
            input_data = input("생활기록부를 입력해주세요: ")
            if input_data.lower() == 'quit':
                break # 'quit' 입력 시 루프 종료
            if not input_data.strip():
                print("입력이 없습니다. 다시 입력해주세요.")
                continue

            try:
                # 사용자 피드백 반영: 각 LLM 호출 전에 새로운 인스턴스 생성 및 설정
                # 이렇게 하면 동일 입력에 대한 캐싱 문제를 회피할 수 있습니다.
                configured_lm = provider_factory()
                dspy.settings.configure(lm=configured_lm)

                # 에이전트 실행
                result = agent(input_data)
                print("\n--- 결과 ---")
                print(result)
                print("------------\n")
            except Exception as e:
                print(f"오류: 생기부 처리 중 오류 발생 - {e}")

        print("단일 생기부 처리 모드 종료.")


if __name__ == '__main__':
    # 생기부 처리 시작

    # 1. LLM 공급자 선택 및 Provider 객체 생성
    # 이 객체는 LLM 인스턴스를 생성하는 팩토리 역할을 합니다.
    provider_factory_instance = Select_provider()

    # 2. LLM 연결 테스트 (Select_provider에서 설정된 초기 LM 인스턴스 사용)
    Testing()

    # 3. 생활기록부 작성 모드 선택
    selected_agent_instance = Select_mode()

    # 4. 메인 처리 루프 실행 (엑셀 또는 단일 입력)
    # provider_factory 객체를 전달하여 루프 내에서 매번 새로운 LLM 인스턴스를 생성하도록 합니다.
    SR_main_loop(provider_factory_instance, selected_agent_instance)

    print("프로그램이 종료되었습니다.")

 

1. 임포트 (Imports)

  • import dspy: DSPy 라이브러리를 가져온다. 이는 언어 모델 파이프라인을 구축하는 프레임워크이다.
  • import os: 운영체제와 상호작용하는 표준 라이브러리이다. 환경 변수를 읽는 데 사용된다.
  • import pandas as pd: 데이터 분석 및 조작 라이브러리인 Pandas를 가져온다. 주로 Excel 파일을 읽는 데 사용된다.
  • from dspy import Example: DSPy에서 입력/출력 데이터를 구조화하는 데 사용되는 Example 클래스를 가져온다.
  • from dotenv import load_dotenv: .env 파일에서 환경 변수를 로드하는 함수를 가져온다. API 키와 같은 민감한 정보를 관리하는 데 유용하다.
  • from openpyxl import Workbook, load_workbook: .xlsx 확장자를 가진 Excel 파일을 읽고 쓰는 데 사용되는 openpyxl 라이브러리의 클래스들을 가져온다.
  • from tqdm import tqdm: 반복문의 진행 상태를 프로그레스 바로 시각화하는 데 사용되는 라이브러리이다.
  • from SR_Agent import Provider: SR_Agent라는 로컬 패키지(또는 모듈)에서 Provider 모듈을 가져온다.
  • from SR_Agent import SingleWriter, DualWriter, TripleWriter, QuadWriter: SR_Agent 패키지에서 다양한 생활기록부 작성 에이전트를 나타내는 클래스들을 가져온다. SingleWriter 등은 각기 다른 전략으로 생활기록부를 작성하는 DSPy Module이다.

2. 환경 변수 로딩

  • load_dotenv(): 현재 디렉터리 또는 상위 디렉터리의 .env 파일을 찾아 환경 변수로 로드한다.
  • GEMINI_API_KEY = os.getenv("GEMINI_API_KEY"): 환경 변수에서 GEMINI_API_KEY 값을 가져와 변수에 저장한다.
  • OPENAI_API_KEY = os.getenv("OPENAI_API_KEY"): 환경 변수에서 OPENAI_API_KEY 값을 가져와 변수에 저장한다.
  • OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL"): 환경 변수에서 OLLAMA_BASE_URL 값을 가져와 변수에 저장한다.

3. Select_provider() 함수

  • 이 함수는 사용자 입력을 받아 DSPy에서 사용할 LLM 공급자(OpenAI, Gemini, Ollama)를 선택하고 설정하는 역할을 한다.
  • while True 루프를 사용하여 유효한 공급자 이름을 입력받을 때까지 반복한다.
  • 사용자로부터 입력받은 공급자 이름에 따라, 이전에 임포트한 Provider.Set_providers 클래스를 사용하여 해당 공급자에 맞는 설정으로 인스턴스를 생성한다. 이때, 로드된 API 키나 Ollama URL을 사용한다. 다른 공급자의 키는 None으로 설정한다.
  • my_provider() 호출은 Set_providers 클래스의 __call__ 메서드를 실행하며, 이는 내부적으로 DSPy 설정을 완료하고 구성된 LLM 인스턴스를 반환한다.
  • dspy.settings.configure(lm=configured_lm): 반환된 LLM 인스턴스를 DSPy의 기본 LLM으로 전역 설정한다. 이제 DSPy 모듈들은 별도로 지정하지 않는 한 이 LLM을 사용하게 된다.
  • 설정 성공 메시지를 출력하고, 구성된 LLM 인스턴스를 반환한다.

4. Testing(lm) 함수

  • 이 함수는 Select_provider 함수에서 설정된 LLM이 제대로 작동하는지 간단한 질문으로 테스트하는 역할을 한다.
  • 테스트 질문으로 "하늘은 무슨 색깔인가요?"를 사용한다.
  • try...except 블록으로 감싸서 테스트 중 발생할 수 있는 오류를 처리한다.
  • dspy.Predict('question -> answer'): 간단한 입력('question')과 출력('answer')을 가지는 DSPy 예측 모델을 생성한다. 이 모델은 dspy.settings에 설정된 기본 LLM을 사용한다.
  • test_answer_predictor(question=test_question): 생성된 예측 모델을 테스트 질문으로 호출하고 응답을 받는다.
  • test_result.answer: 응답 객체에서 'answer' 필드 값을 추출하여 출력한다.
  • 테스트 성공/실패 메시지를 출력한다.

5. Select_mode() 함수

  • 이 함수는 사용자 입력을 받아 생활기록부 작성에 사용할 에이전트 모드(single, dual, triple, quad)를 선택하는 역할을 한다.
  • while True 루프를 사용하여 유효한 모드 이름을 입력받을 때까지 반복한다.
  • 사용자로부터 입력받은 모드 이름에 따라, 임포트한 SingleWriter.Single_Writer, DualWriter.Dual_Writer, TripleWriter.Triple_Writer, QuadWriter.Quad_Writer 클래스 중 하나를 선택하여 인스턴스를 생성한다.
  • 선택된 모드 설정 메시지를 출력하고, 생성된 에이전트 인스턴스를 반환한다. 이 에이전트 인스턴스는 DSPy 모듈이며, 내부적으로 dspy.settings에 설정된 LLM을 사용하여 작업을 수행한다.

6. SR_main_loop(agent) 함수

  • 이 함수는 프로그램의 핵심 로직을 포함하며, 사용자 입력 모드(Excel 파일 처리 또는 단일 입력 처리)에 따라 생활기록부 작성을 실행하는 역할을 한다.
  • while True 루프를 사용하여 엑셀 처리 모드('excel_mode') 또는 단일 입력 모드('one_mode') 중 하나를 유효하게 입력받을 때까지 반복한다.
  • excel_mode 선택 시:
    • 미리 정의된 입력 엑셀 파일 경로('student_record.xlsx')에서 Pandas를 사용하여 데이터를 읽어온다. 파일이 없거나 읽기 오류 발생 시 에러 메시지를 출력하고 함수를 종료한다.
    • 결과를 저장할 엑셀 파일 경로('result.xlsx')를 지정한다.
    • 결과 파일이 존재하지 않으면 openpyxl을 사용하여 새로 생성하고 헤더 행을 추가한다.
    • 결과 파일을 openpyxl로 다시 로드하여 결과를 추가할 준비를 한다.
    • tqdm과 함께 Pandas 데이터프레임의 각 행을 순회하며 생활기록부 데이터를 하나씩 처리한다.
    • 각 행에서 '학생보고서', '순서', '학번', '이름' 데이터를 추출한다.
    • dspy.Example 객체를 생성하여 '학생보고서' 내용을 input_data로 설정한다. 이는 에이전트 모듈의 입력 형식에 맞추기 위함이다.
    • agent(**student_data.inputs()): 전달받은 에이전트 인스턴스를 호출하여 생활기록부 작성을 실행한다. 에이전트는 내부적으로 dspy.settings에 설정된 LLM을 사용한다. 호출 결과는 작성된 생활기록부 내용이다.
    • 처리된 결과(순서, 학번, 이름, 작성된 생기부)를 리스트에 추가한다. 각 처리 과정 중 오류 발생 시에도 try-except 블록으로 감싸서 오류 메시지를 결과 리스트에 추가하고 다음 데이터로 넘어간다.
    • 모든 데이터 처리가 끝나면, 모아둔 결과를 openpyxl을 사용하여 결과 엑셀 파일에 일괄 추가한다.
    • 결과 엑셀 파일을 저장하고 닫는다. 저장 중 오류 발생 시 메시지를 출력한다.
    • 엑셀 처리 모드 종료 메시지를 출력한다.
  • one_mode 선택 시:
    • 단일 입력 모드 시작 메시지를 출력하고, 무한 루프에 진입한다.
    • 사용자에게 생활기록부 내용을 직접 입력받는다.
    • 'quit'을 입력받으면 루프를 종료하고 단일 처리 모드를 종료한다.
    • 입력이 비어있으면 다시 입력을 요청한다.
    • agent(input_data): 입력받은 내용을 인자로 전달받은 에이전트 인스턴스를 호출하여 생활기록부 작성을 실행한다.
    • 작성된 결과를 콘솔에 출력한다. 처리 중 오류 발생 시 에러 메시지를 출력한다.
    • 루프 종료 후 단일 처리 모드 종료 메시지를 출력한다.

7. 메인 실행 블록 (if __name__ == '__main__':)

  • 이 코드는 스크립트가 직접 실행될 때만 수행되는 부분이다.
  • 스크립트의 전체 실행 흐름을 정의한다.
    • Select_provider() 함수를 호출하여 LLM 공급자를 선택하고 DSPy 설정을 완료한다. 설정된 LLM 인스턴스를 반환받는다.
    • Testing() 함수를 호출하여 설정된 LLM이 제대로 작동하는지 간단히 테스트한다.
    • Select_mode() 함수를 호출하여 생활기록부 작성 에이전트 모드를 선택한다. 선택된 에이전트 인스턴스를 반환받는다.
    • SR_main_loop() 함수를 호출하여 실제 생활기록부 작성 메인 루프(Excel 또는 단일 입력 모드)를 시작한다. 이때 위에서 선택된 에이전트 인스턴스를 전달한다.
    • 모든 과정이 완료되면 "프로그램이 종료되었습니다." 메시지를 출력한다.

 

 

4. 실행결과

공급자를 openai, gemini, ollama 중 하나로 입력해 주세요: gemini
공급자를 사용하여 DSPy를 구성하려고 시도 중: gemini
Gemini LM 생성됨: model='gemini-1.5-flash-latest'
DSPy가 gemini로 성공적으로 구성되었습니다.
DSPy LLM 공급자가 'gemini'으로 설정되었습니다.

--- LLM 연결 테스트 ---
테스트 질문: 하늘은 무슨 색깔인가요?
테스트 답변: 하늘은 일반적으로 파란색이지만, 시간과 날씨에 따라 다양한 색깔을 띨 수 있습니다.  일출이나 일몰 때는 붉은색이나 주황색으로 보일 수도 있고, 흐린 
--- 테스트 완료 ---

생활기록부 작성 모델을 single, dual, triple, quad 중 하나로 입력해 주세요: single
작성자(Writer) 단독 모델 선택되었습니다.
생활기록부 작성 모드가 'single'으로 설정되었습니다.
엑셀로 한번에 입력하려면 excel_mode, 한 개씩 입력하려면 one_mode라고 입력해 주세요: excel_mode
'student_record.xlsx' 파일 로드 성공. 총 4개의 데이터를 처리합니다.
결과를 기록하는 엑셀파일이 없어 'result.xlsx' 파일을 생성합니다.
생기부 처리 중:   0%|                                                                                                                                        -----생활기록부 작성중-----
------생활기록부 작성 완료-----
생기부 처리 중:  25%|█████████████████████████████████████████████████████████████████                                                                       ------생활기록부 작성 완료-----
생기부 처리 중:  75%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████-----생활기록부 작성중-----
------생활기록부 작성 완료-----
생기부 처리 중: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
'result.xlsx' 파일에 결과 저장 완료.
엑셀 생기부 처리 모드 종료.
프로그램이 종료되었습니다.

 

학생활동이 기록된 student_record.xlsx 

 

 

작성된 생기부 result.xlsx

google gemma3 12b q4 버전을 이용해서 그런지 가끔 영어가 출력된다. 물론 gemini나 openai를 이용하면 정상적으로 출력된다.