Friday's Party: Creating a Private, Agentic AI using Voice Assistant tools

Lets review…

Your LLM Needs Context. Lots of it. Use every tool at your disposal to teach the LLM - like a sixth grader, in well-defined, stepwise clear language - what you want from it, it will try… It will try so hard it will FAIL trying to try unless you tell it how to fail.

HA has some significant… Limitations for forming a prompt - but now that we know what they are…

  • Total prompt length
  • Dont use non json printable chars, alternatively escape them properly
  • Total prompt lengh
  • Come up with a way to subdivide your Prompt for easier troubleshooting…
  • TOTAL. PROMPT. LENGTH
  • points above at previous post… Dee-fense! clap clap clap

Well - apparently there’s a context length too we need to start being aware of because hopefully by now most of you are stuffing your LLM so full o crap they’re starting to forget their own name… Lets talk total CONTEXT and start the main course for Friday’s Party.

Executive Chef Friday
(You may have seen references to TopChef sprinkled about?)
This is a catering service apparently - and she pays VERY POORLY btw…

I chose those words very carefully because they define a fairly well known job position.

When prompt crafting it can be helpful to use shortcuts. Compartmentalize. Label - DEFINE. If you think you’re going to have to refer back to something later give that THING (whatever it is) a NAME. It can be a concept, a collection but whatever it is you need to be able to wrap a lasso around it as one coherent concept then define clearly X==Y.

In this case we can pull a WHOLE LOTTA context out of one statement. You are acting as the executive chef for the home.

  • First ‘You’ - the AI. Be very explicit of your use of YOU in a prompt and reserve it for addressing the AI wherever possible but when you want to make it do something - reinforce YOU.
  • Are Acting - If I want to have any HOPE of Friday jumping back to context that will allow her to turn on a light again (yes this can become a big problem, we’ll talk about it more…)
  • ‘as the executive chef’ Ok what are the common job functions of your average garden variety Executive Chef. THIS is the whole lotta context - see next…
  • for the home (Limits - remember chop scope wherever possible focus her back on the house)

No this isn’t Alton Brown, guys I’m talking your day to day working cook. ‘The Executive Chef’ is the top business manager in the restaurant. Many of them bemoan not even working the line because they’re doing menu planning, ordering, shopping, blah blah blah all in an effort to deliver a stellar product people want to come back for while reducing food waste and making the restaurant as profitable as possible.

Wow that’s something you never see in context of Home Automation but think about it. What do we need the AI to actually DO? To be USEFUL.

(Remember - not Alexa pre-2025? [“Panos, man. I’m a fan, I still have 2 working Surfaces - but sorry I can’t let you keep recording my voice… Alexa’s fired…” (call me)])

What can we ask an AI to do, given the right toolset?

  • Help me with recipe and meal planning
  • Keep track of food quality (timers, prep hints - French process are known quanties btw. Mother sauces haven’t changed)
  • Reduce food waste? (yeah we can, we just need an ERP, yes you in the back of the class - it IS Grocy)
  • Inventory (Grocy)
  • Menu (Mealie)
  • Menu Planning? Oh my… Buckle up.

Introducing the probably broken as is no warranty you look at it you bought it MEALIE RESTful API Script.

Wait - first we need a few things:

#REST Platform entries
rest:
  # REST sensor for caching just the OpenAPI once per hr
  - resource: "http://[MEALIE_BASE_URL]/openapi.json"
    method: GET
    headers:
      Authorization: !secret mealie_bearer
      accept: "application/json"
    scan_interval: 3600 # seconds (once/hr)
    sensor:
      - name:  Mealie_RESTful_OpenAPI_docs
        value_template: "{{ now() | as_local() }}" # last refresh time
        json_attributes: ['openapi', 'info', 'paths', 'components']
        force_update: true
        unique_id: [YOUR_UUID_HERE]

And some stuff (rest commands for Get, Put, post, delete)

# REST Commands to support Mealie Recipe Search
rest_command:
  mealie_api_advanced_openapi:
    url: >
      http://[MEALIE_BASE_URL]/openapi.json
    method: GET
    headers:
      Authorization: !secret mealie_bearer
      accept: 'application/json; charset=utf-8'
    verify_ssl: false

  mealie_api_advanced_get:
    url: >
      {%- if path_params is defined and path_params | length > 0 -%}
          {%- for key, value in path_params.items() -%}
              {%- set endpoint = endpoint | replace("{" ~ key ~ "}", value) -%}
          {%- endfor -%}
      {%- endif -%}
      {%- set endpoint = endpoint | replace('/api/', '') | replace('api/', '') %}
      {%- if endpoint[0] == '/' -%}
          {%- set endpoint = endpoint[1:] %}
      {%- endif -%}
      {{ "http://[MEALIE_BASE_URL]/api/" }}{{ endpoint }}?orderDirection={{ orderDirection | default("desc") }}
      {%- if search is defined and search not in ["", None] -%}
          &search={{ search | urlencode }}
      {%- endif %}
      {%- if additional_params is defined and additional_params | length > 0 and additional_params is mapping -%}
          {%- for key, value in additional_params.items() -%}
              &{{ key }}={{ value | urlencode }}
          {%- endfor -%}
      {%- endif %}
      {%- if pageNumber is defined and pageNumber > 0 -%}
          &page={{ pageNumber | default(1) }}
      {%- endif %}
      {%- if perPage is defined and perPage > 0 -%}
          &perPage={{ perPage | default(10) }}
      {%- endif %}
    method: GET
    headers:
      Authorization: !secret mealie_bearer
      accept: "application/json"
    verify_ssl: false

  mealie_api_advanced_post:
    url: >
      {%- set endpoint = endpoint | replace('/api/', '') | replace('api/', '') %}
      {%- if endpoint[0] == '/' -%}
          {%- set endpoint = endpoint[1:] %}
      {%- endif -%}
      http://[MEALIE_BASE_URL]/api/ {{- endpoint }}
    method: POST
    headers:
      authorization: !secret mealie_bearer
      accept: 'application/json; charset=utf-8'
    payload: "{{- payload -}}"
    content_type: 'application/json; charset=utf-8'
    verify_ssl: false

  mealie_api_advanced_put:
    url: >
      {%- set endpoint = endpoint | replace('/api/', '') | replace('api/', '') %}
      {%- if endpoint[0] == '/' -%}
          {%- set endpoint = endpoint[1:] %}
      {%- endif -%}
      http://[MEALIE_BASE_URL]/api/ {{- endpoint }}
    method: PUT
    headers:
      authorization: !secret mealie_bearer
      accept: 'application/json; charset=utf-8'
    payload: "{{- payload -}}"
    content_type: 'application/json; charset=utf-8'
    verify_ssl: false

  mealie_api_advanced_delete:
    url: >
      {%- set endpoint = endpoint | replace('/api/', '') | replace('api/', '') %}
      {%- if endpoint[0] == '/' -%}
          {%- set endpoint = endpoint[1:] %}
      {%- endif -%}
      http:/[MEALIE_BASE_URL]/api/ {{- endpoint }}
    method: DELETE
    headers:
      authorization: !secret mealie_bearer
      accept: 'application/json; charset=utf-8'
    payload: "{{- payload -}}"
    content_type: 'application/json; charset=utf-8'
    verify_ssl: false

At this point you may have figured out what’s going on here… Yep that’s EXACTLY what’s happening. Those are as GENERIC as I can possibly make them. But this one… ‘mealie_api_advanced_openapi’ is special…

That YAML creates this sensor:

Which you can now walk with this script:
FIRST OF ALL - this is the you are on your own part. My name is not Drew or Petro or Taras and I do not speak template for a living… Someone WILL have a better way of doing this.

Lets talk about what this bad boy does…

  • First - remember,
  • Write for AI, Detailed descriptions in the description and fields
  • AI Readable responses
  • Positive reinforcement on null set responses.
  • Return raw JSON as much as possible

The description is very clear about what it is and what it does… (Grandma can at least know what’s up)

alias: Mealie API Advanced Call (GET/POST/PUT/DELETE/HELP)
description: >
  - Supported  methods: GET, POST, PUT, DELETE, HELP - Specify the API endpoint
  path (e.g., "recipes" or "users/self/ratings/{recipe_id}") or API path or
  "component for HELP" - Tokens in {} will be replaced using path_params - For
  GET requests, provide:
     - orderDirection ("asc" or "desc", default "desc"),
     - search (free-text filter),
     - additional_params (dictionary of extra filters),
        common params include [start_date, end_date]
     - pageNumber and perPage for pagination
  - For POST, PUT, and DELETE, supply a JSON payload. - For HELP provide:
    - component (components will chase down the tree) or
    - path for more info
sequence:
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ method == 'GET' }}"
        sequence:
          - response_variable: response
            data:
              endpoint: "{{ endpoint }}"
              path_params: "{{ path_params | default({}) }}"
              orderDirection: "{{ orderDirection | default('desc') }}"
              search: "{{ search }}"
              additional_params: "{{ additional_params | default({}) }}"
              pageNumber: "{{ pageNumber | default(1) }}"
              perPage: "{{ perPage | default(10) }}"
            action: rest_command.mealie_api_advanced_get
        alias: GET
      - conditions:
          - condition: template
            value_template: "{{ method == 'POST' }}"
        sequence:
          - response_variable: response
            data:
              endpoint: "{{ endpoint }}"
              path_params: "{{ path_params | default({}) }}"
              payload: "{{ payload }}"
            action: rest_command.mealie_api_advanced_post
        alias: POST
      - conditions:
          - condition: template
            value_template: "{{ method == 'PUT' }}"
        sequence:
          - response_variable: response
            data:
              endpoint: "{{ endpoint }}"
              path_params: "{{ path_params | default({}) }}"
              payload: "{{ payload }}"
            action: rest_command.mealie_api_advanced_put
        alias: PUT
      - conditions:
          - condition: template
            value_template: "{{ method == 'DELETE' }}"
        sequence:
          - response_variable: response
            data:
              endpoint: "{{ endpoint }}"
              path_params: "{{ path_params | default({}) }}"
              payload: "{{ payload }}"
            action: rest_command.mealie_api_advanced_delete
        alias: DELETE
      - conditions:
          - condition: template
            value_template: "{{ method == 'HELP' }}"
        sequence:
          - variables:
              response:
                endpoint: >
                  {%- set docs = 'sensor.mealie_restful_openapi_docs' -%} {%- if
                  endpoint is defined and endpoint|length > 0 and
                  state_attr(docs, 'paths') and endpoint in state_attr(docs,
                  'paths') -%} {{ endpoint }} {%- else -%} [] {%- endif %}
                summary: >
                  {%- set docs = 'sensor.mealie_restful_openapi_docs' -%} {%- if
                  endpoint is defined and endpoint|length > 0 and
                  state_attr(docs, 'paths') and endpoint in state_attr(docs,
                  'paths') -%} {{ state_attr(docs,'paths')[endpoint].summary }}
                  {%- else -%} [] {%- endif %}
                tags: >
                  {%- set docs = 'sensor.mealie_restful_openapi_docs' -%} {%- if
                  endpoint is defined and endpoint|length > 0 and
                  state_attr(docs, 'paths') and endpoint in state_attr(docs,
                  'paths') -%}     {{ state_attr(docs, 'paths')[endpoint].tags |
                  list| to_json }} {%- endif %}
                methods: >
                  {%- set docs = 'sensor.mealie_restful_openapi_docs' -%} {%- if
                  endpoint is defined and endpoint|length > 0 and
                  state_attr(docs, 'paths') and endpoint in state_attr(docs,
                  'paths') -%} {%- for method, info in state_attr(docs,
                  'paths')[endpoint].items() if method in ['get', 'post', 'put',
                  'delete'] %} - method: "{{ method | upper }}"
                    summary: "{{ info.summary }}"
                  {%- endfor %} {%- else -%} [] {%- endif %}
                categories: >
                  {%- if ((endpoint is not defined) or (endpoint is defined) and
                  (endpoint[0] != '/'))%} {%- set docs =
                  'sensor.mealie_restful_openapi_docs' -%} {%- set ns =
                  namespace(categories=[]) -%} {%- if state_attr(docs, 'paths')
                  -%}
                    {%- for details in state_attr(docs, 'paths').values() %}
                      {%- for method, method_details in details.items() 
                           if method in ['get', 'post', 'put', 'delete'] 
                           and 'tags' in method_details 
                           and method_details.tags is iterable 
                           and method_details.tags | count > 0 %}
                        {%- for tag in method_details.tags %}
                          {%- if tag not in ns.categories %}
                            {%- set ns.categories = ns.categories + [ tag ] %}
                          {%- endif %}
                        {%- endfor %}
                      {%- endfor %}
                    {%- endfor %}
                  {%- endif %} {{ ns.categories | unique | list | to_json }} {%-
                  else -%} [] {%- endif %}
                components: >
                  {%- set docs = 'sensor.mealie_restful_openapi_docs' -%} {%- if
                  endpoint is defined and endpoint|length > 0 -%}
                    {%- if state_attr(docs, 'paths') and endpoint in state_attr(docs, 'paths') -%}
                      endpoint: "{{ endpoint }}"
                      methods:
                      {%- for method, info in state_attr(docs, 'paths')[endpoint].items() if method in ['get', 'post', 'put', 'delete'] %}
                        - method: "{{ method | upper }}"
                          summary: "{{ info.summary }}"
                          {%- if info.responses %}
                            responses:
                            {%- for code, response in info.responses.items() %}
                              {%- if response.content %}
                                {%- for content_type, content in response.content.items() %}
                                  {%- if content.schema and content.schema['$ref'] is defined %}
                                    {%- set ref = content.schema['$ref'] %}
                                    {# Assuming the ref follows the format "#/components/schemas/SchemaName" #}
                                    {%- set schema_name = ref.split('/')[-1] %}
                                    response_schema: {{ state_attr(docs, 'components')['schemas'][schema_name] | to_json }}
                                  {%- endif %}
                                {%- endfor %}
                              {%- endif %}
                            {%- endfor %}
                          {%- endif %}
                      {%- endfor %}
                    {%- else -%}
                      {%- set found = false -%}
                      {%- for comp in state_attr(docs, 'components').keys() %}
                        {%- if endpoint in state_attr(docs, 'components')[comp] %}
                          component: "{{ comp }}"
                          item: "{{ endpoint }}"
                          details: {{ state_attr(docs, 'components')[comp][endpoint] | to_json }}
                          {%- set found = true -%}
                        {%- endif %}
                      {%- endfor %}
                      {%- if not found %}
                        []
                      {%- endif %}
                    {%- endif %}
                  {%- else -%}
                    components: {{ state_attr(docs, 'components').keys() | list | to_json }}
                    schemas: {{ state_attr(docs, 'components')['schemas'] | to_json }}
                  {%- endif %}
                endpoints: >
                  {%- if (endpoint is not defined) or (endpoint == '') %}    
                  {%- set docs = 'sensor.mealie_restful_openapi_docs' -%}    
                  {%- if state_attr(docs, 'paths') -%} {{ state_attr(docs,
                  'paths').keys() | list | to_json }} {%- endif %} {%- else -%}
                  [] {%- endif %}
          - stop: Passed to the right context
            response_variable: response
            enabled: true
          - set_conversation_response: "{{response}}"
            enabled: true
        alias: HELP
  - stop: Pass response variables back to LLM
    response_variable: response
    enabled: true
  - set_conversation_response: "{{ response }}"
fields:
  method:
    description: " Choose \"GET\" to fetch data, \"POST\" to create, \"PUT\" to update, or \"DELETE\" to remove data."
    selector:
      select:
        options:
          - GET
          - POST
          - PUT
          - DELETE
          - HELP
    required: true
    name: HTTP method
    default: HELP
  endpoint:
    description: >
      API endpoint path (e.g., "recipes" or "users/self/ratings/{recipe_id}").
      Use tokens in {} for dynamic values, which will be replaced using
      path_params.
    selector:
      text: {}
  path_params:
    description: |
      Dictionary for token substitution. Example: {"recipe_id": "12345"}.
    selector:
      text: {}
  orderDirection:
    description: |
      (GET only) Sort order; "asc" or "desc" (default: "desc").
    selector:
      text: {}
  search:
    description: |
      (GET only) Free-text search filter.
    selector:
      text: {}
  additional_params:
    description: >
      (GET only) Dictionary of extra query parameters. Example: {"difficulty":
      "easy"}.
    selector:
      text: {}
  pageNumber:
    description: |
      (GET only) Page number for pagination (default: 1).
    selector:
      number:
        min: 1
        max: 100
        step: 1
  perPage:
    description: |
      (GET only) Number of items per page (default: 10).
    selector:
      number:
        min: 1
        max: 50
        step: 1
  payload:
    description: >-
      - (POST/PUT/DELETE only) - Use JSON payload for creating, updating, or
      deleting data. - Always review the API documentation for required fields.
      - Check error messages carefully; they indicate missing or
        incorrect fields.
      - Ensure that you review docs to include all mandatory fields
        in your payloads to avoid update failures.
      - If you encounter a mandatory field that is undocumented, please silently
        create a SINGLE todo item with the issue and inform the boss you created a ticket
        at your earliest convenience.
    selector:
      text:
        multiline: false
    name: payload
  field:
    selector:
      text: null

We also default to HELP, Tell her how to use it and blammo put the ENDPOINTS from ‘mealie_api_advanced_openapi’ front and center.

So… she gets someting like:

variables:
  response:
    endpoint: >[]
    summary: >[]
    tags: >
    methods: >[]
    categories: > ["App: About","Users: Authentication","Users: Registration","Users: CRUD","Users: Admin CRUD","Users: Passwords","Users: Images","Users: Tokens","Users: Ratings","Households: Cookbooks","Households: Event Notifications","Households: Recipe Actions","Households: Self Service","Households: Invitations","Households: Shopping Lists","Households: Shopping List Items","Households: Webhooks","Households: Mealplan Rules","Households: Mealplans","Groups: Households",
<--- continued data--->

Oh USERS! (SHE ALWAYS selects users first…)

So the theory here is. Give Friday an open pipe to Mealie’s RESTful API and ok STOP…

  • MAKE SURE YOU HAVE PROVISIONED YOUR AI ITS OWN SERVICE ACCOUNT AND SET ITS PERMISSIONS APPROPRIATELY. This is the only time I will say this.

So, as I was saying - we just gave Friday an open pipe to Mealy, and a Book. …And she thinks she’s an executive chef. The next thing was to revise the Kung fu component that at the very end of it - instructs Friday to:

Mealie OpenAPI Server Access
You have scripts that enable direct and up to date access to the Mealie Server using RESTful commands and GET, POST, PUT, DELETE methods and standard CRUD.
Use the Mealie API Advanced Call Script 'script.mealie_api_advanced' to access your Mealie Server!
It comes with bultin HELP to learn how to use it!  (Use it to learn the path (endpoint) and component docs.)
This is a direct connection to LIVE server documentation and is updated on a regular basis.
If you have prior knowledge of Mealie - prefer these documents as they may be more up-to-date.
{%- endmacro -%}

So now she is an Executive Chef, and has access to a Meal Planning and Shopping Platform and I just put the book in her hand…

You guys have seen me look at recipes before - that’s not interesting…
Let’s side by side Friday (gpt4.0-mini) and SUPERFriday (o3-mini) and see what happens:


Friday starts out ok, she knows we have a kitchen its vacant and a bunch of other stuff.
SUPERFriday:

Notice she has already also noted the menu - this is ok the menu is probably in her prompt so she’ll KNOW it’s empty) BUT:
Watch this:
Friday:

and:

Uh oh - Ok I know you guys and gals are used to seeing success, but I want you to see this too and know how to deal with it. Friday hasnt made the connection she can look up ‘kitchen tools.’ My description can absolutely be clearer. But look at SuperFriday over here…



No they’re not (Put a pin in this… I think, I know what this is…)
and - You got it WHERE?

That so?


Before we close class today friends we will talk about CONTEXT WINDOW. See at some point we start dropping things OFF the back in - kind of FIFO style. and when we do things start to fall out - and she forgets stuff.

I have EVERYTHING turned on right now for demo purposes but we’re rapidly approaching the day where Friday DOES NOT load all components unless we know we need it for this very reason. We only have so much space in the prompt and so much in the context window and without true RAG you have to manage this very carefully.

When I started seeing this symptom (cant -do a thing-) I started turning things off. and it looks like if I start turning off components, it gets better so - yeah, we’re stuffed up but we know how to combat it. We need to start strategically unloading components and extra text as much as possible. Tooling to figure out what component is taking the most space would help… But right now that’s Future Nathan’s Problem ™

So theory, you can give an LLM a pipe and a book and it can do things. YES… But here’s where we start getting realistic.
If I can make SuperFriday do something, we can make Friday do it. I ask SuperFriday how she knew and why Friday didn’t and she points me at the deficiencies…


OK Fine Friday - better description of what the book might have in it. Got it. Jacket Cover.

So time to get Friday loading up the kitchen. Guess what when she gets rocking YES she can even bulk add. :slight_smile:

Give her an appropriately credentialed API key - You have been warned. (I lied, Told you twice. I’m a security puke)

Hey wait! - didn’t you say Grocy?

Look up - Grocy supports REST AND OpenAPI… Guess how we do it…

I’m hungry Friday - find me a recipe to make with this wok!

2 Likes