# uuid=63e765ef-6c95-48a7-93b5-59fba59b7aea
if version\0 < 1:
from channels import send_message_to_thread, wait_for_new_messages_on_thread, format_messages, User, Family
from core_skills import answer_question
from llm import agent
type Recipe:
val kind
val title
val summary
type DietLogEntry:
val who
val when
val when_text
val items
type User:
one diet_log
type Family:
one recipes
def diet_log(user): TODO
if not user.diet_log?:
user.diet_log = []
return user.diet_log
def recipes(user):
fam = user.family
if not fam.recipes?:
fam.recipes = new_vdb()
return fam.recipes
format_diet_log_entry = <e: "[{e.when}] ({e.when_text}) {e.who} had {sjoin(e.items, ', ')}">
format_diet_log = <l: joinlines([format_diet_log_entry(e) for e in l])>
def summarize_recipe(r):
return "{r.kind}: {r.title} - {r.summary}"
{|
| Find good search terms for looking up matching recipes and ingredients
|}
def extract_search_terms(thread, user):
username = user.name
return answer_question(thread, user,
instructions = $"- We need to search a vector database for known recipes and ingredients encompassing {username}'s log entry.
- Return a json list of phrases to search for. These can be single ingredients like "milk" but also
try to include likely implied recipes like "chicken with tomatoes and rice" which might be merely
descriptive (three separate ingredients) or might refer to a family recipe. We need to search to
find out."$,
answer_eg = $"["chicken", "tomatoes", "rice", "chicken with tomatoes and rice"]"$)
{|
| Map named foods to known recipes and ingredients
|}
def map_foods(thread, user, hits):
username = user.name
return answer_question(thread, user,
instructions = $"- We need to re-state {username}'s log entry entirely in terms of known recipes and ingredients.
- Summarize who ate what and when using recipe titles with the recipe number in parentheses like "(R132)".
- We're looking for the simplest formulation. E.g., prefer a recipe that encompasses multiple mentioned things over listing them separately.
- If you're unsure if something refers to a specific recipe, ask {username}.
- If there is no matching recipe or ingredient for something, reply with action="add recipe", recipe="<short description of the item or recipe we need to add>"
- Here are the best matching recipes:
{joinlines(["(R{i}) {summarize_recipe(r)}" for i:r in hits])}
"$,
answer_eg = "John and Sue had Mustard Crusted Salmon (R12) and Soda (R3) for lunch at about 2:15pm. John had Milk (R17) and Chocolate Chip Cookies (R19) for a snack at 4pm.",
more_actions=$"If we need to add/register a new ingredient or recipe, reply, e.g.:
action="add recipe",
recipe="pizza (home made)"
"$)
{|
| Determine who consumed what when
|}
def model_meal_events(thread, user):
return answer_question(thread, user,
instructions = $"- We need a model of the described meal(s): Who consumed what at what meal and when.
- Make sure it's clear who consumed each thing and at what time.
- Don't assume it was a self-only entry unless so-specified. Ask who had what unless clear.
- Assume "we" or similar refers to the whole family unless otherwise specified.
- We need *absolute* times (like "2:15pm") for everything, not relative. Infer absolute where possible, else ask.
- Assume relative times are from the time-stamp of the message.
- DO NOT infer times from meal names. If the time isn't (roughly) specified, ask.
- You MAY infer meal names from the time and foods if obvious enough.
- When the model is clear and complete (who had each thing at what rough absolute time), reply with a full and explicit description.
"$,
answer_eg = "John and Sue had mustard crusted salmon and soda for lunch at about 2:15pm. John had milk and cookies for a snack at 4pm.")
{|
| Format meal events as JSON
|}
def format_meal_events(thread, user, sofar):
return answer_question(thread, user,
instructions = $"- We need a model of the described meal(s): Who ate what at what meal and when.
- It should be a chronological list of entries. Each entry must be a JSON object with, e.g.:
- who="John" -- one person's name
- meal="lunch" -- must be one of "breakfast"|"lunch"|"dinner"|"snack"
- time="2:15pm" -- local time, approximate is fine
- foods=["mustard crusted salmon", "soda", ...] -- List of all foods that person ate at that time
"$,
sofar = sofar,
answer_eg = json([[who="John", meal="lunch", time="2:15pm", foods=["mustard crusted salmon", "soda"]],
[who="John", meal="snack", time="4pm", foods=["milk", "cookies"]]]))
{|
| Match food name to saved recipe or ingredient
|}
def find_recipe(thread, user, food, sofar):
rbook = recipes(user)
for i in 0..2:
opts = vfind(rbook, food)
n = answer_question(thread, user,
instructions=$"- We need to determine which of the following, if any, {json(food)} refers to:
{if length(opts) > 0 then joinlines(["[{i}] {summarize_recipe(r)}" for i:r in opts]) else '<no recipes on file>'}
- If a good match to one of these, return the number.
- If it's none of these, return answer=null, indicating we need to create a new entry.
- If unsure, ask the user about the ambiguous possible matches. Note {user.name} can't see
the (vector db based) match list above--only you can.
"$,
sofar = sofar,
answer_eg = "2")
if n?:
return opts[n]
TODO
a = answer_question(thread, user,
instructions=$"- We need a type, title, and summary for the new ingredient or recipe {json(food)}.
- type can be one of "INGREDIENT" or "RECIPE"
- for an ingredient, title should just be the ingredient, and summary is optional refinement.
- for a recipe, work out a good title and summary with the user, but make sure the summary
mentions all the main/defining ingredients so it can found by a vector search.
- for a recipe, DO NOT ASSUME a title or ingredients!
- confirm the title with {user.name} before proceeding.
"$,
sofar = sofar,
answer_eg = '{"type":"RECIPE", "title":"Kung Pao Chicken", "summary":"Spicy chicken with green onions, sesame oil, and peanuts."}')
r = Recipe(kind=a.type, title=a.title, summary=a.summary)
vadd(rbook, r, summarize_recipe(r))
return none
{|
| Track what the user eats and drinks.
|}
def track_diet(thread, user):
while true:
search_terms = extract_search_terms(thread, user)
rbook = recipes(user)
hits = {}
for search_term in search_terms:
for recipe in vfind(rbook, search_term):
append(hits, recipe)
hits = list(hits)
print("Search terms: {pformat(search_terms, indent=' ')}")
print("Candidate matches: {pformat(hits, indent=' ')}")
reply = map_foods(thread, user, hits)
print("Map_foods:")
pprint(reply)
if reply.action == 'answer':
break while
else if reply.action == 'cancel':
return
else if reply.action == 'add recipe':
food = reply.recipe
a = answer_question(thread, user,
instructions=$"- We need a type, title, and summary for the new ingredient or recipe {json(food)}.
- type can be one of "INGREDIENT" or "RECIPE"
- for an ingredient, title should just be the ingredient, and summary is optional refinement.
- for a recipe, work out a good title and summary with the user, but make sure the summary
mentions all the main/defining ingredients so it can found by a vector search.
- for a recipe, DO NOT ASSUME a title or ingredients!
- confirm the title with {user.name} before proceeding.
"$,
answer_eg = '{"type":"RECIPE", "title":"Kung Pao Chicken", "summary":"Spicy chicken with green onions, sesame oil, and peanuts."}')
r = Recipe(kind=a.type, title=a.title, summary=a.summary)
vadd(rbook, r, summarize_recipe(r))
else:
send_message_to_thread(thread, "(Internal Error)")
print("track_diet: Badly formatted reply")
pprint(reply)
return none
model = model_meal_events(thread, user)
events = format_meal_events(thread, user, sofar="- {model}")
e2 = []
fcache = [:]
for e in events:
f2 = []
for food in e.foods:
if not (r = fcache[food])?:
fcache[food] = r = find_recipe(thread, user, food, sofar="- {model}")
if r?:
append(f2, r)
else:
print("WARNING: Couldn't index {food} in recipe file.")
append(e2, [meal=e.meal, time=e.time, who=e.who, foods=f2])
send_message_to_thread(thread, joinlines(["{e.meal} at {e.time}: {e.who} had {sjoin([r.title for r in e.foods], sep=', ')}" for e in e2]))
{|
| Show a requested recipe
|}
def show_recipe(thread, user):
rbook = recipes(user)
opts = vfind(rbook, thread.messages[-1].text) TODO
n = answer_question(thread, user,
instructions=$"- We need to determine which of the following, if any, matches what the user is asking about:
{if length(opts) > 0 then joinlines(["[{i}] {summarize_recipe(r)}" for i:r in opts]) else '<no recipes on file>'}
- If a good match to one of these, return the number.
- If it's none of these, return answer=null, indicating no match found.
- If unsure, ask the user about the ambiguous possible matches. Note {user.name} can't see
the (vector db based) match list above--only you can.
"$,
answer_eg = "2")
if n?:
send_message_to_thread(thread, summarize_recipe(opts[n]))
else:
send_message_to_thread(thread, "No matching recipe or ingredient found.")
if true:
from channels import get_family
fam = get_family("e0ee81dc-2a1f-4398-bad0-1c3c5663072b")
if not fam.recipes?:
fam.recipes = new_vdb()
rbook = fam.recipes
for r in [
Recipe(kind="RECIPE", title="Mustard Crusted Salmon", summary="baked salmon with mustard, garlic, and wine"),
Recipe(kind="RECIPE", title="Beef, Lentils, and Rice", summary="stewed beef cooked with lentils and served with rice"),
Recipe(kind="INGREDIENT", title="Milk", summary="whole milk"),
Recipe(kind="INGREDIENT", title="Rice, Basmatti", summary="basmatti rice (white)"),
Recipe(kind="INGREDIENT", title="Rice, Jasmine", summary="Jasmine rice (white)")
]:
vadd(rbook, r, summarize_recipe(r))
version = 1