LangChain × Azure OpenAI × HuggingFace Embeddings × Streamlit でPDFチャットアプリを構築する

# **user**:

初学者向けにVSCodeを使ってAzureOpenAIとチャットできるStreamlitアプリを作るガイドを書いてください。ただし、そのチャットの話題はユーザが与えたPDFファイルの中身についてです。ユーザにPDFファイルを入力させて、その内容について、AI自身の持っている知識と擦り合わせながらチャットします。コードは次のコードを参考に、修正して用いてください。

```python
import streamlit as st
import os
import json
import csv
import io
from datetime import datetime
from openai import AzureOpenAI
from dotenv import load_dotenv

# .env の読み込み
load_dotenv()

# Azure OpenAI クライアントの初期化
client = AzureOpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    api_version=os.getenv("OPENAI_API_VERSION"),
    azure_endpoint=os.getenv("OPENAI_API_BASE"),
)

deployment_name = os.getenv("OPENAI_DEPLOYMENT_NAME")

st.set_page_config(page_title="Azure Chat", page_icon="💬")
st.title("💬 Azure OpenAI チャットアプリ")

# チャット履歴の初期化
if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。"}
    ]

# チャット履歴の表示
for msg in st.session_state.messages:
    if msg["role"] != "system":
        with st.chat_message(msg["role"]):
            st.markdown(msg["content"])

# 入力欄
user_input = st.chat_input("あなたのメッセージを入力してください")

if user_input:
    # ユーザー入力を保存・表示
    st.session_state.messages.append({"role": "user", "content": user_input})
    with st.chat_message("user"):
        st.markdown(user_input)

    # OpenAI への問い合わせ
    with st.chat_message("assistant"):
        with st.spinner("考え中..."):
            response = client.chat.completions.create(
                model=deployment_name,
                messages=st.session_state.messages,
            )
            reply = response.choices[0].message.content
            st.markdown(reply)

    # アシスタントの返答を保存
    st.session_state.messages.append({"role": "assistant", "content": reply})

# 🔁 チャット履歴のリセットボタン
if st.button("🔄 チャット履歴をリセット"):
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。"}
    ]
    st.experimental_rerun()

# ✅ チャット履歴の保存機能
messages_to_save = [m for m in st.session_state.messages if m["role"] != "system"]
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")

# JSON形式で保存
def save_chat_as_json():
    return json.dumps(messages_to_save, ensure_ascii=False, indent=2)

st.download_button(
    label="💾 JSONで保存",
    data=save_chat_as_json(),
    file_name=f"chat_log_{timestamp}.json",
    mime="application/json",
)

# CSV形式で保存
def save_chat_as_csv():
    output = io.StringIO()
    writer = csv.DictWriter(output, fieldnames=["role", "content"])
    writer.writeheader()
    writer.writerows(messages_to_save)
    return output.getvalue()

st.download_button(
    label="💾 CSVで保存",
    data=save_chat_as_csv(),
    file_name=f"chat_log_{timestamp}.csv",
    mime="text/csv",
)

# Markdown形式で保存
def save_chat_as_markdown():
    lines = []
    for m in messages_to_save:
        role_label = "**User:**" if m["role"] == "user" else "**Assistant:**"
        content = m["content"].replace("\n", "  \n")  # Markdownの改行
        lines.append(f"{role_label} {content}")
    return "\n\n".join(lines)

st.download_button(
    label="📝 Markdownで保存",
    data=save_chat_as_markdown(),
    file_name=f"chat_log_{timestamp}.md",
    mime="text/markdown",
)
```

# **assistant**:

以下は、**VSCodeでStreamlitアプリを作り、Azure OpenAIとPDFの内容についてチャットできるようにする**ための、初心者向けガイドです。もとにしていただいたコードに加え、「PDFアップロード」と「PDF内容の埋め込みチャット」を実装します。

---

## 🎯 ゴール
ユーザーがPDFをアップロードすると、その内容をAIが読み取り、自身の知識と照らし合わせながらチャットで質問に答えてくれるアプリを作ります。

---

## 🧰 必要な準備

### 1. 必要なツールのインストール
まず、Python環境を整えて以下をインストールします：

```bash
pip install streamlit python-dotenv openai PyPDF2 tiktoken
```

### 2. `.env` ファイルを作成
プロジェクトのルートに以下のような `.env` ファイルを置いてください：

```
OPENAI_API_KEY=your_azure_openai_key
OPENAI_API_BASE=https://your-resource-name.openai.azure.com/
OPENAI_API_VERSION=2023-05-15
OPENAI_DEPLOYMENT_NAME=your-deployment-name
```

---

## 📝 アプリのコード

以下が改良されたStreamlitアプリの全コードです：

```python
import streamlit as st
import os
import json
import csv
import io
from datetime import datetime
from openai import AzureOpenAI
from dotenv import load_dotenv
import PyPDF2
import tiktoken

# 環境変数読み込み
load_dotenv()

# Azure OpenAI クライアントの初期化
client = AzureOpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    api_version=os.getenv("OPENAI_API_VERSION"),
    azure_endpoint=os.getenv("OPENAI_API_BASE"),
)
deployment_name = os.getenv("OPENAI_DEPLOYMENT_NAME")

st.set_page_config(page_title="PDF × Azure Chat", page_icon="📄")
st.title("📄 PDFに基づいてAzure OpenAIとチャット")

# チャット履歴の初期化
if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。"}
    ]

# PDFのアップロード
uploaded_file = st.file_uploader("💡 質問したいPDFファイルをアップロードしてください", type="pdf")
pdf_text = ""

if uploaded_file is not None:
    reader = PyPDF2.PdfReader(uploaded_file)
    for page in reader.pages:
        pdf_text += page.extract_text()

    # PDFテキストをセッションに保存
    st.session_state.pdf_text = pdf_text
    st.success("✅ PDFの内容を読み込みました。質問できます！")

# チャット履歴の表示
for msg in st.session_state.messages:
    if msg["role"] != "system":
        with st.chat_message(msg["role"]):
            st.markdown(msg["content"])

# チャット入力
user_input = st.chat_input("あなたの質問を入力してください")

if user_input:
    # ユーザーの発言を履歴に追加
    st.session_state.messages.append({"role": "user", "content": user_input})
    with st.chat_message("user"):
        st.markdown(user_input)

    # アシスタントからの返答
    with st.chat_message("assistant"):
        with st.spinner("考え中..."):
            # PDFの内容を前提に加える
            context = st.session_state.get("pdf_text", "")
            system_prompt = (
                "あなたはPDF文書の内容に基づいて、親切に回答するAIです。以下がPDFの内容です：\n\n" + context
            )

            messages = [
                {"role": "system", "content": system_prompt}
            ] + st.session_state.messages[1:]

            response = client.chat.completions.create(
                model=deployment_name,
                messages=messages,
            )
            reply = response.choices[0].message.content
            st.markdown(reply)

    # アシスタントの返答を保存
    st.session_state.messages.append({"role": "assistant", "content": reply})

# 🔁 チャット履歴のリセット
if st.button("🔄 チャット履歴をリセット"):
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。"}
    ]
    st.experimental_rerun()

# ✅ チャット履歴の保存機能（JSON/CSV/Markdown）
messages_to_save = [m for m in st.session_state.messages if m["role"] != "system"]
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")

def save_chat_as_json():
    return json.dumps(messages_to_save, ensure_ascii=False, indent=2)

def save_chat_as_csv():
    output = io.StringIO()
    writer = csv.DictWriter(output, fieldnames=["role", "content"])
    writer.writeheader()
    writer.writerows(messages_to_save)
    return output.getvalue()

def save_chat_as_markdown():
    lines = []
    for m in messages_to_save:
        role_label = "**User:**" if m["role"] == "user" else "**Assistant:**"
        content = m["content"].replace("\n", "  \n")
        lines.append(f"{role_label} {content}")
    return "\n\n".join(lines)

st.download_button("💾 JSONで保存", data=save_chat_as_json(), file_name=f"chat_{timestamp}.json", mime="application/json")
st.download_button("💾 CSVで保存", data=save_chat_as_csv(), file_name=f"chat_{timestamp}.csv", mime="text/csv")
st.download_button("📝 Markdownで保存", data=save_chat_as_markdown(), file_name=f"chat_{timestamp}.md", mime="text/markdown")
```

---

## 💡 補足ポイント

### ✅ PDFのトークン数が多すぎる場合
Azure OpenAIの制限により、長すぎるPDFは分割処理が必要です（このコードでは未対応）。必要であればトークン数制限付きの分割処理も追加可能です。

### ✅ VSCodeでの起動
ターミナルで以下を実行：

```bash
streamlit run app.py
```

---

## ✨ まとめ
このガイドを使えば、**自分のPDFファイルに基づいてAzure OpenAIと会話できるアプリ**をVSCodeで簡単に構築できます。

---

必要があれば、PDF分割やLangChainとの統合版など、次のステップもご案内できますよ！

# **user**:

ありがとうございます。LangChainを使ってRAGするようにコードを書き換えてください。

# **assistant**:

以下は、**LangChainを使ってRAG（Retrieval-Augmented Generation）を行うStreamlitアプリ**のサンプルコードです。アップロードされたPDFを読み込み、LangChainの`Document`, `TextSplitter`, `FAISS`を使ってベクトル化し、Azure OpenAIのChatモデルと組み合わせて回答します。

---

## 🧠 RAG × Streamlit × LangChain：構成概要

- ユーザーがPDFをアップロード
- `LangChain`でPDFを分割・ベクトル化（`FAISS`）
- ユーザーの質問をベクトル検索 → 関連する文書を取得
- Azure OpenAIで文脈付き回答を生成

---

## 🛠️ 事前準備（追加で必要なライブラリ）

```bash
pip install langchain faiss-cpu tiktoken
```

> すでにある `streamlit`, `python-dotenv`, `openai`, `PyPDF2` も必要です。

---

## 📝 改良版コード（LangChain + RAG対応）

```python
import streamlit as st
import os
import json
import csv
import io
from datetime import datetime
from dotenv import load_dotenv
import PyPDF2

from langchain.vectorstores import FAISS
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
from langchain.chat_models import AzureChatOpenAI
from langchain.chains import RetrievalQA

# 環境変数の読み込み
load_dotenv()

# Azure OpenAIクライアントの設定
deployment_name = os.getenv("OPENAI_DEPLOYMENT_NAME")

llm = AzureChatOpenAI(
    deployment_name=deployment_name,
    openai_api_key=os.getenv("OPENAI_API_KEY"),
    openai_api_base=os.getenv("OPENAI_API_BASE"),
    openai_api_version=os.getenv("OPENAI_API_VERSION"),
    openai_api_type="azure",
    temperature=0
)

# Embeddingsの設定
embeddings = OpenAIEmbeddings(
    deployment=os.getenv("OPENAI_EMBEDDING_DEPLOYMENT_NAME"),  # 例: "embedding-model"
    openai_api_key=os.getenv("OPENAI_API_KEY"),
    openai_api_base=os.getenv("OPENAI_API_BASE"),
    openai_api_type="azure",
    openai_api_version=os.getenv("OPENAI_API_VERSION")
)

# Streamlitの設定
st.set_page_config(page_title="PDF × RAG Chat", page_icon="📚")
st.title("📚 Azure OpenAI × LangChain PDFチャット")

# チャット履歴の初期化
if "messages" not in st.session_state:
    st.session_state.messages = []

# PDFアップロード
uploaded_file = st.file_uploader("📄 質問したいPDFファイルをアップロードしてください", type="pdf")

if uploaded_file:
    pdf_text = ""
    reader = PyPDF2.PdfReader(uploaded_file)
    for page in reader.pages:
        pdf_text += page.extract_text()

    # 分割してDocument化
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
    texts = text_splitter.split_text(pdf_text)
    docs = [Document(page_content=t) for t in texts]

    # ベクトルストアの作成
    db = FAISS.from_documents(docs, embeddings)
    retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": 3})

    # LangChain RAG QAチェーンの作成
    qa = RetrievalQA.from_chain_type(
        llm=llm,
        retriever=retriever,
        chain_type="stuff",
        return_source_documents=True
    )

    # チャットUI
    for msg in st.session_state.messages:
        with st.chat_message(msg["role"]):
            st.markdown(msg["content"])

    user_input = st.chat_input("質問を入力してください")
    if user_input:
        st.session_state.messages.append({"role": "user", "content": user_input})
        with st.chat_message("user"):
            st.markdown(user_input)

        with st.chat_message("assistant"):
            with st.spinner("PDFの内容を検索中..."):
                result = qa({"query": user_input})
                answer = result["result"]
                st.markdown(answer)

        st.session_state.messages.append({"role": "assistant", "content": answer})

# 🔁 リセットボタン
if st.button("🔄 チャット履歴をリセット"):
    st.session_state.messages = []
    st.experimental_rerun()
```

---

## 🧪 .envファイルの注意点

追加で必要な項目：

```
OPENAI_EMBEDDING_DEPLOYMENT_NAME=your-embedding-deployment-name
```

- Embeddingモデル（例：`text-embedding-ada-002`）もAzureにデプロイしておく必要があります。

---

## ✅ 特徴

- ユーザーの質問に対して、PDFの該当部分を検索し、それをもとに回答
- Azure OpenAIで安全に動作
- StreamlitのUIで簡単にチャット

---

このアプリを元に、さらに検索結果の表示、複数PDF対応、チャット履歴保存なども追加できます。必要があればそちらもお手伝いしますよ！

# **user**:

ありがとうございます。次のようなエラーが出ました。ModuleNotFoundError: Module langchain_community.vectorstores not found. Please install langchain-community to access this module. You can install it using `pip install -U langchain-community`
Traceback:
File "/Users/ikemen/streamlit-app/app.py", line 10, in <module>
    from langchain.vectorstores import FAISS
File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/langchain/vectorstores/__init__.py", line 186, in __getattr__
    return _import_attribute(name)
File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/langchain/_api/module_import.py", line 72, in import_by_name
    raise ModuleNotFoundError(

# **assistant**:

エラーメッセージの内容から察すると、LangChainの最新版ではモジュール構成が変更されており、`FAISS` など一部のベクトルストアは **`langchain_community` パッケージ**に移動されました。

---

## ✅ 解決方法

以下のコマンドを実行して、**`langchain-community` パッケージをインストール**してください：

```bash
pip install -U langchain-community
```

---

## 🔄 そして、インポート部分を修正

次のようにコードも変更してください：

### ✍️ 修正前：

```python
from langchain.vectorstores import FAISS
```

### ✅ 修正後：

```python
from langchain_community.vectorstores import FAISS
```

---

## 💡 なぜこうなった？

LangChainの2024年以降のアップデートで、外部依存モジュール（FAISS、Chroma、Weaviate など）は `langchain_community` に分離されました。これにより、**本体は軽量化**し、必要なベクトルストアだけを個別にインストールできるようになっています。

---

## ✨ まとめ

- ✅ `pip install -U langchain-community`
- ✅ `from langchain_community.vectorstores import FAISS` に修正

これでエラーは解消されるはずです！

もし他にもエラーが出たら、遠慮なくそのまま教えてくださいね。LangChain周りは最近変化が多いので、一緒にアップデートに追いついていきましょう！

# **user**:

ありがとうございます。ちょっと質問があるのですが、下記のコードで、llm用設定キーととembedding用設定キーは別々にしなくていいんですか？詳しく教えてください。

```python
llm = AzureChatOpenAI(
    deployment_name=deployment_name,
    openai_api_key=os.getenv("OPENAI_API_KEY"),
    openai_api_base=os.getenv("OPENAI_API_BASE"),
    openai_api_version=os.getenv("OPENAI_API_VERSION"),
    openai_api_type="azure",
    temperature=0
)

# Embeddingsの設定
embeddings = OpenAIEmbeddings(
    deployment=os.getenv("OPENAI_EMBEDDING_DEPLOYMENT_NAME"),  # 例: "embedding-model"
    openai_api_key=os.getenv("OPENAI_API_KEY"),
    openai_api_base=os.getenv("OPENAI_API_BASE"),
    openai_api_type="azure",
    openai_api_version=os.getenv("OPENAI_API_VERSION")
)
```

# **assistant**:

とても良い質問です！✨

結論から言うと：

> **`llm`（会話モデル）用と `embeddings`（埋め込みモデル）用では、**  
> **同じ `APIキー` や `エンドポイント` を使ってもOKですが、**  
> **`deployment_name` は別々にする必要があります。**

---

## 🔍 詳しく説明します

Azure OpenAIでは、モデルを使うには「**モデルをデプロイ**」する必要があります。

### 🔹 1. LLM用デプロイ（例：`gpt-35-turbo`）

```env
OPENAI_DEPLOYMENT_NAME=gpt35-chat
```

この名前で `AzureChatOpenAI` に接続します。

---

### 🔹 2. Embedding用デプロイ（例：`text-embedding-ada-002`）

```env
OPENAI_EMBEDDING_DEPLOYMENT_NAME=embedding-model
```

この名前で `OpenAIEmbeddings` に接続します。

---

## ✅ なぜ deployment 名を分ける必要があるの？

Azureでは同じリソース（エンドポイント・APIキー）でも、  
**どのモデルを使うか**は `deployment_name` で識別されます。

- `llm` 用：チャットや文章生成など → `gpt-35-turbo` など
- `embeddings` 用：文章のベクトル化 → `text-embedding-ada-002` など

### 🛑 同じ deployment_name を使うとどうなる？

例えば、`gpt-35-turbo` に対してベクトル化を試みたら、エラーになります。  
**モデルの役割が違うため、互換性がありません。**

---

## 🔐 openai_api_key, openai_api_base などは共通でOK

- これらは **Azure OpenAIのリソース単位**で設定されているので、
- 同じ Azure OpenAI リソースから複数のデプロイを使う場合は、共通の値で大丈夫です。

```env
OPENAI_API_KEY=xxxxxxxxxxxxxxxx
OPENAI_API_BASE=https://xxx.openai.azure.com/
OPENAI_API_VERSION=2023-12-01-preview
```

---

## ✅ 最低限必要な `.env` の例

```env
# 共通設定
OPENAI_API_KEY=sk-xxxxx
OPENAI_API_BASE=https://your-resource-name.openai.azure.com/
OPENAI_API_VERSION=2023-12-01-preview

# LLM用デプロイ名
OPENAI_DEPLOYMENT_NAME=gpt35-chat

# Embedding用デプロイ名
OPENAI_EMBEDDING_DEPLOYMENT_NAME=embedding-model
```

---

## 💬 補足：それぞれのモデルが必要な理由

| 用途         | モデル名例            | 役割                     |
|--------------|-----------------------|--------------------------|
| チャット生成 | `gpt-35-turbo`        | 質問に答える             |
| ベクトル化   | `text-embedding-ada`  | 質問や文書を数値化して検索に使う |

---

もし Azure ポータル上でまだ embedding モデルをデプロイしていない場合は、  
「**モデルのデプロイ**」から `text-embedding-ada-002` を選んで名前を付けてデプロイしてください。

---

不安な点やAzureのポータル操作も含めてわからないことがあれば、気軽に聞いてくださいね！

# **user**:

OpenAIのembeddingの代用として、Transformersの中にあるembeddingを使うようにコードを改変してください。

# **assistant**:

了解です！  
では、Azure OpenAIの `text-embedding-ada-002` を使わず、**Hugging Face Transformers の埋め込みモデルを使う形にコードを改変**しましょう。

---

## 🎯 目的

- Azure OpenAIの `OpenAIEmbeddings` を使わず
- `transformers` + `sentence-transformers` ベースで埋め込みを生成

---

## 🧱 追加で必要なパッケージ

```bash
pip install sentence-transformers
```

---

## ✅ 主な修正ポイント

| 修正対象 | 修正内容 |
|----------|----------|
| `OpenAIEmbeddings` | → `HuggingFaceEmbeddings` に変更 |
| AzureのAPI設定 | 不要になるので削除可能 |

---

## 🧠 使う埋め込みモデル

例：`all-MiniLM-L6-v2`（軽量で汎用性あり）

---

## ✍️ 修正後のコード例（埋め込み部分）

```python
from langchain_community.embeddings import HuggingFaceEmbeddings

# Embeddingsの設定（OpenAI → Transformersベースに切り替え）
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
```

この変更により、ベクトルストア作成部分は以下のようになります：

```python
# ベクトルストアの作成（FAISS + Transformers）
db = FAISS.from_documents(docs, embeddings)
retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": 3})
```

---

## ✅ Azure OpenAI との共存

LLM（チャット生成）は `AzureChatOpenAI` のままでOKです。  
つまり以下のように「**Azureは生成だけ、ベクトル化はローカル**」というハイブリッド構成が可能です。

```python
from langchain.chat_models import AzureChatOpenAI
from langchain_community.embeddings import HuggingFaceEmbeddings
```

---

## 🧪 .envファイルの整理（Embedding用の設定は不要になります）

```env
# LLM（生成用）にだけ必要
OPENAI_API_KEY=xxxxx
OPENAI_API_BASE=https://your-resource-name.openai.azure.com/
OPENAI_API_VERSION=2023-12-01-preview
OPENAI_DEPLOYMENT_NAME=gpt35-chat
```

---

## 💡メリットと注意点

| ✅ メリット | ⚠️ 注意点 |
|-------------|-----------|
| APIコストが不要 | GPT-4などの文脈と精度はAzure側 |
| ローカルで動作 | `sentence-transformers`モデルのダウンロードが必要 |
| チューニング自由 | 大規模なドキュメント処理はメモリに注意 |

---

## 📦 その他おすすめモデル

| モデル名 | 特徴 |
|----------|------|
| `all-MiniLM-L6-v2` | 軽量・高速（一般用途） |
| `paraphrase-MiniLM-L12-v2` | パラフレーズに強い |
| `multi-qa-MiniLM-L6-cos-v1` | Q&A用途でベクトル検索精度◎ |

---

必要であれば、全文コードもこの構成でまとめ直しますよ！どうしますか？

# **user**:

はい、全文コードをこの構成でまとめ直してください。

# **assistant**:

了解しました！  
以下は、**Streamlit + LangChain + Azure OpenAI（生成） + HuggingFace Embeddings（ベクトル検索）** の構成で、**PDFに基づいてAIとチャットできるアプリ**の**全文コード**です。

---

## ✅ 事前準備

### インストールコマンド：

```bash
pip install streamlit langchain langchain-community sentence-transformers faiss-cpu pypdf python-dotenv
```

### `.env`ファイルの例：

```env
OPENAI_API_KEY=sk-xxxxx
OPENAI_API_BASE=https://your-resource-name.openai.azure.com/
OPENAI_API_VERSION=2023-12-01-preview
OPENAI_DEPLOYMENT_NAME=gpt35-chat
```

---

## ✅ `app.py`（全文コード）

```python
import os
import streamlit as st
from dotenv import load_dotenv
from datetime import datetime
import json
import csv
import io

from langchain.chat_models import AzureChatOpenAI
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import ConversationalRetrievalChain

# 環境変数の読み込み
load_dotenv()

# Streamlit 設定
st.set_page_config(page_title="PDFチャットアプリ", page_icon="📄")
st.title("📄 PDFに基づいてAzure OpenAIとチャット")

# PDF アップロード
uploaded_file = st.file_uploader("📎 PDFファイルをアップロードしてください", type="pdf")

# チャット履歴の初期化
if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。PDFの内容について答えてください。"}
    ]
if "chat_history" not in st.session_state:
    st.session_state.chat_history = []

# PDF 読み込み＆ベクトルストア作成
if uploaded_file:
    with st.spinner("PDFを読み込んで処理中..."):
        # PDFを読み込む
        loader = PyPDFLoader(uploaded_file)
        documents = loader.load()

        # 文書を分割
        splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
        docs = splitter.split_documents(documents)

        # Embeddingモデル（Hugging Face）を使ってベクトル化
        embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
        vectorstore = FAISS.from_documents(docs, embeddings)

        # Retrieverの作成
        retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})

        # Azure OpenAI の LLM モデル（チャット用）
        llm = AzureChatOpenAI(
            deployment_name=os.getenv("OPENAI_DEPLOYMENT_NAME"),
            openai_api_key=os.getenv("OPENAI_API_KEY"),
            openai_api_base=os.getenv("OPENAI_API_BASE"),
            openai_api_version=os.getenv("OPENAI_API_VERSION"),
            openai_api_type="azure",
            temperature=0,
        )

        # RAG チェーンの作成
        qa_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=retriever)

        st.success("✅ 処理完了！PDFに基づいたチャットが可能です。")

        # チャット入力欄
        user_input = st.chat_input("メッセージを入力してください")

        if user_input:
            # ユーザー入力を表示・保存
            st.chat_message("user").markdown(user_input)
            st.session_state.messages.append({"role": "user", "content": user_input})

            # 回答生成
            with st.chat_message("assistant"):
                with st.spinner("考え中..."):
                    result = qa_chain({"question": user_input, "chat_history": st.session_state.chat_history})
                    reply = result["answer"]
                    st.markdown(reply)

            # チャット履歴保存
            st.session_state.messages.append({"role": "assistant", "content": reply})
            st.session_state.chat_history.append((user_input, reply))

# 🔁 チャット履歴リセットボタン
if st.button("🔄 チャット履歴をリセット"):
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。PDFの内容について答えてください。"}
    ]
    st.session_state.chat_history = []
    st.experimental_rerun()

# 💾 チャット履歴の保存ボタン（JSON, CSV, Markdown）
messages_to_save = [m for m in st.session_state.messages if m["role"] != "system"]
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")

def save_chat_as_json():
    return json.dumps(messages_to_save, ensure_ascii=False, indent=2)

def save_chat_as_csv():
    output = io.StringIO()
    writer = csv.DictWriter(output, fieldnames=["role", "content"])
    writer.writeheader()
    writer.writerows(messages_to_save)
    return output.getvalue()

def save_chat_as_markdown():
    lines = []
    for m in messages_to_save:
        role_label = "**User:**" if m["role"] == "user" else "**Assistant:**"
        content = m["content"].replace("\n", "  \n")
        lines.append(f"{role_label} {content}")
    return "\n\n".join(lines)

st.download_button(
    label="💾 JSONで保存",
    data=save_chat_as_json(),
    file_name=f"chat_log_{timestamp}.json",
    mime="application/json",
)

st.download_button(
    label="💾 CSVで保存",
    data=save_chat_as_csv(),
    file_name=f"chat_log_{timestamp}.csv",
    mime="text/csv",
)

st.download_button(
    label="📝 Markdownで保存",
    data=save_chat_as_markdown(),
    file_name=f"chat_log_{timestamp}.md",
    mime="text/markdown",
)
```

---

## 💬 次にやること

- 複数PDFへの対応（やりたければ）
- チャット履歴の保持・再利用
- LangChainの `ChatPromptTemplate` を使ったプロンプトの微調整

---

必要であれば、このコードをもとにブログ記事テンプレート化もできますよ！どうしましょうか？

# **user**:

ありがとうございます。アプリは立ち上がりましたが、PDFをアップロードしようとしたら次のエラーになりました。

```
ValueError: File path UploadedFile(file_id='7b0838bf-f45c-43d3-adaf-b8b13b2b2a2b', name='attack.mitre.org.tactics.enterprise.pdf', type='application/pdf', size=46588, _file_urls=file_id: "7b0838bf-f45c-43d3-adaf-b8b13b2b2a2b" upload_url: "/_stcore/upload_file/4cbbe319-4d9b-41ce-b99e-b7dcdbeaf023/7b0838bf-f45c-43d3-adaf-b8b13b2b2a2b" delete_url: "/_stcore/upload_file/4cbbe319-4d9b-41ce-b99e-b7dcdbeaf023/7b0838bf-f45c-43d3-adaf-b8b13b2b2a2b" ) is not a valid file or url
Traceback:
File "/Users/ikemen/streamlit-app/app.py", line 38, in <module>
    loader = PyPDFLoader(uploaded_file)
File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/langchain_community/document_loaders/pdf.py", line 281, in __init__
    super().__init__(file_path, headers=headers)
File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/langchain_community/document_loaders/pdf.py", line 140, in __init__
    raise ValueError("File path %s is not a valid file or url" % self.file_path)
```

# **assistant**:

このエラーは、`PyPDFLoader` に `Streamlit` の `uploaded_file` オブジェクトを直接渡したために発生しています。  
`PyPDFLoader` は **ファイルパス（`str`）** を要求しますが、`uploaded_file` は `UploadedFile` オブジェクトです。

---

## ✅ 解決方法：一時ファイルとして保存してから読み込む

`uploaded_file` を一時ファイルに保存し、そのパスを `PyPDFLoader` に渡すように修正します。

---

### 🔧 修正箇所：

**以下のように修正してください：**

```python
import tempfile

if uploaded_file:
    with st.spinner("PDFを読み込んで処理中..."):
        # 一時ファイルに保存
        with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
            tmp_file.write(uploaded_file.read())
            tmp_path = tmp_file.name

        # その一時ファイルのパスを PyPDFLoader に渡す
        loader = PyPDFLoader(tmp_path)
        documents = loader.load()
```

---

### 💡 解説

- `tempfile.NamedTemporaryFile(delete=False)` を使ってアップロードPDFを一時保存
- `tmp_file.write(uploaded_file.read())` でバイトとして保存
- `tmp_file.name` で得られるパスを `PyPDFLoader` に渡す

---

## ✅ 追加でおすすめ：アプリ終了時に一時ファイルを削除したい場合

```python
import atexit
import os

# 終了時に一時ファイルを削除
@atexit.register
def cleanup_temp_file():
    if tmp_path and os.path.exists(tmp_path):
        os.remove(tmp_path)
```

（ただし `tmp_path` をグローバル変数にするか、管理方法を工夫する必要があります）

---

この修正で、PDFアップロード→ベクトル化→チャットが正常に動作するはずです。  
うまくいったら、ブログ記事用の書き方も一緒に整えましょうか？

# **user**:

ありがとうございます。次のエラーになりました。

```
RuntimeError: Numpy is not available
Traceback:
File "/Users/ikemen/streamlit-app/app.py", line 55, in <module>
    vectorstore = FAISS.from_documents(docs, embeddings)
File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/langchain_core/vectorstores/base.py", line 848, in from_documents
    return cls.from_texts(texts, embedding, metadatas=metadatas, **kwargs)
File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/langchain_community/vectorstores/faiss.py", line 1043, in from_texts
    embeddings = embedding.embed_documents(texts)
File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/langchain_community/embeddings/huggingface.py", line 115, in embed_documents
    embeddings = self.client.encode(
File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/sentence_transformers/SentenceTransformer.py", line 742, in encode
    all_embeddings = np.asarray([emb.numpy() for emb in all_embeddings])
File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/sentence_transformers/SentenceTransformer.py", line 742, in <listcomp>
    all_embeddings = np.asarray([emb.numpy() for emb in all_embeddings])
```

# **assistant**:

このエラー：

```
RuntimeError: Numpy is not available
```

は、`sentence-transformers` がベクトルを `np.asarray()` で扱おうとしているのに **NumPy がインストールされていない** ために発生しています。

---

## ✅ 解決方法：NumPyをインストール

以下のコマンドを実行して NumPy をインストールしてください：

```bash
pip install numpy
```

または、Streamlitアプリの開発環境が `requirements.txt` を使っている場合は、そこにも追加してください：

```txt
numpy
```

---

## 🧠 なぜ必要なの？

- `sentence-transformers` や `FAISS` は、ベクトル演算に NumPy を使っています。
- `embeddings = self.client.encode(...)` の出力は NumPy の `array` 型に変換される必要があります。
- そのため NumPy は**必須の依存ライブラリ**です。

---

## 🛠 補足（他に必要になりそうな依存）

もし他にもエラーが出る場合は、以下のライブラリも確認しておくと安心です：

```bash
pip install faiss-cpu sentence-transformers
```

---

NumPyをインストールしたら、アプリをもう一度実行してみてください。うまく動けば、今の構成をブログ記事としてもまとめ直しましょうか？

# **user**:

numpyはインストール済みですし、pip install faiss-cpu sentence-transformers も実行したのに、同じエラーが出ます。ステップバイステップで解決していただけませんか。

# **assistant**:

了解しました！それでは、`RuntimeError: Numpy is not available` の問題を**ステップバイステップで**一緒に解決していきましょう。

---

## 🧪 ステップ 1：NumPy が本当に使えるか確認する

まず、現在の Python 環境で NumPy が使えるか確認します。

### ✅ このテストを実行してみてください：

```python
import numpy as np
print(np.__version__)
print(np.array([1, 2, 3]))
```

このコードを単独で実行したときに **エラーが出るかどうか**を教えてください。  
→ もしここでエラーが出るなら、NumPyが正しくインストールされていません。

---

## 🧪 ステップ 2：環境の確認

あなたの環境は以下のようになっていますか？

| 項目 | 値 |
|------|----|
| Python バージョン | `3.10`（OK） |
| 仮想環境 | `venv` を使用中（OK） |
| NumPy インストール場所 | 仮想環境の中 |

以下のコマンドで NumPy がどこに入ってるか確認してみてください：

```bash
pip show numpy
```

ここで表示される `Location:` が、今使ってる仮想環境のパスと一致しているか確認してください。

---

## 🧪 ステップ 3：`sentence-transformers` の中で Numpy が使えるかを確認

以下のスクリプトを仮に `test_embed.py` などに保存して実行してみてください：

```python
from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')
sentence = "これはテストです。"
embedding = model.encode(sentence)

print("埋め込みベクトル（先頭5個）:", embedding[:5])
```

これが動作するか教えてください。

---

## 🧪 ステップ 4：PyTorch の確認

実はこのエラー、NumPy ではなく **PyTorch → NumPy変換**のところで失敗している可能性が高いです。

以下のコードを実行してみてください：

```python
import torch
import numpy as np

tensor = torch.tensor([1.0, 2.0, 3.0])
np_array = tensor.numpy()
print(np_array)
```

これで `numpy()` を呼び出したときにエラーが出るなら、PyTorch の内部設定またはインストールが壊れている可能性があります。

---

## 🧪 ステップ 5：PyTorch の再インストール

エラーが解決しない場合、以下のコマンドで PyTorch を入れ直してみてください（CPU版）：

```bash
pip uninstall torch -y
pip install torch --index-url https://download.pytorch.org/whl/cpu
```

※ M1 Mac などの特殊環境を使っている場合は、環境に応じたインストールが必要です。

---

## ✅ 最終チェック：再実行

上記すべてがうまくいった後、再度 Streamlit アプリを起動してエラーが解消されているかを確認します。

---

## 💬 おまけ：FAISSとSentenceTransformerの代替策も検討可能

万が一、どうしても組み合わせに問題が出る場合は：

- FAISS → `Chroma`
- SentenceTransformer → `openai/text-embedding-ada-002`

などに切り替えることもできます。

---

まずは **ステップ1〜3** を試してみて、どこで詰まっているか教えていただけますか？それに応じて次のステップをご案内します。

# **user**:

test_embed.py の実行結果は次のとおりでした。

```
A module that was compiled using NumPy 1.x cannot be run in
NumPy 2.2.4 as it may crash. To support both 1.x and 2.x
versions of NumPy, modules must be compiled with NumPy 2.0.
Some module may need to rebuild instead e.g. with 'pybind11>=2.12'.

If you are a user of the module, the easiest solution will be to
downgrade to 'numpy<2' or try to upgrade the affected module.
We expect that some modules will need time to support NumPy 2.

Traceback (most recent call last):  File "/Users/ikemen/streamlit-app/test_embed.py", line 1, in <module>
    from sentence_transformers import SentenceTransformer
  File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/sentence_transformers/__init__.py", line 9, in <module>
    from sentence_transformers.backend import (
  File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/sentence_transformers/backend.py", line 11, in <module>
    from sentence_transformers.util import disable_datasets_caching, is_datasets_available
  File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/sentence_transformers/util.py", line 17, in <module>
    import torch
  File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/torch/__init__.py", line 1477, in <module>
    from .functional import *  # noqa: F403
  File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/torch/functional.py", line 9, in <module>
    import torch.nn.functional as F
  File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/torch/nn/__init__.py", line 1, in <module>
    from .modules import *  # noqa: F403
  File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/torch/nn/modules/__init__.py", line 35, in <module>
    from .transformer import TransformerEncoder, TransformerDecoder, \
  File "/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/torch/nn/modules/transformer.py", line 20, in <module>
    device: torch.device = torch.device(torch._C._get_default_device()),  # torch.device('cpu'),
/Users/ikemen/azure-chat-app/venv/lib/python3.10/site-packages/torch/nn/modules/transformer.py:20: UserWarning: Failed to initialize NumPy: _ARRAY_API not found (Triggered internally at /Users/runner/work/pytorch/pytorch/pytorch/torch/csrc/utils/tensor_numpy.cpp:84.)
  device: torch.device = torch.device(torch._C._get_default_device()),  # torch.device('cpu'),
```

# **assistant**:

ありがとうございます！はい、これは**NumPy 2.x系の互換性問題**が原因で、特に PyTorch や `sentence-transformers` の一部モジュールがまだ NumPy 2 に正式対応していないことによるものです。

---

## 🔥 原因まとめ

- あなたの環境では **NumPy 2.2.4** がインストールされています。
- 一部のライブラリ（PyTorchや`sentence-transformers`）が NumPy 1.x 向けにビルドされており、NumPy 2.x で動かすとクラッシュする。
- そのため `torch` を読み込んだ瞬間にエラーが発生している。

---

## ✅ 解決方法：NumPy のバージョンを 1.x にダウングレードする

以下のコマンドで NumPy を 1系に戻してください（**1.26.x が安定**）：

```bash
pip uninstall numpy -y
pip install numpy==1.26.4
```

その後、再び `test_embed.py` を実行してみてください。正常にベクトルが出力されれば問題解消です。

---

## ✅ 念のための再インストール（必要に応じて）

NumPy をダウングレードしたあと、もしまだエラーが出るようであれば、PyTorch と `sentence-transformers` も再インストールしておくと安心です：

```bash
pip uninstall torch sentence-transformers -y
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install sentence-transformers
```

---

## 🚀 次のステップ

NumPy をダウングレードしたら、以下を順番に試してみてください：

1. `test_embed.py` が正常に動くか
2. Streamlit アプリが起動して、PDFアップロード・チャットが成功するか

---

### ❓補足：NumPy 2.x の対応はいつ？

NumPy 2.x はまだ発展途上で、PyTorch やその他の科学技術系ライブラリが2024年〜2025年にかけて対応していく予定です。現時点では **NumPy 1.26系の使用が推奨** です。

---

うまくいくか確認してみてください！もし問題が続くようであれば、また一緒に見ていきましょう。

# **user**:

ありがとうございました。解決しました。アプリは想定通り動きました。最終的なコードは次のとおりです。

```python
import os
import streamlit as st
from dotenv import load_dotenv
from datetime import datetime
import json
import csv
import io
import numpy as np

from langchain.chat_models import AzureChatOpenAI
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import ConversationalRetrievalChain

# 環境変数の読み込み
load_dotenv()

# Streamlit 設定
st.set_page_config(page_title="PDFチャットアプリ", page_icon="📄")
st.title("📄 PDFに基づいてAzure OpenAIとチャット")

# PDF アップロード
uploaded_file = st.file_uploader("📎 PDFファイルをアップロードしてください", type="pdf")

# チャット履歴の初期化
if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。PDFの内容について答えてください。"}
    ]
if "chat_history" not in st.session_state:
    st.session_state.chat_history = []

# PDF 読み込み＆ベクトルストア作成
import tempfile

if uploaded_file:
    with st.spinner("PDFを読み込んで処理中..."):
        # 一時ファイルに保存
        with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
            tmp_file.write(uploaded_file.read())
            tmp_path = tmp_file.name

        # その一時ファイルのパスを PyPDFLoader に渡す
        loader = PyPDFLoader(tmp_path)
        documents = loader.load()

        # 文書を分割
        splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
        docs = splitter.split_documents(documents)

        # Embeddingモデル（Hugging Face）を使ってベクトル化
        embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
        vectorstore = FAISS.from_documents(docs, embeddings)

        # Retrieverの作成
        retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})

        # Azure OpenAI の LLM モデル（チャット用）
        llm = AzureChatOpenAI(
            deployment_name=os.getenv("OPENAI_DEPLOYMENT_NAME"),
            openai_api_key=os.getenv("OPENAI_API_KEY"),
            openai_api_base=os.getenv("OPENAI_API_BASE"),
            openai_api_version=os.getenv("OPENAI_API_VERSION"),
            openai_api_type="azure",
            temperature=0,
        )

        # RAG チェーンの作成
        qa_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=retriever)

        st.success("✅ 処理完了！PDFに基づいたチャットが可能です。")

        # チャット入力欄
        user_input = st.chat_input("メッセージを入力してください")

        if user_input:
            # ユーザー入力を表示・保存
            st.chat_message("user").markdown(user_input)
            st.session_state.messages.append({"role": "user", "content": user_input})

            # 回答生成
            with st.chat_message("assistant"):
                with st.spinner("考え中..."):
                    result = qa_chain({"question": user_input, "chat_history": st.session_state.chat_history})
                    reply = result["answer"]
                    st.markdown(reply)

            # チャット履歴保存
            st.session_state.messages.append({"role": "assistant", "content": reply})
            st.session_state.chat_history.append((user_input, reply))

# 🔁 チャット履歴リセットボタン
if st.button("🔄 チャット履歴をリセット"):
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。PDFの内容について答えてください。"}
    ]
    st.session_state.chat_history = []
    st.experimental_rerun()

# 💾 チャット履歴の保存ボタン（JSON, CSV, Markdown）
messages_to_save = [m for m in st.session_state.messages if m["role"] != "system"]
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")

def save_chat_as_json():
    return json.dumps(messages_to_save, ensure_ascii=False, indent=2)

def save_chat_as_csv():
    output = io.StringIO()
    writer = csv.DictWriter(output, fieldnames=["role", "content"])
    writer.writeheader()
    writer.writerows(messages_to_save)
    return output.getvalue()

def save_chat_as_markdown():
    lines = []
    for m in messages_to_save:
        role_label = "**User:**" if m["role"] == "user" else "**Assistant:**"
        content = m["content"].replace("\n", "  \n")
        lines.append(f"{role_label} {content}")
    return "\n\n".join(lines)

st.download_button(
    label="💾 JSONで保存",
    data=save_chat_as_json(),
    file_name=f"chat_log_{timestamp}.json",
    mime="application/json",
)

st.download_button(
    label="💾 CSVで保存",
    data=save_chat_as_csv(),
    file_name=f"chat_log_{timestamp}.csv",
    mime="text/csv",
)

st.download_button(
    label="📝 Markdownで保存",
    data=save_chat_as_markdown(),
    file_name=f"chat_log_{timestamp}.md",
    mime="text/markdown",
)
```

ここまでの議論をブログに書きたいと思います。タイトル案を列挙していただけますか？

# **assistant**:

素晴らしいです！トラブルを乗り越えてアプリを完成させた流れ、ブログにぴったりのテーマですね。以下にブログの**タイトル案**をいくつかご提案します。技術的・体験的・読者目線の視点を織り交ぜています：

---

### 🚀 成功体験を共有する系
1. **「PDFをチャット相手に：LangChain × Azure OpenAI × Streamlitで作る簡単RAGアプリ」**
2. **「エラーと格闘しながら完成！PDFベースのChatGPTアプリを作ってみた」**
3. **「StreamlitでPDFチャットアプリをゼロから構築した記録」**
4. **「RAGアプリ開発体験記：LangChainとHuggingFaceでPDFを対話可能に」**

---

### 🧠 技術キーワード重視のSEO寄り系
5. **「LangChainとHuggingFace EmbeddingsでPDFチャットアプリを構築する方法」**
6. **「Azure OpenAIとFAISSで実現するPDF×チャットのRAGアプリ開発入門」**
7. **「sentence-transformersとFAISSで構築するLangChainベースのPDF QAアプリ」**
8. **「NumPy 2.x対応の落とし穴と回避策付き！LangChain × Streamlit 実践例」**

---

### 👶 初心者や実用重視読者向け
9. **「初心者でもできた！PDFチャットアプリをLangChainとStreamlitで作ってみた」**
10. **「ChatGPTでPDFの中身と話すアプリを自作してみた【簡単ステップ解説】」**
11. **「失敗と修正を繰り返してたどり着いた！PDFベースのChatアプリ開発メモ」**

---

### 🛠 エラー対応・トラブル解決メイン系
12. **「RuntimeError: Numpy is not available を乗り越えて作るPDFチャットアプリ」**
13. **「ModuleNotFound, ValueError... それでも作ったRAGアプリ開発の全記録」**
14. **「HuggingFace Embeddingsを使ったLangChainアプリ開発とその落とし穴」**

---

どの方向性で書きたいか（技術的、初心者向け、エラー対処、成功ストーリーなど）を教えていただければ、さらに絞った案もご提案できますよ！

# **user**:

「LangChain × Azure OpenAI × HuggingFace Embeddings × Streamlit でPDFチャットアプリを構築する」にしようと思います。ここまでの議論のまとめを書いてください。

# **assistant**:

いいタイトルですね！構成してきた技術や試行錯誤のプロセスが一目で伝わる、読み応えのある記事になりそうです。以下に、ここまでの議論の**まとめ記事（本文ドラフト）**を提案します。ブログ記事の導入・構成・実装・トラブル対応・感想まで一通りカバーしています：

---

# LangChain × Azure OpenAI × HuggingFace Embeddings × Streamlit でPDFチャットアプリを構築する

## はじめに

今回は、LangChain、Azure OpenAI、HuggingFaceの埋め込みモデル、そしてStreamlitを組み合わせて、**PDFの内容を元にChatGPTと会話できるアプリ**を構築しました。

最終的にはスムーズに動くアプリが完成しましたが、途中では**「Numpy is not available」エラー**や**PyPDFLoaderのパス問題**など、いくつものトラブルにも遭遇。この記事では、構築プロセスとともに、それらのトラブルの解決方法も共有します。

---

## 使用した技術スタック

- **LangChain**：RAG（Retrieval-Augmented Generation）を簡単に構築できるフレームワーク
- **Azure OpenAI**：ChatGPTのAPI（gpt-35-turbo）をAzure経由で利用
- **HuggingFace Embeddings**：OpenAI Embeddingsの代替としてローカルで実行
- **FAISS**：効率的なベクトル検索ライブラリ
- **Streamlit**：アプリUIを素早く作成

---

## 開発の流れ

### 1. PDFアップロードと読み込み
Streamlitを使ってPDFファイルをアップロードし、一時ファイルとして保存。その後、LangChainの`PyPDFLoader`で文書を読み込み。

```python
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
    tmp_file.write(uploaded_file.read())
    tmp_path = tmp_file.name

loader = PyPDFLoader(tmp_path)
documents = loader.load()
```

### 2. 文書の分割とベクトル化
文書をチャンク化してから、`HuggingFaceEmbeddings` を用いてベクトル化。使用モデルは `"all-MiniLM-L6-v2"`。

```python
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
docs = splitter.split_documents(documents)

embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
vectorstore = FAISS.from_documents(docs, embeddings)
```

### 3. RetrieverとConversational Chainの構築
LangChainの`ConversationalRetrievalChain`を使って、過去の会話履歴と関連文書を元に回答を生成。

```python
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})
qa_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=retriever)
```

### 4. チャットUIの構築
Streamlitの`chat_input`と`chat_message`を活用して、双方向チャット形式のUIを構築。チャット履歴はセッションで保持。

---

## つまずいたポイントと解決策

### ✅ `ValueError: File path ... is not a valid file or url`
Streamlitの`file_uploader`で取得される`UploadedFile`オブジェクトを直接`PyPDFLoader`に渡すと失敗。**一時ファイルに保存してから読み込む**ことで解決。

---

### ✅ `RuntimeError: Numpy is not available`
これは実際には **NumPy 2.x系とPyTorchなどの依存関係による互換性問題**。エラーメッセージに従い、以下で解決：

```bash
pip uninstall numpy
pip install "numpy<2"
```

---

## 最終的なコードのポイント

- OpenAIのEmbeddingではなく、ローカルで動作するHuggingFaceモデルを使用
- Streamlitだけで完結するUI
- チャット履歴をJSON / CSV / Markdownで保存可能
- トラブルを乗り越えた実践的な開発例

---

## おわりに

今回の構築プロセスは、LangChainの理解を深めるとともに、LLMアプリの実践的な問題解決にもつながる良い学習体験となりました。

特に、**「EmbeddingをOpenAI以外で行いたい」**という要件に対して、`sentence-transformers` + `FAISS`の組み合わせが非常に強力であることを実感しました。

この記事が、同じような構成でアプリを作りたい方や、トラブルに悩まされている方の参考になれば嬉しいです！

---

ご希望に合わせて「導入」「コード解説」「よくあるエラー」「まとめ」など、もっと分割して詳しく展開することもできます。必要ならお知らせください！

# **user**:

ありがとうございました。また次回もよろしくお願いします！

# **assistant**:

こちらこそありがとうございました！無事にアプリが動いて本当によかったです👏  
次回も何かあれば、エラーでもアイデア相談でも大歓迎ですので、いつでも気軽にどうぞ〜😊

楽しんで開発＆執筆、進めてくださいね！またお会いできるのを楽しみにしています✨

# **user**:

すみません、「追加でおすすめ：アプリ終了時に一時ファイルを削除したい場合」のコードを追加し忘れてしまったので、追加した最終版を全コード書いていただけますか？

# **assistant**:

もちろんです！「アプリ終了時に一時ファイルを削除する処理」を含めた **最終版のフルコード** を以下にまとめました👇  
`tempfile` で作成したPDFファイルを削除するために、`atexit` モジュールを使ってアプリ終了時にクリーンアップするようにしています。

---

```python
import os
import streamlit as st
from dotenv import load_dotenv
from datetime import datetime
import json
import csv
import io
import numpy as np
import tempfile
import atexit

from langchain.chat_models import AzureChatOpenAI
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import ConversationalRetrievalChain

# 環境変数の読み込み
load_dotenv()

# Streamlit 設定
st.set_page_config(page_title="PDFチャットアプリ", page_icon="📄")
st.title("📄 PDFに基づいてAzure OpenAIとチャット")

# チャット履歴の初期化
if "messages" not in st.session_state:
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。PDFの内容について答えてください。"}
    ]
if "chat_history" not in st.session_state:
    st.session_state.chat_history = []

# PDF アップロード
uploaded_file = st.file_uploader("📎 PDFファイルをアップロードしてください", type="pdf")

# 一時ファイルのパス（後で削除するため）
temp_pdf_path = None

# PDF 処理
if uploaded_file:
    with st.spinner("PDFを読み込んで処理中..."):
        # 一時ファイルに保存
        with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
            tmp_file.write(uploaded_file.read())
            temp_pdf_path = tmp_file.name

        # アプリ終了時に一時ファイルを削除
        def cleanup_temp_file():
            if temp_pdf_path and os.path.exists(temp_pdf_path):
                os.remove(temp_pdf_path)

        atexit.register(cleanup_temp_file)

        # PyPDFLoaderでPDF読込
        loader = PyPDFLoader(temp_pdf_path)
        documents = loader.load()

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

        # Embeddingモデル（Hugging Face）でベクトル化
        embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
        vectorstore = FAISS.from_documents(docs, embeddings)

        # Retriever作成
        retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})

        # Azure OpenAI チャットモデル
        llm = AzureChatOpenAI(
            deployment_name=os.getenv("OPENAI_DEPLOYMENT_NAME"),
            openai_api_key=os.getenv("OPENAI_API_KEY"),
            openai_api_base=os.getenv("OPENAI_API_BASE"),
            openai_api_version=os.getenv("OPENAI_API_VERSION"),
            openai_api_type="azure",
            temperature=0,
        )

        # RAG チェーン構築
        qa_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=retriever)

        st.success("✅ 処理完了！PDFに基づいたチャットが可能です。")

        # ユーザー入力
        user_input = st.chat_input("メッセージを入力してください")

        if user_input:
            # ユーザー入力の表示と記録
            st.chat_message("user").markdown(user_input)
            st.session_state.messages.append({"role": "user", "content": user_input})

            # 回答生成
            with st.chat_message("assistant"):
                with st.spinner("考え中..."):
                    result = qa_chain({"question": user_input, "chat_history": st.session_state.chat_history})
                    reply = result["answer"]
                    st.markdown(reply)

            # チャット履歴を保存
            st.session_state.messages.append({"role": "assistant", "content": reply})
            st.session_state.chat_history.append((user_input, reply))

# 🔁 チャット履歴リセット
if st.button("🔄 チャット履歴をリセット"):
    st.session_state.messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。PDFの内容について答えてください。"}
    ]
    st.session_state.chat_history = []
    st.experimental_rerun()

# 💾 チャット履歴保存（JSON / CSV / Markdown）
messages_to_save = [m for m in st.session_state.messages if m["role"] != "system"]
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")

def save_chat_as_json():
    return json.dumps(messages_to_save, ensure_ascii=False, indent=2)

def save_chat_as_csv():
    output = io.StringIO()
    writer = csv.DictWriter(output, fieldnames=["role", "content"])
    writer.writeheader()
    writer.writerows(messages_to_save)
    return output.getvalue()

def save_chat_as_markdown():
    lines = []
    for m in messages_to_save:
        role_label = "**User:**" if m["role"] == "user" else "**Assistant:**"
        content = m["content"].replace("\n", "  \n")
        lines.append(f"{role_label} {content}")
    return "\n\n".join(lines)

st.download_button(
    label="💾 JSONで保存",
    data=save_chat_as_json(),
    file_name=f"chat_log_{timestamp}.json",
    mime="application/json",
)

st.download_button(
    label="💾 CSVで保存",
    data=save_chat_as_csv(),
    file_name=f"chat_log_{timestamp}.csv",
    mime="text/csv",
)

st.download_button(
    label="📝 Markdownで保存",
    data=save_chat_as_markdown(),
    file_name=f"chat_log_{timestamp}.md",
    mime="text/markdown",
)
```

---

これで **一時ファイルがアプリ終了時に自動削除** されるようになり、ローカル環境がクリーンに保たれます👌  
他にも改善したい点があれば、いつでも相談してくださいね！

# **user**:

ありがとうございました！では、また今度！

# **assistant**:

こちらこそありがとうございました！  
アプリもバッチリ動いていて嬉しいです 😊

またいつでも相談してくださいね。お待ちしてます！では、良い開発ライフを〜🚀💻