femwa 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. femwa-1.1.0/Doc_ch_FemWA.md +964 -0
  2. femwa-1.1.0/Doc_developer_FemWA.md +372 -0
  3. femwa-1.1.0/Doc_en_FemWA.md +939 -0
  4. femwa-1.1.0/LICENSE +201 -0
  5. femwa-1.1.0/MANIFEST.in +14 -0
  6. femwa-1.1.0/PKG-INFO +412 -0
  7. femwa-1.1.0/README.md +385 -0
  8. femwa-1.1.0/env_template.txt +95 -0
  9. femwa-1.1.0/femBridges/ContextExample.py +220 -0
  10. femwa-1.1.0/femBridges/MemoryExample.py +163 -0
  11. femwa-1.1.0/femBridges/__init__.py +1 -0
  12. femwa-1.1.0/femBridges/deepseek.py +158 -0
  13. femwa-1.1.0/femBridges/getDir/FEMain_dir.txt +1 -0
  14. femwa-1.1.0/femBridges/getDir/get_dir.py +58 -0
  15. femwa-1.1.0/femBridges/llmBridge.py +204 -0
  16. femwa-1.1.0/femBridges/llmProviders.py +287 -0
  17. femwa-1.1.0/femCompiler/FEM_CLIrenderer.py +198 -0
  18. femwa-1.1.0/femCompiler/FEM_config.py +35 -0
  19. femwa-1.1.0/femCompiler/FEM_interpreter.py +163 -0
  20. femwa-1.1.0/femCompiler/FEM_normalizer.py +314 -0
  21. femwa-1.1.0/femCompiler/FEM_parser.py +1533 -0
  22. femwa-1.1.0/femCompiler/FEM_runtime.py +3345 -0
  23. femwa-1.1.0/femCompiler/FEM_scope_resolver.py +181 -0
  24. femwa-1.1.0/femCompiler/FEM_shared_utils.py +120 -0
  25. femwa-1.1.0/femCompiler/__init__.py +1 -0
  26. femwa-1.1.0/femCompiler/actor_resolver.py +86 -0
  27. femwa-1.1.0/femCompiler/block_collector.py +390 -0
  28. femwa-1.1.0/femCompiler/db_utils.py +468 -0
  29. femwa-1.1.0/femCompiler/femAsync.py +321 -0
  30. femwa-1.1.0/femCompiler/save_dialog.py +255 -0
  31. femwa-1.1.0/femCompiler/task_pause.py +155 -0
  32. femwa-1.1.0/femwa.egg-info/PKG-INFO +412 -0
  33. femwa-1.1.0/femwa.egg-info/SOURCES.txt +53 -0
  34. femwa-1.1.0/femwa.egg-info/dependency_links.txt +1 -0
  35. femwa-1.1.0/femwa.egg-info/entry_points.txt +2 -0
  36. femwa-1.1.0/femwa.egg-info/requires.txt +3 -0
  37. femwa-1.1.0/femwa.egg-info/top_level.txt +3 -0
  38. femwa-1.1.0/main.py +374 -0
  39. femwa-1.1.0/pyproject.toml +34 -0
  40. femwa-1.1.0/requirements.txt +3 -0
  41. femwa-1.1.0/setup.cfg +4 -0
  42. femwa-1.1.0/setup.py +42 -0
  43. femwa-1.1.0/user_data/.DS_Store +0 -0
  44. femwa-1.1.0/user_data/memory/Chronica.wor +0 -0
  45. femwa-1.1.0/user_data/memory/Chronica.wor-shm +0 -0
  46. femwa-1.1.0/user_data/memory/Chronica.wor-wal +0 -0
  47. femwa-1.1.0/user_data/projects/.DS_Store +0 -0
  48. femwa-1.1.0/user_data/projects/fiat/.DS_Store +0 -0
  49. femwa-1.1.0/user_data/projects/fiat/luxfiat.fems +38 -0
  50. femwa-1.1.0/user_data/projects/fiat/wait.py +29 -0
  51. femwa-1.1.0/user_data/projects/super_debug/.DS_Store +0 -0
  52. femwa-1.1.0/user_data/projects/super_debug/super_debug.fems +222 -0
  53. femwa-1.1.0/user_data/projects/werewolf_game/.DS_Store +0 -0
  54. femwa-1.1.0/user_data/projects/werewolf_game/werewolf_game.fems +304 -0
  55. femwa-1.1.0/user_data/projects/werewolf_game/werewolf_utils.py +155 -0
@@ -0,0 +1,964 @@
1
+ # Flow Engine for Minds - Work Automata (FemWA)
2
+ # 语法规范与剧本编写指南(使用者文档)
3
+
4
+ ## 核心设计哲学
5
+
6
+ 1. **Scope = 空间/视野(物理隔间)**
7
+ 处在同一 scope 的角色才能共享上下文(对话记录)。不同 scope 的角色天然隔离,互不可见。
8
+ 2. **Vars = 控制参数 **
9
+ 用以控制流程图走向和角色视野。
10
+ 3. **上下文不是变量**
11
+ 人类与 AI 共享的是上下文。除非需要程序操作变量,角色间对话无需显式传递变量——能看到同一个上下文即可。
12
+ 非必要不要求 AI 输出变量,以免打断思路。记住:**上下文不是变量**。
13
+
14
+ ## 基本约定
15
+ - **拓展名**:FEM语言的文件格式为.fems(s是script),编译器将读入.fems文件,并按照我们约定的规则解析。
16
+ - **缩进**:缩进敏感,缩进表示从属关系(prompt 属于哪个 @agent,分支属于哪个 fork)。
17
+ - **换行**:换行部分敏感,在fork, for, par等语法中,必须严格按照规定的换行和缩进格式写。
18
+ - **大小写**:大小写敏感,变量名、action 名区分大小写。
19
+ - **注释**:行内可使用 `#` 或 `//`。
20
+ - **@ 符号**:Actor 实体名字永远带 `@`。定义、引用、传参均须保留。不然编译器无法认为那是@actor类型。
21
+ - **文件路径与文本值**:
22
+ 所有文本字段(`system_safety`、`output_style`、`prompt`、`code` 路径等)遵循:
23
+ - `file:"path/to/file"` → 读取文件内容(文件不存在则报错)
24
+ - 裸文本(无引号或普通引号) → 字面量字符串
25
+ - `|` 后换行缩进 → 多行字面量字符串
26
+
27
+ ### 符号速查
28
+
29
+ | 符号 | 语义 | 示例 | 助记 |
30
+ |---|---|---|---|
31
+ | `action` | 定义动作 | `action wolf_kill @ai(wolf)` | 类似 Python 的 `def` |
32
+ | `@` | 引用/指向 | `@wolfClaire`, `@ellis.type` | @某人 |
33
+ | `()` | 动作参数/输入 | `@ai(wolf)`, `@func(sys.spawn)` | 函数参数 |
34
+ | `[]` | Node 标签 | `[A]`, `[START]` | 位置标记 |
35
+ | `{}` | 变量替换 | `{alive_players}` | 模板插值 |
36
+ | `<<>>` | 输出信号 | `<<VOTE: 3>>` | 信号弹 |
37
+ | `&` | Module 引用 | `&CoderSandbox(...)` | 打包带走 |
38
+ | `->` | 流程连接 | `[A] -> [B]` | 方向 |
39
+
40
+ 为了方便中文输入,以下字符在 FEM 中具有同等语法效力:
41
+
42
+ | 方便符号 | 英文符号 | 说明 |
43
+ |-|-|-|
44
+ | `:` | `:` | 冒号 |
45
+ | `,` | `,` | 逗号 |
46
+ | `“` `”` | `"` | 引号(文件路径和文本识别) |
47
+ | `()` | `()` | 圆括号 |
48
+ | `【】` | `[]` | 方括号 |
49
+ | `|` | `|` | 竖线 |
50
+ | `--` | `->` | Flow 链连接符号 |
51
+ 注意:
52
+ - `--` 仅在 flow 区域等价 `->`,prompt 中不受影响。
53
+ - 其它中文符号在.fems剧本语法中全局等价为对应的英文符号。
54
+
55
+ ## 1. 剧本整体结构
56
+
57
+ 按以下顺序组织:
58
+
59
+ meta: # 剧本元信息区
60
+ vars: # 全局状态变量区
61
+ code: # 外部 Python 代码区
62
+ actors: # 角色定义区
63
+ memory: # 记忆检索方法定义 (可选)
64
+ context:# 上下文提取方法定义 (可选)
65
+ action: # 动作定义 (可多个)
66
+ module: # 模块定义 (可多个)
67
+ mainflow: # 主流程编排
68
+
69
+ ## 2. 元信息 (meta)
70
+
71
+ 定义剧本基础属性与运行环境。
72
+
73
+ meta:
74
+ name = "我的狼人杀"
75
+ version = 1.0
76
+ owner = [1, 2]
77
+ database = file:"werewolf.db"
78
+ session = new
79
+ system_safety = |
80
+ 不许删库。
81
+ 不许违法犯罪,不许生成危险内容,不许歧视女性。
82
+ output_style = "回复请简洁专业"
83
+
84
+ 字段说明:
85
+
86
+ | 字段 | 类型 | 必填 | 说明 |
87
+ |---|---|---|---|
88
+ | `name` | 文本 | 否 | 剧本名称,新建 session 时写入 `sessions.title` |
89
+ | `version` | 文本 | 否 | 版本号 |
90
+ | `owner` | 整数数组 | 否 | 剧本所有者在数据库中的 user_id 列表,自动注入所有记忆记录的 user_scope |
91
+ | `database` | 文件路径 | 否 | SQLite 数据库路径。未指定默认 `./dialog.db` |
92
+ | `session` | 整数 / `new` | 否 | 运行的 session_id。未指定或为 `new` 时自动新建 (max+1)。指定不存在的数字报错 |
93
+ | `system_safety` | 文本 / 文件路径 | 否 | 安全须知,自动注入上下文 |
94
+ | `output_style` | 文本 / 文件路径 | 否 | 输出质量要求,自动注入上下文 |
95
+
96
+ `owner` 信息会从数据库的 user 表读取,并由编译器生成 `blocks['user_info']`,格式如:
97
+
98
+ [用户 @Alice 的信息]
99
+ 喜欢简洁回答,后端工程师。
100
+
101
+ [用户 @Claire 的信息]
102
+ 喜欢详细解释,前端工程师。
103
+
104
+
105
+
106
+ ## 3. 全局变量池 (vars)
107
+
108
+ 全局状态,所有节点均可读写。变量必须先在此声明,否则赋值报错。
109
+
110
+ vars:
111
+ turn_count = 1
112
+ alive_players = [@seer, @wolf]
113
+ agent_locations = {@ellis: "卧室", @bob: "公园"}
114
+ game_over = false
115
+ @speaker = ""
116
+ hp = {@wolfClaire: 100, @预言家: 100, @player: 100}
117
+
118
+ - **声明变量的必要**:如果 action 中使用了未在 vars 中声明的变量,解析器会报错。
119
+ - **字典缺省值**:prompt 中使用 `dict[key]` 但 key 不存在时,解析为空字符串 `""`,不报错。
120
+ - **Actor 类型变量**:你可以定义一个@actor类型变量并把它赋值为某个人。比如,@speaker = @预言家。空值用空字符串""。
121
+ 注意,actor类型变量必须全程带着`@`开头,不然无法被识别为@actor类型。
122
+
123
+ ## 4. 外部代码 (code)
124
+
125
+ 引入 Python 文件,供 `@func` 和 `resolve` 调用。
126
+ FEM 可以通过 code: 区域声明外部 Python 文件,然后在 action 中调用:
127
+
128
+ **路径必须使用 `file:"..."` 格式**。以下名称为保留字,不可作为别名:
129
+ `meta`, `vars`, `code`, `actors`, `action`, `module`, `flow`, `mainflow`, `memory`, `context`
130
+
131
+ code:
132
+ game_logic = file:"utils/game.py"
133
+ dev_ops = file:"utils/deploy.py"
134
+
135
+ ## 5. 角色定义 (actors) 与 @actor 类型变量
136
+
137
+ 采用 `类型 变量名 = 属性` 的强类型声明风格。Actor 名字永久以 `@` 开头。
138
+
139
+ actors:
140
+ ai hostgod = soul:0, source:deepseek
141
+ ai wolfbob = soul:1, source:glm5.1, tools:[deep_think]
142
+ ai 预言家 = soul:2, source:glm5
143
+ human player = soul:9, source:0
144
+
145
+ 注意:
146
+ - 必须写`actors:`, 下一行缩进。
147
+ **Actor 实体名字永远带 `@`,代码中任何地方不得去掉。**
148
+ 定义时:`ai @ellis = soul:1`
149
+ 引用时:`@ai(@ellis)`、`scope: [@ellis]`
150
+ 变量传递时:`@speaker = @ellis`
151
+ AI输出赋值时:如果要赋值@actor,变量名也必须带着`@`开头,不然无法被识别为@actor类型。
152
+ 你可以理解为`@`是变量名的一部分,就像你不会把Alice写成lice,你也不能把@Alice写成Alice。
153
+ - 定义@actor实体时,开头必须是ai或human,这两者为保留字段。
154
+ - 人名支持中文。
155
+ - `soul:ID`:唯一标识,用于 scope 定位和数据库查询,对应数据库中souls表同一 soul id的角色。数据库中同时存着该角色的系统提示词,编译器会自动去数据库中提取,并生成`blocks['soul']`.
156
+ - `source`:AI 写模型名(可缺省随机分配);人类写数字,对应数据库中user表的具体人。
157
+ - 特殊的,human source为0时,代表无管理员视角的人类用户,只拥有soul的视角。
158
+ 适用于有些场合,比如作为狼人杀玩家,他不能开human source为任何正整数的管理员视角。
159
+ - `tools`:设定该AI角色挂载的工具列表,如 `web_search`、`deep_think`。
160
+
161
+ 未实现TODO:**蓝图角色**(动态生成模板):
162
+ blueprint coder:
163
+ source: ai-glm5
164
+ tools: [code_interpreter]
165
+ 蓝图不指定 `soul`,运行时由 `system.spawn` 动态分配。
166
+
167
+
168
+ ### Actor 类型系统与实体访问
169
+
170
+ 在 FEM 中,Actor 不仅是角色配置,更是一种**变量类型**。它代表“执行人”实体,拥有身份和状态。
171
+
172
+ **赋值**
173
+ 它可以像变量一样在 `vars` 中被赋值和传递:
174
+
175
+ vars:
176
+ current_speaker = @seer # Actor 类型的变量
177
+ alive_players = [@seer, @wolf] # Actor 数组
178
+ locations = {@ellis: "卧室"} # 以 Actor 为 key 的字典
179
+
180
+ - 在 Flow 和 Action 中,你可以用 `@变量名` 取到这个 Actor 实体,也可以直接用 `@角色名` 引用它。
181
+ - 如:当你已经把@ellis赋值给了@speaker,那么:
182
+ action give_a_speech @ai(@speaker):
183
+ 等价于:
184
+ action give_a_speech @ai(@ellis):
185
+
186
+ @speaker.type
187
+ 等价于
188
+ @ellis.type
189
+
190
+ - 支持 f-string:
191
+ prompt:"现在发言者是{@speaker}。"
192
+ 实际上ai收到的prompt会被解析为:现在的发言者是@ellis。
193
+
194
+
195
+ **实体属性双向访问**(字典视角 vs 实体视角):
196
+
197
+ FEM 引入了类似数据库的“行/列”双向视角。假设我们在 `vars` 中定义了一个字典:
198
+ vars:
199
+ hp = {@wolfClaire: 100, @预言家: 100, @player: 100}
200
+ salary = {}
201
+ 当我们想读取或修改 `@wolfClaire` 的血量时,有两种完全等价的写法:
202
+ 1. **字典视角(按列查)**:`hp.@wolfClaire` —— 在 hp 这本花名册里,翻到 wolfClaire 那一页。
203
+ 2. **实体视角(按行查)**:`@wolfClaire.hp` —— wolfClaire 这个人,他的 hp 是多少。
204
+
205
+ **这两种写法在 FEM 中完全等价,可以随语境自由使用**
206
+ - 在 prompt 中:`"你的当前血量是 {hp.@wolfClaire}"` 与 `"你的当前血量是 {@wolfClaire.hp}"` 效果完全相同。
207
+ - 在 AI 输出信号中:`<<hp.@wolfClaire: += 30>>` 与 `<<@wolfClaire.hp: -= 30>>` 均可。
208
+ - 在 action assign 赋值时:`salary.@ellis = 5000` 与 `@ellis.salary = 5000` 均可。
209
+
210
+ #### 静态属性与动态属性
211
+ 既然 `@实体.属性` 可以访问字典,那它和 Actor 定义时的属性(如 `@wolfClaire.soul`)会冲突吗?
212
+ 规则:静态属性优先,保留字不可覆盖。解析 `@实体.属性` 时,先查以下 5 个静态保留属性,未命中则去 `vars` 中查找同名字典。
213
+ | 保留属性 | 含义 | 示例值 |
214
+ |---|---|---|
215
+ | `type` | 角色类型 | `"ai"` 或 `"human"` |
216
+ | `soul` | 角色 ID | `1` |
217
+ | `source` | 模型/来源 | `"glm5.1"` 或 `0` |
218
+ | `tools` | 挂载工具 | `["deep_think"]` |
219
+ | `name` | 角色名 | `"wolfbob"` |
220
+ 例如:
221
+ - `@wolfClaire.soul` -> soul 命中保留字,返回静态属性 `1`
222
+ - `@wolfClaire.hp` -> hp 不是保留字 → 自动查找 vars 中的 `hp` 字典,取 `hp.@wolfClaire` 的值
223
+ - `@ellis.salary` -> salary 不是保留字 → 自动查找 vars 中的 `salary` 字典,取 `salary.@ellis` 的值
224
+
225
+ 这种设计意味着:
226
+ **你不需要为了给角色挂载状态而修改 actors 定义,只需在 vars 中建立相应的字典,即可通过 `@实体.属性` 的直觉方式访问。** Actor 的身份是静态的,但状态是动态且可无限扩展的。
227
+
228
+
229
+ ## 5. memory 和 context 定义(与 action/module 平级)
230
+ ### 5.1 定义方法
231
+ memory 方法名(模块别名.函数名):
232
+ in: 参数1, 参数2, @actor
233
+ out: 返回值变量(类型)
234
+
235
+ context 方法名(模块别名.函数名):
236
+ in: session, @actor
237
+ out: 返回值变量(类型)
238
+
239
+ ### 5.2 参数传入传出约定
240
+ `in:` 中声明的参数在调用用户 Python 函数时按名传入:
241
+ | 预留字段 | 值 |
242
+ |-|-|
243
+ | `prompt` | 当前 action 的 prompt(已变量替换) |
244
+ | `@actor` | 当前说话者的 actor_info 字典,如 `{"soul": 1}` |
245
+ | `session` / `session_id` | 当前 session ID |
246
+ | `turn` / `turn_id` | 当前 turn ID |
247
+ | 其他 | 从全局变量 `vars` 中取值 |
248
+ * `@actor` 在传给 Python 函数时,参数名自动映射为 `actor_info`。
249
+ * 用户自定义函数的 `def` 签名应与 `in:` 声明和上述映射保持一致。
250
+ out:
251
+ 用户提供的context或memory的函数的返回值,会被无脑赋给 `out:` 声明的变量,并放入对应的 block("memory") 或 block("context")中,方便外部代码调用。
252
+ 所以用户提供的函数,返回值必须就是context或memory的文本或json格式,能直接发给AI的那种。
253
+ 如果返回值设定的不对,那么AI看到的上下文大概就会奇奇怪怪吧——反正这里无脑赋值,不做校验。
254
+ 变量必须先在 `vars:` 中声明。
255
+
256
+ ### 5.3 Action 中引用方法
257
+ action speak @ai(@ellis):
258
+ prompt: "你好"
259
+ memory: 方法名
260
+ context: 方法名
261
+ * 如果 action 不指定 `memory:`,则使用第一个定义的 memory 方法。
262
+ * 如果没有定义任何 memory 方法,`memory` 块为空。
263
+ * 如果 action 不指定 `context:`,则使用第一个定义的 context 方法。
264
+ * 如果没有定义任何 context 方法,使用内置默认实现(提取当前 session 上下文)。
265
+
266
+
267
+
268
+ ## 6. 动作定义 (actions)
269
+
270
+ Action 是一个行为单元,描述"做什么"。它本身没有位置信息。
271
+ 使用 `action` 关键字定义,格式:`action 动作名 @执行者类型(执行者参数):`
272
+
273
+ ### 6.1 AI 动作
274
+ action wolf_kill @ai(@wolf):
275
+ prompt: |
276
+ 现在是第{day_count}天夜晚,存活玩家:{alive_players}。
277
+ 你是狼人,请选择击杀目标,你可以分析一段。:
278
+ 分析结束后请务必输出 SET VARIABLE: << KILL = @玩家名 >> 你想击杀哪个玩家你就把 @玩家名 换成 @他的名字。
279
+ scope: [@hostgod, @wolf]
280
+ out: wolf_target(string, "击杀目标")
281
+ resolve: game_logic.resolve_target(arg1, arg2)
282
+ max_retries: 3
283
+ fallback: host_emergency
284
+ context: 方法名
285
+ memory: 记忆方法名
286
+ interrupt: human_interrupt_branch
287
+
288
+ 字段说明:
289
+
290
+ - **`(actor_expr)`**:实际执行角色,可以是静态角色名或动态变量(如 `@speaker`)。
291
+ 当 action 的角色使用动态变量,在循环中复用时,循环变量必须和action定义时括号里的身份一致。
292
+ 比如定义时action @ai(@wolf),那么:
293
+ for @wolf in [...] 可以,
294
+ for @speaker in [...] 不行,会报错。因为@speaker 和 @wolf 对不上。
295
+ 这样设计是因为,action毕竟不是函数,它是基于prompt的AI发言回合,每个action的prompt固定。所以如果来的不是 @wolf 而是 @seer 或者别的什么人,用错人了,可能会造成混乱。
296
+ 如果你实在很想要更大的灵活性,你可以定义比如action .. @ai(@role),然后全局使用for @role in [...]……但这样会降低角色身份的明确性,我个人觉得这不利于你长期维护这个剧本小世界呢。
297
+
298
+
299
+ - **prompt**:
300
+ 支持 `{var}` 进行变量替换。
301
+ 此处存在prompt工程艺术,你可以在提示词中引导AI更好的输出,以适应流程。
302
+ (比如如果你写了个狼人杀,但是懒得加action判断夜间发言角色是否还活着,你可以直接在prompt里写:你是{@speaker}, 目前存活玩家是{alive},如果你已经死了,不许说话,不许输出变量赋值。)
303
+ 不过AI有幻觉,如果你要求流程精确,最好还是用赋值变量和action,自己写flow以控制流程。
304
+
305
+ - **`memory`**:引用已定义的 memory 方法,如 `rag10`。不指定则使用第一个定义的 memory 方法。
306
+ - **`context`**:引用已定义的 context 方法,不指定则使用第一个定义的 context 方法。若未定义任何 context 方法,则使用FEM系统自带的context方法。
307
+
308
+ - **上下文与scope**:[@角色名1, @soulname2]
309
+ - 简单理解:
310
+ 定义此动作发生的"房间"。只有出现在此列表中的 soul:ID 才能看到这轮对话的上下文。
311
+ 支持动态变量。例如 scope: [{playersInPark}],表示当前在公园的所有角色都能看到本条对话。
312
+ 写剧本设置scope时,就写房间内的在场角色就好,非常直觉的写法。
313
+ 在action执行时,变量 playersInPark 会被实时解析为当前的常量 [@agent1, @agent2],不保留变量引用。
314
+ - 开发者视角:
315
+ 所有聊天记录都是由action产生的,系统存储本条聊天记录时,会同时存储加入本action的scope区的实体:
316
+ 角色所对应的soul id会存入数据库的soul_scope字段。
317
+ 人类用户身份会存入本条数据库的user_scope字段(除了source0不存)。
318
+ 这方便我们之后选择性的展示上下文和记忆,方便的做上下文隔离。
319
+ “房间”比喻只是为了方便理解,其实是等价的。更准确的比喻是:神将这个世界的所有历史存入宇宙的硬盘,但只有亲身经历这段历史的人有资格检索到它。
320
+ FEM模块自带的context方法默认支持通过scope检索上下文,如果你不写context方法,直接运行FEM项目,默认就是角色只能看见自己在scope范围内的聊天记录。
321
+ 此外,除了上下文,如果你有长期记忆检索需求,你也可以在你的记忆检索算法中,使用数据库中的scope字段来分角色,让每个角色只拥有属于自己的记忆,而不会想起来别的角色发生的事儿。
322
+
323
+ - **`in`**(显式传入变量,可选):
324
+ 决定 action 的 prompt 中可以访问哪些变量。有两种模式:
325
+ 1. **自动模式(默认)**:action 不写 `in:`,prompt 中所有 `{var}` 自动替换为**全局**变量的值。
326
+ 2. **显式模式**:action 写了 `in:`,只有 `in:` 中列出的变量会被传入 prompt 并替换。
327
+ 格式:`in: 显示名 = 全局变量名`,例如:
328
+ in: my_task = task_list[@coder_1]
329
+ prompt 中用 `{my_task}` 引用,引擎将其替换为 `task_list[@coder_1]` 的值。
330
+ 未在 `in:` 中列出的变量不会替换,`{var}` 保持原文。
331
+ 这是为了**控制信息暴露范围**,防止 AI 看到不该看的数据。
332
+ - **`out`**(AI 输出变量声明):
333
+ 声明此 action 需要 AI 输出的变量。格式 `变量名(类型, "标签")`。
334
+ AI 必须在回复中使用 `SET VARIABLE: <<变量名 = 值>>` 格式输出该变量的值。
335
+ 变量必须已在 `vars:` 中预先声明,否则赋值时引擎将报错。
336
+ `类型`(如 `string`、`enum([a,b])`、`dropdown`)和 `"标签"` 为预留字段,
337
+ 当前版本仅作标记,不进行自动校验或前端渲染。
338
+ 可写入字典键,如 `vote_results.@voter(string, "")`,将值存入字典的指定键。
339
+
340
+ - **AI主动的变量赋值**
341
+ 如果你需要AI进行赋值,需要你在prompt中让 AI 输出 `SET VARIABLE: <<VARIABLE_NAME = 值>>` 变量赋值信号。
342
+
343
+ - 赋值语句
344
+ AI 通过 `SET VARIABLE: <<变量名 = 值>>` 输出信号,解析器提取并赋值。支持以下操作:
345
+ | 格式 | 含义 |
346
+ |---|---|
347
+ | `SET VARIABLE: <<VAR = 值>>` | 直接赋值 |
348
+ | `SET VARIABLE: <<VAR += N>>` | 加 N |
349
+ | `SET VARIABLE: <<VAR -= N>>` | 减 N |
350
+ | `SET VARIABLE: <<VAR = add(元素)>>` | 列表追加 |
351
+ | `SET VARIABLE: <<VAR = remove(元素)>>` | 列表移除 |
352
+ - 输出示例:
353
+ 我觉得3号玩家很可疑,我投票给3号。SET VARIABLE: <<VOTE_TARGET = @player3>>
354
+ 我觉得这个需要涨价一点。SET VARIABLE: <<Price += 200>>
355
+ 我的角色是soul1,我也去了公园。SET VARIABLE: <<playersInPark = add(@soul1)>>
356
+ 我离开了公园。SET VARIABLE: <<playersInPark = remove(@soul1)>>
357
+ 我使用了治疗技能治疗随机一人~SET VARIABLE: <<@randomplayer.blood += 10 >>
358
+
359
+ - 中文支持
360
+ | 中文支持 | 英文支持 | 说明 |
361
+ |-|-|-|
362
+ | `SET VARIABLE:` | `设定变量:` | AI输出的赋值|
363
+ | `《` `〈` `《《` | `<<` | AI 输出赋值标记开始 |
364
+ | `》` `〉` `》》` | `>>` | AI 输出赋值标记结束 |
365
+ 正确示例:
366
+ SET VARIABLE: <<KILL = @Olivia>>
367
+ 设定变量:<< KILL = @Olivia>>
368
+ 设定变量:《KILL = @Olivia》
369
+ SET VARIABLE: <<SCORE += 1>>
370
+ SET VARIABLE: <<TASKS = add(@Alice)>>
371
+ SET VARIABLE: <<DEAD = remove(@Portia)>>
372
+ 注意:
373
+ - 前缀 `SET VARIABLE:` 或 `设定变量:`均可。
374
+ - 括号内支持 `=` 、`+=`、`-=`、` = add()`、`= remove()` 等操作。
375
+ - 表达式末尾需有 `>>` 或 `》` 等闭合符号。
376
+ - `《》` 仅在 AI 输出赋值识别中等价 `<<>>`。
377
+ - 中文前缀后的冒号 `:` 与英文 `:` 等价,无需区分。
378
+
379
+ - **赋值解析流程**
380
+ 1. 引擎扫描 AI 完整回复,找到所有 `SET VARIABLE: <<...>>` 格式的语句。
381
+ 2. 逐个尝试解析 `<<...>>` 内的赋值表达式。
382
+ 3. 解析成功的,直接操作变量值(和 `@assign` 一样)。
383
+ 4. 解析失败的,按原文顺序存入一个名为 `SET_VARIABLE` 的列表。
384
+ 5. 如果 action 没有声明 `resolve` 函数,解析失败的赋值直接丢弃,引擎打印警告。
385
+ 6. 如果 action 声明了 `resolve` 函数,且用户在参数中写入了 `SET_VARIABLE`,
386
+ 引擎将 `SET_VARIABLE` 列表原样传给该函数,由用户自行解析和处理。
387
+ 7. `resolve` 函数返回三元组列表,引擎据此决定是否接受赋值。
388
+ 8. 如果 `resolve` 也解析失败(返回 `is_success = False`),赋值失败,引擎返回错误信息。
389
+
390
+ `SET_VARIABLE` 列表的传递规则:
391
+ - 只有当用户在 `resolve` 的括号参数中显式写了 `SET_VARIABLE` 时,引擎才传入该列表。
392
+ - 如果用户没写,即使有解析失败的项,也不传入 `resolve` 函数。
393
+ - 这样用户可以自行决定是否需要处理 AI 的非标准输出格式。
394
+
395
+ **`resolve`**(校验/解析函数):
396
+ - 可选,FEM 编译器只能对变量赋值做最基础的格式检验。如果你有更复杂的赋值要求,
397
+ 或者你想兼容 AI 的非标准输出格式,可以在这里设定一个函数来处理。
398
+ - 调用格式:`resolve: 模块别名.函数名` 或 `resolve: 模块别名.函数名(参数1, 参数2, ...)`
399
+ - 函数由用户自行在 code 区提供。用户请照着这个标准写。
400
+ - 显式传参模式(括号内有参数):
401
+ `resolve: game_logic.resolve_target(KILL, alive_players, SET_VARIABLE)`
402
+ 括号内的参数名必须在以下三者之一中找到,否则引擎报错:
403
+ - 全局 `vars:` 中声明的变量
404
+ - 当前 action 的 `in:` 中声明的变量
405
+ - 特殊值 `SET_VARIABLE`(解析失败的赋值原文列表)
406
+ 引擎按用户声明的顺序,将对应的值作为关键字参数传入函数。
407
+ 如果用户写了 `SET_VARIABLE`,引擎传入解析失败的列表;如果没写,不传。
408
+
409
+ - 自动传参模式(括号内无参数,兼容旧写法):
410
+ `resolve: game_logic.resolve_target`
411
+ 引擎自动传入以下关键字参数:
412
+ - `prompt`:当前 action 的 prompt 文本
413
+ - `llm_output`:AI 的完整回复原文
414
+ - `SET_VARIABLE`:解析失败的赋值原文列表(如果有)
415
+ - 以及 `in:` 中声明的所有变量
416
+
417
+ - 函数返回请设定为**三元组列表**,每个三元组对应一个 out 变量:
418
+ `[(is_success, show_to_ai, feedback), ...]`
419
+ - `is_success`:本次赋值是否接受(`True`/`False`)
420
+ - `show_to_ai`:是否将反馈信息展示给 AI(**当前版本预留,未生效**)
421
+ - `feedback`:反馈文本(**当前版本预留,未生效**)
422
+
423
+ - 用户需要提供的函数模块示例:
424
+ def resolve_target(KILL=None, alive_players=None, SET_VARIABLE=None, **kwargs):
425
+ if KILL and KILL in alive_players: # 先尝试用内置解析的结果
426
+ return [(True, False, "")]
427
+ if SET_VARIABLE: # 内置解析失败,自己从 SET_VARIABLE 里找
428
+ import re
429
+ for item in SET_VARIABLE:
430
+ m = re.match(r'KILL\s*=\s*@(\w+)', item)
431
+ if m and f"@{m.group(1)}" in alive_players:
432
+ return [(True, False, "")]
433
+ return [(False, True, "目标不在存活玩家中,请重新选择。")]
434
+
435
+ 暂未实现·计划TODO:
436
+ - `show_to_ai`:本轮输出是否返回给 AI 看。
437
+ True = 将反馈结果是否成功告诉AI,会多一轮次调用LLM,这一轮对话记录也将存入对话历史。
438
+ False = AI输出之后,你不打算把赋值结果反馈再次发给他,省轮次。
439
+ - `feedback`:反馈信息,如果你在prompt中设置了反馈信息的f-string,那么你如果再次发给AI,他就能看到了。
440
+ 适用于某些时候赋值失败,你想提醒AI如何正确的赋值。
441
+ - 当前版本中,`resolve` 的返回值仅用于决定是否接受赋值,**重试机制尚未实现**。
442
+ `max_retries` 和 `fallback` 字段已预留,但引擎不会自动重试。
443
+ 计划在未来版本中支持校验失败后携带 feedback 重新调用 AI。
444
+
445
+ - **`max_retries`**:可选。校验失败时最大重试次数,用尽后走 `fallback`。
446
+ `max_retries = 0` 表示不重试直接 fallback。
447
+ - **`fallback`**:可选。重试用尽后跳转的 action 或 module,可不指定(报错终止)。
448
+
449
+ 计划TODO:
450
+ - **`interrupt`**(打断条件,可选):
451
+ 声明在什么条件下可以打断本轮 AI 输出。
452
+ 打断发生时的行为:停止 AI 输出 → 未执行的工具调用不再执行 → 标记 [Interrupted] → 注入 AI 上下文。
453
+ 支持多种触发源:
454
+ 1. **人类打断**:默认人类发言都会打断。
455
+ 2. **变量条件**:当全局变量满足条件时打断`interrupt: night == true`
456
+ 3. **外部 Python 模块**:复杂条件,执行 Python 函数,返回 True 时打断:`interrupt: game_logic.check_interrupt`
457
+ 4. 特殊参数:如需指定特定人类发言不打断:`interrupt: HUMAN_EXCEPT(@ellis, @bob, @1) , 如果写interrupt: HUMAN_EXCEPT,就代表所有人类都不能发言打断。`
458
+
459
+
460
+ ### 6.2 人类动作
461
+
462
+ action human_vote @human(@player):
463
+ prompt: "请投票:"
464
+ scope: [@hostgod, {alive_players}]
465
+ out: vote_target(dropdown, choices={alive_players}, label="投票")
466
+ resolve: game_logic.resolve_human_vote
467
+
468
+ - 语法与 @ai 基本一致。
469
+ - `out` 中的 `dropdown` 会映射为前端 UI 组件。
470
+ - 人类动作**可选** resolve(如游戏规则要求人类也必须合规操作)。
471
+ - 人类动作**有**上下文,context写法和ai动作一样。
472
+ - 人类动作无需 memory,人类自带强大的memory。
473
+
474
+ 也允许让人类玩家扮演特定 AI 角色,继承该角色的视野(scope):
475
+ action seer_act_human @human(@player) as (@预言家):
476
+ prompt: "你现在是预言家,你要查验谁?"
477
+ scope: [@预言家, @hostgod] # 玩家进入预言家的私密频道
478
+
479
+
480
+ ### 6.3 轻量级赋值 (@assign)
481
+ 支持`=`(直接赋值)、`+=`(加)、`-=`(减)、`= add()`(列表追加)、`= remove()`(列表移除)。
482
+ action next_turn @assign:
483
+ out: day_count += 1
484
+
485
+ action reset_scene @assign:
486
+ out: current_scene = "酒馆"
487
+
488
+ action add_player @assign:
489
+ out: playerInPark = add(@Alice)
490
+ playerInMarket = remove(@Alice)
491
+
492
+ 不调用 LLM。
493
+ 变量必须已在 `vars:` 中声明,否则引擎报错。
494
+ assign只支持简单的赋值操作,需要函数调用请用 `@func`。
495
+
496
+
497
+ ### 6.4 Python 函数调用(@func)
498
+
499
+ `@func` 用于调用用户在 `code:` 区域声明的 Python 文件中的函数。
500
+ #### 1. 基本用法
501
+ code 区先声明模块,再用 `模块.函数名` 调用:
502
+ code:
503
+ my_utils = file:"utils/game.py"
504
+
505
+ action tick @func(my_utils.wait_and_tick):
506
+ in: hour
507
+ out: hour, mood
508
+
509
+ - `in` 列出要传给函数的变量,变量名与函数参数名相同可直接写,不同则用 `参数名 = 变量表达式`。
510
+ - `out` 声明接收返回值的变量,按函数返回结构自动映射。
511
+ **核心原则**:`in` 给什么,函数就收什么;函数返回什么,`out` 就接什么,编译器将无脑赋值,不做转换。
512
+
513
+
514
+ #### 2. 输入传参(in)
515
+
516
+ 支持两种模式:显式声明和自动推断。
517
+ **显式声明**:
518
+ 在 action 中写 `in:`,引擎严格按照声明传参。
519
+ action check @func(my_utils.check_soul):
520
+ in:
521
+ target_name = seer_check_target
522
+ souls_dict = souls
523
+ out: seer_check_result
524
+ 左边是 Python 函数的参数名,右边是 FEM 全局变量表达式。编译器将先求值右边的表达式,再传给左边对应的参数。
525
+
526
+ **自动推断**:
527
+ action 没有写 `in:` 时,运行时根据 Python 函数的参数签名,去全局 `vars` 中查找**同名**变量传入。
528
+ 不支持模糊匹配。若找不到同名变量且参数无默认值,引擎报错退出;若有默认值,则跳过该参数。
529
+
530
+
531
+ #### 3. 输出接收(out)
532
+ 无论函数返回什么,编译器都按以下规则把结果塞进 `out` 声明的变量中。
533
+ **函数返回字典**
534
+ 如果 Python 函数返回一个 `dict`,编译器会按 key 名匹配 `out` 中声明的变量名,将对应的值写入。
535
+ - Python 函数
536
+ def resolve_night(kill_target, save, ...):
537
+ return {"dead_tonight": dead, "alive": new_alive}
538
+ - FEM 剧本
539
+ action resolve @func(my_utils.resolve_night):
540
+ in: kill_target, save
541
+ out: dead_tonight, alive
542
+
543
+ - 返回字典的 key 必须与 `out` 中声明的变量名**完全一致**,数量必须相等。
544
+ - 如果返回字典中有未在 `out` 中声明的 key,引擎报错退出。
545
+ - 如果 `out` 中声明的变量在返回字典中不存在,引擎报错退出。
546
+
547
+ **函数返回单值(非字典)**
548
+ 如果函数返回字符串、数字等单一值,且 `out` **恰好声明了一个变量**,则直接赋值。
549
+ - Python 函数
550
+ def collect_vote(voter_name, target):
551
+ return target
552
+ - FEM 剧本
553
+ action collect_vote @func(werewolf_utils.collect_vote):
554
+ in:
555
+ voter_name = @voter
556
+ target = vote
557
+ out: vote_results.@voter(string, "")
558
+ - 函数返回 `"p3"`,编译器自动执行 `vote_results["@p1"] = "p3"`。
559
+ - 函数只需返回最终值,不要自己去组装字典。
560
+ - 如果 `out` 声明了多个变量但函数只返回一个单值,引擎报错退出。
561
+
562
+ **写入字典的特定键**
563
+ 当 `out` 写成 `dict.key` 形式时,编译器会把返回值直接写入该字典的指定键。
564
+ 格式:`out: 字典名.键名(类型, "标签")`
565
+ - 如果字典本身在 `vars:` 中已声明,函数返回的值直接写入对应键。
566
+ - 如果字典不存在(未在 `vars:` 中声明),引擎报错退出。
567
+ - 函数仍然只返回最终值,编译器负责字典写入。
568
+
569
+ #### 暂未实现:内置特殊函数 `system.spawn`
570
+ 用于动态创建临时角色:
571
+ action spawn_team @func(system.spawn):
572
+ in: spawn_requests
573
+ out: team_members
574
+ - 上游 agent 输出 `spawn_requests`,如 `[{blueprint: coder_blueprint, count: 2}]`。
575
+ - `system.spawn` 根据蓝图生成临时角色并返回列表 `[@coder_4, @coder_5]`。
576
+ - 返回的列表可直接用于 `par` 遍历。
577
+ - 临时角色在 Flow 结束后由引擎自动回收。
578
+ - 动态建队完整示例
579
+ action plan @ai(hostgod):
580
+ out: spawn_requests(array, "建队申请"), task_list(object, "任务")
581
+
582
+ action spawn_team @func(system.spawn):
583
+ in: spawn_requests
584
+ out: team_members
585
+
586
+ flow:
587
+ [START] -> plan -> spawn_team
588
+ spawn_team -> par coder in team_members -> &CoderSandbox(coder, task_list[coder])
589
+ join(all) -> [END]
590
+
591
+ 工作流程:manager 规划 → system.spawn 按蓝图生成角色 → 并行分配任务 → 汇总结束。
592
+
593
+ ####@actor 变量与 Python 函数的衔接
594
+ 当 `in:` 中声明的变量是 `@actor` 类型时,引擎会**自动将其解析为结构化字典**再传给 Python 函数,因为 Python 不认识 FEM 的 `@actor` 语法。
595
+
596
+ 解析规则:
597
+ 如果剧本中有:
598
+ vars:
599
+ hp = {@ellis: 100, @bob: 80}
600
+ location = {@ellis: "酒馆", @bob: "公园"}
601
+
602
+ 1. 当 @ellis 传给 Python 函数时,收到的字典是:
603
+ {
604
+ "type": "ai",
605
+ "name": "@ellis",
606
+ "soul": 3,
607
+ "hp": 100,
608
+ "location": "酒馆"
609
+ }
610
+
611
+ 2. 当把 hp.@ellis 传给Python函数时,收到的是hp.@ellis的实时值。
612
+ 在这个例子里就是100.
613
+
614
+ **示例**:
615
+ - FEM 剧本
616
+ action check @func(my_utils.check_soul):
617
+ in: target = @ellis
618
+ out: result
619
+ - Python 函数可以直接访问所有属性:
620
+ def check_soul(actor):
621
+ if actor.get("hp", 100) < 50:
622
+ return {"result": f"{actor['name']} 血量不足,无法行动"}
623
+ if actor["soul"] == 3:
624
+ return {"result": f"{actor['name']} 是狼人,位于{actor.get('location', '未知')}"}
625
+ return {"result": "好人"}
626
+ **规则**:`@actor` 类型变量传入 Python 时,始终打包为该角色的**当前完整状态字典**,包含静态属性(type/name/soul/user)和所有在 `vars` 中以该角色为键的动态属性。
627
+
628
+ #### 常见错误
629
+ | 错误 | 正确 | 说明 |
630
+ |------|------|------|
631
+ | 函数返回完整字典,但 `out` 已指定 `dict.key` | 函数只返回最终值,编译器负责字典写入 | `out: dict.key` 时,函数返回单值即可 |
632
+ | 返回字典的 key 与 `out` 声明的变量不一致 | 确保返回字典的 key 与 `out` 变量一一对应 | 多余或缺失都会报错 |
633
+ | `out` 声明多个变量,函数却返回单值 | `out` 只声明一个变量,或函数返回字典/tuple | 数量必须匹配 |
634
+ | 函数返回 tuple,但数量与 `out` 不匹配 | 确保 tuple 长度与 `out` 变量数相等 | 每个位置对应一个变量 |
635
+
636
+
637
+ ## 7. 模块定义 (modules)
638
+
639
+ 模块是包含子流程的黑盒,方便直接调用,不用管里面怎么写的。
640
+ 模块里面装着一个完整的子流程,有内部vars, 内部actions, 入口、出口、内部flow。有些像python里的class。
641
+
642
+ ### 基本语法
643
+ module ModuleName(参数1, 参数2):
644
+ meta:
645
+ max_steps: 100
646
+ vars:
647
+ 局部变量 = 初值
648
+ action ...
649
+ flow:
650
+ [IN] -> ... -> [OUT]
651
+
652
+ module CoderSandbox(task_var):
653
+ vars:
654
+ finish = false
655
+ action write_code @ai(@coder_actor):
656
+ prompt: |
657
+ 任务: {task_var}
658
+ 你的代码保存在文件夹中,代码测试结果也在文件夹中,请自行查看。
659
+ 你能使用shell工具,请调用shell工具编写代码。
660
+ scope: [@coder_actor]
661
+
662
+ action submit_code @ai(@coder_actor):
663
+ prompt: |
664
+ 你需要测试代码是否能跑通。
665
+ 请使用shell工具进行测试,测试结果请保存在文件夹中,方便后续查看。
666
+ 如果你认为代码没问题了,请输出赋值变量:
667
+ SET VARIABLE : <<finish = true>>
668
+ out: finish(bool, "结束了没")
669
+
670
+ flow:
671
+ [IN] -> write_code -> submit_code ->
672
+ fork:
673
+ -> (if finish == true)[OUT]
674
+ -> (if finish == false)[IN]
675
+
676
+ module DevLoop(task_var):
677
+ vars:
678
+ submit = "discuss"
679
+ action reviewer @ai(@CEO)
680
+ prompt: |
681
+ 审查任务{task_var}的代码。代码在文件夹里你自己调用工具去看。
682
+ 如果你认为代码有问题,你现在可以和写代码的AI讨论,没讨论完时不要输出任何变量赋值。
683
+ 如果你觉得讨论清楚了,决定让写代码的ai去修改,请输出SET VARIABLE : <<submit = "revise">>
684
+ 如果你认为讨论可以结束了,代码很好,请输出SET VARIABLE : <<submit = "goodjob">>
685
+ scope: [@CEO,@coder_actor]
686
+ out: submit
687
+
688
+ action reviewer2 @ai(@coder_actor)
689
+ prompt: "这是CEO在审查你的代码,你们可以讨论。"
690
+ scope: [@CEO, @coder_actor]
691
+
692
+ flow:
693
+ [IN] -> [A]:&CoderSandbox ->
694
+ -> [B]:reviewer -> [C]:reviewr2 ->
695
+ fork:
696
+ -> (if submit == "goodjob") -> [OUT]
697
+ -> (if submit == "revise") -> [IN] # 回到[IN],重写。
698
+ -> (if submit == "discuss") -> [B] # CEO未改变赋值,回到[B],继续讨论的小循环。
699
+
700
+ - **参数与变量**
701
+ 括号内参数可直接在模块内用 {参数名} 引用。如果无需传入参数,可直接写module CoderSandbox:
702
+ 内部 vars 为局部变量,离开模块后清空。
703
+ 需要重命名时使用 in。
704
+
705
+ - **内部锚点**:
706
+ `[IN]`:模块入口。数据从这里流入。
707
+ `[OUT]`:模块正常出口。数据从这里流出,后续可接其他节点。
708
+ `[BREAK]`:模块中断出口。跳出模块且本分支停止,后面不接任何东西。
709
+
710
+ ### 调用方式
711
+ - 在模块内部flow或项目mainflow中,引用模块时加 `&` 前缀以区分action。&ModuleName,或者&ModuleName(args)都行。
712
+ - **嵌套**:模块内可调用其他模块,使用`&moduleName`引用其他module。
713
+ - 模块允许递归调用自己,但:
714
+ **警告**:模块退出后局部变量会清空,不会传递给母模块。所以必须使用全局变量来控制跳出。
715
+ **警告**:不可以设置max_steps参数防止死循环。我们设定的是模块内部steps和全局steps分别计数,所以模块内部步数在全局这里永远为1. 子模块的步数在母模块这里也永远为1,会陷入死循环!
716
+ 所以最好还是别递归了吧。
717
+
718
+
719
+
720
+ ## 8. 节点与流程编排 (flow)
721
+
722
+ **核心概念区分**:
723
+ - **Action**:是行为(做什么),无位置信息。
724
+ - **Module**:是黑箱(装着子流程),有入口出口。
725
+ - **Node**:节点是位置标记和容器,一个 Node 可以装一个 action 或 一个module。
726
+ - **Flow**:将 Node 串起来。
727
+
728
+ 约定:
729
+ - Action 引用:无前缀(如 `action1`),书写流畅。
730
+ - Module 引用:`&` 前缀(如 `&small_module`),一眼看出这是个黑箱。后面可以加(arg)也可不加。
731
+ - 节点与位置:方括号代表位置。`[NodeName]`、`[IN]`、`[OUT]`、`[BREAK]`,`[START]`、`[END]`方括号不可省略。
732
+
733
+ **8.1 Node(节点)与动作**
734
+ Node 是 Flow 中的位置标记。
735
+ 当只是单独的链子时,偷懒只写action名也没问题。
736
+ 但当流程有循环时,必须使用节点来标明位置,以区分"再次执行"和"循环回去":
737
+ - 这不是循环:
738
+ action1 -> action2 -> action3 -> action1 # 只是又执行了一遍action1的顺序执行。
739
+ - 这才是循环:
740
+ [A]:action1 -> [B]:action2 -> [C]:action3 -> [A] # 回到A的**位置**,之后会继续跑B和C的位置。
741
+
742
+ **节点内容定义**:
743
+ 以下定义方法都支持:
744
+ [A]: myaction1
745
+ [B]: &modulename
746
+ [C]: &mymodule(arg)
747
+ [A] -> [D]:actionName2 -> [E]:&mymodule(arg) -> [B] -> [A]
748
+ - 节点允许不绑定任何动作或模块,空节点可以作为占位符。编译器遇到空节点,会继续往下一个节点运行。
749
+
750
+ ### 8.2 顺序链
751
+
752
+ [START] -> action0 -> [B]:action1 -> &module1 -> [END]
753
+ - `[START]` 和 `[END]` 是保留关键字,必须大写。
754
+
755
+ 对于单行链太长时,我们可以拆成多行写。只要首尾节点能接上,那就能续上。
756
+ 但需注意:只有**节点**能接上,因为节点代表位置。Action和module名是无法发生接续的。
757
+ 续行例子:
758
+ [IN] -> wolf_discuss -> seer_check -> seer_result -> [A]:tell_seer
759
+ //中间甚至可以写点别的链子,顺序不敏感,类似mermaid语法。
760
+ [A] -> witch_save_ask -> witch_poison_ask -> resolve_night -> [OUT]
761
+
762
+
763
+ ### 8.3 高级语法
764
+
765
+ #### 并发分支(fork):
766
+ 以fork开头声明此处有分支。fork只管分支,不管合并。
767
+ 分支是并发的,每当遇到分支,编译器会将分支全部运行在新的子线程中。调用LLM需要时间,分支很适合同时调用多个LLM。
768
+ 前链最后指向该节点,下一行将该节点作为母节点,fork加冒号产生分支。
769
+ 分支缩进的“->”指的是从母节点分出的不同路径。
770
+ [A] -> [B] -> [C] ->
771
+ fork:
772
+ -> [visit]
773
+ -> [solo]
774
+
775
+ 另一种分支写法是同缩进多行:
776
+ [A] -> [B] -> [C]
777
+ [C] -> [visit]
778
+ [C] -> [solo]
779
+
780
+ #### 条件判断并行分支(if):
781
+ 当你需要条件判断时,可以简单的在边上加if。
782
+ 无if 和 if 的区别:无if则无条件全通并行,有if时判断为真的才通(也是并行)。
783
+ 编译器会将if判断语句直接交给python,让python去解析。支持任意合法的 Python 表达式。
784
+ 表达式中的 `@actor` 类型变量会自动翻译为 Python 能识别的字典后再求值,所以你可以大胆的在这里使用@actor类型。
785
+
786
+ [A] ->
787
+ fork:
788
+ -> if (var == true) -> [B]
789
+ -> if (score > 5 and level >= 3) -> [C]
790
+
791
+ #### 混合并行分支:
792
+ [A] ->
793
+ fork:
794
+ -> [B]
795
+ -> if (@Portia.hp == 0) -> [C]
796
+ -> if (hp.@Portia >= 100) -> [C]
797
+
798
+
799
+ ### 合并 (join):
800
+ join(all):
801
+ [B] ->
802
+ [C] ->
803
+ to [next_node]
804
+
805
+ Join 在接收端定义,明确声明"我要等谁"。
806
+ 只写上一个node就行,不用写更远的链路。
807
+ 括号里的参数:
808
+ - "all":等所有声明的上游节点都到齐后,执行 [next_node]。
809
+ - 某个数字几:在声明的上游节点中,只要到了几个,就执行 [next_node],其他剩余没到的任务不跑了,直接关闭任务。
810
+ **关于线程管理**:编译器会在解析流程图时预先规划任务。fork 产生多任务并行,join 合并。
811
+ `join(n)` 掐掉其他分支时,只会掐掉属于本次 fork 的任务,不会影响其他模块对同一 action 的调用。
812
+ (这要求编译器为每次 fork 生成唯一的 fork_id 来追踪任务归属。)
813
+
814
+
815
+ ### 串行循环(for):
816
+ for含义:遍历 speaker_array,对每个元素依次执行 [C],串行任务。
817
+ 循环变量 @speaker 会自动绑定到该 action 的 @actor_expr(如果 action 的 @actor_expr 是动态变量)。
818
+ for结束后的下一行,必须以->开头指明for循环结束后去哪里。
819
+ 例子:
820
+ [A] -> [B] ->
821
+ for @speaker in speaker_array:
822
+ -> [C] -> [D] ->
823
+ -> [END]
824
+ 这相当于比如说:
825
+ [A] -> [B] -> [C] -> [D] -> [C] -> [D] -> [C] -> [D] -> [C] -> [D] -> [C] -> [D] -> [END]
826
+
827
+ for循环可以和if共同使用,起到了一定的fork效果:比如这里有个狼人杀的例子:
828
+ [B] ->
829
+ for @player in alive_players:
830
+ -> if (@player.type == ai) -> ai_speak ->
831
+ -> if (@player.type == human) -> human_speak ->
832
+ -> [D]
833
+ 这相当于比如说:
834
+ [B] -> ai_speak -> ai_speak -> ai_speak -> ai_speak -> human_speak -> ai_speak -> [D]
835
+ 编译器会自动将 `@player` 绑定到被调用的 action 的参数 `(@player)` 中。
836
+
837
+ for内部内容,可以多行,语法和外部的链式语法一样。(类似mermaid,顺序不敏感)。
838
+ 但是特别的,我们需要知道for内的内容从哪里开始,到哪里结束进入下一轮循环。
839
+ 所以我们额外要求:
840
+ 在开始节点之前加一个"->"标明循环从这里开始,
841
+ 在结束节点之后加一个"->"标明此处本轮循环结束、进入下一轮循环或是完全结束。
842
+ ->开头的两行并列,就是说这里有分支。for内可以用多个开始符号"->"来创造多条分支,以方便的实现不同if条件下略微不同的循环。分支是并行的,类似fork。
843
+
844
+ ### 并发(par):
845
+
846
+ par含义:
847
+ 相当于先fork出很多条线,再join到同一点。但每条路径上的动作都相同或者极度相似,我们可以用par来简化写法。
848
+ 也可以理解成for的并发版。并发,无先后顺序,比如并发请求 LLM,极大提升运行速度。
849
+ 在par函数体内部如果有多个node,依然是串行执行的。
850
+ par结束后的下一行,必须以->开头指明par结束后去哪里。
851
+ 并发参数作用域仅限当前行及调用的模块参数。
852
+ **并发参数传参**:`par` 的并发参数(如 `coder`)可以作为参数传入模块,
853
+ 模块内部可直接通过变量名引用,或作为 `@ai({变量名})` 的动态 actor。
854
+
855
+ 例子1:
856
+ [A] ->
857
+ par @coder in coders:
858
+ -> &CoderSandbox(@coder, task_list[@coder]) ->
859
+ -> [D]
860
+ 这相当于:
861
+ |-> &CoderSandbox(@coder_1, task_list[@coder_1]) ->|
862
+ [A] ->|-> &CoderSandbox(@coder_2, task_list[@coder_2]) ->|-> [D]
863
+ |-> &CoderSandbox(@coder_3, task_list[@coder_3]) ->|
864
+ ...很多,假设coders很多,但你懒得一个一个用fork写了...
865
+ |-> &CoderSandbox(@coder_n, task_list[@coder_n]) ->|
866
+
867
+ 例子2:
868
+ [START] ->
869
+ par @coder in workers:
870
+ -> [A]:writecode -> [B]:trycode ->
871
+ -> [D]
872
+ 这相当于遍历 workers,每个角色并发出发,依次执行 [A] 和 [B]。
873
+ 并发参数 @coder 写入各自线程的局部上下文,action 内部直接通过变量名取值:
874
+ coder1:|-> writecode -> trycode ->|
875
+ [START] -> coder2:|-> writecode -> trycode ->| -> [D]
876
+ coder3:|-> writecode -> trycode ->|
877
+ ...很多,假设coders很多,但你懒得一个一个用fork写了...
878
+ codern:|-> writecode -> trycode ->|
879
+ 注意:@coder 是线程局部变量,不同线程取值不同,互不干扰。
880
+ action 内部直接用 {c} 或 @coder 引用即可,无需在 flow 中显式传参。
881
+ for的循环变量,par的并发参数名必须与 action 定义括号里的动态变量名一致,否则引擎会直接报错并提示变量名不匹配。这样就不会出现静默用错人的情况。
882
+
883
+ par内部内容,可以多行,语法和外部的链式语法一样。(类似mermaid,顺序不敏感)。
884
+ 但是特别的,我们需要知道par内的内容从哪里开始,到哪里结束进入下一轮循环。
885
+ 所以我们额外要求:
886
+ 在开始节点之前加一个"->"标明循环从这里开始,
887
+ 在结束节点之后加一个"->"标明此处本轮循环结束、进入下一轮循环或是完全结束。
888
+ ->开头的两行并列,就是说这里有分支。par内可以用多个开始符号"->"来创造多条分支,以方便的实现不同if条件下略微不同的循环。分支是并行的,类似fork。
889
+
890
+
891
+
892
+ ## 9. mainflow 主流程
893
+ 每个.fems剧本,有唯一的mainflow区块。
894
+ 必须有[START]和[END],标明流程图的开始和结束。编译器运行时,会从这里的[START]开始运行。
895
+ 语法和之前说的一样,可以调用action和module。
896
+
897
+
898
+ ## 10. 核心机制
899
+
900
+ - 同一 scope 内的 Agent:
901
+ 上下文自动共享,他们能看到彼此的聊天记录,然后直接聊天就行了。
902
+ LLM不是函数,他们是智慧体,他们直接聊天。
903
+ - 跨 scope 的 Agent:
904
+ 他们没有共同经历那件事,所以当然记忆不互通。但他们依然可以通过工具调用等方式,接触到对方存的文件等。
905
+ FEM编译器也支持他们互相传送全局变量,但其实没必要,不如用shell工具去读文件。
906
+ LLM不是函数,他们是智慧体,他们调用工具。
907
+ - FEM编译器的变量赋值:
908
+ 不是用来让AI们彼此传递信息的。
909
+ 变量赋值主要是为了控制流程图的走向,以及控制scope等。
910
+ LLM智慧体可以主动通过变量赋值,来决定自己下一步的走向(只要你在prompt里告诉他们该怎么做)。
911
+ 有的时候为了控制工作流的流程,也可以要求人类在action动作中给某变量赋值。
912
+ - Module
913
+ 内部参数自动绑定。如果Module 内需要重命名,可以用in来映射。
914
+ - for的循环变量,par的并发参数
915
+ 自动绑定到模块参数
916
+
917
+
918
+
919
+ ## 11. **FEM 编译器报错信息**
920
+
921
+ - 变量未声明 → 报错,指出变量名
922
+ - 文件不存在 → 报错,指出文件路径
923
+ - 方法未定义 → 报错,指出方法名
924
+ - Scope 中有无法解析的元素 → 报错
925
+ - 数据库约束违反 → 报错(原样抛出异常)
926
+
927
+
928
+ ## 12. 完整示例
929
+
930
+ meta:
931
+ name = 测试剧本
932
+ database = file:"test.db"
933
+ owner = [1]
934
+ system_safety = 这是一个安全提示
935
+ session = new
936
+
937
+ vars:
938
+ memory_text = ""
939
+ context_text = ""
940
+ reply = ""
941
+
942
+ code:
943
+ memfile = file:"utils/MemoryExample.py"
944
+ ctxfile = file:"utils/ContextExample.py"
945
+
946
+ actors:
947
+ ai @ellis = soul:1
948
+
949
+ memory rag10(memfile.retrieve_example):
950
+ in: prompt, session_id, @actor
951
+ out: memory_text
952
+
953
+ context thisSession(ctxfile.findThisSession):
954
+ in: session, @actor
955
+ out: context_text
956
+
957
+ action speak @ai(@ellis):
958
+ prompt: "你好,请随便说点什么。"
959
+ memory: rag10
960
+ context: thisSession
961
+ out: reply
962
+
963
+ flow:
964
+ [START] -> speak -> [END]