Manish Kumar commited on
Commit
c07244c
·
1 Parent(s): a19c940
.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=1024
17
  DEFAULT_TOP_P=0.9
18
- DEFAULT_CONTEXT_LENGTH=2048
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 React Frontend
2
- FROM node:20-slim as frontend-builder
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 Python Backend & Package app
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
- # Setup environments
45
- ENV PORT=8000
46
- ENV HOST=0.0.0.0
47
- ENV PYTHONPATH=/app
48
 
49
  EXPOSE 8000
50
 
51
- # Start unified server
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
- # Server settings
8
- PORT: int = Field(default=8000, validation_alias="PORT")
9
- HOST: str = Field(default="0.0.0.0", validation_alias="HOST")
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
- # LLM Settings
17
- INFERENCE_MODE: str = Field(default="local", validation_alias="INFERENCE_MODE")
18
 
19
- # Local GGUF Settings
20
- LOCAL_MODEL_PATH: str = Field(
21
- default="models/SmolLM2-360M-Instruct-Q4_K_M.gguf",
22
- validation_alias="LOCAL_MODEL_PATH"
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
- # Generation Defaults
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("Cannot initialize LocalLLMProvider: llama-cpp-python not installed.")
31
  return False
32
-
33
  if not os.path.exists(self.model_path):
34
- logger.error(f"Local GGUF model file not found at: {self.model_path}")
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
- logger.info(f"Loading local model from {self.model_path} with {threads} threads and context {self.context_length}...")
42
-
43
- # Since loading the model blocks, run it in a separate thread to keep event loop active
 
 
44
  def load_model():
45
  return Llama(
46
  model_path=self.model_path,
47
  n_ctx=self.context_length,
48
  n_threads=threads,
49
- verbose=settings.DEBUG
 
 
 
50
  )
51
-
52
  self.llm = await asyncio.to_thread(load_model)
53
  self.initialized = True
54
- logger.info("Local model successfully loaded!")
 
 
 
55
  return True
56
  except Exception as e:
57
- logger.error(f"Failed to load local model: {e}", exc_info=True)
58
  self.initialized = False
59
  self.llm = None
60
  return False
61
 
62
- def _format_prompt(self, prompt: str, system_prompt: Optional[str] = None, messages: Optional[List[Dict[str, str]]] = None) -> str:
63
- # Build standard Qwen ChatML prompt format
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
- role = msg.get("role", "user")
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
- self,
88
- prompt: str,
89
- system_prompt: Optional[str] = None,
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 provider is not initialized.")
97
-
98
- formatted_prompt = self._format_prompt(prompt, system_prompt, messages)
99
-
100
- def run_inference():
101
- return self.llm(
102
- prompt=formatted_prompt,
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(run_inference)
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": 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
- self,
126
- prompt: str,
127
- system_prompt: Optional[str] = None,
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 provider is not initialized.")
 
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
- prompt=formatted_prompt,
142
- max_tokens=max_tokens,
143
- temperature=temperature,
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 async_generator():
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 async_generator()
160
 
161
  def get_info(self) -> Dict[str, Any]:
162
  return {
163
- "provider_name": "local",
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 ensure_model_downloaded(self) -> bool:
17
- """Downloads the GGUF model file if not exists."""
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
- info = p.get_info()
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, Depends, HTTPException, status
11
  from fastapi.middleware.cors import CORSMiddleware
12
- from fastapi.responses import StreamingResponse, JSONResponse
13
  from fastapi.staticfiles import StaticFiles
14
 
15
  from backend.app.config import settings
16
  from backend.app.models import (
17
- ChatRequest, ChatResponse, ChatResponseChunk,
18
- CodeRequest, CompletionRequest, ModelInfoResponse
19
  )
20
- from backend.app.middleware import RateLimitMiddleware, LoggingMiddleware
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
- # Model warm-up on startup (non-blocking for fast server boot)
67
- logger.info("Initializing LLM Manager and warming up model in the background...")
68
  asyncio.create_task(llm_manager.setup_provider())
69
  yield
70
- # Shutdown operations
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
- # Add Middleware
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=settings.cors_origins_list,
87
  allow_credentials=True,
88
  allow_methods=["*"],
89
  allow_headers=["*"],
90
  )
91
 
92
- # Stream Generator Helper
93
- async def run_stream_generator(
94
- prompt: str,
95
- system_prompt: str = None,
96
- messages: list = None,
97
- temperature: float = 0.7,
98
- max_tokens: int = 1024,
99
  ) -> AsyncGenerator[str, None]:
100
- start_time = time.time()
101
- generated_content = ""
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
- system_prompt=system_prompt,
111
- messages=messages,
112
- temperature=temperature,
113
- max_tokens=max_tokens
114
  )
115
-
116
  async for chunk in stream:
117
- generated_content += chunk
118
- # Format SSE yield
119
- yield format_sse_chunk(content=chunk, done=False)
120
-
121
- duration = time.time() - start_time
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"Error in running streaming generator: {e}")
137
- yield format_sse_chunk(content=f"\n[Generation Error: {e}]", done=True)
 
138
 
139
- # Standard Non-Stream Helper
140
- async def run_standard_generation(
141
- prompt: str,
142
- system_prompt: str = None,
143
- messages: list = None,
144
- temperature: float = 0.7,
145
- max_tokens: int = 1024,
146
  ) -> ChatResponse:
147
- start_time = time.time()
148
  try:
149
- response_data = await llm_manager.generate(
150
- prompt=prompt,
151
- system_prompt=system_prompt,
152
- messages=messages,
153
- temperature=temperature,
154
- max_tokens=max_tokens
155
  )
156
-
157
- duration = time.time() - start_time
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=response_data["content"],
178
- usage=metrics_dict,
179
- model=status_info.get("model_id", status_info.get("model_path", "qwen-0.5b"))
 
 
 
 
 
180
  )
181
  except Exception as e:
182
- logger.error(f"Error in standard generation: {e}")
183
- raise HTTPException(
184
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
185
- detail=str(e)
186
- )
187
 
188
- # API ENDPOINTS
189
  @app.post("/chat")
190
  async def chat(request: ChatRequest):
191
- messages_list = [m.model_dump() for m in request.messages]
192
-
193
- # Extract system prompt if present in payload
194
  system_prompt = None
195
- user_messages = []
196
- for msg in messages_list:
197
- if msg["role"] == "system":
198
- system_prompt = msg["content"]
199
  else:
200
- user_messages.append(msg)
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
- run_stream_generator(
208
- prompt=last_prompt,
209
- system_prompt=system_prompt,
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
- run_stream_generator(prompt=prompt, temperature=temp, max_tokens=max_t),
236
- media_type="text/event-stream"
 
237
  )
238
- else:
239
- return await run_standard_generation(prompt=prompt, temperature=temp, max_tokens=max_t)
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
- # Health check responds instantly for cloud pingers
267
- status_info = await llm_manager.get_status_info()
268
  return {
269
- "status": "healthy",
270
- "timestamp": time.time(),
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"Frontend dist found. Serving frontend from {frontend_dir}")
316
  else:
317
- logger.warning(f"Frontend dist not found at {frontend_dir}. Running API-only server mode.")
 
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 collections import defaultdict
4
- from fastapi import Request, Response, status
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
- logger.info(
59
- f"Outgoing: {request.method} {request.url.path} - "
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
- f"Exception: {request.method} {request.url.path} failed - "
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 = Field(..., description="Role of the message author (system, user, assistant)")
6
- content: str = Field(..., description="Content of the message")
7
 
8
  class ChatRequest(BaseModel):
9
- messages: List[ChatMessage] = Field(..., description="Conversation history including current prompt")
10
- temperature: Optional[float] = Field(None, description="Controls randomness (0.0 to 1.0)")
11
- max_tokens: Optional[int] = Field(None, description="Maximum number of tokens to generate")
12
- top_p: Optional[float] = Field(None, description="Nucleus sampling threshold")
13
- stream: Optional[bool] = Field(True, description="Whether to stream responses")
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 = Field(..., description="Code before the cursor position")
25
- suffix: Optional[str] = Field("", description="Code after the cursor position")
26
- language: Optional[str] = Field("python")
27
- max_tokens: Optional[int] = Field(128)
28
- temperature: Optional[float] = Field(0.2)
29
- stream: Optional[bool] = Field(False)
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 time
2
- from typing import Dict, Any, List, Optional
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
- # Average of char-based and word-based estimations
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
- """Format data for Server-Sent Events transmission."""
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"You are a code auto-completion utility. Continue the code for the {language} language. "
77
- "Your task is to fill in the missing code between the Prefix and Suffix. "
78
- "Return ONLY the code that should be inserted directly at the transition point. "
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
- python-dotenv>=1.0.0
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, Sparkles, AlertCircle, Copy, Check,
5
- Upload, Terminal, HelpCircle, FileText, Download,
6
- CornerDownLeft, Paperclip, RotateCcw,
7
- User, Shield, MessageSquare, Plus, Search, Code, Mic, Trash2
8
  } from 'lucide-react';
9
- import type { ChatMessage, Conversation, ModelInfo } from '../types';
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, Terminal, Code2, Copy, Check, ChevronDown,
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, Zap, BarChart2, ChevronRight, HelpCircle, Database
6
  } from 'lucide-react';
7
- import type { ModelInfo, ModelMetrics } from '../types';
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
- // Safe formatting variables matching image values
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 & Metrics */}
106
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 select-none">
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 duration-300">
110
  <div className="flex items-center justify-between">
111
- <span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Model Status</span>
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 Allocation Card with Progress Bar */}
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 duration-300">
124
  <div className="flex items-center justify-between">
125
- <span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Memory Usage</span>
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-xl font-bold text-white">{latency.toFixed(2)} s</h2>
151
- <p className="text-[10px] text-[#58556f]">Per request</p>
152
  </div>
153
  </div>
154
 
155
- {/* Tokens Card */}
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 duration-300">
157
  <div className="flex items-center justify-between">
158
- <span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Tokens Used</span>
159
- <div className="p-1.5 bg-red-500/10 rounded-lg text-red-500">
160
- <BarChart2 size={14} />
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">{totalTokens.toLocaleString()}</h2>
165
- <p className="text-[10px] text-[#58556f]">Total (Input + Output)</p>
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, Globe,
5
- CheckCircle2, AlertCircle, HelpCircle
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('Local database log history has been successfully reset.');
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">Model & Inference</h2>
60
- <p className="text-[10px] text-[#9d99b3] mt-0.5">Configure how the AI model runs and generates responses.</p>
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
- <div className="flex justify-between items-center select-none">
79
- <label className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Model</label>
80
- <span className="flex items-center gap-1 text-[10px] text-emerald-500 font-bold uppercase tracking-wider bg-emerald-500/10 px-2 py-0.5 rounded">
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 focus:outline-none"
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 focus:outline-none"
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 focus:outline-none"
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 cursor-pointer" htmlFor="stream-switch">
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 rounded text-primary focus:ring-primary accent-primary bg-background border-border cursor-pointer focus-ring"
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
- onClick={handleSave}
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 flex items-center gap-2 select-none border-b border-[#1d1b2e] pb-3">
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 Name</span>
238
- <span className="font-semibold text-white font-mono text-[11px] truncate max-w-[140px]">{modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M-Instruct'}</span>
239
  </div>
240
  <div className="flex justify-between">
241
- <span className="text-[#9d99b3]">Quantization</span>
242
- <span className="font-semibold text-white font-mono text-[11px]">Q4_K_M</span>
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]">Vocabulary Size</span>
254
- <span className="font-semibold text-white font-mono text-[11px]">151,936</span>
255
  </div>
256
  <div className="flex justify-between">
257
- <span className="text-[#9d99b3]">Architecture</span>
258
- <span className="font-semibold text-white font-mono text-[11px]">Transformers</span>
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, useCallback } from 'react';
2
- import type { ChatMessage, Conversation, AppSettings, ModelInfo, ModelMetrics } from '../types';
3
 
4
- const LOCAL_STORAGE_CONVS_KEY = 'antigravity_conversations';
5
- const LOCAL_STORAGE_SETTINGS_KEY = 'antigravity_settings';
6
 
7
  const DEFAULT_SETTINGS: AppSettings = {
8
- inferenceMode: 'auto',
9
  temperature: 0.7,
10
- maxTokens: 1024,
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 [modelInfo, setModelInfo] = useState<ModelInfo | null>(null);
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
- // Fetch model metadata
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: modelInfo?.model_name || 'Qwen2.5-Coder',
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: 'explain' | 'debug' | 'refactor' | 'generate-tests' | 'summarize', code: string, language: string, context?: string): Promise<string> => {
296
  setIsLoading(true);
297
  setError(null);
298
  try {
299
- const response = await fetch(`/api/${action}`, {
 
300
  method: 'POST',
301
- headers: {
302
- 'Content-Type': 'application/json',
303
- },
304
  body: JSON.stringify({
305
- code,
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
- # Read settings from environment variables or use default
6
- repo_id = os.getenv("LOCAL_MODEL_REPO", "bartowski/Qwen2.5-Coder-0.5B-Instruct-GGUF")
7
- filename = os.getenv("LOCAL_MODEL_FILE", "Qwen2.5-Coder-0.5B-Instruct-Q4_K_M.gguf")
8
- model_path = os.getenv("LOCAL_MODEL_PATH", "models/Qwen2.5-Coder-0.5B-Instruct-Q4_K_M.gguf")
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
- print("Downloading GGUF file (approx. 397MB)...")
24
- hf_hub_download(
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("Error: 'huggingface_hub' package is not installed. Please run: pip install huggingface_hub")
34
  sys.exit(1)
35
  except Exception as e:
36
- print(f"Error downloading model: {e}")
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 "=== Starting Antigravity Coder Installation ==="
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()