mirror of
https://github.com/furyhawk/agent_alpha.git
synced 2026-07-21 02:25:34 +00:00
feat: Add embedding API configuration and update related services for improved integration
This commit is contained in:
+4
-1
@@ -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=
|
||||
|
||||
|
||||
@@ -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
@@ -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=[
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user