Skip to content

cynthia-song5/phia-hack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Phia

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.


How it works — end to end

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

Features

  • 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

Frontend

Built with Next.js 16 (App Router, Turbopack), React 19, TypeScript, and Tailwind CSS v4.

App structure

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

Key components

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.

Data flow

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')

Virtual try-on route (/api/try-on)

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 and persistence

  • Auth: Supabase Google OAuth. Server components call createClient() from @/lib/supabase/server; client components use @/lib/supabase/client. Every protected route checks supabase.auth.getUser() server-side and redirects to /login if unauthenticated.
  • Sessions: Each prompt creates a row in the sessions table. The session ID becomes the URL (/chat/[id]).
  • Messages: Every user and assistant message is persisted to messages via persistMessage().
  • Saved items: Toggling the heart on any pin writes to saved_items with the full product data as JSONB.

Backend

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.

Agent graph

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)

State schema

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]

Node breakdown

intent_classifier

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.

planner

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.

executor

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.

synthesizer

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).

capability_responder

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.

Tools

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

Observability

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.

Mock mode

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

Tech stack

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

Project structure

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()

Getting started

Prerequisites

  • Node.js 20+, Python 3.11+, uv
  • Supabase project, OpenAI API key, Replicate API token

Backend

cd backend
cp .env.example .env      # fill in keys
uv sync
uv run uvicorn app.main:app --reload --port 8000

Frontend

cd frontend
cp .env.example .env.local
npm install
npm run dev

Open http://localhost:3000.

Environment variables

Backend (.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=

Database

Run supabase-setup.sql against your Supabase project to create the required tables (sessions, messages, saved_items, profiles).


License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors