chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import type {
|
||||
GraphApiResponse,
|
||||
SharepointPageContent,
|
||||
SharepointReadPageResponse,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import {
|
||||
assertGraphNextPageUrl,
|
||||
cleanODataMetadata,
|
||||
escapeODataString,
|
||||
extractTextFromCanvasLayout,
|
||||
getGraphNextPageUrl,
|
||||
optionalTrim,
|
||||
} from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SharePointReadPage')
|
||||
|
||||
export const readPageTool: ToolConfig<SharepointToolParams, SharepointReadPageResponse> = {
|
||||
id: 'sharepoint_read_page',
|
||||
name: 'Read SharePoint Page',
|
||||
description: 'Read a specific page from a SharePoint site',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
siteSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Select the SharePoint site',
|
||||
},
|
||||
siteId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the SharePoint site (internal use)',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the page to read. Example: a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
pageName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The name of the page to read (alternative to pageId). Example: Home.aspx or About-Us.aspx',
|
||||
},
|
||||
maxPages: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Maximum number of pages to return when listing all pages (default: 10, max: 50)',
|
||||
},
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Full @odata.nextLink URL from a previous Microsoft Graph page response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (params.nextPageUrl) {
|
||||
return assertGraphNextPageUrl(params.nextPageUrl)
|
||||
}
|
||||
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const pageId = optionalTrim(params.pageId)
|
||||
const encodedSiteId = encodeURIComponent(siteId)
|
||||
|
||||
let baseUrl: string
|
||||
if (pageId) {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`
|
||||
} else {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/pages/microsoft.graph.sitePage`
|
||||
}
|
||||
|
||||
const url = new URL(baseUrl)
|
||||
|
||||
url.searchParams.append(
|
||||
'$select',
|
||||
'id,name,title,webUrl,pageLayout,description,createdDateTime,lastModifiedDateTime'
|
||||
)
|
||||
|
||||
if (params.pageName && !pageId) {
|
||||
const pageName = params.pageName.trim()
|
||||
const pageNameWithAspx = pageName.endsWith('.aspx') ? pageName : `${pageName}.aspx`
|
||||
const escapedPageName = escapeODataString(pageName)
|
||||
const escapedPageNameWithAspx = escapeODataString(pageNameWithAspx)
|
||||
|
||||
url.searchParams.append(
|
||||
'$filter',
|
||||
`name eq '${escapedPageName}' or name eq '${escapedPageNameWithAspx}'`
|
||||
)
|
||||
url.searchParams.append('$top', '10')
|
||||
} else if (!pageId && !params.pageName) {
|
||||
const requestedMaxPages =
|
||||
typeof params.maxPages === 'number' ? params.maxPages : Number(params.maxPages || 10)
|
||||
const maxPages = Math.min(Number.isFinite(requestedMaxPages) ? requestedMaxPages : 10, 50)
|
||||
url.searchParams.append('$top', maxPages.toString())
|
||||
}
|
||||
|
||||
if (pageId) {
|
||||
url.searchParams.append('$expand', 'canvasLayout')
|
||||
}
|
||||
|
||||
const finalUrl = url.toString()
|
||||
|
||||
logger.info('SharePoint API URL', {
|
||||
finalUrl,
|
||||
siteId,
|
||||
pageId,
|
||||
pageName: params.pageName,
|
||||
searchParams: Object.fromEntries(url.searchParams),
|
||||
})
|
||||
|
||||
return finalUrl
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const data: GraphApiResponse = await response.json()
|
||||
|
||||
logger.info('SharePoint API response', {
|
||||
pageId: params?.pageId,
|
||||
pageName: params?.pageName,
|
||||
resultsCount: data.value?.length || (data.id ? 1 : 0),
|
||||
hasDirectPage: !!data.id,
|
||||
hasSearchResults: !!data.value,
|
||||
})
|
||||
|
||||
if (params?.pageId) {
|
||||
const pageData = data
|
||||
const contentData = {
|
||||
content: extractTextFromCanvasLayout(data.canvasLayout),
|
||||
canvasLayout: data.canvasLayout ?? null,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
page: {
|
||||
id: pageData.id!,
|
||||
name: pageData.name!,
|
||||
title: pageData.title || pageData.name!,
|
||||
webUrl: pageData.webUrl!,
|
||||
pageLayout: pageData.pageLayout,
|
||||
description: pageData.description ?? null,
|
||||
createdDateTime: pageData.createdDateTime,
|
||||
lastModifiedDateTime: pageData.lastModifiedDateTime,
|
||||
},
|
||||
content: contentData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.value || data.value.length === 0) {
|
||||
logger.info('No pages found', {
|
||||
searchName: params?.pageName,
|
||||
siteId: optionalTrim(params?.siteId) || optionalTrim(params?.siteSelector) || 'root',
|
||||
totalResults: data.value?.length || 0,
|
||||
})
|
||||
const message = params?.pageName
|
||||
? `Page with name '${params?.pageName}' not found. Make sure the page exists and you have access to it. Note: SharePoint page names typically include the .aspx extension.`
|
||||
: 'No pages found on this SharePoint site.'
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: {
|
||||
content: message,
|
||||
canvasLayout: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Found pages', {
|
||||
searchName: params?.pageName,
|
||||
foundPages: data.value.map((p) => ({ id: p.id, name: p.name, title: p.title })),
|
||||
totalCount: data.value.length,
|
||||
})
|
||||
|
||||
if (params?.pageName) {
|
||||
const pageData = data.value[0]
|
||||
const siteId = optionalTrim(params?.siteId) || optionalTrim(params?.siteSelector) || 'root'
|
||||
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageData.id)}/microsoft.graph.sitePage?$expand=canvasLayout`
|
||||
|
||||
logger.info('Making API call to get page content for searched page', {
|
||||
pageId: pageData.id,
|
||||
contentUrl,
|
||||
siteId,
|
||||
})
|
||||
|
||||
const contentResponse = await fetch(contentUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
let contentData: SharepointPageContent = { content: '' }
|
||||
if (contentResponse.ok) {
|
||||
const contentResult = await contentResponse.json()
|
||||
contentData = {
|
||||
content: extractTextFromCanvasLayout(contentResult.canvasLayout),
|
||||
canvasLayout: cleanODataMetadata(contentResult.canvasLayout),
|
||||
}
|
||||
} else {
|
||||
logger.error('Failed to fetch page content', {
|
||||
status: contentResponse.status,
|
||||
statusText: contentResponse.statusText,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
page: {
|
||||
id: pageData.id,
|
||||
name: pageData.name,
|
||||
title: pageData.title || pageData.name,
|
||||
webUrl: pageData.webUrl,
|
||||
pageLayout: pageData.pageLayout,
|
||||
description: pageData.description ?? null,
|
||||
createdDateTime: pageData.createdDateTime,
|
||||
lastModifiedDateTime: pageData.lastModifiedDateTime,
|
||||
},
|
||||
content: contentData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const siteId = optionalTrim(params?.siteId) || optionalTrim(params?.siteSelector) || 'root'
|
||||
const pagesWithContent = []
|
||||
const nextPageUrl = getGraphNextPageUrl(data)
|
||||
|
||||
logger.info('Fetching content for all pages', {
|
||||
totalPages: data.value.length,
|
||||
siteId,
|
||||
})
|
||||
|
||||
const encodedSiteId = encodeURIComponent(siteId)
|
||||
for (const pageInfo of data.value) {
|
||||
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/pages/${encodeURIComponent(pageInfo.id)}/microsoft.graph.sitePage?$expand=canvasLayout`
|
||||
|
||||
try {
|
||||
const contentResponse = await fetch(contentUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
let contentData = { content: '', canvasLayout: null }
|
||||
if (contentResponse.ok) {
|
||||
const contentResult = await contentResponse.json()
|
||||
contentData = {
|
||||
content: extractTextFromCanvasLayout(contentResult.canvasLayout),
|
||||
canvasLayout: cleanODataMetadata(contentResult.canvasLayout),
|
||||
}
|
||||
} else {
|
||||
logger.error('Failed to fetch content for page', {
|
||||
pageId: pageInfo.id,
|
||||
pageName: pageInfo.name,
|
||||
status: contentResponse.status,
|
||||
})
|
||||
}
|
||||
|
||||
pagesWithContent.push({
|
||||
page: {
|
||||
id: pageInfo.id,
|
||||
name: pageInfo.name,
|
||||
title: pageInfo.title || pageInfo.name,
|
||||
webUrl: pageInfo.webUrl,
|
||||
pageLayout: pageInfo.pageLayout,
|
||||
description: pageInfo.description ?? null,
|
||||
createdDateTime: pageInfo.createdDateTime,
|
||||
lastModifiedDateTime: pageInfo.lastModifiedDateTime,
|
||||
},
|
||||
content: contentData,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching content for page', {
|
||||
pageId: pageInfo.id,
|
||||
pageName: pageInfo.name,
|
||||
error: toError(error).message,
|
||||
})
|
||||
|
||||
pagesWithContent.push({
|
||||
page: {
|
||||
id: pageInfo.id,
|
||||
name: pageInfo.name,
|
||||
title: pageInfo.title || pageInfo.name,
|
||||
webUrl: pageInfo.webUrl,
|
||||
pageLayout: pageInfo.pageLayout,
|
||||
description: pageInfo.description ?? null,
|
||||
createdDateTime: pageInfo.createdDateTime,
|
||||
lastModifiedDateTime: pageInfo.lastModifiedDateTime,
|
||||
},
|
||||
content: { content: 'Failed to fetch content', canvasLayout: null },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Completed fetching content for all pages', {
|
||||
totalPages: pagesWithContent.length,
|
||||
successfulPages: pagesWithContent.filter(
|
||||
(p) => p.content.content !== 'Failed to fetch content'
|
||||
).length,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
pages: pagesWithContent,
|
||||
totalPages: pagesWithContent.length,
|
||||
nextPageUrl,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
page: {
|
||||
type: 'object',
|
||||
description: 'Information about the SharePoint page',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the page' },
|
||||
name: { type: 'string', description: 'The name of the page' },
|
||||
title: { type: 'string', description: 'The title of the page' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the page' },
|
||||
pageLayout: { type: 'string', description: 'The layout type of the page' },
|
||||
description: { type: 'string', description: 'The description of the page', optional: true },
|
||||
createdDateTime: { type: 'string', description: 'When the page was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the page was last modified' },
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
type: 'array',
|
||||
description: 'List of SharePoint pages',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the page' },
|
||||
name: { type: 'string', description: 'The name of the page' },
|
||||
title: { type: 'string', description: 'The title of the page' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the page' },
|
||||
pageLayout: { type: 'string', description: 'The layout type of the page' },
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'The description of the page',
|
||||
optional: true,
|
||||
},
|
||||
createdDateTime: { type: 'string', description: 'When the page was created' },
|
||||
lastModifiedDateTime: {
|
||||
type: 'string',
|
||||
description: 'When the page was last modified',
|
||||
},
|
||||
},
|
||||
},
|
||||
content: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Extracted text content from the page' },
|
||||
canvasLayout: {
|
||||
type: 'object',
|
||||
description: 'Raw SharePoint canvas layout structure',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
content: {
|
||||
type: 'object',
|
||||
description: 'Content of the SharePoint page',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Extracted text content from the page' },
|
||||
canvasLayout: { type: 'object', description: 'Raw SharePoint canvas layout structure' },
|
||||
},
|
||||
},
|
||||
totalPages: { type: 'number', description: 'Total number of pages found' },
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
description: 'Full Microsoft Graph @odata.nextLink URL for the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user