Skip to content

Latest commit

 

History

History
289 lines (234 loc) · 9.92 KB

File metadata and controls

289 lines (234 loc) · 9.92 KB

OSEM Plugin Python Scripting Interface Specification

This document defines the interaction interface between the OSEM (Outlook Event Monitor) plugin and external Python scripts.

1. Invocation Mechanism

The plugin executes scripts by launching the Python interpreter via the Windows command line.

  • Execution Command: python.exe "<ScriptPath>" "<ContextJsonPath>"
  • Working Directory: The directory where the script resides.
  • Arguments:
    1. ScriptPath: The absolute path to the Python script.
    2. ContextJsonPath: The absolute path to a temporary JSON file containing the current event context information.

2. Input Data (Context JSON)

The temporary JSON file generated by the plugin uses UTF-8 encoding. The root object contains the following fields:

2.1 Root Object Structure

Field Name Type Description
EventId string Unique identifier for the event (GUID).
EventTitle string Title of the event (usually the email subject or a custom title).
DashboardTemplateId string? ID of the currently used dashboard template (can be null).
DashboardValues object Dictionary of key-value pairs containing existing dashboard field values.
Emails array List of related emails (sorted by time descending or relevance).
Attachments array List of related attachments.

2.2 EmailItem Object Structure (in Emails array)

Field Name Type Description
EntryId string Unique EntryID of the Outlook mail object (Key field for COM calls).
StoreId string ID of the store where the email resides.
Subject string Email subject.
Sender string Sender address/name.
Participants array[string] List of all participants (To, CC).
ReceivedOn string Received time (ISO 8601 format).
BodyFingerprint string Email body fingerprint (for deduplication).
InternetMessageId string Internet Message ID of the email.
ConversationId string Conversation ID.

2.3 AttachmentItem Object Structure (in Attachments array)

Field Name Type Description
Id string Unique identifier for the attachment.
FileName string File name (including extension).
FileType string File extension (e.g., .pdf).
FileSizeBytes number File size (bytes).
SourceMailEntryId string EntryID of the email to which this attachment belongs.

2.4 Input Example

{
  "EventId": "550e8400-e29b-41d4-a716-446655440000",
  "EventTitle": "Shipment #12345 Update",
  "DashboardTemplateId": "logistics_template_v1",
  "DashboardValues": {
    "HAWB": "H123456789",
    "Status": "In Transit"
  },
  "Emails": [
    {
      "EntryId": "000000001A4473...",
      "Subject": "Re: Shipment #12345",
      "Sender": "agent@logistics.com",
      "ReceivedOn": "2023-10-27T10:30:00"
    }
  ],
  "Attachments": [
    {
      "FileName": "Invoice.pdf",
      "SourceMailEntryId": "000000001A4473..."
    }
  ]
}

3. Global Scripts Mode

When a script is configured as a "Global Script" and executed from the main interface, the structure of the input data differs.

3.1 Identifying Global Mode

Check the IsGlobalExecution field within DashboardValues to determine if it is running in global execution mode.

{
  "EventId": "GLOBAL",
  "EventTitle": "Global Execution",
  "DashboardValues": {
    "IsGlobalExecution": "true",
    "GlobalDataPath": "C:\\Users\\User\\AppData\\Local\\Temp\\osem-global-data-xxxx.json"
  },
  "Emails": [],
  "Attachments": []
}

3.2 Accessing Global Data

In global mode, the actual list of events is stored in another JSON file pointed to by GlobalDataPath. This file contains an array of EventRecord objects.

Important Note: The global data file directly serializes the C# EventRecord object, so its structure is slightly different from the Context in single-event mode and contains more complete information.

Global Data JSON Structure (Array of EventRecord):

[
  {
    "EventId": "550e8400-...",
    "EventTitle": "Shipment #12345",
    "Status": "Open",
    "DashboardTemplateId": "logistics_v1",
    "CreatedOn": "2023-10-01T10:00:00Z",
    "LastUpdatedOn": "2023-10-05T14:30:00Z",
    
    // Note: This is a DashboardItems array, not a DashboardValues dictionary
    "DashboardItems": [
      { "Key": "HAWB", "Value": "H123", "Confidence": 1.0 },
      { "Key": "ETD", "Value": "2023-11-01", "Confidence": 0.8 }
    ],
    
    "Emails": [
      { "EntryId": "...", "Subject": "...", "Sender": "...", "ReceivedOn": "..." }
    ],
    
    "Attachments": [
      { "FileName": "Inv.pdf", "SourceMailEntryId": "..." }
    ],
    
    // Extra fields
    "ConversationIds": [ "..." ],
    "RelatedSubjects": [ "..." ],
    "Participants": [ "..." ]
  },
  ...
]

3.3 Global Script Reading Example

import sys
import json
import os

def get_dashboard_value(event, key):
    """Helper to get value from EventRecord structure"""
    for item in event.get("DashboardItems", []):
        if item.get("Key") == key:
            return item.get("Value")
    return None

def load_data():
    # 1. Read basic Context
    context_path = sys.argv[1]
    with open(context_path, 'r', encoding='utf-8') as f:
        context = json.load(f)

    # 2. Check if it is Global Mode
    dashboard_values = context.get("DashboardValues", {})
    if dashboard_values.get("IsGlobalExecution") == "true":
        # 3. Read global data file
        global_data_path = dashboard_values.get("GlobalDataPath")
        if global_data_path and os.path.exists(global_data_path):
            with open(global_data_path, 'r', encoding='utf-8') as df:
                all_events = json.load(df)
                return "GLOBAL", all_events
    
    # Normal Single Event Mode
    return "SINGLE", context

def main():
    mode, data = load_data()
    
    if mode == "GLOBAL":
        print(f"Processing {len(data)} events in GLOBAL mode.")
        for event in data:
            # Access full data
            title = event.get('EventTitle')
            hawb = get_dashboard_value(event, 'HAWB')
            email_count = len(event.get('Emails', []))
            print(f" - [{hawb}] {title} ({email_count} emails)")
            
    else:
        # Single Event Mode (Context Structure)
        print(f"Processing single event: {data.get('EventTitle')}")
        # In single event mode, access DashboardValues dictionary directly
        vals = data.get('DashboardValues', {})
        print(f" - HAWB: {vals.get('HAWB')}")

if __name__ == "__main__":
    main()

4. Script Behavior Specification

4.1 Data Reading

Scripts should read the file path passed via command line arguments and parse the JSON content.

import sys
import json

def load_context():
    if len(sys.argv) < 2:
        raise ValueError("Missing context file path argument")
    
    context_path = sys.argv[1]
    with open(context_path, 'r', encoding='utf-8') as f:
        return json.load(f)

4.2 Interacting with Outlook (Optional)

If the metadata in JSON is insufficient (e.g., need to read email body or download attachments), you can use the pywin32 library to connect to Outlook via EntryId.

Get Email Body

import win32com.client

def get_mail_body(entry_id):
    outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
    item = outlook.GetItemFromID(entry_id)
    return item.Body

Get Attachment Files

Since attachment content is not directly included in the JSON, you need to find the original email via SourceMailEntryId and download the attachment.

import os

def save_attachments(context, output_dir):
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
    
    # Note: If in global mode, context is a list, requiring a double loop
    # Here assuming context is a single event object
    for att_info in context.get("Attachments", []):
        entry_id = att_info.get("SourceMailEntryId")
        file_name = att_info.get("FileName")
        
        try:
            mail = outlook.GetItemFromID(entry_id)
            for attachment in mail.Attachments:
                if attachment.FileName == file_name:
                    # SaveAsFile requires an absolute path
                    save_path = os.path.abspath(os.path.join(output_dir, file_name))
                    attachment.SaveAsFile(save_path)
                    break
        except Exception as e:
            # Recommended to print logs to stderr to avoid interfering with stdout JSON output
            sys.stderr.write(f"Error saving {file_name}: {e}\n")

4.3 Output Data (Standard Output)

Scripts should print the processing results to standard output (stdout) in JSON format. Recommendation: Print the JSON result only on the last line, and print other log information to standard error (stderr).

Recommended Output Structure: The plugin should be designed to receive update instructions in the following format (requires plugin-side code support):

{
  "updates": {
    "DashboardValues": {
      "ETD": "2023-11-01",
      "Vessel": "EVER GIVEN"
    }
  },
  "logs": [
    "Extracted ETD successfully",
    "Confidence score: 0.98"
  ]
}

4.4 Exit Codes

  • 0: Execution successful.
  • Non-0: Execution failed (the plugin may capture this state and report an error).

5. Development Notes

  1. Encoding: Always use UTF-8 encoding when reading/writing files and processing strings to avoid character encoding issues (especially with non-ASCII characters).
  2. Dependencies: Minimize third-party library dependencies, or ensure the runtime environment has the required libraries installed (e.g., pywin32, ollama, etc.).
  3. Exception Handling: Scripts should include global exception handling to print error information to stderr or include it in a returned JSON error field, preventing silent failures.