mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-14 02:36:32 +02:00
fix: keep Hermes session across runs and speak long replies as a digest (2.5.3)
Over the runs transport Hermes never reports the session it attached to a
run in the SSE stream, only in GET /v1/runs/{id}. EveFlow only read it in the
polling fallback, so the locally generated id was sent again and again and
each message opened a fresh Hermes session, losing the conversation context.
The client now adopts the run's session id after every run.
Long answers are now spoken as a digest: the first sentences (configurable,
4 by default) plus the closing question, followed by a short notice that the
full text is on screen. The default instructions ask Hermes to open long
replies with the essentials and to rely on the ongoing conversation.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ln4KL1feHtnV4nee4sZFhM
This commit is contained in:
12 files changed
+256
-16
No files matched your search
@@ -0,0 +1,59 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DEFAULT_SETTINGS, useSettings } from '../src/state/settings';
|
||||
|
||||
const enqueue = vi.fn((text: string) => /[\p{L}\p{N}]/u.test(text.replace(/```[\s\S]*?```/g, '')));
|
||||
const speak = vi.fn();
|
||||
const stop = vi.fn();
|
||||
vi.mock('../src/services/voice/tts', () => ({
|
||||
TtsEngine: class {
|
||||
enqueue = enqueue;
|
||||
speak = speak;
|
||||
stop = stop;
|
||||
onState() { return () => undefined; }
|
||||
updateConfig() { /* noop */ }
|
||||
get isActive() { return false; }
|
||||
}
|
||||
}));
|
||||
|
||||
const { speech, DIGEST_NOTICE } = await import('../src/services/voice/speech');
|
||||
|
||||
function stream(text: string, size = 7): void {
|
||||
for (let i = 0; i < text.length; i += size) speech.pushStream(text.slice(i, i + size));
|
||||
}
|
||||
const spoken = () => enqueue.mock.calls.map((c) => c[0]);
|
||||
|
||||
describe('spoken digest of streamed replies', () => {
|
||||
beforeEach(() => {
|
||||
enqueue.mockClear();
|
||||
speak.mockClear();
|
||||
useSettings.setState({ settings: { ...DEFAULT_SETTINGS, speech: { ...DEFAULT_SETTINGS.speech, summarizeReplies: true, replySentences: 2 } } });
|
||||
});
|
||||
afterEach(() => speech.stop());
|
||||
|
||||
it('stops after the configured sentences, then adds the closing question and a notice', () => {
|
||||
const text = 'Première phrase du rapport. Deuxième phrase utile. Troisième phrase de détail. Quatrième phrase encore. Tu veux la suite ?';
|
||||
stream(text);
|
||||
speech.endStream(text);
|
||||
expect(spoken()).toEqual(['Première phrase du rapport.', 'Deuxième phrase utile.', 'Tu veux la suite ?', DIGEST_NOTICE]);
|
||||
});
|
||||
|
||||
it('speaks short replies in full without any notice', () => {
|
||||
const text = 'Une phrase courte. Une seconde phrase';
|
||||
stream(text);
|
||||
speech.endStream(text);
|
||||
expect(spoken()).toEqual(['Une phrase courte.', 'Une seconde phrase']);
|
||||
});
|
||||
|
||||
it('speaks everything when the digest is disabled', () => {
|
||||
useSettings.setState({ settings: { ...DEFAULT_SETTINGS, speech: { ...DEFAULT_SETTINGS.speech, summarizeReplies: false } } });
|
||||
const text = 'Première phrase du rapport. Deuxième phrase utile. Troisième phrase de détail. Quatrième phrase encore.';
|
||||
stream(text);
|
||||
speech.endStream(text);
|
||||
expect(spoken()).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('digests a reply delivered in one piece', () => {
|
||||
speech.sayReply('Première phrase du rapport. Deuxième phrase utile. Troisième phrase de détail. On continue ?');
|
||||
expect(speak).toHaveBeenCalledWith(`Première phrase du rapport. Deuxième phrase utile. On continue ? ${DIGEST_NOTICE}`, expect.anything());
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user