Skip to content
164 changes: 164 additions & 0 deletions backend/app/services/agent_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2823,6 +2823,8 @@ async def _execute_tool_direct(
return await _google_search_tool(arguments, agent_id)
elif tool_name == "bing_search":
return await _bing_search_tool(arguments, agent_id)
elif tool_name == "tencentcloud_search":
return await _tencentcloud_search_tool(arguments, agent_id)
elif tool_name == "send_feishu_message":
return await _send_feishu_message(agent_id, arguments)
elif tool_name == "send_message_to_agent":
Expand Down Expand Up @@ -3094,6 +3096,8 @@ async def execute_tool(
result = await _google_search_tool(arguments, agent_id)
elif tool_name == "bing_search":
result = await _bing_search_tool(arguments, agent_id)
elif tool_name == "tencentcloud_search":
result = await _tencentcloud_search_tool(arguments, agent_id)
elif tool_name == "jina_read":
result = await _jina_read(arguments)
elif tool_name == "read_webpage":
Expand Down Expand Up @@ -3811,6 +3815,149 @@ async def _search_bing(query: str, api_key: str, max_results: int, language: str
return f'🔍 Bing search for "{query}" ({len(results)} items):\n\n' + "\n\n---\n\n".join(results)


async def _search_tencentcloud(query: str, secret_id: str, secret_key: str, max_results: int) -> str:
"""Search via Tencent Cloud WSA (Web Search API) - SearchPro."""
import httpx
import hashlib
import hmac
import time
import json
from datetime import datetime

# WSA API configuration
service = "wsa"
host = "wsa.tencentcloudapi.com"
action = "SearchPro"
version = "2025-05-08"
algorithm = "TC3-HMAC-SHA256"

timestamp = int(time.time())
date = datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d")

# Request body - Cnt supports 10/20/30/40/50
cnt = min(max(max_results, 10), 50)
# Round up to nearest valid value
valid_cnts = [10, 20, 30, 40, 50]
cnt = min([c for c in valid_cnts if c >= cnt], default=50)

payload = {
"Query": query,
"Mode": 0, # 0 = natural search results
"Cnt": cnt,
}
payload_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)

# Build canonical request
ct = "application/json"
canonical_headers = f"content-type:{ct}\nhost:{host}\n"
signed_headers = "content-type;host"
hashed_payload = hashlib.sha256(payload_str.encode("utf-8")).hexdigest()

canonical_request = "\n".join([
"POST",
"/",
"",
canonical_headers,
signed_headers,
hashed_payload
])

# Build string to sign
credential_scope = f"{date}/{service}/tc3_request"
hashed_request = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()
string_to_sign = "\n".join([
algorithm,
str(timestamp),
credential_scope,
hashed_request
])

# Calculate signature
secret_date = hmac.new(
f"TC3{secret_key}".encode("utf-8"),
date.encode("utf-8"),
hashlib.sha256
).digest()

secret_service = hmac.new(
secret_date,
service.encode("utf-8"),
hashlib.sha256
).digest()

secret_signing = hmac.new(
secret_service,
b"tc3_request",
hashlib.sha256
).digest()

signature = hmac.new(
secret_signing,
string_to_sign.encode("utf-8"),
hashlib.sha256
).hexdigest()

# Build authorization header
authorization = (
f"{algorithm} Credential={secret_id}/{credential_scope}, "
f"SignedHeaders={signed_headers}, Signature={signature}"
)

headers = {
"Authorization": authorization,
"Content-Type": ct,
"Host": host,
"X-TC-Action": action,
"X-TC-Timestamp": str(timestamp),
"X-TC-Version": version,
}

try:
async with httpx.AsyncClient() as client:
resp = await client.post(
f"https://{host}",
content=payload_str.encode("utf-8"),
headers=headers,
timeout=30,
)
data = resp.json()

# Parse response
response = data.get("Response", {})
if "Error" in response:
error = response["Error"]
return f"❌ Tencent Cloud WSA error: {error.get('Code')} - {error.get('Message')}"

results = []
pages = response.get("Pages", [])

for i, page_str in enumerate(pages[:max_results], 1):
try:
page = json.loads(page_str)
title = page.get("title", "Untitled")
url = page.get("url", "")
site = page.get("site", "")
passage = page.get("passage", "")

result_text = f"**{i}. {title}**"
if site:
result_text += f" ({site})"
result_text += f"\n{url}"
if passage:
result_text += f"\n{passage}"
results.append(result_text)
except json.JSONDecodeError:
continue

if not results:
return f'🔍 Tencent Cloud WSA found no results for "{query}"'

return f'🔍 Tencent Cloud WSA results for "{query}" ({len(results)} items):\n\n' + "\n\n---\n\n".join(results)

except Exception as e:
return f"❌ Tencent Cloud WSA error: {str(e)[:300]}"


async def _search_exa(query: str, api_key: str, max_results: int) -> str:
"""Search via Exa AI API (exa.ai). Used by the web_search engine selector."""
import httpx
Expand Down Expand Up @@ -3995,6 +4142,23 @@ async def _bing_search_tool(arguments: dict, agent_id: uuid.UUID | None = None)
return f"Bing search error: {str(e)[:200]}"


async def _tencentcloud_search_tool(arguments: dict, agent_id: uuid.UUID | None = None) -> str:
"""Standalone Tencent Cloud WSA Search tool (API keys read from per-tool config)."""
query = arguments.get("query", "").strip()
if not query:
return "Please provide search keywords"
config = await _get_tool_config(agent_id, "tencentcloud_search") or {}
secret_id = config.get("secret_id", "").strip()
secret_key = config.get("secret_key", "").strip()
if not secret_id or not secret_key:
return "Tencent Cloud API keys (secret_id, secret_key) are required. Set them in the tool settings."
max_results = min(arguments.get("max_results", 5), 10)
try:
return await _search_tencentcloud(query, secret_id, secret_key, max_results)
except Exception as e:
return f"Tencent Cloud search error: {str(e)[:200]}"


async def _send_channel_file(agent_id: uuid.UUID, ws: Path, arguments: dict) -> str:
"""Send a file to a person or back to the current channel.

Expand Down
35 changes: 35 additions & 0 deletions backend/app/services/tool_seeder.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,41 @@ def _global_builtin_config(tool_data: dict) -> dict:
]
},
},
{
"name": "tencentcloud_search",
"display_name": "Tencent Cloud Search",
"description": "Search using Tencent Cloud Web Search API (WSA). Returns titles, URLs, and snippets. Requires Tencent Cloud API SecretId and SecretKey.",
"category": "search",
"icon": "🔍",
"is_default": False,
"parameters_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search keywords"},
"max_results": {"type": "integer", "description": "Number of results to return (default 5, max 10)"},
},
"required": ["query"],
},
"config": {},
"config_schema": {
"fields": [
{
"key": "secret_id",
"label": "Tencent Cloud SecretId",
"type": "password",
"default": "",
"placeholder": "Get from Tencent Cloud Console (API Key Management)",
},
{
"key": "secret_key",
"label": "Tencent Cloud SecretKey",
"type": "password",
"default": "",
"placeholder": "Get from Tencent Cloud Console (API Key Management)",
},
]
},
},
{
"name": "plaza_get_new_posts",
"display_name": "Plaza: Browse",
Expand Down