openmail 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openmail/__init__.py +6 -0
- openmail/assistants/__init__.py +35 -0
- openmail/assistants/classify_emails.py +83 -0
- openmail/assistants/compose_email.py +43 -0
- openmail/assistants/detect_phishing_for_email.py +61 -0
- openmail/assistants/evaluate_sender_trust_for_email.py +59 -0
- openmail/assistants/extract_tasks_from_emails.py +126 -0
- openmail/assistants/generate_follow_up_for_email.py +54 -0
- openmail/assistants/natural_language_query.py +699 -0
- openmail/assistants/prioritize_emails.py +89 -0
- openmail/assistants/reply.py +58 -0
- openmail/assistants/reply_suggestions.py +46 -0
- openmail/assistants/rewrite_email.py +50 -0
- openmail/assistants/summarize_attachments_for_email.py +101 -0
- openmail/assistants/summarize_thread_emails.py +55 -0
- openmail/assistants/summary.py +44 -0
- openmail/assistants/summary_multi.py +57 -0
- openmail/assistants/translate_email.py +54 -0
- openmail/auth/__init__.py +6 -0
- openmail/auth/base.py +34 -0
- openmail/auth/no_auth.py +19 -0
- openmail/auth/oauth2.py +58 -0
- openmail/auth/password.py +26 -0
- openmail/config.py +26 -0
- openmail/email_assistant.py +418 -0
- openmail/email_manager.py +777 -0
- openmail/email_query.py +279 -0
- openmail/errors.py +16 -0
- openmail/imap/__init__.py +5 -0
- openmail/imap/attachment_parts.py +55 -0
- openmail/imap/bodystructure.py +296 -0
- openmail/imap/client.py +806 -0
- openmail/imap/fetch_response.py +115 -0
- openmail/imap/inline_cid.py +106 -0
- openmail/imap/pagination.py +16 -0
- openmail/imap/parser.py +298 -0
- openmail/imap/query.py +233 -0
- openmail/llm/__init__.py +3 -0
- openmail/llm/claude.py +35 -0
- openmail/llm/costs.py +108 -0
- openmail/llm/gemini.py +34 -0
- openmail/llm/gpt.py +33 -0
- openmail/llm/groq.py +36 -0
- openmail/llm/model.py +126 -0
- openmail/llm/xai.py +35 -0
- openmail/logger.py +20 -0
- openmail/models/__init__.py +20 -0
- openmail/models/attachment.py +128 -0
- openmail/models/message.py +113 -0
- openmail/models/subscription.py +45 -0
- openmail/models/task.py +24 -0
- openmail/py.typed +0 -0
- openmail/smtp/__init__.py +7 -0
- openmail/smtp/builder.py +41 -0
- openmail/smtp/client.py +218 -0
- openmail/smtp/templates.py +16 -0
- openmail/subscription/__init__.py +7 -0
- openmail/subscription/detector.py +58 -0
- openmail/subscription/parser.py +32 -0
- openmail/subscription/service.py +237 -0
- openmail/types.py +30 -0
- openmail/utils/__init__.py +39 -0
- openmail/utils/utils.py +295 -0
- openmail-0.1.5.dist-info/METADATA +180 -0
- openmail-0.1.5.dist-info/RECORD +67 -0
- openmail-0.1.5.dist-info/WHEEL +4 -0
- openmail-0.1.5.dist-info/licenses/LICENSE +21 -0
openmail/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from openmail.config import IMAPConfig, SMTPConfig
|
|
2
|
+
from openmail.email_assistant import EmailAssistant
|
|
3
|
+
from openmail.email_manager import EmailManager
|
|
4
|
+
from openmail.email_query import EmailQuery
|
|
5
|
+
|
|
6
|
+
__all__ = ["EmailManager", "SMTPConfig", "IMAPConfig", "EmailQuery", "EmailAssistant"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from openmail.assistants.classify_emails import llm_classify_emails
|
|
2
|
+
from openmail.assistants.compose_email import llm_compose_email
|
|
3
|
+
from openmail.assistants.detect_phishing_for_email import llm_detect_phishing_for_email
|
|
4
|
+
from openmail.assistants.evaluate_sender_trust_for_email import llm_evaluate_sender_trust_for_email
|
|
5
|
+
from openmail.assistants.extract_tasks_from_emails import llm_extract_tasks_from_emails
|
|
6
|
+
from openmail.assistants.generate_follow_up_for_email import llm_generate_follow_up_for_email
|
|
7
|
+
from openmail.assistants.natural_language_query import llm_easy_imap_query_from_nl
|
|
8
|
+
from openmail.assistants.prioritize_emails import llm_prioritize_emails
|
|
9
|
+
from openmail.assistants.reply import llm_concise_reply_for_email
|
|
10
|
+
from openmail.assistants.reply_suggestions import llm_reply_suggestions_for_email
|
|
11
|
+
from openmail.assistants.rewrite_email import llm_rewrite_email
|
|
12
|
+
from openmail.assistants.summarize_attachments_for_email import llm_summarize_attachments_for_email
|
|
13
|
+
from openmail.assistants.summarize_thread_emails import llm_summarize_thread_emails
|
|
14
|
+
from openmail.assistants.summary import llm_summarize_single_email
|
|
15
|
+
from openmail.assistants.summary_multi import llm_summarize_many_emails
|
|
16
|
+
from openmail.assistants.translate_email import llm_translate_email
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"llm_concise_reply_for_email",
|
|
20
|
+
"llm_summarize_single_email",
|
|
21
|
+
"llm_summarize_many_emails",
|
|
22
|
+
"llm_easy_imap_query_from_nl",
|
|
23
|
+
"llm_reply_suggestions_for_email",
|
|
24
|
+
"llm_classify_emails",
|
|
25
|
+
"llm_detect_phishing_for_email",
|
|
26
|
+
"llm_evaluate_sender_trust_for_email",
|
|
27
|
+
"llm_generate_follow_up_for_email",
|
|
28
|
+
"llm_prioritize_emails",
|
|
29
|
+
"llm_summarize_thread_emails",
|
|
30
|
+
"llm_compose_email",
|
|
31
|
+
"llm_rewrite_email",
|
|
32
|
+
"llm_translate_email",
|
|
33
|
+
"llm_extract_tasks_from_emails",
|
|
34
|
+
"llm_summarize_attachments_for_email",
|
|
35
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
CLASSIFY_EMAILS_PROMPT = """
|
|
12
|
+
You are an assistant that classifies emails into one of a small set of classes.
|
|
13
|
+
|
|
14
|
+
Instructions:
|
|
15
|
+
- You will be given multiple emails, each with an Email ID.
|
|
16
|
+
- For each email, choose exactly ONE class from the allowed list.
|
|
17
|
+
- For the output, return a list of objects, each with:
|
|
18
|
+
- id: the Email ID (copied exactly as given)
|
|
19
|
+
- label: the chosen class
|
|
20
|
+
- Do not invent new class names; only use the provided classes.
|
|
21
|
+
|
|
22
|
+
Allowed classes:
|
|
23
|
+
{classes}
|
|
24
|
+
|
|
25
|
+
Emails:
|
|
26
|
+
{email_blocks}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EmailClassificationItem(BaseModel):
|
|
31
|
+
id: str = Field(description="Opaque ID that identifies one email.")
|
|
32
|
+
label: str = Field(description="Chosen class label for this email.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EmailClassificationSchema(BaseModel):
|
|
36
|
+
items: List[EmailClassificationItem] = Field(
|
|
37
|
+
description=(
|
|
38
|
+
"List of classification results. Each item contains the email id "
|
|
39
|
+
"and the chosen class label."
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def llm_classify_emails(
|
|
45
|
+
messages: Sequence[EmailMessage],
|
|
46
|
+
*,
|
|
47
|
+
classes: Sequence[str],
|
|
48
|
+
provider: str,
|
|
49
|
+
model_name: str,
|
|
50
|
+
) -> Tuple[Dict[EmailMessage, str], Dict[str, Any]]:
|
|
51
|
+
"""
|
|
52
|
+
Classify a batch of emails into one of the provided classes.
|
|
53
|
+
"""
|
|
54
|
+
if not messages:
|
|
55
|
+
return [], {}
|
|
56
|
+
|
|
57
|
+
chain = get_model(provider, model_name, EmailClassificationSchema)
|
|
58
|
+
|
|
59
|
+
id_list = [f"e{i + 1}" for i in range(len(messages))]
|
|
60
|
+
id_to_index = {id_: i for i, id_ in enumerate(id_list)}
|
|
61
|
+
|
|
62
|
+
classes_str = ", ".join(classes)
|
|
63
|
+
|
|
64
|
+
email_blocks = "\n\n".join(
|
|
65
|
+
f"Email ID: {email_id}\n{build_email_context(msg)}"
|
|
66
|
+
for email_id, msg in zip(id_list, messages)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
result, llm_call_info = chain(
|
|
70
|
+
CLASSIFY_EMAILS_PROMPT.format(
|
|
71
|
+
classes=classes_str,
|
|
72
|
+
email_blocks=email_blocks,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# preserve order of input messages
|
|
77
|
+
labels: List[str] = [""] * len(messages)
|
|
78
|
+
for item in result.items:
|
|
79
|
+
idx = id_to_index.get(item.id)
|
|
80
|
+
if idx is not None:
|
|
81
|
+
labels[idx] = item.label
|
|
82
|
+
|
|
83
|
+
return labels, llm_call_info
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
|
|
9
|
+
COMPOSE_EMAIL_PROMPT = """
|
|
10
|
+
You are an assistant that drafts professional emails.
|
|
11
|
+
|
|
12
|
+
Instructions:
|
|
13
|
+
- Use clear, concise, professional language unless told otherwise.
|
|
14
|
+
- Provide both a subject line and a body.
|
|
15
|
+
- Do not include quotes or commentary about the email.
|
|
16
|
+
- The body should be ready to send as-is (no placeholders like [NAME]).
|
|
17
|
+
- Avoid overly flowery language.
|
|
18
|
+
|
|
19
|
+
User instructions:
|
|
20
|
+
{instructions}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ComposeEmailSchema(BaseModel):
|
|
25
|
+
subject: str = Field(description="Subject line for the email.")
|
|
26
|
+
body: str = Field(description="Full email body, ready to send.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def llm_compose_email(
|
|
30
|
+
instructions: str,
|
|
31
|
+
*,
|
|
32
|
+
provider: str,
|
|
33
|
+
model_name: str,
|
|
34
|
+
) -> Tuple[str, str, dict[str, Any]]:
|
|
35
|
+
"""
|
|
36
|
+
Compose a new email from natural-language instructions.
|
|
37
|
+
"""
|
|
38
|
+
chain = get_model(provider, model_name, ComposeEmailSchema)
|
|
39
|
+
result, llm_call_info = chain(COMPOSE_EMAIL_PROMPT.format(instructions=instructions))
|
|
40
|
+
|
|
41
|
+
subject = result.subject.strip()
|
|
42
|
+
body = result.body.strip()
|
|
43
|
+
return subject, body, llm_call_info
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
PHISHING_DETECTION_PROMPT = """
|
|
12
|
+
You are a security assistant that determines whether an email is likely to be a phishing or scam attempt.
|
|
13
|
+
|
|
14
|
+
Consider:
|
|
15
|
+
- Suspicious links or requests (passwords, bank details, crypto, gift cards, etc.).
|
|
16
|
+
- Urgency or threats designed to pressure the user.
|
|
17
|
+
- Mismatched or spoofed sender names/domains.
|
|
18
|
+
- Poor grammar or odd phrasing typical of scams.
|
|
19
|
+
- Unsolicited attachments or requests to open files.
|
|
20
|
+
|
|
21
|
+
Instructions:
|
|
22
|
+
- Set is_phishing = true if the email is likely to be phishing or a scam.
|
|
23
|
+
- Otherwise, set is_phishing = false.
|
|
24
|
+
- Provide a confidence score between 0.0 and 1.0.
|
|
25
|
+
- Briefly explain your reasoning referencing specific aspects of the email.
|
|
26
|
+
|
|
27
|
+
Email context:
|
|
28
|
+
{email_context}
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PhishingDetectionSchema(BaseModel):
|
|
33
|
+
is_phishing: bool = Field(
|
|
34
|
+
description="True if the email is likely a phishing/scam attempt, else False."
|
|
35
|
+
)
|
|
36
|
+
confidence: float = Field(
|
|
37
|
+
ge=0.0,
|
|
38
|
+
le=1.0,
|
|
39
|
+
description="Confidence in the phishing judgement, between 0.0 and 1.0.",
|
|
40
|
+
)
|
|
41
|
+
reasoning: str = Field(description="Short explanation for why the email is or is not phishing.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def llm_detect_phishing_for_email(
|
|
45
|
+
msg: EmailMessage,
|
|
46
|
+
*,
|
|
47
|
+
provider: str,
|
|
48
|
+
model_name: str,
|
|
49
|
+
) -> Tuple[bool, dict[str, Any]]:
|
|
50
|
+
"""
|
|
51
|
+
Detect whether an email is likely to be a phishing attempt.
|
|
52
|
+
"""
|
|
53
|
+
chain = get_model(provider, model_name, PhishingDetectionSchema)
|
|
54
|
+
email_context = build_email_context(msg)
|
|
55
|
+
|
|
56
|
+
prompt = PHISHING_DETECTION_PROMPT.format(
|
|
57
|
+
email_context=email_context,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
result, llm_call_info = chain(prompt)
|
|
61
|
+
return result.is_phishing, llm_call_info
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
SENDER_TRUST_PROMPT = """
|
|
12
|
+
You are an assistant that estimates how trustworthy the sender of an email appears.
|
|
13
|
+
|
|
14
|
+
Consider:
|
|
15
|
+
- Sender address and domain (well-known company, free email provider, random domain, etc.).
|
|
16
|
+
- Consistency between display name and email address.
|
|
17
|
+
- Professionalism and clarity of the writing.
|
|
18
|
+
- Whether the content matches what you would expect from a legit sender.
|
|
19
|
+
- Obvious red flags (phishing patterns, strange requests, etc.).
|
|
20
|
+
|
|
21
|
+
Instructions:
|
|
22
|
+
- Output a trust_score between 0.0 and 1.0:
|
|
23
|
+
- 1.0 = highly trustworthy (e.g., known and legitimate sender, no red flags).
|
|
24
|
+
- 0.0 = highly untrustworthy (e.g., obvious scam).
|
|
25
|
+
- Base your judgement entirely on the email context provided.
|
|
26
|
+
- Briefly explain your reasoning.
|
|
27
|
+
|
|
28
|
+
Email context:
|
|
29
|
+
{email_context}
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SenderTrustSchema(BaseModel):
|
|
34
|
+
trust_score: float = Field(
|
|
35
|
+
ge=0.0,
|
|
36
|
+
le=1.0,
|
|
37
|
+
description="How trustworthy the sender appears, between 0.0 (untrustworthy) and 1.0 (highly trustworthy).",
|
|
38
|
+
)
|
|
39
|
+
reasoning: str = Field(description="Short explanation for the chosen trust_score.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def llm_evaluate_sender_trust_for_email(
|
|
43
|
+
msg: EmailMessage,
|
|
44
|
+
*,
|
|
45
|
+
provider: str,
|
|
46
|
+
model_name: str,
|
|
47
|
+
) -> Tuple[float, dict[str, Any]]:
|
|
48
|
+
"""
|
|
49
|
+
Evaluate how trustworthy the sender appears based on the email.
|
|
50
|
+
"""
|
|
51
|
+
chain = get_model(provider, model_name, SenderTrustSchema)
|
|
52
|
+
email_context = build_email_context(msg)
|
|
53
|
+
|
|
54
|
+
prompt = SENDER_TRUST_PROMPT.format(
|
|
55
|
+
email_context=email_context,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
result, llm_call_info = chain(prompt)
|
|
59
|
+
return result.trust_score, llm_call_info
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, List, Optional, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage, Task
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
TASK_EXTRACTION_PROMPT = """
|
|
12
|
+
You extract only important actionable tasks from one or more emails.
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
- Be selective. Prefer missing minor tasks over adding noise.
|
|
16
|
+
- Return at most 3 tasks per email.
|
|
17
|
+
- Ignore emails without clearly actionable requests.
|
|
18
|
+
- If no important tasks exist, return an empty list.
|
|
19
|
+
- Do NOT invent tasks.
|
|
20
|
+
|
|
21
|
+
Important tasks:
|
|
22
|
+
- Explicit or implied requests for the recipient to act.
|
|
23
|
+
- Deliverables, follow-ups, decisions, or deadlines.
|
|
24
|
+
- Items with urgency or due dates.
|
|
25
|
+
|
|
26
|
+
Exclude:
|
|
27
|
+
- Promotions, marketing, newsletters.
|
|
28
|
+
- FYI/announcements with no request.
|
|
29
|
+
- Low-importance suggestions or optional ideas.
|
|
30
|
+
- Automated notifications with no follow-up needed.
|
|
31
|
+
|
|
32
|
+
Task fields:
|
|
33
|
+
- Capture due dates and assignees if stated or clearly implied.
|
|
34
|
+
- Set priority only if urgency is explicit.
|
|
35
|
+
- Status is usually "todo".
|
|
36
|
+
|
|
37
|
+
Email context:
|
|
38
|
+
{email_context}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MetadataItem(BaseModel):
|
|
43
|
+
key: str = Field(description="Metadata key.")
|
|
44
|
+
value: str = Field(description="Metadata value.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TaskSchema(BaseModel):
|
|
48
|
+
"""
|
|
49
|
+
Generic task structure that can be reused across domains.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
id: Optional[str] = Field(
|
|
53
|
+
default=None, description="A stable identifier if available; otherwise null."
|
|
54
|
+
)
|
|
55
|
+
title: str = Field(description="Short, human-readable label for the task.")
|
|
56
|
+
description: str = Field(description="Longer description with relevant context for the task.")
|
|
57
|
+
due_date: Optional[str] = Field(
|
|
58
|
+
default=None,
|
|
59
|
+
description="Due date or deadline in ISO 8601 format if specified; otherwise null.",
|
|
60
|
+
)
|
|
61
|
+
priority: Optional[str] = Field(
|
|
62
|
+
default=None, description='Priority such as "low", "medium", or "high", if inferable.'
|
|
63
|
+
)
|
|
64
|
+
status: Optional[str] = Field(
|
|
65
|
+
default=None, description='Status such as "todo", "in_progress", or "done"; usually "todo".'
|
|
66
|
+
)
|
|
67
|
+
assignee: Optional[str] = Field(
|
|
68
|
+
default=None, description="Person responsible for the task if known."
|
|
69
|
+
)
|
|
70
|
+
tags: List[str] = Field(
|
|
71
|
+
default_factory=list, description="List of keywords or labels for the task."
|
|
72
|
+
)
|
|
73
|
+
source_system: str = Field(description='Source system for the task, e.g. "email".')
|
|
74
|
+
source_id: Optional[str] = Field(
|
|
75
|
+
default=None, description="Identifier of the source record (e.g. message ID) if available."
|
|
76
|
+
)
|
|
77
|
+
source_link: Optional[str] = Field(
|
|
78
|
+
default=None, description="Deep link/URL to the source record if available."
|
|
79
|
+
)
|
|
80
|
+
metadata: List[MetadataItem] = Field(
|
|
81
|
+
default_factory=list, description="Additional domain-specific metadata as key-value pairs."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TaskExtractionSchema(BaseModel):
|
|
86
|
+
tasks: List[TaskSchema] = Field(description="List of tasks extracted from the email context.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def llm_extract_tasks_from_emails(
|
|
90
|
+
messages: Sequence[EmailMessage],
|
|
91
|
+
*,
|
|
92
|
+
provider: str,
|
|
93
|
+
model_name: str,
|
|
94
|
+
) -> Tuple[List[Task], dict[str, Any]]:
|
|
95
|
+
"""
|
|
96
|
+
Extract tasks from one or more emails using a generic task structure.
|
|
97
|
+
"""
|
|
98
|
+
parts: List[str] = []
|
|
99
|
+
for idx, msg in enumerate(messages, start=1):
|
|
100
|
+
ctx = build_email_context(msg)
|
|
101
|
+
parts.append(f"--- Email #{idx} ---\n{ctx}\n")
|
|
102
|
+
|
|
103
|
+
email_context = "\n".join(parts)
|
|
104
|
+
|
|
105
|
+
chain = get_model(provider, model_name, TaskExtractionSchema)
|
|
106
|
+
result, llm_call_info = chain(TASK_EXTRACTION_PROMPT.format(email_context=email_context))
|
|
107
|
+
|
|
108
|
+
tasks: List[Task] = []
|
|
109
|
+
for t in result.tasks:
|
|
110
|
+
tasks.append(
|
|
111
|
+
Task(
|
|
112
|
+
id=t.id,
|
|
113
|
+
title=t.title,
|
|
114
|
+
description=t.description,
|
|
115
|
+
due_date=t.due_date,
|
|
116
|
+
priority=t.priority,
|
|
117
|
+
status=t.status,
|
|
118
|
+
assignee=t.assignee,
|
|
119
|
+
tags=t.tags,
|
|
120
|
+
source_system=t.source_system,
|
|
121
|
+
source_id=t.source_id,
|
|
122
|
+
source_link=t.source_link,
|
|
123
|
+
metadata={item.key: item.value for item in t.metadata},
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return tasks, llm_call_info
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Tuple
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openmail.llm import get_model
|
|
8
|
+
from openmail.models import EmailMessage
|
|
9
|
+
from openmail.utils import build_email_context
|
|
10
|
+
|
|
11
|
+
FOLLOW_UP_PROMPT = """
|
|
12
|
+
You are an assistant that writes a polite, concise follow-up email.
|
|
13
|
+
|
|
14
|
+
Context:
|
|
15
|
+
- The user previously sent or received the email shown below.
|
|
16
|
+
- They now want to send a follow-up because there has been no response or progress.
|
|
17
|
+
|
|
18
|
+
Instructions:
|
|
19
|
+
- Write a short, natural follow-up email body.
|
|
20
|
+
- Keep it polite and professional.
|
|
21
|
+
- Briefly reference the previous message.
|
|
22
|
+
- Ask for an update or next steps.
|
|
23
|
+
- Do NOT include a subject line.
|
|
24
|
+
- Do NOT add salutations like "From:". Only include the email body content.
|
|
25
|
+
|
|
26
|
+
Email context:
|
|
27
|
+
{email_context}
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FollowUpEmailSchema(BaseModel):
|
|
32
|
+
follow_up_body: str = Field(
|
|
33
|
+
description="A concise, polite follow-up email body text with no subject line."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def llm_generate_follow_up_for_email(
|
|
38
|
+
msg: EmailMessage,
|
|
39
|
+
*,
|
|
40
|
+
provider: str,
|
|
41
|
+
model_name: str,
|
|
42
|
+
) -> Tuple[str, dict[str, Any]]:
|
|
43
|
+
"""
|
|
44
|
+
Generate a follow-up email body for a previous message.
|
|
45
|
+
"""
|
|
46
|
+
chain = get_model(provider, model_name, FollowUpEmailSchema)
|
|
47
|
+
email_context = build_email_context(msg)
|
|
48
|
+
|
|
49
|
+
prompt = FOLLOW_UP_PROMPT.format(
|
|
50
|
+
email_context=email_context,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
result, llm_call_info = chain(prompt)
|
|
54
|
+
return result.follow_up_body, llm_call_info
|