FGAO-API / src /fgao /integrations /huggingface.py
Angshul's picture
Upload 27 files
0703e2b verified
Raw History Blame Contribute Delete
6.4 kB
"""Hugging Face components (install ``fgao[huggingface]``; ``transformers`` for local models).
* ``HFInferenceLLM`` chat model behind the Hugging Face Inference API / Providers
* ``TransformersLLM`` local ``transformers`` text-generation model
* ``hf_embedder`` embedding function for ``DenseRetriever`` (Inference API)
* ``st_embedder`` embedding function from a local ``sentence-transformers`` model
* ``load_hf_corpus`` documents from a Hugging Face dataset
"""
from __future__ import annotations
import os
from typing import Any, Callable, List, Optional
from ..rag.documents import Document
from ..rag.prompts import SYSTEM_PROMPT
class HFInferenceLLM:
"""``huggingface_hub`` chat completion (Inference API / Providers / a TGI endpoint) as an FGAO LLM.
Sync calls use ``InferenceClient``, async calls ``AsyncInferenceClient``.
The token defaults to ``HF_TOKEN``. ``temperature=None`` leaves the
provider default; set ``temperature=0`` where the provider accepts it for
deterministic calls. ``base_url`` targets a dedicated endpoint.
"""
def __init__(self, model: str, *, token: Optional[str] = None, provider: Optional[str] = None,
base_url: Optional[str] = None, max_tokens: int = 128, temperature: Optional[float] = None,
system: Optional[str] = SYSTEM_PROMPT, timeout: Optional[float] = None, client: Any = None,
aclient: Any = None, **kwargs: Any) -> None:
self.model = model
self._ckw: dict = {"token": token or os.environ.get("HF_TOKEN")}
for k, v in (("provider", provider), ("base_url", base_url), ("timeout", timeout)):
if v is not None:
self._ckw[k] = v
self._client, self._aclient = client, aclient
self.max_tokens, self.temperature, self.system, self.kwargs = max_tokens, temperature, system, kwargs
@property
def client(self):
if self._client is None:
from huggingface_hub import InferenceClient
self._client = InferenceClient(**self._ckw)
return self._client
@property
def aclient(self):
if self._aclient is None:
from huggingface_hub import AsyncInferenceClient
self._aclient = AsyncInferenceClient(**self._ckw)
return self._aclient
def _args(self, prompt: str) -> dict:
messages = ([{"role": "system", "content": self.system}] if self.system else []) + [
{"role": "user", "content": prompt}]
kw = dict(self.kwargs)
if self.temperature is not None:
kw["temperature"] = self.temperature
kw.update(messages=messages, max_tokens=self.max_tokens)
if "base_url" not in self._ckw:
kw["model"] = self.model
return kw
def complete(self, prompt: str) -> str:
out = self.client.chat_completion(**self._args(prompt))
return (out.choices[0].message.content or "").strip()
async def acomplete(self, prompt: str) -> str:
out = await self.aclient.chat_completion(**self._args(prompt))
return (out.choices[0].message.content or "").strip()
class TransformersLLM:
"""A local ``transformers`` model (greedy decoding, chat template when available)."""
def __init__(self, model: Any, *, max_new_tokens: int = 64, system: Optional[str] = SYSTEM_PROMPT,
**pipeline_kwargs: Any) -> None:
if isinstance(model, str):
from transformers import pipeline
model = pipeline("text-generation", model=model, **pipeline_kwargs)
self.pipe, self.max_new_tokens, self.system = model, max_new_tokens, system
def complete(self, prompt: str) -> str:
tok = getattr(self.pipe, "tokenizer", None)
if tok is not None and getattr(tok, "chat_template", None):
msgs = ([{"role": "system", "content": self.system}] if self.system else []) + [
{"role": "user", "content": prompt}]
out = self.pipe(msgs, max_new_tokens=self.max_new_tokens, do_sample=False)
gen = out[0]["generated_text"]
return (gen[-1]["content"] if isinstance(gen, list) else str(gen)).strip()
out = self.pipe(prompt, max_new_tokens=self.max_new_tokens, do_sample=False, return_full_text=False)
return out[0]["generated_text"].strip()
def hf_embedder(model: str, *, token: Optional[str] = None, client: Any = None) -> Callable[[List[str]], List[List[float]]]:
"""Embedding function backed by ``InferenceClient.feature_extraction``."""
if client is None:
from huggingface_hub import InferenceClient
client = InferenceClient(token=token or os.environ.get("HF_TOKEN"))
def embed(texts: List[str]) -> List[List[float]]:
rows = []
for t in texts:
v = client.feature_extraction(t, model=model)
v = v.tolist() if hasattr(v, "tolist") else v
while isinstance(v, list) and v and isinstance(v[0], list) and len(v) == 1:
v = v[0]
if v and isinstance(v[0], list): # token embeddings: mean-pool
v = [sum(col) / len(v) for col in zip(*v)]
rows.append([float(x) for x in v])
return rows
return embed
def st_embedder(model: str, **kwargs: Any) -> Callable[[List[str]], Any]:
"""Embedding function from a local ``sentence-transformers`` model."""
from sentence_transformers import SentenceTransformer
m = SentenceTransformer(model, **kwargs)
return lambda texts: m.encode(list(texts), normalize_embeddings=True)
def load_hf_corpus(path: str, *, split: str = "train", text_field: str = "text", title_field: Optional[str] = None,
id_field: Optional[str] = None, limit: Optional[int] = None, **kwargs: Any) -> List[Document]:
"""Documents from a Hugging Face dataset (``datasets.load_dataset``)."""
from datasets import load_dataset
ds = load_dataset(path, split=split, **kwargs)
out: List[Document] = []
for i, row in enumerate(ds):
if limit is not None and i >= limit:
break
out.append(Document(text=str(row[text_field]), title=str(row[title_field]) if title_field else "",
id=str(row[id_field]) if id_field else str(i)))
return out
__all__ = ["HFInferenceLLM", "TransformersLLM", "hf_embedder", "st_embedder", "load_hf_corpus"]