Files
ragflow/web/src/utils/api.ts
Preston Percival e8f19aa338 feat(graphrag): fix merge concurrency and add resume-from-checkpoint (#14238)
This PR addresses three related GraphRAG reliability issues that
together allow long-running GraphRAG tasks (10+ hours of LLM extraction)
to be resumed after a crash or pause without re-doing completed work. It
builds on #14096 (per-doc subgraph cache) and extends the same idea to
the resolution and community-detection phases.

Fixes #14236.

## 1. Fix concurrent merge crash

Long GraphRAG runs would crash near the end of entity resolution with:
```
RuntimeError: dictionary keys changed during iteration
```
in `Extractor._merge_graph_nodes`. Two changes:

- `rag/graphrag/general/extractor.py`: snapshot `graph.neighbors(node1)`
via `list(...)` before iterating, so concurrent `add_edge` /
`remove_node` mutations on the shared `nx.Graph` cannot invalidate the
iterator. Also tracks each redirected neighbour in `node0_neighbors` so
a later merged node sharing the same external neighbour takes the
edge-merge branch instead of overwriting via `add_edge`.
- `rag/graphrag/entity_resolution.py`: serialize the merge step with a
dedicated `asyncio.Semaphore(1)`. `nx.Graph` is not thread-safe and
concurrent merges on overlapping neighbourhoods can produce incorrect
results even with the snapshot fix.

## 2. Don't wipe partial graph on pause

Previously the pause / cancel UI path called
`settings.docStoreConn.delete({"knowledge_graph_kwd": [...]}, ...)`,
destroying every subgraph, entity, relation, and graph row.
Re-triggering then started GraphRAG from scratch even though #14096 had
already added `load_subgraph_from_store`.

After main was merged in (which deleted `api/apps/kb_app.py` per
#14394), the pause path now lives on the new REST surface `DELETE
/v1/datasets/<id>/<index_type>`:

- `api/apps/services/dataset_api_service.py`: `delete_index` accepts a
`wipe: bool = True` parameter. When `False` the doc-store rows and
GraphRAG phase markers are left intact and only the running task is
cancelled. Default preserves historical behaviour.
- `api/apps/restful_apis/dataset_api.py`: parses `?wipe=false|0|no|off`
from the query string and forwards it.
- `web/src/utils/api.ts` + `web/src/services/knowledge-service.ts`:
`unbindPipelineTask` appends `?wipe=false` when explicitly false.
- The GraphRAG pause action in
`web/src/pages/dataset/dataset/generate-button/hook.ts` passes `wipe:
false` for `KnowledgeGraph`; raptor is unchanged.

**UX impact:** the pause icon next to a running GraphRAG task no longer
wipes graph data. The only path that still wipes is the explicit Delete
action in `GenerateLogButton` (trash icon behind a confirmation modal).

## 3. Phase-completion markers (`rag/graphrag/phase_markers.py`)

A small Redis-backed marker layer at
`graphrag:phase:{kb_id}:{resolution_done|community_done}` (7-day TTL).
`run_graphrag_for_kb` consults the markers on entry and skips phases
that already completed in a prior run. Markers are cleared automatically
when:
- new docs are merged into the graph (which invalidates prior resolution
and community results),
- `delete_index` wipes the graph, or
- `delete_knowledge_graph` is called.

Redis failures never block a run -- markers are an optimization, not a
gate.

## 4. Idempotent community detection

`extract_community` previously did `delete-then-insert` on
`community_report` rows; a crash mid-insert left the dataset with no
reports. Now report IDs are derived deterministically from `(kb_id,
community.title)`, the existing report IDs are snapshotted before
insert, new rows are written, then only stale rows are pruned. A failure
at any step leaves either the prior or the new report set intact --
never a partial mix.

## 5. Tunable doc-store insert pipeline

The GraphRAG insert loop in `rag/graphrag/utils.py` and the
`community_report` insert in `rag/graphrag/general/index.py` were both
hardcoded to `es_bulk_size = 4` and ran strictly sequentially. On a real
KB this meant 1077 chunks took ~21 minutes for a 100-chunk slice -- pure
round-trip overhead.

- New `insert_chunks_bounded()` helper in `rag/graphrag/utils.py`
batches inserts via a bounded `asyncio.Semaphore`. Same retry / timeout
semantics as the prior loop.
- Defaults: 64 docs per batch, 4 batches in flight (matches the regular
ingest pipeline in `document_service.py`). Tunable per-deployment via
`GRAPHRAG_INSERT_BULK_SIZE` and `GRAPHRAG_INSERT_CONCURRENCY`.
- Both `set_graph` and `extract_community` now use the helper.

This dropped the same 1077-chunk insert from minutes to seconds in local
testing without measurable extra pressure on Infinity (total in-flight
docs ≤ `BULK_SIZE × CONCURRENCY` = 256 by default).

## Tests

- `test/unit_test/rag/graphrag/test_merge_graph_nodes.py` (3 tests):
dense neighbourhood merge, neighbour-snapshot regression, concurrent
serialized merges.
- `test/unit_test/rag/graphrag/test_phase_markers.py` (4 tests): set/has
round-trip, kb-scoped clear, no-op on empty input, graceful Redis
failure.
-
`test/testcases/test_web_api/test_dataset_management/test_dataset_sdk_routes_unit.py`:
new `test_delete_index_wipe_flag_unit` covers `wipe=false` for both
GraphRAG and raptor on the new REST route, and confirms the default
still wipes and clears phase markers.

## Compatibility

- Backward compatible: tasks queued before this change behave
identically (default `wipe=true`, no markers expected).
- No schema/migration changes; all new state lives in Redis.
- New optional REST query param `wipe` on `DELETE
/v1/datasets/<id>/<index_type>`.
- New optional env vars `GRAPHRAG_INSERT_BULK_SIZE` and
`GRAPHRAG_INSERT_CONCURRENCY`; defaults preserve safe behaviour.

## Example of resume

Screenshot below shows a test resuming knowledge graph generation after
applying the concurrency fix and re-deploying.

<img width="521" height="677" alt="image"
src="https://github.com/user-attachments/assets/9ef0d405-cbb3-420d-a1a1-e51f3e7e9b7a"
/>

### Type of change

- [X] Bug Fix (non-breaking change which fixes an issue)
- [ ] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):
2026-05-06 15:01:01 +08:00

346 lines
16 KiB
TypeScript

const webAPI = `/v1`;
const restAPIv1 = `/api/v1`;
export { restAPIv1, webAPI };
export default {
// user
login: `${restAPIv1}/auth/login`,
logout: `${restAPIv1}/auth/logout`,
register: `${restAPIv1}/users`,
setting: `${restAPIv1}/users/me`,
userInfo: `${restAPIv1}/users/me`,
tenantInfo: `${restAPIv1}/users/me/models`,
setTenantInfo: `${restAPIv1}/users/me/models`,
loginChannels: `${restAPIv1}/auth/login/channels`,
loginChannel: (channel: string) => `${restAPIv1}/auth/login/${channel}`,
// team
addTenantUser: (tenantId: string) => `${restAPIv1}/tenants/${tenantId}/users`,
listTenantUser: (tenantId: string) =>
`${restAPIv1}/tenants/${tenantId}/users`,
deleteTenantUser: (tenantId: string) =>
`${restAPIv1}/tenants/${tenantId}/users`,
listTenant: `${restAPIv1}/tenants`,
agreeTenant: (tenantId: string) => `${restAPIv1}/tenants/${tenantId}`,
// llm model
factoriesList: `${webAPI}/llm/factories`,
llmList: `${webAPI}/llm/list`,
myLlm: `${webAPI}/llm/my_llms`,
setApiKey: `${webAPI}/llm/set_api_key`,
addLlm: `${webAPI}/llm/add_llm`,
deleteLlm: `${webAPI}/llm/delete_llm`,
enableLlm: `${webAPI}/llm/enable_llm`,
deleteFactory: `${webAPI}/llm/delete_factory`,
// data source
dataSourceUpdate: (id: string) => `${restAPIv1}/connectors/${id}`,
dataSourceSet: `${restAPIv1}/connectors`,
dataSourceList: `${restAPIv1}/connectors`,
dataSourceDel: (id: string) => `${restAPIv1}/connectors/${id}`,
dataSourceResume: (id: string) => `${restAPIv1}/connectors/${id}/resume`,
dataSourceRebuild: (id: string) => `${restAPIv1}/connectors/${id}/rebuild`,
dataSourceLogs: (id: string) => `${restAPIv1}/connectors/${id}/logs`,
dataSourceDetail: (id: string) => `${restAPIv1}/connectors/${id}`,
googleWebAuthStart: (type: 'google-drive' | 'gmail') =>
`${restAPIv1}/connectors/google/oauth/web/start?type=${type}`,
googleWebAuthResult: (type: 'google-drive' | 'gmail') =>
`${restAPIv1}/connectors/google/oauth/web/result?type=${type}`,
boxWebAuthStart: () => `${restAPIv1}/connectors/box/oauth/web/start`,
boxWebAuthResult: () => `${restAPIv1}/connectors/box/oauth/web/result`,
// plugin
llmTools: `${restAPIv1}/plugin/tools`,
chatsTranscriptions: `${restAPIv1}/chat/audio/transcription`,
// knowledge base
checkEmbedding: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/embedding`,
kbList: `${restAPIv1}/datasets`,
createKb: `${restAPIv1}/datasets`,
updateKb: (datasetId: string) => `${restAPIv1}/datasets/${datasetId}`,
rmKb: `${restAPIv1}/datasets`,
getKbDetail: (datasetId: string) => `${restAPIv1}/datasets/${datasetId}`,
getKnowledgeGraph: (knowledgeId: string) =>
`${restAPIv1}/datasets/${knowledgeId}/graph/search`,
knowledgeGraph: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/graph`,
deleteKnowledgeGraph: (knowledgeId: string) =>
`${restAPIv1}/datasets/${knowledgeId}/graph`,
getMeta: `${restAPIv1}/datasets/metadata/flattened`,
getKnowledgeBasicInfo: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/ingestions/summary`,
// data pipeline log
fetchDataPipelineLog: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/ingestions`,
getPipelineDetail: (datasetId: string, logId: string) =>
`${restAPIv1}/datasets/${datasetId}/ingestions/${logId}`,
fetchPipelineDatasetLogs: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/ingestions`,
runIndex: (datasetId: string, indexType: string) =>
`${restAPIv1}/datasets/${datasetId}/index?type=${indexType.toLowerCase()}`,
traceIndex: (datasetId: string, indexType: string) =>
`${restAPIv1}/datasets/${datasetId}/index?type=${indexType.toLowerCase()}`,
unbindPipelineTask: (datasetId: string, indexType: string, wipe?: boolean) =>
`${restAPIv1}/datasets/${datasetId}/${indexType.toLowerCase()}${wipe === false ? '?wipe=false' : ''}`,
pipelineRerun: `${webAPI}/canvas/rerun`,
getMetaData: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/metadata/summary`,
updateDocumentsMetadata: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents/metadatas`,
kbUpdateMetaData: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/metadata/config`,
documentUpdateMetaDataConfig: (datasetId: string, documentId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents/${documentId}/metadata/config`,
// tags
listTag: (knowledgeId: string) => `${restAPIv1}/datasets/${knowledgeId}/tags`,
listTagByKnowledgeIds: `${restAPIv1}/datasets/tags/aggregation`,
removeTag: (knowledgeId: string) =>
`${restAPIv1}/datasets/${knowledgeId}/tags`,
renameTag: (knowledgeId: string) =>
`${restAPIv1}/datasets/${knowledgeId}/tags`,
// chunk
chunkList: (datasetId: string, documentId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents/${documentId}/chunks`,
chunkDetail: (datasetId: string, documentId: string, chunkId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents/${documentId}/chunks/${chunkId}`,
retrievalTest: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/search`,
// document
getDocumentList: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents`,
documentChangeStatus: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents/batch-update-status`,
documentDelete: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents`,
documentRename: (datasetId: string, documentId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents/${documentId}`,
documentIngest: `${restAPIv1}/documents/ingest`,
documentCreate: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents?type=empty`,
documentChangeParser: (datasetId: string, documentId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents/${documentId}`,
documentThumbnails: `${restAPIv1}/thumbnails`,
getDocumentFile: `${webAPI}/document/get`,
getDocumentFileDownload: (docId: string) =>
`${webAPI}/document/download/${docId}`,
documentUpload: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents`,
webCrawl: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents?type=web`,
documentInfoUpload: `${restAPIv1}/documents/upload`,
setMeta: `${webAPI}/document/set_meta`,
getDatasetFilter: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/documents?type=filter`,
// chat
createChat: `${restAPIv1}/chats`,
listChats: `${restAPIv1}/chats`,
getChat: (chatId: string) => `${restAPIv1}/chats/${chatId}`,
updateChat: (chatId: string) => `${restAPIv1}/chats/${chatId}`,
patchChat: (chatId: string) => `${restAPIv1}/chats/${chatId}`,
deleteChat: (chatId: string) => `${restAPIv1}/chats/${chatId}`,
bulkDeleteChats: `${restAPIv1}/chats`,
createSession: (chatId: string) => `${restAPIv1}/chats/${chatId}/sessions`,
listSessions: (chatId: string) => `${restAPIv1}/chats/${chatId}/sessions`,
getSession: (chatId: string, sessionId: string) =>
`${restAPIv1}/chats/${chatId}/sessions/${sessionId}`,
updateSession: (chatId: string, sessionId: string) =>
`${restAPIv1}/chats/${chatId}/sessions/${sessionId}`,
removeSessions: (chatId: string) => `${restAPIv1}/chats/${chatId}/sessions`,
deleteMessage: (chatId: string, sessionId: string, msgId: string) =>
`${restAPIv1}/chats/${chatId}/sessions/${sessionId}/messages/${msgId}`,
thumbup: (chatId: string, sessionId: string, msgId: string) =>
`${restAPIv1}/chats/${chatId}/sessions/${sessionId}/messages/${msgId}/feedback`,
completionUrl: `${restAPIv1}/chat/completions`,
chatsTts: `${restAPIv1}/chat/audio/speech`,
searchCompletion: (searchId: string) =>
`${restAPIv1}/searches/${searchId}/completion`,
chatsMindmap: `${restAPIv1}/chat/mindmap`,
chatsRelatedQuestions: `${restAPIv1}/chat/recommendation`,
// next chat
fetchExternalChatInfo: (id: string) => `${restAPIv1}/chatbots/${id}/info`,
// file manager
listFile: `${restAPIv1}/files`,
uploadFile: `${restAPIv1}/files`,
removeFile: `${restAPIv1}/files`,
getAllParentFolder: `${restAPIv1}/files`,
createFolder: `${restAPIv1}/files`,
connectFileToKnowledge: `${restAPIv1}/files/link-to-datasets`,
getFile: `${restAPIv1}/files`,
moveFile: `${restAPIv1}/files/move`,
// system
getSystemVersion: `${restAPIv1}/system/version`,
getSystemTokenList: `${restAPIv1}/system/tokens`,
createSystemToken: `${restAPIv1}/system/tokens`,
removeSystemToken: `${restAPIv1}/system/tokens`,
getSystemConfig: `${restAPIv1}/system/config`,
setLangfuseConfig: `${restAPIv1}/langfuse/api-key`,
// flow
listAgentTemplate: `${restAPIv1}/agents/templates`,
listAgents: `${restAPIv1}/agents`,
createAgent: `${restAPIv1}/agents`,
updateAgent: (agentId: string) => `${restAPIv1}/agents/${agentId}`,
deleteAgent: (agentId: string) => `${restAPIv1}/agents/${agentId}`,
agentChatCompletion: `${restAPIv1}/agents/chat/completion`,
resetAgent: (agentId: string) => `${restAPIv1}/agents/${agentId}/reset`,
testDbConnect: `${restAPIv1}/agents/test_db_connection`,
getInputElements: `${webAPI}/canvas/input_elements`,
debug: (agentId: string, componentId: string) =>
`${restAPIv1}/agents/${agentId}/components/${componentId}/debug`,
trace: (agentId: string, messageId: string) =>
`${restAPIv1}/agents/${agentId}/logs/${messageId}`,
cancelCanvas: (taskId: string) => `${restAPIv1}/tasks/${taskId}/cancel`,
// agent
inputForm: (agentId: string, componentId: string) =>
`${restAPIv1}/agents/${agentId}/components/${componentId}/input-form`,
fetchVersionList: (id: string) => `${restAPIv1}/agents/${id}/versions`,
fetchVersion: (agentId: string, versionId: string) =>
`${restAPIv1}/agents/${agentId}/versions/${versionId}`,
getAgent: (id: string) => `${restAPIv1}/agents/${id}`,
uploadAgentFile: (id?: string) => `${restAPIv1}/agents/${id}/upload`,
createAgentSession: (agentId: string) =>
`${restAPIv1}/agents/${agentId}/sessions`,
fetchAgentLogs: (canvasId: string) => `${webAPI}/canvas/${canvasId}/sessions`,
fetchAgentSessions: (agentId: string) =>
`${restAPIv1}/agents/${agentId}/sessions`,
fetchAgentSessionById: (agentId: string, sessionId: string) =>
`${restAPIv1}/agents/${agentId}/sessions/${sessionId}`,
fetchExternalAgentInputs: (canvasId: string) =>
`${restAPIv1}/agentbots/${canvasId}/inputs`,
prompt: `${restAPIv1}/agents/prompts`,
cancelDataflow: (id: string) => `${restAPIv1}/tasks/${id}/cancel`,
downloadFile: `${restAPIv1}/agents/download`,
testWebhook: (id: string) => `${restAPIv1}/agents/${id}/webhook/test`,
fetchWebhookTrace: (id: string) => `${restAPIv1}/agents/${id}/webhook/logs`,
// explore
// mcp server
listMcpServer: `${restAPIv1}/mcp/servers`,
getMcpServer: (id: string) => `${restAPIv1}/mcp/servers/${id}`,
createMcpServer: `${restAPIv1}/mcp/servers`,
updateMcpServer: (id: string) => `${restAPIv1}/mcp/servers/${id}`,
deleteMcpServer: (id: string) => `${restAPIv1}/mcp/servers/${id}`,
importMcpServer: `${restAPIv1}/mcp/servers/import`,
exportMcpServer: (id: string) =>
`${restAPIv1}/mcp/servers/${id}?mode=download`,
testMcpServer: (id: string) => `${restAPIv1}/mcp/servers/${id}/test`,
// next-search
createSearch: `${restAPIv1}/searches`,
getSearchList: `${restAPIv1}/searches`,
deleteSearch: (params: { search_id: string }) =>
`${restAPIv1}/searches/${params.search_id}`,
getSearchDetail: (params: { search_id: string }) =>
`${restAPIv1}/searches/${params.search_id}`,
getSearchDetailShare: `${restAPIv1}/searchbots/detail`,
updateSearchSetting: (params: { search_id: string }) =>
`${restAPIv1}/searches/${params.search_id}`,
askShare: `${restAPIv1}/searchbots/ask`,
mindmapShare: `${restAPIv1}/searchbots/mindmap`,
getRelatedQuestionsShare: `${restAPIv1}/searchbots/related_questions`,
retrievalTestShare: `${restAPIv1}/searchbots/retrieval_test`,
// memory
createMemory: `${restAPIv1}/memories`,
getMemoryList: `${restAPIv1}/memories`,
getMemoryConfig: (id: string) => `${restAPIv1}/memories/${id}/config`,
deleteMemory: (id: string) => `${restAPIv1}/memories/${id}`,
getMemoryDetail: (id: string) => `${restAPIv1}/memories/${id}`,
updateMemorySetting: (id: string) => `${restAPIv1}/memories/${id}`,
deleteMemoryMessage: (data: { memory_id: string; message_id: string }) =>
`${restAPIv1}/messages/${data.memory_id}:${data.message_id}`,
getMessageContent: (data: { memory_id: string; message_id: string }) =>
`${restAPIv1}/messages/${data.memory_id}:${data.message_id}/content`,
updateMessageState: (data: { memory_id: string; message_id: string }) =>
`${restAPIv1}/messages/${data.memory_id}:${data.message_id}`,
// data pipeline
fetchDataflow: (id: string) => `${webAPI}/dataflow/get/${id}`,
setDataflow: `${webAPI}/dataflow/set`,
removeDataflow: `${webAPI}/dataflow/rm`,
listDataflow: `${webAPI}/dataflow/list`,
runDataflow: `${webAPI}/dataflow/run`,
// admin
adminLogin: `${restAPIv1}/admin/login`,
adminLogout: `${restAPIv1}/admin/logout`,
adminListUsers: `${restAPIv1}/admin/users`,
adminCreateUser: `${restAPIv1}/admin/users`,
adminSetSuperuser: (username: string) =>
`${restAPIv1}/admin/users/${username}/admin`,
adminGetUserDetails: (username: string) =>
`${restAPIv1}/admin/users/${username}`,
adminUpdateUserStatus: (username: string) =>
`${restAPIv1}/admin/users/${username}/activate`,
adminUpdateUserPassword: (username: string) =>
`${restAPIv1}/admin/users/${username}/password`,
adminDeleteUser: (username: string) => `${restAPIv1}/admin/users/${username}`,
adminListUserDatasets: (username: string) =>
`${restAPIv1}/admin/users/${username}/datasets`,
adminListUserAgents: (username: string) =>
`${restAPIv1}/admin/users/${username}/agents`,
adminListServices: `${restAPIv1}/admin/services`,
adminShowServiceDetails: (serviceId: string) =>
`${restAPIv1}/admin/services/${serviceId}`,
adminListRoles: `${restAPIv1}/admin/roles`,
adminListRolesWithPermission: `${restAPIv1}/admin/roles_with_permission`,
adminGetRolePermissions: (roleName: string) =>
`${restAPIv1}/admin/roles/${roleName}/permissions`,
adminAssignRolePermissions: (roleName: string) =>
`${restAPIv1}/admin/roles/${roleName}/permission`,
adminRevokeRolePermissions: (roleName: string) =>
`${restAPIv1}/admin/roles/${roleName}/permission`,
adminCreateRole: `${restAPIv1}/admin/roles`,
adminDeleteRole: (roleName: string) => `${restAPIv1}/admin/roles/${roleName}`,
adminUpdateRoleDescription: (roleName: string) =>
`${restAPIv1}/admin/roles/${roleName}`,
adminUpdateUserRole: (username: string) =>
`${restAPIv1}/admin/users/${username}/role`,
adminGetUserPermissions: (username: string) =>
`${restAPIv1}/admin/users/${username}/permissions`,
adminListResources: `${restAPIv1}/admin/roles/resource`,
adminListWhitelist: `${restAPIv1}/admin/whitelist`,
adminCreateWhitelistEntry: `${restAPIv1}/admin/whitelist/add`,
adminUpdateWhitelistEntry: (id: number) =>
`${restAPIv1}/admin/whitelist/${id}`,
adminDeleteWhitelistEntry: (email: string) =>
`${restAPIv1}/admin/whitelist/${email}`,
adminImportWhitelist: `${restAPIv1}/admin/whitelist/batch`,
adminGetSystemVersion: `${restAPIv1}/admin/version`,
// Sandbox settings
adminListSandboxProviders: `${restAPIv1}/admin/sandbox/providers`,
adminGetSandboxProviderSchema: (providerId: string) =>
`${restAPIv1}/admin/sandbox/providers/${providerId}/schema`,
adminGetSandboxConfig: `${restAPIv1}/admin/sandbox/config`,
adminSetSandboxConfig: `${restAPIv1}/admin/sandbox/config`,
adminTestSandboxConnection: `${restAPIv1}/admin/sandbox/test`,
// Skill spaces
skillSpaces: `${restAPIv1}/skills/spaces`,
skillSpace: (spaceId: string) => `${restAPIv1}/skills/spaces/${spaceId}`,
skillSpaceByFolder: `${restAPIv1}/skills/space/by-folder`,
skillConfig: `${restAPIv1}/skills/config`,
skillSearch: `${restAPIv1}/skills/search`,
skillIndex: `${restAPIv1}/skills/index`,
skillReindex: `${restAPIv1}/skills/reindex`,
};