diff --git a/README.md b/README.md index a934ddb9..57afe7bd 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Albert API est une initiative d'[Etalab](https://www.etalab.gouv.fr/). Il s'agit - servir des modèles de langage avec [vLLM](https://github.com/vllm-project/vllm) - servir des modèles d'embeddings avec [HuggingFace Text Embeddings Inference](https://github.com/huggingface/text-embeddings-inference) - servir des modèles de reconnaissance vocale avec [Whisper OpenAI API](https://github.com/etalab-ia/whisper-openai-api) -- accès un *vector store* avec [Qdrant](https://qdrant.tech/) pour la recherche de similarité +- accès un *vector store* avec [Elasticsearch](https://www.elastic.co/fr/products/elasticsearch) pour la recherche de similarité (lexicale, sémantique ou hybride) ou [Qdrant](https://qdrant.tech/) pour la recherche sémantique uniquement. En se basant sur les conventions définies par OpenAI, l'API Albert expose des endpoints qui peuvent être appelés avec le [client officiel python d'OpenAI](https://github.com/openai/openai-python/tree/main). Ce formalisme permet d'intégrer facilement l'API Albert avec des bibliothèques tierces comme [Langchain](https://www.langchain.com/) ou [LlamaIndex](https://www.llamaindex.ai/). diff --git a/app/endpoints/audio.py b/app/endpoints/audio.py index 9366361c..d98bf13c 100644 --- a/app/endpoints/audio.py +++ b/app/endpoints/audio.py @@ -1,15 +1,17 @@ +import json from typing import List, Literal -from fastapi import APIRouter, Form, Security, Request, UploadFile, File +from fastapi import APIRouter, File, Form, HTTPException, Request, Security, UploadFile +from fastapi.responses import PlainTextResponse +import httpx -from app.schemas.audio import AudioTranscription, AudioTranscriptionVerbose +from app.schemas.audio import AudioTranscription from app.schemas.settings import AUDIO_MODEL_TYPE -from app.utils.settings import settings -from app.utils.security import check_api_key, check_rate_limit, User -from app.utils.lifespan import clients, limiter from app.utils.exceptions import ModelNotFoundException -from app.utils.variables import SUPPORTED_LANGUAGES - +from app.utils.lifespan import clients, limiter +from app.utils.security import User, check_api_key, check_rate_limit +from app.utils.settings import settings +from app.utils.variables import DEFAULT_TIMEOUT, SUPPORTED_LANGUAGES router = APIRouter() SUPPORTED_LANGUAGES_VALUES = sorted(set(SUPPORTED_LANGUAGES.values())) + sorted(set(SUPPORTED_LANGUAGES.keys())) @@ -21,32 +23,44 @@ async def audio_transcriptions( request: Request, file: UploadFile = File(...), model: str = Form(...), - language: Literal[*SUPPORTED_LANGUAGES_VALUES] = Form("fr"), + language: Literal[*SUPPORTED_LANGUAGES_VALUES] = Form(default="fr"), prompt: str = Form(None), - response_format: str = Form("json"), + response_format: Literal["json", "text"] = Form(default="json"), temperature: float = Form(0), timestamp_granularities: List[str] = Form(alias="timestamp_granularities[]", default=["segment"]), - user: User = Security(check_api_key), -) -> AudioTranscription | AudioTranscriptionVerbose: + user: User = Security(dependency=check_api_key), +) -> AudioTranscription: """ API de transcription similaire à l'API d'OpenAI. """ - client = clients.models[model] - # @TODO: check if the file is an audio file if client.type != AUDIO_MODEL_TYPE: raise ModelNotFoundException() + # @TODO: Implement prompt + # @TODO: Implement timestamp_granularities + # @TODO: Implement verbose response format + file_content = await file.read() - response = await client.audio.transcriptions.create( - file=(file.filename, file_content, file.content_type), - model=model, - language=language, - prompt=prompt, - response_format=response_format, - temperature=temperature, - timestamp_granularities=timestamp_granularities, - ) - return response + url = f"{client.base_url}audio/transcriptions" + headers = {"Authorization": f"Bearer {client.api_key}"} + + try: + async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as async_client: + response = await async_client.post( + url=url, + headers=headers, + files={"file": (file.filename, file_content, file.content_type)}, + data={"language": language, "response_format": response_format, "temperature": temperature}, + ) + response.raise_for_status() + if response_format == "text": + return PlainTextResponse(content=response.text) + + data = response.json() + return AudioTranscription(**data) + + except Exception as e: + raise HTTPException(status_code=e.response.status_code, detail=json.loads(s=e.response.text)["message"]) diff --git a/app/endpoints/chat.py b/app/endpoints/chat.py index 1bf0bf3d..d19f6bd4 100644 --- a/app/endpoints/chat.py +++ b/app/endpoints/chat.py @@ -1,6 +1,6 @@ from typing import Union - -from fastapi import APIRouter, Request, Security +import json +from fastapi import APIRouter, Request, Security, HTTPException from fastapi.responses import StreamingResponse import httpx @@ -9,14 +9,15 @@ from app.utils.settings import settings from app.utils.lifespan import clients, limiter from app.utils.security import check_api_key, check_rate_limit +from app.utils.variables import DEFAULT_TIMEOUT router = APIRouter() -@router.post("/chat/completions") -@limiter.limit(settings.default_rate_limit, key_func=lambda request: check_rate_limit(request=request)) +@router.post(path="/chat/completions") +@limiter.limit(limit_value=settings.default_rate_limit, key_func=lambda request: check_rate_limit(request=request)) async def chat_completions( - request: Request, body: ChatCompletionRequest, user: User = Security(check_api_key) + request: Request, body: ChatCompletionRequest, user: User = Security(dependency=check_api_key) ) -> Union[ChatCompletion, ChatCompletionChunk]: """Completion API similar to OpenAI's API. See https://platform.openai.com/docs/api-reference/chat/create for the API specification. @@ -25,20 +26,25 @@ async def chat_completions( url = f"{client.base_url}chat/completions" headers = {"Authorization": f"Bearer {client.api_key}"} - # non stream case - if not body.stream: - async with httpx.AsyncClient(timeout=20) as async_client: - response = await async_client.request(method="POST", url=url, headers=headers, json=body.model_dump()) - response.raise_for_status() + try: + # non stream case + if not body.stream: + async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as async_client: + response = await async_client.request(method="POST", url=url, headers=headers, json=body.model_dump()) + response.raise_for_status() + + data = response.json() + return ChatCompletion(**data) - data = response.json() - return ChatCompletion(**data) + # stream case + async def forward_stream(url: str, headers: dict, request: dict): + async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as async_client: + async with async_client.stream(method="POST", url=url, headers=headers, json=request) as response: + response.raise_for_status() + async for chunk in response.aiter_raw(): + yield chunk - # stream case - async def forward_stream(url: str, headers: dict, request: dict): - async with httpx.AsyncClient(timeout=20) as async_client: - async with async_client.stream(method="POST", url=url, headers=headers, json=request) as response: - async for chunk in response.aiter_raw(): - yield chunk + return StreamingResponse(forward_stream(url, headers, body.model_dump()), media_type="text/event-stream") - return StreamingResponse(forward_stream(url, headers, body.model_dump()), media_type="text/event-stream") + except Exception as e: + raise HTTPException(status_code=e.response.status_code, detail=json.loads(e.response.text)["message"]) diff --git a/app/endpoints/completions.py b/app/endpoints/completions.py index 42a017f1..0df1759a 100644 --- a/app/endpoints/completions.py +++ b/app/endpoints/completions.py @@ -1,18 +1,20 @@ -from fastapi import APIRouter, Request, Security +from fastapi import APIRouter, Request, Security, HTTPException import httpx +import json from app.schemas.completions import CompletionRequest, Completions from app.schemas.security import User from app.utils.settings import settings from app.utils.lifespan import clients, limiter from app.utils.security import check_api_key, check_rate_limit +from app.utils.variables import DEFAULT_TIMEOUT router = APIRouter() -@router.post("/completions") -@limiter.limit(settings.default_rate_limit, key_func=lambda request: check_rate_limit(request=request)) -async def completions(request: Request, body: CompletionRequest, user: User = Security(check_api_key)) -> Completions: +@router.post(path="/completions") +@limiter.limit(limit_value=settings.default_rate_limit, key_func=lambda request: check_rate_limit(request=request)) +async def completions(request: Request, body: CompletionRequest, user: User = Security(dependency=check_api_key)) -> Completions: """ Completion API similar to OpenAI's API. See https://platform.openai.com/docs/api-reference/completions/create for the API specification. @@ -21,9 +23,13 @@ async def completions(request: Request, body: CompletionRequest, user: User = Se url = f"{client.base_url}completions" headers = {"Authorization": f"Bearer {client.api_key}"} - async with httpx.AsyncClient(timeout=20) as async_client: - response = await async_client.request(method="POST", url=url, headers=headers, json=body.model_dump()) - response.raise_for_status() + try: + async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as async_client: + response = await async_client.request(method="POST", url=url, headers=headers, json=body.model_dump()) + response.raise_for_status() - data = response.json() - return Completions(**data) + data = response.json() + return Completions(**data) + + except Exception as e: + raise HTTPException(status_code=e.response.status_code, detail=json.loads(e.response.text)["message"]) diff --git a/app/endpoints/embeddings.py b/app/endpoints/embeddings.py index fa35cbe2..180b33c3 100644 --- a/app/endpoints/embeddings.py +++ b/app/endpoints/embeddings.py @@ -1,20 +1,21 @@ -from fastapi import APIRouter, Request, Security +from fastapi import APIRouter, Request, Security, HTTPException import httpx +import json from app.schemas.embeddings import Embeddings, EmbeddingsRequest from app.schemas.security import User from app.utils.settings import settings -from app.utils.exceptions import ContextLengthExceededException, WrongModelTypeException +from app.utils.exceptions import WrongModelTypeException from app.utils.lifespan import clients, limiter from app.utils.security import check_api_key, check_rate_limit -from app.utils.variables import EMBEDDINGS_MODEL_TYPE +from app.utils.variables import EMBEDDINGS_MODEL_TYPE, DEFAULT_TIMEOUT router = APIRouter() -@router.post("/embeddings") -@limiter.limit(settings.default_rate_limit, key_func=lambda request: check_rate_limit(request=request)) -async def embeddings(request: Request, body: EmbeddingsRequest, user: User = Security(check_api_key)) -> Embeddings: +@router.post(path="/embeddings") +@limiter.limit(limit_value=settings.default_rate_limit, key_func=lambda request: check_rate_limit(request=request)) +async def embeddings(request: Request, body: EmbeddingsRequest, user: User = Security(dependency=check_api_key)) -> Embeddings: """ Embedding API similar to OpenAI's API. See https://platform.openai.com/docs/api-reference/embeddings/create for the API specification. @@ -27,15 +28,16 @@ async def embeddings(request: Request, body: EmbeddingsRequest, user: User = Sec url = f"{client.base_url}embeddings" headers = {"Authorization": f"Bearer {client.api_key}"} - async with httpx.AsyncClient(timeout=20) as async_client: - response = await async_client.request(method="POST", url=url, headers=headers, json=body.model_dump()) - try: + try: + async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as async_client: + response = await async_client.request(method="POST", url=url, headers=headers, json=body.model_dump()) + # try: response.raise_for_status() - except httpx.HTTPStatusError as e: - if "`inputs` must have less than" in e.response.text: - raise ContextLengthExceededException() - raise e - - data = response.json() - - return Embeddings(**data) + # except httpx.HTTPStatusError as e: + # if "`inputs` must have less than" in e.response.text: + # raise ContextLengthExceededException() + # raise e + data = response.json() + return Embeddings(**data) + except Exception as e: + raise HTTPException(status_code=e.response.status_code, detail=json.loads(e.response.text)["message"]) diff --git a/app/endpoints/search.py b/app/endpoints/search.py index 44a296d5..a797e574 100644 --- a/app/endpoints/search.py +++ b/app/endpoints/search.py @@ -1,38 +1,58 @@ +import uuid + from fastapi import APIRouter, Request, Security from app.schemas.search import Searches, SearchRequest from app.schemas.security import User -from app.utils.settings import settings from app.utils.lifespan import clients, limiter from app.utils.security import check_api_key, check_rate_limit +from app.utils.settings import settings from app.utils.variables import INTERNET_COLLECTION_DISPLAY_ID - router = APIRouter() -@router.post("/search") -@limiter.limit(settings.default_rate_limit, key_func=lambda request: check_rate_limit(request=request)) -async def search(request: Request, body: SearchRequest, user: User = Security(check_api_key)) -> Searches: +@router.post(path="/search") +@limiter.limit(limit_value=settings.default_rate_limit, key_func=lambda request: check_rate_limit(request=request)) +async def search(request: Request, body: SearchRequest, user: User = Security(dependency=check_api_key)) -> Searches: """ Endpoint to search on the internet or with our engine client """ - # TODO: to be handled by a service top to InternetExplorer + body = await request.json() + body = SearchRequest(**body) + + # Internet search need_internet_search = not body.collections or INTERNET_COLLECTION_DISPLAY_ID in body.collections internet_chunks = [] if need_internet_search: - internet_chunks = clients.internet.get_chunks(prompt=body.prompt) + # get internet results chunks + internet_collection_id = str(uuid.uuid4()) + internet_chunks = clients.internet.get_chunks(prompt=body.prompt, collection_id=internet_collection_id) if internet_chunks: - internet_collection = clients.internet.create_temporary_internet_collection(internet_chunks, body.collections, user) + internet_embeddings_model_id = ( + clients.internet.default_embeddings_model_id + if body.collections == [INTERNET_COLLECTION_DISPLAY_ID] + else clients.search.get_collections(collection_ids=body.collections, user=user)[0].model + ) + + clients.search.create_collection( + collection_id=internet_collection_id, + collection_name=internet_collection_id, + collection_model=internet_embeddings_model_id, + user=user, + ) + clients.search.upsert(chunks=internet_chunks, collection_id=internet_collection_id, user=user) + # case: no other collections, only internet, and no internet results + elif body.collections == [INTERNET_COLLECTION_DISPLAY_ID]: + return Searches(data=[]) + + # case: other collections or only internet and internet results if INTERNET_COLLECTION_DISPLAY_ID in body.collections: body.collections.remove(INTERNET_COLLECTION_DISPLAY_ID) - if not body.collections and not internet_chunks: - return Searches(data=[]) - if internet_chunks: - body.collections.append(internet_collection.id) + body.collections.append(internet_collection_id) searches = clients.search.query( prompt=body.prompt, @@ -40,11 +60,13 @@ async def search(request: Request, body: SearchRequest, user: User = Security(ch method=body.method, k=body.k, rff_k=body.rff_k, - score_threshold=body.score_threshold, user=user, ) if internet_chunks: - clients.search.delete_collection(internet_collection.id, user=user) + clients.search.delete_collection(collection_id=internet_collection_id, user=user) + + if body.score_threshold: + searches = [search for search in searches if search.score >= body.score_threshold] return Searches(data=searches) diff --git a/app/helpers/_clientsmanager.py b/app/helpers/_clientsmanager.py index 309941fd..c47df8bf 100644 --- a/app/helpers/_clientsmanager.py +++ b/app/helpers/_clientsmanager.py @@ -21,7 +21,7 @@ def set(self): self.search = SearchClient.import_constructor(self.settings.search.type)(models=self.models, **self.settings.search.args) - self.internet = InternetClient(model_clients=self.models, search_client=self.search, **self.settings.internet.args) + self.internet = InternetClient(model_clients=self.models, search_client=self.search, **self.settings.internet.args.model_dump()) self.auth = AuthenticationClient(cache=self.cache, **self.settings.auth.args) if self.settings.auth else None diff --git a/app/helpers/_internetclient.py b/app/helpers/_internetclient.py index 24cfb92c..c7cc75cc 100644 --- a/app/helpers/_internetclient.py +++ b/app/helpers/_internetclient.py @@ -1,6 +1,5 @@ from io import BytesIO from typing import List, Literal, Optional -import uuid from duckduckgo_search import DDGS from duckduckgo_search.exceptions import RatelimitException @@ -11,11 +10,9 @@ from app.helpers.parsers import HTMLParser from app.helpers.searchclients import SearchClient from app.helpers._modelclients import ModelClients -from app.schemas.collections import Collection from app.schemas.chunks import Chunk -from app.schemas.security import User from app.utils.logging import logger -from app.utils.variables import INTERNET_COLLECTION_DISPLAY_ID, INTERNET_BRAVE_TYPE, INTERNET_DUCKDUCKGO_TYPE +from app.utils.variables import INTERNET_BRAVE_TYPE, INTERNET_DUCKDUCKGO_TYPE, LANGUAGE_MODEL_TYPE, EMBEDDINGS_MODEL_TYPE class InternetClient: @@ -79,19 +76,38 @@ def __init__( self, model_clients: ModelClients, search_client: SearchClient, + default_language_model: str, + default_embeddings_model: str, type: Literal[INTERNET_DUCKDUCKGO_TYPE, INTERNET_BRAVE_TYPE] = INTERNET_BRAVE_TYPE, api_key: Optional[str] = None, - ): - self.model_clients = model_clients - self.search_client = search_client - self.parser = HTMLParser(collection_id=INTERNET_COLLECTION_DISPLAY_ID) + ) -> None: self.type = type self.api_key = api_key + self.model_clients = model_clients + self.search_client = search_client + self.default_language_model_id = default_language_model + self.default_embeddings_model_id = default_embeddings_model + + assert self.default_language_model_id in self.model_clients, "Default internet language model is unavailable." + assert ( + self.model_clients[self.default_language_model_id].type == LANGUAGE_MODEL_TYPE + ), "Default internet language model is not a language model." + assert self.default_embeddings_model_id in self.model_clients, "Default internet embeddings model is unavailable." + assert ( + self.model_clients[self.default_embeddings_model_id].type == EMBEDDINGS_MODEL_TYPE + ), "Default internet embeddings model is not an embeddings model." + + def get_chunks(self, prompt: str, collection_id: str, n: int = 3) -> List[Chunk]: + query = self._get_web_query(prompt=prompt) + urls = self._get_result_urls(query=query, n=n) + chunks = self._build_chunks(urls=urls, query=query, collection_id=collection_id) + + return chunks def _get_web_query(self, prompt: str) -> str: prompt = self.GET_WEB_QUERY_PROMPT.format(prompt=prompt) - response = self.model_clients[self.model_clients.DEFAULT_INTERNET_LANGUAGE_MODEL_ID].chat.completions.create( - messages=[{"role": "user", "content": prompt}], model=self.model_clients.DEFAULT_INTERNET_LANGUAGE_MODEL_ID, temperature=0.2, stream=False + response = self.model_clients[self.default_language_model_id].chat.completions.create( + messages=[{"role": "user", "content": prompt}], model=self.default_language_model_id, temperature=0.2, stream=False ) query = response.choices[0].message.content @@ -119,22 +135,25 @@ def _get_result_urls(self, query: str, n: int = 3) -> List[str]: results = [] return [result["url"].lower() for result in results] - def _build_chunks(self, urls: List[str], query: str) -> List[Chunk]: + def _build_chunks(self, urls: List[str], query: str, collection_id: str) -> List[Chunk]: chunker = LangchainRecursiveCharacterTextSplitter( chunk_size=self.CHUNK_SIZE, chunk_overlap=self.CHUNK_OVERLAP, chunk_min_size=self.CHUNK_MIN_SIZE ) chunks = [] + parser = HTMLParser(collection_id=collection_id) for url in urls: try: assert not self.LIMITED_DOMAINS or any([domain in url for domain in self.LIMITED_DOMAINS]) - response = requests.get(url, headers={"User-Agent": self.USER_AGENT}) + response = requests.get(url=url, headers={"User-Agent": self.USER_AGENT}) assert response.status_code == 200 except Exception: continue + file = BytesIO(response.text.encode("utf-8")) file = UploadFile(filename=url, file=file) + # TODO: parse pdf if url is a pdf or json if url is a json - output = self.parser.parse(file=file) + output = parser.parse(file=file) chunks.extend(chunker.split(input=output)) if len(chunks) == 0: @@ -144,37 +163,3 @@ def _build_chunks(self, urls: List[str], query: str) -> List[Chunk]: for chunk in chunks: chunk.metadata.internet_query = query return chunks - - def get_chunks(self, prompt: str, n: int = 3) -> List[Chunk]: - query = self._get_web_query(prompt=prompt) - urls = self._get_result_urls(query=query, n=n) - return self._build_chunks(urls=urls, query=query) - - def _get_internet_embeddings_model_id(self, collection_ids: List[str], user: User) -> str: - all_collections_with_internet_are_queried = not collection_ids - if all_collections_with_internet_are_queried: - any_first_collection = self.search_client.get_collections([], user=user)[0] - return any_first_collection.model - - collection_ids_without_internet = [collection_id for collection_id in collection_ids if collection_id != INTERNET_COLLECTION_DISPLAY_ID] - only_internet_collection_queried = len(collection_ids_without_internet) == 0 - if only_internet_collection_queried: - return self.model_clients.DEFAULT_INTERNET_EMBEDDINGS_MODEL_ID - - any_first_collection_queried = self.search_client.get_collections(collection_ids_without_internet, user=user)[0] - return any_first_collection_queried.model - - def create_temporary_internet_collection( - self, chunks: List[Chunk], collection_ids: List[str], user: User, limit: int = 3 - ) -> Optional[Collection]: - stored_internet_collection_id = str(uuid.uuid4()) - internet_embeddings_model_id = self._get_internet_embeddings_model_id(collection_ids, user) - internet_collection = self.search_client.create_collection( - collection_id=stored_internet_collection_id, - collection_name=stored_internet_collection_id, - collection_model=internet_embeddings_model_id, - user=user, - ) - self.search_client.upsert(chunks, collection_id=stored_internet_collection_id, user=user) - - return internet_collection diff --git a/app/helpers/_modelclients.py b/app/helpers/_modelclients.py index 788326ae..bd88c703 100644 --- a/app/helpers/_modelclients.py +++ b/app/helpers/_modelclients.py @@ -1,16 +1,17 @@ from functools import partial import time -from typing import Dict, List, Literal, Any +from typing import Literal, Any -from openai import OpenAI, AsyncOpenAI +from openai import OpenAI import requests - +from fastapi import HTTPException +import json from app.schemas.settings import Settings from app.schemas.embeddings import Embeddings from app.schemas.models import Model, Models from app.utils.logging import logger -from app.utils.exceptions import ContextLengthExceededException, ModelNotAvailableException, ModelNotFoundException -from app.utils.variables import EMBEDDINGS_MODEL_TYPE, LANGUAGE_MODEL_TYPE, AUDIO_MODEL_TYPE +from app.utils.exceptions import ModelNotAvailableException, ModelNotFoundException +from app.utils.variables import EMBEDDINGS_MODEL_TYPE, LANGUAGE_MODEL_TYPE, AUDIO_MODEL_TYPE, DEFAULT_TIMEOUT def get_models_list(self, *args, **kwargs) -> Models: @@ -24,7 +25,7 @@ def get_models_list(self, *args, **kwargs) -> Models: try: if self.type == LANGUAGE_MODEL_TYPE: endpoint = f"{self.base_url}models" - response = requests.get(url=endpoint, headers=headers, timeout=self.DEFAULT_TIMEOUT).json() + response = requests.get(url=endpoint, headers=headers, timeout=DEFAULT_TIMEOUT).json() # Multiple models from one vLLM provider are not supported for now assert len(response["data"]) == 1, "Only one model per model API is supported." @@ -38,7 +39,7 @@ def get_models_list(self, *args, **kwargs) -> Models: elif self.type == EMBEDDINGS_MODEL_TYPE: endpoint = str(self.base_url).replace("/v1/", "/info") - response = requests.get(url=endpoint, headers=headers, timeout=self.DEFAULT_TIMEOUT).json() + response = requests.get(url=endpoint, headers=headers, timeout=DEFAULT_TIMEOUT).json() self.id = response["model_id"] self.owned_by = "huggingface-text-embeddings-inference" @@ -47,7 +48,7 @@ def get_models_list(self, *args, **kwargs) -> Models: elif self.type == AUDIO_MODEL_TYPE: endpoint = f"{self.base_url}models" - response = requests.get(url=endpoint, headers=headers, timeout=self.DEFAULT_TIMEOUT).json() + response = requests.get(url=endpoint, headers=headers, timeout=DEFAULT_TIMEOUT).json() response = response["data"][0] self.id = response["id"] @@ -73,26 +74,6 @@ def get_models_list(self, *args, **kwargs) -> Models: return Models(data=[data]) -def check_context_length(self, messages: List[Dict[str, str]], add_special_tokens: bool = True) -> bool: - # TODO: remove this methode and use better context length handling (by catch context length error model) - headers = {"Authorization": f"Bearer {self.api_key}"} - prompt = "\n".join([message["role"] + ": " + message["content"] for message in messages]) - - if self.type == LANGUAGE_MODEL_TYPE: - data = {"model": self.id, "prompt": prompt, "add_special_tokens": add_special_tokens} - elif self.type == EMBEDDINGS_MODEL_TYPE: - data = {"inputs": prompt, "add_special_tokens": add_special_tokens} - - response = requests.post(url=str(self.base_url).replace("/v1/", "/tokenize"), json=data, headers=headers) - response.raise_for_status() - response = response.json() - - if self.type == LANGUAGE_MODEL_TYPE: - return response["count"] <= self.max_context_length - elif self.type == EMBEDDINGS_MODEL_TYPE: - return len(response[0]) <= self.max_context_length - - def create_embeddings(self, *args, **kwargs): try: url = f"{self.base_url}embeddings" @@ -102,19 +83,17 @@ def create_embeddings(self, *args, **kwargs): data = response.json() return Embeddings(**data) except Exception as e: - if "`inputs` must have less than" in e.response.text: - raise ContextLengthExceededException() - raise e + raise HTTPException(status_code=e.response.status_code, detail=json.loads(e.response.text)["message"]) class ModelClient(OpenAI): DEFAULT_TIMEOUT = 120 - def __init__(self, type=Literal[EMBEDDINGS_MODEL_TYPE, LANGUAGE_MODEL_TYPE], *args, **kwargs) -> None: + def __init__(self, type=Literal[EMBEDDINGS_MODEL_TYPE, LANGUAGE_MODEL_TYPE, AUDIO_MODEL_TYPE], *args, **kwargs) -> None: """ - ModelClient class extends OpenAI class to support custom methods. + ModelClient class extends AsyncOpenAI class to support custom methods. """ - super().__init__(timeout=self.DEFAULT_TIMEOUT, *args, **kwargs) + super().__init__(timeout=DEFAULT_TIMEOUT, *args, **kwargs) self.type = type # set attributes for unavailable models @@ -132,38 +111,6 @@ def __init__(self, type=Literal[EMBEDDINGS_MODEL_TYPE, LANGUAGE_MODEL_TYPE], *ar self.vector_size = len(response.data[0].embedding) self.embeddings.create = partial(create_embeddings, self) - self.check_context_length = partial(check_context_length, self) - - -# TODO merge with ModelClient for all models and adapt endpoint to not use anymore the httpx async client -class AsyncModelClient(AsyncOpenAI): - DEFAULT_TIMEOUT = 120 - - def __init__(self, type=Literal[AUDIO_MODEL_TYPE], *args, **kwargs) -> None: - """ - AsyncModelClient class extends AsyncOpenAI class to support custom methods. - """ - - super().__init__(timeout=self.DEFAULT_TIMEOUT, *args, **kwargs) - self.type = type - - # set attributes for unavailable models - self.id = "" - self.owned_by = "" - self.created = round(number=time.time()) - self.max_context_length = None - - # set real attributes if model is available - self.models.list = partial(get_models_list, self) - response = self.models.list() - - if self.type == EMBEDDINGS_MODEL_TYPE: - response = self.embeddings.create(model=self.id, input="hello world") - self.vector_size = len(response.data[0].embedding) - self.embeddings.create = partial(create_embeddings, self) - - self.check_context_length = partial(check_context_length, self) - class ModelClients(dict): """ @@ -172,21 +119,12 @@ class ModelClients(dict): def __init__(self, settings: Settings) -> None: for model_config in settings.models: - model_client_class = ModelClient if model_config.type != AUDIO_MODEL_TYPE else AsyncModelClient - model = model_client_class(base_url=model_config.url, api_key=model_config.key, type=model_config.type) + model = ModelClient(base_url=model_config.url, api_key=model_config.key, type=model_config.type) if model.status == "unavailable": logger.error(msg=f"unavailable model API on {model_config.url}, skipping.") continue self.__setitem__(key=model.id, value=model) - if model_config.url == settings.default_internet_embeddings_model_url: - self.DEFAULT_INTERNET_EMBEDDINGS_MODEL_ID = model.id - elif model_config.url == settings.default_internet_language_model_url: - self.DEFAULT_INTERNET_LANGUAGE_MODEL_ID = model.id - - assert "DEFAULT_INTERNET_EMBEDDINGS_MODEL_ID" in self.__dict__, "Default internet embeddings model is unavailable." - assert "DEFAULT_INTERNET_LANGUAGE_MODEL_ID" in self.__dict__, "Default internet language model is unavailable." - def __setitem__(self, key: str, value) -> None: if any(key == k for k in self.keys()): raise KeyError(msg=f"Model id {key} is duplicated, not allowed.") diff --git a/app/helpers/searchclients/_elasticsearchclient.py b/app/helpers/searchclients/_elasticsearchclient.py index 3f85a4c7..6ece6572 100644 --- a/app/helpers/searchclients/_elasticsearchclient.py +++ b/app/helpers/searchclients/_elasticsearchclient.py @@ -1,64 +1,33 @@ -from typing import List, Literal, Optional +from concurrent.futures import ThreadPoolExecutor import functools import time -from concurrent.futures import ThreadPoolExecutor +from typing import Any, List, Literal, Optional -from elasticsearch import Elasticsearch, helpers, NotFoundError +from elasticsearch import Elasticsearch, NotFoundError, helpers +from openai import APITimeoutError from app.helpers.searchclients._searchclient import SearchClient +from app.schemas.chunks import Chunk from app.schemas.collections import Collection from app.schemas.documents import Document -from app.schemas.chunks import Chunk -from app.schemas.security import Role -from app.schemas.security import User -from app.schemas.search import Filter, Search +from app.schemas.search import Search +from app.schemas.security import Role, User from app.utils.exceptions import ( - DifferentCollectionsModelsException, - WrongModelTypeException, CollectionNotFoundException, + DifferentCollectionsModelsException, InsufficientRightsException, - SearchMethodNotAvailableException, + WrongModelTypeException, ) from app.utils.variables import ( EMBEDDINGS_MODEL_TYPE, HYBRID_SEARCH_TYPE, LEXICAL_SEARCH_TYPE, - SEMANTIC_SEARCH_TYPE, - PUBLIC_COLLECTION_TYPE, PRIVATE_COLLECTION_TYPE, + PUBLIC_COLLECTION_TYPE, + SEMANTIC_SEARCH_TYPE, ) -def retry(tries: int = 3, delay: int = 2): - """ - A simple retry decorator that catch exception to retry multiple times - @TODO: only catch only network error/timeout error. - - Parameters: - - tries: Number of total attempts. - - delay: Delay between retries in seconds. - """ - - def decorator_retry(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - attempts = tries - while attempts > 1: - try: - return func(*args, **kwargs) - # @TODO: Catch network error. - # except (requests.exceptions.RequestException, httpx.RequestError) as e: - except Exception as e: - time.sleep(delay) - attempts -= 1 - # Final attempt without catching exceptions - return func(*args, **kwargs) - - return wrapper - - return decorator_retry - - class ElasticSearchClient(SearchClient, Elasticsearch): BATCH_SIZE = 48 @@ -68,7 +37,7 @@ def __init__(self, models: List[str] = None, hybrid_limit_factor: float = 1.5, * self.models = models self.hybrid_limit_factor = hybrid_limit_factor - def upsert(self, chunks: List[Chunk], collection_id: str, user: Optional[User] = None) -> None: + def upsert(self, chunks: List[Chunk], collection_id: str, user: User) -> None: collection = self.get_collections(collection_ids=[collection_id], user=user)[0] if user.role != Role.ADMIN and collection.type == PUBLIC_COLLECTION_TYPE: @@ -76,16 +45,20 @@ def upsert(self, chunks: List[Chunk], collection_id: str, user: Optional[User] = for i in range(0, len(chunks), self.BATCH_SIZE): batched_chunks = chunks[i : i + self.BATCH_SIZE] + + texts = [chunk.content for chunk in batched_chunks] + embeddings = self._create_embeddings(input=texts, model=collection.model) + actions = [ { "_index": collection_id, "_source": { "body": chunk.content, - "embedding": self._create_embedding(chunk.content, [collection_id], user), + "embedding": embedding, "metadata": chunk.metadata.model_dump(), }, } - for chunk in batched_chunks + for chunk, embedding in zip(batched_chunks, embeddings) ] helpers.bulk(self, actions, index=collection_id) self.indices.refresh(index=collection_id) @@ -98,28 +71,35 @@ def query( method: Literal[HYBRID_SEARCH_TYPE, LEXICAL_SEARCH_TYPE, SEMANTIC_SEARCH_TYPE] = SEMANTIC_SEARCH_TYPE, k: Optional[int] = 4, rff_k: Optional[int] = 20, - score_threshold: Optional[float] = None, # TODO: implement score_threshold - filter: Optional[Filter] = None, # TODO: implement filter ) -> List[Search]: + """ + See SearchClient.query + """ collections = self.get_collections(collection_ids=collection_ids, user=user) if len(set(collection.model for collection in collections)) > 1: raise DifferentCollectionsModelsException() if method == LEXICAL_SEARCH_TYPE: - return self._lexical_query(prompt, collection_ids, k) - elif method == SEMANTIC_SEARCH_TYPE: - embedding = self._create_embedding(prompt, collection_ids, user) - return self._semantic_query(prompt, embedding, collection_ids, k) - elif method == HYBRID_SEARCH_TYPE: - embedding = self._create_embedding(prompt, collection_ids, user) - with ThreadPoolExecutor(max_workers=2) as executor: - lexical_searches = executor.submit(self._lexical_query, prompt, collection_ids, k).result() - semantic_searches = executor.submit(self._semantic_query, prompt, embedding, collection_ids, k).result() - return self.build_ranked_searches([lexical_searches, semantic_searches], k, rff_k) - raise SearchMethodNotAvailableException() - - def get_collections(self, collection_ids: List[str] = [], user: Optional[User] = None) -> List[Collection]: + searches = self._lexical_query(prompt=prompt, collection_ids=collection_ids, k=k) + else: + if len(set(collection.model for collection in collections)) > 1: + raise DifferentCollectionsModelsException() + + embedding = self._create_embeddings(input=[prompt], model=collections[0].model)[0] + + if method == SEMANTIC_SEARCH_TYPE: + searches = self._semantic_query(prompt=prompt, embedding=embedding, collection_ids=collection_ids, size=k) + + elif method == HYBRID_SEARCH_TYPE: + with ThreadPoolExecutor(max_workers=2) as executor: + lexical_searches = executor.submit(self._lexical_query, prompt, collection_ids, k).result() + semantic_searches = executor.submit(self._semantic_query, prompt, embedding, collection_ids, k).result() + searches = self.build_ranked_searches(searches_list=[lexical_searches, semantic_searches], k=k, rff_k=rff_k) + + return searches + + def get_collections(self, user: User, collection_ids: List[str] = []) -> List[Collection]: """ See SearchClient.get_collections """ @@ -133,8 +113,7 @@ def get_collections(self, collection_ids: List[str] = [], user: Optional[User] = except NotFoundError as e: raise CollectionNotFoundException() - if user: - collections = [collection for collection in collections if collection.user == user.id or collection.type == PUBLIC_COLLECTION_TYPE] + collections = [collection for collection in collections if collection.user == user.id or collection.type == PUBLIC_COLLECTION_TYPE] if collection_ids: for collection in collections: @@ -233,8 +212,10 @@ def get_chunks(self, collection_id: str, document_id: str, user: User, limit: in """ See SearchClient.get_chunks """ + collection = self.get_collections(collection_ids=[collection_id], user=user)[0] + body = {"query": {"match": {"metadata.document_id": document_id}}, "_source": ["body", "metadata"]} - results = self.search(index=collection_id, body=body, from_=offset, size=limit) + results = self.search(index=collection.id, body=body, from_=offset, size=limit) chunks = [] for hit in results["hits"]["hits"]: @@ -245,18 +226,18 @@ def get_chunks(self, collection_id: str, document_id: str, user: User, limit: in # @TODO: pagination between qdrant and elasticsearch diverging # @TODO: offset is not supported by elasticsearch - def get_documents(self, collection_id: str, user: Optional[User] = None, limit: int = 10000, offset: int = 0) -> List[Document]: + def get_documents(self, collection_id: str, user: User, limit: int = 10000, offset: int = 0) -> List[Document]: """ See SearchClient.get_documents """ - c = self.get_collections(collection_ids=[collection_id], user=user) # check if collection exists + _ = self.get_collections(collection_ids=[collection_id], user=user) # check if collection exists body = { "query": {"match_all": {}}, "_source": ["metadata"], "aggs": {"document_ids": {"terms": {"field": "metadata.document_id", "size": limit}}}, } - results = self.search(index=collection_id, body=body, size=1, from_=0) + results = self.search(index=collection_id, body=body, from_=0, size=limit) documents = [] @@ -273,7 +254,7 @@ def get_documents(self, collection_id: str, user: Optional[User] = None, limit: return documents - def delete_document(self, collection_id: str, document_id: str, user: Optional[User] = None): + def delete_document(self, collection_id: str, document_id: str, user: User): """ See SearchClient.delete_document """ @@ -293,7 +274,7 @@ def _build_query_filter(self, prompt: str): fuzziness = {"fuzziness": "AUTO"} return {"multi_match": {"query": prompt, **fuzziness}} - def _build_search(self, hit: dict, method: Literal[LEXICAL_SEARCH_TYPE, SEMANTIC_SEARCH_TYPE]) -> Search: + def _build_search(self, hit: dict) -> Search: return Search( score=hit["_score"], chunk=Chunk( @@ -301,18 +282,17 @@ def _build_search(self, hit: dict, method: Literal[LEXICAL_SEARCH_TYPE, SEMANTIC content=hit["_source"]["body"], metadata=hit["_source"]["metadata"], ), - method=method, ) def _lexical_query(self, prompt: str, collection_ids: List[str], size: int) -> List[Search]: body = { - "query": self._build_query_filter(prompt), + "query": self._build_query_filter(prompt=prompt), "size": size, "_source": {"excludes": ["embedding"]}, } results = self.search(index=",".join(collection_ids), body=body) hits = [hit for hit in results["hits"]["hits"] if hit] - return [self._build_search(hit, method=LEXICAL_SEARCH_TYPE) for hit in hits] + return [self._build_search(hit=hit) for hit in hits] def _semantic_query(self, prompt: str, embedding: list[float], collection_ids: List[str], size: int) -> List[Search]: body = { @@ -326,23 +306,73 @@ def _semantic_query(self, prompt: str, embedding: list[float], collection_ids: L } results = self.search(index=",".join(collection_ids), body=body) hits = [hit for hit in results["hits"]["hits"] if hit] - return [self._build_search(hit, method=SEMANTIC_SEARCH_TYPE) for hit in hits] + return [self._build_search(hit) for hit in hits] - # @TODO: support multiple input in same time - @retry(tries=3, delay=2) - def _create_embedding( - self, - prompt: str, - collection_ids: List[str], - user: User, - ) -> list[float] | list[list[float]] | dict: + def _retry(tries: int = 3, delay: int = 2): + """ + A simple retry decorator that catch exception to retry multiple times + + """ + + def decorator_retry(func): + @functools.wraps(wrapped=func) + def wrapper(*args, **kwargs) -> Any: + attempts = tries + while attempts > 1: + try: + return func(*args, **kwargs) + except APITimeoutError: + time.sleep(delay) + attempts -= 1 + + return func(*args, **kwargs) + + return wrapper + + return decorator_retry + + @_retry(tries=3, delay=2) + def _create_embeddings(self, input: List[str], model: str) -> list[float] | list[list[float]] | dict: """ Simple interface to create an embedding vector from a text input. """ - collections = self.get_collections(collection_ids=collection_ids, user=user) - if len(set(collection.model for collection in collections)) > 1: - raise DifferentCollectionsModelsException() - model_name = collections[0].model - model_client = self.models[model_name] - response = model_client.embeddings.create(input=[prompt], model=model_name) - return response.data[0].embedding + + response = self.models[model].embeddings.create(input=input, model=model) + return [vector.embedding for vector in response.data] + + @staticmethod + def build_ranked_searches(searches_list: List[List[Search]], k: int, rff_k: Optional[int] = 20) -> List[Search]: + """ + Combine search results using Reciprocal Rank Fusion (RRF) + + Args: + searches_list (List[List[Search]]): A list of searches from different query + k (int): The number of results to return + rff_k (Optional[int]): The constant k in the RRF formula + + Returns: + A combined list of searches with updated scores + """ + + combined_scores = {} + search_map = {} + for searches in searches_list: + for rank, search in enumerate(searches): + chunk_id = search.chunk.id + if chunk_id not in combined_scores: + combined_scores[chunk_id] = 0 + search_map[chunk_id] = search + else: + search_map[chunk_id].method = search_map[chunk_id].method + "/" + search.method + combined_scores[chunk_id] += 1 / (rff_k + rank + 1) + + ranked_scores = sorted(combined_scores.items(), key=lambda item: item[1], reverse=True) + reranked_searches = [] + for chunk_id, rrf_score in ranked_scores: + search = search_map[chunk_id] + search.score = rrf_score + reranked_searches.append(search) + + if k: + return reranked_searches[:k] + return reranked_searches diff --git a/app/helpers/searchclients/_qdrantsearchclient.py b/app/helpers/searchclients/_qdrantsearchclient.py index d83e07f0..8916f757 100644 --- a/app/helpers/searchclients/_qdrantsearchclient.py +++ b/app/helpers/searchclients/_qdrantsearchclient.py @@ -25,7 +25,7 @@ from app.utils.exceptions import ( CollectionNotFoundException, DifferentCollectionsModelsException, - SearchMethodNotAvailableException, + NotImplementedException, WrongModelTypeException, InsufficientRightsException, ) @@ -118,14 +118,13 @@ def query( k: Optional[int] = 4, rff_k: Optional[int] = 20, score_threshold: Optional[float] = None, - query_filter: Optional[Filter] = None, ) -> List[Search]: """ See SearchClient.query """ if method != SEMANTIC_SEARCH_TYPE: - raise SearchMethodNotAvailableException() + raise NotImplementedException("Lexical and hybrid search are not available for Qdrant database.") collections = self.get_collections(collection_ids=collection_ids, user=user) if len(set(collection.model for collection in collections)) > 1: @@ -143,7 +142,6 @@ def query( limit=k, score_threshold=score_threshold, with_payload=True, - query_filter=query_filter, ) for result in results: result.payload["metadata"]["collection"] = collection.id @@ -158,7 +156,11 @@ def query( return results - def get_collections(self, collection_ids: List[str] = [], user: Optional[User] = None) -> List[Collection]: + def get_collections( + self, + user: User, + collection_ids: List[str] = [], + ) -> List[Collection]: """ See SearchClient.get_collections """ @@ -247,9 +249,7 @@ def delete_collection(self, collection_id: str, user: User) -> None: super().delete_collection(collection_name=collection.id) super().delete(collection_name=self.METADATA_COLLECTION_ID, points_selector=PointIdsList(points=[collection.id])) - def get_chunks( - self, collection_id: str, document_id: str, user: Optional[User] = None, limit: Optional[int] = 10, offset: Optional[int] = None - ) -> List[Chunk]: + def get_chunks(self, collection_id: str, document_id: str, user: User, limit: Optional[int] = 10, offset: Optional[int] = None) -> List[Chunk]: """ See SearchClient.get_chunks """ @@ -261,9 +261,7 @@ def get_chunks( return chunks - def get_documents( - self, collection_id: str, user: Optional[User] = None, limit: Optional[int] = 10, offset: Optional[int] = None - ) -> List[Document]: + def get_documents(self, collection_id: str, user: User, limit: Optional[int] = 10, offset: Optional[int] = None) -> List[Document]: """ See SearchClient.get_documents """ diff --git a/app/helpers/searchclients/_searchclient.py b/app/helpers/searchclients/_searchclient.py index 6cc33744..0f89ffc7 100644 --- a/app/helpers/searchclients/_searchclient.py +++ b/app/helpers/searchclients/_searchclient.py @@ -6,7 +6,7 @@ from app.schemas.chunks import Chunk from app.schemas.collections import Collection from app.schemas.documents import Document -from app.schemas.search import Filter, Search +from app.schemas.search import Search from app.schemas.security import User from app.utils.variables import HYBRID_SEARCH_TYPE, LEXICAL_SEARCH_TYPE, SEMANTIC_SEARCH_TYPE @@ -45,7 +45,6 @@ def query( k: Optional[int] = 4, rff_k: Optional[int] = 20, score_threshold: Optional[float] = None, - query_filter: Optional[Filter] = None, ) -> List[Search]: """ Search for chunks in a collection. @@ -54,10 +53,9 @@ def query( prompt (str): The prompt to search for. user (User): The user searching for the chunks. collection_ids (List[str]): The ids of the collections to search in. - method (Literal[LEXICAL_SEARCH_TYPE, SEMANTIC_SEARCH_TYPE]): The method to use for the search. + method (Literal[LEXICAL_SEARCH_TYPE, SEMANTIC_SEARCH_TYPE, HYBRID_SEARCH_TYPE]): The method to use for the search, default: SEMENTIC_SEARCH_TYPE. k (Optional[int]): The number of chunks to return. score_threshold (Optional[float]): The score threshold for the chunks to return. - filter (Optional[Filter]): The filter to apply to the chunks to return. Returns: List[Search]: A list of search objects containing the retrieved chunks. @@ -105,15 +103,12 @@ def delete_collection(self, collection_id: str, user: User) -> None: pass @abstractmethod - def get_chunks( - self, collection_id: str, document_id: str, user: User, limit: Optional[int] = None, offset: Union[int, UUID] = None - ) -> List[Chunk]: + def get_chunks(self, collection_id: str, document_id: str, limit: Optional[int] = None, offset: Union[int, UUID] = None) -> List[Chunk]: """ Get chunks from a collection and a document. Args: collection_id (str): The id of the collection to get chunks from. document_id (str): The id of the document to get chunks from. - user (User): The user retrieving the chunks. limit (Optional[int]): The number of chunks to return. offset (Optional[int, UUID]): The offset of the chunks to return (UUID is for qdrant and int for elasticsearch) Returns: @@ -148,36 +143,3 @@ def delete_document(self, collection_id: str, document_id: str, user: User): user (User): The user deleting the document. """ pass - - @staticmethod - def build_ranked_searches(searches_list: List[List[Search]], limit: int, rff_k: Optional[int] = 20): - """ - Combine search results using Reciprocal Rank Fusion (RRF) - :param searches_list: A list of searches from different query - :param limit: The number of results to return - :param rff_k: The constant k in the RRF formula - :return: A combined list of searches with updated scores - """ - - combined_scores = {} - search_map = {} - for searches in searches_list: - for rank, search in enumerate(searches): - chunk_id = search.chunk.id - if chunk_id not in combined_scores: - combined_scores[chunk_id] = 0 - search_map[chunk_id] = search - else: - search_map[chunk_id].method = search_map[chunk_id].method + "/" + search.method - combined_scores[chunk_id] += 1 / (rff_k + rank + 1) - - ranked_scores = sorted(combined_scores.items(), key=lambda item: item[1], reverse=True) - reranked_searches = [] - for chunk_id, rrf_score in ranked_scores: - search = search_map[chunk_id] - search.score = rrf_score - reranked_searches.append(search) - - if limit: - return reranked_searches[:limit] - return reranked_searches diff --git a/app/schemas/chat.py b/app/schemas/chat.py index f6c0f5af..0e60ab17 100644 --- a/app/schemas/chat.py +++ b/app/schemas/chat.py @@ -7,7 +7,7 @@ ) from pydantic import BaseModel, Field, model_validator -from app.utils.exceptions import ContextLengthExceededException, MaxTokensExceededException, WrongModelTypeException +from app.utils.exceptions import WrongModelTypeException from app.utils.lifespan import clients from app.utils.variables import LANGUAGE_MODEL_TYPE @@ -39,11 +39,6 @@ def validate_model(cls, values): if clients.models[values.model].type != LANGUAGE_MODEL_TYPE: raise WrongModelTypeException() - if not clients.models[values.model].check_context_length(messages=values.messages): - raise ContextLengthExceededException() - - if values.max_tokens is not None and values.max_tokens > clients.models[values.model].max_context_length: - raise MaxTokensExceededException() return values diff --git a/app/schemas/completions.py b/app/schemas/completions.py index db470a14..0013091a 100644 --- a/app/schemas/completions.py +++ b/app/schemas/completions.py @@ -5,7 +5,7 @@ from app.utils.lifespan import clients from app.utils.variables import LANGUAGE_MODEL_TYPE -from app.utils.exceptions import WrongModelTypeException, ContextLengthExceededException, MaxTokensExceededException +from app.utils.exceptions import WrongModelTypeException class CompletionRequest(BaseModel): @@ -32,12 +32,6 @@ def validate_model(cls, values): if clients.models[values.model].type != LANGUAGE_MODEL_TYPE: raise WrongModelTypeException() - if not clients.models[values.model].check_context_length(messages=values.messages): - raise ContextLengthExceededException() - - if values.max_tokens is not None and values.max_tokens > clients.models[values.model].max_context_length: - raise MaxTokensExceededException() - class Completions(Completion): pass diff --git a/app/schemas/search.py b/app/schemas/search.py index 9032d07d..4ed3f345 100644 --- a/app/schemas/search.py +++ b/app/schemas/search.py @@ -1,7 +1,8 @@ from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, field_validator, model_validator +from app.utils.exceptions import WrongSearchMethodException from app.schemas.chunks import Chunk from app.utils.variables import INTERNET_COLLECTION_DISPLAY_ID, HYBRID_SEARCH_TYPE, LEXICAL_SEARCH_TYPE, SEMANTIC_SEARCH_TYPE @@ -12,7 +13,9 @@ class SearchRequest(BaseModel): rff_k: int = Field(default=20, description="k constant in RFF algorithm") k: int = Field(gt=0, description="Number of results to return") method: Literal[HYBRID_SEARCH_TYPE, LEXICAL_SEARCH_TYPE, SEMANTIC_SEARCH_TYPE] = Field(default=SEMANTIC_SEARCH_TYPE) - score_threshold: Optional[float] = Field(0.0, ge=0.0, le=1.0, description="Score of cosine similarity threshold for filtering results") + score_threshold: Optional[float] = Field( + 0.0, ge=0.0, le=1.0, description="Score of cosine similarity threshold for filtering results, only available for semantic search method." + ) @field_validator("prompt") def blank_string(prompt): @@ -26,34 +29,17 @@ def convert_to_string(cls, collections): return [] return list(set(str(collection) for collection in collections)) + @model_validator(mode="after") + def score_threshold_filter(cls, values): + if values.score_threshold and values.method != SEMANTIC_SEARCH_TYPE: + raise WrongSearchMethodException(detail="Score threshold is only available for semantic search method.") + class Search(BaseModel): score: float chunk: Chunk - method: Optional[Literal[LEXICAL_SEARCH_TYPE, SEMANTIC_SEARCH_TYPE, f"{LEXICAL_SEARCH_TYPE}/{SEMANTIC_SEARCH_TYPE}"]] = None class Searches(BaseModel): object: Literal["list"] = "list" data: List[Search] - - -class Filter(BaseModel): - pass - - -class MatchAny(BaseModel): - any: List[str] - - -class FieldCondition(BaseModel): - key: str - match: MatchAny - - -class FilterSelector(BaseModel): - filter: Filter - - -class PointIdsList(BaseModel): - points: List[str] diff --git a/app/schemas/settings.py b/app/schemas/settings.py index 1fd1da9f..5042bb01 100644 --- a/app/schemas/settings.py +++ b/app/schemas/settings.py @@ -18,7 +18,7 @@ class ConfigBaseModel(BaseModel): class Config: - extra = "forbid" + extra = "allow" class Key(ConfigBaseModel): @@ -51,9 +51,17 @@ class Databases(ConfigBaseModel): search: SearchDB +class InternetArgs(ConfigBaseModel): + default_language_model: str + default_embeddings_model: str + + class Config: + extra = "allow" + + class Internet(ConfigBaseModel): type: Literal[INTERNET_DUCKDUCKGO_TYPE, INTERNET_BRAVE_TYPE] = INTERNET_DUCKDUCKGO_TYPE - args: Optional[dict] = {} + args: InternetArgs class Config(ConfigBaseModel): @@ -94,10 +102,6 @@ class Settings(BaseSettings): app_version: str = "0.0.0" app_description: str = "[See documentation](https://github.com/etalab-ia/albert-api/blob/main/README.md)" - # models - default_internet_language_model_url: Optional[str] = None - default_internet_embeddings_model_url: Optional[str] = None - # rate_limit global_rate_limit: str = "100/minute" default_rate_limit: str = "10/minute" @@ -113,21 +117,6 @@ def config_file_exists(cls, config_file): @model_validator(mode="after") def setup_config(cls, values): config = Config(**yaml.safe_load(stream=open(file=values.config_file, mode="r"))) - if not values.default_internet_language_model_url: - values.default_internet_language_model_url = [model.url for model in config.models if model.type == LANGUAGE_MODEL_TYPE][0] - - else: - assert values.default_internet_language_model_url in [ - model.url for model in config.models if model.type == LANGUAGE_MODEL_TYPE - ], "Wrong default internet language model url" - - if not values.default_internet_embeddings_model_url: - values.default_internet_embeddings_model_url = [model.url for model in config.models if model.type == EMBEDDINGS_MODEL_TYPE][0] - - else: - assert values.default_internet_embeddings_model_url in [ - model.url for model in config.models if model.type == EMBEDDINGS_MODEL_TYPE - ], "Wrong default internet embeddings model url" values.auth = config.auth values.cache = config.databases.cache diff --git a/app/tests/test_chat.py b/app/tests/test_chat.py index 44361169..5b247db9 100644 --- a/app/tests/test_chat.py +++ b/app/tests/test_chat.py @@ -84,12 +84,12 @@ def test_chat_completions_max_tokens_too_large(self, args, session_user, setup): params = { "model": MODEL_ID, "messages": [{"role": "user", "content": prompt}], - "stream": True, + "stream": False, "n": 1, "max_tokens": MAX_CONTEXT_LENGTH + 100, } response = session_user.post(f"{args['base_url']}/chat/completions", json=params) - assert response.status_code == 422, f"error: retrieve chat completions ({response.status_code})" + assert response.status_code == 400, f"error: retrieve chat completions ({response.status_code})" def test_chat_completions_context_too_large(self, args, session_user, setup): MODEL_ID, MAX_CONTEXT_LENGTH = setup @@ -98,9 +98,9 @@ def test_chat_completions_context_too_large(self, args, session_user, setup): params = { "model": MODEL_ID, "messages": [{"role": "user", "content": prompt}], - "stream": True, + "stream": False, "n": 1, "max_tokens": 10, } response = session_user.post(f"{args['base_url']}/chat/completions", json=params) - assert response.status_code == 413, f"error: retrieve chat completions ({response.status_code})" + assert response.status_code == 400, f"error: retrieve chat completions ({response.status_code})" diff --git a/app/tests/test_search.py b/app/tests/test_search.py index 1846599f..a3830c1d 100644 --- a/app/tests/test_search.py +++ b/app/tests/test_search.py @@ -115,7 +115,6 @@ def test_lexical_search(self, args, session_user, setup): if settings.search.type == SEARCH_ELASTIC_TYPE: assert response.status_code == 200 assert "Albert" in result["data"][0]["chunk"]["content"] - assert result["data"][0]["method"] == "lexical" else: assert response.status_code == 400 @@ -129,8 +128,6 @@ def test_semantic_search(self, args, session_user, setup): assert response.status_code == 200 assert "Erasmus" in result["data"][0]["chunk"]["content"] or "Erasmus" in result["data"][1]["chunk"]["content"] assert "Albert" in result["data"][0]["chunk"]["content"] or "Albert" in result["data"][1]["chunk"]["content"] - assert result["data"][0]["method"] == "semantic" - assert result["data"][1]["method"] == "semantic" def test_hybrid_search(self, args, session_user, setup): """Test hybrid search.""" @@ -142,6 +139,5 @@ def test_hybrid_search(self, args, session_user, setup): if settings.search.type == SEARCH_ELASTIC_TYPE: assert response.status_code == 200 assert "Erasmus" in result["data"][0]["chunk"]["content"] - assert result["data"][0]["method"] == "lexical/semantic" else: assert response.status_code == 400 diff --git a/app/utils/exceptions.py b/app/utils/exceptions.py index 618634bb..c95dc0fd 100644 --- a/app/utils/exceptions.py +++ b/app/utils/exceptions.py @@ -22,6 +22,11 @@ def __init__(self, detail: str = "Method not available."): super().__init__(status_code=400, detail=detail) +class WrongSearchMethodException(HTTPException): + def __init__(self, detail: str = "Wrong search method."): + super().__init__(status_code=400, detail=detail) + + # 403 class InvalidAuthenticationSchemeException(HTTPException): def __init__(self, detail: str = "Invalid authentication scheme.") -> None: @@ -50,11 +55,6 @@ def __init__(self, detail: str = "Model not found.") -> None: # 413 -class ContextLengthExceededException(HTTPException): - def __init__(self, detail: str = "Context length exceeded.") -> None: - super().__init__(status_code=413, detail=detail) - - class FileSizeLimitExceededException(HTTPException): MAX_CONTENT_SIZE = 20 * 1024 * 1024 # 20MB @@ -86,3 +86,8 @@ def __init__(self, detail: str = "Different collections models.") -> None: class UnsupportedFileTypeException(HTTPException): def __init__(self, detail: str = "Unsupported file type.") -> None: super().__init__(status_code=422, detail=detail) + + +class NotImplementedException(HTTPException): + def __init__(self, detail: str = "Not implemented.") -> None: + super().__init__(status_code=400, detail=detail) diff --git a/app/utils/variables.py b/app/utils/variables.py index eb5b039c..ef0c9f64 100644 --- a/app/utils/variables.py +++ b/app/utils/variables.py @@ -145,3 +145,5 @@ HYBRID_SEARCH_TYPE = "hybrid" LEXICAL_SEARCH_TYPE = "lexical" SEMANTIC_SEARCH_TYPE = "semantic" + +DEFAULT_TIMEOUT = 120 diff --git a/compose.yml b/compose.yml index 9eba85cd..5cd3df6f 100644 --- a/compose.yml +++ b/compose.yml @@ -3,9 +3,7 @@ services: build: context: . dockerfile: ./app/Dockerfile - depends_on: - elasticsearch: - condition: service_healthy + image: ghcr.io/etalab-ia/albert-api/app:latest command: uvicorn app.main:app --host 0.0.0.0 --port 8000 environment: @@ -15,8 +13,11 @@ services: - 8000:8000 volumes: - .:/home/albert/conf - - ./app:/home/albert/app - - ./pyproject.toml:/home/albert/pyproject.toml + depends_on: + elasticsearch: + condition: service_healthy + redis: + condition: service_healthy streamlit: image: ghcr.io/etalab-ia/albert-api/ui:latest @@ -26,27 +27,12 @@ services: - BASE_URL=http://fastapi:8000/v1 ports: - 8501:8501 - volumes: - - ./ui:/ui - - qdrant: - image: qdrant/qdrant:v1.11.5-unprivileged - restart: always - environment: - - QDRANT__SERVICE__API_KEY=changeme - ports: - - 6333:6333 - - 6334:6334 - volumes: - - qdrant:/qdrant/storage redis: image: redis/redis-stack-server:7.2.0-v11 restart: always environment: REDIS_ARGS: --dir /data --requirepass changeme --user username on >password ~* allcommands --save 60 1 --appendonly yes - ports: - - 6379:6379 volumes: - redis:/data healthcheck: @@ -55,14 +41,20 @@ services: timeout: 10s retries: 5 - elasticsearch: + qdrant: + image: qdrant/qdrant:v1.11.5-unprivileged + restart: always + environment: + - QDRANT__SERVICE__API_KEY=changeme + ports: + - 6333:6333 + - 6334:6334 + volumes: + - qdrant:/qdrant/storage + + elastic: image: docker.elastic.co/elasticsearch/elasticsearch:8.16.0 restart: always - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9200/_cluster/health?wait_for_status=yellow"] - interval: 10s - timeout: 20s - retries: 3 environment: - discovery.type=single-node - xpack.security.enabled=false @@ -71,10 +63,13 @@ services: - ELASTIC_PASSWORD=elastic - logger.level=WARN - rootLogger.level=WARN - ports: - - 9200:9200 volumes: - elasticsearch:/usr/share/elasticsearch/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9200/_cluster/health?wait_for_status=yellow"] + interval: 10s + timeout: 10s + retries: 3 volumes: elasticsearch: diff --git a/docs/deployment.md b/docs/deployment.md index 2b50001d..8ee7e0d9 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -24,8 +24,7 @@ | DEFAULT_RATE_LIMIT | Limite de taux par défaut pour les requêtes API par utilisateur (par défaut : "10/minute") | | CONFIG_FILE | Chemin vers le fichier de configuration (par défaut : "config.yml") | | LOG_LEVEL | Niveau de journalisation (par défaut : DEBUG) | -| DEFAULT_INTERNET_LANGUAGE_MODEL_URL | URL d'un modèle de langage pour RAG sur la recherche internet (par défaut : premier modèle de langage disponible) | -| DEFAULT_INTERNET_EMBEDDINGS_MODEL_URL | URL d'un modèle d'embeddings pour RAG sur la recherche internet (par défaut : premier modèle d'embeddings disponible) | + ### Clients tiers @@ -33,7 +32,8 @@ Pour fonctionner, l'API Albert nécessite des clients tiers : * [Optionnel] Auth : [Grist](https://www.getgrist.com/)* * Cache : [Redis](https://redis.io/) -* Vectors : [Qdrant](https://qdrant.tech/) +* Internet : [DuckDuckGo](https://duckduckgo.com/) ou [Brave](https://search.brave.com/) +* Vectors : [Qdrant](https://qdrant.tech/) ou [Elasticsearch](https://www.elastic.co/fr/products/elasticsearch) * Models : * [vLLM](https://github.com/vllm-project/vllm) * [HuggingFace Text Embeddings Inference](https://github.com/huggingface/text-embeddings-inference) @@ -49,7 +49,15 @@ auth: [optional] args: [optional] [arg_name]: [value] ... - + +internet: + type: duckduckgo|brave + args: + default_language_model: [required] + default_embeddings_model: [required] + [arg_name]: [value] + ... + models: - url: [required] key: [optional] @@ -67,7 +75,7 @@ databases: [arg_name]: [value] ... - search: [required] + search: [required] type: elastic|qdrant args: [required] [arg_name]: [value] diff --git a/docs/tutorials/Search.ipynb b/docs/tutorials/Search.ipynb deleted file mode 100644 index 38bfa457..00000000 --- a/docs/tutorials/Search.ipynb +++ /dev/null @@ -1,1122 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 8, - "id": "1d25c052-5eb7-48c1-87a9-0117a665f294", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import pprint\n", - "\n", - "import requests\n", - "\n", - "base_url = \"http://localhost:8000/v1\"\n", - "api_key = os.getenv(\"API_KEY\")\n", - "session = requests.session()\n", - "session.headers = {\"Authorization\": f\"Bearer {api_key}\"}" - ] - }, - { - "cell_type": "markdown", - "id": "6950f46d-e294-48ed-89cc-2ca5536642af", - "metadata": {}, - "source": [ - "# LEXICAL SEARCH" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "3474088e-6c75-4c92-a4d3-51631df4f025", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'data': [{'chunk': {'content': 'relative à la prise de contrôle conjoint de '\n", - " 'la société Mymika par la société Sesyclau aux '\n", - " \"côtés d'ITM Entreprises RÉPUBLIQUE FRANÇAISE\\n\"\n", - " 'Décision n° 14-DCC-113 du 4 août 2014relative '\n", - " 'à la prise de contrôle conjoint de la société '\n", - " \"Mymika par lasociété Sesyclau aux côtés d'ITM \"\n", - " 'Entreprises\\n'\n", - " \"L'Autorité de la concurrence,\\n\"\n", - " 'Vu le dossier de notification adressé complet '\n", - " 'au service des concentrations le 10 juillet '\n", - " '2014, relatif à la prise de contrôle conjoint '\n", - " 'de la société Mymika par la société Sesyclau '\n", - " \"aux côtés d'ITM Entreprises, et matérialisée \"\n", - " \"par une lettre d'intention contresignée en \"\n", - " 'date du 1er juillet 2014 ;\\n'\n", - " 'Vu le livre IV du code de commerce relatif à '\n", - " 'la liberté des prix et de la concurrence, et '\n", - " 'notamment ses articles L. 430-1 à L. 430-7 ;\\n'\n", - " 'Adopte la décision suivante :\\n'\n", - " \"1. L'opération notifiée consiste en la prise \"\n", - " 'de contrôle conjoint de la société Mymika, '\n", - " 'qui exploite un fonds de commerce de '\n", - " 'distribution à dominante alimentaire sous '\n", - " 'enseigne Intermarché dans la ville de '\n", - " 'Châteauroux (36), par la société Sesyclau aux '\n", - " \"côtés d'ITM Entreprises. Sesyclau exploite \"\n", - " 'des points de vente de commerce de détail, '\n", - " 'situées à Saint- Amand-Montrand (18) et Le '\n", - " \"Magny (36) à l'enseigne Intermarché et Netto. \"\n", - " 'Cette opération constitue une concentration '\n", - " \"au sens de l'article L. 430-1 du code de \"\n", - " \"commerce. Compte tenu des chiffres d'affaires \"\n", - " 'réalisés par les entreprises concernées, '\n", - " \"l'opération ne relève pas de la compétence de \"\n", - " \"l'Union européenne. En revanche, les seuils \"\n", - " 'de contrôle relatifs au commerce de détail '\n", - " \"mentionnés au point I de l'article L. 430-2 \"\n", - " 'du code de commerce sont franchis. La '\n", - " 'présente opération est donc soumise aux '\n", - " 'dispositions des articles L. 430-3 et '\n", - " 'suivants du code de commerce relatifs à la '\n", - " 'concentration économique.\\n'\n", - " \"2. Au vu des éléments du dossier, l'opération \"\n", - " \"n'est pas de nature à porter atteinte à la \"\n", - " 'concurrence sur les marchés concernés.\\n'\n", - " 'DECIDE\\n'\n", - " \"Article unique : L'opération notifiée sous le \"\n", - " 'numéro 14-119 est autorisée.\\n'\n", - " 'La vice-présidente,\\n'\n", - " 'Élisabeth Flüry-Hérard\\n'\n", - " '\\uf0d3 Autorité de la concurrence',\n", - " 'id': 'wXkLS5MBPJr2-PEwfx81',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '04 août 2014',\n", - " 'document_created_at': 1731413785,\n", - " 'document_id': '000bc19b-e34d-4e59-b372-7a4a990bf5bd',\n", - " 'document_name': 'relative à la prise de '\n", - " 'contrôle conjoint de la '\n", - " 'société Mymika par la '\n", - " 'société Sesyclau aux côtés '\n", - " \"d'ITM Entreprises\",\n", - " 'document_part': 1,\n", - " 'id_decision': '14-DCC-113',\n", - " 'secteur_activite': 'Distribution'},\n", - " 'object': 'chunk'},\n", - " 'method': 'lexical',\n", - " 'score': 0.047619047619047616},\n", - " {'chunk': {'content': 'La rapporteure, la rapporteure générale '\n", - " 'adjointe, le commissaire du Gouvernement et '\n", - " 'les représentants des sociétés Materne SAS, '\n", - " 'MBMA SAS et MBMA Holding SAS, des sociétés '\n", - " 'Établissements Frédéric Legros et Sodibel '\n", - " \"entendus lors de la séance de l'Autorité de \"\n", - " 'la concurrence du 19 juillet 2017 ;\\n'\n", - " 'Adopte la décision suivante :\\n'\n", - " 'Résumé1 :\\n'\n", - " \"Dans la décision ci-après, l'Autorité de la \"\n", - " 'concurrence condamne la société Materne SAS '\n", - " 'en tant que société auteur et les sociétés '\n", - " 'MOM, MBMA SAS et MBMA Holding SAS en qualité '\n", - " \"de sociétés mères de l'auteur, à une sanction \"\n", - " 'de 70 000 euros, pour avoir accordé des '\n", - " \"droits exclusifs d'importation des produits \"\n", - " 'Materne à la société Sodibel, filiale de la '\n", - " 'société Établissements Frédéric Legros, sur '\n", - " 'le territoire de la Réunion et de Mayotte, '\n", - " 'pendant la période de 22 mars 2013 au 5 '\n", - " 'juillet 2016.\\n'\n", - " 'Ces droits ont été maintenus postérieurement '\n", - " \"au 22 mars 2013, en violation de l'article L. \"\n", - " '420-2-1 du code de commerce, inséré par la '\n", - " 'loi n° 2012-1270 du 20 novembre 2012 relative '\n", - " 'à la régulation économique outre-mer, dite '\n", - " 'loi « Lurel », qui prohibe les accords ou '\n", - " 'pratiques concertées ayant pour objet ou pour '\n", - " \"effet d'accorder des droits exclusifs \"\n", - " \"d'importation à une entreprise ou à un groupe \"\n", - " \"d'entreprises dans les collectivités \"\n", - " \"d'outre-mer.\\n\"\n", - " \"L'Autorité de la concurrence condamne \"\n", - " 'également la société Sodibel en tant '\n", - " \"qu'auteur et la société Établissements \"\n", - " 'Frédéric Legros en tant que co-auteur et '\n", - " 'société mère, à une sanction de 30 000 euros, '\n", - " 'pour avoir bénéficié de ces droits exclusifs '\n", - " \"d'importation.\\n\"\n", - " 'Ces sanctions ont été prononcées dans le '\n", - " 'respect des termes de la transaction proposée '\n", - " 'par le rapporteur général, sollicitée et '\n", - " 'acceptée par les parties.\\n'\n", - " '1 Ce résumé a un caractère strictement '\n", - " 'informatif. Seuls font foi les motifs de la '\n", - " 'décision numérotés ci-après.\\n'\n", - " 'SOMMAIRE\\n'\n", - " 'I. RAPPEL DE LA PROCÉDURE '\n", - " '............................................................................................. '\n", - " '5\\n'\n", - " 'II. '\n", - " 'CONSTATATIONS.............................................................................................................. '\n", - " '6\\n'\n", - " 'A. Le secteur et les entreprises concernés '\n", - " '.......................................................... '\n", - " '6\\n'\n", - " '1. Le secteur concerné '\n", - " '.......................................................................................... '\n", - " '6\\n'\n", - " '2. Les entreprises concernées '\n", - " '............................................................................... '\n", - " '7\\n'\n", - " 'a) Materne SAS '\n", - " '.................................................................................................. '\n", - " '7\\n'\n", - " 'b) Sodibel '\n", - " '........................................................................................................... '\n", - " '7\\n'\n", - " 'B. Les pratiques constatées '\n", - " '.................................................................................. '\n", - " '8\\n'\n", - " 'C. Les griefs notifiés '\n", - " '............................................................................................. '\n", - " '8\\n'\n", - " 'III. DISCUSSION '\n", - " '................................................................................................................. '\n", - " '9\\n'\n", - " 'A. Sur la mise en œuvre de la procédure de '\n", - " 'transaction '\n", - " '.................................. 9\\n'\n", - " \"B. Sur l'existence de droits exclusifs \"\n", - " \"d'importation \"\n", - " '.......................................... 9\\n'\n", - " 'C. Sur les sanctions '\n", - " '............................................................................................. '\n", - " '10\\n'\n", - " '1. Sur la gravité des pratiques et le dommage '\n", - " \"à l'économie ............................. \"\n", - " '10\\n'\n", - " 'a) Sur la gravité des pratiques '\n", - " '........................................................................ '\n", - " '10',\n", - " 'id': '0HkLS5MBPJr2-PEwhh-V',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '27 juillet 2017',\n", - " 'document_created_at': 1731413832,\n", - " 'document_id': '002e72ed-88d9-4bc5-994e-eda657c8cef4',\n", - " 'document_name': 'relative à des pratiques '\n", - " 'mises en œuvre dans le '\n", - " 'secteur de la distribution '\n", - " 'des produits de grande '\n", - " 'consommation en Outre-mer',\n", - " 'document_part': 2,\n", - " 'id_decision': '17-D-14',\n", - " 'secteur_activite': 'Distribution, '\n", - " 'Outre-Mer'},\n", - " 'object': 'chunk'},\n", - " 'method': 'lexical',\n", - " 'score': 0.045454545454545456},\n", - " {'chunk': {'content': 'relative à des pratiques mises en œuvre dans '\n", - " 'le secteur de la distribution des produits de '\n", - " 'grande consommation en Outre-mer RÉPUBLIQUE '\n", - " 'FRANÇAISE\\n'\n", - " 'Décision n° 17-D-14 du 27 juillet 2017 '\n", - " 'relative à des pratiques mises en œuvre dans '\n", - " 'le secteur de la distribution des produits de '\n", - " 'grande consommation en Outre-mer\\n'\n", - " \"L'Autorité de la concurrence (section V)\\n\"\n", - " 'Vu la décision n° 10-SO-01 du 29 janvier '\n", - " '2010, enregistrée sous le numéro 10/0005 F, '\n", - " \"par laquelle l'Autorité de la concurrence \"\n", - " \"s'est saisie d'office de pratiques mises en \"\n", - " 'œuvre dans le secteur de la distribution de '\n", - " 'produits de grande consommation dans les '\n", - " \"départements d'outre-mer ;\\n\"\n", - " 'Vu la décision n° 14-SO-06 du 14 octobre '\n", - " '2014, enregistrée sous le numéro 14/0078 F, '\n", - " \"par laquelle l'Autorité a étendu sa saisine \"\n", - " \"d'office aux pratiques mises en œuvre dans le \"\n", - " 'secteur de la distribution des produits de '\n", - " \"grande consommation dans l'ensemble des \"\n", - " \"collectivités d'outre-mer pour lesquelles \"\n", - " \"l'Autorité est compétente ;\\n\"\n", - " 'Vu la décision du 17 octobre 2014, par '\n", - " 'laquelle la rapporteure générale a joint '\n", - " \"l'instruction de ces deux dossiers ;\\n\"\n", - " 'Vu la décision du 31 mars 2015 par laquelle '\n", - " 'la rapporteure générale adjointe a procédé à '\n", - " \"la disjonction de l'instruction du volet des \"\n", - " 'saisines n° 10/0005 F et 14/0078 F concernant '\n", - " 'les pratiques autres que celles mises en '\n", - " 'œuvre par les sociétés Bolton Solitaire SA, '\n", - " 'Danone SA, Johnson & Johnson Santé et Beauté '\n", - " \"France et Pernod-Ricard et à l'ouverture d'un \"\n", - " 'dossier distinct pour cette affaire sous le '\n", - " 'numéro 15/0029 F ;\\n'\n", - " 'Vu la décision du 23 novembre 2015 par '\n", - " 'laquelle la rapporteure générale adjointe de '\n", - " \"l'Autorité de la concurrence a procédé à la \"\n", - " \"disjonction de l'instruction du dossier \"\n", - " '15/0029 F pour la partie relative aux '\n", - " 'pratiques concernant la société Henkel France '\n", - " \"et a procédé à l'ouverture d'un nouveau \"\n", - " 'dossier enregistré sous le n° 15/0107 F ;\\n'\n", - " 'Vu la décision du 17 décembre 2015 par '\n", - " 'laquelle la rapporteure générale adjointe de '\n", - " \"l'Autorité de la concurrence a procédé à la \"\n", - " \"disjonction de l'instruction du dossier \"\n", - " '15/0029 F pour la partie relative aux '\n", - " 'pratiques concernant la société Materne SAS '\n", - " \"et a procédé à l'ouverture d'un nouveau \"\n", - " 'dossier enregistré sous le n° 15/0109 F ;\\n'\n", - " 'Vu la décision du rapporteur général en date '\n", - " 'du 16 mars 2017 prise en application de '\n", - " \"l'article L. 463-3 du code du commerce, qui \"\n", - " \"dispose que l'affaire fera l'objet d'une \"\n", - " \"décision de l'Autorité de la concurrence sans \"\n", - " \"établissement préalable d'un rapport ;\\n\"\n", - " 'Vu le procès-verbal de transaction en date du '\n", - " '3 mai 2017 signé par la rapporteure générale '\n", - " 'adjointe et les sociétés Materne SAS, MOM '\n", - " 'SAS, MBMA SAS et MBMA Holding SAS en '\n", - " 'application des dispositions du III de '\n", - " \"l'article L. 464-2 du code de commerce ;\\n\"\n", - " 'Vu le procès-verbal de transaction en date du '\n", - " '24 mai 2017 signé par la rapporteure générale '\n", - " 'adjointe, la société Sodibel et la société '\n", - " 'Établissements Frédéric Legros en application '\n", - " \"des dispositions du III de l'article L. 464-2 \"\n", - " 'du code de commerce ;\\n'\n", - " 'Vu la décision relative au secret des '\n", - " 'affaires n° 17-DSA-82 du 17 février 2017 ;\\n'\n", - " 'Vu le livre IV du code de commerce modifié et '\n", - " \"notamment l'article L. 420-2-1 ;\\n\"\n", - " 'Vu la loi n° 2012-1270 du 20 novembre 2012 '\n", - " 'relative à la régulation économique outre-mer '\n", - " 'et portant diverses dispositions relatives '\n", - " 'aux outre-mer ;\\n'\n", - " 'Vu les autres pièces du dossier ;\\n'\n", - " 'Vu les observations présentées par les '\n", - " 'sociétés Materne SAS, MBMA SAS et MBMA '\n", - " 'Holding SAS et le commissaire du Gouvernement '\n", - " ';',\n", - " 'id': '1HkLS5MBPJr2-PEwhh-W',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '27 juillet 2017',\n", - " 'document_created_at': 1731413832,\n", - " 'document_id': '002e72ed-88d9-4bc5-994e-eda657c8cef4',\n", - " 'document_name': 'relative à des pratiques '\n", - " 'mises en œuvre dans le '\n", - " 'secteur de la distribution '\n", - " 'des produits de grande '\n", - " 'consommation en Outre-mer',\n", - " 'document_part': 1,\n", - " 'id_decision': '17-D-14',\n", - " 'secteur_activite': 'Distribution, '\n", - " 'Outre-Mer'},\n", - " 'object': 'chunk'},\n", - " 'method': 'lexical',\n", - " 'score': 0.043478260869565216}],\n", - " 'object': 'list'}\n" - ] - } - ], - "source": [ - "data = {\n", - " \"prompt\": \"explique moi l'\\''histoire de la société Mymika\",\n", - " \"collections\": [\n", - " \"0983326d-0527-4f78-afa1-d48896c125c2\",\n", - " ],\n", - " \"k\": 3,\n", - " \"method\": \"lexical\",\n", - " \"score_threshold\": 0\n", - "}\n", - "\n", - "response = session.post(f'{base_url}/search', json=data)\n", - "pprint.pprint(response.json())" - ] - }, - { - "cell_type": "markdown", - "id": "8b35b0e0-cce9-48f6-8f88-8030a7dd2e68", - "metadata": {}, - "source": [ - "# SEMANTIC SEARCH" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "a776192c-535e-4052-b5d4-c134cf6517e1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'data': [{'chunk': {'content': 'relative à la prise de contrôle conjoint de '\n", - " 'la société Mymika par la société Sesyclau aux '\n", - " \"côtés d'ITM Entreprises RÉPUBLIQUE FRANÇAISE\\n\"\n", - " 'Décision n° 14-DCC-113 du 4 août 2014relative '\n", - " 'à la prise de contrôle conjoint de la société '\n", - " \"Mymika par lasociété Sesyclau aux côtés d'ITM \"\n", - " 'Entreprises\\n'\n", - " \"L'Autorité de la concurrence,\\n\"\n", - " 'Vu le dossier de notification adressé complet '\n", - " 'au service des concentrations le 10 juillet '\n", - " '2014, relatif à la prise de contrôle conjoint '\n", - " 'de la société Mymika par la société Sesyclau '\n", - " \"aux côtés d'ITM Entreprises, et matérialisée \"\n", - " \"par une lettre d'intention contresignée en \"\n", - " 'date du 1er juillet 2014 ;\\n'\n", - " 'Vu le livre IV du code de commerce relatif à '\n", - " 'la liberté des prix et de la concurrence, et '\n", - " 'notamment ses articles L. 430-1 à L. 430-7 ;\\n'\n", - " 'Adopte la décision suivante :\\n'\n", - " \"1. L'opération notifiée consiste en la prise \"\n", - " 'de contrôle conjoint de la société Mymika, '\n", - " 'qui exploite un fonds de commerce de '\n", - " 'distribution à dominante alimentaire sous '\n", - " 'enseigne Intermarché dans la ville de '\n", - " 'Châteauroux (36), par la société Sesyclau aux '\n", - " \"côtés d'ITM Entreprises. Sesyclau exploite \"\n", - " 'des points de vente de commerce de détail, '\n", - " 'situées à Saint- Amand-Montrand (18) et Le '\n", - " \"Magny (36) à l'enseigne Intermarché et Netto. \"\n", - " 'Cette opération constitue une concentration '\n", - " \"au sens de l'article L. 430-1 du code de \"\n", - " \"commerce. Compte tenu des chiffres d'affaires \"\n", - " 'réalisés par les entreprises concernées, '\n", - " \"l'opération ne relève pas de la compétence de \"\n", - " \"l'Union européenne. En revanche, les seuils \"\n", - " 'de contrôle relatifs au commerce de détail '\n", - " \"mentionnés au point I de l'article L. 430-2 \"\n", - " 'du code de commerce sont franchis. La '\n", - " 'présente opération est donc soumise aux '\n", - " 'dispositions des articles L. 430-3 et '\n", - " 'suivants du code de commerce relatifs à la '\n", - " 'concentration économique.\\n'\n", - " \"2. Au vu des éléments du dossier, l'opération \"\n", - " \"n'est pas de nature à porter atteinte à la \"\n", - " 'concurrence sur les marchés concernés.\\n'\n", - " 'DECIDE\\n'\n", - " \"Article unique : L'opération notifiée sous le \"\n", - " 'numéro 14-119 est autorisée.\\n'\n", - " 'La vice-présidente,\\n'\n", - " 'Élisabeth Flüry-Hérard\\n'\n", - " '\\uf0d3 Autorité de la concurrence',\n", - " 'id': 'wXkLS5MBPJr2-PEwfx81',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '04 août 2014',\n", - " 'document_created_at': 1731413785,\n", - " 'document_id': '000bc19b-e34d-4e59-b372-7a4a990bf5bd',\n", - " 'document_name': 'relative à la prise de '\n", - " 'contrôle conjoint de la '\n", - " 'société Mymika par la '\n", - " 'société Sesyclau aux côtés '\n", - " \"d'ITM Entreprises\",\n", - " 'document_part': 1,\n", - " 'id_decision': '14-DCC-113',\n", - " 'secteur_activite': 'Distribution'},\n", - " 'object': 'chunk'},\n", - " 'method': 'semantic',\n", - " 'score': 0.047619047619047616},\n", - " {'chunk': {'content': \"16. A l'époque des pratiques, le capital de \"\n", - " 'Materne ainsi que ceux des sociétés Mont '\n", - " 'Blanc SAS (RCS 448 954 362), Materne North '\n", - " 'America corp. et Materne Canada Inc. étaient '\n", - " 'détenus à 100 % par MOM SAS (RCS 492 247 '\n", - " '978), elle-même détenue à 100 % par MBMA SAS '\n", - " '(RCS 528 048 572), elle-même détenue à 99,48 '\n", - " '% par MBMA Holding SAS (RCS 527 552 772), '\n", - " 'holding ultime au niveau de laquelle la '\n", - " 'consolidation du groupe MOM est établie. '\n", - " \"L'ensemble de ces entreprises forment le \"\n", - " 'Groupe MOM.\\n'\n", - " '17. Par acte du 23 mai 2017, MBMA SAS, '\n", - " 'associé unique de la société MOM SAS, a '\n", - " 'procédé à la dissolution anticipée sans '\n", - " 'liquidation de la société MOM SAS. Cette '\n", - " 'dissolution a entraîné la transmission '\n", - " 'universelle du patrimoine de la société MOM '\n", - " 'SAS à la société MBMA SAS.\\n'\n", - " '18. Le Groupe MOM a réalisé un chiffre '\n", - " \"d'affaires mondial consolidé de 403 millions \"\n", - " \"d'euros en 2014 puis de 420 millions d'euros \"\n", - " 'en 2015.\\n'\n", - " '19. Depuis le 5 décembre 2016, le Groupe MOM '\n", - " 'est détenu à hauteur de 65 % par les '\n", - " 'Fromageries Bel, leader mondial du fromage en '\n", - " 'portion, et à hauteur de 35 % par les '\n", - " 'investisseurs et principaux dirigeants et '\n", - " 'cadres du groupe.\\n'\n", - " '20. Le Groupe Bel, actionnaire majoritaire du '\n", - " 'Groupe MOM, a réalisé en 2015 un chiffre '\n", - " \"d'affaires mondial consolidé de 2,9 milliards \"\n", - " \"d'euros et un résultat net part du groupe de \"\n", - " \"184 millions d'euros.\\n\"\n", - " '21. Materne fabrique ou distribue quatre '\n", - " 'marques de desserts de fruits en France : « '\n", - " 'Materne », marque de compotes en coupelles, « '\n", - " \"Pom'Potes », marque de compotes en gourdes \"\n", - " 'pour les enfants, « Ma Pause Fruit », marque '\n", - " 'de gourdes de compotes pour adultes et barres '\n", - " 'de fruits, et « Confipote », marque de '\n", - " 'confitures allégées.\\n'\n", - " '22. Materne fabrique également des produits à '\n", - " 'marque de distributeurs.\\n'\n", - " '23. Mont Blanc SAS est spécialisée dans la '\n", - " \"production de produits lactés qu'elle \"\n", - " 'commercialise sous les marques « Mont Blanc '\n", - " \"», « Mont Blanc Récré », « O'LE » et « Gloria \"\n", - " '». Depuis le 1er janvier 2013, Materne '\n", - " 'distribue les produits fabriqués par Mont '\n", - " 'Blanc SAS.\\n'\n", - " 'b) Sodibel\\n'\n", - " '24. La société Sodibel (ci-après « Sodibel '\n", - " '»), créée en 2007, est une société à '\n", - " 'responsabilité limitée située à La Réunion, '\n", - " 'filiale de la société Établissements Frédéric '\n", - " 'Legros (ci-après « Ets Frédéric Legros »). '\n", - " \"Elle est spécialisée dans l'importation et la \"\n", - " \"distribution de produits d'hygiène beauté, de \"\n", - " \"produits d'entretien, d'alcools et de \"\n", - " 'produits alimentaires à La Réunion.\\n'\n", - " '25. Sodibel distribue les produits Materne '\n", - " 'aux différents magasins du Groupe Vindemia '\n", - " '(Casino), tels Jumbo et Score, Carrefour, '\n", - " 'Leader Price, Géant Chatoire et Système U.\\n'\n", - " \"26. Au cours de l'instruction, Sodibel a \"\n", - " 'déclaré couvrir 70 % du marché réunionnais en '\n", - " 'volume de produits Materne, les 30 % restant '\n", - " 'étant importés directement de métropole par '\n", - " 'les distributeurs intégrés (cote 67).\\n'\n", - " '27. En 2014, Sodibel a réalisé un chiffre '\n", - " \"d'affaires hors taxes en France de 5,4 \"\n", - " \"millions d'euros dont 4,9 millions d'euros au \"\n", - " 'titre de la vente de marchandises. La même '\n", - " 'année, les achats de produits Materne ont '\n", - " 'représenté 546 581 euros, soit environ 15 % '\n", - " 'du montant total des achats de Sodibel qui '\n", - " \"s'élève à 3,2 millions d'euros.\\n\"\n", - " \"28. En 2015, le chiffre d'affaires total hors \"\n", - " 'taxes de Sodibel a été porté à 6,3 millions '\n", - " \"d'euros dont 17 % pour les produits Materne.\\n\"\n", - " 'B. LES PRATIQUES CONSTATÉES\\n'\n", - " '29. Le 25 janvier 2012, Materne a conclu avec '\n", - " 'Ets Frédéric Legros un contrat de '\n", - " 'distribution exclusive de ses produits sur '\n", - " \"les territoires de l'Ile de la Réunion, de \"\n", - " \"l'Ile de Mayotte et de l'Ile Maurice.\",\n", - " 'id': 'zXkLS5MBPJr2-PEwhh-V',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '27 juillet 2017',\n", - " 'document_created_at': 1731413832,\n", - " 'document_id': '002e72ed-88d9-4bc5-994e-eda657c8cef4',\n", - " 'document_name': 'relative à des pratiques '\n", - " 'mises en œuvre dans le '\n", - " 'secteur de la distribution '\n", - " 'des produits de grande '\n", - " 'consommation en Outre-mer',\n", - " 'document_part': 5,\n", - " 'id_decision': '17-D-14',\n", - " 'secteur_activite': 'Distribution, '\n", - " 'Outre-Mer'},\n", - " 'object': 'chunk'},\n", - " 'method': 'semantic',\n", - " 'score': 0.045454545454545456},\n", - " {'chunk': {'content': 'La rapporteure, la rapporteure générale '\n", - " 'adjointe, le commissaire du Gouvernement et '\n", - " 'les représentants des sociétés Materne SAS, '\n", - " 'MBMA SAS et MBMA Holding SAS, des sociétés '\n", - " 'Établissements Frédéric Legros et Sodibel '\n", - " \"entendus lors de la séance de l'Autorité de \"\n", - " 'la concurrence du 19 juillet 2017 ;\\n'\n", - " 'Adopte la décision suivante :\\n'\n", - " 'Résumé1 :\\n'\n", - " \"Dans la décision ci-après, l'Autorité de la \"\n", - " 'concurrence condamne la société Materne SAS '\n", - " 'en tant que société auteur et les sociétés '\n", - " 'MOM, MBMA SAS et MBMA Holding SAS en qualité '\n", - " \"de sociétés mères de l'auteur, à une sanction \"\n", - " 'de 70 000 euros, pour avoir accordé des '\n", - " \"droits exclusifs d'importation des produits \"\n", - " 'Materne à la société Sodibel, filiale de la '\n", - " 'société Établissements Frédéric Legros, sur '\n", - " 'le territoire de la Réunion et de Mayotte, '\n", - " 'pendant la période de 22 mars 2013 au 5 '\n", - " 'juillet 2016.\\n'\n", - " 'Ces droits ont été maintenus postérieurement '\n", - " \"au 22 mars 2013, en violation de l'article L. \"\n", - " '420-2-1 du code de commerce, inséré par la '\n", - " 'loi n° 2012-1270 du 20 novembre 2012 relative '\n", - " 'à la régulation économique outre-mer, dite '\n", - " 'loi « Lurel », qui prohibe les accords ou '\n", - " 'pratiques concertées ayant pour objet ou pour '\n", - " \"effet d'accorder des droits exclusifs \"\n", - " \"d'importation à une entreprise ou à un groupe \"\n", - " \"d'entreprises dans les collectivités \"\n", - " \"d'outre-mer.\\n\"\n", - " \"L'Autorité de la concurrence condamne \"\n", - " 'également la société Sodibel en tant '\n", - " \"qu'auteur et la société Établissements \"\n", - " 'Frédéric Legros en tant que co-auteur et '\n", - " 'société mère, à une sanction de 30 000 euros, '\n", - " 'pour avoir bénéficié de ces droits exclusifs '\n", - " \"d'importation.\\n\"\n", - " 'Ces sanctions ont été prononcées dans le '\n", - " 'respect des termes de la transaction proposée '\n", - " 'par le rapporteur général, sollicitée et '\n", - " 'acceptée par les parties.\\n'\n", - " '1 Ce résumé a un caractère strictement '\n", - " 'informatif. Seuls font foi les motifs de la '\n", - " 'décision numérotés ci-après.\\n'\n", - " 'SOMMAIRE\\n'\n", - " 'I. RAPPEL DE LA PROCÉDURE '\n", - " '............................................................................................. '\n", - " '5\\n'\n", - " 'II. '\n", - " 'CONSTATATIONS.............................................................................................................. '\n", - " '6\\n'\n", - " 'A. Le secteur et les entreprises concernés '\n", - " '.......................................................... '\n", - " '6\\n'\n", - " '1. Le secteur concerné '\n", - " '.......................................................................................... '\n", - " '6\\n'\n", - " '2. Les entreprises concernées '\n", - " '............................................................................... '\n", - " '7\\n'\n", - " 'a) Materne SAS '\n", - " '.................................................................................................. '\n", - " '7\\n'\n", - " 'b) Sodibel '\n", - " '........................................................................................................... '\n", - " '7\\n'\n", - " 'B. Les pratiques constatées '\n", - " '.................................................................................. '\n", - " '8\\n'\n", - " 'C. Les griefs notifiés '\n", - " '............................................................................................. '\n", - " '8\\n'\n", - " 'III. DISCUSSION '\n", - " '................................................................................................................. '\n", - " '9\\n'\n", - " 'A. Sur la mise en œuvre de la procédure de '\n", - " 'transaction '\n", - " '.................................. 9\\n'\n", - " \"B. Sur l'existence de droits exclusifs \"\n", - " \"d'importation \"\n", - " '.......................................... 9\\n'\n", - " 'C. Sur les sanctions '\n", - " '............................................................................................. '\n", - " '10\\n'\n", - " '1. Sur la gravité des pratiques et le dommage '\n", - " \"à l'économie ............................. \"\n", - " '10\\n'\n", - " 'a) Sur la gravité des pratiques '\n", - " '........................................................................ '\n", - " '10',\n", - " 'id': '0HkLS5MBPJr2-PEwhh-V',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '27 juillet 2017',\n", - " 'document_created_at': 1731413832,\n", - " 'document_id': '002e72ed-88d9-4bc5-994e-eda657c8cef4',\n", - " 'document_name': 'relative à des pratiques '\n", - " 'mises en œuvre dans le '\n", - " 'secteur de la distribution '\n", - " 'des produits de grande '\n", - " 'consommation en Outre-mer',\n", - " 'document_part': 2,\n", - " 'id_decision': '17-D-14',\n", - " 'secteur_activite': 'Distribution, '\n", - " 'Outre-Mer'},\n", - " 'object': 'chunk'},\n", - " 'method': 'semantic',\n", - " 'score': 0.043478260869565216}],\n", - " 'object': 'list'}\n" - ] - } - ], - "source": [ - "data = {\n", - " \"prompt\": \"explique moi l'\\''histoire de la société Mymika\",\n", - " \"collections\": [\n", - " \"0983326d-0527-4f78-afa1-d48896c125c2\"\n", - " ],\n", - " \"k\": 3,\n", - " \"method\": \"semantic\",\n", - " \"score_threshold\": 0\n", - "}\n", - "\n", - "response = session.post(f'{base_url}/search', json=data)\n", - "pprint.pprint(response.json())" - ] - }, - { - "cell_type": "markdown", - "id": "547106bc-a812-4f93-809e-2c3dd1c6b0b5", - "metadata": {}, - "source": [ - "# HYBRID SEARCH" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "5089e8f2-44fc-4d05-802d-026c07267d12", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'data': [{'chunk': {'content': 'relative à la prise de contrôle conjoint de '\n", - " 'la société Mymika par la société Sesyclau aux '\n", - " \"côtés d'ITM Entreprises RÉPUBLIQUE FRANÇAISE\\n\"\n", - " 'Décision n° 14-DCC-113 du 4 août 2014relative '\n", - " 'à la prise de contrôle conjoint de la société '\n", - " \"Mymika par lasociété Sesyclau aux côtés d'ITM \"\n", - " 'Entreprises\\n'\n", - " \"L'Autorité de la concurrence,\\n\"\n", - " 'Vu le dossier de notification adressé complet '\n", - " 'au service des concentrations le 10 juillet '\n", - " '2014, relatif à la prise de contrôle conjoint '\n", - " 'de la société Mymika par la société Sesyclau '\n", - " \"aux côtés d'ITM Entreprises, et matérialisée \"\n", - " \"par une lettre d'intention contresignée en \"\n", - " 'date du 1er juillet 2014 ;\\n'\n", - " 'Vu le livre IV du code de commerce relatif à '\n", - " 'la liberté des prix et de la concurrence, et '\n", - " 'notamment ses articles L. 430-1 à L. 430-7 ;\\n'\n", - " 'Adopte la décision suivante :\\n'\n", - " \"1. L'opération notifiée consiste en la prise \"\n", - " 'de contrôle conjoint de la société Mymika, '\n", - " 'qui exploite un fonds de commerce de '\n", - " 'distribution à dominante alimentaire sous '\n", - " 'enseigne Intermarché dans la ville de '\n", - " 'Châteauroux (36), par la société Sesyclau aux '\n", - " \"côtés d'ITM Entreprises. Sesyclau exploite \"\n", - " 'des points de vente de commerce de détail, '\n", - " 'situées à Saint- Amand-Montrand (18) et Le '\n", - " \"Magny (36) à l'enseigne Intermarché et Netto. \"\n", - " 'Cette opération constitue une concentration '\n", - " \"au sens de l'article L. 430-1 du code de \"\n", - " \"commerce. Compte tenu des chiffres d'affaires \"\n", - " 'réalisés par les entreprises concernées, '\n", - " \"l'opération ne relève pas de la compétence de \"\n", - " \"l'Union européenne. En revanche, les seuils \"\n", - " 'de contrôle relatifs au commerce de détail '\n", - " \"mentionnés au point I de l'article L. 430-2 \"\n", - " 'du code de commerce sont franchis. La '\n", - " 'présente opération est donc soumise aux '\n", - " 'dispositions des articles L. 430-3 et '\n", - " 'suivants du code de commerce relatifs à la '\n", - " 'concentration économique.\\n'\n", - " \"2. Au vu des éléments du dossier, l'opération \"\n", - " \"n'est pas de nature à porter atteinte à la \"\n", - " 'concurrence sur les marchés concernés.\\n'\n", - " 'DECIDE\\n'\n", - " \"Article unique : L'opération notifiée sous le \"\n", - " 'numéro 14-119 est autorisée.\\n'\n", - " 'La vice-présidente,\\n'\n", - " 'Élisabeth Flüry-Hérard\\n'\n", - " '\\uf0d3 Autorité de la concurrence',\n", - " 'id': 'wXkLS5MBPJr2-PEwfx81',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '04 août 2014',\n", - " 'document_created_at': 1731413785,\n", - " 'document_id': '000bc19b-e34d-4e59-b372-7a4a990bf5bd',\n", - " 'document_name': 'relative à la prise de '\n", - " 'contrôle conjoint de la '\n", - " 'société Mymika par la '\n", - " 'société Sesyclau aux côtés '\n", - " \"d'ITM Entreprises\",\n", - " 'document_part': 1,\n", - " 'id_decision': '14-DCC-113',\n", - " 'secteur_activite': 'Distribution'},\n", - " 'object': 'chunk'},\n", - " 'method': 'semantic/lexical',\n", - " 'score': 0.09523809523809523},\n", - " {'chunk': {'content': 'La rapporteure, la rapporteure générale '\n", - " 'adjointe, le commissaire du Gouvernement et '\n", - " 'les représentants des sociétés Materne SAS, '\n", - " 'MBMA SAS et MBMA Holding SAS, des sociétés '\n", - " 'Établissements Frédéric Legros et Sodibel '\n", - " \"entendus lors de la séance de l'Autorité de \"\n", - " 'la concurrence du 19 juillet 2017 ;\\n'\n", - " 'Adopte la décision suivante :\\n'\n", - " 'Résumé1 :\\n'\n", - " \"Dans la décision ci-après, l'Autorité de la \"\n", - " 'concurrence condamne la société Materne SAS '\n", - " 'en tant que société auteur et les sociétés '\n", - " 'MOM, MBMA SAS et MBMA Holding SAS en qualité '\n", - " \"de sociétés mères de l'auteur, à une sanction \"\n", - " 'de 70 000 euros, pour avoir accordé des '\n", - " \"droits exclusifs d'importation des produits \"\n", - " 'Materne à la société Sodibel, filiale de la '\n", - " 'société Établissements Frédéric Legros, sur '\n", - " 'le territoire de la Réunion et de Mayotte, '\n", - " 'pendant la période de 22 mars 2013 au 5 '\n", - " 'juillet 2016.\\n'\n", - " 'Ces droits ont été maintenus postérieurement '\n", - " \"au 22 mars 2013, en violation de l'article L. \"\n", - " '420-2-1 du code de commerce, inséré par la '\n", - " 'loi n° 2012-1270 du 20 novembre 2012 relative '\n", - " 'à la régulation économique outre-mer, dite '\n", - " 'loi « Lurel », qui prohibe les accords ou '\n", - " 'pratiques concertées ayant pour objet ou pour '\n", - " \"effet d'accorder des droits exclusifs \"\n", - " \"d'importation à une entreprise ou à un groupe \"\n", - " \"d'entreprises dans les collectivités \"\n", - " \"d'outre-mer.\\n\"\n", - " \"L'Autorité de la concurrence condamne \"\n", - " 'également la société Sodibel en tant '\n", - " \"qu'auteur et la société Établissements \"\n", - " 'Frédéric Legros en tant que co-auteur et '\n", - " 'société mère, à une sanction de 30 000 euros, '\n", - " 'pour avoir bénéficié de ces droits exclusifs '\n", - " \"d'importation.\\n\"\n", - " 'Ces sanctions ont été prononcées dans le '\n", - " 'respect des termes de la transaction proposée '\n", - " 'par le rapporteur général, sollicitée et '\n", - " 'acceptée par les parties.\\n'\n", - " '1 Ce résumé a un caractère strictement '\n", - " 'informatif. Seuls font foi les motifs de la '\n", - " 'décision numérotés ci-après.\\n'\n", - " 'SOMMAIRE\\n'\n", - " 'I. RAPPEL DE LA PROCÉDURE '\n", - " '............................................................................................. '\n", - " '5\\n'\n", - " 'II. '\n", - " 'CONSTATATIONS.............................................................................................................. '\n", - " '6\\n'\n", - " 'A. Le secteur et les entreprises concernés '\n", - " '.......................................................... '\n", - " '6\\n'\n", - " '1. Le secteur concerné '\n", - " '.......................................................................................... '\n", - " '6\\n'\n", - " '2. Les entreprises concernées '\n", - " '............................................................................... '\n", - " '7\\n'\n", - " 'a) Materne SAS '\n", - " '.................................................................................................. '\n", - " '7\\n'\n", - " 'b) Sodibel '\n", - " '........................................................................................................... '\n", - " '7\\n'\n", - " 'B. Les pratiques constatées '\n", - " '.................................................................................. '\n", - " '8\\n'\n", - " 'C. Les griefs notifiés '\n", - " '............................................................................................. '\n", - " '8\\n'\n", - " 'III. DISCUSSION '\n", - " '................................................................................................................. '\n", - " '9\\n'\n", - " 'A. Sur la mise en œuvre de la procédure de '\n", - " 'transaction '\n", - " '.................................. 9\\n'\n", - " \"B. Sur l'existence de droits exclusifs \"\n", - " \"d'importation \"\n", - " '.......................................... 9\\n'\n", - " 'C. Sur les sanctions '\n", - " '............................................................................................. '\n", - " '10\\n'\n", - " '1. Sur la gravité des pratiques et le dommage '\n", - " \"à l'économie ............................. \"\n", - " '10\\n'\n", - " 'a) Sur la gravité des pratiques '\n", - " '........................................................................ '\n", - " '10',\n", - " 'id': '0HkLS5MBPJr2-PEwhh-V',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '27 juillet 2017',\n", - " 'document_created_at': 1731413832,\n", - " 'document_id': '002e72ed-88d9-4bc5-994e-eda657c8cef4',\n", - " 'document_name': 'relative à des pratiques '\n", - " 'mises en œuvre dans le '\n", - " 'secteur de la distribution '\n", - " 'des produits de grande '\n", - " 'consommation en Outre-mer',\n", - " 'document_part': 2,\n", - " 'id_decision': '17-D-14',\n", - " 'secteur_activite': 'Distribution, '\n", - " 'Outre-Mer'},\n", - " 'object': 'chunk'},\n", - " 'method': 'semantic/lexical',\n", - " 'score': 0.08893280632411067},\n", - " {'chunk': {'content': \"16. A l'époque des pratiques, le capital de \"\n", - " 'Materne ainsi que ceux des sociétés Mont '\n", - " 'Blanc SAS (RCS 448 954 362), Materne North '\n", - " 'America corp. et Materne Canada Inc. étaient '\n", - " 'détenus à 100 % par MOM SAS (RCS 492 247 '\n", - " '978), elle-même détenue à 100 % par MBMA SAS '\n", - " '(RCS 528 048 572), elle-même détenue à 99,48 '\n", - " '% par MBMA Holding SAS (RCS 527 552 772), '\n", - " 'holding ultime au niveau de laquelle la '\n", - " 'consolidation du groupe MOM est établie. '\n", - " \"L'ensemble de ces entreprises forment le \"\n", - " 'Groupe MOM.\\n'\n", - " '17. Par acte du 23 mai 2017, MBMA SAS, '\n", - " 'associé unique de la société MOM SAS, a '\n", - " 'procédé à la dissolution anticipée sans '\n", - " 'liquidation de la société MOM SAS. Cette '\n", - " 'dissolution a entraîné la transmission '\n", - " 'universelle du patrimoine de la société MOM '\n", - " 'SAS à la société MBMA SAS.\\n'\n", - " '18. Le Groupe MOM a réalisé un chiffre '\n", - " \"d'affaires mondial consolidé de 403 millions \"\n", - " \"d'euros en 2014 puis de 420 millions d'euros \"\n", - " 'en 2015.\\n'\n", - " '19. Depuis le 5 décembre 2016, le Groupe MOM '\n", - " 'est détenu à hauteur de 65 % par les '\n", - " 'Fromageries Bel, leader mondial du fromage en '\n", - " 'portion, et à hauteur de 35 % par les '\n", - " 'investisseurs et principaux dirigeants et '\n", - " 'cadres du groupe.\\n'\n", - " '20. Le Groupe Bel, actionnaire majoritaire du '\n", - " 'Groupe MOM, a réalisé en 2015 un chiffre '\n", - " \"d'affaires mondial consolidé de 2,9 milliards \"\n", - " \"d'euros et un résultat net part du groupe de \"\n", - " \"184 millions d'euros.\\n\"\n", - " '21. Materne fabrique ou distribue quatre '\n", - " 'marques de desserts de fruits en France : « '\n", - " 'Materne », marque de compotes en coupelles, « '\n", - " \"Pom'Potes », marque de compotes en gourdes \"\n", - " 'pour les enfants, « Ma Pause Fruit », marque '\n", - " 'de gourdes de compotes pour adultes et barres '\n", - " 'de fruits, et « Confipote », marque de '\n", - " 'confitures allégées.\\n'\n", - " '22. Materne fabrique également des produits à '\n", - " 'marque de distributeurs.\\n'\n", - " '23. Mont Blanc SAS est spécialisée dans la '\n", - " \"production de produits lactés qu'elle \"\n", - " 'commercialise sous les marques « Mont Blanc '\n", - " \"», « Mont Blanc Récré », « O'LE » et « Gloria \"\n", - " '». Depuis le 1er janvier 2013, Materne '\n", - " 'distribue les produits fabriqués par Mont '\n", - " 'Blanc SAS.\\n'\n", - " 'b) Sodibel\\n'\n", - " '24. La société Sodibel (ci-après « Sodibel '\n", - " '»), créée en 2007, est une société à '\n", - " 'responsabilité limitée située à La Réunion, '\n", - " 'filiale de la société Établissements Frédéric '\n", - " 'Legros (ci-après « Ets Frédéric Legros »). '\n", - " \"Elle est spécialisée dans l'importation et la \"\n", - " \"distribution de produits d'hygiène beauté, de \"\n", - " \"produits d'entretien, d'alcools et de \"\n", - " 'produits alimentaires à La Réunion.\\n'\n", - " '25. Sodibel distribue les produits Materne '\n", - " 'aux différents magasins du Groupe Vindemia '\n", - " '(Casino), tels Jumbo et Score, Carrefour, '\n", - " 'Leader Price, Géant Chatoire et Système U.\\n'\n", - " \"26. Au cours de l'instruction, Sodibel a \"\n", - " 'déclaré couvrir 70 % du marché réunionnais en '\n", - " 'volume de produits Materne, les 30 % restant '\n", - " 'étant importés directement de métropole par '\n", - " 'les distributeurs intégrés (cote 67).\\n'\n", - " '27. En 2014, Sodibel a réalisé un chiffre '\n", - " \"d'affaires hors taxes en France de 5,4 \"\n", - " \"millions d'euros dont 4,9 millions d'euros au \"\n", - " 'titre de la vente de marchandises. La même '\n", - " 'année, les achats de produits Materne ont '\n", - " 'représenté 546 581 euros, soit environ 15 % '\n", - " 'du montant total des achats de Sodibel qui '\n", - " \"s'élève à 3,2 millions d'euros.\\n\"\n", - " \"28. En 2015, le chiffre d'affaires total hors \"\n", - " 'taxes de Sodibel a été porté à 6,3 millions '\n", - " \"d'euros dont 17 % pour les produits Materne.\\n\"\n", - " 'B. LES PRATIQUES CONSTATÉES\\n'\n", - " '29. Le 25 janvier 2012, Materne a conclu avec '\n", - " 'Ets Frédéric Legros un contrat de '\n", - " 'distribution exclusive de ses produits sur '\n", - " \"les territoires de l'Ile de la Réunion, de \"\n", - " \"l'Ile de Mayotte et de l'Ile Maurice.\",\n", - " 'id': 'zXkLS5MBPJr2-PEwhh-V',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '27 juillet 2017',\n", - " 'document_created_at': 1731413832,\n", - " 'document_id': '002e72ed-88d9-4bc5-994e-eda657c8cef4',\n", - " 'document_name': 'relative à des pratiques '\n", - " 'mises en œuvre dans le '\n", - " 'secteur de la distribution '\n", - " 'des produits de grande '\n", - " 'consommation en Outre-mer',\n", - " 'document_part': 5,\n", - " 'id_decision': '17-D-14',\n", - " 'secteur_activite': 'Distribution, '\n", - " 'Outre-Mer'},\n", - " 'object': 'chunk'},\n", - " 'method': 'semantic',\n", - " 'score': 0.045454545454545456},\n", - " {'chunk': {'content': 'relative à des pratiques mises en œuvre dans '\n", - " 'le secteur de la distribution des produits de '\n", - " 'grande consommation en Outre-mer RÉPUBLIQUE '\n", - " 'FRANÇAISE\\n'\n", - " 'Décision n° 17-D-14 du 27 juillet 2017 '\n", - " 'relative à des pratiques mises en œuvre dans '\n", - " 'le secteur de la distribution des produits de '\n", - " 'grande consommation en Outre-mer\\n'\n", - " \"L'Autorité de la concurrence (section V)\\n\"\n", - " 'Vu la décision n° 10-SO-01 du 29 janvier '\n", - " '2010, enregistrée sous le numéro 10/0005 F, '\n", - " \"par laquelle l'Autorité de la concurrence \"\n", - " \"s'est saisie d'office de pratiques mises en \"\n", - " 'œuvre dans le secteur de la distribution de '\n", - " 'produits de grande consommation dans les '\n", - " \"départements d'outre-mer ;\\n\"\n", - " 'Vu la décision n° 14-SO-06 du 14 octobre '\n", - " '2014, enregistrée sous le numéro 14/0078 F, '\n", - " \"par laquelle l'Autorité a étendu sa saisine \"\n", - " \"d'office aux pratiques mises en œuvre dans le \"\n", - " 'secteur de la distribution des produits de '\n", - " \"grande consommation dans l'ensemble des \"\n", - " \"collectivités d'outre-mer pour lesquelles \"\n", - " \"l'Autorité est compétente ;\\n\"\n", - " 'Vu la décision du 17 octobre 2014, par '\n", - " 'laquelle la rapporteure générale a joint '\n", - " \"l'instruction de ces deux dossiers ;\\n\"\n", - " 'Vu la décision du 31 mars 2015 par laquelle '\n", - " 'la rapporteure générale adjointe a procédé à '\n", - " \"la disjonction de l'instruction du volet des \"\n", - " 'saisines n° 10/0005 F et 14/0078 F concernant '\n", - " 'les pratiques autres que celles mises en '\n", - " 'œuvre par les sociétés Bolton Solitaire SA, '\n", - " 'Danone SA, Johnson & Johnson Santé et Beauté '\n", - " \"France et Pernod-Ricard et à l'ouverture d'un \"\n", - " 'dossier distinct pour cette affaire sous le '\n", - " 'numéro 15/0029 F ;\\n'\n", - " 'Vu la décision du 23 novembre 2015 par '\n", - " 'laquelle la rapporteure générale adjointe de '\n", - " \"l'Autorité de la concurrence a procédé à la \"\n", - " \"disjonction de l'instruction du dossier \"\n", - " '15/0029 F pour la partie relative aux '\n", - " 'pratiques concernant la société Henkel France '\n", - " \"et a procédé à l'ouverture d'un nouveau \"\n", - " 'dossier enregistré sous le n° 15/0107 F ;\\n'\n", - " 'Vu la décision du 17 décembre 2015 par '\n", - " 'laquelle la rapporteure générale adjointe de '\n", - " \"l'Autorité de la concurrence a procédé à la \"\n", - " \"disjonction de l'instruction du dossier \"\n", - " '15/0029 F pour la partie relative aux '\n", - " 'pratiques concernant la société Materne SAS '\n", - " \"et a procédé à l'ouverture d'un nouveau \"\n", - " 'dossier enregistré sous le n° 15/0109 F ;\\n'\n", - " 'Vu la décision du rapporteur général en date '\n", - " 'du 16 mars 2017 prise en application de '\n", - " \"l'article L. 463-3 du code du commerce, qui \"\n", - " \"dispose que l'affaire fera l'objet d'une \"\n", - " \"décision de l'Autorité de la concurrence sans \"\n", - " \"établissement préalable d'un rapport ;\\n\"\n", - " 'Vu le procès-verbal de transaction en date du '\n", - " '3 mai 2017 signé par la rapporteure générale '\n", - " 'adjointe et les sociétés Materne SAS, MOM '\n", - " 'SAS, MBMA SAS et MBMA Holding SAS en '\n", - " 'application des dispositions du III de '\n", - " \"l'article L. 464-2 du code de commerce ;\\n\"\n", - " 'Vu le procès-verbal de transaction en date du '\n", - " '24 mai 2017 signé par la rapporteure générale '\n", - " 'adjointe, la société Sodibel et la société '\n", - " 'Établissements Frédéric Legros en application '\n", - " \"des dispositions du III de l'article L. 464-2 \"\n", - " 'du code de commerce ;\\n'\n", - " 'Vu la décision relative au secret des '\n", - " 'affaires n° 17-DSA-82 du 17 février 2017 ;\\n'\n", - " 'Vu le livre IV du code de commerce modifié et '\n", - " \"notamment l'article L. 420-2-1 ;\\n\"\n", - " 'Vu la loi n° 2012-1270 du 20 novembre 2012 '\n", - " 'relative à la régulation économique outre-mer '\n", - " 'et portant diverses dispositions relatives '\n", - " 'aux outre-mer ;\\n'\n", - " 'Vu les autres pièces du dossier ;\\n'\n", - " 'Vu les observations présentées par les '\n", - " 'sociétés Materne SAS, MBMA SAS et MBMA '\n", - " 'Holding SAS et le commissaire du Gouvernement '\n", - " ';',\n", - " 'id': '1HkLS5MBPJr2-PEwhh-W',\n", - " 'metadata': {'collection_id': '0983326d-0527-4f78-afa1-d48896c125c2',\n", - " 'date_decision': '27 juillet 2017',\n", - " 'document_created_at': 1731413832,\n", - " 'document_id': '002e72ed-88d9-4bc5-994e-eda657c8cef4',\n", - " 'document_name': 'relative à des pratiques '\n", - " 'mises en œuvre dans le '\n", - " 'secteur de la distribution '\n", - " 'des produits de grande '\n", - " 'consommation en Outre-mer',\n", - " 'document_part': 1,\n", - " 'id_decision': '17-D-14',\n", - " 'secteur_activite': 'Distribution, '\n", - " 'Outre-Mer'},\n", - " 'object': 'chunk'},\n", - " 'method': 'lexical',\n", - " 'score': 0.043478260869565216}],\n", - " 'object': 'list'}\n" - ] - } - ], - "source": [ - "data = {\n", - " \"prompt\": \"explique moi l'\\''histoire de la société Mymika\",\n", - " \"collections\": [\n", - " \"0983326d-0527-4f78-afa1-d48896c125c2\",\n", - " ],\n", - " \"k\": 6,\n", - " \"method\": \"hybrid\",\n", - " \"score_threshold\": 0\n", - "}\n", - "\n", - "response = session.post(f'{base_url}/search', json=data)\n", - "pprint.pprint(response.json())" - ] - }, - { - "cell_type": "markdown", - "id": "f5df26dd-6122-482c-881c-f9ff5bed6660", - "metadata": {}, - "source": [ - "# HYBRID SEARCH WITH INTERNET" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d2d716c0-a8a3-4992-bbe0-c9cf92116286", - "metadata": {}, - "outputs": [], - "source": [ - "data = {\n", - " \"prompt\": \"explique moi l'\\''histoire de la société Mymika\",\n", - " \"collections\": [\n", - " \"0983326d-0527-4f78-afa1-d48896c125c2\",\n", - " \"internet\n", - " ],\n", - " \"k\": 6,\n", - " \"method\": \"hybrid\",\n", - " \"score_threshold\": 0\n", - "}\n", - "\n", - "response = session.post(f'{base_url}/search', json=data)\n", - "pprint.pprint(response.json())" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/tutorials/retrieval_augmented_generation.ipynb b/docs/tutorials/retrieval_augmented_generation.ipynb index 6cf4f0fe..5e940d6e 100644 --- a/docs/tutorials/retrieval_augmented_generation.ipynb +++ b/docs/tutorials/retrieval_augmented_generation.ipynb @@ -28,15 +28,25 @@ }, { "cell_type": "code", - "execution_count": 12, - "id": "97a5a057", + "execution_count": 2, + "id": "af281185", "metadata": {}, "outputs": [], "source": [ - "# OpenAI client configuration\n", + "import os\n", "import requests\n", "from openai import OpenAI\n", - "\n", + "import wget" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "97a5a057", + "metadata": {}, + "outputs": [], + "source": [ + "# OpenAI client configuration\n", "base_url = \"https://albert.api.etalab.gouv.fr/v1\"\n", "api_key = \"YOUR_API_KEY\"\n", "\n", @@ -58,7 +68,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 5, "id": "e80daa99-3416-4b81-a8aa-4fb7427bbe6c", "metadata": { "colab": { @@ -68,28 +78,13 @@ "id": "e80daa99-3416-4b81-a8aa-4fb7427bbe6c", "outputId": "abf16516-2ef9-40c3-dcad-74b6f9aa42e6" }, - "outputs": [ - { - "data": { - "text/plain": [ - "'my_document (1).pdf'" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Download a file\n", - "import wget\n", - "import os\n", - "\n", "file_path = \"my_document.pdf\"\n", "if not os.path.exists(file_path):\n", " doc_url = \"https://www.legifrance.gouv.fr/download/file/rxcTl0H4YnnzLkMLiP4x15qORfLSKk_h8QsSb2xnJ8Y=/JOE_TEXTE\"\n", - "\n", - "wget.download(doc_url, out=file_path)" + " wget.download(doc_url, out=file_path)\n" ] }, { @@ -106,7 +101,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 8, "id": "Q_5YNzmR_JcK", "metadata": { "colab": { @@ -139,7 +134,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 7, "id": "a0f0adf2", "metadata": {}, "outputs": [ @@ -147,7 +142,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Collection ID: a8ce92d1-4b38-48b2-8459-7ebe9ae84c2f\n" + "Collection ID: 6c6dd988-bd3d-4449-bfdd-106cda5d22ad\n" ] } ], @@ -172,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 8, "id": "6852fc7a-0b09-451b-bbc2-939fa96a4d28", "metadata": { "colab": { @@ -186,7 +181,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'id': '4ab9f9d1-fbf7-40a4-81f0-892bab407f1f'}\n" + "\n" ] } ], @@ -209,7 +204,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 9, "id": "bd6d6140-5c91-4c3e-9350-b6c8550ab145", "metadata": { "colab": { @@ -242,23 +237,40 @@ "Maintenant que nous avons notre collection et notre fichier, nous pouvons faire une recherche vectorielle à l'aide du endpoint POST `/v1/search`. Ces résutats de recherche vectorielle seront utilisés pour générer une réponse à l'aide du modèle de langage." ] }, + { + "cell_type": "markdown", + "id": "7244b5de", + "metadata": {}, + "source": [ + "## Les méthodes de recherche\n", + "\n", + "Trois méthodes de recherche sont disponibles :\n", + "- lexicale\n", + "- sémantique (méthode par défault)\n", + "- hybride \n", + "\n", + "### Lexicale\n", + "\n", + "La méthode lexicale est la plus simple. Elle ne fait pas de recherche vectorielle mais se base uniquement sur la similarité lexicale entre la question et le contenu des documents à l'aide de l'algorithme [BM25](https://en.wikipedia.org/wiki/Okapi_BM25).\n" + ] + }, { "cell_type": "code", - "execution_count": 26, - "id": "c2ce73cc", + "execution_count": 14, + "id": "d071a7aa", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Selon les documents, M. Ulrich Tan est le chef du pôle Datamin du département \"Eatalab\".\n" + "Selon les documents, Ulrich Tan est le chef du pôle Datamin du département \"Étalab\".\n" ] } ], "source": [ "prompt = \"Qui est Ulrich Tan ?\"\n", - "data = {\"collections\": [collection_id], \"k\": 6, \"prompt\": prompt}\n", + "data = {\"collections\": [collection_id], \"k\": 6, \"prompt\": prompt, \"method\": \"lexical\"}\n", "response = session.post(url=f\"{base_url}/search\", json=data, headers={\"Authorization\": f\"Bearer {api_key}\"})\n", "\n", "prompt_template = \"Réponds à la question suivante en te basant sur les documents ci-dessous : {prompt}\\n\\nDocuments :\\n\\n{chunks}\"\n", @@ -279,41 +291,99 @@ }, { "cell_type": "markdown", - "id": "e11557a2", + "id": "789180a5", "metadata": {}, "source": [ - "Nous avons récupéré les sources depuis les metadata de chaque chunk. Nous pouvons constater que les chunks proviennent bien du document que nous avons importé.\n" + "## Sémantique (méthode par défaut)\n", + "\n", + "La méthode sémantique se base sur la similarité vectorielle (similarité cosinus) entre la question et la représentation vectorielle des documents." ] }, { "cell_type": "code", - "execution_count": 27, - "id": "35eda4b7", + "execution_count": null, + "id": "30db0c5b", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = \"Qui est Ulrich Tan ?\"\n", + "data = {\"collections\": [collection_id], \"k\": 6, \"prompt\": prompt, \"method\": \"semantic\"}\n", + "response = session.post(url=f\"{base_url}/search\", json=data)\n", + "\n", + "prompt_template = \"Réponds à la question suivante en te basant sur les documents ci-dessous : {prompt}\\n\\nDocuments :\\n\\n{chunks}\"\n", + "chunks = \"\\n\\n\\n\".join([result[\"chunk\"][\"content\"] for result in response.json()[\"data\"]])\n", + "sources = set([result[\"chunk\"][\"metadata\"][\"document_name\"] for result in response.json()[\"data\"]])\n", + "prompt = prompt_template.format(prompt=prompt, chunks=chunks)\n", + "\n", + "response = client.chat.completions.create(\n", + " messages=[{\"role\": \"user\", \"content\": prompt}],\n", + " model=language_model,\n", + " stream=False,\n", + " n=1,\n", + ")\n", + "\n", + "response = response.choices[0].message.content\n", + "print(response)" + ] + }, + { + "cell_type": "markdown", + "id": "70dac78d", + "metadata": {}, + "source": [ + "## Hybride\n", + "\n", + "La méthode hybride est une combinaison de la méthode lexicale et de la méthode vectorielle. Elle se base sur la similarité lexicale entre la question et le contenu des documents mais également sur la similarité vectorielle entre la question et le contenu des documents. Pour plus d'informations voir [cet article](https://weaviate.io/blog/hybrid-search-explained).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "f4a27806", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'my_document.pdf'}\n" + "Selon les documents fournis, Ulrich Tan est chef du pôle Datamin du département \"Étalab\".\n" ] } ], "source": [ - "print(sources)" + "prompt = \"Qui est Ulrich Tan ?\"\n", + "data = {\"collections\": [collection_id], \"k\": 6, \"prompt\": prompt, \"method\": \"hybrid\"}\n", + "response = session.post(url=f\"{base_url}/search\", json=data, headers={\"Authorization\": f\"Bearer {api_key}\"})\n", + "\n", + "prompt_template = \"Réponds à la question suivante en te basant sur les documents ci-dessous : {prompt}\\n\\nDocuments :\\n\\n{chunks}\"\n", + "chunks = \"\\n\\n\\n\".join([result[\"chunk\"][\"content\"] for result in response.json()[\"data\"]])\n", + "sources = set([result[\"chunk\"][\"metadata\"][\"document_name\"] for result in response.json()[\"data\"]])\n", + "prompt = prompt_template.format(prompt=prompt, chunks=chunks)\n", + "\n", + "response = client.chat.completions.create(\n", + " messages=[{\"role\": \"user\", \"content\": prompt}],\n", + " model=language_model,\n", + " stream=False,\n", + " n=1,\n", + ")\n", + "\n", + "response = response.choices[0].message.content\n", + "print(response)" ] }, { "cell_type": "markdown", - "id": "8a122cb8", + "id": "7b100b95", "metadata": {}, "source": [ + "## Recherche sur internet\n", + "\n", "Vous pouvez également faire ajouter une recherche sur internet en spécifiant \"internet\" dans la liste des collections." ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 9, "id": "f374c1ad-b5ec-4870-a11a-953c7d219f94", "metadata": { "colab": { @@ -327,14 +397,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "Ulrich Tan est le responsable de coordonner l'équipe du DataLab et d'accompagner les acteurs publics dans l'identification et la priorisation de cas d'usage d'intelligence artificielle pour leur administration.\n" + "Selon les documents, Ulrich Tan est le chef du DataLab au sein de la Direction interministérielle du numérique (Dinum), où il est responsable de coordonner l'équipe du DataLab et d'accompagner les acteurs publics dans l'identification et la priorisation de cas d'usage d'intelligence artificielle pour leur administration. Il est également considéré comme un \"jeune quadra génie du numérique\" et a été embauché par l'État un an avant pour introduire l'intelligence artificiale à différents étages de l'administration pour la rendre plus efficace et plus rapide, à la fois pour les fonctionnaires et les citoyens.\n" ] } ], "source": [ "prompt = \"Qui est Ulrich Tan ?\"\n", "data = {\"collections\": [\"internet\"], \"k\": 6, \"prompt\": prompt}\n", - "response = session.post(url=f\"{base_url}/search\", json=data, headers={\"Authorization\": f\"Bearer {api_key}\"})\n", + "response = session.post(url=f\"{base_url}/search\", json=data)\n", "\n", "prompt_template = \"Réponds à la question suivante en te basant sur les documents ci-dessous : {prompt}\\n\\nDocuments :\\n\\n{chunks}\"\n", "chunks = \"\\n\\n\\n\".join([result[\"chunk\"][\"content\"] for result in response.json()[\"data\"]])\n", @@ -354,15 +424,15 @@ }, { "cell_type": "markdown", - "id": "650f984e", + "id": "857a0492", "metadata": {}, "source": [ - "Nous pouvons de la même manière récupérer les sources depuis les metadata de chaque chunk et les afficher.\n" + "On peut observer que les sources sont des pages web." ] }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 7, "id": "8f982989", "metadata": {}, "outputs": [ @@ -370,21 +440,23 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'https://www.etalab.gouv.fr/datalab/equipe/'}\n" + "https://www.lefigaro.fr/conjoncture/ulrich-tan-cet-ingenieur-qui-introduit-l-ia-dans-les-administrations-pour-les-rendre-plus-efficaces-20240422\n", + "https://www.etalab.gouv.fr/datalab/equipe/\n" ] } ], "source": [ - "print(sources)" + "for source in sources:\n", + " print(source)\n" ] }, { - "cell_type": "markdown", - "id": "5ad96d70", + "cell_type": "code", + "execution_count": null, + "id": "ce092d68", "metadata": {}, - "source": [ - "Enfin il est possible de combiner une recherche dans les fichiers et une recherche sur internet." - ] + "outputs": [], + "source": [] } ], "metadata": { @@ -406,7 +478,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.1" + "version": "3.12.7" } }, "nbformat": 4,