😎

langchainとかducduckduckgo search APIとかを使ってperplexity aiの劣化版を作ったお話

2024/08/10に公開

自分でもperplexity aiやduckduckgo aiのようなllmを使ったウェブアプリを作って見たかったので作ってみました。コードはhugging faceに載せてます。

今回はコード全体をサラッと解説してからllmを使ったウェブアプリをhugging faceを使って作る方法を解説していこうと思います。開発環境はhugging faceの無料プランを利用してます。

1. 初期設定

ライブラリのインポート

import streamlit as st
from langchain_groq import ChatGroq
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from duckduckgo_search import DDGS
from langchain_community.tools import DuckDuckGoSearchRun
from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
import re
import clipboard
import io
import os
import pandas as pd
from PyPDF2 import PdfFileReader
from PIL import Image
import pytesseract
import requests
from bs4 import BeautifulSoup

必要なライブラリをインポートします。これには、Streamlit、LangChain、DuckDuckGo検索、PDFや画像処理、回答のコピーのためのライブラリが含まれます。

環境変数からAPIキーを設定します。「setting」の「Variables and secrets」から「Secrets Private」を選択してAPIキーの環境変数を設定します。APIキーが全世界に発信されたら困りますもんね。

Groq APIキーの設定

GROQ_API_KEY = os.getenv("GROQ_API_KEY")

Streamlitアプリケーションの設定

st.set_page_config(page_title="COM COM AI", page_icon="🤖", layout="wide")

Streamlitアプリケーションのページ設定を行います。タイトル、アイコン、レイアウトを指定します。

セッション状態の初期化

if 'messages' not in st.session_state:
    st.session_state.messages = []
if 'uploaded_file_content' not in st.session_state:
    st.session_state.uploaded_file_content = None

セッションステートにメッセージとアップロードされたファイルの内容を保存するための変数を初期化します。

2. ファイル読み込み関数

PDFファイルの読み込み

def read_pdf(file):
    pdf = PdfFileReader(file)
    content = ""
    for page_num in range(pdf.getNumPages()):
        page = pdf.getPage(page_num)
        content += page.extract_text()
    return content

PDFファイルを読み込み、その内容をテキストとして返します。

CSVファイルの読み込み

def read_csv(file):
    df = pd.read_csv(file)
    return df.to_string()

CSVファイルを読み込み、データフレームを文字列形式で返します。

画像ファイルの読み込み

def read_image(file):
    image = Image.open(file)
    text = pytesseract.image_to_string(image)
    return text

画像ファイルを読み込み、OCR(光学文字認識)を使ってテキストを抽出します。

ウェブページのテキスト取得

def fetch_webpage_text(url):
    response = requests.get(url)
    if response.status_code != 200:
        return "ウェブページを取得できませんでした。"
    soup = BeautifulSoup(response.text, 'html.parser')
    paragraphs = soup.find_all('p')
    text_content = ' '.join([paragraph.get_text() for paragraph in paragraphs])
    return text_content

指定されたURLのウェブページを取得し、その内容をテキストとして返します。

3. サイドバー設定

AIモデルとモードの選択

with st.sidebar:
    ai_model = st.selectbox(
        "AIモデルを選択してください",
        ["gemma-7b-it", "gemma2-9b-it", "llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768"],
        key="model_select"
    )
    mode = st.selectbox(
        "モードを選択してください",
        ["focus", "writing", "video","summary", "知恵袋"],
        key="mode_select"
    )

サイドバーにAIモデルとモードの選択ボックスを配置します。

ファイルアップロード機能

uploaded_file = st.file_uploader("ファイルをアップロード", type=["txt", "pdf", "csv", "py", "html", "css", "js", "cs", "php", "java", "png", "jpg"])
if uploaded_file is not None:
    file_extension = uploaded_file.name.split('.')[-1].lower()
    if file_extension in ['txt', 'py', 'html', 'css', 'js', 'cs', 'php', 'java']:
        stringio = io.StringIO(uploaded_file.getvalue().decode("utf-8"))
        st.session_state.uploaded_file_content = stringio.read()
    elif file_extension == 'pdf':
        st.session_state.uploaded_file_content = read_pdf(uploaded_file)
    elif file_extension == 'csv':
        st.session_state.uploaded_file_content = read_csv(uploaded_file)
    elif file_extension in ['png', 'jpg']:
        st.session_state.uploaded_file_content = read_image(uploaded_file)
    st.success(f"ファイル '{uploaded_file.name}' がアップロードされました。")

ファイルアップロード機能を提供し、アップロードされたファイルの内容を読み込みます。

会話をクリアするボタン

if st.button("🔥 会話をクリア"):
    st.session_state.messages = []
    st.session_state.uploaded_file_content = None

会話履歴とアップロードされたファイルの内容をクリアするボタンを配置します。

4. メインコンテンツ

タイトルの表示

st.title("COM COM AI")

アプリケーションのタイトルを表示します。名前は適当です。

チャット履歴の表示

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

セッションステートに保存されたチャット履歴を表示します。

ユーザー入力

if prompt := st.chat_input("メッセージを入力してください"):
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

ユーザーの入力を受け付け、チャット履歴に追加します。

LLMの初期化

llm = ChatGroq(temperature=0.7, groq_api_key=GROQ_API_KEY, model_name=ai_model)

選択されたAIモデルを初期化します。

モードに応じた処理

各モードに応じて異なる処理を行います。

focusモード

if mode == "focus":
    with DDGS() as ddgs:
        search_results = list(ddgs.text(prompt, max_results=5))
    prompt_template = PromptTemplate(
        input_variables=["query", "search_results"],
        template="質問: {query}\n\n検索結果: {search_results}\n\n上記の情報に基づいて、詳細な回答を提供してください:"
    )
    chain = LLMChain(llm=llm, prompt=prompt_template)
    full_response = chain.invoke({"query": prompt, "search_results": search_results})["text"]
    message_placeholder.markdown(full_response)
    if search_results:
        for result in search_results[:3]:
            st.markdown(f"参考URL: {result['href']}")

DuckDuckGo検索を実行し、検索結果を基に回答を生成します。これによりllm特有の最新の情報を答えられないという欠点を克服できます。ここら辺の機能はperplexity aiを参考にして作りました。

writingモード

elif mode == "writing":
    prompt_template = PromptTemplate(
        input_variables=["topic"],
        template="次のトピックについて包括的な記事を書いてください: {topic}\n\n導入、主要なポイント、および結論を含めてください。"
    )
    chain = LLMChain(llm=llm, prompt=prompt_template)
    full_response = chain.invoke({"topic": prompt})["text"]
    message_placeholder.markdown(full_response)

ウェブ検索せずに回答を作成します。選択するAIモデルによって多少の差はありますがどれも最新の情報は持ってません。

summaryモード

elif mode == "summary":
    prompt_template = PromptTemplate(
        input_variables=["text"],
        template="次の文章を簡潔に要約してください:\n\n{text}\n\n主要なポイントを捉えた簡潔な要約を提供してください。"
    )
    chain = LLMChain(llm=llm, prompt=prompt_template)
    full_response = chain.invoke({"text": prompt})["text"]
    message_placeholder.markdown(full_response)

送信された文章を簡潔に要約します。

知恵袋モード

elif mode == "知恵袋":
    prompt_template = PromptTemplate(
        input_variables=["question"],
        template="あなたは知恵袋の回答者です。以下の質問に対して、丁寧かつ詳細に回答してください:\n\n質問: {question}\n\n回答:"
    )
    chain = LLMChain(llm=llm, prompt=prompt_template)
    full_response = chain.invoke({"question": prompt})["text"]
    message_placeholder.markdown(full_response)

知恵袋の質問に対して丁寧かつ詳細に回答します。perplexity aiの「ソーシャル」機能を参考にして作りました。

videoモード

elif mode == "video":
    ddg_search = DuckDuckGoSearchAPIWrapper()
    videos = ddg_search.results(f"{prompt} site:youtube.com", max_results=5)
    if videos:
        for video in videos[:4]:
            video_url = video['link']
            video_id = None
            youtube_patterns = [
                r"(?<=v=)[^]+",
                r"(?<=be/)[^]+",
                r"(?<=embed/)[^]+"
            ]
            for pattern in youtube_patterns:
                match = re.search(pattern, video_url)
                if match:
                    video_id = match.group()
                    break
            if video_id:
                st.markdown(f"""""", unsafe_allow_html=True)
                st.markdown(f"関連動画: {video['title']}")
            else:
                st.warning(f"動画IDを抽出できませんでした: {video_url}")
    else:
        full_response = "関連動画が見つかりませんでした。"
        message_placeholder.markdown(full_response)

DuckDuckgo APIを使ってYouTubeから関連動画を検索し、動画を埋め込み表示します。何故かiPadなどのiOSデバイスでは動画が表示されないです…原因分かる人いたらコメント欄から教えてください。お願いします。

コピー機能の追加

if st.button("📋 生成されたテキストをコピー"):
    clipboard.copy(full_response)
    st.success("テキストがクリップボードにコピーされました!")

生成されたテキストをクリップボードにコピーするボタンを配置します。

あとは必要なライブラリを書き下したrequirements.txtを作成し、ウェブアプリのコードをapp.pyとして作成して「App」をクリックしたらウェブアプリが公開されます。

hugging faceのUIが邪魔なら埋め込みurlを利用しても良いかもしれません。

今回作ったアプリのコードはこちらで公開しています。
https://huggingface.co/spaces/supertakerin2/COMGPT5/tree/main

作ったアプリはこんな感じです。
https://supertakerin2-comgpt5.hf.space/

###おまけ
コードの概要図です。

Discussion