plaita-nodes 0.3.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.
- plaita_nodes-0.3.0/.github/workflows/publish.yml +25 -0
- plaita_nodes-0.3.0/.gitignore +10 -0
- plaita_nodes-0.3.0/AGENTS.md +47 -0
- plaita_nodes-0.3.0/PKG-INFO +98 -0
- plaita_nodes-0.3.0/README.md +80 -0
- plaita_nodes-0.3.0/pyproject.toml +46 -0
- plaita_nodes-0.3.0/src/plaita_nodes/__init__.py +53 -0
- plaita_nodes-0.3.0/src/plaita_nodes/agent_run.py +232 -0
- plaita_nodes-0.3.0/src/plaita_nodes/api.py +120 -0
- plaita_nodes-0.3.0/src/plaita_nodes/capture.py +96 -0
- plaita_nodes-0.3.0/src/plaita_nodes/config.py +178 -0
- plaita_nodes-0.3.0/src/plaita_nodes/database.py +95 -0
- plaita_nodes-0.3.0/src/plaita_nodes/email.py +64 -0
- plaita_nodes-0.3.0/src/plaita_nodes/gate.py +83 -0
- plaita_nodes-0.3.0/src/plaita_nodes/hitl.py +126 -0
- plaita_nodes-0.3.0/src/plaita_nodes/hitl_await.py +110 -0
- plaita_nodes-0.3.0/src/plaita_nodes/hitl_poller.py +96 -0
- plaita_nodes-0.3.0/src/plaita_nodes/llm.py +130 -0
- plaita_nodes-0.3.0/src/plaita_nodes/notify.py +35 -0
- plaita_nodes-0.3.0/src/plaita_nodes/rate_limit.py +79 -0
- plaita_nodes-0.3.0/src/plaita_nodes/report.py +89 -0
- plaita_nodes-0.3.0/src/plaita_nodes/webhooks.py +116 -0
- plaita_nodes-0.3.0/src/plaita_nodes/write_file.py +52 -0
- plaita_nodes-0.3.0/tests/conftest.py +65 -0
- plaita_nodes-0.3.0/tests/fake_exec.py +15 -0
- plaita_nodes-0.3.0/tests/test_agent_run.py +83 -0
- plaita_nodes-0.3.0/tests/test_capture.py +55 -0
- plaita_nodes-0.3.0/tests/test_config.py +75 -0
- plaita_nodes-0.3.0/tests/test_connectors.py +205 -0
- plaita_nodes-0.3.0/tests/test_hitl.py +142 -0
- plaita_nodes-0.3.0/tests/test_hitl_await_distributed.py +129 -0
- plaita_nodes-0.3.0/tests/test_llm.py +103 -0
- plaita_nodes-0.3.0/tests/test_registry.py +11 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# 推送 v* 标签时发布 plaita-nodes 到 PyPI。
|
|
4
|
+
# 前置一次性配置:pypi.org 上为 plaita-nodes 添加 Pending Publisher,
|
|
5
|
+
# owner=jeffkit repo=plaita-nodes workflow=Publish to PyPI environment=pypi
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
publish:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
environment: pypi
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
- uses: actions/setup-python@v6
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.10"
|
|
24
|
+
- run: python -m pip install --upgrade build && python -m build
|
|
25
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# AGENTS.md — plaita-nodes
|
|
2
|
+
|
|
3
|
+
> plaita 通用节点集:AgentRun(经 agentproc)/ Capture / Hitl / Notify / WriteFile。
|
|
4
|
+
> 大仓 ADR-2026-08-27(编排收敛 plaita + agentproc)的节点承载层。
|
|
5
|
+
|
|
6
|
+
## 项目概述
|
|
7
|
+
|
|
8
|
+
为 plaita 流程提供"接到真实世界"的通用节点:Agent CLI 调用(复用 flowcast 的
|
|
9
|
+
agents/providers 配置,执行走 agentproc Python SDK)、本地命令、微信 HITL、通知、写文件。
|
|
10
|
+
业务粘接节点(读 persona/pool 等)**不放本仓**,放业务仓(如 mediaflow/plaita_flows)。
|
|
11
|
+
|
|
12
|
+
**技术栈:** Python 3.10+ · plaita(兄弟仓 editable)· agentproc(兄弟仓 editable)· requests
|
|
13
|
+
**主仓库:** `git@github.com:jeffkit/plaita-nodes.git`
|
|
14
|
+
|
|
15
|
+
## 架构地图
|
|
16
|
+
|
|
17
|
+
依赖方向:`config.py`(flowcast 兼容配置层)← `agent_run.py`(agentproc executor 注册 + 节点)← 其余节点独立。
|
|
18
|
+
对 plaita 只依赖 `plaita.Node` 基类与 `NodeExecutionContext` 窄接口;对 agentproc 只依赖 `runner.run` + `EXECUTORS` 注册表。
|
|
19
|
+
|
|
20
|
+
关键路径:
|
|
21
|
+
- `src/plaita_nodes/config.py` — agents/providers 加载(flowcast 搜索顺序 + 深合并 + `${VAR}` 插值 + provider→env 翻译)
|
|
22
|
+
- `src/plaita_nodes/agent_run.py` — AgentRunNode + `recursive-direct` executor(语义 = flowcast runRecursiveDirect)
|
|
23
|
+
- `pyproject.toml` — `[project.entry-points."plaita.nodes"]` 注册表
|
|
24
|
+
|
|
25
|
+
## 开发约定
|
|
26
|
+
|
|
27
|
+
**分支:** main(dev/test/prod 同支)。
|
|
28
|
+
**禁止事项:**
|
|
29
|
+
- 业务逻辑入仓(业务节点放业务仓)
|
|
30
|
+
- 日志打印 apiKey / ANTHROPIC_AUTH_TOKEN
|
|
31
|
+
- 依赖 plaita/agentproc 的内部私有 API(只走公开窄接口)
|
|
32
|
+
|
|
33
|
+
**dry-run 契约:** 有副作用的节点一律尊重 `globalContext.dry_run`;writefile 例外(照常写,便于检查草稿)。
|
|
34
|
+
|
|
35
|
+
## 常用命令
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e ".[dev]"
|
|
39
|
+
pytest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 深入阅读
|
|
43
|
+
|
|
44
|
+
| 文档 | 说明 |
|
|
45
|
+
|------|------|
|
|
46
|
+
| `README.md` | 节点表 + 配置兼容性说明 |
|
|
47
|
+
| `../docs/ADR-2026-08-27-orchestration-converge-on-plaita.md` | 决议背景 |
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: plaita-nodes
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: plaita 通用节点集:AgentRun(经 agentproc)/ Capture / Hitl / Notify / WriteFile
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: agentproc>=0.14.0
|
|
8
|
+
Requires-Dist: plaita>=0.5.0
|
|
9
|
+
Requires-Dist: requests>=2.31
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
12
|
+
Requires-Dist: pyyaml; extra == 'dev'
|
|
13
|
+
Provides-Extra: sql
|
|
14
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == 'sql'
|
|
15
|
+
Requires-Dist: pymysql>=1.1; extra == 'sql'
|
|
16
|
+
Requires-Dist: sqlalchemy>=2.0; extra == 'sql'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# plaita-nodes
|
|
20
|
+
|
|
21
|
+
plaita 的**通用节点集**(infra 级):把 plaita 声明式流程接到真实世界——Agent CLI、本地命令、微信人工确认、通知、文件。
|
|
22
|
+
|
|
23
|
+
> 背景:[ADR-2026-08-27 编排双轨收敛](../docs/ADR-2026-08-27-orchestration-converge-on-plaita.md)
|
|
24
|
+
> ——编排内核收敛到 plaita,Agent 执行层统一走 agentproc;本仓是这层决议的节点承载。
|
|
25
|
+
|
|
26
|
+
## 节点一览
|
|
27
|
+
|
|
28
|
+
| type | 节点 | 说明 |
|
|
29
|
+
|------|------|------|
|
|
30
|
+
| `agentrun` | Agent 运行 | **Agent 原子**:多步工具循环(模型可调工具自主多轮)。经 [agentproc](../agentproc) 调用 Agent CLI(recursive / claude);配置复用 flowcast 的 `agents.json` / `providers.json` |
|
|
31
|
+
| `llm` | LLM 补全 | **LLM 原子**:单次 chat/completions 生成文本(OpenAI 兼容端点)。与 agentrun 的边界见下 |
|
|
32
|
+
| `capture` | 命令执行 | 跑本地命令捕获输出;失败不抛错(`exit_code` 返回,流程自行分支) |
|
|
33
|
+
| `hitl` | 人工确认 | 直连 hitl-server(iLink 微信通道):发消息 → 轮询回复 |
|
|
34
|
+
| `notify` | 通知 | terminal 后端(stdout) |
|
|
35
|
+
| `writefile` | 写文件 | UTF-8 写文件,支持 JSON 序列化 |
|
|
36
|
+
|
|
37
|
+
节点经 pyproject 的 `[project.entry-points."plaita.nodes"]` 自动注册;`plaita_nodes.register_all()` 可手动注册。
|
|
38
|
+
|
|
39
|
+
## 原子节点设计原则
|
|
40
|
+
|
|
41
|
+
抽象一条硬标准——**原子性**(一个节点只做一件不可再分的事)、**通用性**
|
|
42
|
+
(不绑定业务语义与特定凭证)、**普适性**(覆盖一类外部交互):
|
|
43
|
+
|
|
44
|
+
- **Agent ≠ LLM**:`agentrun` 是多步工具循环的 Agent 原子(重);`llm` 是单次
|
|
45
|
+
补全的 LLM 原子(轻)。流程里"摘要/改写/抽取/分类"用 `llm`,"多步编码/
|
|
46
|
+
工具任务"用 `agentrun`。
|
|
47
|
+
- 纯文本变换(判决提取、frontmatter 解析等)**不做节点**——注册为表达式
|
|
48
|
+
`F.*` 函数(`ExpressionRegistry.register`),在 assignment 里一行使用。
|
|
49
|
+
- 业务领域的状态机(如内容池销账)属于业务仓,不放本仓。
|
|
50
|
+
|
|
51
|
+
## 快速上手
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# monorepo 内可编辑安装(plaita / agentproc 均为兄弟仓)
|
|
55
|
+
pip install -e ../plaita[http] -e ../agentproc/sdk/python -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from plaita import Flow
|
|
60
|
+
|
|
61
|
+
flow = Flow.from_string(open("flow.json").read())
|
|
62
|
+
result = flow.run(track="default", platforms=["twitter"], topic="", repo="/path/to/repo")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
JSON 用法示例(agentrun + 模板表达式):
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"type": "agentrun", "id": "brief",
|
|
70
|
+
"agent": "glm-52",
|
|
71
|
+
"prompt": "{% $F.concat($INPUT.item.brief_prompt) %}",
|
|
72
|
+
"next": "write"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## agents.json / providers.json 兼容性
|
|
77
|
+
|
|
78
|
+
配置搜索顺序与 flowcast 一致:`~/.flowx → ~/.flowcast → <repo>/.flowcast`(深合并)。
|
|
79
|
+
|
|
80
|
+
与 flowcast 的两处行为差异(有意为之):
|
|
81
|
+
|
|
82
|
+
1. agents.json 里的 `env` 字段 flowcast 白名单会**静默丢弃**,本仓按配置透传
|
|
83
|
+
(如 glm-52 的 `RECURSIVE_MAX_TOKENS`)。
|
|
84
|
+
2. recursive 直路径 flowcast 默认无超时,本仓 `timeout_secs` 默认 1800。
|
|
85
|
+
|
|
86
|
+
## 设计边界
|
|
87
|
+
|
|
88
|
+
- **安全**:任何日志不打 apiKey / ANTHROPIC_AUTH_TOKEN。
|
|
89
|
+
- **dry-run**:所有有副作用的节点尊重 `globalContext.dry_run`——agentrun/capture/hitl 返回 fake 结果,writefile 照常写(草稿便于检查)。
|
|
90
|
+
- **断点续跑**:hitl 为阻塞版(Normal 模式);崩溃级恢复走 plaita Distributed + EventNode 模式(见 ADR phase 2)。
|
|
91
|
+
- 新增执行器:在 `agentproc` executor 层扩展 + `config.EXECUTOR_ALIASES` 加映射,本仓节点无需改动。
|
|
92
|
+
|
|
93
|
+
## 开发
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install -e ".[dev]"
|
|
97
|
+
pytest
|
|
98
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# plaita-nodes
|
|
2
|
+
|
|
3
|
+
plaita 的**通用节点集**(infra 级):把 plaita 声明式流程接到真实世界——Agent CLI、本地命令、微信人工确认、通知、文件。
|
|
4
|
+
|
|
5
|
+
> 背景:[ADR-2026-08-27 编排双轨收敛](../docs/ADR-2026-08-27-orchestration-converge-on-plaita.md)
|
|
6
|
+
> ——编排内核收敛到 plaita,Agent 执行层统一走 agentproc;本仓是这层决议的节点承载。
|
|
7
|
+
|
|
8
|
+
## 节点一览
|
|
9
|
+
|
|
10
|
+
| type | 节点 | 说明 |
|
|
11
|
+
|------|------|------|
|
|
12
|
+
| `agentrun` | Agent 运行 | **Agent 原子**:多步工具循环(模型可调工具自主多轮)。经 [agentproc](../agentproc) 调用 Agent CLI(recursive / claude);配置复用 flowcast 的 `agents.json` / `providers.json` |
|
|
13
|
+
| `llm` | LLM 补全 | **LLM 原子**:单次 chat/completions 生成文本(OpenAI 兼容端点)。与 agentrun 的边界见下 |
|
|
14
|
+
| `capture` | 命令执行 | 跑本地命令捕获输出;失败不抛错(`exit_code` 返回,流程自行分支) |
|
|
15
|
+
| `hitl` | 人工确认 | 直连 hitl-server(iLink 微信通道):发消息 → 轮询回复 |
|
|
16
|
+
| `notify` | 通知 | terminal 后端(stdout) |
|
|
17
|
+
| `writefile` | 写文件 | UTF-8 写文件,支持 JSON 序列化 |
|
|
18
|
+
|
|
19
|
+
节点经 pyproject 的 `[project.entry-points."plaita.nodes"]` 自动注册;`plaita_nodes.register_all()` 可手动注册。
|
|
20
|
+
|
|
21
|
+
## 原子节点设计原则
|
|
22
|
+
|
|
23
|
+
抽象一条硬标准——**原子性**(一个节点只做一件不可再分的事)、**通用性**
|
|
24
|
+
(不绑定业务语义与特定凭证)、**普适性**(覆盖一类外部交互):
|
|
25
|
+
|
|
26
|
+
- **Agent ≠ LLM**:`agentrun` 是多步工具循环的 Agent 原子(重);`llm` 是单次
|
|
27
|
+
补全的 LLM 原子(轻)。流程里"摘要/改写/抽取/分类"用 `llm`,"多步编码/
|
|
28
|
+
工具任务"用 `agentrun`。
|
|
29
|
+
- 纯文本变换(判决提取、frontmatter 解析等)**不做节点**——注册为表达式
|
|
30
|
+
`F.*` 函数(`ExpressionRegistry.register`),在 assignment 里一行使用。
|
|
31
|
+
- 业务领域的状态机(如内容池销账)属于业务仓,不放本仓。
|
|
32
|
+
|
|
33
|
+
## 快速上手
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# monorepo 内可编辑安装(plaita / agentproc 均为兄弟仓)
|
|
37
|
+
pip install -e ../plaita[http] -e ../agentproc/sdk/python -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from plaita import Flow
|
|
42
|
+
|
|
43
|
+
flow = Flow.from_string(open("flow.json").read())
|
|
44
|
+
result = flow.run(track="default", platforms=["twitter"], topic="", repo="/path/to/repo")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
JSON 用法示例(agentrun + 模板表达式):
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"type": "agentrun", "id": "brief",
|
|
52
|
+
"agent": "glm-52",
|
|
53
|
+
"prompt": "{% $F.concat($INPUT.item.brief_prompt) %}",
|
|
54
|
+
"next": "write"
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## agents.json / providers.json 兼容性
|
|
59
|
+
|
|
60
|
+
配置搜索顺序与 flowcast 一致:`~/.flowx → ~/.flowcast → <repo>/.flowcast`(深合并)。
|
|
61
|
+
|
|
62
|
+
与 flowcast 的两处行为差异(有意为之):
|
|
63
|
+
|
|
64
|
+
1. agents.json 里的 `env` 字段 flowcast 白名单会**静默丢弃**,本仓按配置透传
|
|
65
|
+
(如 glm-52 的 `RECURSIVE_MAX_TOKENS`)。
|
|
66
|
+
2. recursive 直路径 flowcast 默认无超时,本仓 `timeout_secs` 默认 1800。
|
|
67
|
+
|
|
68
|
+
## 设计边界
|
|
69
|
+
|
|
70
|
+
- **安全**:任何日志不打 apiKey / ANTHROPIC_AUTH_TOKEN。
|
|
71
|
+
- **dry-run**:所有有副作用的节点尊重 `globalContext.dry_run`——agentrun/capture/hitl 返回 fake 结果,writefile 照常写(草稿便于检查)。
|
|
72
|
+
- **断点续跑**:hitl 为阻塞版(Normal 模式);崩溃级恢复走 plaita Distributed + EventNode 模式(见 ADR phase 2)。
|
|
73
|
+
- 新增执行器:在 `agentproc` executor 层扩展 + `config.EXECUTOR_ALIASES` 加映射,本仓节点无需改动。
|
|
74
|
+
|
|
75
|
+
## 开发
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install -e ".[dev]"
|
|
79
|
+
pytest
|
|
80
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "plaita-nodes"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "plaita 通用节点集:AgentRun(经 agentproc)/ Capture / Hitl / Notify / WriteFile"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = [
|
|
13
|
+
"plaita>=0.5.0",
|
|
14
|
+
"agentproc>=0.14.0",
|
|
15
|
+
"requests>=2.31",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = ["pytest>=8", "pyyaml"]
|
|
20
|
+
sql = ["sqlalchemy>=2.0", "psycopg2-binary>=2.9", "pymysql>=1.1"]
|
|
21
|
+
|
|
22
|
+
[project.entry-points."plaita.nodes"]
|
|
23
|
+
agentrun = "plaita_nodes.agent_run:AgentRunNode"
|
|
24
|
+
capture = "plaita_nodes.capture:CaptureNode"
|
|
25
|
+
hitl = "plaita_nodes.hitl:HitlNode"
|
|
26
|
+
llm = "plaita_nodes.llm:LlmNode"
|
|
27
|
+
gate = "plaita_nodes.gate:GateNode"
|
|
28
|
+
rate_limit = "plaita_nodes.rate_limit:RateLimitNode"
|
|
29
|
+
report = "plaita_nodes.report:ReportNode"
|
|
30
|
+
hitl_await = "plaita_nodes.hitl_await:HitlAwaitNode"
|
|
31
|
+
notify = "plaita_nodes.notify:NotifyNode"
|
|
32
|
+
writefile = "plaita_nodes.write_file:WriteFileNode"
|
|
33
|
+
feishu_webhook = "plaita_nodes.webhooks:FeishuWebhookNode"
|
|
34
|
+
wecom_webhook = "plaita_nodes.webhooks:WecomWebhookNode"
|
|
35
|
+
slack_webhook = "plaita_nodes.webhooks:SlackWebhookNode"
|
|
36
|
+
dingtalk_webhook = "plaita_nodes.webhooks:DingtalkWebhookNode"
|
|
37
|
+
generic_webhook = "plaita_nodes.api:GenericWebhookNode"
|
|
38
|
+
api_request = "plaita_nodes.api:ApiRequestNode"
|
|
39
|
+
sql_query = "plaita_nodes.database:SqlQueryNode"
|
|
40
|
+
email_send = "plaita_nodes.email:EmailSendNode"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/plaita_nodes"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""plaita-nodes —— plaita 通用节点集(infra 级,ADR-2026-08-27)。
|
|
2
|
+
|
|
3
|
+
节点通过 pyproject 的 ``[project.entry-points."plaita.nodes"]`` 自动注册;
|
|
4
|
+
若以源码方式引入(未经 pip 安装),可显式调用 :func:`register_all`。
|
|
5
|
+
"""
|
|
6
|
+
from .agent_run import AgentRunError, AgentRunNode, extract_recursive_result, register_recursive_direct
|
|
7
|
+
from .api import ApiRequestNode, GenericWebhookNode
|
|
8
|
+
from .database import SqlQueryNode
|
|
9
|
+
from .email import EmailSendNode
|
|
10
|
+
from .capture import CaptureConfigError, CaptureNode
|
|
11
|
+
from .config import (
|
|
12
|
+
EXECUTOR_ALIASES,
|
|
13
|
+
AgentConfigError,
|
|
14
|
+
interpolate,
|
|
15
|
+
load_merged_config,
|
|
16
|
+
resolve_agent,
|
|
17
|
+
resolve_provider,
|
|
18
|
+
)
|
|
19
|
+
from .hitl import HitlError, HitlNode
|
|
20
|
+
from .gate import GateNode
|
|
21
|
+
from .hitl_await import HitlAwaitNode
|
|
22
|
+
from .llm import LlmError, LlmNode
|
|
23
|
+
from .rate_limit import RateLimitNode
|
|
24
|
+
from .report import ReportNode, append_entry, read_entries
|
|
25
|
+
from .notify import NotifyNode
|
|
26
|
+
from .webhooks import DingtalkWebhookNode, FeishuWebhookNode, SlackWebhookNode, WecomWebhookNode
|
|
27
|
+
from .write_file import WriteFileNode
|
|
28
|
+
|
|
29
|
+
__version__ = "0.3.0"
|
|
30
|
+
|
|
31
|
+
_ALL_NODES = [AgentRunNode, ApiRequestNode, CaptureNode, DingtalkWebhookNode, EmailSendNode, FeishuWebhookNode, GateNode, GenericWebhookNode, HitlAwaitNode, HitlNode, LlmNode, NotifyNode, RateLimitNode, ReportNode, SlackWebhookNode, SqlQueryNode, WecomWebhookNode, WriteFileNode]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def register_all() -> None:
|
|
35
|
+
"""显式注册全部节点到默认 registry(entry_points 之外的手动路径)。"""
|
|
36
|
+
from plaita.node import get_default_registry
|
|
37
|
+
|
|
38
|
+
registry = get_default_registry()
|
|
39
|
+
for node_cls in _ALL_NODES:
|
|
40
|
+
registry.register(node_cls)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"AgentRunNode", "AgentRunError", "extract_recursive_result", "register_recursive_direct",
|
|
45
|
+
"CaptureNode", "CaptureConfigError",
|
|
46
|
+
"HitlNode", "HitlAwaitNode", "HitlError", "LlmNode", "LlmError", "ReportNode", "report_append", "report_read",
|
|
47
|
+
"NotifyNode",
|
|
48
|
+
"WriteFileNode",
|
|
49
|
+
"register_all",
|
|
50
|
+
"load_merged_config", "resolve_agent", "resolve_provider", "interpolate",
|
|
51
|
+
"AgentConfigError", "EXECUTOR_ALIASES",
|
|
52
|
+
"__version__",
|
|
53
|
+
]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""AgentRunNode —— 经 agentproc 调用 Agent CLI(recursive / claude / …)。
|
|
2
|
+
|
|
3
|
+
设计(ADR-2026-08-27):
|
|
4
|
+
- 配置层复用 flowcast 的 agents.json / providers.json(见 config.py);
|
|
5
|
+
- 执行层走 agentproc Python SDK 的 in-process executor 机制;
|
|
6
|
+
- ``recursive-direct`` 是本包注册的直调执行器,语义照搬 flowcast 的
|
|
7
|
+
``runRecursiveDirect``:``recursive --workspace . --output-format json
|
|
8
|
+
[--model M] [--max-steps N] run "<prompt>"``,stdout 末尾为单个 JSON
|
|
9
|
+
对象,取 ``.result`` 为回复、``.is_error`` 为失败。
|
|
10
|
+
- 输出:``{"text", "cli", "session_id", "usage", "dry_run"}``,
|
|
11
|
+
下游用 ``$NODE.<id>.text`` 引用。
|
|
12
|
+
|
|
13
|
+
日志脱敏:任何路径都不打印 apiKey / ANTHROPIC_AUTH_TOKEN。
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
from typing import Any, ClassVar, Optional
|
|
21
|
+
|
|
22
|
+
from pydantic import Field
|
|
23
|
+
|
|
24
|
+
from plaita import Node
|
|
25
|
+
|
|
26
|
+
from .config import EXECUTOR_ALIASES, resolve_agent
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AgentRunError(RuntimeError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── recursive 直调执行器(语义 = flowcast runRecursiveDirect)──────────
|
|
34
|
+
|
|
35
|
+
def _make_recursive_handlers():
|
|
36
|
+
def build_args(message: str, session_id: str, env: dict) -> list:
|
|
37
|
+
bin_name = env.get("RECURSIVE_BIN", "recursive")
|
|
38
|
+
args = [bin_name, "--workspace", env.get("RECURSIVE_WORKSPACE", "."),
|
|
39
|
+
"--output-format", "json"]
|
|
40
|
+
if env.get("RECURSIVE_MODEL"):
|
|
41
|
+
args += ["--model", env["RECURSIVE_MODEL"]]
|
|
42
|
+
if env.get("RECURSIVE_MAX_STEPS"):
|
|
43
|
+
args += ["--max-steps", str(env["RECURSIVE_MAX_STEPS"])]
|
|
44
|
+
args += ["run", message]
|
|
45
|
+
return args
|
|
46
|
+
|
|
47
|
+
return {"build_args": build_args}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register_recursive_direct() -> None:
|
|
51
|
+
from agentproc import EXECUTORS
|
|
52
|
+
|
|
53
|
+
if "recursive-direct" not in EXECUTORS:
|
|
54
|
+
EXECUTORS["recursive-direct"] = {
|
|
55
|
+
"cli_name": "recursive",
|
|
56
|
+
"install_hint": "brew install kongjie/tap/recursive(或 cargo build --release)",
|
|
57
|
+
# plain:stdout 整体作为 reply(recursive --output-format json 输出单个
|
|
58
|
+
# JSON 对象而非 NDJSON),由本节点再做 .result 提取。
|
|
59
|
+
"plain": True,
|
|
60
|
+
"make_handlers": _make_recursive_handlers,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
register_recursive_direct()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def extract_recursive_result(stdout: str) -> dict:
|
|
68
|
+
"""从 recursive 的 stdout 提取末尾 JSON 对象(``--output-format json`` 为
|
|
69
|
+
“单个结果对象收尾”)。
|
|
70
|
+
|
|
71
|
+
策略:优先取**行首 ``{`` 起始**的最后一个可解析对象(日志噪声多出现在行中);
|
|
72
|
+
退回首个 ``{"`` 的括号配对扫描(跳过字符串内花括号,与 flowcast
|
|
73
|
+
extractRecursiveJson 同款);全部失败则把原文当 result 返回。
|
|
74
|
+
"""
|
|
75
|
+
line_starts: list[int] = []
|
|
76
|
+
lines = stdout.split("\n")
|
|
77
|
+
offset = 0
|
|
78
|
+
for line in lines:
|
|
79
|
+
if line.lstrip().startswith("{"):
|
|
80
|
+
line_starts.append(offset)
|
|
81
|
+
offset += len(line) + 1
|
|
82
|
+
find_idx = stdout.find('{"')
|
|
83
|
+
|
|
84
|
+
# 扫描顺序:行首对象从后往前优先(结果对象在末尾),find('{"') 兜底
|
|
85
|
+
scan_order = list(reversed(line_starts))
|
|
86
|
+
if find_idx >= 0:
|
|
87
|
+
scan_order.append(find_idx)
|
|
88
|
+
|
|
89
|
+
for start in scan_order:
|
|
90
|
+
depth = 0
|
|
91
|
+
in_string = False
|
|
92
|
+
escaped = False
|
|
93
|
+
for i in range(start, len(stdout)):
|
|
94
|
+
ch = stdout[i]
|
|
95
|
+
if in_string:
|
|
96
|
+
if escaped:
|
|
97
|
+
escaped = False
|
|
98
|
+
elif ch == "\\":
|
|
99
|
+
escaped = True
|
|
100
|
+
elif ch == '"':
|
|
101
|
+
in_string = False
|
|
102
|
+
continue
|
|
103
|
+
if ch == '"':
|
|
104
|
+
in_string = True
|
|
105
|
+
elif ch == "{":
|
|
106
|
+
depth += 1
|
|
107
|
+
elif ch == "}":
|
|
108
|
+
depth -= 1
|
|
109
|
+
if depth == 0:
|
|
110
|
+
try:
|
|
111
|
+
return json.loads(stdout[start:i + 1])
|
|
112
|
+
except json.JSONDecodeError:
|
|
113
|
+
break
|
|
114
|
+
return {"result": stdout, "is_error": False, "raw": True}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AgentRunNode(Node):
|
|
118
|
+
"""运行一个 Agent CLI 并返回其文本回复。
|
|
119
|
+
|
|
120
|
+
JSON 字段:
|
|
121
|
+
- ``agent``: agents.json 里的名字(默认 ``glm-52``),支持表达式
|
|
122
|
+
- ``prompt``: prompt 文本,支持 ``{% ... %}`` 模板表达式
|
|
123
|
+
- ``repo``: 工作目录(recursive 的 ``--workspace``;默认进程 cwd)
|
|
124
|
+
- ``timeout_secs``: 超时秒数(默认 1800;flowcast 的 recursive 直路径无超时,
|
|
125
|
+
这里是行为改进)
|
|
126
|
+
- ``dry_run``: 为 true(或流程 globalContext.dry_run=true)时不真正调用,
|
|
127
|
+
返回 fake 文本
|
|
128
|
+
|
|
129
|
+
输出:``{"text", "cli", "session_id", "usage", "dry_run"}``。
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
node_type: ClassVar[str] = "agentrun"
|
|
133
|
+
node_name: ClassVar[str] = "Agent 运行"
|
|
134
|
+
|
|
135
|
+
agent: Optional[Any] = "glm-52"
|
|
136
|
+
prompt: Optional[Any] = None
|
|
137
|
+
repo: Optional[Any] = None
|
|
138
|
+
timeout_secs: int = Field(default=1800)
|
|
139
|
+
dry_run: bool = False
|
|
140
|
+
|
|
141
|
+
def execute(self, execution: Any) -> dict:
|
|
142
|
+
agent_name = str(execution.evaluate(self.agent)) if self.agent is not None else "glm-52"
|
|
143
|
+
prompt = execution.evaluate(self.prompt) if self.prompt is not None else ""
|
|
144
|
+
repo = execution.evaluate(self.repo) if self.repo else None
|
|
145
|
+
dry = self.dry_run or bool(execution.get_global_variable("dry_run", False))
|
|
146
|
+
|
|
147
|
+
if dry:
|
|
148
|
+
preview = prompt[:80] + ("…" if len(prompt) > 80 else "")
|
|
149
|
+
return {"text": f"[dry-run] {agent_name} would run: {preview}",
|
|
150
|
+
"cli": agent_name, "session_id": "", "usage": None, "dry_run": True}
|
|
151
|
+
|
|
152
|
+
profile = resolve_agent(agent_name, repo=repo)
|
|
153
|
+
executor = profile["executor"]
|
|
154
|
+
|
|
155
|
+
from agentproc import EXECUTORS as AP_EXECUTORS
|
|
156
|
+
|
|
157
|
+
# 显式别名映射优先;否则若 agentproc 已内置同名 executor(或测试桩)直接使用
|
|
158
|
+
ap_executor = EXECUTOR_ALIASES.get(executor, executor)
|
|
159
|
+
if ap_executor not in AP_EXECUTORS:
|
|
160
|
+
raise AgentRunError(
|
|
161
|
+
f"执行器 '{executor}' 未接入(agentproc 可用:{sorted(AP_EXECUTORS)});"
|
|
162
|
+
f"如需其他 CLI 请在 agentproc executor 层扩展或加 EXECUTOR_ALIASES 映射"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
from agentproc.runner import RunOptions
|
|
166
|
+
from agentproc.runner import run as agentproc_run
|
|
167
|
+
|
|
168
|
+
extra_env = dict(profile["env"])
|
|
169
|
+
extra_env.setdefault("RECURSIVE_WORKSPACE", repo or ".")
|
|
170
|
+
result = agentproc_run(
|
|
171
|
+
{"executor": ap_executor},
|
|
172
|
+
RunOptions(message=str(prompt), extra_env=extra_env,
|
|
173
|
+
timeout_secs=self.timeout_secs),
|
|
174
|
+
)
|
|
175
|
+
if result.error or result.exit_code != 0:
|
|
176
|
+
raise AgentRunError(result.error or f"{agent_name} 退出码 {result.exit_code}")
|
|
177
|
+
|
|
178
|
+
text = result.reply
|
|
179
|
+
if executor == "recursive":
|
|
180
|
+
parsed = extract_recursive_result(result.reply)
|
|
181
|
+
if parsed.get("is_error"):
|
|
182
|
+
raise AgentRunError(f"recursive is_error: {str(parsed.get('result'))[:300]}")
|
|
183
|
+
text = str(parsed.get("result") or "")
|
|
184
|
+
|
|
185
|
+
return {"text": text, "cli": executor, "session_id": result.session_id,
|
|
186
|
+
"usage": result.usage, "dry_run": False}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def recursive_stream_turn(task: str, *, workspace: str, profile: str = "glm-52",
|
|
190
|
+
model: Optional[str] = None,
|
|
191
|
+
max_steps: Optional[int] = None,
|
|
192
|
+
timeout_secs: int = 1800):
|
|
193
|
+
"""以流式方式跑一轮 recursive Agent(生成器:yield 事件 dict)。
|
|
194
|
+
|
|
195
|
+
事件序列:
|
|
196
|
+
{"type": "line", "text": str} # agent 原始输出行(宿主可实时展示)
|
|
197
|
+
{"type": "done", "ok": bool, "result": str, "error": str}
|
|
198
|
+
供宿主进程(如 plaita-console 的 AI 流程生成)把编码 Agent 作为生成后端复用。
|
|
199
|
+
"""
|
|
200
|
+
import time as _time
|
|
201
|
+
|
|
202
|
+
register_recursive_direct()
|
|
203
|
+
agent = resolve_agent(profile)
|
|
204
|
+
env_extra = dict(agent["env"])
|
|
205
|
+
env_extra.setdefault("RECURSIVE_WORKSPACE", workspace)
|
|
206
|
+
if model:
|
|
207
|
+
env_extra["RECURSIVE_MODEL"] = model
|
|
208
|
+
handlers = _make_recursive_handlers()
|
|
209
|
+
if agent.get("model"):
|
|
210
|
+
env_extra.setdefault("RECURSIVE_MODEL", agent["model"])
|
|
211
|
+
argv = handlers["build_args"](task, "", env_extra)
|
|
212
|
+
# 子进程必须拿到 provider 凭证 env(否则无凭证运行得到空回复)
|
|
213
|
+
proc_env = {**os.environ, **env_extra}
|
|
214
|
+
proc = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
215
|
+
text=True, cwd=workspace, env=proc_env)
|
|
216
|
+
lines: list[str] = []
|
|
217
|
+
deadline = _time.monotonic() + timeout_secs
|
|
218
|
+
for line in proc.stdout: # type: ignore[union-attr]
|
|
219
|
+
yield {"type": "line", "text": line.rstrip("\n")}
|
|
220
|
+
lines.append(line)
|
|
221
|
+
proc.wait()
|
|
222
|
+
timed_out = _time.monotonic() > deadline
|
|
223
|
+
stdout = "".join(lines)
|
|
224
|
+
if timed_out:
|
|
225
|
+
yield {"type": "done", "ok": False, "result": "", "error": f"agent 超时(>{timeout_secs}s)"}
|
|
226
|
+
return
|
|
227
|
+
parsed = extract_recursive_result(stdout)
|
|
228
|
+
if parsed.get("is_error"):
|
|
229
|
+
yield {"type": "done", "ok": False, "result": "",
|
|
230
|
+
"error": f"recursive is_error: {str(parsed.get('result'))[:300]}"}
|
|
231
|
+
return
|
|
232
|
+
yield {"type": "done", "ok": True, "result": parsed.get("result") or "", "error": ""}
|