Skip to content
Closed
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
6 changes: 3 additions & 3 deletions gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2413,7 +2413,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`"',;:)\]}]|$))[`"']?'''
r'''[`"']?MEDIA:\s*(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/|[A-Za-z]:[\\/])\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`"',;:)\]}]|$))[`"']?'''
)
for match in media_pattern.finditer(content):
path = match.group("path").strip()
Expand Down Expand Up @@ -2477,9 +2477,9 @@ def extract_local_files(content: str) -> Tuple[List[str], str]:

# (?<![/:\w.]) prevents matching inside URLs (e.g. https://…/img.png)
# and relative paths (./foo.png)
# (?:~/|/) anchors to absolute or home-relative paths
# (?:~/|/|[A-Za-z]:[\\/]) anchors to absolute, home-relative, or Windows drive paths
path_re = re.compile(
r'(?<![/:\w.])(?:~/|/)(?:[\w.\-]+/)*[\w.\-]+\.(?:' + ext_part + r')\b',
r'(?<![/:\w.])(?:~/|/|[A-Za-z]:[\\/])(?:[\w.\-]+[\\/])*[\w.\-]+\.(?:' + ext_part + r')\b',
re.IGNORECASE,
)

Expand Down
18 changes: 15 additions & 3 deletions tests/gateway/test_extract_local_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ def test_tilde_path_image(self):
assert paths == ["/home/user/photos/cat.jpg"]
assert "~/photos/cat.jpg" not in cleaned

def test_windows_drive_path(self):
paths, cleaned = _extract("Report at D:/hermes-workspace/report.txt attached")
assert paths == ["D:/hermes-workspace/report.txt"]
assert "D:/hermes-workspace/report.txt" not in cleaned
assert "Report at" in cleaned

def test_windows_backslash_path(self):
text = r"Report at D:\hermes-workspace\report.txt attached"
paths, cleaned = _extract(text)
assert paths == [r"D:\hermes-workspace\report.txt"]
assert r"D:\hermes-workspace\report.txt" not in cleaned

def test_video_extensions(self):
for ext in (".mp4", ".mov", ".avi", ".mkv", ".webm"):
text = f"Video at /tmp/clip{ext} here"
Expand Down Expand Up @@ -337,10 +349,10 @@ def test_path_with_spaces_not_matched(self):
paths, _ = _extract("File at /tmp/my file.png here")
assert paths == []

def test_windows_path_not_matched(self):
"""Windows-style paths should not match."""
def test_windows_path_matched(self):
"""Windows-style drive paths are supported for Windows hosts."""
paths, _ = _extract("See C:\\Users\\test\\image.png")
assert paths == []
assert paths == ["C:\\Users\\test\\image.png"]

def test_relative_path_not_matched(self):
"""Relative paths like ./image.png should not match."""
Expand Down
13 changes: 13 additions & 0 deletions tests/gateway/test_platform_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,19 @@ 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(self):
content = "Report attached\nMEDIA:D:/hermes-workspace/report.txt"
media, cleaned = BasePlatformAdapter.extract_media(content)
assert media == [("D:/hermes-workspace/report.txt", False)]
assert "MEDIA:" not in cleaned
assert "Report attached" in cleaned

def test_media_tag_supports_windows_backslash_paths(self):
content = r"MEDIA:D:\hermes-workspace\report.txt"
media, cleaned = BasePlatformAdapter.extract_media(content)
assert media == [(r"D:\hermes-workspace\report.txt", 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