Google AI studio ✕ chat bot

Google AI studioくんは、月200万トークンくらいが使えて、お試しには無料で遊べると思うので、遊んでみてください。

import streamlit as st 
import os 
from dotenv import load_dotenv 
import nest_asyncio 

from langchain_community.document_loaders import PyPDFLoader, UnstructuredHTMLLoader 
from langchain.text_splitter import RecursiveCharacterTextSplitter 
from langchain_google_genai import GoogleGenerativeAIEmbeddings, ChatGoogleGenerativeAI 
from langchain_community.vectorstores import FAISS 
from langchain.chains import RetrievalQA 

# Streamlit上でasyncioエラー回避 
nest_asyncio.apply() 

# .envからAPIキー読み込み 
load_dotenv() 
gemini_key = os.getenv("GOOGLE_API_KEY") 
if gemini_key is None: 
st.error(".envに GOOGLE_API_KEY が見つかりません") 
st.stop() 
os.environ["GOOGLE_API_KEY"] = gemini_key 

st.title("📄 PDF / HTML Q&A チャットボット") 

# セッション状態の初期化 
if "messages" not in st.session_state: 
st.session_state.messages = [] 
if "qa_chain" not in st.session_state: 
st.session_state.qa_chain = None 

# 既存メッセージの表示 
for message in st.session_state.messages: 
with st.chat_message(message["role"]): 
st.markdown(message["content"]) 

# ファイルアップロード 
uploaded_file = st.file_uploader( 
"Q&Aを行いたいPDFまたはHTMLファイルをアップロードしてください", 
type=["pdf", "html", "htm"] 
) 

def setup_qa_chain(file_path: str, file_type: str): 
# PDF or HTML の読み込み 
if file_type == "pdf": 
loader = PyPDFLoader(file_path) 
elif file_type in ["html", "htm"]: 
loader = UnstructuredHTMLLoader(file_path) 
else: 
raise ValueError("対応していないファイル形式です") 

documents = loader.load() 
if not documents: 
st.error(f"{file_type.upper()}からテキストを抽出できませんでした。") 
st.stop() 

# テキスト分割 
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) 
texts = text_splitter.split_documents(documents) 

# 埋め込みとベクトルストア 
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001") 
vector_store = FAISS.from_documents(texts, embeddings) 
retriever = vector_store.as_retriever() 

# GeminiモデルとQAチェーン 
model = ChatGoogleGenerativeAI(model="gemini-1.5-flash") 
qa_chain = RetrievalQA.from_chain_type( 
llm=model, retriever=retriever, return_source_documents=True 
) 
return qa_chain 

# ファイルがアップロードされ、QAチェーンが未作成の場合 
if uploaded_file and st.session_state.qa_chain is None: 
with st.spinner("ドキュメントを読み込み中..."): 
try: 
temp_file_path = f"temp_uploaded.{uploaded_file.name.split('.')[-1]}" 
with open(temp_file_path, "wb") as f: 
f.write(uploaded_file.getbuffer()) 

file_type = uploaded_file.name.split(".")[-1].lower() 
st.session_state.qa_chain = setup_qa_chain(temp_file_path, file_type) 
st.success("ドキュメント準備完了!質問を入力してください。") 
os.remove(temp_file_path) 
except Exception as e: 
st.error(f"エラーが発生しました: {e}") 

# QAチェーンが存在する場合にチャット表示 
if st.session_state.qa_chain: 
if prompt := st.chat_input("ドキュメントの内容について質問してください"): 
st.session_state.messages.append({"role": "user", "content": prompt}) 
with st.chat_message("user"): 
st.markdown(prompt) 

with st.spinner("回答を生成中..."): 
try: 
response = st.session_state.qa_chain.invoke({"query": prompt}) 
answer = response['result'] 

st.session_state.messages.append({"role": "assistant", "content": answer}) 
with st.chat_message("assistant"): 
st.markdown(answer) 
except Exception as e: 
st.error(f"質問処理中にエラーが発生しました: {e}")

みたいなのと、.envファイル

GOOGLE_API_KEY = HOGEHOGE;

を同じ階層に書いて、

streamlit run app.pyで実行すると、streamlitの画面が起動して、アップロードしたファイルを呼んで解釈してくれる感じのchat botが作れます。
(事前に

brew install tesseract 
pip install nest_asyncio 
pip install langchain langchain-community pypdf 
pip install pytesseract pillow

が要る。)

## 一応説明

ファイルアップロード

技術: Streamlit st.file_uploader

役割: ユーザーからPDFやHTMLファイルを受け取る
–>
テキスト分割

技術: RecursiveCharacterTextSplitter

役割: 長文をLLMが扱いやすいチャンクに分割
–>
ベクトル化 (Embeddings)

技術: GoogleGenerativeAIEmbeddings

役割: 各テキストチャンクを数値ベクトルに変換
–>
ベクトルストア & 検索

技術: FAISS (FAISS.from_documents + as_retriever)あるいはコレを見てください

役割: 質問時に関連文書を高速に検索できるように格納
–>
QAチェーン作成

技術: ChatGoogleGenerativeAI, RetrievalQA

役割:

検索結果を参照してLLMが自然言語回答を生成

ユーザー質問 → Retriever → LLM → 回答
–>
質問応答

技術: Streamlit st.chat_input + qa_chain.invoke

役割:

ユーザーの質問を受け取り、回答を生成・表示

会話履歴をセッションに保存
てな感じで動いてます。

## 参考・補足

– [ここ](https://github.com/kaisugi/gpt4_vocab_list)に、トーカナイザーで分割された単語が必ず含まれるボキャブラリー表がおいてある。
トーカナイザーについては、[コレ](https://speakerdeck.com/payanotty/tokunaizaru-men?slide=7)とか[コレ](https://dev.classmethod.jp/articles/road-to-llm-advent-calendar-2023-07/)
– HuggingFaceは、機械学習界のgit hubと主張してるプラットフォーム[らしい](https://qiita.com/ski2_1116/items/f74e7b97008663d0702d)
– 下で使ってるlang.chainは[ココ](https://zenn.dev/chips0711/articles/f4ed8ac37eb3a8)とかに載ってるかも。
– Google AI studioでの[ファインチューニング](https://qiita.com/shun_so/items/0f044047553e2fefa66c)について
– [LangChain で社内チャットボット作ってみた](https://zenn.dev/cloud_ace/articles/19bd3554ac8432)
– [RAGチャットボット開発ガイド](https://zenn.dev/daijobu/articles/c1217e40fdf2a5)

### コマンドの意味

– brew install tesseract

意味: Homebrew(macOS用パッケージ管理ツール)を使って Tesseract OCR をインストールする。

Tesseract OCR: 画像内の文字を認識してテキストに変換するツール。

例: スキャンした書類やPDF画像から文字を読み取るのに使う
– pip install pytesseract pillow

意味: 画像処理関連の Python パッケージをインストールする。

pytesseract: Python から Tesseract OCR を呼び出すためのラッパー。(↑とセットで使う。)

Pillow: Python で画像を扱うライブラリ(PIL の後継)。

用途: 画像やPDFから文字を抽出したり、画像を加工したりする。

– pip install langchain langchain-community pypdf

意味: 以下の Python パッケージをまとめてインストールする。

— langchain: LLM(大規模言語モデル)を活用してチェーン処理やアプリ構築を支援するライブラリ。

— langchain-community: LangChain のコミュニティ拡張パッケージ。追加モデルやツールを提供。

— pypdf: PDF ファイルを読み込んだり操作したりできる Python パッケージ。

#### おまけ

# src/scripts/scrape_site.py 
import requests 
from bs4 import BeautifulSoup 
import json 
import os 
import warnings 
from urllib3.exceptions import NotOpenSSLWarning 
from urllib.parse import urljoin, urlparse 

warnings.filterwarnings("ignore", category=NotOpenSSLWarning) 

BASE_URL = "https://www.HOGEFUGA.co.jp/" 
#引っ張ってくるサイトのurl
OUTPUT_DIR = "scraped_site" 
VISITED = set() 

def fetch_text(url): 
"""1ページのテキストを抽出""" 
try: 
headers = { 
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " 
"AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/114.0.0.0 Safari/537.36" } 
resp = requests.get(url, headers=headers) 
resp.raise_for_status() 
soup = BeautifulSoup(resp.text, 'html.parser') 
text_content = "" 
for tag in soup.find_all(['p', 'h1', 'h2', 'h3', 'li', 'span']): 
cleaned = tag.get_text(separator=' ', strip=True) 
if cleaned: 
text_content += cleaned + " " 
return text_content.strip() 
except requests.exceptions.RequestException: 
return None 

def save_text(url, text): 
"""URL に対応したディレクトリ/JSON に保存""" 
parsed = urlparse(url) 
path = parsed.path.strip("/") 

# index ページなら "index.json" に 
if path == "": 
path = "index" 

dir_path = os.path.join(OUTPUT_DIR, os.path.dirname(path)) 
os.makedirs(dir_path, exist_ok=True) 
file_path = os.path.join(OUTPUT_DIR, path + ".json") 

with open(file_path, "w", encoding="utf-8") as f: 
json.dump({"url": url, "text": text}, f, ensure_ascii=False, indent=2) 
print(f"保存: {file_path}") 

def crawl(url, max_pages=50): 
"""再帰的にサイト内リンクをクロール""" 
if url in VISITED or len(VISITED) >= max_pages: 
return 
VISITED.add(url) 

text = fetch_text(url) 
if text: 
save_text(url, text) 

# サイト内リンクを取得して再帰 
headers = { 
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " 
"AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/114.0.0.0 Safari/537.36" } 
try: 
resp = requests.get(url, headers=headers) 
soup = BeautifulSoup(resp.text, 'html.parser') 
for a in soup.find_all('a', href=True): 
link = urljoin(BASE_URL, a['href']) 
if link.startswith(BASE_URL): 
crawl(link, max_pages) 
except requests.exceptions.RequestException: 
pass 

if __name__ == "__main__": 
crawl(BASE_URL) 
print(f"クロール終了。{len(VISITED)} ページ取得")

こういう感じのコード書いて、スクレイピングしてもいいのかも。

Next.js ✕ LM Studio ✕ gpt-oss-20b

# gpt-oss-20b をLM studioで動かしてそれをnext.jsを用いてブラウザでchat botっぽく表示する方法です。

## 結果

↑こんな感じになると思います。

## やりかた

### LM-studio & gpt-oss-20b
なんか最近(8/8現在)でたgpt-oss-20bです。
LM studioの導入はこのお方
の記事を、例えば参考にしてください。

こことか見ておく。

### Approuter

pnp install
pnpm add vercel/ai openai

みたいなのを打っといてください。

Create-next-appとターミナルで打って、いい感じに質問に答えておきましょう。

app/api/chat/route.ts

にディレクトリを作りつつ、以下のコードをコードを貼ってください。

/* app/api/chat/route.ts */

import { NextResponse } from 'next/server'; 
export async function POST(req: Request) { 
const { messages } = await req.json(); 

const response = await fetch(process.env.AI_API_URL!, { 
method: 'POST', 
headers: { 'Content-Type': 'application/json' }, 
body: JSON.stringify({ model: 'openai/gpt-oss-20b', messages }), 
}); 

if (!response.ok) { 
return new NextResponse('Error contacting LM Studio API', { status: 500 }); 
} 

const data = await response.json(); 

// LM Studioのレスポンスから必要な部分だけを抽出して返す 
const message = data.choices?.[0]?.message; 

if (!message) { 
return new NextResponse('Invalid response format from LM Studio API', { status: 500 }); 
} 

// 必要最低限の形で返す 
return NextResponse.json({ 
choices: [ 
{ 
message, 
}, 
], 
}); 
}


### 実際の表示画面部分
これをapp/src にpage.tsxとして置いといてください。(とりあえず)

'use client'; 

import { useState } from 'react'; 
import ReactMarkdown from 'react-markdown'; 

type Message = { 
role: 'user' | 'assistant'; 
content: string; 
id: string; 
}; 

export default function Chat() { 
const [messages, setMessages] = useState<Message[]>([]); 
const [input, setInput] = useState(''); 

const handleSubmit = async (e: React.FormEvent) => { 
e.preventDefault(); 
if (!input.trim()) return; 

const userMessage: Message = { 
role: 'user', 
content: input, 
id: crypto.randomUUID(), 
}; 

// 表示用にユーザーメッセージを先に追加 
setMessages(prev => [...prev, userMessage]); 
setInput(''); 

try { 
const res = await fetch('/api/chat', { 
method: 'POST', 
headers: { 'Content-Type': 'application/json' }, 
body: JSON.stringify({ messages: [...messages, userMessage] }), 
}); 

const data = await res.json(); 

const aiContent = data?.choices?.[0]?.message?.content; 

if (aiContent) { 
const aiMessage: Message = { 
role: 'assistant', 
content: aiContent, 
id: crypto.randomUUID(), 
}; 

setMessages(prev => [...prev, aiMessage]); 
} 
} catch (err) { 
console.error('API error:', err); 
} 
}; 

return ( 
<main> 
<div> {messages.map((m) => ( 
<div key={m.id}> 
<strong>{m.role === 'user' ? '👤 User' : '🤖 AI'}:</strong> 
<ReactMarkdown>{m.content}</ReactMarkdown> 
</div> ))} 
</div> 

<form onSubmit={handleSubmit}> 
<input type="text" 
value={input} 
onChange={e => setInput(e.target.value)} 
placeholder="質問を入力..." 
/> 
<button type="submit">送信</button> 
</form> </main> ); 
}

### `.env.local` 設定

env.localファイルを作って、

AI_API_URL=http://localhost:1234/v1/chat/completions
OPENAI_API_KEY=lm-studio
# - `AI_API_URL`: LM Studio のチャットエンドポイント(デフォルト) 
# - `OPENAI_API_KEY`: 実際には使われないが、ライブラリ上必須なのでダミーを入れる

あとは、

 npm run dev 

でも打って、でてきた

 ▲ Next.js 15.4.6 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.21.9:3000
- Environments: .env.local

http://localhost:3000

みたいなのに飛べばいいと思います。
### 参考

Vercel AI SDK を使って Next.js アプリに AI 機能を追加する

## おまけ(## Google APIでの検索を入れたい場合

## Google APIでの検索を入れたい場合
https://programmablesearchengine.google.com/controlpanel/all
からGoogle CSXの番号を拾ってくる。

Google Custom APIを検索して、登録する。
API key
このAPI keyCSXコードを.env.localっていうmy app直下につけたす。
## 📎 参考
- (https://ai-sdk.dev/providers/ai-sdk-providers/openai)[Vercelai/sdkの説明]
- [https://github.com/vercel/ai](https://github.com/vercel/ai)
- [https://lmstudio.ai](https://lmstudio.ai)
# Google Custom Search API
# GOOGLE_API_KEY=あなたのAPIキー
# GOOGLE_CX=あなたの検索エンジンID
GOOGLE_API_KEY=AIUEO_WADDLE_DEE
GOOGLE_CX=WADDLE_DEE

そしたら、src/app/api/chat/route.tsの中身を

import { NextResponse } from 'next/server';

export async function POST(req: Request) {
    const { messages } = await req.json();

    if (!Array.isArray(messages) || messages.length === 0) {
        return new NextResponse('Invalid messages', { status: 400 });
    }

    // 最新のユーザーメッセージを取得
    const lastUserMessage = [...messages].reverse().find(m => m.role === 'user');
    if (!lastUserMessage) {
        return new NextResponse('No user message found', { status: 400 });
    }

    // Google検索API呼び出し
    const searchUrl = `https://www.googleapis.com/customsearch/v1?q=${encodeURIComponent(
        lastUserMessage.content
    )}&key=${process.env.GOOGLE_API_KEY}&cx=${process.env.GOOGLE_CX}`;

    const searchRes = await fetch(searchUrl);
    if (!searchRes.ok) {
        console.error('Google Search API error:', await searchRes.text());
        return new NextResponse('Google Search API error', { status: 500 });
    }

    const searchData = await searchRes.json();
    const searchText =
        searchData.items?.map((item: any) => `${item.title}: ${item.snippet}`).join('\n') ||
        '検索結果なし';

    // LM Studioに渡すmessages配列を構築
    const lmMessages = [
        { role: 'system', content: '以下の検索結果を参考にして質問に答えてください。' },
        ...messages,
        { role: 'user', content: `検索結果:\n${searchText}` },
    ];

    // LM Studio APIへ問い合わせ
    const lmRes = await fetch(process.env.AI_API_URL!, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            model: 'openai/gpt-oss-20b', // ここはLM Studioで使うモデル名に置き換えてください
            messages: lmMessages,
        }),
    });

    if (!lmRes.ok) {
        console.error('LM Studio API error:', await lmRes.text());
        return new NextResponse('Error contacting LM Studio API', { status: 500 });
    }

    const data = await lmRes.json();
    const message = data.choices?.[0]?.message;

    if (!message) {
        return new NextResponse('Invalid response from LM Studio', { status: 500 });
    }

    return NextResponse.json({
        choices: [{ message }],
    });
}

にしてください。以上です。心配だったら、
src/app/example/page.tsx

'use client';
import React, { useState } from 'react';

export default function Example() {
    const [value, setValue] = useState('');

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setValue(e.target.value);
    };

    return (
        <div>
            <input type="text" value={value} onChange={handleChange} />
            <p>入力された値: {value}</p>
        </div>
    );
}

src/app/page.tsxを

'use client';

import { useState } from 'react';
import ReactMarkdown from 'react-markdown';

type Message = {
    role: 'user' | 'assistant' | 'system';
    content: string;
    id: string;
};

export default function Chat() {
    const [messages, setMessages] = useState<Message[]>([
        {
            role: 'system',
            content: '僕は、親切なAIアシスタントだよ。検索結果を参考にして回答するよ。',
            id: crypto.randomUUID(),
        },
    ]);
    const [input, setInput] = useState('');

    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        if (!input.trim()) return;

        const userMessage: Message = {
            role: 'user',
            content: input,
            id: crypto.randomUUID(),
        };

        // ユーザーメッセージを追加
        const updatedMessages = [...messages, userMessage];
        setMessages(updatedMessages);
        setInput('');

        try {
            const res = await fetch('/api/chat', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ messages: updatedMessages }),
            });

            const data = await res.json();
            const aiContent = data?.choices?.[0]?.message?.content;

            if (aiContent) {
                const aiMessage: Message = {
                    role: 'assistant',
                    content: aiContent,
                    id: crypto.randomUUID(),
                };
                setMessages(prev => [...prev, aiMessage]);
            }
        } catch (err) {
            console.error('API error:', err);
        }
    };

    return (
        <main style={{ maxWidth: 600, margin: 'auto', padding: 20 }}>
            <div style={{ marginBottom: 20 }}>
                {messages.map(m => (
                    <div key={m.id} style={{ marginBottom: 10 }}>
                        <strong>{m.role === 'user' ? '👤 User' : m.role === 'assistant' ? '🤖 AI' : '⚙️ System'}:</strong>
                        <ReactMarkdown>{m.content}</ReactMarkdown>
                    </div>
                ))}
            </div>

            <form onSubmit={handleSubmit}>
                <input
                    type="text"
                    value={input}
                    onChange={e => setInput(e.target.value)}
                    placeholder="質問を入力..."
                    style={{ width: '80%', padding: '8px' }}
                />
                <button type="submit" style={{ padding: '8px 16px', marginLeft: 8 }}>
                    送信
                </button>
            </form>
        </main>
    );
}

src/app/layout.tsxを

export default function RootLayout({
                                       children,
                                   }: Readonly<{
    children: React.ReactNode;
}>) {
    return (
        <html lang="en">
        <body> {/*  class名を削除 */}
        {children}
        </body>
        </html>
    );
}

.env.localのまとめ↓

# LM Studioのローカルサーバーアドレスを指定
# 通常は "http://localhost:1234/v1" です(LM studioの設定画面を見てください)
# OPENAI_API_BASE_URL="http://localhost:1234/v1"
AI_API_URL=http://localhost:3141592/v1/chat/completions


# LM StudioAPIキーを要求しませんが、ライブラリ側で必須の場合があるため
# ダミーの文字列を入れておきます
OPENAI_API_KEY="lm-studio"


# Google Custom Search API
# GOOGLE_API_KEY=あなたのAPIキー
# GOOGLE_CX=あなたの検索エンジンID
GOOGLE_API_KEY=AIUEO_WADDLE_DEE
GOOGLE_CX=WADDLE_DEE