Cloudflareで自宅PCのAPIサーバを公開して、Difyの外部ナレッジAPIとして活用する手順

## 自宅PCのAPIサーバを公開して、Difyの外部ナレッジAPIとして活用する手順です。
＜Difyで使うかどうかに関わらず、Webサーバを公開する際に便利です＞

https://www.creationline.com/tech-blog/cloudnative/75142
上記の記事を参考にして作業しました。
＜terraformが出てきたところから分からなかったので、Chatgptに聞きながら作業しました＞

以下Chatgptとの対話です。

### 質問 <ここは概要を理解するための質問なので、この通り実施しない>
#### Cloudflare Tunnel:　をWINDOWSで使う手順を詳細に教えて

Cloudflare TunnelをWindowsで使用する手順を説明します：

1.Cloudflaredのインストール:
Cloudflareのダウンロードページ から Windows用の実行ファイルをダウンロード
ダウンロードしたexeファイルを実行してインストール

2.Cloudflareアカウントの設定:
PowerShellを管理者として実行し、以下のコマンドを実行
cloudflared.exe login

ブラウザが開き、Cloudflareにログインするように促される
ログイン後、認証証明書が自動的に保存される
（通常は C:\Users\<ユーザー名>\.cloudflared\cert.pem）

3.トンネルの作成:
トンネルの作成
cloudflared.exe tunnel create rag-api

トンネルのIDを確認（後で使用）
cloudflared.exe tunnel list

4.設定ファイルの作成:
C:\Users\<ユーザー名>\.cloudflared\config.yml を作成：

```
tunnel: <トンネルID>
credentials-file: C:\Users\<ユーザー名>\.cloudflared\<トンネルID>.json

ingress:
  - hostname: your-domain.com
    service: http://localhost:8000
  - service: http_status:404
```
5.DNSレコードの設定:
DNSレコードを作成
cloudflared.exe tunnel route dns rag-api yourdomain.com

6.トンネルの実行:
トンネルを起動
cloudflared.exe tunnel run rag-api

7.Windowsサービスとして実行（オプション）:
サービスとしてインストール
cloudflared.exe service install

サービスを開始
Start-Service cloudflared

8.動作確認:
テストAPIの実行
```
curl -X POST https://rag-api.yourdomain.com/retrieval \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your_API_KEY" \
  -d '{
    "knowledge_id": "test-knowledge",
    "query": "RAGについて教えて",
    "retrieval_setting": {
        "top_k": 3,
        "score_threshold": 0.5
    }
}'
```
#### セキュリティ上の注意点：
config.yml のパーミッションを適切に設定
証明書ファイル（cert.pem）を安全に保管
APIキーを環境変数として設定
ファイアウォールの設定を確認
トラブルシューティング：ログは C:\Users\<ユーザー名>\.cloudflared\ に保存される
エラーが発生した場合は 
cloudflared.exe tunnel info rag-api で状態を確認
必要に応じて --loglevel debug オプションを使用


次のステップ：
1.トンネルの作成：
cloudflared.exe tunnel create rag-api-tunnel

2.設定ファイルの作成（C:\Users\<ユーザー名>\.cloudflared\config.yml）：
```
tunnel: 3196188c3762a6aef47dd852d124
credentials-file: C:\Users\<ユーザー名>\.cloudflared\3196188c3762a6aef47dd852d124.json

ingress:
  - hostname: your-domain.cloudflare.com
    service: http://localhost:8000
  - service: http_status:404
```

3.トンネルの起動：
cloudflared.exe tunnel run rag-api-tunnel

### <ここまでは概要を理解するための質問なので、この通り実施しない>


## 質問 windows11 で、Cloudflare を活用して、現在作成中のmain.pyを公開したら、Difyの外部ナレッジとして活用できるよね？

はい、その通りです。現在のコードをCloudflare Tunnelで公開するための手順を説明します：
1.必要な追加パッケージ:
```
scikit-learn
numpy
pandas
flask
python-dotenv
mecab-python3
unidic-lite
gunicorn  # 本番環境用のWSGIサーバー
```

2.config.py の作成 (まだない場合):
```
import os
from dotenv import load_dotenv
load_dotenv()
# APIキー設定
API_KEY = os.getenv("API_KEY", "your_API_KEY")
# RAG設定
DEFAULT_TOP_K = 3
DEFAULT_SCORE_THRESHOLD = 0.5
CSV_PATH = "RAG.csv"
# サーバー設定
HOST = "0.0.0.0"
PORT = int(os.getenv("PORT", 8000))
```

3.Cloudflare Tunnelのセットアップ手順:
3-1. cloudflaredをインストール
https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/ 
から最新のWindows用インストーラーをダウンロード

3-2. Cloudflareにログイン
cloudflared-windows-amd64.exe login

3-3. トンネルを作成
cloudflared-windows-amd64.exe tunnel create rag-api

3-4. トンネルのIDをメモ
cloudflared-windows-amd64.exe tunnel list

4.設定ファイルの作成 (C:\Users\<ユーザー名>\.cloudflared\config.yml):
```
tunnel: <トンネルID>
credentials-file: C:\Users\<ユーザー名>\.cloudflared\<トンネルID>.json

ingress:
  - hostname: rag-api.yourdomain.com
    service: http://localhost:8000
  - service: http_status:404

# オプションの設定
originRequest:
  connectTimeout: 30s
  noTLSVerify: true
```

5.DNSレコードの設定:
cloudflared.exe tunnel route dns rag-api rag-api.yourdomain.com

6.トンネルの起動:
cloudflared.exe tunnel run rag-api

これで、Difyの外部ナレッジ設定で以下のように設定できます：
URL: https://rag-api.yourdomain.com/retrieval
Method: POST
Headers:
Authorization: Bearer your_API_KEY
Content-Type: application/json

注意点：
1.MeCabのパスが環境に依存しているので、以下のように修正することをお勧めします：
```python
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import logging
import MeCab
import re
import traceback
import os

class RAGEngine:
    def __init__(self, csv_path):
        self.df = pd.read_csv(csv_path, encoding='utf-8')
       
        # MeCabのパスを環境変数から取得するか、デフォルトパスを使用
        mecab_path = os.getenv('MECAB_PATH', '-Owakati')
        self.mecab = MeCab.Tagger(mecab_path)
        # 初期化時のエラーを回避するためのダミー解析
        self.mecab.parse('')
       
        self.vectorizer = TfidfVectorizer(
            analyzer='char_wb',
            ngram_range=(2, 3),
            max_features=10000,
            min_df=1,
            max_df=0.95,
            sublinear_tf=True,
            token_pattern=r'(?u)\b\w+\b'  # 単語の境界をより厳密に
        )
        self._prepare_vectors()
       
    def tokenize(self, text):
        """テキストを形態素解析して単語に分割"""
        try:
            text = self.preprocess_text(text)
            result = self.mecab.parse(text)
            if result is None:
                logging.error("MeCab parse returned None")
                return []
            tokens = result.strip().split()
            logging.debug(f"Tokenized text: {tokens}")  # デバッグ用ログ
            return tokens
        except Exception as e:
            logging.error(f"Tokenization error for text '{text}': {str(e)}")
            return []
       
    def preprocess_text(self, text):
        """テキストの前処理"""
        if not isinstance(text, str):
            text = str(text)
        # 改行を空白に置換
        text = re.sub(r'\n', ' ', text)
        # 特殊文字を削除
        text = re.sub(r'[【】「」『』（）\[\]（）\(\)]+', ' ', text)
        # 連続する空白を1つに置換
        text = re.sub(r'\s+', ' ', text)
        return text.strip()
       
    def _prepare_vectors(self):
        """質問のベクトル化を行う"""
        try:
            # 質問テキストを抽出してベクトル化
            questions = self.df['question'].fillna('')
            # 前処理を適用
            processed_questions = [self.preprocess_text(q) for q in questions]
            self.question_vectors = self.vectorizer.fit_transform(processed_questions)
           
            # デバッグ用ログ
            logging.info("Vectors prepared successfully")
            logging.debug(f"Total questions: {len(processed_questions)}")
            for i, (orig, proc) in enumerate(zip(questions, processed_questions)):
                logging.debug(f"Q{i}: Original: {orig}")
                logging.debug(f"Q{i}: Processed: {proc}")
           
        except Exception as e:
            logging.error(f"Error preparing vectors: {str(e)}")
            raise

    def search(self, query, top_k=5, score_threshold=0.5):
        """クエリに類似した質問-回答ペアを検索する"""
        try:
            # ク���リの前処理
            processed_query = self.preprocess_text(query)
            logging.debug(f"Search query: {query} -> {processed_query}")
           
            # クエリをベクトル化
            query_vector = self.vectorizer.transform([processed_query])
           
            # コサイン類似度を計算
            similarities = cosine_similarity(query_vector, self.question_vectors).flatten()
           
            # クエリの意図を分析
            query_keywords = {
                'RAG': ['rag', 'retrieval', 'augmented', 'generation'],
                'Dify': ['dify', 'defy', 'platform', 'プラットフォーム'],
                # 他のトピックも必要に応じて追加
            }
           
            # クエリがどのトピックに関連するか判定
            query_lower = query.lower()
            matched_topic = None
            for topic, keywords in query_keywords.items():
                if any(keyword in query_lower for keyword in keywords):
                    matched_topic = topic
                    break
           
            if matched_topic:
                # トピックに関連する問のスコアを調整
                for i, question in enumerate(self.df['question']):
                    question_lower = question.lower()
                    if any(keyword in question_lower for keyword in query_keywords[matched_topic]):
                        similarities[i] *= 2.0  # 関トピックのスコアを増加
                    else:
                        similarities[i] *= 0.5  # 無係なトピックのスコアを減少
           
            # スコアの正規化（0-1の範囲に）
            max_score = similarities.max()
            if max_score > 0:
                similarities = similarities / max_score
           
            # 上位k件を取得
            top_indices = np.argsort(similarities)[-top_k:][::-1]
           
            results = []
            for idx in top_indices:
                score = float(similarities[idx])
                if score >= score_threshold:  # スコアが閾値以上の結果のみを返す
                    row = self.df.iloc[idx]
                    result = {
                        "content": str(row['Answer']),
                        "score": score,
                        "title": row['Title'],
                        "metadata": {}
                    }
                    if 'url' in row:
                        result["metadata"]["url"] = row['url']
                    results.append(result)
                    logging.info(f"Found match: {row['question']} (score: {score:.4f})")
           
            return results
           
        except Exception as e:
            logging.error(f"Search error: {str(e)}\n{traceback.format_exc()}")
            raise
```

2.セキュリティ強化のために、API_KEYを環境変数から取得するようにしましょう。
これらの設定が完了すれば、DifyからRAG APIとして利用できるようになります。


### 質問 WEB画面でDNSレコードを修正するなどの作業は必要ないですか？

#### Cloudflareのウェブ画面での設定も必要です。以下の手順で設定を行います：
1.Cloudflareのダッシュボードにログイン:
https://dash.cloudflare.com にアクセス

2.ドメインの設定:
使用するドメインを選択（または新規追加）
「DNS」タブを選択

3.DNSレコードの追加:
「レコードを追加」をクリック
以下の設定を入力：
```
Type: CNAME
Name: rag-api（サブドメイン名）
Target: your-tunnel-id.cfargotunnel.com
Proxy status: Proxied（オレンジの雲マーク）
```

4.SSL/TLS設定:
「SSL/TLS」タブを選択
暗号化モード: 「Full」または「Full (Strict)」を選択

5.ゼロトラストの設定（オプション）:
「Zero Trust」→「Access」→「Applications」
「Add an application」をクリック
アプリケーション名とドメインを設定
必要に応じてアクセス制御を設定

6.トンネルの確認:
トンネルの状態確認
cloudflared.exe tunnel info rag-api

DNSレコードの確認
cloudflared.exe tunnel route dns rag-api rag-api.yourdomain.com

これらの設定が完了したら、以下のURLでAPIにアクセスできるようになります：
https://rag-api.yourdomain.com/retrieval

Difyの外部ナレッジ設定では、このURLを使用します。
注意点：DNSの反映には数分から数時間かかる場合があります
SSL証明書の生成と適用も自動的に行われますが、少し時間がかかります
初回アクセス時にCloudflareの保護機能による確認が入る場合があります


### 質問 これは、トンネルIDですか？　
```
.\cloudflared-windows-amd64.exe tunnel create rag-api

Tunnel credentials written to C:\Users\tarou\.cloudflared\e773085c-a587-4c1a-b7f7.json. cloudflared chose this file based on where your origin certificate was found. Keep this file secret. To revoke these credentials, delete the tunnel.

Created tunnel rag-api with id e773085c-a587-4c1a-b7f7
```

はい、その通りです。トンネルIDは: e773085c-a587-4c1a-b7f7 です。
このIDを使って、C:\Users\tarou\.cloudflared\config.yml を以下のように作成します：
```
tunnel: e773085c-a587-4c1a-b7f7
credentials-file: C:\Users\tarou\.cloudflared\e773085c-a587-4c1a-b7f7.json

ingress:
  - hostname: rag-api.your-domain.com
    service: http://localhost:8000
  - service: http_status:404

#オプションの設定
originRequest:
  connectTimeout: 30s
  noTLSVerify: true
```

このトンネルIDは、以下のような操作でも確認できます：
.\cloudflared-windows-amd64.exe tunnel list

このIDは重要な情報なので、安全に保管してください。トンネルを削除する必要がある場合は、このIDを使用します。

### 質問 DNSレコードの設定はＯＫ？　
```
.\cloudflared-windows-amd64.exe tunnel route dns rag-api rag-api.yourdomain.com
2024-12-08T10:39:18Z INF Added CNAME rag-api.yourdomain.com which will route to this tunnel tunnelID=e773085c-a587-4c1a-b7f7
PS C:\Users\tarou\DifyAPI> 
```

はい、DNSレコードの設定は正常に完了しています。メッセージの内容から：
1.CNAMEレコードが正常に追加されました

2.ホスト名: rag-api.yourdomain.com

3.トンネルID: e773085c-a587-4c1a-b7f7
次のステップとして、config.yml を以下のように更新する必要があります：
```
tunnel: e773085c-a587-4c1a-b7f7
credentials-file: C:\Users\tarou\.cloudflared\e773085c-a587-4c1a-b7f7.json

ingress:
  - hostname: rag-api.yourdomain.com
    service: http://localhost:8000
  - service: http_status:404

originRequest:
  connectTimeout: 30s
  noTLSVerify: true
```

これで、以下のURLでAPIにアクセスできるようになります：
https://rag-api.yourdomain.com/retrieval

トンネルを起動するには：
.\cloudflared-windows-amd64.exe tunnel run rag-api

### 質問
- self.mecab = MeCab.Tagger('-Owakati -r "C:/Program Files/MeCab/etc/mecabrc"')　

を削除して、

mecab_path = os.getenv('MECAB_PATH', '-Owakati')
self.mecab = MeCab.Tagger(mecab_path)

を追加するということは、環境変数に
MECAB_PATH=”C:/Program Files/MeCab/etc/mecabrc” 

と記述が必要ですね？

はい、その通りです。.env ファイルに以下のように環境変数を設定する必要があります：
```
# MeCab設定
MECAB_PATH=-Owakati -r "C:/Program Files/MeCab/etc/mecabrc"

# APIキー設定
API_KEY=your_API_KEY

# サーバー設定
PORT=8000
```
この設定により：rag.py で os.getenv('MECAB_PATH', '-Owakati') を使用したとき、
.env ファイルから設定を読み込みます
環境変数 MECAB_PATH が設定されていない場合は、デフォルト値 -Owakati が使用されます
Windows環境でのMeCabの設定が正しく維持されます
注意点：パスに空白が含まれているため、ダブルクォート " で囲む必要があります
パスの区切り文字はフォワードスラッシュ / を使用します


次のステップ：
1.トンネルを起動：
   .\cloudflared-windows-amd64.exe tunnel run rag-api

2.動作確認：
```
   curl -X POST https://rag-api.yourdomain.com/retrieval \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your_API_KEY" \
     -d '{
       "knowledge_id": "test-knowledge",
       "query": "RAGについて教えて",
       "retrieval_setting": {
           "top_k": 3,
           "score_threshold": 0.5
       }
   }'
```

この状態で、Difyの外部ナレッジベース設定で以下のように設定できます：
```
URL: https://rag-api.yourdomain.com/retrieval
Method: POST
Headers:
  - Content-Type: application/json
  - Authorization: Bearer your_API_KEY

Request Body Format:
{
    "knowledge_id": "test-knowledge",
    "query": "${input}",
    "retrieval_setting": {
        "top_k": 3,
        "score_threshold": 0.5
    }
}
```

### 質問 difyの外部ナレッジベースへの接続画面があります。
以下の２か所を設定をする必要があります。
何を入れれば良い？
１）外部ナレッジ名（ナレッジベースの名前を入力してください）
２）外部ナレッジID（ナレッジIDを入力してください）
外部ナレッジAPIには、「rag-api」と入力されてます

Difyの外部ナレッジベース設定では、以下のように入力することをお勧めします：
1.外部ナレッジ名:
RAG Knowledge Base
または
RAG API Knowledge Base

２．外部ナレッジID
test-knowledge

理由：外部ナレッジIDは、APIリクエストの knowledge_id パラメータと一致する必要があります
現在のAPIコードでは test-knowledge を使用しています（curlコマンドのテストで使用した値）
注意：外部ナレッジ名は管理用の表示名なので、分かりやすい名前を付けることができます
外部ナレッジIDは、APIリクエストで使用される識別子なので、コードと一致させる必要があります

以上