Skip to content
← Public packages

@cameronpak/zo-computer

Typed Zo Computer API client with Result-based error handling and SSE streaming.

src/ask.ts

75 lines · 2.2 KB · TypeScript
import { Result } from 'better-result'
import { isRecord, zoJson } from './core'
import { ZoConfigError, ZoParseError, type ZoRequestError } from './errors'
import type { ZoAskInput, ZoAskResult, ZoClientOptions } from './types'

/**
 * Send one message to Zo and wait for the whole answer.
 *
 * Set `outputFormat` to a JSON Schema when you want `output` as an object.
 * Pass the returned `conversationId` back in to continue the thread.
 *
 * @example
 * import ask from 'kody:@cameronpak/zo-computer/ask'
 * const answer = await ask({ input: 'Summarize my week' })
 * if (answer.status === 'ok') console.log(answer.value.output)
 */
export default async function ask(
	params: ZoAskInput & ZoClientOptions,
): Promise<Result<ZoAskResult, ZoRequestError>> {
	if (typeof params?.input !== 'string' || params.input.trim() === '') {
		return Result.err(
			new ZoConfigError({
				field: 'input',
				message: 'input must be a non-empty string',
			}),
		)
	}

	const body: Record<string, unknown> = { input: params.input, stream: false }
	if (params.conversationId !== undefined) {
		body.conversation_id = params.conversationId
	}
	if (params.modelName !== undefined) body.model_name = params.modelName
	if (params.personaId !== undefined) body.persona_id = params.personaId
	if (params.outputFormat !== undefined) {
		body.output_format = params.outputFormat
	}

	const parsed = await zoJson(
		'/zo/ask',
		{ method: 'POST', accept: 'application/json', body },
		params,
	)
	if (Result.isError(parsed)) return parsed

	const payload = parsed.value
	if (!isRecord(payload) || !('output' in payload)) {
		return Result.err(
			new ZoParseError({
				url: '/zo/ask',
				body: JSON.stringify(payload).slice(0, 2000),
				message: 'Zo response did not contain an output field',
			}),
		)
	}

	const output = payload.output
	if (typeof output !== 'string' && !isRecord(output)) {
		return Result.err(
			new ZoParseError({
				url: '/zo/ask',
				body: JSON.stringify(payload).slice(0, 2000),
				message: 'Zo output was neither a string nor an object',
			}),
		)
	}

	return Result.ok({
		output,
		conversationId:
			typeof payload.conversation_id === 'string'
				? payload.conversation_id
				: '',
	})
}