Skip to content
Merged
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
49 changes: 43 additions & 6 deletions .github/workflows/pr-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ jobs:
cd echo/server
ruff check .

- name: Run Ruff Format Check
run: |
cd echo/server
ruff format --check .

- name: Run MyPy Type Check
run: |
cd echo/server
Expand All @@ -45,6 +40,48 @@ jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
env:
# Required environment variables for config.py
DIRECTUS_SECRET: test-secret
DIRECTUS_TOKEN: test-token
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
REDIS_URL: redis://localhost:6379
OPENAI_API_KEY: sk-test-key
ANTHROPIC_API_KEY: sk-ant-test-key
STORAGE_S3_BUCKET: test-bucket
STORAGE_S3_ENDPOINT: https://test.endpoint.com
STORAGE_S3_KEY: test-key
STORAGE_S3_SECRET: test-secret
SMALL_LITELLM_MODEL: gpt-4o-mini
SMALL_LITELLM_API_KEY: test-key
SMALL_LITELLM_API_VERSION: "2024-06-01"
SMALL_LITELLM_API_BASE: https://api.openai.com/v1
MEDIUM_LITELLM_MODEL: gpt-4o
MEDIUM_LITELLM_API_KEY: test-key
MEDIUM_LITELLM_API_VERSION: "2024-06-01"
MEDIUM_LITELLM_API_BASE: https://api.openai.com/v1
LARGE_LITELLM_MODEL: gpt-4o
LARGE_LITELLM_API_KEY: test-key
LARGE_LITELLM_API_VERSION: "2024-06-01"
LARGE_LITELLM_API_BASE: https://api.openai.com/v1
LIGHTRAG_LITELLM_MODEL: gpt-4o-mini
LIGHTRAG_LITELLM_API_KEY: test-key
LIGHTRAG_LITELLM_API_VERSION: "2024-06-01"
LIGHTRAG_LITELLM_API_BASE: https://api.openai.com/v1
LIGHTRAG_LITELLM_AUDIOMODEL_MODEL: gpt-4o-audio
LIGHTRAG_LITELLM_AUDIOMODEL_API_BASE: https://api.openai.com/v1
LIGHTRAG_LITELLM_AUDIOMODEL_API_KEY: test-key
LIGHTRAG_LITELLM_AUDIOMODEL_API_VERSION: "2024-06-01"
LIGHTRAG_LITELLM_TEXTSTRUCTUREMODEL_MODEL: gpt-4o-mini
LIGHTRAG_LITELLM_TEXTSTRUCTUREMODEL_API_BASE: https://api.openai.com/v1
LIGHTRAG_LITELLM_TEXTSTRUCTUREMODEL_API_KEY: test-key
LIGHTRAG_LITELLM_TEXTSTRUCTUREMODEL_API_VERSION: "2024-06-01"
LIGHTRAG_LITELLM_EMBEDDING_MODEL: text-embedding-ada-002
LIGHTRAG_LITELLM_EMBEDDING_API_BASE: https://api.openai.com/v1
LIGHTRAG_LITELLM_EMBEDDING_API_KEY: test-key
LIGHTRAG_LITELLM_EMBEDDING_API_VERSION: "2024-06-01"
LIGHTRAG_LITELLM_INFERENCE_MODEL: claude-3-5-sonnet
LIGHTRAG_LITELLM_INFERENCE_API_KEY: test-key
steps:
- uses: actions/checkout@v4

Expand All @@ -61,7 +98,7 @@ jobs:
- name: Run Unit Tests
run: |
cd echo/server
pytest tests/ -v -m "not integration and not slow" --maxfail=3
pytest tests/ -v -m "not integration and not slow and not smoke" --maxfail=3

build-images:
name: Build Docker Images (validation only)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,4 @@ Thanks to everyone in the community that has contributed to this project!
</a>

![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/Dembrane/echo?utm_source=oss&utm_medium=github&utm_campaign=Dembrane%2Fecho&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)

14 changes: 7 additions & 7 deletions echo/server/dembrane/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@
logger.debug(f"RUNPOD_WHISPER_PRIORITY_BASE_URL: {RUNPOD_WHISPER_PRIORITY_BASE_URL}")

RUNPOD_WHISPER_MAX_REQUEST_THRESHOLD = int(
str(os.environ.get("RUNPOD_WHISPER_MAX_REQUEST_THRESHOLD"))
os.environ.get("RUNPOD_WHISPER_MAX_REQUEST_THRESHOLD") or 100
)

ENABLE_LITELLM_WHISPER_TRANSCRIPTION = os.environ.get(
Expand Down Expand Up @@ -539,23 +539,23 @@
)
logger.debug(f"RUNPOD_DIARIZATION_BASE_URL: {RUNPOD_DIARIZATION_BASE_URL}")

RUNPOD_DIARIZATION_TIMEOUT = int(os.environ.get("RUNPOD_DIARIZATION_TIMEOUT", 30))
RUNPOD_DIARIZATION_TIMEOUT = int(os.environ.get("RUNPOD_DIARIZATION_TIMEOUT") or 30)
if ENABLE_RUNPOD_DIARIZATION:
logger.debug(f"RUNPOD_DIARIZATION_TIMEOUT: {RUNPOD_DIARIZATION_TIMEOUT}")
# ---------------/Secrets---------------


# ---------------Configurations---------------
AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM = int(
os.environ.get("AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM", 10)
os.environ.get("AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM") or 10
)
assert AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM, (
"AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM environment variable is not set"
)
logger.debug(f"AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM: {AUDIO_LIGHTRAG_CONVERSATION_HISTORY_NUM}")

AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS = int(
os.environ.get("AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS", 60)
os.environ.get("AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS") or 60
)
assert AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS, (
"AUDIO_LIGHTRAG_COOL_OFF_TIME_SECONDS environment variable is not set"
Expand All @@ -572,14 +572,14 @@
logger.debug(f"ENABLE_AUDIO_LIGHTRAG_INPUT: {ENABLE_AUDIO_LIGHTRAG_INPUT}")

AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB = int(
os.environ.get("AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB", 15)
os.environ.get("AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB") or 15
)
assert AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB, (
"AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB environment variable is not set"
)
logger.debug(f"AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB: {AUDIO_LIGHTRAG_MAX_AUDIO_FILE_SIZE_MB}")

AUDIO_LIGHTRAG_TOP_K_PROMPT = int(os.environ.get("AUDIO_LIGHTRAG_TOP_K_PROMPT", 100))
AUDIO_LIGHTRAG_TOP_K_PROMPT = int(os.environ.get("AUDIO_LIGHTRAG_TOP_K_PROMPT") or 100)
assert AUDIO_LIGHTRAG_TOP_K_PROMPT, "AUDIO_LIGHTRAG_TOP_K_PROMPT environment variable is not set"
logger.debug(f"AUDIO_LIGHTRAG_TOP_K_PROMPT: {AUDIO_LIGHTRAG_TOP_K_PROMPT}")

Expand All @@ -601,7 +601,7 @@
)
logger.debug(f"AUDIO_LIGHTRAG_REDIS_LOCK_PREFIX: {AUDIO_LIGHTRAG_REDIS_LOCK_PREFIX}")

AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY = int(os.environ.get("AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY", 3600))
AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY = int(os.environ.get("AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY") or 3600)
assert AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY, (
"AUDIO_LIGHTRAG_REDIS_LOCK_EXPIRY environment variable is not set"
)
Expand Down
1 change: 1 addition & 0 deletions echo/server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ markers = [
"integration: integration tests requiring external services",
]
addopts = "--strict-markers --log-cli-level=INFO --log-cli-format='%(asctime)s [%(levelname)8s] %(name)s: %(message)s' --log-cli-date-format='%Y-%m-%d %H:%M:%S'"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.run]
source = ["dembrane"]
Expand Down
2 changes: 2 additions & 0 deletions echo/server/tests/api/test_conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

logger = logging.getLogger("dembrane.tests.api.test_conversation")

pytestmark = pytest.mark.integration


@pytest.mark.asyncio
async def test_get_conversation_transcript():
Expand Down
63 changes: 41 additions & 22 deletions echo/server/tests/service/test_conversation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def project():
project_service.delete(project["id"])


@pytest.mark.integration
def test_create_conversation(project):
conversation = conversation_service.create(
project_id=project["id"],
Expand All @@ -49,6 +50,7 @@ def test_create_conversation(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_create_conversation_with_tags(project):
tags = project_service.create_tags_and_link(project.get("id"), ["tag1", "tag2"])

Expand Down Expand Up @@ -85,6 +87,7 @@ def test_create_conversation_with_tags(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_create_conversation_not_allowed():
project = project_service.create(
name="Test Project No Conversations",
Expand All @@ -101,6 +104,7 @@ def test_create_conversation_not_allowed():
project_service.delete(project["id"])


@pytest.mark.integration
def test_get_by_id_or_raise(project):
conversation = conversation_service.create(
project_id=project["id"],
Expand All @@ -115,13 +119,15 @@ def test_get_by_id_or_raise(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_get_by_id_or_raise_not_found():
try:
conversation_service.get_by_id_or_raise("non-existent-id")
except Exception as e:
assert isinstance(e, ConversationNotFoundException)


@pytest.mark.integration
def test_update_conversation(project):
conversation = conversation_service.create(
project_id=project["id"],
Expand Down Expand Up @@ -149,6 +155,7 @@ def test_update_conversation(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_update_conversation_partial(project):
conversation = conversation_service.create(
project_id=project["id"],
Expand All @@ -174,6 +181,7 @@ def test_update_conversation_partial(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_update_conversation_not_found():
with pytest.raises(ConversationNotFoundException):
conversation_service.update(
Expand All @@ -182,6 +190,7 @@ def test_update_conversation_not_found():
)


@pytest.mark.integration
def test_delete_conversation(project):
conversation = conversation_service.create(
project_id=project["id"],
Expand All @@ -194,32 +203,30 @@ def test_delete_conversation(project):
conversation_service.get_by_id_or_raise(conversation["id"])


# Unit test - tests service initialization with dependencies
def test_conversation_service_property_getters():
"""Test lazy initialization of service dependencies."""
# Create a fresh service instance
service = ConversationService()

# Initially, all services should be None
assert service._file_service is None
assert service._event_service is None
assert service._project_service is None

# Access properties to trigger lazy initialization
file_service = service.file_service
event_service = service.event_service
project_service = service.project_service

# Verify services are initialized
assert file_service is not None
assert event_service is not None
assert project_service is not None

# Verify subsequent access returns same instances
"""Test that service dependencies are properly set on initialization."""
from dembrane.service.file import get_file_service
from dembrane.service.project import ProjectService

# Create service instances
file_service = get_file_service()
project_svc = ProjectService()

# Create conversation service with dependencies
service = ConversationService(
file_service=file_service,
project_service=project_svc,
)

# Verify services are set correctly
assert service.file_service is not None
assert service.project_service is not None
assert service.file_service is file_service
assert service.event_service is event_service
assert service.project_service is project_service
assert service.project_service is project_svc


# Unit test - uses mocks, no external dependencies
def test_get_by_id_directus_bad_request():
"""Test exception handling when Directus returns bad request."""
with patch("dembrane.service.conversation.directus_client_context") as mock_context:
Expand All @@ -231,6 +238,7 @@ def test_get_by_id_directus_bad_request():
conversation_service.get_by_id_or_raise("test-id")


# Unit test - uses mocks, no external dependencies
def test_get_by_id_empty_result():
"""Test exception handling when no conversation found."""
with patch("dembrane.service.conversation.directus_client_context") as mock_context:
Expand All @@ -242,6 +250,7 @@ def test_get_by_id_empty_result():
conversation_service.get_by_id_or_raise("test-id")


# Unit test - uses mocks, no external dependencies
def test_update_conversation_directus_bad_request():
"""Test exception handling when updating non-existent conversation."""
with patch("dembrane.service.conversation.directus_client_context") as mock_context:
Expand All @@ -253,6 +262,8 @@ def test_update_conversation_directus_bad_request():
conversation_service.update(conversation_id="non-existent-id", participant_name="Test")


# Unit test - uses mocks for S3 and events, but still needs DB for conversation
@pytest.mark.integration
def test_create_chunk_from_file(project):
"""Test creating conversation chunk from file upload."""
conversation = conversation_service.create(
Expand Down Expand Up @@ -295,6 +306,7 @@ def test_create_chunk_from_file(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_create_chunk_from_file_finished_conversation(project):
"""Test that chunks cannot be added to finished conversations."""
conversation = conversation_service.create(
Expand All @@ -318,6 +330,8 @@ def test_create_chunk_from_file_finished_conversation(project):
conversation_service.delete(conversation["id"])


# Uses mock for events, but still needs DB for conversation
@pytest.mark.integration
def test_create_chunk_from_text(project):
"""Test creating conversation chunk from text."""
conversation = conversation_service.create(
Expand Down Expand Up @@ -355,6 +369,7 @@ def test_create_chunk_from_text(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_create_chunk_from_text_finished_conversation(project):
"""Test that text chunks cannot be added to finished conversations."""
conversation = conversation_service.create(
Expand All @@ -376,6 +391,7 @@ def test_create_chunk_from_text_finished_conversation(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_get_by_id_with_chunks(project):
"""Test retrieving conversation with chunks sorted by timestamp."""
conversation = conversation_service.create(
Expand Down Expand Up @@ -405,6 +421,7 @@ def test_get_by_id_with_chunks(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_delete_chunk(project):
conversation = conversation_service.create(
project_id=project["id"],
Expand Down Expand Up @@ -446,6 +463,7 @@ def test_delete_chunk(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_chunk_timestamp_functionality(project):
"""Test comprehensive timestamp functionality for conversation chunks."""
conversation = conversation_service.create(
Expand Down Expand Up @@ -536,6 +554,7 @@ def test_chunk_timestamp_functionality(project):
conversation_service.delete(conversation["id"])


@pytest.mark.integration
def test_chunk_timestamp_edge_cases(project):
"""Test edge cases for timestamp handling."""
conversation = conversation_service.create(
Expand Down
2 changes: 2 additions & 0 deletions echo/server/tests/service/test_file_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

logger = logging.getLogger(__name__)

pytestmark = pytest.mark.integration


class TestS3FileService:
"""Test S3FileService implementation."""
Expand Down
Loading