PrefectHQ/marvin

Defining ai_fn at runtime

Opened this issue · 7 comments

First check

  • I added a descriptive title to this issue.
  • I used the GitHub search to look for a similar issue and didn't find it.
  • I searched the Marvin documentation for this feature.

Describe the current behavior

I'm working on a project where I basically want what the ai_fn is doing but I need to be able to define it at runtime. I might be missing something but that doesn't seem possible at the moment. I even tried setting the docstring through __doc__ but it just doesn't work.

Describe the proposed behavior

I think it would be nice to have the functionality of the ai_fn decorator through an actual function like cast and extract. According to the current naming conventions write() could be a good name but it might be worth discussing this a bit more.

Example Use

marvin.write("Hello, my name is Pietz.", str, "Translate the provided text to German")

Additional context

No response

hi @pietz - is there a case where cast doesn't work for you? e.g.

In [1]: import marvin

In [2]: marvin.cast("Hello, my name is Pietz.", str, "Translate the provided text to German")
Out[2]: 'Hallo, mein Name ist Pietz.'

In [3]: !marvin version
Version:		2.1.4.dev27+g8ee6b7c0
Python version:		3.12.1
OS/Arch:		darwin/arm64

Sorry I didn't mention that. I haven't looked into the prompt template of cast but the results were pretty bad for purposes outside the core idea of cast. Generating, summarizing, translating we're all kinda bad.

@zzstoatzz Yes I think a basic abstraction without a pre-defined prompt would be useful as well.
One that takes data, instructions, target and just does what the instructions ask it to do, while also making sure the result is in the target format.
Data could also be a list of messages for conversation history.
Basically a wrapper around the chat completions API with an extra target param.

@HamzaFarhan completely agree with this! It would be great to define it at runtime and also offer async.

I like the decorator syntax for the ai function. It's such a nice mental model that we define a python function but because we're dealing with LLMs, we only write the docstring and not the body. Not being able to define it at runtime, is a bummer though.

Thoughts on this?

import textwrap
from enum import Enum
from inspect import cleandoc

import marvin


class ModelName(str, Enum):
    GPT_3 = "gpt-3.5-turbo-0125"
    GPT_4 = "gpt-4-turbo-preview"


def deindent(text: str) -> str:
    return textwrap.dedent(cleandoc(text))


def message_template(message: dict[str, str]) -> str:
    return deindent(f"## {message['role'].upper()} ##\n\n{message['content']}")


def chat_template(messages: list[dict[str, str]]) -> str:
    chat = [message_template(message) for message in messages]
    return deindent("\n\n".join(chat))


def chat_message(role: str, content: str) -> dict[str, str]:
    return {"role": role, "content": content}


def user_message(content: str) -> dict[str, str]:
    return chat_message(role="user", content=content)


def assistant_message(content: str) -> dict[str, str]:
    return chat_message(role="assistant", content=content)


@marvin.fn(model_kwargs={"model": ModelName.GPT_3, "temperature": 0.5})
def assistant_response(convo: list[dict[str, str]]) -> str:
    """
    Returns the assistant response to the conversation so far.
    """
    # Returning the convo as a fromatted string gives much better results.
    return chat_template(convo)


def ask_marvin(
    messages: list[dict[str, str]] | None = None, prompt: str = ""
) -> list[dict[str, str]]:
    messages = messages or []
    if prompt:
        messages.append(user_message(prompt))
    if messages:
        messages.append(assistant_message(assistant_response(messages)))
    return messages


messages = [
    user_message("It's my first day at a new job."),
    user_message("The commute is an hour long."),
]
messages = ask_marvin(messages=messages, prompt="How should I pass the time?")

# [{'role': 'user', 'content': "It's my first day at a new job."},
#  {'role': 'user', 'content': 'The commute is an hour long.'},
#  {'role': 'user', 'content': 'How should I pass the time?'},
#  {'role': 'assistant',
#   'content': "That's exciting! You could listen to podcasts, read a book, or plan your day ahead during the commute."}]

Personally, I'm not looking for a chat interface. Maybe I misunderstood you. This is my workaround for now:

from marvin.ai.text import _generate_typed_llm_response_with_tool
from marvin.ai.prompts.text_prompts import FUNCTION_PROMPT

async def ai_function(
    instruction: str,
    inputs: dict,
    output_type: Any
):

    # Call the language model to generate the output
    result = await _generate_typed_llm_response_with_tool(
        prompt_template=FUNCTION_PROMPT,
        prompt_kwargs=dict(
            fn_definition=instruction,
            bound_parameters=inputs,
            return_value=str(output_type),  # Assuming return_annotation is a string representation
        ),
        type_=output_type
    )

    return result

It's "just" the normal marvin function but I can define it at runtime. I'm happy with my workaround. Everything else is just convenience.

can you share an example how you use this versus the ai_fn decorator? Thanks.