Skip to content
Avi-Rana-1718Public

About

Bet on yourself — talk to the hand. Chat-only habit tracker with a cheeky AI accountability persona and virtual "fingers" as stakes.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

🖐️ OffHand

Bet on yourself. Talk to the hand.

Python FastAPI MongoDB LangChain uv PWA

Quick Start · API · Architecture · Structure · Development


🎯 What it does

OffHand is a habit-accountability app with exactly one interaction: you talk to The Challenger, a cheeky hand persona, and it does the rest. You say "bet 100 fingers on the run tomorrow"; the LLM calls create_bet and the wager is on record. Later you say you did it; it calls update_bet and your balance moves.

There are no forms, no stake pickers, no confirm buttons — the tool calls are the UI. The backend is API-only; the frontend is static HTML/CSS/JS with no build step, installable as a PWA.

OffHand landing page OffHand chat with The Challenger

Important

Fingers are only credited, never debited on placement. create_bet (src/service/bet_service.py:10) writes the bet but does not touch User.balance. Balance changes only inside update_bet when a bet is marked complete (balance += amount + counter_bet) or reopened. The system prompt tells the model that stakes are deducted at creation time — the code does not do that yet.

Warning

Passwords are stored and compared in plaintext (src/service/user_service.py:24), and GET /user/{user_id} returns the whole User document, password field included. CORS is allow_origins=["*"]. Hobby stage — do not point this at real users.


✨ Features

💬 Chat-driven betting Four LangChain tools (create_bet, get_all_bets, get_bet, update_bet) bound to Gemini; the model places and settles bets itself.
🧠 Per-session memory MongoDBChatMessageHistory in the chat_history collection, keyed by session_id. The last 3 stored messages are replayed into each prompt.
🔒 user_id never reaches the LLM The route stashes it in a ContextVar; tools read it from there, so the model cannot act on another user's bets.
🎙️ Voice in, voice out webkitSpeechRecognition for input, speechSynthesis for replies, with a system voice picker in the menu.
📱 Installable PWA Manifest + service worker (network-first for HTML, cache-first for assets); the landing page redirects to /app.html when launched standalone.
📝 Markdown replies marked + DOMPurify render the bubble; TTS reads the stripped innerText.

🏗️ Architecture

sequenceDiagram
    participant UI as ui/app.html
    participant R as chatbot_router
    participant CV as current_user_id ContextVar
    participant L as call_llm
    participant G as Gemini
    participant T as bet_service tools
    participant M as MongoDB

    UI->>R: POST /chatbot/ (text body + user_id, session_id headers)
    R->>CV: set(user_id)
    R->>L: call_llm(text, session_id)
    L->>M: load chat_history for session_id
    L->>G: SYSTEM_PROMPT + last 3 msgs + input
    G-->>L: tool_calls
    L->>T: ainvoke(args)
    T->>CV: get() to scope the query
    T->>M: read or write bets and users
    T-->>L: tool result
    L->>G: results appended, invoke again
    G-->>L: final text
    L->>M: append user and AI message
    L-->>UI: reply string
    UI->>R: GET /user/{id} to refresh balance
Loading
Piece Concrete component Store
Chat entry src/api/chatbot_router.py —
Tool loop src/ai/llm.py → ChatGoogleGenerativeAI —
Bet tools src/service/bet_service.py bets
Auth / profile src/api/user_router.py → user_service.py users
Memory MongoDBChatMessageHistory chat_history

All three collections live in the offhand database (src/config/database.py).


🚀 Quick Start

Prerequisites: Python 3.13 (see .python-version), uv, a MongoDB you can reach, and a Google AI Studio API key.

git clone https://github.com/Avi-Rana-1718/Offhand.git
cd Offhand
uv sync

1. Environment — all three are required and have no defaults; the app fails at import if any is missing (src/config/setting.py). Create .env in the repo root:

MODEL=gemini-2.5-flash
API_KEY=<google-ai-studio-key>
MONGODB_URL=mongodb://localhost:27017

Note

There is no docker-compose.yaml in this repo. MongoDB is your responsibility — run one locally or point MONGODB_URL at Atlas. Collections and indexes are created by Beanie on first boot.

2. Backend — the makefile is named MakeFile, which GNU make does not auto-discover on case-sensitive filesystems, so pass it explicitly (on Windows plain make run works):

make -f MakeFile run     # uv run uvicorn main:app --reload --app-dir src

Listens on http://localhost:8000 (uvicorn's default — no port is set anywhere in the code).

3. Frontend — separate process, static files only:

make -f MakeFile ui      # python -m http.server 5173 --directory ui

Open http://localhost:5173, sign up in the dialog, and start talking. ui/app.html hardcodes const API = "http://localhost:8000" at line 432 — change it there to target a deployed backend.

4. Verify without the UI:

curl -s -X POST localhost:8000/user/signup \
  -H 'Content-Type: application/json' \
  -d '{"email":"a@b.c","password":"pw","username":"avi"}'
# {"user_id":"1f0c...uuid4...","username":"avi"}

curl -s -X POST localhost:8000/chatbot/ \
  -H 'Content-Type: text/plain' \
  -H 'user_id: <user_id from above>' \
  -H 'session_id: any-string' \
  --data 'bet 100 fingers on running tomorrow'
# "Locked in. **run tomorrow** — 100 fingers. Don't make me regret this."

📡 API

Chat
Method Path Body Headers
POST /chatbot/ raw text/plain — the user's message user_id, session_id (both required, 400 otherwise)

Returns the reply as a JSON-encoded string. There is no request model — the route reads await request.body() directly.

Users
Method Path Body Returns
POST /user/signup {email, password, username} {user_id, username} — 400 if the email exists
POST /user/signin {email, password} {user_id, username} — 401 on mismatch
GET /user/{user_id} — the full User document — 404 if unknown

update_user_about exists in the service layer but is not routed.

LLM tools (not HTTP)
Tool Args Effect
create_bet name, amount, counter_bet=0 Inserts a bet for the ContextVar user. Balance untouched.
get_all_bets completed Bets for the user with that status; 404 if none.
get_bet bet_id One bet, scoped to the user.
update_bet bet_id, name?, amount?, completed? Edits fields; on completed=True credits amount + counter_bet and debits counter_bet from hands_balance.

Bet.expires_at exists on the model but nothing ever sets it — no expiry or auto-forfeit yet.


🧱 Tech stack

Layer Choice
API FastAPI ≥0.137.1, Uvicorn ≥0.49.0
ODM Beanie 1.26.0 + Motor ≥3.7.1 — IDs are uuid4 strings, not ObjectIds (base_entity.py)
LLM langchain-google-genai ≥4.2.6, model from $MODEL
Memory langchain-mongodb ≥0.11.0
Config pydantic-settings ≥2.14.2
Frontend vanilla JS, Space Grotesk + Inter, Ionicons 7.1.0, marked 12.0.2, DOMPurify 3.1.6 (all CDN)
Tooling uv, ruff ≥0.15.17

bcrypt and google-genai are declared in pyproject.toml but imported nowhere in src/.


📁 Project structure

src/
  main.py                 FastAPI app, CORS, lifespan-managed Mongo client
  api/chatbot_router.py   POST /chatbot/
  api/user_router.py      POST /user/signup, /user/signin, GET /user/{id}
  ai/llm.py               tool loop over Gemini
  ai/prompts.py           SYSTEM_PROMPT() — takes no args
  ai/context.py           current_user_id ContextVar
  service/bet_service.py  the four @tool functions
  service/user_service.py plain async helpers (not LLM tools)
  model/                  BaseEntity, User, Bet
  config/                 Settings, init_db
ui/
  index.html              landing page, redirects to app.html when standalone
  app.html                chat UI, auth dialog, mic, TTS
  manifest.webmanifest    start_url = /app.html
  sw.js                   cache "offhand-v2"
  redesign.md             original design notes
Port Process
8000 FastAPI (uvicorn default)
5173 python -m http.server serving ui/
27017 MongoDB, if local

🛠️ Development

make -f MakeFile lint     # uv run ruff check .
uv run ruff format .      # ruff configured: line-length 100, select E/F/I

No test suite and no CI workflows in the repo — nothing under tests/ and no .github/. Bumping CACHE in ui/sw.js forces the service worker to reactivate after frontend edits.


🗺️ Roadmap

  • Deduct fingers at bet creation, matching what the prompt already claims
  • Populate expires_at and auto-forfeit expired bets
  • Hash passwords; stop returning the password field from GET /user/{id}
  • Wire update_user_about to a route
  • Tighten CORS before any deploy

📄 License

No license file in the repo yet — all rights reserved by default until one is added.

About

Bet on yourself — talk to the hand. Chat-only habit tracker with a cheeky AI accountability persona and virtual "fingers" as stakes.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages