AI-powered personal styling agent. Describe an occasion in one sentence. Get a complete, shoppable outfit back — styled to your taste, grounded in your social aesthetic, and ready to try on.
Phia replaces the fragmented loop of Pinterest → ChatGPT → retailer → repeat with a single interface. It reads your public Pinterest boards and Instagram profile to understand your aesthetic, runs a multi-step AI planning pipeline to curate full outfit recommendations, and lets you virtually try on any piece on your own photo.
User types a prompt
│
▼
[Next.js frontend]
Creates a Supabase session → calls POST /v1/chat_completions
│
▼
[FastAPI backend — LangGraph agent graph]
intent_classifier → planner → executor → synthesizer
│
▼
Returns: response_message + recommendations[]
│
▼
[Frontend]
Groups products into Outfit objects → renders masonry grid
User can chat, expand outfits, save items, or trigger try-on
│
▼
[Virtual try-on]
User uploads photo → /api/try-on → Replicate IDM-VTON → result
- Natural-language styling — describe any occasion, budget, or aesthetic; the agent handles the rest
- Social style profile — connect Pinterest and Instagram once; Phia reads your boards and posts to anchor every recommendation to your actual taste
- Outfit-first UX — results are complete named looks (not flat item lists), each with occasion, vibe, season, price total, and match score
- Virtual try-on — upload a full-body photo and see any piece from any outfit rendered on you via Replicate IDM-VTON
- Conversational refinement — follow-up messages re-run the agent and update the grid live
- Gift mode — describe someone else and style an outfit for them
- Saved board — bookmark items across sessions
Built with Next.js 16 (App Router, Turbopack), React 19, TypeScript, and Tailwind CSS v4.
src/app/
├── login/ # Google OAuth entry point
├── (protected)/
│ ├── chat/
│ │ ├── home.tsx # Discover page — prompt composer
│ │ └── [id]/
│ │ ├── page.tsx # Server component — loads session + profile from Supabase
│ │ └── recommendations.tsx # Client component — agent calls, grid, chat
│ ├── saved/ # Saved items board
│ ├── settings/ # Account + style onboarding link
│ └── onboarding/
│ └── wizard.tsx # Style profile wizard (social handles, photo upload)
└── api/
└── try-on/ # Next.js route handler → Replicate IDM-VTON
recommendations.tsx — the core client component. Manages all state: outfit grid, chat messages, expanded item, saved IDs, try-on open/close. On mount, fires chatCompletions() to the backend, maps the returned BackendProduct[] into Pin[] via productToPin(), then groups them into Outfit[] via pinsToOutfits(). On follow-up chat messages, re-runs the same flow and replaces the grid.
OutfitCard.tsx — renders a complete outfit as a masonry card. Automatically switches between 1/2/3/4+ piece image collage layouts. Shows match score badge, piece count, occasion, vibe, total price, and tags.
OutfitExpanded.tsx — full outfit detail view with stats row, description, tags, and a piece grid. Includes a "Try On Outfit" button that opens a piece picker modal — user selects which item they want to try on, then TryOnModal opens for that piece.
TryOnModal.tsx — the virtual try-on UI. Drag-and-drop or click to upload a full-body photo (JPEG/PNG/WebP). Caches the photo in localStorage so it persists across sessions. On submit, sends the photo as a FormData multipart upload to /api/try-on and displays the AI-generated composite.
ChatPanel.tsx — persistent chat interface. Renders as a fixed sidebar on desktop (lg:flex) and a bottom sheet on mobile. Shares the same chatProps between both via a single messages / onSend interface.
Supabase session (server)
└─▶ page.tsx (RSC) — fetches session + profile
└─▶ recommendations.tsx (client)
├─▶ chatCompletions() → /v1/chat_completions (backend)
│ └─▶ BackendProduct[] → productToPin() → Pin[]
│ └─▶ pinsToOutfits() → Outfit[]
│ └─▶ OutfitCard grid
└─▶ toggleSave() → supabase.from('saved_items')
Accepts a FormData POST with a person file and garmentImageUrl string. Converts the person photo buffer to a base64 data URI (no cloud storage required) and passes it directly to Replicate's IDM-VTON model (cuuupid/idm-vton) alongside the garment image URL. Returns { resultUrl } — the AI-generated composite image.
- Auth: Supabase Google OAuth. Server components call
createClient()from@/lib/supabase/server; client components use@/lib/supabase/client. Every protected route checkssupabase.auth.getUser()server-side and redirects to/loginif unauthenticated. - Sessions: Each prompt creates a row in the
sessionstable. The session ID becomes the URL (/chat/[id]). - Messages: Every user and assistant message is persisted to
messagesviapersistMessage(). - Saved items: Toggling the heart on any pin writes to
saved_itemswith the full product data as JSONB.
A FastAPI service built around a five-node LangGraph agent graph. Every chat request enters as a typed AgentState and flows through the graph — each node reads fields from state, writes its outputs back, and passes control forward via explicit edges.
POST /v1/chat_completions
│
▼
┌────────────────────────┐
│ intent_classifier │ GPT-4o-mini · structured output · temp=0
└───────────┬────────────┘
│
┌──────┴──────┐
▼ ▼
┌─────────┐ ┌────────────────────────┐
│capability│ │ planner │ GPT-5.2 · reasoning_effort="low" · temp=0
│responder │ └────────────┬───────────┘
└────┬─────┘ │
│ ┌──────▼──────────────────┐
│ │ executor │ asyncio · topological dispatch
│ └──────┬──────────────────┘
│ │
│ ┌──────▼──────────────────┐
│ │ synthesizer │ deterministic · no LLM
│ └──────┬──────────────────┘
│ │
└───────────► AgentState (END)
All inter-node communication flows through a single AgentState TypedDict — no global state, no side channels:
class AgentState(TypedDict):
# Input
user_message: str
user_id: str
session_id: str
is_gift_mode: bool
gift_recipient_description: Optional[str]
style_profile_context: Optional[dict]
# Set by intent_classifier
intent: str # "styling" | "capability_question"
# Set by planner
plan: Optional[AgentPlan] # dependency-aware tool call graph
# Set by executor
tool_results: dict[str, Any] # step_id → tool output
# Set by synthesizer
response_message: str
recommendations: list[dict]
sources: list[dict]
error: Optional[str]Uses ChatOpenAI.with_structured_output(_IntentResult) where _IntentResult is a Pydantic model with a Literal["styling", "capability_question"] field. The LLM response is guaranteed to be one of the two valid values — no parsing logic, no fallback. The conditional edge in graph.py reads state["intent"] and routes to either planner or capability_responder.
The most important node. Uses GPT-5.2 with reasoning_effort="low" — the model reasons internally about occasion, formality, season, setting, and required clothing slots before emitting output. The system prompt instructs it to produce a dependency-aware JSON plan where each tool call has a unique tool_id (step_1, step_2, ...). When a tool needs output from a prior step, the planner declares it in referenced_parameters as a dotted path:
{
"tool_id": "step_3",
"tool_name": "analyze_style_images",
"referenced_parameters": {
"image_urls": "step_2.image_urls"
}
}If the model returns markdown-wrapped JSON or fails Pydantic validation, the node appends the error back as a correction message and retries up to MAX_RETRIES times before raising.
Pure async orchestration — no LLM calls. Reads plan.tool_calls, builds a dependency graph with _dependency_edges(), and runs a topological dispatch loop:
while remaining:
ready = [c for c in calls if depends_on[c.tool_id] <= completed]
batch = await asyncio.gather(*[_execute_single_tool(c, tool_results) for c in ready])At each iteration, every call whose dependencies are already satisfied is dispatched in the same asyncio.gather batch — so independent tools like fetch_style_profile and fetch_pinterest run concurrently. When a step depends on a prior result, _resolve_referenced_parameters walks the dotted path against tool_results to inject the value at runtime.
If settings.agent_mock_tools is enabled, failed or unknown tool calls return deterministic stubs instead of crashing — the graph always completes cleanly regardless of external API availability.
Deterministic — no LLM, no latency. Walks tool_results, collects all products arrays from search_products calls, merges in pricing from get_phia_pricing keyed by product_id, trims to the top 12, and returns response_message, recommendations, and sources (the list of tools called and why, for frontend attribution).
Short-circuits the full pipeline for meta-questions ("what can you do?", "do you support Instagram?"). Answers directly from a static CAPABILITY_MANIFEST — zero tool calls, one fast LLM call.
| Tool | Description |
|---|---|
fetch_style_profile |
Retrieves user's saved style preferences from DB |
fetch_pinterest |
Scrapes a public Pinterest board or profile (Apify) |
fetch_instagram_profile |
Fetches recent post image URLs from an IG handle (Brightdata) |
fetch_instagram_posts |
Fetches images from specific IG post URLs |
analyze_style_images |
Extracts style signals (tags, palette, aesthetic) from image URLs |
search_products |
Searches fashion products by slot and query (Hasdata) |
get_phia_pricing |
Compares retail and resale prices across a product list |
Every node is wrapped by timed_agent_node, which records wall-clock latency per node and logs it as structured output (agent_step step=planner duration_ms=1823.40). When LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are set, the Langfuse callback handler traces every OpenAI call — prompts, completions, token counts, and latency — automatically, with zero instrumentation code in the nodes themselves.
Three feature flags in config.py stub all external API calls with deterministic responses. The full graph still runs — planner generates a real plan, executor dispatches it, synthesizer ranks results — so the entire pipeline is testable offline.
USE_MOCK_PRODUCTS=true
USE_MOCK_PINTEREST=true
USE_MOCK_INSTAGRAM=true
| Layer | Stack |
|---|---|
| Frontend | Next.js 16 (App Router, Turbopack), React 19, TypeScript, Tailwind CSS v4 |
| Backend | FastAPI, Python 3.11+, Uvicorn |
| Agent orchestration | LangGraph 0.2, LangChain 0.3 |
| LLMs | GPT-4o-mini (classifier, capability responder), GPT-5.2 with extended reasoning (planner) |
| Auth & database | Supabase (PostgreSQL, Google OAuth) |
| Virtual try-on | Replicate cuuupid/idm-vton |
| Product search | Hasdata |
| Social scraping | Apify (Pinterest), Brightdata (Instagram) |
| Observability | Langfuse |
phia-hack/
├── backend/
│ ├── app/
│ │ ├── agent/
│ │ │ ├── graph.py # LangGraph state machine
│ │ │ ├── state.py # AgentState TypedDict
│ │ │ ├── node_timing.py # Wall-clock wrapper for all nodes
│ │ │ ├── prompts.py # Planner system + user prompts
│ │ │ ├── capabilities.py # Static capability manifest
│ │ │ └── nodes/
│ │ │ ├── intent_classifier.py
│ │ │ ├── planner.py
│ │ │ ├── executor.py
│ │ │ ├── synthesizer.py
│ │ │ └── capability_responder.py
│ │ ├── routers/
│ │ │ └── chat.py # POST /v1/chat_completions
│ │ ├── tools/ # search_products, fetch_pinterest, etc.
│ │ ├── models/
│ │ │ └── plan.py # AgentPlan, ToolCall Pydantic models
│ │ └── config.py
│ └── pyproject.toml
│
└── frontend/
└── src/
├── app/
│ ├── (protected)/
│ │ ├── chat/ # Discover home + [id] conversation
│ │ ├── saved/ # Saved items board
│ │ ├── settings/ # Account settings
│ │ └── onboarding/ # Style profile wizard
│ ├── api/try-on/ # Replicate IDM-VTON route handler
│ └── login/
├── components/
│ ├── OutfitCard.tsx # Masonry card with piece collage
│ ├── OutfitExpanded.tsx # Full outfit detail + try-on picker
│ ├── PinCard.tsx / PinExpanded.tsx
│ ├── TryOnModal.tsx # File upload + Replicate try-on
│ ├── ChatPanel.tsx # Desktop sidebar / mobile bottom sheet
│ └── AppChrome.tsx # Header, tabs, wordmark
└── lib/
├── outfits.ts # Outfit interface + mock data
├── mock-pins.ts # Pin interface
└── api.ts # chatCompletions(), productToPin()
- Node.js 20+, Python 3.11+, uv
- Supabase project, OpenAI API key, Replicate API token
cd backend
cp .env.example .env # fill in keys
uv sync
uv run uvicorn app.main:app --reload --port 8000cd frontend
cp .env.example .env.local
npm install
npm run devBackend (.env)
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o-mini
HASDATA_API_KEY=
APIFY_API_KEY=
BRIGHTDATA_API_KEY=
LANGFUSE_PUBLIC_KEY=
LANGFUSE_SECRET_KEY=
USE_MOCK_PRODUCTS=true
USE_MOCK_PINTEREST=true
USE_MOCK_INSTAGRAM=true
Frontend (.env.local)
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_PUBLIC_API_URL=http://localhost:8000
REPLICATE_API_TOKEN=
Run supabase-setup.sql against your Supabase project to create the required tables (sessions, messages, saved_items, profiles).
MIT