- AgentDefinition gains base_url and workspace_path fields (optional) - load_agent_registry parses them from matrix-agents.yaml - _build_platform_from_env uses agent.base_url per agent (falls back to AGENT_BASE_URL) - _agent_workspace_root() resolves workspace per agent from registry - _materialize_incoming_attachments saves files to agent workspace_path/incoming/ - send_outgoing accepts workspace_root param; reads outgoing files from agent workspace_path - dispatch loop computes workspace_root from room agent_id and passes to _send_all - config/matrix-agents.yaml and example updated with base_url and workspace_path
124 lines
3.8 KiB
Python
124 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import mimetypes
|
|
import re
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
from core.protocol import Attachment
|
|
|
|
|
|
def _sanitize_component(value: str) -> str:
|
|
cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", value)
|
|
cleaned = cleaned.strip("._-")
|
|
return cleaned or "unknown"
|
|
|
|
|
|
def _default_filename(attachment: Attachment) -> str:
|
|
if attachment.filename:
|
|
return attachment.filename
|
|
|
|
extension = mimetypes.guess_extension(attachment.mime_type or "") or ""
|
|
base = {
|
|
"image": "image",
|
|
"audio": "audio",
|
|
"video": "video",
|
|
"document": "attachment",
|
|
}.get(attachment.type, "attachment")
|
|
return f"{base}{extension}"
|
|
|
|
|
|
def build_workspace_attachment_path(
|
|
*,
|
|
workspace_root: Path,
|
|
matrix_user_id: str,
|
|
room_id: str,
|
|
filename: str,
|
|
timestamp: str | None = None,
|
|
) -> tuple[str, Path]:
|
|
"""Legacy path builder used when no per-agent workspace_path is configured."""
|
|
stamp = timestamp or datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
|
safe_user = _sanitize_component(matrix_user_id.lstrip("@"))
|
|
safe_room = _sanitize_component(room_id.lstrip("!"))
|
|
safe_name = _sanitize_component(filename) or "attachment.bin"
|
|
relative_path = (
|
|
Path("surfaces") / "matrix" / safe_user / safe_room / "inbox" / f"{stamp}-{safe_name}"
|
|
)
|
|
return relative_path.as_posix(), workspace_root / relative_path
|
|
|
|
|
|
def build_agent_incoming_path(
|
|
*,
|
|
workspace_root: Path,
|
|
filename: str,
|
|
timestamp: str | None = None,
|
|
) -> tuple[str, Path]:
|
|
"""Per-agent path builder: saves to {workspace_root}/incoming/{stamp}-{filename}.
|
|
The returned relative path is what gets passed to agent.send_message(attachments=[...]).
|
|
"""
|
|
stamp = timestamp or datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
|
safe_name = _sanitize_component(filename) or "attachment.bin"
|
|
relative_path = Path("incoming") / f"{stamp}-{safe_name}"
|
|
return relative_path.as_posix(), workspace_root / relative_path
|
|
|
|
|
|
async def download_matrix_attachment(
|
|
*,
|
|
client,
|
|
workspace_root: Path,
|
|
matrix_user_id: str,
|
|
room_id: str,
|
|
attachment: Attachment,
|
|
timestamp: str | None = None,
|
|
) -> Attachment:
|
|
if not attachment.url:
|
|
return attachment
|
|
|
|
filename = _default_filename(attachment)
|
|
|
|
if workspace_root.name and str(workspace_root) not in (".", "/workspace", "/agents"):
|
|
# Per-agent workspace configured — use simple incoming/ layout
|
|
relative_path, absolute_path = build_agent_incoming_path(
|
|
workspace_root=workspace_root,
|
|
filename=filename,
|
|
timestamp=timestamp,
|
|
)
|
|
else:
|
|
relative_path, absolute_path = build_workspace_attachment_path(
|
|
workspace_root=workspace_root,
|
|
matrix_user_id=matrix_user_id,
|
|
room_id=room_id,
|
|
filename=filename,
|
|
timestamp=timestamp,
|
|
)
|
|
|
|
absolute_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
response = await client.download(attachment.url)
|
|
body = getattr(response, "body", None)
|
|
if body is None:
|
|
raise RuntimeError(f"Matrix download response for {attachment.url} has no body")
|
|
absolute_path.write_bytes(body)
|
|
|
|
return Attachment(
|
|
type=attachment.type,
|
|
url=attachment.url,
|
|
filename=filename,
|
|
mime_type=attachment.mime_type,
|
|
workspace_path=relative_path,
|
|
)
|
|
|
|
|
|
def resolve_workspace_attachment_path(workspace_root: Path, workspace_path: str) -> Path:
|
|
path = Path(workspace_path)
|
|
if path.is_absolute():
|
|
return path
|
|
return workspace_root / path
|
|
|
|
|
|
def matrix_msgtype_for_attachment(attachment: Attachment) -> str:
|
|
return {
|
|
"image": "m.image",
|
|
"audio": "m.audio",
|
|
"video": "m.video",
|
|
}.get(attachment.type, "m.file")
|