mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-11 17:29:03 +02:00
feat: effort de raisonnement Hermes complet sur les trois transports (v2.5.3)
- Huit niveaux (none, minimal, low, medium, high, xhigh, max, ultra) au lieu de low / medium / high, avec libellés explicatifs dans Paramètres → Hermes. - model_options.reasoning_effort envoyé sur runs, sessions et chat completions ; il ne l'était qu'en chat completions. - Test des trois transports (effort présent, absent quand laissé au serveur). - Le sélecteur de modèle par fournisseur ayant été livré sur master en 2.5.2, cette branche ne garde que l'effort de raisonnement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMpgFriwxiBgurUVb21oCE
This commit is contained in:
8 files changed
+81
-12
No files matched your search
@@ -1,7 +1,7 @@
|
||||
# EveFlow 2 — Interface vocale JARVIS pour Hermes Agent
|
||||
|
||||
[](https://github.com/R0m1k3/EveFlow/actions)
|
||||
[](https://github.com/R0m1k3/EveFlow/releases)
|
||||
[](https://github.com/R0m1k3/EveFlow/releases)
|
||||
[](LICENSE)
|
||||
|
||||
**EveFlow** est un compagnon de bureau Windows qui transforme [Hermes Agent](https://hermes-agent.nousresearch.com/) en assistant vocal à la JARVIS : un noyau holographique réactif au son, une conversation en streaming, les outils, sous-agents, approbations, crons, skills et sessions d'Hermes pilotés depuis un seul HUD.
|
||||
@@ -46,6 +46,7 @@ La version 2 est une réécriture complète : plus de robot 3D, un pipeline voca
|
||||
1. **Runs API** (`POST /v1/runs` + `GET /v1/runs/{id}/events`) : deltas, outils, sous-agents, `approval.request`, `run.completed`, arrêt (`/stop`) et injection de consignes en cours de run (`/steer`).
|
||||
2. **Sessions API** (`/api/sessions/{id}/chat/stream`) : mémoire côté serveur, fork, suppression, relecture de l'historique.
|
||||
3. **Chat completions** OpenAI (`/v1/chat/completions`) avec `hermes.tool.progress`, continuité de session (`X-Hermes-Session-Id`) et outils EveFlow côté client (état du HUD, fichiers partagés, notifications).
|
||||
* **Effort de raisonnement** : les huit niveaux d'Hermes (none, minimal, low, medium, high, xhigh, max, ultra) au choix dans Paramètres → Hermes, envoyés dans `model_options.reasoning_effort` sur les trois transports (runs, sessions, chat completions).
|
||||
* Mémoire longue durée via `X-Hermes-Session-Key`.
|
||||
* **Approbations** d'outils affichées dans le HUD : une fois, pour la session, toujours, refuser.
|
||||
* **Crons** : création en langage naturel (`every 1h`, `weekdays at 9am`, `in 30m`, expression cron), pause/reprise, exécution immédiate, édition, historique des résultats lus à voix haute.
|
||||
|
||||
@@ -84,6 +84,7 @@ Sources : [jarvis-desktop-ai](https://github.com/ccarloshenri/jarvis-desktop-ai)
|
||||
| Correctifs 2.4.0.1 | réponse vide en chat completions désormais expliquée (JSON non streamé ou erreur HTTP 200), la liaison ne passe plus en « dégradé » quand seule l'API des crons échoue, transcriptions parasites (« (cliquant) », « *Claire* ») ignorées, préférence de voix masculine/féminine |
|
||||
| Parakeet v3 vs Whisper base sur trois phrases Piper (fr) (2.4.1) | Parakeet : 3/3 exactes avec ponctuation, 0,4 à 0,5 s à chaud (6,7 s au premier appel) ; Whisper base : erreurs sur « Jarvis », « Peux-tu », 0,7 à 0,9 s |
|
||||
| Worklets audio sous CSP stricte (2.4.1) | chargement des modules statiques OK dans l'application empaquetée |
|
||||
| Effort de raisonnement Hermes (2.5.3) | huit niveaux (none → ultra) dans les réglages, transmis dans `model_options.reasoning_effort` sur runs, sessions et chat completions (auparavant chat completions seulement, trois niveaux) ; test des trois transports |
|
||||
| Voix françaises (2.5.0) | Edge Henri : MP3 reçu de bout en bout (32 ko pour 4 s). Supertonic 3 en français : 10 voix, RTF 0,14 sur 4 cœurs (8,7 s d'audio en 1,3 s), genres déterminés par mesure de la fréquence fondamentale (voix 0-4 : 170-210 Hz, voix 5-9 : 92-137 Hz) ; le worker compilé accepte `language` et bascule sur l'anglais pour une langue inconnue |
|
||||
| Reconnaissance française : Parakeet v3 vs Qwen3-ASR 0.6B int8 (2.5.0) | Six phrases Supertonic (3,8 s) : Parakeet WER 8,5 % (erreurs surtout de forme : « 14h30 »), 384 ms par phrase ; Qwen3-ASR WER 15,3 % (« mémoires vivres »), 1 477 ms, 940 Mo. Qwen3-ASR n'est pas ajouté au catalogue |
|
||||
| Serveur MCP (2.4.0) | initialize, tools/list (14 outils), tools/call côté principal (presse-papiers, capture image) et côté renderer (état, message dans le fil) |
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eveflow",
|
||||
"version": "2.5.2",
|
||||
"releaseVersion": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"releaseVersion": "2.5.3",
|
||||
"description": "JARVIS-style desktop HUD for Hermes Agent: voice, streaming runs, scheduled jobs, skills and telemetry",
|
||||
"main": "dist-electron/main.js",
|
||||
"private": true,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useVoice } from '../../state/voice';
|
||||
import { installedModels, useVoiceModels } from '../../state/voiceModels';
|
||||
import { ModelsSection } from './ModelsSection';
|
||||
import { ModelSelect } from './ModelSelect';
|
||||
import { REASONING_EFFORTS, isReasoningEffort } from '../../lib/hermesReasoning';
|
||||
|
||||
type Section = 'general' | 'hermes' | 'voice' | 'speech' | 'models' | 'webhook' | 'notifications' | 'ui';
|
||||
|
||||
@@ -244,12 +245,10 @@ export function SettingsDrawer({ onClose }: Props) {
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Effort de raisonnement</label>
|
||||
<select className="select" value={settings.hermes.reasoningEffort} onChange={(e) => update({ hermes: { reasoningEffort: e.target.value as typeof settings.hermes.reasoningEffort } })}>
|
||||
<option value="">Défaut</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<select className="select" value={settings.hermes.reasoningEffort} onChange={(e) => { if (isReasoningEffort(e.target.value)) update({ hermes: { reasoningEffort: e.target.value } }); }}>
|
||||
{REASONING_EFFORTS.map((e) => <option key={e.value} value={e.value}>{e.label}</option>)}
|
||||
</select>
|
||||
<span className="hint">Envoyé à chaque requête (runs, sessions, chat completions) dans model_options.reasoning_effort. Plus c’est haut, plus la réponse est réfléchie et lente.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/** Reasoning effort levels accepted by Hermes (`agent.reasoning_effort` / `model_options.reasoning_effort`). */
|
||||
import type { HermesReasoningEffort } from '../services/hermes/types';
|
||||
|
||||
export const REASONING_EFFORTS: Array<{ value: HermesReasoningEffort; label: string }> = [
|
||||
{ value: '', label: 'Défaut du serveur (medium)' },
|
||||
{ value: 'none', label: 'none : pas de réflexion' },
|
||||
{ value: 'minimal', label: 'minimal' },
|
||||
{ value: 'low', label: 'low : rapide' },
|
||||
{ value: 'medium', label: 'medium : équilibré' },
|
||||
{ value: 'high', label: 'high : réponses plus fouillées' },
|
||||
{ value: 'xhigh', label: 'xhigh' },
|
||||
{ value: 'max', label: 'max' },
|
||||
{ value: 'ultra', label: 'ultra : le plus lent' }
|
||||
];
|
||||
|
||||
export function isReasoningEffort(value: string): value is HermesReasoningEffort {
|
||||
return REASONING_EFFORTS.some((e) => e.value === value);
|
||||
}
|
||||
@@ -160,6 +160,11 @@ export class HermesClient {
|
||||
return hermesBaseUrl(this.config.url);
|
||||
}
|
||||
|
||||
/** `model_options.reasoning_effort`, accepted by runs, session chat and chat completions alike. */
|
||||
private reasoningOptions(): Rec {
|
||||
return this.config.reasoningEffort ? { model_options: { reasoning_effort: this.config.reasoningEffort } } : {};
|
||||
}
|
||||
|
||||
private headers(extra: Record<string, string> = {}, json = true): Record<string, string> {
|
||||
const h: Record<string, string> = { ...extra };
|
||||
if (json) h['Content-Type'] = 'application/json';
|
||||
@@ -335,7 +340,7 @@ export class HermesClient {
|
||||
// ── Runs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async startRun(body: { input: string; session_id?: string; instructions?: string; model?: string; provider?: string }): Promise<{ run_id: string; status: string }> {
|
||||
const payload: Rec = { input: body.input };
|
||||
const payload: Rec = { input: body.input, ...this.reasoningOptions() };
|
||||
if (body.session_id) payload.session_id = body.session_id;
|
||||
if (body.instructions) payload.instructions = body.instructions;
|
||||
if (body.model) payload.model = body.model;
|
||||
@@ -495,7 +500,7 @@ export class HermesClient {
|
||||
}
|
||||
if (isAborted()) return '';
|
||||
const realId = sessionId.slice(3);
|
||||
const body: Rec = { input: options.text };
|
||||
const body: Rec = { input: options.text, ...this.reasoningOptions() };
|
||||
if (this.config.instructions) body.instructions = this.config.instructions;
|
||||
Object.assign(body, modelSelection(this.config.model));
|
||||
|
||||
@@ -539,7 +544,7 @@ export class HermesClient {
|
||||
payload.tools = options.localToolDefinitions;
|
||||
payload.tool_choice = 'auto';
|
||||
}
|
||||
if (this.config.reasoningEffort) payload.model_options = { reasoning_effort: this.config.reasoningEffort };
|
||||
Object.assign(payload, this.reasoningOptions());
|
||||
|
||||
const toolCalls = new Map<number, CompletionToolCallAccumulator>();
|
||||
let finishReason: string | null = null;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export type HermesTransport = 'auto' | 'runs' | 'sessions' | 'completions';
|
||||
|
||||
/** Thinking depth accepted by Hermes (`model_options.reasoning_effort`); empty = server default (medium). */
|
||||
export type HermesReasoningEffort = '' | 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra';
|
||||
|
||||
export interface HermesConfig {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
@@ -7,7 +10,7 @@ export interface HermesConfig {
|
||||
/** Stable per-user key for long-term memory (X-Hermes-Session-Key). */
|
||||
sessionKey: string;
|
||||
transport: HermesTransport;
|
||||
reasoningEffort: '' | 'low' | 'medium' | 'high';
|
||||
reasoningEffort: HermesReasoningEffort;
|
||||
/** Extra instructions layered on top of the Hermes system prompt. */
|
||||
instructions: string;
|
||||
/** Model used in "mission" mode (long tasks); empty = same as `model`. */
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { HermesClient } from '../src/services/hermes/client';
|
||||
import { httpFetch, httpStream } from '../src/lib/transport';
|
||||
import { DEFAULT_SETTINGS } from '../src/state/settings';
|
||||
import { REASONING_EFFORTS, isReasoningEffort } from '../src/lib/hermesReasoning';
|
||||
|
||||
vi.mock('../src/lib/transport', async (original) => ({ ...await original<typeof import('../src/lib/transport')>(), httpFetch: vi.fn(), httpStream: vi.fn() }));
|
||||
const config = { ...DEFAULT_SETTINGS.hermes, url: 'https://example.test', apiKey: 'k' };
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe('reasoning effort levels', () => {
|
||||
it('lists every level Hermes accepts, server default first', () => {
|
||||
expect(REASONING_EFFORTS.map((e) => e.value)).toEqual(['', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
||||
expect(isReasoningEffort('xhigh')).toBe(true);
|
||||
expect(isReasoningEffort('')).toBe(true);
|
||||
expect(isReasoningEffort('turbo')).toBe(false);
|
||||
});
|
||||
|
||||
it.each(['runs', 'sessions', 'completions'] as const)('sends model_options.reasoning_effort over %s', async (transport) => {
|
||||
const requests: Record<string, unknown>[] = [];
|
||||
const capture = async (req: { body?: string }) => {
|
||||
requests.push(JSON.parse(req.body ?? '{}') as Record<string, unknown>);
|
||||
throw new Error('stop');
|
||||
};
|
||||
vi.mocked(httpFetch).mockImplementation(capture as never);
|
||||
vi.mocked(httpStream).mockImplementation(capture as never);
|
||||
const send = new HermesClient({ ...config, reasoningEffort: 'high' }).send({ text: 'Bonjour', sessionId: 'hs:test', history: [], onEvent: vi.fn() }, transport);
|
||||
await expect(send.result).rejects.toThrow('stop');
|
||||
expect(requests[0]).toMatchObject({ model_options: { reasoning_effort: 'high' } });
|
||||
});
|
||||
|
||||
it('omits model_options when the effort is left to the server', async () => {
|
||||
const requests: Record<string, unknown>[] = [];
|
||||
vi.mocked(httpFetch).mockImplementation((async (req: { body?: string }) => {
|
||||
requests.push(JSON.parse(req.body ?? '{}') as Record<string, unknown>);
|
||||
throw new Error('stop');
|
||||
}) as never);
|
||||
const send = new HermesClient({ ...config, reasoningEffort: '' }).send({ text: 'Bonjour', sessionId: 'test', history: [], onEvent: vi.fn() }, 'runs');
|
||||
await expect(send.result).rejects.toThrow('stop');
|
||||
expect(requests[0]).not.toHaveProperty('model_options');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user