deploy
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
"language": "python",
|
"language": "python",
|
||||||
"name": "mailu-openapi-agent",
|
"name": "mailu-openapi-agent",
|
||||||
"description": "Mailu administration agent generated from the live Mailu OpenAPI spec.",
|
"description": "Mailu administration agent generated from the live Mailu OpenAPI spec.",
|
||||||
"version": "1.0",
|
"version": "1.2",
|
||||||
"entrypoint": {
|
"entrypoint": {
|
||||||
"module": "agent",
|
"module": "agent",
|
||||||
"class_name": "MailuOpenapiAgent",
|
"class_name": "MailuOpenapiAgent",
|
||||||
@@ -220,6 +220,65 @@
|
|||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "read_sent",
|
||||||
|
"description": "Mint a temporary Mailu token for a mailbox, read sent messages over IMAPS, then delete the token.",
|
||||||
|
"handler": "read_sent",
|
||||||
|
"tags": [
|
||||||
|
"mailbox",
|
||||||
|
"imap",
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"scopes": [],
|
||||||
|
"stream": false,
|
||||||
|
"policy": {
|
||||||
|
"timeout_seconds": 120.0,
|
||||||
|
"idempotent": false,
|
||||||
|
"max_retries": 0,
|
||||||
|
"cost_class": null,
|
||||||
|
"allow_scope_expansion": false,
|
||||||
|
"grant_mode": null,
|
||||||
|
"grant_allow_patterns": [],
|
||||||
|
"grant_deny_patterns": [],
|
||||||
|
"grant_outputs_prefix": null,
|
||||||
|
"grant_write_prefixes": [],
|
||||||
|
"grant_ttl_seconds": null,
|
||||||
|
"grant_run_timeout_seconds": null,
|
||||||
|
"grant_approval_timeout_seconds": null,
|
||||||
|
"grant_scope_approval_timeout_seconds": null
|
||||||
|
},
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mailbox": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"max_body_chars": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"imap_host": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"imap_port": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"additionalProperties": true,
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "mark_email_read",
|
"name": "mark_email_read",
|
||||||
"description": "Mint a temporary Mailu token for a mailbox, mark one IMAP UID as read, then delete the token.",
|
"description": "Mint a temporary Mailu token for a mailbox, mark one IMAP UID as read, then delete the token.",
|
||||||
@@ -2519,7 +2578,7 @@
|
|||||||
"source": "python-a2a-pack",
|
"source": "python-a2a-pack",
|
||||||
"project_manifest": {
|
"project_manifest": {
|
||||||
"name": "mailu-openapi-agent",
|
"name": "mailu-openapi-agent",
|
||||||
"version": "1.0",
|
"version": "1.2",
|
||||||
"entrypoint": "agent:MailuOpenapiAgent"
|
"entrypoint": "agent:MailuOpenapiAgent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ This agent also includes SMTP/IMAP skills:
|
|||||||
|
|
||||||
- `send_email`
|
- `send_email`
|
||||||
- `read_inbox`
|
- `read_inbox`
|
||||||
|
- `read_sent`
|
||||||
- `mark_email_read`
|
- `mark_email_read`
|
||||||
|
|
||||||
These skills take the mailbox email address, mint a temporary Mailu user token
|
These skills take the mailbox email address, mint a temporary Mailu user token
|
||||||
|
|||||||
2
a2a.yaml
2
a2a.yaml
@@ -1,5 +1,5 @@
|
|||||||
name: mailu-openapi-agent
|
name: mailu-openapi-agent
|
||||||
version: '1.1'
|
version: '1.2'
|
||||||
entrypoint: agent:MailuOpenapiAgent
|
entrypoint: agent:MailuOpenapiAgent
|
||||||
description: Mailu administration agent generated from the live Mailu OpenAPI spec.
|
description: Mailu administration agent generated from the live Mailu OpenAPI spec.
|
||||||
runtime:
|
runtime:
|
||||||
|
|||||||
93
agent.py
93
agent.py
@@ -75,6 +75,15 @@ class ReadInboxInput(BaseModel):
|
|||||||
imap_port: int = Field(default=993, description="IMAPS port.")
|
imap_port: int = Field(default=993, description="IMAPS port.")
|
||||||
|
|
||||||
|
|
||||||
|
class ReadSentInput(BaseModel):
|
||||||
|
email: str = Field(description="Mailbox email address to read. A temporary Mailu token is minted internally.")
|
||||||
|
mailbox: str = Field(default="Sent", description="IMAP sent-mail mailbox name. Override if the client uses another folder.")
|
||||||
|
limit: int = Field(default=10, ge=1, le=50, description="Maximum number of newest sent messages to return.")
|
||||||
|
max_body_chars: int = Field(default=4000, ge=0, le=20000, description="Maximum plain text body characters per message.")
|
||||||
|
imap_host: str = Field(default="mail.a2acloud.io", description="IMAPS host.")
|
||||||
|
imap_port: int = Field(default=993, description="IMAPS port.")
|
||||||
|
|
||||||
|
|
||||||
class MarkEmailReadInput(BaseModel):
|
class MarkEmailReadInput(BaseModel):
|
||||||
email: str = Field(description="Mailbox email address. A temporary Mailu token is minted internally.")
|
email: str = Field(description="Mailbox email address. A temporary Mailu token is minted internally.")
|
||||||
uid: str = Field(description="IMAP UID returned by read_inbox.")
|
uid: str = Field(description="IMAP UID returned by read_inbox.")
|
||||||
@@ -86,7 +95,7 @@ class MarkEmailReadInput(BaseModel):
|
|||||||
class MailuOpenapiAgent(A2AAgent):
|
class MailuOpenapiAgent(A2AAgent):
|
||||||
name = "mailu-openapi-agent"
|
name = "mailu-openapi-agent"
|
||||||
description = "Mailu administration agent generated from the live Mailu OpenAPI spec."
|
description = "Mailu administration agent generated from the live Mailu OpenAPI spec."
|
||||||
version = "1.1"
|
version = "1.2"
|
||||||
consumer_setup = ConsumerSetup.from_fields(
|
consumer_setup = ConsumerSetup.from_fields(
|
||||||
ConsumerSetupField.config("OPENAPI_BASE_URL", label="API base URL", description="Override the default API server (https://mail.a2acloud.io/api/v1).", required=False, input_type="url"),
|
ConsumerSetupField.config("OPENAPI_BASE_URL", label="API base URL", description="Override the default API server (https://mail.a2acloud.io/api/v1).", required=False, input_type="url"),
|
||||||
ConsumerSetupField.secret("AUTHORIZATION", label="Bearer API key", description="Sent as header parameter 'Authorization'. Also used to mint temporary per-user Mailu mailbox tokens for SMTP/IMAP skills.", required=True),
|
ConsumerSetupField.secret("AUTHORIZATION", label="Bearer API key", description="Sent as header parameter 'Authorization'. Also used to mint temporary per-user Mailu mailbox tokens for SMTP/IMAP skills.", required=True),
|
||||||
@@ -223,6 +232,36 @@ class MailuOpenapiAgent(A2AAgent):
|
|||||||
imap_port,
|
imap_port,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@skill(
|
||||||
|
name="read_sent",
|
||||||
|
description="Mint a temporary Mailu token for a mailbox, read sent messages over IMAPS, then delete the token.",
|
||||||
|
tags=("mailbox", "imap", "email"),
|
||||||
|
timeout_seconds=120,
|
||||||
|
)
|
||||||
|
async def read_sent(
|
||||||
|
self,
|
||||||
|
ctx: RunContext,
|
||||||
|
email: str,
|
||||||
|
mailbox: str = "Sent",
|
||||||
|
limit: int = 10,
|
||||||
|
max_body_chars: int = 4000,
|
||||||
|
imap_host: str = "mail.a2acloud.io",
|
||||||
|
imap_port: int = 993,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
async with self._temporary_mail_token(ctx, email, "read_sent") as token_info:
|
||||||
|
return await asyncio.to_thread(
|
||||||
|
_read_mailbox_sync,
|
||||||
|
email,
|
||||||
|
token_info["token"],
|
||||||
|
mailbox,
|
||||||
|
limit,
|
||||||
|
"ALL",
|
||||||
|
False,
|
||||||
|
max_body_chars,
|
||||||
|
imap_host,
|
||||||
|
imap_port,
|
||||||
|
)
|
||||||
|
|
||||||
@skill(
|
@skill(
|
||||||
name="mark_email_read",
|
name="mark_email_read",
|
||||||
description="Mint a temporary Mailu token for a mailbox, mark one IMAP UID as read, then delete the token.",
|
description="Mint a temporary Mailu token for a mailbox, mark one IMAP UID as read, then delete the token.",
|
||||||
@@ -1018,6 +1057,24 @@ class MailuOpenapiAgent(A2AAgent):
|
|||||||
imap_port=imap_port,
|
imap_port=imap_port,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def read_sent_tool(
|
||||||
|
email: str,
|
||||||
|
mailbox: str = "Sent",
|
||||||
|
limit: int = 10,
|
||||||
|
max_body_chars: int = 4000,
|
||||||
|
imap_host: str = "mail.a2acloud.io",
|
||||||
|
imap_port: int = 993,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self.read_sent(
|
||||||
|
ctx,
|
||||||
|
email=email,
|
||||||
|
mailbox=mailbox,
|
||||||
|
limit=limit,
|
||||||
|
max_body_chars=max_body_chars,
|
||||||
|
imap_host=imap_host,
|
||||||
|
imap_port=imap_port,
|
||||||
|
)
|
||||||
|
|
||||||
async def mark_email_read_tool(
|
async def mark_email_read_tool(
|
||||||
email: str,
|
email: str,
|
||||||
uid: str,
|
uid: str,
|
||||||
@@ -1047,6 +1104,12 @@ class MailuOpenapiAgent(A2AAgent):
|
|||||||
description="Read Mailu mailbox messages through IMAPS. Provide the mailbox email; the tool mints and deletes a temporary Mailu user token internally.",
|
description="Read Mailu mailbox messages through IMAPS. Provide the mailbox email; the tool mints and deletes a temporary Mailu user token internally.",
|
||||||
args_schema=ReadInboxInput,
|
args_schema=ReadInboxInput,
|
||||||
),
|
),
|
||||||
|
StructuredTool.from_function(
|
||||||
|
coroutine=read_sent_tool,
|
||||||
|
name="read_sent",
|
||||||
|
description="Read Mailu sent-folder messages through IMAPS. Provide the mailbox email; the tool mints and deletes a temporary Mailu user token internally.",
|
||||||
|
args_schema=ReadSentInput,
|
||||||
|
),
|
||||||
StructuredTool.from_function(
|
StructuredTool.from_function(
|
||||||
coroutine=mark_email_read_tool,
|
coroutine=mark_email_read_tool,
|
||||||
name="mark_email_read",
|
name="mark_email_read",
|
||||||
@@ -1083,13 +1146,14 @@ class MailuOpenapiAgent(A2AAgent):
|
|||||||
"You operate an API through generated OpenAPI tools.",
|
"You operate an API through generated OpenAPI tools.",
|
||||||
"Call tools to get real results. Do not invent API responses.",
|
"Call tools to get real results. Do not invent API responses.",
|
||||||
"For write, update, or delete operations, explain the intended action before calling the tool.",
|
"For write, update, or delete operations, explain the intended action before calling the tool.",
|
||||||
"For mailbox tasks, use send_email, read_inbox, and mark_email_read. These tools mint temporary Mailu user tokens internally; never ask the user for or reveal mailbox tokens.",
|
"For mailbox tasks, use send_email, read_inbox, read_sent, and mark_email_read. These tools mint temporary Mailu user tokens internally; never ask the user for or reveal mailbox tokens.",
|
||||||
"If an operation reports missing consumer setup, tell the user which setup field is required.",
|
"If an operation reports missing consumer setup, tell the user which setup field is required.",
|
||||||
"If an operation returns 404, 405, 410, or a schema/validation error that suggests the live API no longer matches these tools, tell the user this generated agent may need to be refreshed from the latest OpenAPI spec and ask whether they want to refresh it.",
|
"If an operation returns 404, 405, 410, or a schema/validation error that suggests the live API no longer matches these tools, tell the user this generated agent may need to be refreshed from the latest OpenAPI spec and ask whether they want to refresh it.",
|
||||||
"",
|
"",
|
||||||
"Mailbox tools:",
|
"Mailbox tools:",
|
||||||
"- send_email: WRITE SMTP submission using a temporary per-user token.",
|
"- send_email: WRITE SMTP submission using a temporary per-user token.",
|
||||||
"- read_inbox: READ IMAPS mailbox messages using a temporary per-user token.",
|
"- read_inbox: READ IMAPS mailbox messages using a temporary per-user token.",
|
||||||
|
"- read_sent: READ sent-folder messages using a temporary per-user token.",
|
||||||
"- mark_email_read: WRITE IMAP seen flag using a temporary per-user token.",
|
"- mark_email_read: WRITE IMAP seen flag using a temporary per-user token.",
|
||||||
"",
|
"",
|
||||||
"Available operations:",
|
"Available operations:",
|
||||||
@@ -1374,6 +1438,30 @@ def _read_inbox_sync(
|
|||||||
max_body_chars: int,
|
max_body_chars: int,
|
||||||
imap_host: str,
|
imap_host: str,
|
||||||
imap_port: int,
|
imap_port: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _read_mailbox_sync(
|
||||||
|
email_address,
|
||||||
|
token,
|
||||||
|
mailbox,
|
||||||
|
limit,
|
||||||
|
"UNSEEN" if unread_only else "ALL",
|
||||||
|
mark_seen,
|
||||||
|
max_body_chars,
|
||||||
|
imap_host,
|
||||||
|
imap_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_mailbox_sync(
|
||||||
|
email_address: str,
|
||||||
|
token: str,
|
||||||
|
mailbox: str,
|
||||||
|
limit: int,
|
||||||
|
criteria: str,
|
||||||
|
mark_seen: bool,
|
||||||
|
max_body_chars: int,
|
||||||
|
imap_host: str,
|
||||||
|
imap_port: int,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
limit = max(1, min(limit, 50))
|
limit = max(1, min(limit, 50))
|
||||||
messages: list[dict[str, Any]] = []
|
messages: list[dict[str, Any]] = []
|
||||||
@@ -1382,7 +1470,6 @@ def _read_inbox_sync(
|
|||||||
status, _ = imap.select(mailbox, readonly=not mark_seen)
|
status, _ = imap.select(mailbox, readonly=not mark_seen)
|
||||||
if status != "OK":
|
if status != "OK":
|
||||||
return {"ok": False, "error": "mailbox_select_failed", "mailbox": mailbox}
|
return {"ok": False, "error": "mailbox_select_failed", "mailbox": mailbox}
|
||||||
criteria = "UNSEEN" if unread_only else "ALL"
|
|
||||||
status, data = imap.uid("search", None, criteria)
|
status, data = imap.uid("search", None, criteria)
|
||||||
if status != "OK":
|
if status != "OK":
|
||||||
return {"ok": False, "error": "search_failed", "mailbox": mailbox}
|
return {"ok": False, "error": "search_failed", "mailbox": mailbox}
|
||||||
|
|||||||
Reference in New Issue
Block a user