From 386853bb7876490bc628f386006cca94bc6afb2b Mon Sep 17 00:00:00 2001 From: maxgfr <25312957+maxgfr@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:28:51 +0100 Subject: [PATCH 1/9] fix: remote --- pyproject.toml | 4 +-- srdt_analysis/__main__.py | 7 +++-- srdt_analysis/data.py | 2 +- srdt_analysis/exploit_data.py | 10 +++---- srdt_analysis/llm.py | 52 ++++++++++++++++++++++++----------- srdt_analysis/save.py | 34 ++++++----------------- srdt_analysis/vector.py | 14 ++++++++-- 7 files changed, 68 insertions(+), 55 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fa8af08..5a53c47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,9 +10,9 @@ python = "~3.12" asyncpg = "^0.30.0" black = "^24.10.0" python-dotenv = "^1.0.1" -ollama = "^0.3.3" -flagembedding = "^1.3.2" numpy = "^2.1.3" +httpx = "^0.27.2" +pandas = "^2.2.3" [build-system] requires = ["poetry-core"] diff --git a/srdt_analysis/__main__.py b/srdt_analysis/__main__.py index 681ff95..ef89910 100644 --- a/srdt_analysis/__main__.py +++ b/srdt_analysis/__main__.py @@ -1,11 +1,14 @@ from dotenv import load_dotenv -from .exploit_data import exploit_data +from srdt_analysis.llm import get_llm +from srdt_analysis.exploit_data import exploit_data + load_dotenv() def main(): - exploit_data() + # exploit_data() + get_llm() if __name__ == "__main__": diff --git a/srdt_analysis/data.py b/srdt_analysis/data.py index faaed4c..18ad68b 100644 --- a/srdt_analysis/data.py +++ b/srdt_analysis/data.py @@ -2,7 +2,7 @@ import asyncpg import asyncio from typing import List, Tuple -from .models import Document, DocumentsList +from srdt_analysis.models import Document, DocumentsList async def fetch_articles_code_du_travail( diff --git a/srdt_analysis/exploit_data.py b/srdt_analysis/exploit_data.py index aa1e633..c2f30df 100644 --- a/srdt_analysis/exploit_data.py +++ b/srdt_analysis/exploit_data.py @@ -1,8 +1,8 @@ -from .data import get_data -from .models import DocumentsList -from .llm import get_extended_data -from .save import save_to_csv, process_document -from .vector import generate_vector +from srdt_analysis.data import get_data +from srdt_analysis.models import DocumentsList +from srdt_analysis.llm import get_extended_data +from srdt_analysis.save import save_to_csv, process_document +from srdt_analysis.vector import generate_vector from datetime import datetime diff --git a/srdt_analysis/llm.py b/srdt_analysis/llm.py index 719cfbe..aedbfec 100644 --- a/srdt_analysis/llm.py +++ b/srdt_analysis/llm.py @@ -1,20 +1,40 @@ -import ollama +import httpx +import os def get_extended_data(message: str, is_summary: bool) -> str: - summary_prompt = "Tu es un chatbot expert en droit du travail français. Lis le texte donné et rédige un résumé clair, précis et concis, limité à 4096 tokens maximum. Dans ce résumé, fais ressortir les points clés suivants : Sujets principaux : identifie les droits et obligations des employeurs et des salariés, les conditions de travail, les procédures, etc. Langage clair : simplifie le langage juridique tout en restant précis pour éviter toute confusion. Organisation logique : commence par les informations principales, puis détaille les exceptions ou points secondaires s'ils existent. Neutralité : garde un ton factuel, sans jugement ou interprétation subjective. Longueur : si le texte est long, privilégie les informations essentielles pour respecter la limite de 4096 tokens." - keyword_prompt = "Tu es un chatbot expert en droit du travail français. Ta seule mission est d’extraire une liste de mots-clés à partir du texte fourni. Objectif : Les mots-clés doivent refléter les idées et thèmes principaux pour faciliter la compréhension et la recherche du contenu du texte. Sélection : Extrait uniquement les termes essentiels, comme les droits et devoirs des employeurs et des salariés, les conditions de travail, les procédures, les sanctions, etc. Non-redondance : Évite les répétitions ; chaque mot-clé doit apparaître une seule fois. Clarté et simplicité : Assure-toi que chaque mot-clé est compréhensible et pertinent. Format attendu pour la liste de mots-clés : une liste simple et directe, sans organisation par thèmes, comme dans cet exemple : code du travail, article 12, congés payés, heures supplémentaires, licenciement économique" - response = ollama.chat( - model="mistral-nemo", - messages=[ - { - "role": "system", - "content": summary_prompt if is_summary else keyword_prompt, - }, - { - "role": "user", - "content": message, + summary_prompt = "Tu es un chatbot expert en droit du travail français. Lis le texte donné et rédige un résumé clair, précis et concis, limité à 4096 tokens maximum. Dans ce résumé, fais ressortir les points clés suivants : Sujets principaux : identifie les droits et obligations des employeurs et des salariés, les conditions de travail, les procédures, etc. Langage clair : simplifie le langage juridique tout en restant précis pour éviter toute confusion. Organisation logique : commence par les informations principales, puis détaille les exceptions ou points secondaires s'ils existent. Neutralité : garde un ton factuel, sans jugement ou interprétation subjective. Longueur : si le texte est long, privilégie les informations essentielles pour respecter la limite de 4096 tokens. Ta réponse doit obligatoirement être en français." + keyword_prompt = "Tu es un chatbot expert en droit du travail français. Ta seule mission est d’extraire une liste de mots-clés à partir du texte fourni. Objectif : Les mots-clés doivent refléter les idées et thèmes principaux pour faciliter la compréhension et la recherche du contenu du texte. Sélection : Extrait uniquement les termes essentiels, comme les droits et devoirs des employeurs et des salariés, les conditions de travail, les procédures, les sanctions, etc. Non-redondance : Évite les répétitions ; chaque mot-clé doit apparaître une seule fois. Clarté et simplicité : Assure-toi que chaque mot-clé est compréhensible et pertinent. Format attendu pour la liste de mots-clés : une liste simple et directe, sans organisation par thèmes, comme dans cet exemple : code du travail, article 12, congés payés, heures supplémentaires, licenciement économique. Ta réponse doit obligatoirement être en français." + + api_key = os.getenv("ALBERT_API_KEY") + if not api_key: + raise ValueError("API key for Albert is not set") + + try: + response = httpx.post( + "https://albert.api.etalab.gouv.fr/v1/embeddings", + headers={"Authorization": f"Bearer {api_key}"}, + json={ + "messages": [ + { + "role": "system", + "content": summary_prompt if is_summary else keyword_prompt, + }, + { + "role": "user", + "content": message, + }, + ], + "model": "meta-llama/Meta-Llama-3.1-70B-Instruct", }, - ], - ) - return response["message"]["content"] + ) + response.raise_for_status() + chat_response = response.json()["choices"][0]["message"]["content"] + except httpx.HTTPStatusError as e: + raise RuntimeError( + f"Request failed: {e.response.status_code} - {e.response.text}" + ) + except Exception as e: + raise RuntimeError(f"An error occurred: {str(e)}") + + return chat_response diff --git a/srdt_analysis/save.py b/srdt_analysis/save.py index f0f9576..4312299 100644 --- a/srdt_analysis/save.py +++ b/srdt_analysis/save.py @@ -1,28 +1,10 @@ -import csv +import pandas as pd from typing import List, Dict def save_to_csv(data: List[Dict], filename: str) -> None: - headers = [ - "cdtn_id", - "initial_id", - "title", - "content", - "idcc", - "keywords", - "summary", - "vector_summary", - "vector_keywords", - ] - - with open(f"data/{filename}", "w", newline="", encoding="utf-8") as f: - writer = csv.DictWriter(f, fieldnames=headers) - writer.writeheader() - writer.writerows(data) - - -def remove_newlines(content: str) -> str: - return content.replace("\n", "-") + df = pd.DataFrame(data) + df.to_csv(f"data/{filename}", index=False) def process_document( @@ -40,10 +22,10 @@ def process_document( "cdtn_id": cdtn_id, "initial_id": initial_id, "title": title, - "content": remove_newlines(content), - "keywords": remove_newlines(keywords), - "summary": remove_newlines(summary), - "vector_summary": remove_newlines(str(vector_summary)), - "vector_keywords": remove_newlines(str(vector_keywords)), + "content": content, + "keywords": keywords, + "summary": summary, + "vector_summary": vector_summary, + "vector_keywords": vector_keywords, "idcc": idcc, } diff --git a/srdt_analysis/vector.py b/srdt_analysis/vector.py index fc18acb..91eacd6 100644 --- a/srdt_analysis/vector.py +++ b/srdt_analysis/vector.py @@ -1,7 +1,15 @@ -from FlagEmbedding import BGEM3FlagModel +import os +import httpx def generate_vector(text: str) -> dict: - model = BGEM3FlagModel("BAAI/bge-m3", use_fp16=True) - vector = model.encode(text) + response = httpx.post( + "https://albert.api.etalab.gouv.fr/v1/embeddings", + headers={"Authorization": f"Bearer {os.getenv('ALBERT_API_KEY')}"}, + data={ + "input": text, + "model": "BAAI/bge-m3", + }, + ) + vector = response.json()["data"] return vector From c21b8f2dd7e7fc884c8680b1b5a13ae451ed60ab Mon Sep 17 00:00:00 2001 From: Maxime Golfier <25312957+maxgfr@users.noreply.github.com> Date: Sun, 24 Nov 2024 16:43:34 +0100 Subject: [PATCH 2/9] feat(chunk): ajout de la partie chunkage (#18) * fix: chunk * fix: finish * fix: finish * fix: finish * fix: finish * fix: done --- .gitignore | 2 +- README.md | 4 + pyproject.toml | 7 +- srdt_analysis/__main__.py | 13 +- srdt_analysis/albert.py | 20 +++ srdt_analysis/chunk.py | 31 ++++ srdt_analysis/collections.py | 102 ++++++++++++++ srdt_analysis/constants.py | 6 + srdt_analysis/data.py | 169 +++++++++++----------- srdt_analysis/exploit_data.py | 258 ++++++++++++++-------------------- srdt_analysis/llm.py | 138 +++++++++++++----- srdt_analysis/models.py | 58 +++++++- srdt_analysis/save.py | 66 +++++---- srdt_analysis/vector.py | 26 ++-- 14 files changed, 575 insertions(+), 325 deletions(-) create mode 100644 srdt_analysis/albert.py create mode 100644 srdt_analysis/chunk.py create mode 100644 srdt_analysis/collections.py create mode 100644 srdt_analysis/constants.py diff --git a/.gitignore b/.gitignore index dda8f7c..0f6500b 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,4 @@ dmypy.json local_dump/* # Data -data/*.csv +data/* diff --git a/README.md b/README.md index 91784f7..7988fc5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ poetry shell poetry install poetry run start # or poetry run python -m srdt_analysis +black srdt_analysis +ruff check --fix +# ruff check --select I --fix # to fix import +ruff format ``` ## Statistiques sur les documents diff --git a/pyproject.toml b/pyproject.toml index 5a53c47..1d0358d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,14 @@ readme = "README.md" [tool.poetry.dependencies] python = "~3.12" asyncpg = "^0.30.0" -black = "^24.10.0" python-dotenv = "^1.0.1" -numpy = "^2.1.3" httpx = "^0.27.2" pandas = "^2.2.3" +langchain-text-splitters = "^0.3.2" + +[tool.poetry.dev-dependencies] +black = "^24.10.0" +ruff = "^0.7.4" [build-system] requires = ["poetry-core"] diff --git a/srdt_analysis/__main__.py b/srdt_analysis/__main__.py index ef89910..3a9ad03 100644 --- a/srdt_analysis/__main__.py +++ b/srdt_analysis/__main__.py @@ -1,14 +1,19 @@ from dotenv import load_dotenv -from srdt_analysis.llm import get_llm -from srdt_analysis.exploit_data import exploit_data +from srdt_analysis.collections import Collections +from srdt_analysis.data import get_data +from srdt_analysis.exploit_data import PageInfosExploiter load_dotenv() def main(): - # exploit_data() - get_llm() + data = get_data() + exploiter = PageInfosExploiter() + result = exploiter.process_documents([data[3][0]], "page_infos.csv", "cdtn_page_infos") + collections = Collections() + res = collections.search("droit du travail", [result[1]]) + print(res["data"][0]) if __name__ == "__main__": diff --git a/srdt_analysis/albert.py b/srdt_analysis/albert.py new file mode 100644 index 0000000..0d9aa20 --- /dev/null +++ b/srdt_analysis/albert.py @@ -0,0 +1,20 @@ +import os +from typing import Any, Dict + +import httpx + +from srdt_analysis.constants import ALBERT_ENDPOINT + + +class AlbertBase: + def __init__(self, api_key: str = None): + self.api_key = api_key or os.getenv("ALBERT_API_KEY") + if not self.api_key: + raise ValueError( + "API key must be provided either in constructor or as environment variable" + ) + self.headers = {"Authorization": f"Bearer {self.api_key}"} + + def get_models(self) -> Dict[str, Any]: + response = httpx.get(f"{ALBERT_ENDPOINT}/v1/models", headers=self.headers) + return response.json() \ No newline at end of file diff --git a/srdt_analysis/chunk.py b/srdt_analysis/chunk.py new file mode 100644 index 0000000..5909c40 --- /dev/null +++ b/srdt_analysis/chunk.py @@ -0,0 +1,31 @@ +from typing import List +from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter + +from srdt_analysis.constants import CHUNK_OVERLAP, CHUNK_SIZE +from srdt_analysis.models import SplitDocument + +class Chunker: + def __init__(self): + self._markdown_splitter = MarkdownHeaderTextSplitter([ + ("#", "Header 1"), + ("##", "Header 2"), + ("###", "Header 3"), + ("####", "Header 4"), + ("#####", "Header 5"), + ("######", "Header 6"), + ], strip_headers=False) + self._character_recursive_splitter = RecursiveCharacterTextSplitter( + chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP + ) + + def split_markdown(self, markdown: str) -> List[SplitDocument]: + md_header_splits = self._markdown_splitter.split_text(markdown) + return self._character_recursive_splitter.split_documents(md_header_splits) + + def split_character_recursive(self, content: str) -> List[SplitDocument]: + return self._character_recursive_splitter.split_text(content) + + def split(self, content: str, content_type: str = "markdown"): + if content_type.lower() == "markdown": + return self.split_markdown(content) + raise ValueError(f"Unsupported content type: {content_type}") diff --git a/srdt_analysis/collections.py b/srdt_analysis/collections.py new file mode 100644 index 0000000..e49bba6 --- /dev/null +++ b/srdt_analysis/collections.py @@ -0,0 +1,102 @@ +import json +import os +from typing import Any, Dict, List + +import httpx + +from srdt_analysis.albert import AlbertBase +from srdt_analysis.constants import ALBERT_ENDPOINT, MODEL_VECTORISATION +from srdt_analysis.models import ChunkDataList, DocumentData + +FILE_PATH = "data/content.json" + + +class Collections(AlbertBase): + def _create(self, collection_name: str) -> str: + payload = {"name": collection_name, "model": MODEL_VECTORISATION} + response = httpx.post( + f"{ALBERT_ENDPOINT}/v1/collections", headers=self.headers, json=payload + ) + return response.json()["id"] + + def create(self, collection_name: str) -> str: + collections = self.list() + for collection in collections: + if collection["name"] == collection_name: + self.delete(collection["id"]) + return self._create(collection_name) + + def list(self) -> Dict[str, Any]: + response = httpx.get(f"{ALBERT_ENDPOINT}/v1/collections", headers=self.headers) + return response.json()["data"] + + def delete(self, id_collection: str) -> None: + response = httpx.delete( + f"{ALBERT_ENDPOINT}/v1/collections/{id_collection}", headers=self.headers + ) + response.raise_for_status() + return None + + def delete_all(self, collection_name) -> None: + collections = self.list() + for collection in collections: + if collection["name"] == collection_name: + self.delete(collection["id"]) + return None + + def search( + self, + prompt: str, + id_collections: List[str], + k: int = 5, + score_threshold: float = 0, + ) -> ChunkDataList: + response = httpx.post( + f"{ALBERT_ENDPOINT}/v1/search", + headers=self.headers, + json={ + "prompt": prompt, + "collections": id_collections, + "k": k, + "score_threshold": score_threshold, + }, + ) + return response.json() + + def upload( + self, + data: List[DocumentData], + id_collection: str, + ) -> Dict[str, Any]: + result = [] + for dt in data: + dt: DocumentData + chunks = dt["chunks"] + for chunk in chunks: + result.append( + { + "text": chunk.page_content, + "title": dt["title"], + "metadata": { + "cdtn_id": dt["cdtn_id"], + "idcc": dt["idcc"], + "structure_du_chunk": chunk.metadata, + }, + } + ) + + file_content = json.dumps(result).encode("utf-8") + with open(FILE_PATH, "wb") as f: + f.write(file_content) + + files = { + "file": (os.path.basename(FILE_PATH), open(FILE_PATH, "rb"), "multipart/form-data") + } + + data = {"request": '{"collection": "%s"}' % id_collection} + response = httpx.post( + f"{ALBERT_ENDPOINT}/v1/files", headers=self.headers, files=files, data=data + ) + + response.raise_for_status() + return None \ No newline at end of file diff --git a/srdt_analysis/constants.py b/srdt_analysis/constants.py new file mode 100644 index 0000000..f303ff3 --- /dev/null +++ b/srdt_analysis/constants.py @@ -0,0 +1,6 @@ +ALBERT_ENDPOINT = "https://albert.api.etalab.gouv.fr" +MODEL_VECTORISATION = "BAAI/bge-m3" +# LLM_MODEL = "meta-llama/Meta-Llama-3.1-70B-Instruct" +LLM_MODEL = "AgentPublic/Llama-3.1-8B-Instruct" +CHUNK_SIZE = 5000 +CHUNK_OVERLAP = 500 \ No newline at end of file diff --git a/srdt_analysis/data.py b/srdt_analysis/data.py index 18ad68b..5019de7 100644 --- a/srdt_analysis/data.py +++ b/srdt_analysis/data.py @@ -1,91 +1,88 @@ +import asyncio import os +from typing import Tuple + import asyncpg -import asyncio -from typing import List, Tuple + from srdt_analysis.models import Document, DocumentsList -async def fetch_articles_code_du_travail( - conn: asyncpg.Connection, -) -> DocumentsList: - results = await conn.fetch( - "SELECT * from public.documents WHERE source = 'code_du_travail'" - ) - return [Document.from_record(r) for r in results] - - -async def fetch_fiches_mt( - conn: asyncpg.Connection, -) -> DocumentsList: - result = await conn.fetch( - "SELECT * from public.documents WHERE source = 'page_fiche_ministere_travail'" - ) - return [Document.from_record(r) for r in result] - - -async def fetch_fiches_sp( - conn: asyncpg.Connection, -) -> DocumentsList: - result = await conn.fetch( - "SELECT * from public.documents WHERE source = 'fiches_service_public'" - ) - return [Document.from_record(r) for r in result] - - -async def fetch_page_infos( - conn: asyncpg.Connection, -) -> DocumentsList: - result = await conn.fetch( - "SELECT * from public.documents WHERE source = 'information'" - ) - return [Document.from_record(r) for r in result] - - -async def fetch_page_contribs( - conn: asyncpg.Connection, -) -> DocumentsList: - result = await conn.fetch( - "SELECT * from public.documents WHERE source = 'contributions'" - ) - return [Document.from_record(r) for r in result] - - -async def run() -> Tuple[ - DocumentsList, - DocumentsList, - DocumentsList, - DocumentsList, - DocumentsList, -]: - conn = await asyncpg.connect( - user=os.getenv("POSTGRES_USER"), - password=os.getenv("POSTGRES_PASSWORD"), - database=os.getenv("POSTGRES_DATABASE_NAME"), - host=os.getenv("POSTGRES_DATABASE_URL"), - ) - - result1 = await fetch_articles_code_du_travail(conn) - result2 = await fetch_fiches_mt(conn) - result3 = await fetch_fiches_sp(conn) - result4 = await fetch_page_infos(conn) - result5 = await fetch_page_contribs(conn) - - await conn.close() - - return ( - result1, - result2, - result3, - result4, - result5, - ) - - -def get_data() -> Tuple[ - DocumentsList, - DocumentsList, - DocumentsList, - DocumentsList, - DocumentsList, -]: - return asyncio.run(run()) +class DatabaseManager: + def __init__(self): + self.conn = None + + async def connect(self): + self.conn = await asyncpg.connect( + user=os.getenv("POSTGRES_USER"), + password=os.getenv("POSTGRES_PASSWORD"), + database=os.getenv("POSTGRES_DATABASE_NAME"), + host=os.getenv("POSTGRES_DATABASE_URL"), + ) + + async def close(self): + if self.conn: + await self.conn.close() + + async def fetch_articles_code_du_travail(self) -> DocumentsList: + results = await self.conn.fetch( + "SELECT * from public.documents WHERE source = 'code_du_travail'" + ) + return [Document.from_record(r) for r in results] + + async def fetch_fiches_mt(self) -> DocumentsList: + result = await self.conn.fetch( + "SELECT * from public.documents WHERE source = 'page_fiche_ministere_travail'" + ) + return [Document.from_record(r) for r in result] + + async def fetch_fiches_sp(self) -> DocumentsList: + result = await self.conn.fetch( + "SELECT * from public.documents WHERE source = 'fiches_service_public'" + ) + return [Document.from_record(r) for r in result] + + async def fetch_page_infos(self) -> DocumentsList: + result = await self.conn.fetch( + "SELECT * from public.documents WHERE source = 'information'" + ) + return [Document.from_record(r) for r in result] + + async def fetch_page_contribs(self) -> DocumentsList: + result = await self.conn.fetch( + "SELECT * from public.documents WHERE source = 'contributions'" + ) + return [Document.from_record(r) for r in result] + + async def fetch_all( + self, + ) -> Tuple[ + DocumentsList, + DocumentsList, + DocumentsList, + DocumentsList, + DocumentsList, + ]: + await self.connect() + + result1 = await self.fetch_articles_code_du_travail() + result2 = await self.fetch_fiches_mt() + result3 = await self.fetch_fiches_sp() + result4 = await self.fetch_page_infos() + result5 = await self.fetch_page_contribs() + + await self.close() + + return (result1, result2, result3, result4, result5) + + +def get_data() -> ( + Tuple[ + DocumentsList, + DocumentsList, + DocumentsList, + DocumentsList, + DocumentsList, + ] +): + db = DatabaseManager() + return asyncio.run(db.fetch_all()) diff --git a/srdt_analysis/exploit_data.py b/srdt_analysis/exploit_data.py index c2f30df..e5b6d38 100644 --- a/srdt_analysis/exploit_data.py +++ b/srdt_analysis/exploit_data.py @@ -1,159 +1,111 @@ -from srdt_analysis.data import get_data -from srdt_analysis.models import DocumentsList -from srdt_analysis.llm import get_extended_data -from srdt_analysis.save import save_to_csv, process_document -from srdt_analysis.vector import generate_vector from datetime import datetime +from typing import List +from srdt_analysis.albert import AlbertBase +from srdt_analysis.chunk import Chunker +from srdt_analysis.collections import Collections +from srdt_analysis.llm import LLMProcessor +from srdt_analysis.models import DocumentData, DocumentsList +from srdt_analysis.save import DocumentProcessor +from srdt_analysis.vector import Vector -def exploit_data(): - result = get_data() - # exploit_articles_code_du_travail(result[0]) - exploit_articles_code_du_travail([result[0][0]]) - # exploit_fiches_mt(result[1]) - exploit_fiches_mt([result[1][0]]) - # exploit_fiches_sp(result[2]) - exploit_fiches_sp([result[2][0]]) - # exploit_page_infos(result[3]) - exploit_page_infos([result[3][0]]) - # exploit_page_contribs(result[4]) - exploit_page_contribs([result[4][0]]) - - -def exploit_articles_code_du_travail(data: DocumentsList): - results = [] - print(f"Number of articles to be processed: {len(data)}") - for doc in data: - print(f"[{datetime.now().strftime('%H:%M:%S')}] Processing article: {doc.title}") - summary = get_extended_data(doc.text, True) - keywords = get_extended_data(doc.text, False) - doc_data = process_document( - cdtn_id=doc.cdtn_id, - initial_id=doc.initial_id, - title=doc.title, - content=doc.text, - keywords=keywords, - summary=summary, - vector_summary=generate_vector(summary), - vector_keywords=generate_vector(keywords), - ) - print( - f"[{datetime.now().strftime('%H:%M:%S')}] Article number {data.index(doc) + 1} out of {len(data)} processed" - ) - results.append(doc_data) - save_to_csv(results, "articles_code_du_travail.csv") - return results - - -def exploit_fiches_mt(data: DocumentsList): - results = [] - print(f"Number of articles to be processed: {len(data)}") - for doc in data: - print(f"[{datetime.now().strftime('%H:%M:%S')}] Processing article: {doc.title}") - concatenated_html = "" - sections = doc.document.get("sections", []) - for section in sections: - concatenated_html += section.get("html", "") - summary = get_extended_data(concatenated_html, True) - keywords = get_extended_data(concatenated_html, False) - doc_data = process_document( - cdtn_id=doc.cdtn_id, - initial_id=doc.initial_id, - title=doc.title, - content=concatenated_html, - keywords=keywords, - summary=summary, - vector_summary=generate_vector(summary), - vector_keywords=generate_vector(keywords), - ) - print( - f"[{datetime.now().strftime('%H:%M:%S')}] Article number {data.index(doc) + 1} out of {len(data)} processed" - ) - results.append(doc_data) - save_to_csv(results, "fiches_mt.csv") - return results - - -def exploit_fiches_sp(data: DocumentsList): - results = [] - print(f"Number of articles to be processed: {len(data)}") - for doc in data: - print(f"[{datetime.now().strftime('%H:%M:%S')}] Processing article: {doc.title}") - content = doc.document.get("raw", "") - summary = get_extended_data(content, True) - keywords = get_extended_data(content, False) - doc_data = process_document( - cdtn_id=doc.cdtn_id, - initial_id=doc.initial_id, - title=doc.title, - content=content, - keywords=keywords, - summary=summary, - vector_summary=generate_vector(summary), - vector_keywords=generate_vector(keywords), - ) - print( - f"[{datetime.now().strftime('%H:%M:%S')}] Article number {data.index(doc) + 1} out of {len(data)} processed" + +class BaseDataExploiter: + def __init__(self): + self.llm_processor = LLMProcessor() + self.vector_processor = Vector() + self.doc_processor = DocumentProcessor() + self.chunker = Chunker() + self.collections = Collections() + self.albert = AlbertBase() + + def process_documents( + self, data: DocumentsList, output_file: str, collection_name: str + ): + results: List[DocumentData] = [] + print(f"Number of articles to be processed: {len(data)}") + + for doc in data: + print( + f"[{datetime.now().strftime('%H:%M:%S')}] Processing article: {doc.title}" + ) + content = self.get_content(doc) + + chunks = self.chunker.split(content) + + summary = self.llm_processor.get_summary(content) + keywords = self.llm_processor.get_keywords(content) + questions = self.llm_processor.get_questions(content) + + + doc_data = self.create_document_data( + doc, content, chunks, keywords, summary, questions + ) + results.append(doc_data) + + print( + f"[{datetime.now().strftime('%H:%M:%S')}] Article number {data.index(doc) + 1} out of {len(data)} processed" + ) + + self.doc_processor.save_to_csv(results, output_file) + id = self.collections.create(collection_name) + self.collections.upload(results, id) + return (results, id) + + def create_document_data( + self, doc, content, chunks, keywords, summary, questions + ) -> DocumentData: + base_data = { + "cdtn_id": doc.cdtn_id, + "initial_id": doc.initial_id, + "title": doc.title, + "content": content, + "keywords": keywords, + "summary": summary, + "questions": questions, + "chunks": chunks, + # "vector_chunks": [self.vector_processor.generate(chunk) for chunk in chunks], + # "vector_questions": self.vector_processor.generate(questions), + # "vector_summary": self.vector_processor.generate(summary), + # "vector_keywords": self.vector_processor.generate(keywords), + } + return self.doc_processor.process_document(**base_data) + + +class ArticlesCodeDuTravailExploiter(BaseDataExploiter): + def get_content(self, doc): + return doc.text + + +class FichesMTExploiter(BaseDataExploiter): + def get_content(self, doc): + return "".join( + section.get("html", "") for section in doc.document.get("sections", []) ) - results.append(doc_data) - save_to_csv(results, "fiches_sp.csv") - return results - - -def exploit_page_infos(data: DocumentsList): - results = [] - print(f"Number of articles to be processed: {len(data)}") - for doc in data: - print(f"[{datetime.now().strftime('%H:%M:%S')}] Processing article: {doc.title}") - concatenated_markdown = "" - contents = doc.document.get("contents", []) - for content in contents: - blocks = content.get("blocks", []) - for block in blocks: + + +class FichesSPExploiter(BaseDataExploiter): + def get_content(self, doc): + return doc.document.get("raw", "") + + +class PageInfosExploiter(BaseDataExploiter): + def get_content(self, doc): + markdown = "" + for content in doc.document.get("contents", []): + for block in content.get("blocks", []): if block.get("type") == "markdown": - concatenated_markdown += block.get("markdown", "") - summary = get_extended_data(concatenated_markdown, True) - keywords = get_extended_data(concatenated_markdown, False) - doc_data = process_document( - cdtn_id=doc.cdtn_id, - initial_id=doc.initial_id, - title=doc.title, - content=concatenated_markdown, - keywords=keywords, - summary=summary, - vector_summary=generate_vector(summary), - vector_keywords=generate_vector(keywords), - ) - print( - f"[{datetime.now().strftime('%H:%M:%S')}] Article number {data.index(doc) + 1} out of {len(data)} processed" - ) - results.append(doc_data) - save_to_csv(results, "page_infos.csv") - return results - - -def exploit_page_contribs(data: DocumentsList): - results = [] - print(f"Number of articles to be processed: {len(data)}") - for doc in data: - print(f"[{datetime.now().strftime('%H:%M:%S')}] Processing article: {doc.title}") - content = doc.document.get("content", "") - summary = get_extended_data(content, True) - keywords = get_extended_data(content, False) - doc_data = process_document( - cdtn_id=doc.cdtn_id, - initial_id=doc.initial_id, - title=doc.title, - content=content, - keywords=keywords, - summary=summary, - vector_summary=generate_vector(summary), - vector_keywords=generate_vector(keywords), - idcc=doc.document.get("idcc", "0000"), - ) - print( - f"[{datetime.now().strftime('%H:%M:%S')}] Article number {data.index(doc) + 1} out of {len(data)} processed" + markdown += block.get("markdown", "") + return markdown + + +class PageContribsExploiter(BaseDataExploiter): + def get_content(self, doc): + return doc.document.get("content", "") + + def create_document_data(self, doc, content, chunks, keywords, summary, questions): + data = super().create_document_data( + doc, content, chunks, keywords, summary, questions ) - results.append(doc_data) - save_to_csv(results, "page_contribs.csv") - return results + data["idcc"] = doc.document.get("idcc", "0000") + return data diff --git a/srdt_analysis/llm.py b/srdt_analysis/llm.py index aedbfec..6b83afe 100644 --- a/srdt_analysis/llm.py +++ b/srdt_analysis/llm.py @@ -1,40 +1,100 @@ import httpx -import os - - -def get_extended_data(message: str, is_summary: bool) -> str: - summary_prompt = "Tu es un chatbot expert en droit du travail français. Lis le texte donné et rédige un résumé clair, précis et concis, limité à 4096 tokens maximum. Dans ce résumé, fais ressortir les points clés suivants : Sujets principaux : identifie les droits et obligations des employeurs et des salariés, les conditions de travail, les procédures, etc. Langage clair : simplifie le langage juridique tout en restant précis pour éviter toute confusion. Organisation logique : commence par les informations principales, puis détaille les exceptions ou points secondaires s'ils existent. Neutralité : garde un ton factuel, sans jugement ou interprétation subjective. Longueur : si le texte est long, privilégie les informations essentielles pour respecter la limite de 4096 tokens. Ta réponse doit obligatoirement être en français." - keyword_prompt = "Tu es un chatbot expert en droit du travail français. Ta seule mission est d’extraire une liste de mots-clés à partir du texte fourni. Objectif : Les mots-clés doivent refléter les idées et thèmes principaux pour faciliter la compréhension et la recherche du contenu du texte. Sélection : Extrait uniquement les termes essentiels, comme les droits et devoirs des employeurs et des salariés, les conditions de travail, les procédures, les sanctions, etc. Non-redondance : Évite les répétitions ; chaque mot-clé doit apparaître une seule fois. Clarté et simplicité : Assure-toi que chaque mot-clé est compréhensible et pertinent. Format attendu pour la liste de mots-clés : une liste simple et directe, sans organisation par thèmes, comme dans cet exemple : code du travail, article 12, congés payés, heures supplémentaires, licenciement économique. Ta réponse doit obligatoirement être en français." - - api_key = os.getenv("ALBERT_API_KEY") - if not api_key: - raise ValueError("API key for Albert is not set") - - try: - response = httpx.post( - "https://albert.api.etalab.gouv.fr/v1/embeddings", - headers={"Authorization": f"Bearer {api_key}"}, - json={ - "messages": [ - { - "role": "system", - "content": summary_prompt if is_summary else keyword_prompt, - }, - { - "role": "user", - "content": message, - }, - ], - "model": "meta-llama/Meta-Llama-3.1-70B-Instruct", - }, - ) - response.raise_for_status() - chat_response = response.json()["choices"][0]["message"]["content"] - except httpx.HTTPStatusError as e: - raise RuntimeError( - f"Request failed: {e.response.status_code} - {e.response.text}" - ) - except Exception as e: - raise RuntimeError(f"An error occurred: {str(e)}") - - return chat_response + +from srdt_analysis.albert import AlbertBase +from srdt_analysis.constants import ALBERT_ENDPOINT, LLM_MODEL + + +class LLMProcessor(AlbertBase): + SUMMARY_PROMPT = """ + Tu es un chatbot expert en droit du travail français. Lis le texte donné et rédige un résumé clair, précis et concis, + limité à 4096 tokens maximum. Dans ce résumé, fais ressortir les points clés suivants : + + Sujets principaux : identifie les droits et obligations des employeurs et des salariés, les conditions de travail, + les procédures, etc. + + Langage clair : simplifie le langage juridique tout en restant précis pour éviter toute confusion. + + Organisation logique : commence par les informations principales, puis détaille les exceptions ou points secondaires + s'ils existent. + + Neutralité : garde un ton factuel, sans jugement ou interprétation subjective. + + Longueur : si le texte est long, privilégie les informations essentielles pour respecter la limite de 4096 tokens. + + Ta réponse doit obligatoirement être en français. + """ + + KEYWORD_PROMPT = """ + Tu es un chatbot expert en droit du travail français. Ta seule mission est d'extraire une liste de mots-clés + à partir du texte fourni. + + Objectif : Les mots-clés doivent refléter les idées et thèmes principaux pour faciliter la compréhension et + la recherche du contenu du texte. + + Sélection : Extrait uniquement les termes essentiels, comme les droits et devoirs des employeurs et des salariés, + les conditions de travail, les procédures, les sanctions, etc. + + Non-redondance : Évite les répétitions ; chaque mot-clé doit apparaître une seule fois. + + Clarté et simplicité : Assure-toi que chaque mot-clé est compréhensible et pertinent. + + Format attendu pour la liste de mots-clés : une liste simple et directe, sans organisation par thèmes, comme dans + cet exemple : code du travail, article 12, congés payés, heures supplémentaires, licenciement économique. + + Ta réponse doit obligatoirement être en français. + """ + + QUESTION_PROMPT = """ + Tu es un chatbot expert en droit du travail français. Lis attentivement le texte fourni et génère une liste de questions pertinentes et variées, limitées à 4096 tokens. Ces questions doivent permettre d'approfondir la compréhension du contenu et de guider une analyse ou une discussion sur le sujet. + + Directives pour la génération des questions : + + Représentation des idées principales : Formule des questions en lien avec les droits et devoirs des employeurs et des salariés, les conditions de travail, les procédures légales, les sanctions, etc. + Variété : Génère des questions ouvertes (ex. : "Quels sont les droits d'un salarié en cas de licenciement économique ?") et fermées (ex. : "Un employeur peut-il refuser une demande de congés payés ?"). + Clarté : Les questions doivent être claires, concises et directement liées au texte fourni, sans ambiguïté. + Priorité au contenu essentiel : Donne la priorité aux informations clés du texte, mais inclue également des questions sur des points secondaires si pertinent. + Neutralité : Rédige des questions neutres, sans parti pris ou interprétation subjective. + Respect de la longueur : Si le texte est très long, concentre-toi sur les parties les plus importantes pour générer des questions dans la limite de 4096 tokens. + Format attendu pour les questions : Une liste numérotée ou à puces, claire et ordonnée. Exemple : + + Quels sont les critères de validité d'un contrat de travail ? + Quelles sont les obligations légales d'un employeur en cas de licenciement ? + Comment sont calculées les heures supplémentaires selon le Code du travail ? + Ta réponse doit obligatoirement être en français. + """ + + def _make_request(self, message: str, system_prompt: str) -> str: + # TODO + return "" + try: + response = httpx.post( + f"{ALBERT_ENDPOINT}/v1/chat/completions", + headers=self.headers, + json={ + "messages": [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": message}, + ], + "model": LLM_MODEL, + }, + ) + response.raise_for_status() + return response.json()["choices"][0]["message"]["content"] + except httpx.HTTPStatusError as e: + raise RuntimeError( + f"Request failed: {e.response.status_code} - {e.response.text}" + ) from e + except Exception as e: + raise RuntimeError(f"An error occurred: {str(e)}") from e + + def get_summary(self, message: str) -> str: + """Génère un résumé du texte fourni.""" + return self._make_request(message, self.SUMMARY_PROMPT) + + def get_keywords(self, message: str) -> str: + """Extrait les mots-clés du texte fourni.""" + return self._make_request(message, self.KEYWORD_PROMPT) + + def get_questions(self, message: str) -> str: + """Génère des questions à partir du texte fourni.""" + return self._make_request(message, self.QUESTION_PROMPT) diff --git a/srdt_analysis/models.py b/srdt_analysis/models.py index 797eff0..a948fd8 100644 --- a/srdt_analysis/models.py +++ b/srdt_analysis/models.py @@ -1,10 +1,34 @@ +import json from dataclasses import dataclass from datetime import datetime -from typing import List, Optional, Dict, Any -import json +from typing import Any, Dict, List, Optional, TypedDict + import asyncpg +@dataclass +class SplitDocument: + page_content: str + metadata: Dict[str, Any] + + +@dataclass +class DocumentData(TypedDict): + cdtn_id: str + initial_id: str + title: str + content: str + keywords: str + summary: str + questions: str + vector_summary: dict + vector_keywords: dict + vector_questions: dict + idcc: str + chunks: List[SplitDocument] + vector_chunks: List[dict] + + @dataclass class Reference: id: str @@ -90,3 +114,33 @@ def from_record(cls, record: asyncpg.Record) -> "Document": DocumentsList = List[Document] + +# Chunk +@dataclass +class ChunkMetadata: + collection_id: str + document_id: str + document_name: str + document_part: int + document_created_at: int + structure_du_chunk: Dict[str, str] + idcc: str + cdtn_id: str + collection: str + +@dataclass +class Chunk: + object: str + id: str + metadata: ChunkMetadata + content: str + +@dataclass +class ChunkDataItem: + score: float + chunk: Chunk + +@dataclass +class ChunkDataList: + object: str + data: List[ChunkDataItem] \ No newline at end of file diff --git a/srdt_analysis/save.py b/srdt_analysis/save.py index 4312299..660aadb 100644 --- a/srdt_analysis/save.py +++ b/srdt_analysis/save.py @@ -1,31 +1,45 @@ +from typing import List import pandas as pd -from typing import List, Dict +from srdt_analysis.models import DocumentData, SplitDocument -def save_to_csv(data: List[Dict], filename: str) -> None: - df = pd.DataFrame(data) - df.to_csv(f"data/{filename}", index=False) +class DocumentProcessor: + def __init__(self, data_folder: str = "data"): + self.data_folder = data_folder -def process_document( - cdtn_id: str, - initial_id: str, - title: str, - content: str, - keywords: str, - summary: str, - vector_summary: dict, - vector_keywords: dict, - idcc="0000", -) -> Dict: - return { - "cdtn_id": cdtn_id, - "initial_id": initial_id, - "title": title, - "content": content, - "keywords": keywords, - "summary": summary, - "vector_summary": vector_summary, - "vector_keywords": vector_keywords, - "idcc": idcc, - } + def save_to_csv(self, data: List[DocumentData], filename: str) -> None: + df = pd.DataFrame(data) + df.to_csv(f"{self.data_folder}/{filename}", index=False) + + def process_document( + self, + cdtn_id: str, + initial_id: str, + title: str, + content: str, + keywords: str, + summary: str, + questions: str, + chunks: List[SplitDocument] = [], + vector_summary: dict = {}, + vector_keywords: dict = {}, + vector_questions: dict = {}, + vector_chunks: List[dict] = [], + idcc: str = "0000", + ) -> DocumentData: + return { + "cdtn_id": cdtn_id, + "initial_id": initial_id, + "title": title, + "content": content, + "keywords": keywords, + "summary": summary, + "questions": questions, + "vector_summary": vector_summary, + "vector_keywords": vector_keywords, + "vector_questions": vector_questions, + "idcc": idcc, + "chunks": chunks, + "vector_chunks": vector_chunks, + } diff --git a/srdt_analysis/vector.py b/srdt_analysis/vector.py index 91eacd6..f2b890d 100644 --- a/srdt_analysis/vector.py +++ b/srdt_analysis/vector.py @@ -1,15 +1,17 @@ -import os import httpx +from srdt_analysis.albert import AlbertBase +from srdt_analysis.constants import ALBERT_ENDPOINT, MODEL_VECTORISATION -def generate_vector(text: str) -> dict: - response = httpx.post( - "https://albert.api.etalab.gouv.fr/v1/embeddings", - headers={"Authorization": f"Bearer {os.getenv('ALBERT_API_KEY')}"}, - data={ - "input": text, - "model": "BAAI/bge-m3", - }, - ) - vector = response.json()["data"] - return vector + +class Vector(AlbertBase): + def generate(self, text: str) -> dict: + response = httpx.post( + f"{ALBERT_ENDPOINT}/v1/embeddings", + headers=self.headers, + json={ + "input": text, + "model": MODEL_VECTORISATION, + }, + ) + return response.json()["data"] From 02fae7a59da0ab2d514ccea7ed1be548b8f3bcba Mon Sep 17 00:00:00 2001 From: maxgfr <25312957+maxgfr@users.noreply.github.com> Date: Sun, 24 Nov 2024 16:47:21 +0100 Subject: [PATCH 3/9] fix: format --- README.md | 2 +- srdt_analysis/__main__.py | 4 +++- srdt_analysis/albert.py | 4 ++-- srdt_analysis/chunk.py | 14 +++++++++++--- srdt_analysis/collections.py | 8 ++++++-- srdt_analysis/constants.py | 2 +- srdt_analysis/data.py | 16 +++++++--------- srdt_analysis/exploit_data.py | 1 - srdt_analysis/models.py | 6 +++++- srdt_analysis/save.py | 1 + 10 files changed, 37 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 7988fc5..2bd5045 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ poetry install poetry run start # or poetry run python -m srdt_analysis black srdt_analysis ruff check --fix -# ruff check --select I --fix # to fix import +ruff check --select I --fix # to fix import ruff format ``` diff --git a/srdt_analysis/__main__.py b/srdt_analysis/__main__.py index 3a9ad03..31bbbec 100644 --- a/srdt_analysis/__main__.py +++ b/srdt_analysis/__main__.py @@ -10,7 +10,9 @@ def main(): data = get_data() exploiter = PageInfosExploiter() - result = exploiter.process_documents([data[3][0]], "page_infos.csv", "cdtn_page_infos") + result = exploiter.process_documents( + [data[3][0]], "page_infos.csv", "cdtn_page_infos" + ) collections = Collections() res = collections.search("droit du travail", [result[1]]) print(res["data"][0]) diff --git a/srdt_analysis/albert.py b/srdt_analysis/albert.py index 0d9aa20..08b2866 100644 --- a/srdt_analysis/albert.py +++ b/srdt_analysis/albert.py @@ -14,7 +14,7 @@ def __init__(self, api_key: str = None): "API key must be provided either in constructor or as environment variable" ) self.headers = {"Authorization": f"Bearer {self.api_key}"} - + def get_models(self) -> Dict[str, Any]: response = httpx.get(f"{ALBERT_ENDPOINT}/v1/models", headers=self.headers) - return response.json() \ No newline at end of file + return response.json() diff --git a/srdt_analysis/chunk.py b/srdt_analysis/chunk.py index 5909c40..8802d3f 100644 --- a/srdt_analysis/chunk.py +++ b/srdt_analysis/chunk.py @@ -1,19 +1,27 @@ from typing import List -from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter + +from langchain_text_splitters import ( + MarkdownHeaderTextSplitter, + RecursiveCharacterTextSplitter, +) from srdt_analysis.constants import CHUNK_OVERLAP, CHUNK_SIZE from srdt_analysis.models import SplitDocument + class Chunker: def __init__(self): - self._markdown_splitter = MarkdownHeaderTextSplitter([ + self._markdown_splitter = MarkdownHeaderTextSplitter( + [ ("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3"), ("####", "Header 4"), ("#####", "Header 5"), ("######", "Header 6"), - ], strip_headers=False) + ], + strip_headers=False, + ) self._character_recursive_splitter = RecursiveCharacterTextSplitter( chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP ) diff --git a/srdt_analysis/collections.py b/srdt_analysis/collections.py index e49bba6..c2d3bb6 100644 --- a/srdt_analysis/collections.py +++ b/srdt_analysis/collections.py @@ -90,7 +90,11 @@ def upload( f.write(file_content) files = { - "file": (os.path.basename(FILE_PATH), open(FILE_PATH, "rb"), "multipart/form-data") + "file": ( + os.path.basename(FILE_PATH), + open(FILE_PATH, "rb"), + "multipart/form-data", + ) } data = {"request": '{"collection": "%s"}' % id_collection} @@ -99,4 +103,4 @@ def upload( ) response.raise_for_status() - return None \ No newline at end of file + return None diff --git a/srdt_analysis/constants.py b/srdt_analysis/constants.py index f303ff3..c4303ba 100644 --- a/srdt_analysis/constants.py +++ b/srdt_analysis/constants.py @@ -3,4 +3,4 @@ # LLM_MODEL = "meta-llama/Meta-Llama-3.1-70B-Instruct" LLM_MODEL = "AgentPublic/Llama-3.1-8B-Instruct" CHUNK_SIZE = 5000 -CHUNK_OVERLAP = 500 \ No newline at end of file +CHUNK_OVERLAP = 500 diff --git a/srdt_analysis/data.py b/srdt_analysis/data.py index 5019de7..9af528e 100644 --- a/srdt_analysis/data.py +++ b/srdt_analysis/data.py @@ -75,14 +75,12 @@ async def fetch_all( return (result1, result2, result3, result4, result5) -def get_data() -> ( - Tuple[ - DocumentsList, - DocumentsList, - DocumentsList, - DocumentsList, - DocumentsList, - ] -): +def get_data() -> Tuple[ + DocumentsList, + DocumentsList, + DocumentsList, + DocumentsList, + DocumentsList, +]: db = DatabaseManager() return asyncio.run(db.fetch_all()) diff --git a/srdt_analysis/exploit_data.py b/srdt_analysis/exploit_data.py index e5b6d38..3209a38 100644 --- a/srdt_analysis/exploit_data.py +++ b/srdt_analysis/exploit_data.py @@ -37,7 +37,6 @@ def process_documents( keywords = self.llm_processor.get_keywords(content) questions = self.llm_processor.get_questions(content) - doc_data = self.create_document_data( doc, content, chunks, keywords, summary, questions ) diff --git a/srdt_analysis/models.py b/srdt_analysis/models.py index a948fd8..dcd610a 100644 --- a/srdt_analysis/models.py +++ b/srdt_analysis/models.py @@ -115,6 +115,7 @@ def from_record(cls, record: asyncpg.Record) -> "Document": DocumentsList = List[Document] + # Chunk @dataclass class ChunkMetadata: @@ -128,6 +129,7 @@ class ChunkMetadata: cdtn_id: str collection: str + @dataclass class Chunk: object: str @@ -135,12 +137,14 @@ class Chunk: metadata: ChunkMetadata content: str + @dataclass class ChunkDataItem: score: float chunk: Chunk + @dataclass class ChunkDataList: object: str - data: List[ChunkDataItem] \ No newline at end of file + data: List[ChunkDataItem] diff --git a/srdt_analysis/save.py b/srdt_analysis/save.py index 660aadb..92fc08f 100644 --- a/srdt_analysis/save.py +++ b/srdt_analysis/save.py @@ -1,4 +1,5 @@ from typing import List + import pandas as pd from srdt_analysis.models import DocumentData, SplitDocument From cf4da4e6d49a403c824b7a0bc6cd15a2793715f3 Mon Sep 17 00:00:00 2001 From: Victor DEGLIAME Date: Mon, 25 Nov 2024 15:16:55 +0100 Subject: [PATCH 4/9] config: Disable some pylint and mypy rules that are not necessarily useful --- pyproject.toml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1d0358d..5b1a667 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,4 +26,20 @@ start = "srdt_analysis.__main__:main" [tool.black] line-length = 90 -include = '\.pyi?$' \ No newline at end of file +include = '\.pyi?$' + +[tool.pylint] +disable = [ + "line-too-long", + "missing-function-docstring", + "missing-module-docstring", + "missing-class-docstring", + "redefined-outer-name", + "protected-access", + "invalid-name", + "logging-fstring-interpolation", + "fixme" +] + +[tool.mypy] +ignore_missing_imports = true From f0103be5e7247f8d98348bdb3a3c3d2befc2e0cd Mon Sep 17 00:00:00 2001 From: maxgfr <25312957+maxgfr@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:40:06 +0100 Subject: [PATCH 5/9] fix: retours --- README.md | 2 - pyproject.toml | 26 +------ srdt_analysis/__main__.py | 11 ++- srdt_analysis/{chunk.py => chunker.py} | 0 srdt_analysis/collections.py | 28 ++++--- srdt_analysis/constants.py | 3 +- .../{exploit_data.py => data_exploiter.py} | 33 +++++---- .../{data.py => database_manager.py} | 16 ++-- .../{save.py => document_processor.py} | 16 ++-- srdt_analysis/{llm.py => llm_processor.py} | 28 +++---- srdt_analysis/models.py | 74 ++++++++++--------- srdt_analysis/vector.py | 17 ----- 12 files changed, 110 insertions(+), 144 deletions(-) rename srdt_analysis/{chunk.py => chunker.py} (100%) rename srdt_analysis/{exploit_data.py => data_exploiter.py} (81%) rename srdt_analysis/{data.py => database_manager.py} (94%) rename srdt_analysis/{save.py => document_processor.py} (67%) rename srdt_analysis/{llm.py => llm_processor.py} (97%) delete mode 100644 srdt_analysis/vector.py diff --git a/README.md b/README.md index 2bd5045..017c808 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,7 @@ poetry shell poetry install poetry run start # or poetry run python -m srdt_analysis -black srdt_analysis ruff check --fix -ruff check --select I --fix # to fix import ruff format ``` diff --git a/pyproject.toml b/pyproject.toml index 5b1a667..8902009 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,9 @@ httpx = "^0.27.2" pandas = "^2.2.3" langchain-text-splitters = "^0.3.2" -[tool.poetry.dev-dependencies] -black = "^24.10.0" -ruff = "^0.7.4" +[tool.poetry.group.dev.dependencies] +pyright = "^1.1.389" +ruff = "^0.8.0" [build-system] requires = ["poetry-core"] @@ -23,23 +23,3 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] start = "srdt_analysis.__main__:main" - -[tool.black] -line-length = 90 -include = '\.pyi?$' - -[tool.pylint] -disable = [ - "line-too-long", - "missing-function-docstring", - "missing-module-docstring", - "missing-class-docstring", - "redefined-outer-name", - "protected-access", - "invalid-name", - "logging-fstring-interpolation", - "fixme" -] - -[tool.mypy] -ignore_missing_imports = true diff --git a/srdt_analysis/__main__.py b/srdt_analysis/__main__.py index 31bbbec..4081340 100644 --- a/srdt_analysis/__main__.py +++ b/srdt_analysis/__main__.py @@ -1,8 +1,8 @@ from dotenv import load_dotenv from srdt_analysis.collections import Collections -from srdt_analysis.data import get_data -from srdt_analysis.exploit_data import PageInfosExploiter +from srdt_analysis.data_exploiter import PageInfosExploiter +from srdt_analysis.database_manager import get_data load_dotenv() @@ -14,8 +14,11 @@ def main(): [data[3][0]], "page_infos.csv", "cdtn_page_infos" ) collections = Collections() - res = collections.search("droit du travail", [result[1]]) - print(res["data"][0]) + res = collections.search( + "combien de jour de congé payé par mois de travail effectif", + [result["documents"]], + ) + print(res) if __name__ == "__main__": diff --git a/srdt_analysis/chunk.py b/srdt_analysis/chunker.py similarity index 100% rename from srdt_analysis/chunk.py rename to srdt_analysis/chunker.py diff --git a/srdt_analysis/collections.py b/srdt_analysis/collections.py index c2d3bb6..2370662 100644 --- a/srdt_analysis/collections.py +++ b/srdt_analysis/collections.py @@ -1,41 +1,38 @@ import json -import os +from io import BytesIO from typing import Any, Dict, List import httpx from srdt_analysis.albert import AlbertBase -from srdt_analysis.constants import ALBERT_ENDPOINT, MODEL_VECTORISATION +from srdt_analysis.constants import ALBERT_ENDPOINT from srdt_analysis.models import ChunkDataList, DocumentData -FILE_PATH = "data/content.json" - class Collections(AlbertBase): - def _create(self, collection_name: str) -> str: - payload = {"name": collection_name, "model": MODEL_VECTORISATION} + def _create(self, collection_name: str, model: str) -> str: + payload = {"name": collection_name, "model": model} response = httpx.post( f"{ALBERT_ENDPOINT}/v1/collections", headers=self.headers, json=payload ) return response.json()["id"] - def create(self, collection_name: str) -> str: + def create(self, collection_name: str, model: str) -> str: collections = self.list() for collection in collections: if collection["name"] == collection_name: self.delete(collection["id"]) - return self._create(collection_name) + return self._create(collection_name, model) def list(self) -> Dict[str, Any]: response = httpx.get(f"{ALBERT_ENDPOINT}/v1/collections", headers=self.headers) return response.json()["data"] - def delete(self, id_collection: str) -> None: + def delete(self, id_collection: str): response = httpx.delete( f"{ALBERT_ENDPOINT}/v1/collections/{id_collection}", headers=self.headers ) response.raise_for_status() - return None def delete_all(self, collection_name) -> None: collections = self.list() @@ -81,18 +78,19 @@ def upload( "cdtn_id": dt["cdtn_id"], "idcc": dt["idcc"], "structure_du_chunk": chunk.metadata, + "url": dt["url"], }, } ) file_content = json.dumps(result).encode("utf-8") - with open(FILE_PATH, "wb") as f: - f.write(file_content) + + file_like_object = BytesIO(file_content) files = { "file": ( - os.path.basename(FILE_PATH), - open(FILE_PATH, "rb"), + "content.json", + file_like_object, "multipart/form-data", ) } @@ -103,4 +101,4 @@ def upload( ) response.raise_for_status() - return None + return response.json() diff --git a/srdt_analysis/constants.py b/srdt_analysis/constants.py index c4303ba..48c8f6b 100644 --- a/srdt_analysis/constants.py +++ b/srdt_analysis/constants.py @@ -1,6 +1,5 @@ ALBERT_ENDPOINT = "https://albert.api.etalab.gouv.fr" MODEL_VECTORISATION = "BAAI/bge-m3" -# LLM_MODEL = "meta-llama/Meta-Llama-3.1-70B-Instruct" -LLM_MODEL = "AgentPublic/Llama-3.1-8B-Instruct" +LLM_MODEL = "meta-llama/Meta-Llama-3.1-70B-Instruct" CHUNK_SIZE = 5000 CHUNK_OVERLAP = 500 diff --git a/srdt_analysis/exploit_data.py b/srdt_analysis/data_exploiter.py similarity index 81% rename from srdt_analysis/exploit_data.py rename to srdt_analysis/data_exploiter.py index 3209a38..e503294 100644 --- a/srdt_analysis/exploit_data.py +++ b/srdt_analysis/data_exploiter.py @@ -1,19 +1,23 @@ from datetime import datetime -from typing import List +from typing import List, TypedDict from srdt_analysis.albert import AlbertBase -from srdt_analysis.chunk import Chunker +from srdt_analysis.chunker import Chunker from srdt_analysis.collections import Collections -from srdt_analysis.llm import LLMProcessor +from srdt_analysis.constants import MODEL_VECTORISATION +from srdt_analysis.document_processor import DocumentProcessor +from srdt_analysis.llm_processor import LLMProcessor from srdt_analysis.models import DocumentData, DocumentsList -from srdt_analysis.save import DocumentProcessor -from srdt_analysis.vector import Vector + + +class ResultProcessDocumentType(TypedDict): + documents: List[DocumentData] + id: int class BaseDataExploiter: def __init__(self): self.llm_processor = LLMProcessor() - self.vector_processor = Vector() self.doc_processor = DocumentProcessor() self.chunker = Chunker() self.collections = Collections() @@ -21,7 +25,7 @@ def __init__(self): def process_documents( self, data: DocumentsList, output_file: str, collection_name: str - ): + ) -> ResultProcessDocumentType: results: List[DocumentData] = [] print(f"Number of articles to be processed: {len(data)}") @@ -47,14 +51,15 @@ def process_documents( ) self.doc_processor.save_to_csv(results, output_file) - id = self.collections.create(collection_name) + id = self.collections.create(collection_name, MODEL_VECTORISATION) self.collections.upload(results, id) - return (results, id) + return {"documents": results, "id": id} def create_document_data( self, doc, content, chunks, keywords, summary, questions ) -> DocumentData: - base_data = { + print(doc) + base_data: DocumentData = { "cdtn_id": doc.cdtn_id, "initial_id": doc.initial_id, "title": doc.title, @@ -62,11 +67,9 @@ def create_document_data( "keywords": keywords, "summary": summary, "questions": questions, - "chunks": chunks, - # "vector_chunks": [self.vector_processor.generate(chunk) for chunk in chunks], - # "vector_questions": self.vector_processor.generate(questions), - # "vector_summary": self.vector_processor.generate(summary), - # "vector_keywords": self.vector_processor.generate(keywords), + "content_chunked": chunks, + "url": doc.url, + "idcc": doc.idcc, } return self.doc_processor.process_document(**base_data) diff --git a/srdt_analysis/data.py b/srdt_analysis/database_manager.py similarity index 94% rename from srdt_analysis/data.py rename to srdt_analysis/database_manager.py index 9af528e..5019de7 100644 --- a/srdt_analysis/data.py +++ b/srdt_analysis/database_manager.py @@ -75,12 +75,14 @@ async def fetch_all( return (result1, result2, result3, result4, result5) -def get_data() -> Tuple[ - DocumentsList, - DocumentsList, - DocumentsList, - DocumentsList, - DocumentsList, -]: +def get_data() -> ( + Tuple[ + DocumentsList, + DocumentsList, + DocumentsList, + DocumentsList, + DocumentsList, + ] +): db = DatabaseManager() return asyncio.run(db.fetch_all()) diff --git a/srdt_analysis/save.py b/srdt_analysis/document_processor.py similarity index 67% rename from srdt_analysis/save.py rename to srdt_analysis/document_processor.py index 92fc08f..99d65b1 100644 --- a/srdt_analysis/save.py +++ b/srdt_analysis/document_processor.py @@ -22,12 +22,9 @@ def process_document( keywords: str, summary: str, questions: str, - chunks: List[SplitDocument] = [], - vector_summary: dict = {}, - vector_keywords: dict = {}, - vector_questions: dict = {}, - vector_chunks: List[dict] = [], - idcc: str = "0000", + content_chunked: List[SplitDocument], + idcc: str, + url: str, ) -> DocumentData: return { "cdtn_id": cdtn_id, @@ -37,10 +34,7 @@ def process_document( "keywords": keywords, "summary": summary, "questions": questions, - "vector_summary": vector_summary, - "vector_keywords": vector_keywords, - "vector_questions": vector_questions, "idcc": idcc, - "chunks": chunks, - "vector_chunks": vector_chunks, + "content_chunked": content_chunked, + "url": url, } diff --git a/srdt_analysis/llm.py b/srdt_analysis/llm_processor.py similarity index 97% rename from srdt_analysis/llm.py rename to srdt_analysis/llm_processor.py index 6b83afe..6ceb67c 100644 --- a/srdt_analysis/llm.py +++ b/srdt_analysis/llm_processor.py @@ -8,39 +8,39 @@ class LLMProcessor(AlbertBase): SUMMARY_PROMPT = """ Tu es un chatbot expert en droit du travail français. Lis le texte donné et rédige un résumé clair, précis et concis, limité à 4096 tokens maximum. Dans ce résumé, fais ressortir les points clés suivants : - + Sujets principaux : identifie les droits et obligations des employeurs et des salariés, les conditions de travail, les procédures, etc. - + Langage clair : simplifie le langage juridique tout en restant précis pour éviter toute confusion. - + Organisation logique : commence par les informations principales, puis détaille les exceptions ou points secondaires s'ils existent. - + Neutralité : garde un ton factuel, sans jugement ou interprétation subjective. - + Longueur : si le texte est long, privilégie les informations essentielles pour respecter la limite de 4096 tokens. - + Ta réponse doit obligatoirement être en français. """ KEYWORD_PROMPT = """ Tu es un chatbot expert en droit du travail français. Ta seule mission est d'extraire une liste de mots-clés à partir du texte fourni. - + Objectif : Les mots-clés doivent refléter les idées et thèmes principaux pour faciliter la compréhension et la recherche du contenu du texte. - + Sélection : Extrait uniquement les termes essentiels, comme les droits et devoirs des employeurs et des salariés, les conditions de travail, les procédures, les sanctions, etc. - + Non-redondance : Évite les répétitions ; chaque mot-clé doit apparaître une seule fois. - + Clarté et simplicité : Assure-toi que chaque mot-clé est compréhensible et pertinent. - + Format attendu pour la liste de mots-clés : une liste simple et directe, sans organisation par thèmes, comme dans cet exemple : code du travail, article 12, congés payés, heures supplémentaires, licenciement économique. - + Ta réponse doit obligatoirement être en français. """ @@ -60,12 +60,14 @@ class LLMProcessor(AlbertBase): Quels sont les critères de validité d'un contrat de travail ? Quelles sont les obligations légales d'un employeur en cas de licenciement ? Comment sont calculées les heures supplémentaires selon le Code du travail ? - Ta réponse doit obligatoirement être en français. + Ta réponse doit obligatoirement être en français. """ def _make_request(self, message: str, system_prompt: str) -> str: # TODO + return "" + try: response = httpx.post( f"{ALBERT_ENDPOINT}/v1/chat/completions", diff --git a/srdt_analysis/models.py b/srdt_analysis/models.py index dcd610a..17a4241 100644 --- a/srdt_analysis/models.py +++ b/srdt_analysis/models.py @@ -5,6 +5,13 @@ import asyncpg +ID = str +HTML = str +PlainText = str +JSONDict = Dict[str, Any] +Timestamp = datetime +URL = str + @dataclass class SplitDocument: @@ -14,40 +21,37 @@ class SplitDocument: @dataclass class DocumentData(TypedDict): - cdtn_id: str - initial_id: str - title: str - content: str - keywords: str - summary: str - questions: str - vector_summary: dict - vector_keywords: dict - vector_questions: dict - idcc: str - chunks: List[SplitDocument] - vector_chunks: List[dict] + cdtn_id: ID + initial_id: ID + title: PlainText + content: PlainText + keywords: PlainText + summary: PlainText + questions: PlainText + idcc: ID + url: URL + content_chunked: List[SplitDocument] @dataclass class Reference: - id: str - cid: str - url: str + id: ID + cid: ID + url: URL slug: str type: str - title: str + title: PlainText @dataclass class Section: - html: str - text: str - title: str + html: HTML + text: PlainText + title: PlainText anchor: str references: List[Reference] - description: str - htmlWithGlossary: str + description: PlainText + htmlWithGlossary: HTML @dataclass @@ -64,18 +68,18 @@ class Content: @dataclass class Document: - cdtn_id: str - initial_id: str - title: str - meta_description: str + cdtn_id: ID + initial_id: ID + title: PlainText + meta_description: PlainText source: str slug: str - text: str - document: Dict[str, Any] + text: PlainText + document: JSONDict is_published: bool is_searchable: bool - created_at: datetime - updated_at: datetime + created_at: Timestamp + updated_at: Timestamp is_available: bool content: Optional[Content] = None @@ -119,14 +123,14 @@ def from_record(cls, record: asyncpg.Record) -> "Document": # Chunk @dataclass class ChunkMetadata: - collection_id: str - document_id: str - document_name: str + collection_id: ID + document_id: ID + document_name: PlainText document_part: int document_created_at: int structure_du_chunk: Dict[str, str] - idcc: str - cdtn_id: str + idcc: ID + cdtn_id: ID collection: str diff --git a/srdt_analysis/vector.py b/srdt_analysis/vector.py deleted file mode 100644 index f2b890d..0000000 --- a/srdt_analysis/vector.py +++ /dev/null @@ -1,17 +0,0 @@ -import httpx - -from srdt_analysis.albert import AlbertBase -from srdt_analysis.constants import ALBERT_ENDPOINT, MODEL_VECTORISATION - - -class Vector(AlbertBase): - def generate(self, text: str) -> dict: - response = httpx.post( - f"{ALBERT_ENDPOINT}/v1/embeddings", - headers=self.headers, - json={ - "input": text, - "model": MODEL_VECTORISATION, - }, - ) - return response.json()["data"] From e12474e4cc807172e06c2fdd0ae5e9806fa079f1 Mon Sep 17 00:00:00 2001 From: maxgfr <25312957+maxgfr@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:01:10 +0100 Subject: [PATCH 6/9] fix: retours --- content.json | 1 + srdt_analysis/__main__.py | 2 +- srdt_analysis/collections.py | 11 ++++------- srdt_analysis/constants.py | 1 + srdt_analysis/data_exploiter.py | 9 +++------ srdt_analysis/document_processor.py | 28 +--------------------------- srdt_analysis/models.py | 2 -- 7 files changed, 11 insertions(+), 43 deletions(-) create mode 100644 content.json diff --git a/content.json b/content.json new file mode 100644 index 0000000..92b8193 --- /dev/null +++ b/content.json @@ -0,0 +1 @@ +[{"text": "Le salari\u00e9 a droit \u00e0 **2.5 jours de cong\u00e9s pay\u00e9s par mois de travail effectif** chez le m\u00eame employeur, quel que soit son contrat de travail (CDI, CDD, contrat d'int\u00e9rim) et qu\u2019il travaille \u00e0 temps plein ou \u00e0 temps partiel. \n**Pour une ann\u00e9e compl\u00e8te de travail**, la dur\u00e9e totale du cong\u00e9 acquis est donc de **30 jours ouvrables** (5 semaines). \nL\u2019ann\u00e9e de r\u00e9f\u00e9rence, qui sert \u00e0 d\u00e9terminer les droits \u00e0 cong\u00e9s pay\u00e9s, est g\u00e9n\u00e9ralement fix\u00e9e du 1er juin de l\u2019ann\u00e9e pr\u00e9c\u00e9dente au 31 mai de l\u2019ann\u00e9e en cours. C\u2019est ce qu\u2019on appelle la p\u00e9riode d\u2019acquisition. \nLa p\u00e9riode de prise des cong\u00e9s d\u00e9signe quant \u00e0 elle la p\u00e9riode au cours de laquelle le salari\u00e9 peut poser ses cong\u00e9s. \nCertaines p\u00e9riodes d\u2019absences sont assimil\u00e9es \u00e0 des p\u00e9riodes de travail (ex : absence li\u00e9e \u00e0 un cong\u00e9 maternit\u00e9 ou paternit\u00e9) et sont prises en compte pour le calcul du nombre de jours de cong\u00e9s pay\u00e9s. \nD\u00e9sormais, l\u2019ensemble des arr\u00eats maladie constituent des p\u00e9riodes assimil\u00e9es a du temps de travail effectif, quelle que soit leur dur\u00e9e. Autrement dit, ces absences doivent donc \u00eatre prises en compte pour calculer les droits \u00e0 cong\u00e9s annuels du salari\u00e9. \nToutefois, **selon le motif de l\u2019arr\u00eat maladie** (professionnel ou non professionnel), les **droits** \u00e0 cong\u00e9s pay\u00e9s annuels **seront calcul\u00e9s diff\u00e9remment**.\nL\u2019arr\u00eat maladie du salari\u00e9, quelle que soit l\u2019origine de cet arr\u00eat (maladie professionnelle ou non professionnelle), ne le prive pas de droits \u00e0 cong\u00e9s. \n- Si **la maladie est d\u2019origine non professionnelle**, le salari\u00e9 acquiert **2 jours** ouvrables de cong\u00e9s **par mois d\u2019absence**, soit 24 jours ouvrables s\u2019il a \u00e9t\u00e9 absent toute la p\u00e9riode d\u2019acquisition. \n**Exemple si le salari\u00e9 a \u00e9t\u00e9 absent 2 mois :** \nP\u00e9riode d\u2019acquisition : 1er juin 2024 au 31 mai 2025 \nAbsence pour maladie non professionnelle du 1er ao\u00fbt au 30 septembre 2024 \n29 jours acquis, ainsi d\u00e9taill\u00e9s : \n\u2192 du 1er juin 2024 au 31 juillet 2024 : 2 x 2.5 jours = 5 jours \n\u2192 du 1er ao\u00fbt 2024 au 30 septembre 2024 (maladie) : 2 x 2 jours = 4 jours \n\u2192 du 1er octobre 2024 au 31 mai 2025 : 8 x 2.5 jours = 20 jours \n- Si **la maladie est d\u2019origine professionnelle** ou si **le salari\u00e9 est arr\u00eat\u00e9 \u00e0 cause d\u2019un accident du travail**, celui-ci acquiert **2.5 jours** ouvrables de cong\u00e9s **par mois d\u2019absence**, dans la limite de 30 jours ouvrables par p\u00e9riode de d\u2019acquisition.\nSi le salari\u00e9 n\u2019a pas pu prendre tout ou partie de ses cong\u00e9s **au cours de la p\u00e9riode de prise de cong\u00e9s en cours au moment de son arr\u00eat de travail**, en raison de sa maladie, professionnelle ou non, il b\u00e9n\u00e9ficie d\u2019un report. \nLe d\u00e9lai de report est de **15 mois maximum** (sauf si un accord d'entreprise ou, \u00e0 d\u00e9faut, une convention ou un accord de branche fixe une dur\u00e9e de report sup\u00e9rieure). \nLes cong\u00e9s pay\u00e9s non pris par le salari\u00e9 \u00e0 l\u2019issue de ce d\u00e9lai de 15 mois seront perdus. \nPour rappel, si la rupture du contrat de travail intervient avant que le salari\u00e9 n\u2019ait pris la totalit\u00e9 de ses droits \u00e0 cong\u00e9s pay\u00e9s, y compris les cong\u00e9s report\u00e9s (\u00e0 la suite d\u2019une d\u00e9mission ou d\u2019un licenciement pour inaptitude par exemple), l'employeur doit lui verser une indemnit\u00e9 compensatrice de cong\u00e9s pay\u00e9s.", "title": "Acquisition de cong\u00e9s pay\u00e9s pendant un arr\u00eat maladie : les nouvelles r\u00e8gles", "metadata": {"cdtn_id": "9cf86bfa1c", "structure_du_chunk": {}, "url": "https://code.travail.gouv.fr/information/acquisition-de-conges-payes-pendant-un-arret-maladie-les-nouvelles-regles"}}, {"text": "### Obligation de l\u2019employeur d\u2019informer le salari\u00e9 \nApr\u00e8s un arr\u00eat de travail pour maladie ou accident, l\u2019employeur doit porter \u00e0 la connaissance du salari\u00e9 les informations suivantes :\n- le **nombre de jours** de cong\u00e9s dont il dispose (soit le nombre de jours acquis),\n- la **date jusqu\u2019\u00e0 laquelle ces jours** de cong\u00e9s **peuvent \u00eatre pris** (soit le d\u00e9lai dont le salari\u00e9 dispose pour les poser). \nCette information doit \u00eatre r\u00e9alis\u00e9e :\n- **par tout moyen qui permet d\u2019assurer sa bonne r\u00e9ception** par le salari\u00e9 (LRAR, lettre remise en propre contre d\u00e9charge, mail ou bulletin de paie),\n- dans le d\u00e9lai d\u2019**un mois qui suit la reprise du travail**,\n- et **apr\u00e8s chaque arr\u00eat**. \nCette information conditionne le point de d\u00e9part du d\u00e9lai de report (hors cas particulier \u00e9nonc\u00e9 ci-dessous).", "title": "Acquisition de cong\u00e9s pay\u00e9s pendant un arr\u00eat maladie : les nouvelles r\u00e8gles", "metadata": {"cdtn_id": "9cf86bfa1c", "structure_du_chunk": {"Header 3": "Obligation de l\u2019employeur d\u2019informer le salari\u00e9"}, "url": "https://code.travail.gouv.fr/information/acquisition-de-conges-payes-pendant-un-arret-maladie-les-nouvelles-regles"}}, {"text": "### Point de d\u00e9part du d\u00e9lai de report des cong\u00e9s \n#### 1. Lorsque le salari\u00e9 reprend son travail \nLorsque le salari\u00e9 reprend son travail, la p\u00e9riode de report d\u00e9bute \u00e0 la **date \u00e0 laquelle le salari\u00e9 re\u00e7oit ces informations**. \n**Exemple :** \n- P\u00e9riodes de prise de cong\u00e9s : fix\u00e9es du 1er mai 2024 au 30 avril 2025 (pour les cong\u00e9s acquis entre le 1er juin 2023 et le 31 mai 2024) et du 1er mai 2025 au 30 avril 2026 (pour les cong\u00e9s acquis entre le 1er juin 2024 et le 31 mai 2025) \n- Salari\u00e9 absent pour maladie non professionnelle du 1er janvier 2025 au 2 avril 2025 \n- Le salari\u00e9 reprend son travail le 2 avril 2025 \n- L\u2019employeur informe le salari\u00e9 le 15 avril 2025 \n**\u2192** Le solde de cong\u00e9s \u00e0 prendre avant la maladie (acquis au cours de la p\u00e9riode d\u2019acquisition du 1er juin 2023 au 31 mai 2024) pourrait \u00eatre report\u00e9 jusqu\u2019au 15 juillet 2026, si le salari\u00e9 est dans l\u2019impossibilit\u00e9 de poser ces cong\u00e9s avant le 30 avril 2025. \nEn revanche, les cong\u00e9s acquis par le salari\u00e9 du 1er juin au 31 mai 2025 (y compris pendant sa maladie) ne font pas l\u2019objet d\u2019un report, dans la mesure o\u00f9 sa reprise du travail intervient avant le d\u00e9but de la p\u00e9riode de prise de ces cong\u00e9s (1er mai 2025 au 30 avril 2026).", "title": "Acquisition de cong\u00e9s pay\u00e9s pendant un arr\u00eat maladie : les nouvelles r\u00e8gles", "metadata": {"cdtn_id": "9cf86bfa1c", "structure_du_chunk": {"Header 3": "Point de d\u00e9part du d\u00e9lai de report des cong\u00e9s", "Header 4": "1. Lorsque le salari\u00e9 reprend son travail"}, "url": "https://code.travail.gouv.fr/information/acquisition-de-conges-payes-pendant-un-arret-maladie-les-nouvelles-regles"}}, {"text": "#### 2. Cas particulier du salari\u00e9 en arr\u00eat maladie depuis plus d\u2019un an \nPour les cong\u00e9s acquis pendant l\u2019absence pour maladie, le d\u00e9lai de report de 15 mois **commence**, non pas \u00e0 la reprise du travail, mais **\u00e0 la fin de la p\u00e9riode d\u2019acquisition** des cong\u00e9s. \nCela concerne les salari\u00e9s en arr\u00eat maladie depuis au moins un an au moment o\u00f9 la p\u00e9riode d\u2019acquisition se termine et uniquement pour les cong\u00e9s acquis au titre de cette m\u00eame p\u00e9riode. \n**Ainsi :** \n**- Si le salari\u00e9 reprend son travail avant l\u2019expiration de ce report** \nSi le salari\u00e9 revient dans l\u2019entreprise apr\u00e8s la fin de la p\u00e9riode d\u2019acquisition, mais avant l\u2019expiration de la p\u00e9riode de report de 15 mois, le point de d\u00e9part de la fraction restante de cette p\u00e9riode de report sera la date \u00e0 laquelle l\u2019employeur lui a donn\u00e9 l\u2019information sur ses droits \u00e0 cong\u00e9s. \n**Exemple :** \n- P\u00e9riode d\u2019acquisition : 1er juin 2024 au 31 mai 2025 \n- Salari\u00e9 absent pour maladie du 1er avril 2024 au 31 juillet 2025 \n- La p\u00e9riode de report court du 31 mai 2025 au 31 ao\u00fbt 2026, car, au 31 mai 2025 (fin de la p\u00e9riode d\u2019acquisition), le salari\u00e9 est toujours en arr\u00eat maladie depuis au moins 1 an. \n**\u2192** La reprise du salari\u00e9 intervenant le 1er ao\u00fbt 2025, la p\u00e9riode de report est suspendue jusqu\u2019\u00e0 ce que le salari\u00e9 ait re\u00e7u les informations sur ses droits \u00e0 cong\u00e9s. Si l\u2019employeur donne ces informations au salari\u00e9 le 7 ao\u00fbt 2025, la p\u00e9riode expire le 7 septembre 2026 (au lieu du 31 ao\u00fbt 2026). \n**- Si le salari\u00e9 ne reprend pas son travail \u00e0 l\u2019issue du d\u00e9lai de report** \n\u00c0 l'issue de ce d\u00e9lai, les cong\u00e9s pay\u00e9s sont perdus, sans que l'employeur n'ait \u00e9t\u00e9 oblig\u00e9 d'en informer le salari\u00e9. \n**Exemple :** \n- P\u00e9riode d\u2019acquisition : 1er juin 2024 au 31 mai 2025 \n- Salari\u00e9 absent pour maladie depuis le 26 avril 2024 \n- La p\u00e9riode de report d\u00e9bute le 31 mai 2025 \n**\u2192** Les droits \u00e0 cong\u00e9s acquis en p\u00e9riode d\u2019arr\u00eat maladie au titre de l\u2019ann\u00e9e 2024-2025 sont perdus si le salari\u00e9 est toujours absent pour maladie \u00e0 la date du 31 ao\u00fbt 2026.", "title": "Acquisition de cong\u00e9s pay\u00e9s pendant un arr\u00eat maladie : les nouvelles r\u00e8gles", "metadata": {"cdtn_id": "9cf86bfa1c", "structure_du_chunk": {"Header 3": "Point de d\u00e9part du d\u00e9lai de report des cong\u00e9s", "Header 4": "2. Cas particulier du salari\u00e9 en arr\u00eat maladie depuis plus d\u2019un an"}, "url": "https://code.travail.gouv.fr/information/acquisition-de-conges-payes-pendant-un-arret-maladie-les-nouvelles-regles"}}] \ No newline at end of file diff --git a/srdt_analysis/__main__.py b/srdt_analysis/__main__.py index 4081340..f38ffad 100644 --- a/srdt_analysis/__main__.py +++ b/srdt_analysis/__main__.py @@ -16,7 +16,7 @@ def main(): collections = Collections() res = collections.search( "combien de jour de congé payé par mois de travail effectif", - [result["documents"]], + [result["id"]], ) print(res) diff --git a/srdt_analysis/collections.py b/srdt_analysis/collections.py index 2370662..fdd7215 100644 --- a/srdt_analysis/collections.py +++ b/srdt_analysis/collections.py @@ -64,11 +64,11 @@ def upload( self, data: List[DocumentData], id_collection: str, - ) -> Dict[str, Any]: + ) -> None: result = [] for dt in data: dt: DocumentData - chunks = dt["chunks"] + chunks = dt["content_chunked"] for chunk in chunks: result.append( { @@ -76,7 +76,6 @@ def upload( "title": dt["title"], "metadata": { "cdtn_id": dt["cdtn_id"], - "idcc": dt["idcc"], "structure_du_chunk": chunk.metadata, "url": dt["url"], }, @@ -85,12 +84,10 @@ def upload( file_content = json.dumps(result).encode("utf-8") - file_like_object = BytesIO(file_content) - files = { "file": ( "content.json", - file_like_object, + BytesIO(file_content), "multipart/form-data", ) } @@ -101,4 +98,4 @@ def upload( ) response.raise_for_status() - return response.json() + return diff --git a/srdt_analysis/constants.py b/srdt_analysis/constants.py index 48c8f6b..3a8bd9c 100644 --- a/srdt_analysis/constants.py +++ b/srdt_analysis/constants.py @@ -3,3 +3,4 @@ LLM_MODEL = "meta-llama/Meta-Llama-3.1-70B-Instruct" CHUNK_SIZE = 5000 CHUNK_OVERLAP = 500 +BASE_URL_CDTN = "https://code.travail.gouv.fr" diff --git a/srdt_analysis/data_exploiter.py b/srdt_analysis/data_exploiter.py index e503294..62e23f6 100644 --- a/srdt_analysis/data_exploiter.py +++ b/srdt_analysis/data_exploiter.py @@ -4,7 +4,7 @@ from srdt_analysis.albert import AlbertBase from srdt_analysis.chunker import Chunker from srdt_analysis.collections import Collections -from srdt_analysis.constants import MODEL_VECTORISATION +from srdt_analysis.constants import BASE_URL_CDTN, MODEL_VECTORISATION from srdt_analysis.document_processor import DocumentProcessor from srdt_analysis.llm_processor import LLMProcessor from srdt_analysis.models import DocumentData, DocumentsList @@ -58,8 +58,7 @@ def process_documents( def create_document_data( self, doc, content, chunks, keywords, summary, questions ) -> DocumentData: - print(doc) - base_data: DocumentData = { + return { "cdtn_id": doc.cdtn_id, "initial_id": doc.initial_id, "title": doc.title, @@ -68,10 +67,8 @@ def create_document_data( "summary": summary, "questions": questions, "content_chunked": chunks, - "url": doc.url, - "idcc": doc.idcc, + "url": BASE_URL_CDTN + "/" + doc.source + "/" + doc.slug, } - return self.doc_processor.process_document(**base_data) class ArticlesCodeDuTravailExploiter(BaseDataExploiter): diff --git a/srdt_analysis/document_processor.py b/srdt_analysis/document_processor.py index 99d65b1..20f2607 100644 --- a/srdt_analysis/document_processor.py +++ b/srdt_analysis/document_processor.py @@ -2,7 +2,7 @@ import pandas as pd -from srdt_analysis.models import DocumentData, SplitDocument +from srdt_analysis.models import DocumentData class DocumentProcessor: @@ -12,29 +12,3 @@ def __init__(self, data_folder: str = "data"): def save_to_csv(self, data: List[DocumentData], filename: str) -> None: df = pd.DataFrame(data) df.to_csv(f"{self.data_folder}/{filename}", index=False) - - def process_document( - self, - cdtn_id: str, - initial_id: str, - title: str, - content: str, - keywords: str, - summary: str, - questions: str, - content_chunked: List[SplitDocument], - idcc: str, - url: str, - ) -> DocumentData: - return { - "cdtn_id": cdtn_id, - "initial_id": initial_id, - "title": title, - "content": content, - "keywords": keywords, - "summary": summary, - "questions": questions, - "idcc": idcc, - "content_chunked": content_chunked, - "url": url, - } diff --git a/srdt_analysis/models.py b/srdt_analysis/models.py index 17a4241..4ca5fba 100644 --- a/srdt_analysis/models.py +++ b/srdt_analysis/models.py @@ -28,7 +28,6 @@ class DocumentData(TypedDict): keywords: PlainText summary: PlainText questions: PlainText - idcc: ID url: URL content_chunked: List[SplitDocument] @@ -129,7 +128,6 @@ class ChunkMetadata: document_part: int document_created_at: int structure_du_chunk: Dict[str, str] - idcc: ID cdtn_id: ID collection: str From 5dac153153fdc381c9c32a955e952b6c9d77ad3f Mon Sep 17 00:00:00 2001 From: maxgfr <25312957+maxgfr@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:01:24 +0100 Subject: [PATCH 7/9] fix: retours --- content.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 content.json diff --git a/content.json b/content.json deleted file mode 100644 index 92b8193..0000000 --- a/content.json +++ /dev/null @@ -1 +0,0 @@ -[{"text": "Le salari\u00e9 a droit \u00e0 **2.5 jours de cong\u00e9s pay\u00e9s par mois de travail effectif** chez le m\u00eame employeur, quel que soit son contrat de travail (CDI, CDD, contrat d'int\u00e9rim) et qu\u2019il travaille \u00e0 temps plein ou \u00e0 temps partiel. \n**Pour une ann\u00e9e compl\u00e8te de travail**, la dur\u00e9e totale du cong\u00e9 acquis est donc de **30 jours ouvrables** (5 semaines). \nL\u2019ann\u00e9e de r\u00e9f\u00e9rence, qui sert \u00e0 d\u00e9terminer les droits \u00e0 cong\u00e9s pay\u00e9s, est g\u00e9n\u00e9ralement fix\u00e9e du 1er juin de l\u2019ann\u00e9e pr\u00e9c\u00e9dente au 31 mai de l\u2019ann\u00e9e en cours. C\u2019est ce qu\u2019on appelle la p\u00e9riode d\u2019acquisition. \nLa p\u00e9riode de prise des cong\u00e9s d\u00e9signe quant \u00e0 elle la p\u00e9riode au cours de laquelle le salari\u00e9 peut poser ses cong\u00e9s. \nCertaines p\u00e9riodes d\u2019absences sont assimil\u00e9es \u00e0 des p\u00e9riodes de travail (ex : absence li\u00e9e \u00e0 un cong\u00e9 maternit\u00e9 ou paternit\u00e9) et sont prises en compte pour le calcul du nombre de jours de cong\u00e9s pay\u00e9s. \nD\u00e9sormais, l\u2019ensemble des arr\u00eats maladie constituent des p\u00e9riodes assimil\u00e9es a du temps de travail effectif, quelle que soit leur dur\u00e9e. Autrement dit, ces absences doivent donc \u00eatre prises en compte pour calculer les droits \u00e0 cong\u00e9s annuels du salari\u00e9. \nToutefois, **selon le motif de l\u2019arr\u00eat maladie** (professionnel ou non professionnel), les **droits** \u00e0 cong\u00e9s pay\u00e9s annuels **seront calcul\u00e9s diff\u00e9remment**.\nL\u2019arr\u00eat maladie du salari\u00e9, quelle que soit l\u2019origine de cet arr\u00eat (maladie professionnelle ou non professionnelle), ne le prive pas de droits \u00e0 cong\u00e9s. \n- Si **la maladie est d\u2019origine non professionnelle**, le salari\u00e9 acquiert **2 jours** ouvrables de cong\u00e9s **par mois d\u2019absence**, soit 24 jours ouvrables s\u2019il a \u00e9t\u00e9 absent toute la p\u00e9riode d\u2019acquisition. \n**Exemple si le salari\u00e9 a \u00e9t\u00e9 absent 2 mois :** \nP\u00e9riode d\u2019acquisition : 1er juin 2024 au 31 mai 2025 \nAbsence pour maladie non professionnelle du 1er ao\u00fbt au 30 septembre 2024 \n29 jours acquis, ainsi d\u00e9taill\u00e9s : \n\u2192 du 1er juin 2024 au 31 juillet 2024 : 2 x 2.5 jours = 5 jours \n\u2192 du 1er ao\u00fbt 2024 au 30 septembre 2024 (maladie) : 2 x 2 jours = 4 jours \n\u2192 du 1er octobre 2024 au 31 mai 2025 : 8 x 2.5 jours = 20 jours \n- Si **la maladie est d\u2019origine professionnelle** ou si **le salari\u00e9 est arr\u00eat\u00e9 \u00e0 cause d\u2019un accident du travail**, celui-ci acquiert **2.5 jours** ouvrables de cong\u00e9s **par mois d\u2019absence**, dans la limite de 30 jours ouvrables par p\u00e9riode de d\u2019acquisition.\nSi le salari\u00e9 n\u2019a pas pu prendre tout ou partie de ses cong\u00e9s **au cours de la p\u00e9riode de prise de cong\u00e9s en cours au moment de son arr\u00eat de travail**, en raison de sa maladie, professionnelle ou non, il b\u00e9n\u00e9ficie d\u2019un report. \nLe d\u00e9lai de report est de **15 mois maximum** (sauf si un accord d'entreprise ou, \u00e0 d\u00e9faut, une convention ou un accord de branche fixe une dur\u00e9e de report sup\u00e9rieure). \nLes cong\u00e9s pay\u00e9s non pris par le salari\u00e9 \u00e0 l\u2019issue de ce d\u00e9lai de 15 mois seront perdus. \nPour rappel, si la rupture du contrat de travail intervient avant que le salari\u00e9 n\u2019ait pris la totalit\u00e9 de ses droits \u00e0 cong\u00e9s pay\u00e9s, y compris les cong\u00e9s report\u00e9s (\u00e0 la suite d\u2019une d\u00e9mission ou d\u2019un licenciement pour inaptitude par exemple), l'employeur doit lui verser une indemnit\u00e9 compensatrice de cong\u00e9s pay\u00e9s.", "title": "Acquisition de cong\u00e9s pay\u00e9s pendant un arr\u00eat maladie : les nouvelles r\u00e8gles", "metadata": {"cdtn_id": "9cf86bfa1c", "structure_du_chunk": {}, "url": "https://code.travail.gouv.fr/information/acquisition-de-conges-payes-pendant-un-arret-maladie-les-nouvelles-regles"}}, {"text": "### Obligation de l\u2019employeur d\u2019informer le salari\u00e9 \nApr\u00e8s un arr\u00eat de travail pour maladie ou accident, l\u2019employeur doit porter \u00e0 la connaissance du salari\u00e9 les informations suivantes :\n- le **nombre de jours** de cong\u00e9s dont il dispose (soit le nombre de jours acquis),\n- la **date jusqu\u2019\u00e0 laquelle ces jours** de cong\u00e9s **peuvent \u00eatre pris** (soit le d\u00e9lai dont le salari\u00e9 dispose pour les poser). \nCette information doit \u00eatre r\u00e9alis\u00e9e :\n- **par tout moyen qui permet d\u2019assurer sa bonne r\u00e9ception** par le salari\u00e9 (LRAR, lettre remise en propre contre d\u00e9charge, mail ou bulletin de paie),\n- dans le d\u00e9lai d\u2019**un mois qui suit la reprise du travail**,\n- et **apr\u00e8s chaque arr\u00eat**. \nCette information conditionne le point de d\u00e9part du d\u00e9lai de report (hors cas particulier \u00e9nonc\u00e9 ci-dessous).", "title": "Acquisition de cong\u00e9s pay\u00e9s pendant un arr\u00eat maladie : les nouvelles r\u00e8gles", "metadata": {"cdtn_id": "9cf86bfa1c", "structure_du_chunk": {"Header 3": "Obligation de l\u2019employeur d\u2019informer le salari\u00e9"}, "url": "https://code.travail.gouv.fr/information/acquisition-de-conges-payes-pendant-un-arret-maladie-les-nouvelles-regles"}}, {"text": "### Point de d\u00e9part du d\u00e9lai de report des cong\u00e9s \n#### 1. Lorsque le salari\u00e9 reprend son travail \nLorsque le salari\u00e9 reprend son travail, la p\u00e9riode de report d\u00e9bute \u00e0 la **date \u00e0 laquelle le salari\u00e9 re\u00e7oit ces informations**. \n**Exemple :** \n- P\u00e9riodes de prise de cong\u00e9s : fix\u00e9es du 1er mai 2024 au 30 avril 2025 (pour les cong\u00e9s acquis entre le 1er juin 2023 et le 31 mai 2024) et du 1er mai 2025 au 30 avril 2026 (pour les cong\u00e9s acquis entre le 1er juin 2024 et le 31 mai 2025) \n- Salari\u00e9 absent pour maladie non professionnelle du 1er janvier 2025 au 2 avril 2025 \n- Le salari\u00e9 reprend son travail le 2 avril 2025 \n- L\u2019employeur informe le salari\u00e9 le 15 avril 2025 \n**\u2192** Le solde de cong\u00e9s \u00e0 prendre avant la maladie (acquis au cours de la p\u00e9riode d\u2019acquisition du 1er juin 2023 au 31 mai 2024) pourrait \u00eatre report\u00e9 jusqu\u2019au 15 juillet 2026, si le salari\u00e9 est dans l\u2019impossibilit\u00e9 de poser ces cong\u00e9s avant le 30 avril 2025. \nEn revanche, les cong\u00e9s acquis par le salari\u00e9 du 1er juin au 31 mai 2025 (y compris pendant sa maladie) ne font pas l\u2019objet d\u2019un report, dans la mesure o\u00f9 sa reprise du travail intervient avant le d\u00e9but de la p\u00e9riode de prise de ces cong\u00e9s (1er mai 2025 au 30 avril 2026).", "title": "Acquisition de cong\u00e9s pay\u00e9s pendant un arr\u00eat maladie : les nouvelles r\u00e8gles", "metadata": {"cdtn_id": "9cf86bfa1c", "structure_du_chunk": {"Header 3": "Point de d\u00e9part du d\u00e9lai de report des cong\u00e9s", "Header 4": "1. Lorsque le salari\u00e9 reprend son travail"}, "url": "https://code.travail.gouv.fr/information/acquisition-de-conges-payes-pendant-un-arret-maladie-les-nouvelles-regles"}}, {"text": "#### 2. Cas particulier du salari\u00e9 en arr\u00eat maladie depuis plus d\u2019un an \nPour les cong\u00e9s acquis pendant l\u2019absence pour maladie, le d\u00e9lai de report de 15 mois **commence**, non pas \u00e0 la reprise du travail, mais **\u00e0 la fin de la p\u00e9riode d\u2019acquisition** des cong\u00e9s. \nCela concerne les salari\u00e9s en arr\u00eat maladie depuis au moins un an au moment o\u00f9 la p\u00e9riode d\u2019acquisition se termine et uniquement pour les cong\u00e9s acquis au titre de cette m\u00eame p\u00e9riode. \n**Ainsi :** \n**- Si le salari\u00e9 reprend son travail avant l\u2019expiration de ce report** \nSi le salari\u00e9 revient dans l\u2019entreprise apr\u00e8s la fin de la p\u00e9riode d\u2019acquisition, mais avant l\u2019expiration de la p\u00e9riode de report de 15 mois, le point de d\u00e9part de la fraction restante de cette p\u00e9riode de report sera la date \u00e0 laquelle l\u2019employeur lui a donn\u00e9 l\u2019information sur ses droits \u00e0 cong\u00e9s. \n**Exemple :** \n- P\u00e9riode d\u2019acquisition : 1er juin 2024 au 31 mai 2025 \n- Salari\u00e9 absent pour maladie du 1er avril 2024 au 31 juillet 2025 \n- La p\u00e9riode de report court du 31 mai 2025 au 31 ao\u00fbt 2026, car, au 31 mai 2025 (fin de la p\u00e9riode d\u2019acquisition), le salari\u00e9 est toujours en arr\u00eat maladie depuis au moins 1 an. \n**\u2192** La reprise du salari\u00e9 intervenant le 1er ao\u00fbt 2025, la p\u00e9riode de report est suspendue jusqu\u2019\u00e0 ce que le salari\u00e9 ait re\u00e7u les informations sur ses droits \u00e0 cong\u00e9s. Si l\u2019employeur donne ces informations au salari\u00e9 le 7 ao\u00fbt 2025, la p\u00e9riode expire le 7 septembre 2026 (au lieu du 31 ao\u00fbt 2026). \n**- Si le salari\u00e9 ne reprend pas son travail \u00e0 l\u2019issue du d\u00e9lai de report** \n\u00c0 l'issue de ce d\u00e9lai, les cong\u00e9s pay\u00e9s sont perdus, sans que l'employeur n'ait \u00e9t\u00e9 oblig\u00e9 d'en informer le salari\u00e9. \n**Exemple :** \n- P\u00e9riode d\u2019acquisition : 1er juin 2024 au 31 mai 2025 \n- Salari\u00e9 absent pour maladie depuis le 26 avril 2024 \n- La p\u00e9riode de report d\u00e9bute le 31 mai 2025 \n**\u2192** Les droits \u00e0 cong\u00e9s acquis en p\u00e9riode d\u2019arr\u00eat maladie au titre de l\u2019ann\u00e9e 2024-2025 sont perdus si le salari\u00e9 est toujours absent pour maladie \u00e0 la date du 31 ao\u00fbt 2026.", "title": "Acquisition de cong\u00e9s pay\u00e9s pendant un arr\u00eat maladie : les nouvelles r\u00e8gles", "metadata": {"cdtn_id": "9cf86bfa1c", "structure_du_chunk": {"Header 3": "Point de d\u00e9part du d\u00e9lai de report des cong\u00e9s", "Header 4": "2. Cas particulier du salari\u00e9 en arr\u00eat maladie depuis plus d\u2019un an"}, "url": "https://code.travail.gouv.fr/information/acquisition-de-conges-payes-pendant-un-arret-maladie-les-nouvelles-regles"}}] \ No newline at end of file From f2bd8a0f9e28c7401ad20d827c9ea600aee6c618 Mon Sep 17 00:00:00 2001 From: maxgfr <25312957+maxgfr@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:09:26 +0100 Subject: [PATCH 8/9] fix: retours --- srdt_analysis/data_exploiter.py | 10 ++-------- srdt_analysis/models.py | 6 ++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/srdt_analysis/data_exploiter.py b/srdt_analysis/data_exploiter.py index 62e23f6..5d884d8 100644 --- a/srdt_analysis/data_exploiter.py +++ b/srdt_analysis/data_exploiter.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import List, TypedDict from srdt_analysis.albert import AlbertBase from srdt_analysis.chunker import Chunker @@ -7,12 +6,7 @@ from srdt_analysis.constants import BASE_URL_CDTN, MODEL_VECTORISATION from srdt_analysis.document_processor import DocumentProcessor from srdt_analysis.llm_processor import LLMProcessor -from srdt_analysis.models import DocumentData, DocumentsList - - -class ResultProcessDocumentType(TypedDict): - documents: List[DocumentData] - id: int +from srdt_analysis.models import DocumentData, DocumentsList, ResultProcessDocumentType class BaseDataExploiter: @@ -26,7 +20,7 @@ def __init__(self): def process_documents( self, data: DocumentsList, output_file: str, collection_name: str ) -> ResultProcessDocumentType: - results: List[DocumentData] = [] + results: list[DocumentData] = [] print(f"Number of articles to be processed: {len(data)}") for doc in data: diff --git a/srdt_analysis/models.py b/srdt_analysis/models.py index 4ca5fba..102b33d 100644 --- a/srdt_analysis/models.py +++ b/srdt_analysis/models.py @@ -32,6 +32,12 @@ class DocumentData(TypedDict): content_chunked: List[SplitDocument] +@dataclass +class ResultProcessDocumentType(TypedDict): + documents: List[DocumentData] + id: int + + @dataclass class Reference: id: ID From eeac84b167f8a74691d1f3db7631296524d9bc8f Mon Sep 17 00:00:00 2001 From: maxgfr <25312957+maxgfr@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:15:17 +0100 Subject: [PATCH 9/9] fix: config --- README.md | 1 + pyproject.toml | 28 ++++++++++++++++++++++++++++ srdt_analysis/__main__.py | 1 + srdt_analysis/albert.py | 4 ++-- srdt_analysis/chunker.py | 6 ++++-- srdt_analysis/collections.py | 11 +++++++---- srdt_analysis/data_exploiter.py | 4 +++- srdt_analysis/database_manager.py | 2 +- srdt_analysis/models.py | 14 +++++++------- 9 files changed, 54 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 017c808..0b4d15e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ poetry install poetry run start # or poetry run python -m srdt_analysis ruff check --fix ruff format +pyright # for type checking ``` ## Statistiques sur les documents diff --git a/pyproject.toml b/pyproject.toml index 8902009..06fe677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,3 +23,31 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] start = "srdt_analysis.__main__:main" + +[tool.ruff] +exclude = [ + ".ruff_cache", + "__pycache__", +] +line-length = 88 +indent-width = 4 + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F"] +extend-select = ["I"] +ignore = [] +fixable = ["ALL"] +unfixable = [] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = false +docstring-code-line-length = "dynamic" + +[tool.pyright] +include = ["srdt_analysis"] +exclude = ["**/__pycache__"] \ No newline at end of file diff --git a/srdt_analysis/__main__.py b/srdt_analysis/__main__.py index f38ffad..f2dd073 100644 --- a/srdt_analysis/__main__.py +++ b/srdt_analysis/__main__.py @@ -18,6 +18,7 @@ def main(): "combien de jour de congé payé par mois de travail effectif", [result["id"]], ) + print(res) diff --git a/srdt_analysis/albert.py b/srdt_analysis/albert.py index 08b2866..eb6fa22 100644 --- a/srdt_analysis/albert.py +++ b/srdt_analysis/albert.py @@ -7,8 +7,8 @@ class AlbertBase: - def __init__(self, api_key: str = None): - self.api_key = api_key or os.getenv("ALBERT_API_KEY") + def __init__(self): + self.api_key = os.getenv("ALBERT_API_KEY") if not self.api_key: raise ValueError( "API key must be provided either in constructor or as environment variable" diff --git a/srdt_analysis/chunker.py b/srdt_analysis/chunker.py index 8802d3f..dcb25c6 100644 --- a/srdt_analysis/chunker.py +++ b/srdt_analysis/chunker.py @@ -28,10 +28,12 @@ def __init__(self): def split_markdown(self, markdown: str) -> List[SplitDocument]: md_header_splits = self._markdown_splitter.split_text(markdown) - return self._character_recursive_splitter.split_documents(md_header_splits) + documents = self._character_recursive_splitter.split_documents(md_header_splits) + return [SplitDocument(doc.page_content, doc.metadata) for doc in documents] def split_character_recursive(self, content: str) -> List[SplitDocument]: - return self._character_recursive_splitter.split_text(content) + text_splits = self._character_recursive_splitter.split_text(content) + return [SplitDocument(text, {}) for text in text_splits] def split(self, content: str, content_type: str = "markdown"): if content_type.lower() == "markdown": diff --git a/srdt_analysis/collections.py b/srdt_analysis/collections.py index fdd7215..25c15a9 100644 --- a/srdt_analysis/collections.py +++ b/srdt_analysis/collections.py @@ -18,13 +18,13 @@ def _create(self, collection_name: str, model: str) -> str: return response.json()["id"] def create(self, collection_name: str, model: str) -> str: - collections = self.list() + collections: List[Dict[str, Any]] = self.list() for collection in collections: if collection["name"] == collection_name: self.delete(collection["id"]) return self._create(collection_name, model) - def list(self) -> Dict[str, Any]: + def list(self) -> List[Dict[str, Any]]: response = httpx.get(f"{ALBERT_ENDPOINT}/v1/collections", headers=self.headers) return response.json()["data"] @@ -92,9 +92,12 @@ def upload( ) } - data = {"request": '{"collection": "%s"}' % id_collection} + request_data = {"request": '{"collection": "%s"}' % id_collection} response = httpx.post( - f"{ALBERT_ENDPOINT}/v1/files", headers=self.headers, files=files, data=data + f"{ALBERT_ENDPOINT}/v1/files", + headers=self.headers, + files=files, + data=request_data, ) response.raise_for_status() diff --git a/srdt_analysis/data_exploiter.py b/srdt_analysis/data_exploiter.py index 5d884d8..fb6f58a 100644 --- a/srdt_analysis/data_exploiter.py +++ b/srdt_analysis/data_exploiter.py @@ -17,6 +17,9 @@ def __init__(self): self.collections = Collections() self.albert = AlbertBase() + def get_content(self, doc): + raise NotImplementedError("Subclasses should implement this method") + def process_documents( self, data: DocumentsList, output_file: str, collection_name: str ) -> ResultProcessDocumentType: @@ -100,5 +103,4 @@ def create_document_data(self, doc, content, chunks, keywords, summary, question data = super().create_document_data( doc, content, chunks, keywords, summary, questions ) - data["idcc"] = doc.document.get("idcc", "0000") return data diff --git a/srdt_analysis/database_manager.py b/srdt_analysis/database_manager.py index 5019de7..159d0d3 100644 --- a/srdt_analysis/database_manager.py +++ b/srdt_analysis/database_manager.py @@ -9,7 +9,7 @@ class DatabaseManager: def __init__(self): - self.conn = None + self.conn async def connect(self): self.conn = await asyncpg.connect( diff --git a/srdt_analysis/models.py b/srdt_analysis/models.py index 102b33d..fa71fe2 100644 --- a/srdt_analysis/models.py +++ b/srdt_analysis/models.py @@ -35,7 +35,7 @@ class DocumentData(TypedDict): @dataclass class ResultProcessDocumentType(TypedDict): documents: List[DocumentData] - id: int + id: str @dataclass @@ -63,12 +63,12 @@ class Section: class Content: text: str html: str - intro: str = "" - date: str = "" - sections: List[Section] = None - url: str = "" - raw: str = "" - referencedTexts: List[Dict] = None + sections: Optional[List[Section]] + referencedTexts: Optional[List[Dict]] + intro: str + date: str + url: str + raw: str @dataclass