From 8c49ce21e01ee84cd15d062e86a597d6d497e106 Mon Sep 17 00:00:00 2001 From: Steve W Date: Thu, 9 Apr 2026 18:32:32 +0000 Subject: [PATCH] Accept native Outlook message shape in classifier request --- README.md | 57 +++++++++++++++++++++++++++++++++++---------------- app/models.py | 47 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c483937..b5195a0 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,11 @@ export EMAIL_CLASSIFIER_DB_PATH=.data/email_classifier.db ## Input shape -Designed around real Outlook message payloads. Relevant fields: +The request model accepts either: +- simplified input via `email_data` +- or native Outlook-style fields directly + +Full Outlook-shaped example: ```json { @@ -42,38 +46,55 @@ Designed around real Outlook message payloads. Relevant fields: "conversationId": "AAQk...", "subject": "MB Printer", "bodyPreview": "Good morning, ...", + "body": { + "contentType": "html", + "content": "...(full HTML body)" + }, + "sender": { + "emailAddress": { + "name": "Bobbi Johnson", + "address": "bobbi.johnson@grandportage.com" + } + }, + "from": { + "emailAddress": { + "name": "Bobbi Johnson", + "address": "bobbi.johnson@grandportage.com" + } + }, + "toRecipients": [ + { + "emailAddress": { + "name": "IT Helpdesk Mail", + "address": "helpdeskmail@grandportage.com" + } + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], "receivedDateTime": "2026-02-19T15:27:35Z", "sentDateTime": "2026-02-19T15:27:32Z", "hasAttachments": false, "importance": "normal", "isRead": false, - "body": { - "contentType": "html", - "content": "..." - } + "flag": { "flagStatus": "notFlagged" }, + "provider": "anthropic", + "base_url": "https://api.minimax.io/anthropic", + "model": "MiniMax-M2.7" } ``` -API request example: +Simplified request example: ```json { - "id": "AAMk...", - "internetMessageId": "<...@...>", - "conversationId": "AAQk...", - "bodyPreview": "Good morning, ...", - "receivedDateTime": "2026-02-19T15:27:35Z", - "sentDateTime": "2026-02-19T15:27:32Z", - "hasAttachments": false, - "importance": "normal", - "isRead": false, "email_data": { "subject": "MB Printer", "body": "..." }, - "provider": "anthropic", - "base_url": "https://api.minimax.io/anthropic", - "model": "MiniMax-M2.7" + "id": "AAMk...", + "conversationId": "AAQk..." } ``` diff --git a/app/models.py b/app/models.py index 6c5c0b0..e3b1a44 100644 --- a/app/models.py +++ b/app/models.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator class EmailData(BaseModel): @@ -10,8 +10,26 @@ class EmailData(BaseModel): body: str +class EmailAddress(BaseModel): + name: str | None = None + address: str | None = None + + +class Recipient(BaseModel): + emailAddress: EmailAddress | None = None + + +class EmailBody(BaseModel): + contentType: str | None = None + content: str | None = None + + +class Flag(BaseModel): + flagStatus: str | None = None + + class ClassifyRequest(BaseModel): - email_data: EmailData + email_data: EmailData | None = None provider: Literal["openai", "anthropic"] | None = None model: str | None = None base_url: str | None = None @@ -21,14 +39,39 @@ class ClassifyRequest(BaseModel): id: str | None = None internetMessageId: str | None = None conversationId: str | None = None + subject: str | None = None bodyPreview: str | None = None + body: EmailBody | None = None + sender: Recipient | None = None + from_: Recipient | None = Field(default=None, alias="from") + toRecipients: list[Recipient] = Field(default_factory=list) + ccRecipients: list[Recipient] = Field(default_factory=list) + bccRecipients: list[Recipient] = Field(default_factory=list) + replyTo: list[Recipient] = Field(default_factory=list) receivedDateTime: str | None = None sentDateTime: str | None = None hasAttachments: bool | None = None importance: str | None = None isRead: bool | None = None + flag: Flag | None = None from_address: str | None = None + @model_validator(mode="after") + def populate_email_data(self) -> "ClassifyRequest": + subject = self.email_data.subject if self.email_data else self.subject + body = self.email_data.body if self.email_data else (self.body.content if self.body and self.body.content else None) + if not subject or not body: + raise ValueError("Request must include either email_data or Outlook subject/body.content fields") + self.email_data = EmailData(subject=subject, body=body) + if not self.from_address: + self.from_address = ( + (self.from_.emailAddress.address if self.from_ and self.from_.emailAddress else None) + or (self.sender.emailAddress.address if self.sender and self.sender.emailAddress else None) + ) + return self + + model_config = {"populate_by_name": True} + class ClassificationDetails(BaseModel): summary: str | None = None