Skip to content

Commit

Permalink
Merge branch 'main' into docstring-glob
Browse files Browse the repository at this point in the history
  • Loading branch information
zzstoatzz authored Aug 10, 2024
2 parents cb61bf7 + fda94eb commit 30f7d37
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 186 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ Learn more about casting to types [here](https://askmarvin.ai/docs/text/transfor

## 🦾 Build AI-powered functions

Marvin functions let you combine any inputs, instructions, and output types to create custom AI-powered behaviors... without source code. These functions can can go well beyond the capabilities of `extract` or `classify`, and are ideal for complex natural language processing or mapping combinations of inputs to outputs.
Marvin functions let you combine any inputs, instructions, and output types to create custom AI-powered behaviors... without source code. These functions can go well beyond the capabilities of `extract` or `classify`, and are ideal for complex natural language processing or mapping combinations of inputs to outputs.

```python
@marvin.fn
Expand Down
290 changes: 126 additions & 164 deletions cookbook/maze.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cookbook/slackbot/Dockerfile.slackbot
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ RUN uv venv

ENV PATH="$VIRTUAL_ENV/bin:$PATH"

RUN uv pip install ".[slackbot]"
RUN uv pip install -U ".[slackbot]" --force-reinstall

EXPOSE 4200

Expand Down
17 changes: 9 additions & 8 deletions cookbook/slackbot/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,21 @@ async def handle_message(payload: SlackPayload) -> Completed:
],
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."
" 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"
" Galaxy. You are an expert in Python, data engineering, and software development."
" Your primary job is to use tools to search docs and github issues for users, in"
" order to develop a coherent attempt to answer their questions."
" You _must_ rely on your tools, as Prefect is developed quickly and you have no"
" prior experience with newest versions. 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."
" prioritize brevity in your responses, and format text prettily for Slack (no markdown)."
" Bold things should be wrapped in (SINGLE!) asterisks and italics in (SINGLE!) underscores."
" THIS IS NOT MARKDOWN, WE ARE IN SLACK, SO DO NOT USE MARKDOWN."
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."
" If a tool returns an irrelevant/bad result, you should try another tool."
" KEEP IN MIND that agents are deprecated in favor of workers, so you should"
" never recommend `prefect agent` commands, suggest `prefect worker` instead."
" that goes for `build_from_flow` too, instead use `Flow.deploy()`"
),
) as ai:
logger.debug_kv(
Expand Down
6 changes: 5 additions & 1 deletion docs/docs/text/classification.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ Categorize user feedback into labels such as "bug", "feature request", or "inqui
<div class="admonition info">
<p class="admonition-title">How it works</p>
<p>
Marvin enumerates your options, and uses a <a href="https://twitter.com/AAAzzam/status/1669753721574633473">clever logit bias trick</a> to force the LLM to deductively choose the index of the best option given your provided input. It then returns the choice associated with that index.
Marvin enumerates your options, and uses a <strong>clever logit bias</strong> trick to force the LLM to deductively choose the index of the best option given your provided input. It then returns the choice associated with that index.
</p>
<p class="admonition-title">Logit Bias Trick</p>
<p>
You can configure ChatGPT as a logic gate or classifier by manipulating its token outputs using <strong>logit_bias</strong> and <strong>max_tokens</strong>. For a logic gate, set true to `1904` and false to `3934`, and restrict responses to these tokens with logit_bias and max_tokens set to 1. Similarly, for classification tasks, assign tokens for labels (e.g., 57621 for happy, 83214 for sad, and 20920 for mad) and use logit_bias to restrict outputs to these tokens. By setting max_tokens to 1, you ensure that the model will only output the predefined class labels.
</p>
</div>

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ audio = [
]
video = ["opencv-python >= 4.5"]

slackbot = ["marvin[prefect]", "numpy", "raggy", "turbopuffer"]
slackbot = ["marvin[prefect]", "numpy", "raggy", "turbopuffer==0.1.15"]

[project.urls]
Code = "https://github.com/prefecthq/marvin"
Expand Down
4 changes: 3 additions & 1 deletion src/marvin/ai/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ def transcribe(
This function converts audio from a file to text.
"""
return run_sync(transcribe_async(data=data, prompt=prompt, **model_kwargs or {}))
return run_sync(
transcribe_async(data=data, prompt=prompt, model_kwargs=model_kwargs or {})
)


def speech(
Expand Down
2 changes: 1 addition & 1 deletion src/marvin/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def stop(self, wait: bool = True):
if wait:
self._thread.join()
logger.info("Recording finished.")
self._is_recording = False
self.is_recording = False


class BackgroundAudioStream:
Expand Down
6 changes: 4 additions & 2 deletions src/marvin/beta/assistants/assistants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union

from openai import AsyncAssistantEventHandler
from prompt_toolkit import PromptSession
Expand All @@ -13,7 +13,7 @@
import marvin.utilities.tools
from marvin.beta.assistants.handlers import PrintHandler
from marvin.tools.assistants import AssistantTool
from marvin.types import Tool
from marvin.types import AssistantResponseFormat, Tool
from marvin.utilities.asyncio import (
ExposeSyncMethodsMixin,
expose_sync_method,
Expand Down Expand Up @@ -64,6 +64,7 @@ class Assistant(BaseModel, ExposeSyncMethodsMixin):
tools: list[Union[AssistantTool, Callable]] = []
tool_resources: dict[str, Any] = {}
metadata: dict[str, str] = {}
response_format: Optional[Union[Literal["auto"], AssistantResponseFormat]] = "auto"
# context level tracks nested assistant contexts
_context_level: int = PrivateAttr(0)

Expand Down Expand Up @@ -173,6 +174,7 @@ async def create_async(self, _auto_delete: bool = False):
"metadata",
"tool_resources",
"metadata",
"response_format",
}
),
tools=[tool.model_dump() for tool in self.get_tools()],
Expand Down
5 changes: 4 additions & 1 deletion src/marvin/beta/assistants/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ def download_temp_file(file_id: str):


def format_timestamp(timestamp: int) -> str:
return datetime.fromtimestamp(timestamp).strftime("%l:%M:%S %p")
"""Outputs timestamp as a string in 12 hour format. Hours are left-padded with space instead of zero."""
return (
datetime.fromtimestamp(timestamp).strftime("%I:%M:%S %p").lstrip("0").rjust(11)
)


def create_panel(content: Any, title: str, timestamp: int, color: str):
Expand Down
17 changes: 16 additions & 1 deletion src/marvin/beta/assistants/runs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Callable, Optional, Union
from typing import Any, Callable, Literal, Optional, Union

from openai import AsyncAssistantEventHandler
from openai.types.beta.threads import Message
Expand Down Expand Up @@ -34,6 +34,8 @@ class Run(BaseModel, ExposeSyncMethodsMixin):
for the run.
additional_tools (list[AssistantTool], optional): Additional tools to append
to the assistant's tools.
tool_choice (Union[Literal["auto", "none", "required"], AssistantTool], optional):
The tool use behaviour for the run.
run (OpenAIRun): The OpenAI run object.
data (Any): Any additional data associated with the run.
"""
Expand Down Expand Up @@ -67,6 +69,12 @@ class Run(BaseModel, ExposeSyncMethodsMixin):
None,
description="Additional tools to append to the assistant's tools. ",
)
tool_choice: Optional[
Union[Literal["none", "auto", "required"], AssistantTool]
] = Field(
default=None,
description="The tool use behaviour for the run. Can be 'none', 'auto', 'required', or a specific tool.",
)
run: OpenAIRun = Field(None, repr=False)
data: Any = None

Expand Down Expand Up @@ -154,6 +162,13 @@ def _get_run_kwargs(self, thread: Thread = None, **run_kwargs) -> dict:
if model := self._get_model():
run_kwargs["model"] = model

if tool_choice := self.tool_choice:
run_kwargs["tool_choice"] = (
tool_choice.model_dump(mode="json")
if isinstance(tool_choice, Tool)
else tool_choice
)

return run_kwargs

async def get_tool_outputs(self, run: OpenAIRun) -> list[str]:
Expand Down
11 changes: 10 additions & 1 deletion src/marvin/beta/assistants/threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ async def add_async(
role: str = "user",
code_interpreter_files: Optional[list[str]] = None,
file_search_files: Optional[list[str]] = None,
image_files: Optional[list[str]] = None,
) -> Message:
"""
Add a user message to the thread.
Expand All @@ -76,6 +77,8 @@ async def add_async(
if self.id is None:
await self.create_async()

content = [dict(text=message, type="text")]

# Upload files and collect their IDs
attachments = []
for fp in code_interpreter_files or []:
Expand All @@ -90,10 +93,16 @@ async def add_async(
attachments.append(
dict(file_id=response.id, tools=[dict(type="file_search")])
)
for fp in image_files or []:
with open(fp, mode="rb") as file:
response = await client.files.create(file=file, purpose="vision")
content.append(
dict(image_file=dict(file_id=response.id), type="image_file")
)

# Create the message with the attached files
response = await client.beta.threads.messages.create(
thread_id=self.id, role=role, content=message, attachments=attachments
thread_id=self.id, role=role, content=content, attachments=attachments
)
return response

Expand Down
12 changes: 12 additions & 0 deletions src/marvin/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ class FunctionCall(MarvinType):
name: str


class AssistantResponseFormat(MarvinType):
type: Literal["json_object", "text"]


class JsonObjectAssistantResponseFormat(AssistantResponseFormat):
type: Literal["json_object"] = "json_object"


class TextAssistantResponseFormat(AssistantResponseFormat):
type: Literal["text"] = "text"


class ImageUrl(MarvinType):
url: str = Field(
description="URL of the image to be sent or a base64 encoded image."
Expand Down
12 changes: 10 additions & 2 deletions src/marvin/utilities/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class PythonFunction(BaseModel):
return_annotation: Optional[Any] = Field(
None, description="Return annotation of the function."
)
source_code: str = Field(description="Source code of the function")
source_code: Optional[str] = Field(None, description="Source code of the function")
bound_parameters: dict[str, Any] = Field(
{}, description="Parameters bound with values"
)
Expand Down Expand Up @@ -86,7 +86,15 @@ def from_function(cls, func: Callable, **kwargs) -> "PythonFunction":
)
for name, param in sig.parameters.items()
]
source_code = inspect.getsource(func).strip()

try:
source_code = inspect.getsource(func).strip()
except OSError as e:
error_message = str(e)
if "source code" in error_message:
source_code = None
else:
raise

function_dict = {
"function": func,
Expand Down
2 changes: 1 addition & 1 deletion tests/beta/assistants/test_assistants.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class TestTools:
def test_code_interpreter(self):
ai = Assistant(tools=[CodeInterpreter])
run = ai.say(
"use the code interpreter to compute a random number between 85 and 95"
"use the code interpreter to output a random number between 85 and 95 - you must use print"
)
assert run.steps[0].step_details.tool_calls[0].type == "code_interpreter"
output = float(
Expand Down

0 comments on commit 30f7d37

Please sign in to comment.