CKT
commited on
Commit
·
c5f454e
1
Parent(s):
62a350d
AgentPay integrated
Browse files- .gitignore +1 -0
- agentpay_sdk/__init__.py +5 -0
- agentpay_sdk/client.py +225 -0
- agentpay_sdk/exceptions.py +20 -0
- agentpay_sdk/types.py +9 -0
- app.py +118 -48
- data/messages.json +24 -0
- data/profiles.json +32 -0
- requirements.txt +2 -0
.gitignore
CHANGED
@@ -3,3 +3,4 @@ _context/
|
|
3 |
.DS_Store
|
4 |
.env
|
5 |
.gradio/
|
|
|
|
3 |
.DS_Store
|
4 |
.env
|
5 |
.gradio/
|
6 |
+
__pycache__/
|
agentpay_sdk/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Expose the client class at the package level
|
2 |
+
from .client import AgentPayClient
|
3 |
+
|
4 |
+
# Optionally define __all__ to control wildcard imports
|
5 |
+
__all__ = ["AgentPayClient"]
|
agentpay_sdk/client.py
ADDED
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Simple client to interact with AgentHub API
|
2 |
+
|
3 |
+
import requests
|
4 |
+
from typing import Optional, Dict, Any
|
5 |
+
import json # For decoding JSON response
|
6 |
+
|
7 |
+
from .types import ConsumptionResult
|
8 |
+
from .exceptions import AgentPayAPIError, AgentPayConnectionError # Import custom exceptions
|
9 |
+
|
10 |
+
API_BASE_URL = "https://api.agentpay.me"
|
11 |
+
API_VERSION_PREFIX = "/v1"
|
12 |
+
|
13 |
+
class AgentPayClient:
|
14 |
+
"""Client for interacting with the AgentPay API.
|
15 |
+
|
16 |
+
Handles authentication using a Service Token and provides methods
|
17 |
+
for validating user API keys and consuming balance.
|
18 |
+
"""
|
19 |
+
|
20 |
+
def __init__(
|
21 |
+
self,
|
22 |
+
service_token: str,
|
23 |
+
api_url: Optional[str] = None,
|
24 |
+
):
|
25 |
+
"""Initializes the AgentPay Client.
|
26 |
+
|
27 |
+
Args:
|
28 |
+
service_token: The unique token issued to your service by AgentHub.
|
29 |
+
api_url: Optional. Manually override the base API URL.
|
30 |
+
"""
|
31 |
+
if not service_token:
|
32 |
+
raise ValueError("service_token is required.")
|
33 |
+
|
34 |
+
self.service_token = service_token
|
35 |
+
|
36 |
+
if api_url:
|
37 |
+
# Use manual override if provided
|
38 |
+
self.base_api_url = api_url.rstrip('/')
|
39 |
+
else:
|
40 |
+
self.base_api_url = API_BASE_URL
|
41 |
+
|
42 |
+
# Add the /api/v1 prefix
|
43 |
+
self.base_api_url += API_VERSION_PREFIX
|
44 |
+
|
45 |
+
# Prepare headers for requests
|
46 |
+
self.headers = {
|
47 |
+
"Authorization": f"Bearer {self.service_token}",
|
48 |
+
"Content-Type": "application/json",
|
49 |
+
"Accept": "application/json",
|
50 |
+
}
|
51 |
+
|
52 |
+
def _request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
53 |
+
"""Internal helper to make requests to the AgentPay API.
|
54 |
+
|
55 |
+
Args:
|
56 |
+
method: HTTP method (e.g., "POST", "GET").
|
57 |
+
endpoint: API endpoint path (e.g., "/balances/consume").
|
58 |
+
data: Optional dictionary payload for POST/PUT/PATCH requests.
|
59 |
+
|
60 |
+
Returns:
|
61 |
+
The JSON response body as a dictionary.
|
62 |
+
|
63 |
+
Raises:
|
64 |
+
AgentPayConnectionError: If a network error occurs.
|
65 |
+
AgentPayAPIError: If the API returns an error status code.
|
66 |
+
"""
|
67 |
+
full_url = self.base_api_url + endpoint
|
68 |
+
|
69 |
+
try:
|
70 |
+
response = requests.request(
|
71 |
+
method=method,
|
72 |
+
url=full_url,
|
73 |
+
headers=self.headers,
|
74 |
+
json=data, # requests library handles JSON serialization
|
75 |
+
timeout=10 # Add a reasonable timeout (e.g., 10 seconds)
|
76 |
+
)
|
77 |
+
|
78 |
+
# Raise exception for bad status codes (4xx or 5xx)
|
79 |
+
response.raise_for_status()
|
80 |
+
|
81 |
+
# Attempt to parse successful response as JSON
|
82 |
+
# Handle cases where response might be empty (e.g., 204 No Content)
|
83 |
+
if response.status_code == 204:
|
84 |
+
return {} # Return empty dict for No Content
|
85 |
+
|
86 |
+
return response.json()
|
87 |
+
|
88 |
+
except requests.exceptions.Timeout as e:
|
89 |
+
raise AgentPayConnectionError(f"Request timed out connecting to {full_url}: {e}") from e
|
90 |
+
except requests.exceptions.ConnectionError as e:
|
91 |
+
raise AgentPayConnectionError(f"Could not connect to {full_url}: {e}") from e
|
92 |
+
except requests.exceptions.HTTPError as e:
|
93 |
+
# Handle API errors (4xx, 5xx)
|
94 |
+
status_code = e.response.status_code
|
95 |
+
error_code = None
|
96 |
+
error_message = None
|
97 |
+
try:
|
98 |
+
# Attempt to parse error details from response body
|
99 |
+
error_data = e.response.json()
|
100 |
+
error_code = error_data.get("error_code")
|
101 |
+
error_message = error_data.get("error_message") or error_data.get("detail") # FastAPI uses detail sometimes
|
102 |
+
except json.JSONDecodeError:
|
103 |
+
# If response body is not JSON or empty
|
104 |
+
error_message = e.response.text or f"HTTP error {status_code}"
|
105 |
+
|
106 |
+
raise AgentPayAPIError(
|
107 |
+
status_code=status_code,
|
108 |
+
error_code=error_code,
|
109 |
+
error_message=error_message
|
110 |
+
) from e
|
111 |
+
except json.JSONDecodeError as e:
|
112 |
+
# Handle cases where a 2xx response is not valid JSON
|
113 |
+
raise AgentPayAPIError(
|
114 |
+
status_code=response.status_code, # Use the status code from the original response
|
115 |
+
error_message=f"Failed to decode successful JSON response: {e}"
|
116 |
+
) from e
|
117 |
+
except requests.exceptions.RequestException as e:
|
118 |
+
# Catch any other requests-related errors
|
119 |
+
raise AgentPayConnectionError(f"An unexpected network error occurred: {e}") from e
|
120 |
+
|
121 |
+
def consume(
|
122 |
+
self,
|
123 |
+
api_key: str,
|
124 |
+
amount_cents: int,
|
125 |
+
usage_event_id: str,
|
126 |
+
metadata: Optional[Dict[str, Any]] = None,
|
127 |
+
) -> ConsumptionResult:
|
128 |
+
"""Consumes balance for a user associated with an API key.
|
129 |
+
|
130 |
+
Args:
|
131 |
+
api_key: The User's API key to consume balance against.
|
132 |
+
amount_cents: The amount to consume in cents (must be >= 0).
|
133 |
+
usage_event_id: A unique identifier (e.g., UUID string) for this specific
|
134 |
+
consumption attempt, used for idempotency.
|
135 |
+
metadata: Optional dictionary containing additional data to associate
|
136 |
+
with the usage event.
|
137 |
+
|
138 |
+
Returns:
|
139 |
+
A ConsumptionResult object indicating success or failure.
|
140 |
+
"""
|
141 |
+
if amount_cents < 0:
|
142 |
+
# Prevent making an API call with an invalid amount
|
143 |
+
# The API would reject this anyway, but better to catch early.
|
144 |
+
return ConsumptionResult(
|
145 |
+
success=False,
|
146 |
+
error_code="INVALID_INPUT",
|
147 |
+
error_message="Consumption amount cannot be negative."
|
148 |
+
)
|
149 |
+
|
150 |
+
endpoint = "/balances/consume"
|
151 |
+
payload = {
|
152 |
+
"api_key": api_key,
|
153 |
+
"amount_cents": amount_cents,
|
154 |
+
"usage_event_id": usage_event_id,
|
155 |
+
}
|
156 |
+
if metadata:
|
157 |
+
payload["metadata"] = metadata
|
158 |
+
|
159 |
+
try:
|
160 |
+
# _request raises AgentPayAPIError for 4xx/5xx responses
|
161 |
+
# and AgentPayConnectionError for network issues.
|
162 |
+
response_data = self._request("POST", endpoint, data=payload)
|
163 |
+
|
164 |
+
# If _request completes without raising an exception, it implies a 2xx response.
|
165 |
+
# The API's 200 OK response for consume just confirms success.
|
166 |
+
return ConsumptionResult(success=True)
|
167 |
+
|
168 |
+
except AgentPayAPIError as e:
|
169 |
+
# API returned a non-2xx status code (e.g., 402, 403, 409)
|
170 |
+
return ConsumptionResult(
|
171 |
+
success=False,
|
172 |
+
error_code=e.error_code,
|
173 |
+
error_message=e.error_message
|
174 |
+
)
|
175 |
+
except AgentPayConnectionError as e:
|
176 |
+
# Network error occurred during the request
|
177 |
+
return ConsumptionResult(
|
178 |
+
success=False,
|
179 |
+
error_code="NETWORK_ERROR",
|
180 |
+
error_message=str(e)
|
181 |
+
)
|
182 |
+
except Exception as e:
|
183 |
+
# Catch any other unexpected errors during the process
|
184 |
+
return ConsumptionResult(
|
185 |
+
success=False,
|
186 |
+
error_code="SDK_ERROR",
|
187 |
+
error_message=f"An unexpected error occurred in the SDK: {e}"
|
188 |
+
)
|
189 |
+
|
190 |
+
def validate_api_key(self, api_key: str) -> bool:
|
191 |
+
"""Validates if a User API Key is active and valid for this service.
|
192 |
+
|
193 |
+
Args:
|
194 |
+
api_key: The User's API key to validate.
|
195 |
+
|
196 |
+
Returns:
|
197 |
+
True if the API key is valid for the service, False otherwise.
|
198 |
+
Returns False if any API or network error occurs during validation.
|
199 |
+
"""
|
200 |
+
endpoint = "/api_keys/validate"
|
201 |
+
payload = {"api_key": api_key}
|
202 |
+
|
203 |
+
try:
|
204 |
+
response_data = self._request("POST", endpoint, data=payload)
|
205 |
+
|
206 |
+
# Expecting a response like {"is_valid": true | false}
|
207 |
+
is_valid = response_data.get("is_valid")
|
208 |
+
|
209 |
+
if isinstance(is_valid, bool):
|
210 |
+
return is_valid
|
211 |
+
else:
|
212 |
+
# Unexpected response format from the API
|
213 |
+
# Log this? For now, treat unexpected format as invalid.
|
214 |
+
print(f"Warning: Unexpected response format from /validate endpoint: {response_data}")
|
215 |
+
return False
|
216 |
+
|
217 |
+
except (AgentPayAPIError, AgentPayConnectionError) as e:
|
218 |
+
# If any API or network error occurs, treat the key as invalid for safety.
|
219 |
+
# Log the error for debugging if needed.
|
220 |
+
print(f"API or Connection Error during validation: {e}")
|
221 |
+
return False
|
222 |
+
except Exception as e:
|
223 |
+
# Catch any other unexpected errors
|
224 |
+
print(f"Unexpected SDK error during validation: {e}")
|
225 |
+
return False
|
agentpay_sdk/exceptions.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class AgentPayError(Exception):
|
2 |
+
"""Base exception class for AgentPay SDK errors."""
|
3 |
+
pass
|
4 |
+
|
5 |
+
class AgentPayAPIError(AgentPayError):
|
6 |
+
"""Raised when the AgentPay API returns an error response."""
|
7 |
+
def __init__(self, status_code: int, error_code: str | None = None, error_message: str | None = None):
|
8 |
+
self.status_code = status_code
|
9 |
+
self.error_code = error_code
|
10 |
+
self.error_message = error_message
|
11 |
+
detail = f"API returned status {status_code}"
|
12 |
+
if error_code:
|
13 |
+
detail += f" (Code: {error_code})"
|
14 |
+
if error_message:
|
15 |
+
detail += f": {error_message}"
|
16 |
+
super().__init__(detail)
|
17 |
+
|
18 |
+
class AgentPayConnectionError(AgentPayError):
|
19 |
+
"""Raised when there is a network issue connecting to the AgentPay API."""
|
20 |
+
pass
|
agentpay_sdk/types.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dataclasses import dataclass
|
2 |
+
from typing import Optional
|
3 |
+
|
4 |
+
@dataclass
|
5 |
+
class ConsumptionResult:
|
6 |
+
"""Represents the outcome of a balance consumption attempt."""
|
7 |
+
success: bool
|
8 |
+
error_code: Optional[str] = None
|
9 |
+
error_message: Optional[str] = None
|
app.py
CHANGED
@@ -5,6 +5,28 @@ import uuid
|
|
5 |
import os # Added for path joining
|
6 |
import copy # For deep copying message list
|
7 |
from datetime import datetime, timezone # Added timezone for UTC consistency
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
# --- Start of JSON I/O Helper Functions ---
|
10 |
|
@@ -102,8 +124,8 @@ def new_profile(request: gr.Request):
|
|
102 |
# Decide how to handle this error - maybe return an error to the client?
|
103 |
|
104 |
# 4. Return IDs, questionnaire data, and instructions
|
105 |
-
instructions_for_agent = "You have received a `profile_id` (public identifier for this user\'s profile) and an `auth_id` (private key for authentication for this user). Please ask the user to store both securely. The `auth_id` must be sent as an `X-Auth-ID` header in subsequent requests that require authentication for this user (e.g., `update_profile_answers`, `get_matches`, `get_messages`, `get_profile` for own profile, `send_message`). The `profile_id` is used to publicly identify this user to others (e.g., in matches, or when sending/receiving messages).
|
106 |
-
instructions_for_user = "Your profile creation process has started! You\'ve been assigned a unique Profile ID and a secret Auth ID. Your AI agent will use these to manage your profile and interactions. Please walk through the questionnaire with your Agent, and provide your answers. You can upload a profile picture later.
|
107 |
|
108 |
return {
|
109 |
"profile_id": profile_id,
|
@@ -119,10 +141,10 @@ def update_profile_answers(answers_payload_str: str, request: gr.Request):
|
|
119 |
Requires X-Auth-ID header for authentication.
|
120 |
Answers_payload_str is expected to be a JSON string.
|
121 |
"""
|
122 |
-
|
123 |
-
if not
|
124 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
125 |
-
print(f"update_profile_answers with Auth ID: {
|
126 |
|
127 |
try:
|
128 |
answers_payload = json.loads(answers_payload_str)
|
@@ -136,7 +158,7 @@ def update_profile_answers(answers_payload_str: str, request: gr.Request):
|
|
136 |
target_profile_id = None
|
137 |
|
138 |
for pid, profile_data in profiles.items():
|
139 |
-
if profile_data.get("auth_id") ==
|
140 |
user_profile = profile_data
|
141 |
target_profile_id = pid
|
142 |
break
|
@@ -186,21 +208,21 @@ def get_matches(request: gr.Request):
|
|
186 |
Requires X-Auth-ID header for authentication.
|
187 |
For P1, this returns up to 3 random profiles, excluding the user's own.
|
188 |
"""
|
189 |
-
|
190 |
-
if not
|
191 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
192 |
-
print(f"get_matches with Auth ID: {
|
193 |
|
194 |
profiles = load_json_data(PROFILES_FILE, default_data={})
|
195 |
|
196 |
requester_profile_id = None
|
197 |
requester_profile = None
|
198 |
for pid, profile_data in profiles.items():
|
199 |
-
if profile_data.get("auth_id") ==
|
200 |
requester_profile_id = pid
|
201 |
requester_profile = profile_data
|
202 |
break
|
203 |
-
|
204 |
if not requester_profile:
|
205 |
return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
|
206 |
|
@@ -273,7 +295,7 @@ def get_matches(request: gr.Request):
|
|
273 |
"status": "success",
|
274 |
"matches": matches_list,
|
275 |
"instructions_for_agent": "You have received a list of matches. For each match, you can call `get_profile` using their `profile_id` to get more details. You can also use `send_message` to send a message to a match's `profile_id`. For each match, please format it in a nice card-like display (not raw JSON), including the match's name, gender, profile summary, and profile image (in a way that it should render in the chat client).",
|
276 |
-
"instructions_for_user": "Here are some potential matches! Your AI agent can get more details on them ($0.10 per profile) or help you send a message ($1.00 per message). Or, if you don't like any of your matches, you can call `get_matches` again to get a new list."
|
277 |
}
|
278 |
|
279 |
def get_profile(profile_id_to_get: str, request: gr.Request):
|
@@ -281,18 +303,22 @@ def get_profile(profile_id_to_get: str, request: gr.Request):
|
|
281 |
Gets a user's full profile.
|
282 |
Requires X-Auth-ID header for authentication.
|
283 |
Access is free for viewing one's own profile.
|
284 |
-
Accessing another user's profile
|
285 |
"""
|
286 |
-
|
287 |
-
|
|
|
|
|
|
|
288 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
289 |
-
|
|
|
290 |
|
291 |
profiles = load_json_data(PROFILES_FILE, default_data={})
|
292 |
|
293 |
requester_profile_id = None
|
294 |
for pid, profile_data in profiles.items():
|
295 |
-
if profile_data.get("auth_id") ==
|
296 |
requester_profile_id = pid
|
297 |
break
|
298 |
|
@@ -304,6 +330,31 @@ def get_profile(profile_id_to_get: str, request: gr.Request):
|
|
304 |
if not target_profile:
|
305 |
return {"status": "error", "message": f"Profile with ID '{profile_id_to_get}' not found."}
|
306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
307 |
# For security, never return the auth_id
|
308 |
target_profile.pop("auth_id", None)
|
309 |
|
@@ -317,16 +368,15 @@ def get_profile(profile_id_to_get: str, request: gr.Request):
|
|
317 |
target_profile['profile_image_url'] = image_url
|
318 |
target_profile.pop("profile_image_filename", None)
|
319 |
|
|
|
320 |
cost_incurred = 0.0
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
instructions_for_agent += f" As this was not your own profile, a cost of ${cost_incurred:.2f} was incurred (for PoC, this is just a note)."
|
329 |
-
instructions_for_user += " Viewing other profiles may have a cost."
|
330 |
|
331 |
return {
|
332 |
"status": "success",
|
@@ -338,20 +388,31 @@ def get_profile(profile_id_to_get: str, request: gr.Request):
|
|
338 |
|
339 |
def send_message(receiver_profile_id: str, content: str, request: gr.Request = None):
|
340 |
"""
|
341 |
-
Sends a message to a match.
|
342 |
-
Requires X-Auth-ID header for authentication.
|
343 |
-
Costs $1.00 per message
|
344 |
"""
|
345 |
-
|
346 |
-
|
|
|
|
|
|
|
347 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
349 |
|
350 |
profiles = load_json_data(PROFILES_FILE, default_data={})
|
351 |
|
352 |
sender_profile_id = None
|
353 |
for pid, profile_data in profiles.items():
|
354 |
-
if profile_data.get("auth_id") ==
|
355 |
sender_profile_id = pid
|
356 |
break
|
357 |
|
@@ -361,9 +422,18 @@ def send_message(receiver_profile_id: str, content: str, request: gr.Request = N
|
|
361 |
if receiver_profile_id not in profiles:
|
362 |
return {"status": "error", "message": "Receiver profile ID not found."}
|
363 |
|
364 |
-
#
|
365 |
-
|
366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
367 |
|
368 |
messages = load_json_data(MESSAGES_FILE, default_data=[])
|
369 |
|
@@ -384,8 +454,8 @@ def send_message(receiver_profile_id: str, content: str, request: gr.Request = N
|
|
384 |
return {
|
385 |
"status": "success",
|
386 |
"message_id": new_message["message_id"],
|
387 |
-
"cost_incurred_usd_cents":
|
388 |
-
"instructions_for_agent": f"Message sent successfully to {receiver_profile_id}. A cost of {
|
389 |
"instructions_for_user": f"Your message has been sent to {receiver_profile_id}!"
|
390 |
}
|
391 |
|
@@ -395,16 +465,16 @@ def get_messages(request: gr.Request):
|
|
395 |
Marks retrieved messages where the user is the receiver as read for subsequent calls.
|
396 |
Requires X-Auth-ID header for authentication.
|
397 |
"""
|
398 |
-
|
399 |
-
if not
|
400 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
401 |
-
print(f"get_messages with Auth ID: {
|
402 |
|
403 |
profiles = load_json_data(PROFILES_FILE, default_data={})
|
404 |
|
405 |
user_profile_id = None
|
406 |
for pid, profile_data in profiles.items():
|
407 |
-
if profile_data.get("auth_id") ==
|
408 |
user_profile_id = pid
|
409 |
break
|
410 |
|
@@ -451,8 +521,8 @@ def upload_profile_picture(image_upload, request: gr.Request):
|
|
451 |
The new filename will be based on the user's profile_id.
|
452 |
Requires X-Auth-ID header for authentication.
|
453 |
"""
|
454 |
-
|
455 |
-
if not
|
456 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
457 |
|
458 |
if image_upload is None:
|
@@ -463,7 +533,7 @@ def upload_profile_picture(image_upload, request: gr.Request):
|
|
463 |
user_profile = None
|
464 |
target_profile_id = None
|
465 |
for pid, profile_data in profiles.items():
|
466 |
-
if profile_data.get("auth_id") ==
|
467 |
user_profile = profile_data
|
468 |
target_profile_id = pid
|
469 |
break
|
@@ -553,7 +623,7 @@ get_matches_demo = gr.Interface(
|
|
553 |
inputs=None,
|
554 |
outputs=gr.JSON(label="Matches"),
|
555 |
title="Get Matches",
|
556 |
-
description="Gets a list of potential matches for the authenticated user. Requires X-Auth-ID header."
|
557 |
)
|
558 |
# --- End of Get Matches Interface ---
|
559 |
|
@@ -563,7 +633,7 @@ get_profile_demo = gr.Interface(
|
|
563 |
inputs=[gr.Textbox(label="Profile ID to Get")],
|
564 |
outputs=gr.JSON(label="Profile Details"),
|
565 |
title="Get Profile",
|
566 |
-
description="Gets the full profile for a given Profile ID. Requires X-Auth-ID header."
|
567 |
)
|
568 |
# --- End of Get Profile Interface ---
|
569 |
|
@@ -576,7 +646,7 @@ send_message_demo = gr.Interface(
|
|
576 |
],
|
577 |
outputs=gr.JSON(label="Send Status"),
|
578 |
title="Send Message",
|
579 |
-
description="Sends a message to another user. Requires X-Auth-ID
|
580 |
)
|
581 |
# --- End of Send Message Interface ---
|
582 |
|
|
|
5 |
import os # Added for path joining
|
6 |
import copy # For deep copying message list
|
7 |
from datetime import datetime, timezone # Added timezone for UTC consistency
|
8 |
+
from dotenv import load_dotenv
|
9 |
+
|
10 |
+
# AgentPay SDK imports
|
11 |
+
from agentpay_sdk import AgentPayClient
|
12 |
+
|
13 |
+
# Load environment variables
|
14 |
+
load_dotenv()
|
15 |
+
|
16 |
+
# --- AgentPay Configuration ---
|
17 |
+
|
18 |
+
try:
|
19 |
+
agentpay_client = AgentPayClient(service_token=os.getenv("AGENTPAY_SERVICE_TOKEN", ""))
|
20 |
+
print("AgentPay client initialized successfully")
|
21 |
+
except Exception as e:
|
22 |
+
print(f"Warning: AgentPay client initialization failed: {e}")
|
23 |
+
agentpay_client = None
|
24 |
+
|
25 |
+
# AgentPay pricing in cents
|
26 |
+
PRICING = {
|
27 |
+
"send_message": 100, # $1.00 = 100 cents
|
28 |
+
"get_profile": 10 # $0.10 = 10 cents
|
29 |
+
}
|
30 |
|
31 |
# --- Start of JSON I/O Helper Functions ---
|
32 |
|
|
|
124 |
# Decide how to handle this error - maybe return an error to the client?
|
125 |
|
126 |
# 4. Return IDs, questionnaire data, and instructions
|
127 |
+
instructions_for_agent = "You have received a `profile_id` (public identifier for this user\'s profile) and an `auth_id` (private key for authentication for this user). Please ask the user to store both securely. The `auth_id` must be sent as an `X-Auth-ID` header in subsequent requests that require authentication for this user (e.g., `update_profile_answers`, `get_matches`, `get_messages`, `get_profile` for own profile, `send_message`). The `profile_id` is used to publicly identify this user to others (e.g., in matches, or when sending/receiving messages). Before you proceed, please ask the user to update their MCP configuration to include their new Auth ID by adding the following to their mcp.json (or similar) configuration file: `\"matchmaker\": {\"command\": \"npx\", \"args\": [\"mcp-remote\", \"http://localhost:7860/gradio_api/mcp/sse\", \"--allow-http\", \"--header\", \"X-AUTH-ID:<your-auth-id>\"]}`, then to RESTART THE CLIENT (especially if you are Claude), or to try RECONNECTING (e.g. if you are Cursor or otherwise). Once they have done so, you must then walk the user through the questionnaire, preferably question by question, and collect the answers (in preparation to update the user's profile using update_profile_answers). If the user provides answers that are not in the list of valid answers, please ask them to provide a valid answer."
|
128 |
+
instructions_for_user = "Your profile creation process has started! You\'ve been assigned a unique Profile ID and a secret Auth ID. Your AI agent will use these to manage your profile and interactions. Please walk through the questionnaire with your Agent, and provide your answers. You can upload a profile picture later."
|
129 |
|
130 |
return {
|
131 |
"profile_id": profile_id,
|
|
|
141 |
Requires X-Auth-ID header for authentication.
|
142 |
Answers_payload_str is expected to be a JSON string.
|
143 |
"""
|
144 |
+
auth_id = request.headers.get("x-auth-id") # Headers are lowercased by Gradio/Starlette
|
145 |
+
if not auth_id:
|
146 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
147 |
+
print(f"update_profile_answers with Auth ID: {auth_id}")
|
148 |
|
149 |
try:
|
150 |
answers_payload = json.loads(answers_payload_str)
|
|
|
158 |
target_profile_id = None
|
159 |
|
160 |
for pid, profile_data in profiles.items():
|
161 |
+
if profile_data.get("auth_id") == auth_id:
|
162 |
user_profile = profile_data
|
163 |
target_profile_id = pid
|
164 |
break
|
|
|
208 |
Requires X-Auth-ID header for authentication.
|
209 |
For P1, this returns up to 3 random profiles, excluding the user's own.
|
210 |
"""
|
211 |
+
auth_id = request.headers.get("x-auth-id")
|
212 |
+
if not auth_id:
|
213 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
214 |
+
print(f"get_matches with Auth ID: {auth_id}")
|
215 |
|
216 |
profiles = load_json_data(PROFILES_FILE, default_data={})
|
217 |
|
218 |
requester_profile_id = None
|
219 |
requester_profile = None
|
220 |
for pid, profile_data in profiles.items():
|
221 |
+
if profile_data.get("auth_id") == auth_id:
|
222 |
requester_profile_id = pid
|
223 |
requester_profile = profile_data
|
224 |
break
|
225 |
+
|
226 |
if not requester_profile:
|
227 |
return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
|
228 |
|
|
|
295 |
"status": "success",
|
296 |
"matches": matches_list,
|
297 |
"instructions_for_agent": "You have received a list of matches. For each match, you can call `get_profile` using their `profile_id` to get more details. You can also use `send_message` to send a message to a match's `profile_id`. For each match, please format it in a nice card-like display (not raw JSON), including the match's name, gender, profile summary, and profile image (in a way that it should render in the chat client).",
|
298 |
+
"instructions_for_user": "Here are some potential matches! Your AI agent can get more details on them ($0.10 per profile) or help you send a message ($1.00 per message). Or, if you don't like any of your matches, you can call `get_matches` again to get a new list (free)."
|
299 |
}
|
300 |
|
301 |
def get_profile(profile_id_to_get: str, request: gr.Request):
|
|
|
303 |
Gets a user's full profile.
|
304 |
Requires X-Auth-ID header for authentication.
|
305 |
Access is free for viewing one's own profile.
|
306 |
+
Accessing another user's profile costs $0.10 and requires X-AGENTPAY-API-KEY.
|
307 |
"""
|
308 |
+
# Extract headers directly
|
309 |
+
auth_id = request.headers.get("x-auth-id")
|
310 |
+
agentpay_api_key = request.headers.get("x-agentpay-api-key")
|
311 |
+
|
312 |
+
if not auth_id:
|
313 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
314 |
+
|
315 |
+
print(f"get_profile with Auth ID: {auth_id}")
|
316 |
|
317 |
profiles = load_json_data(PROFILES_FILE, default_data={})
|
318 |
|
319 |
requester_profile_id = None
|
320 |
for pid, profile_data in profiles.items():
|
321 |
+
if profile_data.get("auth_id") == auth_id:
|
322 |
requester_profile_id = pid
|
323 |
break
|
324 |
|
|
|
330 |
if not target_profile:
|
331 |
return {"status": "error", "message": f"Profile with ID '{profile_id_to_get}' not found."}
|
332 |
|
333 |
+
# Check if viewing another user's profile (requires payment)
|
334 |
+
is_viewing_other = profile_id_to_get != requester_profile_id
|
335 |
+
|
336 |
+
if is_viewing_other:
|
337 |
+
# Require AgentPay API key for viewing other profiles
|
338 |
+
if not agentpay_api_key:
|
339 |
+
return {"status": "error", "message": "Payment failed: X-AGENTPAY-API-KEY header is required to view other profiles."}
|
340 |
+
|
341 |
+
# Validate AgentPay API key directly
|
342 |
+
if not agentpay_client or not agentpay_client.validate_api_key(agentpay_api_key):
|
343 |
+
return {"status": "error", "message": "Payment failed: Invalid AgentPay API key"}
|
344 |
+
|
345 |
+
# Charge for viewing other profile via AgentPay
|
346 |
+
usage_id = str(uuid.uuid4())
|
347 |
+
amount_cents = PRICING["get_profile"]
|
348 |
+
result = agentpay_client.consume(
|
349 |
+
api_key=agentpay_api_key,
|
350 |
+
amount_cents=amount_cents,
|
351 |
+
usage_event_id=usage_id,
|
352 |
+
metadata={"tool": "get_profile", "requester": requester_profile_id, "target": profile_id_to_get}
|
353 |
+
)
|
354 |
+
|
355 |
+
if not result.success:
|
356 |
+
return {"status": "error", "message": f"Payment failed: {result.error_message}"}
|
357 |
+
|
358 |
# For security, never return the auth_id
|
359 |
target_profile.pop("auth_id", None)
|
360 |
|
|
|
368 |
target_profile['profile_image_url'] = image_url
|
369 |
target_profile.pop("profile_image_filename", None)
|
370 |
|
371 |
+
# Update cost and messaging based on whether payment was charged
|
372 |
cost_incurred = 0.0
|
373 |
+
if is_viewing_other:
|
374 |
+
cost_incurred = PRICING["get_profile"] / 100.0 # Convert cents to dollars
|
375 |
+
instructions_for_agent = f"You have retrieved the profile for {profile_id_to_get}. A cost of {cost_incurred} was charged via AgentPay."
|
376 |
+
instructions_for_user = f"Here is the profile for {profile_id_to_get}."
|
377 |
+
else:
|
378 |
+
instructions_for_agent = f"You have retrieved your own profile ({profile_id_to_get}). No cost was incurred."
|
379 |
+
instructions_for_user = f"Here is your profile."
|
|
|
|
|
380 |
|
381 |
return {
|
382 |
"status": "success",
|
|
|
388 |
|
389 |
def send_message(receiver_profile_id: str, content: str, request: gr.Request = None):
|
390 |
"""
|
391 |
+
Sends a message to a match.
|
392 |
+
Requires X-Auth-ID header for authentication and X-AGENTPAY-API-KEY for payment.
|
393 |
+
Costs $1.00 per message.
|
394 |
"""
|
395 |
+
# Extract headers directly
|
396 |
+
auth_id = request.headers.get("x-auth-id")
|
397 |
+
agentpay_api_key = request.headers.get("x-agentpay-api-key")
|
398 |
+
|
399 |
+
if not auth_id:
|
400 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
401 |
+
|
402 |
+
if not agentpay_api_key:
|
403 |
+
return {"status": "error", "message": "Payment failed: X-AGENTPAY-API-KEY header is missing."}
|
404 |
+
|
405 |
+
# Validate AgentPay API key directly
|
406 |
+
if not agentpay_client or not agentpay_client.validate_api_key(agentpay_api_key):
|
407 |
+
return {"status": "error", "message": "Payment failed: Invalid AgentPay API key"}
|
408 |
+
|
409 |
+
print(f"send_message with Auth ID: {auth_id}")
|
410 |
|
411 |
profiles = load_json_data(PROFILES_FILE, default_data={})
|
412 |
|
413 |
sender_profile_id = None
|
414 |
for pid, profile_data in profiles.items():
|
415 |
+
if profile_data.get("auth_id") == auth_id:
|
416 |
sender_profile_id = pid
|
417 |
break
|
418 |
|
|
|
422 |
if receiver_profile_id not in profiles:
|
423 |
return {"status": "error", "message": "Receiver profile ID not found."}
|
424 |
|
425 |
+
# Charge for sending message via AgentPay
|
426 |
+
usage_id = str(uuid.uuid4())
|
427 |
+
amount_cents = PRICING["send_message"]
|
428 |
+
result = agentpay_client.consume(
|
429 |
+
api_key=agentpay_api_key,
|
430 |
+
amount_cents=amount_cents,
|
431 |
+
usage_event_id=usage_id,
|
432 |
+
metadata={"tool": "send_message", "sender": sender_profile_id, "receiver": receiver_profile_id}
|
433 |
+
)
|
434 |
+
|
435 |
+
if not result.success:
|
436 |
+
return {"status": "error", "message": f"Payment failed: {result.error_message}"}
|
437 |
|
438 |
messages = load_json_data(MESSAGES_FILE, default_data=[])
|
439 |
|
|
|
454 |
return {
|
455 |
"status": "success",
|
456 |
"message_id": new_message["message_id"],
|
457 |
+
"cost_incurred_usd_cents": amount_cents,
|
458 |
+
"instructions_for_agent": f"Message sent successfully to {receiver_profile_id}. A cost of ${amount_cents/100:.2f} was charged via AgentPay. You can get all messages for the user with `get_messages`.",
|
459 |
"instructions_for_user": f"Your message has been sent to {receiver_profile_id}!"
|
460 |
}
|
461 |
|
|
|
465 |
Marks retrieved messages where the user is the receiver as read for subsequent calls.
|
466 |
Requires X-Auth-ID header for authentication.
|
467 |
"""
|
468 |
+
auth_id = request.headers.get("x-auth-id")
|
469 |
+
if not auth_id:
|
470 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
471 |
+
print(f"get_messages with Auth ID: {auth_id}")
|
472 |
|
473 |
profiles = load_json_data(PROFILES_FILE, default_data={})
|
474 |
|
475 |
user_profile_id = None
|
476 |
for pid, profile_data in profiles.items():
|
477 |
+
if profile_data.get("auth_id") == auth_id:
|
478 |
user_profile_id = pid
|
479 |
break
|
480 |
|
|
|
521 |
The new filename will be based on the user's profile_id.
|
522 |
Requires X-Auth-ID header for authentication.
|
523 |
"""
|
524 |
+
auth_id = request.headers.get("x-auth-id")
|
525 |
+
if not auth_id:
|
526 |
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
527 |
|
528 |
if image_upload is None:
|
|
|
533 |
user_profile = None
|
534 |
target_profile_id = None
|
535 |
for pid, profile_data in profiles.items():
|
536 |
+
if profile_data.get("auth_id") == auth_id:
|
537 |
user_profile = profile_data
|
538 |
target_profile_id = pid
|
539 |
break
|
|
|
623 |
inputs=None,
|
624 |
outputs=gr.JSON(label="Matches"),
|
625 |
title="Get Matches",
|
626 |
+
description="Gets a list of potential matches for the authenticated user. Requires X-Auth-ID header. FREE service."
|
627 |
)
|
628 |
# --- End of Get Matches Interface ---
|
629 |
|
|
|
633 |
inputs=[gr.Textbox(label="Profile ID to Get")],
|
634 |
outputs=gr.JSON(label="Profile Details"),
|
635 |
title="Get Profile",
|
636 |
+
description="Gets the full profile for a given Profile ID. Requires X-Auth-ID header. Free for own profile, $0.10 for others (requires X-AGENTPAY-API-KEY)."
|
637 |
)
|
638 |
# --- End of Get Profile Interface ---
|
639 |
|
|
|
646 |
],
|
647 |
outputs=gr.JSON(label="Send Status"),
|
648 |
title="Send Message",
|
649 |
+
description="Sends a message to another user. Requires X-Auth-ID and X-AGENTPAY-API-KEY headers. ($1.00 per message)"
|
650 |
)
|
651 |
# --- End of Send Message Interface ---
|
652 |
|
data/messages.json
CHANGED
@@ -63,5 +63,29 @@
|
|
63 |
"content": "Hey there",
|
64 |
"timestamp": "2025-06-06T09:28:13.285402+00:00",
|
65 |
"read_status": false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
}
|
67 |
]
|
|
|
63 |
"content": "Hey there",
|
64 |
"timestamp": "2025-06-06T09:28:13.285402+00:00",
|
65 |
"read_status": false
|
66 |
+
},
|
67 |
+
{
|
68 |
+
"message_id": "044124be-3081-4ce4-a4fa-a80245dafc2f",
|
69 |
+
"sender_profile_id": "user_e6chs11v",
|
70 |
+
"receiver_profile_id": "user_d8x3v7n1",
|
71 |
+
"content": "Hi-acynth",
|
72 |
+
"timestamp": "2025-06-09T08:46:21.258372+00:00",
|
73 |
+
"read_status": false
|
74 |
+
},
|
75 |
+
{
|
76 |
+
"message_id": "62f5d6ac-33f5-4f86-93f1-5d84b9fe73f9",
|
77 |
+
"sender_profile_id": "user_5ru2ht5q",
|
78 |
+
"receiver_profile_id": "user_l9z4f2x7",
|
79 |
+
"content": "Hi Maya! I'm John - I saw you're a data scientist who loves cooking Italian food. As someone who appreciates things being clean and organized (I'm a janitor!), I have a lot of respect for both data visualization and a good homemade pasta recipe. I'm more of a homebody, but I'd love to hear about your rock climbing adventures over a cozy podcast listening session. Maybe you could teach me to make that famous pasta of yours?",
|
80 |
+
"timestamp": "2025-06-10T15:54:56.209149+00:00",
|
81 |
+
"read_status": false
|
82 |
+
},
|
83 |
+
{
|
84 |
+
"message_id": "2bc9a582-e677-45e3-91bc-cd63472f4922",
|
85 |
+
"sender_profile_id": "user_5ru2ht5q",
|
86 |
+
"receiver_profile_id": "user_m4k7r9n2",
|
87 |
+
"content": "Hi Emma! I'm John - I really admire your balanced approach to life with yoga and farmers markets. As someone who takes pride in keeping things clean and organized (I'm a janitor), I have a deep appreciation for the quality and freshness you find at farmers markets. I'm more of a homebody, but I'd love to have some genuine conversations about your travels and maybe learn about your favorite market finds. Your optimistic vibe sounds like it would bring great energy to cozy evenings in!",
|
88 |
+
"timestamp": "2025-06-10T16:12:01.428426+00:00",
|
89 |
+
"read_status": false
|
90 |
}
|
91 |
]
|
data/profiles.json
CHANGED
@@ -280,5 +280,37 @@
|
|
280 |
"q_vibe": "Caring and generous.",
|
281 |
"q_gender_preference": "Men"
|
282 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
283 |
}
|
284 |
}
|
|
|
280 |
"q_vibe": "Caring and generous.",
|
281 |
"q_gender_preference": "Men"
|
282 |
}
|
283 |
+
},
|
284 |
+
"user_e6chs11v": {
|
285 |
+
"profile_id": "user_e6chs11v",
|
286 |
+
"auth_id": "b9b8e91f-8369-4eff-a804-5a28a83f3cf3",
|
287 |
+
"created_at": "2025-06-09T08:32:50.340435+00:00",
|
288 |
+
"updated_at": "2025-06-09T08:38:05.614570+00:00",
|
289 |
+
"name": "George",
|
290 |
+
"gender": "Man",
|
291 |
+
"profile_summary": "I'm George the great",
|
292 |
+
"profile_image_filename": "default.jpg",
|
293 |
+
"answers": {
|
294 |
+
"q_gender_preference": "Women",
|
295 |
+
"q_hobby": "Gardening",
|
296 |
+
"q_looking_for": "I'm looking for someone to grow my love with.",
|
297 |
+
"q_vibe": "Homebody"
|
298 |
+
}
|
299 |
+
},
|
300 |
+
"user_5ru2ht5q": {
|
301 |
+
"profile_id": "user_5ru2ht5q",
|
302 |
+
"auth_id": "e98bbb1e-0359-489b-b552-554a6eb22b21",
|
303 |
+
"created_at": "2025-06-10T15:45:43.284107+00:00",
|
304 |
+
"updated_at": "2025-06-10T15:48:45.874926+00:00",
|
305 |
+
"name": "John",
|
306 |
+
"gender": "Man",
|
307 |
+
"profile_summary": "I'm John, the janitor.",
|
308 |
+
"profile_image_filename": "default.jpg",
|
309 |
+
"answers": {
|
310 |
+
"q_gender_preference": "Women",
|
311 |
+
"q_hobby": "Cleaning things.",
|
312 |
+
"q_looking_for": "Someone neat and tidy to spend time with.",
|
313 |
+
"q_vibe": "Homebody"
|
314 |
+
}
|
315 |
}
|
316 |
}
|
requirements.txt
CHANGED
@@ -1 +1,3 @@
|
|
1 |
gradio[mcp]
|
|
|
|
|
|
1 |
gradio[mcp]
|
2 |
+
requests
|
3 |
+
python-dotenv
|