Skip to content

Commit

Permalink
Merge branch 'main' into extras
Browse files Browse the repository at this point in the history
  • Loading branch information
zzstoatzz authored Oct 26, 2024
2 parents bf1c9e8 + 69a35c7 commit b8a3bb7
Show file tree
Hide file tree
Showing 8 changed files with 54 additions and 49 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:

env:
MKDOCS_SOCIAL_CARDS: ${{ vars.MKDOCS_SOCIAL_CARDS }}
MKDOCS_MATERIAL_INSIDERS_REPO_RO: ${{ secrets.MKDOCS_MATERIAL_INSIDERS_REPO_RO }}
MKDOCS_MATERIAL_INSIDERS_CONTENTS_RO: ${{ secrets.MKDOCS_MATERIAL_INSIDERS_CONTENTS_RO }}

permissions:
contents: write
Expand All @@ -34,7 +34,7 @@ jobs:
run: pip install -U uv && uv venv

- name: Install Material Insiders
run: pip install git+https://oauth:${MKDOCS_MATERIAL_INSIDERS_REPO_RO}@github.com/PrefectHQ/mkdocs-material-insiders.git
run: pip install git+https://oauth:${MKDOCS_MATERIAL_INSIDERS_CONTENTS_RO}@github.com/PrefectHQ/mkdocs-material-insiders.git

# for now, only install mkdocs. In the future may need to install Marvin itself.
- name: Install dependencies for MKDocs Material
Expand All @@ -47,4 +47,4 @@ jobs:
cairosvg
- name: Build docs
run: |
mkdocs build --config-file mkdocs.insiders.yml
mkdocs build --config-file mkdocs.insiders.yml
2 changes: 1 addition & 1 deletion .github/workflows/publish-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

env:
MKDOCS_SOCIAL_CARDS: ${{ vars.MKDOCS_SOCIAL_CARDS }}
MKDOCS_MATERIAL_INSIDERS_REPO_RO: ${{ secrets.MKDOCS_MATERIAL_INSIDERS_REPO_RO }}
MKDOCS_MATERIAL_INSIDERS_REPO_RO: ${{ secrets.MKDOCS_MATERIAL_INSIDERS_CONTENTS_RO }}

permissions:
contents: write
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

Marvin is a lightweight AI toolkit for building natural language interfaces that are reliable, scalable, and easy to trust.

Each of Marvin's tools is simple and self-documenting, using AI to solve common but complex challenges like entity extraction, classification, and generating synthetic data. Each tool is independent and incrementally adoptable, so you can use them on their own or in combination with any other library. Marvin is also multi-modal, supporting both image and audio generation as well using images as inputs for extraction and classification.
Each of Marvin's tools is simple and self-documenting, using AI to solve common but complex challenges like entity extraction, classification, and generating synthetic data. Each tool is independent and incrementally adoptable, so you can use them on their own or in combination with any other library. Marvin is also multi-modal, supporting both image and audio generation as well as using images as inputs for extraction and classification.

Marvin is for developers who care more about _using_ AI than _building_ AI, and we are focused on creating an exceptional developer experience. Marvin users should feel empowered to bring tightly-scoped "AI magic" into any traditional software project with just a few extra lines of code.

Expand Down
34 changes: 26 additions & 8 deletions cookbook/slackbot/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
)
from marvin.utilities.strings import count_tokens, slice_tokens
from prefect import flow, task
from prefect.blocks.notifications import SlackWebhook
from prefect.states import Completed
from prefect.variables import Variable
from tools import (
get_info,
get_latest_prefect_release_notes,
search_prefect_2x_docs,
search_prefect_3x_docs,
)
Expand All @@ -41,10 +42,10 @@ def engage_marvin_bot(instructions: str, model: str):
model=model,
name="Marvin (from Hitchhiker's Guide to the Galaxy)",
tools=[
get_latest_prefect_release_notes,
search_prefect_2x_docs,
search_prefect_3x_docs,
search_github_issues,
get_info,
],
instructions=instructions,
) as ai:
Expand Down Expand Up @@ -126,14 +127,30 @@ async def handle_message(payload: SlackPayload):

@app.post("/chat")
async def chat_endpoint(request: Request):
payload = SlackPayload(**await request.json())
try:
payload = SlackPayload(**await request.json())
except Exception as e:
logger.error(f"Error parsing Slack payload: {e}")
slack_webhook = await SlackWebhook.load("marvin-bot-pager")
await slack_webhook.notify( # type: ignore
body=f"Error parsing Slack payload: {e}",
subject="Slackbot Error",
)
raise HTTPException(400, "Invalid event type")
match payload.type:
case "event_callback":
options = dict(
flow_run_name=(
"respond in"
f" {await get_channel_name(payload.event.channel)}/{payload.event.thread_ts}"
channel_name = await get_channel_name(payload.event.channel)
if channel_name.startswith("D"):
# This is a DM channel, we should not respond
logger.warning(f"Attempted to respond in DM channel: {channel_name}")
slack_webhook = await SlackWebhook.load("marvin-bot-pager")
await slack_webhook.notify(
body=f"Attempted to respond in DM channel: {channel_name}",
subject="Slackbot DM Warning",
)
return Completed(message="Skipped DM channel", name="SKIPPED")
options = dict(
flow_run_name=f"respond in {channel_name}/{payload.event.thread_ts}"
)
asyncio.create_task(handle_message.with_options(**options)(payload))
case "url_verification":
Expand All @@ -146,6 +163,7 @@ async def chat_endpoint(request: Request):

if __name__ == "__main__":
if not os.getenv("OPENAI_API_KEY", None): # TODO: Remove this
os.environ["OPENAI_API_KEY"] = marvin.settings.openai.api_key.get_secret_value()
assert (api_key := marvin.settings.openai.api_key), "OPENAI_API_KEY not set"
os.environ["OPENAI_API_KEY"] = api_key.get_secret_value()

uvicorn.run(app, host="0.0.0.0", port=4200)
36 changes: 7 additions & 29 deletions cookbook/slackbot/tools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import inspect
from typing import Literal

import httpx
Expand Down Expand Up @@ -50,35 +49,14 @@ async def search_prefect_3x_docs(queries: list[str]) -> str:
return await multi_query_tpuf(queries, namespace="prefect-3")


async def get_latest_release_notes() -> str:
"""Gets the first whole h2 section from the Prefect RELEASE_NOTES.md file."""
async with httpx.AsyncClient() as client:
response = await client.get(
"https://raw.githubusercontent.com/PrefectHQ/prefect/main/RELEASE-NOTES.md"
)
return response.text.split("\n## ")[1]


tool_map = {"latest_prefect_version": get_latest_release_notes}

async def get_latest_prefect_release_notes() -> str:
"""Gets the latest Prefect release notes"""
url = "https://api.github.com/repos/PrefectHQ/prefect/releases/latest"

@task
async def get_info(topic: Topic) -> str:
"""A tool that returns information about a topic using
one of many pre-existing helper functions. You need only
provide the topic name, and the appropriate function will
return information.
As of now, the only topic is "latest_prefect_version".
"""

try:
maybe_coro = tool_map[topic]()
if inspect.iscoroutine(maybe_coro):
return await maybe_coro # type: ignore
return maybe_coro # type: ignore
except KeyError:
raise ValueError(f"Invalid topic: {topic}")
async with httpx.AsyncClient() as client:
response = await client.get(url)
release_notes = response.json().get("body")
return release_notes


async def get_prefect_code_example(related_to: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion docs/ai_style_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ First, here's a style guide.

# AI Style Guide

A style guide for AI documentation authors to adhere to Marvin's documentation standards. Remember, you are an expert technical writer with an extensive background in educating and explaining open-source software. You are not a marketer, a salesperson, or a product manager. Marvin's documentation should resemble renowed technical documentation like Stripe.
A style guide for AI documentation authors to adhere to Marvin's documentation standards. Remember, you are an expert technical writer with an extensive background in educating and explaining open-source software. You are not a marketer, a salesperson, or a product manager. Marvin's documentation should resemble renowned technical documentation like Stripe.

You must follow the below guide. Do not deviate from it.

Expand Down
2 changes: 1 addition & 1 deletion src/marvin/ai/prompts/text_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
- When providing a string response, do not return JSON or a quoted string
unless they provided instructions requiring it. If you do return JSON, it
must be valid and parseable including double quotes.
- When converting too bool, treat "truthy" values as true
- When converting to bool, treat "truthy" values as true
"""
)

Expand Down
19 changes: 14 additions & 5 deletions src/marvin/utilities/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import os
import re
from typing import List, Optional, Union
from typing import Any, Dict, List, Optional, Union

import httpx
from pydantic import BaseModel, field_validator
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator

import marvin

Expand All @@ -31,7 +31,7 @@ class SlackEvent(BaseModel):
client_msg_id: Optional[str] = None
type: str
text: Optional[str] = None
user: Optional[str] = None
user: Union[str, Dict[str, Any], None] = None
ts: Optional[str] = None
team: Optional[str] = None
channel: Optional[str] = None
Expand All @@ -40,6 +40,13 @@ class SlackEvent(BaseModel):
parent_user_id: Optional[str] = None
blocks: Optional[List[EventBlock]] = None

@model_validator(mode="before")
@classmethod
def extract_user_id(cls, data: Dict[str, Any]) -> Dict[str, Any]:
if isinstance(data.get("user"), dict):
data["user"] = data["user"].get("id")
return data


class EventAuthorization(BaseModel):
enterprise_id: Optional[str] = None
Expand All @@ -63,8 +70,10 @@ class SlackPayload(BaseModel):
challenge: Optional[str] = None

@field_validator("event")
def validate_event(cls, v: Optional[SlackEvent]) -> Optional[SlackEvent]:
if v.type != "url_verification" and v is None:
def validate_event(
cls, v: Optional[SlackEvent], info: ValidationInfo
) -> Optional[SlackEvent]:
if v is None and info.data.get("type") != "url_verification":
raise ValueError("event is required")
return v

Expand Down

0 comments on commit b8a3bb7

Please sign in to comment.