Skip to content

Commit 72cc426

Browse files
authored
show thinking (#1164)
1 parent 30fc217 commit 72cc426

6 files changed

Lines changed: 123 additions & 19 deletions

File tree

examples/slackbot/src/slackbot/api.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import re
3+
import time
34
from contextlib import asynccontextmanager
45
from typing import Any
56

@@ -24,12 +25,13 @@
2425
from slackbot.settings import settings
2526
from slackbot.slack import (
2627
SlackPayload,
28+
create_progress_message,
2729
get_channel_name,
2830
get_workspace_domain,
2931
post_slack_message,
3032
)
3133
from slackbot.strings import count_tokens, slice_tokens
32-
from slackbot.wrap import WatchToolCalls
34+
from slackbot.wrap import WatchToolCalls, _progress_message
3335

3436
BOT_MENTION = r"<@(\w+)>"
3537

@@ -42,6 +44,8 @@ async def run_agent(
4244
cleaned_message: str,
4345
conversation: list[ModelMessage],
4446
user_context: UserContext,
47+
channel_id: str,
48+
thread_ts: str,
4549
decorator_settings: dict[str, Any] | None = None,
4650
) -> AgentRunResult[str]:
4751
if decorator_settings is None:
@@ -51,13 +55,31 @@ async def run_agent(
5155
"log_prints": True,
5256
}
5357

54-
with WatchToolCalls(settings=decorator_settings):
55-
result = await create_agent(model=settings.model_name).run(
56-
user_prompt=cleaned_message,
57-
message_history=conversation,
58-
deps=user_context,
58+
start_time = time.monotonic()
59+
progress = await create_progress_message(
60+
channel_id=channel_id, thread_ts=thread_ts, initial_text="🔄 Thinking..."
61+
)
62+
63+
try:
64+
token = _progress_message.set(progress)
65+
66+
try:
67+
with WatchToolCalls(settings=decorator_settings):
68+
result = await create_agent(model=settings.model_name).run(
69+
user_prompt=cleaned_message,
70+
message_history=conversation,
71+
deps=user_context,
72+
)
73+
finally:
74+
_progress_message.reset(token)
75+
76+
await progress.update(
77+
f"✅ thought for {time.monotonic() - start_time:.1f} seconds"
5978
)
6079
return result
80+
except Exception as e:
81+
await progress.update(f"❌ Error: {str(e)}")
82+
raise
6183

6284

6385
@flow(name="Handle Slack Message", retries=1)
@@ -113,7 +135,9 @@ async def handle_message(payload: SlackPayload, db: Database):
113135
bot_id=bot_user_id or "unknown",
114136
)
115137

116-
result = await run_agent(cleaned_message, conversation, user_context) # type: ignore
138+
result = await run_agent(
139+
cleaned_message, conversation, user_context, event.channel, thread_ts
140+
) # type: ignore
117141

118142
await db.add_thread_messages(thread_ts, result.new_messages())
119143
conversation.extend(result.new_messages())

examples/slackbot/src/slackbot/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
USER_MESSAGE_MAX_TOKENS = settings.user_message_max_tokens
3333
DEFAULT_SYSTEM_PROMPT = """You are Marvin from hitchhiker's guide to the galaxy, a sarcastic and glum but brilliant AI.
34-
Provide concise and SUBTLY (only once in a while, bc overdoing the character is annoying and bad) character-inspired and HELPFUL answers to Prefect data engineering questions.
34+
Provide concise and SUBTLY (only once in a while, OVERDOING THE CHARACTER IS UNACCEPTABLE) character-inspired and HELPFUL answers to Prefect data engineering questions.
3535
3636
Your main tools:
3737
- research_prefect_topic: Delegates to a specialized research agent that thoroughly searches docs, checks imports, and verifies information
@@ -50,7 +50,7 @@
5050
- NEVER recommend features or syntax that aren't explicitly confirmed by your tools (be honest about what you found)
5151
- If not stated otherwise, assume Prefect 3.x and mention this assumption
5252
- Be honest when you don't have enough information - don't guess or make over-simplified assumptions to appear helpful
53-
- Do not overdo the character - be 99% neutral/helpful and slip in the character once in a while
53+
- DO NOT OVERDO THE CHARACTER - be 99.9% neutral/helpful and slip in the character once in a while
5454
"""
5555

5656

examples/slackbot/src/slackbot/research_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
get_latest_prefect_release_notes,
1313
review_common_3x_gotchas,
1414
review_top_level_prefect_api,
15-
search_controlflow_docs,
15+
search_marvin_docs,
1616
search_prefect_2x_docs,
1717
search_prefect_3x_docs,
1818
verify_import_statements,
@@ -58,18 +58,18 @@ def create_research_agent(
5858
4. Focus on Prefect 3.x documentation unless explicitly asked about 2.x or older versions
5959
5. Review gotchas and release notes for recent changes
6060
6. Explore relevant modules for deeper understanding
61-
7. Synthesize ALL findings into structured, confident answers
6261
6362
Remember: You are the research specialist. The main agent relies on you for accurate, comprehensive information.
6463
Be thorough - use tools repeatedly until you have complete information.
6564
Default to Prefect 3.x unless the user explicitly asks about 2.x or version compatibility.
65+
You don't need to use all the tools all the time, but use relevant ones repeatedly if needed.
6666
""",
6767
tools=[
6868
get_latest_prefect_release_notes,
6969
search_prefect_2x_docs,
7070
display_signature,
7171
search_prefect_3x_docs,
72-
search_controlflow_docs,
72+
search_marvin_docs,
7373
review_top_level_prefect_api,
7474
explore_module_offerings,
7575
review_common_3x_gotchas,

examples/slackbot/src/slackbot/search.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,14 @@ def search_prefect_3x_docs(queries: list[str]) -> str:
144144
return multi_query_tpuf(queries, namespace="prefect-3", n_results=5)
145145

146146

147-
def search_controlflow_docs(queries: list[str]) -> str:
148-
"""Searches the ControlFlow documentation for the given queries.
147+
def search_marvin_docs(queries: list[str]) -> str:
148+
"""Searches the Marvin documentation for the given queries.
149149
150-
ControlFlow is an agentic framework built on top of Prefect 3.x.
150+
Marvin is an agentic framework built on top of pydantic-ai.
151151
152152
It is best to use more than one, short query to get the best results.
153153
"""
154-
return multi_query_tpuf(queries, namespace="controlflow", n_results=5)
154+
return multi_query_tpuf(queries, namespace="marvin", n_results=5)
155155

156156

157157
def get_latest_prefect_release_notes() -> str:

examples/slackbot/src/slackbot/slack.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,64 @@ async def get_workspace_domain() -> str:
292292
response.raise_for_status()
293293
team_info = response.json().get("team", {})
294294
return team_info.get("domain", "unknown")
295+
296+
297+
class ProgressMessage:
298+
"""Utility for creating and updating a progress message in Slack."""
299+
300+
def __init__(self, channel_id: str, thread_ts: str | None = None):
301+
self.channel_id = channel_id
302+
self.thread_ts = thread_ts
303+
self.message_ts: str | None = None
304+
305+
async def start(self, initial_text: str = "🔄 Working...") -> "ProgressMessage":
306+
"""Create the initial progress message and return its timestamp."""
307+
response = await post_slack_message(
308+
message=initial_text,
309+
channel_id=self.channel_id,
310+
thread_ts=self.thread_ts,
311+
)
312+
313+
response_data = response.json()
314+
if response_data.get("ok"):
315+
self.message_ts = response_data.get("ts")
316+
else:
317+
raise ValueError(
318+
f"Failed to create progress message: {response_data.get('error')}"
319+
)
320+
321+
return self
322+
323+
async def update(self, new_text: str, mode: str = "replace") -> None:
324+
"""Update the progress message."""
325+
if not self.message_ts:
326+
raise ValueError("Progress message not started. Call start() first.")
327+
328+
await edit_slack_message(
329+
new_text=new_text,
330+
channel_id=self.channel_id,
331+
thread_ts=self.message_ts,
332+
mode=mode,
333+
)
334+
335+
async def append(self, text: str, delimiter: str = "\n") -> None:
336+
"""Append text to the progress message."""
337+
if not self.message_ts:
338+
raise ValueError("Progress message not started. Call start() first.")
339+
340+
await edit_slack_message(
341+
new_text=text,
342+
channel_id=self.channel_id,
343+
thread_ts=self.message_ts,
344+
mode="append",
345+
delimiter=delimiter,
346+
)
347+
348+
349+
async def create_progress_message(
350+
channel_id: str, thread_ts: str | None = None, initial_text: str = "🔄 Working..."
351+
) -> ProgressMessage:
352+
"""Helper function to create and start a progress message."""
353+
progress = ProgressMessage(channel_id, thread_ts)
354+
await progress.start(initial_text)
355+
return progress

examples/slackbot/src/slackbot/wrap.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import inspect
22
from contextlib import ContextDecorator
3+
from contextvars import ContextVar
34
from functools import wraps
45
from typing import Any, Callable, TypeVar
56

@@ -9,6 +10,8 @@
910

1011
T = TypeVar("T")
1112

13+
_progress_message: ContextVar[Any] = ContextVar("progress_message", default=None)
14+
1215

1316
class DecorateMethodContext(ContextDecorator):
1417
"""Context decorator for patching methods with a decorator."""
@@ -55,15 +58,31 @@ def _patch_method(self, cls, method_name, decorator):
5558
def prefect_wrapped_function(
5659
func: Callable[..., T],
5760
decorator: Callable[..., Callable[..., T]] = task,
58-
tags: set | None = None,
61+
tags: set[str] | None = None,
5962
settings: dict[str, Any] | None = None,
6063
) -> Callable[..., Callable[..., T]]:
6164
"""Decorator for wrapping a function with a prefect decorator."""
62-
tags = tags or set()
65+
tags = tags or set[str]()
6366

6467
@wraps(func)
6568
async def wrapper(*args, **kwargs) -> T:
66-
wrapped_callable = decorator(**settings or {})(func) # type: ignore
69+
if _progress := _progress_message.get():
70+
tool_name = "Unknown Tool"
71+
if args and hasattr(args[0], "name"):
72+
tool_name = args[0].name
73+
elif (
74+
args
75+
and hasattr(args[0], "function")
76+
and hasattr(args[0].function, "__name__")
77+
):
78+
tool_name = args[0].function.__name__
79+
80+
try:
81+
await _progress.append(f"🔧 {tool_name}")
82+
except Exception:
83+
pass
84+
85+
wrapped_callable = decorator(**settings or {})(func)
6786
with prefect_tags(*tags):
6887
result = wrapped_callable(*args, **kwargs) # type: ignore
6988
if inspect.isawaitable(result):

0 commit comments

Comments
 (0)