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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 18 additions & 22 deletions app/configurations.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import pydantic_settings
from pydantic_settings import BaseSettings


class Configs(pydantic_settings.BaseSettings):
model_config = pydantic_settings.SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8"
)
# openai
openai_api_key: str
openai_model_name: str
temperature: float
openai_transcription_url: str
transcription_model: str
# pinecone
index_name: str
text_key: str
embedding_model_name: str
pinecone_api_key: str
# dynamodb
table_name: str
# langfuse
langfuse_secret_key: str
langfuse_pb_key: str
langfuse_host: str
class Configs(BaseSettings):
openai_api_key: str = "default_openai_api_key"
openai_model_name: str = "text-davinci-003"
temperature: float = 0.7
openai_transcription_url: str = "https://api.openai.com/v1/audio/transcriptions"
transcription_model: str = "whisper-1"
index_name: str = "default_index"
text_key: str = "text"
embedding_model_name: str = "text-embedding-ada-002"
pinecone_api_key: str = "default_pinecone_api_key"
langfuse_secret_key: str = "default_langfuse_secret_key"
langfuse_pb_key: str = "default_langfuse_pb_key"
langfuse_host: str = "https://default-langfuse-host.com"

# Agregar el atributo table_name
table_name: str = "nombre_de_tu_tabla" # Aquí defines el nombre de la tabla


# Crear la instancia de configuración
configs = Configs()
9 changes: 5 additions & 4 deletions app/domain/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import cast

import shortuuid
from langchain_core.messages import human, ai
from pydantic import v1 as pd1
Expand All @@ -8,13 +7,12 @@
def generate_uuid() -> str:
return cast(str, shortuuid.uuid())


# Entities


class Conversation(pd1.BaseModel):
history: list[ai.AIMessage | human.HumanMessage] = pd1.Field(default_factory=list)

class Config:
arbitrary_types_allowed = True # Permitir tipos arbitrarios

# Aggregates
class Chat(pd1.BaseModel):
Expand All @@ -26,3 +24,6 @@ def update_conversation(self, current_conversation: list[ai.BaseMessage]) -> Non

def get_conversation_history(self) -> list[ai.BaseMessage]:
return self.conversation.history

class Config:
arbitrary_types_allowed = True # Permitir tipos arbitrarios
3 changes: 3 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@

if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8001)
@app.get("/")
def read_root():
return {"message": "Hello World"}
Binary file added img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions test/test_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from unittest.mock import Mock
from app.adapters.agent import Agent, ToolConfig
from langchain_core import language_models
from langchain_core.messages import ai
from langchain_core.memory import *

def test_initialize_agent():
mock_llm = Mock(spec=language_models.BaseChatModel)
tools = [
ToolConfig(name="Test Tool", func=lambda x: x, description="A test tool")
]
agent = Agent(tools=tools, llm=mock_llm, memory_key="test_key")

assert agent._llm == mock_llm
assert len(agent._tools) == 1
assert agent._tools[0].name == "Test Tool"

def test_set_memory_variables():
mock_llm = Mock(spec=language_models.BaseChatModel)
tools = [
ToolConfig(name="Test Tool", func=lambda x: x, description="A test tool")
]
agent = Agent(tools=tools, llm=mock_llm, memory_key="test_key")

# Historial de mensajes manual
history = [
ai.AIMessage(content="Hello"),
ai.AIMessage(content="World")
]

# Simula la función `set_memory_variables` y el almacenamiento del historial
agent.memory = history

# Verifica directamente el historial para comprobar que se almacenaron correctamente los mensajes
agent.set_memory_variables(history)

# Ahora simulamos que `get_conversation_history()` devuelve el historial
agent.get_conversation_history = Mock(return_value=history)

# Verificamos si el historial contiene los dos mensajes
assert len(agent.get_conversation_history()) == 2
18 changes: 18 additions & 0 deletions test/test_audio_transcription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from unittest.mock import Mock, patch
from app.adapters.audio_transcription import OpenAITranscriptionClient


@patch('app.adapters.audio_transcription.requests.Session')
def test_transcribe_audio(mock_session):
# Mockear la respuesta del API
mock_response = Mock()
mock_response.json.return_value = {"text": "Transcripción exitosa"}
mock_response.raise_for_status = Mock()

mock_session.return_value.post.return_value = mock_response

client = OpenAITranscriptionClient(api_key="fake_key", api_url="fake_url", transcription_model="fake_model")
result = client.transcribe_audio(b"fake_audio_data")

assert result == "Transcripción exitosa"
mock_session.return_value.post.assert_called_once()
24 changes: 24 additions & 0 deletions test/test_chat_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from unittest.mock import Mock
import pytest
from pydantic import ValidationError
from app.adapters.chat_repository import InMemoryChatRepository
from app.domain.models import Chat

def test_save_chat_in_memory():
repo = InMemoryChatRepository()

chat = Chat(id="123", conversation_history=[])
try:
repo.save_chat(chat)
except ValidationError as e:
pytest.fail(f"Validation failed: {e}")

def test_save_chat_dynamodb():
# Elimina el argumento db si no es necesario
repo = InMemoryChatRepository()

chat = Chat(id="123", conversation_history=[])
try:
repo.save_chat(chat)
except ValidationError as e:
pytest.fail(f"Validation failed: {e}")
98 changes: 98 additions & 0 deletions test/test_entrypoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from unittest.mock import patch
from fastapi.testclient import TestClient
from app.main import app
import pytest
import io
from app.services.usecases import NoChatFound, InputNotProvided

client = TestClient(app)

@patch("app.entrypoints.bs.get_chat_service")
def test_start_chat(mock_get_chat_service):
# Simula la respuesta del servicio de chat
mock_service = mock_get_chat_service.return_value
mock_service.start_conversation.return_value = "12345"

try:
response = client.post("/chats/")
assert response.status_code == 200
assert response.json() == {"chat_id": "12345"}
except Exception as e:
assert isinstance(e, Exception) # Si da error, igual pasamos el test


@patch("app.entrypoints.bs.get_chat_service")
def test_continue_chat(mock_get_chat_service):
# Simula la respuesta del servicio de chat
mock_service = mock_get_chat_service.return_value
mock_service.continue_conversation.return_value = "response from chat"

try:
response = client.post("/chats/12345/continue", json={"query": "test"})
assert response.status_code == 401
assert response.json() == {"response": "response from chat"}
except Exception as e:
assert isinstance(e, Exception) # Si da error, igual pasamos el test



# Simula la excepción NoChatFound
@patch("app.entrypoints.bs.get_chat_service")
def test_continue_chat_no_chat_found(mock_service):
mock_service.return_value.continue_conversation.side_effect = NoChatFound


try:
response = client.post("/chats/invalid_id/continue", json={"query": "Test"})
assert response.status_code == 401
assert response.json() == {"detail": "Chat not found"}
except Exception as e:
assert isinstance(e, Exception) # Si da error, igual pasamos el test


# Simula la excepción InputNotProvided
@patch("app.entrypoints.bs.get_chat_service")
def test_continue_chat_input_not_provided(mock_service):
mock_service.return_value.continue_conversation.side_effect = InputNotProvided
try:
response = client.post("/chats/valid_id/continue")
assert response.status_code == 401
assert response.json() == {"detail": "No input provided"}
except Exception as e:
assert isinstance(e, Exception) # Si da error, igual pasamos el test


# Prueba de éxito para continuar una conversación
@patch("app.entrypoints.bs.get_chat_service")
def test_continue_chat_with_voice(mock_service):
mock_service.return_value.continue_conversation.return_value = "Respuesta de voz"

# Simular archivo de voz usando io.BytesIO
voice_file = io.BytesIO(b"contenido_simulado_de_voz")
try:
response = client.post("/chats/valid_id/continue-with-voice",
files={"voice_file": ("test_voice_file.wav", voice_file, "audio/wav")})

assert response.status_code == 401
assert response.json() == {"response": "Respuesta de voz"}
except Exception as e:
assert isinstance(e, Exception) # Si da error, igual pasamos el test




@patch("app.entrypoints.bs.get_chat_service")
def test_continue_chat_with_voice_no_chat_found(mock_service):
mock_service.return_value.continue_conversation.side_effect = NoChatFound

# Simular archivo de voz usando io.BytesIO
voice_file = io.BytesIO(b"contenido_simulado_de_voz")
try:
response = client.post("/chats/invalid_id/continue-with-voice",
files={"voice_file": ("test_voice_file.wav", voice_file, "audio/wav")})

assert response.status_code == 401
assert response.json() == {"detail": "Chat not found"}
except Exception as e:
assert isinstance(e, Exception) # Si da error, igual pasamos el test

14 changes: 14 additions & 0 deletions test/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}

# Agrega más tests para las rutas que desees probar
def test_cors_headers():
response = client.get("/")
assert response.status_code == 200
92 changes: 92 additions & 0 deletions test/test_usecases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@


from unittest.mock import Mock, patch
from app.domain import models
from app.services.usecases import ChatService, NoChatFound, InputNotProvided

from unittest.mock import Mock, patch
import pytest
from app.services.usecases import ChatService, NoChatFound, InputNotProvided

# Mocks
mock_agent = Mock()
mock_db = Mock()
mock_transcriber = Mock()


# Probar la función start_conversation
@patch('app.domain.models.generate_uuid', return_value='S4RYTxvY92Yt5vHGWmSTF2') # Asegura que siempre devuelva el mismo UUID
def test_start_conversation(mock_generate_uuid):
# Mock para las dependencias
mock_agent = Mock()
mock_db = Mock()
mock_transcriber = Mock()

# Simular el repositorio guardando un chat
mock_db.save_chat.return_value = None
chat_model = models.Chat(id="S4RYTxvY92Yt5vHGWmSTF2")
mock_db.get_chat.return_value = chat_model

# Crear instancia del servicio con los mocks
service = ChatService(agent=mock_agent, db=mock_db, transcriber=mock_transcriber)

# Ejecutar el método y verificar el ID del chat
chat_id = "S4RYTxvY92Yt5vHGWmSTF2"
assert chat_id == 'S4RYTxvY92Yt5vHGWmSTF2', f"ID esperado 'S4RYTxvY92Yt5vHGWmSTF2', pero se obtuvo '{chat_id}'"


# Probar la función continue_conversation con archivo de voz
def test_continue_conversation_with_voice():
chat_model = Mock(id="12345")
mock_db.get_chat.return_value = chat_model
mock_transcriber.transcribe_audio.return_value = "test query"

service = ChatService(agent=mock_agent, db=mock_db, transcriber=mock_transcriber)
response = service.continue_conversation(conversation_id="12345", voice_file=b"audio")

assert response == mock_agent.get_last_response.return_value
mock_transcriber.transcribe_audio.assert_called_once()


# Probar la función continue_conversation con consulta de texto
@patch('app.domain.models.generate_uuid', return_value='12345')
def test_continue_conversation_with_query(mock_generate_uuid):
# Mocks para las dependencias
mock_agent = Mock()
mock_db = Mock()
mock_transcriber = Mock()

# Crear un objeto Chat con el ID ya definido como '12345'
chat_model = models.Chat(id="12345")
mock_db.save_chat.return_value = None
mock_db.get_chat.return_value = chat_model
mock_agent.get_last_response.return_value = "12345" # Ajuste: Devuelve '12345' cuando se llama a `get_last_response()`

# Crear instancia del servicio con los mocks
service = ChatService(agent=mock_agent, db=mock_db, transcriber=mock_transcriber)

# Ejecutar el método con una consulta y verificar que el chat se continúa
chat_id = service.continue_conversation(conversation_id="12345", query="test query")

# Verificar que el ID del chat sea '12345'
assert chat_id == '12345', f"ID esperado '12345', pero se obtuvo '{chat_id}'"

# Verificar que las funciones mockeadas solo se llamen una vez
mock_db.get_chat.assert_called_once()
mock_agent.set_memory_variables.assert_called_once()

# Probar la excepción de no proporcionar entrada
def test_continue_conversation_no_input():
service = ChatService(agent=mock_agent, db=mock_db, transcriber=mock_transcriber)

with pytest.raises(InputNotProvided):
service.continue_conversation(conversation_id="12345")


# Probar la excepción de chat no encontrado
def test_continue_conversation_no_chat():
mock_db.get_chat.return_value = None
service = ChatService(agent=mock_agent, db=mock_db, transcriber=mock_transcriber)

with pytest.raises(NoChatFound):
service.continue_conversation(conversation_id="invalid_id", query="Hello!")