跳转到主要内容

定义由GPT驱动的函数的装饰器

项目描述

gpt-function-decorator

PyPI Tests Changelog License

此库提供了一种装饰器,可以定义由ChatGPT“运行”的无代码函数

from gpt_function_decorator import gpt_function

@gpt_function
def format_date(date):
    """Format the date as yyyy-mm-dd"""

# And just like that, you have a new python function:

format_date("December 9, 1992.") # returns '1992-12-09'
format_date("On May the 4th 1979") # returns '1992-05-04'
format_date("12/31/2008.") # returns '2008-12-31'

这是一个具有更结构化输出的示例

from gpt_function_decorator import gpt_function

@gpt_function
def deduplicate_celebrities(names) -> list[str]:
    """Return the deduplicated version of the celebrities list."""

celebrities = [
    "Leo Messi",
    "Mozart",
    "W. A. Mozart",
    "Lionel Messi",
    "Leo diCaprio"
]
answer = deduplicate_celebrities(celebrities, gpt_reasoning=true)

print (answer)
# ['Leo Messi', 'Mozart', 'Leo diCaprio']

print (answer.__reasoning__)
# `Leo Messi` and `Lionel Messi` refer to the same person,
# and `Mozart` and `W. A. Mozart` also refer to the same individual.
# We include `Leo diCaprio` as it is a distinct name.

此库依赖于OpenAI的新结构化输出功能,该功能允许复杂的输出架构。GPT对于复杂和开放式的任务可能确实不可靠,但在正确的用例中,它们可以替代数小时的脚本编写和数百行代码。

感谢Marvin

在一个经典的“哎呀,我没有意识到已经有库做这个了”的案例中,我在发布gpt_function_decorator之后才发现,另一个库marvin已经有一个等效功能超过一年了

@marvin.fn
def sentiment(text: str) -> float:
    """Returns a sentiment score for `text`
    between -1 (negative) and 1 (positive).
    """

《marvin》的一个优势是强制执行输出架构的可能性,然而,现在我们从OpenAI API中免费获得了这个功能。相比之下,利用新OpenAI功能的《gpt_function》要轻量得多(它只依赖于openai,核心逻辑约为50行代码),并提供额外的实用功能,如自动关键字参数和推理答案。

安装和设置

使用pip安装此库

pip install gpt-function-decorator

此软件包需要一个OpenAI API密钥。要获取一个,您需要创建一个账户,向您的账户充值(2美元可以使用很长时间),生成一个API密钥,并将其存储到环境变量OPENAI_API_KEY中(请参阅OpenAI文档)。

然后,密钥将由 gpt_function 自动检测。如果您或您的用户需要在不同 API 密钥、项目等之间切换 OpenAI 客户端,您可以在执行过程中的任何位置使用以下方法覆盖 gpt_function 的 OpenAI 客户端:

import gpt_function_decorator
from openai import OpenAI
...
gpt_function_decorator.SETTINGS["openai_client"] = OpenAI(api_key="...", ...)

使用方法

基础

导入装饰器并将其应用于一个函数,该函数的文档字符串解释了如何使用参数。文档字符串可以

from gpt_function_decorator import gpt_function

@gpt_function
def synonym(word, tone='formal'):
    """Return a synonym of the word with the given tone."""

# Let's try it!

synonym("man", tone="slang") # returns "dude"
synonym("man", tone="formal") # returns "male" or "gentleman"
synonym("man", tone="academic") # returns "individual" or "male"

文档字符串可以是任何正常的 Python 文档字符串。只要写得很好,较长的文档字符串不仅可以使您的函数更加用户友好,还可以被 GPT 用于更好地理解您的预期输入、输出和输出(GPT 喜欢被提供可以用来模型化其输出的示例)

@gpt_function
def species(breed):
    """Return the species for the given breed.
    
    Parameters
    ----------
    breed:
       The name of the breed.

    Examples
    --------
    >>> species("Fox Terrier") # Returns "dog"
    >>> species("Eagle") # Returns "bird"
    """

species("German Shepard") # Returns "dog"
species("Siamese") # Returns "cat"
species("Black widow") # Returns "spider"

格式化文档字符串

这是一个为懒惰的人准备的库,作为一个选项,您可以将文档字符串写成下面这样,以便将 {括号} 字段替换为函数的参数。在某些情况下,这可以帮助 ChatGPT,因为它将呈现一个更短、更有针对性的提示。

@gpt_function
def find_rhyme(reference_word, topic):
    """Return a word related to {topic} that rhymes with {reference_word}"""
    
find_rhyme("boat", topic="space exploration") # returns "remote"

在文档字符串格式化中未转换的任何参数都将作为单独的参数传递给函数。在下面的示例中,大多数参数都传递在第一句话中(它流畅),但文本将单独传递。

@gpt_function
def find_words_in_text(text, categories, max=3) -> list[str]:
    """Return at most {max} words of the text that relate to {categories}"""

# Call:
text = "The sailor's dog and cat ate a basket of apples and biscuits"
find_words_in_text(text, categories=["animal", "food"])

# Returns:
['dog', 'cat', 'apples']

基本输出格式化

默认情况下,使用 @gpt_function 装饰的函数返回一个字符串,但您可以在函数中指定返回类型,通常使用 -> 提示

@gpt_function
def positivity(sentence) -> int:
    """Return the positivity of "{sentence}" on a 0-100 scale"""

positivity("I am desperate") # returns 10
positivity("Everyone was stoked!!!") # returns 90

列表应始终声明其元素类型(例如 list[str]

@gpt_function
def list_famous_composers(n) -> list[str]:
    "Return the {n} most famous composers."
    
list_famous_composers(20)
# Returns ['Johann Sebastian Bach',  'Ludwig van Beethoven', ...]

(无耻的广告:如果您喜欢古典音乐,我在此函数和一些其他由 ChatGPT 驱动的函数之上建立了一个 GPT 自动化网站

使用 Pydantic 的高级输出格式化

OpenAI API 似乎不太喜欢像 tuple 这样的类型,并且会拒绝 Dict 类型,因为它不知道应该使用哪些键名。如果您想以最小化的模板指定 Dict 输出,可以使用 TypedDict

from typing_extensions import TypedDict # or just "typing", for Python>=3.12

@gpt_function
def first_us_presidents(n) -> list[TypedDict("i", dict(birth=int, name=str))]:
    """Return the {n} first US presidents with their birth year"""

first_us_presidents(3)
# [{'birth': 1732, 'name': 'George Washington'},
#  {'birth': 1735, 'name': 'John Adams'},
#  {'birth': 1751, 'name': 'Thomas Jefferson'}]

但最干净的方法(也是 OpenAI 正式支持的)是提供一个 Pydantic 模型作为类型

from pydantic import BaseModel

class USPresident(BaseModel):
    birth: int
    name: str
    

@gpt_function
def first_us_presidents(n) -> list[USPresident]:
    """Return the {n} first US presidents with their birth year"""

first_us_presidents(3)
# [USPresident(birth=1732, name='George Washington'),
#  USPresident(birth=1735, name='John Adams'),
#  USPresident(birth=1743, name='Thomas Jefferson')]

使用 Pydantic 模型,您可以拥有嵌套和复杂程度如意的输出模式(参见 文档),尽管看起来您会让 GPT 更难理解如何填充模式,这需要更长的时间(不确定成本)。

使用 Pydantic 字段指定输出

最后,@gpt_function 装饰器还将任何 Pydantic 字段描述传递给 GPT,这是一种为输出每个元素提供更多规范的好方法

from pydantic import BaseModel, Field

class USPresident(BaseModel):
    birth_date: str = Field(description="date in yyyy-mm-dd")
    name: str = Field(description="Family name")

@gpt_function
def first_us_presidents(n) -> list[USPresident]:
    """Return the {n} first US presidents with their birth date"""

first_us_presidents(3)
# [USPresident(birth_date='1732-02-22', name='Washington'),
#  USPresident(birth_date='1735-10-30', name='Adams'),
#  USPresident(birth_date='1751-03-30', name='Jefferson')]

在类方法中使用 gpt_function

类方法可以像任何其他函数一样使用 gpt_function。然后可以使用 self 在文档字符串中进行插值,但请注意,仅支持对属性的访问,而不是其他类方法(通过 property 计算的属性也支持)

from gpt_function_decorator import gpt_function
from pydantic import BaseModel

class Event:
    year: int
    summary: str

class City:
    def __init__(self, name, country):
        self.name = name
        self.country = country
    
    @property
    def full_name(self):
        return f"{self.name} ({self.country})"
    
    @gpt_function
    def events(self, period) -> list[Event]:
        """List events from {period} that happened in {self.full_name}"""

city = City("Boston", "England")

city.events(period="14th century", gpt_model="gpt-4o")
# [Event(year=1313, summary='Boston fairs are among the busiest...')
#  Event(year=1390, summary='Boston Guildhall is constructed...'),
#  ...]

staticmethods 也可以是 gpt 函数,这使得将 Pydantic 输出格式与生成它的函数分组成为可能

from gpt_function_decorator import gpt_function
from pydantic import BaseModel

class Car(BaseModel):
    brand: str
    age: int
    damaged: bool

    @staticmethod
    @gpt_function
    def from_description(description) -> "Car":
        """Extract car properties from the description."""

car = Car.from_description("A 5-year-old Subaru in mint condition")
# Car(brand='Subaru', age=5, damaged=False)

要求 GPT 提供一个有理的答案

考虑这个函数

@gpt_function
def could_have_met(person: str, celebrities: list) -> list[str]:
    """List the names in {celebrities} that {person} could have met."""

could_have_met("Chopin", celebrities=[
    "Napoleon", "Jefferson", "Mozart", "Julius Cesar", "Peggy Lee", "Beethoven"
])

结果证明,这个函数很难找到正确答案。它的输出(由 gpt-4o-mini 生成)变化很大,通常包括佩吉·李,她生活在不同的世纪。这是因为这个简短的提示实际上需要 GPT 进行真正的推理:首先列出每个人的出生和死亡年份,然后检查谁与肖邦重叠。

为了得到更智能的答案,我们为装饰了 gpt 的函数提供了一个 gpt_reasoning 参数。具体来说,它要求 GPT 答案包含两个字段,reasoningresult(最终结果将在 .__reasoning__ 属性下包含推理过程)。GPT 答案更加详细,因此速度较慢、成本更高,但也更有帮助。

  • reasoning 字段给 GPT 留出“思考”问题的空间,从而产生更好的答案。
  • 现在用户可以查看 GPT 的“推理”过程,以及错误答案是否是由于知识不足、逻辑错误等原因造成的。
  • 这减少了 GPT 的推理和格式化过程最终污染结果架构的风险。

因此,让我们请求推理并观察改进效果。

from gpt_function_decorator import gpt_function

@gpt_function
def could_have_met(person, celebrities) -> list[str]:
    """List the names in {celebrities} that {person} could have met."""

celebrities = [
    "Napoleon", "Jefferson", "Mozart", "Julius Cesar", "Peggy Lee", "Beethoven"
]
answer = could_have_met("Chopin", celebrities, gpt_reasoning=True)

print (answer)
# ['Napoleon', 'Jefferson', 'Beethoven']

print (answer.__reasoning__)
# Frédéric Chopin was born in 1810 and died in 1849. To determine which
# celebrities he could have met, we need to consider the lifespans of
# each individual listed:  
# - Napoleon Bonaparte (1769-1821) could have met Chopin.  
# - Thomas Jefferson (1743-1826) could have met Chopin.  
# - Wolfgang Amadeus Mozart (1756-1791) could not have met Chopin,
#   as he died before Chopin was born.  
# - Julius Caesar (100 BC-44 BC) definitely could not have met Chopin,
#   as he lived in ancient times.  
# - Peggy Lee (1920-2002) could not have met Chopin, as she was born
#   after Chopin died.  
# - Ludwig van Beethoven (1770-1827) could have met Chopin.  

GPT 模型参数

gpt_function 装饰器为其装饰的函数添加了两个参数

  • gpt_model:这允许函数的用户在 gpt-4o-mini(默认,快速且成本低但能力较弱)和 gpt-4o(任何兼容版本)之间切换。
  • gpt_system_prompt:这使用户能够通过要求 GPT 专注于某些方面或进行角色扮演来调整答案。
  • 如前所述的 gpt_reasoning
  • gpt_debug:这将导致函数打印出它发送给 GPT 的完整提示(对于故障排除或仅仅了解正在发生的事情非常有用)。

以下是一个示例,让我们从这个函数开始

@gpt_function
def list_movies(actor, n=2) -> list[str]:
    """Return {n} movies featuring {actor}, e.g. "Batman", "Up"..."""

list_movies("Brad Pitt")
# ['Fight Club', 'Once Upon a Time in Hollywood']

现在在调用此函数时,我们还要求一个更具体的列表和一个更好的(更昂贵)的 GPT 模型

list_movies(
    "Brad Pitt",
    gpt_system_prompt="Don't list movies released before 2020.",
    gpt_model="gpt-4o" #gpt-4o knows more than -mini
)
# ['Bullet Train', 'Babylon']

异步 GPT 函数

您的 GPT 函数可以是 async,这非常有用,因为 OpenAI 可能对某些请求的响应较慢,但您也可以并行发送许多请求。

import asyncio

@gpt_function
async def summarize(text):
    """Summarize the text."""

# In another async function, or directly in a notebook, call the function
# to summarize several texts asynchronously (in "parallel"):
summaries = await asyncio.gather(*[summarize(txt) for txt in texts])

为了实用性,用 @gpt_function 装饰的异步函数会获得一个额外的参数 semaphore,这可以用来限制对 OpenAI 的并发调用数量。在上面的示例中,如果有很多文本要总结,您可以一次请求 10 个 OpenAI 请求。

semaphore = asyncio.Semaphore(10)
summaries = await asyncio.gather(*[
    summarize(txt, semaphore=semaphore) for txt in texts
])

限制

请注意

  • 只有拥有 OpenAI API 密钥的人才能使用这些函数。
  • GPT 有一个令牌大小限制,因此如果您的输入或输出太大(如长列表等),这些函数将失败。
  • GPT 答案可能会变化且不可靠。
  • 对 OpenAI 驱动的函数的调用通常有 ~0.5-1 秒的开销,然后随着输入和输出大小的增加而变慢。因此,纯 Python 解决方案通常会优于基于 GPT 的解决方案。有时最好只是向 ChatGPT 应用程序请求 Python 代码并运行该 Python 代码。

带有 GPT 驱动函数的未来?

GPT 尚未足够快且成本低到可以用于任何地方,但一旦它们做到了,它们将彻底改变我们编写代码的方式(假设我们仍然在编写代码)。

例如,我们可以用这样的函数来代替开发者编写成千上万的消息来帮助用户排除错误。

@gpt_function(reasoning=True)
def help_troubleshoot(error_traceback: str) -> str:
    """Return a short analysis of the Python error: {error_traceback}"""

有了这个,我们可以编写一个查询网页并在需要时调查问题的函数。

import requests
import traceback

def query_webpages_and_help_troubleshoot(url):
    try:
        requests.get(url)
    except Exception as error:
        gpt_advice = help_troubleshoot(traceback.format_exc())
        raise Exception(f"{gpt_advice}\n{gpt_advice.__reasoning__}") from error

现在我们可以将其应用到墙上并观察它如何理解问题。

query_webpages_and_help_troubleshoot("https://wykipedia.com")

# Raises:
"""
< traceback with ConnectionError, MaxRetryError, etc... />

Exception: The immediate cause of the error is an inability to resolve the
hostname 'wykipedia.com'. To resolve this, ensure the URL is correctly
spelled as 'wikipedia.com'.

GPT reasoning: The error traceback indicates that the Python script attempted
to make a network request to 'wykipedia.com' using the requests library. The
main issue arises from the `socket.gaierror` ... etc.
"""

贡献!

这个开源项目托管在 Github 上,采用 Apache 2.0 许可证。欢迎每个人贡献!

发布新版本

  • pyproject.toml 中增加版本号
  • 在 Github 上创建一个带有该版本的 new-release。
  • Github Actions 将从中选取并发布。

感谢

感谢 Simon Willison 的 python 库项目模板

项目详情


下载文件

下载您平台的文件。如果您不确定选择哪一个,请了解更多关于安装包的信息。

源代码分发

gpt_function_decorator-0.5.tar.gz (24.0 kB 查看哈希值)

上传时间 源代码

构建分发

gpt_function_decorator-0.5-py3-none-any.whl (18.0 kB 查看哈希值)

上传时间 Python 3

由以下支持