Manish Kumar commited on
Commit ·
c07244c
1
Parent(s): a19c940
new
Browse files- .env.example +2 -16
- .gitattributes +0 -2
- Dockerfile +6 -31
- README.md +0 -107
- backend/app/config.py +9 -48
- backend/app/llm/local.py +53 -89
- backend/app/llm/manager.py +3 -55
- backend/app/main.py +76 -225
- backend/app/middleware.py +6 -58
- backend/app/models.py +13 -31
- backend/app/utils.py +8 -70
- backend/requirements.txt +1 -5
- backend/run.py +0 -11
- docker/Dockerfile.backend +0 -45
- docker/Dockerfile.frontend +0 -13
- docker/docker-compose.yml +0 -36
- docs/API.md +0 -109
- docs/DEPLOYMENT.md +0 -47
- frontend/src/App.tsx +0 -2
- frontend/src/components/ChatInterface.tsx +4 -6
- frontend/src/components/CodePlayground.tsx +1 -3
- frontend/src/components/Dashboard.tsx +19 -48
- frontend/src/components/Navbar.tsx +2 -2
- frontend/src/components/Settings.tsx +28 -175
- frontend/src/hooks/useChat.ts +15 -70
- frontend/src/types.ts +0 -22
- models/.gitkeep +0 -1
- render.yaml +4 -0
- scripts/download_model.py +11 -25
- scripts/setup.sh +2 -34
- tests/__init__.py +0 -1
- tests/test_backend.py +0 -61
.env.example
CHANGED
|
@@ -1,22 +1,8 @@
|
|
| 1 |
-
# AI Coding Assistant - Environment Configuration
|
| 2 |
-
|
| 3 |
-
# Backend Server Configuration
|
| 4 |
PORT=8000
|
| 5 |
HOST=0.0.0.0
|
| 6 |
DEBUG=false
|
| 7 |
-
CORS_ORIGINS=http://localhost:5173,http://localhost:3000,https://*.railway.app,https://*.render.com
|
| 8 |
-
|
| 9 |
-
# SmolLM2 Model Settings
|
| 10 |
LOCAL_MODEL_PATH=models/SmolLM2-360M-Instruct-Q4_K_M.gguf
|
| 11 |
-
LOCAL_MODEL_REPO=bartowski/SmolLM2-360M-Instruct-GGUF
|
| 12 |
-
LOCAL_MODEL_FILE=SmolLM2-360M-Instruct-Q4_K_M.gguf
|
| 13 |
-
|
| 14 |
-
# Generation Parameters
|
| 15 |
DEFAULT_TEMPERATURE=0.7
|
| 16 |
-
DEFAULT_MAX_TOKENS=
|
| 17 |
DEFAULT_TOP_P=0.9
|
| 18 |
-
DEFAULT_CONTEXT_LENGTH=
|
| 19 |
-
|
| 20 |
-
# Security & Limits
|
| 21 |
-
RATE_LIMIT_PER_MINUTE=60
|
| 22 |
-
SECRET_KEY=generate_a_secure_random_key_here
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
PORT=8000
|
| 2 |
HOST=0.0.0.0
|
| 3 |
DEBUG=false
|
|
|
|
|
|
|
|
|
|
| 4 |
LOCAL_MODEL_PATH=models/SmolLM2-360M-Instruct-Q4_K_M.gguf
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
DEFAULT_TEMPERATURE=0.7
|
| 6 |
+
DEFAULT_MAX_TOKENS=512
|
| 7 |
DEFAULT_TOP_P=0.9
|
| 8 |
+
DEFAULT_CONTEXT_LENGTH=512
|
|
|
|
|
|
|
|
|
|
|
|
.gitattributes
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
*.gguf filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
models/*.gguf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,52 +1,27 @@
|
|
| 1 |
-
# Stage 1: Build
|
| 2 |
-
FROM node:20-
|
| 3 |
-
|
| 4 |
WORKDIR /frontend
|
| 5 |
-
|
| 6 |
-
# Copy package config and lock files
|
| 7 |
COPY frontend/package*.json ./
|
| 8 |
-
|
| 9 |
-
# Install packages
|
| 10 |
RUN npm ci
|
| 11 |
-
|
| 12 |
-
# Copy frontend source files
|
| 13 |
COPY frontend/ ./
|
| 14 |
-
|
| 15 |
-
# Build production static bundle
|
| 16 |
RUN npm run build
|
| 17 |
|
| 18 |
-
# Stage 2: Build
|
| 19 |
FROM python:3.11-slim
|
| 20 |
-
|
| 21 |
WORKDIR /app
|
| 22 |
|
| 23 |
-
# Install compilation tools for building llama-cpp-python in backend
|
| 24 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 25 |
-
build-essential \
|
| 26 |
-
gcc \
|
| 27 |
-
g++ \
|
| 28 |
-
make \
|
| 29 |
-
python3-dev \
|
| 30 |
-
git \
|
| 31 |
libgomp1 \
|
| 32 |
&& rm -rf /var/lib/apt/lists/*
|
| 33 |
|
| 34 |
-
# Copy requirements and install dependencies
|
| 35 |
COPY backend/requirements.txt ./backend/
|
| 36 |
-
RUN pip install --no-cache-dir -r ./backend/requirements.txt
|
| 37 |
|
| 38 |
-
# Copy backend source code
|
| 39 |
COPY backend/ ./backend/
|
| 40 |
-
|
| 41 |
-
# Copy static frontend build from Stage 1 into frontend/dist
|
| 42 |
COPY --from=frontend-builder /frontend/dist ./frontend/dist
|
| 43 |
|
| 44 |
-
|
| 45 |
-
ENV PORT=8000
|
| 46 |
-
ENV HOST=0.0.0.0
|
| 47 |
-
ENV PYTHONPATH=/app
|
| 48 |
|
| 49 |
EXPOSE 8000
|
| 50 |
|
| 51 |
-
|
| 52 |
-
CMD ["python", "backend/run.py"]
|
|
|
|
| 1 |
+
# Stage 1: Build frontend
|
| 2 |
+
FROM node:20-alpine AS frontend-builder
|
|
|
|
| 3 |
WORKDIR /frontend
|
|
|
|
|
|
|
| 4 |
COPY frontend/package*.json ./
|
|
|
|
|
|
|
| 5 |
RUN npm ci
|
|
|
|
|
|
|
| 6 |
COPY frontend/ ./
|
|
|
|
|
|
|
| 7 |
RUN npm run build
|
| 8 |
|
| 9 |
+
# Stage 2: Build backend
|
| 10 |
FROM python:3.11-slim
|
|
|
|
| 11 |
WORKDIR /app
|
| 12 |
|
|
|
|
| 13 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
libgomp1 \
|
| 15 |
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
|
|
|
|
| 17 |
COPY backend/requirements.txt ./backend/
|
| 18 |
+
RUN pip install --no-cache-dir --only-binary :all: -r ./backend/requirements.txt
|
| 19 |
|
|
|
|
| 20 |
COPY backend/ ./backend/
|
|
|
|
|
|
|
| 21 |
COPY --from=frontend-builder /frontend/dist ./frontend/dist
|
| 22 |
|
| 23 |
+
ENV PORT=8000 HOST=0.0.0.0 PYTHONPATH=/app
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
EXPOSE 8000
|
| 26 |
|
| 27 |
+
CMD ["python", "-m", "uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
|
README.md
DELETED
|
@@ -1,107 +0,0 @@
|
|
| 1 |
-
# Antigravity AI Coding Assistant ⚡
|
| 2 |
-
|
| 3 |
-
A production-ready, resource-optimized, containerized AI Coding Assistant using **Qwen2.5-Coder-0.5B-Instruct** as its core engine. Built with a Python FastAPI backend and a stunning dark-theme React + Vite + TypeScript frontend.
|
| 4 |
-
|
| 5 |
-
```mermaid
|
| 6 |
-
graph TD
|
| 7 |
-
User([User]) <--> |HTTP / SSE| FE[React SPA - Vite + TS]
|
| 8 |
-
subgraph Backend [FastAPI Server]
|
| 9 |
-
API[API Endpoints] <--> MGR[LLM Manager]
|
| 10 |
-
MGR --> |Check RAM / Models| Decision{Load Local GGUF?}
|
| 11 |
-
Decision -->|Yes: RAM >= 1.5GB| GGUF[Local llama-cpp-python]
|
| 12 |
-
Decision -->|No: Low memory / Failed| HF[Hugging Face Cloud API]
|
| 13 |
-
end
|
| 14 |
-
GGUF <--> |Read / Write| Models[(models/ folder)]
|
| 15 |
-
HF <--> |HTTPS Request| HFHub[Hugging Face Inference Hub]
|
| 16 |
-
```
|
| 17 |
-
|
| 18 |
-
---
|
| 19 |
-
|
| 20 |
-
## 🌟 Key Features
|
| 21 |
-
|
| 22 |
-
* **Cascading Fallback Pipeline:** Automatically runs local Qwen GGUF inference. If RAM is constrained (e.g. Render Free, Railway Starter), it transparently falls back to Hugging Face serverless API.
|
| 23 |
-
* **Interactive Code Playground:** Features Monaco Editor (VS Code core) with language syntax highlight selectors and quick AI commands (`Explain`, `Find Bugs`, `Refactor`, `Generate Tests`, `Summarize`).
|
| 24 |
-
* **Advanced Chat Window:** Smooth response streaming (Server-Sent Events), code block copy buttons, and drag-and-drop file imports.
|
| 25 |
-
* **Production Deployment Ready:** Pre-configured Dockerfiles, Docker Compose, Railway config, and Render deployment specifications.
|
| 26 |
-
* **Performance Telemetry:** Live dashboards displaying generation speed (tokens/sec), latency, request tallies, and RAM footprint.
|
| 27 |
-
|
| 28 |
-
---
|
| 29 |
-
|
| 30 |
-
## 🛠️ Tech Stack
|
| 31 |
-
|
| 32 |
-
* **Frontend:** React 19, Vite, TypeScript, Tailwind CSS, Monaco Editor, Lucide Icons, Framer Motion
|
| 33 |
-
* **Backend:** FastAPI, Python 3.11+, Uvicorn, llama-cpp-python, Hugging Face Hub Client, Psutil
|
| 34 |
-
|
| 35 |
-
---
|
| 36 |
-
|
| 37 |
-
## 🚀 Quick Start (Local Setup)
|
| 38 |
-
|
| 39 |
-
### Prerequisites
|
| 40 |
-
* Python 3.11+
|
| 41 |
-
* Node.js 20+
|
| 42 |
-
|
| 43 |
-
### Step 1: Clone and Setup Workspace
|
| 44 |
-
Clone this repository and navigate to the project directory:
|
| 45 |
-
```bash
|
| 46 |
-
git clone https://github.com/your-repo/antigravity-coder.git
|
| 47 |
-
cd antigravity-coder
|
| 48 |
-
```
|
| 49 |
-
|
| 50 |
-
### Step 2: Install and Download GGUF Model
|
| 51 |
-
Use our automated installer script:
|
| 52 |
-
```bash
|
| 53 |
-
# On Linux/macOS
|
| 54 |
-
chmod +x scripts/setup.sh
|
| 55 |
-
./scripts/setup.sh
|
| 56 |
-
|
| 57 |
-
# On Windows (PowerShell)
|
| 58 |
-
pip install -r backend/requirements.txt
|
| 59 |
-
cd frontend; npm install; npm run build; cd ..
|
| 60 |
-
python scripts/download_model.py
|
| 61 |
-
```
|
| 62 |
-
|
| 63 |
-
### Step 3: Run the Application
|
| 64 |
-
Start the backend server:
|
| 65 |
-
```bash
|
| 66 |
-
# Run backend (activates virtual env if created)
|
| 67 |
-
python backend/run.py
|
| 68 |
-
```
|
| 69 |
-
This runs the API server on `http://localhost:8000` and automatically compiles & hosts the React frontend assets. Navigate to [http://localhost:8000](http://localhost:8000) to view the application!
|
| 70 |
-
|
| 71 |
-
For hot-reloading frontend development:
|
| 72 |
-
```bash
|
| 73 |
-
cd frontend
|
| 74 |
-
npm run dev
|
| 75 |
-
```
|
| 76 |
-
Open [http://localhost:5173](http://localhost:5173) in your browser.
|
| 77 |
-
|
| 78 |
-
---
|
| 79 |
-
|
| 80 |
-
## 🐳 Running with Docker
|
| 81 |
-
|
| 82 |
-
Run both services in hot-reloading development mode using Docker Compose:
|
| 83 |
-
```bash
|
| 84 |
-
docker compose -f docker/docker-compose.yml up --build
|
| 85 |
-
```
|
| 86 |
-
Build and run the production-ready unified container (hosting both frontend and API on port 8000):
|
| 87 |
-
```bash
|
| 88 |
-
docker build -t antigravity-coder .
|
| 89 |
-
docker run -p 8000:8000 antigravity-coder
|
| 90 |
-
```
|
| 91 |
-
|
| 92 |
-
---
|
| 93 |
-
|
| 94 |
-
## 🌐 Cloud Deployment
|
| 95 |
-
|
| 96 |
-
Detailed step-by-step guides for deployment configurations:
|
| 97 |
-
* [Railway Deployment Guide](docs/DEPLOYMENT.md#railway)
|
| 98 |
-
* [Render Deployment Guide](docs/DEPLOYMENT.md#render)
|
| 99 |
-
* [API Reference Documentation](docs/API.md)
|
| 100 |
-
|
| 101 |
-
---
|
| 102 |
-
|
| 103 |
-
## 🔒 Security
|
| 104 |
-
|
| 105 |
-
* Standard CORS origin protections.
|
| 106 |
-
* Secure in-memory sliding rate limiter per client IP.
|
| 107 |
-
* Configuration parsing using Pydantic Settings from `.env` files. Secrets are never hardcoded.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/config.py
CHANGED
|
@@ -1,57 +1,18 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from typing import List
|
| 3 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 4 |
from pydantic import Field
|
| 5 |
|
| 6 |
class Settings(BaseSettings):
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
DEBUG: bool = Field(default=False, validation_alias="DEBUG")
|
| 11 |
-
CORS_ORIGINS: str = Field(
|
| 12 |
-
default="http://localhost:5173,http://localhost:3000,http://localhost:8000",
|
| 13 |
-
validation_alias="CORS_ORIGINS"
|
| 14 |
-
)
|
| 15 |
|
| 16 |
-
|
| 17 |
-
INFERENCE_MODE: str = Field(default="local", validation_alias="INFERENCE_MODE")
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
)
|
| 24 |
-
LOCAL_MODEL_REPO: str = Field(
|
| 25 |
-
default="bartowski/SmolLM2-360M-Instruct-GGUF",
|
| 26 |
-
validation_alias="LOCAL_MODEL_REPO"
|
| 27 |
-
)
|
| 28 |
-
LOCAL_MODEL_FILE: str = Field(
|
| 29 |
-
default="SmolLM2-360M-Instruct-Q4_K_M.gguf",
|
| 30 |
-
validation_alias="LOCAL_MODEL_FILE"
|
| 31 |
-
)
|
| 32 |
|
| 33 |
-
|
| 34 |
-
DEFAULT_TEMPERATURE: float = Field(default=0.7, validation_alias="DEFAULT_TEMPERATURE")
|
| 35 |
-
DEFAULT_MAX_TOKENS: int = Field(default=1024, validation_alias="DEFAULT_MAX_TOKENS")
|
| 36 |
-
DEFAULT_TOP_P: float = Field(default=0.9, validation_alias="DEFAULT_TOP_P")
|
| 37 |
-
DEFAULT_CONTEXT_LENGTH: int = Field(default=2048, validation_alias="DEFAULT_CONTEXT_LENGTH")
|
| 38 |
|
| 39 |
-
# Security / Limits
|
| 40 |
-
RATE_LIMIT_PER_MINUTE: int = Field(default=60, validation_alias="RATE_LIMIT_PER_MINUTE")
|
| 41 |
-
SECRET_KEY: str = Field(
|
| 42 |
-
default="dev-secret-key-must-be-changed-in-production-environments!",
|
| 43 |
-
validation_alias="SECRET_KEY"
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
@property
|
| 47 |
-
def cors_origins_list(self) -> List[str]:
|
| 48 |
-
return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()]
|
| 49 |
-
|
| 50 |
-
model_config = SettingsConfigDict(
|
| 51 |
-
env_file=".env",
|
| 52 |
-
env_file_encoding="utf-8",
|
| 53 |
-
extra="ignore"
|
| 54 |
-
)
|
| 55 |
-
|
| 56 |
-
# Instantiate singleton settings
|
| 57 |
settings = Settings()
|
|
|
|
|
|
|
|
|
|
| 1 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 2 |
from pydantic import Field
|
| 3 |
|
| 4 |
class Settings(BaseSettings):
|
| 5 |
+
PORT: int = Field(default=8000)
|
| 6 |
+
HOST: str = Field(default="0.0.0.0")
|
| 7 |
+
DEBUG: bool = Field(default=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
LOCAL_MODEL_PATH: str = Field(default="models/SmolLM2-360M-Instruct-Q4_K_M.gguf")
|
|
|
|
| 10 |
|
| 11 |
+
DEFAULT_TEMPERATURE: float = Field(default=0.7)
|
| 12 |
+
DEFAULT_MAX_TOKENS: int = Field(default=512)
|
| 13 |
+
DEFAULT_TOP_P: float = Field(default=0.9)
|
| 14 |
+
DEFAULT_CONTEXT_LENGTH: int = Field(default=512)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
settings = Settings()
|
backend/app/llm/local.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import os
|
| 2 |
-
import sys
|
| 3 |
import logging
|
| 4 |
import asyncio
|
| 5 |
from typing import AsyncIterator, List, Dict, Any, Optional
|
|
@@ -8,12 +7,10 @@ from backend.app.config import settings
|
|
| 8 |
|
| 9 |
logger = logging.getLogger(__name__)
|
| 10 |
|
| 11 |
-
# Safe imports for environment portability
|
| 12 |
try:
|
| 13 |
from llama_cpp import Llama
|
| 14 |
except ImportError:
|
| 15 |
Llama = None
|
| 16 |
-
logger.warning("llama-cpp-python is not installed. Local inference will be unavailable.")
|
| 17 |
|
| 18 |
class LocalLLMProvider(BaseLLMProvider):
|
| 19 |
def __init__(self):
|
|
@@ -25,144 +22,111 @@ class LocalLLMProvider(BaseLLMProvider):
|
|
| 25 |
async def initialize(self) -> bool:
|
| 26 |
if self.initialized:
|
| 27 |
return True
|
| 28 |
-
|
| 29 |
if Llama is None:
|
| 30 |
-
logger.error("
|
| 31 |
return False
|
| 32 |
-
|
| 33 |
if not os.path.exists(self.model_path):
|
| 34 |
-
logger.error(f"
|
| 35 |
return False
|
| 36 |
|
| 37 |
try:
|
| 38 |
-
# Determine thread count (default to CPU cores minus 1, min 1)
|
| 39 |
threads = max(1, (os.cpu_count() or 2) - 1)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
| 44 |
def load_model():
|
| 45 |
return Llama(
|
| 46 |
model_path=self.model_path,
|
| 47 |
n_ctx=self.context_length,
|
| 48 |
n_threads=threads,
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
| 50 |
)
|
| 51 |
-
|
| 52 |
self.llm = await asyncio.to_thread(load_model)
|
| 53 |
self.initialized = True
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
| 55 |
return True
|
| 56 |
except Exception as e:
|
| 57 |
-
logger.error(f"Failed to load
|
| 58 |
self.initialized = False
|
| 59 |
self.llm = None
|
| 60 |
return False
|
| 61 |
|
| 62 |
-
def _format_prompt(self, prompt: str, system_prompt: Optional[str] = None,
|
| 63 |
-
|
| 64 |
formatted = ""
|
| 65 |
-
|
| 66 |
-
# Determine system prompt
|
| 67 |
-
sys_p = system_prompt or "You are Qwen, a helpful, precise, and state-of-the-art AI programming assistant."
|
| 68 |
-
|
| 69 |
-
# Check if messages already contain a system prompt
|
| 70 |
has_system = any(m.get("role") == "system" for m in messages) if messages else False
|
| 71 |
if not has_system:
|
| 72 |
formatted += f"<|im_start|>system\n{sys_p}<|im_end|>\n"
|
| 73 |
-
|
| 74 |
if messages:
|
| 75 |
for msg in messages:
|
| 76 |
-
|
| 77 |
-
content = msg.get("content", "")
|
| 78 |
-
formatted += f"<|im_start|>{role}\n{content}<|im_end|>\n"
|
| 79 |
else:
|
| 80 |
-
# If no chat history is provided, construct a simple message pair
|
| 81 |
formatted += f"<|im_start|>user\n{prompt}<|im_end|>\n"
|
| 82 |
-
|
| 83 |
formatted += "<|im_start|>assistant\n"
|
| 84 |
return formatted
|
| 85 |
|
| 86 |
-
async def generate(
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
messages: Optional[List[Dict[str, str]]] = None,
|
| 91 |
-
temperature: float = 0.7,
|
| 92 |
-
max_tokens: int = 1024,
|
| 93 |
-
top_p: float = 0.9,
|
| 94 |
-
) -> Dict[str, Any]:
|
| 95 |
if not await self.initialize():
|
| 96 |
-
raise RuntimeError("Local LLM
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
max_tokens=max_tokens,
|
| 104 |
-
temperature=temperature,
|
| 105 |
-
top_p=top_p,
|
| 106 |
-
stop=["<|im_end|>", "<|im_start|>", "im_end", "im_start"],
|
| 107 |
-
)
|
| 108 |
|
| 109 |
-
response = await asyncio.to_thread(
|
| 110 |
-
|
| 111 |
-
content = response["choices"][0]["text"]
|
| 112 |
-
prompt_tokens = response["usage"]["prompt_tokens"]
|
| 113 |
-
completion_tokens = response["usage"]["completion_tokens"]
|
| 114 |
-
|
| 115 |
return {
|
| 116 |
-
"content":
|
| 117 |
"usage": {
|
| 118 |
-
"prompt_tokens": prompt_tokens,
|
| 119 |
-
"completion_tokens": completion_tokens,
|
| 120 |
-
"total_tokens": prompt_tokens + completion_tokens
|
| 121 |
}
|
| 122 |
}
|
| 123 |
|
| 124 |
-
async def generate_stream(
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
messages: Optional[List[Dict[str, str]]] = None,
|
| 129 |
-
temperature: float = 0.7,
|
| 130 |
-
max_tokens: int = 1024,
|
| 131 |
-
top_p: float = 0.9,
|
| 132 |
-
) -> AsyncIterator[str]:
|
| 133 |
if not await self.initialize():
|
| 134 |
-
raise RuntimeError("Local LLM
|
|
|
|
| 135 |
|
| 136 |
-
formatted_prompt = self._format_prompt(prompt, system_prompt, messages)
|
| 137 |
-
|
| 138 |
-
# Generator for streaming
|
| 139 |
def run_stream():
|
| 140 |
-
return self.llm(
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
top_p=top_p,
|
| 145 |
-
stop=["<|im_end|>", "<|im_start|>", "im_end", "im_start"],
|
| 146 |
-
stream=True
|
| 147 |
-
)
|
| 148 |
-
|
| 149 |
stream = await asyncio.to_thread(run_stream)
|
| 150 |
-
|
| 151 |
-
async def
|
| 152 |
for chunk in stream:
|
| 153 |
text = chunk["choices"][0]["text"]
|
| 154 |
if text:
|
| 155 |
yield text
|
| 156 |
-
# Yield CPU control to event loop
|
| 157 |
await asyncio.sleep(0)
|
| 158 |
-
|
| 159 |
-
return
|
| 160 |
|
| 161 |
def get_info(self) -> Dict[str, Any]:
|
| 162 |
return {
|
| 163 |
-
"
|
| 164 |
"initialized": self.initialized,
|
| 165 |
"model_path": self.model_path,
|
| 166 |
"context_length": self.context_length,
|
| 167 |
-
"device": "CPU" # Llama.cpp runs on CPU in basic config, can use GPU via CUDA wrappers
|
| 168 |
}
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
import logging
|
| 3 |
import asyncio
|
| 4 |
from typing import AsyncIterator, List, Dict, Any, Optional
|
|
|
|
| 7 |
|
| 8 |
logger = logging.getLogger(__name__)
|
| 9 |
|
|
|
|
| 10 |
try:
|
| 11 |
from llama_cpp import Llama
|
| 12 |
except ImportError:
|
| 13 |
Llama = None
|
|
|
|
| 14 |
|
| 15 |
class LocalLLMProvider(BaseLLMProvider):
|
| 16 |
def __init__(self):
|
|
|
|
| 22 |
async def initialize(self) -> bool:
|
| 23 |
if self.initialized:
|
| 24 |
return True
|
|
|
|
| 25 |
if Llama is None:
|
| 26 |
+
logger.error("llama-cpp-python not installed.")
|
| 27 |
return False
|
|
|
|
| 28 |
if not os.path.exists(self.model_path):
|
| 29 |
+
logger.error(f"Model not found: {self.model_path}")
|
| 30 |
return False
|
| 31 |
|
| 32 |
try:
|
|
|
|
| 33 |
threads = max(1, (os.cpu_count() or 2) - 1)
|
| 34 |
+
logger.info(
|
| 35 |
+
f"Loading model: {self.model_path} | "
|
| 36 |
+
f"ctx={self.context_length} threads={threads} "
|
| 37 |
+
f"mmap=true mlock=false n_batch=8"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
def load_model():
|
| 41 |
return Llama(
|
| 42 |
model_path=self.model_path,
|
| 43 |
n_ctx=self.context_length,
|
| 44 |
n_threads=threads,
|
| 45 |
+
n_batch=8,
|
| 46 |
+
use_mmap=True,
|
| 47 |
+
use_mlock=False,
|
| 48 |
+
verbose=False,
|
| 49 |
)
|
| 50 |
+
|
| 51 |
self.llm = await asyncio.to_thread(load_model)
|
| 52 |
self.initialized = True
|
| 53 |
+
|
| 54 |
+
model_size_mb = os.path.getsize(self.model_path) / (1024 * 1024)
|
| 55 |
+
logger.info(f"Model loaded ({model_size_mb:.0f}MB on disk, "
|
| 56 |
+
f"memory-mapped: ~{model_size_mb * 0.1:.0f}MB RSS per active page)")
|
| 57 |
return True
|
| 58 |
except Exception as e:
|
| 59 |
+
logger.error(f"Failed to load model: {e}")
|
| 60 |
self.initialized = False
|
| 61 |
self.llm = None
|
| 62 |
return False
|
| 63 |
|
| 64 |
+
def _format_prompt(self, prompt: str, system_prompt: Optional[str] = None,
|
| 65 |
+
messages: Optional[List[Dict[str, str]]] = None) -> str:
|
| 66 |
formatted = ""
|
| 67 |
+
sys_p = system_prompt or "You are a helpful AI coding assistant."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
has_system = any(m.get("role") == "system" for m in messages) if messages else False
|
| 69 |
if not has_system:
|
| 70 |
formatted += f"<|im_start|>system\n{sys_p}<|im_end|>\n"
|
|
|
|
| 71 |
if messages:
|
| 72 |
for msg in messages:
|
| 73 |
+
formatted += f"<|im_start|>{msg['role']}\n{msg['content']}<|im_end|>\n"
|
|
|
|
|
|
|
| 74 |
else:
|
|
|
|
| 75 |
formatted += f"<|im_start|>user\n{prompt}<|im_end|>\n"
|
|
|
|
| 76 |
formatted += "<|im_start|>assistant\n"
|
| 77 |
return formatted
|
| 78 |
|
| 79 |
+
async def generate(self, prompt: str, system_prompt: Optional[str] = None,
|
| 80 |
+
messages: Optional[List[Dict[str, str]]] = None,
|
| 81 |
+
temperature: float = 0.7, max_tokens: int = 512,
|
| 82 |
+
top_p: float = 0.9) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
if not await self.initialize():
|
| 84 |
+
raise RuntimeError("Local LLM not initialized.")
|
| 85 |
+
formatted = self._format_prompt(prompt, system_prompt, messages)
|
| 86 |
+
|
| 87 |
+
def run():
|
| 88 |
+
return self.llm(prompt=formatted, max_tokens=max_tokens,
|
| 89 |
+
temperature=temperature, top_p=top_p,
|
| 90 |
+
stop=["<|im_end|>", "<|im_start|>"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
response = await asyncio.to_thread(run)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
return {
|
| 94 |
+
"content": response["choices"][0]["text"],
|
| 95 |
"usage": {
|
| 96 |
+
"prompt_tokens": response["usage"]["prompt_tokens"],
|
| 97 |
+
"completion_tokens": response["usage"]["completion_tokens"],
|
| 98 |
+
"total_tokens": response["usage"]["prompt_tokens"] + response["usage"]["completion_tokens"]
|
| 99 |
}
|
| 100 |
}
|
| 101 |
|
| 102 |
+
async def generate_stream(self, prompt: str, system_prompt: Optional[str] = None,
|
| 103 |
+
messages: Optional[List[Dict[str, str]]] = None,
|
| 104 |
+
temperature: float = 0.7, max_tokens: int = 512,
|
| 105 |
+
top_p: float = 0.9) -> AsyncIterator[str]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
if not await self.initialize():
|
| 107 |
+
raise RuntimeError("Local LLM not initialized.")
|
| 108 |
+
formatted = self._format_prompt(prompt, system_prompt, messages)
|
| 109 |
|
|
|
|
|
|
|
|
|
|
| 110 |
def run_stream():
|
| 111 |
+
return self.llm(prompt=formatted, max_tokens=max_tokens,
|
| 112 |
+
temperature=temperature, top_p=top_p,
|
| 113 |
+
stop=["<|im_end|>", "<|im_start|>"], stream=True)
|
| 114 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
stream = await asyncio.to_thread(run_stream)
|
| 116 |
+
|
| 117 |
+
async def async_gen():
|
| 118 |
for chunk in stream:
|
| 119 |
text = chunk["choices"][0]["text"]
|
| 120 |
if text:
|
| 121 |
yield text
|
|
|
|
| 122 |
await asyncio.sleep(0)
|
| 123 |
+
|
| 124 |
+
return async_gen()
|
| 125 |
|
| 126 |
def get_info(self) -> Dict[str, Any]:
|
| 127 |
return {
|
| 128 |
+
"provider": "local",
|
| 129 |
"initialized": self.initialized,
|
| 130 |
"model_path": self.model_path,
|
| 131 |
"context_length": self.context_length,
|
|
|
|
| 132 |
}
|
backend/app/llm/manager.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
import logging
|
| 3 |
-
import asyncio
|
| 4 |
from typing import AsyncIterator, Dict, Any, Optional
|
| 5 |
-
from backend.app.config import settings
|
| 6 |
from backend.app.llm.base import BaseLLMProvider
|
| 7 |
from backend.app.llm.local import LocalLLMProvider
|
| 8 |
|
|
@@ -11,57 +9,15 @@ logger = logging.getLogger(__name__)
|
|
| 11 |
class LLMManager:
|
| 12 |
def __init__(self):
|
| 13 |
self.provider: Optional[BaseLLMProvider] = None
|
| 14 |
-
self.is_downloading: bool = False
|
| 15 |
|
| 16 |
-
async def
|
| 17 |
-
"
|
| 18 |
-
model_path = os.path.abspath(settings.LOCAL_MODEL_PATH)
|
| 19 |
-
if os.path.exists(model_path):
|
| 20 |
-
logger.info(f"Model file already exists at: {model_path}")
|
| 21 |
-
return True
|
| 22 |
-
|
| 23 |
-
# Ensure directory exists
|
| 24 |
-
os.makedirs(os.path.dirname(model_path), exist_ok=True)
|
| 25 |
-
|
| 26 |
-
self.is_downloading = True
|
| 27 |
-
logger.info(f"Downloading model {settings.LOCAL_MODEL_FILE} from repo {settings.LOCAL_MODEL_REPO}...")
|
| 28 |
-
|
| 29 |
-
try:
|
| 30 |
-
from huggingface_hub import hf_hub_download
|
| 31 |
-
|
| 32 |
-
def download_job():
|
| 33 |
-
return hf_hub_download(
|
| 34 |
-
repo_id=settings.LOCAL_MODEL_REPO,
|
| 35 |
-
filename=settings.LOCAL_MODEL_FILE,
|
| 36 |
-
local_dir=os.path.dirname(model_path),
|
| 37 |
-
local_dir_use_symlinks=False
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
# Download model in background thread to not block the main application loop
|
| 41 |
-
await asyncio.to_thread(download_job)
|
| 42 |
-
logger.info("Model download complete!")
|
| 43 |
-
self.is_downloading = False
|
| 44 |
-
return True
|
| 45 |
-
except Exception as e:
|
| 46 |
-
logger.error(f"Failed to download model automatically: {e}", exc_info=True)
|
| 47 |
-
self.is_downloading = False
|
| 48 |
-
return False
|
| 49 |
-
|
| 50 |
-
async def setup_provider(self) -> BaseLLMProvider:
|
| 51 |
-
"""
|
| 52 |
-
Initializes the local LLM provider strictly.
|
| 53 |
-
"""
|
| 54 |
-
logger.info("Setting up local provider for SmolLM2...")
|
| 55 |
-
await self.ensure_model_downloaded()
|
| 56 |
-
|
| 57 |
local = LocalLLMProvider()
|
| 58 |
-
# Initialize in background, do not raise exception on failure to support diagnostics
|
| 59 |
await local.initialize()
|
| 60 |
self.provider = local
|
| 61 |
return local
|
| 62 |
|
| 63 |
async def get_active_provider(self) -> BaseLLMProvider:
|
| 64 |
-
"""Returns the active provider. Lazily initializes if needed."""
|
| 65 |
if not self.provider:
|
| 66 |
await self.setup_provider()
|
| 67 |
return self.provider
|
|
@@ -75,15 +31,7 @@ class LLMManager:
|
|
| 75 |
return await p.generate_stream(*args, **kwargs)
|
| 76 |
|
| 77 |
async def get_status_info(self) -> Dict[str, Any]:
|
| 78 |
-
"""Returns diagnostic and current provider stats."""
|
| 79 |
p = await self.get_active_provider()
|
| 80 |
-
|
| 81 |
-
info.update({
|
| 82 |
-
"configured_mode": "local",
|
| 83 |
-
"active_mode": "local",
|
| 84 |
-
"is_downloading": self.is_downloading
|
| 85 |
-
})
|
| 86 |
-
return info
|
| 87 |
|
| 88 |
-
# Instantiate global singleton manager
|
| 89 |
llm_manager = LLMManager()
|
|
|
|
| 1 |
import os
|
| 2 |
import logging
|
|
|
|
| 3 |
from typing import AsyncIterator, Dict, Any, Optional
|
|
|
|
| 4 |
from backend.app.llm.base import BaseLLMProvider
|
| 5 |
from backend.app.llm.local import LocalLLMProvider
|
| 6 |
|
|
|
|
| 9 |
class LLMManager:
|
| 10 |
def __init__(self):
|
| 11 |
self.provider: Optional[BaseLLMProvider] = None
|
|
|
|
| 12 |
|
| 13 |
+
async def setup_provider(self):
|
| 14 |
+
logger.info("Initializing local provider...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
local = LocalLLMProvider()
|
|
|
|
| 16 |
await local.initialize()
|
| 17 |
self.provider = local
|
| 18 |
return local
|
| 19 |
|
| 20 |
async def get_active_provider(self) -> BaseLLMProvider:
|
|
|
|
| 21 |
if not self.provider:
|
| 22 |
await self.setup_provider()
|
| 23 |
return self.provider
|
|
|
|
| 31 |
return await p.generate_stream(*args, **kwargs)
|
| 32 |
|
| 33 |
async def get_status_info(self) -> Dict[str, Any]:
|
|
|
|
| 34 |
p = await self.get_active_provider()
|
| 35 |
+
return p.get_info()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
|
|
|
| 37 |
llm_manager = LLMManager()
|
backend/app/main.py
CHANGED
|
@@ -3,102 +3,57 @@ import time
|
|
| 3 |
import uuid
|
| 4 |
import logging
|
| 5 |
import asyncio
|
| 6 |
-
import psutil
|
| 7 |
from contextlib import asynccontextmanager
|
| 8 |
from typing import AsyncGenerator, Dict, Any
|
| 9 |
|
| 10 |
-
from fastapi import FastAPI,
|
| 11 |
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
-
from fastapi.responses import StreamingResponse
|
| 13 |
from fastapi.staticfiles import StaticFiles
|
| 14 |
|
| 15 |
from backend.app.config import settings
|
| 16 |
from backend.app.models import (
|
| 17 |
-
ChatRequest, ChatResponse,
|
| 18 |
-
CodeRequest, CompletionRequest, ModelInfoResponse
|
| 19 |
)
|
| 20 |
-
from backend.app.middleware import
|
| 21 |
from backend.app.llm.manager import llm_manager
|
| 22 |
from backend.app.utils import (
|
| 23 |
-
estimate_tokens, format_sse_chunk,
|
| 24 |
-
get_code_prompt, get_completion_prompt
|
| 25 |
)
|
| 26 |
|
| 27 |
-
# Setup logging configuration
|
| 28 |
logging.basicConfig(
|
| 29 |
level=logging.INFO if not settings.DEBUG else logging.DEBUG,
|
| 30 |
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
|
| 31 |
)
|
| 32 |
logger = logging.getLogger("backend.app.main")
|
| 33 |
|
| 34 |
-
# Metrics memory store
|
| 35 |
-
class SystemMetrics:
|
| 36 |
-
def __init__(self):
|
| 37 |
-
self.total_requests = 0
|
| 38 |
-
self.total_prompt_tokens = 0
|
| 39 |
-
self.total_completion_tokens = 0
|
| 40 |
-
self.total_generation_time_sec = 0.0
|
| 41 |
-
|
| 42 |
-
def add(self, prompt_tokens: int, completion_tokens: int, duration: float):
|
| 43 |
-
self.total_requests += 1
|
| 44 |
-
self.total_prompt_tokens += prompt_tokens
|
| 45 |
-
self.total_completion_tokens += completion_tokens
|
| 46 |
-
self.total_generation_time_sec += duration
|
| 47 |
-
|
| 48 |
-
def get_metrics_report(self) -> Dict[str, Any]:
|
| 49 |
-
avg_latency = (
|
| 50 |
-
self.total_generation_time_sec / self.total_requests
|
| 51 |
-
if self.total_requests > 0 else 0.0
|
| 52 |
-
)
|
| 53 |
-
return {
|
| 54 |
-
"total_requests": self.total_requests,
|
| 55 |
-
"total_prompt_tokens": self.total_prompt_tokens,
|
| 56 |
-
"total_completion_tokens": self.total_completion_tokens,
|
| 57 |
-
"total_tokens": self.total_prompt_tokens + self.total_completion_tokens,
|
| 58 |
-
"average_latency_seconds": round(avg_latency, 4),
|
| 59 |
-
"total_generation_time_seconds": round(self.total_generation_time_sec, 2)
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
metrics = SystemMetrics()
|
| 63 |
|
| 64 |
@asynccontextmanager
|
| 65 |
async def lifespan(app: FastAPI):
|
| 66 |
-
|
| 67 |
-
logger.info("Initializing LLM Manager and warming up model in the background...")
|
| 68 |
asyncio.create_task(llm_manager.setup_provider())
|
| 69 |
yield
|
| 70 |
-
|
| 71 |
-
logger.info("Server shutting down.")
|
| 72 |
|
| 73 |
-
# Main app instantiation
|
| 74 |
-
app = FastAPI(
|
| 75 |
-
title="AI Coding Assistant API",
|
| 76 |
-
version="1.0.0",
|
| 77 |
-
lifespan=lifespan
|
| 78 |
-
)
|
| 79 |
|
| 80 |
-
|
| 81 |
-
app.add_middleware(LoggingMiddleware)
|
| 82 |
-
app.add_middleware(RateLimitMiddleware, limit=settings.RATE_LIMIT_PER_MINUTE)
|
| 83 |
|
|
|
|
| 84 |
app.add_middleware(
|
| 85 |
CORSMiddleware,
|
| 86 |
-
allow_origins=
|
| 87 |
allow_credentials=True,
|
| 88 |
allow_methods=["*"],
|
| 89 |
allow_headers=["*"],
|
| 90 |
)
|
| 91 |
|
| 92 |
-
|
| 93 |
-
async def
|
| 94 |
-
prompt: str,
|
| 95 |
-
|
| 96 |
-
messages: list = None,
|
| 97 |
-
temperature: float = 0.7,
|
| 98 |
-
max_tokens: int = 1024,
|
| 99 |
) -> AsyncGenerator[str, None]:
|
| 100 |
-
|
| 101 |
-
|
| 102 |
prompt_tokens = estimate_tokens(prompt)
|
| 103 |
if messages:
|
| 104 |
for m in messages:
|
|
@@ -106,212 +61,108 @@ async def run_stream_generator(
|
|
| 106 |
|
| 107 |
try:
|
| 108 |
stream = await llm_manager.generate_stream(
|
| 109 |
-
prompt=prompt,
|
| 110 |
-
|
| 111 |
-
messages=messages,
|
| 112 |
-
temperature=temperature,
|
| 113 |
-
max_tokens=max_tokens
|
| 114 |
)
|
| 115 |
-
|
| 116 |
async for chunk in stream:
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
completion_tokens = estimate_tokens(generated_content)
|
| 123 |
-
metrics.add(prompt_tokens, completion_tokens, duration)
|
| 124 |
-
|
| 125 |
-
# Send final chunk with usage metrics
|
| 126 |
-
usage_data = {
|
| 127 |
"prompt_tokens": prompt_tokens,
|
| 128 |
"completion_tokens": completion_tokens,
|
| 129 |
"total_tokens": prompt_tokens + completion_tokens,
|
| 130 |
"duration_ms": int(duration * 1000),
|
| 131 |
-
"tokens_per_second": round(completion_tokens / duration, 2) if duration > 0 else 0
|
| 132 |
-
}
|
| 133 |
-
yield format_sse_chunk(content="", done=True, usage=usage_data)
|
| 134 |
-
|
| 135 |
except Exception as e:
|
| 136 |
-
logger.error(f"
|
| 137 |
-
yield format_sse_chunk(content=f"\n[
|
|
|
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
system_prompt: str = None,
|
| 143 |
-
messages: list = None,
|
| 144 |
-
temperature: float = 0.7,
|
| 145 |
-
max_tokens: int = 1024,
|
| 146 |
) -> ChatResponse:
|
| 147 |
-
|
| 148 |
try:
|
| 149 |
-
|
| 150 |
-
prompt=prompt,
|
| 151 |
-
|
| 152 |
-
messages=messages,
|
| 153 |
-
temperature=temperature,
|
| 154 |
-
max_tokens=max_tokens
|
| 155 |
)
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
usage = response_data.get("usage", {})
|
| 159 |
-
|
| 160 |
-
prompt_tokens = usage.get("prompt_tokens", estimate_tokens(prompt))
|
| 161 |
-
completion_tokens = usage.get("completion_tokens", estimate_tokens(response_data["content"]))
|
| 162 |
-
metrics.add(prompt_tokens, completion_tokens, duration)
|
| 163 |
-
|
| 164 |
-
# Build enriched usage dict
|
| 165 |
-
metrics_dict = {
|
| 166 |
-
"prompt_tokens": prompt_tokens,
|
| 167 |
-
"completion_tokens": completion_tokens,
|
| 168 |
-
"total_tokens": prompt_tokens + completion_tokens,
|
| 169 |
-
"duration_ms": int(duration * 1000),
|
| 170 |
-
"tokens_per_second": round(completion_tokens / duration, 2) if duration > 0 else 0
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
status_info = await llm_manager.get_status_info()
|
| 174 |
-
|
| 175 |
return ChatResponse(
|
| 176 |
id=str(uuid.uuid4()),
|
| 177 |
-
content=
|
| 178 |
-
usage=
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
)
|
| 181 |
except Exception as e:
|
| 182 |
-
logger.error(f"
|
| 183 |
-
raise HTTPException(
|
| 184 |
-
|
| 185 |
-
detail=str(e)
|
| 186 |
-
)
|
| 187 |
|
| 188 |
-
# API ENDPOINTS
|
| 189 |
@app.post("/chat")
|
| 190 |
async def chat(request: ChatRequest):
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
# Extract system prompt if present in payload
|
| 194 |
system_prompt = None
|
| 195 |
-
|
| 196 |
-
for
|
| 197 |
-
if
|
| 198 |
-
system_prompt =
|
| 199 |
else:
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
# The last user message is the primary prompt
|
| 203 |
-
last_prompt = user_messages[-1]["content"] if user_messages else ""
|
| 204 |
|
| 205 |
if request.stream:
|
| 206 |
return StreamingResponse(
|
| 207 |
-
|
| 208 |
-
prompt=last_prompt,
|
| 209 |
-
|
| 210 |
-
messages=messages_list,
|
| 211 |
temperature=request.temperature or settings.DEFAULT_TEMPERATURE,
|
| 212 |
-
max_tokens=request.max_tokens or settings.DEFAULT_MAX_TOKENS
|
| 213 |
),
|
| 214 |
-
media_type="text/event-stream"
|
| 215 |
-
)
|
| 216 |
-
else:
|
| 217 |
-
return await run_standard_generation(
|
| 218 |
-
prompt=last_prompt,
|
| 219 |
-
system_prompt=system_prompt,
|
| 220 |
-
messages=messages_list,
|
| 221 |
-
temperature=request.temperature or settings.DEFAULT_TEMPERATURE,
|
| 222 |
-
max_tokens=request.max_tokens or settings.DEFAULT_MAX_TOKENS
|
| 223 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
@app.post("/complete")
|
| 226 |
async def complete(request: CompletionRequest):
|
| 227 |
prompt = get_completion_prompt(request.prefix, request.suffix, request.language or "python")
|
| 228 |
-
|
| 229 |
-
# We want low temperature for completion tasks
|
| 230 |
-
temp = request.temperature or 0.2
|
| 231 |
-
max_t = request.max_tokens or 128
|
| 232 |
-
|
| 233 |
if request.stream:
|
| 234 |
return StreamingResponse(
|
| 235 |
-
|
| 236 |
-
|
|
|
|
| 237 |
)
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
# Helper decorator for modular code API endpoints
|
| 242 |
-
def make_code_endpoint(action: str):
|
| 243 |
-
async def endpoint(request: CodeRequest):
|
| 244 |
-
prompt = get_code_prompt(action, request.code, request.language or "python", request.context)
|
| 245 |
-
temp = request.temperature or settings.DEFAULT_TEMPERATURE
|
| 246 |
-
max_t = request.max_tokens or settings.DEFAULT_MAX_TOKENS
|
| 247 |
-
|
| 248 |
-
if request.stream:
|
| 249 |
-
return StreamingResponse(
|
| 250 |
-
run_stream_generator(prompt=prompt, temperature=temp, max_tokens=max_t),
|
| 251 |
-
media_type="text/event-stream"
|
| 252 |
-
)
|
| 253 |
-
else:
|
| 254 |
-
return await run_standard_generation(prompt=prompt, temperature=temp, max_tokens=max_t)
|
| 255 |
-
return endpoint
|
| 256 |
|
| 257 |
-
# Register code utility endpoints
|
| 258 |
-
app.post("/explain")(make_code_endpoint("explain"))
|
| 259 |
-
app.post("/debug")(make_code_endpoint("debug"))
|
| 260 |
-
app.post("/refactor")(make_code_endpoint("refactor"))
|
| 261 |
-
app.post("/generate-tests")(make_code_endpoint("generate-tests"))
|
| 262 |
-
app.post("/summarize")(make_code_endpoint("summarize"))
|
| 263 |
|
| 264 |
@app.get("/health")
|
| 265 |
async def health():
|
| 266 |
-
|
| 267 |
-
status_info = await llm_manager.get_status_info()
|
| 268 |
return {
|
| 269 |
-
"status": "
|
| 270 |
-
"
|
| 271 |
-
"active_mode": status_info.get("active_mode"),
|
| 272 |
-
"local_model_loaded": status_info.get("initialized", False),
|
| 273 |
-
"is_downloading": status_info.get("is_downloading", False)
|
| 274 |
}
|
| 275 |
|
| 276 |
-
@app.get("/metrics")
|
| 277 |
-
async def get_metrics():
|
| 278 |
-
# Enriches metrics with current server state
|
| 279 |
-
report = metrics.get_metrics_report()
|
| 280 |
-
status_info = await llm_manager.get_status_info()
|
| 281 |
-
ram_stats = psutil.virtual_memory()
|
| 282 |
-
report.update({
|
| 283 |
-
"active_mode": "local",
|
| 284 |
-
"system_ram_gb": round(ram_stats.total / (1024 ** 3), 2),
|
| 285 |
-
"device": status_info.get("device", "CPU")
|
| 286 |
-
})
|
| 287 |
-
return report
|
| 288 |
-
|
| 289 |
-
@app.get("/model-info", response_model=ModelInfoResponse)
|
| 290 |
-
async def get_model_info():
|
| 291 |
-
status_info = await llm_manager.get_status_info()
|
| 292 |
-
|
| 293 |
-
# Calculate memory stats
|
| 294 |
-
ram_stats = psutil.virtual_memory()
|
| 295 |
-
total_gb = ram_stats.total / (1024 ** 3)
|
| 296 |
-
used_gb = ram_stats.used / (1024 ** 3)
|
| 297 |
-
|
| 298 |
-
local_exists = os.path.exists(settings.LOCAL_MODEL_PATH)
|
| 299 |
-
|
| 300 |
-
return ModelInfoResponse(
|
| 301 |
-
model_name=os.path.basename(settings.LOCAL_MODEL_PATH),
|
| 302 |
-
inference_mode="local",
|
| 303 |
-
status="loaded" if status_info.get("initialized") else "loading",
|
| 304 |
-
memory_usage_gb=round(used_gb, 2),
|
| 305 |
-
total_memory_gb=round(total_gb, 2),
|
| 306 |
-
local_model_exists=local_exists,
|
| 307 |
-
local_model_path=settings.LOCAL_MODEL_PATH,
|
| 308 |
-
device=status_info.get("device", "CPU")
|
| 309 |
-
)
|
| 310 |
|
| 311 |
-
# Serve Frontend static assets if available (production mode)
|
| 312 |
frontend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "frontend", "dist"))
|
| 313 |
if os.path.exists(frontend_dir):
|
| 314 |
app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend")
|
| 315 |
-
logger.info(f"
|
| 316 |
else:
|
| 317 |
-
logger.warning(
|
|
|
|
| 3 |
import uuid
|
| 4 |
import logging
|
| 5 |
import asyncio
|
|
|
|
| 6 |
from contextlib import asynccontextmanager
|
| 7 |
from typing import AsyncGenerator, Dict, Any
|
| 8 |
|
| 9 |
+
from fastapi import FastAPI, HTTPException, status
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
+
from fastapi.responses import StreamingResponse
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
|
| 14 |
from backend.app.config import settings
|
| 15 |
from backend.app.models import (
|
| 16 |
+
ChatRequest, ChatResponse, CompletionRequest
|
|
|
|
| 17 |
)
|
| 18 |
+
from backend.app.middleware import LoggingMiddleware
|
| 19 |
from backend.app.llm.manager import llm_manager
|
| 20 |
from backend.app.utils import (
|
| 21 |
+
estimate_tokens, format_sse_chunk, get_completion_prompt
|
|
|
|
| 22 |
)
|
| 23 |
|
|
|
|
| 24 |
logging.basicConfig(
|
| 25 |
level=logging.INFO if not settings.DEBUG else logging.DEBUG,
|
| 26 |
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
|
| 27 |
)
|
| 28 |
logger = logging.getLogger("backend.app.main")
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
@asynccontextmanager
|
| 32 |
async def lifespan(app: FastAPI):
|
| 33 |
+
logger.info("Warming up model in background...")
|
|
|
|
| 34 |
asyncio.create_task(llm_manager.setup_provider())
|
| 35 |
yield
|
| 36 |
+
logger.info("Shutting down.")
|
|
|
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
app = FastAPI(title="Levi AI Coder", version="1.0.0", lifespan=lifespan)
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
app.add_middleware(LoggingMiddleware)
|
| 42 |
app.add_middleware(
|
| 43 |
CORSMiddleware,
|
| 44 |
+
allow_origins=["*"],
|
| 45 |
allow_credentials=True,
|
| 46 |
allow_methods=["*"],
|
| 47 |
allow_headers=["*"],
|
| 48 |
)
|
| 49 |
|
| 50 |
+
|
| 51 |
+
async def run_stream(
|
| 52 |
+
prompt: str, system_prompt: str = None, messages: list = None,
|
| 53 |
+
temperature: float = 0.7, max_tokens: int = 512,
|
|
|
|
|
|
|
|
|
|
| 54 |
) -> AsyncGenerator[str, None]:
|
| 55 |
+
start = time.time()
|
| 56 |
+
generated = ""
|
| 57 |
prompt_tokens = estimate_tokens(prompt)
|
| 58 |
if messages:
|
| 59 |
for m in messages:
|
|
|
|
| 61 |
|
| 62 |
try:
|
| 63 |
stream = await llm_manager.generate_stream(
|
| 64 |
+
prompt=prompt, system_prompt=system_prompt,
|
| 65 |
+
messages=messages, temperature=temperature, max_tokens=max_tokens,
|
|
|
|
|
|
|
|
|
|
| 66 |
)
|
|
|
|
| 67 |
async for chunk in stream:
|
| 68 |
+
generated += chunk
|
| 69 |
+
yield format_sse_chunk(content=chunk)
|
| 70 |
+
duration = time.time() - start
|
| 71 |
+
completion_tokens = estimate_tokens(generated)
|
| 72 |
+
yield format_sse_chunk(content="", done=True, usage={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
"prompt_tokens": prompt_tokens,
|
| 74 |
"completion_tokens": completion_tokens,
|
| 75 |
"total_tokens": prompt_tokens + completion_tokens,
|
| 76 |
"duration_ms": int(duration * 1000),
|
| 77 |
+
"tokens_per_second": round(completion_tokens / duration, 2) if duration > 0 else 0,
|
| 78 |
+
})
|
|
|
|
|
|
|
| 79 |
except Exception as e:
|
| 80 |
+
logger.error(f"Stream error: {e}")
|
| 81 |
+
yield format_sse_chunk(content=f"\n[Error: {e}]", done=True)
|
| 82 |
+
|
| 83 |
|
| 84 |
+
async def run_standard(
|
| 85 |
+
prompt: str, system_prompt: str = None, messages: list = None,
|
| 86 |
+
temperature: float = 0.7, max_tokens: int = 512,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
) -> ChatResponse:
|
| 88 |
+
start = time.time()
|
| 89 |
try:
|
| 90 |
+
result = await llm_manager.generate(
|
| 91 |
+
prompt=prompt, system_prompt=system_prompt,
|
| 92 |
+
messages=messages, temperature=temperature, max_tokens=max_tokens,
|
|
|
|
|
|
|
|
|
|
| 93 |
)
|
| 94 |
+
duration = time.time() - start
|
| 95 |
+
usage = result.get("usage", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
return ChatResponse(
|
| 97 |
id=str(uuid.uuid4()),
|
| 98 |
+
content=result["content"],
|
| 99 |
+
usage={
|
| 100 |
+
"prompt_tokens": usage.get("prompt_tokens", 0),
|
| 101 |
+
"completion_tokens": usage.get("completion_tokens", 0),
|
| 102 |
+
"total_tokens": usage.get("total_tokens", 0),
|
| 103 |
+
"duration_ms": int(duration * 1000),
|
| 104 |
+
},
|
| 105 |
+
model="SmolLM2-360M-Q4_K_M",
|
| 106 |
)
|
| 107 |
except Exception as e:
|
| 108 |
+
logger.error(f"Generation error: {e}")
|
| 109 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
| 110 |
+
|
|
|
|
|
|
|
| 111 |
|
|
|
|
| 112 |
@app.post("/chat")
|
| 113 |
async def chat(request: ChatRequest):
|
| 114 |
+
msgs = [m.model_dump() for m in request.messages]
|
|
|
|
|
|
|
| 115 |
system_prompt = None
|
| 116 |
+
user_msgs = []
|
| 117 |
+
for m in msgs:
|
| 118 |
+
if m["role"] == "system":
|
| 119 |
+
system_prompt = m["content"]
|
| 120 |
else:
|
| 121 |
+
user_msgs.append(m)
|
| 122 |
+
last_prompt = user_msgs[-1]["content"] if user_msgs else ""
|
|
|
|
|
|
|
| 123 |
|
| 124 |
if request.stream:
|
| 125 |
return StreamingResponse(
|
| 126 |
+
run_stream(
|
| 127 |
+
prompt=last_prompt, system_prompt=system_prompt,
|
| 128 |
+
messages=msgs,
|
|
|
|
| 129 |
temperature=request.temperature or settings.DEFAULT_TEMPERATURE,
|
| 130 |
+
max_tokens=request.max_tokens or settings.DEFAULT_MAX_TOKENS,
|
| 131 |
),
|
| 132 |
+
media_type="text/event-stream",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
)
|
| 134 |
+
return await run_standard(
|
| 135 |
+
prompt=last_prompt, system_prompt=system_prompt, messages=msgs,
|
| 136 |
+
temperature=request.temperature or settings.DEFAULT_TEMPERATURE,
|
| 137 |
+
max_tokens=request.max_tokens or settings.DEFAULT_MAX_TOKENS,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
|
| 141 |
@app.post("/complete")
|
| 142 |
async def complete(request: CompletionRequest):
|
| 143 |
prompt = get_completion_prompt(request.prefix, request.suffix, request.language or "python")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
if request.stream:
|
| 145 |
return StreamingResponse(
|
| 146 |
+
run_stream(prompt=prompt, temperature=request.temperature or 0.2,
|
| 147 |
+
max_tokens=request.max_tokens or 128),
|
| 148 |
+
media_type="text/event-stream",
|
| 149 |
)
|
| 150 |
+
return await run_standard(prompt=prompt, temperature=request.temperature or 0.2,
|
| 151 |
+
max_tokens=request.max_tokens or 128)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
@app.get("/health")
|
| 155 |
async def health():
|
| 156 |
+
info = await llm_manager.get_status_info()
|
|
|
|
| 157 |
return {
|
| 158 |
+
"status": "ok",
|
| 159 |
+
"model_loaded": info.get("initialized", False),
|
|
|
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
|
|
|
| 163 |
frontend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "frontend", "dist"))
|
| 164 |
if os.path.exists(frontend_dir):
|
| 165 |
app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend")
|
| 166 |
+
logger.info(f"Serving frontend from {frontend_dir}")
|
| 167 |
else:
|
| 168 |
+
logger.warning("No frontend dist found. API-only mode.")
|
backend/app/middleware.py
CHANGED
|
@@ -1,73 +1,21 @@
|
|
| 1 |
import time
|
| 2 |
import logging
|
| 3 |
-
from
|
| 4 |
-
from fastapi import
|
| 5 |
-
from fastapi.responses import JSONResponse
|
| 6 |
from starlette.middleware.base import BaseHTTPMiddleware
|
| 7 |
-
from backend.app.config import settings
|
| 8 |
|
| 9 |
logger = logging.getLogger(__name__)
|
| 10 |
|
| 11 |
-
class RateLimiter:
|
| 12 |
-
def __init__(self, requests_per_minute: int):
|
| 13 |
-
self.limit = requests_per_minute
|
| 14 |
-
self.clients = defaultdict(list)
|
| 15 |
-
|
| 16 |
-
def is_allowed(self, ip: str) -> bool:
|
| 17 |
-
now = time.time()
|
| 18 |
-
# Clean up requests older than 60 seconds
|
| 19 |
-
self.clients[ip] = [req_time for req_time in self.clients[ip] if now - req_time < 60]
|
| 20 |
-
|
| 21 |
-
if len(self.clients[ip]) >= self.limit:
|
| 22 |
-
return False
|
| 23 |
-
|
| 24 |
-
self.clients[ip].append(now)
|
| 25 |
-
return True
|
| 26 |
-
|
| 27 |
-
class RateLimitMiddleware(BaseHTTPMiddleware):
|
| 28 |
-
def __init__(self, app, limit: int = None):
|
| 29 |
-
super().__init__(app)
|
| 30 |
-
self.limiter = RateLimiter(limit or settings.RATE_LIMIT_PER_MINUTE)
|
| 31 |
-
|
| 32 |
-
async def dispatch(self, request: Request, call_next) -> Response:
|
| 33 |
-
# Bypass rate limits for health and metric endpoints
|
| 34 |
-
if request.url.path in ["/health", "/metrics", "/model-info"]:
|
| 35 |
-
return await call_next(request)
|
| 36 |
-
|
| 37 |
-
# Get client IP
|
| 38 |
-
client_ip = request.client.host if request.client else "unknown"
|
| 39 |
-
|
| 40 |
-
if not self.limiter.is_allowed(client_ip):
|
| 41 |
-
logger.warning(f"Rate limit exceeded for client: {client_ip} on path {request.url.path}")
|
| 42 |
-
return JSONResponse(
|
| 43 |
-
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
| 44 |
-
content={"detail": "Too many requests. Please try again in a minute."}
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
return await call_next(request)
|
| 48 |
-
|
| 49 |
class LoggingMiddleware(BaseHTTPMiddleware):
|
| 50 |
async def dispatch(self, request: Request, call_next) -> Response:
|
| 51 |
start_time = time.time()
|
| 52 |
-
client_ip = request.client.host if request.client else "unknown"
|
| 53 |
-
logger.info(f"Incoming: {request.method} {request.url.path} from {client_ip}")
|
| 54 |
-
|
| 55 |
try:
|
| 56 |
response = await call_next(request)
|
| 57 |
duration = time.time() - start_time
|
| 58 |
-
|
| 59 |
-
f"
|
| 60 |
-
f"Status: {response.status_code} - Duration: {duration:.4f}s"
|
| 61 |
-
)
|
| 62 |
return response
|
| 63 |
except Exception as e:
|
| 64 |
duration = time.time() - start_time
|
| 65 |
-
logger.error(
|
| 66 |
-
|
| 67 |
-
f"Error: {e} - Duration: {duration:.4f}s",
|
| 68 |
-
exc_info=True
|
| 69 |
-
)
|
| 70 |
-
return JSONResponse(
|
| 71 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 72 |
-
content={"detail": "Internal server error occurred."}
|
| 73 |
-
)
|
|
|
|
| 1 |
import time
|
| 2 |
import logging
|
| 3 |
+
from fastapi import Request
|
| 4 |
+
from fastapi.responses import Response
|
|
|
|
| 5 |
from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
|
| 6 |
|
| 7 |
logger = logging.getLogger(__name__)
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
class LoggingMiddleware(BaseHTTPMiddleware):
|
| 10 |
async def dispatch(self, request: Request, call_next) -> Response:
|
| 11 |
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
| 12 |
try:
|
| 13 |
response = await call_next(request)
|
| 14 |
duration = time.time() - start_time
|
| 15 |
+
if duration > 1.0:
|
| 16 |
+
logger.info(f"{request.method} {request.url.path} - {response.status_code} - {duration:.2f}s")
|
|
|
|
|
|
|
| 17 |
return response
|
| 18 |
except Exception as e:
|
| 19 |
duration = time.time() - start_time
|
| 20 |
+
logger.error(f"{request.method} {request.url.path} failed - {e} - {duration:.2f}s")
|
| 21 |
+
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/models.py
CHANGED
|
@@ -2,31 +2,23 @@ from typing import List, Optional, Dict, Any
|
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
|
| 4 |
class ChatMessage(BaseModel):
|
| 5 |
-
role: str
|
| 6 |
-
content: str
|
| 7 |
|
| 8 |
class ChatRequest(BaseModel):
|
| 9 |
-
messages: List[ChatMessage]
|
| 10 |
-
temperature: Optional[float] =
|
| 11 |
-
max_tokens: Optional[int] =
|
| 12 |
-
top_p: Optional[float] =
|
| 13 |
-
stream: Optional[bool] =
|
| 14 |
-
|
| 15 |
-
class CodeRequest(BaseModel):
|
| 16 |
-
code: str = Field(..., description="The code snippet to process")
|
| 17 |
-
language: Optional[str] = Field("python", description="Programming language of the code")
|
| 18 |
-
context: Optional[str] = Field(None, description="Additional developer instructions or query context")
|
| 19 |
-
temperature: Optional[float] = Field(None)
|
| 20 |
-
max_tokens: Optional[int] = Field(None)
|
| 21 |
-
stream: Optional[bool] = Field(True)
|
| 22 |
|
| 23 |
class CompletionRequest(BaseModel):
|
| 24 |
-
prefix: str
|
| 25 |
-
suffix: Optional[str] =
|
| 26 |
-
language: Optional[str] =
|
| 27 |
-
max_tokens: Optional[int] =
|
| 28 |
-
temperature: Optional[float] =
|
| 29 |
-
stream: Optional[bool] =
|
| 30 |
|
| 31 |
class ChatResponseChunk(BaseModel):
|
| 32 |
content: str
|
|
@@ -38,13 +30,3 @@ class ChatResponse(BaseModel):
|
|
| 38 |
content: str
|
| 39 |
usage: Dict[str, Any]
|
| 40 |
model: str
|
| 41 |
-
|
| 42 |
-
class ModelInfoResponse(BaseModel):
|
| 43 |
-
model_name: str
|
| 44 |
-
inference_mode: str # "local" or "huggingface"
|
| 45 |
-
status: str # "loaded", "error", "fallback"
|
| 46 |
-
memory_usage_gb: float
|
| 47 |
-
total_memory_gb: float
|
| 48 |
-
local_model_exists: bool
|
| 49 |
-
local_model_path: str
|
| 50 |
-
device: str # "cpu", "cuda", etc.
|
|
|
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
|
| 4 |
class ChatMessage(BaseModel):
|
| 5 |
+
role: str
|
| 6 |
+
content: str
|
| 7 |
|
| 8 |
class ChatRequest(BaseModel):
|
| 9 |
+
messages: List[ChatMessage]
|
| 10 |
+
temperature: Optional[float] = None
|
| 11 |
+
max_tokens: Optional[int] = None
|
| 12 |
+
top_p: Optional[float] = None
|
| 13 |
+
stream: Optional[bool] = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
class CompletionRequest(BaseModel):
|
| 16 |
+
prefix: str
|
| 17 |
+
suffix: Optional[str] = ""
|
| 18 |
+
language: Optional[str] = "python"
|
| 19 |
+
max_tokens: Optional[int] = 128
|
| 20 |
+
temperature: Optional[float] = 0.2
|
| 21 |
+
stream: Optional[bool] = False
|
| 22 |
|
| 23 |
class ChatResponseChunk(BaseModel):
|
| 24 |
content: str
|
|
|
|
| 30 |
content: str
|
| 31 |
usage: Dict[str, Any]
|
| 32 |
model: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/utils.py
CHANGED
|
@@ -1,82 +1,20 @@
|
|
| 1 |
-
import
|
| 2 |
-
from typing import Dict, Any,
|
| 3 |
|
| 4 |
def estimate_tokens(text: str) -> int:
|
| 5 |
-
"""
|
| 6 |
-
Estimates token count for Qwen/GPT-like models without heavy tokenizers.
|
| 7 |
-
Rule of thumb: 1 token ≈ 4 characters, or ~1.3 tokens per word.
|
| 8 |
-
"""
|
| 9 |
if not text:
|
| 10 |
return 0
|
| 11 |
-
|
| 12 |
-
char_est = len(text) / 4.0
|
| 13 |
-
word_est = len(text.split()) * 1.3
|
| 14 |
-
return int((char_est + word_est) / 2.0) + 1
|
| 15 |
|
| 16 |
def format_sse_chunk(content: str, done: bool = False, usage: Optional[Dict[str, Any]] = None) -> str:
|
| 17 |
-
|
| 18 |
-
data
|
| 19 |
-
"content": content,
|
| 20 |
-
"done": done,
|
| 21 |
-
"usage": usage
|
| 22 |
-
}
|
| 23 |
-
return f"data: {json_dumps(data)}\n\n"
|
| 24 |
-
|
| 25 |
-
def json_dumps(obj: Any) -> str:
|
| 26 |
-
# A safe helper for JSON serialization
|
| 27 |
-
import json
|
| 28 |
-
return json.dumps(obj)
|
| 29 |
-
|
| 30 |
-
def get_code_prompt(action: str, code: str, language: str, context: Optional[str] = None) -> str:
|
| 31 |
-
"""Returns specialized system/user prompts for code tasks."""
|
| 32 |
-
prompt_templates = {
|
| 33 |
-
"explain": (
|
| 34 |
-
"You are an expert software engineer. Explain this code step-by-step. "
|
| 35 |
-
"Highlight the flow of execution, core algorithms, and potential performance implications. "
|
| 36 |
-
"Write the explanation in clear, readable markdown.\n\n"
|
| 37 |
-
f"Language: {language}\n"
|
| 38 |
-
f"Code:\n```\n{code}\n```"
|
| 39 |
-
),
|
| 40 |
-
"debug": (
|
| 41 |
-
"You are a senior debugger. Identify errors, logical bugs, edge cases, memory leaks, "
|
| 42 |
-
"or security vulnerabilities in the code below. Explain each issue found and "
|
| 43 |
-
"provide a corrected version of the code, indicating what changes were made.\n\n"
|
| 44 |
-
f"Language: {language}\n"
|
| 45 |
-
f"Code:\n```\n{code}\n```"
|
| 46 |
-
),
|
| 47 |
-
"refactor": (
|
| 48 |
-
"You are a principal engineer. Refactor this code to improve its readability, "
|
| 49 |
-
"efficiency, and modularity. Adhere to SOLID principles and industry design patterns. "
|
| 50 |
-
"Provide the refactored code and list the improvements.\n\n"
|
| 51 |
-
f"Language: {language}\n"
|
| 52 |
-
f"Code:\n```\n{code}\n```"
|
| 53 |
-
),
|
| 54 |
-
"generate-tests": (
|
| 55 |
-
"You are a QA automation lead. Write comprehensive unit tests for the following code snippet. "
|
| 56 |
-
"Cover typical inputs, edge cases, and error conditions. Use standard testing libraries.\n\n"
|
| 57 |
-
f"Language: {language}\n"
|
| 58 |
-
f"Code:\n```\n{code}\n```"
|
| 59 |
-
),
|
| 60 |
-
"summarize": (
|
| 61 |
-
"Provide a brief, high-level summary of what this code does in 2-3 sentences. "
|
| 62 |
-
"Focus on the main inputs, transformations, and outputs.\n\n"
|
| 63 |
-
f"Language: {language}\n"
|
| 64 |
-
f"Code:\n```\n{code}\n```"
|
| 65 |
-
)
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
prompt = prompt_templates.get(action, f"Analyze the following code:\n\n```{language}\n{code}\n```")
|
| 69 |
-
if context:
|
| 70 |
-
prompt = f"Context/Instructions: {context}\n\n{prompt}"
|
| 71 |
-
return prompt
|
| 72 |
|
| 73 |
def get_completion_prompt(prefix: str, suffix: str, language: str) -> str:
|
| 74 |
-
"""Constructs instructions for code completion."""
|
| 75 |
return (
|
| 76 |
-
f"
|
| 77 |
-
"
|
| 78 |
-
"Return ONLY the code
|
| 79 |
-
"Do NOT write any explanations. Do NOT wrap your response in markdown code blocks.\n\n"
|
| 80 |
f"--- PREFIX ---\n{prefix}\n"
|
| 81 |
f"--- SUFFIX ---\n{suffix}\n"
|
| 82 |
"--- INSERT COMPLETED CODE BELOW ---"
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from typing import Dict, Any, Optional
|
| 3 |
|
| 4 |
def estimate_tokens(text: str) -> int:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
if not text:
|
| 6 |
return 0
|
| 7 |
+
return int(len(text) / 4) + 1
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
def format_sse_chunk(content: str, done: bool = False, usage: Optional[Dict[str, Any]] = None) -> str:
|
| 10 |
+
data = {"content": content, "done": done, "usage": usage}
|
| 11 |
+
return f"data: {json.dumps(data)}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
def get_completion_prompt(prefix: str, suffix: str, language: str) -> str:
|
|
|
|
| 14 |
return (
|
| 15 |
+
f"Continue the code for the {language} language. "
|
| 16 |
+
f"Fill in the missing code between the Prefix and Suffix. "
|
| 17 |
+
f"Return ONLY the code to insert. No explanations, no markdown.\n\n"
|
|
|
|
| 18 |
f"--- PREFIX ---\n{prefix}\n"
|
| 19 |
f"--- SUFFIX ---\n{suffix}\n"
|
| 20 |
"--- INSERT COMPLETED CODE BELOW ---"
|
backend/requirements.txt
CHANGED
|
@@ -2,9 +2,5 @@ fastapi>=0.100.0
|
|
| 2 |
uvicorn>=0.22.0
|
| 3 |
pydantic>=2.0
|
| 4 |
pydantic-settings>=2.0
|
| 5 |
-
|
| 6 |
-
requests>=2.31.0
|
| 7 |
-
httpx>=0.24.0
|
| 8 |
-
huggingface_hub>=0.16.0
|
| 9 |
-
psutil>=5.9.0
|
| 10 |
llama-cpp-python>=0.2.0
|
|
|
|
| 2 |
uvicorn>=0.22.0
|
| 3 |
pydantic>=2.0
|
| 4 |
pydantic-settings>=2.0
|
| 5 |
+
huggingface-hub>=0.16.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
llama-cpp-python>=0.2.0
|
backend/run.py
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
import uvicorn
|
| 2 |
-
from backend.app.config import settings
|
| 3 |
-
|
| 4 |
-
if __name__ == "__main__":
|
| 5 |
-
print(f"Starting server on http://{settings.HOST}:{settings.PORT}")
|
| 6 |
-
uvicorn.run(
|
| 7 |
-
"backend.app.main:app",
|
| 8 |
-
host=settings.HOST,
|
| 9 |
-
port=settings.PORT,
|
| 10 |
-
reload=settings.DEBUG
|
| 11 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docker/Dockerfile.backend
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
# Multi-stage build to reduce final image size
|
| 2 |
-
FROM python:3.11-slim as builder
|
| 3 |
-
|
| 4 |
-
WORKDIR /app
|
| 5 |
-
|
| 6 |
-
# Install compilation tools needed for compiling llama-cpp-python
|
| 7 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 8 |
-
build-essential \
|
| 9 |
-
gcc \
|
| 10 |
-
g++ \
|
| 11 |
-
make \
|
| 12 |
-
python3-dev \
|
| 13 |
-
git \
|
| 14 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
-
|
| 16 |
-
# Copy backend requirements
|
| 17 |
-
COPY backend/requirements.txt .
|
| 18 |
-
|
| 19 |
-
# Install dependencies and build llama-cpp-python
|
| 20 |
-
RUN pip install --no-cache-dir --user -r requirements.txt
|
| 21 |
-
|
| 22 |
-
# Final production stage
|
| 23 |
-
FROM python:3.11-slim
|
| 24 |
-
|
| 25 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 26 |
-
libgomp1 \
|
| 27 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 28 |
-
|
| 29 |
-
WORKDIR /app
|
| 30 |
-
|
| 31 |
-
# Copy installed site-packages from builder stage
|
| 32 |
-
COPY --from=builder /root/.local /root/.local
|
| 33 |
-
ENV PATH=/root/.local/bin:$PATH
|
| 34 |
-
|
| 35 |
-
# Copy backend codebase
|
| 36 |
-
COPY backend/ /app/backend/
|
| 37 |
-
|
| 38 |
-
ENV PORT=8000
|
| 39 |
-
ENV HOST=0.0.0.0
|
| 40 |
-
ENV PYTHONPATH=/app
|
| 41 |
-
|
| 42 |
-
EXPOSE 8000
|
| 43 |
-
|
| 44 |
-
# Start command
|
| 45 |
-
CMD ["python", "backend/run.py"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docker/Dockerfile.frontend
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
FROM node:20-slim
|
| 2 |
-
|
| 3 |
-
WORKDIR /app
|
| 4 |
-
|
| 5 |
-
COPY frontend/package*.json ./
|
| 6 |
-
|
| 7 |
-
RUN npm install
|
| 8 |
-
|
| 9 |
-
COPY frontend/ ./
|
| 10 |
-
|
| 11 |
-
EXPOSE 5173
|
| 12 |
-
|
| 13 |
-
CMD ["npm", "run", "dev", "--", "--host"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docker/docker-compose.yml
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
version: '3.8'
|
| 2 |
-
|
| 3 |
-
services:
|
| 4 |
-
backend:
|
| 5 |
-
build:
|
| 6 |
-
context: ../
|
| 7 |
-
dockerfile: docker/Dockerfile.backend
|
| 8 |
-
ports:
|
| 9 |
-
- "8000:8000"
|
| 10 |
-
volumes:
|
| 11 |
-
- ../backend:/app/backend
|
| 12 |
-
- ../models:/app/models
|
| 13 |
-
environment:
|
| 14 |
-
- PORT=8000
|
| 15 |
-
- HOST=0.0.0.0
|
| 16 |
-
- DEBUG=true
|
| 17 |
-
- INFERENCE_MODE=local
|
| 18 |
-
- LOCAL_MODEL_PATH=models/SmolLM2-360M-Instruct-Q4_K_M.gguf
|
| 19 |
-
- CORS_ORIGINS=http://localhost:5173,http://localhost:8000
|
| 20 |
-
restart: unless-stopped
|
| 21 |
-
|
| 22 |
-
frontend:
|
| 23 |
-
build:
|
| 24 |
-
context: ../
|
| 25 |
-
dockerfile: docker/Dockerfile.frontend
|
| 26 |
-
ports:
|
| 27 |
-
- "5173:5173"
|
| 28 |
-
volumes:
|
| 29 |
-
- ../frontend:/app
|
| 30 |
-
- /app/node_modules
|
| 31 |
-
environment:
|
| 32 |
-
- VITE_API_URL=http://backend:8000
|
| 33 |
-
command: npm run dev -- --host --force
|
| 34 |
-
depends_on:
|
| 35 |
-
- backend
|
| 36 |
-
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/API.md
DELETED
|
@@ -1,109 +0,0 @@
|
|
| 1 |
-
# API Reference Documentation 📖
|
| 2 |
-
|
| 3 |
-
This document details the REST API specifications for the Antigravity Coding Assistant.
|
| 4 |
-
|
| 5 |
-
All APIs use JSON request bodies. Streaming endpoints support Server-Sent Events (`text/event-stream`).
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## Endpoints Summary
|
| 10 |
-
|
| 11 |
-
| Method | Path | Description | Streaming Support |
|
| 12 |
-
| :--- | :--- | :--- | :--- |
|
| 13 |
-
| `POST` | `/chat` | Chat message handler | Yes |
|
| 14 |
-
| `POST` | `/complete` | Code auto-completion | Yes |
|
| 15 |
-
| `POST` | `/explain` | Generates explanation for a code block | Yes |
|
| 16 |
-
| `POST` | `/debug` | Locates bugs and provides fixes | Yes |
|
| 17 |
-
| `POST` | `/refactor` | Refactors code for quality & SOLID rules | Yes |
|
| 18 |
-
| `POST` | `/generate-tests`| Creates automated unit tests | Yes |
|
| 19 |
-
| `POST` | `/summarize` | Summarizes code in 2-3 sentences | Yes |
|
| 20 |
-
| `GET` | `/health` | Check backend service health | No |
|
| 21 |
-
| `GET` | `/metrics` | Get performance metrics telemetry | No |
|
| 22 |
-
| `GET` | `/model-info` | Check active LLM provider specifications| No |
|
| 23 |
-
|
| 24 |
-
---
|
| 25 |
-
|
| 26 |
-
## Detailed Specifications
|
| 27 |
-
|
| 28 |
-
### 1. POST `/chat`
|
| 29 |
-
Generates conversational assistance based on prompt history.
|
| 30 |
-
|
| 31 |
-
**Request Payload:**
|
| 32 |
-
```json
|
| 33 |
-
{
|
| 34 |
-
"messages": [
|
| 35 |
-
{ "role": "system", "content": "You are a coding assistant." },
|
| 36 |
-
{ "role": "user", "content": "Write a bubble sort in Python." }
|
| 37 |
-
],
|
| 38 |
-
"temperature": 0.7,
|
| 39 |
-
"max_tokens": 1024,
|
| 40 |
-
"top_p": 0.9,
|
| 41 |
-
"stream": true
|
| 42 |
-
}
|
| 43 |
-
```
|
| 44 |
-
|
| 45 |
-
---
|
| 46 |
-
|
| 47 |
-
### 2. POST `/complete`
|
| 48 |
-
Fills in missing code between a prefix and suffix (Fill-in-the-Middle).
|
| 49 |
-
|
| 50 |
-
**Request Payload:**
|
| 51 |
-
```json
|
| 52 |
-
{
|
| 53 |
-
"prefix": "def add_numbers(a, b):\n ",
|
| 54 |
-
"suffix": "\n\nprint(add_numbers(5, 10))",
|
| 55 |
-
"language": "python",
|
| 56 |
-
"max_tokens": 64,
|
| 57 |
-
"temperature": 0.1,
|
| 58 |
-
"stream": false
|
| 59 |
-
}
|
| 60 |
-
```
|
| 61 |
-
|
| 62 |
-
---
|
| 63 |
-
|
| 64 |
-
### 3. POST `/explain` | `/debug` | `/refactor` | `/generate-tests` | `/summarize`
|
| 65 |
-
Specialized endpoints for code-focused instructions.
|
| 66 |
-
|
| 67 |
-
**Request Payload:**
|
| 68 |
-
```json
|
| 69 |
-
{
|
| 70 |
-
"code": "def double(x): return x * 2",
|
| 71 |
-
"language": "python",
|
| 72 |
-
"context": "Optimize this code",
|
| 73 |
-
"temperature": 0.7,
|
| 74 |
-
"max_tokens": 1024,
|
| 75 |
-
"stream": true
|
| 76 |
-
}
|
| 77 |
-
```
|
| 78 |
-
|
| 79 |
-
---
|
| 80 |
-
|
| 81 |
-
## Response Formats
|
| 82 |
-
|
| 83 |
-
### Non-Streaming Response
|
| 84 |
-
Returned when `stream` parameter is `false`:
|
| 85 |
-
```json
|
| 86 |
-
{
|
| 87 |
-
"id": "uuid-string-here",
|
| 88 |
-
"content": "Generated text or code review block here",
|
| 89 |
-
"usage": {
|
| 90 |
-
"prompt_tokens": 42,
|
| 91 |
-
"completion_tokens": 128,
|
| 92 |
-
"total_tokens": 170,
|
| 93 |
-
"duration_ms": 1240,
|
| 94 |
-
"tokens_per_second": 103.2
|
| 95 |
-
},
|
| 96 |
-
"model": "bartowski/Qwen2.5-Coder-0.5B-Instruct-GGUF"
|
| 97 |
-
}
|
| 98 |
-
```
|
| 99 |
-
|
| 100 |
-
### Streaming Response (SSE)
|
| 101 |
-
Returned when `stream` parameter is `true`. Standard SSE format:
|
| 102 |
-
```
|
| 103 |
-
data: {"content": "Hello", "done": false}
|
| 104 |
-
|
| 105 |
-
data: {"content": " World", "done": false}
|
| 106 |
-
|
| 107 |
-
data: {"content": "", "done": true, "usage": {"prompt_tokens": 12, "completion_tokens": 2, "total_tokens": 14, "duration_ms": 230, "tokens_per_second": 8.7}}
|
| 108 |
-
```
|
| 109 |
-
Each data line represents a single generated text token. The final SSE chunk contains `done: true` and the generation metrics.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/DEPLOYMENT.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
# Cloud Deployment Guide 🌐
|
| 2 |
-
|
| 3 |
-
This document covers instructions on deploying the unified **Antigravity AI Coder** container to Railway and Render.
|
| 4 |
-
|
| 5 |
-
---
|
| 6 |
-
|
| 7 |
-
## <a name="railway"></a> 🚄 Railway Deployment (Preferred)
|
| 8 |
-
|
| 9 |
-
Railway is the recommended host because of its native support for Dockerfile builds, fast container deployments, and reliable persistent volumes.
|
| 10 |
-
|
| 11 |
-
### Step 1: Create a Railway Project
|
| 12 |
-
1. Log into your [Railway Console](https://railway.app/).
|
| 13 |
-
2. Click **New Project** -> **Deploy from GitHub repo**.
|
| 14 |
-
3. Select your repository.
|
| 15 |
-
|
| 16 |
-
### Step 2: Configure Environment Variables
|
| 17 |
-
Add the following variables in Railway's **Variables** tab:
|
| 18 |
-
* `PORT` = `8000`
|
| 19 |
-
* `HOST` = `0.0.0.0`
|
| 20 |
-
* `INFERENCE_MODE` = `auto` (Routes to Hugging Face Cloud if container RAM is limited)
|
| 21 |
-
* `HF_API_TOKEN` = `your_huggingface_api_token` (Strongly recommended to avoid rate limits)
|
| 22 |
-
* `HF_MODEL_ID` = `Qwen/Qwen2.5-Coder-0.5B-Instruct`
|
| 23 |
-
* `SECRET_KEY` = `generate-a-long-random-string`
|
| 24 |
-
|
| 25 |
-
### Step 3: Mount a Persistent Volume (Optional but Recommended)
|
| 26 |
-
To prevent the container from re-downloading the 397MB local model file on every restart:
|
| 27 |
-
1. In the service settings, click **Volume** -> **Add Volume**.
|
| 28 |
-
2. Mount the volume to: `/app/models`.
|
| 29 |
-
3. Save the changes. Railway will now persist the downloaded GGUF file inside this volume.
|
| 30 |
-
|
| 31 |
-
---
|
| 32 |
-
|
| 33 |
-
## <a name="render"></a> 💎 Render Deployment
|
| 34 |
-
|
| 35 |
-
Render supports Docker builds out of the box using our blueprint `render.yaml` configuration.
|
| 36 |
-
|
| 37 |
-
### Step 1: Deploy using render.yaml Blueprint
|
| 38 |
-
1. Log into your [Render Dashboard](https://dashboard.render.com/).
|
| 39 |
-
2. Click **New** -> **Blueprint**.
|
| 40 |
-
3. Link your repository. Render will automatically read the `render.yaml` file from your repo root.
|
| 41 |
-
4. Render will prompt you for:
|
| 42 |
-
* **Service Name:** `antigravity-ai-coder`
|
| 43 |
-
* **HF_API_TOKEN:** Provide your Hugging Face API key.
|
| 44 |
-
|
| 45 |
-
### Step 2: Custom Scaling & Memory Fallback
|
| 46 |
-
* **On Free Instances:** Render's free tier provides 512MB RAM. Since 512MB is below our local inference threshold, the backend will boot up, detect the system memory limit, and automatically switch to `huggingface` cloud fallback mode.
|
| 47 |
-
* **On Paid Instances:** If you upgrade to a Starter or Standard instance (>= 2GB RAM) and keep `INFERENCE_MODE=auto`, it will download and run the GGUF model locally. The blueprint allocates a 10GB persistent disk mounted to `/app/models` to store the model file.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/App.tsx
CHANGED
|
@@ -14,7 +14,6 @@ export const App: React.FC = () => {
|
|
| 14 |
activeConversation,
|
| 15 |
settings,
|
| 16 |
modelInfo,
|
| 17 |
-
metrics,
|
| 18 |
isLoading,
|
| 19 |
error,
|
| 20 |
updateSettings,
|
|
@@ -105,7 +104,6 @@ export const App: React.FC = () => {
|
|
| 105 |
{activeTab === 'dashboard' && (
|
| 106 |
<Dashboard
|
| 107 |
modelInfo={modelInfo}
|
| 108 |
-
metrics={metrics}
|
| 109 |
setActiveTab={selectTabFromNav}
|
| 110 |
createNewConversation={createNewConversation}
|
| 111 |
sendMessage={sendMessage}
|
|
|
|
| 14 |
activeConversation,
|
| 15 |
settings,
|
| 16 |
modelInfo,
|
|
|
|
| 17 |
isLoading,
|
| 18 |
error,
|
| 19 |
updateSettings,
|
|
|
|
| 104 |
{activeTab === 'dashboard' && (
|
| 105 |
<Dashboard
|
| 106 |
modelInfo={modelInfo}
|
|
|
|
| 107 |
setActiveTab={selectTabFromNav}
|
| 108 |
createNewConversation={createNewConversation}
|
| 109 |
sendMessage={sendMessage}
|
frontend/src/components/ChatInterface.tsx
CHANGED
|
@@ -1,12 +1,10 @@
|
|
| 1 |
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
-
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
import {
|
| 4 |
-
Send,
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
User, Shield, MessageSquare, Plus, Search, Code, Mic, Trash2
|
| 8 |
} from 'lucide-react';
|
| 9 |
-
import type {
|
| 10 |
|
| 11 |
interface ChatInterfaceProps {
|
| 12 |
activeConversation: Conversation | null;
|
|
|
|
| 1 |
import React, { useState, useRef, useEffect } from 'react';
|
|
|
|
| 2 |
import {
|
| 3 |
+
Send, AlertCircle, Copy, Check, Download,
|
| 4 |
+
Paperclip, Terminal, Plus,
|
| 5 |
+
User, Shield, Search, Code, Mic
|
|
|
|
| 6 |
} from 'lucide-react';
|
| 7 |
+
import type { Conversation, ModelInfo } from '../types';
|
| 8 |
|
| 9 |
interface ChatInterfaceProps {
|
| 10 |
activeConversation: Conversation | null;
|
frontend/src/components/CodePlayground.tsx
CHANGED
|
@@ -1,8 +1,6 @@
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
-
import { motion } from 'framer-motion';
|
| 3 |
import {
|
| 4 |
-
Play, Trash2, Save,
|
| 5 |
-
Maximize2, RefreshCw
|
| 6 |
} from 'lucide-react';
|
| 7 |
import { MonacoEditorWrapper } from './MonacoEditorWrapper';
|
| 8 |
|
|
|
|
| 1 |
import React, { useState } from 'react';
|
|
|
|
| 2 |
import {
|
| 3 |
+
Play, Trash2, Save, Copy, Check, ChevronDown
|
|
|
|
| 4 |
} from 'lucide-react';
|
| 5 |
import { MonacoEditorWrapper } from './MonacoEditorWrapper';
|
| 6 |
|
frontend/src/components/Dashboard.tsx
CHANGED
|
@@ -1,14 +1,12 @@
|
|
| 1 |
import React from 'react';
|
| 2 |
-
import { motion } from 'framer-motion';
|
| 3 |
import {
|
| 4 |
MessageSquare, Code, FileText, ShieldAlert, List, RefreshCw,
|
| 5 |
-
Cpu, HardDrive,
|
| 6 |
} from 'lucide-react';
|
| 7 |
-
import type { ModelInfo
|
| 8 |
|
| 9 |
interface DashboardProps {
|
| 10 |
modelInfo: ModelInfo | null;
|
| 11 |
-
metrics: ModelMetrics | null;
|
| 12 |
setActiveTab: (tab: string) => void;
|
| 13 |
createNewConversation: (title?: string) => void;
|
| 14 |
sendMessage: (msg: string) => void;
|
|
@@ -16,7 +14,6 @@ interface DashboardProps {
|
|
| 16 |
|
| 17 |
export const Dashboard: React.FC<DashboardProps> = ({
|
| 18 |
modelInfo,
|
| 19 |
-
metrics,
|
| 20 |
setActiveTab,
|
| 21 |
createNewConversation,
|
| 22 |
sendMessage,
|
|
@@ -69,14 +66,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|
| 69 |
}, 100);
|
| 70 |
};
|
| 71 |
|
| 72 |
-
|
| 73 |
-
const modelName = modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M';
|
| 74 |
-
const memoryUsed = modelInfo?.memory_usage_gb || 1.53;
|
| 75 |
-
const memoryTotal = modelInfo?.total_memory_gb || 7.65;
|
| 76 |
-
const memoryPercent = Math.min(100, Math.round((memoryUsed / memoryTotal) * 100)) || 20;
|
| 77 |
-
|
| 78 |
-
const latency = metrics?.average_latency_seconds || 22.13;
|
| 79 |
-
const totalTokens = metrics?.total_tokens || 816;
|
| 80 |
|
| 81 |
return (
|
| 82 |
<div className="flex-1 overflow-y-auto bg-[#090810] p-6 space-y-6 font-sans max-w-[1400px] mx-auto w-full">
|
|
@@ -102,13 +92,13 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|
| 102 |
</div>
|
| 103 |
</div>
|
| 104 |
|
| 105 |
-
{/* Grid: System Status
|
| 106 |
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-
|
| 107 |
|
| 108 |
{/* Model Status Card */}
|
| 109 |
-
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all
|
| 110 |
<div className="flex items-center justify-between">
|
| 111 |
-
<span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Model
|
| 112 |
<div className="p-1.5 bg-emerald-500/10 rounded-lg text-emerald-500">
|
| 113 |
<Cpu size={14} />
|
| 114 |
</div>
|
|
@@ -119,50 +109,31 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
|
| 122 |
-
{/* Memory
|
| 123 |
-
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all
|
| 124 |
<div className="flex items-center justify-between">
|
| 125 |
-
<span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">
|
| 126 |
<div className="p-1.5 bg-blue-500/10 rounded-lg text-blue-500">
|
| 127 |
<HardDrive size={14} />
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
-
<div className="space-y-2 flex-1 flex flex-col justify-end">
|
| 131 |
-
<div className="flex justify-between items-baseline">
|
| 132 |
-
<h2 className="text-sm font-bold text-white">{memoryUsed.toFixed(2)} GB <span className="text-[10px] text-[#58556f] font-normal">/ {memoryTotal.toFixed(2)} GB</span></h2>
|
| 133 |
-
<span className="text-[10px] text-blue-400 font-semibold">{memoryPercent}%</span>
|
| 134 |
-
</div>
|
| 135 |
-
<div className="w-full bg-[#1c1a30] h-1.5 rounded-full overflow-hidden">
|
| 136 |
-
<div className="bg-blue-500 h-full rounded-full" style={{ width: `${memoryPercent}%` }}></div>
|
| 137 |
-
</div>
|
| 138 |
-
</div>
|
| 139 |
-
</div>
|
| 140 |
-
|
| 141 |
-
{/* Latency Card */}
|
| 142 |
-
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all duration-300">
|
| 143 |
-
<div className="flex items-center justify-between">
|
| 144 |
-
<span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Avg. Latency</span>
|
| 145 |
-
<div className="p-1.5 bg-primary/10 rounded-lg text-primary">
|
| 146 |
-
<Zap size={14} />
|
| 147 |
-
</div>
|
| 148 |
-
</div>
|
| 149 |
<div className="space-y-1 flex-1 flex flex-col justify-end">
|
| 150 |
-
<h2 className="text-
|
| 151 |
-
<p className="text-[10px] text-[#58556f]">
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
|
| 155 |
-
{/*
|
| 156 |
-
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all
|
| 157 |
<div className="flex items-center justify-between">
|
| 158 |
-
<span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">
|
| 159 |
-
<div className="p-1.5 bg-
|
| 160 |
-
<
|
| 161 |
</div>
|
| 162 |
</div>
|
| 163 |
<div className="space-y-1 flex-1 flex flex-col justify-end">
|
| 164 |
-
<h2 className="text-xl font-bold text-white">
|
| 165 |
-
<p className="text-[10px] text-[#58556f]">
|
| 166 |
</div>
|
| 167 |
</div>
|
| 168 |
|
|
|
|
| 1 |
import React from 'react';
|
|
|
|
| 2 |
import {
|
| 3 |
MessageSquare, Code, FileText, ShieldAlert, List, RefreshCw,
|
| 4 |
+
Cpu, HardDrive, Database
|
| 5 |
} from 'lucide-react';
|
| 6 |
+
import type { ModelInfo } from '../types';
|
| 7 |
|
| 8 |
interface DashboardProps {
|
| 9 |
modelInfo: ModelInfo | null;
|
|
|
|
| 10 |
setActiveTab: (tab: string) => void;
|
| 11 |
createNewConversation: (title?: string) => void;
|
| 12 |
sendMessage: (msg: string) => void;
|
|
|
|
| 14 |
|
| 15 |
export const Dashboard: React.FC<DashboardProps> = ({
|
| 16 |
modelInfo,
|
|
|
|
| 17 |
setActiveTab,
|
| 18 |
createNewConversation,
|
| 19 |
sendMessage,
|
|
|
|
| 66 |
}, 100);
|
| 67 |
};
|
| 68 |
|
| 69 |
+
const modelName = modelInfo?.model_name || 'SmolLM2-360M-Q4_K_M';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
return (
|
| 72 |
<div className="flex-1 overflow-y-auto bg-[#090810] p-6 space-y-6 font-sans max-w-[1400px] mx-auto w-full">
|
|
|
|
| 92 |
</div>
|
| 93 |
</div>
|
| 94 |
|
| 95 |
+
{/* Grid: System Status */}
|
| 96 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 select-none">
|
| 97 |
|
| 98 |
{/* Model Status Card */}
|
| 99 |
+
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all">
|
| 100 |
<div className="flex items-center justify-between">
|
| 101 |
+
<span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Model</span>
|
| 102 |
<div className="p-1.5 bg-emerald-500/10 rounded-lg text-emerald-500">
|
| 103 |
<Cpu size={14} />
|
| 104 |
</div>
|
|
|
|
| 109 |
</div>
|
| 110 |
</div>
|
| 111 |
|
| 112 |
+
{/* Memory Card */}
|
| 113 |
+
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all">
|
| 114 |
<div className="flex items-center justify-between">
|
| 115 |
+
<span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Model Size</span>
|
| 116 |
<div className="p-1.5 bg-blue-500/10 rounded-lg text-blue-500">
|
| 117 |
<HardDrive size={14} />
|
| 118 |
</div>
|
| 119 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
<div className="space-y-1 flex-1 flex flex-col justify-end">
|
| 121 |
+
<h2 className="text-sm font-bold text-white">~180 MB</h2>
|
| 122 |
+
<p className="text-[10px] text-[#58556f]">Q4_K_M quantized</p>
|
| 123 |
</div>
|
| 124 |
</div>
|
| 125 |
|
| 126 |
+
{/* Context Card */}
|
| 127 |
+
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all">
|
| 128 |
<div className="flex items-center justify-between">
|
| 129 |
+
<span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Context</span>
|
| 130 |
+
<div className="p-1.5 bg-primary/10 rounded-lg text-primary">
|
| 131 |
+
<RefreshCw size={14} />
|
| 132 |
</div>
|
| 133 |
</div>
|
| 134 |
<div className="space-y-1 flex-1 flex flex-col justify-end">
|
| 135 |
+
<h2 className="text-xl font-bold text-white">512</h2>
|
| 136 |
+
<p className="text-[10px] text-[#58556f]">Tokens context window</p>
|
| 137 |
</div>
|
| 138 |
</div>
|
| 139 |
|
frontend/src/components/Navbar.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
import React from 'react';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
import {
|
| 4 |
-
Bell, Search, Sun, Moon, Cpu,
|
| 5 |
-
CheckCircle2, AlertCircle
|
| 6 |
} from 'lucide-react';
|
| 7 |
import type { ModelInfo, AppSettings } from '../types';
|
| 8 |
|
|
|
|
| 1 |
import React from 'react';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
import {
|
| 4 |
+
Bell, Search, Sun, Moon, Cpu,
|
| 5 |
+
CheckCircle2, AlertCircle
|
| 6 |
} from 'lucide-react';
|
| 7 |
import type { ModelInfo, AppSettings } from '../types';
|
| 8 |
|
frontend/src/components/Settings.tsx
CHANGED
|
@@ -1,8 +1,5 @@
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
-
import {
|
| 3 |
-
Cpu, Sliders, AlertTriangle, ShieldCheck, Save, CheckCircle2,
|
| 4 |
-
HardDrive, Info, Activity, RefreshCw
|
| 5 |
-
} from 'lucide-react';
|
| 6 |
import type { AppSettings, ModelInfo } from '../types';
|
| 7 |
|
| 8 |
interface SettingsProps {
|
|
@@ -34,259 +31,115 @@ export const Settings: React.FC<SettingsProps> = ({
|
|
| 34 |
clearHistory();
|
| 35 |
setConfirmClear(false);
|
| 36 |
setResetting(false);
|
| 37 |
-
alert('
|
| 38 |
}, 800);
|
| 39 |
} else {
|
| 40 |
setConfirmClear(true);
|
| 41 |
}
|
| 42 |
};
|
| 43 |
|
| 44 |
-
// Safe formatting variables matching image specifications
|
| 45 |
-
const memoryUsed = modelInfo?.memory_usage_gb || 1.53;
|
| 46 |
-
const memoryTotal = modelInfo?.total_memory_gb || 7.65;
|
| 47 |
-
const memoryPercent = Math.min(100, Math.round((memoryUsed / memoryTotal) * 100)) || 20;
|
| 48 |
-
|
| 49 |
return (
|
| 50 |
<div className="flex-1 overflow-y-auto bg-[#090810] p-6 lg:p-8 font-sans select-none">
|
| 51 |
-
|
| 52 |
-
{/* Settings Split Layout grid */}
|
| 53 |
<div className="max-w-6xl grid grid-cols-1 lg:grid-cols-5 gap-8 items-start">
|
| 54 |
-
|
| 55 |
-
{/* Left Column Form (3/5 width) */}
|
| 56 |
<div className="lg:col-span-3 space-y-5 bg-[#131121] border border-[#1d1b2e] p-6 rounded-2xl">
|
| 57 |
-
|
| 58 |
<div className="border-b border-[#1d1b2e] pb-4">
|
| 59 |
-
<h2 className="text-md font-bold text-white tracking-tight">
|
| 60 |
-
<p className="text-[10px] text-[#9d99b3] mt-0.5">Configure how the AI model
|
| 61 |
</div>
|
| 62 |
-
|
| 63 |
<div className="space-y-5 pt-1">
|
| 64 |
-
|
| 65 |
-
{/* Inference Mode selector */}
|
| 66 |
-
<div className="space-y-1.5">
|
| 67 |
-
<label className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Inference Mode</label>
|
| 68 |
-
<select
|
| 69 |
-
className="w-full bg-[#090810] border border-[#1d1b2e] text-xs text-white rounded-xl px-3 py-2.5 outline-none font-semibold cursor-pointer"
|
| 70 |
-
disabled
|
| 71 |
-
>
|
| 72 |
-
<option>Local Inference</option>
|
| 73 |
-
</select>
|
| 74 |
-
</div>
|
| 75 |
|
| 76 |
-
{/* Target model selector */}
|
| 77 |
<div className="space-y-1.5">
|
| 78 |
-
<
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
<CheckCircle2 size={10} />
|
| 82 |
-
Loaded
|
| 83 |
-
</span>
|
| 84 |
</div>
|
| 85 |
-
<select
|
| 86 |
-
className="w-full bg-[#090810] border border-[#1d1b2e] text-xs text-white rounded-xl px-3 py-2.5 outline-none font-mono cursor-default"
|
| 87 |
-
disabled
|
| 88 |
-
>
|
| 89 |
-
<option>{modelInfo?.local_model_path.split('/').pop() || 'SmolLM2-360M-Instruct-Q4_K_M.gguf'}</option>
|
| 90 |
-
</select>
|
| 91 |
</div>
|
| 92 |
|
| 93 |
<div className="h-px bg-[#1d1b2e] my-2"></div>
|
| 94 |
|
| 95 |
-
{/* Context length slider */}
|
| 96 |
-
<div className="space-y-2">
|
| 97 |
-
<div className="flex justify-between text-xs font-semibold">
|
| 98 |
-
<span className="text-white">Context Length</span>
|
| 99 |
-
<span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.contextLength}</span>
|
| 100 |
-
</div>
|
| 101 |
-
<input
|
| 102 |
-
type="range"
|
| 103 |
-
min="256"
|
| 104 |
-
max="4096"
|
| 105 |
-
step="256"
|
| 106 |
-
value={settings.contextLength}
|
| 107 |
-
onChange={(e) => updateSettings({ contextLength: parseInt(e.target.value) })}
|
| 108 |
-
className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer focus:outline-none"
|
| 109 |
-
/>
|
| 110 |
-
<p className="text-[9px] text-[#9d99b3]">Maximum context length constraints for the model.</p>
|
| 111 |
-
</div>
|
| 112 |
-
|
| 113 |
-
{/* Temperature Slider */}
|
| 114 |
<div className="space-y-2">
|
| 115 |
<div className="flex justify-between text-xs font-semibold">
|
| 116 |
<span className="text-white">Temperature</span>
|
| 117 |
<span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.temperature}</span>
|
| 118 |
</div>
|
| 119 |
<input
|
| 120 |
-
type="range"
|
| 121 |
-
min="0.1"
|
| 122 |
-
max="1.5"
|
| 123 |
-
step="0.1"
|
| 124 |
value={settings.temperature}
|
| 125 |
onChange={(e) => updateSettings({ temperature: parseFloat(e.target.value) })}
|
| 126 |
-
className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer
|
| 127 |
/>
|
| 128 |
-
<p className="text-[9px] text-[#9d99b3]">Controls randomness in responses.</p>
|
| 129 |
</div>
|
| 130 |
|
| 131 |
-
{/* Max Output Tokens Slider */}
|
| 132 |
<div className="space-y-2">
|
| 133 |
<div className="flex justify-between text-xs font-semibold">
|
| 134 |
<span className="text-white">Max Tokens</span>
|
| 135 |
<span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.maxTokens}</span>
|
| 136 |
</div>
|
| 137 |
<input
|
| 138 |
-
type="range"
|
| 139 |
-
min="64"
|
| 140 |
-
max="2048"
|
| 141 |
-
step="64"
|
| 142 |
value={settings.maxTokens}
|
| 143 |
onChange={(e) => updateSettings({ maxTokens: parseInt(e.target.value) })}
|
| 144 |
-
className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer
|
| 145 |
/>
|
| 146 |
-
<p className="text-[9px] text-[#9d99b3]">Maximum tokens in the response.</p>
|
| 147 |
</div>
|
| 148 |
|
| 149 |
-
{/* Top P Slider */}
|
| 150 |
<div className="space-y-2">
|
| 151 |
<div className="flex justify-between text-xs font-semibold">
|
| 152 |
<span className="text-white">Top P</span>
|
| 153 |
<span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.topP}</span>
|
| 154 |
</div>
|
| 155 |
<input
|
| 156 |
-
type="range"
|
| 157 |
-
min="0.1"
|
| 158 |
-
max="1.0"
|
| 159 |
-
step="0.05"
|
| 160 |
value={settings.topP}
|
| 161 |
onChange={(e) => updateSettings({ topP: parseFloat(e.target.value) })}
|
| 162 |
-
className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer
|
| 163 |
/>
|
| 164 |
-
<p className="text-[9px] text-[#9d99b3]">Nucleus sampling probability parameter.</p>
|
| 165 |
</div>
|
| 166 |
|
| 167 |
<div className="h-px bg-[#1d1b2e] my-2"></div>
|
| 168 |
|
| 169 |
-
{/* Stream response switch */}
|
| 170 |
<div className="flex items-center justify-between py-1.5">
|
| 171 |
<div className="space-y-0.5">
|
| 172 |
-
<label className="text-xs font-bold text-white
|
| 173 |
-
Stream Response
|
| 174 |
-
</label>
|
| 175 |
<p className="text-[10px] text-[#9d99b3]">Stream tokens as they are generated.</p>
|
| 176 |
</div>
|
| 177 |
-
<input
|
| 178 |
-
type="checkbox"
|
| 179 |
-
id="stream-switch"
|
| 180 |
checked={settings.streaming}
|
| 181 |
onChange={(e) => updateSettings({ streaming: e.target.checked })}
|
| 182 |
-
className="w-4.5 h-4.5
|
| 183 |
/>
|
| 184 |
</div>
|
| 185 |
|
| 186 |
-
{/* Keep history switch */}
|
| 187 |
-
<div className="flex items-center justify-between py-1.5">
|
| 188 |
-
<div className="space-y-0.5">
|
| 189 |
-
<label className="text-xs font-bold text-white cursor-pointer" htmlFor="history-switch">
|
| 190 |
-
Keep Conversation History
|
| 191 |
-
</label>
|
| 192 |
-
<p className="text-[10px] text-[#9d99b3]">Maintain conversation history log records in memory.</p>
|
| 193 |
-
</div>
|
| 194 |
-
<input
|
| 195 |
-
type="checkbox"
|
| 196 |
-
id="history-switch"
|
| 197 |
-
checked={true}
|
| 198 |
-
readOnly
|
| 199 |
-
className="w-4.5 h-4.5 rounded text-primary focus:ring-primary accent-primary bg-background border-border cursor-pointer focus-ring"
|
| 200 |
-
/>
|
| 201 |
-
</div>
|
| 202 |
-
|
| 203 |
-
{/* Save Button */}
|
| 204 |
<div className="flex items-center justify-end pt-3">
|
| 205 |
-
<button
|
| 206 |
-
|
| 207 |
-
className="flex items-center justify-center gap-2 px-5 py-2.5 bg-[#6344d5] hover:bg-[#6344d5]/95 text-white font-bold text-xs rounded-xl shadow-md transition-all active:scale-[0.98] cursor-pointer glow-primary"
|
| 208 |
>
|
| 209 |
-
{saveSuccess ?
|
| 210 |
-
<>
|
| 211 |
-
<CheckCircle2 size={13} />
|
| 212 |
-
<span>Settings Saved</span>
|
| 213 |
-
</>
|
| 214 |
-
) : (
|
| 215 |
-
<>
|
| 216 |
-
<Save size={13} />
|
| 217 |
-
<span>Save Settings</span>
|
| 218 |
-
</>
|
| 219 |
-
)}
|
| 220 |
</button>
|
| 221 |
</div>
|
| 222 |
|
| 223 |
</div>
|
| 224 |
</div>
|
| 225 |
|
| 226 |
-
{/* Right Column Stats (2/5 width) */}
|
| 227 |
<div className="lg:col-span-2 space-y-6">
|
| 228 |
-
|
| 229 |
-
{/* Model Information metadata */}
|
| 230 |
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl space-y-4">
|
| 231 |
-
<h3 className="text-xs font-bold text-white uppercase tracking-wider
|
| 232 |
-
<Info size={13} className="text-primary" />
|
| 233 |
-
Model Information
|
| 234 |
-
</h3>
|
| 235 |
<div className="space-y-3.5 text-xs select-none">
|
| 236 |
<div className="flex justify-between">
|
| 237 |
-
<span className="text-[#9d99b3]">Model
|
| 238 |
-
<span className="font-semibold text-white font-mono text-[11px]
|
| 239 |
</div>
|
| 240 |
<div className="flex justify-between">
|
| 241 |
-
<span className="text-[#9d99b3]">
|
| 242 |
-
<span className="font-semibold text-
|
| 243 |
-
</div>
|
| 244 |
-
<div className="flex justify-between">
|
| 245 |
-
<span className="text-[#9d99b3]">Model Size</span>
|
| 246 |
-
<span className="font-semibold text-white font-mono text-[11px]">1.6 GB</span>
|
| 247 |
-
</div>
|
| 248 |
-
<div className="flex justify-between">
|
| 249 |
-
<span className="text-[#9d99b3]">Context Length</span>
|
| 250 |
-
<span className="font-semibold text-white font-mono text-[11px]">4096</span>
|
| 251 |
</div>
|
| 252 |
<div className="flex justify-between">
|
| 253 |
-
<span className="text-[#9d99b3]">
|
| 254 |
-
<span className="font-semibold text-white font-mono text-[11px]">
|
| 255 |
</div>
|
| 256 |
<div className="flex justify-between">
|
| 257 |
-
<span className="text-[#9d99b3]">
|
| 258 |
-
<span className="font-semibold text-white font-mono text-[11px]">
|
| 259 |
-
</div>
|
| 260 |
-
</div>
|
| 261 |
-
</div>
|
| 262 |
-
|
| 263 |
-
{/* System resource bars */}
|
| 264 |
-
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl space-y-5 select-none">
|
| 265 |
-
<h3 className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-2 border-b border-[#1d1b2e] pb-3">
|
| 266 |
-
<Activity size={13} className="text-primary" />
|
| 267 |
-
System Resources
|
| 268 |
-
</h3>
|
| 269 |
-
|
| 270 |
-
{/* RAM Usage progress bar */}
|
| 271 |
-
<div className="space-y-2">
|
| 272 |
-
<div className="flex justify-between text-xs font-semibold">
|
| 273 |
-
<span className="text-[#9d99b3]">RAM Usage</span>
|
| 274 |
-
<span className="text-white font-mono">{memoryPercent}%</span>
|
| 275 |
-
</div>
|
| 276 |
-
<div className="w-full bg-[#090810] h-1.5 rounded-full overflow-hidden">
|
| 277 |
-
<div className="bg-emerald-500 h-full" style={{ width: `${memoryPercent}%` }}></div>
|
| 278 |
-
</div>
|
| 279 |
-
<div className="text-[10px] text-[#58556f]">{memoryUsed.toFixed(2)} GB / {memoryTotal.toFixed(2)} GB</div>
|
| 280 |
-
</div>
|
| 281 |
-
|
| 282 |
-
{/* CPU Usage progress bar */}
|
| 283 |
-
<div className="space-y-2">
|
| 284 |
-
<div className="flex justify-between text-xs font-semibold">
|
| 285 |
-
<span className="text-[#9d99b3]">CPU Usage</span>
|
| 286 |
-
<span className="text-white font-mono">12%</span>
|
| 287 |
-
</div>
|
| 288 |
-
<div className="w-full bg-[#090810] h-1.5 rounded-full overflow-hidden">
|
| 289 |
-
<div className="bg-[#6344d5] h-full" style={{ width: '12%' }}></div>
|
| 290 |
</div>
|
| 291 |
</div>
|
| 292 |
</div>
|
|
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
+
import { Save, CheckCircle2, AlertTriangle, RefreshCw } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
| 3 |
import type { AppSettings, ModelInfo } from '../types';
|
| 4 |
|
| 5 |
interface SettingsProps {
|
|
|
|
| 31 |
clearHistory();
|
| 32 |
setConfirmClear(false);
|
| 33 |
setResetting(false);
|
| 34 |
+
alert('Conversation history cleared.');
|
| 35 |
}, 800);
|
| 36 |
} else {
|
| 37 |
setConfirmClear(true);
|
| 38 |
}
|
| 39 |
};
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
return (
|
| 42 |
<div className="flex-1 overflow-y-auto bg-[#090810] p-6 lg:p-8 font-sans select-none">
|
|
|
|
|
|
|
| 43 |
<div className="max-w-6xl grid grid-cols-1 lg:grid-cols-5 gap-8 items-start">
|
|
|
|
|
|
|
| 44 |
<div className="lg:col-span-3 space-y-5 bg-[#131121] border border-[#1d1b2e] p-6 rounded-2xl">
|
|
|
|
| 45 |
<div className="border-b border-[#1d1b2e] pb-4">
|
| 46 |
+
<h2 className="text-md font-bold text-white tracking-tight">Generation Settings</h2>
|
| 47 |
+
<p className="text-[10px] text-[#9d99b3] mt-0.5">Configure how the AI model generates responses.</p>
|
| 48 |
</div>
|
|
|
|
| 49 |
<div className="space-y-5 pt-1">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
|
|
|
| 51 |
<div className="space-y-1.5">
|
| 52 |
+
<label className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Model</label>
|
| 53 |
+
<div className="bg-[#090810] border border-[#1d1b2e] text-xs text-white rounded-xl px-3 py-2.5 font-mono">
|
| 54 |
+
SmolLM2-360M-Instruct-Q4_K_M.gguf
|
|
|
|
|
|
|
|
|
|
| 55 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</div>
|
| 57 |
|
| 58 |
<div className="h-px bg-[#1d1b2e] my-2"></div>
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
<div className="space-y-2">
|
| 61 |
<div className="flex justify-between text-xs font-semibold">
|
| 62 |
<span className="text-white">Temperature</span>
|
| 63 |
<span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.temperature}</span>
|
| 64 |
</div>
|
| 65 |
<input
|
| 66 |
+
type="range" min="0.1" max="1.5" step="0.1"
|
|
|
|
|
|
|
|
|
|
| 67 |
value={settings.temperature}
|
| 68 |
onChange={(e) => updateSettings({ temperature: parseFloat(e.target.value) })}
|
| 69 |
+
className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer"
|
| 70 |
/>
|
|
|
|
| 71 |
</div>
|
| 72 |
|
|
|
|
| 73 |
<div className="space-y-2">
|
| 74 |
<div className="flex justify-between text-xs font-semibold">
|
| 75 |
<span className="text-white">Max Tokens</span>
|
| 76 |
<span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.maxTokens}</span>
|
| 77 |
</div>
|
| 78 |
<input
|
| 79 |
+
type="range" min="64" max="2048" step="64"
|
|
|
|
|
|
|
|
|
|
| 80 |
value={settings.maxTokens}
|
| 81 |
onChange={(e) => updateSettings({ maxTokens: parseInt(e.target.value) })}
|
| 82 |
+
className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer"
|
| 83 |
/>
|
|
|
|
| 84 |
</div>
|
| 85 |
|
|
|
|
| 86 |
<div className="space-y-2">
|
| 87 |
<div className="flex justify-between text-xs font-semibold">
|
| 88 |
<span className="text-white">Top P</span>
|
| 89 |
<span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.topP}</span>
|
| 90 |
</div>
|
| 91 |
<input
|
| 92 |
+
type="range" min="0.1" max="1.0" step="0.05"
|
|
|
|
|
|
|
|
|
|
| 93 |
value={settings.topP}
|
| 94 |
onChange={(e) => updateSettings({ topP: parseFloat(e.target.value) })}
|
| 95 |
+
className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer"
|
| 96 |
/>
|
|
|
|
| 97 |
</div>
|
| 98 |
|
| 99 |
<div className="h-px bg-[#1d1b2e] my-2"></div>
|
| 100 |
|
|
|
|
| 101 |
<div className="flex items-center justify-between py-1.5">
|
| 102 |
<div className="space-y-0.5">
|
| 103 |
+
<label className="text-xs font-bold text-white" htmlFor="stream-switch">Stream Response</label>
|
|
|
|
|
|
|
| 104 |
<p className="text-[10px] text-[#9d99b3]">Stream tokens as they are generated.</p>
|
| 105 |
</div>
|
| 106 |
+
<input type="checkbox" id="stream-switch"
|
|
|
|
|
|
|
| 107 |
checked={settings.streaming}
|
| 108 |
onChange={(e) => updateSettings({ streaming: e.target.checked })}
|
| 109 |
+
className="w-4.5 h-4.5 accent-primary cursor-pointer"
|
| 110 |
/>
|
| 111 |
</div>
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
<div className="flex items-center justify-end pt-3">
|
| 114 |
+
<button onClick={handleSave}
|
| 115 |
+
className="flex items-center justify-center gap-2 px-5 py-2.5 bg-[#6344d5] hover:bg-[#6344d5]/95 text-white font-bold text-xs rounded-xl cursor-pointer transition-all"
|
|
|
|
| 116 |
>
|
| 117 |
+
{saveSuccess ? <><CheckCircle2 size={13} /><span>Saved</span></> : <><Save size={13} /><span>Save Settings</span></>}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
</button>
|
| 119 |
</div>
|
| 120 |
|
| 121 |
</div>
|
| 122 |
</div>
|
| 123 |
|
|
|
|
| 124 |
<div className="lg:col-span-2 space-y-6">
|
|
|
|
|
|
|
| 125 |
<div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl space-y-4">
|
| 126 |
+
<h3 className="text-xs font-bold text-white uppercase tracking-wider border-b border-[#1d1b2e] pb-3">Model Information</h3>
|
|
|
|
|
|
|
|
|
|
| 127 |
<div className="space-y-3.5 text-xs select-none">
|
| 128 |
<div className="flex justify-between">
|
| 129 |
+
<span className="text-[#9d99b3]">Model</span>
|
| 130 |
+
<span className="font-semibold text-white font-mono text-[11px]">{modelInfo?.model_name || 'SmolLM2-360M-Q4_K_M'}</span>
|
| 131 |
</div>
|
| 132 |
<div className="flex justify-between">
|
| 133 |
+
<span className="text-[#9d99b3]">Status</span>
|
| 134 |
+
<span className="font-semibold text-emerald-500 text-[11px]">{modelInfo?.status || 'loaded'}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
</div>
|
| 136 |
<div className="flex justify-between">
|
| 137 |
+
<span className="text-[#9d99b3]">Quantization</span>
|
| 138 |
+
<span className="font-semibold text-white font-mono text-[11px]">Q4_K_M</span>
|
| 139 |
</div>
|
| 140 |
<div className="flex justify-between">
|
| 141 |
+
<span className="text-[#9d99b3]">Disk Size</span>
|
| 142 |
+
<span className="font-semibold text-white font-mono text-[11px]">~180 MB</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
</div>
|
| 144 |
</div>
|
| 145 |
</div>
|
frontend/src/hooks/useChat.ts
CHANGED
|
@@ -1,27 +1,22 @@
|
|
| 1 |
-
import { useState, useEffect
|
| 2 |
-
import type { ChatMessage, Conversation, AppSettings
|
| 3 |
|
| 4 |
-
const LOCAL_STORAGE_CONVS_KEY = '
|
| 5 |
-
const LOCAL_STORAGE_SETTINGS_KEY = '
|
| 6 |
|
| 7 |
const DEFAULT_SETTINGS: AppSettings = {
|
| 8 |
-
inferenceMode: 'auto',
|
| 9 |
temperature: 0.7,
|
| 10 |
-
maxTokens:
|
| 11 |
topP: 0.9,
|
| 12 |
-
contextLength: 4096,
|
| 13 |
streaming: true,
|
| 14 |
theme: 'dark',
|
| 15 |
-
hfToken: '',
|
| 16 |
-
hfModelId: 'Qwen/Qwen2.5-Coder-0.5B-Instruct',
|
| 17 |
};
|
| 18 |
|
| 19 |
export const useChat = () => {
|
| 20 |
const [conversations, setConversations] = useState<Conversation[]>([]);
|
| 21 |
const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
|
| 22 |
const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS);
|
| 23 |
-
const
|
| 24 |
-
const [metrics, setMetrics] = useState<ModelMetrics | null>(null);
|
| 25 |
const [isLoading, setIsLoading] = useState<boolean>(false);
|
| 26 |
const [error, setError] = useState<string | null>(null);
|
| 27 |
|
|
@@ -76,42 +71,7 @@ export const useChat = () => {
|
|
| 76 |
localStorage.setItem(LOCAL_STORAGE_SETTINGS_KEY, JSON.stringify(settings));
|
| 77 |
}, [settings]);
|
| 78 |
|
| 79 |
-
//
|
| 80 |
-
const fetchModelInfo = useCallback(async () => {
|
| 81 |
-
try {
|
| 82 |
-
const response = await fetch('/api/model-info');
|
| 83 |
-
if (response.ok) {
|
| 84 |
-
const data = await response.json();
|
| 85 |
-
setModelInfo(data);
|
| 86 |
-
}
|
| 87 |
-
} catch (e) {
|
| 88 |
-
console.error('Failed to load model info', e);
|
| 89 |
-
}
|
| 90 |
-
}, []);
|
| 91 |
-
|
| 92 |
-
// Fetch performance metrics
|
| 93 |
-
const fetchMetrics = useCallback(async () => {
|
| 94 |
-
try {
|
| 95 |
-
const response = await fetch('/api/metrics');
|
| 96 |
-
if (response.ok) {
|
| 97 |
-
const data = await response.json();
|
| 98 |
-
setMetrics(data);
|
| 99 |
-
}
|
| 100 |
-
} catch (e) {
|
| 101 |
-
console.error('Failed to load performance metrics', e);
|
| 102 |
-
}
|
| 103 |
-
}, []);
|
| 104 |
-
|
| 105 |
-
useEffect(() => {
|
| 106 |
-
fetchModelInfo();
|
| 107 |
-
fetchMetrics();
|
| 108 |
-
// Poll metrics and model status every 10 seconds
|
| 109 |
-
const interval = setInterval(() => {
|
| 110 |
-
fetchModelInfo();
|
| 111 |
-
fetchMetrics();
|
| 112 |
-
}, 10000);
|
| 113 |
-
return () => clearInterval(interval);
|
| 114 |
-
}, [fetchModelInfo, fetchMetrics]);
|
| 115 |
|
| 116 |
const updateSettings = (newSettings: Partial<AppSettings>) => {
|
| 117 |
setSettings((prev) => {
|
|
@@ -126,7 +86,7 @@ export const useChat = () => {
|
|
| 126 |
id: Math.random().toString(36).substring(7),
|
| 127 |
title: cleanTitle,
|
| 128 |
messages: [],
|
| 129 |
-
activeModel:
|
| 130 |
timestamp: Date.now(),
|
| 131 |
};
|
| 132 |
setConversations((prev) => [newConv, ...prev]);
|
|
@@ -243,10 +203,7 @@ export const useChat = () => {
|
|
| 243 |
);
|
| 244 |
}
|
| 245 |
|
| 246 |
-
if (chunkData.done && chunkData.usage) {
|
| 247 |
-
fetchMetrics();
|
| 248 |
-
fetchModelInfo();
|
| 249 |
-
}
|
| 250 |
} catch (e) {
|
| 251 |
// Ignore incomplete JSON chunks
|
| 252 |
}
|
|
@@ -267,8 +224,6 @@ export const useChat = () => {
|
|
| 267 |
: c
|
| 268 |
)
|
| 269 |
);
|
| 270 |
-
fetchMetrics();
|
| 271 |
-
fetchModelInfo();
|
| 272 |
}
|
| 273 |
} catch (e: any) {
|
| 274 |
console.error(e);
|
|
@@ -292,31 +247,23 @@ export const useChat = () => {
|
|
| 292 |
}
|
| 293 |
};
|
| 294 |
|
| 295 |
-
const executeCodeAction = async (action:
|
| 296 |
setIsLoading(true);
|
| 297 |
setError(null);
|
| 298 |
try {
|
| 299 |
-
const
|
|
|
|
| 300 |
method: 'POST',
|
| 301 |
-
headers: {
|
| 302 |
-
'Content-Type': 'application/json',
|
| 303 |
-
},
|
| 304 |
body: JSON.stringify({
|
| 305 |
-
|
| 306 |
-
language,
|
| 307 |
-
context,
|
| 308 |
temperature: settings.temperature,
|
| 309 |
max_tokens: settings.maxTokens,
|
| 310 |
stream: false,
|
| 311 |
}),
|
| 312 |
});
|
| 313 |
-
|
| 314 |
-
if (!response.ok) {
|
| 315 |
-
throw new Error(`API failed: ${response.statusText}`);
|
| 316 |
-
}
|
| 317 |
-
|
| 318 |
const result = await response.json();
|
| 319 |
-
fetchMetrics();
|
| 320 |
return result.content;
|
| 321 |
} catch (e: any) {
|
| 322 |
console.error(e);
|
|
@@ -367,7 +314,6 @@ export const useChat = () => {
|
|
| 367 |
activeConversation,
|
| 368 |
settings,
|
| 369 |
modelInfo,
|
| 370 |
-
metrics,
|
| 371 |
isLoading,
|
| 372 |
error,
|
| 373 |
updateSettings,
|
|
@@ -377,6 +323,5 @@ export const useChat = () => {
|
|
| 377 |
executeCodeAction,
|
| 378 |
getCompletion,
|
| 379 |
clearHistory,
|
| 380 |
-
fetchModelInfo,
|
| 381 |
};
|
| 382 |
};
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
import type { ChatMessage, Conversation, AppSettings } from '../types';
|
| 3 |
|
| 4 |
+
const LOCAL_STORAGE_CONVS_KEY = 'levi_conversations';
|
| 5 |
+
const LOCAL_STORAGE_SETTINGS_KEY = 'levi_settings';
|
| 6 |
|
| 7 |
const DEFAULT_SETTINGS: AppSettings = {
|
|
|
|
| 8 |
temperature: 0.7,
|
| 9 |
+
maxTokens: 512,
|
| 10 |
topP: 0.9,
|
|
|
|
| 11 |
streaming: true,
|
| 12 |
theme: 'dark',
|
|
|
|
|
|
|
| 13 |
};
|
| 14 |
|
| 15 |
export const useChat = () => {
|
| 16 |
const [conversations, setConversations] = useState<Conversation[]>([]);
|
| 17 |
const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
|
| 18 |
const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS);
|
| 19 |
+
const modelInfo = { model_name: 'SmolLM2-360M-Q4_K_M', status: 'loaded' };
|
|
|
|
| 20 |
const [isLoading, setIsLoading] = useState<boolean>(false);
|
| 21 |
const [error, setError] = useState<string | null>(null);
|
| 22 |
|
|
|
|
| 71 |
localStorage.setItem(LOCAL_STORAGE_SETTINGS_KEY, JSON.stringify(settings));
|
| 72 |
}, [settings]);
|
| 73 |
|
| 74 |
+
// no-op: model info is static for 512MB-optimized local mode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
const updateSettings = (newSettings: Partial<AppSettings>) => {
|
| 77 |
setSettings((prev) => {
|
|
|
|
| 86 |
id: Math.random().toString(36).substring(7),
|
| 87 |
title: cleanTitle,
|
| 88 |
messages: [],
|
| 89 |
+
activeModel: 'SmolLM2-360M-Q4_K_M',
|
| 90 |
timestamp: Date.now(),
|
| 91 |
};
|
| 92 |
setConversations((prev) => [newConv, ...prev]);
|
|
|
|
| 203 |
);
|
| 204 |
}
|
| 205 |
|
| 206 |
+
if (chunkData.done && chunkData.usage) {}
|
|
|
|
|
|
|
|
|
|
| 207 |
} catch (e) {
|
| 208 |
// Ignore incomplete JSON chunks
|
| 209 |
}
|
|
|
|
| 224 |
: c
|
| 225 |
)
|
| 226 |
);
|
|
|
|
|
|
|
| 227 |
}
|
| 228 |
} catch (e: any) {
|
| 229 |
console.error(e);
|
|
|
|
| 247 |
}
|
| 248 |
};
|
| 249 |
|
| 250 |
+
const executeCodeAction = async (action: string, code: string, language: string, context?: string): Promise<string> => {
|
| 251 |
setIsLoading(true);
|
| 252 |
setError(null);
|
| 253 |
try {
|
| 254 |
+
const prompt = `${action} the following ${language} code:\n\n${code}${context ? '\n\nContext: ' + context : ''}`;
|
| 255 |
+
const response = await fetch('/api/chat', {
|
| 256 |
method: 'POST',
|
| 257 |
+
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
|
| 258 |
body: JSON.stringify({
|
| 259 |
+
messages: [{ role: 'user', content: prompt }],
|
|
|
|
|
|
|
| 260 |
temperature: settings.temperature,
|
| 261 |
max_tokens: settings.maxTokens,
|
| 262 |
stream: false,
|
| 263 |
}),
|
| 264 |
});
|
| 265 |
+
if (!response.ok) throw new Error(`API failed: ${response.statusText}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
const result = await response.json();
|
|
|
|
| 267 |
return result.content;
|
| 268 |
} catch (e: any) {
|
| 269 |
console.error(e);
|
|
|
|
| 314 |
activeConversation,
|
| 315 |
settings,
|
| 316 |
modelInfo,
|
|
|
|
| 317 |
isLoading,
|
| 318 |
error,
|
| 319 |
updateSettings,
|
|
|
|
| 323 |
executeCodeAction,
|
| 324 |
getCompletion,
|
| 325 |
clearHistory,
|
|
|
|
| 326 |
};
|
| 327 |
};
|
frontend/src/types.ts
CHANGED
|
@@ -15,35 +15,13 @@ export interface Conversation {
|
|
| 15 |
|
| 16 |
export interface ModelInfo {
|
| 17 |
model_name: string;
|
| 18 |
-
inference_mode: string;
|
| 19 |
status: string;
|
| 20 |
-
memory_usage_gb: number;
|
| 21 |
-
total_memory_gb: number;
|
| 22 |
-
local_model_exists: boolean;
|
| 23 |
-
local_model_path: string;
|
| 24 |
-
device: string;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
export interface ModelMetrics {
|
| 28 |
-
total_requests: number;
|
| 29 |
-
total_prompt_tokens: number;
|
| 30 |
-
total_completion_tokens: number;
|
| 31 |
-
total_tokens: number;
|
| 32 |
-
average_latency_seconds: number;
|
| 33 |
-
total_generation_time_seconds: number;
|
| 34 |
-
active_mode: string;
|
| 35 |
-
system_ram_gb: number;
|
| 36 |
-
device: string;
|
| 37 |
}
|
| 38 |
|
| 39 |
export interface AppSettings {
|
| 40 |
-
inferenceMode: 'auto' | 'local' | 'huggingface';
|
| 41 |
temperature: number;
|
| 42 |
maxTokens: number;
|
| 43 |
topP: number;
|
| 44 |
-
contextLength: number;
|
| 45 |
streaming: boolean;
|
| 46 |
theme: 'dark' | 'light';
|
| 47 |
-
hfToken: string;
|
| 48 |
-
hfModelId: string;
|
| 49 |
}
|
|
|
|
| 15 |
|
| 16 |
export interface ModelInfo {
|
| 17 |
model_name: string;
|
|
|
|
| 18 |
status: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
}
|
| 20 |
|
| 21 |
export interface AppSettings {
|
|
|
|
| 22 |
temperature: number;
|
| 23 |
maxTokens: number;
|
| 24 |
topP: number;
|
|
|
|
| 25 |
streaming: boolean;
|
| 26 |
theme: 'dark' | 'light';
|
|
|
|
|
|
|
| 27 |
}
|
models/.gitkeep
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# Keep this folder so that models can be downloaded here
|
|
|
|
|
|
render.yaml
CHANGED
|
@@ -25,3 +25,7 @@ services:
|
|
| 25 |
generateValue: true
|
| 26 |
- key: HF_API_TOKEN
|
| 27 |
sync: false # Set in Render UI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
generateValue: true
|
| 26 |
- key: HF_API_TOKEN
|
| 27 |
sync: false # Set in Render UI
|
| 28 |
+
disk:
|
| 29 |
+
name: models-storage
|
| 30 |
+
mountPath: /app/models
|
| 31 |
+
sizeGB: 10
|
scripts/download_model.py
CHANGED
|
@@ -2,38 +2,24 @@ import os
|
|
| 2 |
import sys
|
| 3 |
|
| 4 |
def download_model():
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
target_dir = os.path.dirname(os.path.abspath(model_path))
|
| 11 |
os.makedirs(target_dir, exist_ok=True)
|
| 12 |
-
|
| 13 |
-
print(f"
|
| 14 |
-
print(f"Starting Qwen GGUF model downloader...")
|
| 15 |
-
print(f"Source HF Repo: {repo_id}")
|
| 16 |
-
print(f"Target Filename: {filename}")
|
| 17 |
-
print(f"Destination Dir: {target_dir}")
|
| 18 |
-
print(f"============================================================")
|
| 19 |
-
|
| 20 |
try:
|
| 21 |
from huggingface_hub import hf_hub_download
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
repo_id=repo_id,
|
| 26 |
-
filename=filename,
|
| 27 |
-
local_dir=target_dir,
|
| 28 |
-
local_dir_use_symlinks=False
|
| 29 |
-
)
|
| 30 |
-
print("Model file downloaded successfully!")
|
| 31 |
-
print(f"Located at: {os.path.abspath(model_path)}")
|
| 32 |
except ImportError:
|
| 33 |
-
print("
|
| 34 |
sys.exit(1)
|
| 35 |
except Exception as e:
|
| 36 |
-
print(f"
|
| 37 |
sys.exit(1)
|
| 38 |
|
| 39 |
if __name__ == "__main__":
|
|
|
|
| 2 |
import sys
|
| 3 |
|
| 4 |
def download_model():
|
| 5 |
+
repo_id = os.getenv("LOCAL_MODEL_REPO", "bartowski/SmolLM2-360M-Instruct-GGUF")
|
| 6 |
+
filename = os.getenv("LOCAL_MODEL_FILE", "SmolLM2-360M-Instruct-Q4_K_M.gguf")
|
| 7 |
+
model_path = os.getenv("LOCAL_MODEL_PATH", "models/SmolLM2-360M-Instruct-Q4_K_M.gguf")
|
| 8 |
+
|
|
|
|
| 9 |
target_dir = os.path.dirname(os.path.abspath(model_path))
|
| 10 |
os.makedirs(target_dir, exist_ok=True)
|
| 11 |
+
|
| 12 |
+
print(f"Downloading {filename} (~180MB) from {repo_id}...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
try:
|
| 14 |
from huggingface_hub import hf_hub_download
|
| 15 |
+
hf_hub_download(repo_id=repo_id, filename=filename,
|
| 16 |
+
local_dir=target_dir, local_dir_use_symlinks=False)
|
| 17 |
+
print(f"Saved to {os.path.abspath(model_path)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
except ImportError:
|
| 19 |
+
print("Run: pip install huggingface_hub")
|
| 20 |
sys.exit(1)
|
| 21 |
except Exception as e:
|
| 22 |
+
print(f"Download failed: {e}")
|
| 23 |
sys.exit(1)
|
| 24 |
|
| 25 |
if __name__ == "__main__":
|
scripts/setup.sh
CHANGED
|
@@ -1,50 +1,18 @@
|
|
| 1 |
#!/bin/bash
|
| 2 |
-
# Antigravity Coder Setup Script
|
| 3 |
-
|
| 4 |
set -e
|
| 5 |
|
| 6 |
-
echo "===
|
| 7 |
-
|
| 8 |
-
# Check requirements
|
| 9 |
-
if ! command -v python3 &> /dev/null; then
|
| 10 |
-
echo "Error: Python 3 is not installed. Please install Python 3.11+."
|
| 11 |
-
exit 1
|
| 12 |
-
fi
|
| 13 |
|
| 14 |
-
if ! command -v node &> /dev/null; then
|
| 15 |
-
echo "Error: Node.js is not installed. Please install Node.js 20+."
|
| 16 |
-
exit 1
|
| 17 |
-
fi
|
| 18 |
-
|
| 19 |
-
# 1. Setup Python Virtual Environment
|
| 20 |
-
echo "Setting up Python virtual environment..."
|
| 21 |
python3 -m venv venv
|
| 22 |
source venv/bin/activate
|
| 23 |
-
|
| 24 |
-
# 2. Install Python Dependencies
|
| 25 |
-
echo "Installing backend dependencies (this may compile llama-cpp-python)..."
|
| 26 |
pip install --upgrade pip
|
| 27 |
pip install -r backend/requirements.txt
|
| 28 |
|
| 29 |
-
# 3. Install Frontend Dependencies
|
| 30 |
-
echo "Installing frontend dependencies..."
|
| 31 |
cd frontend
|
| 32 |
npm install
|
| 33 |
-
cd ..
|
| 34 |
-
|
| 35 |
-
# 4. Build Frontend Assets
|
| 36 |
-
echo "Compiling frontend assets..."
|
| 37 |
-
cd frontend
|
| 38 |
npm run build
|
| 39 |
cd ..
|
| 40 |
|
| 41 |
-
# 5. Pre-download local model
|
| 42 |
-
echo "Downloading Qwen2.5-Coder model..."
|
| 43 |
-
source venv/bin/activate
|
| 44 |
python scripts/download_model.py
|
| 45 |
|
| 46 |
-
echo "======
|
| 47 |
-
echo "Setup Complete!"
|
| 48 |
-
echo "To start the backend: source venv/bin/activate && python backend/run.py"
|
| 49 |
-
echo "To start frontend dev server: cd frontend && npm run dev"
|
| 50 |
-
echo "============================================="
|
|
|
|
| 1 |
#!/bin/bash
|
|
|
|
|
|
|
| 2 |
set -e
|
| 3 |
|
| 4 |
+
echo "=== Levi AI Coder Setup ==="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
python3 -m venv venv
|
| 7 |
source venv/bin/activate
|
|
|
|
|
|
|
|
|
|
| 8 |
pip install --upgrade pip
|
| 9 |
pip install -r backend/requirements.txt
|
| 10 |
|
|
|
|
|
|
|
| 11 |
cd frontend
|
| 12 |
npm install
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
npm run build
|
| 14 |
cd ..
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
python scripts/download_model.py
|
| 17 |
|
| 18 |
+
echo "=== Done! Run: source venv/bin/activate && python -m uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 ==="
|
|
|
|
|
|
|
|
|
|
|
|
tests/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# Test package
|
|
|
|
|
|
tests/test_backend.py
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import sys
|
| 3 |
-
import unittest
|
| 4 |
-
from fastapi.testclient import TestClient
|
| 5 |
-
|
| 6 |
-
# Ensure python paths are mapped correctly
|
| 7 |
-
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 8 |
-
|
| 9 |
-
from backend.app.main import app
|
| 10 |
-
from backend.app.utils import estimate_tokens, get_code_prompt, get_completion_prompt
|
| 11 |
-
from backend.app.config import settings
|
| 12 |
-
|
| 13 |
-
class TestBackendUtilities(unittest.TestCase):
|
| 14 |
-
def test_token_estimation(self):
|
| 15 |
-
# Verify basic estimation boundaries
|
| 16 |
-
self.assertEqual(estimate_tokens(""), 0)
|
| 17 |
-
|
| 18 |
-
sample_text = "def hello_world():\n print('Hello World')"
|
| 19 |
-
tokens = estimate_tokens(sample_text)
|
| 20 |
-
self.assertGreater(tokens, 0)
|
| 21 |
-
self.assertLess(tokens, len(sample_text))
|
| 22 |
-
|
| 23 |
-
def test_prompt_constructors(self):
|
| 24 |
-
# Test code prompt builder
|
| 25 |
-
prompt = get_code_prompt("explain", "print(10)", "python")
|
| 26 |
-
self.assertIn("print(10)", prompt)
|
| 27 |
-
self.assertIn("explain", prompt.lower())
|
| 28 |
-
|
| 29 |
-
# Test code completion prompt builder
|
| 30 |
-
completion_prompt = get_completion_prompt("def add(a, b):", "return a + b", "python")
|
| 31 |
-
self.assertIn("def add(a, b):", completion_prompt)
|
| 32 |
-
self.assertIn("return a + b", completion_prompt)
|
| 33 |
-
|
| 34 |
-
class TestAPIEndpoints(unittest.TestCase):
|
| 35 |
-
def setUp(self):
|
| 36 |
-
self.client = TestClient(app)
|
| 37 |
-
|
| 38 |
-
def test_health_check(self):
|
| 39 |
-
response = self.client.get("/health")
|
| 40 |
-
self.assertEqual(response.status_code, 200)
|
| 41 |
-
data = response.json()
|
| 42 |
-
self.assertEqual(data["status"], "healthy")
|
| 43 |
-
self.assertIn("active_mode", data)
|
| 44 |
-
|
| 45 |
-
def test_metrics_endpoints(self):
|
| 46 |
-
response = self.client.get("/metrics")
|
| 47 |
-
self.assertEqual(response.status_code, 200)
|
| 48 |
-
data = response.json()
|
| 49 |
-
self.assertIn("total_requests", data)
|
| 50 |
-
self.assertIn("average_latency_seconds", data)
|
| 51 |
-
|
| 52 |
-
def test_model_info(self):
|
| 53 |
-
response = self.client.get("/model-info")
|
| 54 |
-
self.assertEqual(response.status_code, 200)
|
| 55 |
-
data = response.json()
|
| 56 |
-
self.assertIn("model_name", data)
|
| 57 |
-
self.assertIn("inference_mode", data)
|
| 58 |
-
self.assertIn("device", data)
|
| 59 |
-
|
| 60 |
-
if __name__ == "__main__":
|
| 61 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|