llm-toolforge 0.3.0__py3-none-any.whl
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.
- llm_toolforge/__init__.py +47 -0
- llm_toolforge/cli.py +403 -0
- llm_toolforge/config.py +88 -0
- llm_toolforge/core/__init__.py +11 -0
- llm_toolforge/core/ai_client.py +670 -0
- llm_toolforge/core/base.py +120 -0
- llm_toolforge/core/load_monitor.py +269 -0
- llm_toolforge/core/log_store.py +1125 -0
- llm_toolforge/core/oss_client.py +146 -0
- llm_toolforge/core/redis_client.py +220 -0
- llm_toolforge/discovery.py +94 -0
- llm_toolforge/fonts/NotoSansSC-Regular.ttf +0 -0
- llm_toolforge/server/__init__.py +1 -0
- llm_toolforge/server/api.py +653 -0
- llm_toolforge/server/mcp_server.py +223 -0
- llm_toolforge/server/runner.py +107 -0
- llm_toolforge/static/favicon.ico +0 -0
- llm_toolforge/static/index.html +3116 -0
- llm_toolforge-0.3.0.dist-info/METADATA +195 -0
- llm_toolforge-0.3.0.dist-info/RECORD +24 -0
- llm_toolforge-0.3.0.dist-info/WHEEL +5 -0
- llm_toolforge-0.3.0.dist-info/entry_points.txt +2 -0
- llm_toolforge-0.3.0.dist-info/licenses/LICENSE +21 -0
- llm_toolforge-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
llm-toolforge
|
|
3
|
+
|
|
4
|
+
工具开发只需从这里导入:
|
|
5
|
+
|
|
6
|
+
from llm_toolforge import (
|
|
7
|
+
Plugin, InputField, BaseGenerator,
|
|
8
|
+
GenerateResult, GenerateProgress, ResultStatus,
|
|
9
|
+
LLMServiceError, UploadError,
|
|
10
|
+
AIClient, OSSClient,
|
|
11
|
+
load_tool_config,
|
|
12
|
+
)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .core.base import (
|
|
16
|
+
BaseGenerator,
|
|
17
|
+
GenerateProgress,
|
|
18
|
+
GenerateResult,
|
|
19
|
+
GeneratorError,
|
|
20
|
+
InputField,
|
|
21
|
+
LLMServiceError,
|
|
22
|
+
Plugin,
|
|
23
|
+
ResultStatus,
|
|
24
|
+
UploadError,
|
|
25
|
+
)
|
|
26
|
+
from .core.ai_client import AIClient
|
|
27
|
+
from .core.oss_client import OSSClient
|
|
28
|
+
from .config import load_tool_config, load_config, find_config
|
|
29
|
+
from .discovery import discover_plugins
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Plugin",
|
|
33
|
+
"BaseGenerator",
|
|
34
|
+
"InputField",
|
|
35
|
+
"GenerateResult",
|
|
36
|
+
"GenerateProgress",
|
|
37
|
+
"ResultStatus",
|
|
38
|
+
"GeneratorError",
|
|
39
|
+
"LLMServiceError",
|
|
40
|
+
"UploadError",
|
|
41
|
+
"AIClient",
|
|
42
|
+
"OSSClient",
|
|
43
|
+
"load_tool_config",
|
|
44
|
+
"load_config",
|
|
45
|
+
"find_config",
|
|
46
|
+
"discover_plugins",
|
|
47
|
+
]
|
llm_toolforge/cli.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
toolforge CLI 入口
|
|
3
|
+
|
|
4
|
+
命令:
|
|
5
|
+
toolforge run [--config FILE] [--api-only | --mcp-only]
|
|
6
|
+
toolforge list [--config FILE]
|
|
7
|
+
toolforge new project <dest>
|
|
8
|
+
toolforge new tool <name>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ── 模板内容 ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
_CONFIG_TEMPLATE = """\
|
|
19
|
+
# ============================================================
|
|
20
|
+
# llm-toolforge 配置
|
|
21
|
+
# ============================================================
|
|
22
|
+
|
|
23
|
+
# 扫描哪些目录下的工具子包(相对于本文件所在目录)
|
|
24
|
+
plugins_dirs: [tools]
|
|
25
|
+
|
|
26
|
+
# ── 服务标识 ──────────────────────────────────────────────────
|
|
27
|
+
# name 用于日志/心跳/统计的 service_name 隔离,多部署时必须唯一
|
|
28
|
+
service:
|
|
29
|
+
name: "my-toolset"
|
|
30
|
+
version: "v1.0.0"
|
|
31
|
+
|
|
32
|
+
# ── HTTP / MCP 服务 ───────────────────────────────────────────
|
|
33
|
+
server:
|
|
34
|
+
host: "0.0.0.0"
|
|
35
|
+
port: 12345
|
|
36
|
+
mcp_port: 12346
|
|
37
|
+
|
|
38
|
+
# Web 测试台(访问 http://host:port/)
|
|
39
|
+
enable_web_ui: true
|
|
40
|
+
|
|
41
|
+
# API 鉴权:enable_auth=true 时所有接口需要 Authorization 头
|
|
42
|
+
enable_auth: false
|
|
43
|
+
authorization: "Bearer change-me"
|
|
44
|
+
|
|
45
|
+
# Worker 数量:auto = min(cpu_count * worker_multiplier, max_api_workers)
|
|
46
|
+
api_workers: auto
|
|
47
|
+
mcp_workers: 1
|
|
48
|
+
# max_api_workers: 8 # auto 模式的上限(默认 8)
|
|
49
|
+
# max_mcp_workers: 4 # MCP auto 模式的上限(默认 4)
|
|
50
|
+
# worker_multiplier: 1 # auto 模式的 CPU 倍数(默认 1)
|
|
51
|
+
|
|
52
|
+
# 单请求并发限制(0 = 不限制)
|
|
53
|
+
max_concurrency: 4
|
|
54
|
+
# overload_timeout_ms: 1000 # 等待并发许可的超时,超时返回 503(默认 1000ms)
|
|
55
|
+
|
|
56
|
+
# 单次请求超时(秒,0 = 不限制)
|
|
57
|
+
request_timeout_s: 180
|
|
58
|
+
|
|
59
|
+
# 文件上传大小限制(MB,0 = 不限制)
|
|
60
|
+
max_upload_mb: 50
|
|
61
|
+
|
|
62
|
+
# 多 Pod 部署时手动指定实例 ID(不填则自动取 POD_NAME / HOSTNAME / gethostname)
|
|
63
|
+
# instance_id: "pod-0"
|
|
64
|
+
|
|
65
|
+
# 跨 service 全局视图的管理员 Token(不填则禁用 scope=global 接口)
|
|
66
|
+
# global_admin_token: "your-admin-token"
|
|
67
|
+
|
|
68
|
+
# ── 调用日志 ──────────────────────────────────────────────────
|
|
69
|
+
logging:
|
|
70
|
+
# storage: mysql | sqlite | disabled
|
|
71
|
+
storage: sqlite
|
|
72
|
+
sqlite_path: logs.db
|
|
73
|
+
|
|
74
|
+
# 日志保留天数(0 = 永久保留)
|
|
75
|
+
retention_days: 90
|
|
76
|
+
|
|
77
|
+
# 心跳配置(用于 /load 接口的 worker 存活检测,依赖 Redis)
|
|
78
|
+
heartbeat_interval_s: 5 # 写心跳的间隔(秒)
|
|
79
|
+
heartbeat_ttl_s: 15 # 心跳在 Redis 中的过期时间(秒)
|
|
80
|
+
|
|
81
|
+
# 日志批量写入配置
|
|
82
|
+
# batch_size: 100 # 攒够多少条触发一次批量写(默认 100)
|
|
83
|
+
# batch_flush_ms: 1000 # 最长等待多少毫秒强制 flush(默认 1000)
|
|
84
|
+
|
|
85
|
+
# 单条日志 output 字段的最大字节数(0 = 不截断)
|
|
86
|
+
# max_output_size: 0
|
|
87
|
+
|
|
88
|
+
# ── MySQL 日志后端(logging.storage: mysql 时填写)──────────────
|
|
89
|
+
# mysql:
|
|
90
|
+
# host: "127.0.0.1"
|
|
91
|
+
# port: 3306
|
|
92
|
+
# username: "root"
|
|
93
|
+
# password: "your-password"
|
|
94
|
+
# database: "toolforge"
|
|
95
|
+
# pool_max: 10 # 连接池最大连接数(默认 10)
|
|
96
|
+
# events_table: "tool_call_events" # 日志表名(默认 tool_call_events)
|
|
97
|
+
|
|
98
|
+
# ── Redis(可选,用于心跳/实时流/统计缓存)──────────────────────────
|
|
99
|
+
# 不配置时对应功能自动降级:/load 心跳检测、/logs/stream 实时流、统计缓存不可用,
|
|
100
|
+
# 其余功能(工具调用、日志落库)不受影响。
|
|
101
|
+
# redis_config:
|
|
102
|
+
# host: "127.0.0.1"
|
|
103
|
+
# port: 6379
|
|
104
|
+
# password: ""
|
|
105
|
+
# database: 0
|
|
106
|
+
# key_prefix: "toolforge" # Redis key 前缀,多部署共用同一 Redis 时用于隔离
|
|
107
|
+
|
|
108
|
+
# ── 大模型渠道(按需填写,工具内通过 provider 名称引用)────────
|
|
109
|
+
# 支持 alibaba / doubao / openai / gemini,按需取消注释并填入密钥
|
|
110
|
+
ai_providers:
|
|
111
|
+
openai:
|
|
112
|
+
key: "your-api-key"
|
|
113
|
+
base_url: "https://api.openai.com/v1"
|
|
114
|
+
timeout_s: 120
|
|
115
|
+
# alibaba:
|
|
116
|
+
# key: "your-dashscope-api-key"
|
|
117
|
+
# base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
118
|
+
# timeout_s: 120
|
|
119
|
+
# doubao:
|
|
120
|
+
# key: "your-ark-api-key"
|
|
121
|
+
# base_url: "https://ark.cn-beijing.volces.com/api/v3"
|
|
122
|
+
# timeout_s: 120
|
|
123
|
+
# gemini:
|
|
124
|
+
# key: "your-gemini-api-key"
|
|
125
|
+
# base_url: "https://generativelanguage.googleapis.com"
|
|
126
|
+
# timeout_s: 120
|
|
127
|
+
|
|
128
|
+
# ── 对象存储(文件上传 / 产物托管,按需启用)────────────────────
|
|
129
|
+
# storage:
|
|
130
|
+
# provider: aliyun # aliyun | tencent(默认 aliyun)
|
|
131
|
+
#
|
|
132
|
+
# # ── 阿里云 OSS(provider: aliyun)──
|
|
133
|
+
# access_key_id: ""
|
|
134
|
+
# access_key_secret: ""
|
|
135
|
+
# endpoint: "http://oss-cn-hangzhou.aliyuncs.com"
|
|
136
|
+
# bucket_host: "https://your-bucket.oss-cn-hangzhou.aliyuncs.com"
|
|
137
|
+
# bucket: "your-bucket"
|
|
138
|
+
# region: "cn-hangzhou"
|
|
139
|
+
# path_prefix: "toolforge"
|
|
140
|
+
#
|
|
141
|
+
# # ── 腾讯云 COS(provider: tencent)──
|
|
142
|
+
# secret_id: "your-secret-id"
|
|
143
|
+
# secret_key: "your-secret-key"
|
|
144
|
+
# bucket: "your-bucket-1234567890"
|
|
145
|
+
# region: "ap-guangzhou"
|
|
146
|
+
# path_prefix: "toolforge"
|
|
147
|
+
# cdn_endpoint: "" # 留空则使用默认 COS 域名
|
|
148
|
+
|
|
149
|
+
# ── 工具私有配置(key 与 tools/ 下子目录同名)────────────────────
|
|
150
|
+
tools: {}
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
_TOOL_INIT_TEMPLATE = '''\
|
|
154
|
+
from pathlib import Path
|
|
155
|
+
from llm_toolforge import Plugin, InputField, load_tool_config
|
|
156
|
+
from .generator import {ClassName}Generator
|
|
157
|
+
|
|
158
|
+
_DIR = Path(__file__).parent
|
|
159
|
+
_CFG = load_tool_config("{tool_name}")
|
|
160
|
+
|
|
161
|
+
plugin = Plugin(
|
|
162
|
+
name="{display_name}",
|
|
163
|
+
description="TODO: 描述这个工具的功能",
|
|
164
|
+
generator={ClassName}Generator(),
|
|
165
|
+
route_path="/{route_path}",
|
|
166
|
+
operation_id="{operation_id}",
|
|
167
|
+
route_summary="TODO: 一句话接口摘要",
|
|
168
|
+
inputs=[
|
|
169
|
+
InputField(
|
|
170
|
+
name="input_text",
|
|
171
|
+
description="输入内容",
|
|
172
|
+
examples=["示例输入"],
|
|
173
|
+
),
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
'''
|
|
177
|
+
|
|
178
|
+
_TOOL_GENERATOR_TEMPLATE = '''\
|
|
179
|
+
from llm_toolforge import BaseGenerator, GenerateResult, ResultStatus
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class {ClassName}Generator(BaseGenerator):
|
|
183
|
+
"""TODO: 工具的业务实现"""
|
|
184
|
+
|
|
185
|
+
def generate(self, **kwargs) -> GenerateResult:
|
|
186
|
+
input_text = kwargs.get("input_text", "")
|
|
187
|
+
|
|
188
|
+
# TODO: 实现你的业务逻辑
|
|
189
|
+
result = f"处理结果:{{input_text}}"
|
|
190
|
+
|
|
191
|
+
return GenerateResult(
|
|
192
|
+
status=ResultStatus.SUCCESS,
|
|
193
|
+
outputs={{"result": result}},
|
|
194
|
+
)
|
|
195
|
+
'''
|
|
196
|
+
|
|
197
|
+
_REQUIREMENTS_TEMPLATE = """\
|
|
198
|
+
llm-toolforge
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
_DOCKERFILE_TEMPLATE = """\
|
|
202
|
+
FROM python:3.10-slim
|
|
203
|
+
|
|
204
|
+
WORKDIR /app
|
|
205
|
+
|
|
206
|
+
# 按需取消注释(qr_recognize 需要 libzbar0 / libgl1)
|
|
207
|
+
# RUN apt-get update && \\
|
|
208
|
+
# apt-get install -y --no-install-recommends libglib2.0-0 libgl1 libzbar0 && \\
|
|
209
|
+
# rm -rf /var/lib/apt/lists/*
|
|
210
|
+
|
|
211
|
+
COPY requirements.txt .
|
|
212
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
213
|
+
|
|
214
|
+
COPY . .
|
|
215
|
+
|
|
216
|
+
EXPOSE 12345 12346
|
|
217
|
+
|
|
218
|
+
CMD ["toolforge", "run"]
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
_README_TEMPLATE = """\
|
|
222
|
+
# {project_name}
|
|
223
|
+
|
|
224
|
+
基于 [llm-toolforge](https://github.com/xxx/llm-toolforge) 构建的工具集。
|
|
225
|
+
|
|
226
|
+
## 快速开始
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pip install -r requirements.txt
|
|
230
|
+
toolforge run
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
访问 http://localhost:12345 查看 Web 测试台。
|
|
234
|
+
|
|
235
|
+
## 添加新工具
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
toolforge new tool my_tool
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
然后编辑 `tools/my_tool/generator.py` 实现业务逻辑。
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ── 子命令实现 ──────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
def cmd_run(args) -> int:
|
|
248
|
+
from .server.runner import run
|
|
249
|
+
return run(
|
|
250
|
+
config_path=args.config,
|
|
251
|
+
api_only=args.api_only,
|
|
252
|
+
mcp_only=args.mcp_only,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def cmd_list(args) -> int:
|
|
257
|
+
from .config import load_config
|
|
258
|
+
from .discovery import discover_plugins
|
|
259
|
+
try:
|
|
260
|
+
config = load_config(args.config)
|
|
261
|
+
plugins = discover_plugins(config)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
print(f"错误:{e}", file=sys.stderr)
|
|
264
|
+
return 1
|
|
265
|
+
if not plugins:
|
|
266
|
+
print("(没有发现任何插件)")
|
|
267
|
+
return 0
|
|
268
|
+
print(f"发现 {len(plugins)} 个插件:\n")
|
|
269
|
+
for p in plugins:
|
|
270
|
+
print(f" [{p.operation_id}] {p.name}")
|
|
271
|
+
print(f" 路由: POST {p.route_path}")
|
|
272
|
+
print(f" 描述: {p.description[:80]}{'...' if len(p.description) > 80 else ''}")
|
|
273
|
+
print()
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def cmd_new_project(args) -> int:
|
|
278
|
+
dest = Path(args.dest).resolve()
|
|
279
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
|
|
281
|
+
cfg = dest / "config.yaml"
|
|
282
|
+
if cfg.exists():
|
|
283
|
+
print(f"config.yaml 已存在,跳过:{cfg}")
|
|
284
|
+
else:
|
|
285
|
+
cfg.write_text(_CONFIG_TEMPLATE, encoding="utf-8")
|
|
286
|
+
print(f"已创建:{cfg}")
|
|
287
|
+
|
|
288
|
+
tools_dir = dest / "tools"
|
|
289
|
+
tools_dir.mkdir(exist_ok=True)
|
|
290
|
+
(tools_dir / ".gitkeep").touch()
|
|
291
|
+
print(f"已创建:{tools_dir}/")
|
|
292
|
+
|
|
293
|
+
req = dest / "requirements.txt"
|
|
294
|
+
if not req.exists():
|
|
295
|
+
req.write_text(_REQUIREMENTS_TEMPLATE, encoding="utf-8")
|
|
296
|
+
print(f"已创建:{req}")
|
|
297
|
+
|
|
298
|
+
readme = dest / "README.md"
|
|
299
|
+
if not readme.exists():
|
|
300
|
+
project_name = dest.name
|
|
301
|
+
readme.write_text(_README_TEMPLATE.format(project_name=project_name), encoding="utf-8")
|
|
302
|
+
print(f"已创建:{readme}")
|
|
303
|
+
|
|
304
|
+
dockerfile = dest / "Dockerfile"
|
|
305
|
+
if not dockerfile.exists():
|
|
306
|
+
dockerfile.write_text(_DOCKERFILE_TEMPLATE, encoding="utf-8")
|
|
307
|
+
print(f"已创建:{dockerfile}")
|
|
308
|
+
|
|
309
|
+
print(f"\n项目初始化完成!下一步:")
|
|
310
|
+
print(f" 1. 编辑 {dest}/config.yaml,填入 Redis / AI provider 等配置")
|
|
311
|
+
print(f" 2. toolforge new tool <name> 添加第一个工具")
|
|
312
|
+
print(f" 3. toolforge run 启动服务")
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def cmd_new_tool(args) -> int:
|
|
317
|
+
tool_name: str = args.name.strip().replace("-", "_").lower()
|
|
318
|
+
class_name = "".join(w.capitalize() for w in tool_name.split("_"))
|
|
319
|
+
display_name = tool_name.replace("_", " ").title()
|
|
320
|
+
route_path = tool_name.replace("_", "-")
|
|
321
|
+
operation_id = "".join(
|
|
322
|
+
(w[0].upper() + w[1:] if i > 0 else w) for i, w in enumerate(tool_name.split("_"))
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# 目标目录:优先放在 ./tools/,其次 CWD/tool_name
|
|
326
|
+
tools_root = Path.cwd() / "tools"
|
|
327
|
+
if tools_root.exists():
|
|
328
|
+
tool_dir = tools_root / tool_name
|
|
329
|
+
else:
|
|
330
|
+
tool_dir = Path.cwd() / tool_name
|
|
331
|
+
|
|
332
|
+
if tool_dir.exists():
|
|
333
|
+
print(f"目录已存在:{tool_dir},请手动检查是否需要覆盖。", file=sys.stderr)
|
|
334
|
+
return 1
|
|
335
|
+
|
|
336
|
+
tool_dir.mkdir(parents=True)
|
|
337
|
+
(tool_dir / "__init__.py").write_text(
|
|
338
|
+
_TOOL_INIT_TEMPLATE.format(
|
|
339
|
+
ClassName=class_name,
|
|
340
|
+
tool_name=tool_name,
|
|
341
|
+
display_name=display_name,
|
|
342
|
+
route_path=route_path,
|
|
343
|
+
operation_id=operation_id,
|
|
344
|
+
),
|
|
345
|
+
encoding="utf-8",
|
|
346
|
+
)
|
|
347
|
+
(tool_dir / "generator.py").write_text(
|
|
348
|
+
_TOOL_GENERATOR_TEMPLATE.format(ClassName=class_name),
|
|
349
|
+
encoding="utf-8",
|
|
350
|
+
)
|
|
351
|
+
print(f"已创建工具骨架:{tool_dir}/")
|
|
352
|
+
print(f" {tool_dir}/__init__.py — 插件声明(Plugin)")
|
|
353
|
+
print(f" {tool_dir}/generator.py — 业务逻辑({class_name}Generator)")
|
|
354
|
+
print(f"\n下一步:")
|
|
355
|
+
print(f" 1. 编辑 {tool_dir}/generator.py 实现业务逻辑")
|
|
356
|
+
print(f" 2. 运行 toolforge list 确认工具已被发现")
|
|
357
|
+
print(f" 3. 运行 toolforge run 启动服务并在 Web 测试台验证")
|
|
358
|
+
return 0
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ── 参数解析 ────────────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
364
|
+
parser = argparse.ArgumentParser(
|
|
365
|
+
prog="toolforge",
|
|
366
|
+
description="llm-toolforge 命令行工具",
|
|
367
|
+
)
|
|
368
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
369
|
+
sub.required = True
|
|
370
|
+
|
|
371
|
+
# run
|
|
372
|
+
p_run = sub.add_parser("run", help="启动 API + MCP 服务")
|
|
373
|
+
p_run.add_argument("--config", "-c", metavar="FILE", help="config.yaml 路径(默认:自动查找)")
|
|
374
|
+
g = p_run.add_mutually_exclusive_group()
|
|
375
|
+
g.add_argument("--api-only", action="store_true", help="只启动 HTTP API")
|
|
376
|
+
g.add_argument("--mcp-only", action="store_true", help="只启动 MCP SSE 服务")
|
|
377
|
+
p_run.set_defaults(func=cmd_run)
|
|
378
|
+
|
|
379
|
+
# list
|
|
380
|
+
p_list = sub.add_parser("list", help="列出已发现的工具")
|
|
381
|
+
p_list.add_argument("--config", "-c", metavar="FILE")
|
|
382
|
+
p_list.set_defaults(func=cmd_list)
|
|
383
|
+
|
|
384
|
+
# new
|
|
385
|
+
p_new = sub.add_parser("new", help="生成脚手架")
|
|
386
|
+
new_sub = p_new.add_subparsers(dest="new_type", metavar="<type>")
|
|
387
|
+
new_sub.required = True
|
|
388
|
+
|
|
389
|
+
p_new_project = new_sub.add_parser("project", help="初始化新项目")
|
|
390
|
+
p_new_project.add_argument("dest", nargs="?", default=".", help="目标目录(默认:当前目录)")
|
|
391
|
+
p_new_project.set_defaults(func=cmd_new_project)
|
|
392
|
+
|
|
393
|
+
p_new_tool = new_sub.add_parser("tool", help="在 tools/ 目录下生成工具骨架")
|
|
394
|
+
p_new_tool.add_argument("name", help="工具目录名(如 my_tool)")
|
|
395
|
+
p_new_tool.set_defaults(func=cmd_new_tool)
|
|
396
|
+
|
|
397
|
+
return parser
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def main() -> None:
|
|
401
|
+
parser = _build_parser()
|
|
402
|
+
args = parser.parse_args()
|
|
403
|
+
sys.exit(args.func(args))
|
llm_toolforge/config.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置加载
|
|
3
|
+
|
|
4
|
+
所有 config.yaml 相关的 I/O 都在这个模块,框架其余部分不直接读文件。
|
|
5
|
+
|
|
6
|
+
查找优先级(find_config):
|
|
7
|
+
1. 环境变量 TOOLFORGE_CONFIG(绝对路径或相对于 CWD)
|
|
8
|
+
2. CWD/config.yaml
|
|
9
|
+
3. 从 CWD 向上逐级搜索(兼容 cd 到子目录运行的场景)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_config(path: Optional[str] = None) -> Path:
|
|
20
|
+
"""定位 config.yaml 并返回其绝对路径。
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
path: 明确指定的路径(优先级最高),None 则自动查找。
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
FileNotFoundError: 找不到配置文件时抛出,消息包含修复建议。
|
|
27
|
+
"""
|
|
28
|
+
if path:
|
|
29
|
+
p = Path(path)
|
|
30
|
+
if not p.is_absolute():
|
|
31
|
+
p = Path.cwd() / p
|
|
32
|
+
if not p.exists():
|
|
33
|
+
raise FileNotFoundError(f"指定的配置文件不存在:{p}")
|
|
34
|
+
return p.resolve()
|
|
35
|
+
|
|
36
|
+
env = os.environ.get("TOOLFORGE_CONFIG")
|
|
37
|
+
if env:
|
|
38
|
+
p = Path(env)
|
|
39
|
+
if not p.is_absolute():
|
|
40
|
+
p = Path.cwd() / p
|
|
41
|
+
if p.exists():
|
|
42
|
+
return p.resolve()
|
|
43
|
+
raise FileNotFoundError(
|
|
44
|
+
f"环境变量 TOOLFORGE_CONFIG 指向的文件不存在:{p}\n"
|
|
45
|
+
"请检查路径是否正确。"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
cwd_cfg = Path.cwd() / "config.yaml"
|
|
49
|
+
if cwd_cfg.exists():
|
|
50
|
+
return cwd_cfg
|
|
51
|
+
|
|
52
|
+
d = Path.cwd()
|
|
53
|
+
for _ in range(4):
|
|
54
|
+
d = d.parent
|
|
55
|
+
cfg = d / "config.yaml"
|
|
56
|
+
if cfg.exists():
|
|
57
|
+
return cfg
|
|
58
|
+
|
|
59
|
+
raise FileNotFoundError(
|
|
60
|
+
"找不到 config.yaml。\n"
|
|
61
|
+
"请在项目根目录创建配置文件,或通过 TOOLFORGE_CONFIG 环境变量指定路径。\n"
|
|
62
|
+
"可以用 `toolforge new project .` 生成配置模板。"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_config(path: Optional[str] = None) -> dict:
|
|
67
|
+
"""加载并返回 config.yaml 的内容(dict)。"""
|
|
68
|
+
return yaml.safe_load(find_config(path).read_text(encoding="utf-8")) or {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_tool_config(name: str, config: Optional[dict] = None) -> dict:
|
|
72
|
+
"""加载工具私有配置段 tools.<name>。
|
|
73
|
+
|
|
74
|
+
工具在 __init__.py 中调用(不需要知道文件路径):
|
|
75
|
+
|
|
76
|
+
from llm_toolforge import load_tool_config
|
|
77
|
+
_CFG = load_tool_config("my_tool")
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
name: 工具注册名(与 tools/ 下子目录名一致)。
|
|
81
|
+
config: 已加载的 config dict,传入可避免重复读文件。
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
dict,没有对应配置段时返回 {}。
|
|
85
|
+
"""
|
|
86
|
+
if config is None:
|
|
87
|
+
config = load_config()
|
|
88
|
+
return (config.get("tools") or {}).get(name) or {}
|