# RAG 文件搜索系統實作方案

## 概述

實現用戶上傳文件 → Gemini 搜索相關片段 → Claude 回答的 RAG 系統。

### 核心功能

1. **多檔上傳**：用戶在 Dashboard 上傳多個文件（PDF、TXT、DOCX 等）
2. **用戶隔離**：每個用戶只能搜索自己的文件
3. **智能搜索**：使用 Gemini File Search API 找到相關片段
4. **Claude 回答**：將片段加入 prompt，由 Claude SDK 生成回答

### 系統架構

```
┌─────────────────────────────────────────────────────────────────────┐
│  用戶 Dashboard                                                      │
│                                                                     │
│  1. 上傳文件 ──────────────────────────────────────────────────────►│
│  2. 提問 "關於 XXX 的內容？"                                         │
└───────────────────────────┬─────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Flask Server                                                       │
│                                                                     │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐ │
│  │  文件上傳處理    │    │  Gemini 搜索    │    │  Claude 回答    │ │
│  │                 │    │                 │    │                 │ │
│  │  • 接收文件     │───►│  • 語義搜索     │───►│  • 組合 prompt  │ │
│  │  • 存入 Corpus  │    │  • 取得片段     │    │  • 調用 Claude  │ │
│  │  • 用戶隔離     │    │  • 用戶過濾     │    │  • 返回答案     │ │
│  └─────────────────┘    └─────────────────┘    └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
                            │                            │
                            ▼                            ▼
                   ┌─────────────────┐          ┌─────────────────┐
                   │  Gemini API     │          │  Claude API     │
                   │  (File Search)  │          │  (回答生成)     │
                   └─────────────────┘          └─────────────────┘
```

---

## 第一部分：Gemini File Search API 設定

### 1.1 取得 Gemini API Key

1. 前往 [Google AI Studio](https://aistudio.google.com/)
2. 點擊 **Get API Key**
3. 建立或選擇專案
4. 複製 API Key

### 1.2 安裝依賴

```bash
pip install google-generativeai
```

### 1.3 Gemini Semantic Retrieval 概念

Gemini 使用 **Corpus（語料庫）** 來管理文件：

```
Corpus (語料庫)
├── Document 1
│   ├── Chunk 1 (文字片段)
│   ├── Chunk 2
│   └── Chunk 3
├── Document 2
│   ├── Chunk 1
│   └── Chunk 2
└── ...
```

**用戶隔離策略**：每個用戶一個 Corpus

---

## 第二部分：資料庫 Schema 擴充

### 2.1 新增資料表

```sql
-- 用戶的 Gemini Corpus
CREATE TABLE IF NOT EXISTS user_corpus (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL UNIQUE,
    corpus_name TEXT NOT NULL,           -- Gemini corpus 名稱
    corpus_display_name TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- 用戶上傳的文件
CREATE TABLE IF NOT EXISTS user_documents (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    corpus_id INTEGER NOT NULL,
    document_name TEXT NOT NULL,          -- Gemini document 名稱
    original_filename TEXT NOT NULL,      -- 原始檔名
    file_type TEXT,                       -- pdf, txt, docx
    file_size INTEGER,
    chunk_count INTEGER DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (corpus_id) REFERENCES user_corpus(id)
);

-- 搜索歷史（可選）
CREATE TABLE IF NOT EXISTS search_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    query TEXT NOT NULL,
    result_count INTEGER,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
```

---

## 第三部分：Gemini Corpus 管理

### 3.1 建立用戶 Corpus

```python
# gemini_service.py

import google.generativeai as genai
from google.generativeai import caching

# 設定 API Key
GEMINI_API_KEY = "your-gemini-api-key"
genai.configure(api_key=GEMINI_API_KEY)


def create_user_corpus(user_id: int, user_email: str) -> str:
    """
    為用戶建立專屬的 Corpus

    Returns:
        corpus_name: Gemini corpus 的唯一名稱
    """
    # 建立 Corpus
    corpus = genai.create_corpus(
        display_name=f"FWAuto-User-{user_id}",
        # 可選：設定描述
    )

    return corpus.name  # 格式: corpora/xxx


def get_or_create_corpus(user_id: int, user_email: str, db) -> str:
    """
    取得用戶的 Corpus，如果不存在則建立
    """
    # 檢查資料庫
    existing = db.execute(
        'SELECT corpus_name FROM user_corpus WHERE user_id = ?',
        (user_id,)
    ).fetchone()

    if existing:
        return existing['corpus_name']

    # 建立新 Corpus
    corpus_name = create_user_corpus(user_id, user_email)

    # 存入資料庫
    db.execute(
        'INSERT INTO user_corpus (user_id, corpus_name, corpus_display_name) VALUES (?, ?, ?)',
        (user_id, corpus_name, f"FWAuto-User-{user_id}")
    )
    db.commit()

    return corpus_name
```

### 3.2 上傳文件到 Corpus

```python
def upload_document_to_corpus(
    corpus_name: str,
    file_path: str,
    display_name: str
) -> dict:
    """
    上傳文件到用戶的 Corpus

    Args:
        corpus_name: 用戶的 corpus 名稱
        file_path: 文件路徑
        display_name: 顯示名稱

    Returns:
        document_info: 包含 document_name 和 chunk_count
    """
    # 取得 corpus
    corpus = genai.get_corpus(name=corpus_name)

    # 讀取文件內容
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()

    # 建立 document
    document = corpus.create_document(
        display_name=display_name,
    )

    # 將內容分割成 chunks 並加入
    chunks = split_into_chunks(content)

    for i, chunk_text in enumerate(chunks):
        document.create_chunk(
            data=chunk_text,
            # 可選：加入 metadata
            custom_metadata=[
                {"key": "chunk_index", "string_value": str(i)},
            ]
        )

    return {
        "document_name": document.name,
        "chunk_count": len(chunks)
    }


def split_into_chunks(content: str, chunk_size: int = 1000, overlap: int = 200) -> list[str]:
    """
    將文件內容分割成片段

    Args:
        content: 文件內容
        chunk_size: 每個片段的大小（字元數）
        overlap: 重疊的字元數
    """
    chunks = []
    start = 0

    while start < len(content):
        end = start + chunk_size
        chunk = content[start:end]
        chunks.append(chunk)
        start = end - overlap

    return chunks
```

### 3.3 搜索文件

```python
def search_user_documents(
    corpus_name: str,
    query: str,
    top_k: int = 5
) -> list[dict]:
    """
    在用戶的 Corpus 中搜索相關片段

    Args:
        corpus_name: 用戶的 corpus 名稱
        query: 搜索查詢
        top_k: 返回前幾個結果

    Returns:
        list of {text, score, document_name}
    """
    corpus = genai.get_corpus(name=corpus_name)

    # 執行語義搜索
    results = corpus.query(
        query=query,
        results_count=top_k
    )

    # 整理結果
    chunks = []
    for result in results:
        chunks.append({
            "text": result.chunk.data,
            "score": result.relevance_score,
            "document_name": result.chunk.document_name,
        })

    return chunks
```

---

## 第四部分：Flask API 實作

### 4.1 文件上傳端點

```python
# app.py 新增

import os
from werkzeug.utils import secure_filename

UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'docx', 'md'}

os.makedirs(UPLOAD_FOLDER, exist_ok=True)


def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/api/documents/upload', methods=['POST'])
@require_auth
def upload_documents():
    """
    上傳文件（支援多檔）

    Request:
        multipart/form-data
        files: 多個文件
    """
    user = g.current_user

    if 'files' not in request.files:
        return jsonify({"error": "No files provided"}), 400

    files = request.files.getlist('files')

    if not files or all(f.filename == '' for f in files):
        return jsonify({"error": "No files selected"}), 400

    # 取得或建立用戶的 Corpus
    db = get_db()
    corpus_name = get_or_create_corpus(user['id'], user['email'], db)

    uploaded = []
    errors = []

    for file in files:
        if file.filename == '':
            continue

        if not allowed_file(file.filename):
            errors.append(f"{file.filename}: 不支援的檔案格式")
            continue

        try:
            # 儲存文件
            filename = secure_filename(file.filename)
            filepath = os.path.join(UPLOAD_FOLDER, f"{user['id']}_{filename}")
            file.save(filepath)

            # 上傳到 Gemini Corpus
            doc_info = upload_document_to_corpus(
                corpus_name=corpus_name,
                file_path=filepath,
                display_name=filename
            )

            # 記錄到資料庫
            corpus_id = db.execute(
                'SELECT id FROM user_corpus WHERE user_id = ?',
                (user['id'],)
            ).fetchone()['id']

            db.execute('''
                INSERT INTO user_documents
                (user_id, corpus_id, document_name, original_filename, file_type, file_size, chunk_count)
                VALUES (?, ?, ?, ?, ?, ?, ?)
            ''', (
                user['id'],
                corpus_id,
                doc_info['document_name'],
                filename,
                filename.rsplit('.', 1)[1].lower(),
                os.path.getsize(filepath),
                doc_info['chunk_count']
            ))

            uploaded.append({
                "filename": filename,
                "chunks": doc_info['chunk_count']
            })

            # 刪除本地文件（可選）
            os.remove(filepath)

        except Exception as e:
            errors.append(f"{file.filename}: {str(e)}")

    db.commit()

    return jsonify({
        "uploaded": uploaded,
        "errors": errors
    })


@app.route('/api/documents', methods=['GET'])
@require_auth
def list_documents():
    """
    列出用戶的所有文件
    """
    user = g.current_user
    db = get_db()

    documents = db.execute('''
        SELECT original_filename, file_type, file_size, chunk_count, created_at
        FROM user_documents
        WHERE user_id = ?
        ORDER BY created_at DESC
    ''', (user['id'],)).fetchall()

    return jsonify({
        "documents": [
            {
                "filename": d['original_filename'],
                "type": d['file_type'],
                "size": d['file_size'],
                "chunks": d['chunk_count'],
                "uploaded_at": d['created_at']
            }
            for d in documents
        ]
    })


@app.route('/api/documents/<int:doc_id>', methods=['DELETE'])
@require_auth
def delete_document(doc_id):
    """
    刪除文件
    """
    user = g.current_user
    db = get_db()

    # 取得文件資訊
    doc = db.execute(
        'SELECT document_name FROM user_documents WHERE id = ? AND user_id = ?',
        (doc_id, user['id'])
    ).fetchone()

    if not doc:
        return jsonify({"error": "Document not found"}), 404

    # 從 Gemini 刪除
    try:
        genai.delete_document(name=doc['document_name'])
    except Exception:
        pass  # 可能已經不存在

    # 從資料庫刪除
    db.execute('DELETE FROM user_documents WHERE id = ?', (doc_id,))
    db.commit()

    return jsonify({"status": "deleted"})
```

### 4.2 搜索端點

```python
@app.route('/api/search', methods=['POST'])
@require_auth
def search_documents():
    """
    搜索用戶的文件

    Request:
        {
            "query": "搜索內容",
            "top_k": 5  // 可選，預設 5
        }

    Response:
        {
            "results": [
                {"text": "片段內容", "score": 0.95, "document": "filename.pdf"},
                ...
            ]
        }
    """
    user = g.current_user
    data = request.json

    query = data.get('query')
    top_k = data.get('top_k', 5)

    if not query:
        return jsonify({"error": "Query is required"}), 400

    # 取得用戶的 Corpus
    db = get_db()
    corpus = db.execute(
        'SELECT corpus_name FROM user_corpus WHERE user_id = ?',
        (user['id'],)
    ).fetchone()

    if not corpus:
        return jsonify({"results": [], "message": "尚未上傳任何文件"})

    # 執行搜索
    results = search_user_documents(
        corpus_name=corpus['corpus_name'],
        query=query,
        top_k=top_k
    )

    # 對應回原始檔名
    doc_names = {
        d['document_name']: d['original_filename']
        for d in db.execute(
            'SELECT document_name, original_filename FROM user_documents WHERE user_id = ?',
            (user['id'],)
        ).fetchall()
    }

    return jsonify({
        "results": [
            {
                "text": r['text'],
                "score": r['score'],
                "document": doc_names.get(r['document_name'], 'unknown')
            }
            for r in results
        ]
    })
```

### 4.3 RAG 問答端點（搜索 + Claude 回答）

```python
from anthropic import Anthropic

claude_client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))


@app.route('/api/ask', methods=['POST'])
@require_auth
def ask_with_rag():
    """
    RAG 問答：搜索文件 + Claude 回答

    Request:
        {
            "question": "問題內容",
            "top_k": 5  // 可選
        }

    Response:
        {
            "answer": "Claude 的回答",
            "sources": [
                {"text": "引用片段", "document": "filename.pdf"},
                ...
            ]
        }
    """
    user = g.current_user
    data = request.json

    question = data.get('question')
    top_k = data.get('top_k', 5)

    if not question:
        return jsonify({"error": "Question is required"}), 400

    # 1. 搜索相關片段
    db = get_db()
    corpus = db.execute(
        'SELECT corpus_name FROM user_corpus WHERE user_id = ?',
        (user['id'],)
    ).fetchone()

    context_chunks = []
    if corpus:
        results = search_user_documents(
            corpus_name=corpus['corpus_name'],
            query=question,
            top_k=top_k
        )
        context_chunks = results

    # 2. 組合 prompt
    if context_chunks:
        context_text = "\n\n---\n\n".join([
            f"[來源: {c.get('document_name', 'unknown')}]\n{c['text']}"
            for c in context_chunks
        ])

        prompt = f"""根據以下參考資料回答問題。如果資料中沒有相關資訊，請說明。

## 參考資料

{context_text}

## 問題

{question}

## 回答

請根據上述參考資料回答問題："""
    else:
        prompt = f"""用戶尚未上傳任何文件。

問題：{question}

請告知用戶需要先上傳文件才能進行文件相關的問答。"""

    # 3. 呼叫 Claude
    response = claude_client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=2048,
        messages=[
            {"role": "user", "content": prompt}
        ]
    )

    answer = response.content[0].text

    # 4. 記錄用量
    db.execute('''
        INSERT INTO usage_log (user_id, task_type, model, input_tokens, output_tokens, cost_usd)
        VALUES (?, ?, ?, ?, ?, ?)
    ''', (
        user['id'],
        'rag_query',
        'claude-sonnet-4',
        response.usage.input_tokens,
        response.usage.output_tokens,
        None  # 費用可後續計算
    ))
    db.commit()

    # 5. 回傳結果
    doc_names = {
        d['document_name']: d['original_filename']
        for d in db.execute(
            'SELECT document_name, original_filename FROM user_documents WHERE user_id = ?',
            (user['id'],)
        ).fetchall()
    }

    return jsonify({
        "answer": answer,
        "sources": [
            {
                "text": c['text'][:200] + "..." if len(c['text']) > 200 else c['text'],
                "document": doc_names.get(c.get('document_name'), 'unknown'),
                "score": c['score']
            }
            for c in context_chunks
        ],
        "usage": {
            "input_tokens": response.usage.input_tokens,
            "output_tokens": response.usage.output_tokens
        }
    })
```

---

## 第五部分：Dashboard UI 更新

### 5.1 文件上傳區塊

```html
<!-- 新增到 dashboard.html -->

<div class="upload-card">
    <h3>📁 我的文件</h3>

    <!-- 上傳區域 -->
    <div class="upload-area" id="uploadArea">
        <input type="file" id="fileInput" multiple accept=".txt,.pdf,.docx,.md" hidden>
        <p>📤 拖放文件到這裡，或 <a href="#" onclick="document.getElementById('fileInput').click()">點擊上傳</a></p>
        <p class="hint">支援 TXT, PDF, DOCX, MD（可多選）</p>
    </div>

    <!-- 文件列表 -->
    <div id="documentList">
        <!-- 動態載入 -->
    </div>
</div>

<style>
.upload-card {
    background: white;
    padding: 25px;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    margin-bottom: 20px;
}

.upload-area {
    border: 2px dashed #ddd;
    border-radius: 10px;
    padding: 30px;
    text-align: center;
    cursor: pointer;
    transition: border-color 0.3s;
}

.upload-area:hover {
    border-color: #4285f4;
}

.upload-area.dragover {
    border-color: #4285f4;
    background: #f8f9ff;
}

.hint {
    color: #888;
    font-size: 12px;
}

.document-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px;
    border-bottom: 1px solid #eee;
}

.document-item:last-child {
    border-bottom: none;
}

.delete-btn {
    color: #dc3545;
    cursor: pointer;
}
</style>

<script>
// 文件上傳
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');

uploadArea.addEventListener('dragover', (e) => {
    e.preventDefault();
    uploadArea.classList.add('dragover');
});

uploadArea.addEventListener('dragleave', () => {
    uploadArea.classList.remove('dragover');
});

uploadArea.addEventListener('drop', (e) => {
    e.preventDefault();
    uploadArea.classList.remove('dragover');
    handleFiles(e.dataTransfer.files);
});

fileInput.addEventListener('change', () => {
    handleFiles(fileInput.files);
});

async function handleFiles(files) {
    const formData = new FormData();
    for (const file of files) {
        formData.append('files', file);
    }

    try {
        const response = await fetch('/api/documents/upload', {
            method: 'POST',
            body: formData
        });

        const result = await response.json();

        if (result.uploaded.length > 0) {
            alert(`成功上傳 ${result.uploaded.length} 個文件`);
            loadDocuments();
        }

        if (result.errors.length > 0) {
            alert('部分文件上傳失敗:\n' + result.errors.join('\n'));
        }
    } catch (error) {
        alert('上傳失敗: ' + error.message);
    }
}

async function loadDocuments() {
    const response = await fetch('/api/documents');
    const data = await response.json();

    const list = document.getElementById('documentList');

    if (data.documents.length === 0) {
        list.innerHTML = '<p style="color: #888; text-align: center;">尚未上傳任何文件</p>';
        return;
    }

    list.innerHTML = data.documents.map(doc => `
        <div class="document-item">
            <span>📄 ${doc.filename} (${doc.chunks} 片段)</span>
            <span class="delete-btn" onclick="deleteDocument(${doc.id})">🗑️</span>
        </div>
    `).join('');
}

async function deleteDocument(docId) {
    if (!confirm('確定要刪除這個文件？')) return;

    await fetch(`/api/documents/${docId}`, { method: 'DELETE' });
    loadDocuments();
}

// 頁面載入時載入文件列表
loadDocuments();
</script>
```

### 5.2 問答區塊

```html
<div class="ask-card">
    <h3>💬 文件問答</h3>

    <div class="ask-input">
        <input type="text" id="questionInput" placeholder="輸入問題..." />
        <button onclick="askQuestion()">送出</button>
    </div>

    <div id="answerArea" style="display: none;">
        <h4>回答：</h4>
        <div id="answerText"></div>

        <h4>參考來源：</h4>
        <div id="sourcesList"></div>
    </div>
</div>

<style>
.ask-card {
    background: white;
    padding: 25px;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.ask-input {
    display: flex;
    gap: 10px;
    margin-bottom: 20px;
}

.ask-input input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 5px;
}

.ask-input button {
    padding: 10px 20px;
    background: #4285f4;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

#answerText {
    background: #f8f9fa;
    padding: 15px;
    border-radius: 5px;
    margin-bottom: 15px;
    white-space: pre-wrap;
}

.source-item {
    background: #fff3cd;
    padding: 10px;
    border-radius: 5px;
    margin-bottom: 10px;
    font-size: 14px;
}

.source-doc {
    color: #666;
    font-size: 12px;
}
</style>

<script>
async function askQuestion() {
    const question = document.getElementById('questionInput').value;
    if (!question.trim()) return;

    document.getElementById('answerArea').style.display = 'none';

    try {
        const response = await fetch('/api/ask', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ question })
        });

        const data = await response.json();

        document.getElementById('answerText').textContent = data.answer;

        document.getElementById('sourcesList').innerHTML = data.sources.map(s => `
            <div class="source-item">
                <div>${s.text}</div>
                <div class="source-doc">📄 ${s.document} (相關度: ${(s.score * 100).toFixed(1)}%)</div>
            </div>
        `).join('');

        document.getElementById('answerArea').style.display = 'block';

    } catch (error) {
        alert('查詢失敗: ' + error.message);
    }
}

// Enter 鍵送出
document.getElementById('questionInput').addEventListener('keypress', (e) => {
    if (e.key === 'Enter') askQuestion();
});
</script>
```

---

## 第六部分：用戶隔離機制

### 6.1 隔離策略

```
用戶 A
├── Corpus: corpora/user-a-xxx
│   ├── Document 1 (只有用戶 A 能搜索)
│   └── Document 2
│
用戶 B
├── Corpus: corpora/user-b-xxx
│   ├── Document 3 (只有用戶 B 能搜索)
│   └── Document 4
```

### 6.2 安全檢查

```python
def ensure_user_owns_corpus(user_id: int, corpus_name: str, db) -> bool:
    """確認 corpus 屬於該用戶"""
    result = db.execute(
        'SELECT 1 FROM user_corpus WHERE user_id = ? AND corpus_name = ?',
        (user_id, corpus_name)
    ).fetchone()
    return result is not None


def ensure_user_owns_document(user_id: int, document_id: int, db) -> bool:
    """確認文件屬於該用戶"""
    result = db.execute(
        'SELECT 1 FROM user_documents WHERE user_id = ? AND id = ?',
        (user_id, document_id)
    ).fetchone()
    return result is not None
```

---

## 第七部分：完整 API 列表

### 文件管理

| 端點 | 方法 | 說明 |
|------|------|------|
| `/api/documents/upload` | POST | 上傳文件（多檔） |
| `/api/documents` | GET | 列出用戶文件 |
| `/api/documents/<id>` | DELETE | 刪除文件 |

### 搜索 & 問答

| 端點 | 方法 | 說明 |
|------|------|------|
| `/api/search` | POST | 搜索文件片段 |
| `/api/ask` | POST | RAG 問答（搜索 + Claude 回答） |

---

## 第八部分：費用估算

### Gemini File Search API

- 免費額度：每月一定量的搜索
- 超出後按量計費

### Claude API

- 根據 input/output tokens 計費
- 每次 RAG 查詢約消耗：
  - Input: ~2000-5000 tokens（含 context）
  - Output: ~500-1000 tokens

### 建議

1. 限制每個用戶的文件數量和大小
2. 限制每日查詢次數
3. 在 Dashboard 顯示用量統計

---

## 第九部分：部署注意事項

### 環境變數

```bash
export GEMINI_API_KEY="your-gemini-api-key"
export ANTHROPIC_API_KEY="your-anthropic-api-key"
export GOOGLE_CLIENT_ID="your-google-client-id"
export GOOGLE_CLIENT_SECRET="your-google-client-secret"
```

### 文件大小限制

```python
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB
```

### 安全性

1. 驗證文件類型（不只看副檔名）
2. 掃描惡意文件
3. 限制上傳頻率

---

## 快速參考

### 流程圖

```
用戶上傳文件
     │
     ▼
Flask 接收 → 存入 Gemini Corpus（用戶專屬）
     │
     ▼
用戶提問
     │
     ▼
Gemini 搜索用戶的 Corpus → 取得相關片段
     │
     ▼
組合 prompt（片段 + 問題）
     │
     ▼
Claude SDK 生成回答
     │
     ▼
返回答案 + 引用來源
```

### 關鍵程式碼

```python
# 1. 建立用戶 Corpus
corpus = genai.create_corpus(display_name=f"User-{user_id}")

# 2. 上傳文件
document = corpus.create_document(display_name="filename")
document.create_chunk(data="文件內容片段")

# 3. 搜索
results = corpus.query(query="問題", results_count=5)

# 4. Claude 回答
prompt = f"根據以下資料回答：\n{context}\n\n問題：{question}"
response = claude_client.messages.create(model="claude-sonnet-4-20250514", ...)
```

---

## 結論

這個方案可以實現：

✅ 用戶上傳多個文件
✅ 每個用戶只能搜索自己的文件（Corpus 隔離）
✅ 使用 Gemini 進行語義搜索
✅ 使用 Claude 生成回答
✅ 完整的用量追蹤

需要的 API：
- Google Gemini API（File Search）
- Anthropic Claude API（回答生成）
