Skip to content

Commit 1cabf1d

Browse files
v2: modernized -- modular architecture, multi-provider LLM, modern UI
0 parents  commit 1cabf1d

60 files changed

Lines changed: 3043 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.git
2+
.gitignore
3+
.venv
4+
venv
5+
__pycache__
6+
*.pyc
7+
.pytest_cache
8+
.ruff_cache
9+
.mypy_cache
10+
.coverage
11+
htmlcov
12+
.env
13+
.env.*
14+
!.env.example
15+
.streamlit/secrets.toml
16+
tests
17+
*.md
18+
!README.md

.env.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# --- LLM provider credentials ---
2+
# Fill in only the providers you intend to use.
3+
OPENAI_API_KEY=
4+
ANTHROPIC_API_KEY=
5+
6+
# --- Local Ollama (optional) ---
7+
OLLAMA_BASE_URL=http://localhost:11434
8+
9+
# --- Defaults ---
10+
DEFAULT_PROVIDER=openai
11+
DEFAULT_MODEL=gpt-4o-mini
12+
DEFAULT_TEMPERATURE=0.2
13+
DEFAULT_MAX_TOKENS=1024
14+
15+
# --- App ---
16+
ENVIRONMENT=dev
17+
LOG_LEVEL=INFO

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
__pycache__/
2+
*.pyc
3+
*.pyo
4+
*.pyd
5+
*.egg-info/
6+
.venv/
7+
venv/
8+
env/
9+
.env
10+
.env.*
11+
!.env.example
12+
.streamlit/secrets.toml
13+
.pytest_cache/
14+
.coverage
15+
htmlcov/
16+
.ruff_cache/
17+
.mypy_cache/
18+
.ipynb_checkpoints/
19+
.DS_Store
20+
Thumbs.db
21+
*.log
22+
build/
23+
dist/

.streamlit/config.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Streamlit configuration.
2+
# Keep settings minimal so the user's OS theme drives the look-and-feel.
3+
4+
[server]
5+
maxUploadSize = 200 # MB. Bump for big CSVs.
6+
headless = true
7+
8+
[browser]
9+
gatherUsageStats = false
10+
11+
[theme]
12+
# Leaving "base" unset means we respect the user's system preference (light/dark).
13+
primaryColor = "#7C3AED"
14+
font = "sans serif"

.streamlit/secrets.toml.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copy this file to .streamlit/secrets.toml (gitignored).
2+
# Streamlit Community Cloud reads these as st.secrets[...].
3+
4+
OPENAI_API_KEY = ""
5+
ANTHROPIC_API_KEY = ""
6+
OLLAMA_BASE_URL = "http://localhost:11434"

ARCHITECTURE.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Architecture
2+
3+
CSV-AI v2 is a layered Streamlit application designed so that the **business logic is independent of the UI**. The same `services/` package could be served from FastAPI, a CLI, or a Discord bot tomorrow without changes.
4+
5+
```
6+
┌───────────────────────────────────────────────────────────────────┐
7+
│ streamlit_app.py │
8+
│ (thin entry) │
9+
└──────────────────────────┬────────────────────────────────────────┘
10+
11+
┌────────▼────────┐
12+
│ app.ui │
13+
│ pages + comps │ ← Streamlit lives here only
14+
└────────┬────────┘
15+
16+
┌────────▼────────┐
17+
│ app.services │
18+
│ Chat·Summary· │ ← pure Python; UI-free
19+
│ Analysis │
20+
└────────┬────────┘
21+
┌──────────────┼──────────────┐
22+
▼ ▼ ▼
23+
┌────────────┐ ┌────────────┐ ┌────────────┐
24+
│ app.data │ │ app.llm │ │ app.prompts │
25+
│ load·prof │ │ provider │ │ templates │
26+
│ sample │ │ layer │ │ │
27+
└────────────┘ └────┬───────┘ └────────────┘
28+
29+
┌────────────┼────────────┐
30+
▼ ▼ ▼
31+
OpenAI Anthropic Ollama
32+
```
33+
34+
## Why each layer exists
35+
36+
### `app.config`
37+
Single source of truth for settings via `pydantic-settings`. Reads `.env`, OS env, and (best-effort) Streamlit secrets. Anything that needs an API key or a default should call `get_settings()` rather than reading `os.environ`.
38+
39+
### `app.llm`
40+
A provider-agnostic interface (`LLMProvider`) with three concrete implementations. Adding a new backend (Mistral API, Bedrock, …) is one file plus one factory entry. Keeping streaming in the interface forces every provider to behave consistently for the chat UI.
41+
42+
### `app.data`
43+
Pure-pandas. **Never** imports Streamlit or any LLM SDK. Three concerns:
44+
- `loader.py` — robust CSV reading (encoding fallback chain, delimiter sniffing).
45+
- `profiler.py` — fast statistical profile (rows, dtypes, missing %, correlations, samples).
46+
- `sampler.py` + `context.py` — turn a DataFrame into an LLM-friendly markdown context.
47+
48+
### `app.prompts`
49+
System prompts as constants. Versioning prompts in code (not strings sprinkled through services) keeps prompt iteration reviewable in git.
50+
51+
### `app.services`
52+
Three services map 1:1 to the three product workflows. Each takes a `provider` and a `df`, exposes a non-streaming and streaming method, and never imports Streamlit. They're trivially callable from FastAPI later.
53+
54+
### `app.ui`
55+
The only place we import Streamlit. Page modules render; component modules are reusable. Session state is centralized in `state.py` so key names stay consistent.
56+
57+
### `app.utils`
58+
Cross-cutting: logging, exception hierarchy, token counting. No business logic.
59+
60+
## Key design choices
61+
62+
**No FAISS / no retrieval for chat.** The original CSV-AI embedded every row chunk into FAISS and retrieved on every question. For small files this was wasted compute; for analytical questions ("what's the average X?") it was actively worse than just showing the model the schema. v2 sends a structured schema + a smart sample. Result: lower latency, lower cost, fewer hallucinations.
63+
64+
**Deterministic stats first, LLM second.** The Analyze page computes the actual numbers with pandas, then asks the LLM to write the narrative around those numbers. The LLM never invents arithmetic — pure-LLM dataframe agents do.
65+
66+
**Streaming everywhere.** Every provider yields token chunks. The UI uses `st.empty()` plus `st.markdown` with a trailing cursor for the classic typewriter feel.
67+
68+
**Custom-model escape hatch.** The sidebar's model picker has a `custom...` option so new model ids (e.g. tomorrow's `gpt-5`) work without a code change.
69+
70+
**Future API split.** Because services don't import Streamlit, exposing an HTTP layer is mostly:
71+
72+
```python
73+
@app.post("/chat")
74+
def chat(req: ChatReq) -> ChatResp:
75+
provider = build_provider(req.provider, model=req.model, ...)
76+
df = pd.read_csv(req.csv_path)
77+
service = ChatService(provider=provider, df=df)
78+
return ChatResp(answer=service.ask(req.question))
79+
```
80+
81+
## Adding a new LLM provider
82+
83+
1. Create `app/llm/<name>_provider.py` subclassing `LLMProvider`. Implement `complete` and `stream`.
84+
2. Register it in `app/llm/factory.py` (`_PROVIDERS`).
85+
3. Add suggested models to `_MODEL_CATALOGUE` in the same file.
86+
4. Extend `Settings` with any new credential fields.
87+
5. Update the sidebar's `_render_provider_section` if it needs a non-standard field (e.g. a region).
88+
89+
## Adding a new page
90+
91+
1. Create `app/ui/pages/<name>.py` with a `render()` function.
92+
2. Add it to `PAGE_RENDERERS` in `app/main.py` and `PAGES` in `app/ui/components/sidebar.py`.
93+
94+
That's it. No central router to wire up.

DEPLOYMENT.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Deployment
2+
3+
CSV-AI v2 is structured to deploy three ways out of the box, with a fourth (API split) being a small future task.
4+
5+
## 1. Streamlit Community Cloud
6+
7+
1. Push this repo to GitHub.
8+
2. On [streamlit.io/cloud](https://streamlit.io/cloud), create a new app pointing at this repo.
9+
3. Set **Main file path** to `streamlit_app.py`.
10+
4. Under **Settings → Secrets**, paste:
11+
12+
```toml
13+
OPENAI_API_KEY = "sk-..."
14+
ANTHROPIC_API_KEY = "sk-ant-..."
15+
```
16+
17+
5. Deploy. `requirements.txt` is picked up automatically.
18+
19+
> Streamlit Cloud's sandbox can't reach a local Ollama instance — pick `openai` or `anthropic` as your default provider in `Settings → Advanced`.
20+
21+
## 2. Local development
22+
23+
```bash
24+
git clone https://github.com/Safiullah-Rahu/CSV-AI.git
25+
cd CSV-AI
26+
python -m venv .venv && source .venv/bin/activate
27+
pip install -r requirements-dev.txt
28+
cp .env.example .env # fill in keys
29+
streamlit run streamlit_app.py
30+
```
31+
32+
Optional — run a local Ollama:
33+
34+
```bash
35+
ollama serve &
36+
ollama pull llama3.1
37+
# then in the sidebar: provider = ollama, model = llama3.1
38+
```
39+
40+
## 3. Docker / Docker Compose
41+
42+
```bash
43+
cp .env.example .env # fill in keys
44+
docker compose up --build
45+
```
46+
47+
Open <http://localhost:8501>. The container runs `streamlit run streamlit_app.py` on port 8501 with a healthcheck.
48+
49+
For a bare-metal Docker run:
50+
51+
```bash
52+
docker build -t csv-ai .
53+
docker run --rm -p 8501:8501 --env-file .env csv-ai
54+
```
55+
56+
## 4. Generic VPS
57+
58+
The Dockerfile is the recommended path on any Linux VPS — drop it behind nginx with an SSL terminator. If you want to skip Docker:
59+
60+
```bash
61+
# As a non-root user on the VPS:
62+
git clone https://github.com/Safiullah-Rahu/CSV-AI.git /opt/csv-ai
63+
cd /opt/csv-ai
64+
python3.11 -m venv .venv && source .venv/bin/activate
65+
pip install -r requirements.txt
66+
cp .env.example .env # fill in keys
67+
68+
# Run as a systemd unit (see snippet below):
69+
sudo systemctl enable --now csv-ai.service
70+
```
71+
72+
`csv-ai.service`:
73+
74+
```ini
75+
[Unit]
76+
Description=CSV-AI Streamlit app
77+
After=network.target
78+
79+
[Service]
80+
User=csvai
81+
WorkingDirectory=/opt/csv-ai
82+
EnvironmentFile=/opt/csv-ai/.env
83+
ExecStart=/opt/csv-ai/.venv/bin/streamlit run streamlit_app.py --server.port=8501 --server.address=127.0.0.1
84+
Restart=on-failure
85+
86+
[Install]
87+
WantedBy=multi-user.target
88+
```
89+
90+
Front it with nginx:
91+
92+
```nginx
93+
server {
94+
listen 443 ssl http2;
95+
server_name csv-ai.example.com;
96+
# SSL certs ...
97+
location / {
98+
proxy_pass http://127.0.0.1:8501;
99+
proxy_http_version 1.1;
100+
proxy_set_header Upgrade $http_upgrade;
101+
proxy_set_header Connection "upgrade";
102+
proxy_set_header Host $host;
103+
proxy_read_timeout 300;
104+
}
105+
}
106+
```
107+
108+
## 5. Future: API/backend split
109+
110+
`app/services/` has no Streamlit imports, so wrapping it in FastAPI is mostly mechanical:
111+
112+
```python
113+
# api/main.py — future
114+
from fastapi import FastAPI
115+
from pydantic import BaseModel
116+
import pandas as pd
117+
118+
from app.llm.factory import build_provider
119+
from app.services import ChatService
120+
121+
api = FastAPI()
122+
123+
class ChatReq(BaseModel):
124+
provider: str
125+
model: str
126+
question: str
127+
csv_path: str
128+
129+
@api.post("/chat")
130+
def chat(req: ChatReq):
131+
provider = build_provider(req.provider, model=req.model)
132+
df = pd.read_csv(req.csv_path)
133+
return {"answer": ChatService(provider=provider, df=df).ask(req.question)}
134+
```
135+
136+
Same engine, different transport. The Streamlit app can stay, become a thin client of the API, or both can run side-by-side.
137+
138+
## Health checks & observability
139+
140+
- The Streamlit endpoint `/_stcore/health` returns 200 when ready (used by the Dockerfile healthcheck).
141+
- Logs go to stderr under the `csvai.*` namespace (see `app/utils/logging.py`). Set `LOG_LEVEL=DEBUG` for verbose output.

Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# syntax=docker/dockerfile:1.6
2+
3+
FROM python:3.11-slim AS base
4+
5+
ENV PYTHONUNBUFFERED=1 \
6+
PIP_NO_CACHE_DIR=1 \
7+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
8+
STREAMLIT_SERVER_HEADLESS=true \
9+
STREAMLIT_SERVER_ENABLE_CORS=false
10+
11+
WORKDIR /app
12+
13+
# System deps: minimal — pandas wheels are precompiled on x86_64.
14+
RUN apt-get update \
15+
&& apt-get install -y --no-install-recommends build-essential \
16+
&& rm -rf /var/lib/apt/lists/*
17+
18+
COPY requirements.txt ./
19+
RUN pip install --no-cache-dir -r requirements.txt
20+
21+
COPY . ./
22+
23+
EXPOSE 8501
24+
25+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
26+
CMD curl -fsS http://localhost:8501/_stcore/health || exit 1
27+
28+
CMD ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023-2026 Safiullah Rahu
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 commit comments

Comments
 (0)