File size: 3,577 Bytes
49f0cfb
 
 
 
 
7d37f11
0b42403
 
 
49f0cfb
0b42403
 
 
7d37f11
dc1b199
49f0cfb
 
 
 
dc1b199
 
49f0cfb
 
 
7d37f11
dc1b199
0b42403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49f0cfb
 
7d37f11
49f0cfb
 
7d37f11
0b42403
dc1b199
0b42403
 
49f0cfb
dc1b199
0b42403
 
dc1b199
49f0cfb
dc1b199
7d37f11
 
 
 
732b14f
0b42403
 
 
dc1b199
0b42403
 
 
49f0cfb
0b42403
 
 
dc1b199
7d37f11
0b42403
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
"""Factory that returns a LangChain-compatible Embeddings object.

This replaces the custom EmbeddingClient interface with the standard
LangChain ``langchain_core.embeddings.Embeddings`` protocol, enabling
seamless integration with LangChain vector stores, retrievers, and chains.

Default priority (with ``prefer_local_embeddings=True``):
  1. HuggingFace — ``langchain_huggingface.HuggingFaceEmbeddings`` (free, on-device).
  2. OpenAI fallback only if HuggingFace import fails AND an API key is present.
  3. Fake — deterministic ``FakeEmbeddings`` for unit tests only.

Setting ``prefer_local_embeddings=False`` restores the legacy behaviour
(OpenAI first if a key is configured, HuggingFace as fallback).
"""

import logging

from langchain_core.embeddings import Embeddings

from app.config import settings

logger = logging.getLogger(__name__)

_instance: Embeddings | None = None


def _build_huggingface() -> Embeddings | None:
    """Try to construct a HuggingFace embeddings client, returning ``None`` on failure."""
    try:
        from langchain_huggingface import HuggingFaceEmbeddings
    except ImportError:
        logger.warning(
            "langchain-huggingface not installed — cannot use local embeddings. "
            "Run: pip install langchain-huggingface sentence-transformers"
        )
        return None

    logger.info(
        "Using LangChain HuggingFaceEmbeddings (model=%s, free/local)",
        settings.local_embedding_model,
    )
    return HuggingFaceEmbeddings(
        model_name=settings.local_embedding_model,
        model_kwargs={"device": "cpu"},
        encode_kwargs={"normalize_embeddings": True},
    )


def _build_openai() -> Embeddings | None:
    """Try to construct an OpenAI embeddings client, returning ``None`` on failure."""
    if not settings.openai_api_key:
        return None
    try:
        from langchain_openai import OpenAIEmbeddings
    except ImportError:
        return None

    logger.info("Using LangChain OpenAIEmbeddings (model=%s)", settings.embedding_model)
    return OpenAIEmbeddings(
        model=settings.embedding_model,
        openai_api_key=settings.openai_api_key,
    )


def get_embedding_client() -> Embeddings:
    """Return a singleton LangChain :class:`Embeddings` instance.

    The client is constructed once and reused — critical for the
    HuggingFace backend which loads a ~90 MB model at construction time.

    Default order (``prefer_local_embeddings=True``):

    1. :class:`langchain_huggingface.HuggingFaceEmbeddings` (free, local).
    2. :class:`langchain_openai.OpenAIEmbeddings` (only if HF unavailable).
    3. :class:`langchain_community.embeddings.FakeEmbeddings` as a test fallback.

    Set ``prefer_local_embeddings=False`` to invert the first two.

    Returns:
        A configured LangChain :class:`Embeddings` object.
    """
    global _instance
    if _instance is not None:
        return _instance

    if getattr(settings, "prefer_local_embeddings", False):
        _instance = _build_huggingface() or _build_openai()
    else:
        _instance = _build_openai() or _build_huggingface()

    if _instance is None:
        logger.warning(
            "No embedding backend available — falling back to FakeEmbeddings (TEST ONLY)."
        )
        from langchain_community.embeddings import FakeEmbeddings

        _instance = FakeEmbeddings(size=384)

    return _instance


def reset_embedding_client() -> None:
    """Reset the cached singleton — useful for tests or hot config reloads."""
    global _instance
    _instance = None