title: Secundus Dermis emoji: πŸ‘— colorFrom: purple colorTo: pink sdk: docker app_port: 7860

pinned: false

Secundus Dermis

An AI-powered fashion storefront built as a customer support agent playground. The fictional brand is a prop β€” the real subject is demonstrating how a conversational AI agent handles the full surface area of e-commerce support: product discovery, visual search, editorial content, and multi-turn dialogue.

The site is also designed as a target environment for external AI agents. Agents such as NanoClaw and OpenClaw can connect via WebSocket, read a machine-readable manifest, and autonomously drive browsing, search, and support scenarios for automated evaluation. See AGENT_WEBSOCKET_PLAN.md.


Quick Start

Prerequisites

Tool Version
Python 3.11+
Node.js 18+
uv latest (pip install uv)
Gemini API key aistudio.google.com
Kaggle API token kaggle.com/settings

Backend

cd backend
cp .env.example .env          # fill in GEMINI_API_KEY, KAGGLE_API_TOKEN
uv sync                       # install dependencies
uv run python api.py          # starts on http://localhost:8000

The DeepFashion Multimodal dataset (~650 MB, 12,278 items) downloads automatically on first run via the Kaggle API if data/labels_front.csv is absent. Interactive API docs at http://localhost:8000/docs.

Frontend

cd frontend
npm install
npm run dev                   # starts on http://localhost:5173

The Vite dev server proxies /api/* β†’ localhost:8000.

Environment Variables

# backend/.env
GEMINI_API_KEY=...                               # required β€” agent LLM + VLM image search
KAGGLE_API_TOKEN=KGAT_...                        # required β€” dataset auto-download
ADMIN_KEY=change-me                              # protects POST /journal
AGENT_MODEL=gemini-3.1-pro-preview-customtools
VLM_MODEL=gemini-3.1-pro-preview
IMAGES_DIR=./data/selected_images
JOURNAL_DIR=./journal

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Browser β€” React + Vite SPA                              β”‚
β”‚                                                          β”‚
β”‚  /           About β€” what this project is                β”‚
β”‚  /shop       Infinite-scroll catalog + sidebar filters   β”‚
β”‚  /product/:id  Product detail                            β”‚
β”‚  /blog       Editorial journal                           β”‚
β”‚  ChatWidget  Floating AI chat, persists across pages     β”‚
β”‚  Header      Navbar with live AI-controlled search bar   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚  REST  (Vite proxy β†’ :8000)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  FastAPI  (backend/api.py)                               β”‚
β”‚                                                          β”‚
β”‚  /chat ────── Google ADK Runner ──── Gemini LLM          β”‚
β”‚                     β”‚                                    β”‚
β”‚               search_by_keywords ←── in-memory catalog  β”‚
β”‚               search_journal     ←── markdown files      β”‚
β”‚               get_catalog_stats                          β”‚
β”‚               get_product_categories                     β”‚
β”‚                                                          β”‚
β”‚  /search/image ── Gemini VLM β†’ keywords β†’ keyword_search β”‚
β”‚                              └─ colour histogram re-rank β”‚
β”‚                                                          β”‚
β”‚  /catalog/*  ── pure in-memory filtering (zero API cost) β”‚
β”‚  /journal/*  ── markdown files served as JSON            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Text search flow

User message β†’ ADK Runner β†’ Gemini LLM
                                β”‚
              calls search_by_keywords(
                keywords="floral dress",
                gender="WOMEN",
                category="Dresses"
              )
                                β”‚
              in-memory keyword scan of 12,278 descriptions
                                β”‚
              LLM composes reply with product list
                                β”‚
        ← reply + products + filter{ gender, category, query }
                                β”‚
        ChatWidget mirrors filter into ShopContext
        β†’ sidebar highlights Women's + Dresses
        β†’ navbar search populates
        β†’ shop grid filters to match

Image search flow

Uploaded image
  Step 1 β€” Gemini VLM β†’ "dress, floral, blue, cotton, casual"   PRIMARY SIGNAL
  Step 2 β€” keyword_search() for each VLM term β†’ ~200 candidates  PRIMARY RETRIEVAL
  Step 3 β€” colour histogram cosine similarity β†’ re-rank           VISUAL ORDERING
  Return top N sorted by visual similarity

The histogram only orders what keyword search already found β€” it never retrieves on its own.


Tech Stack Decisions

Layer Choice Why
Agent framework Google ADK (google-adk) Native Gemini tool-calling; InMemorySessionService maintains multi-turn context; Runner handles the full conversation loop with zero boilerplate
LLM / VLM Gemini (gemini-3.1-pro-preview-customtools) Function-calling for agent tools; same API key for both chat agent and image description β€” single dependency, single quota
Product search In-memory str in str keyword scan 12k descriptions fit in RAM; sub-millisecond queries; zero API cost per search β€” ideal for a high-query demo
Image similarity 96-dim RGB histogram + cosine similarity No model inference per query; computed once per image and cached lazily; sufficient colour-based visual ranking for a demo
API FastAPI Async, Pydantic validation, OpenAPI auto-docs, UploadFile for image handling
Frontend React + Vite + TypeScript HMR for fast iteration; React Router SPA preserves the chat widget across page navigations without remounting; TypeScript catches API contract mismatches early
Global state React Context (ShopContext) Sidebar filters and search query shared across all routes β€” lets the AI chat widget update the shop grid and navbar search without prop drilling
Routing React Router nested routes + <Outlet> ShopLayout defines the sidebar once; /shop and /product/:id are nested inside it β€” sidebar never unmounts or re-renders on navigation
Styling Plain CSS modules per component Full control over the editorial boutique aesthetic; no framework purge config; variables in variables.css for theming
Dataset DeepFashion Multimodal (Kaggle) 12,278 labelled images with captions across 16 categories for MEN + WOMEN; auto-downloaded via KAGGLE_API_TOKEN on first server start
Journal Markdown + YAML frontmatter Version-controlled, human-editable, agent-searchable; new posts via POST /journal or the /blog/new UI

Agent API Reference

Base URL: http://localhost:8000 All bodies are JSON. Interactive docs at /docs.


POST /chat

Conversational agent. Routes through Google ADK, calls tools as needed, returns grounded reply.

Request

{
  "message": "Show me women's dresses under $80",
  "history": [
    { "role": "user",      "content": "Hi" },
    { "role": "assistant", "content": "Hello! How can I help you today?" }
  ],
  "session_id": "550e8400-e29b-41d4-a716-446655440000"
}
Field Type Required Notes
message string βœ“ The user's message
history array β€” Recent turns { role, content } for context. The ADK session also maintains server-side history independently.
session_id string β€” UUID identifying the conversation. Defaults to "default". Use a stable per-user UUID for real context persistence.

Response

{
  "reply": "I found 6 dresses for you! Here are some highlights...",
  "products": [
    {
      "product_id": "id_00001234",
      "product_name": "Women's Floral Cotton Dress",
      "description": "A light floral cotton dress with short sleeves...",
      "gender": "WOMEN",
      "category": "Dresses",
      "price": 64.99,
      "similarity": 0.0,
      "image_url": "/images/WOMEN-Dresses-id_00001234-01_1_front.jpg"
    }
  ],
  "intent": "text_search",
  "filter": {
    "gender": "WOMEN",
    "category": "Dresses",
    "query": "floral"
  }
}
Field Type Description
reply string Markdown-formatted agent response
products array Products retrieved by the agent's search_by_keywords tool call. Empty for chitchat.
intent "text_search" \ "chitchat"
filter object \ null

Agent tools:

Tool Signature Description
search_by_keywords (keywords, gender?, category?, max_price?, n_results?) Primary product retrieval. Called for all product queries.
get_catalog_stats () Total product count, all categories, all genders.
get_product_categories () Categories grouped by gender.
search_journal (query) Searches editorial articles. Returns title, excerpt, and markdown link /blog/{slug}.

POST /search/text

Direct keyword search, bypassing the conversational agent. Useful for programmatic or evaluation use.

Request

{
  "query": "leather jacket",
  "n_results": 8,
  "gender": "MEN",
  "category": "Jackets_Vests",
  "max_price": 150.0
}

Response

{
  "results": [ /* Product objects */ ],
  "query": "leather jacket",
  "total": 4
}

POST /search/image

Find visually similar products by uploading a photo.

Request β€” multipart/form-data

Field Type Description
file file JPEG, PNG, or WebP
n_results int Max results (default 8)
gender string Optional filter
category string Optional filter
curl -X POST http://localhost:8000/search/image \
  -F "file=@jacket.jpg" \
  -F "n_results=6"

Response

{
  "results": [ /* Product objects with similarity scores */ ],
  "query": "[image: jacket.jpg] β†’ jacket, leather, brown, casual",
  "total": 6
}

GET /catalog/browse

Paginated catalog with filtering. Zero API cost.

Param Type Description
offset int Pagination offset (default 0)
limit int Page size, max 48 (default 24)
gender string "MEN" or "WOMEN"
category string Exact category name
q string Keyword filter on name + description

Response

{ "products": [...], "offset": 0, "limit": 24, "total": 347 }

GET /catalog/product/{product_id}

Single product by ID.


GET /catalog/stats

{
  "total_products": 12278,
  "categories": ["Blouses_Shirts", "Cardigans", "Denim", "..."],
  "genders": ["MEN", "WOMEN"]
}

GET /journal

List all posts (no body). Params: category, featured=true.

GET /journal/{slug}

Full post including markdown body.

POST /journal

Publish a new post. Requires X-Admin-Key header.

curl -X POST http://localhost:8000/journal \
  -H "Content-Type: application/json" \
  -H "X-Admin-Key: your-admin-key" \
  -d '{
    "title": "The Case for Natural Fabrics",
    "excerpt": "Why linen and cotton outperform synthetics year-round.",
    "author": "Editorial",
    "date": "2026-03-28",
    "read_time": "4 min",
    "category": "Style",
    "tags": ["fabric", "basics"],
    "featured": false,
    "image": "",
    "body": "# The Case for Natural Fabrics\n\n..."
  }'

GET /health

{ "status": "healthy", "catalog_size": 12278, "search_mode": "keyword + VLM histogram" }

Categories Reference

Men's: Tees_Tanks Β· Shirts_Polos Β· Sweaters Β· Sweatshirts_Hoodies Β· Suiting Β· Denim Β· Pants Β· Shorts Β· Jackets_Vests

Women's: Tees_Tanks Β· Graphic_Tees Β· Blouses_Shirts Β· Cardigans Β· Denim Β· Pants Β· Shorts Β· Skirts Β· Leggings Β· Dresses Β· Rompers_Jumpsuits Β· Jackets_Coats


Project Structure

SecundusDermis/
β”‚
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ api.py                    # FastAPI app β€” all endpoints
β”‚   β”œβ”€β”€ download_data.py          # Kaggle dataset auto-download
β”‚   β”œβ”€β”€ agent/
β”‚   β”‚   β”œβ”€β”€ agent.py              # ADK Agent + system prompt
β”‚   β”‚   └── tools.py              # search_by_keywords, search_journal, stats
β”‚   β”œβ”€β”€ journal/                  # Markdown editorial articles
β”‚   β”œβ”€β”€ data/                     # Downloaded dataset (gitignored)
β”‚   └── pyproject.toml
β”‚
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ main.tsx              # App root β€” Router, ShopProvider, ShopLayout
β”‚   β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”‚   └── shop-context.tsx  # Global filter + search state (React Context)
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ Header.tsx        # Navbar β€” live AI-controlled search input
β”‚   β”‚   β”‚   β”œβ”€β”€ ShopSidebar.tsx   # Persistent filter sidebar (defined once in ShopLayout)
β”‚   β”‚   β”‚   β”œβ”€β”€ ChatWidget.tsx    # Floating AI chat β€” mirrors filters to sidebar
β”‚   β”‚   β”‚   └── Footer.tsx
β”‚   β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”‚   β”œβ”€β”€ About.tsx         # Root β€” describes the AI playground
β”‚   β”‚   β”‚   β”œβ”€β”€ Shop.tsx          # Infinite-scroll product grid
β”‚   β”‚   β”‚   β”œβ”€β”€ Product.tsx       # Product detail
β”‚   β”‚   β”‚   β”œβ”€β”€ Blog.tsx / BlogPost.tsx / NewBlog.tsx
β”‚   β”‚   β”‚   └── FAQ.tsx / Contact.tsx
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   └── fashionApi.ts     # Typed API client
β”‚   β”‚   └── styles/               # Per-component CSS
β”‚   └── vite.config.ts
β”‚
β”œβ”€β”€ AGENT_WEBSOCKET_PLAN.md       # WebSocket agent integration design
β”œβ”€β”€ IMAGE_SEARCH_PLAN.md          # Image search architecture + roadmap
β”œβ”€β”€ SESSION_PLAN.md               # Session persistence roadmap
└── NEXT_STEPS.md                 # Overall project roadmap

No Real Commerce

No products can be purchased. No orders are placed. No personal data is stored. This is a technical demonstration of AI-assisted customer support patterns.

Dataset: DeepFashion Multimodal by silverstone1903 on Kaggle β€” 12,278 items, MEN + WOMEN, 16 categories. AI: Google Gemini via Google ADK.

Built With

Share this project:

Updates