mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-10-13 14:28:00 +00:00
feat: add attachment reader/writer with RFC and download all media types
This commit is contained in:
parent
8041c085d2
commit
00b6657185
4 changed files with 181 additions and 18 deletions
40
plugins/_whatsapp_integration/helpers/attachment_reader.py
Normal file
40
plugins/_whatsapp_integration/helpers/attachment_reader.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""
|
||||
Read attachment files from execution runtime.
|
||||
|
||||
No agent/tool dependencies.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import os
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Data models
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class AttachmentData(TypedDict):
|
||||
name: str
|
||||
content_b64: str
|
||||
error: str
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# File reader
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def read_attachment(path: str) -> AttachmentData:
|
||||
try:
|
||||
if not os.path.isfile(path):
|
||||
return AttachmentData(
|
||||
name="", content_b64="", error=f"file not found: {path}")
|
||||
name = os.path.basename(path)
|
||||
with open(path, "rb") as f:
|
||||
content = f.read()
|
||||
return AttachmentData(
|
||||
name=name,
|
||||
content_b64=base64.b64encode(content).decode(),
|
||||
error="",
|
||||
)
|
||||
except Exception as e:
|
||||
return AttachmentData(name="", content_b64="", error=str(e))
|
||||
35
plugins/_whatsapp_integration/helpers/attachment_writer.py
Normal file
35
plugins/_whatsapp_integration/helpers/attachment_writer.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""
|
||||
Write attachment files into execution runtime.
|
||||
|
||||
No agent/tool dependencies.
|
||||
"""
|
||||
|
||||
import base64
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Data models
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class WriteResult(TypedDict):
|
||||
path: str
|
||||
error: str
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# File writer
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def write_attachment(rel_path: str, content_b64: str) -> WriteResult:
|
||||
try:
|
||||
from helpers import files
|
||||
abs_path = files.get_abs_path(rel_path)
|
||||
files.make_dirs(abs_path)
|
||||
|
||||
content = base64.b64decode(content_b64)
|
||||
files.write_file_bin(abs_path, content)
|
||||
|
||||
return WriteResult(path=abs_path, error="")
|
||||
except Exception as e:
|
||||
return WriteResult(path="", error=str(e))
|
||||
|
|
@ -5,10 +5,12 @@ Requires agent context.
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from agent import Agent, AgentContext, UserMessage
|
||||
from helpers import plugins
|
||||
from helpers import plugins, files, runtime
|
||||
from helpers import message_queue as mq
|
||||
from helpers.persist_chat import save_tmp_chat
|
||||
from helpers.print_style import PrintStyle
|
||||
|
|
@ -126,7 +128,8 @@ async def _start_new_chat(config: dict, msg: dict) -> None:
|
|||
system_ctx = context.agent0.read_prompt("fw.wa.system_context.md")
|
||||
|
||||
msg_id = str(uuid.uuid4())
|
||||
attachments = msg.get("mediaUrls", [])
|
||||
media_urls = msg.get("mediaUrls", [])
|
||||
attachments = await _save_incoming_media(media_urls) if media_urls else []
|
||||
mq.log_user_message(
|
||||
context, user_msg, attachments, message_id=msg_id, source=" (whatsapp)",
|
||||
)
|
||||
|
|
@ -154,7 +157,8 @@ async def _route_to_chat(
|
|||
|
||||
user_msg = _build_user_message(context.agent0, msg, config)
|
||||
msg_id = str(uuid.uuid4())
|
||||
attachments = msg.get("mediaUrls", [])
|
||||
media_urls = msg.get("mediaUrls", [])
|
||||
attachments = await _save_incoming_media(media_urls) if media_urls else []
|
||||
mq.log_user_message(
|
||||
context, user_msg, attachments, message_id=msg_id, source=" (whatsapp)",
|
||||
)
|
||||
|
|
@ -244,12 +248,13 @@ async def send_wa_reply(
|
|||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
# Send attachments
|
||||
# Send attachments via RFC (files may live in execution runtime)
|
||||
if attachments:
|
||||
for file_path in attachments:
|
||||
host_paths = await _read_attachments_to_host(attachments)
|
||||
for host_path in host_paths:
|
||||
try:
|
||||
result = await wa_client.send_media(
|
||||
base_url, chat_id, file_path,
|
||||
base_url, chat_id, host_path,
|
||||
)
|
||||
if result.get("error"):
|
||||
PrintStyle.warning(f"WhatsApp: attachment error: {result['error']}")
|
||||
|
|
@ -257,3 +262,55 @@ async def send_wa_reply(
|
|||
PrintStyle.warning(f"WhatsApp: attachment error: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Attachment reading (via RFC into execution runtime)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def _read_attachments_to_host(
|
||||
paths: list[str],
|
||||
) -> list[str]:
|
||||
"""Read files from execution runtime and write to host media cache."""
|
||||
from plugins._whatsapp_integration.helpers.attachment_reader import read_attachment
|
||||
|
||||
host_paths: list[str] = []
|
||||
for path in paths:
|
||||
data = await runtime.call_development_function(read_attachment, path)
|
||||
if data["error"]:
|
||||
PrintStyle.warning(f"WhatsApp attachment: {data['error']}")
|
||||
continue
|
||||
# Write decoded bytes to host-side media cache
|
||||
host_path = os.path.join(
|
||||
files.get_abs_path(MEDIA_FOLDER), data["name"],
|
||||
)
|
||||
os.makedirs(os.path.dirname(host_path), exist_ok=True)
|
||||
with open(host_path, "wb") as f:
|
||||
f.write(base64.b64decode(data["content_b64"]))
|
||||
host_paths.append(host_path)
|
||||
return host_paths
|
||||
|
||||
|
||||
async def _save_incoming_media(
|
||||
media_urls: list[str],
|
||||
) -> list[str]:
|
||||
"""Save incoming media files into execution runtime via RFC."""
|
||||
from plugins._whatsapp_integration.helpers.attachment_writer import write_attachment
|
||||
|
||||
runtime_paths: list[str] = []
|
||||
for host_path in media_urls:
|
||||
if not os.path.isfile(host_path):
|
||||
continue
|
||||
name = os.path.basename(host_path)
|
||||
with open(host_path, "rb") as f:
|
||||
content_b64 = base64.b64encode(f.read()).decode()
|
||||
rel_path = os.path.join(MEDIA_FOLDER, name)
|
||||
result = await runtime.call_development_function(
|
||||
write_attachment, rel_path, content_b64,
|
||||
)
|
||||
if result.get("error"):
|
||||
PrintStyle.warning(f"WhatsApp media save: {result['error']}")
|
||||
runtime_paths.append(host_path) # fallback to host path
|
||||
else:
|
||||
runtime_paths.append(result["path"])
|
||||
return runtime_paths
|
||||
|
|
|
|||
|
|
@ -217,17 +217,6 @@ async function startSocket() {
|
|||
body = msg.message.imageMessage.caption || '';
|
||||
hasMedia = true;
|
||||
mediaType = 'image';
|
||||
try {
|
||||
const buf = await downloadMediaMessage(msg, 'buffer', {}, { logger, reuploadRequest: sock.updateMediaMessage });
|
||||
const mime = msg.message.imageMessage.mimetype || 'image/jpeg';
|
||||
const extMap = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp', 'image/gif': '.gif' };
|
||||
const ext = extMap[mime] || '.jpg';
|
||||
const filePath = path.join(CACHE_DIR, `img_${randomBytes(6).toString('hex')}${ext}`);
|
||||
writeFileSync(filePath, buf);
|
||||
mediaUrls.push(filePath);
|
||||
} catch (err) {
|
||||
console.error('[bridge] Failed to download image:', err.message);
|
||||
}
|
||||
} else if (msg.message.videoMessage) {
|
||||
body = msg.message.videoMessage.caption || '';
|
||||
hasMedia = true;
|
||||
|
|
@ -236,11 +225,53 @@ async function startSocket() {
|
|||
hasMedia = true;
|
||||
mediaType = msg.message.pttMessage ? 'ptt' : 'audio';
|
||||
} else if (msg.message.documentMessage) {
|
||||
body = msg.message.documentMessage.caption || msg.message.documentMessage.fileName || '';
|
||||
body = msg.message.documentMessage.caption || '';
|
||||
hasMedia = true;
|
||||
mediaType = 'document';
|
||||
}
|
||||
|
||||
// Download media to disk
|
||||
if (hasMedia) {
|
||||
try {
|
||||
const buf = await downloadMediaMessage(msg, 'buffer', {}, { logger, reuploadRequest: sock.updateMediaMessage });
|
||||
let ext = '.bin';
|
||||
let prefix = mediaType;
|
||||
if (mediaType === 'image') {
|
||||
const mime = msg.message.imageMessage?.mimetype || 'image/jpeg';
|
||||
const extMap = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp', 'image/gif': '.gif' };
|
||||
ext = extMap[mime] || '.jpg';
|
||||
} else if (mediaType === 'video') {
|
||||
const mime = msg.message.videoMessage?.mimetype || 'video/mp4';
|
||||
ext = mime.includes('mp4') ? '.mp4' : '.mkv';
|
||||
} else if (mediaType === 'audio' || mediaType === 'ptt') {
|
||||
const mime = msg.message.audioMessage?.mimetype || msg.message.pttMessage?.mimetype || 'audio/ogg';
|
||||
ext = mime.includes('opus') || mime.includes('ogg') ? '.ogg' : '.mp3';
|
||||
} else if (mediaType === 'document') {
|
||||
const docMsg = msg.message.documentMessage;
|
||||
const fileName = docMsg?.fileName || '';
|
||||
if (fileName) {
|
||||
// Use original filename for documents
|
||||
const filePath = path.join(CACHE_DIR, `${randomBytes(4).toString('hex')}_${fileName}`);
|
||||
writeFileSync(filePath, buf);
|
||||
mediaUrls.push(filePath);
|
||||
if (!body) body = fileName;
|
||||
} else {
|
||||
const mime = docMsg?.mimetype || 'application/octet-stream';
|
||||
const docExtMap = { 'application/pdf': '.pdf', 'application/msword': '.doc' };
|
||||
ext = docExtMap[mime] || '.bin';
|
||||
}
|
||||
}
|
||||
// Write file if not already handled (document with fileName)
|
||||
if (mediaUrls.length === 0) {
|
||||
const filePath = path.join(CACHE_DIR, `${prefix}_${randomBytes(6).toString('hex')}${ext}`);
|
||||
writeFileSync(filePath, buf);
|
||||
mediaUrls.push(filePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[bridge] Failed to download ${mediaType}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// For media without caption, use a placeholder
|
||||
if (hasMedia && !body) {
|
||||
body = `[${mediaType} received]`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue