Files
assistant/agent/integrations/hyperf_client.py

539 lines
14 KiB
Python
Raw Normal View History

"""
Hyperf PHP API Client for B2B Shopping AI Assistant
"""
from typing import Any, Optional
from dataclasses import dataclass
from enum import Enum
import httpx
from config import settings
from utils.logger import get_logger
logger = get_logger(__name__)
class APIError(Exception):
"""API error with code and message"""
def __init__(self, code: int, message: str, data: Optional[Any] = None):
self.code = code
self.message = message
self.data = data
super().__init__(f"[{code}] {message}")
@dataclass
class APIResponse:
"""Standardized API response"""
code: int
message: str
data: Any
meta: Optional[dict[str, Any]] = None
@property
def success(self) -> bool:
return self.code == 0
class HyperfClient:
"""Hyperf PHP API Client"""
def __init__(
self,
api_url: Optional[str] = None,
api_token: Optional[str] = None
):
"""Initialize Hyperf client
Args:
api_url: Hyperf API base URL, defaults to settings
api_token: API access token, defaults to settings
"""
self.api_url = (api_url or settings.hyperf_api_url).rstrip("/")
self.api_token = api_token or settings.hyperf_api_token
self._client: Optional[httpx.AsyncClient] = None
logger.info("Hyperf client initialized", api_url=self.api_url)
async def _get_client(self) -> httpx.AsyncClient:
"""Get or create HTTP client"""
if self._client is None:
self._client = httpx.AsyncClient(
base_url=f"{self.api_url}/api/v1",
headers={
"Authorization": f"Bearer {self.api_token}",
"Content-Type": "application/json",
"Accept": "application/json"
},
timeout=30.0
)
return self._client
async def close(self) -> None:
"""Close HTTP client"""
if self._client:
await self._client.aclose()
self._client = None
async def _request(
self,
method: str,
endpoint: str,
params: Optional[dict[str, Any]] = None,
json: Optional[dict[str, Any]] = None,
headers: Optional[dict[str, str]] = None
) -> APIResponse:
"""Make API request
Args:
method: HTTP method
endpoint: API endpoint
params: Query parameters
json: JSON body
headers: Additional headers
Returns:
Parsed API response
Raises:
APIError: If API returns error
"""
client = await self._get_client()
# Merge headers
request_headers = {}
if headers:
request_headers.update(headers)
logger.debug(
"API request",
method=method,
endpoint=endpoint
)
try:
response = await client.request(
method=method,
url=endpoint,
params=params,
json=json,
headers=request_headers
)
response.raise_for_status()
data = response.json()
result = APIResponse(
code=data.get("code", 0),
message=data.get("message", "success"),
data=data.get("data"),
meta=data.get("meta")
)
if not result.success:
raise APIError(result.code, result.message, result.data)
logger.debug(
"API response",
endpoint=endpoint,
code=result.code
)
return result
except httpx.HTTPStatusError as e:
logger.error(
"HTTP error",
endpoint=endpoint,
status_code=e.response.status_code
)
raise APIError(
e.response.status_code,
f"HTTP error: {e.response.status_code}"
)
except Exception as e:
logger.error("API request failed", endpoint=endpoint, error=str(e))
raise
async def get(
self,
endpoint: str,
params: Optional[dict[str, Any]] = None,
**kwargs: Any
) -> APIResponse:
"""GET request"""
return await self._request("GET", endpoint, params=params, **kwargs)
async def post(
self,
endpoint: str,
json: Optional[dict[str, Any]] = None,
**kwargs: Any
) -> APIResponse:
"""POST request"""
return await self._request("POST", endpoint, json=json, **kwargs)
async def put(
self,
endpoint: str,
json: Optional[dict[str, Any]] = None,
**kwargs: Any
) -> APIResponse:
"""PUT request"""
return await self._request("PUT", endpoint, json=json, **kwargs)
async def delete(
self,
endpoint: str,
**kwargs: Any
) -> APIResponse:
"""DELETE request"""
return await self._request("DELETE", endpoint, **kwargs)
# ============ Order APIs ============
async def query_orders(
self,
user_id: str,
account_id: str,
order_id: Optional[str] = None,
status: Optional[str] = None,
date_start: Optional[str] = None,
date_end: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> APIResponse:
"""Query orders
Args:
user_id: User ID
account_id: Account ID
order_id: Optional specific order ID
status: Optional order status filter
date_start: Optional start date (YYYY-MM-DD)
date_end: Optional end date (YYYY-MM-DD)
page: Page number
page_size: Items per page
Returns:
Orders list response
"""
payload = {
"user_id": user_id,
"account_id": account_id,
"page": page,
"page_size": page_size
}
if order_id:
payload["order_id"] = order_id
if status:
payload["status"] = status
if date_start:
payload["date_range"] = {"start": date_start}
if date_end:
payload.setdefault("date_range", {})["end"] = date_end
return await self.post("/orders/query", json=payload)
async def get_logistics(
self,
order_id: str,
tracking_number: Optional[str] = None
) -> APIResponse:
"""Get order logistics information
Args:
order_id: Order ID
tracking_number: Optional tracking number
Returns:
Logistics tracking response
"""
params = {}
if tracking_number:
params["tracking_number"] = tracking_number
return await self.get(f"/orders/{order_id}/logistics", params=params)
async def modify_order(
self,
order_id: str,
user_id: str,
modifications: dict[str, Any]
) -> APIResponse:
"""Modify order
Args:
order_id: Order ID
user_id: User ID for permission check
modifications: Changes to apply
Returns:
Modified order response
"""
return await self.put(
f"/orders/{order_id}/modify",
json={
"user_id": user_id,
"modifications": modifications
}
)
async def cancel_order(
self,
order_id: str,
user_id: str,
reason: str
) -> APIResponse:
"""Cancel order
Args:
order_id: Order ID
user_id: User ID for permission check
reason: Cancellation reason
Returns:
Cancellation result with refund info
"""
return await self.post(
f"/orders/{order_id}/cancel",
json={
"user_id": user_id,
"reason": reason
}
)
# ============ Product APIs ============
async def search_products(
self,
query: str,
filters: Optional[dict[str, Any]] = None,
sort: str = "relevance",
page: int = 1,
page_size: int = 20
) -> APIResponse:
"""Search products
Args:
query: Search query
filters: Optional filters (category, price_range, brand, etc.)
sort: Sort order
page: Page number
page_size: Items per page
Returns:
Products list response
"""
payload = {
"query": query,
"sort": sort,
"page": page,
"page_size": page_size
}
if filters:
payload["filters"] = filters
return await self.post("/products/search", json=payload)
async def get_product(self, product_id: str) -> APIResponse:
"""Get product details
Args:
product_id: Product ID
Returns:
Product details response
"""
return await self.get(f"/products/{product_id}")
async def get_recommendations(
self,
user_id: str,
account_id: str,
context: Optional[dict[str, Any]] = None,
limit: int = 10
) -> APIResponse:
"""Get product recommendations
Args:
user_id: User ID
account_id: Account ID
context: Optional context (recent views, current query)
limit: Number of recommendations
Returns:
Recommendations response
"""
payload = {
"user_id": user_id,
"account_id": account_id,
"limit": limit
}
if context:
payload["context"] = context
return await self.post("/products/recommend", json=payload)
async def get_quote(
self,
product_id: str,
quantity: int,
account_id: str,
delivery_address: Optional[dict[str, str]] = None
) -> APIResponse:
"""Get B2B price quote
Args:
product_id: Product ID
quantity: Quantity
account_id: Account ID for pricing tier
delivery_address: Optional delivery address
Returns:
Quote response with pricing details
"""
payload = {
"product_id": product_id,
"quantity": quantity,
"account_id": account_id
}
if delivery_address:
payload["delivery_address"] = delivery_address
return await self.post("/products/quote", json=payload)
# ============ Aftersale APIs ============
async def apply_return(
self,
order_id: str,
user_id: str,
items: list[dict[str, Any]],
description: str,
images: Optional[list[str]] = None
) -> APIResponse:
"""Apply for return
Args:
order_id: Order ID
user_id: User ID
items: Items to return with quantity and reason
description: Description of issue
images: Optional image URLs
Returns:
Return application response
"""
payload = {
"order_id": order_id,
"user_id": user_id,
"items": items,
"description": description
}
if images:
payload["images"] = images
return await self.post("/aftersales/return", json=payload)
async def apply_exchange(
self,
order_id: str,
user_id: str,
items: list[dict[str, Any]],
description: str
) -> APIResponse:
"""Apply for exchange
Args:
order_id: Order ID
user_id: User ID
items: Items to exchange with reason
description: Description of issue
Returns:
Exchange application response
"""
return await self.post(
"/aftersales/exchange",
json={
"order_id": order_id,
"user_id": user_id,
"items": items,
"description": description
}
)
async def create_complaint(
self,
user_id: str,
complaint_type: str,
title: str,
description: str,
related_order_id: Optional[str] = None,
attachments: Optional[list[str]] = None
) -> APIResponse:
"""Create complaint
Args:
user_id: User ID
complaint_type: Type of complaint
title: Complaint title
description: Detailed description
related_order_id: Optional related order
attachments: Optional attachment URLs
Returns:
Complaint creation response
"""
payload = {
"user_id": user_id,
"type": complaint_type,
"title": title,
"description": description
}
if related_order_id:
payload["related_order_id"] = related_order_id
if attachments:
payload["attachments"] = attachments
return await self.post("/aftersales/complaint", json=payload)
async def query_aftersales(
self,
user_id: str,
aftersale_id: Optional[str] = None
) -> APIResponse:
"""Query aftersale records
Args:
user_id: User ID
aftersale_id: Optional specific aftersale ID
Returns:
Aftersale records response
"""
params = {"user_id": user_id}
if aftersale_id:
params["aftersale_id"] = aftersale_id
return await self.get("/aftersales/query", params=params)
# Global Hyperf client instance
hyperf_client: Optional[HyperfClient] = None
def get_hyperf_client() -> HyperfClient:
"""Get or create global Hyperf client instance"""
global hyperf_client
if hyperf_client is None:
hyperf_client = HyperfClient()
return hyperf_client