refactor: remove Ollama integration and unused AI configuration
- Remove src/models/ollama_client.py, embeddings.py, embeddings_ollama.py - Remove model aliases and AI config from settings (both config.py files) - Update health endpoints to only check database connectivity - Update tests to reflect database-only health checks - Update README, .env.example, and OIDC docstrings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
4df5cfc106
commit
c1f16d44e5
@@ -1,128 +0,0 @@
|
||||
"""
|
||||
Embedding model client for text vectorization
|
||||
|
||||
Uses sentence-transformers for generating embeddings.
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from sentence_transformers import SentenceTransformer
|
||||
from src.config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class EmbeddingClient:
|
||||
"""Client for generating text embeddings"""
|
||||
|
||||
def __init__(self, model_name: Optional[str] = None):
|
||||
"""
|
||||
Initialize embedding client
|
||||
|
||||
Args:
|
||||
model_name: Optional model name, defaults to config
|
||||
"""
|
||||
self.model_name = model_name or settings.embedding_model
|
||||
self.dimension = settings.embedding_dimension
|
||||
self._model: Optional[SentenceTransformer] = None
|
||||
logger.info(f"Initializing EmbeddingClient with model: {self.model_name}")
|
||||
|
||||
def _load_model(self) -> SentenceTransformer:
|
||||
"""
|
||||
Lazy load the embedding model
|
||||
|
||||
Returns:
|
||||
Loaded SentenceTransformer model
|
||||
"""
|
||||
if self._model is None:
|
||||
logger.info(f"Loading embedding model: {self.model_name}")
|
||||
self._model = SentenceTransformer(self.model_name)
|
||||
logger.info(f"Model loaded successfully. Embedding dimension: {self.dimension}")
|
||||
return self._model
|
||||
|
||||
def embed_text(self, text: str) -> List[float]:
|
||||
"""
|
||||
Generate embedding for a single text
|
||||
|
||||
Args:
|
||||
text: Input text to embed
|
||||
|
||||
Returns:
|
||||
List of floats representing the embedding vector
|
||||
"""
|
||||
model = self._load_model()
|
||||
embedding = model.encode(text, convert_to_numpy=True)
|
||||
return embedding.tolist()
|
||||
|
||||
def embed_batch(self, texts: List[str]) -> List[List[float]]:
|
||||
"""
|
||||
Generate embeddings for multiple texts
|
||||
|
||||
Args:
|
||||
texts: List of input texts
|
||||
|
||||
Returns:
|
||||
List of embedding vectors
|
||||
"""
|
||||
model = self._load_model()
|
||||
embeddings = model.encode(
|
||||
texts,
|
||||
batch_size=settings.embedding_batch_size,
|
||||
convert_to_numpy=True,
|
||||
show_progress_bar=False
|
||||
)
|
||||
return embeddings.tolist()
|
||||
|
||||
def get_dimension(self) -> int:
|
||||
"""
|
||||
Get embedding dimension
|
||||
|
||||
Returns:
|
||||
Embedding vector dimension
|
||||
"""
|
||||
return self.dimension
|
||||
|
||||
|
||||
# Global instance
|
||||
_embedding_client: Optional[EmbeddingClient] = None
|
||||
|
||||
|
||||
def get_embedding_client() -> EmbeddingClient:
|
||||
"""
|
||||
Get or create global embedding client instance
|
||||
|
||||
Returns:
|
||||
EmbeddingClient instance
|
||||
"""
|
||||
global _embedding_client
|
||||
if _embedding_client is None:
|
||||
_embedding_client = EmbeddingClient()
|
||||
return _embedding_client
|
||||
|
||||
|
||||
async def embed_text_async(text: str) -> List[float]:
|
||||
"""
|
||||
Async wrapper for embedding text
|
||||
|
||||
Args:
|
||||
text: Input text
|
||||
|
||||
Returns:
|
||||
Embedding vector
|
||||
"""
|
||||
client = get_embedding_client()
|
||||
return client.embed_text(text)
|
||||
|
||||
|
||||
async def embed_batch_async(texts: List[str]) -> List[List[float]]:
|
||||
"""
|
||||
Async wrapper for batch embedding
|
||||
|
||||
Args:
|
||||
texts: List of input texts
|
||||
|
||||
Returns:
|
||||
List of embedding vectors
|
||||
"""
|
||||
client = get_embedding_client()
|
||||
return client.embed_batch(texts)
|
||||
@@ -1,136 +0,0 @@
|
||||
"""
|
||||
Ollama-based embedding client for text vectorization
|
||||
|
||||
Uses Ollama's embedding API instead of local sentence-transformers.
|
||||
This eliminates the need for PyTorch and heavy ML dependencies.
|
||||
"""
|
||||
import logging
|
||||
import httpx
|
||||
from typing import List, Optional
|
||||
from src.config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class OllamaEmbeddingClient:
|
||||
"""Client for generating text embeddings using Ollama"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
timeout: int = 30
|
||||
):
|
||||
"""
|
||||
Initialize Ollama embedding client
|
||||
|
||||
Args:
|
||||
model_name: Embedding model name (default: nomic-embed-text)
|
||||
base_url: Ollama base URL (default from settings)
|
||||
timeout: Request timeout in seconds
|
||||
"""
|
||||
self.model_name = model_name or settings.embedding_model
|
||||
self.base_url = (base_url or settings.ollama_base_url).rstrip("/")
|
||||
self.timeout = timeout
|
||||
self.dimension = settings.embedding_dimension
|
||||
|
||||
logger.info(f"Initializing OllamaEmbeddingClient with model: {self.model_name}")
|
||||
logger.info(f"Ollama URL: {self.base_url}")
|
||||
|
||||
async def embed_text(self, text: str) -> List[float]:
|
||||
"""
|
||||
Generate embedding for a single text using Ollama
|
||||
|
||||
Args:
|
||||
text: Input text to embed
|
||||
|
||||
Returns:
|
||||
List of floats representing the embedding vector
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/api/embeddings",
|
||||
json={
|
||||
"model": self.model_name,
|
||||
"prompt": text
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
return result["embedding"]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating embedding via Ollama: {e}")
|
||||
raise
|
||||
|
||||
async def embed_batch(self, texts: List[str]) -> List[List[float]]:
|
||||
"""
|
||||
Generate embeddings for multiple texts
|
||||
|
||||
Args:
|
||||
texts: List of input texts
|
||||
|
||||
Returns:
|
||||
List of embedding vectors
|
||||
"""
|
||||
embeddings = []
|
||||
for text in texts:
|
||||
embedding = await self.embed_text(text)
|
||||
embeddings.append(embedding)
|
||||
return embeddings
|
||||
|
||||
def get_dimension(self) -> int:
|
||||
"""
|
||||
Get embedding dimension
|
||||
|
||||
Returns:
|
||||
Embedding vector dimension
|
||||
"""
|
||||
return self.dimension
|
||||
|
||||
|
||||
# Global instance
|
||||
_embedding_client: Optional[OllamaEmbeddingClient] = None
|
||||
|
||||
|
||||
def get_embedding_client() -> OllamaEmbeddingClient:
|
||||
"""
|
||||
Get or create global Ollama embedding client instance
|
||||
|
||||
Returns:
|
||||
OllamaEmbeddingClient instance
|
||||
"""
|
||||
global _embedding_client
|
||||
if _embedding_client is None:
|
||||
_embedding_client = OllamaEmbeddingClient()
|
||||
return _embedding_client
|
||||
|
||||
|
||||
async def embed_text_async(text: str) -> List[float]:
|
||||
"""
|
||||
Async wrapper for embedding text
|
||||
|
||||
Args:
|
||||
text: Input text
|
||||
|
||||
Returns:
|
||||
Embedding vector
|
||||
"""
|
||||
client = get_embedding_client()
|
||||
return await client.embed_text(text)
|
||||
|
||||
|
||||
async def embed_batch_async(texts: List[str]) -> List[List[float]]:
|
||||
"""
|
||||
Async wrapper for batch embedding
|
||||
|
||||
Args:
|
||||
texts: List of input texts
|
||||
|
||||
Returns:
|
||||
List of embedding vectors
|
||||
"""
|
||||
client = get_embedding_client()
|
||||
return await client.embed_batch(texts)
|
||||
@@ -1,223 +0,0 @@
|
||||
"""
|
||||
Ollama client for model inference.
|
||||
Handles both streaming and non-streaming requests.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import json
|
||||
import logging
|
||||
from typing import AsyncIterator, Dict, Any, Optional
|
||||
from src.config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class OllamaClient:
|
||||
"""Client for interacting with Ollama API."""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = settings.ollama_base_url
|
||||
self.timeout = settings.ollama_timeout
|
||||
self.client = httpx.AsyncClient(timeout=self.timeout)
|
||||
logger.info(f"Initialized Ollama client: {self.base_url}")
|
||||
|
||||
async def close(self):
|
||||
"""Close the HTTP client."""
|
||||
await self.client.aclose()
|
||||
|
||||
def resolve_model(self, model_name: str) -> str:
|
||||
"""
|
||||
Resolve model alias to actual Ollama model.
|
||||
|
||||
Args:
|
||||
model_name: Requested model name (e.g., "gpt-3.5-turbo")
|
||||
|
||||
Returns:
|
||||
Actual Ollama model name (e.g., "gemma:7b")
|
||||
"""
|
||||
resolved = settings.model_aliases.get(model_name, model_name)
|
||||
if resolved != model_name:
|
||||
logger.info(f"Model resolution: {model_name} → {resolved}")
|
||||
return resolved
|
||||
|
||||
async def generate_non_streaming(
|
||||
self,
|
||||
model: str,
|
||||
prompt: str,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate non-streaming response from Ollama using chat endpoint.
|
||||
|
||||
Args:
|
||||
model: Model name
|
||||
prompt: User prompt
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
|
||||
Returns:
|
||||
Dict with 'response' and 'tokens' keys
|
||||
"""
|
||||
actual_model = self.resolve_model(model)
|
||||
|
||||
payload = {
|
||||
"model": actual_model,
|
||||
"messages": [
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": temperature,
|
||||
}
|
||||
}
|
||||
|
||||
if max_tokens:
|
||||
payload["options"]["num_predict"] = max_tokens
|
||||
|
||||
logger.debug(f"Ollama request to {actual_model}")
|
||||
|
||||
try:
|
||||
response = await self.client.post(
|
||||
f"{self.base_url}/api/chat",
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
return {
|
||||
"response": result.get("message", {}).get("content", ""),
|
||||
"tokens": {
|
||||
"prompt": result.get("prompt_eval_count", 0),
|
||||
"completion": result.get("eval_count", 0),
|
||||
"total": result.get("prompt_eval_count", 0) + result.get("eval_count", 0)
|
||||
}
|
||||
}
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Ollama request failed: {e}")
|
||||
raise
|
||||
|
||||
async def generate_streaming(
|
||||
self,
|
||||
model: str,
|
||||
prompt: str,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None
|
||||
) -> AsyncIterator[str]:
|
||||
"""
|
||||
Generate streaming response from Ollama using chat endpoint.
|
||||
|
||||
Args:
|
||||
model: Model name
|
||||
prompt: User prompt
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
|
||||
Yields:
|
||||
Token strings
|
||||
"""
|
||||
actual_model = self.resolve_model(model)
|
||||
|
||||
payload = {
|
||||
"model": actual_model,
|
||||
"messages": [
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"stream": True,
|
||||
"options": {
|
||||
"temperature": temperature,
|
||||
}
|
||||
}
|
||||
|
||||
if max_tokens:
|
||||
payload["options"]["num_predict"] = max_tokens
|
||||
|
||||
logger.debug(f"Ollama streaming request to {actual_model}")
|
||||
|
||||
try:
|
||||
async with self.client.stream(
|
||||
"POST",
|
||||
f"{self.base_url}/api/chat",
|
||||
json=payload
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
async for line in response.aiter_lines():
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
chunk = json.loads(line)
|
||||
if "message" in chunk:
|
||||
content = chunk["message"].get("content", "")
|
||||
if content:
|
||||
yield content
|
||||
|
||||
# Check if done
|
||||
if chunk.get("done", False):
|
||||
break
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Failed to parse JSON: {line}")
|
||||
continue
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Ollama streaming request failed: {e}")
|
||||
raise
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""
|
||||
Check if Ollama is healthy.
|
||||
|
||||
Returns:
|
||||
True if healthy, False otherwise
|
||||
"""
|
||||
try:
|
||||
response = await self.client.get(
|
||||
f"{self.base_url}/api/tags",
|
||||
timeout=5.0
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(f"Ollama health check failed: {e}")
|
||||
return False
|
||||
|
||||
async def list_models(self) -> Dict[str, Any]:
|
||||
"""
|
||||
List all available models in Ollama.
|
||||
|
||||
Returns:
|
||||
Dict with 'models' key containing list of model info
|
||||
"""
|
||||
try:
|
||||
response = await self.client.get(
|
||||
f"{self.base_url}/api/tags",
|
||||
timeout=5.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list Ollama models: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# Global client instance
|
||||
_ollama_client: Optional[OllamaClient] = None
|
||||
|
||||
|
||||
def get_ollama_client() -> OllamaClient:
|
||||
"""Get or create the global Ollama client instance."""
|
||||
global _ollama_client
|
||||
if _ollama_client is None:
|
||||
_ollama_client = OllamaClient()
|
||||
return _ollama_client
|
||||
|
||||
|
||||
async def close_ollama_client():
|
||||
"""Close the global Ollama client."""
|
||||
global _ollama_client
|
||||
if _ollama_client is not None:
|
||||
await _ollama_client.close()
|
||||
_ollama_client = None
|
||||
Reference in New Issue
Block a user