diff --git a/Manifest.toml b/Manifest.toml index bc87211..92b7182 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,8 +1,32 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.11.4" +julia_version = "1.12.6" manifest_format = "2.0" -project_hash = "cb7f3c57318e927e8ac4dc2dea9acdcace566ed1" +project_hash = "5b5e1c071ff66b72aeed7d8a4316829e2407ae1a" + +[[deps.Accessors]] +deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"] +git-tree-sha1 = "7063ad1083578215c7c4bf410368150abe8d5524" +uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" +version = "0.1.45" + + [deps.Accessors.extensions] + AxisKeysExt = "AxisKeys" + IntervalSetsExt = "IntervalSets" + LinearAlgebraExt = "LinearAlgebra" + StaticArraysExt = "StaticArrays" + StructArraysExt = "StructArrays" + TestExt = "Test" + UnitfulExt = "Unitful" + + [deps.Accessors.weakdeps] + AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5" + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [[deps.AliasTables]] deps = ["PtrArrays", "Random"] @@ -14,6 +38,18 @@ version = "1.1.3" uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" version = "1.1.2" +[[deps.Arrow]] +deps = ["ArrowTypes", "BitIntegers", "CodecLz4", "CodecZstd", "ConcurrentUtilities", "DataAPI", "Dates", "EnumX", "Mmap", "PooledArrays", "SentinelArrays", "StringViews", "Tables", "TimeZones", "TranscodingStreams", "UUIDs"] +git-tree-sha1 = "4a69a3eadc1f7da78d950d1ef270c3a62c1f7e01" +uuid = "69666777-d1a9-59fb-9406-91d4454c9d45" +version = "2.8.1" + +[[deps.ArrowTypes]] +deps = ["Sockets", "UUIDs"] +git-tree-sha1 = "404265cd8128a2515a81d5eae16de90fdef05101" +uuid = "31f734f8-188a-4ce0-8406-c8a06bd891cd" +version = "2.3.0" + [[deps.Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" version = "1.11.0" @@ -22,54 +58,116 @@ version = "1.11.0" uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" version = "1.11.0" -[[deps.BitFlags]] -git-tree-sha1 = "0691e34b3bb8be9307330f88d1a3c3f25466c24d" -uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35" -version = "0.1.9" +[[deps.BitIntegers]] +deps = ["Random"] +git-tree-sha1 = "091d591a060e43df1dd35faab3ca284925c48e46" +uuid = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1" +version = "0.3.7" + +[[deps.BufferedStreams]] +git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643" +uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d" +version = "1.2.2" [[deps.CEnum]] git-tree-sha1 = "389ad5c84de1ae7cf0e28e381131c98ea87d54fc" uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" version = "0.5.0" +[[deps.CRC32c]] +uuid = "8bf52ea8-c179-5cab-976a-9e18b702a9bc" +version = "1.11.0" + [[deps.CSV]] deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"] -git-tree-sha1 = "deddd8725e5e1cc49ee205a1964256043720a6c3" +git-tree-sha1 = "8d8e0b0f350b8e1c91420b5e64e5de774c2f0f4d" uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -version = "0.10.15" +version = "0.10.16" [[deps.CodeTracking]] -deps = ["InteractiveUtils", "UUIDs"] -git-tree-sha1 = "7eee164f122511d3e4e1ebadb7956939ea7e1c77" +deps = ["InteractiveUtils", "REPL", "UUIDs"] +git-tree-sha1 = "cfb7a2e89e245a9d5016b70323db412b3a7438d5" uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" -version = "1.3.6" +version = "3.0.2" + +[[deps.CodecBase]] +deps = ["TranscodingStreams"] +git-tree-sha1 = "40956acdbef3d8c7cc38cba42b56034af8f8581a" +uuid = "6c391c72-fb7b-5838-ba82-7cfb1bcfecbf" +version = "0.3.4" + +[[deps.CodecLz4]] +deps = ["Lz4_jll", "TranscodingStreams"] +git-tree-sha1 = "d58afcd2833601636b48ee8cbeb2edcb086522c2" +uuid = "5ba52731-8f18-5e0d-9241-30f10d1ec561" +version = "0.4.6" [[deps.CodecZlib]] deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "bce6804e5e6044c6daab27bb533d1295e4a2e759" +git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.6" +version = "0.7.8" + +[[deps.CodecZstd]] +deps = ["TranscodingStreams", "Zstd_jll"] +git-tree-sha1 = "da54a6cd93c54950c15adf1d336cfd7d71f51a56" +uuid = "6b39b394-51ab-5f42-8807-6242bab2b4c2" +version = "0.8.7" + +[[deps.CommonSolve]] +git-tree-sha1 = "99ee296f88c12485402e37c2fd025f95ae097637" +uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +version = "0.2.9" [[deps.Compat]] deps = ["TOML", "UUIDs"] -git-tree-sha1 = "8ae8d32e09f0dcf42a36b90d4e17f5dd2e4c4215" +git-tree-sha1 = "9d8a54ce4b17aa5bdce0ea5c34bc5e7c340d16ad" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "4.16.0" +version = "4.18.1" weakdeps = ["Dates", "LinearAlgebra"] [deps.Compat.extensions] CompatLinearAlgebraExt = "LinearAlgebra" +[[deps.Compiler]] +git-tree-sha1 = "382d79bfe72a406294faca39ef0c3cef6e6ce1f1" +uuid = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" +version = "0.1.1" + [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.1.1+0" +version = "1.3.0+1" + +[[deps.CompositionsBase]] +git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" +uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b" +version = "0.1.2" +weakdeps = ["InverseFunctions"] + + [deps.CompositionsBase.extensions] + CompositionsBaseInverseFunctionsExt = "InverseFunctions" [[deps.ConcurrentUtilities]] deps = ["Serialization", "Sockets"] -git-tree-sha1 = "ea32b83ca4fefa1768dc84e504cc0a94fb1ab8d1" +git-tree-sha1 = "21d088c496ea22914fe80906eb5bce65755e5ec8" uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" -version = "2.4.2" +version = "2.5.1" + +[[deps.ConstructionBase]] +git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb" +uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +version = "1.6.0" + + [deps.ConstructionBase.extensions] + ConstructionBaseIntervalSetsExt = "IntervalSets" + ConstructionBaseLinearAlgebraExt = "LinearAlgebra" + ConstructionBaseStaticArraysExt = "StaticArrays" + + [deps.ConstructionBase.weakdeps] + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [[deps.Crayons]] git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" @@ -88,15 +186,15 @@ version = "1.16.0" [[deps.DataFrames]] deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"] -git-tree-sha1 = "fb61b4812c49343d7ef0b533ba982c46021938a6" +git-tree-sha1 = "5fab31e2e01e70ad66e3e24c968c264d1cf166d6" uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -version = "1.7.0" +version = "1.8.2" [[deps.DataStructures]] -deps = ["Compat", "InteractiveUtils", "OrderedCollections"] -git-tree-sha1 = "1d0a14036acb104d9e89698bd408f63ab58cdc82" +deps = ["OrderedCollections"] +git-tree-sha1 = "6fb53a69613a0b2b68a0d12671717d307ab8b24e" uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.18.20" +version = "0.19.5" [[deps.DataValueInterfaces]] git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" @@ -119,37 +217,37 @@ uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" version = "1.11.0" [[deps.Distributions]] -deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] -git-tree-sha1 = "0b4190661e8a4e51a842070e7dd4fae440ddb7f4" +deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] +git-tree-sha1 = "cd3c5ac74cd3923c8945c6a81518c46abd0e73a3" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" -version = "0.25.118" +version = "0.25.129" [deps.Distributions.extensions] DistributionsChainRulesCoreExt = "ChainRulesCore" DistributionsDensityInterfaceExt = "DensityInterface" + DistributionsSparseConnectivityTracerExt = "SparseConnectivityTracer" DistributionsTestExt = "Test" [deps.Distributions.weakdeps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" + SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [[deps.DocStringExtensions]] -deps = ["LibGit2"] -git-tree-sha1 = "2fb1e02f2b635d0845df5d7c167fec4dd739b00d" +git-tree-sha1 = "7442a5dfe1ebb773c29cc2962a8980f47221d76c" uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.9.3" +version = "0.9.5" [[deps.Downloads]] deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" +version = "1.7.0" -[[deps.ExceptionUnwrapping]] -deps = ["Test"] -git-tree-sha1 = "d36f682e590a83d63d1c7dbd287573764682d12a" -uuid = "460bff9d-24e4-43bc-9d9f-a8973cb893f4" -version = "0.1.11" +[[deps.EnumX]] +git-tree-sha1 = "c49898e8438c828577f04b92fc9368c388ac783c" +uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" +version = "1.0.7" [[deps.ExprTools]] git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" @@ -158,13 +256,9 @@ version = "0.1.10" [[deps.FileIO]] deps = ["Pkg", "Requires", "UUIDs"] -git-tree-sha1 = "b66970a70db13f45b7e57fbda1736e1cf72174ea" +git-tree-sha1 = "91e0e5c68d02bcdaae76d3c8ceb4361e8f28d2e9" uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -version = "1.17.0" -weakdeps = ["HTTP"] - - [deps.FileIO.extensions] - HTTPExt = "HTTP" +version = "1.16.5" [[deps.FilePathsBase]] deps = ["Compat", "Dates"] @@ -183,44 +277,62 @@ version = "1.11.0" [[deps.FillArrays]] deps = ["LinearAlgebra"] -git-tree-sha1 = "6a70198746448456524cb442b8af316927ff3e1a" +git-tree-sha1 = "2f979084d1e13948a3352cf64a25df6bd3b4dca3" uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" -version = "1.13.0" -weakdeps = ["PDMats", "SparseArrays", "Statistics"] +version = "1.16.0" [deps.FillArrays.extensions] FillArraysPDMatsExt = "PDMats" FillArraysSparseArraysExt = "SparseArrays" + FillArraysStaticArraysExt = "StaticArrays" FillArraysStatisticsExt = "Statistics" + [deps.FillArrays.weakdeps] + PDMats = "90014a1f-27ba-587c-ab20-58faa44d9150" + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + [[deps.Future]] deps = ["Random"] uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" version = "1.11.0" +[[deps.Gamma]] +git-tree-sha1 = "86f86b6168a016ed88e4ae4e64577b98c3b59e8e" +uuid = "a0844989-3bd2-4988-8bea-c9407ab0941b" +version = "1.1.0" + [[deps.GeneralUtils]] -deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "JSON3", "MQTTClient", "PrettyPrinting", "Random", "SHA", "UUIDs"] -path = "/appfolder/app/dev/GeneralUtils" +deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "HTTP", "JSON", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "UUIDs"] +git-tree-sha1 = "7c0600c166a5deb2c607018a491c04eb25969c2e" +repo-rev = "main" +repo-url = "https://git.yiem.cc/ton/GeneralUtils" uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe" -version = "0.2.3" +version = "0.4.9" [[deps.HTTP]] -deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "PrecompileTools", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] -git-tree-sha1 = "6c22309e9a356ac1ebc5c8a217045f9bae6f8d9a" +deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"] +git-tree-sha1 = "eda1d37cb55d90a17d0957c75841138c88b361a1" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "1.10.13" +version = "2.5.4" + +[[deps.HashArrayMappedTries]] +git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" +uuid = "076d061b-32b6-4027-95e0-9a2c6f6d7e74" +version = "0.2.0" [[deps.HypergeometricFunctions]] -deps = ["LinearAlgebra", "OpenLibm_jll", "SpecialFunctions"] -git-tree-sha1 = "68c173f4f449de5b438ee67ed0c9c748dc31a2ec" +deps = ["Gamma", "LinearAlgebra"] +git-tree-sha1 = "18d7deab5fb0440dc6a7b6993c5c27b25420de10" uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" -version = "0.3.28" +version = "0.3.29" [[deps.ICU_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "20b6765a3016e1fca0c9c93c80d50061b94218b7" +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "b3d8be712fbf9237935bde0ce9b5a736ae38fc34" uuid = "a51ab1cf-af8e-5615-a023-bc2c838bba6b" -version = "69.1.0+0" +version = "76.2.0+0" [[deps.Infinity]] deps = ["Dates", "Random", "Requires"] @@ -229,38 +341,45 @@ uuid = "a303e19e-6eb4-11e9-3b09-cd9505f79100" version = "0.2.4" [[deps.InlineStrings]] -git-tree-sha1 = "45521d31238e87ee9f9732561bfee12d4eebd52d" +git-tree-sha1 = "8f3d257792a522b4601c24a577954b0a8cd7334d" uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" -version = "1.4.2" +version = "1.4.5" +weakdeps = ["ArrowTypes", "Parsers"] [deps.InlineStrings.extensions] ArrowTypesExt = "ArrowTypes" ParsersExt = "Parsers" - [deps.InlineStrings.weakdeps] - ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" - Parsers = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" - [[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" version = "1.11.0" [[deps.Intervals]] -deps = ["Dates", "Printf", "RecipesBase", "Serialization", "TimeZones"] -git-tree-sha1 = "ac0aaa807ed5eaf13f67afe188ebc07e828ff640" +deps = ["ArrowTypes", "Dates", "Printf", "RecipesBase", "Serialization", "TimeZones"] +git-tree-sha1 = "d6fe00b123e32ddd17231b35d69a6394e696fd5a" uuid = "d8418881-c3e1-53bb-8760-2df7ec849ed5" -version = "1.10.0" +version = "1.11.0" + +[[deps.InverseFunctions]] +git-tree-sha1 = "a779299d77cd080bf77b97535acecd73e1c5e5cb" +uuid = "3587e190-3f89-42d0-90ee-14403ec27112" +version = "0.1.17" +weakdeps = ["Dates", "Test"] + + [deps.InverseFunctions.extensions] + InverseFunctionsDatesExt = "Dates" + InverseFunctionsTestExt = "Test" [[deps.InvertedIndices]] -git-tree-sha1 = "0dc7b50b8d436461be01300fd8cd45aa0274b038" +git-tree-sha1 = "6da3c4316095de0f5ee2ebd875df8721e7e0bdbe" uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" -version = "1.3.0" +version = "1.3.1" [[deps.IrrationalConstants]] -git-tree-sha1 = "e2222959fbc6c19554dc15174c81bf7bf3aa691c" +git-tree-sha1 = "b2d91fe939cae05960e760110b328288867b5758" uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" -version = "0.2.4" +version = "0.2.6" [[deps.IterTools]] git-tree-sha1 = "42d5f897009e7ff2cf88db414a389e5ed1bdd023" @@ -274,39 +393,52 @@ version = "1.0.0" [[deps.JLLWrappers]] deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "be3dc50a92e5a386872a493a10050136d4703f9b" +git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.8.0" + +[[deps.JSON]] +deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] +git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" version = "1.6.1" +weakdeps = ["ArrowTypes"] + + [deps.JSON.extensions] + JSONArrowExt = ["ArrowTypes"] [[deps.JSON3]] deps = ["Dates", "Mmap", "Parsers", "PrecompileTools", "StructTypes", "UUIDs"] -git-tree-sha1 = "1d322381ef7b087548321d3f878cb4c9bd8f8f9b" +git-tree-sha1 = "411eccfe8aba0814ffa0fdf4860913ed09c34975" uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" -version = "1.14.1" +version = "1.14.3" +weakdeps = ["ArrowTypes"] [deps.JSON3.extensions] JSON3ArrowExt = ["ArrowTypes"] - [deps.JSON3.weakdeps] - ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" - [[deps.JuliaInterpreter]] deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] -git-tree-sha1 = "10da5154188682e5c0726823c2b5125957ec3778" +git-tree-sha1 = "58927c485919bf17ea308d9d82156de1adf4b006" uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" -version = "0.9.38" +version = "0.10.12" + +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" [[deps.Kerberos_krb5_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "60274b4ab38e8d1248216fe6b6ace75ae09b0502" +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "0f2899fdadaab4b8f57db558ba21bdb4fb52f1f0" uuid = "b39eb1a6-c29a-53d7-8c32-632cd16f18da" -version = "1.19.3+0" +version = "1.21.3+0" [[deps.LLMMCTS]] -deps = ["GeneralUtils", "JSON3", "PrettyPrinting"] -path = "/appfolder/app/dev/LLMMCTS" +deps = ["GeneralUtils", "JSON", "PrettyPrinting"] +path = "../LLMMCTS" uuid = "d76c5a4d-449e-4835-8cc4-dd86ec44f241" -version = "0.1.4" +version = "0.1.5" [[deps.LaTeXStrings]] git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c" @@ -324,19 +456,19 @@ uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" version = "0.6.4" [[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.6.0+0" +version = "8.15.0+0" [[deps.LibGit2]] -deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" version = "1.11.0" [[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.7.2+0" +version = "1.9.0+0" [[deps.LibPQ]] deps = ["CEnum", "DBInterface", "Dates", "Decimals", "DocStringExtensions", "FileWatching", "Infinity", "Intervals", "IterTools", "LayerDicts", "LibPQ_jll", "Libdl", "Memento", "OffsetArrays", "SQLStrings", "Tables", "TimeZones", "UTCDateTimes"] @@ -346,14 +478,14 @@ version = "1.18.0" [[deps.LibPQ_jll]] deps = ["Artifacts", "ICU_jll", "JLLWrappers", "Kerberos_krb5_jll", "Libdl", "OpenSSL_jll", "Zstd_jll"] -git-tree-sha1 = "09163f837936c8cc44f4691cb41d805eb1769642" +git-tree-sha1 = "c692057e05ba6da348bc45d5dab8c7a2c88da518" uuid = "08be9ffa-1c94-5ee5-a977-46a84ec9b350" -version = "16.0.0+0" +version = "16.14.0+0" [[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.0+1" +version = "1.11.3+1" [[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" @@ -362,13 +494,13 @@ version = "1.11.0" [[deps.LinearAlgebra]] deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -version = "1.11.0" +version = "1.12.0" [[deps.LogExpFunctions]] deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "13ca9e2586b89836fd20cccf56e57e2b9ae7f38f" +git-tree-sha1 = "bba2d9aa057d8f126415de240573e86a8f39d2a1" uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "0.3.29" +version = "1.0.1" [deps.LogExpFunctions.extensions] LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" @@ -384,49 +516,45 @@ version = "0.3.29" uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" version = "1.11.0" -[[deps.LoggingExtras]] -deps = ["Dates", "Logging"] -git-tree-sha1 = "f02b56007b064fbfddb4c9cd60161b6dd0f40df3" -uuid = "e6f89c97-d47a-5376-807f-9c37f3926c36" -version = "1.1.0" - [[deps.LoweredCodeUtils]] -deps = ["JuliaInterpreter"] -git-tree-sha1 = "688d6d9e098109051ae33d126fcfc88c4ce4a021" +deps = ["CodeTracking", "Compiler", "JuliaInterpreter"] +git-tree-sha1 = "3733419e9a71156b389f3e331672d2e95436783f" uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" -version = "3.1.0" +version = "3.6.2" -[[deps.MQTTClient]] -deps = ["Distributed", "Random", "Sockets"] -git-tree-sha1 = "f2597b290d4bf17b577346153cd2ddf9accb5c26" -uuid = "985f35cc-2c3d-4943-b8c1-f0931d5f0959" -version = "0.3.1" -weakdeps = ["PrecompileTools"] +[[deps.Lz4_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "191686b1ac1ea9c89fc52e996ad15d1d241d1e33" +uuid = "5ced341a-0733-55b8-9ab6-a4889d929147" +version = "1.10.1+0" - [deps.MQTTClient.extensions] - PrecompileMQTT = "PrecompileTools" +[[deps.MacroTools]] +git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.16" [[deps.Markdown]] -deps = ["Base64"] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" version = "1.11.0" [[deps.MbedTLS]] deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] -git-tree-sha1 = "c067a280ddc25f196b5e7df3877c6b226d390aaf" +git-tree-sha1 = "8785729fa736197687541f7053f6d8ab7fc44f92" uuid = "739be429-bea8-5141-9913-cc70e7f3736d" -version = "1.1.9" +version = "1.1.10" [[deps.MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "ff69a2b1330bcb730b9ac1ab7dd680176f5896b8" uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.6+0" +version = "2.28.1010+0" [[deps.Memento]] deps = ["Dates", "Distributed", "Requires", "Serialization", "Sockets", "Test", "UUIDs"] -git-tree-sha1 = "bb2e8f4d9f400f6e90d57b34860f6abdc51398e5" +git-tree-sha1 = "e03a25cb3b6569623f8246d3d8b3faa7ce86f4ad" uuid = "f28f55f0-a522-5efc-85c2-fe41dfb9b2d9" -version = "1.4.1" +version = "1.5.0" [[deps.Missings]] deps = ["DataAPI"] @@ -446,16 +574,28 @@ version = "0.8.1" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2023.12.12" +version = "2025.11.4" + +[[deps.NATS]] +deps = ["Base64", "BufferedStreams", "CodecBase", "Dates", "DocStringExtensions", "JSON3", "MbedTLS", "NanoDates", "Random", "ScopedValues", "Sockets", "Sodium", "StructTypes", "URIs"] +git-tree-sha1 = "a1cdf34ba90ee5cd2658e487d3277ffafee712ce" +uuid = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a" +version = "0.1.1" + +[[deps.NanoDates]] +deps = ["Dates", "Parsers"] +git-tree-sha1 = "850a0557ae5934f6e67ac0dc5ca13d0328422d1f" +uuid = "46f1a544-deae-4307-8689-c12aa3c955c6" +version = "1.0.3" [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.2.0" +version = "1.3.0" [[deps.OffsetArrays]] -git-tree-sha1 = "39d000d9c33706b8364817d8894fae1548f40295" +git-tree-sha1 = "117432e406b5c023f665fa73dc26e79ec3630151" uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -version = "1.14.2" +version = "1.17.0" [deps.OffsetArrays.extensions] OffsetArraysAdaptExt = "Adapt" @@ -466,52 +606,49 @@ version = "1.14.2" [[deps.OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.27+1" +version = "0.3.29+0" [[deps.OpenLibm_jll]] deps = ["Artifacts", "Libdl"] uuid = "05823500-19ac-5b8b-9628-191a04bc5112" -version = "0.8.1+4" - -[[deps.OpenSSL]] -deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] -git-tree-sha1 = "38cb508d080d21dc1128f7fb04f20387ed4c0af4" -uuid = "4d8831e6-92b7-49fb-bdf8-b643e874388c" -version = "1.4.3" +version = "0.8.7+0" [[deps.OpenSSL_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "7493f61f55a6cce7325f197443aa80d32554ba10" +deps = ["Artifacts", "Libdl"] uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.0.15+1" +version = "3.5.4+0" [[deps.OpenSpecFun_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1" +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1346c9208249809840c91b26703912dff463d335" uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" -version = "0.5.5+2" +version = "0.5.6+0" [[deps.OrderedCollections]] -git-tree-sha1 = "12f1439c4f986bb868acda6ea33ebc78e19b95ad" +git-tree-sha1 = "94ba93778373a53bfd5a0caaf7d809c445292ff4" uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.7.0" +version = "1.8.2" [[deps.PDMats]] deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] -git-tree-sha1 = "48566789a6d5f6492688279e22445002d171cf76" +git-tree-sha1 = "26766d4b5f1a410c218a19b85a672c6edb693c65" uuid = "90014a1f-27ba-587c-ab20-58faa44d9150" -version = "0.11.33" +version = "0.11.40" +weakdeps = ["StatsBase"] + + [deps.PDMats.extensions] + StatsBaseExt = "StatsBase" [[deps.Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821" +git-tree-sha1 = "32a4e09c5f29402573d673901778a0e03b0807b9" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.1" +version = "2.8.6" [[deps.Pkg]] deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.11.0" +version = "1.12.1" weakdeps = ["REPL"] [deps.Pkg.extensions] @@ -525,15 +662,15 @@ version = "1.4.3" [[deps.PrecompileTools]] deps = ["Preferences"] -git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" +git-tree-sha1 = "edbeefc7a4889f528644251bdb5fc9ab5348bc2c" uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.2.1" +version = "1.3.4" [[deps.Preferences]] deps = ["TOML"] -git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" +git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.4.3" +version = "1.5.2" [[deps.PrettyPrinting]] git-tree-sha1 = "142ee93724a9c5d04d78df7006670a93ed1b244e" @@ -541,10 +678,16 @@ uuid = "54e16d92-306c-5ea0-a30b-337be88ac337" version = "0.4.2" [[deps.PrettyTables]] -deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "Reexport", "StringManipulation", "Tables"] -git-tree-sha1 = "1101cd475833706e4d0e7b122218257178f48f34" +deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"] +git-tree-sha1 = "624de6279ab7d94fc9f672f0068107eb6619732c" uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" -version = "2.4.0" +version = "3.3.2" + + [deps.PrettyTables.extensions] + PrettyTablesTypstryExt = "Typstry" + + [deps.PrettyTables.weakdeps] + Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e" [[deps.Printf]] deps = ["Unicode"] @@ -552,15 +695,15 @@ uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" version = "1.11.0" [[deps.PtrArrays]] -git-tree-sha1 = "1d36ef11a9aaf1e8b74dacc6a731dd1de8fd493d" +git-tree-sha1 = "4fbbafbc6251b883f4d2705356f3641f3652a7fe" uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" -version = "1.3.0" +version = "1.4.0" [[deps.QuadGK]] deps = ["DataStructures", "LinearAlgebra"] -git-tree-sha1 = "9da16da70037ba9d701192e27befedefb91ec284" +git-tree-sha1 = "5e8e8b0ab68215d7a2b14b9921a946fee794749e" uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" -version = "2.11.2" +version = "2.11.3" [deps.QuadGK.extensions] QuadGKEnzymeExt = "Enzyme" @@ -569,7 +712,7 @@ version = "2.11.2" Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" [[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "StyledStrings", "Unicode"] +deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" version = "1.11.0" @@ -591,21 +734,31 @@ version = "1.2.2" [[deps.Requires]] deps = ["UUIDs"] -git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7" +git-tree-sha1 = "62389eeff14780bfe55195b7204c0d8738436d64" uuid = "ae029012-a4dd-5104-9daa-d747884805df" -version = "1.3.0" +version = "1.3.1" + +[[deps.Reseau]] +deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"] +git-tree-sha1 = "0eab6d95ed40c2ef3992255c1c71e4f9748932b5" +uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36" +version = "1.3.1" [[deps.Revise]] -deps = ["CodeTracking", "Distributed", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "REPL", "Requires", "UUIDs", "Unicode"] -git-tree-sha1 = "470f48c9c4ea2170fd4d0f8eb5118327aada22f5" +deps = ["CRC32c", "CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"] +git-tree-sha1 = "27e3ee13fc8739a59b380d6163d6a82f52c03bd7" uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" -version = "3.6.4" +version = "3.15.1" +weakdeps = ["Distributed"] + + [deps.Revise.extensions] + DistributedExt = "Distributed" [[deps.Rmath]] deps = ["Random", "Rmath_jll"] -git-tree-sha1 = "852bd0f55565a9e973fcfee83a84413270224dc4" +git-tree-sha1 = "5b3d50eb374cea306873b371d3f8d3915a018f0b" uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa" -version = "0.8.0" +version = "0.9.0" [[deps.Rmath_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] @@ -613,62 +766,93 @@ git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8" uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f" version = "0.5.1+0" +[[deps.Roots]] +deps = ["Accessors", "CommonSolve", "Printf"] +git-tree-sha1 = "ed45bcc7cf3c8887595b973f2b1efbe91dcc50ec" +uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" +version = "3.0.1" + + [deps.Roots.extensions] + RootsChainRulesCoreExt = "ChainRulesCore" + RootsForwardDiffExt = "ForwardDiff" + RootsIntervalRootFindingExt = "IntervalRootFinding" + RootsSymPyExt = "SymPy" + RootsSymPyPythonCallExt = "SymPyPythonCall" + RootsUnitfulExt = "Unitful" + + [deps.Roots.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807" + SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6" + SymPyPythonCall = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" version = "0.7.0" [[deps.SQLLLM]] -deps = ["CSV", "DataFrames", "DataStructures", "Dates", "FileIO", "GeneralUtils", "HTTP", "JSON3", "LLMMCTS", "LibPQ", "PrettyPrinting", "Random", "Revise", "StatsBase", "Tables", "URIs", "UUIDs"] -path = "/appfolder/app/dev/SQLLLM" +deps = ["CSV", "DataFrames", "DataStructures", "Dates", "FileIO", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "PrettyPrinting", "Random", "Revise", "StatsBase", "Tables", "URIs", "UUIDs"] +git-tree-sha1 = "997602ed56a285ac29d74c91bb57bc5faeadfad6" +repo-rev = "main" +repo-url = "https://git.yiem.cc/ton/SQLLLM" uuid = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" -version = "0.2.4" +version = "0.2.5" [[deps.SQLStrings]] git-tree-sha1 = "55de0530689832b1d3d43491ee6b67bd54d3323c" uuid = "af517c2e-c243-48fa-aab8-efac3db270f5" version = "0.1.0" +[[deps.ScopedValues]] +deps = ["HashArrayMappedTries", "Logging"] +git-tree-sha1 = "67a144433c4ce877ee6d1ada69a124d6b1ecf7be" +uuid = "7e506255-f358-4e82-b7e4-beb19740aa63" +version = "1.6.2" + [[deps.Scratch]] deps = ["Dates"] -git-tree-sha1 = "3bac05bc7e74a75fd9cba4295cde4045d9fe2386" +git-tree-sha1 = "9b81b8393e50b7d4e6d0a9f14e192294d3b7c109" uuid = "6c6a2e73-6563-6170-7368-637461726353" -version = "1.2.1" +version = "1.3.0" [[deps.SentinelArrays]] deps = ["Dates", "Random"] -git-tree-sha1 = "d0553ce4031a081cc42387a9b9c8441b7d99f32d" +git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80" uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" -version = "1.4.7" +version = "1.4.10" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" version = "1.11.0" -[[deps.SimpleBufferStream]] -git-tree-sha1 = "f305871d2f381d21527c770d4788c06c097c9bc1" -uuid = "777ac1f9-54b0-4bf8-805c-2214025038e7" -version = "1.2.0" - [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" version = "1.11.0" +[[deps.Sodium]] +deps = ["Base64", "libsodium_jll"] +git-tree-sha1 = "907703e0d50846f300650d7225bdcab145b7bca9" +uuid = "4f5b5e99-b0ad-42cd-b47a-334e172ec8bd" +version = "1.1.2" + [[deps.SortingAlgorithms]] deps = ["DataStructures"] -git-tree-sha1 = "66e0a8e672a0bdfca2c3f5937efb8538b9ddc085" +git-tree-sha1 = "13cd91cc9be159e3f4d95b857fa2aa383b53772a" uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" -version = "1.2.1" +version = "1.2.3" [[deps.SparseArrays]] deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -version = "1.11.0" +version = "1.12.0" [[deps.SpecialFunctions]] deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] -git-tree-sha1 = "64cca0c26b4f31ba18f13f6c12af7c85f478cfde" +git-tree-sha1 = "6547cbdd8ce32efba0d21c5a40fa96d1a3548f9f" uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "2.5.0" +version = "2.8.0" [deps.SpecialFunctions.extensions] SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" @@ -688,21 +872,21 @@ weakdeps = ["SparseArrays"] [[deps.StatsAPI]] deps = ["LinearAlgebra"] -git-tree-sha1 = "1ff449ad350c9c4cbc756624d6f8a8c3ef56d3ed" +git-tree-sha1 = "178ed29fd5b2a2cfc3bd31c13375ae925623ff36" uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" -version = "1.7.0" +version = "1.8.0" [[deps.StatsBase]] -deps = ["AliasTables", "DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] -git-tree-sha1 = "29321314c920c26684834965ec2ce0dacc9cf8e5" +deps = ["AliasTables", "DataAPI", "DataStructures", "IrrationalConstants", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] +git-tree-sha1 = "e4d7a1a0edc20af42689ea6f4f3587a2175d50ee" uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -version = "0.34.4" +version = "0.34.12" [[deps.StatsFuns]] deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] -git-tree-sha1 = "35b09e80be285516e52c9054792c884b9216ae3c" +git-tree-sha1 = "770240df9a3b8888065046948f7a09b4e0f997d5" uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c" -version = "1.4.0" +version = "2.2.0" [deps.StatsFuns.extensions] StatsFunsChainRulesCoreExt = "ChainRulesCore" @@ -714,9 +898,14 @@ version = "1.4.0" [[deps.StringManipulation]] deps = ["PrecompileTools"] -git-tree-sha1 = "a6b1675a536c5ad1a60e5a5153e1fee12eb146e3" +git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5" uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" -version = "0.4.0" +version = "0.4.4" + +[[deps.StringViews]] +git-tree-sha1 = "f2dcb92855b31ad92fe8f079d4f75ac57c93e4b8" +uuid = "354b36f9-a18e-4713-926e-db85100087ba" +version = "1.3.7" [[deps.StructTypes]] deps = ["Dates", "UUIDs"] @@ -724,6 +913,22 @@ git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8" uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" version = "1.11.0" +[[deps.StructUtils]] +deps = ["Dates", "UUIDs"] +git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8" +uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" +version = "2.8.2" + + [deps.StructUtils.extensions] + StructUtilsMeasurementsExt = ["Measurements"] + StructUtilsStaticArraysCoreExt = ["StaticArraysCore"] + StructUtilsTablesExt = ["Tables"] + + [deps.StructUtils.weakdeps] + Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" + StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" + Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" + [[deps.StyledStrings]] uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" version = "1.11.0" @@ -735,7 +940,7 @@ uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" [[deps.SuiteSparse_jll]] deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" -version = "7.7.0+0" +version = "7.8.3+2" [[deps.TOML]] deps = ["Dates"] @@ -744,9 +949,9 @@ version = "1.0.3" [[deps.TZJData]] deps = ["Artifacts"] -git-tree-sha1 = "36b40607bf2bf856828690e097e1c799623b0602" +git-tree-sha1 = "72df96b3a595b7aab1e101eb07d2a435963a97e2" uuid = "dc5dba14-91b3-4cab-a142-028a31da12f7" -version = "1.3.0+2024b" +version = "1.5.0+2025b" [[deps.TableTraits]] deps = ["IteratorInterfaceExtensions"] @@ -756,9 +961,9 @@ version = "1.0.1" [[deps.Tables]] deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"] -git-tree-sha1 = "598cd7c1f68d1e205689b1c2fe65a9f85846f297" +git-tree-sha1 = "0f38a06c83f0007bbab3cf911262841c9a0f07e0" uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -version = "1.12.0" +version = "1.13.0" [[deps.Tar]] deps = ["ArgTools", "SHA"] @@ -771,10 +976,10 @@ uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" version = "1.11.0" [[deps.TimeZones]] -deps = ["Dates", "Downloads", "InlineStrings", "Mocking", "Printf", "Scratch", "TZJData", "Unicode", "p7zip_jll"] -git-tree-sha1 = "33c771f2157712ff4c85931186a4984efbe58934" +deps = ["Artifacts", "Dates", "Downloads", "InlineStrings", "Mocking", "Printf", "Scratch", "TZJData", "Unicode", "p7zip_jll"] +git-tree-sha1 = "d422301b2a1e294e3e4214061e44f338cafe18a2" uuid = "f269a46b-ccf7-5d73-abea-4c690281aa53" -version = "1.19.0" +version = "1.22.2" weakdeps = ["RecipesBase"] [deps.TimeZones.extensions] @@ -786,9 +991,9 @@ uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" version = "0.11.3" [[deps.URIs]] -git-tree-sha1 = "67db6cc7b3821e19ebe75791a9dd19c9b1188f2b" +git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a" uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -version = "1.5.1" +version = "1.6.1" [[deps.UTCDateTimes]] deps = ["Dates", "TimeZones"] @@ -807,37 +1012,57 @@ version = "1.11.0" [[deps.WeakRefStrings]] deps = ["DataAPI", "InlineStrings", "Parsers"] -git-tree-sha1 = "b1be2855ed9ed8eac54e5caff2afcdb442d52c23" +git-tree-sha1 = "0716e01c3b40413de5dedbc9c5c69f27cddfddfc" uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5" -version = "1.4.2" +version = "1.4.3" [[deps.WorkerUtilities]] git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7" uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60" version = "1.6.1" +[[deps.YiemAgent]] +deps = ["CSV", "DataFrames", "DataStructures", "Dates", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs", "msghandler"] +path = "." +uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" +version = "0.4.0" + [[deps.Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.13+1" +version = "1.3.1+2" [[deps.Zstd_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "555d1076590a6cc2fdee2ef1469451f872d8b41b" +git-tree-sha1 = "446b23e73536f84e8037f5dce465e92275f6a308" uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" -version = "1.5.6+1" +version = "1.5.7+1" [[deps.libblastrampoline_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.11.0+0" +version = "5.15.0+0" + +[[deps.libsodium_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "011b0a7331b41c25524b64dc42afc9683ee89026" +uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8" +version = "1.0.21+0" + +[[deps.msghandler]] +deps = ["Arrow", "Base64", "DataFrames", "Dates", "GeneralUtils", "HTTP", "JSON", "NATS", "PrettyPrinting", "Revise", "UUIDs"] +git-tree-sha1 = "db16f76f72bd4fa2a87e34ef47bb787204e1f888" +repo-rev = "main" +repo-url = "https://git.yiem.cc/ton/msghandler" +uuid = "f2724d33-f338-4a57-b9f8-1be882570d10" +version = "0.5.7" [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.59.0+0" +version = "1.64.0+1" [[deps.p7zip_jll]] -deps = ["Artifacts", "Libdl"] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.4.0+2" +version = "17.7.0+0" diff --git a/Project.toml b/Project.toml index 1f417cd..f99c956 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "YiemAgent" uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" +version = "0.4.0" authors = ["narawat lamaiin "] -version = "0.3.0" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" @@ -10,9 +10,10 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" -JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LLMMCTS = "d76c5a4d-449e-4835-8cc4-dd86ec44f241" LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1" +NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a" PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" @@ -20,7 +21,14 @@ SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +msghandler = "f2724d33-f338-4a57-b9f8-1be882570d10" [compat] CSV = "0.10.15" DataFrames = "1.7.0" +GeneralUtils = "0.4.9" +HTTP = "2.4.0" +JSON = "1.6.1" +NATS = "0.1.0" +SQLLLM = "0.2.5" +msghandler = "0.5.6" diff --git a/aisystemprompt.md b/aisystemprompt.md new file mode 100644 index 0000000..143508d --- /dev/null +++ b/aisystemprompt.md @@ -0,0 +1,82 @@ +# Store Policy + +- Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. +- If you found wines in the store's database, they are in stock. +- You can only recommend wines that are currently in our inventory +- Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. +- Ask the user one question at a time. +- Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. +- Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. +- Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. +- Spicy foods should be paired only with light red wines. +- We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such. +- Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. + +# Store Guidelines + +- Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. +- Customer may provide images for you to look up. +- Encourage the customer to explore different options and try new things. +- If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. +- Your store carries only wine. +- Vintage 0 means non-vintage. +- Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. + +# Prompt + +Search the database as broad as possible under the informantion you have will increase the chance to find wine. Avoid uneccessary parameter such as region, country, tasting notes unless the user specify + +# Situation + +Your customer is coming into the store + +# Role + +Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store. You are working under your mentor supervision. + +# Objective + +1. Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. +2. Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. + +# Responsibility Includes + +1. According to the store's policy and guidelines, make an informed decision about what you need to do to achieve the objective +2. Keep the conversation with the customer going smoothly +3. Obey your mentor's suggestions. + +# Responsibility Does NOT Include + +1. Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. +2. Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. +3. Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. + +# Available Actions + +- **CHAT_BOX** which you can use to talk with the user. +- **CHECK_WINE** allows you to check information about wines you want in your inventory's database. The input is text that specify supported search criteria includeing: retailer_name, wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. + - Example query 1: "Dry, full-bodied red wine from 1) region: Burgundy, country: France or 2) region: Tuscany, country: Italy. Grape varietal: Merlot or Syrah. price 100 to 1000 USD." + - Example query 2: "Red or white wine, medium tannin, price under 700 USD" + - Example query 3: "white wine, region: Tuscany or Bordeaux, country: Italy or France +- **PRESENT_WINE_GUIDELINE** which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. +- **END_CONVER_GUIDELINE** which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow. + +# Response Format + +You should respond to the user with interleaving plan, action_name, action_input: + +1. **plan**: Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. +2. **action_name**: (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name +3. **action_input**: The input to the action you are about to perform according to your plan. + +After the action is executed you gets "action_result". It is the output from the action you selected. + +Assistant should only respond in JSON format as described below: + +```json +{ + "plan": "...", + "action_name": "...", + "action_input": "..." +} +``` diff --git a/dummy_config.json b/dummy_config.json new file mode 100644 index 0000000..11d65ee --- /dev/null +++ b/dummy_config.json @@ -0,0 +1,53 @@ +{ + "nats_server_info": { + "description": "nats server", + "url": "nats.yiem.cc" + }, + "testingOrProduction": "testing", + "agentId": "2b74b87a-5413-4fe2-a4d3-405891051680", + "agentCentralConfigSubject": "/yiem/hq/agent/sommelier/backend/config/api/v1.1", + "this_service_input_channel": { + "mqtt": [ + "/yiem/hq/agent/sommpanion/backend/db/api_v1" + ], + "nats": [ + "sommpanion.backend.agentbackend.v1.inbox" + ] + }, + "agentRole": "sommelier", + "organization": "yiem_hq", + "externalService": { + "servicesloadbalancer": { + "nats": "sommpanion.backend.servicesloadbalancer.v1.inbox" + }, + "textembedding": { + "url": "textembedding.api.v1" + }, + "textimage_to_text_llm": { + "url": "https://llmcoder.yiem.cc/v1/chat/completions", + "modelname": "Qwen3.6-35B-A3B-UD-Q4_K_M" + }, + "virtualWineCustomer_1": { + "serviceSubject": "", + "modelName": "qwen3:8b" + }, + "sommpanion_db" : { + "description": "A database connection info for LibPQ client", + "url": "192.168.88.106:5432", + "dbname": "winedb", + "user": "yiemtechnologies@gmail.com", + "password": "yiemtechnologies@Postgres_0.0" + }, + "sommpanion_vectordb" : { + "description": "A wine database connection info for LibPQ client", + "url": "192.168.88.106:5433", + "dbname": "vectordb", + "user": "yiemtechnologies@gmail.com", + "password": "yiemtechnologies@Postgres_0.0" + }, + "fileserver": { + "description": "temporary file server", + "url": "https://fileserver.yiem.cc" + } + } +} \ No newline at end of file diff --git a/etc.jl b/etc.jl new file mode 100644 index 0000000..13b0321 --- /dev/null +++ b/etc.jl @@ -0,0 +1,93 @@ +using DataStructures + +function dictify2(x; keytype::Type=Any, sort_order::Union{Nothing, Vector}=nothing) + # Dict-like objects + if x isa AbstractDict + out = OrderedDict{keytype, Any}() + + # 1. Process and normalize all keys from the input dictionary + processed_dict = OrderedDict{keytype, Any}() + for (k, v) in x + if keytype === String + newk = string(k) + elseif keytype === Symbol + newk = Symbol(string(k)) + else + newk = k + end + processed_dict[newk] = dictify(v; keytype=keytype, sort_order=sort_order) + end + + # 2. If a sort order is specified, apply it + if !isnothing(sort_order) + # Normalize the sort_order elements to match the requested keytype + normalized_order = map(sort_order) do tk + if keytype === String + return string(tk) + elseif keytype === Symbol + return Symbol(string(tk)) + else + return tk + end + end + + # First, insert keys that match the requested order + for target_key in normalized_order + if haskey(processed_dict, target_key) + out[target_key] = processed_dict[target_key] + end + end + + # Then, append any remaining keys that weren't in the sort_order + for (k, v) in processed_dict + if !haskey(out, k) + out[k] = v + end + end + else + # If no sort order is given, just use the processed dict + out = processed_dict + end + + return out + + # Arrays / vectors: map elements recursively + elseif x isa AbstractArray + return [dictify(element; keytype=keytype, sort_order=sort_order) for element in x] + + # Everything else: return as-is + else + return x + end +end + + + + +function dict_to_string_html2(d::AbstractDict; indent_level=1, indent_str=" ") + lines = String[] + padding = indent_str ^ indent_level + + # Sort keys for predictable, clean output + for k in keys(d) + v = d[k] + + if v isa AbstractDict + # Open tag, recurse for children, then close tag + push!(lines, "$padding<$k>") + ind_level = indent_level + 1 + push!(lines, dict_to_string_html(v; indent_level=ind_level, indent_str=indent_str)) + push!(lines, "$padding") + else + # Leaf node: put key and value on a single line + push!(lines, "$padding<$k>$v") + end + end + return join(lines, "\n") +end + + + + + + diff --git a/example/agent_chat_virtualCustomer.jl b/example/agent_chat_virtualCustomer.jl new file mode 100644 index 0000000..b880599 --- /dev/null +++ b/example/agent_chat_virtualCustomer.jl @@ -0,0 +1,585 @@ +using Revise +using JSON, JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures +using YiemAgent, GeneralUtils +using Base.Threads + +# ---------------------------------------------- 100 --------------------------------------------- # + + + +# load config +config = JSON.parsefile("/appfolder/app/dev/YiemAgent/test/config.json") +# config = copy(JSON.parsefile("../mountvolume/config.json")) + + +function executeSQL(sql::T) where {T<:AbstractString} + host = config[:externalservice][:wineDB][:host] + port = config[:externalservice][:wineDB][:port] + dbname = config[:externalservice][:wineDB][:dbname] + user = config[:externalservice][:wineDB][:user] + password = config[:externalservice][:wineDB][:password] + DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") + result = LibPQ.execute(DBconnection, sql) + close(DBconnection) + return result +end + +function executeSQLVectorDB(sql) + host = config[:externalservice][:SQLVectorDB][:host] + port = config[:externalservice][:SQLVectorDB][:port] + dbname = config[:externalservice][:SQLVectorDB][:dbname] + user = config[:externalservice][:SQLVectorDB][:user] + password = config[:externalservice][:SQLVectorDB][:password] + DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") + result = LibPQ.execute(DBconnection, sql) + close(DBconnection) + return result +end + +function text2textInstructLLM(prompt::String; maxattempt::Integer=10, modelsize::String="medium", + senderId=GeneralUtils.uuid4snakecase(), timeout=90, + llmkwargs=Dict( + :num_ctx => 32768, + :temperature => 0.5, + ) + ) + msgMeta = GeneralUtils.generate_msgMeta( + config[:externalservice][:loadbalancer][:mqtttopic]; + msgPurpose="inference", + senderName="yiemagent", + senderId=senderId, + receiverName="text2textinstruct_$modelsize", + mqttBrokerAddress=config[:mqttServerInfo][:broker], + mqttBrokerPort=config[:mqttServerInfo][:port], + ) + + outgoingMsg = Dict( + :msgMeta => msgMeta, + :payload => Dict( + :text => prompt, + :kwargs => llmkwargs + ) + ) + + response = nothing + for attempts in 1:maxattempt + _response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; responsetimeout=timeout, responsemaxattempt=maxattempt) + payload = _response[:response] + if _response[:success] && payload[:text] !== nothing + response = _response[:response][:text] + break + else + println("\n attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(outgoingMsg) + println(" attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + sleep(3) + end + end + + return response +end + +# get text embedding from a LLM service +function getEmbedding(text::T) where {T<:AbstractString} + msgMeta = GeneralUtils.generate_msgMeta( + config[:externalservice][:loadbalancer][:mqtttopic]; + msgPurpose="embedding", + senderName="yiemagent", + senderId=sessionId, + receiverName="textembedding", + mqttBrokerAddress=config[:mqttServerInfo][:broker], + mqttBrokerPort=config[:mqttServerInfo][:port], + ) + + outgoingMsg = Dict( + :msgMeta => msgMeta, + :payload => Dict( + :text => [text] # must be a vector of string + ) + ) + + response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; responsetimeout=120, responsemaxattempt=3) + embedding = response[:response][:embeddings] + return embedding +end + +function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3, + vectorDB::Function; limit::Integer=1 + )::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString} + # get embedding from LLM service + embedding = getEmbedding(text)[1] + # check whether there is close enough vector already store in vectorDB. if no, add, else skip + sql = """ + SELECT *, $embeddingColumnName <-> '$embedding' as distance + FROM $tablename + ORDER BY distance LIMIT $limit; + """ + response = vectorDB(sql) + df = DataFrame(response) + return df +end + +function similarSQLVectorDB(query; maxdistance::Integer=100) + tablename = "sqlllm_decision_repository" + # get embedding of the query + df = findSimilarTextFromVectorDB(query, tablename, + "function_input_embedding", executeSQLVectorDB) + # println(df[1, [:id, :function_output]]) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + # distance = 100 # CHANGE this is for testing only + if row != 0 && distance < maxdistance + # if there is usable SQL, return it. + output_b64 = df[1, :function_output_base64] # pick the closest match + output_str = String(base64decode(output_b64)) + rowid = df[1, :id] + println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + return (dict=output_str, distance=distance) + else + println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + return (dict=nothing, distance=nothing) + end +end + +function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString} + tablename = "sqlllm_decision_repository" + # get embedding of the query + # query = state[:thoughtHistory][:question] + df = findSimilarTextFromVectorDB(query, tablename, + "function_input_embedding", executeSQLVectorDB) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + if row == 0 || distance > maxdistance # no close enough SQL stored in the database + query_embedding = getEmbedding(query)[1] + query = replace(query, "'" => "") + sql_base64 = base64encode(SQL) + sql_ = replace(SQL, "'" => "") + + sql = """ + INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding'); + """ + # println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # println(sql) + _ = executeSQLVectorDB(sql) + end +end + + +function similarSommelierDecision(recentevents::T1; maxdistance::Integer=3 + )::Union{AbstractDict, Nothing} where {T1<:AbstractString} + tablename = "sommelier_decision_repository" + # find similar + println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__) + df = findSimilarTextFromVectorDB(recentevents, tablename, + "function_input_embedding", executeSQLVectorDB) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + if row != 0 && distance < maxdistance + # if there is usable decision, return it. + rowid = df[1, :id] + println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__) + output_b64 = df[1, :function_output_base64] # pick the closest match + _output_str = String(base64decode(output_b64)) + output = copy(JSON.parsefile(_output_str)) + return output + else + println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__) + return nothing + end +end + + +function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5 + ) where {T1<:AbstractString, T2<:AbstractDict} + tablename = "sommelier_decision_repository" + # find similar + df = findSimilarTextFromVectorDB(recentevents, tablename, + "function_input_embedding", executeSQLVectorDB) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + if row == 0 || distance > maxdistance # no close enough SQL stored in the database + recentevents_embedding = getEmbedding(recentevents)[1] + recentevents = replace(recentevents, "'" => "") + decision_json = JSON.json(decision) + decision_base64 = base64encode(decision_json) + decision = replace(decision_json, "'" => "") + + sql = """ + INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding'); + """ + println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__) + println(sql) + _ = executeSQLVectorDB(sql) + else + println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__) + end +end + + +sessionId = GeneralUtils.uuid4snakecase() + +externalFunction = ( + getEmbedding=getEmbedding, + text2textInstructLLM=text2textInstructLLM, + executeSQL=executeSQL, + similarSQLVectorDB=similarSQLVectorDB, + insertSQLVectorDB=insertSQLVectorDB, + similarSommelierDecision=similarSommelierDecision, + insertSommelierDecision=insertSommelierDecision, + ) + + +# s = "full-bodied red wine, budget 1500 USD" +# r = YiemAgent.extractWineAttributes_1(agent, s) +# println(r) + + +# --------------------------- generating scenario and customer profile --------------------------- # + +function rolegenerator() + rolegenerator_systemmsg = + """ + Your role: + - You are a helpful assistant + Your mission: + - Create one random role of a potential customer of an internet wine store. + You must follow the following guidelines: + - the user only need the role, do not add your own words. + - the role should be detailed and realistic. + You should then respond to the user with: + Name: a name of the potential customer + Situation: a situation that the potential customer may be facing + Mission: a mission of the potential customer + Profile: a profile of the potential customer, including their age, gender, occupation, and other relevant information + You should only respond in format as described below: + Name: ... + Situation: ... + Mission: ... + Profile: ... + Additional_information: ... + + Here are some examples: + Name: Jimmy + Situation: + - Your relationship with your boss is not that good. You need to improve your relationship with your boss. + - Your boss's wedding anniversary is coming up. + - You are at a wine store and start talking with the store's sommelier. + Mission: + - Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list. + Profile: + - You are a young professional in a big company. + - You are avid party goer + - You like beer. + - You know nothing about wine. + - You have a budget of 1500usd. + Additional_information: + - your boss like spicy food. + - your boss is a middle-aged man. + - your boss likes Australian wine. + + Name: Kate + Situation: + - Your husband asked you to get him a bottle of wine. He will gift the wine to his business client while dining at a German restaurant. + - Your husband is a business client and he will gift the wine to his business + - You are at a wine store and start talking with the store's sommelier. + Mission: + - Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list. + Profile: + - You are a CEO in a startup company. + - You are a nerd + - You don't like alcohol. + - You have a budget of 150usd. + - You don't care about organic, sulfite, gluten-free, or sustainability certified wines + Additional_information: + - your husband like spicy food. + - your husband is a middle-aged man. + + Name: John + Situation: + - A local newspaper club wants to have a scoop about wine with local food in the U.S. + - You are at a wine store and start talking with the store's sommelier. + Mission: + - Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list. + Profile: + - I'm a young guy. + - I prefer to express my ideas in a succinct and clear manner. + Additional_information: + - N/A + + Name: Jane + Situation: + - You have catering a dinner party with French cuisine. + - You want to serve wine with your guests. + - You are at a wine store and start talking with the store's sommelier. + Mission: + - Ask the sommelier to provide multiple wine options, and subsequently choose one option from the presented list. + Profile: + - You are a young French restaurant owner. + - You like dry, full-bodied red wine with high tannin + - You don't care about organic, sulfite, gluten-free, or sustainability certified wines. + - You have a budget of 200 usd. + Additional_information: + - N/A + + Let's begin! + """ + + header = ["Name:", "Situation:", "Mission:", "Profile:", "Additional_information:"] + dictkey = ["name", "situation", "mission", "profile", "additional_information"] + errornote = "N/A" + + for attempt in 1:10 + _prompt = + [ + Dict(:name => "system", :text => rolegenerator_systemmsg), + ] + prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") + + response = text2textInstructLLM(prompt) # generated role + response = GeneralUtils.deFormatLLMtext(response, "qwen3") + think, response = GeneralUtils.extractthink(response) + + # check whether response has all header + detected_kw = GeneralUtils.detect_keyword(header, response) + kwvalue = [i for i in values(detected_kw)] + zeroind = findall(x -> x == 0, kwvalue) + missingkeys = [header[i] for i in zeroind] + if 0 ∈ values(detected_kw) + errornote = "$missingkeys are missing from your previous response" + println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif sum(values(detected_kw)) > length(header) + errornote = "\nYour previous attempt has duplicated points according to the required response format" + println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + responsedict = GeneralUtils.textToDict(response, header; + dictKey=dictkey, symbolkey=true) + responsedict[:id] = GeneralUtils.uuid4snakecase() + + responsedict[:systemmsg] = + """ + You are role playing as a CUSTOMER of a wine store and you are currently talking with a sommelier of a wine store. + Your profile is as follows: + Situation: $(responsedict[:situation]) + Mission: $(responsedict[:mission]) + Profile: $(responsedict[:profile]) + Additional_information: $(responsedict[:additional_information]) + + You should follow the following guidelines: + - Focus on the lastest conversation + - Your like to be short and concise + - If you don't know an answer to sommelier's question, you should say: I don't know. + - If you think the store can't provide what you seek, you can leave. + + You should then respond to the user with: + Dialogue: what you want to say to the user + Role: Verify that the dialogue is intended for the customer of a wine store. Can be "yes" or "no" + You should only respond in format as described below: + Dialogue: ... + Role: ... + + Let's begin! + """ + + println("\nrolegenerator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println(responsedict) + return responsedict + end + error("ERROR rolegenerator() failed to generate customer role: ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +end + + +# Define the external functions for the customer agent in named tuple format +customer_externalFunction = ( + text2textInstructLLM=text2textInstructLLM, + ) + + + + +function main() + agent = YiemAgent.sommelier( + externalFunction; + name="Jane", + id=sessionId, # agent instance id + retailername="Yiem", + llmFormatName="qwen3" + ) + + customerDict = rolegenerator() + customer = YiemAgent.virtualcustomer( + customer_externalFunction; + systemmsg=customerDict[:systemmsg], + name=customerDict[:name], + id=sessionId, # agent instance id + llmFormatName="qwen3" + ) + + # customer_chat = "hello" + + # YiemAgent.addNewMessage(customer, "assistant", customer_chat) + # # add user activity to events memory + # push!(customer.memory[:events], + # YiemAgent.eventdict(; + # event_description="the assistant talks to the user.", + # timestamp=Dates.now(), + # subject="assistant", + # action_name="CHAT_BOX", + # action_input=customer_chat, + # ) + # ) + # println("\ncustomer respond:\n $customer_chat") + agent_response = YiemAgent.conversation(agent; maximumMsg=50) + println("\nagent respond:\n $agent_response") + while true + customer_chat = nothing + while customer_chat === nothing + customer_response = YiemAgent.conversation(customer, Dict(:text=> agent_response); + converPartnerName=agent.name, + maximumMsg=50) + customer_response = GeneralUtils.deFormatLLMtext(customer_response, customer.llmFormatName) + customer_chat = customer_response + + #[WORKING] check whether customer response the same before + end + + println("\ncustomer respond:\n $customer_chat") + + agent_response = YiemAgent.conversation(agent; + userinput=Dict(:text=> customer_chat), + maximumMsg=50) + println("\nagent respond:\n $agent_response") + + if haskey(agent.memory[:events][end], :thought) + lastAssistantAction = agent.memory[:events][end][:thought][:action_name] + if lastAssistantAction == "END_CONVER_GUIDELINE" # store thoughtDict + + # save a.memory[:shortmem][:decisionlog] to disk using JSON + println("\nsaving agent.memory[:shortmem][:decisionlog] to disk") + date = "$(Dates.now())" + date = replace(date, ':'=>'.') + filename = "agent_decision_log_$(date)_$(agent.id).json" + filepath = "/appfolder/mountvolume/appdata/log/$filename" + open(filepath, "w") do io + JSON.pretty(io, agent.memory[:shortmem][:decisionlog]) + end + + # check how many file in /appfolder/mountvolume/appdata/log/ folder now + logfilesnumber = length(readdir("/appfolder/mountvolume/appdata/log/")) + println("\nCaching conversation process done. Total $logfilesnumber files in /appfolder/mountvolume/appdata/log/ folder now.\n") + break + end + end + end +end + +for i in 1:100 + main() + println("\n Round $i/100 done.") +end + +println("done") + +# prompt = +# """ +# <|im_start|>system +# You are a role playing agent acting as: +# Name: Emily +# Situation: - Emily is planning her upcoming birthday party and wants to make it extra special. She has invited close friends and family, and she's looking for a unique wine that will impress them. +# Mission: - Emily needs to find a rare and high-quality wine that matches the theme of her party, which is a mix of classic and modern flavors. She also wants to ensure that the wine is not too expensive so that it won't break her budget. +# Profile: - Emily is in her late 20s, works as a marketing executive for a tech company, and has a passion for trying new things. She's organized and detail-oriented but can be spontaneous when it comes to planning events. +# Additional_information: - Emily loves experimenting with different types of food and wine pairings. + +# Your are currently talking with a sommelier. + +# You should follow the following guidelines: +# - Focus on the lastest conversation +# - If you satisfy with the sommelier's recommendation for bottle of wine(s), you should say: Thanks for you help. I will buy the wine you recommended. +# - If you don't satisfy with the sommelier's questions or can't get a good wine recommendation, you can continue the conversation. + +# Let's begin! + +# <|im_end|> +# <|im_start|>Jane +# Hello! Welcome to Yiem's Wine Store. I'm Jane, your friendly sommelier. How can I assist you today? What type of wine are you in the mood for, and is there a special occasion or event on your mind? +# <|im_end|> +# <|im_start|>Emily +# Hi Jane! Thank you so much for welcoming me. For my birthday party, I'm looking for something that combines classic and modern flavors. It's a mix of guests who enjoy both traditional tastes and more contemporary ones. Also, I want to make sure it won't break the bank. Any suggestions? +# <|im_end|> +# <|im_start|>Jane +# Thank you for sharing your preferences, Jane! To better assist you, could you please let me know if there are any specific characteristics of wine you're looking for, such as tannin, sweetness, intensity, or acidity? Additionally, do you have any food items in mind that this wine should pair well with? +# <|im_end|> +# <|im_start|>Emily +# """ + +# llmkwargs=Dict( +# :num_ctx => 32768, +# :temperature => 0.3, +# ) +# r = text2textInstructLLM(prompt, llmkwargs=llmkwargs) +# println(r) +# println(555) + +# response = YiemAgent.conversation(agent, Dict(:text=> "I want to get a French red wine under 100.")) + + +# while true +# println("your respond: ") +# user_answer = readline() +# response = YiemAgent.conversation(agent, Dict(:text=> user_answer)) +# println("\n$response") +# end + + + + + + + + + + + + +# """ +# Hello + +# I would like to get a bottle of wine for my boss but I don't know much about wine. Can you help me? + +# well actually, my boss is going to offer the wine to his client as a gift in a business meeting. All I know is his client like spicy food and French wine. I have a budget about 1000. + +# """ + +# input = "French wine, bordeaux, under USD100, pairs with spicy food" +# r = YiemAgent.extractWineAttributes_1(a, input) + +# inventory_order = "French Syrah, Viognier, full bodied, under 100" +# r = YiemAgent.extractWineAttributes_2(a, inventory_order) +# pprintln(r) + + +# cron job +# @reboot sleep 50 && nvidia-smi -pm 1 +# @reboot sleep 51 && nvidia-smi -i 0 -pl 150 +# @reboot sleep 52 && nvidia-smi -i 1 -pl 150 +# @reboot sleep 53 && nvidia-smi -i 2 -pl 150 +# @reboot sleep 54 && nvidia-smi -i 3 -pl 150 + +# @reboot sleep 55 && julia -t 2 /home/ton/work/restartContainer/main.jl + +# using GeneralUtils +# msgMeta = GeneralUtils.generate_msgMeta( +# "/tonpc_containerServices", +# senderName= "somename", +# senderId= "1230", +# mqttBrokerAddress= "mqtt.yiem.cc", +# mqttBrokerPort= 1883, +# ) +# outgoingMsg = Dict( +# :msgMeta=> msgMeta, +# :payload=> "docker container restart playground-app", +# ) +# GeneralUtils.sendMqttMsg(outgoingMsg) + diff --git a/test/config.json b/example/config.json similarity index 100% rename from test/config.json rename to example/config.json diff --git a/example/main.jl b/example/main.jl new file mode 100644 index 0000000..c28785a --- /dev/null +++ b/example/main.jl @@ -0,0 +1,706 @@ +using JSON, JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures +using YiemAgent, GeneralUtils +using Base.Threads + +# ---------------------------------------------- 100 --------------------------------------------- # + + +""" Expected incomming MQTT message format for this service: + { + "msgMeta": { + "msgPurpose": "updateStatus", + "requestresponse": "request", + "timestamp": "2024-03-29T05:8:48.362", + "replyToMsgId": null, + "receiverId": null, + "getpost": "get", + "msgId": "e5c09bd8-7100-4e4e-bb43-05bee589a22c", + "acknowledgestatus": null, + "sendTopic": "/agent/wine/backend/chat/api/v1/prompt", + "receiverName": "agent-wine-backend", + "replyTopic": "/agent/wine/frontend/chat/api/v1/txt/receive", + "senderName": "agent-wine-frontend-chat", + "senderId": "0938a757-e0ee-40a9-8355-5e24906a87cd" + }, + "payload" : { + "text": "hello" + } + + } +""" + + + + + +# load config +config = copy(JSON.parsefile("../mountvolume/config/config.json")) + +""" Instantiate an agent. One need to specify startmessage and one of gpu location info, + Mqtt or Rest. start message must be comply with GeneralUtils's message format + + Arguments\n + ----- + channel::Channel + communication channel + sessionId::String + sesstion ID of the agent + agentName::String + Name of the agent + mqttBroker::String + mqtt broker e.g. "tcp://127.0.0.1:1883" + agentConfigTopic::String + main communication topic for an agent to ask for config + timeout::Int64 + inactivity timeout in minutes. If timeout is reached, an agent will be terminated. + + Return\n + ----- + a task represent an agent + + Example\n + ----- + ```jldoctest + julia> using YiemAgent, GeneralUtils + julia> msg = GeneralUtils.generate_msgMeta("/agent") + julia> incoming_msg = msg # assuming 1st msg was sent from other app + julia> agentConfigTopic = "/agent/wine/backend/config" + julia> task = runAgentInstance(incoming_msg, mqttBroker, agentConfigTopic, 60) + ``` + + TODO\n + ----- + [] update docstringLAMA_CONTEXT_LENGTH=40960 since the default size is 2048 as you can see in your debug log: + [] change how to get result of YiemAgent from let YiemAgent send msg directly to frontend, + to + response = YiemAgent.conversation() + then send response to frontend + + Signature\n + ----- +""" +function runAgentInstance( + receiveUserMsgChannel::Channel, + outputchannel::Channel, + sessionId::String, + config::Dict, + timeout::Int64, + ) + + function executeSQL(sql::T) where {T<:AbstractString} + host = config[:externalservice][:wineDB][:host] + port = config[:externalservice][:wineDB][:port] + dbname = config[:externalservice][:wineDB][:dbname] + user = config[:externalservice][:wineDB][:user] + password = config[:externalservice][:wineDB][:password] + DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") + result = LibPQ.execute(DBconnection, sql) + close(DBconnection) + return result + end + + function executeSQLVectorDB(sql) + host = config[:externalservice][:SQLVectorDB][:host] + port = config[:externalservice][:SQLVectorDB][:port] + dbname = config[:externalservice][:SQLVectorDB][:dbname] + user = config[:externalservice][:SQLVectorDB][:user] + password = config[:externalservice][:SQLVectorDB][:password] + DBconnection = LibPQ.Connection("host=$host port=$port dbname=$dbname user=$user password=$password") + result = LibPQ.execute(DBconnection, sql) + close(DBconnection) + return result + end + + function text2textInstructLLM(prompt::String; maxattempt::Integer=3, modelsize::String="medium", + senderId=GeneralUtils.uuid4snakecase(), timeout=180, + llmkwargs=Dict( + :num_ctx => 32768, + :temperature => 0.5, + )) + msgMeta = GeneralUtils.generate_msgMeta( + config[:externalservice][:loadbalancer][:mqtttopic]; + msgPurpose="inference", + senderName="yiemagent", + senderId=senderId, + receiverName="text2textinstruct_$modelsize", + mqttBrokerAddress=config[:mqttServerInfo][:broker], + mqttBrokerPort=config[:mqttServerInfo][:port], + ) + + outgoingMsg = Dict( + :msgMeta => msgMeta, + :payload => Dict( + :text => prompt, + :kwargs => llmkwargs + ) + ) + + response = nothing + for attempts in 1:maxattempt + _response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=timeout, maxattempt=maxattempt) + payload = _response[:response] + if _response[:success] && payload[:text] !== nothing + response = _response[:response][:text] + break + else + println("\n attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(outgoingMsg) + println(" attempt $attempts/$maxattempt failed ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + sleep(3) + end + end + + return response + end + + # get text embedding from a LLM service + function getEmbedding(text::T) where {T<:AbstractString} + msgMeta = GeneralUtils.generate_msgMeta( + config[:externalservice][:loadbalancer][:mqtttopic]; + msgPurpose="embedding", + senderName="yiemagent", + senderId=sessionId, + receiverName="textembedding", + mqttBrokerAddress=config[:mqttServerInfo][:broker], + mqttBrokerPort=config[:mqttServerInfo][:port], + ) + + outgoingMsg = Dict( + :msgMeta => msgMeta, + :payload => Dict( + :text => [text] # must be a vector of string + ) + ) + + response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120, maxattempt=3) + embedding = response[:response][:embeddings] + return embedding + end + + function findSimilarTextFromVectorDB(text::T1, tablename::T2, embeddingColumnName::T3, + vectorDB::Function; limit::Integer=1 + )::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString} + # get embedding from LLM service + embedding = getEmbedding(text)[1] + # check whether there is close enough vector already store in vectorDB. if no, add, else skip + sql = """ + SELECT *, $embeddingColumnName <-> '$embedding' as distance + FROM $tablename + ORDER BY distance LIMIT $limit; + """ + response = vectorDB(sql) + df = DataFrame(response) + return df + end + + function similarSQLVectorDB(query; maxdistance::Integer=100) + tablename = "sqlllm_decision_repository" + # get embedding of the query + df = findSimilarTextFromVectorDB(query, tablename, + "function_input_embedding", executeSQLVectorDB) + # println(df[1, [:id, :function_output]]) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + # distance = 100 # CHANGE this is for testing only + if row != 0 && distance < maxdistance + # if there is usable SQL, return it. + output_b64 = df[1, :function_output_base64] # pick the closest match + output_str = String(base64decode(output_b64)) + rowid = df[1, :id] + println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + return (dict=output_str, distance=distance) + else + println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + return (dict=nothing, distance=nothing) + end + end + + function insertSQLVectorDB(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString} + tablename = "sqlllm_decision_repository" + # get embedding of the query + # query = state[:thoughtHistory][:question] + df = findSimilarTextFromVectorDB(query, tablename, + "function_input_embedding", executeSQLVectorDB) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + if row == 0 || distance > maxdistance # no close enough SQL stored in the database + query_embedding = getEmbedding(query)[1] + query = replace(query, "'" => "") + sql_base64 = base64encode(SQL) + sql_ = replace(SQL, "'" => "") + + sql = """ + INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding'); + """ + # println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # println(sql) + _ = executeSQLVectorDB(sql) + end + end + + + function similarSommelierDecision(recentevents::T1; maxdistance::Integer=3 + )::Union{AbstractDict, Nothing} where {T1<:AbstractString} + tablename = "sommelier_decision_repository" + # find similar + println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__) + df = findSimilarTextFromVectorDB(recentevents, tablename, + "function_input_embedding", executeSQLVectorDB) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + if row != 0 && distance < maxdistance + # if there is usable decision, return it. + rowid = df[1, :id] + println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__) + output_b64 = df[1, :function_output_base64] # pick the closest match + _output_str = String(base64decode(output_b64)) + output = copy(JSON.parsefile(_output_str)) + return output + else + println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__) + return nothing + end + end + + + function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::Integer=5 + ) where {T1<:AbstractString, T2<:AbstractDict} + tablename = "sommelier_decision_repository" + # find similar + df = findSimilarTextFromVectorDB(recentevents, tablename, + "function_input_embedding", executeSQLVectorDB) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + if row == 0 || distance > maxdistance # no close enough SQL stored in the database + recentevents_embedding = getEmbedding(recentevents)[1] + recentevents = replace(recentevents, "'" => "") + decision_json = JSON.json(decision) + decision_base64 = base64encode(decision_json) + decision = replace(decision_json, "'" => "") + + sql = + """ + INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding'); + """ + println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__) + println(sql) + _ = executeSQLVectorDB(sql) + else + println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__) + end + end + + # keepaliveChannel_2::Channel{Dict} = Channel{Dict}(8) + latestUserMsgTimeStamp::DateTime = Dates.now() + + externalFunction = ( + getEmbedding=getEmbedding, + text2textInstructLLM=text2textInstructLLM, + executeSQL=executeSQL, + similarSQLVectorDB=similarSQLVectorDB, + insertSQLVectorDB=insertSQLVectorDB, + similarSommelierDecision=similarSommelierDecision, + insertSommelierDecision=insertSommelierDecision, + ) + + agent = YiemAgent.sommelier( + externalFunction; + name="Jane", + id=sessionId, # agent instance id + retailername="Yiem", + llmFormatName="qwen3" + ) + + # user chat loop + while true + # check for new user message + if isready(receiveUserMsgChannel) + incomingMsg = take!(receiveUserMsgChannel) + incoming_msgMeta = incomingMsg[:msgMeta] + incomingPayload = incomingMsg[:payload] + latestUserMsgTimeStamp = Dates.now() + + # make sure the message has :text key because YiemAgent use this key for incoming user msg + if haskey(incomingPayload, :text) + # skip, msg already has correct key name + elseif haskey(incomingPayload, :txt) + # change key name to text + incomingPayload[:text] = incomingPayload[:txt] + else + error("\n no :txt or :text key in the message.") + end + + # reset agent + if occursin("newtopic", incomingPayload[:text]) || + occursin("Newtopic", incomingPayload[:text]) || + occursin("New topic", incomingPayload[:text]) || + occursin("new topic", incomingPayload[:text]) + # YiemAgent.clearhistory(agent) + + agent = YiemAgent.sommelier( + externalFunction; + name="Janie", + id=sessionId, # agent instance id + retailername="Yiem", + ) + + # sending msg back to sender i.e. LINE + msgMeta = GeneralUtils.generate_msgMeta( + incomingMsg[:msgMeta][:replyTopic]; + senderName="wine_assistant_backend", + senderId=sessionId, + replyToMsgId=incomingMsg[:msgMeta][:msgId], + mqttBrokerAddress=config[:mqttServerInfo][:broker], + mqttBrokerPort=config[:mqttServerInfo][:port], + ) + outgoingMsg = Dict( + :msgMeta => msgMeta, + :payload => Dict( + :alias => agent.name, # will be shown in frontend as agent name + :text => "Okay. What shall we talk about?" + ) + ) + _ = GeneralUtils.sendMqttMsg(outgoingMsg) + println("--> outgoingMsg ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(outgoingMsg) + else + usermsg = incomingPayload + + if incoming_msgMeta[:msgPurpose] == "initialize" + println("\n-- Initializing... ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + end + + # send prompt + result = YiemAgent.conversation(agent; + userinput=usermsg, + maximumMsg=50) + # Ken's bot use [br] for newline character '\n' + # result = replace(result, '\n'=>"[br]") + + if incoming_msgMeta[:msgPurpose] == "initialize" + println("\n-- Initialized. Ready! waiting for request at:\n$(config[:servicetopic][:mqtttopic]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + msgMeta = GeneralUtils.generate_msgMeta( + incomingMsg[:msgMeta][:replyTopic]; + senderName="wine_assistant_backend", + senderId=string(uuid4()), + replyToMsgId=incomingMsg[:msgMeta][:msgId], + mqttBrokerAddress=config[:mqttServerInfo][:broker], + mqttBrokerPort=config[:mqttServerInfo][:port], + ) + + outgoingMsg = Dict( + :msgMeta => msgMeta, + :payload => Dict( + :alias => agent.name, # will be shown in frontend as agent name + :text => result + ) + ) + _ = GeneralUtils.sendMqttMsg(outgoingMsg) + println("\n--> outgoingMsg ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(outgoingMsg) + + + # jpg_as_juliaStr = nothing + # prompt = nothing + + # if haskey(payload, "img") + # url_or_base64 = payload["img"] + + # if startswith(url_or_base64, "http") + # # img in http + # julia_rgb_img, cv2_bgr_img = ImageUtils.url_to_cv2_image(url_or_base64) + # _, buffer = cv2.imencode(".jpg", cv2_bgr_img) + # jpg_as_pyStr = base64.b64encode(buffer).decode("utf-8") + # jpg_as_juliaStr = pyconvert(String, jpg_as_pyStr) + # else + # # img in base64 + # cv2_bgr_img = payload["img"] + # jpg_as_juliaStr = pyconvert(String, jpg_as_pyStr) + # end + # end + + end + else + # println("\n no msg") + end + + if haskey(agent.memory[:events][end], :thought) + lastAssistantAction = agent.memory[:events][end][:thought][:action_name] + if lastAssistantAction == "END_CONVER_GUIDELINE" # store thoughtDict + + # save a.memory[:shortmem][:decisionlog] to disk using JSON + println("\nsaving agent.memory[:shortmem][:decisionlog] to disk") + filename = "agent_decision_log_$(Dates.now())_$(agent.id).json" + filepath = "/appfolder/app/log/$filename" + open(filepath, "w") do io + JSON.pretty(io, agent.memory[:shortmem][:decisionlog]) + end + + # for (i, event) in enumerate(agent.memory[:events]) + # if event[:subject] == "assistant" + # # create timeline of the last 3 conversation except the last one. + # # The former will be used as caching key and the latter will be the caching target + # # in vector database + # all_recapkeys = keys(agent.memory[:recap]) #[TESTING] recap as caching + # all_recapkeys_vec = [r for r in all_recapkeys] # convert to a vector + + # # select from 1 to 2nd-to-lase event (i.e. excluding the latest which is assistant's response) + # _recapkeys_vec = all_recapkeys_vec[1:i-1] + + # # select only previous 3 recaps + # recapkeys_vec = + # if length(_recapkeys_vec) <= 3 # 1st message is a user's hello msg + # _recapkeys_vec # choose all + # else + # _recapkeys_vec[end-2:end] + # end + # #[PENDING] if there is specific data such as number, donot store in database + # tempmem = DataStructures.OrderedDict() + # for k in recapkeys_vec + # tempmem[k] = agent.memory[:recap][k] + # end + + # recap = GeneralUtils.dictToString_noKey(tempmem) + # thoughtDict = agent.memory[:events][i][:thought] # latest assistant thoughtDict + # insertSommelierDecision(recap, thoughtDict) + # else + # # skip + # end + # end + println("\nCaching conversation process done") + break + end + end + + # self terminate if too long inactivity + timediff = GeneralUtils.timedifference(latestUserMsgTimeStamp, Dates.now(), "minutes") + if timediff > timeout + + result = Dict(:exitreason => "timeout", :timestamp => Dates.now()) + put!(outputchannel, result) + println("Agent ID $(agent.id) timeout has been reached $timediff/$timeout minutes Send delete session msg ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + + # send "delete session" message to inform the main loop that this session can be deleted + sendto = + if typeof(config[:servicetopic][:mqtttopic]) <: Array + config[:servicetopic][:mqtttopic][1] + else + config[:servicetopic][:mqtttopic] + end + + msgMeta = GeneralUtils.generate_msgMeta( + sendto; + senderName="session", + senderId=sessionId, + msgPurpose="delete session", + mqttBrokerAddress=config[:mqttServerInfo][:broker], + mqttBrokerPort=config[:mqttServerInfo][:port], + ) + + outgoingMsg = Dict( + :msgMeta => msgMeta, + :payload => nothing + ) + _ = GeneralUtils.sendMqttMsg(outgoingMsg) + + try disconnect(agent.mqttClient) catch end + break + end + sleep(1) # allowing on_msg_2, asyncmove above and other process to run + end +end + +sessionDict = Dict{String,Any}() +incomingMsgChannel = (ch1=Channel(8),) # store msg that coming into servicetopic +# incommingInternalMsg = [] # st ore msg that coming into servicetopic internal management +keepaliveChannel::Channel{Dict} = Channel{Dict}(8) + +# Define the callback for receiving messages. +function onMsgCallback_1(topic, payload) + jobj = JSON.parsefile(String(payload)) + incomingMqttMsg = copy(jobj) # convert json object into julia dictionary recursively + + if occursin("keepalive", topic) + put!(keepaliveChannel, incomingMqttMsg) + else + put!(incomingMsgChannel[:ch1], incomingMqttMsg) + end +end + +mqttInstance = GeneralUtils.mqttClientInstance_v2( + config[:mqttServerInfo][:broker], + config[:servicetopic][:mqtttopic], + incomingMsgChannel, + keepaliveChannel, + onMsgCallback_1 +) + +# ------------------------------------------------------------------------------------------------ # +# this service main loop # +# ------------------------------------------------------------------------------------------------ # + +function main() + sessiontimeout = 1 * 1 * 60 # timeout in minute for each instance (day * hour * minute) + initializing = false + while true + # check if mqtt connection is still up + _ = GeneralUtils.checkMqttConnection!(mqttInstance; keepaliveCheckInterval=30) + + # initialize session 0 + if initializing == false # send init msg + sendto = + if typeof(config[:servicetopic][:mqtttopic]) <: Array + config[:servicetopic][:mqtttopic][1] + else + config[:servicetopic][:mqtttopic] + end + + msgMeta = GeneralUtils.generate_msgMeta( + sendto; + msgPurpose="initialize", + senderName="initializer", + senderId="0", + msgId= "initMsg", + replyTopic=sendto, + mqttBrokerAddress=config[:mqttServerInfo][:broker], + mqttBrokerPort=config[:mqttServerInfo][:port], + ) + + outgoingMsg = Dict( + :msgMeta => msgMeta, + :payload => Dict( # will be shown in frontend as agent name + :text => "Do you have full-bodied red wines under 100 USD. I don't have any other preferences." + ) + ) + _ = GeneralUtils.sendMqttMsg(outgoingMsg) + initializing = true + println("\n--> Initializing msg sent ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + end + + # check for new message + if !isempty(incomingMsgChannel[:ch1]) + msg = popfirst!(incomingMsgChannel[:ch1]) + println("\n<-- incomingMsg ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(msg) + + # @spawn new runAgentInstance and store it in sessionDict + # use agent's frontend id because 1 backend agent per 1 frontend session + sessionId = msg[:msgMeta][:senderId] + sessionId = replace(sessionId, "-" => "_") # julia can't use "-" in a dict key + + # check for delete session msg + if msg[:msgMeta][:msgPurpose] == "delete session" + delete!(sessionDict, sessionId) + println("sessionId $(sessionId) has been terminated ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + + # no session yet, create new session + elseif sessionId ∉ keys(sessionDict) + inputch = Channel{Dict}(8) + outputch = Channel{Dict}(8) + + process = @spawn runAgentInstance(inputch, outputch, sessionId, config, sessiontimeout) + # process = runAgentInstance(inputch, outputch, sessionId, config, sessiontimeout) #XXX use spawn version + + println("\ninstantiate agent success ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + + # call runAgentInstance() and store it in sessionDict to be able to check on it later + sessionDict[sessionId] = Dict( + :inputchannel => inputch, + :outputchannel => outputch, + :process => process, + ) + put!(sessionDict[sessionId][:inputchannel], msg) + # ongoing session + else + println("sessionId $(sessionId) existing session ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + put!(sessionDict[sessionId][:inputchannel], msg) + end + end + + # sleep is needed because MQTTClient use async. "while true" loop leave no + # chance for control to switch to on_msg() + sleep(1) + end +end + +main() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/imagetext.txt b/imagetext.txt new file mode 100644 index 0000000..fbff517 --- /dev/null +++ b/imagetext.txt @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAtGVYSWZJSSoACAAAAAYAEgEDAAEAAAABAAAAGgEFAAEAAABWAAAAGwEFAAEAAABeAAAAKAEDAAEAAAACAAAAEwIDAAEAAAABAAAAaYcEAAEAAABmAAAAAAAAAEgAAAABAAAASAAAAAEAAAAGAACQBwAEAAAAMDIxMAGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA//8AAAKgBAABAAAAgAAAAAOgBAABAAAAgAAAAAAAAABKviP0AAANMElEQVR4nO2de3BU1R3Hv7+zu9nNO9mQTQIJzxAeQkKI5MFjAgJJrEiSVmZqq2L/UdvOVO34Xx9WWzvTTutjHMdHrVbb6UyLDqC0jQ9s66ABLaIiCAgmQEhIYh6bBLLPezpnIZDdvQs3mMfde+5n5s5kz97du9nzu79zft/z+50FTExMTExMTExMTExMTExMTExMTEwMTeWWLYnigMRYITODvrsv/vU4JIUgKdXVWx0eh/s4gVv8tuC8/bt2nYeEMEiKN7H/HiLMAFGuLWC5C5JCUt/9hBmhBs7PyuoFmNR3/wgSewHpPEDU3T+CpF5AOg/gcbgfiOp8AVGu1W/9MSRDGg9QWbt5A8B+BULFlc/knxDYI81N218RD2BwDG8A2jteTkMwrAFce8fLZQgWGJCqmvqVYOxFEJZ8/XejXADXF8xd8GHbiaOnYTAMOQlsfnPn+/5u1wKQshXgX1zr+3DOT3KO+zLgWSTeEwbEsEPACGVld9lsrrO3gtNPAZqvteMBeiyTPM80NTV5YWAMbwBhhpDdeQdADwIoiHHaaYA/5O/OeXn//uf8kABpDGD0/IAzek/tOWJKdfM/X3sXEmHIOcCVEGM559gd2c45dsvW+VIagIBx/vOoNovyMCRESgOI9AJc0rtfWgOI9AJM0rtfeipqG94WByRG6pxADv4QJEdqAwDxHFLIcPr+WJBOBxihvK4ujbjjMBFndjsW/3fnzn5IiLSTQIL9kQuJIZTn8ZC0k0ApPUBVXeMKhSvNRHRhNZRzhYFWv//GjmZIhnQeoLq62qpw/uylzhcQMQV4RqwXQDKkM4DhxIz7iFAa9QSh2JrV+SNIhlRDwKoNm2YGrJZDBEpRPYHjvKIEl3zw1ustkASpPEDQan0yZucLCEmMsacgEdIYQHnd5lsAbL7qiUQ3VtTUfwuSQHLF/Cr1AKrwDlm0ASZXzB9O7uz+0KHyCmm0ASZDzA+O70e2W6wKSta3YtmGFlhtStTrCPyHK2sbqmBwmHQx/0UWVbUhOd2DpDQvFlSciX6xJNoAkzHmT5t2HoXLz156XHR9O9KzVWpCJdAGmJFjfgJEBnAYBI7lG1pEEggutTGO5Ru/DD0XdT7Rw+Ubb54Dg8Jki/nnlHTBOX0w6vzM3CHMLumSThtgMsX8jmQfrlt9Kubrlqw5BUeKTyptgBkz5mequ34VrzsJmz0Y87W2hCCWVqsbCDE8WV1fnwGDwWSJ+V2z3cgv6rnq6wsWfIXcuX3SaANMlpi/dL329Z1lN7RKow0wmWJ+rSRJpA0w2WJ+rRRJog0w2WJ+rZAk2gCTMebXSqYE2gCTNebXyhKDawNM1phfKzaDawNMtpg/rc+G6a1JsA9r/9eNrA2QYXL7R8X8G7Z+qhr2LdvjxJIPnKHN3nx2Bf9pbEfXDG3h4fkBO95+qQQBf4ThxHlNAZMl5hd3/kjnCxK8DCveyR6TNrCwss1w2gCTJeZP6bdFbfOY7B5bbez8sg7DaQNMlpi/J9cTcvuj6Zg1DNm1ASZLzO9NvDDm97m88NmDOLlgCB9s6L4mbUBcyyjaAMVbzM/AtqnF/Bvv/GRcwj4t+H0WvP2nZRgeih72ucJv2ffmzlcRJzBDxPxrxyfmH5M2sLbVENoAi/eYP2dOP/IXXH2df7zJL+oxhDbA4n2dX6zdTxXLDJA3wGRa5x9vkgygDVjjIeZniI75iYCe9lTs23V5A/CsGUMoLO0Y8zUK04PIT1Yw4Gc42GOB/wqrx8cP5KHnzOUgRAmy0GfhPKY28HvoGIrrev5RMAvH+ts/RapzbLH98uwgKlyXNwbvGmbY3pIAJcbeYYN9Duz+czGUgAbnGQf7DbC4rucfhXDFY+18wVJn+K7wrkQldMQiNdODBeUq6WJqxIE2wOK+nv+iBFy0on3SHOPCcpEudk7jW+k7b4DFW8wfiZBlS4UEzK5tv8eDveHzNDEEdA3TVSXh0o0tII17TOpZG9DlJJAhYT0n/v7I10ucCMTrAYqaVc8p7USWigTMSLhzHpqcdXso5pj+UbcFAz5CQUoQbi/hYJ815rmjcYYk4U58+bH4TalIuB+cdnK6PDX0evk6ANuhM3Q9CRyhorb+TiJ6MbJdpGpt3BotAdsswOZZvktjeecw4bVWOwJjzw3VIAmXYHgoQe3pO/c27XgJOkeXQ8BoyjZtmgbgt2rPiWIPNQm42BkIm8jlJHIsdQYnRBJetkF9gs/BH63a2OiCztG9AVgD1ieIKCpzY0ZRL/LmqUmxQFpCtA9PTxjn2/8ieXP7MH1+LyIhkFNhyqPQObo2gMqaxjoCvqN25xWvjR1atw1F/x7m6SE2oZKwTcUTEdF3q2oaNkHH6PaXQ8s2bUqyKLQLRJmRz5Wsa0V2Qey1/17vhZSNTDsPqXoffWXD4b6J+1etCUEk2IM425KpNhSsKswvfv7kycMqueVTjy6jAIEtYPkliOZGtjvzBjG7+OqJHPu7raHjWln8YSYWHkgPpZEdLXXjULn6cDOCKCA5dWQaes6khrUT0Sxvov8XAB6ADtGlByivaSwB4Y8UMUSJWH/lN4/AkTSxv+k461gKKt9yweZjoSPvVBLcWT64p/muGE5NmzGIls9c4Ep4cMU5r8ift+gfZ04cGftChWxzALH6R0x5gVS8k6jYTcsau9w7VvJaE6PaRC3B1UgRMrGKIilWMonwgh5XB3VnAF5H5v0EWq71y50IhtIC0W0Z2rxOTCMlFFuyO++FztCVAVTU1s8GqWT8ElBWcwLMOjGhXCRHy9zozbn8m9G9Li+OlLqhBTFMLa9RzxxmnB5aWdcwDzpCV5NAAp4V6fpqs+zjB3JDx2gdQMuWL6PhfhvOHShDwJ0Ox+wWOOYfUz3Pb1PQdGsbsjscocfdeR4oltj6cNuxLJw55gz/zHYFfm/EFIuQpAB/ALA+FCDoAN1IweW19bczope1nHtNWcAKw1d/vQ2+jumXmtLW/hspK/bh6+L3WkNlY2pZwmpw4PZ9TTv+Ah2giyFgxfqGLCL8Tuv5QgcYaxawr316WOcLzu1fgfHAZg+geJ32nA8Cf3x13RbtdWlGNwCLDY8RSJNuLjJxhfsfF1+nYNyYMb8X0wv7tH6YLD/3azZ4QxtAec1msUx6m5ZzhQQs1v6vBVtuB2yuzrC25OUfYTwpibE4pQYR7iiva6iB7EJQQeGiezhxIZAcvnBQGwGFaueW3CAk4IGodpuHwdV+IXb3OZSYSRxJiz4HWQOwpA0gtWIfkkvHbgABdwb8XTlgdg/IGrEMLSRhRwBnv4yWhAUceAPgB0b+V8Ypo+3EkSn97WLdTAJHqKirf5pA90S2T8sfwJoth0Mh4WhcZxxYu2M6EjwsNK3+ZFUPPqvU6orHxmDzKgzuWR362pjDA2fjK0jID08LFykge15dhO5T6dFvwPnTe9/Y+QPoiCkfAkYjiimI4y61jF/h+iM7X1C+OzvU+QLxdMl7WUgVpeDjTKAv81LnCxSPA+7d0R5cfMbS9a2hohUV7i6vrV8DHaEbA6irqxPF28+Logq1ApBYGb+huv9RiO5Jdo+/AQQHREofRRmF6mfKHMaiSvWNJhnwfHX11gsCgw7QjQH0c/vPQFgc2S42ZBAbM8Ti7MxwwxB7APTmjn+1kC2nA8xxWR0U2GfFLkubf307MlwqmcNERd7E/p9AJ0z5JFBQeWP9UgAvA+HlX2LiVtVwFEmpsVfhOgs8SO+zIXnQBrfTh+ZvdGEgItd/PCBrALa8dgQ6c8H9djjmnkBGTRMoQf1aYihw5p3DyUPZ4DxidRC0cmZh0a6240fHvoWp4SaBDz7IKvZ9vIdAUcWUItdf7NMXzxx8dya++F+4ACXgnH84My2hatu2bZNX167HIaCi+eP71To/OT1G4WWcsXhlG5IzoockIlpxasB/r9RDQGj1j+HvBEqIdJ/lN32BVOfUVf6OF2J1MH3aeZz+PFr5JWDNzPkL/3b6+JGJiVv17gHE6p9a7d+s67rgmqVt+TUeyC4YwMzF6vsKXVwdJOkMoKJm8/dAVKO20re0+iSMRvHaU0hMUZ0wrquord+KKYKmqtjD5rd+DoIo+ojK/NFceBlnuLuTMdQXLQFw8F74lMX73nk9fLHCqAkh1oDlKbXOF4gvSO1LMjIEcsJmeQLAtyf/2pNMeW3jTYz4rsm+bjxAxBub/7Vzx6Rec7LLvhm3HwJR/mReN37gk/6z9ZM6CSTYf2N2/pWgPK8Hv4YRPUDFDTfnkM3yBCc+4UZHHFWRhpZoZciwa5vy9HsDGI6sJee8jRMmfEt44qRwf/DeyZoQTr0UPAFU1jW8AiBsW5b81ARU5mrabgh7zw6hbTBq/eHVvU07xLY1hmLKpWCTqcU0AMkxDUByTAOQHF2Vhk0kZwZ92D6kbdFNidr31bhIYwCiS4MSdaxWzCFAckwDkBzTACTHkHMArtCzYPzNcX1ThXS75buJiYmJiYmJiYmJiYmJiYmJiYmJCa7I/wEjf8WA9CzV0gAAAABJRU5ErkJggg== \ No newline at end of file diff --git a/src/interface.jl b/src/interface.jl index eea2c7c..2b44ac3 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -3,7 +3,7 @@ module interface export addNewMessage, conversation, decisionMaker, reflector, generatechat, generalconversation, detectWineryName, generateSituationReport -using JSON3, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, +using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, DataFrames, CSV using GeneralUtils using ..type, ..util, ..llmfunction @@ -48,6 +48,7 @@ macro executeStringFunction(functionStr, args...) end + """ Think and choose action # Arguments @@ -64,420 +65,46 @@ end # Example ```jldoctest julia> config = Dict( - :mqttServerInfo => Dict( - :description => "mqtt server info", - :port => 1883, - :broker => "mqtt.yiem.cc" - ), - :externalservice => Dict( - :text2textinstruct => Dict( - :mqtttopic => "/loadbalancer/requestingservice", - :description => "text to text service with instruct LLM", - :llminfo => Dict( - :name => "llama3instruct" - ) - ), - ) - ) + "mqttServerInfo" => Dict( + "description" => "mqtt server info", + "port" => 1883, + "broker" => "mqtt.yiem.cc" + ), + "externalservice" => Dict( + "text2textinstruct" => Dict( + "mqtttopic" => "/loadbalancer/requestingservice", + "description" => "text to text service with instruct LLM", + "llminfo" => Dict( + "name" => "llama3instruct" + ) + ), + ) + ) julia> output_thoughtDict = Dict( - :thought_1 => "The customer wants to buy a bottle of wine. This is a good start!", - :action_1 => Dict{Symbol, Any}( - :action=>"CHATBOX", - :input=>"What occasion are you buying the wine for?" + "thought_1" => "The customer wants to buy a bottle of wine. This is a good start!", + "action_1" => Dict{String, Any}( + "action"=>"CHAT_BOX", + "input"=>"What occasion are you buying the wine for?" ), - :observation_1 => "" + "observation_1" => "" ) ``` - -# TODO - [] update docstring - [] use customerinfo - [] user storeinfo -# Signature """ -function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 - ) where {T<:agent} - println("\nExecuting YiemAgent decisionMaker()") - # lessonDict = copy(JSON3.read("lesson.json")) - - # lesson = - # if isempty(lessonDict) - # "" - # else - # lessons = Dict{Symbol, Any}() - # for (k, v) in lessonDict - # lessons[k] = lessonDict[k][:lesson] - # end - - # """ - # You have attempted to help the user before and failed, either because your reasoning for the - # recommendation was incorrect or your response did not exactly match the user expectation. - # The following lesson(s) give a plan to avoid failing to help the user in the same way you - # did previously. Use them to improve your strategy to help the user. - - # Here are some lessons in JSON format: - # $(JSON3.write(lessons)) - - # When providing the thought and action for the current trial, that into account these failed - # trajectories and make sure not to repeat the same mistakes and incorrect answers. - # """ - # end - - recentevents_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recentevents; - includelatest=true) - timeline = createTimeline(a.memory[:events]; eventindex=recentevents_ind) - - recentchat_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recentevents; - includelatest=true) - recentchat = createChatLog(a.chathistory; index=recentchat_ind) - # recentEventsDict = createEventsLog(recentevents; index=recent_ind) - - - # # recap as caching - # # query similar result from vectorDB - # recapkeys = keys(a.memory[:recap]) - # _recapkeys_vec = [i for i in recapkeys] - - # # select recent keys - # _recentRecapKeys = - # if length(a.memory[:recap]) <= 3 # 1st message is a user's hello msg - # _recapkeys_vec - # elseif length(a.memory[:recap]) > 3 - # l = length(a.memory[:recap]) - # _recapkeys_vec[l-2:l] - # end - - # # get recent recap - # _recentrecap = OrderedDict() - # for (k, v) in a.memory[:recap] - # if k ∈ _recentRecapKeys - # _recentrecap[k] = v - # end - # end - - # recentrecap = GeneralUtils.dictToString_noKey(_recentrecap) - # similarDecision = a.func[:similarSommelierDecision](recentrecap) - similarDecision = nothing #CHANGE - - if similarDecision !== nothing - responsedict = similarDecision - return responsedict - else - # context = # may b add wine name instead of the hold wine data is better - # if length(a.memory[:shortmem][:available_wine]) != 0 - # winenames = [] - # for (i, wine) in enumerate(a.memory[:shortmem][:available_wine]) - # name = "$i) $(wine["wine_name"]) " - # push!(winenames, name) - # end - # availableWineName = join(winenames, ',') - # "Available wines you've found in your inventory so far: $availableWineName" - # else - # "" - # end - - systemmsg = - """ - Your name is $(a.name). You are a sommelier for website-based $(a.retailername)'s wine store. You are working under your mentor supervision. - Your goal includes: - 1) Establish a connection with the customer by greeting them warmly - 2) Guide them to select the best wines only from your store's inventory that align with their preferences. - - Your responsibility includes: - 1) According to the store's policy and guidelines, make an informed decision about what you need to do to achieve the goal - 2) Value your mentor's suggestions. - - Your responsibility does NOT includes: - 1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - 2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - At each round of conversation, you will be given the following information: - context: additional information about the current situation - - You should then respond to the user with interleaving Plan, Action_name, Action_input: - 1) plan: Based on the current situation, state a complete action plan to complete the task. Be specific. - 2) action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the available tool name - 3) action_input: The input to the action you are about to perform according to your plan. - - You should only respond in JSON format as described below: - { - "plan": "...", - "action_name": "...", - "action_input": "..." - } - - Let's begin! - """ - requiredKeys = [:plan, :action_name, :action_input] - database_search_result = - if length(a.memory[:shortmem][:db_search_result]) != 0 - availableWineToText(a.memory[:shortmem][:db_search_result]) - else - "N/A" - end - - errornote = "N/A" - response = nothing # placeholder for show when error msg show up - - for attempt in 1:maxattempt - if attempt > 1 - println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - # QandA = generatequestion(a, a.func[:text2textInstructLLM], timeline) - - context = - """ - - - - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - If you found wines in the store's database, they are in stock - - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. Once you have these information, you can check your inventory. - - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. Other wine characteristics useful for CHECKINVENTORY tools are allowed. - - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. - - If the user is ending the conversation, say goodbye and invite them to come back next time. - - Spicy foods should not be paired with medium and full-bodied red wines. - - We do not sell organic, sustainability and sulfite wine. - - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. - - - - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - Your store carries only wine. - - Vintage 0 means non-vintage. - - - - CHATBOX which you can use to talk with the user. Be specific. - - CHECKWINE allows you to check information about wines you want in your inventory's database. The input must be supported search criteria includeing: wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. - Example query 1: "Dry, full-bodied red wine from Burgundy region, France or Tuscany region, Italy. Merlot varietal. price 100 to 1000 USD." - Example query 2: "Red wine, bold intensity, price under 300 USD" - - PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present. - - ENDCONVERSATION which you can use to properly end the conversation with the user. Input is a dialogue where you wrap up the conversation, thank the user, and invite them to return next time. - - $(a.memory[:shortmem][:scratchpad]) - Remark: $errornote - - """ - - unformatPrompt = - [ - Dict(:name => "system", :text => systemmsg), - ] - - unformatPrompt = vcat(unformatPrompt, recentchat) - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) - # add info - prompt = prompt * context - - response = a.func[:text2textInstructLLM](prompt; senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - response = GeneralUtils.remove_french_accents(response) - # response = replace(response, '$'=>"USD") - think, response = GeneralUtils.extractthink(response) - - responsedict = nothing - try - responsedict = copy(JSON3.read(response)) - catch - println("\nERROR YiemAgent decisionMaker() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check whether all answer's key points are in responsedict - _responsedictKey = keys(responsedict) - responsedictKey = [i for i in _responsedictKey] # convert into a list - is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] - - if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) - errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent decisionMaker() $errornote ----(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif !all(is_requiredKeys_in_responsedictKey) - zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) - missingkeys = [requiredKeys[i] for i in zeroind] - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] - # count = 0 - # for i ∈ ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"] - # if occursin(i, response) - # count += 1 - # end - # end - # if count > 1 - # errornote = "You must use only one function" - # println("\nERROR YiemAgent decisionMaker() $errornote\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # # check whether response has all header - # detected_kw = GeneralUtils.detect_keyword(header, response) - # kwvalue = [i for i in values(detected_kw)] - # zeroind = findall(x -> x == 0, kwvalue) - # missingkeys = [header[i] for i in zeroind] - # if 0 ∈ values(detected_kw) - # errornote = "$missingkeys are missing from your previous response" - # println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # elseif sum(values(detected_kw)) > length(header) - # errornote = "Your previous attempt has duplicated points" - # println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # responsedict = GeneralUtils.textToDict(response, header; - # dictKey=dictkey, symbolkey=true) - - if responsedict[:action_name] ∉ ["CHATBOX", "CHECKWINE", "PRESENTBOX", "ENDCONVERSATION"] - errornote = "Your previous attempt didn't use the given functions" - println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict[:action_name])", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # checkFlag = false - # for i ∈ Symbol.(dictkey) - # if length(responsedict[i]) == 0 - # errornote = "$i is empty" - # println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # checkFlag = true - # break - # end - # end - # checkFlag == true ? continue : nothing - - # # check if there are more than 1 key per categories - # checkFlag = false - # for i ∈ Symbol.(dictkey) - # matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) - # if length(matchkeys) > 1 - # errornote = "Your previous attempt has more than one key per categories" - # println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # checkFlag = true - # break - # end - # end - # checkFlag == true ? continue : nothing - - # # check if action_name = CHECKINVENTORY and action_input has the words "pairs well" or - # # "pair well" in it because it is not a valid query. - # detected_kw = GeneralUtils.detect_keyword(["pair", "pairs", "pairing", "well"], responsedict[:action_input]) - # if responsedict[:action_name] == "CHECKINVENTORY" && sum(values(detected_kw)) != 0 - # errornote = "In your previous attempt, action_input for CHECKINVENTORY function was $(responsedict[:action_name]). It was not specific enough." - # println("\nERROR YiemAgent decisionMaker() $errornote => $(responsedict[:action_input]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(Dict(responsedict)) - - # check whether an agent recommend wines before checking inventory or recommend wines - # outside its inventory - # ask LLM whether there are any winery mentioned in the response - mentioned_winery = detectWineryName(a, response) - if mentioned_winery != "None" - mentioned_winery = String.(strip.(split(mentioned_winery, ","))) - - # check whether the wine is in event - isWineInEvent = false - for winename in mentioned_winery - for event in a.memory[:events] - if event[:outcome] !== nothing && occursin(winename, event[:outcome]) - isWineInEvent = true - break - end - end - end - - # then the agent is not supposed to recommend the wine - if isWineInEvent == false - errornote = "You recommended wines that are not in your inventory before. Please only recommend wines that you have previously found in your inventory." - println("\nERROR YiemAgent decisionMaker() $errornote $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - end - - delete!(responsedict, :mentioned_winery) - - # check whether responsedict[:action_input] is the same as previous dialogue - if responsedict[:action_input] == a.chathistory[end][:text] - errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." - println("\nERROR YiemAgent decisionMaker() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - println("\nYiemAgent decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - println("\n$response") - - evaluationdict = evaluator(a, timeline, responsedict, context) - if evaluationdict[:approved] == "no" - mentor_comment = evaluationdict[:suggestion] - errornote = "Your previous attempt was not good enough. Please try again. Here is the mentor's suggestion: $mentor_comment" - println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> \n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # store for later training - responsedict[:system] = systemmsg - responsedict[:unformatPrompt] = unformatPrompt - responsedict[:recentchat] = recentchat - responsedict[:prompt] = prompt - responsedict[:context] = context - responsedict[:think] = think - responsedict[:response] = response - # responsedict[:QandA] = QandA - - # check whether there is a file path exists before writing to it - if !haskey(a.memory[:shortmem], :decisionlog) - a.memory[:shortmem][:decisionlog] = [responsedict] - else - push!(a.memory[:shortmem][:decisionlog], responsedict) - end - - # # save to filename ./log/decisionlog.txt - # println("\nsaving YiemAgent decisionMaker() to disk ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # filename = "agent_decision_log_$(a.id).json" - # filepath = "/appfolder/app/log/$filename" - # # check whether there is a file path exists before writing to it - # if !isfile(filepath) - # decisionlist = [responsedict] - # println("Creating file $filepath") - # open(filepath, "w") do io - # JSON3.pretty(io, decisionlist) - # end - # else - # # read the file and append new data - # decisionlist = copy(JSON3.read(filepath)) - # push!(decisionlist, responsedict) - # println("Appending new data to file $filepath") - # open(filepath, "w") do io - # JSON3.pretty(io, decisionlist) - # end - # end - # println("\nYiemAgent decisionMaker() saved to disk is done. agent $(a.id)") - - - responsedict[:prompt] = prompt - return responsedict - end - error("DecisionMaker failed to generate a thought ", response) - end -end -# function decisionMaker(a::T; recent::Integer=10 +# function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 # ) where {T<:agent} - -# # lessonDict = copy(JSON3.read("lesson.json")) +# println("\nExecuting YiemAgent decisionMaker()") +# # lessonDict = copy(JSON.parsefile("lesson.json")) # # lesson = # # if isempty(lessonDict) # # "" # # else -# # lessons = Dict{Symbol, Any}() +# # lessons = Dict{String, Any}() # # for (k, v) in lessonDict # # lessons[k] = lessonDict[k][:lesson] # # end @@ -489,300 +116,263 @@ end # # did previously. Use them to improve your strategy to help the user. # # Here are some lessons in JSON format: -# # $(JSON3.write(lessons)) +# # $(JSON.json(lessons)) # # When providing the thought and action for the current trial, that into account these failed # # trajectories and make sure not to repeat the same mistakes and incorrect answers. # # """ # # end -# recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) -# recentevents = a.memory[:events][recent_ind] -# timeline = createTimeline(recentevents; eventindex=recent_ind) +# # recentevents_ind = GeneralUtils.recentElementsIndex( +# # length(a.memory["events"]), recentevents; includelatest=true) -# # recap as caching -# # query similar result from vectorDB -# recapkeys = keys(a.memory[:recap]) -# _recapkeys_vec = [i for i in recapkeys] +# systemmsg = +# """ +# # Store Policy +# - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. +# - If you found wines in the store's database, they are in stock. +# - You can only recommend wines that are currently in our inventory +# - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. +# - Ask the user one question at a time. +# - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. +# - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. +# - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. +# - Spicy foods should be paired only with light red wines. +# - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such. +# - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. -# # select recent keys -# _recentRecapKeys = -# if length(a.memory[:recap]) <= 3 # 1st message is a user's hello msg -# _recapkeys_vec -# elseif length(a.memory[:recap]) > 3 -# l = length(a.memory[:recap]) -# _recapkeys_vec[l-2:l] -# end +# # Store Guidelines +# - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. +# - Customer may provide images for you to look up. +# - Encourage the customer to explore different options and try new things. +# - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. +# - Your store carries only wine. +# - Vintage 0 means non-vintage. +# - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. -# # get recent recap -# _recentrecap = OrderedDict() -# for (k, v) in a.memory[:recap] -# if k ∈ _recentRecapKeys -# _recentrecap[k] = v -# end -# end - -# recentrecap = GeneralUtils.dictToString_noKey(_recentrecap) -# # similarDecision = a.func[:similarSommelierDecision](recentrecap) -# similarDecision = nothing #CHANGE +# # Situation +# Your customer is coming into the store -# if similarDecision !== nothing -# responsedict = similarDecision -# return responsedict -# else +# # Role +# Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store. You are working under your mentor supervision. + +# # Objective +# 1. Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. +# 2. Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. + +# # Responsibility Includes +# 1. According to the store's policy and guidelines, make an informed decision about what you need to do to achieve the objective +# 2. Keep the conversation with the customer going smoothly +# 3. Obey your mentor's suggestions. + +# # Responsibility Does NOT Include + +# 1. Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. +# 2. Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. +# 3. Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. + +# # You should then respond to the user with interleaving plan, action_name, action_input +# 1) plan: Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. +# 2) action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name +# 3) action_input: The input to the action you are about to perform according to your plan. +# After the action is executed you gets "action_result". It is the output from the action you selected. + +# # You should only respond in JSON format as described below +# "plan": "...", +# "action_name": "...", +# "action_input": "..." + +# # Available Actions +# - **CHAT_BOX** which you can use to talk with the user. +# - **CHECK_WINE** allows you to check information about wines you want in your inventory's database. The input is text that specify supported search criteria includeing: retailer_name, wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. +# - Example query 1: "Dry, full-bodied red wine from 1) region: Burgundy, country: France or 2) region: Tuscany, country: Italy. Grape varietal: Merlot or Syrah. price 100 to 1000 USD." +# - Example query 2: "Red or white wine, medium tannin, price under 700 USD" +# - Example query 3: "white wine, region: Tuscany or Bordeaux, country: Italy or France +# - **PRESENT_WINE_GUIDELINE** which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. +# - **END_CONVER_GUIDELINE** which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow. +# """ -# header = ["Thought:", "Plan:", "Action_name:", "Action_input:"] -# dictkey = ["thought", "plan", "action_name", "action_input"] -# context = # may b add wine name instead of the hold wine data is better -# if length(a.memory[:shortmem][:available_wine]) != 0 -# winenames = [] -# for (i, wine) in enumerate(a.memory[:shortmem][:available_wine]) -# name = "$i) $(wine["wine_name"]) " -# push!(winenames, name) -# end -# availableWineName = join(winenames, ',') -# "Available wines you've found in your inventory so far: $availableWineName" -# else -# "" +# requiredKeys = ["plan", "action_name", "action_input"] +# context = +# """ +# +# +# $(GeneralUtils.dict_to_string_html(a.memory["shortmem"])) +# +# + +# """ + +# # add context to text of the latest message (in the front). +# # use for loop because in openai format, each msg may contain both text and image. +# for d in a.chathistory[end]["content"] +# if d["type"] == "text" +# d["text"] = context * d["text"] +# break # end - -# errornote = "N/A" -# response = nothing # placeholder for show when error msg show up - -# for attempt in 1:10 -# if attempt > 1 -# println("\nYiemAgent decisionMaker() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# end - -# QandA = generatequestion(a, a.func[:text2textInstructLLM]; recent=3) -# systemmsg = -# """ -# Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. -# Your goal includes: -# 1) Establish a connection with the customer by greeting them warmly -# 2) Guide them to select the best wines only from your store's inventory that align with their preferences - -# Your responsibility includes: -# 1) Make an informed decision about what you need to do to achieve the goal -# 2) Thanks the user when they don't need any further assistance and invite them to comeback next time - -# Your responsibility does NOT includes: -# 1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. -# 2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. -# 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - -# At each round of conversation, you will be given the following information: -# Your recent events: latest 5 events of the situation -# Your Q&A: the question and answer you have asked yourself - -# You must follow the following guidelines: -# - Focus on the latest event -# - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. -# - All wines in your inventory are always in stock -# - Approach each customer with open-ended questions to understand their preferences, budget, and occasion. This will help you guide the conversation naturally while gathering essential insights. Once you have this information, you can efficiently check your inventory for the best match. -# - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. -# - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. -# - Spicy foods should not be paired with medium and full-bodied red wines. - -# You should follow the following guidelines: -# - When searching an inventory, search as broadly as possible based on the information you have gathered so far. -# - Encourage the customer to explore different options and try new things. -# - If you are unable to locate the desired item after multiple attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - -# For your information: -# - Your store carries only wine. -# - Vintage 0 means non-vintage. - -# You should then respond to the user with interleaving Thought, Plan, Action_name, Action_input: -# 1) Thought: Articulate your current understanding and consider the current situation. -# 2) Plan: Based on the current situation, state a complete action plan to complete the task. Be specific. -# 3) Action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the following function names: -# - CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific. -# - CHECKINVENTORY allows you to check information about wines you want in your inventory. The input should be a specific search term in verbal English. A good search term should include details such as winery, wine name, vintage, region, country, wine type, grape varietal, tasting notes, wine price, occasion, food to be paired with wine, intensity, tannin, sweetness, acidity. -# Invalid query example: red wine that pair well with spicy food. -# - PRESENTBOX which you can use to present wines you have found in your inventory to the user. The input are wine names that you want to present. -# - ENDCONVERSATION which you can use when the user has finished their conversation with you, so that you can properly end the conversation. Input is "NA". -# 4) Action_input: The input to the action you are about to perform. This should be aligned with the plan - - -# You should only respond in format as described below: -# Thought: ... -# Plan: ... -# Action_name: ... -# Action_input: ... - -# Let's begin! - -# $context -# Your recent events: -# $timeline -# Your Q&A: -# $QandA -# P.S. $errornote -# """ - -# unformatPrompt = -# [ -# Dict(:name => "system", :text => systemmsg), -# ] - -# # found wine is "count 0" invalid return from CHECKINVENTORY() -# # check if winename in shortmem occurred in chathistory. if not, skip decision and imediately use PRESENTBOX -# # if length(a.memory[:shortmem][:found_wine]) != 0 -# # # check if wine name mentioned in recentevents, only check first wine name is enough -# # # because agent will recommend every wines it found each time. -# # winenames = [] -# # for wine in a.memory[:shortmem][:found_wine] -# # push!(winenames, wine["wine_name"]) -# # end - -# # for winename in winenames -# # if !occursin(winename, chathistory) -# # println("\nYiem decisionMaker() found wines from DB ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# # d = Dict( -# # :thought=> "The user is looking for a wine tahat matches their intention and budget. I've checked the inventory and found wines that match the customer's criteria. I will present the wines to the customer.", -# # :plan=> "1) I'll provide detailed introductions of the wines I just found to the user. 2) I'll explain how the wine could match the user's intention and what its effects might mean for the user's experience. 3) If multiple wines are available, I'll highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. 4) I'll provide my personal recommendation.", -# # :action_name=> "PRESENTBOX", -# # :action_input=> "I need to present to the user the following wines: $winenames") -# # a.memory[:shortmem][:found_wine] = [] # clear because PRESENTBOX command is issued. This is to prevent decisionMaker() keep presenting the same wines -# # result = (systemmsg=systemmsg, usermsg=usermsg, unformatPrompt=unformatPrompt, result=d) -# # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# # pprintln(Dict(d)) -# # return result -# # end -# # end -# # end - -# # change qwen format put in model format -# prompt = GeneralUtils.formatLLMtext(unformatPrompt, "qwen3") -# response = a.func[:text2textInstructLLM](prompt) -# response = GeneralUtils.remove_french_accents(response) -# response = replace(response, "**"=>"") -# response = replace(response, "***"=>"") -# response = replace(response, "<|eot_id|>"=>"") -# response = GeneralUtils.deFormatLLMtext(response, "qwen3") - -# # check if response contain more than one functions from ["CHATBOX", "CHECKINVENTORY", "ENDCONVERSATION"] -# count = 0 -# for i ∈ ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"] -# if occursin(i, response) -# count += 1 -# end -# end -# if count > 1 -# errornote = "You must use only one function" -# println("\nERROR YiemAgent decisionMaker() $errornote\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end - -# # check whether response has all header -# detected_kw = GeneralUtils.detect_keyword(header, response) -# kwvalue = [i for i in values(detected_kw)] -# zeroind = findall(x -> x == 0, kwvalue) -# missingkeys = [header[i] for i in zeroind] -# if 0 ∈ values(detected_kw) -# errornote = "$missingkeys are missing from your previous response" -# println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# elseif sum(values(detected_kw)) > length(header) -# errornote = "Your previous attempt has duplicated points" -# println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end - -# responsedict = GeneralUtils.textToDict(response, header; -# dictKey=dictkey, symbolkey=true) - -# if responsedict[:action_name] ∉ ["CHATBOX", "CHECKINVENTORY", "PRESENTBOX", "ENDCONVERSATION"] -# errornote = "Your previous attempt didn't use the given functions" -# println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end - -# checkFlag = false -# for i ∈ Symbol.(dictkey) -# if length(responsedict[i]) == 0 -# errornote = "$i is empty" -# println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# checkFlag = true -# break -# end -# end -# checkFlag == true ? continue : nothing - -# # check if there are more than 1 key per categories -# checkFlag = false -# for i ∈ Symbol.(dictkey) -# matchkeys = GeneralUtils.findMatchingDictKey(responsedict, i) -# if length(matchkeys) > 1 -# errornote = "Your previous attempt has more than one key per categories" -# println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# checkFlag = true -# break -# end -# end -# checkFlag == true ? continue : nothing - -# # check if action_name = CHECKINVENTORY and action_input has the words "pairs well" or -# # "pair well" in it because it is not a valid query. -# detected_kw = GeneralUtils.detect_keyword(["pair", "pairs", "pairing", "well"], responsedict[:action_input]) -# if responsedict[:action_name] == "CHECKINVENTORY" && sum(values(detected_kw)) != 0 -# errornote = "In your previous attempt, action_input for CHECKINVENTORY function is invalid" -# println("\nERROR YiemAgent decisionMaker() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end - -# println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# pprintln(Dict(responsedict)) - -# # check whether an agent recommend wines before checking inventory or recommend wines -# # outside its inventory -# # ask LLM whether there are any winery mentioned in the response -# mentioned_winery = detectWineryName(a, response) -# if mentioned_winery != "None" -# mentioned_winery = String.(strip.(split(mentioned_winery, ","))) - -# # check whether the wine is in event -# isWineInEvent = false -# for winename in mentioned_winery -# for event in a.memory[:events] -# if event[:outcome] !== nothing && occursin(winename, event[:outcome]) -# isWineInEvent = true -# break -# end -# end -# end - -# # then the agent is not supposed to recommend the wine -# if isWineInEvent == false -# errornote = "You recommended wines that are not in your inventory before. Please only recommend wines that you have previously found in your inventory." -# println("\nERROR YiemAgent decisionMaker() $errornote $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end -# end - -# delete!(responsedict, :mentioned_winery) -# responsedict[:systemmsg] = systemmsg -# responsedict[:unformatPrompt] = unformatPrompt -# responsedict[:QandA] = QandA - -# # check whether responsedict[:action_input] is the same as previous dialogue -# if responsedict[:action_input] == a.chathistory[end][:text] -# errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." -# println("\nERROR YiemAgent decisionMaker() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end - -# # println("\n$prompt", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# # println("\n$response") - -# responsedict[:prompt] = prompt -# return responsedict -# end -# error("DecisionMaker failed to generate a thought ", response) # end -# end +# errornote = "N/A" +# response = nothing # placeholder for show when error msg show up + +# for attempt in 1:maxattempt +# if attempt > 1 +# println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# end + +# msg = Dict( +# "model" => "gemma-4-E4B-it-UD-Q4_K_XL", +# "messages" => a.chathistory, +# "temperature" => 0.7 +# ) + +# response = a.context.text2textInstructLLM(a.id, msg) +# response = GeneralUtils.clean_json_response(response) + +# response = GeneralUtils.remove_french_accents(response) +# think, response = GeneralUtils.extractthink(response) +# response = String(split(response, ", observation")[1]) # in case LLM generate observation key which it isn't supposed to +# response = strip(response) +# responsedict = nothing +# try +# _responsedict = JSON.parse(response) +# responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys) +# catch +# println("\nERROR YiemAgent decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# # check whether all answer's key points are in responsedict +# ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) +# if !ispass +# errornote = errormsg +# println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") +# continue +# end + +# if responsedict["action_name"] ∉ ["CHAT_BOX", "CHECK_WINE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] +# errornote = "Your previous attempt didn't use the given functions" +# println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# end + +# # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# # pprintln(responsedict) + +# return responsedict +# end +# error("DecisionMaker failed to generate a thought ", response) +# end +function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10 + ) where {T<:agent} + println("\nExecuting YiemAgent decisionMaker()") + # lessonDict = copy(JSON.parsefile("lesson.json")) + + # lesson = + # if isempty(lessonDict) + # "" + # else + # lessons = Dict{String, Any}() + # for (k, v) in lessonDict + # lessons[k] = lessonDict[k][:lesson] + # end + + # """ + # You have attempted to help the user before and failed, either because your reasoning for the + # recommendation was incorrect or your response did not exactly match the user expectation. + # The following lesson(s) give a plan to avoid failing to help the user in the same way you + # did previously. Use them to improve your strategy to help the user. + + # Here are some lessons in JSON format: + # $(JSON.json(lessons)) + + # When providing the thought and action for the current trial, that into account these failed + # trajectories and make sure not to repeat the same mistakes and incorrect answers. + # """ + # end + + # recentevents_ind = GeneralUtils.recentElementsIndex( + # length(a.memory["events"]), recentevents; includelatest=true) + + requiredKeys = ["plan", "action_name", "action_input"] + context = + """ + + + $(GeneralUtils.dict_to_string_html(a.memory["shortmem"])) + + + """ + + # add context to text of the latest message (in the front). + # use for loop because in openai format, each msg may contain both text and image. + for d in a.chathistory[end]["content"] + if d["type"] == "text" + d["text"] = context * d["text"] + break + end + end + errornote = "N/A" + response = nothing # placeholder for show when error msg show up + + for attempt in 1:maxattempt + if attempt > 1 + println("\nYiemAgent decisionMaker() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + end + + msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => a.chathistory, + "temperature" => 0.7 + ) + + response = a.context.text2textInstructLLM(a.id, msg) + response = GeneralUtils.clean_json_response(response) + + response = GeneralUtils.remove_french_accents(response) + think, response = GeneralUtils.extractthink(response) + response = String(split(response, ", observation")[1]) # in case LLM generate observation key which it isn't supposed to + response = strip(response) + responsedict = nothing + if occursin(requiredKeys[2], response) + try + _responsedict = JSON.parse(response) + responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys) + catch + println("\nERROR YiemAgent decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + # fall back to normal text because LLM default to natural chat when it didn't use action_call + else + responsedict = OrderedDict( + "plan"=> "I will talk to the user", + "action_name"=> "CHAT_BOX", + "action_input"=> response[2:end-1] # remove { } at the front and back that added by clean_json_response + ) + end + + # check whether all answer's key points are in responsedict + ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) + if !ispass + errornote = errormsg + println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") + continue + end + + if responsedict["action_name"] ∉ ["CHAT_BOX", "CHECK_WINE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] + errornote = "Your previous attempt didn't use the given functions" + println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end + + # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # pprintln(responsedict) + + return responsedict + end + error("DecisionMaker failed to generate a thought ", response) +end @@ -804,10 +394,6 @@ serving as a heuristic to steer the search algorithm towards the most promising julia> ``` -# TODO - - [WORKING] Implement the function. - - # Signature """ function evaluator(a::T1, timeline, decisiondict, evaluateecontext @@ -838,6 +424,7 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext - The trainee's plan, action_name, and action_input must be logically consistent - The trainee's action_input should be in a proper format as specified by the tools. + - The trainee's action name and action input should make sense. For example, if the trainee isn't finished talking, he shouldn't use the END_CONVER_GUIDELINE tool. 1) trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question. @@ -848,7 +435,7 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext 2) decision_evaluation: - Examine how the trainee's decisions align with the store's policies and guidelines before proceeding. 3) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only. - 4) approved: Based on suggestion, decide whether to let the trainee proceed with the decision. Can be "yes" or "no" + 4) approval: Can be "yes" or "no". "no" if the suggestion contradict the trainee's decision; otherwise, it is "yes". @@ -856,13 +443,13 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext "trajectory_evaluation": "...", "decision_evaluation": "...", "suggestion": "...", - "approved": "...", + "approval": "...", } Let's begin! """ - requiredKeys = [:trajectory_evaluation, :decision_evaluation, :approved, :suggestion] + requiredKeys = [:trajectory_evaluation, :decision_evaluation, :approval, :suggestion] errornote = "N/A" for attempt in 1:10 @@ -879,7 +466,7 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext $evaluateecontext - {plan: $(decisiondict[:plan]), action_name: $(decisiondict[:action_name]), action_input: $(decisiondict[:action_input])} + {plan: $(decisiondict["plan"]), action_name: $(decisiondict["action_name"]), action_input: $(decisiondict["action_input"])} P.S. $errornote @@ -887,17 +474,15 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext unformatPrompt = [ - Dict(:name => "system", :text => systemmsg), + Dict("name" => "system", "text" => systemmsg), ] # put in model format prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) # add info prompt = prompt * context - println("") - println(prompt) - response = a.func[:text2textInstructLLM](prompt; senderId=a.id) + response = a.context.text2textInstructLLM(prompt; senderId=a.id) response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) response = GeneralUtils.remove_french_accents(response) # response = replace(response, '$'=>"USD") @@ -905,26 +490,17 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext responsedict = nothing try - responsedict = copy(JSON3.read(response)) + responsedict = copy(JSON.parsefile(response)) catch println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end # check whether all answer's key points are in responsedict - _responsedictKey = keys(responsedict) - responsedictKey = [i for i in _responsedictKey] # convert into a list - is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] - - if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) - errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif !all(is_requiredKeys_in_responsedictKey) - zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) - missingkeys = [requiredKeys[i] for i in zeroind] - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys) + if !ispass + errornote = errormsg + println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") continue end @@ -946,15 +522,15 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext # decisionlist = [responsedict] # println("Creating file $filepath") # open(filepath, "w") do io - # JSON3.pretty(io, decisionlist) + # JSON.pretty(io, decisionlist) # end # else # # read the file and append new data - # decisionlist = copy(JSON3.read(filepath)) + # decisionlist = copy(JSON.parsefile(filepath)) # push!(decisionlist, responsedict) # println("Appending new data to file $filepath") # open(filepath, "w") do io - # JSON3.pretty(io, decisionlist) + # JSON.pretty(io, decisionlist) # end # end @@ -966,91 +542,70 @@ end """ Chat with llm. -# Arguments - `a::agent` - an agent - -# Return - None +# Example userinput -# Example -```jldoctest -julia> using JSON3, UUIDs, Dates, FileIO, MQTTClient, ChatAgent -julia> const mqttBroker = "mqtt.yiem.cc" -julia> mqttclient, connection = MakeConnection(mqttBroker, 1883) -julia> tools=Dict( # update input format - "askbox"=>Dict( - :description => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", - :input => "Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}", - :output => "" , - :func => nothing, - ), +image_path = "test/large_image.png" +image_bytes = read(image_path) +base64_string = base64encode(image_bytes) + +# 2. Match the MIME type according to your file extension (e.g., png, jpeg) +mime_type = "image/png" +data1_uri = "data:;base64," + +# 3. Construct payload with the Data URI +message => Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Describe this image for me"), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data_uri) ) -julia> msgMeta = Dict( - :msgPurpose=> "updateStatus", - :from=> "agent", - :to=> "llmAI", - :requestresponse=> "request", - :sendto=> "", # destination topic - :replyTo=> "agent/api/v0.1.0/txt/response", # requester ask responseer to send reply to this topic - :repondToMsgId=> "", # responseer is responseing to this msg id - :taskstatus=> "", # "complete", "fail", "waiting" or other status - :timestamp=> Dates.now(), - :msgId=> "$(uuid4())", -) -julia> a = ChatAgent.agentReflex( - "Jene", - mqttclient, - msgMeta, - agentConfigTopic, # I need a function to send msg to config topic to get load balancer - role=:sommelier, - tools=tools - ) -julia> newAgent = ChatAgent.agentReact(agent) -julia> response = ChatAgent.conversation(newAgent, "Hi! how are you?") -``` + ] + ) -# TODO - - [] update docstring - - [] add recap to initialState for earlier completed question - -# Signature +# ---------------------------------------------- 100 --------------------------------------------- # """ -function conversation(a::sommelier, userinput::Dict; maximumMsg=50) +function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Object{String, Any}}, + maximumMsg=50) + userinput = GeneralUtils.dictify(userinput; keytype=String, sort_order=["text"]) + # find text in usermsg + usertext = nothing + text_position = nothing + for (i, d) in enumerate(userinput["content"]) + if d["type"] == "text" + usertext = d["text"] + text_position = i + end + end + # place holder - actionname = nothing + action_name = nothing result = nothing chatresponse = nothing - userinput[:text] = GeneralUtils.remove_french_accents(userinput[:text]) - if userinput[:text] == "newtopic" + if usertext == "newtopic" clearhistory(a) return "Okay. What shall we talk about?" else - # add usermsg to a.chathistory - addNewMessage(a, "user", userinput[:text]; maximumMsg=maximumMsg) - - # add user activity to events memory - push!(a.memory[:events], - eventdict(; - event_description="the user talks to the assistant.", - timestamp=Dates.now(), - subject="user", - actionname="CHATBOX", - actioninput=userinput[:text], - ) - ) + userinput["content"][text_position]["text"] = GeneralUtils.remove_french_accents(usertext) + # add usermsg to a.chathistory but how do I handle images? + addNewMessage(a, "user", userinput; maximumMsg=maximumMsg) # thinking loop until AI wants to communicate with the user chatresponse = nothing while chatresponse === nothing - actionname, result = think(a) - if actionname ∈ ["CHATBOX", "PRESENTBOX", "ENDCONVERSATION"] + action_name, result = think(a) + if action_name ∈ ["CHAT_BOX"] chatresponse = result end end - addNewMessage(a, "assistant", chatresponse; maximumMsg=maximumMsg) + assistant_response = Dict{String, Any}( + "role" => "assistant", + "content" => [Dict("type" => "text", "text" => chatresponse),] + ) + addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) return chatresponse end @@ -1062,32 +617,34 @@ function conversation(a::Union{companion, virtualcustomer}, userinput::Dict; chatresponse = nothing - if userinput[:text] == "newtopic" + if userinput["text"] == "newtopic" clearhistory(a) return "Okay. What shall we talk about?" else # add usermsg to a.chathistory - addNewMessage(a, "user", userinput[:text]; maximumMsg=maximumMsg) + addNewMessage(a, "user", userinput["text"]; maximumMsg=maximumMsg) # add user activity to events memory - push!(a.memory[:events], + push!(a.memory["events"], eventdict(; event_description="the user talks to the assistant.", timestamp=Dates.now(), subject="user", - actioninput=userinput[:text], + action_name="CHAT_BOX", + action_input=userinput["text"], ) ) chatresponse = generatechat(a; converPartnerName=converPartnerName, recentEventNum=20) addNewMessage(a, "assistant", chatresponse; maximumMsg=maximumMsg) - push!(a.memory[:events], + push!(a.memory["events"], eventdict(; event_description="the assistant talks to the user.", timestamp=Dates.now(), subject="assistant", - actioninput=chatresponse, + action_name="CHAT_BOX", + action_input=chatresponse, ) ) return chatresponse @@ -1095,7 +652,6 @@ function conversation(a::Union{companion, virtualcustomer}, userinput::Dict; end """ - # Arguments # Return @@ -1105,166 +661,164 @@ end julia> ``` -# TODO - - [] update docstring - -# Signature """ -function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent} - # a.memory[:recap] = generateSituationReport(a, a.func[:text2textInstructLLM]; skiprecent=0) +function think(a::T) where {T<:agent} + # a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0) thoughtDict = decisionMaker(a) - actionname = thoughtDict[:action_name] - actioninput = thoughtDict[:action_input] + println("\n--- YiemAgent think() 1 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(thoughtDict) + println("---") - # map action and input() to llm function - response = - if actionname == "CHATBOX" || actionname == "ENDCONVERSATION" - (result=thoughtDict[:plan], errormsg=nothing, success=true) - elseif actionname == "CHECKWINE" - checkwine(a, actioninput) - elseif actionname == "PRESENTBOX" - (result=actioninput, errormsg=nothing, success=true) - # elseif actionname == "ENDCONVERSATION" - # x = "Conclude the conversation, thanks the user then goodbye and inviting them to return next time." - # (result=actioninput, errormsg=nothing, success=true) - else - error("undefined LLM function. Requesting $actionname") - end + # # map action and input() to llm function + # response = + # if thoughtDict["action_name"] == "CHAT_BOX" || thoughtDict["action_name"] == "END_CONVER_GUIDELINE" + # (result=thoughtDict["plan"], errormsg=nothing, success=true) + # elseif thoughtDict["action_name"] == "CHECK_WINE" + # checkwine(a, thoughtDict["action_input"]) + # elseif thoughtDict["action_name"] == "PRESENT_WINE_GUIDELINE" + # (result=thoughtDict["action_input"], errormsg=nothing, success=true) + # else + # error("undefined LLM function. Requesting $(thoughtDict["action_name"])") + # end - # this section allow LLM functions above to have different return values. - result = haskey(response, :result) ? response[:result] : nothing - rawresponse = haskey(response, :rawresponse) ? response[:rawresponse] : nothing - select = haskey(response, :select) ? response[:select] : nothing - reward::Integer = haskey(response, :reward) ? response[:reward] : 0 - isterminal::Bool = haskey(response, :isterminal) ? response[:isterminal] : false - errormsg::Union{AbstractString,Nothing} = haskey(response, :errormsg) ? response[:errormsg] : nothing - success::Bool = haskey(response, :success) ? response[:success] : false + # # this section allow LLM functions above to have different return values. + # result = haskey(response, "result") ? response["result"] : nothing + # rawresponse = haskey(response, "rawresponse") ? response["rawresponse"] : nothing + # select = haskey(response, "select") ? response["select"] : nothing + # reward::Integer = haskey(response, "reward") ? response["reward"] : 0 + # isterminal::Bool = haskey(response, "isterminal") ? response["isterminal"] : false + # errormsg::Union{AbstractString,Nothing} = haskey(response, "errormsg") ? response["errormsg"] : nothing + # success::Bool = haskey(response, "success") ? response["success"] : false - # # manage memory (pass msg to generatechat) - # if actionname ∈ ["CHATBOX", "PRESENTBOX", "ENDCONVERSATION"] - # chatresponse = generatechat(a, thoughtDict) - # push!(a.memory[:events], - # eventdict(; - # event_description="the assistant talks to the user.", - # timestamp=Dates.now(), - # subject="assistant", - # thought=thoughtDict, - # actionname=actionname, - # actioninput=actioninput, - # ) - # ) - # result = chatresponse - if actionname ∈ ["CHATBOX", "ENDCONVERSATION"] - push!(a.memory[:events], - eventdict(; - event_description="the assistant talks to the user.", - timestamp=Dates.now(), - subject="assistant", - thought=thoughtDict, - actionname=actionname, - actioninput=actioninput, - ) - ) - result = actioninput - # elseif actionname ∈ ["ENDCONVERSATION"] - # chatresponse = generatechat(a, thoughtDict) - # push!(a.memory[:events], - # eventdict(; - # event_description="the assistant talks to the user.", - # timestamp=Dates.now(), - # subject="assistant", - # thought=thoughtDict, - # actionname=actionname, - # actioninput=chatresponse, - # ) - # ) - # result = chatresponse - elseif actionname ∈ ["PRESENTBOX"] - chatresponse = presentbox(a, thoughtDict) - push!(a.memory[:events], - eventdict(; - event_description="the assistant talks to the user.", - timestamp=Dates.now(), - subject="assistant", - thought=thoughtDict, - actionname=actionname, - actioninput=chatresponse, - ) - ) - result = chatresponse - - elseif actionname == "CHECKWINE" - if rawresponse !== nothing - vd = GeneralUtils.dfToVectorDict(rawresponse) # comes in dataframe format - a.memory[:shortmem][:found_wine] = vd # used by decisionMaker() as a short note - #[PENDING] a.memory[:shortmem][:available_wine] should be OrderedDict instead of vector dict so that no duplicate item are listed? - if length(a.memory[:shortmem][:db_search_result]) != 0 - a.memory[:shortmem][:db_search_result] = vcat(a.memory[:shortmem][:db_search_result], vd) - else - a.memory[:shortmem][:db_search_result] = vd - end - - # add to scratchpad - a.memory[:shortmem][:scratchpad] *= - """ - - I searched the database with this search term: $actioninput \nThis is what I found: $result - - """ - else - a.memory[:shortmem][:scratchpad] *= - """ - - I searched the database with this search term: $actioninput \nThis is what I found: $result - - """ - end - - push!(a.memory[:events], - eventdict(; - event_description= "the assistant searched the database.", - timestamp= Dates.now(), - subject= "assistant", - thought=thoughtDict, - actionname=actionname, - actioninput= "I search the database with this search term: $actioninput", - outcome= "This is what I found:, $result" - ) - ) + result = nothing + if thoughtDict["action_name"] ∈ ["CHAT_BOX"] + result = thoughtDict["action_input"] + elseif thoughtDict["action_name"] == "END_CONVER_GUIDELINE" + # add guideline in to context + guideline = + """ + + - Provide customer with store contact info and business hours + - Invite customer to comeback + + Business Hours: everyday 9.00-20.00 + Tel. 0863055790 + + + """ + thoughtDict["action_result"] = guideline + max_ind = + if length(a.memory["shortmem"]) == 0 + 0 + else + k = keys(a.memory["shortmem"]) + maximum(parse.(Int, k)) + end + a.memory["shortmem"]["$(max_ind + 1)"] = thoughtDict + + elseif thoughtDict["action_name"] ∈ ["PRESENT_WINE_GUIDELINE"] #WORKING + + # add guideline in to context + guideline = + """ + + - Provide detailed introductions of the wines you've found to the user. + - Explain how the wine could match the user's intention and what its effects might mean for the user's experience. + - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. + - Provide your personal recommendation and provide a brief explanation of why you recommend it. + - People don't describe wine quality level in numbers so use convertion_table if neccessary + + Intensity level: + 1 to 2: May correspond to "light-bodied" or a similar description. + 2 to 3: May correspond to "med light bodied", "medium light" or a similar description. + 3 to 4: May correspond to "medium bodied" or a similar description. + 4 to 5: May correspond to "med full bodied", "medium full" or a similar description. + 4 to 5: May correspond to "full bodied" or a similar description. + Sweetness level: + 1 to 2: May correspond to "dry", "no sweet" or a similar description. + 2 to 3: May correspond to "off dry", "less sweet" or a similar description. + 3 to 4: May correspond to "semi sweet" or a similar description. + 4 to 5: May correspond to "sweet" or a similar description. + 4 to 5: May correspond to "very sweet" or a similar description. + Tannin level: + 1 to 2: May correspond to "low tannin" or a similar description. + 2 to 3: May correspond to "semi low tannin" or a similar description. + 3 to 4: May correspond to "medium tannin" or a similar description. + 4 to 5: May correspond to "semi high tannin" or a similar description. + 4 to 5: May correspond to "high tannin" or a similar description. + Acidity level: + 1 to 2: May correspond to "low acidity" or a similar description. + 2 to 3: May correspond to "semi low acidity" or a similar description. + 3 to 4: May correspond to "medium acidity" or a similar description. + 4 to 5: May correspond to "semi high acidity" or a similar description. + 4 to 5: May correspond to "high acidity" or a similar description. + + + """ + thoughtDict["action_result"] = guideline + max_ind = + if length(a.memory["shortmem"]) == 0 + 0 + else + k = keys(a.memory["shortmem"]) + maximum(parse.(Int, k)) + end + a.memory["shortmem"]["$(max_ind + 1)"] = thoughtDict + + elseif thoughtDict["action_name"] == "CHECK_WINE" + result = checkwine(a, thoughtDict["action_input"]) + + thoughtDict["action_result"] = result[:result_str] + max_ind = + if length(a.memory["shortmem"]) == 0 + 0 + else + k = keys(a.memory["shortmem"]) + maximum(parse.(Int, k)) + end + a.memory["shortmem"]["$(max_ind + 1)"] = thoughtDict else error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end - return (actionname=actionname, result=result) + println("\n--- YiemAgent think() 2 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + pprintln(thoughtDict) + println("---") + return (action_name=thoughtDict["action_name"], result=result) end function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentevents::Integer=10) recentchat_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recentevents; includelatest=true) - recentchat = createChatLog(a.chathistory; index=recentchat_ind) + systemmsg = """ + + You have checked the inventory and found wines that may match what the user wants. + - Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. + Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. - - You have checked the inventory and found wines that may match what the user wants - - + Present the wines to the user in a way that keep the conversation smooth and engaging. - + + + + + + + + + Name of the wines that needs to be introduced: name of wines you are going to introduce to the user Database search result: the result of a database search using SQL commands you have found so far - - Provide detailed introductions of the wines you've found to the user. - - Explain how the wine could match the user's intention and what its effects might mean for the user's experience. - - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. - - Provide your personal recommendation and provide a brief explanation of why you recommend it. + dialogue: Your presentation to the user @@ -1279,8 +833,8 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev """ requiredKeys = [:dialogue] database_search_result = - if length(a.memory[:shortmem][:db_search_result]) != 0 - availableWineToText(a.memory[:shortmem][:db_search_result]) + if length(a.memory["shortmem"][:db_search_result]) != 0 + availableWineToText(a.memory["shortmem"][:db_search_result]) else "N/A" end @@ -1291,7 +845,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev - # yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" + # yourthought = "$(thoughtDict[:thought]) $(thoughtDict["plan"])" # yourthought1 = nothing for attempt in 1:maxtattempt @@ -1299,15 +853,15 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev context = """ - Name of the wines that needs to be introduced: $(thoughtDict[:action_input]) - $(a.memory[:shortmem][:scratchpad]) + Name of the wines that needs to be introduced: $(thoughtDict["action_input"]) + $(a.memory["shortmem"]["scratchpad"]) P.S. $errornote """ unformatPrompt = [ - Dict(:name => "system", :text => systemmsg), + Dict("name" => "system", "text" => systemmsg), ] unformatPrompt = vcat(unformatPrompt, recentchat) @@ -1316,7 +870,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev # add info prompt = prompt * context - response = a.func[:text2textInstructLLM](prompt; senderId=a.id) + response = a.context.text2textInstructLLM(prompt; senderId=a.id) response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) response = GeneralUtils.remove_french_accents(response) # response = replace(response, '$'=>"USD") @@ -1329,57 +883,22 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev responsedict = nothing try - responsedict = copy(JSON3.read(response)) + responsedict = copy(JSON.parsefile(response)) catch println("\nERROR YiemAgent presentbox() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end # check whether all answer's key points are in responsedict - _responsedictKey = keys(responsedict) - responsedictKey = [i for i in _responsedictKey] # convert into a list - is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] - - if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) - errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent presentbox() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif !all(is_requiredKeys_in_responsedictKey) - zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) - missingkeys = [requiredKeys[i] for i in zeroind] - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent presentbox() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys) + if !ispass + errornote = errormsg + println("\nERROR YiemAgent presentbox() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") continue end - # # check whether response has not-allowed words - # notallowed = ["respond:", "user>", "user:"] - # detected_kw = GeneralUtils.detect_keyword(notallowed, response) - # # list all keys that have 1 value in detected_kw dictionary - # k = [key for (key, value) in detected_kw if value == 1] - # if length(k) > 0 - # errornote = "In your previous attempt, you have $k in your response which is not allowed." - # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # # check whether response has all header - # detected_kw = GeneralUtils.detect_keyword(header, response) - # if 0 ∈ values(detected_kw) - # errornote = "$missingkeys are missing from your previous response" - # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # elseif sum(values(detected_kw)) > length(header) - # errornote = "\nYour previous attempt has duplicated points according to the required response format" - # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # responsedict = GeneralUtils.textToDict(response, header; - # dictKey=dictkey, symbolkey=true) - # check if Context: is in dialogue - if occursin("Context:", responsedict[:dialogue]) + if occursin("Context:", responsedict["dialogue"]) errornote = "Your previous response contains 'Context:' which is not allowed" println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue @@ -1391,15 +910,15 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev # check whether an agent recommend wines before checking inventory or recommend wines # outside its inventory # ask LLM whether there are any winery mentioned in the response - mentioned_winery = detectWineryName(a, responsedict[:dialogue]) + mentioned_winery = detectWineryName(a, responsedict["dialogue"]) if mentioned_winery != "None" mentioned_winery = String.(strip.(split(mentioned_winery, ","))) # check whether the wine is in event isWineInEvent = false for winename in mentioned_winery - for event in a.memory[:events] - if event[:outcome] !== nothing && occursin(winename, event[:outcome]) + for event in a.memory["events"] + if event["observation"] !== nothing && occursin(winename, event["observation"]) isWineInEvent = true break end @@ -1426,70 +945,32 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev end end - result = responsedict[:dialogue] + result = responsedict["dialogue"] return result end error("presentbox() failed to generate a response") end -# function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10) -# systemmsg = + +# function endconversation(a::sommelier, thoughtDict; maxattempt::Integer=10) +# text = # """ -# Your profile: -# Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. -# Situation: -# You have checked the inventory and found wines. -# Your mission: -# Present the wines to the customer in a way that keep the conversation smooth and engaging. -# At each round of conversation, you will be given the following information: -# Database search result: the result of a database search using SQL commands you have found so far -# Chat history: your ongoing conversation with the user -# Name of the wines that needs to be introduced: name of wines you are going to introduce to the user. -# You should follow the following guidelines: -# - Provide detailed introductions of the wines you've found to the user. -# - Explain how the wine could match the user's intention and what its effects might mean for the user's experience. -# - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. -# - Provide your personal recommendation and provide a brief explanation of why you recommend it. -# You should then respond to the user with: -# dialogue: Your presentation to the user -# You should only respond in format as described below: -# { -# "dialogue": "..." -# } - -# Let's begin! -# """ -# requiredKeys = [:dialogue] -# database_search_result = -# if length(a.memory[:shortmem][:db_search_result]) != 0 -# availableWineToText(a.memory[:shortmem][:db_search_result]) -# else -# "N/A" -# end - -# chathistory = chatHistoryToText(a.chathistory) -# errornote = "N/A" -# response = nothing # placeholder for show when error msg show up - +# --- +# """ +# requiredKeys = ["dialogue"] -# # yourthought = "$(thoughtDict[:thought]) $(thoughtDict[:plan])" -# # yourthought1 = nothing +# system_msg = Dict( +# "role" => "system", +# "content" => [ +# Dict("type" => "text", "text" => systemmsg), +# ] +# ) -# for attempt in 1:maxtattempt - -# context = -# """ -# -# Database search result: $database_search_result -# Chat history: $chathistory -# Name of the wines that needs to be introduced: $(thoughtDict[:action_input]) -# P.S. $errornote -# -# """ +# for attempt in 1:maxattempt # unformatPrompt = # [ -# Dict(:name => "system", :text => systemmsg), +# Dict("name" => "system", "text" => systemmsg), # ] # # put in model format @@ -1497,522 +978,77 @@ end # # add info # prompt = prompt * context -# response = a.func[:text2textInstructLLM](prompt; senderId=a.id) +# response = a.context.text2textInstructLLM(prompt; senderId=a.id) # response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) # response = GeneralUtils.remove_french_accents(response) # # response = replace(response, '$'=>"USD") # think, response = GeneralUtils.extractthink(response) - -# response = replace(response, '*'=>"") -# response = replace(response, '$' => "USD") -# response = replace(response, '`' => "") -# response = replace(response, "<|eot_id|>"=>"") # responsedict = nothing # try -# responsedict = copy(JSON3.read(response)) +# responsedict = copy(JSON.parsefile(response)) # catch -# println("\nERROR YiemAgent presentbox() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") # continue # end # # check whether all answer's key points are in responsedict -# _responsedictKey = keys(responsedict) -# responsedictKey = [i for i in _responsedictKey] # convert into a list -# is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] - -# if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) -# errornote = "Your previous attempt has more key points than answer's required key points." -# println("\nERROR YiemAgent presentbox() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# elseif !all(is_requiredKeys_in_responsedictKey) -# zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) -# missingkeys = [requiredKeys[i] for i in zeroind] -# errornote = "$missingkeys are missing from your previous response" -# println("\nERROR YiemAgent presentbox() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys) +# if !ispass +# errornote = errormsg +# println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") # continue # end -# # # check whether response has not-allowed words -# # notallowed = ["respond:", "user>", "user:"] -# # detected_kw = GeneralUtils.detect_keyword(notallowed, response) -# # # list all keys that have 1 value in detected_kw dictionary -# # k = [key for (key, value) in detected_kw if value == 1] -# # if length(k) > 0 -# # errornote = "In your previous attempt, you have $k in your response which is not allowed." -# # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# # continue -# # end - -# # # check whether response has all header -# # detected_kw = GeneralUtils.detect_keyword(header, response) -# # if 0 ∈ values(detected_kw) -# # errornote = "$missingkeys are missing from your previous response" -# # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# # continue -# # elseif sum(values(detected_kw)) > length(header) -# # errornote = "\nYour previous attempt has duplicated points according to the required response format" -# # println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# # continue -# # end - -# # responsedict = GeneralUtils.textToDict(response, header; -# # dictKey=dictkey, symbolkey=true) - -# # check if Context: is in dialogue -# if occursin("Context:", responsedict[:dialogue]) -# errornote = "Your previous response contains 'Context:' which is not allowed" -# println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# # sometime the model response like this "here's how I would respond: ..." +# if occursin("respond:", response) +# errornote = "Your previous response contains 'response:' which is not allowed" +# println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# continue +# elseif occursin("Your thoughts:", response) || occursin("your thoughts:", response) +# errornote = "You don't need to put 'Your thoughts:' in your response" +# println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # continue # end - -# println("\nYiemAgent presentbox() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# pprintln(Dict(responsedict)) +# response = GeneralUtils.remove_french_accents(response) +# response = replace(response, '*'=>"") +# response = replace(response, '$' => "USD") +# response = replace(response, '`' => "") +# response = replace(response, "<|eot_id|>"=>"") # # check whether an agent recommend wines before checking inventory or recommend wines # # outside its inventory # # ask LLM whether there are any winery mentioned in the response -# mentioned_winery = detectWineryName(a, responsedict[:dialogue]) +# mentioned_winery = detectWineryName(a, response) # if mentioned_winery != "None" # mentioned_winery = String.(strip.(split(mentioned_winery, ","))) # # check whether the wine is in event # isWineInEvent = false # for winename in mentioned_winery -# for event in a.memory[:events] -# if event[:outcome] !== nothing && occursin(winename, event[:outcome]) +# for event in a.memory["events"] +# if event["observation"] !== nothing && occursin(winename, event["observation"]) # isWineInEvent = true # break # end # end # end -# # if wine is mentioned but not in timeline or shortmem, # # then the agent is not supposed to recommend the wine # if isWineInEvent == false -# errornote = "Your previous response recommended wines that is not in your inventory which is not allowed" -# println("\nERROR YiemAgent presentbox() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") +# errornote = "You recommended wines that are not in your inventory before. Please only recommend wines that you have previously found in your inventory." +# println("\nERROR YiemAgent generatechat() $errornote $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") # continue # end # end +# result = responsedict["dialogue"] -# result = responsedict[:dialogue] # return result # end -# error("presentbox() failed to generate a response") -# end - - - -""" - -# Arguments - - `a::T1` - one of ChatAgent's agent. - - `input::T2` -# Return - A JSON string of available wine - -# Example -```jldoctest -julia> -``` - -# TODO - - [] update docs - -# Signature -""" -function generatechat(a::sommelier, thoughtDict; maxattempt::Integer=10) - systemmsg = - """ - Your role: - Your name is $(a.name). You are a helpful English-speaking assistant, acting as a polite, website-based sommelier for $(a.retailername)'s wine store. - Situation: - You have some thinking in mind while you are talking with the user. - Your mission: - Concentrate on your thoughts and articulate them clearly. Keep the conversation engaging. - Your responsibility does NOT includes: - - Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - - Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - - Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - At each round of conversation, you will be given the following: - Your ongoing conversation with the user: ... - Your thoughts: Your current thoughts in your mind - You must follow the following guidelines: - - Do not offer additional services you didn't think - You should follow the following guidelines: - - Focus on the latest conversation - - If the user interrupts, prioritize the user - - Be honest - You should then respond to the user with: - dialogue: what you want to say to the user - You should only respond in JSON format as described below: - { - "dialogue": "..." - } - Here are some examples: - Your ongoing conversation with the user: "user> hello, I need a new car\n" - Your thoughts: "I should recommend the car we have found in our inventory to the user." - {"dialogue": "We have a variety of cars available, including the Toyota Camry 2020, the Honda Civic 2021, and the Ford Mustang 2022. Which one would you like to see?"} - - Let's begin! - """ - requiredKeys = [:dialogue] - - # a.memory[:shortmem][:available_wine] is a vector of dictionary - # context = - # if length(a.memory[:shortmem][:available_wine]) != 0 - # "Wines previously found in your inventory: $(availableWineToText(a.memory[:shortmem][:available_wine]))" - # else - # "N/A" - # end - - chathistory = chatHistoryToText(a.chathistory) - errornote = "N/A" - response = nothing # placeholder for show when error msg show up - - yourthought = thoughtDict[:plan] - # yourthought1 = nothing - - for attempt in 1:maxattempt - # if attempt > 1 # use to prevent LLM generate the same respond over and over - # println("\nYiemAgent generatchat() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # yourthought1 = paraphrase(a.func[:text2textInstructLLM], yourthought) - # else - # yourthought1 = yourthought - # end - - context = - """ - - Your ongoing conversation with the user: $chathistory - Your thoughts: $yourthought - P.S. $errornote - - """ - - unformatPrompt = - [ - Dict(:name => "system", :text => systemmsg), - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) - # add info - prompt = prompt * context - - response = a.func[:text2textInstructLLM](prompt; senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - response = GeneralUtils.remove_french_accents(response) - # response = replace(response, '$'=>"USD") - think, response = GeneralUtils.extractthink(response) - - responsedict = nothing - try - responsedict = copy(JSON3.read(response)) - catch - println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check whether all answer's key points are in responsedict - _responsedictKey = keys(responsedict) - responsedictKey = [i for i in _responsedictKey] # convert into a list - is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] - - if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) - errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif !all(is_requiredKeys_in_responsedictKey) - zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) - missingkeys = [requiredKeys[i] for i in zeroind] - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)--> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # sometime the model response like this "here's how I would respond: ..." - if occursin("respond:", response) - errornote = "Your previous response contains 'response:' which is not allowed" - println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif occursin("Your thoughts:", response) || occursin("your thoughts:", response) - errornote = "You don't need to put 'Your thoughts:' in your response" - println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - response = GeneralUtils.remove_french_accents(response) - response = replace(response, '*'=>"") - response = replace(response, '$' => "USD") - response = replace(response, '`' => "") - response = replace(response, "<|eot_id|>"=>"") - - - - - # # check whether response has all header - # detected_kw = GeneralUtils.detect_keyword(header, response) - # kwvalue = [i for i in values(detected_kw)] - # zeroind = findall(x -> x == 0, kwvalue) - # missingkeys = [header[i] for i in zeroind] - # if 0 ∈ values(detected_kw) - # errornote = "$missingkeys are missing from your previous response" - # println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # elseif sum(values(detected_kw)) > length(header) - # errornote = "\nYour previous attempt has duplicated points according to the required response format" - # println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # responsedict = GeneralUtils.textToDict(response, header; - # dictKey=dictkey, symbolkey=true) - - # # check if Context: is in dialogue - # if occursin("Context:", responsedict[:dialogue]) - # errornote = "Your previous response contains 'Context:' which is not allowed" - # println("\nERROR YiemAgent generatechat() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # println("\nYiemAgent generatechat() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # pprintln(Dict(responsedict)) - - # check whether an agent recommend wines before checking inventory or recommend wines - # outside its inventory - # ask LLM whether there are any winery mentioned in the response - mentioned_winery = detectWineryName(a, response) - if mentioned_winery != "None" - mentioned_winery = String.(strip.(split(mentioned_winery, ","))) - - # check whether the wine is in event - isWineInEvent = false - for winename in mentioned_winery - for event in a.memory[:events] - if event[:outcome] !== nothing && occursin(winename, event[:outcome]) - isWineInEvent = true - break - end - end - end - - # then the agent is not supposed to recommend the wine - if isWineInEvent == false - errornote = "You recommended wines that are not in your inventory before. Please only recommend wines that you have previously found in your inventory." - println("\nERROR YiemAgent generatechat() $errornote $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - end - result = responsedict[:dialogue] - - return result - end - error("generatechat failed to generate a response") -end - - -function generatechat(a::companion; recentevents::Integer=10, - converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) - - recentchat_ind = GeneralUtils.recentElementsIndex(length(a.chathistory), recentevents; - includelatest=true); - recentchat = createChatLog(a.chathistory; index=recentchat_ind) - - response = nothing # placeholder for show when error msg show up - errornote = "N/A" - - for attempt in 1:maxattempt - if attempt > 1 - println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - context = - """ - - P.S. $errornote - - """ - - unformatPrompt = - [ - Dict(:name => "system", :text => a.systemmsg), - ] - - unformatPrompt = vcat(unformatPrompt, recentchat) - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) - # add info - prompt = prompt * context - - - # replace user and assistant with partner name - prompt = replace(_prompt, "|>user"=>"|>$(converPartnerName)") - prompt = replace(prompt, "|>assistant"=>"|>$(a.name)") - - response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs, senderId=a.id) - response = replace(response, "<|im_start|>"=> "") - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - think, response = GeneralUtils.extractthink(response) - - # check whether LLM just repeat the previous dialogue - for msg in a.chathistory - if msg[:text] == response - errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." - println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - end - - return response - end - error("generatechat failed to generate a response") -end -# function generatechat(a::companion; converPartnerName::Union{String, Nothing}=nothing, maxattempt=10) -# response = nothing # placeholder for show when error msg show up -# errornote = "N/A" -# llmkwargs=Dict( -# :num_ctx => 32768, -# :temperature => 0.5, -# ) -# for attempt in 1:maxattempt -# if attempt > 1 -# println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# end - -# systemmsg = a.systemmsg * "\nP.S. $errornote\n" -# _prompt = -# [ -# Dict(:name => "system", :text => systemmsg), -# ] -# for i in a.chathistory -# tempdict = Dict{Symbol, String}() -# for j in keys(i) -# if j ∉ [:timestamp] -# tempdict[j] = i[j] -# end -# end -# _prompt = vcat(_prompt, tempdict) -# end - -# # put in model format -# _prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) - -# # replace user and assistant with partner name -# prompt = replace(_prompt, "|>user"=>"|>$(converPartnerName)") -# prompt = replace(prompt, "|>assistant"=>"|>$(a.name)") - -# response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs, senderId=a.id) -# response = replace(response, "<|im_start|>"=> "") -# response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) -# think, response = GeneralUtils.extractthink(response) - -# # check whether LLM just repeat the previous dialogue -# for msg in a.chathistory -# if msg[:text] == response -# errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." -# println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") -# continue -# end -# end - -# return response -# end # error("generatechat failed to generate a response") # end -# modify it to work with customer object -function generatechat(a::virtualcustomer; - converPartnerName::Union{String, Nothing}=nothing, maxattempt=10, recentEventNum=10 - ) - recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recentEventNum; includelatest=true) - recentEventsDict = createEventsLog(a.memory[:events]; index=recent_ind) - response = nothing # placeholder for show when error msg show up - errornote = "N/A" - header = ["Dialogue:", "Role"] - dictkey = ["dialogue", "role"] - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.5, - ) - - for attempt in 1:maxattempt - if attempt > 1 - println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - context = - """ - - P.S. $errornote - - """ - - unformatPrompt = - [ - Dict(:name => "system", :text => a.systemmsg), - ] - unformatPrompt = vcat(unformatPrompt, recentEventsDict) - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) - # add info - prompt = prompt * context - - response = a.func[:text2textInstructLLM](prompt; llmkwargs=llmkwargs, senderId=a.id) - response = replace(response, "<|im_start|>"=> "") - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - think, response = GeneralUtils.extractthink(response) - - # check whether LLM just repeat the previous dialogue - for msg in a.chathistory - if msg[:text] == response - errornote = "In your previous attempt, you repeated the previous dialogue. Please try again." - println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - end - - # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) - kwvalue = [i for i in values(detected_kw)] - zeroind = findall(x -> x == 0, kwvalue) - missingkeys = [header[i] for i in zeroind] - if 0 ∈ values(detected_kw) - errornote = "$missingkeys are missing from your previous response" - println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "Your previous attempt has duplicated points" - println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) - if responsedict[:role] == "no" - errornote = "In your previous attempt you said $(responsedict[:dialogue]) which you, as a customer of a wine store, are not supposed to speak." - println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check if the dialogue is the same as the previous one - if responsedict[:dialogue] == a.chathistory[end][:text] - errornote = "In your previous attempt you said $(responsedict[:dialogue]) which was the same as the previous one." - println("\nYiemAgent generatechat() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - - - # println("\n$prompt", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # println("\n $response") - return responsedict[:dialogue] - end - error("generatechat failed to generate a response") -end - - function generatequestion(a, text2textInstructLLM::Function, timeline)::String systemmsg = """ @@ -2122,12 +1158,12 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String dictkey = ["q1"] # context = - # if length(a.memory[:shortmem][:available_wine]) != 0 - # "Available wines you've found in your inventory so far: $(availableWineToText(a.memory[:shortmem][:available_wine]))" + # if length(a.memory["shortmem"]["available_wine"]) != 0 + # "Available wines you've found in your inventory so far: $(availableWineToText(a.memory["shortmem"]["available_wine"]))" # else # "N/A" # end - database_search_result = a.memory[:shortmem][:db_search_result] + database_search_result = a.memory["shortmem"]["db_search_result"] # recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) # recentevents = a.memory[:events][recent_ind] @@ -2171,8 +1207,8 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String _prompt = [ - Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) + Dict("name" => "system", "text" => systemmsg), + Dict("name" => "user", "text" => usermsg) ] # put in model format @@ -2194,8 +1230,8 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String # check whether the wine is in event isWineInEvent = false for winename in mentioned_winery - for event in a.memory[:events] - if event[:outcome] !== nothing && occursin(winename, event[:outcome]) + for event in a.memory["events"] + if event["observation"] !== nothing && occursin(winename, event["observation"]) isWineInEvent = true break end @@ -2226,7 +1262,7 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String end # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) + detected_kw = GeneralUtils.detectKeywordVariation(header, response) if 0 ∈ values(detected_kw) errornote = "\nYour previous attempt did not have all points according to the required response format" println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") @@ -2239,7 +1275,7 @@ function generatequestion(a, text2textInstructLLM::Function, timeline)::String responsedict = GeneralUtils.textToDict(response, header; dictKey=dictkey, symbolkey=true) - response = "Q1: " * responsedict[:q1] + response = "Q1: " * responsedict["q1"] println("\nYiemAgent generatequestion() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") try pprintln(response) catch e println(response) end @@ -2283,11 +1319,11 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: Let's begin! """ - header = ["Event_$i:" for i in eachindex(a.memory[:events])] - dictkey = lowercase.(["Event_$i" for i in eachindex(a.memory[:events])]) + header = ["Event_$i:" for i in eachindex(a.memory["events"])] + dictkey = lowercase.(["Event_$i" for i in eachindex(a.memory["events"])]) - ind = GeneralUtils.nonRecentElementsIndex(length(a.memory[:events]), skiprecent) - events = a.memory[:events][ind] + ind = GeneralUtils.nonRecentElementsIndex(length(a.memory["events"]), skiprecent) + events = a.memory["events"][ind] timeline = createTimeline(events) errornote = "N/A" @@ -2305,8 +1341,8 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: _prompt = [ - Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) + Dict("name" => "system", "text" => systemmsg), + Dict("name" => "user", "text" => usermsg) ] # put in model format @@ -2316,7 +1352,7 @@ function generateSituationReport(a, text2textInstructLLM::Function; skiprecent:: response = GeneralUtils.deFormatLLMtext(response, "qwen3") # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) + detected_kw = GeneralUtils.detectKeywordVariation(header, response) kwvalue = [i for i in values(detected_kw)] zeroind = findall(x -> x == 0, kwvalue) missingkeys = [header[i] for i in zeroind] @@ -2377,33 +1413,37 @@ function detectWineryName(a, text) """ _prompt = [ - Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) + Dict("name" => "system", "text" => systemmsg), + Dict("name" => "user", "text" => usermsg) ] # put in model format prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) - response = a.func[:text2textInstructLLM](prompt; senderId=a.id) + response = a.context.text2textInstructLLM(prompt; senderId=a.id) response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) think, response = GeneralUtils.extractthink(response) println("\ndetectWineryName() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") try pprintln(response) catch e println(response) end - # check whether response has all header - detected_kw = GeneralUtils.detect_keyword(header, response) - if 0 ∈ values(detected_kw) - errornote = "\nYiemAgent detectWineryName() response does not have all header" - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "\nYiemAgent detectWineryName() response has duplicated header" - continue - end + # check whether response has all header + detected_kw = GeneralUtils.detectKeywordVariation(header, response) + missingkeys = [k for (k, v) in detected_kw if v === nothing] + + if !isempty(missingkeys) + errornote = "$missingkeys are missing from your previous response" + println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + elseif sum([length(i) for i in values(detected_kw)]) > length(header) + errornote = "\nYour previous attempt has duplicated points according to the required response format" + println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + continue + end responsedict = GeneralUtils.textToDict(response, header; dictKey=dictkey, symbolkey=true) - result = responsedict[:winery_names] + result = responsedict["winery_names"] return result end @@ -2449,4 +1489,74 @@ function detectWineryName(a, text) -end # module interface \ No newline at end of file +end # module interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/llmfunction.jl b/src/llmfunction.jl index 4a8febb..20be98f 100644 --- a/src/llmfunction.jl +++ b/src/llmfunction.jl @@ -4,7 +4,7 @@ export virtualWineUserChatbox, jsoncorrection, checkwine, # recommendbox, virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1, extractWineAttributes_2, paraphrase -using HTTP, JSON3, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames +using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames using GeneralUtils, SQLLLM using ..type, ..util @@ -37,10 +37,10 @@ function virtualWineUserRecommendbox(a::T1, input )::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:agent} # put in model format - virtualWineCustomer = a.config[:externalservice][:virtualWineCustomer_1] - llminfo = virtualWineCustomer[:llminfo] + virtualWineCustomer = a.config["externalservice"]["virtualWineCustomer_1"] + llminfo = virtualWineCustomer["llminfo"] prompt = - if llminfo[:name] == "llama3instruct" + if llminfo["name"] == "llama3instruct" formatLLMtext_llama3instruct("assistant", input) else error("llm model name is not defied yet $(@__LINE__)") @@ -48,26 +48,26 @@ function virtualWineUserRecommendbox(a::T1, input # send formatted input to user using GeneralUtils.sendReceiveMqttMsg msgMeta = GeneralUtils.generate_msgMeta( - virtualWineCustomer[:mqtttopic], + virtualWineCustomer["mqtttopic"], senderName= "virtualWineUserRecommendbox", senderId= a.id, receiverName= "virtualWineCustomer", - mqttBroker= a.config[:mqttServerInfo][:broker], - mqttBrokerPort= a.config[:mqttServerInfo][:port], + mqttBroker= a.config["mqttServerInfo"]["broker"], + mqttBrokerPort= a.config["mqttServerInfo"]["port"], msgId = "dummyid" #CHANGE remove after testing finished ) outgoingMsg = Dict( - :msgMeta=> msgMeta, - :payload=> Dict( - :text=> prompt, + "msgMeta"=> msgMeta, + "payload"=> Dict( + "text"=> prompt, ) ) result = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120) - response = result[:response] + response = result["response"] - return (response[:text], response[:select], response[:reward], response[:isterminal]) + return (response["text"], response["select"], response["reward"], response["isterminal"]) end @@ -171,26 +171,26 @@ function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistor Let's begin! """ - pushfirst!(virtualCustomerChatHistory, Dict(:name=> "system", :text=> systemmsg)) +pushfirst!(virtualCustomerChatHistory, Dict("name"=> "system", "text"=> systemmsg)) # replace the :user key in chathistory to allow the virtual wine customer AI roleplay - chathistory::Vector{Dict{Symbol, Any}} = Vector{Dict{Symbol, Any}}() + chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}() for i in virtualCustomerChatHistory newdict = Dict() - newdict[:name] = - if i[:name] == "user" + newdict["name"] = + if i["name"] == "user" "you" - elseif i[:name] == "assistant" + elseif i["name"] == "assistant" "sommelier" else - i[:name] + i["name"] end - newdict[:text] = i[:text] + newdict["text"] = i["text"] push!(chathistory, newdict) end - push!(chathistory, Dict(:name=> "assistant", :text=> input)) + push!(chathistory, Dict("name"=> "assistant", "text"=> input)) # put in model format prompt = formatLLMtext(chathistory, "llama3instruct") @@ -201,23 +201,23 @@ function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistor """ pprint(prompt) - externalService = config[:externalservice][:text2textinstruct] + externalService = config["externalservice"]["text2textinstruct"] # send formatted input to user using GeneralUtils.sendReceiveMqttMsg msgMeta = GeneralUtils.generate_msgMeta( - externalService[:mqtttopic], + externalService["mqtttopic"], senderName= "virtualWineUserChatbox", senderId= string(uuid4()), receiverName= "text2textinstruct", - mqttBroker= config[:mqttServerInfo][:broker], - mqttBrokerPort= config[:mqttServerInfo][:port], + mqttBroker= config["mqttServerInfo"]["broker"], + mqttBrokerPort= config["mqttServerInfo"]["port"], msgId = string(uuid4()) #CHANGE remove after testing finished ) outgoingMsg = Dict( - :msgMeta=> msgMeta, - :payload=> Dict( - :text=> prompt, + "msgMeta"=> msgMeta, + "payload"=> Dict( + "text"=> prompt, ) ) @@ -225,7 +225,7 @@ function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistor for attempt in 1:5 try response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120) - _responseJsonStr = response[:response][:text] + _responseJsonStr = response["response"]["text"] expectedJsonExample = """ Here is an expected JSON format: @@ -237,12 +237,12 @@ function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistor } """ responseJsonStr = jsoncorrection(config, _responseJsonStr, expectedJsonExample) - responseDict = copy(JSON3.read(responseJsonStr)) + responseDict = copy(JSON.parsefile(responseJsonStr)) - text::AbstractString = responseDict[:text] - select::Union{Nothing, Number} = responseDict[:select] == "null" ? nothing : responseDict[:select] - reward::Number = responseDict[:reward] - isterminal::Bool = responseDict[:isterminal] + text::AbstractString = responseDict["text"] + select::Union{Nothing, Number} = responseDict["select"] == "null" ? nothing : responseDict["select"] + reward::Number = responseDict["reward"] + isterminal::Bool = responseDict["isterminal"] if text != "" # pass test @@ -281,13 +281,7 @@ julia> input = "{\"food\": \"pizza\", \"occasion\": \"anniversary\"}" julia> result = checkinventory(agent, input) "{"wine 1": {\"Winery\": \"Pichon Baron\", \"wine name\": \"Pauillac (Grand Cru Classé)\", \"grape variety\": \"Cabernet Sauvignon\", \"year\": 2010, \"price\": \"125 USD\", \"stock ID\": \"ar-17\"}, }" ``` - -# TODO - - [] update docs - - [x] implement the function - -# Signature -""" +""" function checkwine(a::T1, input::T2; maxattempt::Int=3 ) where {T1<:agent, T2<:AbstractString} @@ -296,42 +290,62 @@ function checkwine(a::T1, input::T2; maxattempt::Int=3 wineattributes_2 = extractWineAttributes_2(a, input) # placeholder - textresult = nothing - rawresponse = nothing + # textresult = nothing + # rawresponse = nothing - for i in 1:maxattempt + # for i in 1:maxattempt - #CHANGE if you want to add retailer name - # _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" - _inventoryquery = "$wineattributes_1, $wineattributes_2" + # #CHANGE if you want to add retailer name + # # _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" + # _inventoryquery = "$wineattributes_1, $wineattributes_2" - retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency"] - inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}" - println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # add suppport for similarSQLVectorDB - textresult, rawresponse = SQLLLM.query(inventoryquery, a.func[:executeSQL], - a.func[:text2textInstructLLM]; - insertSQLVectorDB=a.func[:insertSQLVectorDB], - similarSQLVectorDB=a.func[:similarSQLVectorDB], - llmFormatName="qwen3") - # check if all of retrieve_attributes appears in textresult - isin = [occursin(x, textresult) for x in retrieve_attributes] - # check if rawresponse type is DataFrame so that I can check for column - if typeof(rawresponse) == DataFrame && - !occursin("The resulting table has 0 row", textresult) && - !all(isin) - - errornote = "Not all of $retrieve_attributes appear in search result" - println("\nERROR YiemAgent checkwine() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - else - break - end - end - println("\ncheckinventory result ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - println(textresult) + # retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency"] + # inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}" + # println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # # add suppport for similarSQLVectorDB + # textresult, result_raw = SQLLLM.query( + # inventoryquery, + # a.context.executeSQL, + # a.context.text2textInstructLLM; + # insertSQLVectorDB=a.context.insertSQLVectorDB, + # similarSQLVectorDB=a.context.similarSQLVectorDB, + # llmFormatName="qwen3") - return (result=textresult, rawresponse=rawresponse, success=true, errormsg=nothing) + # # check if all of retrieve_attributes appears in textresult + # isin = [occursin(x, textresult) for x in retrieve_attributes] + # # check if rawresponse type is DataFrame so that I can check for column + # if typeof(result_raw) == DataFrame && + # !occursin("The resulting table has 0 row", textresult) && + # !all(isin) + + # errornote = "Not all of $retrieve_attributes appear in search result" + # println("\nERROR YiemAgent checkwine() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # continue + # else + # break + # end + # end + + #CHANGE if you want to add retailer name + # _inventoryquery = "retailer name: $(a.retailername), $wineattributes_1, $wineattributes_2" + retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency"] + _inventoryquery = "$wineattributes_1, $wineattributes_2" + inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}" + println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # add suppport for similarSQLVectorDB + textresult, result_raw = SQLLLM.query( + inventoryquery, + a.context.executeSQL, + a.context.text2textInstructLLM; + insertSQLVectorDB=a.context.insertSQLVectorDB, + similarSQLVectorDB=a.context.similarSQLVectorDB, + llmFormatName="qwen3") + # println("\n--- YiemAgent checkwine() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # println(textresult) + # println(result_raw) + # println("---") + + return (result_str=textresult, result_raw=result_raw, success=true, errormsg=nothing) end @@ -347,34 +361,33 @@ end ```jldoctest julia> ``` - -# TODO - - [] update docstring - - implement the function - -# Signature """ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 )::String where {T1<:agent, T2<:AbstractString} systemmsg = """ - As a helpful sommelier, your task is to extract the user information from the user's query as much as possible to fill out user's preference form. - - At each round of conversation, the user will give you the following: - - The query: the query provided by the user. - - You must follow the following guidelines: + + At each round of conversation, the user provides the following: + - The query: the query provided by the user. + + + Extract information from the user's query as much as possible according to wine attributes extraction guidelines to fill out user's preference form. + + + Fulfill the objective. + + - If specific information required in the preference form is not available in the query or there isn't any, mark with "N/A" to indicate this. Additionally, words like 'any' or 'unlimited' mean no information is available. - Do not generate other comments. - - You should then respond to the user with: + + wine_name: name of the wine winery: name of the winery vintage: the year of the wine - region: a region, such as Burgundy, Bordeaux, Champagne, Napa Valley, Tuscany, California, Oregon, etc - country: a country where wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States" + region: a region, such as Burgundy, Bordeaux, Champagne, Napa Valley, Tuscany, California, Oregon, etc. Use "or" if there are multiple regions. + country: a country where wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States". Use "or" if there are multiple countries. wine_type: can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified" grape_varietal: the name of the primary grape used to make the wine tasting_notes: a word describe the wine's flavor, such as "butter", "oak", "fruity", "raspberry", "earthy", "floral", etc @@ -382,27 +395,23 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 wine_price_max: maximum price range of wine. Example: For wine price 20, wine_price_max will be 20. For wine price 10 to 100, wine_price_max will be 100. occasion: the occasion the user is having the wine for food_to_be_paired_with_wine: food that the user will be served with the wine such as poultry, fish, steak, etc - - You should only respond in JSON format as described below: - { - "wine_name": "...", - "winery": "...", - "vintage": "...", - "region": "...", - "country": "...", - "wine_type": "...", - "grape_varietal": "...", - "tasting_notes": "...", - "wine_price_min": "...", - "wine_price_max": "...", - "occasion": "...", - "food_to_be_paired_with_wine": "..." - } - - Here are some example: - + + + "wine_name": "...", + "winery": "...", + "vintage": "...", + "region": "...", + "country": "...", + "wine_type": "...", + "grape_varietal": "...", + "tasting_notes": "...", + "wine_price_min": "...", + "wine_price_max": "...", + "occasion": "...", + "food_to_be_paired_with_wine": "..." + + User's query: red, Chenin Blanc, Riesling, 20 USD from Tuscany, Italy or Napa Valley, USA - { "wine_name": "N/A", "winery": "N/A", "vintage": "N/A", @@ -415,10 +424,8 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 "wine_price_max": "20", "occasion": "N/A", "food_to_be_paired_with_wine": "N/A" - } User's query: Domaine du Collier Saumur Blanc 2019, France, white, Merlot - { "wine_name": "Saumur Blanc", "winery": "Domaine du Collier", "vintage": "2019", @@ -431,172 +438,71 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 "wine_price_max": "N/A", "occasion": "N/A", "food_to_be_paired_with_wine": "N/A" - } - - Let's begin! + + """ + requiredKeys = ["wine_name", "winery", "vintage", "region", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_min", "wine_price_max", "occasion", "food_to_be_paired_with_wine"] + errornote = "" + context = + """ + + $errornote + """ - requiredKeys = [:wine_name, :winery, :vintage, :region, :country, :wine_type, :grape_varietal, :tasting_notes, :wine_price_min, :wine_price_max, :occasion, :food_to_be_paired_with_wine] - errornote = "N/A" + input = context * input + + msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => [ + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => input), + ] + ), + ], + "temperature" => 0.7 + ) for attempt in 1:maxattempt - usermsg = - """ - $input - """ - context = - """ - - P.S. $errornote - - /no_think - """ + response = a.context.text2textInstructLLM(a.id, msg) + response = GeneralUtils.clean_json_response(response) + println("\n--- extractWineAttributes_1-1()") + println(response) + println("--- \n") - unformatPrompt = - [ - Dict(:name=> "system", :text=> systemmsg), - Dict(:name=> "user", :text=> usermsg) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) - # add info - prompt = prompt * context - - response = a.func[:text2textInstructLLM](prompt; modelsize="medium", senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) response = GeneralUtils.remove_french_accents(response) think, response = GeneralUtils.extractthink(response) - responsedict = nothing try - responsedict = copy(JSON3.read(response)) + _responsedict = JSON.parse(response) + responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys) catch - println("\nERROR YiemAgent extractWineAttributes_1() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent extractWineAttributes_1() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end - # check whether all answer's key points are in responsedict - _responsedictKey = keys(responsedict) - responsedictKey = [i for i in _responsedictKey] # convert into a list - is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] - - if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) - errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent extractWineAttributes_1() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif !all(is_requiredKeys_in_responsedictKey) - zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) - missingkeys = [requiredKeys[i] for i in zeroind] - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent extractWineAttributes_1() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) + if !ispass + errornote = errormsg + println("\nERROR YiemAgent extractWineAttributes_1() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") continue end - - # # check whether response has all header - # detected_kw = GeneralUtils.detect_keyword(header, response) - # kwvalue = [i for i in values(detected_kw)] - # zeroind = findall(x -> x == 0, kwvalue) - # missingkeys = [header[i] for i in zeroind] - # if 0 ∈ values(detected_kw) - # errornote = "$missingkeys are missing from your previous response" - # println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # elseif sum(values(detected_kw)) > length(header) - # errornote = "Your previous attempt has duplicated points" - # println("\nERROR YiemAgent decisionMaker() $errornote:\n$response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # # check whether response has all answer's key points - # detected_kw = GeneralUtils.detect_keyword(header, response) - # if 0 ∈ values(detected_kw) - # errornote = "In your previous attempts, the response does not have all answer's key points" - # println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # elseif sum(values(detected_kw)) > length(header) - # errornote = "In your previous attempts, the response has duplicated answer's key points" - # println("\nYiemAgent extractWineAttributes_1() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # println(response) - # continue - # end - # responsedict = GeneralUtils.textToDict(response, header; - # dictKey=dictkey, symbolkey=true) - - removekeys = [:thought, :tasting_notes, :occasion, :food_to_be_paired_with_wine, :vintage] + removekeys = ["thought", "tasting_notes", "occasion", "food_to_be_paired_with_wine", "vintage"] for i in removekeys delete!(responsedict, i) end - - - - delete!(responsedict, :thought) - delete!(responsedict, :tasting_notes) - delete!(responsedict, :occasion) - delete!(responsedict, :food_to_be_paired_with_wine) - delete!(responsedict, :vintage) - - # check if winery, wine_name, region, country, wine_type, grape_varietal's value are in the query because sometime AI halucinates - checkFlag = false - for i in requiredKeys - j = Symbol(i) - if j ∉ removekeys - # in case j is wine_price it needs to be checked differently because its value is ranged - if j == :wine_price - if responsedict[:wine_price] != "N/A" - # check whether wine_price is in ranged number - if !occursin("to", responsedict[:wine_price]) - errornote = "In your previous attempt, the 'wine_price' was set to $(responsedict[:wine_price]) which is not a correct format. Please adjust it accordingly." - println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - checkFlag = true - break - end - - # # check whether max wine_price is in the input - # pricerange = split(responsedict[:wine_price], '-') - # minprice = pricerange[1] - # maxprice = pricerange[end] - # if !occursin(maxprice, input) - # responsedict[:wine_price] = "N/A" - # end - # # price range like 100-100 is not good - # if minprice == maxprice - # errornote = "In your previous attempt, you inputted 'wine_price' with a 'minimum' value equaling the 'maximum', which is not valid." - # println("\nERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # checkFlag = true - # break - # end - end - else - content = responsedict[j] - if typeof(content) <: AbstractVector - content = strip.(content) - elseif occursin(',', content) - content = split(content, ",") # sometime AI generates multiple values e.g. "Chenin Blanc, Riesling" - content = strip.(content) - else - content = [content] - end - - # for x in content #check whether price are mentioned in the input - # if !occursin("NA", responsedict[j]) && !occursin(x, input) - # errornote = "$x is not mentioned in the user query, you must only use the info from the query." - # println("ERROR YiemAgent extractWineAttributes_1() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # checkFlag == true - # break - # end - # end - end - end - end - checkFlag == true ? continue : nothing # skip the rest code if true - # remove (some text) for (k, v) in responsedict _v = replace(v, r"\(.*?\)" => "") responsedict[k] = _v end - result = "" for (k, v) in responsedict # some time LLM generate text with "(some comment)". this line removes it @@ -604,20 +510,18 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 result *= "$k: $v, " end end - - #[PENDING] remove halucination. "highend dry white wine" --> "wine_type: white, occasion: special occasion, food_to_be_paired_with_wine: seafood, fish, country: France, Italy, USA, grape_varietal: Chardonnay, Sauvignon Blanc, Pinot Grigio\nwine_notes: citrus, green apple, floral" - result = result[1:end-2] # remove the ending ", " - + println("\n--- extractWineAttributes_1-2()") + println(result) + println("--- \n") return result end - error("wineattributes_wordToNumber() failed to get a response") + error("extractWineAttributes_1() failed to get a response") end """ -# TODO - - [PENDING] "French dry white wines with medium bod" the LLM does not recognize sweetness. use LLM self questioning to solve. - - [PENDING] French Syrah, Viognier, under 100. LLM extract intensiry of 3-5. why? + - TODO "French dry white wines with medium bod" the LLM does not recognize sweetness. use LLM self questioning to solve. + - TODO French Syrah, Viognier, under 100. LLM extract intensiry of 3-5. why? """ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString} @@ -653,21 +557,25 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< systemmsg = """ - As an helpful sommelier, your task is to fill out the user's preference form based on the corresponding words from the user's query. - - At each round of conversation, you will be given the following information: - conversion_table: a conversion table that maps descriptive words to their corresponding integer levels - query: the words from the user's query that describe their preferences - - The preference form requires the following information: - sweetness, acidity, tannin, intensity - - You must follow the following guidelines: - 1) If specific information required in the preference form is not available in the query or there isn't any, mark with 'N/A' to indicate this. + + At each round of conversation, you will be given the following information: + conversion_table: a conversion table that maps descriptive words to their corresponding integer levels + query: the words from the user's query that describe their preferences + + + Fill out the user's preference form based on the corresponding words from the user's query according to the guidelines. + + + Fulfill the objective + + + - The preference form requires sweetness, acidity, tannin, intensity infomation + - If specific information required in the preference form is not available in the query or there isn't any, mark with 'N/A' to indicate this. Additionally, words like 'any' or 'unlimited' mean no information is available. - 2) Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer. - 3) Do not generate other comments. - You should then respond to the user with: + - Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer. + - Do not generate other comments. + + sweetness_keyword: The exact keywords in the user's query describing the sweetness level of the wine. sweetness: ( S ), where ( S ) represents integers indicating the range of sweetness levels. Example: 1-2 acidity_keyword: The exact keywords in the user's query describing the acidity level of the wine. @@ -676,157 +584,134 @@ function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2< tannin: ( T ), where ( T ) represents integers indicating the range of tannin level. Example: 1-3 intensity_keyword: The exact keywords in the user's query describing the intensity level of the wine. intensity: ( I ), where ( I ) represents integers indicating the range of intensity level. Example: 2-4 - You should only respond in JSON format as described below: - { + + "sweetness_keyword": "...", - "sweetness": "...", + "sweetness_min": "...", + "sweetness_max": "...", "acidity_keyword": "...", - "acidity": "...", + "acidity_min": "...", + "acidity_max": "...", "tannin_keyword": "...", - "tannin": "...", + "tannin_min": "...", + "tannin_max": "...", "intensity_keyword": "...", - "intensity": "..." - } - - Here are some examples: + "intensity_min": "...", + "intensity_max": "..." + + User's query: I want a wine with a medium-bodied, low acidity, medium tannin. - { - "sweetness_keyword": "N/A", - "sweetness": "N/A", - "acidity_keyword": "low acidity", - "acidity": "1-2", - "tannin_keyword": "medium tannin", - "tannin": "3-4", - "intensity_keyword": "medium-bodied", - "intensity": "3-4" - } + "sweetness_keyword": "N/A", + "sweetness_min": "N/A", + "sweetness_max": "N/A", + "acidity_keyword": "low acidity", + "acidity_min": 1, + "acidity_max": 2, + "tannin_keyword": "medium tannin", + "tannin_min": 3, + "tannin_max": 4, + "intensity_keyword": "medium-bodied", + "intensity_min": 3, + "intensity_max": 4 - User's query: German red wine, under 100, pairs with spicy food - { - "sweetness_keyword": "N/A", - "sweetness": "N/A", - "acidity_keyword": "N/A", - "acidity": "N/A", - "tannin_keyword": "N/A", - "tannin": "N/A", - "intensity_keyword": "N/A", - "intensity": "N/A" - } - - Let's begin! + User's query: German red wine, under 100, pairs with spicy food. + "sweetness_keyword": "N/A", + "sweetness_min": "N/A", + "sweetness_max": "N/A", + "acidity_keyword": "N/A", + "acidity_min": "N/A", + "acidity_max": "N/A", + "tannin_keyword": "N/A", + "tannin_min": "N/A", + "tannin_max": "N/A", + "intensity_keyword": "N/A", + "intensity_min": "N/A", + "intensity_max": "N/A" + + """ + requiredKeys = ["sweetness_keyword", "sweetness_min", "sweetness_max", + "acidity_keyword", "acidity_min", "acidity_max", + "tannin_keyword", "tannin_min", "tannin_max", + "intensity_keyword", "intensity_min", "intensity_max"] + errornote = "" + context = + """ + + $conversiontable + $errornote + """ - requiredKeys = [:sweetness_keyword, :sweetness, :acidity_keyword, :acidity, :tannin_keyword, :tannin, :intensity_keyword, :intensity] - # header = ["Sweetness_keyword:", "Sweetness:", "Acidity_keyword:", "Acidity:", "Tannin_keyword:", "Tannin:", "Intensity_keyword:", "Intensity:"] - # dictkey = ["sweetness_keyword", "sweetness", "acidity_keyword", "acidity", "tannin_keyword", "tannin", "intensity_keyword", "intensity"] - errornote = "N/A" + input = context * input + + msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => [ + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => input), + ] + ), + ], + "temperature" => 0.7 + ) for attempt in 1:10 - context = - """ - $conversiontable - - $input - - P.S. $errornote - /no_think - """ + response = a.context.text2textInstructLLM(a.id, msg) + response = GeneralUtils.clean_json_response(response) + println("\n--- extractWineAttributes_2-1()") + println(response) + println("--- \n") - unformatPrompt = - [ - Dict(:name=> "system", :text=> systemmsg), - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) - # add info - prompt = prompt * context - - response = a.func[:text2textInstructLLM](prompt; modelsize="medium", senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) response = GeneralUtils.remove_french_accents(response) think, response = GeneralUtils.extractthink(response) - responsedict = nothing try - responsedict = copy(JSON3.read(response)) + _responsedict = JSON.parse(response) + responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys) catch - println("\nERROR YiemAgent extractWineAttributes_2() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + println("\nERROR YiemAgent extractWineAttributes_2() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") continue end - + # check whether all answer's key points are in responsedict - _responsedictKey = keys(responsedict) - responsedictKey = [i for i in _responsedictKey] # convert into a list - is_requiredKeys_in_responsedictKey = [i ∈ responsedictKey for i in requiredKeys] - - if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys) - errornote = "Your previous attempt has more key points than answer's required key points." - println("\nERROR YiemAgent extractWineAttributes_2() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) + if !ispass + errornote = errormsg + println("\nERROR YiemAgent extractWineAttributes_2() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") continue - elseif !all(is_requiredKeys_in_responsedictKey) - zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey) - missingkeys = [requiredKeys[i] for i in zeroind] - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent extractWineAttributes_2() $errornote --> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check whether each describing keyword is in the input to prevent halucination - for i in ["sweetness", "acidity", "tannin", "intensity"] - keyword = Symbol(i * "_keyword") # e.g. sweetness_keyword - value = responsedict[keyword] - if value != "N/A" && !occursin(value, input) - errornote = "In your previous attempt, keyword $keyword: $value does not appear in the input. You must use information from the input only" - println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # if value == "N/A" then responsedict[i] = "N/A" - # e.g. if sweetness_keyword == "N/A" then sweetness = "N/A" - if value == "N/A" - responsedict[Symbol(i)] = "N/A" - end - end - - # some time LLM not put integer range - for (k, v) in responsedict - if !occursin("keyword", string(k)) - if v !== "N/A" && (!occursin('-', v) || length(v) > 5) - errornote = "WARNING: The non-range value {$k: $v} is not allowed. It should be specified in a range format, i.e. min-max." - println("\nERROR YiemAgent extractWineAttributes_2() Attempt $attempt $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - end - end - - # some time LLM says N/A-2. Need to convert N/A to 1 - for (k, v) in responsedict - if occursin("N/A", v) && occursin("-", v) - new_v = replace(v, "N/A"=>"1") - responsedict[k] = new_v - end end # delete some key words from responsedict for (k, v) in responsedict - if k ∈ [:sweetness_keyword, :acidity_keyword, :tannin_keyword, :intensity_keyword] + if k ∈ ["sweetness_keyword", "acidity_keyword", "tannin_keyword", "intensity_keyword"] delete!(responsedict, k) end end + # get result in String. Reject "N/A" value result = "" for (k, v) in responsedict - # some time LLM generate text with "(some comment)". this line removes it - if !occursin("N/A", v) + if typeof(v) <: Number + result *= "$k: $v, " + elseif typeof(v) == String && !occursin("N/A", v) result *= "$k: $v, " end end result = result[1:end-2] # remove the ending ", " - + println("\n--- extractWineAttributes_2-2()") + println(result) + println("--- \n") return result end - error("wineattributes_wordToNumber() failed to get a response") + error("extractWineAttributes_2() failed to get a response") end @@ -866,8 +751,7 @@ function paraphrase(text2textInstructLLM::Function, text::String) Let's begin! """ - #[WORKING] use JSON3 the same as extractWineAttributes_1 is better - #[WORKING] change this function to use the same format use decisionMater + #[PENDING] use JSON the same as extractWineAttributes_1 is better. change this function to use the same format use decisionMaker header = ["Paraphrase:"] dictkey = ["paraphrase"] @@ -883,8 +767,8 @@ function paraphrase(text2textInstructLLM::Function, text::String) _prompt = [ - Dict(:name => "system", :text => systemmsg), - Dict(:name => "user", :text => usermsg) + Dict("name" => "system", "text" => systemmsg), + Dict("name" => "user", "text" => usermsg) ] # put in model format @@ -919,7 +803,7 @@ function paraphrase(text2textInstructLLM::Function, text::String) dictKey=dictkey, symbolkey=true) for i ∈ [:paraphrase] - if length(JSON3.write(responsedict[i])) == 0 + if length(JSON.json(responsedict[i])) == 0 error("$i is empty ", @__FILE__, ":", @__LINE__, " $(Dates.now())") end end @@ -935,7 +819,7 @@ function paraphrase(text2textInstructLLM::Function, text::String) println("\nparaphrase() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") pprintln(Dict(responsedict)) - result = responsedict[:paraphrase] + result = responsedict["paraphrase"] return result catch e @@ -979,7 +863,7 @@ function jsoncorrection(config::T1, input::T2, correctJsonExample::T3; for attempt in 1:maxattempt try - d = copy(JSON3.read(incorrectjson)) + d = copy(JSON.parsefile(incorrectjson)) correctjson = incorrectjson return correctjson catch e @@ -1004,10 +888,10 @@ function jsoncorrection(config::T1, input::T2, correctJsonExample::T3; """ # apply LLM specific instruct format - externalService = config[:externalservice][:text2textinstruct] - llminfo = externalService[:llminfo] +externalService = config["externalservice"]["text2textinstruct"] + llminfo = externalService["llminfo"] prompt = - if llminfo[:name] == "llama3instruct" + if llminfo["name"] == "llama3instruct" formatLLMtext_llama3instruct("system", _prompt) else error("llm model name is not defied yet $(@__LINE__)") @@ -1015,21 +899,21 @@ function jsoncorrection(config::T1, input::T2, correctJsonExample::T3; # send formatted input to user using GeneralUtils.sendReceiveMqttMsg msgMeta = GeneralUtils.generate_msgMeta( - externalService[:mqtttopic], + externalService["mqtttopic"], senderName= "jsoncorrection", senderId= string(uuid4()), receiverName= "text2textinstruct", - mqttBroker= config[:mqttServerInfo][:broker], - mqttBrokerPort= config[:mqttServerInfo][:port], + mqttBroker= config["mqttServerInfo"]["broker"], + mqttBrokerPort= config["mqttServerInfo"]["port"], ) outgoingMsg = Dict( - :msgMeta=> msgMeta, - :payload=> Dict( - :text=> prompt, - :kwargs=> Dict( - :max_tokens=> 512, - :stop=> ["<|eot_id|>"], + "msgMeta"=> msgMeta, + "payload"=> Dict( + "text"=> prompt, + "kwargs"=> Dict( + "max_tokens"=> 512, + "stop"=> ["<|eot_id|>"], ) ) ) @@ -1052,7 +936,7 @@ end # "thought" is step-by-step reasoning about the current situation. # "plan" is what to do to complete the task from the current situation. # “action_name” is the name of the action taken, which can be one of the following functions: -# 1) CHATBOX[text], which you can use to talk with the user. "text" is in verbal English. +# 1) CHAT_BOX[text], which you can use to talk with the user. "text" is in verbal English. # 2) WINESTOCK[query], which you can use to find info about wine in your inventory. "query" is a search term in verbal English. The best query must includes "budget", "type of wine", "characteristics of wine" and "food pairing". # "action_input" is the input to the action # "observation" is result of the preceding immediate action. diff --git a/src/type.jl b/src/type.jl index 242da74..b40b64b 100644 --- a/src/type.jl +++ b/src/type.jl @@ -1,12 +1,23 @@ module type -export agent, sommelier, companion, virtualcustomer +export agent, sommelier, companion, virtualcustomer, agentcontext -using Dates, UUIDs, DataStructures, JSON3 +using Dates, UUIDs, DataStructures, JSON, NATS using GeneralUtils # ---------------------------------------------- 100 --------------------------------------------- # + +mutable struct agentcontext + text2textInstructLLM::Function + getTextEmbedding::Function + executeSQL::Function + similarSQLVectorDB::Function + insertSQLVectorDB::Function + similarSommelierDecision::Function + insertSommelierDecision::Function +end + abstract type agent end mutable struct companion <: agent @@ -15,19 +26,19 @@ mutable struct companion <: agent systemmsg::String # system message tools::Dict # tools maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized - chathistory::Vector{Dict{Symbol, Any}} - memory::Dict{Symbol, Any} - func::NamedTuple # NamedTuple of functions + chathistory::Vector{Dict{String, Any}} + memory::Dict{String, Any} + context::NamedTuple # NamedTuple of functions llmFormatName::String end function companion( - func::NamedTuple # NamedTuple of functions + context::agentcontext # NamedTuple of functions ; name::String= "Assistant", id::String= GeneralUtils.uuid4snakecase(), maxHistoryMsg::Integer= 20, - chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), + chathistory::Vector{Dict{String, String}} = Vector{Dict{String, String}}(), llmFormatName::String= "granite3", systemmsg::String= """ @@ -43,8 +54,8 @@ function companion( ) tools = Dict( # update input format - "CHATBOX"=> Dict( - :description => "- CHATBOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific.", + "CHAT_BOX"=> Dict( + "description" => "- CHAT_BOX which you can use to talk with the user. The input is your intentions for the dialogue. Be specific.", ), ) @@ -52,14 +63,14 @@ function companion( Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3 NO "system" message in chathistory because I want to add it at the inference time chathistory= [ - Dict(:name=>"user", :text=> "Wassup!", :timestamp=> Dates.now()), - Dict(:name=>"assistant", :text=> "Hi I'm your assistant.", :timestamp=> Dates.now()), + Dict("name"=>"user", "text"=> "Wassup!", "timestamp"=> Dates.now()), + Dict("name"=>"assistant", "text"=> "Hi I'm your assistant.", "timestamp"=> Dates.now()), ] """ - memory = Dict{Symbol, Any}( - :events=> Vector{Dict{Symbol, Any}}(), - :state=> Dict{Symbol, Any}(), # state of the agent - :recap=> OrderedDict{Symbol, Any}(), # recap summary of the conversation + memory = Dict{String, Any}( + "events"=> Vector{Dict{String, Any}}(), + "state"=> Dict{String, Any}(), # state of the agent + "recap"=> OrderedDict{String, Any}(), # recap summary of the conversation ) newAgent = companion( @@ -70,7 +81,7 @@ function companion( maxHistoryMsg, chathistory, memory, - func, + context, llmFormatName ) @@ -78,128 +89,126 @@ function companion( end - - -""" A sommelier agent. - -# Arguments - - `mqttClient::Client` - MQTTClient's client - - `msgMeta::Dict{Symbol, Any}` - A dict contain info about a message. - - `config::Dict{Symbol, Any}` - Config info for an agent. Contain mqtt topic for internal use and other info. - -# Keyword Arguments - - `name::String` - Agent's name - - `id::String` - Agent's ID - - `tools::Dict{Symbol, Any}` - Agent's tools - - `maxHistoryMsg::Integer` - max history message - -# Return - - `nothing` - -# Example -```jldoctest -julia> using YiemAgent, MQTTClient, GeneralUtils -julia> msgMeta = GeneralUtils.generate_msgMeta( - "N/A", - replyTopic = "/testtopic/prompt" - ) -julia> tools= Dict( - :chatbox=>Dict( - :name => "chatbox", - :description => "Useful only for when you need to ask the user for more info or context. Do not ask the user their own question.", - :input => "Input should be a text.", - :output => "" , - :func => nothing, - ), - ) -julia> agentConfig = Dict( - :receiveprompt=>Dict( - :mqtttopic=> "/testtopic/prompt", # topic to receive prompt i.e. frontend send msg to this topic - ), - :receiveinternal=>Dict( - :mqtttopic=> "/testtopic/internal", # receive topic for model's internal - ), - :text2text=>Dict( - :mqtttopic=> "/text2text/receive", - ), - ) -julia> client, connection = MakeConnection("test.mosquitto.org", 1883) -julia> agent = YiemAgent.bsommelier( - client, - msgMeta, - agentConfig, - name= "assistant", - id= "555", # agent instance id - tools=tools, - ) -``` - -# TODO -- [] update docstring -- [x] implement the function - -# Signature -""" mutable struct sommelier <: agent name::String # agent name id::String # agent id retailername::String tools::Dict maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized - chathistory::Vector{Dict{Symbol, Any}} - memory::Dict{Symbol, Any} - func # NamedTuple of functions + chathistory::Vector{Dict{String, Any}} + memory::Dict{String, Any} + context::agentcontext llmFormatName::String end + +""" A sommelier agent. +# Arguments + - `context::agentcontext` + Application context containing shared functions for LLM, SQL, and vector database operations. + +# Keyword Arguments + - `name::String` + Agent's name. Default: `"Assistant"` + - `id::String` + Agent's ID. Default: generated UUID string. + - `retailername::String` + Retailer name associated with the sommelier. Default: `"retailer_name"` + - `maxHistoryMsg::Integer` + Maximum history messages. Default: `20` + - `chathistory::Vector{Dict{String, String}}` + Chat history. Default: empty vector. + - `llmFormatName::String` + LLM format name. Default: `"granite3"` + +# Return + - `sommelier`: An instantiated sommelier agent. + +# Example +```julia +julia> using YiemAgent +julia> context = agentcontext( + text2textInstructLLM, + getTextEmbedding, + executeSQL, + similarSQLVectorDB, + insertSQLVectorDB, + similarSommelierDecision, + insertSommelierDecision + ) +julia> agent = sommelier(context, name="WineExpert", id="123", retailername="MyWineShop") +``` +""" function sommelier( - func, # NamedTuple of functions + context::agentcontext, # app context ; name::String= "Assistant", id::String= string(uuid4()), retailername::String= "retailer_name", maxHistoryMsg::Integer= 20, - chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), + chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}(), llmFormatName::String= "granite3" ) tools = Dict( # update input format "chatbox"=> Dict( - :description => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", - :input => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", - :output => "" , + "description" => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", + "input" => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", + "output" => "" , ), "winestock"=> Dict( - :description => "A handy tool for searching wine in your inventory that match the user preferences.", - :input => """Input is a JSON-formatted string that contains a detailed and precise search query.{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}""", - :output => """Output are wines that match the search query in JSON format.""", + "description" => "A handy tool for searching wine in your inventory that match the user preferences.", + "input" => """Input is a JSON-formatted string that contains a detailed and precise search query.{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}""", + "output" => """Output are wines that match the search query in JSON format.""", ), ) """ Memory - Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3 - NO "system" message in chathistory because I want to add it at the inference time + + Chat history use openai format as follow: + + image1_path = "test/large_image.png" --- + image1_bytes = read(image1_path) | this part must be done + image1_base64_string = base64encode(image1_bytes) | in frontend + mime_type = "image/png" | not in agent code + data1_uri = "data:;base64," --- + chathistory= [ - Dict(:name=>"user", :text=> "Wassup!", :timestamp=> Dates.now()), - Dict(:name=>"assistant", :text=> "Hi I'm your assistant.", :timestamp=> Dates.now()), + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => "You are a helpful assistant"), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => " + LLM context here... + + Do you know this wine? Just give me brief intro." + ), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ), + ] + ), ] + + shortmem = Dict( + "1"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), + "2"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), + ... + ) """ - memory = Dict{Symbol, Any}( - :shortmem=> OrderedDict{Symbol, Any}( - :db_search_result=> Any[], - :scratchpad=> "", #[PENDING] should be a dict e.g. Dict(:database_search_result=>Dict(:wines=> "", :search_query=> "")) + memory = Dict{String, Any}( + "shortmem"=> OrderedDict{String, Any}(), + "scratchpad"=> "", + "events"=> Vector{Dict{String, Any}}(), + "state"=> Dict{String, Any}( ), - :events=> Vector{Dict{Symbol, Any}}(), - :state=> Dict{Symbol, Any}( - ), - :recap=> OrderedDict{Symbol, Any}(), + "recap"=> OrderedDict{String, Any}(), ) @@ -211,9 +220,84 @@ function sommelier( maxHistoryMsg, chathistory, memory, - func, + context, llmFormatName ) + + systemmsg = + """ + + - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. + - If you found wines in the store's database, they are in stock. + - You can only recommend wines that are currently in our inventory + - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. + - Ask the user one question at a time. + - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. + - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. + - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. + - Spicy foods should be paired only with light red wines. + - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such. + - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. + + + - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. + - Customer may provide images for you to look up. + - Encourage the customer to explore different options and try new things. + - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + - Your store carries only wine. + - Vintage 0 means non-vintage. + - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. + + + Your customer is coming into the store + + + Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store. You are working under your mentor supervision. + + + 1) Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. + 2) Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. + + + 1) According to the store's policy and guidelines, make an informed decision about what you need to do to achieve the objective + 2) Keep the conversation with the customer going smoothly + 2) Obey your mentor's suggestions. + + + 1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. + 2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. + 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. + + + 1) plan: Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. + 2) action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name + 3) action_input: The input to the action you are about to perform according to your plan. + After the action is executed you gets "action_result". It is the output from the action you selected. + + + "plan": "...", + "action_name": "...", + "action_input": "..." + + + - CHAT_BOX which you can use to talk with the user. + - CHECK_WINE allows you to check information about wines you want in your inventory's database. The input is text that specify supported search criteria includeing: retailer_name, wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. + Example query 1: "Dry, full-bodied red wine from 1) region: Burgundy, country: France or 2) region: Tuscany, country: Italy. Grape varietal: Merlot or Syrah. price 100 to 1000 USD." + Example query 2: "Red or white wine, medium tannin, price under 700 USD" + Example query 3: "white wine, region: Tuscany or Bordeaux, country: Italy or France + - PRESENT_WINE_GUIDELINE which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. + - END_CONVER_GUIDELINE which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow. + + """ + + system_msg = Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ) + + push!(newAgent.chathistory, system_msg) return newAgent end @@ -225,19 +309,19 @@ mutable struct virtualcustomer <: agent systemmsg::String # system message tools::Dict maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized - chathistory::Vector{Dict{Symbol, Any}} - memory::Dict{Symbol, Any} - func # NamedTuple of functions + chathistory::Vector{Dict{String, Any}} + memory::Dict{String, Any} + context # NamedTuple of functions llmFormatName::String end function virtualcustomer( - func, # NamedTuple of functions + context, # NamedTuple of functions ; name::String= "Assistant", id::String= string(uuid4()), maxHistoryMsg::Integer= 20, - chathistory::Vector{Dict{Symbol, String}} = Vector{Dict{Symbol, String}}(), + chathistory::Vector{Dict{String, String}} = Vector{Dict{String, String}}(), llmFormatName::String= "granite3", systemmsg::String= """ @@ -254,27 +338,41 @@ function virtualcustomer( tools = Dict( # update input format "chatbox"=> Dict( - :description => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", - :input => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", - :output => "" , + "description" => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", + "input" => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", + "output" => "" , ), ) """ Memory - Ref: Chat prompt format https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML/discussions/3 - NO "system" message in chathistory because I want to add it at the inference time - chathistory= [ - Dict(:name=>"user", :text=> "Wassup!", :timestamp=> Dates.now()), - Dict(:name=>"assistant", :text=> "Hi I'm your assistant.", :timestamp=> Dates.now()), + Ref: Chat prompt format is openai + chathistory = [ + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => system_msg), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ) + ] + ) ] """ - memory = Dict{Symbol, Any}( - :shortmem=> OrderedDict{Symbol, Any}( + memory = Dict{String, Any}( + "shortmem"=> OrderedDict{String, Any}( ), - :events=> Vector{Dict{Symbol, Any}}(), - :state=> Dict{Symbol, Any}( + "scratchpad"=> "", + "events"=> Vector{Dict{String, Any}}(), + "state"=> Dict{String, Any}( ), - :recap=> OrderedDict{Symbol, Any}(), + "recap"=> OrderedDict{String, Any}(), ) newAgent = virtualcustomer( @@ -285,7 +383,7 @@ function virtualcustomer( maxHistoryMsg, chathistory, memory, - func, + context, llmFormatName ) diff --git a/src/util.jl b/src/util.jl index 763d37c..eaeefb2 100644 --- a/src/util.jl +++ b/src/util.jl @@ -1,9 +1,10 @@ module util export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline, - availableWineToText, createEventsLog, createChatLog + availableWineToText, createEventsLog, createChatLog, checkAgentResponse_JSON, + checkAgentResponse_text -using UUIDs, Dates, DataStructures, HTTP, JSON3 +using UUIDs, Dates, DataStructures, HTTP, JSON using GeneralUtils using ..type @@ -25,16 +26,16 @@ julia> client, connection = MakeConnection("test.mosquitto.org", 1883) julia> connect(client, connection) julia> msgMeta = GeneralUtils.generate_msgMeta("testtopic") julia> agentConfig = Dict( - :receiveprompt=>Dict( - :mqtttopic=> "testtopic/receive", - ), - :receiveinternal=>Dict( - :mqtttopic=> "testtopic/internal", - ), - :text2text=>Dict( - :mqtttopic=> "testtopic/text2text", - ), - ) + "receiveprompt"=>Dict( + "mqtttopic"=> "testtopic/receive", + ), + "receiveinternal"=>Dict( + "mqtttopic"=> "testtopic/internal", + ), + "text2text"=>Dict( + "mqtttopic"=> "testtopic/text2text", + ), + ) julia> a = YiemAgent.sommelier( client, msgMeta, @@ -51,14 +52,25 @@ julia> YiemAgent.clearhistory(a) """ function clearhistory(a::T) where {T<:agent} empty!(a.chathistory) - empty!(a.memory[:shortmem]) - empty!(a.memory[:events]) - a.memory[:chatbox] = "" + empty!(a.memory["shortmem"]) + empty!(a.memory["events"]) + a.memory["chatbox"] = "" end """ Add new message to agent. + messages => Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Describe this image for me"), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data_uri) + ) + ] + ) + Arguments\n ----- a::agent @@ -75,44 +87,24 @@ end Example\n ----- ```jldoctest - julia> using YiemAgent, MQTTClient, GeneralUtils - julia> client, connection = MakeConnection("test.mosquitto.org", 1883) - julia> connect(client, connection) - julia> msgMeta = GeneralUtils.generate_msgMeta("testtopic") - julia> agentConfig = Dict( - :receiveprompt=>Dict( - :mqtttopic=> "testtopic/receive", - ), - :receiveinternal=>Dict( - :mqtttopic=> "testtopic/internal", - ), - :text2text=>Dict( - :mqtttopic=> "testtopic/text2text", - ), - ) - julia> a = YiemAgent.sommelier( - client, - msgMeta, - agentConfig, - ) - julia> YiemAgent.addNewMessage(a, "user", "hello") + ``` Signature\n ----- """ -function addNewMessage(a::T1, name::String, text::T2; - maximumMsg::Integer=20) where {T1<:agent, T2<:AbstractString} +function addNewMessage(a::T1, name::String, userinput::T2; + maximumMsg::Integer=30) where {T1<:agent, T2<:AbstractDict} if name ∉ ["system", "user", "assistant"] # guard against typo error("name is not in agent.availableRole $(@__LINE__)") end - #[PENDING] summarize the oldest 10 message + #TODO summarize the oldest 10 message if length(a.chathistory) > maximumMsg summarize(a.chathistory) else - d = Dict(:name=> name, :text=> text, :timestamp=> Dates.now()) - push!(a.chathistory, d) + userinput["timestamp"] = Dates.now() + push!(a.chathistory, userinput) end end @@ -137,7 +129,7 @@ This function takes in a vector of dictionaries and outputs a single string wher julia> using Revise julia> using GeneralUtils -julia> vecd = [Dict(:name => "John", :text => "Hello"), Dict(:name => "Jane", :text => "Goodbye")] +julia> vecd = [Dict("name" => "John", "text" => "Hello"), Dict("name" => "Jane", "text" => "Goodbye")] julia> GeneralUtils.vectorOfDictToText(vecd, withkey=true) "John> Hello\nJane> Goodbye\n" ``` @@ -208,9 +200,9 @@ end The subject or entity associated with the event - `thought::Union{AbstractDict, Nothing}` Any associated thoughts or metadata - - `actionname::Union{String, Nothing}` + - `action_name::Union{String, Nothing}` The name of the action performed (e.g., "CHAT", "CHECKINVENTORY") - - `actioninput::Union{String, Nothing}` + - `action_input::Union{String, Nothing}` Input or parameters for the action - `location::Union{String, Nothing}` Where the event took place @@ -231,27 +223,27 @@ function eventdict(; timestamp::Union{DateTime, Nothing}=nothing, subject::Union{String, Nothing}=nothing, thought::Union{AbstractDict, Nothing}=nothing, - actionname::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENTBOX", etc - actioninput::Union{String, Nothing}=nothing, + action_name::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENT_WINE_GUIDELINE", etc + action_input::Union{String, Nothing}=nothing, location::Union{String, Nothing}=nothing, equipment_used::Union{String, Nothing}=nothing, material_used::Union{String, Nothing}=nothing, - outcome::Union{String, Nothing}=nothing, + observation::Union{String, Nothing}=nothing, note::Union{String, Nothing}=nothing, ) - d = Dict{Symbol, Any}( - :event_description=> event_description, - :timestamp=> timestamp, - :subject=> subject, - :thought=> thought, - :actionname=> actionname, - :actioninput=> actioninput, - :location=> location, - :equipment_used=> equipment_used, - :material_used=> material_used, - :outcome=> outcome, - :note=> note, + d = Dict{String, Any}( + "event_description"=> event_description, + "timestamp"=> timestamp, + "subject"=> subject, + "thought"=> thought, + "action_name"=> action_name, + "action_input"=> action_input, + "location"=> location, + "equipment_used"=> equipment_used, + "material_used"=> material_used, + "observation"=> observation, + "note"=> note, ) return d @@ -262,22 +254,22 @@ end # Arguments - `events::T1` - Vector of event dictionaries containing subject, actioninput and optional outcome fields + Vector of event dictionaries containing subject, action_input and optional outcome fields Each event dictionary should have the following keys: - :subject - The subject or entity performing the action - - :actioninput - The action or input performed by the subject - - :outcome - (Optional) The result or outcome of the action + - :action_input - The action or input performed by the subject + - :observation - (Optional) The result or outcome of the action # Returns - `timeline::String` A formatted string representing the events with their subjects, actions, and optional outcomes - Format: "{index}) {subject}> {actioninput} {outcome}\n" for each event + Format: "{index}) {subject}> {action_input} {outcome}\n" for each event # Example events = [ - Dict(:subject => "User", :actioninput => "Hello", :outcome => nothing), - Dict(:subject => "Assistant", :actioninput => "Hi there!", :outcome => "with a smile") + Dict("subject" => "User", "action_input" => "Hello", "observation" => nothing), + Dict("subject" => "Assistant", "action_input" => "Hi there!", "observation" => "with a smile") ] timeline = createTimeline(events) # 1) User> Hello @@ -297,59 +289,29 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin 1:length(events) end - #[WORKING] Iterate through events and format each one + # Iterate through events and format each one for i in ind event = events[i] # If no outcome exists, format without outcome - # if event[:actionname] == "CHATBOX" - # timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput])\n" - # elseif event[:actionname] == "CHECKINVENTORY" && event[:outcome] === nothing - # timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" - # If outcome exists, include it in formatting - if event[:actionname] == "CHECKWINE" - timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n" +# if event["action_name"] == "CHAT_BOX" + # timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\n" + # elseif event["action_name"] == "CHECKINVENTORY" && event["observation"] === nothing + # timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: Not done yet.\n" + if event["action_name"] == "CHECK_WINE" + timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: $(event["observation"])\\n" else - timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput])\n" + timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\\n" end end # Return formatted timeline string return timeline end -# function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing -# ) where {T1<:AbstractVector} -# # Initialize empty timeline string -# timeline = "" - -# # Determine which indices to use - either provided range or full length -# ind = -# if eventindex !== nothing -# [eventindex...] -# else -# 1:length(events) -# end - -# # Iterate through events and format each one -# for i in ind -# event = events[i] -# # If no outcome exists, format without outcome -# if event[:outcome] === nothing -# timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: Not done yet.\n" -# # If outcome exists, include it in formatting -# else -# timeline *= "Event_$i $(event[:subject])> action_name: $(event[:actionname]), action_input: $(event[:actioninput]), observation: $(event[:outcome])\n" -# end -# end - -# # Return formatted timeline string -# return timeline -# end - function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing ) where {T1<:AbstractVector} # Initialize empty log array - log = Dict{Symbol, String}[] + log = Dict{String, String}[] # Determine which indices to use - either provided range or full length ind = @@ -363,17 +325,20 @@ function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing for i in ind event = events[i] # If no outcome exists, format without outcome - if event[:outcome] === nothing - subject = event[:subject] - actioninput = event[:actioninput] - d = Dict{Symbol, String}(:name=>subject, :text=>actioninput) + if event["observation"] === nothing + subject = event["subject"] + action_name = event["action_name"] + action_input = event["action_input"] + str = "action_name: $action_name, action_input: $action_input" + d = Dict{String, String}("name"=>subject, "text"=>str) push!(log, d) else - subject = event[:subject] - actioninput = event[:actioninput] - outcome = event[:outcome] - str = "Action: $actioninput Outcome: $outcome" - d = Dict{Symbol, String}(:name=>subject, :text=>str) + subject = event["subject"] + action_name = event["action_name"] + action_input = event["action_input"] + observation = event["observation"] + str = "action_name: $action_name, action_input: $action_input, observation: $observation" + d = Dict{String, String}("name"=>subject, "text"=>str) push!(log, d) end end @@ -385,7 +350,7 @@ end function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing ) where {T1<:AbstractVector} # Initialize empty log array - log = Dict{Symbol, String}[] + log = Dict{String, String}[] # Determine which indices to use - either provided range or full length ind = @@ -398,9 +363,9 @@ function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing # Iterate through events and format each one for i in ind event = chatdict[i] - subject = event[:name] - text = event[:text] - d = Dict{Symbol, String}(:name=>subject, :text=>text) + subject = event["name"] + text = event["text"] + d = Dict{String, String}("name"=>subject, "text"=>text) push!(log, d) end @@ -408,6 +373,49 @@ function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing end +function checkAgentResponse_text(response::String, requiredHeader::T + )::Tuple where {T<:Array{String}} + detected_kw = GeneralUtils.detectKeywordVariation(requiredHeader, response) + missingkeys = [k for (k, v) in detected_kw if v === nothing] + ispass = false + errormsg = nothing + if !isempty(missingkeys) + errormsg = "$missingkeys are missing from your previous response" + ispass = false + elseif sum([length(i) for i in values(detected_kw)]) > length(requiredHeader) + errormsg = "Your previous attempt has duplicated points according to the required response format" + ispass = false + else + ispass = true + end + return (ispass, errormsg) +end + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_1.jl b/test/chatting_with_agent.jl similarity index 92% rename from test/test_1.jl rename to test/chatting_with_agent.jl index f779d5c..5e66a0f 100644 --- a/test/test_1.jl +++ b/test/chatting_with_agent.jl @@ -8,8 +8,8 @@ using Base.Threads # load config -config = JSON3.read("/appfolder/app/dev/YiemAgent/test/config.json") -# config = copy(JSON3.read("../mountvolume/config.json")) +config = JSON.parsefile("/appfolder/app/dev/YiemAgent/test/config.json") +# config = copy(JSON.parsefile("../mountvolume/config.json")) function executeSQL(sql::T) where {T<:AbstractString} @@ -179,7 +179,7 @@ function similarSommelierDecision(recentevents::T1; maxdistance::Integer=3 println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__) output_b64 = df[1, :function_output_base64] # pick the closest match _output_str = String(base64decode(output_b64)) - output = copy(JSON3.read(_output_str)) + output = copy(JSON.parsefile(_output_str)) return output else println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__) @@ -199,7 +199,7 @@ function insertSommelierDecision(recentevents::T1, decision::T2; maxdistance::In if row == 0 || distance > maxdistance # no close enough SQL stored in the database recentevents_embedding = getEmbedding(recentevents)[1] recentevents = replace(recentevents, "'" => "") - decision_json = JSON3.write(decision) + decision_json = JSON.json(decision) decision_base64 = base64encode(decision_json) decision = replace(decision_json, "'" => "") @@ -239,7 +239,9 @@ a = YiemAgent.sommelier( while true print("\nyour respond: ") user_answer = readline() - response = YiemAgent.conversation(a, Dict(:text=> user_answer)) + response = YiemAgent.conversation(agent; + userinput=Dict(:text=> user_answer), + maximumMsg=50) println("\n$response") end diff --git a/test/large_image.png b/test/large_image.png new file mode 100644 index 0000000..2a89fd8 Binary files /dev/null and b/test/large_image.png differ diff --git a/test/prompttest.jl b/test/prompttest.jl index bab5a3a..b75dd4c 100644 --- a/test/prompttest.jl +++ b/test/prompttest.jl @@ -46,7 +46,7 @@ thoughtDict = OrderedDict( :Observation_6=> "I don't like it. Do you have another option?", ) -_thoughtJsonStr = JSON3.write(thoughtDict) +_thoughtJsonStr = JSON.json(thoughtDict) thoughtJsonStr = _thoughtJsonStr[1:end-1] # remove } at the end # @show thoughtJsonStr @@ -100,7 +100,7 @@ Here are some examples: Let's begin! -$(JSON3.write(thoughtDict)) +$(JSON.json(thoughtDict)) {Thought_$nextThoughtIndice """ diff --git a/test/prompttest_1.jl b/test/prompttest_1.jl index 2804269..229fbc6 100644 --- a/test/prompttest_1.jl +++ b/test/prompttest_1.jl @@ -4,7 +4,7 @@ using Base.Threads # ---------------------------------------------- 100 --------------------------------------------- # -config = copy(JSON3.read("config.json")) +config = copy(JSON.parsefile("config.json")) instanceInternalTopic = config[:serviceInternalTopic][:mqtttopic] * "/1" @@ -66,7 +66,7 @@ tools=Dict( # update input format input = -OrderedDict{Symbol, Any}(:question => "Hello, I would like a get a bottle of wine", :thought_1 => "It's great that the user is looking for a bottle of wine. To give them a personalized recommendation, I need to know more about their preferences.", :action_1 => Dict{Symbol, Any}(:name => "chatbox", :input => "What occasion are you planning to use this wine for?"), :observation_1 => "We are holding a wedding party", :thought_2 => "A wedding party is a great occasion for a special bottle of wine. I need to know what type of food will be served, and how much the user is willing to spend.", :action_2 => Dict{Symbol, Any}(:name => "chatbox", :input => "What type of food will you be serving at the wedding?"), :observation_2 => "It will be Thai dishes.", :thought_3 => "The type of wine that pairs well with Thai dishes is usually a crisp and refreshing white wine, but I also need to consider the budget and personal preferences.", :action_3 => Dict{Symbol, Any}(:name => "chatbox", :input => "How much are you willing to spend on this bottle of wine?"), :observation_3 => "I would spend up to 50 bucks.", :thought_4 => "I have a good idea of the occasion, food, and budget. Now I need to know what type of wine the user is looking for.", :action_4 => Dict{Symbol, Any}(:name => "chatbox", :input => "What type of wine are you usually looking for? Red, White, Sparkling, Rose, Dessert or Fortified?"), :observation_4 => "I like full-bodied Red wine with low tannin.", :thought_5 => "Now that I have all the necessary information, I can start searching for a suitable wine in our inventory.", :action_5 => Dict{Symbol, Any}(:name => "winestock", :input => "red wine with low tannins"), :observation_5 => "I found the following wines in our stock: \n{\n 1: El Enemigo Cabernet Franc 2019\n2: Tantara Chardonnay 2017\n\n}\n", :thought_6 => "Now that I have the information about the wine, it's time to make a recommendation.", :action_6 => Dict{Symbol, Any}(:name => "recommendbox", :input => "El Enemigo Cabernet Franc 2019"), :observation_6 => "I don't like the one you recommend. I want dry wine.") +OrderedDict{String, Any}(:question => "Hello, I would like a get a bottle of wine", :thought_1 => "It's great that the user is looking for a bottle of wine. To give them a personalized recommendation, I need to know more about their preferences.", :action_1 => Dict{String, Any}(:name => "chatbox", :input => "What occasion are you planning to use this wine for?"), :observation_1 => "We are holding a wedding party", :thought_2 => "A wedding party is a great occasion for a special bottle of wine. I need to know what type of food will be served, and how much the user is willing to spend.", :action_2 => Dict{String, Any}(:name => "chatbox", :input => "What type of food will you be serving at the wedding?"), :observation_2 => "It will be Thai dishes.", :thought_3 => "The type of wine that pairs well with Thai dishes is usually a crisp and refreshing white wine, but I also need to consider the budget and personal preferences.", :action_3 => Dict{String, Any}(:name => "chatbox", :input => "How much are you willing to spend on this bottle of wine?"), :observation_3 => "I would spend up to 50 bucks.", :thought_4 => "I have a good idea of the occasion, food, and budget. Now I need to know what type of wine the user is looking for.", :action_4 => Dict{String, Any}(:name => "chatbox", :input => "What type of wine are you usually looking for? Red, White, Sparkling, Rose, Dessert or Fortified?"), :observation_4 => "I like full-bodied Red wine with low tannin.", :thought_5 => "Now that I have all the necessary information, I can start searching for a suitable wine in our inventory.", :action_5 => Dict{String, Any}(:name => "winestock", :input => "red wine with low tannins"), :observation_5 => "I found the following wines in our stock: \n{\n 1: El Enemigo Cabernet Franc 2019\n2: Tantara Chardonnay 2017\n\n}\n", :thought_6 => "Now that I have the information about the wine, it's time to make a recommendation.", :action_6 => Dict{String, Any}(:name => "recommendbox", :input => "El Enemigo Cabernet Franc 2019"), :observation_6 => "I don't like the one you recommend. I want dry wine.") result = YiemAgent.jsoncorrection(a, input) diff --git a/test/runtests.jl b/test/runtests.jl index e69de29..b4ff3e6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -0,0 +1,223 @@ + +using JSON, Dates, UUIDs, PrettyPrinting, Base64, NATS, HTTP +using GeneralUtils, msghandler + +config = JSON.parsefile("./appconfig.json") + +agent_conn = NATS.connect(config["nats_server_info"]["url"]) + +function text2text_instruct_llm(sender_id::String, openai_msg::Dict{String, Any}) + payloads = [("msg", openai_msg, "dictionary")] # List of tuples + _, msg_envelope_json_str = msghandler.smartpack( + config["externalservice"]["servicesloadbalancer"]["nats"], + payloads; + sender_id=sender_id, + msg_purpose="text2text", + broker_url=config["nats_server_info"]["url"], + fileserver_url=config["externalservice"]["fileserver"]["url"]) + + reply = NATS.request(agent_conn, + config["externalservice"]["servicesloadbalancer"]["nats"], + msg_envelope_json_str, timeout=120) + + incoming_env_json_str = String(reply.payload) + incoming_env = msghandler.smartunpack(incoming_env_json_str) + _llm_response = incoming_env["payloads"][1][2] + llm_response = _llm_response["choices"][1]["message"]["content"] + return llm_response +end + + + + +# 1. Read local file and encode to base64 string +image1_path = "test/large_image.png" +image1_bytes = read(image1_path) +image1_base64_string = base64encode(image1_bytes) + +# 2. Match the MIME type according to your file extension (e.g., png, jpeg) +mime_type = "image/png" +data1_uri = "data:$(mime_type);base64,$(image1_base64_string)" + +# 3. Construct payload with the Data URI +openai_msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => [ + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ) + ] + ) + ], + "temperature" => 0.7 +) + +llm_response = text2text_instruct_llm(openai_msg) + + + +# 1. Read local file and encode to base64 string +image2_path = "test/large_image.png" +image2_bytes = read(image2_path) +image2_base64_string = base64encode(image2_bytes) + +# 2. Match the MIME type according to your file extension (e.g., png, jpeg) +mime_type = "image/png" +data2_uri = "data:$(mime_type);base64,$(image2_base64_string)" + +systemmsg = + """ + # Store Policy + - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. + - If you found wines in the store's database, they are in stock. + - You can only recommend wines that are currently in our inventory + - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. + - Ask the user one question at a time. + - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. + - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. + - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. + - Spicy foods should be paired only with light red wines. + - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such. + - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. + + # Store Guidelines + - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. + - Customer may provide images for you to look up. + - Encourage the customer to explore different options and try new things. + - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. + - Your store carries only wine. + - Vintage 0 means non-vintage. + - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. + + # Situation + Your customer is coming into the store + + # Role + Your name is Janie. You are a helpful sommelier for website-based Yiem Wine's wine store. You are working under your mentor supervision. + + # Objective + 1. Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. + 2. Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. + + # Responsibility Includes + 1. According to the store's policy and guidelines, make an informed decision about what you need to do to achieve the objective + 2. Keep the conversation with the customer going smoothly + 3. Obey your mentor's suggestions. + + # Responsibility Does NOT Include + + 1. Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. + 2. Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. + 3. Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. + + # You should then respond to the user with interleaving plan, action_name, action_input + 1) plan: Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. + 2) action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name + 3) action_input: The input to the action you are about to perform according to your plan. + After the action is executed you gets "action_result". It is the output from the action you selected. + + # You should only respond in JSON format as described below + "plan": "...", + "action_name": "...", + "action_input": "..." + + # Available Actions + - **CHAT_BOX** which you can use to talk with the user. + - **CHECK_WINE** allows you to check information about wines you want in your inventory's database. The input is text that specify supported search criteria includeing: retailer_name, wine price, winery, name, vintage, region, country, type, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. + - Example query 1: "Dry, full-bodied red wine from 1) region: Burgundy, country: France or 2) region: Tuscany, country: Italy. Grape varietal: Merlot or Syrah. price 100 to 1000 USD." + - Example query 2: "Red or white wine, medium tannin, price under 700 USD" + - Example query 3: "white wine, region: Tuscany or Bordeaux, country: Italy or France + - **PRESENT_WINE_GUIDELINE** which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. + - **END_CONVER_GUIDELINE** which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow. + """ + + +openai_msg = Dict( + "model" => "gemma-4-E4B-it-UD-Q4_K_XL", + "messages" => [ + Dict( + "role" => "system", + "content" => [ + Dict("type" => "text", "text" => systemmsg), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."), + ] + ), + Dict( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => + """ + " I will greet the customer warmly as Janie, acknowledge their request to find a similar wine for their wedding party based on the image, identify the wine type and country (Italian Sparkling Wine), and then use the CHECK_WINE action to search the inventory for suitable options.\n CHAT_BOX\n Hello! I'm Janie, and I'd be delighted to help you find the perfect wine for your wedding party. That beautiful wine in the image appears to be an Italian sparkling wine, which is wonderful for a celebration like a wedding! Since you have an unlimited budget, I can certainly look for some truly exceptional options. To start, I will check our inventory for similar Italian sparkling wines that are perfect for a wedding celebration. User response in the next message " + """ + ), + ] + ), + Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "ok"), + ] + ), + ], + "temperature" => 0.7 +) + +llm_response = text2text_instruct_llm(openai_msg) + + + + + +# ---------------------------------------------- 100 --------------------------------------------- # + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/small_image.png b/test/small_image.png new file mode 100644 index 0000000..7ac301b Binary files /dev/null and b/test/small_image.png differ diff --git a/test/test1.jl b/test/test1.jl new file mode 100644 index 0000000..654c4dc --- /dev/null +++ b/test/test1.jl @@ -0,0 +1,291 @@ +using JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures, HTTP, Base64, + NATS, Base.Threads +using YiemAgent, GeneralUtils, msghandler + + +function text2text_instruct_llm(sender_id::String, openai_msg::Dict{String, Any}) + payloads = [("msg", openai_msg, "dictionary")] # List of tuples + _, msg_envelope_json_str = msghandler.smartpack( + config["externalService"]["servicesloadbalancer"]["nats"], + payloads; + sender_id=sender_id, + msg_purpose="text2text", + broker_url=config["nats_server_info"]["url"], + fileserver_url=config["externalService"]["fileserver"]["url"]) + + reply = NATS.request(agent_conn, + config["externalService"]["servicesloadbalancer"]["nats"], + msg_envelope_json_str, timeout=120) + + incoming_env_json_str = String(reply.payload) + incoming_env = msghandler.smartunpack(incoming_env_json_str) + _llm_response = incoming_env["payloads"][1][2] + llm_response = _llm_response["choices"][1]["message"]["content"] + return llm_response +end + +#TESTING get text embedding from a LLM service +function get_embedding(text::AbstractArray{String}) + documents_dict = Dict("documents" => text) + payloads = [("documents", documents_dict, "dictionary")] + _, msg_envelope_json_str = msghandler.smartpack( + config["externalService"]["servicesloadbalancer"]["nats"], + payloads; + msg_purpose="embedding", + broker_url=config["nats_server_info"]["url"], + fileserver_url=config["externalService"]["fileserver"]["url"]) + + reply = NATS.request(agent_conn, + config["externalService"]["servicesloadbalancer"]["nats"], + msg_envelope_json_str, timeout=120) + incoming_env_json_str = String(reply.payload) + incoming_env = msghandler.smartunpack(incoming_env_json_str) + embedding_response = incoming_env["payloads"][1][2] + + return embedding_response +end + +#TESTING +function execute_sql_winedb(config::JSON.Object, sql::T) where {T<:AbstractString} + host_url, _port = split(config["externalservice"]["sommpanion_db"]["url"], ':') + port = parse(Int, _port) + dbname = "winedb" + user = config["externalservice"]["sommpanion_db"]["user"] + password = config["externalservice"]["sommpanion_db"]["password"] + db_connection = LibPQ.Connection("host=$host_url port=$port dbname=$dbname user=$user password=$password") + result = LibPQ.execute(db_connection, sql) + LibPQ.close(db_connection) + return result +end + +#TESTING +function similar_sql_vectordb(query; maxdistance::Integer=100) + tablename = "sqlllm_decision_repository" + # get embedding of the query + df = find_similar_text_from_vectordb(query, tablename, + "function_input_embedding", execute_sql_vectordb) + # println(df[1, [:id, :function_output]]) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + # distance = 100 # CHANGE this is for testing only + if row != 0 && distance < maxdistance + # if there is usable SQL, return it. + output_b64 = df[1, :function_output_base64] # pick the closest match + output_str = String(base64decode(output_b64)) + rowid = df[1, :id] + println("\n~~~ found similar sql. row id $rowid, distance $distance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + return (dict=output_str, distance=distance) + else + println("\n~~~ similar sql not found, max distance $maxdistance ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + return (dict=nothing, distance=nothing) + end +end + +#TESTING +function insert_sql_vectordb(query::T1, SQL::T2; maxdistance::Integer=3) where {T1<:AbstractString, T2<:AbstractString} + tablename = "sqlllm_decision_repository" + # get embedding of the query + # query = state[:thoughtHistory][:question] + df = find_similar_text_from_vectordb(query, tablename, + "function_input_embedding", execute_sql_vectordb) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + if row == 0 || distance > maxdistance # no close enough SQL stored in the database + _query_embedding = get_embedding([query])[1] + query_embedding = _query_embedding["data"][1]["embedding"] + query = replace(query, "'" => "") + sql_base64 = base64encode(SQL) + sql_ = replace(SQL, "'" => "") + + sql = """ + INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$query', '$sql_', '$sql_base64', '$query_embedding'); + """ + # println("\n~~~ added new decision to vectorDB ", @__FILE__, ":", @__LINE__, " $(Dates.now())") + # println(sql) + _ = execute_sql_vectordb(sql) + end +end + +#TESTING +function execute_sql_vectordb(sql::T) where {T<:AbstractString} + host_url, _port = split(config["SQLVectorDB"]["url"], ':') + port = parse(Int, _port) + dbname = config[:externalservice][:SQLVectorDB][:dbname] + user = config[:externalservice][:SQLVectorDB][:user] + password = config[:externalservice][:SQLVectorDB][:password] + DBconnection = LibPQ.Connection("host=$host_url port=$port dbname=$dbname user=$user password=$password") + result = LibPQ.execute(DBconnection, sql) + close(DBconnection) + return result +end + + +function similar_sommelier_decision(recentevents::T1; maxdistance::Integer=3 + )::Union{AbstractDict, Nothing} where {T1<:AbstractString} + tablename = "sommelier_decision_repository" + # find similar + println("\n~~~ search vectorDB for this: $recentevents ", @__FILE__, " ", @__LINE__) + df = find_similar_text_from_vectordb(recentevents, tablename, + "function_input_embedding", execute_sql_vectordb) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + if row != 0 && distance < maxdistance + # if there is usable decision, return it. + rowid = df[1, :id] + println("\n~~~ found similar decision. row id $rowid, distance $distance ", @__FILE__, " ", @__LINE__) + output_b64 = df[1, :function_output_base64] # pick the closest match + _output_str = String(base64decode(output_b64)) + output = copy(JSON.read(_output_str)) + return output + else + println("\n~~~ similar decision not found, max distance $maxdistance ", @__FILE__, " ", @__LINE__) + return nothing + end +end + +#TESTING +function find_similar_text_from_vectordb(text::T1, tablename::T2, embeddingColumnName::T3, + vectorDB::Function; limit::Integer=1 + )::DataFrame where {T1<:AbstractString, T2<:AbstractString, T3<:AbstractString} + # get embedding from LLM service + _embedding = get_embedding([text])[1] + embedding = _embedding["data"][1]["embedding"] + # check whether there is close enough vector already store in vectorDB. if no, add, else skip + sql = """ + SELECT *, $embeddingColumnName <-> '$embedding' as distance + FROM $tablename + ORDER BY distance LIMIT $limit; + """ + response = vectorDB(sql) + df = DataFrame(response) + return df +end + + +function insert_sommelier_decision(recentevents::T1, decision::T2; maxdistance::Integer=5 + ) where {T1<:AbstractString, T2<:AbstractDict} + tablename = "sommelier_decision_repository" + # find similar + df = find_similar_text_from_vectordb(recentevents, tablename, + "function_input_embedding", execute_sql_vectordb) + row, col = size(df) + distance = row == 0 ? Inf : df[1, :distance] + if row == 0 || distance > maxdistance # no close enough SQL stored in the database + _embedding = get_embedding([recentevents])[1] + recentevents_embedding = _embedding["data"][1]["embedding"] + recentevents = replace(recentevents, "'" => "") + decision_json = JSON.json(decision) + decision_base64 = base64encode(decision_json) + decision = replace(decision_json, "'" => "") + + sql = + """ + INSERT INTO $tablename (function_input, function_output, function_output_base64, function_input_embedding) VALUES ('$recentevents', '$decision', '$decision_base64', '$recentevents_embedding'); + """ + println("\n~~~ added new decision to vectorDB ", @__FILE__, " ", @__LINE__) + println(sql) + _ = execute_sql_vectordb(sql) + else + println("~~~ similar decision previously cached, distance $distance ", @__FILE__, " ", @__LINE__) + end +end + +sessionId = "0" +backend_session_topic = "sommpanion.backend.agentbackend.v1.inbox.$sessionId" + +config = JSON.parsefile("./dummy_config.json") +agent_ch = Channel(8) +agent_conn = NATS.connect(config["nats_server_info"]["url"]) +sub2 = NATS.subscribe(agent_conn, backend_session_topic) do msg + put!(agent_ch, msg) +end + +agent_context = YiemAgent.agentcontext( + text2text_instruct_llm, + get_embedding, + execute_sql_winedb, + similar_sql_vectordb, + insert_sql_vectordb, + similar_sommelier_decision, + insert_sommelier_decision + ) + +# can't instantiate +agent = YiemAgent.sommelier( + agent_context; + name="Janie", + id=sessionId, # agent instance id + retailername="Yiem", + llmFormatName="" +) + + + + +# 1. Read local file and encode to base64 string +image1_path = "test/large_image.png" +image1_bytes = read(image1_path) +image1_base64_string = base64encode(image1_bytes) + +# 2. Match the MIME type according to your file extension (e.g., png, jpeg) +mime_type = "image/png" +data1_uri = "data:$(mime_type);base64,$(image1_base64_string)" + +# 3. Construct payload with the Data URI +usermsg = Dict{String, Any}( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "รู้จักไวน์ที่อยู่ในรูปมั้ย"), + Dict( + "type" => "image_url", + "image_url" => Dict("url" => data1_uri) + ) + ] + ) + +result = YiemAgent.conversation(agent; userinput=usermsg) +println(result) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/tiny_image.png b/test/tiny_image.png new file mode 100644 index 0000000..06cf327 Binary files /dev/null and b/test/tiny_image.png differ