DevelopersIO記事中のLlamaIndexを使ったサンプルコードをLlamaIndex 0.6.32で動作するように修正してみる

# はじめに
OpenAI APIを使って､あらかじめ用意したデータ(PDFやドキュメントファイルなど)の内容にもとづいて質問に回答するようなものが作れないか調べたところ､以下の記事や参考情報にあげた[LlamaIndexの記事一覧](https://dev.classmethod.jp/tags/llamaindex/)がとても参考になりました｡

https://dev.classmethod.jp/articles/get-reference-in-query-of-llamaindex/

LlamaIndex 0.6以降でLlamaIndexに仕様変更があり､元記事のサンプルコードはそのままでは動作しませんでした｡そこで､DevelopersIOに掲載されているLlamaIndexに関する様々な記事を参考に､LlamaIndex 0.6.32で動作するようにコードを修正してみました｡

# 参考情報
https://github.com/jerryjliu/llama_index/issues/1900

https://dev.classmethod.jp/tags/llamaindex/

# 環境構築

## 環境
```console
$ python3 -V
Python 3.11.4
```

## 必要なPythonパッケージのインストール

```console
$ pip install llama-index pypdf
```

元記事ではPDFファイルを読み込むために`PyPDF2`をインストールすると書かれていますが､今回使用したPython3.11の環境ではPyPDF2ではなく`pypdf`が必要でした｡

```
ImportError: pypdf is required to read PDF files: `pip install pypdf`
```

どうやら､PyPDF2は開発が終了しpypdfとして開発がつづいているようです｡

https://pypi.org/project/PyPDF2/

インストールされた主なライブラリは以下のとおりです｡
```console
$ pip freeze | grep -e "openai" -e "llama-index" -e "pypdf" -e "langchain"
langchain==0.0.214
langchainplus-sdk==0.0.17
llama-index==0.6.32
openai==0.27.8
pypdf==3.11.0
```

その他､OpenAIのAPIキーを環境変数として設定する手順やフォルダ構成は元記事のとおりです｡

## Pythonコード
print_source_of_response()はどこからも呼ばれていません｡ここで出力したかった内容はshow_response()に含まれているようなので､おそらく､リファレンスの情報を取得するための実装例として挙げているのだと思います｡



```python
import json
import os
from typing import List

from llama_index import (Document, GPTVectorStoreIndex, ServiceContext, SimpleDirectoryReader, StorageContext, load_index_from_storage)
from llama_index.data_structs.node import DocumentRelationship
from llama_index.response.schema import RESPONSE_TYPE

def print_source_of_response(response: RESPONSE_TYPE, documents: List[Document]):
    for node in response.source_nodes:
        if DocumentRelationship.SOURCE not in node.node.relationships:
            print("no source doc found")
            continue
        source_doc_id = node.node.relationships[DocumentRelationship.SOURCE]
        source_docs = [doc for doc in documents if doc.doc_id == source_doc_id]
        if len(source_docs) == 0:
            print("no source doc found")
            continue
        elif len(source_docs) == 1:
            source_doc = source_docs[0]
            print(source_doc.get_text())
        else:
            print("More than one source doc found")
            continue

def show_response(response: RESPONSE_TYPE):
    print("============================================================================================================")
    print(response)
    print("============================================================================================================")
    for node in response.source_nodes:
        if node.node.extra_info is not None:
            if "file_name" in node.node.extra_info:
                print(node.node.extra_info["file_name"])
        print(node.node.node_info)
        if node.score is not None:
            print(node.score)
        print("============================================================================================================")
        print(node.node.text)
        print("============================================================================================================")

def query_with_index(
    folderpath_documents: str,
    folderpath_index: str,
    filepath_cache_metadata: str,
    filepath_cache_index: str,
    chunk_size_limit: int,
):
    # select docments
    filename_fn = lambda filename: {"file_name": filename}
    directory_reader = SimpleDirectoryReader(
        folderpath_documents,
        file_metadata=filename_fn,
    )
    filenames = [str(input_file) for input_file in directory_reader.input_files]

    # decide whether to use cache
    use_cache = False
    if os.path.exists(filepath_cache_metadata):
        metadata = json.loads(open(filepath_cache_metadata, "r").read())
        filenames_ = metadata["filenames"]
        if sorted(filenames_) == sorted(filenames):
            use_cache = True

    # load index
    if use_cache:
        storage_context = StorageContext.from_defaults(persist_dir=filepath_cache_index)
        index = load_index_from_storage(storage_context)
    else:
        # read documents
        documents = directory_reader.load_data()

        # make index
        service_context = ServiceContext.from_defaults(chunk_size_limit=chunk_size_limit)
        storage_context = StorageContext.from_defaults()
        index = GPTVectorStoreIndex.from_documents(documents, storage_context=storage_context, service_context=service_context,)

        # save cache
        os.makedirs(folderpath_index, exist_ok=True)
        index.storage_context.persist(filepath_cache_index)
        json.dump({"filenames": filenames}, open(filepath_cache_metadata, "w"), indent=2)

    # query with index
    query_engine = index.as_query_engine()
    response = query_engine.query("What is Workplace Violence?")

    return response

def main():
    # settings for paths
    folderpath_index = os.path.join("data", "indexes")
    folderpath_documents = os.path.join("data", "documents")
    filepath_cache_metadata = os.path.join(folderpath_index, "metadata.json")
    filepath_cache_index = os.path.join(folderpath_index, "index.json")
    chunk_size_limit = 512

    response = query_with_index(
        folderpath_documents,
        folderpath_index,
        filepath_cache_metadata,
        filepath_cache_index,
        chunk_size_limit,
    )

    show_response(response)

if __name__ == "__main__":
    main()
```

### 元コードとの差分
GPTSimpleVectorIndexの名前がGPTVectorStoreIndexに変わり､併せてQuery Engineをとおしてクエリを投げるようになったようです｡インデックスの登録､呼び出し方法も変更されたため､それにあわせて修正しました｡

```diff
5,6c5,6
< from llama_index import Document, GPTSimpleVectorIndex, ServiceContext, SimpleDirectoryReader
< from llama_index.data_structs.node_v2 import DocumentRelationship
---
> from llama_index import (Document, GPTVectorStoreIndex, ServiceContext, SimpleDirectoryReader, StorageContext, load_index_from_storage)
> from llama_index.data_structs.node import DocumentRelationship
47d46
<     similarity_top_k: int,
67c66,67
<         index = GPTSimpleVectorIndex.load_from_disk(filepath_cache_index)
---
>         storage_context = StorageContext.from_defaults(persist_dir=filepath_cache_index)
>         index = load_index_from_storage(storage_context)
74c74,75
<         index = GPTSimpleVectorIndex.from_documents(documents, service_context=service_context)
---
>         storage_context = StorageContext.from_defaults()
>         index = GPTVectorStoreIndex.from_documents(documents, storage_context=storage_context, service_context=service_context,)
78c79
<         index.save_to_disk(filepath_cache_index)
---
>         index.storage_context.persist(filepath_cache_index)
82c83,84
<     response = index.query("What is Workplace Violence?", similarity_top_k=similarity_top_k)
---
>     query_engine = index.as_query_engine()
>     response = query_engine.query("What is Workplace Violence?")
93d94
<     similarity_top_k = 1
101d101
<         similarity_top_k,
```

# 結果
元記事と同じ結果が得られました｡元記事では参照箇所が1ヶ所だけですが､今回実行したコードでは､参照したPDFの同一ページに書かれてる`We are committed`で始まる文章も参照箇所として挙がっていました｡この文章も質問文章に対するリファレンスの類似度が高いと判断されたようです｡

```text
============================================================================================================

Workplace violence is any act of physical aggression, intimidation, or threat of physical harm toward another individual in the workplace. This includes but is not limited to physical assault, threats of violence, verbal abuse, intimidation, harassment, bullying, stalking, and any other behavior that creates a hostile work environment.
============================================================================================================
data/documents/employee_handbook.pdf
{'start': 0, 'end': 2061, '_node_type': '1'}
0.8621174877234363
============================================================================================================

At Contoso Electronics, we are committed t o providing a safe, respectful and healthy
workplace for all of our employees. In order to ensure that we maintain this, we have
developed a comprehensive Workplace Violence Prevention Program.

Purpose

The purpose of this program is to promote a safe and  healthy work environment by
preventing violence, threats, and abuse in the workplace. It is also intended to provide a
safe, secure and protected environment for our employees, customers, and visitors.

Definition of Workplace Violence

Workplace violence  is any act of physical aggression, intimidation, or threat of physical
harm toward another individual in the workplace. This includes but is not limited to
physical assault, threats of violence, verbal abuse, intimidation, harassment, bullying,
stalking, and any other behavior that creates a hostile work environment.

Prevention and Response

Contoso Electronics is committed to preventing workplace violence and will not tolerate
any acts of violence, threats, or abuse in the workplace. All employees are ex pected to
follow the company’s zero tolerance policy for workplace violence.

If an employee believes that they are in danger or are the victim or witness of workplace
violence, they should immediately notify their supervisor or Human Resources
Representat ive. Employees are also encouraged to report any suspicious activity or
behavior to their supervisor or Human Resources Representative.

In the event of an incident of workplace violence, Contoso Electronics will respond
promptly and appropriately. All inc idents will be thoroughly investigated and the
appropriate disciplinary action will be taken.

Training and Education

Contoso Electronics will provide regular training and education to all employees on
workplace violence prevention and response. This trai ning will include information on
recognizing potential signs of workplace violence, strategies for responding to incidents,
and the company’s zero tolerance policy.

We
============================================================================================================
data/documents/employee_handbook.pdf
{'start': 1999, 'end': 2272, '_node_type': '1'}
0.8255657810388666
============================================================================================================
to incidents,
and the company’s zero tolerance policy.

We are committed to creating a safe and secure work environment for all of our employees.
By following the guidelines outlined in this program, we can ensure that our workplace is
free from violence and abuse.
============================================================================================================
```


# その他
今回のコードでは､テキスト応答を得るための言語モデルがデフォルトの`text-davinci-003`であったり､プロンプトが指定されていないなどカスタマイズが入っていない状態です｡カスタマイズには､[LlamaIndexを完全に理解するチュートリアル その１：処理の概念や流れを理解する基礎編（v0.6.8対応）](https://dev.classmethod.jp/articles/llamaindex-tutorial-001-overview/) から続くチュートリアルや､その他の[LlamaIndexの記事](https://dev.classmethod.jp/tags/llamaindex/)がとても参考になります｡