1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-09 02:42:47 +00:00

feat: init sdoc summary when add summary column (#6449)

* feat: init sdoc summary when add summary column

* feat/meta: add summary single sdoc

* optimize some naming

* add summarize files api

* optimize code

* feat: update code

* feat: optimzie code

* feat: rebase code

* fix/mv: fix invalid path param when create summary

* feat/mv: add batch update summary text func

* feat/mv: add some details in parameter judgment

* feat: optimize code

* feat: delete useless import

* execute return api

* feat/summary: execute file not found situation

* fix: summary

---------

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
cir9no
2024-08-23 17:51:52 +08:00
committed by GitHub
parent 18a96ed607
commit 500b1dbb6b
9 changed files with 220 additions and 13 deletions

View File

@@ -186,6 +186,15 @@ class MetadataManagerAPI {
}; };
return this.req.put(url, params); return this.req.put(url, params);
}; };
// ai
generateSummary = (repoID, filePaths) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/ai/summarize-documents/';
const params = {
file_paths_list: filePaths,
};
return this.req.post(url, params);
};
} }
const metadataAPI = new MetadataManagerAPI(); const metadataAPI = new MetadataManagerAPI();

View File

@@ -8,7 +8,7 @@ import { CellType, isFunction, Z_INDEX, getCellValueByColumn, getColumnOptionNam
import { isCellValueChanged } from '../../../utils/cell-comparer'; import { isCellValueChanged } from '../../../utils/cell-comparer';
import { EVENT_BUS_TYPE } from '../../../constants'; import { EVENT_BUS_TYPE } from '../../../constants';
import Editor from '../editor'; import Editor from '../editor';
import { canEditCell } from '../../../utils/column-utils'; import { canEditCell, getColumnOriginName } from '../../../utils/column-utils';
const NOT_SUPPORT_EDITOR_COLUMN_TYPES = [ const NOT_SUPPORT_EDITOR_COLUMN_TYPES = [
CellType.CTIME, CellType.MTIME, CellType.CREATOR, CellType.LAST_MODIFIER, CellType.FILE_NAME CellType.CTIME, CellType.MTIME, CellType.CREATOR, CellType.LAST_MODIFIER, CellType.FILE_NAME
@@ -124,7 +124,8 @@ class PopupEditorContainer extends React.Component {
getOldRowData = (originalOldCellValue) => { getOldRowData = (originalOldCellValue) => {
const { column } = this.props; const { column } = this.props;
const { key: columnKey, name: columnName, type: columnType } = column; const columnName = getColumnOriginName(column);
const { key: columnKey, type: columnType } = column;
let oldValue = originalOldCellValue; let oldValue = originalOldCellValue;
if (this.getEditor().getOldValue) { if (this.getEditor().getOldValue) {
const original = this.getEditor().getOldValue(); const original = this.getEditor().getOldValue();
@@ -133,7 +134,7 @@ class PopupEditorContainer extends React.Component {
if (columnType === CellType.LONG_TEXT) { if (columnType === CellType.LONG_TEXT) {
oldValue = this.getEditor().getValue(); // long-text cell value need format to {text: '', links: [], ...} oldValue = this.getEditor().getValue(); // long-text cell value need format to {text: '', links: [], ...}
} }
const oldRowData = PRIVATE_COLUMN_KEYS.includes(columnKey) ? { [columnName]: oldValue } : { [columnName]: oldValue } ; const oldRowData = { [columnName]: oldValue };
const originalOldRowData = { [columnKey]: originalOldCellValue }; // { [column.key]: cellValue } const originalOldRowData = { [columnKey]: originalOldCellValue }; // { [column.key]: cellValue }
return { oldRowData, originalOldRowData }; return { oldRowData, originalOldRowData };
}; };

View File

@@ -1,9 +1,11 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PRIVATE_COLUMN_KEY } from '../../_basic'; import { getColumnByKey, PRIVATE_COLUMN_KEY } from '../../_basic';
import { gettext } from '../../utils'; import { gettext } from '../../utils';
import { siteRoot } from '../../../../utils/constants'; import { siteRoot } from '../../../../utils/constants';
import { Utils } from '../../../../utils/utils'; import { Utils } from '../../../../utils/utils';
import { useMetadata } from '../../hooks';
import toaster from '../../../../components/toast';
import './index.css'; import './index.css';
@@ -12,6 +14,7 @@ const OPERATION = {
COPY_SELECTED: 'copy-selected', COPY_SELECTED: 'copy-selected',
OPEN_PARENT_FOLDER: 'open-parent-folder', OPEN_PARENT_FOLDER: 'open-parent-folder',
OPEN_IN_NEW_TAB: 'open-new-tab', OPEN_IN_NEW_TAB: 'open-new-tab',
GENERATE_SUMMARY: 'generate-summary',
}; };
const ContextMenu = ({ const ContextMenu = ({
@@ -22,23 +25,55 @@ const ContextMenu = ({
recordGetterByIndex, recordGetterByIndex,
onClearSelected, onClearSelected,
onCopySelected, onCopySelected,
updateRecords,
}) => { }) => {
const menuRef = useRef(null); const menuRef = useRef(null);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 }); const [position, setPosition] = useState({ top: 0, left: 0 });
const { metadata } = useMetadata();
const options = useMemo(() => { const options = useMemo(() => {
if (!visible) return [];
const permission = window.sfMetadataContext.getPermission(); const permission = window.sfMetadataContext.getPermission();
const isReadonly = permission === 'r'; const isReadonly = permission === 'r';
const { columns } = metadata;
const summaryColumn = getColumnByKey(columns, PRIVATE_COLUMN_KEY.FILE_SUMMARY);
const canModifyRow = window.sfMetadataContext.canModifyRow;
let list = []; let list = [];
if (selectedRange) { if (selectedRange) {
!isReadonly && list.push({ value: OPERATION.CLEAR_SELECTED, label: gettext('Clear selected') }); !isReadonly && list.push({ value: OPERATION.CLEAR_SELECTED, label: gettext('Clear selected') });
list.push({ value: OPERATION.COPY_SELECTED, label: gettext('Copy selected') }); list.push({ value: OPERATION.COPY_SELECTED, label: gettext('Copy selected') });
if (summaryColumn) {
const { topLeft, bottomRight } = selectedRange;
for (let i = topLeft.rowIdx; i <= bottomRight.rowIdx; i++) {
const record = recordGetterByIndex({ isGroupView, groupRecordIndex: topLeft.groupRecordIndex, recordIndex: i });
const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME];
if (Utils.isSdocFile(fileName) && canModifyRow(record)) {
list.push({ value: OPERATION.GENERATE_SUMMARY, label: gettext('Generate summary') });
break;
}
}
}
return list; return list;
} }
if (Object.keys(recordMetrics.idSelectedRecordMap).length > 1) { const selectedRecords = Object.keys(recordMetrics.idSelectedRecordMap);
if (selectedRecords.length > 1) {
if (summaryColumn) {
const isIncludeSdocRecord = selectedRecords.filter(id => {
const record = metadata.id_row_map[id];
if (record) {
const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME];
return Utils.isSdocFile(fileName) && canModifyRow(record);
}
return false;
});
if (isIncludeSdocRecord.length > 0) {
list.push({ value: OPERATION.GENERATE_SUMMARY, label: gettext('Generate summary') });
}
}
return list; return list;
} }
@@ -49,9 +84,15 @@ const ContextMenu = ({
const isFolder = record[PRIVATE_COLUMN_KEY.IS_DIR]; const isFolder = record[PRIVATE_COLUMN_KEY.IS_DIR];
list.push({ value: OPERATION.OPEN_IN_NEW_TAB, label: isFolder ? gettext('Open folder in new tab') : gettext('Open file in new tab') }); list.push({ value: OPERATION.OPEN_IN_NEW_TAB, label: isFolder ? gettext('Open folder in new tab') : gettext('Open file in new tab') });
list.push({ value: OPERATION.OPEN_PARENT_FOLDER, label: gettext('Open parent folder') }); list.push({ value: OPERATION.OPEN_PARENT_FOLDER, label: gettext('Open parent folder') });
if (summaryColumn) {
const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME];
if (Utils.isSdocFile(fileName) && canModifyRow(record)) {
list.push({ value: OPERATION.GENERATE_SUMMARY, label: gettext('Generate summary') });
}
}
return list; return list;
}, [isGroupView, selectedPosition, recordMetrics, selectedRange, recordGetterByIndex]); }, [visible, isGroupView, selectedPosition, recordMetrics, selectedRange, metadata, recordGetterByIndex]);
const handleHide = useCallback((event) => { const handleHide = useCallback((event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) { if (menuRef.current && !menuRef.current.contains(event.target)) {
@@ -89,6 +130,66 @@ const ContextMenu = ({
window.open(url, '_blank'); window.open(url, '_blank');
}, [isGroupView, recordGetterByIndex, selectedPosition]); }, [isGroupView, recordGetterByIndex, selectedPosition]);
const generateSummary = useCallback(() => {
const canModifyRow = window.sfMetadataContext.canModifyRow;
const selectedRecords = Object.keys(recordMetrics.idSelectedRecordMap);
const summaryColumnKey = PRIVATE_COLUMN_KEY.FILE_SUMMARY;
let paths = [];
let idOldRecordData = {};
let idOriginalOldRecordData = {};
if (selectedRange) {
const { topLeft, bottomRight } = selectedRange;
for (let i = topLeft.rowIdx; i <= bottomRight.rowIdx; i++) {
const record = recordGetterByIndex({ isGroupView, groupRecordIndex: topLeft.groupRecordIndex, recordIndex: i });
if (!canModifyRow(record)) continue;
const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME];
if (!Utils.isSdocFile(fileName)) continue;
const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR];
paths.push(Utils.joinPath(parentDir, fileName));
idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] };
idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] };
}
} else if (selectedRecords.length > 0) {
selectedRecords.forEach(recordId => {
const record = metadata.id_row_map[recordId];
const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME];
if (Utils.isSdocFile(fileName) && canModifyRow(record)) {
const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR];
paths.push(Utils.joinPath(parentDir, fileName));
idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] };
idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] };
}
});
} else if (selectedPosition) {
const { groupRecordIndex, rowIdx } = selectedPosition;
const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex: rowIdx });
const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME];
if (Utils.isSdocFile(fileName) && canModifyRow(record)) {
const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR];
paths.push(Utils.joinPath(parentDir, fileName));
idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] };
idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [summaryColumnKey]: record[summaryColumnKey] };
}
}
if (paths.length === 0) return;
window.sfMetadataContext.generateSummary(paths).then(res => {
const updatedRecords = res.data.rows;
let recordIds = [];
let idRecordUpdates = {};
let idOriginalRecordUpdates = {};
updatedRecords.forEach(updatedRecord => {
const { _id: updateRecordId, _summary } = updatedRecord;
recordIds.push(updateRecordId);
idRecordUpdates[updateRecordId] = { [summaryColumnKey]: _summary };
idOriginalRecordUpdates[updateRecordId] = { [summaryColumnKey]: _summary };
});
updateRecords({ recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData });
}).catch(error => {
const errorMessage = gettext('Failed to generate summary');
toaster.danger(errorMessage);
});
}, [isGroupView, selectedRange, selectedPosition, recordMetrics, metadata, recordGetterByIndex, updateRecords]);
const handleOptionClick = useCallback((event, option) => { const handleOptionClick = useCallback((event, option) => {
event.stopPropagation(); event.stopPropagation();
switch (option.value) { switch (option.value) {
@@ -108,12 +209,16 @@ const ContextMenu = ({
onClearSelected && onClearSelected(); onClearSelected && onClearSelected();
break; break;
} }
case OPERATION.GENERATE_SUMMARY: {
generateSummary && generateSummary();
break;
}
default: { default: {
break; break;
} }
} }
setVisible(false); setVisible(false);
}, [onOpenFileInNewTab, onOpenParentFolder, onCopySelected, onClearSelected]); }, [onOpenFileInNewTab, onOpenParentFolder, onCopySelected, onClearSelected, generateSummary]);
const getMenuPosition = (x = 0, y = 0) => { const getMenuPosition = (x = 0, y = 0) => {
let menuStyles = { let menuStyles = {

View File

@@ -610,14 +610,14 @@ class Records extends Component {
}; };
renderRecordsBody = ({ containerWidth }) => { renderRecordsBody = ({ containerWidth }) => {
const { isGroupView, recordGetterByIndex } = this.props; const { isGroupView, recordGetterByIndex, updateRecords } = this.props;
const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state; const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state;
const { columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth } = columnMetrics; const { columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth } = columnMetrics;
const commonProps = { const commonProps = {
...this.props, ...this.props,
columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth, columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth,
recordMetrics, colOverScanStartIdx, colOverScanEndIdx, recordMetrics, colOverScanStartIdx, colOverScanEndIdx,
contextMenu: (<ContextMenu isGroupView={isGroupView} recordGetterByIndex={recordGetterByIndex} />), contextMenu: (<ContextMenu isGroupView={isGroupView} recordGetterByIndex={recordGetterByIndex} updateRecords={updateRecords} />),
hasSelectedRecord: this.hasSelectedRecord(), hasSelectedRecord: this.hasSelectedRecord(),
getScrollLeft: this.getScrollLeft, getScrollLeft: this.getScrollLeft,
getScrollTop: this.getScrollTop, getScrollTop: this.getScrollTop,

View File

@@ -73,6 +73,11 @@ class Context {
return this.metadataAPI.getMetadata(repoID, params); return this.metadataAPI.getMetadata(repoID, params);
}; };
getRecord = (parentDir, fileName) => {
const repoID = this.settings['repoID'];
return this.metadataAPI.getMetadataRecordInfo(repoID, parentDir, fileName);
};
getViews = () => { getViews = () => {
const repoID = this.settings['repoID']; const repoID = this.settings['repoID'];
return this.metadataAPI.listViews(repoID); return this.metadataAPI.listViews(repoID);
@@ -186,6 +191,11 @@ class Context {
// todo // todo
}; };
// ai
generateSummary = (filePaths) => {
const repoID = this.settings['repoID'];
return this.metadataAPI.generateSummary(repoID, filePaths);
};
} }
export default Context; export default Context;

View File

@@ -11,11 +11,13 @@ from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews
from seahub.views import check_folder_permission from seahub.views import check_folder_permission
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, get_sys_columns from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
get_sys_columns, update_docs_summary
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
from seahub.utils.timeutils import datetime_to_isoformat_timestr from seahub.utils.timeutils import datetime_to_isoformat_timestr
from seahub.utils.repo import is_repo_admin from seahub.utils.repo import is_repo_admin
from seahub.ai.utils import get_file_download_token
from pysearpc import SearpcError
from seaserv import seafile_api from seaserv import seafile_api
@@ -781,3 +783,62 @@ class MetadataViewsMoveView(APIView):
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'navigation': results['navigation']}) return Response({'navigation': results['navigation']})
class MetadataSummarizeDocs(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def post(self, request, repo_id):
file_paths_list = request.data.get('file_paths_list', '')
if not file_paths_list or not isinstance(file_paths_list, list):
error_msg = 'file_paths_list should be a non-empty list..'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
if not record or not record.enabled:
error_msg = f'The metadata module is disabled for repo {repo_id}.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
permission = check_folder_permission(request, repo_id, '/')
if permission != 'rw':
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
files_info_list = []
for file_path in file_paths_list:
try:
file_id = seafile_api.get_file_id_by_path(repo_id, file_path)
except SearpcError as e:
logger.error(e)
return api_error(
status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error'
)
if not file_id:
return api_error(
status.HTTP_404_NOT_FOUND, f"File {file_path} not found"
)
if token := get_file_download_token(repo_id, file_id, request.user.username):
files_info_list.append(
{'file_path': file_path, 'download_token': token}
)
else:
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
try:
resp = update_docs_summary(repo_id, files_info_list)
resp_json = resp.json()
except Exception as e:
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response(resp_json, resp.status_code)

View File

@@ -5,7 +5,7 @@ import json
import random import random
from urllib.parse import urljoin from urllib.parse import urljoin
from seahub.settings import SECRET_KEY, SEAFEVENTS_SERVER_URL from seahub.settings import SECRET_KEY, SEAFEVENTS_SERVER_URL, SEAFILE_AI_SECRET_KEY, SEAFILE_AI_SERVER_URL
def add_init_metadata_task(params): def add_init_metadata_task(params):
@@ -63,3 +63,16 @@ def init_metadata(metadata_server_api):
# init sys column # init sys column
sys_columns = get_sys_columns() sys_columns = get_sys_columns()
metadata_server_api.add_columns(METADATA_TABLE.id, sys_columns) metadata_server_api.add_columns(METADATA_TABLE.id, sys_columns)
def update_docs_summary(repo_id, files_info_list):
payload = {'exp': int(time.time()) + 300, }
token = jwt.encode(payload, SEAFILE_AI_SECRET_KEY, algorithm='HS256')
headers = {"Authorization": "Token %s" % token}
url = urljoin(SEAFILE_AI_SERVER_URL, '/api/v1/update-docs-summary')
params = {
'repo_id': repo_id,
'files_info_list': files_info_list,
}
resp = requests.post(url, json=params, headers=headers)
return resp

View File

@@ -899,6 +899,13 @@ METADATA_FILE_TYPES = {
'_audio': ('mp3', 'oga', 'ogg', 'wav', 'flac', 'opus', 'aac', 'au', 'm4a', 'aif', 'aiff', 'wma', 'rm', 'mp1', 'mp2') '_audio': ('mp3', 'oga', 'ogg', 'wav', 'flac', 'opus', 'aac', 'au', 'm4a', 'aif', 'aiff', 'wma', 'rm', 'mp1', 'mp2')
} }
##############################
# seafile ai #
##############################
SEAFILE_AI_SERVER_URL = ''
SEAFILE_AI_SECRET_KEY = ''
d = os.path.dirname d = os.path.dirname
EVENTS_CONFIG_FILE = os.environ.get( EVENTS_CONFIG_FILE = os.environ.get(
'EVENTS_CONFIG_FILE', 'EVENTS_CONFIG_FILE',

View File

@@ -209,7 +209,7 @@ from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView,
Wiki2DuplicatePageView, WikiPageTrashView Wiki2DuplicatePageView, WikiPageTrashView
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \ from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataSummarizeDocs
from seahub.api2.endpoints.user_list import UserListView from seahub.api2.endpoints.user_list import UserListView
@@ -1042,5 +1042,6 @@ if settings.ENABLE_METADATA_MANAGEMENT:
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/$', MetadataViews.as_view(), name='api-v2.1-metadata-views'), re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/$', MetadataViews.as_view(), name='api-v2.1-metadata-views'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/(?P<view_id>[-0-9a-zA-Z]{4})/$', MetadataViewsDetailView.as_view(), name='api-v2.1-metadata-views-detail'), re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/(?P<view_id>[-0-9a-zA-Z]{4})/$', MetadataViewsDetailView.as_view(), name='api-v2.1-metadata-views-detail'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/move-views/$', MetadataViewsMoveView.as_view(), name='api-v2.1-metadata-views-move'), re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/move-views/$', MetadataViewsMoveView.as_view(), name='api-v2.1-metadata-views-move'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/ai/summarize-documents/$', MetadataSummarizeDocs.as_view(), name='api-v2.1-metadata-summarize-documents'),
] ]