Skip to content

Commit 010a0e5

Browse files
authored
Merge pull request #1 from qiuethan/zac/interview
feat: extract Header component and add Sign Up button
2 parents 6c1f3d1 + 58e5e86 commit 010a0e5

File tree

17 files changed

+876
-63
lines changed

17 files changed

+876
-63
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,8 @@ Thumbs.db
4444

4545
# Output folders
4646
output/
47+
48+
.gemini/
49+
gha-creds-*.json
50+
GEMINI.md
51+
plans/

api/app/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from .models import AvatarCreate, AvatarUpdate, ApiResponse, AgentRequest, AgentResponse, GenerateAvatarResponse
2121
from . import database as db
22+
from . import onboarding
2223

2324
# Add image_gen to path for importing pipeline
2425
IMAGE_GEN_PATH = Path(__file__).parent.parent.parent / "image_gen"
@@ -58,6 +59,8 @@ async def lifespan(app: FastAPI):
5859
allow_headers=["*"],
5960
)
6061

62+
app.include_router(onboarding.router)
63+
6164
# ============================================================================
6265
# ROUTES
6366
# ============================================================================

api/app/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,25 @@ class GenerateAvatarResponse(BaseModel):
6868
message: Optional[str] = None
6969
error: Optional[str] = None
7070
images: Optional[dict[str, str]] = None # {front: url, back: url, left: url, right: url}
71+
72+
73+
class OnboardingChatRequest(BaseModel):
74+
message: str
75+
conversation_id: Optional[str] = None
76+
77+
78+
class OnboardingChatResponse(BaseModel):
79+
response: str
80+
conversation_id: str
81+
status: str = "active" # "active" or "completed"
82+
83+
84+
class OnboardingStateResponse(BaseModel):
85+
history: list[dict]
86+
conversation_id: Optional[str]
87+
is_completed: bool
88+
89+
90+
class OnboardingCompleteRequest(BaseModel):
91+
conversation_id: str
92+

api/app/onboarding.py

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import json
2+
import os
3+
from pathlib import Path
4+
from typing import Optional, List
5+
from fastapi import APIRouter, HTTPException, Depends, Request
6+
from pydantic import BaseModel
7+
from openai import OpenAI
8+
9+
from .models import OnboardingChatRequest, OnboardingChatResponse, OnboardingStateResponse, OnboardingCompleteRequest
10+
from .supabase_client import supabase
11+
12+
router = APIRouter(prefix="/onboarding", tags=["onboarding"])
13+
14+
# Load questions
15+
QUESTIONS_PATH = Path(__file__).parent.parent / "data" / "questions.json"
16+
try:
17+
with open(QUESTIONS_PATH, "r") as f:
18+
QUESTIONS = json.load(f)
19+
except Exception as e:
20+
print(f"Error loading questions: {e}")
21+
QUESTIONS = []
22+
23+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
24+
if not OPENROUTER_API_KEY:
25+
print("Warning: OPENROUTER_API_KEY not set. Onboarding chat will fail.")
26+
27+
client = None
28+
if OPENROUTER_API_KEY:
29+
try:
30+
client = OpenAI(
31+
base_url="https://openrouter.ai/api/v1",
32+
api_key=OPENROUTER_API_KEY,
33+
)
34+
except Exception as e:
35+
print(f"Failed to init OpenAI/OpenRouter client: {e}")
36+
37+
MODEL_NAME = "xiaomi/mimo-v2-flash:free"
38+
39+
async def get_current_user(request: Request):
40+
auth_header = request.headers.get("Authorization")
41+
if not auth_header:
42+
raise HTTPException(status_code=401, detail="Missing Authorization header")
43+
44+
token = auth_header.replace("Bearer ", "")
45+
try:
46+
user_response = supabase.auth.get_user(token)
47+
if not user_response.user:
48+
raise HTTPException(status_code=401, detail="Invalid token")
49+
return user_response.user
50+
except Exception as e:
51+
print(f"Auth error: {e}")
52+
raise HTTPException(status_code=401, detail="Invalid authentication")
53+
54+
@router.get("/state", response_model=OnboardingStateResponse)
55+
async def get_onboarding_state(user = Depends(get_current_user)):
56+
# Find active onboarding conversation
57+
response = supabase.table("conversations")\
58+
.select("*")\
59+
.eq("participant_a", user.id)\
60+
.eq("is_onboarding", True)\
61+
.order("created_at", desc=True)\
62+
.limit(1)\
63+
.execute()
64+
65+
if response.data:
66+
conv = response.data[0]
67+
return {
68+
"history": conv.get("transcript", []),
69+
"conversation_id": conv["id"],
70+
"is_completed": False
71+
}
72+
73+
return {
74+
"history": [],
75+
"conversation_id": None,
76+
"is_completed": False
77+
}
78+
79+
@router.post("/chat", response_model=OnboardingChatResponse)
80+
async def chat_onboarding(req: OnboardingChatRequest, user = Depends(get_current_user)):
81+
if not client:
82+
raise HTTPException(status_code=503, detail="AI service unavailable")
83+
84+
conversation_id = req.conversation_id
85+
transcript = []
86+
87+
# 1. Retrieve or Create Conversation
88+
if conversation_id:
89+
res = supabase.table("conversations").select("*").eq("id", conversation_id).single().execute()
90+
if not res.data:
91+
raise HTTPException(status_code=404, detail="Conversation not found")
92+
if res.data["participant_a"] != user.id:
93+
raise HTTPException(status_code=403, detail="Not your conversation")
94+
transcript = res.data.get("transcript", [])
95+
else:
96+
res = supabase.table("conversations")\
97+
.select("*")\
98+
.eq("participant_a", user.id)\
99+
.eq("is_onboarding", True)\
100+
.order("created_at", desc=True)\
101+
.limit(1)\
102+
.execute()
103+
104+
if res.data:
105+
conv = res.data[0]
106+
conversation_id = conv["id"]
107+
transcript = conv.get("transcript", [])
108+
else:
109+
new_conv = supabase.table("conversations").insert({
110+
"participant_a": user.id,
111+
"is_onboarding": True,
112+
"transcript": []
113+
}).execute()
114+
conversation_id = new_conv.data[0]["id"]
115+
transcript = []
116+
117+
# 2. Append User Message
118+
if req.message != "[START]":
119+
user_msg_obj = {"role": "user", "content": req.message}
120+
transcript.append(user_msg_obj)
121+
122+
# 3. Construct LLM Prompt
123+
system_instruction = f"""
124+
You are a friendly, casual interviewer for a virtual world called 'Avatar World'.
125+
Your goal is to welcome the new user and get to know them by getting answers to the following questions.
126+
127+
REQUIRED QUESTIONS:
128+
{json.dumps(QUESTIONS, indent=2)}
129+
130+
INSTRUCTIONS:
131+
1. Ask these questions ONE BY ONE. Do not dump them all at once.
132+
2. Maintain a conversational flow. React to their answers (e.g., "Oh, that's cool!", "I love pizza too!").
133+
3. You can change the order if it flows better, but ensure all are covered eventually.
134+
4. Keep your responses concise (1-2 sentences usually).
135+
5. If the user asks you questions, answer briefly and steer back to the interview.
136+
6. When you are satisfied that you have answers to ALL specific questions (or the user has declined to answer enough times),
137+
you MUST signal completion by calling the 'end_interview' tool.
138+
139+
Current Progress:
140+
Review the transcript below. See which questions have been answered. Ask the next one.
141+
"""
142+
143+
messages = [{"role": "system", "content": system_instruction}]
144+
# Append transcript messages
145+
# Ensure roles are 'user' or 'assistant'. OpenRouter/OpenAI expects 'assistant' not 'model'.
146+
for msg in transcript:
147+
# My transcript uses 'assistant' internally, so it's fine.
148+
messages.append(msg)
149+
150+
# Define the tool
151+
tools = [
152+
{
153+
"type": "function",
154+
"function": {
155+
"name": "end_interview",
156+
"description": "Call this when all questions have been answered to finish the onboarding.",
157+
"parameters": {
158+
"type": "object",
159+
"properties": {},
160+
"required": []
161+
}
162+
}
163+
}
164+
]
165+
166+
try:
167+
completion = client.chat.completions.create(
168+
model=MODEL_NAME,
169+
messages=messages,
170+
tools=tools,
171+
tool_choice="auto"
172+
)
173+
except Exception as e:
174+
print(f"OpenRouter API Error: {e}")
175+
return OnboardingChatResponse(
176+
response="I'm having a bit of trouble connecting to my brain right now. Can you say that again?",
177+
conversation_id=conversation_id,
178+
status="active"
179+
)
180+
181+
# 5. Process Response
182+
ai_text = ""
183+
status = "active"
184+
185+
response_message = completion.choices[0].message
186+
187+
# Check for tool calls
188+
if response_message.tool_calls:
189+
# Check if it's the right tool
190+
for tool_call in response_message.tool_calls:
191+
if tool_call.function.name == "end_interview":
192+
status = "completed"
193+
ai_text = "Thanks! That's everything I needed. Enjoy the world!"
194+
break
195+
196+
if status != "completed":
197+
ai_text = response_message.content or "Hmm, I didn't catch that."
198+
199+
# 6. Save AI Response
200+
ai_msg_obj = {"role": "assistant", "content": ai_text}
201+
transcript.append(ai_msg_obj)
202+
203+
supabase.table("conversations").update({
204+
"transcript": transcript,
205+
"updated_at": "now()"
206+
}).eq("id", conversation_id).execute()
207+
208+
return OnboardingChatResponse(
209+
response=ai_text,
210+
conversation_id=conversation_id,
211+
status=status
212+
)
213+
214+
@router.post("/complete")
215+
async def complete_onboarding(req: OnboardingCompleteRequest, user = Depends(get_current_user)):
216+
if not client:
217+
raise HTTPException(status_code=503, detail="AI service unavailable")
218+
219+
# 1. Fetch Transcript
220+
res = supabase.table("conversations").select("*").eq("id", req.conversation_id).single().execute()
221+
if not res.data:
222+
raise HTTPException(status_code=404, detail="Conversation not found")
223+
224+
conversation = res.data
225+
transcript = conversation.get("transcript", [])
226+
227+
# 2. Generate Memory Summary
228+
summary_prompt = f"""
229+
Analyze the following onboarding transcript for user '{user.id}'.
230+
231+
Transcript:
232+
{json.dumps(transcript)}
233+
234+
Task:
235+
1. Extract key facts (Name, Job, Hobbies, etc.).
236+
2. Analyze their speaking style (Formal/Casual, Emoji usage, Length).
237+
3. Create a concise summary paragraph.
238+
239+
Output JSON:
240+
{{
241+
"facts": {{ ... }},
242+
"style": "...",
243+
"summary": "..."
244+
}}
245+
"""
246+
247+
try:
248+
completion = client.chat.completions.create(
249+
model=MODEL_NAME,
250+
messages=[
251+
{"role": "system", "content": "You are a helpful assistant that outputs JSON."},
252+
{"role": "user", "content": summary_prompt}
253+
],
254+
response_format={"type": "json_object"}
255+
)
256+
content = completion.choices[0].message.content
257+
summary_data = json.loads(content)
258+
summary_text = summary_data.get("summary", "New user joined the world.")
259+
except Exception as e:
260+
print(f"Summary generation failed: {e}")
261+
summary_text = "User completed onboarding."
262+
263+
# 3. Save Memory
264+
supabase.table("memories").insert({
265+
"conversation_id": req.conversation_id,
266+
"owner_id": user.id,
267+
"partner_id": None,
268+
"summary": summary_text,
269+
"conversation_score": 10
270+
}).execute()
271+
272+
# 4. Update User Metadata
273+
try:
274+
supabase.auth.admin.update_user_by_id(
275+
user.id,
276+
{"user_metadata": {"onboarding_completed": True}}
277+
)
278+
except Exception as e:
279+
print(f"Failed to update user metadata: {e}")
280+
# Note: If service key is invalid/missing rights, this fails.
281+
raise HTTPException(status_code=500, detail="Failed to finalize onboarding.")
282+
283+
return {"ok": True}

api/app/supabase_client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
from typing import Optional
3+
from dotenv import load_dotenv
4+
from supabase import create_client, Client
5+
6+
load_dotenv()
7+
8+
SUPABASE_URL = os.getenv("SUPABASE_URL")
9+
SUPABASE_SERVICE_KEY = os.getenv("SUPABASE_SERVICE_KEY")
10+
11+
supabase: Optional[Client] = None
12+
13+
if SUPABASE_URL and SUPABASE_SERVICE_KEY:
14+
try:
15+
supabase = create_client(SUPABASE_URL, SUPABASE_SERVICE_KEY)
16+
except Exception as e:
17+
print(f"Failed to initialize Supabase client: {e}")
18+
else:
19+
print("Warning: SUPABASE_URL or SUPABASE_SERVICE_KEY not set.")

api/data/questions.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
"What is your name?",
3+
"Where are you from?",
4+
"What do you do for work or study?",
5+
"What are your main hobbies or interests?",
6+
"What kind of music do you like?",
7+
"Do you have a favorite movie or TV show?",
8+
"What brings you to this virtual world today?",
9+
"If you could have any superpower, what would it be?",
10+
"What's your favorite food?",
11+
"Is there anything else you'd like to share about yourself?"
12+
]

0 commit comments

Comments
 (0)