Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
DEFAULT_MODEL="qwen3:0.6b-q4_K_M"

# Port the API listens on *inside* the container. Clients running inside the
# container (e.g. chat.sh) should target this port.
APP_PORT="8000"

# Host port to publish the REST API on. Mapped to APP_PORT in docker-compose.
# Overridable to avoid conflicts with other services on the host.
API_PORT="18000"

OLLAMA_URL="http://ollama:11434"
DB_PATH="/data/enmemoryalpha_db"
TEXT_COLLECTION_NAME="memoryalpha_text"
26 changes: 17 additions & 9 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,25 @@ jobs:
run: |
# Start services in background
docker compose up -d


# Host publishes on API_PORT (see .env / docker-compose.yml)
set -a; [ -f .env ] && . ./.env; set +a
PORT="${API_PORT:-8000}"

# Wait for services to be ready (max 5 minutes)
timeout 300 bash -c 'until curl -f http://localhost:8000/memoryalpha/health > /dev/null 2>&1; do sleep 5; echo "Waiting for API..."; done'
timeout 300 bash -c "until curl -f http://localhost:${PORT}/memoryalpha/health > /dev/null 2>&1; do sleep 5; echo 'Waiting for API...'; done"

# Verify health endpoint
curl -f http://localhost:8000/memoryalpha/health
curl -f "http://localhost:${PORT}/memoryalpha/health"

echo "✅ Health check passed"

- name: Test ask endpoint
run: |
set -a; [ -f .env ] && . ./.env; set +a
PORT="${API_PORT:-8000}"
# Test the ask endpoint with a simple query
response=$(curl -X POST "http://localhost:8000/memoryalpha/rag/ask" -H "Content-Type: application/json" -d '{
response=$(curl -X POST "http://localhost:${PORT}/memoryalpha/rag/ask" -H "Content-Type: application/json" -d '{
"question": "What is the color of Vulcan blood?"
}')
# Check if response contains expected content
Expand All @@ -52,11 +58,13 @@ jobs:
echo "Response: $response"
exit 1
fi

- name: Generate OpenAPI spec
run: |
set -a; [ -f .env ] && . ./.env; set +a
PORT="${API_PORT:-8000}"
# Download OpenAPI spec
curl -s http://localhost:8000/openapi.json -o memoryalpha-rag-api-spec.json
curl -s "http://localhost:${PORT}/openapi.json" -o memoryalpha-rag-api-spec.json
cat memoryalpha-rag-api-spec.json

- name: Cleanup
Expand Down
24 changes: 16 additions & 8 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,25 @@ jobs:
run: |
# Start services in background
docker compose up -d


# Host publishes on API_PORT (see .env / docker-compose.yml)
set -a; [ -f .env ] && . ./.env; set +a
PORT="${API_PORT:-8000}"

# Wait for services to be ready (max 5 minutes)
timeout 300 bash -c 'until curl -f http://localhost:8000/memoryalpha/health > /dev/null 2>&1; do sleep 5; echo "Waiting for API..."; done'
timeout 300 bash -c "until curl -f http://localhost:${PORT}/memoryalpha/health > /dev/null 2>&1; do sleep 5; echo 'Waiting for API...'; done"

# Verify health endpoint
curl -f http://localhost:8000/memoryalpha/health
curl -f "http://localhost:${PORT}/memoryalpha/health"

echo "✅ Health check passed"

- name: Test ask endpoint
run: |
set -a; [ -f .env ] && . ./.env; set +a
PORT="${API_PORT:-8000}"
# Test the ask endpoint with a simple query
response=$(curl -X POST "http://localhost:8000/memoryalpha/rag/ask" -H "Content-Type: application/json" -d '{
response=$(curl -X POST "http://localhost:${PORT}/memoryalpha/rag/ask" -H "Content-Type: application/json" -d '{
"question": "What was the name of human who discovered warp drive?"
}')
# Check if response contains expected content
Expand All @@ -54,8 +60,10 @@ jobs:

- name: Generate OpenAPI spec
run: |
set -a; [ -f .env ] && . ./.env; set +a
PORT="${API_PORT:-8000}"
# Download OpenAPI spec
curl -s http://localhost:8000/openapi.json -o memoryalpha-rag-api-spec.json
curl -s "http://localhost:${PORT}/openapi.json" -o memoryalpha-rag-api-spec.json
cat memoryalpha-rag-api-spec.json

- name: Cleanup
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,18 @@ jobs:
- name: Generate OpenAPI spec
run: |
docker compose up -d lcars
# Host publishes on API_PORT (see .env / docker-compose.yml)
set -a; [ -f .env ] && . ./.env; set +a
PORT="${API_PORT:-8000}"
# Wait for API to be ready
timeout 120 bash -c 'until curl -f http://localhost:8000/memoryalpha/health > /dev/null 2>&1; do sleep 5; echo "Waiting for API..."; done'
timeout 120 bash -c "until curl -f http://localhost:${PORT}/memoryalpha/health > /dev/null 2>&1; do sleep 5; echo 'Waiting for API...'; done"
# Download OpenAPI spec
curl -s http://localhost:8000/openapi.json -o memoryalpha-rag-api-spec.json
curl -s "http://localhost:${PORT}/openapi.json" -o memoryalpha-rag-api-spec.json
docker compose down -v

- name: Upload OpenAPI spec to release
uses: softprops/action-gh-release@v2
with:
files: memoryalpha-rag-api-spec.json
env:
GITHUB_TOKEN: ${{ secrets.MEMORYALPHA_RAG_API_MODIFY_RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.MEMORYALPHA_RAG_API_MODIFY_RELEASE_TOKEN }}
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ RUN pip install --no-cache-dir -r /tmp/pip-tmp/requirements.txt \

WORKDIR /data

ARG MEMORYALPHA_DB_RELEASE=v0.5.0
ARG MEMORYALPHA_DB_RELEASE=v0.5.2
RUN wget https://github.com/aniongithub/memoryalpha-vectordb/releases/download/${MEMORYALPHA_DB_RELEASE}/enmemoryalpha_db.tar.gz &&\
tar -xzf enmemoryalpha_db.tar.gz &&\
rm enmemoryalpha_db.tar.gz &&\
Expand Down
34 changes: 20 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ This project provides a REST API that enables natural language queries over the
The system implements:

- **Retrieval-Augmented Generation (RAG)** for context-aware responses
- **Single-pass retrieval** (retrieve → cross-encoder rerank → stuff → generate) that works well with small local models — no tool-calling required
- **Streaming responses** for real-time interaction
- **Cross-encoder reranking** for improved document relevance
- **Conversation history** for multi-turn dialogues
- **Thinking modes** (disabled/quiet/verbose) for different interaction styles

## Quick Start

Expand Down Expand Up @@ -54,19 +54,25 @@ The system implements:
### API Endpoints

- **Health Check:** `GET /memoryalpha/health`
- **Streaming Chat:** `GET /memoryalpha/rag/stream`
- **Ask (full response):** `GET` or `POST /memoryalpha/rag/ask` — returns the complete answer as JSON
- **Streaming Chat:** `GET` or `POST /memoryalpha/rag/stream` — streams the answer as `text/plain` chunks

> The host port defaults to `8000` but is configurable via `API_PORT` in `.env`
> (the container always listens on `8000`). Adjust the URLs below to match your `API_PORT`.

#### Example API Usage

* Streaming API
* Synchronous API (full JSON response)
```bash
curl "http://localhost:8000/memoryalpha/rag/ask?question=What%20is%20a%20Transporter?&max_tokens=512&top_k=10&top_p=0.8&temperature=0.3"
```
* Streaming API (plain-text chunks)
```bash
curl -N -H "Accept: text/event-stream" \
"http://localhost:8000/memoryalpha/rag/stream?question=What%20is%20the%20Enterprise?&thinkingmode=DISABLED&max_tokens=512&top_k=5"
curl -N "http://localhost:8000/memoryalpha/rag/stream?question=What%20is%20the%20Enterprise?&max_tokens=512&top_k=10"
```
* Synchronous API
* Legacy tool-calling path (opt-in; unreliable with very small models)
```bash
curl -N -H "Accept: text/event-stream" \
"http://localhost:8000/memoryalpha/rag/ask?question=What%20is%20a%20Transporter?&thinkingmode=VERBOSE&max_tokens=512&top_k=5&top_p=0.8&temperature=0.3"
curl "http://localhost:8000/memoryalpha/rag/ask?question=What%20is%20a%20Transporter?&use_tools=true"
```

## Configuration
Expand All @@ -78,26 +84,26 @@ The system uses the following environment variables (set in `.env`):
```env
# Ollama Configuration
OLLAMA_URL=http://ollama:11434
DEFAULT_MODEL=qwen3:0.5b
DEFAULT_MODEL=qwen3:0.6b-q4_K_M

# Database Configuration
# Database Configuration
DB_PATH=/data/enmemoryalpha_db
COLLECTION_NAME=memoryalpha
TEXT_COLLECTION_NAME=memoryalpha_text

# API Configuration
THINKING_MODE=DISABLED
API_PORT=8000
MAX_TOKENS=2048
TOP_K=10
```

### Query Parameters

- `question`: Your Star Trek question
- `thinkingmode`: `DISABLED`, `QUIET`, or `VERBOSE`
- `max_tokens`: Maximum response length (default: 2048)
- `top_k`: Number of documents to retrieve (default: 10)
- `top_p`: Sampling parameter (default: 0.8)
- `temperature`: Response creativity (default: 0.3)
- `use_tools`: Use the legacy tool-calling agent loop instead of single-pass RAG (default: `false`; `/ask` only)

## Development

Expand Down Expand Up @@ -137,7 +143,7 @@ If you prefer local development without containers:
3. **Set up Ollama locally:**
```bash
# Install Ollama (see https://ollama.ai)
ollama pull qwen3:0.5b
ollama pull qwen3:0.6b-q4_K_M
```
4. **Download the MemoryAlpha database:**
```bash
Expand Down
44 changes: 39 additions & 5 deletions api/memoryalpha/ask.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi import APIRouter, Query, Body
from fastapi.responses import JSONResponse
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel
from typing import Optional

Expand All @@ -16,6 +16,7 @@ class AskRequest(BaseModel):
top_k: Optional[int] = 10
top_p: Optional[float] = 0.8
temperature: Optional[float] = 0.3
use_tools: Optional[bool] = False

@router.post("/memoryalpha/rag/ask")
def ask_endpoint_post(request: AskRequest):
Expand All @@ -29,7 +30,8 @@ def ask_endpoint_post(request: AskRequest):
max_tokens=request.max_tokens,
top_k=request.top_k,
top_p=request.top_p,
temperature=request.temperature
temperature=request.temperature,
use_tools=request.use_tools,
)
return JSONResponse(content=result)
except Exception as e:
Expand All @@ -41,20 +43,52 @@ def ask_endpoint(
max_tokens: int = Query(2048, description="Maximum tokens to generate"),
top_k: int = Query(10, description="Number of documents to retrieve"),
top_p: float = Query(0.8, description="Sampling parameter"),
temperature: float = Query(0.3, description="Randomness/creativity of output")
temperature: float = Query(0.3, description="Randomness/creativity of output"),
use_tools: bool = Query(False, description="Use the legacy tool-calling agent loop instead of single-pass RAG"),
):
"""
Query the RAG pipeline and return the full response.
Now uses advanced tool-enabled RAG by default for better results.
Uses single-pass (no tool-calling) RAG by default; set use_tools=true for the legacy loop.
"""
try:
result = rag_instance.ask(
question,
max_tokens=max_tokens,
top_k=top_k,
top_p=top_p,
temperature=temperature
temperature=temperature,
use_tools=use_tools,
)
return JSONResponse(content=result)
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})

@router.post("/memoryalpha/rag/stream")
def stream_endpoint_post(request: AskRequest):
"""Stream the answer as it is generated (single-pass RAG). Returns text/plain chunks."""
generator = rag_instance.ask_stream(
request.question,
max_tokens=request.max_tokens,
top_k=request.top_k,
top_p=request.top_p,
temperature=request.temperature,
)
return StreamingResponse(generator, media_type="text/plain; charset=utf-8")

@router.get("/memoryalpha/rag/stream")
def stream_endpoint(
question: str = Query(..., description="The user question"),
max_tokens: int = Query(2048, description="Maximum tokens to generate"),
top_k: int = Query(10, description="Number of documents to retrieve"),
top_p: float = Query(0.8, description="Sampling parameter"),
temperature: float = Query(0.3, description="Randomness/creativity of output"),
):
"""Stream the answer as it is generated (single-pass RAG). Returns text/plain chunks."""
generator = rag_instance.ask_stream(
question,
max_tokens=max_tokens,
top_k=top_k,
top_p=top_p,
temperature=temperature,
)
return StreamingResponse(generator, media_type="text/plain; charset=utf-8")
Loading
Loading