2024년 12월 7일 토요일

벡터데이터베이스 기반 간단한 PDF, Web 기반 검색 전문 에이전트 만들기

이 글은 벡터데이터베이스 기반 간단한 PDF, Web 기반 검색 전문 에이전트 만드는 방법을 정리한 것이다. 이 글은 LangChain을 사용한다. lanchain은 급속히 버전과 함수가 개선되고 있어 API가 자주 변경되는 경향이 있다(Deprecated error). 이런 이유로, 현지 시점에서 관련 에이전트를 만드는 주요 코드를 남긴다. 
에이전트 예시(Build Ask Data app - Streamlit)
에이전트 예시(Local_RAG_Agents_Intel_cpu)

LangChain 설명 및 사용법은 앞의 블로그 내용(검색)을 참고한다. 다음 같은 순서로 LLM이 사용된다는 것만 이해하고 있으면 코드 사용에 큰 어려움은 없을 것이다. 

사용자 입력 > 컨텐츠 검색 입력 > 에이전트 도구에서 데이터 입력 > 과거 메모리 내용 입력 > 프롬프트 생성 > LLM 전달 > 추론 내용 출력

설치 
파이썬, 아나콘다가 준비되어 있다는 가정에서, 라이브러리 설치는 다음과 같다. 
pip install --upgrade openai langchain-openai langchain langchain-experimental

이 글은 OpenAI API Key, Tavily API Key를 신청해 가지고 있다고 가정한다. 실행 시 로그나 성능을 확인하고자 한다면, Langsmith에 가입한다. 각 API는 해당 웹사이트에서 가입하여 얻을 수 있다. 각 사이트에서 생성한 API키는 .env 파일을 생성해 다음과 같이 입력해 놓는다.
OPENAI_API_KEY=<OpenAI API Key>
LANGCHAIN_TRACING_V2=false
LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
LANGCHAIN_API_KEY=<Langsmith API Key>
LANGCHAIN_PROJECT=AGENT TUTORIAL

코딩
다음과 같이 코딩한다. 우선 필요한 라이브러리를 임포트한다.
import os, getpass
from openai import OpenAI  
from dotenv import load_dotenv
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.agents import create_openai_functions_agent
from langchain_community.document_loaders import PyPDFLoader
from langchain.tools.retriever import create_retriever_tool
from langchain.agents import AgentExecutor
from langchain_openai import ChatOpenAI
from langchain import hub

os.environ["TAVILY_API_KEY"] = "input your key"
os.environ["LANGCHAIN_PROJECT"] = "AGENT TUTORIAL"
load_dotenv()
client = OpenAI(api_key="input your key")

# Travily의 웹 검색 객체 획득
web_search = TavilySearchResults(k=5) 

# PDF 데이터를 벡터DB에 청크로 저장하고, 문서 검색 객체를 획득
loader = PyPDFLoader("./202212_LiDAR.pdf")  # 적절한 PDF 입력
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
split_docs = loader.load_and_split(text_splitter)

embeddings = OpenAIEmbeddings(api_key=os.environ["OPENAI_API_KEY"])
vector = FAISS.from_documents(split_docs, embeddings)
vectordb_retriever = vector.as_retriever()
output = vectordb_retriever.get_relevant_documents(
    "PCL(Point Cloud Library) 라이브러리에 대해 설명해줘"
)[0]
print(output)  # PCL 검색 예시

pdf_retriever_tool = create_retriever_tool(
    vectordb_retriever,
    name="pdf_search",
    description="2023년 12월 PCL(Point Cloud Library) 정보를 PDF 문서에서 검색합니다. '2023년 12월 라이다 포인트 클라우드 처리 기술' 과 관련된 질문은 이 도구를 사용해야 합니다!",
) # 문서 검색 객체

# 에이전트 도구들 설정
tools = [web_search, pdf_retriever_tool]

# LLM 객체 설정
llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0)

# 프롬프트 설정
prompt = hub.pull("hwchase17/openai-functions-agent")
print(prompt.messages)

# LLM 함수 호출 에이전트 설정
agent = create_openai_functions_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 전문가 에이전트 질문 수행
response = agent_executor.invoke(
    {
        "input": "2010년부터 PCL 라이브러리 기술에 대한 대한 내용을 PDF 문서에서 알려줘"
    }
)
print(f'답변: {response["output"]}')

우선 PDF에서 검색하라고 에이전트에게 명령했으므로, tools에 등록된 pdf vector database를 검색하는 tool을 실행한다. 다음은 이 결과의 예이다. 


> Finished chain.
답변: PCL(Point Cloud Library)은 BSD 라이선스로 개발되어 상업 및 연구 목적에 서 무료로 사용할 수 있습니다. 이 라이브러리는 크로스 플랫폼 개발을 지원하여 리눅스, 맥, 윈도우, 안드로이드 등 다양한 운영 체제에서 사용할 수 있습니다. PCL은 잘 모듈화되어 있어 배포가 용이하며, ROS(Robot Operating System), PDAL 등 유명한 오픈소스 프로젝트에서 직접 사용됩니다....

이제, web 검색 도구를 실행해 관련 내용을 추론하고, 기존 추론된 내용은 메모리를 이용해 같이 이용해 보도록 한다. 다음 코드를 입력해 본다. 
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
message_history = ChatMessageHistory()  # 메모리 사용

agent_with_chat_history = RunnableWithMessageHistory(
    agent_executor,
    lambda session_id: message_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

response = agent_with_chat_history.invoke(
    {
        "input": "2024년부터 PCL에 대한 새로운 내용을 인터넷 웹 문서에서 알려줘"
    },
    config={"configurable": {"session_id": "MyTestSessionID"}},
)
print(f"답변: {response['output']}")

이 코드를 실행하면, 다음과 같이 travily web search 도구를 이용해, 인터넷에서 해당 질문에 대한 검색을 수행하여, 결과를 LLM이 추론한다.


추론 결과는 다음과 같다.
> Finished chain.
답변: 2024년에 대한 Point Cloud Library (PCL)의 새로운 업데이트와 관련된 정보는 다음과 같습니다:

- **GitHub Pull Requests**: 2024년에는 PCL 개발에 몇 가지 주목할 만한 기여가 있었습니다. 예를 들어, 2024년 2월 10일에 larshg에 의해 열린 [#5958 (https://github.com/PointCloudLibrary/pcl/pulls)는 기능을 두 개의 라이브러리로 분할하 
는 작업에 대한 초안입니다. 또한, 2024년 1월 15일에 larshg에 의해 열린 [#5932 (https://github.com/PointCloudLibrary/pcl/pulls)는 관련된 또 다른 기여입니다.

- **공식 문서 및 웹사이트**: PCL은 대규모 오픈 프로젝트로, 포인트 클라우드 처리를 위한 다양한 최신 알고리즘을 포함하고 있습니다. 이러한 알고리즘에는 노이즈 데이터에서 이상치를 필터링하는 것과 같은 작업을 위한 필터링, 특징 추정, 표면 재구성, 등록, 모델 피팅 및 세분화가 포함됩니다. [공식 문서](http://pointclouds.org/documentation/index.html)와 [공식 웹사이트](https://pointclouds.org/)에서는 PCL에 대한 자세한 정보와 리소스를 제공합니다.

결론
langchain의 tool 에이전트 기능을 이용하면, 파일 벡터 검색, 웹 검색, 계산, 추론, 텍스트 및 차트 생성 등과 같은 기능을 쉽게 개발할 수 있다.

참고로, langchain과 연동되는 langsmith 사이트를 방문하면, 다음과 같이 얼마나 토큰을 사용했는 지 확인할 수 있다. 
LangSmith 로그 결과

레퍼런스
사용자 정의 체인을 위한 LCEL은 기본으로 Runnable 에서 파생되어 처리된다. 이는 다음 표준적인 인터페이스를 지원한다.
  • stream: 응답 청크를 스트리밍
  • invoke: 입력에 대한 체인을 실행 호출
  • batch: 입력 목록에 대해 체인들을 실행 호출
  • astream: 비동기적으로 응답 청크를 스트리밍
  • ainvoke: invoke의 비동기 버전
LCEL는 유닉스 파이프라인처럼 입력 | 처리 | 실행 | 출력 형태로 유연한 LLM 오케스트레이션을 지원한다.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI()
prompt = PromptTemplate.from_template("Explain about {topic} as one paragraph.")
chain = prompt | model | StrOutputParser()

다음 코드를 이용해 체인 그래프의 구조를 확인할 수 있다.
chain.get_graph().nodes
chain.get_graph().edges
chain.get_graph().print_ascii()

각 체인은 이터레이션(iteration)을 이용해 각 단계를 개별 관찰, 실행, 제어할 수 있도록 한다.

Runnable
runnable을 사용하면, 프롬프트 실행 중 동적으로 입력 단계에 참여할 수 있다. 이는 프롬프트에 데이터를 전달할 수 있다.
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough # RunnablePassthrough().invoke({"num": 10})
from langchain_core.runnables import RunnableLambda, RunnablePassthrough

prompt = PromptTemplate.from_template(
    "List top {n} famous people which have birthday {today}."
)
llm = ChatOpenAI(temperature=0, model_name="gpt-4o")

chain = (
    {"today": RunnableLambda(get_today), "n": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
print(chain.invoke({'n': 3}))

사용자 정의 람다를 지원하는 RunnableLambda, 입력에 따른 동적 라우팅 RunnableBranch, 병렬 처리 RunnableParall, 이전 내용 기억을 하는 RunnableWithMessageHistory 등이 있다. 

@chain을 사용하면, 알아서 RunnableLambda로 주어진 함수를 래핑한다.
@chain
def custom_chain(text):
    chain1 = prompt1 | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
    output1 = chain1.invoke({"topic": text})
    chain2 = prompt2 | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
    return chain2.invoke({"sentence": output1})

PydanticOutputParser
LLM 출력을 기계처리 가능한 구조화된 형태로 변환하는 파서이다. 다음은 그 예이다. 
class EmailSummary(BaseModel):
    person: str = Field(description="person who send email")
    email: str = Field(description="email address of sender")
    subject: str = Field(description="email title")
    summary: str = Field(description="email's summerized text")
    date: str = Field(description="meeting date and time in email")

parser = PydanticOutputParser(pydantic_object=EmailSummary)
print(parser.get_format_instructions())

이와 유사한, StructuredOutputParser, JsonOutputParser, PandasDataFrameOutputParser 등이 있다.

OutputFixingParser
출력 파싱 시 발생하는 오류를 자동으로 수정한다. 

LLM 모델 종류
lanchain이 지원하는 주요 LLM 모델은 openai, anthropic claude, cohere aya, facebook llama, microsoft phi, google gemini (ChatGoogleGenerativeAI), GPT4All 등이 있다. 이 중 몇몇 모델은 멀티모달을 지원한다.
gemini = ChatGoogleGenerativeAI(model="gemini-1.5-pro-latest")

system_prompt = (
    "You're writer. From given image, write short novel."
)

user_prompt = "Write short novel from the follow image."

multimodal_gemini = MultiModal(
    llm, system_prompt=system_prompt, user_prompt=user_prompt
)
IMAGE_URL = "house.jpg"
answer = multimodal_gemini.stream(IMAGE_URL)

Caching
이미 얻은 답변을 캐쉬처리 하여, LLM API 호출을 줄여주고, 실행 속도를 높여준다. InMemoryCache, SQLiteCache 등이 있다.

체인 직렬화
앞서 정의한 LLM 체인을 직렬화(langchain_core.load)하면, 개별로 파일 저장해, 필요 시 동적으로 로딩하여 사용할 수 있다. 

LLM 모델 로컬 실행
LLM 모델을 로컬 PC에서도 실행할 수 있다. 이 경우, 허깅페이스, Ollama를 이용한다. 
llm = HuggingFacePipeline.from_model_id(
    model_id="beomi/llama-2-ko-7b",  
    task="text-generation",  
    pipeline_kwargs={"max_new_tokens": 512},
)

llm = ChatOllama(model="EEVE-Korean-10.8B:latest")

문서 로더
LangChain은 매우 다양한 문서 종류를 지원한다. PDF, CSV, Excel, Word, Web, JSON, Arxiv 등을 지원한다.

문서 청크 분할 및 요약
문서를 LLM에 입력 가능하도록 청크로 분할하는 CharacterTextSplitter(/n 기준 분할), 재귀적 문서 분할, 토큰 제한 분할, 의미론적 분할, 코드 분할, HTML 분할, JSON 분할(RecursiveJsonSplitter) 등이 있다. 

문서를 요약하는 방법은 전체 문서 요약하는 Stuff, 분할 요약하는 Map reduce, 분할 요약 후 정리하는 Map refine 및 chain of density, clustering map refine 방법 등이 있다. 

임베딩 모델
문서의 청크를 검색 가능한 벡터 형태의 임베딩 벡터로 변환하는 모델을 말한다. 보통, OpenAIEmbedding, CacheBackedEmbeddings, HuggingFace Embeddings, OllamaEmbeddings, GPT4All 등이 사용된다. 

벡터 데이터베이스와 검색기
보통, Chroma, Vectordatabase, FAISS, Pinecone 등이 사용된다. 여기서 원하는 임베딩 벡터를 검색해 문서를 리턴하는 방법은 vectorstore 기반 similarity search, MMR, ContextualCompressionRetriever(압축 검색), ContextualCompressionRetriever(컨테텍스트 압축), LongContextReorder(긴 문맥 기록), ParentDocumentRetriever(계층적 문서 검색), MultiQueryRetriever(다중 질의 후 검색), SelfQueryRetriever 등이 있다.

이와 더불어, 재순위(Reranker)를 이용해, 원하는 답을 검색할 때까지 우선순위를 조정할 수 있다. Cross Encoder Reranker, Cohere reranker, Jina Reranker, FlashRank reranker 등이 있다.

메모리
채팅과 같이 앞의 입출력에 대한 맥락을 기억해야 할 때 사용한다. RunnableWithMessageHistory 등이 있다.

Fallback 오류 방지
만약 체인 실행 중 여러 이유로 에러가 발생할 경우, 다시 재 시도하는 방법이 폴백이다. with_fallbacks 함수를 사용한다.

도구와 에이전트
앞서 언급한 도구나 사전 정의된 에이전트를 이용하면, 멀티 에이전트를 만들 수 있다. 도구들 중에서는 파일관리 도구, 이미지 생성 도구, 보고서 작성 도구 등이 있다.
랭체인 무료 제공 도구 예

예를 들어, agent_toolkits에는 langchain 커뮤니티 생태계에서 업로드한 다양한 에이전트가 있다. 이 중에 엑셀 분석 에이전트, SQL agent인 create_sql_agent 등은 많이 사용된다.
랭체인 커뮤니티 제공 에이전트 예

이런 에이전트는 BaseToolkit, BaseTool, RunnableSerializable를 기본 클래스로 파생되어 개발할 수 있다. 이 클래스의 아키텍처는 컴포짓(Composite) 디자인 패턴으로 다음과 같다.
도구를 정의할 때 앞의 @chain 처럼 @tool 데코레이터를 사용할 수도 있다. description은 어떤 도구를 LLM이 선택할 지를 체인에서 결정하는 힌트로 동작한다. 예는 다음과 같다.
import re
import requests
from bs4 import BeautifulSoup
from langchain.agents import tool

@tool
def get_word_length(word: str) -> int:
    """Returns the length of a word."""
    return len(word)

@tool
def add_function(a: float, b: float) -> float:
    """Adds two numbers together."""
    return a + b

@tool
def naver_news_crawl(news_url: str) -> str:
    """Crawls a naver.com news article and returns the body content."""
    response = requests.get(news_url)
    if response.status_code == 200:
        soup = BeautifulSoup(response.text, "html.parser")
        title = soup.find("h2", id="title_area").get_text()
        content = soup.find("div", id="contents").get_text()
        cleaned_title = re.sub(r"\n{2,}", "\n", title)
        cleaned_content = re.sub(r"\n{2,}", "\n", content)
    else:
        print(f"HTTP fail code: {response.status_code}")
    return f"{cleaned_title}\n{cleaned_content}"

tools = [get_word_length, add_function, naver_news_crawl]

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)

LLM 평가
생성 결과에 대한 평가 방법은 자동 계산이 가능한 BLEU, ROUGE, METEOR, SemScore, 수동 평가, 작업 기반 평가, LLM 기반 평가 등이 있다. 평가용 라이브러리는 RAGAS, LangSmith, W&B 등이 있다.

평가를 위해, 테스트 데이터셋을 from ragas.testset.generator import TestsetGenerator 를 통해 자동 생성할 수 있다. 다만, 토큰 비용이 과대해 질 수 있는 문제가 발생할 수 있다. 이 경우, 오픈소스 LLM 모델을 사용할 수 있다.

2024년 10월 20일 일요일

가성비 있는 웹 서비스 호스팅 Fly.IO 사용하기

이 글은 가성비 있는 웹 서비스 호스팅이 가능한 Fly.IO 사용방법을 정리한다. 이 글은 다음 글의 후속편으로, DNA, 호스팅 등 개념은 이 글을 참고하길 바란다.

머리말
FLY.IO는 Kurt Mackey가 공동 설립한 회사로 개발되었다. 그는 이전에 Compose.io라는 데이터베이스 호스팅 플랫폼을 공동 창업했으며, 이를 IBM에 매각한 경험이 있다. FLY.IO는 주로 개발자 친화적인 글로벌 애플리케이션 호스팅 서비스를 제공하기 위해 설립되었다.

FLY.IO의 장단점은 다음과 같다.

장점
1. 글로벌 배포: 애플리케이션을 여러 지역에 쉽게 배포할 수 있어 지리적으로 분산된 사용자에게 낮은 지연 시간을 제공한다.
2. 간단한 사용성: 간단한 명령어로 애플리케이션을 배포할 수 있으며, 설정도 비교적 쉬운 편이다.
3. 자동 확장: 필요에 따라 인프라를 자동으로 확장할 수 있어, 트래픽이 급증할 때도 유연하게 대응 가능하다.
4. 애플리케이션 근접 배치: 사용자 근처에 애플리케이션을 배치할 수 있어 성능 향상과 지연 시간 감소에 효과적이다.
5. 무료 계층 제공: 제한적이지만 무료로 사용할 수 있는 계층을 제공해, 작은 프로젝트나 테스트에 유용하다. 평소에는 배포 앱이 비활성화되었다가 URL 접속하면 자동 활성화되는 Pay As You Go Plan (기존 Hobby Plan) 을 지원한다.

단점
1. 제한된 문서화: 다른 클라우드 서비스에 비해 문서화가 부족한 편이며, 일부 기능에 대한 정보가 불충분할 수 있다.
2. 복잡한 설정: 고급 기능을 사용할 때 설정이 복잡해질 수 있으며, 초보자에게는 어려울 수 있다.
3. 비교적 작은 생태계: AWS나 Google Cloud와 비교하면 생태계가 작아, 지원되는 서비스나 도구가 제한적이다.
4. 무료 계층 한계: 무료 계층의 자원이 제한적이므로, 트래픽이 많은 애플리케이션에는 적합하지 않다.
5. 서비스 안정성: 일부 사용자들은 특정 상황에서 예기치 않은 중단을 경험할 수 있다고 보고했다.

환경 설치
FLY.IO 사용법을 다음과 같다.

1. 회원가입 및 설치
FLY.IO 웹사이트(https://fly.io)에 접속하여 회원가입을 한다.

2. 파워셀에서 설치 스크립트 실행
pwsh -Command "iwr https://fly.io/install.ps1 -useb | iex"

3. 터미널에서 로그인 실행
fly auth login

로그인 결과는 다음과 같다.

4. Github 로그인함

소스코드에서 데모앱 빌드, 배포 및 실행
FLY.IO에서 제공하는 데모앱을 간단히 실행해 보자. 
다음 예제를 터미널에서 실행한다.
git clone https://github.com/fly-apps/hello-fly.git
cd hello-fly
코드 예시

앱을 배포하고 실행한다.
fly launch --now

그 결과, 도커를 자동 빌드한다. 다음은 이미지 스크립트이다. 
FROM node:16.19.0-slim

WORKDIR /usr/src/app
COPY package*.json ./

RUN npm install
COPY . .

EXPOSE 8080
CMD [ "npm", "start" ]

도커 이미지는 자동으로 FLY.IO 클라우드에 업로드되고, 설정파일대로 웹 인터페이스를 연결한다.
도커 이미지 생성 화면
Fly.IO의 웹 앱 배포된 모습

여기서 배포된 링크를 클릭한다.

결과는 다음과 같다. 웹 앱이 정상 서비스된 것을 확인할 수 있다.

이미 개발된 도커 이미지에서 데모앱 빌드, 배포 및 실행
Fly Launch를 사용해 도커 이미지로 앱을 배포할 수 있다. 미리 빌드된 Docker이미지를 이용해 데모앱을 만들고 배포해 본다. 

도커가 설치되었다는 가정 하에 다음 명령을 실행한다.
fly launch --image flyio/hellofly:latest
fly launch

이 결과 fly.toml 설정파일이 생성된다. 참고로, 웹 인터페이스는 다음과 같은 fly.toml 구성파일을 통해 설정된다. 
app = 'fly-io-delicate-surf-7133'
primary_region = 'nrt'

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1

앱을 배포한 후 상태를 확인한다.
앱 링크를 클릭해 오픈하거나 다음 명령을 이용해 방문한다.
fly apps open /fred

그럼, 다음 웹 페이지를 확인할 수 있다.

이외, 변경사항 배포는 fly deploy, 데모 앱 삭제는 fly apps destory 명령을 사용할 수 있다.
변경사항 배포 예

index 파일을 다음과 같이 수정한 후 fly deploy 명령을 실행한다.
배포에 성공하면, 다음과 같이 같은 DNS에서 변경된 서비스를 확인할 수 있다.

원격 터미널 접속
Fly.IO는 SSH 터미널 접속을 제공한다. 다음과 같이, SSH 키를 생성한다.
ssh-keygen -t rsa -b 4096 -C "your email@gmail.com"

그럼. id_rsa 암호키가 생성될 것이다.

원격 터미널에 접속한다. 도커 이미지의 리눅스가 접속된 것을 확인할 수 있다.
fly ssh console

결론
Kurt Mackey는 소프트웨어 엔지니어이자 창업가로, 클라우드 인프라와 개발자 도구 분야에서 주목받는 인물이다. 그는 여러 기술 회사에서 경력을 쌓아왔고, Compose.io 등 두 개의 성공적인 스타트업을 공동 창업했다. 2015년, IBM이 Compose.io를 인수하면서 Mackey는 IBM에서 기술 리더십 역할을 맡았다. 

Mackey는 오랜 시간 소프트웨어 엔지니어로 활동해왔으며, 주로 인프라 서비스와 클라우드 컴퓨팅에 대한 깊은 전문성을 보유하고 있다. 그의 작업 철학은 개발자가 인프라 관리에 신경 쓰지 않고도 효율적으로 작업할 수 있도록 도와주는 도구와 플랫폼을 제공하는 데 중점을 두고 있다.

Kurt Mackey는 기술 커뮤니티 내에서 활발하게 활동하며, 개발자 도구의 발전과 클라우드 기반 서비스의 미래에 대해 꾸준히 발언하고 있다. 그의 비전은 애플리케이션이 물리적 서버나 데이터 센터에 묶이지 않고, 사용자 근처에서 자동으로 최적화되고 배포될 수 있는 환경을 조성하는 것이다.


레퍼런스

2024년 10월 7일 월요일

Three.js 이용한 web3d 프로그램 개발하기

이 글은 웹 기반 3D 그래픽 라이브러리로 거의 표준적으로 사용되고 있는 three.js를 간략히 소개한다. 아울러, 이를 이용한 web3d 프로그램 개발하는 방법을 간략히 정리한다.
three.js 예제

개요
Three.js는 웹 브라우저에서 3D 그래픽을 쉽게 만들고 렌더링할 수 있도록 도와주는 자바스크립트 라이브러리다. WebGL을 기반으로 작동하며, 복잡한 3D 그래픽 처리를 단순화해준다.

Scene (장면)
장면은 Three.js에서 3D 객체들이 배치되는 공간이다. 카메라, 조명, 메쉬와 같은 객체들이 장면에 추가되며, 이 장면을 렌더링하여 화면에 표시하게 된다.

Camera (카메라)
카메라는 장면을 바라보는 시점을 결정하는 객체다. Three.js에서 일반적으로 많이 사용하는 카메라는 원근법을 적용하는 PerspectiveCamera와 직교 투영을 사용하는 OrthographicCamera가 있다.

Renderer (렌더러)
렌더러는 장면을 렌더링해 브라우저의 캔버스에 표시하는 역할을 한다. Three.js에서는 주로 WebGLRenderer를 사용하여 장면을 화면에 출력한다.

Mesh (메쉬)
메쉬는 3D 장면에서 실제로 보이는 객체를 나타낸다. Geometry(기하학적 형태)와 Material(재질)로 구성되며, Geometry는 객체의 모양을 정의하고, Material은 객체의 표면이 어떻게 보일지를 결정한다.

Light (조명)
조명은 장면 내 객체에 빛을 비추는 역할을 한다. 다양한 유형의 조명이 있으며, 예를 들어 PointLight는 특정 지점에서 모든 방향으로 빛을 발산하고, DirectionalLight는 태양처럼 일정한 방향으로 빛을 비춘다.

Three.js를 사용하면 웹에서 고성능 3D 그래픽과 애니메이션을 구현할 수 있으며, 비교적 간단한 코드로도 복잡한 장면을 만들 수 있다.

상세한 내용은 다음 튜토리얼을 참고한다.

개발 방법
이 예제는 간단한 3D Cube를 웹에 렌더링하는 프로그램이다. 
<html>
   <head>
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
      <title>3D viewer</title>
   </head>
   <body>
      <div id="model-view" style="width: 350px; height: 350px"></div>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/loaders/OBJLoader.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/loaders/MTLLoader.js"></script>  
      <script>         
         const model_container = document.getElementById('model-view');

         const model_scene = new THREE.Scene();
         const model_camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
         const model_renderer = new THREE.WebGLRenderer();
         model_renderer.setSize(model_container.clientWidth, model_container.clientHeight);
         model_renderer.setClearColor(0xADD8E6);  
         model_container.appendChild(model_renderer.domElement);

         const model_light = new THREE.PointLight(0xffffff, 1, 100);
         model_light.position.set(10, 1, 5);
         model_scene.add(model_light);

         function make_cube() {
            const geometry = new THREE.BoxGeometry();
            const material = new THREE.MeshBasicMaterial({ color: 0x00aa00, wireframe: true });
            const cube = new THREE.Mesh(geometry, material);
            model_scene.add(cube);
            return cube;
         }
         model_cube = make_cube();

         model_camera.position.set(2, 2, 2);
         model_camera.lookAt(0, 0, 0);

         function animate() {
            requestAnimationFrame(animate);

            model_cube.rotation.x += 0.01;
            model_cube.rotation.y += 0.01;
            model_renderer.render(model_scene, model_camera);
         }
         animate();

         window.addEventListener('resize', () => {
            model_camera.aspect = window.innerWidth / window.innerHeight;
            model_camera.updateProjectionMatrix();
            model_renderer.setSize(window.innerWidth, window.innerHeight);
         });         
      </script>
   </body>
</html>

실행하면, 간단한 3D cube 모델이 웹페이지에 렌더링 될 것이다.
마무리
Three.js는 Ricardo Cabello(Mr.doob)라는 개발자가 2010년에 처음 개발했다. Cabello는 원래 그래픽 디자이너이자 개발자로 활동했으며, WebGL을 더 쉽게 사용할 수 있는 라이브러리가 필요하다고 생각해 Three.js를 만들었다. 이후 Three.js는 오픈 소스로 공개되어 많은 개발자들의 기여로 지속적으로 발전해왔다.

레퍼런스

2024년 10월 6일 일요일

LangGraph 기반 데이터 분석 멀티 에이전트 만들기

이 글은 LangGraph 기반 데이터 분석 멀티 에이전트 만드는 방법을 간략히 설명한다.

LangChain Tool 기반 수학 에이전트 개발
다음 코드를 입력해 실행한다. 

from langchain_openai import OpenAI
from langchain_community.chat_models import ChatOllama
from langchain.chains import LLMMathChain, LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.utilities import WikipediaAPIWrapper
from langchain.agents.agent_types import AgentType
from langchain.agents import Tool, initialize_agent
from dotenv import load_dotenv
import chainlit as cl

load_dotenv()

@cl.on_chat_start
def math_chatbot():
llm = ChatOllama(model='llama3', temperature=0.0)

  # prompt for reasoning based tool
word_problem_template = """You are a reasoning agent tasked with solving t he user's logic-based questions. Logically arrive at the solution, and be factual. In your answers, clearly detail the steps involved and give the final answer. Provide the response in bullet points. Question  {question} Answer"""

math_assistant_prompt = PromptTemplate(
input_variables=["question"],
template=word_problem_template
)
  # chain for reasoning based tool
word_problem_chain = LLMChain(llm=llm,
  prompt=math_assistant_prompt)
# reasoning based tool                              
word_problem_tool = Tool.from_function(name="Reasoning Tool",
   func=word_problem_chain.run,
   description="Useful for when you need to answer logic-based/reasoning questions."
   )
  # calculator tool for arithmetics
problem_chain = LLMMathChain.from_llm(llm=llm)
math_tool = Tool.from_function(name="Calculator",
   func=problem_chain.run,
   description="Useful for when you need to answer numeric questions. This tool is only for math questions and nothing else. Only input math expressions, without text",
   )
  
  # Wikipedia Tool
wikipedia = WikipediaAPIWrapper()
wikipedia_tool = Tool(
name="Wikipedia",
func=wikipedia.run,
description="A useful tool for searching the Internet to find information on world events, issues, dates, "
"years, etc. Worth using for general topics. Use precise questions.",
)
  
  # agent
agent = initialize_agent(
tools=[wikipedia_tool, math_tool, word_problem_tool],
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=False,
handle_parsing_errors=True
)
cl.user_session.set("agent", agent)

@cl.on_message
async def process_user_query(message: cl.Message):
agent = cl.user_session.get("agent")

response = await agent.acall(message.content,
callbacks=[cl.AsyncLangchainCallbackHandler()])

await cl.Message(response["output"]).send()

결과는 다음과 같다.


레퍼런스

2024년 10월 4일 금요일

BIM IFC 파일을 Cesium 디지털트윈 플랫폼에 3D tiles로 가시화하는 방법과 구조

이 글은 BIM(Building Information Modeling) 포맷 중 하나인 IFC 파일을 Cesium 플랫폼에 3D tiles로 가시화하는 방법을 간략히 설명한다. 아울러, 마지막 부분에 3D tiles 개념과 구조를 간략히 나눔한다. Cesium에서 개발된 3D tiles는 3D 고속 렌더링을 위한 모델 구조와 렌더링 메커니즘을 제공한다. 이 기술은 현재 공간정보산업표준을 담당하는 OGC(Open Geospatial Consortium)와 유기적 협력을 통해 발전하고 있다.
Cesium 3D tiles 가시화 모습 예시

개요
Cesium은 구글 어스와 유사한 지구 스케일의 디지털트윈 플랫폼이다. 이를 이용하면, 도시 차원에서 분석하고, 실내 건물을 탐색하는 등의 유스케이스를 개발할 수 있다. 국내 대부분의 3차원 도시 플랫폼 기반 서비스에서 Cesium이 사용되고 있다. Cesium은 디지털트윈 모델을 다루기 위한 저작도구도 함께 제공한다. 개발자는 서비스에 필요한 메뉴 기능, 데쉬보드에 표출한 데이터 처리에만 신경을 쓰면 된다. 
Cesium 저작도구 예시

공간정보 기술을 연구하다 보면, 가끔 BIM 파일 포맷 중 하나 인 IFC(Industry Foundation Classes)를 Cesium위에 가시화해야 하는 경우가 종종 발생할 때가 있다. 하지만, Cesium은 IFC를 직접적으로 지원하지 않는다. 
IFC 추가 에러 발생 모습

Cesium은 IFC를 포함한 모든 3D 모델파일을 3D tiles로 변환해 업로드하도록 하고 있다. 이는 무거운 3D 모델의 가시화 성능을 고려한 것이다. 

3D tiles은 웹에서 가시화하기에 무거운 3D 파일을 공간인덱싱 기법을 이용해 Octree형식으로 표현하고, 각 노트에 분할된 3D 모델의 부분을 담아둔다. 메쉬 간략화 기법을 이용해, 카메라가 모델을 비추는 거리에 따라 적절한 LoD(Level of Detail)의 메쉬를 보여준다. 이는 게임에서 FPS 성능을 올리기 위해 개발된 기법과 매우 유사하다. glTF 20은 3D 타일의 기본 형식이다. 
glTF 2.0 기능(3차원 점군, 텍스쳐, 모델 지원 예시)

Cesium은 다양한 샘플 코드를 sandcastle이란 플랫폼으로 제공하여, 편리한 개발을 지원하고 있다.

3D tile 모델 변환 및 업로드
먼저, 다음 링크를 방문해 Cesium ion 에 가입한다.
이후, 세슘의 API, 어셋(asset)을 관리하는 클라우드, Javascript 기반 예제 등을 무료로 사용할 수 있다. 여기서 어셋이란 플랫폼에서 사용하는 GIS, BIM 등 모든 파일 및 데이터셋을 의미한다. 가입 후, 아래를 클릭해 어셋을 추가해 보자. 
Cesium ion 메뉴 화면

Cesium은 3D 모델을 3차원 타일 형식으로 내부 표현한다. 이 형식을 지원하는 파일 포맷은 다음과 같다. 
IFC를 fbx와 같은 형식으로 변환한 후, My Assets 메뉴(My Assets | Cesium ion)를 이용해 데이터를 추가한다. 추가된 데이터는 고유의 Asset ID가 부여된다. 다음 그림에서 첫 행의 어셋인 2716386은 어셋 ID를 보여준다. 현재 세슘은 5GB 무료 어셋 저장소를 제공한다.

참고로, IFC파일을 다른 형식으로 변환하기 위해서는 Revit 등 상용 모델러를 사용하거나, Blender와 같은 오픈소스 도구에 IFC import 애드인을 설치하고, fbx 포맷 등으로 저장하면 된다. 
Blender에서 IFC to FBX 변환 모습

3차원 모델은 좌표 원점에 표시되므로, 모델의 원점과 각도를 재조정해야 한다. 

다음과 같이 MyAssets 메뉴에 Adjust Tileset Location 메뉴를 클릭한다.

참고로, 만약 모델 원점이 0.0.0이 아니면, 다음 그림과 같이 재조정하기 어렵다. 모델링 할 때 원점을 맞추고 진행한다.

3D Tile Location Editor에서 모델의 위치, 방향을 수정한 후 저장버튼을 클릭한다. 참고로, Click position 버튼을 클릭하면, 현재 커서 위치의 지표면에 맞춰 타일 모델 위치를 자동 입력한다.

앱 서비스 개발
특정 서비스에 3D tile을 사용하고 싶다면, 단순히 primities.add 함수를 사용하여 3D tile을 추가할 수 있다. API를 사용해야 하므로, API 토큰을 생성 한다. 
API 키 토큰 생성 예시

다음 링크를 참고해, API 키 토큰을 생성하고, 이 문자열을 코드의 Token에 할당한다.
HTML를 만들어, 다음 같이 자바스크립트 파일을 입력한다. 어셋 ID를 이용해 fromAssetID에서 어셋을 가져온다.

         Cesium.Ion.defaultAccessToken = '';
         var viewer = new Cesium.Viewer('cesiumContainer', {
            animation: false,
            homeButton: true,
            navigationHelpButton: true
         });

         const tileset = viewer.scene.primitives.add(
            new Cesium.Cesium3DTileset({
               url: Cesium.IonResource.fromAssetId(1378646),
            })
         );

3D tile 코드 사용 예시

웹 서비스에서 코드 호출한 결과는 다음과 같다. 3D tile이 잘 가시화되는 것을 확인할 수 있다.

3D tile 구조
Cesium의 3D Tiles 데이터 구조는 대규모 3D 지리 데이터를 효율적으로 스트리밍하고 시각화하기 위해 설계되었다. 3D Tiles 구조는 오픈소스로 다음 깃허브 링크에 공개되어 있다. 

타일 데이터셋은 다음과 같다.
  • Tileset
Tileset.json 파일이 3D 타일의 상위 구성 파일 역할을 하며, 타일 계층 구조와 데이터 위치 정보를 담고 있다. 이 파일은 전체 타일셋의 메타데이터와 루트 타일에 대한 참조를 제공한다. 
 {
  "transform": [
     4.843178171884396,   1.2424271388626869, 0,                  0,
    -0.7993325488216595,  3.1159251367235608, 3.8278032889280675, 0,
     0.9511533376784163, -3.7077466670407433, 3.2168186118075526, 0,
     1215001.7612985559, -4736269.697480114,  4081650.708604793,  1
  ],
  "boundingVolume": {
    "box": [
      0,     0,    6.701,
      3.738, 0,    0,
      0,     3.72, 0,
      0,     0,    13.402
    ]
  },
  "geometricError": 32,
  "content": {
    "uri": "building.b3dm"
  },
  "extensions": {
    "VENDOR_collision_volume": {
      "box": [
        0,     0,    6.8,
        3.8,   0,    0,
        0,     3.8,  0,
        0,     0,    13.5
      ]
    }
  }
}
  • 타일 계층 구조
타일 계층은 루트 타일에서 시작해 더 작은 타일로 분할되며, 쿼드트리나 옥트리 구조를 사용한다. 타일들은 자신보다 작은 자식 타일들을 가지며, 각각의 해상도가 다르다.
루트 타일은 대략적인 모델 데이터를 제공하고, 자식 타일은 줌 인할 때 더 높은 해상도를 제공한다.
  • 타일 구성 요소
Bounding Volume (경계 볼륨)은 각 타일이 포함하는 공간 영역을 정의한다. 이 경계는 카메라의 위치에 따라 타일을 로드할지 결정하는 데 사용된다.
Geometric Error (기하학적 오차)는 타일의 해상도와 관련된 값으로, 클라이언트가 어떤 타일을 로드할지 선택하는 데 도움을 준다.
  • 타일 콘텐츠
타일은 다양한 형태의 3D 데이터를 포함할 수 있다.
Batched 3D Model (B3DM)은 여러 개의 3D 객체를 포함한 배치된 모델이다.
Instanced 3D Model (I3DM)은 동일한 3D 모델을 여러 위치에 인스턴스화하여 사용한다.
Point Cloud (PNTS)는 점군 데이터를 포함한다.
Composite (CMPT)는 다양한 콘텐츠를 하나의 타일에 혼합할 수 있다.
  • LOD (Level of Detail)
3D Tiles는 LOD(세부 수준)에 따라 계층 구조를 가지며, 줌 수준에 따라 더 높은 해상도의 타일을 로드하거나, 멀리 있는 객체는 낮은 해상도를 유지함으로써 성능을 최적화한다.
  • 압축 및 최적화
Cesium의 3D Tiles는 데이터를 압축하여 전송 속도와 메모리 효율성을 높인다.
Draco 압축을 사용하여 기하학 데이터를 압축하고 파일 크기를 줄인다.
이처럼 Cesium의 3D Tiles 구조는 대규모 지리 데이터를 효율적으로 처리하고 동적으로 로드하여 성능을 극대화한다.

다음은 앞의 구조를 표현한 그림이다.
다음 그림은 3D 타일 공식 포맷인 glTF 2.0의 구조와 개념을 상세히 보여준다. 
레퍼런스