Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Welcome to the `api` repository! This repository hosts the core API infrastructu
- **FastAPI**: A modern, fast (high-performance) web framework for building APIs with Python 3.13 based on standard Python type hints.
- **MongoDB**: A NoSQL database used for storing application data, offering flexibility and scalability.
- **Qdrant**: A vector database used for implementing Retrieval-Augmented Generation (RAG) features, enhancing information retrieval capabilities.
- **LLM Integration**: Routes for calling the MistralAI API to leverage Large Language Models (LLMs) for various NLP tasks.
- **LLM Integration**: Routes for calling the MistralAI, OpenAI, or Anthropic APIs to leverage Large Language Models (LLMs) for various NLP tasks.
- **Versioning**: A systematic versioning approach with endpoints like `/v1`, `/v2`, etc., to manage API changes and ensure backward compatibility.

## 🚀 Getting Started
Expand All @@ -21,7 +21,7 @@ Welcome to the `api` repository! This repository hosts the core API infrastructu
- Python 3.13
- MongoDB instance
- Qdrant instance
- MistralAI API key
- API keys for MistralAI, OpenAI, or Anthropic (depending on the provider you wish to use)

### Installation

Expand All @@ -46,8 +46,10 @@ Welcome to the `api` repository! This repository hosts the core API infrastructu
Create a `.env` file in the root directory and add the following variables:
```
MONGODB_URI=your_mongodb_connection_string
QDRANT_URI=your_qdrant_connection_string
QDRANT_URL=your_qdrant_connection_string
MISTRAL_API_KEY=your_mistral_api_key
OPENAI_API_KEY=your_openai_api_key
ANTHROPIC_API_KEY=your_anthropic_api_key
```

### Running the API
Expand All @@ -72,7 +74,7 @@ The API will be accessible at `http://127.0.0.1:3001`.
- `GET /v1/health`: Check the status of the API.

- **LLM Routes**:
- `POST /v1/chat/completions`: Generate text completions using the MistralAI API.
- `POST /v1/chat/completions`: Generate text completions using the configured AI provider.

- **Data Routes**:
- `GET /v1/data`: Retrieve data from MongoDB.
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ dependencies = [
"pymongo>=4.3.3",
"uvicorn>=0.34.0",
"mistralai>=1.5.0",
"openai>=1.3.8",
"anthropic>=0.21.3",
"PyJWT>=2.10.1"
]

Expand Down
9 changes: 5 additions & 4 deletions src/api/classes/embeddings.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from api.providers import AIProvider
from api.utils import CustomLogger, InferenceUtils

logger = CustomLogger.get_logger(__name__)


class Embeddings:
def __init__(self, mistralai_service, inference_utils: InferenceUtils):
self.mistralai_service = mistralai_service
def __init__(self, provider: AIProvider, inference_utils: InferenceUtils):
self.provider = provider
self.inference_utils = inference_utils

async def generate_embeddings(
Expand All @@ -15,14 +16,14 @@ async def generate_embeddings(
job_id: str,
output_format: str = "dict",
):
response = await self.mistralai_service.generate_embeddings(
response = await self.provider.generate_embeddings(
inputs=inputs,
model=model,
)
data = response.get("data", None)

if not data:
raise ValueError("Invalid response from Mistral API")
raise ValueError("Invalid response from AI provider")

if output_format == "points":
return self.inference_utils.embedding_to_points(inputs, data)
Expand Down
10 changes: 6 additions & 4 deletions src/api/classes/text_generation.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Any, AsyncGenerator, Dict

from api.providers import AIProvider


class TextGeneration:
def __init__(self, mistralai_service, inference_utils):
self.mistralai_service = mistralai_service
def __init__(self, provider: AIProvider, inference_utils):
self.provider = provider
self.inference_utils = inference_utils

async def generate_stream_response(
Expand All @@ -29,7 +31,7 @@ async def generate_stream_response(
Returns:
Un générateur asynchrone de dictionnaires contenant les chunks et métadonnées
"""
async for response, finish_reason in self.mistralai_service.stream(
async for response, finish_reason in self.provider.stream(
model=model,
messages=messages,
temperature=temperature,
Expand Down Expand Up @@ -67,7 +69,7 @@ async def complete(
Returns:
Un dictionnaire contenant la réponse et l'identifiant de la tâche
"""
response = await self.mistralai_service.complete(
response = await self.provider.complete(
model=model,
messages=messages,
temperature=temperature,
Expand Down
4 changes: 3 additions & 1 deletion src/api/databases/qdrant_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ async def insert_vectors(self, collection_name, vectors, payloads=None):

points = [
models.PointStruct(id=idx, vector=vector, payload=payload)
for idx, (vector, payload) in enumerate(zip(vectors, payloads))
for idx, (vector, payload) in enumerate(
zip(vectors, payloads, strict=False)
)
]

await self.client.upsert(collection_name=collection_name, points=points)
Expand Down
15 changes: 15 additions & 0 deletions src/api/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from .ai_provider import (
AIProvider,
AnthropicProvider,
MistralAIProvider,
OpenAIProvider,
get_provider,
)

__all__ = [
"AIProvider",
"MistralAIProvider",
"OpenAIProvider",
"AnthropicProvider",
"get_provider",
]
233 changes: 233 additions & 0 deletions src/api/providers/ai_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
from __future__ import annotations

import json
import os
from abc import ABC, abstractmethod
from json.decoder import JSONDecodeError
from typing import AsyncGenerator, Tuple

from anthropic import AsyncAnthropic
from fastapi import HTTPException
from mistralai import Mistral
from openai import AsyncOpenAI

from api.utils import CustomLogger

logger = CustomLogger.get_logger(__name__)


class AIProvider(ABC):
@abstractmethod
async def complete(
self,
model: str,
messages: list,
temperature: float,
max_tokens: int,
top_p: float,
) -> str:
pass

@abstractmethod
async def stream(
self,
model: str,
messages: list,
temperature: float,
max_tokens: int,
top_p: float,
) -> AsyncGenerator[Tuple[str, str | None], None]:
pass

@abstractmethod
async def check_model(self, model: str):
pass

@abstractmethod
async def list_models(self):
pass

@abstractmethod
async def generate_embeddings(self, inputs: list[str], model: str):
pass


class MistralAIProvider(AIProvider):
def __init__(self) -> None:
self.api_key = os.getenv("MISTRAL_API_KEY")
if not self.api_key:
raise ValueError("MISTRAL_API_KEY environment variable is not set")
self.client = Mistral(api_key=self.api_key)

async def complete(self, model, messages, temperature, max_tokens, top_p):
response = await self.client.chat.complete_async(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
)
return response.choices[0].message.content

async def stream(self, model, messages, temperature, max_tokens, top_p):
response = await self.client.chat.stream_async(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
)
async for chunk in response:
if chunk.data.choices[0].delta.content is not None:
yield (
chunk.data.choices[0].delta.content,
chunk.data.choices[0].finish_reason,
)

async def check_model(self, model):
try:
model = await self.client.models.retrieve_async(model_id=model)
if model is None:
raise HTTPException(status_code=404, detail="Model not found")
return model
except Exception as e:
if "status 404" in str(e).lower():
raise HTTPException(status_code=404, detail="Model not found") from e
logger.error(f"An error occurred while checking model : {e}")
raise HTTPException(status_code=503, detail="Service unavailable") from e

async def list_models(self):
return await self.client.models.list_async()

async def generate_embeddings(
self, inputs: list[str], model: str = "mistral-embed"
):
try:
response = await self.client.embeddings.create_async(
model=model, inputs=inputs
)
return json.loads(response.model_dump_json())
except JSONDecodeError as e:
logger.error(f"An error occurred while generating embeddings : {e}")
raise ConnectionError(
f"An error occurred while generating embeddings : {e}"
) from e
except Exception as e:
logger.error(f"An error occurred while generating embeddings : {e}")
raise ConnectionError(
f"An error occurred while generating embeddings : {e}"
) from e


class OpenAIProvider(AIProvider):
def __init__(self) -> None:
self.api_key = os.getenv("OPENAI_API_KEY")
if not self.api_key:
raise ValueError("OPENAI_API_KEY environment variable is not set")
self.client = AsyncOpenAI(api_key=self.api_key)

async def complete(self, model, messages, temperature, max_tokens, top_p):
resp = await self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
)
return resp.choices[0].message.content

async def stream(self, model, messages, temperature, max_tokens, top_p):
stream = await self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
stream=True,
)
async for chunk in stream:
delta = chunk.choices[0].delta
if delta and delta.content:
yield delta.content, chunk.choices[0].finish_reason

async def check_model(self, model):
try:
return await self.client.models.retrieve(model)
except Exception as e:
logger.error(f"OpenAI model check failed: {e}")
raise HTTPException(status_code=404, detail="Model not found") from e

async def list_models(self):
return await self.client.models.list()

async def generate_embeddings(
self, inputs: list[str], model: str = "text-embedding-3-small"
):
try:
resp = await self.client.embeddings.create(model=model, input=inputs)
return json.loads(resp.model_dump_json())
except Exception as e:
logger.error(f"OpenAI embeddings error: {e}")
raise ConnectionError(f"OpenAI embeddings error: {e}") from e


class AnthropicProvider(AIProvider):
def __init__(self) -> None:
self.api_key = os.getenv("ANTHROPIC_API_KEY")
if not self.api_key:
raise ValueError("ANTHROPIC_API_KEY environment variable is not set")
self.client = AsyncAnthropic(api_key=self.api_key)

async def complete(self, model, messages, temperature, max_tokens, top_p):
resp = await self.client.messages.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
)
return resp.content[0].text if resp.content else ""

async def stream(self, model, messages, temperature, max_tokens, top_p):
stream = await self.client.messages.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
stream=True,
)
async for event in stream:
if event.type == "content_block_delta" and event.delta.text:
yield event.delta.text, None

async def check_model(self, model):
try:
return await self.client.models.retrieve(model)
except Exception as e:
logger.error(f"Anthropic model check failed: {e}")
raise HTTPException(status_code=404, detail="Model not found") from e

async def list_models(self):
return await self.client.models.list()

async def generate_embeddings(
self, inputs: list[str], model: str = "claude-3-sonnet-20240229"
):
try:
resp = await self.client.embeddings.create(model=model, input=inputs)
return json.loads(resp.model_dump_json())
except Exception as e:
logger.error(f"Anthropic embeddings error: {e}")
raise ConnectionError(f"Anthropic embeddings error: {e}") from e


def get_provider(name: str) -> AIProvider:
normalized = name.lower()
if normalized in {"mistral", "mistralai"}:
return MistralAIProvider()
if normalized == "openai":
return OpenAIProvider()
if normalized == "anthropic":
return AnthropicProvider()
raise ValueError(f"Unknown provider: {name}")
3 changes: 2 additions & 1 deletion src/api/v1/routes/auth/auth_routes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from datetime import timedelta
from typing import List

from fastapi import APIRouter, Depends, Form, HTTPException, status

from api.v1.security import (
APIAuth,
APIKeyNotFoundError,
Expand All @@ -13,7 +15,6 @@
get_current_user_with_token,
oauth2_scheme,
)
from fastapi import APIRouter, Depends, Form, HTTPException, status

from .auth_models import (
ApiKeyEntry,
Expand Down
3 changes: 3 additions & 0 deletions src/api/v1/routes/chat/chat_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class ChatCompletionsRequest(BaseModel):
"""Modèle pour une requête de complétion de chat"""

model: str = Field(..., description="Identifiant du modèle à utiliser")
provider: str = Field(
"mistral", description="Provider à utiliser (mistral, openai, anthropic)"
)
prompt: Optional[str] = Field(
None, description="Prompt à utiliser pour la génération"
)
Expand Down
Loading