fr-cli 2.2.5__tar.gz → 2.2.7__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.
- {fr_cli-2.2.5/fr_cli.egg-info → fr_cli-2.2.7}/PKG-INFO +7 -7
- {fr_cli-2.2.5 → fr_cli-2.2.7}/README.md +3 -3
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/README.md +11 -4
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/__init__.py +1 -1
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/executor.py +22 -4
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/generator.py +2 -1
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/manager.py +27 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/workflow.py +59 -49
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/command/executor.py +32 -6
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/core.py +40 -1
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/llm.py +51 -14
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/lang/i18n.py +21 -5
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/main.py +2 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/repl/commands.py +172 -15
- {fr_cli-2.2.5 → fr_cli-2.2.7/fr_cli.egg-info}/PKG-INFO +7 -7
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/SOURCES.txt +2 -13
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/requires.txt +2 -2
- {fr_cli-2.2.5 → fr_cli-2.2.7}/pyproject.toml +4 -4
- fr_cli-2.2.7/tests/test_integration_real.py +745 -0
- fr_cli-2.2.7/tests/test_model_config.py +391 -0
- fr_cli-2.2.5/tests/test_agent_client.py +0 -199
- fr_cli-2.2.5/tests/test_agent_server.py +0 -199
- fr_cli-2.2.5/tests/test_ai_save_file_with_verify.py +0 -228
- fr_cli-2.2.5/tests/test_all.py +0 -917
- fr_cli-2.2.5/tests/test_auto_session.py +0 -116
- fr_cli-2.2.5/tests/test_builtins.py +0 -67
- fr_cli-2.2.5/tests/test_dataframe.py +0 -42
- fr_cli-2.2.5/tests/test_gatekeeper.py +0 -118
- fr_cli-2.2.5/tests/test_integration.py +0 -224
- fr_cli-2.2.5/tests/test_intent_classification.py +0 -268
- fr_cli-2.2.5/tests/test_launcher.py +0 -98
- fr_cli-2.2.5/tests/test_master_agent.py +0 -162
- fr_cli-2.2.5/tests/test_structured_tools.py +0 -245
- {fr_cli-2.2.5 → fr_cli-2.2.7}/LICENSE +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/MANIFEST.in +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/WEAPON.MD +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/addon/plugin.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/_utils.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/db.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/local.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/rag.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/rag_watcher_daemon.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/remote.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/spider.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/client.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/master.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/master_prompt.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/remote.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/server.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/breakthrough/update.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/command/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/command/registry.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/command/security.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/conf/config.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/conf/wizard.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/chat.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/intent.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/recommender.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/stream.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/sysmon.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/thinking.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/gatekeeper/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/gatekeeper/daemon.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/gatekeeper/manager.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/memory/context.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/memory/history.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/memory/session.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/repl/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/security/security.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/ui/ui.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/cron.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/dataframe.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/disk.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/fs.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/launcher.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/loader.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/mail.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/mcp.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/vision.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/web.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/dependency_links.txt +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/entry_points.txt +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/top_level.txt +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.7}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fr-cli
|
|
3
|
-
Version: 2.2.
|
|
4
|
-
Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具
|
|
3
|
+
Version: 2.2.7
|
|
4
|
+
Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark/Doubao/MiMo)的终极全能终端工具
|
|
5
5
|
Author: FANREN CLI Author
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/yourname/fr-cli
|
|
@@ -37,7 +37,7 @@ Requires-Dist: pyodbc>=4.0.0; extra == "db"
|
|
|
37
37
|
Requires-Dist: oracledb>=1.3.0; extra == "db"
|
|
38
38
|
Provides-Extra: rag
|
|
39
39
|
Requires-Dist: chromadb>=0.4.0; extra == "rag"
|
|
40
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
40
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "rag"
|
|
41
41
|
Provides-Extra: remote
|
|
42
42
|
Requires-Dist: paramiko>=3.0.0; extra == "remote"
|
|
43
43
|
Provides-Extra: spider
|
|
@@ -56,7 +56,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
|
56
56
|
Requires-Dist: pyodbc>=4.0.0; extra == "all"
|
|
57
57
|
Requires-Dist: oracledb>=1.3.0; extra == "all"
|
|
58
58
|
Requires-Dist: chromadb>=0.4.0; extra == "all"
|
|
59
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
59
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "all"
|
|
60
60
|
Requires-Dist: paramiko>=3.0.0; extra == "all"
|
|
61
61
|
Requires-Dist: selenium>=4.10.0; extra == "all"
|
|
62
62
|
Requires-Dist: bypy; extra == "all"
|
|
@@ -71,11 +71,11 @@ Dynamic: license-file
|
|
|
71
71
|
|
|
72
72
|
# 凡人打字机 (fr-cli)
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
|
|
75
75
|
|
|
76
76
|
## ✨ 功能特性
|
|
77
77
|
|
|
78
|
-
- 🤖 **AI
|
|
78
|
+
- 🤖 **AI 对话**:支持多模型(智谱 GLM / DeepSeek / Kimi / 通义千问 / StepFun / MiniMax / 讯飞星火)
|
|
79
79
|
- 🧠 **MasterAgent 主控**:自我进化的 ReAct 主控 Agent,自动规划、调用工具、反思进化
|
|
80
80
|
- 🧩 **思维模式**:direct / CoT / ToT / ReAct 四种推理模式切换
|
|
81
81
|
- 📁 **文件沙盒**:安全的虚拟文件系统,支持读写/目录操作
|
|
@@ -105,7 +105,7 @@ pip install fr-cli
|
|
|
105
105
|
fr-cli
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
首次运行会引导输入当前道统的 API Key。
|
|
109
109
|
|
|
110
110
|
## 📝 使用示例
|
|
111
111
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# 凡人打字机 (fr-cli)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
|
|
4
4
|
|
|
5
5
|
## ✨ 功能特性
|
|
6
6
|
|
|
7
|
-
- 🤖 **AI
|
|
7
|
+
- 🤖 **AI 对话**:支持多模型(智谱 GLM / DeepSeek / Kimi / 通义千问 / StepFun / MiniMax / 讯飞星火)
|
|
8
8
|
- 🧠 **MasterAgent 主控**:自我进化的 ReAct 主控 Agent,自动规划、调用工具、反思进化
|
|
9
9
|
- 🧩 **思维模式**:direct / CoT / ToT / ReAct 四种推理模式切换
|
|
10
10
|
- 📁 **文件沙盒**:安全的虚拟文件系统,支持读写/目录操作
|
|
@@ -34,7 +34,7 @@ pip install fr-cli
|
|
|
34
34
|
fr-cli
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
首次运行会引导输入当前道统的 API Key。
|
|
38
38
|
|
|
39
39
|
## 📝 使用示例
|
|
40
40
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# 凡人打字机 (fr-cli)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
|
|
4
4
|
|
|
5
5
|
**🇨🇳 中文简介**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
支持:多模型 AI 对话(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)、MasterAgent 自我进化主控、思维模式切换(direct/CoT/ToT/ReAct)、文件沙盒操作、联网搜索(SSRF 防护)、图片生成与识别、邮件收发、定时任务(shlex 安全解析)、云盘集成、会话记忆、按日期自动存档、插件进化(子进程隔离)、四阶安全拦截、Shell 管道直通 AI。
|
|
8
8
|
|
|
9
9
|
**🇺🇸 English Intro**
|
|
10
10
|
|
|
11
|
-
The ultimate all-knowing terminal tool
|
|
11
|
+
The ultimate all-knowing terminal tool supporting multiple LLM providers (Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark). Supports AI chat, MasterAgent self-evolving controller, thinking modes (direct/CoT/ToT/ReAct), virtual filesystem, web search (SSRF-protected), image generation & vision, email, cron jobs (shlex-safe), cloud drive, session memory, auto date-based archiving, self-evolving plugins (subprocess-isolated), and powerful Shell piping.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -19,7 +19,7 @@ pip install fr-cli
|
|
|
19
19
|
fr-cli
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
首次运行会引导输入当前道统的 API Key。
|
|
23
23
|
|
|
24
24
|
## 🎮 使用方式
|
|
25
25
|
|
|
@@ -41,6 +41,13 @@ fr-cli
|
|
|
41
41
|
/session_load <idx> 加载存档会话
|
|
42
42
|
/mode direct|cot|tot|react 切换思维模式
|
|
43
43
|
/master on|off|status MasterAgent 主控
|
|
44
|
+
/model <模型名> 切换当前道统模型
|
|
45
|
+
/model <道统>:<模型名> 同时切换道统和模型
|
|
46
|
+
/key <key> 修改当前道统 API Key
|
|
47
|
+
/key <道统> <key> 为指定道统设置 Key
|
|
48
|
+
/providers 查看所有道统配置
|
|
49
|
+
/providers add <p> <k> [m] 添加/更新道统配置
|
|
50
|
+
/providers use <p> 切换到指定道统
|
|
44
51
|
/mcp_list 列出 MCP 服务器及工具
|
|
45
52
|
/mcp_add <名> <命令> [参数] 添加 MCP 服务器
|
|
46
53
|
/mcp_del <名> 删除 MCP 服务器
|
|
@@ -19,12 +19,17 @@ def run_agent(name, state, **kwargs):
|
|
|
19
19
|
return None, "agent.py missing run(context, **kwargs)"
|
|
20
20
|
progress = load_progress(name)
|
|
21
21
|
latest = progress.get("latest", {})
|
|
22
|
+
|
|
23
|
+
# 解析 Agent 专属 LLM 配置
|
|
24
|
+
client, provider, model = state.resolve_agent_llm(name)
|
|
25
|
+
|
|
22
26
|
context = {
|
|
23
27
|
"persona": persona,
|
|
24
28
|
"memory": memory,
|
|
25
29
|
"skills": skills,
|
|
26
|
-
"client":
|
|
27
|
-
"
|
|
30
|
+
"client": client,
|
|
31
|
+
"provider": provider,
|
|
32
|
+
"model": model,
|
|
28
33
|
"lang": state.lang,
|
|
29
34
|
"executor": state.executor,
|
|
30
35
|
"state": state,
|
|
@@ -34,11 +39,15 @@ def run_agent(name, state, **kwargs):
|
|
|
34
39
|
"latest_status": latest.get("status", ""),
|
|
35
40
|
"execution_count": progress.get("counter", 0),
|
|
36
41
|
}
|
|
42
|
+
# 将工具调用的 LLM 上下文切换为 Agent 专属配置
|
|
43
|
+
state.executor.push_agent_context(client, model)
|
|
37
44
|
try:
|
|
38
45
|
result = mod.run(context, **kwargs)
|
|
39
46
|
return result, None
|
|
40
47
|
except Exception as e:
|
|
41
48
|
return None, str(e)
|
|
49
|
+
finally:
|
|
50
|
+
state.executor.pop_agent_context()
|
|
42
51
|
|
|
43
52
|
|
|
44
53
|
def delegate_to_agent(name, state, pipeline_input=None, **kwargs):
|
|
@@ -53,23 +62,32 @@ def delegate_to_agent(name, state, pipeline_input=None, **kwargs):
|
|
|
53
62
|
return None, "agent.py not found or load failed"
|
|
54
63
|
if not hasattr(mod, "run"):
|
|
55
64
|
return None, "agent.py missing run(context, **kwargs)"
|
|
65
|
+
|
|
66
|
+
# 解析 Agent 专属 LLM 配置
|
|
67
|
+
client, provider, model = state.resolve_agent_llm(name)
|
|
68
|
+
|
|
56
69
|
context = {
|
|
57
70
|
"persona": persona,
|
|
58
71
|
"memory": memory,
|
|
59
72
|
"skills": skills,
|
|
60
|
-
"client":
|
|
61
|
-
"
|
|
73
|
+
"client": client,
|
|
74
|
+
"provider": provider,
|
|
75
|
+
"model": model,
|
|
62
76
|
"lang": state.lang,
|
|
63
77
|
"executor": state.executor,
|
|
64
78
|
"state": state,
|
|
65
79
|
"agent_name": name,
|
|
66
80
|
"pipeline_input": pipeline_input,
|
|
67
81
|
}
|
|
82
|
+
# 将工具调用的 LLM 上下文切换为 Agent 专属配置
|
|
83
|
+
state.executor.push_agent_context(client, model)
|
|
68
84
|
try:
|
|
69
85
|
result = mod.run(context, **kwargs)
|
|
70
86
|
return result, None
|
|
71
87
|
except Exception as e:
|
|
72
88
|
return None, str(e)
|
|
89
|
+
finally:
|
|
90
|
+
state.executor.pop_agent_context()
|
|
73
91
|
|
|
74
92
|
|
|
75
93
|
def run_multi_agent(names, state, initial_input=None, **kwargs):
|
|
@@ -33,7 +33,8 @@ Agent 名称: {name}
|
|
|
33
33
|
- 'persona': str — 人设文本
|
|
34
34
|
- 'memory': str — 记忆文本
|
|
35
35
|
- 'skills': str — 技能文本
|
|
36
|
-
- 'client':
|
|
36
|
+
- 'client': LLM 客户端实例(已根据 Agent 专属配置或全局默认初始化)
|
|
37
|
+
- 'provider': str — 当前使用的道统/提供商 ID(如 'zhipu', 'deepseek')
|
|
37
38
|
- 'model': str — 模型名称
|
|
38
39
|
- 'lang': str — 语言代码('zh' 或 'en')
|
|
39
40
|
- 'executor': CommandExecutor 实例(可使用 invoke_tool/execute 调用工具)
|
|
@@ -12,6 +12,7 @@ MEMORY_FILE = "memory.md"
|
|
|
12
12
|
SKILLS_FILE = "skills.md"
|
|
13
13
|
AGENT_CODE_FILE = "agent.py"
|
|
14
14
|
PROGRESS_FILE = "progress.json"
|
|
15
|
+
AGENT_CONFIG_FILE = "config.json"
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def _agent_dir(name: str) -> Path:
|
|
@@ -53,6 +54,7 @@ def list_agents() -> list:
|
|
|
53
54
|
"has_persona": (d / PERSONA_FILE).exists(),
|
|
54
55
|
"has_memory": (d / MEMORY_FILE).exists(),
|
|
55
56
|
"has_skills": (d / SKILLS_FILE).exists(),
|
|
57
|
+
"has_config": (d / AGENT_CONFIG_FILE).exists(),
|
|
56
58
|
})
|
|
57
59
|
return agents
|
|
58
60
|
|
|
@@ -191,3 +193,28 @@ def get_progress_history(name: str, limit: int = 10) -> list:
|
|
|
191
193
|
progress = load_progress(name)
|
|
192
194
|
history = progress.get("history", [])
|
|
193
195
|
return history[-limit:] if history else []
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------- Agent 专属配置读写 ----------
|
|
199
|
+
|
|
200
|
+
def load_agent_config(name: str) -> dict:
|
|
201
|
+
"""读取 Agent 的专属模型配置(config.json)"""
|
|
202
|
+
f = _agent_dir(name) / AGENT_CONFIG_FILE
|
|
203
|
+
if not f.exists():
|
|
204
|
+
return {}
|
|
205
|
+
try:
|
|
206
|
+
return json.loads(f.read_text(encoding="utf-8"))
|
|
207
|
+
except Exception:
|
|
208
|
+
return {}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def save_agent_config(name: str, data: dict):
|
|
212
|
+
"""保存 Agent 的专属模型配置到 config.json"""
|
|
213
|
+
d = _agent_dir(name)
|
|
214
|
+
# 确保 Agent 洞府存在,避免在无效目录创建孤立文件
|
|
215
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
216
|
+
f = d / AGENT_CONFIG_FILE
|
|
217
|
+
try:
|
|
218
|
+
f.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
@@ -4,6 +4,7 @@ Agent workflow engine
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
|
+
from fr_cli.agent.manager import load_persona, load_memory, load_skills, save_memory
|
|
7
8
|
|
|
8
9
|
WORKFLOW_FILE = "workflow.md"
|
|
9
10
|
|
|
@@ -101,64 +102,73 @@ def run_workflow(name, state, user_input=None, **kwargs):
|
|
|
101
102
|
memory = load_memory(name)
|
|
102
103
|
skills = load_skills(name)
|
|
103
104
|
|
|
105
|
+
# 解析 Agent 专属 LLM 配置
|
|
106
|
+
client, provider, model = state.resolve_agent_llm(name)
|
|
107
|
+
|
|
104
108
|
context = {
|
|
105
109
|
"persona": persona,
|
|
106
110
|
"memory": memory,
|
|
107
111
|
"skills": skills,
|
|
108
|
-
"client":
|
|
109
|
-
"
|
|
112
|
+
"client": client,
|
|
113
|
+
"provider": provider,
|
|
114
|
+
"model": model,
|
|
110
115
|
"lang": state.lang,
|
|
111
116
|
"executor": state.executor,
|
|
112
117
|
"state": state,
|
|
113
118
|
"agent_name": name,
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
# 将工具调用的 LLM 上下文切换为 Agent 专属配置
|
|
122
|
+
state.executor.push_agent_context(client, model)
|
|
116
123
|
step_results = []
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
124
|
+
try:
|
|
125
|
+
for step in steps:
|
|
126
|
+
action = step["action"]
|
|
127
|
+
params = {k: _substitute_vars(v, context, step_results, user_input) for k, v in step["params"].items()}
|
|
128
|
+
result = None
|
|
129
|
+
error = None
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
if action in ("invoke_tool", "tool"):
|
|
133
|
+
tool_name = params.pop("tool", list(params.keys())[0] if params else "")
|
|
134
|
+
tool_params = params
|
|
135
|
+
result, error = state.executor.invoke_tool(tool_name, tool_params)
|
|
136
|
+
elif action in ("execute_cmd", "cmd", "command"):
|
|
137
|
+
cmd_str = params.get("cmd", "")
|
|
138
|
+
result, error = state.executor.execute(cmd_str)
|
|
139
|
+
elif action in ("agent_call", "agent", "call_agent"):
|
|
140
|
+
target = params.get("target") or params.get("agent") or params.get("to")
|
|
141
|
+
message = params.get("message", "")
|
|
142
|
+
result, error = run_agent(target, state, pipeline_input=message, **kwargs)
|
|
143
|
+
elif action in ("ai_generate", "ai", "generate", "ask"):
|
|
144
|
+
prompt = params.get("prompt", "")
|
|
145
|
+
from fr_cli.core.stream import stream_cnt
|
|
146
|
+
msgs = [{"role": "user", "content": prompt}]
|
|
147
|
+
# 使用 context 中的 client/model,已根据 Agent 专属配置解析
|
|
148
|
+
result, _, _ = stream_cnt(context["client"], context["model"], msgs, state.lang)
|
|
149
|
+
elif action in ("save_memory", "memory_append"):
|
|
150
|
+
mem = params.get("content", "")
|
|
151
|
+
old = load_memory(name)
|
|
152
|
+
save_memory(name, old + "\n" + mem if old else mem)
|
|
153
|
+
result = "记忆已更新"
|
|
154
|
+
else:
|
|
155
|
+
error = f"未知动作: {action}"
|
|
156
|
+
except Exception as e:
|
|
157
|
+
error = str(e)
|
|
158
|
+
|
|
159
|
+
step_results.append({
|
|
160
|
+
"step": step["num"],
|
|
161
|
+
"title": step["title"],
|
|
162
|
+
"action": action,
|
|
163
|
+
"result": result,
|
|
164
|
+
"error": error,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
if error:
|
|
168
|
+
return None, f"步骤 {step['num']} ({step['title']}) 失败: {error}", step_results
|
|
169
|
+
|
|
170
|
+
final_result = step_results[-1]["result"] if step_results else None
|
|
171
|
+
return final_result, None, step_results
|
|
172
|
+
finally:
|
|
173
|
+
state.executor.pop_agent_context()
|
|
164
174
|
|
|
@@ -10,8 +10,13 @@ from fr_cli.command.registry import get_registry
|
|
|
10
10
|
from fr_cli.addon.plugin import exec_plugin
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def _build_deps(state):
|
|
14
|
-
"""根据 AppState 动态构建依赖命名空间(每次调用实时反射,避免快照过时)
|
|
13
|
+
def _build_deps(state, client=None, model_name=None):
|
|
14
|
+
"""根据 AppState 动态构建依赖命名空间(每次调用实时反射,避免快照过时)
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
client: 可选的覆盖 client(如 Agent 专属模型)
|
|
18
|
+
model_name: 可选的覆盖模型名
|
|
19
|
+
"""
|
|
15
20
|
return SimpleNamespace(
|
|
16
21
|
vfs=state.vfs,
|
|
17
22
|
mail_c=state.mail_c,
|
|
@@ -21,8 +26,8 @@ def _build_deps(state):
|
|
|
21
26
|
lang=state.lang,
|
|
22
27
|
security=state.security,
|
|
23
28
|
cfg=state.cfg,
|
|
24
|
-
client=state.client,
|
|
25
|
-
model_name=state.model_name,
|
|
29
|
+
client=client or state.client,
|
|
30
|
+
model_name=model_name or state.model_name,
|
|
26
31
|
mcp=getattr(state, "mcp", None),
|
|
27
32
|
)
|
|
28
33
|
|
|
@@ -41,13 +46,34 @@ class CommandExecutor:
|
|
|
41
46
|
def __init__(self, state):
|
|
42
47
|
self.state = state
|
|
43
48
|
self._reg = get_registry()
|
|
49
|
+
# Agent 专属模型上下文覆盖(栈结构,支持嵌套 Agent 调用)
|
|
50
|
+
self._agent_ctx_stack = []
|
|
51
|
+
|
|
52
|
+
# ------------------------------------------------------------------
|
|
53
|
+
# Agent 上下文覆盖管理
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
def push_agent_context(self, client, model_name):
|
|
56
|
+
"""临时将工具调用的 LLM 上下文切换为 Agent 专属配置"""
|
|
57
|
+
self._agent_ctx_stack.append((client, model_name))
|
|
58
|
+
|
|
59
|
+
def pop_agent_context(self):
|
|
60
|
+
"""恢复工具调用的 LLM 上下文为全局默认"""
|
|
61
|
+
if self._agent_ctx_stack:
|
|
62
|
+
self._agent_ctx_stack.pop()
|
|
63
|
+
|
|
64
|
+
def _get_deps(self):
|
|
65
|
+
"""构建依赖命名空间,优先使用 Agent 专属覆盖"""
|
|
66
|
+
if self._agent_ctx_stack:
|
|
67
|
+
client, model_name = self._agent_ctx_stack[-1]
|
|
68
|
+
return _build_deps(self.state, client, model_name)
|
|
69
|
+
return _build_deps(self.state)
|
|
44
70
|
|
|
45
71
|
# ------------------------------------------------------------------
|
|
46
72
|
# 第一层:结构化工具调用
|
|
47
73
|
# ------------------------------------------------------------------
|
|
48
74
|
def invoke_tool(self, tool_name, kwargs, msgs=None):
|
|
49
75
|
"""根据工具名和结构化参数,通过注册表调度执行。返回 (result, error)"""
|
|
50
|
-
return self._reg.dispatch(
|
|
76
|
+
return self._reg.dispatch(self._get_deps(), tool_name, msgs=msgs, **kwargs)
|
|
51
77
|
|
|
52
78
|
# ------------------------------------------------------------------
|
|
53
79
|
# 第二层:传统命令解析(用户输入 / 插件调用)
|
|
@@ -65,7 +91,7 @@ class CommandExecutor:
|
|
|
65
91
|
exec_plugin(cmd, self.state.plugins[cmd], p_args, self.state.lang)
|
|
66
92
|
return f"Plugin {cmd} executed", None
|
|
67
93
|
# 其余命令通过注册表内部接口直接调度,避免 dispatch_cmd 再次 split
|
|
68
|
-
return self._reg._dispatch_cmd_parts(
|
|
94
|
+
return self._reg._dispatch_cmd_parts(self._get_deps(), parts, msgs=msgs)
|
|
69
95
|
|
|
70
96
|
# ------------------------------------------------------------------
|
|
71
97
|
# 第三层:AI 回复解析
|
|
@@ -11,7 +11,7 @@ from fr_cli.command.security import SecurityManager
|
|
|
11
11
|
from fr_cli.command.executor import CommandExecutor
|
|
12
12
|
from fr_cli.weapon.loader import load_weapon_md
|
|
13
13
|
from fr_cli.weapon.mcp import MCPManager
|
|
14
|
-
from fr_cli.core.llm import create_llm_client, list_providers, get_provider_info, resolve_provider_model
|
|
14
|
+
from fr_cli.core.llm import create_llm_client, create_llm_client_for, list_providers, get_provider_info, resolve_provider_model
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class AppState:
|
|
@@ -49,6 +49,9 @@ class AppState:
|
|
|
49
49
|
# 自动会话存档路径(按日期编号)
|
|
50
50
|
self.auto_session_path = None
|
|
51
51
|
|
|
52
|
+
# LLM 客户端缓存(供 Agent 专属模型复用)
|
|
53
|
+
self._client_cache = {}
|
|
54
|
+
|
|
52
55
|
# 命令执行引擎
|
|
53
56
|
self.executor = CommandExecutor(self)
|
|
54
57
|
|
|
@@ -146,3 +149,39 @@ class AppState:
|
|
|
146
149
|
self.cfg["thinking_mode"] = mode
|
|
147
150
|
self.thinking_mode = mode
|
|
148
151
|
self.save_cfg()
|
|
152
|
+
|
|
153
|
+
def get_client_for(self, provider: str, model: str, override_key: str = None):
|
|
154
|
+
"""
|
|
155
|
+
获取指定 provider + model 的 LLM 客户端,带缓存避免重复初始化
|
|
156
|
+
若提供了 override_key,则优先使用(如 Agent 专属 key)
|
|
157
|
+
"""
|
|
158
|
+
cache_key = (provider, model, override_key)
|
|
159
|
+
cached = self._client_cache.get(cache_key)
|
|
160
|
+
if cached is not None:
|
|
161
|
+
return cached
|
|
162
|
+
|
|
163
|
+
client, _, _ = create_llm_client_for(provider, model, self.cfg, override_key)
|
|
164
|
+
self._client_cache[cache_key] = client
|
|
165
|
+
return client
|
|
166
|
+
|
|
167
|
+
def resolve_agent_llm(self, agent_name: str):
|
|
168
|
+
"""
|
|
169
|
+
解析 Agent 的 LLM 配置:优先读取 Agent 的 config.json,
|
|
170
|
+
若无专属配置则回退到全局默认。
|
|
171
|
+
|
|
172
|
+
返回: (client, provider, model)
|
|
173
|
+
"""
|
|
174
|
+
from fr_cli.agent.manager import load_agent_config
|
|
175
|
+
agent_cfg = load_agent_config(agent_name)
|
|
176
|
+
|
|
177
|
+
provider = agent_cfg.get("provider")
|
|
178
|
+
model = agent_cfg.get("model")
|
|
179
|
+
override_key = agent_cfg.get("key") or None
|
|
180
|
+
|
|
181
|
+
# 防御性校验:provider 和 model 必须均为非空字符串才生效
|
|
182
|
+
if provider and model and isinstance(provider, str) and isinstance(model, str):
|
|
183
|
+
client = self.get_client_for(provider, model, override_key)
|
|
184
|
+
return client, provider, model
|
|
185
|
+
|
|
186
|
+
# 回退到全局默认
|
|
187
|
+
return self.client, self.provider, self.model_name
|
|
@@ -118,9 +118,43 @@ _PROVIDERS: Dict[str, Dict[str, Any]] = {
|
|
|
118
118
|
"client_class": OpenAICompatibleClient,
|
|
119
119
|
"base_url": "https://spark-api-open.xf-yun.com/v1",
|
|
120
120
|
},
|
|
121
|
+
"doubao": {
|
|
122
|
+
"name": "豆包 (Doubao)",
|
|
123
|
+
"default_model": "doubao-1-5-pro-32k-250115",
|
|
124
|
+
"client_class": OpenAICompatibleClient,
|
|
125
|
+
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
|
|
126
|
+
},
|
|
127
|
+
"mimo": {
|
|
128
|
+
"name": "小米 MiMo",
|
|
129
|
+
"default_model": "mimo-v2-flash",
|
|
130
|
+
"client_class": OpenAICompatibleClient,
|
|
131
|
+
"base_url": "https://api.xiaomimimo.com/v1",
|
|
132
|
+
},
|
|
121
133
|
}
|
|
122
134
|
|
|
123
135
|
|
|
136
|
+
def _resolve_llm_kwargs(provider: str, cfg: dict, override_key: str = None):
|
|
137
|
+
"""
|
|
138
|
+
根据配置解析创建 LLM 客户端所需的参数。
|
|
139
|
+
返回: (client_class, kwargs_dict)
|
|
140
|
+
"""
|
|
141
|
+
providers_cfg = cfg.get("providers", {})
|
|
142
|
+
pcfg = providers_cfg.get(provider, {})
|
|
143
|
+
|
|
144
|
+
# 解析 key:override_key > provider 专属 > 顶层 key(zhipu 向后兼容)
|
|
145
|
+
api_key = override_key or pcfg.get("key") or cfg.get("key", "")
|
|
146
|
+
|
|
147
|
+
info = _PROVIDERS.get(provider, _PROVIDERS["zhipu"])
|
|
148
|
+
client_class = info["client_class"]
|
|
149
|
+
base_url = pcfg.get("base_url") or info.get("base_url")
|
|
150
|
+
|
|
151
|
+
kwargs = {"api_key": api_key}
|
|
152
|
+
if base_url:
|
|
153
|
+
kwargs["base_url"] = base_url
|
|
154
|
+
|
|
155
|
+
return client_class, kwargs
|
|
156
|
+
|
|
157
|
+
|
|
124
158
|
def create_llm_client(cfg: dict):
|
|
125
159
|
"""
|
|
126
160
|
根据配置创建对应的 LLM 客户端
|
|
@@ -133,25 +167,12 @@ def create_llm_client(cfg: dict):
|
|
|
133
167
|
"""
|
|
134
168
|
provider = cfg.get("provider", "zhipu")
|
|
135
169
|
providers_cfg = cfg.get("providers", {})
|
|
136
|
-
|
|
137
|
-
# 获取当前提供商配置
|
|
138
170
|
pcfg = providers_cfg.get(provider, {})
|
|
139
171
|
|
|
140
|
-
# 向后兼容:如果 providers 中没有当前 provider,从顶层读取 key/model
|
|
141
|
-
# 使用 'or' 确保空字符串也能正确回退到顶层 key
|
|
142
|
-
api_key = pcfg.get("key") or cfg.get("key", "")
|
|
143
172
|
default_model = _PROVIDERS.get(provider, _PROVIDERS["zhipu"])["default_model"]
|
|
144
173
|
model = pcfg.get("model") or cfg.get("model", default_model)
|
|
145
174
|
|
|
146
|
-
|
|
147
|
-
client_class = info["client_class"]
|
|
148
|
-
# 优先使用用户自定义的 base_url,其次使用内置默认
|
|
149
|
-
base_url = pcfg.get("base_url") or info.get("base_url")
|
|
150
|
-
|
|
151
|
-
kwargs = {"api_key": api_key}
|
|
152
|
-
if base_url:
|
|
153
|
-
kwargs["base_url"] = base_url
|
|
154
|
-
|
|
175
|
+
client_class, kwargs = _resolve_llm_kwargs(provider, cfg)
|
|
155
176
|
return client_class(**kwargs), provider, model
|
|
156
177
|
|
|
157
178
|
|
|
@@ -168,6 +189,22 @@ def get_provider_info(provider_id: str):
|
|
|
168
189
|
return _PROVIDERS.get(provider_id)
|
|
169
190
|
|
|
170
191
|
|
|
192
|
+
def create_llm_client_for(provider: str, model: str, cfg: dict, override_key: str = None):
|
|
193
|
+
"""
|
|
194
|
+
根据全局配置创建指定 provider + model 的 LLM 客户端
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
provider: 提供商 ID
|
|
198
|
+
model: 模型名称
|
|
199
|
+
cfg: 全局配置字典
|
|
200
|
+
override_key: 可选的覆盖 key(如 Agent 专属 key)
|
|
201
|
+
|
|
202
|
+
返回: (client_instance, provider_id, model_name)
|
|
203
|
+
"""
|
|
204
|
+
client_class, kwargs = _resolve_llm_kwargs(provider, cfg, override_key)
|
|
205
|
+
return client_class(**kwargs), provider, model
|
|
206
|
+
|
|
207
|
+
|
|
171
208
|
def resolve_provider_model(arg: str) -> tuple:
|
|
172
209
|
"""
|
|
173
210
|
解析用户输入的模型参数
|