mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-10 13:31:38 +00:00
opentui(copy): assistant-text extraction helpers
This commit is contained in:
parent
aa5489e804
commit
0f53d67ee4
2 changed files with 130 additions and 0 deletions
36
ui-tui-opentui-v2/src/logic/copy.ts
Normal file
36
ui-tui-opentui-v2/src/logic/copy.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Assistant-text extraction (the /copy command's pure logic). An assistant turn's
|
||||
* answer lives in `parts` (the `type:'text'` fragments, concatenated) while live,
|
||||
* OR in `.text` once settled/resumed. We copy the ANSWER only — reasoning and tool
|
||||
* parts are excluded. `nthAssistantResponse` indexes newest-first (1-based).
|
||||
*/
|
||||
import type { Message } from './store.ts'
|
||||
|
||||
/** The answer text of one message: concat the `text` parts (trimmed) when live, else `.text`. */
|
||||
export function messageText(m: Message): string {
|
||||
if (m.parts && m.parts.length) {
|
||||
return m.parts
|
||||
.filter(p => p.type === 'text')
|
||||
.map(p => p.text)
|
||||
.join('')
|
||||
.trim()
|
||||
}
|
||||
return m.text
|
||||
}
|
||||
|
||||
/** Newest-first list of the non-empty answer text for every assistant message. */
|
||||
export function assistantResponses(messages: Message[]): string[] {
|
||||
const out: string[] = []
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i]
|
||||
if (!m || m.role !== 'assistant') continue
|
||||
const text = messageText(m)
|
||||
if (text) out.push(text)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** The n-th newest assistant response (1-based; n=1 → last). `undefined` if out of range. */
|
||||
export function nthAssistantResponse(messages: Message[], n: number): string | undefined {
|
||||
return assistantResponses(messages)[n - 1]
|
||||
}
|
||||
94
ui-tui-opentui-v2/src/test/copy.test.ts
Normal file
94
ui-tui-opentui-v2/src/test/copy.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Assistant-text extraction helpers (the /copy command's logic). Pure functions:
|
||||
* pull the answer text out of a live (parts) or settled (.text) assistant turn,
|
||||
* excluding reasoning/tool parts; pick the n-th newest assistant response.
|
||||
*/
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { assistantResponses, messageText, nthAssistantResponse } from '../logic/copy.ts'
|
||||
import type { Message } from '../logic/store.ts'
|
||||
|
||||
describe('messageText', () => {
|
||||
test('a live parts turn concatenates text parts; excludes reasoning/tool', () => {
|
||||
const m: Message = {
|
||||
role: 'assistant',
|
||||
text: '',
|
||||
parts: [
|
||||
{ type: 'reasoning', id: 'p1', text: 'thinking…' },
|
||||
{ type: 'text', id: 'p2', text: 'Hello' },
|
||||
{ type: 'tool', id: 't1', name: 'bash', state: 'complete', resultText: 'ran' },
|
||||
{ type: 'text', id: 'p3', text: ' world' }
|
||||
]
|
||||
}
|
||||
expect(messageText(m)).toBe('Hello world')
|
||||
})
|
||||
|
||||
test('trims surrounding whitespace from concatenated text parts', () => {
|
||||
const m: Message = {
|
||||
role: 'assistant',
|
||||
text: '',
|
||||
parts: [{ type: 'text', id: 'p1', text: ' spaced ' }]
|
||||
}
|
||||
expect(messageText(m)).toBe('spaced')
|
||||
})
|
||||
|
||||
test('a settled/resumed turn (no parts) returns .text', () => {
|
||||
const m: Message = { role: 'assistant', text: 'resumed answer' }
|
||||
expect(messageText(m)).toBe('resumed answer')
|
||||
})
|
||||
|
||||
test('empty parts array falls back to .text', () => {
|
||||
const m: Message = { role: 'assistant', text: 'flat body', parts: [] }
|
||||
expect(messageText(m)).toBe('flat body')
|
||||
})
|
||||
})
|
||||
|
||||
describe('assistantResponses', () => {
|
||||
test('picks only assistant rows, newest-first, non-empty', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', text: 'welcome' },
|
||||
{ role: 'user', text: 'hi' },
|
||||
{ role: 'assistant', text: 'first reply' },
|
||||
{ role: 'user', text: 'and?' },
|
||||
{ role: 'assistant', text: '', parts: [{ type: 'text', id: 'p1', text: 'second reply' }] }
|
||||
]
|
||||
expect(assistantResponses(messages)).toEqual(['second reply', 'first reply'])
|
||||
})
|
||||
|
||||
test('skips assistant rows that resolve to empty text', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'assistant', text: 'kept' },
|
||||
{ role: 'assistant', text: '', parts: [{ type: 'reasoning', id: 'r1', text: 'only thinking' }] }
|
||||
]
|
||||
expect(assistantResponses(messages)).toEqual(['kept'])
|
||||
})
|
||||
|
||||
test('empty messages → []', () => {
|
||||
expect(assistantResponses([])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('nthAssistantResponse', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'assistant', text: 'oldest' },
|
||||
{ role: 'user', text: 'q' },
|
||||
{ role: 'assistant', text: 'newest' }
|
||||
]
|
||||
|
||||
test('n=1 is the last assistant response', () => {
|
||||
expect(nthAssistantResponse(messages, 1)).toBe('newest')
|
||||
})
|
||||
|
||||
test('n=2 is the previous assistant response', () => {
|
||||
expect(nthAssistantResponse(messages, 2)).toBe('oldest')
|
||||
})
|
||||
|
||||
test('n past the end → undefined', () => {
|
||||
expect(nthAssistantResponse(messages, 3)).toBeUndefined()
|
||||
})
|
||||
|
||||
test('no assistant responses → undefined', () => {
|
||||
expect(nthAssistantResponse([{ role: 'user', text: 'hi' }], 1)).toBeUndefined()
|
||||
expect(nthAssistantResponse([], 1)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue