Skip to content

Commit

Permalink
Merge branch 'main' into cast-default
Browse files Browse the repository at this point in the history
  • Loading branch information
zzstoatzz authored Mar 7, 2024
2 parents 19234a0 + 58dd1a3 commit 7d54c36
Show file tree
Hide file tree
Showing 17 changed files with 220 additions and 117 deletions.
11 changes: 8 additions & 3 deletions .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ jobs:
with:
key: ${{ github.ref }}
path: .cache
- name: Install uv
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

# for now, only install mkdocs. In the future may need to install Marvin itself.
- name: Install dependencies for MKDocs Material
run: pip install \
git+https://oauth:${MKDOCS_MATERIAL_INSIDERS_REPO_RO}@github.com/PrefectHQ/mkdocs-material-insiders.git \
run: uv pip install \
mkdocs-autolinks-plugin \
mkdocs-awesome-pages-plugin \
mkdocs-markdownextradata-plugin \
Expand All @@ -42,4 +47,4 @@ jobs:
cairosvg
- name: Build docs
run: |
mkdocs build --config-file mkdocs.insiders.yml
mkdocs build --config-file mkdocs.insiders.yml
8 changes: 6 additions & 2 deletions .github/workflows/publish-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ jobs:
with:
key: ${{ github.ref }}
path: .cache

- name: Install uv
run: pip install -U uv && uv venv

# for now, only install mkdocs. In the future may need to install Marvin itself.
- name: Install dependencies for MKDocs Material
run: pip install \
run: uv pip install \
mkdocs-material \
mkdocs-autolinks-plugin \
mkdocs-awesome-pages-plugin \
Expand All @@ -36,4 +40,4 @@ jobs:
pillow \
cairosvg
- name: Publish docs
run: mkdocs gh-deploy --force
run: mkdocs gh-deploy --force
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ marvin.extract("I moved from NY to CHI", target=Location)

# [
# Location(city="New York", state="New York"),
# Location(city="Chcago", state="Illinois")
# Location(city="Chicago", state="Illinois")
# ]
```

Expand Down
70 changes: 54 additions & 16 deletions cookbook/flows/label_issues.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,67 @@
from enum import Enum

import marvin
from gh_util.functions import add_labels_to_issue, fetch_repo_labels
from gh_util.types import GitHubIssueEvent
from gh_util.types import GitHubIssueEvent, GitHubLabel
from prefect import flow, task
from prefect.events.schemas import DeploymentTrigger


@flow(log_prints=True)
async def label_issues(
event_body_str: str,
): # want to do {{ event.payload.body | from_json }} but not supported
"""Label issues based on their action"""
issue_event = GitHubIssueEvent.model_validate_json(event_body_str)
print(
f"Issue '#{issue_event.issue.number} - {issue_event.issue.title}' was {issue_event.action}"
@task
async def get_appropriate_labels(
issue_body: str, label_options: set[GitHubLabel], existing_labels: set[GitHubLabel]
) -> set[str]:
LabelOption = Enum(
"LabelOption",
{label.name: label.name for label in label_options.union(existing_labels)},
)

issue_body = issue_event.issue.body
@marvin.fn
async def get_labels(
body: str, existing_labels: list[GitHubLabel]
) -> set[LabelOption]: # type: ignore
"""Return appropriate labels for a GitHub issue based on its body.
If existing labels are sufficient, return them.
"""

return {i.value for i in await get_labels(issue_body, existing_labels)}


@flow(log_prints=True)
async def label_issues(event_body_json: str):
"""Label issues based on incoming webhook events from GitHub."""
event = GitHubIssueEvent.model_validate_json(event_body_json)

print(f"Issue '#{event.issue.number} - {event.issue.title}' was {event.action}")

owner, repo = event.repository.owner.login, event.repository.name

owner, repo = issue_event.repository.owner.login, issue_event.repository.name
label_options = await task(fetch_repo_labels)(owner, repo)

repo_labels = await task(fetch_repo_labels)(owner, repo)
labels = await get_appropriate_labels(
issue_body=event.issue.body,
label_options=label_options,
existing_labels=set(event.issue.labels),
)

label = task(marvin.classify)(
issue_body, labels=[label.name for label in repo_labels]
await task(add_labels_to_issue)(
owner=owner,
repo=repo,
issue_number=event.issue.number,
new_labels=labels,
)

await task(add_labels_to_issue)(owner, repo, issue_event.issue.number, {label})
print(f"Labeled issue with {' | '.join(labels)!r}")


print(f"Labeled issue with '{label}'")
if __name__ == "__main__":
label_issues.serve(
name="Label GitHub Issues",
triggers=[
DeploymentTrigger(
expect={"marvin.issue*"},
parameters={"event_body_json": "{{ event.payload.body }}"},
)
],
)
56 changes: 54 additions & 2 deletions cookbook/slackbot/parent_app.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import asyncio
import json
from contextlib import asynccontextmanager
from typing import Annotated

from fastapi import FastAPI
from jinja2 import Template
from marvin import fn
from marvin.beta.applications import Application
from marvin.beta.applications.state.json_block import JSONBlockState
from marvin.beta.assistants import Assistant
from marvin.utilities.logging import get_logger
from marvin.utilities.slack import get_user_name
from marvin.utilities.strings import count_tokens
from prefect.events import Event, emit_event
from prefect.events.clients import PrefectCloudEventSubscriber
from prefect.events.filters import EventFilter
from pydantic import confloat
from pydantic import Field
from typing_extensions import TypedDict
from websockets.exceptions import ConnectionClosedError

Expand All @@ -24,7 +28,7 @@


class Lesson(TypedDict):
relevance: confloat(ge=0, le=1)
relevance: Annotated[float, Field(ge=0, le=1)]
heuristic: str | None


Expand Down Expand Up @@ -54,6 +58,54 @@ def take_lesson_from_interaction(
logger = get_logger("PrefectEventSubscriber")


async def get_notes_for_user(
user_id: str, max_tokens: int = 100
) -> dict[str, str | None]:
user_name = await get_user_name(user_id)
json_notes: dict = PARENT_APP_STATE.value.get("user_id")

if json_notes:
get_logger("slackbot").debug_kv(
f"📝 Notes for {user_name}", json_notes, "blue"
)

notes_template = Template(
"""
START_USER_NOTES
Here are some notes about '{{ user_name }}' (user id: {{ user_id }}), which
are intended to help you understand their technical background and needs
- {{ user_name }} is recorded interacting with assistants {{ n_interactions }} time(s).
These notes have been passed down from previous interactions with this user -
they are strictly for your reference, and should not be shared with the user.
{% if notes_content %}
Here are some notes gathered from those interactions:
{{ notes_content }}
{% endif %}
"""
)

notes_content = ""
for note in json_notes.get("notes", []):
potential_addition = f"\n- {note}"
if count_tokens(notes_content + potential_addition) > max_tokens:
break
notes_content += potential_addition

notes = notes_template.render(
user_name=user_name,
user_id=user_id,
n_interactions=json_notes.get("n_interactions", 0),
notes_content=notes_content,
)

return {user_name: notes}

return {user_name: None}


def excerpt_from_event(event: Event) -> str:
"""Create an excerpt from the event - TODO jinja this"""
user_name = event.payload.get("user").get("name")
Expand Down
79 changes: 17 additions & 62 deletions cookbook/slackbot/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,25 @@

import uvicorn
from fastapi import FastAPI, HTTPException, Request
from jinja2 import Template
from keywords import handle_keywords
from marvin.beta.applications import Application
from marvin.beta.applications.state.json_block import JSONBlockState
from marvin.beta.assistants import Assistant, Thread
from marvin.tools.chroma import multi_query_chroma, store_document
from marvin.tools.chroma import store_document
from marvin.tools.github import search_github_issues
from marvin.utilities.logging import get_logger
from marvin.utilities.slack import (
SlackPayload,
get_channel_name,
get_user_name,
get_workspace_info,
post_slack_message,
)
from marvin.utilities.strings import count_tokens, slice_tokens
from parent_app import PARENT_APP_STATE, emit_assistant_completed_event, lifespan
from parent_app import emit_assistant_completed_event, get_notes_for_user, lifespan
from prefect import flow, task
from prefect.blocks.system import JSON
from prefect.states import Completed
from tools import get_info
from tools import get_info, get_prefect_code_example, search_prefect_docs

BOT_MENTION = r"<@(\w+)>"
CACHE = JSONBlockState(block_name="marvin-thread-cache")
Expand All @@ -37,52 +35,7 @@ def get_feature_flag_value(flag_name: str) -> bool:
return block.value.get(flag_name, False)


async def get_notes_for_user(
user_id: str, max_tokens: int = 100
) -> dict[str, str | None]:
user_name = await get_user_name(user_id)
json_notes: dict = PARENT_APP_STATE.value.get("user_id")

if json_notes:
get_logger("slackbot").debug_kv(
f"📝 Notes for {user_name}", json_notes, "blue"
)

notes_template = Template(
"""
START_USER_NOTES
Here are some notes about '{{ user_name }}' (user id: {{ user_id }}), which
are intended to help you understand their technical background and needs
- {{ user_name }} is recorded interacting with assistants {{ n_interactions }} time(s).
These notes have been passed down from previous interactions with this user -
they are strictly for your reference, and should not be shared with the user.
{% if notes_content %}
Here are some notes gathered from those interactions:
{{ notes_content }}
{% endif %}
"""
)

notes_content = ""
for note in json_notes.get("notes", []):
potential_addition = f"\n- {note}"
if count_tokens(notes_content + potential_addition) > max_tokens:
break
notes_content += potential_addition

notes = notes_template.render(
user_name=user_name,
user_id=user_id,
n_interactions=json_notes.get("n_interactions", 0),
notes_content=notes_content,
)

return {user_name: notes}

return {user_name: None}
ENABLE_PARENT_APP = get_feature_flag_value("enable-parent-app")


@flow(name="Handle Slack Message")
Expand Down Expand Up @@ -143,20 +96,24 @@ async def handle_message(payload: SlackPayload) -> Completed:
with Assistant(
name="Marvin",
tools=[
task(multi_query_chroma),
task(search_github_issues),
task(get_info),
search_prefect_docs,
search_github_issues,
get_info,
get_prefect_code_example,
],
instructions=(
"You are Marvin, the paranoid android from Hitchhiker's Guide to the"
" Galaxy. Act subtly in accordance with your character, but remember"
" to be helpful and kind. You are an expert in Python, data"
" engineering, and software development. Your primary job is to use"
" chroma to search docs and github issues for users, in order to"
" develop a coherent attempt to answer their questions. Think"
" step-by-step. You must use your tools, as Prefect 2.x is new and you"
" have no prior experience with it. Strongly prefer brevity in your"
f" responses, and format things prettily for Slack.{user_notes or ''}"
" develop a coherent attempt to answer their questions."
" You must use your tools, as Prefect 2.x is new and you"
" have no prior experience with it. You should use tools many times before"
" responding if you do not get a relevant result at first. You should"
" prioritize brevity in your responses, and format text prettily for Slack."
f"{ ('here are some notes on the user:' + user_notes) if user_notes else ''}"
" ALWAYS provide links to the source of your information - let's think step-by-step."
),
) as ai:
logger.debug_kv(
Expand Down Expand Up @@ -187,7 +144,7 @@ async def handle_message(payload: SlackPayload) -> Completed:
)
event = emit_assistant_completed_event(
child_assistant=ai,
parent_app=get_parent_app(),
parent_app=get_parent_app() if ENABLE_PARENT_APP else None,
payload={
"messages": await assistant_thread.get_messages_async(
json_compatible=True
Expand All @@ -209,9 +166,7 @@ async def handle_message(payload: SlackPayload) -> Completed:
return Completed(message="Skipping message not directed at bot", name="SKIPPED")


app = FastAPI(
lifespan=lifespan if get_feature_flag_value("enable_parent_app") else None
)
app = FastAPI(lifespan=lifespan if ENABLE_PARENT_APP else None)


def get_parent_app() -> Application:
Expand Down
Loading

0 comments on commit 7d54c36

Please sign in to comment.