# Copyright (c) 2025 Apala Cap. All rights reserved.
# This software is proprietary and confidential.
"""
Main client for interacting with the Phoenix Message Analysis API.
"""
import time
from typing import Dict, List, Optional, Tuple
from urllib.parse import urljoin
import requests
from .metadata import CustomerMetadata
from .models import (
AuthResponse,
BulkFeedbackResponse,
FeedbackResponse,
Message,
MessageFeedback,
MessageHistory,
MessageOptimizationResponse,
MessageProcessingResponse,
RefreshResponse,
)
[docs]
class ApalaClient:
"""
Client for Phoenix Message Analysis Services.
Provides authentication and methods for message processing and feedback.
"""
[docs]
def __init__(self, api_key: str, base_url: str = "http://localhost:4000"):
"""
Initialize the client.
Args:
api_key: Your API key for authentication
base_url: Base URL of the Phoenix server
"""
self.api_key = api_key
self.base_url = base_url.rstrip("/")
self.access_token: Optional[str] = None
self.refresh_token: Optional[str] = None
self.token_expires_at: Optional[float] = None
self._session = requests.Session()
[docs]
def authenticate(self) -> AuthResponse:
"""
Exchange API key for JWT tokens.
Returns:
Authentication response data
Raises:
requests.HTTPError: If authentication fails
requests.RequestException: For network-related errors
"""
url = urljoin(self.base_url, "/api/auth/token")
payload = {"api_key": self.api_key}
response = self._session.post(url, json=payload)
response.raise_for_status()
data = AuthResponse(**response.json())
self.access_token = data.access_token
self.refresh_token = data.refresh_token
# Set expiration time (subtract 60 seconds for safety margin)
self.token_expires_at = time.time() + data.expires_in - 60
return data
[docs]
def refresh_access_token(self) -> RefreshResponse:
"""
Refresh the access token using the refresh token.
Returns:
Refresh response data
Raises:
requests.HTTPError: If token refresh fails
requests.RequestException: For network-related errors
"""
if not self.refresh_token:
raise ValueError("No refresh token available. Please authenticate first.")
url = urljoin(self.base_url, "/api/auth/refresh")
payload = {"refresh_token": self.refresh_token}
response = self._session.post(url, json=payload)
response.raise_for_status()
data = RefreshResponse(**response.json())
self.access_token = data.access_token
# Update expiration time
self.token_expires_at = time.time() + data.expires_in - 60
return data
def _ensure_valid_token(self) -> None:
"""Ensure we have a valid access token, refreshing if necessary."""
current_time = time.time()
if not self.access_token:
self.authenticate()
elif self.token_expires_at and current_time >= self.token_expires_at:
try:
self.refresh_access_token()
except requests.RequestException:
# If refresh fails, try full authentication
self.authenticate()
def _get_auth_headers(self) -> Dict[str, str]:
"""Get headers with authentication token."""
self._ensure_valid_token()
return {"Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json"}
[docs]
def message_process(
self,
message_history: List[Message],
candidate_message: Message,
customer_id: str,
zip_code: str,
company_guid: str,
) -> MessageProcessingResponse:
"""
Process customer message history and candidate message.
Args:
message_history: List of customer messages
candidate_message: The candidate response message
customer_id: Customer UUID
zip_code: Customer's 5-digit zip code
company_guid: Company UUID
Returns:
Processing response data
Raises:
requests.HTTPError: If the request fails
requests.RequestException: For network-related errors
"""
# Create MessageHistory object for validation
history = MessageHistory(
messages=message_history,
candidate_message=candidate_message,
customer_id=customer_id,
zip_code=zip_code,
company_guid=company_guid,
)
url = urljoin(self.base_url, "/api/message_processing")
headers = self._get_auth_headers()
payload = history.to_processing_dict()
response = self._session.post(url, json=payload, headers=headers)
response.raise_for_status()
return MessageProcessingResponse(**response.json())
[docs]
def optimize_message(
self,
message_history: List[Message],
candidate_message: Message,
customer_id: str,
zip_code: str,
company_guid: str,
metadata: Optional[CustomerMetadata] = None,
) -> MessageOptimizationResponse:
"""
Optimize a message for maximum customer engagement.
Args:
message_history: List of customer messages
candidate_message: The candidate message to optimize
customer_id: Customer UUID
zip_code: Customer's 5-digit zip code
company_guid: Company UUID
metadata: Optional customer metadata for enhanced personalization
Returns:
Optimization response with optimized_message and recommended_channel
Raises:
requests.HTTPError: If the request fails
requests.RequestException: For network-related errors
Example:
>>> from apala_client import CustomerMetadata, CreditScoreBin, ApplicationReason
>>> metadata = CustomerMetadata(
... is_repeat_borrower=1,
... credit_score_bin=CreditScoreBin.SCORE_650_700,
... application_reason=ApplicationReason.HOME_IMPROVEMENT
... )
>>> result = client.optimize_message(
... message_history=messages,
... candidate_message=candidate,
... customer_id=customer_id,
... zip_code="90210",
... company_guid=company_guid,
... metadata=metadata
... )
"""
# Create MessageHistory object for validation
history = MessageHistory(
messages=message_history,
candidate_message=candidate_message,
customer_id=customer_id,
zip_code=zip_code,
company_guid=company_guid,
)
url = urljoin(self.base_url, "/api/message_optimizer")
headers = self._get_auth_headers()
payload = history.to_optimization_dict()
# Add metadata if provided
if metadata is not None:
payload.update(metadata.to_dict())
response = self._session.post(url, json=payload, headers=headers)
response.raise_for_status()
return MessageOptimizationResponse(**response.json())
[docs]
def message_feedback(
self, feedback_list: List[Dict[str, any]]
) -> BulkFeedbackResponse:
"""
Submit feedback for multiple processed messages using bulk endpoint.
Args:
feedback_list: List of feedback dictionaries, each containing:
- message_id: str - The message UUID
- customer_responded: bool - Whether customer responded
- score: int - Quality score (0-100)
Returns:
Bulk feedback submission response
Raises:
requests.HTTPError: If the request fails
requests.RequestException: For network-related errors
"""
return self.submit_feedback_bulk(feedback_list)
[docs]
def submit_single_feedback(
self,
message_id: str,
customer_responded: bool,
score: int,
actual_sent_message: Optional[str] = None
) -> FeedbackResponse:
"""
Submit feedback for a single processed message.
Args:
message_id: The message UUID from optimization response
customer_responded: Whether the customer responded
score: Quality score (0-100)
actual_sent_message: Optional - The actual message content sent to the customer.
Useful if you modified the optimized message before sending.
Returns:
Feedback submission response
Raises:
requests.HTTPError: If the request fails
requests.RequestException: For network-related errors
"""
url = urljoin(self.base_url, "/api/feedback")
headers = self._get_auth_headers()
payload = {
"message_id": message_id,
"customer_responded": customer_responded,
"score": score
}
if actual_sent_message is not None:
payload["actual_sent_message"] = actual_sent_message
response = self._session.post(url, json=payload, headers=headers)
response.raise_for_status()
return FeedbackResponse(**response.json())
[docs]
def submit_feedback_bulk(self, feedback_list: List[Dict[str, any]]) -> BulkFeedbackResponse:
"""
Submit feedback for multiple messages in bulk.
Args:
feedback_list: List of feedback dictionaries, each containing:
- message_id: str - The message UUID
- customer_responded: bool - Whether customer responded
- score: int - Quality score (0-100)
- actual_sent_message: str (optional) - The actual message content sent to customer
Returns:
Bulk feedback submission response with success status, count, and individual feedback items
Raises:
requests.HTTPError: If the request fails
requests.RequestException: For network-related errors
Example:
>>> feedback_list = [
... {
... "message_id": "550e8400-e29b-41d4-a716-446655440000",
... "customer_responded": True,
... "score": 85,
... "actual_sent_message": "Hi! Ready to help with your loan."
... },
... {
... "message_id": "660e8400-e29b-41d4-a716-446655440001",
... "customer_responded": False,
... "score": 60
... }
... ]
>>> response = client.submit_feedback_bulk(feedback_list)
>>> print(f"Submitted {response['count']} feedback items")
"""
url = urljoin(self.base_url, "/api/feedback/bulk")
headers = self._get_auth_headers()
payload = {"feedback": feedback_list}
response = self._session.post(url, json=payload, headers=headers)
response.raise_for_status()
return BulkFeedbackResponse(**response.json())
[docs]
def close(self) -> None:
"""Close the HTTP session."""
self._session.close()