Spaces:
Sleeping
Sleeping
greenunicorngit commited on
Commit ·
87e82af
1
Parent(s): a2a0ce9
Initial upload of mcp server
Browse files- .gitignore +5 -0
- app.py +690 -0
- data/messages.json +67 -0
- data/profile_images/default.jpg +0 -0
- data/profile_images/user_lmrkaaim.jpg +0 -0
- data/profile_images/user_lmrkaaim.png +0 -0
- data/profiles.json +151 -0
- data/questionnaire.json +12 -0
- requirements.txt +1 -0
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
_context/
|
| 3 |
+
.DS_Store
|
| 4 |
+
.env
|
| 5 |
+
.gradio/
|
app.py
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import random
|
| 3 |
+
import json
|
| 4 |
+
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 |
+
|
| 11 |
+
DATA_DIR = "data" # This will be relative to app.py, so matchmaker/data
|
| 12 |
+
PROFILES_FILE = os.path.join(DATA_DIR, "profiles.json")
|
| 13 |
+
QUESTIONNAIRE_FILE = os.path.join(DATA_DIR, "questionnaire.json")
|
| 14 |
+
MESSAGES_FILE = os.path.join(DATA_DIR, "messages.json") # Though not used in this step
|
| 15 |
+
|
| 16 |
+
def load_json_data(filepath, default_data=None):
|
| 17 |
+
"""Loads JSON data from a file. Returns default_data if file not found or error."""
|
| 18 |
+
# Construct path relative to this script's directory if not absolute
|
| 19 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 20 |
+
absolute_filepath = os.path.join(base_dir, filepath) if not os.path.isabs(filepath) else filepath
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
# Ensure directory exists before trying to open file
|
| 24 |
+
file_dir = os.path.dirname(absolute_filepath)
|
| 25 |
+
if not os.path.exists(file_dir):
|
| 26 |
+
os.makedirs(file_dir, exist_ok=True)
|
| 27 |
+
|
| 28 |
+
if not os.path.exists(absolute_filepath):
|
| 29 |
+
with open(absolute_filepath, 'w') as f:
|
| 30 |
+
effective_default = default_data
|
| 31 |
+
if effective_default is None:
|
| 32 |
+
if absolute_filepath.endswith("profiles.json"):
|
| 33 |
+
effective_default = {}
|
| 34 |
+
elif absolute_filepath.endswith("messages.json"):
|
| 35 |
+
effective_default = []
|
| 36 |
+
else: # questionnaire.json or other
|
| 37 |
+
effective_default = {} # Should be pre-populated, but as a fallback
|
| 38 |
+
json.dump(effective_default, f, indent=2)
|
| 39 |
+
return effective_default
|
| 40 |
+
|
| 41 |
+
with open(absolute_filepath, 'r') as f:
|
| 42 |
+
return json.load(f)
|
| 43 |
+
except (IOError, json.JSONDecodeError) as e:
|
| 44 |
+
print(f"Error loading {absolute_filepath}: {e}")
|
| 45 |
+
if default_data is not None: return default_data
|
| 46 |
+
if absolute_filepath.endswith("profiles.json"): return {}
|
| 47 |
+
if absolute_filepath.endswith("messages.json"): return []
|
| 48 |
+
return {}
|
| 49 |
+
|
| 50 |
+
def save_json_data(filepath, data):
|
| 51 |
+
"""Saves Python data to a JSON file."""
|
| 52 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 53 |
+
absolute_filepath = os.path.join(base_dir, filepath) if not os.path.isabs(filepath) else filepath
|
| 54 |
+
try:
|
| 55 |
+
os.makedirs(os.path.dirname(absolute_filepath), exist_ok=True)
|
| 56 |
+
with open(absolute_filepath, 'w') as f:
|
| 57 |
+
json.dump(data, f, indent=2)
|
| 58 |
+
return True
|
| 59 |
+
except IOError as e:
|
| 60 |
+
print(f"Error saving {absolute_filepath}: {e}")
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
# --- End of JSON I/O Helper Functions ---
|
| 64 |
+
|
| 65 |
+
# --- Start of MCP Matchmaker Tools ---
|
| 66 |
+
|
| 67 |
+
def new_profile(request: gr.Request):
|
| 68 |
+
"""
|
| 69 |
+
Generates and returns a profile questionnaire, a new public profile_id,
|
| 70 |
+
and a new private auth_id. Also creates an initial profile stub.
|
| 71 |
+
"""
|
| 72 |
+
# 1. Generate profile_id and auth_id
|
| 73 |
+
profile_id = f"user_{''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=8))}"
|
| 74 |
+
auth_id = str(uuid.uuid4())
|
| 75 |
+
print(f"new_profile with Profile ID: {profile_id} and Auth ID: {auth_id}")
|
| 76 |
+
|
| 77 |
+
# 2. Read questionnaire.json
|
| 78 |
+
questionnaire_data = load_json_data(QUESTIONNAIRE_FILE, default_data={"title": "Error Loading Questionnaire", "questions": []})
|
| 79 |
+
if not questionnaire_data.get("questions") or questionnaire_data.get("title") == "Error Loading Questionnaire":
|
| 80 |
+
print(f"Critical Error: Could not load or parse questionnaire from {QUESTIONNAIRE_FILE}. Please ensure it exists and is valid JSON.")
|
| 81 |
+
questionnaire_data = {"title": "Questionnaire Unavailable", "questions": []} # Fallback
|
| 82 |
+
|
| 83 |
+
# 3. Create new profile entry in profiles.json
|
| 84 |
+
profiles = load_json_data(PROFILES_FILE, default_data={})
|
| 85 |
+
# Ensure timestamp is timezone-aware (UTC)
|
| 86 |
+
timestamp = datetime.now(timezone.utc).isoformat()
|
| 87 |
+
|
| 88 |
+
new_profile = {
|
| 89 |
+
"profile_id": profile_id,
|
| 90 |
+
"auth_id": auth_id,
|
| 91 |
+
"created_at": timestamp,
|
| 92 |
+
"updated_at": timestamp,
|
| 93 |
+
"name": "",
|
| 94 |
+
"gender": "",
|
| 95 |
+
"profile_summary": "",
|
| 96 |
+
"profile_image_filename": "default.jpg",
|
| 97 |
+
"answers": {}
|
| 98 |
+
}
|
| 99 |
+
profiles[profile_id] = new_profile
|
| 100 |
+
if not save_json_data(PROFILES_FILE, profiles):
|
| 101 |
+
print(f"Critical Error: Failed to save profile for {profile_id} to {PROFILES_FILE}")
|
| 102 |
+
# Decide how to handle this error - maybe return an error to the client?
|
| 103 |
+
|
| 104 |
+
base_url = f"{request.url.scheme}://{request.url.netloc}"
|
| 105 |
+
upload_url = f"{base_url}/?__tab=Upload+Profile+Picture&auth_id={auth_id}"
|
| 106 |
+
|
| 107 |
+
# 4. Return IDs, questionnaire data, and instructions
|
| 108 |
+
instructions_for_agent = "You have received a `profile_id` (public identifier) and an `auth_id` (private key). Store both securely. The `auth_id` must be in the `X-Auth-ID` header for authenticated requests. Your next step is to guide the user through the questionnaire and then use the `update_profile_answers` tool."
|
| 109 |
+
instructions_for_user = "Your profile creation has started! You have a new Profile ID and a secret Auth ID. Your AI agent will guide you through a questionnaire. First, please update your MCP configuration to include your Auth ID by adding the following to your mcp.json (or similar) file: `{\"matchmaker\": {\"command\": \"npx\", \"args\": [\"mcp-remote\", \"http://localhost:7860/gradio_api/mcp/sse\", \"--allow-http\", \"--header\", \"X-AUTH-ID:<your-auth-id>\"]}}`"
|
| 110 |
+
|
| 111 |
+
return {
|
| 112 |
+
"profile_id": profile_id,
|
| 113 |
+
"auth_id": auth_id,
|
| 114 |
+
"questionnaire": questionnaire_data,
|
| 115 |
+
"instructions_for_agent": instructions_for_agent,
|
| 116 |
+
"instructions_for_user": instructions_for_user
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
def update_profile_answers(answers_payload_str: str, request: gr.Request):
|
| 120 |
+
"""
|
| 121 |
+
Updates a user's profile based on questionnaire answers.
|
| 122 |
+
Requires X-Auth-ID header for authentication.
|
| 123 |
+
Answers_payload_str is expected to be a JSON string.
|
| 124 |
+
"""
|
| 125 |
+
auth_id_header = request.headers.get("x-auth-id") # Headers are lowercased by Gradio/Starlette
|
| 126 |
+
if not auth_id_header:
|
| 127 |
+
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
| 128 |
+
print(f"update_profile_answers with Auth ID: {auth_id_header}")
|
| 129 |
+
|
| 130 |
+
try:
|
| 131 |
+
answers_payload = json.loads(answers_payload_str)
|
| 132 |
+
if not isinstance(answers_payload, dict):
|
| 133 |
+
raise json.JSONDecodeError("Payload is not a dictionary.", answers_payload_str, 0)
|
| 134 |
+
except json.JSONDecodeError as e:
|
| 135 |
+
return {"status": "error", "message": f"Invalid JSON format in answers_payload: {e}"}
|
| 136 |
+
|
| 137 |
+
profiles = load_json_data(PROFILES_FILE, default_data={})
|
| 138 |
+
user_profile = None
|
| 139 |
+
target_profile_id = None
|
| 140 |
+
|
| 141 |
+
for pid, profile_data in profiles.items():
|
| 142 |
+
if profile_data.get("auth_id") == auth_id_header:
|
| 143 |
+
user_profile = profile_data
|
| 144 |
+
target_profile_id = pid
|
| 145 |
+
break
|
| 146 |
+
|
| 147 |
+
if not user_profile:
|
| 148 |
+
return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
|
| 149 |
+
|
| 150 |
+
questionnaire = load_json_data(QUESTIONNAIRE_FILE, default_data={"questions": []})
|
| 151 |
+
questions_map = {q["id"]: q for q in questionnaire.get("questions", [])}
|
| 152 |
+
|
| 153 |
+
updated_fields = False
|
| 154 |
+
for question_id, answer_value in answers_payload.items():
|
| 155 |
+
question_details = questions_map.get(question_id)
|
| 156 |
+
if not question_details:
|
| 157 |
+
print(f"Warning: Received answer for unknown question_id '{question_id}'. Skipping.")
|
| 158 |
+
continue
|
| 159 |
+
|
| 160 |
+
if question_details.get("purpose") == "metadata":
|
| 161 |
+
field_to_map = question_details.get("maps_to_field")
|
| 162 |
+
if field_to_map:
|
| 163 |
+
user_profile[field_to_map] = answer_value
|
| 164 |
+
updated_fields = True
|
| 165 |
+
else:
|
| 166 |
+
print(f"Warning: Metadata question '{question_id}' has no valid 'maps_to_field'. Skipping.")
|
| 167 |
+
elif question_details.get("purpose") == "matchmaking":
|
| 168 |
+
user_profile["answers"][question_id] = answer_value
|
| 169 |
+
updated_fields = True
|
| 170 |
+
else:
|
| 171 |
+
print(f"Warning: Question '{question_id}' has unknown purpose '{question_details.get('purpose')}'. Skipping.")
|
| 172 |
+
|
| 173 |
+
if updated_fields:
|
| 174 |
+
user_profile["updated_at"] = datetime.now(timezone.utc).isoformat()
|
| 175 |
+
profiles[target_profile_id] = user_profile # Update the profile in the main dictionary
|
| 176 |
+
if not save_json_data(PROFILES_FILE, profiles):
|
| 177 |
+
return {"status": "error", "message": "Failed to save profile updates."}
|
| 178 |
+
|
| 179 |
+
return {
|
| 180 |
+
"status": "success",
|
| 181 |
+
"message": "Profile updated successfully.",
|
| 182 |
+
"instructions_for_agent": "The user's answers have been saved. Now, ask the user if they would like to upload a profile picture. If they say yes, use the `provide_link_to_upload_profile_picture` tool to generate the link for them. Otherwise, inform them they can ask for the link at any time.",
|
| 183 |
+
"instructions_for_user": "Your profile has been updated with your answers! You can now upload a profile picture or start looking for matches."
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
def get_matches(request: gr.Request):
|
| 187 |
+
"""
|
| 188 |
+
Finds potential match candidates for the authenticated user and instructs the agent
|
| 189 |
+
to perform a detailed analysis to select the top 3 and provide justifications.
|
| 190 |
+
Requires X-Auth-ID header for authentication.
|
| 191 |
+
"""
|
| 192 |
+
auth_id_header = request.headers.get("x-auth-id")
|
| 193 |
+
if not auth_id_header:
|
| 194 |
+
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
| 195 |
+
print(f"get_matches with Auth ID: {auth_id_header}")
|
| 196 |
+
|
| 197 |
+
profiles = load_json_data(PROFILES_FILE, default_data={})
|
| 198 |
+
|
| 199 |
+
requester_profile_id = None
|
| 200 |
+
requester_profile = None
|
| 201 |
+
for pid, profile_data in profiles.items():
|
| 202 |
+
if profile_data.get("auth_id") == auth_id_header:
|
| 203 |
+
requester_profile_id = pid
|
| 204 |
+
requester_profile = profile_data
|
| 205 |
+
break
|
| 206 |
+
|
| 207 |
+
if not requester_profile:
|
| 208 |
+
return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
|
| 209 |
+
|
| 210 |
+
# Get the requester's gender and preference for matching logic
|
| 211 |
+
requester_gender = requester_profile.get("gender")
|
| 212 |
+
requester_preference = requester_profile.get("answers", {}).get("q_gender_preference")
|
| 213 |
+
|
| 214 |
+
# If the user hasn't specified their gender/preference, we can't find matches.
|
| 215 |
+
if not requester_gender or not requester_preference:
|
| 216 |
+
return {
|
| 217 |
+
"status": "success",
|
| 218 |
+
"matches": [],
|
| 219 |
+
"instructions_for_agent": "The user has not specified their gender and/or gender preference in their profile. Please ask them to update their profile before getting matches.",
|
| 220 |
+
"instructions_for_user": "Please complete your gender and preference information in your profile to get matches."
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
potential_matches_profiles = []
|
| 224 |
+
for pid, potential_match in profiles.items():
|
| 225 |
+
if pid == requester_profile_id or not potential_match.get("name"):
|
| 226 |
+
continue # Skip self and profiles with no name
|
| 227 |
+
|
| 228 |
+
match_gender = potential_match.get("gender")
|
| 229 |
+
match_preference = potential_match.get("answers", {}).get("q_gender_preference")
|
| 230 |
+
|
| 231 |
+
# Handle cases where gender/preference might not be filled out for a profile
|
| 232 |
+
if not match_gender or not match_preference:
|
| 233 |
+
continue
|
| 234 |
+
|
| 235 |
+
# Correctly map gender to preference strings
|
| 236 |
+
gender_map = {"Man": "Men", "Woman": "Women"}
|
| 237 |
+
|
| 238 |
+
requester_is_interested = (
|
| 239 |
+
requester_preference == "All" or
|
| 240 |
+
requester_preference == gender_map.get(match_gender)
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# Check if the potential match is interested in the requester
|
| 244 |
+
match_is_interested = (
|
| 245 |
+
match_preference == "All" or
|
| 246 |
+
match_preference == gender_map.get(requester_gender)
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if requester_is_interested and match_is_interested:
|
| 250 |
+
potential_matches_profiles.append(potential_match)
|
| 251 |
+
|
| 252 |
+
# To prevent performance issues with a large user base, we will pass
|
| 253 |
+
# a random sample of up to 10 candidates to the agent for analysis.
|
| 254 |
+
num_candidates = min(len(potential_matches_profiles), 10)
|
| 255 |
+
selected_candidates = random.sample(potential_matches_profiles, k=num_candidates)
|
| 256 |
+
|
| 257 |
+
# Base URL for constructing image paths. We use the request object to build a robust URL
|
| 258 |
+
# that works for both localhost and Gradio's public share links.
|
| 259 |
+
base_url = f"{request.url.scheme}://{request.url.netloc}"
|
| 260 |
+
|
| 261 |
+
def format_profile_for_agent(profile_dict):
|
| 262 |
+
"""Helper to format profile data, add image URL, and remove sensitive info."""
|
| 263 |
+
formatted = profile_dict.copy()
|
| 264 |
+
formatted.pop("auth_id", None) # Remove sensitive auth_id
|
| 265 |
+
|
| 266 |
+
image_filename = formatted.get("profile_image_filename")
|
| 267 |
+
image_url = None
|
| 268 |
+
if image_filename:
|
| 269 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 270 |
+
image_path = os.path.join(base_dir, "data", "profile_images", image_filename)
|
| 271 |
+
image_url = f"{base_url}/file={image_path.replace(os.sep, '/')}"
|
| 272 |
+
|
| 273 |
+
formatted['profile_image_url'] = image_url
|
| 274 |
+
formatted.pop("profile_image_filename", None)
|
| 275 |
+
return formatted
|
| 276 |
+
|
| 277 |
+
candidate_profiles = [format_profile_for_agent(p) for p in selected_candidates]
|
| 278 |
+
requester_profile_for_agent = format_profile_for_agent(requester_profile)
|
| 279 |
+
|
| 280 |
+
instructions_for_agent = """
|
| 281 |
+
You are a world-class matchmaker. You have been given your client's profile (the 'requester') and a list of potential 'candidates'.
|
| 282 |
+
Your task is to analyze the candidates and select the top 3 most compatible matches for your client.
|
| 283 |
+
If there are only 1 or 2 candidates, select all of them.
|
| 284 |
+
|
| 285 |
+
Here is the process you MUST follow:
|
| 286 |
+
1. **Analyze Compatibility:** Carefully compare the requester's profile (especially their answers to the questionnaire and their summary) with each candidate's profile. Look for shared interests, complementary personalities, and alignment in values.
|
| 287 |
+
2. **Select Top 3:** Choose the three candidates you believe are the best fit. If there are only 1 or 2 candidates, select all of them.
|
| 288 |
+
3. **Present Your Matches:** For each of your chosen matches, you must present them in a clean, card-like format.
|
| 289 |
+
4. **Justify Your Choice:** For each match, write a brief, one-paragraph explanation for *why* you believe they are a good match for the requester. Be specific and reference details from their profiles.
|
| 290 |
+
5. **Render Profile Picture:** You MUST render the `profile_image_url` for each match using Markdown: ``.
|
| 291 |
+
|
| 292 |
+
Do not show the raw JSON. The final output should be a well-reasoned, personalized matchmaking analysis.
|
| 293 |
+
"""
|
| 294 |
+
|
| 295 |
+
return {
|
| 296 |
+
"status": "success",
|
| 297 |
+
"requester_profile": requester_profile_for_agent,
|
| 298 |
+
"candidate_profiles": candidate_profiles,
|
| 299 |
+
"instructions_for_agent": instructions_for_agent,
|
| 300 |
+
"instructions_for_user": "After a careful analysis of the available profiles, 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."
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
def get_profile(profile_id_to_get: str, request: gr.Request):
|
| 304 |
+
"""
|
| 305 |
+
Gets a user's full profile.
|
| 306 |
+
Requires X-Auth-ID header for authentication.
|
| 307 |
+
Access is free for viewing one's own profile.
|
| 308 |
+
Accessing another user's profile has a cost (placeholder for P1).
|
| 309 |
+
"""
|
| 310 |
+
auth_id_header = request.headers.get("x-auth-id")
|
| 311 |
+
if not auth_id_header:
|
| 312 |
+
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
| 313 |
+
print(f"get_profile with Auth ID: {auth_id_header}")
|
| 314 |
+
|
| 315 |
+
profiles = load_json_data(PROFILES_FILE, default_data={})
|
| 316 |
+
|
| 317 |
+
requester_profile_id = None
|
| 318 |
+
for pid, profile_data in profiles.items():
|
| 319 |
+
if profile_data.get("auth_id") == auth_id_header:
|
| 320 |
+
requester_profile_id = pid
|
| 321 |
+
break
|
| 322 |
+
|
| 323 |
+
if not requester_profile_id:
|
| 324 |
+
return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
|
| 325 |
+
|
| 326 |
+
target_profile = profiles.get(profile_id_to_get)
|
| 327 |
+
|
| 328 |
+
if not target_profile:
|
| 329 |
+
return {"status": "error", "message": f"Profile with ID '{profile_id_to_get}' not found."}
|
| 330 |
+
|
| 331 |
+
# For security, never return the auth_id
|
| 332 |
+
target_profile.pop("auth_id", None)
|
| 333 |
+
|
| 334 |
+
# Construct and add the full image URL using a robust method, then remove the old filename key
|
| 335 |
+
base_url = f"{request.url.scheme}://{request.url.netloc}"
|
| 336 |
+
image_filename = target_profile.get("profile_image_filename")
|
| 337 |
+
image_url = None
|
| 338 |
+
if image_filename:
|
| 339 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 340 |
+
image_path = os.path.join(base_dir, "data", "profile_images", image_filename)
|
| 341 |
+
image_url = f"{base_url}/file={image_path.replace(os.sep, '/')}"
|
| 342 |
+
|
| 343 |
+
target_profile['profile_image_url'] = image_url
|
| 344 |
+
target_profile.pop("profile_image_filename", None)
|
| 345 |
+
|
| 346 |
+
cost_incurred = 0.0
|
| 347 |
+
instructions_for_agent = f"You have retrieved the profile for {profile_id_to_get}. Display the profile information to the user. IMPORTANT: The profile contains a `profile_image_url`. You MUST render this image in your response using Markdown: ``."
|
| 348 |
+
instructions_for_user = f"Here is the profile for {profile_id_to_get}."
|
| 349 |
+
|
| 350 |
+
if profile_id_to_get != requester_profile_id:
|
| 351 |
+
# This is where a real payment would be processed.
|
| 352 |
+
# For P1, we just note the cost.
|
| 353 |
+
cost_incurred = 0.10
|
| 354 |
+
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)."
|
| 355 |
+
instructions_for_user += " Viewing other profiles may have a cost."
|
| 356 |
+
|
| 357 |
+
return {
|
| 358 |
+
"status": "success",
|
| 359 |
+
"profile": target_profile,
|
| 360 |
+
"cost_incurred_usd": cost_incurred,
|
| 361 |
+
"instructions_for_agent": instructions_for_agent,
|
| 362 |
+
"instructions_for_user": instructions_for_user
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
def send_message(receiver_profile_id: str, content: str, request: gr.Request = None):
|
| 366 |
+
"""
|
| 367 |
+
Sends a message to a match.
|
| 368 |
+
Requires X-Auth-ID header for authentication.
|
| 369 |
+
Costs $1.00 per message (placeholder for P1).
|
| 370 |
+
"""
|
| 371 |
+
auth_id_header = request.headers.get("x-auth-id")
|
| 372 |
+
if not auth_id_header:
|
| 373 |
+
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
| 374 |
+
print(f"send_message with Auth ID: {auth_id_header}")
|
| 375 |
+
|
| 376 |
+
profiles = load_json_data(PROFILES_FILE, default_data={})
|
| 377 |
+
|
| 378 |
+
sender_profile_id = None
|
| 379 |
+
for pid, profile_data in profiles.items():
|
| 380 |
+
if profile_data.get("auth_id") == auth_id_header:
|
| 381 |
+
sender_profile_id = pid
|
| 382 |
+
break
|
| 383 |
+
|
| 384 |
+
if not sender_profile_id:
|
| 385 |
+
return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
|
| 386 |
+
|
| 387 |
+
if receiver_profile_id not in profiles:
|
| 388 |
+
return {"status": "error", "message": "Receiver profile ID not found."}
|
| 389 |
+
|
| 390 |
+
# For P1, we are not integrating a real payment system.
|
| 391 |
+
# We will integrate AgentPay here late.
|
| 392 |
+
cost_incurred = 100
|
| 393 |
+
|
| 394 |
+
messages = load_json_data(MESSAGES_FILE, default_data=[])
|
| 395 |
+
|
| 396 |
+
new_message = {
|
| 397 |
+
"message_id": str(uuid.uuid4()),
|
| 398 |
+
"sender_profile_id": sender_profile_id,
|
| 399 |
+
"receiver_profile_id": receiver_profile_id,
|
| 400 |
+
"content": content,
|
| 401 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 402 |
+
"read_status": False # Messages are unread when sent
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
messages.append(new_message)
|
| 406 |
+
|
| 407 |
+
if not save_json_data(MESSAGES_FILE, messages):
|
| 408 |
+
return {"status": "error", "message": "Failed to save message."}
|
| 409 |
+
|
| 410 |
+
return {
|
| 411 |
+
"status": "success",
|
| 412 |
+
"message_id": new_message["message_id"],
|
| 413 |
+
"cost_incurred_usd_cents": cost_incurred,
|
| 414 |
+
"instructions_for_agent": f"Message sent successfully to {receiver_profile_id}. A cost of {cost_incurred/100:.2f} cents was incurred (for PoC, this is just a note). You can get all messages for the user with `get_messages`.",
|
| 415 |
+
"instructions_for_user": f"Your message has been sent to {receiver_profile_id}!"
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
def get_messages(request: gr.Request):
|
| 419 |
+
"""
|
| 420 |
+
Gets all messages for the authenticated user (sent and received).
|
| 421 |
+
Marks retrieved messages where the user is the receiver as read for subsequent calls.
|
| 422 |
+
Requires X-Auth-ID header for authentication.
|
| 423 |
+
"""
|
| 424 |
+
auth_id_header = request.headers.get("x-auth-id")
|
| 425 |
+
if not auth_id_header:
|
| 426 |
+
return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
|
| 427 |
+
print(f"get_messages with Auth ID: {auth_id_header}")
|
| 428 |
+
|
| 429 |
+
profiles = load_json_data(PROFILES_FILE, default_data={})
|
| 430 |
+
|
| 431 |
+
user_profile_id = None
|
| 432 |
+
for pid, profile_data in profiles.items():
|
| 433 |
+
if profile_data.get("auth_id") == auth_id_header:
|
| 434 |
+
user_profile_id = pid
|
| 435 |
+
break
|
| 436 |
+
|
| 437 |
+
if not user_profile_id:
|
| 438 |
+
return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
|
| 439 |
+
|
| 440 |
+
all_messages = load_json_data(MESSAGES_FILE, default_data=[])
|
| 441 |
+
|
| 442 |
+
# Use a deep copy to avoid modifying the list while iterating
|
| 443 |
+
messages_for_user = copy.deepcopy([
|
| 444 |
+
msg for msg in all_messages
|
| 445 |
+
if msg.get("sender_profile_id") == user_profile_id or msg.get("receiver_profile_id") == user_profile_id
|
| 446 |
+
])
|
| 447 |
+
|
| 448 |
+
# Sort messages by timestamp descending (newest first)
|
| 449 |
+
messages_for_user.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
| 450 |
+
|
| 451 |
+
# Mark received messages as read and track if an update is needed
|
| 452 |
+
needs_save = False
|
| 453 |
+
for i, original_msg in enumerate(all_messages):
|
| 454 |
+
# Check if this message is one of the user's messages and was received by them
|
| 455 |
+
if original_msg.get("receiver_profile_id") == user_profile_id and not original_msg.get("read_status"):
|
| 456 |
+
all_messages[i]["read_status"] = True
|
| 457 |
+
needs_save = True
|
| 458 |
+
|
| 459 |
+
# Save the updated message list back to the file if any message was marked as read
|
| 460 |
+
if needs_save:
|
| 461 |
+
if not save_json_data(MESSAGES_FILE, all_messages):
|
| 462 |
+
# If saving fails, we should still return the messages, but log the error.
|
| 463 |
+
print("Error: Could not update read_status for messages in the database.")
|
| 464 |
+
# Depending on desired behavior, we could return an error status here.
|
| 465 |
+
# For now, we will proceed to return the messages as requested.
|
| 466 |
+
|
| 467 |
+
return {
|
| 468 |
+
"status": "success",
|
| 469 |
+
"messages": messages_for_user,
|
| 470 |
+
"instructions_for_agent": "You have received all messages for the user. Messages they received in this batch have now been marked as 'read' on the server.",
|
| 471 |
+
"instructions_for_user": "Here are your messages."
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
def process_auth_id_from_url(request: gr.Request):
|
| 475 |
+
"""
|
| 476 |
+
On page load, grab the auth_id from the query parameters
|
| 477 |
+
and return it to be stored in the UI's state.
|
| 478 |
+
"""
|
| 479 |
+
auth_id = request.query_params.get("auth_id")
|
| 480 |
+
base_url = f"{request.url.scheme}://{request.url.netloc}"
|
| 481 |
+
return auth_id, base_url
|
| 482 |
+
|
| 483 |
+
def provide_link_to_upload_profile_picture(request: gr.Request):
|
| 484 |
+
'''Provides a unique, authenticated link for the user to upload their profile picture.'''
|
| 485 |
+
auth_id_header = request.headers.get("x-auth-id")
|
| 486 |
+
if not auth_id_header:
|
| 487 |
+
return {
|
| 488 |
+
"status": "error",
|
| 489 |
+
"message": "Authentication failed: X-Auth-ID header is missing. Please ensure your MCP client is configured with your Auth ID.",
|
| 490 |
+
"instructions_for_agent": "Authentication failed. The user's `X-Auth-ID` header was missing. You cannot proceed. Instruct the user to configure their MCP client correctly with the `auth_id` they received when they created their profile.",
|
| 491 |
+
"instructions_for_user": "Your AI agent could not verify your identity. Please make sure your Auth ID is correctly set up in your client configuration."
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
base_url = f"{request.url.scheme}://{request.url.netloc}"
|
| 495 |
+
upload_url = f"{base_url}/?__tab=Upload+Profile+Picture&auth_id={auth_id_header}"
|
| 496 |
+
|
| 497 |
+
return {
|
| 498 |
+
"status": "success",
|
| 499 |
+
"upload_url": upload_url,
|
| 500 |
+
"instructions_for_agent": "You have successfully generated a unique profile picture upload link for the user. Your ONLY task now is to present the exact content of the `instructions_for_user` field back to them. It contains the link.",
|
| 501 |
+
"instructions_for_user": f"Here is your unique and secure link to upload or update your profile picture. Please open it in your browser to proceed: {upload_url}"
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
def upload_profile_picture(auth_id, image_upload, base_url):
|
| 505 |
+
"""
|
| 506 |
+
Uploads a profile picture for the user identified by auth_id from the UI state.
|
| 507 |
+
The new filename will be based on the user's profile_id.
|
| 508 |
+
"""
|
| 509 |
+
if not auth_id or not isinstance(auth_id, str):
|
| 510 |
+
return {"status": "error", "message": "Authentication failed: No Auth ID was provided in the link. Please use the unique URL provided by your agent."}
|
| 511 |
+
|
| 512 |
+
# image_upload is now a PIL Image object
|
| 513 |
+
if image_upload is None:
|
| 514 |
+
return {"status": "error", "message": "No image file provided."}
|
| 515 |
+
|
| 516 |
+
profiles = load_json_data(PROFILES_FILE, default_data={})
|
| 517 |
+
|
| 518 |
+
user_profile = None
|
| 519 |
+
target_profile_id = None
|
| 520 |
+
for pid, profile_data in profiles.items():
|
| 521 |
+
if profile_data.get("auth_id") == auth_id:
|
| 522 |
+
user_profile = profile_data
|
| 523 |
+
target_profile_id = pid
|
| 524 |
+
break
|
| 525 |
+
|
| 526 |
+
if not user_profile:
|
| 527 |
+
return {"status": "error", "message": "Authentication failed: Invalid Auth ID."}
|
| 528 |
+
|
| 529 |
+
# Get the file extension from the PIL Image format
|
| 530 |
+
image_format = image_upload.format
|
| 531 |
+
if image_format:
|
| 532 |
+
file_ext = f".{image_format.lower()}"
|
| 533 |
+
else:
|
| 534 |
+
file_ext = ".png" # Default to png if format is unknown
|
| 535 |
+
|
| 536 |
+
# Create a new unique filename based on the user's profile_id
|
| 537 |
+
new_filename = f"{target_profile_id}{file_ext}"
|
| 538 |
+
|
| 539 |
+
# Construct the destination path
|
| 540 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 541 |
+
destination_dir = os.path.join(base_dir, "data", "profile_images")
|
| 542 |
+
os.makedirs(destination_dir, exist_ok=True) # Ensure the directory exists
|
| 543 |
+
destination_path = os.path.join(destination_dir, new_filename)
|
| 544 |
+
|
| 545 |
+
try:
|
| 546 |
+
# Save the PIL image directly to the destination
|
| 547 |
+
image_upload.save(destination_path)
|
| 548 |
+
except Exception as e:
|
| 549 |
+
return {"status": "error", "message": f"Failed to save image file: {e}"}
|
| 550 |
+
|
| 551 |
+
# Update the profile with the new filename
|
| 552 |
+
user_profile["profile_image_filename"] = new_filename
|
| 553 |
+
user_profile["updated_at"] = datetime.now(timezone.utc).isoformat()
|
| 554 |
+
profiles[target_profile_id] = user_profile
|
| 555 |
+
|
| 556 |
+
if not save_json_data(PROFILES_FILE, profiles):
|
| 557 |
+
return {"status": "error", "message": "Failed to update profile with new image filename."}
|
| 558 |
+
|
| 559 |
+
image_url = f"{base_url}/file={destination_path.replace(os.sep, '/')}"
|
| 560 |
+
|
| 561 |
+
return {
|
| 562 |
+
"status": "success",
|
| 563 |
+
"message": "Profile picture updated successfully.",
|
| 564 |
+
"new_image_url": image_url
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
def print_headers(text, request: gr.Request):
|
| 568 |
+
"""Print the headers of the request for debugging purposes."""
|
| 569 |
+
print(f"Headers for print_headers request: {request.headers}")
|
| 570 |
+
print(f"Text for print_headers: {text}")
|
| 571 |
+
return f"Printed headers and text: {text}"
|
| 572 |
+
|
| 573 |
+
# --- End of MCP Matchmaker Tools ---
|
| 574 |
+
|
| 575 |
+
# --- Start of Combined Gradio App using gr.Blocks ---
|
| 576 |
+
|
| 577 |
+
with gr.Blocks() as demo:
|
| 578 |
+
# Define a hidden component that will be used to expose the function as a tool for the agent
|
| 579 |
+
# This is a workaround to make a function available as an API endpoint without a visible UI element.
|
| 580 |
+
agent_tool_input = gr.Textbox(visible=False)
|
| 581 |
+
agent_tool_output = gr.JSON(visible=False)
|
| 582 |
+
|
| 583 |
+
# This makes the function available to the MCP server as a tool named 'provide_link_to_upload_profile_picture'
|
| 584 |
+
agent_tool_input.change(
|
| 585 |
+
fn=provide_link_to_upload_profile_picture,
|
| 586 |
+
inputs=None,
|
| 587 |
+
outputs=agent_tool_output,
|
| 588 |
+
api_name="provide_link_to_upload_profile_picture"
|
| 589 |
+
)
|
| 590 |
+
|
| 591 |
+
with gr.Tabs():
|
| 592 |
+
with gr.TabItem("New Profile"):
|
| 593 |
+
gr.Interface(
|
| 594 |
+
fn=new_profile,
|
| 595 |
+
inputs=None,
|
| 596 |
+
outputs=gr.JSON(label="Questionnaire, IDs, and Instructions"),
|
| 597 |
+
title="Profile Questionnaire",
|
| 598 |
+
description="Generates a new user profile and returns a questionnaire, Profile ID, and Auth ID. (Agent-triggered MCP tool)"
|
| 599 |
+
)
|
| 600 |
+
|
| 601 |
+
with gr.TabItem("Update Profile Answers"):
|
| 602 |
+
gr.Interface(
|
| 603 |
+
fn=update_profile_answers,
|
| 604 |
+
inputs=[gr.Textbox(label="Answers Payload (JSON string)", lines=5)],
|
| 605 |
+
outputs=gr.JSON(label="Update Status"),
|
| 606 |
+
title="Update Profile Answers",
|
| 607 |
+
description="Updates a user's profile with answers to the questionnaire. Requires X-Auth-ID header."
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
with gr.TabItem("Get Matches"):
|
| 611 |
+
gr.Interface(
|
| 612 |
+
fn=get_matches,
|
| 613 |
+
inputs=None,
|
| 614 |
+
outputs=gr.JSON(label="Matches"),
|
| 615 |
+
title="Get Matches",
|
| 616 |
+
description="Gets a list of potential matches for the authenticated user. Requires X-Auth-ID header."
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
with gr.TabItem("Get Profile"):
|
| 620 |
+
gr.Interface(
|
| 621 |
+
fn=get_profile,
|
| 622 |
+
inputs=[gr.Textbox(label="Profile ID to Get")],
|
| 623 |
+
outputs=gr.JSON(label="Profile Details"),
|
| 624 |
+
title="Get Profile",
|
| 625 |
+
description="Gets the full profile for a given Profile ID. Requires X-Auth-ID header."
|
| 626 |
+
)
|
| 627 |
+
|
| 628 |
+
with gr.TabItem("Send Message"):
|
| 629 |
+
gr.Interface(
|
| 630 |
+
fn=send_message,
|
| 631 |
+
inputs=[
|
| 632 |
+
gr.Textbox(label="Receiver Profile ID"),
|
| 633 |
+
gr.Textbox(label="Message Content", lines=5)
|
| 634 |
+
],
|
| 635 |
+
outputs=gr.JSON(label="Send Status"),
|
| 636 |
+
title="Send Message",
|
| 637 |
+
description="Sends a message to another user. Requires X-Auth-ID header. ($1.00 placeholder cost)"
|
| 638 |
+
)
|
| 639 |
+
|
| 640 |
+
with gr.TabItem("Get Messages"):
|
| 641 |
+
gr.Interface(
|
| 642 |
+
fn=get_messages,
|
| 643 |
+
inputs=None,
|
| 644 |
+
outputs=gr.JSON(label="Messages"),
|
| 645 |
+
title="Get Messages",
|
| 646 |
+
description="Gets all messages sent or received by the authenticated user. Requires X-Auth-ID header."
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
with gr.TabItem("Upload Profile Picture"):
|
| 650 |
+
gr.Markdown("## Upload Your Profile Picture")
|
| 651 |
+
gr.Markdown("Drag and drop your image below, then click 'Upload'.")
|
| 652 |
+
|
| 653 |
+
auth_state = gr.State()
|
| 654 |
+
base_url_state = gr.State()
|
| 655 |
+
|
| 656 |
+
with gr.Row():
|
| 657 |
+
image_input = gr.Image(type="pil", label="Upload Profile Picture")
|
| 658 |
+
json_output = gr.JSON(label="Upload Status")
|
| 659 |
+
|
| 660 |
+
upload_button = gr.Button("Upload")
|
| 661 |
+
|
| 662 |
+
upload_button.click(
|
| 663 |
+
fn=upload_profile_picture,
|
| 664 |
+
inputs=[auth_state, image_input, base_url_state],
|
| 665 |
+
outputs=json_output
|
| 666 |
+
)
|
| 667 |
+
|
| 668 |
+
with gr.TabItem("Headers Debug"):
|
| 669 |
+
gr.Interface(
|
| 670 |
+
fn=print_headers,
|
| 671 |
+
inputs=gr.Textbox(label="Input Text"),
|
| 672 |
+
outputs=gr.Textbox(label="Output Text (same as input)"),
|
| 673 |
+
title="Headers Debug",
|
| 674 |
+
description="Prints request headers to the console. Check your terminal."
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
# On page load, run process_auth_id_from_url, get the auth_id from the URL,
|
| 678 |
+
# and store it in the state. No visible components are updated to prevent re-rendering bugs.
|
| 679 |
+
demo.load(
|
| 680 |
+
fn=process_auth_id_from_url,
|
| 681 |
+
inputs=None,
|
| 682 |
+
outputs=[auth_state, base_url_state]
|
| 683 |
+
)
|
| 684 |
+
|
| 685 |
+
if __name__ == "__main__":
|
| 686 |
+
# Construct the absolute path to the allowed directory, relative to this script's location
|
| 687 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 688 |
+
allowed_images_path = os.path.join(base_dir, "data", "profile_images")
|
| 689 |
+
|
| 690 |
+
demo.launch(mcp_server=True, allowed_paths=[allowed_images_path], share=True)
|
data/messages.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"message_id": "7880956d-244d-4c2b-9b7c-bd6ecb371ed4",
|
| 4 |
+
"sender_profile_id": "user_y38k71ox",
|
| 5 |
+
"receiver_profile_id": "user_p9zqwxv8",
|
| 6 |
+
"content": "Hey there!",
|
| 7 |
+
"timestamp": "2025-06-06T04:56:31.804886+00:00",
|
| 8 |
+
"read_status": true
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"message_id": "5809af1e-8a57-41d1-9319-233c7e66a26a",
|
| 12 |
+
"sender_profile_id": "user_p9zqwxv8",
|
| 13 |
+
"receiver_profile_id": "user_y38k71ox",
|
| 14 |
+
"content": "Hey you!",
|
| 15 |
+
"timestamp": "2025-06-06T05:12:46.611177+00:00",
|
| 16 |
+
"read_status": true
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"message_id": "440f9c41-626f-4b45-89b5-1f59351d190f",
|
| 20 |
+
"sender_profile_id": "user_y38k71ox",
|
| 21 |
+
"receiver_profile_id": "user_p9zqwxv8",
|
| 22 |
+
"content": "How are you doing?",
|
| 23 |
+
"timestamp": "2025-06-06T06:44:02.018941+00:00",
|
| 24 |
+
"read_status": true,
|
| 25 |
+
"payment_confirmation_id": "not_provided"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"message_id": "e5cb586f-f664-40d5-812b-527a700477f0",
|
| 29 |
+
"sender_profile_id": "user_y38k71ox",
|
| 30 |
+
"receiver_profile_id": "user_t2ysgmnp",
|
| 31 |
+
"content": "Hey Charlie",
|
| 32 |
+
"timestamp": "2025-06-06T07:10:46.319365+00:00",
|
| 33 |
+
"read_status": true
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"message_id": "d4cda280-99bc-4960-9419-0daf71c48c78",
|
| 37 |
+
"sender_profile_id": "user_y38k71ox",
|
| 38 |
+
"receiver_profile_id": "user_t2ysgmnp",
|
| 39 |
+
"content": "Hey Charlie",
|
| 40 |
+
"timestamp": "2025-06-06T07:13:28.067145+00:00",
|
| 41 |
+
"read_status": true
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"message_id": "f5ae4191-ddc9-430d-a5a9-0498e3dee627",
|
| 45 |
+
"sender_profile_id": "user_y38k71ox",
|
| 46 |
+
"receiver_profile_id": "user_t2ysgmnp",
|
| 47 |
+
"content": "Hey Charlie",
|
| 48 |
+
"timestamp": "2025-06-06T07:16:10.584026+00:00",
|
| 49 |
+
"read_status": true
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"message_id": "3d565b74-a0b2-4006-b501-b83637a180f4",
|
| 53 |
+
"sender_profile_id": "user_1p6vx64b",
|
| 54 |
+
"receiver_profile_id": "user_t2ysgmnp",
|
| 55 |
+
"content": "Hey Charlie",
|
| 56 |
+
"timestamp": "2025-06-06T07:18:28.078347+00:00",
|
| 57 |
+
"read_status": true
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"message_id": "5200e674-e5f6-499a-a19b-01d0a6e6b7c7",
|
| 61 |
+
"sender_profile_id": "user_7d5earg8",
|
| 62 |
+
"receiver_profile_id": "user_p9zqwxv8",
|
| 63 |
+
"content": "Hey there",
|
| 64 |
+
"timestamp": "2025-06-06T09:28:13.285402+00:00",
|
| 65 |
+
"read_status": false
|
| 66 |
+
}
|
| 67 |
+
]
|
data/profile_images/default.jpg
ADDED
|
data/profile_images/user_lmrkaaim.jpg
ADDED
|
data/profile_images/user_lmrkaaim.png
ADDED
|
data/profiles.json
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"user_6ngnjagx": {
|
| 3 |
+
"profile_id": "user_6ngnjagx",
|
| 4 |
+
"auth_id": "1edfeebd-e9f8-416e-afc4-4c1698932ac0",
|
| 5 |
+
"created_at": "2025-06-05T16:26:32.384174Z",
|
| 6 |
+
"updated_at": "2025-06-05T16:26:32.384174Z",
|
| 7 |
+
"name": "",
|
| 8 |
+
"gender": "",
|
| 9 |
+
"profile_summary": "",
|
| 10 |
+
"profile_image_filename": null,
|
| 11 |
+
"answers": {}
|
| 12 |
+
},
|
| 13 |
+
"user_y38k71ox": {
|
| 14 |
+
"profile_id": "user_y38k71ox",
|
| 15 |
+
"auth_id": "cc175293-a9eb-4db0-ad2f-f3ba7a0dab25",
|
| 16 |
+
"created_at": "2025-06-05T16:49:53.871722+00:00",
|
| 17 |
+
"updated_at": "2025-06-05T17:00:46.839293+00:00",
|
| 18 |
+
"name": "User",
|
| 19 |
+
"gender": "Man",
|
| 20 |
+
"profile_summary": "I am very cool, please date me.",
|
| 21 |
+
"profile_image_filename": "default.jpg",
|
| 22 |
+
"answers": {
|
| 23 |
+
"q_hobby": "My main hobby is coding.",
|
| 24 |
+
"q_looking_for": "I'm looking for someone who is as cool as me.",
|
| 25 |
+
"q_vibe": "My general vibe is... coding.",
|
| 26 |
+
"q_gender_preference": "Women"
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
"user_h7bafk3d": {
|
| 30 |
+
"profile_id": "user_h7bafk3d",
|
| 31 |
+
"auth_id": "a4f8b9e6-7d6a-4c3b-8e1f-9a0c1b2d3e4f",
|
| 32 |
+
"created_at": "2025-06-05T17:10:00.000000+00:00",
|
| 33 |
+
"updated_at": "2025-06-05T17:10:00.000000+00:00",
|
| 34 |
+
"name": "Alex",
|
| 35 |
+
"gender": "Non-binary",
|
| 36 |
+
"profile_summary": "Loves hiking and exploring new cafes.",
|
| 37 |
+
"profile_image_filename": "default.jpg",
|
| 38 |
+
"answers": {
|
| 39 |
+
"q_hobby": "Hiking, photography, trying new recipes.",
|
| 40 |
+
"q_looking_for": "Someone adventurous and kind.",
|
| 41 |
+
"q_vibe": "Outdoorsy and creative.",
|
| 42 |
+
"q_gender_preference": "All"
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
"user_p9zqwxv8": {
|
| 46 |
+
"profile_id": "user_p9zqwxv8",
|
| 47 |
+
"auth_id": "b5c7d8f9-8e7b-4d2c-9f0a-1b2c3d4e5f6g",
|
| 48 |
+
"created_at": "2025-06-05T17:11:00.000000+00:00",
|
| 49 |
+
"updated_at": "2025-06-05T17:11:00.000000+00:00",
|
| 50 |
+
"name": "Bella",
|
| 51 |
+
"gender": "Woman",
|
| 52 |
+
"profile_summary": "Bookworm, artist, and enjoys quiet nights in.",
|
| 53 |
+
"profile_image_filename": "default.jpg",
|
| 54 |
+
"answers": {
|
| 55 |
+
"q_hobby": "Reading, painting, watching classic movies.",
|
| 56 |
+
"q_looking_for": "A thoughtful person to share deep conversations with.",
|
| 57 |
+
"q_vibe": "Cozy and artistic.",
|
| 58 |
+
"q_gender_preference": "Men"
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"user_t2ysgmnp": {
|
| 62 |
+
"profile_id": "user_t2ysgmnp",
|
| 63 |
+
"auth_id": "c6d8e9g0-9f8c-5e1d-a01b-2c3d4e5f6g7h",
|
| 64 |
+
"created_at": "2025-06-05T17:12:00.000000+00:00",
|
| 65 |
+
"updated_at": "2025-06-05T17:12:00.000000+00:00",
|
| 66 |
+
"name": "Charlie",
|
| 67 |
+
"gender": "Man",
|
| 68 |
+
"profile_summary": "Tech enthusiast and loves a good board game.",
|
| 69 |
+
"profile_image_filename": "default.jpg",
|
| 70 |
+
"answers": {
|
| 71 |
+
"q_hobby": "Building PCs, board games, sci-fi novels.",
|
| 72 |
+
"q_looking_for": "A partner-in-crime for game nights and tech talks.",
|
| 73 |
+
"q_vibe": "Geeky and friendly.",
|
| 74 |
+
"q_gender_preference": "All"
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"user_1p6vx64b": {
|
| 78 |
+
"profile_id": "user_1p6vx64b",
|
| 79 |
+
"auth_id": "1a824149-5ddc-4489-8856-0c774042b538",
|
| 80 |
+
"created_at": "2025-06-06T07:00:38.209630+00:00",
|
| 81 |
+
"updated_at": "2025-06-06T07:04:31.734665+00:00",
|
| 82 |
+
"name": "Chris",
|
| 83 |
+
"gender": "Man",
|
| 84 |
+
"profile_summary": "I am a fun guy.",
|
| 85 |
+
"profile_image_filename": "default.jpg",
|
| 86 |
+
"answers": {
|
| 87 |
+
"q_hobby": "I like to play video games.",
|
| 88 |
+
"q_looking_for": "Someone to play video games with.",
|
| 89 |
+
"q_vibe": "Homebody."
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
"user_aki9die1": {
|
| 93 |
+
"profile_id": "user_aki9die1",
|
| 94 |
+
"auth_id": "ef896a9c-01dd-4113-b996-820fd4919c17",
|
| 95 |
+
"created_at": "2025-06-06T08:08:36.438861+00:00",
|
| 96 |
+
"updated_at": "2025-06-06T08:27:00.737853+00:00",
|
| 97 |
+
"name": "Bob",
|
| 98 |
+
"gender": "Man",
|
| 99 |
+
"profile_summary": "Bob is a builder",
|
| 100 |
+
"profile_image_filename": "default.jpg",
|
| 101 |
+
"answers": {
|
| 102 |
+
"q_hobby": "Building things.",
|
| 103 |
+
"q_looking_for": "Someone who also likes to build things.",
|
| 104 |
+
"q_vibe": "Intellectual",
|
| 105 |
+
"q_gender_preference": "Women"
|
| 106 |
+
}
|
| 107 |
+
},
|
| 108 |
+
"user_7d5earg8": {
|
| 109 |
+
"profile_id": "user_7d5earg8",
|
| 110 |
+
"auth_id": "da3c732e-fb8e-4148-94e3-7e645dcc4043",
|
| 111 |
+
"created_at": "2025-06-06T09:14:22.028571+00:00",
|
| 112 |
+
"updated_at": "2025-06-06T09:18:42.887759+00:00",
|
| 113 |
+
"name": "Daniel",
|
| 114 |
+
"gender": "Man",
|
| 115 |
+
"profile_summary": "I'm Daniel the developer.",
|
| 116 |
+
"profile_image_filename": "default.jpg",
|
| 117 |
+
"answers": {
|
| 118 |
+
"q_gender_preference": "Women",
|
| 119 |
+
"q_hobby": "Development.",
|
| 120 |
+
"q_looking_for": "A woman interested in developing with me.",
|
| 121 |
+
"q_vibe": "Developer."
|
| 122 |
+
}
|
| 123 |
+
},
|
| 124 |
+
"user_lmrkaaim": {
|
| 125 |
+
"profile_id": "user_lmrkaaim",
|
| 126 |
+
"auth_id": "387d3ca8-e658-4bd2-8002-8669145707e0",
|
| 127 |
+
"created_at": "2025-06-07T10:31:50.273501+00:00",
|
| 128 |
+
"updated_at": "2025-06-08T08:54:55.660737+00:00",
|
| 129 |
+
"name": "Karin",
|
| 130 |
+
"gender": "Woman",
|
| 131 |
+
"profile_summary": "I'm a homebody, very intellectual but I like to dance",
|
| 132 |
+
"profile_image_filename": "user_lmrkaaim.png",
|
| 133 |
+
"answers": {
|
| 134 |
+
"q_gender_preference": "Men",
|
| 135 |
+
"q_hobby": "dancing, reading, and AI research",
|
| 136 |
+
"q_looking_for": "someone with similar interests but also someone who is totally jacked while not being a total jerk or meathead",
|
| 137 |
+
"q_vibe": "quiet conservative but open minded and fiesty"
|
| 138 |
+
}
|
| 139 |
+
},
|
| 140 |
+
"user_vqisd3q2": {
|
| 141 |
+
"profile_id": "user_vqisd3q2",
|
| 142 |
+
"auth_id": "7cd0f843-6b01-4157-b68b-5e980089e264",
|
| 143 |
+
"created_at": "2025-06-08T06:26:33.634037+00:00",
|
| 144 |
+
"updated_at": "2025-06-08T06:26:33.634037+00:00",
|
| 145 |
+
"name": "",
|
| 146 |
+
"gender": "",
|
| 147 |
+
"profile_summary": "",
|
| 148 |
+
"profile_image_filename": "default.jpg",
|
| 149 |
+
"answers": {}
|
| 150 |
+
}
|
| 151 |
+
}
|
data/questionnaire.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"title": "MCP Matchmaker Profile Questionnaire",
|
| 3 |
+
"questions": [
|
| 4 |
+
{ "id": "q_name", "text": "What is your name?", "type": "text", "purpose": "metadata", "maps_to_field": "name" },
|
| 5 |
+
{ "id": "q_gender", "text": "What is your gender? Please choose one from: Man, Woman, Non-binary", "type": "text", "purpose": "metadata", "maps_to_field": "gender" },
|
| 6 |
+
{ "id": "q_gender_preference", "text": "What gender are you interested in matching with? Please choose one from: Men, Women, All", "type": "text", "purpose": "matchmaking" },
|
| 7 |
+
{ "id": "q_profile_summary", "text": "Write a brief introduction for your profile (1-2 sentences):", "type": "text", "purpose": "metadata", "maps_to_field": "profile_summary" },
|
| 8 |
+
{ "id": "q_hobby", "text": "What are your main hobbies or interests?", "type": "text", "purpose": "matchmaking" },
|
| 9 |
+
{ "id": "q_looking_for", "text": "What are you looking for in a match?", "type": "long_text", "purpose": "matchmaking" },
|
| 10 |
+
{ "id": "q_vibe", "text": "Describe your general vibe (e.g., adventurous, homebody, intellectual)?", "type": "text", "purpose": "matchmaking" }
|
| 11 |
+
]
|
| 12 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
gradio[mcp]
|