d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
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
231 lines
7.3 KiB
TypeScript
231 lines
7.3 KiB
TypeScript
import type {
|
|
GoogleFormsGetResponsesParams,
|
|
GoogleFormsResponse,
|
|
GoogleFormsResponseList,
|
|
} from '@/tools/google_forms/types'
|
|
import { buildGetResponseUrl, buildListResponsesUrl } from '@/tools/google_forms/utils'
|
|
import type { ToolConfig } from '@/tools/types'
|
|
|
|
export const getResponsesTool: ToolConfig<GoogleFormsGetResponsesParams> = {
|
|
id: 'google_forms_get_responses',
|
|
name: 'Google Forms: Get Responses',
|
|
description: 'Retrieve a single response or list responses from a Google Form',
|
|
version: '1.0.0',
|
|
|
|
oauth: {
|
|
required: true,
|
|
provider: 'google-forms',
|
|
},
|
|
|
|
params: {
|
|
accessToken: {
|
|
type: 'string',
|
|
required: true,
|
|
visibility: 'hidden',
|
|
description: 'OAuth2 access token',
|
|
},
|
|
formId: {
|
|
type: 'string',
|
|
required: true,
|
|
visibility: 'user-or-llm',
|
|
description: 'Google Forms form ID',
|
|
},
|
|
responseId: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Response ID - if provided, returns this specific response',
|
|
},
|
|
pageSize: {
|
|
type: 'number',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description:
|
|
'Maximum number of responses to return (service may return fewer). Defaults to 5000.',
|
|
},
|
|
pageToken: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Page token from a previous list response to fetch the next page of responses',
|
|
},
|
|
filter: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description:
|
|
'Filter responses, e.g. "timestamp > 2024-01-01T00:00:00Z" (RFC3339 UTC). Only timestamp filters are supported.',
|
|
},
|
|
},
|
|
|
|
request: {
|
|
url: (params: GoogleFormsGetResponsesParams) =>
|
|
params.responseId
|
|
? buildGetResponseUrl({ formId: params.formId, responseId: params.responseId })
|
|
: buildListResponsesUrl({
|
|
formId: params.formId,
|
|
pageSize: params.pageSize ? Number(params.pageSize) : undefined,
|
|
pageToken: params.pageToken,
|
|
filter: params.filter,
|
|
}),
|
|
method: 'GET',
|
|
headers: (params: GoogleFormsGetResponsesParams) => ({
|
|
Authorization: `Bearer ${params.accessToken}`,
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
},
|
|
|
|
transformResponse: async (response: Response, params?: GoogleFormsGetResponsesParams) => {
|
|
const data = (await response.json()) as unknown
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = response.statusText || 'Failed to fetch responses'
|
|
if (data && typeof data === 'object') {
|
|
const record = data as Record<string, unknown>
|
|
const error = record.error as { message?: string } | undefined
|
|
if (error?.message) {
|
|
errorMessage = error.message
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
output: (data as Record<string, unknown>) || {},
|
|
error: errorMessage,
|
|
}
|
|
}
|
|
|
|
// Normalize answers into a flat key/value map per response
|
|
const normalizeAnswerContainer = (container: unknown): unknown => {
|
|
if (!container || typeof container !== 'object') return container
|
|
const record = container as Record<string, unknown>
|
|
const answers = record.answers as unknown[] | undefined
|
|
if (Array.isArray(answers)) {
|
|
const values = answers.map((entry) => {
|
|
if (entry && typeof entry === 'object') {
|
|
const er = entry as Record<string, unknown>
|
|
if (typeof er.value !== 'undefined') return er.value
|
|
}
|
|
return entry
|
|
})
|
|
return values.length === 1 ? values[0] : values
|
|
}
|
|
return container
|
|
}
|
|
|
|
const normalizeAnswers = (answers: unknown): Record<string, unknown> => {
|
|
if (!answers || typeof answers !== 'object') return {}
|
|
const src = answers as Record<string, unknown>
|
|
const out: Record<string, unknown> = {}
|
|
for (const [questionId, answerObj] of Object.entries(src)) {
|
|
if (answerObj && typeof answerObj === 'object') {
|
|
const aRec = answerObj as Record<string, unknown>
|
|
// Find first *Answers property that contains an answers array
|
|
const key = Object.keys(aRec).find(
|
|
(k) => k.toLowerCase().endsWith('answers') && Array.isArray((aRec[k] as any)?.answers)
|
|
)
|
|
if (key) {
|
|
out[questionId] = normalizeAnswerContainer(aRec[key])
|
|
continue
|
|
}
|
|
}
|
|
out[questionId] = answerObj as unknown
|
|
}
|
|
return out
|
|
}
|
|
|
|
const normalizeResponse = (r: GoogleFormsResponse): Record<string, unknown> => ({
|
|
responseId: r.responseId,
|
|
createTime: r.createTime,
|
|
lastSubmittedTime: r.lastSubmittedTime,
|
|
answers: normalizeAnswers(r.answers as unknown),
|
|
})
|
|
|
|
// Distinguish single vs list response shapes
|
|
const isList = (obj: unknown): obj is GoogleFormsResponseList =>
|
|
!!obj && typeof obj === 'object' && Array.isArray((obj as GoogleFormsResponseList).responses)
|
|
|
|
if (isList(data)) {
|
|
const listData = data as GoogleFormsResponseList
|
|
const toTimestamp = (s?: string): number => {
|
|
if (!s) return 0
|
|
const t = Date.parse(s)
|
|
return Number.isNaN(t) ? 0 : t
|
|
}
|
|
const sorted = (listData.responses || [])
|
|
.slice()
|
|
.sort(
|
|
(a, b) =>
|
|
toTimestamp(b.lastSubmittedTime || b.createTime) -
|
|
toTimestamp(a.lastSubmittedTime || a.createTime)
|
|
)
|
|
const normalized = sorted.map((r) => normalizeResponse(r))
|
|
const output: Record<string, unknown> = {
|
|
responses: normalized,
|
|
nextPageToken: listData.nextPageToken ?? null,
|
|
raw: listData,
|
|
}
|
|
return {
|
|
success: true,
|
|
output,
|
|
}
|
|
}
|
|
|
|
const single = data as GoogleFormsResponse
|
|
const normalizedSingle = normalizeResponse(single)
|
|
const output: Record<string, unknown> = {
|
|
response: normalizedSingle,
|
|
raw: single,
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
output,
|
|
}
|
|
},
|
|
|
|
outputs: {
|
|
responses: {
|
|
type: 'array',
|
|
description: 'Array of form responses (when no responseId provided)',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
responseId: { type: 'string', description: 'Unique response ID' },
|
|
createTime: { type: 'string', description: 'When the response was created' },
|
|
lastSubmittedTime: {
|
|
type: 'string',
|
|
description: 'When the response was last submitted',
|
|
},
|
|
answers: {
|
|
type: 'json',
|
|
description: 'Map of question IDs to answer values',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
nextPageToken: {
|
|
type: 'string',
|
|
description: 'Token to fetch the next page of responses (null when no more pages)',
|
|
optional: true,
|
|
},
|
|
response: {
|
|
type: 'object',
|
|
description: 'Single form response (when responseId is provided)',
|
|
properties: {
|
|
responseId: { type: 'string', description: 'Unique response ID' },
|
|
createTime: { type: 'string', description: 'When the response was created' },
|
|
lastSubmittedTime: { type: 'string', description: 'When the response was last submitted' },
|
|
answers: {
|
|
type: 'json',
|
|
description: 'Map of question IDs to answer values',
|
|
},
|
|
},
|
|
},
|
|
raw: {
|
|
type: 'json',
|
|
description: 'Raw API response data',
|
|
},
|
|
},
|
|
}
|