feat: Add embedding API configuration and update related services for improved integration

This commit is contained in:
2026-06-13 22:28:41 +08:00
parent bb4b614711
commit eb1aecd9d8
7 changed files with 111 additions and 72 deletions
+4 -1
View File
@@ -2,9 +2,12 @@
DEBUG=false
# OpenAI-compatible API configuration
LLM_BASE_URL=http://localhost:11434/v1
LLM_BASE_URL=http://host.docker.internal:11434/v1
LLM_MODEL=llama
EMBEDDING_BASE_URL=http://host.docker.internal:11434/v1
EMBEDDING_API_KEY=
# API key for the LLM provider (leave empty for local servers like Ollama)
LLM_API_KEY=
+8
View File
@@ -59,6 +59,14 @@ class Settings(BaseSettings):
hf_token: str = ""
models_cache_dir: Path = Path.home() / ".cache" / "agent-alpha" / "models"
# ── Embeddings ─────────────────────────────────────────────────────────
embedding_base_url: str = ""
"""Base URL for the embedding API. If empty, uses the LLM base URL."""
embedding_api_key: str = ""
"""API key for the embedding API. If empty, uses the LLM API key."""
# ── AI Configuration ───────────────────────────────────────────────────
ai_model: str = "gpt-4o"
+47 -22
View File
@@ -53,14 +53,21 @@ router = APIRouter(prefix="/api/rag", tags=["rag"])
def _get_vector_store() -> VectorStore:
"""Return a shared VectorStore instance for route handlers."""
"""Return a shared VectorStore instance for route handlers.
Routes that don't need embeddings (collections list/info/create/delete)
can use this helper — it won't initialise the embedding service.
"""
rag_settings = settings.rag
embed_service = EmbeddingService(settings=rag_settings)
return VectorStore(settings=rag_settings, embedding_service=embed_service)
def _get_ingestion_service() -> IngestionService:
"""Return a shared IngestionService instance."""
"""Return a shared IngestionService instance.
Requires a working embedding endpoint for document processing.
"""
rag_settings = settings.rag
embed_service = EmbeddingService(settings=rag_settings)
vector_store = VectorStore(settings=rag_settings, embedding_service=embed_service)
@@ -142,29 +149,47 @@ async def delete_collection(name: str) -> RAGMessageResponse:
async def search_rag(
body: RAGSearchRequest,
) -> RAGSearchResponse:
"""Search across one or more collections."""
rag_settings = settings.rag
embed_service = EmbeddingService(settings=rag_settings)
store = VectorStore(settings=rag_settings, embedding_service=embed_service)
retrieval = RetrievalService(vector_store=store, settings=rag_settings)
"""Search across one or more collections.
Requires a working embedding endpoint. Configure ``EMBEDDING_BASE_URL``
in your environment (defaults to ``LLM_BASE_URL``). The server must
support the OpenAI-compatible ``/v1/embeddings`` endpoint.
"""
try:
rag_settings = settings.rag
embed_service = EmbeddingService(settings=rag_settings)
store = VectorStore(settings=rag_settings, embedding_service=embed_service)
retrieval = RetrievalService(vector_store=store, settings=rag_settings)
except Exception as exc:
raise HTTPException(
status_code=502,
detail=f"Failed to initialise embedding service: {exc}. "
f"Set EMBEDDING_BASE_URL to a server that supports /v1/embeddings.",
) from exc
collection_names = body.collection_names or [body.collection_name]
if len(collection_names) == 1:
results = await retrieval.retrieve(
query=body.query,
collection_name=collection_names[0],
limit=body.limit,
min_score=body.min_score,
filter=body.filter or "",
)
else:
results = await retrieval.retrieve_multi(
query=body.query,
collection_names=collection_names,
limit=body.limit,
min_score=body.min_score,
)
try:
if len(collection_names) == 1:
results = await retrieval.retrieve(
query=body.query,
collection_name=collection_names[0],
limit=body.limit,
min_score=body.min_score,
filter=body.filter or "",
)
else:
results = await retrieval.retrieve_multi(
query=body.query,
collection_names=collection_names,
limit=body.limit,
min_score=body.min_score,
)
except Exception as exc:
raise HTTPException(
status_code=502,
detail=f"Search failed — check EMBEDDING_BASE_URL: {exc}",
) from exc
return RAGSearchResponse(
results=[
+29 -9
View File
@@ -48,19 +48,34 @@ class OpenAIEmbeddingProvider(BaseEmbeddingProvider):
Uses OpenAI's embedding models to generate text embeddings.
"""
def __init__(self, model: str, base_url: str | None = None) -> None:
def __init__(self, model: str, api_key: str | None = None, base_url: str | None = None) -> None:
"""Initialize the OpenAI embedding provider.
Args:
model: The OpenAI embedding model name (e.g., 'text-embedding-3-small').
base_url: Explicit base URL for the OpenAI API. If not provided,
defaults to the standard OpenAI API, explicitly ignoring any
OPENAI_BASE_URL env var (which may point to a chat-only local
server that doesn't support embeddings).
api_key: API key for the embedding endpoint. If not provided, falls
back to ``EMBEDDING_API_KEY``, then ``LLM_API_KEY`` from settings,
or ``"no-key-required"``.
base_url: Explicit base URL for the embedding API. If not provided,
falls back to ``EMBEDDING_BASE_URL``, then ``LLM_BASE_URL`` from
settings. As a last resort defaults to the standard OpenAI API.
"""
from backend.core.config import settings as app_settings
self.model = model
# Explicitly pass base_url to avoid picking up OPENAI_BASE_URL from env
self.client = OpenAI(base_url=base_url or "https://api.openai.com/v1")
resolved_key = (
api_key
or app_settings.embedding_api_key
or app_settings.llm_api_key
or "no-key-required"
)
resolved_url = (
base_url
or app_settings.embedding_base_url
or app_settings.llm_base_url
or "https://api.openai.com/v1"
)
self.client = OpenAI(base_url=resolved_url, api_key=resolved_key)
def embed_queries(self, texts: list[str]) -> list[list[float]]:
"""Embed a list of query texts using OpenAI.
@@ -104,15 +119,20 @@ class EmbeddingService:
Supports multiple backends: OpenAI, Voyage AI, and Sentence Transformers.
"""
def __init__(self, settings: RAGSettings):
def __init__(self, settings: RAGSettings, api_key: str | None = None):
"""Initialize the embedding service.
Args:
settings: RAG configuration settings.
api_key: Optional API key for the embedding endpoint. If not provided,
falls back to the application LLM API key.
"""
if api_key is None:
from backend.core.config import settings as app_settings
api_key = app_settings.llm_api_key
config = settings.embeddings_config
self.expected_dim = config.dim
self.provider = OpenAIEmbeddingProvider(model=config.model)
self.provider = OpenAIEmbeddingProvider(model=config.model, api_key=api_key)
def embed_query(self, query: str) -> list[float]:
"""Embed a single query text.
+3 -4
View File
@@ -59,9 +59,6 @@ class BaseReranker(ABC):
pass
from sentence_transformers import CrossEncoder
class CrossEncoderReranker(BaseReranker):
"""Cross Encoder reranker using local Sentence Transformers model.
@@ -87,9 +84,11 @@ class CrossEncoderReranker(BaseReranker):
self._model = None
@property
def model(self) -> CrossEncoder:
def model(self) -> object:
"""Lazy load the cross-encoder model."""
if self._model is None:
from sentence_transformers import CrossEncoder
from backend.core.config import settings as app_settings
cache_path = self.cache_dir or str(app_settings.models_cache_dir)
+19 -32
View File
@@ -33,6 +33,10 @@ services:
- MILVUS_TOKEN=${MILVUS_TOKEN:-}
- MEDIA_DIR=${MEDIA_DIR:-/app/media}
- MAX_UPLOAD_SIZE_MB=${MAX_UPLOAD_SIZE_MB:-50}
# Embeddings — defaults to LLM_BASE_URL; set to a server that supports
# the OpenAI-compatible embeddings endpoint (e.g. Ollama, OpenAI).
- EMBEDDING_BASE_URL=${EMBEDDING_BASE_URL:-}
- EMBEDDING_API_KEY=${EMBEDDING_API_KEY:-}
restart: unless-stopped
depends_on:
@@ -41,7 +45,7 @@ services:
valkey:
condition: service_healthy
milvus:
condition: service_healthy
condition: service_started
# Uncomment to use a local .env file instead of env vars:
# env_file:
# - .env
@@ -64,7 +68,7 @@ services:
# ── PostgreSQL ──────────────────────────────────────────────────
postgres:
image: postgres:16-alpine
image: docker.io/postgres:16-alpine
container_name: agent-alpha-postgres
ports:
- "5432:5432"
@@ -83,7 +87,7 @@ services:
# ── Valkey (Redis-compatible) ──────────────────────────────────
valkey:
image: valkey/valkey:8-alpine
image: docker.io/valkey/valkey:8-alpine
container_name: agent-alpha-valkey
ports:
- "6379:6379"
@@ -96,71 +100,54 @@ services:
retries: 10
restart: unless-stopped
# ── Milvus Vector Database ──────────────────────────────────────
# Requires etcd (coordinator) and MinIO (storage).
# ── Milvus Vector Database (standalone — bundles etcd + MinIO) ──
milvus:
image: milvusdb/milvus:v2.5-latest
image: docker.io/milvusdb/milvus:v2.4.1
container_name: agent-alpha-milvus
ports:
- "19530:19530"
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
volumes:
- milvus_data:/var/lib/milvus
command: milvus run standalone
depends_on:
etcd:
condition: service_healthy
minio:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/health"]
interval: 10s
timeout: 5s
retries: 10
- etcd
- minio
restart: unless-stopped
etcd:
image: quay.io/coreos/etcd:v3.5.17
image: quay.io/coreos/etcd:v3.5.4
container_name: agent-alpha-etcd
environment:
ETCD_AUTO_COMPACTION_MODE: revision
ETCD_AUTO_COMPACTION_RETENTION: "1000"
ETCD_QUOTA_BACKEND_BYTES: "4294967296"
ETCD_SNAPSHOT_COUNT: "50000"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379"
volumes:
- etcd_data:/etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
minio:
image: minio/minio:RELEASE.2024-11-17T22-20-09Z
image: docker.io/minio/minio:latest
container_name: agent-alpha-minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio_data:/data
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
# ── Ollama (optional — uncomment to run the LLM locally) ────────
# ollama:
# image: ollama/ollama:latest
# image: docker.io/ollama/ollama:latest
# container_name: ollama
# ports:
# - "11434:11434"
+1 -4
View File
@@ -23,13 +23,10 @@ dependencies = [
"pymupdf>=1.27.2.3",
"python-docx>=1.2.0",
"langchain-text-splitters>=1.1.2",
"sentence-transformers>=5.5.1",
# Vector Database
"pymilvus>=2.5.0",
# Async Task Queue (lightweight, redis-based)
# (using redis directly instead of arq to avoid dep conflicts)
# Embeddings
"openai>=1.0.0",
# Hybrid Search
# Hybrid Search (optional: reranker requires sentence-transformers)
"rank-bm25>=0.2.2",
]