Fragments: Void.

This commit is contained in:
Enrico Ros
2024-10-16 18:26:20 -07:00
parent 216fe20e52
commit 57bb1edcfc
19 changed files with 143 additions and 107 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "big-agi",
"version": "1.16.0",
"version": "1.91.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "big-agi",
"version": "1.16.0",
"version": "1.91.0",
"hasInstallScript": true,
"dependencies": {
"@emotion/cache": "^11.13.1",
+2 -2
View File
@@ -18,7 +18,7 @@ import type { DConversation, DConversationId } from '~/common/stores/chat/chat.c
import type { OptimaBarControlMethods } from '~/common/layout/optima/bar/OptimaBarDropdown';
import { ConfirmationModal } from '~/common/components/modals/ConfirmationModal';
import { ConversationsManager } from '~/common/chat-overlay/ConversationsManager';
import { createErrorContentFragment, createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragmentsNoPH } from '~/common/stores/chat/chat.fragments';
import { createErrorContentFragment, createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragmentsNoVoid } from '~/common/stores/chat/chat.fragments';
import { LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Vision } from '~/common/stores/llms/llms.types';
import { OptimaDrawerIn, OptimaToolbarIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
import { PanelResizeInset } from '~/common/components/panes/GoodPanelResizeHandler';
@@ -249,7 +249,7 @@ export function AppChat() {
// create the user:message
// NOTE: this can lead to multiple chat messages with data refs that are referring to the same dblobs,
// however, we already got transferred ownership of the dblobs at this point.
const userMessage = createDMessageFromFragments('user', duplicateDMessageFragmentsNoPH(fragments)); // [chat] create user:message to send per-chat
const userMessage = createDMessageFromFragments('user', duplicateDMessageFragmentsNoVoid(fragments)); // [chat] create user:message to send per-chat
if (metadata) userMessage.metadata = duplicateDMessageMetadata(metadata);
ConversationsManager.getHandler(conversation.id).messageAppend(userMessage); // [chat] append user message in each conversation
@@ -33,7 +33,7 @@ import { animationEnterBelow } from '~/common/util/animUtils';
import { browserSpeechRecognitionCapability, PLACEHOLDER_INTERIM_TRANSCRIPT, SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation';
import { copyToClipboard, supportsClipboardRead } from '~/common/util/clipboardUtils';
import { createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragmentsNoPH } from '~/common/stores/chat/chat.fragments';
import { createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragmentsNoVoid } from '~/common/stores/chat/chat.fragments';
import { estimateTextTokens, glueForMessageTokens, marshallWrapDocFragments } from '~/common/stores/chat/chat.tokens';
import { getConversation, isValidConversation, useChatStore } from '~/common/stores/chat/store-chats';
import { launchAppCall } from '~/common/app.routes';
@@ -476,7 +476,7 @@ export function Composer(props: {
const conversation = getConversation(conversationId);
const messageToEmbed = conversation?.messages.find(m => m.id === messageId);
if (conversation && messageToEmbed) {
const fragmentsCopy = duplicateDMessageFragmentsNoPH(messageToEmbed.fragments); // [attach] deep copy a message's fragments to attach to ego
const fragmentsCopy = duplicateDMessageFragmentsNoVoid(messageToEmbed.fragments); // [attach] deep copy a message's fragments to attach to ego
if (fragmentsCopy.length) {
const chatTitle = conversationTitle(conversation);
const messageText = messageFragmentsReduceText(fragmentsCopy);
@@ -207,13 +207,13 @@ export function ChatMessage(props: {
const isVndAndCacheUser = !!props.showAntPromptCaching && messageHasUserFlag(props.message, MESSAGE_FLAG_VND_ANT_CACHE_USER);
const {
imageAttachments, // Stamp-sized Images
contentFragments, // Text (Markdown + Code + ... blocks), Errors, (large) Images, Placeholders
nonImageAttachments, // Document Attachments, likely the User dropped them in
imageAttachments, // Stamp-sized Images
contentOrVoidFragments, // Text (Markdown + Code + ... blocks), Errors, (large) Images, Placeholders
nonImageAttachments, // Document Attachments, likely the User dropped them in
} = useFragmentBuckets(messageFragments);
const fragmentFlattenedText = React.useMemo(() => messageFragmentsReduceText(messageFragments), [messageFragments]);
const handleHighlightSelText = useSelHighlighterMemo(messageId, selText, contentFragments, fromAssistant, props.onMessageFragmentReplace);
const handleHighlightSelText = useSelHighlighterMemo(messageId, selText, contentOrVoidFragments, fromAssistant, props.onMessageFragmentReplace);
const textSubject = selText ? selText : fragmentFlattenedText;
const isSpecialT2I = textSubject.startsWith('https://images.prodia.xyz/') || textSubject.startsWith('/draw ') || textSubject.startsWith('/imagine ') || textSubject.startsWith('/img ');
@@ -695,7 +695,7 @@ export function ChatMessage(props: {
{/* Content Fragments */}
<ContentFragments
fragments={contentFragments}
fragments={contentOrVoidFragments}
showEmptyNotice={!messageFragments.length && !messagePendingIncomplete}
contentScaling={adjContentScaling}
@@ -8,7 +8,7 @@ import { ScaledTextBlockRenderer } from '~/modules/blocks/ScaledTextBlockRendere
import type { ContentScaling, UIComplexityMode } from '~/common/app.theme';
import type { DMessageRole } from '~/common/stores/chat/chat.message';
import { DMessageContentFragment, DMessageFragment, DMessageFragmentId, isContentFragment, isTextPart } from '~/common/stores/chat/chat.fragments';
import { DMessageContentFragment, DMessageFragment, DMessageFragmentId, isContentFragment, isPlaceholderPart, isTextPart, isVoidFragment } from '~/common/stores/chat/chat.fragments';
import type { ChatMessageTextPartEditState } from '../ChatMessage';
import { BlockEdit_TextFragment } from './BlockEdit_TextFragment';
@@ -76,7 +76,7 @@ export function ContentFragments(props: {
const isEditingText = !!props.textEditsState;
// const isMonoFragment = props.fragments.length < 2;
const enableRestartFromEdit = !fromAssistant && props.messageRole !== 'system';
const showDataStreamViz = props.uiComplexityMode !== 'minimal' && props.fragments.length === 1 && isContentFragment(props.fragments[0]) && props.fragments[0].part.pt === 'ph';
const showDataStreamViz = props.uiComplexityMode !== 'minimal' && props.fragments.length === 1 && isVoidFragment(props.fragments[0]) && isPlaceholderPart(props.fragments[0].part);
// Content Fragments Edit Zero-State: button to create a new TextContentFragment
if (isEditingText && !props.fragments.length)
@@ -107,7 +107,37 @@ export function ContentFragments(props: {
{props.fragments.map((fragment) => {
// only proceed with DMessageContentFragment
// Render VOID fragments
if (isVoidFragment(fragment)) {
const { fId, part } = fragment;
switch (part.pt) {
case 'ph': {
return (
<BlockPartPlaceholder
key={fId}
placeholderText={part.pText}
messageRole={props.messageRole}
contentScaling={props.contentScaling}
showAsItalic
showAsDataStreamViz={showDataStreamViz}
/>
);
}
case '_pt_sentinel':
default:
<ScaledTextBlockRenderer
key={fId}
text={`Unknown Void Fragment: ${part.pt}`}
contentScaling={props.contentScaling}
textRenderVariant='text'
showAsDanger
/>;
break;
}
}
// Render CONTENT fragments
if (!isContentFragment(fragment))
return null;
@@ -153,18 +183,6 @@ export function ContentFragments(props: {
/>
);
case 'ph':
return (
<BlockPartPlaceholder
key={fragment.fId}
placeholderText={fragment.part.pText}
messageRole={props.messageRole}
contentScaling={props.contentScaling}
showAsItalic
showAsDataStreamViz={showDataStreamViz}
/>
);
// This is the most frequent part by far, and can be broken down into sub-blocks
case 'text':
return (
@@ -1,11 +1,11 @@
import * as React from 'react';
import { DMessageAttachmentFragment, DMessageContentFragment, DMessageFragment, isAttachmentFragment, isContentFragment, isImageRefPart } from '~/common/stores/chat/chat.fragments';
import { DMessageAttachmentFragment, DMessageContentFragment, DMessageFragment, DMessageVoidFragment, isAttachmentFragment, isContentFragment, isImageRefPart, isPlaceholderPart, isVoidFragment } from '~/common/stores/chat/chat.fragments';
import { shallowEquals } from '~/common/util/hooks/useShallowObject';
interface FragmentBuckets {
contentFragments: DMessageContentFragment[];
contentOrVoidFragments: (DMessageContentFragment | DMessageVoidFragment)[];
imageAttachments: DMessageAttachmentFragment[];
nonImageAttachments: DMessageAttachmentFragment[];
}
@@ -16,32 +16,35 @@ interface FragmentBuckets {
export function useFragmentBuckets(messageFragments: DMessageFragment[]): FragmentBuckets {
// Refs to store the last stable value for each bucket
const contentFragmentsRef = React.useRef<DMessageContentFragment[]>([]);
const contentOrVoidFragmentsRef = React.useRef<(DMessageContentFragment | DMessageVoidFragment)[]>([]);
const imageAttachmentsRef = React.useRef<DMessageAttachmentFragment[]>([]);
const nonImageAttachmentsRef = React.useRef<DMessageAttachmentFragment[]>([]);
// Use useMemo to recalculate buckets only when messageFragments changes
return React.useMemo(() => {
const contentFragments: DMessageContentFragment[] = [];
const contentOrVoidFragments: (DMessageContentFragment | DMessageVoidFragment)[] = [];
const imageAttachments: DMessageAttachmentFragment[] = [];
const nonImageAttachments: DMessageAttachmentFragment[] = [];
messageFragments.forEach(fragment => {
if (isContentFragment(fragment))
contentFragments.push(fragment);
contentOrVoidFragments.push(fragment);
else if (isAttachmentFragment(fragment)) {
if (isImageRefPart(fragment.part))
imageAttachments.push(fragment);
else
nonImageAttachments.push(fragment);
} else if (isVoidFragment(fragment)) {
if (isPlaceholderPart(fragment.part))
contentOrVoidFragments.push(fragment);
} else
console.warn('[DEV] Unexpected fragment type:', fragment.ft);
});
// For each bucket, return the new value if it's different, otherwise return the stable ref
if (!shallowEquals(contentFragments, contentFragmentsRef.current))
contentFragmentsRef.current = contentFragments;
if (!shallowEquals(contentOrVoidFragments, contentOrVoidFragmentsRef.current))
contentOrVoidFragmentsRef.current = contentOrVoidFragments;
if (!shallowEquals(imageAttachments, imageAttachmentsRef.current))
imageAttachmentsRef.current = imageAttachments;
@@ -50,7 +53,7 @@ export function useFragmentBuckets(messageFragments: DMessageFragment[]): Fragme
nonImageAttachmentsRef.current = nonImageAttachments;
return {
contentFragments: contentFragmentsRef.current,
contentOrVoidFragments: contentOrVoidFragmentsRef.current,
imageAttachments: imageAttachmentsRef.current,
nonImageAttachments: nonImageAttachmentsRef.current,
};
@@ -1,7 +1,7 @@
import * as React from 'react';
import type { DMessageId } from '~/common/stores/chat/chat.message';
import { createTextContentFragment, DMessageContentFragment, DMessageFragment, DMessageFragmentId, isTextPart } from '~/common/stores/chat/chat.fragments';
import { createTextContentFragment, DMessageFragment, DMessageFragmentId, isContentFragment, isTextPart } from '~/common/stores/chat/chat.fragments';
import { wrapWithMarkdownSyntax } from '~/modules/blocks/markdown/markdown.wrapper';
import { BUBBLE_MIN_TEXT_LENGTH } from './ChatMessage';
@@ -29,7 +29,7 @@ type HighlightTool = 'highlight' | 'strike' | 'strong';
export function useSelHighlighterMemo(
messageId: DMessageId,
selText: string | null,
contentFragments: DMessageContentFragment[],
fragments: DMessageFragment[],
fromAssistant: boolean,
onMessageFragmentReplace?: (messageId: DMessageId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) => void,
): ((tool: HighlightTool) => void) | null {
@@ -40,8 +40,8 @@ export function useSelHighlighterMemo(
return null;
// Create the highlighter function, if there's 1 and only 1 occurrence of the selection
const highlightFunction = contentFragments.reduce((acc: false /* not found */ | ((tool: HighlightTool) => void) | true /* more than one */, fragment) => {
if (!acc && isTextPart(fragment.part)) {
const highlightFunction = fragments.reduce((acc: false /* not found */ | ((tool: HighlightTool) => void) | true /* more than one */, fragment) => {
if (!acc && isContentFragment(fragment) && isTextPart(fragment.part)) {
const fragmentText = fragment.part.text;
let index = fragmentText.indexOf(selText);
@@ -73,5 +73,5 @@ export function useSelHighlighterMemo(
}, false);
return typeof highlightFunction === 'function' ? highlightFunction : null;
}, [selText, fromAssistant, onMessageFragmentReplace, contentFragments, messageId]);
}, [fragments, fromAssistant, messageId, onMessageFragmentReplace, selText]);
}
+3 -3
View File
@@ -2,7 +2,7 @@ import { defaultSystemPurposeId, SystemPurposeId } from '../../../data';
import { agiUuid } from '~/common/util/idUtils';
import { DMessage, DMessageId, duplicateDMessageNoPH } from './chat.message';
import { DMessage, DMessageId, duplicateDMessageNoVoid } from './chat.message';
/// Conversation
@@ -66,7 +66,7 @@ export function createDConversation(systemPurposeId?: SystemPurposeId): DConvers
};
}
export function duplicateDConversationNoPH(conversation: DConversation, lastMessageId?: DMessageId): DConversation {
export function duplicateDConversationNoVoid(conversation: DConversation, lastMessageId?: DMessageId): DConversation {
// cut short messages, if requested
let messagesToKeep = conversation.messages.length; // By default, include all messages if messageId is null
@@ -84,7 +84,7 @@ export function duplicateDConversationNoPH(conversation: DConversation, lastMess
messages: conversation.messages
.slice(0, messagesToKeep)
.map(duplicateDMessageNoPH), // [*] duplicate conversation - see downstream
.map(duplicateDMessageNoVoid), // [*] duplicate conversation - see downstream
// userTitle: conversation.userTitle, // undefined
autoTitle: newTitle,
+50 -24
View File
@@ -22,26 +22,32 @@ import { agiId } from '~/common/util/idUtils';
export type DMessageFragment =
| DMessageContentFragment
| DMessageAttachmentFragment
| DMessageVoidFragment
// | DMessageBeamFragment
| _DMessageSentinelFragment
| _SentinelFragment
;
// Content: real signal, needs to be sent to the llm
/**
* Content Fragments: understood by ai and humans, processed by llms and stored
*/
export type DMessageContentFragment = _DMessageFragmentWrapper<'content',
| DMessageTextPart // plain text or mixed content -> BlockRenderer
| DMessageImageRefPart // large image
| DMessageToolInvocationPart // shown to dev only, singature of the llm function call
| DMessageToolResponsePart // shown to dev only, response of the llm
| DMessageErrorPart // red message, e.g. non-content application issues
| DMetaPlaceholderPart // (non submitted) placeholder to be replaced by another part
| _DMetaSentinelPart
| _SentinelPart
>;
// Attachments: labeled docs or images, output of Composer > Attachments
/**
* Attachment Fragments: higher level representation of content, usually from attachments,
* - image references, documents, etc.
* - may still have upstream links for instance
*/
export type DMessageAttachmentFragment = _DMessageFragmentWrapper<'attachment',
| DMessageDocPart // document Attachment
| DMessageImageRefPart // image Attachment
| _DMetaSentinelPart
| _SentinelPart
> & {
title: string; // label of the attachment (filename, named id, content overview, title..)
caption: string; // additional information, such as provenance, content preview, etc.
@@ -49,6 +55,15 @@ export type DMessageAttachmentFragment = _DMessageFragmentWrapper<'attachment',
liveFileId?: LiveFileId; // [LiveFile] Optional. Relate to a LiveFile; if present, it may still be invalid, hence we cleanup on load
};
/**
* Void Fragments: no meaning, pure cosmetic, not stored, not processed
*/
export type DMessageVoidFragment = _DMessageFragmentWrapper<'void',
| DVoidPlaceholderPart // (non submitted) placeholder to be replaced by another part
| _SentinelPart
>;
// Future Examples: up to 1 per message, containing the Rays and Merges that would be used to restore the Beam state - could be volatile (omitted at save)
// could not be the data store itself, but only used for save/reload
// export type DMessageBeamFragment = DMessageBaseFragment<'beam'> & {
@@ -58,7 +73,7 @@ export type DMessageAttachmentFragment = _DMessageFragmentWrapper<'attachment',
// }
// Sentinel: force the typesystem to work, bark, and detect/reveal corner cases - unused aside from revealing fragment type issues
type _DMessageSentinelFragment = { ft: '_ft_sentinel', fId: DMessageFragmentId };
type _SentinelFragment = { ft: '_ft_sentinel', fId: DMessageFragmentId };
export type DMessageFragmentId = string; // not unique, 8 bytes
type _DMessageFragmentWrapper<TFragment, TPart extends { pt: string }> = {
@@ -135,9 +150,9 @@ export type DMessageToolResponsePart = {
};
type DMessageToolEnvironment = 'upstream' | 'server' | 'client';
export type DMetaPlaceholderPart = { pt: 'ph', pText: string };
type DVoidPlaceholderPart = { pt: 'ph', pText: string };
type _DMetaSentinelPart = { pt: '_pt_sentinel' };
type _SentinelPart = { pt: '_pt_sentinel' };
//
@@ -169,6 +184,10 @@ export function isContentOrAttachmentFragment(fragment: DMessageFragment) {
return fragment.ft === 'content' || fragment.ft === 'attachment';
}
export function isVoidFragment(fragment: DMessageFragment) {
return fragment.ft === 'void';
}
export function isDocPart(part: DMessageContentFragment['part'] | DMessageAttachmentFragment['part']) {
return part.pt === 'doc';
@@ -190,6 +209,10 @@ export function isToolResponseFunctionCallPart(part: DMessageContentFragment['pa
return part.pt === 'tool_response' && part.response.type === 'function_call';
}
export function isPlaceholderPart(part: DMessageVoidFragment['part']) {
return part.pt === 'ph';
}
/// Content Fragments - Creation & Duplication
@@ -221,11 +244,6 @@ export function create_CodeExecutionResponse_ContentFragment(id: string, error:
return _createContentFragment(_create_CodeExecutionResponse_Part(id, error, result, executor, environment));
}
export function specialShallowReplaceTextContentFragment(copyFragment: DMessageContentFragment, text: string): DMessageContentFragment {
// TODO: remove?
return { ...copyFragment, part: _create_Text_Part(text) };
}
function _createContentFragment(part: DMessageContentFragment['part']): DMessageContentFragment {
return { ft: 'content', fId: agiId('chat-dfragment' /* -content */), part };
}
@@ -257,20 +275,26 @@ function _createAttachmentFragment(title: string, caption: string, part: DMessag
}
/// Meta Fragments - Creation & Duplication
/// Void Fragments - Creation & Duplication
export function createPlaceholderMetaFragment(placeholderText: string): DMessageContentFragment {
return _createContentFragment(_create_Placeholder_Part(placeholderText));
export function createPlaceholderVoidFragment(placeholderText: string): DMessageVoidFragment {
return _createVoidFragment(_create_Placeholder_Part(placeholderText));
}
function _createVoidFragment(part: DMessageVoidFragment['part']): DMessageVoidFragment {
return { ft: 'void', fId: agiId('chat-dfragment' /* -void */), part };
}
function _createSentinelFragment(): _DMessageSentinelFragment {
/// Sentinel Fragments - only here to force the typesystem to work
function _createSentinelFragment(): _SentinelFragment {
return { ft: '_ft_sentinel', fId: agiId('chat-dfragment' /* -_sentinel */) };
}
export function duplicateDMessageFragmentsNoPH(fragments: Readonly<DMessageFragment[]>): DMessageFragment[] {
return fragments.map(_duplicateFragment).filter(f => f.ft !== 'content' || f.part.pt !== 'ph');
export function duplicateDMessageFragmentsNoVoid(fragments: Readonly<DMessageFragment[]>): DMessageFragment[] {
return fragments.map(_duplicateFragment).filter(f => f.ft !== 'void');
}
function _duplicateFragment(fragment: DMessageFragment): DMessageFragment {
@@ -281,6 +305,9 @@ function _duplicateFragment(fragment: DMessageFragment): DMessageFragment {
case 'attachment':
return _createAttachmentFragment(fragment.title, fragment.caption, _duplicate_Part(fragment.part), fragment.liveFileId);
case 'void':
return _createVoidFragment(_duplicate_Part(fragment.part));
case '_ft_sentinel':
return _createSentinelFragment();
@@ -324,15 +351,15 @@ function _create_CodeExecutionResponse_Part(id: string, error: boolean | string,
return { pt: 'tool_response', id, error, response: { type: 'code_execution', result, executor }, environment };
}
function _create_Placeholder_Part(placeholderText: string): DMetaPlaceholderPart {
function _create_Placeholder_Part(placeholderText: string): DVoidPlaceholderPart {
return { pt: 'ph', pText: placeholderText };
}
function _create_Sentinel_Part(): _DMetaSentinelPart {
function _create_Sentinel_Part(): _SentinelPart {
return { pt: '_pt_sentinel' };
}
function _duplicate_Part<TPart extends (DMessageContentFragment | DMessageAttachmentFragment)['part']>(part: TPart): TPart {
function _duplicate_Part<TPart extends (DMessageContentFragment | DMessageAttachmentFragment | DMessageVoidFragment)['part']>(part: TPart): TPart {
switch (part.pt) {
case 'doc':
return _create_Doc_Part(part.vdt, _duplicate_InlineData(part.data), part.ref, part.l1Title, part.meta ? { ...part.meta } : undefined) as TPart;
@@ -414,7 +441,6 @@ function _duplicate_DataReference(ref: DMessageDataRef): DMessageDataRef {
/// Editor Helpers - Fragment Editing
export function editTextPartsInline(fragments: DMessageFragment[], editText: (text: string, idx: number) => string): void {
fragments.forEach((fragment, idx) => {
if (isContentFragment(fragment) && isTextPart(fragment.part))
+8 -21
View File
@@ -1,6 +1,6 @@
import { agiUuid } from '~/common/util/idUtils';
import { createPlaceholderMetaFragment, createTextContentFragment, DMessageContentFragment, DMessageFragment, duplicateDMessageFragmentsNoPH, isAttachmentFragment, isContentFragment, isContentOrAttachmentFragment, isTextPart, specialShallowReplaceTextContentFragment } from './chat.fragments';
import { createPlaceholderVoidFragment, createTextContentFragment, DMessageFragment, duplicateDMessageFragmentsNoVoid, isAttachmentFragment, isContentFragment, isContentOrAttachmentFragment, isVoidFragment } from './chat.fragments';
import type { ModelVendorId } from '~/modules/llms/vendors/vendors.registry';
@@ -112,7 +112,7 @@ export function createDMessageTextContent(role: DMessageRole, text: string): DMe
}
export function createDMessagePlaceholderIncomplete(role: DMessageRole, placeholderText: string): DMessage {
const placeholderFragment = createPlaceholderMetaFragment(placeholderText);
const placeholderFragment = createPlaceholderVoidFragment(placeholderText);
const message = createDMessageFromFragments(role, [placeholderFragment]);
message.pendingIncomplete = true;
return message;
@@ -145,12 +145,12 @@ export function createDMessageFromFragments(role: DMessageRole, fragments: DMess
// helpers - duplication
export function duplicateDMessageNoPH(message: Readonly<DMessage>): DMessage {
export function duplicateDMessageNoVoid(message: Readonly<DMessage>): DMessage {
return {
id: agiUuid('chat-dmessage'),
role: message.role,
fragments: duplicateDMessageFragmentsNoPH(message.fragments), // [*] full message duplication (see downstream)
fragments: duplicateDMessageFragmentsNoVoid(message.fragments), // [*] full message duplication (see downstream)
...(message.pendingIncomplete ? { pendingIncomplete: true } : {}),
@@ -245,11 +245,9 @@ export function messageFragmentsReduceText(fragments: DMessageFragment[], fragme
return fragment.part.error;
case 'image_ref':
return '';
case 'ph':
return '';
// ignore tools
case 'tool_invocation':
case 'tool_response':
// Ignore tools for the text reduction
return '';
}
} else if (isAttachmentFragment(fragment)) {
@@ -259,6 +257,9 @@ export function messageFragmentsReduceText(fragments: DMessageFragment[], fragme
case 'image_ref':
return '';
}
} else if (isVoidFragment(fragment)) {
// all void fragments are ignored by definition when doing a text reduction
return '';
}
console.warn(`DEV: messageFragmentsReduceText: unexpected '${fragment.ft}' fragment with '${(fragment as any)?.part?.pt}' part`);
return '';
@@ -267,20 +268,6 @@ export function messageFragmentsReduceText(fragments: DMessageFragment[], fragme
.join(fragmentSeparator);
}
export function messageFragmentsReplaceLastContentText(fragments: Readonly<DMessageFragment[]>, newText: string, appendText?: boolean): DMessageFragment[] {
// if there's no text fragment, create it
const lastTextFragment = fragments.findLast(f => isContentFragment(f) && isTextPart(f.part)) as DMessageContentFragment | undefined;
if (!lastTextFragment)
return [...fragments, createTextContentFragment(newText)];
// append/replace the last text fragment
return fragments.map(fragment =>
(fragment === lastTextFragment)
? specialShallowReplaceTextContentFragment(lastTextFragment, (appendText && isTextPart(lastTextFragment.part)) ? lastTextFragment.part.text + newText : newText)
: fragment,
);
}
// TODO: remove once the port is fully done - at 2.0.0 ?
export function messageSingleTextOrThrow(message: DMessage): string {
+5 -3
View File
@@ -3,7 +3,7 @@ import { imageTokensForLLM } from '~/common/tokens/tokens.image';
import { textTokensForLLM } from '~/common/tokens/tokens.text';
import type { DMessageRole } from './chat.message';
import { DMessageAttachmentFragment, DMessageFragment, isAttachmentFragment, isContentFragment, isContentOrAttachmentFragment, isDocPart } from './chat.fragments';
import { DMessageAttachmentFragment, DMessageFragment, isAttachmentFragment, isContentFragment, isContentOrAttachmentFragment, isDocPart, isVoidFragment } from './chat.fragments';
export function estimateTokensForFragments(llm: DLLM, role: DMessageRole, fragments: DMessageFragment[], addTopGlue: boolean, debugFrom: string) {
@@ -54,8 +54,6 @@ function _fragmentTokens(llm: DLLM, role: DMessageRole, fragment: DMessageFragme
case 'image_ref':
const forcedSize = role === 'assistant' ? 512 : undefined;
return estimateImageTokens(forcedSize || cPart.width, forcedSize || cPart.height, debugFrom, llm);
case 'ph':
return 0;
case 'text':
return estimateTextTokens(cPart.text, llm, debugFrom);
case 'tool_invocation':
@@ -63,6 +61,10 @@ function _fragmentTokens(llm: DLLM, role: DMessageRole, fragment: DMessageFragme
console.warn('Unhandled token preview for content type:', cPart.pt);
return 0;
}
} else if (isVoidFragment(fragment)) {
// all void fragments are ignored by definition and never sent to the llm
// NOTE: make sure you collapse/don't account for the containing message as well, if left empty
return 0;
} else {
console.warn('Unhandled token preview for fragment type:', (fragment as any).ft);
return 0;
+4 -3
View File
@@ -8,7 +8,7 @@ import type { DModelsService } from '~/common/stores/llms/modelsservice.types';
import { createDConversation, DConversation, type DConversationId } from './chat.conversation';
import { createDMessageTextContent, DMessage, MESSAGE_FLAG_NOTIFY_COMPLETE, messageSetUserFlag } from './chat.message';
import { createErrorContentFragment, isAttachmentFragment, isContentFragment, isContentOrAttachmentFragment, isDocPart, isTextPart } from './chat.fragments';
import { createErrorContentFragment, isAttachmentFragment, isContentFragment, isContentOrAttachmentFragment, isDocPart, isPlaceholderPart, isTextPart, isVoidFragment } from './chat.fragments';
// configuration
@@ -56,8 +56,9 @@ export namespace V4ToHeadConverters {
delete fragment.liveFileId;
// show the aborted ops: convert a Placeholder fragment [part.pt='ph'] to an Error fragment
if (isContentFragment(fragment) && fragment.part.pt === 'ph')
m.fragments[i] = createErrorContentFragment(`${fragment.part.pText} (did not complete)`);
if ((isVoidFragment(fragment) && isPlaceholderPart(fragment.part))
|| (isContentFragment(fragment) && (fragment.part as any)?.pt === 'ph') /* NOTE: REMOVE FOR 2.0: helper during the 'void' fragment transition */)
m.fragments[i] = createErrorContentFragment(`${(fragment.part as any).pText} (did not complete)`);
// [Emergency] validate part types, can mess up in development
if (EMERGENCY_CLEANUP_PARTS) {
+2 -2
View File
@@ -16,7 +16,7 @@ import { workspaceForConversationIdentity } from '~/common/stores/workspace/work
import { DMessage, DMessageId, DMessageMetadata, MESSAGE_FLAG_AIX_SKIP, messageHasUserFlag } from './chat.message';
import type { DMessageFragment, DMessageFragmentId } from './chat.fragments';
import { V3StoreDataToHead, V4ToHeadConverters } from './chats.converters';
import { conversationTitle, createDConversation, DConversation, DConversationId, duplicateDConversationNoPH } from './chat.conversation';
import { conversationTitle, createDConversation, DConversation, DConversationId, duplicateDConversationNoVoid } from './chat.conversation';
import { estimateTokensForFragments } from './chat.tokens';
@@ -120,7 +120,7 @@ export const useChatStore = create<ConversationsStore>()(/*devtools(*/
if (!conversation)
return null;
const branched = duplicateDConversationNoPH(conversation, messageId ?? undefined);
const branched = duplicateDConversationNoVoid(conversation, messageId ?? undefined);
_set({
conversations: [branched, ...conversations],
@@ -7,7 +7,7 @@ import { aixChatGenerateContent_DMessage, aixCreateChatGenerateContext } from '~
import { ConversationsManager } from '~/common/chat-overlay/ConversationsManager';
import { createDMessageTextContent, messageFragmentsReduceText } from '~/common/stores/chat/chat.message';
import { createErrorContentFragment, createPlaceholderMetaFragment, createTextContentFragment } from '~/common/stores/chat/chat.fragments';
import { createErrorContentFragment, createPlaceholderVoidFragment, createTextContentFragment } from '~/common/stores/chat/chat.fragments';
import { getLLMIdOrThrow } from '~/common/stores/llms/store-llms';
import { marshallWrapText } from '~/common/stores/chat/chat.tokens';
import { processPromptTemplate } from '~/common/util/promptUtils';
@@ -175,7 +175,7 @@ export async function autoChatFollowUps(conversationId: string, assistantMessage
if (suggestDiagrams && !['@startuml', '@startmindmap', '```plantuml', '```mermaid'].some(s => assistantMessageText.includes(s))) {
// Placeholder for the diagram
const placeholderFragment = createPlaceholderMetaFragment('Auto-Diagram ...');
const placeholderFragment = createPlaceholderVoidFragment('Auto-Diagram ...');
cHandler.messageFragmentAppend(assistantMessageId, placeholderFragment, false, false);
// Instructions
@@ -226,7 +226,7 @@ export async function autoChatFollowUps(conversationId: string, assistantMessage
if (suggestHTMLUI && !['<html', '<HTML', '<Html'].some(s => assistantMessageText.includes(s))) {
// Placeholder for the UI
const placeholderFragment = createPlaceholderMetaFragment('Auto-UI ...');
const placeholderFragment = createPlaceholderVoidFragment('Auto-UI ...');
cHandler.messageFragmentAppend(assistantMessageId, placeholderFragment, false, false);
// Instructions
@@ -105,7 +105,7 @@ export async function aixCGR_FromDMessagesOrThrow(
const aixChatMessageUser = await dMessageUserFragments.reduce(async (uMsgPromise, uFragment: DMessageFragment) => {
const uMsg = await uMsgPromise;
if (!isContentOrAttachmentFragment(uFragment) || uFragment.part.pt === '_pt_sentinel' || uFragment.part.pt === 'ph')
if (!isContentOrAttachmentFragment(uFragment) || uFragment.part.pt === '_pt_sentinel')
return uMsg;
switch (uFragment.part.pt) {
@@ -158,7 +158,7 @@ export async function aixCGR_FromDMessagesOrThrow(
for (const aFragment of m.fragments) {
if (!isContentOrAttachmentFragment(aFragment) || aFragment.part.pt === '_pt_sentinel' || aFragment.part.pt === 'ph')
if (!isContentOrAttachmentFragment(aFragment) || aFragment.part.pt === '_pt_sentinel')
continue;
switch (aFragment.part.pt) {
-1
View File
@@ -309,7 +309,6 @@ function _llToText(src: AixChatGenerateContent_LL, dest: AixChatGenerateText_Sim
break;
case 'tool_invocation':
throw new Error(`AIX: Unexpected tool invocation ${fragment.part.invocation?.type === 'function_call' ? fragment.part.invocation.name : fragment.part.id} in the Text response.`);
case 'ph': // impossible
case 'image_ref': // impossible
case 'tool_response': // impossible - stopped at the invocation alrady
case '_pt_sentinel': // impossible
@@ -3,7 +3,7 @@ import { Typography } from '@mui/joy';
import type { DLLMId } from '~/common/stores/llms/llms.types';
import { createDMessageEmpty, DMessage } from '~/common/stores/chat/chat.message';
import { createPlaceholderMetaFragment } from '~/common/stores/chat/chat.fragments';
import { createPlaceholderVoidFragment } from '~/common/stores/chat/chat.fragments';
import type { BFusion, FusionUpdateOrFn } from '../beam.gather';
import { executeGatherInstruction, GatherInstruction } from './GatherInstruction';
@@ -110,7 +110,7 @@ export function gatherStartFusion(
);
// reset the intermediate message
inputState.intermediateDMessage.fragments = [createPlaceholderMetaFragment(GATHER_PLACEHOLDER)];
inputState.intermediateDMessage.fragments = [createPlaceholderVoidFragment(GATHER_PLACEHOLDER)];
inputState.intermediateDMessage.pendingIncomplete = true;
inputState.intermediateDMessage.updated = null;
+4 -4
View File
@@ -4,8 +4,8 @@ import { AixChatGenerateContent_DMessage, aixChatGenerateContent_DMessage_FromHi
import type { DLLMId } from '~/common/stores/llms/llms.types';
import { agiUuid } from '~/common/util/idUtils';
import { createDMessageEmpty, DMessage, duplicateDMessageNoPH, messageWasInterruptedAtStart } from '~/common/stores/chat/chat.message';
import { createPlaceholderMetaFragment } from '~/common/stores/chat/chat.fragments';
import { createDMessageEmpty, DMessage, duplicateDMessageNoVoid, messageWasInterruptedAtStart } from '~/common/stores/chat/chat.message';
import { createPlaceholderVoidFragment } from '~/common/stores/chat/chat.fragments';
import { findLLMOrThrow } from '~/common/stores/llms/store-llms';
import { getUXLabsHighPerformance } from '~/common/state/store-ux-labs';
@@ -93,7 +93,7 @@ function rayScatterStart(ray: BRay, llmId: DLLMId | null, inputHistory: DMessage
const newMessage: DMessage = {
...ray.message,
fragments: [createPlaceholderMetaFragment(SCATTER_PLACEHOLDER)],
fragments: [createPlaceholderVoidFragment(SCATTER_PLACEHOLDER)],
pendingIncomplete: true,
created: Date.now(),
updated: null,
@@ -261,7 +261,7 @@ export const createScatterSlice: StateCreator<RootStoreSlice & ScatterStoreSlice
// pre-fill the ray with the imported message
if (message.fragments.length) {
emptyRay.status = 'success';
emptyRay.message = duplicateDMessageNoPH(message); // [beam] import dmessage copy from chat
emptyRay.message = duplicateDMessageNoVoid(message); // [beam] import dmessage copy from chat
emptyRay.message.updated = Date.now();
emptyRay.imported = true;
}