-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi_client.py
More file actions
276 lines (235 loc) · 13 KB
/
Copy pathapi_client.py
File metadata and controls
276 lines (235 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""API client for YC Startup School cofounder matching."""
import json
from playwright.sync_api import Page
from rich.console import Console
console = Console()
class YCCofounderAPI:
"""Client for YC Startup School cofounder matching API."""
def __init__(self):
self.next_candidate_url = 'https://www.startupschool.org/cofounder-matching/candidate/next'
self.graphql_url = 'https://www.startupschool.org/graphql'
def get_next_candidate(self, page: Page) -> dict:
"""
Navigate to next candidate and extract data from GraphQL response.
Returns candidate data dict or None if no more candidates.
"""
try:
# Set up response listener for GraphQL
graphql_data = []
def handle_response(response):
if 'graphql' in response.url:
try:
data = response.json()
graphql_data.append(data)
except:
pass
page.on('response', handle_response)
# Navigate to next candidate
console.print(f"[dim]Navigating to: {self.next_candidate_url}[/dim]")
page.goto(self.next_candidate_url, wait_until='networkidle', timeout=30000)
# Wait a bit for GraphQL response
page.wait_for_timeout(2000)
# Remove listener
page.remove_listener('response', handle_response)
# Find the cofounder matching data
for data in graphql_data:
if 'data' in data and 'cofounderMatching' in data.get('data', {}):
cf_data = data['data']['cofounderMatching']
# Check if we have a candidate (could be None when no more candidates)
if cf_data and 'candidate' in cf_data and cf_data['candidate']:
# Pass both my profile and the candidate data
my_profile = cf_data.get('profile', {})
return self._parse_candidate(cf_data, my_profile)
# No more candidates available
console.print("[dim]No more candidates in queue[/dim]")
return None
except Exception as e:
console.print(f"[red]Error getting next candidate: {e}[/red]")
import traceback
traceback.print_exc()
return None
def _parse_candidate(self, cf_data: dict, my_profile: dict = None) -> dict:
"""Parse candidate data from GraphQL response into structured format."""
if not cf_data:
return None
candidate = cf_data.get('candidate', {})
if not candidate:
return None
user = candidate.get('user', {}) or {}
company = candidate.get('company', {}) or {}
# Extract my info for context
my_user = (my_profile.get('user', {}) or {}) if my_profile else {}
# Build structured profile
profile = {
'slug': candidate.get('slug', ''),
'name': user.get('name', 'Unknown'),
'first_name': user.get('firstName', ''),
'location': user.get('location', ''),
'country': user.get('country', ''),
'region': user.get('region', ''),
'age': user.get('age'),
'avatar_url': user.get('avatarUrl', ''),
'linkedin': user.get('linkedin', ''),
'is_technical': user.get('isTechnical', False),
# Profile details
'intro': candidate.get('intro', ''),
'timing': candidate.get('timing', ''),
'has_idea': candidate.get('hasIdea', ''),
'responsibilities': candidate.get('responsibilities', []),
'interests': candidate.get('interests', []),
'impressive_thing': user.get('impressiveThing', ''),
'education': user.get('education', ''),
'employment': user.get('employment', ''),
'life_story': candidate.get('lifeStory', ''),
'free_time': candidate.get('freeTime', ''),
# What they're looking for
'req_free_text': candidate.get('reqFreeText', ''),
'cf_is_technical': candidate.get('cfIsTechnical'),
'cf_responsibilities': candidate.get('cfResponsibilities', []),
'cf_has_idea': candidate.get('cfHasIdea'),
# Company info
'company_name': company.get('name', ''),
'company_description': company.get('description', ''),
'company_progress': company.get('progress', ''),
# Current cofounder info (if looking for 3rd)
'has_cf': candidate.get('hasCf', False),
'current_cf_linkedin': candidate.get('currentCfLinkedin', ''),
'current_cf_technical': candidate.get('currentCfTechnical'),
'why_looking_for_third': candidate.get('whyLookingForThirdCf', ''),
# Location preferences (for filtering by LLM)
'cf_location': candidate.get('cfLocation', ''), # 'geographic', 'region', 'anywhere'
'cf_location_importance': candidate.get('cfLocationImportance', ''),
'cf_location_km_range': candidate.get('cfLocationKmRange'), # km range they want
# My info (for LLM context)
'my_location': my_user.get('location', ''),
'my_country': my_user.get('country', ''),
'my_region': my_user.get('region', ''),
# Metadata
'invites_remaining': cf_data.get('invitesRemaining', 0),
'conversation_slug': candidate.get('conversationSlug'),
}
# Build location string with country context
location_parts = [profile['location']]
if profile['country'] and profile['country'] not in profile['location']:
location_parts.append(profile['country'])
location_str = ', '.join([p for p in location_parts if p]) or 'Unknown'
# Build full text for evaluation with clear field explanations
full_text_parts = [
f"Name: {profile['name']}",
f"Location: {location_str}",
f"Age: {profile['age']}" if profile['age'] else "",
"",
"--- THEIR PROFILE FLAGS (interpret with nuance) ---",
f"isTechnical: {profile['is_technical']} (YC checkbox - they self-identify as technical, but may be product/ops person who checked 'technical')",
f"Timing: {profile['timing']}",
f"Has Idea: {profile['has_idea']}",
"",
"--- THEIR INTRO & WHAT THEY DO ---",
f"{profile['intro']}" if profile['intro'] else "(No intro provided)",
f"\nResponsibilities they handle: {', '.join(profile['responsibilities'])}" if profile['responsibilities'] else "",
f"Interests: {', '.join(profile['interests'])}" if profile['interests'] else "",
"",
"--- WHAT THEY'RE LOOKING FOR IN A CO-FOUNDER ---",
f"cfIsTechnical: {profile['cf_is_technical']} (Do they WANT a technical cofounder? True = GOOD for me!)" if profile['cf_is_technical'] is not None else "cfIsTechnical: Not specified",
f"cfHasIdea: {profile['cf_has_idea']}" if profile['cf_has_idea'] is not None else "",
f"Responsibilities they want cofounder to handle: {', '.join(profile['cf_responsibilities'])}" if profile['cf_responsibilities'] else "",
f"\nIdeal cofounder description:\n{profile['req_free_text']}" if profile['req_free_text'] else "",
f"\nOther notes:\n{candidate.get('other', '')}" if candidate.get('other') else "",
"",
"--- THEIR BACKGROUND ---",
f"Impressive Achievement:\n{profile['impressive_thing']}" if profile['impressive_thing'] else "",
f"\nEducation:\n{profile['education']}" if profile['education'] else "",
f"\nEmployment:\n{profile['employment']}" if profile['employment'] else "",
f"\nLife Story:\n{profile['life_story']}" if profile['life_story'] else "",
f"\nFree Time:\n{profile['free_time']}" if profile['free_time'] else "",
]
if profile['company_name']:
full_text_parts.extend([
f"\nCompany: {profile['company_name']}",
f"Description: {profile['company_description']}",
f"Progress: {profile['company_progress']}",
])
if profile['has_cf']:
full_text_parts.append(f"\nNote: Already has a cofounder. Looking for 3rd because: {profile['why_looking_for_third']}")
# Add location preference context for LLM
full_text_parts.extend([
"",
"--- LOCATION COMPATIBILITY CHECK ---",
f"Their location: {location_str}",
f"My location: {profile['my_location']} ({profile['my_country']}, {profile['my_region']})",
f"Their cofounder location preference: cfLocation={profile['cf_location']}, cfLocationImportance={profile['cf_location_importance']}, cfLocationKmRange={profile['cf_location_km_range']}",
"(If they have cfLocation='geographic' with a small cfLocationKmRange and they're in a different country from me, they explicitly want someone local - this is a hard mismatch, score 1-2)",
])
profile['full_text'] = '\n'.join([p for p in full_text_parts if p])
# Build skills list for invitation template
skills = []
if profile['responsibilities']:
skills.extend(profile['responsibilities'])
if profile['interests']:
skills.extend(profile['interests'][:3]) # Top 3 interests
profile['skills'] = skills
# Bio for invitation - prioritize startup info over employment
if profile['company_name'] and profile['company_description']:
profile['bio'] = f"Building {profile['company_name']}: {profile['company_description'][:150]}"
elif profile['intro']:
profile['bio'] = profile['intro'][:200]
else:
profile['bio'] = profile['req_free_text'][:200] if profile['req_free_text'] else ''
return profile
def send_invitation(self, page: Page, candidate_slug: str, message: str) -> bool:
"""
Send invitation message to candidate.
The page should already be showing the candidate's profile.
Returns True if successful, False otherwise.
"""
try:
# Page is already showing the candidate - no need to navigate
page.wait_for_timeout(500)
# Fill the message in the textarea
console.print("[dim]Filling message...[/dim]")
message_input = page.locator('textarea').first
try:
message_input.wait_for(state='visible', timeout=3000)
message_input.fill(message)
console.print("[dim]Message filled[/dim]")
page.wait_for_timeout(300)
except Exception as e:
console.print(f"[yellow]Could not find/fill textarea: {e}[/yellow]")
# Find and click "Invite to connect" button
console.print("[dim]Clicking invite button...[/dim]")
invite_button = None
button_selectors = [
'button:has-text("Invite to connect")',
'button:has-text("Invite to Connect")',
'button:has-text("Send Invite")',
'button:has-text("Invite")',
]
for selector in button_selectors:
try:
btn = page.locator(selector)
if btn.count() > 0 and btn.first.is_visible():
invite_button = btn.first
console.print(f"[dim]Found: {selector}[/dim]")
break
except:
continue
if invite_button:
# Try JavaScript click as fallback (works better with React)
try:
invite_button.click(timeout=2000)
except:
console.print("[dim]Normal click failed, trying JS click...[/dim]")
page.evaluate('document.querySelector("button").click()')
page.wait_for_timeout(1500)
console.print("[green]✓ Invitation sent[/green]")
return True
else:
console.print("[yellow]Could not find invite button - please click manually[/yellow]")
console.print("[dim]Waiting 10 seconds for manual action...[/dim]")
page.wait_for_timeout(10000)
return True
except Exception as e:
console.print(f"[red]Error: {e}[/red]")
console.print("[yellow]Please complete the invite manually[/yellow]")
page.wait_for_timeout(5000)
return False