[AI] LlamaIndexアプリ製作 その9 max_tokensの適切な設定

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

以前LangChainライブラリにあるOpenAIクラスの設定でmax_tokensを3500以上の大きな数値にして不具合が生じていましたが、2048以下にすると正常動作しました。上限はまだ調べていません。

デフォルトでは256になっていて回答が尻切れになるケースがあり困っていましたが、これで解決です。

ネットで収集した料理レシピを1つのPDFファイルにまとめてインデックスファイルにするつもりです。ただ広告も取り込んでいるためそれらを消去する必要があり、下準備に結構時間が掛かりそうです。あと似たような料理でレシピの内容が混ざらないかチェックが必要ですね。

def loadIDX(self):
    # インデックスの読込
    llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-davinci-003", max_tokens=2048))
    service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)
    
    choice = self.choice.currentText()
    
    if choice == "C++逆引き":
        index_path = "/AI/LlamaIndex/index/index_C++逆引き.json"
    elif choice == "Python逆引き":
        index_path = "/AI/LlamaIndex/index/index_Python逆引き.json"
    else:
        index_path = self.input2.text()

    try:
        self.index = GPTSimpleVectorIndex.load_from_disk(save_path= index_path, service_context=service_context)
    except Exception as e:
        print('エラーが発生しました:', e)
        self.output.setText(str(e))
        self.box.setStyleSheet('background-color: #ff00ff')
        return
    else:
        self.box.setStyleSheet('background-color: #00ffff')
        
    self.output.setText("IDX読込完了")

[AI] LlamaIndexアプリ製作 その8 例外処理

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

LlamaIndexアプリが簡単には落ちないよう例外処理を強化しました。

def makeIDX(self):
    # インデックスの作成および保存
    llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-embedding-ada-002"))
    service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)
    
    data_path = self.input.text()
    data_path2 = data_path.replace("'", "") # 拡張子判定用
    data_path3 = Path(data_path2) # loader用

    if data_path2.endswith('.csv'):
        SimpleCSVReader = download_loader("SimpleCSVReader")
        loader = SimpleCSVReader()
    elif data_path2.endswith('.pdf'):
        PDFReader = download_loader("PDFReader")
        loader = PDFReader()
    else:
        print('ファイルがcsv,pdfではありません')
        self.output.setText("ファイルがcsv,pdfではありません")
        self.box.setStyleSheet('background-color: #ff00ff')
        return

    try:
        nodes = loader.load_data(file=data_path3)
    except Exception as e:
        print('エラーが発生しました:', e)
        self.output.setText(str(e))
        self.box.setStyleSheet('background-color: #ff00ff')
        return
    else:
        self.box.setStyleSheet('background-color: #00ffff')

    index = GPTSimpleVectorIndex.from_documents(nodes, service_context=service_context)
    
    now = datetime.datetime.now()
    formatted_time = now.strftime('%y%m%d_%H%M%S')

    index_file = "/AI/LlamaIndex/index/" + formatted_time + "_index.json"
    index.save_to_disk(index_file)
    
    self.input2.setText(index_file)
    self.idxBtn2.click()

[Python] 354 PyQt6でドラッグ&ドロップ

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

PyQt6ではQLineEdit(FLTKにおけるFl_Input)にドラッグ&ドロップできるようにするには、QLineEditを継承したクラスを別途作成する必要があります。

機能が簡素なFLTKでもドラッグ&ドロップは標準装備されているのでこれは意外です。

from PyQt6.QtWidgets import QLineEdit

class MyLineEdit(QLineEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setDragEnabled(True)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        if event.mimeData().hasUrls():
            for url in event.mimeData().urls():
                file_path = url.toLocalFile()
                self.setText(file_path)
            event.accept()
        else:
            event.ignore()

[AI] LlamaIndexアプリ製作 その7 C++とPythonの逆引きレファレンス化

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

LlamaIndexアプリをC++やPyhonの逆引きレファレンスとして使えるようにしました。

読み込ませるインデックスファイルはコードの説明とコードの内容を対比させたCSVファイルをインデックス化したものです。C++は8行、Pythonはたった3行です。

インデックスファイルでこちらの求める回答形式を察してくれるので、言語の指定は不要です。

学習モデルの内容が2021年6月までですから、PyQt6については基本的には回答してくれないです。機嫌が良いと回答してくれることもあります。

全く別ジャンルの質問にも答えてくれます。専門ボットにもなれる単発質問対応AIといったところでしょうか。ChatGPTと比べて回答時間はかなり短いですが、コストはgpt-3.5-turboの10倍です。

Python用CSV

[AI] LlamaIndexアプリ製作 その6 動作確認2 max_tokensなしで正常動作

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

前回動作確認での不具合がささいなことで解決しました。

インデックスファイル読み込み時のllm_predictor設定でmax_tokensの設定を削除すると正常動作しました。

例えばmax_tokensを3500に設定すると5000 tokens以上の回答が返ってきてエラーになるのですが、設定しないとtext-davinci-003の最大である4097 tokens以内で返ってきます。

まあかなり複雑な構造のライブラリですからこれ位の不可解な動作は仕方ないでしょう。

なおChatGPT(gpt-3.5-turbo)では回答に23秒を要したのに対し、この専門Botでは当たり前ですがものの数秒で答えが返ってきます。

これでC++逆引きレファレンスCSVを完成させる決心がつきました。ただ別に完成させなくてもインデックスファイルの内容で傾向を学ぶようなので、今のたった8行のCSVでも十分な感じがします。

インデックスファイルにない内容でも素早く正確に答えてくれる
import os, logging, sys
from pathlib import Path
from PyQt6.QtWidgets import QLabel,QWidget,QApplication,QTextEdit,QLineEdit,QPushButton
from PyQt6.QtCore import Qt
from llama_index_fork import download_loader,LLMPredictor, GPTSimpleVectorIndex, ServiceContext, QueryMode
from langchain import OpenAI
import datetime

class LlamaIndex(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("LlamaIndex")
        self.setGeometry(100,100,480,480)
        self.setStyleSheet('background-color: #483D8B')
        self.setAcceptDrops(True)
        
        file = QLabel('FILE',self)
        file.setGeometry(10,15,34,16)
        file.setStyleSheet('color: #FFFFFF; font-size: 14pt;')
        file.setAlignment(Qt.AlignmentFlag.AlignCenter)
        
        self.input= QLineEdit('',self)
        self.input.setGeometry(55,10,355,25)
        self.input.setAcceptDrops(True)
        
        idxBtn = QPushButton('IDX作成',self)
        idxBtn.setGeometry(420,10,50,25)
        idxBtn.setStyleSheet('color: #FFFFFF; font-size: 12pt;')
        idxBtn.released.connect(self.makeIDX)
        
        idx = QLabel('IDX',self)
        idx.setGeometry(10,45,26,16)
        idx.setStyleSheet('color: #FFFFFF; font-size: 14pt;')
        idx.setAlignment(Qt.AlignmentFlag.AlignCenter)
        
        self.input2= QLineEdit('',self)
        self.input2.setGeometry(55,40,355,25)
        self.input2.setAcceptDrops(True)
        
        self.idxBtn2 = QPushButton('IDX読込',self)
        self.idxBtn2.setGeometry(420,40,50,25)
        self.idxBtn2.setStyleSheet('color: #FFFFFF; font-size: 12pt;')
        self.idxBtn2.released.connect(self.loadIDX)
        
        send = QPushButton('送信',self)
        send.setGeometry(420,70,50,25)
        send.setStyleSheet('color: #FFFFFF; font-size: 14pt;')
        send.released.connect(self.sendQuestion)
        
        clear = QPushButton('CL',self)
        clear.setGeometry(420,100,50,25)
        clear.setStyleSheet('color: #FFFFFF; font-size: 14pt;')
        clear.released.connect(self.clear)
            
        self.questionInput = QTextEdit('',self)
        self.questionInput.setGeometry(10,70,400,95)
        self.questionInput.setAcceptDrops(False)
        
        self.output = QTextEdit('',self)
        self.output.setGeometry(10,170,460,305)
        self.output.setAcceptDrops(False)
        
        # APIキーを環境変数から取得
        apiKey = os.getenv("CHATGPT_API_KEY")
        os.environ["OPENAI_API_KEY"] = apiKey

        # ログレベルの設定(DEBUG)
        logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True)
        logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

    def makeIDX(self):
        # インデックスの作成および保存
        llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-embedding-ada-002"))
        service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)
        
        data_path = self.input.text()
        data_path2 = data_path.replace("'", "") # 拡張子判定用
        data_path3 = Path(data_path2) # loader用

        if data_path2.endswith('.csv'):
            SimpleCSVReader = download_loader("SimpleCSVReader")
            loader = SimpleCSVReader()
        elif data_path2.endswith('.pdf'):
            PDFReader = download_loader("PDFReader")
            loader = PDFReader()
        else:
            print('ファイルがcsv,pdfではありません')
            sys.exit()

        nodes = loader.load_data(file=data_path3)
        index = GPTSimpleVectorIndex.from_documents(nodes, service_context=service_context)
        
        now = datetime.datetime.now()
        formatted_time = now.strftime('%y%m%d_%H%M%S')

        index_file = "/AI/LlamaIndex/index/" + formatted_time + "_index.json"
        index.save_to_disk(index_file)
        
        self.input2.setText(index_file)
        self.idxBtn2.click()

    def loadIDX(self):
        # インデックスの読込
        # llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-davinci-003", max_tokens=3500, verbose=False))
        llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-davinci-003"))
        service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)
        
        index_path = self.input2.text()

        self.index = GPTSimpleVectorIndex.load_from_disk(save_path= index_path, service_context=service_context)
        self.output.setText("IDX読込完了")
        
    def sendQuestion(self):
        question = self.questionInput.toPlainText()
        response = self.index.query(question)
        
        print(response)
        self.output.setText(str(response))

    def clear(self):
        self.input.clear()
        self.input2.clear()
        self.questionInput.clear()
        self.output.clear()

app = QApplication(sys.argv)
window = LlamaIndex()
window.show()
app.exec()

[AI] LlamaIndexアプリ製作 その5 動作確認1

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

一応完成させたLlamaIndexアプリですが、早速C++の逆引きレファレンスCSVを読み込ませて使ってみました。

ログを見ると最初の回答で十分な感じなんですがそれを出力するわけでもなく、何回かリファインして最後にエラーになります。リファインの回数を設定できれば良いのですが。

使いこなすにはまだまだ時間が掛かりそうです。

ところで4/8(土)頃からChatGPTの回答時間が極端に長くなることがあります。その前は大分ましになっていたので残念です。

C++逆引きレファレンスCSV(作成中)
ChatGPT 最近の回答時間(右下グラフ)

[AI] LlamaIndexアプリ製作 その4 完成 PyQt6

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

LlamaIndexアプリを完成させました。

exeファイルやappファイルには出来ませんでしたが、シェルスクリプトにするとアプリ風になるのでこれでけりを付けました。

ChatGPT APIを使い出して1ヶ月と1週が経過し、私自身だいぶ落ち着きを取り戻しつつあります。

import os, logging, sys
from pathlib import Path
from PyQt6.QtWidgets import QLabel,QWidget,QApplication,QTextEdit,QLineEdit,QPushButton
from PyQt6.QtCore import Qt
from llama_index import download_loader,LLMPredictor, GPTSimpleVectorIndex, ServiceContext
from langchain import OpenAI
import datetime

class LlamaIndex(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("LlamaIndex")
        self.setGeometry(100,100,480,480)
        self.setStyleSheet('background-color: #483D8B')
        self.setAcceptDrops(True)
        
        file = QLabel('FILE',self)
        file.setGeometry(10,15,34,16)
        file.setStyleSheet('color: #FFFFFF; font-size: 14pt;')
        file.setAlignment(Qt.AlignmentFlag.AlignCenter)
        
        self.input= QLineEdit('',self)
        self.input.setGeometry(55,10,355,25)
        self.input.setAcceptDrops(True)
        
        idxBtn = QPushButton('IDX作成',self)
        idxBtn.setGeometry(420,10,50,25)
        idxBtn.setStyleSheet('color: #FFFFFF; font-size: 12pt;')
        idxBtn.released.connect(self.makeIDX)
        
        idx = QLabel('IDX',self)
        idx.setGeometry(10,45,26,16)
        idx.setStyleSheet('color: #FFFFFF; font-size: 14pt;')
        idx.setAlignment(Qt.AlignmentFlag.AlignCenter)
        
        self.input2= QLineEdit('',self)
        self.input2.setGeometry(55,40,355,25)
        self.input2.setAcceptDrops(True)
        
        self.idxBtn2 = QPushButton('IDX読込',self)
        self.idxBtn2.setGeometry(420,40,50,25)
        self.idxBtn2.setStyleSheet('color: #FFFFFF; font-size: 12pt;')
        self.idxBtn2.released.connect(self.loadIDX)
        
        send = QPushButton('送信',self)
        send.setGeometry(420,70,50,25)
        send.setStyleSheet('color: #FFFFFF; font-size: 14pt;')
        send.released.connect(self.sendQuestion)
        
        clear = QPushButton('CL',self)
        clear.setGeometry(420,100,50,25)
        clear.setStyleSheet('color: #FFFFFF; font-size: 14pt;')
        clear.released.connect(self.clear)
            
        self.questionInput = QTextEdit('',self)
        self.questionInput.setGeometry(10,70,400,95)
        self.questionInput.setAcceptDrops(False)
        
        self.output = QTextEdit('',self)
        self.output.setGeometry(10,170,460,305)
        self.output.setAcceptDrops(False)
        
        # APIキーを環境変数から取得
        apiKey = os.getenv("CHATGPT_API_KEY")
        os.environ["OPENAI_API_KEY"] = apiKey

        # ログレベルの設定(DEBUG)
        logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True)
        logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

    def makeIDX(self):
        # インデックスの作成および保存
        llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-embedding-ada-002"))
        service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)
        
        data_path = self.input.text()
        data_path2 = data_path.replace("'", "") # 拡張子判定用
        data_path3 = Path(data_path2) # loader用

        if data_path2.endswith('.csv'):
            SimpleCSVReader = download_loader("SimpleCSVReader")
            loader = SimpleCSVReader()
        elif data_path2.endswith('.pdf'):
            PDFReader = download_loader("PDFReader")
            loader = PDFReader()
        else:
            print('ファイルがcsv,pdfではありません')
            sys.exit()

        nodes = loader.load_data(file=data_path3)

        index = GPTSimpleVectorIndex.from_documents(nodes, service_context=service_context)
        
        now = datetime.datetime.now()
        formatted_time = now.strftime('%y%m%d_%H%M%S')

        index_file = "/AI/LlamaIndex/index/" + formatted_time + "_index.json"
        index.save_to_disk(index_file)
        
        self.input2.setText(index_file)
        self.idxBtn2.click()

    def loadIDX(self):
        # インデックスの読込
        llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-davinci-003", max_tokens=3500))
        service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)
        
        index_path = self.input2.text()

        self.index = GPTSimpleVectorIndex.load_from_disk(save_path= index_path, service_context=service_context)
        self.output.setText("IDX読込完了")
        
    def sendQuestion(self):
        question = self.questionInput.toPlainText()
        response = self.index.query(question)
        
        print(response)
        self.output.setText(str(response))

    def clear(self):
        self.input.clear()
        self.input2.clear()
        self.questionInput.clear()
        self.output.clear()

app = QApplication(sys.argv)
window = LlamaIndex()
window.show()
app.exec()

[AI] LlamaIndexアプリ製作 その3 LlamaIndexライブラリ分析 pdbによるデバッグ

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

py2appによるpyファイルのappファイル化がうまくいかないので、LlamaIndexライブラリの内容を分析し、C++への移植を目指すことにしました。

分析にあたりLlamaIndexライブラリを改変し、デバッグ仕様にしています。

思っていたよりも内容が複雑で何度かくじけかけましたが、ようやく光が見えてきました。

Pythonスクリプトを走らせた時の出力の内容とLlamaIndexライブラリの関連がとても見えにくく、printデバッグで細かくスクリプトやライブラリの動作を追跡しないと何が何だか分かりません。

そんなこんなでCSVファイルをindex化する所までたどり着いたものの、ここでいよいよ分からなくなりPython用のデバッガpdbを使いました。

下図赤枠の所をrコマンド、nコマンド、sコマンドで刻んで進めていきましたが、どこまで行っても目的箇所へたどり着けず300行ぐらいで追いかけるのを断念しました。Pythonの標準ライブラリを多用していますし、とてもじゃないですがC++へ移植など工数がいくつあっても足りないです。

Embbedingについては他のアプローチを模索していきます。

由来が不明だった出力の根拠となるスクリプトをようやく見つけた
pdbによるデバッグ
import os, logging, sys
from pathlib import Path
# llama_index改変版
from llama_index_fork import download_loader,LLMPredictor, GPTSimpleVectorIndex, ServiceContext
from langchain import OpenAI
from mylib import MySimpleCSVReader
import pdb

# APIキーを環境変数から取得
apiKey = os.getenv("CHATGPT_API_KEY")
os.environ["OPENAI_API_KEY"] = apiKey

# ログレベルの設定(DEBUG)
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

# インデックスの作成および保存
llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-embedding-ada-002"))
service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)

print("ファイルパスを入力して下さい")
file_path = input()
file_path2 = file_path.replace("'", "") # 拡張子判定用
file_path3 = Path(file_path2) # loader用

if file_path2.endswith('.csv'):
	# SimpleCSVReader = download_loader("SimpleCSVReader")
	loader = MySimpleCSVReader.MySimpleCSVReader()
elif file_path2.endswith('.pdf'):
	PDFReader = download_loader("PDFReader")
	loader = PDFReader()
else:
	print('ファイルがcsv,pdfではありません')
	sys.exit()

# documentsの型は、List[Document]
documents = loader.load_data(file=file_path3)

print("documentsの内容")
num = 1
for doc in documents:
    print(str(num) + ": " + str(doc) + "\n")
    num = num + 1

# ブレークポイント設定
pdb.set_trace()

index = GPTSimpleVectorIndex.from_documents(documents, service_context=service_context)
print("\nindex: " + str(index) + "\n")

index.save_to_disk('index.json')

# インデックスの読込
llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-davinci-003", max_tokens=3500))
service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)

index = GPTSimpleVectorIndex.load_from_disk(save_path="index.json", service_context=service_context)

# 質問(Ctrl+cで終了)
while True :
  print("質問を入力して下さい")
  question = input()
  
  print(index.query(question))
  

[AI] LlamaIndexアプリ製作 その2 仮想環境を使ったアプリ軽量化 PyQt6 / py2app / virtualenv

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

py2appを使ったappファイル作成の際の出力をチェックするとどうやら無関係なライブラリも取り込んでいるようなので、仮想環境を使って軽量化を図りました。

今回はvirtualenvを使いました。venvは標準機能として入っていますが、システムのPythonから離れることは出来ません。Pythonのバージョンを自由に選べるvirtualenvの方が使い勝手は良さそうです。

Pythonの主な仮想環境:pyenv, venv, virtualenv

仮想環境を使って最低限のライブラリのみ取り込むようにした結果、248.3MBを213.8MBまで減らすことが出来ました。それでもまだまだ大きいですね。言語自体を変えないとこれ以上は厳しそうです。

py2appで作成したappファイルについてうまく起動できないケースが散見されますが、appファイルの内部にあるMacOSディレクトリに実行ファイルがありますので、これをダブルクリックするとターミナルが起動し、エラー出力の内容を確認できます。

# Python3.10の仮想環境envを作成
virtualenv -p python3.10 env

# ライブラリをインストール
./env/bin/pip install ライブラリ名

# 仮想環境envの有効化
source env/bin/activate

# 仮想環境envの無効化
deactivate
LlamaIndexプロジェクト内に仮想環境envを作成
appファイル起動トラブル時は中にある実行ファイルを起動してエラー出力を確認する

[AI] LlamaIndexアプリ製作 その1 PyQt6 / py2app

[M1 Mac, Monterey 12.6.3, Python 3.10.4]

LlamaIndexアプリの製作に着手しました。

簡単なアプリなのでそんなに工数は掛からないでしょう。取りあえずガワは作りました。

C++で書こうとするとlibcurlおよびEmbeddingのところがかなりややこしくなるため、今回はPyQt6で作ります。

現時点でサイズは240MBに達しています。自分的には論外な大きさですが、軽量高速化よりAI最新情報へのキャッチアップを優先します。