whatsmynote 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sarthak Jha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: whatsmynote
3
+ Version: 0.1.0
4
+ Summary: A CLI app for managing notes and finances using LLMs.
5
+ Author-email: Sarthak Jha <sargedevx@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/SarJ2004/WhatsMyNote
8
+ Project-URL: Repository, https://github.com/SarJ2004/WhatsMyNote
9
+ Keywords: cli,llm,finance,notes
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: python-dotenv>=0.9.9
17
+ Requires-Dist: inquirerpy>=0.3.4
18
+ Requires-Dist: plotext>=5.3.2
19
+ Requires-Dist: rich>=13.9.4
20
+ Requires-Dist: termcharts>=1.1.2
21
+ Requires-Dist: platformdirs>=4.2.2
22
+ Requires-Dist: supabase>=2.3.0
23
+ Requires-Dist: requests>=2.31.0
24
+ Requires-Dist: fastapi[standard]>=0.139.0
25
+ Requires-Dist: uvicorn>=0.51.0
26
+ Requires-Dist: twine>=6.2.0
27
+ Dynamic: license-file
28
+
29
+ # WhatsMyNote
30
+
31
+ WhatsMyNote is a natural-language personal finance assistant for logging and querying lending, expenses, income, and transfers. It uses advanced **Llama 3 LLMs** to extract structured data, store it in PostgreSQL (Supabase), and answer your complex queries accurately.
32
+
33
+ ## 🚀 Bring Your Own Key (BYOK) Architecture
34
+ WhatsMyNote now supports a secure **BYOK architecture**. You bring your own **Groq API key** to power the inference engine locally and securely.
35
+
36
+ On your first run, the app will prompt you for your `GROQ_API_KEY`. It is stored securely in a local `.env` file and is never exposed. The application uses state-of-the-art Groq models:
37
+ - **`llama-3.3-70b-versatile`** for reasoning, intent classification, and SQL generation.
38
+ - **`llama-3.1-8b-instant`** for fast and efficient data extraction.
39
+
40
+ ---
41
+
42
+ ## 🛠 What It Supports & Examples
43
+
44
+ ### 📉 Expenses
45
+ Track where your money is going.
46
+ - Create expense records with amount, category, merchant, payment source, item, and notes.
47
+ - **Example:** `I spent 320 on groceries at Walmart using HDFC`
48
+ - **Example:** `Update my last grocery expense amount to 400`
49
+ - **Example:** `Delete the Walmart expense`
50
+ - **Example:** `Show my highest expenses this month`
51
+
52
+ ### 💰 Income
53
+ Record earnings, cashback, or refunds.
54
+ - **Example:** `I received 50000 as salary from Acme`
55
+ - **Example:** `Update my salary entry notes to include the July bonus`
56
+ - **Example:** `How much income did I receive from salary sources this year?`
57
+
58
+ ### 🤝 Lending & Borrowing
59
+ Keep track of who owes you, and who you owe.
60
+ - **Example:** `I lent Sumit 500 today`
61
+ - **Example:** `Update Sumit's loan amount to 700`
62
+ - **Example:** `How much money have I lent to Sumit?`
63
+
64
+ ### 🔁 Transfers
65
+ Record money moving between your own accounts.
66
+ - **Example:** `I transferred 3000 from HDFC to SBI`
67
+ - **Example:** `Change the transfer destination to ICICI`
68
+ - **Example:** `Show all transfers from HDFC this month`
69
+
70
+ ### 📊 Budgets and Balances
71
+ Track your overall financial health.
72
+ - Set opening balances for your accounts (e.g., cash, wallet, SBI).
73
+ - Track category budgets (e.g., groceries, transport) or an `Overall` limit.
74
+ - **Example:** `Set opening balance for SBI to 50000`
75
+ - **Example:** `Set monthly budget for groceries to 10000`
76
+ - **Example:** `What is my current balance in SBI?`
77
+ - **Example:** `How much budget do I have left for groceries this month?`
78
+
79
+ ### 📈 Analytics (Read-Only)
80
+ Run advanced queries without touching write access.
81
+ - Safe SQL generation with automated validation.
82
+ - Prefix queries with `sql:` for advanced read-only raw queries.
83
+ - **Example:** `What is my total spending this month?`
84
+ - **Example:** `Compare my income and expense totals for July`
85
+ - **Example:** `What is my net worth estimate?`
86
+
87
+ ---
88
+
89
+ ## ⚙️ Environment Setup
90
+
91
+ Install dependencies and start your virtual environment:
92
+
93
+ ```bash
94
+ uv venv
95
+ .venv\Scripts\activate
96
+ uv pip install -r requirements.txt
97
+ ```
98
+
99
+ Set up your database connection string in your `.env` (we recommend a **Supabase PostgreSQL** instance):
100
+ ```env
101
+ DATABASE_URL=postgresql+psycopg2://postgres:YOUR_PASSWORD@db.YOUR_SUPABASE_REF.supabase.co:5432/postgres
102
+ ```
103
+
104
+ Run the app. It will prompt you for your `GROQ_API_KEY` on the first launch!
105
+ ```bash
106
+ python app/main.py
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 🏗 Tech Stack
112
+
113
+ - **Python** for core logic.
114
+ - **LangGraph & LangChain** for robust stateful intent routing and HITL (Human-in-the-Loop) flows.
115
+ - **Pydantic** for extraction validation.
116
+ - **SQLAlchemy & PostgreSQL** for data persistence.
117
+ - **Groq LLMs (Llama 3)** for lightning-fast, high-quality inference.
118
+
119
+ ---
120
+
121
+ ## 🚀 Vision
122
+ WhatsMyNote aims to become a fully conversational financial memory system, helping you control your lending, borrowing, expenses, income, and transfers through pure natural language.
@@ -0,0 +1,94 @@
1
+ # WhatsMyNote
2
+
3
+ WhatsMyNote is a natural-language personal finance assistant for logging and querying lending, expenses, income, and transfers. It uses advanced **Llama 3 LLMs** to extract structured data, store it in PostgreSQL (Supabase), and answer your complex queries accurately.
4
+
5
+ ## 🚀 Bring Your Own Key (BYOK) Architecture
6
+ WhatsMyNote now supports a secure **BYOK architecture**. You bring your own **Groq API key** to power the inference engine locally and securely.
7
+
8
+ On your first run, the app will prompt you for your `GROQ_API_KEY`. It is stored securely in a local `.env` file and is never exposed. The application uses state-of-the-art Groq models:
9
+ - **`llama-3.3-70b-versatile`** for reasoning, intent classification, and SQL generation.
10
+ - **`llama-3.1-8b-instant`** for fast and efficient data extraction.
11
+
12
+ ---
13
+
14
+ ## 🛠 What It Supports & Examples
15
+
16
+ ### 📉 Expenses
17
+ Track where your money is going.
18
+ - Create expense records with amount, category, merchant, payment source, item, and notes.
19
+ - **Example:** `I spent 320 on groceries at Walmart using HDFC`
20
+ - **Example:** `Update my last grocery expense amount to 400`
21
+ - **Example:** `Delete the Walmart expense`
22
+ - **Example:** `Show my highest expenses this month`
23
+
24
+ ### 💰 Income
25
+ Record earnings, cashback, or refunds.
26
+ - **Example:** `I received 50000 as salary from Acme`
27
+ - **Example:** `Update my salary entry notes to include the July bonus`
28
+ - **Example:** `How much income did I receive from salary sources this year?`
29
+
30
+ ### 🤝 Lending & Borrowing
31
+ Keep track of who owes you, and who you owe.
32
+ - **Example:** `I lent Sumit 500 today`
33
+ - **Example:** `Update Sumit's loan amount to 700`
34
+ - **Example:** `How much money have I lent to Sumit?`
35
+
36
+ ### 🔁 Transfers
37
+ Record money moving between your own accounts.
38
+ - **Example:** `I transferred 3000 from HDFC to SBI`
39
+ - **Example:** `Change the transfer destination to ICICI`
40
+ - **Example:** `Show all transfers from HDFC this month`
41
+
42
+ ### 📊 Budgets and Balances
43
+ Track your overall financial health.
44
+ - Set opening balances for your accounts (e.g., cash, wallet, SBI).
45
+ - Track category budgets (e.g., groceries, transport) or an `Overall` limit.
46
+ - **Example:** `Set opening balance for SBI to 50000`
47
+ - **Example:** `Set monthly budget for groceries to 10000`
48
+ - **Example:** `What is my current balance in SBI?`
49
+ - **Example:** `How much budget do I have left for groceries this month?`
50
+
51
+ ### 📈 Analytics (Read-Only)
52
+ Run advanced queries without touching write access.
53
+ - Safe SQL generation with automated validation.
54
+ - Prefix queries with `sql:` for advanced read-only raw queries.
55
+ - **Example:** `What is my total spending this month?`
56
+ - **Example:** `Compare my income and expense totals for July`
57
+ - **Example:** `What is my net worth estimate?`
58
+
59
+ ---
60
+
61
+ ## ⚙️ Environment Setup
62
+
63
+ Install dependencies and start your virtual environment:
64
+
65
+ ```bash
66
+ uv venv
67
+ .venv\Scripts\activate
68
+ uv pip install -r requirements.txt
69
+ ```
70
+
71
+ Set up your database connection string in your `.env` (we recommend a **Supabase PostgreSQL** instance):
72
+ ```env
73
+ DATABASE_URL=postgresql+psycopg2://postgres:YOUR_PASSWORD@db.YOUR_SUPABASE_REF.supabase.co:5432/postgres
74
+ ```
75
+
76
+ Run the app. It will prompt you for your `GROQ_API_KEY` on the first launch!
77
+ ```bash
78
+ python app/main.py
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 🏗 Tech Stack
84
+
85
+ - **Python** for core logic.
86
+ - **LangGraph & LangChain** for robust stateful intent routing and HITL (Human-in-the-Loop) flows.
87
+ - **Pydantic** for extraction validation.
88
+ - **SQLAlchemy & PostgreSQL** for data persistence.
89
+ - **Groq LLMs (Llama 3)** for lightning-fast, high-quality inference.
90
+
91
+ ---
92
+
93
+ ## 🚀 Vision
94
+ WhatsMyNote aims to become a fully conversational financial memory system, helping you control your lending, borrowing, expenses, income, and transfers through pure natural language.
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools.packages.find]
6
+ where = ["."]
7
+ include = ["whatsmynote*"]
8
+
9
+ [project]
10
+ name = "whatsmynote"
11
+ version = "0.1.0"
12
+ description = "A CLI app for managing notes and finances using LLMs."
13
+ readme = "README.md"
14
+ requires-python = ">=3.11"
15
+ authors = [
16
+ { name = "Sarthak Jha", email = "sargedevx@gmail.com" }
17
+ ]
18
+ license = { text = "MIT" }
19
+ keywords = ["cli", "llm", "finance", "notes"]
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+
26
+ dependencies = [
27
+ "python-dotenv>=0.9.9",
28
+ "inquirerpy>=0.3.4",
29
+ "plotext>=5.3.2",
30
+ "rich>=13.9.4",
31
+ "termcharts>=1.1.2",
32
+ "platformdirs>=4.2.2",
33
+ "supabase>=2.3.0",
34
+ "requests>=2.31.0",
35
+ "fastapi[standard]>=0.139.0",
36
+ "uvicorn>=0.51.0",
37
+ "twine>=6.2.0",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/SarJ2004/WhatsMyNote"
42
+ Repository = "https://github.com/SarJ2004/WhatsMyNote"
43
+
44
+ [project.scripts]
45
+ whatsmynote = "whatsmynote.app.chat:main"
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "langgraph>=0.2.6",
50
+ "langchain-groq>=0.2.1",
51
+ "sqlalchemy>=2.0.36",
52
+ "psycopg2-binary>=2.9.10",
53
+ ]
54
+
55
+ # Trigger PyPI release for v0.1.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1 @@
1
+ # App module — CLI application layer
@@ -0,0 +1,150 @@
1
+ from rich.console import Console
2
+ from rich.prompt import Prompt
3
+ from supabase import create_client, Client
4
+ import os
5
+ import json
6
+ import webbrowser
7
+ from urllib.parse import urlparse, parse_qs
8
+ from http.server import BaseHTTPRequestHandler, HTTPServer
9
+ import threading
10
+
11
+ console = Console()
12
+ from whatsmynote.app.config import get_session_path, SUPABASE_URL, SUPABASE_KEY
13
+ SESSION_FILE = get_session_path()
14
+
15
+ def _save_session(session):
16
+ if session:
17
+ try:
18
+ with open(SESSION_FILE, "w") as f:
19
+ f.write(session.model_dump_json())
20
+ except Exception:
21
+ pass
22
+ else:
23
+ if os.path.exists(SESSION_FILE):
24
+ try:
25
+ os.remove(SESSION_FILE)
26
+ except Exception:
27
+ pass
28
+
29
+ def _load_session():
30
+ supabase = get_supabase()
31
+ if os.path.exists(SESSION_FILE):
32
+ try:
33
+ with open(SESSION_FILE, "r") as f:
34
+ data = json.load(f)
35
+ if 'access_token' in data and 'refresh_token' in data:
36
+ res = supabase.auth.set_session(
37
+ access_token=data['access_token'],
38
+ refresh_token=data['refresh_token']
39
+ )
40
+ return res.user
41
+ except Exception:
42
+ pass
43
+ return None
44
+
45
+ _supabase: Client = None
46
+
47
+ def get_supabase() -> Client:
48
+ global _supabase
49
+ if _supabase is None:
50
+ url = SUPABASE_URL
51
+ key = SUPABASE_KEY
52
+ if not url or not key:
53
+ console.print("[red]Supabase URL and Key must be set in .env[/red]")
54
+ exit(1)
55
+ _supabase = create_client(url, key)
56
+ return _supabase
57
+
58
+ def ensure_authenticated():
59
+ supabase = get_supabase()
60
+
61
+ # Try restoring previous session
62
+ user = _load_session()
63
+ if user:
64
+ os.environ["CURRENT_USER_ID"] = user.id
65
+ console.print(f"[green]Restored session for {user.email}[/green]")
66
+ return
67
+
68
+ console.print("\n[bold blue]WhatsMyNote - Supabase Authentication[/bold blue]")
69
+ while True:
70
+ action = Prompt.ask("Do you want to (L)og in, (S)ign up, or (O)Auth [Google/GitHub]?", choices=["l", "s", "o", "q"], default="l").lower()
71
+ if action == "q":
72
+ exit(0)
73
+
74
+ if action == "o":
75
+ provider = Prompt.ask("Which provider?", choices=["google", "github"], default="google").lower()
76
+
77
+ # Setup a one-time local server to receive the callback
78
+ auth_code = [None]
79
+
80
+ class OAuthCallbackHandler(BaseHTTPRequestHandler):
81
+ def do_GET(self):
82
+ parsed_path = urlparse(self.path)
83
+ query_params = parse_qs(parsed_path.query)
84
+
85
+ if 'code' in query_params:
86
+ auth_code[0] = query_params['code'][0]
87
+ self.send_response(200)
88
+ self.send_header("Content-type", "text/html")
89
+ self.end_headers()
90
+ self.wfile.write(b"<html><body><h1>Authentication Successful!</h1><p>You can close this tab and return to the terminal.</p></body></html>")
91
+ else:
92
+ self.send_response(400)
93
+ self.send_header("Content-type", "text/html")
94
+ self.end_headers()
95
+ self.wfile.write(b"<html><body><h1>Authentication Failed!</h1><p>Missing auth code.</p></body></html>")
96
+
97
+ def log_message(self, format, *args):
98
+ pass # Silence server logs
99
+
100
+ server = HTTPServer(('localhost', 8080), OAuthCallbackHandler)
101
+
102
+ try:
103
+ res = supabase.auth.sign_in_with_oauth({
104
+ "provider": provider,
105
+ "options": {
106
+ "skip_browser_redirect": True,
107
+ "redirect_to": "http://localhost:8080"
108
+ }
109
+ })
110
+
111
+ console.print(f"[dim]Opening browser to authenticate with {provider.capitalize()}...[/dim]")
112
+ webbrowser.open(res.url)
113
+
114
+ # Wait for exactly one request
115
+ server.handle_request()
116
+
117
+ if auth_code[0]:
118
+ supabase.auth.exchange_code_for_session({"auth_code": auth_code[0]})
119
+ console.print(f"[green]Successfully authenticated via {provider.capitalize()}![/green]")
120
+ break
121
+ else:
122
+ console.print("[red]Authentication failed: No code received.[/red]")
123
+ except Exception as e:
124
+ console.print(f"[red]OAuth failed: {str(e)}[/red]")
125
+ finally:
126
+ server.server_close()
127
+ continue
128
+
129
+ email = Prompt.ask("Email")
130
+ password = Prompt.ask("Password", password=True)
131
+
132
+ try:
133
+ if action == "s":
134
+ res = supabase.auth.sign_up({"email": email, "password": password})
135
+ console.print("[green]Sign up successful! You can now log in.[/green]")
136
+ if res.user:
137
+ break
138
+ elif action == "l":
139
+ res = supabase.auth.sign_in_with_password({"email": email, "password": password})
140
+ console.print(f"[green]Logged in successfully as {res.user.email}[/green]")
141
+ break
142
+ except Exception as e:
143
+ console.print(f"[red]Authentication failed: {str(e)}[/red]")
144
+
145
+ # Save the user ID in global state and persist to file
146
+ session = supabase.auth.get_session()
147
+ if session and session.user:
148
+ _save_session(session)
149
+ # We store it globally so config.py can inject it into SQLAlchemy
150
+ os.environ["CURRENT_USER_ID"] = session.user.id
@@ -0,0 +1,131 @@
1
+ """Chat loop — the main interactive CLI with HITL support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.prompt import Prompt
8
+
9
+ from whatsmynote.app.render import render_result
10
+ from whatsmynote.app.setup import ensure_initial_setup
11
+ from whatsmynote.app.selector import display_search_results
12
+
13
+ console = Console()
14
+
15
+ # Global state to persist between turns
16
+ app_state = {}
17
+
18
+ def _send_state() -> dict:
19
+ global app_state
20
+ import requests
21
+ from whatsmynote.app.config import API_URL, get_groq_api_key
22
+ from whatsmynote.app.auth import get_supabase
23
+
24
+ session = get_supabase().auth.get_session()
25
+ token = session.access_token if session else ""
26
+
27
+ headers = {
28
+ "Authorization": f"Bearer {token}",
29
+ "X-Groq-Api-Key": get_groq_api_key() or ""
30
+ }
31
+ payload = {"message": app_state.get("raw_text", ""), "state": app_state}
32
+
33
+ try:
34
+ response = requests.post(f"{API_URL}/chat", json=payload, headers=headers)
35
+ response.raise_for_status()
36
+ app_state = response.json()["state"]
37
+ except Exception as e:
38
+ console.print(f"[red]Error communicating with backend: {e}[/red]")
39
+
40
+ return app_state
41
+
42
+ def _invoke(message: str) -> dict:
43
+ """Invoke the stateless graph via backend API."""
44
+ global app_state
45
+ app_state["raw_text"] = message
46
+ return _send_state()
47
+ def run_chat_loop() -> None:
48
+ console.print(
49
+ Panel.fit(
50
+ "[bold]WhatsMyNote CLI[/bold]\nType finance messages. Use [bold]exit[/bold] or [bold]quit[/bold] to stop.",
51
+ border_style="blue",
52
+ )
53
+ )
54
+
55
+ global app_state
56
+
57
+ while True:
58
+ message = Prompt.ask("[bold blue]You[/bold blue]")
59
+ msg_lower = message.strip().lower()
60
+ if msg_lower in {"exit", "quit", "q"}:
61
+ console.print("[dim]Bye.[/dim]")
62
+ break
63
+ elif msg_lower == "/clear":
64
+ console.clear()
65
+ continue
66
+ elif msg_lower == "/config":
67
+ from whatsmynote.app.config import set_groq_api_key
68
+ new_key = Prompt.ask("Enter new Groq API Key", password=True)
69
+ if new_key:
70
+ set_groq_api_key(new_key.strip())
71
+ console.print("[green]API Key updated successfully.[/green]")
72
+ else:
73
+ console.print("[yellow]API Key update cancelled.[/yellow]")
74
+ continue
75
+ elif msg_lower == "/logout":
76
+ from whatsmynote.app.auth import get_supabase, SESSION_FILE
77
+ import os
78
+ try:
79
+ get_supabase().auth.sign_out()
80
+ if os.path.exists(SESSION_FILE):
81
+ os.remove(SESSION_FILE)
82
+ if "CURRENT_USER_ID" in os.environ:
83
+ del os.environ["CURRENT_USER_ID"]
84
+ console.print("[green]Logged out successfully.[/green]")
85
+ except Exception as e:
86
+ console.print(f"[red]Error logging out: {e}[/red]")
87
+ break
88
+
89
+ state = _invoke(message)
90
+
91
+ # HITL: If the system needs the user to select a record
92
+ if state.get("awaiting_selection") and state.get("search_results"):
93
+ render_result(state) # Show the "please select" message
94
+
95
+ record_type = state.get("record_type", "record")
96
+ intent = state.get("intent")
97
+
98
+ selected_result = display_search_results(
99
+ state["search_results"], record_type, multiselect=(intent == "delete")
100
+ )
101
+
102
+ if selected_result:
103
+ # Set selection and manually re-invoke
104
+ app_state["awaiting_selection"] = False
105
+ if isinstance(selected_result, list):
106
+ app_state["raw_text"] = f"selected multiple: {selected_result}"
107
+ app_state["selected_record_ids"] = selected_result
108
+ app_state["selected_record_id"] = None
109
+ else:
110
+ app_state["raw_text"] = f"select {selected_result}"
111
+ app_state["selected_record_id"] = selected_result
112
+ app_state["selected_record_ids"] = None
113
+
114
+ state = _send_state()
115
+ app_state = state
116
+ render_result(state)
117
+ else:
118
+ console.print("[dim]Operation cancelled.[/dim]")
119
+ continue
120
+
121
+ render_result(state)
122
+
123
+
124
+ def main() -> None:
125
+ from whatsmynote.app.config import ensure_env_config
126
+ ensure_env_config()
127
+ from whatsmynote.app.auth import ensure_authenticated
128
+ ensure_authenticated()
129
+ from whatsmynote.app.setup import ensure_initial_setup
130
+ ensure_initial_setup()
131
+ run_chat_loop()
@@ -0,0 +1,70 @@
1
+ """CLI Configuration."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from platformdirs import user_data_dir
6
+
7
+ # --- Replace these before publishing ---
8
+ API_URL = "https://whatsmynote.onrender.com"
9
+ SUPABASE_URL = "https://emcdruetkqkrplrrxqbm.supabase.co"
10
+ SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImVtY2RydWV0a3FrcnBscnJ4cWJtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODM3NjQ1NTksImV4cCI6MjA5OTM0MDU1OX0.-GRm3ZhaFGDGyy8ocDq0tAov0CxCOwCK9K9JeWPTCvY"
11
+ # ---------------------------------------
12
+
13
+ APP_NAME = "whatsmynote"
14
+ APP_AUTHOR = "whatsmynote"
15
+
16
+ def get_data_dir() -> Path:
17
+ """Return the user data directory for the application."""
18
+ data_dir = Path(user_data_dir(APP_NAME, APP_AUTHOR))
19
+ data_dir.mkdir(parents=True, exist_ok=True)
20
+ return data_dir
21
+
22
+ def get_env_path() -> Path:
23
+ return get_data_dir() / "config.env"
24
+
25
+ def get_session_path() -> Path:
26
+ """Return the path to the Supabase session file."""
27
+ return get_data_dir() / "session.json"
28
+
29
+ def get_groq_api_key() -> str | None:
30
+ from dotenv import load_dotenv
31
+ load_dotenv(get_env_path())
32
+ return os.environ.get("GROQ_API_KEY")
33
+
34
+ def set_groq_api_key(api_key: str) -> None:
35
+ env_path = get_env_path()
36
+ content = ""
37
+ if env_path.exists():
38
+ content = env_path.read_text()
39
+
40
+ lines = [line for line in content.splitlines() if not line.startswith("GROQ_API_KEY=")]
41
+ lines.append(f"GROQ_API_KEY={api_key}")
42
+ env_path.write_text("\n".join(lines) + "\n")
43
+ os.environ["GROQ_API_KEY"] = api_key
44
+
45
+ def ensure_env_config() -> None:
46
+ from dotenv import load_dotenv
47
+ from rich.console import Console
48
+ from rich.prompt import Prompt
49
+
50
+ env_path = get_env_path()
51
+ console = Console()
52
+
53
+ if not env_path.exists():
54
+ env_path.touch(mode=0o600, exist_ok=True)
55
+
56
+ load_dotenv(env_path)
57
+
58
+ if not os.environ.get("GROQ_API_KEY"):
59
+ console.print("[bold yellow]First time setup: Missing required configuration[/bold yellow]")
60
+ val = Prompt.ask("Enter GROQ_API_KEY", password=True)
61
+ if not val:
62
+ console.print("[red]GROQ_API_KEY is required. Exiting.[/red]")
63
+ exit(1)
64
+
65
+ content = env_path.read_text()
66
+ lines = [line for line in content.splitlines() if not line.startswith("GROQ_API_KEY=")]
67
+ lines.append(f"GROQ_API_KEY={val}")
68
+ env_path.write_text("\n".join(lines) + "\n")
69
+ os.environ["GROQ_API_KEY"] = val
70
+ console.print("[green]Configuration saved successfully.[/green]")
@@ -0,0 +1,15 @@
1
+ """WhatsMyNote CLI — single entry point."""
2
+ import sys
3
+ import os
4
+
5
+ if sys.platform == "win32":
6
+ # Force UTF-8 encoding for rich/plotext on Windows legacy terminals
7
+ try:
8
+ sys.stdout.reconfigure(encoding='utf-8')
9
+ except Exception:
10
+ pass
11
+
12
+ from whatsmynote.app.chat import main
13
+
14
+ if __name__ == "__main__":
15
+ main()
@@ -0,0 +1,221 @@
1
+ """Rich output rendering for CLI results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ console = Console()
13
+
14
+
15
+ def _normalize_item(item: Any) -> Any:
16
+ if hasattr(item, "__dict__"):
17
+ return {
18
+ key: value
19
+ for key, value in vars(item).items()
20
+ if not key.startswith("_") and key != "record"
21
+ }
22
+ return item
23
+
24
+
25
+ def _render_table_from_rows(rows: list[Any]) -> None:
26
+ if not rows:
27
+ console.print(Panel("No rows.", title="Result", border_style="yellow"))
28
+ return
29
+
30
+ normalized_rows = [_normalize_item(row) for row in rows]
31
+ first_row = normalized_rows[0]
32
+ if not isinstance(first_row, dict):
33
+ console.print(Panel(str(rows), title="Result", border_style="cyan"))
34
+ return
35
+
36
+ table = Table(title="Result", show_lines=False, header_style="bold cyan")
37
+ for column in first_row.keys():
38
+ table.add_column(str(column), overflow="fold")
39
+
40
+ for row in normalized_rows:
41
+ table.add_row(*[str(row.get(column, "")) for column in first_row.keys()])
42
+
43
+ console.print(table)
44
+
45
+
46
+ def _render_analytics_chart(rows: list[dict[str, Any]], chart_config: dict[str, Any]) -> None | str:
47
+ chart_type = chart_config.get("chart_type")
48
+ if not chart_type or chart_type == "none":
49
+ return None
50
+
51
+ x_col = chart_config.get("x_axis")
52
+ y_col = chart_config.get("y_axis")
53
+ if not x_col or not y_col:
54
+ return None
55
+
56
+ title = chart_config.get("title") or "Analytics Chart"
57
+ color = chart_config.get("color")
58
+
59
+ if chart_type in ["pie", "donut"]:
60
+ try:
61
+ import termcharts
62
+ data = {}
63
+ for row in rows:
64
+ k = str(row.get(x_col, ""))
65
+ v = float(row.get(y_col) or 0)
66
+ data[k] = v
67
+
68
+ if chart_type == "donut":
69
+ chart_obj = termcharts.donut(data, title=title)
70
+ else:
71
+ chart_obj = termcharts.pie(data, title=title)
72
+
73
+ return str(chart_obj)
74
+ except Exception:
75
+ # Fallback if termcharts fails
76
+ chart_type = "bar"
77
+
78
+ import plotext as plt
79
+ plt.clf()
80
+
81
+ if chart_type == "line":
82
+ plt.date_form("Y-m-d")
83
+
84
+ # Sort by date
85
+ sorted_rows = sorted(rows, key=lambda row: str(row.get(x_col, "")))
86
+ labels = [str(row.get(x_col, "")) for row in sorted_rows]
87
+ values = [float(row.get(y_col) or 0) for row in sorted_rows]
88
+
89
+ plt.plot(labels, values, marker="dot")
90
+ else:
91
+ # Sort by value for bar chart
92
+ top_rows = sorted(rows, key=lambda row: float(row.get(y_col) or 0), reverse=True)[:15]
93
+ labels = [str(row.get(x_col, "")) for row in top_rows]
94
+ values = [float(row.get(y_col) or 0) for row in top_rows]
95
+
96
+ if chart_type == "scatter":
97
+ plt.scatter(labels, values)
98
+ else:
99
+ plt.bar(labels, values, orientation="horizontal")
100
+
101
+ plt.title(title)
102
+
103
+ if color:
104
+ try:
105
+ if color == "diverse":
106
+ plt.colorless() # plotext does not strictly have diverse, but we can set multiple colors. Actually plotext uses diverse colors automatically if we don't set color.
107
+ else:
108
+ plt.color(color)
109
+ except Exception:
110
+ pass
111
+
112
+ plt.plotsize(60, 20)
113
+ plt.theme("clear")
114
+
115
+ # Render and encode safely to avoid Windows cp1252 crash
116
+ try:
117
+ chart_str = plt.build()
118
+ return chart_str
119
+ except Exception:
120
+ plt.theme("pro") # Fallback to a simpler theme
121
+ return plt.build()
122
+
123
+
124
+ def _render_budget_alerts(alerts: list[str]) -> None:
125
+ if not alerts:
126
+ return
127
+
128
+ table = Table(title="Budget Alerts", header_style="bold yellow")
129
+ table.add_column("Alert")
130
+ for alert in alerts:
131
+ table.add_row(alert)
132
+ console.print(table)
133
+
134
+
135
+ def render_result(state: dict[str, Any]) -> None:
136
+ """Render the graph output state to the CLI."""
137
+ if state.get("error"):
138
+ console.print(
139
+ Panel.fit(
140
+ f"[bold red]Error[/bold red]\n{state['error']}", border_style="red"
141
+ )
142
+ )
143
+ return
144
+
145
+ analytics_sql = state.get("analytics_sql")
146
+ analytics_review = state.get("analytics_review")
147
+ query_result = state.get("query_result")
148
+ answer = state.get("answer")
149
+ budget_alerts = state.get("budget_alerts") or []
150
+
151
+ if analytics_sql:
152
+ console.print(Panel.fit(analytics_sql, title="SQL", border_style="dim"))
153
+
154
+ if analytics_review:
155
+ console.print(
156
+ Panel.fit(str(analytics_review), title="SQL Review", border_style="cyan")
157
+ )
158
+
159
+ if budget_alerts:
160
+ _render_budget_alerts(list(dict.fromkeys(budget_alerts)))
161
+
162
+ chart_config = state.get("chart_config")
163
+
164
+ if query_result is not None:
165
+ if isinstance(query_result, list):
166
+ if query_result and isinstance(query_result[0], dict) and chart_config:
167
+ chart = _render_analytics_chart(query_result, chart_config)
168
+ if chart:
169
+ chart_type = chart_config.get("chart_type")
170
+ if chart_type in ["pie", "donut"]:
171
+ # termcharts returns raw ANSI strings. We MUST parse them via Text.from_ansi
172
+ # otherwise Rich's markup parser gets confused and prints literal [31m tags.
173
+ from rich.text import Text
174
+ console.print(f"\n[bold green]Analytics Chart:[/bold green] {chart_config.get('title', '')}")
175
+ console.print(Text.from_ansi(chart))
176
+ else:
177
+ console.print(
178
+ Panel.fit(chart, title="Analytics Chart", border_style="green")
179
+ )
180
+ _render_table_from_rows(query_result)
181
+ return
182
+
183
+ if isinstance(query_result, dict):
184
+ table = Table(title="Result", header_style="bold cyan")
185
+ table.add_column("Key")
186
+ table.add_column("Value")
187
+ for key, value in query_result.items():
188
+ table.add_row(str(key), str(value))
189
+ console.print(table)
190
+ return
191
+
192
+ console.print(Panel.fit(str(query_result), title="Result", border_style="cyan"))
193
+ return
194
+
195
+ if answer:
196
+ console.print(Panel.fit(answer, title="Result", border_style="green"))
197
+ return
198
+
199
+ # Check for successful operations
200
+ if state.get("saved_record_ids"):
201
+ count = len(state["saved_record_ids"])
202
+ console.print(
203
+ Panel.fit(f"Successfully created {count} record(s).", title="Result", border_style="green")
204
+ )
205
+ return
206
+
207
+ if state.get("updated_record_id"):
208
+ console.print(
209
+ Panel.fit(f"Record #{state['updated_record_id']} updated successfully.", title="Result", border_style="green")
210
+ )
211
+ return
212
+
213
+ if state.get("deleted_record_id"):
214
+ console.print(
215
+ Panel.fit(f"Record #{state['deleted_record_id']} deleted successfully.", title="Result", border_style="green")
216
+ )
217
+ return
218
+
219
+ console.print(
220
+ Panel.fit("Operation completed.", title="Result", border_style="green")
221
+ )
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from rich.console import Console
6
+ from InquirerPy import inquirer
7
+ from InquirerPy.base.control import Choice
8
+ from InquirerPy.prompts.fuzzy import FuzzyPrompt
9
+
10
+ # Monkey-patch FuzzyPrompt to prevent IndexError when toggling an empty search result
11
+ _original_toggle = FuzzyPrompt._handle_toggle_choice
12
+ def _safe_toggle(self, event) -> None:
13
+ try:
14
+ _original_toggle(self, event)
15
+ except IndexError:
16
+ pass
17
+ FuzzyPrompt._handle_toggle_choice = _safe_toggle
18
+
19
+ console = Console()
20
+
21
+ def display_search_results(results: list[dict[str, Any]], record_type: str, multiselect: bool = False) -> int | list[int] | None:
22
+ """Display search results as a fuzzy search menu and prompt user to pick one or more.
23
+
24
+ Returns the selected record ID(s), or None if cancelled.
25
+ """
26
+ if not results:
27
+ console.print("[yellow]No records found.[/yellow]")
28
+ return None
29
+
30
+ choices = []
31
+ display_keys = [k for k in results[0].keys() if k != "id"]
32
+
33
+ for record in results:
34
+ # Build a searchable string out of the values
35
+ parts = []
36
+ for key in display_keys:
37
+ val = record.get(key)
38
+ if val is not None and val != "" and val != "-":
39
+ parts.append(f"{key.title()}: {val}")
40
+
41
+ display_str = " | ".join(parts)
42
+ choices.append(Choice(value=record.get("id"), name=display_str))
43
+
44
+ msg = f"Search and select a {record_type} record (Type to filter, Enter to select, Ctrl-C to cancel):"
45
+ if multiselect:
46
+ msg = f"Search and select multiple {record_type} records (Tab/Space to toggle, Ctrl-A to select all, Enter to confirm):"
47
+
48
+ try:
49
+ prompt = inquirer.fuzzy(
50
+ message=msg,
51
+ choices=choices,
52
+ instruction="(Use arrow keys or type to fuzzy search)",
53
+ max_height="60%",
54
+ multiselect=multiselect,
55
+ )
56
+
57
+ if multiselect:
58
+ prompt._all_selected = False
59
+
60
+ @prompt.register_kb("c-a")
61
+ def _toggle_all(event):
62
+ prompt._all_selected = not getattr(prompt, "_all_selected", False)
63
+ prompt._handle_toggle_all(None, prompt._all_selected)
64
+
65
+ choice = prompt.execute()
66
+
67
+ return choice
68
+ except KeyboardInterrupt:
69
+ console.print("\n[dim]Selection cancelled.[/dim]")
70
+ return None
@@ -0,0 +1,94 @@
1
+ """First-run setup — accounts and budgets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import requests
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.prompt import Confirm, IntPrompt, Prompt
9
+
10
+ from whatsmynote.app.config import API_URL, get_groq_api_key
11
+ from whatsmynote.app.auth import get_supabase
12
+
13
+ console = Console()
14
+
15
+ def _get_headers() -> dict:
16
+ session = get_supabase().auth.get_session()
17
+ token = session.access_token if session else ""
18
+ return {
19
+ "Authorization": f"Bearer {token}",
20
+ "X-Groq-Api-Key": get_groq_api_key() or ""
21
+ }
22
+
23
+ def ensure_initial_setup() -> None:
24
+ headers = _get_headers()
25
+ try:
26
+ res = requests.get(f"{API_URL}/setup/status", headers=headers)
27
+ res.raise_for_status()
28
+ status = res.json()
29
+ except Exception as e:
30
+ console.print(f"[red]Error connecting to backend: {e}[/red]")
31
+ return
32
+
33
+ account_count = status.get("account_count", 0)
34
+ budget_count = status.get("budget_count", 0)
35
+ default_account = status.get("default_account")
36
+
37
+ accounts = []
38
+ default_account_index = None
39
+
40
+ if account_count == 0:
41
+ console.print(Panel.fit("[bold]Initial account setup[/bold]\nEnter starting balances once.", border_style="cyan"))
42
+ count = IntPrompt.ask("How many accounts do you want to set up?", default=1)
43
+ for index in range(count):
44
+ console.print(f"[bold cyan]Account {index + 1}/{count}[/bold cyan]")
45
+ name = Prompt.ask("Account name")
46
+ opening_balance = IntPrompt.ask("Opening balance", default=0)
47
+ currency = Prompt.ask("Currency", default="INR")
48
+ notes = Prompt.ask("Notes", default="")
49
+ accounts.append({
50
+ "name": name,
51
+ "opening_balance": opening_balance,
52
+ "currency": currency,
53
+ "notes": notes or None,
54
+ })
55
+ if accounts:
56
+ default_index = IntPrompt.ask("Which account should be default?", default=1)
57
+ default_account_index = max(1, min(default_index, len(accounts))) - 1
58
+ else:
59
+ console.print(f"[dim]Found {account_count} account record(s). Skipping account setup.[/dim]")
60
+ if default_account:
61
+ console.print(f"[dim]Default account: {default_account}.[/dim]")
62
+
63
+ budgets = []
64
+ if budget_count == 0:
65
+ if Confirm.ask("Set up budgets now?", default=True):
66
+ console.print(Panel.fit("[bold]Initial budget setup[/bold]\nSet monthly limits.", border_style="magenta"))
67
+ count = IntPrompt.ask("How many budgets do you want to set up?", default=0)
68
+ for index in range(count):
69
+ console.print(f"[bold magenta]Budget {index + 1}/{count}[/bold magenta]")
70
+ category = Prompt.ask("Budget category", default="Overall")
71
+ amount = IntPrompt.ask("Budget limit")
72
+ period = Prompt.ask("Period", default="monthly")
73
+ notes = Prompt.ask("Notes", default="")
74
+ budgets.append({
75
+ "category": category,
76
+ "amount": amount,
77
+ "period": period,
78
+ "notes": notes or None,
79
+ })
80
+ else:
81
+ console.print(f"[dim]Found {budget_count} budget record(s). Skipping budget setup.[/dim]")
82
+
83
+ if accounts or budgets:
84
+ payload = {
85
+ "accounts": accounts,
86
+ "budgets": budgets,
87
+ "default_account_index": default_account_index
88
+ }
89
+ try:
90
+ res = requests.post(f"{API_URL}/setup", json=payload, headers=headers)
91
+ res.raise_for_status()
92
+ console.print("[green]Setup completed successfully.[/green]")
93
+ except Exception as e:
94
+ console.print(f"[red]Error saving setup to backend: {e}[/red]")
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: whatsmynote
3
+ Version: 0.1.0
4
+ Summary: A CLI app for managing notes and finances using LLMs.
5
+ Author-email: Sarthak Jha <sargedevx@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/SarJ2004/WhatsMyNote
8
+ Project-URL: Repository, https://github.com/SarJ2004/WhatsMyNote
9
+ Keywords: cli,llm,finance,notes
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: python-dotenv>=0.9.9
17
+ Requires-Dist: inquirerpy>=0.3.4
18
+ Requires-Dist: plotext>=5.3.2
19
+ Requires-Dist: rich>=13.9.4
20
+ Requires-Dist: termcharts>=1.1.2
21
+ Requires-Dist: platformdirs>=4.2.2
22
+ Requires-Dist: supabase>=2.3.0
23
+ Requires-Dist: requests>=2.31.0
24
+ Requires-Dist: fastapi[standard]>=0.139.0
25
+ Requires-Dist: uvicorn>=0.51.0
26
+ Requires-Dist: twine>=6.2.0
27
+ Dynamic: license-file
28
+
29
+ # WhatsMyNote
30
+
31
+ WhatsMyNote is a natural-language personal finance assistant for logging and querying lending, expenses, income, and transfers. It uses advanced **Llama 3 LLMs** to extract structured data, store it in PostgreSQL (Supabase), and answer your complex queries accurately.
32
+
33
+ ## 🚀 Bring Your Own Key (BYOK) Architecture
34
+ WhatsMyNote now supports a secure **BYOK architecture**. You bring your own **Groq API key** to power the inference engine locally and securely.
35
+
36
+ On your first run, the app will prompt you for your `GROQ_API_KEY`. It is stored securely in a local `.env` file and is never exposed. The application uses state-of-the-art Groq models:
37
+ - **`llama-3.3-70b-versatile`** for reasoning, intent classification, and SQL generation.
38
+ - **`llama-3.1-8b-instant`** for fast and efficient data extraction.
39
+
40
+ ---
41
+
42
+ ## 🛠 What It Supports & Examples
43
+
44
+ ### 📉 Expenses
45
+ Track where your money is going.
46
+ - Create expense records with amount, category, merchant, payment source, item, and notes.
47
+ - **Example:** `I spent 320 on groceries at Walmart using HDFC`
48
+ - **Example:** `Update my last grocery expense amount to 400`
49
+ - **Example:** `Delete the Walmart expense`
50
+ - **Example:** `Show my highest expenses this month`
51
+
52
+ ### 💰 Income
53
+ Record earnings, cashback, or refunds.
54
+ - **Example:** `I received 50000 as salary from Acme`
55
+ - **Example:** `Update my salary entry notes to include the July bonus`
56
+ - **Example:** `How much income did I receive from salary sources this year?`
57
+
58
+ ### 🤝 Lending & Borrowing
59
+ Keep track of who owes you, and who you owe.
60
+ - **Example:** `I lent Sumit 500 today`
61
+ - **Example:** `Update Sumit's loan amount to 700`
62
+ - **Example:** `How much money have I lent to Sumit?`
63
+
64
+ ### 🔁 Transfers
65
+ Record money moving between your own accounts.
66
+ - **Example:** `I transferred 3000 from HDFC to SBI`
67
+ - **Example:** `Change the transfer destination to ICICI`
68
+ - **Example:** `Show all transfers from HDFC this month`
69
+
70
+ ### 📊 Budgets and Balances
71
+ Track your overall financial health.
72
+ - Set opening balances for your accounts (e.g., cash, wallet, SBI).
73
+ - Track category budgets (e.g., groceries, transport) or an `Overall` limit.
74
+ - **Example:** `Set opening balance for SBI to 50000`
75
+ - **Example:** `Set monthly budget for groceries to 10000`
76
+ - **Example:** `What is my current balance in SBI?`
77
+ - **Example:** `How much budget do I have left for groceries this month?`
78
+
79
+ ### 📈 Analytics (Read-Only)
80
+ Run advanced queries without touching write access.
81
+ - Safe SQL generation with automated validation.
82
+ - Prefix queries with `sql:` for advanced read-only raw queries.
83
+ - **Example:** `What is my total spending this month?`
84
+ - **Example:** `Compare my income and expense totals for July`
85
+ - **Example:** `What is my net worth estimate?`
86
+
87
+ ---
88
+
89
+ ## ⚙️ Environment Setup
90
+
91
+ Install dependencies and start your virtual environment:
92
+
93
+ ```bash
94
+ uv venv
95
+ .venv\Scripts\activate
96
+ uv pip install -r requirements.txt
97
+ ```
98
+
99
+ Set up your database connection string in your `.env` (we recommend a **Supabase PostgreSQL** instance):
100
+ ```env
101
+ DATABASE_URL=postgresql+psycopg2://postgres:YOUR_PASSWORD@db.YOUR_SUPABASE_REF.supabase.co:5432/postgres
102
+ ```
103
+
104
+ Run the app. It will prompt you for your `GROQ_API_KEY` on the first launch!
105
+ ```bash
106
+ python app/main.py
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 🏗 Tech Stack
112
+
113
+ - **Python** for core logic.
114
+ - **LangGraph & LangChain** for robust stateful intent routing and HITL (Human-in-the-Loop) flows.
115
+ - **Pydantic** for extraction validation.
116
+ - **SQLAlchemy & PostgreSQL** for data persistence.
117
+ - **Groq LLMs (Llama 3)** for lightning-fast, high-quality inference.
118
+
119
+ ---
120
+
121
+ ## 🚀 Vision
122
+ WhatsMyNote aims to become a fully conversational financial memory system, helping you control your lending, borrowing, expenses, income, and transfers through pure natural language.
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ whatsmynote/__init__.py
5
+ whatsmynote.egg-info/PKG-INFO
6
+ whatsmynote.egg-info/SOURCES.txt
7
+ whatsmynote.egg-info/dependency_links.txt
8
+ whatsmynote.egg-info/entry_points.txt
9
+ whatsmynote.egg-info/requires.txt
10
+ whatsmynote.egg-info/top_level.txt
11
+ whatsmynote/app/__init__.py
12
+ whatsmynote/app/auth.py
13
+ whatsmynote/app/chat.py
14
+ whatsmynote/app/config.py
15
+ whatsmynote/app/main.py
16
+ whatsmynote/app/render.py
17
+ whatsmynote/app/selector.py
18
+ whatsmynote/app/setup.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ whatsmynote = whatsmynote.app.chat:main
@@ -0,0 +1,11 @@
1
+ python-dotenv>=0.9.9
2
+ inquirerpy>=0.3.4
3
+ plotext>=5.3.2
4
+ rich>=13.9.4
5
+ termcharts>=1.1.2
6
+ platformdirs>=4.2.2
7
+ supabase>=2.3.0
8
+ requests>=2.31.0
9
+ fastapi[standard]>=0.139.0
10
+ uvicorn>=0.51.0
11
+ twine>=6.2.0
@@ -0,0 +1 @@
1
+ whatsmynote