跳到主要内容

第 1 章:三件套与函数存储

本章讲地基:Functionz 怎么把活儿分给两个助手,以及一个函数被登记时,它的「签名、依赖、代码、版本」分别落到数据库哪张表。读完你就知道「函数 = 一行数据」具体长什么样。

1.1 三件套:门面 + 注册器 + 执行器

functionz 的核心只有三个类,职责干净分离:

角色类比
Functionz门面(facade),自己几乎不干活,把方法转发给下面两个和 db前台
FunctionRegistrar「存」:把一个函数解析、查重、写成数据库里的新版本档案管理员
FunctionExecutor「跑」:从库里取出代码,exec 复活并执行放映员

构造时一次性把三者接好,Functionz 持有另外两个的实例:

# babyagi/functionz/core/framework.py:17 — Functionz.__init__
self.db = DBRouter(db_type, **db_kwargs)
self.executor = FunctionExecutor(self) # 跑
self.registrar = FunctionRegistrar(self) # 存

整个进程只有一个 Functionz 实例,在包加载时建好(_func_instance = Functionz(),babyagi/__init__.py:13),所有函数共享同一个库。

门面最妙的一招:__getattr__ 把库里的函数变成「可点出来的属性」

你能写 babyagi.hello_world(),但 babyagi 模块里根本没有 hello_world 这个名字。靠的是 Python 的模块级 __getattr__ 钩子:

# babyagi/__init__.py:112 — 模块级 __getattr__
def __getattr__(name):
if _func_instance.get_function(name): # 库里有这个函数吗?
# 有 → 返回一个执行它的 lambda
return lambda *args, **kwargs: _func_instance.executor.execute(name, *args, **kwargs)
raise AttributeError(...)

Functionz 类自己也有同样套路的 __getattr__(framework.py:26),所以 pack 里能写 func.gpt_call(...) 调到库里的 gpt_call「点一个不存在的属性 → 去数据库找同名函数 → 找到就执行」,这是整个框架「函数即数据」体验的门面糖。

1.2 注册一个函数,到底发生了什么

入口是装饰器 @register_function。它的核心工作是把函数体变成字符串代码,连同元数据交给数据库。

怎么把「函数」抽成数据

关键一步:用 inspect.getsourcelines 拿到源码文本,从 def 行开始截取——这样去掉了装饰器那几行,只留函数本体:

# babyagi/functionz/core/registration.py:24 — register_function 内
source_lines = inspect.getsourcelines(func)[0]
func_start = next(i for i, line in enumerate(source_lines) if line.strip().startswith('def '))
function_code = ''.join(source_lines[func_start:]).strip() # 纯函数体字符串

这段 function_code 字符串就是以后要存进 FunctionVersion.code、执行时被 exec 的东西。

自动解析签名和返回值(用 AST)

Registrar 不止存代码,还用 Python 的 ast 模块静态解析出输入/输出参数,这样 dashboard 和 chat 工具能知道函数的接口:

# babyagi/functionz/core/registration.py:45 — parse_function_parameters
tree = ast.parse(code)
function_def = next(n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef))
for arg in function_def.args.args: # 遍历形参
param_type = ast.unparse(arg.annotation) if arg.annotation else 'Any'
input_params.append({'name': arg.arg, 'type': param_type})

输出参数靠扫第一个 return 语句猜:返回 dict → 每个 key 算一个输出;返回字符串字面量 → 标 str;其它 → 一个泛型 output(registration.py:64-82)。这是静态猜测,不是运行时真值,复杂返回会被笼统标成 Any

查重:没变就不写新版本

注册/更新前会先比对,代码、描述、imports、依赖、触发器全一样就直接 return,避免每次 import 都堆一个新版本:

# babyagi/functionz/core/registration.py:152 — function_has_no_changes
if (existing_code == code and existing_description == new_description and
set(existing_imports) == set(import_names) and
set(existing_dependencies) == set(dependencies) and
set(existing_triggers) == set(triggers)):
return True # 无变化

1.3 函数在数据库里长什么样:5 张表

落库由 SQLAlchemy ORM 定义,babyagi/functionz/db/models.py。先看关系:

Function (一个函数名,唯一)
│ 1

│ N
FunctionVersion (这个函数的某一版:代码/元数据/参数/触发器/is_active)
├──► dependencies ── M:N ──► Function (我依赖哪些函数)
└──► imports ── M:N ──► Import (我需要哪些第三方库)

Log (执行日志,自引用成父子树:parent_log_id / triggered_by_log_id)
SecretKey (加密存储的 API key)

核心设计:Function 与 FunctionVersion 分离

一个函数名对应一行 Function;它的每一次改动是一行 FunctionVersion。靠 is_active 布尔位标记当前生效的版本:

关键字段作用
Functionname(unique)、versions函数的「身份」,只是个名字 + 版本集合
FunctionVersioncodefunction_metadata(JSON)、is_activeinput_parametersoutput_parameterstriggers(JSON)一次具体快照
Importnamelib第三方依赖,M:N 挂到版本上
Logparent_log_idtriggered_by_log_id执行轨迹,自引用成树
SecretKeyname_encrypted_valueFernet 对称加密后的密钥

写新版本时,版本号 = 现有版本数 +1,并把旧版本全部置为非活跃:

# babyagi/functionz/db/local_db.py:77 — add_or_update_function
version = FunctionVersion(function=function, version=len(function.versions) + 1,
code=code, is_active=True, triggers=triggers or [], ...)
# ...
for v in function.versions: # local_db.py:98 — 旧版本全部下线
if v != version:
v.is_active = False

这就是「改函数 = 加一个新版本并切活跃位」,回滚只要 activate_function_version(name, version) 把活跃位拨回去(db_router.py:124)。

DBRouter:把 ORM 对象拍平成 dict

执行引擎和 pack 代码不想碰 SQLAlchemy 对象,所以 DBRouter.get_function 把活跃版本拍平成一个普通 dict——后面所有 function_version['code']['dependencies'] 都来自这里:

# babyagi/functionz/db/db_router.py:64 — get_function 返回的 dict 形状
return {
'name': function.name,
'code': active_version.code,
'metadata': active_version.function_metadata,
'dependencies': [dep.name for dep in active_version.dependencies],
'imports': [imp.name for imp in active_version.imports],
'triggers': active_version.triggers,
# ...input_parameters / output_parameters / version / created_date
}

记住这个 dict 形状,第 2 章执行引擎全程在消费它。

Secret key:Fernet 加密落库

API key 不明文存。SecretKey.value 是个 hybrid property,读时解密、写时加密:

# babyagi/functionz/db/models.py:116 — value 的 getter/setter
@hybrid_property
def value(self):
return fernet.decrypt(self._encrypted_value).decode() # 读 → 解密
@value.setter
def value(self, plaintext):
self._encrypted_value = fernet.encrypt(plaintext.encode()) # 写 → 加密

加密 key 本身存在项目根的 encryption_key.json(models.py:17),首次运行自动生成。注意: 这个文件丢了/换了,所有已存密钥都解不开(getter 会捕获 InvalidToken 返回 None)。

1.4 小结

  • Functionz 是空壳门面,真活儿在 FunctionRegistrar(存)和 FunctionExecutor(跑)。
  • 注册 = 用 inspect 把函数体抽成字符串,用 ast 猜出参数,查重后写成一个新 FunctionVersion
  • 「函数名」(Function)和「函数版本」(FunctionVersion)分离,is_active 位决定当前跑哪版——这是版本控制与回滚的根基。
  • DBRouter 把 ORM 拍平成 dict,执行引擎只跟 dict 打交道。

下一章:这堆「代码字符串」是怎么在执行时被复活成可调用对象的。