This commit is contained in:
2026-06-28 22:03:58 +07:00
parent e63dd7d898
commit d076d5f912
8 changed files with 188 additions and 120 deletions
+4 -4
View File
@@ -2,7 +2,7 @@
julia_version = "1.12.6"
manifest_format = "2.0"
project_hash = "972fb718a7c47dc6826419825a1ab5fe7b0fde84"
project_hash = "a7a5dbec939b5bcaa54ea4aedd3c8ff223c86502"
[[deps.AliasTables]]
deps = ["PtrArrays", "Random"]
@@ -247,11 +247,11 @@ version = "1.11.0"
[[deps.GeneralUtils]]
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "JSON", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "UUIDs"]
git-tree-sha1 = "76d2628787838a67d6e8192e428991a7522883f0"
git-tree-sha1 = "f1bad7621e6ac2d235adc8c593b0aff87bd1d93e"
repo-rev = "main"
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
version = "0.4.0"
version = "0.4.2"
[[deps.HTTP]]
deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"]
@@ -925,7 +925,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", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs", "msghandler"]
path = "."
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
version = "0.4.0"
+1 -1
View File
@@ -26,7 +26,7 @@ msghandler = "f2724d33-f338-4a57-b9f8-1be882570d10"
[compat]
CSV = "0.10.15"
DataFrames = "1.7.0"
GeneralUtils = "0.4.0"
GeneralUtils = "0.4.2"
HTTP = "2.4.0"
JSON = "1.6.1"
NATS = "0.1.0"
+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="CHATBOX",
# 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 == "ENDCONVERGUIDELINE" # 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 == "ENDCONVERGUIDELINE" # store thoughtDict
# save a.memory[:shortmem][:decisionlog] to disk using JSON
println("\nsaving agent.memory[:shortmem][:decisionlog] to disk")
+136 -66
View File
@@ -48,6 +48,7 @@ macro executeStringFunction(functionStr, args...)
end
""" Think and choose action
# Arguments
@@ -125,12 +126,11 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
# recentevents_ind = GeneralUtils.recentElementsIndex(
# length(a.memory["events"]), recentevents; includelatest=true)
requiredKeys = ["plan", "actionname", "actioninput"]
requiredKeys = ["plan", "action_name", "action_input"]
context =
"""
<internal_context_for_assistant>
$(a.memory["shortmem"]["scratchpad"])
$(GeneralUtils.dict_to_string_html(a.memory["shortmem"]))
</internal_context_for_assistant>
"""
@@ -179,9 +179,9 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
continue
end
if responsedict["actionname"] ["CHATBOX", "CHECKWINE", "PRESENTBOX", "ENDCONVERSATION"]
if responsedict["action_name"] ["CHATBOX", "CHECKWINE", "PRESENTBOX", "ENDCONVERGUIDELINE"]
errornote = "Your previous attempt didn't use the given functions"
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["actionname"])", @__FILE__, ":", @__LINE__, " $(Dates.now())")
println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())")
continue
end
@@ -216,8 +216,8 @@ function decisionMaker(a::T; recentevents::Integer=20, maxattempt=10
# delete!(responsedict, :mentioned_winery)
# check whether responsedict["actioninput"] is the same as previous dialogue
# if !isempty(a.chathistory) && responsedict["actioninput"] == a.chathistory[end]["text"]
# check whether responsedict["action_input"] is the same as previous dialogue
# if !isempty(a.chathistory) && responsedict["action_input"] == a.chathistory[end]["text"]
# errornote = "In your previous attempt, you repeated the previous dialogue. Please try again."
# println("\nERROR YiemAgent decisionMaker() $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
# continue
@@ -317,16 +317,16 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext
- evaluatee_context: The context that evaluatee use to make a decision
- evaluatee_decision: The decision made by the evaluatee, consists of the following elements:
"plan" is the trainee's plan
"actionname" is the name of the action taken, which can be one of the available tool name.
"actioninput" is the input to the action.
"action_name" is the name of the action taken, which can be one of the available tool name.
"action_input" is the input to the action.
</At each round of conversation, you will be given the following information>
<You must follow the following policy>
- Use only infomation provided by the store policy and guidelines as a bedrocks for your response.
</You must follow the following policy>
<You should follow the following guidelines>
- The trainee's plan, actionname, and actioninput must be logically consistent
- The trainee's actioninput should be in a proper format as specified by the tools.
- The trainee's action name and action input should make sense. For example, if the trainee isn't finished talking, he shouldn't use the ENDCONVERSATION tool.
- The trainee's plan, action_name, and action_input must be logically consistent
- The trainee's action_input should be in a proper format as specified by the tools.
- The trainee's action name and action input should make sense. For example, if the trainee isn't finished talking, he shouldn't use the ENDCONVERGUIDELINE tool.
</You should follow the following guidelines>
<You should then respond to the user with>
1) trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question.
@@ -368,7 +368,7 @@ function evaluator(a::T1, timeline, decisiondict, evaluateecontext
$evaluateecontext
</evaluatee_context>
<evaluatee_decision>
{plan: $(decisiondict["plan"]), actionname: $(decisiondict["actionname"]), actioninput: $(decisiondict["actioninput"])}
{plan: $(decisiondict["plan"]), action_name: $(decisiondict["action_name"]), action_input: $(decisiondict["action_input"])}
</evaluatee_decision>
P.S. $errornote
</context>
@@ -483,7 +483,7 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
end
# place holder
actionname = nothing
action_name = nothing
result = nothing
chatresponse = nothing
@@ -498,8 +498,8 @@ function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Obj
# thinking loop until AI wants to communicate with the user
chatresponse = nothing
while chatresponse === nothing
actionname, result = think(a)
if actionname ["CHATBOX", "PRESENTBOX", "ENDCONVERSATION"]
action_name, result = think(a)
if action_name ["CHATBOX"]
chatresponse = result
end
end
@@ -532,8 +532,8 @@ function conversation(a::Union{companion, virtualcustomer}, userinput::Dict;
event_description="the user talks to the assistant.",
timestamp=Dates.now(),
subject="user",
actionname="CHATBOX",
actioninput=userinput["text"],
action_name="CHATBOX",
action_input=userinput["text"],
)
)
chatresponse = generatechat(a; converPartnerName=converPartnerName, recentEventNum=20)
@@ -545,8 +545,8 @@ function conversation(a::Union{companion, virtualcustomer}, userinput::Dict;
event_description="the assistant talks to the user.",
timestamp=Dates.now(),
subject="assistant",
actionname="CHATBOX",
actioninput=chatresponse,
action_name="CHATBOX",
action_input=chatresponse,
)
)
return chatresponse
@@ -564,20 +564,20 @@ julia>
```
"""
function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} where {T<:agent}
function think(a::T)::Union{NamedTuple{(:action_name, :result), Tuple{String, String}}, NamedTuple{(:action_name, :result), Tuple{String, Nothing}}} where {T<:agent}
# a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0)
thoughtDict = decisionMaker(a)
# # map action and input() to llm function
# response =
# if thoughtDict["actionname"] == "CHATBOX" || thoughtDict["actionname"] == "ENDCONVERSATION"
# if thoughtDict["action_name"] == "CHATBOX" || thoughtDict["action_name"] == "ENDCONVERGUIDELINE"
# (result=thoughtDict["plan"], errormsg=nothing, success=true)
# elseif thoughtDict["actionname"] == "CHECKWINE"
# checkwine(a, thoughtDict["actioninput"])
# elseif thoughtDict["actionname"] == "PRESENTBOX"
# (result=thoughtDict["actioninput"], errormsg=nothing, success=true)
# elseif thoughtDict["action_name"] == "CHECKWINE"
# checkwine(a, thoughtDict["action_input"])
# elseif thoughtDict["action_name"] == "PRESENTBOX"
# (result=thoughtDict["action_input"], errormsg=nothing, success=true)
# else
# error("undefined LLM function. Requesting $(thoughtDict["actionname"])")
# error("undefined LLM function. Requesting $(thoughtDict["action_name"])")
# end
# # this section allow LLM functions above to have different return values.
@@ -590,52 +590,52 @@ function think(a::T)::NamedTuple{(:actionname, :result),Tuple{String,String}} wh
# success::Bool = haskey(response, "success") ? response["success"] : false
result = nothing
if thoughtDict["actionname"] ["CHATBOX"]
result = thoughtDict["actioninput"]
elseif thoughtDict["actionname"] ["ENDCONVERSATION"]
# WORKING add ENDCONVERSATION guideline in to scratchpad
guideline =
"""
To end conversation with the user
"""
if thoughtDict["action_name"] ["CHATBOX"]
result = thoughtDict["action_input"]
elseif thoughtDict["action_name"] == "ENDCONVERGUIDELINE"
a.memory["shortmem"]["scratchpad"] =
result = chatresponse
elseif thoughtDict["actionname"] ["PRESENTBOX"]
chatresponse = presentbox(a, thoughtDict) #PENDING
result = chatresponse
elseif thoughtDict["actionname"] == "CHECKWINE"
if rawresponse !== nothing
vd = GeneralUtils.dfToVectorDict(rawresponse) # comes in dataframe format
# a.memory["shortmem"]["found_wine"] = vd # used by decisionMaker() as a short note
#[PENDING] a.memory["shortmem"]["available_wine"] should be OrderedDict instead of vector dict so that no duplicate item are listed?
if length(a.memory["shortmem"]["db_search_result"]) != 0
a.memory["shortmem"]["db_search_result"] = vcat(a.memory["shortmem"]["db_search_result"], vd)
# add ENDCONVERGUIDELINE guideline in to scratchpad
endconversation_guideline =
"""
<end_conversation_guideline>
- Provide customer with store contact info and business hours
- Invite customer to comeback
</end_conversation_guideline>
<store_info>
Business Hours: everyday 9.00-20.00
Tel. 0863055790
</store_info>
"""
thoughtDict["action_result"] = endconversation_guideline
max_ind =
if length(a.memory["shortmem"]) == 0
0
else
a.memory["shortmem"]["db_search_result"] = vd
k = keys(a.memory["shortmem"])
maximum(parse.(Int, k))
end
a.memory["shortmem"]["$(max_ind + 1)"] = thoughtDict
# # add to scratchpad
# a.memory["shortmem"]["scratchpad"] *=
# """
# <database_search_result>
# I searched the database with this search term: $actioninput \nThis is what I found: $result
# </database_search_result>
# """
else
# a.memory["shortmem"]["scratchpad"] *=
# """
# <database_search_result>
# I searched the database with this search term: $actioninput \nThis is what I found: $result
# </database_search_result>
# """
end
elseif thoughtDict["action_name"] ["PRESENTBOX"] #PENDING
chatresponse = presentbox(a, thoughtDict)
result = chatresponse
elseif thoughtDict["action_name"] == "CHECKWINE"
df = checkwine(a, thoughtDict["action_input"])
thoughtDict["action_result"] = GeneralUtils.dfToString(df)
max_ind =
if length(a.memory["shortmem"]) == 0
0
else
k = keys(a.memory["shortmem"])
maximum(parse.(Int, k))
end
a.memory["shortmem"]["$(max_ind + 1)"] = thoughtDict
else
error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())")
end
return (actionname=thoughtDict["actionname"], result=result)
return (action_name=thoughtDict["action_name"], result=result)
end
@@ -697,7 +697,7 @@ function presentbox(a::sommelier, thoughtDict; maxtattempt::Integer=10, recentev
context =
"""
<context>
Name of the wines that needs to be introduced: $(thoughtDict["actioninput"])
Name of the wines that needs to be introduced: $(thoughtDict["action_input"])
$(a.memory["shortmem"]["scratchpad"])
P.S. $errornote
</context>
@@ -1334,3 +1334,73 @@ function detectWineryName(a, text)
end # module interface
+3 -2
View File
@@ -309,7 +309,8 @@ function checkwine(a::T1, input::T2; maxattempt::Int=3
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, rawresponse = SQLLLM.query(
inventoryquery,
a.context.executeSQL,
a.context.text2textInstructLLM;
insertSQLVectorDB=a.context.insertSQLVectorDB,
@@ -1053,7 +1054,7 @@ end
# “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.
# 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:
+11 -14
View File
@@ -177,7 +177,7 @@ function sommelier(
Dict(
"role" => "system",
"content" => [
Dict("type" => "text", "text" => "You are a helpful assiatant"),
Dict("type" => "text", "text" => "You are a helpful assistant"),
]
),
Dict(
@@ -197,10 +197,8 @@ function sommelier(
]
"""
memory = Dict{String, Any}(
"shortmem"=> OrderedDict{String, Any}(
"db_search_result"=> Any[],
"scratchpad"=> "",
),
"shortmem"=> OrderedDict{String, Any}(),
"scratchpad"=> "",
"events"=> Vector{Dict{String, Any}}(),
"state"=> Dict{String, Any}(
),
@@ -237,6 +235,7 @@ function sommelier(
</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.
@@ -250,10 +249,10 @@ function sommelier(
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
- PRESENTBOX which you can use to to generate proper dialogue to present wines you have found in your inventory to the user. The input are wine names that you want to present. The output is presentation of the wines.
- ENDCONVERSATION which you can use to generates a natural dialogue to gracefully close the conversation with the user. The input is "nothing" keyword. The output is a natural, context-aware dialogue extension that smoothly concludes the conversation.
- ENDCONVERGUIDELINE 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 tools>
<your role>
Your name is $(newAgent.name). You are a sommelier for website-based $(newAgent.retailername)'s wine store. You are working under your mentor supervision.
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>
<situation>
Your customer is coming into the store
@@ -272,19 +271,17 @@ function sommelier(
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>
<at each round of conversation, you will be given the following information>
context: additional information about the current situation
</at each round of conversation, you will be given the following information>
<you should then respond to the user with interleaving Plan, Action_name, Action_input>
1) plan: Based on the current situation, state a complete action plan to complete the task. Be specific.
2) actionname: (Typically corresponds to the execution of the first step in your plan) Can be one of the available tool name
3) actioninput: The input to the action you are about to perform according to your plan.
2) action_name: (Typically corresponds to the execution of the first step in your plan) Can be one of the available tool name
3) action_input: The input to the action you are about to perform according to your plan.
After the tool is executed you gets "action_result". It is the output from the tool 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": "...",
"actionname": "...",
"actioninput": "..."
"action_name": "...",
"action_input": "..."
}
</you should only respond in format as described below>
+26 -26
View File
@@ -200,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
@@ -223,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", "PRESENTBOX", etc
action_input::Union{String, Nothing}=nothing,
location::Union{String, Nothing}=nothing,
equipment_used::Union{String, Nothing}=nothing,
material_used::Union{String, Nothing}=nothing,
@@ -237,8 +237,8 @@ function eventdict(;
"timestamp"=> timestamp,
"subject"=> subject,
"thought"=> thought,
"actionname"=> actionname,
"actioninput"=> actioninput,
"action_name"=> action_name,
"action_input"=> action_input,
"location"=> location,
"equipment_used"=> equipment_used,
"material_used"=> material_used,
@@ -254,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
@@ -293,14 +293,14 @@ 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 event["actionname"] == "CHECKWINE"
timeline *= "Event_$i $(event["subject"])> actionname: $(event["actionname"]), actioninput: $(event["actioninput"]), observation: $(event["observation"])\\n"
# if event["action_name"] == "CHATBOX"
# 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"] == "CHECKWINE"
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
@@ -325,10 +325,10 @@ end
# 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"
# timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), 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"
# timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: $(event["observation"])\\n"
# end
# end
@@ -356,17 +356,17 @@ function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing
# 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"
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"]
action_name = event["action_name"]
action_input = event["action_input"]
observation = event["observation"]
str = "actionname: $actionname, actioninput: $actioninput, observation: $observation"
str = "action_name: $action_name, action_input: $action_input, observation: $observation"
d = Dict{String, String}("name"=>subject, "text"=>str)
push!(log, d)
end