FitFindr is a multi-tool AI agent that helps users search for secondhand clothing items, style them with an existing wardrobe, and generate a short shareable outfit caption.
The project demonstrates how an agent can use multiple tools, maintain state across tool calls, handle errors, and adapt when a tool returns no useful result.
A user enters a natural language request such as:
vintage graphic tee under $30
FitFindr then:
- Searches a mock secondhand listings dataset.
- Selects the best matching item.
- Suggests an outfit using the user's wardrobe.
- Creates a short social media-style fit card.
The agent does not simply call every tool no matter what happens. It checks the result of each step before deciding whether to continue.
Install dependencies:
pip install -r requirements.txtCreate a .env file in the project root:
GROQ_API_KEY=your_key_here
Run the app:
python app.pyThen open the local URL shown in the terminal, usually:
http://127.0.0.1:7860
OutfitAgent/
├── app.py
├── agent.py
├── tools.py
├── data/
│ ├── listings.json
│ └── wardrobe_schema.json
├── utils/
│ └── data_loader.py
├── tests/
│ ├── conftest.py
│ └── test_tools.py
├── planning.md
├── requirements.txt
└── README.md
Purpose: Searches the mock secondhand listings dataset for matching items.
Inputs:
description(str): Search keywords, such as"vintage graphic tee".size(str | None): Optional size filter. IfNone, size filtering is skipped.max_price(float | None): Optional maximum price. IfNone, price filtering is skipped.
Output:
Returns a list[dict] of matching listings sorted by relevance.
Each listing dictionary includes:
idtitledescriptioncategorystyle_tagssizeconditionpricecolorsbrandplatform
Failure handling:
If no listings match, the tool returns an empty list []. It does not crash.
Purpose: Suggests one or two complete outfit ideas using the selected thrifted item and the user's wardrobe.
Inputs:
new_item(dict): The selected listing fromsearch_listings.wardrobe(dict): The user's wardrobe dictionary with an"items"list.
Output:
Returns a non-empty string containing outfit suggestions.
Failure handling:
If the wardrobe is empty, the tool asks the LLM for general styling advice instead of crashing or returning nothing.
Purpose: Creates a short, casual caption for the complete outfit.
Inputs:
outfit(str): The outfit suggestion fromsuggest_outfit.new_item(dict): The selected listing.
Output:
Returns a 2–4 sentence caption that mentions the item, price, platform, and outfit vibe.
Failure handling:
If outfit is empty or missing, the tool returns a descriptive error message string instead of raising an exception.
I added retry logic with fallback.
If the first search returns no results and the user provided a size filter, the agent retries the search without the size filter. If the fallback search returns results, the agent continues with the best fallback item and tells the user that the size filter was removed.
Example:
graphic tee size M under $30
If no exact size M graphic tee is found, the agent retries:
search_listings("graphic tee", None, 30.0)If fallback results exist, the UI displays a message like:
No results found for size M. Retried without the size filter.
If the fallback also fails, the agent returns an error message.
The main planning loop is implemented in run_agent() inside agent.py.
The loop works like this:
- Create a new session dictionary.
- Parse the user query into
description,size, andmax_price. - Call
search_listings. - If no results are found and size was provided, retry without the size filter.
- If still no results are found, set
session["error"]and return early. - Select the top search result.
- Store the selected item in the session.
- Call
suggest_outfit. - Store the outfit suggestion in the session.
- Call
create_fit_card. - Store the fit card in the session.
- Return the completed session.
The planning loop changes behavior depending on what the tools return. It does not continue to outfit generation if no listing is found.
The agent uses a session dictionary to store all data from one interaction.
Example session structure:
{
"query": query,
"parsed": {},
"search_results": [],
"selected_item": None,
"wardrobe": wardrobe,
"outfit_suggestion": None,
"fit_card": None,
"error": None,
"fallback_message": None
}The session allows information from one tool to flow into the next:
search_listings
↓
selected_item
↓
suggest_outfit
↓
outfit_suggestion
↓
create_fit_card
↓
fit_card
This means the user does not need to repeat information between steps.
Failure mode: no listings match.
Response: the agent retries without size if size was provided. If there are still no results, the agent returns:
No matching listings found. Try broadening your search or increasing your budget.
Failure mode: wardrobe is empty.
Response: the tool still returns general styling advice for the selected item.
Failure mode: outfit input is empty.
Response: the tool returns:
Could not create a fit card because no outfit suggestion was provided.
Tests are written with pytest in tests/test_tools.py.
Run tests with:
pytest tests/test_tools.py -vThe tests check:
search_listingsreturns results for valid queries.search_listingsreturns[]for impossible queries.- Price filtering works.
- Size filtering works.
suggest_outfitreturns a string with the example wardrobe.suggest_outfitreturns a string with an empty wardrobe.create_fit_cardreturns a caption for valid input.create_fit_cardhandles an empty outfit string.
All tests passed before connecting the tools into the full agent loop.
User query:
vintage graphic tee under $30
The agent parses:
{
"description": "vintage graphic tee",
"size": None,
"max_price": 30.0
}The agent calls:
search_listings("vintage graphic tee", None, 30.0)A top listing is selected, such as:
Y2K Baby Tee — Butterfly Print
Then the agent calls:
suggest_outfit(selected_item, wardrobe)The outfit suggestion might pair the tee with baggy jeans, chunky sneakers, and a denim jacket.
Then the agent calls:
create_fit_card(outfit_suggestion, selected_item)The final output includes:
- The selected listing.
- The outfit idea.
- The fit card caption.
User query:
designer ballgown size XXS under $5
The agent searches the dataset and finds no matching listings. It may retry without the size filter, but if no listings match after fallback, it returns:
No matching listings found. Try broadening your search or increasing your budget.
The agent does not call suggest_outfit or create_fit_card when no item is found.
I used ChatGPT to help with implementation and documentation.
I gave ChatGPT the starter code for tools.py, including the function signatures and TODO comments. I asked for an implementation of search_listings that used load_listings(), filtered by size and price, scored keyword matches, and returned an empty list when no results were found.
Before using the code, I checked that it matched the assignment requirements and then verified it with pytest tests.
I gave ChatGPT the agent.py starter code, the session dictionary structure, and my planning loop description. I asked for a run_agent() implementation that parsed the query, called each tool in order, stored results in session state, and returned early if search failed.
I reviewed the code to make sure the agent did not call suggest_outfit or create_fit_card when search_listings returned no results.
I used ChatGPT to help turn my implementation details into clear planning.md and README.md sections. I reviewed and edited the content so it matched my actual code and testing results.
The demo video shows:
- A complete successful interaction using all three tools.
- The selected listing flowing into the outfit suggestion.
- The outfit suggestion flowing into the fit card.
- A no-results or fallback scenario.
- The app running through the Gradio interface.
This project helped me understand that an AI agent is more than a single LLM call. The important part is the planning loop: deciding what tool to call, checking the result, storing state, and deciding whether to continue or stop.
Testing each tool individually made the full agent easier to debug. Once the tools passed pytest, most remaining issues were in the planning loop or UI formatting instead of the tool logic itself.