v0.4.0-fix_appcontext #1

Merged
ton merged 9 commits from v0.4.0-fix_appcontext into v0.4.0 2026-07-04 06:33:32 +00:00
19 changed files with 2161 additions and 1640 deletions
+169 -23
View File
@@ -2,7 +2,31 @@
julia_version = "1.12.6"
manifest_format = "2.0"
project_hash = "e6bd85ad2679c39ab370f878253f2eeb45c1b6ae"
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,12 @@ 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"
@@ -28,6 +58,12 @@ version = "1.11.0"
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
version = "1.11.0"
[[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"
@@ -60,12 +96,29 @@ 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 = "962834c22b66e32aa10f7611c08c8ca4e20749a9"
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
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 = "9d8a54ce4b17aa5bdce0ea5c34bc5e7c340d16ad"
@@ -86,6 +139,36 @@ deps = ["Artifacts", "Libdl"]
uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
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 = "21d088c496ea22914fe80906eb5bce65755e5ec8"
uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
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"
uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
@@ -134,10 +217,10 @@ 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 = "3c8a0a9a6d4a10bdfb6b751bd2b6051ed3e25fd4"
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.127"
version = "0.25.129"
[deps.Distributions.extensions]
DistributionsChainRulesCoreExt = "ChainRulesCore"
@@ -215,19 +298,24 @@ 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", "JSON", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "UUIDs"]
git-tree-sha1 = "8720a31344bc85ad610ae12f7e1247de22070765"
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.3.2"
version = "0.4.9"
[[deps.HTTP]]
deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"]
git-tree-sha1 = "a95f80749437ffb42948848d2d2ec81a5050ef4b"
git-tree-sha1 = "eda1d37cb55d90a17d0957c75841138c88b361a1"
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
version = "2.4.0"
version = "2.5.4"
[[deps.HashArrayMappedTries]]
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
@@ -235,10 +323,10 @@ 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"]
@@ -273,6 +361,16 @@ git-tree-sha1 = "d6fe00b123e32ddd17231b35d69a6394e696fd5a"
uuid = "d8418881-c3e1-53bb-8760-2df7ec849ed5"
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 = "6da3c4316095de0f5ee2ebd875df8721e7e0bdbe"
uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f"
@@ -340,7 +438,7 @@ version = "1.21.3+0"
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"
@@ -420,9 +518,20 @@ version = "1.11.0"
[[deps.LoweredCodeUtils]]
deps = ["CodeTracking", "Compiler", "JuliaInterpreter"]
git-tree-sha1 = "0aad96d7b987a5600e260eec50147b254d5ff7e6"
git-tree-sha1 = "3733419e9a71156b389f3e331672d2e95436783f"
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
version = "3.6.0"
version = "3.6.2"
[[deps.Lz4_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "191686b1ac1ea9c89fc52e996ad15d1d241d1e33"
uuid = "5ced341a-0733-55b8-9ab6-a4889d929147"
version = "1.10.1+0"
[[deps.MacroTools]]
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
version = "0.5.16"
[[deps.Markdown]]
deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"]
@@ -522,9 +631,9 @@ version = "1.8.2"
[[deps.PDMats]]
deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
git-tree-sha1 = "e4cff168707d441cd6bf3ff7e4832bdf34278e4a"
git-tree-sha1 = "26766d4b5f1a410c218a19b85a672c6edb693c65"
uuid = "90014a1f-27ba-587c-ab20-58faa44d9150"
version = "0.11.37"
version = "0.11.40"
weakdeps = ["StatsBase"]
[deps.PDMats.extensions]
@@ -657,15 +766,39 @@ 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 = "../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"
@@ -769,6 +902,11 @@ git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
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"]
git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8"
@@ -823,9 +961,9 @@ version = "1.0.1"
[[deps.Tables]]
deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"]
git-tree-sha1 = "f2c1efbc8f3a609aadf318094f8fc5204bdaf344"
git-tree-sha1 = "0f38a06c83f0007bbab3cf911262841c9a0f07e0"
uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
version = "1.12.1"
version = "1.13.0"
[[deps.Tar]]
deps = ["ArgTools", "SHA"]
@@ -884,7 +1022,7 @@ 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"]
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"
@@ -911,6 +1049,14 @@ 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"
+4 -1
View File
@@ -21,11 +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.3.2"
GeneralUtils = "0.4.9"
HTTP = "2.4.0"
JSON = "1.6.1"
NATS = "0.1.0"
SQLLLM = "0.2.5"
msghandler = "0.5.6"
+82
View File
@@ -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": "..."
}
```
+53
View File
@@ -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"
}
}
}
+93
View File
@@ -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</$k>")
else
# Leaf node: put key and value on a single line
push!(lines, "$padding<$k>$v</$k>")
end
end
return join(lines, "\n")
end
+3 -3
View File
@@ -426,7 +426,7 @@ function main()
# event_description="the assistant talks to the user.",
# timestamp=Dates.now(),
# subject="assistant",
# actionname="CHATBOX",
# action_name="CHAT_BOX",
# action_input=customer_chat,
# )
# )
@@ -453,8 +453,8 @@ function main()
println("\nagent respond:\n $agent_response")
if haskey(agent.memory[:events][end], :thought)
lastAssistantAction = agent.memory[:events][end][:thought][:actionname]
if lastAssistantAction == "ENDCONVERSATION" # store thoughtDict
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")
+1 -1
View File
@@ -429,7 +429,7 @@ function runAgentInstance(
if haskey(agent.memory[:events][end], :thought)
lastAssistantAction = agent.memory[:events][end][:thought][:action_name]
if lastAssistantAction == "ENDCONVERSATION" # store thoughtDict
if lastAssistantAction == "END_CONVER_GUIDELINE" # store thoughtDict
# save a.memory[:shortmem][:decisionlog] to disk using JSON
println("\nsaving agent.memory[:shortmem][:decisionlog] to disk")
+1
View File
@@ -0,0 +1 @@
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
+589 -824
View File
File diff suppressed because it is too large Load Diff
+245 -361
View File
@@ -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:
@@ -239,10 +239,10 @@ function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistor
responseJsonStr = jsoncorrection(config, _responseJsonStr, expectedJsonExample)
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,12 +281,6 @@ 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,43 +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"
# 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")
# # 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"
_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 = "$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, rawresponse = SQLLLM.query(inventoryquery,
textresult, result_raw = SQLLLM.query(
inventoryquery,
a.context.executeSQL,
a.context.text2textInstructLLM;
insertSQLVectorDB=a.context.insertSQLVectorDB,
similarSQLVectorDB=a.context.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)
# println("\n--- YiemAgent checkwine() ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# println(textresult)
# println(result_raw)
# println("---")
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)
return (result=textresult, rawresponse=rawresponse, success=true, errormsg=nothing)
return (result_str=textresult, result_raw=result_raw, success=true, errormsg=nothing)
end
@@ -348,29 +361,28 @@ 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:
<situation>
At each round of conversation, the user provides the following:
- The query: the query provided by the user.
You must follow the following guidelines:
</situation>
<objective>
Extract information from the user's query as much as possible according to wine attributes extraction guidelines to fill out user's preference form.
</objective>
<your responsibility includes>
Fulfill the objective.
</your responsibility includes>
<wine attributes extraction guidelines>
- 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 attributes extraction guidelines>
<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
@@ -383,9 +395,8 @@ 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:
{
</you should then respond to the user with>
<you should only respond in JSON format as described below>
"wine_name": "...",
"winery": "...",
"vintage": "...",
@@ -398,12 +409,9 @@ function extractWineAttributes_1(a::T1, input::T2; maxattempt=10
"wine_price_max": "...",
"occasion": "...",
"food_to_be_paired_with_wine": "..."
}
Here are some example:
</you should only respond in JSON format as described below>
<here are some examples>
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",
@@ -416,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",
@@ -432,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 = "N/A"
for attempt in 1:maxattempt
usermsg =
"""
$input
</here are some examples>
"""
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 =
"""
<context>
P.S. $errornote
</context>
/no_think
<internal_context_for_assistant>
$errornote
</internal_context_for_assistant>
"""
unformatPrompt =
[
Dict(:name=> "system", :text=> systemmsg),
Dict(:name=> "user", :text=> usermsg)
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
)
# put in model format
prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName)
# add info
prompt = prompt * context
for attempt in 1:maxattempt
response = a.context.text2textInstructLLM(a.id, msg)
response = GeneralUtils.clean_json_response(response)
println("\n--- extractWineAttributes_1-1()")
println(response)
println("--- \n")
response = a.context.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(JSON.parsefile(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
@@ -605,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}
@@ -654,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.
<situation>
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.
</situation>
<objective>
Fill out the user's preference form based on the corresponding words from the user's query according to the guidelines.
</objective>
<your responsibility includes>
Fulfill the objective
</your responsibility includes>
<guidelines>
- 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.
</guidelines>
<you should then respond to the user with>
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.
@@ -677,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:
{
</you should then respond to the user with>
<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": "..."
</you should only respond in JSON format as described below>
<here are some examples>
User's query: I want a wine with a medium-bodied, low acidity, medium tannin.
{
"sweetness_keyword": "N/A",
"sweetness": "N/A",
"sweetness_min": "N/A",
"sweetness_max": "N/A",
"acidity_keyword": "low acidity",
"acidity": "1-2",
"acidity_min": 1,
"acidity_max": 2,
"tannin_keyword": "medium tannin",
"tannin": "3-4",
"tannin_min": 3,
"tannin_max": 4,
"intensity_keyword": "medium-bodied",
"intensity": "3-4"
}
"intensity_min": 3,
"intensity_max": 4
User's query: German red wine, under 100, pairs with spicy food
{
User's query: German red wine, under 100, pairs with spicy food.
"sweetness_keyword": "N/A",
"sweetness": "N/A",
"sweetness_min": "N/A",
"sweetness_max": "N/A",
"acidity_keyword": "N/A",
"acidity": "N/A",
"acidity_min": "N/A",
"acidity_max": "N/A",
"tannin_keyword": "N/A",
"tannin": "N/A",
"tannin_min": "N/A",
"tannin_max": "N/A",
"intensity_keyword": "N/A",
"intensity": "N/A"
}
Let's begin!
"intensity_min": "N/A",
"intensity_max": "N/A"
<here are some examples>
"""
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"
for attempt in 1:10
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 =
"""
<internal_context_for_assistant>
$conversiontable
<query>
$input
</query>
P.S. $errornote
/no_think
$errornote
</internal_context_for_assistant>
"""
unformatPrompt =
[
Dict(:name=> "system", :text=> systemmsg),
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
)
# put in model format
prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName)
# add info
prompt = prompt * context
for attempt in 1:10
response = a.context.text2textInstructLLM(a.id, msg)
response = GeneralUtils.clean_json_response(response)
println("\n--- extractWineAttributes_2-1()")
println(response)
println("--- \n")
response = a.context.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(JSON.parsefile(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
@@ -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
@@ -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
@@ -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,9 +936,9 @@ 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".
# "actioninput" is the input to the action
# "action_input" is the input to the action
# "observation" is result of the preceding immediate action.
# At each round of conversation, the user will give you:
+210 -125
View File
@@ -1,6 +1,6 @@
module type
export agent, sommelier, companion, virtualcustomer, appcontext
export agent, sommelier, companion, virtualcustomer, agentcontext
using Dates, UUIDs, DataStructures, JSON, NATS
using GeneralUtils
@@ -8,11 +8,9 @@ using GeneralUtils
# ---------------------------------------------- 100 --------------------------------------------- #
mutable struct appcontext
const connection::NATS.Connection
const text2textInstructLLMServiceSubject::String
getTextEmbedding::Function
mutable struct agentcontext
text2textInstructLLM::Function
getTextEmbedding::Function
executeSQL::Function
similarSQLVectorDB::Function
insertSQLVectorDB::Function
@@ -28,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}
chathistory::Vector{Dict{String, Any}}
memory::Dict{String, Any}
context::NamedTuple # NamedTuple of functions
llmFormatName::String
end
function companion(
context::appcontext # 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=
"""
@@ -56,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.",
),
)
@@ -65,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(
@@ -91,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}
context # 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(
context::appcontext, # app context
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 => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
:input => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
:output => "" ,
"description" => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
"input" => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
"output" => "" ,
),
"winestock"=> Dict(
:description => "<winestock tool description>A handy tool for searching wine in your inventory that match the user preferences.</winestock tool description>",
:input => """<input>Input is a JSON-formatted string that contains a detailed and precise search query.</input><input example>{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}</input example>""",
:output => """<output>Output are wines that match the search query in JSON format.""",
"description" => "<winestock tool description>A handy tool for searching wine in your inventory that match the user preferences.</winestock tool description>",
"input" => """<input>Input is a JSON-formatted string that contains a detailed and precise search query.</input><input example>{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}</input example>""",
"output" => """<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:<mime_type>;base64,<image1_base64_string>" ---
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" => "<internal_context_for_assistant>
LLM context here...
</internal_context_for_assistant>
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}(),
)
@@ -228,6 +224,81 @@ function sommelier(
llmFormatName
)
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_policy>
<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.
</store_guidelines>
<situation>
Your customer is coming into the store
</situation>
<your 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.
</your role>
<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.
</objective>
<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 objective
2) Keep the conversation with the customer going smoothly
2) Obey your mentor's suggestions.
</your responsibility includes>
<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.
</your responsibility does NOT includes>
<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 then respond to the user with interleaving plan, action_name, action_input>
<you should only respond in JSON format as described below>
"plan": "...",
"action_name": "...",
"action_input": "..."
</you should only respond in JSON format as described below>
<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.
</available_actions>
"""
system_msg = Dict(
"role" => "system",
"content" => [
Dict("type" => "text", "text" => systemmsg),
]
)
push!(newAgent.chathistory, system_msg)
return newAgent
end
@@ -238,8 +309,8 @@ 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}
chathistory::Vector{Dict{String, Any}}
memory::Dict{String, Any}
context # NamedTuple of functions
llmFormatName::String
end
@@ -250,7 +321,7 @@ function virtualcustomer(
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=
"""
@@ -267,27 +338,41 @@ function virtualcustomer(
tools = Dict( # update input format
"chatbox"=> Dict(
:description => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
:input => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
:output => "" ,
"description" => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
"input" => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
"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(
+102 -131
View File
@@ -26,14 +26,14 @@ 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",
"receiveprompt"=>Dict(
"mqtttopic"=> "testtopic/receive",
),
:receiveinternal=>Dict(
:mqtttopic=> "testtopic/internal",
"receiveinternal"=>Dict(
"mqtttopic"=> "testtopic/internal",
),
:text2text=>Dict(
:mqtttopic=> "testtopic/text2text",
"text2text"=>Dict(
"mqtttopic"=> "testtopic/text2text",
),
)
julia> a = YiemAgent.sommelier(
@@ -52,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
@@ -76,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=30) 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
@@ -138,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"
```
@@ -209,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
@@ -232,8 +223,8 @@ 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,
@@ -241,18 +232,18 @@ function eventdict(;
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,
:observation=> observation,
: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
@@ -263,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
- :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", :observation => nothing),
Dict(:subject => "Assistant", :actioninput => "Hi there!", :observation => "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
@@ -302,55 +293,25 @@ function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothin
for i in ind
event = events[i]
# If no outcome exists, format without outcome
# if event[:actionname] == "CHATBOX"
# timeline *= "Event_$i $(event[:subject])> actionname: $(event[:actionname]), actioninput: $(event[:actioninput])\n"
# elseif event[:actionname] == "CHECKINVENTORY" && event[:observation] === nothing
# timeline *= "Event_$i $(event[:subject])> actionname: $(event[:actionname]), actioninput: $(event[:actioninput]), observation: Not done yet.\n"
# If outcome exists, include it in formatting
if event[:actionname] == "CHECKWINE"
timeline *= "Event_$i $(event[:subject])> actionname: $(event[:actionname]), actioninput: $(event[:actioninput]), observation: $(event[:observation])\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])> actionname: $(event[:actionname]), actioninput: $(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[:observation] === nothing
# timeline *= "Event_$i $(event[:subject])> actionname: $(event[:actionname]), actioninput: $(event[:actioninput]), observation: Not done yet.\n"
# # If outcome exists, include it in formatting
# else
# timeline *= "Event_$i $(event[:subject])> actionname: $(event[:actionname]), actioninput: $(event[:actioninput]), observation: $(event[:observation])\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 =
@@ -364,20 +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[:observation] === nothing
subject = event[:subject]
actionname = event[:actionname]
actioninput = event[:actioninput]
str = "actionname: $actionname, actioninput: $actioninput"
d = Dict{Symbol, String}(:name=>subject, :text=>str)
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]
actionname = event[:actionname]
actioninput = event[:actioninput]
observation = event[:observation]
str = "actionname: $actionname, actioninput: $actioninput, observation: $observation"
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
@@ -389,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 =
@@ -402,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
@@ -431,26 +392,36 @@ function checkAgentResponse_text(response::String, requiredHeader::T
end
function checkAgentResponse_JSON(responsedict::Dict, requiredKeys::T
)::Tuple where {T<:Array{Symbol}}
_responsedictKey = keys(responsedict)
responsedictKey = [i for i in _responsedictKey] # convert into a list
is_requiredKeys_in_responsedictKey = [i responsedictKey for i in requiredKeys]
ispass = false
errormsg = nothing
if length(is_requiredKeys_in_responsedictKey) > length(requiredKeys)
errormsg = "Your previous attempt has duplicated points according to the required response format"
ispass = false
elseif !all(is_requiredKeys_in_responsedictKey)
zeroind = findall(x -> x == 0, is_requiredKeys_in_responsedictKey)
missingkeys = [requiredKeys[i] for i in zeroind]
errormsg = "$missingkeys are missing from your previous response"
ispass = false
else
ispass = true
end
return (ispass, errormsg)
end
-76
View File
@@ -1,76 +0,0 @@
{
"mqttServerInfo": {
"description": "mqtt server info",
"port": 1883,
"broker": "mqtt.yiem.cc"
},
"testingOrProduction": {
"value": "testing",
"description": "agent status, couldbe testing or production"
},
"agentid": {
"value": "2b74b87a-5413-4fe2-a4d3-405891051680",
"description": "a unique id for this agent"
},
"agentCentralConfigTopic": {
"mqtttopic": "/yiem_branch_1/agent/sommelier/backend/config/api/v1.1",
"description": "a central agent server's topic to get this agent config"
},
"servicetopic": {
"mqtttopic": [
"/yiem/hq/agent/sommelier/backend/prompt/api_v1/testing"
],
"description": "a topic this agent are waiting for service request"
},
"role": {
"value": "sommelier",
"description": "agent role"
},
"organization": {
"value": "yiem_branch_1",
"description": "organization name"
},
"externalservice": {
"loadbalancer": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM"
},
"text2textinstruct": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
},
"virtualWineCustomer_1": {
"mqtttopic": "/virtualenvironment/winecustomer",
"description": "text to text service with instruct LLM that act as wine customer",
"llminfo": {
"name": "llama3instruct"
}
},
"text2textchat": {
"mqtttopic": "/loadbalancer/requestingservice",
"description": "text to text service with instruct LLM",
"llminfo": {
"name": "llama3instruct"
}
},
"wineDB" : {
"description": "A wine database connection info for LibPQ client",
"host": "192.168.88.12",
"port": 10201,
"dbname": "wineDB",
"user": "yiemtechnologies",
"password": "yiemtechnologies@Postgres_0.0"
},
"SQLVectorDB" : {
"description": "A wine database connection info for LibPQ client",
"host": "192.168.88.12",
"port": 10203,
"dbname": "SQLVectorDB",
"user": "yiemtechnologies",
"password": "yiemtechnologies@Postgres_0.0"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+1 -1
View File
@@ -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)
+223
View File
@@ -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" =>
"""
" <plan>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.</plan>\n <action_name>CHAT_BOX</action_name>\n <action_input>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.</action_input><action_result> User response in the next message </action_result>"
"""
),
]
),
Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "ok"),
]
),
],
"temperature" => 0.7
)
llm_response = text2text_instruct_llm(openai_msg)
# ---------------------------------------------- 100 --------------------------------------------- #
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+291
View File
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB