Databricks上でLlamaIndexのフルスタックWebアプリケーションを動かしてみる

さらにこちらの続きです。

https://qiita.com/taka_yayoi/items/ef62d14a4f7d4874e68c

自分のデータを使ってRAGを構築したら、GUIと組み合わせたくなるのが人情(?)です。

マニュアルにも[Putting it All Together(すべてをまとめる)のセクション](https://docs.llamaindex.ai/en/stable/understanding/putting_it_all_together/putting_it_all_together.html)があります。

こちらを参考に実装します。

[A Guide to Building a Full-Stack Web App with LLamaIndex](https://docs.llamaindex.ai/en/stable/understanding/putting_it_all_together/apps/fullstack_app_guide.html)

これまでのおさらいを兼ねて、コードをウォークスルーします。

# ライブラリのインストール

```py
%pip install -U llama-index
%pip install "databricks-sql-connector[sqlalchemy]"
dbutils.library.restartPython()
```

# Unity Catalogとの接続

クラスターのHTTPパス、カタログ名、スキーマ名は適宜変更してください。

```py
from llama_index import download_loader
from llama_index.utilities.sql_wrapper import SQLDatabase

DatabaseReader = download_loader('DatabaseReader')

access_token    = dbutils.notebook.entry_point.getDbutils().notebook().getContext().apiToken().getOrElse(None)
server_hostname = "<Databricksワークスペースのホスト名>"
http_path       = "sql/protocolv1/o/2556758628403379/1118-004519-nu0u899x"
catalog         = "takaakiyayoi_catalog"
schema          = "qiita_2023"

reader = DatabaseReader(
    sql_database = SQLDatabase.from_uri(f"databricks://token:{access_token}@{server_hostname}?" +
        f"http_path={http_path}&catalog={catalog}&schema={schema}")
)
```

# テキストデータのロード

テキストを格納しているテーブルに応じてSQLを変更してください。

```py
query = f"""
SELECT
    body
FROM takaakiyayoi_catalog.qiita_2023.taka_qiita_2023
"""

documents = reader.load_data(query=query)
```

# インデックスの作成

OpenAIのAPIを使っています。Azure OpenAIを使う際には[こちら](https://qiita.com/taka_yayoi/items/88ec0e401bf7b2f3057a)を参考にしてください。

```py
import os
os.environ["OPENAI_API_KEY"] = dbutils.secrets.get("demo-token-takaaki.yayoi", "openai_api_key")

from llama_index import VectorStoreIndex, SimpleDirectoryReader

index = VectorStoreIndex.from_documents(documents)
```

# RAGへの問い合わせ

動作確認します。[ウィジェット](https://qiita.com/taka_yayoi/items/00042df409f52f750680)を使ってます。

```py
query_engine = index.as_query_engine()
response = query_engine.query(dbutils.widgets.get("prompt"))
print("回答: ", response)
```

![Screenshot 2024-01-21 at 10.19.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/450fa6db-6398-fd26-9079-1fed7a89ca14.png)

> 回答:  Databricksの利点は、以下のようなものです：
> - Apache Sparkを基にしており、大規模データの効率的な処理が可能です。
> - クラウドベースのプラットフォームであり、データの処理を容易にします。
> - データの共有や協力を容易にするための強力なコラボレーションツールがあります。
> - Machine Learningのための強力なツールがあります。
> - データの管理や安全性を確保するための強力なデータ管理機能があります。
> - 各種データソースとの統合が可能です。

# Flask

ここからが新規パートです。事前に以下のポートを決めておきます。

- Flaskのポート: ここでは1501
- streamlitのポート: ここでは1502

動作確認のためFlaskのURLを表示させます。ここでは[ドライバープロキシー](https://qiita.com/taka_yayoi/items/f7538636e88bc8973769)の機能を使っています。なお、この時点で表示されているURLにアクセスしても動作しません。

```py
from dbruntime.databricks_repl_context import get_context

def front_url(port):
    """
    フロントエンドを実行するための URL を返す

    Returns
    -------
    proxy_url : str
        フロントエンドのURL
    """
    ctx = get_context()
    proxy_url = f"https://{ctx.browserHostName}/driver-proxy/o/{ctx.workspaceId}/{ctx.clusterId}/{port}/"

    return proxy_url

PORT = 1501

# Driver ProxyのURLを表示
print(front_url(PORT))

# 利便性のためにリンクをHTML表示
displayHTML(f"<a href='{front_url(PORT)}' target='_blank' rel='noopener noreferrer'>別ウインドウで開く</a>")
```

以下を実行して、Flaskサーバーを起動します。

```py
from flask import Flask, request

app = Flask("llama_index")

@app.route("/", methods=["GET"])
def query_index():
    global index
    query_text = request.args.get("text", None)
    if query_text is None:
        return (
            "テキストが入力されていません。URLに ?text=blah パラメータを含めてください",
            400,
        )
    query_engine = index.as_query_engine()
    response = query_engine.query(query_text)
    return str(response), 200
  

app.run(host="0.0.0.0", port=PORT)
```

上で表示されたURLにアクセスします。質問が入力されていない旨のメッセージが表示されます。
![Screenshot 2024-01-21 at 10.22.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/f03eb044-ac20-c517-a04f-c27eed27e7ee.png)

URLの末尾に以下のようにパラメータを指定します。

> .../1501/?text=クラスターのおすすめは

以下のように結果を確認できます。これでサーバーが稼働している状態になりました。

> シングルユーザーアクセスモードのクラスターがおすすめです。このクラスターは、使用できる機能に制限がなく、単一のユーザーに割り当てて利用することができます。ただし、ユーザーごとにクラスターが作成されるため、管理の手間が増えることに注意が必要です。

# streamlit

あとは画面です。別のファイルとしてpyファイルを作成します。**作成 > ファイル**を選択して、以下のファイルを作成します。テキストのやり取りなのでシンプルです。

```py:streamlit.py
import streamlit as st 
import numpy as np 
import json
import requests

st.title('Databricks Q&A bot')
#st.header('Databricks Q&A bot')

def generate_answer(question):
  # Driver Proxyと異なるクラスター、ローカルからDriver Proxyにアクセスする際にはパーソナルアクセストークンを設定してください
  token = "" 
  url = f"http://127.0.0.1:1501/?text={question}"

  headers = {
      "Content-Type": "application/json",
      "Authentication": f"Bearer {token}"
  }

  response = requests.get(url, headers=headers)
  if response.status_code != 200:
    raise Exception(
       f"Request failed with status {response.status_code}, {response.text}"
    )
  
  response_text = response.text
  return response_text

question = st.text_input("**質問**")

if question != "":
  response = generate_answer(question)

  st.write(f"**回答:** {response}")
```

streamlitを稼働させるために、今度は別のノートブックを作成します。

```py
%pip install streamlit
dbutils.library.restartPython()
```

こちらはstreamlit用のポートを指定します。

```py
from dbruntime.databricks_repl_context import get_context

def front_url(port):
    """
    フロントエンドを実行するための URL を返す

    Returns
    -------
    proxy_url : str
        フロントエンドのURL
    """
    ctx = get_context()
    proxy_url = f"https://{ctx.browserHostName}/driver-proxy/o/{ctx.workspaceId}/{ctx.clusterId}/{port}/"

    return proxy_url

PORT = 1502

# Driver ProxyのURLを表示
print(front_url(PORT))

# 利便性のためにリンクをHTML表示
displayHTML(f"<a href='{front_url(PORT)}' target='_blank' rel='noopener noreferrer'>別ウインドウで開く</a>")
```

上で作成したstreamlitのファイルを指定して、streamlitを起動します。

```py
streamlit_file = "/Workspace/Users/takaaki.yayoi@databricks.com/20240116_llama_index/streamlit.py"

!streamlit run {streamlit_file} --server.port {PORT}
```

上で表示されたリンクにアクセスします。
![Screenshot 2024-01-21 at 10.27.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/6a2b0d9f-329e-9654-841d-a24b33cbed0c.png)

動きました！
![Screenshot 2024-01-21 at 10.27.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/e2529529-15fe-5253-68b7-a52f5204a4b1.png)

当たり前ですが、Databricksの記事に特化しているので、質問にDatabricksを含めなくてもDatabricksに関する答えが返ってきます。
![Screenshot 2024-01-21 at 10.33.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/25e80a78-46bb-1b0e-14ce-57dbc918242d.png)
![Screenshot 2024-01-21 at 10.42.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/ec540622-5afd-5e2b-e368-183f0675efbd.png)

RAGアプリケーションを動かすという目的であれば、LlamaIndexだと手軽に実装できて便利ですね。

### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)