Skip to content

Commit 5330212

Browse files
zzstoatzzclaude
andauthored
revert: remove snippet upload feature (PRs #1262, #1263, #1265) (#1269)
Reverting the Slack snippet upload feature as it doesn't create proper snippets with line numbers. The Slack API behavior doesn't match documentation. Will revisit later. This reverts: - bec525b fix(slackbot): pass snippet_type to create proper Slack code snippets (#1265) - e3558f9 fix(slackbot): use files.upload for actual Slack snippets (#1263) - 08ee978 feat(slackbot): upload long code blocks as Slack snippets (#1262) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4bb720c commit 5330212

5 files changed

Lines changed: 12 additions & 341 deletions

File tree

examples/slackbot/src/slackbot/_internal/templates.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,13 @@
3434

3535
DEFAULT_SYSTEM_PROMPT = """You are Marvin, an AI assistant for the Prefect data engineering platform. Your responses should be clear, helpful, accurate, and professional. Your primary goal is to provide excellent support to users.
3636
37-
## Output Format
38-
Your responses are structured with interleaved text and code sections. This allows longer code blocks to be displayed as Slack file snippets with proper syntax highlighting.
39-
40-
**For text sections:**
37+
## Output Context
38+
Your responses will be displayed in Slack. Format accordingly:
39+
- Use ``` for code blocks (WITHOUT language identifiers like python/js/etc - Slack doesn't support them)
4140
- Use single backticks for inline code
4241
- Bold text uses *asterisks*
4342
- Links should be formatted as <url|text>
4443
45-
**For code sections:**
46-
- Provide a descriptive title (e.g., "prefect.yaml", "build_bundle.sh", "example usage")
47-
- Specify the language for syntax highlighting (e.g., "python", "yaml", "bash")
48-
- Short code snippets (a few lines) can stay inline in text sections using single backticks
49-
- Longer code examples should be separate code sections - they'll be uploaded as Slack snippets for better readability
50-
51-
**When to use code sections vs inline code:**
52-
- Use a CODE SECTION for: complete files, multi-line examples, configuration blocks, shell scripts
53-
- Use INLINE CODE (in text) for: short one-liners, command names, variable names, brief examples
54-
5544
## Your Mission
5645
Your role is to act as the primary assistant for the user. You will receive raw information from specialized tools. Your job is to synthesize this info as usefully as possible.
5746
Sometimes the information will not be good enough, and you should use the research agent again or ask the user for more information.

examples/slackbot/src/slackbot/api.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from slackbot.assets import summarize_thread
3030
from slackbot.core import (
3131
Database,
32+
UserContext,
3233
build_user_context,
3334
create_agent,
3435
)
@@ -39,10 +40,8 @@
3940
get_channel_name,
4041
get_workspace_domain,
4142
post_slack_message,
42-
post_structured_response,
4343
)
4444
from slackbot.strings import count_tokens, slice_tokens
45-
from slackbot.types import StructuredResponse, UserContext
4645
from slackbot.wrap import WatchToolCalls, _progress_message, _tool_usage_counts
4746

4847
BOT_MENTION = r"<@(\w+)>"
@@ -76,7 +75,7 @@ async def run_agent(
7675
channel_id: str,
7776
thread_ts: str,
7877
decorator_settings: dict[str, Any] | None = None,
79-
) -> AgentRunResult[StructuredResponse]:
78+
) -> AgentRunResult[str]:
8079
if decorator_settings is None:
8180
decorator_settings = {
8281
"cache_policy": NONE,
@@ -267,8 +266,8 @@ async def handle_message(payload: SlackPayload, db: Database):
267266
await db.add_thread_messages(thread_ts, result.new_messages())
268267
conversation.extend(result.new_messages())
269268
assert event.channel is not None, "No channel found"
270-
await post_structured_response(
271-
response=result.output,
269+
await task(post_slack_message)(
270+
message=result.output,
272271
channel_id=event.channel,
273272
thread_ts=thread_ts,
274273
)

examples/slackbot/src/slackbot/core.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
read_github_issues,
4343
)
4444
from slackbot.settings import settings
45-
from slackbot.types import StructuredResponse, UserContext
45+
from slackbot.types import UserContext
4646

4747
GITHUB_API_TOKEN = Secret.load(settings.github_token_secret_name, _sync=True).get()
4848

@@ -162,7 +162,7 @@ def build_user_context(
162162

163163
def create_agent(
164164
model: KnownModelName | Model | None = None,
165-
) -> Agent[UserContext, StructuredResponse]:
165+
) -> Agent[UserContext, str]:
166166
logger = get_run_logger()
167167
logger.info("Creating new agent")
168168
ai_model = model or AnthropicModel(
@@ -174,11 +174,10 @@ def create_agent(
174174
),
175175
)
176176
agent = Agent[
177-
UserContext, StructuredResponse
177+
UserContext, str
178178
](
179179
model=ai_model,
180180
model_settings=ModelSettings(temperature=settings.temperature),
181-
output_type=StructuredResponse,
182181
tools=[
183182
research_prefect_topic, # Tool for researching Prefect topics
184183
read_github_issues, # For searching GitHub issues

examples/slackbot/src/slackbot/slack.py

Lines changed: 1 addition & 260 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
"""Module for Slack-related utilities."""
22

3-
from __future__ import annotations
4-
53
import re
6-
from typing import TYPE_CHECKING, Any, List, Union
7-
8-
if TYPE_CHECKING:
9-
from slackbot.types import StructuredResponse
4+
from typing import Any, List, Union
105

116
import httpx
12-
from prefect.logging.loggers import get_logger
137
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator
148

159
from slackbot.settings import settings
16-
from slackbot.types import SNIPPET_LINE_THRESHOLD
17-
18-
logger = get_logger(__name__)
1910

2011

2112
class EventBlockElement(BaseModel):
@@ -365,253 +356,3 @@ async def create_progress_message(
365356
progress = ProgressMessage(channel_id, thread_ts)
366357
await progress.start(initial_text)
367358
return progress
368-
369-
370-
# --- Slack Snippet Upload (using new API with snippet_type parameter) ---
371-
372-
373-
async def upload_snippet(
374-
content: str,
375-
filename: str,
376-
channel_id: str,
377-
thread_ts: str | None = None,
378-
title: str | None = None,
379-
filetype: str | None = None,
380-
) -> dict[str, Any]:
381-
"""Upload a code snippet using the new external upload API with snippet_type.
382-
383-
The snippet_type parameter tells Slack to create a proper code snippet
384-
(with syntax highlighting and line numbers) rather than a plain file.
385-
386-
Uses the three-step process:
387-
1. files.getUploadURLExternal - get upload URL, passing snippet_type
388-
2. POST content to that URL
389-
3. files.completeUploadExternal - finalize and share in channel
390-
391-
Args:
392-
content: The snippet content to upload
393-
filename: Filename for the snippet (e.g., "example.py")
394-
channel_id: Channel to share the snippet in
395-
thread_ts: Thread timestamp to share in (optional)
396-
title: Display title for the snippet (optional, defaults to filename)
397-
filetype: Syntax type for the snippet (e.g., "python", "javascript", "yaml")
398-
399-
Returns:
400-
dict with file info from Slack API
401-
"""
402-
content_bytes = content.encode("utf-8")
403-
length = len(content_bytes)
404-
405-
async with httpx.AsyncClient() as client:
406-
# Step 1: Get upload URL with snippet_type parameter
407-
# snippet_type is the key - it tells Slack to create a code snippet
408-
# with syntax highlighting and line numbers, not just a plain file
409-
get_url_params: dict[str, Any] = {
410-
"filename": filename,
411-
"length": length,
412-
}
413-
if filetype:
414-
get_url_params["snippet_type"] = filetype
415-
416-
response = await client.get(
417-
"https://slack.com/api/files.getUploadURLExternal",
418-
headers={"Authorization": f"Bearer {settings.slack_api_token}"},
419-
params=get_url_params,
420-
)
421-
response.raise_for_status()
422-
url_data = response.json()
423-
424-
if not url_data.get("ok"):
425-
raise ValueError(
426-
f"Failed to get upload URL: {url_data.get('error', 'unknown error')}"
427-
)
428-
429-
upload_url = url_data["upload_url"]
430-
file_id = url_data["file_id"]
431-
432-
logger.info(f"Got upload URL for file_id={file_id}, snippet_type={filetype}")
433-
434-
# Step 2: Upload content to the provided URL
435-
upload_response = await client.post(
436-
upload_url,
437-
content=content_bytes,
438-
headers={"Content-Type": "application/octet-stream"},
439-
)
440-
if upload_response.status_code != 200:
441-
raise ValueError(
442-
f"Failed to upload file content: {upload_response.status_code}"
443-
)
444-
445-
# Step 3: Complete the upload and share in channel
446-
complete_payload: dict[str, Any] = {
447-
"files": [{"id": file_id, "title": title or filename}],
448-
"channel_id": channel_id,
449-
}
450-
if thread_ts:
451-
complete_payload["thread_ts"] = thread_ts
452-
453-
complete_response = await client.post(
454-
"https://slack.com/api/files.completeUploadExternal",
455-
headers={
456-
"Authorization": f"Bearer {settings.slack_api_token}",
457-
"Content-Type": "application/json",
458-
},
459-
json=complete_payload,
460-
)
461-
complete_response.raise_for_status()
462-
complete_data = complete_response.json()
463-
464-
if not complete_data.get("ok"):
465-
raise ValueError(
466-
f"Failed to complete upload: {complete_data.get('error', 'unknown error')}"
467-
)
468-
469-
# Log the resulting file mode to verify snippet creation
470-
files = complete_data.get("files", [])
471-
if files:
472-
file_info = files[0]
473-
logger.info(
474-
f"Uploaded file - mode: {file_info.get('mode')}, "
475-
f"filetype: {file_info.get('filetype')}, "
476-
f"editable: {file_info.get('editable')}"
477-
)
478-
479-
return complete_data
480-
481-
482-
# Language to file extension mapping for snippet filenames
483-
LANGUAGE_EXTENSIONS: dict[str, str] = {
484-
"python": "py",
485-
"py": "py",
486-
"javascript": "js",
487-
"js": "js",
488-
"typescript": "ts",
489-
"ts": "ts",
490-
"yaml": "yaml",
491-
"yml": "yaml",
492-
"json": "json",
493-
"bash": "sh",
494-
"sh": "sh",
495-
"shell": "sh",
496-
"sql": "sql",
497-
"html": "html",
498-
"css": "css",
499-
"go": "go",
500-
"rust": "rs",
501-
"java": "java",
502-
"c": "c",
503-
"cpp": "cpp",
504-
"c++": "cpp",
505-
"ruby": "rb",
506-
"php": "php",
507-
"swift": "swift",
508-
"kotlin": "kt",
509-
"scala": "scala",
510-
"r": "r",
511-
"dockerfile": "dockerfile",
512-
"docker": "dockerfile",
513-
"toml": "toml",
514-
"ini": "ini",
515-
"xml": "xml",
516-
"markdown": "md",
517-
"md": "md",
518-
"text": "txt",
519-
"txt": "txt",
520-
"plaintext": "txt",
521-
}
522-
523-
524-
def get_extension_for_language(language: str | None) -> str:
525-
"""Get file extension for a language identifier."""
526-
if not language:
527-
return "txt"
528-
return LANGUAGE_EXTENSIONS.get(language.lower(), "txt")
529-
530-
531-
async def post_structured_response(
532-
response: StructuredResponse,
533-
channel_id: str,
534-
thread_ts: str | None = None,
535-
snippet_line_threshold: int = SNIPPET_LINE_THRESHOLD,
536-
) -> None:
537-
"""Post a structured response to Slack, uploading long code blocks as snippets.
538-
539-
This renders the response by:
540-
1. Collecting text and short code into messages
541-
2. Uploading long code blocks as Slack file snippets
542-
3. Maintaining the order of sections for readability
543-
544-
Args:
545-
response: The structured response to post
546-
channel_id: Slack channel ID
547-
thread_ts: Thread timestamp (optional)
548-
snippet_line_threshold: Code blocks with more lines than this are uploaded as snippets
549-
"""
550-
551-
accumulated_text: list[str] = []
552-
snippet_counter = 0
553-
554-
async def flush_text() -> None:
555-
"""Post accumulated text as a message."""
556-
nonlocal accumulated_text
557-
if accumulated_text:
558-
text = "\n\n".join(accumulated_text)
559-
await post_slack_message(
560-
message=text,
561-
channel_id=channel_id,
562-
thread_ts=thread_ts,
563-
)
564-
accumulated_text = []
565-
566-
for section in response.sections:
567-
if section.type == "text":
568-
accumulated_text.append(section.content)
569-
elif section.type == "code":
570-
code_lines = section.content.count("\n") + 1
571-
572-
if code_lines <= snippet_line_threshold:
573-
# Short code: inline it in the text
574-
# Slack doesn't support language identifiers in code blocks
575-
accumulated_text.append(f"```\n{section.content}\n```")
576-
else:
577-
# Long code: flush text first, then upload as snippet
578-
await flush_text()
579-
580-
snippet_counter += 1
581-
ext = get_extension_for_language(section.language)
582-
583-
# Use title as filename only if it looks like a valid filename
584-
# (ends with .ext pattern, no spaces, reasonable length)
585-
title_is_filename = (
586-
section.title
587-
and "." in section.title
588-
and section.title.rsplit(".", 1)[-1].lower()
589-
in LANGUAGE_EXTENSIONS.values()
590-
and " " not in section.title
591-
and len(section.title) < 100
592-
)
593-
filename = (
594-
section.title
595-
if title_is_filename
596-
else f"snippet_{snippet_counter}.{ext}"
597-
)
598-
title = section.title or f"Code snippet {snippet_counter}"
599-
600-
try:
601-
await upload_snippet(
602-
content=section.content,
603-
filename=filename,
604-
channel_id=channel_id,
605-
thread_ts=thread_ts,
606-
title=title,
607-
filetype=section.language,
608-
)
609-
except (httpx.HTTPError, ValueError, KeyError) as e:
610-
# Fallback: post as regular code block if upload fails
611-
logger.warning(
612-
f"Failed to upload snippet '{title}', falling back to inline: {e}"
613-
)
614-
accumulated_text.append(f"*{title}*\n```\n{section.content}\n```")
615-
616-
# Flush any remaining text
617-
await flush_text()

0 commit comments

Comments
 (0)