Mothership block pudate

This commit is contained in:
Siddharth Ganesan
2026-03-02 15:05:56 -08:00
parent fce10241a5
commit 1d48289c53
4 changed files with 124 additions and 128 deletions

View File

@@ -1,5 +1,5 @@
import { Rocket } from 'lucide-react'
import type { BlockConfig } from '@/blocks/types'
import { Blimp } from '@/components/emcn'
import type { ToolResponse } from '@/tools/types'
interface MothershipResponse extends ToolResponse {
@@ -27,7 +27,7 @@ export const MothershipBlock: BlockConfig<MothershipResponse> = {
`,
category: 'blocks',
bgColor: '#802FDE',
icon: Rocket,
icon: Blimp,
subBlocks: [
{
id: 'messages',

View File

@@ -12,8 +12,9 @@ import {
validateModelProvider,
validateSkillsAllowed,
} from '@/ee/access-control/utils/permission-check'
import { AGENT, BlockType, DEFAULTS, REFERENCE, stripCustomToolPrefix } from '@/executor/constants'
import { AGENT, BlockType, DEFAULTS, stripCustomToolPrefix } from '@/executor/constants'
import { memoryService } from '@/executor/handlers/agent/memory'
import { parseResponseFormat } from '@/executor/handlers/shared/response-format'
import {
buildLoadSkillTool,
buildSkillsSystemPromptSection,
@@ -55,7 +56,7 @@ export class AgentBlockHandler implements BlockHandler {
await this.validateToolPermissions(ctx, filteredInputs.tools || [])
const responseFormat = this.parseResponseFormat(filteredInputs.responseFormat)
const responseFormat = parseResponseFormat(filteredInputs.responseFormat)
const model = filteredInputs.model || AGENT.DEFAULT_MODEL
await validateModelProvider(ctx.userId, model, ctx)
@@ -112,55 +113,6 @@ export class AgentBlockHandler implements BlockHandler {
return result
}
private parseResponseFormat(responseFormat?: string | object): any {
if (!responseFormat || responseFormat === '') return undefined
if (typeof responseFormat === 'object' && responseFormat !== null) {
const formatObj = responseFormat as any
if (!formatObj.schema && !formatObj.name) {
return {
name: 'response_schema',
schema: responseFormat,
strict: true,
}
}
return responseFormat
}
if (typeof responseFormat === 'string') {
const trimmedValue = responseFormat.trim()
if (trimmedValue.startsWith(REFERENCE.START) && trimmedValue.includes(REFERENCE.END)) {
return undefined
}
try {
const parsed = JSON.parse(trimmedValue)
if (parsed && typeof parsed === 'object' && !parsed.schema && !parsed.name) {
return {
name: 'response_schema',
schema: parsed,
strict: true,
}
}
return parsed
} catch (error: any) {
logger.warn('Failed to parse response format as JSON, using default behavior:', {
error: error.message,
value: trimmedValue,
})
return undefined
}
}
logger.warn('Unexpected response format type, using default behavior:', {
type: typeof responseFormat,
value: responseFormat,
})
return undefined
}
private async validateToolPermissions(ctx: ExecutionContext, tools: ToolInput[]): Promise<void> {
if (!Array.isArray(tools) || tools.length === 0) return

View File

@@ -1,6 +1,11 @@
import { createLogger } from '@sim/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType } from '@/executor/constants'
import {
parseResponseFormat,
processStructuredResponse,
resolveMessages,
} from '@/executor/handlers/shared/response-format'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http'
import type { SerializedBlock } from '@/serializer/types'
@@ -24,8 +29,8 @@ export class MothershipBlockHandler implements BlockHandler {
block: SerializedBlock,
inputs: Record<string, any>
): Promise<BlockOutput> {
const messages = this.resolveMessages(inputs)
const responseFormat = this.parseResponseFormat(inputs.responseFormat)
const messages = resolveMessages(inputs.messages)
const responseFormat = parseResponseFormat(inputs.responseFormat)
const memoryType = inputs.memoryType || 'none'
const chatId =
@@ -68,7 +73,7 @@ export class MothershipBlockHandler implements BlockHandler {
const result = await response.json()
if (responseFormat && result.content) {
return this.processStructuredResponse(result)
return processStructuredResponse(result, 'mothership')
}
return {
@@ -77,76 +82,4 @@ export class MothershipBlockHandler implements BlockHandler {
tokens: result.tokens || {},
}
}
private resolveMessages(
inputs: Record<string, any>
): Array<{ role: string; content: string }> {
const raw = inputs.messages
if (!raw) {
throw new Error('Messages input is required for the Mothership block')
}
let messages: unknown[]
if (typeof raw === 'string') {
try {
messages = JSON.parse(raw)
} catch {
throw new Error('Messages must be a valid JSON array')
}
} else if (Array.isArray(raw)) {
messages = raw
} else {
throw new Error('Messages must be an array of {role, content} objects')
}
return messages.map((msg: any, i: number) => {
if (!msg.role || typeof msg.content !== 'string') {
throw new Error(
`Message at index ${i} must have "role" (string) and "content" (string)`
)
}
return { role: String(msg.role), content: msg.content }
})
}
private parseResponseFormat(responseFormat?: string | object): any {
if (!responseFormat || responseFormat === '') return undefined
if (typeof responseFormat === 'object') return responseFormat
if (typeof responseFormat === 'string') {
const trimmed = responseFormat.trim()
if (!trimmed) return undefined
if (trimmed.startsWith('<') || trimmed.startsWith('{{')) return undefined
try {
return JSON.parse(trimmed)
} catch {
logger.warn('Failed to parse responseFormat as JSON', {
preview: trimmed.slice(0, 100),
})
return undefined
}
}
return undefined
}
private processStructuredResponse(result: any): BlockOutput {
const content = result.content
try {
const parsed = JSON.parse(content.trim())
return {
...parsed,
model: result.model || 'mothership',
tokens: result.tokens || {},
}
} catch {
logger.warn('Failed to parse structured response, returning raw content')
return {
content,
model: result.model || 'mothership',
tokens: result.tokens || {},
}
}
}
}

View File

@@ -0,0 +1,111 @@
import { createLogger } from '@sim/logger'
import type { BlockOutput } from '@/blocks/types'
import { REFERENCE } from '@/executor/constants'
const logger = createLogger('SharedResponseFormat')
/**
* Parse a raw responseFormat value (string or object) into a usable schema.
*
* Handles:
* - Empty / falsy → undefined
* - Already an object → wraps bare schemas with `{ name, schema, strict }`
* - JSON string → parsed, then same wrapping logic
* - Unresolved block references (`<block.field>`) → undefined
*/
export function parseResponseFormat(responseFormat?: string | object): any {
if (!responseFormat || responseFormat === '') return undefined
if (typeof responseFormat === 'object' && responseFormat !== null) {
const formatObj = responseFormat as any
if (!formatObj.schema && !formatObj.name) {
return { name: 'response_schema', schema: responseFormat, strict: true }
}
return responseFormat
}
if (typeof responseFormat === 'string') {
const trimmed = responseFormat.trim()
if (!trimmed) return undefined
if (trimmed.startsWith(REFERENCE.START) && trimmed.includes(REFERENCE.END)) {
return undefined
}
try {
const parsed = JSON.parse(trimmed)
if (parsed && typeof parsed === 'object' && !parsed.schema && !parsed.name) {
return { name: 'response_schema', schema: parsed, strict: true }
}
return parsed
} catch (error: any) {
logger.warn('Failed to parse response format as JSON', {
error: error.message,
preview: trimmed.slice(0, 100),
})
return undefined
}
}
return undefined
}
/**
* Validate and extract messages from a raw input value.
*
* Accepts a JSON string or an array. Each entry must have
* `role` (string) and `content` (string).
*/
export function resolveMessages(
raw: unknown
): Array<{ role: string; content: string }> {
if (!raw) {
throw new Error('Messages input is required')
}
let messages: unknown[]
if (typeof raw === 'string') {
try {
messages = JSON.parse(raw)
} catch {
throw new Error('Messages must be a valid JSON array')
}
} else if (Array.isArray(raw)) {
messages = raw
} else {
throw new Error('Messages must be an array of {role, content} objects')
}
return messages.map((msg: any, i: number) => {
if (!msg.role || typeof msg.content !== 'string') {
throw new Error(
`Message at index ${i} must have "role" (string) and "content" (string)`
)
}
return { role: String(msg.role), content: msg.content }
})
}
/**
* Try to parse the LLM response content as structured JSON and spread
* the fields into the block output. Falls back to returning raw content.
*/
export function processStructuredResponse(
result: { content?: string; model?: string; tokens?: any },
defaultModel: string
): BlockOutput {
const content = result.content ?? ''
try {
const parsed = JSON.parse(content.trim())
return {
...parsed,
model: result.model || defaultModel,
tokens: result.tokens || {},
}
} catch {
logger.warn('Failed to parse structured response, returning raw content')
return {
content,
model: result.model || defaultModel,
tokens: result.tokens || {},
}
}
}