그날, 메모하기가 귀찮은 나는 카카오톡에 URL만 쌓아두었는데, Langchain을 만나 슬랙과 노션으로 자동 정리하는 마법을 터득했다!

제목은 GPT가 정해줬습니다.(책임회피..)

지피터스에서 활동하며 열심히 메모와 노트를 작성하는 사람들을 많이 목격했습니다.

그러나 제 경우는 달랐습니다. 귀찮음의 극치를 느끼며, 카카오톡 '나에게 보내기'에 무심코 쌓아둔 URL들을 바라보곤 했습니다. 이런 일상 속에서, 귀찮고 시간이 아까운 일들을 GPT에 맡기자는 결심이 섰고, 그래서 이번 프로젝트를 기획하게 되었습니다. 이 프로젝트는 바로 그 귀찮음을 해결하고자 하는 저의 강렬한 의지와 필요성을 반영한 것입니다.


프로젝트 정리

목표 : 슬랙으로 url을 입력받아 적절히 처리하여 나중에 무슨 내용의 url인지 찾기 쉽도록 노션 데이터베이스에 저장하기

순서 :

  1. URL슬랙 메세지로 입력받기

  2. URL분석하기 (URL이 일반 웹사이트인지, 유튜브인지, 이미지인지, pdf인지 분석합니다.)

  3. 분석된 URL에 맞춰 적절한 Tool로 내용 가져오기

    1. 일반 url : 페이지 접속해서 텍스트 페이지의 텍스트를 가져옵니다.

    2. 유튜브 url : 유튜브 내용을 가져옵니다.

      1. 자막(ko, en)을 가져옵니다.

      2. 자막이 없다면 음성파일을 추출하여 whisper로 STT(Speech To Text)합니다.

    3. 이미지 url : 이미지를 가져와 설명합니다.

      1. transfomer 패키지를 이용해서 로컬 ai로 이미지를 해석합니다.(로컬 서버 성능 이슈로 포기)

      2. GPT-4v를 이용해서 이미지를 해석합니다.

    4. pdf url : pdf의 내용을 가져옵니다. (이후 업데이트 예정)

  4. 가져온 내용을 제목, 내용으로 짧게 요약하기

  5. 요약된 내용을 노션 데이터 베이스에 저장하기


슬랙으로 URL 메세지 입력받기

api키를 발급 받는 과정은 생략하겠습니다. (저보다 GPT가 더 잘 설명해줍니다.)

여기서는 제가 작성한 코드와 어려웠던 권한 설정, 보안 설정만 짧게 설명하겠습니다.

1. slack bot 권한 설정(Scope)

이 프로젝트의 봇은 DM으로 제 URL을 입력받아 API서버에 메세지를 전달해야합니다.

그래서 권한이 매우 제한적으로 필요합니다. (일반적인 봇보다 권한이 그다지 필요없습니다.)

하지만 그럼에도 권한 설정이 몇번이나 실패했고 결과적으로 성공한 권한만 공유하겠습니다.

Scope

  • 스코프 설명

    1. im:history: 이 스코프는 봇이 추가된 DM에서 메시지와 기타 콘텐츠를 볼 수 있게 해줍니다. 이것은 사용자가 봇에게 보낸 DM을 읽고 처리하는 데 필수적입니다.

    2. im:read: 이 스코프는 봇이 추가된 DM에 대한 기본 정보를 볼 수 있게 해줍니다. 이 스코프는 DM의 메타데이터나 상태 정보 같은 것들을 확인하는 데 유용할 수 있습니다.


이벤트 구독(Subscribe to bot events)

Slack 앱이 봇 이벤트에 구독하도록 설정하는 것은, 봇 사용자가 접근할 수 있는 이벤트(예를 들어, 채널에서의 새 메시지)에 대해 알림을 받기 위한 것입니다. 여기서 "message.im" 이벤트에 구독하는 것은 다음과 같은 의미를 가집니다:

  • Event Name: message.im

    • im:history: 직접 메시지 채널에서 메시지가 게시되었을 때.


서버 코드

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import json
from slack_sdk import WebClient
import os
from langchain_openai import ChatOpenAI

import sys
sys.path.append('../')
from url_agent import URLAgent

slack_token = os.getenv('SLACK_BOT_TOKEN')
client = WebClient(token=slack_token)

# 전역 변수로 처리된 이벤트 ID 저장
processed_events = set()

@csrf_exempt
@require_POST
def slack_events(request):
    json_data = json.loads(request.body)
    print(json_data)

    # 'challenge' 요청 처리
    if 'challenge' in json_data:
        return JsonResponse({'challenge': json_data['challenge']})
    
    event_id = json_data.get('event_id', '')
    # 중복된 이벤트인지 확인
    if event_id in processed_events:
        return JsonResponse({'status': 'Already processed'})

    processed_events.add(event_id)
    
    text = json_data.get('event', {}).get('text', '')

    # ChatOpenAI 인스턴스 생성 및 URLAgent 처리
    key = os.getenv('OPENAI_API_KEY')
    llm = ChatOpenAI(api_key=key, temperature=0)
    URLAgent(llm, text)

    return JsonResponse({'status': 'OK'})


설명

파이썬으로 빠르게 구현하기위해 서버 프레임워크는 장고로 정했습니다.

단순 api서버이기 때문에 코드가 간단합니다.

주의하점은 ‘challenge’ 처리와 ‘event_id’중복 처리 입니다.


  • ‘challenge’ 처리

GPT의 challenge 설명
Slack API에서 "challenge"는 주로 이벤트 구독 시 사용되는 보안 메커니즘의 일부입니다. 이는 Slack이 앱의 이벤트 URL을 검증하기 위해 사용합니다. 여기서의 목적은 악의적인 사용자나 의도치 않은 소스로부터 오는 요청을 차단하여, 오직 실제 Slack 서버로부터의 요청만이 처리되도록 하는 것입니다.

설명처럼 Slack이벤트 수신은 url로 challenge키와 임의의 문자열 값을 포함하여 간단한 보안 처리를 합니다.

그래서 challenge의 임의의 문자열 값을 리턴해 주지 않으면 처리에 필요햔 진짜 요청값을 보내주지 않습니다.

    # 'challenge' 요청 처리
    if 'challenge' in json_data:
        return JsonResponse({'challenge': json_data['challenge']})


  • ‘event_id’중복 처리

여러 알수없는 이유로 slack이벤트로 중복된 이밴트 요청이 들어올때가 있습니다.

이 코드에서는 간단히 전역변수에 event_id를 저장해서 같은 값인경우 처리하지 않도록 했습니다.

evnet_id는 각 slack event별로 고유한 값이어서 이 처리만으로도 중복 이벤트 처리가 가능 합니다.

# 전역 변수로 처리된 이벤트 ID 저장
processed_events = set()

@csrf_exempt
@require_POST
def slack_events(request):
...
    event_id = json_data.get('event_id', '')
    # 중복된 이벤트인지 확인
    if event_id in processed_events:
        return JsonResponse({'status': 'Already processed'})
 processed_events.add(event_id)
...

LangChain - Agent

URL을 해석해서 적절한 처리를 하도록 Agent를 만들어야 합니다.

이 부분부터 진짜 우리가 원하는 알아서 움직이는 AI의 첫 걸음 입니다.


URLAgent 코드

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents.format_scratchpad.openai_tools import format_to_openai_tool_messages 
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser

from langchain.agents import tool
from langchain.schema.agent import AgentFinish
from langchain.agents import AgentExecutor

from image_caption import load_image_caption
from youtube_loader import load_youtube
from web_loader import load_web
from summarize_documents import summarize
from save_to_notion import save_to_notion

from langchain_openai import ChatOpenAI
import os
import dotenv
dotenv.load_dotenv(verbose=True)

key = os.getenv('OPENAI_API_KEY')

llm = ChatOpenAI(api_key=key, temperature=0)

def summarize_to_notion(url, documents, type):
    summaries = summarize(llm, documents)
    save_to_notion(url, summaries, type)

@tool
def summarize_web(url):
    """Describes a web page"""
    documents = load_web(url)
    summarize_to_notion(url, documents, 'Web')
    return AgentFinish(log="summarize_web", return_values={"output": "작업 완료!"})
    
@tool
def summarize_youtube(url):
    """Describes a youtube video"""
    documents = load_youtube(url)
    summarize_to_notion(url, documents, 'Youtube')
    return AgentFinish(log="summarize_youtube", return_values={"output": "작업 완료!"})
    
@tool
def summarize_image(url):
    """Describes an image"""
    documents = load_image_caption(url)
    summarize_to_notion(url, documents, 'Image')
    return AgentFinish(log="summarize_image", return_values={"output": "작업 완료!"})

tools = [summarize_web, summarize_youtube, summarize_image]

def URLAgent(llm, input):
    prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "You need to look at the URL and invoke the appropriate tool.",
            ),
            ("user", "{input}"),
            MessagesPlaceholder(variable_name="URL_judgment"),
        ]
    )

    llm_with_tools = llm.bind_tools(tools)

    agent = (
        {
            "input": lambda x: x["input"],
            "URL_judgment": lambda x: format_to_openai_tool_messages(
                x["intermediate_steps"]
            ),
        }
        | prompt
        | llm_with_tools
        | OpenAIToolsAgentOutputParser()
    )

    agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

    list(agent_executor.stream({"input": input}))


설명

URLAgent는 Langchain의 사용 개념 3가지가 단순하게 섞인 모양이기 때문에 보기보다 훨씬 이해하기 쉬운 내용입니다.

코드를 다시 보시면, URLAgent는 3가지 단계로 구동됩니다.

  1. ChatPromptTemplat

prompt = ChatPromptTemplate.from_messages(...)

프롬프트 템플렛은 랭체인의 기본이자 가장 손쉬운 기능중 하나이고 안쓰이는 곳이 없는 기능입니다.

템플렛은 LLM과의 Chain과정에서 AI의 답변과 사람의 입력값을 구분해주고, 이 메세지를 리스트로 관리해줍니다.

그래서 모-든 Chain과정에 반드시 사용됩니다.(안쓰면 너무 귀찮거든요.)

ChatPromptTemplat 하나만으로도 한시간은 떠들어야 할만큼 다양한 기능이 있지만 여기서는 단순히 AI의 답변과 사람의 입력값을 구분하는 용도로 사용했다 정도만 기억하시면 됩니다.


  1. tools

Agent는 사실 혼자서는 별볼일없는 페르소나 프롬프트일 뿐입니다.

하지만 tools를 장착하면 그 역할은 전혀 달라집니다.

tool은 Agent가 장착할 도구들이고 Agent는 이 툴들의 설명을 보고 적절한 툴을 호출해서 처리를 맡깁니다.

마치 가제트처럼요!

tools = [summarize_web, summarize_youtube, summarize_image]
...
llm_with_tools = llm.bind_tools(tools)

여기서는 summarize_web, summarize_youtube, summarize_image를 만들었고 이 툴들에 대해서는 후술하겠습니다.


  1. agent

이제 agent만이 남았습니다.

3가지를 전달해줍니다.

  • slack을 통해 받은 input

  • prompt(ChatPromptTemplat)

  • tools


이 코드에서 보듯 agent는 ‘|’로 인자를 받습니다. 이는 "LECL” 개념으로 여러 처리 단계를 순차적으로 연결하는 데 사용됩니다.

즉 아래 코드를 해석하면,

  • input을 ‘format_to_openai_tool_messages함수로 처리하는 1단계

  • ChatPromptTemplat로 처리된 값을 정리하는 2단계

  • 적절한 tools을 호출하여 처리를 맡기는 3단계

  • OpenAIToolsAgentOutputParser로 Agent의 출력을 최종 출력 형식으로 변환합니다.


이러한 과정을 AgentExecutor객체를 생성하고 ‘agent_executor.stream({"input": input})’로 실행합니다.

    agent = (
        {
            "input": lambda x: x["input"],
            "URL_judgment": lambda x: format_to_openai_tool_messages(
                x["intermediate_steps"]
            ),
        }
        | prompt
        | llm_with_tools
        | OpenAIToolsAgentOutputParser()
    )
...
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
list(agent_executor.stream({"input": input}))

여기까지 오시느라 고생하셨습니다.

지금부터는 Tool들을 설명하겠습니다. (거의다 왔습니다!)

LangChain - Tools

agent가 사용할 툴 3가지를 만들 차례입니다. 허무할만큼 쉽게 구현되어 있으니 너무 걱정하지 마세요!

agent가 사용할 툴들은 langchain을 이용해 커스텀으로 직접 구현해서 사용할수 있습니다.

하지만 langchain_community 패키지에는 langchain 커뮤니티러 부터 인증받은 좋은 툴들이 이미 구현되어 있습니다.


part 1 Web_loader

가장 쉬운 WebBaseLoder부터 살펴보겠습니다.

from langchain_community.document_loaders import WebBaseLoader

def load_web(url):
    loader = WebBaseLoader(url, autoset_encoding=True)
    documents = loader.load()
    return documents

이 짧은 6줄(공백포함)의 코드로 url에서 텍스트를 크롤링해오고 이를 documents객체에 담아서 저장해줍니다.


WebBaseLoader객체 내부를 보면 복잡한 과정을 거쳐 우리가 원하는 내용만 뚝딱 가져다 줍니다.

모든 코드를 보진 못했지만 ‘beautiful soup 4’를 이용해서 정적 웹사이트를 크롤링 하는 것 같습니다.


part 2 youtube_loader

유튜브 처리는 조금 더 복잡한 과정을 거쳐야 합니다.

유튜브는 2가지 방식이 있습니다.

  1. 자막을 가져오기

  2. 음성을 가져와서 텍스트로 변환하기


제가 택한 방식은 자막을 먼저 사용하고 없으면 음성으로 처리하게 했습니다.


  1. 자막가져오기

from langchain_community.document_loaders import YoutubeLoader

def load_youtube(url):
    urls = url.split("=")[-1]
    loader = YoutubeLoader(urls, language=["ko", "en"])  # 'ko'가 우선, 없을 경우 'en' 사용
    documents = loader.load()
...

앞서 봤던 webloader처럼 이미 툴 구현도 완벽하고 사용도 너무나 쉽습니다.


2.음성가져와서 변환하기

rom langchain_community.document_loaders.generic import GenericLoader
from langchain_community.document_loaders.parsers import OpenAIWhisperParser

def load_youtube_audio(url):
    urls = [url]
    save_dir = "~/data/youtube_audio"
    loader = GenericLoader(YoutubeAudioLoader(urls, save_dir), OpenAIWhisperParser())
    documents = loader.load()
    return documents

YoutubeAudioLoader로 오디오를 저장한 다음 OpenAIWhisperParser로 위스퍼로 처리했습니다.

툴을 잘 쓰기만 하면 너무너무 쉽게 이 과정이 뚝딱 됩니다.


전체 코드

from langchain_community.document_loaders import YoutubeLoader

from langchain_community.document_loaders.blob_loaders.youtube_audio import (
    YoutubeAudioLoader,
)
from langchain_community.document_loaders.generic import GenericLoader
from langchain_community.document_loaders.parsers import OpenAIWhisperParser

def load_youtube_audio(url):
    urls = [url]
    save_dir = "~/data/youtube_audio"
    loader = GenericLoader(YoutubeAudioLoader(urls, save_dir), OpenAIWhisperParser())
    documents = loader.load()
    return documents

def load_youtube(url):
    # remove https://www.youtube.com/watch?v=
    urls = url.split("=")[-1]
    loader = YoutubeLoader(urls, language=["ko", "en"])  # 'ko'가 우선, 없을 경우 'en' 사용
    documents = loader.load()
    if len(documents) == 0:
        documents = load_youtube_audio(url)
    if len(documents) == 0:
        raise Exception("No documents found")
    return documents


part 3 image_caption

숨은 복병 이미지 캡션입니다.

이것때문에 Aws → Auzre → 로컬 서버 순으로 서버를 옮기는 고생을 했습니다.

설명을 위해 제가 지금 사용하고 있는 코드가 아닌 구버전을 먼저 설명 드리겠습니다.

from langchain_community.document_loaders import ImageCaptionLoader

import requests
import datetime

def download_image(image_url):
    ...

def load_image_caption(image_url):
    loader = ImageCaptionLoader(image_url)
    documents = loader.load()
    return documents


앞서 본 webloader, youtubeloader 처럼 너무너무 쉽게 사용이 가능합니다.

제 맥북에서는요…

ImageCaptionLoader 는 transformer라는 패키지를 사용해 이미지를 분석합니다. 그런데 이 패키지가 상당히 무겁습니다. 고작 1코어짜리 작고 소중한 프리티어 클라우스 서버로는 감당하지 못할 만큼요. (시도하진 않았지만 제 귀여운 라즈베리파이4 서버도 힘들것같습니다.)

심지어 이걸 서버 이전을 이미 한번 한 이후에 깨달았습니다. (으이구!!!)

그래서 해결책으로 GPT-4v로 눈을 돌렸습니다. (대부분은 돈으로 해결이 가능하니까요..)


part 3-1 GPT-4v를 이용한 ImageCaptionLoader


모든 질문을 GPT-4v을 사용해서 답변하기에는 너무 비용이 비싸니 이미지 처리만 담당하는 툴의 llm모델을 GPT-4v로 구현하였습니다.

아쉽게도 GPT-4v이 아직 프리뷰 상태여서 그런지 ImageCaptionLoader의 gpt-4v버전은 없었습니다.

from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from langchain_core.documents.base import Document

import os
os.getenv('OPENAI_API_KEY')

import requests
import datetime
import base64

def download_image(image_url):
   ...

def encode_image(image_path):
    """Getting the base64 string"""
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode("utf-8")

def load_image_caption(url):
    """Make image summary"""
    file_name = download_image(url)
    
    base64_string = encode_image(file_name)
    chat = ChatOpenAI(model="gpt-4-vision-preview", max_tokens=1024)

    try:
        chat_completion = chat.invoke(
            [
                HumanMessage(
                    content=[
                        {"type": "text", "text": "Describe the image:"},
                        {
                            "type": "image_url",
                            "image_url": {"url": f"data:image/jpg;base64,{base64_string}"},
                        },
                    ]
                )
            ]
        )

        # Create a Document object with the image description
        document = Document(page_content=chat_completion.content)

        # Return a list containing the single Document
        return [document]
    except Exception as e:
        print("An error occurred:", e)
        return []


openAI의 GPT-4v 문서를 보면 이미지는 url에 base64형태로 이미지를 담아서 보낸다고 되어 있습니다. (참고) 지금 다시 읽어보니 url로도 가능할지도??

가장 일반적인 형태의 챗 모델(ChatOpenAI)을 가져와 한번의 요청을 보내고 받은 결과를 리턴하도록 했습니다.

tool을 만드실때 주의하실 점은 모든 tool의 리턴값이 동일해야합니다.

이 지점에서 한참을 헤맸습니다.


LangChain - Chains

각 툴들이 리턴하는 텍스트들을 요약하고 정리하는 기능을 만들어 최종적으로 우리가 원하는 형태로 가공하도록 해야합니다.

이런 과정을 Chains 패키지의 summarize를 이용해 구현하였습니다.


전체 코드

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains.summarize import load_summarize_chain
from langchain.prompts import PromptTemplate

def summarize(llm, documents):
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=3000)
    docs = text_splitter.split_documents(documents)
    combine_template = """{text}
    You summarize this document in the following format.
    Be sure to write it in Korean.
    Write the results of your summary in the following format:
    제목: Title
    내용: Write your key takeaways in bullet point format
    """
    combine_prompt = PromptTemplate(template=combine_template, input_variables=['text'])
    chain = load_summarize_chain(
        llm,
        chain_type="map_reduce",
        combine_prompt=combine_prompt,
        verbose=True
        )
    summaries = chain.invoke(docs)
    return summaries  # Directly return the summaries if they are already strings


TextSplitter

가장먼저 텍스트를 작은 단위로 나누어야 합니다.

LangChain은 다양한 TextSplitter를 지원하며 그중에서 RecursiveCharacterTextSplitter를 사용하였습니다.

    text_splitter = RecursiveCharacterTextSplitter(chunk_size=3000)
    docs = text_splitter.split_documents(documents)

chunk_size는 토큰사이즈와는 다르게 단순 텍스트 캐릭터 단위로 생각하시면 편합니다.


load_summarize_chain

여러 chain중에서 요약에 특화된 chain으로 2가지 프롬프트 타입과 3가지 모드가 있습니다.

  • prompt

    • 요약 작업을 위해 언어 모델에 전달되는 초기 프롬프트입니다. prompt는 모델에게 어떤 작업을 수행할지 지시하는 지침을 제공합니다. 예를 들어, 특정 텍스트를 요약하도록 요청하는 문장이 될 수 있습니다.

  • combine_prompt

    • 여러 개의 입력이나 결과를 조합할 때 사용되는 프롬프트입니다. 이 프롬프트는 여러 부분의 정보를 통합하고, 그 결과를 요약하는 데 사용됩니다. 예를 들어, 여러 문서의 내용을 하나의 요약된 텍스트로 결합하는 경우에 활용될 수 있습니다.


  • stuff

    • 체인은 주로 데이터나 정보를 처리하고 구조화하는 데 초점을 맞춥니다. 이 체인은 복잡한 데이터 세트나 다양한 정보 소스로부터 중요한 내용을 추출하고 구조화하는 데 사용될 수 있습니다. (설명은 이렇지만 데이터가 은 경우 사용할수 없어서 보통 map_reduce로 처리합니다.)

  • map_reduce

    • 대규모 데이터를 처리하고 요약하는 데 적합하며, 데이터를 여러 부분으로 나누어 각 부분을 개별적으로 처리한 후 결과를 합쳐서 최종 결과를 도출합니다.

  • refine

    • 체인은 주어진 내용을 정제하고 개선하는 데 초점을 맞춥니다. 이 체인은 초기 요약본을 생성한 후 이를 점차 개선하고 세부적으로 조정하는 과정을 거칩니다.


이 프로젝트에서는 많은 양의 텍스트를 요약하고 특정 형태로 저장하는게 필요했기 때문에 ‘map_reduce’ 타입을 사용했으며 요약 과정에서 정해진 포멧으로 요약하기 위해 ‘combine_prompt’을 사용했습니다.

    combine_template = """{text}
    You summarize this document in the following format.
    Be sure to write it in Korean.
    Write the results of your summary in the following format:
    제목: Title
    내용: Write your key takeaways in bullet point format
    """
    combine_prompt = PromptTemplate(template=combine_template, input_variables=['text'])
    chain = load_summarize_chain(
        llm,
        chain_type="map_reduce",
        combine_prompt=combine_prompt,
        verbose=True
        )
    summaries = chain.invoke(docs)
    return summaries  # Directly return the summaries if they are already strings



노션에 저장하기


결과 도출을 위한 모든 과정이 끝나고 이 결과를 노션에 저장해야 합니다.

이 과정은 너무나 자세히 설명해둔 곳이 많아서 짧게 코드만 공유하겠습니다.

rom notion_client import Client

import re
import datetime
import os
import dotenv
dotenv.load_dotenv(verbose=True)

def extract_title(summary):
    # 정규 표현식을 사용하여 제목 추출
    match = re.search(r'제목: (.+?)\n', summary)
    return match.group(1) if match else 'No Title Found'

def save_to_notion(url, summaries, type):
    notion_api_key = os.getenv('NOTION_API_KEY')
    database_id = os.getenv('NOTION_DATABASE_ID')
    current_datetime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
    notion = Client(auth=notion_api_key)
    
    title = extract_title(summaries.get('output_text', ''))

    # Create a new page
    new_page = notion.pages.create(parent={"database_id": database_id}, properties={
        "title": {
            "title": [
                {
                    "text": {
                        "content": title
                    }
                }
            ]
        },
        "Type": {
            "select": {
                "name": type
            }
        },
        "Created": {
            "date": {
                "start": current_datetime
            }
        },
        "URL": {
            "url": url
        }
    })

    # Add summary to the page
    notion.blocks.children.append(block_id=new_page["id"], children=[
        {
            "object": "block",
            "type": "paragraph",
            "paragraph": {
                "rich_text": [
                    {
                        "type": "text",
                        "text": {
                            "content": summaries.get('output_text', '')
                        }
                    }
                ]
            }
        },
        {
            "object": "block",
            "type": "embed",
            "embed": {
                "url": url
            }
        }
    ])



마무리

처음 이 프로젝트를 시작해서 구현할때 상당히 막막했던 기억이 납니다.

아직 구현할 내용이 많이 남아 있지만 지금까지의 구현만으로도 작게나마 제 삶에 도움을 주는 도구를 만들수있었습니다.


랭체인을 처음 공부할때부터 만들어 보고 싶던 기능이기도 해서 지속적으로 발전시켜서 더 도움이 될만한 개인용 자비스가 될때까지 업데이트하고 공유하겠습니다.

엄청 긴 글을 읽으시느라 너무 고생하셨습니다.


질문이나 코맨트는 댓글로 남겨주시면 답변드리겠습니다.


#9기 #랭체인 #9기 랭체인

8
3개의 답글

👉 이 게시글도 읽어보세요

모집 중인 AI 스터디