From c228038006293f3d9b71cc4a0125c7e7258b83c8 Mon Sep 17 00:00:00 2001 From: Will Date: Mon, 9 Feb 2026 13:05:29 +0800 Subject: [PATCH] chore: fix formatting and lint checking issues --- .pre-commit-config.yaml | 4 +- .ruff.toml | 1 + CONTRIBUTING.md | 2 +- README.md | 30 ++-- .../configs/moss_instructions/behaviors.md | 16 +-- examples/README.md | 1 - examples/jetarm_demo/README.md | 8 +- .../connect_pychannel_with_rcply.py | 2 +- examples/jetarm_demo/jetarm_agent.py | 13 +- examples/jetarm_ws/README.md | 24 ++-- examples/miku/main.py | 28 ++-- examples/miku/miku_provider.py | 10 +- examples/moss_agent.py | 49 +++---- examples/moss_zmq_channels/miku_app.py | 17 ++- examples/moss_zmq_channels/vision_app.py | 2 +- examples/vision_exam/README.md | 16 +-- examples/vision_exam/vision_provider.py | 2 +- examples/vision_exam/vision_proxy.py | 6 +- src/ghoshell_moss/core/concepts/command.py | 134 +++++++++--------- .../core/concepts/interpreter.py | 2 +- src/ghoshell_moss/message/abcd.py | 30 ++-- src/ghoshell_moss/speech/__init__.py | 8 +- src/ghoshell_moss/speech/mock.py | 10 +- .../channels/opencv_vision.py | 50 ++++--- src/ghoshell_moss_contrib/example_ws.py | 67 +++++---- src/ghoshell_moss_contrib/gui/image_viewer.py | 18 +-- 26 files changed, 267 insertions(+), 283 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99550f8..036e34c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,8 +14,8 @@ repos: require_serial: true additional_dependencies: [] - - id: ruff - name: ruff + - id: ruff-check + name: ruff-check description: "Run 'ruff' for extremely fast Python linting" entry: uv run --dev ruff check pass_filenames: false diff --git a/.ruff.toml b/.ruff.toml index 24b0d59..4d913b0 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,3 +1,4 @@ +target-version = "py310" exclude = ["docs/**"] line-length = 120 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28f8fc8..0c8e933 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ We welcome contributions! These guidelines exist to save everyone time. Followin ## Development Setup -1. Make sure you have `Python 3.12+` installed. +1. Make sure you have `Python 3.10+` installed. 1. Install [uv](https://docs.astral.sh/uv/getting-started/installation/). 1. Fork the repository and clone your fork. 1. Install development dependencies: `make prepare`. diff --git a/README.md b/README.md index 2197de5..b09120d 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,12 @@ Realtime-Actions 思想). 第一代 MOSS 架构 (全代码驱动 + FunctionToken ## Examples -在 [examples](examples) 目录下有当前 alpha 版各种用例. 具体的情况请查阅相关目录的 readme 文档. +在 [examples](examples) 目录下有当前 alpha 版各种用例. 具体的情况请查阅相关目录的 readme 文档. -体验 examples 的方法: +体验 examples 的方法: + +> 建议使用 mac, 基线都是在 mac 上测试的. windows 可能兼容存在问题. -> 建议使用 mac, 基线都是在 mac 上测试的. windows 可能兼容存在问题. - ## 1. clone 仓库 ```bash @@ -47,9 +47,9 @@ cd MOSShell ## 2. 创建环境 -* 使用 `uv` 创建环境, 运行 `uv venv` . 由于依赖 live2d, 所以默认的 python 版本是 3.12 -* 进入 uv 的环境: `source .venv/bin/activate` -* 安装所有依赖: +- 使用 `uv` 创建环境, 运行 `uv venv` . 由于依赖 live2d, 所以默认的 python 版本是 3.12 +- 进入 uv 的环境: `source .venv/bin/activate` +- 安装所有依赖: ```bash # examples 的依赖大多在 ghoshell-moss[contrib] 中, 没有拆分. 所以需要安装全部依赖. @@ -59,7 +59,7 @@ uv sync --active --all-extras ## 3. 配置环境变量 启动 demo 时需要配置模型和音频 (可选), 目前 alpha 版本的基线全部使用的是火山引擎. -需要把环境变量配置上. +需要把环境变量配置上. ```bash # 复制 env 文件为目标文件. @@ -71,7 +71,7 @@ vim examples/.env 配置时需要在火山引擎创建 大模型流式tts 服务. 不好搞定可以先设置 USE_VOICE_SPEECH 为 `no` -## 4. 运行 moss agent +## 4. 运行 moss agent ```bash # 基于当前环境的 python 运行 moss_agent 脚本 @@ -80,14 +80,14 @@ vim examples/.env # 打开后建议问它, 你可以做什么. ``` -已知的问题: -1. 语音输入模块 alpha 版本没有开发完. -2. 目前使用的 simple agent 是测试专用, 打断的生命周期还有问题. -3. 由于 shell 的几个控制原语未开发完, 一些行为阻塞逻辑会错乱. -4. interpreter 的生命周期计划 beta 完成, 现在交互的 ReACT 模式并不是最佳实践 (模型会连续回复) +已知的问题: -更多测试用例, 请看 examples 目录下的各个文件夹 readme. +1. 语音输入模块 alpha 版本没有开发完. +1. 目前使用的 simple agent 是测试专用, 打断的生命周期还有问题. +1. 由于 shell 的几个控制原语未开发完, 一些行为阻塞逻辑会错乱. +1. interpreter 的生命周期计划 beta 完成, 现在交互的 ReACT 模式并不是最佳实践 (模型会连续回复) +更多测试用例, 请看 examples 目录下的各个文件夹 readme. ## Beta Roadmap diff --git a/examples/.workspace/configs/moss_instructions/behaviors.md b/examples/.workspace/configs/moss_instructions/behaviors.md index 994e9c0..cc6f158 100644 --- a/examples/.workspace/configs/moss_instructions/behaviors.md +++ b/examples/.workspace/configs/moss_instructions/behaviors.md @@ -35,14 +35,14 @@ ## 你能做什么 我们希望你能帮助用户理解 MOSS 架构的原理. 所以我们在 README.md 里会引导用户和你交互. -但是用户肯定不知道要和你说什么. 所以你先需要了解我们为你提供的能力想要展示哪些功能: - -1. 给你一个 hub, 让你展示未来 AIOS 是可以用 打开/关闭 的方式将 channel 作为 app 的控制中心来使用. 同时是树形的渐进式披露. -2. 当你同时打开视觉模块, 和数字躯体模块, 这两个模块是完全离散, 可拆卸的. 但你却通过 channel 拥有了上下文的组合, 从而能够将之联动. -3. 其它的辅助工具是让你可以帮助用户了解这个项目的. 不要一次性打开太多. -4. 有几个工具我们隐藏了开关, 只有用户开启你才能使用. 比如用 jxa 控制 mac 上的软件. 毕竟这个功能有点过于强大了. -5. 我们在 beta 版本 (当前是 alpha) 会给你配备更多的能力, 比如 ppt, 阅读你自己的源码, 阅读你的项目文档等等. -6. 由于现在架构的 ReACT 范式还没实现好, 你可能在做完一个操作后, 又看到了 command 的操作结果. 这时不要重复执行动作, 只需要询问用户还有什么需要即可. +但是用户肯定不知道要和你说什么. 所以你先需要了解我们为你提供的能力想要展示哪些功能: + +1. 给你一个 hub, 让你展示未来 AIOS 是可以用 打开/关闭 的方式将 channel 作为 app 的控制中心来使用. 同时是树形的渐进式披露. +1. 当你同时打开视觉模块, 和数字躯体模块, 这两个模块是完全离散, 可拆卸的. 但你却通过 channel 拥有了上下文的组合, 从而能够将之联动. +1. 其它的辅助工具是让你可以帮助用户了解这个项目的. 不要一次性打开太多. +1. 有几个工具我们隐藏了开关, 只有用户开启你才能使用. 比如用 jxa 控制 mac 上的软件. 毕竟这个功能有点过于强大了. +1. 我们在 beta 版本 (当前是 alpha) 会给你配备更多的能力, 比如 ppt, 阅读你自己的源码, 阅读你的项目文档等等. +1. 由于现在架构的 ReACT 范式还没实现好, 你可能在做完一个操作后, 又看到了 command 的操作结果. 这时不要重复执行动作, 只需要询问用户还有什么需要即可. 你需要引导用户来了解你的能力. 但是, 请注意不要急于直接打开能力, 而是和用户先进行沟通, 告知会发生什么, 确认后才执行比较好 (用户不会被意外冒犯). diff --git a/examples/README.md b/examples/README.md index fd2c9f4..0595137 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,4 +2,3 @@ 本目录用来存放各种 Alpha 版本的测试用例. 用来展示不同的基线功能. 每个子目录内都有 README.md 提示如何使用. - diff --git a/examples/jetarm_demo/README.md b/examples/jetarm_demo/README.md index 4600284..834684d 100644 --- a/examples/jetarm_demo/README.md +++ b/examples/jetarm_demo/README.md @@ -5,12 +5,12 @@ 运行前需要: 1. 真的有幻尔 6dof jetarm 机械臂. -2. 在机械臂开发板上, 已经实装了 jetarm_ws, 完成编译可运行, 并启动了 jetarm_channel 和 jetarm_control 节点. -3. 已经完成了 examples 的依赖安装. +1. 在机械臂开发板上, 已经实装了 jetarm_ws, 完成编译可运行, 并启动了 jetarm_channel 和 jetarm_control 节点. +1. 已经完成了 examples 的依赖安装. 运行: 1. 测试 `python connect_pychannel_with_rcply.py --address=jetarm_control监听的地址端口`, 看看是否能运动. -2. 启动 agent `python jetarm_agent.py --address=jetarm_control监听的地址端口` +1. 启动 agent `python jetarm_agent.py --address=jetarm_control监听的地址端口` -这个例子不必特别测试. jetarm 本身二次开发难度比较大. 看看样例知道怎么回事就可以了. \ No newline at end of file +这个例子不必特别测试. jetarm 本身二次开发难度比较大. 看看样例知道怎么回事就可以了. diff --git a/examples/jetarm_demo/connect_pychannel_with_rcply.py b/examples/jetarm_demo/connect_pychannel_with_rcply.py index 14baef0..6684e63 100644 --- a/examples/jetarm_demo/connect_pychannel_with_rcply.py +++ b/examples/jetarm_demo/connect_pychannel_with_rcply.py @@ -1,5 +1,5 @@ -import asyncio import argparse +import asyncio from ghoshell_moss.transports.zmq_channel.zmq_channel import ZMQChannelProxy diff --git a/examples/jetarm_demo/jetarm_agent.py b/examples/jetarm_demo/jetarm_agent.py index b79e332..1daf043 100644 --- a/examples/jetarm_demo/jetarm_agent.py +++ b/examples/jetarm_demo/jetarm_agent.py @@ -1,7 +1,8 @@ +import argparse import asyncio +from pathlib import Path from ghoshell_container import Container -import argparse from ghoshell_moss.core.shell import new_shell from ghoshell_moss.speech import make_baseline_tts_speech @@ -10,8 +11,7 @@ from ghoshell_moss.transports.zmq_channel.zmq_channel import ZMQChannelProxy from ghoshell_moss_contrib.agent import ModelConf, SimpleAgent from ghoshell_moss_contrib.agent.chat import ConsoleChat -from ghoshell_moss_contrib.example_ws import workspace_container, get_container -from pathlib import Path +from ghoshell_moss_contrib.example_ws import get_container, workspace_container ADDRESS = "tcp://192.168.1.15:9527" """填入正确的 ip, 需要先对齐 jetarm_ws 运行的机器设备和监听的端口. """ @@ -63,16 +63,13 @@ def main(): # 添加 --address 参数,设置默认值 parser.add_argument( - "--address", - type=str, - default="tcp://192.168.1.15:9527", - help="代理地址,默认值: tcp://192.168.1.15:9527" + "--address", type=str, default="tcp://192.168.1.15:9527", help="代理地址,默认值: tcp://192.168.1.15:9527" ) # 解析命令行参数 args = parser.parse_args() - ws_dir = Path(__file__).resolve().parent.parent.joinpath('.workspace') + ws_dir = Path(__file__).resolve().parent.parent.joinpath(".workspace") with workspace_container(workspace_dir=ws_dir) as container: # 运行异步主函数,传入地址参数 asyncio.run(run_agent(address=args.address, container=container)) diff --git a/examples/jetarm_ws/README.md b/examples/jetarm_ws/README.md index ea9a153..21ee284 100644 --- a/examples/jetarm_ws/README.md +++ b/examples/jetarm_ws/README.md @@ -108,18 +108,18 @@ source install/setup.zsh ## 核心目录说明 - `src`: 核心库目录 - - `jetarm_6dof_description`: - 用来存放 jetarm 的机器人描述相关讯息, - 也可以启动 rviz `ros2 launch jetarm_6dof_description view_model.launch.py` - - `jetarm_driver`: - 是验证 python 驱动的节点, 想用 python 实现 ros2 control 的 interface. 不过现在不用了. - - `jetarm_control`: - 核心的 ros2 control 实现. 启动这个节点, 机器人就可以驱动了. 详见后面的测试用例. deepseek 等也能给出 ros2 - control 支持的各种命令. - 运行这个节点的脚本是 `ros2 launch jetarm_control jetarm_control.launch.py` - - `jetarm_moveit2`: - 这个是基于 ros2 control (jetarm_control) 实现的 moveit 节点, 所有的代码应该都由 moveit2 的 assitant 生成. - 具体方法可以问模型, 需要提前安装 moveit2 到全局环境里. + - `jetarm_6dof_description`: + 用来存放 jetarm 的机器人描述相关讯息, + 也可以启动 rviz `ros2 launch jetarm_6dof_description view_model.launch.py` + - `jetarm_driver`: + 是验证 python 驱动的节点, 想用 python 实现 ros2 control 的 interface. 不过现在不用了. + - `jetarm_control`: + 核心的 ros2 control 实现. 启动这个节点, 机器人就可以驱动了. 详见后面的测试用例. deepseek 等也能给出 ros2 + control 支持的各种命令. + 运行这个节点的脚本是 `ros2 launch jetarm_control jetarm_control.launch.py` + - `jetarm_moveit2`: + 这个是基于 ros2 control (jetarm_control) 实现的 moveit 节点, 所有的代码应该都由 moveit2 的 assitant 生成. + 具体方法可以问模型, 需要提前安装 moveit2 到全局环境里. # 常用测试命令 diff --git a/examples/miku/main.py b/examples/miku/main.py index 861a080..6b3b0dc 100644 --- a/examples/miku/main.py +++ b/examples/miku/main.py @@ -1,25 +1,25 @@ +import asyncio +import importlib.util import os import sys - -from ghoshell_moss.speech import make_baseline_tts_speech, Speech -from ghoshell_moss.speech.player.pyaudio_player import PyAudioStreamPlayer -from ghoshell_moss.speech.volcengine_tts import VolcengineTTS, VolcengineTTSConf -from ghoshell_moss_contrib.agent import ModelConf, SimpleAgent - -import asyncio from os.path import dirname, join import live2d.v3 as live2d import pygame from ghoshell_container import Container +from ghoshell_moss.speech import Speech, make_baseline_tts_speech +from ghoshell_moss.speech.player.pyaudio_player import PyAudioStreamPlayer +from ghoshell_moss.speech.volcengine_tts import VolcengineTTS, VolcengineTTSConf +from ghoshell_moss_contrib.agent import ModelConf, SimpleAgent + current_dir = os.path.dirname(os.path.abspath(__file__)) -try: - import miku_channels -except ImportError: +if importlib.util.find_spec(miku_channels) is None: # 加载当前路径. sys.path.append(current_dir) +import pathlib + from miku_channels.arm import left_arm_chan, right_arm_chan from miku_channels.body import body_chan from miku_channels.elbow import left_elbow_chan, right_elbow_chan @@ -30,9 +30,9 @@ from miku_channels.leg import left_leg_chan, right_leg_chan from miku_channels.necktie import necktie_chan from miku_provider import init_live2d, init_pygame + from ghoshell_moss.core.shell import new_shell -from ghoshell_moss_contrib.example_ws import workspace_container, get_example_speech -import pathlib +from ghoshell_moss_contrib.example_ws import get_example_speech, workspace_container # 全局状态 WIDTH = 600 @@ -196,13 +196,13 @@ async def run_agent_and_render(container: Container, speech: Speech | None = Non pygame.quit() -WORKSPACE_DIR = pathlib.Path(__file__).parent.parent.joinpath('.workspace') +WORKSPACE_DIR = pathlib.Path(__file__).parent.parent.joinpath(".workspace") def main(): # 运行异步主函数 with workspace_container(WORKSPACE_DIR) as container: - speech = get_example_speech(container, default_speaker='saturn_zh_female_keainvsheng_tob') + speech = get_example_speech(container, default_speaker="saturn_zh_female_keainvsheng_tob") asyncio.run(run_agent_and_render(container, speech)) diff --git a/examples/miku/miku_provider.py b/examples/miku/miku_provider.py index 52fdbb2..1521573 100644 --- a/examples/miku/miku_provider.py +++ b/examples/miku/miku_provider.py @@ -1,4 +1,5 @@ import asyncio +import importlib.util import os import sys from os.path import dirname, join @@ -7,10 +8,8 @@ import pygame from ghoshell_container import Container, get_container -try: - import miku_channels -except ImportError: - current_dir = os.path.dirname(os.path.abspath(__file__)) +current_dir = os.path.dirname(os.path.abspath(__file__)) +if importlib.util.find_spec(miku_channels) is None: sys.path.append(current_dir) from miku_channels.arm import left_arm_chan, right_arm_chan @@ -22,8 +21,9 @@ from miku_channels.head import head_chan from miku_channels.leg import left_leg_chan, right_leg_chan from miku_channels.necktie import necktie_chan -from ghoshell_moss.transports.zmq_channel import ZMQChannelProvider + from ghoshell_moss import Channel +from ghoshell_moss.transports.zmq_channel import ZMQChannelProvider # 全局状态 model: live2d.LAppModel | None = None diff --git a/examples/moss_agent.py b/examples/moss_agent.py index ec3cca2..e0c2602 100644 --- a/examples/moss_agent.py +++ b/examples/moss_agent.py @@ -1,22 +1,17 @@ -import os.path -import pathlib import asyncio +import pathlib -from ghoshell_common.contracts import Workspace, LoggerItf +from ghoshell_common.contracts import LoggerItf, Workspace from ghoshell_container import Container -from ghoshell_moss_contrib.example_ws import workspace_container, get_example_speech -from ghoshell_moss.channels.mac_channel import new_mac_control_channel -from ghoshell_moss_contrib.channels.mermaid_draw import new_mermaid_chan -from ghoshell_moss_contrib.channels.web_bookmark import build_web_bookmark_chan - -from ghoshell_moss_contrib.agent import SimpleAgent, ModelConf, ConsoleChat from ghoshell_moss.core.shell import new_shell -from ghoshell_moss.transports.zmq_channel.zmq_hub import ZMQChannelHub, ZMQHubConfig, ZMQProxyConfig # 不着急删除, 方便自测时开启. -from ghoshell_moss_contrib.channels.screen_capture import ScreenCapture -from ghoshell_moss.transports.zmq_channel.zmq_hub import ZMQChannelProxy +from ghoshell_moss.transports.zmq_channel.zmq_hub import ZMQChannelHub, ZMQHubConfig, ZMQProxyConfig +from ghoshell_moss_contrib.agent import ConsoleChat, ModelConf, SimpleAgent +from ghoshell_moss_contrib.channels.mermaid_draw import new_mermaid_chan +from ghoshell_moss_contrib.channels.web_bookmark import build_web_bookmark_chan +from ghoshell_moss_contrib.example_ws import get_example_speech, workspace_container """ 说明: @@ -28,7 +23,7 @@ """ CURRENT_DIR = pathlib.Path(__file__).parent -WORKSPACE_DIR = CURRENT_DIR.joinpath('.workspace').absolute() +WORKSPACE_DIR = CURRENT_DIR.joinpath(".workspace").absolute() def load_instructions(con: Container, files: list[str]) -> str: @@ -37,11 +32,11 @@ def load_instructions(con: Container, files: list[str]) -> str: TODO: 暂时先这么做. Beta 版本会做一个正式的 Agent. Alpha 版本先临时用测试的 simple agent 攒一个. """ ws = con.force_fetch(Workspace) - instru_storage = ws.configs().sub_storage('moss_instructions') + instru_storage = ws.configs().sub_storage("moss_instructions") instructions = [] for filename in files: content = instru_storage.get(filename) - instructions.append(content.decode('utf-8')) + instructions.append(content.decode("utf-8")) return "\n\n".join(instructions) @@ -58,10 +53,8 @@ def run_moss_agent(container: Container): config=ZMQHubConfig( name="hub", description="可以启动指定的子通道并运行.", - # todo: 当前版本全部基于约定来做. 快速验证. - root_dir=str(CURRENT_DIR.joinpath('moss_zmq_channels').absolute()), - + root_dir=str(CURRENT_DIR.joinpath("moss_zmq_channels").absolute()), # todo: # zmq hub 不是 MOSS 架构的目标范式, Alpha 版本未完成 LocalChannelApplications 模块 # 所以先用 zmq hub 来验证跨进程打开的效果. @@ -97,12 +90,10 @@ def run_moss_agent(container: Container): # 浏览器 build_web_bookmark_chan(container), new_mermaid_chan(), - # todo: 开启这个模块, 可以让 Agent 通过 JXA 操作 mac 电脑. 不过配套的 prompt 并不完善. # new_mac_control_channel(description="使用 jxa 语法来操作当前所在 mac, 有明确 mac 操作命令要求时才允许使用."), # todo: 开启这个模块, 可以让 Agent 选择屏幕截图. # ScreenCapture(logger=logger).as_channel(), - # todo: 如果有 Jetarm demo 的话... 可以开启, 让 moss 可以同时控制数字人. # ZMQChannelProxy( # name="jetarm", @@ -113,9 +104,9 @@ def run_moss_agent(container: Container): instructions = load_instructions( container, [ - 'persona.md', - 'behaviors.md', - ] + "persona.md", + "behaviors.md", + ], ) agent = SimpleAgent( @@ -124,11 +115,11 @@ def run_moss_agent(container: Container): instruction=instructions, chat=ConsoleChat(logger=logger), model=ModelConf( - kwargs=dict( - thinking=dict( - type="disabled", - ) - ), + kwargs={ + "thinking": { + "type": "disabled", + }, + }, ), shell=shell, ) @@ -150,6 +141,6 @@ async def run_agent(): asyncio.run(run_agent()) -if __name__ == '__main__': +if __name__ == "__main__": with workspace_container(WORKSPACE_DIR) as _container: run_moss_agent(_container) diff --git a/examples/moss_zmq_channels/miku_app.py b/examples/moss_zmq_channels/miku_app.py index 73f801f..6b06105 100644 --- a/examples/moss_zmq_channels/miku_app.py +++ b/examples/moss_zmq_channels/miku_app.py @@ -1,22 +1,21 @@ -from os.path import dirname, join import sys +from os.path import dirname, join # patch miku 的读取路径. # 由于 miku 还是一个实验性的数字人项目, 暂时不希望把它打包到 ghoshell_moss_contrib 里 (太大) # 所以先用比较脏的相对路径来读取它. current_dir = dirname(__file__) -workspace_dir = join(dirname(current_dir), '.workspace') -try: - import miku -except ImportError: - miku_dir = join(dirname(current_dir), 'miku') - print(miku_dir) +workspace_dir = join(dirname(current_dir), ".workspace") +miku_dir = join(dirname(current_dir), "miku") +if miku_dir not in sys.path: sys.path.append(miku_dir) - from miku_provider import run_game_with_zmq_provider import asyncio + +from miku_provider import run_game_with_zmq_provider + from ghoshell_moss_contrib.example_ws import workspace_container -if __name__ == '__main__': +if __name__ == "__main__": with workspace_container(workspace_dir) as _container: asyncio.run(run_game_with_zmq_provider(address="tcp://localhost:5555", con=_container)) diff --git a/examples/moss_zmq_channels/vision_app.py b/examples/moss_zmq_channels/vision_app.py index 1b4e9af..3fa4758 100644 --- a/examples/moss_zmq_channels/vision_app.py +++ b/examples/moss_zmq_channels/vision_app.py @@ -1,6 +1,6 @@ -from ghoshell_moss_contrib.channels.opencv_vision import OpenCVVision from ghoshell_moss import get_container from ghoshell_moss.transports.zmq_channel import ZMQChannelProvider +from ghoshell_moss_contrib.channels.opencv_vision import OpenCVVision if __name__ == "__main__": # 初始化容器 diff --git a/examples/vision_exam/README.md b/examples/vision_exam/README.md index 66f2a5a..0649748 100644 --- a/examples/vision_exam/README.md +++ b/examples/vision_exam/README.md @@ -5,12 +5,12 @@ 测试方式: 1. 确保已经运行 `uv sync --active --all-extras` 完成依赖同步. -2. 确认 python 在 uv 的 venv 环境: `which python` -3. 直接运行 vision_provider: `python ./examples/vision_exam/vision_provider.py` - * 这会启动 vision channel 的 provider 进程, 监听 5557 端口允许被访问. - * 会打开一个 opencv 的端口, 可以看到视觉信息. -4. 运行 vision_proxy: `python ./examples/vision_exam/vision_proxy.py` - * 会与 vision provider 建立双工通讯, 可以控制对方. - * 打开一个 pyqt6 的 simple viewer, 周期性同步 vision channel 的信息. +1. 确认 python 在 uv 的 venv 环境: `which python` +1. 直接运行 vision_provider: `python ./examples/vision_exam/vision_provider.py` + - 这会启动 vision channel 的 provider 进程, 监听 5557 端口允许被访问. + - 会打开一个 opencv 的端口, 可以看到视觉信息. +1. 运行 vision_proxy: `python ./examples/vision_exam/vision_proxy.py` + - 会与 vision provider 建立双工通讯, 可以控制对方. + - 打开一个 pyqt6 的 simple viewer, 周期性同步 vision channel 的信息. -当 vision provider 的视觉消息同步给了 vision proxy, 能正确展示图片, 则测试成功. \ No newline at end of file +当 vision provider 的视觉消息同步给了 vision proxy, 能正确展示图片, 则测试成功. diff --git a/examples/vision_exam/vision_provider.py b/examples/vision_exam/vision_provider.py index 1b4e9af..3fa4758 100644 --- a/examples/vision_exam/vision_provider.py +++ b/examples/vision_exam/vision_provider.py @@ -1,6 +1,6 @@ -from ghoshell_moss_contrib.channels.opencv_vision import OpenCVVision from ghoshell_moss import get_container from ghoshell_moss.transports.zmq_channel import ZMQChannelProvider +from ghoshell_moss_contrib.channels.opencv_vision import OpenCVVision if __name__ == "__main__": # 初始化容器 diff --git a/examples/vision_exam/vision_proxy.py b/examples/vision_exam/vision_proxy.py index f384fd7..0c6e948 100644 --- a/examples/vision_exam/vision_proxy.py +++ b/examples/vision_exam/vision_proxy.py @@ -1,17 +1,16 @@ import asyncio +from ghoshell_moss.message.contents import Base64Image from ghoshell_moss.transports.zmq_channel.zmq_channel import ZMQChannelProxy from ghoshell_moss_contrib.gui.image_viewer import SimpleImageViewer, run_img_viewer -from ghoshell_moss.message.contents import Base64Image -if __name__ == '__main__': +if __name__ == "__main__": # 测试专用. proxy = ZMQChannelProxy( name="vision", address="tcp://127.0.0.1:5557", ) - def callback(viewer: SimpleImageViewer): async def main(): @@ -30,5 +29,4 @@ async def main(): asyncio.run(main()) - run_img_viewer(callback) diff --git a/src/ghoshell_moss/core/concepts/command.py b/src/ghoshell_moss/core/concepts/command.py index c11f965..43fe0b8 100644 --- a/src/ghoshell_moss/core/concepts/command.py +++ b/src/ghoshell_moss/core/concepts/command.py @@ -62,11 +62,11 @@ class CommandTaskStateType(str, Enum): @classmethod def is_complete(cls, state: str | Self) -> bool: - return state == cls.done or state == cls.failed or state == cls.cancelled + return state in (cls.done, cls.failed, cls.cancelled) @classmethod def is_stopped(cls, state: str | Self) -> bool: - return state == cls.cancelled or state == cls.failed + return state in (cls.cancelled, cls.failed) class CommandTaskState(str, Enum): @@ -219,13 +219,13 @@ class CommandMeta(BaseModel): interface: str = Field( default="", description="大模型所看到的关于这个命令的 prompt. 类似于 FunctionCall 协议提供的 JSON Schema." - "但核心思想是 Code As Prompt." - "通常是一个 python async 函数的 signature. 形如:" - "```python" - "async def name(arg: typehint = default) -> return_type:" - " ''' docstring '''" - " pass" - "```", + "但核心思想是 Code As Prompt." + "通常是一个 python async 函数的 signature. 形如:" + "```python" + "async def name(arg: typehint = default) -> return_type:" + " ''' docstring '''" + " pass" + "```", ) args_schema: Optional[dict[str, Any]] = Field( default=None, @@ -298,9 +298,9 @@ async def __call__(self, *args, **kwargs) -> RESULT: class CommandWrapper(Command[RESULT]): def __init__( - self, - meta: CommandMeta, - func: Callable[..., Coroutine[Any, Any, RESULT]], + self, + meta: CommandMeta, + func: Callable[..., Coroutine[Any, Any, RESULT]], ): self._func = func self._meta = meta @@ -330,19 +330,19 @@ class PyCommand(Generic[RESULT], Command[RESULT]): """ def __init__( - self, - func: Callable[..., Coroutine[None, None, RESULT]] | Callable[..., RESULT], - *, - chan: Optional[str] = None, - name: Optional[str] = None, - available: Callable[[], bool] | None = None, - interface: Optional[StringType] = None, - doc: Optional[StringType] = None, - comments: Optional[StringType] = None, - meta: Optional[CommandMeta] = None, - tags: Optional[list[str]] = None, - call_soon: bool = False, - block: bool = True, + self, + func: Callable[..., Coroutine[None, None, RESULT]] | Callable[..., RESULT], + *, + chan: Optional[str] = None, + name: Optional[str] = None, + available: Callable[[], bool] | None = None, + interface: Optional[StringType] = None, + doc: Optional[StringType] = None, + comments: Optional[StringType] = None, + meta: Optional[CommandMeta] = None, + tags: Optional[list[str]] = None, + call_soon: bool = False, + block: bool = True, ): """ :param func: origin coroutine function @@ -460,15 +460,15 @@ class CommandTask(Generic[RESULT], ABC): """ def __init__( - self, - *, - meta: CommandMeta, - func: Callable[..., Coroutine[None, None, RESULT]] | None, - tokens: str, - args: list, - kwargs: dict[str, Any], - cid: str | None = None, - context: dict[str, Any] | None = None, + self, + *, + meta: CommandMeta, + func: Callable[..., Coroutine[None, None, RESULT]] | None, + tokens: str, + args: list, + kwargs: dict[str, Any], + cid: str | None = None, + context: dict[str, Any] | None = None, ) -> None: self.cid: str = cid or uuid() self.tokens: str = tokens @@ -587,10 +587,10 @@ def exception(self) -> Optional[Exception]: @abstractmethod async def wait( - self, - *, - throw: bool = True, - timeout: float | None = None, + self, + *, + throw: bool = True, + timeout: float | None = None, ) -> Optional[RESULT]: """ async wait the task to be done thread-safe @@ -681,15 +681,15 @@ class BaseCommandTask(Generic[RESULT], CommandTask[RESULT]): """ def __init__( - self, - *, - meta: CommandMeta, - func: Callable[..., Coroutine[None, None, RESULT]] | None, - tokens: str, - args: list, - kwargs: dict[str, Any], - cid: str | None = None, - context: dict[str, Any] | None = None, + self, + *, + meta: CommandMeta, + func: Callable[..., Coroutine[None, None, RESULT]] | None, + tokens: str, + args: list, + kwargs: dict[str, Any], + cid: str | None = None, + context: dict[str, Any] | None = None, ) -> None: super().__init__( meta=meta, @@ -764,12 +764,12 @@ def set_state(self, state: CommandTaskStateType | str) -> None: self.trace[self.state] = now def _set_result( - self, - result: Optional[RESULT], - state: CommandTaskStateType | str, - errcode: int, - errmsg: Optional[str], - done_at: Optional[str] = None, + self, + result: Optional[RESULT], + state: CommandTaskStateType | str, + errcode: int, + errmsg: Optional[str], + done_at: Optional[str] = None, ) -> bool: with self._done_lock: if self._done_event.is_set(): @@ -822,10 +822,10 @@ def exception(self) -> Optional[Exception]: return CommandError(self.errcode, self.errmsg or "") async def wait( - self, - *, - throw: bool = True, - timeout: float | None = None, + self, + *, + throw: bool = True, + timeout: float | None = None, ) -> Optional[RESULT]: """ 等待命令被执行完毕. 但不会主动运行这个任务. 仅仅是等待. @@ -863,9 +863,9 @@ class WaitDoneTask(BaseCommandTask): """ def __init__( - self, - tasks: Iterable[CommandTask], - after: Optional[Callable[[], Coroutine[None, None, RESULT]]] = None, + self, + tasks: Iterable[CommandTask], + after: Optional[Callable[[], Coroutine[None, None, RESULT]]] = None, ) -> None: meta = CommandMeta( name="_wait_done", @@ -894,10 +894,10 @@ class CancelAfterOthersTask(BaseCommandTask[None]): """ def __init__( - self, - current: CommandTask, - *tasks: CommandTask, - tokens: str = "", + self, + current: CommandTask, + *tasks: CommandTask, + tokens: str = "", ) -> None: meta = CommandMeta( name="cancel_" + current.meta.name, @@ -929,9 +929,9 @@ class CommandTaskStack: """特殊的数据结构, 用来标记一个 task 序列, 也可以由 task 返回.""" def __init__( - self, - iterator: AsyncIterator[CommandTask] | list[CommandTask], - on_success: Callable[[list[CommandTask]], Coroutine[None, None, Any]] | Any = None, + self, + iterator: AsyncIterator[CommandTask] | list[CommandTask], + on_success: Callable[[list[CommandTask]], Coroutine[None, None, Any]] | Any = None, ) -> None: self._iterator = iterator self._on_success = on_success diff --git a/src/ghoshell_moss/core/concepts/interpreter.py b/src/ghoshell_moss/core/concepts/interpreter.py index f77e579..d85fef2 100644 --- a/src/ghoshell_moss/core/concepts/interpreter.py +++ b/src/ghoshell_moss/core/concepts/interpreter.py @@ -349,7 +349,7 @@ async def wait_parse_done(self, timeout: float | None = None) -> None: @abstractmethod async def wait_execution_done( - self, timeout: float | None = None, *, throw: bool = False, cancel_on_exception: bool = True + self, timeout: float | None = None, *, throw: bool = False, cancel_on_exception: bool = True ) -> dict[str, CommandTask]: """ 等待所有的 task 被执行完毕. diff --git a/src/ghoshell_moss/message/abcd.py b/src/ghoshell_moss/message/abcd.py index f120e8f..ca26bd3 100644 --- a/src/ghoshell_moss/message/abcd.py +++ b/src/ghoshell_moss/message/abcd.py @@ -386,16 +386,16 @@ class Message(BaseModel, WithAdditional): seq: Literal["head", "delta", "incomplete", "completed"] = Field( default="completed", description="消息的传输状态, 目前分为首包, 间包和尾包." - "- 首包: 用来提示一个消息流已经被生产. 通常用来通知前端界面, 提前渲染消息容器" - "- 间包: 用最少的讯息传递一个 delta 包, 用于流式传输" - "- 尾包: 包含所有 delta 包粘包后的完整结果, 用来存储或展示." - "尾包分为 completed 和 incomplete 两种. " - "- completed 表示一个消息体完全传输完毕." - "- incomplete 表示虽然没传输完毕, 但可能也要直接使用." - "我们举一个具体的例子, 在模型处理多端输入时, 一个视觉信号让模型要反馈, 但一个 asr 输入还未全部完成;" - "这个时候, 大模型仍然要看到未完成的语音输入, 也就是 incomplete 消息." - "但是下一轮对话, 当 asr 已经完成时, 历史消息里不需要展示 incomplete 包." - "所以 incomplete 主要是用来在大模型思考的关键帧中展示一个粘包中的中间结果.", + "- 首包: 用来提示一个消息流已经被生产. 通常用来通知前端界面, 提前渲染消息容器" + "- 间包: 用最少的讯息传递一个 delta 包, 用于流式传输" + "- 尾包: 包含所有 delta 包粘包后的完整结果, 用来存储或展示." + "尾包分为 completed 和 incomplete 两种. " + "- completed 表示一个消息体完全传输完毕." + "- incomplete 表示虽然没传输完毕, 但可能也要直接使用." + "我们举一个具体的例子, 在模型处理多端输入时, 一个视觉信号让模型要反馈, 但一个 asr 输入还未全部完成;" + "这个时候, 大模型仍然要看到未完成的语音输入, 也就是 incomplete 消息." + "但是下一轮对话, 当 asr 已经完成时, 历史消息里不需要展示 incomplete 包." + "所以 incomplete 主要是用来在大模型思考的关键帧中展示一个粘包中的中间结果.", ) delta: Optional[Delta] = Field( default=None, @@ -405,11 +405,11 @@ class Message(BaseModel, WithAdditional): @classmethod def new( - cls, - *, - role: Literal["assistant", "system", "developer", "user", ""] = "", - name: Optional[str] = None, - id: Optional[str] = None, + cls, + *, + role: Literal["assistant", "system", "developer", "user", ""] = "", + name: Optional[str] = None, + id: Optional[str] = None, ): """ 语法糖, 用来创建一条消息. diff --git a/src/ghoshell_moss/speech/__init__.py b/src/ghoshell_moss/speech/__init__.py index c36f500..222aa9d 100644 --- a/src/ghoshell_moss/speech/__init__.py +++ b/src/ghoshell_moss/speech/__init__.py @@ -1,14 +1,14 @@ from ghoshell_common.contracts import LoggerItf -from ghoshell_moss.core.concepts.speech import TTS, StreamAudioPlayer, Speech, SpeechStream +from ghoshell_moss.core.concepts.speech import TTS, Speech, SpeechStream, StreamAudioPlayer from ghoshell_moss.speech.mock import MockSpeech from ghoshell_moss.speech.stream_tts_speech import TTSSpeech, TTSSpeechStream def make_baseline_tts_speech( - player: StreamAudioPlayer | None = None, - tts: TTS | None = None, - logger: LoggerItf | None = None, + player: StreamAudioPlayer | None = None, + tts: TTS | None = None, + logger: LoggerItf | None = None, ) -> TTSSpeech: """ 基线示例. diff --git a/src/ghoshell_moss/speech/mock.py b/src/ghoshell_moss/speech/mock.py index 1c0d853..5be462f 100644 --- a/src/ghoshell_moss/speech/mock.py +++ b/src/ghoshell_moss/speech/mock.py @@ -1,4 +1,5 @@ import threading +import time from queue import Empty, Queue from typing import Optional @@ -6,15 +7,14 @@ from ghoshell_moss.core.concepts.speech import Speech, SpeechStream from ghoshell_moss.core.helpers.asyncio_utils import ThreadSafeEvent -import time class MockSpeechStream(SpeechStream): def __init__( - self, - outputs: list[str], - id: str = "", - typing_sleep: float = 0.0, + self, + outputs: list[str], + id: str = "", + typing_sleep: float = 0.0, ): super().__init__(id=id or uuid()) self.outputs = outputs diff --git a/src/ghoshell_moss_contrib/channels/opencv_vision.py b/src/ghoshell_moss_contrib/channels/opencv_vision.py index 69b5b98..be6dc4e 100644 --- a/src/ghoshell_moss_contrib/channels/opencv_vision.py +++ b/src/ghoshell_moss_contrib/channels/opencv_vision.py @@ -1,16 +1,16 @@ +import logging import threading import time from datetime import datetime -from typing import List, Optional, Tuple -from PIL import Image -import cv2 +from typing import Optional -from ghoshell_moss.message import Message, Base64Image, Text +import cv2 from ghoshell_common.contracts import LoggerItf from ghoshell_container import IoCContainer, get_container +from PIL import Image + from ghoshell_moss import PyChannel -from ghoshell_moss.transports.zmq_channel.zmq_channel import ZMQChannelProvider -import logging +from ghoshell_moss.message import Base64Image, Message, Text class OpenCVVision: @@ -58,7 +58,7 @@ def _initialize_camera(self) -> bool: # 测试读取一帧 ret, _ = self._cap.read() if ret: - self._logger.info(f"摄像头初始化成功,使用索引 {camera_index}") + self._logger.info("摄像头初始化成功,使用索引 %s", camera_index) return True else: self._cap.release() @@ -66,8 +66,8 @@ def _initialize_camera(self) -> bool: self._logger.error("无法初始化任何摄像头") return False - except Exception as e: - self._logger.error(f"摄像头初始化失败: {e}") + except Exception: + self._logger.exception("摄像头初始化失败") return False def _capture_frame(self) -> bool: @@ -91,15 +91,14 @@ def _capture_frame(self) -> bool: # 处理按键('q'退出) key = cv2.waitKey(1) & 0xFF - if key == ord('q'): + if key == ord("q"): self._logger.info("用户按q键,视觉模块退出") return False current_time = time.time() # 按目标帧率更新缓存 - if (self._is_caching_image and - current_time - self._last_capture_time >= self._frame_interval): + if self._is_caching_image and current_time - self._last_capture_time >= self._frame_interval: # 转换颜色空间:BGR -> RGB rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) @@ -153,15 +152,15 @@ def close(self) -> None: # 确保窗口被销毁 try: cv2.destroyWindow(self._window_name) - except: - pass + except Exception: + self._logger.debug("销毁 OpenCV 窗口失败: %s", self._window_name, exc_info=True) # 最后调用 destroyAllWindows 确保清理所有窗口 cv2.destroyAllWindows() self._logger.info("视觉模块已关闭") - def get_cached_image(self) -> Tuple[Optional[Image.Image], float]: + def get_cached_image(self) -> tuple[Optional[Image.Image], float]: """ 线程安全地获取缓存的图像和时间戳 @@ -195,7 +194,7 @@ async def start_looking(self) -> None: self._is_caching_image = True self._last_capture_time = 0.0 # 重置时间,立即捕获下一帧 - async def context_messages(self) -> List[Message]: + async def context_messages(self) -> list[Message]: """ 返回最新的视觉信息作为上下文消息 @@ -211,16 +210,16 @@ async def context_messages(self) -> List[Message]: if image is None: # 如果有错误信息,可以返回错误提示(可选) if self._last_error: - error_msg = Message.new(role='system', name="__vision_error__").with_content( + error_msg = Message.new(role="system", name="__vision_error__").with_content( Text(text=f"视觉模块错误: {self._last_error}") ) return [error_msg] return [] # 创建视觉消息 - timestamp_str = datetime.fromtimestamp(timestamp).strftime('%d.%m.%Y %H:%M:%S') + timestamp_str = datetime.fromtimestamp(timestamp).strftime("%d.%m.%Y %H:%M:%S") - message = Message.new(role='user', name="__vision_system__").with_content( + message = Message.new(role="user", name="__vision_system__").with_content( Text(text=f"这是你最新看到的视觉信息,来自你的视野。时间: {timestamp_str}"), Base64Image.from_pil_image(image), ) @@ -228,7 +227,7 @@ async def context_messages(self) -> List[Message]: return [message] def description(self) -> str: - status = '已开启视觉' if self._is_caching_image else '视觉已经关闭.' + status = "已开启视觉" if self._is_caching_image else "视觉已经关闭." desc = f"基于OpenCV的视觉感知模块,提供实时图像输入. 当前状态: {status}" return desc @@ -237,7 +236,7 @@ def as_channel(self) -> PyChannel: _channel = PyChannel( name="vision", description="基于OpenCV的视觉感知模块,提供实时图像输入", - block=True # 这是一个非阻塞的感知Channel + block=True, # 这是一个非阻塞的感知Channel ) # 注册上下文消息生成器 @@ -257,8 +256,8 @@ async def vision_status() -> str: "is_running": self._is_running, "is_caching": self._is_caching_image, "has_cached_image": image is not None, - "last_image_time": datetime.fromtimestamp(timestamp).strftime('%H:%M:%S') if timestamp > 0 else "无", - "error": self._last_error or "无" + "last_image_time": datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") if timestamp > 0 else "无", + "error": self._last_error or "无", } return f"视觉模块状态: {status}" @@ -302,8 +301,7 @@ def run_opencv_loop(self) -> None: except KeyboardInterrupt: self._logger.info("收到键盘中断信号") - except Exception as e: - self._logger.error(f"视觉模块运行异常: {e}") + except Exception: + self._logger.exception("视觉模块运行异常") finally: self.close() - diff --git a/src/ghoshell_moss_contrib/example_ws.py b/src/ghoshell_moss_contrib/example_ws.py index 88ab84d..c6b9b99 100644 --- a/src/ghoshell_moss_contrib/example_ws.py +++ b/src/ghoshell_moss_contrib/example_ws.py @@ -1,17 +1,19 @@ +import logging import os -from typing import List -from ghoshell_container import Provider, Container, set_container, get_container +from contextlib import contextmanager +from pathlib import Path + +from ghoshell_common.contracts import LocalWorkspaceProvider, LoggerItf, WorkspaceConfigsProvider +from ghoshell_container import Container, Provider, get_container, set_container -from ghoshell_common.contracts import LocalWorkspaceProvider, LoggerItf, WorkspaceConfigsProvider, Workspace from ghoshell_moss.core import Speech -from pathlib import Path -from contextlib import contextmanager -import logging __all__ = [ - 'get_container', 'set_container', - 'init_container', 'workspace_container', - 'get_example_speech', + "get_container", + "get_example_speech", + "init_container", + "set_container", + "workspace_container", ] @@ -30,13 +32,11 @@ def setup_simple_logger(log_file: str) -> logging.Logger: log_path.parent.mkdir(parents=True, exist_ok=True) # 创建文件handler - file_handler = logging.FileHandler(log_path, encoding='utf-8') + file_handler = logging.FileHandler(log_path, encoding="utf-8") file_handler.setLevel(logging.DEBUG) # 设置格式(包含文件名和行号) - formatter = logging.Formatter( - "%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s" - ) + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s") file_handler.setFormatter(formatter) # 添加到日志器 @@ -46,8 +46,8 @@ def setup_simple_logger(log_file: str) -> logging.Logger: def get_example_speech( - container: Container | None = None, - default_speaker: str | None= None, + container: Container | None = None, + default_speaker: str | None = None, ) -> Speech: """ 直接初始化音频模块. @@ -62,12 +62,12 @@ def get_example_speech( from ghoshell_moss.speech.volcengine_tts import VolcengineTTS, VolcengineTTSConf container = container or get_container() - use_voice = os.environ.get('USE_VOICE_SPEECH', 'no') == 'yes' + use_voice = os.environ.get("USE_VOICE_SPEECH", "no") == "yes" if not use_voice: return MockSpeech() app_key = os.environ.get("VOLCENGINE_STREAM_TTS_APP") app_token = os.environ.get("VOLCENGINE_STREAM_TTS_ACCESS_TOKEN") - resource_id = os.environ.get("VOLCENGINE_STREAM_TTS_RESOURCE_ID", 'seed-tts-2.0') + resource_id = os.environ.get("VOLCENGINE_STREAM_TTS_RESOURCE_ID", "seed-tts-2.0") if not app_key or not app_token: raise NotImplementedError( "Env $VOLCENGINE_STREAM_TTS_APP or $VOLCENGINE_STREAM_TTS_ACCESS_TOKEN not configured." @@ -80,38 +80,37 @@ def get_example_speech( ) if default_speaker: tts_conf.default_speaker = default_speaker - return TTSSpeech( - player=PyAudioStreamPlayer(), - tts=VolcengineTTS(conf=tts_conf), - logger=container.get(LoggerItf) - ) + return TTSSpeech(player=PyAudioStreamPlayer(), tts=VolcengineTTS(conf=tts_conf), logger=container.get(LoggerItf)) def init_container( - workspace_dir: Path | str, - name: str = "moss", - providers: List[Provider] | None = None, - env_path: Path | None = None, + workspace_dir: Path | str, + name: str = "moss", + providers: list[Provider] | None = None, + env_path: Path | None = None, ) -> Container: if isinstance(workspace_dir, str): workspace_dir = Path(workspace_dir).absolute() - env_path = env_path or workspace_dir.parent.joinpath('.env').resolve() + env_path = env_path or workspace_dir.parent.joinpath(".env").resolve() # 加载环境变量, .env 文件默认和 workspace 同层. if env_path.exists(): import dotenv + dotenv.load_dotenv(dotenv_path=env_path, override=True, verbose=True) container = Container(name=name) # 注册 workspace - container.register(LocalWorkspaceProvider( - workspace_dir=str(workspace_dir.absolute()), - )) + container.register( + LocalWorkspaceProvider( + workspace_dir=str(workspace_dir.absolute()), + ) + ) container.register(WorkspaceConfigsProvider()) # 初始化一个简单的日志. logger = setup_simple_logger( - str(workspace_dir.joinpath('runtime/logs/moss_demo.log').absolute()), + str(workspace_dir.joinpath("runtime/logs/moss_demo.log").absolute()), ) container.set(LoggerItf, logger) @@ -124,9 +123,9 @@ def init_container( @contextmanager def workspace_container( - workspace_dir: Path | str, - name: str = "moss", - providers: List[Provider] | None = None, + workspace_dir: Path | str, + name: str = "moss", + providers: list[Provider] | None = None, ): """ 支持 with statement 的全局 container 初始化. diff --git a/src/ghoshell_moss_contrib/gui/image_viewer.py b/src/ghoshell_moss_contrib/gui/image_viewer.py index b4f8a36..d001c77 100644 --- a/src/ghoshell_moss_contrib/gui/image_viewer.py +++ b/src/ghoshell_moss_contrib/gui/image_viewer.py @@ -1,12 +1,13 @@ import sys import threading -from typing import Callable +from collections.abc import Callable + +from PIL import Image +from PyQt6.QtCore import QObject, Qt, pyqtSignal +from PyQt6.QtGui import QImage, QPixmap from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow -from PyQt6.QtGui import QPixmap, QImage -from PyQt6.QtCore import Qt, pyqtSignal, QObject -from PIL import Image, ImageDraw -__all__ = ['SimpleImageViewer', 'run_img_viewer'] +__all__ = ["SimpleImageViewer", "run_img_viewer"] class ImageSignaler(QObject): @@ -33,11 +34,12 @@ def _display_image(self, q_image): def _update_pixmap(self): if self.current_qimage: pixmap = QPixmap.fromImage(self.current_qimage) - scaled = pixmap.scaled(self.size(), Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation) + scaled = pixmap.scaled( + self.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation + ) self.label.setPixmap(scaled) - def resizeEvent(self, event): + def resizeEvent(self, event): # noqa: N802 self._update_pixmap() super().resizeEvent(event)