diff --git a/poetry.lock b/poetry.lock index afedf77..fc2d752 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3756,6 +3756,29 @@ pydantic = "<3.0" pylint = ">2.0,<3.0" pylint-plugin-utils = "*" +[[package]] +name = "pypdf" +version = "5.4.0" +description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pypdf-5.4.0-py3-none-any.whl", hash = "sha256:db994ab47cadc81057ea1591b90e5b543e2b7ef2d0e31ef41a9bfe763c119dab"}, + {file = "pypdf-5.4.0.tar.gz", hash = "sha256:9af476a9dc30fcb137659b0dec747ea94aa954933c52cf02ee33e39a16fe9175"}, +] + +[package.dependencies] +typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +crypto = ["cryptography"] +cryptodome = ["PyCryptodome"] +dev = ["black", "flit", "pip-tools", "pre-commit (<2.18.0)", "pytest-cov", "pytest-socket", "pytest-timeout", "pytest-xdist", "wheel"] +docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"] +full = ["Pillow (>=8.0.0)", "cryptography"] +image = ["Pillow (>=8.0.0)"] + [[package]] name = "pysbd" version = "0.3.4" @@ -5959,4 +5982,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "395a5f09607b7ca5550b4f6f6002fdd8104f1e374f41e83701f8991c59198242" +content-hash = "9368cb6daecdfaa9dd3d28cd0234989e076b063c69711804b892dd6149a83afa" diff --git a/pyproject.toml b/pyproject.toml index 038f711..bb67594 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ uvicorn = {extras = ["standard"], version = "^0.29.0"} autogen-core = "^0.4.9.3" autogen-ext = "^0.4.9.3" autogen-agentchat = "^0.4.9.2" +pypdf = "^5.4.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" diff --git a/src/app/api/api_v1/endpoints/tutor.py b/src/app/api/api_v1/endpoints/tutor.py index 293b862..9cc9601 100644 --- a/src/app/api/api_v1/endpoints/tutor.py +++ b/src/app/api/api_v1/endpoints/tutor.py @@ -1,6 +1,7 @@ from typing import Annotated from fastapi import APIRouter, File, Response, UploadFile +from pypdf import PdfReader from src.app.api.dependencies import get_settings from src.app.services.abst_chat import AbstractChat, ChatFactory @@ -20,7 +21,7 @@ settings = get_settings() -chatfactory: AbstractChat = ChatFactory().create_chat("openai") +chatfactory: AbstractChat = ChatFactory().create_chat("openai", model="gpt-4o") chatfactory.init_client() sp = SearchService() @@ -39,7 +40,21 @@ async def tutor_search( files: Annotated[list[UploadFile], File()], response: Response, ): - file_content: list[bytes] = [await file.read() for file in files] + files_content: list[bytes] = [] + for file in files: + if ( + file.content_type == "application/pdf" + or file.content_type == "application/x-pdf" + ): + file_content = "" + reader = PdfReader(file.file) + for page in reader.pages: + file_content += page.extract_text() + files_content.append(file_content.encode("utf-8", errors="ignore")) + else: + file_content = await file.read() + files_content.append(file_content) + doc_list_to_string = "Document {doc_nb}: {content}" file_content_str = [ @@ -47,18 +62,29 @@ async def tutor_search( doc_nb=index + 1, content=content.decode("utf-8", errors="ignore"), ) - for index, content in enumerate(file_content) + for index, content in enumerate(files_content) ] file_content_str = "\n\n".join(file_content_str) + print(file_content_str) + messages = [ {"role": "system", "content": extractor_prompt}, {"role": "assistant", "content": file_content_str}, ] - themes_extracted = await chatfactory.chat_schema( - model="gpt-4o-mini", messages=messages, response_format=ExtractorOuputList # type: ignore - ) + try: + themes_extracted = await chatfactory.chat_schema( + model="gpt-4o", messages=messages, response_format=ExtractorOuputList # type: ignore + ) + except Exception as e: + logger.error(f"Error in chat schema: {e}") + # todo: handle error + return TutorSearchResponse( + extracts=[], + nb_results=0, + documents=[], + ) if not themes_extracted or not themes_extracted.extracts: return TutorSearchResponse( diff --git a/src/app/services/abst_chat.py b/src/app/services/abst_chat.py index f1a9978..f967c9a 100644 --- a/src/app/services/abst_chat.py +++ b/src/app/services/abst_chat.py @@ -19,12 +19,12 @@ from abc import ABC, abstractmethod from typing import AsyncIterable, Dict, List, Literal, Optional -import openai from azure.ai.inference import ChatCompletionsClient from azure.core.credentials import AzureKeyCredential # from ecologits import EcoLogits # type: ignore from mistralai import Mistral +from openai import AsyncAzureOpenAI from pydantic import BaseModel from src.app.api.dependencies import get_settings @@ -456,7 +456,7 @@ def init_client(self): raise ValueError("API_BASE or API_VERSION not provided") try: - self.chat_client = openai.AsyncAzureOpenAI( + self.chat_client = AsyncAzureOpenAI( api_key=self.API_KEY, azure_endpoint=self.API_BASE, api_version=self.API_VERSION, @@ -507,7 +507,7 @@ async def chat_schema( ): try: completion = await self.chat_client.beta.chat.completions.parse( - model=model, + model=self.model or model, messages=messages, temperature=0.2, response_format=response_format, @@ -653,6 +653,26 @@ def create_chat( raise ValueError(f"Unsupported chat type: {chat_type}") if chat_type == "openai": + openai_models = { + "gpt-4o-mini": ( + settings.AZURE_API_KEY, + settings.AZURE_API_BASE, + settings.AZURE_API_VERSION, + ), + "gpt-4o": ( + settings.AZURE_GPT_4O_API_KEY, + settings.AZURE_GPT_4O_API_BASE, + "2025-01-01-preview", + ), + } + if model: + key, base, version = openai_models.get(model, (None, None, None)) + if not key or not base or not version: + raise ValueError(f"Unsupported model: {model}") + return chat_classes[chat_type]( + API_KEY=key, API_BASE=base, API_VERSION=version, model=model + ) + return chat_classes[chat_type]( API_VERSION=settings.AZURE_API_VERSION, API_KEY=settings.AZURE_API_KEY, diff --git a/src/app/services/tutor/tutor.py b/src/app/services/tutor/tutor.py index d68d255..4070dd1 100644 --- a/src/app/services/tutor/tutor.py +++ b/src/app/services/tutor/tutor.py @@ -22,7 +22,12 @@ sdg_expert_topic_type, university_teacher_topic_type, ) -from src.app.services.tutor.models import Message, TaskResponse, TutorSearchResponse, MessageWithResources +from src.app.services.tutor.models import ( + Message, + MessageWithResources, + TaskResponse, + TutorSearchResponse, +) from src.app.services.tutor.utils import extract_doc_info settings = get_settings() @@ -40,7 +45,9 @@ async def tutor_manager(content: TutorSearchResponse) -> Message: queue = asyncio.Queue[TaskResponse]() - formatted_content = MessageWithResources(content=content.extracts, resources=extract_doc_info(content.documents)) + formatted_content = MessageWithResources( + content=content.extracts, resources=extract_doc_info(content.documents) + ) async def collect_result( _agent: ClosureContext, message: TaskResponse, ctx: MessageContext