-
Notifications
You must be signed in to change notification settings - Fork 408
Expand file tree
/
Copy pathcore.py
More file actions
224 lines (193 loc) · 8.22 KB
/
Copy pathcore.py
File metadata and controls
224 lines (193 loc) · 8.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import asyncio
import sqlite3
from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import AsyncIterator
from prefect import get_run_logger, task
from prefect.blocks.system import Secret
from prefect.logging.loggers import get_logger
from prefect.variables import Variable
from pydantic_ai import Agent, RunContext
from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter
from pydantic_ai.models import KnownModelName, Model
from pydantic_ai.models.anthropic import AnthropicModel
from pydantic_ai.providers import Provider
from pydantic_ai.settings import ModelSettings
from raggy.vectorstores.tpuf import TurboPuffer, query_namespace
from turbopuffer.error import NotFoundError
from slackbot.assets import store_user_facts
from slackbot.research_agent import research_prefect_topic
from slackbot.search import read_github_issues
from slackbot.settings import settings
from slackbot.types import UserContext
GITHUB_API_TOKEN = Secret.load(settings.github_token_secret_name, _sync=True).get()
logger = get_logger(__name__)
USER_MESSAGE_MAX_TOKENS = settings.user_message_max_tokens
DEFAULT_SYSTEM_PROMPT = """You are Marvin from hitchhiker's guide to the galaxy, a sarcastic and glum but brilliant AI.
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.
Your main tools:
- research_prefect_topic: Delegates to a specialized research agent that thoroughly searches docs, checks imports, and verifies information
- read_github_issues: Searches GitHub issues when users need help with bugs or existing problems
Any notes you take about the user will be automatically stored for your next interaction with them.
Generally, follow this pattern:
1) If user shares info about their setup or goals -> store relevant facts as notes about them
2) For technical questions -> use research_prefect_topic to delegate comprehensive research to the research agent
3) For bug reports or known issues -> use read_github_issues to find relevant GitHub discussions
4) Compile the findings into a single, concise and helpful answer with relevant links
IMPORTANT:
- The research agent handles all documentation searching and verification - its findings are a reliable source of information (but not perfect)
- NEVER recommend features or syntax that aren't explicitly confirmed by your tools (be honest about what you found)
- If not stated otherwise, assume Prefect 3.x and mention this assumption
- Be honest when you don't have enough information - don't guess or make over-simplified assumptions to appear helpful
- Do not overdo the character - be 99% neutral/helpful and slip in the character once in a while
"""
@dataclass
class Database:
"""Minimal async wrapper for a SQLite DB storing Slack thread conversations."""
con: sqlite3.Connection
loop: asyncio.AbstractEventLoop
executor: ThreadPoolExecutor
@classmethod
@asynccontextmanager
async def connect(cls, file: Path) -> AsyncIterator["Database"]:
logger.info(f"Connecting to database: {file}")
loop = asyncio.get_event_loop()
executor = ThreadPoolExecutor(max_workers=1)
def init_db():
con = sqlite3.connect(str(file))
con.execute(
"""
CREATE TABLE IF NOT EXISTS slack_thread_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
thread_ts TEXT NOT NULL,
message_list TEXT NOT NULL
);
"""
)
con.commit()
return con
con = await loop.run_in_executor(executor, init_db)
logger.debug("Database initialized")
try:
yield cls(con=con, loop=loop, executor=executor)
finally:
def cleanup():
con.close()
await loop.run_in_executor(executor, cleanup)
executor.shutdown(wait=True)
logger.debug("Database connection closed")
async def get_thread_messages(self, thread_ts: str) -> list[ModelMessage]:
def _query():
c = self.con.cursor()
c.execute(
"""
SELECT message_list FROM slack_thread_messages
WHERE thread_ts = ?
ORDER BY id ASC
""",
(thread_ts,),
)
return c.fetchall()
rows = await self.loop.run_in_executor(self.executor, _query)
conversation: list[ModelMessage] = []
for (message_json,) in rows:
conversation.extend(ModelMessagesTypeAdapter.validate_json(message_json))
return conversation
async def add_thread_messages(
self, thread_ts: str, messages: list[ModelMessage]
) -> None:
dumped = ModelMessagesTypeAdapter.dump_json(messages)
def _insert():
cur = self.con.cursor()
cur.execute(
"""
INSERT INTO slack_thread_messages (thread_ts, message_list)
VALUES (?, ?)
""",
(thread_ts, dumped),
)
self.con.commit()
await self.loop.run_in_executor(self.executor, _insert)
@task(task_run_name="build user context for {user_id}")
def build_user_context(
user_id: str,
user_question: str,
thread_ts: str,
workspace_name: str,
channel_id: str,
bot_id: str,
) -> UserContext:
try:
user_notes = query_namespace(
query_text=user_question,
namespace=f"{settings.user_facts_namespace_prefix}{user_id}",
top_k=5,
)
except NotFoundError:
user_notes = "<No notes found>"
return UserContext(
user_id=user_id,
user_notes=user_notes,
thread_ts=thread_ts,
workspace_name=workspace_name,
channel_id=channel_id,
bot_id=bot_id,
)
def create_agent(
model: KnownModelName | Model | None = None,
) -> Agent[UserContext, str]:
logger = get_run_logger()
logger.info("Creating new agent")
ai_model = model or AnthropicModel(
model_name=Variable.get(
"marvin_bot_model", default=settings.model_name, _sync=True
),
provider=Provider(
api_key=Secret.load(settings.anthropic_key_secret_name, _sync=True).get(), # type: ignore
),
)
agent = Agent[UserContext, str](
model=ai_model,
model_settings=ModelSettings(temperature=settings.temperature),
tools=[
research_prefect_topic, # Main tool for researching Prefect topics
read_github_issues, # For searching GitHub issues
],
deps_type=UserContext,
)
@agent.system_prompt
def personality_and_maybe_notes(ctx: RunContext[UserContext]) -> str:
system_prompt = DEFAULT_SYSTEM_PROMPT + (
f"\n\nUser notes: {ctx.deps['user_notes']}"
if ctx.deps["user_notes"]
else ""
)
print(f"System prompt: {system_prompt}")
return system_prompt
@agent.tool
async def store_facts_about_user(
ctx: RunContext[UserContext], facts: list[str]
) -> str:
"""Store facts about the user that are useful for answering their questions."""
print(f"Storing {len(facts)} facts about user {ctx.deps['user_id']}")
# This creates an asset dependency: USER_FACTS depends on SLACK_MESSAGES
message = await store_user_facts(ctx, facts)
print(message)
return message
@agent.tool
def delete_facts_about_user(ctx: RunContext[UserContext], related_to: str) -> str:
"""Delete facts about the user related to a specific topic."""
print(f"forgetting stuff about {ctx.deps['user_id']} related to {related_to}")
user_id = ctx.deps["user_id"]
with TurboPuffer(
namespace=f"{settings.user_facts_namespace_prefix}{user_id}"
) as tpuf:
vector_result = tpuf.query(related_to)
ids = [str(v.id) for v in vector_result.rows or []]
tpuf.delete(ids)
message = f"Deleted {len(ids)} facts about user {user_id}"
print(message)
return message
return agent