mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-06 17:02:14 +00:00
feat: add Slack image support with url_private auth and content-type validation [closes: OPE-23] (#1073)
* feat: add Slack image support with url_private auth and content-type validation * fix: simplify Slack image fetch by removing manual redirect logic * fix: use urlparse hostname check to resolve CodeQL URL sanitization warning * Update agent/utils/multimodal.py Co-authored-by: Brace Sproul <braceasproul@gmail.com> * fix: simplify host matching conditions in multimodal image fetching * reverting changes --------- Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
parent
46d7ed9d43
commit
f79e824d8e
2 changed files with 41 additions and 2 deletions
|
|
@ -8,6 +8,7 @@ import mimetypes
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from langchain_core.messages.content import create_image_block
|
from langchain_core.messages.content import create_image_block
|
||||||
|
|
@ -44,7 +45,8 @@ async def fetch_image_block(
|
||||||
try:
|
try:
|
||||||
logger.debug("Fetching image from %s", image_url)
|
logger.debug("Fetching image from %s", image_url)
|
||||||
headers = None
|
headers = None
|
||||||
if "uploads.linear.app" in image_url:
|
host = (urlparse(image_url).hostname or "").lower()
|
||||||
|
if host == "uploads.linear.app" or host.endswith(".uploads.linear.app"):
|
||||||
linear_api_key = os.environ.get("LINEAR_API_KEY", "")
|
linear_api_key = os.environ.get("LINEAR_API_KEY", "")
|
||||||
if linear_api_key:
|
if linear_api_key:
|
||||||
headers = {"Authorization": linear_api_key}
|
headers = {"Authorization": linear_api_key}
|
||||||
|
|
@ -53,7 +55,16 @@ async def fetch_image_block(
|
||||||
"LINEAR_API_KEY not set; cannot authenticate image fetch for %s",
|
"LINEAR_API_KEY not set; cannot authenticate image fetch for %s",
|
||||||
image_url,
|
image_url,
|
||||||
)
|
)
|
||||||
response = await client.get(image_url, headers=headers)
|
elif host == "files.slack.com" or host.endswith(".files.slack.com"):
|
||||||
|
slack_bot_token = os.environ.get("SLACK_BOT_TOKEN", "")
|
||||||
|
if slack_bot_token:
|
||||||
|
headers = {"Authorization": f"Bearer {slack_bot_token}"}
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"SLACK_BOT_TOKEN not set; cannot authenticate image fetch for %s",
|
||||||
|
image_url,
|
||||||
|
)
|
||||||
|
response = await client.get(image_url, headers=headers, follow_redirects=True)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
content_type = response.headers.get("Content-Type", "").split(";")[0].strip()
|
content_type = response.headers.get("Content-Type", "").split(";")[0].strip()
|
||||||
if not content_type:
|
if not content_type:
|
||||||
|
|
@ -66,6 +77,15 @@ async def fetch_image_block(
|
||||||
return None
|
return None
|
||||||
content_type = guessed
|
content_type = guessed
|
||||||
|
|
||||||
|
supported_types = {"image/jpeg", "image/png", "image/gif", "image/webp"}
|
||||||
|
if content_type not in supported_types:
|
||||||
|
logger.warning(
|
||||||
|
"Unsupported content type '%s' for %s; skipping image",
|
||||||
|
content_type,
|
||||||
|
image_url,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
encoded = base64.b64encode(response.content).decode("ascii")
|
encoded = base64.b64encode(response.content).decode("ascii")
|
||||||
logger.info(
|
logger.info(
|
||||||
"Fetched image %s (%s, %d bytes)",
|
"Fetched image %s (%s, %d bytes)",
|
||||||
|
|
|
||||||
|
|
@ -771,6 +771,25 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
|
||||||
)
|
)
|
||||||
content_blocks: list[dict[str, Any]] = [create_text_block(prompt)]
|
content_blocks: list[dict[str, Any]] = [create_text_block(prompt)]
|
||||||
|
|
||||||
|
image_urls = dedupe_urls(
|
||||||
|
[url for msg in context_messages for url in extract_image_urls(msg.get("text", ""))]
|
||||||
|
+ [
|
||||||
|
f["url_private"]
|
||||||
|
for msg in context_messages
|
||||||
|
for f in msg.get("files", [])
|
||||||
|
if isinstance(f, dict)
|
||||||
|
and f.get("mimetype", "").startswith("image/")
|
||||||
|
and f.get("url_private")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if image_urls:
|
||||||
|
logger.info("Preparing %d image(s) for Slack mention", len(image_urls))
|
||||||
|
async with httpx.AsyncClient() as http_client:
|
||||||
|
for image_url in image_urls:
|
||||||
|
image_block = await fetch_image_block(image_url, http_client)
|
||||||
|
if image_block:
|
||||||
|
content_blocks.append(image_block)
|
||||||
|
|
||||||
configurable: dict[str, Any] = {
|
configurable: dict[str, Any] = {
|
||||||
"repo": repo_config,
|
"repo": repo_config,
|
||||||
"slack_thread": {
|
"slack_thread": {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue