axi-cli 0.0.4__tar.gz → 0.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {axi_cli-0.0.4 → axi_cli-0.0.5}/CHANGELOG.md +10 -0
  2. {axi_cli-0.0.4 → axi_cli-0.0.5}/PKG-INFO +1 -1
  3. {axi_cli-0.0.4 → axi_cli-0.0.5}/pyproject.toml +1 -1
  4. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/cli.py +1 -1
  5. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/providers/mcp.py +1 -60
  6. axi_cli-0.0.5/src/axi/providers/native.py +194 -0
  7. axi_cli-0.0.4/src/axi/providers/native.py +0 -66
  8. {axi_cli-0.0.4 → axi_cli-0.0.5}/.gitignore +0 -0
  9. {axi_cli-0.0.4 → axi_cli-0.0.5}/README.md +0 -0
  10. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/__init__.py +0 -0
  11. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/config.py +0 -0
  12. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/daemon/client.py +0 -0
  13. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/daemon/protocol.py +0 -0
  14. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/daemon/server.py +0 -0
  15. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/executor.py +0 -0
  16. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/models.py +0 -0
  17. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/providers/__init__.py +0 -0
  18. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/registry.py +0 -0
  19. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/search/__init__.py +0 -0
  20. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/search/bm25.py +0 -0
  21. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/search/cache.py +0 -0
  22. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/search/embedding.py +0 -0
  23. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/search/hybrid.py +0 -0
  24. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/search/regex.py +0 -0
  25. {axi_cli-0.0.4 → axi_cli-0.0.5}/src/axi/search/tokenize.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.5] - 2026-04-23
4
+
5
+ ### Added
6
+ - Python entry_points 自动发现原生工具:扩展包在 `pyproject.toml` 里声明 `[project.entry-points."axi.native_tools"]` 即可被 axi 自动发现,无需 `axi.json` 登记
7
+ - `docs/configuration.md` 补充 entry_points 机制说明与合并规则(去重、同名 server collision、加载失败降级)
8
+
9
+ ### Changed
10
+ - `load_native_tool_modules` 从 `providers/mcp.py` 搬到 `providers/native.py`,职责回归原生工具 Provider
11
+ - 模块加载失败时不再占位,避免对后续合法包误报 server 名 collision
12
+
3
13
  ## [0.0.4] - 2026-04-20
4
14
 
5
15
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axi-cli
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Agent eXecution Interface - unified tool layer for AI Agents
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: bm25s>=0.3.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "axi-cli"
3
- version = "0.0.4"
3
+ version = "0.0.5"
4
4
  description = "Agent eXecution Interface - unified tool layer for AI Agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -34,7 +34,7 @@ _executor = Executor(_registry)
34
34
  @app.callback(invoke_without_command=True)
35
35
  def main_callback(ctx: typer.Context) -> None:
36
36
  """CLI 启动时加载 axi.json 中配置的原生工具模块。"""
37
- from axi.providers.mcp import load_native_tool_modules
37
+ from axi.providers.native import load_native_tool_modules
38
38
 
39
39
  load_native_tool_modules()
40
40
  if ctx.invoked_subcommand is None:
@@ -1,13 +1,11 @@
1
1
  """MCP Provider:读取 axi.json,连接 MCP server,注册工具。"""
2
2
 
3
3
  import asyncio
4
- import importlib.util
5
4
  import json
6
5
  import logging
7
6
  import os
8
7
  import threading
9
8
  from contextlib import AsyncExitStack
10
- from pathlib import Path
11
9
  from typing import Any, Self
12
10
 
13
11
  from mcp import ClientSession, StdioServerParameters
@@ -15,11 +13,7 @@ from mcp.client.stdio import stdio_client
15
13
  from mcp.client.streamable_http import streamable_http_client
16
14
  from pydantic import Field, model_validator
17
15
 
18
- from axi.config import (
19
- MCPServerConfig as MCPServerBaseConfig,
20
- NativeToolEntry,
21
- app_config,
22
- )
16
+ from axi.config import MCPServerConfig as MCPServerBaseConfig, app_config
23
17
  from axi.models import RunResult, ToolMeta, ToolSource
24
18
 
25
19
  logger = logging.getLogger(__name__)
@@ -220,59 +214,6 @@ def run_async(coro: Any) -> Any:
220
214
  return future.result()
221
215
 
222
216
 
223
- def _import_from_file(file_path: str) -> None:
224
- """通过文件路径 import 模块,触发 @tool 注册。"""
225
- path = Path(file_path).resolve()
226
- if not path.exists():
227
- raise FileNotFoundError(f"File not found: {file_path}")
228
- module_name = path.stem
229
- spec = importlib.util.spec_from_file_location(module_name, path)
230
- if spec is None or spec.loader is None:
231
- raise ImportError(f"Cannot load module from: {file_path}")
232
- module = importlib.util.module_from_spec(spec)
233
- spec.loader.exec_module(module)
234
-
235
-
236
- def _resolve_server_name(entry: NativeToolEntry) -> str:
237
- """推导 native tool 的 server 名称。"""
238
- if entry.name is not None:
239
- return entry.name
240
- if entry.module.endswith(".py"):
241
- return Path(entry.module).stem
242
- return entry.module.rsplit(".", 1)[-1]
243
-
244
-
245
- def load_native_tool_modules() -> None:
246
- """从全局配置的 nativeTools 列表中加载模块,触发 @tool 注册。"""
247
- entries = app_config.native_tools
248
- if not entries:
249
- return
250
-
251
- from axi.cli import get_registry
252
-
253
- registry = get_registry()
254
-
255
- for entry in entries:
256
- try:
257
- server_name = _resolve_server_name(entry)
258
-
259
- # 记录导入前已有的工具
260
- before = set(registry.list_names())
261
-
262
- if entry.module.endswith(".py"):
263
- _import_from_file(entry.module)
264
- else:
265
- importlib.import_module(entry.module)
266
-
267
- # 为新增的工具设置 server
268
- new_names = [k for k in registry.list_names() if k not in before]
269
- for name in new_names:
270
- registry.set_server(name, server_name)
271
-
272
- except Exception:
273
- logger.exception("Failed to load native tool '%s'", entry)
274
-
275
-
276
217
  def load_mcp_tools_sync() -> tuple[MCPProvider, list[ToolMeta]]:
277
218
  """同步包装:加载配置并连接所有 MCP server。"""
278
219
  provider = MCPProvider()
@@ -0,0 +1,194 @@
1
+ """原生工具 Provider:通过 @tool 装饰器注册 Python 函数,
2
+ 并负责从 axi.json 的 nativeTools 与 Python entry_points 加载工具模块。"""
3
+
4
+ import importlib
5
+ import importlib.metadata
6
+ import importlib.util
7
+ import inspect
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Any, Callable, get_type_hints
11
+
12
+ from pydantic import create_model
13
+
14
+ from axi.config import NativeToolEntry, app_config
15
+ from axi.models import ToolMeta, ToolSource
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # 全局注册表,存储原生工具的函数引用
20
+ _native_functions: dict[str, Callable] = {}
21
+
22
+
23
+ # ── 工具注册(@tool 装饰器的执行层)────────────────────────────
24
+
25
+
26
+ def _extract_input_schema(func: Callable) -> dict[str, Any]:
27
+ """从函数签名和 type hints 提取 JSON Schema。
28
+
29
+ 利用 Pydantic 的 create_model 动态构建模型,
30
+ 自动支持 Literal、Optional、list[T]、嵌套 BaseModel、Annotated[..., Field()] 等。
31
+ """
32
+ hints = get_type_hints(func, include_extras=True)
33
+ sig = inspect.signature(func)
34
+ fields: dict[str, Any] = {}
35
+
36
+ for name, param in sig.parameters.items():
37
+ hint = hints.get(name, Any)
38
+ if param.default is inspect.Parameter.empty:
39
+ fields[name] = (hint, ...)
40
+ else:
41
+ fields[name] = (hint, param.default)
42
+
43
+ model = create_model(func.__name__, **fields)
44
+ schema = model.model_json_schema()
45
+
46
+ # 移除 Pydantic 自动添加的 title 字段,保持输出紧凑
47
+ schema.pop("title", None)
48
+ for prop in schema.get("properties", {}).values():
49
+ prop.pop("title", None)
50
+
51
+ return schema
52
+
53
+
54
+ def register_tool(
55
+ func: Callable,
56
+ name: str | None = None,
57
+ description: str | None = None,
58
+ output_example: Any | None = None,
59
+ ) -> ToolMeta:
60
+ """注册一个原生 Python 函数为 axi 工具。"""
61
+ tool_name = name or func.__name__
62
+ tool_desc = description or func.__doc__ or ""
63
+
64
+ meta = ToolMeta(
65
+ name=tool_name,
66
+ description=tool_desc,
67
+ input_schema=_extract_input_schema(func),
68
+ output_example=output_example,
69
+ source=ToolSource.NATIVE,
70
+ )
71
+
72
+ _native_functions[tool_name] = func
73
+ return meta
74
+
75
+
76
+ def get_native_function(name: str) -> Callable | None:
77
+ """获取已注册的原生函数。"""
78
+ return _native_functions.get(name)
79
+
80
+
81
+ # ── 模块发现与加载 ─────────────────────────────────────────────
82
+
83
+ NATIVE_TOOLS_ENTRY_POINT_GROUP = "axi.native_tools"
84
+
85
+
86
+ def _import_from_file(file_path: str) -> None:
87
+ """通过文件路径 import 模块,触发 @tool 注册。"""
88
+ path = Path(file_path).resolve()
89
+ if not path.exists():
90
+ raise FileNotFoundError(f"File not found: {file_path}")
91
+ module_name = path.stem
92
+ spec = importlib.util.spec_from_file_location(module_name, path)
93
+ if spec is None or spec.loader is None:
94
+ raise ImportError(f"Cannot load module from: {file_path}")
95
+ module = importlib.util.module_from_spec(spec)
96
+ spec.loader.exec_module(module)
97
+
98
+
99
+ def _resolve_server_name(entry: NativeToolEntry) -> str:
100
+ """推导 native tool 的 server 名称。"""
101
+ if entry.name is not None:
102
+ return entry.name
103
+ if entry.module.endswith(".py"):
104
+ return Path(entry.module).stem
105
+ return entry.module.rsplit(".", 1)[-1]
106
+
107
+
108
+ def _load_native_entry(
109
+ registry: Any,
110
+ module: str,
111
+ server_name: str,
112
+ loaded: dict[str, str],
113
+ source: str,
114
+ ) -> None:
115
+ """加载单个原生工具模块并把新增工具归到 server_name 下。
116
+
117
+ ``loaded`` 以 "模块路径 → server 名" 映射**仅记录成功加载**的模块:
118
+ - 模块路径重复 → 跳过(axi.json 与 entry_points 声明同一个模块是预期情况)
119
+ - 导入抛异常 → logger.exception 后 return,不占坑也不让一个坏包搞崩 CLI
120
+ - 加载成功但 server 名被别的模块占了 → logger.warning,继续挂到该 server 下
121
+
122
+ 失败的模块不写入 ``loaded`` 可以避免对后续合法包误报 collision。
123
+ """
124
+ if module in loaded:
125
+ return
126
+ try:
127
+ before = set(registry.list_names())
128
+ if module.endswith(".py"):
129
+ _import_from_file(module)
130
+ else:
131
+ importlib.import_module(module)
132
+ new_names = [n for n in registry.list_names() if n not in before]
133
+ except Exception:
134
+ logger.exception(
135
+ "Failed to load native tool module '%s' (from %s)", module, source
136
+ )
137
+ return
138
+ if server_name in loaded.values():
139
+ other = next(m for m, s in loaded.items() if s == server_name)
140
+ logger.warning(
141
+ "Server name collision: '%s' already claimed by module '%s'; "
142
+ "tools from '%s' (%s) will be merged under the same server",
143
+ server_name,
144
+ other,
145
+ module,
146
+ source,
147
+ )
148
+ loaded[module] = server_name
149
+ for name in new_names:
150
+ registry.set_server(name, server_name)
151
+
152
+
153
+ def load_native_tool_modules() -> None:
154
+ """从 axi.json 的 nativeTools 与 Python entry_points 加载原生工具模块。
155
+
156
+ 合并规则:
157
+ - axi.json 先遍历,entry_points 后遍历;按**模块路径**去重(同一个模块只加载一次)
158
+ - 因此重复声明时 axi.json 的 server 名赢
159
+ - 不同模块声明同一个 server 名时仅 log warning,两边工具都会注册到该 server
160
+ """
161
+ from axi.cli import get_registry
162
+
163
+ registry = get_registry()
164
+ loaded: dict[str, str] = {}
165
+
166
+ # 来源 1:axi.json 的 nativeTools(显式配置,优先级更高)
167
+ for entry in app_config.native_tools:
168
+ _load_native_entry(
169
+ registry,
170
+ entry.module,
171
+ _resolve_server_name(entry),
172
+ loaded,
173
+ source="axi.json",
174
+ )
175
+
176
+ # 来源 2:Python entry_points group="axi.native_tools"
177
+ # 任何 pip 安装的包在 pyproject.toml 里声明就会被自动发现,
178
+ # 无需在 axi.json 里重复登记。
179
+ try:
180
+ eps = importlib.metadata.entry_points(group=NATIVE_TOOLS_ENTRY_POINT_GROUP)
181
+ except Exception:
182
+ logger.exception(
183
+ "Failed to query entry_points for %s", NATIVE_TOOLS_ENTRY_POINT_GROUP
184
+ )
185
+ eps = []
186
+ for ep in eps:
187
+ module_path = ep.value.split(":", 1)[0].strip()
188
+ _load_native_entry(
189
+ registry,
190
+ module_path,
191
+ ep.name,
192
+ loaded,
193
+ source=f"entry_points:{ep.name}",
194
+ )
@@ -1,66 +0,0 @@
1
- """原生工具注册 Provider:通过 @tool 装饰器注册 Python 函数。"""
2
-
3
- import inspect
4
- from typing import Any, Callable, get_type_hints
5
-
6
- from pydantic import create_model
7
-
8
- from axi.models import ToolMeta, ToolSource
9
-
10
- # 全局注册表,存储原生工具的函数引用
11
- _native_functions: dict[str, Callable] = {}
12
-
13
-
14
- def _extract_input_schema(func: Callable) -> dict[str, Any]:
15
- """从函数签名和 type hints 提取 JSON Schema。
16
-
17
- 利用 Pydantic 的 create_model 动态构建模型,
18
- 自动支持 Literal、Optional、list[T]、嵌套 BaseModel、Annotated[..., Field()] 等。
19
- """
20
- hints = get_type_hints(func, include_extras=True)
21
- sig = inspect.signature(func)
22
- fields: dict[str, Any] = {}
23
-
24
- for name, param in sig.parameters.items():
25
- hint = hints.get(name, Any)
26
- if param.default is inspect.Parameter.empty:
27
- fields[name] = (hint, ...)
28
- else:
29
- fields[name] = (hint, param.default)
30
-
31
- model = create_model(func.__name__, **fields)
32
- schema = model.model_json_schema()
33
-
34
- # 移除 Pydantic 自动添加的 title 字段,保持输出紧凑
35
- schema.pop("title", None)
36
- for prop in schema.get("properties", {}).values():
37
- prop.pop("title", None)
38
-
39
- return schema
40
-
41
-
42
- def register_tool(
43
- func: Callable,
44
- name: str | None = None,
45
- description: str | None = None,
46
- output_example: Any | None = None,
47
- ) -> ToolMeta:
48
- """注册一个原生 Python 函数为 axi 工具。"""
49
- tool_name = name or func.__name__
50
- tool_desc = description or func.__doc__ or ""
51
-
52
- meta = ToolMeta(
53
- name=tool_name,
54
- description=tool_desc,
55
- input_schema=_extract_input_schema(func),
56
- output_example=output_example,
57
- source=ToolSource.NATIVE,
58
- )
59
-
60
- _native_functions[tool_name] = func
61
- return meta
62
-
63
-
64
- def get_native_function(name: str) -> Callable | None:
65
- """获取已注册的原生函数。"""
66
- return _native_functions.get(name)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes