@@ -12,12 +12,13 @@ Needs[ "Wolfram`Chatbook`" -> "cb`" ];
1212(* ::**************************************************************************************************************:: *)
1313(* ::Section::Closed:: *)
1414(*Configuration*)
15- $protocolVersion = "2024-11-05" ;
16- $toolWarmupDelay = 5 ; (* seconds *)
17- $clientName = None ;
18- $clientSupportsUI = False ;
19- $currentMCPServer = None ;
20- $mcpEvaluation = False ;
15+ $protocolVersion = "2024-11-05" ;
16+ $toolWarmupDelay = 5 ; (* seconds *)
17+ $waImageFetchTimeout = 5 ; (* seconds, applied to the whole WA image batch via TaskWait *)
18+ $clientName = None ;
19+ $clientSupportsUI = False ;
20+ $currentMCPServer = None ;
21+ $mcpEvaluation = False ;
2122
2223$logTimeStamp := DateString [
2324 {
@@ -726,6 +727,28 @@ graphicsToImageContent[ g_ ] := Enclose[
726727
727728graphicsToImageContent // endDefinition ;
728729
730+ (* ::**************************************************************************************************************:: *)
731+ (* ::Subsubsection::Closed:: *)
732+ (*makeImageContent*)
733+ makeImageContent // beginDefinition ;
734+
735+ makeImageContent [
736+ URL [ url_ String ],
737+ KeyValuePattern @ {
738+ "StatusCode" -> 200 ,
739+ "BodyByteArray" -> bytes_ ByteArray ,
740+ "Headers" -> KeyValuePattern [ "content-type" -> type_ String ? (StringStartsQ [ "image/" ]) ]
741+ }
742+ ] := {
743+ < | "type" -> "text" , "text" -> "" |> ,
744+ < | "type" -> "image" , "data" -> BaseEncode @ bytes , "mimeType" -> type |>
745+ };
746+
747+ makeImageContent [ URL [ url_ String ], _ ] :=
748+ { < | "type" -> "text" , "text" -> "" |> };
749+
750+ makeImageContent // endDefinition ;
751+
729752(* ::**************************************************************************************************************:: *)
730753(* ::Subsubsection::Closed:: *)
731754(*extractWolframAlphaImages*)
@@ -734,7 +757,7 @@ graphicsToImageContent // endDefinition;
734757(* Matches: public6.wolframalpha.com, www6.wolframalpha.com, etc. *)
735758$$waImageURLPattern = Shortest [
736759 "![" ~~ Except [ "]" ]... ~~ "](" ~~
737- url : ("https://" ~~ __ ~~ "wolframalpha.com/files/" ~~ __ ~~ (".gif" | ".png" | ".jpg" | ".jpeg" )) ~~
760+ url : ("https://" ~~ __ ~~ "wolframalpha.com/files/" ~~ __ ~~ (".gif" | ".png" | ".jpg" | ".jpeg" | ".webp" | ".svg " )) ~~
738761 ")"
739762];
740763
@@ -744,44 +767,50 @@ extractWolframAlphaImages // beginDefinition;
744767extractWolframAlphaImages [ str_ String ] /; ! $mcpEvaluation := str ;
745768
746769extractWolframAlphaImages [ str_ String ] := Enclose [
747- Catch @ Module [ { parts , hasImages , contentItems },
770+ Catch @ Module [ { parts , urls , fetched , tasks , replaced , contentItems },
771+
772+ (* Split string into text segments and URL[..] tokens *)
773+ parts = StringSplit [ str , $$waImageURLPattern :> URL [ url ] ];
774+ urls = Cases [ parts , _ URL ];
775+
776+ (* If no images found, return plain text for backward compatibility *)
777+ If [ urls === { }, Throw @ str ];
778+
779+ (* Pre-fill every URL with a text-only fallback so a timeout still yields the markdown link *)
780+ fetched = AssociationMap [ < | "type" -> "text" , "text" -> "" |> & , urls ];
781+
782+ (* Submit all URLs concurrently; each handler overwrites its slot in `fetched` on success.
783+ The outer Function captures the URL in a closure so each handler knows its own key. *)
784+ tasks = Function [ u ,
785+ URLSubmit [
786+ u ,
787+ HandlerFunctions -> < | "BodyReceived" -> Function [ fetched [ u ] = makeImageContent [ u , # ] ] |> ,
788+ HandlerFunctionsKeys -> { "StatusCode" , "BodyByteArray" , "Headers" }
789+ ]
790+ ] /@ urls ;
748791
749- (* Split string into text segments and URLs *)
750- parts = StringSplit [ str , $$waImageURLPattern :> url ];
792+ (* Bound the whole batch, not each request *)
793+ TaskWait [ tasks , TimeConstraint -> $waImageFetchTimeout ];
794+ Quiet [ TaskRemove /@ tasks ];
751795
752- (* If no images found, return plain text *)
753- If [ Length @ parts === 1 && StringQ @ First @ parts ,
754- Throw @ str (* Return plain string for backward compatibility *)
796+ replaced = Flatten @ Replace [
797+ parts ,
798+ {
799+ "" :> Nothing ,
800+ s_ String :> < | "type" -> "text" , "text" -> s |> ,
801+ u_ URL :> fetched [ u ]
802+ },
803+ { 1 }
755804 ];
756805
757- hasImages = False ;
758- contentItems = Flatten @ Map [
759- Function [ item ,
760- If [ StringQ @ item && ! StringStartsQ [ item , "https://" ],
761- (* Text segment: create text content *)
762- If [ StringLength @ item > 0 ,
763- { < | "type" -> "text" , "text" -> item |> },
764- { }
765- ],
766- (* URL: import image and create both text + image content *)
767- hasImages = True ;
768- Module [ { img , imageContent },
769- img = Quiet @ TimeConstrained [ Import [ item , "Image" ], 5 , $Failed ];
770- imageContent = If [ ImageQ @ img , graphicsToImageContent @ img , $Failed ];
771- Flatten @ {
772- (* Always include the markdown link as text *)
773- < | "type" -> "text" , "text" -> "" |> ,
774- (* Add base64 image if import succeeded *)
775- If [ AssociationQ @ imageContent , imageContent , Nothing ]
776- }
777- ]
778- ]
779- ],
780- parts
806+ (* Merge runs of adjacent text items into one *)
807+ contentItems = SequenceReplace [
808+ replaced ,
809+ { as : KeyValuePattern [ "type" -> "text" ].. } :>
810+ < | "type" -> "text" , "text" -> StringJoin @ Lookup [ { as }, "text" ] |>
781811 ];
782812
783- (* If we successfully extracted images, return structured content *)
784- If [ TrueQ @ hasImages && MatchQ [ contentItems , { __ Association } ],
813+ If [ MatchQ [ contentItems , { __ Association } ],
785814 < | "Content" -> contentItems |> ,
786815 str (* Fallback to plain string *)
787816 ]
0 commit comments