File size: 24,367 Bytes
9f6a257
5fcb72f
9f6a257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47412a4
9f6a257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718ae12
 
 
 
9f6a257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718ae12
9f6a257
 
 
 
 
718ae12
9f6a257
 
 
718ae12
9f6a257
718ae12
9f6a257
 
 
 
 
718ae12
9f6a257
718ae12
 
 
 
 
9f6a257
 
 
 
 
 
718ae12
9f6a257
 
 
 
 
 
 
 
 
718ae12
9f6a257
718ae12
9f6a257
718ae12
 
9f6a257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718ae12
9f6a257
 
 
 
 
718ae12
9f6a257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718ae12
 
 
 
 
 
 
9f6a257
 
718ae12
9f6a257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718ae12
 
9f6a257
718ae12
 
 
 
 
 
 
 
 
 
 
 
 
 
96f2763
 
718ae12
 
 
 
 
 
 
 
 
9f6a257
718ae12
 
 
 
 
 
 
 
 
 
 
9f6a257
718ae12
9f6a257
718ae12
 
 
 
 
9f6a257
718ae12
 
 
 
 
9f6a257
718ae12
 
 
9f6a257
718ae12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f6a257
718ae12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96f2763
718ae12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96f2763
718ae12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f6a257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
import streamlit as st
import os
import io
import json
import numpy as np
import faiss
import uuid
import time
import sys

# === HuggingFace 模型相關套件 (新增) ===
try:
    # 確保只在需要時載入,避免在無 GPU 環境下強制載入導致錯誤
    from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
    import torch
    # 針對本地大模型:
    # from accelerate import Accelerator # 建議安裝
    # import bitsandbytes # 建議安裝
except ImportError:
    st.error("請檢查是否安裝了所有 Hugging Face 相關依賴:pip install transformers torch accelerate bitsandbytes")
    # 如果缺少,則退出或將相關變數設為 None
    AutoModelForCausalLM, AutoTokenizer, pipeline, torch = None, None, None, None

# === LangChain/RAG 相關套件 (保持不變) ===
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_core.documents import Document
from langchain_community.vectorstores import FAISS
from langchain_community.vectorstores.utils import DistanceStrategy
from langchain_community.docstore.in_memory import InMemoryDocstore

# 嘗試匯入 pypdf
try:
    import pypdf
except ImportError:
    pypdf = None

# --- 頁面設定 ---
st.set_page_config(page_title="Cybersecurity AI Assistant (Hugging Face RAG & Batch Analysis)", page_icon="🛡️", layout="wide")
st.title("🛡️ Foundation-Sec-1.1-8B-Instruct with FAISS RAG & Batch Analysis")
st.markdown("已啟用:**IndexFlatIP** + **L2 正規化** + **Hugging Face LLM**。上傳 JSON 執行批量分析,上傳其他檔案作為 RAG 參考庫。")

# 設定模型 ID (替換為 Hugging Face 模型名稱)
MODEL_ID = "Qwen/Qwen2.5-7B-Instruct" 
WINDOW_SIZE = 8

# --- 側邊欄設定 ---
with st.sidebar:
    st.header("⚙️ 設定")
    
    # === 替換為 Hugging Face 模型名稱顯示 (移除 API Key 輸入) ===
    st.info(f"LLM 模型:**{MODEL_ID}** (Hugging Face Model)")
    st.warning("⚠️ **注意**: 8B 模型需要大量 RAM/VRAM 和算力。運行可能較慢或失敗。")
    
    st.divider()
    
    st.subheader("📂 檔案上傳")
    # === 1. JSON 批量分析檔案 (新的上傳器) ===
    json_uploaded_file = st.file_uploader(
        "1️⃣ 上傳 **JSON** Log/Alert 檔案 (用於批量分析)",
        type=['json'],
        key="json_uploader"
    )
    # === 2. RAG 知識庫檔案 (新的上傳器) ===
    rag_uploaded_file = st.file_uploader(
        "2️⃣ 上傳 **RAG 參考知識庫** (Logs/PDF/Code 等)",
        type=['txt', 'py', 'log', 'csv', 'md', 'pdf'],
        key="rag_uploader"
    )
    st.divider()
    
    st.subheader("💡 批量分析指令 (針對 JSON 檔案)")
    analysis_prompt = st.text_area(
        "針對每個 Log/Alert 執行的指令",
        value="You are a security expert in charge of analyzing a single alert and prioritizing its criticality. Respond with a clear, structured analysis using the following mandatory sections: \n\n- Criticality/Priority: Is this alert critical? (Answer Yes/No only), and provide the overall priority level. (Answer High, Medium, or Low only) \n- Explanation: If this alert is critical or medium~high priority level, explain the potential impact and why this specific alert requires attention. If not, omit the explanation section. \n- Action Plan: If this alert is critical or medium~high priority level, What should be the immediate steps to address this specific alert? If not, omit the action plan section. \n\nStrictly use the information in the provided Log.",
        height=200
    )
    st.markdown("此指令將對 JSON 檔案中的**每一個 Log 條目**執行一次獨立分析。")
    
    if json_uploaded_file: # 移除 API Key 檢查
        if st.button("🚀 執行批量分析"):
            st.session_state.execute_batch_analysis = True
    else:
        st.info("請上傳 JSON 檔案以啟用批量分析按鈕。")
        
    st.divider()
    
    st.subheader("🔍 RAG 檢索設定")
    similarity_threshold = st.slider(
        "📐 Cosine Similarity 門檻",
        0.0, 1.0, 0.4, 0.01,
        help="數值越大越相似。一般建議 0.4~0.7"
    )
    st.divider()
    
    st.subheader("模型參數")
    system_prompt = st.text_area("System Prompt (LLM 使用)", value="You are a Senior Security Analyst. Be professional.", height=100)
    max_output_tokens = st.slider("Max Output Tokens", 128, 4096, 2048, 128)
    temperature = st.slider("Temperature", 0.0, 1.0, 0.1, 0.1) 
    top_p = st.slider("Top P", 0.1, 1.0, 0.95, 0.05)
    
    st.divider()
    if st.button("🗑️ 清除所有紀錄"):
        for key in list(st.session_state.keys()):
            if key not in []:
                del st.session_state[key]
        st.rerun()

# --- 初始化 Hugging Face LLM Client (重大替換) ---
@st.cache_resource
def load_huggingface_llm(model_id):
    if AutoModelForCausalLM is None:
        st.error("無法載入 Hugging Face 依賴,請安裝:pip install transformers torch accelerate bitsandbytes")
        return None
    try:
        # 使用量化 (4-bit) 減少記憶體消耗,這是運行 8B 模型的常見做法
        tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
        model = AutoModelForCausalLM.from_pretrained(
            model_id,
            torch_dtype=torch.bfloat16 if torch.cuda.is_available() else None,
            device_map="auto", # <--- 讓 accelerate 管理裝置
            trust_remote_code=True,
            # load_in_4bit=True # 如果需要 4-bit 量化
        )
        # 使用 pipeline 簡化呼叫
        llm_pipeline = pipeline(
            "text-generation",
            model=model,
            tokenizer=tokenizer,
            # device=(0 if torch.cuda.is_available() else -1) # <--- **移除此參數**
        )
        st.success(f"Hugging Face 模型 **{model_id}** 載入成功。")
        return llm_pipeline
    except Exception as e:
        st.error(f"Hugging Face 模型載入失敗: {e}")
        return None

# 在 main 區塊外初始化 pipeline
llm_pipeline = None
if AutoModelForCausalLM is not None:
    with st.spinner(f"正在載入 LLM 模型: {MODEL_ID} (8B)... (可能需要數分鐘)"):
        llm_pipeline = load_huggingface_llm(MODEL_ID)

if llm_pipeline is None:
    st.warning("Hugging Face LLM 無法載入。請檢查依賴和環境資源。")
# =======================================================================


# === Embedding 模型 (用於 RAG 參考庫) (保持不變) ===
@st.cache_resource
def load_embedding_model():
    model_kwargs = {
        'device': 'cpu',
        'trust_remote_code': True
    }
    encode_kwargs = {
        'normalize_embeddings': False
    }
    # 選擇一個適合 RAG 的中文 Embedding Model
    return HuggingFaceEmbeddings(
        model_name="BAAI/bge-large-zh-v1.5",
        model_kwargs=model_kwargs,
        encode_kwargs=encode_kwargs
    )

with st.spinner("正在載入 Embedding 模型..."):
    embedding_model = load_embedding_model()

# === 建立向量庫 / Search 函數 (保持不變) ===
def process_file_to_faiss(uploaded_file):
    text_content = ""
    try:
        if uploaded_file.type == "application/pdf":
            if pypdf:
                pdf_reader = pypdf.PdfReader(uploaded_file)
                for page in pdf_reader.pages:
                    text_content += page.extract_text() + "\n"
            else:
                return None, "PDF library missing"
        else:
            stringio = io.StringIO(uploaded_file.getvalue().decode("utf-8"))
            text_content = stringio.read()
            
        if not text_content.strip():
            return None, "File is empty"
        
        # 嘗試以 </Event> 分割 Log,否則以換行符分割
        events = [e + "</Event>" for e in text_content.split("</Event>") if e.strip()]
        if len(events) <= 1:
            events = [line for line in text_content.split("\n") if line.strip()]
            
        docs = [Document(page_content=e) for e in events]
        
        if not docs:
            return None, "No documents created"
        
        embeddings = embedding_model.embed_documents([d.page_content for d in docs])
        embeddings_np = np.array(embeddings).astype("float32")
        faiss.normalize_L2(embeddings_np) # L2 正規化
        
        dimension = embeddings_np.shape[1]
        index = faiss.IndexFlatIP(dimension) # IndexFlatIP (內積)
        index.add(embeddings_np)
        
        doc_ids = [str(uuid.uuid4()) for _ in range(len(docs))]
        docstore = InMemoryDocstore({_id: doc for _id, doc in zip(doc_ids, docs)})
        index_to_docstore_id = {i: _id for i, _id in enumerate(doc_ids)}
        
        vector_store = FAISS(
            embedding_function=embedding_model,
            index=index,
            docstore=docstore,
            index_to_docstore_id=index_to_docstore_id,
            distance_strategy=DistanceStrategy.COSINE # 使用 Cosine 距離 (對應 IndexFlatIP)
        )
        
        return vector_store, f"{len(docs)} chunks created."
    except Exception as e:
        return None, f"Error: {str(e)}"

def faiss_cosine_search_all(vector_store, query, threshold):
    q_emb = embedding_model.embed_query(query)
    q_emb = np.array([q_emb]).astype("float32")
    faiss.normalize_L2(q_emb)
    
    index = vector_store.index
    D, I = index.search(q_emb, k=index.ntotal)
    
    selected = []
    for score, idx in zip(D[0], I[0]):
        if idx == -1: continue
        # IndexFlatIP 輸出內積,與歸一化後的 Cosine Similarity 相同
        if score >= threshold:
            doc_id = vector_store.index_to_docstore_id[idx]
            doc = vector_store.docstore.search(doc_id)
            selected.append((doc, score))
            
    selected.sort(key=lambda x: x[1], reverse=True)
    return selected

# === Hugging Face 生成單一 Log 分析回答 (核心批量處理函數) (重大替換) ===
def generate_rag_response_hf_for_log(llm_pipeline, model_id, log_sequence_text, user_prompt, sys_prompt, vector_store, threshold, max_output_tokens, temperature, top_p):
    """
    使用 Hugging Face LLM 執行 RAG 增強的 Log 序列分析。
    """
    if llm_pipeline is None:
        return "ERROR: Hugging Face LLM Pipeline 未載入。", ""
    
    context_text = ""
    # 1. RAG 檢索邏輯
    if vector_store:
        selected = faiss_cosine_search_all(vector_store, log_sequence_text, threshold)
        if selected:
            retrieved_contents = [
                f"--- Reference Chunk (sim={score:.3f}) ---\n{doc.page_content}"
                for i, (doc, score) in enumerate(selected[:5]) # 限制檢索結果數量
            ]
            context_text = "\n".join(retrieved_contents)
            
    # 2. 建構 Prompt 的 RAG 部分和指令部分 (針對 HF 指令模型)
    rag_instruction = f"""=== RETRIEVED REFERENCE CONTEXT (Cosine ≥ {threshold}) ===
{context_text if context_text else 'No relevant reference context found.'}
=== END REFERENCE CONTEXT ===
ANALYSIS INSTRUCTION: {user_prompt}
Based on the provided LOG SEQUENCE and REFERENCE CONTEXT, you must analyze the **entire sequence** to detect any continuous attack chains or evolving threats. Focus on the **last log entry in the sequence** to determine its final criticality and priority, considering the preceding {WINDOW_SIZE} logs."""

    log_content_section = f"""=== CURRENT LOG SEQUENCE TO ANALYZE (Window Size: {WINDOW_SIZE}) ===
{log_sequence_text}
=== END LOG SEQUENCE ==="""
    
    # 整合 System Prompt、RAG、和 Log 內容
    # 注意:fdtn-ai/Foundation-Sec-1.1-8B-Instruct 遵循 ChatML 格式,但此處使用簡化的 instruction-tuning 格式
    full_prompt = (
        f"**SYSTEM INSTRUCTION**: {sys_prompt}\n\n"
        f"**RAG & ANALYSIS INSTRUCTION**:\n{rag_instruction}\n\n"
        f"**LOG DATA**:\n{log_content_section}\n\n"
        f"**RESPONSE**:"
    )
    
    # 3. 呼叫 Hugging Face Pipeline
    try:
        # Pipeline 參數設定
        response = llm_pipeline(
            full_prompt,
            max_new_tokens=max_output_tokens,
            temperature=temperature,
            top_p=top_p,
            do_sample=True, # 啟用採樣
            return_full_text=False # 只返回生成的文本
        )
        
        # 處理 pipeline 的輸出格式
        if response and isinstance(response, list) and 'generated_text' in response[0]:
            return response[0]['generated_text'].strip(), context_text
        else:
            return f"Hugging Face Pipeline 輸出格式錯誤: {response}", context_text

    except Exception as e:
        # 如果模型呼叫失敗,回傳詳細錯誤訊息
        return f"Hugging Face Model Error: {str(e)}", context_text


# === 檔案處理和主執行邏輯 (保持結構,替換 LLM 呼叫) ===
# 初始化 Session State
if 'execute_batch_analysis' not in st.session_state:
    st.session_state.execute_batch_analysis = False
if 'batch_results' not in st.session_state:
    st.session_state.batch_results = None
    
# --- 1. 處理 RAG 知識庫檔案 (rag_uploaded_file) ---
if 'rag_current_file_key' not in st.session_state:
    st.session_state.rag_current_file_key = None
    
if rag_uploaded_file:
    file_key = f"vs_{rag_uploaded_file.name}_{rag_uploaded_file.size}"
    
    if st.session_state.rag_current_file_key != file_key or 'vector_store' not in st.session_state:
        # 偵測到新 RAG 檔案,需要重新建立知識庫
        with st.spinner(f"正在建立 RAG 參考知識庫 ({rag_uploaded_file.name})..."):
            vs, msg = process_file_to_faiss(rag_uploaded_file)
            if vs:
                st.session_state.vector_store = vs
                st.session_state.rag_current_file_key = file_key
                st.toast(f"RAG 參考知識庫已更新!{msg}", icon="✅")
            else:
                st.error(msg)
# 檔案移除/狀態清理 (如果使用者移除了 RAG 檔案)
elif 'vector_store' in st.session_state:
    del st.session_state.vector_store
    del st.session_state.rag_current_file_key
    st.info("RAG 檔案已移除,已清除相關知識庫。")
    
# --- 2. 處理 JSON 批量分析檔案 (json_uploaded_file) ---
if 'json_current_file_key' not in st.session_state:
    st.session_state.json_current_file_key = None
    
if json_uploaded_file:
    json_file_key = f"json_{json_uploaded_file.name}_{json_uploaded_file.size}"
    
    if st.session_state.json_current_file_key != json_file_key or 'json_data_for_batch' not in st.session_state:
        try:
            # 偵測到新 JSON 檔案
            json_data = json.load(io.StringIO(json_uploaded_file.getvalue().decode("utf-8")))
            st.session_state.json_data_for_batch = json_data
            st.session_state.json_current_file_key = json_file_key
            st.toast("JSON Log 檔案已載入,請按 '執行批量分析'。", icon="📄")
            
        except Exception as e:
            st.error(f"JSON 檔案解析錯誤: {e}")
            if 'json_data_for_batch' in st.session_state:
                del st.session_state.json_data_for_batch
                
# 檔案移除/狀態清理 (如果使用者移除了 JSON 檔案)
elif 'json_data_for_batch' in st.session_state:
    del st.session_state.json_data_for_batch
    del st.session_state.json_current_file_key
    if "batch_results" in st.session_state:
        del st.session_state.batch_results
    st.info("JSON 檔案已移除,已清除日誌數據和分析結果。")

# === 執行批量分析邏輯 (包含顏色控制) ===
if st.session_state.execute_batch_analysis and 'json_data_for_batch' in st.session_state:
    st.session_state.execute_batch_analysis = False
    start_time = time.time() # 開始計時
    st.session_state.batch_results = []
    
    if llm_pipeline is None:
        st.error("Hugging Face LLM Pipeline 未載入,請檢查依賴和環境資源,無法執行批量分析。")
        # 由於這是一個 Streamlit App,我們不直接 st.stop(),讓使用者可以檢查設定
        st.session_state.execute_batch_analysis = False
    
    data_to_process = st.session_state.json_data_for_batch
    
    # 提取 Log 列表的邏輯 (保持不變)
    logs_list = []
    if isinstance(data_to_process, list):
        logs_list = data_to_process
    elif isinstance(data_to_process, dict):
        if all(isinstance(v, (dict, str, list)) for v in data_to_process.values()):
            logs_list = list(data_to_process.values())
        elif 'alerts' in data_to_process and isinstance(data_to_process['alerts'], list):
            logs_list = data_to_process['alerts']
        elif 'logs' in data_to_process and isinstance(data_to_process['logs'], list):
            logs_list = data_to_process['logs']
        else:
            logs_list = [data_to_process]
    else:
        logs_list = [data_to_process]
        
    if logs_list:
        vs = st.session_state.get("vector_store", None)
        if vs:
            st.success("✅ RAG 知識庫已啟用並用於分析。")
        else:
            st.warning("⚠️ RAG 知識庫未載入,將單純執行 Log 分析。")
            
        # --- 新增:創建平移視窗序列 ---
        
        # 將所有 Log 轉換為 JSON 格式化字串列表,以便後續拼接
        formatted_logs = [json.dumps(log, indent=2, ensure_ascii=False) for log in logs_list]
        
        # 創建要分析的序列 (Sliding Window) 列表
        analysis_sequences = []
        
        for i in range(len(formatted_logs)):
            start_index = max(0, i - WINDOW_SIZE + 1)
            end_index = i + 1 # 終點為當前 Log
            
            current_window = formatted_logs[start_index:end_index]
            
            sequence_text = []
            for j, log_str in enumerate(current_window):
                is_target = " <<< TARGET LOG TO ANALYZE" if j == len(current_window) - 1 else ""
                # 使用 i-len(current_window)+j+1 來計算原始索引
                sequence_text.append(f"--- Log Index {i - len(current_window) + j + 1} ({len(current_window)-j} prior logs){is_target} ---\n{log_str}")
            
            analysis_sequences.append({
                "sequence_text": "\n\n".join(sequence_text),
                "target_log_id": i + 1, # 該序列的分析目標是原始列表中的第 i+1 條 Log
                "original_log_entry": logs_list[i]
            })
            
        total_sequences = len(analysis_sequences)
        if total_sequences < WINDOW_SIZE:
            st.warning(f"Log 總數 ({total_sequences}) 少於視窗大小 ({WINDOW_SIZE}),分析的結果可能較不準確。")
            
        # --- 執行序列分析 ---
        st.header(f"⚡ 批量分析執行中 (平移視窗 $N={WINDOW_SIZE}$)...")
        progress_bar = st.progress(0, text=f"準備處理 {total_sequences} 個序列...")
        results_container = st.container()
        full_report_chunks = ["## Cybersecurity Batch Analysis Report\n\n"]
        
        priority_keyword = "Criticality/Priority:"
        
        for i, seq_data in enumerate(analysis_sequences):
            log_id = seq_data["target_log_id"]
            progress_bar.progress((i + 1) / total_sequences, text=f"已處理 {i + 1}/{total_sequences} 個序列 (目標 Log #{log_id})...")
            
            try:
                # *** 替換為 Hugging Face 呼叫函數 ***
                response, retrieved_ctx = generate_rag_response_hf_for_log(
                    llm_pipeline=llm_pipeline, # <--- 新的 LLM pipeline
                    model_id=MODEL_ID,
                    log_sequence_text=seq_data["sequence_text"],
                    user_prompt=analysis_prompt,
                    sys_prompt=system_prompt,
                    vector_store=vs,
                    threshold=similarity_threshold,
                    max_output_tokens=max_output_tokens,
                    temperature=temperature,
                    top_p=top_p
                )
                
                # 儲存結果
                item = {
                    "log_id": log_id,
                    "log_content": seq_data["original_log_entry"], # 記錄原始 Log 條目
                    "sequence_analyzed": seq_data["sequence_text"], # 記錄分析的序列
                    "analysis_result": response,
                    "context": retrieved_ctx
                }
                st.session_state.batch_results.append(item)
                
                # 結果顯示邏輯
                with results_container:
                    st.subheader(f"Log/Alert #{item['log_id']} (序列分析完成)")
                    with st.expander(f"序列內容 (包含 {len(seq_data['sequence_text'].split('--- Log Index'))-1} 條 Log)"):
                        st.code(item["sequence_analyzed"], language='text')
                        
                    # 顏色控制:
                    is_high_priority = False
                    if 'criticality/priority:' in response.lower():
                        try:
                            priority_section = response.split('Criticality/Priority:')[1].split('\n')[0].strip()
                            if 'high' in priority_section.lower() or 'medium' in priority_section.lower() or 'yes' in priority_section.lower():
                                is_high_priority = True
                        except IndexError:
                            pass
                            
                    st.markdown(f"### 🤖 分析結果 (針對 Log #{log_id})")
                    if is_high_priority:
                        st.error(item['analysis_result'])
                    else:
                        st.info(item['analysis_result'])
                        
                    if item['context']:
                        with st.expander("參考的 RAG 知識庫片段"):
                            st.code(item['context'])
                    st.markdown("---")
                    
                    # 報告 chunks
                    log_content_str_for_report = json.dumps(item["log_content"], indent=2, ensure_ascii=False).replace("`", "\\`")
                    full_report_chunks.append(f"---\n\n### Log/Alert #{item['log_id']} (序列分析)\n\n#### 分析的序列內容\n```\n{seq_data['sequence_text']}\n```\n\n#### LLM 分析結果\n{item['analysis_result']}\n")
                    
            except Exception as e:
                error_message = f"ERROR: Log {log_id} 序列處理失敗: {e}"
                st.session_state.batch_results.append({
                    "log_id": log_id,
                    "log_content": seq_data["original_log_entry"],
                    "sequence_analyzed": seq_data["sequence_text"],
                    "analysis_result": error_message,
                    "context": ""
                })
                with results_container:
                    st.error(error_message)
                    
        end_time = time.time()
        progress_bar.empty()
        st.success(f"批量分析完成!共處理 {total_sequences} 個 Log 序列,耗時 {end_time - start_time:.2f} 秒。")
        st.divider()
        
    else:
        st.error("無法從上傳的 JSON 檔案中提取 Log 列表或有效的 Log 條目。請檢查檔案結構。")

# === 顯示結果 (歷史紀錄) (保持不變) ===
if st.session_state.batch_results and not st.session_state.execute_batch_analysis:
    st.header("⚡ 上次分析結果 (歷史紀錄)")
    
    full_report_chunks = ["## Cybersecurity Batch Analysis Report\n\n"]
    for item in st.session_state.batch_results:
        log_content_str_for_report = json.dumps(item["log_content"], indent=2, ensure_ascii=False).replace("`", "\\`")
        full_report_chunks.append(f"---\n\n### Log/Alert #{item['log_id']}\n\n#### 原始內容\n```json\n{log_content_str_for_report}\n```\n\n#### LLM 分析結果\n{item['analysis_result']}\n")
        
    st.info(f"偵測到 {len(st.session_state.batch_results)} 條 Log 的歷史分析結果。")
    st.download_button(
        label="📥 下載上次的完整報告 (.md)",
        data="\n".join(full_report_chunks),
        file_name="security_batch_analysis_report_history.md",
        mime="text/markdown"
    )