import { toRaw } from 'vue'
import type { StreamHandler } from '@opentiny/tiny-robot-kit'
import {
RobotMessageContentType,
type LLMMessage,
type RobotInputContentPart,
type RobotMessage,
type RobotMessageContent,
type RobotRenderContentItem
} from '../types'
export const formatMessages = (messages: LLMMessage[]) => {
const validMessageFilter = (message: LLMMessage) => message.content || message.tool_calls || message.tool_call_id
return toRaw(messages)
.filter(validMessageFilter)
.map((message) => ({
role: message.role,
content: message.content,
...(message.tool_calls ? { tool_calls: message.tool_calls } : {}),
...(message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}),
...(message.reasoning_content ? { reasoning_content: message.reasoning_content } : {})
}))
}
export const serializeError = (err: unknown): string => {
if (err === undefined || err === null) {
return ''
}
if (err instanceof Error) {
return JSON.stringify({ name: err.name, message: err.message })
}
if (typeof err === 'string') return err
try {
return JSON.stringify(err)
} catch {
return String(err)
}
}
export const extractMessageText = (content: RobotMessageContent | RobotRenderContentItem[] | unknown): string => {
if (typeof content === 'string') {
return content
}
if (!Array.isArray(content)) {
return ''
}
return content
.map((item) => {
if (typeof item === 'string') {
return item
}
const typedItem = item as RobotInputContentPart | RobotRenderContentItem
if (typedItem?.type === RobotMessageContentType.Text) {
return typedItem.text ?? typedItem.content ?? ''
}
return ''
})
.filter(Boolean)
.join('\n')
}
* 合并字符串字段。如果值是对象,则递归合并字符串字段
* @param target 目标对象
* @param source 源对象
* @returns 合并后的对象
*/
export const mergeStringFields = (target: Record<string, any>, source: Record<string, any>) => {
for (const [key, value] of Object.entries(source)) {
const targetValue = target[key]
if (targetValue) {
if (typeof targetValue === 'string' && typeof value === 'string') {
target[key] = targetValue + value
} else if (targetValue && typeof targetValue === 'object' && value && typeof value === 'object') {
target[key] = mergeStringFields(targetValue, value)
}
} else {
target[key] = value
}
}
return target
}
export const processSSEStream = (data: string, handler: StreamHandler) => {
let finishReason: string | undefined
let latestFinishReason: string | undefined
const lines = data.split('\n\n')
lines.pop()
for (const line of lines) {
if (line.trim() === '') continue
if (line.trim() === 'data: [DONE]') {
if (latestFinishReason) {
finishReason = latestFinishReason
}
handler.onDone(finishReason)
continue
}
try {
const dataMatch = line.match(/^data: (.+)$/m)
if (!dataMatch) continue
const data = JSON.parse(dataMatch[1])
handler.onData(data)
latestFinishReason = data.choices?.[0]?.finish_reason || undefined
} catch (error) {
console.error('Error parsing SSE message:', error, line)
}
}
}
export const removeLoading = (messages: RobotMessage[], name?: string) => {
const renderContent = messages.at(-1)?.renderContent
if (!renderContent || !renderContent.length) return
const index = renderContent.findLastIndex(
(item) => item.type.includes('loading') && (name ? item.content === name : true)
)
if (index !== -1) {
renderContent?.splice(index, 1)
}
}
export const addSystemPrompt = (messages: LLMMessage[], prompt: string = '') => {
if (!messages.length || messages[0].role !== 'system') {
messages.unshift({ role: 'system', content: prompt })
} else if (messages[0].role === 'system' && messages[0].content !== prompt) {
messages[0].content = prompt
}
}