wip: desparate models fucking around
This commit is contained in:
@@ -5,6 +5,7 @@ import { planAssistantRequest } from '../../navigation/assistantConversation';
|
||||
import { dispatchAssistantAction } from '../../navigation/assistantActionDispatcher';
|
||||
import { extractAssistantResponseContent, type AssistantPanelElement } from '../../navigation/assistantPanelSpec';
|
||||
import { toClarificationElements } from '../../navigation/protocolNeedsInput';
|
||||
import { buildActionPoliciesFromEnvelope } from '../../navigation/protocolActionPolicies';
|
||||
import { ensureConversationId } from '../../navigation/chatSession';
|
||||
import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
|
||||
import { useChatMessageSender } from '../../navigation/useChatMessageSender';
|
||||
@@ -54,6 +55,7 @@ export const AssistantSidebar: React.FC = () => {
|
||||
finalizeAssistantTurn,
|
||||
appendAssistantMessage,
|
||||
stopStreaming,
|
||||
getStreamingContent,
|
||||
} = useChatSurfaceState();
|
||||
const activeTab = useMemo(() => tabs.find((tab) => tab.id === activeTabId) ?? null, [tabs, activeTabId]);
|
||||
|
||||
@@ -173,20 +175,18 @@ export const AssistantSidebar: React.FC = () => {
|
||||
? (sendResult.envelope.ui?.elements as AssistantPanelElement[])
|
||||
: toClarificationElements(sendResult.envelope.needsInput);
|
||||
setPanelElements(uiElements);
|
||||
setActionPolicies(
|
||||
sendResult.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {}),
|
||||
);
|
||||
} else if (sendResult.message) {
|
||||
const parsedResponse = extractAssistantResponseContent(sendResult.message);
|
||||
finalizeAssistantTurn(resolvedConversationId, parsedResponse.displayText);
|
||||
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
|
||||
setActionPolicies({});
|
||||
setActionPolicies(buildActionPoliciesFromEnvelope(sendResult.envelope));
|
||||
} else {
|
||||
appendAssistantMessage(resolvedConversationId, tr('chat.errorEmptyResponse'));
|
||||
stopStreaming();
|
||||
const assistantContent = getStreamingContent() || sendResult.message;
|
||||
if (assistantContent) {
|
||||
const parsedResponse = extractAssistantResponseContent(assistantContent);
|
||||
finalizeAssistantTurn(resolvedConversationId, parsedResponse.displayText);
|
||||
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
|
||||
setActionPolicies({});
|
||||
} else {
|
||||
appendAssistantMessage(resolvedConversationId, tr('chat.errorEmptyResponse'));
|
||||
stopStreaming();
|
||||
}
|
||||
}
|
||||
|
||||
setPrompt('');
|
||||
@@ -230,17 +230,13 @@ export const AssistantSidebar: React.FC = () => {
|
||||
? (sendResult.envelope.ui?.elements as AssistantPanelElement[])
|
||||
: toClarificationElements(sendResult.envelope.needsInput);
|
||||
setPanelElements(uiElements);
|
||||
setActionPolicies(
|
||||
sendResult.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {}),
|
||||
);
|
||||
setActionPolicies(buildActionPoliciesFromEnvelope(sendResult.envelope));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendResult.message) {
|
||||
const parsedResponse = extractAssistantResponseContent(sendResult.message);
|
||||
const assistantContent = getStreamingContent() || sendResult.message;
|
||||
if (assistantContent) {
|
||||
const parsedResponse = extractAssistantResponseContent(assistantContent);
|
||||
finalizeAssistantTurn(conversationId, parsedResponse.displayText);
|
||||
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
|
||||
setActionPolicies({});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
|
||||
import { dispatchAssistantAction } from '../../navigation/assistantActionDispatcher';
|
||||
import { extractAssistantResponseContent, type AssistantPanelElement } from '../../navigation/assistantPanelSpec';
|
||||
import { toClarificationElements } from '../../navigation/protocolNeedsInput';
|
||||
import { buildActionPoliciesFromEnvelope } from '../../navigation/protocolActionPolicies';
|
||||
import { useAppStore } from '../../store';
|
||||
import { ChatTranscript } from '../ChatSurface';
|
||||
import { AssistantPanelControls } from '../AssistantPanelControls';
|
||||
@@ -198,12 +199,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
? (result.envelope.ui?.elements as AssistantPanelElement[])
|
||||
: toClarificationElements(result.envelope.needsInput);
|
||||
setPanelElements(uiElements);
|
||||
setActionPolicies(
|
||||
result.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {}),
|
||||
);
|
||||
setActionPolicies(buildActionPoliciesFromEnvelope(result.envelope));
|
||||
} else if (assistantContent) {
|
||||
const parsedResponse = extractAssistantResponseContent(assistantContent);
|
||||
finalizeAssistantTurn(conversationId, parsedResponse.displayText);
|
||||
@@ -272,12 +268,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
? (result.envelope.ui?.elements as AssistantPanelElement[])
|
||||
: toClarificationElements(result.envelope.needsInput);
|
||||
setPanelElements(uiElements);
|
||||
setActionPolicies(
|
||||
result.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {}),
|
||||
);
|
||||
setActionPolicies(buildActionPoliciesFromEnvelope(result.envelope));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,15 +170,35 @@ function toRecord(value: unknown): Record<string, unknown> | null {
|
||||
}
|
||||
|
||||
function normalizeChartElement(record: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const chartType = record.chartType;
|
||||
const normalized: Record<string, unknown> = {
|
||||
...record,
|
||||
type: 'chart',
|
||||
chartType: chartType === 'line' || chartType === 'pie' ? chartType : 'bar',
|
||||
};
|
||||
|
||||
const dataRecord = toRecord(record.data);
|
||||
if (Array.isArray(record.series)) {
|
||||
return normalized;
|
||||
if (typeof record.title === 'string' && record.title.trim().length > 0) {
|
||||
normalized.title = record.title;
|
||||
}
|
||||
|
||||
if (Array.isArray(record.series)) {
|
||||
const series = record.series
|
||||
.map((entry) => {
|
||||
const item = toRecord(entry);
|
||||
if (!item || typeof item.label !== 'string' || typeof item.value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return { label: item.label, value: item.value };
|
||||
})
|
||||
.filter((entry): entry is { label: string; value: number } => Boolean(entry));
|
||||
|
||||
if (series.length > 0) {
|
||||
normalized.series = series;
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
const dataRecord = toRecord(record.data);
|
||||
|
||||
if (!dataRecord) {
|
||||
return normalized;
|
||||
}
|
||||
@@ -204,7 +224,6 @@ function normalizeChartElement(record: Record<string, unknown>): Record<string,
|
||||
}
|
||||
|
||||
normalized.series = series;
|
||||
delete normalized.data;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -269,6 +288,10 @@ function normalizeElement(value: unknown): Record<string, unknown> | null {
|
||||
}
|
||||
|
||||
const type = typeof record.type === 'string' ? record.type : '';
|
||||
if (type === 'text' && typeof record.content === 'string' && typeof record.text !== 'string') {
|
||||
return { type: 'text', text: record.content };
|
||||
}
|
||||
|
||||
if (type === 'markdown') {
|
||||
const textValue = typeof record.content === 'string' ? record.content : typeof record.text === 'string' ? record.text : '';
|
||||
if (!textValue.trim()) {
|
||||
@@ -302,6 +325,10 @@ function normalizeCandidate(parsed: unknown): AssistantPanelSpec | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (record.protocolVersion === '2.0' && record.ui) {
|
||||
return normalizeCandidate(record.ui);
|
||||
}
|
||||
|
||||
if (record.type === 'tab' && record.content) {
|
||||
return normalizeCandidate(record.content);
|
||||
}
|
||||
@@ -366,9 +393,9 @@ export function extractAssistantPanelSpec(message: string): AssistantPanelSpec |
|
||||
export function extractAssistantResponseContent(message: string): AssistantResponseContent {
|
||||
const trimmed = message.trim();
|
||||
|
||||
const fencedMatches = [...trimmed.matchAll(/```(json)?\s*([\s\S]*?)```/gi)];
|
||||
const fencedMatches = [...trimmed.matchAll(/```(?:[a-zA-Z0-9_-]+)?\s*([\s\S]*?)```/gi)];
|
||||
for (const match of fencedMatches) {
|
||||
const candidate = match[2]?.trim();
|
||||
const candidate = match[1]?.trim();
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
@@ -384,8 +411,21 @@ export function extractAssistantResponseContent(message: string): AssistantRespo
|
||||
}
|
||||
|
||||
const parsedWholeMessage = parseSpecCandidate(trimmed);
|
||||
let displayText = parsedWholeMessage ? '' : trimmed;
|
||||
|
||||
if (parsedWholeMessage) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
if (parsed.protocolVersion === '2.0' && typeof parsed.assistantText === 'string') {
|
||||
displayText = parsed.assistantText;
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: parsedWholeMessage ? '' : trimmed,
|
||||
displayText,
|
||||
panelSpec: parsedWholeMessage,
|
||||
};
|
||||
}
|
||||
|
||||
18
src/renderer/navigation/protocolActionPolicies.ts
Normal file
18
src/renderer/navigation/protocolActionPolicies.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { ProtocolResponseEnvelope } from '../types/electron';
|
||||
|
||||
export type ActionPolicyLevel = 'silent' | 'confirm' | 'danger';
|
||||
|
||||
export function buildActionPoliciesFromEnvelope(
|
||||
envelope: Pick<ProtocolResponseEnvelope, 'actions' | 'needsInput'>,
|
||||
): Record<string, ActionPolicyLevel> {
|
||||
const policies = envelope.actions.reduce<Record<string, ActionPolicyLevel>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
if (envelope.needsInput.required && envelope.needsInput.fields.length > 0 && !policies.submitNeedsInput) {
|
||||
policies.submitNeedsInput = 'confirm';
|
||||
}
|
||||
|
||||
return policies;
|
||||
}
|
||||
@@ -186,7 +186,7 @@ const METHODS_V1: PythonApiMethodContractV1[] = [
|
||||
method('chat.getConversation', 'Fetch one chat conversation by id.', [requiredString('id')], 'ChatConversation | null'),
|
||||
method('chat.updateConversation', 'Update chat conversation metadata.', [requiredString('id'), requiredObject('updates')], 'ChatConversation | null'),
|
||||
method('chat.deleteConversation', 'Delete chat conversation by id.', [requiredString('id')], 'boolean'),
|
||||
method('chat.sendMessage', 'Send message to chat conversation.', [requiredString('conversationId'), requiredString('message')], '{ success: boolean; message?: string; error?: string }'),
|
||||
method('chat.sendMessage', 'Send message to chat conversation.', [requiredString('conversationId'), requiredString('message'), optionalObject('metadata')], "{ success: boolean; message?: string; envelope?: ProtocolResponseEnvelope; protocolVersion?: '2.0'; traceId?: string; warnings?: string[]; error?: string }"),
|
||||
method('chat.abortMessage', 'Abort active streaming chat response.', [requiredString('conversationId')], 'void'),
|
||||
method('chat.getHistory', 'Get message history for conversation.', [requiredString('conversationId')], 'ChatMessage[]'),
|
||||
method('chat.clearMessages', 'Clear messages for conversation.', [requiredString('conversationId')], 'void'),
|
||||
@@ -360,6 +360,45 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
|
||||
{ name: 'maskedKey', type: 'string', required: true, description: 'Masked key representation for UI display.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ProtocolNeedsInputField',
|
||||
description: 'A required clarification input field used for needsInput prompts.',
|
||||
fields: [
|
||||
{ name: 'key', type: 'string', required: true, description: 'Stable field key used in submitted values.' },
|
||||
{ name: 'label', type: 'string', required: true, description: 'User-facing field label.' },
|
||||
{ name: 'inputType', type: "'text' | 'textarea' | 'select' | 'checkbox' | 'date' | 'number'", required: true, description: 'Rendered input control type.' },
|
||||
{ name: 'required', type: 'boolean', required: false, description: 'Whether user input is required.' },
|
||||
{ name: 'options', type: 'Array<{ label: string; value: string }>', required: false, description: 'Selectable options for select controls.' },
|
||||
{ name: 'placeholder', type: 'string', required: false, description: 'Optional placeholder text for text-like controls.' },
|
||||
{ name: 'defaultValue', type: 'string | number | boolean', required: false, description: 'Default field value shown in UI.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ProtocolAction',
|
||||
description: 'A declarative assistant action exposed to the UI runtime.',
|
||||
fields: [
|
||||
{ name: 'id', type: 'string', required: true, description: 'Stable action id within a response envelope.' },
|
||||
{ name: 'action', type: 'string', required: true, description: 'Action name to dispatch in renderer.' },
|
||||
{ name: 'label', type: 'string', required: false, description: 'Optional user-facing action label.' },
|
||||
{ name: 'payload', type: 'Record<string, unknown>', required: false, description: 'Optional action payload arguments.' },
|
||||
{ name: 'policy', type: "'silent' | 'confirm' | 'danger'", required: true, description: 'Action confirmation policy level.' },
|
||||
{ name: 'requiresConfirmation', type: 'boolean', required: true, description: 'Whether confirmation is required before dispatch.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ProtocolResponseEnvelope',
|
||||
description: 'Canonical AGUI response envelope returned from chat.sendMessage.',
|
||||
fields: [
|
||||
{ name: 'protocolVersion', type: "'2.0'", required: true, description: 'Envelope protocol version.' },
|
||||
{ name: 'assistantText', type: 'string', required: true, description: 'Assistant text content rendered in transcript.' },
|
||||
{ name: 'ui', type: "{ specVersion: '1'; elements: unknown[] }", required: false, description: 'Optional structured UI payload.' },
|
||||
{ name: 'intent', type: "'analyze' | 'ask_input' | 'propose_action' | 'execute_action' | 'summarize'", required: true, description: 'Turn intent classification.' },
|
||||
{ name: 'needsInput', type: '{ required: boolean; fields: ProtocolNeedsInputField[] }', required: true, description: 'Clarification requirements for next step.' },
|
||||
{ name: 'actions', type: 'ProtocolAction[]', required: true, description: 'Declarative actions available for this turn.' },
|
||||
{ name: 'confidence', type: 'number', required: true, description: 'Model confidence score from 0 to 1.' },
|
||||
{ name: 'traceId', type: 'string', required: true, description: 'Trace id for observability and debugging.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ProtocolTelemetrySnapshot',
|
||||
description: 'Aggregated protocol telemetry metrics for AGUI response health.',
|
||||
@@ -377,7 +416,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
|
||||
];
|
||||
|
||||
export const BDS_PYTHON_API_CONTRACT_V1: PythonApiContractV1 = {
|
||||
version: '1.4.0',
|
||||
version: '1.5.0',
|
||||
generatedAt: '2026-02-25T00:00:00.000Z',
|
||||
methods: METHODS_V1,
|
||||
dataStructures: DATA_STRUCTURES_V1,
|
||||
|
||||
Reference in New Issue
Block a user