diff --git a/Source/Chatbook/Actions.wl b/Source/Chatbook/Actions.wl index 35cbaa08..787497d6 100644 --- a/Source/Chatbook/Actions.wl +++ b/Source/Chatbook/Actions.wl @@ -113,7 +113,6 @@ ChatbookAction[ "TabRight" , args___ ] := catchMine @ TabRight @ arg ChatbookAction[ "ToggleFormatting" , args___ ] := catchMine @ ToggleFormatting @ args; ChatbookAction[ "ToolManage" , args___ ] := catchMine @ ToolManage @ args; ChatbookAction[ "WidgetSend" , args___ ] := catchMine @ WidgetSend @ args; -ChatbookAction[ name_String , args___ ] := catchMine @ throwFailure[ "NotImplemented", name, args ]; ChatbookAction[ args___ ] := catchMine @ throwInternalFailure @ ChatbookAction @ args; (* ::**************************************************************************************************************:: *) @@ -634,8 +633,9 @@ CopyChatObject // endDefinition; (*constructChatObject*) constructChatObject // beginDefinition; +(* cSpell: ignore bdprompt *) constructChatObject[ messages_List ] := - With[ { chat = chatObject @ standardizeMessageKeys @ messages }, + With[ { chat = Quiet[ ChatObject @ standardizeMessageKeys @ messages, ChatObject::bdprompt ] }, chat /; MatchQ[ chat, _chatObject ] ]; @@ -644,8 +644,6 @@ constructChatObject[ messages_List ] := constructChatObject // endDefinition; -chatObject := chatObject = Symbol[ "System`ChatObject" ]; - (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) (*standardizeMessageKeys*) diff --git a/Source/Chatbook/ChatMessages.wl b/Source/Chatbook/ChatMessages.wl index d6bf88be..67ec5f20 100644 --- a/Source/Chatbook/ChatMessages.wl +++ b/Source/Chatbook/ChatMessages.wl @@ -9,7 +9,12 @@ BeginPackage[ "Wolfram`Chatbook`ChatMessages`" ]; Wolfram`Chatbook`CellToChatMessage; `$chatDataTag; +`$multimodalMessages; +`$tokenBudget; +`$tokenPressure; `constructMessages; +`getTokenizer; +`resizeMultimodalImage; Begin[ "`Private`" ]; @@ -25,12 +30,22 @@ Needs[ "Wolfram`Chatbook`Prompting`" ]; Needs[ "Wolfram`Chatbook`Serialization`" ]; Needs[ "Wolfram`Chatbook`Settings`" ]; Needs[ "Wolfram`Chatbook`Tools`" ]; +Needs[ "Wolfram`Chatbook`Utils`" ]; + +$ContextAliases[ "tokens`" ] = "Wolfram`LLMFunctions`Utilities`Tokenization`"; (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) (*Configuration*) -$$validMessageResult = _Association? AssociationQ | _Missing | Nothing; -$$validMessageResults = $$validMessageResult | { $$validMessageResult ... }; +$maxMMImageSize = 512; +$multimodalMessages = False; +$tokenBudget = 2^13; +$tokenPressure = 0.0; +$reservedTokens = 500; (* TODO: determine this at submit time *) +$cellStringBudget = Automatic; +$initialCellStringBudget = $defaultMaxCellStringLength; +$$validMessageResult = _Association? AssociationQ | _Missing | Nothing; +$$validMessageResults = $$validMessageResult | { $$validMessageResult ... }; $$inlineModifierCell = Alternatives[ Cell[ _, "InlineModifierReference", ___ ], @@ -188,18 +203,48 @@ applyPromptTemplate // endDefinition; (*makeChatMessages*) makeChatMessages // beginDefinition; -makeChatMessages[ settings_, { cells___, cell_ ? promptFunctionCellQ } ] := ( +makeChatMessages[ settings_, cells_ ] := + Block[ + { + $multimodalMessages = TrueQ @ settings[ "Multimodal" ], + $tokenBudget = settings[ "MaxContextTokens" ], + $tokenPressure = 0.0 + }, + If[ settings[ "BasePrompt" ] =!= None, decreaseTokenBudget[ settings, $fullBasePrompt ] ]; + (* FIXME: need to account for persona/tool prompting as well *) + makeChatMessages0[ settings, cells ] + ]; + +makeChatMessages // endDefinition; + + +makeChatMessages0 // beginDefinition; + +makeChatMessages0[ settings_, { cells___, cell_ ? promptFunctionCellQ } ] := ( Sow[ <| "RawOutput" -> True |>, $chatDataTag ]; makePromptFunctionMessages[ settings, { cells, cell } ] ); -makeChatMessages[ settings0_, cells_List ] := Enclose[ - Module[ { settings, role, message, toMessage, cell, history, messages, merged }, - settings = ConfirmBy[ <| settings0, "HistoryPosition" -> 0, "Cells" -> cells |>, AssociationQ, "Settings" ]; - role = makeCurrentRole @ settings; - cell = ConfirmMatch[ Last[ cells, $Failed ], _Cell, "Cell" ]; - toMessage = Confirm[ getCellMessageFunction @ settings, "CellMessageFunction" ]; - message = ConfirmMatch[ toMessage[ cell, settings ], $$validMessageResults, "Message" ]; +makeChatMessages0[ settings0_, cells_List ] := Enclose[ + Module[ { settings, role, message, toMessage0, toMessage, cell, history, messages, merged }, + settings = ConfirmBy[ <| settings0, "HistoryPosition" -> 0, "Cells" -> cells |>, AssociationQ, "Settings" ]; + role = makeCurrentRole @ settings; + cell = ConfirmMatch[ Last[ cells, $Failed ], _Cell, "Cell" ]; + toMessage0 = Confirm[ getCellMessageFunction @ settings, "CellMessageFunction" ]; + + $tokenBudgetLog = Internal`Bag[ ]; + $initialCellStringBudget = Replace[ + settings[ "MaxCellStringLength" ], + Except[ $$size ] -> $defaultMaxCellStringLength + ]; + + toMessage = Function @ With[ + { msg = toMessage0[ #1, <| #2, "TokenBudget" -> $tokenBudget, "TokenPressure" -> $tokenPressure |> ] }, + decreaseTokenBudget[ settings, msg ]; + msg + ]; + + message = ConfirmMatch[ toMessage[ cell, settings ], $$validMessageResults, "Message" ]; history = ConfirmMatch[ Reverse @ Flatten @ MapIndexed[ @@ -210,14 +255,125 @@ makeChatMessages[ settings0_, cells_List ] := Enclose[ "History" ]; - messages = DeleteMissing @ Flatten @ { role, history, message }; - merged = If[ TrueQ @ Lookup[ settings, "MergeMessages" ], mergeMessageData @ messages, messages ]; - merged + messages = addExcisedCellMessage @ DeleteMissing @ Flatten @ { role, history, message }; + + merged = If[ TrueQ @ Lookup[ settings, "MergeMessages" ], mergeMessageData @ messages, messages ]; + $lastMessageList = merged ], - throwInternalFailure[ makeChatMessages[ settings0, cells ], ## ] & + throwInternalFailure[ makeChatMessages0[ settings0, cells ], ## ] & ]; -makeChatMessages // endDefinition; +makeChatMessages0 // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*addExcisedCellMessage*) +addExcisedCellMessage // beginDefinition; + +addExcisedCellMessage[ messages: { ___Association } ] := Enclose[ + Module[ { split }, + split = SplitBy[ messages, MatchQ @ KeyValuePattern[ "Content" -> "[Cell Excised]" ] ]; + ConfirmMatch[ Flatten[ combineExcisedMessages /@ split ], { ___Association }, "Messages" ] + ], + throwInternalFailure[ addExcisedCellMessage @ messages, ## ] & +]; + +addExcisedCellMessage // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsubsection::Closed:: *) +(*combineExcisedMessages*) +combineExcisedMessages // beginDefinition; + +combineExcisedMessages[ messages: { msg: KeyValuePattern[ "Content" -> "[Cell Excised]" ], ___ } ] := + <| "Role" -> "System", "Content" -> "[" <> ToString @ Length @ messages <> " Cells Excised]" |>; + +combineExcisedMessages[ messages_ ] := messages; + +combineExcisedMessages // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*decreaseTokenBudget*) +decreaseTokenBudget // beginDefinition; + +decreaseTokenBudget[ as_Association, message_ ] := Enclose[ + Module[ { count, budget }, + + count = ConfirmMatch[ tokenCount[ as, message ], $$size, "Count" ]; + budget = ConfirmMatch[ $tokenBudget, $$size, "Budget" ]; + + $tokenBudget = Max[ 0, budget - count ]; + $tokenPressure = 1.0 - ($tokenBudget / as[ "MaxContextTokens" ]); + $cellStringBudget = If[ $tokenBudget < $reservedTokens, + 0, + Ceiling[ (1 - $tokenPressure) * $initialCellStringBudget ] + ]; + + Internal`StuffBag[ + $tokenBudgetLog, + <| + "PreviousBudget" -> budget, + "TokenBudget" -> $tokenBudget, + "Pressure" -> $tokenPressure, + "CellStringBudget" -> $cellStringBudget, + "MessageTokens" -> count, + "Message" -> message + |> + ] + ], + throwInternalFailure[ decreaseTokenBudget[ as, message ], ## ] & +]; + +decreaseTokenBudget // endDefinition; + + +$tokenBudgetLog = Internal`Bag[ ]; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*tokenCount*) +tokenCount // beginDefinition; + +tokenCount[ as_Association, message_ ] := Enclose[ + Module[ { tokenizer, content }, + tokenizer = getTokenizer @ as; + content = ConfirmBy[ messageContent @ message, validContentQ, "Content" ]; + Length @ ConfirmMatch[ applyTokenizer[ tokenizer, content ], _List, "TokenCount" ] + ], + throwInternalFailure[ tokenCount[ as, message ], ## ] & +]; + +tokenCount // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*applyTokenizer*) +applyTokenizer // beginDefinition; +applyTokenizer[ tokenizer_, content_String ] := tokenizer @ content; +applyTokenizer[ tokenizer_, content_? graphicsQ ] := tokenizer @ content; +applyTokenizer[ tokenizer_, content_List ] := Flatten[ tokenizer /@ content ]; +applyTokenizer[ tokenizer_, KeyValuePattern[ "Data" -> data_ ] ] := tokenizer @ data; +applyTokenizer // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*messageContent*) +messageContent // beginDefinition; + +messageContent[ "[Cell Excised]" ] := ""; +messageContent[ content_String ] := content; +messageContent[ KeyValuePattern[ "Content" -> content_ ] ] := messageContent @ content; +messageContent[ KeyValuePattern @ { "Type" -> "Text"|"Image", "Data" -> content_ } ] := messageContent @ content; + +messageContent[ content_List ] := + With[ { s = messageContent /@ content }, + StringRiffle[ s, "\n\n" ] /; AllTrue[ s, StringQ ] + ]; + +messageContent[ content_ ] := content; + +messageContent // endDefinition; (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) @@ -433,7 +589,7 @@ makeCurrentCellMessage[ settings_, { cells___, cell0_ } ] := Enclose[ Module[ { modifiers, cell, role, content }, { modifiers, cell } = ConfirmMatch[ extractModifiers @ cell0, { _, _ }, "Modifiers" ]; role = ConfirmBy[ cellRole @ cell, StringQ, "CellRole" ]; - content = ConfirmBy[ Block[ { $CurrentCell = True }, CellToString @ cell ], StringQ, "Content" ]; + content = ConfirmBy[ Block[ { $CurrentCell = True }, makeMessageContent @ cell ], validContentQ, "Content" ]; Flatten @ { expandModifierMessages[ settings, modifiers, { cells }, cell ], <| "Role" -> role, "Content" -> content |> @@ -444,11 +600,62 @@ makeCurrentCellMessage[ settings_, { cells___, cell0_ } ] := Enclose[ makeCurrentCellMessage // endDefinition; +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*makeMessageContent*) +makeMessageContent // beginDefinition; + +makeMessageContent[ cell_Cell ] /; $multimodalMessages := Enclose[ + Module[ { string, split, joined }, + string = ConfirmBy[ cellToString @ cell, StringQ, "CellToString" ]; + + split = Flatten @ StringSplit[ + string, + md: Shortest[ "MarkdownImageBox[\"" ~~ link: ("![" ~~ __ ~~ "](" ~~ __ ~~ ")") ~~ "\"]" ] :> { + md, + ConfirmBy[ GetExpressionURI[ link, Tooltip -> False ], ImageQ, "GetExpressionURI" ] + } + ]; + + joined = FixedPoint[ Replace[ { a___, b_String, c_String, d___ } :> { a, b<>c, d } ], split ]; + Replace[ joined, { msg_String } :> msg ] + ], + throwInternalFailure[ makeMessageContent @ cell, ## ] & +]; + +makeMessageContent[ cell_Cell ] := + cellToString @ cell; + +makeMessageContent // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*cellToString*) +cellToString // beginDefinition; +cellToString[ cell_Cell ] := CellToString[ cell, "MaxCellStringLength" -> $cellStringBudget ]; +cellToString // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*validContentQ*) +validContentQ[ string_String ] := StringQ @ string; +validContentQ[ content_List ] := AllTrue[ content, validContentPartQ ]; +validContentQ[ ___ ] := False; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*validContentPartQ*) +validContentPartQ[ _String? StringQ ] := True; +validContentPartQ[ _URL | _File ] := True; +validContentPartQ[ _? graphicsQ ] := True; +validContentPartQ[ KeyValuePattern[ "Type" -> _String ] ] := True; +validContentPartQ[ ___ ] := False; + (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*makeCellMessage*) makeCellMessage // beginDefinition; -makeCellMessage[ cell_Cell ] := <| "Role" -> cellRole @ cell, "Content" -> CellToString @ cell |>; +makeCellMessage[ cell_Cell ] := <| "Role" -> cellRole @ cell, "Content" -> makeMessageContent @ cell |>; makeCellMessage // endDefinition; (* ::**************************************************************************************************************:: *) @@ -474,9 +681,19 @@ cellRole // endDefinition; (* ::Subsection::Closed:: *) (*mergeMessageData*) mergeMessageData // beginDefinition; -mergeMessageData[ messages_ ] := mergeMessages /@ SplitBy[ messages, Lookup[ "Role" ] ]; + +mergeMessageData[ { sys: KeyValuePattern[ "Role" -> "System" ], rest___ } ] := + Flatten @ { sys, mergeMessageData0 @ { rest } }; + +mergeMessageData[ messages_List ] := + mergeMessageData0 @ messages; + mergeMessageData // endDefinition; +mergeMessageData0 // beginDefinition; +mergeMessageData0[ messages_List ] := Flatten[ mergeMessages /@ SplitBy[ messages, Lookup[ "Role" ] ] ]; +mergeMessageData0 // endDefinition; + (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*mergeMessages*) @@ -485,13 +702,28 @@ mergeMessages // beginDefinition; mergeMessages[ { } ] := Nothing; mergeMessages[ { message_ } ] := message; mergeMessages[ messages: { first_Association, __Association } ] := - Module[ { role, strings }, - role = Lookup[ first , "Role" ]; - strings = Lookup[ messages, "Content" ]; - <| - "Role" -> role, - "Content" -> StringDelete[ StringRiffle[ strings, "\n\n" ], "```\n\n```" ] - |> + Module[ { role, content, stitch }, + role = Lookup[ first, "Role" ]; + content = Flatten @ Lookup[ messages, "Content" ]; + stitch = StringDelete[ #1, "```\n\n```" ] &; + + If[ AllTrue[ content, StringQ ], + <| + "Role" -> role, + "Content" -> stitch @ StringRiffle[ content, "\n\n" ] + |>, + <| + "Role" -> role, + "Content" -> FixedPoint[ + Replace @ { + { a___, b_String, c_String, d___ } :> { a, stitch[ b<>"\n\n"<>c ], d }, + { a___, b_String, { c_String, d___ }, e___ } :> { a, { stitch[ b<>"\n\n"<>c ], d }, e }, + { a___, { b___, c_String }, d_String, e___ } :> { a, { b, stitch[ c<>"\n\n"<>d ] }, e } + }, + content + ] + |> + ] ]; mergeMessages // endDefinition; @@ -707,7 +939,7 @@ argumentTokenToString[ b___ ] } -] := CellToString @ Cell[ TextData @ { a }, "ChatInput", b ]; +] := cellToString @ Cell[ TextData @ { a }, "ChatInput", b ]; argumentTokenToString[ ">", @@ -722,17 +954,125 @@ argumentTokenToString[ } ] := ""; -argumentTokenToString[ "^", name_, { ___, cell_, _ } ] := CellToString @ cell; +argumentTokenToString[ "^", name_, { ___, cell_, _ } ] := cellToString @ cell; -argumentTokenToString[ "^^", name_, { history___, _ } ] := StringRiffle[ CellToString /@ { history }, "\n\n" ]; +argumentTokenToString[ "^^", name_, { history___, _ } ] := StringRiffle[ cellToString /@ { history }, "\n\n" ]; argumentTokenToString // endDefinition; +(* ::**************************************************************************************************************:: *) +(* ::Section::Closed:: *) +(*Tokenization*) +$tokenizer := gpt2Tokenizer; + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*getTokenizer*) +getTokenizer // beginDefinition; +getTokenizer[ KeyValuePattern[ "Tokenizer" -> tokenizer: Except[ $$unspecified ] ] ] := tokenizer; +getTokenizer[ KeyValuePattern[ "Model" -> model_ ] ] := getTokenizer @ model; +getTokenizer[ model_ ] := cachedTokenizer @ toModelName @ model; +getTokenizer // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*cachedTokenizer*) +cachedTokenizer // beginDefinition; +cachedTokenizer[ All ] := AssociationMap[ cachedTokenizer, $cachedTokenizerNames ]; +cachedTokenizer[ name_String ] := cachedTokenizer0 @ tokenizerName @ toModelName @ name; +cachedTokenizer // endDefinition; + + +cachedTokenizer0 // beginDefinition; + +cachedTokenizer0[ "chat-bison" ] = ToCharacterCode[ #, "UTF8" ] &; + +cachedTokenizer0[ "gpt-4-vision" ] := + If[ graphicsQ[ # ], + gpt4ImageTokenizer[ # ], + cachedTokenizer[ "gpt-4" ][ # ] + ] &; + +cachedTokenizer0[ model_String ] := Enclose[ + Quiet @ Module[ { name, tokenizer }, + initTools[ ]; + Quiet @ Needs[ "Wolfram`LLMFunctions`Utilities`Tokenization`" -> None ]; + name = ConfirmBy[ tokens`FindTokenizer @ model, StringQ, "Name" ]; + tokenizer = ConfirmMatch[ tokens`LLMTokenizer[ Method -> name ], Except[ _tokens`LLMTokenizer ], "Tokenizer" ]; + ConfirmMatch[ tokenizer[ "test" ], _List, "TokenizerTest" ]; + cachedTokenizer0[ model ] = tokenizer + ], + gpt2Tokenizer & +]; + +cachedTokenizer0 // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*tokenizerName*) +tokenizerName // beginDefinition; +tokenizerName[ name_String ] := SelectFirst[ $cachedTokenizerNames, StringContainsQ[ name, # ] &, name ]; +tokenizerName // endDefinition; + +$cachedTokenizerNames = { "gpt-4-vision", "gpt-4", "gpt-3.5", "gpt-2", "claude-2", "claude-instant-1", "chat-bison" }; + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*GPT-4 Vision Image Tokenizer *) + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*resizeMultimodalImage*) +resizeMultimodalImage // beginDefinition; + +resizeMultimodalImage[ image0_ ] := Enclose[ + Module[ { image, dimensions, max, small, resized }, + image = ConfirmBy[ If[ image2DQ @ image0, image0, Rasterize @ image0 ], image2DQ, "Image" ]; + dimensions = ConfirmMatch[ ImageDimensions @ image, { _Integer, _Integer }, "Dimensions" ]; + max = ConfirmMatch[ $maxMMImageSize, _Integer? Positive, "MaxSize" ]; + small = ConfirmMatch[ AllTrue[ dimensions, LessThan @ max ], True|False, "Small" ]; + resized = ConfirmBy[ If[ small, image, ImageResize[ image, { UpTo @ max, UpTo @ max } ] ], ImageQ, "Resized" ]; + resizeMultimodalImage[ image0 ] = resized + ], + throwInternalFailure[ resizeMultimodalImage @ image0, ## ] & +]; + +resizeMultimodalImage // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*gpt4ImageTokenizer*) +gpt4ImageTokenizer // beginDefinition; +gpt4ImageTokenizer[ image_ ] := gpt4ImageTokenizer[ image, gpt4ImageTokenCount @ image ]; +gpt4ImageTokenizer[ image_, count: $$size ] := ConstantArray[ 0, count ]; (* TODO: just a placeholder for counting *) +gpt4ImageTokenizer // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*gpt4ImageTokenCount*) +gpt4ImageTokenCount // beginDefinition; +gpt4ImageTokenCount[ image_ ] := gpt4ImageTokenCount[ image, gpt4ImageTokenCount0 @ image ]; +gpt4ImageTokenCount[ image_, count: $$size ] := gpt4ImageTokenCount[ image ] = count; +gpt4ImageTokenCount // endDefinition; + + +gpt4ImageTokenCount0 // beginDefinition; +gpt4ImageTokenCount0[ image_ ] := gpt4ImageTokenCount0[ image, resizeMultimodalImage @ image ]; +gpt4ImageTokenCount0[ image_, resized_Image ] := gpt4ImageTokenCount0[ image, ImageDimensions @ resized ]; +gpt4ImageTokenCount0[ image_, { w_, h_ } ] := gpt4ImageTokenCount0[ w, h ]; +gpt4ImageTokenCount0[ w_Integer, h_Integer ] := 85 + 170 * Ceiling[ h / 512 ] * Ceiling[ w / 512 ]; +gpt4ImageTokenCount0 // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*Fallback Tokenizer*) +gpt2Tokenizer := gpt2Tokenizer = ResourceFunction[ "GPT2Tokenizer" ][ ]; + (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) (*Package Footer*) If[ Wolfram`ChatbookInternal`$BuildingMX, - Null; + cachedTokenizer[ All ]; ]; (* :!CodeAnalysis::EndBlock:: *) diff --git a/Source/Chatbook/Common.wl b/Source/Chatbook/Common.wl index 071afce8..954e15f8 100644 --- a/Source/Chatbook/Common.wl +++ b/Source/Chatbook/Common.wl @@ -26,6 +26,8 @@ BeginPackage[ "Wolfram`Chatbook`Common`" ]; `$$excludeHistoryStyle; `$$nestedCellStyle; +`$$optionsSequence; +`$$size; `$$textData; `$$textDataList; `$$unspecified; @@ -105,6 +107,8 @@ $$nestedCellStyle = cellStylePattern @ $nestedCellStyles; $$textDataItem = (_String|_Cell|_StyleBox|_ButtonBox); $$textDataList = { $$textDataItem... }; $$textData = $$textDataItem | $$textDataList; +$$optionsSequence = (Rule|RuleDelayed)[ _Symbol|_String, _ ] ...; +$$size = Infinity | (_Real|_Integer)? NonNegative; $$unspecified = _Missing | Automatic | Inherited; (* ::**************************************************************************************************************:: *) @@ -117,16 +121,21 @@ KeyValueMap[ Function[ MessageName[ Chatbook, #1 ] = #2 ], <| "ConnectionFailure" -> "Server connection failure: `1`. Please try again later.", "ConnectionFailure2" -> "Could not get a valid response from the server: `1`. Please try again later.", "ExpectedInstallableResourceType" -> "Expected a resource of type `1` instead of `2`.", + "IntegratedServiceError" -> "The `1` service returned an error: `2`", + "IntegratedServiceUnavailable" -> "The `1` service is currently not available. Please try again in a few minutes.", "Internal" -> "An unexpected error occurred. `1`", "InvalidAPIKey" -> "Invalid value for API key: `1`", "InvalidArguments" -> "Invalid arguments given for `1` in `2`.", + "InvalidFrontEndScope" -> "The value `1` is not a valid scope for `2`.", "InvalidFunctions" -> "Invalid setting for ProcessingFunctions: `1`; using defaults instead.", + "InvalidHandlerArguments" -> "Invalid value for $ChatHandlerData: `1`; resetting to default value.", "InvalidHandlerKeys" -> "Invalid setting for HandlerFunctionsKeys: `1`; using defaults instead.", "InvalidHandlers" -> "Invalid setting for HandlerFunctions: `1`; using defaults instead.", - "InvalidHandlerArguments" -> "Invalid value for $ChatHandlerData: `1`; resetting to default value.", - "InvalidResourceSpecification" -> "The argument `1` is not a valid resource specification.", "InvalidMessages" -> "The value `2` returned by `1` is not a valid list of messages.", + "InvalidResourceSpecification" -> "The argument `1` is not a valid resource specification.", "InvalidResourceURL" -> "The specified URL does not represent a valid resource object.", + "InvalidRootSettings" -> "The value `1` is not valid for root chat settings.", + "InvalidSettingsKey" -> "The value `1` is not a valid key for `2`.", "InvalidStreamingOutputMethod" -> "Invalid streaming output method: `1`.", "InvalidWriteMethod" -> "Invalid setting for NotebookWriteMethod: `1`; using default instead.", "ModelToolSupport" -> "The model `1` does not support tools.", diff --git a/Source/Chatbook/Formatting.wl b/Source/Chatbook/Formatting.wl index 010551ce..66e043c9 100644 --- a/Source/Chatbook/Formatting.wl +++ b/Source/Chatbook/Formatting.wl @@ -606,7 +606,9 @@ $textDataFormatRules = { "`" ~~ code: Except[ WhitespaceCharacter ].. ~~ "`" /; inlineSyntaxQ @ code :> inlineCodeCell @ code, "`" ~~ code: Except[ "`"|"\n" ].. ~~ "`" :> inlineCodeCell @ code, "$$" ~~ math: Except[ "$" ].. ~~ "$$" :> mathCell @ math, - "$" ~~ math: Except[ "$" ].. ~~ "$" /; StringFreeQ[ math, "\n" ] :> mathCell @ math + "\\(" ~~ math__ ~~ "\\)" /; StringFreeQ[ math, "\\)" ] :> mathCell @ math, + "\\[" ~~ math__ ~~ "\\]" /; StringFreeQ[ math, "\\]" ] :> mathCell @ math, + "$" ~~ math: Except[ "$" ].. ~~ "$" :> mathCell @ math }; (* ::**************************************************************************************************************:: *) @@ -637,6 +639,8 @@ $dynamicSplitRules = { (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*$stringFormatRules*) + +(* cSpell: ignore textit, textbf *) $stringFormatRules = { "***" ~~ text: Except[ "*" ].. ~~ "***" :> styleBox[ text, FontWeight -> Bold, FontSlant -> Italic ], "___" ~~ text: Except[ "_" ].. ~~ "___" :> styleBox[ text, FontWeight -> Bold, FontSlant -> Italic ], @@ -644,7 +648,9 @@ $stringFormatRules = { "__" ~~ text: Except[ "_" ].. ~~ "__" :> styleBox[ text, FontWeight -> Bold ], "*" ~~ text: Except[ "*" ].. ~~ "*" :> styleBox[ text, FontSlant -> Italic ], "_" ~~ text: Except[ "_" ].. ~~ "_" :> styleBox[ text, FontSlant -> Italic ], - "[" ~~ label: Except[ "[" ].. ~~ "](" ~~ url: Except[ ")" ].. ~~ ")" :> hyperlink[ label, url ] + "[" ~~ label: Except[ "[" ].. ~~ "](" ~~ url: Except[ ")" ].. ~~ ")" :> hyperlink[ label, url ], + "\\textit{" ~~ text__ ~~ "}" /; StringFreeQ[ text, "{"|"}" ] :> styleBox[ text, FontSlant -> Italic ], + "\\textbf{" ~~ text__ ~~ "}" /; StringFreeQ[ text, "{"|"}" ] :> styleBox[ text, FontWeight -> Bold ] }; (* ::**************************************************************************************************************:: *) @@ -679,6 +685,13 @@ inlineToolCall[ string_String, as_Association ] := Cell[ TaggingRules -> KeyDrop[ as, { "Icon", "Result" } ] ]; +inlineToolCall[ string_String, failed_Failure ] := Cell[ + BoxData @ ToBoxes @ failed, + "FailedToolCall", + Background -> None, + TaggingRules -> <| "ToolCall" -> string |> +]; + inlineToolCall // endDefinition; (* ::**************************************************************************************************************:: *) @@ -732,6 +745,9 @@ parseFullToolCallString[ id_String, string_String ] := parseFullToolCallString[ id_, _Missing, string_String ] := parsePartialToolCallString @ string; +parseFullToolCallString[ id_, failed_Failure, string_String ] := + failed; + parseFullToolCallString[ id_String, resp: HoldPattern[ _LLMToolResponse ], string_String ] := parseFullToolCallString[ id, diff --git a/Source/Chatbook/FrontEnd.wl b/Source/Chatbook/FrontEnd.wl index 605f3c59..f70d3950 100644 --- a/Source/Chatbook/FrontEnd.wl +++ b/Source/Chatbook/FrontEnd.wl @@ -599,12 +599,14 @@ openerView0[ { a_, b_ }, args___ ] /; ByteCount @ b > 50000 := openerView1[ { a, openerView0[ { a_, b_ }, args___ ] := openerView1[ { a, b }, args ]; openerView0 // endDefinition; - -openerView1[ args___ ] := +(*cspell: ignore patv *) +openerView1[ args___ ] := Quiet[ RawBoxes @ ReplaceAll[ Replace[ ToBoxes @ OpenerView @ args, TagBox[ boxes_, ___ ] :> boxes ], InterpretationBox[ boxes_, _OpenerView, ___ ] :> boxes - ]; + ], + Pattern::patv +]; (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) diff --git a/Source/Chatbook/Models.wl b/Source/Chatbook/Models.wl index 40d38403..d1a38e7e 100644 --- a/Source/Chatbook/Models.wl +++ b/Source/Chatbook/Models.wl @@ -9,6 +9,7 @@ BeginPackage[ "Wolfram`Chatbook`Models`" ]; `chatModelQ; `getModelList; `modelDisplayName; +`multimodalModelQ; `snapshotModelQ; `standardizeModelData; `toModelName; @@ -138,6 +139,9 @@ modelName // endDefinition; (*toModelName*) toModelName // beginDefinition; +toModelName[ KeyValuePattern @ { "Service" -> service_, "Model" -> model_ } ] := + toModelName @ { service, model }; + toModelName[ { service_String, name_String } ] := toModelName @ name; toModelName[ name_String? StringQ ] := toModelName[ name ] = @@ -163,17 +167,35 @@ toModelName0 // endDefinition; (* ::Subsection::Closed:: *) (*snapshotModelQ*) snapshotModelQ // beginDefinition; -snapshotModelQ[ name_String? fineTunedModelQ ] := snapshotModelQ @ StringSplit[ name, ":" ][[ 2 ]]; -snapshotModelQ[ name_String ] := StringMatchQ[ name, "gpt-"~~__~~"-"~~Repeated[ DigitCharacter, { 4 } ] ]; + +snapshotModelQ[ name_String? fineTunedModelQ ] := snapshotModelQ[ name ] = + snapshotModelQ @ StringSplit[ name, ":" ][[ 2 ]]; + +snapshotModelQ[ name_String? StringQ ] := snapshotModelQ[ name ] = + StringMatchQ[ toModelName @ name, "gpt-"~~__~~"-"~~Repeated[ DigitCharacter, { 4 } ]~~(""|"-preview") ]; + +snapshotModelQ[ other_ ] := + With[ { name = toModelName @ other }, snapshotModelQ @ name /; StringQ @ name ]; + snapshotModelQ // endDefinition; (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*fineTunedModelQ*) fineTunedModelQ // beginDefinition; -fineTunedModelQ[ name_String ] := StringMatchQ[ name, "ft:"~~__~~":"~~__ ]; +fineTunedModelQ[ name_String ] := StringMatchQ[ toModelName @ name, "ft:"~~__~~":"~~__ ]; +fineTunedModelQ[ other_ ] := With[ { name = toModelName @ other }, fineTunedModelQ @ name /; StringQ @ name ]; fineTunedModelQ // endDefinition; +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*multimodalModelQ*) +(* FIXME: this should be a queryable property from LLMServices: *) +multimodalModelQ // beginDefinition; +multimodalModelQ[ name_String? StringQ ] := StringContainsQ[ toModelName @ name, "gpt-"~~$$modelVersion~~"-vision" ]; +multimodalModelQ[ other_ ] := With[ { name = toModelName @ other }, multimodalModelQ @ name /; StringQ @ name ]; +multimodalModelQ // endDefinition; + (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*modelDisplayName*) @@ -200,6 +222,9 @@ modelDisplayName[ { "gpt", rest___ } ] := modelDisplayName[ { before__, date_String } ] /; StringMatchQ[ date, Repeated[ DigitCharacter, { 4 } ] ] := modelDisplayName @ { before, DateObject @ Flatten @ { 0, ToExpression @ StringPartition[ date, 2 ] } }; +modelDisplayName[ { before__, "preview" } ] := + modelDisplayName @ { before } <> " (Preview)"; + modelDisplayName[ { before___, date_DateObject } ] := modelDisplayName @ { before, diff --git a/Source/Chatbook/Personas.wl b/Source/Chatbook/Personas.wl index 5bfe4925..167f1034 100644 --- a/Source/Chatbook/Personas.wl +++ b/Source/Chatbook/Personas.wl @@ -32,6 +32,7 @@ Needs[ "Wolfram`Chatbook`" ]; Needs[ "Wolfram`Chatbook`Errors`" ]; Needs[ "Wolfram`Chatbook`ErrorUtils`" ]; Needs[ "Wolfram`Chatbook`ResourceInstaller`" ]; +Needs[ "Wolfram`Chatbook`Utils`" ]; (*========================================================*) @@ -213,12 +214,12 @@ loadPersonaFromDirectory[paclet_PacletObject, personaName_, dir_?StringQ] := Mod config = FileNameJoin[{dir, "LLMConfiguration.wl"}]; pre = If[FileType[pre] === File, - readPromptString[pre], + readString[pre], Missing["NotAvailable", pre] ]; post = If[FileType[post] === File, - readPromptString[post], + readString[post], Missing["NotAvailable", post] ]; @@ -251,8 +252,6 @@ loadPersonaFromDirectory[paclet_PacletObject, personaName_, dir_?StringQ] := Mod ] ] -readPromptString[ file_ ] := StringReplace[ ByteArrayToString @ ReadByteArray @ file, "\r\n" -> "\n" ]; - standardizePersonaData // beginDefinition; diff --git a/Source/Chatbook/Prompting.wl b/Source/Chatbook/Prompting.wl index c1188bac..b1792c0d 100644 --- a/Source/Chatbook/Prompting.wl +++ b/Source/Chatbook/Prompting.wl @@ -2,9 +2,9 @@ (*Package Header*) BeginPackage[ "Wolfram`Chatbook`Prompting`" ]; -(* TODO: select portions of base prompt during serialization as needed *) `$basePrompt; `$basePromptComponents; +`$fullBasePrompt; `needsBasePrompt; `withBasePromptBuilder; @@ -31,6 +31,7 @@ $basePromptOrder = { "MessageConversionHeader", "ConversionLargeOutputs", "ConversionGraphics", + "MarkdownImageBox", "ConversionFormatting", "VisibleUserInput", "TrivialCode", @@ -65,6 +66,7 @@ $basePromptDependencies = Append[ "GeneralInstructionsHeader" ] /@ <| "MessageConversionHeader" -> { "NotebooksPreamble" }, "ConversionLargeOutputs" -> { "MessageConversionHeader" }, "ConversionGraphics" -> { "MessageConversionHeader" }, + "MarkdownImageBox" -> { "MessageConversionHeader" }, "ConversionFormatting" -> { "MessageConversionHeader" }, "VisibleUserInput" -> { }, "TrivialCode" -> { }, @@ -111,10 +113,10 @@ $basePromptComponents[ "DoubleBackticks" ] = "\ * ALWAYS surround inline code with double backticks to avoid ambiguity with context names: ``MyContext`MyFunction[x]``"; $basePromptComponents[ "MathExpressions" ] = "\ -* Write math expressions using LaTeX and surround them with dollar signs: $x^2 + y^2$"; +* Write math expressions using LaTeX and surround them with dollar signs: $$x^2 + y^2$$"; $basePromptComponents[ "EscapedCharacters" ] = "\ -* IMPORTANT! Whenever you write a literal backtick or dollar sign in text, ALWAYS escape it with a backslash. \ +* IMPORTANT! Whenever you write a literal backtick (`) or dollar sign ($) in text, ALWAYS escape it with a backslash. \ Example: It costs me \\$99.95 every time you forget to escape \\` or \\$ properly!"; $basePromptComponents[ "DocumentationLinkSyntax" ] = "\ @@ -136,11 +138,15 @@ $basePromptComponents[ "ConversionLargeOutputs" ] = "\ $basePromptComponents[ "ConversionGraphics" ] = "\ * Rendered graphics will typically be replaced with a shortened box representation: \\!\\(\\*GraphicsBox[<<>>]\\)"; +$basePromptComponents[ "MarkdownImageBox" ] = "\ + * If there are images embedded in the notebook, they will be replaced by a box representation in the \ +form ``MarkdownImageBox[\"![label](uri)\"]``. You will also receive the original image immediately after this. \ +You can use the markdown from this box ``![label](uri)`` in your responses if you want to display the original image."; + $basePromptComponents[ "ConversionFormatting" ] = "\ * Cell formatting is removed when converting to text, so \ ``Cell[TextData[{StyleBox[\"Styled\", FontSlant -> \"Italic\"], \" message\"}], \"ChatInput\"]`` \ -becomes \ -``Styled message``."; +becomes ``Styled message``."; $basePromptComponents[ "VisibleUserInput" ] = "\ * The user can still see their input, so there's no need to repeat it in your response"; @@ -260,8 +266,10 @@ $collectedPromptComponents = AssociationMap[ Keys @ Association[ KeyTake[ $basePromptComponents, $basePromptOrder ], $basePromptComponents ] ]; +$fullBasePrompt = $basePrompt; + If[ Wolfram`ChatbookInternal`$BuildingMX, - Null + Null; ]; End[ ]; diff --git a/Source/Chatbook/Sandbox.wl b/Source/Chatbook/Sandbox.wl index fe7700e9..369a7845 100644 --- a/Source/Chatbook/Sandbox.wl +++ b/Source/Chatbook/Sandbox.wl @@ -566,48 +566,6 @@ sandboxFormatter[ result_, ___ ] := result; sandboxFormatter // endDefinition; -(* ::**************************************************************************************************************:: *) -(* ::Section::Closed:: *) -(*Misc*) - -(* ::**************************************************************************************************************:: *) -(* ::Subsection::Closed:: *) -(*Graphics Utilities*) - -$$graphics = HoldPattern[ _GeoGraphics | _Graphics | _Graphics3D | _Image | _Image3D | _Legended ]; - -(* ::**************************************************************************************************************:: *) -(* ::Subsubsection::Closed:: *) -(*graphicsQ*) -graphicsQ[ $$graphics ] := True; -graphicsQ[ g_ ] := MatchQ[ Quiet @ Show @ Unevaluated @ g, $$graphics ]; -graphicsQ[ ___ ] := False; - -(* ::**************************************************************************************************************:: *) -(* ::Subsubsection::Closed:: *) -(*validGraphicsQ*) -validGraphicsQ[ g_? graphicsQ ] := getGraphicsErrors @ Unevaluated @ g === { }; -validGraphicsQ[ ___ ] := False; - -(* ::**************************************************************************************************************:: *) -(* ::Subsubsection::Closed:: *) -(*getGraphicsErrors*) -getGraphicsErrors // beginDefinition; - -(* TODO: hook this up to outputs to give feedback about pink boxes *) -getGraphicsErrors[ gfx_ ] := - Module[ { cell, nbo }, - cell = Cell @ BoxData @ MakeBoxes @ gfx; - UsingFrontEnd @ WithCleanup[ - nbo = NotebookPut[ Notebook @ { cell }, Visible -> False ], - SelectionMove[ First @ Cells @ nbo, All, Cell ]; - MathLink`CallFrontEnd @ FrontEnd`GetErrorsInSelectionPacket @ nbo, - NotebookClose @ nbo - ] - ]; - -getGraphicsErrors // endDefinition; - (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) (*Package Footer*) diff --git a/Source/Chatbook/SendChat.wl b/Source/Chatbook/SendChat.wl index a87fe3f9..b9842951 100644 --- a/Source/Chatbook/SendChat.wl +++ b/Source/Chatbook/SendChat.wl @@ -2,6 +2,8 @@ (*Package Header*) BeginPackage[ "Wolfram`Chatbook`SendChat`" ]; +(* cSpell: ignore evaliator *) + (* :!CodeAnalysis::BeginBlock:: *) `$debugLog; @@ -24,6 +26,7 @@ Needs[ "Wolfram`Chatbook`Handlers`" ]; Needs[ "Wolfram`Chatbook`InlineReferences`" ]; Needs[ "Wolfram`Chatbook`Models`" ]; Needs[ "Wolfram`Chatbook`Personas`" ]; +Needs[ "Wolfram`Chatbook`Serialization`" ]; Needs[ "Wolfram`Chatbook`Services`" ]; Needs[ "Wolfram`Chatbook`Settings`" ]; Needs[ "Wolfram`Chatbook`Tools`" ]; @@ -306,12 +309,12 @@ makeHTTPRequest[ settings_Association? AssociationQ, messages: { __Association } stream = True; (* model parameters *) - model = Lookup[ settings, "Model" , "gpt-3.5-turbo" ]; - tokens = Lookup[ settings, "MaxTokens" , Automatic ]; - temperature = Lookup[ settings, "Temperature" , 0.7 ]; - topP = Lookup[ settings, "TopP" , 1 ]; - freqPenalty = Lookup[ settings, "FrequencyPenalty", 0.1 ]; - presPenalty = Lookup[ settings, "PresencePenalty" , 0.1 ]; + model = Lookup[ settings, "Model" , $DefaultModel ]; + tokens = Lookup[ settings, "MaxTokens" , Automatic ]; + temperature = Lookup[ settings, "Temperature" , 0.7 ]; + topP = Lookup[ settings, "TopP" , 1 ]; + freqPenalty = Lookup[ settings, "FrequencyPenalty", 0.1 ]; + presPenalty = Lookup[ settings, "PresencePenalty" , 0.1 ]; data = DeleteCases[ <| @@ -330,6 +333,7 @@ makeHTTPRequest[ settings_Association? AssociationQ, messages: { __Association } body = ConfirmBy[ Developer`WriteRawJSONString[ data, "Compact" -> True ], StringQ ]; + $lastHTTPParameters = data; $lastRequest = HTTPRequest[ "https://api.openai.com/v1/chat/completions", <| @@ -349,19 +353,128 @@ makeHTTPRequest // endDefinition; (* ::Subsubsection::Closed:: *) (*prepareMessagesForHTTPRequest*) prepareMessagesForHTTPRequest // beginDefinition; -prepareMessagesForHTTPRequest[ message_Association ] := prepareMessagesForHTTPRequest0 @ KeyMap[ ToLowerCase, message ]; -prepareMessagesForHTTPRequest[ messages_List ] := prepareMessagesForHTTPRequest /@ messages; +prepareMessagesForHTTPRequest[ messages_List ] := $lastHTTPMessages = prepareMessageForHTTPRequest /@ messages; prepareMessagesForHTTPRequest // endDefinition; -prepareMessagesForHTTPRequest0 // beginDefinition; +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*prepareMessageForHTTPRequest*) +prepareMessageForHTTPRequest // beginDefinition; +prepareMessageForHTTPRequest[ message_Association? AssociationQ ] := AssociationMap[ prepareMessageRule, message ]; +prepareMessageForHTTPRequest // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*prepareMessageRule*) +prepareMessageRule // beginDefinition; +prepareMessageRule[ "Role"|"role" -> role_String ] := "role" -> ToLowerCase @ role; +prepareMessageRule[ "Content"|"content" -> content_ ] := "content" -> prepareMessageContent @ content; +prepareMessageRule[ key_String -> value_ ] := ToLowerCase @ key -> value; +prepareMessageRule // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*prepareMessageContent*) +prepareMessageContent // beginDefinition; +prepareMessageContent[ content_String ] := content; +prepareMessageContent[ content_List ] := prepareMessagePart /@ content; +prepareMessageContent // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*prepareMessagePart*) +prepareMessagePart // beginDefinition; +prepareMessagePart[ text_String ] := prepareMessagePart @ <| "Type" -> "Text" , "Data" -> text |>; +prepareMessagePart[ image_? graphicsQ ] := prepareMessagePart @ <| "Type" -> "Image", "Data" -> image |>; +prepareMessagePart[ file_File ] := prepareMessagePart @ <| "Type" -> partType @ file, "Data" -> file |>; +prepareMessagePart[ url_URL ] := prepareMessagePart @ <| "Type" -> partType @ url , "Data" -> url |>; +prepareMessagePart[ part_? AssociationQ ] := prepareMessagePart0 @ KeyMap[ ToLowerCase, part ]; +prepareMessagePart // endDefinition; + +prepareMessagePart0 // beginDefinition; + +prepareMessagePart0[ part_Association ] := <| + KeyDrop[ part, "data" ], + prepareMessagePart0[ part[ "type" ], part[ "data" ] ] +|>; + +prepareMessagePart0[ "Text" , text_ ] := <| "type" -> "text" , "text" -> prepareTextPart @ text |>; +prepareMessagePart0[ "Image", img_ ] := <| "type" -> "image_url", "image_url" -> prepareImageURLPart @ img |>; +prepareMessagePart0 // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*prepareImageURLPart*) +prepareImageURLPart // beginDefinition; +prepareImageURLPart[ URL[ url_String ] ] := <| "url" -> url |>; +prepareImageURLPart[ file_File ] := With[ { img = importImage @ file }, prepareImageURLPart @ img /; graphicsQ @ img ]; +prepareImageURLPart[ image_ ] := prepareImageURLPart @ URL @ toDataURI @ image; +prepareImageURLPart // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsubsection::Closed:: *) +(*importImage*) +importImage // beginDefinition; +importImage[ file_File ] := importImage[ file, fastFileHash @ file ]; +importImage[ file_, hash_Integer ] := With[ { img = $importImageCache[ file, hash ] }, img /; image2DQ @ img ]; +importImage[ file_, hash_Integer ] := $importImageCache[ file ] = <| hash -> importImage0 @ file |>; +importImage // endDefinition; + +$importImageCache = <| |>; + +importImage0 // beginDefinition; +importImage0[ file_ ] := importImage0[ file, Import[ file, "Image" ] ]; +importImage0[ file_, img_? graphicsQ ] := img; +importImage0 // endDefinition; -prepareMessagesForHTTPRequest0[ message: KeyValuePattern[ "role" -> role_String ] ] := - Insert[ message, "role" -> ToLowerCase @ role, Key[ "role" ] ]; +(* TODO: if an "ExternalFile" type of chat cell is defined, this should throw appropriate error text here *) -prepareMessagesForHTTPRequest0[ message_Association ] := - message; +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*prepareTextPart*) +prepareTextPart // beginDefinition; +prepareTextPart[ text_String? StringQ ] := text; +prepareTextPart[ file_File? FileExistsQ ] := prepareTextPart @ readString @ file; +prepareTextPart[ url_URL ] := prepareTextPart @ URLRead[ url, "Body" ]; +prepareTextPart[ failure_Failure ] := makeFailureString @ failure; +prepareTextPart // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*partType*) +partType // beginDefinition; +partType[ url_URL ] := "Image"; (* this could determine type automatically, but it would likely be too slow *) +partType[ file_File ] := partType[ file, fastFileHash @ file ]; +partType[ file_, hash_Integer ] := With[ { type = $partTypeCache[ file, hash ] }, type /; StringQ @ type ]; +partType[ file_, hash_Integer ] := With[ { t = partType0 @ file }, $partTypeCache[ file ] = <| hash -> t |>; t ]; +partType // endDefinition; + +$partTypeCache = <| |>; + +partType0 // beginDefinition; +partType0[ file_ ] := partType0[ file, FileFormat @ file ]; +partType0[ file_, fmt_String ] := partType0[ file, fmt, FileFormatProperties[ fmt, "ImportElements" ] ]; +partType0[ file_, fmt_, { ___, "Image", ___ } ] := "Image"; +partType0[ file_, fmt_, _List ] := "Text"; +partType0 // endDefinition; + +(* TODO: if an "ExternalFile" type of chat cell is defined, this should throw appropriate error text here *) + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*toDataURI*) +toDataURI // beginDefinition; + +toDataURI[ image_ ] := Enclose[ + Module[ { resized, base64 }, + resized = ConfirmBy[ resizeMultimodalImage @ image, image2DQ, "Resized" ]; + base64 = ConfirmBy[ ExportString[ resized, { "Base64", "PNG" } ], StringQ, "Base64" ]; + toDataURI[ image ] = "data:image/png;base64," <> StringDelete[ base64, "\n" ] + ], + throwInternalFailure[ toDataURI @ image, ## ] & +]; -prepareMessagesForHTTPRequest0 // endDefinition; +toDataURI // endDefinition; (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) @@ -451,7 +564,7 @@ ServiceConnectionUtilities`ConnectionInformation["Anthropic", "ProcessedRequests makeLLMConfiguration[ as_Association ] := $lastLLMConfiguration = LLMConfiguration @ Association[ - KeyTake[ as, { "Model" } ], + KeyTake[ as, { "Model", "MaxTokens" } ], "StopTokens" -> { "ENDTOOLCALL" } ]; @@ -649,12 +762,24 @@ appendStringContent // Attributes = { HoldFirst }; appendStringContent[ container_, text_String ] := If[ StringQ @ container, - container = StringDelete[ container <> convertUTF8 @ text, StartOfString~~Whitespace ], - container = convertUTF8 @ text + container = autoCorrect @ StringDelete[ container <> convertUTF8 @ text, StartOfString~~Whitespace ], + container = autoCorrect @ convertUTF8 @ text ]; appendStringContent // endDefinition; +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*autoCorrect*) +autoCorrect // beginDefinition; +autoCorrect[ string_String ] := StringReplace[ string, $llmAutoCorrectRules ]; +autoCorrect // endDefinition; + +$llmAutoCorrectRules = { + "wolfram_language_evaliator" -> "wolfram_language_evaluator", + "\\!\\(\\*MarkdownImageBox[\"" ~~ uri__ ~~ "\"]\\)" :> uri +}; + (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) (*splitDynamicContent*) @@ -1057,12 +1182,8 @@ resolveAutoSettings // beginDefinition; resolveAutoSettings[ settings: KeyValuePattern[ _ :> _ ] ] := resolveAutoSettings @ AssociationMap[ Apply @ Rule, settings ]; -(* Determine if tools are actually enabled based on settings *) -resolveAutoSettings[ settings: KeyValuePattern[ "ToolsEnabled" -> Automatic ] ] := - resolveAutoSettings @ Association[ settings, "ToolsEnabled" -> toolsEnabledQ @ settings ]; - (* Add additional settings and resolve actual LLMTool expressions *) -resolveAutoSettings[ settings_Association ] := resolveTools @ <| +resolveAutoSettings[ settings_Association ] := resolveAutoSettings0 @ <| settings, "HandlerFunctions" -> getHandlerFunctions @ settings, "LLMEvaluator" -> getLLMEvaluator @ settings, @@ -1071,7 +1192,157 @@ resolveAutoSettings[ settings_Association ] := resolveTools @ <| resolveAutoSettings // endDefinition; -(* TODO: define singular `resolveAutoSetting` that expands each `Automatic` value *) + +resolveAutoSettings0 // beginDefinition; + +resolveAutoSettings0[ settings_Association ] := Enclose[ + Module[ { auto, sorted, resolved }, + auto = ConfirmBy[ Select[ settings, SameAs @ Automatic ], AssociationQ, "Auto" ]; + sorted = ConfirmBy[ <| KeyTake[ auto, $autoSettingKeyPriority ], auto |>, AssociationQ, "Sorted" ]; + resolved = ConfirmBy[ Fold[ resolveAutoSetting, settings, Normal @ sorted ], AssociationQ, "Resolved" ]; + ConfirmBy[ resolveTools @ KeySort @ resolved, AssociationQ, "ResolveTools" ] + ], + throwInternalFailure[ resolveAutoSettings0 @ settings, ## ] & +]; + +resolveAutoSettings0 // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*resolveAutoSetting*) +resolveAutoSetting // beginDefinition; +resolveAutoSetting[ settings_, key_ -> value_ ] := <| settings, key -> resolveAutoSetting0[ settings, key ] |>; +resolveAutoSetting // endDefinition; + +resolveAutoSetting0 // beginDefinition; +resolveAutoSetting0[ as_, "ToolsEnabled" ] := toolsEnabledQ @ as; +resolveAutoSetting0[ as_, "DynamicAutoFormat" ] := dynamicAutoFormatQ @ as; +resolveAutoSetting0[ as_, "EnableLLMServices" ] := $useLLMServices; +resolveAutoSetting0[ as_, "HandlerFunctionsKeys" ] := chatHandlerFunctionsKeys @ as; +resolveAutoSetting0[ as_, "IncludeHistory" ] := Automatic; +resolveAutoSetting0[ as_, "NotebookWriteMethod" ] := "PreemptiveLink"; +resolveAutoSetting0[ as_, "ShowMinimized" ] := Automatic; +resolveAutoSetting0[ as_, "StreamingOutputMethod" ] := "PartialDynamic"; +resolveAutoSetting0[ as_, "TrackScrollingWhenPlaced" ] := scrollOutputQ @ as; +resolveAutoSetting0[ as_, "Multimodal" ] := multimodalQ @ as; +resolveAutoSetting0[ as_, "MaxContextTokens" ] := autoMaxContextTokens @ as; +resolveAutoSetting0[ as_, "MaxTokens" ] := autoMaxTokens @ as; +resolveAutoSetting0[ as_, "MaxCellStringLength" ] := chooseMaxCellStringLength @ as; +resolveAutoSetting0[ as_, "MaxOutputCellStringLength" ] := chooseMaxOutputCellStringLength @ as; +resolveAutoSetting0[ as_, "Tokenizer" ] := getTokenizer @ as; +resolveAutoSetting0[ as_, key_String ] := Automatic; +resolveAutoSetting0 // endDefinition; + +(* Settings that require other settings to be resolved first: *) +$autoSettingKeyDependencies = <| + "HandlerFunctionsKeys" -> "EnableLLMServices", + "MaxCellStringLength" -> { "Model", "MaxContextTokens" }, + "MaxContextTokens" -> "Model", + "MaxOutputCellStringLength" -> "MaxCellStringLength", + "MaxTokens" -> "Model", + "Multimodal" -> { "EnableLLMServices", "Model" }, + "Tokenizer" -> "Model", + "Tools" -> { "LLMEvaluator", "ToolsEnabled" }, + "ToolsEnabled" -> "Model" +|>; + +(* Sort topologically so dependencies will be satisfied in order: *) +$autoSettingKeyPriority := Enclose[ + $autoSettingKeyPriority = ConfirmMatch[ + TopologicalSort @ Flatten @ KeyValueMap[ + Thread @* Reverse @* Rule, + $autoSettingKeyDependencies + ], + { __String? StringQ } + ], + throwInternalFailure[ $autoSettingKeyPriority, ## ] & +]; + +(* TODO: resolve these automatic values here: + * BasePrompt (might not be possible here) + * ChatContextPreprompt +*) + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*chooseMaxCellStringLength*) +(* FIXME: need to hook into token pressure to gradually decrease limits *) +chooseMaxCellStringLength // beginDefinition; +chooseMaxCellStringLength[ as_Association ] := chooseMaxCellStringLength[ as, as[ "MaxContextTokens" ] ]; +chooseMaxCellStringLength[ as_, tokens: $$size ] := Ceiling[ $defaultMaxCellStringLength * tokens / 2^13 ]; +chooseMaxCellStringLength // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*chooseMaxOutputCellStringLength*) +chooseMaxOutputCellStringLength // beginDefinition; +chooseMaxOutputCellStringLength[ as_Association ] := chooseMaxOutputCellStringLength[ as, as[ "MaxCellStringLength" ] ]; +chooseMaxOutputCellStringLength[ as_, size: $$size ] := Min[ Ceiling[ size / 10 ], 1000 ]; +chooseMaxOutputCellStringLength // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*autoMaxContextTokens*) +autoMaxContextTokens // beginDefinition; +autoMaxContextTokens[ as_Association ] := autoMaxContextTokens[ as, as[ "Model" ] ]; +autoMaxContextTokens[ as_, model_ ] := autoMaxContextTokens[ as, model, toModelName @ model ]; +autoMaxContextTokens[ _, _, name_String ] := autoMaxContextTokens0 @ name; +autoMaxContextTokens // endDefinition; + +autoMaxContextTokens0 // beginDefinition; +autoMaxContextTokens0[ name_String ] := autoMaxContextTokens0 @ StringSplit[ name, "-"|Whitespace ]; +autoMaxContextTokens0[ { ___, "gpt", "4", "vision", ___ } ] := 2^17; +autoMaxContextTokens0[ { ___, "gpt", "4", "turbo" , ___ } ] := 2^17; +autoMaxContextTokens0[ { ___, "claude", "2" , ___ } ] := 10^5; +autoMaxContextTokens0[ { ___, "16k" , ___ } ] := 2^14; +autoMaxContextTokens0[ { ___, "32k" , ___ } ] := 2^15; +autoMaxContextTokens0[ { ___, "gpt", "4" , ___ } ] := 2^13; +autoMaxContextTokens0[ { ___, "gpt", "3.5" , ___ } ] := 2^12; +autoMaxContextTokens0[ _List ] := 2^12; +autoMaxContextTokens0 // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*autoMaxTokens*) +autoMaxTokens // beginDefinition; +autoMaxTokens[ as_Association ] := autoMaxTokens[ as, as[ "Model" ] ]; +autoMaxTokens[ as_, model_ ] := autoMaxTokens[ as, model, toModelName @ model ]; +autoMaxTokens[ as_, model_, name_String ] := Lookup[ $maxTokensTable, name, Automatic ]; +autoMaxTokens // endDefinition; + +(* FIXME: this should be something queryable from LLMServices: *) +$maxTokensTable = <| + "gpt-4-vision-preview" -> 4096, + "gpt-4-1106-preview" -> 4096 +|>; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*multimodalQ*) +multimodalQ // beginDefinition; +multimodalQ[ as_Association ] := multimodalQ[ as, multimodalModelQ @ as[ "Model" ], as[ "EnableLLMServices" ] ]; +multimodalQ[ as_, True , False ] := True; +multimodalQ[ as_, True , True ] := multimodalPacletsAvailable[ ]; +multimodalQ[ as_, False, _ ] := False; +multimodalQ // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsubsection::Closed:: *) +(*$multimodalPacletsAvailable*) +multimodalPacletsAvailable // beginDefinition; + +multimodalPacletsAvailable[ ] := multimodalPacletsAvailable[ ] = ( + initTools[ ]; + multimodalPacletsAvailable[ + PacletObject[ "Wolfram/LLMFunctions" ], + PacletObject[ "ServiceConnection_OpenAI" ] + ] +); + +multimodalPacletsAvailable[ llmFunctions_PacletObject? PacletObjectQ, openAI_PacletObject? PacletObjectQ ] := + TrueQ @ And[ PacletNewerQ[ llmFunctions, "1.2.4" ], PacletNewerQ[ openAI, "13.3.18" ] ]; + +multimodalPacletsAvailable // endDefinition; (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) @@ -1650,7 +1921,9 @@ writeReformattedCell[ settings_, string_String, cell_CellObject ] := Enclose[ With[ { new = new, info = info }, createTask @ applyProcessingFunction[ settings, "WriteChatOutputCell", HoldComplete[ cell, new, info ] ] - ] + ]; + + waitForCellObject[ settings, output ] ] ], throwInternalFailure[ writeReformattedCell[ settings, string, cell ], ## ] & @@ -1675,6 +1948,19 @@ writeReformattedCell[ settings_, other_, cell_CellObject ] := writeReformattedCell // endDefinition; +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*waitForCellObject*) +waitForCellObject // beginDefinition; + +waitForCellObject[ settings_, cell_CellObject, timeout_: 1 ] := + If[ settings[ "HandlerFunctions", "ChatPost" ] =!= None, + TimeConstrained[ While[ ! StringQ @ CurrentValue[ cell, ExpressionUUID ], Pause[ 0.05 ] ], timeout ]; + cell + ]; + +waitForCellObject // endDefinition; + (* ::**************************************************************************************************************:: *) (* ::Subsubsubsection::Closed:: *) (*scrollOutputQ*) @@ -2096,7 +2382,7 @@ errorBoxes[ as___ ] := (* ::Section::Closed:: *) (*Package Footer*) If[ Wolfram`ChatbookInternal`$BuildingMX, - Null; + $autoSettingKeyPriority; ]; (* :!CodeAnalysis::EndBlock:: *) diff --git a/Source/Chatbook/Serialization.wl b/Source/Chatbook/Serialization.wl index cfa9bf9c..4955516a 100644 --- a/Source/Chatbook/Serialization.wl +++ b/Source/Chatbook/Serialization.wl @@ -11,16 +11,23 @@ CellToString[cell$] serializes a Cell expression as a string for use in chat.\ `$CellToStringDebug; `$CurrentCell; +`$defaultMaxCellStringLength; +`$defaultMaxOutputCellStringLength; `documentationSearchAPI; `escapeMarkdownString; -`$maxOutputCellStringLength; +`truncateString; Begin[ "`Private`" ]; -Needs[ "Wolfram`Chatbook`" ]; -Needs[ "Wolfram`Chatbook`ErrorUtils`" ]; -Needs[ "Wolfram`Chatbook`FrontEnd`" ]; -Needs[ "Wolfram`Chatbook`Prompting`" ]; +Needs[ "Wolfram`Chatbook`" ]; +Needs[ "Wolfram`Chatbook`ChatMessages`" ]; +Needs[ "Wolfram`Chatbook`Common`" ]; +Needs[ "Wolfram`Chatbook`ErrorUtils`" ]; +Needs[ "Wolfram`Chatbook`FrontEnd`" ]; +Needs[ "Wolfram`Chatbook`Models`" ]; +Needs[ "Wolfram`Chatbook`Prompting`" ]; +Needs[ "Wolfram`Chatbook`Tools`" ]; +Needs[ "Wolfram`Chatbook`Utils`" ]; (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) @@ -39,13 +46,23 @@ $$outputStyle = "Output"|"Print"|"Echo"; $cellCharacterEncoding = "Unicode"; (* Set a max string length for output cells to avoid blowing up token counts *) -$maxOutputCellStringLength = 500; +$maxOutputCellStringLength = Automatic; +$defaultMaxOutputCellStringLength = 500; (* Set an overall max string length for any type of cell *) -$maxCellStringLength = 5000; +$maxCellStringLength = Automatic; +$defaultMaxCellStringLength = 10000; (* Set a page width for expressions that need to be serialized as InputForm *) -$cellPageWidth = 100; +$cellPageWidth = 100; +$defaultCellPageWidth = $cellPageWidth; + +(* Window width to use when converting cells to multimodal images (Automatic means derive from $cellPageWidth): *) +$windowWidth = Automatic; +$defaultWindowWidth = 625; + +(* Maximum number of images to include in multimodal messages per cell before switching to a fully rasterized cell: *) +$maxMarkdownBoxes = 5; (* Whether to collect data that can help discover missing definitions *) $CellToStringDebug = False; @@ -90,6 +107,8 @@ $graphicsHeads = Alternatives[ Graphics3DBox ]; +$$graphicsBox = $graphicsHeads[ ___ ] | TemplateBox[ _, "Legended", ___ ]; + (* Serialize the first argument of these and ignore the rest *) $stringStripHeads = Alternatives[ ButtonBox, @@ -173,22 +192,40 @@ WOLFRAM_ALPHA_PARSED_INPUT: %%Code%% CellToString // SetFallthroughError; CellToString // Options = { - CharacterEncoding -> $cellCharacterEncoding, - "CharacterNormalization" -> "NFKC", (* FIXME: do this *) - "Debug" :> $CellToStringDebug, - PageWidth -> $cellPageWidth + "CharacterEncoding" -> $cellCharacterEncoding, + "CharacterNormalization" -> "NFKC", (* FIXME: do this *) + "Debug" :> $CellToStringDebug, + "MaxCellStringLength" -> $maxCellStringLength, + "MaxOutputCellStringLength" -> $maxOutputCellStringLength, + "PageWidth" -> $cellPageWidth, + "WindowWidth" -> $windowWidth }; (* :!CodeAnalysis::BeginBlock:: *) (* :!CodeAnalysis::Disable::SuspiciousSessionSymbol:: *) CellToString[ cell_, opts: OptionsPattern[ ] ] := - Block[ + Catch @ Block[ { $cellCharacterEncoding = OptionValue[ "CharacterEncoding" ], - $CellToStringDebug = TrueQ @ OptionValue[ "Debug" ], - $cellPageWidth = OptionValue[ "PageWidth" ] + $CellToStringDebug = TrueQ @ OptionValue[ "Debug" ], + $cellPageWidth, $windowWidth, $maxCellStringLength, $maxOutputCellStringLength }, - $fasterCellToStringFailBag = Internal`Bag[ ]; + $cellPageWidth = toSize[ OptionValue @ PageWidth, $defaultCellPageWidth ]; + $windowWidth = toWindowWidth[ OptionValue @ WindowWidth, $cellPageWidth ]; + + $maxCellStringLength = Ceiling @ toSize[ + OptionValue[ "MaxCellStringLength" ], + $defaultMaxCellStringLength + ]; + + If[ $maxCellStringLength <= 0, Throw[ "[Cell Excised]" ] ]; + + $maxOutputCellStringLength = Ceiling @ toSize[ + OptionValue[ "MaxOutputCellStringLength" ], + $defaultMaxOutputCellStringLength + ]; + + If[ $CellToStringDebug, $fasterCellToStringFailBag = Internal`Bag[ ] ]; If[ ! StringQ @ $cellCharacterEncoding, $cellCharacterEncoding = "UTF-8" ]; WithCleanup[ Replace[ @@ -204,10 +241,25 @@ CellToString[ cell_, opts: OptionsPattern[ ] ] := ]; (* :!CodeAnalysis::EndBlock:: *) +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*toSize*) +toSize // beginDefinition; +toSize[ size: $$size, default_ ] := size; +toSize[ size_, default: $$size ] := default; +toSize // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*toWindowWidth*) +toWindowWidth[ width: $$size, pageWidth_ ] := width; +toWindowWidth[ width_, pageWidth: $$size ] := 6.25 * pageWidth; +toWindowWidth[ ___ ] := $defaultWindowWidth; + (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*cellToString*) -cellToString // SetFallthroughError; +cellToString // beginDefinition; (* Argument normalization *) cellToString[ data: _TextData|_BoxData|_RawData ] := cellToString @ Cell @ data; @@ -311,6 +363,8 @@ cellToString[ cell: Cell[ _String, "ChatOutput", ___ ] ] := Block[ { $escapeMark cellToString[ cell: Cell[ _TextData|_String, ___ ] ] := Block[ { $escapeMarkdown = True }, cellToString0 @ cell ]; cellToString[ cell_ ] := Block[ { $escapeMarkdown = False }, cellToString0 @ cell ]; +cellToString // endDefinition; + (* Recursive serialization of the cell content *) cellToString0[ cell0_ ] := With[ @@ -346,10 +400,10 @@ fasterCellToString[ arg_ ] := (* ::**************************************************************************************************************:: *) (* ::Subsubsubsection::Closed:: *) -(*Ignored/Skipped*) - -fasterCellToString0[ $ignoredBoxPatterns ] := ""; -fasterCellToString0[ $stringStripHeads[ a_, ___ ] ] := fasterCellToString0 @ a; +(*Multimodal Cell Images*) +fasterCellToString0[ cell: Cell[ _BoxData, ___ ] ] /; + $multimodalMessages && Count[ cell, $$graphicsBox, Infinity ] > $maxMarkdownBoxes := + toMarkdownImageBox @ cell; (* ::**************************************************************************************************************:: *) (* ::Subsubsubsection::Closed:: *) @@ -361,6 +415,15 @@ fasterCellToString0[ (Cell|StyleBox)[ a_, "Subsubsubsection", ___ ] ] := "#### " fasterCellToString0[ (Cell|StyleBox)[ a_, "Subsubsubsubsection", ___ ] ] := "##### "<>fasterCellToString0 @ a; fasterCellToString0[ (Cell|StyleBox)[ a_, "ChatBlockDivider", ___ ] ] := "# "<>fasterCellToString0 @ a; +(* ::**************************************************************************************************************:: *) +(* ::Subsubsubsection::Closed:: *) +(*Styles*) +fasterCellToString0[ (h: Cell|StyleBox)[ a__, FontWeight -> Bold|"Bold", b___ ] ] := + "**" <> fasterCellToString0 @ h[ a, b ] <> "**"; + +fasterCellToString0[ (h: Cell|StyleBox)[ a__, FontSlant -> Italic|"Italic", b___ ] ] := + "*" <> fasterCellToString0 @ h[ a, b ] <> "*"; + (* ::**************************************************************************************************************:: *) (* ::Subsubsubsection::Closed:: *) (*String Normalization*) @@ -481,7 +544,7 @@ fasterCellToString0[ NamespaceBox[ (* ::**************************************************************************************************************:: *) (* ::Subsubsubsection::Closed:: *) (*Graphics*) -fasterCellToString0[ box: GraphicsBox[ TagBox[ RasterBox[ _, r___ ], t___ ], g___ ] ] := +fasterCellToString0[ box: GraphicsBox[ TagBox[ RasterBox[ _, r___ ], t___ ], g___ ] ] /; ! TrueQ @ $multimodalMessages := StringJoin[ "\\!\\(\\*", StringReplace[ @@ -491,23 +554,66 @@ fasterCellToString0[ box: GraphicsBox[ TagBox[ RasterBox[ _, r___ ], t___ ], g__ "\\)" ]; -fasterCellToString0[ box: $graphicsHeads[ ___ ] ] := - If[ TrueQ[ ByteCount @ box < $maxOutputCellStringLength ], +fasterCellToString0[ box: $$graphicsBox ] := + Which[ + (* If in multimodal mode, sow the rasterized box and insert the id: *) + TrueQ @ $multimodalMessages, + toMarkdownImageBox @ box, + (* For relatively small graphics expressions, we'll give an InputForm string *) - needsBasePrompt[ "Notebooks" ]; - truncateString @ makeGraphicsString @ box, + TrueQ[ ByteCount @ box < $maxOutputCellStringLength ], + ( + needsBasePrompt[ "Notebooks" ]; + truncateString @ makeGraphicsString @ box + ), + (* Otherwise, give the same thing you'd get in a standalone kernel*) - needsBasePrompt[ "ConversionGraphics" ]; - truncateString[ "\\!\\(\\*" <> StringReplace[ inputFormString @ box, $graphicsBoxStringReplacements ] <> "\\)" ] + True, + ( + needsBasePrompt[ "ConversionGraphics" ]; + truncateString[ + "\\!\\(\\*" <> StringReplace[ inputFormString @ box, $graphicsBoxStringReplacements ] <> "\\)" + ] + ) ]; + $graphicsBoxStringReplacements = { a: DigitCharacter ~~ "." ~~ b: Repeated[ DigitCharacter, { 4, Infinity } ] :> a <> "." <> StringTake[ b, 3 ], "\"$$DATA$$\"" -> "...", "$$DATA$$" -> "..." }; +(* ::**************************************************************************************************************:: *) +(* ::Subsubsubsubsection::Closed:: *) +(*toMarkdownImageBox*) +toMarkdownImageBox // beginDefinition; + +toMarkdownImageBox[ graphics_ ] := Enclose[ + Module[ { img, uri }, + img = ConfirmBy[ rasterizeGraphics @ graphics, ImageQ, "RasterizeGraphics" ]; + uri = ConfirmBy[ MakeExpressionURI[ "image", img ], StringQ, "RasterID" ]; + needsBasePrompt[ "MarkdownImageBox" ]; + "\\!\\(\\*MarkdownImageBox[\"" <> uri <> "\"]\\)" + ], + throwInternalFailure[ toMarkdownImageBox @ graphics, ## ] & +]; + +toMarkdownImageBox // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsubsubsection::Closed:: *) +(*rasterizeGraphics*) +rasterizeGraphics // beginDefinition; +rasterizeGraphics[ gfx: $$graphicsBox ] := rasterizeGraphics[ gfx ] = Rasterize @ RawBoxes @ gfx; +rasterizeGraphics[ cell_Cell ] := rasterizeGraphics[ cell, 6.25*$cellPageWidth ]; + +rasterizeGraphics[ cell_Cell, width_Real ] := rasterizeGraphics[ cell, width ] = + Rasterize @ Append[ cell, PageWidth -> width ]; + +rasterizeGraphics // endDefinition; + (* ::**************************************************************************************************************:: *) (* ::Subsubsubsection::Closed:: *) (*Template Boxes*) @@ -525,6 +631,12 @@ fasterCellToString0[ StyleBox[ code_, "TI", ___ ] ] := "``" <> fasterCellToString0 @ code <> "``" ]; +fasterCellToString0[ Cell[ code_, "InlineCode", ___ ] ] := + Block[ { $escapeMarkdown = False }, + needsBasePrompt[ "DoubleBackticks" ]; + "``" <> fasterCellToString0 @ code <> "``" + ]; + (* Messages *) fasterCellToString0[ TemplateBox[ args: { _, _, str_String, ___ }, "MessageTemplate" ] ] := ( needsBasePrompt[ "WolframLanguage" ]; @@ -575,6 +687,17 @@ fasterCellToString0[ ButtonBox[ StyleBox[ label_, "SymbolsRefLink", ___ ], ___, "[" <> fasterCellToString0 @ label <> "](" <> uri <> ")" ); +fasterCellToString0[ + ButtonBox[ + label_, + OrderlessPatternSequence[ + BaseStyle -> "Hyperlink", + ButtonData -> { url: _String|_URL, _ }, + ___ + ] + ] +] := "[" <> fasterCellToString0 @ label <> "](" <> TextString @ url <> ")"; + (* TeXAssistantTemplate *) fasterCellToString0[ TemplateBox[ KeyValuePattern[ "input" -> string_ ], "TeXAssistantTemplate" ] ] := ( needsBasePrompt[ "Math" ]; @@ -805,6 +928,12 @@ fasterCellToString0[ DynamicModuleBox[ a___ ] ] /; ! TrueQ @ $CellToStringDebug "DynamicModule[<<" <> ToString @ Length @ HoldComplete @ a <> ">>]" ); +(* ::**************************************************************************************************************:: *) +(* ::Subsubsubsection::Closed:: *) +(*Ignored/Skipped*) +fasterCellToString0[ $ignoredBoxPatterns ] := ""; +fasterCellToString0[ $stringStripHeads[ a_, ___ ] ] := fasterCellToString0 @ a; + (* ::**************************************************************************************************************:: *) (* ::Subsubsubsection::Closed:: *) (*Missing Definition*) @@ -940,14 +1069,13 @@ escapeMarkdownCharactersQ[ ___ ] := True; (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) (*truncateString*) -truncateString // SetFallthroughError; +truncateString // beginDefinition; truncateString[ str_String ] := truncateString[ str, $maxOutputCellStringLength ]; -truncateString[ str_String, max_Integer ] := truncateString[ str, Ceiling[ max / 2 ], Floor[ max / 2 ] ]; -truncateString[ str_String, l_Integer, r_Integer ] /; StringLength @ str <= l + r + 5 := str; -truncateString[ str_String, l_Integer, r_Integer ] := StringTake[ str, l ] <> " ... " <> StringTake[ str, -r ]; +truncateString[ str_String, Automatic ] := truncateString[ str, $defaultMaxOutputCellStringLength ]; +truncateString[ str_String, max: $$size ] := stringTrimMiddle[ str, max ]; truncateString[ other_ ] := other; truncateString[ other_, _Integer ] := other; -truncateString[ other_, _Integer, _Integer ] := other; +truncateString // endDefinition; (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) @@ -1282,5 +1410,9 @@ firstMatchingCellGroup[ nb_, patt_, "Content" ] := Catch[ (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) (*Package Footer*) +If[ Wolfram`ChatbookInternal`$BuildingMX, + Null; +]; + End[ ]; EndPackage[ ]; diff --git a/Source/Chatbook/Settings.wl b/Source/Chatbook/Settings.wl index a2639a37..abbc46d5 100644 --- a/Source/Chatbook/Settings.wl +++ b/Source/Chatbook/Settings.wl @@ -20,43 +20,49 @@ Needs[ "Wolfram`Chatbook`FrontEnd`" ]; (* ::Section::Closed:: *) (*Configuration*) $defaultChatSettings = <| - "Assistance" -> Automatic, - "AutoFormat" -> True, - "BasePrompt" -> Automatic, - "ChatContextPreprompt" -> Automatic, - "ChatDrivenNotebook" -> False, - "ChatHistoryLength" -> 25, - "DynamicAutoFormat" -> Automatic, - "EnableChatGroupSettings" -> False, - "EnableLLMServices" -> Automatic, (* TODO: remove this once LLMServices is widely available *) - "FrequencyPenalty" -> 0.1, - "HandlerFunctions" :> $DefaultChatHandlerFunctions, - "HandlerFunctionsKeys" -> Automatic, - "IncludeHistory" -> Automatic, - "InitialChatCell" -> True, - "LLMEvaluator" -> "CodeAssistant", - "MaxTokens" -> Automatic, - "MergeMessages" -> True, - "Model" :> $DefaultModel, - "NotebookWriteMethod" -> Automatic, - "OpenAIKey" -> Automatic, (* TODO: remove this once LLMServices is widely available *) - "PresencePenalty" -> 0.1, - "ProcessingFunctions" :> $DefaultChatProcessingFunctions, - "Prompts" -> { }, - "ShowMinimized" -> Automatic, - "StreamingOutputMethod" -> Automatic, - "Temperature" -> 0.7, - "ToolOptions" :> $DefaultToolOptions, - "Tools" -> Automatic, - "ToolsEnabled" -> Automatic, - "TopP" -> 1, - "TrackScrollingWhenPlaced" -> Automatic + "Assistance" -> Automatic, + "AutoFormat" -> True, + "BasePrompt" -> Automatic, + "ChatContextPreprompt" -> Automatic, + "ChatDrivenNotebook" -> False, + "ChatHistoryLength" -> 100, + "DynamicAutoFormat" -> Automatic, + "EnableChatGroupSettings" -> False, + "EnableLLMServices" -> Automatic, (* TODO: remove this once LLMServices is widely available *) + "FrequencyPenalty" -> 0.1, + "HandlerFunctions" :> $DefaultChatHandlerFunctions, + "HandlerFunctionsKeys" -> Automatic, + "IncludeHistory" -> Automatic, + "InitialChatCell" -> True, + "LLMEvaluator" -> "CodeAssistant", + "MaxCellStringLength" -> Automatic, + "MaxContextTokens" -> Automatic, + "MaxOutputCellStringLength" -> Automatic, + "MaxTokens" -> Automatic, + "MergeMessages" -> True, + "Model" :> $DefaultModel, + "Multimodal" -> Automatic, + "NotebookWriteMethod" -> Automatic, + "OpenAIKey" -> Automatic, (* TODO: remove this once LLMServices is widely available *) + "PresencePenalty" -> 0.1, + "ProcessingFunctions" :> $DefaultChatProcessingFunctions, + "Prompts" -> { }, + "ShowMinimized" -> Automatic, + "StreamingOutputMethod" -> Automatic, + "Temperature" -> 0.7, + "Tokenizer" -> Automatic, + "ToolOptions" :> $DefaultToolOptions, + "Tools" -> Automatic, + "ToolsEnabled" -> Automatic, + "TopP" -> 1, + "TrackScrollingWhenPlaced" -> Automatic |>; (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*Argument Patterns*) $$feObj = _FrontEndObject | $FrontEndSession | _NotebookObject | _CellObject | _BoxObject; +$$validRootSettingValue = Inherited | _? (AssociationQ@*Association); (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) @@ -140,21 +146,120 @@ CurrentChatSettings[ args___ ] := (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*UpValues*) -CurrentChatSettings /: HoldPattern @ Set[ CurrentChatSettings[ obj0_, key_String ], value_ ] := - With[ { obj = obj0 }, - (CurrentValue[ obj, { TaggingRules, "ChatNotebookSettings", key } ] = value) /; MatchQ[ obj, $$feObj ] - ]; +CurrentChatSettings /: HoldPattern @ Set[ CurrentChatSettings[ args___ ], value_ ] := + catchTop[ UsingFrontEnd @ setCurrentChatSettings[ args, value ], CurrentChatSettings ]; + +CurrentChatSettings /: HoldPattern @ Unset[ CurrentChatSettings[ args___ ] ] := + catchTop[ UsingFrontEnd @ unsetCurrentChatSettings @ args, CurrentChatSettings ]; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*setCurrentChatSettings*) +setCurrentChatSettings // beginDefinition; + +(* Root settings: *) +setCurrentChatSettings[ value: $$validRootSettingValue ] := + setCurrentChatSettings0[ $FrontEnd, value ]; + +setCurrentChatSettings[ obj: $$feObj, value: $$validRootSettingValue ] := + setCurrentChatSettings0[ obj, value ]; + +(* Key settings: *) +setCurrentChatSettings[ key_String? StringQ, value_ ] := + setCurrentChatSettings0[ $FrontEnd, key, value ]; + +setCurrentChatSettings[ obj: $$feObj, key_String? StringQ, value_ ] := + setCurrentChatSettings0[ obj, key, value ]; + +(* Invalid scope: *) +setCurrentChatSettings[ obj: Except[ $$feObj ], a__ ] := throwFailure[ + "InvalidFrontEndScope", + obj, + CurrentChatSettings, + HoldForm @ setCurrentChatSettings[ obj, a ] +]; + +(* Invalid key: *) +setCurrentChatSettings[ obj: $$feObj, key_, value_ ] := throwFailure[ + "InvalidSettingsKey", + key, + CurrentChatSettings, + HoldForm @ setCurrentChatSettings[ obj, key, value ] +]; + +(* Invalid root settings: *) +setCurrentChatSettings[ value: Except[ $$validRootSettingValue ] ] := throwFailure[ + "InvalidRootSettings", + value, + CurrentChatSettings, + HoldForm @ setCurrentChatSettings @ value +]; + +setCurrentChatSettings[ obj: $$feObj, value: Except[ $$validRootSettingValue ] ] := throwFailure[ + "InvalidRootSettings", + value, + CurrentChatSettings, + HoldForm @ setCurrentChatSettings @ value +]; -CurrentChatSettings /: HoldPattern @ Set[ CurrentChatSettings[ key_String ], value_ ] := - CurrentValue[ $currentEvaluationObject, { TaggingRules, "ChatNotebookSettings", key } ] = value; +setCurrentChatSettings // endDefinition; -CurrentChatSettings /: HoldPattern @ Unset[ CurrentChatSettings[ obj0_, key_String ] ] := - With[ { obj = obj0 }, - (CurrentValue[ obj, { TaggingRules, "ChatNotebookSettings", key } ] = Inherited) /; MatchQ[ obj, $$feObj ] + +setCurrentChatSettings0 // beginDefinition; + +setCurrentChatSettings0[ scope: $$feObj, Inherited ] := + CurrentValue[ scope, { TaggingRules, "ChatNotebookSettings" } ] = Inherited; + +setCurrentChatSettings0[ scope: $$feObj, value_ ] := + With[ { as = Association @ value }, + (CurrentValue[ scope, { TaggingRules, "ChatNotebookSettings" } ] = as) /; AssociationQ @ as ]; -CurrentChatSettings /: HoldPattern @ Unset[ CurrentChatSettings[ key_String ] ] := - CurrentValue[ $currentEvaluationObject, { TaggingRules, "ChatNotebookSettings", key } ] = Inherited; +setCurrentChatSettings0[ scope: $$feObj, key_String? StringQ, value_ ] := + CurrentValue[ scope, { TaggingRules, "ChatNotebookSettings", key } ] = value; + +setCurrentChatSettings0 // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*unsetCurrentChatSettings*) +unsetCurrentChatSettings // beginDefinition; + +(* Root settings: *) +unsetCurrentChatSettings[ ] := unsetCurrentChatSettings0 @ $FrontEnd; +unsetCurrentChatSettings[ obj: $$feObj ] := unsetCurrentChatSettings0 @ obj; + +(* Key settings: *) +unsetCurrentChatSettings[ key_? StringQ ] := unsetCurrentChatSettings0[ $FrontEnd, key ]; +unsetCurrentChatSettings[ obj: $$feObj, key_? StringQ ] := unsetCurrentChatSettings0[ obj, key ]; + +(* Invalid scope: *) +unsetCurrentChatSettings[ obj: Except[ $$feObj ], a___ ] := throwFailure[ + "InvalidFrontEndScope", + obj, + CurrentChatSettings, + HoldForm @ unsetCurrentChatSettings[ obj, a ] +]; + +(* Invalid key: *) +unsetCurrentChatSettings[ obj: $$feObj, key_ ] := throwFailure[ + "InvalidSettingsKey", + key, + CurrentChatSettings, + HoldForm @ unsetCurrentChatSettings[ obj, key ] +]; + +unsetCurrentChatSettings // endDefinition; + +unsetCurrentChatSettings0 // beginDefinition; + +unsetCurrentChatSettings0[ obj: $$feObj ] := + (CurrentValue[ obj, { TaggingRules, "ChatNotebookSettings" } ] = Inherited); + +unsetCurrentChatSettings0[ obj: $$feObj, key_? StringQ ] := + (CurrentValue[ obj, { TaggingRules, "ChatNotebookSettings" } ] = Inherited); + +unsetCurrentChatSettings0 // endDefinition; (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) @@ -293,6 +398,8 @@ mergeChatSettings0[ { e_ } ] := e; mergeChatSettings0[ { } ] := Missing[ ]; mergeChatSettings0 // endDefinition; +(* TODO: need to apply special merging/inheritance for things like "Prompts" *) + (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) (*getPrecedingDelimiter*) diff --git a/Source/Chatbook/Tools.wl b/Source/Chatbook/Tools.wl index b2f96f35..d52ecd52 100644 --- a/Source/Chatbook/Tools.wl +++ b/Source/Chatbook/Tools.wl @@ -828,7 +828,7 @@ documentationBasicExamples // endDefinition; (* ::**************************************************************************************************************:: *) (* ::Subsubsubsection::Closed:: *) (*cellToString*) -cellToString[ args___ ] := Block[ { $maxOutputCellStringLength = 100 }, CellToString @ args ]; +cellToString[ args___ ] := CellToString[ args, "MaxCellStringLength" -> 100 ]; (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) @@ -1489,31 +1489,40 @@ MakeExpressionURI[ args: Repeated[ _, { 1, 3 } ] ] := makeExpressionURI @ args; (* ::Subsection::Closed:: *) (*GetExpressionURIs*) GetExpressionURIs // ClearAll; +GetExpressionURIs // Options = { Tooltip -> Automatic }; -GetExpressionURIs[ str_ ] := GetExpressionURIs[ str, ## & ]; +GetExpressionURIs[ str_, opts: OptionsPattern[ ] ] := + GetExpressionURIs[ str, ## &, opts ]; -GetExpressionURIs[ str_String, wrapper_ ] := catchMine @ StringSplit[ - str, - link: Shortest[ "![" ~~ __ ~~ "](" ~~ __ ~~ ")" ] :> catchAlways @ GetExpressionURI[ link, wrapper ] -]; +GetExpressionURIs[ str_String, wrapper_, opts: OptionsPattern[ ] ] := + catchMine @ Block[ { $uriTooltip = OptionValue @ Tooltip }, + StringSplit[ + str, + link: Shortest[ "![" ~~ __ ~~ "](" ~~ __ ~~ ")" ] :> catchAlways @ GetExpressionURI[ link, wrapper ] + ] + ]; (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*GetExpressionURI*) GetExpressionURI // ClearAll; +GetExpressionURI // Options = { Tooltip -> Automatic }; -GetExpressionURI[ uri_ ] := catchMine @ GetExpressionURI[ uri, ## & ]; -GetExpressionURI[ URL[ uri_ ], wrapper_ ] := catchMine @ GetExpressionURI[ uri, wrapper ]; +GetExpressionURI[ uri_, opts: OptionsPattern[ ] ] := + catchMine @ GetExpressionURI[ uri, ## &, opts ]; -GetExpressionURI[ uri_String, wrapper_ ] := catchMine @ Enclose[ +GetExpressionURI[ URL[ uri_ ], wrapper_, opts: OptionsPattern[ ] ] := + catchMine @ GetExpressionURI[ uri, wrapper, opts ]; + +GetExpressionURI[ uri_String, wrapper_, opts: OptionsPattern[ ] ] := catchMine @ Enclose[ Module[ { held }, - held = ConfirmMatch[ getExpressionURI @ uri, _HoldComplete, "GetExpressionURI" ]; + held = ConfirmMatch[ getExpressionURI[ uri, OptionValue[ Tooltip ] ], _HoldComplete, "GetExpressionURI" ]; wrapper @@ held ], throwInternalFailure[ GetExpressionURI[ uri, wrapper ], ## ] & ]; -GetExpressionURI[ All, wrapper_ ] := catchMine @ Enclose[ +GetExpressionURI[ All, wrapper_, opts: OptionsPattern[ ] ] := catchMine @ Enclose[ Module[ { attachments }, attachments = ConfirmBy[ $attachments, AssociationQ, "Attachments" ]; ConfirmAssert[ AllTrue[ attachments, MatchQ[ _HoldComplete ] ], "HeldAttachments" ]; @@ -1524,26 +1533,31 @@ GetExpressionURI[ All, wrapper_ ] := catchMine @ Enclose[ (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) -(*getExpressionURI*) +(*getExpressionURI0*) getExpressionURI // beginDefinition; +getExpressionURI[ uri_, tooltip_ ] := Block[ { $tooltip = tooltip }, getExpressionURI0 @ uri ]; +getExpressionURI // endDefinition; -getExpressionURI[ str_String ] := + +getExpressionURI0 // beginDefinition; + +getExpressionURI0[ str_String ] := Module[ { split }, split = First[ StringSplit[ str, "![" ~~ alt__ ~~ "](" ~~ url__ ~~ ")" :> { alt, url } ], $Failed ]; - getExpressionURI @@ split /; MatchQ[ split, { _String, _String } ] + getExpressionURI0 @@ split /; MatchQ[ split, { _String, _String } ] ]; -getExpressionURI[ uri_String ] := getExpressionURI[ None, uri ]; +getExpressionURI0[ uri_String ] := getExpressionURI0[ None, uri ]; -getExpressionURI[ tooltip_, uri_String ] := getExpressionURI[ tooltip, uri, URLParse @ uri ]; +getExpressionURI0[ tooltip_, uri_String ] := getExpressionURI0[ tooltip, uri, URLParse @ uri ]; -getExpressionURI[ tooltip_, uri_, as: KeyValuePattern @ { "Scheme" -> $$expressionScheme, "Domain" -> key_ } ] := +getExpressionURI0[ tooltip_, uri_, as: KeyValuePattern @ { "Scheme" -> $$expressionScheme, "Domain" -> key_ } ] := Enclose[ ConfirmMatch[ displayAttachment[ uri, tooltip, key ], _HoldComplete, "DisplayAttachment" ], - throwInternalFailure[ getExpressionURI[ tooltip, uri, as ], ## ] & + throwInternalFailure[ getExpressionURI0[ tooltip, uri, as ], ## ] & ]; -getExpressionURI // endDefinition; +getExpressionURI0 // endDefinition; (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) @@ -1556,7 +1570,11 @@ displayAttachment[ uri_, None, key_ ] := displayAttachment[ uri_, tooltip_String, key_ ] := Enclose[ Replace[ ConfirmMatch[ getAttachment[ uri, key ], _HoldComplete, "GetAttachment" ], - HoldComplete[ expr_ ] :> HoldComplete @ Tooltip[ expr, tooltip ] + HoldComplete[ expr_ ] :> + If[ TrueQ @ $tooltip, + HoldComplete @ Tooltip[ expr, tooltip ], + HoldComplete @ expr + ] ], throwInternalFailure[ displayAttachment[ uri, tooltip, key ], ## ] & ]; @@ -1578,6 +1596,8 @@ getAttachment // endDefinition; (*makeToolResponseString*) makeToolResponseString // beginDefinition; +makeToolResponseString[ failure_Failure ] := makeFailureString @ failure; + makeToolResponseString[ expr_? simpleResultQ ] := With[ { string = fixLineEndings @ TextString @ expr }, If[ StringLength @ string < $toolResultStringLength, diff --git a/Source/Chatbook/Utils.wl b/Source/Chatbook/Utils.wl index 17d57911..1cd71a84 100644 --- a/Source/Chatbook/Utils.wl +++ b/Source/Chatbook/Utils.wl @@ -1,99 +1,27 @@ -(* - This package contains utility functions that are not tied to Chatbook - directly in any way. -*) +(* ::Section::Closed:: *) +(*Package Header*) +BeginPackage[ "Wolfram`Chatbook`Utils`" ]; + +HoldComplete[ + `associationKeyDeflatten; + `convertUTF8; + `fastFileHash; + `fixLineEndings; + `getPinkBoxErrors; + `graphicsQ; + `image2DQ; + `makeFailureString; + `readString; + `stringTrimMiddle; + `validGraphicsQ; +]; -(* cSpell: ignore deflatten *) +Begin[ "`Private`" ]; -BeginPackage["Wolfram`Chatbook`Utils`"] - -`associationKeyDeflatten; -`convertUTF8; -`fixLineEndings; - -CellPrint2 - -FirstMatchingPositionOrder::usage = "FirstMatchingPositionOrder[patterns][a, b] returns an ordering value based on the positions of the first pattern in patterns to match a and b." - -Begin["`Private`"] - -Needs[ "Wolfram`Chatbook`" ]; -Needs[ "Wolfram`Chatbook`Common`" ]; -Needs[ "Wolfram`Chatbook`ErrorUtils`" ]; -Needs[ "Wolfram`Chatbook`UI`" ]; - -(*====================================*) - -SetFallthroughError[CellPrint2] - -(* - Alternative to CellPrint[] where the first argument is an evaluation input - cell, and the new cell will be printed after any existing output cells. - - `CellPrint[arg]` is conceptually `CellPrint2[EvaluationCell[], arg]`. -*) -CellPrint2[ - evalCell_CellObject, - Cell[cellData_, cellStyles___?StringQ, cellOpts___?OptionQ] -] := With[{ - uuid = CreateUUID[] -}, Module[{ - cell = Cell[ - cellData, - cellStyles, - GeneratedCell -> True, - CellAutoOverwrite -> True, - ExpressionUUID -> uuid, - cellOpts - ], - obj -}, - RaiseAssert[ - Experimental`CellExistsQ[evalCell], - "Unable to print cell: evaluation cell does not exist." - ]; - - Wolfram`Chatbook`UI`Private`moveAfterPreviousOutputs[evalCell]; - - RaiseConfirm @ NotebookWrite[ParentNotebook[evalCell], cell]; - - obj = CellObject[uuid]; - - RaiseAssert[ - Experimental`CellExistsQ[obj], - <| "CellExpression" -> cell |>, - "Error printing cell: written cell was not created or could not be found." - ]; - - obj -]] - -(*====================================*) - -FirstMatchingPositionOrder[patterns_?ListQ][a_, b_] := Module[{ - aPos, - bPos -}, - aPos = FirstPosition[patterns, _?(patt |-> MatchQ[a, patt]), None, {1}]; - bPos = FirstPosition[patterns, _?(patt |-> MatchQ[b, patt]), None, {1}]; - - Replace[{aPos, bPos}, { - (* If neither `a` nor `b` match, then they are already in order. *) - {None, None} -> True, - (* If only `a` matches, it's already in order. *) - {Except[None], None} -> True, - (* If only `b` matches, it should come earlier. *) - {None, Except[None]} -> -1, - (* If both `a` and `b` match, sort based on the position of the matched pattern. *) - {{aIdx_?IntegerQ}, {bIdx_?IntegerQ}} :> Order[aIdx, bIdx], - other_ :> FailureMessage[ - FirstMatchingPositionOrder::unexpected, - "Unexpected position values: ``", - {other} - ] - }] -] +Needs[ "Wolfram`Chatbook`" ]; +Needs[ "Wolfram`Chatbook`Common`" ]; +(* cSpell: ignore deflatten *) (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) (*AssociationKeyDeflatten*) @@ -102,13 +30,17 @@ importResourceFunction[ associationKeyDeflatten, "AssociationKeyDeflatten" ]; (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) +(*Strings*) + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) (*fixLineEndings*) fixLineEndings // beginDefinition; fixLineEndings[ string_String? StringQ ] := StringReplace[ string, "\r\n" -> "\n" ]; fixLineEndings // endDefinition; (* ::**************************************************************************************************************:: *) -(* ::Section::Closed:: *) +(* ::Subsection::Closed:: *) (*convertUTF8*) convertUTF8 // beginDefinition; convertUTF8[ string_String ] := convertUTF8[ string, True ]; @@ -116,6 +48,170 @@ convertUTF8[ string_String, True ] := FromCharacterCode[ ToCharacterCode @ stri convertUTF8[ string_String, False ] := FromCharacterCode @ ToCharacterCode[ string, "UTF-8" ]; convertUTF8 // endDefinition; +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*stringTrimMiddle*) +stringTrimMiddle // beginDefinition; +stringTrimMiddle[ str_String, Infinity ] := str; +stringTrimMiddle[ str_String, max_Integer? Positive ] := stringTrimMiddle[ str, Ceiling[ max / 2 ], Floor[ max / 2 ] ]; +stringTrimMiddle[ str_String, l_Integer, r_Integer ] /; StringLength @ str <= l + r + 5 := str; +stringTrimMiddle[ str_String, l_Integer, r_Integer ] := StringTake[ str, l ] <> " ... " <> StringTake[ str, -r ]; +stringTrimMiddle // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*makeFailureString*) +makeFailureString // beginDefinition; + +makeFailureString[ failure: Failure[ tag_, as_Association ] ] := Enclose[ + Module[ { message }, + message = ToString @ ConfirmBy[ failure[ "Message" ], StringQ, "Message" ]; + StringJoin[ + "Failure[", + ToString[ tag, InputForm ], + ", ", + StringReplace[ + ToString[ as, InputForm ], + StartOfString~~"<|" -> "<|" <> ToString[ "Message" -> message, InputForm ] <> ", " + ], + "]" + ] + ], + throwInternalFailure[ makeFailureString @ failure, ##1 ] & +]; + +makeFailureString // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Section::Closed:: *) +(*Files*) + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*readString*) +readString // beginDefinition; + +readString[ file_ ] := Quiet[ readString[ file, ReadByteArray @ file ], $CharacterEncoding::utf8 ]; + +readString[ file_, bytes_? ByteArrayQ ] := readString[ file, ByteArrayToString @ bytes ]; +readString[ file_, string_? StringQ ] := fixLineEndings @ string; +readString[ file_, failure_Failure ] := failure; + +readString[ file_, $Failed ] := + Module[ { exists, tag, template }, + exists = TrueQ @ FileExistsQ @ file; + tag = If[ exists, "FileUnreadable", "FileNotFound" ]; + template = If[ exists, "Cannot read content from `1`.", "The file `1` does not exist." ]; + Failure[ tag, <| "MessageTemplate" -> template, "MessageParameters" -> { file } |> ] + ]; + +readString // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*unreadableFileFailure*) +unreadableFileFailure // beginDefinition; + +unreadableFileFailure[ file_ ] := + Failure[ + "FileUnreadable", + <| + "MessageTemplate" -> "Cannot read content from `1`.", + "MessageParameters" -> { file } + |> + ]; + +unreadableFileFailure // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*fastFileHash*) +fastFileHash // beginDefinition; +fastFileHash[ file_ ] := fastFileHash[ file, ReadByteArray @ file ]; +fastFileHash[ file_, bytes_ByteArray ] := Hash @ bytes; +fastFileHash // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Section::Closed:: *) +(*Graphics*) +$$graphics = HoldPattern @ Alternatives[ + _System`AstroGraphics, + _GeoGraphics, + _Graphics, + _Graphics3D, + _Image, + _Image3D, + _Legended +]; + +$$definitelyNotGraphics = HoldPattern @ Alternatives[ + _Association, + _CloudObject, + _File, + _List, + _String, + _URL, + Null, + True|False +]; + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*graphicsQ*) +graphicsQ[ $$graphics ] := True; +graphicsQ[ $$definitelyNotGraphics ] := False; +graphicsQ[ g_ ] := MatchQ[ Quiet @ Show @ Unevaluated @ g, $$graphics ]; +graphicsQ[ ___ ] := False; + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*validGraphicsQ*) +validGraphicsQ[ g_? graphicsQ ] := getPinkBoxErrors @ Unevaluated @ g === { }; +validGraphicsQ[ ___ ] := False; + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*getPinkBoxErrors*) +getPinkBoxErrors // beginDefinition; +(* TODO: hook this up to evaluator outputs and CellToString to give feedback about pink boxes *) + +getPinkBoxErrors[ { } ] := + { }; + +getPinkBoxErrors[ cells: _CellObject | { __CellObject } ] := + getPinkBoxErrors @ NotebookRead @ cells; + +getPinkBoxErrors[ cells: _Cell | { __Cell } ] := + Module[ { nbo }, + UsingFrontEnd @ WithCleanup[ + nbo = NotebookPut[ Notebook @ Flatten @ { cells }, Visible -> False ], + SelectionMove[ nbo, All, Notebook ]; + MathLink`CallFrontEnd @ FrontEnd`GetErrorsInSelectionPacket @ nbo, + NotebookClose @ nbo + ] + ]; + +getPinkBoxErrors[ data: _TextData | _BoxData | { __BoxData } ] := + getPinkBoxErrors @ Cell @ data; + +getPinkBoxErrors[ exprs_List ] := + getPinkBoxErrors[ Cell @* BoxData /@ MakeBoxes /@ Unevaluated @ exprs ]; + +getPinkBoxErrors[ expr_ ] := + getPinkBoxErrors @ { Cell @ BoxData @ MakeBoxes @ expr }; + +getPinkBoxErrors // endDefinition; + + +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*image2DQ*) +(* Matches against the head in addition to checking ImageQ to avoid passing Image3D when a 2D image is expected: *) +image2DQ // beginDefinition; +image2DQ[ _Image? ImageQ ] := True; +image2DQ[ _ ] := False; +image2DQ // endDefinition; + (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) (*Package Footer*)