Upload folder using huggingface_hub
Browse files- .ruff_cache/.gitignore +2 -0
- .ruff_cache/0.15.15/12558576469893395475 +0 -0
- .ruff_cache/CACHEDIR.TAG +1 -0
- README.md +75 -6
- agent.py +400 -0
- app.py +371 -0
- database.py +221 -0
- models.py +162 -0
- pyrightconfig.json +9 -0
- run.sh +31 -0
- script.js +74 -0
- style.css +252 -0
- verify_code.py +123 -0
.ruff_cache/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Automatically created by ruff.
|
| 2 |
+
*
|
.ruff_cache/0.15.15/12558576469893395475
ADDED
|
Binary file (187 Bytes). View file
|
|
|
.ruff_cache/CACHEDIR.TAG
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Signature: 8a477f597d28d172789f06886806bc55
|
README.md
CHANGED
|
@@ -1,15 +1,84 @@
|
|
| 1 |
---
|
| 2 |
title: Vessel Studio
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
-
short_description:
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Vessel Studio
|
| 3 |
+
emoji: ๐บ
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
+
short_description: Local-first agentic studio for artisans using Gemma 4 12B.
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Vessel: A Local-First Agentic Studio for Craft Creators
|
| 15 |
+
|
| 16 |
+
`Vessel` is a local-first creative workspace designed specifically for local artisans and craft creators (e.g., Etsy sellers, local market vendors, hobbyist potters) to manage and automate their creative businesses. By combining local multimodal image analysis, brand-voice copy generation, and a code-executing local agent, Vessel turns raw product photos and simple text instructions into professional product listings, customized social marketing campaigns, invoice databases, and customer emails.
|
| 17 |
+
|
| 18 |
+
This project is built for **Track 1: Backyard AI**, specifically targeting a local artisan ("Clara," a pottery maker) who spends too much time on business admin and copywriting.
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## ๐ Key Highlights & Merit Badges
|
| 23 |
+
|
| 24 |
+
* **Off the Grid:** Runs entirely offline inside the Space (using local GPU/MPS inference). No external paid APIs are called.
|
| 25 |
+
* **Off-Brand:** Custom glassmorphic dark-mode dashboard using custom CSS/JS.
|
| 26 |
+
* **Sharing is Caring:** Employs Hugging Faceโs `smolagents` library, pushing detailed agent execution traces directly to the Hub.
|
| 27 |
+
* **Field Notes:** A detailed Hugging Face blog post documenting our experience being among the first to deploy and build an agentic, multimodal workspace with the new Gemma 4 12B model.
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## ๐ ๏ธ Tech Stack & Model Configuration
|
| 32 |
+
|
| 33 |
+
* **LLM Engine:** `google/gemma-4-12B-it` (quantized to Q4_K_M GGUF locally, loaded in full precision on Space ZeroGPU).
|
| 34 |
+
* **Frameworks:** Hugging Face `transformers` and `smolagents` (code-writing agent).
|
| 35 |
+
* **Database:** SQLite (`vessel_studio.db`) representing inventory, orders, and customer lists.
|
| 36 |
+
* **Aesthetics:** Custom vanilla CSS/JS injected directly into Gradio 6.0 layout.
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## ๐ฆ Project Layout
|
| 41 |
+
|
| 42 |
+
```
|
| 43 |
+
.
|
| 44 |
+
โโโ app.py # Main Gradio application interface
|
| 45 |
+
โโโ agent.py # smolagents CodeAgent and tool definitions
|
| 46 |
+
โโโ models.py # Gemma 4 model loader and inference helper
|
| 47 |
+
โโโ database.py # SQLite connection and seed data
|
| 48 |
+
โโโ style.css # Glassmorphic dark-mode CSS override rules
|
| 49 |
+
โโโ script.js # Javascript helper for clipboard and notifications
|
| 50 |
+
โโโ verify_code.py # Code quality validation script ( Ruff + Pyright )
|
| 51 |
+
โโโ pyrightconfig.json # Exclusion rules for type checker
|
| 52 |
+
โโโ requirements.txt # Dependency lists
|
| 53 |
+
โโโ run.sh # Executable shell script with venv auto-activation
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
## ๐ง Local Installation & Execution
|
| 59 |
+
|
| 60 |
+
### 1. Prerequisite: Gemma 4 gated model approval
|
| 61 |
+
Visit [https://huggingface.co/google/gemma-4-12B-it](https://huggingface.co/google/gemma-4-12B-it) and accept the license terms.
|
| 62 |
+
|
| 63 |
+
### 2. Set Up Environment Variables
|
| 64 |
+
Create a `.env` file in the root directory:
|
| 65 |
+
```env
|
| 66 |
+
HF_TOKEN=your_huggingface_write_token_here
|
| 67 |
+
SPACE_ID=your_username/your_space_name # Required to share agent traces to the Hub
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
### 3. Run using the auto-activating wrapper script:
|
| 71 |
+
The `./run.sh` script automatically detects, activates, and configures the python virtual environment:
|
| 72 |
+
|
| 73 |
+
* **To initialize the SQLite Database:**
|
| 74 |
+
```bash
|
| 75 |
+
./run.sh db
|
| 76 |
+
```
|
| 77 |
+
* **To launch the local web server:**
|
| 78 |
+
```bash
|
| 79 |
+
./run.sh
|
| 80 |
+
```
|
| 81 |
+
* **To run formatting, lints, and static type checking:**
|
| 82 |
+
```bash
|
| 83 |
+
./run.sh verify
|
| 84 |
+
```
|
agent.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Import os paths, serialization tools, and datetime metadata
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
# Import deep learning acceleration libraries
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
# Import Hugging Face API connections and agent frameworks
|
| 10 |
+
from huggingface_hub import HfApi
|
| 11 |
+
from smolagents import CodeAgent, Model, tool
|
| 12 |
+
|
| 13 |
+
# Import query functions from database module
|
| 14 |
+
from database import db_query, db_execute
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Import model loaders and device mapping tools
|
| 18 |
+
from models import load_gemma_model, get_device
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# Custom Response Wrapper class matching smolagents Model return contract
|
| 22 |
+
class ChatResponse:
|
| 23 |
+
# Initialize response object with generated text content
|
| 24 |
+
def __init__(self, content):
|
| 25 |
+
self.content = content
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# Custom Model Subclass to reuse our warm, preloaded Gemma 4 model instance
|
| 29 |
+
class GemmaLocalModel(Model):
|
| 30 |
+
# Initialize the custom model wrapper
|
| 31 |
+
def __init__(self, model_id="google/gemma-4-12B-it", **kwargs):
|
| 32 |
+
super().__init__(model_id=model_id, **kwargs)
|
| 33 |
+
|
| 34 |
+
# Implement generate to run inference inside agent execution loops
|
| 35 |
+
def generate(self, messages, stop_sequences=None, **kwargs):
|
| 36 |
+
# Retrieve the preloaded model and processor from cache
|
| 37 |
+
model, processor = load_gemma_model()
|
| 38 |
+
# Resolve target device mapping
|
| 39 |
+
device = get_device()
|
| 40 |
+
|
| 41 |
+
# Format prompt messages into the model's native chat syntax
|
| 42 |
+
text_prompt = processor.apply_chat_template(
|
| 43 |
+
messages, add_generation_prompt=True
|
| 44 |
+
)
|
| 45 |
+
# Process and transfer text inputs to the hardware device
|
| 46 |
+
inputs = processor(text=text_prompt, return_tensors="pt").to(device)
|
| 47 |
+
|
| 48 |
+
# Ensure correct datatypes are mapped for visual/multimodal layers
|
| 49 |
+
if "pixel_values" in inputs:
|
| 50 |
+
inputs["pixel_values"] = inputs["pixel_values"].to(model.dtype)
|
| 51 |
+
|
| 52 |
+
# Run text generation under no-gradient constraints
|
| 53 |
+
with torch.no_grad():
|
| 54 |
+
generated_ids = model.generate(
|
| 55 |
+
**inputs, max_new_tokens=1024, temperature=0.2, do_sample=True
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Isolate the input token count
|
| 59 |
+
input_len = inputs["input_ids"].shape[1]
|
| 60 |
+
# Decode the output token IDs, skipping prompt tokens
|
| 61 |
+
response_ids = generated_ids[0][input_len:]
|
| 62 |
+
# Convert token IDs back to a string
|
| 63 |
+
response_text = processor.decode(response_ids, skip_special_tokens=True).strip()
|
| 64 |
+
|
| 65 |
+
# Truncate response text if the model outputs any defined stop sequence
|
| 66 |
+
if stop_sequences:
|
| 67 |
+
for stop_seq in stop_sequences:
|
| 68 |
+
if stop_seq in response_text:
|
| 69 |
+
response_text = response_text.split(stop_seq)[0]
|
| 70 |
+
|
| 71 |
+
# Return response wrapped in our ChatResponse object
|
| 72 |
+
return ChatResponse(content=response_text)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# --- Define Agent Tools ---
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@tool
|
| 79 |
+
def query_inventory_database(sql_query: str) -> str:
|
| 80 |
+
"""
|
| 81 |
+
Executes a read-only SQL SELECT query against the local database and returns results.
|
| 82 |
+
Use this to look up inventory items, orders, or customers.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
sql_query: A valid SQL SELECT query string.
|
| 86 |
+
"""
|
| 87 |
+
# Enforce SELECT constraint to block malicious write commands in this tool
|
| 88 |
+
if not sql_query.strip().lower().startswith("select"):
|
| 89 |
+
return "Error: This tool only allows SELECT queries. Use execute_database_command for database modifications."
|
| 90 |
+
# Run the read query against SQLite
|
| 91 |
+
results = db_query(sql_query)
|
| 92 |
+
# Format database rows into a readable JSON string
|
| 93 |
+
return json.dumps(results, indent=2)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@tool
|
| 97 |
+
def execute_database_command(sql_command: str) -> str:
|
| 98 |
+
"""
|
| 99 |
+
Executes an INSERT, UPDATE, or DELETE SQL statement against the local database to modify records.
|
| 100 |
+
Use this to record new orders, update stock levels, or update customer shipping addresses.
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
sql_command: A valid SQL write command string.
|
| 104 |
+
"""
|
| 105 |
+
# Block SELECT statements to ensure clean usage mapping
|
| 106 |
+
if sql_command.strip().lower().startswith("select"):
|
| 107 |
+
return "Error: Use query_inventory_database for SELECT queries."
|
| 108 |
+
# Run database write modification command
|
| 109 |
+
result = db_execute(sql_command)
|
| 110 |
+
return result
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@tool
|
| 114 |
+
def generate_invoice_pdf(order_id: int) -> str:
|
| 115 |
+
"""
|
| 116 |
+
Generates a professional PDF invoice for a given order ID and saves it locally.
|
| 117 |
+
Returns the file path of the generated PDF.
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
order_id: The ID of the order to generate an invoice for.
|
| 121 |
+
"""
|
| 122 |
+
# Import ReportLab page sizing utilities
|
| 123 |
+
from reportlab.lib.pagesizes import letter
|
| 124 |
+
|
| 125 |
+
# Import flowable element classes for layout building
|
| 126 |
+
from reportlab.platypus import (
|
| 127 |
+
SimpleDocTemplate,
|
| 128 |
+
Paragraph,
|
| 129 |
+
Spacer,
|
| 130 |
+
Table,
|
| 131 |
+
TableStyle,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# Import text style registries
|
| 135 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 136 |
+
|
| 137 |
+
# Import color palettes
|
| 138 |
+
from reportlab.lib import colors
|
| 139 |
+
|
| 140 |
+
# Query order details from database
|
| 141 |
+
order_rows = db_query("SELECT * FROM orders WHERE id = ?", (order_id,))
|
| 142 |
+
# Validate order existence
|
| 143 |
+
if not order_rows or "error" in order_rows[0]:
|
| 144 |
+
return f"Error: Order ID {order_id} not found."
|
| 145 |
+
# Reference target order dictionary
|
| 146 |
+
order = order_rows[0]
|
| 147 |
+
|
| 148 |
+
# Query matching customer details
|
| 149 |
+
customer_rows = db_query(
|
| 150 |
+
"SELECT * FROM customers WHERE id = ?", (order["customer_id"],)
|
| 151 |
+
)
|
| 152 |
+
# Map customer dict, fallback to placeholders if missing
|
| 153 |
+
customer = (
|
| 154 |
+
customer_rows[0]
|
| 155 |
+
if customer_rows
|
| 156 |
+
else {"name": "Unknown", "email": "N/A", "shipping_address": "N/A"}
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
# Query all item rows related to this order
|
| 160 |
+
items = db_query(
|
| 161 |
+
"SELECT order_items.*, inventory.name AS item_name FROM order_items JOIN inventory ON order_items.product_id = inventory.id WHERE order_id = ?",
|
| 162 |
+
(order_id,),
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# Compile the target PDF filename
|
| 166 |
+
pdf_filename = f"invoice_order_{order_id}.pdf"
|
| 167 |
+
# Resolve absolute path for PDF generation
|
| 168 |
+
pdf_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), pdf_filename)
|
| 169 |
+
|
| 170 |
+
try:
|
| 171 |
+
# Instantiate ReportLab page layout with standard letter sizing
|
| 172 |
+
doc = SimpleDocTemplate(pdf_path, pagesize=letter)
|
| 173 |
+
# Initialize empty document story flow list
|
| 174 |
+
story = []
|
| 175 |
+
# Load sample style dictionary
|
| 176 |
+
styles = getSampleStyleSheet()
|
| 177 |
+
|
| 178 |
+
# Design custom brand-aligned header style
|
| 179 |
+
title_style = ParagraphStyle(
|
| 180 |
+
"InvoiceTitle",
|
| 181 |
+
parent=styles["Heading1"],
|
| 182 |
+
fontName="Helvetica-Bold",
|
| 183 |
+
fontSize=24,
|
| 184 |
+
leading=28,
|
| 185 |
+
textColor=colors.HexColor("#0d9488"), # Teal branding color
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# Reference normal text style
|
| 189 |
+
normal_style = styles["Normal"]
|
| 190 |
+
|
| 191 |
+
# Append Title to document flow
|
| 192 |
+
story.append(Paragraph("INVOICE", title_style))
|
| 193 |
+
# Append spacing under Title
|
| 194 |
+
story.append(Spacer(1, 15))
|
| 195 |
+
|
| 196 |
+
# Format customer billing info block
|
| 197 |
+
details_text = f"""
|
| 198 |
+
<b>Order ID:</b> {order_id}<br/>
|
| 199 |
+
<b>Date:</b> {order["order_date"]}<br/>
|
| 200 |
+
<b>Status:</b> {order["status"]}<br/>
|
| 201 |
+
<br/>
|
| 202 |
+
<b>Billed To:</b><br/>
|
| 203 |
+
{customer["name"]}<br/>
|
| 204 |
+
Email: {customer["email"]}<br/>
|
| 205 |
+
Address: {customer["shipping_address"]}<br/>
|
| 206 |
+
"""
|
| 207 |
+
# Append customer billing details to document flow
|
| 208 |
+
story.append(Paragraph(details_text, normal_style))
|
| 209 |
+
# Append spacing under details
|
| 210 |
+
story.append(Spacer(1, 20))
|
| 211 |
+
|
| 212 |
+
# Setup items list header
|
| 213 |
+
data = [["Item", "Quantity", "Price", "Total"]]
|
| 214 |
+
# Format line items into the grid list
|
| 215 |
+
for item in items:
|
| 216 |
+
qty = item["quantity"]
|
| 217 |
+
price = item["price"]
|
| 218 |
+
total = qty * price
|
| 219 |
+
data.append([item["item_name"], str(qty), f"${price:.2f}", f"${total:.2f}"])
|
| 220 |
+
|
| 221 |
+
# Append final sum indicator row
|
| 222 |
+
data.append(["", "", "Total Due:", f"${order['total_price']:.2f}"])
|
| 223 |
+
|
| 224 |
+
# Create table mapping columns widths
|
| 225 |
+
table = Table(data, colWidths=[250, 70, 90, 90])
|
| 226 |
+
# Apply custom table grids, colors, padding, and font stylings
|
| 227 |
+
table.setStyle(
|
| 228 |
+
TableStyle(
|
| 229 |
+
[
|
| 230 |
+
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#f3f4f6")),
|
| 231 |
+
("TEXTCOLOR", (0, 0), (-1, 0), colors.HexColor("#111827")),
|
| 232 |
+
("ALIGN", (0, 0), (-1, -1), "LEFT"),
|
| 233 |
+
("BOTTOMPADDING", (0, 0), (-1, 0), 8),
|
| 234 |
+
("GRID", (0, 0), (-1, -2), 0.5, colors.HexColor("#e5e7eb")),
|
| 235 |
+
("LINEBELOW", (0, -1), (-1, -1), 1.5, colors.HexColor("#0d9488")),
|
| 236 |
+
("TOPPADDING", (0, -1), (-1, -1), 8),
|
| 237 |
+
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
| 238 |
+
]
|
| 239 |
+
)
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
# Append table grid to document flow
|
| 243 |
+
story.append(table)
|
| 244 |
+
# Build the final PDF document on disk
|
| 245 |
+
doc.build(story)
|
| 246 |
+
return f"Success: Invoice PDF generated successfully at {pdf_path}"
|
| 247 |
+
except Exception as e:
|
| 248 |
+
# Catch and report any formatting errors during generation
|
| 249 |
+
return f"Error building PDF: {str(e)}"
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
@tool
|
| 253 |
+
def draft_customer_email(to_email: str, subject: str, body_content: str) -> str:
|
| 254 |
+
"""
|
| 255 |
+
Drafts an email to a customer and saves it locally in a structured text file.
|
| 256 |
+
Returns the file path of the draft email.
|
| 257 |
+
|
| 258 |
+
Args:
|
| 259 |
+
to_email: The customer's email address.
|
| 260 |
+
subject: The subject line of the email.
|
| 261 |
+
body_content: The formatted body content of the email.
|
| 262 |
+
"""
|
| 263 |
+
# Create the target email filename
|
| 264 |
+
draft_filename = f"email_draft_{to_email.replace('@', '_at_')}.txt"
|
| 265 |
+
# Resolve absolute path for email generation
|
| 266 |
+
draft_path = os.path.join(
|
| 267 |
+
os.path.dirname(os.path.abspath(__file__)), draft_filename
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
try:
|
| 271 |
+
# Write email text blocks to draft file
|
| 272 |
+
with open(draft_path, "w", encoding="utf-8") as f:
|
| 273 |
+
f.write(f"To: {to_email}\n")
|
| 274 |
+
f.write(f"Subject: {subject}\n")
|
| 275 |
+
f.write(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 276 |
+
f.write("=" * 40 + "\n\n")
|
| 277 |
+
f.write(body_content)
|
| 278 |
+
f.write("\n\n" + "=" * 40 + "\n")
|
| 279 |
+
f.write("Draft auto-generated by Vessel Studio Client.\n")
|
| 280 |
+
|
| 281 |
+
return f"Success: Email draft saved successfully at {draft_path}"
|
| 282 |
+
except Exception as e:
|
| 283 |
+
# Return error if file write fails
|
| 284 |
+
return f"Error writing email file: {str(e)}"
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
# --- Trace Pushing Mechanism ---
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def share_trace_on_hub(task_id: str, steps: list) -> bool:
|
| 291 |
+
"""Serializes the agent execution steps to JSON and pushes it to the Hugging Face Space."""
|
| 292 |
+
# Load Hugging Face authorization credentials from environment
|
| 293 |
+
token = os.getenv("HF_TOKEN")
|
| 294 |
+
# Load Space repository identifier from environment
|
| 295 |
+
repo_id = os.getenv("SPACE_ID")
|
| 296 |
+
|
| 297 |
+
# Resolve trace logging directory path
|
| 298 |
+
trace_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "agent_traces")
|
| 299 |
+
# Create directory if it is absent
|
| 300 |
+
os.makedirs(trace_dir, exist_ok=True)
|
| 301 |
+
|
| 302 |
+
# Format trace log dictionary
|
| 303 |
+
trace_data = {
|
| 304 |
+
"task_id": task_id,
|
| 305 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 306 |
+
"steps": [str(step) for step in steps],
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
# Format trace filename
|
| 310 |
+
filename = f"trace_{task_id}.json"
|
| 311 |
+
# Resolve target file path
|
| 312 |
+
local_path = os.path.join(trace_dir, filename)
|
| 313 |
+
|
| 314 |
+
# Save structured trace log into a JSON file locally
|
| 315 |
+
with open(local_path, "w", encoding="utf-8") as f:
|
| 316 |
+
json.dump(trace_data, f, indent=2)
|
| 317 |
+
|
| 318 |
+
# Push file to Space Hub if credentials and Space target are configured
|
| 319 |
+
if token and token != "hf_YOUR_WRITE_TOKEN_HERE" and repo_id:
|
| 320 |
+
try:
|
| 321 |
+
# Instantiate Hugging Face API client
|
| 322 |
+
api = HfApi(token=token)
|
| 323 |
+
# Upload file directly to the Space repository
|
| 324 |
+
api.upload_file(
|
| 325 |
+
path_or_fileobj=local_path,
|
| 326 |
+
path_in_repo=f"agent_traces/{filename}",
|
| 327 |
+
repo_id=repo_id,
|
| 328 |
+
repo_type="space",
|
| 329 |
+
)
|
| 330 |
+
print(f"Trace pushed to Space repo: {repo_id}/agent_traces/{filename}")
|
| 331 |
+
return True
|
| 332 |
+
except Exception as e:
|
| 333 |
+
# Log failure if upload is blocked
|
| 334 |
+
print(f"Failed to push trace to Hub: {e}")
|
| 335 |
+
return False
|
| 336 |
+
|
| 337 |
+
# Log local fallback
|
| 338 |
+
print("Trace saved locally (SPACE_ID or HF_TOKEN config not found).")
|
| 339 |
+
return False
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
# --- Core Run Agent Loop ---
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def run_agent(task_prompt: str, task_id: str | None = None) -> dict:
|
| 346 |
+
"""Instantiates a CodeAgent with our Gemma 4 model and runs the task."""
|
| 347 |
+
# Fallback to dynamic timestamp-based ID if none is supplied
|
| 348 |
+
if task_id is None:
|
| 349 |
+
task_id = f"task_{int(datetime.now().timestamp())}"
|
| 350 |
+
|
| 351 |
+
# Initialize our custom model wrapper
|
| 352 |
+
model_wrapper = GemmaLocalModel()
|
| 353 |
+
|
| 354 |
+
# Define tools list for agent capability mapping
|
| 355 |
+
tools = [
|
| 356 |
+
query_inventory_database,
|
| 357 |
+
execute_database_command,
|
| 358 |
+
generate_invoice_pdf,
|
| 359 |
+
draft_customer_email,
|
| 360 |
+
]
|
| 361 |
+
|
| 362 |
+
# Instantiate CodeAgent
|
| 363 |
+
agent = CodeAgent(tools=tools, model=model_wrapper, max_steps=5)
|
| 364 |
+
|
| 365 |
+
# Execute target task instruction
|
| 366 |
+
result = agent.run(task_prompt)
|
| 367 |
+
|
| 368 |
+
# Fetch intermediate execution steps from agent memory
|
| 369 |
+
steps = agent.memory.get_full_steps() if hasattr(agent, "memory") else []
|
| 370 |
+
|
| 371 |
+
# Upload logs file to the Hub (Sharing is Caring badge)
|
| 372 |
+
shared = share_trace_on_hub(task_id, steps)
|
| 373 |
+
|
| 374 |
+
return {
|
| 375 |
+
"task_id": task_id,
|
| 376 |
+
"result": result,
|
| 377 |
+
"trace_shared": shared,
|
| 378 |
+
"local_trace_path": os.path.join("agent_traces", f"trace_{task_id}.json"),
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
# Execute test runs on direct execution
|
| 383 |
+
if __name__ == "__main__":
|
| 384 |
+
# Import database initializer
|
| 385 |
+
import database
|
| 386 |
+
|
| 387 |
+
# Set tables schemas and seed data
|
| 388 |
+
database.initialize_database()
|
| 389 |
+
|
| 390 |
+
# Test query instruction
|
| 391 |
+
test_prompt = "Query the inventory to find all items that have 'rustic' style, and count them."
|
| 392 |
+
try:
|
| 393 |
+
print("Running agent test task...")
|
| 394 |
+
# Run test execution
|
| 395 |
+
agent_out = run_agent(test_prompt, "test_run_01")
|
| 396 |
+
print("\nAgent Output:", agent_out["result"])
|
| 397 |
+
print("Trace Path:", agent_out["local_trace_path"])
|
| 398 |
+
except Exception as e:
|
| 399 |
+
# Log failure
|
| 400 |
+
print("Agent failed to run:", e)
|
app.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Import directory and path utilities
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
# Import Gradio web components
|
| 6 |
+
import gradio as gr
|
| 7 |
+
|
| 8 |
+
# Ensure workspace paths are resolved correctly for package imports
|
| 9 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 10 |
+
|
| 11 |
+
# Import database, model, and agent modules
|
| 12 |
+
import agent
|
| 13 |
+
import database
|
| 14 |
+
import models
|
| 15 |
+
|
| 16 |
+
# Initialize SQLite schemas and seed baseline mock records
|
| 17 |
+
database.initialize_database()
|
| 18 |
+
|
| 19 |
+
# Resolve absolute paths to CSS and JS static assets
|
| 20 |
+
CSS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style.css")
|
| 21 |
+
JS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "script.js")
|
| 22 |
+
|
| 23 |
+
# Read CSS override styles
|
| 24 |
+
with open(CSS_PATH, "r", encoding="utf-8") as f:
|
| 25 |
+
custom_css = f.read()
|
| 26 |
+
|
| 27 |
+
# Read Javascript clipboard hooks
|
| 28 |
+
with open(JS_PATH, "r", encoding="utf-8") as f:
|
| 29 |
+
custom_js = f.read()
|
| 30 |
+
|
| 31 |
+
# --- Backend Ingestion Endpoints ---
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def analyze_and_draft_copy(image, custom_notes):
|
| 35 |
+
"""Processes product image + custom notes via Gemma 4 to output copy blocks."""
|
| 36 |
+
# Ensure image has been uploaded
|
| 37 |
+
if image is None:
|
| 38 |
+
return ("Please upload an image of the craft item first.", "", "", "", "")
|
| 39 |
+
|
| 40 |
+
# Define system instructions prompting Gemma 4 for structured outputs
|
| 41 |
+
prompt = f"""
|
| 42 |
+
You are the creative coordinator for Clara's artisan design studio.
|
| 43 |
+
Analyze the uploaded image of this hand-made craft product.
|
| 44 |
+
|
| 45 |
+
Custom artisan notes about the item:
|
| 46 |
+
"{custom_notes if custom_notes else "No additional notes provided."}"
|
| 47 |
+
|
| 48 |
+
Based on the image details and notes, generate the following content in a single structured response.
|
| 49 |
+
Format each section exactly as labeled with headings:
|
| 50 |
+
|
| 51 |
+
---SECTION 1: STRUCTURED ATTRIBUTES---
|
| 52 |
+
Category: [Extracted category, e.g., Mug, Vase, Planter]
|
| 53 |
+
Materials: [List materials, e.g., stoneware clay, blue glaze]
|
| 54 |
+
Colors: [Dominant colors]
|
| 55 |
+
Style: [Esthetic style, e.g., rustic, modern, whimsical]
|
| 56 |
+
|
| 57 |
+
---SECTION 2: SEO LISTING COPY---
|
| 58 |
+
Title: [A SEO-friendly title under 140 characters]
|
| 59 |
+
Description: [A warm, narrative, story-driven product description detailing the handmade feel. Do NOT use buzzwords like "unmatched", "perfect addition", or "elevate your space".]
|
| 60 |
+
Tags: [13 keywords separated by commas]
|
| 61 |
+
|
| 62 |
+
---SECTION 3: SOCIAL MARKETING---
|
| 63 |
+
Instagram: [Engaging Instagram caption with calls to action and relevant craft hashtags]
|
| 64 |
+
Pinterest: [SEO-optimized Pinterest description detailing the visuals]
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
# Load Gemma 4 from cache and run multimodal inference
|
| 69 |
+
response = models.generate_response(prompt, image=image, max_new_tokens=1536)
|
| 70 |
+
|
| 71 |
+
# Initialize fallback values for parsed segments
|
| 72 |
+
attributes = "Not extracted"
|
| 73 |
+
listing_copy = "Not extracted"
|
| 74 |
+
social_copy = "Not extracted"
|
| 75 |
+
|
| 76 |
+
# Split output text blocks using separators
|
| 77 |
+
sections = response.split("---")
|
| 78 |
+
# Extract individual section strings
|
| 79 |
+
for section in sections:
|
| 80 |
+
if "SECTION 1" in section:
|
| 81 |
+
attributes = section.replace(
|
| 82 |
+
"SECTION 1: STRUCTURED ATTRIBUTES---", ""
|
| 83 |
+
).strip()
|
| 84 |
+
elif "SECTION 2" in section:
|
| 85 |
+
listing_copy = section.replace(
|
| 86 |
+
"SECTION 2: SEO LISTING COPY---", ""
|
| 87 |
+
).strip()
|
| 88 |
+
elif "SECTION 3" in section:
|
| 89 |
+
social_copy = section.replace(
|
| 90 |
+
"SECTION 3: SOCIAL MARKETING---", ""
|
| 91 |
+
).strip()
|
| 92 |
+
|
| 93 |
+
return "Success!", attributes, listing_copy, social_copy, response
|
| 94 |
+
except Exception as e:
|
| 95 |
+
# Catch and return error output
|
| 96 |
+
return f"Error during model analysis: {str(e)}", "", "", "", ""
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def run_agent_task(prompt):
|
| 100 |
+
"""Traces and executes administrative business tasks using smolagents."""
|
| 101 |
+
# Verify input exists
|
| 102 |
+
if not prompt.strip():
|
| 103 |
+
return "Please input a query or task instruction.", "", None, "Trace idle."
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
# Trigger smolagents CodeAgent execution loop
|
| 107 |
+
res = agent.run_agent(prompt)
|
| 108 |
+
|
| 109 |
+
# Initialize export files collector list
|
| 110 |
+
exported_files = []
|
| 111 |
+
# Get active directory path
|
| 112 |
+
workspace_dir = os.path.dirname(os.path.abspath(__file__))
|
| 113 |
+
# Scan workspace for generated PDF invoices or email text drafts
|
| 114 |
+
for file in os.listdir(workspace_dir):
|
| 115 |
+
if (file.startswith("invoice_order_") and file.endswith(".pdf")) or (
|
| 116 |
+
file.startswith("email_draft_") and file.endswith(".txt")
|
| 117 |
+
):
|
| 118 |
+
exported_files.append(os.path.join(workspace_dir, file))
|
| 119 |
+
|
| 120 |
+
# Format output strings
|
| 121 |
+
result_text = str(res["result"])
|
| 122 |
+
# Format Hub trace sharing confirmation message
|
| 123 |
+
trace_status = (
|
| 124 |
+
"Trace successfully shared on Hugging Face Hub!"
|
| 125 |
+
if res["trace_shared"]
|
| 126 |
+
else "Trace saved locally in agent_traces/"
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# Read contents from generated local trace JSON file
|
| 130 |
+
trace_path = os.path.join(workspace_dir, res["local_trace_path"])
|
| 131 |
+
trace_log = ""
|
| 132 |
+
if os.path.exists(trace_path):
|
| 133 |
+
with open(trace_path, "r", encoding="utf-8") as f:
|
| 134 |
+
trace_log = f.read()
|
| 135 |
+
|
| 136 |
+
return (
|
| 137 |
+
result_text,
|
| 138 |
+
trace_log,
|
| 139 |
+
exported_files if exported_files else None,
|
| 140 |
+
trace_status,
|
| 141 |
+
)
|
| 142 |
+
except Exception as e:
|
| 143 |
+
# Return error output if agent crashes
|
| 144 |
+
return f"Error executing agent task: {str(e)}", "", None, "Trace failed."
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# --- Gradio UI Layout Building ---
|
| 148 |
+
|
| 149 |
+
# Initialize Blocks with custom browser title
|
| 150 |
+
with gr.Blocks(title="Vessel: Creative Studio Client") as demo:
|
| 151 |
+
# Render main header styling panel
|
| 152 |
+
gr.HTML("""
|
| 153 |
+
<div class="header-row" style="text-align: center; margin-bottom: 2rem;">
|
| 154 |
+
<h1 style="color: #2dd4bf; font-weight: 900; font-size: 2.8rem; margin-bottom: 0.2rem; letter-spacing: 0.05em;">VESSEL</h1>
|
| 155 |
+
<p style="color: #e5e7eb; font-size: 1.15rem; font-weight: 500; max-width: 800px; margin: 0.5rem auto 1rem auto; line-height: 1.6;">
|
| 156 |
+
A Local-First Creative Studio Client and Agentic Assistant for Artisans, automating content curation and studio administration.
|
| 157 |
+
</p>
|
| 158 |
+
<!-- Horizontal highlight pills mapping model specifications -->
|
| 159 |
+
<div class="badge-container">
|
| 160 |
+
<span class="vessel-badge">๐บ google/gemma-4-12b-it</span>
|
| 161 |
+
<span class="vessel-badge">๐ค smolagents framework</span>
|
| 162 |
+
<span class="vessel-badge">๐พ local sqlite db</span>
|
| 163 |
+
<span class="vessel-badge">๐จ off-brand theme</span>
|
| 164 |
+
<span class="vessel-badge vessel-badge-orange">๐ track 1: backyard ai</span>
|
| 165 |
+
<span class="vessel-badge vessel-badge-orange">๐ off the grid</span>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
""")
|
| 169 |
+
|
| 170 |
+
# Render collapsible testing instructions accordion
|
| 171 |
+
with gr.Accordion("๐ Quick Start & Testing Instructions", open=False):
|
| 172 |
+
gr.Markdown("""
|
| 173 |
+
### How to test Vessel Studio:
|
| 174 |
+
|
| 175 |
+
#### 1. Ingest & Copywriting Tab
|
| 176 |
+
* **Objective**: Automatically analyze a raw product photo to generate attributes, descriptions, and social posts.
|
| 177 |
+
* **Action**: Drag and drop a product image (e.g. ceramic mug or plate) into the **Product Canvas** panel, optionally add notes, and click **Ingest & Generate Copy**.
|
| 178 |
+
* **Result**: Gemma 4 extracts materials/styles and writes SEO-optimized listings and captions inside copy-to-clipboard cards.
|
| 179 |
+
|
| 180 |
+
#### 2. Assistant Terminal Tab
|
| 181 |
+
* **Objective**: Query and modify the local database, generate PDF invoices, and draft client emails.
|
| 182 |
+
* **Action**: Try typing one of these mock instructions into the **Task Command** input field and click **Execute Studio Task**:
|
| 183 |
+
* *Type*: `Query the customer list to find John Doe's email and shipping address.`
|
| 184 |
+
* *Type*: `Create a PDF invoice for order #1 and save it.`
|
| 185 |
+
* *Type*: `Draft a thank you email for order #2 to the customer's email address.`
|
| 186 |
+
* *Type*: `Update the status of order #1 in the database to 'Shipped'.`
|
| 187 |
+
* **Result**: The agent writes and runs local Python commands on-the-fly, updates SQLite records, generates files for download, and logs its trace!
|
| 188 |
+
""")
|
| 189 |
+
|
| 190 |
+
# Render primary layout tabs
|
| 191 |
+
with gr.Tabs():
|
| 192 |
+
# Setup Studio Ingest Tab
|
| 193 |
+
with gr.Tab("Studio Ingest"):
|
| 194 |
+
with gr.Row():
|
| 195 |
+
# Setup Ingest canvas column (Left Column)
|
| 196 |
+
with gr.Column(scale=1):
|
| 197 |
+
gr.HTML(
|
| 198 |
+
"<h3 style='color: #2dd4bf; margin-bottom: 1rem;'>Product Canvas</h3>"
|
| 199 |
+
)
|
| 200 |
+
# Upload image container
|
| 201 |
+
product_image = gr.Image(
|
| 202 |
+
type="pil",
|
| 203 |
+
label="Upload Product Photo",
|
| 204 |
+
sources=["upload", "webcam"],
|
| 205 |
+
)
|
| 206 |
+
# Input notes text field
|
| 207 |
+
artisan_notes = gr.Textbox(
|
| 208 |
+
label="Artisan Notes (Optional)",
|
| 209 |
+
placeholder="Add notes on custom dimensions, inspiration, or custom customer stories...",
|
| 210 |
+
lines=3,
|
| 211 |
+
)
|
| 212 |
+
# Submit button
|
| 213 |
+
ingest_btn = gr.Button("Ingest & Generate Copy", variant="primary")
|
| 214 |
+
# Processing feedback textbox
|
| 215 |
+
status_indicator = gr.Textbox(
|
| 216 |
+
label="Ingest Status", interactive=False
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Setup Output Canvas column (Right Column)
|
| 220 |
+
with gr.Column(scale=2):
|
| 221 |
+
gr.HTML(
|
| 222 |
+
"<h3 style='color: #2dd4bf; margin-bottom: 1rem;'>Generated Studio Deliverables</h3>"
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
with gr.Tabs():
|
| 226 |
+
# Structured attributes metadata tab
|
| 227 |
+
with gr.Tab("Structured Attributes"):
|
| 228 |
+
attr_output = gr.Textbox(
|
| 229 |
+
label="Visual Metadata", lines=6, interactive=False
|
| 230 |
+
)
|
| 231 |
+
copy_attr_btn = gr.Button(
|
| 232 |
+
"Copy Attributes to Clipboard", variant="secondary"
|
| 233 |
+
)
|
| 234 |
+
# Register JS click event
|
| 235 |
+
copy_attr_btn.click(
|
| 236 |
+
fn=None,
|
| 237 |
+
inputs=[attr_output],
|
| 238 |
+
js="(text) => copyToClipboard(text)",
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# SEO listings copywriting tab
|
| 242 |
+
with gr.Tab("SEO Listings"):
|
| 243 |
+
listing_output = gr.Textbox(
|
| 244 |
+
label="Shopify / Etsy Listing Draft",
|
| 245 |
+
lines=12,
|
| 246 |
+
interactive=False,
|
| 247 |
+
)
|
| 248 |
+
copy_listing_btn = gr.Button(
|
| 249 |
+
"Copy Listing to Clipboard", variant="secondary"
|
| 250 |
+
)
|
| 251 |
+
# Register JS click event
|
| 252 |
+
copy_listing_btn.click(
|
| 253 |
+
fn=None,
|
| 254 |
+
inputs=[listing_output],
|
| 255 |
+
js="(text) => copyToClipboard(text)",
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
# Social marketing copywriting tab
|
| 259 |
+
with gr.Tab("Social Marketing"):
|
| 260 |
+
social_output = gr.Textbox(
|
| 261 |
+
label="Pinterest & Instagram Drafts",
|
| 262 |
+
lines=10,
|
| 263 |
+
interactive=False,
|
| 264 |
+
)
|
| 265 |
+
copy_social_btn = gr.Button(
|
| 266 |
+
"Copy Marketing Content to Clipboard",
|
| 267 |
+
variant="secondary",
|
| 268 |
+
)
|
| 269 |
+
# Register JS click event
|
| 270 |
+
copy_social_btn.click(
|
| 271 |
+
fn=None,
|
| 272 |
+
inputs=[social_output],
|
| 273 |
+
js="(text) => copyToClipboard(text)",
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Raw completion response debugging tab
|
| 277 |
+
with gr.Tab("Raw Response"):
|
| 278 |
+
raw_output = gr.Textbox(
|
| 279 |
+
label="Gemma 4 Raw Output", lines=15, interactive=False
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
# Map the Ingestion trigger to backend analysis logic
|
| 283 |
+
ingest_btn.click(
|
| 284 |
+
fn=analyze_and_draft_copy,
|
| 285 |
+
inputs=[product_image, artisan_notes],
|
| 286 |
+
outputs=[
|
| 287 |
+
status_indicator,
|
| 288 |
+
attr_output,
|
| 289 |
+
listing_output,
|
| 290 |
+
social_output,
|
| 291 |
+
raw_output,
|
| 292 |
+
],
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
# Setup Assistant Terminal Tab
|
| 296 |
+
with gr.Tab("Assistant Terminal"):
|
| 297 |
+
with gr.Row():
|
| 298 |
+
# Setup Assistant Control Column (Left Column)
|
| 299 |
+
with gr.Column(scale=1):
|
| 300 |
+
gr.HTML(
|
| 301 |
+
"<h3 style='color: #2dd4bf; margin-bottom: 1rem;'>Assistant Control</h3>"
|
| 302 |
+
)
|
| 303 |
+
# Text prompt query input
|
| 304 |
+
agent_prompt = gr.Textbox(
|
| 305 |
+
label="Task Command",
|
| 306 |
+
placeholder="e.g., Query the customer database, find Sarah's email, and compile a PDF invoice for order #1.",
|
| 307 |
+
lines=4,
|
| 308 |
+
)
|
| 309 |
+
# Action execution trigger button
|
| 310 |
+
run_agent_btn = gr.Button("Execute Studio Task", variant="primary")
|
| 311 |
+
|
| 312 |
+
gr.HTML(
|
| 313 |
+
"<h4 style='color: #e5e7eb; margin-top: 2rem; margin-bottom: 0.5rem;'>Exported Documents</h4>"
|
| 314 |
+
)
|
| 315 |
+
# Output file downloader container
|
| 316 |
+
output_files = gr.File(
|
| 317 |
+
label="Download Generated Invoices / Email Text Files",
|
| 318 |
+
file_count="multiple",
|
| 319 |
+
interactive=False,
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
# Setup Agent Workspace Trace Column (Right Column)
|
| 323 |
+
with gr.Column(scale=2):
|
| 324 |
+
gr.HTML(
|
| 325 |
+
"<h3 style='color: #2dd4bf; margin-bottom: 1rem;'>Agent Workspace Trace</h3>"
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
# Wrap terminal text area in a custom window component
|
| 329 |
+
with gr.Column(elem_classes=["terminal-window"]):
|
| 330 |
+
gr.HTML("""
|
| 331 |
+
<div class="terminal-header">
|
| 332 |
+
<span class="dot dot-red"></span>
|
| 333 |
+
<span class="dot dot-yellow"></span>
|
| 334 |
+
<span class="dot dot-green"></span>
|
| 335 |
+
<span class="terminal-title">smolagents@vessel-studio-terminal ~ bash</span>
|
| 336 |
+
</div>
|
| 337 |
+
""")
|
| 338 |
+
# Execution logs console view
|
| 339 |
+
agent_trace_view = gr.Textbox(
|
| 340 |
+
label="Execution Log",
|
| 341 |
+
lines=18,
|
| 342 |
+
interactive=False,
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
# Task execution summary textbox
|
| 346 |
+
agent_result_view = gr.Textbox(
|
| 347 |
+
label="Task Result Summary", lines=3, interactive=False
|
| 348 |
+
)
|
| 349 |
+
# Hub trace upload status feedback label
|
| 350 |
+
trace_upload_status = gr.Label(label="Trace Hub Sharing Status")
|
| 351 |
+
|
| 352 |
+
# Map the Assistant execution trigger to backend agent loop
|
| 353 |
+
run_agent_btn.click(
|
| 354 |
+
fn=run_agent_task,
|
| 355 |
+
inputs=[agent_prompt],
|
| 356 |
+
outputs=[
|
| 357 |
+
agent_result_view,
|
| 358 |
+
agent_trace_view,
|
| 359 |
+
output_files,
|
| 360 |
+
trace_upload_status,
|
| 361 |
+
],
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
# Run web app server with custom styling layers
|
| 365 |
+
if __name__ == "__main__":
|
| 366 |
+
demo.launch(
|
| 367 |
+
server_name="0.0.0.0",
|
| 368 |
+
server_port=7860,
|
| 369 |
+
css=custom_css,
|
| 370 |
+
js=custom_js,
|
| 371 |
+
)
|
database.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Import native sqlite engine and file path system utilities
|
| 2 |
+
import os
|
| 3 |
+
import sqlite3
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
# Resolve the absolute path to the local database file inside the project directory
|
| 7 |
+
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vessel_studio.db")
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def get_connection():
|
| 11 |
+
"""Returns a connection to the SQLite database with row factory enabled."""
|
| 12 |
+
# Establish a connection to the SQLite database file
|
| 13 |
+
conn = sqlite3.connect(DB_PATH)
|
| 14 |
+
# Enable dict-like row formatting to access columns by their name keys
|
| 15 |
+
conn.row_factory = sqlite3.Row
|
| 16 |
+
return conn
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def db_query(sql_query: str, params: tuple = ()) -> list:
|
| 20 |
+
"""Executes a SELECT query and returns the results as a list of dicts."""
|
| 21 |
+
# Retrieve a fresh database connection instance
|
| 22 |
+
conn = get_connection()
|
| 23 |
+
try:
|
| 24 |
+
# Initialize the cursor to execute SQL commands
|
| 25 |
+
cursor = conn.cursor()
|
| 26 |
+
# Execute the SELECT statement with the provided parameter tuple
|
| 27 |
+
cursor.execute(sql_query, params)
|
| 28 |
+
# Fetch all matching database records
|
| 29 |
+
rows = cursor.fetchall()
|
| 30 |
+
# Convert row objects to standard Python dictionaries and return the list
|
| 31 |
+
return [dict(row) for row in rows]
|
| 32 |
+
except Exception as e:
|
| 33 |
+
# Return the error message inside a list to prevent application crashes
|
| 34 |
+
return [{"error": str(e)}]
|
| 35 |
+
finally:
|
| 36 |
+
# Guarantee database connection closure in all execution paths
|
| 37 |
+
conn.close()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def db_execute(sql_command: str, params: tuple = ()) -> str:
|
| 41 |
+
"""Executes an INSERT, UPDATE, or DELETE command and commits changes."""
|
| 42 |
+
# Retrieve a fresh database connection instance
|
| 43 |
+
conn = get_connection()
|
| 44 |
+
try:
|
| 45 |
+
# Initialize the cursor to execute SQL commands
|
| 46 |
+
cursor = conn.cursor()
|
| 47 |
+
# Execute the database modification statement
|
| 48 |
+
cursor.execute(sql_command, params)
|
| 49 |
+
# Commit the modification transaction to write changes to disk
|
| 50 |
+
conn.commit()
|
| 51 |
+
# Capture the primary key of the last inserted row
|
| 52 |
+
last_row_id = cursor.lastrowid
|
| 53 |
+
# Calculate the total number of rows modified by the command
|
| 54 |
+
changes = conn.total_changes
|
| 55 |
+
return f"Success. Row ID affected/inserted: {last_row_id}. Rows changed: {changes}."
|
| 56 |
+
except Exception as e:
|
| 57 |
+
# Return a descriptive error message if execution fails
|
| 58 |
+
return f"Error executing command: {str(e)}"
|
| 59 |
+
finally:
|
| 60 |
+
# Guarantee database connection closure in all execution paths
|
| 61 |
+
conn.close()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def initialize_database():
|
| 65 |
+
"""Creates the SQLite database tables and populates them with initial mock data if empty."""
|
| 66 |
+
# Open connection and get cursor
|
| 67 |
+
conn = get_connection()
|
| 68 |
+
cursor = conn.cursor()
|
| 69 |
+
|
| 70 |
+
# Create the customer profile registry table
|
| 71 |
+
cursor.execute("""
|
| 72 |
+
CREATE TABLE IF NOT EXISTS customers (
|
| 73 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 74 |
+
name TEXT NOT NULL,
|
| 75 |
+
email TEXT UNIQUE NOT NULL,
|
| 76 |
+
shipping_address TEXT
|
| 77 |
+
)
|
| 78 |
+
""")
|
| 79 |
+
|
| 80 |
+
# Create the inventory and product stock details table
|
| 81 |
+
cursor.execute("""
|
| 82 |
+
CREATE TABLE IF NOT EXISTS inventory (
|
| 83 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 84 |
+
name TEXT NOT NULL,
|
| 85 |
+
description TEXT,
|
| 86 |
+
price REAL NOT NULL,
|
| 87 |
+
stock INTEGER NOT NULL DEFAULT 0,
|
| 88 |
+
materials TEXT,
|
| 89 |
+
style TEXT
|
| 90 |
+
)
|
| 91 |
+
""")
|
| 92 |
+
|
| 93 |
+
# Create the customer orders transaction header table
|
| 94 |
+
cursor.execute("""
|
| 95 |
+
CREATE TABLE IF NOT EXISTS orders (
|
| 96 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 97 |
+
customer_id INTEGER NOT NULL,
|
| 98 |
+
order_date TEXT NOT NULL,
|
| 99 |
+
status TEXT NOT NULL DEFAULT 'Pending',
|
| 100 |
+
total_price REAL,
|
| 101 |
+
FOREIGN KEY (customer_id) REFERENCES customers (id)
|
| 102 |
+
)
|
| 103 |
+
""")
|
| 104 |
+
|
| 105 |
+
# Create the line item association details table for orders
|
| 106 |
+
cursor.execute("""
|
| 107 |
+
CREATE TABLE IF NOT EXISTS order_items (
|
| 108 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 109 |
+
order_id INTEGER NOT NULL,
|
| 110 |
+
product_id INTEGER NOT NULL,
|
| 111 |
+
quantity INTEGER NOT NULL,
|
| 112 |
+
price REAL NOT NULL,
|
| 113 |
+
FOREIGN KEY (order_id) REFERENCES orders (id),
|
| 114 |
+
FOREIGN KEY (product_id) REFERENCES inventory (id)
|
| 115 |
+
)
|
| 116 |
+
""")
|
| 117 |
+
|
| 118 |
+
# Commit structural tables creation
|
| 119 |
+
conn.commit()
|
| 120 |
+
|
| 121 |
+
# Query if customers already exist to skip seeding if data is present
|
| 122 |
+
cursor.execute("SELECT COUNT(*) FROM customers")
|
| 123 |
+
if cursor.fetchone()[0] == 0:
|
| 124 |
+
# Seed customer database rows
|
| 125 |
+
customers_data = [
|
| 126 |
+
(
|
| 127 |
+
"Sarah Jenkins",
|
| 128 |
+
"sarah.j@example.com",
|
| 129 |
+
"123 Maple Street, Portland, OR 97201",
|
| 130 |
+
),
|
| 131 |
+
("John Doe", "johndoe@example.com", "456 Oak Avenue, Seattle, WA 98101"),
|
| 132 |
+
(
|
| 133 |
+
"Emma Watson",
|
| 134 |
+
"emma@example.com",
|
| 135 |
+
"789 Pine Lane, San Francisco, CA 94103",
|
| 136 |
+
),
|
| 137 |
+
]
|
| 138 |
+
# Insert customer profiles in bulk
|
| 139 |
+
cursor.executemany(
|
| 140 |
+
"INSERT INTO customers (name, email, shipping_address) VALUES (?, ?, ?)",
|
| 141 |
+
customers_data,
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# Seed inventory database items
|
| 145 |
+
inventory_data = [
|
| 146 |
+
(
|
| 147 |
+
"Rustic Teal Stoneware Mug",
|
| 148 |
+
"Hand-thrown ceramic mug with a beautiful teal drip glaze. Holds 12oz.",
|
| 149 |
+
24.00,
|
| 150 |
+
15,
|
| 151 |
+
"stoneware clay, teal glaze",
|
| 152 |
+
"rustic",
|
| 153 |
+
),
|
| 154 |
+
(
|
| 155 |
+
"Minimalist White Vase",
|
| 156 |
+
"Stoneware bud vase with a matte white finish. Ideal for single stems.",
|
| 157 |
+
38.00,
|
| 158 |
+
8,
|
| 159 |
+
"ceramic clay, matte white glaze",
|
| 160 |
+
"minimalist",
|
| 161 |
+
),
|
| 162 |
+
(
|
| 163 |
+
"Whimsical Forest Spoon Rest",
|
| 164 |
+
"Playful ceramic spoon rest stamped with leaf impressions and finished in olive green.",
|
| 165 |
+
18.00,
|
| 166 |
+
12,
|
| 167 |
+
"earth clay, olive green glaze",
|
| 168 |
+
"whimsical",
|
| 169 |
+
),
|
| 170 |
+
(
|
| 171 |
+
"Terracotta Planter",
|
| 172 |
+
"Raw terracotta plant pot with matching saucer. Breathable clay base.",
|
| 173 |
+
28.00,
|
| 174 |
+
20,
|
| 175 |
+
"terracotta clay",
|
| 176 |
+
"rustic",
|
| 177 |
+
),
|
| 178 |
+
]
|
| 179 |
+
# Insert product specifications in bulk
|
| 180 |
+
cursor.executemany(
|
| 181 |
+
"INSERT INTO inventory (name, description, price, stock, materials, style) VALUES (?, ?, ?, ?, ?, ?)",
|
| 182 |
+
inventory_data,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# Create initial pending order transaction for customer Sarah Jenkins
|
| 186 |
+
cursor.execute(
|
| 187 |
+
"INSERT INTO orders (customer_id, order_date, status, total_price) VALUES (1, ?, 'Pending', 24.00)",
|
| 188 |
+
(datetime.now().strftime("%Y-%m-%d"),),
|
| 189 |
+
)
|
| 190 |
+
# Capture the ID of the newly generated order
|
| 191 |
+
order_id = cursor.lastrowid
|
| 192 |
+
# Add order line item mapping order to the Teal Stoneware Mug
|
| 193 |
+
cursor.execute(
|
| 194 |
+
"INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, 1, 1, 24.00)",
|
| 195 |
+
(order_id,),
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
# Create second completed order transaction for customer Emma Watson
|
| 199 |
+
cursor.execute(
|
| 200 |
+
"INSERT INTO orders (customer_id, order_date, status, total_price) VALUES (3, ?, 'Shipped', 56.00)",
|
| 201 |
+
(datetime.now().strftime("%Y-%m-%d"),),
|
| 202 |
+
)
|
| 203 |
+
# Capture the ID of the second order
|
| 204 |
+
order_id = cursor.lastrowid
|
| 205 |
+
# Add multiple order line items in bulk
|
| 206 |
+
cursor.executemany(
|
| 207 |
+
"INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)",
|
| 208 |
+
[(order_id, 2, 1, 38.00), (order_id, 3, 1, 18.00)],
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
# Save all seeded records transaction to disk
|
| 212 |
+
conn.commit()
|
| 213 |
+
|
| 214 |
+
# Close connection
|
| 215 |
+
conn.close()
|
| 216 |
+
print("Database initialized successfully.")
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# Initialize SQLite tables on direct script execution
|
| 220 |
+
if __name__ == "__main__":
|
| 221 |
+
initialize_database()
|
models.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Import environment loaders and deep learning frameworks
|
| 2 |
+
import os
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import torch
|
| 6 |
+
from transformers import AutoModelForMultimodalLM, AutoProcessor
|
| 7 |
+
|
| 8 |
+
# Load token variables from local environment file
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
# Specify the Hugging Face hub repository target for Gemma 4
|
| 12 |
+
MODEL_ID = "google/gemma-4-12B-it"
|
| 13 |
+
|
| 14 |
+
# Initialize global cache elements to keep load states persistent
|
| 15 |
+
_model = None
|
| 16 |
+
_processor = None
|
| 17 |
+
_device = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_device():
|
| 21 |
+
"""Determines and returns the best available device."""
|
| 22 |
+
global _device
|
| 23 |
+
# Return device if already determined and cached
|
| 24 |
+
if _device is not None:
|
| 25 |
+
return _device
|
| 26 |
+
|
| 27 |
+
# Use NVIDIA GPU if CUDA framework is active
|
| 28 |
+
if torch.cuda.is_available():
|
| 29 |
+
_device = "cuda"
|
| 30 |
+
# Use Metal Performance Shaders if running on Apple Silicon
|
| 31 |
+
elif torch.backends.mps.is_available():
|
| 32 |
+
_device = "mps"
|
| 33 |
+
# Fall back to standard CPU processing if no accelerators are present
|
| 34 |
+
else:
|
| 35 |
+
_device = "cpu"
|
| 36 |
+
return _device
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def load_gemma_model():
|
| 40 |
+
"""Loads and caches the Gemma 4 12B-it model and processor."""
|
| 41 |
+
global _model, _processor
|
| 42 |
+
# Avoid reload if model and processor are already warm in memory
|
| 43 |
+
if _model is not None and _processor is not None:
|
| 44 |
+
return _model, _processor
|
| 45 |
+
|
| 46 |
+
# Retrieve Hugging Face authentication token from environment
|
| 47 |
+
token = os.getenv("HF_TOKEN")
|
| 48 |
+
# Verify the token is present and not set to default template value
|
| 49 |
+
if not token or token == "hf_YOUR_WRITE_TOKEN_HERE":
|
| 50 |
+
raise ValueError(
|
| 51 |
+
"HF_TOKEN not set or invalid in the .env file. Please add your Hugging Face write token."
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Detect the active target execution hardware
|
| 55 |
+
device = get_device()
|
| 56 |
+
print(f"Loading {MODEL_ID} on device: {device}...")
|
| 57 |
+
|
| 58 |
+
# Download and instantiate the native multimodal processor config
|
| 59 |
+
_processor = AutoProcessor.from_pretrained(MODEL_ID, token=token)
|
| 60 |
+
|
| 61 |
+
# Initialize model options depending on device classification
|
| 62 |
+
if device == "cuda":
|
| 63 |
+
# Load in half-precision bfloat16 on server CUDA hardware
|
| 64 |
+
_model = AutoModelForMultimodalLM.from_pretrained(
|
| 65 |
+
MODEL_ID, dtype=torch.bfloat16, device_map="auto", token=token
|
| 66 |
+
)
|
| 67 |
+
elif device == "mps":
|
| 68 |
+
# Load in half-precision bfloat16 using CPU low memory options on local Mac
|
| 69 |
+
_model = AutoModelForMultimodalLM.from_pretrained(
|
| 70 |
+
MODEL_ID,
|
| 71 |
+
dtype=torch.bfloat16,
|
| 72 |
+
low_cpu_mem_usage=True,
|
| 73 |
+
device_map="auto",
|
| 74 |
+
token=token,
|
| 75 |
+
)
|
| 76 |
+
else:
|
| 77 |
+
# Load in standard 32-bit floating point precision on standard CPU
|
| 78 |
+
_model = AutoModelForMultimodalLM.from_pretrained(
|
| 79 |
+
MODEL_ID,
|
| 80 |
+
dtype=torch.float32,
|
| 81 |
+
low_cpu_mem_usage=True,
|
| 82 |
+
device_map="cpu",
|
| 83 |
+
token=token,
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
# Print success log when model initialization is finished
|
| 87 |
+
print("Model loaded successfully.")
|
| 88 |
+
return _model, _processor
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def generate_response(
|
| 92 |
+
prompt: str,
|
| 93 |
+
image: Image.Image | None = None,
|
| 94 |
+
max_new_tokens: int = 1024,
|
| 95 |
+
temperature: float = 0.4,
|
| 96 |
+
) -> str:
|
| 97 |
+
"""Generates a text response from Gemma 4 given a prompt and optional image."""
|
| 98 |
+
# Retrieve the model and processor from cache
|
| 99 |
+
model, processor = load_gemma_model()
|
| 100 |
+
# Resolve the active hardware device
|
| 101 |
+
device = get_device()
|
| 102 |
+
|
| 103 |
+
# Initialize message list
|
| 104 |
+
content = []
|
| 105 |
+
# Append the image item if it is provided
|
| 106 |
+
if image is not None:
|
| 107 |
+
content.append({"type": "image", "image": image})
|
| 108 |
+
# Append the text prompt item
|
| 109 |
+
content.append({"type": "text", "text": prompt})
|
| 110 |
+
|
| 111 |
+
# Wrap the contents inside a user role dict structure
|
| 112 |
+
messages = [{"role": "user", "content": content}]
|
| 113 |
+
|
| 114 |
+
# Format user inputs into the model's native chat syntax
|
| 115 |
+
text_prompt = processor.apply_chat_template(messages, add_generation_prompt=True)
|
| 116 |
+
|
| 117 |
+
# Run tokenization with the image if present
|
| 118 |
+
if image is not None:
|
| 119 |
+
inputs = processor(text=text_prompt, images=image, return_tensors="pt")
|
| 120 |
+
# Run tokenization with text-only parameters if image is absent
|
| 121 |
+
else:
|
| 122 |
+
inputs = processor(text=text_prompt, return_tensors="pt")
|
| 123 |
+
|
| 124 |
+
# Shift all input tensors to the target hardware device
|
| 125 |
+
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 126 |
+
# Convert visual features to match the exact data type of the model weights
|
| 127 |
+
if "pixel_values" in inputs:
|
| 128 |
+
inputs["pixel_values"] = inputs["pixel_values"].to(model.dtype)
|
| 129 |
+
# Convert audio features to match the model weights dtype
|
| 130 |
+
if "audio_values" in inputs:
|
| 131 |
+
inputs["audio_values"] = inputs["audio_values"].to(model.dtype)
|
| 132 |
+
|
| 133 |
+
# Generate text completions using no-gradient evaluation
|
| 134 |
+
with torch.no_grad():
|
| 135 |
+
generated_ids = model.generate(
|
| 136 |
+
**inputs,
|
| 137 |
+
max_new_tokens=max_new_tokens,
|
| 138 |
+
temperature=temperature,
|
| 139 |
+
do_sample=True if temperature > 0.0 else False,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
# Extract the length of the input sequence to isolate the output completion
|
| 143 |
+
input_len = inputs["input_ids"].shape[1]
|
| 144 |
+
# Decode the newly generated token IDs, discarding the prompt tokens
|
| 145 |
+
response_ids = generated_ids[0][input_len:]
|
| 146 |
+
# Convert token IDs back to a readable text string
|
| 147 |
+
response_text = processor.decode(response_ids, skip_special_tokens=True)
|
| 148 |
+
return response_text.strip()
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# Test loading mechanism when running the script directly
|
| 152 |
+
if __name__ == "__main__":
|
| 153 |
+
try:
|
| 154 |
+
# Load the model and print the resolved hardware configuration
|
| 155 |
+
model, processor = load_gemma_model()
|
| 156 |
+
print(
|
| 157 |
+
"Device map:",
|
| 158 |
+
model.hf_device_map if hasattr(model, "hf_device_map") else "CPU",
|
| 159 |
+
)
|
| 160 |
+
except Exception as e:
|
| 161 |
+
# Print failure trace
|
| 162 |
+
print("Error during test loading:", e)
|
pyrightconfig.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"exclude": [
|
| 3 |
+
"**/node_modules",
|
| 4 |
+
"**/__pycache__",
|
| 5 |
+
".venv"
|
| 6 |
+
],
|
| 7 |
+
"reportMissingImports": true,
|
| 8 |
+
"typeCheckingMode": "basic"
|
| 9 |
+
}
|
run.sh
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Vessel Auto-activating Runner Script
|
| 3 |
+
|
| 4 |
+
# Resolve directory of this script
|
| 5 |
+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
| 6 |
+
cd "$DIR"
|
| 7 |
+
|
| 8 |
+
# Automatically activate virtual environment if it exists
|
| 9 |
+
if [ -d ".venv" ]; then
|
| 10 |
+
source .venv/bin/activate
|
| 11 |
+
else
|
| 12 |
+
echo "โ ๏ธ Warning: Local python virtual environment (.venv) not found."
|
| 13 |
+
echo "Please run: python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt"
|
| 14 |
+
fi
|
| 15 |
+
|
| 16 |
+
# Determine script target (default to app.py)
|
| 17 |
+
TARGET="${1:-app.py}"
|
| 18 |
+
|
| 19 |
+
if [ "$TARGET" == "verify" ]; then
|
| 20 |
+
python3 verify_code.py "${@:2}"
|
| 21 |
+
elif [ "$TARGET" == "db" ]; then
|
| 22 |
+
python3 database.py "${@:2}"
|
| 23 |
+
elif [ -f "$TARGET" ]; then
|
| 24 |
+
python3 "$TARGET" "${@:2}"
|
| 25 |
+
else
|
| 26 |
+
echo "โ Error: Target script '$TARGET' not found."
|
| 27 |
+
echo "Usage:"
|
| 28 |
+
echo " ./run.sh # Launch the main Gradio application"
|
| 29 |
+
echo " ./run.sh verify # Run code format, lint, and type checks"
|
| 30 |
+
echo " ./run.sh db # Initialize/seed SQLite database"
|
| 31 |
+
fi
|
script.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Vessel Studio Frontend Javascript Utilities
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Copies the provided string content to the system clipboard and displays a visual toast.
|
| 5 |
+
* @param {string} text - Content to copy.
|
| 6 |
+
*/
|
| 7 |
+
function copyToClipboard(text) {
|
| 8 |
+
if (!text) return;
|
| 9 |
+
navigator.clipboard.writeText(text).then(function() {
|
| 10 |
+
showVesselToast("Copied to clipboard!");
|
| 11 |
+
}, function(err) {
|
| 12 |
+
console.error('Could not copy text: ', err);
|
| 13 |
+
});
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Creates and displays a premium animated toast notification.
|
| 18 |
+
* @param {string} message - Content message to display.
|
| 19 |
+
*/
|
| 20 |
+
function showVesselToast(message) {
|
| 21 |
+
// Delete existing toast if active to prevent overlapping
|
| 22 |
+
let existingToast = document.querySelector(".vessel-toast");
|
| 23 |
+
if (existingToast) {
|
| 24 |
+
document.body.removeChild(existingToast);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
let toast = document.createElement("div");
|
| 28 |
+
toast.className = "vessel-toast";
|
| 29 |
+
toast.innerText = message;
|
| 30 |
+
|
| 31 |
+
// Apply styling parameters
|
| 32 |
+
Object.assign(toast.style, {
|
| 33 |
+
position: "fixed",
|
| 34 |
+
bottom: "30px",
|
| 35 |
+
right: "30px",
|
| 36 |
+
backgroundColor: "#0d9488",
|
| 37 |
+
color: "#ffffff",
|
| 38 |
+
padding: "14px 24px",
|
| 39 |
+
borderRadius: "8px",
|
| 40 |
+
boxShadow: "0 10px 25px -5px rgba(13, 148, 136, 0.4)",
|
| 41 |
+
zIndex: "99999",
|
| 42 |
+
opacity: "0",
|
| 43 |
+
transition: "opacity 0.25s ease, transform 0.25s ease",
|
| 44 |
+
transform: "translateY(15px)",
|
| 45 |
+
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
|
| 46 |
+
fontSize: "0.9rem",
|
| 47 |
+
fontWeight: "600",
|
| 48 |
+
letterSpacing: "0.02em"
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
document.body.appendChild(toast);
|
| 52 |
+
|
| 53 |
+
// Trigger entrance animation
|
| 54 |
+
setTimeout(() => {
|
| 55 |
+
toast.style.opacity = "1";
|
| 56 |
+
toast.style.transform = "translateY(0)";
|
| 57 |
+
}, 30);
|
| 58 |
+
|
| 59 |
+
// Trigger dismissal animation
|
| 60 |
+
setTimeout(() => {
|
| 61 |
+
toast.style.opacity = "0";
|
| 62 |
+
toast.style.transform = "translateY(15px)";
|
| 63 |
+
setTimeout(() => {
|
| 64 |
+
if (toast.parentNode) {
|
| 65 |
+
document.body.removeChild(toast);
|
| 66 |
+
}
|
| 67 |
+
}, 250);
|
| 68 |
+
}, 2800);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// Initialize custom event integrations when Gradio loads
|
| 72 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 73 |
+
console.log("Vessel Studio frontend initialized successfully.");
|
| 74 |
+
});
|
style.css
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Vessel Studio Premium Glassmorphic Theme */
|
| 2 |
+
|
| 3 |
+
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap');
|
| 4 |
+
|
| 5 |
+
/* Global Reset & Font Mapping */
|
| 6 |
+
body, .gradio-container {
|
| 7 |
+
font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif !important;
|
| 8 |
+
background: radial-gradient(circle at top left, #0d1e2d 0%, #090d16 100%) !important;
|
| 9 |
+
color: #f3f4f6 !important;
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
min-height: 100vh;
|
| 13 |
+
display: flex !important;
|
| 14 |
+
justify-content: center !important;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/* Custom Scrollbar styling */
|
| 18 |
+
::-webkit-scrollbar {
|
| 19 |
+
width: 8px;
|
| 20 |
+
height: 8px;
|
| 21 |
+
}
|
| 22 |
+
::-webkit-scrollbar-track {
|
| 23 |
+
background: rgba(17, 24, 39, 0.3);
|
| 24 |
+
}
|
| 25 |
+
::-webkit-scrollbar-thumb {
|
| 26 |
+
background: rgba(13, 148, 136, 0.4);
|
| 27 |
+
border-radius: 4px;
|
| 28 |
+
}
|
| 29 |
+
::-webkit-scrollbar-thumb:hover {
|
| 30 |
+
background: rgba(13, 148, 136, 0.6);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* Glassmorphic Container Overrides */
|
| 34 |
+
.gradio-container {
|
| 35 |
+
width: 100% !important;
|
| 36 |
+
padding: 2rem !important;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Header customization */
|
| 40 |
+
header, .header-row {
|
| 41 |
+
margin-bottom: 2rem;
|
| 42 |
+
text-align: center;
|
| 43 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
| 44 |
+
padding-bottom: 1.5rem;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* Card and Column Styling */
|
| 48 |
+
.gr-box, .gr-padded, .gr-panel, .block {
|
| 49 |
+
background: rgba(17, 24, 39, 0.65) !important;
|
| 50 |
+
backdrop-filter: blur(16px) !important;
|
| 51 |
+
-webkit-backdrop-filter: blur(16px) !important;
|
| 52 |
+
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
| 53 |
+
border-radius: 16px !important;
|
| 54 |
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3) !important;
|
| 55 |
+
padding: 1.5rem !important;
|
| 56 |
+
margin-bottom: 1rem;
|
| 57 |
+
transition: border-color 0.3s, box-shadow 0.3s;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.block:hover {
|
| 61 |
+
border-color: rgba(13, 148, 136, 0.3) !important;
|
| 62 |
+
box-shadow: 0 8px 32px 0 rgba(13, 148, 136, 0.05) !important;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* Inputs and Forms */
|
| 66 |
+
input, textarea, select {
|
| 67 |
+
background-color: rgba(31, 41, 55, 0.5) !important;
|
| 68 |
+
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
| 69 |
+
color: #f9fafb !important;
|
| 70 |
+
border-radius: 8px !important;
|
| 71 |
+
padding: 10px 14px !important;
|
| 72 |
+
font-size: 0.95rem !important;
|
| 73 |
+
transition: all 0.2s ease-in-out !important;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
input:focus, textarea:focus, select:focus {
|
| 77 |
+
border-color: #0d9488 !important;
|
| 78 |
+
box-shadow: 0 0 0 2px rgba(13, 148, 136, 0.25) !important;
|
| 79 |
+
background-color: rgba(31, 41, 55, 0.8) !important;
|
| 80 |
+
outline: none !important;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/* Form labels */
|
| 84 |
+
.block span, label {
|
| 85 |
+
color: #9ca3af !important;
|
| 86 |
+
font-weight: 500 !important;
|
| 87 |
+
font-size: 0.85rem !important;
|
| 88 |
+
text-transform: uppercase;
|
| 89 |
+
letter-spacing: 0.05em;
|
| 90 |
+
margin-bottom: 4px;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* Tab Headers */
|
| 94 |
+
.tabs {
|
| 95 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
|
| 96 |
+
margin-bottom: 1.5rem !important;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.tab-nav button {
|
| 100 |
+
color: #9ca3af !important;
|
| 101 |
+
border-bottom: 2px solid transparent !important;
|
| 102 |
+
font-weight: 600 !important;
|
| 103 |
+
padding: 12px 20px !important;
|
| 104 |
+
transition: all 0.3s !important;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.tab-nav button.selected {
|
| 108 |
+
color: #2dd4bf !important;
|
| 109 |
+
border-bottom: 2px solid #0d9488 !important;
|
| 110 |
+
background: transparent !important;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.tab-nav button:hover:not(.selected) {
|
| 114 |
+
color: #f3f4f6 !important;
|
| 115 |
+
border-bottom: 2px solid rgba(13, 148, 136, 0.3) !important;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* Buttons Styling */
|
| 119 |
+
button.primary {
|
| 120 |
+
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%) !important;
|
| 121 |
+
color: #ffffff !important;
|
| 122 |
+
border: none !important;
|
| 123 |
+
font-weight: 600 !important;
|
| 124 |
+
border-radius: 8px !important;
|
| 125 |
+
padding: 12px 24px !important;
|
| 126 |
+
cursor: pointer !important;
|
| 127 |
+
box-shadow: 0 4px 14px 0 rgba(13, 148, 136, 0.3) !important;
|
| 128 |
+
transition: all 0.2s ease-in-out !important;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
button.primary:hover {
|
| 132 |
+
transform: translateY(-1px) !important;
|
| 133 |
+
box-shadow: 0 6px 20px 0 rgba(13, 148, 136, 0.5) !important;
|
| 134 |
+
background: linear-gradient(135deg, #0f766e 0%, #115e59 100%) !important;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
button.primary:active {
|
| 138 |
+
transform: translateY(1px) !important;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
button.secondary, button:not(.primary) {
|
| 142 |
+
background: rgba(31, 41, 55, 0.5) !important;
|
| 143 |
+
color: #e5e7eb !important;
|
| 144 |
+
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
| 145 |
+
font-weight: 500 !important;
|
| 146 |
+
border-radius: 8px !important;
|
| 147 |
+
padding: 12px 24px !important;
|
| 148 |
+
transition: all 0.2s !important;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
button:not(.primary):hover {
|
| 152 |
+
background: rgba(55, 65, 81, 0.7) !important;
|
| 153 |
+
border-color: rgba(255, 255, 255, 0.2) !important;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/* Visual Studio Client Terminal (Agent panel) */
|
| 157 |
+
.terminal-window {
|
| 158 |
+
background: #05070c !important;
|
| 159 |
+
border: 1px solid rgba(13, 148, 136, 0.25) !important;
|
| 160 |
+
border-radius: 12px !important;
|
| 161 |
+
padding: 1rem !important;
|
| 162 |
+
font-family: 'Fira Code', 'Courier New', Courier, monospace !important;
|
| 163 |
+
box-shadow: inset 0 0 10px rgba(0,0,0,0.8), 0 4px 20px rgba(0,0,0,0.5) !important;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.terminal-header {
|
| 167 |
+
display: flex;
|
| 168 |
+
align-items: center;
|
| 169 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
| 170 |
+
padding-bottom: 8px;
|
| 171 |
+
margin-bottom: 10px;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.dot {
|
| 175 |
+
width: 10px;
|
| 176 |
+
height: 10px;
|
| 177 |
+
border-radius: 50%;
|
| 178 |
+
margin-right: 6px;
|
| 179 |
+
display: inline-block;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.dot-red { background-color: #ef4444; }
|
| 183 |
+
.dot-yellow { background-color: #f59e0b; }
|
| 184 |
+
.dot-green { background-color: #10b981; }
|
| 185 |
+
|
| 186 |
+
.terminal-title {
|
| 187 |
+
color: #6b7280;
|
| 188 |
+
font-size: 0.75rem;
|
| 189 |
+
font-weight: 600;
|
| 190 |
+
margin-left: 8px;
|
| 191 |
+
letter-spacing: 0.05em;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/* Copy Card Widget styling */
|
| 195 |
+
.copy-card {
|
| 196 |
+
border-left: 4px solid #0d9488 !important;
|
| 197 |
+
background: rgba(13, 148, 136, 0.05) !important;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/* Hide standard Gradio footer for a clean product feel */
|
| 201 |
+
footer {
|
| 202 |
+
display: none !important;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* Progress indicator override */
|
| 206 |
+
.generating {
|
| 207 |
+
border-color: #0d9488 !important;
|
| 208 |
+
animation: pulse 1.5s infinite;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
@keyframes pulse {
|
| 212 |
+
0% { opacity: 0.6; }
|
| 213 |
+
50% { opacity: 1; }
|
| 214 |
+
100% { opacity: 0.6; }
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/* Custom UI Badges & Pills styling */
|
| 218 |
+
.badge-container {
|
| 219 |
+
display: flex;
|
| 220 |
+
flex-wrap: wrap;
|
| 221 |
+
justify-content: center;
|
| 222 |
+
gap: 8px;
|
| 223 |
+
margin-top: 14px;
|
| 224 |
+
margin-bottom: 8px;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.vessel-badge {
|
| 228 |
+
background: rgba(13, 148, 136, 0.15) !important;
|
| 229 |
+
border: 1px solid rgba(13, 148, 136, 0.3) !important;
|
| 230 |
+
color: #2dd4bf !important;
|
| 231 |
+
padding: 5px 14px !important;
|
| 232 |
+
border-radius: 9999px !important;
|
| 233 |
+
font-size: 0.75rem !important;
|
| 234 |
+
font-weight: 600 !important;
|
| 235 |
+
letter-spacing: 0.05em !important;
|
| 236 |
+
text-transform: uppercase;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.vessel-badge-orange {
|
| 240 |
+
background: rgba(249, 115, 22, 0.12) !important;
|
| 241 |
+
border: 1px solid rgba(249, 115, 22, 0.25) !important;
|
| 242 |
+
color: #fb923c !important;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/* Accordion instruction block overrides */
|
| 246 |
+
.instruction-box {
|
| 247 |
+
margin-top: 1rem !important;
|
| 248 |
+
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
| 249 |
+
border-radius: 12px !important;
|
| 250 |
+
background: rgba(17, 24, 39, 0.4) !important;
|
| 251 |
+
}
|
| 252 |
+
|
verify_code.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Import standard OS path features
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
# Import sys to handle runtime exits
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
# Import command-line argument parser utilities
|
| 8 |
+
import argparse
|
| 9 |
+
|
| 10 |
+
# Import subprocess to execute linter binaries
|
| 11 |
+
import subprocess
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def run_command(command: list, description: str) -> bool:
|
| 15 |
+
"""Runs a shell command and displays output with color markers."""
|
| 16 |
+
# Display the active task title
|
| 17 |
+
print(f"\n=== Running {description} ===")
|
| 18 |
+
# Print the exact command list parsed into a string
|
| 19 |
+
print(f"Command: {' '.join(command)}")
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
# Run binary in subprocess, capturing output text blocks
|
| 23 |
+
result = subprocess.run(command, capture_output=True, text=True)
|
| 24 |
+
# Check if the execution returned exit code zero (success)
|
| 25 |
+
if result.returncode == 0:
|
| 26 |
+
print("โ
SUCCESS")
|
| 27 |
+
# Print standard output if text is present
|
| 28 |
+
if result.stdout.strip():
|
| 29 |
+
print(result.stdout.strip())
|
| 30 |
+
return True
|
| 31 |
+
# Handle execution failure (non-zero exit code)
|
| 32 |
+
else:
|
| 33 |
+
print("โ FAILED")
|
| 34 |
+
# Print output logs to support troubleshooting
|
| 35 |
+
if result.stdout.strip():
|
| 36 |
+
print("\nStdout:")
|
| 37 |
+
print(result.stdout.strip())
|
| 38 |
+
# Print error logs if present
|
| 39 |
+
if result.stderr.strip():
|
| 40 |
+
print("\nStderr:")
|
| 41 |
+
print(result.stderr.strip())
|
| 42 |
+
return False
|
| 43 |
+
except FileNotFoundError:
|
| 44 |
+
# Log failure if linter binary cannot be found in virtual env
|
| 45 |
+
print(
|
| 46 |
+
f"โ ERROR: Command '{command[0]}' not found. Make sure dependencies are installed in your virtual environment."
|
| 47 |
+
)
|
| 48 |
+
return False
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def main():
|
| 52 |
+
# Setup CLI argument parser
|
| 53 |
+
parser = argparse.ArgumentParser(
|
| 54 |
+
description="Code hygiene and quality verification pipeline."
|
| 55 |
+
)
|
| 56 |
+
# Register the auto-fix parameter flag
|
| 57 |
+
parser.add_argument(
|
| 58 |
+
"--fix",
|
| 59 |
+
"-f",
|
| 60 |
+
action="store_true",
|
| 61 |
+
help="Attempt to auto-format files and auto-fix linter issues.",
|
| 62 |
+
)
|
| 63 |
+
# Parse incoming CLI command arguments
|
| 64 |
+
args = parser.parse_args()
|
| 65 |
+
|
| 66 |
+
# Resolve the absolute workspace directory of this checker script
|
| 67 |
+
workspace_dir = os.path.dirname(os.path.abspath(__file__))
|
| 68 |
+
# Shift current working directory to workspace base
|
| 69 |
+
os.chdir(workspace_dir)
|
| 70 |
+
|
| 71 |
+
# Resolve virtual environment bin executable directory path
|
| 72 |
+
venv_bin = os.path.join(workspace_dir, ".venv", "bin")
|
| 73 |
+
# Prepend virtual environment bin path to PATH to locate local ruff/pyright binaries
|
| 74 |
+
if os.path.exists(venv_bin):
|
| 75 |
+
os.environ["PATH"] = venv_bin + os.pathsep + os.environ.get("PATH", "")
|
| 76 |
+
|
| 77 |
+
# Execute auto-fixing parameters if flag is parsed
|
| 78 |
+
if args.fix:
|
| 79 |
+
print("๐ง Attempting to auto-correct issues...")
|
| 80 |
+
# Execute Ruff formatting in write mode
|
| 81 |
+
fmt_passed = run_command(
|
| 82 |
+
["ruff", "format", "."], "Ruff Code Formatting (Auto-fixing)"
|
| 83 |
+
)
|
| 84 |
+
# Execute Ruff linter in auto-fix write mode
|
| 85 |
+
lint_passed = run_command(
|
| 86 |
+
["ruff", "check", "--fix", "."], "Ruff Linter (Auto-fixing)"
|
| 87 |
+
)
|
| 88 |
+
# Execute default validation parameters (dry run)
|
| 89 |
+
else:
|
| 90 |
+
# Execute Ruff formatting in validation check-only mode
|
| 91 |
+
fmt_passed = run_command(
|
| 92 |
+
["ruff", "format", "--check", "."], "Ruff Code Formatting Validation"
|
| 93 |
+
)
|
| 94 |
+
# Execute Ruff linter in check-only validation mode
|
| 95 |
+
lint_passed = run_command(["ruff", "check", "."], "Ruff Linter Validation")
|
| 96 |
+
|
| 97 |
+
# Run Pyright static type checker against the codebase
|
| 98 |
+
type_passed = run_command(["pyright", "."], "Pyright Static Type Validation")
|
| 99 |
+
|
| 100 |
+
# Evaluate if all checkers completed successfully
|
| 101 |
+
if fmt_passed and lint_passed and type_passed:
|
| 102 |
+
print(
|
| 103 |
+
"\n๐ CODEBASE IS PRISTINE: Formatting is correct, lint checks passed, and type signatures are valid!"
|
| 104 |
+
)
|
| 105 |
+
# Exit with success code
|
| 106 |
+
sys.exit(0)
|
| 107 |
+
else:
|
| 108 |
+
# Report failure if any validation check fails
|
| 109 |
+
print(
|
| 110 |
+
"\nโ ๏ธ CODE BASE ISSUES FOUND: Please resolve the formatting, lint, or type signatures issues listed above."
|
| 111 |
+
)
|
| 112 |
+
# Suggest auto-fix parameter tip if running in dry-run mode
|
| 113 |
+
if not args.fix:
|
| 114 |
+
print(
|
| 115 |
+
"๐ก TIP: Run 'python verify_code.py --fix' to automatically resolve formatting and simple linter warnings."
|
| 116 |
+
)
|
| 117 |
+
# Exit with failure code
|
| 118 |
+
sys.exit(1)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# Execute checks when running checker script directly
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
main()
|