Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions backend/app/services/agent_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2097,6 +2097,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 @@ -2265,6 +2267,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 @@ -2664,6 +2668,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 @@ -2848,6 +2995,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 @@ -609,6 +609,41 @@
]
},
},
{
"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