diff --git a/backend/app/services/agent_tools.py b/backend/app/services/agent_tools.py index c026f9b8..4ab30681 100644 --- a/backend/app/services/agent_tools.py +++ b/backend/app/services/agent_tools.py @@ -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": @@ -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": @@ -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 @@ -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. diff --git a/backend/app/services/tool_seeder.py b/backend/app/services/tool_seeder.py index 7584ab0f..e026b54a 100644 --- a/backend/app/services/tool_seeder.py +++ b/backend/app/services/tool_seeder.py @@ -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",