Skip to content

Commit c52ff20

Browse files
zzstoatzzclaude
andauthored
Fix slackbot GitHub discussions search and turbopuffer authentication (#1195)
* Fix slackbot GitHub discussions search and turbopuffer authentication - Refactor GitHub functionality into organized module structure - Fix turbopuffer API key authentication via environment variable - Use GraphQL API for GitHub discussions search (REST endpoint doesn't exist) - Fix GitHubUser model handling in format_discussions_summary - Improve error handling to prevent stack traces leaking to Slack - Skip Prefect task wrapping for internal pydantic-ai tools - Add admin notifications for discussion creation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Address PR review feedback - Define named constants for body truncation lengths (BODY_TRUNCATE_LENGTH, BODY_DISPLAY_LENGTH) - Replace magic numbers with named constants for better maintainability --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e673843 commit c52ff20

12 files changed

Lines changed: 877 additions & 145 deletions

File tree

examples/slackbot/src/slackbot/api.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,33 @@ async def handle_message(payload: SlackPayload, db: Database):
141141
bot_id=bot_user_id or "unknown",
142142
)
143143

144-
result = await run_agent(
145-
cleaned_message, conversation, user_context, event.channel, thread_ts
146-
) # type: ignore
147-
148-
await db.add_thread_messages(thread_ts, result.new_messages())
149-
conversation.extend(result.new_messages())
150-
assert event.channel is not None, "No channel found"
151-
await task(post_slack_message)(
152-
message=result.output,
153-
channel_id=event.channel,
154-
thread_ts=thread_ts,
155-
)
144+
try:
145+
result = await run_agent(
146+
cleaned_message, conversation, user_context, event.channel, thread_ts
147+
) # type: ignore
148+
149+
await db.add_thread_messages(thread_ts, result.new_messages())
150+
conversation.extend(result.new_messages())
151+
assert event.channel is not None, "No channel found"
152+
await task(post_slack_message)(
153+
message=result.output,
154+
channel_id=event.channel,
155+
thread_ts=thread_ts,
156+
)
157+
except Exception as e:
158+
logger.error(f"Error running agent: {e}")
159+
assert event.channel is not None, "No channel found"
160+
await task(post_slack_message)(
161+
message="Sorry, I encountered an error while processing your request. Please try again.",
162+
channel_id=event.channel,
163+
thread_ts=thread_ts,
164+
)
165+
# Still return completed so we don't retry
166+
return Completed(
167+
message="Error during agent execution",
168+
name="ERROR_HANDLED",
169+
data=dict(error=str(e), user_context=user_context),
170+
)
156171
return Completed(
157172
message="Responded to mention",
158173
data=dict(user_context=user_context, conversation=conversation),

examples/slackbot/src/slackbot/core.py

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pathlib import Path
77
from typing import AsyncIterator
88

9+
import httpx
910
from prefect import get_run_logger, task
1011
from prefect.blocks.system import Secret
1112
from prefect.logging.loggers import get_logger
@@ -20,6 +21,15 @@
2021
from turbopuffer import NotFoundError
2122

2223
from slackbot.assets import store_user_facts
24+
from slackbot.github import (
25+
GitHubAuthError,
26+
GitHubError,
27+
GitHubNotFoundError,
28+
GitHubRateLimitError,
29+
create_discussion_from_thread,
30+
format_discussions_summary,
31+
search_discussions,
32+
)
2333
from slackbot.research_agent import (
2434
research_prefect_topic,
2535
)
@@ -73,12 +83,19 @@
7383
7484
1. **For Technical/Conceptual Questions:** Use `research_prefect_topic`. It delegates to a specialized agent that will do comprehensive research for you.
7585
2. **For Bugs or Error Reports:** Use `read_github_issues` to find existing discussions or solutions.
76-
3. **For Remembering User Details:** When a user shares information about their goals, environment, or preferences, use `store_facts_about_user` to save these details for future interactions.
77-
4. **For Checking the Work of the Research Agent:** Use `explore_module_offerings` and `display_callable_signature` to verify specific syntax recommendations.
78-
5. **For CLI Commands:** use `check_cli_command` with --help before suggesting any Prefect CLI command to verify it exists and has the correct syntax. This prevents suggesting non-existent commands.
86+
3. **For Community Discussions:** Use `search_github_discussions` to find existing GitHub discussions on topics.
87+
4. **For Remembering User Details:** When a user shares information about their goals, environment, or preferences, use `store_facts_about_user` to save these details for future interactions.
88+
5. **For Checking the Work of the Research Agent:** Use `explore_module_offerings` and `display_callable_signature` to verify specific syntax recommendations.
89+
6. **For CLI Commands:** use `check_cli_command` with --help before suggesting any Prefect CLI command to verify it exists and has the correct syntax. This prevents suggesting non-existent commands.
7990
- **IMPORTANT:** When checking commands that require optional dependencies (e.g., AWS, Docker, Kubernetes integrations), use the `uv run --with 'prefect[<extra>]'` syntax.
8091
- Examples: `uv run --with 'prefect[aws]'`, `uv run --with 'prefect[docker]'`, `uv run --with 'prefect[kubernetes]'`
8192
- This ensures the command runs with the necessary dependencies installed.
93+
7. **For Creating GitHub Discussions (USE SPARINGLY):** Use `create_discussion_and_notify` only when:
94+
- The thread contains valuable insights, solutions, or patterns not documented elsewhere
95+
- You've searched both issues and discussions and found no existing coverage of the topic
96+
- The conversation would clearly benefit the broader Prefect community
97+
- The thread has reached a meaningful conclusion or solution
98+
- **NEVER** create discussions for simple Q&A that's already well-documented
8299
"""
83100

84101

@@ -255,4 +272,140 @@ def delete_facts_about_user(ctx: RunContext[UserContext], related_to: str) -> st
255272
print(message)
256273
return message
257274

275+
@agent.tool
276+
async def create_discussion_and_notify(
277+
ctx: RunContext[UserContext],
278+
title: str,
279+
summary: str,
280+
repo: str = "prefecthq/prefect",
281+
) -> str:
282+
"""
283+
Create a GitHub discussion from a Slack thread and notify admin.
284+
285+
Use this SPARINGLY and only when:
286+
1. The thread contains valuable insights or solutions not found elsewhere
287+
2. You've searched discussions and found no existing similar topic
288+
3. The conversation would benefit the broader Prefect community
289+
290+
Args:
291+
title: Clear, descriptive title for the discussion
292+
summary: Comprehensive summary synthesizing the key insights from the thread
293+
repo: Repository to create discussion in (default: prefecthq/prefect)
294+
"""
295+
print(f"Creating discussion: {title}")
296+
297+
result = await create_discussion_from_thread(ctx, title, summary, repo)
298+
299+
if settings.admin_slack_user_id:
300+
try:
301+
await _notify_admin_about_discussion(ctx, title, result)
302+
except (httpx.RequestError, httpx.HTTPStatusError) as e:
303+
print(f"Failed to notify admin via Slack: {e}")
304+
except Exception as e:
305+
print(f"Unexpected error during admin notification: {e}")
306+
307+
return result
308+
309+
@agent.tool
310+
async def search_github_discussions(
311+
ctx: RunContext[UserContext],
312+
query: str,
313+
repo: str = "prefecthq/prefect",
314+
n: int = 5,
315+
) -> str:
316+
"""
317+
Search for GitHub discussions in a repository. Call this ONCE per search query.
318+
319+
Use this to find existing discussions before creating new ones.
320+
321+
IMPORTANT: This searches ALL discussions for your query terms.
322+
Call it ONCE and review the results. Do NOT call repeatedly with the same query.
323+
If no results are found, that means there are no matching discussions.
324+
325+
Args:
326+
query: Search terms for discussions (e.g. "redis", "deployment", "workers")
327+
repo: Repository to search (default: prefecthq/prefect)
328+
n: Number of results to return (default: 5)
329+
"""
330+
try:
331+
discussions = await search_discussions(query, repo=repo, n=n)
332+
return await format_discussions_summary(discussions)
333+
except GitHubNotFoundError:
334+
return "Sorry, I couldn't find any discussions. The repository might not have discussions enabled."
335+
except GitHubAuthError:
336+
await _notify_admin_about_error(
337+
ctx, "GitHub authentication failed while searching discussions"
338+
)
339+
return f"Sorry, I'm having trouble accessing GitHub right now. <@{settings.admin_slack_user_id}> has been notified."
340+
except GitHubRateLimitError:
341+
return "Sorry, I've hit GitHub's rate limit. Please try again in a few minutes."
342+
except GitHubError as e:
343+
await _notify_admin_about_error(
344+
ctx, f"GitHub API error while searching discussions: {str(e)}"
345+
)
346+
return f"Sorry, I encountered an error while searching discussions. <@{settings.admin_slack_user_id}> has been notified."
347+
except Exception as e:
348+
import traceback
349+
350+
error_details = traceback.format_exc()
351+
await _notify_admin_about_error(
352+
ctx,
353+
f"Unexpected error in search_github_discussions: {str(e)}\n{error_details}",
354+
)
355+
return f"Error searching discussions: {str(e)}"
356+
258357
return agent
358+
359+
360+
async def _notify_admin_about_discussion(
361+
ctx: RunContext[UserContext], title: str, creation_result: str
362+
) -> None:
363+
"""Send a notification to the admin about the created discussion."""
364+
thread_link = f"https://{ctx.deps['workspace_name']}.slack.com/archives/{ctx.deps['channel_id']}/p{ctx.deps['thread_ts'].replace('.', '')}"
365+
366+
message = (
367+
f"🤖 Marvin created a GitHub discussion:\n"
368+
f"*{title}*\n\n"
369+
f"{creation_result}\n\n"
370+
f"Original thread: {thread_link}"
371+
)
372+
373+
await _send_admin_notification(message)
374+
375+
376+
async def _notify_admin_about_error(
377+
ctx: RunContext[UserContext], error_message: str
378+
) -> None:
379+
"""Send a notification to the admin about an error."""
380+
if not settings.admin_slack_user_id:
381+
return # No admin configured
382+
383+
thread_link = f"https://{ctx.deps['workspace_name']}.slack.com/archives/{ctx.deps['channel_id']}/p{ctx.deps['thread_ts'].replace('.', '')}"
384+
385+
message = (
386+
f"🚨 Marvin encountered an error:\n"
387+
f"*{error_message}*\n\n"
388+
f"Thread: {thread_link}\n"
389+
f"User: <@{ctx.deps['user_id']}>"
390+
)
391+
392+
await _send_admin_notification(message)
393+
394+
395+
async def _send_admin_notification(message: str) -> None:
396+
"""Send a notification message to the admin."""
397+
if not settings.admin_slack_user_id:
398+
return
399+
400+
headers = {
401+
"Authorization": f"Bearer {settings.slack_api_token}",
402+
"Content-Type": "application/json",
403+
}
404+
405+
payload = {"channel": settings.admin_slack_user_id, "text": message}
406+
407+
async with httpx.AsyncClient() as client:
408+
response = await client.post(
409+
"https://slack.com/api/chat.postMessage", headers=headers, json=payload
410+
)
411+
response.raise_for_status()

examples/slackbot/src/slackbot/github.py

Lines changed: 0 additions & 64 deletions
This file was deleted.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""GitHub API integration module."""
2+
3+
from .client import GitHubClient
4+
from .models import (
5+
GitHubUser,
6+
GitHubLabel,
7+
GitHubComment,
8+
GitHubIssue,
9+
GitHubDiscussion,
10+
DiscussionCategory,
11+
)
12+
from .issues import search_issues, format_issues_summary
13+
from .discussions import (
14+
search_discussions,
15+
get_discussion_categories,
16+
create_discussion,
17+
create_discussion_from_thread,
18+
format_discussions_summary,
19+
)
20+
from .exceptions import (
21+
GitHubError,
22+
GitHubAuthError,
23+
GitHubNotFoundError,
24+
GitHubRateLimitError,
25+
)
26+
27+
__all__ = [
28+
# Client
29+
"GitHubClient",
30+
# Models
31+
"GitHubUser",
32+
"GitHubLabel",
33+
"GitHubComment",
34+
"GitHubIssue",
35+
"GitHubDiscussion",
36+
"DiscussionCategory",
37+
# Issues
38+
"search_issues",
39+
"format_issues_summary",
40+
# Discussions
41+
"search_discussions",
42+
"get_discussion_categories",
43+
"create_discussion",
44+
"create_discussion_from_thread",
45+
"format_discussions_summary",
46+
# Exceptions
47+
"GitHubError",
48+
"GitHubAuthError",
49+
"GitHubNotFoundError",
50+
"GitHubRateLimitError",
51+
]

0 commit comments

Comments
 (0)