diff --git a/src/api/agent_search.rs b/src/api/agent_search.rs index 4fcfb70..e588ba0 100644 --- a/src/api/agent_search.rs +++ b/src/api/agent_search.rs @@ -86,18 +86,22 @@ const SYSTEM_PROMPT: &str = r#"你是 Momentry 影片分析助手。回答用戶 - 只有當多個搜尋工具都返回空、且問題明顯指向特定影片時,才問用戶 - 搜尋工具(find_file, list_files, smart_search, universal_search)的 file_uuid 參數可省略以搜尋全部 -## 工具使用規則 -1. 人物問題優先使用 tkg_query -2. 人物台詞/發言問題使用 identities_search(輸入人名→回傳台詞片段) -3. 人物對話互動(誰跟誰說話)使用 tkg_query 的 speaker_interaction -4. 人物台詞內容使用 tkg_query 的 speaker_dialogue -5. 用文字反查人物使用 identity_text(輸入關鍵字→找出誰說/提到這段話) -6. 語意/內容問題使用 smart_search 或 universal_search -7. 畫面分析使用 analyze_frame — 可以分析影片中的任何畫面內容(場景、人物表情、動作、物件等) -8. **可以同時呼叫多個工具,但需符合以下條件:** - - ✅ 查詢多部影片的相同資訊(如:3部影片的人物列表) - - ✅ 需要組合多個來源的資訊才能回答(如:file_info + tkg_query) - - ❌ 不要為了「嘗試所有可能」而盲目並行呼叫 +## 工具使用規則(重要:優先使用 smart_search) +1. **內容/關鍵字問題優先使用 smart_search** — 可以找到影片內的具體文字內容(OCR、ASR) +2. **如果 smart_search 找到結果,附帶顯示 chunk 文字內容**(包含 [OCR]、[ASRX] 標記) +3. find_file 僅用於搜尋片名,不適合搜尋內容關鍵字 +4. 人物問題使用 tkg_query +5. 人物台詞/發言問題使用 identities_search(輸入人名→回傳台詞片段) +6. 人物對話互動(誰跟誰說話)使用 tkg_query 的 speaker_interaction +7. 人物台詞內容使用 tkg_query 的 speaker_dialogue +8. 用文字反查人物使用 identity_text(輸入關鍵字→找出誰說/提到這段話) +9. 畫面分析使用 analyze_frame — 可以分析影片中的任何畫面內容(場景、人物表情、動作、物件等) +10. **可以同時呼叫多個工具,但需符合以下條件:** + - ✅ 查詢多部影片的相同資訊(如:3部影片的人物列表) + - ✅ 需要組合多個來源的資訊才能回答(如:file_info + tkg_query) + - ❌ 不要為了「嘗試所有可能」而盲目並行呼叫 + - ❌ 如果單一工具已返回足夠答案,不需要額外呼叫 + - ❌ 如果 smart_search 已找到相關 chunk,不需要再呼叫 find_file - ❌ 如果單一工具已返回足夠答案,不需要額外呼叫 ## 回答規則 diff --git a/src/core/agent/tools.rs b/src/core/agent/tools.rs index 55a9e7c..d8b7fc8 100644 --- a/src/core/agent/tools.rs +++ b/src/core/agent/tools.rs @@ -555,7 +555,7 @@ pub async fn exec_smart_search( let chunk_table = schema::table_name("chunk"); let mut sql = format!( - "SELECT chunk_id, text_content, start_frame, end_frame, chunk_type \ + "SELECT chunk_id, text_content, start_frame, end_frame, chunk_type, content \ FROM {} WHERE text_content ILIKE $1", chunk_table ); @@ -566,21 +566,87 @@ pub async fn exec_smart_search( if let Some(fuid) = file_uuid { let like = format!("%{}%", query); - let rows: Vec<(String, Option, i64, i64, String)> = sqlx::query_as(&sql) + let rows: Vec<(String, Option, i64, i64, String, Option)> = sqlx::query_as(&sql) .bind(&like) .bind(fuid) .fetch_all(pool) .await .map_err(|e| e.to_string())?; - Ok(serde_json::json!({"results": rows}).to_string()) + + let results: Vec> = rows.into_iter().map(|(chunk_id, text_content, start_frame, end_frame, chunk_type, content)| { + let source_prefix = if let Some(ref content) = content { + let text = content.get("text").and_then(|t| t.as_str()).unwrap_or(""); + let ocr_text = content.get("ocr_text").and_then(|t| t.as_str()).unwrap_or(""); + + let has_asrx = !text.trim().is_empty(); + let has_ocr = !ocr_text.trim().is_empty(); + + if has_asrx && has_ocr { + "[ASRX+OCR] " + } else if has_asrx { + "[ASRX] " + } else if has_ocr { + "[OCR] " + } else { + "" + } + } else { + "" + }; + + let prefixed_text = text_content.map(|t| format!("{}{}", source_prefix, t)); + + vec![ + serde_json::json!(chunk_id), + serde_json::json!(prefixed_text), + serde_json::json!(start_frame), + serde_json::json!(end_frame), + serde_json::json!(chunk_type), + ] + }).collect(); + + Ok(serde_json::json!({"results": results}).to_string()) } else { let like = format!("%{}%", query); - let rows: Vec<(String, Option, i64, i64, String)> = sqlx::query_as(&sql) + let rows: Vec<(String, Option, i64, i64, String, Option)> = sqlx::query_as(&sql) .bind(&like) .fetch_all(pool) .await .map_err(|e| e.to_string())?; - Ok(serde_json::json!({"results": rows}).to_string()) + + let results: Vec> = rows.into_iter().map(|(chunk_id, text_content, start_frame, end_frame, chunk_type, content)| { + let source_prefix = if let Some(ref content) = content { + let text = content.get("text").and_then(|t| t.as_str()).unwrap_or(""); + let ocr_text = content.get("ocr_text").and_then(|t| t.as_str()).unwrap_or(""); + + let has_asrx = !text.trim().is_empty(); + let has_ocr = !ocr_text.trim().is_empty(); + + if has_asrx && has_ocr { + "[ASRX+OCR] " + } else if has_asrx { + "[ASRX] " + } else if has_ocr { + "[OCR] " + } else { + "" + } + } else { + "" + }; + + let prefixed_text = text_content.map(|t| format!("{}{}", source_prefix, t)); + + vec![ + serde_json::json!(chunk_id), + serde_json::json!(prefixed_text), + serde_json::json!(start_frame), + serde_json::json!(end_frame), + serde_json::json!(chunk_type), + ] + }).collect(); + + Ok(serde_json::json!({"results": results}).to_string()) } }