LangChainゆる勉強会(1)

## はじめに

LangChainの安定版V0.1.0がリリースされたのを記念して、
LangChainゆる勉強会を開催します。

## LangChainとは？

https://python.langchain.com/docs/get_started/introduction

> LangChain is a framework for developing applications powered by language models. It enables applications that:
>
> Are context-aware: connect a language model to sources of context (prompt instructions, few shot examples, content to ground its response in, etc.)
>Reason: rely on a language model to reason (about how to answer based on provided context, what actions to take, etc.)

**LangChainは言語モデルを利用したアプリケーション開発のためのフレームワークです。** 
以下のようなアプリケーションを可能にします：

コンテキストを認識する：言語モデルをコンテキストのソース（プロンプトの指示、いくつかの例、応答の根拠となるコンテンツなど）に接続する。
推論する：言語モデルに依存して推論する（提供されたコンテキストに基づいてどのように答えるか、どのようなアクションを取るか、など）。

## LangChainに用意されている6つのモジュール

* Model I/O 言語モデルを扱いやすくする
    * プロンプト準備、言語モデルの呼び出し、結果の受け取りの3つのステップを簡単に実装するための機能

* Retrieval 未知のデータを扱えるようにする
    * LLMは未知のデータ（学習していないデータ）を扱えません
    * この問題に対応するのがRetrieval-Augmented Generation(RAG)です
    
* Memory　過去の対話を短期・長期で記憶する
    * 前の文脈を踏まえた対話形式で言語モデルに回答させるには、それまでの会話をすべて送信する必要がある
    * 会話履歴の保存と、読み込みを簡単にする機能を提供
* Chains　複数の処理をまとめる
    * LangChainでは多数のモジュールや別の機能を組み合わせつつ1つのアプリケーションを作成
    * このモジュールは組み合わせを簡単にする機能を提供します
* Agents 自律的に外部と干渉して言語モデルの限界を超える
    * ReActやOpenAI Function Callingという手法を用い、言語モデルでは対応できないタスクをおこなう

* Callbacks さまざまなイベント発生時に処理を行う
    * ログ出力や外部ライブラリとの連携




## 参考書

以下の書籍を参考にしています。

[LangChain完全入門　生成AIアプリケーション開発がはかどる大規模言語モデルの操り方](https://www.amazon.co.jp/LangChain%E5%AE%8C%E5%85%A8%E5%85%A5%E9%96%80-%E7%94%9F%E6%88%90AI%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E9%96%8B%E7%99%BA%E3%81%8C%E3%81%AF%E3%81%8B%E3%81%A9%E3%82%8B%E5%A4%A7%E8%A6%8F%E6%A8%A1%E8%A8%80%E8%AA%9E%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E6%93%8D%E3%82%8A%E6%96%B9-%E7%94%B0%E6%9D%91%E6%82%A0/dp/4295017965)
![Screenshot 2024-01-11 at 9.44.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/75bd18c2-7e63-0a26-8a3a-877746b559ea.png)

書籍のソースコードはここにあります。
https://github.com/harukaxq/langchain-book

## 今回の環境の特徴

多くの場合、LangChainから接続するLLMは、OpenAI社のAPIを使うと思います。（そのような記事が圧倒的に多いです）
しかし、今回は、私の環境の都合で**Amazon Bedrock**を利用します。

なお、以下の書籍ではLangChainのバージョンは、0.0.261を利用していますが、せっかくなので、最新安定版の0.1.0を利用します。


## 環境のセットアップ

今回は以下の環境を使っています。
* MacOS
* 複数バージョンの管理：https://asdf-vm.com/
* Pythonのパッケージマネージャー：https://python-poetry.org/

なお、poetry、Pythonともにasdfからインストールできます。

ここでは、asdfがインストールされている前提で、Python、Poetryのインストールから説明します。


### Pythonインストール（asdfから）

基本的には以下のサイトの手順でOKです。
https://dev.classmethod.jp/articles/asdf-python-introduction/

```bash
# Python のプラグインとリポジトリを確認します
% asdf plugin list all | grep python
（出力↓）
python                       *https://github.com/danhper/asdf-python.git

# プラグインをインストールします。
% asdf plugin add python https://github.com/danhper/asdf-python.git

# プラグインがインストールできたか確認します。
% asdf plugin list
（出力↓）
python

# インストール可能なバージョン一覧を確認
% asdf list all python
（出力↓）
ズラズラと出てきます。

# 今回はPython3.11.6をローカルにインストールします。
% asdf install python 3.11.6
% asdf local python 3.11.6
% python --version
（出力↓）
Python 3.11.6
```

### poetryインストール（asdfから）

poetryのインストールもPythonと基本的な手順は一緒です。

```bash

% asdf plugin-add poetry
% asdf list-all poetry
（出力↓）
（中略）
1.7.1
% asdf install poetry 1.7.1
% asdf local poetry 1.7.1
% poetry --version
（出力↓）
Poetry (version 1.7.1)
```

### poetryのプロジェクト初期化
```bash

# poetry初期化（基本的に全部デフォルトで良い）
% poetry init

# 仮想環境の使用
% poetry config virtualenvs.in-project true --local

# pythonバージョン確認
% poetry run python --version
% poetry env list
```

## LangChainのインストール

以下のようにしてv0.1.0を指定します。
```bash
poetry add langchain==0.1.0
```
この結果は、以下のようにpyproject.tomlに書き込まれます。

```toml:pyproject.toml
[tool.poetry]
name = "impress-book-v0-1-0"
version = "0.1.0"
description = ""
authors = ["Akira Abe"]
readme = "README.md"
packages = [{include = "impress_book_v0"}]

[tool.poetry.dependencies]
python = "^3.11"
langchain = "0.1.0"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

```

## 環境変数などの定義
.envを作成しAWSのアクセスキーなどを登録します。

```.env
AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=YYYYYYYYYYYYYYYYYYYY
AWS_DEFAULT_REGION=us-east-1
MODEL_ID=anthropic.claude-v2
```

:::note warn
警告
.envはgitにコミットしないでください！
:::


## Model I/Oのサンプル実装

このタイミングで他に必要なライブラリをインストールします。
```bash
poetry add python-dotenv==1.0.0
poetry add boto3==1.34.16
poetry add langchain-community==0.0.11
```

まずは非常に単純な、LLMを呼び出すケース

```python:language_models.py
import os
from dotenv import load_dotenv
from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage
import langchain
langchain.verbose = True

load_dotenv()

chat = BedrockChat(
    model_id=os.getenv("MODEL_ID"),
    streaming=True,
    model_kwargs={"temperature": 0.5, "max_tokens_to_sample" : 2048}
  )

result = chat([HumanMessage(content="Hello, how are you?")])

print(result.content)

```

プロンプトテンプレートを使ったケース
```python:prompt_and_language_model.py
import os
from dotenv import load_dotenv
from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage
from langchain_core.prompts import PromptTemplate
import langchain
langchain.verbose = True

load_dotenv()

# BedrockChatを初期化する
chat = BedrockChat(
    model_id=os.getenv("MODEL_ID"),
    streaming=True,
    model_kwargs={"temperature": 0.5, "max_tokens_to_sample" : 2048}
  )

# PromptTemplateを使って、文章を生成する
prompt = PromptTemplate(
  template="{product}はどこの会社が開発した製品ですか？",
  input_variables=[
    "product"
  ]
)

result = chat(
  [
    HumanMessage(content=prompt.format(product="iPhone")),
  ]
)

print(result.content)
```

OutputParserを用いてLLMからの出力をパースするケース
```python:pydantic_output_parser.py
import os
from dotenv import load_dotenv

from langchain_community.chat_models import BedrockChat
# from langchain_core.output_parsers import PydanticOutputParser, OutputFixingParser
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field, ValidationInfo, field_validator
import langchain
langchain.verbose = True

load_dotenv()

# BedrockChatを初期化する
chat = BedrockChat(
    model_id=os.getenv("MODEL_ID"),
    streaming=True,
    model_kwargs={"temperature": 0.5, "max_tokens_to_sample" : 2048}
  )

# Pydanticのモデルを定義する
class Smartphone(BaseModel): 
    release_date: str=  Field(description="スマートフォンの発売日") #← Fieldを使って説明を追加する
    screen_inches: float = Field(description="スマートフォンの画面サイズ(インチ)")
    os_installed: str = Field(description="スマートフォンにインストールされているOS")
    model_name: str = Field(description="スマートフォンのモデル名")

    @field_validator("screen_inches") #← validatorを使って値を検証する
    def validate_screen_inches(cls, field: int, info: ValidationInfo): #← validatorの引数には、検証するフィールドと値が渡される
        assert info.config is not None #← configがNoneでないことを確認する
        if field <= 0: #← screen_inchesが0以下の場合はエラーを返す
            raise ValueError("Screen inches must be a positive number")
        return field

# PydanticOutputParserを定義する
parser = OutputFixingParser.from_llm(
    parser = PydanticOutputParser(pydantic_object=Smartphone),
    llm=chat
) #← PydanticOutputParserをSmartPhoneモデルで初期化する 

result = chat([ #← Chat modelsにHumanMessageを渡して、文章を生成する
    HumanMessage(content="Androidで最近リリースしたスマートフォンを1個挙げて"),
    HumanMessage(content=parser.get_format_instructions())
])

parsed_result = parser.parse(result.content) #← PydanticOutputParserを使って、文章をパースする

print(f"モデル名: {parsed_result.model_name}")
print(f"画面サイズ: {parsed_result.screen_inches}")
print(f"OS: {parsed_result.os_installed}")
print(f"発売日: {parsed_result.release_date}")
```

## Retrievalのサンプル実装

このタイミングでchainlitなどのライブラリをインストールします。
```bash
poetry add spacy==3.7.2
poetry add pymupdf==1.23.8
poetry add tiktoken==0.5.2
poetry add chromadb==0.4.22
poetry add chainlit==0.6.402

# spacyの日本語対応モデルをダウンロードする
poetry run python -m spacy download ja_core_news_sm
```

PDFの内容を元に回答するケース

```python:chat_3.py
import os
from dotenv import load_dotenv

import chainlit as cl
from langchain_community.chat_models import BedrockChat
from langchain.document_loaders import PyMuPDFLoader
from langchain.embeddings import BedrockEmbeddings
from langchain_core.prompts import PromptTemplate
from langchain_core.messages import HumanMessage
from langchain.text_splitter import SpacyTextSplitter
from langchain_community.vectorstores import Chroma

import langchain
langchain.verbose = True
load_dotenv()

# Chat開始時に検索対象のPDFをアップロードするようにする。
embeddings = BedrockEmbeddings()

chat = BedrockChat(
    model_id=os.getenv("MODEL_ID"),
    streaming=True,
    model_kwargs={"temperature": 0.5, "max_tokens_to_sample" : 2048}
  )
prompt = PromptTemplate(template="""文章を元に質問に答えてください。 

文章: 
{document}

質問: {query}
""", input_variables=["document", "query"])

text_splitter = SpacyTextSplitter(chunk_size=300, pipeline="ja_core_news_sm")

@cl.on_chat_start
async def on_chat_start():
    files = None #← ファイルが選択されているか確認する変数

    while files is None: #← ファイルが選択されるまで繰り返す
        files = await cl.AskFileMessage(
            max_size_mb=20,
            content="PDFを選択してください",
            accept=["application/pdf"],
            raise_on_timeout=False,
        ).send()
    file = files[0]

    if not os.path.exists("tmp"): #← tmpディレクトリが存在するか確認
        os.mkdir("tmp") #← 存在しなければ作成する
    with open(f"tmp/{file.name}", "wb") as f: #← PDFファイルを保存する
        f.write(file.content) #← ファイルの内容を書き込む

    documents = PyMuPDFLoader(f"tmp/{file.name}").load() #← 保存したPDFファイルを読み込む
    splitted_documents = text_splitter.split_documents(documents) #← ドキュメントを分割する

    database = Chroma( #← データベースを初期化する
        embedding_function=embeddings,
        # 今回はpersist_directoryを指定しないことでデータベースの永続化を行わない
    )

    database.add_documents(splitted_documents) #← ドキュメントをデータベースに追加する

    cl.user_session.set(  #← データベースをセッションに保存する
        "database",  #← セッションに保存する名前
        database  #← セッションに保存する値
    )

    await cl.Message(content=f"`{file.name}`の読み込みが完了しました。質問を入力してください。").send() #← 読み込み完了を通知する

@cl.on_message
async def on_message(input_message):
    print("入力されたメッセージ: " + input_message)
    database = cl.user_session.get("database") #← セッションからデータベースを取得する
    documents = database.similarity_search(input_message)
    documents_string = ""

    for document in documents:
        documents_string += f"""
    ---------------------------
    {document.page_content}
    """

    result = chat([
        HumanMessage(content=prompt.format(document=documents_string,
                                           query=input_message)) #← input_messageに変更
    ])
    await cl.Message(content=result.content).send()

```