diff --git a/examples/slackbot/CLAUDE.md b/examples/slackbot/CLAUDE.md index b757bd986..4246ab4fe 100644 --- a/examples/slackbot/CLAUDE.md +++ b/examples/slackbot/CLAUDE.md @@ -37,7 +37,7 @@ An intelligent Slack bot built with Marvin that provides AI-powered assistance i uv sync # Start the bot server -uv run examples/slackbot/start.py +uv run --extra slackbot -m slackbot # Or with Docker docker build -f examples/slackbot/Dockerfile.slackbot -t marvin-slackbot . diff --git a/examples/slackbot/Dockerfile.slackbot b/examples/slackbot/Dockerfile.slackbot index aa6df291d..cdbc9018d 100644 --- a/examples/slackbot/Dockerfile.slackbot +++ b/examples/slackbot/Dockerfile.slackbot @@ -1,4 +1,4 @@ -FROM python:3.12-slim +FROM python:3.13-slim COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ @@ -11,8 +11,8 @@ RUN apt-get update && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -RUN uv sync --extra slackbot +RUN uv sync --extra slackbot --no-dev EXPOSE 4200 -CMD ["uv", "run", "examples/slackbot/start.py"] \ No newline at end of file +CMD ["uv", "run", "--no-sync", "-m", "slackbot"] \ No newline at end of file diff --git a/examples/slackbot/README.md b/examples/slackbot/README.md index 24ee2fb7a..164c83450 100644 --- a/examples/slackbot/README.md +++ b/examples/slackbot/README.md @@ -8,7 +8,7 @@ A Slack chatbot powered by Claude with memories and Prefect-specific knowledge. ├── api.py # FastAPI app and Slack event handlers ├── core.py # Database, agent, and memory management ├── settings.py # Configuration management -└── start.py # Entry point +└── __main__.py # Entry point ``` ## Setup @@ -70,7 +70,7 @@ ngrok http 4200 # Or your configured port 2. Start the bot in another terminal: ```console -uv run -m examples.slackbot.start +uv run --extra slackbot -m slackbot ``` ### Testing diff --git a/examples/slackbot/pyproject.toml b/examples/slackbot/pyproject.toml new file mode 100644 index 000000000..709bdf4aa --- /dev/null +++ b/examples/slackbot/pyproject.toml @@ -0,0 +1,63 @@ +[build-system] +requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"] +build-backend = "hatchling.build" + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +vcs = "git" +style = "pep440" +bump = true + +[project] +name = "slackbot" +dynamic = ["version", "dependencies"] +description = "Slackbot for Prefect" +authors = [{ name = "Nathan Nowack", email = "nate@prefect.io" }] +license = "MIT" +readme = "README.md" +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Operating System :: Unix", + "Operating System :: POSIX :: Linux", + "Environment :: Console", + "Environment :: MacOS X", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Internet", +] +requires-python = ">=3.9" + +[tool.hatch.metadata.hooks.uv-dynamic-versioning] +dependencies = [ + "marvin", + "prefect", + "numpy", + "raggy[tpuf]@git+https://github.com/zzstoatzz/raggy.git", +] + + +[dependency-groups] +dev = [] + +[tool.hatch.metadata] +allow-direct-references = true + + +[tool.hatch.build.targets.wheel] +packages = ["src/slackbot"] + +[tool.uv.sources] +marvin = { workspace = true } diff --git a/examples/slackbot/__init__.py b/examples/slackbot/src/slackbot/__init__.py similarity index 100% rename from examples/slackbot/__init__.py rename to examples/slackbot/src/slackbot/__init__.py diff --git a/examples/slackbot/start.py b/examples/slackbot/src/slackbot/__main__.py similarity index 87% rename from examples/slackbot/start.py rename to examples/slackbot/src/slackbot/__main__.py index a8aa4d7f2..8cd953856 100644 --- a/examples/slackbot/start.py +++ b/examples/slackbot/src/slackbot/__main__.py @@ -4,7 +4,8 @@ if __name__ == "__main__": import uvicorn - from settings import settings + + from slackbot.settings import settings if not (openai_api_key := os.getenv("OPENAI_API_KEY")): os.environ["OPENAI_API_KEY"] = Secret.load( @@ -12,7 +13,7 @@ ).get() uvicorn.run( - "api:app", + "slackbot.api:app", host=settings.host, port=settings.port, # reload=settings.test_mode, diff --git a/examples/slackbot/api.py b/examples/slackbot/src/slackbot/api.py similarity index 93% rename from examples/slackbot/api.py rename to examples/slackbot/src/slackbot/api.py index 8f0b69ea9..c112a5c81 100644 --- a/examples/slackbot/api.py +++ b/examples/slackbot/src/slackbot/api.py @@ -3,12 +3,6 @@ from contextlib import asynccontextmanager from typing import Any -from core import ( - Database, - UserContext, - build_user_context, - create_agent, -) from fastapi import FastAPI, HTTPException, Request from prefect import flow, get_run_logger, task from prefect.blocks.notifications import SlackWebhook @@ -18,10 +12,17 @@ from prefect.variables import Variable from pydantic_ai.agent import AgentRunResult from pydantic_ai.messages import ModelMessage -from settings import settings -from slack import SlackPayload, get_channel_name, post_slack_message -from strings import count_tokens, slice_tokens -from wrap import WatchToolCalls + +from slackbot.core import ( + Database, + UserContext, + build_user_context, + create_agent, +) +from slackbot.settings import settings +from slackbot.slack import SlackPayload, get_channel_name, post_slack_message +from slackbot.strings import count_tokens, slice_tokens +from slackbot.wrap import WatchToolCalls BOT_MENTION = r"<@(\w+)>" @@ -120,11 +121,11 @@ async def lifespan(app: FastAPI): @app.post("/chat") async def chat_endpoint(request: Request) -> dict[str, Any]: try: - payload = SlackPayload(**await request.json()) + payload = SlackPayload.model_validate(await request.json()) except Exception as e: logger.error(f"Error parsing Slack payload: {e}") - slack_webhook = await SlackWebhook.load("marvin-bot-pager") # type: ignore - await slack_webhook.notify( # type: ignore + slack_webhook = await SlackWebhook.load("marvin-bot-pager") + await slack_webhook.notify( body=f"Error parsing Slack payload: {e}", subject="Slackbot Error", ) @@ -155,8 +156,8 @@ async def chat_endpoint(request: Request) -> dict[str, Any]: channel_name = await get_channel_name(payload.event.channel) if channel_name.startswith("D"): logger.warning(f"Attempted DM in channel: {channel_name}") - slack_webhook = await SlackWebhook.load("marvin-bot-pager") # type: ignore - await slack_webhook.notify( # type: ignore + slack_webhook = await SlackWebhook.load("marvin-bot-pager") + await slack_webhook.notify( body=f"Attempted DM: {channel_name}", subject="Slackbot DM Warning", ) diff --git a/examples/slackbot/core.py b/examples/slackbot/src/slackbot/core.py similarity index 96% rename from examples/slackbot/core.py rename to examples/slackbot/src/slackbot/core.py index 25fce591c..3f376c60a 100644 --- a/examples/slackbot/core.py +++ b/examples/slackbot/src/slackbot/core.py @@ -4,7 +4,7 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from pathlib import Path -from typing import AsyncIterator, TypedDict, cast +from typing import AsyncIterator, TypedDict from prefect import get_run_logger, task from prefect.blocks.system import Secret @@ -17,11 +17,12 @@ from pydantic_ai.settings import ModelSettings from raggy.documents import Document from raggy.vectorstores.tpuf import TurboPuffer, query_namespace -from research_agent import research_prefect_topic -from search import read_github_issues -from settings import settings from turbopuffer.error import NotFoundError +from slackbot.research_agent import research_prefect_topic +from slackbot.search import read_github_issues +from slackbot.settings import settings + GITHUB_API_TOKEN = Secret.load(settings.github_token_secret_name, _sync=True).get() # type: ignore logger = get_logger(__name__) @@ -155,12 +156,11 @@ def create_agent( 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="anthropic", api_key=Secret.load(settings.claude_key_secret_name, _sync=True).get(), # type: ignore - model=cast( - str, - Variable.get("marvin_bot_model", default=settings.model_name, _sync=True), # type: ignore - ), ) agent = Agent[UserContext, str]( model=ai_model, diff --git a/examples/slackbot/modules.py b/examples/slackbot/src/slackbot/modules.py similarity index 100% rename from examples/slackbot/modules.py rename to examples/slackbot/src/slackbot/modules.py diff --git a/examples/slackbot/src/slackbot/py.typed b/examples/slackbot/src/slackbot/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/examples/slackbot/research_agent.py b/examples/slackbot/src/slackbot/research_agent.py similarity index 98% rename from examples/slackbot/research_agent.py rename to examples/slackbot/src/slackbot/research_agent.py index 78cdac219..acb5a4437 100644 --- a/examples/slackbot/research_agent.py +++ b/examples/slackbot/src/slackbot/research_agent.py @@ -1,12 +1,13 @@ """Research agent for improved information gathering.""" -from modules import display_signature from prefect import task from prefect.cache_policies import INPUTS from pydantic import BaseModel, Field from pydantic_ai import Agent from pydantic_ai.models import Model -from search import ( + +from slackbot.modules import display_signature +from slackbot.search import ( explore_module_offerings, get_latest_prefect_release_notes, review_common_3x_gotchas, diff --git a/examples/slackbot/search.py b/examples/slackbot/src/slackbot/search.py similarity index 98% rename from examples/slackbot/search.py rename to examples/slackbot/src/slackbot/search.py index 94b2fe2d5..ceb49cbc7 100644 --- a/examples/slackbot/search.py +++ b/examples/slackbot/src/slackbot/search.py @@ -6,16 +6,16 @@ import httpx import turbopuffer as tpuf -from modules import ModuleTreeExplorer from prefect import task from prefect.blocks.system import Secret from pydantic import BaseModel, Field, field_validator from raggy.vectorstores.tpuf import multi_query_tpuf -from settings import settings -from strings import slice_tokens import marvin from marvin.utilities.logging import get_logger +from slackbot.modules import ModuleTreeExplorer +from slackbot.settings import settings +from slackbot.strings import slice_tokens def verify_import_statements(import_statements: list[str]) -> str: diff --git a/examples/slackbot/settings.py b/examples/slackbot/src/slackbot/settings.py similarity index 100% rename from examples/slackbot/settings.py rename to examples/slackbot/src/slackbot/settings.py diff --git a/examples/slackbot/slack.py b/examples/slackbot/src/slackbot/slack.py similarity index 99% rename from examples/slackbot/slack.py rename to examples/slackbot/src/slackbot/slack.py index 4cf987939..0297a744c 100644 --- a/examples/slackbot/slack.py +++ b/examples/slackbot/src/slackbot/slack.py @@ -5,7 +5,8 @@ import httpx from pydantic import BaseModel, ValidationInfo, field_validator, model_validator -from settings import settings + +from slackbot.settings import settings class EventBlockElement(BaseModel): diff --git a/examples/slackbot/strings.py b/examples/slackbot/src/slackbot/strings.py similarity index 100% rename from examples/slackbot/strings.py rename to examples/slackbot/src/slackbot/strings.py diff --git a/examples/slackbot/wrap.py b/examples/slackbot/src/slackbot/wrap.py similarity index 100% rename from examples/slackbot/wrap.py rename to examples/slackbot/src/slackbot/wrap.py diff --git a/justfile b/justfile index a32245ad2..d8e1f3034 100644 --- a/justfile +++ b/justfile @@ -37,4 +37,4 @@ run-pre-commits: check-uv uv run pre-commit run --all-files run-slackbot: check-uv - uv run --extra slackbot examples/slackbot/start.py \ No newline at end of file + uv run --extra slackbot -m slackbot \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 97a7ba6a0..c3e7578c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,12 +45,7 @@ dev = [ [project.optional-dependencies] audio = ["pyaudio>=0.2.14"] -slackbot = [ - "pydantic-ai", - "prefect", - "numpy", - "raggy[tpuf]@git+https://github.com/zzstoatzz/raggy.git", -] +slackbot = ["slackbot"] mcp = ["fastmcp"] @@ -101,6 +96,12 @@ extend-select = ["I"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401", "I001", "RUF013"] +[tool.uv.sources] +slackbot = { workspace = true } + +[tool.uv.workspace] +members = ["examples/slackbot"] + [build-system] requires = ["hatchling>=1.21.0", "hatch-vcs>=0.4.0"] build-backend = "hatchling.build" diff --git a/uv.lock b/uv.lock index 5ce4ca1a6..7d18f922e 100644 --- a/uv.lock +++ b/uv.lock @@ -8,6 +8,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[manifest] +members = [ + "marvin", + "slackbot", +] + [[package]] name = "aiofiles" version = "24.1.0" @@ -2080,10 +2086,7 @@ mcp = [ { name = "fastmcp" }, ] slackbot = [ - { name = "numpy" }, - { name = "prefect" }, - { name = "pydantic-ai" }, - { name = "raggy", extra = ["tpuf"] }, + { name = "slackbot" }, ] [package.dev-dependencies] @@ -2117,16 +2120,13 @@ requires-dist = [ { name = "alembic", specifier = ">=1.12.0" }, { name = "fastmcp", marker = "extra == 'mcp'" }, { name = "jinja2", specifier = ">=3.1.4" }, - { name = "numpy", marker = "extra == 'slackbot'" }, { name = "partial-json-parser", specifier = ">=0.2.1.1.post5" }, - { name = "prefect", marker = "extra == 'slackbot'" }, { name = "pyaudio", marker = "extra == 'audio'", specifier = ">=0.2.14" }, { name = "pydantic", extras = ["email"], specifier = ">=2.10.6" }, { name = "pydantic-ai", specifier = ">=0.0.31" }, - { name = "pydantic-ai", marker = "extra == 'slackbot'" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, - { name = "raggy", extras = ["tpuf"], marker = "extra == 'slackbot'", git = "https://github.com/zzstoatzz/raggy.git" }, { name = "rich", specifier = ">=13.9.4" }, + { name = "slackbot", marker = "extra == 'slackbot'", editable = "examples/slackbot" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.36" }, { name = "typer", specifier = ">=0.15.1" }, ] @@ -3450,14 +3450,14 @@ wheels = [ [[package]] name = "pypdf" -version = "5.5.0" +version = "5.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/c8/543f8ae1cd9e182e9f979d9ab1df18e3445350471abadbdabc0166ae5741/pypdf-5.5.0.tar.gz", hash = "sha256:8ce6a18389f7394fd09a1d4b7a34b097b11c19088a23cfd09e5008f85893e254", size = 5021690, upload-time = "2025-05-11T14:00:42.043Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/46/67de1d7a65412aa1c896e6b280829b70b57d203fadae6859b690006b8e0a/pypdf-5.6.0.tar.gz", hash = "sha256:a4b6538b77fc796622000db7127e4e58039ec5e6afd292f8e9bf42e2e985a749", size = 5023749, upload-time = "2025-06-01T12:19:40.101Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/4e/931b90b51e3ebc69699be926b3d5bfdabae2d9c84337fd0c9fb98adbf70c/pypdf-5.5.0-py3-none-any.whl", hash = "sha256:2f61f2d32dde00471cd70b8977f98960c64e84dd5ba0d070e953fcb4da0b2a73", size = 303371, upload-time = "2025-05-11T14:00:40.064Z" }, + { url = "https://files.pythonhosted.org/packages/71/8b/dc3a72d98c22be7a4cbd664ad14c5a3e6295c2dbdf572865ed61e24b5e38/pypdf-5.6.0-py3-none-any.whl", hash = "sha256:ca6bf446bfb0a2d8d71d6d6bb860798d864c36a29b3d9ae8d7fc7958c59f88e7", size = 304208, upload-time = "2025-06-01T12:19:38.003Z" }, ] [[package]] @@ -3732,8 +3732,8 @@ fastembed = [ [[package]] name = "raggy" -version = "0.3.5.dev1+gb0eae24" -source = { git = "https://github.com/zzstoatzz/raggy.git#b0eae24e1c50cbe9af6c6c2e764077c4f20c9d96" } +version = "0.3.5.dev6+g5baad82" +source = { git = "https://github.com/zzstoatzz/raggy.git#5baad82b23117ab392523c0f0674c766d69a9c30" } dependencies = [ { name = "aiofiles" }, { name = "bs4" }, @@ -3747,6 +3747,7 @@ dependencies = [ { name = "tiktoken" }, { name = "trafilatura" }, { name = "turbopuffer" }, + { name = "typing-extensions" }, { name = "xxhash" }, { name = "yake" }, ] @@ -4136,6 +4137,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "slackbot" +source = { editable = "examples/slackbot" } +dependencies = [ + { name = "marvin" }, + { name = "numpy" }, + { name = "prefect" }, + { name = "raggy", extra = ["tpuf"] }, +] + +[package.metadata] +requires-dist = [ + { name = "marvin", editable = "." }, + { name = "numpy" }, + { name = "prefect" }, + { name = "raggy", extras = ["tpuf"], git = "https://github.com/zzstoatzz/raggy.git" }, +] + +[package.metadata.requires-dev] +dev = [] + [[package]] name = "smmap" version = "5.0.2"