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
- discord
- python
Log in or sign up for Devpost to join the conversation.