# Part 7.3: LLM 애플리케이션 개발 실습 (LangChain)

# 이 스크립트를 실행하기 전에 라이브러리를 설치해야 합니다.
# pip install langchain langchain_community faiss-cpu sentence-transformers

import os
from typing import Any, Dict, List, Optional

from langchain.chains import LLMChain

# --- 모의(Mock) 객체 ---
# 실제 환경에서는 HuggingFaceEmbeddings, ChatOllama 등 실제 모델을 사용해야 합니다.
# 여기서는 API 키나 모델 다운로드 없이 실행 가능하도록 간단한 모의 객체를 사용합니다.
from langchain.embeddings.base import Embeddings
from langchain.llms.base import LLM
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import PromptTemplate


class MockEmbeddings(Embeddings):
    """간단한 모의 임베딩 클래스"""

    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        # 각 텍스트의 길이를 기반으로 간단한 벡터 생성
        return [[len(text) / 10] for text in texts]

    def embed_query(self, text: str) -> List[float]:
        # 쿼리의 길이를 기반으로 간단한 벡터 생성
        return [len(text) / 10]


class MockLLM(LLM):
    """간단한 모의 LLM 클래스"""

    @property
    def _llm_type(self) -> str:
        return "mock"

    def _call(
        self, prompt: str, stop: Optional[List[str]] = None, **kwargs: Any
    ) -> str:
        return f"모의 LLM이 응답합니다: '{prompt}'라는 질문을 받았습니다."

    @property
    def _identifying_params(self) -> Dict[str, Any]:
        return {"name": "mock_llm"}


# --- 1. 문서 준비 및 로드 ---
print("--- 1. 문서 준비 및 로드 ---")
# 실습을 위한 간단한 텍스트 파일 생성
doc_content = """
인공지능(AI)은 인간의 학습능력, 추론능력, 지각능력을 인공적으로 구현하려는 컴퓨터 과학의 한 분야입니다.
머신러닝은 AI의 하위 분야로, 기계가 데이터로부터 학습하여 스스로 성능을 향상시키는 기술을 의미합니다.
딥러닝은 머신러닝의 한 종류로, 인공신경망(Artificial Neural Network)을 기반으로 합니다.
최근에는 거대 언어 모델(LLM)이 큰 주목을 받고 있으며, 이는 방대한 텍스트 데이터로 사전 학습된 모델입니다.
RAG(Retrieval-Augmented Generation)는 LLM이 외부 지식 베이스를 참조하여 답변을 생성하는 기술입니다.
"""
doc_path = "sample_document.txt"
with open(doc_path, "w", encoding="utf-8") as f:
    f.write(doc_content)

# TextLoader로 문서 로드
loader = TextLoader(doc_path, encoding="utf-8")
documents = loader.load()
print("문서 로드 완료.")
# print(documents)
print("-" * 30)

# --- 2. 문서 분할 (Splitting) ---
print("\n--- 2. 문서 분할 ---")
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=20)
docs = text_splitter.split_documents(documents)
print(f"{len(docs)}개의 조각(chunk)으로 분할되었습니다.")
# print(docs[0])
print("-" * 30)


# --- 3. 임베딩 및 벡터 저장소 생성 (Embedding & Vector Store) ---
print("\n--- 3. 임베딩 및 벡터 저장소 생성 ---")
# 실제 환경: from langchain_community.embeddings import HuggingFaceEmbeddings
# embeddings = HuggingFaceEmbeddings(model_name="jhgan/ko-sbert-nli")
embeddings = MockEmbeddings()
print(f"사용한 임베딩 모델: {type(embeddings).__name__}")

# FAISS 벡터 저장소에 문서 임베딩하여 저장
# from_documents: 문서 목록을 받아 임베딩을 수행하고 인덱스를 빌드
vectorstore = FAISS.from_documents(docs, embeddings)
print("FAISS 벡터 저장소 생성 완료.")
print("-" * 30)


# --- 4. 검색 (Retrieval) ---
print("\n--- 4. 검색 ---")
query = "RAG 기술이란 무엇인가요?"
# similarity_search: 쿼리와 가장 유사한 문서를 벡터 공간에서 검색
retrieved_docs = vectorstore.similarity_search(query, k=1)
retrieved_text = retrieved_docs[0].page_content
print(f"쿼리: '{query}'")
print(f"검색된 문서: '{retrieved_text}'")
print("-" * 30)


# --- 5. 생성 (Generation) - LLM Chain 활용 ---
print("\n--- 5. 생성 (RAG) ---")
# 실제 환경: from langchain_community.chat_models import ChatOllama
# llm = ChatOllama(model="llama3")
llm = MockLLM()
print(f"사용한 LLM: {llm._identifying_params['name']}")

# 프롬프트 템플릿 정의
# 검색된 문서를 'context'로, 사용자의 질문을 'question'으로 받아 프롬프트를 구성
prompt_template = """
주어진 컨텍스트를 바탕으로 질문에 답변해주세요.

컨텍스트: {context}

질문: {question}

답변:
"""
prompt = PromptTemplate.from_template(prompt_template)

# LLMChain 생성
llm_chain = LLMChain(llm=llm, prompt=prompt)

# 검색된 문서와 원본 질문을 이용해 답변 생성
result = llm_chain.invoke({"context": retrieved_text, "question": query})

print("\n--- 최종 결과 ---")
print(f"질문: {result['question']}")
print(f"제공된 컨텍스트: {result['context']}")
print(f"생성된 답변: {result['text']}")
print("-" * 30)

# 임시 파일 삭제
os.remove(doc_path)
