ghoshell-moss 0.1.0a0__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.
- ghoshell_moss/README.md +7 -0
- ghoshell_moss/__init__.py +15 -0
- ghoshell_moss/channels/README.md +5 -0
- ghoshell_moss/channels/__init__.py +1 -0
- ghoshell_moss/channels/mac_channel.py +99 -0
- ghoshell_moss/cli/README.md +5 -0
- ghoshell_moss/cli/__init__.py +1 -0
- ghoshell_moss/compatible/README.md +7 -0
- ghoshell_moss/compatible/__init__.py +1 -0
- ghoshell_moss/compatible/mcp_channel/__init__.py +3 -0
- ghoshell_moss/compatible/mcp_channel/mcp_channel.py +468 -0
- ghoshell_moss/compatible/mcp_channel/types.py +17 -0
- ghoshell_moss/compatible/mcp_channel/utils.py +25 -0
- ghoshell_moss/core/README.md +6 -0
- ghoshell_moss/core/__init__.py +13 -0
- ghoshell_moss/core/concepts/README.md +20 -0
- ghoshell_moss/core/concepts/__init__.py +118 -0
- ghoshell_moss/core/concepts/channel.py +813 -0
- ghoshell_moss/core/concepts/command.py +981 -0
- ghoshell_moss/core/concepts/errors.py +73 -0
- ghoshell_moss/core/concepts/interpreter.py +365 -0
- ghoshell_moss/core/concepts/shell.py +332 -0
- ghoshell_moss/core/concepts/speech.py +548 -0
- ghoshell_moss/core/concepts/states.py +216 -0
- ghoshell_moss/core/concepts/topics.py +186 -0
- ghoshell_moss/core/ctml/README.md +9 -0
- ghoshell_moss/core/ctml/__init__.py +5 -0
- ghoshell_moss/core/ctml/elements.py +480 -0
- ghoshell_moss/core/ctml/interpreter.py +558 -0
- ghoshell_moss/core/ctml/prompt.py +9 -0
- ghoshell_moss/core/ctml/prompt_v1.md +174 -0
- ghoshell_moss/core/ctml/token_parser.py +427 -0
- ghoshell_moss/core/duplex/README.md +9 -0
- ghoshell_moss/core/duplex/__init__.py +54 -0
- ghoshell_moss/core/duplex/connection.py +57 -0
- ghoshell_moss/core/duplex/protocol.py +244 -0
- ghoshell_moss/core/duplex/provider.py +650 -0
- ghoshell_moss/core/duplex/proxy.py +1046 -0
- ghoshell_moss/core/duplex/thread_channel.py +180 -0
- ghoshell_moss/core/helpers/README.md +3 -0
- ghoshell_moss/core/helpers/__init__.py +1 -0
- ghoshell_moss/core/helpers/asyncio_utils.py +215 -0
- ghoshell_moss/core/helpers/func.py +166 -0
- ghoshell_moss/core/helpers/result.py +104 -0
- ghoshell_moss/core/helpers/stream.py +141 -0
- ghoshell_moss/core/helpers/token_filters.py +48 -0
- ghoshell_moss/core/py_channel.py +599 -0
- ghoshell_moss/core/shell/README.md +3 -0
- ghoshell_moss/core/shell/__init__.py +2 -0
- ghoshell_moss/core/shell/channel_runtime.py +632 -0
- ghoshell_moss/core/shell/main_channel.py +36 -0
- ghoshell_moss/core/shell/shell_impl.py +329 -0
- ghoshell_moss/core/shell/shell_runtime.py +431 -0
- ghoshell_moss/message/README.md +13 -0
- ghoshell_moss/message/__init__.py +4 -0
- ghoshell_moss/message/abcd.py +592 -0
- ghoshell_moss/message/adapters/__init__.py +1 -0
- ghoshell_moss/message/adapters/openai_adapter.py +101 -0
- ghoshell_moss/message/addtions.py +17 -0
- ghoshell_moss/message/contents.py +149 -0
- ghoshell_moss/message/deltas.py +32 -0
- ghoshell_moss/message/utils.py +15 -0
- ghoshell_moss/speech/README.md +9 -0
- ghoshell_moss/speech/__init__.py +23 -0
- ghoshell_moss/speech/mock.py +124 -0
- ghoshell_moss/speech/player/__init__.py +1 -0
- ghoshell_moss/speech/player/base_player.py +250 -0
- ghoshell_moss/speech/player/pulseaudio_player.py +74 -0
- ghoshell_moss/speech/player/pyaudio_player.py +71 -0
- ghoshell_moss/speech/stream_tts_speech.py +183 -0
- ghoshell_moss/speech/volcengine_tts/__init__.py +9 -0
- ghoshell_moss/speech/volcengine_tts/protocol.py +552 -0
- ghoshell_moss/speech/volcengine_tts/tts.py +728 -0
- ghoshell_moss/transports/README.md +19 -0
- ghoshell_moss/transports/__init__.py +1 -0
- ghoshell_moss/transports/redis_channel/__init__.py +7 -0
- ghoshell_moss/transports/redis_channel/redis_channel.py +228 -0
- ghoshell_moss/transports/ws_channel/__init__.py +7 -0
- ghoshell_moss/transports/ws_channel/ws_channel.py +209 -0
- ghoshell_moss/transports/zmq_channel/__init__.py +2 -0
- ghoshell_moss/transports/zmq_channel/zmq_channel.py +403 -0
- ghoshell_moss/transports/zmq_channel/zmq_hub.py +340 -0
- ghoshell_moss-0.1.0a0.dist-info/METADATA +159 -0
- ghoshell_moss-0.1.0a0.dist-info/RECORD +125 -0
- ghoshell_moss-0.1.0a0.dist-info/WHEEL +4 -0
- ghoshell_moss-0.1.0a0.dist-info/entry_points.txt +4 -0
- ghoshell_moss-0.1.0a0.dist-info/licenses/LICENSE +200 -0
- ghoshell_moss-0.1.0a0.dist-info/licenses/NOTICE +18 -0
- ghoshell_moss_contrib/README.md +4 -0
- ghoshell_moss_contrib/__init__.py +0 -0
- ghoshell_moss_contrib/agent/README.md +5 -0
- ghoshell_moss_contrib/agent/__init__.py +23 -0
- ghoshell_moss_contrib/agent/chat/__init__.py +1 -0
- ghoshell_moss_contrib/agent/chat/base.py +42 -0
- ghoshell_moss_contrib/agent/chat/console.py +202 -0
- ghoshell_moss_contrib/agent/chat/queue.py +191 -0
- ghoshell_moss_contrib/agent/depends.py +12 -0
- ghoshell_moss_contrib/agent/output.py +114 -0
- ghoshell_moss_contrib/agent/simple_agent.py +367 -0
- ghoshell_moss_contrib/channels/__init__.py +0 -0
- ghoshell_moss_contrib/channels/docs_reader.py +9 -0
- ghoshell_moss_contrib/channels/mermaid_draw.py +79 -0
- ghoshell_moss_contrib/channels/mpv_video.py +195 -0
- ghoshell_moss_contrib/channels/opencv_vision.py +307 -0
- ghoshell_moss_contrib/channels/project_manager.py +9 -0
- ghoshell_moss_contrib/channels/screen_capture.py +231 -0
- ghoshell_moss_contrib/channels/slide_studio.py +302 -0
- ghoshell_moss_contrib/channels/terminal.py +9 -0
- ghoshell_moss_contrib/channels/web_bookmark.py +75 -0
- ghoshell_moss_contrib/example_ws.py +148 -0
- ghoshell_moss_contrib/gui/__init__.py +0 -0
- ghoshell_moss_contrib/gui/image_viewer.py +86 -0
- ghoshell_moss_contrib/gui/slide_studio_creator.py +260 -0
- ghoshell_moss_contrib/prototypes/README.md +4 -0
- ghoshell_moss_contrib/prototypes/__init__.py +1 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/README.md +11 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/__init__.py +1 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/abcd.py +578 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/joint_parsers.py +26 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/main_channel.py +237 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/manager.py +84 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/mocks.py +51 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/models.py +407 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/ros2_controller.py +203 -0
- ghoshell_moss_contrib/prototypes/ros2_robot/ros2_node.py +209 -0
ghoshell_moss/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# 目录介绍
|
|
2
|
+
|
|
3
|
+
- core: ghoshell moss 的核心功能模块
|
|
4
|
+
- message: 兼容性的模型消息协议. 暂时放到 ghoshell-moss 库, 未来可能迁出
|
|
5
|
+
- transports: 通过 provider -> proxy 范式, 跨进程的构建 channel 之间的双工通讯.
|
|
6
|
+
- compatible: 兼容性模块, 用来兼容行业生态. 比如 claude mcp 和 claude skills.
|
|
7
|
+
- channels: ghoshell-moss 库认为需要开箱自带的 channel 实现.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ghoshell_container import (
|
|
2
|
+
Container,
|
|
3
|
+
IoCContainer,
|
|
4
|
+
get_container,
|
|
5
|
+
set_container,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from ghoshell_moss.core import *
|
|
9
|
+
from ghoshell_moss.message import *
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
Ghoshell MOSS 库的 facade, 用来存放最常用的类库引用.
|
|
13
|
+
|
|
14
|
+
考虑只对外暴露最基础的常用函数.
|
|
15
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from ghoshell_moss.core import PyChannel
|
|
5
|
+
|
|
6
|
+
__all__ = ["new_mac_control_channel"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JXAError(Exception):
|
|
10
|
+
"""JXA 执行错误"""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def run(*, timeout: Optional[float] = 30.0, text__: str = "") -> Any:
|
|
16
|
+
"""
|
|
17
|
+
在当前 MAC 上执行一个 JXA 函数, 子进程阻塞到执行完毕.
|
|
18
|
+
如果你需要与 mac 应用做交互, 又没有别的手段时, 可以用这个办法.
|
|
19
|
+
|
|
20
|
+
:param timeout: 过期时间.
|
|
21
|
+
:param text__: jxa 脚本的源码.
|
|
22
|
+
|
|
23
|
+
CTML 使用举例:
|
|
24
|
+
<chan_name:run><![CDATA[
|
|
25
|
+
(function() {
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
// 1. 使用 Bundle Identifier 获取日历应用对象
|
|
29
|
+
var Calendar = Application('com.apple.iCal');
|
|
30
|
+
|
|
31
|
+
// 2. 检查是否正在运行
|
|
32
|
+
var wasRunning = Calendar.running();
|
|
33
|
+
|
|
34
|
+
// 3. 尝试激活 (如果没有运行,这会启动它)
|
|
35
|
+
Calendar.activate();
|
|
36
|
+
|
|
37
|
+
// 4. 等待一小会儿,让系统处理激活请求
|
|
38
|
+
delay(0.5); // JXA 中的 delay 函数,单位秒
|
|
39
|
+
|
|
40
|
+
// 5. 再次检查运行状态和窗口状态
|
|
41
|
+
var isRunning = Calendar.running();
|
|
42
|
+
var windowCount = Calendar.windows ? Calendar.windows.length : 0;
|
|
43
|
+
|
|
44
|
+
// 6. 返回结构化的调试信息
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
action: 'activate_calendar',
|
|
48
|
+
bundleId: 'com.apple.iCal',
|
|
49
|
+
wasRunning: wasRunning,
|
|
50
|
+
isRunning: isRunning,
|
|
51
|
+
windowCount: windowCount,
|
|
52
|
+
timestamp: Date.now()
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
]]></chan_name:run>
|
|
56
|
+
|
|
57
|
+
:return: 返回操作结果. 你必须等操作结果到手后, 才能知道它运行的效果如何.
|
|
58
|
+
"""
|
|
59
|
+
cmd = ["osascript", "-l", "JavaScript", "-"]
|
|
60
|
+
|
|
61
|
+
process = None
|
|
62
|
+
try:
|
|
63
|
+
process = await asyncio.create_subprocess_exec(
|
|
64
|
+
*cmd, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
stdout, stderr = await asyncio.wait_for(process.communicate(input=text__.encode("utf-8")), timeout=timeout)
|
|
68
|
+
|
|
69
|
+
if process.returncode != 0:
|
|
70
|
+
error_msg = stderr.decode("utf-8", errors="replace").strip()
|
|
71
|
+
raise JXAError(f"JXA 执行失败 (code: {process.returncode}): {error_msg}")
|
|
72
|
+
|
|
73
|
+
output = stdout.decode("utf-8", errors="replace").strip()
|
|
74
|
+
|
|
75
|
+
# 尝试解析 JSON
|
|
76
|
+
return output
|
|
77
|
+
|
|
78
|
+
except asyncio.TimeoutError:
|
|
79
|
+
if process and process.returncode is None:
|
|
80
|
+
process.kill()
|
|
81
|
+
await process.wait()
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def new_mac_control_channel(
|
|
86
|
+
name: str = "mac_control",
|
|
87
|
+
description: str = "使用 jxa 语法来操作当前所在 mac",
|
|
88
|
+
) -> PyChannel:
|
|
89
|
+
"""
|
|
90
|
+
创建一个控制 mac 的 channel.
|
|
91
|
+
"""
|
|
92
|
+
mac_jxa_channel = PyChannel(
|
|
93
|
+
name=name,
|
|
94
|
+
description=description,
|
|
95
|
+
block=True,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
mac_jxa_channel.build.command()(run)
|
|
99
|
+
return mac_jxa_channel
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from collections.abc import Callable, Coroutine
|
|
4
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
5
|
+
|
|
6
|
+
from ghoshell_moss import CommandError, CommandErrorCode
|
|
7
|
+
from ghoshell_moss.compatible.mcp_channel.utils import mcp_call_tool_result_to_message
|
|
8
|
+
from ghoshell_moss.core.concepts.states import MemoryStateStore, StateStore
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import mcp
|
|
12
|
+
from mcp import types
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise ImportError("Could not import mcp. Please install ghoshell-moss[mcp].")
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
from ghoshell_common.helpers import uuid
|
|
19
|
+
from ghoshell_container import Container, IoCContainer
|
|
20
|
+
|
|
21
|
+
from ghoshell_moss.core.concepts.channel import Builder, Channel, ChannelBroker, ChannelMeta
|
|
22
|
+
from ghoshell_moss.core.concepts.command import (
|
|
23
|
+
Command,
|
|
24
|
+
CommandDeltaType,
|
|
25
|
+
CommandMeta,
|
|
26
|
+
CommandTask,
|
|
27
|
+
CommandWrapper,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
R = TypeVar("R") # 泛型结果类型
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MCPChannelBroker(ChannelBroker, Generic[R]):
|
|
34
|
+
"""MCPChannel的运行时客户端,负责对接MCP服务"""
|
|
35
|
+
|
|
36
|
+
MCP_CONTAINER_TYPES: list[str] = ["array", "object"]
|
|
37
|
+
|
|
38
|
+
MCP_PY_TYPES_TRANS_TABLE: dict[str, str] = {
|
|
39
|
+
"string": "str",
|
|
40
|
+
"integer": "int",
|
|
41
|
+
"number": "float",
|
|
42
|
+
"boolean": "bool",
|
|
43
|
+
"array": "list",
|
|
44
|
+
"object": "dict",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
COMMAND_DELTA_PARAMTER: str = f"{CommandDeltaType.TEXT.value}:str"
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
name: str,
|
|
53
|
+
mcp_client: mcp.ClientSession,
|
|
54
|
+
container: Optional[IoCContainer] = None,
|
|
55
|
+
):
|
|
56
|
+
self._name = name
|
|
57
|
+
self._mcp_client: Optional[mcp.ClientSession] = mcp_client # MCP客户端实例
|
|
58
|
+
self._commands: dict[str, Command] = {} # 映射后的Mosshell Command
|
|
59
|
+
self._meta: Optional[ChannelMeta] = None # Channel元信息
|
|
60
|
+
self._running = False # 运行状态标记
|
|
61
|
+
self._logger: logging.Logger | None = None
|
|
62
|
+
self._id = uuid()
|
|
63
|
+
self._container = Container(parent=container, name="mcp_channel:" + self._name)
|
|
64
|
+
self._states: Optional[StateStore] = None
|
|
65
|
+
|
|
66
|
+
def children(self) -> dict[str, "Channel"]:
|
|
67
|
+
return {}
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def container(self) -> IoCContainer:
|
|
71
|
+
return self._container
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def id(self) -> str:
|
|
75
|
+
return self._id
|
|
76
|
+
|
|
77
|
+
def name(self) -> str:
|
|
78
|
+
return self._name
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def logger(self) -> logging.Logger:
|
|
82
|
+
if self._logger is None:
|
|
83
|
+
self._logger = self.container.get(logging.Logger) or logging.getLogger("moss")
|
|
84
|
+
return self._logger
|
|
85
|
+
|
|
86
|
+
# --- ChannelBroker 核心方法实现 --- #
|
|
87
|
+
async def start(self) -> None:
|
|
88
|
+
"""启动MCP客户端并同步工具元信息"""
|
|
89
|
+
if self._running:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# 同步远端工具元信息
|
|
93
|
+
try:
|
|
94
|
+
await asyncio.to_thread(self._container.bootstrap)
|
|
95
|
+
initialize_result = await self._mcp_client.initialize() # 初始化MCP连接
|
|
96
|
+
tools = await self._mcp_client.list_tools()
|
|
97
|
+
|
|
98
|
+
# 转换为Mosshell Command和ChannelMeta
|
|
99
|
+
self._meta = self._build_channel_meta(initialize_result, tools)
|
|
100
|
+
self._running = True
|
|
101
|
+
except Exception as e:
|
|
102
|
+
raise RuntimeError(f"MCP tool discovery failed: {str(e)}") from e
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def states(self) -> StateStore:
|
|
106
|
+
if self._states is None:
|
|
107
|
+
_states = self._container.get(StateStore)
|
|
108
|
+
if _states is None:
|
|
109
|
+
_states = MemoryStateStore(self._name)
|
|
110
|
+
self._container.set(StateStore, _states)
|
|
111
|
+
self._states = _states
|
|
112
|
+
return self._states
|
|
113
|
+
|
|
114
|
+
async def close(self) -> None:
|
|
115
|
+
if not self._running:
|
|
116
|
+
return
|
|
117
|
+
await asyncio.to_thread(self._container.shutdown)
|
|
118
|
+
|
|
119
|
+
def is_running(self) -> bool:
|
|
120
|
+
return self._running
|
|
121
|
+
|
|
122
|
+
def meta(self) -> ChannelMeta:
|
|
123
|
+
# todo: 还没有实现动态更新, 主要是更新 command
|
|
124
|
+
if not self.is_running():
|
|
125
|
+
raise RuntimeError(f"Channel client {self._name} is not running")
|
|
126
|
+
return self._meta.model_copy()
|
|
127
|
+
|
|
128
|
+
async def refresh_meta(self) -> None:
|
|
129
|
+
# todo: shall refresh command metas
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
def is_connected(self) -> bool:
|
|
133
|
+
# todo: 检查状态.
|
|
134
|
+
return self.is_running()
|
|
135
|
+
|
|
136
|
+
async def wait_connected(self) -> None:
|
|
137
|
+
# todo: 检查状态.
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
def commands(self, available_only: bool = True) -> dict[str, Command]:
|
|
141
|
+
# todo: 这里每次更新, 和上面好像冲突.
|
|
142
|
+
meta = self.meta()
|
|
143
|
+
result = {}
|
|
144
|
+
for command_meta in meta.commands:
|
|
145
|
+
if not available_only or command_meta.available:
|
|
146
|
+
func = self._get_command_func(command_meta)
|
|
147
|
+
command = CommandWrapper(meta=command_meta, func=func)
|
|
148
|
+
result[command_meta.name] = command
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
def get_command(self, name: str) -> Optional[Command]:
|
|
152
|
+
meta = self.meta()
|
|
153
|
+
for command_meta in meta.commands:
|
|
154
|
+
if command_meta.name == name:
|
|
155
|
+
func = self._get_command_func(command_meta)
|
|
156
|
+
return CommandWrapper(meta=command_meta, func=func)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def _get_command_func(self, meta: CommandMeta) -> Callable[[...], Coroutine[None, None, Any]] | None:
|
|
160
|
+
name = meta.name
|
|
161
|
+
|
|
162
|
+
args_schema_properties = meta.args_schema.get("properties", {})
|
|
163
|
+
required_args_list = meta.args_schema.get("required", [])
|
|
164
|
+
schema_param_count = len(args_schema_properties)
|
|
165
|
+
required_schema_param_count = len(required_args_list)
|
|
166
|
+
|
|
167
|
+
# 回调服务端.
|
|
168
|
+
async def _server_caller_as_command(*args, **kwargs):
|
|
169
|
+
# 调用MCP客户端执行工具
|
|
170
|
+
try:
|
|
171
|
+
if required_schema_param_count > schema_param_count:
|
|
172
|
+
raise CommandError(
|
|
173
|
+
code=CommandErrorCode.INVALID_PARAMETER.value,
|
|
174
|
+
message=(
|
|
175
|
+
"MCP tool: invalid parameter count, required parameter: "
|
|
176
|
+
f"{required_schema_param_count}, schema parameter: {schema_param_count}"
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
param_count = len(args) + len(kwargs)
|
|
181
|
+
final_kwargs = {}
|
|
182
|
+
if schema_param_count == 0: # do nothing
|
|
183
|
+
if not param_count == 0:
|
|
184
|
+
raise CommandError(
|
|
185
|
+
code=CommandErrorCode.INVALID_PARAMETER.value,
|
|
186
|
+
message=f"MCP tool: no parameter, invalid, args={args}, kwargs={kwargs}",
|
|
187
|
+
)
|
|
188
|
+
else: # schema_param_count > 1
|
|
189
|
+
if not (param_count == 1 or required_schema_param_count <= param_count <= schema_param_count):
|
|
190
|
+
raise CommandError(
|
|
191
|
+
code=CommandErrorCode.INVALID_PARAMETER.value,
|
|
192
|
+
message=f"MCP tool: invalid parameters, invalid, args={args}, kwargs={kwargs}",
|
|
193
|
+
)
|
|
194
|
+
if param_count == 1:
|
|
195
|
+
if len(args) == 1:
|
|
196
|
+
if required_schema_param_count == 1:
|
|
197
|
+
if type(args[0]) is not str:
|
|
198
|
+
[param_name, param_info], *_ = args_schema_properties.items()
|
|
199
|
+
if param_type := param_info.get("type", None):
|
|
200
|
+
if type(args[0]).__name__ == self._mcp_type_2_py_type(param_type):
|
|
201
|
+
final_kwargs[param_name] = args[0]
|
|
202
|
+
|
|
203
|
+
if not len(final_kwargs):
|
|
204
|
+
try:
|
|
205
|
+
final_kwargs = json.loads(args[0])
|
|
206
|
+
except TypeError as e:
|
|
207
|
+
raise CommandError(
|
|
208
|
+
code=CommandErrorCode.VALUE_ERROR.value,
|
|
209
|
+
message=f'MCP tool: invalid "text__" type, {str(e)}',
|
|
210
|
+
)
|
|
211
|
+
except json.JSONDecodeError as e:
|
|
212
|
+
raise CommandError(
|
|
213
|
+
code=CommandErrorCode.VALUE_ERROR.value,
|
|
214
|
+
message=(
|
|
215
|
+
f"MCP tool: invalid `text__` parameter format, INVALID JSON schema, {e}"
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
if "text__" in kwargs:
|
|
220
|
+
final_kwargs = json.loads(kwargs["text__"])
|
|
221
|
+
elif required_schema_param_count == 1:
|
|
222
|
+
param_name = required_args_list[0]
|
|
223
|
+
if param_name not in kwargs:
|
|
224
|
+
raise CommandError(
|
|
225
|
+
code=CommandErrorCode.INVALID_PARAMETER.value,
|
|
226
|
+
message=f'MCP tool: unknown parameter "{param_name}" parameter format.',
|
|
227
|
+
)
|
|
228
|
+
final_kwargs.update(kwargs)
|
|
229
|
+
else:
|
|
230
|
+
raise CommandError(
|
|
231
|
+
code=CommandErrorCode.INVALID_PARAMETER.value,
|
|
232
|
+
message=f'MCP tool: missing "text__" parameters, kwargs={kwargs}',
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
for arg_name, arg in zip(args_schema_properties.keys(), args):
|
|
236
|
+
final_kwargs[arg_name] = arg
|
|
237
|
+
final_kwargs.update(kwargs)
|
|
238
|
+
|
|
239
|
+
mcp_result = await self._mcp_client.call_tool(
|
|
240
|
+
name=meta.name,
|
|
241
|
+
arguments=final_kwargs,
|
|
242
|
+
)
|
|
243
|
+
# convert to moss Message
|
|
244
|
+
return mcp_call_tool_result_to_message(mcp_result, name=self.name())
|
|
245
|
+
except mcp.McpError as e:
|
|
246
|
+
raise CommandError(code=CommandErrorCode.FAILED.value, message=f"MCP call failed: {str(e)}") from e
|
|
247
|
+
except Exception as e:
|
|
248
|
+
raise CommandError(
|
|
249
|
+
code=CommandErrorCode.FAILED.value, message=f"MCP tool execution failed: {str(e)}"
|
|
250
|
+
) from e
|
|
251
|
+
|
|
252
|
+
return _server_caller_as_command
|
|
253
|
+
|
|
254
|
+
async def execute(self, task: CommandTask[R]) -> R:
|
|
255
|
+
if not self.is_running():
|
|
256
|
+
raise RuntimeError("MCPChannel is not running")
|
|
257
|
+
func = self._get_command_func(task.meta)
|
|
258
|
+
if func is None:
|
|
259
|
+
raise LookupError(f"Channel {self._name} can find command {task.meta.name}")
|
|
260
|
+
return await func(*task.args, **task.kwargs)
|
|
261
|
+
|
|
262
|
+
# --- 工具转Command的核心逻辑 --- #
|
|
263
|
+
|
|
264
|
+
def _convert_tools_to_command_metas(self, tools: list[types.Tool]) -> list[CommandMeta]:
|
|
265
|
+
"""将MCP工具转换为Mosshell的CommandMeta"""
|
|
266
|
+
metas = []
|
|
267
|
+
for tool in tools:
|
|
268
|
+
tool_name = tool.name
|
|
269
|
+
|
|
270
|
+
# 生成符合Code as Prompt的interface(模型可见的函数签名)
|
|
271
|
+
interface, description = self._generate_code_as_prompt(tool)
|
|
272
|
+
|
|
273
|
+
metas.append(
|
|
274
|
+
CommandMeta(
|
|
275
|
+
name=tool_name,
|
|
276
|
+
description=description or "",
|
|
277
|
+
chan=self._name,
|
|
278
|
+
interface=interface,
|
|
279
|
+
available=True,
|
|
280
|
+
args_schema=tool.inputSchema,
|
|
281
|
+
delta_arg=CommandDeltaType.TEXT,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
return metas
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _mcp_type_2_py_type(param_info_type: str) -> str:
|
|
288
|
+
param_type = MCPChannelBroker.MCP_PY_TYPES_TRANS_TABLE.get(param_info_type.lower(), "Any")
|
|
289
|
+
return param_type
|
|
290
|
+
|
|
291
|
+
def _parse_schema(self, schema: dict) -> tuple[list, list]:
|
|
292
|
+
required_params = []
|
|
293
|
+
optional_params = []
|
|
294
|
+
required_param_docs = []
|
|
295
|
+
optional_param_docs = []
|
|
296
|
+
|
|
297
|
+
for param_name, param_info in schema.get("properties", {}).items():
|
|
298
|
+
# 确定参数类型
|
|
299
|
+
param_type = self._mcp_type_2_py_type(param_info.get("type", ""))
|
|
300
|
+
|
|
301
|
+
# 确定默认值
|
|
302
|
+
param_str = f"{param_name}: {param_type}"
|
|
303
|
+
if param_name not in schema.get("required", []):
|
|
304
|
+
default_value = "None" if param_type != "bool" else "False"
|
|
305
|
+
param_str += f"={default_value}"
|
|
306
|
+
|
|
307
|
+
# 根据是否必需参数,添加到不同的列表
|
|
308
|
+
if param_name in schema.get("required", []):
|
|
309
|
+
required_params.append(param_str)
|
|
310
|
+
# 添加参数文档
|
|
311
|
+
if "description" in param_info:
|
|
312
|
+
required_param_docs.append(f" :param {param_name}: {param_info['description']}")
|
|
313
|
+
else:
|
|
314
|
+
optional_params.append(param_str)
|
|
315
|
+
# 添加参数文档
|
|
316
|
+
if "description" in param_info:
|
|
317
|
+
optional_param_docs.append(f" :param {param_name}: {param_info['description']}")
|
|
318
|
+
|
|
319
|
+
return required_params + optional_params, required_param_docs + optional_param_docs
|
|
320
|
+
|
|
321
|
+
def _parse_schema_container(self, schema: dict) -> tuple[list, list]:
|
|
322
|
+
params = [self.COMMAND_DELTA_PARAMTER]
|
|
323
|
+
try:
|
|
324
|
+
required_param_docs = [
|
|
325
|
+
"param text__: 用 JSON 描述参数,它的 JSON Schema 如右:",
|
|
326
|
+
json.dumps(schema),
|
|
327
|
+
]
|
|
328
|
+
except Exception as e:
|
|
329
|
+
raise e
|
|
330
|
+
|
|
331
|
+
return params, required_param_docs
|
|
332
|
+
|
|
333
|
+
def _parse_input_schema(self, input_schema: dict[str, Any], error_prefix="") -> tuple[list[str], list[str]]:
|
|
334
|
+
"""解析inputSchema并提取参数信息和参数文档"""
|
|
335
|
+
# todo: 考虑直接将 json schema 作为 text__ 参数.
|
|
336
|
+
if not input_schema:
|
|
337
|
+
return [], []
|
|
338
|
+
|
|
339
|
+
params = []
|
|
340
|
+
param_docs = []
|
|
341
|
+
try:
|
|
342
|
+
# 解析inputSchema
|
|
343
|
+
schema = input_schema
|
|
344
|
+
if isinstance(schema, str):
|
|
345
|
+
schema = json.loads(schema)
|
|
346
|
+
|
|
347
|
+
if "properties" not in schema:
|
|
348
|
+
return params, param_docs
|
|
349
|
+
|
|
350
|
+
# 合并列表,必需参数在前,可选参数在后
|
|
351
|
+
# mcp_types = [i.get('type', '') for i in list(schema['properties'].values())]
|
|
352
|
+
# if any([x in self.MCP_CONTAINER_TYPES for x in mcp_types]):
|
|
353
|
+
# params, param_docs = self._parse_schema_container(schema)
|
|
354
|
+
# else:
|
|
355
|
+
# params, param_docs = self._parse_schema(schema)
|
|
356
|
+
params, param_docs = self._parse_schema_container(schema)
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
print(f"{error_prefix}解析inputSchema出错: {e}")
|
|
360
|
+
return params, param_docs
|
|
361
|
+
|
|
362
|
+
def _adjust_description(self, description: str, param_doc: str) -> str:
|
|
363
|
+
return f"{description}\n{param_doc}\n"
|
|
364
|
+
|
|
365
|
+
def _generate_code_as_prompt(self, tool: types.Tool) -> tuple[str, str]:
|
|
366
|
+
"""生成模型可见的Command接口(Code as Prompt)"""
|
|
367
|
+
|
|
368
|
+
# 提取函数名(将连字符替换为下划线)
|
|
369
|
+
function_name = tool.name.replace("-", "_")
|
|
370
|
+
|
|
371
|
+
# 提取参数信息
|
|
372
|
+
params, param_docs = self._parse_input_schema(tool.inputSchema, "")
|
|
373
|
+
|
|
374
|
+
description = tool.description or ""
|
|
375
|
+
if len(params) == 1 and params[0] == self.COMMAND_DELTA_PARAMTER:
|
|
376
|
+
description = self._adjust_description(description, "".join(param_docs))
|
|
377
|
+
|
|
378
|
+
# 生成Async函数签名(符合Python语法)
|
|
379
|
+
interface = (
|
|
380
|
+
f"async def {function_name}({', '.join(params)}) -> Any:\n"
|
|
381
|
+
f" '''\n"
|
|
382
|
+
f" {description}\n"
|
|
383
|
+
# f" {''.join(param_docs)}\n"
|
|
384
|
+
f" '''\n"
|
|
385
|
+
f" pass"
|
|
386
|
+
)
|
|
387
|
+
return interface, description
|
|
388
|
+
|
|
389
|
+
def _build_channel_meta(
|
|
390
|
+
self, initialize_result: types.InitializeResult, tool_result: types.ListToolsResult
|
|
391
|
+
) -> ChannelMeta:
|
|
392
|
+
"""构建Channel元信息(包含所有工具的CommandMeta)"""
|
|
393
|
+
return ChannelMeta(
|
|
394
|
+
name=self._name,
|
|
395
|
+
channel_id=self._name,
|
|
396
|
+
available=True,
|
|
397
|
+
description=initialize_result.instructions or "",
|
|
398
|
+
commands=self._convert_tools_to_command_metas(tools=tool_result.tools),
|
|
399
|
+
children=[],
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# --- 未使用的生命周期方法(默认空实现) --- #
|
|
403
|
+
async def policy_run(self) -> None:
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
async def policy_pause(self) -> None:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
async def clear(self) -> None:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
def is_available(self) -> bool:
|
|
413
|
+
return True
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class MCPChannel(Channel):
|
|
417
|
+
"""对接MCP服务的Channel"""
|
|
418
|
+
|
|
419
|
+
def __init__(
|
|
420
|
+
self,
|
|
421
|
+
*,
|
|
422
|
+
name: str,
|
|
423
|
+
description: str,
|
|
424
|
+
mcp_client: mcp.ClientSession,
|
|
425
|
+
):
|
|
426
|
+
self._name = name
|
|
427
|
+
self._desc = description
|
|
428
|
+
self._mcp_client = mcp_client
|
|
429
|
+
self._client: Optional[MCPChannelBroker] = None
|
|
430
|
+
|
|
431
|
+
# --- Channel 核心方法实现 --- #
|
|
432
|
+
def name(self) -> str:
|
|
433
|
+
return self._name
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def broker(self) -> ChannelBroker:
|
|
437
|
+
if not self._client or not self._client.is_running():
|
|
438
|
+
raise RuntimeError("MCPChannel not bootstrapped")
|
|
439
|
+
return self._client
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def build(self) -> Builder:
|
|
443
|
+
raise NotImplementedError("MCPChannel does not implement `build`")
|
|
444
|
+
|
|
445
|
+
def bootstrap(self, container: Optional[IoCContainer] = None) -> ChannelBroker:
|
|
446
|
+
if self._client is not None and self._client.is_running():
|
|
447
|
+
raise RuntimeError(f"Channel {self} has already been started.")
|
|
448
|
+
|
|
449
|
+
self._client = MCPChannelBroker(
|
|
450
|
+
name=self._name,
|
|
451
|
+
container=container,
|
|
452
|
+
mcp_client=self._mcp_client,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
return self._client
|
|
456
|
+
|
|
457
|
+
# --- 未使用的Channel方法(默认空实现) --- #
|
|
458
|
+
def import_channels(self, *children: Channel) -> Channel:
|
|
459
|
+
raise NotImplementedError("MCPChannel does not support children")
|
|
460
|
+
|
|
461
|
+
def new_child(self, name: str) -> Channel:
|
|
462
|
+
raise NotImplementedError("MCPChannel does not support children")
|
|
463
|
+
|
|
464
|
+
def children(self) -> dict[str, Channel]:
|
|
465
|
+
return {}
|
|
466
|
+
|
|
467
|
+
def is_running(self) -> bool:
|
|
468
|
+
return self._client is not None and self._client.is_running()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from mcp import types as mcp_types
|
|
2
|
+
|
|
3
|
+
from ghoshell_moss.message import Addition
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MCPCallToolResultAddition(Addition, mcp_types.CallToolResult):
|
|
7
|
+
@classmethod
|
|
8
|
+
def keyword(cls) -> str:
|
|
9
|
+
return "mcp_call_tool_result"
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def from_mcp_types(cls, mcp_result: mcp_types.CallToolResult):
|
|
13
|
+
return cls(
|
|
14
|
+
isError=mcp_result.isError,
|
|
15
|
+
structuredContent=mcp_result.structuredContent,
|
|
16
|
+
content=mcp_result.content,
|
|
17
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from mcp import types
|
|
2
|
+
|
|
3
|
+
from ghoshell_moss import CommandError, CommandErrorCode
|
|
4
|
+
from ghoshell_moss.compatible.mcp_channel.types import MCPCallToolResultAddition
|
|
5
|
+
from ghoshell_moss.message import Base64Image, Message, Text
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def mcp_call_tool_result_to_message(mcp_result: types.CallToolResult, name: str | None = None) -> Message:
|
|
9
|
+
if mcp_result.isError:
|
|
10
|
+
raise CommandError(
|
|
11
|
+
code=CommandErrorCode.FAILED.value,
|
|
12
|
+
message=f"MCP tool: call failed, {mcp_result.content}",
|
|
13
|
+
)
|
|
14
|
+
result = Message.new(role="assistant", name=name)
|
|
15
|
+
for mcp_content in mcp_result.content:
|
|
16
|
+
if isinstance(mcp_content, types.TextContent):
|
|
17
|
+
result.with_content(Text(text=mcp_content.text))
|
|
18
|
+
if isinstance(mcp_content, types.ImageContent):
|
|
19
|
+
result.with_content(Base64Image(image_type=mcp_content.mimeType, encoded=mcp_content.data))
|
|
20
|
+
if isinstance(mcp_content, types.AudioContent):
|
|
21
|
+
pass
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
result.with_additions(MCPCallToolResultAddition.from_mcp_types(mcp_result))
|
|
25
|
+
return result
|