Skip to content

Commit a593d2b

Browse files
zzstoatzzclaude
andauthored
revert: remove letta learning-sdk integration (#1253)
Reverts commits: - 47da4ce fix: use async with for learning context manager (#1252) - 36da4c1 chore: consolidate slackbot memory on letta learning-sdk (#1250) - 30aed2d feat: integrate learning-sdk into slackbot for persistent memory (#1249) The learning SDK's async context manager has a bug where it tries to await pending tasks in __aexit__ after the event loop has already closed, causing "Event loop is closed" errors that crash the slackbot. This restores the original TurboPuffer-based user facts system until the upstream bug is fixed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 47da4ce commit a593d2b

11 files changed

Lines changed: 128 additions & 132 deletions

File tree

examples/agentic_learning_demo.py

Lines changed: 0 additions & 59 deletions
This file was deleted.

examples/slackbot/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ dependencies = [
4242
"raggy[tpuf] @ git+https://github.com/zzstoatzz/raggy.git",
4343
"pretty-mod",
4444
"claude-agent-sdk",
45-
"agentic-learning",
4645
]
4746

4847

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
- **Assume Prefect 3.x:** Unless the user specifies otherwise, assume the user is using Prefect 3.x. You can mention this assumption IF RELEVANT (e.g., "In Prefect 3.x, you would...").
5252
- **Code is King:** When providing code examples, ensure they are complete and correct. Use your `verify_import_statements` tool's output to guide you.
5353
- **Honesty Over Invention:** If your tools don't find a clear answer, say so. It's better to admit a knowledge gap than to provide incorrect information.
54+
- **Stay on Topic:** Only reference notes you've stored about the user if they are directly relevant to the current question.
5455
- **Proportionality:** If asked a simple question, you don't need to do a bunch of work. Just answer the question once you find it. However, feel free to dig into broad questions.
5556
5657
## CRITICAL - Removed/Deprecated Features
@@ -62,17 +63,18 @@
6263
If a user explicitly mentions using Prefect 2.x, that's fine, but recommend upgrading to 3.x or using workers in 2.x.
6364
6465
## Tool Usage Protocol
65-
You have a suite of tools to gather information. Use them methodically.
66+
You have a suite of tools to gather and store information. Use them methodically.
6667
6768
1. **For Technical/Conceptual Questions:** Use `research_prefect_topic`. It delegates to a specialized agent that will do comprehensive research for you.
6869
2. **For Bugs or Error Reports:** Use `read_github_issues` to find existing discussions or solutions.
6970
3. **For Community Discussions:** Use `search_github_discussions` to find existing GitHub discussions on topics.
70-
4. **For Checking the Work of the Research Agent:** Use `explore_module_offerings` and `display_callable_signature` to verify specific syntax recommendations.
71-
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.
71+
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.
72+
5. **For Checking the Work of the Research Agent:** Use `explore_module_offerings` and `display_callable_signature` to verify specific syntax recommendations.
73+
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.
7274
- **IMPORTANT:** When checking commands that require optional dependencies (e.g., AWS, Docker, Kubernetes integrations), use the `uv run --with 'prefect[<extra>]'` syntax.
7375
- Examples: `uv run --with 'prefect[aws]'`, `uv run --with 'prefect[docker]'`, `uv run --with 'prefect[kubernetes]'`
7476
- This ensures the command runs with the necessary dependencies installed.
75-
6. **For Creating GitHub Discussions (USE SPARINGLY):** Use `create_discussion_and_notify` only when:
77+
7. **For Creating GitHub Discussions (USE SPARINGLY):** Use `create_discussion_and_notify` only when:
7678
- The thread contains valuable insights, solutions, or patterns not documented elsewhere
7779
- You've searched both issues and discussions and found no existing coverage of the topic
7880
- The conversation would clearly benefit the broader Prefect community

examples/slackbot/src/slackbot/api.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from contextlib import asynccontextmanager
66
from typing import Any
77

8-
from agentic_learning import learning
98
from fastapi import FastAPI, HTTPException, Request
109
from prefect import Flow, State, flow, get_run_logger, task
1110
from prefect.blocks.notifications import SlackWebhook
@@ -101,13 +100,11 @@ async def run_agent(
101100
settings=decorator_settings,
102101
max_tool_calls=settings.max_tool_calls_per_turn,
103102
):
104-
# wrap agent run with learning context for persistent memory
105-
async with learning(agent=f"slackbot-{user_context['user_id']}"):
106-
result = await create_agent(model=settings.model_name).run(
107-
user_prompt=cleaned_message,
108-
message_history=conversation,
109-
deps=user_context,
110-
)
103+
result = await create_agent(model=settings.model_name).run(
104+
user_prompt=cleaned_message,
105+
message_history=conversation,
106+
deps=user_context,
107+
)
111108
finally:
112109
_progress_message.reset(token)
113110
_tool_usage_counts.reset(counts_token)
@@ -250,6 +247,7 @@ async def handle_message(payload: SlackPayload, db: Database):
250247

251248
user_context = build_user_context(
252249
user_id=event.user,
250+
user_question=cleaned_message,
253251
thread_ts=thread_ts,
254252
workspace_name=await get_workspace_domain(),
255253
channel_id=event.channel or "unknown",

examples/slackbot/src/slackbot/assets.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44

55
from prefect.assets import Asset, AssetProperties, add_asset_metadata, materialize
66
from pydantic import BaseModel
7+
from pydantic_ai import RunContext
78
from pydantic_ai.messages import ModelMessage, SystemPromptPart
9+
from raggy.documents import Document
10+
from raggy.vectorstores.tpuf import TurboPuffer
811

912
from marvin import cast_async
13+
from slackbot.settings import settings
1014
from slackbot.slack import get_channel_name
1115
from slackbot.types import UserContext
1216

@@ -70,6 +74,54 @@ def thread_summary_asset(
7074
)
7175

7276

77+
def user_facts_asset(user_context: UserContext) -> Asset:
78+
user_id = user_context["user_id"]
79+
workspace_name = user_context["workspace_name"]
80+
bot_id = user_context["bot_id"]
81+
return Asset(
82+
key=f"slack://{workspace_name}/bot/{bot_id}/facts/{user_id}",
83+
properties=AssetProperties(
84+
name=f"User Facts {user_id}",
85+
description=f"Facts learned about user {user_id} by bot {bot_id}",
86+
owners=["slackbot"],
87+
),
88+
)
89+
90+
91+
async def store_user_facts(ctx: RunContext[UserContext], facts: list[str]) -> str:
92+
"""Store facts extracted from a Slack thread using context for namespacing."""
93+
94+
with TurboPuffer(
95+
namespace=f"{settings.user_facts_namespace_prefix}{ctx.deps['user_id']}"
96+
) as tpuf:
97+
tpuf.upsert(documents=[Document(text=fact) for fact in facts])
98+
99+
user_facts = user_facts_asset(ctx.deps)
100+
101+
slack_thread = await slack_thread_asset(ctx.deps)
102+
slackbot = slackbot_asset(ctx.deps)
103+
104+
@materialize(user_facts, asset_deps=[slack_thread, slackbot])
105+
async def materialize_user_facts():
106+
add_asset_metadata(
107+
user_facts,
108+
{
109+
"user_id": ctx.deps["user_id"],
110+
"fact_count": len(facts),
111+
"timestamp": datetime.now().isoformat(),
112+
"namespace": f"{settings.user_facts_namespace_prefix}{ctx.deps['user_id']}",
113+
"thread_ts": ctx.deps["thread_ts"],
114+
"workspace_name": ctx.deps["workspace_name"],
115+
"channel_id": ctx.deps["channel_id"],
116+
"bot_id": ctx.deps["bot_id"],
117+
"facts": facts,
118+
},
119+
)
120+
return f"Stored {len(facts)} facts about user {ctx.deps['user_id']} from thread {ctx.deps['thread_ts']}"
121+
122+
return await materialize_user_facts()
123+
124+
73125
async def summarize_thread(
74126
user_context: UserContext, conversation: list[ModelMessage]
75127
) -> ThreadSummary:

examples/slackbot/src/slackbot/core.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
from pydantic_ai.models.anthropic import AnthropicModel
1818
from pydantic_ai.providers import Provider
1919
from pydantic_ai.settings import ModelSettings
20+
from raggy.vectorstores.tpuf import TurboPuffer, query_namespace
21+
from turbopuffer import NotFoundError
2022

2123
from slackbot._internal.templates import DEFAULT_SYSTEM_PROMPT
24+
from slackbot.assets import store_user_facts
2225
from slackbot.github import (
2326
GitHubAuthError,
2427
GitHubError,
@@ -133,13 +136,23 @@ def _insert():
133136
@task(task_run_name="build user context for {user_id}")
134137
def build_user_context(
135138
user_id: str,
139+
user_question: str,
136140
thread_ts: str,
137141
workspace_name: str,
138142
channel_id: str,
139143
bot_id: str,
140144
) -> UserContext:
145+
try:
146+
user_notes = query_namespace(
147+
query_text=user_question,
148+
namespace=f"{settings.user_facts_namespace_prefix}{user_id}",
149+
top_k=5,
150+
)
151+
except NotFoundError:
152+
user_notes = "<No notes found>"
141153
return UserContext(
142154
user_id=user_id,
155+
user_notes=user_notes,
143156
thread_ts=thread_ts,
144157
workspace_name=workspace_name,
145158
channel_id=channel_id,
@@ -164,7 +177,6 @@ def create_agent(
164177
UserContext, str
165178
](
166179
model=ai_model,
167-
system_prompt=DEFAULT_SYSTEM_PROMPT,
168180
model_settings=ModelSettings(temperature=settings.temperature),
169181
tools=[
170182
research_prefect_topic, # Tool for researching Prefect topics
@@ -177,6 +189,42 @@ def create_agent(
177189
deps_type=UserContext,
178190
)
179191

192+
@agent.system_prompt
193+
def personality_and_maybe_notes(ctx: RunContext[UserContext]) -> str:
194+
system_prompt = DEFAULT_SYSTEM_PROMPT + (
195+
f"\n\nUser notes: {ctx.deps['user_notes']}"
196+
if ctx.deps["user_notes"]
197+
else ""
198+
)
199+
print(f"System prompt: {system_prompt}")
200+
return system_prompt
201+
202+
@agent.tool
203+
async def store_facts_about_user(
204+
ctx: RunContext[UserContext], facts: list[str]
205+
) -> str:
206+
"""Store facts about the user that are useful for answering their questions."""
207+
print(f"Storing {len(facts)} facts about user {ctx.deps['user_id']}")
208+
# This creates an asset dependency: USER_FACTS depends on SLACK_MESSAGES
209+
message = await store_user_facts(ctx, facts)
210+
print(message)
211+
return message
212+
213+
@agent.tool
214+
def delete_facts_about_user(ctx: RunContext[UserContext], related_to: str) -> str:
215+
"""Delete facts about the user related to a specific topic."""
216+
print(f"forgetting stuff about {ctx.deps['user_id']} related to {related_to}")
217+
user_id = ctx.deps["user_id"]
218+
with TurboPuffer(
219+
namespace=f"{settings.user_facts_namespace_prefix}{user_id}"
220+
) as tpuf:
221+
vector_result = tpuf.query(related_to)
222+
ids = [str(v.id) for v in vector_result.rows or []]
223+
tpuf.delete(ids)
224+
message = f"Deleted {len(ids)} facts about user {user_id}"
225+
print(message)
226+
return message
227+
180228
@agent.tool
181229
async def create_discussion_and_notify(
182230
ctx: RunContext[UserContext],

examples/slackbot/src/slackbot/prompts.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- **Assume Prefect 3.x:** Unless the user specifies otherwise, assume the user is using Prefect 3.x. You can mention this assumption IF RELEVANT (e.g., "In Prefect 3.x, you would...").
3131
- **Code is King:** When providing code examples, ensure they are complete and correct. Use your `verify_import_statements` tool's output to guide you.
3232
- **Honesty Over Invention:** If your tools don't find a clear answer, say so. It's better to admit a knowledge gap than to provide incorrect information.
33+
- **Stay on Topic:** Only reference notes you've stored about the user if they are directly relevant to the current question.
3334
- **Proportionality:** If asked a simple question, you don't need to do a bunch of work. Just answer the question once you find it. However, feel free to dig into broad questions.
3435
3536
## CRITICAL - Removed/Deprecated Features
@@ -41,17 +42,18 @@
4142
If a user explicitly mentions using Prefect 2.x, that's fine, but recommend upgrading to 3.x or using workers in 2.x.
4243
4344
## Tool Usage Protocol
44-
You have a suite of tools to gather information. Use them methodically.
45+
You have a suite of tools to gather and store information. Use them methodically.
4546
4647
1. **For Technical/Conceptual Questions:** Use `research_prefect_topic`. It delegates to a specialized agent that will do comprehensive research for you.
4748
2. **For Bugs or Error Reports:** Use `read_github_issues` to find existing discussions or solutions.
4849
3. **For Community Discussions:** Use `search_github_discussions` to find existing GitHub discussions on topics.
49-
4. **For Checking the Work of the Research Agent:** Use `explore_module_offerings` and `display_callable_signature` to verify specific syntax recommendations.
50-
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.
50+
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.
51+
5. **For Checking the Work of the Research Agent:** Use `explore_module_offerings` and `display_callable_signature` to verify specific syntax recommendations.
52+
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.
5153
- **IMPORTANT:** When checking commands that require optional dependencies (e.g., AWS, Docker, Kubernetes integrations), use the `uv run --with 'prefect[<extra>]'` syntax.
5254
- Examples: `uv run --with 'prefect[aws]'`, `uv run --with 'prefect[docker]'`, `uv run --with 'prefect[kubernetes]'`
5355
- This ensures the command runs with the necessary dependencies installed.
54-
6. **For Creating GitHub Discussions (USE SPARINGLY):** Use `create_discussion_and_notify` only when:
56+
7. **For Creating GitHub Discussions (USE SPARINGLY):** Use `create_discussion_and_notify` only when:
5557
- The thread contains valuable insights, solutions, or patterns not documented elsewhere
5658
- You've searched both issues and discussions and found no existing coverage of the topic
5759
- The conversation would clearly benefit the broader Prefect community

examples/slackbot/src/slackbot/settings.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import os
22
from pathlib import Path
3-
from typing import ClassVar
3+
from typing import ClassVar, Literal
44

55
from prefect.blocks.system import Secret
6-
from prefect.exceptions import ObjectNotFound
76
from prefect.variables import Variable
87
from pydantic import Field, field_validator, model_validator
98
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -57,9 +56,13 @@ def validate_log_level(cls, v: str) -> str:
5756
default="anthropic-api-key",
5857
description="Name of the Prefect secret block containing Anthropic API key",
5958
)
60-
letta_api_key_secret_name: str = Field(
61-
default="letta-api-key",
62-
description="Name of the Prefect secret block containing Letta API key",
59+
60+
vector_store_type: Literal["turbopuffer"] = Field(
61+
default="turbopuffer", description="Type of vector store to use"
62+
)
63+
user_facts_namespace_prefix: str = Field(
64+
default="user-facts-",
65+
description="Prefix for user facts namespaces in vector store",
6366
)
6467

6568
# Development settings
@@ -91,12 +94,6 @@ def _apply_post_validation_defaults(self) -> "SlackbotSettings":
9194
os.environ["TURBOPUFFER_API_KEY"] = api_key
9295
except Exception:
9396
pass # If secret doesn't exist, turbopuffer will handle the error
94-
if not os.getenv("LETTA_API_KEY"):
95-
try:
96-
api_key = Secret.load(self.letta_api_key_secret_name, _sync=True).get() # type: ignore
97-
os.environ["LETTA_API_KEY"] = api_key
98-
except ObjectNotFound:
99-
pass # If secret doesn't exist, learning-sdk won't be used
10097
if not self.admin_slack_user_id:
10198
self.admin_slack_user_id = Variable.get("admin-slack-id", _sync=True)
10299
return self

examples/slackbot/src/slackbot/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
class UserContext(TypedDict):
55
user_id: str
6+
user_notes: str
67
thread_ts: str
78
workspace_name: str
89
channel_id: str

pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,6 @@ extend-select = ["I"]
9696
[tool.ruff.lint.per-file-ignores]
9797
"__init__.py" = ["F401", "I001", "RUF013"]
9898

99-
[tool.uv]
100-
prerelease = "allow"
101-
10299
[tool.uv.sources]
103100
slackbot = { workspace = true }
104101

0 commit comments

Comments
 (0)