fix(chat): support Hermes clarify batches
This commit is contained in:
@@ -189,6 +189,11 @@ const queuedMessages = computed(() => {
|
||||
const visibleApproval = computed(() => chatStore.activePendingApproval);
|
||||
const visibleClarify = computed(() => chatStore.activePendingClarify);
|
||||
const clarifyResponse = ref("");
|
||||
const clarifySelections = ref<string[]>([]);
|
||||
watch(() => visibleClarify.value?.clarifyId, () => {
|
||||
clarifyResponse.value = "";
|
||||
clarifySelections.value = [];
|
||||
});
|
||||
const hasFloatingPrompt = computed(() => !!visibleApproval.value || !!visibleClarify.value);
|
||||
const virtualListPadding = computed(() => {
|
||||
if (queuedMessages.value.length > 0 && hasFloatingPrompt.value) return "20px 20px 380px";
|
||||
@@ -242,6 +247,13 @@ function handleClarify(response?: string) {
|
||||
clarifyResponse.value = "";
|
||||
}
|
||||
|
||||
function handleClarifyMulti() {
|
||||
const responses = [...clarifySelections.value];
|
||||
const custom = clarifyResponse.value.trim();
|
||||
if (custom) responses.push(custom);
|
||||
handleClarify(responses.length ? JSON.stringify(responses) : '');
|
||||
}
|
||||
|
||||
function removeQueuedMessage(messageId: string) {
|
||||
const sid = chatStore.activeSessionId;
|
||||
if (!sid) return;
|
||||
@@ -829,7 +841,10 @@ defineExpose({
|
||||
</div>
|
||||
<div class="approval-float-title">{{ t("chat.clarifyTitle") }}</div>
|
||||
<div class="approval-float-desc">{{ visibleClarify.question }}</div>
|
||||
<div v-if="visibleClarify.choices && visibleClarify.choices.length" class="approval-float-actions">
|
||||
<a-checkbox-group v-if="visibleClarify.multiSelect && visibleClarify.choices?.length" v-model="clarifySelections" class="approval-float-actions">
|
||||
<a-checkbox v-for="choice in visibleClarify.choices" :key="choice" :value="choice">{{ choice }}</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
<div v-else-if="visibleClarify.choices && visibleClarify.choices.length" class="approval-float-actions">
|
||||
<a-button
|
||||
v-for="choice in visibleClarify.choices"
|
||||
:key="choice"
|
||||
@@ -839,19 +854,19 @@ defineExpose({
|
||||
>
|
||||
{{ choice }}
|
||||
</a-button>
|
||||
<a-button size="small" status="danger" type="secondary" @click="handleClarify('')">
|
||||
{{ t("chat.clarifyDismiss") }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-else class="clarify-float-input-row">
|
||||
<div class="clarify-float-input-row">
|
||||
<a-input
|
||||
v-model="clarifyResponse"
|
||||
size="small"
|
||||
:placeholder="t('chat.clarifyPlaceholder')"
|
||||
/>
|
||||
<a-button size="small" type="primary" @click="handleClarify()">
|
||||
<a-button size="small" type="primary" @click="visibleClarify.multiSelect ? handleClarifyMulti() : handleClarify()">
|
||||
{{ t("chat.clarifySubmit") }}
|
||||
</a-button>
|
||||
<a-button size="small" status="danger" type="secondary" @click="handleClarify('')">
|
||||
{{ t("chat.clarifyDismiss") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -88,6 +88,7 @@ export interface PendingClarify {
|
||||
clarifyId: string
|
||||
question: string
|
||||
choices: string[] | null
|
||||
multiSelect: boolean
|
||||
timeoutMs: number
|
||||
requestedAt: number
|
||||
}
|
||||
@@ -2113,6 +2114,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
clarifyId,
|
||||
question: String((evt as any).question || ''),
|
||||
choices: Array.isArray((evt as any).choices) ? (evt as any).choices : null,
|
||||
multiSelect: !!(evt as any).multi_select,
|
||||
timeoutMs: Number((evt as any).timeout_ms) || 300000,
|
||||
requestedAt: Date.now(),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Adapt Hermes batch clarify callbacks to the bridge's per-question UI."""
|
||||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
import uuid
|
||||
|
||||
|
||||
def make_clarify_callback(pool, session_id, legacy_callback):
|
||||
def callback(questions, choices=None):
|
||||
# Older Hermes versions call callback(question, choices) and expect text.
|
||||
if not isinstance(questions, list):
|
||||
return legacy_callback(questions, choices)
|
||||
|
||||
answers = {}
|
||||
for index, item in enumerate(questions):
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError('invalid clarify question')
|
||||
question_id = str(item.get('qid') or f'q{index}')
|
||||
clarify_id = uuid.uuid4().hex
|
||||
response_queue = queue.Queue(maxsize=1)
|
||||
with pool._lock:
|
||||
pool._clarify_requests[clarify_id] = response_queue
|
||||
try:
|
||||
pool._append_event(session_id, {
|
||||
'event': 'clarify.requested',
|
||||
'clarify_id': clarify_id,
|
||||
'question': str(item.get('question') or ''),
|
||||
'choices': item.get('choices') or None,
|
||||
'multi_select': bool(item.get('multi_select')),
|
||||
'timeout_ms': 300_000,
|
||||
})
|
||||
try:
|
||||
response = response_queue.get(timeout=300)
|
||||
except queue.Empty:
|
||||
return {'answers': answers, 'outcome': 'timed_out'}
|
||||
answers[question_id] = response or None
|
||||
finally:
|
||||
with pool._lock:
|
||||
pool._clarify_requests.pop(clarify_id, None)
|
||||
return {'answers': answers, 'outcome': 'submitted'}
|
||||
return callback
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Launch the read-only vendored bridge with local compatibility adapters."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
compat_dir = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(compat_dir))
|
||||
sys.path.insert(0, str(compat_dir.parent / 'python'))
|
||||
|
||||
import hermes_bridge as bridge
|
||||
from clarify_compat import make_clarify_callback
|
||||
|
||||
original_callback = bridge.AgentPool._clarify_callback
|
||||
|
||||
|
||||
def clarify_callback(self, session_id):
|
||||
return make_clarify_callback(self, session_id, original_callback(self, session_id))
|
||||
|
||||
|
||||
bridge.AgentPool._clarify_callback = clarify_callback
|
||||
# Workers must launch this same compatibility entrypoint, not the vendored one.
|
||||
bridge._transport.__file__ = __file__
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(bridge.main())
|
||||
@@ -332,11 +332,11 @@ export function buildAgentBridgeLaunchArgs(command: BridgeCommand, script: strin
|
||||
|
||||
function bridgeScriptPath(): string {
|
||||
const candidates = [
|
||||
// Built server: dist/server/index.js -> dist/server/agent-bridge/python/hermes_bridge.py
|
||||
resolve(__dirname, 'agent-bridge', 'python', 'hermes_bridge.py'),
|
||||
// Built server: dist/server/index.js -> dist/server/agent-bridge/compat/hermes_bridge.py
|
||||
resolve(__dirname, 'agent-bridge', 'compat', 'hermes_bridge.py'),
|
||||
// ts-node/dev source tree.
|
||||
resolve(__dirname, 'services/hermes/agent-bridge/python/hermes_bridge.py'),
|
||||
resolve(process.cwd(), 'packages/server/src/services/hermes/agent-bridge/python/hermes_bridge.py'),
|
||||
resolve(__dirname, 'services/hermes/agent-bridge/compat/hermes_bridge.py'),
|
||||
resolve(process.cwd(), 'packages/server/src/services/hermes/agent-bridge/compat/hermes_bridge.py'),
|
||||
]
|
||||
const found = candidates.find(candidate => existsSync(candidate))
|
||||
if (!found) {
|
||||
|
||||
@@ -1202,6 +1202,7 @@ async function applyBridgeChunkAsync(
|
||||
clarify_id: ev.clarify_id,
|
||||
question: ev.question,
|
||||
choices: Array.isArray(ev.choices) ? ev.choices : null,
|
||||
multi_select: Boolean(ev.multi_select),
|
||||
timeout_ms: ev.timeout_ms,
|
||||
}
|
||||
replaceState(sessionMap, sessionId, 'clarify.requested', payload)
|
||||
|
||||
Reference in New Issue
Block a user