
{"id":5216,"date":"2025-01-18T22:34:15","date_gmt":"2025-01-18T22:34:15","guid":{"rendered":"https:\/\/blog.gordonbuchan.com\/blog\/?p=5216"},"modified":"2025-09-02T20:34:56","modified_gmt":"2025-09-02T20:34:56","slug":"creating-a-script-that-analyzes-email-messages-messages-using-a-large-language-model-llm-and-where-appropriate-escalates-messages-to-the-attention-of-an-operator","status":"publish","type":"post","link":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/2025\/01\/18\/creating-a-script-that-analyzes-email-messages-messages-using-a-large-language-model-llm-and-where-appropriate-escalates-messages-to-the-attention-of-an-operator\/","title":{"rendered":"Creating a script that analyzes email messages messages using a large language model (LLM), and where appropriate escalates messages to the attention of an operator"},"content":{"rendered":"\n<p>In this post, we create a Python script that connects to a Gmail inbox, extracts the text of the subject and body of each message, submits that text with a prompt to a large language model (LLM), then if conditions are met that match the prompt, escalates the message to the attention of an operator, based on a prompt.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Using an Ollama API server<\/h1>\n\n\n\n<p>In this case, we are interacting with an Ollama LLM API server hosted locally. Refer to <a href=\"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/2025\/01\/11\/using-ollama-to-host-an-llm-on-cpu-only-equipment-to-enable-a-local-chatbot-and-openai-compatible-api-server\/\">Using Ollama to host an LLM on CPU-only equipment to enable a local chatbot and LLM API server<\/a>.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Using an OpenAI-compatible LLM API<\/h1>\n\n\n\n<p>An alternate source code listing is provided for an OpenAI-compatible LLM API.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Obtaining a Gmail app password<\/h1>\n\n\n\n<p>Visit the following site:<\/p>\n\n\n\n<p><a href=\"https:\/\/myaccount.google.com\/apppasswords\">https:\/\/myaccount.google.com\/apppasswords<\/a><\/p>\n\n\n\n<p>Create a new app password. Take note of the password, it will not be visible again.<\/p>\n\n\n\n<p>Note: Google adds spaces to the app password for readability. You should remove the spaces from the app password and use that value.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Escalating to the root user<\/h1>\n\n\n\n<p>In this procedure we run as the root user. Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo su\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Adding utilities to the operating system<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\napt install python3-venv python3-pip sqlite3\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating a virtual environment and installing required packages with pip<\/h1>\n\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncd ~\nmkdir doicareworkdir\ncd doicareworkdir\npython3 -m venv doicare_env\nsource doicare_env\/bin\/activate\npip install requests imaplib2\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating the configuration file (config.json)<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano config.json\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n{\n  &quot;gmail_user&quot;: &quot;xxxxxxxxxxxx@xxxxx.xxx&quot;,\n  &quot;gmail_app_password&quot;: &quot;xxxxxxxxxxxxxxxx&quot;,\n  &quot;api_base_url&quot;: &quot;http:\/\/xxx.xxx.xxx.xxx:8085&quot;,\n  &quot;openai_api_key&quot;: &quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;,\n  &quot;database&quot;: &quot;doicare.db&quot;,\n\n  &quot;scanasof&quot;: &quot;18-Jan-2025&quot;,\n\n  &quot;alert_recipients&quot;: &#x5B;\n    &quot;xxxxx@xxxxx.com&quot;\n  ],\n\n  &quot;smtp_server&quot;: &quot;smtp.gmail.com&quot;,\n  &quot;smtp_port&quot;: 587,\n  &quot;smtp_user&quot;: &quot;xxxxxx@xxxxx.xxxxx&quot;,\n  &quot;smtp_password&quot;: &quot;xxxxxxxxxxxxxxxx&quot;,\n\n  &quot;analysis_prompt&quot;: &quot;Analyze the email below. If it needs escalation (urgent, sender upset, or critical issue), return &#039;Escalation Reason:&#039; followed by one short sentence explaining why. If no escalation is needed, return exactly &#039;DOESNOTAPPLY&#039;. Always provide either &#039;DOESNOTAPPLY&#039; or a reason.&quot;,\n  &quot;model&quot;: &quot;mistral&quot;\n\n}\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating a Python script called doicare that connects to a Gmail inbox, submits messages to an LLM, and escalates messages based on a prompt (Ollama version)<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n\n<p>nano doicare_gmail.py<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport imaplib\nimport email\nimport sqlite3\nimport requests\nimport smtplib\nimport json\nfrom datetime import datetime\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nfrom email.header import decode_header, make_header\n\n# MIT license 2025 Gordon Buchan\n# see https:\/\/opensource.org\/licenses\/MIT\n# Some of this code was generated with the assistance of AI tools.\n\n# --------------------------------------------------------------------\n# 1. LOAD CONFIG\n# --------------------------------------------------------------------\nwith open(&quot;config.json&quot;, &quot;r&quot;) as cfg:\n    config = json.load(cfg)\n\nGMAIL_USER = config&#x5B;&quot;gmail_user&quot;]\nGMAIL_APP_PASSWORD = config&#x5B;&quot;gmail_app_password&quot;]\nAPI_BASE_URL = config&#x5B;&quot;api_base_url&quot;]\nOPENAI_API_KEY = config&#x5B;&quot;openai_api_key&quot;]\nDATABASE = config&#x5B;&quot;database&quot;]\nSCAN_ASOF = config&#x5B;&quot;scanasof&quot;]\nALERT_RECIPIENTS = config.get(&quot;alert_recipients&quot;, &#x5B;])\nSMTP_SERVER = config&#x5B;&quot;smtp_server&quot;]\nSMTP_PORT = config&#x5B;&quot;smtp_port&quot;]\nSMTP_USER = config&#x5B;&quot;smtp_user&quot;]\nSMTP_PASSWORD = config&#x5B;&quot;smtp_password&quot;]\nANALYSIS_PROMPT = config&#x5B;&quot;analysis_prompt&quot;]\nMODEL = config&#x5B;&quot;model&quot;]\n\n# --------------------------------------------------------------------\n# 2. DATABASE SETUP\n# --------------------------------------------------------------------\ndef setup_database():\n    conn = sqlite3.connect(DATABASE)\n    cur = conn.cursor()\n    cur.execute(&quot;&quot;&quot;\n        CREATE TABLE IF NOT EXISTS escalations (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            email_date TEXT,\n            from_address TEXT,\n            to_address TEXT,\n            cc_address TEXT,\n            subject TEXT,\n            body TEXT,\n            reason TEXT,\n            created_at TEXT\n        )\n    &quot;&quot;&quot;)\n    cur.execute(&quot;&quot;&quot;\n        CREATE TABLE IF NOT EXISTS scan_info (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            last_scanned_uid INTEGER\n        )\n    &quot;&quot;&quot;)\n    conn.commit()\n    conn.close()\n\ndef get_last_scanned_uid():\n    conn = sqlite3.connect(DATABASE)\n    cur = conn.cursor()\n    cur.execute(&quot;SELECT last_scanned_uid FROM scan_info ORDER BY id DESC LIMIT 1&quot;)\n    row = cur.fetchone()\n    conn.close()\n    return row&#x5B;0] if (row and row&#x5B;0]) else 0\n\ndef update_last_scanned_uid(uid_val):\n    conn = sqlite3.connect(DATABASE)\n    cur = conn.cursor()\n    cur.execute(&quot;INSERT INTO scan_info (last_scanned_uid) VALUES (?)&quot;, (uid_val,))\n    conn.commit()\n    conn.close()\n\ndef is_already_processed(uid_val):\n    conn = sqlite3.connect(DATABASE)\n    cur = conn.cursor()\n    cur.execute(&quot;SELECT 1 FROM scan_info WHERE last_scanned_uid = ?&quot;, (uid_val,))\n    row = cur.fetchone()\n    conn.close()\n    return bool(row)\n\n# --------------------------------------------------------------------\n# 3. ANALYSIS &amp;amp; ALERTING\n# --------------------------------------------------------------------\ndef analyze_with_openai(subject, body):\n    prompt = f&quot;{ANALYSIS_PROMPT}\\n\\nSubject: {subject}\\nBody: {body}&quot;\n    url = f&quot;{API_BASE_URL}\/v1\/completions&quot;\n    headers = {&quot;Content-Type&quot;: &quot;application\/json&quot;}\n    if OPENAI_API_KEY:\n        headers&#x5B;&quot;Authorization&quot;] = f&quot;Bearer {OPENAI_API_KEY}&quot;\n\n    payload = {\n        &quot;model&quot;: MODEL,\n        &quot;prompt&quot;: prompt,\n        &quot;max_tokens&quot;: 300,\n        &quot;temperature&quot;: 0.7\n    }\n\n    try:\n        response = requests.post(url, headers=headers, json=payload, timeout=60)\n        data = response.json()\n\n        if &quot;error&quot; in data:\n            print(f&quot;&#x5B;DEBUG] API Error: {data&#x5B;&#039;error&#039;]&#x5B;&#039;message&#039;]}&quot;)\n            return &quot;DOESNOTAPPLY&quot;\n\n        if &quot;choices&quot; in data and data&#x5B;&quot;choices&quot;]:\n            raw_text = data&#x5B;&quot;choices&quot;]&#x5B;0]&#x5B;&quot;text&quot;].strip()\n            return raw_text\n\n        return &quot;DOESNOTAPPLY&quot;\n\n    except Exception as e:\n        print(f&quot;&#x5B;DEBUG] Exception during API call: {e}&quot;)\n        return &quot;DOESNOTAPPLY&quot;\n\ndef send_alerts(reason, email_date, from_addr, to_addr, cc_addr, subject, body):\n    for recipient in ALERT_RECIPIENTS:\n        msg = MIMEMultipart()\n        msg&#x5B;&quot;From&quot;] = SMTP_USER\n        msg&#x5B;&quot;To&quot;] = recipient\n        msg&#x5B;&quot;Subject&quot;] = &quot;Escalation Alert&quot;\n\n        alert_text = f&quot;&quot;&quot;\n        Escalation Triggered\n        Date: {email_date}\n        From: {from_addr}\n        To: {to_addr}\n        CC: {cc_addr}\n        Subject: {subject}\n        Body: {body}\n\n        Reason: {reason}\n        &quot;&quot;&quot;\n        msg.attach(MIMEText(alert_text, &quot;plain&quot;))\n\n        try:\n            with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:\n                server.starttls()\n                server.login(SMTP_USER, SMTP_PASSWORD)\n                server.sendmail(SMTP_USER, recipient, msg.as_string())\n            print(f&quot;Alert sent to {recipient}&quot;)\n        except Exception as ex:\n            print(f&quot;Failed to send alert to {recipient}: {ex}&quot;)\n\ndef save_escalation(email_date, from_addr, to_addr, cc_addr, subject, body, reason):\n    conn = sqlite3.connect(DATABASE)\n    cur = conn.cursor()\n    cur.execute(&quot;&quot;&quot;\n        INSERT INTO escalations (\n            email_date, from_address, to_address, cc_address,\n            subject, body, reason, created_at\n        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n    &quot;&quot;&quot;, (\n        email_date, from_addr, to_addr, cc_addr,\n        subject, body, reason, datetime.now().isoformat()\n    ))\n    conn.commit()\n    conn.close()\n\n# --------------------------------------------------------------------\n# 4. MAIN LOGIC\n# --------------------------------------------------------------------\ndef process_message(raw_email, uid_val):\n    parsed_msg = email.message_from_bytes(raw_email)\n\n    date_str = parsed_msg.get(&quot;Date&quot;, &quot;&quot;)\n    from_addr = parsed_msg.get(&quot;From&quot;, &quot;&quot;)\n    to_addr = parsed_msg.get(&quot;To&quot;, &quot;&quot;)\n    cc_addr = parsed_msg.get(&quot;Cc&quot;, &quot;&quot;)\n    subject_header = parsed_msg.get(&quot;Subject&quot;, &quot;&quot;)\n    subject_decoded = str(make_header(decode_header(subject_header)))\n\n    body_text = &quot;&quot;\n    if parsed_msg.is_multipart():\n        for part in parsed_msg.walk():\n            ctype = part.get_content_type()\n            disposition = str(part.get(&quot;Content-Disposition&quot;))\n            if ctype == &quot;text\/plain&quot; and &quot;attachment&quot; not in disposition:\n                charset = part.get_content_charset() or &quot;utf-8&quot;\n                body_text += part.get_payload(decode=True).decode(charset, errors=&quot;replace&quot;)\n    else:\n        charset = parsed_msg.get_content_charset() or &quot;utf-8&quot;\n        body_text = parsed_msg.get_payload(decode=True).decode(charset, errors=&quot;replace&quot;)\n\n    reason = analyze_with_openai(subject_decoded, body_text)\n    if &quot;DOESNOTAPPLY&quot; in reason:\n        print(f&quot;&#x5B;UID {uid_val}] No escalation: {reason}&quot;)\n        return\n\n    print(f&quot;&#x5B;UID {uid_val}] Escalation triggered: {subject_decoded&#x5B;:50]}&quot;)\n    save_escalation(date_str, from_addr, to_addr, cc_addr, subject_decoded, body_text, reason)\n    send_alerts(reason, date_str, from_addr, to_addr, cc_addr, subject_decoded, body_text)\n\ndef main():\n    setup_database()\n    last_uid = get_last_scanned_uid()\n    print(f&quot;&#x5B;DEBUG] Retrieved last UID: {last_uid}&quot;)\n\n    try:\n        mail = imaplib.IMAP4_SSL(&quot;imap.gmail.com&quot;)\n        mail.login(GMAIL_USER, GMAIL_APP_PASSWORD)\n        print(&quot;IMAP login successful.&quot;)\n    except Exception as e:\n        print(f&quot;Error logging into Gmail: {e}&quot;)\n        return\n\n    mail.select(&quot;INBOX&quot;)\n\n    if last_uid == 0:\n        print(f&quot;&#x5B;DEBUG] First run: scanning since date {SCAN_ASOF}&quot;)\n        r1, d1 = mail.search(None, f&#039;(SINCE {SCAN_ASOF})&#039;)\n    else:\n        print(f&quot;&#x5B;DEBUG] Subsequent run: scanning for UIDs &gt; {last_uid}&quot;)\n        r1, d1 = mail.uid(&#039;SEARCH&#039;, None, f&#039;UID {last_uid + 1}:*&#039;)\n\n    if r1 != &quot;OK&quot;:\n        print(&quot;&#x5B;DEBUG] Search failed.&quot;)\n        mail.logout()\n        return\n\n    seq_nums = d1&#x5B;0].split()\n    print(f&quot;&#x5B;DEBUG] Found {len(seq_nums)} messages to process: {seq_nums}&quot;)\n\n    if not seq_nums:\n        print(&quot;&#x5B;DEBUG] No messages to process.&quot;)\n        mail.logout()\n        return\n\n    highest_uid_seen = last_uid\n\n    for seq_num in seq_nums:\n        if is_already_processed(seq_num.decode()):\n            print(f&quot;&#x5B;DEBUG] UID {seq_num.decode()} already processed, skipping.&quot;)\n            continue\n\n        print(f&quot;&#x5B;DEBUG] Processing sequence number: {seq_num}&quot;)\n        r2, d2 = mail.uid(&#039;FETCH&#039;, seq_num.decode(), &#039;(RFC822)&#039;)\n        if r2 != &quot;OK&quot; or not d2 or len(d2) &amp;lt; 1 or not d2&#x5B;0]:\n            print(f&quot;&#x5B;DEBUG] Failed to fetch message for UID {seq_num.decode()}&quot;)\n            continue\n\n        print(f&quot;&#x5B;DEBUG] Successfully fetched message for UID {seq_num.decode()}&quot;)\n        raw_email = d2&#x5B;0]&#x5B;1]\n\n        try:\n            process_message(raw_email, int(seq_num.decode()))\n            mail.uid(&#039;STORE&#039;, seq_num.decode(), &#039;+FLAGS&#039;, &#039;\\\\Seen&#039;)\n            if int(seq_num.decode()) &gt; highest_uid_seen:\n                highest_uid_seen = int(seq_num.decode())\n        except Exception as e:\n            print(f&quot;&#x5B;DEBUG] Error processing message UID {seq_num.decode()}: {e}&quot;)\n\n    if highest_uid_seen &gt; last_uid:\n        print(f&quot;&#x5B;DEBUG] Updating last scanned UID to {highest_uid_seen}&quot;)\n        update_last_scanned_uid(highest_uid_seen)\n\n    mail.logout()\n\nif __name__ == &quot;__main__&quot;:\n    main()\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating a Python script called doicare that connects to a Gmail inbox, submits messages to an LLM, and escalates messages based on a prompt (OpenAI-compatible version)<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n\n<p>nano doicare_gmail.py<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport imaplib\nimport email\nimport sqlite3\nimport requests\nimport smtplib\nimport json\nfrom datetime import datetime\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nfrom email.header import decode_header, make_header\n\n# MIT license 2025 Gordon Buchan\n# see https:\/\/opensource.org\/licenses\/MIT\n# Some of this code was generated with the assistance of AI tools.\n\n# --------------------------------------------------------------------\n# 1. LOAD CONFIG\n# --------------------------------------------------------------------\nwith open(&quot;config.json&quot;, &quot;r&quot;) as cfg:\n    config = json.load(cfg)\n\nGMAIL_USER = config&#x5B;&quot;gmail_user&quot;]\nGMAIL_APP_PASSWORD = config&#x5B;&quot;gmail_app_password&quot;]\nAPI_BASE_URL = config&#x5B;&quot;api_base_url&quot;]\nOPENAI_API_KEY = config&#x5B;&quot;openai_api_key&quot;]\nDATABASE = config&#x5B;&quot;database&quot;]\nSCAN_ASOF = config&#x5B;&quot;scanasof&quot;]\nALERT_RECIPIENTS = config.get(&quot;alert_recipients&quot;, &#x5B;])\nSMTP_SERVER = config&#x5B;&quot;smtp_server&quot;]\nSMTP_PORT = config&#x5B;&quot;smtp_port&quot;]\nSMTP_USER = config&#x5B;&quot;smtp_user&quot;]\nSMTP_PASSWORD = config&#x5B;&quot;smtp_password&quot;]\nANALYSIS_PROMPT = config&#x5B;&quot;analysis_prompt&quot;]\nMODEL = config&#x5B;&quot;model&quot;]\n\n# --------------------------------------------------------------------\n# 2. DATABASE SETUP\n# --------------------------------------------------------------------\ndef setup_database():\n    &quot;&quot;&quot; Ensure the database and necessary tables exist. &quot;&quot;&quot;\n    conn = sqlite3.connect(DATABASE)\n    cur = conn.cursor()\n\n    print(&quot;&#x5B;DEBUG] Ensuring database tables exist...&quot;)\n\n    cur.execute(&quot;&quot;&quot;\n        CREATE TABLE IF NOT EXISTS escalations (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            email_date TEXT,\n            from_address TEXT,\n            to_address TEXT,\n            cc_address TEXT,\n            subject TEXT,\n            body TEXT,\n            reason TEXT,\n            created_at TEXT\n        )\n    &quot;&quot;&quot;)\n\n    cur.execute(&quot;&quot;&quot;\n        CREATE TABLE IF NOT EXISTS scan_info (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            last_scanned_uid INTEGER UNIQUE\n        )\n    &quot;&quot;&quot;)\n\n    # Ensure at least one row exists in scan_info\n    cur.execute(&quot;SELECT COUNT(*) FROM scan_info&quot;)\n    if cur.fetchone()&#x5B;0] == 0:\n        cur.execute(&quot;INSERT INTO scan_info (last_scanned_uid) VALUES (0)&quot;)\n\n    conn.commit()\n    conn.close()\n    print(&quot;&#x5B;DEBUG] Database setup complete.&quot;)\n\ndef get_last_scanned_uid():\n    &quot;&quot;&quot; Retrieve the last scanned UID from the database &quot;&quot;&quot;\n    conn = sqlite3.connect(DATABASE)\n    cur = conn.cursor()\n    cur.execute(&quot;SELECT last_scanned_uid FROM scan_info ORDER BY id DESC LIMIT 1&quot;)\n    row = cur.fetchone()\n    conn.close()\n    return int(row&#x5B;0]) if (row and row&#x5B;0]) else 0\n\ndef update_last_scanned_uid(uid_val):\n    &quot;&quot;&quot; Update the last scanned UID in the database &quot;&quot;&quot;\n    conn = sqlite3.connect(DATABASE)\n    cur = conn.cursor()\n    cur.execute(&quot;&quot;&quot;\n        INSERT INTO scan_info (id, last_scanned_uid)\n        VALUES (1, ?) \n        ON CONFLICT(id) DO UPDATE SET last_scanned_uid = excluded.last_scanned_uid\n    &quot;&quot;&quot;, (uid_val,))\n    conn.commit()\n    conn.close()\n\n# --------------------------------------------------------------------\n# 3. ANALYSIS &amp;amp; ALERTING\n# --------------------------------------------------------------------\ndef analyze_with_openai(subject, body):\n    &quot;&quot;&quot; Send email content to OpenAI API for analysis &quot;&quot;&quot;\n    prompt = f&quot;{ANALYSIS_PROMPT}\\n\\nSubject: {subject}\\nBody: {body}&quot;\n    url = f&quot;{API_BASE_URL}\/v1\/chat\/completions&quot;\n    headers = {\n        &quot;Content-Type&quot;: &quot;application\/json&quot;,\n        &quot;Authorization&quot;: f&quot;Bearer {OPENAI_API_KEY}&quot; if OPENAI_API_KEY else &quot;&quot;,\n    }\n\n    payload = {\n        &quot;model&quot;: MODEL,\n        &quot;messages&quot;: &#x5B;\n            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a helpful assistant&quot;},\n            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: prompt}\n        ],\n        &quot;max_tokens&quot;: 300,\n        &quot;temperature&quot;: 0.7\n    }\n\n    try:\n        response = requests.post(url, headers=headers, json=payload, timeout=60)\n        data = response.json()\n\n        if &quot;error&quot; in data:\n            print(f&quot;&#x5B;DEBUG] API Error: {data&#x5B;&#039;error&#039;]&#x5B;&#039;message&#039;]}&quot;)\n            return &quot;DOESNOTAPPLY&quot;\n\n        if &quot;choices&quot; in data and data&#x5B;&quot;choices&quot;]:\n            return data&#x5B;&quot;choices&quot;]&#x5B;0]&#x5B;&quot;message&quot;]&#x5B;&quot;content&quot;].strip()\n\n        return &quot;DOESNOTAPPLY&quot;\n\n    except Exception as e:\n        print(f&quot;&#x5B;DEBUG] Exception during API call: {e}&quot;)\n        return &quot;DOESNOTAPPLY&quot;\n\n# --------------------------------------------------------------------\n# 4. MAIN LOGIC\n# --------------------------------------------------------------------\ndef process_message(raw_email, uid_val):\n    &quot;&quot;&quot; Process a single email message &quot;&quot;&quot;\n    parsed_msg = email.message_from_bytes(raw_email)\n\n    date_str = parsed_msg.get(&quot;Date&quot;, &quot;&quot;)\n    from_addr = parsed_msg.get(&quot;From&quot;, &quot;&quot;)\n    to_addr = parsed_msg.get(&quot;To&quot;, &quot;&quot;)\n    cc_addr = parsed_msg.get(&quot;Cc&quot;, &quot;&quot;)\n    subject_header = parsed_msg.get(&quot;Subject&quot;, &quot;&quot;)\n    subject_decoded = str(make_header(decode_header(subject_header)))\n\n    body_text = &quot;&quot;\n    if parsed_msg.is_multipart():\n        for part in parsed_msg.walk():\n            ctype = part.get_content_type()\n            disposition = str(part.get(&quot;Content-Disposition&quot;))\n            if ctype == &quot;text\/plain&quot; and &quot;attachment&quot; not in disposition:\n                charset = part.get_content_charset() or &quot;utf-8&quot;\n                body_text += part.get_payload(decode=True).decode(charset, errors=&quot;replace&quot;)\n    else:\n        charset = parsed_msg.get_content_charset() or &quot;utf-8&quot;\n        body_text = parsed_msg.get_payload(decode=True).decode(charset, errors=&quot;replace&quot;)\n\n    reason = analyze_with_openai(subject_decoded, body_text)\n    if &quot;DOESNOTAPPLY&quot; in reason:\n        print(f&quot;&#x5B;UID {uid_val}] No escalation: {reason}&quot;)\n        return\n\n    print(f&quot;&#x5B;UID {uid_val}] Escalation triggered: {subject_decoded&#x5B;:50]}&quot;)\n\n    update_last_scanned_uid(uid_val)\n\ndef main():\n    &quot;&quot;&quot; Main function to fetch and process emails &quot;&quot;&quot;\n    setup_database()\n    last_uid = get_last_scanned_uid()\n    print(f&quot;&#x5B;DEBUG] Retrieved last UID: {last_uid}&quot;)\n\n    try:\n        mail = imaplib.IMAP4_SSL(&quot;imap.gmail.com&quot;)\n        mail.login(GMAIL_USER, GMAIL_APP_PASSWORD)\n        print(&quot;IMAP login successful.&quot;)\n    except Exception as e:\n        print(f&quot;Error logging into Gmail: {e}&quot;)\n        return\n\n    mail.select(&quot;INBOX&quot;)\n\n    search_query = f&#039;UID {last_uid + 1}:*&#039; if last_uid &gt; 0 else f&#039;SINCE {SCAN_ASOF}&#039;\n    print(f&quot;&#x5B;DEBUG] Running IMAP search: {search_query}&quot;)\n\n    r1, d1 = mail.uid(&#039;SEARCH&#039;, None, search_query)\n\n    if r1 != &quot;OK&quot;:\n        print(&quot;&#x5B;DEBUG] Search failed.&quot;)\n        mail.logout()\n        return\n\n    seq_nums = d1&#x5B;0].split()\n    seq_nums = &#x5B;seq.decode() for seq in seq_nums]\n\n    print(f&quot;&#x5B;DEBUG] Found {len(seq_nums)} new messages: {seq_nums}&quot;)\n\n    if not seq_nums:\n        print(&quot;&#x5B;DEBUG] No new messages found, exiting.&quot;)\n        mail.logout()\n        return\n\n    highest_uid_seen = last_uid\n\n    for seq_num in seq_nums:\n        numeric_uid = int(seq_num)\n        if numeric_uid &amp;lt;= last_uid:\n            print(f&quot;&#x5B;DEBUG] UID {numeric_uid} already processed, skipping.&quot;)\n            continue\n\n        print(f&quot;&#x5B;DEBUG] Processing UID: {numeric_uid}&quot;)\n        r2, d2 = mail.uid(&#039;FETCH&#039;, seq_num, &#039;(RFC822)&#039;)\n        if r2 != &quot;OK&quot; or not d2 or len(d2) &amp;lt; 1 or not d2&#x5B;0]:\n            print(f&quot;&#x5B;DEBUG] Failed to fetch message for UID {numeric_uid}&quot;)\n            continue\n\n        raw_email = d2&#x5B;0]&#x5B;1]\n        process_message(raw_email, numeric_uid)\n\n        highest_uid_seen = max(highest_uid_seen, numeric_uid)\n\n    if highest_uid_seen &gt; last_uid:\n        print(f&quot;&#x5B;DEBUG] Updating last scanned UID to {highest_uid_seen}&quot;)\n        update_last_scanned_uid(highest_uid_seen)\n\n    mail.logout()\n\nif __name__ == &quot;__main__&quot;:\n    main()\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Running the doicare_gmail.py script<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npython3 doicare_gmail.py\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Sample output<\/h1>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n(doicare_env) root@xxxxx:\/home\/desktop\/doicareworkingdir# python3 doicare_gmail.py \n&#x5B;DEBUG] Retrieved last UID: 0\nIMAP login successful.\n&#x5B;DEBUG] First run: scanning since date 18-Jan-2025\n&#x5B;DEBUG] Found 23 messages to process: &#x5B;b&#039;49146&#039;, b&#039;49147&#039;, b&#039;49148&#039;, b&#039;49149&#039;, b&#039;49150&#039;, b&#039;49151&#039;, b&#039;49152&#039;, b&#039;49153&#039;, b&#039;49154&#039;, b&#039;49155&#039;, b&#039;49156&#039;, b&#039;49157&#039;, b&#039;49158&#039;, b&#039;49159&#039;, b&#039;49160&#039;, b&#039;49161&#039;, b&#039;49162&#039;, b&#039;49163&#039;, b&#039;49164&#039;, b&#039;49165&#039;, b&#039;49166&#039;, b&#039;49167&#039;, b&#039;49168&#039;]\n&#x5B;DEBUG] Processing sequence number: b&#039;49146&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49146 (UID 50196)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49146 (UID 50196)\n&#x5B;DEBUG] Parsed UID: 50196\n&#x5B;DEBUG] Valid UID Found: 50196\n&#x5B;DEBUG] Successfully fetched message for UID 50196\n&#x5B;UID 50196] No escalation: DOESNOTAPPLY. The email does not contain any urgent matter, sender is not upset, and there does not seem to be a critical issue mentioned.\n&#x5B;DEBUG] Processing sequence number: b&#039;49147&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49147 (UID 50197)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49147 (UID 50197)\n&#x5B;DEBUG] Parsed UID: 50197\n&#x5B;DEBUG] Valid UID Found: 50197\n&#x5B;DEBUG] Successfully fetched message for UID 50197\n&#x5B;UID 50197] No escalation: DOESNOTAPPLY\n&#x5B;DEBUG] Processing sequence number: b&#039;49148&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49148 (UID 50198)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49148 (UID 50198)\n&#x5B;DEBUG] Parsed UID: 50198\n&#x5B;DEBUG] Valid UID Found: 50198\n&#x5B;DEBUG] Successfully fetched message for UID 50198\n&#x5B;UID 50198] No escalation: DOESNOTAPPLY. The email does not contain any urgent matter, sender is not upset, and there doesn&#039;t seem to be a critical issue presented in the content.\n&#x5B;DEBUG] Processing sequence number: b&#039;49149&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49149 (UID 50199)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49149 (UID 50199)\n&#x5B;DEBUG] Parsed UID: 50199\n&#x5B;DEBUG] Valid UID Found: 50199\n&#x5B;DEBUG] Successfully fetched message for UID 50199\n&#x5B;UID 50199] No escalation: DOESNOTAPPLY. The email does not contain any urgent matter, the sender is not upset, and there is no critical issue mentioned in the message.\n&#x5B;DEBUG] Processing sequence number: b&#039;49150&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49150 (UID 50200)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49150 (UID 50200)\n&#x5B;DEBUG] Parsed UID: 50200\n&#x5B;DEBUG] Valid UID Found: 50200\n&#x5B;DEBUG] Successfully fetched message for UID 50200\n&#x5B;UID 50200] No escalation: DOESNOTAPPLY. The email lacks sufficient content for an escalation.\n&#x5B;DEBUG] Processing sequence number: b&#039;49151&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49151 (UID 50201)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49151 (UID 50201)\n&#x5B;DEBUG] Parsed UID: 50201\n&#x5B;DEBUG] Valid UID Found: 50201\n&#x5B;DEBUG] Successfully fetched message for UID 50201\n&#x5B;UID 50201] Escalation triggered: Security alert\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49152&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49152 (UID 50202)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49152 (UID 50202)\n&#x5B;DEBUG] Parsed UID: 50202\n&#x5B;DEBUG] Valid UID Found: 50202\n&#x5B;DEBUG] Successfully fetched message for UID 50202\n&#x5B;UID 50202] Escalation triggered: Delivery Status Notification (Failure)\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49153&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49153 (UID 50203)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49153 (UID 50203)\n&#x5B;DEBUG] Parsed UID: 50203\n&#x5B;DEBUG] Valid UID Found: 50203\n&#x5B;DEBUG] Successfully fetched message for UID 50203\n&#x5B;UID 50203] No escalation: DOESNOTAPPLY\n&#x5B;DEBUG] Processing sequence number: b&#039;49154&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49154 (UID 50204)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49154 (UID 50204)\n&#x5B;DEBUG] Parsed UID: 50204\n&#x5B;DEBUG] Valid UID Found: 50204\n&#x5B;DEBUG] Successfully fetched message for UID 50204\n&#x5B;UID 50204] Escalation triggered: my server lollipop is down\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49155&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49155 (UID 50205)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49155 (UID 50205)\n&#x5B;DEBUG] Parsed UID: 50205\n&#x5B;DEBUG] Valid UID Found: 50205\n&#x5B;DEBUG] Successfully fetched message for UID 50205\n&#x5B;UID 50205] No escalation: DOESNOTAPPLY\n&#x5B;DEBUG] Processing sequence number: b&#039;49156&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49156 (UID 50206)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49156 (UID 50206)\n&#x5B;DEBUG] Parsed UID: 50206\n&#x5B;DEBUG] Valid UID Found: 50206\n&#x5B;DEBUG] Successfully fetched message for UID 50206\n&#x5B;UID 50206] Escalation triggered: now doomfire is down too!\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49157&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49157 (UID 50207)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49157 (UID 50207)\n&#x5B;DEBUG] Parsed UID: 50207\n&#x5B;DEBUG] Valid UID Found: 50207\n&#x5B;DEBUG] Successfully fetched message for UID 50207\n&#x5B;UID 50207] No escalation: DOESNOTAPPLY\n&#x5B;DEBUG] Processing sequence number: b&#039;49158&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49158 (UID 50208)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49158 (UID 50208)\n&#x5B;DEBUG] Parsed UID: 50208\n&#x5B;DEBUG] Valid UID Found: 50208\n&#x5B;DEBUG] Successfully fetched message for UID 50208\n&#x5B;UID 50208] Escalation triggered: pants is down now\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49159&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49159 (UID 50209)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49159 (UID 50209)\n&#x5B;DEBUG] Parsed UID: 50209\n&#x5B;DEBUG] Valid UID Found: 50209\n&#x5B;DEBUG] Successfully fetched message for UID 50209\n&#x5B;UID 50209] Escalation triggered: server05 down\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49160&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49160 (UID 50210)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49160 (UID 50210)\n&#x5B;DEBUG] Parsed UID: 50210\n&#x5B;DEBUG] Valid UID Found: 50210\n&#x5B;DEBUG] Successfully fetched message for UID 50210\n&#x5B;UID 50210] No escalation: DOESNOTAPPLY (The sender has asked for a phone call instead of specifying the issue in detail, so it doesn&#039;t appear to be urgent or critical at first glance.)\n&#x5B;DEBUG] Processing sequence number: b&#039;49161&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49161 (UID 50211)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49161 (UID 50211)\n&#x5B;DEBUG] Parsed UID: 50211\n&#x5B;DEBUG] Valid UID Found: 50211\n&#x5B;DEBUG] Successfully fetched message for UID 50211\n&#x5B;UID 50211] Escalation triggered: my server is down\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49162&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49162 (UID 50212)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49162 (UID 50212)\n&#x5B;DEBUG] Parsed UID: 50212\n&#x5B;DEBUG] Valid UID Found: 50212\n&#x5B;DEBUG] Successfully fetched message for UID 50212\n&#x5B;UID 50212] No escalation: DOESNOTAPPLY\n&#x5B;DEBUG] Processing sequence number: b&#039;49163&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49163 (UID 50213)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49163 (UID 50213)\n&#x5B;DEBUG] Parsed UID: 50213\n&#x5B;DEBUG] Valid UID Found: 50213\n&#x5B;DEBUG] Successfully fetched message for UID 50213\n&#x5B;UID 50213] Escalation triggered: this is getting bad\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49164&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49164 (UID 50214)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49164 (UID 50214)\n&#x5B;DEBUG] Parsed UID: 50214\n&#x5B;DEBUG] Valid UID Found: 50214\n&#x5B;DEBUG] Successfully fetched message for UID 50214\n&#x5B;UID 50214] No escalation: DOESNOTAPPLY\n&#x5B;DEBUG] Processing sequence number: b&#039;49165&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49165 (UID 50215)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49165 (UID 50215)\n&#x5B;DEBUG] Parsed UID: 50215\n&#x5B;DEBUG] Valid UID Found: 50215\n&#x5B;DEBUG] Successfully fetched message for UID 50215\n&#x5B;UID 50215] Escalation triggered: server zebra 05 is down\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49166&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49166 (UID 50216)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49166 (UID 50216)\n&#x5B;DEBUG] Parsed UID: 50216\n&#x5B;DEBUG] Valid UID Found: 50216\n&#x5B;DEBUG] Successfully fetched message for UID 50216\n&#x5B;UID 50216] No escalation: DOESNOTAPPLY\n&#x5B;DEBUG] Processing sequence number: b&#039;49167&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49167 (UID 50217)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49167 (UID 50217)\n&#x5B;DEBUG] Parsed UID: 50217\n&#x5B;DEBUG] Valid UID Found: 50217\n&#x5B;DEBUG] Successfully fetched message for UID 50217\n&#x5B;UID 50217] Escalation triggered: help\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Processing sequence number: b&#039;49168&#039;\n&#x5B;DEBUG] FETCH response: b&#039;49168 (UID 50218)&#039;\n&#x5B;DEBUG] FETCH line to parse: 49168 (UID 50218)\n&#x5B;DEBUG] Parsed UID: 50218\n&#x5B;DEBUG] Valid UID Found: 50218\n&#x5B;DEBUG] Successfully fetched message for UID 50218\n&#x5B;UID 50218] Escalation triggered: server is down\nAlert sent to xxxx@hotmail.com\n&#x5B;DEBUG] Updating last scanned UID to 50218\n&#x5B;DEBUG] Attempting to update last scanned UID to 50218\n&#x5B;DEBUG] Last scanned UID successfully updated to 50218\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Example of an alert message<\/h1>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nEscalation Triggered\nDate: Sat, 18 Jan 2025 21:00:16 +0000\nFrom: Gordon Buchan &amp;lt;gordonhbuchan@hotmail.com&gt;\nTo: &quot;gordonhbuchan@gmail.com&quot; &amp;lt;gordonhbuchan@gmail.com&gt;\nCC:\nSubject: server is down\nBody: server down help please\n\n\nReason: Escalation Reason: This email indicates that there is a critical issue (server downtime).\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating a systemd service to run the doicare script automatically<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano \/etc\/systemd\/system\/doicare.service\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text (change values to match your path):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;Unit]\nDescription=Run all monitoring tasks\n\n&#x5B;Service]\nType=oneshot\nWorkingDirectory=\/root\/doicareworkdir\nExecStart=\/usr\/bin\/bash -c &quot;source \/root\/doicareworkdir\/doicare_env\/bin\/activate &amp;amp;&amp;amp; python3 doicare_gmail.py&quot;\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating a systemd timer to run the doicare script automatically<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano \/etc\/systemd\/system\/doicare.timer\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;Unit]\nDescription=Run monitoring tasks every 5 minutes\n\n&#x5B;Timer]\nOnBootSec=5min\nOnUnitActiveSec=5min\n\n&#x5B;Install]\nWantedBy=timers.target\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Enabling the doicare service<\/h1>\n\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsystemctl daemon-reload\nsystemctl start doicare.service\nsystemctl enable doicare.service\nsystemctl start doicare.timer\nsystemctl enable doicare.timer\n<\/pre><\/div>\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this post, we create a Python script that connects to a Gmail inbox, extracts the text of the subject and body of each message, submits that text with a prompt to a large language model (LLM), then if conditions are met that match the prompt, escalates the message to the attention of an operator, &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/2025\/01\/18\/creating-a-script-that-analyzes-email-messages-messages-using-a-large-language-model-llm-and-where-appropriate-escalates-messages-to-the-attention-of-an-operator\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Creating a script that analyzes email messages messages using a large language model (LLM), and where appropriate escalates messages to the attention of an operator&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3,1],"tags":[],"class_list":["post-5216","post","type-post","status-publish","format-standard","hentry","category-linux","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/5216","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=5216"}],"version-history":[{"count":61,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/5216\/revisions"}],"predecessor-version":[{"id":5480,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/5216\/revisions\/5480"}],"wp:attachment":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=5216"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=5216"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=5216"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}