シェルスクリプトマガジン

香川大学SLPからお届け!(Vol.95掲載)

著者:遠藤幸太郎

AIと自然言語処理技術の発展により、「ChatGPT」に代表される、人間と自然な形で対話できる会話型AIが注目を集めています。今回は、PythonでAIチャットボットアプリを開発していきます。米Google社の「Gemini API」を利用し、対話だけでなく、音声や映像ファイルをアップロードして分析できるようにします。

シェルスクリプトマガジン Vol.95は以下のリンク先でご購入できます。

図3 「win.py」ファイルに記述するコード

from PySide6.QtWidgets import (
  QApplication, QMainWindow, QLabel
)
from PySide6.QtCore import Qt
import sys

class MainWindow(QMainWindow):
  def __init__(self):
    super().__init__()
    self.setWindowTitle("ウィンドウタイトル")
    self.setGeometry(100, 100, 400, 300)
    label = QLabel("テキスト", self)
    label.setGeometry(0, 0, 400, 300)
    label.setAlignment(Qt.AlignCenter)

if __name__ == "__main__":
  app = QApplication(sys.argv)
  window = MainWindow()
  window.show()
  sys.exit(app.exec())

図5 基本的なチャットボットアプリのコード

from PySide6.QtWidgets import (
  QApplication, QMainWindow, QWidget,
  QVBoxLayout, QHBoxLayout, QPushButton,
  QTextEdit, QLineEdit,
        ①
)
from PySide6.QtCore import Qt, QThread, Signal
import os, sys, re ②
import google.generativeai as genai
from dotenv import load_dotenv
from markdown2 import markdown

MAX_HISTORY_LENGTH = 10

class ChatWindow(QMainWindow):
  def __init__(self):
    super().__init__()
    self.history = []
    self.setWindowTitle("ChatBot")
    self.setGeometry(100, 100, 600, 400)
    self.central_widget = QWidget()
    self.setCentralWidget(self.central_widget)
    self.main_layout = QVBoxLayout()
    self.chat_layout = QVBoxLayout()
    self.input_layout = QHBoxLayout()
    self.file_layout = QHBoxLayout()
    self.chat_display = QTextEdit()
    self.chat_display.setReadOnly(True)
    self.main_layout.addWidget(self.chat_display)
    self.input_box = QLineEdit()
    self.input_box.setPlaceholderText("ここにメッセージを入力")
    self.input_layout.addWidget(self.input_box)
    self.send_button = QPushButton("送信")
    self.send_button.clicked.connect(self.handle_send)
    self.input_layout.addWidget(self.send_button)
    self.main_layout.addLayout(self.input_layout)
        ③
    self.central_widget.setLayout(self.main_layout)
  def handle_send(self):
    user_message = self.input_box.text().strip()
    if not user_message: return
    self.send_button.setEnabled(False)
    self.chat_display.append(f"<b>あなた:</b> {user_message}")
        ④
    self.input_box.clear()
    self.response_thread = BotResponseThread(
      user_message, self.history ⑤
    )
    self.response_thread.response_ready.connect(
      self.display_bot_response
    )
    self.response_thread.start()
  def safe_markdown(self, text):
    lines = text.splitlines()
    formatted_lines = []
    for i, line in enumerate(lines):
      if re.match(r"^\s*(\d+\.\s+|[-*+]\s+)", line) and (
        i + 1 < len(lines) and lines[i + 1].strip()
      ):
        formatted_lines.append(line)
        formatted_lines.append("")
      else:
      formatted_lines.append(line)
    formatted_text = "\n".join(formatted_lines)
    return markdown(formatted_text)
  def display_bot_response(self, response_text):
    html_content = self.safe_markdown(response_text)
    self.chat_display.append(f"<b>Bot:</b> {html_content}")
    self.send_button.setEnabled(True)
        ⑥

class BotResponseThread(QThread):
  response_ready = Signal(str)
  def __init__(self, user_message, history): ⑦
    super().__init__()
    self.user_message = user_message
        ⑧
    self.history = history
  def run(self):
    response_text = self.get_bot_response(self.user_message) ⑨
    self.response_ready.emit(response_text)
  def get_bot_response(self, message): ⑩
    input_text = message
    self.history.append({"role": "user", "parts": [input_text]})
        ⑪
    response = model.generate_content(self.history)
    self.history.append(response.candidates[0].content)
    if len(self.history) > MAX_HISTORY_LENGTH:
      self.history.pop(0)
    return response.text

def initialize_genai():
  load_dotenv()
  api_key = os.getenv("GOOGLE_API_KEY")
  genai.configure(api_key=api_key)
  return genai.GenerativeModel("gemini-1.5-flash")

if __name__ == "__main__":
  model = initialize_genai()
  app = QApplication(sys.argv)
  window = ChatWindow()
  window.show()
  sys.exit(app.exec())

図9 図5の③で示す箇所に挿入するコード

self.file_label = QLabel("ファイルが選択されていません")
self.file_label.setAlignment(Qt.AlignLeft)
self.file_layout.addWidget(self.file_label)
self.upload_button = QPushButton("ファイルを選択")
self.upload_button.clicked.connect(self.handle_file_selection)
self.file_layout.addWidget(self.upload_button)
self.main_layout.addLayout(self.file_layout)
self.selected_file_path = None

図10 図5の④で示す箇所に挿入するコード

if self.selected_file_path:
  self.chat_display.append(
    f"<b>添付ファイル:</b> {self.selected_file_path}"
  )
  file_path = self.selected_file_path
  self.selected_file_path = None
  self.file_label.setText("ファイルが選択されていません")
else:
  file_path = None

図11 図5の⑥で示す箇所に挿入するコード

def handle_file_selection(self):
  file_dialog = QFileDialog()
  file_path, _ = file_dialog.getOpenFileName(
    self, "ファイルを選択"
  )
  if file_path:
    self.selected_file_path = file_path
    self.file_label.setText(
      f"選択したファイル: {file_path.split('/')[-1]}"
    )

図12 図5の⑪で示す箇所に挿入するコード

if file_path:
  attached_file = genai.upload_file(path=file_path)
  while attached_file.state.name == "PROCESSING":
    time.sleep(10)
    attached_file = genai.get_file(attached_file.name)
  self.history.append(
    {"role": "user", "parts": [attached_file]}
  )
else:
  pass