@flowfuse/flowfuse
Version:
An open source low-code development platform
129 lines (121 loc) • 5.27 kB
JavaScript
const { default: axios } = require('axios')
/**
* Assistant api routes
*
* - /api/v1/assistant
*
* @namespace assistant
* @memberof forge.routes.api
*/
module.exports = async function (app) {
app.addHook('preHandler', app.verifySession)
app.addHook('preHandler', async (request, reply) => {
// Only permit requests made by a valid device or instance token
if (!request.session || request.session.provisioning) {
reply.code(401).send({ code: 'unauthorized', error: 'unauthorized' })
} else if (request.session.ownerType !== 'device' && request.session.ownerType !== 'project') {
reply.code(401).send({ code: 'unauthorized', error: 'unauthorized' })
} else {
// get the owner object and team
if (request.session.ownerType === 'device') {
request.owner = await app.db.models.Device.byId(+request.session.ownerId)
request.ownerType = 'device'
request.ownerId = request.owner.hashid
} else {
request.owner = await app.db.models.Project.byId(request.session.ownerId)
request.ownerType = 'project'
request.ownerId = request.owner.id
}
request.team = await app.db.models.Team.byId(request.owner.Team.id)
}
})
/**
* Endpoint for assistant methods
* For now, this is simply a relay to an external assistant service
* In the future, we may decide to bring that service inside the core or
* use an alternative means of accessing it.
*/
app.post('/:method', {
config: {
rateLimit: app.config.rate_limits
? {
hook: 'preHandler', // apply the rate as a preHandler so that session is available
max: 5, // max requests per window
timeWindow: 30000, // 30 seconds
keyGenerator: (request) => {
return request.ownerId || request.ip
}
}
: false
},
schema: {
hide: true, // dont show in swagger
body: {
type: 'object',
properties: {
// The prompt to send to the assistant (required)
prompt: { type: 'string' },
// A correlation id for the transaction (required)
transactionId: { type: 'string' },
// Additional context for the function (optional)
context: { type: 'object', additionalProperties: true }
},
required: ['prompt', 'transactionId']
},
response: {
200: {
type: 'object',
additionalProperties: true
},
'4xx': {
$ref: 'APIError'
}
}
}
},
async (request, reply) => {
const method = request.params.method // the method to call at the assistant service
if (/^[a-z0-9_-]+$/.test(method) === false) {
return reply.code(400).send({ code: 'invalid_method', error: 'Invalid method name' })
}
const serviceUrl = app.config.assistant?.service?.url
const serviceToken = app.config.assistant?.service?.token
const enabled = app.config.assistant?.enabled !== false && serviceUrl
const requestTimeout = app.config.assistant?.service?.requestTimeout || 60000
if (!enabled) {
return reply.code(501).send({ code: 'service_disabled', error: 'Assistant service is not enabled' })
}
const url = `${serviceUrl.replace(/\/+$/, '')}/${method.replace(/^\/+/, '')}`
// post to the assistant service
const headers = {
'ff-owner-type': request.ownerType,
'ff-owner-id': request.ownerId
}
// include license information, team id and trial status so that we can make decisions in the assistant service
const isLicensed = app.license?.active() || false
const licenseType = isLicensed ? (app.license.get('dev') ? 'DEV' : 'EE') : 'CE'
const tier = isLicensed ? app.license.get('tier') : null
headers['ff-license-active'] = isLicensed
headers['ff-license-type'] = licenseType
headers['ff-license-tier'] = tier
headers['ff-team-id'] = request.team.hashid
if (app.billing && request.team.getSubscription) {
const subscription = await request.team.getSubscription()
headers['ff-team-trial'] = subscription ? subscription.isTrial() : null
}
if (serviceToken) {
headers.Authorization = `Bearer ${serviceToken}`
}
try {
const response = await axios.post(url, {
...request.body
}, {
headers,
timeout: requestTimeout
})
reply.send(response.data)
} catch (error) {
reply.code(error.response?.status || 500).send({ code: error.response?.data?.code || 'unexpected_error', error: error.response?.data?.error || error.message })
}
})
}