mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-14 09:11:54 +00:00
314 lines
8 KiB
TypeScript
314 lines
8 KiB
TypeScript
import type { ThreadMessage } from '@assistant-ui/react'
|
|
|
|
import type { QuickModelOption } from '@/app/chat/composer/types'
|
|
import type { ClientSessionState, CommandDispatchResponse } from '@/app/types'
|
|
import { formatRefValue } from '@/components/assistant-ui/directive-text'
|
|
import { type ChatMessage, type ChatMessagePart, chatMessageText, textPart } from '@/lib/chat-messages'
|
|
import type { ComposerAttachment } from '@/store/composer'
|
|
import type { ModelOptionsResponse, SessionInfo } from '@/types/hermes'
|
|
|
|
export const INTERRUPTED_MARKER = '\n\n_[interrupted]_'
|
|
export const SLASH_COMMAND_RE = /^\/[^\s/]*(?:\s|$)/
|
|
export const BUILTIN_PERSONALITIES = [
|
|
'helpful',
|
|
'concise',
|
|
'technical',
|
|
'creative',
|
|
'teacher',
|
|
'kawaii',
|
|
'catgirl',
|
|
'pirate',
|
|
'shakespeare',
|
|
'surfer',
|
|
'noir',
|
|
'uwu',
|
|
'philosopher',
|
|
'hype'
|
|
]
|
|
|
|
const THINKING_STATUS_PREFIX_RE =
|
|
/^\s*(?:(?:[^\s.]{1,16})\s+)?(?:processing|thinking|reasoning|analyzing|pondering|contemplating|musing|cogitating|ruminating|deliberating|mulling|reflecting|computing|synthesizing|formulating|brainstorming)\.\.\.\s*/i
|
|
|
|
const EMPTY_THINKING_PLACEHOLDER_RE =
|
|
/\b(?:current rewritten thinking|next thinking to process|provide the thinking content|don't see any .*thinking)\b/i
|
|
|
|
export function createClientSessionState(
|
|
storedSessionId: string | null = null,
|
|
messages: ChatMessage[] = []
|
|
): ClientSessionState {
|
|
return {
|
|
storedSessionId,
|
|
messages,
|
|
busy: false,
|
|
awaitingResponse: false,
|
|
streamId: null,
|
|
sawAssistantPayload: false,
|
|
pendingBranchGroup: null,
|
|
interrupted: false
|
|
}
|
|
}
|
|
|
|
export function sessionTitle(session: SessionInfo): string {
|
|
return session.title?.trim() || session.preview?.trim() || 'Untitled session'
|
|
}
|
|
|
|
export function coerceGatewayText(value: unknown): string {
|
|
if (typeof value === 'string') {
|
|
return value
|
|
}
|
|
|
|
if (value === null || value === undefined) {
|
|
return ''
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value
|
|
.map(item => {
|
|
if (typeof item === 'string') {
|
|
return item
|
|
}
|
|
|
|
if (item && typeof item === 'object') {
|
|
const row = item as Record<string, unknown>
|
|
|
|
if (typeof row.text === 'string') {
|
|
return row.text
|
|
}
|
|
|
|
if (typeof row.output_text === 'string') {
|
|
return row.output_text
|
|
}
|
|
}
|
|
|
|
return ''
|
|
})
|
|
.join('')
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
const row = value as Record<string, unknown>
|
|
|
|
if (typeof row.text === 'string') {
|
|
return row.text
|
|
}
|
|
|
|
if (typeof row.output_text === 'string') {
|
|
return row.output_text
|
|
}
|
|
|
|
try {
|
|
return JSON.stringify(value)
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
return String(value)
|
|
}
|
|
|
|
export function coerceThinkingText(value: unknown): string {
|
|
const text = coerceGatewayText(value).replace(THINKING_STATUS_PREFIX_RE, '').trim()
|
|
|
|
return EMPTY_THINKING_PLACEHOLDER_RE.test(text) ? '' : text
|
|
}
|
|
|
|
export function isImageGenerationTool(name?: string): boolean {
|
|
return name === 'image_generate'
|
|
}
|
|
|
|
export function contextPath(path: string, cwd: string): string {
|
|
if (!cwd) {
|
|
return path
|
|
}
|
|
|
|
const normalizedCwd = cwd.endsWith('/') ? cwd : `${cwd}/`
|
|
|
|
return path.startsWith(normalizedCwd) ? path.slice(normalizedCwd.length) : path
|
|
}
|
|
|
|
export function attachmentId(kind: ComposerAttachment['kind'], value: string): string {
|
|
return `${kind}:${value}`
|
|
}
|
|
|
|
export function pathLabel(path: string): string {
|
|
return path.split(/[\\/]/).filter(Boolean).pop() || path
|
|
}
|
|
|
|
export function attachmentDisplayText(attachment: ComposerAttachment): string | null {
|
|
if (attachment.refText) {
|
|
return attachment.refText
|
|
}
|
|
|
|
if (attachment.kind === 'image') {
|
|
const id = attachment.detail || attachment.path || attachment.label
|
|
|
|
return id ? `@image:${formatRefValue(id)}` : null
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function personalityNamesFromConfig(config: unknown): string[] {
|
|
const root = config && typeof config === 'object' ? (config as Record<string, unknown>) : {}
|
|
const agent = root.agent && typeof root.agent === 'object' ? (root.agent as Record<string, unknown>) : {}
|
|
const personalities = agent.personalities
|
|
|
|
return personalities && typeof personalities === 'object' && !Array.isArray(personalities)
|
|
? Object.keys(personalities as Record<string, unknown>)
|
|
: []
|
|
}
|
|
|
|
export function normalizePersonalityValue(value: string): string {
|
|
const trimmed = value.trim().toLowerCase()
|
|
|
|
return !trimmed || trimmed === 'default' || trimmed === 'none' ? '' : trimmed
|
|
}
|
|
|
|
export function parseSlashCommand(command: string) {
|
|
const match = command.replace(/^\/+/, '').match(/^(\S+)\s*(.*)$/)
|
|
|
|
return match ? { name: match[1], arg: match[2].trim() } : { name: '', arg: '' }
|
|
}
|
|
|
|
export function parseCommandDispatch(raw: unknown): CommandDispatchResponse | null {
|
|
if (!raw || typeof raw !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const row = raw as Record<string, unknown>
|
|
const str = (value: unknown) => (typeof value === 'string' ? value : undefined)
|
|
|
|
switch (row.type) {
|
|
case 'exec':
|
|
|
|
case 'plugin':
|
|
return { type: row.type, output: str(row.output) }
|
|
|
|
case 'alias':
|
|
return typeof row.target === 'string' ? { type: 'alias', target: row.target } : null
|
|
|
|
case 'skill':
|
|
return typeof row.name === 'string' ? { type: 'skill', name: row.name, message: str(row.message) } : null
|
|
|
|
case 'send':
|
|
return typeof row.message === 'string' ? { type: 'send', message: row.message } : null
|
|
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function quickModelOptions(
|
|
data: ModelOptionsResponse | undefined,
|
|
currentProvider: string,
|
|
currentModel: string
|
|
): QuickModelOption[] {
|
|
const seen = new Set<string>()
|
|
const options: QuickModelOption[] = []
|
|
|
|
const providers = [...(data?.providers ?? [])].sort((a, b) => {
|
|
if (a.slug === currentProvider) {
|
|
return -1
|
|
}
|
|
|
|
if (b.slug === currentProvider) {
|
|
return 1
|
|
}
|
|
|
|
if (a.is_current) {
|
|
return -1
|
|
}
|
|
|
|
if (b.is_current) {
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
})
|
|
|
|
const add = (provider: string, providerName: string, model: string) => {
|
|
const key = `${provider}:${model}`
|
|
|
|
if (!model || seen.has(key)) {
|
|
return
|
|
}
|
|
|
|
seen.add(key)
|
|
options.push({ provider, providerName, model })
|
|
}
|
|
|
|
if (currentProvider && currentModel) {
|
|
add(currentProvider, currentProvider, currentModel)
|
|
}
|
|
|
|
for (const provider of providers) {
|
|
const models = [...(provider.models ?? [])].sort((a, b) => {
|
|
if (provider.slug === currentProvider && a === currentModel) {
|
|
return -1
|
|
}
|
|
|
|
if (provider.slug === currentProvider && b === currentModel) {
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
})
|
|
|
|
for (const model of models) {
|
|
add(provider.slug, provider.name, model)
|
|
}
|
|
|
|
if (options.length >= 8) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return options.slice(0, 8)
|
|
}
|
|
|
|
export function toRuntimeMessage(message: ChatMessage): ThreadMessage {
|
|
const role =
|
|
message.role === 'user' || message.role === 'assistant' || message.role === 'system' ? message.role : 'assistant'
|
|
|
|
const createdAt = message.timestamp
|
|
? new Date(message.timestamp * 1000)
|
|
: new Date(Number(message.id.match(/\d+/)?.[0]) || Date.now())
|
|
|
|
if (role === 'user') {
|
|
return {
|
|
id: message.id,
|
|
role,
|
|
content: message.parts.filter((part): part is Extract<ChatMessagePart, { type: 'text' }> => part.type === 'text'),
|
|
attachments: [],
|
|
createdAt,
|
|
metadata: { custom: {} }
|
|
} as ThreadMessage
|
|
}
|
|
|
|
if (role === 'system') {
|
|
const text = chatMessageText(message)
|
|
|
|
return {
|
|
id: message.id,
|
|
role,
|
|
content: [textPart(text)],
|
|
createdAt,
|
|
metadata: { custom: {} }
|
|
} as ThreadMessage
|
|
}
|
|
|
|
return {
|
|
id: message.id,
|
|
role,
|
|
content: message.parts as Extract<ThreadMessage, { role: 'assistant' }>['content'],
|
|
createdAt,
|
|
status: message.pending ? { type: 'running' } : { type: 'complete', reason: 'stop' },
|
|
metadata: {
|
|
unstable_state: null,
|
|
unstable_annotations: [],
|
|
unstable_data: [],
|
|
steps: [],
|
|
custom: {}
|
|
}
|
|
} as ThreadMessage
|
|
}
|