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
83 changes: 49 additions & 34 deletions echo/server/dembrane/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
# patterns are inconsistent - ENABLE_LITELLM_WHISPER_TRANSCRIPTION needs to be set
# better yet modularize it and have modules manage their own config?

## ENABLE_ASSEMBLYAI_TRANSCRIPTION = os.environ.get(
# "ENABLE_ASSEMBLYAI_TRANSCRIPTION", "false"
# ).lower() in ["true", "1"]
# This is a bad pattern for hygiene because it allows for multiple values to be set if you want it to be true/false

# This file inits twice for some reason...

Comment on lines +9 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Drop stale commented code.

This dead snippet adds noise and confuses the enablement story.

🤖 Prompt for AI Agents
In echo/server/dembrane/config.py around lines 9 to 15, remove the stale
commented-out environment variable block and the stray note ("This file inits
twice for some reason...") so the file contains only active configuration code;
if an enablement flag is required, replace the commented snippet with a single
canonical implementation elsewhere (or document its location) and run
linters/tests to ensure no references rely on the deleted comments.

import os
import sys
import logging
Expand Down Expand Up @@ -177,13 +184,20 @@
DISABLE_CORS = os.environ.get("DISABLE_CORS", "false").lower() in ["true", "1"]
logger.debug(f"DISABLE_CORS: {DISABLE_CORS}")

ENABLE_ENGLISH_TRANSCRIPTION_WITH_LITELLM = os.environ.get(
"ENABLE_ENGLISH_TRANSCRIPTION_WITH_LITELLM", "false"
### Transcription

ENABLE_ASSEMBLYAI_TRANSCRIPTION = os.environ.get(
"ENABLE_ASSEMBLYAI_TRANSCRIPTION", "false"
).lower() in ["true", "1"]
# ENABLE_ENGLISH_TRANSCRIPTION_WITH_LITELLM is optional and defaults to false
logger.debug(
"ENABLE_ENGLISH_TRANSCRIPTION_WITH_LITELLM: %s", ENABLE_ENGLISH_TRANSCRIPTION_WITH_LITELLM
)
logger.debug(f"ENABLE_ASSEMBLYAI_TRANSCRIPTION: {ENABLE_ASSEMBLYAI_TRANSCRIPTION}")

ASSEMBLYAI_API_KEY = os.environ.get("ASSEMBLYAI_API_KEY")
if ENABLE_ASSEMBLYAI_TRANSCRIPTION:
assert ASSEMBLYAI_API_KEY, "ASSEMBLYAI_API_KEY environment variable is not set"
logger.debug("ASSEMBLYAI_API_KEY: set")

ASSEMBLYAI_BASE_URL = os.environ.get("ASSEMBLYAI_BASE_URL", "https://api.eu.assemblyai.com")
logger.debug(f"ASSEMBLYAI_BASE_URL: {ASSEMBLYAI_BASE_URL}")

ENABLE_RUNPOD_WHISPER_TRANSCRIPTION = os.environ.get(
"ENABLE_RUNPOD_WHISPER_TRANSCRIPTION", "false"
Expand All @@ -195,7 +209,6 @@
assert RUNPOD_WHISPER_API_KEY, "RUNPOD_WHISPER_API_KEY environment variable is not set"
logger.debug("RUNPOD_WHISPER_API_KEY: set")


RUNPOD_WHISPER_BASE_URL = os.environ.get("RUNPOD_WHISPER_BASE_URL")
if ENABLE_RUNPOD_WHISPER_TRANSCRIPTION:
assert RUNPOD_WHISPER_BASE_URL, "RUNPOD_WHISPER_BASE_URL environment variable is not set"
Expand All @@ -212,6 +225,35 @@
str(os.environ.get("RUNPOD_WHISPER_MAX_REQUEST_THRESHOLD"))
)

ENABLE_LITELLM_WHISPER_TRANSCRIPTION = os.environ.get(
"ENABLE_LITELLM_WHISPER_TRANSCRIPTION", "false"
).lower() in ["true", "1"]
logger.debug(f"ENABLE_LITELLM_WHISPER_TRANSCRIPTION: {ENABLE_LITELLM_WHISPER_TRANSCRIPTION}")

LITELLM_WHISPER_API_KEY = os.environ.get("LITELLM_WHISPER_API_KEY")
if ENABLE_LITELLM_WHISPER_TRANSCRIPTION:
assert LITELLM_WHISPER_API_KEY, "LITELLM_WHISPER_API_KEY environment variable is not set"
logger.debug("LITELLM_WHISPER_API_KEY: set")

LITELLM_WHISPER_API_VERSION = os.environ.get("LITELLM_WHISPER_API_VERSION", "2024-06-01")
if ENABLE_LITELLM_WHISPER_TRANSCRIPTION:
assert LITELLM_WHISPER_API_VERSION, (
"LITELLM_WHISPER_API_VERSION environment variable is not set"
)
logger.debug(f"LITELLM_WHISPER_API_VERSION: {LITELLM_WHISPER_API_VERSION}")

LITELLM_WHISPER_MODEL = os.environ.get("LITELLM_WHISPER_MODEL")
if ENABLE_LITELLM_WHISPER_TRANSCRIPTION:
assert LITELLM_WHISPER_MODEL, "LITELLM_WHISPER_MODEL environment variable is not set"
logger.debug(f"LITELLM_WHISPER_MODEL: {LITELLM_WHISPER_MODEL}")

LITELLM_WHISPER_URL = os.environ.get("LITELLM_WHISPER_URL")
if ENABLE_LITELLM_WHISPER_TRANSCRIPTION:
assert LITELLM_WHISPER_URL, "LITELLM_WHISPER_URL environment variable is not set"
logger.debug(f"LITELLM_WHISPER_URL: {LITELLM_WHISPER_URL}")

### END Transcription

RUNPOD_TOPIC_MODELER_URL = os.environ.get("RUNPOD_TOPIC_MODELER_URL")
logger.debug(f"RUNPOD_TOPIC_MODELER_URL: {RUNPOD_TOPIC_MODELER_URL}")

Expand Down Expand Up @@ -276,33 +318,6 @@
assert LARGE_LITELLM_API_BASE, "LARGE_LITELLM_API_BASE environment variable is not set"
logger.debug(f"LARGE_LITELLM_API_BASE: {LARGE_LITELLM_API_BASE}")

ENABLE_LITELLM_WHISPER_TRANSCRIPTION = os.environ.get(
"ENABLE_LITELLM_WHISPER_TRANSCRIPTION", "false"
).lower() in ["true", "1"]
logger.debug(f"ENABLE_LITELLM_WHISPER_TRANSCRIPTION: {ENABLE_LITELLM_WHISPER_TRANSCRIPTION}")

LITELLM_WHISPER_API_KEY = os.environ.get("LITELLM_WHISPER_API_KEY")
if ENABLE_LITELLM_WHISPER_TRANSCRIPTION:
assert LITELLM_WHISPER_API_KEY, "LITELLM_WHISPER_API_KEY environment variable is not set"
logger.debug("LITELLM_WHISPER_API_KEY: set")

LITELLM_WHISPER_API_VERSION = os.environ.get("LITELLM_WHISPER_API_VERSION", "2024-06-01")
if ENABLE_LITELLM_WHISPER_TRANSCRIPTION:
assert LITELLM_WHISPER_API_VERSION, (
"LITELLM_WHISPER_API_VERSION environment variable is not set"
)
logger.debug(f"LITELLM_WHISPER_API_VERSION: {LITELLM_WHISPER_API_VERSION}")

LITELLM_WHISPER_MODEL = os.environ.get("LITELLM_WHISPER_MODEL")
if ENABLE_LITELLM_WHISPER_TRANSCRIPTION:
assert LITELLM_WHISPER_MODEL, "LITELLM_WHISPER_MODEL environment variable is not set"
logger.debug(f"LITELLM_WHISPER_MODEL: {LITELLM_WHISPER_MODEL}")

LITELLM_WHISPER_URL = os.environ.get("LITELLM_WHISPER_URL")
if ENABLE_LITELLM_WHISPER_TRANSCRIPTION:
assert LITELLM_WHISPER_URL, "LITELLM_WHISPER_URL environment variable is not set"
logger.debug(f"LITELLM_WHISPER_URL: {LITELLM_WHISPER_URL}")

# *****************LIGHTRAG CONFIGURATIONS*****************

# Lightrag LLM model: Makes nodes and answers queries
Expand Down
61 changes: 38 additions & 23 deletions echo/server/dembrane/conversation_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from dembrane.s3 import get_signed_url
from dembrane.utils import get_utc_timestamp
from dembrane.config import (
ENABLE_RUNPOD_DIARIZATION,
RUNPOD_DIARIZATION_API_KEY,
RUNPOD_DIARIZATION_TIMEOUT,
RUNPOD_DIARIZATION_BASE_URL,
Expand All @@ -23,7 +24,7 @@
def _fetch_chunk_data(chunk_id: str) -> tuple[str, str] | None:
"""
Retrieves the audio file URI and project language for a given chunk ID from Directus.

Returns:
A tuple containing (audio_file_uri, project_language) if successful, or None if retrieval fails.
"""
Expand All @@ -33,13 +34,15 @@ def _fetch_chunk_data(chunk_id: str) -> tuple[str, str] | None:
{
"query": {
"filter": {"id": {"_eq": chunk_id}},
"fields": ["path", "conversation_id.project_id.language"]
"fields": ["path", "conversation_id.project_id.language"],
}
}
},
)[0]
audio_file_uri = directus_item["path"]
project_language = directus_item["conversation_id"]["project_id"]["language"]
logger.debug(f"Starting diarization for chunk_id: {chunk_id}, path: {audio_file_uri}, project_language: {project_language}")
logger.debug(
f"Starting diarization for chunk_id: {chunk_id}, path: {audio_file_uri}, project_language: {project_language}"
)
return audio_file_uri, project_language
except Exception as e:
logger.error(f"Failed to fetch audio_file_uri for chunk_id {chunk_id}: {e}")
Expand All @@ -49,10 +52,10 @@ def _fetch_chunk_data(chunk_id: str) -> tuple[str, str] | None:
def _generate_audio_url(audio_file_uri: str) -> str | None:
"""
Generates a signed URL for the specified audio file.

Args:
audio_file_uri: The URI of the audio file to sign.

Returns:
The signed URL as a string if successful, or None if signing fails.
"""
Expand All @@ -68,7 +71,7 @@ def _generate_audio_url(audio_file_uri: str) -> str | None:
def _should_skip_diarization(project_language: str) -> bool:
"""
Determines whether diarization should be skipped for a given project language.

Returns True if diarization is disabled for non-English languages based on configuration; otherwise, returns False.
"""
if DISABLE_MULTILINGUAL_DIARIZATION and project_language != "en":
Expand All @@ -80,11 +83,11 @@ def _should_skip_diarization(project_language: str) -> bool:
def _submit_diarization_job(audio_url: str, project_language: str) -> tuple[str, str] | None:
"""
Submits an audio diarization job to RunPod using the provided audio URL and project language.

Args:
audio_url: The signed URL of the audio file to be processed.
project_language: The language code associated with the project.

Returns:
A tuple containing the job ID and the job status link if submission is successful, or None if the request fails.
"""
Expand All @@ -98,7 +101,7 @@ def _submit_diarization_job(audio_url: str, project_language: str) -> tuple[str,
"Authorization": f"Bearer {api_key}",
}
data = {"input": {"audio": audio_url, "language": project_language}}

try:
logger.debug(f"Sending POST to {base_url}/run with data: {data}")
response = requests.post(f"{base_url}/run", headers=headers, json=data, timeout=timeout)
Expand All @@ -115,11 +118,11 @@ def _submit_diarization_job(audio_url: str, project_language: str) -> tuple[str,
def _poll_job_status(job_status_link: str, headers: dict) -> dict | None:
"""
Retrieves the current status of a diarization job from the provided status link.

Args:
job_status_link: The URL to poll for job status.
headers: HTTP headers to include in the request.

Returns:
The JSON response containing job status information, or None if the request fails.
"""
Expand All @@ -136,7 +139,7 @@ def _poll_job_status(job_status_link: str, headers: dict) -> dict | None:
def _update_chunk_with_results(chunk_id: str, dirz_response_data: dict) -> None:
"""
Updates a conversation chunk in Directus with diarization analysis results.

Args:
chunk_id: The ID of the conversation chunk to update.
dirz_response_data: Dictionary containing diarization metrics and results to store.
Expand All @@ -145,7 +148,7 @@ def _update_chunk_with_results(chunk_id: str, dirz_response_data: dict) -> None:
cross_talk_instances = dirz_response_data.get("cross_talk_instances")
silence_ratio = dirz_response_data.get("silence_ratio")
joined_diarization = dirz_response_data.get("joined_diarization")

directus.update_item(
"conversation_chunk",
chunk_id,
Expand All @@ -162,7 +165,7 @@ def _update_chunk_with_results(chunk_id: str, dirz_response_data: dict) -> None:
def _cancel_job_on_timeout(job_id: str) -> None:
"""
Cancels a diarization job on RunPod if it has exceeded the allowed processing time.

Logs a warning before attempting cancellation and logs an error if the cancellation fails.
"""
base_url = RUNPOD_DIARIZATION_BASE_URL
Expand All @@ -171,7 +174,7 @@ def _cancel_job_on_timeout(job_id: str) -> None:
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}

try:
cancel_endpoint = f"{base_url}/cancel/{job_id}"
logger.warning(f"Timeout reached. Cancelling diarization job {job_id} at {cancel_endpoint}")
Expand All @@ -187,10 +190,14 @@ def get_runpod_diarization(
) -> None:
"""
Orchestrates the diarization process for a given chunk by submitting an audio diarization job to RunPod, polling for completion within a timeout, and updating Directus with the results or canceling the job if it times out.

Args:
chunk_id: The identifier of the audio chunk to process.
"""
if not ENABLE_RUNPOD_DIARIZATION:
logger.debug("Skipping diarization because ENABLE_RUNPOD_DIARIZATION is disabled")
return None

# Fetch chunk data
chunk_data = _fetch_chunk_data(chunk_id)
if not chunk_data:
Expand Down Expand Up @@ -219,24 +226,28 @@ def get_runpod_diarization(
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}

start_time = time.time()
while time.time() - start_time < timeout:
response_data = _poll_job_status(job_status_link, headers)
if response_data:
status = response_data.get("status")
logger.debug(f"Job {job_id} status: {status}")

if status == "COMPLETED":
dirz_response_data = response_data.get("output")
if dirz_response_data:
logger.info(f"Diarization job {job_id} completed. Updating chunk {chunk_id} with results.")
logger.info(
f"Diarization job {job_id} completed. Updating chunk {chunk_id} with results."
)
_update_chunk_with_results(chunk_id, dirz_response_data)
return
else:
logger.warning(f"Diarization job {job_id} completed but no output data received.")
logger.warning(
f"Diarization job {job_id} completed but no output data received."
)
return
Comment on lines 237 to 249
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Handle failure states; don’t spin until timeout.

If RunPod returns FAILED/CANCELED, bail early and optionally annotate the chunk.

         if response_data:
             status = response_data.get("status")
             logger.debug(f"Job {job_id} status: {status}")
 
             if status == "COMPLETED":
                 dirz_response_data = response_data.get("output")
                 if dirz_response_data:
                     logger.info(
                         f"Diarization job {job_id} completed. Updating chunk {chunk_id} with results."
                     )
                     _update_chunk_with_results(chunk_id, dirz_response_data)
                     return
                 else:
                     logger.warning(
                         f"Diarization job {job_id} completed but no output data received."
                     )
                     return
+            elif status in {"FAILED", "CANCELED", "CANCELLED", "ERROR"}:
+                logger.warning(f"Diarization job {job_id} ended with status={status}")
+                return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if status == "COMPLETED":
dirz_response_data = response_data.get("output")
if dirz_response_data:
logger.info(f"Diarization job {job_id} completed. Updating chunk {chunk_id} with results.")
logger.info(
f"Diarization job {job_id} completed. Updating chunk {chunk_id} with results."
)
_update_chunk_with_results(chunk_id, dirz_response_data)
return
else:
logger.warning(f"Diarization job {job_id} completed but no output data received.")
logger.warning(
f"Diarization job {job_id} completed but no output data received."
)
return
if status == "COMPLETED":
dirz_response_data = response_data.get("output")
if dirz_response_data:
logger.info(
f"Diarization job {job_id} completed. Updating chunk {chunk_id} with results."
)
_update_chunk_with_results(chunk_id, dirz_response_data)
return
else:
logger.warning(
f"Diarization job {job_id} completed but no output data received."
)
return
elif status in {"FAILED", "CANCELED", "CANCELLED", "ERROR"}:
logger.warning(f"Diarization job {job_id} ended with status={status}")
return
🤖 Prompt for AI Agents
In echo/server/dembrane/conversation_health.py around lines 237 to 249, the code
only handles the "COMPLETED" case and does not bail early on RunPod failure
states; add explicit checks for status == "FAILED" and status == "CANCELED" that
log a warning or error, annotate the chunk to mark it as failed/canceled (use
the existing chunk update/annotation helper or add a small helper like
_mark_chunk_failed_with_reason), and then return immediately so we don't spin
until timeout; ensure the log includes job_id, chunk_id and the
response_data/error reason and that the chunk annotation persists so callers
know the diarization failed.


time.sleep(3)

# Timeout: cancel the job
Expand All @@ -254,6 +265,10 @@ def get_health_status(
"""
Get the health status of conversations.
"""
if not ENABLE_RUNPOD_DIARIZATION:
logger.debug("Skipping diarization because ENABLE_RUNPOD_DIARIZATION is disabled")
return {}

if not project_ids and not conversation_ids:
raise ValueError("Either project_ids or conversation_ids must be provided")

Expand Down Expand Up @@ -325,7 +340,7 @@ def _get_timebound_conversation_chunks(
},
},
)
try:
try:
response = response[:max_chunks_for_conversation]
aggregated_response.extend(_flatten_response(response))
except Exception as e:
Expand Down
3 changes: 3 additions & 0 deletions echo/server/dembrane/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ def save_to_s3_from_file_like(


def get_signed_url(file_name: str, expires_in_seconds: int = 3600) -> str:
"""
WARNING: this will also "get fake signed urls" for files that don't exist
"""
Comment on lines +144 to +146
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Don’t just warn—give callers a way to fail fast for missing objects.

Presigned URLs are generated without existence checks. Offer an opt‑in existence check to prevent queuing downstream work with dead links.

Apply within this function:

-def get_signed_url(file_name: str, expires_in_seconds: int = 3600) -> str:
+def get_signed_url(file_name: str, expires_in_seconds: int = 3600, verify_exists: bool = False) -> str:
     """
     WARNING: this will also "get fake signed urls" for files that don't exist
     """
-    return s3_client.generate_presigned_url(
+    key = get_sanitized_s3_key(file_name)
+    if verify_exists:
+        # HEAD check so callers can opt-in to strict behavior
+        s3_client.head_object(Bucket=STORAGE_S3_BUCKET, Key=key)
+    return s3_client.generate_presigned_url(
         "get_object",
-        Params={"Bucket": STORAGE_S3_BUCKET, "Key": get_sanitized_s3_key(file_name)},
+        Params={"Bucket": STORAGE_S3_BUCKET, "Key": key},
         ExpiresIn=expires_in_seconds,
     )

And add the import up top:

-from botocore.response import StreamingBody
+from botocore.response import StreamingBody
+from botocore.exceptions import ClientError  # optional: catch/translate on HEAD failures

Would you like me to wire this stricter mode where we generate signed URLs for diarization/transcription?

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""
WARNING: this will also "get fake signed urls" for files that don't exist
"""
from botocore.response import StreamingBody
from botocore.exceptions import ClientError # optional: catch/translate on HEAD failures
def get_signed_url(file_name: str, expires_in_seconds: int = 3600, verify_exists: bool = False) -> str:
"""
WARNING: this will also "get fake signed urls" for files that don't exist
"""
key = get_sanitized_s3_key(file_name)
if verify_exists:
# HEAD check so callers can opt-in to strict behavior
s3_client.head_object(Bucket=STORAGE_S3_BUCKET, Key=key)
return s3_client.generate_presigned_url(
"get_object",
Params={"Bucket": STORAGE_S3_BUCKET, "Key": key},
ExpiresIn=expires_in_seconds,
)
🤖 Prompt for AI Agents
In echo/server/dembrane/s3.py around lines 144 to 146, the docstring warns but
the function still generates presigned URLs for objects that may not exist; add
an opt-in parameter (e.g., verify_exists: bool = False) to this function
signature, and when True perform an S3 existence check (head_object) before
generating a presigned URL; if the head_object call returns a 404/NotFound raise
a clear exception so callers can fail fast and avoid downstream dead links. Also
add the necessary import at the top (e.g., from botocore.exceptions import
ClientError) and handle ClientError to distinguish missing objects vs other
errors; ensure the new behavior is documented in the function docstring and unit
tests updated to cover both verify_exists True/False.

return s3_client.generate_presigned_url(
"get_object",
Params={"Bucket": STORAGE_S3_BUCKET, "Key": get_sanitized_s3_key(file_name)},
Expand Down
33 changes: 25 additions & 8 deletions echo/server/dembrane/service/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,35 @@
project = project_service.get_by_id_or_raise(project_id)
"""

# Import service classes
from .file import get_file_service
from .event import EventService
from .project import ProjectService
from .conversation import ConversationService
from .file import FileServiceException, get_file_service
from .project import ProjectService, ProjectServiceException, ProjectNotFoundException
from .conversation import (
ConversationService,
ConversationServiceException,
ConversationNotFoundException,
ConversationChunkNotFoundException,
ConversationNotOpenForParticipationException,
)

# Create service instances without circular dependencies
file_service = get_file_service()
event_service = EventService()
project_service = ProjectService()
conversation_service = ConversationService(
file_service=file_service,
event_service=event_service,
project_service=project_service,
)

exceptions = {
"file": {
"FileServiceException": FileServiceException,
},
"conversation": {
"ConversationChunkNotFoundException": ConversationChunkNotFoundException,
"ConversationNotFoundException": ConversationNotFoundException,
"ConversationNotOpenForParticipationException": ConversationNotOpenForParticipationException,
"ConversationServiceException": ConversationServiceException,
},
"project": {
"ProjectNotFoundException": ProjectNotFoundException,
"ProjectServiceException": ProjectServiceException,
},
}
Comment on lines +35 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Public API polish: export surface explicitly.

Optional: define all so downstreams get a stable import surface for services and exceptions.

Example:

 exceptions = {
   ...
 }
+
+__all__ = [
+  "file_service", "project_service", "conversation_service",
+  "FileServiceException",
+  "ProjectService", "ProjectServiceException", "ProjectNotFoundException",
+  "ConversationService", "ConversationServiceException",
+  "ConversationNotFoundException", "ConversationChunkNotFoundException",
+  "ConversationNotOpenForParticipationException",
+  "exceptions",
+]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exceptions = {
"file": {
"FileServiceException": FileServiceException,
},
"conversation": {
"ConversationChunkNotFoundException": ConversationChunkNotFoundException,
"ConversationNotFoundException": ConversationNotFoundException,
"ConversationNotOpenForParticipationException": ConversationNotOpenForParticipationException,
"ConversationServiceException": ConversationServiceException,
},
"project": {
"ProjectNotFoundException": ProjectNotFoundException,
"ProjectServiceException": ProjectServiceException,
},
}
exceptions = {
"file": {
"FileServiceException": FileServiceException,
},
"conversation": {
"ConversationChunkNotFoundException": ConversationChunkNotFoundException,
"ConversationNotFoundException": ConversationNotFoundException,
"ConversationNotOpenForParticipationException": ConversationNotOpenForParticipationException,
"ConversationServiceException": ConversationServiceException,
},
"project": {
"ProjectNotFoundException": ProjectNotFoundException,
"ProjectServiceException": ProjectServiceException,
},
}
__all__ = [
"file_service", "project_service", "conversation_service",
"FileServiceException",
"ProjectService", "ProjectServiceException", "ProjectNotFoundException",
"ConversationService", "ConversationServiceException",
"ConversationNotFoundException", "ConversationChunkNotFoundException",
"ConversationNotOpenForParticipationException",
"exceptions",
]
🤖 Prompt for AI Agents
In echo/server/dembrane/service/__init__.py around lines 35-49, add an explicit
module export surface by defining __all__ to include the publicly intended names
(at minimum the exceptions mapping and the top-level service classes); e.g. set
__all__ = ["exceptions", "FileService", "ConversationService", "ProjectService"]
so downstream code importing * gets a stable API surface; adjust the list to
match the actual service class names defined in this package.

Loading