# Project export: Hermes - Shopping Assistant

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: AI/ML powered shopping assistant empowering buyers and local businesses to connect.
- Devpost: https://devpost.com/software/hermes-shopping-assistant
- GitHub: https://github.com/Hezy4/BerkAiHackathon
- Video: https://www.youtube.com/embed/HGyCbWeQfHk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Henry Boes (58 commits), Pooja (27 commits), Monterey (17 commits)

## Devpost submission (written by the team)

### Overview

💡

### Inspiration

According to NYC Small Business services, almost 8000 small businesses closed in NYC alone last year. Much of this is attributed to the rise and dominance of online commerce sites such as Temu, Ebay, and Amazon. At the same time, increased prices on goods squeeze consumers dry, making a task as simple as survival near impossible for many Americans. Hermes aims to hit both these stones with one winged boot. 🛠️

### What it does

Hermes is a shopping assistant powered by Google Gemini and Groq. Hermes allows users to locate local businesses on a map, and a natural language search utility is present. The user can ask Hermes to find them anything. Want to find all the lowest cost groceries in your area? Hermes' got it. Want to build a tree house? Hermes' will find your wooden planks, tools, and safety hat. Want to build a $5000 gaming computer? Hermes will find you all the parts and set you on your way. If its near you, Hermes will show you where. By creating such a simple discovery utility for users, local businesses can upload their stock, location, pricing, and store hours to passively boost foot traffic and sales. Everyone using Hermes will see your business. 🏗️

### How we built it

Hermes is powered by Groq and Google Gemini. Hermes code is built under 3 pillars: frontend, backend, and data. For our frontend, we use a react interface to keep our it lightweight, scalable, and versatile. For our backend, it gets a little more interesting. Because Groq us build to be incredible efficient and quick, we used it to generate fast, real-time summaries of each business. when you view an area with Hermes, Groq will passively summarize all the businesses on your window. click on one, and a full summary of that business will be shown to the user, without them having to wait for annoying loading times. This allows to user to get to what they need fast, without waiting for loading times for each business. When a user interacts with the natural language discovery interface, they will be able to express complex needs including their budget, allergies, and store preferences to discover the products they need locally. We use Google Gemini 2.5-flash to generate powerful chatbot functionality. Hermes can lookup recipes, find ingredients, and present them at stores in your price range. Hermes can also discuss ratings with the user, ensuring the user a quality in-store shopping experience. For our data, we obviously didn't create a global list of local partners (yet). so for this demo version, we have generated example companies based off real stores in the Marin County, CA area. This allows us to simulate real-world data, while also allowing us to build Hermes in our designated time-frame. 🚧

### Challenges we ran into

Our main challenge came with the sponsor Letta. For the first 12 hours of the build, our backend was completely built and dependent on Letta's platform. However, we found that the long-term based memory system was unreliable at best, and it often misinterpreted, changed, or outright ignored out instructions. The final straw came when at 11pm, the platform failed to load altogether, setting half a days work up in flames. We had to pivot. Initially, Hermes only supposed to catalog and report on grocery stores. Instead of keeping this limited scope, we decided to take a massive risk. We moved our entire platform to Gemini, and constructed 2 AI agents to catalog, asses, and recommend stores and products of all kind, from hardware stores to gas stations. While Letta's let-down initially spelled disaster, our team was able to turn this misfortune into Hermes' greatest strength: its versatility. ✅

### Accomplishments we're proud of

Our team is most proud of our perseverance. We ran into roadblock after roadblock, and one of us even spent an hour trying to overhaul the entire backend, just to found out our problems were caused by a one word discrepancy: they had written "recommendation" instead of "response". Despite these (very annoying) hurdles, our team worked until the end (we all haven't slept in more than 24 hours, and we're all on a lot of caffeine) 🧠

### What we learned

We learned the value of communication while developing in a group setting. When the project began, we were uncoordinated. There was no clear direction, and for the first hour or two, we all fumbled to get a foothold. Eventually, we found our center, and assigned each other tasks so we could work together as a group. After that? We were a well oiled machine. 🚀

### What's next

We want Hermes to be the Amazon for in store shopping. By giving a chance for local businesses to compete with the big online names, everyone benefits. By allowing consumers to more easily compare the costs of common goods, everyone benefits. We want to make Hermes a go-to download application mobile devices, and a go-to bookmark on the web.

## README (from the GitHub repository)

# 🛍️ AI-Powered Shopping Assistant

## 🧠 Overview

This project is an AI-powered shopping assistant that helps users find the best local options for their shopping needs through a natural language interface. The system understands complex shopping requests and provides personalized recommendations based on user preferences (price, quality, or balanced approach).

## 🏗️ System Architecture

The application follows a client-server architecture with the following components:

### 🔙 Backend (Python/Flask)

The backend serves as the brain of the application, handling all AI processing and data management.

#### Core Components:

1. **`app.py` - Main Application Server**
   - Initializes the Flask application and CORS middleware
   - Manages in-memory conversation history
   - Provides RESTful API endpoints:
     - `GET /api/stores`: Retrieves store information for map visualization
     - `POST /api/converse`: Main endpoint for processing user requests
     - `POST /api/clear`: Clears conversation history
   - Loads and manages store inventory data

2. **`agent.py` - AI Recommendation Engine**
   - Implements the core recommendation logic using Google's Gemini API
   - Key functions:
     - `get_recommendation()`: Main entry point that processes user requests
     - `_assemble_list_from_inventory()`: Helper function that builds shopping lists from store inventory
   - Handles natural language understanding and response generation
   - Manages conversation context and history

3. **Data Management**
   - `data/stores.json`: Contains store information and inventory data
   - In-memory storage for conversation history

### 🌐 Frontend (React/Vite)

Modern web interface that provides a seamless user experience.

#### Key Features:
- Interactive chat interface
- Real-time map visualization using Leaflet.js
- Responsive design for various screen sizes
- Integration with backend API endpoints

## 🛠️ Technical Stack

### Backend
- **Framework**: Flask (Python)
- **AI/ML**: Google Gemini API
- **Data Storage**: JSON-based storage for stores and inventory
- **API**: RESTful endpoints

### Frontend
- **Framework**: React
- **Maps**: Leaflet.js
- **Build Tool**: Vite
- **Styling**: CSS Modules

## 🔄 Data Flow

1. User submits a request through the frontend chat interface
2. Request is sent to the backend's `/api/converse` endpoint
3. Backend processes the request using the AI recommendation engine
4. System queries store inventory and generates recommendations
5. Response is formatted and sent back to the frontend
6. Frontend updates the UI with the response and any relevant map data

## 🚀 Getting Started

### Prerequisites
- Python 3.8+
- Node.js 16+
- Google Gemini API key

### Installation
1. Clone the repository
2. Install backend dependencies: `pip install -r requirements.txt`
3. Install frontend dependencies: `cd frontend/chat-interface && npm install`
4. Set up your environment variables (API keys, etc.)

### Running the Application
1. Start the backend: `python backend/app.py`
2. Start the frontend: `cd frontend/chat-interface && npm run dev`
3. Access the application at `http://localhost:5173`

## 🤖 AI Capabilities

The system demonstrates several advanced AI capabilities:

1. **Natural Language Understanding**
   - Processes complex, conversational shopping requests
   - Understands context from previous messages
   - Handles ambiguous or incomplete requests

2. **Recommendation Engine**
   - Suggests complete shopping lists based on user goals
   - Considers multiple factors (price, quality, availability)
   - Provides alternatives when exact matches aren't available

3. **Context Management**
   - Maintains conversation history
   - Remembers user preferences
   - Handles follow-up questions naturally

## 📊 Data Model

The application uses a simple but effective data model:

- **Stores**: Physical locations with inventory
- **Inventory**: Products available at each store
- **Conversation History**: User and assistant message history
- **User Preferences**: Stated preferences for recommendations

## 🔄 API Endpoints

### `GET /api/stores`
- Returns: List of all stores with their inventory
- Used by: Frontend map visualization

### `POST /api/converse`
- Payload: `{ "request": string, "preference": "price|quality|balanced" }`
- Returns: AI-generated response with recommendations
- Used by: Frontend chat interface

### `POST /api/clear`
- Clears the conversation history
- Returns: Success status
- Used by: Frontend reset functionality

---

### 🔙 Backend (`/backend`)

Built with **Python** and **Flask**, the backend is where the core AI logic resides.

- **`app.py`**  
  - The entry point of the Flask server  
  - Handles API requests (`/api/converse`)  
  - Manages in-memory conversation history  
  - Delegates AI logic to `one.py`

- **`one.py`**  
  The “brain” of the assistant. The `process_request()` function:
  - Analyzes the full conversation history
  - Determines user intent and shopping category
  - Uses `stores.json` to find matching inventory
  - Calculates recommendation scores based on **price**, **quality**, or **balanced**
  - Generates a natural-language response using the **Gemini API**

- **`stores.json`**  
  The "Digital Stockroom" – a static JSON file with:
  - Store metadata (name, location)
  - Inventory categorized by shopping themes

- **`requirements.txt`**  
  Lists all Python dependencies

---

### 💻 Frontend (`/chat-interface`)

A modern **React** application built with **Vite**.

- **`src/App.jsx`**  
  The main React component that:
  - Manages chat messages and user preferences
  - Makes API calls to the Flask backend
  - Displays conversational history
  - Shows store summaries powered by **Groq**
  - Embeds an interactive map using **React-Leaflet**

> 🛠️ *Note:* Linking map markers to chat recommendations is a planned future enhancement.

---

## ⚙️ Setup & Installation

### 🔧 Prerequisites

- Python 3.8+
- Node.js 16+
- A valid Google Gemini API key
- A valid Groq API key

---

## Prerequisites ##

    Python 3.8+ and packages listed in requirements.txt

    Node.js and npm

1. Backend Setup

    Navigate to the backend directory:

    cd backend

    Create and activate a virtual environment:
    For macOS/Linux:

    python3 -m venv venv
    source venv/bin/activate

    For Windows:

    py -m venv venv
    .\venv\Scripts\activate

    Install Python dependencies:

    pip install -r requirements.txt

    Set Your API Key:
    Open one.py and replace the placeholder "YOUR_API_KEY_HERE" with your actual Google Gemini API key.

2. Frontend Setup

    Navigate to the frontend directory from the project root:

    cd chat-interface

    Install Node.js dependencies:

    npm install

## Running the Application## 

You must have both the backend and frontend servers running simultaneously in separate terminal windows.

    Start the Backend Server:

        Make sure you are in the /backend directory with your virtual environment activated.

        Run the Flask application:

        python app.py

        The server will start and be listening on http://127.0.0.1:5001.

    Start the Frontend Server:

        Open a new terminal window.

        Navigate to the /chat-interface directory.

        Run the Vite development server:

        npm run dev

        Vite will automatically open the application in your default web browser, usually at http://localhost:5173.

You can now interact with the Shopping Assistant through the chat interface in your browser.

### A special thanks to our sponsors Google and Groq for helping us power our project! ###


## Detected evidence (automated analysis)

Indexed codebase: 17 recognized source files, 64 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (29 of 29)

```
.DS_Store
backend/.env
backend/add_ratings.py
backend/agent.py
backend/app.py
backend/cli.py
backend/data/stores.json
backend/generate_descriptions.py
frontend/.DS_Store
frontend/chat-interface/.gitignore
frontend/chat-interface/eslint.config.js
frontend/chat-interface/index.html
frontend/chat-interface/package.json
frontend/chat-interface/public/data/stores_backup.json
frontend/chat-interface/public/data/stores.json
frontend/chat-interface/README.md
frontend/chat-interface/src/App.css
frontend/chat-interface/src/App.jsx
frontend/chat-interface/src/data/sampleStores.js
frontend/chat-interface/src/index.css
frontend/chat-interface/src/main.jsx
frontend/chat-interface/vite.config.js
frontend/readme.md
Pythoncode/Example.json
Pythoncode/groqimplement.py
Pythoncode/majorstoreprice.json
Pythoncode/verysmallstoreprice.json
README.md
requirements.txt
```

### Dependencies

- frontend/chat-interface/package.json: @eslint/js@^9.25.0, @types/react@^19.1.2, @types/react-dom@^19.1.2, @vitejs/plugin-react@^4.4.1, eslint@^9.25.0, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.19, globals@^16.0.0, leaflet@^1.9.4, lucide-react@^0.522.0, react@^19.1.0, react-dom@^19.1.0, react-icons@^5.5.0, react-leaflet@^5.0.0, react-router-dom@^7.6.2, vite@^6.3.5
- requirements.txt: fastapi, flask, flask_cors, google-generativeai, groq, numpy, pydantic, uvicorn

### Recent commits (newest first)

- Comments.
- Upload for Public View
- VICTORY!
- MADE COMMENTS
- Update app.py
- Update Naming
- Merge branch 'main' of https://github.com/Hezy4/BerkAiHackathon
- Upload New Agent Logic
- readme
- Adjusted the directions button
- fix
- Merge branch 'main' of https://github.com/Hezy4/BerkAiHackathon
- Prompt Engineering
- fixing
- clear memory
- working version
- changed styling
- commit
- Delete smalldata.txt
- Delete .env

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
fastapi
uvicorn
numpy
pydantic
groq
google-generativeai
flask
flask_cors

```

### frontend/chat-interface/package.json

```
{
  "name": "chat-interface",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "leaflet": "^1.9.4",
    "lucide-react": "^0.522.0",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-icons": "^5.5.0",
    "react-leaflet": "^5.0.0",
    "react-router-dom": "^7.6.2"
  },
  "devDependencies": {
    "@eslint/js": "^9.25.0",
    "@types/react": "^19.1.2",
    "@types/react-dom": "^19.1.2",
    "@vitejs/plugin-react": "^4.4.1",
    "eslint": "^9.25.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.19",
    "globals": "^16.0.0",
    "vite": "^6.3.5"
  }
}

```

### backend/app.py

```python
# app.py - The Orchestrator (Unified Agent & Restored Stores API)

import os
import json
from flask import Flask, request, jsonify
from flask_cors import CORS

# Import the primary function from our new unified "LLM Brain"
from agent import get_recommendation

# --- Constants ---
STORES_FILE = 'data/stores.json'

# --- Flask App Initialization ---
app = Flask(__name__)
CORS(app)

# --- In-Memory Session Management ---
CONVERSATION_HISTORY = []

# --- Data Loading ---
try:
    with open(STORES_FILE, 'r') as f:
        STORES_DB = json.load(f)
    print(f"Successfully loaded {STORES_FILE} database.")
except FileNotFoundError:
    print(f"FATAL ERROR: {STORES_FILE} not found. Please ensure it's in the same directory as app.py.")
    STORES_DB = []


# --- API Routes ---

# This route is included to serve store data for the map on the frontend.
@app.route('/api/stores', methods=['GET'])
def get_stores():
    """An endpoint to serve the store data to the frontend."""
    return jsonify(STORES_DB)


@app.route('/api/converse', methods=['POST'])
def converse_with_agent():
    """A single endpoint that calls the primary agent brain."""
    global CONVERSATION_HISTORY

    user_data = request.json
    raw_request = user_data.get('request')
    preference = user_data.get('preference', 'balanced')
    
    if not raw_request:
        return jsonify({"error": "No request text provided."}), 400

    print("--- Pipeline Start ---")
    print(f"User Request: '{raw_request}', Preference: '{preference}'")
    
    # Call the LLM Brain (agent.py), which now handles all logic internally
    final_recommendation = get_recommendation(raw_request, CONVERSATION_HISTORY, STORES_DB, preference)
    
    # Update history
    CONVERSATION_HISTORY.append({"role": "user", "content": raw_request})
    CONVERSATION_HISTORY.append({"role": "model", "content": final_recommendation})
    
    print("--- Pipeline End: In-memory history updated. ---")

    return jsonify({"response": final_recommendation})
git 
@app.route('/api/memory/clear', methods=['POST'])
def clear_memory():
    """An endpoint to wipe the in-memory conversation history."""
    global CONVERSATION_HISTORY
    CONVERSATION_HISTORY = []
    print("In-memory history has been cleared by user request.")
    return jsonify({"status": "Memory cleared successfully."})


# --- Main Execution ---
if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5001)
#n


```

### backend/cli.py

```python
# cli.py - Command-Line Interface for the Multi-Agent Shopping Assistant

import requests
import json

# The address of our running Flask backend
API_BASE_URL = "http://127.0.0.1:5001"

def clear_server_memory():
    """Sends a request to the backend to clear the conversation history."""
    try:
        response = requests.post(f"{API_BASE_URL}/api/memory/clear")
        if response.status_code == 200:
            print("\n[System] Memory cleared. Ready for a fresh start.")
        else:
            print(f"\n[Error] Could not clear memory. Server responded with: {response.status_code}")
    except requests.exceptions.ConnectionError:
        print("\n[Fatal Error] Could not connect to the backend server. Is app.py running?")

def main():
    """Main function to run the interactive CLI."""
    print("--- Multi-Agent Shopping Assistant CLI ---")
    print("Type 'quit' or 'exit' to close.")
    print("Type 'clear' to reset the conversation memory.")
    print("Set preference with 'pref price', 'pref quality', or 'pref balanced'.")
    print("-" * 40)

    # Initialize the preference. It will be sticky until changed.
    preference = 'balanced'

    while True:
        # Get user input
        user_input = input("\nYou: ")

        if user_input.lower() in ['quit', 'exit']:
            print("\n[System] Shutting down CLI. Goodbye.")
            break
        
        if user_input.lower() == 'clear':
            clear_server_memory()
            continue

        if user_input.lower().startswith('pref '):
            new_pref = user_input.split(' ', 1)[1].lower()
            if new_pref in ['price', 'quality', 'balanced']:
                preference = new_pref
                print(f"[System] Preference set to: {preference}")
            else:
                print("[System] Invalid preference. Choose from: price, quality, balanced.")
            continue

        # Prepare the data payload for the API
        payload = {
            "request": user_input,
            "preference": preference
        }

        try:
            # Send the request to our backend
            print("[System] Sending request to agents...")
            response = requests.post(f"{API_BASE_URL}/api/converse", json=payload)
            response.raise_for_status()  # Raises an exception for bad status codes (4xx or 5xx)

            # Parse the JSON response
            response_data = response.json()
            recommendation = response_data.get("recommendation", "No recommendation received.")

            # Print the formatted response from Agent 2
            print("\nAgent:")
            print(recommendation)

        except requests.exceptions.ConnectionError:
            print("\n[Fatal Error] Could not connect to the backend server. Please ensure app.py is running.")
        except requests.exceptions.HTTPError as e:
            print(f"\n[HTTP Error] The server responded with an error: {e.response.status_code}")
            # Try to print the error message from the server if it exists
            try:
                error_details = e.response.json()
                print(f"Server message: {error_details.get('error', 'No details provided.')}")
            except json.JSONDecodeError:
                print("Could not parse error response from server.")
        except Exception as e:
            print(f"\n[An unexpected error occurred]: {e}")


if __name__ == "__main__":
    main()

```

### frontend/chat-interface/src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### frontend/chat-interface/src/App.jsx

```javascript
import { useCallback, useEffect, useRef, useState } from 'react';
// Import is no longer needed as we'll use stores.json
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { FiMessageSquare, FiMoon, FiSun } from 'react-icons/fi';
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet';
import './App.css';

// Fix for default marker icons in Leaflet with Webpack
import icon from 'leaflet/dist/images/marker-icon.png';
import iconShadow from 'leaflet/dist/images/marker-shadow.png';

let DefaultIcon = L.icon({
  iconUrl: icon,
  shadowUrl: iconShadow,
  iconSize: [25, 41],
  iconAnchor: [12, 41],
  popupAnchor: [1, -34],
  shadowSize: [41, 41]
});

L.Marker.prototype.options.icon = DefaultIcon;

// Map configuration
const MAP_CONFIG = {
  defaultCoords: [37.9506, -122.5495], // Coordinates for 835 College Ave, Kentfield, CA
  defaultZoom: 15,
  minZoom: 14,
  maxZoom: 18,
  bounds: {
    southWest: [37.93, -122.57],
    northEast: [37.97, -122.53]
  }
};

function App() {
  const [isDarkMode, setIsDarkMode] = useState(false);
  const [isResizing, setIsResizing] = useState(false);
  const [chatWidth, setChatWidth] = useState(350);
  const [userLocation, setUserLocation] = useState(null);
  const [groceryStores, setGroceryStores] = useState([]);
  const [showMarkers, setShowMarkers] = useState(true);
  const [locationError, setLocationError] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const mapRef = useRef(null);
  const chatRef = useRef(null);
  const startX = useRef(0);
  const startWidth = useRef(0);
  
  // Load grocery stores from backend API
  const fetchLocalGroceryStores = async () => {
    try {
      const response = await fetch('http://localhost:5001/api/stores');
      if (!response.ok) {
        throw new Error('Failed to load stores data from server')
      }
      const data = await response.json();
      
      // Transform the data to match the expected format
      const stores = data.map(store => {
        // Create address parts from the location string
        const [street, ...cityParts] = store.location.split(',').map(s => s.trim());
        const city = cityParts.join(', ');
        
        // Use the store's tags if they exist, otherwise create default tags
        const tags = store.tags || {};
        
        return {
          id: store.id,
          name: store.name,
          lat: store.lat,
          lon: store.long, // Note: using 'long' from the JSON to match 'lon' in the app
          category: store.category,
          location: store.location,
          inventory: store.inventory || [],
          // Use the store's rating, default to 4.0 if not present
          rating: typeof store.rating === 'number' ? store.rating : 4.0,
          // Include all tags with proper structure
          tags: {
            description: tags.description || `${store.name} is a ${store.category} located at ${store.location}`,
            'addr:street': tags['addr:street'] || street,
            'addr:city': tags['addr:city'] || city,
            'addr:postcode': tags['addr:postcode'] || '94941',
            'opening_hours': tags['opening_hours'] || 'Mo-Su 08:00-22:00',
            'phone': tags['phone'] || store.phone || '(415) 555-1234',
            'website': tags['website'] || store.website || `https://${store.name.toLowerCase().replace(/\s+/g, '')}.com`
          }
        };
      });
      
      console.log('Loaded local stores:', stores);
      return stores;
    } catch (error) {
      console.error('Error loading local grocery stores:', error);
      return [];
    }
  };
  
  // Function to load grocery stores data from JSON file
  const loadGroceryStores = async () => {
    try {
      const response = await fetch('/data/stores.json');
      if (!response.ok) {
        throw new Error('Failed to load stores data');
      }
      const data = await response.json();
      setGroceryStores(data);
      return data;
    } catch (error) {
      console.error('Error loading grocery stores:', error);
      setGroceryStores([]);
      return [];
    }
  };
  
  // Load grocery stores data on component mount
  useEffect(() => {
    loadGroceryStores();
  }, []);

  // Load local grocery stores data
  useEffect(() => {
    const loadStores = async () => {
      setIsLoading(true);
      try {
        // Use default coordinates for the map center
        setUserLocation(MAP_CONFIG.defaultCoords);
        
        // Load stores from local JSON
        const stores = await fetchLocalGroceryStores();
        console.log('Loaded stores:', stores);
        
        if (stores.length > 0) {
          setGroceryStores(stores);
          
          // Center the map on the first store if available
          if (mapRef.current && stores[0]) {
            mapRef.current.flyTo([stores[0].lat, stores[0].lon], 15);
          }
        } else {
          console.warn('No stores found in the local data');
          setLocationError('No stores data available');
        }
      } catch (error) {
        console.error('Error loading stores:', error);
        setLocationError('Failed to load stores data');
      } finally {
        setIsLoading(false);
      }
    };
    
    loadStores();
  }, []);

  // Toggle theme and save preference
  const toggleTheme = () => {
    const newMode = !isDarkMode;
    setIsDarkMode(newMode);
    document.documentElement.setAttribute('data-theme', newMode ? 'dark' : 'light');
    localStorage.setItem('theme', newMode ? 'dark' : 'light');
  };

  // Handle mouse down on resizer
  const startResizing = useCallback((e) => {
    e.preventDefault();
    setIsResizing(true);
    startX.current = e.clientX;
    startWidth.current = chatRef.current.getBoundingClientRect().width;
  }, []);

  // Handle mouse move during resize
  const resize = useCallback((e) => {
    if (!isResizing) return;
    
    const currentWidth = startWidth.current + e.clientX - startX.current;
    const minWidth = 280; // matches --mi
[truncated — 11359 more characters]
```

### backend/add_ratings.py

```python
import json
import random

def add_ratings():
    # Load the stores data
    with open('data/stores.json', 'r') as f:
        data = json.load(f)
    
    # Add a random rating between 3.0 and 5.0 to each store
    for store in data:
        # Generate a random rating between 3.0 and 5.0 with one decimal place
        store['rating'] = round(random.uniform(3.0, 5.0), 1)
    
    # Save the updated data back to the file
    with open('data/stores.json', 'w') as f:
        json.dump(data, f, indent=2)
    
    print(f"Added ratings to {len(data)} stores.")

if __name__ == "__main__":
    add_ratings()

```

### Pythoncode/groqimplement.py

```python
import os
import json

from groq import Groq

filepath = "verysmallstoreprice.json"

def load_json_file(filepath):
    with open(filepath, 'r') as f:
        data = json.load(f)
    return data

storename = "good earth natural foods"
json_data = load_json_file('verysmallstoreprice.json')
json_string = json.dumps(json_data, indent=2)  # indent for readability in the prompt

client = Groq(
#
    api_key="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"

)


chat_completion = client.chat.completions.create(

    messages=[

        {

            "role": "system",

            "content": "You are an assistant that analyzes JSON data.",


        },

        {

            "role": "user",

            "content": f"Analyze the following JSON data: {json_string}"+" give a summary of "+ storename + "in about 40 words. Focus on summarizing rather than raw numbers",


        }

    ],

    model="llama-3.3-70b-versatile",

)


print(chat_completion.choices[0].message.content)
```

### backend/generate_descriptions.py

```python
import json
import os
from groq import Groq
from dotenv import load_dotenv

# Load environment variables
load_dotenv()

# Initialize Groq client
try:
    client = Groq(api_key=os.getenv("GROQ_API_KEY"))
except Exception as e:
    print("Error initializing Groq client. Make sure GROQ_API_KEY is set in .env file.")
    print(f"Error: {str(e)}")
    exit(1)

def generate_store_description(store):
    """Generate a detailed and engaging description for a store using Groq AI"""
    name = store['name']
    tags = store.get('tags', {})
    category = store.get('category', 'grocery store')
    
    # Prepare the prompt with strict instructions for a very concise description
    prompt = f"""Write exactly 3 sentences maximum describing "{name}", a {category}. """
    
    # Add location context if available
    if 'addr:street' in tags and 'addr:city' in tags:
        prompt += f"The store is located at {tags['addr:street']} in {tags['addr:city']}. "
    
    # Add specific instructions for conciseness and quality
    prompt += """
    RULES:
    - Must be 1-3 complete sentences only
    - Maximum of 3 sentences total
    - Each sentence should be clear and concise
    - Focus on what makes this store unique
    - Include key products or services
    - Keep it engaging but professional
    - No bullet points or lists
    - No quotation marks
    - No line breaks or paragraph breaks
    
    Example format (but specific to this store):
    "Store Name offers quality products in a welcoming environment. Our specialty is X and Y. Customers love our Z."
    """
    
    # Add inventory context if available
    if 'inventory' in store and store['inventory']:
        # Get unique categories from inventory
        categories = list(set(item.get('category', '') for item in store['inventory'] if item.get('category')))
        if categories:
            prompt += f"\nThe store offers products in categories like: {', '.join(categories[:5])}."
    
    # Add rating context if available
    rating = store.get('rating')
    if rating is not None:
        prompt += f" With a rating of {rating}, "
        if float(rating) >= 4.0:
            prompt += "it's a customer favorite known for its quality and service."
        else:
            prompt += "it provides good value to the local community."
    
    prompt += """
    The description should be written in a friendly, conversational tone that makes customers want to visit.
    Avoid using the store's name more than once in the description.
    """
    
    try:
        # Call Groq API
        chat_completion = client.chat.completions.create(
            messages=[
                {
                    "role": "system",
                    "content": "You are a helpful assistant that writes engaging store descriptions."
                },
                {
                    "role": "user",
                    "content": prompt
                }
            ],
            model="llama3-8b-8192",
            temperature=0.7,
            max_tokens=150,
            top_p=1,
        )
        
        return chat_completion.choices[0].message.content.strip()
    except Exception as e:
        print(f"Error generating description for {name}: {str(e)}")
        return f"{name} is a local grocery store offering a variety of products."

def main():
    # Path to the stores.json file
    input_file = "../frontend/chat-interface/public/data/stores.json"
    backup_file = "../frontend/chat-interface/public/data/stores_backup.json"
    
    # Create a backup of the original file
    import shutil
    shutil.copy2(input_file, backup_file)
    print(f"Created backup at {backup_file}")
    
    # Read the existing data
    try:
        with open(input_file, 'r') as f:
            stores = json.load(f)
    except Exception as e:
        print(f"Error reading {input_file}: {str(e)}")
        return
    
    # Process each store
    for i, store in enumerate(stores):
        print(f"Processing store {i+1}/{len(stores)}: {store['name']}")
        
        # Remove existing description if it exists
        if 'tags' in store and 'description' in store['tags']:
            del store['tags']['description']
            
        # Generate new description
        description = generate_store_description(store)
        
        # Add description to store data
        if 'tags' not in store:
            store['tags'] = {}
        store['tags']['description'] = description
        
        print(f"  - Added description: {description[:80]}...")
        
        # Save after each store in case of errors
        with open(input_file, 'w') as f:
            json.dump(stores, f, indent=2)
    
    print(f"\nUpdated {len(stores)} stores with descriptions in {input_file}")

if __name__ == "__main__":
    main()

```

### backend/agent.py

```python
#agent.py - Logic for Agent 1: The Recommender (LIVE & CONTEXT-AWARE)

import json
import google.generativeai as genai

# --- Configuration ---
# IMPORTANT: Replace "YOUR_API_KEY_HERE" with your actual key.
API_KEY = "INSERT_API_KEY_HERE!" 
genai.configure(api_key=API_KEY)

llm = genai.GenerativeModel('gemini-1.5-flash')

def _assemble_list_from_inventory(user_request: str, store: dict) -> dict:
    """
    This helper function, formerly in one.py, tries to build a list for a conceptual 
    request using ONLY the inventory of a single store.
    """
    print(f"[Unified Agent: Attempting to build '{user_request}' from '{store['name']}' inventory...]")
    store_inventory_names = [item['itemName'] for item in store['inventory'] if item['inStock']]

    prompt = f"""
    You are a resourceful shopping assistant. Your task is to act as a personal shopper for a user at a specific store.

    **User's Goal:** "{user_request}"

    **This Store's Available Inventory:**
    {store_inventory_names}

    **Your Task:**
    Based on the user's goal, assemble a complete and reasonable shopping list using ONLY items from the store's inventory.
    - If the user wants a "sandwich", select a type of bread, a protein, a cheese, and a condiment from the inventory.
    - If you cannot create a reasonable and complete list to satisfy the user's goal with the given inventory, you must indicate failure.

    **Output Format (Strict):**
    Respond with ONLY a valid JSON object with a single key "assembled_list".
    - If you can assemble a complete list, "assembled_list" MUST be a list of the *exact* item names you used from the store's inventory.
    - If you cannot assemble a complete list, "assembled_list" MUST be `null`.
    """
    try:
        response = llm.generate_content(prompt)
        clean_response_text = response.text.strip().replace("```json", "").replace("```", "").strip()
        llm_output = json.loads(clean_response_text)
        return llm_output
    except Exception as e:
        print(f"[Unified Agent: FATAL ERROR parsing assembly response. Error: {e}\nRaw Text: {response.text}]")
        return {"assembled_list": None}


def get_recommendation(raw_request: str, conversation_history: list, stores_db: list, preference: str) -> str:
    """
    This is the new primary function. It orchestrates the entire process.
    """
    print("\n[Unified Agent: Processing request...]")

    # Step 1: Determine Category
    full_conversation_for_prompt = conversation_history + [{"role": "user", "content": raw_request}]
    history_prompt = "\n".join([f"{msg['role'].capitalize()}: {msg['content']}" for msg in full_conversation_for_prompt])
    
    # THE FIX: Hardcode the valid categories as requested by the user for reliability.
    valid_categories = ["Groceries", "Hardware", "Electronics", "Gas"]

    prompt_category = f"""
    Analyze the conversation and determine the single most relevant shopping category for the user's latest request.

    **Conversation History:**
    {history_prompt}

    **Valid Categories:** {valid_categories}

    You MUST choose one of the "Valid Categories". Do not invent a new one.
    Respond with ONLY a valid JSON object with a single key "category".
    Example: {{"category": "Groceries"}}
    """
    print("[Unified Agent: Step 1 - Determining Category...]")
    try:
        response = llm.generate_content(prompt_category)
        clean_response_text = response.text.strip().replace("```json", "").replace("```", "").strip()
        llm_output = json.loads(clean_response_text)
        store_category = llm_output.get("category")
        if store_category not in valid_categories:
            print(f"[Unified Agent: ERROR - Invalid category '{store_category}' returned.]")
            # If the AI fails, we try to infer the category from the text as a fallback
            for cat in valid_categories:
                if cat.lower() in raw_request.lower():
                    store_category = cat
                    break
            else:
                 return "I'm not sure which category of store to look at for that request. Could you be more specific?"
    except Exception as e:
        print(f"[Unified Agent: FATAL ERROR in Step 1. Error: {e}]")
        return "I'm having trouble understanding your request. Could you please rephrase?"
    
    print(f"[Unified Agent: Category locked: {store_category}]")

    # Step 2: Assemble Options
    relevant_stores = [store for store in stores_db if store.get('category') == store_category]
    shopping_options = []
    
    for store in relevant_stores:
        assembly_result = _assemble_list_from_inventory(raw_request, store)
        if assembly_result and assembly_result.get("assembled_list"):
            option = {
                "storeInfo": {k: v for k, v in store.items() if k != 'inventory'}, 
                "matchedItemsDetails": [item for item in store['inventory'] if item['itemName'] in assembly_result["assembled_list"]]
            }
            shopping_options.append(option)
    
    if not shopping_options:
        return "I'm sorry, but after checking the local stores, I couldn't assemble a complete shopping list for that request."

    # Step 3: Augment, Sort, and Respond
    for option in shopping_options:
        details = option['matchedItemsDetails']
        option['totalPrice'] = round(sum(item['price'] for item in details), 2)
        option['averageQuality'] = round(sum(item['qualityScore'] for item in details) / len(details) if details else 0, 1)

    if preference == 'price':
        shopping_options.sort(key=lambda x: x['totalPrice'])
    elif preference == 'quality':
        shopping_options.sort(key=lambda x: x['averageQuality'], reverse=True)
    else:
        shopping_options.sort(key=lambda x: x['averageQuality'] / x['totalPrice'] if x['totalPrice'] > 0 else 0, reverse=True)

    top_options_for_prompt = []
    for option in shopping_options[:3]:
        top_options_for_prompt.append({
          
[truncated — 1394 more characters]
```

[6 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]