inkcre-cli 0.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.
- inkcre_cli-0.1.0/PKG-INFO +77 -0
- inkcre_cli-0.1.0/README.md +64 -0
- inkcre_cli-0.1.0/pyproject.toml +65 -0
- inkcre_cli-0.1.0/src/inkcre_cli/__init__.py +1 -0
- inkcre_cli-0.1.0/src/inkcre_cli/command.py +187 -0
- inkcre_cli-0.1.0/src/inkcre_cli/commands/__init__.py +1 -0
- inkcre_cli-0.1.0/src/inkcre_cli/commands/agent.py +121 -0
- inkcre_cli-0.1.0/src/inkcre_cli/commands/config.py +95 -0
- inkcre_cli-0.1.0/src/inkcre_cli/commands/connection.py +109 -0
- inkcre_cli-0.1.0/src/inkcre_cli/commands/extension.py +125 -0
- inkcre_cli-0.1.0/src/inkcre_cli/commands/info.py +245 -0
- inkcre_cli-0.1.0/src/inkcre_cli/commands/jobs.py +279 -0
- inkcre_cli-0.1.0/src/inkcre_cli/commands/peer.py +67 -0
- inkcre_cli-0.1.0/src/inkcre_cli/commands/source.py +147 -0
- inkcre_cli-0.1.0/src/inkcre_cli/connection.py +64 -0
- inkcre_cli-0.1.0/src/inkcre_cli/errors.py +17 -0
- inkcre_cli-0.1.0/src/inkcre_cli/http.py +107 -0
- inkcre_cli-0.1.0/src/inkcre_cli/main.py +75 -0
- inkcre_cli-0.1.0/src/inkcre_cli/output.py +92 -0
- inkcre_cli-0.1.0/src/inkcre_cli/schema.py +71 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inkcre-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An agent-friendly REST interface to InKCre
|
|
5
|
+
License-Expression: GPL-3.0-only
|
|
6
|
+
Project-URL: Repository, https://github.com/InKCre/core-py
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: click<9,>=8.1
|
|
9
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
10
|
+
Requires-Dist: httpx<0.29,>=0.28.1
|
|
11
|
+
Requires-Dist: PyJWT<3,>=2.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# inkcre-cli
|
|
15
|
+
|
|
16
|
+
InKCre 的独立命令行界面,通过 Core REST API 检索信息、读取 Resolver 内容和管理部署。
|
|
17
|
+
它不安装 Core、不访问数据库,也不是执行 Job 的 Peer。
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
python -m pip install inkcre-cli
|
|
21
|
+
inkcre-cli --help
|
|
22
|
+
inkcre-cli connection set personal --input connection.json
|
|
23
|
+
inkcre-cli recall 'Agent 工具' --mode lexical --json
|
|
24
|
+
inkcre-cli get block:42 relation:8 --json
|
|
25
|
+
inkcre-cli resolver invoke block:42 --method get_solved_content
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
connection.json 包含 `base_url` 与 `jwt_secret`。默认连接文件是
|
|
29
|
+
`~/.inkcre/cli/connections.json`;可用 `--connections-file` 指定隔离文件。
|
|
30
|
+
这里的地址是 Core REST 入口,不是 PostgREST 或 MCP endpoint。
|
|
31
|
+
首个连接自动成为默认连接;`connection use personal` 修改默认值,`--connection NAME` 只选择本次接入。
|
|
32
|
+
|
|
33
|
+
`--help` 离线可用;有结构化输入的命令提供 `--schema`,从当前 Core 发现准确合同。
|
|
34
|
+
使用 `--input FILE`、`--input -` 或 `--input-json JSON` 提供输入。
|
|
35
|
+
`--json` 输出完整紧凑 JSON;二进制内容保存为文件,不做 Base64 编码。
|
|
36
|
+
`--output-dir DIR` 显式保存完整结果,stdout 返回入口文件路径。
|
|
37
|
+
|
|
38
|
+
## 发现与管理
|
|
39
|
+
|
|
40
|
+
先发现 exact type、method 或 schema,再提交它所描述的输入。例如:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
inkcre-cli source types --json
|
|
44
|
+
inkcre-cli source create --type SOURCE_TYPE --schema
|
|
45
|
+
inkcre-cli source create --type SOURCE_TYPE --input source.json
|
|
46
|
+
inkcre-cli source collect 42 --input-json '{}'
|
|
47
|
+
inkcre-cli job wait 17 --for 10s --json
|
|
48
|
+
inkcre-cli job abort 17
|
|
49
|
+
inkcre-cli cron create --job-type core.source.collect.v1 --schema
|
|
50
|
+
inkcre-cli agent tools --json
|
|
51
|
+
inkcre-cli ai models --json
|
|
52
|
+
inkcre-cli config schemas --json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
这里的 ID 仅为示例,应使用实际创建回执或查询返回的 ID。`source collect`、`source backfill` 和
|
|
56
|
+
`organization ruminate` 创建 Job,而不是在 CLI 或接入 Core 中同步执行。`job wait --for 10s` 最多观察
|
|
57
|
+
十秒,可能返回仍为 pending/running 的记录;停止观察不取消 Job。`job abort` 请求 best-effort 停止,
|
|
58
|
+
`abort_requested: true` 不代表执行端已经完成清理。只有后续记录才能说明实际结果。
|
|
59
|
+
|
|
60
|
+
`connection` 只管理本机连接,`config` 管理远端 deployment config;Agent definition 由 `agent` 管理,
|
|
61
|
+
模型目录由 `ai` 发现。Extension 配置和运行目标使用 `extension`,不混入 connection。
|
|
62
|
+
|
|
63
|
+
## 输出与续读
|
|
64
|
+
|
|
65
|
+
结果写 stdout,命令错误写 stderr。退出码 0 表示命令成功,1 表示远端/IO/部分结果失败,2 表示本机输入
|
|
66
|
+
错误,130 表示观察被中断。读取一个状态为 failed 的 Job 仍是成功读取,脚本应检查 Job 的实际 status。
|
|
67
|
+
批量读取与混合 recall 保留各项结果,不因一项失败丢弃其它成功项。
|
|
68
|
+
|
|
69
|
+
支持分页的目录使用 `--limit`、`--cursor` 和返回的 `next_cursor`;CLI 不自动翻页。
|
|
70
|
+
检索仍保留各模式自身的 limit 和结果结构,混合 recall 不融合排名。
|
|
71
|
+
|
|
72
|
+
人类可读输出过长时显示摘要和完整文件路径;`--json` 不截断、不 pretty-print。
|
|
73
|
+
二进制自动保存为文件;显式 `--output-dir DIR` 导出的入口 JSON 使用相对文件引用,整个子目录可以搬移。
|
|
74
|
+
这些是同一次 HTTP 响应的本地文件,不是新下载会话,也不会再次调用 Resolver。
|
|
75
|
+
|
|
76
|
+
开发使用独立 PDM 项目:`pdm install -p cli`、`pdm run -p cli check`、`pdm build -p cli`。
|
|
77
|
+
0.0.0 是未发布的开发版本;首次发行由仓库的 Release PR 准备。
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# inkcre-cli
|
|
2
|
+
|
|
3
|
+
InKCre 的独立命令行界面,通过 Core REST API 检索信息、读取 Resolver 内容和管理部署。
|
|
4
|
+
它不安装 Core、不访问数据库,也不是执行 Job 的 Peer。
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
python -m pip install inkcre-cli
|
|
8
|
+
inkcre-cli --help
|
|
9
|
+
inkcre-cli connection set personal --input connection.json
|
|
10
|
+
inkcre-cli recall 'Agent 工具' --mode lexical --json
|
|
11
|
+
inkcre-cli get block:42 relation:8 --json
|
|
12
|
+
inkcre-cli resolver invoke block:42 --method get_solved_content
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
connection.json 包含 `base_url` 与 `jwt_secret`。默认连接文件是
|
|
16
|
+
`~/.inkcre/cli/connections.json`;可用 `--connections-file` 指定隔离文件。
|
|
17
|
+
这里的地址是 Core REST 入口,不是 PostgREST 或 MCP endpoint。
|
|
18
|
+
首个连接自动成为默认连接;`connection use personal` 修改默认值,`--connection NAME` 只选择本次接入。
|
|
19
|
+
|
|
20
|
+
`--help` 离线可用;有结构化输入的命令提供 `--schema`,从当前 Core 发现准确合同。
|
|
21
|
+
使用 `--input FILE`、`--input -` 或 `--input-json JSON` 提供输入。
|
|
22
|
+
`--json` 输出完整紧凑 JSON;二进制内容保存为文件,不做 Base64 编码。
|
|
23
|
+
`--output-dir DIR` 显式保存完整结果,stdout 返回入口文件路径。
|
|
24
|
+
|
|
25
|
+
## 发现与管理
|
|
26
|
+
|
|
27
|
+
先发现 exact type、method 或 schema,再提交它所描述的输入。例如:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
inkcre-cli source types --json
|
|
31
|
+
inkcre-cli source create --type SOURCE_TYPE --schema
|
|
32
|
+
inkcre-cli source create --type SOURCE_TYPE --input source.json
|
|
33
|
+
inkcre-cli source collect 42 --input-json '{}'
|
|
34
|
+
inkcre-cli job wait 17 --for 10s --json
|
|
35
|
+
inkcre-cli job abort 17
|
|
36
|
+
inkcre-cli cron create --job-type core.source.collect.v1 --schema
|
|
37
|
+
inkcre-cli agent tools --json
|
|
38
|
+
inkcre-cli ai models --json
|
|
39
|
+
inkcre-cli config schemas --json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
这里的 ID 仅为示例,应使用实际创建回执或查询返回的 ID。`source collect`、`source backfill` 和
|
|
43
|
+
`organization ruminate` 创建 Job,而不是在 CLI 或接入 Core 中同步执行。`job wait --for 10s` 最多观察
|
|
44
|
+
十秒,可能返回仍为 pending/running 的记录;停止观察不取消 Job。`job abort` 请求 best-effort 停止,
|
|
45
|
+
`abort_requested: true` 不代表执行端已经完成清理。只有后续记录才能说明实际结果。
|
|
46
|
+
|
|
47
|
+
`connection` 只管理本机连接,`config` 管理远端 deployment config;Agent definition 由 `agent` 管理,
|
|
48
|
+
模型目录由 `ai` 发现。Extension 配置和运行目标使用 `extension`,不混入 connection。
|
|
49
|
+
|
|
50
|
+
## 输出与续读
|
|
51
|
+
|
|
52
|
+
结果写 stdout,命令错误写 stderr。退出码 0 表示命令成功,1 表示远端/IO/部分结果失败,2 表示本机输入
|
|
53
|
+
错误,130 表示观察被中断。读取一个状态为 failed 的 Job 仍是成功读取,脚本应检查 Job 的实际 status。
|
|
54
|
+
批量读取与混合 recall 保留各项结果,不因一项失败丢弃其它成功项。
|
|
55
|
+
|
|
56
|
+
支持分页的目录使用 `--limit`、`--cursor` 和返回的 `next_cursor`;CLI 不自动翻页。
|
|
57
|
+
检索仍保留各模式自身的 limit 和结果结构,混合 recall 不融合排名。
|
|
58
|
+
|
|
59
|
+
人类可读输出过长时显示摘要和完整文件路径;`--json` 不截断、不 pretty-print。
|
|
60
|
+
二进制自动保存为文件;显式 `--output-dir DIR` 导出的入口 JSON 使用相对文件引用,整个子目录可以搬移。
|
|
61
|
+
这些是同一次 HTTP 响应的本地文件,不是新下载会话,也不会再次调用 Resolver。
|
|
62
|
+
|
|
63
|
+
开发使用独立 PDM 项目:`pdm install -p cli`、`pdm run -p cli check`、`pdm build -p cli`。
|
|
64
|
+
0.0.0 是未发布的开发版本;首次发行由仓库的 Release PR 准备。
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "inkcre-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "An agent-friendly REST interface to InKCre"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "GPL-3.0-only"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"click>=8.1,<9",
|
|
10
|
+
"pydantic>=2.10,<3",
|
|
11
|
+
"httpx>=0.28.1,<0.29",
|
|
12
|
+
"PyJWT>=2.10,<3",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
inkcre-cli = "inkcre_cli.main:main"
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Repository = "https://github.com/InKCre/core-py"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = [
|
|
23
|
+
"pdm-backend",
|
|
24
|
+
]
|
|
25
|
+
build-backend = "pdm.backend"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"ruff>=0.15,<0.16",
|
|
30
|
+
"pyrefly>=0.56,<0.57",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.pdm]
|
|
34
|
+
distribution = true
|
|
35
|
+
|
|
36
|
+
[tool.pdm.scripts]
|
|
37
|
+
lint = "ruff check src"
|
|
38
|
+
format = "ruff format src"
|
|
39
|
+
"format:check" = "ruff format --check src"
|
|
40
|
+
typecheck = "pyrefly check"
|
|
41
|
+
|
|
42
|
+
[tool.pdm.scripts.check]
|
|
43
|
+
composite = [
|
|
44
|
+
"lint",
|
|
45
|
+
"format:check",
|
|
46
|
+
"typecheck",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 100
|
|
51
|
+
target-version = "py312"
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = [
|
|
55
|
+
"E",
|
|
56
|
+
"F",
|
|
57
|
+
"I",
|
|
58
|
+
"UP",
|
|
59
|
+
"B",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.pyrefly]
|
|
63
|
+
project-includes = [
|
|
64
|
+
"src",
|
|
65
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Independent InKCre REST consumer; no Core runtime imports."""
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Small shared Click options and one invocation's resources."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from pydantic import BaseModel, TypeAdapter
|
|
10
|
+
|
|
11
|
+
from . import connection
|
|
12
|
+
from .errors import CommandError
|
|
13
|
+
from .http import CoreRESTClient
|
|
14
|
+
from .output import Output
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Command(click.Command):
|
|
18
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
19
|
+
original = args.copy()
|
|
20
|
+
try:
|
|
21
|
+
return super().parse_args(ctx, args)
|
|
22
|
+
except click.UsageError:
|
|
23
|
+
# Click rejects unknown options before eager callbacks run. Its native
|
|
24
|
+
# tolerant parse can still find presentation options without scanning
|
|
25
|
+
# raw argv (which would mistake an --input-json value for an option).
|
|
26
|
+
with self.make_context(
|
|
27
|
+
ctx.info_name,
|
|
28
|
+
original,
|
|
29
|
+
parent=ctx.parent,
|
|
30
|
+
obj=ctx.obj,
|
|
31
|
+
resilient_parsing=True,
|
|
32
|
+
ignore_unknown_options=True,
|
|
33
|
+
):
|
|
34
|
+
pass
|
|
35
|
+
raise
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Group(Command, click.Group):
|
|
39
|
+
command_class = Command
|
|
40
|
+
group_class = type
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Entity(BaseModel):
|
|
44
|
+
type: Literal["block", "relation"]
|
|
45
|
+
id: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def entity(value: str, *, block_only: bool = False) -> Entity:
|
|
49
|
+
kind, separator, identity = value.partition(":")
|
|
50
|
+
if not separator:
|
|
51
|
+
raise click.BadParameter("使用 block:<id> 或 relation:<id>")
|
|
52
|
+
result = Entity.model_validate({"type": kind, "id": identity})
|
|
53
|
+
if block_only and result.type != "block":
|
|
54
|
+
raise click.BadParameter("此操作需要 block:<id>")
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def segment(value: str | int) -> str:
|
|
59
|
+
return quote(str(value), safe="")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def duration(value: str) -> float:
|
|
63
|
+
suffix = value[-1:] if value[-1:].isalpha() else ""
|
|
64
|
+
scale = {"": 1, "s": 1, "m": 60, "h": 3600}
|
|
65
|
+
try:
|
|
66
|
+
result = float(value[:-1] if suffix else value) * scale[suffix]
|
|
67
|
+
if not 0 < result < float("inf"):
|
|
68
|
+
raise ValueError
|
|
69
|
+
return result
|
|
70
|
+
except (ValueError, KeyError) as error:
|
|
71
|
+
raise click.BadParameter("使用正数秒,或 30s / 2m / 1h") from error
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Invocation:
|
|
75
|
+
def __init__(self):
|
|
76
|
+
self.connection_name: str | None = None
|
|
77
|
+
self.connections_file: Path | None = None
|
|
78
|
+
self.output = Output()
|
|
79
|
+
self._client: CoreRESTClient | None = None
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def local_file(self) -> Path:
|
|
83
|
+
return connection.file_path(self.connections_file)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def client(self) -> CoreRESTClient:
|
|
87
|
+
if self._client is None:
|
|
88
|
+
_, selected = connection.select(connection.load(self.local_file), self.connection_name)
|
|
89
|
+
self._client = CoreRESTClient(selected)
|
|
90
|
+
return self._client
|
|
91
|
+
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
if self._client:
|
|
94
|
+
self._client.close()
|
|
95
|
+
|
|
96
|
+
def send(
|
|
97
|
+
self,
|
|
98
|
+
method: str,
|
|
99
|
+
path: str,
|
|
100
|
+
*,
|
|
101
|
+
params: dict | None = None,
|
|
102
|
+
body: Any = None,
|
|
103
|
+
strip: tuple[str, ...] = (),
|
|
104
|
+
selectors: dict[str, str] | None = None,
|
|
105
|
+
) -> Any:
|
|
106
|
+
try:
|
|
107
|
+
return self.client.request(method, path, params=params, body=body)
|
|
108
|
+
except CommandError as error:
|
|
109
|
+
if error.status == 422 and isinstance(error.detail, list):
|
|
110
|
+
detail = []
|
|
111
|
+
for item in error.detail:
|
|
112
|
+
loc = list(item.get("loc", ()))
|
|
113
|
+
if loc[:1] == ["body"]:
|
|
114
|
+
loc = loc[1:]
|
|
115
|
+
if loc[: len(strip)] == list(strip):
|
|
116
|
+
loc = loc[len(strip) :]
|
|
117
|
+
if loc and selectors and loc[0] in selectors:
|
|
118
|
+
loc[0] = selectors[loc[0]]
|
|
119
|
+
detail.append({**item, "loc": loc})
|
|
120
|
+
raise CommandError(detail, status=error.status) from error
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
def show(self, value: Any, *, no_content: bool = False, partial_failure: bool = False) -> None:
|
|
124
|
+
self.output.emit(value, no_content=no_content)
|
|
125
|
+
if partial_failure:
|
|
126
|
+
raise CommandError("结果中存在逐项错误;成功项已保留")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _option(ctx: click.Context, parameter: click.Parameter, value: Any) -> Any:
|
|
130
|
+
invocation = ctx.ensure_object(Invocation)
|
|
131
|
+
if value is not None:
|
|
132
|
+
if parameter.name == "json_mode":
|
|
133
|
+
invocation.output.json_mode = value
|
|
134
|
+
elif parameter.name == "output_dir":
|
|
135
|
+
invocation.output.directory = value
|
|
136
|
+
else:
|
|
137
|
+
setattr(invocation, parameter.name, value)
|
|
138
|
+
return value
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def common(function):
|
|
142
|
+
options: list[tuple[tuple[str, ...], dict[str, Any]]] = [
|
|
143
|
+
(("--connection", "connection_name"), {"help": "本次使用的本机连接名"}),
|
|
144
|
+
(("--connections-file",), {"type": click.Path(path_type=Path), "help": "替换连接配置文件"}),
|
|
145
|
+
(
|
|
146
|
+
("--output-dir",),
|
|
147
|
+
{"type": click.Path(path_type=Path), "help": "保存完整结果,返回入口文件"},
|
|
148
|
+
),
|
|
149
|
+
(
|
|
150
|
+
("--json", "json_mode"),
|
|
151
|
+
{"is_flag": True, "default": None, "is_eager": True, "help": "完整紧凑 JSON"},
|
|
152
|
+
),
|
|
153
|
+
]
|
|
154
|
+
for args, kwargs in options:
|
|
155
|
+
function = click.option(*args, callback=_option, expose_value=False, **kwargs)(function)
|
|
156
|
+
return function
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def input_options(function):
|
|
160
|
+
function = click.option("--schema", is_flag=True, help="只查询输入 JSON Schema,不执行操作")(
|
|
161
|
+
function
|
|
162
|
+
)
|
|
163
|
+
function = click.option("--input-json", help="直接提供 JSON 对象")(function)
|
|
164
|
+
return click.option(
|
|
165
|
+
"--input", "input_file", type=click.Path(path_type=Path), help="JSON 文件,- 表示 stdin"
|
|
166
|
+
)(function)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def paging(function):
|
|
170
|
+
function = click.option("--cursor", help="前一页返回的 next_cursor;保留相同过滤条件")(function)
|
|
171
|
+
return click.option(
|
|
172
|
+
"--limit", type=click.IntRange(min=1), help="本次最多返回多少项;不自动翻页"
|
|
173
|
+
)(function)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def load_input(input_file: Path | None, input_json: str | None) -> dict[str, Any]:
|
|
177
|
+
if input_file is not None and input_json is not None:
|
|
178
|
+
raise click.UsageError("--input 与 --input-json 不能同时使用")
|
|
179
|
+
if input_file is not None:
|
|
180
|
+
text = (
|
|
181
|
+
sys.stdin.read() if str(input_file) == "-" else input_file.read_text(encoding="utf-8")
|
|
182
|
+
)
|
|
183
|
+
elif input_json is not None:
|
|
184
|
+
text = input_json
|
|
185
|
+
else:
|
|
186
|
+
return {}
|
|
187
|
+
return TypeAdapter(dict[str, Any]).validate_json(text)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ordinary command groups, not an Extension command registry."""
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Agent definitions and read-only AI/profile discovery."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..command import Group, Invocation, common, input_options, load_input, paging, segment
|
|
6
|
+
from ..schema import request_schema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group(cls=Group)
|
|
10
|
+
def agent():
|
|
11
|
+
"""管理 Agent definition;不启动对话或本机 Agent runtime。"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@agent.command("list")
|
|
15
|
+
@paging
|
|
16
|
+
@common
|
|
17
|
+
@click.pass_obj
|
|
18
|
+
def list_agents(inv: Invocation, limit, cursor):
|
|
19
|
+
"""列出 Agent definition。"""
|
|
20
|
+
inv.show(inv.send("GET", "/agents", params={"limit": limit, "cursor": cursor}))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@agent.command()
|
|
24
|
+
@click.argument("agent_id", type=int)
|
|
25
|
+
@common
|
|
26
|
+
@click.pass_obj
|
|
27
|
+
def get(inv: Invocation, agent_id):
|
|
28
|
+
"""读取一个 Agent definition。"""
|
|
29
|
+
inv.show(inv.send("GET", f"/agents/{agent_id}"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@agent.command()
|
|
33
|
+
@input_options
|
|
34
|
+
@common
|
|
35
|
+
@click.pass_obj
|
|
36
|
+
def create(inv: Invocation, schema, input_file, input_json):
|
|
37
|
+
"""创建 Agent definition;模型用 ai models、工具用 agent tools 发现。"""
|
|
38
|
+
if schema:
|
|
39
|
+
return inv.show(request_schema(inv.client, "/agents", "POST"))
|
|
40
|
+
inv.show(inv.send("POST", "/agents", body=load_input(input_file, input_json)))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@agent.command()
|
|
44
|
+
@click.argument("agent_id", type=int)
|
|
45
|
+
@input_options
|
|
46
|
+
@common
|
|
47
|
+
@click.pass_obj
|
|
48
|
+
def update(inv: Invocation, agent_id, schema, input_file, input_json):
|
|
49
|
+
"""修改提交的 Agent definition 字段。"""
|
|
50
|
+
if schema:
|
|
51
|
+
return inv.show(request_schema(inv.client, "/agents/{agent_id}", "PATCH"))
|
|
52
|
+
inv.show(inv.send("PATCH", f"/agents/{agent_id}", body=load_input(input_file, input_json)))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@agent.command()
|
|
56
|
+
@click.argument("agent_id", type=int)
|
|
57
|
+
@common
|
|
58
|
+
@click.pass_obj
|
|
59
|
+
def delete(inv: Invocation, agent_id):
|
|
60
|
+
"""删除 Agent definition。"""
|
|
61
|
+
inv.show(inv.send("DELETE", f"/agents/{agent_id}"), no_content=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@agent.command()
|
|
65
|
+
@click.argument("tool_id", required=False)
|
|
66
|
+
@paging
|
|
67
|
+
@common
|
|
68
|
+
@click.pass_obj
|
|
69
|
+
def tools(inv: Invocation, tool_id, limit, cursor):
|
|
70
|
+
"""列出本机已注册 Agent Tool;给定 ID 时取得完整合同,不执行工具。"""
|
|
71
|
+
inv.show(
|
|
72
|
+
inv.send(
|
|
73
|
+
"GET",
|
|
74
|
+
"/agent-tools" + ("/" + segment(tool_id) if tool_id else ""),
|
|
75
|
+
params={"limit": limit, "cursor": cursor},
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@click.group(cls=Group)
|
|
81
|
+
def ai():
|
|
82
|
+
"""发现 AI 模型,不混入 Agent definition 的管理范围。"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@ai.command()
|
|
86
|
+
@click.argument("model_id", type=int, required=False)
|
|
87
|
+
@paging
|
|
88
|
+
@common
|
|
89
|
+
@click.pass_obj
|
|
90
|
+
def models(inv: Invocation, model_id, limit, cursor):
|
|
91
|
+
"""列出 AIModel,或读取指定 ID;不返回 Provider 的 secret config。"""
|
|
92
|
+
inv.show(
|
|
93
|
+
inv.send(
|
|
94
|
+
"GET",
|
|
95
|
+
"/ai/models" + (f"/{model_id}" if model_id is not None else ""),
|
|
96
|
+
params={"limit": limit, "cursor": cursor},
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@click.group("embedding-profile", cls=Group)
|
|
102
|
+
def embedding_profile():
|
|
103
|
+
"""只读发现语义检索可选的 embedding profile。"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@embedding_profile.command("list")
|
|
107
|
+
@paging
|
|
108
|
+
@common
|
|
109
|
+
@click.pass_obj
|
|
110
|
+
def list_profiles(inv: Invocation, limit, cursor):
|
|
111
|
+
"""列出 EmbeddingProfile。"""
|
|
112
|
+
inv.show(inv.send("GET", "/embedding-profiles", params={"limit": limit, "cursor": cursor}))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@embedding_profile.command("get")
|
|
116
|
+
@click.argument("profile_id", type=int)
|
|
117
|
+
@common
|
|
118
|
+
@click.pass_obj
|
|
119
|
+
def get_profile(inv: Invocation, profile_id):
|
|
120
|
+
"""读取 EmbeddingProfile。"""
|
|
121
|
+
inv.show(inv.send("GET", f"/embedding-profiles/{profile_id}"))
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Deployment config values and their runtime-owned input schemas."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..command import Group, Invocation, common, input_options, load_input, paging, segment
|
|
6
|
+
from ..schema import partial
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group(cls=Group)
|
|
10
|
+
def config():
|
|
11
|
+
"""远端 deployment 配置,与本机 connection 文件分开。"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@config.command("list")
|
|
15
|
+
@paging
|
|
16
|
+
@common
|
|
17
|
+
@click.pass_obj
|
|
18
|
+
def list_configs(inv: Invocation, limit, cursor):
|
|
19
|
+
"""列出保存的 deployment config,不要求 schema 已加载。"""
|
|
20
|
+
inv.show(inv.send("GET", "/configs", params={"limit": limit, "cursor": cursor}))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@config.command()
|
|
24
|
+
@click.argument("key")
|
|
25
|
+
@common
|
|
26
|
+
@click.pass_obj
|
|
27
|
+
def get(inv: Invocation, key):
|
|
28
|
+
"""读取配置的实际 schema/value。"""
|
|
29
|
+
inv.show(inv.send("GET", "/configs/" + segment(key)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@config.command()
|
|
33
|
+
@click.argument("schema_id", required=False)
|
|
34
|
+
@paging
|
|
35
|
+
@common
|
|
36
|
+
@click.pass_obj
|
|
37
|
+
def schemas(inv: Invocation, schema_id, limit, cursor):
|
|
38
|
+
"""发现接入 Peer 已加载的 schema;给出 ID 时读取输入合同。"""
|
|
39
|
+
inv.show(
|
|
40
|
+
inv.send(
|
|
41
|
+
"GET",
|
|
42
|
+
"/config-schemas" + ("/" + segment(schema_id) if schema_id else ""),
|
|
43
|
+
params={"limit": limit, "cursor": cursor},
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@config.command()
|
|
49
|
+
@click.argument("key")
|
|
50
|
+
@click.option("--schema-id", required=True)
|
|
51
|
+
@input_options
|
|
52
|
+
@common
|
|
53
|
+
@click.pass_obj
|
|
54
|
+
def replace(inv: Invocation, key, schema_id, schema, input_file, input_json):
|
|
55
|
+
"""创建或完整替换;JSON 是 value 本身,不再包装 value。"""
|
|
56
|
+
if schema:
|
|
57
|
+
return inv.show(inv.send("GET", "/config-schemas/" + segment(schema_id))["input_schema"])
|
|
58
|
+
inv.show(
|
|
59
|
+
inv.send(
|
|
60
|
+
"PUT",
|
|
61
|
+
"/configs/" + segment(key),
|
|
62
|
+
body={
|
|
63
|
+
"schema": schema_id,
|
|
64
|
+
"value": load_input(input_file, input_json),
|
|
65
|
+
},
|
|
66
|
+
strip=("value",),
|
|
67
|
+
selectors={"schema": "--schema-id"},
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@config.command()
|
|
73
|
+
@click.argument("key")
|
|
74
|
+
@input_options
|
|
75
|
+
@common
|
|
76
|
+
@click.pass_obj
|
|
77
|
+
def update(inv: Invocation, key, schema, input_file, input_json):
|
|
78
|
+
"""更新 value 的提交字段;嵌套值按 owner 合同处理。"""
|
|
79
|
+
if schema:
|
|
80
|
+
existing = inv.send("GET", "/configs/" + segment(key))
|
|
81
|
+
return inv.show(
|
|
82
|
+
partial(
|
|
83
|
+
inv.send("GET", "/config-schemas/" + segment(existing["schema"]))["input_schema"]
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
inv.show(inv.send("PATCH", "/configs/" + segment(key), body=load_input(input_file, input_json)))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@config.command()
|
|
90
|
+
@click.argument("key")
|
|
91
|
+
@common
|
|
92
|
+
@click.pass_obj
|
|
93
|
+
def delete(inv: Invocation, key):
|
|
94
|
+
"""删除保存的配置值。"""
|
|
95
|
+
inv.show(inv.send("DELETE", "/configs/" + segment(key)), no_content=True)
|