NexusCoder / nexus /tools /graphql_client.py
AdminReal's picture
Import NexusCoder from github.com/mhieuhonda/NexusCoder
eca5751 verified
Raw
History Blame Contribute Delete
6.35 kB
"""
GraphQL Client Tool - Gửi GraphQL query/mutation tới một endpoint.
Author: Hieu Louis (2026)
Dùng stdlib urllib (fallback) hoặc requests nếu có. Hỗ trợ variables + headers.
"""
from __future__ import annotations
import json
from typing import Any, Dict, Optional
from .base import Tool, ToolResult, ToolContext, ToolCategory, ToolSafety
class GraphQLClientTool(Tool):
"""Gửi GraphQL query/mutation tới một endpoint HTTP."""
category = ToolCategory.WEB # theo spec: category=WEB
safety = ToolSafety.MODERATE # network call nhưng query GraphQL
requires_confirmation = False
@property
def name(self) -> str:
return "graphql_client"
@property
def description(self) -> str:
return (
"Gửi GraphQL query hoặc mutation tới một endpoint. Hỗ trợ variables, "
"headers (auth), timeout. Trả về JSON response."
)
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"endpoint": {"type": "string", "description": "GraphQL endpoint URL (https://...)"},
"query": {"type": "string", "description": "GraphQL query/mutation string"},
"variables": {"type": "object", "description": "Biến cho GraphQL operation"},
"operation_name": {"type": "string", "description": "Tên operation (nếu nhiều op trong query)"},
"headers": {"type": "object", "description": "HTTP headers (Authorization, Content-Type, ...)"},
"method": {"type": "string", "enum": ["POST", "GET"], "description": "HTTP method (default POST)"},
"timeout": {"type": "integer", "description": "Request timeout (s)"},
},
"required": ["endpoint", "query"],
}
def validate_args(self, args: Dict[str, Any]) -> Optional[str]:
if not args.get("endpoint"):
return "Missing required arg: endpoint"
if not args.get("query"):
return "Missing required arg: query"
if not str(args["endpoint"]).startswith(("http://", "https://")):
return "endpoint phải là URL http(s)://"
return None
def execute(self, args: Dict[str, Any], context: ToolContext) -> ToolResult:
endpoint: str = args["endpoint"]
query: str = args["query"]
variables = args.get("variables") or {}
operation_name = args.get("operation_name")
headers: Dict[str, str] = args.get("headers") or {}
method = str(args.get("method") or "POST").upper()
timeout = int(args.get("timeout") or context.timeout or 30)
# Dry-run // dry-run
if context.dry_run:
return ToolResult(
success=True,
output=f"[dry-run] Would send {method} GraphQL to {endpoint}",
metadata={"dry_run": True, "endpoint": endpoint, "method": method, "operation_name": operation_name},
)
payload = {
"query": query,
"variables": variables,
}
if operation_name:
payload["operationName"] = operation_name
# Ưu tiên requests (nếu có), fallback urllib // prefer requests, fallback urllib
try:
import requests # type: ignore
use_requests = True
except ImportError:
use_requests = False
try:
if use_requests:
# POST application/json (chuẩn GraphQL) // standard JSON POST
if method == "POST":
resp = requests.post( # type: ignore[union-attr]
endpoint,
json=payload,
headers=headers,
timeout=timeout,
)
else:
# GET với query string // GET with querystring
import urllib.parse as up
qs = up.urlencode({"query": query, "variables": json.dumps(variables)})
resp = requests.get( # type: ignore[union-attr]
f"{endpoint}?{qs}",
headers=headers,
timeout=timeout,
)
status = resp.status_code
try:
body = resp.json()
except Exception:
body = {"raw": resp.text}
else:
# Fallback urllib // urllib fallback
import urllib.request as ur
import urllib.parse as up
if method == "POST":
data = json.dumps(payload).encode("utf-8")
req_headers = dict(headers)
req_headers.setdefault("Content-Type", "application/json")
req = ur.Request(endpoint, data=data, headers=req_headers, method="POST")
else:
qs = up.urlencode({"query": query, "variables": json.dumps(variables)})
req = ur.Request(f"{endpoint}?{qs}", headers=headers, method="GET")
with ur.urlopen(req, timeout=timeout) as r: # noqa: S310
status = r.status
raw = r.read().decode("utf-8", errors="replace")
try:
body = json.loads(raw)
except Exception:
body = {"raw": raw}
# GraphQL trả về 200 ngay cả khi có errors // GraphQL may have errors
has_errors = isinstance(body, dict) and bool(body.get("errors"))
return ToolResult(
success=(200 <= status < 300) and not has_errors,
output=json.dumps(body, ensure_ascii=False, indent=2),
error=(json.dumps(body.get("errors"), ensure_ascii=False, indent=2) if has_errors else None),
return_code=status,
metadata={
"endpoint": endpoint,
"method": method,
"status_code": status,
"has_errors": has_errors,
"operation_name": operation_name,
},
)
except Exception as e:
return ToolResult(success=False, error=str(e), return_code=1)