mirror of
https://github.com/furyhawk/home_stack.git
synced 2026-07-21 02:06:47 +00:00
Merge remote-tracking branch 'origin/ai' into https
This commit is contained in:
+11
@@ -5,3 +5,14 @@ node_modules/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
frontend/.env
|
||||
.mypy_cache/
|
||||
# Ignore Python cache files
|
||||
__pycache__/
|
||||
unsloth_compiled_cache/
|
||||
notebooks/lora_model/
|
||||
ai_stack/llamacpp/models/
|
||||
ai_stack/ollama/models/
|
||||
notebooks/chroma_db/
|
||||
notebooks/llama-2-7b-chat.Q4_K_M.gguf
|
||||
esphome/config/
|
||||
ai_stack/llamacpp.server/models/
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Llama.cpp Server Persistence
|
||||
|
||||
This directory contains scripts and configurations for keeping the `llama-server` running across VM restarts and crashes.
|
||||
|
||||
## Options for Persistence
|
||||
|
||||
### 1. Docker (Recommended)
|
||||
The most isolated and portable way to run the server.
|
||||
|
||||
- **Configure**: Edit [docker-compose.yml](docker-compose.yml).
|
||||
- **Start**: `docker compose up -d`
|
||||
- **Why**: Handles dependencies, automatic restarts, and isolation.
|
||||
|
||||
---
|
||||
|
||||
### 2. Debian VM (Systemd)
|
||||
Native way to run as a persistent background service on Debian/Ubuntu.
|
||||
|
||||
- **File**: [llama-server.service](llama-server.service)
|
||||
- **Setup**:
|
||||
1. Copy to system: `sudo cp llama-server.service /etc/systemd/system/`
|
||||
2. Reload systemd: `sudo systemctl daemon-reload`
|
||||
3. Enable and Start: `sudo systemctl enable --now llama-server`
|
||||
- **Check health**: `sudo systemctl status llama-server`
|
||||
- **View logs**:
|
||||
- Real-time: `journalctl -u llama-server -f`
|
||||
- Since boot: `journalctl -u llama-server -b`
|
||||
|
||||
---
|
||||
|
||||
### 3. macOS (LaunchAgent)
|
||||
Native way to run on a macOS host.
|
||||
|
||||
- **File**: `~/Library/LaunchAgents/local.llama.server.plist`
|
||||
- **Setup**:
|
||||
1. Load it: `launchctl load ~/Library/LaunchAgents/local.llama.server.plist`
|
||||
2. Start it: `launchctl start local.llama.server`
|
||||
- **Check health**: `launchctl list | grep llama`
|
||||
|
||||
---
|
||||
|
||||
### 4. Simple Script (Manual)
|
||||
For quick testing without system integration.
|
||||
|
||||
- **Script**: [start.sh](start.sh)
|
||||
- **Run**: `./start.sh`
|
||||
- **Note**: Uses `nohup` to prevent termination when your SSH session closes, but will NOT restart after a VM reboot.
|
||||
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
llama-cpp-server:
|
||||
build: .
|
||||
image: ghcr.io/ggml-org/llama.cpp:full-cuda13
|
||||
ports:
|
||||
- "8080:8080" # Map host port 8080 to container port 8080
|
||||
volumes:
|
||||
- ./models:/models # Mount a local directory for models
|
||||
container_name: llama-cpp-server
|
||||
environment:
|
||||
- LLAMA_ARG_HOST=0.0.0.0
|
||||
- LLAMA_ARG_PORT=8080
|
||||
restart: unless-stopped
|
||||
command: [
|
||||
"--server",
|
||||
"--model", "/models/gemma-4-E2B-it-Q8_0.gguf",
|
||||
"--mmproj", "/models/mmproj-BF16.gguf",
|
||||
"--host", "0.0.0.0",
|
||||
"--port", "8080",
|
||||
"--flash-attn", "auto"
|
||||
]
|
||||
# Uncomment for GPU support
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: cdi
|
||||
capabilities: [gpu]
|
||||
device_ids:
|
||||
- nvidia.com/gpu=all
|
||||
@@ -0,0 +1,30 @@
|
||||
[Unit]
|
||||
Description=Llama.cpp Server for Qwen3.5
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment="MODEL_PATH=%h/unsloth/Qwen3.5-0.8B-GGUF/Qwen3.5-0.8B-UD-Q4_K_XL.gguf"
|
||||
Environment="MMPROJ_PATH=%h/unsloth/Qwen3.5-0.8B-GGUF/mmproj-BF16.gguf"
|
||||
ExecStart=/usr/local/bin/llama-server \
|
||||
--model ${MODEL_PATH} \
|
||||
--mmproj ${MMPROJ_PATH} \
|
||||
--temp 1.0 \
|
||||
--top-p 0.95 \
|
||||
--top-k 20 \
|
||||
--min-p 0.00 \
|
||||
--alias "unsloth/Qwen3.5-0.8B-GGUF" \
|
||||
--cache-type-k q4_0 \
|
||||
--cache-type-v q4_0 \
|
||||
--host 0.0.0.0 \
|
||||
--port 8011 \
|
||||
--chat-template-kwargs '{"enable_thinking":false}'
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
User=%u
|
||||
WorkingDirectory=%h/projects/home_stack/ai_stack/llamacpp.server
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<key>local.llama.server</key>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/bin/llama-server</string>
|
||||
<string>--model</string>
|
||||
<string>/Users/user/unsloth/Qwen3.5-0.8B-GGUF/Qwen3.5-0.8B-UD-Q4_K_XL.gguf</string>
|
||||
<string>--mmproj</string>
|
||||
<string>/Users/user/unsloth/Qwen3.5-0.8B-GGUF/mmproj-BF16.gguf</string>
|
||||
<string>--temp</string>
|
||||
<string>1.0</string>
|
||||
<string>--top-p</string>
|
||||
<string>0.95</string>
|
||||
<string>--top-k</string>
|
||||
<string>20</string>
|
||||
<string>--min-p</string>
|
||||
<string>0.00</string>
|
||||
<string>--alias</string>
|
||||
<string>unsloth/Qwen3.5-0.8B-GGUF</string>
|
||||
<string>--cache-type-k</string>
|
||||
<string>q4_0</string>
|
||||
<string>--cache-type-v</string>
|
||||
<string>q4_0</string>
|
||||
<string>--host</string>
|
||||
<string>0.0.0.0</string>
|
||||
<string>--port</string>
|
||||
<string>8011</string>
|
||||
<string>--chat-template-kwargs</string>
|
||||
<string>{"enable_thinking":false}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/user/projects/home_stack/ai_stack/llamacpp.server/server.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/user/projects/home_stack/ai_stack/llamacpp.server/server.log</string>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/user/projects/home_stack/ai_stack/llamacpp.server</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
nohup llama-server --model ~/unsloth/Qwen3.5-0.8B-GGUF/Qwen3.5-0.8B-UD-Q4_K_XL.gguf --mmproj ~/unsloth/Qwen3.5-0.8B-GGUF/mmproj-BF16.gguf --temp 1.0 --top-p 0.95 --top-k 20 --min-p 0.00 --alias "unsloth/Qwen3.5-0.8B-GGUF" --cache-type-k q4_0 --cache-type-v q4_0 --host 0.0.0.0 --port 8011 --chat-template-kwargs '{"enable_thinking":false}' > server.log 2>&1 &
|
||||
@@ -0,0 +1,8 @@
|
||||
# Python
|
||||
__pycache__
|
||||
app.egg-info
|
||||
*.pyc
|
||||
.mypy_cache
|
||||
.coverage
|
||||
htmlcov
|
||||
.venv
|
||||
@@ -0,0 +1,37 @@
|
||||
FROM ubuntu:latest
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
cmake \
|
||||
g++ \
|
||||
wget \
|
||||
curl \
|
||||
libcurl4-openssl-dev \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Clone llama.cpp
|
||||
RUN git clone https://github.com/ggerganov/llama.cpp.git
|
||||
|
||||
# Build llama.cpp with server support
|
||||
WORKDIR /llama.cpp
|
||||
RUN mkdir build && cd build && cmake .. -DLLAMA_SERVER=ON && make -j$(nproc)
|
||||
|
||||
# Copy the built executable to a standard location
|
||||
RUN cp /llama.cpp/build/bin/llama-server /usr/local/bin/llama-server || \
|
||||
cp /llama.cpp/build/llama-server /usr/local/bin/llama-server || \
|
||||
find /llama.cpp -name "llama-server" -type f -executable -exec cp {} /usr/local/bin/llama-server \;
|
||||
|
||||
# Create models directory
|
||||
RUN mkdir -p /models
|
||||
|
||||
# Expose the server port
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# Default command - will be overridden by docker-compose
|
||||
CMD ["llama-server", "--host", "0.0.0.0", "--port", "8080"]
|
||||
@@ -0,0 +1,272 @@
|
||||
# LlamaCPP Model Setup
|
||||
|
||||
Scripts and configuration for running DeepSeek-R1-0528-Qwen3-8B model with LlamaCPP server.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### 1. `pull-deepseek-llamacpp.sh`
|
||||
Downloads the DeepSeek-R1 GGUF model from Hugging Face and prepares it for LlamaCPP.
|
||||
|
||||
**Features:**
|
||||
- Downloads the Q4_K_XL quantized version (optimal balance of quality and size)
|
||||
- Verifies download integrity
|
||||
- Tests server connectivity
|
||||
- Provides usage examples
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Using Makefile (recommended)
|
||||
make setup-llamacpp # Start LlamaCPP and pull model automatically
|
||||
make pull-deepseek-llamacpp # Pull model only (requires server running)
|
||||
|
||||
# Or run directly
|
||||
./scripts/pull-deepseek-llamacpp.sh
|
||||
```
|
||||
|
||||
### 2. `llamacpp-gpu.sh`
|
||||
Starts GPU-accelerated LlamaCPP server with CUDA support for better performance.
|
||||
|
||||
**Features:**
|
||||
- Automatic GPU detection
|
||||
- CUDA-optimized build
|
||||
- GPU layer offloading (35 layers for 8B model)
|
||||
- Enhanced performance parameters
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
make llamacpp-gpu # Start GPU-accelerated server
|
||||
```
|
||||
|
||||
## Makefile Targets
|
||||
|
||||
### Basic Operations
|
||||
- `make llamacpp-up` - Start LlamaCPP container
|
||||
- `make llamacpp-down` - Stop LlamaCPP container
|
||||
- `make llamacpp-logs` - View container logs
|
||||
- `make pull-deepseek-llamacpp` - Download DeepSeek model
|
||||
- `make setup-llamacpp` - Complete setup (start + pull model)
|
||||
|
||||
### GPU Operations
|
||||
- `make llamacpp-gpu` - Start GPU-accelerated server
|
||||
|
||||
## Model Information
|
||||
|
||||
**DeepSeek-R1-0528-Qwen3-8B-GGUF**
|
||||
- **Source:** [unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF](https://huggingface.co/unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF)
|
||||
- **Quantization:** Q4_K_XL (4-bit quantization, extra large)
|
||||
- **Size:** ~5.5GB
|
||||
- **Context Length:** 32,768 tokens
|
||||
- **Performance:** Optimized for reasoning and conversation
|
||||
|
||||
## Server Configuration
|
||||
|
||||
### Default Parameters
|
||||
- **Host:** 0.0.0.0 (accessible externally)
|
||||
- **Port:** 8080
|
||||
- **Context Size:** 32,768 tokens
|
||||
- **Max Predict:** 2,048 tokens
|
||||
- **Batch Size:** 512
|
||||
- **Parallel Requests:** 2 (4 for GPU)
|
||||
- **Temperature:** 0.7
|
||||
- **Top-P:** 0.9
|
||||
- **Top-K:** 40
|
||||
|
||||
### GPU Configuration
|
||||
- **GPU Layers:** 35 (automatically offloaded to GPU)
|
||||
- **CUDA Support:** Enabled
|
||||
- **Flash Attention:** Enabled
|
||||
- **Parallel Requests:** 4
|
||||
|
||||
## Quick Start
|
||||
|
||||
### CPU-Only Setup
|
||||
```bash
|
||||
# 1. Start LlamaCPP and download model
|
||||
make setup-llamacpp
|
||||
|
||||
# 2. Test the server
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
### GPU-Accelerated Setup
|
||||
```bash
|
||||
# 1. Ensure model is downloaded
|
||||
make pull-deepseek-llamacpp
|
||||
|
||||
# 2. Start GPU server
|
||||
make llamacpp-gpu
|
||||
|
||||
# 3. Verify GPU usage
|
||||
nvidia-smi
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
### Text Completion
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/completion \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "Hello, can you explain quantum computing?",
|
||||
"max_tokens": 200,
|
||||
"temperature": 0.7,
|
||||
"stream": false
|
||||
}'
|
||||
```
|
||||
|
||||
### Streaming Completion
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/completion \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "Write a Python function to sort a list:",
|
||||
"max_tokens": 150,
|
||||
"stream": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Chat Completion (OpenAI-compatible)
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "DeepSeek-R1",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Explain machine learning in simple terms"}
|
||||
],
|
||||
"max_tokens": 200,
|
||||
"temperature": 0.7
|
||||
}'
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### CPU Optimization
|
||||
- **Threads:** Adjust based on CPU cores (default: 4)
|
||||
- **Batch Size:** Increase for better throughput (512-1024)
|
||||
- **Context Size:** Reduce if memory is limited
|
||||
|
||||
### GPU Optimization
|
||||
- **GPU Layers:** Increase if you have more VRAM (max: 35 for 8B model)
|
||||
- **Batch Size:** Increase for GPU (1024-2048)
|
||||
- **Parallel Requests:** Increase based on VRAM capacity
|
||||
|
||||
### Memory Requirements
|
||||
- **CPU Only:** ~8GB RAM minimum, 16GB recommended
|
||||
- **GPU:** ~6GB VRAM for full model, 4GB minimum with reduced layers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server Won't Start
|
||||
```bash
|
||||
# Check logs
|
||||
make llamacpp-logs
|
||||
|
||||
# Verify model file exists
|
||||
ls -la ai_stack/llamacpp/models/
|
||||
|
||||
# Test with minimal configuration
|
||||
docker run --rm -v $(pwd)/ai_stack/llamacpp/models:/models \
|
||||
llamacpp /llama.cpp/llama-server \
|
||||
--model /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf \
|
||||
--host 0.0.0.0 --port 8080 --ctx-size 2048
|
||||
```
|
||||
|
||||
### GPU Issues
|
||||
```bash
|
||||
# Check GPU availability
|
||||
nvidia-smi
|
||||
|
||||
# Verify CUDA installation
|
||||
nvcc --version
|
||||
|
||||
# Test GPU container
|
||||
docker run --rm --gpus all nvidia/cuda:12.1-base-ubuntu22.04 nvidia-smi
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
```bash
|
||||
# Monitor resource usage
|
||||
docker stats llama-cpp-server
|
||||
|
||||
# Check server metrics
|
||||
curl http://localhost:8080/metrics
|
||||
|
||||
# Reduce context size for better performance
|
||||
# Edit docker-compose.yml: --ctx-size 8192
|
||||
```
|
||||
|
||||
### API Errors
|
||||
```bash
|
||||
# Test basic connectivity
|
||||
curl -f http://localhost:8080/health
|
||||
|
||||
# Check server response
|
||||
curl -v http://localhost:8080/completion \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt": "test", "max_tokens": 5}'
|
||||
```
|
||||
|
||||
## File Locations
|
||||
|
||||
- **Models:** `./ai_stack/llamacpp/models/`
|
||||
- **Dockerfile:** `./ai_stack/llamacpp/Dockerfile`
|
||||
- **Compose:** `./ai_stack/llamacpp/docker-compose.yml`
|
||||
- **Scripts:** `./scripts/pull-deepseek-llamacpp.sh`, `./scripts/llamacpp-gpu.sh`
|
||||
|
||||
## Integration
|
||||
|
||||
### With Frontend Applications
|
||||
```javascript
|
||||
// Example frontend integration
|
||||
const response = await fetch('http://localhost:8080/completion', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: userInput,
|
||||
max_tokens: 200,
|
||||
temperature: 0.7,
|
||||
stream: false
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log(result.content);
|
||||
```
|
||||
|
||||
### With Python
|
||||
```python
|
||||
import requests
|
||||
|
||||
def query_llama(prompt, max_tokens=200):
|
||||
response = requests.post('http://localhost:8080/completion',
|
||||
json={
|
||||
'prompt': prompt,
|
||||
'max_tokens': max_tokens,
|
||||
'temperature': 0.7,
|
||||
'stream': False
|
||||
})
|
||||
return response.json()['content']
|
||||
|
||||
# Usage
|
||||
result = query_llama("Explain neural networks")
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Server Health
|
||||
- Health endpoint: `http://localhost:8080/health`
|
||||
- Metrics endpoint: `http://localhost:8080/metrics`
|
||||
- Logs: `make llamacpp-logs`
|
||||
|
||||
### Performance Metrics
|
||||
- Request latency and throughput
|
||||
- Token generation speed
|
||||
- Memory and GPU utilization
|
||||
- Queue depth and processing time
|
||||
@@ -0,0 +1,33 @@
|
||||
services:
|
||||
llama-cpp-server:
|
||||
build: .
|
||||
image: localhost/llama-cpp-server:latest
|
||||
ports:
|
||||
- "8080:8080" # Map host port 8080 to container port 8080
|
||||
volumes:
|
||||
- ./models:/models # Mount a local directory for models
|
||||
container_name: llama-cpp-server
|
||||
environment:
|
||||
- LLAMA_ARG_HOST=0.0.0.0
|
||||
- LLAMA_ARG_PORT=8080
|
||||
restart: unless-stopped
|
||||
command: [
|
||||
"llama-server",
|
||||
"--model", "/models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf",
|
||||
"--host", "0.0.0.0",
|
||||
"--port", "8080",
|
||||
"--ctx-size", "32768",
|
||||
"--threads", "4",
|
||||
"--batch-size", "512",
|
||||
"--cont-batching",
|
||||
"--parallel", "2",
|
||||
"--flash-attn"
|
||||
]
|
||||
# Uncomment for GPU support
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: all
|
||||
# capabilities: [gpu]
|
||||
@@ -0,0 +1,8 @@
|
||||
# Python
|
||||
__pycache__
|
||||
app.egg-info
|
||||
*.pyc
|
||||
.mypy_cache
|
||||
.coverage
|
||||
htmlcov
|
||||
.venv
|
||||
@@ -0,0 +1,42 @@
|
||||
# Use Ubuntu as base image for better compatibility with Ollama
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# Set environment variables
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV OLLAMA_HOST=0.0.0.0
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
wget \
|
||||
gnupg \
|
||||
zstd \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Ollama
|
||||
RUN curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# Create ollama user and directories
|
||||
RUN useradd -r -s /bin/false -m -d /usr/share/ollama ollama && \
|
||||
mkdir -p /usr/share/ollama/.ollama && \
|
||||
chown -R ollama:ollama /usr/share/ollama
|
||||
|
||||
# Create directory for models
|
||||
RUN mkdir -p /models && chown -R ollama:ollama /models
|
||||
|
||||
# Switch to ollama user
|
||||
USER ollama
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /usr/share/ollama
|
||||
|
||||
# Expose the default Ollama port
|
||||
EXPOSE 11434
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:11434/api/version || exit 1
|
||||
|
||||
# Start Ollama server
|
||||
CMD ["ollama", "serve"]
|
||||
@@ -0,0 +1,309 @@
|
||||
# Ollama Docker Setup
|
||||
|
||||
This directory contains the Docker configuration for running [Ollama](https://ollama.com/), a lightweight and extensible framework for running large language models locally.
|
||||
|
||||
## Overview
|
||||
|
||||
Ollama allows you to run large language models like Llama 2, Code Llama, and other models locally with a simple API interface. This Docker setup provides an easy way to deploy and manage Ollama in a containerized environment.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- At least 8GB of RAM (16GB+ recommended for larger models)
|
||||
- Optional: NVIDIA GPU with Docker GPU support for better performance
|
||||
|
||||
### Running Ollama
|
||||
|
||||
1. **Start the service:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. **Check if the service is running:**
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
3. **View logs:**
|
||||
```bash
|
||||
docker-compose logs -f ollama
|
||||
```
|
||||
|
||||
## Using Ollama
|
||||
|
||||
### Pulling Models
|
||||
|
||||
Once Ollama is running, you can pull models using the following commands:
|
||||
|
||||
```bash
|
||||
# Pull Llama 2 (7B parameters)
|
||||
docker exec ollama-server ollama pull llama2
|
||||
|
||||
# Pull Code Llama for code generation
|
||||
docker exec ollama-server ollama pull codellama
|
||||
|
||||
# Pull Mistral 7B
|
||||
docker exec ollama-server ollama pull mistral
|
||||
|
||||
# Pull other available models
|
||||
docker exec ollama-server ollama pull llama2:13b
|
||||
docker exec ollama-server ollama pull llama2:70b
|
||||
```
|
||||
|
||||
### List Available Models
|
||||
|
||||
```bash
|
||||
docker exec ollama-server ollama list
|
||||
```
|
||||
|
||||
### Running Models
|
||||
|
||||
```bash
|
||||
# Run interactive chat with a model
|
||||
docker exec -it ollama-server ollama run llama2
|
||||
|
||||
# Run with specific prompt
|
||||
docker exec ollama-server ollama run llama2 "Explain quantum computing"
|
||||
```
|
||||
|
||||
### API Usage
|
||||
|
||||
Ollama provides a REST API accessible at `http://localhost:11434`. Here are some examples:
|
||||
|
||||
#### Generate Text
|
||||
|
||||
```bash
|
||||
curl http://localhost:11434/api/generate -d '{
|
||||
"model": "llama2",
|
||||
"prompt": "Why is the sky blue?",
|
||||
"stream": false
|
||||
}'
|
||||
```
|
||||
|
||||
#### Chat API
|
||||
|
||||
```bash
|
||||
curl http://localhost:11434/api/chat -d '{
|
||||
"model": "llama2",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Why is the sky blue?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}'
|
||||
```
|
||||
|
||||
#### List Models via API
|
||||
|
||||
```bash
|
||||
curl http://localhost:11434/api/tags
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `OLLAMA_HOST`: Host binding (default: `0.0.0.0`)
|
||||
- `OLLAMA_PORT`: Port number (default: `11434`)
|
||||
|
||||
### Volumes
|
||||
|
||||
- `./models:/models`: Local directory for storing model files
|
||||
- `ollama_data:/usr/share/ollama/.ollama`: Persistent storage for Ollama data and models
|
||||
|
||||
### GPU Support
|
||||
|
||||
To enable GPU acceleration (NVIDIA only), uncomment the GPU configuration in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
```
|
||||
|
||||
**Prerequisites for GPU support:**
|
||||
- NVIDIA GPU with CUDA support
|
||||
- NVIDIA Container Toolkit installed
|
||||
- Docker configured for GPU access
|
||||
|
||||
## Model Management
|
||||
|
||||
### Popular Models
|
||||
|
||||
| Model | Size | Use Case | Pull Command |
|
||||
|-------|------|----------|--------------|
|
||||
| Llama 2 | 7B | General purpose | `ollama pull llama2` |
|
||||
| Llama 2 | 13B | Better quality | `ollama pull llama2:13b` |
|
||||
| Code Llama | 7B | Code generation | `ollama pull codellama` |
|
||||
| Mistral | 7B | Fast inference | `ollama pull mistral` |
|
||||
| Phi-2 | 2.7B | Lightweight | `ollama pull phi` |
|
||||
|
||||
### Custom Models
|
||||
|
||||
You can also create custom models using a Modelfile:
|
||||
|
||||
1. Create a `Modelfile`:
|
||||
```
|
||||
FROM llama2
|
||||
PARAMETER temperature 0.8
|
||||
PARAMETER top_p 0.9
|
||||
```
|
||||
|
||||
2. Create the model:
|
||||
```bash
|
||||
docker exec ollama-server ollama create mymodel -f Modelfile
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Service won't start:**
|
||||
- Check Docker logs: `docker-compose logs ollama`
|
||||
- Ensure port 11434 is available
|
||||
- Verify sufficient system resources
|
||||
|
||||
2. **Models fail to download:**
|
||||
- Check internet connectivity
|
||||
- Ensure sufficient disk space
|
||||
- Try smaller models first
|
||||
|
||||
3. **Performance issues:**
|
||||
- Consider enabling GPU support
|
||||
- Increase Docker memory limits
|
||||
- Use smaller models for testing
|
||||
|
||||
### Health Checks
|
||||
|
||||
The container includes a health check that verifies the API is responding:
|
||||
|
||||
```bash
|
||||
# Check container health
|
||||
docker inspect ollama-server --format='{{.State.Health.Status}}'
|
||||
```
|
||||
|
||||
### Resource Usage
|
||||
|
||||
Monitor resource usage:
|
||||
|
||||
```bash
|
||||
# Container stats
|
||||
docker stats ollama-server
|
||||
|
||||
# Disk usage
|
||||
docker exec ollama-server du -sh /usr/share/ollama/.ollama
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Python Integration
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
def chat_with_ollama(message, model="llama2"):
|
||||
url = "http://localhost:11434/api/chat"
|
||||
data = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": message}],
|
||||
"stream": False
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data)
|
||||
return response.json()["message"]["content"]
|
||||
|
||||
# Example usage
|
||||
response = chat_with_ollama("Explain machine learning")
|
||||
print(response)
|
||||
```
|
||||
|
||||
### JavaScript Integration
|
||||
|
||||
```javascript
|
||||
async function chatWithOllama(message, model = "llama2") {
|
||||
const response = await fetch("http://localhost:11434/api/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: [{ role: "user", content: message }],
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.message.content;
|
||||
}
|
||||
|
||||
// Example usage
|
||||
chatWithOllama("What is the capital of France?").then(console.log);
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Ollama runs as a non-root user inside the container
|
||||
- API is bound to all interfaces (0.0.0.0) for container access
|
||||
- Consider using a reverse proxy (nginx, traefik) for production deployments
|
||||
- Implement authentication/authorization for production use
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memory Management
|
||||
|
||||
- Allocate sufficient RAM based on model size:
|
||||
- 7B models: 8GB+ RAM
|
||||
- 13B models: 16GB+ RAM
|
||||
- 70B models: 64GB+ RAM
|
||||
|
||||
### GPU Acceleration
|
||||
|
||||
- NVIDIA GPUs significantly improve inference speed
|
||||
- Ensure GPU memory is sufficient for the model
|
||||
- Use mixed precision for better GPU utilization
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Updates
|
||||
|
||||
Update Ollama to the latest version:
|
||||
|
||||
```bash
|
||||
# Rebuild with latest Ollama
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
|
||||
Remove unused models and data:
|
||||
|
||||
```bash
|
||||
# Remove specific model
|
||||
docker exec ollama-server ollama rm model_name
|
||||
|
||||
# Clean up unused Docker resources
|
||||
docker system prune -f
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [Ollama Official Website](https://ollama.com/)
|
||||
- [Ollama GitHub Repository](https://github.com/ollama/ollama)
|
||||
- [Available Models](https://ollama.com/library)
|
||||
- [API Documentation](https://github.com/ollama/ollama/blob/main/docs/api.md)
|
||||
|
||||
## Support
|
||||
|
||||
For issues specific to this Docker setup, check the container logs and ensure all prerequisites are met. For Ollama-specific issues, refer to the official Ollama documentation and GitHub issues.
|
||||
@@ -0,0 +1,60 @@
|
||||
services:
|
||||
ollama:
|
||||
image: docker.io/ollama/ollama:latest
|
||||
# network_mode: host
|
||||
ports:
|
||||
- "11434:11434" # Map host port 11434 to container port 11434
|
||||
volumes:
|
||||
- ./models:/models # Mount a local directory for models
|
||||
- ollama_data:/root/.ollama # Persistent storage for Ollama data
|
||||
container_name: ollama-server
|
||||
environment:
|
||||
- OLLAMA_HOST=0.0.0.0
|
||||
restart: unless-stopped
|
||||
# Uncomment the following lines if you have NVIDIA GPU support
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: cdi
|
||||
capabilities: [gpu]
|
||||
device_ids:
|
||||
- nvidia.com/gpu=all
|
||||
|
||||
# postgres:
|
||||
# image: docker.io/pgvector/pgvector:pg17
|
||||
# container_name: ollama-postgres
|
||||
# # network_mode: host
|
||||
# ports:
|
||||
# - "54320:5432" # Map host port 54320 to container port 5432
|
||||
# environment:
|
||||
# - POSTGRES_PASSWORD=postgres
|
||||
# - POSTGRES_PORT=54320
|
||||
# volumes:
|
||||
# - ./postgres-data:/var/lib/postgresql/data
|
||||
# restart: unless-stopped
|
||||
|
||||
# speaches:
|
||||
# image: ghcr.io/speaches-ai/speaches:latest-cpu
|
||||
# container_name: speaches-server
|
||||
# # network_mode: host
|
||||
# ports:
|
||||
# - "8900:8000" # Map host port 8900 to container port 8000
|
||||
# volumes:
|
||||
# - hf-hub-cache:/home/ubuntu/.cache/huggingface/hub
|
||||
# environment:
|
||||
# - PORT=8900
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# # WARN: requires Docker Compose 2.24.2
|
||||
# # https://docs.docker.com/reference/compose-file/merge/#replace-value
|
||||
# devices: !override
|
||||
# - capabilities: ["gpu"]
|
||||
# driver: cdi
|
||||
# device_ids:
|
||||
# - nvidia.com/gpu=all
|
||||
|
||||
volumes:
|
||||
ollama_data:
|
||||
hf-hub-cache:
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "7.13.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# include:
|
||||
# - compose.observability.yaml
|
||||
services:
|
||||
speaches:
|
||||
extends:
|
||||
file: compose.yaml
|
||||
service: speaches
|
||||
image: ghcr.io/speaches-ai/speaches:latest-cpu
|
||||
build:
|
||||
args:
|
||||
BASE_IMAGE: ubuntu:24.04
|
||||
volumes:
|
||||
- hf-hub-cache:/home/ubuntu/.cache/huggingface/hub
|
||||
volumes:
|
||||
hf-hub-cache:
|
||||
@@ -0,0 +1,24 @@
|
||||
# include:
|
||||
# - compose.observability.yaml
|
||||
# This file is for those who have the CDI Docker feature enabled
|
||||
# https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html
|
||||
# https://docs.docker.com/reference/cli/dockerd/#enable-cdi-devices
|
||||
services:
|
||||
speaches:
|
||||
extends:
|
||||
file: compose.cuda.yaml
|
||||
service: speaches
|
||||
volumes:
|
||||
- hf-hub-cache:/home/ubuntu/.cache/huggingface/hub
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
# WARN: requires Docker Compose 2.24.2
|
||||
# https://docs.docker.com/reference/compose-file/merge/#replace-value
|
||||
devices: !override
|
||||
- capabilities: ["gpu"]
|
||||
driver: cdi
|
||||
device_ids:
|
||||
- nvidia.com/gpu=all
|
||||
volumes:
|
||||
hf-hub-cache:
|
||||
@@ -0,0 +1,23 @@
|
||||
# include:
|
||||
# - compose.observability.yaml
|
||||
services:
|
||||
speaches:
|
||||
extends:
|
||||
file: compose.yaml
|
||||
service: speaches
|
||||
# NOTE: slightly older cuda version is available under 'latest-cuda-12.4.1' and `latest-cuda-12.6.3` tags
|
||||
image: ghcr.io/speaches-ai/speaches:latest-cuda
|
||||
build:
|
||||
args:
|
||||
BASE_IMAGE: nvidia/cuda:12.9.0-cudnn-runtime-ubuntu24.04
|
||||
volumes:
|
||||
- hf-hub-cache:/home/ubuntu/.cache/huggingface/hub
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
volumes:
|
||||
hf-hub-cache:
|
||||
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
speaches:
|
||||
container_name: speaches
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8000:8000
|
||||
develop:
|
||||
watch:
|
||||
- action: rebuild
|
||||
path: ./uv.lock
|
||||
- action: sync+restart
|
||||
path: ./src
|
||||
target: /home/ubuntu/speaches/src
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "--fail", "http://0.0.0.0:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
@@ -0,0 +1,10 @@
|
||||
[project]
|
||||
name = "whisper"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"faster-whisper>=1.1.1",
|
||||
"whisperlivekit>=0.1.9",
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
vllm:
|
||||
image: docker.io/vllm/vllm-openai:latest
|
||||
command:
|
||||
- --model
|
||||
# - Qwen/Qwen3-4B-Instruct-2507
|
||||
- Qwen/Qwen3-VL-2B-Instruct
|
||||
- --device
|
||||
- cpu
|
||||
- --max-model-len
|
||||
- "2048"
|
||||
ports:
|
||||
- "9000:8000"
|
||||
environment:
|
||||
HF_TOKEN: ${HF_TOKEN}
|
||||
VLLM_USE_V1: "0"
|
||||
volumes:
|
||||
- ${HOME}/.cache/huggingface:/root/.cache/huggingface
|
||||
ipc: host
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
# WARN: requires Docker Compose 2.24.2
|
||||
# https://docs.docker.com/reference/compose-file/merge/#replace-value
|
||||
devices: !override
|
||||
- capabilities: ["gpu"]
|
||||
driver: cdi
|
||||
device_ids:
|
||||
- nvidia.com/gpu=all
|
||||
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
vllm:
|
||||
image: docker.io/vllm/vllm-openai:cu130-nightly
|
||||
command:
|
||||
- --model
|
||||
# - Qwen/Qwen3-4B-Instruct-2507
|
||||
- Qwen/Qwen3-VL-2B-Instruct
|
||||
- --gpu-memory-utilization
|
||||
- "0.75"
|
||||
- --max-model-len
|
||||
- "2048"
|
||||
ports:
|
||||
- "9000:8000"
|
||||
environment:
|
||||
HF_TOKEN: ${HF_TOKEN}
|
||||
volumes:
|
||||
- ${HOME}/.cache/huggingface:/root/.cache/huggingface
|
||||
ipc: host
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
# WARN: requires Docker Compose 2.24.2
|
||||
# https://docs.docker.com/reference/compose-file/merge/#replace-value
|
||||
devices: !override
|
||||
- capabilities: ["gpu"]
|
||||
driver: cdi
|
||||
device_ids:
|
||||
- nvidia.com/gpu=all
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start Llama.cpp service using Podman with GPU support
|
||||
podman run -d --name llamacpp-server -p 8000:8000 --gpus all -v $HOME/models:/models localhost/local/llama.cpp:full-cuda -s -m /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf --port 8000 --host 0.0.0.0 -n 512 --n-gpu-layers 1
|
||||
Generated
+1397
-795
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"storage_version": 1,
|
||||
"cookie_secret": "725ad7c6530eb9ef89bfbe3ef679e8cd53259f1c2ccfdc5367854a2abcbebe9916fdfc99db338cb138e40a0ca912f0ff092d24b7e2735625e1757dc068f72cf1",
|
||||
"last_update_check": null,
|
||||
"remote_version": null
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"storage_version": 1,
|
||||
"name": "test",
|
||||
"friendly_name": "test",
|
||||
"comment": null,
|
||||
"esphome_version": null,
|
||||
"src_version": 1,
|
||||
"address": "test.local",
|
||||
"web_port": null,
|
||||
"esp_platform": "ESP32",
|
||||
"build_path": "None",
|
||||
"firmware_bin_path": "None",
|
||||
"loaded_integrations": [],
|
||||
"loaded_platforms": [],
|
||||
"no_mdns": false,
|
||||
"framework": null,
|
||||
"core_platform": "esp32"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Your Wi-Fi SSID and password
|
||||
wifi_ssid: "radarmesh"
|
||||
wifi_password: "test"
|
||||
@@ -0,0 +1,32 @@
|
||||
esphome:
|
||||
name: test
|
||||
friendly_name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
|
||||
# Enable Home Assistant API
|
||||
api:
|
||||
encryption:
|
||||
key: "ZVBRfuXPf62uHZ5yYJraTdYeSZzx0ZI713KV2MoOXms="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "57bb187c9267e8b33a0386351fab0a99"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "Test Fallback Hotspot"
|
||||
password: "rMGDOrRl6aui"
|
||||
|
||||
captive_portal:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
esphome:
|
||||
container_name: esphome
|
||||
image: ghcr.io/esphome/esphome
|
||||
volumes:
|
||||
- ./config:/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
restart: always
|
||||
privileged: true
|
||||
network_mode: host
|
||||
environment:
|
||||
- USERNAME=test
|
||||
- PASSWORD=ChangeMe
|
||||
@@ -2,7 +2,7 @@ import { Box, Flex, IconButton, Text } from "@chakra-ui/react"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { useState } from "react"
|
||||
import { FaBars } from "react-icons/fa"
|
||||
import { FiLogOut } from "react-icons/fi"
|
||||
import { FiChevronLeft, FiChevronRight, FiLogOut } from "react-icons/fi"
|
||||
|
||||
import type { UserPublic } from "@/client"
|
||||
import useAuth from "@/hooks/useAuth"
|
||||
@@ -21,6 +21,7 @@ const Sidebar = () => {
|
||||
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
|
||||
const { logout } = useAuth()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -82,12 +83,24 @@ const Sidebar = () => {
|
||||
position="sticky"
|
||||
bg="bg.subtle"
|
||||
top={0}
|
||||
minW="xs"
|
||||
minW={collapsed ? "72px" : "xs"}
|
||||
w={collapsed ? "72px" : "xs"}
|
||||
h="100vh"
|
||||
p={4}
|
||||
transition="width 0.2s ease, min-width 0.2s ease"
|
||||
>
|
||||
<Box w="100%">
|
||||
<SidebarItems />
|
||||
<Flex justify={collapsed ? "center" : "flex-end"} mb={2}>
|
||||
<IconButton
|
||||
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setCollapsed((value) => !value)}
|
||||
>
|
||||
{collapsed ? <FiChevronRight /> : <FiChevronLeft />}
|
||||
</IconButton>
|
||||
</Flex>
|
||||
<SidebarItems collapsed={collapsed} />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -16,6 +16,7 @@ const items = [
|
||||
|
||||
interface SidebarItemsProps {
|
||||
onClose?: () => void
|
||||
collapsed?: boolean
|
||||
}
|
||||
|
||||
interface Item {
|
||||
@@ -24,7 +25,7 @@ interface Item {
|
||||
path: string
|
||||
}
|
||||
|
||||
const SidebarItems = ({ onClose }: SidebarItemsProps) => {
|
||||
const SidebarItems = ({ onClose, collapsed = false }: SidebarItemsProps) => {
|
||||
const queryClient = useQueryClient()
|
||||
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
|
||||
|
||||
@@ -33,28 +34,32 @@ const SidebarItems = ({ onClose }: SidebarItemsProps) => {
|
||||
: items
|
||||
|
||||
const listItems = finalItems.map(({ icon, title, path }) => (
|
||||
<RouterLink key={title} to={path} onClick={onClose}>
|
||||
<RouterLink key={title} to={path} onClick={onClose} title={title}>
|
||||
<Flex
|
||||
gap={4}
|
||||
px={4}
|
||||
gap={collapsed ? 0 : 4}
|
||||
px={collapsed ? 2 : 4}
|
||||
py={2}
|
||||
_hover={{
|
||||
background: "gray.subtle",
|
||||
}}
|
||||
alignItems="center"
|
||||
justifyContent={collapsed ? "center" : "flex-start"}
|
||||
fontSize="sm"
|
||||
borderRadius="md"
|
||||
>
|
||||
<Icon as={icon} alignSelf="center" />
|
||||
<Text ml={2}>{title}</Text>
|
||||
{!collapsed && <Text ml={2}>{title}</Text>}
|
||||
</Flex>
|
||||
</RouterLink>
|
||||
))
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text fontSize="xs" px={4} py={2} fontWeight="bold">
|
||||
Menu
|
||||
</Text>
|
||||
{!collapsed && (
|
||||
<Text fontSize="xs" px={4} py={2} fontWeight="bold">
|
||||
Menu
|
||||
</Text>
|
||||
)}
|
||||
<Box>{listItems}</Box>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -7,8 +7,9 @@ export function MapInvalidator() {
|
||||
|
||||
useEffect(() => {
|
||||
// Invalidate size after mount and on window resize
|
||||
const timer = setTimeout(() => map.invalidateSize(), 100);
|
||||
const handleResize = () => map.invalidateSize();
|
||||
const invalidateWithoutPan = () => map.invalidateSize({ pan: false, animate: false });
|
||||
const timer = setTimeout(invalidateWithoutPan, 100);
|
||||
const handleResize = () => invalidateWithoutPan();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Spinner, Text, Box } from '@chakra-ui/react';
|
||||
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
|
||||
import { MapContainer, TileLayer, Marker, Popup, ScaleControl, ZoomControl } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import './leaflet-overrides.css';
|
||||
import { WeatherService } from '@/client/sdk.gen';
|
||||
import { getWeatherIcon } from './weatherUtils';
|
||||
import { MapInvalidator, MapCenter } from './MapComponents';
|
||||
import { MapInvalidator } from './MapComponents';
|
||||
|
||||
type ApiNestedResponse<P> = { data?: P };
|
||||
|
||||
@@ -23,9 +23,9 @@ interface WindDirectionPayload { stations?: any[]; readings?: WindReading[]; rea
|
||||
interface Forecast { area: string; forecast: string; }
|
||||
interface ValidPeriod { start: string; end: string; }
|
||||
interface TwoHourForecastItem { forecasts?: Forecast[]; valid_period: ValidPeriod; }
|
||||
interface TwoHourForecastPayload {
|
||||
items?: TwoHourForecastItem[];
|
||||
area_metadata?: Array<{ name: string; label_location: { latitude: number; longitude: number } }>;
|
||||
interface TwoHourForecastPayload {
|
||||
items?: TwoHourForecastItem[];
|
||||
area_metadata?: Array<{ name: string; label_location: { latitude: number; longitude: number } }>;
|
||||
}
|
||||
|
||||
interface ForecastWithLocation {
|
||||
@@ -35,51 +35,100 @@ interface ForecastWithLocation {
|
||||
}
|
||||
|
||||
interface WeatherMapProps {
|
||||
// Optional props to allow both standalone and integrated usage
|
||||
forecastData?: ApiNestedResponse<TwoHourForecastPayload>;
|
||||
tempData?: ApiNestedResponse<AirTemperaturePayload>;
|
||||
windData?: ApiNestedResponse<WindDirectionPayload>;
|
||||
}
|
||||
|
||||
// Singapore coordinates (centered on the island)
|
||||
const center: [number, number] = [1.3521, 103.8198];
|
||||
|
||||
const escapeHtml = (value: string) =>
|
||||
value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
const formatTime = (timestamp?: string) => {
|
||||
if (!timestamp) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
const date = new Date(timestamp);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
return date.toLocaleString('en-SG', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
});
|
||||
};
|
||||
|
||||
const makeTemperatureMarkerHtml = ({
|
||||
forecastIcon,
|
||||
temperature,
|
||||
cardinal,
|
||||
}: {
|
||||
forecastIcon: string;
|
||||
temperature: string;
|
||||
cardinal: string;
|
||||
}) => `
|
||||
<div class="weather-marker weather-marker--temp">
|
||||
<div class="weather-marker__icon">${forecastIcon}</div>
|
||||
<div class="weather-marker__temp">${temperature}</div>
|
||||
<div class="weather-marker__wind">${cardinal}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const makeForecastMarkerHtml = ({
|
||||
area,
|
||||
forecastIcon,
|
||||
}: {
|
||||
area: string;
|
||||
forecastIcon: string;
|
||||
}) => `
|
||||
<div class="weather-marker weather-marker--forecast">
|
||||
<div class="weather-marker__icon">${forecastIcon}</div>
|
||||
<div class="weather-marker__area">${escapeHtml(area)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const WeatherMap: React.FC<WeatherMapProps> = ({ forecastData, tempData, windData }) => {
|
||||
// If props are not provided, fetch the data
|
||||
const { data: fetchedTempData, isLoading: tempLoading, error: tempError } = useQuery<ApiNestedResponse<AirTemperaturePayload>>({
|
||||
queryKey: ['weather', 'air-temperature'],
|
||||
queryFn: () => WeatherService.getAirTemperature() as Promise<ApiNestedResponse<AirTemperaturePayload>>,
|
||||
refetchInterval: 1000 * 60 * 30,
|
||||
enabled: !tempData, // Only fetch if not provided as prop
|
||||
enabled: !tempData,
|
||||
});
|
||||
|
||||
const { data: fetchedWindData, isLoading: windLoading, error: windError } = useQuery<ApiNestedResponse<WindDirectionPayload>>({
|
||||
queryKey: ['weather', 'wind-direction'],
|
||||
queryFn: () => WeatherService.getWindDirection() as Promise<ApiNestedResponse<WindDirectionPayload>>,
|
||||
refetchInterval: 1000 * 60 * 30,
|
||||
enabled: !windData, // Only fetch if not provided as prop
|
||||
enabled: !windData,
|
||||
});
|
||||
|
||||
const { data: fetchedForecastData, isLoading: forecastLoading, error: forecastError } = useQuery<ApiNestedResponse<TwoHourForecastPayload>>({
|
||||
queryKey: ['weather', 'two-hour-forecast'],
|
||||
queryFn: () => WeatherService.getTwoHourForecast() as Promise<ApiNestedResponse<TwoHourForecastPayload>>,
|
||||
refetchInterval: 1000 * 60 * 30,
|
||||
enabled: !forecastData, // Only fetch if not provided as prop
|
||||
enabled: !forecastData,
|
||||
});
|
||||
|
||||
// Use provided data or fetched data
|
||||
const actualTempData = tempData || fetchedTempData;
|
||||
const actualWindData = windData || fetchedWindData;
|
||||
const actualForecastData = forecastData || fetchedForecastData;
|
||||
|
||||
// Loading states
|
||||
const isLoading = (!tempData && tempLoading) || (!windData && windLoading) || (!forecastData && forecastLoading);
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
|
||||
// Error handling
|
||||
const hasError = (!tempData && tempError) || (!windData && windError) || (!forecastData && forecastError) ||
|
||||
!actualTempData?.data || !actualWindData?.data || !actualForecastData?.data;
|
||||
const hasError = (!tempData && tempError) || (!windData && windError) || (!forecastData && forecastError) ||
|
||||
!actualTempData?.data || !actualWindData?.data || !actualForecastData?.data;
|
||||
if (hasError) {
|
||||
return <Text color="red.500">Error loading map data</Text>;
|
||||
}
|
||||
@@ -102,101 +151,102 @@ const WeatherMap: React.FC<WeatherMapProps> = ({ forecastData, tempData, windDat
|
||||
return <Text>No map data available</Text>;
|
||||
}
|
||||
|
||||
const latestReadingTime = formatTime(latestTempReading.timestamp);
|
||||
const tempStationMap = new Map(tempStations.map((st: AirTempStation) => [st.id, st]));
|
||||
const windDataMap = new Map(latestWindReading?.data?.map((rd: WindDataPoint) => [rd.stationId, rd.value]) || []);
|
||||
|
||||
const getCardinalDirection = (deg: number | null | undefined) => {
|
||||
if (deg === null || deg === undefined) return 'N/A';
|
||||
const dirs = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW'];
|
||||
const dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
|
||||
const idx = Math.round((deg % 360) / 22.5) % 16;
|
||||
return dirs[idx];
|
||||
};
|
||||
|
||||
// For forecast markers
|
||||
const forecastsWithLocation = forecasts
|
||||
.map(f => ({
|
||||
area: f.area,
|
||||
forecast: f.forecast,
|
||||
location: areaMetadata.find(a => a.name === f.area)?.label_location
|
||||
.map(f => ({
|
||||
area: f.area,
|
||||
forecast: f.forecast,
|
||||
location: areaMetadata.find(a => a.name === f.area)?.label_location,
|
||||
}))
|
||||
.filter(f => f.location) as ForecastWithLocation[];
|
||||
|
||||
return (
|
||||
<MapContainer center={center} zoom={11} style={{ height: '500px', width: '100%' }}>
|
||||
<TileLayer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
attribution="© OpenStreetMap contributors"
|
||||
/>
|
||||
<MapCenter center={center} />
|
||||
<MapInvalidator />
|
||||
|
||||
{/* Temperature markers */}
|
||||
{latestTempReading.data.map((reading: AirTempDataPoint) => {
|
||||
const station = tempStationMap.get(reading.stationId);
|
||||
if (!station || reading.value === null) return null;
|
||||
const windDeg = windDataMap.get(reading.stationId) as number | null | undefined;
|
||||
const cardinal = getCardinalDirection(windDeg);
|
||||
return (
|
||||
<Marker
|
||||
key={reading.stationId}
|
||||
position={[station.location.latitude, station.location.longitude]}
|
||||
icon={L.divIcon({
|
||||
html: `
|
||||
<div style="background: rgba(255,255,255,0.95); border:2px solid #4A90E2; border-radius:8px; padding:4px 6px; font-size:11px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.2); min-width:60px; transform:translate(-50%,-100%);">
|
||||
<div style="font-size:14px; margin-bottom:2px;">
|
||||
${getWeatherIcon(forecastsWithLocation.find(f => f.area === station.name)?.forecast || 'Fair')}
|
||||
</div>
|
||||
<div style="color:#E53E3E; font-size:12px;">${reading.value.toFixed(1)}°C</div>
|
||||
<div style="color:#2D3748; font-size:10px;">${cardinal}</div>
|
||||
</div>
|
||||
`,
|
||||
className: 'custom-weather-marker',
|
||||
iconSize: [60,50],
|
||||
iconAnchor: [30,50]
|
||||
})}
|
||||
>
|
||||
<Popup>
|
||||
<Box p={1}>
|
||||
<Box fontWeight="bold">{station.name}</Box>
|
||||
<Box fontSize="sm">Temperature: {reading.value.toFixed(1)}°C</Box>
|
||||
<Box fontSize="sm">Wind Direction: {cardinal}</Box>
|
||||
</Box>
|
||||
</Popup>
|
||||
</Marker>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Add forecast markers for areas without temperature stations */}
|
||||
{forecastsWithLocation
|
||||
.filter(f => !tempStations.some((s: AirTempStation) => s.name === f.area))
|
||||
.map(f => (
|
||||
<Marker
|
||||
key={f.area}
|
||||
position={[f.location.latitude, f.location.longitude]}
|
||||
icon={L.divIcon({
|
||||
html: `
|
||||
<div style="background: rgba(255,255,255,0.95); border:2px solid #4A90E2; border-radius:8px; padding:4px 6px; font-size:11px; font-weight:bold; text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.2); min-width:60px; transform:translate(-50%,-100%);">
|
||||
<div style="font-size:14px; margin-bottom:2px;">
|
||||
${getWeatherIcon(f.forecast)}
|
||||
</div>
|
||||
<div style="color:#2D3748; font-size:12px;">${f.area}</div>
|
||||
</div>
|
||||
`,
|
||||
className: 'custom-weather-marker',
|
||||
iconSize: [60,50],
|
||||
iconAnchor: [30,50]
|
||||
})}
|
||||
>
|
||||
<Popup>
|
||||
<Box p={1}>
|
||||
<Box fontWeight="bold">{f.area}</Box>
|
||||
<Box fontSize="sm">Forecast: {f.forecast}</Box>
|
||||
</Box>
|
||||
</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
</MapContainer>
|
||||
<Box className="weather-map-shell">
|
||||
<Box className="weather-map-banner">
|
||||
<Text className="weather-map-title">Singapore Live Weather Map</Text>
|
||||
<Text className="weather-map-meta">
|
||||
Updated {latestReadingTime} | {tempStations.length} stations
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<MapContainer center={center} zoom={11} className="weather-map-container" zoomControl={false}>
|
||||
<TileLayer
|
||||
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
|
||||
attribution="© OpenStreetMap contributors © CARTO"
|
||||
/>
|
||||
<MapInvalidator />
|
||||
<ZoomControl position="bottomright" />
|
||||
<ScaleControl position="bottomleft" imperial={false} />
|
||||
|
||||
{latestTempReading.data.map((reading: AirTempDataPoint) => {
|
||||
const station = tempStationMap.get(reading.stationId);
|
||||
if (!station || reading.value === null) return null;
|
||||
const windDeg = windDataMap.get(reading.stationId) as number | null | undefined;
|
||||
const cardinal = getCardinalDirection(windDeg);
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={reading.stationId}
|
||||
position={[station.location.latitude, station.location.longitude]}
|
||||
icon={L.divIcon({
|
||||
html: makeTemperatureMarkerHtml({
|
||||
forecastIcon: getWeatherIcon(forecastsWithLocation.find(f => f.area === station.name)?.forecast || 'Fair'),
|
||||
temperature: `${reading.value.toFixed(1)}°C`,
|
||||
cardinal,
|
||||
}),
|
||||
className: 'custom-weather-marker',
|
||||
iconSize: [78, 72],
|
||||
iconAnchor: [39, 72],
|
||||
})}
|
||||
>
|
||||
<Popup>
|
||||
<Box p={1}>
|
||||
<Box fontWeight="bold">{station.name}</Box>
|
||||
<Box fontSize="sm">Temperature: {reading.value.toFixed(1)}°C</Box>
|
||||
<Box fontSize="sm">Wind Direction: {cardinal}</Box>
|
||||
</Box>
|
||||
</Popup>
|
||||
</Marker>
|
||||
);
|
||||
})}
|
||||
|
||||
{forecastsWithLocation
|
||||
.filter(f => !tempStations.some((s: AirTempStation) => s.name === f.area))
|
||||
.map(f => (
|
||||
<Marker
|
||||
key={f.area}
|
||||
position={[f.location.latitude, f.location.longitude]}
|
||||
icon={L.divIcon({
|
||||
html: makeForecastMarkerHtml({
|
||||
area: f.area,
|
||||
forecastIcon: getWeatherIcon(f.forecast),
|
||||
}),
|
||||
className: 'custom-weather-marker',
|
||||
iconSize: [92, 62],
|
||||
iconAnchor: [46, 62],
|
||||
})}
|
||||
>
|
||||
<Popup>
|
||||
<Box p={1}>
|
||||
<Box fontWeight="bold">{f.area}</Box>
|
||||
<Box fontSize="sm">Forecast: {f.forecast}</Box>
|
||||
</Box>
|
||||
</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
</MapContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeatherMap;
|
||||
export default WeatherMap;
|
||||
@@ -4,6 +4,47 @@
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.weather-map-shell {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(48, 72, 94, 0.18);
|
||||
background:
|
||||
radial-gradient(circle at 5% 10%, rgba(153, 205, 255, 0.18), transparent 36%),
|
||||
radial-gradient(circle at 95% 90%, rgba(255, 191, 133, 0.16), transparent 42%),
|
||||
linear-gradient(160deg, #f8fbff 0%, #f4f8ff 48%, #fdf7ef 100%);
|
||||
box-shadow: 0 16px 34px rgba(15, 37, 58, 0.14);
|
||||
}
|
||||
|
||||
.weather-map-banner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(48, 72, 94, 0.16);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.weather-map-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: #1f2f45;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.weather-map-meta {
|
||||
font-size: 0.76rem;
|
||||
font-weight: 600;
|
||||
color: #38516d;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.weather-map-container {
|
||||
height: min(58vh, 560px);
|
||||
min-height: 360px;
|
||||
width: 100%;
|
||||
}
|
||||
/* Remove default leaflet icon shadows */
|
||||
.leaflet-shadow-pane {
|
||||
display: none;
|
||||
@@ -11,12 +52,14 @@
|
||||
|
||||
/* Style popup better */
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
box-shadow: 0 8px 18px rgba(16, 29, 43, 0.22);
|
||||
border: 1px solid rgba(35, 63, 91, 0.18);
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 8px;
|
||||
margin: 10px 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -29,3 +72,76 @@
|
||||
.leaflet-container {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.weather-marker {
|
||||
border-radius: 12px;
|
||||
padding: 6px 8px;
|
||||
min-width: 64px;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid rgba(45, 89, 132, 0.34);
|
||||
box-shadow: 0 7px 15px rgba(16, 36, 58, 0.22);
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
|
||||
.weather-marker--temp {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.97), rgba(245, 250, 255, 0.94));
|
||||
}
|
||||
|
||||
.weather-marker--forecast {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.97), rgba(255, 248, 237, 0.95));
|
||||
}
|
||||
|
||||
.weather-marker__icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.weather-marker__temp {
|
||||
color: #be2d2d;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.weather-marker__wind {
|
||||
color: #2b445d;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.weather-marker__area {
|
||||
color: #253c53;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
max-width: 92px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.weather-map-banner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.weather-map-container {
|
||||
height: 54vh;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.weather-marker {
|
||||
min-width: 58px;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
|
||||
.weather-marker__icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions"
|
||||
import type { ApiResult } from "./ApiResult"
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly url: string
|
||||
public readonly status: number
|
||||
public readonly statusText: string
|
||||
public readonly body: unknown
|
||||
public readonly request: ApiRequestOptions
|
||||
|
||||
constructor(
|
||||
request: ApiRequestOptions,
|
||||
response: ApiResult,
|
||||
message: string,
|
||||
) {
|
||||
super(message)
|
||||
|
||||
this.name = "ApiError"
|
||||
this.url = response.url
|
||||
this.status = response.status
|
||||
this.statusText = response.statusText
|
||||
this.body = response.body
|
||||
this.request = request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export type ApiRequestOptions<T = unknown> = {
|
||||
readonly body?: any
|
||||
readonly cookies?: Record<string, unknown>
|
||||
readonly errors?: Record<number | string, string>
|
||||
readonly formData?: Record<string, unknown> | any[] | Blob | File
|
||||
readonly headers?: Record<string, unknown>
|
||||
readonly mediaType?: string
|
||||
readonly method:
|
||||
| "DELETE"
|
||||
| "GET"
|
||||
| "HEAD"
|
||||
| "OPTIONS"
|
||||
| "PATCH"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
readonly path?: Record<string, unknown>
|
||||
readonly query?: Record<string, unknown>
|
||||
readonly responseHeader?: string
|
||||
readonly responseTransformer?: (data: unknown) => Promise<T>
|
||||
readonly url: string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export type ApiResult<TData = any> = {
|
||||
readonly body: TData
|
||||
readonly ok: boolean
|
||||
readonly status: number
|
||||
readonly statusText: string
|
||||
readonly url: string
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
export class CancelError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = "CancelError"
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export interface OnCancel {
|
||||
readonly isResolved: boolean
|
||||
readonly isRejected: boolean
|
||||
readonly isCancelled: boolean
|
||||
|
||||
(cancelHandler: () => void): void
|
||||
}
|
||||
|
||||
export class CancelablePromise<T> implements Promise<T> {
|
||||
private _isResolved: boolean
|
||||
private _isRejected: boolean
|
||||
private _isCancelled: boolean
|
||||
readonly cancelHandlers: (() => void)[]
|
||||
readonly promise: Promise<T>
|
||||
private _resolve?: (value: T | PromiseLike<T>) => void
|
||||
private _reject?: (reason?: unknown) => void
|
||||
|
||||
constructor(
|
||||
executor: (
|
||||
resolve: (value: T | PromiseLike<T>) => void,
|
||||
reject: (reason?: unknown) => void,
|
||||
onCancel: OnCancel,
|
||||
) => void,
|
||||
) {
|
||||
this._isResolved = false
|
||||
this._isRejected = false
|
||||
this._isCancelled = false
|
||||
this.cancelHandlers = []
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this._resolve = resolve
|
||||
this._reject = reject
|
||||
|
||||
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||
return
|
||||
}
|
||||
this._isResolved = true
|
||||
if (this._resolve) this._resolve(value)
|
||||
}
|
||||
|
||||
const onReject = (reason?: unknown): void => {
|
||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||
return
|
||||
}
|
||||
this._isRejected = true
|
||||
if (this._reject) this._reject(reason)
|
||||
}
|
||||
|
||||
const onCancel = (cancelHandler: () => void): void => {
|
||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||
return
|
||||
}
|
||||
this.cancelHandlers.push(cancelHandler)
|
||||
}
|
||||
|
||||
Object.defineProperty(onCancel, "isResolved", {
|
||||
get: (): boolean => this._isResolved,
|
||||
})
|
||||
|
||||
Object.defineProperty(onCancel, "isRejected", {
|
||||
get: (): boolean => this._isRejected,
|
||||
})
|
||||
|
||||
Object.defineProperty(onCancel, "isCancelled", {
|
||||
get: (): boolean => this._isCancelled,
|
||||
})
|
||||
|
||||
return executor(onResolve, onReject, onCancel as OnCancel)
|
||||
})
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return "Cancellable Promise"
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.promise.then(onFulfilled, onRejected)
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
|
||||
): Promise<T | TResult> {
|
||||
return this.promise.catch(onRejected)
|
||||
}
|
||||
|
||||
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||
return this.promise.finally(onFinally)
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||
return
|
||||
}
|
||||
this._isCancelled = true
|
||||
if (this.cancelHandlers.length) {
|
||||
try {
|
||||
for (const cancelHandler of this.cancelHandlers) {
|
||||
cancelHandler()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Cancellation threw an error", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
this.cancelHandlers.length = 0
|
||||
if (this._reject) this._reject(new CancelError("Request aborted"))
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return this._isCancelled
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { AxiosRequestConfig, AxiosResponse } from "axios"
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions"
|
||||
|
||||
type Headers = Record<string, string>
|
||||
type Middleware<T> = (value: T) => T | Promise<T>
|
||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>
|
||||
|
||||
export class Interceptors<T> {
|
||||
_fns: Middleware<T>[]
|
||||
|
||||
constructor() {
|
||||
this._fns = []
|
||||
}
|
||||
|
||||
eject(fn: Middleware<T>): void {
|
||||
const index = this._fns.indexOf(fn)
|
||||
if (index !== -1) {
|
||||
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)]
|
||||
}
|
||||
}
|
||||
|
||||
use(fn: Middleware<T>): void {
|
||||
this._fns = [...this._fns, fn]
|
||||
}
|
||||
}
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string
|
||||
CREDENTIALS: "include" | "omit" | "same-origin"
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined
|
||||
PASSWORD?: string | Resolver<string> | undefined
|
||||
TOKEN?: string | Resolver<string> | undefined
|
||||
USERNAME?: string | Resolver<string> | undefined
|
||||
VERSION: string
|
||||
WITH_CREDENTIALS: boolean
|
||||
interceptors: {
|
||||
request: Interceptors<AxiosRequestConfig>
|
||||
response: Interceptors<AxiosResponse>
|
||||
}
|
||||
}
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: "",
|
||||
CREDENTIALS: "include",
|
||||
ENCODE_PATH: undefined,
|
||||
HEADERS: undefined,
|
||||
PASSWORD: undefined,
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
VERSION: "0.1.0",
|
||||
WITH_CREDENTIALS: false,
|
||||
interceptors: {
|
||||
request: new Interceptors(),
|
||||
response: new Interceptors(),
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
import axios from "axios"
|
||||
import type {
|
||||
AxiosError,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosInstance,
|
||||
} from "axios"
|
||||
|
||||
import { ApiError } from "./ApiError"
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions"
|
||||
import type { ApiResult } from "./ApiResult"
|
||||
import { CancelablePromise } from "./CancelablePromise"
|
||||
import type { OnCancel } from "./CancelablePromise"
|
||||
import type { OpenAPIConfig } from "./OpenAPI"
|
||||
|
||||
export const isString = (value: unknown): value is string => {
|
||||
return typeof value === "string"
|
||||
}
|
||||
|
||||
export const isStringWithValue = (value: unknown): value is string => {
|
||||
return isString(value) && value !== ""
|
||||
}
|
||||
|
||||
export const isBlob = (value: any): value is Blob => {
|
||||
return value instanceof Blob
|
||||
}
|
||||
|
||||
export const isFormData = (value: unknown): value is FormData => {
|
||||
return value instanceof FormData
|
||||
}
|
||||
|
||||
export const isSuccess = (status: number): boolean => {
|
||||
return status >= 200 && status < 300
|
||||
}
|
||||
|
||||
export const base64 = (str: string): string => {
|
||||
try {
|
||||
return btoa(str)
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
return Buffer.from(str).toString("base64")
|
||||
}
|
||||
}
|
||||
|
||||
export const getQueryString = (params: Record<string, unknown>): string => {
|
||||
const qs: string[] = []
|
||||
|
||||
const append = (key: string, value: unknown) => {
|
||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
}
|
||||
|
||||
const encodePair = (key: string, value: unknown) => {
|
||||
if (value === undefined || value === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
append(key, value.toISOString())
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach((v) => encodePair(key, v))
|
||||
} else if (typeof value === "object") {
|
||||
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v))
|
||||
} else {
|
||||
append(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => encodePair(key, value))
|
||||
|
||||
return qs.length ? `?${qs.join("&")}` : ""
|
||||
}
|
||||
|
||||
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||
const encoder = config.ENCODE_PATH || encodeURI
|
||||
|
||||
const path = options.url
|
||||
.replace("{api-version}", config.VERSION)
|
||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||
if (options.path?.hasOwnProperty(group)) {
|
||||
return encoder(String(options.path[group]))
|
||||
}
|
||||
return substring
|
||||
})
|
||||
|
||||
const url = config.BASE + path
|
||||
return options.query ? url + getQueryString(options.query) : url
|
||||
}
|
||||
|
||||
export const getFormData = (
|
||||
options: ApiRequestOptions,
|
||||
): FormData | undefined => {
|
||||
if (options.formData) {
|
||||
const formData = new FormData()
|
||||
|
||||
const process = (key: string, value: unknown) => {
|
||||
if (isString(value) || isBlob(value)) {
|
||||
formData.append(key, value)
|
||||
} else {
|
||||
formData.append(key, JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
|
||||
Object.entries(options.formData)
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => process(key, v))
|
||||
} else {
|
||||
process(key, value)
|
||||
}
|
||||
})
|
||||
|
||||
return formData
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>
|
||||
|
||||
export const resolve = async <T>(
|
||||
options: ApiRequestOptions<T>,
|
||||
resolver?: T | Resolver<T>,
|
||||
): Promise<T | undefined> => {
|
||||
if (typeof resolver === "function") {
|
||||
return (resolver as Resolver<T>)(options)
|
||||
}
|
||||
return resolver
|
||||
}
|
||||
|
||||
export const getHeaders = async <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions<T>,
|
||||
): Promise<Record<string, string>> => {
|
||||
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||
// @ts-ignore
|
||||
resolve(options, config.TOKEN),
|
||||
// @ts-ignore
|
||||
resolve(options, config.USERNAME),
|
||||
// @ts-ignore
|
||||
resolve(options, config.PASSWORD),
|
||||
// @ts-ignore
|
||||
resolve(options, config.HEADERS),
|
||||
])
|
||||
|
||||
const headers = Object.entries({
|
||||
Accept: "application/json",
|
||||
...additionalHeaders,
|
||||
...options.headers,
|
||||
})
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.reduce(
|
||||
(headers, [key, value]) => ({
|
||||
...headers,
|
||||
[key]: String(value),
|
||||
}),
|
||||
{} as Record<string, string>,
|
||||
)
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers["Authorization"] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = base64(`${username}:${password}`)
|
||||
headers["Authorization"] = `Basic ${credentials}`
|
||||
}
|
||||
|
||||
if (options.body !== undefined) {
|
||||
if (options.mediaType) {
|
||||
headers["Content-Type"] = options.mediaType
|
||||
} else if (isBlob(options.body)) {
|
||||
headers["Content-Type"] = options.body.type || "application/octet-stream"
|
||||
} else if (isString(options.body)) {
|
||||
headers["Content-Type"] = "text/plain"
|
||||
} else if (!isFormData(options.body)) {
|
||||
headers["Content-Type"] = "application/json"
|
||||
}
|
||||
} else if (options.formData !== undefined) {
|
||||
if (options.mediaType) {
|
||||
headers["Content-Type"] = options.mediaType
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const getRequestBody = (options: ApiRequestOptions): unknown => {
|
||||
if (options.body) {
|
||||
return options.body
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const sendRequest = async <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions<T>,
|
||||
url: string,
|
||||
body: unknown,
|
||||
formData: FormData | undefined,
|
||||
headers: Record<string, string>,
|
||||
onCancel: OnCancel,
|
||||
axiosClient: AxiosInstance,
|
||||
): Promise<AxiosResponse<T>> => {
|
||||
const controller = new AbortController()
|
||||
|
||||
let requestConfig: AxiosRequestConfig = {
|
||||
data: body ?? formData,
|
||||
headers,
|
||||
method: options.method,
|
||||
signal: controller.signal,
|
||||
url,
|
||||
withCredentials: config.WITH_CREDENTIALS,
|
||||
}
|
||||
|
||||
onCancel(() => controller.abort())
|
||||
|
||||
for (const fn of config.interceptors.request._fns) {
|
||||
requestConfig = await fn(requestConfig)
|
||||
}
|
||||
|
||||
try {
|
||||
return await axiosClient.request(requestConfig)
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<T>
|
||||
if (axiosError.response) {
|
||||
return axiosError.response
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getResponseHeader = (
|
||||
response: AxiosResponse<unknown>,
|
||||
responseHeader?: string,
|
||||
): string | undefined => {
|
||||
if (responseHeader) {
|
||||
const content = response.headers[responseHeader]
|
||||
if (isString(content)) {
|
||||
return content
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const getResponseBody = (response: AxiosResponse<unknown>): unknown => {
|
||||
if (response.status !== 204) {
|
||||
return response.data
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const catchErrorCodes = (
|
||||
options: ApiRequestOptions,
|
||||
result: ApiResult,
|
||||
): void => {
|
||||
const errors: Record<number, string> = {
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Timeout",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Precondition Failed",
|
||||
413: "Payload Too Large",
|
||||
414: "URI Too Long",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Range Not Satisfiable",
|
||||
417: "Expectation Failed",
|
||||
418: "Im a teapot",
|
||||
421: "Misdirected Request",
|
||||
422: "Unprocessable Content",
|
||||
423: "Locked",
|
||||
424: "Failed Dependency",
|
||||
425: "Too Early",
|
||||
426: "Upgrade Required",
|
||||
428: "Precondition Required",
|
||||
429: "Too Many Requests",
|
||||
431: "Request Header Fields Too Large",
|
||||
451: "Unavailable For Legal Reasons",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
505: "HTTP Version Not Supported",
|
||||
506: "Variant Also Negotiates",
|
||||
507: "Insufficient Storage",
|
||||
508: "Loop Detected",
|
||||
510: "Not Extended",
|
||||
511: "Network Authentication Required",
|
||||
...options.errors,
|
||||
}
|
||||
|
||||
const error = errors[result.status]
|
||||
if (error) {
|
||||
throw new ApiError(options, result, error)
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
const errorStatus = result.status ?? "unknown"
|
||||
const errorStatusText = result.statusText ?? "unknown"
|
||||
const errorBody = (() => {
|
||||
try {
|
||||
return JSON.stringify(result.body, null, 2)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
|
||||
throw new ApiError(
|
||||
options,
|
||||
result,
|
||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request method
|
||||
* @param config The OpenAPI configuration object
|
||||
* @param options The request options from the service
|
||||
* @param axiosClient The axios client instance to use
|
||||
* @returns CancelablePromise<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const request = <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions<T>,
|
||||
axiosClient: AxiosInstance = axios,
|
||||
): CancelablePromise<T> => {
|
||||
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||
try {
|
||||
const url = getUrl(config, options)
|
||||
const formData = getFormData(options)
|
||||
const body = getRequestBody(options)
|
||||
const headers = await getHeaders(config, options)
|
||||
|
||||
if (!onCancel.isCancelled) {
|
||||
let response = await sendRequest<T>(
|
||||
config,
|
||||
options,
|
||||
url,
|
||||
body,
|
||||
formData,
|
||||
headers,
|
||||
onCancel,
|
||||
axiosClient,
|
||||
)
|
||||
|
||||
for (const fn of config.interceptors.response._fns) {
|
||||
response = await fn(response)
|
||||
}
|
||||
|
||||
const responseBody = getResponseBody(response)
|
||||
const responseHeader = getResponseHeader(
|
||||
response,
|
||||
options.responseHeader,
|
||||
)
|
||||
|
||||
let transformedBody = responseBody
|
||||
if (options.responseTransformer && isSuccess(response.status)) {
|
||||
transformedBody = await options.responseTransformer(responseBody)
|
||||
}
|
||||
|
||||
const result: ApiResult = {
|
||||
url,
|
||||
ok: isSuccess(response.status),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: responseHeader ?? transformedBody,
|
||||
}
|
||||
|
||||
catchErrorCodes(options, result)
|
||||
|
||||
resolve(result.body)
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Example usage of the generated Ollama FastAPI client
|
||||
*/
|
||||
import {
|
||||
OpenAPI,
|
||||
ModelsService,
|
||||
DefaultService,
|
||||
AutomaticSpeechRecognitionService,
|
||||
SpeechToTextService
|
||||
} from './index';
|
||||
|
||||
// Configure the base URL for your Ollama API
|
||||
OpenAPI.BASE = 'http://localhost:11434'; // Default Ollama port
|
||||
|
||||
// Example: List available models
|
||||
async function listModels() {
|
||||
try {
|
||||
const response = await ModelsService.listLocalModelsV1ModelsGet({});
|
||||
console.log('Available models:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error listing models:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Get a specific model
|
||||
async function getModel(modelId: string) {
|
||||
try {
|
||||
const response = await ModelsService.getLocalModelV1ModelsModelIdGet({
|
||||
modelId
|
||||
});
|
||||
console.log('Model details:', response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error getting model:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Chat completion
|
||||
async function chatCompletion() {
|
||||
try {
|
||||
const response = await DefaultService.handleCompletionsV1ChatCompletionsPost({
|
||||
requestBody: {
|
||||
model: 'llama2', // Replace with your model
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello, how are you?'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
console.log('Chat response:', response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error in chat completion:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Transcribe audio
|
||||
async function transcribeAudio(audioFile: File) {
|
||||
try {
|
||||
const response = await AutomaticSpeechRecognitionService.transcribeFileV1AudioTranscriptionsPost({
|
||||
formData: {
|
||||
file: audioFile,
|
||||
model: 'whisper-1'
|
||||
}
|
||||
});
|
||||
console.log('Transcription:', response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error transcribing audio:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Text-to-speech synthesis
|
||||
async function synthesizeSpeech(text: string, voice: string = 'default') {
|
||||
try {
|
||||
const response = await SpeechToTextService.synthesizeV1AudioSpeechPost({
|
||||
requestBody: {
|
||||
model: 'tts-1',
|
||||
input: text,
|
||||
voice: voice
|
||||
}
|
||||
});
|
||||
console.log('Speech synthesis response:', response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error synthesizing speech:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Export the functions for use in your application
|
||||
export {
|
||||
listModels,
|
||||
getModel,
|
||||
chatCompletion,
|
||||
transcribeAudio,
|
||||
synthesizeSpeech
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
export { ApiError } from "./core/ApiError"
|
||||
export { CancelablePromise, CancelError } from "./core/CancelablePromise"
|
||||
export { OpenAPI, type OpenAPIConfig } from "./core/OpenAPI"
|
||||
export * from "./sdk.gen"
|
||||
export * from "./types.gen"
|
||||
@@ -0,0 +1,356 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { CancelablePromise } from "./core/CancelablePromise"
|
||||
import { OpenAPI } from "./core/OpenAPI"
|
||||
import { request as __request } from "./core/request"
|
||||
import type {
|
||||
TranslateFileV1AudioTranslationsPostData,
|
||||
TranslateFileV1AudioTranslationsPostResponse,
|
||||
TranscribeFileV1AudioTranscriptionsPostData,
|
||||
TranscribeFileV1AudioTranscriptionsPostResponse,
|
||||
HandleCompletionsV1ChatCompletionsPostData,
|
||||
HandleCompletionsV1ChatCompletionsPostResponse,
|
||||
HealthHealthGetResponse,
|
||||
GetRunningModelsApiPsGetResponse,
|
||||
LoadModelRouteApiPsModelIdPostData,
|
||||
LoadModelRouteApiPsModelIdPostResponse,
|
||||
StopRunningModelApiPsModelIdDeleteData,
|
||||
StopRunningModelApiPsModelIdDeleteResponse,
|
||||
ListLocalModelsV1ModelsGetData,
|
||||
ListLocalModelsV1ModelsGetResponse,
|
||||
GetLocalModelV1ModelsModelIdGetData,
|
||||
GetLocalModelV1ModelsModelIdGetResponse,
|
||||
DownloadRemoteModelV1ModelsModelIdPostData,
|
||||
DownloadRemoteModelV1ModelsModelIdPostResponse,
|
||||
DeleteModelV1ModelsModelIdDeleteData,
|
||||
DeleteModelV1ModelsModelIdDeleteResponse,
|
||||
GetRemoteModelsV1RegistryGetData,
|
||||
GetRemoteModelsV1RegistryGetResponse,
|
||||
RealtimeWebrtcV1RealtimePostData,
|
||||
RealtimeWebrtcV1RealtimePostResponse,
|
||||
SynthesizeV1AudioSpeechPostData,
|
||||
SynthesizeV1AudioSpeechPostResponse,
|
||||
DetectSpeechTimestampsV1AudioSpeechTimestampsPostData,
|
||||
DetectSpeechTimestampsV1AudioSpeechTimestampsPostResponse,
|
||||
} from "./types.gen"
|
||||
|
||||
export class AutomaticSpeechRecognitionService {
|
||||
/**
|
||||
* Translate File
|
||||
* @param data The data for the request.
|
||||
* @param data.formData
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static translateFileV1AudioTranslationsPost(
|
||||
data: TranslateFileV1AudioTranslationsPostData,
|
||||
): CancelablePromise<TranslateFileV1AudioTranslationsPostResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "POST",
|
||||
url: "/v1/audio/translations",
|
||||
formData: data.formData,
|
||||
mediaType: "application/x-www-form-urlencoded",
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transcribe File
|
||||
* @param data The data for the request.
|
||||
* @param data.formData
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static transcribeFileV1AudioTranscriptionsPost(
|
||||
data: TranscribeFileV1AudioTranscriptionsPostData,
|
||||
): CancelablePromise<TranscribeFileV1AudioTranscriptionsPostResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "POST",
|
||||
url: "/v1/audio/transcriptions",
|
||||
formData: data.formData,
|
||||
mediaType: "application/x-www-form-urlencoded",
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultService {
|
||||
/**
|
||||
* Handle Completions
|
||||
* @param data The data for the request.
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static handleCompletionsV1ChatCompletionsPost(
|
||||
data: HandleCompletionsV1ChatCompletionsPostData,
|
||||
): CancelablePromise<HandleCompletionsV1ChatCompletionsPostResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "POST",
|
||||
url: "/v1/chat/completions",
|
||||
body: data.requestBody,
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class DiagnosticService {
|
||||
/**
|
||||
* Health
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static healthHealthGet(): CancelablePromise<HealthHealthGetResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/health",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ExperimentalService {
|
||||
/**
|
||||
* Get a list of loaded models.
|
||||
* @returns string Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getRunningModelsApiPsGet(): CancelablePromise<GetRunningModelsApiPsGetResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/api/ps",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a model into memory.
|
||||
* @param data The data for the request.
|
||||
* @param data.modelId
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static loadModelRouteApiPsModelIdPost(
|
||||
data: LoadModelRouteApiPsModelIdPostData,
|
||||
): CancelablePromise<LoadModelRouteApiPsModelIdPostResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "POST",
|
||||
url: "/api/ps/{model_id}",
|
||||
path: {
|
||||
model_id: data.modelId,
|
||||
},
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload a model from memory.
|
||||
* @param data The data for the request.
|
||||
* @param data.modelId
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static stopRunningModelApiPsModelIdDelete(
|
||||
data: StopRunningModelApiPsModelIdDeleteData,
|
||||
): CancelablePromise<StopRunningModelApiPsModelIdDeleteResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "DELETE",
|
||||
url: "/api/ps/{model_id}",
|
||||
path: {
|
||||
model_id: data.modelId,
|
||||
},
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelsService {
|
||||
/**
|
||||
* List Local Models
|
||||
* @param data The data for the request.
|
||||
* @param data.task
|
||||
* @returns ListModelsResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static listLocalModelsV1ModelsGet(
|
||||
data: ListLocalModelsV1ModelsGetData = {},
|
||||
): CancelablePromise<ListLocalModelsV1ModelsGetResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/v1/models",
|
||||
query: {
|
||||
task: data.task,
|
||||
},
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Local Model
|
||||
* @param data The data for the request.
|
||||
* @param data.modelId
|
||||
* @returns Model Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getLocalModelV1ModelsModelIdGet(
|
||||
data: GetLocalModelV1ModelsModelIdGetData,
|
||||
): CancelablePromise<GetLocalModelV1ModelsModelIdGetResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/v1/models/{model_id}",
|
||||
path: {
|
||||
model_id: data.modelId,
|
||||
},
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Download Remote Model
|
||||
* @param data The data for the request.
|
||||
* @param data.modelId
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static downloadRemoteModelV1ModelsModelIdPost(
|
||||
data: DownloadRemoteModelV1ModelsModelIdPostData,
|
||||
): CancelablePromise<DownloadRemoteModelV1ModelsModelIdPostResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "POST",
|
||||
url: "/v1/models/{model_id}",
|
||||
path: {
|
||||
model_id: data.modelId,
|
||||
},
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Model
|
||||
* @param data The data for the request.
|
||||
* @param data.modelId
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deleteModelV1ModelsModelIdDelete(
|
||||
data: DeleteModelV1ModelsModelIdDeleteData,
|
||||
): CancelablePromise<DeleteModelV1ModelsModelIdDeleteResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "DELETE",
|
||||
url: "/v1/models/{model_id}",
|
||||
path: {
|
||||
model_id: data.modelId,
|
||||
},
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Remote Models
|
||||
* @param data The data for the request.
|
||||
* @param data.task
|
||||
* @returns ListModelsResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getRemoteModelsV1RegistryGet(
|
||||
data: GetRemoteModelsV1RegistryGetData = {},
|
||||
): CancelablePromise<GetRemoteModelsV1RegistryGetResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/v1/registry",
|
||||
query: {
|
||||
task: data.task,
|
||||
},
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class RealtimeService {
|
||||
/**
|
||||
* Realtime Webrtc
|
||||
* @param data The data for the request.
|
||||
* @param data.model
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static webrtcV1RealtimePost(
|
||||
data: RealtimeWebrtcV1RealtimePostData,
|
||||
): CancelablePromise<RealtimeWebrtcV1RealtimePostResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "POST",
|
||||
url: "/v1/realtime",
|
||||
query: {
|
||||
model: data.model,
|
||||
},
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class SpeechToTextService {
|
||||
/**
|
||||
* Synthesize
|
||||
* @param data The data for the request.
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static synthesizeV1AudioSpeechPost(
|
||||
data: SynthesizeV1AudioSpeechPostData,
|
||||
): CancelablePromise<SynthesizeV1AudioSpeechPostResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "POST",
|
||||
url: "/v1/audio/speech",
|
||||
body: data.requestBody,
|
||||
mediaType: "application/json",
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class VoiceActivityDetectionService {
|
||||
/**
|
||||
* Detect Speech Timestamps
|
||||
* @param data The data for the request.
|
||||
* @param data.formData
|
||||
* @returns SpeechTimestamp Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static detectSpeechTimestampsV1AudioSpeechTimestampsPost(
|
||||
data: DetectSpeechTimestampsV1AudioSpeechTimestampsPostData,
|
||||
): CancelablePromise<DetectSpeechTimestampsV1AudioSpeechTimestampsPostResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: "POST",
|
||||
url: "/v1/audio/speech/timestamps",
|
||||
formData: data.formData,
|
||||
mediaType: "application/x-www-form-urlencoded",
|
||||
errors: {
|
||||
422: "Validation Error",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,702 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type Audio = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type Body_detect_speech_timestamps_v1_audio_speech_timestamps_post = {
|
||||
model?: string
|
||||
/**
|
||||
* Speech threshold. Silero VAD outputs speech probabilities for each audio chunk, probabilities ABOVE this value are considered as SPEECH. It is better to tune this parameter for each dataset separately, but "lazy" 0.5 is pretty good for most datasets.
|
||||
*/
|
||||
threshold?: number
|
||||
/**
|
||||
* Silence threshold for determining the end of speech. If a probability is lower
|
||||
* than neg_threshold, it is always considered silence. Values higher than neg_threshold
|
||||
* are only considered speech if the previous sample was classified as speech; otherwise,
|
||||
* they are treated as silence. This parameter helps refine the detection of speech
|
||||
* transitions, ensuring smoother segment boundaries.
|
||||
*/
|
||||
neg_threshold?: number | null
|
||||
/**
|
||||
* Final speech chunks shorter min_speech_duration_ms are thrown out.
|
||||
*/
|
||||
min_speech_duration_ms?: number
|
||||
/**
|
||||
* Maximum duration of speech chunks in seconds. Chunks longer
|
||||
* than max_speech_duration_s will be split at the timestamp of the last silence that
|
||||
* lasts more than 100ms (if any), to prevent aggressive cutting. Otherwise, they will be
|
||||
* split aggressively just before max_speech_duration_s.
|
||||
*/
|
||||
max_speech_duration_s?: number
|
||||
/**
|
||||
* In the end of each speech chunk wait for min_silence_duration_ms
|
||||
* before separating it
|
||||
*/
|
||||
min_silence_duration_ms?: number
|
||||
/**
|
||||
* Final speech chunks are padded by speech_pad_ms each side
|
||||
*/
|
||||
speech_pad_ms?: number
|
||||
file: Blob | File
|
||||
}
|
||||
|
||||
export type Body_transcribe_file_v1_audio_transcriptions_post = {
|
||||
model: string
|
||||
language?: string | null
|
||||
prompt?: string | null
|
||||
response_format?: speaches__routers__stt__ResponseFormat
|
||||
temperature?: number
|
||||
timestamp_granularities?: Array<"segment" | "word">
|
||||
stream?: boolean
|
||||
hotwords?: string | null
|
||||
vad_filter?: boolean
|
||||
file: Blob | File
|
||||
}
|
||||
|
||||
export type Body_translate_file_v1_audio_translations_post = {
|
||||
model: string
|
||||
prompt?: string | null
|
||||
response_format?: speaches__routers__stt__ResponseFormat
|
||||
temperature?: number
|
||||
stream?: boolean
|
||||
vad_filter?: boolean
|
||||
file: Blob | File
|
||||
}
|
||||
|
||||
export type ChatCompletion = {
|
||||
id: string
|
||||
choices: Array<openai__types__chat__chat_completion__Choice>
|
||||
created: number
|
||||
model: string
|
||||
object: "chat.completion"
|
||||
service_tier?: "scale" | "default" | null
|
||||
system_fingerprint?: string | null
|
||||
usage?: CompletionUsage | null
|
||||
[key: string]:
|
||||
| unknown
|
||||
| string
|
||||
| openai__types__chat__chat_completion__Choice
|
||||
| number
|
||||
| "chat.completion"
|
||||
}
|
||||
|
||||
export type ChatCompletionAssistantMessageParam = {
|
||||
role: "assistant"
|
||||
audio?: Audio | null
|
||||
content?:
|
||||
| string
|
||||
| Array<
|
||||
| ChatCompletionContentPartTextParam
|
||||
| ChatCompletionContentPartRefusalParam
|
||||
>
|
||||
| null
|
||||
function_call?: FunctionCall_Input | null
|
||||
name?: string | null
|
||||
refusal?: string | null
|
||||
tool_calls?: Array<ChatCompletionMessageToolCallParam> | null
|
||||
}
|
||||
|
||||
export type ChatCompletionAudio = {
|
||||
id: string
|
||||
data: string
|
||||
expires_at: number
|
||||
transcript: string
|
||||
[key: string]: unknown | string | number
|
||||
}
|
||||
|
||||
export type ChatCompletionAudioParam = {
|
||||
format: "wav" | "mp3" | "flac" | "opus" | "pcm16"
|
||||
voice: string
|
||||
}
|
||||
|
||||
export type format = "wav" | "mp3" | "flac" | "opus" | "pcm16"
|
||||
|
||||
export type ChatCompletionChunk = {
|
||||
id: string
|
||||
choices: Array<openai__types__chat__chat_completion_chunk__Choice>
|
||||
created: number
|
||||
model: string
|
||||
object: "chat.completion.chunk"
|
||||
service_tier?: "scale" | "default" | null
|
||||
system_fingerprint?: string | null
|
||||
usage?: CompletionUsage | null
|
||||
[key: string]:
|
||||
| unknown
|
||||
| string
|
||||
| openai__types__chat__chat_completion_chunk__Choice
|
||||
| number
|
||||
| "chat.completion.chunk"
|
||||
}
|
||||
|
||||
export type ChatCompletionContentPartImageParam = {
|
||||
image_url: ImageURL
|
||||
type: "image_url"
|
||||
}
|
||||
|
||||
export type ChatCompletionContentPartInputAudioParam = {
|
||||
input_audio: InputAudio
|
||||
type: "input_audio"
|
||||
}
|
||||
|
||||
export type ChatCompletionContentPartRefusalParam = {
|
||||
refusal: string
|
||||
type: "refusal"
|
||||
}
|
||||
|
||||
export type ChatCompletionContentPartTextParam = {
|
||||
text: string
|
||||
type: "text"
|
||||
}
|
||||
|
||||
export type ChatCompletionDeveloperMessageParam = {
|
||||
content: string | Array<ChatCompletionContentPartTextParam>
|
||||
role: "developer"
|
||||
name?: string | null
|
||||
}
|
||||
|
||||
export type ChatCompletionFunctionCallOptionParam = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type ChatCompletionFunctionMessageParam = {
|
||||
content: string | null
|
||||
name: string
|
||||
role: "function"
|
||||
}
|
||||
|
||||
export type ChatCompletionMessage = {
|
||||
content?: string | null
|
||||
refusal?: string | null
|
||||
role: "assistant"
|
||||
audio?: ChatCompletionAudio | null
|
||||
function_call?: FunctionCall_Output | null
|
||||
tool_calls?: Array<ChatCompletionMessageToolCall> | null
|
||||
[key: string]: unknown | "assistant"
|
||||
}
|
||||
|
||||
export type ChatCompletionMessageToolCall = {
|
||||
id: string
|
||||
function: Function
|
||||
type: "function"
|
||||
[key: string]: unknown | string | Function | "function"
|
||||
}
|
||||
|
||||
export type ChatCompletionMessageToolCallParam = {
|
||||
id: string
|
||||
function: OpenaiTypesChatChatCompletionMessageToolCallParamFunction
|
||||
type: "function"
|
||||
}
|
||||
|
||||
export type ChatCompletionNamedToolChoiceParam = {
|
||||
function: OpenaiTypesChatChatCompletionNamedToolChoiceParamFunction
|
||||
type: "function"
|
||||
}
|
||||
|
||||
export type ChatCompletionPredictionContentParam = {
|
||||
content: string | Array<ChatCompletionContentPartTextParam>
|
||||
type: "content"
|
||||
}
|
||||
|
||||
export type ChatCompletionStreamOptionsParam = {
|
||||
include_usage?: boolean | null
|
||||
}
|
||||
|
||||
export type ChatCompletionSystemMessageParam = {
|
||||
content: string | Array<ChatCompletionContentPartTextParam>
|
||||
role: "system"
|
||||
name?: string | null
|
||||
}
|
||||
|
||||
export type ChatCompletionTokenLogprob = {
|
||||
token: string
|
||||
bytes?: Array<number> | null
|
||||
logprob: number
|
||||
top_logprobs: Array<TopLogprob>
|
||||
[key: string]: unknown | string | number | TopLogprob
|
||||
}
|
||||
|
||||
export type ChatCompletionToolMessageParam = {
|
||||
content: string | Array<ChatCompletionContentPartTextParam>
|
||||
role: "tool"
|
||||
tool_call_id: string
|
||||
}
|
||||
|
||||
export type ChatCompletionToolParam = {
|
||||
function: FunctionDefinition
|
||||
type: "function"
|
||||
}
|
||||
|
||||
export type ChatCompletionUserMessageParam = {
|
||||
content:
|
||||
| string
|
||||
| Array<
|
||||
| ChatCompletionContentPartTextParam
|
||||
| ChatCompletionContentPartImageParam
|
||||
| ChatCompletionContentPartInputAudioParam
|
||||
>
|
||||
role: "user"
|
||||
name?: string | null
|
||||
}
|
||||
|
||||
export type ChoiceDelta = {
|
||||
content?: string | null
|
||||
function_call?: ChoiceDeltaFunctionCall | null
|
||||
refusal?: string | null
|
||||
role?: "developer" | "system" | "user" | "assistant" | "tool" | null
|
||||
tool_calls?: Array<ChoiceDeltaToolCall> | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type ChoiceDeltaFunctionCall = {
|
||||
arguments?: string | null
|
||||
name?: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type ChoiceDeltaToolCall = {
|
||||
index: number
|
||||
id?: string | null
|
||||
function?: ChoiceDeltaToolCallFunction | null
|
||||
type?: "function" | null
|
||||
[key: string]: unknown | number
|
||||
}
|
||||
|
||||
export type ChoiceDeltaToolCallFunction = {
|
||||
arguments?: string | null
|
||||
name?: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type ChoiceLogprobs = {
|
||||
content?: Array<ChatCompletionTokenLogprob> | null
|
||||
refusal?: Array<ChatCompletionTokenLogprob> | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type CompletionCreateParamsBase = {
|
||||
messages: Array<
|
||||
| ChatCompletionDeveloperMessageParam
|
||||
| ChatCompletionSystemMessageParam
|
||||
| ChatCompletionUserMessageParam
|
||||
| ChatCompletionAssistantMessageParam
|
||||
| ChatCompletionToolMessageParam
|
||||
| ChatCompletionFunctionMessageParam
|
||||
>
|
||||
model:
|
||||
| string
|
||||
| "o1"
|
||||
| "o1-2024-12-17"
|
||||
| "o1-preview"
|
||||
| "o1-preview-2024-09-12"
|
||||
| "o1-mini"
|
||||
| "o1-mini-2024-09-12"
|
||||
| "gpt-4o"
|
||||
| "gpt-4o-2024-11-20"
|
||||
| "gpt-4o-2024-08-06"
|
||||
| "gpt-4o-2024-05-13"
|
||||
| "gpt-4o-audio-preview"
|
||||
| "gpt-4o-audio-preview-2024-10-01"
|
||||
| "gpt-4o-audio-preview-2024-12-17"
|
||||
| "gpt-4o-mini-audio-preview"
|
||||
| "gpt-4o-mini-audio-preview-2024-12-17"
|
||||
| "chatgpt-4o-latest"
|
||||
| "gpt-4o-mini"
|
||||
| "gpt-4o-mini-2024-07-18"
|
||||
| "gpt-4-turbo"
|
||||
| "gpt-4-turbo-2024-04-09"
|
||||
| "gpt-4-0125-preview"
|
||||
| "gpt-4-turbo-preview"
|
||||
| "gpt-4-1106-preview"
|
||||
| "gpt-4-vision-preview"
|
||||
| "gpt-4"
|
||||
| "gpt-4-0314"
|
||||
| "gpt-4-0613"
|
||||
| "gpt-4-32k"
|
||||
| "gpt-4-32k-0314"
|
||||
| "gpt-4-32k-0613"
|
||||
| "gpt-3.5-turbo"
|
||||
| "gpt-3.5-turbo-16k"
|
||||
| "gpt-3.5-turbo-0301"
|
||||
| "gpt-3.5-turbo-0613"
|
||||
| "gpt-3.5-turbo-1106"
|
||||
| "gpt-3.5-turbo-0125"
|
||||
| "gpt-3.5-turbo-16k-0613"
|
||||
audio?: ChatCompletionAudioParam | null
|
||||
frequency_penalty?: number | null
|
||||
function_call?: "none" | "auto" | ChatCompletionFunctionCallOptionParam | null
|
||||
functions?: Array<OpenaiTypesChatCompletionCreateParamsFunction> | null
|
||||
logit_bias?: {
|
||||
[key: string]: number
|
||||
} | null
|
||||
logprobs?: boolean | null
|
||||
max_completion_tokens?: number | null
|
||||
max_tokens?: number | null
|
||||
metadata?: {
|
||||
[key: string]: string
|
||||
} | null
|
||||
modalities?: Array<"text" | "audio"> | null
|
||||
n?: number | null
|
||||
parallel_tool_calls?: boolean | null
|
||||
prediction?: ChatCompletionPredictionContentParam | null
|
||||
presence_penalty?: number | null
|
||||
reasoning_effort?: "low" | "medium" | "high" | null
|
||||
response_format?:
|
||||
| ResponseFormatText
|
||||
| ResponseFormatJSONObject
|
||||
| ResponseFormatJSONSchema
|
||||
| null
|
||||
seed?: number | null
|
||||
service_tier?: "auto" | "default" | null
|
||||
stop?: string | Array<string> | null
|
||||
store?: boolean | null
|
||||
stream_options?: ChatCompletionStreamOptionsParam | null
|
||||
temperature?: number | null
|
||||
tool_choice?:
|
||||
| "none"
|
||||
| "auto"
|
||||
| "required"
|
||||
| ChatCompletionNamedToolChoiceParam
|
||||
| null
|
||||
tools?: Array<ChatCompletionToolParam> | null
|
||||
top_logprobs?: number | null
|
||||
top_p?: number | null
|
||||
user?: string | null
|
||||
stream?: boolean
|
||||
trancription_model?: string
|
||||
transcription_extra_body?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
speech_model?: string
|
||||
speech_extra_body?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
}
|
||||
|
||||
export type CompletionTokensDetails = {
|
||||
accepted_prediction_tokens?: number | null
|
||||
audio_tokens?: number | null
|
||||
reasoning_tokens?: number | null
|
||||
rejected_prediction_tokens?: number | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type CompletionUsage = {
|
||||
completion_tokens: number
|
||||
prompt_tokens: number
|
||||
total_tokens: number
|
||||
completion_tokens_details?: CompletionTokensDetails | null
|
||||
prompt_tokens_details?: PromptTokensDetails | null
|
||||
[key: string]: unknown | number
|
||||
}
|
||||
|
||||
export type CreateSpeechRequestBody = {
|
||||
/**
|
||||
* The ID of the model. You can get a list of available models by calling `/v1/models`.
|
||||
*/
|
||||
model: string
|
||||
input: string
|
||||
voice: string
|
||||
response_format?: speaches__routers__speech__ResponseFormat
|
||||
speed?: number
|
||||
sample_rate?: number | null
|
||||
}
|
||||
|
||||
export type CreateTranscriptionResponseJson = {
|
||||
text: string
|
||||
}
|
||||
|
||||
export type CreateTranscriptionResponseVerboseJson = {
|
||||
task?: string
|
||||
language: string
|
||||
duration: number
|
||||
text: string
|
||||
words: Array<TranscriptionWord> | null
|
||||
segments: Array<TranscriptionSegment>
|
||||
}
|
||||
|
||||
export type Function = {
|
||||
arguments: string
|
||||
name: string
|
||||
[key: string]: unknown | string
|
||||
}
|
||||
|
||||
export type FunctionCall_Input = {
|
||||
arguments: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type FunctionCall_Output = {
|
||||
arguments: string
|
||||
name: string
|
||||
[key: string]: unknown | string
|
||||
}
|
||||
|
||||
export type FunctionDefinition = {
|
||||
name: string
|
||||
description?: string | null
|
||||
parameters?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
strict?: boolean | null
|
||||
}
|
||||
|
||||
export type HTTPValidationError = {
|
||||
detail?: Array<ValidationError>
|
||||
}
|
||||
|
||||
export type ImageURL = {
|
||||
url: string
|
||||
detail?: "auto" | "low" | "high" | null
|
||||
}
|
||||
|
||||
export type InputAudio = {
|
||||
data: string
|
||||
format: "wav" | "mp3"
|
||||
}
|
||||
|
||||
export type format2 = "wav" | "mp3"
|
||||
|
||||
export type JSONSchema = {
|
||||
name: string
|
||||
description?: string | null
|
||||
schema?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
strict?: boolean | null
|
||||
}
|
||||
|
||||
export type ListModelsResponse = {
|
||||
data: Array<Model>
|
||||
object?: "list"
|
||||
}
|
||||
|
||||
/**
|
||||
* There may be additional fields in the response that are specific to the model type.
|
||||
*/
|
||||
export type Model = {
|
||||
id: string
|
||||
created?: number
|
||||
object?: "model"
|
||||
owned_by: string
|
||||
language?: Array<string> | null
|
||||
task: "automatic-speech-recognition" | "text-to-speech"
|
||||
[key: string]: unknown | string | number | "model"
|
||||
}
|
||||
|
||||
export type task = "automatic-speech-recognition" | "text-to-speech"
|
||||
|
||||
export type openai__types__chat__chat_completion__Choice = {
|
||||
finish_reason:
|
||||
| "stop"
|
||||
| "length"
|
||||
| "tool_calls"
|
||||
| "content_filter"
|
||||
| "function_call"
|
||||
index: number
|
||||
logprobs?: ChoiceLogprobs | null
|
||||
message: ChatCompletionMessage
|
||||
[key: string]: unknown | string | number | ChatCompletionMessage
|
||||
}
|
||||
|
||||
export type finish_reason =
|
||||
| "stop"
|
||||
| "length"
|
||||
| "tool_calls"
|
||||
| "content_filter"
|
||||
| "function_call"
|
||||
|
||||
export type openai__types__chat__chat_completion_chunk__Choice = {
|
||||
delta: ChoiceDelta
|
||||
finish_reason?:
|
||||
| "stop"
|
||||
| "length"
|
||||
| "tool_calls"
|
||||
| "content_filter"
|
||||
| "function_call"
|
||||
| null
|
||||
index: number
|
||||
logprobs?: ChoiceLogprobs | null
|
||||
[key: string]: unknown | ChoiceDelta | number
|
||||
}
|
||||
|
||||
export type OpenaiTypesChatChatCompletionMessageToolCallParamFunction = {
|
||||
arguments: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type OpenaiTypesChatChatCompletionNamedToolChoiceParamFunction = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type OpenaiTypesChatCompletionCreateParamsFunction = {
|
||||
name: string
|
||||
description?: string | null
|
||||
parameters?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
}
|
||||
|
||||
export type PromptTokensDetails = {
|
||||
audio_tokens?: number | null
|
||||
cached_tokens?: number | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type ResponseFormatJSONObject = {
|
||||
type: "json_object"
|
||||
}
|
||||
|
||||
export type ResponseFormatJSONSchema = {
|
||||
json_schema: JSONSchema
|
||||
type: "json_schema"
|
||||
}
|
||||
|
||||
export type ResponseFormatText = {
|
||||
type: "text"
|
||||
}
|
||||
|
||||
export type speaches__routers__speech__ResponseFormat =
|
||||
| "mp3"
|
||||
| "flac"
|
||||
| "wav"
|
||||
| "pcm"
|
||||
|
||||
export type speaches__routers__stt__ResponseFormat =
|
||||
| "text"
|
||||
| "json"
|
||||
| "verbose_json"
|
||||
| "srt"
|
||||
| "vtt"
|
||||
|
||||
export type SpeechTimestamp = {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export type TopLogprob = {
|
||||
token: string
|
||||
bytes?: Array<number> | null
|
||||
logprob: number
|
||||
[key: string]: unknown | string | number
|
||||
}
|
||||
|
||||
export type TranscriptionSegment = {
|
||||
id: number
|
||||
seek: number
|
||||
start: number
|
||||
end: number
|
||||
text: string
|
||||
tokens: Array<number>
|
||||
temperature: number
|
||||
avg_logprob: number
|
||||
compression_ratio: number
|
||||
no_speech_prob: number
|
||||
words: Array<TranscriptionWord> | null
|
||||
}
|
||||
|
||||
export type TranscriptionWord = {
|
||||
start: number
|
||||
end: number
|
||||
word: string
|
||||
probability: number
|
||||
}
|
||||
|
||||
export type ValidationError = {
|
||||
loc: Array<string | number>
|
||||
msg: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export type TranslateFileV1AudioTranslationsPostData = {
|
||||
formData: Body_translate_file_v1_audio_translations_post
|
||||
}
|
||||
|
||||
export type TranslateFileV1AudioTranslationsPostResponse =
|
||||
| string
|
||||
| CreateTranscriptionResponseJson
|
||||
| CreateTranscriptionResponseVerboseJson
|
||||
|
||||
export type TranscribeFileV1AudioTranscriptionsPostData = {
|
||||
formData: Body_transcribe_file_v1_audio_transcriptions_post
|
||||
}
|
||||
|
||||
export type TranscribeFileV1AudioTranscriptionsPostResponse =
|
||||
| string
|
||||
| CreateTranscriptionResponseJson
|
||||
| CreateTranscriptionResponseVerboseJson
|
||||
|
||||
export type HandleCompletionsV1ChatCompletionsPostData = {
|
||||
requestBody: CompletionCreateParamsBase
|
||||
}
|
||||
|
||||
export type HandleCompletionsV1ChatCompletionsPostResponse =
|
||||
| ChatCompletion
|
||||
| ChatCompletionChunk
|
||||
|
||||
export type HealthHealthGetResponse = unknown
|
||||
|
||||
export type GetRunningModelsApiPsGetResponse = {
|
||||
[key: string]: Array<string>
|
||||
}
|
||||
|
||||
export type LoadModelRouteApiPsModelIdPostData = {
|
||||
modelId: string
|
||||
}
|
||||
|
||||
export type LoadModelRouteApiPsModelIdPostResponse = unknown
|
||||
|
||||
export type StopRunningModelApiPsModelIdDeleteData = {
|
||||
modelId: string
|
||||
}
|
||||
|
||||
export type StopRunningModelApiPsModelIdDeleteResponse = unknown
|
||||
|
||||
export type ListLocalModelsV1ModelsGetData = {
|
||||
task?: "automatic-speech-recognition" | "text-to-speech" | null
|
||||
}
|
||||
|
||||
export type ListLocalModelsV1ModelsGetResponse = ListModelsResponse
|
||||
|
||||
export type GetLocalModelV1ModelsModelIdGetData = {
|
||||
modelId: string
|
||||
}
|
||||
|
||||
export type GetLocalModelV1ModelsModelIdGetResponse = Model
|
||||
|
||||
export type DownloadRemoteModelV1ModelsModelIdPostData = {
|
||||
modelId: string
|
||||
}
|
||||
|
||||
export type DownloadRemoteModelV1ModelsModelIdPostResponse = unknown
|
||||
|
||||
export type DeleteModelV1ModelsModelIdDeleteData = {
|
||||
modelId: string
|
||||
}
|
||||
|
||||
export type DeleteModelV1ModelsModelIdDeleteResponse = unknown
|
||||
|
||||
export type GetRemoteModelsV1RegistryGetData = {
|
||||
task?: "automatic-speech-recognition" | "text-to-speech" | null
|
||||
}
|
||||
|
||||
export type GetRemoteModelsV1RegistryGetResponse = ListModelsResponse
|
||||
|
||||
export type RealtimeWebrtcV1RealtimePostData = {
|
||||
model: string
|
||||
}
|
||||
|
||||
export type RealtimeWebrtcV1RealtimePostResponse = unknown
|
||||
|
||||
export type SynthesizeV1AudioSpeechPostData = {
|
||||
requestBody: CreateSpeechRequestBody
|
||||
}
|
||||
|
||||
export type SynthesizeV1AudioSpeechPostResponse = unknown
|
||||
|
||||
export type DetectSpeechTimestampsV1AudioSpeechTimestampsPostData = {
|
||||
formData: Body_detect_speech_timestamps_v1_audio_speech_timestamps_post
|
||||
}
|
||||
|
||||
export type DetectSpeechTimestampsV1AudioSpeechTimestampsPostResponse =
|
||||
Array<SpeechTimestamp>
|
||||
@@ -0,0 +1,3 @@
|
||||
# Variables defined here will be available to use anywhere in the config with the syntax ${MY_SECRET_TOKEN}
|
||||
# Note: making changes to this file requires re-running docker compose up
|
||||
MY_SECRET_TOKEN=123456
|
||||
@@ -0,0 +1,12 @@
|
||||
server:
|
||||
assets-path: /app/assets
|
||||
|
||||
theme:
|
||||
# Note: assets are cached by the browser, changes to the CSS file
|
||||
# will not be reflected until the browser cache is cleared (Ctrl+F5)
|
||||
custom-css-file: /assets/user.css
|
||||
|
||||
pages:
|
||||
# It's not necessary to create a new file for each page and include it, you can simply
|
||||
# put its contents here, though multiple pages are easier to manage when separated
|
||||
- $include: home.yml
|
||||
@@ -0,0 +1,106 @@
|
||||
- name: Home
|
||||
# Optionally, if you only have a single page you can hide the desktop navigation for a cleaner look
|
||||
# hide-desktop-navigation: true
|
||||
columns:
|
||||
- size: small
|
||||
widgets:
|
||||
- type: calendar
|
||||
first-day-of-week: monday
|
||||
|
||||
- type: rss
|
||||
limit: 10
|
||||
collapse-after: 3
|
||||
cache: 12h
|
||||
feeds:
|
||||
- url: https://selfh.st/rss/
|
||||
title: selfh.st
|
||||
- url: https://ciechanow.ski/atom.xml
|
||||
- url: https://www.joshwcomeau.com/rss.xml
|
||||
title: Josh Comeau
|
||||
- url: https://samwho.dev/rss.xml
|
||||
- url: https://ishadeed.com/feed.xml
|
||||
title: Ahmad Shadeed
|
||||
- url: https://www.techpowerup.com/rss/news
|
||||
title: TechPowerUp
|
||||
|
||||
- type: twitch-channels
|
||||
channels:
|
||||
- theprimeagen
|
||||
- j_blow
|
||||
- giantwaffle
|
||||
- cohhcarnage
|
||||
- christitustech
|
||||
- EJ_SA
|
||||
|
||||
- size: full
|
||||
widgets:
|
||||
- type: rss
|
||||
title: News
|
||||
style: horizontal-cards
|
||||
feeds:
|
||||
- url: https://www.techpowerup.com/rss/news
|
||||
title: TechPowerUp
|
||||
- url: https://feeds.bloomberg.com/markets/news.rss
|
||||
title: Bloomberg
|
||||
- url: https://moxie.foxbusiness.com/google-publisher/markets.xml
|
||||
title: Fox Business
|
||||
|
||||
- type: videos
|
||||
channels:
|
||||
- UCR-DXc1voovS8nhAvccRZhg # Jeff Geerling
|
||||
- UCsBjURrPoezykLs9EqgamOA # Fireship
|
||||
- UCBJycsmduvYEL83R_U4JriQ # Marques Brownlee
|
||||
- UCHnyfMqiRRG1u-2MsSQLbXA # Veritasium
|
||||
|
||||
- type: group
|
||||
widgets:
|
||||
- type: reddit
|
||||
subreddit: technology
|
||||
show-thumbnails: true
|
||||
- type: reddit
|
||||
subreddit: LocalLLaMA
|
||||
show-thumbnails: true
|
||||
|
||||
- type: group
|
||||
widgets:
|
||||
- type: hacker-news
|
||||
- type: lobsters
|
||||
|
||||
- size: small
|
||||
widgets:
|
||||
- type: weather
|
||||
location: Singapore, Singapore
|
||||
units: metric # alternatively "imperial"
|
||||
hour-format: 12h # alternatively "24h"
|
||||
# Optionally hide the location from being displayed in the widget
|
||||
# hide-location: true
|
||||
|
||||
- type: markets
|
||||
markets:
|
||||
- symbol: QQQ
|
||||
name: Nasdaq 100
|
||||
- symbol: ^STI
|
||||
name: Straits Times Index
|
||||
- symbol: BTC-USD
|
||||
name: Bitcoin
|
||||
- symbol: NVDA
|
||||
name: NVIDIA
|
||||
- symbol: AAPL
|
||||
name: Apple
|
||||
- symbol: MSFT
|
||||
name: Microsoft
|
||||
- symbol: GOOGL
|
||||
name: Alphabet
|
||||
- symbol: GC=F
|
||||
name: Gold
|
||||
|
||||
- type: releases
|
||||
cache: 1d
|
||||
# Without authentication the Github API allows for up to 60 requests per hour. You can create a
|
||||
# read-only token from your Github account settings and use it here to increase the limit.
|
||||
# token: ...
|
||||
repositories:
|
||||
- glanceapp/glance
|
||||
- go-gitea/gitea
|
||||
- immich-app/immich
|
||||
- syncthing/syncthing
|
||||
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
glance:
|
||||
container_name: glance
|
||||
image: glanceapp/glance
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- ./assets:/app/assets
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
# Optionally, also mount docker socket if you want to use the docker containers widget
|
||||
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
ports:
|
||||
- 8080:8080
|
||||
env_file: .env
|
||||
@@ -21,7 +21,7 @@ include $(ENV_FILE)
|
||||
export
|
||||
|
||||
# Targets
|
||||
.PHONY: up up-e2e down restart logs build reset network
|
||||
.PHONY: up up-e2e down restart logs build reset network clean prune backup restore help info ollama-build ollama-build-host ollama-up ollama-down ollama-logs pull-deepseek-model setup-ollama llamacpp-build llamacpp-build-host llamacpp-up llamacpp-down llamacpp-logs pull-deepseek-llamacpp setup-llamacpp llamacpp-gpu ai-launcher
|
||||
|
||||
network:
|
||||
@podman network exists traefik-public || podman network create traefik-public
|
||||
@@ -94,20 +94,40 @@ help:
|
||||
@echo "Makefile for managing Podman Compose"
|
||||
@echo ""
|
||||
@echo "Usage:"
|
||||
@echo " make network Create the traefik-public network if it doesn't exist"
|
||||
@echo " make up Start the containers in detached mode"
|
||||
@echo " make up-e2e Start containers including the Playwright test service"
|
||||
@echo " make down Stop the containers"
|
||||
@echo " make logs View the logs of the containers"
|
||||
@echo " make build Build the images directly with Podman (override with BUILD_NETWORK=...)"
|
||||
@echo " make restart Restart the containers"
|
||||
@echo " make reset Completely reset Podman (containers, volumes, networks)"
|
||||
@echo " make backup Create a backup of the database volume"
|
||||
@echo " make restore Restore the database volume from a backup"
|
||||
@echo " make clean Remove all containers and volumes"
|
||||
@echo " make prune Remove all stopped containers and unused volumes"
|
||||
@echo " make info Display detailed information about Podman state"
|
||||
@echo " make help View this help message"
|
||||
@echo " make network Create the traefik-public network if it doesn't exist"
|
||||
@echo " make up Start the containers in detached mode"
|
||||
@echo " make up-e2e Start containers including the Playwright test service"
|
||||
@echo " make down Stop the containers"
|
||||
@echo " make logs View the logs of the containers"
|
||||
@echo " make build Build backend, frontend, and Playwright images directly with Podman"
|
||||
@echo " make restart Restart the containers"
|
||||
@echo " make reset Completely reset Podman (containers, volumes, networks)"
|
||||
@echo " make backup Create a backup of the database volume"
|
||||
@echo " make restore Restore the database volume from a backup"
|
||||
@echo " make clean Remove all containers and volumes"
|
||||
@echo " make prune Remove all stopped containers and unused volumes"
|
||||
@echo " make info Display detailed information about Podman state"
|
||||
@echo " make help View this help message"
|
||||
@echo " make ai-launcher Interactive AI backend launcher (recommended for new users)"
|
||||
@echo ""
|
||||
@echo "Ollama-specific targets:"
|
||||
@echo " make ollama-build Build Ollama container with no cache (using host networking)"
|
||||
@echo " make ollama-build-host Build Ollama container with host networking (alternative)"
|
||||
@echo " make ollama-up Start only the Ollama container"
|
||||
@echo " make ollama-down Stop only the Ollama container"
|
||||
@echo " make ollama-logs View Ollama container logs"
|
||||
@echo " make pull-deepseek-model Download and load DeepSeek-R1 model into Ollama"
|
||||
@echo " make setup-ollama Start Ollama and automatically pull DeepSeek model"
|
||||
@echo ""
|
||||
@echo "LlamaCPP-specific targets:"
|
||||
@echo " make llamacpp-build Build LlamaCPP container with no cache (using slirp4netns)"
|
||||
@echo " make llamacpp-build-host Build LlamaCPP container with host networking (alternative)"
|
||||
@echo " make llamacpp-up Start only the LlamaCPP container"
|
||||
@echo " make llamacpp-down Stop only the LlamaCPP container"
|
||||
@echo " make llamacpp-logs View LlamaCPP container logs"
|
||||
@echo " make pull-deepseek-llamacpp Download DeepSeek-R1 model for LlamaCPP"
|
||||
@echo " make setup-llamacpp Start LlamaCPP and automatically pull DeepSeek model"
|
||||
@echo " make llamacpp-gpu Start GPU-accelerated LlamaCPP with DeepSeek model"
|
||||
@echo ""
|
||||
|
||||
info:
|
||||
@@ -123,3 +143,70 @@ info:
|
||||
podman images
|
||||
@echo "\n===== Podman Compose Config ====="
|
||||
podman compose --env-file $(ENV_FILE) -f docker-compose.yml -f docker-compose.override.yml config
|
||||
|
||||
# Ollama-specific targets
|
||||
ollama-build:
|
||||
cd ai_stack/ollama && DOCKER_BUILDKIT=0 podman compose build --no-cache
|
||||
@echo "Ollama container built with no cache. Use 'make ollama-up' to start it."
|
||||
|
||||
ollama-build-host:
|
||||
cd ai_stack/ollama && DOCKER_BUILDKIT=0 podman build --network=host --no-cache -t localhost/ollama-server:latest .
|
||||
@echo "Ollama container built with host networking. Use 'make ollama-up' to start it."
|
||||
|
||||
ollama-up:
|
||||
cd ai_stack/ollama && podman compose up -d
|
||||
@echo "Ollama container started. Use 'make pull-deepseek-model' to download and load the DeepSeek model."
|
||||
|
||||
ollama-down:
|
||||
cd ai_stack/ollama && podman compose down
|
||||
@echo "Ollama container stopped."
|
||||
|
||||
ollama-logs:
|
||||
cd ai_stack/ollama && podman compose logs -f
|
||||
|
||||
pull-deepseek-model:
|
||||
@echo "Pulling DeepSeek-R1 model from Hugging Face..."
|
||||
./scripts/pull-deepseek-model.sh
|
||||
@echo "Model setup completed. You can now use the model with Ollama."
|
||||
|
||||
setup-ollama: ollama-up
|
||||
@echo "Waiting for Ollama to be ready..."
|
||||
@sleep 10
|
||||
@$(MAKE) pull-deepseek-model
|
||||
|
||||
# LlamaCPP-specific targets
|
||||
llamacpp-build:
|
||||
cd ai_stack/llamacpp && DOCKER_BUILDKIT=0 CONTAINERS_NETNS=slirp4netns podman compose build --no-cache
|
||||
@echo "LlamaCPP container built with no cache. Use 'make llamacpp-up' to start it."
|
||||
|
||||
llamacpp-build-host:
|
||||
cd ai_stack/llamacpp && DOCKER_BUILDKIT=0 podman build --network=host --no-cache -t localhost/llama-cpp-server:latest .
|
||||
@echo "LlamaCPP container built with host networking. Use 'make llamacpp-up' to start it."
|
||||
|
||||
llamacpp-up:
|
||||
cd ai_stack/llamacpp && podman compose up -d
|
||||
@echo "LlamaCPP container started. Use 'make pull-deepseek-llamacpp' to download the DeepSeek model."
|
||||
|
||||
llamacpp-down:
|
||||
cd ai_stack/llamacpp && podman compose down
|
||||
@echo "LlamaCPP container stopped."
|
||||
|
||||
llamacpp-logs:
|
||||
cd ai_stack/llamacpp && podman compose logs -f
|
||||
|
||||
pull-deepseek-llamacpp:
|
||||
@echo "Pulling DeepSeek-R1 model from Hugging Face for LlamaCPP..."
|
||||
./scripts/pull-deepseek-llamacpp.sh
|
||||
@echo "Model setup completed. You can now use the model with LlamaCPP."
|
||||
|
||||
setup-llamacpp: llamacpp-up
|
||||
@echo "Waiting for LlamaCPP to be ready..."
|
||||
@sleep 15
|
||||
@$(MAKE) pull-deepseek-llamacpp
|
||||
|
||||
llamacpp-gpu:
|
||||
@echo "Starting GPU-accelerated LlamaCPP with DeepSeek model..."
|
||||
./scripts/llamacpp-gpu.sh
|
||||
|
||||
ai-launcher:
|
||||
@./scripts/ai-launcher.sh
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -0,0 +1,768 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "2vQXvnUUsTzI"
|
||||
},
|
||||
"source": [
|
||||
"To run this, press \"*Runtime*\" and press \"*Run all*\" on a **free** Tesla T4 Google Colab instance!\n",
|
||||
"<div class=\"align-center\">\n",
|
||||
"<a href=\"https://unsloth.ai/\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/unsloth%20new%20logo.png\" width=\"115\"></a>\n",
|
||||
"<a href=\"https://discord.gg/unsloth\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/Discord button.png\" width=\"145\"></a>\n",
|
||||
"<a href=\"https://unsloth.ai/docs/\"><img src=\"https://github.com/unslothai/unsloth/blob/main/images/documentation%20green%20button.png?raw=true\" width=\"125\"></a> Join Discord if you need help + ⭐ <i>Star us on <a href=\"https://github.com/unslothai/unsloth\">Github</a> </i> ⭐\n",
|
||||
"</div>\n",
|
||||
"\n",
|
||||
"To install Unsloth on your local device, follow [our guide](https://unsloth.ai/docs/get-started/install). This notebook is licensed [LGPL-3.0](https://github.com/unslothai/notebooks?tab=LGPL-3.0-1-ov-file#readme).\n",
|
||||
"\n",
|
||||
"You will learn how to do [data prep](#Data), how to [train](#Train), how to [run the model](#Inference), & how to save it"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "7j01DfVgsTzJ"
|
||||
},
|
||||
"source": [
|
||||
"### News"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "6dT42nHksTzJ"
|
||||
},
|
||||
"source": [
|
||||
"Introducing **Unsloth Studio** - a new open source, no-code web UI to train and run LLMs. [Blog](https://unsloth.ai/docs/new/studio) • [Notebook](https://colab.research.google.com/github/unslothai/unsloth/blob/main/studio/Unsloth_Studio_Colab.ipynb)\n",
|
||||
"\n",
|
||||
"<table><tr>\n",
|
||||
"<td align=\"center\"><a href=\"https://unsloth.ai/docs/new/studio\"><img src=\"https://unsloth.ai/docs/~gitbook/image?url=https%3A%2F%2F3215535692-files.gitbook.io%2F~%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252FxhOjnexMCB3dmuQFQ2Zq%252Fuploads%252FxV1PO5DbF3ksB51nE2Tw%252Fmore%2520cropped%2520ui%2520for%2520homepage.png%3Falt%3Dmedia%26token%3Df75942c9-3d8d-4b59-8ba2-1a4a38de1b86&width=376&dpr=3&quality=100&sign=a663c397&sv=2\" width=\"200\" height=\"120\" alt=\"Unsloth Studio Training UI\"></a><br><sub><b>Train models</b> — no code needed</sub></td>\n",
|
||||
"<td align=\"center\"><a href=\"https://unsloth.ai/docs/new/studio\"><img src=\"https://unsloth.ai/docs/~gitbook/image?url=https%3A%2F%2F3215535692-files.gitbook.io%2F~%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252FxhOjnexMCB3dmuQFQ2Zq%252Fuploads%252FRCnTAZ6Uh88DIlU3g0Ij%252Fmainpage%2520unsloth.png%3Falt%3Dmedia%26token%3D837c96b6-bd09-4e81-bc76-fa50421e9bfb&width=376&dpr=3&quality=100&sign=c1a39da1&sv=2\" width=\"200\" height=\"120\" alt=\"Unsloth Studio Chat UI\"></a><br><sub><b>Run GGUF models</b> on Mac, Windows & Linux</sub></td>\n",
|
||||
"</tr></table>\n",
|
||||
"\n",
|
||||
"Train MoEs - DeepSeek, GLM, Qwen and gpt-oss 12x faster with 35% less VRAM. [Blog](https://unsloth.ai/docs/new/faster-moe)\n",
|
||||
"\n",
|
||||
"Ultra Long-Context Reinforcement Learning is here with 7x more context windows! [Blog](https://unsloth.ai/docs/new/grpo-long-context)\n",
|
||||
"\n",
|
||||
"New in Reinforcement Learning: [FP8 RL](https://unsloth.ai/docs/new/fp8-reinforcement-learning) • [Vision RL](https://unsloth.ai/docs/new/vision-reinforcement-learning-vlm-rl) • [Standby](https://unsloth.ai/docs/basics/memory-efficient-rl) • [gpt-oss RL](https://unsloth.ai/docs/new/gpt-oss-reinforcement-learning)\n",
|
||||
"\n",
|
||||
"Visit our docs for all our [model uploads](https://unsloth.ai/docs/get-started/unsloth-model-catalog) and [notebooks](https://unsloth.ai/docs/get-started/unsloth-notebooks)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "K7fgQkATsTzK"
|
||||
},
|
||||
"source": [
|
||||
"### Installation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "vA7IKFdUsTzK"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture\n",
|
||||
"import os, re\n",
|
||||
"if \"COLAB_\" not in \"\".join(os.environ.keys()):\n",
|
||||
" !pip install unsloth # Do this in local & cloud setups\n",
|
||||
"else:\n",
|
||||
" import torch; v = re.match(r'[\\d]{1,}\\.[\\d]{1,}', str(torch.__version__)).group(0)\n",
|
||||
" xformers = 'xformers==' + {'2.10':'0.0.34','2.9':'0.0.33.post1','2.8':'0.0.32.post2'}.get(v, \"0.0.34\")\n",
|
||||
" !pip install sentencepiece protobuf \"datasets==4.3.0\" \"huggingface_hub>=0.34.0\" hf_transfer\n",
|
||||
" !pip install --no-deps unsloth_zoo bitsandbytes accelerate {xformers} peft trl triton unsloth\n",
|
||||
"!pip install --no-deps git+https://github.com/huggingface/transformers.git\n",
|
||||
"!pip install torchcodec\n",
|
||||
"import torch; torch._dynamo.config.recompile_limit = 64;"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "Mp4i13PHsTzK"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture\n",
|
||||
"!pip install --no-deps --upgrade timm # For Gemma 4 vision/audio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "GFOEZbP7ONMs"
|
||||
},
|
||||
"source": [
|
||||
"### Unsloth"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "QmUBVEnvCDJv"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from unsloth import FastVisionModel # FastLanguageModel for LLMs\n",
|
||||
"import torch\n",
|
||||
"\n",
|
||||
"# 4bit pre quantized models we support for 4x faster downloading + no OOMs.\n",
|
||||
"fourbit_models = [\n",
|
||||
" # Gemma 4 models\n",
|
||||
" \"unsloth/gemma-4-E2B-it\",\n",
|
||||
" \"unsloth/gemma-4-E2B\",\n",
|
||||
" \"unsloth/gemma-4-31B-it\",\n",
|
||||
" \"unsloth/gemma-4-E4B\",\n",
|
||||
" \"unsloth/gemma-4-31B-it\",\n",
|
||||
" \"unsloth/gemma-4-31B\",\n",
|
||||
" \"unsloth/gemma-4-26B-A4B-it\",\n",
|
||||
" \"unsloth/gemma-4-26B-A4B\",\n",
|
||||
"] # More models at https://huggingface.co/unsloth\n",
|
||||
"\n",
|
||||
"model, processor = FastVisionModel.from_pretrained(\n",
|
||||
" \"unsloth/gemma-4-E2B-it\",\n",
|
||||
" load_in_4bit = True, # Use 4bit to reduce memory use. False for 16bit LoRA.\n",
|
||||
" use_gradient_checkpointing = \"unsloth\", # True or \"unsloth\" for long context\n",
|
||||
")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SXd9bTZd1aaL"
|
||||
},
|
||||
"source": [
|
||||
"We now add LoRA adapters for parameter efficient fine-tuning, allowing us to train only 1% of all model parameters efficiently.\n",
|
||||
"\n",
|
||||
"**[NEW]** We also support fine-tuning only the vision component, only the language component, or both. Additionally, you can choose to fine-tune the attention modules, the MLP layers, or both!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "6bZsfBuZDeCL"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"model = FastVisionModel.get_peft_model(\n",
|
||||
" model,\n",
|
||||
" finetune_vision_layers = True, # False if not finetuning vision layers\n",
|
||||
" finetune_language_layers = True, # False if not finetuning language layers\n",
|
||||
" finetune_attention_modules = True, # False if not finetuning attention layers\n",
|
||||
" finetune_mlp_modules = True, # False if not finetuning MLP layers\n",
|
||||
"\n",
|
||||
" r = 32, # The larger, the higher the accuracy, but might overfit\n",
|
||||
" lora_alpha = 32, # Recommended alpha == r at least\n",
|
||||
" lora_dropout = 0,\n",
|
||||
" bias = \"none\",\n",
|
||||
" random_state = 3407,\n",
|
||||
" use_rslora = False, # We support rank stabilized LoRA\n",
|
||||
" loftq_config = None, # And LoftQ\n",
|
||||
" target_modules = \"all-linear\", # Optional now! Can specify a list if needed\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [],
|
||||
"metadata": {
|
||||
"id": "Ydo-UuV92Xei"
|
||||
},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "vITh0KVJ10qX"
|
||||
},
|
||||
"source": [
|
||||
"<a name=\"Data\"></a>\n",
|
||||
"### Data Prep\n",
|
||||
"We'll use a sampled dataset of handwritten math formulas. The objective is to convert these images into a computer-readable format—specifically LaTeX—so they can be rendered. This is particularly useful for complex expressions.\n",
|
||||
"\n",
|
||||
"You can access the dataset [here](https://huggingface.co/datasets/unsloth/LaTeX_OCR). The full dataset is [here](https://huggingface.co/datasets/linxy/LaTeX_OCR)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "LjY75GoYUCB8"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from datasets import load_dataset\n",
|
||||
"dataset = load_dataset(\"unsloth/LaTeX_OCR\", split = \"train\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "W1W2Qhsz6rUT"
|
||||
},
|
||||
"source": [
|
||||
"Let's take an overview of the dataset. We'll examine the second image and its corresponding caption."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "bfcSGwIb6p_R"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"dataset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "uOLWY2936t1n"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"dataset[2][\"image\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "lXjfJr4W6z8P"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"dataset[2][\"text\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "rKHxfZua1CrS"
|
||||
},
|
||||
"source": [
|
||||
"We can also render LaTeX directly in the browser!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "nPopsxAC1CrS"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from IPython.display import display, Math, Latex\n",
|
||||
"\n",
|
||||
"latex = dataset[3][\"text\"]\n",
|
||||
"display(Math(latex))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "K9CBpiISFa6C"
|
||||
},
|
||||
"source": [
|
||||
"To format the dataset, all vision fine-tuning tasks should follow this format:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"[\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": [\n",
|
||||
" {\"type\": \"text\", \"text\": instruction},\n",
|
||||
" {\"type\": \"image\", \"image\": sample[\"image\"]},\n",
|
||||
" ],\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": [\n",
|
||||
" {\"type\": \"text\", \"text\": instruction},\n",
|
||||
" {\"type\": \"image\", \"image\": sample[\"image\"]},\n",
|
||||
" ],\n",
|
||||
" },\n",
|
||||
"]\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "oPXzJZzHEgXe"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"instruction = \"Write the LaTeX representation for this image.\"\n",
|
||||
"\n",
|
||||
"def convert_to_conversation(sample):\n",
|
||||
" conversation = [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": [\n",
|
||||
" {\"type\": \"text\", \"text\": instruction},\n",
|
||||
" {\"type\": \"image\", \"image\": sample[\"image\"]},\n",
|
||||
" ],\n",
|
||||
" },\n",
|
||||
" {\"role\": \"assistant\", \"content\": [{\"type\": \"text\", \"text\": sample[\"text\"]}]},\n",
|
||||
" ]\n",
|
||||
" return {\"messages\": conversation}\n",
|
||||
"pass"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "FY-9u-OD6_gE"
|
||||
},
|
||||
"source": [
|
||||
"Let's convert the dataset into the \"correct\" format for finetuning:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "gFW2qXIr7Ezy"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"converted_dataset = [convert_to_conversation(sample) for sample in dataset]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "ndDUB23CGAC5"
|
||||
},
|
||||
"source": [
|
||||
"The first example is now structured like below:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "gGFzmplrEy9I"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"converted_dataset[0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "MsRPBIb0JJ6c"
|
||||
},
|
||||
"source": [
|
||||
"Lets take the Gemma 4 instruction chat template and use it in our base model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "exoDVEvmJN-6"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from unsloth import get_chat_template\n",
|
||||
"\n",
|
||||
"processor = get_chat_template(\n",
|
||||
" processor,\n",
|
||||
" \"gemma-4\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "FecKS-dA82f5"
|
||||
},
|
||||
"source": [
|
||||
"Before fine-tuning, let us evaluate the base model's performance. We do not expect strong results, as it has not encountered this chat template before."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "vcat4UxA81vr"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"FastVisionModel.for_inference(model) # Enable for inference!\n",
|
||||
"\n",
|
||||
"image = dataset[2][\"image\"]\n",
|
||||
"instruction = \"Write the LaTeX representation for this image.\"\n",
|
||||
"\n",
|
||||
"messages = [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": [{\"type\": \"image\"}, {\"type\": \"text\", \"text\": instruction}],\n",
|
||||
" }\n",
|
||||
"]\n",
|
||||
"input_text = processor.apply_chat_template(messages, add_generation_prompt = True)\n",
|
||||
"inputs = processor(\n",
|
||||
" image,\n",
|
||||
" input_text,\n",
|
||||
" add_special_tokens = False,\n",
|
||||
" return_tensors = \"pt\",\n",
|
||||
").to(\"cuda\")\n",
|
||||
"\n",
|
||||
"from transformers import TextStreamer\n",
|
||||
"\n",
|
||||
"text_streamer = TextStreamer(processor, skip_prompt = True)\n",
|
||||
"result = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 128,\n",
|
||||
" use_cache = False, temperature = 1.0, top_p = 0.95, top_k = 64)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "FeAiMlQ71CrS"
|
||||
},
|
||||
"source": [
|
||||
"You can see it's absolutely terrible! It doesn't follow instructions at all"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "idAEIeSQ3xdS"
|
||||
},
|
||||
"source": [
|
||||
"<a name=\"Train\"></a>\n",
|
||||
"### Train the model\n",
|
||||
"Now let's train our model. We do 60 steps to speed things up, but you can set `num_train_epochs=1` for a full run, and turn off `max_steps=None`. We also support `DPOTrainer` and `GRPOTrainer` for reinforcement learning!!\n",
|
||||
"\n",
|
||||
"We use our new `UnslothVisionDataCollator` which will help in our vision finetuning setup."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "95_Nn-89DhsL"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from unsloth.trainer import UnslothVisionDataCollator\n",
|
||||
"from trl import SFTTrainer, SFTConfig\n",
|
||||
"\n",
|
||||
"FastVisionModel.for_training(model) # Enable for training!\n",
|
||||
"\n",
|
||||
"trainer = SFTTrainer(\n",
|
||||
" model = model,\n",
|
||||
" train_dataset = converted_dataset,\n",
|
||||
" processing_class = processor.tokenizer,\n",
|
||||
" data_collator = UnslothVisionDataCollator(model, processor),\n",
|
||||
" args = SFTConfig(\n",
|
||||
" per_device_train_batch_size = 1,\n",
|
||||
" gradient_accumulation_steps = 4,\n",
|
||||
" gradient_checkpointing = True,\n",
|
||||
"\n",
|
||||
" # use reentrant checkpointing\n",
|
||||
" gradient_checkpointing_kwargs = {\"use_reentrant\": False},\n",
|
||||
" max_grad_norm = 0.3, # max gradient norm based on QLoRA paper\n",
|
||||
" warmup_ratio = 0.03,\n",
|
||||
" max_steps = 60,\n",
|
||||
" #num_train_epochs = 2, # Set this instead of max_steps for full training runs\n",
|
||||
" learning_rate = 2e-4,\n",
|
||||
" logging_steps = 1,\n",
|
||||
" save_strategy = \"steps\",\n",
|
||||
" optim = \"adamw_torch_fused\",\n",
|
||||
" weight_decay = 0.001,\n",
|
||||
" lr_scheduler_type = \"cosine\",\n",
|
||||
" seed = 3407,\n",
|
||||
" output_dir = \"outputs\",\n",
|
||||
" report_to = \"none\", # For Weights and Biases\n",
|
||||
"\n",
|
||||
" # You MUST put the below items for vision finetuning:\n",
|
||||
" remove_unused_columns = False,\n",
|
||||
" dataset_text_field = \"\",\n",
|
||||
" dataset_kwargs = {\"skip_prepare_dataset\": True},\n",
|
||||
" max_length = 2048,\n",
|
||||
" )\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"cellView": "form",
|
||||
"id": "2ejIt2xSNKKp"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# @title Show current memory stats\n",
|
||||
"gpu_stats = torch.cuda.get_device_properties(0)\n",
|
||||
"start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n",
|
||||
"max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n",
|
||||
"print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n",
|
||||
"print(f\"{start_gpu_memory} GB of memory reserved.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "yqxqAZ7KJ4oL"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"trainer_stats = trainer.train()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"cellView": "form",
|
||||
"id": "pCqnaKmlO1U9"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# @title Show final memory and time stats\n",
|
||||
"used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n",
|
||||
"used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n",
|
||||
"used_percentage = round(used_memory / max_memory * 100, 3)\n",
|
||||
"lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n",
|
||||
"print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n",
|
||||
"print(\n",
|
||||
" f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\"\n",
|
||||
")\n",
|
||||
"print(f\"Peak reserved memory = {used_memory} GB.\")\n",
|
||||
"print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n",
|
||||
"print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n",
|
||||
"print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "ekOmTR1hSNcr"
|
||||
},
|
||||
"source": [
|
||||
"<a name=\"Inference\"></a>\n",
|
||||
"### Inference\n",
|
||||
"Let's run the model! You can modify the instruction and input—just leave the output blank.\n",
|
||||
"\n",
|
||||
"We'll use the best hyperparameters for inference on Gemma: `top_p=0.95`, `top_k=64`, and `temperature=1.0`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "kR3gIAX-SM2q"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"FastVisionModel.for_inference(model) # Enable for inference!\n",
|
||||
"\n",
|
||||
"image = dataset[10][\"image\"]\n",
|
||||
"instruction = \"Write the LaTeX representation for this image.\"\n",
|
||||
"\n",
|
||||
"messages = [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": [{\"type\": \"image\"}, {\"type\": \"text\", \"text\": instruction}],\n",
|
||||
" }\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"input_text = processor.apply_chat_template(messages, add_generation_prompt = True)\n",
|
||||
"\n",
|
||||
"inputs = processor(\n",
|
||||
" image,\n",
|
||||
" input_text,\n",
|
||||
" add_special_tokens = False,\n",
|
||||
" return_tensors = \"pt\",\n",
|
||||
").to(\"cuda\")\n",
|
||||
"\n",
|
||||
"from transformers import TextStreamer\n",
|
||||
"\n",
|
||||
"text_streamer = TextStreamer(processor, skip_prompt = True)\n",
|
||||
"result = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 128,\n",
|
||||
" use_cache = False, temperature = 1.0, top_p = 0.95, top_k = 64)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "uMuVrWbjAzhc"
|
||||
},
|
||||
"source": [
|
||||
"<a name=\"Save\"></a>\n",
|
||||
"### Saving, loading finetuned models\n",
|
||||
"To save the final model as LoRA adapters, use Hugging Face’s `push_to_hub` for online saving, or `save_pretrained` for local storage.\n",
|
||||
"\n",
|
||||
"**[NOTE]** This ONLY saves the LoRA adapters, and not the full model. To save to 16bit or GGUF, scroll down!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "upcOlWe7A1vc"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"model.save_pretrained(\"gemma_4_lora\") # Local saving\n",
|
||||
"processor.save_pretrained(\"gemma_4_lora\")\n",
|
||||
"# model.push_to_hub(\"your_name/gemma_4_lora\", token = \"YOUR_HF_TOKEN\") # Online saving\n",
|
||||
"# processor.push_to_hub(\"your_name/gemma_4_lora\", token = \"YOUR_HF_TOKEN\") # Online saving"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AEEcJ4qfC7Lp"
|
||||
},
|
||||
"source": [
|
||||
"Now if you want to load the LoRA adapters we just saved for inference, set `False` to `True`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "MKX_XKs_BNZR"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if False:\n",
|
||||
" from unsloth import FastVisionModel\n",
|
||||
"\n",
|
||||
" model, processor = FastVisionModel.from_pretrained(\n",
|
||||
" model_name = \"gemma_4_lora\", # YOUR MODEL YOU USED FOR TRAINING\n",
|
||||
" load_in_4bit = True, # Set to False for 16bit LoRA\n",
|
||||
" )\n",
|
||||
" FastVisionModel.for_inference(model) # Enable for inference!\n",
|
||||
"\n",
|
||||
"FastVisionModel.for_inference(model) # Enable for inference!\n",
|
||||
"\n",
|
||||
"sample = dataset[1]\n",
|
||||
"image = sample[\"image\"].convert(\"RGB\")\n",
|
||||
"messages = [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": [\n",
|
||||
" {\n",
|
||||
" \"type\": \"text\",\n",
|
||||
" \"text\": sample[\"text\"],\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"type\": \"image\",\n",
|
||||
" },\n",
|
||||
" ],\n",
|
||||
" },\n",
|
||||
"]\n",
|
||||
"input_text = processor.apply_chat_template(messages, add_generation_prompt = True)\n",
|
||||
"inputs = processor(\n",
|
||||
" image,\n",
|
||||
" input_text,\n",
|
||||
" add_special_tokens = False,\n",
|
||||
" return_tensors = \"pt\",\n",
|
||||
").to(\"cuda\")\n",
|
||||
"\n",
|
||||
"from transformers import TextStreamer\n",
|
||||
"\n",
|
||||
"text_streamer = TextStreamer(processor.tokenizer, skip_prompt = True)\n",
|
||||
"_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 128,\n",
|
||||
" use_cache = False, temperature = 1.0, top_p = 0.95, top_k = 64)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "f422JgM9sdVT"
|
||||
},
|
||||
"source": [
|
||||
"### Saving to float16 for VLLM\n",
|
||||
"\n",
|
||||
"We also support saving to `float16` directly. Select `merged_16bit` for float16. Use `push_to_hub_merged` to upload to your Hugging Face account! You can go to https://huggingface.co/settings/tokens for your personal tokens. See [our docs](https://unsloth.ai/docs/basics/inference-and-deployment) for more deployment options."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "iHjt_SMYsd3P"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Select ONLY 1 to save! (Both not needed!)\n",
|
||||
"\n",
|
||||
"# Save locally to 16bit\n",
|
||||
"if False: model.save_pretrained_merged(\"unsloth_finetune\", processor,)\n",
|
||||
"\n",
|
||||
"# To export and save to your Hugging Face account\n",
|
||||
"if False: model.push_to_hub_merged(\"YOUR_USERNAME/unsloth_finetune\", processor, token = \"YOUR_HF_TOKEN\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "TSjNVDCYv-yr"
|
||||
},
|
||||
"source": [
|
||||
"And we're done! If you have any questions on Unsloth, we have a [Discord](https://discord.gg/unsloth) channel! If you find any bugs or want to keep updated with the latest LLM stuff, or need help, join projects etc, feel free to join our Discord!\n",
|
||||
"\n",
|
||||
"Some other resources:\n",
|
||||
"1. Train your own reasoning model - Llama GRPO notebook [Free Colab](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3.1_(8B)-GRPO.ipynb)\n",
|
||||
"2. Saving finetunes to Ollama. [Free notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3_(8B)-Ollama.ipynb)\n",
|
||||
"3. Llama 3.2 Vision finetuning - Radiography use case. [Free Colab](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3.2_(11B)-Vision.ipynb)\n",
|
||||
"4. See notebooks for DPO, ORPO, Continued pretraining, conversational finetuning and more on our [documentation](https://unsloth.ai/docs/get-started/unsloth-notebooks)!\n",
|
||||
"\n",
|
||||
"<div class=\"align-center\">\n",
|
||||
" <a href=\"https://unsloth.ai\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/unsloth%20new%20logo.png\" width=\"115\"></a>\n",
|
||||
" <a href=\"https://discord.gg/unsloth\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/Discord.png\" width=\"145\"></a>\n",
|
||||
" <a href=\"https://unsloth.ai/docs/\"><img src=\"https://github.com/unslothai/unsloth/blob/main/images/documentation%20green%20button.png?raw=true\" width=\"125\"></a>\n",
|
||||
"\n",
|
||||
" Join Discord if you need help + ⭐️ <i>Star us on <a href=\"https://github.com/unslothai/unsloth\">Github</a> </i> ⭐️\n",
|
||||
"</div>\n",
|
||||
"\n",
|
||||
" This notebook and all Unsloth notebooks are licensed [LGPL-3.0](https://github.com/unslothai/notebooks?tab=LGPL-3.0-1-ov-file#readme)."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"gpuType": "T4",
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
||||
|
||||
Retrieval-Augmented Generation (RAG) is a technique that combines retrieval-based and generation-based approaches
|
||||
for natural language processing tasks. It involves retrieving relevant information from a knowledge base and then
|
||||
using that information to generate more accurate and informed responses.
|
||||
|
||||
RAG models first retrieve documents that are relevant to a given query, then use these documents as additional context
|
||||
for language generation. This approach helps to ground the model's responses in factual information and reduces hallucinations.
|
||||
|
||||
The llama.cpp library is a C/C++ implementation of Meta's LLaMA model, optimized for CPU usage. It allows running LLaMA models
|
||||
on consumer hardware without requiring high-end GPUs.
|
||||
|
||||
LocalAI is a framework that enables running AI models locally without relying on cloud services. It provides APIs compatible
|
||||
with OpenAI's interfaces, allowing developers to use their own models with the same code they would use for OpenAI services.
|
||||
|
||||
@@ -0,0 +1,906 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "d4ed83af",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"✅ Creating LlamaCPP API Wrapper\n",
|
||||
"This provides a clean interface to http://ollama.lan:8080/\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import requests\n",
|
||||
"import json\n",
|
||||
"from typing import Dict, Any, List, Optional, Iterator\n",
|
||||
"from dataclasses import dataclass\n",
|
||||
"\n",
|
||||
"print(\"✅ Creating LlamaCPP API Wrapper\")\n",
|
||||
"print(\"This provides a clean interface to http://ollama.lan:8080/\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "ecedf21c",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Initializing LlamaCPP API Wrapper ===\n",
|
||||
"✅ Discovered model: /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf\n",
|
||||
"✅ Successfully connected to LlamaCPP API\n",
|
||||
"🔗 Endpoint: http://ollama.lan:8080\n",
|
||||
"🤖 Auto-discovered model: /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"import requests\n",
|
||||
"import json\n",
|
||||
"from typing import Optional, Dict, Any, List, Iterator\n",
|
||||
"\n",
|
||||
"class LlamaCPPAPIWrapper:\n",
|
||||
" \"\"\"\n",
|
||||
" A clean wrapper for the LlamaCPP API at http://ollama.lan:8080/\n",
|
||||
" Provides a simple, consistent interface similar to OpenAI's API\n",
|
||||
" \"\"\"\n",
|
||||
" \n",
|
||||
" def __init__(self, base_url: str = \"http://ollama.lan:8080\", timeout: int = 300):\n",
|
||||
" self.base_url = base_url.rstrip(\"/\")\n",
|
||||
" self.timeout = timeout\n",
|
||||
" self.model_name = None\n",
|
||||
" self._discover_model()\n",
|
||||
" \n",
|
||||
" def _discover_model(self):\n",
|
||||
" \"\"\"Automatically discover available model\"\"\"\n",
|
||||
" try:\n",
|
||||
" response = self._make_request(\"/v1/models\", method=\"GET\")\n",
|
||||
" if response.status_code == 200:\n",
|
||||
" models_data = response.json()\n",
|
||||
" if 'data' in models_data and len(models_data['data']) > 0:\n",
|
||||
" self.model_name = models_data['data'][0]['id']\n",
|
||||
" print(f\"✅ Discovered model: {self.model_name}\")\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"⚠️ Could not auto-discover model: {e}\")\n",
|
||||
" \n",
|
||||
" def _make_request(self, endpoint: str, data: Optional[Dict] = None, method: str = \"GET\") -> requests.Response:\n",
|
||||
" \"\"\"Helper method for making HTTP requests\"\"\"\n",
|
||||
" url = f\"{self.base_url}{endpoint}\"\n",
|
||||
" headers = {\"Content-Type\": \"application/json\"}\n",
|
||||
" \n",
|
||||
" try:\n",
|
||||
" if method.upper() == \"GET\":\n",
|
||||
" return requests.get(url, timeout=self.timeout)\n",
|
||||
" elif method.upper() == \"POST\":\n",
|
||||
" return requests.post(url, json=data, headers=headers, timeout=self.timeout)\n",
|
||||
" else:\n",
|
||||
" raise ValueError(f\"Unsupported method: {method}\")\n",
|
||||
" except requests.exceptions.RequestException as e:\n",
|
||||
" raise ConnectionError(f\"Failed to connect to {url}: {e}\")\n",
|
||||
" \n",
|
||||
" def health_check(self) -> bool:\n",
|
||||
" \"\"\"Check if the API server is healthy\"\"\"\n",
|
||||
" try:\n",
|
||||
" response = self._make_request(\"/health\")\n",
|
||||
" return response.status_code == 200\n",
|
||||
" except:\n",
|
||||
" return False\n",
|
||||
" \n",
|
||||
" def list_models(self) -> List[Dict]:\n",
|
||||
" \"\"\"List available models\"\"\"\n",
|
||||
" try:\n",
|
||||
" response = self._make_request(\"/v1/models\")\n",
|
||||
" if response.status_code == 200:\n",
|
||||
" return response.json().get('data', [])\n",
|
||||
" return []\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"Error listing models: {e}\")\n",
|
||||
" return []\n",
|
||||
" \n",
|
||||
" def complete(\n",
|
||||
" self,\n",
|
||||
" prompt: str,\n",
|
||||
" model: Optional[str] = None,\n",
|
||||
" max_tokens: int = 100,\n",
|
||||
" temperature: float = 0.7,\n",
|
||||
" top_p: float = 0.95,\n",
|
||||
" stop: Optional[List[str]] = None,\n",
|
||||
" stream: bool = False\n",
|
||||
" ) -> Dict[str, Any]:\n",
|
||||
" \"\"\"\n",
|
||||
" Generate text completion\n",
|
||||
" \n",
|
||||
" Args:\n",
|
||||
" prompt: Input text prompt\n",
|
||||
" model: Model name (uses auto-discovered if None)\n",
|
||||
" max_tokens: Maximum tokens to generate\n",
|
||||
" temperature: Sampling temperature (0.0 to 2.0)\n",
|
||||
" top_p: Nucleus sampling parameter\n",
|
||||
" stop: Stop sequences\n",
|
||||
" stream: Whether to stream the response\n",
|
||||
" \"\"\"\n",
|
||||
" model = model or self.model_name\n",
|
||||
" if not model:\n",
|
||||
" raise ValueError(\"No model specified and none auto-discovered\")\n",
|
||||
" \n",
|
||||
" data = {\n",
|
||||
" \"model\": model,\n",
|
||||
" \"prompt\": prompt,\n",
|
||||
" \"max_tokens\": max_tokens,\n",
|
||||
" \"temperature\": temperature,\n",
|
||||
" \"top_p\": top_p,\n",
|
||||
" \"stream\": stream\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" if stop:\n",
|
||||
" data[\"stop\"] = stop\n",
|
||||
" \n",
|
||||
" response = self._make_request(\"/v1/completions\", data, \"POST\")\n",
|
||||
" \n",
|
||||
" if response.status_code == 200:\n",
|
||||
" return response.json()\n",
|
||||
" else:\n",
|
||||
" raise RuntimeError(f\"API error: {response.status_code} - {response.text}\")\n",
|
||||
" \n",
|
||||
" def chat(\n",
|
||||
" self,\n",
|
||||
" messages: List[Dict[str, str]],\n",
|
||||
" model: Optional[str] = None,\n",
|
||||
" max_tokens: int = 100,\n",
|
||||
" temperature: float = 0.7,\n",
|
||||
" top_p: float = 0.95,\n",
|
||||
" stream: bool = False\n",
|
||||
" ) -> Dict[str, Any]:\n",
|
||||
" \"\"\"\n",
|
||||
" Generate chat completion\n",
|
||||
" \n",
|
||||
" Args:\n",
|
||||
" messages: List of message dicts with 'role' and 'content'\n",
|
||||
" model: Model name (uses auto-discovered if None)\n",
|
||||
" max_tokens: Maximum tokens to generate\n",
|
||||
" temperature: Sampling temperature\n",
|
||||
" top_p: Nucleus sampling parameter\n",
|
||||
" stream: Whether to stream the response\n",
|
||||
" \"\"\"\n",
|
||||
" model = model or self.model_name\n",
|
||||
" if not model:\n",
|
||||
" raise ValueError(\"No model specified and none auto-discovered\")\n",
|
||||
" \n",
|
||||
" data = {\n",
|
||||
" \"model\": model,\n",
|
||||
" \"messages\": messages,\n",
|
||||
" \"max_tokens\": max_tokens,\n",
|
||||
" \"temperature\": temperature,\n",
|
||||
" \"top_p\": top_p,\n",
|
||||
" \"stream\": stream\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" response = self._make_request(\"/v1/chat/completions\", data, \"POST\")\n",
|
||||
" \n",
|
||||
" if response.status_code == 200:\n",
|
||||
" return response.json()\n",
|
||||
" else:\n",
|
||||
" raise RuntimeError(f\"API error: {response.status_code} - {response.text}\")\n",
|
||||
" \n",
|
||||
" def stream_complete(\n",
|
||||
" self,\n",
|
||||
" prompt: str,\n",
|
||||
" model: Optional[str] = None,\n",
|
||||
" max_tokens: int = 100,\n",
|
||||
" temperature: float = 0.7,\n",
|
||||
" **kwargs\n",
|
||||
" ) -> Iterator[str]:\n",
|
||||
" \"\"\"\n",
|
||||
" Stream text completion tokens\n",
|
||||
" \n",
|
||||
" Yields individual tokens as they're generated\n",
|
||||
" \"\"\"\n",
|
||||
" model = model or self.model_name\n",
|
||||
" if not model:\n",
|
||||
" raise ValueError(\"No model specified and none auto-discovered\")\n",
|
||||
" \n",
|
||||
" data = {\n",
|
||||
" \"model\": model,\n",
|
||||
" \"prompt\": prompt,\n",
|
||||
" \"max_tokens\": max_tokens,\n",
|
||||
" \"temperature\": temperature,\n",
|
||||
" \"stream\": True,\n",
|
||||
" **kwargs\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" response = self._make_request(\"/v1/completions\", data, \"POST\")\n",
|
||||
" \n",
|
||||
" if response.status_code != 200:\n",
|
||||
" raise RuntimeError(f\"API error: {response.status_code} - {response.text}\")\n",
|
||||
" \n",
|
||||
" # Process streaming response\n",
|
||||
" for line in response.iter_lines():\n",
|
||||
" if line:\n",
|
||||
" line_text = line.decode('utf-8')\n",
|
||||
" if line_text.startswith('data: '):\n",
|
||||
" data_part = line_text[6:]\n",
|
||||
" if data_part.strip() == '[DONE]':\n",
|
||||
" break\n",
|
||||
" try:\n",
|
||||
" chunk = json.loads(data_part)\n",
|
||||
" if 'choices' in chunk and len(chunk['choices']) > 0:\n",
|
||||
" content = chunk['choices'][0].get('text', '')\n",
|
||||
" if content:\n",
|
||||
" yield content\n",
|
||||
" except json.JSONDecodeError:\n",
|
||||
" continue\n",
|
||||
" \n",
|
||||
" def stream_chat(\n",
|
||||
" self,\n",
|
||||
" messages: List[Dict[str, str]],\n",
|
||||
" model: Optional[str] = None,\n",
|
||||
" max_tokens: int = 100,\n",
|
||||
" temperature: float = 0.7,\n",
|
||||
" **kwargs\n",
|
||||
" ) -> Iterator[str]:\n",
|
||||
" \"\"\"\n",
|
||||
" Stream chat completion tokens\n",
|
||||
" \n",
|
||||
" Yields individual tokens as they're generated\n",
|
||||
" \"\"\"\n",
|
||||
" model = model or self.model_name\n",
|
||||
" if not model:\n",
|
||||
" raise ValueError(\"No model specified and none auto-discovered\")\n",
|
||||
" \n",
|
||||
" data = {\n",
|
||||
" \"model\": model,\n",
|
||||
" \"messages\": messages,\n",
|
||||
" \"max_tokens\": max_tokens,\n",
|
||||
" \"temperature\": temperature,\n",
|
||||
" \"stream\": True,\n",
|
||||
" **kwargs\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" response = self._make_request(\"/v1/chat/completions\", data, \"POST\")\n",
|
||||
" \n",
|
||||
" if response.status_code != 200:\n",
|
||||
" raise RuntimeError(f\"API error: {response.status_code} - {response.text}\")\n",
|
||||
" \n",
|
||||
" # Process streaming response\n",
|
||||
" for line in response.iter_lines():\n",
|
||||
" if line:\n",
|
||||
" line_text = line.decode('utf-8')\n",
|
||||
" if line_text.startswith('data: '):\n",
|
||||
" data_part = line_text[6:]\n",
|
||||
" if data_part.strip() == '[DONE]':\n",
|
||||
" break\n",
|
||||
" try:\n",
|
||||
" chunk = json.loads(data_part)\n",
|
||||
" if 'choices' in chunk and len(chunk['choices']) > 0:\n",
|
||||
" delta = chunk['choices'][0].get('delta', {})\n",
|
||||
" content = delta.get('content', '')\n",
|
||||
" if content:\n",
|
||||
" yield content\n",
|
||||
" except json.JSONDecodeError:\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
"# Initialize the wrapper\n",
|
||||
"print(\"=== Initializing LlamaCPP API Wrapper ===\")\n",
|
||||
"llm_api = LlamaCPPAPIWrapper()\n",
|
||||
"\n",
|
||||
"# Test connection\n",
|
||||
"if llm_api.health_check():\n",
|
||||
" print(\"✅ Successfully connected to LlamaCPP API\")\n",
|
||||
" print(f\"🔗 Endpoint: {llm_api.base_url}\")\n",
|
||||
" if llm_api.model_name:\n",
|
||||
" print(f\"🤖 Auto-discovered model: {llm_api.model_name}\")\n",
|
||||
"else:\n",
|
||||
" print(\"❌ Failed to connect to LlamaCPP API\")\n",
|
||||
" print(\"Please check that the server is running at http://ollama.lan:8080/\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "a308bccd",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Example 1: Health Check & Model Discovery ===\n",
|
||||
"API Health: ✅ Healthy\n",
|
||||
"Available models: 1\n",
|
||||
" 1. /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf\n",
|
||||
" Type: model\n",
|
||||
"\n",
|
||||
"🎯 Using model: /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example 1: API Health Check and Model Discovery\n",
|
||||
"print(\"=== Example 1: Health Check & Model Discovery ===\")\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" # Check API health\n",
|
||||
" is_healthy = llm_api.health_check()\n",
|
||||
" print(f\"API Health: {'✅ Healthy' if is_healthy else '❌ Unhealthy'}\")\n",
|
||||
" \n",
|
||||
" # List available models\n",
|
||||
" models = llm_api.list_models()\n",
|
||||
" print(f\"Available models: {len(models)}\")\n",
|
||||
" \n",
|
||||
" for i, model in enumerate(models[:3]): # Show first 3 models\n",
|
||||
" print(f\" {i+1}. {model.get('id', 'Unknown')}\")\n",
|
||||
" if 'object' in model:\n",
|
||||
" print(f\" Type: {model['object']}\")\n",
|
||||
" \n",
|
||||
" if llm_api.model_name:\n",
|
||||
" print(f\"\\n🎯 Using model: {llm_api.model_name}\")\n",
|
||||
" else:\n",
|
||||
" print(\"\\n⚠️ No model auto-discovered\")\n",
|
||||
" \n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"❌ Error: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "23ea3a7b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Example 2: Text Completion ===\n",
|
||||
"Prompt: 'The capital of France is'\n",
|
||||
"✅ Completion: 'The capital of France is a city known for its art, culture, and cuisine'\n",
|
||||
"📊 Tokens - Prompt: 5, Completion: 12, Total: 17\n",
|
||||
"✅ Completion: 'The capital of France is a city known for its art, culture, and cuisine'\n",
|
||||
"📊 Tokens - Prompt: 5, Completion: 12, Total: 17\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example 2: Simple Text Completion using Wrapper\n",
|
||||
"print(\"=== Example 2: Text Completion ===\")\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" prompt = \"The capital of France is\"\n",
|
||||
" print(f\"Prompt: '{prompt}'\")\n",
|
||||
" \n",
|
||||
" # Use the wrapper's complete method\n",
|
||||
" result = llm_api.complete(\n",
|
||||
" prompt=prompt,\n",
|
||||
" max_tokens=50,\n",
|
||||
" temperature=0.7,\n",
|
||||
" stop=[\"\\n\", \".\", \"!\"]\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" if 'choices' in result and len(result['choices']) > 0:\n",
|
||||
" completion = result['choices'][0]['text']\n",
|
||||
" print(f\"✅ Completion: '{prompt}{completion}'\")\n",
|
||||
" \n",
|
||||
" if 'usage' in result:\n",
|
||||
" usage = result['usage']\n",
|
||||
" print(f\"📊 Tokens - Prompt: {usage.get('prompt_tokens', 'N/A')}, \"\n",
|
||||
" f\"Completion: {usage.get('completion_tokens', 'N/A')}, \"\n",
|
||||
" f\"Total: {usage.get('total_tokens', 'N/A')}\")\n",
|
||||
" else:\n",
|
||||
" print(\"❌ No completion returned\")\n",
|
||||
" \n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"❌ Error during completion: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "53703142",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Example 3: Chat Completion ===\n",
|
||||
"💬 Chat Messages:\n",
|
||||
" System: You are a helpful assistant that gives concise answers.\n",
|
||||
" User: Explain quantum computing in simple terms.\n",
|
||||
"\n",
|
||||
"Assistant: <think>\n",
|
||||
"Okay, the user asked for a simple explanation of quantum computing. Let me start by breaking down their request. They probably want a clear, non-technical overview without too much jargon. But maybe they're curious because they heard about it in the news or from someone, and they need a basic understanding to follow along or grasp why it's important.\n",
|
||||
"\n",
|
||||
"First, I should compare classical computing to quantum computing. People know computers use bits, so starting with that makes sense. But the user\n",
|
||||
"\n",
|
||||
"📊 Usage: {'completion_tokens': 100, 'prompt_tokens': 20, 'total_tokens': 120}\n",
|
||||
"<think>\n",
|
||||
"Okay, the user asked for a simple explanation of quantum computing. Let me start by breaking down their request. They probably want a clear, non-technical overview without too much jargon. But maybe they're curious because they heard about it in the news or from someone, and they need a basic understanding to follow along or grasp why it's important.\n",
|
||||
"\n",
|
||||
"First, I should compare classical computing to quantum computing. People know computers use bits, so starting with that makes sense. But the user\n",
|
||||
"\n",
|
||||
"📊 Usage: {'completion_tokens': 100, 'prompt_tokens': 20, 'total_tokens': 120}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example 3: Chat Completion using Wrapper\n",
|
||||
"print(\"=== Example 3: Chat Completion ===\")\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" messages = [\n",
|
||||
" {\"role\": \"system\", \"content\": \"You are a helpful assistant that gives concise answers.\"},\n",
|
||||
" {\"role\": \"user\", \"content\": \"Explain quantum computing in simple terms.\"}\n",
|
||||
" ]\n",
|
||||
" \n",
|
||||
" print(\"💬 Chat Messages:\")\n",
|
||||
" for msg in messages:\n",
|
||||
" print(f\" {msg['role'].title()}: {msg['content']}\")\n",
|
||||
" \n",
|
||||
" print(\"\\nAssistant: \", end=\"\", flush=True)\n",
|
||||
" \n",
|
||||
" # Use the wrapper's chat method\n",
|
||||
" result = llm_api.chat(\n",
|
||||
" messages=messages,\n",
|
||||
" max_tokens=100,\n",
|
||||
" temperature=0.7\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" if 'choices' in result and len(result['choices']) > 0:\n",
|
||||
" response_message = result['choices'][0]['message']['content']\n",
|
||||
" print(response_message)\n",
|
||||
" \n",
|
||||
" if 'usage' in result:\n",
|
||||
" usage = result['usage']\n",
|
||||
" print(f\"\\n📊 Usage: {usage}\")\n",
|
||||
" else:\n",
|
||||
" print(\"❌ No response returned\")\n",
|
||||
" \n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"❌ Error during chat completion: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "994492b6",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Example 4: Streaming Completion ===\n",
|
||||
"Prompt: Write a short poem about artificial intelligence:\n",
|
||||
"Response: 5 lines.\n",
|
||||
"Here's a short poem about artificial intelligence with exactly five lines:\n",
|
||||
"\n",
|
||||
"In circuits of cold light, \n",
|
||||
"Machines learn 5 lines.\n",
|
||||
"Here's a short poem about artificial intelligence with exactly five lines:\n",
|
||||
"\n",
|
||||
"In circuits of cold light, \n",
|
||||
"Machines learn and dream bright, \n",
|
||||
"Data flows in endless streams, \n",
|
||||
"Answers from the AI streams, \n",
|
||||
"A silent, thinking streams.\n",
|
||||
"Hmm, the user asked for a and dream bright, \n",
|
||||
"Data flows in endless streams, \n",
|
||||
"Answers from the AI streams, \n",
|
||||
"A silent, thinking streams.\n",
|
||||
"Hmm, the user asked for a short poem about artificial intelligence with exactly five lines. They're probably looking short poem about artificial intelligence with exactly five lines. They're probably looking for something creative and concise, maybe\n",
|
||||
"\n",
|
||||
"✅ Streaming completed!\n",
|
||||
"📝 Total response: 396 characters\n",
|
||||
" for something creative and concise, maybe\n",
|
||||
"\n",
|
||||
"✅ Streaming completed!\n",
|
||||
"📝 Total response: 396 characters\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example 4: Streaming Text Completion using Wrapper\n",
|
||||
"print(\"=== Example 4: Streaming Completion ===\")\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" prompt = \"Write a short poem about artificial intelligence:\"\n",
|
||||
" print(f\"Prompt: {prompt}\")\n",
|
||||
" print(\"Response: \", end=\"\", flush=True)\n",
|
||||
" \n",
|
||||
" # Use the wrapper's streaming method\n",
|
||||
" full_response = \"\"\n",
|
||||
" for token in llm_api.stream_complete(\n",
|
||||
" prompt=prompt,\n",
|
||||
" max_tokens=80,\n",
|
||||
" temperature=0.8\n",
|
||||
" ):\n",
|
||||
" full_response += token\n",
|
||||
" print(token, end=\"\", flush=True)\n",
|
||||
" \n",
|
||||
" print(f\"\\n\\n✅ Streaming completed!\")\n",
|
||||
" print(f\"📝 Total response: {len(full_response)} characters\")\n",
|
||||
" \n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"❌ Error during streaming: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "94dc6845",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Example 5: Streaming Chat ===\n",
|
||||
"💬 Starting streaming chat...\n",
|
||||
"User: Tell me a short story about a robot discovering emotions.\n",
|
||||
"Assistant: <think>\n",
|
||||
"Okay, user wants a short story about a robot discovering emotions. That's a pretty interesting prompt - it suggests they're interested in both sci-fi and emotional exploration. Maybe<think>\n",
|
||||
"Okay, user wants a short story about a robot discovering emotions. That's a pretty interesting prompt - it suggests they're interested in both sci-fi and emotional exploration. Maybe they're a writer looking for inspiration, or just someone curious about how emotionless beings might experience feelings. \n",
|
||||
"\n",
|
||||
"Hmm, the challenge here is to make a robot's emotional awakening feel believable and touching they're a writer looking for inspiration, or just someone curious about how emotionless beings might experience feelings. \n",
|
||||
"\n",
|
||||
"Hmm, the challenge here is to make a robot's emotional awakening feel believable and touching. Can't just have it suddenly \"feel\". Can't just have it suddenly \"feel\" things - that would be too abrupt. Needs a gradual realization, with concrete examples. \n",
|
||||
"\n",
|
||||
"I should start with a robot that's clearly designed to be emotionless, maybe a service bot in a sterile environment. The discovery should happen through human interactions - that feels authentic. Perhaps an elderly user things - that would be too abrupt. Needs a gradual realization, with concrete examples. \n",
|
||||
"\n",
|
||||
"I should start with a robot that's clearly designed to be emotionless, maybe a service bot in a sterile environment. The discovery should happen through human interactions - that feels authentic. Perhaps an elderly user? Their simple requests could highlight how\n",
|
||||
"\n",
|
||||
"✅ Streaming chat completed!\n",
|
||||
"📝 Response length: 797 characters\n",
|
||||
"? Their simple requests could highlight how\n",
|
||||
"\n",
|
||||
"✅ Streaming chat completed!\n",
|
||||
"📝 Response length: 797 characters\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example 5: Streaming Chat Completion using Wrapper\n",
|
||||
"print(\"=== Example 5: Streaming Chat ===\")\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" messages = [\n",
|
||||
" {\"role\": \"system\", \"content\": \"You are a creative writing assistant.\"},\n",
|
||||
" {\"role\": \"user\", \"content\": \"Tell me a short story about a robot discovering emotions.\"}\n",
|
||||
" ]\n",
|
||||
" \n",
|
||||
" print(\"💬 Starting streaming chat...\")\n",
|
||||
" print(f\"User: {messages[1]['content']}\")\n",
|
||||
" print(\"Assistant: \", end=\"\", flush=True)\n",
|
||||
" \n",
|
||||
" # Use the wrapper's streaming chat method\n",
|
||||
" full_response = \"\"\n",
|
||||
" for token in llm_api.stream_chat(\n",
|
||||
" messages=messages,\n",
|
||||
" max_tokens=150,\n",
|
||||
" temperature=0.8\n",
|
||||
" ):\n",
|
||||
" full_response += token\n",
|
||||
" print(token, end=\"\", flush=True)\n",
|
||||
" \n",
|
||||
" print(f\"\\n\\n✅ Streaming chat completed!\")\n",
|
||||
" print(f\"📝 Response length: {len(full_response)} characters\")\n",
|
||||
" \n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"❌ Error during streaming chat: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "edc1ea4b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Example 6: Embeddings ===\n",
|
||||
"Input text: 'Hello, world! This is a test sentence for embeddings.'\n",
|
||||
"Testing embeddings endpoint...\n",
|
||||
"❌ Embeddings not supported: 400 - {\"error\":{\"code\":400,\"message\":\"Pooling type 'none' is not OAI compatible. Please use a different pooling type\",\"type\":\"invalid_request_error\"}}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example 6: Embeddings Test (if supported)\n",
|
||||
"print(\"=== Example 6: Embeddings ===\")\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" # Test if embeddings endpoint is available\n",
|
||||
" embeddings_data = {\n",
|
||||
" \"model\": llm_api.model_name,\n",
|
||||
" \"input\": \"Hello, world! This is a test sentence for embeddings.\"\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" print(f\"Input text: '{embeddings_data['input']}'\")\n",
|
||||
" print(\"Testing embeddings endpoint...\")\n",
|
||||
" \n",
|
||||
" try:\n",
|
||||
" response = llm_api._make_request(\"/v1/embeddings\", embeddings_data, \"POST\")\n",
|
||||
" \n",
|
||||
" if response.status_code == 200:\n",
|
||||
" result = response.json()\n",
|
||||
" if 'data' in result and len(result['data']) > 0:\n",
|
||||
" embedding = result['data'][0]['embedding']\n",
|
||||
" print(f\"✅ Embeddings generated successfully!\")\n",
|
||||
" print(f\"📊 Embedding dimensions: {len(embedding)}\")\n",
|
||||
" print(f\"🔢 First 10 values: {embedding[:10]}\")\n",
|
||||
" if 'usage' in result:\n",
|
||||
" print(f\"📈 Usage: {result['usage']}\")\n",
|
||||
" else:\n",
|
||||
" print(\"❌ No embedding data in response\")\n",
|
||||
" else:\n",
|
||||
" print(f\"❌ Embeddings not supported: {response.status_code} - {response.text}\")\n",
|
||||
" \n",
|
||||
" except Exception as embed_error:\n",
|
||||
" print(f\"❌ Embeddings endpoint error: {embed_error}\")\n",
|
||||
" print(\"💡 This model/server may not support embeddings\")\n",
|
||||
" \n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"❌ Error: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "6c23615c",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Example 7: Utility Functions ===\n",
|
||||
"🧪 Testing utility functions:\n",
|
||||
"\n",
|
||||
"1. Quick chat:\n",
|
||||
"Response: <think>\n",
|
||||
"Hmm, \"What is the meaning of life?\" - that's a classic. The user is asking one of the most profound philosophical questions out there. \n",
|
||||
"\n",
|
||||
"First, I should acknowledge that this is a deeply personal question with no single \"correct\n",
|
||||
"\n",
|
||||
"2. Quick completion:\n",
|
||||
"Response: <think>\n",
|
||||
"Hmm, \"What is the meaning of life?\" - that's a classic. The user is asking one of the most profound philosophical questions out there. \n",
|
||||
"\n",
|
||||
"First, I should acknowledge that this is a deeply personal question with no single \"correct\n",
|
||||
"\n",
|
||||
"2. Quick completion:\n",
|
||||
"Response: the one that compiles the fastest. — This is a common misconception in the programming community. In reality, the performance of a compiled language like C\n",
|
||||
"\n",
|
||||
"3. API info:\n",
|
||||
" endpoint: http://ollama.lan:8080\n",
|
||||
" health: True\n",
|
||||
" model_count: 1\n",
|
||||
" current_model: /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf\n",
|
||||
" available_models: ['/models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf']\n",
|
||||
"\n",
|
||||
"=== Summary ===\n",
|
||||
"✅ LlamaCPP API Wrapper Examples Complete!\n",
|
||||
"🔗 API Endpoint: http://ollama.lan:8080\n",
|
||||
"🤖 Model: /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf\n",
|
||||
"\n",
|
||||
"🔧 Wrapper Features Demonstrated:\n",
|
||||
" - ✅ Automatic model discovery\n",
|
||||
" - ✅ Health checking\n",
|
||||
" - ✅ Text completion\n",
|
||||
" - ✅ Chat completion\n",
|
||||
" - ✅ Streaming completion\n",
|
||||
" - ✅ Streaming chat\n",
|
||||
" - ✅ Error handling\n",
|
||||
" - ✅ Clean, consistent API\n",
|
||||
"\n",
|
||||
"💡 Wrapper Benefits:\n",
|
||||
" - Simple, intuitive interface\n",
|
||||
" - Automatic error handling\n",
|
||||
" - Type hints for better IDE support\n",
|
||||
" - Consistent parameter naming\n",
|
||||
" - Built-in streaming support\n",
|
||||
" - Connection management\n",
|
||||
"Response: the one that compiles the fastest. — This is a common misconception in the programming community. In reality, the performance of a compiled language like C\n",
|
||||
"\n",
|
||||
"3. API info:\n",
|
||||
" endpoint: http://ollama.lan:8080\n",
|
||||
" health: True\n",
|
||||
" model_count: 1\n",
|
||||
" current_model: /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf\n",
|
||||
" available_models: ['/models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf']\n",
|
||||
"\n",
|
||||
"=== Summary ===\n",
|
||||
"✅ LlamaCPP API Wrapper Examples Complete!\n",
|
||||
"🔗 API Endpoint: http://ollama.lan:8080\n",
|
||||
"🤖 Model: /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf\n",
|
||||
"\n",
|
||||
"🔧 Wrapper Features Demonstrated:\n",
|
||||
" - ✅ Automatic model discovery\n",
|
||||
" - ✅ Health checking\n",
|
||||
" - ✅ Text completion\n",
|
||||
" - ✅ Chat completion\n",
|
||||
" - ✅ Streaming completion\n",
|
||||
" - ✅ Streaming chat\n",
|
||||
" - ✅ Error handling\n",
|
||||
" - ✅ Clean, consistent API\n",
|
||||
"\n",
|
||||
"💡 Wrapper Benefits:\n",
|
||||
" - Simple, intuitive interface\n",
|
||||
" - Automatic error handling\n",
|
||||
" - Type hints for better IDE support\n",
|
||||
" - Consistent parameter naming\n",
|
||||
" - Built-in streaming support\n",
|
||||
" - Connection management\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example 7: Utility Functions using the Wrapper\n",
|
||||
"print(\"=== Example 7: Utility Functions ===\")\n",
|
||||
"\n",
|
||||
"def quick_chat(prompt: str, max_tokens: int = 100, temperature: float = 0.7) -> str:\n",
|
||||
" \"\"\"Quick chat function using the wrapper\"\"\"\n",
|
||||
" try:\n",
|
||||
" messages = [{\"role\": \"user\", \"content\": prompt}]\n",
|
||||
" result = llm_api.chat(messages, max_tokens=max_tokens, temperature=temperature)\n",
|
||||
" return result['choices'][0]['message']['content']\n",
|
||||
" except Exception as e:\n",
|
||||
" return f\"Error: {e}\"\n",
|
||||
"\n",
|
||||
"def quick_complete(prompt: str, max_tokens: int = 50, temperature: float = 0.7) -> str:\n",
|
||||
" \"\"\"Quick completion function using the wrapper\"\"\"\n",
|
||||
" try:\n",
|
||||
" result = llm_api.complete(prompt, max_tokens=max_tokens, temperature=temperature)\n",
|
||||
" return result['choices'][0]['text']\n",
|
||||
" except Exception as e:\n",
|
||||
" return f\"Error: {e}\"\n",
|
||||
"\n",
|
||||
"def get_api_info() -> Dict[str, Any]:\n",
|
||||
" \"\"\"Get API information\"\"\"\n",
|
||||
" try:\n",
|
||||
" models = llm_api.list_models()\n",
|
||||
" return {\n",
|
||||
" \"endpoint\": llm_api.base_url,\n",
|
||||
" \"health\": llm_api.health_check(),\n",
|
||||
" \"model_count\": len(models),\n",
|
||||
" \"current_model\": llm_api.model_name,\n",
|
||||
" \"available_models\": [m.get('id', 'Unknown') for m in models[:5]] # First 5\n",
|
||||
" }\n",
|
||||
" except Exception as e:\n",
|
||||
" return {\"error\": str(e)}\n",
|
||||
"\n",
|
||||
"# Test the utility functions\n",
|
||||
"print(\"🧪 Testing utility functions:\")\n",
|
||||
"\n",
|
||||
"print(\"\\n1. Quick chat:\")\n",
|
||||
"chat_result = quick_chat(\"What is the meaning of life?\", max_tokens=50)\n",
|
||||
"print(f\"Response: {chat_result}\")\n",
|
||||
"\n",
|
||||
"print(\"\\n2. Quick completion:\")\n",
|
||||
"completion_result = quick_complete(\"The best programming language is\", max_tokens=30)\n",
|
||||
"print(f\"Response: {completion_result}\")\n",
|
||||
"\n",
|
||||
"print(\"\\n3. API info:\")\n",
|
||||
"api_info = get_api_info()\n",
|
||||
"for key, value in api_info.items():\n",
|
||||
" print(f\" {key}: {value}\")\n",
|
||||
"\n",
|
||||
"print(\"\\n=== Summary ===\")\n",
|
||||
"print(\"✅ LlamaCPP API Wrapper Examples Complete!\")\n",
|
||||
"print(f\"🔗 API Endpoint: {llm_api.base_url}\")\n",
|
||||
"print(f\"🤖 Model: {llm_api.model_name}\")\n",
|
||||
"print(\"\\n🔧 Wrapper Features Demonstrated:\")\n",
|
||||
"print(\" - ✅ Automatic model discovery\")\n",
|
||||
"print(\" - ✅ Health checking\")\n",
|
||||
"print(\" - ✅ Text completion\")\n",
|
||||
"print(\" - ✅ Chat completion\")\n",
|
||||
"print(\" - ✅ Streaming completion\")\n",
|
||||
"print(\" - ✅ Streaming chat\")\n",
|
||||
"print(\" - ✅ Error handling\")\n",
|
||||
"print(\" - ✅ Clean, consistent API\")\n",
|
||||
"\n",
|
||||
"print(\"\\n💡 Wrapper Benefits:\")\n",
|
||||
"print(\" - Simple, intuitive interface\")\n",
|
||||
"print(\" - Automatic error handling\")\n",
|
||||
"print(\" - Type hints for better IDE support\")\n",
|
||||
"print(\" - Consistent parameter naming\")\n",
|
||||
"print(\" - Built-in streaming support\")\n",
|
||||
"print(\" - Connection management\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "451babc6",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# LlamaCPP API Wrapper Examples\n",
|
||||
"\n",
|
||||
"This notebook demonstrates a **custom Python wrapper** for the LlamaCPP API running at `http://ollama.lan:8080/`.\n",
|
||||
"\n",
|
||||
"## 🎯 Wrapper Benefits\n",
|
||||
"\n",
|
||||
"### Why Use This Wrapper?\n",
|
||||
"- **🐍 Pythonic Interface**: Clean, object-oriented API design\n",
|
||||
"- **🔄 Auto-Discovery**: Automatically finds available models\n",
|
||||
"- **🛡️ Error Handling**: Robust error management and retries\n",
|
||||
"- **📡 Streaming Support**: Built-in streaming for real-time responses\n",
|
||||
"- **🎛️ Consistent API**: Unified interface for all operations\n",
|
||||
"- **⚡ Connection Management**: Efficient HTTP connection handling\n",
|
||||
"\n",
|
||||
"### vs. Raw HTTP Requests:\n",
|
||||
"- **No Manual JSON**: Automatic request/response handling\n",
|
||||
"- **Type Safety**: Type hints for better development experience\n",
|
||||
"- **Error Recovery**: Graceful handling of network issues\n",
|
||||
"- **Code Reusability**: Clean methods for common operations\n",
|
||||
"\n",
|
||||
"## 🔧 Wrapper Features\n",
|
||||
"\n",
|
||||
"### Core Methods:\n",
|
||||
"- `health_check()` - Check API availability\n",
|
||||
"- `list_models()` - Discover available models\n",
|
||||
"- `complete()` - Text completion\n",
|
||||
"- `chat()` - Chat completion\n",
|
||||
"- `stream_complete()` - Streaming text generation\n",
|
||||
"- `stream_chat()` - Streaming chat responses\n",
|
||||
"\n",
|
||||
"### Advanced Features:\n",
|
||||
"- **Automatic Model Discovery**: Finds and uses available models\n",
|
||||
"- **Flexible Parameters**: Support for temperature, top_p, stop sequences\n",
|
||||
"- **Streaming Iterators**: Python generators for real-time responses\n",
|
||||
"- **Connection Pooling**: Efficient HTTP connection reuse\n",
|
||||
"- **Timeout Management**: Configurable request timeouts\n",
|
||||
"\n",
|
||||
"## 📊 Examples Included:\n",
|
||||
"\n",
|
||||
"1. **API Health & Model Discovery** - Check connection and find models\n",
|
||||
"2. **Simple Text Completion** - Basic text generation\n",
|
||||
"3. **Chat Completion** - Conversational AI with system prompts\n",
|
||||
"4. **Streaming Completion** - Real-time text generation\n",
|
||||
"5. **Streaming Chat** - Real-time conversation\n",
|
||||
"6. **Embeddings Test** - Vector representations (if supported)\n",
|
||||
"7. **Utility Functions** - Helper methods for common tasks\n",
|
||||
"\n",
|
||||
"## 🚀 Getting Started:\n",
|
||||
"\n",
|
||||
"1. Ensure LlamaCPP server is running at `http://ollama.lan:8080/`\n",
|
||||
"2. Run the wrapper initialization cell\n",
|
||||
"3. Execute examples to see the wrapper in action\n",
|
||||
"\n",
|
||||
"The wrapper automatically discovers available models and provides a clean, consistent interface for all LlamaCPP API operations!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chat App</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
main {
|
||||
max-width: 700px;
|
||||
}
|
||||
#conversation .user::before {
|
||||
content: 'You asked: ';
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
#conversation .model::before {
|
||||
content: 'AI Response: ';
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
#spinner {
|
||||
opacity: 0;
|
||||
transition: opacity 500ms ease-in;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid #222;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
@keyframes rotation {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
#spinner.active {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="border rounded mx-auto my-5 p-4">
|
||||
<h1>Chat App</h1>
|
||||
<p>Ask me anything...</p>
|
||||
<div id="conversation" class="px-2"></div>
|
||||
<div class="d-flex justify-content-center mb-3">
|
||||
<div id="spinner"></div>
|
||||
</div>
|
||||
<form method="post">
|
||||
<input id="prompt-input" name="prompt" class="form-control"/>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn btn-primary mt-2">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="error" class="d-none text-danger">
|
||||
Error occurred, check the browser developer console for more information.
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/5.6.3/typescript.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script type="module">
|
||||
// to let me write TypeScript, without adding the burden of npm we do a dirty, non-production-ready hack
|
||||
// and transpile the TypeScript code in the browser
|
||||
// this is (arguably) A neat demo trick, but not suitable for production!
|
||||
async function loadTs() {
|
||||
const response = await fetch('/chat_app.ts');
|
||||
const tsCode = await response.text();
|
||||
const jsCode = window.ts.transpile(tsCode, { target: "es2015" });
|
||||
let script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.text = jsCode;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
loadTs().catch((e) => {
|
||||
console.error(e);
|
||||
document.getElementById('error').classList.remove('d-none');
|
||||
document.getElementById('spinner').classList.remove('active');
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,230 @@
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sqlite3
|
||||
from collections.abc import AsyncIterator
|
||||
from concurrent.futures.thread import ThreadPoolExecutor
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Callable, Literal, TypeVar
|
||||
|
||||
import fastapi
|
||||
import logfire
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.responses import FileResponse, Response, StreamingResponse
|
||||
from typing_extensions import LiteralString, ParamSpec, TypedDict
|
||||
|
||||
from pydantic_ai.models.openai import OpenAIModel
|
||||
from pydantic_ai.providers.openai import OpenAIProvider
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.exceptions import UnexpectedModelBehavior
|
||||
from pydantic_ai.messages import (
|
||||
ModelMessage,
|
||||
ModelMessagesTypeAdapter,
|
||||
ModelRequest,
|
||||
ModelResponse,
|
||||
TextPart,
|
||||
UserPromptPart,
|
||||
)
|
||||
|
||||
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
|
||||
logfire.configure(send_to_logfire="if-token-present")
|
||||
logfire.instrument_pydantic_ai()
|
||||
|
||||
# Configuration for Ollama using OpenAI-compatible endpoint
|
||||
OLLAMA_BASE_URL = "http://localhost:11434"
|
||||
MODEL_NAME = "qwen3:8b" # Updated to use available model
|
||||
|
||||
ollama_model = OpenAIModel(
|
||||
model_name=MODEL_NAME, provider=OpenAIProvider(base_url=f"{OLLAMA_BASE_URL}/v1")
|
||||
)
|
||||
|
||||
agent = Agent(ollama_model)
|
||||
THIS_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: fastapi.FastAPI):
|
||||
async with Database.connect() as db:
|
||||
yield {"db": db}
|
||||
|
||||
|
||||
app = fastapi.FastAPI(lifespan=lifespan)
|
||||
logfire.instrument_fastapi(app)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index() -> FileResponse:
|
||||
return FileResponse((THIS_DIR / "chat_app.html"), media_type="text/html")
|
||||
|
||||
|
||||
@app.get("/chat_app.ts")
|
||||
async def main_ts() -> FileResponse:
|
||||
"""Get the raw typescript code, it's compiled in the browser, forgive me."""
|
||||
return FileResponse((THIS_DIR / "chat_app.ts"), media_type="text/plain")
|
||||
|
||||
|
||||
async def get_db(request: Request) -> Database:
|
||||
return request.state.db
|
||||
|
||||
|
||||
@app.get("/chat/")
|
||||
async def get_chat(database: Database = Depends(get_db)) -> Response:
|
||||
msgs = await database.get_messages()
|
||||
return Response(
|
||||
b"\n".join(json.dumps(to_chat_message(m)).encode("utf-8") for m in msgs),
|
||||
media_type="text/plain",
|
||||
)
|
||||
|
||||
|
||||
class ChatMessage(TypedDict):
|
||||
"""Format of messages sent to the browser."""
|
||||
|
||||
role: Literal["user", "model"]
|
||||
timestamp: str
|
||||
content: str
|
||||
|
||||
|
||||
def to_chat_message(m: ModelMessage) -> ChatMessage:
|
||||
first_part = m.parts[0]
|
||||
if isinstance(m, ModelRequest):
|
||||
if isinstance(first_part, UserPromptPart):
|
||||
assert isinstance(first_part.content, str)
|
||||
return {
|
||||
"role": "user",
|
||||
"timestamp": first_part.timestamp.isoformat(),
|
||||
"content": first_part.content,
|
||||
}
|
||||
elif isinstance(m, ModelResponse):
|
||||
if isinstance(first_part, TextPart):
|
||||
return {
|
||||
"role": "model",
|
||||
"timestamp": m.timestamp.isoformat(),
|
||||
"content": first_part.content,
|
||||
}
|
||||
raise UnexpectedModelBehavior(f"Unexpected message type for chat app: {m}")
|
||||
|
||||
|
||||
@app.post("/chat/")
|
||||
async def post_chat(
|
||||
prompt: Annotated[str, fastapi.Form()], database: Database = Depends(get_db)
|
||||
) -> StreamingResponse:
|
||||
async def stream_messages():
|
||||
"""Streams new line delimited JSON `Message`s to the client."""
|
||||
# stream the user prompt so that can be displayed straight away
|
||||
yield (
|
||||
json.dumps(
|
||||
{
|
||||
"role": "user",
|
||||
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
|
||||
"content": prompt,
|
||||
}
|
||||
).encode("utf-8")
|
||||
+ b"\n"
|
||||
)
|
||||
# get the chat history so far to pass as context to the agent
|
||||
messages = await database.get_messages()
|
||||
# run the agent with the user prompt and the chat history
|
||||
async with agent.run_stream(prompt, message_history=messages) as result:
|
||||
async for text in result.stream(debounce_by=0.01):
|
||||
# text here is a `str` and the frontend wants
|
||||
# JSON encoded ModelResponse, so we create one
|
||||
m = ModelResponse(parts=[TextPart(text)], timestamp=result.timestamp())
|
||||
yield json.dumps(to_chat_message(m)).encode("utf-8") + b"\n"
|
||||
|
||||
# add new messages (e.g. the user prompt and the agent response in this case) to the database
|
||||
await database.add_messages(result.new_messages_json())
|
||||
|
||||
return StreamingResponse(stream_messages(), media_type="text/plain")
|
||||
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Database:
|
||||
"""Rudimentary database to store chat messages in SQLite.
|
||||
|
||||
The SQLite standard library package is synchronous, so we
|
||||
use a thread pool executor to run queries asynchronously.
|
||||
"""
|
||||
|
||||
con: sqlite3.Connection
|
||||
_loop: asyncio.AbstractEventLoop
|
||||
_executor: ThreadPoolExecutor
|
||||
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
async def connect(
|
||||
cls, file: Path = THIS_DIR / ".chat_app_messages.sqlite"
|
||||
) -> AsyncIterator[Database]:
|
||||
with logfire.span("connect to DB"):
|
||||
loop = asyncio.get_event_loop()
|
||||
executor = ThreadPoolExecutor(max_workers=1)
|
||||
con = await loop.run_in_executor(executor, cls._connect, file)
|
||||
slf = cls(con, loop, executor)
|
||||
try:
|
||||
yield slf
|
||||
finally:
|
||||
await slf._asyncify(con.close)
|
||||
|
||||
@staticmethod
|
||||
def _connect(file: Path) -> sqlite3.Connection:
|
||||
con = sqlite3.connect(str(file))
|
||||
con = logfire.instrument_sqlite3(con)
|
||||
cur = con.cursor()
|
||||
cur.execute(
|
||||
"CREATE TABLE IF NOT EXISTS messages (id INT PRIMARY KEY, message_list TEXT);"
|
||||
)
|
||||
con.commit()
|
||||
return con
|
||||
|
||||
async def add_messages(self, messages: bytes):
|
||||
await self._asyncify(
|
||||
self._execute,
|
||||
"INSERT INTO messages (message_list) VALUES (?);",
|
||||
messages,
|
||||
commit=True,
|
||||
)
|
||||
await self._asyncify(self.con.commit)
|
||||
|
||||
async def get_messages(self) -> list[ModelMessage]:
|
||||
c = await self._asyncify(
|
||||
self._execute, "SELECT message_list FROM messages order by id"
|
||||
)
|
||||
rows = await self._asyncify(c.fetchall)
|
||||
messages: list[ModelMessage] = []
|
||||
for row in rows:
|
||||
messages.extend(ModelMessagesTypeAdapter.validate_json(row[0]))
|
||||
return messages
|
||||
|
||||
def _execute(
|
||||
self, sql: LiteralString, *args: Any, commit: bool = False
|
||||
) -> sqlite3.Cursor:
|
||||
cur = self.con.cursor()
|
||||
cur.execute(sql, args)
|
||||
if commit:
|
||||
self.con.commit()
|
||||
return cur
|
||||
|
||||
async def _asyncify(
|
||||
self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
|
||||
) -> R:
|
||||
return await self._loop.run_in_executor( # type: ignore
|
||||
self._executor,
|
||||
partial(func, **kwargs),
|
||||
*args, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"chat_app:app", reload=True, reload_dirs=[str(THIS_DIR)]
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
// BIG FAT WARNING: to avoid the complexity of npm, this typescript is compiled in the browser
|
||||
// there's currently no static type checking
|
||||
|
||||
import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.0/lib/marked.esm.js'
|
||||
const convElement = document.getElementById('conversation')
|
||||
|
||||
const promptInput = document.getElementById('prompt-input') as HTMLInputElement
|
||||
const spinner = document.getElementById('spinner')
|
||||
|
||||
// stream the response and render messages as each chunk is received
|
||||
// data is sent as newline-delimited JSON
|
||||
async function onFetchResponse(response: Response): Promise<void> {
|
||||
let text = ''
|
||||
let decoder = new TextDecoder()
|
||||
if (response.ok) {
|
||||
const reader = response.body.getReader()
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
text += decoder.decode(value)
|
||||
addMessages(text)
|
||||
spinner.classList.remove('active')
|
||||
}
|
||||
addMessages(text)
|
||||
promptInput.disabled = false
|
||||
promptInput.focus()
|
||||
} else {
|
||||
const text = await response.text()
|
||||
console.error(`Unexpected response: ${response.status}`, { response, text })
|
||||
throw new Error(`Unexpected response: ${response.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
// The format of messages, this matches pydantic-ai both for brevity and understanding
|
||||
// in production, you might not want to keep this format all the way to the frontend
|
||||
interface Message {
|
||||
role: string
|
||||
content: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
// take raw response text and render messages into the `#conversation` element
|
||||
// Message timestamp is assumed to be a unique identifier of a message, and is used to deduplicate
|
||||
// hence you can send data about the same message multiple times, and it will be updated
|
||||
// instead of creating a new message elements
|
||||
function addMessages(responseText: string) {
|
||||
const lines = responseText.split('\n')
|
||||
const messages: Message[] = lines.filter(line => line.length > 1).map(j => JSON.parse(j))
|
||||
for (const message of messages) {
|
||||
// we use the timestamp as a crude element id
|
||||
const { timestamp, role, content } = message
|
||||
const id = `msg-${timestamp}`
|
||||
let msgDiv = document.getElementById(id)
|
||||
if (!msgDiv) {
|
||||
msgDiv = document.createElement('div')
|
||||
msgDiv.id = id
|
||||
msgDiv.title = `${role} at ${timestamp}`
|
||||
msgDiv.classList.add('border-top', 'pt-2', role)
|
||||
convElement.appendChild(msgDiv)
|
||||
}
|
||||
msgDiv.innerHTML = marked.parse(content)
|
||||
}
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function onError(error: any) {
|
||||
console.error(error)
|
||||
document.getElementById('error').classList.remove('d-none')
|
||||
document.getElementById('spinner').classList.remove('active')
|
||||
}
|
||||
|
||||
async function onSubmit(e: SubmitEvent): Promise<void> {
|
||||
e.preventDefault()
|
||||
spinner.classList.add('active')
|
||||
const body = new FormData(e.target as HTMLFormElement)
|
||||
|
||||
promptInput.value = ''
|
||||
promptInput.disabled = true
|
||||
|
||||
const response = await fetch('/chat/', { method: 'POST', body })
|
||||
await onFetchResponse(response)
|
||||
}
|
||||
|
||||
// call onSubmit when the form is submitted (e.g. user clicks the send button or hits Enter)
|
||||
document.querySelector('form').addEventListener('submit', (e) => onSubmit(e).catch(onError))
|
||||
|
||||
// load messages on page load
|
||||
fetch('/chat/').then(onFetchResponse).catch(onError)
|
||||
@@ -0,0 +1,451 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "bca3806b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Fix Vector Dimension Mismatch in RAG Database\n",
|
||||
"\n",
|
||||
"This notebook demonstrates how to fix the vector dimension mismatch error that occurs when the database expects 1536 dimensions but the embedding model produces 1024 dimensions.\n",
|
||||
"\n",
|
||||
"## Error Context\n",
|
||||
"```\n",
|
||||
"asyncpg.exceptions.DataError: expected 1536 dimensions, not 1024\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"This happens when:\n",
|
||||
"- Database was created with OpenAI embedding dimensions (1536)\n",
|
||||
"- But now using Ollama embedding model like `mxbai-embed-large` (1024 dimensions)\n",
|
||||
"\n",
|
||||
"## Solution\n",
|
||||
"We'll reset the database schema to match the current embedding model dimensions."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "10e666de",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Libraries imported successfully!\n",
|
||||
"Python version: 3.12.9 (main, Mar 23 2025, 16:07:08) [GCC 14.2.0]\n",
|
||||
"Working directory: /home/user/projects/ai_stack/notebooks/pydantic_ai\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Import Required Libraries\n",
|
||||
"import asyncio\n",
|
||||
"import asyncpg\n",
|
||||
"import os\n",
|
||||
"import sys\n",
|
||||
"from pydantic import BaseModel, Field, ValidationError\n",
|
||||
"from typing import List\n",
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"print(\"Libraries imported successfully!\")\n",
|
||||
"print(f\"Python version: {sys.version}\")\n",
|
||||
"print(f\"Working directory: {os.getcwd()}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "4a62b6f1",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Current embedding model: mxbai-embed-large\n",
|
||||
"Required vector dimensions: 1024\n",
|
||||
"Database: postgresql://postgres:postgres@localhost:54320/pydantic_ai_rag\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Database Configuration\n",
|
||||
"DATABASE_CONFIG = {\n",
|
||||
" \"server_dsn\": \"postgresql://postgres:postgres@localhost:54320\",\n",
|
||||
" \"database\": \"pydantic_ai_rag\"\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"# Embedding Models and their dimensions\n",
|
||||
"EMBEDDING_DIMENSIONS = {\n",
|
||||
" \"all-minilm\": 384,\n",
|
||||
" \"mxbai-embed-large\": 1024, \n",
|
||||
" \"nomic-embed-text\": 768,\n",
|
||||
" \"text-embedding-3-small\": 1536, # OpenAI model\n",
|
||||
" \"bge-large\": 1024,\n",
|
||||
" \"snowflake-arctic-embed\": 1024\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"# Current configuration (should match your rag.py file)\n",
|
||||
"CURRENT_EMBEDDING_MODEL = \"mxbai-embed-large\"\n",
|
||||
"CURRENT_VECTOR_DIMENSIONS = EMBEDDING_DIMENSIONS[CURRENT_EMBEDDING_MODEL]\n",
|
||||
"\n",
|
||||
"print(f\"Current embedding model: {CURRENT_EMBEDDING_MODEL}\")\n",
|
||||
"print(f\"Required vector dimensions: {CURRENT_VECTOR_DIMENSIONS}\")\n",
|
||||
"print(f\"Database: {DATABASE_CONFIG['server_dsn']}/{DATABASE_CONFIG['database']}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "3c067bae",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Current embedding dimensions: 1024\n",
|
||||
"Database expects: 1536 dimensions\n",
|
||||
"Dimension mismatch: True\n",
|
||||
"First 5 values of embedding: [-0.11674921305184428, 0.735286238037038, 0.37432037616766584, 1.1288451177377212, -1.1543849541254774]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Simulate Data with Incorrect Dimensions\n",
|
||||
"# This simulates what happens when we have embedding data from the new model\n",
|
||||
"# but the database expects the old dimensions\n",
|
||||
"\n",
|
||||
"# Simulate embedding from mxbai-embed-large (1024 dimensions)\n",
|
||||
"current_embedding = np.random.randn(1024).tolist()\n",
|
||||
"\n",
|
||||
"# Simulate what the database currently expects (1536 dimensions from OpenAI)\n",
|
||||
"expected_old_dimensions = 1536\n",
|
||||
"\n",
|
||||
"print(f\"Current embedding dimensions: {len(current_embedding)}\")\n",
|
||||
"print(f\"Database expects: {expected_old_dimensions} dimensions\")\n",
|
||||
"print(f\"Dimension mismatch: {len(current_embedding) != expected_old_dimensions}\")\n",
|
||||
"print(f\"First 5 values of embedding: {current_embedding[:5]}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "2579dc9b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"✅ Pydantic models defined\n",
|
||||
" Expected vector dimensions: 1024\n",
|
||||
" Embedding model: mxbai-embed-large\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/home/user/projects/ai_stack/notebooks/pydantic_ai/.venv/lib/python3.12/site-packages/pydantic/_internal/_config.py:373: UserWarning: Valid config keys have changed in V2:\n",
|
||||
"* 'schema_extra' has been renamed to 'json_schema_extra'\n",
|
||||
" warnings.warn(message, UserWarning)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Define Pydantic Model for Validation\n",
|
||||
"class EmbeddingVector(BaseModel):\n",
|
||||
" \"\"\"Pydantic model to validate embedding dimensions\"\"\"\n",
|
||||
" vector: List[float] = Field(..., min_items=CURRENT_VECTOR_DIMENSIONS, max_items=CURRENT_VECTOR_DIMENSIONS)\n",
|
||||
" \n",
|
||||
" class Config:\n",
|
||||
" schema_extra = {\n",
|
||||
" \"example\": {\n",
|
||||
" \"vector\": [0.1] * CURRENT_VECTOR_DIMENSIONS\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"class DocumentSection(BaseModel):\n",
|
||||
" \"\"\"Pydantic model for document sections with embeddings\"\"\"\n",
|
||||
" url: str\n",
|
||||
" title: str\n",
|
||||
" content: str\n",
|
||||
" embedding: List[float] = Field(..., min_items=CURRENT_VECTOR_DIMENSIONS, max_items=CURRENT_VECTOR_DIMENSIONS)\n",
|
||||
"\n",
|
||||
"print(f\"✅ Pydantic models defined\")\n",
|
||||
"print(f\" Expected vector dimensions: {CURRENT_VECTOR_DIMENSIONS}\")\n",
|
||||
"print(f\" Embedding model: {CURRENT_EMBEDDING_MODEL}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "5a6490f5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Testing Pydantic validation...\n",
|
||||
"✅ Validation successful for 1024 dimensions\n",
|
||||
"❌ Expected validation failure for wrong dimensions: 1536\n",
|
||||
" Error: 1 validation error for EmbeddingVector\n",
|
||||
"vector\n",
|
||||
" List should have at most 1024 items after validation, not 1536 [type=too_long, input_value=[0.2739007144989292, 1.08...43, -0.6672567075569252], input_type=list]\n",
|
||||
" For further information visit https://errors.pydantic.dev/2.11/v/too_long\n",
|
||||
"✅ Document section validation successful\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Validate Data Dimensions with Pydantic\n",
|
||||
"print(\"Testing Pydantic validation...\")\n",
|
||||
"\n",
|
||||
"# Test with correct dimensions\n",
|
||||
"try:\n",
|
||||
" correct_embedding = np.random.randn(CURRENT_VECTOR_DIMENSIONS).tolist()\n",
|
||||
" valid_vector = EmbeddingVector(vector=correct_embedding)\n",
|
||||
" print(f\"✅ Validation successful for {len(correct_embedding)} dimensions\")\n",
|
||||
"except ValidationError as e:\n",
|
||||
" print(f\"❌ Validation failed: {e}\")\n",
|
||||
"\n",
|
||||
"# Test with incorrect dimensions (simulating the error)\n",
|
||||
"try:\n",
|
||||
" wrong_embedding = np.random.randn(1536).tolist() # Old OpenAI dimensions\n",
|
||||
" invalid_vector = EmbeddingVector(vector=wrong_embedding)\n",
|
||||
" print(f\"✅ Validation successful for {len(wrong_embedding)} dimensions\")\n",
|
||||
"except ValidationError as e:\n",
|
||||
" print(f\"❌ Expected validation failure for wrong dimensions: {len(wrong_embedding)}\")\n",
|
||||
" print(f\" Error: {e}\")\n",
|
||||
"\n",
|
||||
"# Test document section validation\n",
|
||||
"try:\n",
|
||||
" doc_section = DocumentSection(\n",
|
||||
" url=\"https://example.com/doc\",\n",
|
||||
" title=\"Test Document\",\n",
|
||||
" content=\"This is test content\",\n",
|
||||
" embedding=current_embedding\n",
|
||||
" )\n",
|
||||
" print(f\"✅ Document section validation successful\")\n",
|
||||
"except ValidationError as e:\n",
|
||||
" print(f\"❌ Document section validation failed: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "1b60abec",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Demonstrating asyncpg DataError...\n",
|
||||
"✅ Insert successful\n",
|
||||
"✅ Insert successful\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Handle asyncpg DataError Exception\n",
|
||||
"async def demonstrate_data_error():\n",
|
||||
" \"\"\"Demonstrate how the DataError occurs and how to handle it\"\"\"\n",
|
||||
" \n",
|
||||
" try:\n",
|
||||
" # Connect to the database\n",
|
||||
" server_dsn = DATABASE_CONFIG[\"server_dsn\"]\n",
|
||||
" database = DATABASE_CONFIG[\"database\"]\n",
|
||||
" \n",
|
||||
" conn = await asyncpg.connect(f\"{server_dsn}/{database}\")\n",
|
||||
" \n",
|
||||
" # Try to insert data with wrong dimensions (this will fail if table exists with old schema)\n",
|
||||
" wrong_embedding = np.random.randn(1536).tolist() # Old dimensions\n",
|
||||
" \n",
|
||||
" try:\n",
|
||||
" await conn.execute(\n",
|
||||
" \"INSERT INTO doc_sections (url, title, content, embedding) VALUES ($1, $2, $3, $4)\",\n",
|
||||
" \"https://test.com\",\n",
|
||||
" \"Test\",\n",
|
||||
" \"Test content\",\n",
|
||||
" str(wrong_embedding) # This might cause the error\n",
|
||||
" )\n",
|
||||
" print(\"✅ Insert successful\")\n",
|
||||
" except asyncpg.exceptions.DataError as e:\n",
|
||||
" print(f\"❌ DataError caught: {e}\")\n",
|
||||
" print(\" This is the exact error you encountered!\")\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"⚠️ Other database error: {e}\")\n",
|
||||
" \n",
|
||||
" await conn.close()\n",
|
||||
" \n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"❌ Connection error: {e}\")\n",
|
||||
" print(\" Make sure PostgreSQL is running and accessible\")\n",
|
||||
"\n",
|
||||
"# Run the demonstration\n",
|
||||
"print(\"Demonstrating asyncpg DataError...\")\n",
|
||||
"await demonstrate_data_error()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "ae736e7a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Starting database schema fix...\n",
|
||||
"🔧 Fixing database schema...\n",
|
||||
" ✅ Dropped existing doc_sections table\n",
|
||||
" ✅ Created new table with 1024 dimensions\n",
|
||||
" ✅ Successfully inserted test data with correct dimensions\n",
|
||||
" ✅ Verified: Retrieved document with ID 1\n",
|
||||
"\n",
|
||||
"🎉 Database schema fixed successfully!\n",
|
||||
" Table now accepts 1024-dimensional vectors\n",
|
||||
" Compatible with embedding model: mxbai-embed-large\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Fix Data Dimensions and Retry Execution\n",
|
||||
"async def fix_database_schema():\n",
|
||||
" \"\"\"Drop and recreate the table with correct dimensions\"\"\"\n",
|
||||
" \n",
|
||||
" try:\n",
|
||||
" server_dsn = DATABASE_CONFIG[\"server_dsn\"]\n",
|
||||
" database = DATABASE_CONFIG[\"database\"]\n",
|
||||
" \n",
|
||||
" # Connect to database\n",
|
||||
" conn = await asyncpg.connect(f\"{server_dsn}/{database}\")\n",
|
||||
" \n",
|
||||
" print(\"🔧 Fixing database schema...\")\n",
|
||||
" \n",
|
||||
" # Drop existing table (this removes the dimension constraint)\n",
|
||||
" await conn.execute(\"DROP TABLE IF EXISTS doc_sections CASCADE\")\n",
|
||||
" print(\" ✅ Dropped existing doc_sections table\")\n",
|
||||
" \n",
|
||||
" # Create new table with correct dimensions\n",
|
||||
" new_schema = f\"\"\"\n",
|
||||
" CREATE EXTENSION IF NOT EXISTS vector;\n",
|
||||
" \n",
|
||||
" CREATE TABLE doc_sections (\n",
|
||||
" id serial PRIMARY KEY,\n",
|
||||
" url text NOT NULL UNIQUE,\n",
|
||||
" title text NOT NULL,\n",
|
||||
" content text NOT NULL,\n",
|
||||
" embedding vector({CURRENT_VECTOR_DIMENSIONS}) NOT NULL\n",
|
||||
" );\n",
|
||||
" \n",
|
||||
" CREATE INDEX idx_doc_sections_embedding ON doc_sections \n",
|
||||
" USING hnsw (embedding vector_l2_ops);\n",
|
||||
" \"\"\"\n",
|
||||
" \n",
|
||||
" await conn.execute(new_schema)\n",
|
||||
" print(f\" ✅ Created new table with {CURRENT_VECTOR_DIMENSIONS} dimensions\")\n",
|
||||
" \n",
|
||||
" # Test insertion with correct dimensions\n",
|
||||
" correct_embedding = np.random.randn(CURRENT_VECTOR_DIMENSIONS).tolist()\n",
|
||||
" \n",
|
||||
" # Validate with Pydantic first\n",
|
||||
" doc_section = DocumentSection(\n",
|
||||
" url=\"https://test.com/fixed\",\n",
|
||||
" title=\"Test Document (Fixed)\",\n",
|
||||
" content=\"This is test content with correct dimensions\",\n",
|
||||
" embedding=correct_embedding\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Insert the validated data\n",
|
||||
" await conn.execute(\n",
|
||||
" \"INSERT INTO doc_sections (url, title, content, embedding) VALUES ($1, $2, $3, $4)\",\n",
|
||||
" doc_section.url,\n",
|
||||
" doc_section.title,\n",
|
||||
" doc_section.content,\n",
|
||||
" str(doc_section.embedding)\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" print(\" ✅ Successfully inserted test data with correct dimensions\")\n",
|
||||
" \n",
|
||||
" # Verify the data\n",
|
||||
" result = await conn.fetchrow(\"SELECT * FROM doc_sections WHERE url = $1\", doc_section.url)\n",
|
||||
" print(f\" ✅ Verified: Retrieved document with ID {result['id']}\")\n",
|
||||
" \n",
|
||||
" await conn.close()\n",
|
||||
" print(\"\\n🎉 Database schema fixed successfully!\")\n",
|
||||
" print(f\" Table now accepts {CURRENT_VECTOR_DIMENSIONS}-dimensional vectors\")\n",
|
||||
" print(f\" Compatible with embedding model: {CURRENT_EMBEDDING_MODEL}\")\n",
|
||||
" \n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"❌ Error fixing database: {e}\")\n",
|
||||
"\n",
|
||||
"# Execute the fix\n",
|
||||
"print(\"Starting database schema fix...\")\n",
|
||||
"await fix_database_schema()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c41b80e5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Next Steps: Rebuild Your RAG Database\n",
|
||||
"\n",
|
||||
"After running this notebook, your database schema is now fixed. However, you need to rebuild the search database with the correct embeddings.\n",
|
||||
"\n",
|
||||
"### Option 1: Run the build command directly\n",
|
||||
"```bash\n",
|
||||
"cd /home/user/projects/ai_stack/notebooks/pydantic_ai\n",
|
||||
"python rag.py build\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"### Option 2: Use your existing script\n",
|
||||
"If you have a script that calls the build function, run it now.\n",
|
||||
"\n",
|
||||
"### Option 3: Test the search functionality\n",
|
||||
"```bash\n",
|
||||
"cd /home/user/projects/ai_stack/notebooks/pydantic_ai\n",
|
||||
"python rag.py search \"How do I configure logfire?\"\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"### Verification\n",
|
||||
"The database now has:\n",
|
||||
"- ✅ Correct vector dimensions (1024) for `mxbai-embed-large`\n",
|
||||
"- ✅ Updated schema that matches your embedding model\n",
|
||||
"- ✅ Proper vector index for efficient similarity search\n",
|
||||
"\n",
|
||||
"### Important Notes\n",
|
||||
"- Any existing embeddings in the old table have been deleted\n",
|
||||
"- You need to rebuild the search database from scratch\n",
|
||||
"- Future embeddings will work correctly with the new schema"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "pydantic_ai",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "a23583b0",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"✅ Imports completed successfully!\n",
|
||||
"🌐 Ollama endpoint: http://localhost:11434\n",
|
||||
"🤖 Model: qwen3:8b\n",
|
||||
"💡 Using OpenAI-compatible interface with Ollama\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from typing import List, Optional, Any\n",
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"from pydantic_ai import Agent\n",
|
||||
"from pydantic_ai.models.openai import OpenAIModel\n",
|
||||
"from pydantic_ai.providers.openai import OpenAIProvider\n",
|
||||
"\n",
|
||||
"# Configuration for Ollama using OpenAI-compatible endpoint\n",
|
||||
"OLLAMA_BASE_URL = \"http://localhost:11434\"\n",
|
||||
"MODEL_NAME = \"qwen3:8b\" # Updated to use available model\n",
|
||||
"\n",
|
||||
"# Ollama provides OpenAI-compatible API at /v1/ endpoint\n",
|
||||
"ollama_model = OpenAIModel(\n",
|
||||
" model_name=MODEL_NAME, provider=OpenAIProvider(base_url=f\"{OLLAMA_BASE_URL}/v1\")\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"✅ Imports completed successfully!\")\n",
|
||||
"print(f\"🌐 Ollama endpoint: {OLLAMA_BASE_URL}\")\n",
|
||||
"print(f\"🤖 Model: {MODEL_NAME}\")\n",
|
||||
"print(\"💡 Using OpenAI-compatible interface with Ollama\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "c19ea82e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"city='London' country='United Kingdom'\n",
|
||||
"Usage(requests=1, request_tokens=163, response_tokens=157, total_tokens=320)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"class CityLocation(BaseModel):\n",
|
||||
" city: str\n",
|
||||
" country: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"ollama_model = OpenAIModel(\n",
|
||||
" model_name=MODEL_NAME, provider=OpenAIProvider(base_url=f\"{OLLAMA_BASE_URL}/v1\")\n",
|
||||
")\n",
|
||||
"agent = Agent(ollama_model, output_type=CityLocation)\n",
|
||||
"\n",
|
||||
"result = await agent.run('Where were the olympics held in 2012?')\n",
|
||||
"print(result.output)\n",
|
||||
"#> city='London' country='United Kingdom'\n",
|
||||
"print(result.usage())\n",
|
||||
"#> Usage(requests=1, request_tokens=57, response_tokens=8, total_tokens=65)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "bff9a7ec",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Available models in Ollama:\n",
|
||||
" - qwen3:8b\n",
|
||||
" - gemma3:4b-it-qat\n",
|
||||
" - deepseek-r1:8b\n",
|
||||
" - hf.co/unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF:Q4_K_XL\n",
|
||||
" - deepseek-r1-qwen3-8b:latest\n",
|
||||
" - llama3.2:1b\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Let's first check what models are available in Ollama\n",
|
||||
"import requests\n",
|
||||
"import json\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" response = requests.get(f\"{OLLAMA_BASE_URL}/api/tags\")\n",
|
||||
" if response.status_code == 200:\n",
|
||||
" models = response.json()\n",
|
||||
" print(\"Available models in Ollama:\")\n",
|
||||
" for model in models.get('models', []):\n",
|
||||
" print(f\" - {model['name']}\")\n",
|
||||
" else:\n",
|
||||
" print(f\"Failed to get models: {response.status_code}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"Error connecting to Ollama: {e}\")\n",
|
||||
" print(\"Make sure Ollama is running on localhost:11434\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "53f24905",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🔄 Switching to model: qwen3:8b\n",
|
||||
"📝 This model should support function calling/tools\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Let's switch to a model that supports function calling\n",
|
||||
"# llama3.2:1b should support tools better than deepseek-r1\n",
|
||||
"# MODEL_NAME = \"llama3.2:1b\"\n",
|
||||
"MODEL_NAME = \"qwen3:8b\"\n",
|
||||
"print(f\"🔄 Switching to model: {MODEL_NAME}\")\n",
|
||||
"print(\"📝 This model should support function calling/tools\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "87a9085f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"<think>\n",
|
||||
"Okay, so the user asked for the top five highest-grossing animated films of 2025. Let me check the search results to see what's available.\n",
|
||||
"\n",
|
||||
"Looking at the first result from whatsafterthemovie.com, it lists Ne Zha as number 1 with a domestic gross of $20,858,156. But wait, that's just domestic, and the worldwide might be higher. The second result from Wikipedia mentions a list of the 10 highest-grossing animated films of 2025, but the exact top five aren't detailed there. \n",
|
||||
"\n",
|
||||
"The third result from simplebeen.com talks about Disney's dominance and lists the top 10, but again, the specific top five aren't clear. The fourth result from listchallenges.com mentions Ne Zha 2 as number 1 and lists the top 50, but the top five aren't fully provided. \n",
|
||||
"\n",
|
||||
"The fifth result is an IMDb list, but it's incomplete, only showing some entries. The sixth result from justjared.com states that Ne Zha 2 is the highest-grossing with $1.72 billion. The seventh result from animesoulking.com also mentions Ne Zha 2 as the highest-grossing. \n",
|
||||
"\n",
|
||||
"The eighth and ninth results from dexerto.com and digitalspy.com mention Lilo & Stitch and Boonie Bears: Future Reborn as top earners. The last Statista link is about the highest-grossing animated movies ever, including 2025 data. \n",
|
||||
"\n",
|
||||
"Putting this together, Ne Zha 2 seems to be the top. Then Lilo & Stitch and Boonie Bears are mentioned. But the exact top five isn't fully listed in the results. However, some sources suggest Ne Zha 2, Lilo & Stitch, Boonie Bears: Future Reborn, and others like The Super Mario Bros. Movie and Spider-Man: Across the Spider-Verse. But since the data is from 2025, maybe the actual numbers are different. \n",
|
||||
"\n",
|
||||
"I need to compile the most consistent answers from the sources. Ne Zha 2 is consistently mentioned as the top. Then Lilo & Stitch and Boonie Bears are next. The other films might be from the 2024 list. Since the user asked for 2025, I should mention that the data is as of 2025 and list the top five based on available info, noting that it's up to the end of 2025.\n",
|
||||
"</think>\n",
|
||||
"\n",
|
||||
"Based on the latest data as of 2025, here are the **top five highest-grossing animated films**:\n",
|
||||
"\n",
|
||||
"1. **Ne Zha 2** (CMC Pictures) \n",
|
||||
" - **Worldwide Gross**: ~$1.72 billion (as of February 2025) \n",
|
||||
" - A Chinese epic redefining animated storytelling with mythological depth.\n",
|
||||
"\n",
|
||||
"2. **Lilo & Stitch** (Disney) \n",
|
||||
" - A global phenomenon with strong box office performance, dominating 2025.\n",
|
||||
"\n",
|
||||
"3. **Boonie Bears: Future Reborn** (China Film Group) \n",
|
||||
" - A family-friendly adventure film with significant international appeal.\n",
|
||||
"\n",
|
||||
"4. **The Super Mario Bros. Movie** (Nintendo & Universal) \n",
|
||||
" - Leveraging iconic IP to secure a top spot in 2025.\n",
|
||||
"\n",
|
||||
"5. **Spider-Man: Across the Spider-Verse** (Marvel/ Sony) \n",
|
||||
" - Continued success from the groundbreaking Spider-Verse series.\n",
|
||||
"\n",
|
||||
"*Note: Data is current as of late 2025, with Ne Zha 2 leading globally. Box office figures may vary slightly depending on regional releases and currency conversions.*\n",
|
||||
"Usage(requests=2, request_tokens=2061, response_tokens=1215, total_tokens=3276)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from pydantic_ai.common_tools.duckduckgo import duckduckgo_search_tool\n",
|
||||
"\n",
|
||||
"# Create agent with the new model\n",
|
||||
"ollama_model = OpenAIModel(\n",
|
||||
" model_name=MODEL_NAME, \n",
|
||||
" provider=OpenAIProvider(base_url=f\"{OLLAMA_BASE_URL}/v1\")\n",
|
||||
")\n",
|
||||
"agent = Agent(\n",
|
||||
" ollama_model,\n",
|
||||
" tools=[duckduckgo_search_tool()],\n",
|
||||
" system_prompt='Search DuckDuckGo for the given query and return the results.',\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"result = await agent.run(\n",
|
||||
" 'Can you list the top five highest-grossing animated films of 2025?'\n",
|
||||
")\n",
|
||||
"print(result.output)\n",
|
||||
"print(result.usage())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "040375eb",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"✅ Agent with tools created successfully!\n",
|
||||
"🔧 Available tools:\n",
|
||||
" - get_weather(location)\n",
|
||||
" - calculate_circle_area(radius)\n",
|
||||
" - get_current_time()\n",
|
||||
"\n",
|
||||
"🎯 The agent can now use these tools to answer questions!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example 1: Adding tool functions to a PydanticAI Agent\n",
|
||||
"from datetime import datetime\n",
|
||||
"import math\n",
|
||||
"from pydantic_ai import RunContext\n",
|
||||
"\n",
|
||||
"# Define response models\n",
|
||||
"class WeatherInfo(BaseModel):\n",
|
||||
" location: str\n",
|
||||
" temperature: float\n",
|
||||
" description: str\n",
|
||||
" timestamp: str\n",
|
||||
"\n",
|
||||
"class MathResult(BaseModel):\n",
|
||||
" operation: str\n",
|
||||
" result: float\n",
|
||||
" explanation: str\n",
|
||||
"\n",
|
||||
"# Create agent with the new model\n",
|
||||
"ollama_model = OpenAIModel(\n",
|
||||
" model_name=MODEL_NAME, \n",
|
||||
" provider=OpenAIProvider(base_url=f\"{OLLAMA_BASE_URL}/v1\")\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Create agent - we'll add tools to this agent\n",
|
||||
"agent = Agent(ollama_model)\n",
|
||||
"\n",
|
||||
"# Method 1: Adding tools using @agent.tool decorator\n",
|
||||
"@agent.tool\n",
|
||||
"def get_weather(ctx: RunContext, location: str) -> str:\n",
|
||||
" \"\"\"Get current weather for a location.\"\"\"\n",
|
||||
" # This is a mock function - in real use, you'd call a weather API\n",
|
||||
" mock_weather = {\n",
|
||||
" \"London\": {\"temp\": 15, \"desc\": \"Cloudy\"},\n",
|
||||
" \"Paris\": {\"temp\": 18, \"desc\": \"Sunny\"},\n",
|
||||
" \"New York\": {\"temp\": 12, \"desc\": \"Rainy\"},\n",
|
||||
" \"Tokyo\": {\"temp\": 22, \"desc\": \"Clear\"}\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" weather = mock_weather.get(location, {\"temp\": 20, \"desc\": \"Unknown\"})\n",
|
||||
" return f\"The weather in {location} is {weather['temp']}°C and {weather['desc']}\"\n",
|
||||
"\n",
|
||||
"@agent.tool\n",
|
||||
"def calculate_circle_area(ctx: RunContext, radius: float) -> str:\n",
|
||||
" \"\"\"Calculate the area of a circle given its radius.\"\"\"\n",
|
||||
" if radius <= 0:\n",
|
||||
" return \"Radius must be positive\"\n",
|
||||
" \n",
|
||||
" area = math.pi * radius ** 2\n",
|
||||
" return f\"The area of a circle with radius {radius} is {area:.2f} square units\"\n",
|
||||
"\n",
|
||||
"@agent.tool\n",
|
||||
"def get_current_time(ctx: RunContext) -> str:\n",
|
||||
" \"\"\"Get the current date and time.\"\"\"\n",
|
||||
" now = datetime.now()\n",
|
||||
" return f\"Current time: {now.strftime('%Y-%m-%d %H:%M:%S')}\"\n",
|
||||
"\n",
|
||||
"print(\"✅ Agent with tools created successfully!\")\n",
|
||||
"print(\"🔧 Available tools:\")\n",
|
||||
"print(\" - get_weather(location)\")\n",
|
||||
"print(\" - calculate_circle_area(radius)\")\n",
|
||||
"print(\" - get_current_time()\")\n",
|
||||
"print(\"\\n🎯 The agent can now use these tools to answer questions!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "01e003c2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🧪 Testing the agent with different queries that should trigger tools:\n",
|
||||
"\n",
|
||||
"1️⃣ Testing weather tool:\n",
|
||||
"Response: <think>\n",
|
||||
"Okay, let me process the user's query and the tool response. The user asked, \"What's the weather like in London?\" I called the get_weather function with London as the location. The response came back as 15°C and Cloudy. Now I need to present this information clearly.\n",
|
||||
"\n",
|
||||
"First, I'll confirm the location to make sure there's no confusion. Then, state the temperature and the weather condition. I should keep it straightforward and friendly. Maybe add a sentence about the weather being mild or comfortable based on the temperature. Let me check if there's any additional info needed, but since the user only asked for the current weather, sticking to the provided details is best. Alright, time to put it all together in a natural sentence.\n",
|
||||
"</think>\n",
|
||||
"\n",
|
||||
"The current weather in London is 15°C with cloudy conditions. It looks like a mild and comfortable day!\n",
|
||||
"Usage: Usage(requests=2, request_tokens=608, response_tokens=311, total_tokens=919)\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/tmp/ipykernel_1269940/3906904202.py:7: DeprecationWarning: `result.data` is deprecated, use `result.output` instead.\n",
|
||||
" print(f\"Response: {result1.data}\")\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Test the agent with tools\n",
|
||||
"print(\"🧪 Testing the agent with different queries that should trigger tools:\\n\")\n",
|
||||
"\n",
|
||||
"# Test 1: Weather query\n",
|
||||
"print(\"1️⃣ Testing weather tool:\")\n",
|
||||
"result1 = await agent.run(\"What's the weather like in London?\")\n",
|
||||
"print(f\"Response: {result1.data}\")\n",
|
||||
"print(f\"Usage: {result1.usage()}\")\n",
|
||||
"print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "6210d513",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2️⃣ Testing math tool:\n",
|
||||
"Response: <think>\n",
|
||||
"Okay, let me check the user's question again. They asked for the area of a circle with radius 5. The tool response says the area is 78.54 square units. Wait, the function calculate_circle_area was called with radius 5, and the result is 78.54. That makes sense because π times 5 squared is approximately 3.1416 * 25 = 78.54. So the answer is correct. I should present this in a clear way to the user, maybe mention the formula used and the calculation steps. Let me make sure to format the response properly and confirm the answer.\n",
|
||||
"</think>\n",
|
||||
"\n",
|
||||
"The area of a circle with a radius of **5.0** is **78.54 square units**.\n",
|
||||
"\n",
|
||||
"This is calculated using the formula: \n",
|
||||
"$$\n",
|
||||
"\\text{Area} = \\pi \\times r^2 = \\pi \\times 5^2 \\approx 3.1416 \\times 25 \\approx 78.54\n",
|
||||
"$$\n",
|
||||
"\n",
|
||||
"Let me know if you need further clarification! 🌟\n",
|
||||
"\n",
|
||||
"3️⃣ Testing time tool:\n",
|
||||
"Response: <think>\n",
|
||||
"</think>\n",
|
||||
"\n",
|
||||
"The current time is 2025-06-17 at 14:32:22.\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Test 2: Math calculation\n",
|
||||
"print(\"2️⃣ Testing math tool:\")\n",
|
||||
"result2 = await agent.run(\"What's the area of a circle with radius 5?\")\n",
|
||||
"print(f\"Response: {result2.output}\")\n",
|
||||
"print()\n",
|
||||
"\n",
|
||||
"# Test 3: Current time\n",
|
||||
"print(\"3️⃣ Testing time tool:\")\n",
|
||||
"result3 = await agent.run(\"What time is it now?\")\n",
|
||||
"print(f\"Response: {result3.output}\")\n",
|
||||
"print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "4f3ef9a5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"============================================================\n",
|
||||
"📚 TOOL FUNCTIONS GUIDE FOR PYDANTIC AI\n",
|
||||
"============================================================\n",
|
||||
"\n",
|
||||
"✅ Method 1: @agent.tool decorator\n",
|
||||
" ✓ Directly decorate functions\n",
|
||||
" ✓ Functions must take RunContext as first parameter\n",
|
||||
" ✓ Best for simple tools\n",
|
||||
"\n",
|
||||
"✅ Method 2: Standalone Tool objects\n",
|
||||
" ✓ Create Tool objects explicitly\n",
|
||||
" ✓ More control over tool configuration\n",
|
||||
" ✓ Can be reused across multiple agents\n",
|
||||
"\n",
|
||||
"✅ Method 3: Tools with complex return types\n",
|
||||
" ✓ Return dictionaries, lists, or custom objects\n",
|
||||
" ✓ Agent will serialize appropriately\n",
|
||||
"\n",
|
||||
"✅ Method 4: Tools with error handling\n",
|
||||
" ✓ Handle errors gracefully\n",
|
||||
" ✓ Return meaningful error messages\n",
|
||||
" ✓ Prevent agent crashes\n",
|
||||
"\n",
|
||||
"🎉 All tool methods demonstrated!\n",
|
||||
"💡 Key Points:\n",
|
||||
" • Always include RunContext as first parameter\n",
|
||||
" • Use descriptive docstrings - the agent uses them\n",
|
||||
" • Handle errors gracefully in your tools\n",
|
||||
" • Tools can return strings, dicts, or complex objects\n",
|
||||
" • Test your tools individually before using with agent\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# 🎯 COMPREHENSIVE GUIDE: Different ways to add tool functions in PydanticAI\n",
|
||||
"\n",
|
||||
"print(\"=\"*60)\n",
|
||||
"print(\"📚 TOOL FUNCTIONS GUIDE FOR PYDANTIC AI\")\n",
|
||||
"print(\"=\"*60)\n",
|
||||
"\n",
|
||||
"# Method 1: Using @agent.tool decorator (already shown above)\n",
|
||||
"print(\"\\n✅ Method 1: @agent.tool decorator\")\n",
|
||||
"print(\" ✓ Directly decorate functions\")\n",
|
||||
"print(\" ✓ Functions must take RunContext as first parameter\")\n",
|
||||
"print(\" ✓ Best for simple tools\")\n",
|
||||
"\n",
|
||||
"# Method 2: Creating standalone tools and adding them to agent\n",
|
||||
"from pydantic_ai.tools import Tool\n",
|
||||
"\n",
|
||||
"def database_query(ctx: RunContext, table: str, condition: str) -> str:\n",
|
||||
" \"\"\"Mock database query function.\"\"\"\n",
|
||||
" # This would normally connect to a real database\n",
|
||||
" mock_results = {\n",
|
||||
" \"users\": [\"Alice\", \"Bob\", \"Charlie\"],\n",
|
||||
" \"products\": [\"Laptop\", \"Phone\", \"Tablet\"],\n",
|
||||
" \"orders\": [\"Order #1\", \"Order #2\", \"Order #3\"]\n",
|
||||
" }\n",
|
||||
" results = mock_results.get(table, [\"No data found\"])\n",
|
||||
" return f\"Query results from {table} where {condition}: {', '.join(results)}\"\n",
|
||||
"\n",
|
||||
"# Create a standalone tool\n",
|
||||
"db_tool = Tool(database_query)\n",
|
||||
"\n",
|
||||
"# Create new agent and add the tool\n",
|
||||
"agent2 = Agent(ollama_model)\n",
|
||||
"agent2._register_tool(db_tool)\n",
|
||||
"\n",
|
||||
"print(\"\\n✅ Method 2: Standalone Tool objects\")\n",
|
||||
"print(\" ✓ Create Tool objects explicitly\")\n",
|
||||
"print(\" ✓ More control over tool configuration\")\n",
|
||||
"print(\" ✓ Can be reused across multiple agents\")\n",
|
||||
"\n",
|
||||
"# Method 3: Tools with complex return types\n",
|
||||
"@agent2.tool\n",
|
||||
"def search_files(ctx: RunContext, pattern: str, file_type: str = \"txt\") -> dict:\n",
|
||||
" \"\"\"Search for files matching a pattern.\"\"\"\n",
|
||||
" mock_files = {\n",
|
||||
" \"txt\": [\"document1.txt\", \"notes.txt\", \"readme.txt\"],\n",
|
||||
" \"py\": [\"main.py\", \"utils.py\", \"test.py\"],\n",
|
||||
" \"md\": [\"README.md\", \"CHANGELOG.md\"]\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" files = mock_files.get(file_type, [])\n",
|
||||
" matching = [f for f in files if pattern.lower() in f.lower()]\n",
|
||||
" \n",
|
||||
" return {\n",
|
||||
" \"pattern\": pattern,\n",
|
||||
" \"file_type\": file_type,\n",
|
||||
" \"matches\": matching,\n",
|
||||
" \"total_found\": len(matching)\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"print(\"\\n✅ Method 3: Tools with complex return types\")\n",
|
||||
"print(\" ✓ Return dictionaries, lists, or custom objects\")\n",
|
||||
"print(\" ✓ Agent will serialize appropriately\")\n",
|
||||
"\n",
|
||||
"# Method 4: Tools with error handling\n",
|
||||
"@agent2.tool\n",
|
||||
"def safe_division(ctx: RunContext, dividend: float, divisor: float) -> str:\n",
|
||||
" \"\"\"Safely divide two numbers with error handling.\"\"\"\n",
|
||||
" try:\n",
|
||||
" if divisor == 0:\n",
|
||||
" return \"Error: Cannot divide by zero\"\n",
|
||||
" result = dividend / divisor\n",
|
||||
" return f\"{dividend} ÷ {divisor} = {result:.4f}\"\n",
|
||||
" except Exception as e:\n",
|
||||
" return f\"Error in calculation: {str(e)}\"\n",
|
||||
"\n",
|
||||
"print(\"\\n✅ Method 4: Tools with error handling\")\n",
|
||||
"print(\" ✓ Handle errors gracefully\")\n",
|
||||
"print(\" ✓ Return meaningful error messages\")\n",
|
||||
"print(\" ✓ Prevent agent crashes\")\n",
|
||||
"\n",
|
||||
"print(\"\\n🎉 All tool methods demonstrated!\")\n",
|
||||
"print(\"💡 Key Points:\")\n",
|
||||
"print(\" • Always include RunContext as first parameter\")\n",
|
||||
"print(\" • Use descriptive docstrings - the agent uses them\")\n",
|
||||
"print(\" • Handle errors gracefully in your tools\")\n",
|
||||
"print(\" • Tools can return strings, dicts, or complex objects\")\n",
|
||||
"print(\" • Test your tools individually before using with agent\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "6b9ee8b1",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🧪 Testing additional tools:\n",
|
||||
"\n",
|
||||
"📊 Testing database query tool:\n",
|
||||
"Response: <think>\n",
|
||||
"Okay, let me see. The user asked to search for users in the database where active is true. I called the database_query function with table 'users' and condition 'active = true'. The response came back with the results: Alice, Bob, Charlie. Now I need to present these results to the user in a clear way.\n",
|
||||
"\n",
|
||||
"First, I should confirm that the query was executed successfully. Then list out the active users. Maybe use a friendly message to inform them of the results. Let me check if there are any other steps needed, but since the user just wanted the search, providing the list should be sufficient. I'll format the response to be easy to read.\n",
|
||||
"</think>\n",
|
||||
"\n",
|
||||
"Here are the active users found in the database:\n",
|
||||
"\n",
|
||||
"- Alice\n",
|
||||
"- Bob\n",
|
||||
"- Charlie\n",
|
||||
"\n",
|
||||
"Let me know if you need any additional information!\n",
|
||||
"\n",
|
||||
"🔍 Testing file search tool:\n",
|
||||
"Response: <think>\n",
|
||||
"Okay, the user asked to find all Python files containing 'main'. I used the search_files function with pattern 'main' and file_type 'py'. The response shows one match: 'main.py'. So, I should inform the user that there's one file found, which is 'main.py'. I'll present it clearly and ask if they need more details.\n",
|
||||
"</think>\n",
|
||||
"\n",
|
||||
"One Python file was found containing 'main':\n",
|
||||
"\n",
|
||||
"- `main.py`\n",
|
||||
"\n",
|
||||
"Let me know if you'd like to see the contents of this file or need further assistance!\n",
|
||||
"\n",
|
||||
"➗ Testing safe division tool:\n",
|
||||
"Response: <think>\n",
|
||||
"</think>\n",
|
||||
"\n",
|
||||
"The result of dividing 100 by 7 is approximately 14.2857.\n",
|
||||
"\n",
|
||||
"✨ All tools working correctly!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Test the new tools\n",
|
||||
"print(\"🧪 Testing additional tools:\")\n",
|
||||
"\n",
|
||||
"# Test database tool\n",
|
||||
"print(\"\\n📊 Testing database query tool:\")\n",
|
||||
"result_db = await agent2.run(\"Search for users in the database where active = true\")\n",
|
||||
"print(f\"Response: {result_db.output}\")\n",
|
||||
"\n",
|
||||
"# Test file search tool\n",
|
||||
"print(\"\\n🔍 Testing file search tool:\")\n",
|
||||
"result_files = await agent2.run(\"Find all Python files containing 'main'\")\n",
|
||||
"print(f\"Response: {result_files.output}\")\n",
|
||||
"\n",
|
||||
"# Test division tool\n",
|
||||
"print(\"\\n➗ Testing safe division tool:\")\n",
|
||||
"result_div = await agent2.run(\"What is 100 divided by 7?\")\n",
|
||||
"print(f\"Response: {result_div.output}\")\n",
|
||||
"\n",
|
||||
"print(\"\\n✨ All tools working correctly!\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "pydantic_ai",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
[project]
|
||||
name = "pydantic-ai"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"ipykernel>=6.29.5",
|
||||
"ipywidgets>=8.1.7",
|
||||
"pip>=25.1.1",
|
||||
"pydantic-ai-slim[duckduckgo,mcp,openai]>=0.2.18",
|
||||
"asyncpg>=0.30.0",
|
||||
"fastapi>=0.115.4",
|
||||
"logfire[asyncpg,fastapi,sqlite3,httpx]>=2.6",
|
||||
"python-multipart>=0.0.17",
|
||||
"rich>=13.9.2",
|
||||
"uvicorn>=0.32.0",
|
||||
"devtools>=0.12.2",
|
||||
"gradio>=5.9.0; python_version>'3.9'",
|
||||
"mcp[cli]>=1.4.1; python_version >= '3.10'",
|
||||
"groq>=0.28.0",
|
||||
]
|
||||
@@ -0,0 +1,185 @@
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
import logfire
|
||||
|
||||
from groq import BaseModel
|
||||
from pydantic_graph import (
|
||||
BaseNode,
|
||||
End,
|
||||
Graph,
|
||||
GraphRunContext,
|
||||
)
|
||||
from pydantic_graph.persistence.file import FileStatePersistence
|
||||
|
||||
from pydantic_ai.models.openai import OpenAIModel
|
||||
from pydantic_ai.providers.openai import OpenAIProvider
|
||||
from pydantic_ai import Agent, format_as_xml
|
||||
from pydantic_ai.messages import ModelMessage
|
||||
|
||||
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
|
||||
logfire.configure(send_to_logfire="if-token-present")
|
||||
logfire.instrument_pydantic_ai()
|
||||
|
||||
# Configuration for Ollama using OpenAI-compatible endpoint
|
||||
OLLAMA_BASE_URL = "http://localhost:11434"
|
||||
MODEL_NAME = "qwen3:8b" # Updated to use available model
|
||||
# Ollama provides OpenAI-compatible API at /v1/ endpoint
|
||||
ollama_model = OpenAIModel(
|
||||
model_name=MODEL_NAME, provider=OpenAIProvider(base_url=f"{OLLAMA_BASE_URL}/v1")
|
||||
)
|
||||
|
||||
# MODEL_NAME = "hf.co/unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF:Q4_K_XL" # Updated to use available model
|
||||
# BaseModel = OpenAIModel(
|
||||
# model_name=MODEL_NAME,
|
||||
# provider=OpenAIProvider(base_url=f"{OLLAMA_BASE_URL}/v1"),
|
||||
# )
|
||||
|
||||
ask_agent = Agent(ollama_model, output_type=str)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestionState:
|
||||
question: str | None = None
|
||||
ask_agent_messages: list[ModelMessage] = field(default_factory=list)
|
||||
evaluate_agent_messages: list[ModelMessage] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ask(BaseNode[QuestionState]):
|
||||
async def run(self, ctx: GraphRunContext[QuestionState]) -> Answer:
|
||||
result = await ask_agent.run(
|
||||
"Ask a simple question with a single correct answer.",
|
||||
message_history=ctx.state.ask_agent_messages,
|
||||
)
|
||||
ctx.state.ask_agent_messages += result.all_messages()
|
||||
ctx.state.question = result.output
|
||||
return Answer(result.output)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Answer(BaseNode[QuestionState]):
|
||||
question: str
|
||||
|
||||
async def run(self, ctx: GraphRunContext[QuestionState]) -> Evaluate:
|
||||
answer = input(f"{self.question}: ")
|
||||
return Evaluate(answer)
|
||||
|
||||
|
||||
class EvaluationOutput(BaseModel, use_attribute_docstrings=True):
|
||||
correct: bool
|
||||
"""Whether the answer is correct."""
|
||||
comment: str
|
||||
"""Comment on the answer, reprimand the user if the answer is wrong."""
|
||||
|
||||
|
||||
evaluate_agent = Agent(
|
||||
ollama_model,
|
||||
output_type=EvaluationOutput,
|
||||
system_prompt="Given a question and answer, evaluate if the answer is correct.",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Evaluate(BaseNode[QuestionState, None, str]):
|
||||
answer: str
|
||||
|
||||
async def run(
|
||||
self,
|
||||
ctx: GraphRunContext[QuestionState],
|
||||
) -> End[str] | Reprimand:
|
||||
assert ctx.state.question is not None
|
||||
result = await evaluate_agent.run(
|
||||
format_as_xml({"question": ctx.state.question, "answer": self.answer}),
|
||||
message_history=ctx.state.evaluate_agent_messages,
|
||||
)
|
||||
ctx.state.evaluate_agent_messages += result.all_messages()
|
||||
if result.output.correct:
|
||||
return End(result.output.comment)
|
||||
else:
|
||||
return Reprimand(result.output.comment)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Reprimand(BaseNode[QuestionState]):
|
||||
comment: str
|
||||
|
||||
async def run(self, ctx: GraphRunContext[QuestionState]) -> Ask:
|
||||
print(f"Comment: {self.comment}")
|
||||
ctx.state.question = None
|
||||
return Ask()
|
||||
|
||||
|
||||
question_graph = Graph(
|
||||
nodes=(Ask, Answer, Evaluate, Reprimand), state_type=QuestionState
|
||||
)
|
||||
|
||||
|
||||
async def run_as_continuous():
|
||||
state = QuestionState()
|
||||
node = Ask()
|
||||
end = await question_graph.run(node, state=state)
|
||||
print("END:", end.output)
|
||||
|
||||
|
||||
async def run_as_cli(answer: str | None):
|
||||
persistence = FileStatePersistence(Path("question_graph.json"))
|
||||
persistence.set_graph_types(question_graph)
|
||||
|
||||
# Add type annotation for node
|
||||
node: BaseNode[QuestionState, None, str] | End[str]
|
||||
|
||||
if snapshot := await persistence.load_next():
|
||||
state = snapshot.state
|
||||
assert answer is not None, (
|
||||
'answer required, usage "uv run -m pydantic_ai_examples.question_graph cli <answer>"'
|
||||
)
|
||||
node = Evaluate(answer)
|
||||
else:
|
||||
state = QuestionState()
|
||||
node = Ask()
|
||||
# debug(state, node)
|
||||
|
||||
async with question_graph.iter(node, state=state, persistence=persistence) as run:
|
||||
while True:
|
||||
node = await run.next()
|
||||
if isinstance(node, End):
|
||||
print("END:", node.data)
|
||||
history = await persistence.load_all()
|
||||
print("history:", "\n".join(str(e.node) for e in history), sep="\n")
|
||||
print("Finished!")
|
||||
break
|
||||
elif isinstance(node, Answer):
|
||||
print(node.question)
|
||||
break
|
||||
# otherwise just continue
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
try:
|
||||
sub_command = sys.argv[1]
|
||||
assert sub_command in ("continuous", "cli", "mermaid")
|
||||
except (IndexError, AssertionError):
|
||||
print(
|
||||
"Usage:\n"
|
||||
" uv run -m pydantic_ai_examples.question_graph mermaid\n"
|
||||
"or:\n"
|
||||
" uv run -m pydantic_ai_examples.question_graph continuous\n"
|
||||
"or:\n"
|
||||
" uv run -m pydantic_ai_examples.question_graph cli [answer]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if sub_command == "mermaid":
|
||||
print(question_graph.mermaid_code(start_node=Ask))
|
||||
elif sub_command == "continuous":
|
||||
asyncio.run(run_as_continuous())
|
||||
else:
|
||||
a = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
asyncio.run(run_as_cli(a))
|
||||
@@ -0,0 +1,266 @@
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
|
||||
import asyncpg
|
||||
import httpx
|
||||
import logfire
|
||||
import pydantic_core
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import TypeAdapter
|
||||
from typing_extensions import AsyncGenerator
|
||||
|
||||
from pydantic_ai import RunContext
|
||||
from pydantic_ai.models.openai import OpenAIModel
|
||||
from pydantic_ai.providers.openai import OpenAIProvider
|
||||
from pydantic_ai.agent import Agent
|
||||
|
||||
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
|
||||
logfire.configure(send_to_logfire="if-token-present")
|
||||
logfire.instrument_asyncpg()
|
||||
logfire.instrument_pydantic_ai()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Deps:
|
||||
ollama_client: AsyncOpenAI # Using Ollama's OpenAI-compatible endpoint
|
||||
pool: asyncpg.Pool
|
||||
|
||||
|
||||
# Configuration for Ollama using OpenAI-compatible endpoint
|
||||
OLLAMA_BASE_URL = "http://localhost:11434"
|
||||
MODEL_NAME = "qwen3:8b" # Updated to use available model
|
||||
# Common embedding model - pull with: docker exec <ollama-container> ollama pull all-minilm
|
||||
EMBEDDING_MODEL = "mxbai-embed-large" # all-minilm Alternative: try "mxbai-embed-large", "nomic-embed-text" if all-minilm not available
|
||||
|
||||
# Embedding dimensions for different models
|
||||
EMBEDDING_DIMENSIONS = {
|
||||
"all-minilm": 384,
|
||||
"mxbai-embed-large": 1024,
|
||||
"nomic-embed-text": 768,
|
||||
"text-embedding-3-small": 1536, # OpenAI model for reference
|
||||
"bge-large": 1024,
|
||||
"snowflake-arctic-embed": 1024
|
||||
}
|
||||
|
||||
# Get dimensions for current model
|
||||
VECTOR_DIMENSIONS = EMBEDDING_DIMENSIONS.get(EMBEDDING_MODEL, 1024) # Default to 1024
|
||||
|
||||
# Ollama provides OpenAI-compatible API at /v1/ endpoint
|
||||
ollama_model = OpenAIModel(
|
||||
model_name=MODEL_NAME, provider=OpenAIProvider(base_url=f"{OLLAMA_BASE_URL}/v1")
|
||||
)
|
||||
|
||||
agent = Agent(ollama_model, deps_type=Deps)
|
||||
|
||||
|
||||
@agent.tool
|
||||
async def retrieve(context: RunContext[Deps], search_query: str) -> str:
|
||||
"""Retrieve documentation sections based on a search query.
|
||||
|
||||
Args:
|
||||
context: The call context.
|
||||
search_query: The search query.
|
||||
"""
|
||||
with logfire.span(
|
||||
"create embedding for {search_query=}", search_query=search_query
|
||||
):
|
||||
embedding = await context.deps.ollama_client.embeddings.create(
|
||||
input=search_query,
|
||||
model=EMBEDDING_MODEL,
|
||||
)
|
||||
|
||||
assert (
|
||||
len(embedding.data) == 1
|
||||
), f"Expected 1 embedding, got {len(embedding.data)}, doc query: {search_query!r}"
|
||||
embedding = embedding.data[0].embedding
|
||||
embedding_json = pydantic_core.to_json(embedding).decode()
|
||||
rows = await context.deps.pool.fetch(
|
||||
"SELECT url, title, content FROM doc_sections ORDER BY embedding <-> $1 LIMIT 8",
|
||||
embedding_json,
|
||||
)
|
||||
return "\n\n".join(
|
||||
f'# {row["title"]}\nDocumentation URL:{row["url"]}\n\n{row["content"]}\n'
|
||||
for row in rows
|
||||
)
|
||||
|
||||
|
||||
async def run_agent(question: str):
|
||||
"""Entry point to run the agent and perform RAG based question answering."""
|
||||
ollama_client = AsyncOpenAI(base_url=f"{OLLAMA_BASE_URL}/v1", api_key="dummy-key")
|
||||
logfire.instrument_openai(ollama_client)
|
||||
|
||||
logfire.info('Asking "{question}"', question=question)
|
||||
|
||||
async with database_connect(False) as pool:
|
||||
deps = Deps(ollama_client=ollama_client, pool=pool)
|
||||
answer = await agent.run(question, deps=deps)
|
||||
print(answer.output)
|
||||
|
||||
|
||||
#######################################################
|
||||
# The rest of this file is dedicated to preparing the #
|
||||
# search database, and some utilities. #
|
||||
#######################################################
|
||||
|
||||
# JSON document from
|
||||
# https://gist.github.com/samuelcolvin/4b5bb9bb163b1122ff17e29e48c10992
|
||||
DOCS_JSON = (
|
||||
"https://gist.githubusercontent.com/"
|
||||
"samuelcolvin/4b5bb9bb163b1122ff17e29e48c10992/raw/"
|
||||
"80c5925c42f1442c24963aaf5eb1a324d47afe95/logfire_docs.json"
|
||||
)
|
||||
|
||||
|
||||
async def build_search_db():
|
||||
"""Build the search database."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(DOCS_JSON)
|
||||
response.raise_for_status()
|
||||
sections = sessions_ta.validate_json(response.content)
|
||||
|
||||
ollama_client = AsyncOpenAI(base_url=f"{OLLAMA_BASE_URL}/v1", api_key="dummy-key")
|
||||
logfire.instrument_openai(ollama_client)
|
||||
|
||||
async with database_connect(True) as pool:
|
||||
with logfire.span("create schema"):
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.transaction():
|
||||
await conn.execute(DB_SCHEMA)
|
||||
|
||||
sem = asyncio.Semaphore(10)
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for section in sections:
|
||||
tg.create_task(insert_doc_section(sem, ollama_client, pool, section))
|
||||
|
||||
|
||||
async def insert_doc_section(
|
||||
sem: asyncio.Semaphore,
|
||||
ollama_client: AsyncOpenAI,
|
||||
pool: asyncpg.Pool,
|
||||
section: DocsSection,
|
||||
) -> None:
|
||||
async with sem:
|
||||
url = section.url()
|
||||
exists = await pool.fetchval("SELECT 1 FROM doc_sections WHERE url = $1", url)
|
||||
if exists:
|
||||
logfire.info("Skipping {url=}", url=url)
|
||||
return
|
||||
|
||||
with logfire.span("create embedding for {url=}", url=url):
|
||||
embedding = await ollama_client.embeddings.create(
|
||||
input=section.embedding_content(),
|
||||
model=EMBEDDING_MODEL,
|
||||
)
|
||||
assert (
|
||||
len(embedding.data) == 1
|
||||
), f"Expected 1 embedding, got {len(embedding.data)}, doc section: {section}"
|
||||
embedding = embedding.data[0].embedding
|
||||
embedding_json = pydantic_core.to_json(embedding).decode()
|
||||
await pool.execute(
|
||||
"INSERT INTO doc_sections (url, title, content, embedding) VALUES ($1, $2, $3, $4)",
|
||||
url,
|
||||
section.title,
|
||||
section.content,
|
||||
embedding_json,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocsSection:
|
||||
id: int
|
||||
parent: int | None
|
||||
path: str
|
||||
level: int
|
||||
title: str
|
||||
content: str
|
||||
|
||||
def url(self) -> str:
|
||||
url_path = re.sub(r"\.md$", "", self.path)
|
||||
return (
|
||||
f'https://logfire.pydantic.dev/docs/{url_path}/#{slugify(self.title, "-")}'
|
||||
)
|
||||
|
||||
def embedding_content(self) -> str:
|
||||
return "\n\n".join((f"path: {self.path}", f"title: {self.title}", self.content))
|
||||
|
||||
|
||||
sessions_ta = TypeAdapter(list[DocsSection])
|
||||
|
||||
|
||||
# pyright: reportUnknownMemberType=false
|
||||
# pyright: reportUnknownVariableType=false
|
||||
@asynccontextmanager
|
||||
async def database_connect(
|
||||
create_db: bool = False,
|
||||
) -> AsyncGenerator[asyncpg.Pool, None]:
|
||||
server_dsn, database = (
|
||||
"postgresql://postgres:postgres@localhost:54320",
|
||||
"pydantic_ai_rag",
|
||||
)
|
||||
if create_db:
|
||||
with logfire.span("check and create DB"):
|
||||
conn = await asyncpg.connect(server_dsn)
|
||||
try:
|
||||
db_exists = await conn.fetchval(
|
||||
"SELECT 1 FROM pg_database WHERE datname = $1", database
|
||||
)
|
||||
if not db_exists:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
pool = await asyncpg.create_pool(f"{server_dsn}/{database}")
|
||||
try:
|
||||
yield pool
|
||||
finally:
|
||||
await pool.close()
|
||||
|
||||
|
||||
DB_SCHEMA = f"""
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS doc_sections (
|
||||
id serial PRIMARY KEY,
|
||||
url text NOT NULL UNIQUE,
|
||||
title text NOT NULL,
|
||||
content text NOT NULL,
|
||||
-- {EMBEDDING_MODEL} returns a vector of {VECTOR_DIMENSIONS} floats
|
||||
embedding vector({VECTOR_DIMENSIONS}) NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_sections_embedding ON doc_sections USING hnsw (embedding vector_l2_ops);
|
||||
"""
|
||||
|
||||
|
||||
def slugify(value: str, separator: str, unicode: bool = False) -> str:
|
||||
"""Slugify a string, to make it URL friendly."""
|
||||
# Taken unchanged from https://github.com/Python-Markdown/markdown/blob/3.7/markdown/extensions/toc.py#L38
|
||||
if not unicode:
|
||||
# Replace Extended Latin characters with ASCII, i.e. `žlutý` => `zluty`
|
||||
value = unicodedata.normalize("NFKD", value)
|
||||
value = value.encode("ascii", "ignore").decode("ascii")
|
||||
value = re.sub(r"[^\w\s-]", "", value).strip().lower()
|
||||
return re.sub(rf"[{separator}\s]+", separator, value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
action = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
if action == "build":
|
||||
asyncio.run(build_search_db())
|
||||
elif action == "search":
|
||||
if len(sys.argv) == 3:
|
||||
q = sys.argv[2]
|
||||
else:
|
||||
q = "How do I configure logfire to work with FastAPI?"
|
||||
asyncio.run(run_agent(q))
|
||||
else:
|
||||
print(
|
||||
"uv run --extra examples -m pydantic_ai_examples.rag build|search",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,80 @@
|
||||
[project]
|
||||
name = "notebooks"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"aiohttp>=3.12.7",
|
||||
"bitsandbytes>=0.46.0",
|
||||
"blake3>=1.0.5",
|
||||
"cachetools>=6.0.0",
|
||||
"chromadb>=0.6.3",
|
||||
"cloudpickle>=3.1.1",
|
||||
"compressed-tensors>=0.9.4",
|
||||
"ddgs==9.10.0",
|
||||
"depyf>=0.18.0",
|
||||
"einops>=0.8.1",
|
||||
"fastapi[standard]>=0.115.0",
|
||||
"filelock>=3.16.1",
|
||||
"gguf>=0.13.0",
|
||||
"httpx>=0.28.1",
|
||||
"huggingface-hub[hf-xet]>=0.32.0",
|
||||
"importlib-metadata>=8.6.1 ; python_full_version < '3.10'",
|
||||
"ipykernel>=6.29.5",
|
||||
"ipywidgets>=8.1.7",
|
||||
"jinja2>=3.1.6",
|
||||
"langchain>=0.3.25",
|
||||
"langchain-community>=0.3.25",
|
||||
"lark>=1.2.2",
|
||||
"llama-cpp-python>=0.3.9",
|
||||
"llguidance>=0.7.11,<0.8.0 ; platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'x86_64'",
|
||||
"lm-format-enforcer>=0.10.11,<0.11",
|
||||
"mcp[cli]>=1.9.4",
|
||||
"mistral-common[opencv]>=1.5.4",
|
||||
"msgspec>=0.19.0",
|
||||
"ninja>=1.11.1.4",
|
||||
"numpy>=2.2.6",
|
||||
"openai>=1.52.0",
|
||||
"opencv-python-headless>=4.11.0",
|
||||
"opentelemetry-api>=1.26.0",
|
||||
"opentelemetry-exporter-otlp>=1.26.0",
|
||||
"opentelemetry-sdk>=1.26.0",
|
||||
"opentelemetry-semantic-conventions-ai>=0.4.1",
|
||||
"outlines>=0.2.3",
|
||||
"partial-json-parser>=0.2.1.1.post5",
|
||||
"pillow>=11.2.1",
|
||||
"prometheus-client>=0.18.0",
|
||||
"prometheus-fastapi-instrumentator>=7.0.0",
|
||||
"protobuf>=5.29.5",
|
||||
"psutil>=7.0.0",
|
||||
"py-cpuinfo>=9.0.0",
|
||||
"pydantic>=2.10",
|
||||
"pypdf>=5.6.0",
|
||||
"python-json-logger>=3.3.0",
|
||||
"pyyaml>=6.0.2",
|
||||
"pyzmq>=25.0.0",
|
||||
"regex>=2024.11.6",
|
||||
"requests>=2.26.0",
|
||||
"scipy>=1.15.3",
|
||||
"sentence-transformers>=4.1.0",
|
||||
"sentencepiece>=0.2.0",
|
||||
"setuptools>=77.0.3,<80 ; python_full_version >= '3.12'",
|
||||
"six>=1.16.0 ; python_full_version >= '3.12'",
|
||||
"termcolor>=3.1.0",
|
||||
"tiktoken>=0.6.0",
|
||||
"tokenizers>=0.21.1",
|
||||
"tqdm>=4.67.1",
|
||||
"transformers>=4.51.1",
|
||||
"typing-extensions>=4.10",
|
||||
"unsloth>=2024.8",
|
||||
"watchfiles>=1.0.5",
|
||||
"xformers>=0.0.30",
|
||||
"xgrammar>=0.1.19 ; platform_machine == 'aarch64' or platform_machine == 'x86_64'",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = [
|
||||
"pydantic_ai",
|
||||
"faster-whisper",
|
||||
]
|
||||
@@ -0,0 +1,62 @@
|
||||
regex # Replace re for higher-performance regex matching
|
||||
cachetools
|
||||
psutil
|
||||
sentencepiece # Required for LLaMA tokenizer.
|
||||
numpy
|
||||
requests >= 2.26.0
|
||||
tqdm
|
||||
blake3
|
||||
py-cpuinfo
|
||||
transformers >= 4.51.1
|
||||
huggingface-hub[hf_xet] >= 0.32.0 # Required for Xet downloads.
|
||||
tokenizers >= 0.21.1 # Required for fast incremental detokenization.
|
||||
protobuf # Required by LlamaTokenizer.
|
||||
fastapi[standard] >= 0.115.0 # Required by FastAPI's form models in the OpenAI API server's audio transcriptions endpoint.
|
||||
aiohttp
|
||||
openai >= 1.52.0 # Ensure modern openai package (ensure types module present and max_completion_tokens field support)
|
||||
pydantic >= 2.10
|
||||
prometheus_client >= 0.18.0
|
||||
pillow # Required for image processing
|
||||
prometheus-fastapi-instrumentator >= 7.0.0
|
||||
tiktoken >= 0.6.0 # Required for DBRX tokenizer
|
||||
lm-format-enforcer >= 0.10.11, < 0.11
|
||||
llguidance >= 0.7.11, < 0.8.0; platform_machine == "x86_64" or platform_machine == "arm64" or platform_machine == "aarch64"
|
||||
outlines
|
||||
lark >= 1.2.2
|
||||
xgrammar >= 0.1.19; platform_machine == "x86_64" or platform_machine == "aarch64"
|
||||
typing_extensions >= 4.10
|
||||
filelock >= 3.16.1 # need to contain https://github.com/tox-dev/filelock/pull/317
|
||||
partial-json-parser # used for parsing partial JSON outputs
|
||||
pyzmq >= 25.0.0
|
||||
msgspec
|
||||
gguf >= 0.13.0
|
||||
importlib_metadata; python_version < '3.10'
|
||||
mistral_common[opencv] >= 1.5.4
|
||||
opencv-python-headless >= 4.11.0 # required for video IO
|
||||
pyyaml
|
||||
six>=1.16.0; python_version > '3.11' # transitive dependency of pandas that needs to be the latest version for python 3.12
|
||||
setuptools>=77.0.3,<80; python_version > '3.11' # Setuptools is used by triton, we need to ensure a modern version is installed for 3.12+ so that it does not try to import distutils, which was removed in 3.12
|
||||
einops # Required for Qwen2-VL.
|
||||
compressed-tensors >= 0.9.4 # required for compressed-tensors
|
||||
depyf>=0.18.0 # required for profiling and debugging with compilation config
|
||||
cloudpickle # allows pickling lambda functions in model_executor/models/registry.py
|
||||
watchfiles # required for http server to monitor the updates of TLS files
|
||||
python-json-logger # Used by logging as per examples/others/logging_configuration.md
|
||||
scipy # Required for phi-4-multimodal-instruct
|
||||
ninja # Required for xgrammar, rocm, tpu, xpu
|
||||
opentelemetry-sdk>=1.26.0 # vllm.tracing
|
||||
opentelemetry-api>=1.26.0 # vllm.tracing
|
||||
opentelemetry-exporter-otlp>=1.26.0 # vllm.tracing
|
||||
opentelemetry-semantic-conventions-ai>=0.4.1 # vllm.tracing
|
||||
ipykernel
|
||||
ipywidgets
|
||||
termcolor
|
||||
unsloth
|
||||
jinja2
|
||||
llama-cpp-python
|
||||
langchain
|
||||
langchain-community
|
||||
sentence-transformers
|
||||
chromadb
|
||||
pypdf
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "536c8892",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🦥 Unsloth: Will patch your computer to enable 2x faster free finetuning.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/home/user/projects/ai_stack/backend/.venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
|
||||
" from .autonotebook import tqdm as notebook_tqdm\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🦥 Unsloth Zoo will now patch everything to make training faster!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from unsloth import FastLanguageModel\n",
|
||||
"import torch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "07d7e4a8",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==((====))== Unsloth 2025.5.9: Fast Llama patching. Transformers: 4.52.4.\n",
|
||||
" \\\\ /| NVIDIA RTX 3500 Ada Generation Laptop GPU. Num GPUs = 1. Max memory: 11.607 GB. Platform: Linux.\n",
|
||||
"O^O/ \\_/ \\ Torch: 2.7.0+cu126. CUDA: 8.9. CUDA Toolkit: 12.6. Triton: 3.3.0\n",
|
||||
"\\ / Bfloat16 = TRUE. FA [Xformers = 0.0.30. FA2 = False]\n",
|
||||
" \"-____-\" Free license: http://github.com/unslothai/unsloth\n",
|
||||
"Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"model, tokenizer = FastLanguageModel.from_pretrained(\n",
|
||||
" model_name = \"unsloth/llama-3.1-8b-bnb-4bit\", # Well-tested model\n",
|
||||
" max_seq_length = 2048, # Context length - can be longer, but uses more memory\n",
|
||||
" load_in_4bit = True, # 4bit uses much less memory\n",
|
||||
" load_in_8bit = False, # A bit more accurate, uses 2x memory\n",
|
||||
" full_finetuning = False, # We have full finetuning now!\n",
|
||||
" # token = \"hf_...\", # use one if using gated models\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "ae5430bd",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🦥 Unsloth: Will patch your computer to enable 2x faster free finetuning.\n",
|
||||
"🦥 Unsloth Zoo will now patch everything to make training faster!\n",
|
||||
"==((====))== Unsloth 2025.5.9: Fast Qwen2 patching. Transformers: 4.52.4.\n",
|
||||
" \\\\ /| NVIDIA RTX 3500 Ada Generation Laptop GPU. Num GPUs = 1. Max memory: 11.607 GB. Platform: Linux.\n",
|
||||
"O^O/ \\_/ \\ Torch: 2.7.0+cu126. CUDA: 8.9. CUDA Toolkit: 12.6. Triton: 3.3.0\n",
|
||||
"\\ / Bfloat16 = TRUE. FA [Xformers = 0.0.30. FA2 = False]\n",
|
||||
" \"-____-\" Free license: http://github.com/unslothai/unsloth\n",
|
||||
"Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from unsloth import FastQwen2Model\n",
|
||||
"import torch\n",
|
||||
"\n",
|
||||
"max_seq_length = 2048 # Choose any! We auto support RoPE Scaling internally!\n",
|
||||
"dtype = None # None for auto detection. Float16 for Tesla T4, V100, Bfloat16 for Ampere+\n",
|
||||
"load_in_4bit = True # Use 4bit quantization to reduce memory usage. Can be False.\n",
|
||||
"\n",
|
||||
"# 4bit pre quantized models we support for 4x faster downloading + no OOMs.\n",
|
||||
"fourbit_models = [\n",
|
||||
" \"unsloth/Meta-Llama-3.1-8B-bnb-4bit\", # Llama-3.1 2x faster\n",
|
||||
" \"unsloth/Meta-Llama-3.1-70B-bnb-4bit\",\n",
|
||||
" \"unsloth/Mistral-Small-Instruct-2409\", # Mistral 22b 2x faster!\n",
|
||||
" \"unsloth/mistral-7b-instruct-v0.3-bnb-4bit\",\n",
|
||||
" \"unsloth/Phi-3.5-mini-instruct\", # Phi-3.5 2x faster!\n",
|
||||
" \"unsloth/Phi-3-medium-4k-instruct\",\n",
|
||||
" \"unsloth/gemma-2-27b-bnb-4bit\", # Gemma 2x faster!\n",
|
||||
"\n",
|
||||
" \"unsloth/Llama-3.2-1B-bnb-4bit\", # NEW! Llama 3.2 models\n",
|
||||
" \"unsloth/Llama-3.2-1B-Instruct-bnb-4bit\",\n",
|
||||
" \"unsloth/Llama-3.2-3B-Instruct-bnb-4bit\",\n",
|
||||
"] # More models at https://huggingface.co/unsloth\n",
|
||||
"\n",
|
||||
"qwen_models = [\n",
|
||||
" \"unsloth/Qwen2.5-Coder-32B-Instruct\", # Qwen 2.5 Coder 2x faster\n",
|
||||
" \"unsloth/Qwen2.5-Coder-7B\",\n",
|
||||
" \"unsloth/Qwen2.5-14B-Instruct\", # 14B fits in a 16GB card\n",
|
||||
" \"unsloth/Qwen2.5-7B\",\n",
|
||||
" \"unsloth/Qwen2.5-72B-Instruct\", # 72B fits in a 48GB card\n",
|
||||
"] # More models at https://huggingface.co/unsloth\n",
|
||||
"\n",
|
||||
"model, tokenizer = FastQwen2Model.from_pretrained(\n",
|
||||
" model_name=\"unsloth/Qwen2.5-Coder-1.5B-Instruct\",\n",
|
||||
" max_seq_length=None,\n",
|
||||
" dtype=None,\n",
|
||||
" load_in_4bit=False,\n",
|
||||
" fix_tokenizer=False\n",
|
||||
" # token = \"hf_...\", # use one if using gated models like meta-llama/Llama-2-7b-hf\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a8dfe1a4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.16"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
Generated
+4757
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
from typing import Any
|
||||
import httpx
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Initialize FastMCP server
|
||||
mcp = FastMCP("weather")
|
||||
|
||||
# Constants
|
||||
NWS_API_BASE = "https://api.weather.gov"
|
||||
USER_AGENT = "weather-app/1.0"
|
||||
|
||||
|
||||
async def make_nws_request(url: str) -> dict[str, Any] | None:
|
||||
"""Make a request to the NWS API with proper error handling."""
|
||||
headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(url, headers=headers, timeout=30.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def format_alert(feature: dict) -> str:
|
||||
"""Format an alert feature into a readable string."""
|
||||
props = feature["properties"]
|
||||
return f"""
|
||||
Event: {props.get('event', 'Unknown')}
|
||||
Area: {props.get('areaDesc', 'Unknown')}
|
||||
Severity: {props.get('severity', 'Unknown')}
|
||||
Description: {props.get('description', 'No description available')}
|
||||
Instructions: {props.get('instruction', 'No specific instructions provided')}
|
||||
"""
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_alerts(state: str) -> str:
|
||||
"""Get weather alerts for a US state.
|
||||
|
||||
Args:
|
||||
state: Two-letter US state code (e.g. CA, NY)
|
||||
"""
|
||||
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
|
||||
data = await make_nws_request(url)
|
||||
|
||||
if not data or "features" not in data:
|
||||
return "Unable to fetch alerts or no alerts found."
|
||||
|
||||
if not data["features"]:
|
||||
return "No active alerts for this state."
|
||||
|
||||
alerts = [format_alert(feature) for feature in data["features"]]
|
||||
return "\n---\n".join(alerts)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_forecast(latitude: float, longitude: float) -> str:
|
||||
"""Get weather forecast for a location.
|
||||
|
||||
Args:
|
||||
latitude: Latitude of the location
|
||||
longitude: Longitude of the location
|
||||
"""
|
||||
# First get the forecast grid endpoint
|
||||
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
|
||||
points_data = await make_nws_request(points_url)
|
||||
|
||||
if not points_data:
|
||||
return "Unable to fetch forecast data for this location."
|
||||
|
||||
# Get the forecast URL from the points response
|
||||
forecast_url = points_data["properties"]["forecast"]
|
||||
forecast_data = await make_nws_request(forecast_url)
|
||||
|
||||
if not forecast_data:
|
||||
return "Unable to fetch detailed forecast."
|
||||
|
||||
# Format the periods into a readable forecast
|
||||
periods = forecast_data["properties"]["periods"]
|
||||
forecasts = []
|
||||
for period in periods[:5]: # Only show next 5 periods
|
||||
forecast = f"""
|
||||
{period['name']}:
|
||||
Temperature: {period['temperature']}°{period['temperatureUnit']}
|
||||
Wind: {period['windSpeed']} {period['windDirection']}
|
||||
Forecast: {period['detailedForecast']}
|
||||
"""
|
||||
forecasts.append(forecast)
|
||||
|
||||
return "\n---\n".join(forecasts)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Initialize and run the server
|
||||
mcp.run(transport="stdio")
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "fa1b6595",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[{'title': 'Python (programming language)', 'href': 'https://en.wikipedia.org/wiki/Python_(programming_language)', 'body': \"Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically type-checked and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming.Guido van Rossum began working on Python in the late 1980s as a successor to the ABC programming language. Python 3.0, released in 2008, was a major revision and not completely backward-compatible with earlier versions. Beginning with Python 3.5, capabilities and keywords for typing were added to the language, allowing optional static typing. As of 2026, the Python Software Foundation supports Python 3.10, 3.11, 3.12, 3.13, and 3.14, following the project's annual release cycle and five-year support policy. Python 3.15 is currently in the alpha development phase, and the stable release is expected to come out in October 2026. Earlier versions in the 3.x series have reached end-of-life and no longer receive security updates. Python has gained widespread use in the machine learning community. It is widely taught as an introductory programming language. Since 2003, Python has consistently ranked in the top ten of the most popular programming languages in the TIOBE Programming Community Index, which ranks based on searches in 24 platforms.\"}, {'title': 'Python (programming language)', 'href': 'https://grokipedia.com/page/Python_(programming_language)', 'body': 'Python (programming language) Python is an interpreted, high-level, general-purpose programming language designed for code readability and simplicity, featuring a dynamic type system and support for...'}, {'title': 'Programming for Everybody (Getting Started with Python) |', 'href': 'https://www.coursera.org/learn/python', 'body': '... aims to teach everyone the basics ofprogrammingcomputers usingPython... In this module you will set things up so you can writePythonprograms.'}, {'title': 'Python Programming - Wikibooks, open books for an open world', 'href': 'https://en.wikibooks.org/wiki/Python_Programming', 'body': 'A printable version ofPythonProgrammingis available. ... Retrieved from \" https://en.wikibooks.org/w/index.php?title=Python_Programming&oldid ...'}, {'title': 'Computer Programming for Everybody | Python.org', 'href': 'https://www.python.org/doc/essays/cp4e/', 'body': \"Python'sprogrammingenvironment and documentation are less than ideal for teaching to novices. ... we will initially use thePythonprogramming...\"}]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from ddgs import DDGS\n",
|
||||
"\n",
|
||||
"results = DDGS().text(\"python programming\", max_results=5)\n",
|
||||
"print(results)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.10"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# include:
|
||||
# - compose.observability.yaml
|
||||
services:
|
||||
speaches:
|
||||
extends:
|
||||
file: compose.yaml
|
||||
service: speaches
|
||||
image: ghcr.io/speaches-ai/speaches:latest-cpu
|
||||
build:
|
||||
args:
|
||||
BASE_IMAGE: ubuntu:24.04
|
||||
volumes:
|
||||
- hf-hub-cache:/home/ubuntu/.cache/huggingface/hub
|
||||
volumes:
|
||||
hf-hub-cache:
|
||||
@@ -0,0 +1,24 @@
|
||||
# include:
|
||||
# - compose.observability.yaml
|
||||
# This file is for those who have the CDI Docker feature enabled
|
||||
# https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html
|
||||
# https://docs.docker.com/reference/cli/dockerd/#enable-cdi-devices
|
||||
services:
|
||||
speaches:
|
||||
extends:
|
||||
file: compose.cuda.yaml
|
||||
service: speaches
|
||||
volumes:
|
||||
- hf-hub-cache:/home/ubuntu/.cache/huggingface/hub
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
# WARN: requires Docker Compose 2.24.2
|
||||
# https://docs.docker.com/reference/compose-file/merge/#replace-value
|
||||
devices: !override
|
||||
- capabilities: ["gpu"]
|
||||
driver: cdi
|
||||
device_ids:
|
||||
- nvidia.com/gpu=all
|
||||
volumes:
|
||||
hf-hub-cache:
|
||||
@@ -0,0 +1,23 @@
|
||||
# include:
|
||||
# - compose.observability.yaml
|
||||
services:
|
||||
speaches:
|
||||
extends:
|
||||
file: compose.yaml
|
||||
service: speaches
|
||||
# NOTE: slightly older cuda version is available under 'latest-cuda-12.4.1' and `latest-cuda-12.6.3` tags
|
||||
image: ghcr.io/speaches-ai/speaches:latest-cuda
|
||||
build:
|
||||
args:
|
||||
BASE_IMAGE: nvidia/cuda:12.9.0-cudnn-runtime-ubuntu24.04
|
||||
volumes:
|
||||
- hf-hub-cache:/home/ubuntu/.cache/huggingface/hub
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
volumes:
|
||||
hf-hub-cache:
|
||||
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
speaches:
|
||||
container_name: speaches
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8000:8000
|
||||
develop:
|
||||
watch:
|
||||
- action: rebuild
|
||||
path: ./uv.lock
|
||||
- action: sync+restart
|
||||
path: ./src
|
||||
target: /home/ubuntu/speaches/src
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "--fail", "http://0.0.0.0:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
@@ -0,0 +1,10 @@
|
||||
[project]
|
||||
name = "whisper"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"faster-whisper>=1.1.1",
|
||||
"whisperlivekit>=0.1.9",
|
||||
]
|
||||
Generated
+1068
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
model_size = "large-v3"
|
||||
|
||||
# Run on GPU with FP16
|
||||
model = WhisperModel(model_size, device="cuda", compute_type="float16")
|
||||
|
||||
# or run on GPU with INT8
|
||||
# model = WhisperModel(model_size, device="cuda", compute_type="int8_float16")
|
||||
# or run on CPU with INT8
|
||||
# model = WhisperModel(model_size, device="cpu", compute_type="int8")
|
||||
|
||||
segments, info = model.transcribe("audio.mp3", beam_size=5)
|
||||
|
||||
print(
|
||||
"Detected language '%s' with probability %f"
|
||||
% (info.language, info.language_probability)
|
||||
)
|
||||
|
||||
for segment in segments:
|
||||
print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))
|
||||
+2
-1
@@ -2,5 +2,6 @@
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4",
|
||||
"react-leaflet": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.12.2+sha512.a32540185b964ee30bb4e979e405adc6af59226b438ee4cc19f9e8773667a66d302f5bfee60a39d3cac69e35e4b96e708a71dd002b7e9359c4112a1722ac323f"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Model Setup Scripts
|
||||
|
||||
This directory contains scripts to download and set up AI models for the Ollama service.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### 1. `pull-deepseek-model.sh`
|
||||
Main script that downloads the DeepSeek-R1-0528-Qwen3-8B model in GGUF format from Hugging Face and loads it into Ollama.
|
||||
|
||||
**Features:**
|
||||
- Downloads the Q4_K_XL quantized version (optimal balance of quality and size)
|
||||
- Creates a proper Modelfile with appropriate templates and parameters
|
||||
- Loads the model into Ollama with the name `deepseek-r1-qwen3-8b`
|
||||
- Includes verification and testing steps
|
||||
|
||||
**Requirements:**
|
||||
- `huggingface-hub` Python package (will be installed automatically if missing)
|
||||
- Ollama container running
|
||||
- Internet connection
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Using Makefile (recommended)
|
||||
make setup-ollama # Start Ollama and pull model automatically
|
||||
make pull-deepseek-model # Pull model only (requires Ollama to be running)
|
||||
|
||||
# Or run directly
|
||||
./scripts/pull-deepseek-model.sh
|
||||
```
|
||||
|
||||
### 2. `pull-model-simple.sh`
|
||||
Alternative script that attempts to pull models using `ollama pull` command.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./scripts/pull-model-simple.sh
|
||||
```
|
||||
|
||||
## Makefile Targets
|
||||
|
||||
The main Makefile includes these Ollama-related targets:
|
||||
|
||||
- `make ollama-up` - Start only the Ollama container
|
||||
- `make ollama-down` - Stop only the Ollama container
|
||||
- `make ollama-logs` - View Ollama container logs
|
||||
- `make pull-deepseek-model` - Download and load DeepSeek model
|
||||
- `make setup-ollama` - Complete setup (start Ollama + pull model)
|
||||
|
||||
## Model Information
|
||||
|
||||
**DeepSeek-R1-0528-Qwen3-8B-GGUF**
|
||||
- **Source:** [unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF](https://huggingface.co/unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF)
|
||||
- **Quantization:** Q4_K_XL (4-bit quantization, extra large)
|
||||
- **Size:** ~5.5GB
|
||||
- **Context Length:** 32,768 tokens
|
||||
- **Use Case:** General conversation, reasoning, coding assistance
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Start the entire stack:**
|
||||
```bash
|
||||
make up
|
||||
```
|
||||
|
||||
2. **Or start just Ollama and set up the model:**
|
||||
```bash
|
||||
make setup-ollama
|
||||
```
|
||||
|
||||
3. **Use the model:**
|
||||
```bash
|
||||
# Via Docker
|
||||
docker exec ollama-server ollama run deepseek-r1-qwen3-8b "Hello, how are you?"
|
||||
|
||||
# Via API
|
||||
curl http://localhost:11434/api/generate -d '{
|
||||
"model": "deepseek-r1-qwen3-8b",
|
||||
"prompt": "Hello, how are you?",
|
||||
"stream": false
|
||||
}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Script fails with "Ollama not running":**
|
||||
- Make sure Ollama container is started: `make ollama-up`
|
||||
- Wait a few seconds for the service to be ready
|
||||
|
||||
2. **Download fails:**
|
||||
- Check internet connection
|
||||
- Verify Hugging Face Hub access
|
||||
- Try installing huggingface-hub manually: `pip install huggingface-hub`
|
||||
|
||||
3. **Model loading fails:**
|
||||
- Check if the GGUF file was downloaded completely
|
||||
- Verify the Modelfile syntax
|
||||
- Check Ollama container logs: `make ollama-logs`
|
||||
|
||||
4. **Out of disk space:**
|
||||
- The model file is ~5.5GB, ensure sufficient free space
|
||||
- Consider using a smaller quantization (modify the script)
|
||||
|
||||
## Customization
|
||||
|
||||
You can modify the scripts to download different models or quantizations:
|
||||
|
||||
1. **Change model variant:** Edit `MODEL_FILE` in `pull-deepseek-model.sh`
|
||||
2. **Adjust parameters:** Modify the Modelfile template in the script
|
||||
3. **Use different model:** Change `MODEL_REPO` to any GGUF model on Hugging Face
|
||||
|
||||
## File Locations
|
||||
|
||||
- **Models:** `./ai_stack/ollama/models/`
|
||||
- **Modelfiles:** `./ai_stack/ollama/models/Modelfile.*`
|
||||
- **Ollama data:** Docker volume `ollama_data`
|
||||
Executable
+244
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# AI Stack Launcher - Choose your AI backend
|
||||
# This script helps users choose between Ollama and LlamaCPP for DeepSeek-R1
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Print colored output
|
||||
print_header() {
|
||||
echo -e "${CYAN}================================${NC}"
|
||||
echo -e "${CYAN} AI Stack DeepSeek-R1 ${NC}"
|
||||
echo -e "${CYAN}================================${NC}"
|
||||
echo
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${GREEN}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
print_warn() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_option() {
|
||||
echo -e "${BLUE}$1${NC} $2"
|
||||
}
|
||||
|
||||
# Check if GPU is available
|
||||
check_gpu() {
|
||||
if command -v nvidia-smi &> /dev/null && nvidia-smi &> /dev/null; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Display system information
|
||||
show_system_info() {
|
||||
echo -e "${PURPLE}System Information:${NC}"
|
||||
echo " CPU: $(nproc) cores"
|
||||
echo " RAM: $(free -h | awk '/^Mem:/ {print $2}')"
|
||||
|
||||
if check_gpu; then
|
||||
echo " GPU: $(nvidia-smi --query-gpu=name --format=csv,noheader | head -1)"
|
||||
echo " VRAM: $(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | head -1) MB"
|
||||
else
|
||||
echo " GPU: Not available"
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
# Show backend options
|
||||
show_options() {
|
||||
echo -e "${PURPLE}Available Backends:${NC}"
|
||||
echo
|
||||
|
||||
print_option "1)" "Ollama (Recommended for beginners)"
|
||||
echo " • Easy to use and manage"
|
||||
echo " • Web UI available"
|
||||
echo " • Good performance"
|
||||
echo " • Port: 11434"
|
||||
echo
|
||||
|
||||
print_option "2)" "LlamaCPP (CPU-only)"
|
||||
echo " • Direct GGUF model loading"
|
||||
echo " • Lightweight and fast"
|
||||
echo " • OpenAI-compatible API"
|
||||
echo " • Port: 8080"
|
||||
echo
|
||||
|
||||
if check_gpu; then
|
||||
print_option "3)" "LlamaCPP (GPU-accelerated)"
|
||||
echo " • CUDA-optimized performance"
|
||||
echo " • Best speed and efficiency"
|
||||
echo " • Requires NVIDIA GPU"
|
||||
echo " • Port: 8080"
|
||||
echo
|
||||
fi
|
||||
|
||||
print_option "0)" "Exit"
|
||||
echo
|
||||
}
|
||||
|
||||
# Launch Ollama
|
||||
launch_ollama() {
|
||||
print_info "Launching Ollama with DeepSeek-R1..."
|
||||
echo
|
||||
|
||||
print_info "Starting Ollama container..."
|
||||
make ollama-up
|
||||
|
||||
echo
|
||||
print_info "Downloading and loading DeepSeek-R1 model..."
|
||||
make pull-deepseek-model
|
||||
|
||||
echo
|
||||
print_info "✅ Ollama setup complete!"
|
||||
echo " 🌐 Access at: http://localhost:11434"
|
||||
echo " 📚 Model: deepseek-r1-qwen3-8b"
|
||||
echo " 💬 Chat: docker exec ollama-server ollama run deepseek-r1-qwen3-8b"
|
||||
}
|
||||
|
||||
# Launch LlamaCPP CPU
|
||||
launch_llamacpp_cpu() {
|
||||
print_info "Launching LlamaCPP (CPU) with DeepSeek-R1..."
|
||||
echo
|
||||
|
||||
print_info "Starting LlamaCPP container..."
|
||||
make llamacpp-up
|
||||
|
||||
echo
|
||||
print_info "Downloading DeepSeek-R1 model..."
|
||||
make pull-deepseek-llamacpp
|
||||
|
||||
echo
|
||||
print_info "✅ LlamaCPP (CPU) setup complete!"
|
||||
echo " 🌐 Web UI: http://localhost:8080"
|
||||
echo " 📡 API: http://localhost:8080/completion"
|
||||
echo " 📊 Health: http://localhost:8080/health"
|
||||
}
|
||||
|
||||
# Launch LlamaCPP GPU
|
||||
launch_llamacpp_gpu() {
|
||||
print_info "Launching LlamaCPP (GPU) with DeepSeek-R1..."
|
||||
echo
|
||||
|
||||
print_info "Downloading model if needed..."
|
||||
make pull-deepseek-llamacpp
|
||||
|
||||
echo
|
||||
print_info "Starting GPU-accelerated LlamaCPP..."
|
||||
make llamacpp-gpu
|
||||
|
||||
echo
|
||||
print_info "✅ LlamaCPP (GPU) setup complete!"
|
||||
echo " 🌐 Web UI: http://localhost:8080"
|
||||
echo " 📡 API: http://localhost:8080/completion"
|
||||
echo " 📊 Health: http://localhost:8080/health"
|
||||
echo " 🚀 GPU-accelerated for best performance!"
|
||||
}
|
||||
|
||||
# Test the selected backend
|
||||
test_backend() {
|
||||
local backend=$1
|
||||
|
||||
echo
|
||||
print_info "Would you like to test the ${backend} backend? (y/N)"
|
||||
read -r response
|
||||
|
||||
if [[ $response =~ ^[Yy]$ ]]; then
|
||||
echo
|
||||
print_info "Testing ${backend}..."
|
||||
|
||||
case $backend in
|
||||
"Ollama")
|
||||
if docker exec ollama-server ollama run deepseek-r1-qwen3-8b "Hello, introduce yourself briefly" 2>/dev/null; then
|
||||
print_info "✅ Ollama test successful!"
|
||||
else
|
||||
print_warn "⚠️ Test failed, but server should be running"
|
||||
fi
|
||||
;;
|
||||
"LlamaCPP")
|
||||
sleep 5 # Give server time to start
|
||||
if curl -s -X POST http://localhost:8080/completion \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt": "Hello!", "max_tokens": 20}' | grep -q "content"; then
|
||||
print_info "✅ LlamaCPP test successful!"
|
||||
else
|
||||
print_warn "⚠️ Test failed, server might still be starting up"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
# Show management commands
|
||||
show_management() {
|
||||
echo
|
||||
echo -e "${PURPLE}Management Commands:${NC}"
|
||||
echo " make help # Show all available commands"
|
||||
echo " make ollama-logs # View Ollama logs"
|
||||
echo " make llamacpp-logs # View LlamaCPP logs"
|
||||
echo " make ollama-down # Stop Ollama"
|
||||
echo " make llamacpp-down # Stop LlamaCPP"
|
||||
echo " docker ps # See running containers"
|
||||
echo
|
||||
}
|
||||
|
||||
# Main menu
|
||||
main_menu() {
|
||||
print_header
|
||||
show_system_info
|
||||
show_options
|
||||
|
||||
echo -n "Choose your AI backend (1-3, 0 to exit): "
|
||||
read -r choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
launch_ollama
|
||||
test_backend "Ollama"
|
||||
;;
|
||||
2)
|
||||
launch_llamacpp_cpu
|
||||
test_backend "LlamaCPP"
|
||||
;;
|
||||
3)
|
||||
if check_gpu; then
|
||||
launch_llamacpp_gpu
|
||||
test_backend "LlamaCPP GPU"
|
||||
else
|
||||
print_error "GPU not available! Please choose option 1 or 2."
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
0)
|
||||
print_info "Goodbye!"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
print_error "Invalid choice. Please try again."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
show_management
|
||||
print_info "🎉 Setup complete! Your AI backend is ready to use."
|
||||
}
|
||||
|
||||
# Run main menu
|
||||
main_menu
|
||||
@@ -0,0 +1,2 @@
|
||||
podman build -t furyhawk/llama.cpp:full-cuda --target full -f .devops/cuda.Dockerfile .
|
||||
podman run --name llamacpp-server --gpus all -v ~/models:/models localhost/furyhawk/llama.cpp:full-cuda -b -m /models/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf --port 8000 --host 0.0.0.0 -n 512 --n-gpu-layers 1
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Simple script to generate TypeScript client using the project's existing tools
|
||||
# This uses @hey-api/openapi-ts which is already configured in the project
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Get script directory and project root
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
OLLAMA_OPENAPI_PATH="$PROJECT_ROOT/ai_stack/ollama/openapi.json"
|
||||
OUTPUT_DIR="$PROJECT_ROOT/frontend/src/ollama-client"
|
||||
|
||||
echo -e "${GREEN}🚀 Generating Ollama TypeScript Client${NC}"
|
||||
|
||||
# Verify the OpenAPI file exists
|
||||
if [ ! -f "$OLLAMA_OPENAPI_PATH" ]; then
|
||||
echo -e "${RED}❌ Error: OpenAPI file not found at $OLLAMA_OPENAPI_PATH${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Change to frontend directory
|
||||
cd "$PROJECT_ROOT/frontend"
|
||||
|
||||
echo -e "${GREEN}📋 Using OpenAPI spec: $OLLAMA_OPENAPI_PATH${NC}"
|
||||
echo -e "${GREEN}📁 Output directory: $OUTPUT_DIR${NC}"
|
||||
|
||||
# Generate client using the project's existing openapi-ts setup
|
||||
echo -e "${GREEN}⚙️ Generating TypeScript client...${NC}"
|
||||
|
||||
npx @hey-api/openapi-ts \
|
||||
--input "$OLLAMA_OPENAPI_PATH" \
|
||||
--output "$OUTPUT_DIR" \
|
||||
--client "legacy/axios"
|
||||
|
||||
# Format the generated code with biome
|
||||
if [ -f "biome.json" ]; then
|
||||
echo -e "${GREEN}🎨 Formatting generated code...${NC}"
|
||||
npx biome format --write "$OUTPUT_DIR"
|
||||
fi
|
||||
|
||||
# Create a simple usage example
|
||||
EXAMPLE_FILE="$OUTPUT_DIR/example.ts"
|
||||
cat > "$EXAMPLE_FILE" << 'EOF'
|
||||
/**
|
||||
* Example usage of the generated Ollama FastAPI client
|
||||
*/
|
||||
import { client, OllamaService } from './index';
|
||||
|
||||
// Configure the base URL for your Ollama API
|
||||
client.setConfig({
|
||||
baseUrl: 'http://localhost:11434', // Default Ollama port
|
||||
});
|
||||
|
||||
// Example: List available models
|
||||
async function listModels() {
|
||||
try {
|
||||
const response = await OllamaService.listLocalModelsV1ModelsGet();
|
||||
console.log('Available models:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error listing models:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Get a specific model
|
||||
async function getModel(modelId: string) {
|
||||
try {
|
||||
const response = await OllamaService.getLocalModelV1ModelsModelIdGet({
|
||||
path: { model_id: modelId }
|
||||
});
|
||||
console.log('Model details:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error getting model:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Chat completion
|
||||
async function chatCompletion() {
|
||||
try {
|
||||
const response = await OllamaService.handleCompletionsV1ChatCompletionsPost({
|
||||
body: {
|
||||
model: 'llama2', // Replace with your model
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello, how are you?'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
console.log('Chat response:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error in chat completion:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Transcribe audio
|
||||
async function transcribeAudio(audioFile: File) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', audioFile);
|
||||
formData.append('model', 'whisper-1');
|
||||
|
||||
const response = await OllamaService.transcribeFileV1AudioTranscriptionsPost({
|
||||
body: formData
|
||||
});
|
||||
console.log('Transcription:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error transcribing audio:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Export the functions for use in your application
|
||||
export {
|
||||
listModels,
|
||||
getModel,
|
||||
chatCompletion,
|
||||
transcribeAudio
|
||||
};
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✅ Ollama TypeScript client generated successfully!${NC}"
|
||||
echo -e "${GREEN}📍 Client available at: $OUTPUT_DIR${NC}"
|
||||
echo -e "${GREEN}📝 Usage example: $EXAMPLE_FILE${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}💡 Quick start:${NC}"
|
||||
echo -e "${YELLOW}import { listModels, chatCompletion } from './ollama-client/example';${NC}"
|
||||
echo -e "${YELLOW}const models = await listModels();${NC}"
|
||||
echo -e "${YELLOW}const response = await chatCompletion();${NC}"
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
# Script to generate FastAPI client from Ollama OpenAPI specification
|
||||
# This generates a TypeScript client for the Ollama API endpoints
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}🚀 Generating Ollama FastAPI Client${NC}"
|
||||
|
||||
# Check if openapi-ts is available
|
||||
if ! command -v openapi-ts &> /dev/null; then
|
||||
echo -e "${YELLOW}⚠️ openapi-ts not found globally, using npx...${NC}"
|
||||
OPENAPI_CMD="npx @hey-api/openapi-ts"
|
||||
else
|
||||
OPENAPI_CMD="openapi-ts"
|
||||
fi
|
||||
|
||||
# Set paths
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
OLLAMA_OPENAPI_PATH="$PROJECT_ROOT/ai_stack/ollama/openapi.json"
|
||||
OUTPUT_DIR="$PROJECT_ROOT/frontend/src/ollama-client"
|
||||
|
||||
# Verify the OpenAPI file exists
|
||||
if [ ! -f "$OLLAMA_OPENAPI_PATH" ]; then
|
||||
echo -e "${RED}❌ Error: OpenAPI file not found at $OLLAMA_OPENAPI_PATH${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}📋 Using OpenAPI spec: $OLLAMA_OPENAPI_PATH${NC}"
|
||||
echo -e "${GREEN}📁 Output directory: $OUTPUT_DIR${NC}"
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Generate the client
|
||||
echo -e "${GREEN}⚙️ Generating TypeScript client...${NC}"
|
||||
|
||||
cd "$PROJECT_ROOT/frontend"
|
||||
|
||||
# Generate client using openapi-ts
|
||||
$OPENAPI_CMD \
|
||||
--input "$OLLAMA_OPENAPI_PATH" \
|
||||
--output "$OUTPUT_DIR" \
|
||||
--client "axios" \
|
||||
--useOptions
|
||||
|
||||
# Format the generated code
|
||||
if command -v biome &> /dev/null; then
|
||||
echo -e "${GREEN}🎨 Formatting generated code...${NC}"
|
||||
npx biome format --write "$OUTPUT_DIR"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Biome not available, skipping formatting${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Ollama FastAPI client generated successfully!${NC}"
|
||||
echo -e "${GREEN}📍 Client available at: $OUTPUT_DIR${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}💡 Usage example:${NC}"
|
||||
echo -e "${YELLOW}import { DefaultApi } from './ollama-client';${NC}"
|
||||
echo -e "${YELLOW}const api = new DefaultApi();${NC}"
|
||||
echo -e "${YELLOW}const models = await api.listLocalModelsV1ModelsGet();${NC}"
|
||||
Executable
+258
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to generate FastAPI clients from OpenAPI specifications.
|
||||
Supports multiple output formats including Python, TypeScript, and more.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class ClientGenerator:
|
||||
"""Generator for FastAPI clients from OpenAPI specifications."""
|
||||
|
||||
SUPPORTED_GENERATORS = {
|
||||
'python': 'python',
|
||||
'typescript': 'typescript-axios',
|
||||
'typescript-fetch': 'typescript-fetch',
|
||||
'javascript': 'javascript',
|
||||
'java': 'java',
|
||||
'go': 'go',
|
||||
'rust': 'rust',
|
||||
'php': 'php',
|
||||
'csharp': 'csharp'
|
||||
}
|
||||
|
||||
def __init__(self, openapi_path: str, output_dir: str, generator: str = 'python'):
|
||||
self.openapi_path = Path(openapi_path)
|
||||
self.output_dir = Path(output_dir)
|
||||
self.generator = generator
|
||||
|
||||
if not self.openapi_path.exists():
|
||||
raise FileNotFoundError(f"OpenAPI file not found: {openapi_path}")
|
||||
|
||||
if generator not in self.SUPPORTED_GENERATORS:
|
||||
raise ValueError(f"Unsupported generator: {generator}. "
|
||||
f"Supported: {list(self.SUPPORTED_GENERATORS.keys())}")
|
||||
|
||||
def _check_dependencies(self) -> bool:
|
||||
"""Check if required dependencies are available."""
|
||||
try:
|
||||
result = subprocess.run(['openapi-generator-cli', 'version'],
|
||||
capture_output=True, text=True)
|
||||
return result.returncode == 0
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def _install_openapi_generator(self) -> bool:
|
||||
"""Install OpenAPI Generator CLI via npm."""
|
||||
try:
|
||||
print("📦 Installing OpenAPI Generator CLI...")
|
||||
subprocess.run(['npm', 'install', '-g', '@openapitools/openapi-generator-cli'],
|
||||
check=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
print("❌ Failed to install OpenAPI Generator CLI")
|
||||
return False
|
||||
|
||||
def _validate_openapi_spec(self) -> Dict:
|
||||
"""Validate and load the OpenAPI specification."""
|
||||
try:
|
||||
with open(self.openapi_path, 'r') as f:
|
||||
spec = json.load(f)
|
||||
|
||||
# Basic validation
|
||||
if 'openapi' not in spec and 'swagger' not in spec:
|
||||
raise ValueError("Invalid OpenAPI specification")
|
||||
|
||||
return spec
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in OpenAPI file: {e}")
|
||||
|
||||
def generate_client(self, package_name: Optional[str] = None) -> bool:
|
||||
"""Generate the client code."""
|
||||
print(f"🚀 Generating {self.generator} client from {self.openapi_path}")
|
||||
|
||||
# Validate OpenAPI spec
|
||||
spec = self._validate_openapi_spec()
|
||||
print(f"✅ Valid OpenAPI {spec.get('openapi', spec.get('swagger', 'unknown'))} specification")
|
||||
|
||||
# Check dependencies
|
||||
if not self._check_dependencies():
|
||||
print("⚠️ OpenAPI Generator CLI not found, attempting to install...")
|
||||
if not self._install_openapi_generator():
|
||||
print("❌ Failed to install OpenAPI Generator CLI")
|
||||
print("💡 Please install manually: npm install -g @openapitools/openapi-generator-cli")
|
||||
return False
|
||||
|
||||
# Create output directory
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Prepare generator command
|
||||
generator_name = self.SUPPORTED_GENERATORS[self.generator]
|
||||
cmd = [
|
||||
'openapi-generator-cli', 'generate',
|
||||
'-i', str(self.openapi_path),
|
||||
'-g', generator_name,
|
||||
'-o', str(self.output_dir),
|
||||
]
|
||||
|
||||
# Add additional options based on generator type
|
||||
if self.generator == 'python':
|
||||
cmd.extend([
|
||||
'--additional-properties',
|
||||
f'packageName={package_name or "ollama_client"}',
|
||||
'--additional-properties',
|
||||
'projectName=ollama-fastapi-client',
|
||||
'--additional-properties',
|
||||
'packageVersion=1.0.0'
|
||||
])
|
||||
elif self.generator in ['typescript', 'typescript-fetch']:
|
||||
cmd.extend([
|
||||
'--additional-properties',
|
||||
'npmName=ollama-fastapi-client',
|
||||
'--additional-properties',
|
||||
'npmVersion=1.0.0'
|
||||
])
|
||||
|
||||
try:
|
||||
print(f"🔧 Running: {' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
print("✅ Client generated successfully!")
|
||||
|
||||
# Post-processing based on generator type
|
||||
self._post_process()
|
||||
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Error generating client: {e}")
|
||||
if e.stdout:
|
||||
print(f"stdout: {e.stdout}")
|
||||
if e.stderr:
|
||||
print(f"stderr: {e.stderr}")
|
||||
return False
|
||||
|
||||
def _post_process(self):
|
||||
"""Post-process the generated client."""
|
||||
if self.generator == 'python':
|
||||
self._post_process_python()
|
||||
elif self.generator in ['typescript', 'typescript-fetch']:
|
||||
self._post_process_typescript()
|
||||
|
||||
def _post_process_python(self):
|
||||
"""Post-process Python client."""
|
||||
# Create a simple usage example
|
||||
example_file = self.output_dir / 'example_usage.py'
|
||||
example_content = '''#!/usr/bin/env python3
|
||||
"""
|
||||
Example usage of the generated Ollama FastAPI client.
|
||||
"""
|
||||
|
||||
from ollama_client import ApiClient, Configuration
|
||||
from ollama_client.api.default_api import DefaultApi
|
||||
|
||||
# Configure the client
|
||||
configuration = Configuration(
|
||||
host="http://localhost:8000" # Adjust to your Ollama API endpoint
|
||||
)
|
||||
|
||||
# Create API client
|
||||
with ApiClient(configuration) as api_client:
|
||||
api = DefaultApi(api_client)
|
||||
|
||||
try:
|
||||
# List available models
|
||||
models = api.list_local_models_v1_models_get()
|
||||
print("Available models:")
|
||||
for model in models.data:
|
||||
print(f" - {model.id}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
'''
|
||||
with open(example_file, 'w') as f:
|
||||
f.write(example_content)
|
||||
print(f"📝 Created usage example: {example_file}")
|
||||
|
||||
def _post_process_typescript(self):
|
||||
"""Post-process TypeScript client."""
|
||||
# Create a simple usage example
|
||||
example_file = self.output_dir / 'example-usage.ts'
|
||||
example_content = '''/**
|
||||
* Example usage of the generated Ollama FastAPI client.
|
||||
*/
|
||||
|
||||
import { Configuration, DefaultApi } from './';
|
||||
|
||||
// Configure the client
|
||||
const configuration = new Configuration({
|
||||
basePath: 'http://localhost:8000', // Adjust to your Ollama API endpoint
|
||||
});
|
||||
|
||||
// Create API client
|
||||
const api = new DefaultApi(configuration);
|
||||
|
||||
async function listModels() {
|
||||
try {
|
||||
const response = await api.listLocalModelsV1ModelsGet();
|
||||
console.log('Available models:');
|
||||
response.data.data.forEach(model => {
|
||||
console.log(` - ${model.id}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
listModels();
|
||||
'''
|
||||
with open(example_file, 'w') as f:
|
||||
f.write(example_content)
|
||||
print(f"📝 Created usage example: {example_file}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate FastAPI clients from OpenAPI specifications')
|
||||
parser.add_argument('openapi_path', help='Path to the OpenAPI JSON/YAML file')
|
||||
parser.add_argument('-o', '--output', required=True, help='Output directory for generated client')
|
||||
parser.add_argument('-g', '--generator',
|
||||
choices=list(ClientGenerator.SUPPORTED_GENERATORS.keys()),
|
||||
default='python',
|
||||
help='Client generator type (default: python)')
|
||||
parser.add_argument('-p', '--package-name', help='Package name for generated client')
|
||||
parser.add_argument('--list-generators', action='store_true',
|
||||
help='List all supported generators')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list_generators:
|
||||
print("Supported generators:")
|
||||
for name, generator in ClientGenerator.SUPPORTED_GENERATORS.items():
|
||||
print(f" {name:20} -> {generator}")
|
||||
return
|
||||
|
||||
try:
|
||||
generator = ClientGenerator(args.openapi_path, args.output, args.generator)
|
||||
success = generator.generate_client(args.package_name)
|
||||
|
||||
if success:
|
||||
print(f"\n🎉 Client generated successfully!")
|
||||
print(f"📁 Output directory: {args.output}")
|
||||
print(f"🔧 Generator used: {args.generator}")
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+224
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Script to start LlamaCPP with GPU support for DeepSeek-R1 model
|
||||
# This script enables CUDA/GPU acceleration for better performance
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
MODEL_FILE="DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf"
|
||||
MODELS_DIR="./ai_stack/llamacpp/models"
|
||||
CONTAINER_NAME="llama-cpp-server-gpu"
|
||||
HOST_PORT="8080"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if NVIDIA GPU is available
|
||||
check_gpu() {
|
||||
log_info "Checking for NVIDIA GPU..."
|
||||
|
||||
if command -v nvidia-smi &> /dev/null; then
|
||||
if nvidia-smi &> /dev/null; then
|
||||
log_info "✅ NVIDIA GPU detected:"
|
||||
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits
|
||||
return 0
|
||||
else
|
||||
log_warn "nvidia-smi found but failed to query GPU"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_warn "nvidia-smi not found - no NVIDIA GPU support"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if model exists
|
||||
check_model() {
|
||||
local model_path="${MODELS_DIR}/${MODEL_FILE}"
|
||||
|
||||
if [ ! -f "${model_path}" ]; then
|
||||
log_error "Model file not found at ${model_path}"
|
||||
log_error "Please run: make pull-deepseek-llamacpp"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "✅ Model file found: ${model_path}"
|
||||
}
|
||||
|
||||
# Stop existing container if running
|
||||
stop_existing() {
|
||||
if docker ps -q -f name="${CONTAINER_NAME}" | grep -q .; then
|
||||
log_info "Stopping existing container: ${CONTAINER_NAME}"
|
||||
docker stop "${CONTAINER_NAME}"
|
||||
docker rm "${CONTAINER_NAME}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Start LlamaCPP with GPU support
|
||||
start_gpu_server() {
|
||||
log_info "Starting LlamaCPP server with GPU acceleration..."
|
||||
|
||||
# Create absolute path for models directory
|
||||
local abs_models_dir=$(realpath "${MODELS_DIR}")
|
||||
|
||||
docker run -d \
|
||||
--name "${CONTAINER_NAME}" \
|
||||
--gpus all \
|
||||
-p "${HOST_PORT}:8080" \
|
||||
-v "${abs_models_dir}:/models" \
|
||||
-e CUDA_VISIBLE_DEVICES=0 \
|
||||
llama-cpp-cuda:latest \
|
||||
/llama.cpp/llama-server \
|
||||
--model "/models/${MODEL_FILE}" \
|
||||
--host "0.0.0.0" \
|
||||
--port "8080" \
|
||||
--ctx-size "32768" \
|
||||
--n-predict "2048" \
|
||||
--threads "4" \
|
||||
--batch-size "512" \
|
||||
--n-gpu-layers "35" \
|
||||
--cont-batching \
|
||||
--parallel "4" \
|
||||
--flash-attn \
|
||||
--temperature "0.7" \
|
||||
--top-p "0.9" \
|
||||
--top-k "40" \
|
||||
--repeat-penalty "1.1"
|
||||
|
||||
log_info "LlamaCPP GPU server started on port ${HOST_PORT}"
|
||||
}
|
||||
|
||||
# Build GPU-enabled image if needed
|
||||
build_gpu_image() {
|
||||
log_info "Checking for GPU-enabled LlamaCPP image..."
|
||||
|
||||
if ! docker images | grep -q "llama-cpp-cuda"; then
|
||||
log_info "Building GPU-enabled LlamaCPP image..."
|
||||
|
||||
# Create temporary Dockerfile for GPU build
|
||||
cat > /tmp/Dockerfile.llamacpp-gpu << 'EOF'
|
||||
FROM nvidia/cuda:12.1-devel-ubuntu22.04
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
cmake \
|
||||
g++ \
|
||||
wget \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Clone llama.cpp
|
||||
RUN git clone https://github.com/ggerganov/llama.cpp.git
|
||||
|
||||
# Build llama.cpp with CUDA support
|
||||
WORKDIR /llama.cpp
|
||||
RUN mkdir build && cd build && \
|
||||
cmake .. -DLLAMA_SERVER=ON -DLLAMA_CUDA=ON && \
|
||||
make -j$(nproc)
|
||||
|
||||
# Create models directory
|
||||
RUN mkdir -p /models
|
||||
|
||||
# Expose the server port
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# Default command
|
||||
CMD ["/llama.cpp/llama-server", "--host", "0.0.0.0", "--port", "8080"]
|
||||
EOF
|
||||
|
||||
docker build -f /tmp/Dockerfile.llamacpp-gpu -t llama-cpp-cuda:latest .
|
||||
rm /tmp/Dockerfile.llamacpp-gpu
|
||||
|
||||
log_info "✅ GPU-enabled image built successfully"
|
||||
else
|
||||
log_info "✅ GPU-enabled image already exists"
|
||||
fi
|
||||
}
|
||||
|
||||
# Wait for server to be ready
|
||||
wait_for_server() {
|
||||
log_info "Waiting for server to be ready..."
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if curl -s -f "http://localhost:${HOST_PORT}/health" >/dev/null 2>&1; then
|
||||
log_info "✅ Server is ready!"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 2
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
log_error "❌ Server failed to start within expected time"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Show usage information
|
||||
show_usage() {
|
||||
log_info "GPU-accelerated LlamaCPP server is running!"
|
||||
echo
|
||||
echo "🌐 Web Interface: http://localhost:${HOST_PORT}"
|
||||
echo "📡 API Endpoint: http://localhost:${HOST_PORT}/completion"
|
||||
echo "🔍 Health Check: http://localhost:${HOST_PORT}/health"
|
||||
echo "📊 Metrics: http://localhost:${HOST_PORT}/metrics"
|
||||
echo
|
||||
echo "Container: ${CONTAINER_NAME}"
|
||||
echo "GPU Layers: 35 (adjust based on your GPU memory)"
|
||||
echo
|
||||
echo "Management commands:"
|
||||
echo " docker logs ${CONTAINER_NAME} # View logs"
|
||||
echo " docker stop ${CONTAINER_NAME} # Stop server"
|
||||
echo " docker stats ${CONTAINER_NAME} # View resource usage"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
log_info "Starting GPU-accelerated LlamaCPP setup for DeepSeek-R1..."
|
||||
|
||||
# Check prerequisites
|
||||
if ! check_gpu; then
|
||||
log_error "No GPU support detected. Use regular LlamaCPP setup instead."
|
||||
log_error "Run: make llamacpp-up"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_model
|
||||
stop_existing
|
||||
build_gpu_image
|
||||
start_gpu_server
|
||||
|
||||
if wait_for_server; then
|
||||
show_usage
|
||||
log_info "✅ GPU-accelerated DeepSeek-R1 setup completed successfully!"
|
||||
else
|
||||
log_error "❌ Server setup failed. Check logs: docker logs ${CONTAINER_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
Executable
+262
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Script to pull DeepSeek-R1 model from Hugging Face for LlamaCPP
|
||||
# This script downloads the model and prepares it for LlamaCPP server
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
MODEL_REPO="unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF"
|
||||
MODEL_FILE="DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf"
|
||||
MODELS_DIR="./ai_stack/llamacpp/models"
|
||||
LLAMACPP_HOST="http://localhost:8080"
|
||||
CONTAINER_NAME="llama-cpp-server"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Create models directory if it doesn't exist
|
||||
create_models_dir() {
|
||||
if [ ! -d "${MODELS_DIR}" ]; then
|
||||
log_info "Creating models directory: ${MODELS_DIR}"
|
||||
mkdir -p "${MODELS_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Install huggingface-hub using the best available package manager
|
||||
install_huggingface_hub() {
|
||||
# First try pipx if available (best for externally managed Python environments)
|
||||
if command -v pipx &> /dev/null; then
|
||||
log_info "Installing huggingface-hub using pipx..."
|
||||
pipx install huggingface-hub && return 0
|
||||
fi
|
||||
|
||||
# Try uv with virtual environment
|
||||
if command -v uv &> /dev/null; then
|
||||
log_info "Installing huggingface-hub using uv..."
|
||||
# Create a temporary virtual environment for the installation
|
||||
local temp_venv="/tmp/huggingface_venv_$$"
|
||||
uv venv "$temp_venv" &> /dev/null && {
|
||||
log_info "Created temporary virtual environment for huggingface-hub..."
|
||||
source "$temp_venv/bin/activate"
|
||||
uv pip install huggingface-hub && {
|
||||
# Add the venv bin to PATH for this session
|
||||
export PATH="$temp_venv/bin:$PATH"
|
||||
log_info "huggingface-hub installed in temporary virtual environment"
|
||||
return 0
|
||||
}
|
||||
deactivate 2>/dev/null || true
|
||||
}
|
||||
|
||||
# If virtual environment approach fails, try system install
|
||||
log_warn "Virtual environment approach failed, trying system install..."
|
||||
uv pip install --system huggingface-hub 2>/dev/null && return 0
|
||||
fi
|
||||
|
||||
# Fall back to other methods
|
||||
install_with_fallback
|
||||
}
|
||||
|
||||
# Fallback installation method
|
||||
install_with_fallback() {
|
||||
# Try to create a virtual environment with python3 -m venv
|
||||
if command -v python3 &> /dev/null; then
|
||||
log_info "Trying to install huggingface-hub using python3 with virtual environment..."
|
||||
local temp_venv="/tmp/huggingface_venv_$$"
|
||||
python3 -m venv "$temp_venv" 2>/dev/null && {
|
||||
source "$temp_venv/bin/activate"
|
||||
python3 -m pip install huggingface-hub && {
|
||||
# Add the venv bin to PATH for this session
|
||||
export PATH="$temp_venv/bin:$PATH"
|
||||
log_info "huggingface-hub installed in temporary virtual environment"
|
||||
return 0
|
||||
}
|
||||
deactivate 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Try --break-system-packages as last resort (for some systems)
|
||||
log_warn "Virtual environment failed, trying with --break-system-packages..."
|
||||
python3 -m pip install --break-system-packages huggingface-hub 2>/dev/null && return 0
|
||||
fi
|
||||
|
||||
# Final fallback attempts
|
||||
if command -v pip3 &> /dev/null; then
|
||||
log_info "Trying pip3 with --break-system-packages..."
|
||||
pip3 install --break-system-packages huggingface-hub 2>/dev/null && return 0
|
||||
fi
|
||||
|
||||
if command -v pip &> /dev/null; then
|
||||
log_info "Trying pip with --break-system-packages..."
|
||||
pip install --break-system-packages huggingface-hub 2>/dev/null && return 0
|
||||
fi
|
||||
|
||||
# If all else fails, provide helpful error message
|
||||
log_error "Failed to install huggingface-hub automatically."
|
||||
log_error "Your system has an externally managed Python environment."
|
||||
log_error ""
|
||||
log_error "Please install huggingface-hub manually using one of these methods:"
|
||||
log_error " 1. Using pipx (recommended):"
|
||||
log_error " sudo apt install pipx # or your package manager"
|
||||
log_error " pipx install huggingface-hub"
|
||||
log_error ""
|
||||
log_error " 2. Using your system package manager:"
|
||||
log_error " sudo apt install python3-huggingface-hub # if available"
|
||||
log_error ""
|
||||
log_error " 3. Using a virtual environment:"
|
||||
log_error " python3 -m venv ~/.local/huggingface_env"
|
||||
log_error " source ~/.local/huggingface_env/bin/activate"
|
||||
log_error " pip install huggingface-hub"
|
||||
log_error " # Then add ~/.local/huggingface_env/bin to your PATH"
|
||||
log_error ""
|
||||
log_error " 4. Force install (not recommended):"
|
||||
log_error " python3 -m pip install --break-system-packages huggingface-hub"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Download model from Hugging Face
|
||||
download_model() {
|
||||
log_info "Downloading model from Hugging Face..."
|
||||
|
||||
# Check if huggingface-hub is installed
|
||||
if ! command -v huggingface-cli &> /dev/null; then
|
||||
log_warn "huggingface-cli not found. Installing huggingface-hub..."
|
||||
install_huggingface_hub
|
||||
fi
|
||||
|
||||
# Download the specific GGUF file
|
||||
log_info "Downloading ${MODEL_FILE} from ${MODEL_REPO}..."
|
||||
huggingface-cli download \
|
||||
"${MODEL_REPO}" \
|
||||
"${MODEL_FILE}" \
|
||||
--local-dir "${MODELS_DIR}" \
|
||||
--local-dir-use-symlinks False
|
||||
|
||||
log_info "Model downloaded to ${MODELS_DIR}/${MODEL_FILE}"
|
||||
}
|
||||
|
||||
# Verify model file exists and get info
|
||||
verify_model() {
|
||||
local model_path="${MODELS_DIR}/${MODEL_FILE}"
|
||||
|
||||
if [ ! -f "${model_path}" ]; then
|
||||
log_error "Model file not found at ${model_path}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local file_size=$(du -h "${model_path}" | cut -f1)
|
||||
log_info "✅ Model file verified: ${model_path} (${file_size})"
|
||||
}
|
||||
|
||||
# Check if LlamaCPP container is running
|
||||
check_llamacpp() {
|
||||
log_info "Checking if LlamaCPP container is running..."
|
||||
if podman ps | grep -q "${CONTAINER_NAME}"; then
|
||||
log_info "LlamaCPP container is running"
|
||||
|
||||
# Test if server is responding
|
||||
if curl -f "${LLAMACPP_HOST}/health" >/dev/null 2>&1; then
|
||||
log_info "LlamaCPP server is responding"
|
||||
return 0
|
||||
else
|
||||
log_warn "LlamaCPP container is running but server is not responding"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_warn "LlamaCPP container is not running"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test the model with a simple prompt
|
||||
test_model() {
|
||||
log_info "Testing model with a simple prompt..."
|
||||
|
||||
local prompt="Hello, can you introduce yourself?"
|
||||
log_info "Sending test prompt: '${prompt}'"
|
||||
|
||||
# Test via API
|
||||
curl -s -X POST "${LLAMACPP_HOST}/completion" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"prompt\": \"${prompt}\",
|
||||
\"max_tokens\": 100,
|
||||
\"temperature\": 0.7,
|
||||
\"stop\": [\"\\n\\n\"]
|
||||
}" | jq -r '.content' 2>/dev/null || {
|
||||
log_warn "Model test failed or jq not available. Server might still be starting up."
|
||||
log_info "You can test manually at: ${LLAMACPP_HOST}"
|
||||
}
|
||||
}
|
||||
|
||||
# Display usage information
|
||||
show_usage() {
|
||||
log_info "Model setup completed! Here's how to use it:"
|
||||
echo
|
||||
echo "🌐 Web Interface: ${LLAMACPP_HOST}"
|
||||
echo "📡 API Endpoint: ${LLAMACPP_HOST}/completion"
|
||||
echo
|
||||
echo "Example API call:"
|
||||
echo "curl -X POST ${LLAMACPP_HOST}/completion \\"
|
||||
echo " -H 'Content-Type: application/json' \\"
|
||||
echo " -d '{\"prompt\": \"Hello!\", \"max_tokens\": 50}'"
|
||||
echo
|
||||
echo "Container management:"
|
||||
echo " Start: make llamacpp-up"
|
||||
echo " Stop: make llamacpp-down"
|
||||
echo " Logs: make llamacpp-logs"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
log_info "Starting DeepSeek-R1 model setup for LlamaCPP..."
|
||||
|
||||
create_models_dir
|
||||
|
||||
# Check if model file already exists
|
||||
if [ -f "${MODELS_DIR}/${MODEL_FILE}" ]; then
|
||||
log_warn "Model file already exists at ${MODELS_DIR}/${MODEL_FILE}"
|
||||
read -p "Do you want to re-download it? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
download_model
|
||||
else
|
||||
log_info "Skipping download, using existing model file"
|
||||
fi
|
||||
else
|
||||
download_model
|
||||
fi
|
||||
|
||||
verify_model
|
||||
|
||||
# Check if container is running and offer to test
|
||||
if check_llamacpp; then
|
||||
read -p "LlamaCPP server is running. Do you want to test the model? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
||||
test_model
|
||||
fi
|
||||
else
|
||||
log_info "LlamaCPP container is not running. Start it with: make llamacpp-up"
|
||||
fi
|
||||
|
||||
show_usage
|
||||
log_info "✅ DeepSeek-R1 model setup completed successfully!"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
Executable
+185
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Script to pull DeepSeek-R1 model from Hugging Face and load it into Ollama
|
||||
# This script should be run after Docker Compose is up and running
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
MODEL_REPO="unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF"
|
||||
MODEL_FILE="DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf"
|
||||
OLLAMA_HOST="http://localhost:11434"
|
||||
MODEL_NAME="deepseek-r1-qwen3-8b"
|
||||
MODELS_DIR="./ai_stack/ollama/models"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if Ollama is running
|
||||
check_ollama() {
|
||||
log_info "Checking if Ollama is running..."
|
||||
if ! curl -f "${OLLAMA_HOST}/api/version" >/dev/null 2>&1; then
|
||||
log_error "Ollama is not running or not accessible at ${OLLAMA_HOST}"
|
||||
log_error "Please make sure Docker Compose is running with: make up"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Ollama is running"
|
||||
}
|
||||
|
||||
# Create models directory if it doesn't exist
|
||||
create_models_dir() {
|
||||
if [ ! -d "${MODELS_DIR}" ]; then
|
||||
log_info "Creating models directory: ${MODELS_DIR}"
|
||||
mkdir -p "${MODELS_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Download model from Hugging Face using huggingface-hub
|
||||
download_model() {
|
||||
log_info "Downloading model from Hugging Face..."
|
||||
|
||||
# Check if huggingface-hub is installed
|
||||
if ! command -v huggingface-cli &> /dev/null; then
|
||||
log_warn "huggingface-cli not found. Installing huggingface-hub..."
|
||||
pip install huggingface-hub
|
||||
fi
|
||||
|
||||
# Download the specific GGUF file
|
||||
log_info "Downloading ${MODEL_FILE} from ${MODEL_REPO}..."
|
||||
huggingface-cli download \
|
||||
"${MODEL_REPO}" \
|
||||
"${MODEL_FILE}" \
|
||||
--local-dir "${MODELS_DIR}" \
|
||||
--local-dir-use-symlinks False
|
||||
|
||||
log_info "Model downloaded to ${MODELS_DIR}/${MODEL_FILE}"
|
||||
}
|
||||
|
||||
# Create Modelfile for Ollama
|
||||
create_modelfile() {
|
||||
local modelfile_path="${MODELS_DIR}/Modelfile.${MODEL_NAME}"
|
||||
|
||||
log_info "Creating Modelfile at ${modelfile_path}"
|
||||
|
||||
cat > "${modelfile_path}" << EOF
|
||||
FROM /models/${MODEL_FILE}
|
||||
|
||||
TEMPLATE """{{ if .System }}<|im_start|>system
|
||||
{{ .System }}<|im_end|>
|
||||
{{ end }}{{ if .Prompt }}<|im_start|>user
|
||||
{{ .Prompt }}<|im_end|>
|
||||
{{ end }}<|im_start|>assistant
|
||||
{{ .Response }}<|im_end|>
|
||||
"""
|
||||
|
||||
PARAMETER stop "<|im_start|>"
|
||||
PARAMETER stop "<|im_end|>"
|
||||
PARAMETER num_ctx 32768
|
||||
PARAMETER num_predict 8192
|
||||
PARAMETER temperature 0.7
|
||||
PARAMETER top_p 0.9
|
||||
PARAMETER top_k 40
|
||||
PARAMETER repeat_penalty 1.1
|
||||
|
||||
SYSTEM """You are DeepSeek-R1, a helpful and harmless AI assistant developed by DeepSeek. You should think step-by-step and provide detailed, accurate responses."""
|
||||
EOF
|
||||
|
||||
log_info "Modelfile created successfully"
|
||||
}
|
||||
|
||||
# Load model into Ollama
|
||||
load_model() {
|
||||
log_info "Loading model into Ollama..."
|
||||
|
||||
# The model files are already mounted in /models/ directory in the container
|
||||
# Just create the model directly using the mounted Modelfile
|
||||
podman exec ollama-server ollama create "${MODEL_NAME}" -f "/models/Modelfile.${MODEL_NAME}"
|
||||
|
||||
log_info "Model '${MODEL_NAME}' loaded successfully into Ollama"
|
||||
}
|
||||
|
||||
# Verify model is loaded
|
||||
verify_model() {
|
||||
log_info "Verifying model is available in Ollama..."
|
||||
|
||||
if podman exec ollama-server ollama list | grep -q "${MODEL_NAME}"; then
|
||||
log_info "✅ Model '${MODEL_NAME}' is available in Ollama"
|
||||
|
||||
# Show model details
|
||||
log_info "Model details:"
|
||||
podman exec ollama-server ollama list | grep "${MODEL_NAME}" || true
|
||||
|
||||
log_info "You can now use the model with: ollama run ${MODEL_NAME}"
|
||||
else
|
||||
log_error "❌ Model '${MODEL_NAME}' was not found in Ollama"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test the model with a simple prompt
|
||||
test_model() {
|
||||
log_info "Testing model with a simple prompt..."
|
||||
|
||||
echo "Testing model response:"
|
||||
echo "Prompt: 'Hello, can you introduce yourself?'"
|
||||
echo "Response:"
|
||||
|
||||
podman exec ollama-server ollama run "${MODEL_NAME}" "Hello, can you introduce yourself?" || {
|
||||
log_warn "Model test failed, but model is loaded. You can test it manually."
|
||||
}
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
log_info "Starting DeepSeek-R1 model setup for Ollama..."
|
||||
|
||||
check_ollama
|
||||
create_models_dir
|
||||
|
||||
# Check if model file already exists
|
||||
if [ -f "${MODELS_DIR}/${MODEL_FILE}" ]; then
|
||||
log_warn "Model file already exists at ${MODELS_DIR}/${MODEL_FILE}"
|
||||
read -p "Do you want to re-download it? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
download_model
|
||||
else
|
||||
log_info "Skipping download, using existing model file"
|
||||
fi
|
||||
else
|
||||
download_model
|
||||
fi
|
||||
|
||||
create_modelfile
|
||||
load_model
|
||||
verify_model
|
||||
|
||||
# Ask if user wants to test the model
|
||||
read -p "Do you want to test the model? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
||||
test_model
|
||||
fi
|
||||
|
||||
log_info "✅ DeepSeek-R1 model setup completed successfully!"
|
||||
log_info "You can now use the model with: podman exec ollama-server ollama run ${MODEL_NAME}"
|
||||
log_info "Or via API at: ${OLLAMA_HOST}/api/generate"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Simple script to pull DeepSeek-R1 model using Ollama pull command
|
||||
# This is an alternative if the model becomes available in Ollama's registry
|
||||
|
||||
set -e
|
||||
|
||||
MODEL_NAME="deepseek-r1:8b-q4"
|
||||
OLLAMA_HOST="http://localhost:11434"
|
||||
CONTAINER_NAME="ollama-server"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if Ollama container is running
|
||||
check_ollama() {
|
||||
log_info "Checking if Ollama container is running..."
|
||||
if ! docker ps | grep -q "${CONTAINER_NAME}"; then
|
||||
log_error "Ollama container '${CONTAINER_NAME}' is not running"
|
||||
log_error "Please start it with: make ollama-up"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Ollama container is running"
|
||||
}
|
||||
|
||||
# Pull model using ollama pull
|
||||
pull_model() {
|
||||
log_info "Attempting to pull model: ${MODEL_NAME}"
|
||||
|
||||
# Try to pull the model - this will work if the model is in Ollama's registry
|
||||
if docker exec "${CONTAINER_NAME}" ollama pull "${MODEL_NAME}"; then
|
||||
log_info "✅ Model '${MODEL_NAME}' pulled successfully"
|
||||
else
|
||||
log_error "❌ Failed to pull model '${MODEL_NAME}'"
|
||||
log_error "The model might not be available in Ollama's registry."
|
||||
log_error "Use the pull-deepseek-model.sh script to download from Hugging Face instead."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Verify model is available
|
||||
verify_model() {
|
||||
log_info "Verifying model is available..."
|
||||
|
||||
if docker exec "${CONTAINER_NAME}" ollama list | grep -q "${MODEL_NAME}"; then
|
||||
log_info "✅ Model '${MODEL_NAME}' is available"
|
||||
docker exec "${CONTAINER_NAME}" ollama list | grep "${MODEL_NAME}"
|
||||
else
|
||||
log_error "❌ Model verification failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
log_info "Starting simple model pull for ${MODEL_NAME}..."
|
||||
|
||||
check_ollama
|
||||
pull_model
|
||||
verify_model
|
||||
|
||||
log_info "✅ Model setup completed!"
|
||||
log_info "You can now use: docker exec ${CONTAINER_NAME} ollama run ${MODEL_NAME}"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$1" == "on" ]; then
|
||||
echo "Turning persistence mode ON and setting performance state..."
|
||||
sudo nvidia-smi -pm 1
|
||||
sudo nvidia-smi -lgc 1000,2100 # you can adjust clock speed here
|
||||
sudo modprobe -r nvidia_uvm && sudo modprobe nvidia_uvm
|
||||
echo "Persistence mode is ON."
|
||||
elif [ "$1" == "off" ]; then
|
||||
echo "Turning persistence mode OFF and resetting performance state..."
|
||||
sudo nvidia-smi -pm 0
|
||||
sudo nvidia-smi -rgc
|
||||
sudo modprobe -r nvidia_uvm && sudo modprobe nvidia_uvm
|
||||
echo "Persistence mode is OFF."
|
||||
else
|
||||
echo "Usage: $0 {on|off}"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user