fix: integrations (#2642)

This pull request adds support for recursive folder retrieval in the
`get_files_folder_user_sync` method. Previously, the method only
retrieved files from the specified folder, but now it can also retrieve
files from all subfolders recursively. This enhancement improves the
functionality and flexibility of the method, allowing for more
comprehensive file retrieval in sync operations.
This commit is contained in:
Stan Girard 2024-06-08 11:55:11 +02:00 committed by GitHub
parent ca6341372d
commit 47c6e24bf1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 457 additions and 78 deletions

View File

@ -93,6 +93,7 @@ class SyncFileInput(BaseModel):
syncs_active_id: int syncs_active_id: int
last_modified: str last_modified: str
brain_id: str brain_id: str
supported: Optional[bool] = True
class SyncFileUpdateInput(BaseModel): class SyncFileUpdateInput(BaseModel):
@ -103,4 +104,5 @@ class SyncFileUpdateInput(BaseModel):
last_modified (datetime.datetime): The updated last modified date and time. last_modified (datetime.datetime): The updated last modified date and time.
""" """
last_modified: str last_modified: Optional[str] = None
supported: Optional[bool] = None

View File

@ -1,8 +1,16 @@
from datetime import datetime from datetime import datetime
from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
class SyncsUser(BaseModel):
id: int
user_id: str
name: str
provider: str
class SyncsActive(BaseModel): class SyncsActive(BaseModel):
id: int id: int
name: str name: str
@ -12,6 +20,7 @@ class SyncsActive(BaseModel):
last_synced: datetime last_synced: datetime
sync_interval_minutes: int sync_interval_minutes: int
brain_id: str brain_id: str
syncs_user: Optional[SyncsUser]
class SyncsFiles(BaseModel): class SyncsFiles(BaseModel):
@ -20,3 +29,4 @@ class SyncsFiles(BaseModel):
syncs_active_id: int syncs_active_id: int
last_modified: str last_modified: str
brain_id: str brain_id: str
supported: bool

View File

@ -64,7 +64,10 @@ class Sync(SyncInterface):
""" """
logger.info("Retrieving active syncs for user_id: %s", user_id) logger.info("Retrieving active syncs for user_id: %s", user_id)
response = ( response = (
self.db.from_("syncs_active").select("*").eq("user_id", user_id).execute() self.db.from_("syncs_active")
.select("*, syncs_user(*)")
.eq("user_id", user_id)
.execute()
) )
if response.data: if response.data:
logger.info("Active syncs retrieved successfully: %s", response.data) logger.info("Active syncs retrieved successfully: %s", response.data)

View File

@ -81,9 +81,9 @@ class SyncFiles(SyncFileInterface):
sync_file_id, sync_file_id,
sync_file_input, sync_file_input,
) )
self.db.from_("syncs_files").update(sync_file_input.model_dump()).eq( self.db.from_("syncs_files").update(
"id", sync_file_id sync_file_input.model_dump(exclude_unset=True)
).execute() ).eq("id", sync_file_id).execute()
logger.info("Sync file updated successfully") logger.info("Sync file updated successfully")
def delete_sync_file(self, sync_file_id: int): def delete_sync_file(self, sync_file_id: int):

View File

@ -45,7 +45,11 @@ class SyncUserInterface(ABC):
@abstractmethod @abstractmethod
def get_files_folder_user_sync( def get_files_folder_user_sync(
self, sync_active_id: int, user_id: str, folder_id: int = None self,
sync_active_id: int,
user_id: str,
folder_id: int = None,
recursive: bool = False,
): ):
pass pass

View File

@ -159,7 +159,7 @@ class SyncUser(SyncUserInterface):
logger.info("Sync user updated successfully") logger.info("Sync user updated successfully")
def get_files_folder_user_sync( def get_files_folder_user_sync(
self, sync_active_id: int, user_id: str, folder_id: str = None self, sync_active_id: int, user_id: str, folder_id: str = None, recursive: bool = False
): ):
""" """
Retrieve files from a user's sync folder, either from Google Drive or Azure. Retrieve files from a user's sync folder, either from Google Drive or Azure.
@ -195,10 +195,12 @@ class SyncUser(SyncUserInterface):
provider = sync_user["provider"].lower() provider = sync_user["provider"].lower()
if provider == "google": if provider == "google":
logger.info("Getting files for Google sync") logger.info("Getting files for Google sync")
return get_google_drive_files(sync_user["credentials"], folder_id) return {
"files": get_google_drive_files(sync_user["credentials"], folder_id)
}
elif provider == "azure": elif provider == "azure":
logger.info("Getting files for Azure sync") logger.info("Getting files for Azure sync")
return list_azure_files(sync_user["credentials"], folder_id) return {"files": list_azure_files(sync_user["credentials"], folder_id, recursive)}
else: else:
logger.warning("No sync found for provider: %s", sync_user["provider"]) logger.warning("No sync found for provider: %s", sync_user["provider"], recursive)
return "No sync found" return "No sync found"

View File

@ -36,7 +36,7 @@ class SyncUserService:
def get_sync_user_by_state(self, state: dict): def get_sync_user_by_state(self, state: dict):
return self.repository.get_sync_user_by_state(state) return self.repository.get_sync_user_by_state(state)
def get_sync_user_by_id(self, sync_id: int): def get_sync_user_by_id(self, sync_id: int):
return self.repository.get_sync_user_by_id(sync_id) return self.repository.get_sync_user_by_id(sync_id)
@ -46,10 +46,14 @@ class SyncUserService:
return self.repository.update_sync_user(sync_user_id, state, sync_user_input) return self.repository.update_sync_user(sync_user_id, state, sync_user_input)
def get_files_folder_user_sync( def get_files_folder_user_sync(
self, sync_active_id: int, user_id: str, folder_id: str = None self,
sync_active_id: int,
user_id: str,
folder_id: str = None,
recursive: bool = False,
): ):
return self.repository.get_files_folder_user_sync( return self.repository.get_files_folder_user_sync(
sync_active_id, user_id, folder_id sync_active_id, user_id, folder_id, recursive
) )

View File

@ -16,7 +16,10 @@ from modules.sync.dto.inputs import (
) )
from modules.sync.repository.sync_files import SyncFiles from modules.sync.repository.sync_files import SyncFiles
from modules.sync.service.sync_service import SyncService, SyncUserService from modules.sync.service.sync_service import SyncService, SyncUserService
from modules.sync.utils.list_files import get_google_drive_files from modules.sync.utils.list_files import (
get_google_drive_files,
get_google_drive_files_by_id,
)
from modules.sync.utils.upload import upload_file from modules.sync.utils.upload import upload_file
from modules.upload.service.upload_file import check_file_exists from modules.upload.service.upload_file import check_file_exists
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
@ -131,13 +134,15 @@ class GoogleSyncUtils(BaseModel):
filename=file_name, filename=file_name,
) )
await upload_file(to_upload_file, brain_id, current_user) # type: ignore
# Check if the file already exists in the database # Check if the file already exists in the database
existing_files = self.sync_files_repo.get_sync_files(sync_active_id) existing_files = self.sync_files_repo.get_sync_files(sync_active_id)
existing_file = next( existing_file = next(
(f for f in existing_files if f.path == file_name), None (f for f in existing_files if f.path == file_name), None
) )
supported = False
if (existing_file and existing_file.supported) or not existing_file:
supported = True
await upload_file(to_upload_file, brain_id, current_user) # type: ignore
if existing_file: if existing_file:
# Update the existing file record # Update the existing file record
@ -145,6 +150,7 @@ class GoogleSyncUtils(BaseModel):
existing_file.id, existing_file.id,
SyncFileUpdateInput( SyncFileUpdateInput(
last_modified=modified_time, last_modified=modified_time,
supported=supported,
), ),
) )
else: else:
@ -155,6 +161,7 @@ class GoogleSyncUtils(BaseModel):
syncs_active_id=sync_active_id, syncs_active_id=sync_active_id,
last_modified=modified_time, last_modified=modified_time,
brain_id=brain_id, brain_id=brain_id,
supported=supported,
) )
) )
@ -164,6 +171,30 @@ class GoogleSyncUtils(BaseModel):
"An error occurred while downloading Google Drive files: %s", "An error occurred while downloading Google Drive files: %s",
error, error,
) )
# Check if the file already exists in the database
existing_files = self.sync_files_repo.get_sync_files(sync_active_id)
existing_file = next(
(f for f in existing_files if f.path == file["name"]), None
)
# Update the existing file record
if existing_file:
self.sync_files_repo.update_sync_file(
existing_file.id,
SyncFileUpdateInput(
supported=False,
),
)
else:
# Create a new file record
self.sync_files_repo.create_sync_file(
SyncFileInput(
path=file["name"],
syncs_active_id=sync_active_id,
last_modified=file["last_modified"],
brain_id=brain_id,
supported=False,
)
)
return {"downloaded_files": downloaded_files} return {"downloaded_files": downloaded_files}
async def sync(self, sync_active_id: int, user_id: str): async def sync(self, sync_active_id: int, user_id: str):
@ -231,12 +262,25 @@ class GoogleSyncUtils(BaseModel):
sync_active_id, sync_active_id,
) )
# Get the folder id from the settings from sync_active
settings = sync_active.get("settings", {}) settings = sync_active.get("settings", {})
folders = settings.get("folders", []) folders = settings.get("folders", [])
files = get_google_drive_files( files_to_download = settings.get("files", [])
sync_user["credentials"], folder_id=folders[0] if folders else None files = []
) if len(folders) > 0:
files = []
for folder in folders:
files.extend(
get_google_drive_files(
sync_user["credentials"],
folder_id=folder,
recursive=True,
)
)
if len(files_to_download) > 0:
files_metadata = get_google_drive_files_by_id(
sync_user["credentials"], files_to_download
)
files = files + files_metadata # type: ignore
if "error" in files: if "error" in files:
logger.error( logger.error(
"Failed to download files from Google Drive for sync_active_id: %s", "Failed to download files from Google Drive for sync_active_id: %s",
@ -249,7 +293,7 @@ class GoogleSyncUtils(BaseModel):
files_to_download = [ files_to_download = [
file file
for file in files.get("files", []) for file in files
if not file["is_folder"] if not file["is_folder"]
and ( and (
( (

View File

@ -1,4 +1,5 @@
import os import os
from typing import List
import msal import msal
import requests import requests
@ -12,13 +13,62 @@ from requests import HTTPError
logger = get_logger(__name__) logger = get_logger(__name__)
def get_google_drive_files(credentials: dict, folder_id: str = None): def get_google_drive_files_by_id(credentials: dict, file_ids: List[str]):
"""
Retrieve files from Google Drive by their IDs.
Args:
credentials (dict): The credentials for accessing Google Drive.
file_ids (list): The list of file IDs to retrieve.
Returns:
list: A list of dictionaries containing the metadata of each file or an error message.
"""
logger.info("Retrieving Google Drive files with file_ids: %s", file_ids)
creds = Credentials.from_authorized_user_info(credentials)
if creds.expired and creds.refresh_token:
creds.refresh(GoogleRequest())
logger.info("Google Drive credentials refreshed")
try:
service = build("drive", "v3", credentials=creds)
files = []
for file_id in file_ids:
result = (
service.files()
.get(fileId=file_id, fields="id, name, mimeType, modifiedTime")
.execute()
)
files.append(
{
"name": result["name"],
"id": result["id"],
"is_folder": result["mimeType"]
== "application/vnd.google-apps.folder",
"last_modified": result["modifiedTime"],
"mime_type": result["mimeType"],
}
)
logger.info("Google Drive files retrieved successfully: %s", len(files))
return files
except HTTPError as error:
logger.error("An error occurred while retrieving Google Drive files: %s", error)
return {"error": f"An error occurred: {error}"}
def get_google_drive_files(
credentials: dict, folder_id: str = None, recursive: bool = False
):
""" """
Retrieve files from Google Drive. Retrieve files from Google Drive.
Args: Args:
credentials (dict): The credentials for accessing Google Drive. credentials (dict): The credentials for accessing Google Drive.
folder_id (str, optional): The folder ID to filter files. Defaults to None. folder_id (str, optional): The folder ID to filter files. Defaults to None.
recursive (bool, optional): If True, fetch files from all subfolders. Defaults to False.
Returns: Returns:
dict: A dictionary containing the list of files or an error message. dict: A dictionary containing the list of files or an error message.
@ -32,34 +82,63 @@ def get_google_drive_files(credentials: dict, folder_id: str = None):
try: try:
service = build("drive", "v3", credentials=creds) service = build("drive", "v3", credentials=creds)
query = f"'{folder_id}' in parents" if folder_id else None if folder_id:
results = ( query = f"'{folder_id}' in parents"
service.files() else:
.list( query = "'root' in parents or sharedWithMe"
q=query, page_token = None
pageSize=10, files = []
fields="nextPageToken, files(id, name, mimeType, modifiedTime)",
while True:
results = (
service.files()
.list(
q=query,
pageSize=100,
fields="nextPageToken, files(id, name, mimeType, modifiedTime)",
pageToken=page_token,
)
.execute()
) )
.execute() items = results.get("files", [])
)
items = results.get("files", [])
if not items: if not items:
logger.info("No files found in Google Drive") logger.info("No files found in Google Drive")
return {"files": "No files found."} break
files = [ for item in items:
{ files.append(
"name": item["name"], {
"id": item["id"], "name": item["name"],
"is_folder": item["mimeType"] == "application/vnd.google-apps.folder", "id": item["id"],
"last_modified": item["modifiedTime"], "is_folder": item["mimeType"]
"mime_type": item["mimeType"], == "application/vnd.google-apps.folder",
} "last_modified": item["modifiedTime"],
for item in items "mime_type": item["mimeType"],
] }
logger.info("Google Drive files retrieved successfully: %s", files) )
return {"files": files}
# If recursive is True and the item is a folder, get files from the folder
if item["name"] == "Monotype":
logger.warning(item)
if (
recursive
and item["mimeType"] == "application/vnd.google-apps.folder"
):
logger.warning(
"Calling Recursive for folder: %s",
item["name"],
)
files.extend(
get_google_drive_files(credentials, item["id"], recursive)
)
page_token = results.get("nextPageToken", None)
if page_token is None:
break
logger.info("Google Drive files retrieved successfully: %s", len(files))
return files
except HTTPError as error: except HTTPError as error:
logger.error("An error occurred while retrieving Google Drive files: %s", error) logger.error("An error occurred while retrieving Google Drive files: %s", error)
return {"error": f"An error occurred: {error}"} return {"error": f"An error occurred: {error}"}
@ -103,7 +182,17 @@ def get_azure_headers(token_data):
} }
def list_azure_files(credentials, folder_id=None): def list_azure_files(credentials, folder_id=None, recursive=False):
def fetch_files(endpoint, headers):
response = requests.get(endpoint, headers=headers)
if response.status_code == 401:
token_data = refresh_azure_token(credentials)
headers = get_azure_headers(token_data)
response = requests.get(endpoint, headers=headers)
if response.status_code != 200:
return {"error": response.text}
return response.json().get("value", [])
token_data = get_azure_token_data(credentials) token_data = get_azure_token_data(credentials)
headers = get_azure_headers(token_data) headers = get_azure_headers(token_data)
endpoint = f"https://graph.microsoft.com/v1.0/me/drive/root/children" endpoint = f"https://graph.microsoft.com/v1.0/me/drive/root/children"
@ -111,28 +200,76 @@ def list_azure_files(credentials, folder_id=None):
endpoint = ( endpoint = (
f"https://graph.microsoft.com/v1.0/me/drive/items/{folder_id}/children" f"https://graph.microsoft.com/v1.0/me/drive/items/{folder_id}/children"
) )
response = requests.get(endpoint, headers=headers)
if response.status_code == 401: items = fetch_files(endpoint, headers)
token_data = refresh_azure_token(credentials)
headers = get_azure_headers(token_data)
response = requests.get(endpoint, headers=headers)
if response.status_code != 200:
return {"error": response.text}
items = response.json().get("value", [])
if not items: if not items:
logger.info("No files found in Azure Drive") logger.info("No files found in Azure Drive")
return {"files": "No files found."} return []
files = [ files = []
{ for item in items:
file_data = {
"name": item["name"], "name": item["name"],
"id": item["id"], "id": item["id"],
"is_folder": "folder" in item, "is_folder": "folder" in item,
"last_modified": item["lastModifiedDateTime"], "last_modified": item["lastModifiedDateTime"],
"mime_type": item.get("file", {}).get("mimeType", "folder"), "mime_type": item.get("file", {}).get("mimeType", "folder"),
} }
for item in items files.append(file_data)
]
logger.info("Azure Drive files retrieved successfully: %s", files) # If recursive option is enabled and the item is a folder, fetch files from it
return {"files": files} if recursive and file_data["is_folder"]:
folder_files = list_azure_files(
credentials, folder_id=file_data["id"], recursive=True
)
files.extend(folder_files)
logger.info("Azure Drive files retrieved successfully: %s", len(files))
return files
def get_azure_files_by_id(credentials: dict, file_ids: List[str]):
"""
Retrieve files from Azure Drive by their IDs.
Args:
credentials (dict): The credentials for accessing Azure Drive.
file_ids (list): The list of file IDs to retrieve.
Returns:
list: A list of dictionaries containing the metadata of each file or an error message.
"""
logger.info("Retrieving Azure Drive files with file_ids: %s", file_ids)
token_data = get_azure_token_data(credentials)
headers = get_azure_headers(token_data)
files = []
for file_id in file_ids:
endpoint = f"https://graph.microsoft.com/v1.0/me/drive/items/{file_id}"
response = requests.get(endpoint, headers=headers)
if response.status_code == 401:
token_data = refresh_azure_token(credentials)
headers = get_azure_headers(token_data)
response = requests.get(endpoint, headers=headers)
if response.status_code != 200:
logger.error(
"An error occurred while retrieving Azure Drive files: %s",
response.text,
)
return {"error": response.text}
result = response.json()
files.append(
{
"name": result["name"],
"id": result["id"],
"is_folder": "folder" in result,
"last_modified": result["lastModifiedDateTime"],
"mime_type": result.get("file", {}).get("mimeType", "folder"),
}
)
logger.info("Azure Drive files retrieved successfully: %s", len(files))
return files

View File

@ -15,7 +15,7 @@ from modules.sync.dto.inputs import (
) )
from modules.sync.repository.sync_files import SyncFiles from modules.sync.repository.sync_files import SyncFiles
from modules.sync.service.sync_service import SyncService, SyncUserService from modules.sync.service.sync_service import SyncService, SyncUserService
from modules.sync.utils.list_files import list_azure_files from modules.sync.utils.list_files import get_azure_files_by_id, list_azure_files
from modules.sync.utils.upload import upload_file from modules.sync.utils.upload import upload_file
from modules.upload.service.upload_file import check_file_exists from modules.upload.service.upload_file import check_file_exists
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
@ -75,9 +75,9 @@ class AzureSyncUtils(BaseModel):
logger.info("Downloading Azure files with metadata: %s", files) logger.info("Downloading Azure files with metadata: %s", files)
headers = self.get_headers(token_data) headers = self.get_headers(token_data)
try: downloaded_files = []
downloaded_files = [] for file in files:
for file in files: try:
file_id = file["id"] file_id = file["id"]
file_name = file["name"] file_name = file["name"]
modified_time = file["last_modified"] modified_time = file["last_modified"]
@ -127,20 +127,24 @@ class AzureSyncUtils(BaseModel):
filename=file_name, filename=file_name,
) )
await upload_file(to_upload_file, brain_id, current_user)
# Check if the file already exists in the database # Check if the file already exists in the database
existing_files = self.sync_files_repo.get_sync_files(sync_active_id) existing_files = self.sync_files_repo.get_sync_files(sync_active_id)
existing_file = next( existing_file = next(
(f for f in existing_files if f.path == file_name), None (f for f in existing_files if f.path == file_name), None
) )
supported = False
if (existing_file and existing_file.supported) or not existing_file:
supported = True
await upload_file(to_upload_file, brain_id, current_user)
if existing_file: if existing_file:
# Update the existing file record # Update the existing file record
self.sync_files_repo.update_sync_file( self.sync_files_repo.update_sync_file(
existing_file.id, existing_file.id,
SyncFileUpdateInput( SyncFileUpdateInput(
last_modified=modified_time, last_modified=modified_time,
supported=supported,
), ),
) )
else: else:
@ -151,14 +155,40 @@ class AzureSyncUtils(BaseModel):
syncs_active_id=sync_active_id, syncs_active_id=sync_active_id,
last_modified=modified_time, last_modified=modified_time,
brain_id=brain_id, brain_id=brain_id,
supported=supported,
) )
) )
downloaded_files.append(file_name) downloaded_files.append(file_name)
return {"downloaded_files": downloaded_files} except Exception as error:
except Exception as error: logger.error(
logger.error("An error occurred while downloading Azure files: %s", error) "An error occurred while downloading Azure files: %s", error
return {"error": f"An error occurred: {error}"} )
# Check if the file already exists in the database
existing_files = self.sync_files_repo.get_sync_files(sync_active_id)
existing_file = next(
(f for f in existing_files if f.path == file["name"]), None
)
# Update the existing file record
if existing_file:
self.sync_files_repo.update_sync_file(
existing_file.id,
SyncFileUpdateInput(
supported=False,
),
)
else:
# Create a new file record
self.sync_files_repo.create_sync_file(
SyncFileInput(
path=file["name"],
syncs_active_id=sync_active_id,
last_modified=file["last_modified"],
brain_id=brain_id,
supported=False,
)
)
return {"downloaded_files": downloaded_files}
async def sync(self, sync_active_id: int, user_id: str): async def sync(self, sync_active_id: int, user_id: str):
""" """
@ -228,9 +258,25 @@ class AzureSyncUtils(BaseModel):
# Get the folder id from the settings from sync_active # Get the folder id from the settings from sync_active
settings = sync_active.get("settings", {}) settings = sync_active.get("settings", {})
folders = settings.get("folders", []) folders = settings.get("folders", [])
files = list_azure_files( files_to_download = settings.get("files", [])
sync_user["credentials"], folder_id=folders[0] if folders else None files = []
) if len(folders) > 0:
files = []
for folder in folders:
files.extend(
list_azure_files(
sync_user["credentials"],
folder_id=folder,
recursive=True,
)
)
if len(files_to_download) > 0:
files_metadata = get_azure_files_by_id(
sync_user["credentials"],
files_to_download,
)
files = files + files_metadata # type: ignore
if "error" in files: if "error" in files:
logger.error( logger.error(
"Failed to download files from Azure for sync_active_id: %s", "Failed to download files from Azure for sync_active_id: %s",
@ -244,10 +290,11 @@ class AzureSyncUtils(BaseModel):
if last_synced if last_synced
else None else None
) )
logger.info("Files retrieved from Azure: %s", files.get("files", [])) logger.info("Files retrieved from Azure: %s", len(files))
logger.info("Files retrieved from Azure: %s", files)
files_to_download = [ files_to_download = [
file file
for file in files.get("files", []) for file in files
if not file["is_folder"] if not file["is_folder"]
and ( and (
( (

View File

@ -0,0 +1,2 @@
alter table "public"."syncs_files" add column "supported" boolean not null default true;

124
backend/tach.yml Normal file
View File

@ -0,0 +1,124 @@
modules:
- path: <root>
depends_on:
- modules.analytics
- modules.api_key
- modules.assistant
- modules.brain
- modules.chat
- modules.contact_support
- modules.knowledge
- modules.misc
- modules.notification
- modules.onboarding
- modules.prompt
- modules.sync
- modules.upload
- modules.user
strict: false
- path: modules.analytics
depends_on:
- <root>
- modules.brain
strict: false
- path: modules.api_key
depends_on:
- <root>
- modules.user
strict: false
- path: modules.assistant
depends_on:
- <root>
- modules.chat
- modules.contact_support
# - modules.upload
- modules.user
strict: false
- path: modules.authorization
depends_on: []
strict: false
- path: modules.brain
depends_on:
- <root>
- modules.chat
- modules.knowledge
- modules.prompt
- modules.tools
- modules.upload
- modules.user
strict: false
- path: modules.chat
depends_on:
- <root>
- modules.brain
- modules.notification
- modules.prompt
- modules.user
strict: false
- path: modules.contact_support
depends_on:
- <root>
strict: false
- path: modules.ingestion
depends_on: []
strict: false
- path: modules.knowledge
depends_on:
- <root>
- modules.brain
- modules.upload
- modules.user
strict: false
- path: modules.message
depends_on: []
strict: false
- path: modules.misc
depends_on: []
strict: false
- path: modules.notification
depends_on:
- <root>
strict: false
- path: modules.onboarding
depends_on:
- <root>
- modules.user
strict: false
- path: modules.prompt
depends_on:
- <root>
- modules.brain
strict: false
- path: modules.sync
depends_on:
- <root>
- modules.brain
- modules.knowledge
- modules.notification
- modules.upload
- modules.user
strict: false
- path: modules.tools
depends_on:
- <root>
- modules.contact_support
strict: false
- path: modules.upload
depends_on:
- <root>
- modules.brain
- modules.knowledge
- modules.notification
- modules.user
strict: false
- path: modules.user
depends_on:
- <root>
- modules.brain
strict: false
exclude:
- docs
- tests
exact: false
disable_logging: false
ignore_type_checking_imports: false