Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,16 @@ class BasePlatformAdapter(ABC):
- Sending messages/responses
- Handling media
"""
_MEDIA_EXTENSIONS_RE = (
r"png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|"
r"epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|apk|ipa|kmz|kml|"
r"json|xml|html?|geojson|gpx"
)
_MEDIA_TAG_RE = re.compile(
r'''[`"']?MEDIA:\s*(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|[^\n]+?\.(?:'''
+ _MEDIA_EXTENSIONS_RE
+ r''')(?=[\s`"',;:)\]}]|$)|\S+)[`"']?'''
)

def __init__(self, config: PlatformConfig, platform: Platform):
self.config = config
Expand Down Expand Up @@ -2136,9 +2146,7 @@ def extract_media(content: str) -> Tuple[List[Tuple[str, bool]], str]:

# Extract MEDIA:<path> tags, allowing optional whitespace after the colon
# and quoted/backticked paths for LLM-formatted outputs.
media_pattern = re.compile(
r'''[`"']?MEDIA:\s*(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|apk|ipa)(?=[\s`"',;:)\]}]|$))[`"']?'''
)
media_pattern = BasePlatformAdapter._MEDIA_TAG_RE
for match in media_pattern.finditer(content):
path = match.group("path").strip()
if len(path) >= 2 and path[0] == path[-1] and path[0] in "`\"'":
Expand Down Expand Up @@ -3150,7 +3158,7 @@ async def _stop_typing_task() -> None:
# Strip any remaining internal directives from message body (fixes #1561)
text_content = text_content.replace("[[audio_as_voice]]", "").strip()
text_content = text_content.replace("[[as_document]]", "").strip()
text_content = re.sub(r"MEDIA:\s*\S+", "", text_content).strip()
text_content = BasePlatformAdapter._MEDIA_TAG_RE.sub("", text_content).strip()
if images:
logger.info("[%s] extract_images found %d image(s) in response (%d chars)", self.name, len(images), len(response))

Expand Down
20 changes: 2 additions & 18 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15778,15 +15778,7 @@ def _clarify_callback_sync(question: str, choices) -> str:
if _hm.get("role") in {"tool", "function"}:
_hc = _hm.get("content", "")
if "MEDIA:" in _hc:
_TOOL_MEDIA_RE = re.compile(
r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
r'txt|csv|apk|ipa))',
re.IGNORECASE
)
for _match in _TOOL_MEDIA_RE.finditer(_hc):
_p = _match.group(1).strip().rstrip('",}')
for _p, _ in BasePlatformAdapter.extract_media(_hc)[0]:
if _p:
_history_media_paths.add(_p)

Expand Down Expand Up @@ -16074,15 +16066,7 @@ def _approval_notify_sync(approval_data: dict) -> None:
if msg.get("role") in {"tool", "function"}:
content = msg.get("content", "")
if "MEDIA:" in content:
_TOOL_MEDIA_RE = re.compile(
r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
r'txt|csv|apk|ipa))',
re.IGNORECASE
)
for match in _TOOL_MEDIA_RE.finditer(content):
path = match.group(1).strip().rstrip('",}')
for path, _ in BasePlatformAdapter.extract_media(content)[0]:
if path and path not in _history_media_paths:
media_tags.append(f"MEDIA:{path}")
if "[[audio_as_voice]]" in content:
Expand Down
2 changes: 1 addition & 1 deletion gateway/stream_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ async def run(self) -> None:
# Pattern to strip MEDIA:<path> tags (including optional surrounding quotes).
# Matches the simple cleanup regex used by the non-streaming path in
# gateway/platforms/base.py for post-processing.
_MEDIA_RE = re.compile(r'''[`"']?MEDIA:\s*\S+[`"']?''')
_MEDIA_RE = _BasePlatformAdapter._MEDIA_TAG_RE

@staticmethod
def _clean_for_display(text: str) -> str:
Expand Down
7 changes: 3 additions & 4 deletions mcp_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import json
import logging
import os
import re
import sys
import threading
import time
Expand Down Expand Up @@ -176,9 +175,9 @@ def _extract_attachments(msg: dict) -> List[dict]:
# MEDIA: tags in text content
text = _extract_message_content(msg)
if text:
media_pattern = re.compile(r'MEDIA:\s*(\S+)')
for match in media_pattern.finditer(text):
path = match.group(1)
from gateway.platforms.base import BasePlatformAdapter

for path, _ in BasePlatformAdapter.extract_media(text)[0]:
attachments.append({"type": "media", "path": path})

return attachments
Expand Down
29 changes: 28 additions & 1 deletion tests/gateway/test_platform_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,34 @@ def test_media_tag_supports_unquoted_flac_paths_with_spaces(self):
assert media == [("/tmp/Jane Doe/speech.flac", False)]
assert cleaned == ""

def test_media_tag_supports_windows_drive_paths_with_spaces(self):
content = r"Here is the file MEDIA:C:\Users\Confera\OneDrive\Nusa Alam Kreasindo\Project\Foo\report.pdf for you."
media, cleaned = BasePlatformAdapter.extract_media(content)
assert media == [
(r"C:\Users\Confera\OneDrive\Nusa Alam Kreasindo\Project\Foo\report.pdf", False)
]
assert "MEDIA:" not in cleaned
assert "Alam Kreasindo" not in cleaned
assert "Here is the file" in cleaned
assert "for you." in cleaned

def test_media_tag_supports_unc_paths_with_spaces(self):
content = r"MEDIA:\\fileserver\shared reports\Q2\map export.kml"
media, cleaned = BasePlatformAdapter.extract_media(content)
assert media == [
(r"\\fileserver\shared reports\Q2\map export.kml", False)
]
assert cleaned == ""

def test_media_tag_supports_structured_data_extensions_with_spaces(self):
content = "MEDIA:/home/user/My Folder/coords.kmz\nMEDIA:/home/user/My Folder/schema.geojson"
media, cleaned = BasePlatformAdapter.extract_media(content)
assert media == [
("/home/user/My Folder/coords.kmz", False),
("/home/user/My Folder/schema.geojson", False),
]
assert cleaned == ""

def test_as_document_directive_stripped_from_cleaned_text(self):
"""[[as_document]] is a routing directive — strip it from
user-visible text just like [[audio_as_voice]]. Callers detect the
Expand Down Expand Up @@ -728,4 +756,3 @@ def test_http_proxy_falls_back_without_aiohttp_socks(self):
sess_kw, req_kw = proxy_kwargs_for_aiohttp("http://proxy:8080")
assert sess_kw == {}
assert req_kw == {"proxy": "http://proxy:8080"}

19 changes: 18 additions & 1 deletion tests/gateway/test_stream_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ def test_media_mid_sentence(self):
assert "generated" in result
assert "for you." in result

def test_windows_spaced_media_path_stripped_without_leaking_tail(self):
"""Windows MEDIA paths with spaces should be stripped as one token."""
text = r"Generated MEDIA:C:\Users\Confera\OneDrive\Nusa Alam Kreasindo\Project\Foo\report.pdf for you."
result = GatewayStreamConsumer._clean_for_display(text)
assert "MEDIA:" not in result
assert "Alam Kreasindo" not in result
assert "Generated" in result
assert "for you." in result

def test_structured_data_media_path_with_spaces_stripped(self):
"""GIS / structured-data MEDIA extensions should strip like images."""
text = "Map ready\nMEDIA:/home/user/My Folder/coords.kmz\nDone"
result = GatewayStreamConsumer._clean_for_display(text)
assert "MEDIA:" not in result
assert "My Folder" not in result
assert "Map ready" in result
assert "Done" in result

def test_preserves_non_media_colons(self):
"""Normal colons and text with 'MEDIA' as a word aren't stripped."""
text = "The media: files are stored in /tmp. Use social MEDIA carefully."
Expand Down Expand Up @@ -1780,4 +1798,3 @@ def test_codepoint_only_adapter_falls_back_to_len(self):
# auto-attr mock. Verified indirectly by all the other tests in
# this file passing — they all use MagicMock adapters.
assert consumer is not None

19 changes: 19 additions & 0 deletions tests/test_mcp_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,25 @@ def test_media_tag_in_text(self):
assert len(att) == 1
assert att[0] == {"type": "media", "path": "/tmp/out.png"}

def test_windows_spaced_media_tag_in_text(self):
from mcp_serve import _extract_attachments
msg = {
"content": r"Here MEDIA: C:\Users\Confera\OneDrive\Nusa Alam Kreasindo\Project\Foo\report.pdf done"
}
att = _extract_attachments(msg)
assert len(att) == 1
assert att[0] == {
"type": "media",
"path": r"C:\Users\Confera\OneDrive\Nusa Alam Kreasindo\Project\Foo\report.pdf",
}

def test_structured_data_media_tag_in_text(self):
from mcp_serve import _extract_attachments
msg = {"content": "Here MEDIA: /home/user/My Folder/coords.kmz done"}
att = _extract_attachments(msg)
assert len(att) == 1
assert att[0] == {"type": "media", "path": "/home/user/My Folder/coords.kmz"}

def test_multiple_media_tags(self):
from mcp_serve import _extract_attachments
msg = {"content": "MEDIA: /a.png and MEDIA: /b.mp3"}
Expand Down
6 changes: 6 additions & 0 deletions ui-tui/src/__tests__/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ describe('protocol sentinels', () => {
)
expect('`MEDIA:/tmp/a.png`'.match(MEDIA_LINE_RE)?.[1]).toBe('/tmp/a.png')
expect('"MEDIA:C:\\files\\a.png"'.match(MEDIA_LINE_RE)?.[1]).toBe('C:\\files\\a.png')
expect('MEDIA:C:\\Users\\Confera\\OneDrive\\Nusa Alam Kreasindo\\Project\\Foo\\report.pdf'.match(MEDIA_LINE_RE)?.[1]).toBe(
'C:\\Users\\Confera\\OneDrive\\Nusa Alam Kreasindo\\Project\\Foo\\report.pdf'
)
expect('MEDIA:/home/user/My Folder/coords.kmz'.match(MEDIA_LINE_RE)?.[1]).toBe(
'/home/user/My Folder/coords.kmz'
)
})

it('ignores MEDIA: tokens embedded in prose', () => {
Expand Down
41 changes: 40 additions & 1 deletion ui-tui/src/components/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,46 @@ const MATH_BLOCK_OPEN_RE = /^\s*(\$\$|\\\[)(.*)$/
const MATH_BLOCK_CLOSE_DOLLAR_RE = /^(.*?)\$\$\s*$/
const MATH_BLOCK_CLOSE_BRACKET_RE = /^(.*?)\\\]\s*$/

export const MEDIA_LINE_RE = /^\s*[`"']?MEDIA:\s*(\S+?)[`"']?\s*$/
// Keep in sync with BasePlatformAdapter._MEDIA_EXTENSIONS_RE in
// gateway/platforms/base.py. TypeScript cannot import the Python constant.
export const MEDIA_EXTENSIONS = [
'png',
'jpe?g',
'gif',
'webp',
'mp4',
'mov',
'avi',
'mkv',
'webm',
'ogg',
'opus',
'mp3',
'wav',
'm4a',
'flac',
'epub',
'pdf',
'zip',
'rar',
'7z',
'docx?',
'xlsx?',
'pptx?',
'txt',
'csv',
'apk',
'ipa',
'kmz',
'kml',
'json',
'xml',
'html?',
'geojson',
'gpx',
]
const MEDIA_EXTENSIONS_RE = MEDIA_EXTENSIONS.join('|')
export const MEDIA_LINE_RE = new RegExp(String.raw`^\s*[\`"']?MEDIA:\s*(?=\S)(.+?\.(?:${MEDIA_EXTENSIONS_RE})(?=[\s\`"',;:)\]}]|$)|\S+?)[\`"']?\s*$`)
export const AUDIO_DIRECTIVE_RE = /^\s*\[\[audio_as_voice\]\]\s*$/

// Inline markdown tokens, in priority order. The outer regex picks the
Expand Down
Loading