RAGを使ってModelerスクリプトを生成②スクリプト生成

# はじめに
@nishikyonさんの[この記事](https://qiita.com/nishikyon/items/8fa659d6392357925a92)を参考にして、生成AIのRAGを使って[Modelerスクリプトのマニュアル](https://www.ibm.com/docs/ja/SS3RA7_18.5.0/nl/ja/pdf/ModelerScriptingAutomation.pdf")を検索して、Modelerスクリプトを生成してみます。

記事は以下の二本からなります。
[①コレクションの作成](https://qiita.com/spssfun2017/items/5c64bf5dcd3424d422f3)
②スクリプト生成（この記事）

前の記事で[①コレクションの作成](https://qiita.com/spssfun2017/items/5c64bf5dcd3424d422f3)を行いました。watsonx.dataのMilvusというベクターDBに、ModelerスクリプトのPDFマニュアルを登録したので、この記事では②スクリプト生成を行います。

RAGでwatsonx.dataのModelerスクリプトのPDFマニュアル検索した結果を用いて、生成AIのwatsonx.aiでModelerスクリプトを生成します。

![{5BC0028E-E774-44AC-876E-2D68C89E175F}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/82869085-fa3e-043c-6872-8c14eb1da3c2.png)


この記事は[SPSS秋のユーザーイベント2024](https://www.ibm.com/blogs/solutions/jp-ja/smuc-2024-autumn/)の[東日本旅客鉄道の堀様のご講演の「ものづくり領域で活かされるSPSSの今とこれから-2異常検知と自動化」](https://speakerdeck.com/jpspss/20241127spssqiu-02-dong-ri-ben-lu-ke-tie-dao-ku-yang-zi-liao)でご紹介したデモ③「ModelerのマニュアルをRAGで参照してスクリプトを生成」の解説です。


■テスト環境
* Modeler 18.5
* Windows 11 64bit
* python:3.11.9
* ibm_watsonx_ai:1.1.24
* langchain:0.3.9
* langchain_community:0.3.9
* pypdf:5.1.0
* langchain_huggingface:0.1.2
* langchain_milvus:0.1.7
* langchain-ibm:0.3.5



# 1. 事前準備
前の記事を参考にspss_pdfのコレクションを作っておいてください。Attuでみて、以下のようなコレクションがあれば、正しく作成されているのでOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/3dd6f2a6-c233-0d6a-231c-acb6b0bffc04.png)

# 2. スクリプト生成のPythonのプログラム
## 2-1. Milvusへの接続情報をセット
[①の記事](https://qiita.com/spssfun2017/items/5c64bf5dcd3424d422f3#2-1-milvus%E3%81%B8%E3%81%AE%E6%8E%A5%E7%B6%9A%E6%83%85%E5%A0%B1%E3%82%92%E3%82%BB%E3%83%83%E3%83%88)と同様です。

wxdata_host:GRPCホスト
wxdata_port=GRPCポート
wxdata_user='ibmlhapikey' ←固定値
wxdata_apikey=watsonx.dataに接続できるAPIキー　←パスワードとして使用します。



```python:Milvusへの接続情報
wxdata_host='xxxxxxxxxxxxxxxxxxxxxxx.lakehouse.appdomain.cloud' 
wxdata_port='xxxxx'
wxdata_user='ibmlhapikey'
wxdata_apikey='xxxxxxxxxxxxxxxxxxxxxxx'
```

接続するDB名（ここでは`default`）と①で作成したコレクション名（ここでは`spss_pdf`）を指定しておきます。

```python:DB名と作成するコレクション名
db_name ="default"

collection_name = "spss_pdf"
```

そしてコレクション名以外は、辞書形式にして`milvus_connection_args`に保存しています（以前はhostとportの指定で動きましたが、`langchain_milvus==0.1.7`ではuriでの指定が必要でした）。


```python:辞書形式のMilvus接続引数
milvus_connection_args ={
 'db_name':db_name,
 'uri': "https://"+wxdata_host+":"+wxdata_port,
 'user':wxdata_user,
 'password':wxdata_apikey,
 'secure':True
}
```
## 2-3. watsonx.aiへの接続情報をセット
[この記事](https://qiita.com/spssfun2017/items/e528d0f41749d32976fb)参考に、watsonx.aiへの接続情報をセットします。
watsonx.dataと同じアカウント、同じユーザーであれば同じAPIキーも使えます。

```python:watsonxaiへの接続情報
# watsonx.aiのAPIキー
wxai_apikey = 'xxxxxxxxxxxxxxxxxxxx' 
# watsonx.aiのプロジェクトID
project_id = 'xxxxxxxxxxxxxxxxxxxx'
# エンドポイントURL
wxai_url = 'https://xxxxxxxxxxxxxxxxxx' #'https://jp-tok.ml.cloud.ibm.com'
```

## 2-4. embeddingsの指定
[①の記事](https://qiita.com/spssfun2017/items/5c64bf5dcd3424d422f3#2-2-embeddings%E3%81%AE%E6%8C%87%E5%AE%9A)と同様です。
必ず、コレクションを作成したときと同じembeddingsを指定します。

```python:embeddingsの取得
from langchain_huggingface import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-mpnet-base-v2")
```

## 2-5. コレクションへの接続
①の記事で作ったコレクション`spss_pdf`に接続します。

```python:Milvusのコレクションへの接続
from langchain_milvus import Milvus
vector_store = Milvus(
    embeddings,
    connection_args=milvus_connection_args,
    collection_name=collection_name
)
```


## 2-6. watsonx.aiのLLMに接続
watsonx.aiのLLMに接続します。
ここでは`llama-3-1-70b-instruct`のモデルを使います。

```python:watsonx.aiのLLMに接続
from ibm_watsonx_ai.metanames import GenTextParamsMetaNames as GenParams
from langchain_ibm import WatsonxLLM

# 使用するLLMのパラメータ
generate_params = {
    GenParams.MAX_NEW_TOKENS: 500,
    GenParams.MIN_NEW_TOKENS: 0,
    GenParams.DECODING_METHOD: "greedy",
    GenParams.REPETITION_PENALTY: 1.06
}

# LangChainで使うllm
custom_llm = WatsonxLLM(
    model_id="meta-llama/llama-3-1-70b-instruct", 
    url=wxai_url,
    apikey=wxai_apikey,
    project_id=project_id,
    params=generate_params,
)
```

## 2-7. RAGなしでModelerスクリプトを生成：ヒストグラム表示
それでは、Modelerスクリプトを以下のプロンプトで生成します。
`可変長ファイルノードで"$CLEO_DEMOS/DRUG1n"を読み込み、ヒストグラムノードをつないで、Age列のヒストグラムを表示するModelerスクリプトをpythonで書いてください。`

最初はRAGを使わない場合にどうなるかを見てみます。
単にLLMに上のプロンプトをinvokeして実行しています。

```python:RAGなしでModelerスクリプトを生成：ヒストグラム表示
query="""可変長ファイルノードで"$CLEO_DEMOS/DRUG1n"を読み込み、ヒストグラムノードをつないで、Age列のヒストグラムを表示するModelerスクリプトをpythonで書いてください。""" 
result=custom_llm.invoke(query)
print(result)
```

生成されたスクリプトは以下です。


```python
import cleo

# CLEOのデモデータセットをロードします。
cleo.load_demos()

# ファイルノードを作成し、データセットを読み込みます。
file_node = cleo.FileNode()
file_node.file_path = "$CLEO_DEMOS/DRUG1n"

# ヒストグラムノードを作成し、ファイルノードに接続します。
histogram_node = cleo.HistogramNode()
histogram_node.input = file_node.output

# ヒストグラムノードの設定を行います。
histogram_node.column_name = "Age"
histogram_node.bin_count = 10

# Modelerを実行し、結果を取得します。
modeler = cleo.Modeler()
modeler.nodes = [file_node, histogram_node]
result = modeler.run()

# 結果を表示します。
print(result)
```

一見、それっぽく見えるかもしれませんが、Modelerスクリプトに設定して実行してみると、いきなり`import cleo`が存在しないので動きません。また、メソッドも存在しないものばかりです。ハルシネーションが起きています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/3d18e625-bc6b-3b87-9eee-87f6b677ca2d.png)


## 2-8. RAGでModelerスクリプトを生成：ヒストグラム表示
それではRAGでModelerスクリプトを生成してみます。
今度は`RetrievalQA.from_chain_type`でLLMとvector_storeのretrieverをセットで与えています。
また、`return_source_documents=True`で検索したマニュアルの情報も取り出しています。

```python:Milvusのコレクションへの接続
from langchain.chains import RetrievalQA

retriever = vector_store.as_retriever()
qa = RetrievalQA.from_chain_type(llm=custom_llm, chain_type="stuff", retriever=retriever,return_source_documents=True)
answer = qa.invoke(query)
print(answer['result'])
```
生成されたスクリプトは以下です。

```python
stream = modeler.script.stream()
# 可変長ファイルノードを作成
varfilenode = stream.createAt("variablefile", "DRUG Data", 96, 96)
varfilenode.setPropertyValue("full_filename", "$CLEO_DEMOS/DRUG1n")

# ヒストグラムノードを作成
histogramnode = stream.createAt("histogram", "Histogram", 192, 96)

# 可変長ファイルノードとヒストグラムノードをつなげる
stream.link(varfilenode, histogramnode)

# ヒストグラムノードのプロパティを設定
histogramnode.setPropertyValue("fields", ["Age"])

# ストリームを実行
stream.run()
```

Modelerスクリプトに設定して実行してみると、ノードは作成されましたが、途中でエラーになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/6d2064bd-816e-19a5-32f8-eafccbb22eef.png)

ヒストグラムノードは単一フィールドしか受け取れないので`fields`というプロパティがないというエラーでした。以下のように修正します。
`fields`→`field`
 `["Age"]`→ `"Age"`
```python
# ストリームを実行
histogramnode.setPropertyValue("field", "Age")
```

修正して再度実行してみると、Ageの設定はできましたが、ストリーム実行で失敗しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/eec28485-0487-36d2-2a4d-da1a67e8f7c5.png)

ヒストグラムノードだけを実行すればいいのと、戻り値は受け取らなくてよいので以下のように修正します。
```python
# ヒストグラムノードのプロパティを設定
histogramnode.run(None)
```

再修正して、実行すると、うまく実行できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/b9a9626a-2a8a-256a-91c5-09408a75bdb4.png)

一部修正が必要でしたが、かなり使えるスクリプトを生成できたことがわかります。


## 2-9. ソース情報の確認
マニュアルのどこを参照したのかを表示してみます。
`RetrievalQA.from_chain_type`で`return_source_documents=True`を指定したので、`source_documents`に参照したマニュアルのページ情報が取れています。

```python
# ソースの情報
source_docs=answer["source_documents"]
for doc in source_docs:
    print("Page {:3d}: {:s}".format(doc.metadata['page'],repr(doc.page_content[0:50])))
```

結果は以下です。
```
Page  66: '表 26. ノードおよび出力\nノード名 出力名 コンテナ ID\ntable table "table'
Page 483: 'サポート・ベクトル・マシン・モデル\nノードのスクリプト・プロパティー 307\n散布図ノード\nプロパテ'
Page 449: '表 269. tsnenode プロパティー (続き)\ntsnenode プロパティー データ型 プ'
Page  41: '表 12. ノードを作成するためのメソッド (続き)\n方法 戻り値の型 説明\ns.createAt('
```

以下のページが最も近いと判断したようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/73aa1cb1-7861-eaca-e42b-acffb63808b1.png)

この検索結果から検索やプロンプトのチューニングを考えることができます。

## 2-10. RAGでModelerスクリプトを生成：income平均計算
もう一つ別のプロンプトで、今度はincomeの平均を計算させてみます。
プロンプトを変えただけで、RAGでスクリプトを生成させるプログラムのコードは同じです。

```python
# RAGでModelerスクリプトを生成：income平均計算
query="""以下のModelerスクリプトをmodeler.apiを用いてpythonで書いてください。
$CLEO_DEMOS/telco.savをfull_filenameとしてstatisticsimportで読込み、
aggregateノードにリンクし、"region"をキーにして、setKeyedPropertyValueをつかってincomeの平均値を算出してください。
そして、テーブルノードにリンクして下さい""" 

from langchain.chains import RetrievalQA

retriever = vector_store.as_retriever()
qa = RetrievalQA.from_chain_type(llm=custom_llm, chain_type="stuff", retriever=retriever,return_source_documents=True)
answer = qa.invoke(query)
print(answer['result'])
```

生成されたスクリプトは以下です。

```python
import modeler.api

# ストリームの取得
stream = modeler.script.stream()

# statisticsimportノードの作成
stats_import_node = stream.createAt("statisticsimport", "Import", 96, 96)

# full_filenameの設定
stats_import_node.setPropertyValue("full_filename", "$CLEO_DEMOS/telco.sav")

# aggregateノードの作成
aggregate_node = stream.createAt("aggregate", "Aggregate", 192, 96)

# statisticsimportノードからaggregateノードへのリンク
stream.link(stats_import_node, aggregate_node)

# キーの設定
aggregate_node.setPropertyValue("keys", ["region"])

# incomeの平均値の算出
aggregate_node.setKeyedPropertyValue("aggregates", "income", ["Mean"])

# テーブルノードの作成
table_node = stream.createAt("table", "Table", 288, 96)

# aggregateノードからテーブルノードへのリンク
stream.link(aggregate_node, table_node)
```

Modelerスクリプトに設定して実行してみると、エラーなく、ノードが作られました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/396a7d74-6b65-4907-6c50-c81d330030aa.png)

集計ノードを確認してみると、正しくincomeの平均をとっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/56e8c2ff-3fc2-c417-3bb7-a169f90503fc.png)

実行してみると、正しく動きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/61e06e30-1f49-acd6-82a1-3607c5e2b24b.png)



# 3. まとめ

RAGをつかってModelerスクリプトのマニュアルを読ませることで、なかなかの精度のスクリプトを生成することができました。
Qiita記事なども読ませたりすれば、もっと精度高く生成することができるのではないかと思います。


# 4. サンプルプログラム

https://github.com/hkwd/220201watsonstudio/blob/master/241220RAGModelerScript/watsonx-langchain-milvus-PDF_spss2_02GenScript_Pub.ipynb

# 5. 参考
#### 20241127SPSS秋02_東日本旅客鉄道 堀様資料
https://speakerdeck.com/jpspss/20241127spssqiu-02-dong-ri-ben-lu-ke-tie-dao-ku-yang-zi-liao

#### RAGを使ってModelerスクリプトを生成①コレクションの作成 #SPSS_Modeler - Qiita 

https://qiita.com/spssfun2017/items/5c64bf5dcd3424d422f3

#### watsonx.aiのLLMでLangChainとMilvusを使ってPDFの内容をQ&Aしてみた(=RAG)

https://qiita.com/nishikyon/items/8fa659d6392357925a92/