Skip to content

Commit f8feee0

Browse files
committed
feat: add Tencent Cloud Search(Web Search API) tool
1 parent 8ecf72c commit f8feee0

2 files changed

Lines changed: 184 additions & 0 deletions

File tree

backend/app/services/agent_tools.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2440,6 +2440,12 @@ async def _web_search(arguments: dict, agent_id: uuid.UUID | None = None) -> str
24402440
return await _search_google(query, api_key, max_results, language)
24412441
elif engine == "bing" and api_key:
24422442
return await _search_bing(query, api_key, max_results, language)
2443+
elif engine == "tencentcloud":
2444+
secret_id = config.get("tencent_secret_id", "")
2445+
secret_key = config.get("tencent_secret_key", "")
2446+
if secret_id and secret_key:
2447+
return await _search_tencentcloud(query, secret_id, secret_key, max_results)
2448+
return "❌ Tencent Cloud WSA requires SecretId and SecretKey configuration"
24432449
elif engine == "exa" and api_key:
24442450
return await _search_exa(query, api_key, max_results)
24452451
else:
@@ -2664,6 +2670,149 @@ async def _search_bing(query: str, api_key: str, max_results: int, language: str
26642670
return f'🔍 Bing search for "{query}" ({len(results)} items):\n\n' + "\n\n---\n\n".join(results)
26652671

26662672

2673+
async def _search_tencentcloud(query: str, secret_id: str, secret_key: str, max_results: int) -> str:
2674+
"""Search via Tencent Cloud WSA (Web Search API) - SearchPro."""
2675+
import httpx
2676+
import hashlib
2677+
import hmac
2678+
import time
2679+
import json
2680+
from datetime import datetime
2681+
2682+
# WSA API configuration
2683+
service = "wsa"
2684+
host = "wsa.tencentcloudapi.com"
2685+
action = "SearchPro"
2686+
version = "2025-05-08"
2687+
algorithm = "TC3-HMAC-SHA256"
2688+
2689+
timestamp = int(time.time())
2690+
date = datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d")
2691+
2692+
# Request body - Cnt supports 10/20/30/40/50
2693+
cnt = min(max(max_results, 10), 50)
2694+
# Round up to nearest valid value
2695+
valid_cnts = [10, 20, 30, 40, 50]
2696+
cnt = min([c for c in valid_cnts if c >= cnt], default=50)
2697+
2698+
payload = {
2699+
"Query": query,
2700+
"Mode": 0, # 0 = natural search results
2701+
"Cnt": cnt,
2702+
}
2703+
payload_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
2704+
2705+
# Build canonical request
2706+
ct = "application/json"
2707+
canonical_headers = f"content-type:{ct}\nhost:{host}\n"
2708+
signed_headers = "content-type;host"
2709+
hashed_payload = hashlib.sha256(payload_str.encode("utf-8")).hexdigest()
2710+
2711+
canonical_request = "\n".join([
2712+
"POST",
2713+
"/",
2714+
"",
2715+
canonical_headers,
2716+
signed_headers,
2717+
hashed_payload
2718+
])
2719+
2720+
# Build string to sign
2721+
credential_scope = f"{date}/{service}/tc3_request"
2722+
hashed_request = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()
2723+
string_to_sign = "\n".join([
2724+
algorithm,
2725+
str(timestamp),
2726+
credential_scope,
2727+
hashed_request
2728+
])
2729+
2730+
# Calculate signature
2731+
secret_date = hmac.new(
2732+
f"TC3{secret_key}".encode("utf-8"),
2733+
date.encode("utf-8"),
2734+
hashlib.sha256
2735+
).digest()
2736+
2737+
secret_service = hmac.new(
2738+
secret_date,
2739+
service.encode("utf-8"),
2740+
hashlib.sha256
2741+
).digest()
2742+
2743+
secret_signing = hmac.new(
2744+
secret_service,
2745+
b"tc3_request",
2746+
hashlib.sha256
2747+
).digest()
2748+
2749+
signature = hmac.new(
2750+
secret_signing,
2751+
string_to_sign.encode("utf-8"),
2752+
hashlib.sha256
2753+
).hexdigest()
2754+
2755+
# Build authorization header
2756+
authorization = (
2757+
f"{algorithm} Credential={secret_id}/{credential_scope}, "
2758+
f"SignedHeaders={signed_headers}, Signature={signature}"
2759+
)
2760+
2761+
headers = {
2762+
"Authorization": authorization,
2763+
"Content-Type": ct,
2764+
"Host": host,
2765+
"X-TC-Action": action,
2766+
"X-TC-Timestamp": str(timestamp),
2767+
"X-TC-Version": version,
2768+
}
2769+
2770+
try:
2771+
async with httpx.AsyncClient() as client:
2772+
resp = await client.post(
2773+
f"https://{host}",
2774+
content=payload_str.encode("utf-8"),
2775+
headers=headers,
2776+
timeout=30,
2777+
)
2778+
data = resp.json()
2779+
2780+
# Parse response
2781+
response = data.get("Response", {})
2782+
if "Error" in response:
2783+
error = response["Error"]
2784+
return f"❌ Tencent Cloud WSA error: {error.get('Code')} - {error.get('Message')}"
2785+
2786+
results = []
2787+
pages = response.get("Pages", [])
2788+
2789+
for i, page_str in enumerate(pages[:max_results], 1):
2790+
try:
2791+
page = json.loads(page_str)
2792+
title = page.get("title", "Untitled")
2793+
url = page.get("url", "")
2794+
site = page.get("site", "")
2795+
passage = page.get("passage", "")
2796+
2797+
result_text = f"**{i}. {title}**"
2798+
if site:
2799+
result_text += f" ({site})"
2800+
result_text += f"\n{url}"
2801+
if passage:
2802+
result_text += f"\n{passage}"
2803+
results.append(result_text)
2804+
except json.JSONDecodeError:
2805+
continue
2806+
2807+
if not results:
2808+
return f'🔍 Tencent Cloud WSA found no results for "{query}"'
2809+
2810+
return f'🔍 Tencent Cloud WSA results for "{query}" ({len(results)} items):\n\n' + "\n\n---\n\n".join(results)
2811+
2812+
except Exception as e:
2813+
return f"❌ Tencent Cloud WSA error: {str(e)[:300]}"
2814+
2815+
26672816
async def _search_exa(query: str, api_key: str, max_results: int) -> str:
26682817
"""Search via Exa AI API (exa.ai). Used by the web_search engine selector."""
26692818
import httpx

backend/app/services/tool_seeder.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,41 @@
609609
]
610610
},
611611
},
612+
{
613+
"name": "tencentcloud_search",
614+
"display_name": "Tencent Cloud Search",
615+
"description": "Search using Tencent Cloud Web Search API (WSA). Returns titles, URLs, and snippets. Requires Tencent Cloud API SecretId and SecretKey.",
616+
"category": "search",
617+
"icon": "🔍",
618+
"is_default": False,
619+
"parameters_schema": {
620+
"type": "object",
621+
"properties": {
622+
"query": {"type": "string", "description": "Search keywords"},
623+
"max_results": {"type": "integer", "description": "Number of results to return (default 5, max 10)"},
624+
},
625+
"required": ["query"],
626+
},
627+
"config": {},
628+
"config_schema": {
629+
"fields": [
630+
{
631+
"key": "secret_id",
632+
"label": "Tencent Cloud SecretId",
633+
"type": "password",
634+
"default": "",
635+
"placeholder": "Get from Tencent Cloud Console (API Key Management)",
636+
},
637+
{
638+
"key": "secret_key",
639+
"label": "Tencent Cloud SecretKey",
640+
"type": "password",
641+
"default": "",
642+
"placeholder": "Get from Tencent Cloud Console (API Key Management)",
643+
},
644+
]
645+
},
646+
},
612647
{
613648
"name": "plaza_get_new_posts",
614649
"display_name": "Plaza: Browse",

0 commit comments

Comments
 (0)